flan/DISCUSS.org
Joseph Ferano 99f519ba6f Two byte fills: (filled BYTE) and (sentinel-filled)
DISCUSS.org's sentinel-fill idea, built as two builtins because the author
asked for both: a memset with a byte the program picks, and the fixed
DE AD BE EF pattern a hex dump reads as DEADBEEF.

Both are spelled the way (zeroed) is — the value of whatever type is
expected of them — so (set grid (filled 0xFF)) fills a place and there is
no second, place-taking form beside set.

What may be filled is numbers, and structs and fixed arrays built out of
them. Everything else is refused by name: a filled dyn is a collector root
pointing at nothing, a filled Vec header frees a wild address, a filled
slice length is a bounds check that passes, and a filled bool is an i1 to
LLVM and a whole byte to x86, which is the one divergence this feature
cannot have.

The byte fill is llvm.memset / rep stosb. The four-byte pattern cannot be
a memset on either side — the intrinsic takes one repeated i8 — so it is a
counted dword loop in emit.ml and rep stosd in x86.ml, with the pattern
bytes and their little-endian word living once, in Emit. A size that is
not a multiple of four ends on DE, DE AD, or DE AD BE.
2026-09-20 18:15:19 +07:00

14 KiB
Raw Blame History

Additional things

flan dev blocks 4-5s before first eval when the program doesn't call agent/start

"ready on sock" prints in [merged_setup] before [accept_loop] ever runs. [merged_serve] (lib/dev.ml:4417) waits up to 10s (await ~ms:10000, line 4427) for agent.sock to exist before starting accept_loop. The listen socket is already up, so Emacs connects fine, but nothing answers until that wait finishes/times out. So a program with no (agent/start …) looks "ready" but actually stalls the first request for up to 10s.

Maybe accept_loop should start immediately and the agent-socket wait should happen concurrently / only gate agent-specific requests?

Update: happening a lot with sand.flan, not just once. sand.flan calls agent/start (sand.flan:125) but only after rl/init-window (line 122) returns, so if window setup is slow the 10s wait in merged_serve lapses before agent/start ever runs — and since accept_loop is gated behind that wait, this isn't just a noisy log line, it's a real Emacs stall every time. Reinforces: fix should be starting accept_loop unconditionally rather than tuning the timeout.

Update: likely explains C-M-i (completion-at-point) and eldoc both breaking at once. Both read flandefs (emacs/flan.el:1511), refreshed only on connect and after an accepted eval, always through `ignore-errors` (flan.el:592, 1743, 1841, 2333). If a refresh lands during the accept_loop stall it just silently fails, leaving flandefs empty/stale with nothing retrying it — killing both features with no visible error. Same underlying fix (start accept_loop unconditionally) should resolve this too.

Update: also probably explains "redefined game-draw via C-c C-c but raylib never updates" — deliver (lib/dev.ml:159, called at line 764) pushes the new module over the same agent connection. A stale/half-set-up connection from the accept_loop race could report "ok" on install without the game thread's agent/poll ever actually picking it up at the next frame boundary. Quick workaround: M-x flan-restart-program for a fresh connection, then redefine again. Real fix is still the accept_loop one.

add // for forced truncating division?

Lexically free (comments are `;`, not `/`). But nothing is actually missing: `/` on two ints already truncates toward zero (sdiv/udiv, lib/emit.ml:2529), and casting a float division truncates toward zero too ((i32 (/ 7.0 2.0)) -> fptosi -> 3, lib/emit.ml:2909). So `//` as sugar for that is trivial.

Catch: Python's `//` is not toward-zero truncation, it's floor division — rounds toward -infinity. (-7 / 2) is -4 in Python, but sdiv/fptosi give -3. So "Python-like /" and "forced truncation" are two different asks.

Resolved: Joe wants the toward-zero-truncation one, for when a float division needs an int result. (int (/ a b)) already does this — no new operator needed. Not adding //.

"generic code over type variable X" is a bad error for a plain typo

Hit with `int` before defining the defalias for it. resolve_name (lib/check.ml:776-778) treats any unrecognized lowercase name as a type variable by default; near_miss (lib/check.ml:670-698, single-edit-distance only) is what rescues real typos into "unknown type X — did you mean Y?" (line 770-772). `int` -> `i32` is 2 substitutions, outside that net, so it falls into "generic code over the type variable int is not implemented yet — milestone 5" instead of a plain "unknown type int".

Common near-misses (int, float, double, str, …) all get this confusing message rather than "unknown type". Maybe near_miss should also check a short hardcoded list of common non-flan spellings (int/float/double/bool/str etc. from other languages) regardless of edit distance.

eval result ghost text position/minibuffer, both deliberate

Both things I want changed are documented decisions, not bugs:

  • Position is "end of the form sent" (flan.el:1822-1825), not end of line — deliberately different from flan-watch's end-of-line ghost text so the two don't get confused (flan.el:1381-1389).
  • Overlay and minibuffer echo are mutually exclusive on purpose (flan-inline-result docstring, flan.el:91-101): "saying the same number twice is how a reader learns to stop reading both."

I want it at end of line + always also in the minibuffer. Revisit — would mean changing flanreport's `at` computation and the shown/echo either-or in flan.el:1847-1848.

"unknown name u8" from (defconst grid [rows [cols u8]]) — not a real i8/u8 bug

i8/u8 etc. all exist fine (Types.ikind_of_name, lib/types.ml:72). Real issue: defconst's 2-arg form (defconst name value) has no type slot — the second form is always parsed as a VALUE via expr, never as a type (lib/parse.ml: 1341-1345). So [rows [cols u8]] became an array literal, and `u8` inside it read as a bare variable reference, which is genuinely unbound as a value -> "unknown name u8".

defconst has no "type only, no value" form; that's defvar's job. Fix: use defvar instead — (defvar grid [rows [cols u8]]) is defvar's 2-arg (name Type) form (lib/parse.ml:1333-1335), zero-initialized, and parses the bracket correctly as a type via texpr. Fits anyway since grid is mutable.

(when test) with no body should work like (when test (do))

lib/parse.ml:305-309: `when` is a hardcoded special form (not a macro yet — "until macros land at milestone 5") and requires `body <> []`, else fails "when is (when test body …)". No real reason for the restriction — empty body could just become Ast.Do [], same as (do) already means. One-line fix (drop the `body <> []` guard) whenever this file gets touched.

bare struct literal {.field v} can't infer its type from context (e.g. defn return type)

`(defn get-mouse-cell [] Cell … {.row r .col c})` fails to parse — "a bare map is not an expression; write (Type {.field v})". Root cause: this refusal is in `expr` at PARSE time (lib/parse.ml:274-276), before type-checking ever runs, so it can't see that the enclosing defn's return type is Cell. check.ml does support expected-type-driven checking elsewhere (`check ~want:ty`), but a bare struct literal never reaches it — parse rejects it first.

To make this work, the refusal would need to move from parse into check, so it can consult a `want` type (return type, let annotation, etc.) and only fail when there truly isn't one available. Real feature, not a small change.

struct destructuring shorthand: {.row .col} binding same-named locals

Only two struct-pattern forms exist: {:keys [x y]} (lib/parse.ml:729-753) and {name .field} pairs (line 759-762) — both spell the field name twice ({:keys [x y]}) or need name+dot per field. :keys makes sense for dyn maps (keys aren't statically known field names), but a struct's fields are typed and known, so a bare {.row .col} that just binds `row`/`col` locals directly would read better and is the common case.

Would be a new match arm in `dmap` (lib/parse.ml:724-778) for a lone .field symbol with no name before it. No obvious grammar collision — nothing currently matches a bare .field symbol on its own. Not implemented.

C-c C-i should auto-inspect at point, C-u C-c C-i for the minibuffer

Feature request. Today flan-inspect (emacs/flan-inspect.el:681-696) always prompts via read-string, pre-filled with the sexp before point — no current-prefix-arg handling anywhere in the file, and emacs/MANUAL.md:369 documents no C-u variant either. Not sure if I'm misremembering discussing this for flan-inspect specifically or some other command — check that too if it comes up again. Wanted: plain C-c C-i inspects the expression at point with no prompt; C-u C-c C-i opens the minibuffer to type a different one.

inc/dec (pure) and ++/ (mutating) — doable as user macros, no compiler change

No inc/dec/1+/++/ exist anywhere (prelude or special forms) — (+ x 1) is the only spelling today.

Unlike most items above, this needs no compiler change: defmacro already exists at user level (vendor/raylib's with-drawing etc.), and `place` (what `set` parses — lib/parse.ml:961-983: vars, .field, at, deref) is already general enough. Can write today:

(defmacro inc [args] `(+ ~(at args 0) 1)) (defmacro dec [args] `(- ~(at args 0) 1)) (defmacro + [args] `(set ~(at args 0) ( ~(at args 0) 1))) (defmacro [args] `(set ~(at args 0) (- ~(at args 0) 1)))

inc/dec are generic for free since +/- already are. ++/ reuse the place twice (read then write) — fine for a plain var, double-evaluates a place like (at arr (compute-index)) if the index has side effects. Same non-hygienic-macro tradeoff with-drawing/with-mode-2d already accept. Maybe worth putting these in the prelude itself rather than per-project.

(Cell 1 2) as a positional struct constructor

Designated initializers (C/Odin-style) already exist: {.field v …} + ZII zero-fill for omitted fields (zii_fill, lib/check.ml:3814), any order, {} fully zeroed. Not missing — just spelled with braces/dots.

Positional (Cell 1 2) genuinely doesn't exist, and can't be added as a parser special case the way {.field …} was: struct-literal recognition happens at PARSE time purely by syntax shape (Sym name applied to a {.field…}-shaped map, lib/parse.ml:593-600) — the parser has no symbol table, so (Cell 1 2) is syntactically identical to any function call (parse.ml:602-603 Ast.Call). Would need check.ml to notice an Ast.Call's head resolves to a struct type rather than a function, and reinterpret the positional args in field-declaration order there instead. Same parse/check boundary issue as the bare-struct-literal-return-type item above — recurring pattern worth keeping in mind for future struct/type ergonomics asks.

a DEADBEEF-style sentinel-fill builtin, instead of zero

Idea: a debug memset that writes a real byte pattern instead of the zero llvm.memset already uses (lib/emit.ml:934), for catching reads of uninitialized memory. Distinct from Tast.Uninit, which compiles to LLVM `poison` (lib/emit.ml:1613) — a semantic "don't care" marker, not an actual inspectable byte pattern.

Technical snag: llvm.memset only takes a single repeated i8, so a real 4-byte 0xDEADBEEF pattern needs a fill loop, not memset — more expensive. This is why MSVC/glibc-style debug allocators use a single repeated byte instead (0xCC/0xCD/0xDD/0xFD). Need to decide: cheap single-byte sentinel, or a real 4-byte pattern via a loop. Name candidates:

  • single-byte (memset-cheap): 0xCC (MSVC uninit stack), 0xCD (MSVC "Clean" unwritten heap), 0xDD (MSVC "Dead" freed heap), 0xFD (MSVC fence/guard)
  • 4-byte (loop, reads great in a hex dump): 0xDEADBEEF, 0xBAADF00D (Windows LocalAlloc uninit), 0xFEEEFEEE (Windows HeapFree'd), 0xDEADC0DE, 0xC0FFEE, 0x8BADF00D (Apple watchdog-timeout crash code)

Built, 2026-09-20, as two builtins rather than one — the author's answer to the cheap-byte-or-real-pattern question was "why not both? we need some sort of memset -1 right? and dead-beef can loop, that's fine". They are (filled BYTE) and (sentinel-filled), spelled the way [zeroed] is: the value of whatever type is expected of them, so (set grid (filled 0xFF)) is how a place is filled and there is no place-taking form to learn beside [set].

The snag above is why there are two and not one with a wider operand. The byte fill is one llvm.memset / one rep stosb; the 4-byte pattern is a loop on both sides, a counted dword loop in emit.ml and rep stosd in x86.ml, because the intrinsic really does only take a repeated i8.

What may be filled is numbers, and structs and fixed arrays built out of numbers — nothing that carries a tag, a length, an owning pointer or a collector descriptor. That boundary, and why each refusal is the runtime's rather than a matter of taste, is written up in FIX.org, "The two byte fills".

println output goes to flan-output, not inline in the repl

Deliberate per emacs/flan-repl.el:40-44: "a value and the program's output are different things and arrive by different routes… showing them in one place would be convenient and wrong." Same split in flan-mode's C-x C-e.

Not sure I like it — printed output feels disconnected from the eval that produced it when it's in a separate buffer/window. Revisit.

main cannot actually be redefined

Confirmed. emit_main (lib/emit.ml:3858-3861) calls the flan-level `main` via `fname "main"` — a direct symbol call. Every other call site goes through `body_of` (lib/emit.ml:1980-1994) instead, which loads from the dev-build indirection cell so a C-c C-c redefinition takes effect. emit_main skips that, so flan_program_main (what M-x flan-rerun re-enters) always runs the body main had at the initial build, no matter what's redefined into main's cell afterward. Not documented as a known limitation anywhere. Should probably route through body_of too, or say plainly that main is special.

the "queued; the program is parked…" note is annoying

lib/dev.ml:778-790. Fires on every C-c C-c redefine while parked — and a finished program is always parked, so re-evaling main after each run always shows this long note. Message is correct, just too verbose/frequent for the common case. Maybe shorten, or only show once per park / make it toggleable?

(rl/with-drawing ()) fails with a confusing error

`()` is never a valid expression (lib/parse.ml:278). with-drawing splices its arg verbatim (vendor/raylib/modes.flan:79-84), so `(rl/with-drawing ())` expands to `(do (begin-drawing) () (end-drawing))` and the bare `()` blows up deep in the expansion. Workaround: use `(rl/with-drawing (do))` — `(do)` with zero forms is a valid no-op (used elsewhere, e.g. examples/core-input-gamepad.flan:268).

The macro does have an empty-body guard ((< (len args) 1) -> with-drawing-takes-a-body) but it only catches zero arguments ((rl/with-drawing)), not one argument that is itself (). Guard should probably also treat a single bare-() arg as "no body".

`()` is reserved as the type-position spelling of Unit (Types.to_string Unit = "()", lib/types.ml:125) and has no value-position meaning today — the unit value only comes from `(do)`, a form with no body, etc. Position already disambiguates Vec/Map literals from their type spellings (parse.ml comments near line 266-273); worth checking whether () could get the same treatment — type in type position, unit value in expression position — instead of being refused outright everywhere.