A type's fields over the wire, keyed by the qualified name that is already an identity
This commit is contained in:
commit
bd030c9e51
51
BUILT.md
51
BUILT.md
@ -1009,6 +1009,57 @@ state an editor has to cope with and the hardest one to arrange later. The Emacs
|
||||
round, by installing a `step` that errors into a loop that calls it, fixes it while stopped, and then resumes: `C-x C-e`
|
||||
answering while the program sits in the break loop is checked there against the real client, not only in OCaml.
|
||||
|
||||
### `layout` — a type's fields, with no program involved
|
||||
|
||||
```
|
||||
(:op "layout" :type "sim/Cell") → (:status "ok" :type "sim/Cell"
|
||||
:fields (("heat" "f32") ("next" "(Option sim/Cell)")))
|
||||
→ (:status "error" :message "Missing is not a qualified name; …"
|
||||
:candidates ("a/Missing" "b/Missing"))
|
||||
```
|
||||
|
||||
The daemon can answer this with nothing running. A layout is a fact about the *build*, and the daemon owns the build —
|
||||
`Tast.structs` is sitting in the session it compiled the process from. That is why the conditions buffer can name and
|
||||
type a condition's fields while every one of their *values* stays refused: the shape is knowable and the contents are
|
||||
not, and drawing them apart says more than drawing neither.
|
||||
|
||||
**The type is a name, and the name is the qualified one.** This was the open question — a class name is not an identity,
|
||||
and two packages each declaring `Missing` would leave the daemon unable to pick. It turned out to need no new
|
||||
machinery: `Load.qualify_decl` rewrites `Defstruct (n, …)` to `Defstruct (alias/n, …)` at import, so by the time
|
||||
anything reaches `Tast.structs` the names are one flat namespace in which a collision cannot exist. The name *is* the
|
||||
type id, with no table to keep in step across a reload, and the existing spelling of a type — `Types.to_string` —
|
||||
already prints it.
|
||||
|
||||
**And the break loop was already speaking it.** `Emit.struct_name_of` takes `Types.Named n` — the qualified name — and
|
||||
passes it to `flan_error`; the agent holds it in `condition_name`; `break` answers it as `:condition`. So the string
|
||||
the conditions buffer already had in hand resolves as `:type` by construction, and `test_dev.ml` round-trips exactly
|
||||
that: the condition a stopped program reports, handed straight back, answers with that condition's fields. One caveat
|
||||
worth writing down — `condition_name` is a `char[128]`, so the round trip holds for names up to 127 bytes and a longer
|
||||
one is truncated and will not resolve.
|
||||
|
||||
**A bare name is refused, not resolved**, even when only one struct's last segment matches it. Resolving a unique
|
||||
suffix would reintroduce the ambiguity the rule exists to remove, and a rule with an exception is one a client cannot
|
||||
rely on. The refusal carries `:candidates`, so a person is one copy-paste from the answer and a client has its
|
||||
completion list — the same shape as `package_of` refusing a directory imported under two aliases rather than picking
|
||||
one. An enum is refused by *kind* (`X is an enum, not a struct`): its members are erased to `i32` before `Tast.program`
|
||||
exists, which is the same fact that makes a `defenum` unreloadable. A union is refused the same way and for its own
|
||||
reason — it is declared, and union *values* are milestone 6. Both are `Types.Named` at a use site, so falling through
|
||||
to "no struct is named X" would say a type does not exist about one that plainly does.
|
||||
|
||||
**`render.ml` is not reused, and that is not a second walk.** It walks a *value* and emits the code that prints it;
|
||||
this describes a *type* and emits text. What is shared is the spelling: field types go through `Types.to_string`, which
|
||||
is what `defs` spells a signature with, so `(Option T)`, `[T]`, `[n T]` and `(Ptr T)` read the same in a layout, in a
|
||||
signature and in the source. A field that is itself a struct shows its qualified name — which is a `:type` this op
|
||||
accepts, so nesting is another request rather than a recursion, and nothing here can be made to walk forever. Prelude
|
||||
structs are answered like any other, because `Render` resolves against the same list and an editor that could see a
|
||||
`Split` printed but not ask about it would be the two disagreeing.
|
||||
|
||||
On the Emacs side `flan-cnr-layout` makes the request and `flan-cnr-show` passes the result into
|
||||
`flan-cnr-state-from-reply`, which stays a function from data to data so the fixture-driven tests keep working without
|
||||
a socket. A refusal is nil, not an error: the buffer already draws a section explaining why a section is empty, and
|
||||
turning `C-c C-b` into an error would take away the restarts — the decision the buffer exists for — over a missing
|
||||
annotation.
|
||||
|
||||
### Conditions — step 2: `restart-case` and `invoke-restart`
|
||||
|
||||
`spec-conditions.md` §3 to §6: the transfer. A handler runs where the signal was, decides, and control resumes at a
|
||||
|
||||
12
NEXT.md
12
NEXT.md
@ -521,15 +521,15 @@ Sixty mutations, nineteen left the whole suite green. The severe cluster is clos
|
||||
|
||||
### Asked for by the editor lanes
|
||||
|
||||
- **`(:op "layout" :type T)` → the struct's fields and their types.** `Tast.structs` is held by the daemon at all
|
||||
times because it owns the build, and **no running program is involved** — this is the cheapest real win on the
|
||||
list, and the C&R buffer already draws its result.
|
||||
- **`(:op "condition")` → the stopped program's condition, rendered.** Two steps: `break_loop` currently does
|
||||
`(void)condition;` and *discards the pointer*, so stash it beside `condition_name`; then the daemon builds a render
|
||||
thunk aimed at that address, which is `Session.render` rooted at a `Ptr` instead of an expression.
|
||||
- **One thing to get right for both:** the type must be an identity the daemon can resolve to a `Tast` type, not a
|
||||
bare class name. The hook is handed a string, and two packages both declaring `Missing` leave the daemon unable to
|
||||
pick a layout. A qualified name or a type id. The same wrinkle bites locals later, because DWARF also gives a name.
|
||||
- **The type identity is settled, and it is the qualified name** — `layout` is in, see BUILT.md. `Load` qualifies
|
||||
every declaration at import, so the names in `Tast.structs` are a flat namespace where two packages' `Missing` are
|
||||
`a/Missing` and `b/Missing`; a bare name is refused with the candidates rather than resolved. `condition` inherits
|
||||
it for free: the string the break loop already reports *is* that name, because `Emit.struct_name_of` writes
|
||||
`Types.Named` into `flan_error`. It is still open for **locals**, where DWARF gives a name and the name a debugger
|
||||
reads is not qualified by anything.
|
||||
- **`(:op "backtrace")` is blocked** on frame metadata — unlocked by the DWARF work, then a new agent verb. Locals are
|
||||
blocked twice: DWARF for the frame layout, *and* the pointer-rooted render thunk. Restart source locations and
|
||||
arity are blocked too — `flan_restart` carries `prev`, `name_id`, `name` and `namelen`, so both need a new field in
|
||||
|
||||
@ -160,6 +160,20 @@ silently omitted.
|
||||
**`C-c C-M-b`** is the same choice as a quick one-key prompt, when you already
|
||||
know which restart you want and do not need the buffer.
|
||||
|
||||
**The condition's fields are named and typed, and have no values.** Under the
|
||||
condition you get the struct it is — `:path string`, `:tried i32` — because the
|
||||
daemon compiled the program and knows what that type looks like without asking
|
||||
the program anything. What is beside each field is a note saying the value is
|
||||
not available, not a blank: a value lives in the stopped frame, and nothing yet
|
||||
hands the break loop's condition pointer back. Knowing the shape is still worth
|
||||
having — it tells you whether the field you were about to blame is a field of
|
||||
this condition at all.
|
||||
|
||||
If that section says it could not resolve the name, read it: a package
|
||||
qualifies what it declares, so two packages' `Missing` are `a/Missing` and
|
||||
`b/Missing`. The daemon refuses a bare name and says what it could have meant
|
||||
rather than picking one.
|
||||
|
||||
After you choose, the program carries on from the restart. It never unwound, so
|
||||
everything it had is still there.
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ from fixtures, and so `flan-dev.el' is named in one place.")
|
||||
|
||||
(defconst flan-cnr-unavailable
|
||||
'((layout
|
||||
. "which struct the class name refers to [needs a daemon op: `layout'. The field names and types are in `Tast.structs', which the daemon holds because it owns the build — no running program is involved. It is the one piece of this buffer that is a day's work on one side of the socket]")
|
||||
. "the daemon could not resolve this condition's name to a struct [the `layout' op answers out of `Tast.structs' and needs a *qualified* name. A package qualifies what it declares, so two packages' `Missing' are two names and neither is `Missing'; the daemon refuses a bare one and lists what it could have meant rather than picking. A name it cannot place at all is a program built from source this daemon did not compile]")
|
||||
(value
|
||||
. "the break loop is handed the condition as an opaque pointer, and `break_loop' currently discards it; nothing at run time can render a value whose type it does not know [needs an agent verb: `condition', to stash and hand back the pointer, and a daemon-built render thunk aimed at it]")
|
||||
(site
|
||||
@ -382,20 +382,51 @@ list already says it."
|
||||
"What a stopped Flan program is offering."
|
||||
(setq buffer-read-only t))
|
||||
|
||||
(defun flan-cnr-state-from-reply (reply)
|
||||
(defun flan-cnr-state-from-reply (reply &optional fields)
|
||||
"The buffer's state, out of a `break' REPLY.
|
||||
FIELDS is the condition's layout, if it was asked for and answered — a list of
|
||||
(NAME TYPE VALUE), where VALUE is nil because no running program was consulted
|
||||
to get it.
|
||||
|
||||
Everything this cannot fill in is left nil deliberately: the renderer draws a
|
||||
section saying why rather than leaving one out, and a section that is absent
|
||||
cannot be told from one that is empty."
|
||||
cannot be told from one that is empty.
|
||||
|
||||
Takes the layout rather than fetching it, so this stays a function from data to
|
||||
data and the fixture-driven tests can drive it without a socket."
|
||||
(list :condition (plist-get reply :condition)
|
||||
:restarts (plist-get reply :restarts)
|
||||
;; Both nil deliberately: `break' answers a class name and a list of
|
||||
;; restart names, and nothing else exists to put here yet. The renderer
|
||||
;; draws a section saying why rather than leaving one out.
|
||||
:fields nil
|
||||
:fields fields
|
||||
;; Still nil deliberately: nothing here is attached to the stopped
|
||||
;; program's frames, and the renderer says so by name.
|
||||
:stack nil
|
||||
:locals nil))
|
||||
|
||||
(defun flan-cnr-layout (type)
|
||||
"The fields of TYPE, as the renderer wants them, or nil.
|
||||
`layout' is answered out of the daemon's own `Tast.structs' — it owns the
|
||||
build — so this costs no round trip to the program and works while it is
|
||||
stopped, which is the only moment this buffer exists in.
|
||||
|
||||
TYPE is the string `break' reported as `:condition', and that string is the
|
||||
qualified struct name the compiler put into the error: `Emit' takes it from
|
||||
`Types.Named', the agent holds it in `condition_name', and the daemon looks it
|
||||
up by the same name. So it resolves without anything here knowing what package
|
||||
it came from.
|
||||
|
||||
A refusal is not an error here: nil means the layout section draws the reason
|
||||
it is empty, which is the degradation the buffer is already built for."
|
||||
(when (and type (not (string-empty-p type)))
|
||||
(let ((r (funcall flan-cnr-request-function (list :op "layout" :type type))))
|
||||
(when (equal (plist-get r :status) "ok")
|
||||
(mapcar (lambda (f)
|
||||
;; The value is nil, and that is the honest half: the shape
|
||||
;; of a condition is a fact about the build, its contents
|
||||
;; are a fact about the stopped frame, and only the first is
|
||||
;; knowable today.
|
||||
(list (nth 0 f) (nth 1 f) nil))
|
||||
(plist-get r :fields))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun flan-cnr-show ()
|
||||
"Show what the stopped program is offering, in a buffer.
|
||||
@ -410,7 +441,8 @@ walk from a running program."
|
||||
(let ((buf (get-buffer-create flan-cnr-buffer)))
|
||||
(with-current-buffer buf
|
||||
(unless (derived-mode-p 'flan-cnr-mode) (flan-cnr-mode))
|
||||
(flan-cnr--render (flan-cnr-state-from-reply r)))
|
||||
(flan-cnr--render
|
||||
(flan-cnr-state-from-reply r (flan-cnr-layout (plist-get r :condition)))))
|
||||
(pop-to-buffer buf)
|
||||
buf)))
|
||||
|
||||
|
||||
@ -465,6 +465,50 @@
|
||||
(test-flan--check "and n goes back to abort"
|
||||
(get-text-property (point) 'flan-cnr-abort)))
|
||||
|
||||
|
||||
;; The `layout' op, from this side: the condition's own name goes out as
|
||||
;; `:type' and comes back as fields with no values. Two requests are made for
|
||||
;; one `C-c C-b' — `break' then `layout' — so the stub records both.
|
||||
(let ((asked nil))
|
||||
(let ((flan-cnr-request-function
|
||||
(lambda (form)
|
||||
(push form asked)
|
||||
(pcase (plist-get form :op)
|
||||
("break" '(:status "ok" :stopped t :condition "sim/Missing"
|
||||
:restarts ("retry")))
|
||||
("layout" '(:status "ok" :type "sim/Missing"
|
||||
:fields (("path" "string") ("tried" "i32"))))))))
|
||||
(let ((text (with-current-buffer (save-window-excursion (flan-cnr-show))
|
||||
(buffer-string))))
|
||||
(test-flan--check "the condition's name is what `layout' is asked for"
|
||||
(equal (plist-get (car (last asked)) :op) "break"))
|
||||
(test-flan--check "and it is sent back verbatim, qualified as it came"
|
||||
(equal (plist-get (car asked) :type) "sim/Missing"))
|
||||
(test-flan--check "the fields are drawn, named and typed"
|
||||
(string-match-p ":path *string" text))
|
||||
;; Shape and contents are two different questions, and only the first is
|
||||
;; answerable without the pointer the break loop discards.
|
||||
(test-flan--check "and every value still says why it is missing"
|
||||
(string-match-p ":tried *i32 *value not available" text)))))
|
||||
|
||||
;; A layout the daemon refuses — a bare name it will not guess between two
|
||||
;; packages, or a type it cannot place — leaves the section drawing its reason
|
||||
;; rather than turning `C-c C-b' into an error. The restarts are the decision
|
||||
;; in front of you and they are still there.
|
||||
(let ((flan-cnr-request-function
|
||||
(lambda (form)
|
||||
(pcase (plist-get form :op)
|
||||
("break" '(:status "ok" :stopped t :condition "Missing"
|
||||
:restarts ("retry")))
|
||||
("layout" '(:status "error" :message "Missing is not a qualified name"
|
||||
:candidates ("a/Missing" "b/Missing")))))))
|
||||
(let ((text (with-current-buffer (save-window-excursion (flan-cnr-show))
|
||||
(buffer-string))))
|
||||
(test-flan--check "a refused layout is a section that says so"
|
||||
(string-match-p "not available.*needs a \\*qualified\\* name" text))
|
||||
(test-flan--check "and the restarts are drawn anyway"
|
||||
(string-match-p "\\[retry\\]" text))))
|
||||
|
||||
(let ((flan-cnr-request-function
|
||||
(lambda (_) '(:status "ok" :restarts nil :stopped nil))))
|
||||
(test-flan--check "a running program is refused, by name"
|
||||
|
||||
@ -105,6 +105,17 @@ is written instead — the real `message' call the real command makes."
|
||||
(test-flan--check "describe lists the program's globals"
|
||||
(member "ticks" (plist-get r :globals))))
|
||||
|
||||
;; `layout' against the real daemon, through `flan-cnr-layout', which is how
|
||||
;; the conditions buffer gets it. The reply is the first one with a list of
|
||||
;; lists in it, so `read' on this side is doing something it does nowhere
|
||||
;; else — and the daemon answers it without asking the program anything.
|
||||
(require 'flan-cnr)
|
||||
(let ((flan-cnr-request-function #'flan-dev--request))
|
||||
(test-flan--check "a struct's fields come back named and typed"
|
||||
(equal (flan-cnr-layout "Missing") '(("id" "i32" nil))))
|
||||
(test-flan--check "and a type the daemon cannot place is nil, not an error"
|
||||
(null (flan-cnr-layout "Nonesuch"))))
|
||||
|
||||
;; C-c C-c on the form at point: put point inside `step' and send it. The
|
||||
;; text comes from the buffer, so this exercises `beginning-of-defun' against
|
||||
;; Flan's own syntax table as much as it does the wire.
|
||||
|
||||
90
lib/dev.ml
90
lib/dev.ml
@ -478,6 +478,92 @@ let defs t =
|
||||
in
|
||||
ok [ ":defs " ^ Wire.list (fns @ globals @ externs) ]
|
||||
|
||||
(* [(:op "layout" :type T)] — a struct's fields and their types.
|
||||
|
||||
The daemon can answer this with no running program at all: [Tast.structs] is
|
||||
what it built the process from, and a layout is a fact about the build. That
|
||||
is why it is the one thing the conditions buffer can fill in while the
|
||||
condition's *values* stay refused.
|
||||
|
||||
**The type is a name, and the name is the qualified one.** [Load] qualifies
|
||||
every declaration as it imports it — [Defstruct (qualify alias n, ...)] — so
|
||||
the names in [Tast.structs] are a flat namespace in which two packages each
|
||||
declaring [Missing] are [a/Missing] and [b/Missing] and no collision is
|
||||
possible. That makes the name a type identity rather than a class name, with
|
||||
no id table to keep in step, and it is the same string on both ends of the
|
||||
wire already: [Emit.struct_name_of] puts [Types.Named n] into [flan_error],
|
||||
the agent holds it in [condition_name], and [break] answers it as
|
||||
[:condition]. Handing that string straight back as [:type] therefore
|
||||
resolves, by construction.
|
||||
|
||||
A bare name is **refused, not resolved**, even when only one struct's last
|
||||
segment matches: resolving it is exactly the ambiguity that made this op
|
||||
need a rule, and a rule with an exception cannot be relied on by a client.
|
||||
The refusal lists the qualified names it could have meant, so a person who
|
||||
typed [Missing] is one copy-paste from the answer and a client can offer
|
||||
them as completions.
|
||||
|
||||
Field types are spelled by [Types.to_string], which is what [defs] spells a
|
||||
signature with — so [(Option T)], [[T]], [[n T]] and [(Ptr T)] read here
|
||||
exactly as they read in a signature and in the source. A field that is
|
||||
itself a struct shows its qualified name, which is a [:type] this op
|
||||
accepts: nesting is another request rather than a second walk, and nothing
|
||||
here can recurse forever. [Render] is the other walk over a type and is not
|
||||
reused, because it walks a *value* and emits code that prints it; this emits
|
||||
text about the type and never touches the program. *)
|
||||
let layout t ~ty =
|
||||
let structs = t.session.Session.program.Tast.structs in
|
||||
match
|
||||
List.find_opt (fun (s : Tast.structure) -> String.equal s.Tast.sname ty)
|
||||
structs
|
||||
with
|
||||
| Some s ->
|
||||
ok
|
||||
[ ":type " ^ Wire.quote s.Tast.sname;
|
||||
":fields "
|
||||
^ Wire.list
|
||||
(List.map
|
||||
(fun (f : Tast.field) ->
|
||||
Wire.list
|
||||
[ Wire.quote f.Tast.fname;
|
||||
Wire.quote (Types.to_string f.Tast.fty) ])
|
||||
s.Tast.fields) ]
|
||||
| None ->
|
||||
(* Two types the checker knows and this op cannot describe. An enum's
|
||||
members are erased to i32 before [Tast.program] exists, which is the
|
||||
same fact that makes a defenum unreloadable; a union is declared and
|
||||
has no values yet. Either way, saying which kind it is beats "no such
|
||||
type" for a name that plainly exists. *)
|
||||
if Hashtbl.mem t.session.Session.env.Check.enums ty then
|
||||
error (ty ^ " is an enum, not a struct; its members are erased to i32")
|
||||
else if
|
||||
List.exists (fun (u : Tast.union) -> String.equal u.Tast.uname ty)
|
||||
t.session.Session.program.Tast.unions
|
||||
then
|
||||
error (ty ^ " is a union, not a struct; union values are milestone 6")
|
||||
else
|
||||
let suffix = "/" ^ ty in
|
||||
let candidates =
|
||||
List.filter_map
|
||||
(fun (s : Tast.structure) ->
|
||||
let n = s.Tast.sname in
|
||||
let k = String.length n - String.length suffix in
|
||||
if k >= 0 && String.equal (String.sub n k (String.length suffix)) suffix
|
||||
then Some n else None)
|
||||
structs
|
||||
in
|
||||
(match candidates with
|
||||
| [] -> error ("no struct is named " ^ ty)
|
||||
| cs ->
|
||||
(* Resolved on the client's side, deliberately: two packages can each
|
||||
declare [Missing], and picking one of them here would answer a
|
||||
layout for a type the asker did not mean. *)
|
||||
"(:status \"error\" :message "
|
||||
^ Wire.quote
|
||||
(ty ^ " is not a qualified name; a package qualifies its \
|
||||
declarations, so say which one")
|
||||
^ " :candidates " ^ Wire.strings cs ^ ")")
|
||||
|
||||
(* What is on offer where the program stopped. [:stopped] and [:condition] are
|
||||
not here: the annotation puts them on this reply as it puts them on every
|
||||
other, so an editor reads the same two keys whatever it asked. What this op
|
||||
@ -896,6 +982,10 @@ let handle t req =
|
||||
| Some "describe" -> describe t
|
||||
| Some "defs" -> defs t
|
||||
| Some "break" -> break t
|
||||
| Some "layout" ->
|
||||
(match Wire.string_field req "type" with
|
||||
| Some ty -> layout t ~ty
|
||||
| None -> error "layout needs :type")
|
||||
| Some "restart" ->
|
||||
(match Wire.string_field req "name" with
|
||||
| Some name -> choose t ~name
|
||||
|
||||
138
test/test_dev.ml
138
test/test_dev.ml
@ -126,6 +126,123 @@ let () =
|
||||
| None -> fail "defs did not mention an imported extern")
|
||||
| _ -> fail "defs did not answer with a list");
|
||||
|
||||
(* [layout]: a struct's fields and their types, out of [Tast.structs],
|
||||
with no running program involved at all. *)
|
||||
let strings_of f =
|
||||
match f with
|
||||
| Some { Form.v = Form.List xs; _ } ->
|
||||
List.filter_map
|
||||
(fun (x : Form.t) ->
|
||||
match x.Form.v with Form.Str s -> Some s | _ -> None)
|
||||
xs
|
||||
| _ -> []
|
||||
in
|
||||
let fields r =
|
||||
match Wire.field r "fields" with
|
||||
| Some { Form.v = Form.List fs; _ } ->
|
||||
List.filter_map
|
||||
(fun (f : Form.t) ->
|
||||
match f.Form.v with
|
||||
| Form.List
|
||||
[ { Form.v = Form.Str n; _ }; { Form.v = Form.Str t; _ } ] ->
|
||||
Some (n ^ " " ^ t)
|
||||
| _ -> None)
|
||||
fs
|
||||
| _ -> []
|
||||
in
|
||||
let r = request c "(:op \"layout\" :type \"Missing\")" in
|
||||
if status r <> "ok" then
|
||||
fail "layout Missing: %s"
|
||||
(Option.value ~default:"" (Wire.string_field r "message"))
|
||||
else begin
|
||||
if Wire.string_field r "type" <> Some "Missing" then
|
||||
fail "layout answered a different type than it was asked for";
|
||||
if fields r <> [ "id i32" ] then
|
||||
fail "Missing's fields: %s" (String.concat ", " (fields r))
|
||||
end;
|
||||
|
||||
(* The prelude's structs are in [Tast.structs] because [Check.program]
|
||||
prepends the prelude, and they are answered for the same reason the
|
||||
REPL's renderer resolves against the same list: an editor that could
|
||||
see a type printed and not ask about it would be the two disagreeing.
|
||||
[Rune] also pins the spelling — the types read exactly as [defs]
|
||||
spells a signature, because both go through [Types.to_string]. *)
|
||||
let r = request c "(:op \"layout\" :type \"Split\")" in
|
||||
if fields r <> [ "rest [u8]"; "sep u8"; "more bool" ] then
|
||||
fail "Split's fields: %s" (String.concat ", " (fields r));
|
||||
|
||||
let r = request c "(:op \"layout\" :type \"Nonesuch\")" in
|
||||
if status r <> "error" then fail "a type that does not exist got a layout";
|
||||
|
||||
(* A name that plainly exists and is not a struct is refused by *kind*.
|
||||
Both of these are types the checker knows and this op cannot
|
||||
describe, and "no struct is named X" would read as "X does not
|
||||
exist". *)
|
||||
let refusal r =
|
||||
Option.value ~default:(status r) (Wire.string_field r "message")
|
||||
in
|
||||
let contains hay needle =
|
||||
let n = String.length needle in
|
||||
let rec go i =
|
||||
i + n <= String.length hay
|
||||
&& (String.equal (String.sub hay i n) needle || go (i + 1))
|
||||
in
|
||||
go 0
|
||||
in
|
||||
let r =
|
||||
request c
|
||||
"(:op \"eval\" :code \"(defenum Colour [red 0 green 1])\" :file \"/tmp/buf.flan\")"
|
||||
in
|
||||
if status r <> "ok" then fail "a new enum: %s" (refusal r)
|
||||
else begin
|
||||
let r = request c "(:op \"layout\" :type \"Colour\")" in
|
||||
if status r <> "error" then fail "an enum answered a struct layout"
|
||||
else if not (contains (refusal r) "is an enum") then
|
||||
fail "an enum is refused as: %s" (refusal r)
|
||||
end;
|
||||
let r =
|
||||
request c
|
||||
"(:op \"eval\" :code \"(defunion Shape [(Circle [r f32])])\" :file \"/tmp/buf.flan\")"
|
||||
in
|
||||
if status r <> "ok" then fail "a new union: %s" (refusal r)
|
||||
else begin
|
||||
let r = request c "(:op \"layout\" :type \"Shape\")" in
|
||||
if status r <> "error" then fail "a union answered a struct layout"
|
||||
else if not (contains (refusal r) "is a union") then
|
||||
fail "a union is refused as: %s" (refusal r)
|
||||
end;
|
||||
|
||||
(* The identity rule, and the case NEXT.md named: a second [Blob] typed
|
||||
into a package is [agent/Blob], the qualified name resolves, and the
|
||||
bare one is refused with the names it could have meant rather than
|
||||
resolved to either. The daemon derives the package from the path, so
|
||||
the file this is sent with is the one the import qualified. *)
|
||||
let agent_file =
|
||||
let p = "../vendor/agent/agent.flan" in
|
||||
try Unix.realpath p with Unix.Unix_error _ -> p
|
||||
in
|
||||
let r =
|
||||
request c
|
||||
(Printf.sprintf
|
||||
"(:op \"eval\" :code \"(defstruct Blob [id i32])\" :file %s)"
|
||||
(Wire.quote agent_file))
|
||||
in
|
||||
if status r <> "ok" then
|
||||
fail "a struct typed into a package: %s"
|
||||
(Option.value ~default:"" (Wire.string_field r "message"))
|
||||
else begin
|
||||
let r = request c "(:op \"layout\" :type \"agent/Blob\")" in
|
||||
if status r <> "ok" || fields r <> [ "id i32" ] then
|
||||
fail "a qualified name did not resolve: %s"
|
||||
(Option.value ~default:(status r) (Wire.string_field r "message"));
|
||||
let r = request c "(:op \"layout\" :type \"Blob\")" in
|
||||
if status r <> "error" then
|
||||
fail "a bare package-qualified name was resolved rather than refused"
|
||||
else if strings_of (Wire.field r "candidates") <> [ "agent/Blob" ] then
|
||||
fail "the refusal did not name what it could have meant: %s"
|
||||
(String.concat ", " (strings_of (Wire.field r "candidates")))
|
||||
end;
|
||||
|
||||
(* A form that does not check comes back as an error with a location,
|
||||
and must not disturb the session. *)
|
||||
let r = request c "(:op \"eval\" :code \"(defn step [] i64 nonsense)\" :file \"/tmp/buf.flan\")" in
|
||||
@ -257,6 +374,27 @@ let () =
|
||||
fail "the condition is reported as %S, wanted %S" (condition !last)
|
||||
"Missing";
|
||||
|
||||
(* The identity claim, round-tripped: the string [break] reports is the
|
||||
qualified struct name [Emit] put into [flan_error] and the agent
|
||||
held in [condition_name], so handing it straight back as [:type]
|
||||
has to resolve. This is the conditions buffer's whole path — it has
|
||||
the condition's name and nothing else, and asks for the fields with
|
||||
it. A layout that only answered a name typed by hand would leave
|
||||
that path guessing. *)
|
||||
let r =
|
||||
ask
|
||||
(Printf.sprintf "(:op \"layout\" :type %s)"
|
||||
(Wire.quote (condition !last)))
|
||||
in
|
||||
if status r <> "ok" then
|
||||
fail "the condition's own name did not resolve to a layout: %s"
|
||||
(Option.value ~default:"" (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"; _ } ]; _ } ]; _ } -> ()
|
||||
| _ -> fail "the stopped program's condition has the wrong layout");
|
||||
|
||||
(* What is on offer, innermost first. [break] carries the names and
|
||||
nothing else — the state is the annotation's business, so there is
|
||||
one place in the daemon that decides it. *)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user