flan/FIX.org

12 KiB
Raw Blame History

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

Decisions, 2026-09-17

1. Re-runnable main — DISPATCHED

The process does not actually die. [flan_exit_hook] is [flan_merged_exit] (lib/dev.ml:2669): it flushes, reclaims fd 1, and parks in [for (;;) pause()]. What is missing is a way to wake it. The park becomes a condvar wait, a daemon op signals it, and the main thread — not a new one, because raylib wants the main thread — re-enters [flan_program_main]. [alive] gains a third state, parked-and-re-runnable, and each of the ten guard sites decides for itself whether it accepts one.

Globals are NOT reset between runs. That is the CL/Clojure semantics asked for: the process never died, so a second (main) sees what the first one left.

2. C-x C-e on a top-level form — QUEUED behind 1

Same file as 1 (emacs/flan-dev.el), so it waits rather than merging by hand. No design questions; the note specifies it.

3. Runtime-loaded owning globals — DISPATCHED

Not a missing global ownership model. One rule: a move-only global is legal, and reading one is always a borrow, never a move. Nothing takes ownership, nothing frees it, its lifetime is the process's. Sound precisely because the lifetime question has a constant answer.

Mutable in place as well — a global Vec can be pushed to. Aliasing follows whatever locals already do; no new borrow regime for globals that locals lack.

[embed] (lib/check.ml:4265) still covers build-time data and is untouched.

4. edn both typed and dynamic — REDIRECTED to arenas, drop parked

Two projects, not one. (read-edn T bytes) does not exist — vendor/edn/edn.flan is only a tokenizer, and the compile-time struct walk is NEXT.md item 9.

[drop] was dispatched to unblock the dynamic half and is being PARKED unmerged on its branch, not reverted, because the premise was wrong. The refusal at check.ml:599 is about teardown, not ownership: the type-erased runtime releases slots bytewise and cannot walk a move-only element. An arena never releases a slot — [free-all] takes the whole region — so the premise does not hold there.

That is also what Odin does, which NEXT.md:1620 already recorded: no destructors, no drop, no finalizers; [delete] frees container memory and nothing else. core:encoding/json ships a hand-written recursive [destroy_value] in the library, and the idiomatic alternative is to parse against temp_allocator and [free_all]. Neither is a language feature. Building [drop] was a departure from NEXT.md:1589's settled "defer stays the answer", taken on the assistant's prompting and withdrawn.

So: lift check.ml:599 for arena-allocated containers, and let read-edn take an allocator — which is already the idiom, since spec-memory.md:283 makes the allocator part of the calling convention with an explicit override.

The real cost, stated because it is not free: [can-free] is a RUNTIME capability on the allocator value while check.ml:599 is a COMPILE-TIME refusal, and the compiler cannot generally know statically that a construction site's allocator is an arena. The spec's answer for the analogous drop case is a check at the point of construction, one branch per container — a runtime branch. This likely becomes a runtime trap rather than a static guarantee.

Ownership tracking itself is untouched. Moves are still tracked; what is given up is freeing one element individually, which is the point of an arena.

5. defenum autoincrement — DISPATCHED

C's rule: no value means previous+1, the first is 0, explicit and implicit mix.

Duplicates: an explicitly written one is an intended alias and is allowed. One produced by autoincrement walking into a value another member holds is an accident and is refused, naming both members.

6. JavaScript backend — HELD

wasm32 already works: test/wasm-run.mjs is a WASI host, the test table runs wasm32 builds, web/index.html is in the tree. A second backend beside emit.ml and x86.ml is the largest item here and the dev loop comes first.

7. defdata and defunion — QUEUED last

Today's [defunion] is already the tagged sum type. It is renamed [defdata], and [defunion] becomes the C-style untagged one. Serves both FFI and type punning, and cimport verifies it against the header where one exists — cimport.ml:295 currently skips any record holding an anonymous union, leaving the defstruct beside it unchecked.

Last, because the rename sweeps parse/check/emit/prelude/docs and every .flan file, and would conflict with everything above.

Open, found while working the list

A transient signal -11 on the globals daemon

Seen once, in one of three consecutive test runs, by the agent doing the C-x C-e work; the runs either side of it were clean. Not reproduced since — three forced full runs (dune test force) are green, 232 checks, 0 failures.

Worth remembering rather than chasing, because the daemon it appeared on is one the move-only-global work (c124df3) changed: test_reload's fixture gained a host global Vec and a run-time-new one. A teardown or a reload module that defines rather than declares a global Vec would strand the block the live process is using, which is exactly the shape a rare SIGSEGV takes. If it comes back, start there.

Status, end of 2026-09-17

Six of seven items are merged on dev-loop and green (dune build, dune test force, @x86, @page). One agent is still running: the arena work for item 4.

item what state
1 re-runnable main after the window closes merged
2 C-x C-e installs a top-level form merged
3 move-only globals, borrowed never moved merged
4 edn dynamic value merged (arena route)
5 defenum autoincrement merged
6 javascript backend first lane IN FLIGHT, 2026-09-17 evening
7 defdata rename + C-style defunion merged

Also merged, not from the list: macro-module symbol visibility, which unblocked [flan dev x86] in one process. sand.flan x86 builds in 292ms against 985ms on LLVM.

Parked branches, kept deliberately

  • worktree-agent-a18e9e62485eaedb5 — [drop] and recursive teardown. Finished and green, not merged. See docs/handoffs/HANDOFF-drop.md, which is the part worth keeping. Withdrawn because the refusal it answered is about teardown, and an arena has none; see item 4 above.

Open, carried forward

  • A transient signal -11 on the globals daemon, seen once, not reproduced. Recorded below.
  • [drop]'s handoff flags that the [clone] / [get] / [map-next!] refusals are needed by the arena route too — they are about a copy of a header, which an arena does not make safe — and that a Map has no operation answering where a value lives, which is what reading an arena-parsed EDN document back would need. Both were relayed to the arena agent.
  • [Tast.Addr (Tast.Pfield …)] on an Option fails in both backends. The working route is [Prim (AddrOf, [Field …])]. Found by the drop lane, not fixed.
  • Re-run still does not work under two-process: a finished child is genuinely gone. It now works under x86 because x86 runs merged.
  • sand.flan still holds an uncommitted experiment line that is refused with a message naming the fix: use (defvar the-data (Vec u8)) and fill it in a function.

Landed on dev-loop

Items 1, 2, 3 and 5 are merged and green (dune test force, 232 elisp checks, 0 failures). Items 4 (drop) and 7 (defdata) are still being written. Item 6 is held.

Re-run is merged, and does not work under x86

Park and re-run live in the merged entry point's main(), and x86 refuses the merged daemon by design: a merged host exports every flan.* body for -rdynamic and so interposes the prelude bodies of the LLVM-built macro module the compiler loads into itself. x86 therefore runs two-process, where the program is a child, and a child that finishes is genuinely Gone — there is nothing to wake. [Program.rerun] answers with the two-process refusal rather than the merged one, and a test pins it.

Re-run on x86 needs the merged daemon to accept x86 first. Separate work.

The headline complaint is verified fixed on sand.flan

Window opened, closed, the daemon reported parked, (:op "rerun") returned ok, and xdotool found a live window from the second run.

The earlier claim that [flan dev sand.flan] failed with "unknown function begin-drawing" was true only on the stale base the work started from, and was retracted after a re-test. Nothing to chase.

Evening of 2026-09-17 — the review, and four lanes off it

docs/REVIEW-production-readiness.md is the production-readiness review, written to be implemented from. Item 4 (arena EDN) merged green before it was written, so the FIX list proper is six of seven done and one in flight.

Four agent lanes are running off the review, each in its own worktree:

  • Tier 1 + the runtime half of Tier 4: overflow guards, map removal (Odin's backward-shift), the registry race, the scratch buffer, Addr-on-Option, the dev-runtime aborts. One lane because they share runtime/*.c.
  • Tier 3: clock, math, getenv, basic file ops. Appends to flan_rt.c in its own section so the merge with the lane above stays clean.
  • Tier 4 without the runtime: CLI error arms, flan run flags and -O, CI, the stale DISCUSS.md x86 table, README's missing subcommands and env-var table.
  • The JS backend's first slice, per docs/DISCUSS.md §5's settled decisions: #_ first, then one function under node, then the corpus with a MATCH/DIFFER/REFUSED survey. New reference clones for it are recorded in docs/REFERENCES.md ("Compiling to JavaScript").

Tier 2 (install and shipping) was explicitly passed over. Package visibility is skipped in every lane — it needs a syntax decision from the author.