Flan's tagged sum has been spelled defunion since it landed, which was accurate right up until the language wanted C's untagged union as well. Both cannot be called the same thing, and the tagged one is the one with an alternative name that says what it is: a case, its fields, and a tag that steers which case is live is a data type, not a union. So the form is defdata everywhere -- the parser, the AST, the checker, both backends, the prelude's Form, the editor's font-locking and imenu, the docs and every .flan file in the tree. The internal vocabulary moves with it: Tast.union is Tast.data, uname is dname, the tables the checker and the emitter keep are datas. Leaving them would have inverted the words permanently, with surface defunion meaning one thing and env.unions meaning the other, which is exactly the kind of drift the comments in those files exist to prevent. What did not move is case, variant and vfields: a tagged sum still has cases, and it still has one live at a time. defunion is not kept as an alias. An alias would compile the day the untagged form lands and mean the opposite of what it used to -- the same silent misparse that made defn's return type mandatory, and worse, because the reader would have no reason to look. The old spelling is a named refusal instead, parse/defunion-renamed, which says what it is now called and that the name is reserved for something else. It fires on the head alone, so (defunion U [A B]) -- which would otherwise have parsed cleanly as one field A of type B -- is refused with the rest.
Flan is an experimental, ahead-of-time compiled Lisp for programs that need predictable memory use and a fast edit–run 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
VecandMapcontainers, 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
- web/index.html — language reference and fuller examples.
- spec-memory.md — ownership, containers, and generics.
- spec-conditions.md — conditions, handlers, and restarts.
- emacs/MANUAL.md — the interactive editor workflow.
- docs/BUILT.md — implementation rationale.
- NEXT.md — current work and known limits.
License
Flan is released under the MIT License. Third-party material under
vendor/ is distributed under its own licenses.