A program that wants to load its data once and keep it could not say so. Every move-only global was refused where it was declared, on an argument about the dead set being per function: two functions each freeing the same global would be a double free nothing could see. The argument was sound and the conclusion was too strong. It assumed a global has an owner. It does not. Reading a move-only global is now always a borrow. Nothing may take ownership of one, so nothing may free one, and with no owner to hand over there is no double free left to catch. This is not a general ownership model for globals and is not meant to grow into one: it is sound precisely because the lifetime question that model would exist to answer has a constant answer here, the process's. The refusal lands at the read, which is where a move would have been recorded for a local -- passing the global to something that owns its parameter, binding it to a local, returning it and freeing it all reach the same place, and each is told to borrow instead, or to clone if it really wants something of its own. Such a global is mutable where it stands. push, put, reserve and set already take their target through the borrow path, so a global (Vec u8) is filled and grown in place, and the aliasing that raises is the one every Vec has: spec-memory.md's explicit Zig/Odin contract, where a push that reallocates invalidates a slice taken before it and the dev build's generation word traps on the stale one. Globals get no borrow rule locals do not have, because the hazard is not new and the trap lives on the Vec rather than on the binding. What a move-only global may not do is carry a computed initialiser. A global's initialiser is a link-time constant -- there is no init-at-startup path in the LLVM backend by design, and the x86 backend that has one deliberately leaves it out of a reload module, because re-running an initialiser wipes the live state reloading exists to preserve. So the global starts zeroed, which for a Vec is an empty Vec and therefore a value rather than a placeholder, and the load is an ordinary assignment in whichever function loads it. That is also what makes the data survive: nothing runs between one entry to main and the next, so a re-entered main finds the global as it left it. A defconst cannot be one at all, since a constant is not an assignable place and nothing could ever load it; both refusals name the (defvar g (Vec u8)) that works. The reload fixture gains a global Vec in the host and another that arrives at run time, because that is where declaring instead of defining has teeth: a module that defined the host's Vec would take a zeroed header of its own and strand the block the process is still using, which a re-zeroed i64 cannot demonstrate.
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.
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.