Joseph Ferano 0c523cfe8b The program can be run again, in the process that is already there
You run a program under flan dev, it opens a raylib window, you close the
window, main returns — and there is no way to get another window short of
flan-dev-restart-program, which throws away the build, the session and every
global with it. In Common Lisp or Clojure the image outlives main, so you call
it again. The process here already outlived main: the exit hook flushed, closed
stdout and sat in for (;;) pause(). Nothing could wake it.

So main() is a loop. The hook records the status and longjmps back into a
setjmp in main() — there is no return available, since flan_exit is reached
from wherever the program happened to be — and the thread waits on a condition
variable until the new rerun op signals it. The main thread is the one that
runs main again: a window belongs to the thread that opened it, and on macOS to
the first thread of the process. A longjmp pops no frame, so the park first
empties the handler stack, the restart stack and the shadow frame chain, each
of which was a chain of allocas in stack the next run is about to write over.
Nothing else is reset; the second run reads whatever the first left in the
globals, which is the semantics that was asked for.

Closing stdout had to go with it. That was how the compiler learned the program
was done, but a pipe delivers EOF once, so the signal and the program's output
were the same resource and spending it left the second run with nowhere to
print. The descriptor hazard the old code reopened /dev/null for goes away with
the close that caused it. Liveness is asked for instead, through a weak symbol
in the same style as the agent's, and is now three states rather than two: Live,
Parked and Gone. Every guard branches on that before consulting the break
state, because the agent's listener answers "running" while the program is
parked and telling somebody whose program has finished that it is running is
worse than saying nothing. Only eval accepts a parked program — it queues and
waits for nothing, and the queued module installs at the first frame boundary
of the next run, so a body can be fixed while parked and the re-run executes
it. Everything else needs a frame boundary or a stopped stack, has neither, and
says which, naming the command that gets the program back.

A re-run while the program is running is refused rather than queued: the test
and the signal happen under one mutex, so two mains writing the same globals at
once never starts.

:parked rides on every reply beside :stopped, for the reason :stopped does —
finishing is as unannounced as stopping, more so when the way it happens is a
mouse click on a title bar. Emacs shows flan:parked in the modeline and binds
flan-rerun to C-c C-M-x.
2026-09-17 19:01:01 +07:00
2024-07-09 21:01:55 +10:00
2026-09-10 14:56:35 +07:00
2026-09-10 14:40:34 +07:00

Flan

Flan

A statically typed Lisp for native games and interactive development.

Flan is an experimental, ahead-of-time compiled Lisp for programs that need predictable memory use and a fast editrun loop. It combines S-expressions, static types, explicit ownership, and a development session that can replace a function in a running program without resetting its state.

It is being built around games, but the interesting part is broader: a compiled language where the running program remains available for inspection, experimentation, and small changes.

In practical terms: you get parentheses, a debugger that would like to have a conversation, and no garbage collector quietly choosing the dramatic moment to join your frame loop.

What it has

  • Native compilation through LLVM, plus an in-progress direct x86-64 backend.
  • C-like data layout: structs, fixed arrays, pointers, slices, and explicit allocation. There is no garbage collector.
  • Owned Vec and Map containers, plus checked moves and borrowing-oriented slice operations.
  • Generics, algebraic unions, enums, macros, packages, defer, and a C FFI.
  • Conditions and restarts for recoverable failures and interactive debugging.
  • A raylib package and a collection of ported raylib examples.
  • Native, WASI, and web build targets. The cross targets are useful but less complete than the native development workflow.

The project is exploratory software, not a stable language release. Some features are deliberately refused while their semantics are still undecided; the compiler aims to say why rather than quietly accepting a partial version. It has opinions, but at least they arrive as error messages.

Quick start

Building requires a current OCaml/Dune toolchain, LLVM/Clang, and the native C toolchain. Raylib is only needed for programs that use the bundled graphics package.

dune build
dune exec ./bin/main.exe -- run web/examples/hello.flan

To build a standalone native executable:

dune exec ./bin/main.exe -- build web/examples/hello.flan -o hello
./hello

The falling-sand demo uses raylib:

dune exec ./bin/main.exe -- run sand.flan

Once you are iterating regularly, put the built executable on your PATH if you want to use the shorter flan commands shown below.

The live development loop

Start a long-lived development session:

flan dev sand.flan

The program runs normally and publishes a local socket beside the source file. The bundled Emacs mode can attach to it, evaluate expressions in the live process, inspect a stopped program, and recompile a top-level function from the buffer. A body change takes effect on the next call; changing a function's signature is intentionally rejected. The program keeps its state, which is especially nice when you have finally arranged the sand into something almost worth saving.

To set up the mode:

(add-to-list 'load-path "~/path/to/flan/emacs")
(require 'flan-mode)

Then use M-x flan-dev to start and attach, or C-c C-z to attach to a session started in a terminal. The editor workflow is documented in emacs/MANUAL.md.

A small example

(defstruct AssetMissing [id i32])

(defn load-asset [id i32] i32
  (signal (AssetMissing {.id id}))
  100)

(defn asset-or-placeholder [id i32] i32
  (restart-case (load-asset id)
    (use-placeholder [] -1)))

(defn main [] ()
  (handler-bind [(AssetMissing [_] (invoke-restart 'use-placeholder))]
    (println (asset-or-placeholder 7))))

Here a missing asset signals a typed condition. The handler chooses a restart, so execution continues with a placeholder instead of requiring error values to be threaded through every caller. See web/examples/restart.flan for a runnable version.

Commands

flan check <file.flan>                         type-check a program
flan run <file.flan> [args...]                 build and run it
flan build <file.flan> [-o out] [options]      build a native executable
flan dev <file.flan> [-s socket]               start a live development session

Useful build options include --debug, --sanitize, --no-bounds-checks, --x86, and --target=wasm32-wasi|web. run is native-only; cross-built output should be run with an appropriate WASI runtime or browser. A .wasm file is not a tiny native executable in a trench coat.

Checking it

dune test                    the suite. Seconds. Run it constantly.
dune build @checks           everything else that can fail. A couple of minutes.
dune build @sanitize         the corpus under ASan and UBSan.
dune build @valgrind         the corpus under memcheck. Tens of minutes.

dune test means "the language still works". @checks — which is @page, @x86 and @cells — means "and everything written down about it is still true": the reference page's examples still print what the page says, the hand-written x86 backend still agrees with LLVM, and a --dev build still calls through its indirection cells.

The two are separate on purpose. A suite that goes red because prose drifted teaches you to skim past red. But @checks only helps if it is run, and nothing runs it for you — there is no CI here. The convention that has to carry it is that a lane's handoff quotes @checks, the way the x86 handoffs already quote the survey's counts. Both of this repository's silent failures — two backend refusals that sat for a month, a page of examples that stopped compiling for two days — were found by accident, and neither would have survived one person typing one command.

Project map

License

Flan is released under the MIT License. Third-party material under vendor/ is distributed under its own licenses.

Description
No description provided
Readme MIT 7.5 MiB
Languages
OCaml 67.2%
Emacs Lisp 15.2%
C 10.4%
HTML 2.9%
Standard ML 2.8%
Other 1.5%