A prompt on the running program
flan-repl.el is a comint buffer whose every line goes through the same eval-expr request C-x C-e uses - no new protocol, no compiler support. Deriving from comint rather than hand-rolling a prompt is the same call as deriving flan-mode from lisp-mode: history, the input ring and kill/yank already exist and are not worth rewriting. There is no subprocess behind it; the "process" is a stub comint needs in order to have a prompt. It is program-scoped: a name typed at the prompt resolves against the running program's top-level namespace, so in sand you write sim/settle. A buffer visiting a package's file gets the alias applied for it because the file says which package it belongs to, and a prompt has no file to derive one from. RET on a half-typed form opens a line instead of sending it, with balance checked through the Flan syntax table so a paren inside a string does not count. A value and the program's output are different things and arrive by different routes: the value is the result of the request and appears at the prompt, while anything printed rides along on the same reply into *flan-output*. Showing them in one place would be convenient and wrong, so there is a test for the separation - and it caught a real bug. The renderer's Unit case emitted () with no evaluation at all, so (print-line "x"), the most ordinary thing anyone types at a prompt, answered while nothing happened. A Unit expression is almost always a call made for its effect; it is evaluated and then reported.
This commit is contained in:
parent
20fedd4ad8
commit
5993875539
39
NEXT.md
39
NEXT.md
@ -46,7 +46,7 @@ reader ✅ → parse ✅ → load ✅ → check ✅ → emit ✅ → clang ✅
|
||||
| `runtime/flan_dev.c` | **dev only: the by-name registry a run-time-new name needs** |
|
||||
| `vendor/raylib/` | **the raylib package: `raylib.flan`, `shim.c`, `link`** |
|
||||
| `vendor/agent/` | **the dev agent: a socket, a loader thread, install at a frame boundary** |
|
||||
| `emacs/` | **`flan-mode.el` and `flan-dev.el`: the editor half of the dev loop** |
|
||||
| `emacs/` | **`flan-mode.el`, `flan-dev.el`, `flan-repl.el`: the editor half of the dev loop** |
|
||||
| `sand-sim/` | **the falling-sand simulation, with no raylib in it** |
|
||||
| `bin/main.ml` | `flan read \| parse \| check \| emit \| build \| run \| reload \| dev` |
|
||||
| `test/test_flan.ml` | reader, parser and checker |
|
||||
@ -687,6 +687,7 @@ of the protocol choice: `prin1` writes a request and `read` reads a reply.
|
||||
| `C-x C-e` | the expression before point, evaluated *in the running program* |
|
||||
| `C-c C-z` / `C-c C-q` | connect (finds `.flan-dev.sock` upward) / disconnect |
|
||||
| `C-c C-o` | the running program's own output, in `*flan-output*` |
|
||||
| `C-c C-r` | a prompt on the running program (`*flan-repl*`) |
|
||||
| `C-c C-d` | what the running program currently defines |
|
||||
|
||||
`C-c C-k` sends one module rather than a form at a time on purpose: a `defvar`
|
||||
@ -802,11 +803,39 @@ The test that matters is the same expression twice: the fixture increments
|
||||
`ticks` every frame, so two evaluations must disagree. A value computed in the
|
||||
compiler, or read out of a copy of the program's state, would not.
|
||||
|
||||
### The REPL buffer
|
||||
|
||||
`flan-repl.el` is a `comint-mode` buffer whose every line goes through the same
|
||||
`eval-expr` request `C-x C-e` uses. No new protocol and no compiler support.
|
||||
Deriving from `comint` rather than hand-rolling a prompt is the same call as
|
||||
deriving `flan-mode` from `lisp-mode`: history, the input ring and kill/yank
|
||||
already exist. There is no subprocess behind it — the "process" is a stub
|
||||
comint needs in order to have a prompt at all.
|
||||
|
||||
Three things about it that are decisions:
|
||||
|
||||
- **It is program-scoped.** A name typed at the prompt resolves against the
|
||||
running program's top-level namespace, so in sand you write `sim/settle` and
|
||||
not `settle`. A buffer visiting a package's own file gets the alias applied
|
||||
for it because the file says which package it belongs to; a prompt has no
|
||||
file and nothing to derive one from.
|
||||
- **RET on a half-typed form opens a line instead of sending it.** Balance is
|
||||
checked with the Flan syntax table, so a paren inside a string does not
|
||||
count.
|
||||
- **A value and the program's output are different things and arrive by
|
||||
different routes.** The value is the result of the request and appears at the
|
||||
prompt; anything the program printed while evaluating it rides along on the
|
||||
same reply and goes to `*flan-output*`. Showing them in one place would be
|
||||
convenient and wrong, so there is a test for the separation.
|
||||
|
||||
That test is what caught a real bug: the renderer's `Unit` case emitted `()`
|
||||
without evaluating the expression, so `(print-line "x")` — the most ordinary
|
||||
thing anyone types at a prompt — answered `()` while nothing happened. A Unit
|
||||
expression is almost always a call made for its effect, and is now evaluated
|
||||
and *then* reported.
|
||||
|
||||
### What is left
|
||||
- **A REPL buffer.** `C-x C-e` echoes into the minibuffer; there is no prompt
|
||||
to type at and no history.
|
||||
- **Printers beyond the scalars.** See below — a struct, an `(Option T)` or a
|
||||
slice of structs still refuses by name.
|
||||
- **Editor comforts**: completion, eldoc, jump-to-definition, error overlays.
|
||||
|
||||
**Session identity is the daemon that owns the build.** A session's struct
|
||||
layouts and global types have to describe the memory of the process it is
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
(declare-function flan-disconnect "flan-dev")
|
||||
(declare-function flan-describe "flan-dev")
|
||||
(declare-function flan-show-output "flan-dev")
|
||||
(declare-function flan-repl "flan-repl")
|
||||
|
||||
(defgroup flan nil
|
||||
"Editing and evaluating Flan."
|
||||
@ -78,6 +79,7 @@
|
||||
(define-key map (kbd "C-c C-q") #'flan-disconnect)
|
||||
(define-key map (kbd "C-c C-d") #'flan-describe)
|
||||
(define-key map (kbd "C-c C-o") #'flan-show-output)
|
||||
(define-key map (kbd "C-c C-r") #'flan-repl)
|
||||
map)
|
||||
"Keymap for `flan-mode'.")
|
||||
|
||||
|
||||
128
emacs/flan-repl.el
Normal file
128
emacs/flan-repl.el
Normal file
@ -0,0 +1,128 @@
|
||||
;;; flan-repl.el --- A prompt for a running Flan program -*- lexical-binding: t; -*-
|
||||
|
||||
;; A buffer to type expressions at, sent to the program `flan dev' is running
|
||||
;; and answered with the value they had *there*. It adds no protocol and no
|
||||
;; compiler support: every line goes through the same `eval-expr' request that
|
||||
;; C-x C-e uses.
|
||||
;;
|
||||
;; Derived from `comint-mode', for the same reason `flan-mode' derives from
|
||||
;; `lisp-mode': history, the input ring, and kill/yank behaviour already exist
|
||||
;; and are not worth rewriting. There is no subprocess behind it — the
|
||||
;; "process" is a stub comint needs in order to have a prompt at all.
|
||||
;;
|
||||
;; Two things about it that are decisions, not accidents:
|
||||
;;
|
||||
;; - **It is program-scoped.** A name typed here resolves against the running
|
||||
;; program's top-level namespace, so in sand you write `sim/settle' and not
|
||||
;; `settle'. A buffer visiting a package's own file gets the alias applied
|
||||
;; for it, because the file says which package it belongs to; a prompt has no
|
||||
;; file and nothing to derive it from.
|
||||
;;
|
||||
;; - **A value and the program's output are different things** and arrive by
|
||||
;; different routes. The value of the expression appears at the prompt;
|
||||
;; anything the program printed while evaluating it goes to *flan-output*,
|
||||
;; riding along on the same reply. Showing them in one place would be
|
||||
;; convenient and wrong.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'comint)
|
||||
(require 'flan-mode)
|
||||
(require 'flan-dev)
|
||||
|
||||
(defcustom flan-repl-buffer "*flan-repl*"
|
||||
"Name of the Flan REPL buffer."
|
||||
:type 'string
|
||||
:group 'flan)
|
||||
|
||||
(defvar flan-repl-prompt "flan> "
|
||||
"Prompt shown in `flan-repl-mode'.")
|
||||
|
||||
(defvar flan-repl-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "C-c C-o") #'flan-show-output)
|
||||
(define-key map (kbd "C-c C-d") #'flan-describe)
|
||||
(define-key map (kbd "C-c C-q") #'flan-disconnect)
|
||||
map)
|
||||
"Keymap for `flan-repl-mode'.")
|
||||
|
||||
(define-derived-mode flan-repl-mode comint-mode "Flan-REPL"
|
||||
"Type expressions; they are evaluated in the running program.
|
||||
|
||||
\\{flan-repl-mode-map}"
|
||||
:syntax-table flan-mode-syntax-table
|
||||
(setq-local comint-prompt-regexp (concat "^" (regexp-quote flan-repl-prompt)))
|
||||
(setq-local comint-prompt-read-only t)
|
||||
(setq-local comint-input-sender #'flan-repl--send)
|
||||
;; Nothing is echoed back by a process, because there is no process.
|
||||
(setq-local comint-process-echoes nil)
|
||||
(setq-local font-lock-defaults '(flan-font-lock-keywords)))
|
||||
|
||||
(defun flan-repl--complete-p (text)
|
||||
"Is TEXT a whole form?
|
||||
Parens balanced and not inside a string or comment. RET on a half-typed form
|
||||
should open a line, not send something the reader will reject."
|
||||
(let ((state (with-temp-buffer
|
||||
(set-syntax-table flan-mode-syntax-table)
|
||||
(insert text)
|
||||
(parse-partial-sexp (point-min) (point-max)))))
|
||||
(and (<= (nth 0 state) 0) ; depth
|
||||
(not (nth 3 state)) ; in a string
|
||||
(not (nth 4 state))))) ; in a comment
|
||||
|
||||
(defun flan-repl--output (text)
|
||||
"Insert TEXT into the REPL buffer above the next prompt."
|
||||
(let ((proc (get-buffer-process (current-buffer))))
|
||||
(comint-output-filter proc (concat text "\n" flan-repl-prompt))))
|
||||
|
||||
(defun flan-repl--send (_proc text)
|
||||
"Evaluate TEXT in the running program and show what it was."
|
||||
(let ((code (string-trim text)))
|
||||
(cond
|
||||
((string-empty-p code) (flan-repl--output ""))
|
||||
(t
|
||||
(let ((reply (condition-case err
|
||||
(flan-dev--request
|
||||
(list :op "eval-expr" :code code :file "<repl>"))
|
||||
(error (list :status "error"
|
||||
:message (error-message-string err))))))
|
||||
(flan-repl--output
|
||||
(if (equal (plist-get reply :status) "ok")
|
||||
(or (plist-get reply :value) "")
|
||||
(concat "error: " (or (plist-get reply :message) "rejected")
|
||||
(let ((loc (plist-get reply :loc)))
|
||||
(if loc (concat " (" loc ")") ""))))))))))
|
||||
|
||||
(defun flan-repl-return ()
|
||||
"Send the input if it is a whole form, otherwise open a line."
|
||||
(interactive)
|
||||
(let ((input (buffer-substring-no-properties
|
||||
(process-mark (get-buffer-process (current-buffer)))
|
||||
(point-max))))
|
||||
(if (flan-repl--complete-p input)
|
||||
(comint-send-input)
|
||||
(insert "\n"))))
|
||||
|
||||
(define-key flan-repl-mode-map (kbd "RET") #'flan-repl-return)
|
||||
|
||||
;;;###autoload
|
||||
(defun flan-repl ()
|
||||
"Open a prompt on the program `flan dev' is running.
|
||||
Connects first if it has to."
|
||||
(interactive)
|
||||
(unless (and flan-dev--connection (process-live-p flan-dev--connection))
|
||||
(call-interactively #'flan-connect))
|
||||
(let ((buf (get-buffer-create flan-repl-buffer)))
|
||||
(with-current-buffer buf
|
||||
(unless (derived-mode-p 'flan-repl-mode)
|
||||
(flan-repl-mode)
|
||||
;; comint wants a process to hang a prompt and a process mark off.
|
||||
;; There is nothing to run, so this one does nothing and is never
|
||||
;; written to; every request goes over the daemon's socket instead.
|
||||
(let ((proc (start-process "flan-repl" buf "cat")))
|
||||
(set-process-query-on-exit-flag proc nil)
|
||||
(comint-output-filter proc flan-repl-prompt))))
|
||||
(pop-to-buffer buf)))
|
||||
|
||||
(provide 'flan-repl)
|
||||
;;; flan-repl.el ends here
|
||||
@ -12,6 +12,7 @@
|
||||
|
||||
(require 'flan-mode)
|
||||
(require 'flan-dev)
|
||||
(require 'flan-repl)
|
||||
|
||||
(defvar test-flan--failures 0)
|
||||
|
||||
@ -75,6 +76,54 @@
|
||||
(string-match-p "HELLO" (buffer-string)))))
|
||||
(test-flan--check "the program's output reaches its buffer" seen))
|
||||
|
||||
;; The REPL buffer: typed input goes through the same eval-expr request, and
|
||||
;; the value lands at the prompt while the program's own output goes to
|
||||
;; *flan-output*. Conflating those two is the bug worth testing for.
|
||||
(test-flan--check "an incomplete form is not sent"
|
||||
(not (flan-repl--complete-p "(+ 1")))
|
||||
(test-flan--check "a whole form is sent" (flan-repl--complete-p "(+ 1 2)"))
|
||||
(test-flan--check "a paren in a string does not count"
|
||||
(not (flan-repl--complete-p "(f \"(\"")))
|
||||
(flan-repl)
|
||||
(with-current-buffer flan-repl-buffer
|
||||
(goto-char (point-max))
|
||||
(insert "(+ 20 3)")
|
||||
(flan-repl-return)
|
||||
(let ((deadline (+ (float-time) 15)))
|
||||
(while (and (not (string-match-p "23" (buffer-string)))
|
||||
(< (float-time) deadline))
|
||||
(accept-process-output nil 0.05)))
|
||||
(test-flan--check "the REPL shows a value"
|
||||
(string-match-p "23" (buffer-string)))
|
||||
(goto-char (point-max))
|
||||
(insert "no-such-thing")
|
||||
(flan-repl-return)
|
||||
(let ((deadline (+ (float-time) 15)))
|
||||
(while (and (not (string-match-p "unknown name" (buffer-string)))
|
||||
(< (float-time) deadline))
|
||||
(accept-process-output nil 0.05)))
|
||||
(test-flan--check "the REPL shows an error"
|
||||
(string-match-p "unknown name" (buffer-string)))
|
||||
;; A value and the program's output travel by different routes: the value
|
||||
;; is the result of the request, the output rides along with the reply.
|
||||
;; Showing them in one place would be convenient and wrong.
|
||||
(goto-char (point-max))
|
||||
(insert "(print-line \"PRINTED\")")
|
||||
(flan-repl-return)
|
||||
(let ((deadline (+ (float-time) 15)))
|
||||
(while (and (not (with-current-buffer flan-dev-output-buffer
|
||||
(string-match-p "PRINTED" (buffer-string))))
|
||||
(< (float-time) deadline))
|
||||
(ignore-errors (flan-dev--request '(:op "describe")))
|
||||
(accept-process-output nil 0.05)))
|
||||
(test-flan--check "printed text goes to the output buffer"
|
||||
(with-current-buffer flan-dev-output-buffer
|
||||
(string-match-p "PRINTED" (buffer-string))))
|
||||
;; ...and the prompt got the *value*, which for a call made for its effect
|
||||
;; is Unit. The text it printed is not the value and does not belong here.
|
||||
(test-flan--check "and the prompt got the value, not the text"
|
||||
(string-match-p "()" (buffer-string))))
|
||||
|
||||
(flan-disconnect)
|
||||
(test-flan--check "disconnected" (not (process-live-p flan-dev--connection)))
|
||||
|
||||
|
||||
@ -428,7 +428,11 @@ let rec render c depth (e : Tast.expr) : Tast.expr list =
|
||||
| Types.Float _ -> [ call emit_f64 (cast (Types.Float Types.F64) e) ]
|
||||
| Types.Bool ->
|
||||
[ unit_ (Tast.If (e, lit "true", lit "false")) ]
|
||||
| Types.Unit -> [ lit "()" ]
|
||||
(* Evaluated *and then* reported. A Unit expression is almost always a call
|
||||
made for its effect — (print-line "x") is the REPL's most ordinary
|
||||
input — so emitting the literal without running it would make the prompt
|
||||
answer () while nothing happened. *)
|
||||
| Types.Unit -> [ e; lit "()" ]
|
||||
| Types.String ->
|
||||
[ call emit_str
|
||||
{ Tast.e = Tast.Prim (Tast.Bytes, [ e ]);
|
||||
|
||||
@ -102,6 +102,10 @@ let () =
|
||||
(* An enum's members are erased to i32 before the backend sees them, so
|
||||
the name is recovered from the checker's table. *)
|
||||
value "an enum" "col" ":blue";
|
||||
(* A Unit expression is almost always a call made for its effect, so it
|
||||
has to be *evaluated* and then reported as (). Emitting the literal
|
||||
without running it made the prompt answer while nothing happened. *)
|
||||
value "a call made for its effect" "(print-line \"printed\")" "()";
|
||||
|
||||
(* The one that proves it ran inside the process: the program increments
|
||||
[ticks] every frame, so two evaluations of it must disagree. A copy
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user