Two frames offering `retry` put both on the break loop's list and only the inner one within reach: §4's walk takes the first frame offering a name, by definition, so the outer clause was drawn, offered, and unreachable. The old prompt showed `retry` twice and sent the string either way. An index is the only thing that can say which one, which is why SBCL identifies them positionally too. An index is worthless against a stack that moves, though, and this one moves: the break loop is the poll loop, so every restart-case an evaluation enters pushes and pops the same global list between the listing and the choice. So the list is read once on entry and copied — names into the agent's own buffer, frames as the addresses a transfer carries — and every answer comes from that. The name still travels with the index as a receipt, checked against the snapshot and refused if the two have drifted, so a bare integer can be wrong out loud. And the third state. A restart below the thunk a break is inside was accepted, announced, and silently not taken: `flan_reload_call` holds its own transfer channel and drops it on return, so the unwind stops at the thunk. The boundary is now recorded where it is made, at the call — frames a restart-case inside the thunk pushes are above it and still work — and such a restart is listed, marked, and refused with the reason. `break.flan` grew the shadowed pair, and 900 is a value no by-name lookup in that file can produce.
106 lines
3.4 KiB
OCaml
106 lines
3.4 KiB
OCaml
(** The editor protocol: one s-expression per message, length framed.
|
|
|
|
Not bencode and not nREPL, and the reason is that both ends are ours. There
|
|
is no CIDER to be compatible with, nREPL's [eval] is string-in/string-out
|
|
with no slot for *which form, from which file*, and Emacs already has
|
|
[read] and [prin1] — so a sexp protocol is no parsing code on that side and
|
|
a few lines here, where the reader that parses it is the language's own.
|
|
An nREPL server can be a second front end on the same [Session] later; it
|
|
should not gate the editor.
|
|
|
|
Framing is a decimal byte count, a newline, then that many bytes. A message
|
|
carries Flan source, which contains newlines, so a line-oriented protocol
|
|
would need an escape layer that this does not. *)
|
|
|
|
(* Elisp's [read] understands \\n and \\t but not OCaml's \\ddd, so this
|
|
escapes the two characters that must be escaped and passes everything else
|
|
through as itself. Both readers take a raw newline inside a string. *)
|
|
let quote s =
|
|
let b = Buffer.create (String.length s + 8) in
|
|
Buffer.add_char b '"';
|
|
String.iter
|
|
(fun c ->
|
|
if c = '"' || c = '\\' then Buffer.add_char b '\\';
|
|
Buffer.add_char b c)
|
|
s;
|
|
Buffer.add_char b '"';
|
|
Buffer.contents b
|
|
|
|
let list items = "(" ^ String.concat " " items ^ ")"
|
|
let strings ss = list (List.map quote ss)
|
|
|
|
let send fd payload =
|
|
let framed = Printf.sprintf "%d\n%s" (String.length payload) payload in
|
|
let n = String.length framed in
|
|
let rec go i =
|
|
if i < n then
|
|
match Unix.write_substring fd framed i (n - i) with
|
|
| 0 -> ()
|
|
| k -> go (i + k)
|
|
in
|
|
go 0
|
|
|
|
exception Closed
|
|
|
|
let read_exactly fd n =
|
|
let b = Bytes.create n in
|
|
let rec go i =
|
|
if i = n then Bytes.to_string b
|
|
else
|
|
match Unix.read fd b i (n - i) with
|
|
| 0 -> raise Closed
|
|
| k -> go (i + k)
|
|
in
|
|
go 0
|
|
|
|
(* The header is short and read a byte at a time, which keeps the payload
|
|
boundary exact without a buffer that would have to be carried between
|
|
calls. *)
|
|
let recv fd =
|
|
let b = Bytes.create 1 in
|
|
let buf = Buffer.create 16 in
|
|
let rec header () =
|
|
match Unix.read fd b 0 1 with
|
|
| 0 -> raise Closed
|
|
| _ ->
|
|
if Bytes.get b 0 = '\n' then Buffer.contents buf
|
|
else begin Buffer.add_char buf (Bytes.get b 0); header () end
|
|
in
|
|
let n =
|
|
match int_of_string_opt (String.trim (header ())) with
|
|
| Some n when n >= 0 -> n
|
|
| _ -> raise Closed
|
|
in
|
|
read_exactly fd n
|
|
|
|
(* A request is read by the language's own reader, so [:op] is a keyword and a
|
|
payload of Flan source is an ordinary string literal. *)
|
|
let field (form : Form.t) key =
|
|
let rec go = function
|
|
| { Form.v = Form.Kw k; _ } :: v :: rest ->
|
|
if String.equal k key then Some v else go rest
|
|
| _ :: rest -> go rest
|
|
| [] -> None
|
|
in
|
|
match form.Form.v with Form.List l -> go l | _ -> None
|
|
|
|
let string_field form key =
|
|
match field form key with
|
|
| Some { Form.v = Form.Str s; _ } -> Some s
|
|
| _ -> None
|
|
|
|
(* A restart is chosen by index, so the protocol has to carry one. Kept as
|
|
narrow as [string_field]: a form that is not an integer is [None] and the
|
|
op says what it wanted, rather than this guessing at a string. *)
|
|
let int_field form key =
|
|
match field form key with
|
|
| Some { Form.v = Form.Int i; _ } -> Some (Int64.to_int i)
|
|
| _ -> None
|
|
|
|
let ints ns = list (List.map string_of_int ns)
|
|
|
|
let parse src =
|
|
match Reader.read_all ~file:"<wire>" src with
|
|
| [ f ] -> f
|
|
| _ -> Loc.fail Loc.unknown "one form per message"
|