Merge branch 'worktree-agent-ac5ad16091bc3a40e' into dev-loop
This commit is contained in:
commit
861f591bb0
75
BUILT.md
75
BUILT.md
@ -4802,9 +4802,78 @@ the heap tier, the arena tier and the pool tier; a release build answers 0 to ev
|
|||||||
the two expectations *is* the assertion, and writing it as one program means nobody can change what a dev build does
|
the two expectations *is* the assertion, and writing it as one program means nobody can change what a dev build does
|
||||||
without the release row noticing.
|
without the release row noticing.
|
||||||
|
|
||||||
The inspector's pointer arm is **not** covered. `test/programs/dev-ptr.flan` is the program, its header carries the
|
`test/programs/dev-ptr.flan` covers the inspector's pointer arm, and it is driven now rather than read by hand. Its
|
||||||
two lines above, and they were read off a running session by hand — the `test_dev.ml` case that would drive it belongs
|
header still carries the two lines a session answers with; `test_dev.ml`'s *"a pointer the registry knows about"* case
|
||||||
to another lane's file. `NEXT.md` says so rather than letting the verification read as automated.
|
asserts the live one whole and the dead one **around** its step number, which is the registry's event counter and
|
||||||
|
moves if anything allocates ahead of that program. It also asserts that no address appears in the epitaph — an
|
||||||
|
assertion that would otherwise depend on where the heap landed, which is the point of leaving one out.
|
||||||
|
|
||||||
|
## An address you have in your hand
|
||||||
|
|
||||||
|
The table above is the *recording* side. This is what reads it, and it is three things that turned out to be one
|
||||||
|
thing: pointing at a bare address, a breakdown by type, and what is still held.
|
||||||
|
|
||||||
|
### The recorded name, back to a type
|
||||||
|
|
||||||
|
The table records a **string**, and it has to. The note is built in `check.ml` at the allocation site, where the
|
||||||
|
concrete element type exists, and what crosses into the runtime is bytes — an ABI carrying a type would be an ABI that
|
||||||
|
had to agree with the checker's representation of one, which is the coupling the whole no-header-no-tag-word design
|
||||||
|
refuses.
|
||||||
|
|
||||||
|
What closes it is that the string is not a *description*. It is `Types.to_string` of the type, which is the **source
|
||||||
|
spelling** — `check.ml`'s `reg_note` says so, as the reason the name is worth printing at all — so the round trip is
|
||||||
|
the language's own reader, its own `Parse.texpr`, and the session's own `Check.resolve`. `Enemy` resolves against the
|
||||||
|
structs this session holds; `(Vec i32)` rebuilds through `Tapp`; `[3 i32]` through `Tarray`. **No table of spellings
|
||||||
|
is written down anywhere**, so nothing can fall behind `Types.to_string`.
|
||||||
|
|
||||||
|
And it is allowed to **fail**, which matters more than it looks. Not every recorded name is a type: `flan_rt.c` notes
|
||||||
|
a pool's slot headers as `"pool slots"`, because after a `free-all` an address landing in them must not come back as
|
||||||
|
an element. That string is not Flan source and must not become one, so a name that does not resolve is refused with
|
||||||
|
the name quoted and never defaulted to bytes.
|
||||||
|
|
||||||
|
### The address root renders a pointer, not a pointee
|
||||||
|
|
||||||
|
`(:op "at" :addr N)` — `M-x flan-inspect-address` — builds a `(Ptr T)` at the address and renders **that**. Rendering
|
||||||
|
the `T` directly would read the storage whatever the registry said, which is the hex dump this project is trying not
|
||||||
|
to be. Rendering the pointer puts the walk through `render.ml`'s pointer arm, which is the arm that asks first, so an
|
||||||
|
address root and a slot root reach the same two answers by the same code and permission is asked in exactly one place
|
||||||
|
in the compiler.
|
||||||
|
|
||||||
|
The one piece the thunk cannot do for itself is the address: **Flan has no integer-to-pointer cast**, deliberately, so
|
||||||
|
`flan_dev_reg_addr` is an extern beside `flan_agent_frame_slot` and for the same reason — the compiler knows the type
|
||||||
|
and something outside the language supplies the address. `flan_dev_reg_number` is the other direction, for a *program*
|
||||||
|
that has to say an address out loud; the language still has no operator for either.
|
||||||
|
|
||||||
|
Three refusals, each by name: an address the registry never saw **with no `:type` given** (a stack local, a global, a
|
||||||
|
pointer from C — the first two are answered by name already); an address the block's element size does not divide,
|
||||||
|
because rendering the element type there shows one element's tail as another's head, which is a plausible-looking
|
||||||
|
answer and therefore the worst kind; and a running program, because live-or-dead is exactly what a running program is
|
||||||
|
changing. A named `:type` overrides all of the first two — overriding is the point of being able to say it — and the
|
||||||
|
reply carries `:recorded` whenever the table had a name, so the disagreement is never silent.
|
||||||
|
|
||||||
|
**No `:path`.** A path steps from the pointee, and the pointee is what the registry has only just been asked to bless.
|
||||||
|
The whole answer here is the branch.
|
||||||
|
|
||||||
|
### One walk, two questions, and what "at exit" means
|
||||||
|
|
||||||
|
`flan_dev_reg_by_type` is the group-by, and there is one of it: a leak report **is** a breakdown with the dead left
|
||||||
|
out, and two walks would drift. Formatting is in the agent and ordering is in the daemon — biggest first, by bytes,
|
||||||
|
because a breakdown in table order is a list of everything and answers nothing.
|
||||||
|
|
||||||
|
**"At exit" is not a hook, and the honest reason is that a game is killed.** A program stopped by a signal runs no
|
||||||
|
`atexit` handler, no destructor, nothing — so no code written inside the program could report anything about the run
|
||||||
|
that matters most. The authoritative reader is therefore `(:op "leaks")`, which reads the same table over the agent
|
||||||
|
socket and can be asked at any moment, including the one before the kill. `flan-dev.el` does not ask on teardown
|
||||||
|
either: that would put a request on a path that runs every time the editor closes, for an answer nobody asked for.
|
||||||
|
|
||||||
|
The hook exists for the other program — the one that returns from `main` — and it is two decisions:
|
||||||
|
|
||||||
|
- **Registered by `atexit` from inside `flan_dev_reg_enable`**, not a file-scope `__attribute__((destructor))`. This
|
||||||
|
file is compiled into *every* build, so a destructor would run in a release build too, and that is exactly the third
|
||||||
|
place a release build is not free. Registered where it is, a release binary still carries a null pointer, a zero
|
||||||
|
flag and the declarations.
|
||||||
|
- **Off unless `FLAN_DEV_LEAKS` is set.** The acceptance table reads `programs/registry.flan`'s output with stderr
|
||||||
|
folded in, so a report nobody asked for is a report that changes what a dev build prints.
|
||||||
## A restart is not a transaction
|
## A restart is not a transaction
|
||||||
|
|
||||||
Written down in three places rather than fixed, because it is a property and not a defect. If a frame mutates a global
|
Written down in three places rather than fixed, because it is a property and not a defect. If a frame mutates a global
|
||||||
|
|||||||
43
NEXT.md
43
NEXT.md
@ -488,46 +488,31 @@ re-applied.
|
|||||||
This matters more here than in most Lisps because the intended use is a **game loop**, where the author's plan is to
|
This matters more here than in most Lisps because the intended use is a **game loop**, where the author's plan is to
|
||||||
skip a frame and carry on rather than die — exactly the case where a non-idempotent mutation bites.
|
skip a frame and carry on rather than die — exactly the case where a non-idempotent mutation bites.
|
||||||
|
|
||||||
## ~~Queued: a dev-build allocation registry~~ — **landed, in part; three of the six remain**
|
## ~~Queued: a dev-build allocation registry~~ — **landed; one item left, and it is not a registry item**
|
||||||
## Queued: a dev-build allocation registry — address to type
|
|
||||||
|
|
||||||
Built. The table is in `runtime/flan_dev.c`, the note is emitted by `check.ml` and dropped by `emit.ml` in a release
|
Items 1 to 5 are built and the test is written. `BUILT.md`'s *"An address answers with a type"* is the account of the
|
||||||
build, and the inspector reads it. `BUILT.md`'s *"An address answers with a type"* is the account of it; what follows
|
table, the note and the inspector's pointer arm; *"An address you have in your hand"* is the account of the reader —
|
||||||
is only what is **not** there, so that the gap is a queue entry rather than a discovery.
|
the address root, the breakdown, the leak report, and what "at exit" turned out to mean.
|
||||||
|
|
||||||
**Landed:** items 1 and 2 — the inspector follows a live `(Ptr T)` and renders the pointee, and names what died at a
|
**Left:** **the memcheck half of item 6, and nothing else.** The registry knows an arena's `free-all` killed
|
||||||
dead one (`<ptr dead: was Enemy, freed at step 15>`). The dead-marking covers the heap `free`, a heap resize's old
|
everything in the region, so a later read through a pointer into it is *answerable*. Memcheck still says nothing,
|
||||||
block, an arena `free-all` and `arena-destroy`.
|
because nothing told it: the pages stay mapped and `free-all` is an integer going to zero inside one allocation.
|
||||||
|
Closing that is `VALGRIND_MAKE_MEM_UNDEFINED` in `flan_arena_proc`, which `test/test_valgrind.ml` already names.
|
||||||
**Left, and in this order:**
|
**The two must not be blurred** — the registry answer and the memcheck answer are different tools reaching different
|
||||||
|
people, and building one is not progress on the other.
|
||||||
- **Item 3, point at any heap address.** The lookup is there and answers for any address; nothing exposes it as an
|
|
||||||
editor op. It wants a verb beside `inspect` that takes an address and a type rather than a frame and a slot, and the
|
|
||||||
registry's own answer for the type when none is given — which needs the recorded name resolved back to a
|
|
||||||
`Types.t`, and the table records a string.
|
|
||||||
- **Item 4, a breakdown by type**, and **item 5, leak attribution at exit.** Both are a walk over the table and a
|
|
||||||
group-by; `flan_dev_reg_count` is the whole of what exists. Neither is hard and neither has a reader yet, which is
|
|
||||||
why they were left rather than half-built.
|
|
||||||
- **The memcheck half of item 6.** The registry now knows an arena's `free-all` killed everything in the region, so a
|
|
||||||
later read through a pointer into it is *answerable*. Memcheck still says nothing, because nothing told it: the
|
|
||||||
pages stay mapped and `free-all` is an integer going to zero inside one allocation. Closing that is
|
|
||||||
`VALGRIND_MAKE_MEM_UNDEFINED` in `flan_arena_proc`, which `test/test_valgrind.ml` already names. **The two must not
|
|
||||||
be blurred** — the registry answer and the memcheck answer are different tools reaching different people.
|
|
||||||
- **A test that drives the inspector's pointer arm.** `test/programs/dev-ptr.flan` is the program and its header has
|
|
||||||
the two lines a session answers with; they were read off a running session **by hand**. The case belongs beside the
|
|
||||||
other `locals`/`inspect` cases in `test_dev.ml`, which was another lane's file. `programs/registry.flan` covers the
|
|
||||||
table itself from the acceptance table, in a dev build and a release one.
|
|
||||||
|
|
||||||
**What it does not cover, and does not need to:** stack locals and globals, which the shadow stack and the static type
|
**What it does not cover, and does not need to:** stack locals and globals, which the shadow stack and the static type
|
||||||
table already answer by name. A stack address is deliberately not in the table, and a pointer to one still renders
|
table already answer by name. A stack address is deliberately not in the table, and a pointer to one still renders
|
||||||
`<ptr>`.
|
`<ptr>` — the address root refuses such an address by name rather than rendering bytes at it.
|
||||||
|
|
||||||
**Note on classes:** `defclass` instances will carry shape metadata by design, so they get identification for free and
|
**Note on classes:** `defclass` instances will carry shape metadata by design, so they get identification for free and
|
||||||
do not need the registry. This is for plain structs, `Vec`, `Map` and pool storage.
|
do not need the registry. This is for plain structs, `Vec`, `Map` and pool storage.
|
||||||
|
|
||||||
**On cost, as built.** One insert per allocation, always on in a dev build, no opt-out — the author's instruction,
|
**On cost, as built.** One insert per allocation, always on in a dev build, no opt-out — the author's instruction,
|
||||||
followed literally. Nothing was built per-region, no range recording, no per-allocator opt-out. Revisit only if a real
|
followed literally. Nothing was built per-region, no range recording, no per-allocator opt-out. Revisit only if a real
|
||||||
program shows a problem, and `BUILT.md` names the two places a release build is not quite free.
|
program shows a problem, and `BUILT.md` still names **two** places a release build is not quite free: the readers
|
||||||
|
added since are functions nothing in a release build calls, and the exit report is registered by `atexit` from inside
|
||||||
|
`flan_dev_reg_enable` rather than by a file-scope destructor, precisely so that it is not a third.
|
||||||
|
|
||||||
## Picked up first, 2026-09-13
|
## Picked up first, 2026-09-13
|
||||||
|
|
||||||
|
|||||||
@ -230,9 +230,9 @@ is harmless, but the expression itself need not be: if you inspect
|
|||||||
`(spawn-enemy)`, you spawn one per keystroke. That is why there is no
|
`(spawn-enemy)`, you spawn one per keystroke. That is why there is no
|
||||||
auto-refresh and why `g` is a key you press rather than a timer.
|
auto-refresh and why `g` is a key you press rather than a timer.
|
||||||
|
|
||||||
### Two ways to root a walk
|
### Three ways to root a walk
|
||||||
|
|
||||||
There are two, they are not equally capable, and the top line of the buffer
|
There are three, they are not equally capable, and the top line of the buffer
|
||||||
says which one you are on.
|
says which one you are on.
|
||||||
|
|
||||||
**An expression** — `C-c C-i`, and `i` on a global line in the break buffer.
|
**An expression** — `C-c C-i`, and `i` on a global line in the break buffer.
|
||||||
@ -250,8 +250,33 @@ 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.
|
||||||
|
|
||||||
Neither subsumes the other, which is why both are here. The one you get is
|
**An address** — `M-x flan-inspect-address`. A number, `#x7f…` or decimal, of
|
||||||
chosen for you by the line you press `i` on.
|
the kind a debugger, a valgrind report or a C shim's `printf` hands you. There
|
||||||
|
is no frame in it and no expression: what it shows is the `(Ptr T)` at that
|
||||||
|
address, followed if the storage is still live and an epitaph naming what died
|
||||||
|
there if it is not.
|
||||||
|
|
||||||
|
**The type is optional, and leaving it out is the point.** A dev build records
|
||||||
|
the type at every allocation — the allocator's caller knew it, and a Flan value
|
||||||
|
carries no header, so that is the only moment anything could — and this command
|
||||||
|
is the one place that recorded name is read back and turned into a type again.
|
||||||
|
Naming a type overrides it, for reading half a struct or an element the table
|
||||||
|
recorded under a container's name; the reply still carries what the allocator
|
||||||
|
wrote down, so you are never shown one type while the program believes another.
|
||||||
|
|
||||||
|
It needs a **stopped** program, for a reason of its own: whether an address is
|
||||||
|
still live is exactly what a running program is changing. And it takes **no
|
||||||
|
path** — `RET` does not go into it — because the whole answer is the pointer
|
||||||
|
arm's branch, and stepping in would step from the pointee, which is the deref
|
||||||
|
the registry has only just been asked to bless.
|
||||||
|
|
||||||
|
An address the registry has never seen is refused by name rather than
|
||||||
|
rendered. That is a stack local, a global, or a pointer from C; the first two
|
||||||
|
are answered by name in the stack and globals sections already.
|
||||||
|
|
||||||
|
None of the three subsumes the others, which is why all three are here. The
|
||||||
|
one you get is chosen for you by the line you press `i` on, or by starting an
|
||||||
|
address root by hand.
|
||||||
|
|
||||||
**`l` never crosses between them**, and that is structural rather than a rule
|
**`l` never crosses between them**, and that is structural rather than a rule
|
||||||
someone has to remember. Every entry on the buffer's stack carries its own
|
someone has to remember. Every entry on the buffer's stack carries its own
|
||||||
@ -259,6 +284,28 @@ root; `RET` only ever lengthens the path under the root already in hand; and
|
|||||||
starting a new root starts an empty stack. So a stack with both kinds in it
|
starting a new root starts an empty stack. So a stack with both kinds in it
|
||||||
cannot be built, and `l` has nothing to cross into.
|
cannot be built, and `l` has nothing to cross into.
|
||||||
|
|
||||||
|
### Where the memory went — `M-x flan-allocations` and `M-x flan-leaks`
|
||||||
|
|
||||||
|
The same registry, read as a table rather than at one address. **`M-x
|
||||||
|
flan-allocations`** is every block it recorded, live and dead both, grouped by
|
||||||
|
the type the allocator's caller named and ordered biggest first by bytes. The
|
||||||
|
dead are in it on purpose: in a long-running program they are the bulk of it,
|
||||||
|
and they are what says where the allocation *went* rather than only where it
|
||||||
|
stayed.
|
||||||
|
|
||||||
|
**`M-x flan-leaks`** is the same walk with the dead left out — what the program
|
||||||
|
is still holding.
|
||||||
|
|
||||||
|
**"Still holding" means at the moment you ask, and there is no exit report to
|
||||||
|
wait for.** A program killed by a signal, which is how a program under this
|
||||||
|
editor usually ends, runs no exit handler at all, so nothing written inside it
|
||||||
|
could report anything. Asking is the answer, and you can ask at any time
|
||||||
|
including just before you quit. A program that returns from `main` on its own
|
||||||
|
can print the same breakdown to stderr by being run with `FLAN_DEV_LEAKS` set
|
||||||
|
— off by default, because a dev build's output belongs to the program.
|
||||||
|
|
||||||
|
Both are dev-build only. A release build records nothing and says so.
|
||||||
|
|
||||||
### The watch buffer — values while the program runs
|
### The watch buffer — values while the program runs
|
||||||
|
|
||||||
Everything above is for a program you have stopped, or one you interrupt with a
|
Everything above is for a program you have stopped, or one you interrupt with a
|
||||||
@ -535,8 +582,10 @@ Use `C-c C-g` if you need frames.
|
|||||||
| `M-.` / `M-,` | where a name is written / back |
|
| `M-.` / `M-,` | where a name is written / back |
|
||||||
|
|
||||||
Commands with no key: `M-x flan-dev` (start a program), `M-x flan-dev-quit`
|
Commands with no key: `M-x flan-dev` (start a program), `M-x flan-dev-quit`
|
||||||
(stop it), `M-x flan-watch` (the watch buffer), `M-x flan-watch-stop`, and
|
(stop it), `M-x flan-watch` (the watch buffer), `M-x flan-watch-stop`,
|
||||||
`M-x flan-watch-ghost-mode` (the same values inline).
|
`M-x flan-watch-ghost-mode` (the same values inline),
|
||||||
|
`M-x flan-inspect-address` (what is at an address), and `M-x flan-allocations`
|
||||||
|
/ `M-x flan-leaks` (where the memory went, and what is still held).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1657,5 +1657,80 @@ that the IR half is findable by name rather than only by a modifier."
|
|||||||
nil t))))
|
nil t))))
|
||||||
(flan-disassemble name t))
|
(flan-disassemble name t))
|
||||||
|
|
||||||
|
;;; What the program is made of, and what it is still holding
|
||||||
|
|
||||||
|
;; Two readings of one table. A dev build records the type at every
|
||||||
|
;; allocation — the allocator's caller knew it, and a Flan value carries no
|
||||||
|
;; header, so that is the only moment anything could — and this is that table
|
||||||
|
;; grouped by the name it wrote down.
|
||||||
|
;;
|
||||||
|
;; Biggest first, by bytes. Read in table order it is a list of everything
|
||||||
|
;; and answers nothing; read biggest-first it answers "where did the memory
|
||||||
|
;; go", which is the only reason either command exists.
|
||||||
|
|
||||||
|
(defcustom flan-allocations-buffer "*flan-allocations*"
|
||||||
|
"Where the allocation breakdown and the leak report are shown."
|
||||||
|
:type 'string)
|
||||||
|
|
||||||
|
(defun flan-allocations--show (op title)
|
||||||
|
"Ask the daemon for OP and show its rows under TITLE."
|
||||||
|
(let ((r (flan-dev--request (list :op op))))
|
||||||
|
(unless (equal (plist-get r :status) "ok")
|
||||||
|
(user-error "flan: %s" (or (plist-get r :message) "refused")))
|
||||||
|
(let ((rows (plist-get r :types))
|
||||||
|
(blocks (plist-get r :blocks))
|
||||||
|
(bytes (plist-get r :bytes)))
|
||||||
|
(with-current-buffer (get-buffer-create flan-allocations-buffer)
|
||||||
|
(let ((inhibit-read-only t))
|
||||||
|
(erase-buffer)
|
||||||
|
(special-mode)
|
||||||
|
(insert (propertize (format "%s\n" title) 'face 'bold))
|
||||||
|
(insert (propertize (format "%s\n\n" (or (plist-get r :note) ""))
|
||||||
|
'face 'font-lock-comment-face))
|
||||||
|
;; An overflowed table has blocks in the program that are in
|
||||||
|
;; nobody's row, so every number below it is a floor. Said before
|
||||||
|
;; the numbers, not after them, because a reader who missed it would
|
||||||
|
;; quote them as counts.
|
||||||
|
(when (plist-get r :overflow)
|
||||||
|
(insert (propertize
|
||||||
|
"the registry overflowed: these are floors, not counts\n\n"
|
||||||
|
'face 'warning)))
|
||||||
|
(if (null rows)
|
||||||
|
(insert "nothing recorded\n")
|
||||||
|
(insert (propertize (format "%8s %12s %s\n" "blocks" "bytes" "type")
|
||||||
|
'face 'shadow))
|
||||||
|
(dolist (row rows)
|
||||||
|
(insert (format "%8d %12d %s\n" (nth 1 row) (nth 2 row)
|
||||||
|
(nth 0 row))))
|
||||||
|
(insert (propertize
|
||||||
|
(format "\n%8s %12s in %d type%s\n" (or blocks 0)
|
||||||
|
(or bytes 0) (length rows)
|
||||||
|
(if (= (length rows) 1) "" "s"))
|
||||||
|
'face 'shadow))))
|
||||||
|
(goto-char (point-min)))
|
||||||
|
(display-buffer flan-allocations-buffer))))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun flan-allocations ()
|
||||||
|
"Every block the allocation registry recorded, grouped by type.
|
||||||
|
Live and dead both: in a long-running program the dead are the bulk of it, and
|
||||||
|
they are what says where the allocation went rather than only where it
|
||||||
|
stayed. A dev build only — a release build records nothing, and says so."
|
||||||
|
(interactive)
|
||||||
|
(flan-allocations--show "allocations" "Allocations, by type"))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun flan-leaks ()
|
||||||
|
"What the allocation registry is still holding live, grouped by type.
|
||||||
|
|
||||||
|
The same walk with the dead left out, and \"still holding\" means at the moment
|
||||||
|
you ask. There is no exit report to wait for: a program killed by a signal —
|
||||||
|
which is how a program under this editor usually ends — runs no handler at
|
||||||
|
all, so this command, asked whenever you like and including just before you
|
||||||
|
quit, is what answers for one. A program that returns from main on its own
|
||||||
|
can print the same breakdown to stderr under FLAN_DEV_LEAKS."
|
||||||
|
(interactive)
|
||||||
|
(flan-allocations--show "leaks" "Still held, by type"))
|
||||||
|
|
||||||
(provide 'flan-dev)
|
(provide 'flan-dev)
|
||||||
;;; flan-dev.el ends here
|
;;; flan-dev.el ends here
|
||||||
|
|||||||
@ -297,10 +297,19 @@ whatever the value came from."
|
|||||||
|
|
||||||
;;; A root, and the path walked from it
|
;;; A root, and the path walked from it
|
||||||
|
|
||||||
;; A root is `(:expr EXPR)' or `(:slot FRAME SLOT NAME)'. The path is a list
|
;; A root is `(:expr EXPR)', `(:slot FRAME SLOT NAME)' or `(:addr N TYPE)'.
|
||||||
;; of steps applied to it in order, and the pair is the whole of this buffer's
|
;; The path is a list of steps applied to it in order, and the pair is the
|
||||||
;; position — which is why a stack entry carries both and `l' cannot cross
|
;; whole of this buffer's position — which is why a stack entry carries both
|
||||||
;; between two kinds of root by accident.
|
;; and `l' cannot cross between two kinds of root by accident.
|
||||||
|
;;
|
||||||
|
;; The third one has no frame in it. An expression root is evaluated wherever
|
||||||
|
;; the evaluator stands and a slot root is a frame and an index; an address
|
||||||
|
;; root is a number somebody has in their hand, out of a debugger or a
|
||||||
|
;; valgrind report, and the daemon asks the allocation registry what is there.
|
||||||
|
;; It takes no path: what it renders is a `(Ptr T)', so the whole answer is
|
||||||
|
;; the pointer arm's branch — followed if the storage is live, an epitaph if
|
||||||
|
;; it is not — and stepping in would be stepping from the pointee, which is
|
||||||
|
;; the deref the registry has only just been asked to bless.
|
||||||
|
|
||||||
(defun flan-inspect--root-label (root path)
|
(defun flan-inspect--root-label (root path)
|
||||||
"How ROOT walked by PATH is named at the top of the buffer and in the trail."
|
"How ROOT walked by PATH is named at the top of the buffer and in the trail."
|
||||||
@ -316,6 +325,11 @@ whatever the value came from."
|
|||||||
(`(:some) ".some")
|
(`(:some) ".some")
|
||||||
(_ "")))
|
(_ "")))
|
||||||
path "")))
|
path "")))
|
||||||
|
;; The address in hex, because that is the spelling every tool that hands
|
||||||
|
;; one out uses, and the type only when it was *named*: an unnamed one is
|
||||||
|
;; the registry's own answer and the reply's own `:type' line says it.
|
||||||
|
(`(:addr ,addr ,ty)
|
||||||
|
(if ty (format "#x%x as %s" addr ty) (format "#x%x" addr)))
|
||||||
(_ "?")))
|
(_ "?")))
|
||||||
|
|
||||||
;;; Why a thing cannot be entered
|
;;; Why a thing cannot be entered
|
||||||
@ -353,8 +367,16 @@ root, which is the older and the more limited of the two."
|
|||||||
('trunc
|
('trunc
|
||||||
"truncated: the walk stopped at its depth bound of 4. Inspect the field that holds it, which re-roots the walk")
|
"truncated: the walk stopped at its depth bound of 4. Inspect the field that holds it, which re-roots the walk")
|
||||||
('opaque
|
('opaque
|
||||||
|
;; A *followed* pointer lands here rather than on the `ptr' arm above,
|
||||||
|
;; because that arm matches the bare word. Saying "no structure for this
|
||||||
|
;; type" of it would be false and unhelpful at once: it has structure, it
|
||||||
|
;; is right there, and the reason you cannot step in is that the step
|
||||||
|
;; would start from the pointee — the deref the registry blessed once and
|
||||||
|
;; is not being asked about again.
|
||||||
|
(if (string-prefix-p "<ptr " (or (plist-get node :text) ""))
|
||||||
|
"a pointer the registry let the renderer follow: the pointee is already drawn, one level deeper. Stepping in would step from it, which is a second deref nobody has asked about — root at it with `M-x flan-inspect-address' instead"
|
||||||
(format "%s: the walk had no structure for this type, so there are no fields to show"
|
(format "%s: the walk had no structure for this type, so there are no fields to show"
|
||||||
(plist-get node :text)))
|
(plist-get node :text))))
|
||||||
('atom (format "%s is an atom; it has no fields" (plist-get node :text)))
|
('atom (format "%s is an atom; it has no fields" (plist-get node :text)))
|
||||||
(_ "not something this inspector knows how to enter")))
|
(_ "not something this inspector knows how to enter")))
|
||||||
|
|
||||||
@ -362,7 +384,7 @@ root, which is the older and the more limited of the two."
|
|||||||
|
|
||||||
(defvar-local flan-inspect--root nil
|
(defvar-local flan-inspect--root nil
|
||||||
"What this buffer's walk starts from.
|
"What this buffer's walk starts from.
|
||||||
Either `(:expr EXPR)\=' or `(:slot FRAME SLOT NAME)\='.")
|
One of `(:expr EXPR)\=', `(:slot FRAME SLOT NAME)\=' or `(:addr N TYPE)\='.")
|
||||||
(defvar-local flan-inspect--path nil
|
(defvar-local flan-inspect--path nil
|
||||||
"The steps walked from `flan-inspect--root\=', outermost first.
|
"The steps walked from `flan-inspect--root\=', outermost first.
|
||||||
Together with the root this is the whole of where the buffer is. It is a
|
Together with the root this is the whole of where the buffer is. It is a
|
||||||
@ -561,6 +583,17 @@ root exists to fix."
|
|||||||
(when path
|
(when path
|
||||||
(list :path
|
(list :path
|
||||||
(mapcar #'flan-inspect-wire-step path))))))
|
(mapcar #'flan-inspect-wire-step path))))))
|
||||||
|
(`(:addr ,addr ,ty)
|
||||||
|
(when path
|
||||||
|
;; Refused rather than dropped. A path with a step silently
|
||||||
|
;; gone would render a *different* value and say nothing,
|
||||||
|
;; which is the failure this whole buffer is built to avoid.
|
||||||
|
(user-error
|
||||||
|
"flan: an address root has no path; it renders the pointer, \
|
||||||
|
and whether that may be followed is the answer"))
|
||||||
|
(funcall flan-inspect-request-function
|
||||||
|
(append (list :op "at" :addr addr)
|
||||||
|
(when ty (list :type ty)))))
|
||||||
(_ (user-error "flan: %S is not a root this inspector knows" root)))))
|
(_ (user-error "flan: %S is not a root this inspector knows" root)))))
|
||||||
(unless (equal (plist-get r :status) "ok")
|
(unless (equal (plist-get r :status) "ok")
|
||||||
(user-error "flan: %s" (or (plist-get r :message) "refused")))
|
(user-error "flan: %s" (or (plist-get r :message) "refused")))
|
||||||
@ -613,6 +646,36 @@ the listing picks one out. The index is what `locals\=' puts on every line for
|
|||||||
this."
|
this."
|
||||||
(flan-inspect--show (list :slot frame slot name) nil nil))
|
(flan-inspect--show (list :slot frame slot name) nil nil))
|
||||||
|
|
||||||
|
(defvar flan-inspect-address-history nil
|
||||||
|
"Addresses `flan-inspect-address\=' has been given, most recent first.")
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun flan-inspect-address (addr &optional type)
|
||||||
|
"Show what is at ADDR in the stopped program, as TYPE.
|
||||||
|
|
||||||
|
The address root. ADDR is a number — `#x7f…\=', or decimal — of the kind a
|
||||||
|
debugger, a valgrind report or a C shim\='s printf hands you, and which nothing
|
||||||
|
inside Flan will make for you: there is no integer-to-pointer cast in the
|
||||||
|
language, deliberately.
|
||||||
|
|
||||||
|
TYPE is optional, and leaving it out is the interesting half. A dev build
|
||||||
|
records the type at every allocation, so the registry already knows what was
|
||||||
|
put there and the daemon resolves that recorded name back to a type. Naming
|
||||||
|
one instead overrides it — for reading half a struct, or an element the table
|
||||||
|
recorded under a container\='s name — and the reply still carries what the
|
||||||
|
allocator wrote down, so the disagreement is never silent.
|
||||||
|
|
||||||
|
Refused while the program is running: whether an address is still live is
|
||||||
|
exactly what a running program is changing."
|
||||||
|
(interactive
|
||||||
|
(list (read-number "Address: "
|
||||||
|
(car (mapcar #'string-to-number
|
||||||
|
flan-inspect-address-history)))
|
||||||
|
(let ((s (read-string "As type (empty for what was recorded): ")))
|
||||||
|
(and (not (string-empty-p s)) s))))
|
||||||
|
(add-to-history 'flan-inspect-address-history (number-to-string addr))
|
||||||
|
(flan-inspect--show (list :addr addr type) nil nil))
|
||||||
|
|
||||||
(defun flan-inspect-into ()
|
(defun flan-inspect-into ()
|
||||||
"Go into the field or element at point.
|
"Go into the field or element at point.
|
||||||
Extends the path under the root this buffer already has; it never replaces the
|
Extends the path under the root this buffer already has; it never replaces the
|
||||||
|
|||||||
@ -208,6 +208,122 @@
|
|||||||
(string-match-p "\\.name +\"all\"\n" text))))
|
(string-match-p "\\.name +\"all\"\n" text))))
|
||||||
|
|
||||||
|
|
||||||
|
;;; The address root, and the registry listings
|
||||||
|
|
||||||
|
(message "\nthe address root")
|
||||||
|
|
||||||
|
;; The rooting mode with no frame in it. What is asserted here is the *wire*:
|
||||||
|
;; the buffer sends `at' with the address, sends `:type' only when one was
|
||||||
|
;; named, and refuses a path rather than dropping it. What the daemon does
|
||||||
|
;; with that is test_dev.ml's business, over a real program.
|
||||||
|
|
||||||
|
(let* ((sent nil)
|
||||||
|
(flan-inspect-request-function
|
||||||
|
(lambda (form) (setq sent form) '(:status "ok" :value "<ptr (Enemy {.hp 41 .x 2})>"
|
||||||
|
:type "(Ptr Enemy)" :live t :recorded "Enemy")))
|
||||||
|
(flan-inspect-buffer " *test-inspect*"))
|
||||||
|
(when (get-buffer " *test-inspect*") (kill-buffer " *test-inspect*"))
|
||||||
|
(let ((text (with-current-buffer
|
||||||
|
(save-window-excursion
|
||||||
|
(flan-inspect--show (list :addr 4096 nil) nil))
|
||||||
|
(buffer-string))))
|
||||||
|
(test-flan--check "an address root asks the daemon for `at'"
|
||||||
|
(equal (plist-get sent :op) "at"))
|
||||||
|
(test-flan--check "with the address"
|
||||||
|
(equal (plist-get sent :addr) 4096))
|
||||||
|
;; Absent and not empty: leaving it out is what makes the registry answer
|
||||||
|
;; for the type, which is the whole of what the address root buys.
|
||||||
|
(test-flan--check "and no :type when none was named"
|
||||||
|
(null (plist-get sent :type)))
|
||||||
|
(test-flan--check "the address is named in hex at the top"
|
||||||
|
(string-match-p "#x1000" text))))
|
||||||
|
|
||||||
|
(let* ((sent nil)
|
||||||
|
(flan-inspect-request-function
|
||||||
|
(lambda (form) (setq sent form) '(:status "ok" :value "<ptr 41>" :type "(Ptr i32)")))
|
||||||
|
(flan-inspect-buffer " *test-inspect*"))
|
||||||
|
(when (get-buffer " *test-inspect*") (kill-buffer " *test-inspect*"))
|
||||||
|
(let ((text (with-current-buffer
|
||||||
|
(save-window-excursion
|
||||||
|
(flan-inspect--show (list :addr 4096 "i32") nil))
|
||||||
|
(buffer-string))))
|
||||||
|
(test-flan--check "a named type is sent as :type"
|
||||||
|
(equal (plist-get sent :type) "i32"))
|
||||||
|
(test-flan--check "and shown beside the address"
|
||||||
|
(string-match-p "#x1000 as i32" text))))
|
||||||
|
|
||||||
|
;; A path is refused rather than dropped. A path with a step silently gone
|
||||||
|
;; would render a different value and say nothing, which is the failure the
|
||||||
|
;; whole buffer is built to avoid.
|
||||||
|
(let ((flan-inspect-request-function
|
||||||
|
(lambda (_) '(:status "ok" :value "<ptr 41>"))))
|
||||||
|
(test-flan--check "an address root refuses a path"
|
||||||
|
(eq 'caught
|
||||||
|
(condition-case nil
|
||||||
|
(flan-inspect--value (list :addr 4096 nil) '((:field "x")))
|
||||||
|
(user-error 'caught)))))
|
||||||
|
|
||||||
|
;; And a followed pointer is refused for the true reason rather than for "no
|
||||||
|
;; structure": it has structure, it is drawn, and the step would be a second
|
||||||
|
;; deref nobody asked about.
|
||||||
|
(test-flan--check "a followed pointer says why it cannot be entered"
|
||||||
|
(string-match-p
|
||||||
|
"second deref"
|
||||||
|
(flan-inspect-refusal
|
||||||
|
(flan-inspect-parse "<ptr (Enemy {.hp 41 .x 2})>"))))
|
||||||
|
|
||||||
|
(message "\nthe registry listings")
|
||||||
|
|
||||||
|
(let* ((sent nil)
|
||||||
|
(flan-dev--request-stub
|
||||||
|
(lambda (form)
|
||||||
|
(setq sent form)
|
||||||
|
'(:status "ok"
|
||||||
|
:types (("Enemy" 2 64) ("i32" 1 16))
|
||||||
|
:blocks 3 :bytes 80 :overflow nil
|
||||||
|
:note "every block the registry recorded"))))
|
||||||
|
(cl-letf (((symbol-function 'flan-dev--request) flan-dev--request-stub)
|
||||||
|
((symbol-function 'display-buffer) #'ignore))
|
||||||
|
(let ((flan-allocations-buffer " *test-allocations*"))
|
||||||
|
(flan-allocations)
|
||||||
|
(test-flan--check "the breakdown asks for `allocations'"
|
||||||
|
(equal (plist-get sent :op) "allocations"))
|
||||||
|
(let ((text (with-current-buffer " *test-allocations*" (buffer-string))))
|
||||||
|
(test-flan--check "and lists each type with its blocks and bytes"
|
||||||
|
(string-match-p "2 +64 +Enemy" text))
|
||||||
|
(test-flan--check "with a total under it"
|
||||||
|
(string-match-p "3 +80 +in 2 types" text)))
|
||||||
|
(flan-leaks)
|
||||||
|
(test-flan--check "the leak report asks for `leaks'"
|
||||||
|
(equal (plist-get sent :op) "leaks"))
|
||||||
|
(kill-buffer " *test-allocations*"))))
|
||||||
|
|
||||||
|
;; An overflowed table has blocks in the program that are in nobody's row, so
|
||||||
|
;; every number under it is a floor. Said before the numbers, because a reader
|
||||||
|
;; who missed it would quote them as counts.
|
||||||
|
(cl-letf (((symbol-function 'flan-dev--request)
|
||||||
|
(lambda (_) '(:status "ok" :types (("Enemy" 1 32)) :blocks 1 :bytes 32
|
||||||
|
:overflow t)))
|
||||||
|
((symbol-function 'display-buffer) #'ignore))
|
||||||
|
(let ((flan-allocations-buffer " *test-allocations*"))
|
||||||
|
(flan-allocations)
|
||||||
|
(let ((text (with-current-buffer " *test-allocations*" (buffer-string))))
|
||||||
|
(test-flan--check "an overflowed table is said to be a floor"
|
||||||
|
(string-match-p "floors, not counts" text)))
|
||||||
|
(kill-buffer " *test-allocations*")))
|
||||||
|
|
||||||
|
;; And a release build, which records nothing and says so. The refusal comes
|
||||||
|
;; back as an ordinary error status and reaches the person, rather than an
|
||||||
|
;; empty listing that reads like a program holding nothing.
|
||||||
|
(cl-letf (((symbol-function 'flan-dev--request)
|
||||||
|
(lambda (_) '(:status "error"
|
||||||
|
:message "the allocation registry is off; this is not a dev build")))
|
||||||
|
((symbol-function 'display-buffer) #'ignore))
|
||||||
|
(test-flan--check "a build with no registry refuses rather than showing nothing"
|
||||||
|
(eq 'caught
|
||||||
|
(condition-case nil (flan-leaks) (user-error 'caught)))))
|
||||||
|
|
||||||
|
|
||||||
;;; The inspector buffer
|
;;; The inspector buffer
|
||||||
|
|
||||||
(message "\nthe inspector buffer")
|
(message "\nthe inspector buffer")
|
||||||
|
|||||||
402
lib/dev.ml
402
lib/dev.ml
@ -1032,6 +1032,372 @@ let inspect t ~frame ~slot ~path =
|
|||||||
":type " ^ Wire.quote ty; ":value " ^ Wire.quote v ]))
|
":type " ^ Wire.quote ty; ":value " ^ Wire.quote v ]))
|
||||||
|
|
||||||
|
|
||||||
|
(* ── The allocation registry, read from this end ───────────────────── *)
|
||||||
|
|
||||||
|
(* [BUILT.md]'s "An address answers with a type" is what the table is and why.
|
||||||
|
What follows is the reader: three verbs that ask the agent for what is
|
||||||
|
recorded, and one of them turns a recorded *name* back into a type.
|
||||||
|
|
||||||
|
Nothing here is emitted and nothing here is in a release build. A release
|
||||||
|
binary's table is a null pointer, so every one of these comes back with the
|
||||||
|
agent saying the registry is off — which is an answer, and is the same
|
||||||
|
answer [programs/registry.flan]'s release row asserts. *)
|
||||||
|
|
||||||
|
type reg_entry =
|
||||||
|
{ rlive : bool;
|
||||||
|
roff : int; (* how far into the block the address lands *)
|
||||||
|
rbytes : int; (* the block's extent *)
|
||||||
|
relem : int; (* one element, or 0 where the block is not an array *)
|
||||||
|
rseq : int; (* when it was recorded *)
|
||||||
|
rdied : int; (* when it was released, or 0 while it is live *)
|
||||||
|
rtype : string } (* the Flan spelling the compiler wrote beside the call *)
|
||||||
|
|
||||||
|
(* [reg at ADDR] answers one of three things and they are three different
|
||||||
|
facts: a row, "never heard of it", or "there is no table". Kept apart here
|
||||||
|
rather than collapsed into an option, because an address the registry never
|
||||||
|
saw is a stack local or a pointer from C — a perfectly good address with no
|
||||||
|
entry — and a release build is a build that records nothing about any
|
||||||
|
address at all. A caller that could not tell them apart would report the
|
||||||
|
second as the first. *)
|
||||||
|
let reg_at t ~addr : (reg_entry option, string) result =
|
||||||
|
match request t (Printf.sprintf "reg at %d" addr) with
|
||||||
|
| exception Unix.Unix_error (e, _, _) ->
|
||||||
|
Error ("cannot reach the program: " ^ Unix.error_message e)
|
||||||
|
| text ->
|
||||||
|
let line = String.trim (List.hd (String.split_on_char '\n' text)) in
|
||||||
|
if line = "none" then Ok None
|
||||||
|
else if String.length line > 4 && String.sub line 0 4 = "err " then
|
||||||
|
Error (String.sub line 4 (String.length line - 4))
|
||||||
|
else
|
||||||
|
(* "ok LIVE OFF BYTES ELEM SEQ DIED\tTYPE". The type is last and behind
|
||||||
|
a tab because a spelling holds spaces — "(Vec i32)" — and nothing
|
||||||
|
else on the line does. *)
|
||||||
|
(match String.index_opt line '\t' with
|
||||||
|
| None -> Error ("the program answered " ^ line)
|
||||||
|
| Some tab ->
|
||||||
|
let head = String.sub line 0 tab
|
||||||
|
and ty = String.sub line (tab + 1) (String.length line - tab - 1) in
|
||||||
|
(match String.split_on_char ' ' head with
|
||||||
|
| [ "ok"; live; off; bytes; elem; seq; died ] ->
|
||||||
|
(match List.map int_of_string_opt [ live; off; bytes; elem; seq; died ] with
|
||||||
|
| [ Some live; Some off; Some bytes; Some elem; Some seq; Some died ] ->
|
||||||
|
Ok (Some { rlive = live <> 0; roff = off; rbytes = bytes;
|
||||||
|
relem = elem; rseq = seq; rdied = died; rtype = ty })
|
||||||
|
| _ -> Error ("the program answered " ^ line))
|
||||||
|
| _ -> Error ("the program answered " ^ line)))
|
||||||
|
|
||||||
|
(* The recorded name, back to a [Types.t].
|
||||||
|
|
||||||
|
This is the one thing item 3 needed that nothing else in the registry did,
|
||||||
|
and the whole of the difficulty is that **the table records a string**. It
|
||||||
|
has to: the note is built in [check.ml] at the allocation site, where the
|
||||||
|
concrete type exists, and what crosses into the runtime is bytes — an
|
||||||
|
[ABI] that carried a type would be an ABI that had to agree with the
|
||||||
|
checker's representation of one, which is the coupling the whole
|
||||||
|
no-header-no-tag-word design refuses.
|
||||||
|
|
||||||
|
What closes it is that the string is not a description. It is
|
||||||
|
[Types.to_string] of the type, which is the *source spelling* — that is
|
||||||
|
said in [check.ml]'s [reg_note] as the reason the name is worth printing at
|
||||||
|
all — so the round trip is the language's own reader, the language's own
|
||||||
|
type-expression parser, and the session's own resolver. `Enemy' resolves
|
||||||
|
against the structs this session holds, `(Vec i32)' rebuilds through
|
||||||
|
[Tapp], `[3 i32]' through [Tarray]. No table of spellings is written down
|
||||||
|
anywhere, so nothing can fall behind [Types.to_string].
|
||||||
|
|
||||||
|
And it is allowed to fail, which matters more than it looks. Not every
|
||||||
|
recorded name is a type: [flan_rt.c] notes a pool's slot headers as
|
||||||
|
"pool slots", because after a free-all an address landing in them must not
|
||||||
|
come back as an element. That string is not Flan source and must not
|
||||||
|
become one — so a name that does not resolve is refused with the name
|
||||||
|
quoted, and never defaulted to bytes. *)
|
||||||
|
let type_of_spelling t spelling : (Types.t, string) result =
|
||||||
|
let refuse why =
|
||||||
|
Error
|
||||||
|
(Printf.sprintf "%s is not a type this session can resolve: %s"
|
||||||
|
(Wire.quote spelling) why)
|
||||||
|
in
|
||||||
|
match Reader.read_all ~file:"<registry>" spelling with
|
||||||
|
| exception Loc.Error { Loc.dmsg = why; _ } -> refuse why
|
||||||
|
| [] -> refuse "there is nothing in it"
|
||||||
|
| _ :: _ :: _ -> refuse "it is more than one form"
|
||||||
|
| [ f ] ->
|
||||||
|
(match Check.resolve t.session.Session.env (Parse.texpr f) with
|
||||||
|
| ty -> Ok ty
|
||||||
|
| exception Loc.Error { Loc.dmsg = why; _ } -> refuse why)
|
||||||
|
|
||||||
|
(* The extern that hands a number back as a pointer.
|
||||||
|
|
||||||
|
The one piece an address-rooted thunk cannot work out for itself, and it is
|
||||||
|
the same arrangement [flan/dev-slot] has for a frame's slot: Flan has no
|
||||||
|
integer-to-pointer cast, deliberately, and the inspector is not a Flan
|
||||||
|
program. Everything after this is ordinary — a pointer-to-pointer cast and
|
||||||
|
a render, which is what [Session.render_slot] already does at a slot's
|
||||||
|
address. *)
|
||||||
|
let addr_extern : Tast.extern =
|
||||||
|
{ Tast.ename = "flan/dev-addr"; esym = "flan_dev_reg_addr";
|
||||||
|
eparams = [ Types.Int Types.I64 ];
|
||||||
|
eret = Types.Ptr (Types.Int Types.U8) }
|
||||||
|
|
||||||
|
(* Renders the value [(Ptr ty)] holding [addr], in the program.
|
||||||
|
|
||||||
|
**A pointer and not the pointee, and that is the design.** Rendering the
|
||||||
|
[ty] at that address directly would read the storage whatever the registry
|
||||||
|
said, which is the hex dump this project does not want to be. Rendering a
|
||||||
|
[(Ptr ty)] puts the walk through [render.ml]'s pointer arm, which is the
|
||||||
|
arm that asks first: live, and the pointee is rendered one level deeper;
|
||||||
|
dead, and the epitaph says what died there instead. So an address root and
|
||||||
|
a slot root reach the same two answers by the same path, and the permission
|
||||||
|
question is asked in exactly one place in the compiler.
|
||||||
|
|
||||||
|
Built here rather than in [session.ml] because it is the inspector's
|
||||||
|
rooting mode and not the session's: a session renders what a *program*
|
||||||
|
holds — a frame's slot, a global — and an address handed in from outside is
|
||||||
|
neither of those. *)
|
||||||
|
let render_addr (s : Session.t) ~addr ~(ty : Types.t)
|
||||||
|
: (Session.change, string) result =
|
||||||
|
let loc = Loc.unknown in
|
||||||
|
let extra = ref [] and nslots = ref 0 in
|
||||||
|
let c =
|
||||||
|
{ Render.structs = s.Session.program.Tast.structs;
|
||||||
|
unions = s.Session.program.Tast.unions;
|
||||||
|
enums =
|
||||||
|
Hashtbl.fold (fun k v acc -> (k, v) :: acc) s.Session.env.Check.enums [];
|
||||||
|
emit = Session.dev_emitter;
|
||||||
|
ptrs = Some Session.dev_pointers;
|
||||||
|
alloc = (fun ty ->
|
||||||
|
let i = !nslots in
|
||||||
|
incr nslots;
|
||||||
|
extra := ty :: !extra;
|
||||||
|
i) }
|
||||||
|
in
|
||||||
|
let pty = Types.Ptr ty in
|
||||||
|
let root =
|
||||||
|
{ Tast.e =
|
||||||
|
Tast.Prim
|
||||||
|
(Tast.Cast pty,
|
||||||
|
[ { Tast.e =
|
||||||
|
Tast.Call
|
||||||
|
("flan/dev-addr",
|
||||||
|
[ { Tast.e = Tast.Int (Int64.of_int addr, Types.I64);
|
||||||
|
ty = Types.Int Types.I64; loc } ]);
|
||||||
|
ty = Types.Ptr (Types.Int Types.U8); loc } ]);
|
||||||
|
ty = pty; loc }
|
||||||
|
in
|
||||||
|
match Render.render c 0 root with
|
||||||
|
| exception Loc.Error { Loc.dmsg = why; _ } -> Error why
|
||||||
|
| parts ->
|
||||||
|
let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in
|
||||||
|
s.Session.thunks <- s.Session.thunks + 1;
|
||||||
|
let name = Printf.sprintf "at/%d" s.Session.thunks in
|
||||||
|
let thunk : Tast.fn =
|
||||||
|
{ Tast.name; params = []; ret = Types.Unit;
|
||||||
|
body = (nullary "flan/dev-begin" :: parts) @ [ nullary "flan/dev-end" ];
|
||||||
|
fdefers = []; fparent = None; floc = loc;
|
||||||
|
slots = Array.of_list (List.rev !extra);
|
||||||
|
(* Every slot in here is the walk's own scratch: what is being shown
|
||||||
|
is storage this thunk reaches by address. *)
|
||||||
|
snames = Array.make (List.length !extra) None }
|
||||||
|
in
|
||||||
|
let program =
|
||||||
|
{ s.Session.program with
|
||||||
|
Tast.fns = s.Session.program.Tast.fns @ [ thunk ];
|
||||||
|
externs = s.Session.program.Tast.externs @ Session.externs @ [ addr_extern ] }
|
||||||
|
in
|
||||||
|
let ir =
|
||||||
|
Emit.redefinition ~dev:true ~debug:s.Session.debug ~known:(Session.known s)
|
||||||
|
~call:name program ~fns:[ name ]
|
||||||
|
in
|
||||||
|
Ok { Session.ir; names = []; fns = []; installs = true }
|
||||||
|
|
||||||
|
(* [(:op "at" :addr N :type "Enemy")] — point at any heap address.
|
||||||
|
|
||||||
|
The inspector's third rooting mode, and the one that needs no frame.
|
||||||
|
[locals] and [inspect] root at a frame and a slot, which is the address the
|
||||||
|
shadow stack knows and the type [Tast.fn.slots] knows. This roots at an
|
||||||
|
address somebody has in their hand — out of a C debugger, out of a printed
|
||||||
|
[Ptr], out of a leak report — and there is no frame to read a type off.
|
||||||
|
|
||||||
|
**So the type comes from the registry when it is not given**, which is what
|
||||||
|
the table was carrying a string for all along and what nothing had yet
|
||||||
|
read. See [type_of_spelling] for how the string becomes a [Types.t] and why
|
||||||
|
it is allowed to refuse.
|
||||||
|
|
||||||
|
**A given [:type] wins over the recorded one**, and is not checked against
|
||||||
|
it. Overriding is the point of being able to say it: a pointer into the
|
||||||
|
middle of a block, a struct the registry recorded under a container's
|
||||||
|
spelling, a reinterpretation someone is doing on purpose. What is *not*
|
||||||
|
silent is the disagreement — the reply carries [:recorded] whenever the
|
||||||
|
table had a name, so a client showing one type while the allocator wrote
|
||||||
|
down another can say so.
|
||||||
|
|
||||||
|
**Refused while running**, the same as [inspect] and for a related reason
|
||||||
|
rather than the same one: there is no frame here to be redefined under us,
|
||||||
|
but there is a table, and live-or-dead is exactly the thing a running
|
||||||
|
program is changing. An answer read off a program mid-frame is an answer
|
||||||
|
about a moment that has already gone.
|
||||||
|
|
||||||
|
**Refused at an address the block does not divide.** When the entry records
|
||||||
|
an element size and the offset is not a multiple of it, the address is
|
||||||
|
inside an element rather than at one, and rendering the element type there
|
||||||
|
would read one element's tail as another's head — a plausible-looking
|
||||||
|
answer, which is the worst kind. Said with the offset, so the reader can
|
||||||
|
see how far off it is, and overridable by naming a [:type] the way any
|
||||||
|
other reinterpretation is.
|
||||||
|
|
||||||
|
**No [:path].** A path steps from the pointee, and the pointee is what the
|
||||||
|
registry has only just been asked to bless: the whole answer here is the
|
||||||
|
pointer arm's branch. Somebody who wants to walk from what they found
|
||||||
|
reaches it the way the break buffer already does — the value is rendered,
|
||||||
|
and stepping into it is a different root. *)
|
||||||
|
let inspect_addr t ~addr ~want_type =
|
||||||
|
if addr <= 0 then error "an address is a positive number"
|
||||||
|
else if not (alive t) then error "the program exited; restart flan dev"
|
||||||
|
else
|
||||||
|
match state t with
|
||||||
|
| Running ->
|
||||||
|
error
|
||||||
|
"the program is running; whether an address is still live is exactly \
|
||||||
|
what a running program is changing, so it is read from a stopped one"
|
||||||
|
| Unreachable m -> error ("cannot ask the program about that address: " ^ m)
|
||||||
|
| Stopped _ ->
|
||||||
|
(match reg_at t ~addr with
|
||||||
|
| Error m -> error m
|
||||||
|
| Ok entry ->
|
||||||
|
let recorded = Option.map (fun e -> e.rtype) entry in
|
||||||
|
let chosen =
|
||||||
|
match want_type with
|
||||||
|
| Some spelling -> type_of_spelling t spelling
|
||||||
|
| None ->
|
||||||
|
(match entry with
|
||||||
|
| Some e -> type_of_spelling t e.rtype
|
||||||
|
| None ->
|
||||||
|
Error
|
||||||
|
"the registry has never seen that address and no :type was \
|
||||||
|
given, so there is nothing to say what is there — a stack \
|
||||||
|
local, a global or a pointer from C is deliberately not in \
|
||||||
|
the table, and the shadow stack answers for the first two \
|
||||||
|
by name")
|
||||||
|
in
|
||||||
|
(match chosen with
|
||||||
|
| Error m -> error m
|
||||||
|
| Ok ty ->
|
||||||
|
let misaligned =
|
||||||
|
match (want_type, entry) with
|
||||||
|
| None, Some e when e.relem > 0 && e.roff mod e.relem <> 0 ->
|
||||||
|
Some e
|
||||||
|
| _ -> None
|
||||||
|
in
|
||||||
|
(match misaligned with
|
||||||
|
| Some e ->
|
||||||
|
error
|
||||||
|
(Printf.sprintf
|
||||||
|
"that address is %d bytes into a block of %s, whose \
|
||||||
|
elements are %d bytes: it is inside an element rather \
|
||||||
|
than at one, and reading %s there would show one \
|
||||||
|
element's tail as another's head. Name a :type to read \
|
||||||
|
it anyway."
|
||||||
|
e.roff e.rtype e.relem e.rtype)
|
||||||
|
| None ->
|
||||||
|
let told =
|
||||||
|
match recorded with
|
||||||
|
| None -> [ ":recorded nil" ]
|
||||||
|
| Some r -> [ ":recorded " ^ Wire.quote r ]
|
||||||
|
in
|
||||||
|
let where =
|
||||||
|
match entry with
|
||||||
|
| None -> []
|
||||||
|
| Some e ->
|
||||||
|
[ Printf.sprintf ":offset %d" e.roff;
|
||||||
|
Printf.sprintf ":bytes %d" e.rbytes;
|
||||||
|
Printf.sprintf ":elem %d" e.relem;
|
||||||
|
Printf.sprintf ":step %d" e.rseq;
|
||||||
|
Printf.sprintf ":freed %d" e.rdied ]
|
||||||
|
in
|
||||||
|
let live =
|
||||||
|
match entry with Some e when e.rlive -> "t" | _ -> "nil"
|
||||||
|
in
|
||||||
|
(match render_addr t.session ~addr ~ty with
|
||||||
|
| Error m -> error m
|
||||||
|
| exception Failure m -> error m
|
||||||
|
| Ok c ->
|
||||||
|
(match run_render_thunk t ~tag:"a" ~c with
|
||||||
|
| Error m -> error m
|
||||||
|
| Ok v ->
|
||||||
|
ok
|
||||||
|
([ Printf.sprintf ":addr %d" addr;
|
||||||
|
":type " ^ Wire.quote (Types.to_string (Types.Ptr ty));
|
||||||
|
":value " ^ Wire.quote v; ":live " ^ live ]
|
||||||
|
@ told @ where))))))
|
||||||
|
|
||||||
|
(* [reg types] and [reg leaks] — the table grouped by type spelling.
|
||||||
|
|
||||||
|
The walk and the group-by are one function in [flan_dev.c], because a leak
|
||||||
|
report is a breakdown with the dead left out and two walks would drift.
|
||||||
|
What this end adds is the order: biggest first, by bytes. A breakdown read
|
||||||
|
in table order is a list of everything and tells you nothing; a breakdown
|
||||||
|
read biggest-first is the answer to "where did the memory go", which is the
|
||||||
|
only reason either verb exists.
|
||||||
|
|
||||||
|
[:overflow] is carried rather than swallowed. A table that filled has
|
||||||
|
blocks in the program that are in nobody's row, so every number below it is
|
||||||
|
a floor and not a count, and a reader that could not tell would quote them
|
||||||
|
as counts. *)
|
||||||
|
let reg_rows t ~verb =
|
||||||
|
match request t verb with
|
||||||
|
| exception Unix.Unix_error (e, _, _) ->
|
||||||
|
Error ("cannot reach the program: " ^ Unix.error_message e)
|
||||||
|
| text ->
|
||||||
|
(match String.split_on_char '\n' text with
|
||||||
|
| [] -> Error "the program answered nothing"
|
||||||
|
| hdr :: rest ->
|
||||||
|
let hdr = String.trim hdr in
|
||||||
|
if String.length hdr > 4 && String.sub hdr 0 4 = "err " then
|
||||||
|
Error (String.sub hdr 4 (String.length hdr - 4))
|
||||||
|
else
|
||||||
|
(match String.split_on_char ' ' hdr with
|
||||||
|
| [ n; over ] when int_of_string_opt n <> None ->
|
||||||
|
let rows =
|
||||||
|
List.filter_map
|
||||||
|
(fun line ->
|
||||||
|
match String.index_opt line '\t' with
|
||||||
|
| None -> None
|
||||||
|
| Some tab ->
|
||||||
|
let ty =
|
||||||
|
String.sub line (tab + 1) (String.length line - tab - 1)
|
||||||
|
in
|
||||||
|
(match
|
||||||
|
List.map int_of_string_opt
|
||||||
|
(String.split_on_char ' ' (String.sub line 0 tab))
|
||||||
|
with
|
||||||
|
| [ Some count; Some bytes ] -> Some (ty, count, bytes)
|
||||||
|
| _ -> None))
|
||||||
|
rest
|
||||||
|
in
|
||||||
|
let rows =
|
||||||
|
List.stable_sort (fun (_, _, a) (_, _, b) -> compare b a) rows
|
||||||
|
in
|
||||||
|
Ok (rows, over <> "0")
|
||||||
|
| _ -> Error ("the program answered " ^ hdr)))
|
||||||
|
|
||||||
|
let reg_listing t ~verb ~note =
|
||||||
|
match reg_rows t ~verb with
|
||||||
|
| Error m -> error m
|
||||||
|
| Ok (rows, overflow) ->
|
||||||
|
let blocks = List.fold_left (fun a (_, c, _) -> a + c) 0 rows
|
||||||
|
and bytes = List.fold_left (fun a (_, _, b) -> a + b) 0 rows in
|
||||||
|
ok
|
||||||
|
[ ":types "
|
||||||
|
^ Wire.list
|
||||||
|
(List.map
|
||||||
|
(fun (ty, c, b) ->
|
||||||
|
Wire.list [ Wire.quote ty; string_of_int c; string_of_int b ])
|
||||||
|
rows);
|
||||||
|
Printf.sprintf ":blocks %d" blocks;
|
||||||
|
Printf.sprintf ":bytes %d" bytes;
|
||||||
|
(if overflow then ":overflow t" else ":overflow nil");
|
||||||
|
":note " ^ Wire.quote note ]
|
||||||
|
|
||||||
(* [(:op "globals")] — the globals the stopped stack reaches, in one section.
|
(* [(:op "globals")] — the globals the stopped stack reaches, in one section.
|
||||||
|
|
||||||
Locals were the half the shadow stack was built for; these are arguably the
|
Locals were the half the shadow stack was built for; these are arguably the
|
||||||
@ -1786,6 +2152,42 @@ let handle t req =
|
|||||||
(* No :frame, and that is the point: the section is the stack's, not a
|
(* No :frame, and that is the point: the section is the stack's, not a
|
||||||
frame's. See [globals_op]. *)
|
frame's. See [globals_op]. *)
|
||||||
| Some "globals" -> globals_op t
|
| Some "globals" -> globals_op t
|
||||||
|
(* [(:op "at" :addr N)] and an optional [:type]. The rooting mode with no
|
||||||
|
frame in it: an address somebody has in their hand, and the registry's
|
||||||
|
own answer for what is there when none is named. See [inspect_addr]. *)
|
||||||
|
| Some "at" ->
|
||||||
|
(match Wire.int_field req "addr" with
|
||||||
|
| None ->
|
||||||
|
error
|
||||||
|
"at needs :addr, the address to point at; it is the one thing this \
|
||||||
|
verb cannot work out for itself"
|
||||||
|
| Some addr -> inspect_addr t ~addr ~want_type:(Wire.string_field req "type"))
|
||||||
|
(* What this program is made of, by type. Everything the table holds, live
|
||||||
|
and dead both — the dead are the bulk of it in a long-running program and
|
||||||
|
they are what says where the allocation went, not only where it stayed. *)
|
||||||
|
| Some "allocations" ->
|
||||||
|
reg_listing t ~verb:"reg types"
|
||||||
|
~note:
|
||||||
|
"every block the registry recorded, live and dead, grouped by the \
|
||||||
|
type the allocator's caller named"
|
||||||
|
(* And what is still held.
|
||||||
|
|
||||||
|
"At exit" is the question this answers and it needs saying plainly,
|
||||||
|
because the obvious reading does not survive contact with a game. A
|
||||||
|
program killed by a signal — which is how a program under this editor
|
||||||
|
usually ends — runs no handler at all, so nothing written inside it could
|
||||||
|
report anything. There are therefore two readers and they are not
|
||||||
|
alternatives: this verb, which reads the same table over the agent socket
|
||||||
|
and can be asked at any moment, including the last one before the kill;
|
||||||
|
and an atexit hook in [flan_dev.c] for the program that returns from main
|
||||||
|
on its own, which is off unless FLAN_DEV_LEAKS is set, because a dev
|
||||||
|
build's output is read by the acceptance table. *)
|
||||||
|
| Some "leaks" ->
|
||||||
|
reg_listing t ~verb:"reg leaks"
|
||||||
|
~note:
|
||||||
|
"what the registry still holds live at the moment it was asked; a \
|
||||||
|
program that is killed runs no exit handler, so this verb and not a \
|
||||||
|
hook is what answers for one"
|
||||||
| Some "layout" ->
|
| Some "layout" ->
|
||||||
(match Wire.string_field req "type" with
|
(match Wire.string_field req "type" with
|
||||||
| Some ty -> layout t ~ty
|
| Some ty -> layout t ~ty
|
||||||
|
|||||||
@ -1055,6 +1055,8 @@ static int flan_reg_full; /* something found no slot */
|
|||||||
* second version of the allocator gated on a build flag is worse than a
|
* second version of the allocator gated on a build flag is worse than a
|
||||||
* branch. That is a real cost and not zero; BUILT.md says so rather than
|
* branch. That is a real cost and not zero; BUILT.md says so rather than
|
||||||
* repeating the claim that a release build carries nothing. */
|
* repeating the claim that a release build carries nothing. */
|
||||||
|
static void flan_reg_report(void); /* the exit report, at the bottom */
|
||||||
|
|
||||||
void flan_dev_reg_enable(void) {
|
void flan_dev_reg_enable(void) {
|
||||||
if (flan_reg_on) return;
|
if (flan_reg_on) return;
|
||||||
flan_reg = (flan_reg_entry *)calloc(FLAN_REG_CAP, sizeof *flan_reg);
|
flan_reg = (flan_reg_entry *)calloc(FLAN_REG_CAP, sizeof *flan_reg);
|
||||||
@ -1063,6 +1065,12 @@ void flan_dev_reg_enable(void) {
|
|||||||
which is what a release build answers too. */
|
which is what a release build answers too. */
|
||||||
if (flan_reg == NULL) return;
|
if (flan_reg == NULL) return;
|
||||||
flan_reg_on = 1;
|
flan_reg_on = 1;
|
||||||
|
/* Here and not at file scope: a destructor attribute would run in every
|
||||||
|
build, since this file is linked into every build, and that would be a
|
||||||
|
third place a release build is not free. Registered from inside the one
|
||||||
|
function only a dev build's constructor calls, a release binary still
|
||||||
|
carries a null pointer, a zero flag and the declarations. */
|
||||||
|
if (getenv("FLAN_DEV_LEAKS") != NULL) atexit(flan_reg_report);
|
||||||
}
|
}
|
||||||
|
|
||||||
int flan_dev_reg_enabled(void) { return flan_reg_on; }
|
int flan_dev_reg_enabled(void) { return flan_reg_on; }
|
||||||
@ -1259,3 +1267,148 @@ int64_t flan_dev_reg_count(int32_t live_only) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Reading the table back ───────────────────────────────────────────
|
||||||
|
*
|
||||||
|
* Three accessors and no formatter. Everything above this point is on the
|
||||||
|
* writer's side — a game loop — and everything below is read by a person
|
||||||
|
* pressing a key, so the shape that matters here is "hand back what is
|
||||||
|
* recorded" rather than "hand back a sentence". The agent turns these into
|
||||||
|
* protocol lines and the daemon turns those into an editor reply; the one
|
||||||
|
* piece of text this file still writes is the exit report at the bottom,
|
||||||
|
* which has nowhere else to go.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* What the table records for the address [p], live or dead. The containment
|
||||||
|
* lookup, exposed: [flan_dev_reg_live] answers the renderer's yes/no and
|
||||||
|
* [flan_dev_reg_emit] writes the epitaph, and neither hands back the *name*,
|
||||||
|
* which is what a reader that wants to point at a bare address needs — it has
|
||||||
|
* no (Ptr T) to read the type off, so the table's own answer is the only
|
||||||
|
* answer there is.
|
||||||
|
*
|
||||||
|
* [off] is how far into the block [p] lands, and it is not decoration: with
|
||||||
|
* [elem] it is what says whether the address is an element boundary or the
|
||||||
|
* middle of one. A caller that renders a T at an offset that is not a
|
||||||
|
* multiple of [elem] would be reading one element's tail as another's head,
|
||||||
|
* so it is given the two numbers rather than a flag it cannot check. */
|
||||||
|
int32_t flan_dev_reg_at(const void *p, const char **type, int64_t *typelen,
|
||||||
|
int64_t *off, int64_t *bytes, int64_t *elem,
|
||||||
|
int64_t *seq, int64_t *died) {
|
||||||
|
flan_reg_entry *e = flan_reg_on ? flan_reg_find((uintptr_t)p) : NULL;
|
||||||
|
if (e == NULL) return 0;
|
||||||
|
if (type) *type = e->type;
|
||||||
|
if (typelen) *typelen = e->typelen;
|
||||||
|
if (off) *off = (int64_t)((uintptr_t)p - e->base);
|
||||||
|
if (bytes) *bytes = e->bytes;
|
||||||
|
if (elem) *elem = e->elem;
|
||||||
|
if (seq) *seq = e->seq;
|
||||||
|
if (died) *died = e->died;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An address, as a number, handed back as a pointer. The one thing an
|
||||||
|
* address-rooted render thunk cannot do for itself: Flan has no integer-to-
|
||||||
|
* pointer cast, deliberately — a program that could make a pointer out of
|
||||||
|
* arithmetic is a program the type system stops describing — and the
|
||||||
|
* inspector is not a program. It is the same arrangement [flan_agent_frame_
|
||||||
|
* slot] already has for a frame's slot, and for the same reason: the compiler
|
||||||
|
* knows the type, and something outside the language supplies the address. */
|
||||||
|
void *flan_dev_reg_addr(int64_t a) { return (void *)(uintptr_t)a; }
|
||||||
|
|
||||||
|
/* And back the other way, which is the half a *program* needs rather than the
|
||||||
|
* inspector. The address root above takes a number, and the things that hand
|
||||||
|
* out addresses as numbers all live outside the language — gdb, valgrind, a C
|
||||||
|
* library's callback, a printf("%p") in somebody's shim. A Flan program that
|
||||||
|
* wants to say one out loud has no cast for it, deliberately: pointer
|
||||||
|
* arithmetic out of an integer is the thing the type system stops describing.
|
||||||
|
* So the conversion is a C function, named and visible, and the language
|
||||||
|
* still has no operator for it. */
|
||||||
|
int64_t flan_dev_reg_number(const void *p) { return (int64_t)(uintptr_t)p; }
|
||||||
|
|
||||||
|
/* The table, by type spelling: one row per distinct name, with how many
|
||||||
|
* blocks carry it and how many bytes they hold. [live_only] is the whole
|
||||||
|
* difference between "what is this program made of" and "what is still held",
|
||||||
|
* which is why there is one walk here and not two — a leak report is a
|
||||||
|
* breakdown with the dead left out, and writing it twice would let the two
|
||||||
|
* drift.
|
||||||
|
*
|
||||||
|
* Caller-owned buffers, and the return is how many distinct types there were
|
||||||
|
* rather than how many were written: a caller whose buffers were too small is
|
||||||
|
* told so by the number coming back larger than [cap], which is the same
|
||||||
|
* contract the watch table's count has.
|
||||||
|
*
|
||||||
|
* The grouping is O(rows x types) on string compare. The table is 4096 slots
|
||||||
|
* and the reader is a person, so this is the side the cost belongs on — the
|
||||||
|
* same judgement [flan_reg_find] is written down for. Compared by *content*
|
||||||
|
* and not by pointer: the name is a literal the compiler emitted beside a
|
||||||
|
* call site, and two modules that both allocate an Enemy emit two of them. */
|
||||||
|
int64_t flan_dev_reg_by_type(int32_t live_only, int64_t *counts,
|
||||||
|
int64_t *bytes, const char **types,
|
||||||
|
int64_t *typelens, int64_t cap) {
|
||||||
|
int64_t i, n = 0;
|
||||||
|
if (!flan_reg_on) return 0;
|
||||||
|
for (i = 0; i < FLAN_REG_CAP; i++) {
|
||||||
|
flan_reg_entry *e = &flan_reg[i];
|
||||||
|
int64_t j;
|
||||||
|
int found = 0;
|
||||||
|
if (e->base == 0) continue;
|
||||||
|
if (live_only && e->died != 0) continue;
|
||||||
|
for (j = 0; j < n && j < cap; j++) {
|
||||||
|
if (typelens[j] != e->typelen) continue;
|
||||||
|
if (memcmp(types[j], e->type, (size_t)e->typelen) != 0) continue;
|
||||||
|
counts[j]++;
|
||||||
|
bytes[j] += e->bytes;
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (found) continue;
|
||||||
|
if (n < cap) {
|
||||||
|
types[n] = e->type;
|
||||||
|
typelens[n] = e->typelen;
|
||||||
|
counts[n] = 1;
|
||||||
|
bytes[n] = e->bytes;
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── What is still held when the program returns ──────────────────────
|
||||||
|
*
|
||||||
|
* Registered by [flan_dev_reg_enable] and therefore only in a dev build,
|
||||||
|
* which is the point: a file-scope destructor would run in *every* build,
|
||||||
|
* because this file is compiled into every build, and that would be a third
|
||||||
|
* place a release build is not free. BUILT.md names two and only two.
|
||||||
|
*
|
||||||
|
* Off unless FLAN_DEV_LEAKS is set, and that is not timidity. The acceptance
|
||||||
|
* table reads programs/registry.flan's output with stderr folded in, so a
|
||||||
|
* report nobody asked for is a report that changes what a dev build prints.
|
||||||
|
*
|
||||||
|
* And it says "at exit" honestly. This runs when main returns or something
|
||||||
|
* calls exit(). A program killed with a signal — which is how a game under
|
||||||
|
* the editor usually ends — runs no handler at all, and no hook written here
|
||||||
|
* could change that. The answer for that program is the daemon's own verb,
|
||||||
|
* which reads the same table over the agent socket and can be asked at any
|
||||||
|
* moment, including the one before the kill. This hook is for the program
|
||||||
|
* that finishes on its own. */
|
||||||
|
static void flan_reg_report(void) {
|
||||||
|
enum { ROWS = 128 };
|
||||||
|
int64_t counts[ROWS], bytes[ROWS], typelens[ROWS];
|
||||||
|
const char *types[ROWS];
|
||||||
|
int64_t n, i, blocks = 0, held = 0;
|
||||||
|
n = flan_dev_reg_by_type(1, counts, bytes, types, typelens, ROWS);
|
||||||
|
if (n == 0) return;
|
||||||
|
for (i = 0; i < n && i < ROWS; i++) { blocks += counts[i]; held += bytes[i]; }
|
||||||
|
fprintf(stderr, "flan: %lld block%s still held at exit, %lld bytes\n",
|
||||||
|
(long long)blocks, blocks == 1 ? "" : "s", (long long)held);
|
||||||
|
for (i = 0; i < n && i < ROWS; i++)
|
||||||
|
fprintf(stderr, "flan: %6lld %10lld %.*s\n", (long long)counts[i],
|
||||||
|
(long long)bytes[i], (int)typelens[i], types[i]);
|
||||||
|
if (n > ROWS)
|
||||||
|
fprintf(stderr, "flan: and %lld more type%s than this report holds\n",
|
||||||
|
(long long)(n - ROWS), n - ROWS == 1 ? "" : "s");
|
||||||
|
if (flan_reg_full)
|
||||||
|
fprintf(stderr,
|
||||||
|
"flan: the table overflowed, so this is a floor and not a "
|
||||||
|
"count\n");
|
||||||
|
}
|
||||||
|
|||||||
@ -7,13 +7,12 @@
|
|||||||
;;;;
|
;;;;
|
||||||
;;;; N is the registry's own event counter and is no part of the claim: it
|
;;;; N is the registry's own event counter and is no part of the claim: it
|
||||||
;;;; moves if anything allocates or frees ahead of this program's two Vecs.
|
;;;; moves if anything allocates or frees ahead of this program's two Vecs.
|
||||||
;;;; A test that asserts these lines should match around it, not on it.
|
;;;; `test_dev.ml' matches around it and never on it.
|
||||||
;;;;
|
;;;;
|
||||||
;;;; Both lines are what `(:op "locals" :frame 1)` answers with today, and
|
;;;; Both lines are what `(:op "locals" :frame 1)` answers with, and the
|
||||||
;;;; both were read off a running session by hand. **No test drives this
|
;;;; `a pointer the registry knows about' case in test_dev.ml is what drives
|
||||||
;;;; program yet**: the case belongs beside the other `locals` and `inspect`
|
;;;; them. They were read off a running session by hand once; they are not
|
||||||
;;;; cases in test_dev.ml, which is another lane's file. NEXT.md says so
|
;;;; read by hand any more.
|
||||||
;;;; rather than letting the verification read as automated.
|
|
||||||
;;;;
|
;;;;
|
||||||
;;;; Why a Vec rather than a struct on the stack: a stack address is not in
|
;;;; Why a Vec rather than a struct on the stack: a stack address is not in
|
||||||
;;;; the registry by design — the shadow stack already answers for a local by
|
;;;; the registry by design — the shadow stack already answers for a local by
|
||||||
@ -24,6 +23,20 @@
|
|||||||
(defstruct Boom [why i32])
|
(defstruct Boom [why i32])
|
||||||
(defstruct Enemy [hp i32 x i32])
|
(defstruct Enemy [hp i32 x i32])
|
||||||
|
|
||||||
|
;;; The address root — `(:op "at" :addr N)' — takes a number, and the things
|
||||||
|
;;; that hand out addresses as numbers all live outside the language: a
|
||||||
|
;;; debugger, valgrind, a C shim's printf. A Flan program has no cast for it
|
||||||
|
;;; on purpose — pointer arithmetic out of an integer is what the type system
|
||||||
|
;;; stops describing — so saying one out loud is a declare-c.
|
||||||
|
(declare-c ptr-num [p (Ptr Enemy)] i64 "flan_dev_reg_number")
|
||||||
|
|
||||||
|
;;; Where the two addresses are left for a reader to find. Globals rather than
|
||||||
|
;;; a printed line because a global is reachable by name from `C-x C-e' while
|
||||||
|
;;; the program is stopped, and a local is not: the test asks the session for
|
||||||
|
;;; them the way a person at the break loop would.
|
||||||
|
(defvar live-addr i64)
|
||||||
|
(defvar dead-addr i64)
|
||||||
|
|
||||||
(defn deeper [] i64
|
(defn deeper [] i64
|
||||||
(restart-case
|
(restart-case
|
||||||
(do (error (Boom {.why 7})) 1)
|
(do (error (Boom {.why 7})) 1)
|
||||||
@ -37,6 +50,8 @@
|
|||||||
(let [live (addr (at v 0))
|
(let [live (addr (at v 0))
|
||||||
dead (addr (at w 0))]
|
dead (addr (at w 0))]
|
||||||
(free w)
|
(free w)
|
||||||
|
(set live-addr (ptr-num live))
|
||||||
|
(set dead-addr (ptr-num dead))
|
||||||
(deeper))))
|
(deeper))))
|
||||||
|
|
||||||
(defvar ticks i64)
|
(defvar ticks i64)
|
||||||
|
|||||||
309
test/test_dev.ml
309
test/test_dev.ml
@ -1348,6 +1348,315 @@ let () =
|
|||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
(* ── A pointer the registry knows about ───────────────────────── *)
|
||||||
|
|
||||||
|
(* The inspector's pointer arm, and the address root beside it.
|
||||||
|
|
||||||
|
[programs/dev-ptr.flan] carried the two lines a session answers with in
|
||||||
|
its own header and said, in the header, that they had been read off a
|
||||||
|
running session **by hand**. This is the case that makes that stop
|
||||||
|
being true. Nothing else drives it: [programs/registry.flan] asserts
|
||||||
|
the *table* from the acceptance side — live or not, in a dev build and
|
||||||
|
a release one — and says nothing about what an inspector renders.
|
||||||
|
|
||||||
|
The claim has two halves and they are what the registry bought. A
|
||||||
|
pointer into live Vec storage is *followed*, one level deeper, and its
|
||||||
|
pointee rendered by the same walk as anything else. A pointer into
|
||||||
|
storage that has been freed is not followed, and names what died there
|
||||||
|
instead. Both pointers have the same static type, so nothing but the
|
||||||
|
table can tell them apart — which is the whole argument of "permission,
|
||||||
|
not identification". *)
|
||||||
|
let psock = tmp "ptr.sock" and pout = tmp "ptr.out" in
|
||||||
|
(try Sys.remove psock with Sys_error _ -> ());
|
||||||
|
let pfd = Unix.openfile pout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
|
||||||
|
let ppid =
|
||||||
|
Unix.create_process flan
|
||||||
|
[| flan; "dev"; "programs/dev-ptr.flan"; "-s"; psock |]
|
||||||
|
Unix.stdin pfd Unix.stderr
|
||||||
|
in
|
||||||
|
Unix.close pfd;
|
||||||
|
if not (listening ~pid:ppid psock) then begin
|
||||||
|
fail "the pointer daemon %s" !listen_why;
|
||||||
|
(try Unix.kill ppid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||||
|
end
|
||||||
|
else begin
|
||||||
|
let c = connect psock in
|
||||||
|
let ask sexp = Wire.parse (Wire.send c sexp; Wire.recv c) in
|
||||||
|
let stopped r =
|
||||||
|
match Wire.field r "stopped" with
|
||||||
|
| Some { Form.v = Form.Sym "t"; _ } -> true
|
||||||
|
| _ -> false
|
||||||
|
in
|
||||||
|
let value r = Option.value ~default:"" (Wire.string_field r "value") in
|
||||||
|
let message r =
|
||||||
|
Option.value ~default:(status r) (Wire.string_field r "message")
|
||||||
|
in
|
||||||
|
let flag r key =
|
||||||
|
match Wire.field r key with
|
||||||
|
| Some { Form.v = Form.Sym "t"; _ } -> true
|
||||||
|
| _ -> false
|
||||||
|
in
|
||||||
|
let starts s pre =
|
||||||
|
String.length s >= String.length pre
|
||||||
|
&& String.equal (String.sub s 0 (String.length pre)) pre
|
||||||
|
in
|
||||||
|
let contains hay needle =
|
||||||
|
let n = String.length needle in
|
||||||
|
let rec go i =
|
||||||
|
i + n <= String.length hay
|
||||||
|
&& (String.equal (String.sub hay i n) needle || go (i + 1))
|
||||||
|
in
|
||||||
|
go 0
|
||||||
|
in
|
||||||
|
if not (await (fun () -> stopped (ask "(:op \"describe\")"))) then
|
||||||
|
fail "the pointer program never stopped"
|
||||||
|
else begin
|
||||||
|
let entries r =
|
||||||
|
match Wire.field r "locals" with
|
||||||
|
| Some { Form.v = Form.List l; _ } ->
|
||||||
|
List.filter_map
|
||||||
|
(fun (e : Form.t) ->
|
||||||
|
match e.Form.v with
|
||||||
|
| Form.List
|
||||||
|
[ { Form.v = Form.Str n; _ }; { Form.v = Form.Str ty; _ };
|
||||||
|
{ Form.v = Form.Str v; _ }; _ ] -> Some (n, ty, v)
|
||||||
|
| _ -> None)
|
||||||
|
l
|
||||||
|
| _ -> []
|
||||||
|
in
|
||||||
|
let listing = ask "(:op \"locals\" :frame 1)" in
|
||||||
|
if status listing <> "ok" then
|
||||||
|
fail "locals of the frame holding the two pointers: %s"
|
||||||
|
(message listing)
|
||||||
|
else begin
|
||||||
|
let got = entries listing in
|
||||||
|
let find n = List.find_opt (fun (m, _, _) -> String.equal m n) got in
|
||||||
|
(* The live half, asserted whole. A pointer with nobody to ask
|
||||||
|
renders [<ptr>]; this is what having somebody to ask buys. *)
|
||||||
|
(match find "live" with
|
||||||
|
| Some ("live", "(Ptr Enemy)", "<ptr (Enemy {.hp 41 .x 2})>") -> ()
|
||||||
|
| Some (_, ty, v) ->
|
||||||
|
fail "a live pointer into Vec storage rendered %s : %s" v ty
|
||||||
|
| None ->
|
||||||
|
fail "the frame holding the two pointers listed no `live': %s"
|
||||||
|
(String.concat ", " (List.map (fun (n, _, _) -> n) got)));
|
||||||
|
(* And the dead half, asserted *around* the step number and never on
|
||||||
|
it. The step is the registry's own event counter: it moves if
|
||||||
|
anything allocates or frees ahead of this program's two Vecs, and
|
||||||
|
the program's header says so. What is being claimed is that the
|
||||||
|
pointer was not followed and that what died is named. *)
|
||||||
|
(match find "dead" with
|
||||||
|
| None -> fail "the frame holding the two pointers listed no `dead'"
|
||||||
|
| Some (_, ty, v) ->
|
||||||
|
if ty <> "(Ptr Enemy)" then
|
||||||
|
fail "the dead pointer's type is %s, not (Ptr Enemy)" ty;
|
||||||
|
let pre = "<ptr dead: was Enemy, freed at step " in
|
||||||
|
if not (starts v pre && String.length v > String.length pre
|
||||||
|
&& v.[String.length v - 1] = '>')
|
||||||
|
then fail "a pointer into freed Vec storage rendered %s" v
|
||||||
|
else
|
||||||
|
let n =
|
||||||
|
String.sub v (String.length pre)
|
||||||
|
(String.length v - String.length pre - 1)
|
||||||
|
in
|
||||||
|
if int_of_string_opt n = None then
|
||||||
|
fail "the epitaph's step is %S, which is not a number" n;
|
||||||
|
(* No address in it, and that is deliberate rather than an
|
||||||
|
omission: an address is not stable across two runs, so
|
||||||
|
printing one would make this very assertion depend on where
|
||||||
|
the heap landed. *)
|
||||||
|
if contains v "0x" then
|
||||||
|
fail "the epitaph carried an address: %s" v)
|
||||||
|
end;
|
||||||
|
|
||||||
|
(* ── The address root ─────────────────────────────────────── *)
|
||||||
|
|
||||||
|
(* [(:op "at" :addr N)] is the rooting mode with no frame in it. The
|
||||||
|
two addresses are left in globals by the program, which is how a
|
||||||
|
person at a break loop reaches them too — a global is evaluable by
|
||||||
|
name while stopped and a local is not. *)
|
||||||
|
let addr name =
|
||||||
|
let r =
|
||||||
|
ask (Printf.sprintf "(:op \"eval-expr\" :code %S :file \"<t>\")" name)
|
||||||
|
in
|
||||||
|
if status r <> "ok" then None else int_of_string_opt (value r)
|
||||||
|
in
|
||||||
|
(match (addr "live-addr", addr "dead-addr") with
|
||||||
|
| Some live, Some dead when live > 0 && dead > 0 ->
|
||||||
|
(* The type is not given, so the answer for it is the registry's
|
||||||
|
own — the recorded *string*, resolved back to a type by the
|
||||||
|
session. That resolution is the whole of what item 3 needed and
|
||||||
|
it is what this line is really asserting: nothing but the table
|
||||||
|
said `Enemy' here. *)
|
||||||
|
let r = ask (Printf.sprintf "(:op \"at\" :addr %d)" live) in
|
||||||
|
if status r <> "ok" then fail "pointing at a live address: %s" (message r)
|
||||||
|
else begin
|
||||||
|
if value r <> "<ptr (Enemy {.hp 41 .x 2})>" then
|
||||||
|
fail "the address root rendered %s at a live address" (value r);
|
||||||
|
if Option.value ~default:"" (Wire.string_field r "type")
|
||||||
|
<> "(Ptr Enemy)"
|
||||||
|
then
|
||||||
|
fail "the address root resolved the recorded name to %s"
|
||||||
|
(Option.value ~default:"" (Wire.string_field r "type"));
|
||||||
|
if not (flag r "live") then
|
||||||
|
fail "a live address came back not live";
|
||||||
|
if Option.value ~default:"" (Wire.string_field r "recorded")
|
||||||
|
<> "Enemy"
|
||||||
|
then fail "the reply did not carry what the table recorded"
|
||||||
|
end;
|
||||||
|
(* And the dead one, by the same route and with the same type,
|
||||||
|
which is the point: the static type cannot tell these apart. *)
|
||||||
|
let r = ask (Printf.sprintf "(:op \"at\" :addr %d)" dead) in
|
||||||
|
if status r <> "ok" then fail "pointing at a dead address: %s" (message r)
|
||||||
|
else begin
|
||||||
|
if not (starts (value r) "<ptr dead: was Enemy, freed at step ")
|
||||||
|
then fail "the address root rendered %s at a dead address" (value r);
|
||||||
|
if flag r "live" then fail "a freed address came back live"
|
||||||
|
end;
|
||||||
|
(* A named :type wins over the recorded one and is not checked
|
||||||
|
against it — overriding is the point of being able to say it —
|
||||||
|
but the disagreement is never silent: [:recorded] is carried
|
||||||
|
whenever the table had a name. *)
|
||||||
|
let r =
|
||||||
|
ask (Printf.sprintf "(:op \"at\" :addr %d :type \"i32\")" live)
|
||||||
|
in
|
||||||
|
if status r <> "ok" then
|
||||||
|
fail "reading a live address as a named type: %s" (message r)
|
||||||
|
else begin
|
||||||
|
if value r <> "<ptr 41>" then
|
||||||
|
fail "a named :type did not win over the recorded one: %s"
|
||||||
|
(value r);
|
||||||
|
if Option.value ~default:"" (Wire.string_field r "recorded")
|
||||||
|
<> "Enemy"
|
||||||
|
then fail "an overridden read did not say what was recorded"
|
||||||
|
end;
|
||||||
|
(* An address inside an element rather than at one. Rendering the
|
||||||
|
element type there would show one element's tail as another's
|
||||||
|
head — a plausible-looking answer, which is the worst kind — so
|
||||||
|
it is refused with the offset, and a named :type reads it
|
||||||
|
anyway. *)
|
||||||
|
let r = ask (Printf.sprintf "(:op \"at\" :addr %d)" (live + 1)) in
|
||||||
|
if status r <> "error" then
|
||||||
|
fail "an address inside an element answered anyway: %s" (value r)
|
||||||
|
else if not (contains (message r) "inside an element") then
|
||||||
|
fail "a misaligned address was refused without saying why: %s"
|
||||||
|
(message r);
|
||||||
|
let r =
|
||||||
|
ask (Printf.sprintf "(:op \"at\" :addr %d :type \"i32\")" (live + 4))
|
||||||
|
in
|
||||||
|
if status r <> "ok" then
|
||||||
|
fail "a named :type did not reach the second half of an element: %s"
|
||||||
|
(message r)
|
||||||
|
else if value r <> "<ptr 2>" then
|
||||||
|
fail "reading the second i32 of an Enemy gave %s" (value r)
|
||||||
|
| _ ->
|
||||||
|
fail "the program did not leave its two addresses in globals");
|
||||||
|
(* An address the registry has never seen is a fact and not a failure
|
||||||
|
— a stack local, a global, or a pointer from C — and with no
|
||||||
|
[:type] there is nothing to say what is there. Refused by name,
|
||||||
|
rather than answered with bytes. *)
|
||||||
|
let r = ask "(:op \"at\" :addr 12345)" in
|
||||||
|
if status r <> "error" then
|
||||||
|
fail "an address the registry never saw was rendered anyway: %s"
|
||||||
|
(value r)
|
||||||
|
else if not (contains (message r) "never seen") then
|
||||||
|
fail "an unknown address was refused without saying why: %s" (message r);
|
||||||
|
|
||||||
|
(* ── The breakdown, and what is still held ─────────────────── *)
|
||||||
|
|
||||||
|
(* Both are one walk over the table in [flan_dev.c] with the dead
|
||||||
|
left out of the second, and the difference between the two answers
|
||||||
|
is the assertion: this program freed one of its two Enemy blocks,
|
||||||
|
so the breakdown has both and the leak report has one. Counting
|
||||||
|
only the rows would pass with the walk stubbed out; counting the
|
||||||
|
difference cannot. *)
|
||||||
|
let enemy r =
|
||||||
|
match Wire.field r "types" with
|
||||||
|
| Some { Form.v = Form.List l; _ } ->
|
||||||
|
List.fold_left
|
||||||
|
(fun acc (e : Form.t) ->
|
||||||
|
match e.Form.v with
|
||||||
|
| Form.List
|
||||||
|
[ { Form.v = Form.Str "Enemy"; _ };
|
||||||
|
{ Form.v = Form.Int n; _ }; { Form.v = Form.Int b; _ } ] ->
|
||||||
|
Some (Int64.to_int n, Int64.to_int b)
|
||||||
|
| _ -> acc)
|
||||||
|
None l
|
||||||
|
| _ -> None
|
||||||
|
in
|
||||||
|
let all = ask "(:op \"allocations\")" in
|
||||||
|
let live = ask "(:op \"leaks\")" in
|
||||||
|
if status all <> "ok" then fail "the breakdown by type: %s" (message all)
|
||||||
|
else if status live <> "ok" then fail "the leak report: %s" (message live)
|
||||||
|
else begin
|
||||||
|
(match (enemy all, enemy live) with
|
||||||
|
| Some (2, _), Some (1, _) -> ()
|
||||||
|
| got, held ->
|
||||||
|
let say = function
|
||||||
|
| None -> "no row"
|
||||||
|
| Some (n, b) -> Printf.sprintf "%d blocks, %d bytes" n b
|
||||||
|
in
|
||||||
|
fail
|
||||||
|
"the table should hold two Enemy blocks with one of them freed; \
|
||||||
|
the breakdown says %s and the leak report says %s" (say got)
|
||||||
|
(say held));
|
||||||
|
(* Ordered biggest first, by bytes. A breakdown read in table order
|
||||||
|
is a list of everything and answers nothing; biggest-first is the
|
||||||
|
answer to "where did the memory go", which is the only reason
|
||||||
|
either verb exists. *)
|
||||||
|
let bytes r =
|
||||||
|
match Wire.field r "types" with
|
||||||
|
| Some { Form.v = Form.List l; _ } ->
|
||||||
|
List.filter_map
|
||||||
|
(fun (e : Form.t) ->
|
||||||
|
match e.Form.v with
|
||||||
|
| Form.List [ _; _; { Form.v = Form.Int b; _ } ] ->
|
||||||
|
Some (Int64.to_int b)
|
||||||
|
| _ -> None)
|
||||||
|
l
|
||||||
|
| _ -> []
|
||||||
|
in
|
||||||
|
let rec descending = function
|
||||||
|
| a :: (b :: _ as rest) -> a >= b && descending rest
|
||||||
|
| _ -> true
|
||||||
|
in
|
||||||
|
if not (descending (bytes all)) then
|
||||||
|
fail "the breakdown is not ordered biggest first";
|
||||||
|
(* And a leak report is a subset of the breakdown, always: nothing
|
||||||
|
can be live that was never recorded. *)
|
||||||
|
let sum r = List.fold_left ( + ) 0 (bytes r) in
|
||||||
|
if sum live > sum all then
|
||||||
|
fail "the leak report holds more bytes than the whole table does"
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
(* And a running program is refused. There is no frame here to be
|
||||||
|
redefined under us — the registry is a table and not a stack — but
|
||||||
|
live-or-dead is exactly what a running program is changing, so an
|
||||||
|
answer read mid-frame is an answer about a moment that has gone. *)
|
||||||
|
let r = ask "(:op \"restart\" :name \"carry-on\")" in
|
||||||
|
if status r <> "ok" then
|
||||||
|
fail "resuming the pointer program: %s" (message r);
|
||||||
|
if not (await (fun () -> not (stopped (ask "(:op \"describe\")")))) then
|
||||||
|
fail "the pointer program never resumed"
|
||||||
|
else begin
|
||||||
|
let r = ask "(:op \"at\" :addr 4096)" in
|
||||||
|
if status r <> "error" then
|
||||||
|
fail "a running program answered the address root"
|
||||||
|
end;
|
||||||
|
ignore (ask "(:op \"close\")");
|
||||||
|
Unix.close c;
|
||||||
|
if not
|
||||||
|
(await ~ms:5000 (fun () ->
|
||||||
|
match Unix.waitpid [ Unix.WNOHANG ] ppid with
|
||||||
|
| 0, _ -> false
|
||||||
|
| _ -> true
|
||||||
|
| exception Unix.Unix_error _ -> true))
|
||||||
|
then begin
|
||||||
|
(try Unix.kill ppid Sys.sigkill with Unix.Unix_error _ -> ());
|
||||||
|
(try ignore (Unix.waitpid [] ppid) with Unix.Unix_error _ -> ())
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
|
||||||
(* ── The globals a stopped stack reaches ───────────────────────── *)
|
(* ── The globals a stopped stack reaches ───────────────────────── *)
|
||||||
|
|
||||||
(* The other half of what a break loop can show. Locals are one frame's;
|
(* The other half of what a break loop can show. Locals are one frame's;
|
||||||
|
|||||||
134
vendor/agent/flan_agent.c
vendored
134
vendor/agent/flan_agent.c
vendored
@ -93,6 +93,25 @@ int flan_dev_watch_read(uint32_t i, char *nd, uint64_t ncap,
|
|||||||
uint64_t flan_dev_watch_name_cap(void);
|
uint64_t flan_dev_watch_name_cap(void);
|
||||||
uint64_t flan_dev_watch_val_cap(void);
|
uint64_t flan_dev_watch_val_cap(void);
|
||||||
|
|
||||||
|
/* The allocation registry, read the same way and on the same thread. The
|
||||||
|
* renderer's two questions — is this address live, and what died here — are
|
||||||
|
* asked from inside a compiled thunk and never come through this file; these
|
||||||
|
* three are the *reader's* side, which has no (Ptr T) to read a type off and
|
||||||
|
* so has to ask the table what it recorded.
|
||||||
|
*
|
||||||
|
* Formatting lives here rather than in flan_dev.c for the reason [watch] and
|
||||||
|
* [result] already have: this runs on the listener thread, where allocating
|
||||||
|
* and snprintf are legal, and what the game thread writes stays a fixed
|
||||||
|
* table nobody has to format to fill. */
|
||||||
|
int32_t flan_dev_reg_at(const void *p, const char **type, int64_t *typelen,
|
||||||
|
int64_t *off, int64_t *bytes, int64_t *elem,
|
||||||
|
int64_t *seq, int64_t *died);
|
||||||
|
int64_t flan_dev_reg_by_type(int32_t live_only, int64_t *counts,
|
||||||
|
int64_t *bytes, const char **types,
|
||||||
|
int64_t *typelens, int64_t cap);
|
||||||
|
int flan_dev_reg_enabled(void);
|
||||||
|
int flan_dev_reg_overflowed(void);
|
||||||
|
|
||||||
/* A ring the listener writes and the game thread reads. One producer, one
|
/* A ring the listener writes and the game thread reads. One producer, one
|
||||||
* consumer, so two atomics and no lock — the game thread must never block on
|
* consumer, so two atomics and no lock — the game thread must never block on
|
||||||
* the loader.
|
* the loader.
|
||||||
@ -975,6 +994,121 @@ static void handle_line(char *line, sink *o) {
|
|||||||
free(v);
|
free(v);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* "reg at ADDR" — what the allocation registry records for one address.
|
||||||
|
*
|
||||||
|
* The reader's side of the table, and the one question the renderer's own
|
||||||
|
* two cannot answer. Inside a thunk the type at the far end of a pointer is
|
||||||
|
* already static — (Ptr Enemy) says Enemy — so [reg-live] and [reg-emit]
|
||||||
|
* only ever needed permission. Somebody pointing at a bare address has no
|
||||||
|
* (Ptr T) to read a type off, and the recorded name is the whole of what
|
||||||
|
* there is to go on.
|
||||||
|
*
|
||||||
|
* Answered while the program is running as well as while it is stopped:
|
||||||
|
* this reads a table, not a stack, and nothing here walks a chain another
|
||||||
|
* thread is pushing. Whether the *answer* holds still long enough to be
|
||||||
|
* worth acting on is the caller's judgement, and the daemon makes it.
|
||||||
|
*
|
||||||
|
* ADDR is read with base 0, so both 0x-hex and decimal arrive; an editor
|
||||||
|
* that has an address as text has it in one of those two spellings. */
|
||||||
|
if (strncmp(line, "reg at ", 7) == 0) {
|
||||||
|
const char *type = NULL;
|
||||||
|
int64_t typelen = 0, off = 0, bytes = 0, elem = 0, seq = 0, died = 0;
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long long a;
|
||||||
|
if (!flan_dev_reg_enabled()) {
|
||||||
|
reply(o, "err the allocation registry is off; this is not a dev build\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
a = strtoull(line + 7, &end, 0);
|
||||||
|
if (end == line + 7 || a == 0) {
|
||||||
|
reply(o, "err reg at wants an address\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!flan_dev_reg_at((const void *)(uintptr_t)a, &type, &typelen, &off,
|
||||||
|
&bytes, &elem, &seq, &died)) {
|
||||||
|
/* Never heard of it, which is a fact and not a failure: a stack local,
|
||||||
|
* a global, or a pointer from C. The daemon says which of those it
|
||||||
|
* might be; this says only that the table has nothing. */
|
||||||
|
reply(o, "none\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char hdr[128];
|
||||||
|
int k = snprintf(hdr, sizeof hdr, "ok %d %lld %lld %lld %lld %lld\t",
|
||||||
|
died == 0 ? 1 : 0, (long long)off, (long long)bytes,
|
||||||
|
(long long)elem, (long long)seq, (long long)died);
|
||||||
|
if (k > 0) emit(o, hdr, (size_t)k);
|
||||||
|
/* Last, and after a tab, because a type spelling holds spaces —
|
||||||
|
* "(Vec i32)" — and nothing else on the line does. It cannot hold a tab
|
||||||
|
* or a newline: it is Types.to_string of a type the programmer wrote. */
|
||||||
|
if (typelen > 0) emit(o, type, (size_t)typelen);
|
||||||
|
reply(o, "\n");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* "reg types" — the whole table grouped by type spelling; "reg leaks" — the
|
||||||
|
* same walk with the dead left out.
|
||||||
|
*
|
||||||
|
* One verb would have done with a flag, and two exist because the two
|
||||||
|
* questions are asked at different moments and read differently: a
|
||||||
|
* breakdown is "what is this program made of", a leak report is "what is
|
||||||
|
* still held". The *walk* is one function in flan_dev.c for exactly that
|
||||||
|
* reason — two of them would drift.
|
||||||
|
*
|
||||||
|
* A header first, like [watch]: how many rows follow, and whether the table
|
||||||
|
* ever overflowed. The second is not decoration — an overflowed table has
|
||||||
|
* blocks in the program that are in nobody's row, so every number below is
|
||||||
|
* a floor, and a reader that could not tell would quote them as counts. */
|
||||||
|
if (strcmp(line, "reg types") == 0 || strcmp(line, "reg leaks") == 0) {
|
||||||
|
enum { REG_ROWS = 256 };
|
||||||
|
/* Allocated here and freed before the reply is finished, not declared as
|
||||||
|
* static arrays. 256 rows of four words is 8KB, and static would put that
|
||||||
|
* 8KB in the BSS of every build this package is linked into — including a
|
||||||
|
* release build of a game that imports the agent, which never writes a
|
||||||
|
* row. That is flan_dev.c's own argument against a fixed table, at a
|
||||||
|
* thirty-second of the size, and it is the same rule: a release build
|
||||||
|
* carries a null pointer and the declarations.
|
||||||
|
*
|
||||||
|
* Allocating is legal here for [watch]'s reason and no other: this runs
|
||||||
|
* on the listener thread, or on the compiler thread in a merged build.
|
||||||
|
* The table the game thread writes is a fixed static in flan_dev.c
|
||||||
|
* precisely so that *it* allocates nothing. */
|
||||||
|
int64_t *counts = malloc(REG_ROWS * sizeof *counts);
|
||||||
|
int64_t *bytes = malloc(REG_ROWS * sizeof *bytes);
|
||||||
|
int64_t *typelens = malloc(REG_ROWS * sizeof *typelens);
|
||||||
|
const char **types = malloc(REG_ROWS * sizeof *types);
|
||||||
|
int64_t n, i;
|
||||||
|
int live_only = line[4] == 'l';
|
||||||
|
if (counts == NULL || bytes == NULL || typelens == NULL || types == NULL) {
|
||||||
|
free(counts); free(bytes); free(typelens); free(types);
|
||||||
|
reply(o, "err out of memory reading the allocation registry\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!flan_dev_reg_enabled()) {
|
||||||
|
free(counts); free(bytes); free(typelens); free(types);
|
||||||
|
reply(o, "err the allocation registry is off; this is not a dev build\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
n = flan_dev_reg_by_type(live_only ? 1 : 0, counts, bytes, types, typelens,
|
||||||
|
REG_ROWS);
|
||||||
|
{
|
||||||
|
char hdr[64];
|
||||||
|
int k = snprintf(hdr, sizeof hdr, "%lld %d\n",
|
||||||
|
(long long)(n < REG_ROWS ? n : REG_ROWS),
|
||||||
|
flan_dev_reg_overflowed());
|
||||||
|
if (k > 0) emit(o, hdr, (size_t)k);
|
||||||
|
}
|
||||||
|
for (i = 0; i < n && i < REG_ROWS; i++) {
|
||||||
|
char row[64];
|
||||||
|
int k = snprintf(row, sizeof row, "%lld %lld\t", (long long)counts[i],
|
||||||
|
(long long)bytes[i]);
|
||||||
|
if (k > 0) emit(o, row, (size_t)k);
|
||||||
|
if (typelens[i] > 0) emit(o, types[i], (size_t)typelens[i]);
|
||||||
|
reply(o, "\n");
|
||||||
|
}
|
||||||
|
free(counts); free(bytes); free(typelens); free(types);
|
||||||
|
return;
|
||||||
|
}
|
||||||
/* Before the dlopen, not after it: a module there is no room to queue is
|
/* Before the dlopen, not after it: a module there is no room to queue is
|
||||||
* one there is no point relocating, and refusing here means no handle is
|
* one there is no point relocating, and refusing here means no handle is
|
||||||
* taken for it at all. Only one producer runs at a time, so room seen now is
|
* taken for it at all. Only one producer runs at a time, so room seen now is
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user