flan/lib/wire.ml
Joseph Ferano 4a6a8fa0f7 Take a restart by its position, off a list that stopped moving
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.
2026-09-12 04:56:18 +07:00

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"