The break loop keeps its condition, and the daemon renders it
The break loop used to discard the pointer it was handed, so the buffer could name a BoundsError's fields and never show 648. Now the snapshot stashes it, flan_agent_condition hands it back on the stopped thread, and a daemon-built thunk — locals pointed at the condition — renders each field. Delivered at-stop, so a resume-and-restop cannot get the old type read over the new pointer. The trap sites publish their loc around the hook call, the snapshot copies it, and break answers :site with the line's text as :source — the frame lines say where each call was; this is the only record of the indexing itself. Compiler temps are hidden from the locals listing rather than refused as s4; a shadowing rebind strips its ~N except where the outer binding is on the same list, where both keep their raw spelling.
This commit is contained in:
parent
aa989931dc
commit
1cbe8ed386
152
lib/dev.ml
152
lib/dev.ml
@ -388,6 +388,29 @@ let restarts t =
|
|||||||
end
|
end
|
||||||
| exception Unix.Unix_error (e, _, _) -> Error (Unix.error_message e)
|
| exception Unix.Unix_error (e, _, _) -> Error (Unix.error_message e)
|
||||||
|
|
||||||
|
(* Whether the break on top holds a condition value a render thunk can be
|
||||||
|
aimed at. [+] or [-]; the pointer itself never crosses the wire. *)
|
||||||
|
let condition_present t =
|
||||||
|
match ask t "condition" with
|
||||||
|
| exception Unix.Unix_error (e, _, _) -> Error (Unix.error_message e)
|
||||||
|
| text ->
|
||||||
|
let line = String.trim text in
|
||||||
|
if line = "+" then Ok true
|
||||||
|
else if line = "-" then Ok false
|
||||||
|
else Error line
|
||||||
|
|
||||||
|
(* Where the expression that trapped is written — file:line:col — or [None]
|
||||||
|
for a stop that has no site: a user (error ...), a (pause). The frame
|
||||||
|
lines say where each call was; this is the only record of the indexing or
|
||||||
|
the division itself. *)
|
||||||
|
let trap_site t =
|
||||||
|
match ask t "site" with
|
||||||
|
| exception Unix.Unix_error (e, _, _) -> Error (Unix.error_message e)
|
||||||
|
| text ->
|
||||||
|
let line = String.trim text in
|
||||||
|
if String.length line >= 3 && String.sub line 0 3 = "err" then Error line
|
||||||
|
else Ok (if line = "-" || line = "" then None else Some line)
|
||||||
|
|
||||||
(* Where a stopped program is, one frame per line, innermost first — the same
|
(* Where a stopped program is, one frame per line, innermost first — the same
|
||||||
framing [restarts] uses, terminated by a lone dot, because it comes back
|
framing [restarts] uses, terminated by a lone dot, because it comes back
|
||||||
over the same one-line-out socket.
|
over the same one-line-out socket.
|
||||||
@ -1355,6 +1378,47 @@ let layout t ~ty =
|
|||||||
other, so an editor reads the same two keys whatever it asked. What this op
|
other, so an editor reads the same two keys whatever it asked. What this op
|
||||||
adds is the restart names, which cost a second round trip to the program and
|
adds is the restart names, which cost a second round trip to the program and
|
||||||
are wanted only when someone is about to choose one. *)
|
are wanted only when someone is about to choose one. *)
|
||||||
|
(* The text of the line a site names, for the pointer the break buffer draws
|
||||||
|
under the headline. Best effort by design: the site is authoritative and a
|
||||||
|
file this end cannot read simply contributes no line — a build directory
|
||||||
|
moved, a program compiled on another machine. Parsed from the right,
|
||||||
|
because the path is the one piece that could contain a colon. *)
|
||||||
|
let source_line site =
|
||||||
|
match String.rindex_opt site ':' with
|
||||||
|
| None -> None
|
||||||
|
| Some c ->
|
||||||
|
(match String.rindex_from_opt site (c - 1) ':' with
|
||||||
|
| None -> None
|
||||||
|
| Some l ->
|
||||||
|
(match int_of_string_opt (String.sub site (l + 1) (c - l - 1)) with
|
||||||
|
| None -> None
|
||||||
|
| Some line when line > 0 ->
|
||||||
|
let path = String.sub site 0 l in
|
||||||
|
(match open_in path with
|
||||||
|
| exception Sys_error _ -> None
|
||||||
|
| ic ->
|
||||||
|
let rec skip n =
|
||||||
|
match input_line ic with
|
||||||
|
| exception End_of_file -> None
|
||||||
|
| text -> if n <= 1 then Some text else skip (n - 1)
|
||||||
|
in
|
||||||
|
let r = skip line in
|
||||||
|
close_in_noerr ic;
|
||||||
|
r)
|
||||||
|
| Some _ -> None))
|
||||||
|
|
||||||
|
(* The two site fields [break] adds when the stop has one. [:site] is where
|
||||||
|
the expression that trapped is written; [:source] is that line's text, when
|
||||||
|
the file can be read from here. *)
|
||||||
|
let site_fields t =
|
||||||
|
match trap_site t with
|
||||||
|
| Error _ | Ok None -> []
|
||||||
|
| Ok (Some site) ->
|
||||||
|
(":site " ^ Wire.quote site)
|
||||||
|
:: (match source_line site with
|
||||||
|
| None -> []
|
||||||
|
| Some text -> [ ":source " ^ Wire.quote text ])
|
||||||
|
|
||||||
let break t =
|
let break t =
|
||||||
match liveness t with
|
match liveness t with
|
||||||
| Gone -> error gone
|
| Gone -> error gone
|
||||||
@ -1386,12 +1450,13 @@ let break t =
|
|||||||
rather than filtered, because a client that quietly dropped them
|
rather than filtered, because a client that quietly dropped them
|
||||||
would leave someone asking where their restart went. *)
|
would leave someone asking where their restart went. *)
|
||||||
ok
|
ok
|
||||||
[ ":restarts " ^ Wire.strings (List.map (fun (_, _, n) -> n) rs);
|
([ ":restarts " ^ Wire.strings (List.map (fun (_, _, n) -> n) rs);
|
||||||
":unreachable "
|
":unreachable "
|
||||||
^ Wire.ints
|
^ Wire.ints
|
||||||
(List.filter_map
|
(List.filter_map
|
||||||
(fun (i, ok, _) -> if ok then None else Some i)
|
(fun (i, ok, _) -> if ok then None else Some i)
|
||||||
rs) ]
|
rs) ]
|
||||||
|
@ site_fields t)
|
||||||
| Error m -> error ("the program refused to list its restarts: " ^ m))
|
| Error m -> error ("the program refused to list its restarts: " ^ m))
|
||||||
|
|
||||||
(* [(:op "backtrace")] — the frames of a stopped program, innermost first.
|
(* [(:op "backtrace")] — the frames of a stopped program, innermost first.
|
||||||
@ -1714,9 +1779,7 @@ let locals t ~frame =
|
|||||||
if Array.length fn.Tast.slots = 0 then
|
if Array.length fn.Tast.slots = 0 then
|
||||||
ok
|
ok
|
||||||
[ ":frame " ^ Wire.quote name; ":locals ()"; ":refused ()";
|
[ ":frame " ^ Wire.quote name; ":locals ()"; ":refused ()";
|
||||||
":note "
|
":note " ^ Wire.quote "this frame has no named locals" ]
|
||||||
^ Wire.quote
|
|
||||||
"that frame records no slots; every slot in it is one the compiler made up" ]
|
|
||||||
else
|
else
|
||||||
(match bound_slots t ~frame with
|
(match bound_slots t ~frame with
|
||||||
| Error m -> error ("the program refused to say which slots are bound: " ^ m)
|
| Error m -> error ("the program refused to say which slots are bound: " ^ m)
|
||||||
@ -1751,6 +1814,86 @@ let locals t ~frame =
|
|||||||
(fun (n, why) -> Wire.list [ Wire.quote n; Wire.quote why ])
|
(fun (n, why) -> Wire.list [ Wire.quote n; Wire.quote why ])
|
||||||
refused) ]))
|
refused) ]))
|
||||||
|
|
||||||
|
(* [(:op "condition")] — the stopped condition's fields, with their values.
|
||||||
|
|
||||||
|
[layout] answers the *shape* out of [Tast.structs] with no program
|
||||||
|
involved; this is the other half. The break loop stashed the pointer it
|
||||||
|
was handed in the agent's snapshot, and this end knows the type at that
|
||||||
|
address — it compiled it, and [status] reports its qualified name. So it
|
||||||
|
is [locals] pointed at the condition: a thunk renders each field through
|
||||||
|
[flan/dev-cond], on the stopped thread, and the text comes back the same
|
||||||
|
way.
|
||||||
|
|
||||||
|
Delivered at-stop, and that is the correctness of it rather than a nicety.
|
||||||
|
The thunk reads whatever pointer the snapshot on top holds when it runs; a
|
||||||
|
program that resumed and stopped again holds a *different* condition, and
|
||||||
|
rendering the old stop's type over the new stop's pointer would be a
|
||||||
|
misread with a plausible shape. Naming the stop makes the agent drop the
|
||||||
|
thunk instead.
|
||||||
|
|
||||||
|
Refused, by name, for a stop that has no value to read: a trap like
|
||||||
|
[NullAllocator] is a name with no struct behind it, and a trap with no
|
||||||
|
transfer channel reached the break loop with no condition at all. *)
|
||||||
|
let condition_op t =
|
||||||
|
match liveness t with
|
||||||
|
| Gone -> error gone
|
||||||
|
| Parked when not (parked_break t) ->
|
||||||
|
parked "a parked program is not stopped on a condition"
|
||||||
|
| Live | Parked ->
|
||||||
|
match state t with
|
||||||
|
| Running ->
|
||||||
|
error "the program is running; a condition is read where it stopped"
|
||||||
|
| Unreachable m -> error ("cannot ask the program what it stopped on: " ^ m)
|
||||||
|
| Stopped cname ->
|
||||||
|
match
|
||||||
|
List.find_opt
|
||||||
|
(fun (s : Tast.structure) -> String.equal s.Tast.sname cname)
|
||||||
|
t.session.Session.program.Tast.structs
|
||||||
|
with
|
||||||
|
| None ->
|
||||||
|
error
|
||||||
|
(cname
|
||||||
|
^ " is not a struct this session knows, so there are no fields to \
|
||||||
|
read")
|
||||||
|
| Some st ->
|
||||||
|
match condition_present t with
|
||||||
|
| Error m -> error ("cannot ask the program for its condition: " ^ m)
|
||||||
|
| Ok false ->
|
||||||
|
error
|
||||||
|
("this stop was not handed a condition value; there is nothing to \
|
||||||
|
render for " ^ cname)
|
||||||
|
| Ok true ->
|
||||||
|
match stop_gen t with
|
||||||
|
| None | Some 0 ->
|
||||||
|
error "cannot pin the stop this condition belongs to; ask again"
|
||||||
|
| Some gen ->
|
||||||
|
let c, refused = Session.render_condition t.session ~st in
|
||||||
|
(match run_render_thunk ~at_stop:gen t ~tag:"c" ~c with
|
||||||
|
| Error m -> error m
|
||||||
|
| Ok v ->
|
||||||
|
(* One line per field — name, type, value, tab separated, and
|
||||||
|
safe because every string the renderer emits is escaped. *)
|
||||||
|
let entries =
|
||||||
|
List.filter_map
|
||||||
|
(fun line ->
|
||||||
|
match String.split_on_char '\t' line with
|
||||||
|
| [ n; ty; value ] ->
|
||||||
|
Some
|
||||||
|
(Wire.list
|
||||||
|
[ Wire.quote n; Wire.quote ty; Wire.quote value ])
|
||||||
|
| _ -> None)
|
||||||
|
(String.split_on_char '\n' v)
|
||||||
|
in
|
||||||
|
ok
|
||||||
|
[ ":type " ^ Wire.quote cname;
|
||||||
|
":fields " ^ Wire.list entries;
|
||||||
|
":refused "
|
||||||
|
^ Wire.list
|
||||||
|
(List.map
|
||||||
|
(fun (n, why) ->
|
||||||
|
Wire.list [ Wire.quote n; Wire.quote why ])
|
||||||
|
refused) ])
|
||||||
|
|
||||||
(* [(:op "inspect" :frame N :slot I :path (...))] — the inspector's second
|
(* [(:op "inspect" :frame N :slot I :path (...))] — the inspector's second
|
||||||
rooting mode. [docs/BUILT.md]'s "Two ways to root a walk" says what each root
|
rooting mode. [docs/BUILT.md]'s "Two ways to root a walk" says what each root
|
||||||
can and cannot do; this is the half that names a frame.
|
can and cannot do; this is the half that names a frame.
|
||||||
@ -3222,6 +3365,7 @@ let handle t req =
|
|||||||
| Some "describe" -> describe t
|
| Some "describe" -> describe t
|
||||||
| Some "defs" -> defs t
|
| Some "defs" -> defs t
|
||||||
| Some "break" -> break t
|
| Some "break" -> break t
|
||||||
|
| Some "condition" -> condition_op t
|
||||||
| Some "backtrace" -> backtrace_op t
|
| Some "backtrace" -> backtrace_op t
|
||||||
| Some "locals" ->
|
| Some "locals" ->
|
||||||
locals t ~frame:(match Wire.int_field req "frame" with Some n -> n | None -> 0)
|
locals t ~frame:(match Wire.int_field req "frame" with Some n -> n | None -> 0)
|
||||||
|
|||||||
168
lib/session.ml
168
lib/session.ml
@ -941,6 +941,12 @@ let externs : Tast.extern list =
|
|||||||
{ Tast.ename = "flan/dev-slot"; esym = "flan_agent_frame_slot";
|
{ Tast.ename = "flan/dev-slot"; esym = "flan_agent_frame_slot";
|
||||||
eparams = [ Types.Int Types.I64; Types.Int Types.I64 ];
|
eparams = [ Types.Int Types.I64; Types.Int Types.I64 ];
|
||||||
eret = Types.Ptr (Types.Int Types.U8); eloc = Loc.unknown };
|
eret = Types.Ptr (Types.Int Types.U8); eloc = Loc.unknown };
|
||||||
|
(* The condition the stopped program is holding, same contract: the agent
|
||||||
|
resolves it against the snapshot on top when the thunk runs, and NULL
|
||||||
|
when there is none. See [render_condition]. *)
|
||||||
|
{ Tast.ename = "flan/dev-cond"; esym = "flan_agent_condition";
|
||||||
|
eparams = []; eret = Types.Ptr (Types.Int Types.U8);
|
||||||
|
eloc = Loc.unknown };
|
||||||
{ Tast.ename = "flan/dev-begin"; esym = "flan_dev_result_begin";
|
{ Tast.ename = "flan/dev-begin"; esym = "flan_dev_result_begin";
|
||||||
eparams = []; eret = Types.Unit; eloc = Loc.unknown };
|
eparams = []; eret = Types.Unit; eloc = Loc.unknown };
|
||||||
{ Tast.ename = "flan/dev-end"; esym = "flan_dev_result_end";
|
{ Tast.ename = "flan/dev-end"; esym = "flan_dev_result_end";
|
||||||
@ -1011,6 +1017,51 @@ let dev_pointers : Render.pointers =
|
|||||||
|
|
||||||
(* ── The locals of a stopped frame ─────────────────────────────────── *)
|
(* ── The locals of a stopped frame ─────────────────────────────────── *)
|
||||||
|
|
||||||
|
(* What a slot is *shown as*. Two departures from the raw [snames] entry,
|
||||||
|
both about keeping the listing in the words the person wrote.
|
||||||
|
|
||||||
|
A compiler temp — [dotimes]'s hidden bound, the slot a (min) evaluates an
|
||||||
|
operand into — has no name at all, and it is hidden rather than refused:
|
||||||
|
[s4] is not a variable anyone can find in the file, and a row explaining
|
||||||
|
its absence was noise on every frame that had one. [None] here means
|
||||||
|
"not shown".
|
||||||
|
|
||||||
|
A shadowing rebind — [check.ml]'s [bind] suffixes the repeat as [v~2] so
|
||||||
|
the debug info never claims one binding is the other — is shown under the
|
||||||
|
written name, because the depth is the compiler's bookkeeping. Only a
|
||||||
|
trailing [~N] is stripped: [~] is the reader's delimiter and a synthesized
|
||||||
|
name like [destructure~nth] carries it for a different reason. And when
|
||||||
|
stripping would put one name on two slots of this frame, both keep their
|
||||||
|
raw spelling — two rows called [v] with nothing to tell them apart is the
|
||||||
|
lie the suffix existed to prevent. *)
|
||||||
|
let strip_rebind name =
|
||||||
|
match String.rindex_opt name '~' with
|
||||||
|
| Some k when k > 0 && k < String.length name - 1 ->
|
||||||
|
let suffix = String.sub name (k + 1) (String.length name - k - 1) in
|
||||||
|
if String.for_all (fun c -> c >= '0' && c <= '9') suffix
|
||||||
|
then String.sub name 0 k
|
||||||
|
else name
|
||||||
|
| _ -> name
|
||||||
|
|
||||||
|
let shown_names (fn : Tast.fn) : string option array =
|
||||||
|
let n = Array.length fn.Tast.slots in
|
||||||
|
let raw =
|
||||||
|
Array.init n (fun i ->
|
||||||
|
if i < Array.length fn.Tast.snames then fn.Tast.snames.(i) else None)
|
||||||
|
in
|
||||||
|
let stripped = Array.map (Option.map strip_rebind) raw in
|
||||||
|
let count name =
|
||||||
|
Array.fold_left
|
||||||
|
(fun acc s -> if s = Some name then acc + 1 else acc)
|
||||||
|
0 stripped
|
||||||
|
in
|
||||||
|
Array.mapi
|
||||||
|
(fun i s ->
|
||||||
|
match s with
|
||||||
|
| None -> None
|
||||||
|
| Some d -> if count d > 1 then raw.(i) else Some d)
|
||||||
|
stripped
|
||||||
|
|
||||||
(* The second half of what a break loop can show, and it is the same primitive
|
(* The second half of what a break loop can show, and it is the same primitive
|
||||||
as [C-x C-e] pointed somewhere else.
|
as [C-x C-e] pointed somewhere else.
|
||||||
|
|
||||||
@ -1095,27 +1146,20 @@ let render_locals ?(origin = "<locals>") t ~frame ~(fn : Tast.fn) ~bound
|
|||||||
refuse name why;
|
refuse name why;
|
||||||
None
|
None
|
||||||
in
|
in
|
||||||
|
let names = shown_names fn in
|
||||||
let body =
|
let body =
|
||||||
List.concat
|
List.concat
|
||||||
((List.filter_map
|
((List.filter_map
|
||||||
(fun i ->
|
(fun i ->
|
||||||
let ty = fn.Tast.slots.(i) in
|
let ty = fn.Tast.slots.(i) in
|
||||||
let name =
|
match names.(i) with
|
||||||
if i < Array.length fn.Tast.snames then fn.Tast.snames.(i)
|
|
||||||
else None
|
|
||||||
in
|
|
||||||
match name with
|
|
||||||
| None ->
|
| None ->
|
||||||
(* A slot the compiler made up: [dotimes]'s hidden bound, the
|
(* A slot the compiler made up — hidden, not refused; see
|
||||||
temporary a (min) evaluates an operand into. There is no
|
[shown_names]. *)
|
||||||
name to show and inventing one would put a variable in the
|
|
||||||
list that nobody can find in the file. *)
|
|
||||||
refuse (Printf.sprintf "s%d" i)
|
|
||||||
"a slot the compiler made up; no name was written for it";
|
|
||||||
None
|
None
|
||||||
| Some name when not (List.mem i bound) ->
|
| Some name when not (List.mem i bound) ->
|
||||||
refuse name
|
refuse name
|
||||||
"not bound yet at the point the program stopped";
|
"not bound yet where the program stopped";
|
||||||
None
|
None
|
||||||
| Some name -> one i ty name)
|
| Some name -> one i ty name)
|
||||||
(List.init (Array.length fn.Tast.slots) (fun i -> i))))
|
(List.init (Array.length fn.Tast.slots) (fun i -> i))))
|
||||||
@ -1142,6 +1186,90 @@ let render_locals ?(origin = "<locals>") t ~frame ~(fn : Tast.fn) ~bound
|
|||||||
ignore origin;
|
ignore origin;
|
||||||
({ ir; x86 = t.x86; names = []; fns = []; installs = true }, List.rev !refused)
|
({ ir; x86 = t.x86; names = []; fns = []; installs = true }, List.rev !refused)
|
||||||
|
|
||||||
|
(* ── The fields of the condition a break is holding ────────────────── *)
|
||||||
|
|
||||||
|
(* [render_locals] pointed at the condition instead of a frame. The break
|
||||||
|
loop stashes the pointer it was handed in the snapshot, the thunk reads it
|
||||||
|
back through [flan/dev-cond], and the type at that address is the struct
|
||||||
|
whose qualified name the agent reported as the condition — this end
|
||||||
|
compiled it, so the layout is its own to know. One line per field:
|
||||||
|
name, type, value, tab separated.
|
||||||
|
|
||||||
|
The thunk carries no address of its own — [flan/dev-cond] resolves against
|
||||||
|
the snapshot on top when it runs — but the *type* it reads with was chosen
|
||||||
|
against a particular stop, so the caller delivers it at-stop: a program
|
||||||
|
that resumed and stopped again holds a different condition, and rendering
|
||||||
|
the old type over the new pointer is the misread the at-stop check
|
||||||
|
refuses. *)
|
||||||
|
let render_condition t ~(st : Tast.structure) : change * (string * string) list =
|
||||||
|
let loc = Loc.unknown in
|
||||||
|
let extra = ref [] and nslots = ref 0 in
|
||||||
|
let c =
|
||||||
|
{ Render.structs = t.program.Tast.structs;
|
||||||
|
datas = t.program.Tast.datas;
|
||||||
|
unions = t.program.Tast.unions;
|
||||||
|
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums [];
|
||||||
|
emit = dev_emitter;
|
||||||
|
ptrs = Some dev_pointers;
|
||||||
|
alloc = (fun ty ->
|
||||||
|
let i = !nslots in
|
||||||
|
incr nslots;
|
||||||
|
extra := ty :: !extra;
|
||||||
|
i) }
|
||||||
|
in
|
||||||
|
let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in
|
||||||
|
let bytes_of str =
|
||||||
|
{ Tast.e =
|
||||||
|
Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str str; ty = Types.String; loc } ]);
|
||||||
|
ty = Types.Slice (Types.Int Types.U8); loc }
|
||||||
|
in
|
||||||
|
let lit str = c.Render.emit.Render.ebytes (bytes_of str) in
|
||||||
|
let refused = ref [] in
|
||||||
|
let cty = Types.Named st.Tast.sname in
|
||||||
|
let address =
|
||||||
|
{ Tast.e = Tast.Call ("flan/dev-cond", []);
|
||||||
|
ty = Types.Ptr (Types.Int Types.U8); loc }
|
||||||
|
in
|
||||||
|
let typed =
|
||||||
|
{ Tast.e = Tast.Prim (Tast.Cast (Types.Ptr cty), [ address ]);
|
||||||
|
ty = Types.Ptr cty; loc }
|
||||||
|
in
|
||||||
|
let root = { Tast.e = Tast.Deref typed; ty = cty; loc } in
|
||||||
|
let one i (f : Tast.field) =
|
||||||
|
let v = { Tast.e = Tast.Field (root, i); ty = f.Tast.fty; loc } in
|
||||||
|
match Render.render c 0 v with
|
||||||
|
| parts ->
|
||||||
|
Some
|
||||||
|
((lit (f.Tast.fname ^ "\t" ^ Types.to_string f.Tast.fty ^ "\t") :: parts)
|
||||||
|
@ [ lit "\n" ])
|
||||||
|
| exception Loc.Error { Loc.dmsg = why; _ } ->
|
||||||
|
(* A field the structural printer has no arm for. Named with the
|
||||||
|
reason, so the buffer shows the field and says why its value is
|
||||||
|
not beside it. *)
|
||||||
|
refused := (f.Tast.fname, why) :: !refused;
|
||||||
|
None
|
||||||
|
in
|
||||||
|
let body =
|
||||||
|
List.concat
|
||||||
|
(List.filter_map Fun.id (List.mapi (fun i f -> one i f) st.Tast.fields))
|
||||||
|
in
|
||||||
|
t.thunks <- t.thunks + 1;
|
||||||
|
let name = Printf.sprintf "condition/%d" t.thunks in
|
||||||
|
let thunk : Tast.fn =
|
||||||
|
{ Tast.name; params = []; ret = Types.Unit;
|
||||||
|
body = (nullary "flan/dev-begin" :: body) @ [ nullary "flan/dev-end" ];
|
||||||
|
fdefers = []; fparent = None; floc = loc;
|
||||||
|
slots = Array.of_list (List.rev !extra);
|
||||||
|
snames = Array.make (List.length !extra) None }
|
||||||
|
in
|
||||||
|
let program =
|
||||||
|
{ t.program with
|
||||||
|
Tast.fns = t.program.Tast.fns @ [ thunk ];
|
||||||
|
externs = t.program.Tast.externs @ externs }
|
||||||
|
in
|
||||||
|
let ir = redefinition t ~call:name program ~fns:[ name ] in
|
||||||
|
({ ir; x86 = t.x86; names = []; fns = []; installs = true }, List.rev !refused)
|
||||||
|
|
||||||
(* ── One slot of a stopped frame, walked ───────────────────────────── *)
|
(* ── One slot of a stopped frame, walked ───────────────────────────── *)
|
||||||
|
|
||||||
(* The inspector's second rooting mode, and the whole of what it needed.
|
(* The inspector's second rooting mode, and the whole of what it needed.
|
||||||
@ -1327,15 +1455,13 @@ let render_slot ?(origin = "<inspect>") t ~frame ~(fn : Tast.fn) ~slot ~path
|
|||||||
(Printf.sprintf "there is no slot %d in %s; it has %d" slot fn.Tast.name
|
(Printf.sprintf "there is no slot %d in %s; it has %d" slot fn.Tast.name
|
||||||
nslots_of_fn)
|
nslots_of_fn)
|
||||||
else
|
else
|
||||||
let sname =
|
let sname = (shown_names fn).(slot) in
|
||||||
if slot < Array.length fn.Tast.snames then fn.Tast.snames.(slot) else None
|
|
||||||
in
|
|
||||||
match sname with
|
match sname with
|
||||||
| None ->
|
| None ->
|
||||||
Error
|
Error
|
||||||
(Printf.sprintf
|
(Printf.sprintf
|
||||||
"slot %d of %s is one the compiler made up; no name was written for \
|
"slot %d of %s has no name in the source; the listing does not \
|
||||||
it, and it is not something the listing offers"
|
show it and there is nothing here to inspect"
|
||||||
slot fn.Tast.name)
|
slot fn.Tast.name)
|
||||||
| Some name ->
|
| Some name ->
|
||||||
let extra = ref [] and nslots = ref 0 in
|
let extra = ref [] and nslots = ref 0 in
|
||||||
@ -1529,15 +1655,13 @@ let write_slot ?(origin = "<set>") t ~frame ~(fn : Tast.fn) ~slot ~path
|
|||||||
third of a second to say it. *)
|
third of a second to say it. *)
|
||||||
Error "there is nothing to store: nothing in this was changed"
|
Error "there is nothing to store: nothing in this was changed"
|
||||||
else
|
else
|
||||||
let sname =
|
let sname = (shown_names fn).(slot) in
|
||||||
if slot < Array.length fn.Tast.snames then fn.Tast.snames.(slot) else None
|
|
||||||
in
|
|
||||||
match sname with
|
match sname with
|
||||||
| None ->
|
| None ->
|
||||||
Error
|
Error
|
||||||
(Printf.sprintf
|
(Printf.sprintf
|
||||||
"slot %d of %s is one the compiler made up; no name was written for \
|
"slot %d of %s has no name in the source; the listing does not \
|
||||||
it, and it is not something the listing offers"
|
show it and there is nothing here to inspect"
|
||||||
slot fn.Tast.name)
|
slot fn.Tast.name)
|
||||||
| Some name ->
|
| Some name ->
|
||||||
let where = name ^ path_text path in
|
let where = name ^ path_text path in
|
||||||
|
|||||||
@ -761,10 +761,24 @@ typedef struct { int64_t low, high, length; } flan_bounds_cond;
|
|||||||
static const uint8_t flan_bounds_name[] = "BoundsError";
|
static const uint8_t flan_bounds_name[] = "BoundsError";
|
||||||
#define FLAN_BOUNDS_NAMELEN 11
|
#define FLAN_BOUNDS_NAMELEN 11
|
||||||
|
|
||||||
|
/* Where the expression that trapped is written — the loc every checked site
|
||||||
|
* already passes for its unhandled message, published for the break hook.
|
||||||
|
* The frame chain says where each *call* was; this is the only record of the
|
||||||
|
* `at` or the division itself, which is the line a person wants pointed at.
|
||||||
|
*
|
||||||
|
* Set immediately before the hook runs and cleared when it returns, so the
|
||||||
|
* agent's snapshot (taken on entry to the break loop, on this same thread)
|
||||||
|
* reads it while it is true and a later break through [flan_error] — a user
|
||||||
|
* (error ...), which carries no loc — cannot inherit a stale one. NULL
|
||||||
|
* outside that window, and NULL is the honest answer for a signal that has
|
||||||
|
* no expression to point at. */
|
||||||
|
const uint8_t *flan_break_site;
|
||||||
|
int64_t flan_break_site_len;
|
||||||
|
|
||||||
/* Returns nonzero if something transferred, in which case the caller returns
|
/* Returns nonzero if something transferred, in which case the caller returns
|
||||||
* and its caller's guard carries the transfer out. */
|
* and its caller's guard carries the transfer out. */
|
||||||
static int flan_bounds_signal(void *xfer, int64_t low, int64_t high,
|
static int flan_bounds_signal(const uint8_t *loc, int64_t loclen, void *xfer,
|
||||||
int64_t len) {
|
int64_t low, int64_t high, int64_t len) {
|
||||||
flan_bounds_cond c;
|
flan_bounds_cond c;
|
||||||
uint32_t id = flan_name_id(flan_bounds_name, FLAN_BOUNDS_NAMELEN);
|
uint32_t id = flan_name_id(flan_bounds_name, FLAN_BOUNDS_NAMELEN);
|
||||||
c.low = low;
|
c.low = low;
|
||||||
@ -773,7 +787,11 @@ static int flan_bounds_signal(void *xfer, int64_t low, int64_t high,
|
|||||||
flan_signal(id, &c, xfer);
|
flan_signal(id, &c, xfer);
|
||||||
if (*(void **)xfer != NULL) return 1;
|
if (*(void **)xfer != NULL) return 1;
|
||||||
if (flan_break_hook != NULL) {
|
if (flan_break_hook != NULL) {
|
||||||
|
flan_break_site = loc;
|
||||||
|
flan_break_site_len = loclen;
|
||||||
flan_break_hook(flan_bounds_name, FLAN_BOUNDS_NAMELEN, &c, xfer);
|
flan_break_hook(flan_bounds_name, FLAN_BOUNDS_NAMELEN, &c, xfer);
|
||||||
|
flan_break_site = NULL;
|
||||||
|
flan_break_site_len = 0;
|
||||||
if (*(void **)xfer != NULL) return 1;
|
if (*(void **)xfer != NULL) return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@ -781,13 +799,13 @@ static int flan_bounds_signal(void *xfer, int64_t low, int64_t high,
|
|||||||
|
|
||||||
void flan_bounds_error(const uint8_t *loc, int64_t loclen, int64_t idx,
|
void flan_bounds_error(const uint8_t *loc, int64_t loclen, int64_t idx,
|
||||||
int64_t len, void *xfer) {
|
int64_t len, void *xfer) {
|
||||||
if (flan_bounds_signal(xfer, idx, idx, len)) return;
|
if (flan_bounds_signal(loc, loclen, xfer, idx, idx, len)) return;
|
||||||
flan_bounds_fail(loc, loclen, idx, len);
|
flan_bounds_fail(loc, loclen, idx, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
void flan_slice_error(const uint8_t *loc, int64_t loclen, int64_t lo,
|
void flan_slice_error(const uint8_t *loc, int64_t loclen, int64_t lo,
|
||||||
int64_t hi, int64_t len, void *xfer) {
|
int64_t hi, int64_t len, void *xfer) {
|
||||||
if (flan_bounds_signal(xfer, lo, hi, len)) return;
|
if (flan_bounds_signal(loc, loclen, xfer, lo, hi, len)) return;
|
||||||
flan_slice_fail(loc, loclen, lo, hi, len);
|
flan_slice_fail(loc, loclen, lo, hi, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -832,7 +850,7 @@ _Noreturn void flan_slice_promise_fail(const uint8_t *loc, int64_t loclen,
|
|||||||
|
|
||||||
void flan_slice_promise_error(const uint8_t *loc, int64_t loclen, int64_t n,
|
void flan_slice_promise_error(const uint8_t *loc, int64_t loclen, int64_t n,
|
||||||
void *xfer) {
|
void *xfer) {
|
||||||
if (flan_bounds_signal(xfer, 0, n, 0)) return;
|
if (flan_bounds_signal(loc, loclen, xfer, 0, n, 0)) return;
|
||||||
flan_slice_promise_fail(loc, loclen, n);
|
flan_slice_promise_fail(loc, loclen, n);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -936,7 +954,11 @@ void flan_arith_error(const uint8_t *loc, int64_t loclen, int32_t op,
|
|||||||
flan_signal(id, &c, xfer);
|
flan_signal(id, &c, xfer);
|
||||||
if (*(void **)xfer != NULL) return;
|
if (*(void **)xfer != NULL) return;
|
||||||
if (flan_break_hook != NULL) {
|
if (flan_break_hook != NULL) {
|
||||||
|
flan_break_site = loc;
|
||||||
|
flan_break_site_len = loclen;
|
||||||
flan_break_hook(flan_arith_name, FLAN_ARITH_NAMELEN, &c, xfer);
|
flan_break_hook(flan_arith_name, FLAN_ARITH_NAMELEN, &c, xfer);
|
||||||
|
flan_break_site = NULL;
|
||||||
|
flan_break_site_len = 0;
|
||||||
if (*(void **)xfer != NULL) return;
|
if (*(void **)xfer != NULL) return;
|
||||||
}
|
}
|
||||||
flan_arith_fail(loc, loclen, op, lhs, rhs);
|
flan_arith_fail(loc, loclen, op, lhs, rhs);
|
||||||
@ -1706,7 +1728,8 @@ void *flan_vec_at(flan_vec *v, int32_t i, int64_t size, const uint8_t *loc,
|
|||||||
/* The same unsigned comparison the fixed-array bounds check uses: a negative
|
/* The same unsigned comparison the fixed-array bounds check uses: a negative
|
||||||
* index sign-extends to a huge unsigned and is caught by the one test. */
|
* index sign-extends to a huge unsigned and is caught by the one test. */
|
||||||
if ((uint64_t)(int64_t)i >= (uint64_t)v->len) {
|
if ((uint64_t)(int64_t)i >= (uint64_t)v->len) {
|
||||||
if (flan_bounds_signal(xfer, (int64_t)i, (int64_t)i, v->len)) return NULL;
|
if (flan_bounds_signal(loc, loclen, xfer, (int64_t)i, (int64_t)i, v->len))
|
||||||
|
return NULL;
|
||||||
flan_vec_bounds_fail(loc, loclen, (int64_t)i, v->len);
|
flan_vec_bounds_fail(loc, loclen, (int64_t)i, v->len);
|
||||||
}
|
}
|
||||||
return (uint8_t *)v->ptr + (int64_t)i * size;
|
return (uint8_t *)v->ptr + (int64_t)i * size;
|
||||||
@ -1723,7 +1746,7 @@ void flan_vec_as_slice(flan_vec *v, void *out, int32_t lo, int32_t hi,
|
|||||||
/* Both ends, because both are what went wrong — the fixed-array slice
|
/* Both ends, because both are what went wrong — the fixed-array slice
|
||||||
* check reports the same pair. [out] is left untouched on the transfer
|
* check reports the same pair. [out] is left untouched on the transfer
|
||||||
* path; the caller's guard branches before it reads the slice. */
|
* path; the caller's guard branches before it reads the slice. */
|
||||||
if (flan_bounds_signal(xfer, l, h, v->len)) return;
|
if (flan_bounds_signal(loc, loclen, xfer, l, h, v->len)) return;
|
||||||
flan_vec_bounds_fail(loc, loclen, l, v->len);
|
flan_vec_bounds_fail(loc, loclen, l, v->len);
|
||||||
}
|
}
|
||||||
s.p = (uint8_t *)v->ptr + l * size;
|
s.p = (uint8_t *)v->ptr + l * size;
|
||||||
|
|||||||
@ -15,13 +15,22 @@
|
|||||||
(let [p (Point {.x 1.5 .y 2.5})
|
(let [p (Point {.x 1.5 .y 2.5})
|
||||||
xs [10 20 30]
|
xs [10 20 30]
|
||||||
flag (> n 0)]
|
flag (> n 0)]
|
||||||
|
;; A loop, for its hidden bound: [dotimes] allocates a slot nobody named,
|
||||||
|
;; and the listing must *hide* it rather than refuse it by an invented
|
||||||
|
;; name — [s6] is not a variable anyone can find in this file.
|
||||||
|
(dotimes [hop 0] (print ""))
|
||||||
|
;; And a shadowing rebind. The checker suffixes the repeat as [label~2]
|
||||||
|
;; so the debug info never claims one binding is the other; the listing
|
||||||
|
;; keeps both raw spellings, because two rows both called [label] with
|
||||||
|
;; nothing to tell them apart would be worse.
|
||||||
|
(let [label "inner"]
|
||||||
(restart-case
|
(restart-case
|
||||||
(do (error (Boom {.why 7}))
|
(do (error (Boom {.why 7}))
|
||||||
;; Never reached before the break, so [after] is a slot with nothing
|
;; Never reached before the break, so [after] is a slot with nothing
|
||||||
;; in it: the frame records a null for it and this is what "not bound
|
;; in it: the frame records a null for it and this is what "not bound
|
||||||
;; yet" has to mean.
|
;; yet" has to mean.
|
||||||
(let [after (i64 99)] after))
|
(let [after (i64 99)] after))
|
||||||
(carry-on [] 5))))
|
(carry-on [] 5)))))
|
||||||
|
|
||||||
(defvar ticks i64)
|
(defvar ticks i64)
|
||||||
|
|
||||||
|
|||||||
@ -875,6 +875,21 @@ let () =
|
|||||||
[ { Form.v = Form.Str "id"; _ }; { Form.v = Form.Str "i32"; _ } ]; _ } ]; _ } -> ()
|
[ { Form.v = Form.Str "id"; _ }; { Form.v = Form.Str "i32"; _ } ]; _ } ]; _ } -> ()
|
||||||
| _ -> fail "the stopped program's condition has the wrong layout");
|
| _ -> fail "the stopped program's condition has the wrong layout");
|
||||||
|
|
||||||
|
(* And the value behind the shape: [fetch 1] built the condition, so
|
||||||
|
[.id] holds 1, and the daemon's thunk reads it out of the stopped
|
||||||
|
frame's own storage — a user [error], not a trap, so the same path
|
||||||
|
serves both. *)
|
||||||
|
let r = ask "(:op \"condition\")" in
|
||||||
|
if status r <> "ok" then
|
||||||
|
fail "condition values at a user error: %s"
|
||||||
|
(Option.value ~default:(status r) (Wire.string_field r "message"))
|
||||||
|
else
|
||||||
|
(match Wire.field r "fields" with
|
||||||
|
| Some { Form.v = Form.List [ { Form.v = Form.List
|
||||||
|
[ { Form.v = Form.Str "id"; _ }; { Form.v = Form.Str "i32"; _ };
|
||||||
|
{ Form.v = Form.Str "1"; _ } ]; _ } ]; _ } -> ()
|
||||||
|
| _ -> fail "the condition's own field value did not render");
|
||||||
|
|
||||||
(* What is on offer, innermost first. [break] carries the names and
|
(* What is on offer, innermost first. [break] carries the names and
|
||||||
nothing else — the state is the annotation's business, so there is
|
nothing else — the state is the annotation's business, so there is
|
||||||
one place in the daemon that decides it. *)
|
one place in the daemon that decides it. *)
|
||||||
@ -1273,6 +1288,58 @@ let () =
|
|||||||
if names <> [ "low"; "high"; "length" ] then
|
if names <> [ "low"; "high"; "length" ] then
|
||||||
fail "BoundsError's fields: %s" (String.concat ", " names)
|
fail "BoundsError's fields: %s" (String.concat ", " names)
|
||||||
| _ -> fail "BoundsError's layout has no fields");
|
| _ -> fail "BoundsError's layout has no fields");
|
||||||
|
(* The values, not just the shape. The break loop stashed the pointer
|
||||||
|
it was handed, the daemon knows the type — it compiled it — and a
|
||||||
|
thunk it builds renders the fields in the stopped program. This is
|
||||||
|
what turns "BoundsError" into "9 is past the end of a length-4
|
||||||
|
array" in the buffer, with nothing special-casing BoundsError. *)
|
||||||
|
let r = ask "(:op \"condition\")" in
|
||||||
|
if status r <> "ok" then
|
||||||
|
fail "condition values: %s"
|
||||||
|
(Option.value ~default:(status r) (Wire.string_field r "message"))
|
||||||
|
else begin
|
||||||
|
let fields =
|
||||||
|
match Wire.field r "fields" 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
|
||||||
|
if
|
||||||
|
fields
|
||||||
|
<> [ ("low", "i64", "9"); ("high", "i64", "9");
|
||||||
|
("length", "i64", "4") ]
|
||||||
|
then
|
||||||
|
fail "the condition's fields: %s"
|
||||||
|
(String.concat ", "
|
||||||
|
(List.map (fun (n, ty, v) -> n ^ " " ^ ty ^ " = " ^ v) fields))
|
||||||
|
end;
|
||||||
|
(* Where the *expression* is. The frame lines say where each call was;
|
||||||
|
the trap's own loc is the only record of the indexing itself, and
|
||||||
|
[break] carries it with the line's text so a buffer can point at
|
||||||
|
the column Elm-style. *)
|
||||||
|
(let r = ask "(:op \"break\")" in
|
||||||
|
match Wire.string_field r "site" with
|
||||||
|
| None -> fail "break over a bad index carries no :site"
|
||||||
|
| Some site ->
|
||||||
|
let has hay needle =
|
||||||
|
let n = String.length hay and m = String.length needle in
|
||||||
|
let rec go i = i + m <= n && (String.sub hay i m = needle || go (i + 1)) in
|
||||||
|
m = 0 || go 0
|
||||||
|
in
|
||||||
|
if not (has site "dev-break-bounds.flan:") then
|
||||||
|
fail "the site does not point into the program: %s" site;
|
||||||
|
(match Wire.string_field r "source" with
|
||||||
|
| Some line when has line "(at grid i)" -> ()
|
||||||
|
| Some line -> fail "the site's source line reads %S" line
|
||||||
|
| None -> fail "break carries a :site but no :source line"));
|
||||||
(* Only the program's own restart is on offer. Nothing is pushed at the
|
(* Only the program's own restart is on offer. Nothing is pushed at the
|
||||||
failing index, so a list with anything else on it would mean a site
|
failing index, so a list with anything else on it would mean a site
|
||||||
restart had been established after all. *)
|
restart had been established after all. *)
|
||||||
@ -1573,12 +1640,33 @@ let () =
|
|||||||
note here used to say was still owed. *)
|
note here used to say was still owed. *)
|
||||||
("p", "Point", "(Point {.x 1.5 .y 2.5})");
|
("p", "Point", "(Point {.x 1.5 .y 2.5})");
|
||||||
("xs", "[3 i32]", "[ 10 20 30]");
|
("xs", "[3 i32]", "[ 10 20 30]");
|
||||||
("flag", "bool", "true") ]
|
("flag", "bool", "true");
|
||||||
|
(* [hop] is [dotimes]'s index and it is listed; the loop's
|
||||||
|
hidden bound sits in the very next slot and is *not* — a
|
||||||
|
compiler temp is hidden, not refused, because [s6] is not a
|
||||||
|
variable anyone can find in the file. *)
|
||||||
|
("hop", "i32", "0");
|
||||||
|
(* The shadowing rebind keeps its raw spelling here because the
|
||||||
|
outer [label] is on the same list: strip the suffix from one
|
||||||
|
and the frame shows two rows called [label] with nothing to
|
||||||
|
tell them apart. *)
|
||||||
|
("label~2", "string", "\"inner\"") ]
|
||||||
in
|
in
|
||||||
if got <> want then
|
if got <> want then
|
||||||
fail "locals of the stopped frame: %s"
|
fail "locals of the stopped frame: %s"
|
||||||
(String.concat ", "
|
(String.concat ", "
|
||||||
(List.map (fun (n, ty, v) -> n ^ " " ^ ty ^ " = " ^ v) got));
|
(List.map (fun (n, ty, v) -> n ^ " " ^ ty ^ " = " ^ v) got));
|
||||||
|
(* No refusal mentions an invented name: the hidden bound must be
|
||||||
|
absent from both lists, not moved to the other one. *)
|
||||||
|
(match
|
||||||
|
List.filter
|
||||||
|
(fun (n, _, _) -> String.length n > 0 && n.[0] = 's')
|
||||||
|
(pairs r "refused")
|
||||||
|
with
|
||||||
|
| [] -> ()
|
||||||
|
| rs ->
|
||||||
|
fail "a compiler temp leaked into the refusals: %s"
|
||||||
|
(String.concat ", " (List.map (fun (n, _, _) -> n) rs)));
|
||||||
(* And the one that must not be rendered. [after] is bound inside
|
(* And the one that must not be rendered. [after] is bound inside
|
||||||
the restart-case *past* the error, so its slot is storage nothing
|
the restart-case *past* the error, so its slot is storage nothing
|
||||||
has written: the frame records a null for it, and a thunk that
|
has written: the frame records a null for it, and a thunk that
|
||||||
@ -1629,7 +1717,7 @@ let () =
|
|||||||
holding [p]'s value and nothing would say so. *)
|
holding [p]'s value and nothing would say so. *)
|
||||||
let r =
|
let r =
|
||||||
ask
|
ask
|
||||||
"(:op \"eval\" :code \"(defn look [n i64 label string] i64 (let [q (Point {.x 9.0 .y 9.0}) ys [1 2 3] mark (< n 0)] (restart-case (do (error (Boom {.why 7})) (let [after (i64 99)] after)) (carry-on [] 5))))\" :file \"/tmp/buf.flan\")"
|
"(:op \"eval\" :code \"(defn look [n i64 label string] i64 (let [q (Point {.x 9.0 .y 9.0}) ys [1 2 3] mark (< n 0)] (dotimes [pip 0] (print \\\"\\\")) (let [tag \\\"x\\\"] (restart-case (do (error (Boom {.why 7})) (let [later (i64 99)] later)) (carry-on [] 5)))))\" :file \"/tmp/buf.flan\")"
|
||||||
in
|
in
|
||||||
if status r <> "ok" then
|
if status r <> "ok" then
|
||||||
fail "installing a renamed body while stopped: %s"
|
fail "installing a renamed body while stopped: %s"
|
||||||
@ -4574,7 +4662,12 @@ let () =
|
|||||||
("label", "string", "\"hello\"");
|
("label", "string", "\"hello\"");
|
||||||
("p", "Point", "(Point {.x 1.5 .y 2.5})");
|
("p", "Point", "(Point {.x 1.5 .y 2.5})");
|
||||||
("xs", "[3 i32]", "[ 10 20 30]");
|
("xs", "[3 i32]", "[ 10 20 30]");
|
||||||
("flag", "bool", "true") ]
|
("flag", "bool", "true");
|
||||||
|
(* Same two rows the LLVM listing pins: the loop index shown,
|
||||||
|
the loop's hidden bound hidden, the shadowing rebind kept
|
||||||
|
raw because the outer [label] is on the same list. *)
|
||||||
|
("hop", "i32", "0");
|
||||||
|
("label~2", "string", "\"inner\"") ]
|
||||||
in
|
in
|
||||||
let got = triples r "locals" in
|
let got = triples r "locals" in
|
||||||
if got <> want then
|
if got <> want then
|
||||||
|
|||||||
77
vendor/agent/flan_agent.c
vendored
77
vendor/agent/flan_agent.c
vendored
@ -325,6 +325,12 @@ extern void *flan_dev_frame_slot(const void *frame, int32_t i);
|
|||||||
extern const uint8_t *flan_restart_name(int32_t i, int64_t *len);
|
extern const uint8_t *flan_restart_name(int32_t i, int64_t *len);
|
||||||
extern void *flan_restart_frame(int32_t i);
|
extern void *flan_restart_frame(int32_t i);
|
||||||
extern void flan_restart_take(void *frame, void *xfer);
|
extern void flan_restart_take(void *frame, void *xfer);
|
||||||
|
/* Where the expression that trapped is written (runtime/flan_rt.c). Set by
|
||||||
|
* the trap sites around their call into the break hook and NULL otherwise,
|
||||||
|
* so it is read here exactly once, while the snapshot is being taken on the
|
||||||
|
* thread the trap stopped. */
|
||||||
|
extern const uint8_t *flan_break_site;
|
||||||
|
extern int64_t flan_break_site_len;
|
||||||
|
|
||||||
/* -- How far down a transfer can actually land ----------------------- */
|
/* -- How far down a transfer can actually land ----------------------- */
|
||||||
|
|
||||||
@ -441,6 +447,18 @@ typedef struct {
|
|||||||
int32_t fmine[FRAME_MAX]; /* 0 = the evaluation's, not the
|
int32_t fmine[FRAME_MAX]; /* 0 = the evaluation's, not the
|
||||||
* program's */
|
* program's */
|
||||||
char ftext[FRAME_TEXT];
|
char ftext[FRAME_TEXT];
|
||||||
|
/* The condition this break was entered with, or NULL for a trap that
|
||||||
|
* carries none. An address into the signalling frame, which is live for
|
||||||
|
* exactly as long as this snapshot is on top — nothing unwound — so a
|
||||||
|
* render thunk aimed at it through [flan_agent_condition] reads storage
|
||||||
|
* that is still there. Never dereferenced here: its type is the daemon's
|
||||||
|
* to know, and rendering it is the daemon-built thunk's job. */
|
||||||
|
void *cond;
|
||||||
|
/* Where the expression that trapped is written, copied from the runtime's
|
||||||
|
* [flan_break_site] at the same held-still moment as everything else.
|
||||||
|
* Empty for a stop with no site — a user (error ...), a (pause). */
|
||||||
|
int32_t sitelen;
|
||||||
|
char site[512];
|
||||||
} snapshot;
|
} snapshot;
|
||||||
|
|
||||||
/* One per nested break loop, because an inner break must not answer with the
|
/* One per nested break loop, because an inner break must not answer with the
|
||||||
@ -477,6 +495,17 @@ void *flan_agent_frame_slot(int64_t frame, int64_t slot) {
|
|||||||
return flan_dev_frame_slot(s->fframe[frame], (int32_t)slot);
|
return flan_dev_frame_slot(s->fframe[frame], (int32_t)slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The condition this break holds, for the render thunk the daemon builds to
|
||||||
|
* show its fields. Same contract as [flan_agent_frame_slot]: called on the
|
||||||
|
* stopped game thread, resolved against the snapshot on top *when the thunk
|
||||||
|
* runs*, NULL for a break that carries none — and the daemon delivers the
|
||||||
|
* thunk at-stop, so a resume between the asking and the running drops it
|
||||||
|
* rather than rendering one break's type over another break's pointer. */
|
||||||
|
void *flan_agent_condition(void) {
|
||||||
|
snapshot *s = snap_top();
|
||||||
|
return s == NULL ? NULL : s->cond;
|
||||||
|
}
|
||||||
|
|
||||||
static snapshot *snap_top(void) {
|
static snapshot *snap_top(void) {
|
||||||
int d = atomic_load(&snap_depth);
|
int d = atomic_load(&snap_depth);
|
||||||
return d <= 0 ? NULL : &snaps[d - 1];
|
return d <= 0 ? NULL : &snaps[d - 1];
|
||||||
@ -486,13 +515,21 @@ static snapshot *snap_top(void) {
|
|||||||
* to nest, which the caller reports rather than serving a stale one. */
|
* to nest, which the caller reports rather than serving a stale one. */
|
||||||
static int32_t snap_gen; /* monotone; 0 is "no snapshot" */
|
static int32_t snap_gen; /* monotone; 0 is "no snapshot" */
|
||||||
|
|
||||||
static int snap_push(int resumable) {
|
static int snap_push(int resumable, void *cond) {
|
||||||
int d = atomic_load(&snap_depth);
|
int d = atomic_load(&snap_depth);
|
||||||
if (d >= BREAK_MAX) return 0;
|
if (d >= BREAK_MAX) return 0;
|
||||||
snapshot *s = &snaps[d];
|
snapshot *s = &snaps[d];
|
||||||
int32_t n = flan_restart_count();
|
int32_t n = flan_restart_count();
|
||||||
s->gen = ++snap_gen;
|
s->gen = ++snap_gen;
|
||||||
s->resumable = resumable;
|
s->resumable = resumable;
|
||||||
|
s->cond = cond;
|
||||||
|
s->sitelen = 0;
|
||||||
|
if (flan_break_site != NULL && flan_break_site_len > 0) {
|
||||||
|
int64_t k = flan_break_site_len;
|
||||||
|
if (k > (int64_t)sizeof s->site) k = (int64_t)sizeof s->site;
|
||||||
|
memcpy(s->site, flan_break_site, (size_t)k);
|
||||||
|
s->sitelen = (int32_t)k;
|
||||||
|
}
|
||||||
s->total = n;
|
s->total = n;
|
||||||
s->used = 0;
|
s->used = 0;
|
||||||
s->n = 0;
|
s->n = 0;
|
||||||
@ -563,10 +600,11 @@ static void snap_pop(void) {
|
|||||||
if (d > 0) atomic_store(&snap_depth, d - 1);
|
if (d > 0) atomic_store(&snap_depth, d - 1);
|
||||||
}
|
}
|
||||||
/* The condition's class name, so an editor can say what stopped rather than
|
/* The condition's class name, so an editor can say what stopped rather than
|
||||||
* only that something did. It is all there is to say: the hook is handed the
|
* only that something did. The pointer beside it goes into the snapshot: this
|
||||||
* name and an opaque pointer, and nothing at run time can render a value whose
|
* side still cannot render a value whose type it does not know, but the
|
||||||
* type it does not know. Written before [broken] is set and read only while
|
* daemon knows the type — it compiled it — and builds a thunk that reads the
|
||||||
* [broken] is 1, so the listener never sees half of it. */
|
* fields through [flan_agent_condition]. Written before [broken] is set and
|
||||||
|
* read only while [broken] is 1, so the listener never sees half of it. */
|
||||||
static char condition_name[128];
|
static char condition_name[128];
|
||||||
|
|
||||||
int32_t flan_agent_poll(void);
|
int32_t flan_agent_poll(void);
|
||||||
@ -651,7 +689,6 @@ static _Noreturn void die_now(void) {
|
|||||||
static void break_loop_at(const uint8_t *name, int64_t namelen, void *condition,
|
static void break_loop_at(const uint8_t *name, int64_t namelen, void *condition,
|
||||||
void *xfer, int resumable) {
|
void *xfer, int resumable) {
|
||||||
struct timespec step = { 0, 2000000 }; /* 2ms */
|
struct timespec step = { 0, 2000000 }; /* 2ms */
|
||||||
(void)condition;
|
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
fprintf(stderr, "\nflan: unhandled %.*s — stopped, not dead.\n",
|
fprintf(stderr, "\nflan: unhandled %.*s — stopped, not dead.\n",
|
||||||
(int)namelen, (const char *)name);
|
(int)namelen, (const char *)name);
|
||||||
@ -660,7 +697,7 @@ static void break_loop_at(const uint8_t *name, int64_t namelen, void *condition,
|
|||||||
* are then the same list, numbered the same way, and the numbers are what a
|
* are then the same list, numbered the same way, and the numbers are what a
|
||||||
* choice is made of. */
|
* choice is made of. */
|
||||||
int32_t my_gen;
|
int32_t my_gen;
|
||||||
if (!snap_push(resumable)) {
|
if (!snap_push(resumable, condition)) {
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
fprintf(stderr, "flan: %d nested break loops - giving up rather than "
|
fprintf(stderr, "flan: %d nested break loops - giving up rather than "
|
||||||
"spinning\n", BREAK_MAX);
|
"spinning\n", BREAK_MAX);
|
||||||
@ -1010,6 +1047,32 @@ static void handle_line(char *line, sink *o) {
|
|||||||
reply(o, "running\n");
|
reply(o, "running\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* Whether the break on top holds a condition value a render thunk could be
|
||||||
|
* aimed at. [+] or [-] and nothing else: the pointer itself never crosses
|
||||||
|
* the wire — an address in another process's frame is not something the
|
||||||
|
* daemon can read — and the *type* is already answered by [status]. The
|
||||||
|
* daemon asks this before spending a build on a thunk that would render
|
||||||
|
* nothing. */
|
||||||
|
if (strcmp(line, "condition") == 0) {
|
||||||
|
if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; }
|
||||||
|
snapshot *s = snap_top();
|
||||||
|
if (s == NULL) { reply(o, "err no snapshot\n"); return; }
|
||||||
|
reply(o, s->cond != NULL ? "+\n" : "-\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Where the expression that trapped is written — file:line:col, or [-] for
|
||||||
|
* a stop that has no site (a user (error ...), a (pause)). The frame lines
|
||||||
|
* say where each call was; this is the only record of the indexing or the
|
||||||
|
* division itself. */
|
||||||
|
if (strcmp(line, "site") == 0) {
|
||||||
|
if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; }
|
||||||
|
snapshot *s = snap_top();
|
||||||
|
if (s == NULL) { reply(o, "err no snapshot\n"); return; }
|
||||||
|
if (s->sitelen > 0) emit(o, s->site, (size_t)s->sitelen);
|
||||||
|
else reply(o, "-");
|
||||||
|
reply(o, "\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
/* One line per restart, innermost first: the index it is taken by, a flag
|
/* One line per restart, innermost first: the index it is taken by, a flag
|
||||||
* for whether it can be taken at all, and the name. The index leads
|
* for whether it can be taken at all, and the name. The index leads
|
||||||
* because it is the identity - two frames can offer [retry] and only one
|
* because it is the identity - two frames can offer [retry] and only one
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user