(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
|
when the processes merged. The table-and-read design stands; whether it should stay
|
||||||
pushed is open.
|
pushed is open.
|
||||||
|
|
||||||
** TODO A watch over a struct or a slice
|
** DONE A watch over a struct or a slice
|
||||||
Scalars work today through four runtime entry points and need no compiler change.
|
CLOSED: [2026-09-25]
|
||||||
A struct or a slice needs a compile-time walk over its type — one arm beside
|
=(watch "name" v)= is a checker arm beside =print=, sharing its render context
|
||||||
=print=.
|
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
|
** DONE Two ways to root a walk
|
||||||
The inspector takes a frame and a slot index as well as an expression. An index is
|
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
|
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.
|
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
|
It is not *free*, and the distinction is worth keeping honest. The `(watch ...)` form below still makes the call, and
|
||||||
the form, which is the `check.ml` arm below. A load and a branch per watched value per frame is the real number.
|
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
|
```flan
|
||||||
(declare-c watch-i64 [name string x i64] i32 "flan_dev_watch_i64")
|
(declare-c watch-i64 [name string x i64] i32 "flan_dev_watch_i64")
|
||||||
(watch-i64 "ticks" ticks)
|
(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
|
They return `i32` rather than nothing for a blunt reason: `declare-c` refuses a void return outright — "which is not a
|
||||||
for a blunt reason: `declare-c` refuses a void return outright — "which is not a value C can carry", `shim.ml` — so a
|
value C can carry", `shim.ml` — so a function a program can declare has to return something, and since it must, it
|
||||||
function a program can declare has to return something, and since it must, it returns the useful thing: 1 if the value
|
returns the useful thing: 1 if the value was written, 0 if nobody is watching or the table is full.
|
||||||
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
|
A composite — a struct, a slice, a union — cannot be reached that way. A Flan value carries no header, so nothing at run
|
||||||
Flan value carries no header, so nothing at run time can say what it is, and rendering one is a compile-time walk over
|
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`
|
||||||
its *type*. **That is the same reason `C-x C-e` renders in the thunk rather than marshalling anything**, and it is the
|
renders in the thunk rather than marshalling anything**, and it is the layout decision's bill, paid in the same place.
|
||||||
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
|
So `(watch "name" v)` is an arm in `check.ml`, beside `print`, and it is `print` with the emitter aimed elsewhere. The
|
||||||
lane. It sits beside `print` (`check.ml:3480`) and is the same shape as it:
|
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.
|
||||||
|
|
||||||
```
|
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
|
||||||
| "watch" ->
|
the walk names its argument once per field and a call would otherwise run once per field; and it is bound *before* the
|
||||||
arity loc name 2 args; (* a name and a value *)
|
table is asked, so a side effect in the value happens whether or not anyone is watching — the program's behaviour does
|
||||||
(* a read, not a move — as print is, for the same reason: (watch "v" v)
|
not depend on an editor window. A string watches quoted, as it renders inside a structure, because a table row is a
|
||||||
must not consume a Vec and make that its last showing *)
|
value and an unquoted `5` could not be told from the number.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
with `flan/watch-begin`, `flan/watch-end` and the four emit functions declared as externs the way `Session.externs`
|
The daemon and the editor cannot tell which kind of caller filled the table.
|
||||||
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.
|
|
||||||
|
|
||||||
~~That arm is also what **ghost text** is gated on.~~ **It was not, and ghost text is built without it** — see
|
~~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
|
"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
|
*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
|
would have to be generated. True of the table and false of the conclusion: the call site is in the buffer.
|
||||||
composite renderer still wants the form, for its own reason, and it is the only one of the two that does.
|
|
||||||
|
|
||||||
## An error is a value, and there is more than one of them
|
## 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
|
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
|
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
|
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
|
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,
|
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
|
**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
|
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
|
```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
|
(defn step [] i64
|
||||||
(set ticks (+ ticks 1))
|
(set ticks (+ ticks 1))
|
||||||
(watch-i64 "ticks" ticks)
|
(watch "ticks" ticks)
|
||||||
|
(watch "player" player)
|
||||||
ticks)
|
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
|
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
|
the same way it changes anything else, so the watch list is edited in the place
|
||||||
you were already looking.
|
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
|
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.
|
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
|
**The scalar entry points.** `watch` is a form the compiler knows. The same
|
||||||
struct or a slice does not. That is not an oversight in the runtime — a Flan
|
table is also reachable as plain C functions, one per scalar type, which a
|
||||||
value carries no header, so rendering one is a walk over its *type* at compile
|
program declares like any other:
|
||||||
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.
|
```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
|
### Ghost text — the same values, inline
|
||||||
|
|
||||||
|
|||||||
@ -62,19 +62,17 @@
|
|||||||
;; insert, because it diffs, so point and scroll survive a repaint instead of
|
;; insert, because it diffs, so point and scroll survive a repaint instead of
|
||||||
;; being yanked to the top five times a second.
|
;; being yanked to the top five times a second.
|
||||||
;;
|
;;
|
||||||
;; What a program writes today, with no compiler change:
|
;; What a program writes:
|
||||||
;;
|
|
||||||
;; (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")
|
|
||||||
;;
|
;;
|
||||||
;; (defn step [] i64
|
;; (defn step [] i64
|
||||||
;; (set ticks (+ ticks 1))
|
;; (set ticks (+ ticks 1))
|
||||||
;; (watch-i64 "ticks" ticks)
|
;; (watch "ticks" ticks)
|
||||||
|
;; (watch "player" player)
|
||||||
;; ticks)
|
;; ticks)
|
||||||
;;
|
;;
|
||||||
;; Scalars only, so far. A struct or a slice needs a compile-time walk over
|
;; `watch' renders any value the way `print' does — a struct, a slice, a dyn
|
||||||
;; its type — a `(watch "hp" hp)' form in the checker — and that is a file this
|
;; value — into the table. The scalar entry points underneath it,
|
||||||
;; change does not own. See docs/BUILT.md.
|
;; `flan_dev_watch_i64' and the rest, are reachable through `declare-c' too.
|
||||||
|
|
||||||
;;; Code:
|
;;; 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."
|
"The buffer's text for ROWS. OVERFLOW means some name found no slot."
|
||||||
(if (null rows)
|
(if (null rows)
|
||||||
(concat "nothing is being watched\n\n"
|
(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"
|
"from your own loop:\n\n"
|
||||||
" (declare-c watch-i64 [name string x i64] i32 \"flan_dev_watch_i64\")\n"
|
" (watch \"ticks\" ticks)\n")
|
||||||
" ...\n"
|
|
||||||
" (watch-i64 \"ticks\" ticks)\n")
|
|
||||||
(let ((w (apply #'max (mapcar (lambda (r) (length (car r))) rows))))
|
(let ((w (apply #'max (mapcar (lambda (r) (length (car r))) rows))))
|
||||||
(concat
|
(concat
|
||||||
(mapconcat (lambda (r)
|
(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;
|
ctx.slot_names <- name :: ctx.slot_names;
|
||||||
s
|
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
|
(* 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
|
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
|
[!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));
|
ef64 = (fun x -> write (conv Tast.F64ToBytes x));
|
||||||
edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_print", [ x ]))) }
|
edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_print", [ x ]))) }
|
||||||
in
|
in
|
||||||
let rc =
|
let rc = render_ctx ctx emitter in
|
||||||
{ 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 render_one a =
|
let render_one a =
|
||||||
match a.Tast.ty with
|
match a.Tast.ty with
|
||||||
| Types.String | Types.Slice (Types.Int Types.U8) ->
|
| Types.String | Types.Slice (Types.Int Types.U8) ->
|
||||||
@ -8667,6 +8671,71 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
|||||||
else []
|
else []
|
||||||
in
|
in
|
||||||
expect ctx loc ~want (mk loc Types.Unit (Tast.Do (parts @ nl)))
|
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" ->
|
| "exit" ->
|
||||||
arity ctx loc name 1 args;
|
arity ctx loc name 1 args;
|
||||||
prim Tast.Exit Types.Never [ check ctx ~want:index_ty (List.hd 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.");
|
is a read, so it does not consume the value.");
|
||||||
("println", "println [T ...] ()",
|
("println", "println [T ...] ()",
|
||||||
"print, with a newline after it — (println) alone is the newline.");
|
"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",
|
("exit", "exit [i32] never",
|
||||||
"Ends the process with this status. It has no value, so nothing written \
|
"Ends the process with this status. It has no value, so nothing written \
|
||||||
after it runs.");
|
after it runs.");
|
||||||
|
|||||||
@ -4027,6 +4027,15 @@ declare void @flan_dyn_push(i64, i64)
|
|||||||
declare void @flan_dyn_print(i64)
|
declare void @flan_dyn_print(i64)
|
||||||
declare void @flan_dyn_emit_dev(i64)
|
declare void @flan_dyn_emit_dev(i64)
|
||||||
declare void @flan_dyn_emit_watch(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 i64 @flan_dyn_need_i64(i64)
|
||||||
declare double @flan_dyn_need_f64(i64)
|
declare double @flan_dyn_need_f64(i64)
|
||||||
declare i32 @flan_dyn_need_bool(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
|
* 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.
|
* 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
|
* What it is *not*: free. The [(watch ...)] form still makes the call, so it
|
||||||
* the form, which is a [check.ml] arm this does not have. A load and a branch
|
* 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
|
* per watched value per frame is the honest number, and it is the same number
|
||||||
* in both builds rather than a dev-only tax. */
|
* in both builds rather than a dev-only tax. */
|
||||||
static int watch_on;
|
static int watch_on;
|
||||||
@ -639,6 +639,22 @@ int flan_dev_watch_begin(const char *name) {
|
|||||||
return 1;
|
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) {
|
void flan_dev_watch_emit(const uint8_t *bytes, int64_t len) {
|
||||||
watch_slot *s = watch_cur;
|
watch_slot *s = watch_cur;
|
||||||
if (s == NULL) return;
|
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
|
* which is either nobody watching or a full table. A caller is free to ignore
|
||||||
* it and normally does.
|
* it and normally does.
|
||||||
*
|
*
|
||||||
* A composite — a struct, a slice, a union — cannot be done this way, and that
|
* A composite — a struct, a slice, a union — cannot be done this way: a Flan
|
||||||
* is not a shortcoming of these four: a Flan value carries no header, so
|
* value carries no header, so nothing at run time can say what it is, and
|
||||||
* nothing at run time can say what it is, and rendering one is a compile-time
|
* rendering one is a compile-time walk over its *type*. That is the
|
||||||
* walk over its *type*. The walk already exists — [Render.render] — and the
|
* [(watch "hp" hp)] form in check.ml, which points [Render.render] at
|
||||||
* four [flan_dev_watch_emit_*] above are the emitter it would be pointed at,
|
* [flan_dev_watch_begin_n], the [flan_dev_watch_emit_*] above and
|
||||||
* shaped exactly like the [print] arm's. What is missing is the
|
* [flan_dev_watch_end]. */
|
||||||
* [(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. */
|
|
||||||
int32_t flan_dev_watch_i64(const char *name, int64_t x) {
|
int32_t flan_dev_watch_i64(const char *name, int64_t x) {
|
||||||
if (!flan_dev_watch_begin(name)) return 0;
|
if (!flan_dev_watch_begin(name)) return 0;
|
||||||
flan_dev_watch_emit_i64(x);
|
flan_dev_watch_emit_i64(x);
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
;;;; A program that pushes values into the watch table from its own loop.
|
;;;; 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
|
;;;; Two ways in. A scalar reaches the watch entry points as ordinary C
|
||||||
;;;; entry points are ordinary C functions, so a program reaches them through
|
;;;; functions, through [declare-c]. A struct, a slice or anything else goes
|
||||||
;;;; [declare-c] the same way it reaches anything else in the runtime. That is
|
;;;; through the [(watch "name" v)] form, which renders it the way print does.
|
||||||
;;;; deliberate — a [(watch "hp" hp)] form would be an arm in the checker, and
|
|
||||||
;;;; a scalar does not need one.
|
|
||||||
;;;;
|
;;;;
|
||||||
;;;; The values are written every iteration and are *not* read back from here.
|
;;;; 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
|
;;;; What reads them is the daemon's [watch] op, over the agent, while this
|
||||||
@ -22,6 +20,8 @@
|
|||||||
|
|
||||||
(defonce ticks i64)
|
(defonce ticks i64)
|
||||||
|
|
||||||
|
(defstruct Pos [x i32 y f64])
|
||||||
|
|
||||||
(defn loop-cells [] i32
|
(defn loop-cells [] i32
|
||||||
(let [i 0]
|
(let [i 0]
|
||||||
(while (< i 8)
|
(while (< i 8)
|
||||||
@ -37,6 +37,10 @@
|
|||||||
(watch-i64 "ticks" ticks)
|
(watch-i64 "ticks" ticks)
|
||||||
(watch-f64 "half" (/ (f64 ticks) 2.0))
|
(watch-f64 "half" (/ (f64 ticks) 2.0))
|
||||||
(watch-str "label" "sand")
|
(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
|
;; 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
|
;; 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
|
;; 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;
|
"programs/dyn-class.flan" dyn_class_out;
|
||||||
outputs ~x86:true "dyn: classes and dispatch, --x86"
|
outputs ~x86:true "dyn: classes and dispatch, --x86"
|
||||||
"programs/dyn-class.flan" dyn_class_out;
|
"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 ─────────────────────────────
|
(* ── Per-type descriptors, M2 item 2 ─────────────────────────────
|
||||||
The first program anywhere with a dyn field in a struct, which was a
|
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
|
refusal until the descriptors landed. It matters at all three rows
|
||||||
|
|||||||
@ -3889,6 +3889,20 @@ let () =
|
|||||||
| Some "\"sand\"" -> ()
|
| Some "\"sand\"" -> ()
|
||||||
| Some v -> fail "watch rendered a string as %s, unquoted" v
|
| Some v -> fail "watch rendered a string as %s, unquoted" v
|
||||||
| None -> fail "watch lost the string");
|
| 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
|
(* The accumulator, which is the other half of the watch and the
|
||||||
half a scalar row cannot stand in for. [loop-cells] samples "cell"
|
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
|
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
|
failure above would leave behind — and this fixture then polls for
|
||||||
twenty seconds and parks, so the wait below would be a hang rather
|
twenty seconds and parks, so the wait below would be a hang rather
|
||||||
than a report. The signal is for that case only. *)
|
than a report. The signal is for that case only. *)
|
||||||
if status (request c "(:op \"abort\")") <> "ok" then
|
(* Through [aborted], because the connection closing before the reply
|
||||||
(try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ());
|
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 Unix.close c with Unix.Unix_error _ -> ());
|
||||||
(try ignore (Unix.waitpid [] hpid) with Unix.Unix_error _ -> ())
|
(try ignore (Unix.waitpid [] hpid) with Unix.Unix_error _ -> ())
|
||||||
end;
|
end;
|
||||||
|
|||||||
@ -213,6 +213,9 @@ let corpus =
|
|||||||
immortal and is not a collector object. If that reasoning is wrong,
|
immortal and is not a collector object. If that reasoning is wrong,
|
||||||
50000 instances past the one-megabyte floor is where ASan says so. *)
|
50000 instances past the one-megabyte floor is where ASan says so. *)
|
||||||
"programs/dyn-class.flan", [];
|
"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
|
(* 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
|
with a raw [Field] the surface language never writes (check.ml's
|
||||||
[box_option]/[unbox_option], the same access Render's structural
|
[box_option]/[unbox_option], the same access Render's structural
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user