flan/DISCUSS.org
Joseph Ferano 0a4c52d5ee filled and dead-beef, the two byte fills
# Conflicts:
#	DISCUSS.org
#	FIX.org
#	test/test_acceptance.ml
2026-09-20 19:25:08 +07:00

526 lines
30 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)
Scope: should take a struct or an array (by place/pointer) and fill it with
the sentinel — general over both, not just raw byte buffers. Ties into the
memcpy discussion (2026-09-20): a struct/array is a first-class aggregate
value in flan today (copies happen via load/store, no memcpy builtin
exists), so this would be the first builtin that reaches into one of those
and overwrites it as raw bytes rather than treating it as a typed value —
same category of operation llvm.memset already does for zero, just exposed
as a real user-facing builtin instead of only an internal codegen detail.
Not designed or implemented, just an idea.
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".
** struct field type change: got a plain type error, not the documented refusal
Hit "expected i32, found u8" after changing Cell's row/col from u8 to i32.
Turned out to be a leftover (u8 (/ my cell-size)) cast at the construction
site that I hadn't updated to i32 yet — an ordinary, correct type error, not
a hot-reload problem. Already fixed in the file.
Worth remembering: a struct field type change is one of exactly three
redefinitions the session actually refuses outright rather than silently
mis-loading (docs/BUILT.md:1213-1219: "a struct's fields | the values the
process is holding have the old layout"). That refusal has its own distinct
message — if I hit it for real it won't look like an ordinary type mismatch.
** compiler messages need a persistent, copyable buffer — not just *Messages*
No dedicated log/output buffer for compiler errors exists anywhere in
emacs/flan.el. Today it's only: a transient error overlay
(flan--error-overlays, cleared on the next command) and `message` to
*Messages* (append-only, awkward to copy out of, no structure). Want both
the ghost text AND a persistent buffer that accumulates these, so a message
can actually be copy-pasted (e.g. to hand to Claude) without hunting through
*Messages*. Not designed or implemented.
** defmacro should support real parameter lists, not just one [Form] arg
Every defmacro today takes exactly one parameter, the whole call's arg list
as [Form] (lib/parse.ml comment: (defmacro m [args] body) is (defn m [args
[Form]] Form body)). Porting a Clojure macro like
(defmacro do-grid [[r rows c cols] & body] ...) means manually picking apart
`args` by hand every time — (at args 0), match on Form.Vec to unwrap a
binding vector, (form-rest args 1) for the body, no arity checking, no
destructuring in the signature itself. with-drawing/with-mode-2d
(vendor/raylib/modes.flan) already do this by hand and it works, but it's
real boilerplate for something Clojure gets for free from defmacro's own
parameter list. Real design gap, not a small fix — would mean parsing macro
params with the same destructuring patterns [let] already has (dvec/dmap,
lib/parse.ml:698-799) plus variadic &body support, applied at the macro
call site before expansion rather than left to the macro body.
** profiling: use Tracy, don't build our own
No profiling infra or prior discussion exists in FIX.org/plan.org/docs.
Recommendation: bind Tracy (TracyC.h, a pure C API) via declare-c — same
mechanism raylib already goes through. Building a bespoke profiler means
redoing a capture protocol, a viewer, flame graphs and a timeline, all of
which Tracy already does well, for a domain (real-time/game loops) it was
built for.
Two integration points already fit flan's own design:
- Tracy's frame mark maps directly onto flan's existing frame-boundary
notion — where agent/poll is called each loop iteration.
- flan already has precedent for dev-build-only instrumentation wired in as
an LLVM constructor (the allocation registry, lib/emit.ml ~4165). Automatic
per-function Tracy zones in dev builds, using the same dev/cell
indirection every call already goes through, could follow that pattern
instead of requiring hand-instrumentation everywhere.
Alternatives considered:
- Optick — similar to Tracy but less active, historically Windows-first. No
real advantage over Tracy.
- Superluminal — great UI, but commercial and Windows-only. Ruled out (Linux).
- perf/Hotspot — free, zero-instrumentation sampling, but not frame-aware,
not live-viewable, Linux-only. Fine as a second opinion, not primary.
- Chrome Trace Event Format / Perfetto (or a tiny header-only exporter for
it) — much smaller lift than Tracy: push timestamped begin/end onto a
buffer, dump JSON, view in Perfetto/chrome://tracing after the fact. No
live view, no socket server — but it's the one option with an actual story
for wasm32 (plan.org's committed third target: write trace to a buffer,
pull out via JS). Tracy's capture side assumes a real OS socket the viewer
connects to, which doesn't really work in a wasm sandbox.
Conclusion: Tracy for native dev-loop profiling now (best tool, trivial
declare-c binding, fits the frame-boundary idea already in the design). If
wasm32 profiling becomes a real need later, a small Chrome-Trace-Format
exporter is the justified "build our own" — not a Tracy replacement, a
different target Tracy doesn't reach. Not designed or implemented.
** no type-limit constants: u8-max, i32-max, f32-infinity, etc. — nothing exists
Confirmed missing entirely. No MAX/MIN constants in the prelude for any
sized int type; min/max are just binary comparison builtins, not type-bound
limits. No C limits.h/float.h macro import either (cimport only pulls in
declared functions/structs/typedefs, not #define constants) — no path to
INT_MAX/FLT_MAX that way.
Worse for infinity/NaN: there's no literal syntax at all. float_repr
(lib/form.ml:87-104) prints inf/nan as words for a runtime value that
happens to be one, but the reader doesn't parse those words back ("nan.0 is
no improvement on a literal no reader accepts either way"). Only way to get
+inf today is at runtime: (/ 1.0 0.0) (float division by zero is IEEE-754
defined, not a trap the way integer division by zero is) — no way to just
write it down. Real gap, not implemented.
** investigate SBCL's redefinition model — warn + keep old value until callers update
Right now a global/struct type change is a hard refusal (lib/session.ml:
304-311, docs/BUILT.md:1213-1219) — "restart to change it." I remembered a
design where the OLD value/layout keeps being used until the calling code
is recompiled against the new one, since code using the old shape probably
won't even compile anymore anyway, so nothing unsafe reads mismatched
memory. Found it — it's real, but only planned for a narrower case:
plan.org's Hot Reload section (~685-696): a signature-changing function
redefinition creates a new internal function version + trampoline. Newly
compiled callers use the new one; existing callers and stored function
values keep the old version safely. The session warns at every tracked old
caller site; recompiling one either updates it or gives a normal type error.
plan.org's dev/release table (~671-682) also lists "Structs: version word"
for dev builds — implying a similar versioned-layout plan existed for
structs too. But docs/BUILT.md:1223-1230 says none of this is built for
functions either ("no function versions, no trampolines... the refusal
stays, because the alternative to refusing is not the new design, it is a
silent argument mismatch. It is a stopgap"). So today BOTH functions and
globals/structs just hard-refuse; the versioned/warn-and-keep-old-value
design exists on paper for functions but nowhere for structs/globals, and
isn't implemented for either.
Action: look at what SBCL actually does on struct/type redefinition (it
warns rather than refusing, and instances of the old layout get an
"obsolete instance"-style condition on next access rather than corrupting
memory) as a model for what flan's struct/global version-word plan could
be, versus the current hard-refuse stopgap.
** need a value-producing array constructor, usable as a defvar initializer
Wanted to fill `grid` with 255 as part of its defvar declaration, not as a
separate mutation step after. Doesn't work today: dotimes returns Unit, not
an array value, so it can only mutate an already-existing place — it can't
be the initializer expression itself, which has to produce the whole typed
value in one go. Had to declare grid zeroed then mutate it in main instead.
(array 4 rl/Vector2) (Ast.ArrayOf, "the one position with no type slot",
docs/BUILT.md ~3475) already exists but only zero-fills — no way to give it
a fill value or a generator. Want something like (array-fill n v) or a
repeat/generate form that IS an expression (produces the array value
directly, works nested for 2D), so it composes as a defvar initializer the
same way a bracket literal does. Not designed or implemented.
** revamp the flan buffers: *flan-dev* has no compilation-mode / jump-to-error
Related to the earlier "compiler messages need a persistent buffer" item but
distinct — that one was about eval-time messages; this is *flan-dev*, the
daemon's own startup/build output (emacs/flan.el:729-765,
flan--start-daemon), which is where a `main` that fails to compile shows its
errors. It's a plain buffer — get-buffer-create with no major mode, raw
process output appended via make-process. No compilation-mode, no
compilation-shell-minor-mode, so no next-error / M-g M-n / jump-to-source.
The fix is probably small: flan's own diagnostics already print in the
ordinary path:line:col: message shape (every error pasted in this session
has been that format) — the same shape Emacs' built-in
compilation-error-regexp-alist already parses (it's the GCC/Clang shape
compile-mode was built around). So this likely isn't a custom-regex job,
just turning on compilation-minor-mode in *flan-dev* (or a dedicated
flan-daemon-mode derived from compilation-mode). Umbrella task: revisit all
the flan-* buffers (*flan-dev*, *flan-output*, error overlays, the eval
result overlay/minibuffer item) together rather than patching each one
separately. Not implemented.
** implicit numeric conversions with a warning flag, instead of hard errors
Not liking the strictness; want implicit typing with an opt-in warning flag
for implicit conversions instead of a hard type error.
Worth being clear-eyed first: "no implicit widening/narrowing anywhere" is
not an oversight, it's a core invariant repeated all over the codebase —
lib/check.ml:1774 ("this language has no implicit narrowing anywhere"),
:4736, :5640, :6575-6579 ("no implicit widening, so one side has to decide
it"), docs/BUILT.md:809. It's the Odin/Rust-style bet against C's decades of
silent-precision-loss bugs, and it's load-bearing in how binary-op checking
picks which side "decides" the type. A warn-instead-of-refuse mode isn't a
flag on top of that — it's a second type-checking mode that would need
threading through every one of those sites, not a small change.
That said, real middle-ground precedent exists: C/C++'s -Wconversion is
exactly "implicit, but warn" bolted on after the fact. Something similar
here would mean: allow widening/narrowing between numeric types silently at
the type-check level, but have the checker also emit a separate warning
list (surfaced same as any other diagnostic) for every site where a
conversion happened that wasn't an explicit cast. Not designed, and a real
philosophy question for the language, not just an implementation task.
** pos?/neg?/zero? don't exist, and need to be generic over numeric types
Nothing named pos?/neg?/zero?/sign anywhere in the prelude. Trivial to write
per-type ((defn pos? [x i64] bool (> x 0))), but that's the problem: we want
these (and inc/dec from earlier) to work across every numeric type without
writing one copy per type.
This is blocked on real generics, which don't exist yet — "generic code over
the type variable %s is not implemented yet — milestone 5" (lib/check.ml:
776-778, hit earlier when `int` fell through to the type-variable path).
min/max already do this at the builtin level (special-cased in the checker,
not written as generic Flan functions), which is the current workaround for
"needs to work over any numeric type" — but that doesn't scale to arbitrary
user-defined functions like pos?/neg?/inc/dec. Two related asks logged
together: (1) add pos?/neg?/zero?, (2) make milestone-5 generics real so
functions like these don't need special-casing into the compiler to be
generic.
** 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.
** error inside a macro-expanded body points at the macro call site, not the real line
Hit with do-grid: "unknown function neg?" reported at the do-grid call line,
not the actual (unless (neg? cell-val) ...) line inside the spliced ~@body.
Already known/tracked, not a fresh gap: the Form wire format a macro
receives and returns (lib/expand.ml:96-101) is Form.value, not Form.t — it
structurally has no loc field, "a macro cannot invent a source location and
the image has no room for one." So EVERY node a macro returns, including
~@body forms spliced through completely unchanged (my own original source,
not compiler-generated), gets the call site's location stamped on it
(lib/prelude.ml:1863-1868 says the same thing). The comment names the real
fix directly: preserving locations through a macro needs "the
structured-error rewrite," which doesn't exist yet — today's stamping is
explicitly the stopgap "that can be had now without" it.
So: not asking for something new, just noting I hit the documented
limitation and it's genuinely annoying for macros like do-grid that splice
a large body through — every error in the body mislocates to the macro call.
** built-in comment
No (comment ...) exists. Trivial as a one-line user macro today —
(defmacro comment [args] `(do)) — and it already has the useful property:
macro args are raw unparsed Form, never checked as expressions, so whatever
is inside never needs to type-check. Want it built in (prelude or special
form) rather than something every project defines for itself.
** #_ (discard) isn't syntax-highlighted, though it works correctly
Compiler side is fine and deliberate — lib/reader.ml:16-24, 202-262 reads
and throws away the next form, Clojure-style, including #_#_ counting-free
chaining. Purely an editor gap: flan-font-lock-keywords
(emacs/flan-mode.el:126-146) has no rule for #_ at all, and
flan-mode-syntax-table (line 180+, derived from lisp-mode-syntax-table) has
no notion of it either since Common Lisp doesn't have this construct. So a
discarded form renders as plain text, not grayed out/comment-styled.
real clojure-mode.el's approach is precedent: a syntax-propertize-function
that marks a discarded form's characters with the comment syntax class, so
font-lock renders it as a comment for free. Not implemented here.
** syntax highlighting: defmacro/when/etc missing, and macros aren't dynamic
Two separate gaps.
Easy: flan--definers (emacs/flan-mode.el:114-118) is missing `defmacro`
entirely, even though it introduces a top-level name like defn does.
flan--special (120-124) is missing when/cond/and/or/break/continue/recur/
handler-bind/handler-case/restart-case/invoke-restart — all real lib/parse.ml
special forms, just never added to the static lists. Plain oversights,
trivial to fix.
Harder: highlighting a user-defined macro like do-grid can't be a static
list — and the daemon doesn't even have the info today. A macro compiles
down to an ordinary Tast.fn (defmacro m [args] body is exactly
defn m [args [Form]] Form body) — macro-ness is erased by check time.
Confirmed: defs (lib/dev.ml:1080-1133) reports every function with
kind:"fn", no "macro" kind exists anywhere, because Tast.fn has no field
recording it came from a defmacro. So even flan--defs (the live cache
already driving eldoc/completion) can't tell do-grid is a macro right now.
Would need: (1) plumb a macro flag through Check/Tast so defs can report a
real "macro" kind, (2) have flan-mode treat flan--defs as a dynamic
font-lock source — filter macro-kind names, font-lock-add-keywords +
font-lock-flush/fontify-buffer whenever the cache refreshes (same points
flan-refresh-defs already runs at: connect, after an accepted eval). CIDER
does exactly this for clojure-mode off a live nREPL connection — real
working precedent. Not designed or implemented here.