A global is program state a frame happened to touch, not part of it, so nesting it under one implies an ownership that is not there and repeats the name once per frame that reads it. One section instead, holding the union of the globals every frame on the stack references — the compiler does the choosing, since Reach.expr_refs already answers a body's reference set, and listing every global a program has would bury the one that matters under the prelude's PRNG state. Each entry says which frames touch it, by the index the stack section already numbers them with, which recovers what per-frame nesting would have told you at no cost in duplication. Ordered by the innermost frame that touches it: a deep stack makes the union large and proximity to the error is what puts the likely culprit on top. Simpler than locals, because a global is reached by name rather than by address. Emit.redefinition writes a global the host has as external, so the thunk binds to the program's own storage and nothing is asked of the stopped thread — no dev-slot round trip and no not-yet-bound case to refuse. A frame that cannot be attributed contributes nothing and is named in :skipped; the union being incomplete and the union being complete are different answers. The hole in that is stated rather than papered over: slot_fingerprint hashes a body's slots, which is the right cut for locals and not for this, so a body that names different globals while binding the same locals is not caught. The test drives the case that is. MANUAL.md also loses a stale paragraph claiming the fingerprint check never fires with a failing test pinned to it. It fires, and test_dev covers it.
40 lines
1.4 KiB
Plaintext
40 lines
1.4 KiB
Plaintext
;;;; A program that stops with state worth looking at *outside* the frame.
|
|
;;;;
|
|
;;;; dev-locals.flan is about what one frame holds. This is about what the
|
|
;;;; whole stopped stack is reading, which in this language is most of the
|
|
;;;; program: a game keeps its state in top-level defvars, and sand.flan holds
|
|
;;;; its entire grid that way.
|
|
;;;;
|
|
;;;; The globals are declared in an order the answer must *not* come back in.
|
|
;;;; [label] is written first and is touched only by [main], the outer frame,
|
|
;;;; so ordering by the innermost frame that touches it has to put it last;
|
|
;;;; [grid] and [pressure] are both innermost-touched and must keep the order
|
|
;;;; they are declared in. [untouched] is read by no frame on the stack and
|
|
;;;; must not appear at all — that is the whole claim of scoping the section to
|
|
;;;; the stack rather than listing everything the program has.
|
|
(import agent "vendor:agent")
|
|
|
|
(defstruct Boom [why i32])
|
|
|
|
(defvar label string)
|
|
(defvar grid [4 i32])
|
|
(defvar pressure i64)
|
|
(defvar untouched i64 99)
|
|
|
|
;; The inner frame. It writes two globals and then errors with nothing
|
|
;; handling the condition, so the program stops here with [main] under it.
|
|
(defn inner [] i64
|
|
(set pressure 12)
|
|
(set (at grid 0) 7)
|
|
(error (Boom {.why 3}))
|
|
0)
|
|
|
|
(defn main [] i32
|
|
(agent/start "/tmp/flan-dev-globals-fallback.sock")
|
|
(set label "running")
|
|
(set (at grid 1) 5)
|
|
(print (inner)) (println "")
|
|
(dotimes [i 4000]
|
|
(agent/wait 5))
|
|
0)
|