flan/DISCUSS.org
Joseph Ferano 7bd2c99353 sentinel-filled is now dead-beef, and takes the pattern
The author's revision. The name says what it writes, and the pattern is the
program's to choose: (dead-beef) is DEADBEEF, (dead-beef 0xBAADF00D) is
BA AD F0 0D. One byte-order rule covers both — a pattern's ascending bytes
are its big-endian bytes, which is how the hex literal reads left to right —
so every candidate DISCUSS.org listed is now spellable without the compiler
naming any of them.

The bare form is not a case a backend knows about: the checker writes
Tast.dead_beef_default in where the argument would have been, so
(dead-beef) and (dead-beef 0xDEADBEEF) are the same node and an acceptance
row prints both to say so.

The operand is an ordinary u32 expression, which is what the byte arm
already accepts for its byte. A literal is byte-reversed at compile time and
still reaches the loop as an immediate; a computed one is reversed at run
time, by llvm.bswap.i32 on one backend and bswap on the other, after which
the tail shifts its bytes out of the word rather than folding them. The
program runs a computed pattern over lengths 6 and 7 deliberately: that is
the case a constant-only implementation would pass by accident.

filled is untouched, and so is the fill boundary.
2026-09-20 18:33:05 +07:00

257 lines
15 KiB
Org Mode

* 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 flan--defs (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 flan--defs 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 flan--report'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 (dead-beef), 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].
(dead-beef) writes the default DEADBEEF and (dead-beef 0xBAADF00D) writes the
pattern given, under one byte-order rule: a pattern's ascending bytes are its
big-endian bytes, which is how the hex literal reads left to right. So every
candidate listed above is spellable without the compiler naming any of them,
and the bare form is checked into the spelled-out default rather than being a
case a backend knows about. The pattern may be computed, not only written —
same rule as the byte arm, and both backends byte-reverse at run time when it
is.
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.