Where a program starts, how a big array clears, and which key folds

flan-dev's start command proposed the last program it had started, so
invoking it from a fresh project's buffer offered the previous project's
file. It now proposes the buffer it was called from; restarting the
previous program is what flan-dev-restart-program is for.

Zeroing a fixed array wrote one typed store per element. Above 64 bytes
that becomes a memset, which LLVM can lower as a bulk clear; below it the
inline stores are still cheaper than a call.

Outline's minor-mode map owned TAB in the lowering buffer, so the folding
keys that buffer defines never ran. A buffer-local overriding map gives
them back without touching Outline anywhere else.

FIX.org collects the rough edges found while using the dev loop.
This commit is contained in:
Joseph Ferano 2026-09-17 18:08:51 +07:00
parent 65d14f42f0
commit 798c852934
6 changed files with 76 additions and 12 deletions

26
FIX.org Normal file
View File

@ -0,0 +1,26 @@
* Stuff I've found
** Why do I need to call flan-dev to open another window?
let: flan: the program exited; restart flan dev
** I can't eval a toplevel defvar, need to eval-defun (C-c C-c)
For Flans intended live-program workflow, C-x C-e on any complete top-level form should do the natural thing:
- expression → compile/run temporary thunk; print its value
- defn, defvar, defmacro, etc. → compile/install it; report what changed
The compiler already has both paths. The current split is an editor/UI artifact: C-x C-e is wired directly to eval-expr, while C-c C-c is wired to declaration reload. It is not a fundamental limitation.
A good fix would make C-x C-e context-aware: if the enclosing form is top-level, send it through the declaration evaluator; otherwise use expression evaluation. Then C-c C-c can remain a convenient explicit “reload this definition” alias, but not the only way defvar works.
** I can't eval a top level Vec
slurp returns (Vec u8), an owning, move-only buffer. Flan currently forbids every move-only global because it has no global ownership/lifetime model: any function could read and free it, while ownership tracking only exists within one function.
For data that is fixed at build time, use an embedded immutable array instead:
(defconst the-data (embed "game-data.edn"))
That produces a fixed [u8], not a heap-owning Vec, so it can live globally. It also resolves relative to sand.flan.
If game-data.edn genuinely must be loaded at runtime, then today it has to be owned by a local—typically load it in main and pass it through the functions that need it. For a game-wide runtime-owned data asset, that is a missing language/runtime feature, not a bad use case on your part.
** The edn module seems to need a struct declaration, it should do both; go into a struct but also return a Map with Vecs and Sets when we don't provide a type
** defenum needs optional autoincrementing discriminants
** We need a javascript backend so we can reach the world
** We need to have C-style unions, maybe those are called defunion, and then sum types are defdata or deftype

View File

@ -1,4 +1 @@
* Additional things
** Why do we have prints dedicted per type? That's terrible, what do we need to have proper (println) work for everything?
** The current std is suffering from the same problem, functions should be generic? Why is it like this?
** Why do we have =at= and =nth= if they're the same? Pick one, maybe =at=. For all STD functions make sure we don't have redundancy, even if it matches clojure or lisp

View File

@ -570,13 +570,19 @@ When called interactively while a daemon this Emacs started is alive, asks
before stopping it and switching programs. A noninteractive call still
refuses: callers cannot silently discard a running program's state."
(interactive
;; The program last started, where there was one: a restart after a quit is
;; the common case, and it is rarely the buffer point happens to be in —
;; you quit from wherever you were reading when you decided to.
(list (read-file-name "flan dev: " nil flan-dev--file t
(and buffer-file-name
(string-suffix-p ".flan" buffer-file-name)
(file-name-nondirectory buffer-file-name)))))
;; Starting a program is about the buffer the command was called from.
;; Keeping [flan-dev--file] as DEFAULT here made a previous project win
;; over the current buffer: after working on sand.flan, invoking this from
;; ~/Development/gameboy/gameboy.flan still proposed (and could start)
;; sand.flan. [flan-dev-restart-program] is the deliberate way to restart
;; the previous program; an ordinary M-x command must not do that.
(let ((file (and buffer-file-name
(string-suffix-p ".flan" buffer-file-name)
(expand-file-name buffer-file-name))))
(list (read-file-name "flan dev: "
(and file (file-name-directory file))
file t
(and file (file-name-nondirectory file))))))
(when (process-live-p flan-dev--daemon)
;; `interactive' has already read FILE. Refusing only here used to make
;; that selection look as though it had been ignored: the old daemon kept

View File

@ -562,6 +562,16 @@ that is felt."
(setq-local outline-regexp "[▸▾] ")
(setq-local outline-level (lambda () 1))
(outline-minor-mode 1)
;; Minor-mode bindings take precedence over this mode's map. Outline owns
;; TAB by default, so give this buffer its intended folding keys back
;; without changing Outline's bindings anywhere else.
(let ((map (copy-keymap outline-minor-mode-map)))
(define-key map (kbd "TAB") #'flan-lower-toggle)
(define-key map [backtab] #'flan-lower-cycle-all)
(setq-local minor-mode-overriding-map-alist
(cons (cons 'outline-minor-mode map)
(assq-delete-all 'outline-minor-mode
minor-mode-overriding-map-alist))))
(add-hook 'kill-buffer-hook #'flan-lower--clean nil t))
;;;###autoload

View File

@ -853,6 +853,8 @@ is written instead — the real `message' call the real command makes."
(with-current-buffer flan-lower-buffer
(test-flan--check "C-c C-l opens a lowering buffer in its own mode"
(derived-mode-p 'flan-lower-mode))
(test-flan--check "TAB is the lowering buffer's toggle, not Outline's"
(eq (key-binding (kbd "TAB")) #'flan-lower-toggle))
;; The claim the header makes, which is the opposite of the one
;; `flan-disassemble' makes, and the reason both commands exist.
(test-flan--check "whose header says it is the file and not the program"

View File

@ -577,6 +577,25 @@ let ins f fmt =
Printf.ksprintf
(fun s -> if f.live then Buffer.add_string f.b (" " ^ s ^ f.dloc ^ "\n")) fmt
(* A fixed array has no padding between elements, so its zero value is exactly
a run of zero bytes. Naming that operation lets LLVM choose its bulk-clear
implementation instead of expanding a large aggregate store into one store
per element. Keep small arrays as typed stores: their inline code is
cheaper than a call on targets which do not inline the intrinsic. *)
let bulk_zero_min_bytes = 64
let emit_bulk_zero f ptr ty =
match ty with
| Types.Array _ ->
let size, align = lay f.md ty in
if size >= bulk_zero_min_bytes then begin
ins f
"call void @llvm.memset.p0.i64(ptr align %d %s, i8 0, i64 %d, i1 false)"
align ptr size;
true
end else false
| _ -> false
let term f fmt =
Printf.ksprintf
(fun s ->
@ -1084,8 +1103,11 @@ and value_at f (e : Tast.expr) : string =
"zeroinitializer"
| Tast.Set (p, v) ->
let ptr, ty = place f p in
let v' = value f v in
ins f "store %s %s, ptr %s" (ll ty) v' ptr;
(match v.Tast.e with
| Tast.Zero _ when emit_bulk_zero f ptr ty -> ()
| _ ->
let v' = value f v in
ins f "store %s %s, ptr %s" (ll ty) v' ptr);
"zeroinitializer"
| Tast.Make (_, fields) -> aggregate f e.Tast.ty fields
| Tast.MakeCase (uname, case, fields) ->
@ -2602,6 +2624,7 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher
%flanframe = type { ptr, ptr, ptr }
@flan_frame_head = external global ptr
declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg)
declare void @flan_rt_init(i32, ptr)
declare void @flan_argv(ptr)
declare void @flan_write_stdout(ptr, i64)