(watch "name" v) renders any value into the watch table the way print renders it
This commit is contained in:
parent
aa5052e401
commit
85b9f35a66
12
TODO.org
12
TODO.org
@ -1557,10 +1557,14 @@ Push was chosen partly because polling costs a compile, and that premise weakene
|
||||
when the processes merged. The table-and-read design stands; whether it should stay
|
||||
pushed is open.
|
||||
|
||||
** TODO A watch over a struct or a slice
|
||||
Scalars work today through four runtime entry points and need no compiler change.
|
||||
A struct or a slice needs a compile-time walk over its type — one arm beside
|
||||
=print=.
|
||||
** DONE A watch over a struct or a slice
|
||||
CLOSED: [2026-09-25]
|
||||
=(watch "name" v)= is a checker arm beside =print=, sharing its render context
|
||||
with the emitter aimed at the watch slot, so any value watches as it prints. The
|
||||
value is evaluated once, before the table is asked whether it is armed, so the
|
||||
program behaves the same with or without a watch buffer open. The =declare-c=
|
||||
scalar entry points stay. See docs/BUILT.md, "The scalar entry points, and the
|
||||
form for everything else".
|
||||
|
||||
** DONE Two ways to root a walk
|
||||
The inspector takes a frame and a slot index as well as an expression. An index is
|
||||
|
||||
@ -5441,52 +5441,44 @@ So the cost of a watch call in a program nobody is debugging is **one relaxed lo
|
||||
is the same number in a release build as in a dev one: `flan_dev.c` is linked into every build (`Build`, which says
|
||||
why), so the symbols resolve either way and there is no second version of the file.
|
||||
|
||||
It is not *free*, and the distinction is worth keeping honest. Eliding the call entirely needs the compiler to know
|
||||
the form, which is the `check.ml` arm below. A load and a branch per watched value per frame is the real number.
|
||||
It is not *free*, and the distinction is worth keeping honest. The `(watch ...)` form below still makes the call, and
|
||||
still evaluates its value, so a load and a branch per watched value per frame is the real number.
|
||||
|
||||
### Scalars work today; composites need a `check.ml` arm that was not built
|
||||
### The scalar entry points, and the form for everything else
|
||||
|
||||
The four entry points a program can reach through `declare-c` are the whole feature for a scalar:
|
||||
Four entry points a program can reach through `declare-c` write a scalar:
|
||||
|
||||
```flan
|
||||
(declare-c watch-i64 [name string x i64] i32 "flan_dev_watch_i64")
|
||||
(watch-i64 "ticks" ticks)
|
||||
```
|
||||
|
||||
No arm in the checker, no new special form, nothing the compiler has to learn. They return `i32` rather than nothing
|
||||
for a blunt reason: `declare-c` refuses a void return outright — "which is not a value C can carry", `shim.ml` — so a
|
||||
function a program can declare has to return something, and since it must, it returns the useful thing: 1 if the value
|
||||
was written, 0 if nobody is watching or the table is full.
|
||||
They return `i32` rather than nothing for a blunt reason: `declare-c` refuses a void return outright — "which is not a
|
||||
value C can carry", `shim.ml` — so a function a program can declare has to return something, and since it must, it
|
||||
returns the useful thing: 1 if the value was written, 0 if nobody is watching or the table is full.
|
||||
|
||||
A composite — a struct, a slice, a union — cannot be reached this way, and that is not a shortcoming of the four. A
|
||||
Flan value carries no header, so nothing at run time can say what it is, and rendering one is a compile-time walk over
|
||||
its *type*. **That is the same reason `C-x C-e` renders in the thunk rather than marshalling anything**, and it is the
|
||||
layout decision's bill, paid in the same place.
|
||||
A composite — a struct, a slice, a union — cannot be reached that way. A Flan value carries no header, so nothing at run
|
||||
time can say what it is, and rendering one is a compile-time walk over its *type*. **That is the same reason `C-x C-e`
|
||||
renders in the thunk rather than marshalling anything**, and it is the layout decision's bill, paid in the same place.
|
||||
|
||||
**The missing piece is one arm in `check.ml`, and it was deliberately not written** — that file is held by another
|
||||
lane. It sits beside `print` (`check.ml:3480`) and is the same shape as it:
|
||||
So `(watch "name" v)` is an arm in `check.ml`, beside `print`, and it is `print` with the emitter aimed elsewhere. The
|
||||
two share `render_ctx`; the watch emitter points each piece at `flan_dev_watch_emit*` rather than at `WriteStdout`, and a
|
||||
dyn value at `flan_dyn_emit_watch`, the dyn printer with the watch slot as its sink. The walk is wrapped in
|
||||
`flan_dev_watch_begin_n` — the name as bytes and a length, since that is how a Flan string crosses — and
|
||||
`flan_dev_watch_end`, and runs only when `begin` answers that the table is armed and has room.
|
||||
|
||||
```
|
||||
| "watch" ->
|
||||
arity loc name 2 args; (* a name and a value *)
|
||||
(* a read, not a move — as print is, for the same reason: (watch "v" v)
|
||||
must not consume a Vec and make that its last showing *)
|
||||
let n = check ctx (List.nth args 0) in (* must be String *)
|
||||
let a = borrowed ctx target (fun () -> check ctx (List.nth args 1)) in
|
||||
(* begin, the walk, end — with the emitter aimed at the four
|
||||
flan_dev_watch_emit_* rather than at WriteStdout *)
|
||||
Render.render { rc with emit = watch_emitter } 0 a
|
||||
```
|
||||
Two choices in it. The value is bound to a slot of the calling frame unless it is already a local or a global, because
|
||||
the walk names its argument once per field and a call would otherwise run once per field; and it is bound *before* the
|
||||
table is asked, so a side effect in the value happens whether or not anyone is watching — the program's behaviour does
|
||||
not depend on an editor window. A string watches quoted, as it renders inside a structure, because a table row is a
|
||||
value and an unquoted `5` could not be told from the number.
|
||||
|
||||
with `flan/watch-begin`, `flan/watch-end` and the four emit functions declared as externs the way `Session.externs`
|
||||
already declares `flan_dev_emit*`. Nothing else has to move: `Render.render` is unchanged, the runtime side is built
|
||||
and tested, and the daemon and the editor cannot tell which kind of caller filled the table.
|
||||
The daemon and the editor cannot tell which kind of caller filled the table.
|
||||
|
||||
~~That arm is also what **ghost text** is gated on.~~ **It was not, and ghost text is built without it** — see
|
||||
"Ghost text finds its anchor in the buffer, not in the table" below. The claim was that values shown inline need a
|
||||
*place* and nothing in the table has one, so a source location would have to be carried per entry, so the call site
|
||||
would have to be generated. True of the table and false of the conclusion: the call site is in the buffer. The
|
||||
composite renderer still wants the form, for its own reason, and it is the only one of the two that does.
|
||||
would have to be generated. True of the table and false of the conclusion: the call site is in the buffer.
|
||||
|
||||
## An error is a value, and there is more than one of them
|
||||
|
||||
@ -5773,7 +5765,7 @@ rather than a discovery.
|
||||
A Flan struct is exactly its C layout. No header, no tag word — deliberately, and it is what makes a struct free and
|
||||
what makes the FFI work. The consequence is stated elsewhere in this file more than once: *a Flan value carries no
|
||||
header, so nothing at run time can say what it is*, which is why a rendering is a compile-time walk over a type and
|
||||
why `(watch "v" v)` cannot reach a composite without an arm in `check.ml`.
|
||||
why `(watch "v" v)` is an arm in `check.ml` rather than a function.
|
||||
|
||||
The allocation registry does not answer that question. It sidesteps it. **The allocator's caller knows the type at the
|
||||
moment it asks for memory**, and the compiler is standing right there, so a dev build writes it down: base address,
|
||||
|
||||
@ -684,19 +684,21 @@ closes it down, and killing the buffer does the same.
|
||||
|
||||
**The program decides what is shown.** There is no watch list to maintain, no
|
||||
per-variable registration, no place to type an expression. The program says
|
||||
what it wants seen, from inside its own loop:
|
||||
what it wants seen, from inside its own loop, with `(watch "name" value)`:
|
||||
|
||||
```flan
|
||||
(declare-c watch-i64 [name string x i64] i32 "flan_dev_watch_i64")
|
||||
(declare-c watch-f64 [name string x f64] i32 "flan_dev_watch_f64")
|
||||
(declare-c watch-str [name string s string] i32 "flan_dev_watch_str")
|
||||
|
||||
(defn step [] i64
|
||||
(set ticks (+ ticks 1))
|
||||
(watch-i64 "ticks" ticks)
|
||||
(watch "ticks" ticks)
|
||||
(watch "player" player)
|
||||
ticks)
|
||||
```
|
||||
|
||||
The value is rendered the way `print` renders it, so a struct, an array, a
|
||||
slice, an option or a dyn value watches as it prints: `(Pos {.x 3 .y 1.5})`,
|
||||
`[ 1 2 3]`. A string is quoted. The value is evaluated once whether or not a
|
||||
watch buffer is open.
|
||||
|
||||
That is the whole of it. `C-c C-c` on `step` adds or removes a watched value
|
||||
the same way it changes anything else, so the watch list is edited in the place
|
||||
you were already looking.
|
||||
@ -749,11 +751,18 @@ move again, so the two most useful of the five would go dead exactly when you
|
||||
start playing. A whole number prints as one, because a spy on an array index
|
||||
reading `66.0000` sends you looking for a rounding bug that is not there.
|
||||
|
||||
**Scalars only, so far.** `i64`, `u64`, `f64` and `string` have entry points; a
|
||||
struct or a slice does not. That is not an oversight in the runtime — a Flan
|
||||
value carries no header, so rendering one is a walk over its *type* at compile
|
||||
time, and a `(watch "hp" hp)` form in the compiler is what would do that walk.
|
||||
It is not built. `flan-watch.el` says what it would need.
|
||||
**The scalar entry points.** `watch` is a form the compiler knows. The same
|
||||
table is also reachable as plain C functions, one per scalar type, which a
|
||||
program declares like any other:
|
||||
|
||||
```flan
|
||||
(declare-c watch-i64 [name string x i64] i32 "flan_dev_watch_i64")
|
||||
(declare-c watch-f64 [name string x f64] i32 "flan_dev_watch_f64")
|
||||
(declare-c watch-str [name string s string] i32 "flan_dev_watch_str")
|
||||
```
|
||||
|
||||
Each returns 1 if the value was written and 0 if nobody is watching or the
|
||||
table is full.
|
||||
|
||||
### Ghost text — the same values, inline
|
||||
|
||||
|
||||
@ -62,19 +62,17 @@
|
||||
;; insert, because it diffs, so point and scroll survive a repaint instead of
|
||||
;; being yanked to the top five times a second.
|
||||
;;
|
||||
;; What a program writes today, with no compiler change:
|
||||
;;
|
||||
;; (declare-c watch-i64 [name string x i64] i32 "flan_dev_watch_i64")
|
||||
;; (declare-c watch-f64 [name string x f64] i32 "flan_dev_watch_f64")
|
||||
;; What a program writes:
|
||||
;;
|
||||
;; (defn step [] i64
|
||||
;; (set ticks (+ ticks 1))
|
||||
;; (watch-i64 "ticks" ticks)
|
||||
;; (watch "ticks" ticks)
|
||||
;; (watch "player" player)
|
||||
;; ticks)
|
||||
;;
|
||||
;; Scalars only, so far. A struct or a slice needs a compile-time walk over
|
||||
;; its type — a `(watch "hp" hp)' form in the checker — and that is a file this
|
||||
;; change does not own. See docs/BUILT.md.
|
||||
;; `watch' renders any value the way `print' does — a struct, a slice, a dyn
|
||||
;; value — into the table. The scalar entry points underneath it,
|
||||
;; `flan_dev_watch_i64' and the rest, are reachable through `declare-c' too.
|
||||
|
||||
;;; Code:
|
||||
|
||||
@ -118,11 +116,9 @@ was the only consumer and wrong the moment it was not.")
|
||||
"The buffer's text for ROWS. OVERFLOW means some name found no slot."
|
||||
(if (null rows)
|
||||
(concat "nothing is being watched\n\n"
|
||||
"The program decides what is shown. Call into the watch table\n"
|
||||
"The program decides what is shown. Write into the watch table\n"
|
||||
"from your own loop:\n\n"
|
||||
" (declare-c watch-i64 [name string x i64] i32 \"flan_dev_watch_i64\")\n"
|
||||
" ...\n"
|
||||
" (watch-i64 \"ticks\" ticks)\n")
|
||||
" (watch \"ticks\" ticks)\n")
|
||||
(let ((w (apply #'max (mapcar (lambda (r) (length (car r))) rows))))
|
||||
(concat
|
||||
(mapconcat (lambda (r)
|
||||
|
||||
107
lib/check.ml
107
lib/check.ml
@ -571,6 +571,26 @@ let fresh_slot ?name ctx ty =
|
||||
ctx.slot_names <- name :: ctx.slot_names;
|
||||
s
|
||||
|
||||
(* The structural printer's context over this checker's tables, with the
|
||||
pieces aimed wherever [emit] sends them. [print] and [watch] differ in the
|
||||
emitter and in nothing else, and a second copy of this would be a second
|
||||
answer to which types the walk knows. *)
|
||||
let render_ctx ctx (emit : Render.emitter) : Render.ctx =
|
||||
{ Render.structs = Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.structs [];
|
||||
datas = Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.datas [];
|
||||
unions = Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.unions [];
|
||||
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) ctx.env.enums [];
|
||||
emit;
|
||||
(* Neither [println] nor [watch] follows a pointer, and the allocation
|
||||
registry does not change that. spec-memory.md fixes what it prints —
|
||||
"Ptr and Handle print their address or identity rather than
|
||||
recursively dereferencing" — and a printed line belongs to the program,
|
||||
so it must read the same in a release build, where there is no registry
|
||||
to ask. Following one is the *inspector's* move, and session.ml is where
|
||||
that context is built. *)
|
||||
ptrs = None;
|
||||
alloc = (fun ty -> fresh_slot ctx ty) }
|
||||
|
||||
(* Shadowing is legal -- [(let [v 11] (let [v 22] ...))] is two slots, both
|
||||
named [v] -- and the debug info has nowhere to put the distinction. Every
|
||||
[!DILocalVariable] is scoped to the subprogram, because the typed IR has no
|
||||
@ -8621,23 +8641,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
||||
ef64 = (fun x -> write (conv Tast.F64ToBytes x));
|
||||
edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_print", [ x ]))) }
|
||||
in
|
||||
let rc =
|
||||
{ Render.structs =
|
||||
Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.structs [];
|
||||
datas = Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.datas [];
|
||||
unions = Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.unions [];
|
||||
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) ctx.env.enums [];
|
||||
emit = emitter;
|
||||
(* [println] never follows a pointer, and the allocation registry does
|
||||
not change that. spec-memory.md fixes what it prints — "Ptr and
|
||||
Handle print their address or identity rather than recursively
|
||||
dereferencing" — and a printed line belongs to the program, so it
|
||||
must read the same in a release build, where there is no registry to
|
||||
ask. Following one is the *inspector's* move, and session.ml is
|
||||
where that context is built. *)
|
||||
ptrs = None;
|
||||
alloc = (fun ty -> fresh_slot ctx ty) }
|
||||
in
|
||||
let rc = render_ctx ctx emitter in
|
||||
let render_one a =
|
||||
match a.Tast.ty with
|
||||
| Types.String | Types.Slice (Types.Int Types.U8) ->
|
||||
@ -8667,6 +8671,71 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
||||
else []
|
||||
in
|
||||
expect ctx loc ~want (mk loc Types.Unit (Tast.Do (parts @ nl)))
|
||||
(* (watch "name" v) — v rendered into the dev watch table under the name.
|
||||
|
||||
The same walk as [print], with the pieces aimed at flan_dev.c's watch
|
||||
slot instead of stdout, so a struct, a slice, an option or a dyn value
|
||||
watches the way it prints. flan_dev.c is linked into every build, so this
|
||||
compiles the same in a release one, where nothing ever arms the table.
|
||||
|
||||
The value is evaluated once, before the table is asked whether anyone is
|
||||
looking, so a side effect in it happens whether or not a watch buffer is
|
||||
open. The walk runs only when one is: [flan_dev_watch_begin_n] answers 0
|
||||
when the table is not armed or is full, and the render is skipped. *)
|
||||
| "watch" ->
|
||||
arity ctx loc name 2 args;
|
||||
let label = check ctx ~want:Types.String (List.hd args) in
|
||||
let v = check ctx (List.nth args 1) in
|
||||
if generic_ty v.Tast.ty then mk loc Types.Unit Tast.Unit
|
||||
else begin
|
||||
let unit_rt sym args = mk loc Types.Unit (Tast.Prim (Tast.Rt sym, args)) in
|
||||
let bslice = Types.Slice (Types.Int Types.U8) in
|
||||
let emitter : Render.emitter =
|
||||
{ Render.ebytes = (fun x -> unit_rt "flan_dev_watch_emit" [ x ]);
|
||||
estr = (fun x -> unit_rt "flan_dev_watch_emit_str" [ x ]);
|
||||
ei64 = (fun x -> unit_rt "flan_dev_watch_emit_i64" [ x ]);
|
||||
eu64 = (fun x -> unit_rt "flan_dev_watch_emit_u64" [ x ]);
|
||||
ef64 = (fun x -> unit_rt "flan_dev_watch_emit_f64" [ x ]);
|
||||
edyn = (fun x -> unit_rt "flan_dyn_emit_watch" [ x ]) }
|
||||
in
|
||||
(* A place is read where it stands; anything else is bound to a slot of
|
||||
this frame first, so the walk — which names its argument once per
|
||||
field — does not run it once per field. *)
|
||||
let bind, value =
|
||||
match v.Tast.e with
|
||||
| Tast.Local _ | Tast.Global _ -> [], v
|
||||
| _ ->
|
||||
let s = fresh_slot ctx v.Tast.ty in
|
||||
[ (s, v) ], mk loc v.Tast.ty (Tast.Local s)
|
||||
in
|
||||
let body =
|
||||
match value.Tast.ty with
|
||||
(* A string watches quoted, as it renders inside a structure: the
|
||||
table's rows are values, and an unquoted one could not be told from
|
||||
a number. *)
|
||||
| Types.String ->
|
||||
[ emitter.Render.estr (mk loc bslice (Tast.Prim (Tast.Bytes, [ value ]))) ]
|
||||
| _ -> Render.render (render_ctx ctx emitter) 0 value
|
||||
in
|
||||
let begin_ =
|
||||
mk loc (Types.Int Types.I32)
|
||||
(Tast.Prim (Tast.Rt "flan_dev_watch_begin_n", [ label ]))
|
||||
in
|
||||
let zero = mk loc (Types.Int Types.I32) (Tast.Int (0L, Types.I32)) in
|
||||
let open_ = mk loc Types.Bool (Tast.Prim (Tast.Ne, [ begin_; zero ])) in
|
||||
let guarded =
|
||||
mk loc Types.Unit
|
||||
(Tast.If
|
||||
( open_,
|
||||
mk loc Types.Unit
|
||||
(Tast.Do (body @ [ unit_rt "flan_dev_watch_end" [] ])),
|
||||
mk loc Types.Unit Tast.Unit ))
|
||||
in
|
||||
expect ctx loc ~want
|
||||
(match bind with
|
||||
| [] -> guarded
|
||||
| _ -> mk loc Types.Unit (Tast.Let (bind, [ guarded ])))
|
||||
end
|
||||
| "exit" ->
|
||||
arity ctx loc name 1 args;
|
||||
prim Tast.Exit Types.Never [ check ctx ~want:index_ty (List.hd args) ]
|
||||
@ -9951,6 +10020,10 @@ let builtins : (string * string * string) list =
|
||||
is a read, so it does not consume the value.");
|
||||
("println", "println [T ...] ()",
|
||||
"print, with a newline after it — (println) alone is the newline.");
|
||||
("watch", "watch [string T] ()",
|
||||
"Writes the value, rendered as print renders it, into the dev session's \
|
||||
watch table under the name, where M-x flan-watch shows it. Does nothing \
|
||||
when no watch buffer is open, and in a build with no dev session.");
|
||||
("exit", "exit [i32] never",
|
||||
"Ends the process with this status. It has no value, so nothing written \
|
||||
after it runs.");
|
||||
|
||||
@ -4027,6 +4027,15 @@ declare void @flan_dyn_push(i64, i64)
|
||||
declare void @flan_dyn_print(i64)
|
||||
declare void @flan_dyn_emit_dev(i64)
|
||||
declare void @flan_dyn_emit_watch(i64)
|
||||
; The watch table, which (watch "name" v) renders into. flan_dev.c is linked
|
||||
; into every build, so these resolve in a release build too.
|
||||
declare i32 @flan_dev_watch_begin_n(ptr, i64)
|
||||
declare void @flan_dev_watch_emit(ptr, i64)
|
||||
declare void @flan_dev_watch_emit_str(ptr, i64)
|
||||
declare void @flan_dev_watch_emit_i64(i64)
|
||||
declare void @flan_dev_watch_emit_u64(i64)
|
||||
declare void @flan_dev_watch_emit_f64(double)
|
||||
declare void @flan_dev_watch_end()
|
||||
declare i64 @flan_dyn_need_i64(i64)
|
||||
declare double @flan_dyn_need_f64(i64)
|
||||
declare i32 @flan_dyn_need_bool(i64)
|
||||
|
||||
@ -566,8 +566,8 @@ static uint64_t watch_epoch;
|
||||
* build — [flan_dev.c] is linked into both (see [Build], which says why) so the
|
||||
* symbols resolve either way and there is no second version of this file.
|
||||
*
|
||||
* What it is *not*: free. Eliding the call entirely needs the compiler to know
|
||||
* the form, which is a [check.ml] arm this does not have. A load and a branch
|
||||
* What it is *not*: free. The [(watch ...)] form still makes the call, so it
|
||||
* costs the same as a scalar entry point does. A load and a branch
|
||||
* per watched value per frame is the honest number, and it is the same number
|
||||
* in both builds rather than a dev-only tax. */
|
||||
static int watch_on;
|
||||
@ -639,6 +639,22 @@ int flan_dev_watch_begin(const char *name) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* [flan_dev_watch_begin] for a name that arrives as bytes and a length, which
|
||||
* is how a Flan string crosses to the runtime: (watch "name" v) calls this.
|
||||
* The name is copied to a NUL-terminated buffer on the stack, cut to what a
|
||||
* slot holds, so nothing here allocates. */
|
||||
int flan_dev_watch_begin_n(const uint8_t *name, int64_t len) {
|
||||
/* The same first test [flan_dev_watch_begin] makes, ahead of the copy, so an
|
||||
* unarmed table costs a load and a branch here too. */
|
||||
if (!__atomic_load_n(&watch_on, __ATOMIC_RELAXED)) { watch_cur = NULL; return 0; }
|
||||
char buf[WATCH_NAME];
|
||||
size_t n = len < 0 ? 0 : (size_t)len;
|
||||
if (n > WATCH_NAME - 1) n = WATCH_NAME - 1;
|
||||
memcpy(buf, name, n);
|
||||
buf[n] = '\0';
|
||||
return flan_dev_watch_begin(buf);
|
||||
}
|
||||
|
||||
void flan_dev_watch_emit(const uint8_t *bytes, int64_t len) {
|
||||
watch_slot *s = watch_cur;
|
||||
if (s == NULL) return;
|
||||
@ -703,14 +719,12 @@ void flan_dev_watch_end(void) {
|
||||
* which is either nobody watching or a full table. A caller is free to ignore
|
||||
* it and normally does.
|
||||
*
|
||||
* A composite — a struct, a slice, a union — cannot be done this way, and that
|
||||
* is not a shortcoming of these four: a Flan value carries no header, so
|
||||
* nothing at run time can say what it is, and rendering one is a compile-time
|
||||
* walk over its *type*. The walk already exists — [Render.render] — and the
|
||||
* four [flan_dev_watch_emit_*] above are the emitter it would be pointed at,
|
||||
* shaped exactly like the [print] arm's. What is missing is the
|
||||
* [(watch "hp" hp)] arm in check.ml that joins the two, which is a file this
|
||||
* change does not own. docs/BUILT.md says what that arm is. */
|
||||
* A composite — a struct, a slice, a union — cannot be done this way: a Flan
|
||||
* value carries no header, so nothing at run time can say what it is, and
|
||||
* rendering one is a compile-time walk over its *type*. That is the
|
||||
* [(watch "hp" hp)] form in check.ml, which points [Render.render] at
|
||||
* [flan_dev_watch_begin_n], the [flan_dev_watch_emit_*] above and
|
||||
* [flan_dev_watch_end]. */
|
||||
int32_t flan_dev_watch_i64(const char *name, int64_t x) {
|
||||
if (!flan_dev_watch_begin(name)) return 0;
|
||||
flan_dev_watch_emit_i64(x);
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
;;;; A program that pushes values into the watch table from its own loop.
|
||||
;;;;
|
||||
;;;; The point of the case is that this needs no compiler change: the watch
|
||||
;;;; entry points are ordinary C functions, so a program reaches them through
|
||||
;;;; [declare-c] the same way it reaches anything else in the runtime. That is
|
||||
;;;; deliberate — a [(watch "hp" hp)] form would be an arm in the checker, and
|
||||
;;;; a scalar does not need one.
|
||||
;;;; Two ways in. A scalar reaches the watch entry points as ordinary C
|
||||
;;;; functions, through [declare-c]. A struct, a slice or anything else goes
|
||||
;;;; through the [(watch "name" v)] form, which renders it the way print does.
|
||||
;;;;
|
||||
;;;; The values are written every iteration and are *not* read back from here.
|
||||
;;;; What reads them is the daemon's [watch] op, over the agent, while this
|
||||
@ -22,6 +20,8 @@
|
||||
|
||||
(defonce ticks i64)
|
||||
|
||||
(defstruct Pos [x i32 y f64])
|
||||
|
||||
(defn loop-cells [] i32
|
||||
(let [i 0]
|
||||
(while (< i 8)
|
||||
@ -37,6 +37,10 @@
|
||||
(watch-i64 "ticks" ticks)
|
||||
(watch-f64 "half" (/ (f64 ticks) 2.0))
|
||||
(watch-str "label" "sand")
|
||||
;; The form: a struct, a slice, and a scalar through the same form.
|
||||
(watch "pos" (Pos {.x 3 .y 1.5}))
|
||||
(watch "row" [1 2 3])
|
||||
(watch "t2" (* ticks 2))
|
||||
;; A hot inner loop, and the value *varies* across it — which is what makes
|
||||
;; the row a test of the accumulator rather than of the plumbing. A slot that
|
||||
;; only kept [last] would report 21 and no range; n, min and max are each
|
||||
|
||||
28
test/programs/watch-release.flan
Normal file
28
test/programs/watch-release.flan
Normal file
@ -0,0 +1,28 @@
|
||||
;;;; (watch "name" v) in a build with no dev session behind it.
|
||||
;;;;
|
||||
;;;; Nothing arms the watch table here, so every watch is the load and the
|
||||
;;;; branch and nothing else: the program must compile, link and print exactly
|
||||
;;;; what it prints without them. The value is still evaluated once, which the
|
||||
;;;; counter shows.
|
||||
|
||||
(defstruct Pos [x i32 y f64])
|
||||
|
||||
(defonce calls i64)
|
||||
|
||||
(defn bump [] i64
|
||||
(set calls (+ calls 1))
|
||||
calls)
|
||||
|
||||
(defn main [] i32
|
||||
(let [p (Pos {.x 3 .y 1.5})
|
||||
xs [1 2 3]
|
||||
d {:a 1}]
|
||||
(watch "pos" p)
|
||||
(watch "xs" xs)
|
||||
(watch "slice" (slice xs 0 2))
|
||||
(watch "dyn" d)
|
||||
(watch "label" "sand")
|
||||
(watch "bump" (bump))
|
||||
(watch "bump" (bump))
|
||||
(println (.x p) calls))
|
||||
0)
|
||||
@ -4397,6 +4397,15 @@ level "1"
|
||||
"programs/dyn-class.flan" dyn_class_out;
|
||||
outputs ~x86:true "dyn: classes and dispatch, --x86"
|
||||
"programs/dyn-class.flan" dyn_class_out;
|
||||
(* (watch "name" v) with nothing arming the table: a struct, an array, a
|
||||
slice, a dyn map and a string all compile against flan_dev.c's watch
|
||||
entry points on both backends, write nothing, and evaluate the value
|
||||
once — [calls] is 2 after two watched calls to [bump]. *)
|
||||
outputs "watch: a release build" "programs/watch-release.flan" "3 2\n";
|
||||
outputs ~opt:"-O0" "watch: a release build, -O0"
|
||||
"programs/watch-release.flan" "3 2\n";
|
||||
outputs ~x86:true "watch: a release build, --x86"
|
||||
"programs/watch-release.flan" "3 2\n";
|
||||
(* ── Per-type descriptors, M2 item 2 ─────────────────────────────
|
||||
The first program anywhere with a dyn field in a struct, which was a
|
||||
refusal until the descriptors landed. It matters at all three rows
|
||||
|
||||
@ -3889,6 +3889,20 @@ let () =
|
||||
| Some "\"sand\"" -> ()
|
||||
| Some v -> fail "watch rendered a string as %s, unquoted" v
|
||||
| None -> fail "watch lost the string");
|
||||
(* The (watch ...) form: a struct and a slice, rendered by the same
|
||||
walk print uses, and a scalar through the same form. *)
|
||||
(match List.assoc_opt "pos" t with
|
||||
| Some "(Pos {.x 3 .y 1.5})" -> ()
|
||||
| Some v -> fail "watch rendered a struct as %s" v
|
||||
| None -> fail "the (watch ...) form never wrote a struct");
|
||||
(match List.assoc_opt "row" t with
|
||||
| Some "[ 1 2 3]" -> ()
|
||||
| Some v -> fail "watch rendered a slice as %s" v
|
||||
| None -> fail "the (watch ...) form never wrote a slice");
|
||||
(match List.assoc_opt "t2" t with
|
||||
| Some v when int_of_string_opt v <> None -> ()
|
||||
| Some v -> fail "watch rendered a computed i64 as %s" v
|
||||
| None -> fail "the (watch ...) form never wrote a scalar");
|
||||
(* The accumulator, which is the other half of the watch and the
|
||||
half a scalar row cannot stand in for. [loop-cells] samples "cell"
|
||||
eight times per step at 0, 3, ... 21, so the row has to show a
|
||||
@ -5633,8 +5647,12 @@ let () =
|
||||
failure above would leave behind — and this fixture then polls for
|
||||
twenty seconds and parks, so the wait below would be a hang rather
|
||||
than a report. The signal is for that case only. *)
|
||||
if status (request c "(:op \"abort\")") <> "ok" then
|
||||
(try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ());
|
||||
(* Through [aborted], because the connection closing before the reply
|
||||
is an abort that worked; see there. *)
|
||||
(match aborted c with
|
||||
| Some r when status r <> "ok" ->
|
||||
(try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||
| _ -> ());
|
||||
(try Unix.close c with Unix.Unix_error _ -> ());
|
||||
(try ignore (Unix.waitpid [] hpid) with Unix.Unix_error _ -> ())
|
||||
end;
|
||||
|
||||
@ -213,6 +213,9 @@ let corpus =
|
||||
immortal and is not a collector object. If that reasoning is wrong,
|
||||
50000 instances past the one-megabyte floor is where ASan says so. *)
|
||||
"programs/dyn-class.flan", [];
|
||||
(* (watch ...) with the table unarmed: every value is bound and every
|
||||
call reaches flan_dev_watch_begin_n, and nothing past it runs. *)
|
||||
"programs/watch-release.flan", [];
|
||||
(* nil <-> None at (Option T), M2 queue item 4: an Option's tag is read
|
||||
with a raw [Field] the surface language never writes (check.ml's
|
||||
[box_option]/[unbox_option], the same access Render's structural
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user