Typed structs do not version; an evolving shape is a defclass
This commit is contained in:
parent
dc39631db7
commit
1526b6fe3f
231
DISCUSS.org
Normal file
231
DISCUSS.org
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
* 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)
|
||||||
|
|
||||||
|
Not designed or implemented, just an idea.
|
||||||
|
|
||||||
|
** 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.
|
||||||
13
FIX.org
13
FIX.org
@ -1225,6 +1225,19 @@ symbol has to resolve against flan_dyn.o, so a rename, a removal or a changed
|
|||||||
argument list is a compile or link error in =dune test=. A function nothing
|
argument list is a compile or link error in =dune test=. A function nothing
|
||||||
here calls gets neither. Both comments now say that instead.
|
here calls gets neither. Both comments now say that instead.
|
||||||
|
|
||||||
|
* Typed structs do not version; a shape that evolves is a defclass, decided 2026-09-20
|
||||||
|
The struct-version-word design (plan.org's dev/release table, candidate B in
|
||||||
|
docs/SBCL-REDEFINITION-NOTES.md) is dropped, not deferred. A typed struct
|
||||||
|
redefinition that changes layout keeps today's refusal; the author's call —
|
||||||
|
"let's just ignore it then, we should be using defclass instead." The
|
||||||
|
division of labour is the two-model one: a shape still being discovered
|
||||||
|
lives on the dyn side as a defclass, where CLOS-style lazy migration handles
|
||||||
|
redefinition (its own lane); a typed defstruct is a commitment to a layout,
|
||||||
|
and changing a commitment restarts the process. SBCL context that settled
|
||||||
|
it: SBCL also refuses by default (a continuable error), and its
|
||||||
|
push-through-and-invalidate behaviour is cheap only because its instances
|
||||||
|
carry headers, which Flan's flat structs deliberately do not.
|
||||||
|
|
||||||
* A defconst is a compiler const, decided 2026-09-20
|
* A defconst is a compiler const, decided 2026-09-20
|
||||||
The author's words: "defconst should not be computed, it's the equivalent to a
|
The author's words: "defconst should not be computed, it's the equivalent to a
|
||||||
compiler const." So a defconst's initialiser has to be a compile-time constant,
|
compiler const." So a defconst's initialiser has to be a compile-time constant,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user