spike/js/survey.sh is the x86 sweep's shape with one deliberate difference in what it counts. That backend is behind, so a refusal there is a regression and its strict mode fails on one. This is a dialect, so a refusal is the design working -- a pointer, an allocator, a Map, the FFI and conditions are refused permanently and correctly. What fails the @js alias is a DIFFER, which is a wrong answer, and a CRASH, which is JS this backend emitted and node would not run. Two probes carry the decisions the corpus does not reach. p1-int-semantics prints wrapping at all eight widths, a multiply past 2^53, truncating division with a negative operand, shifts whose count is out of range, bitwise over a u32, f32 that is not a double, and the conversions both ways -- 35 lines, all identical to the LLVM build. It found two real bugs: >>> binds tighter than & in JavaScript, so a bit-and on a u32 answered -1; and a 64-bit value through Number() rounds to 53 bits before it can be truncated, so (i32 i64hi) answered 0 where it must answer -1. p2-value-copies goes past values.flan to the cases a shallow copy would pass: a struct inside a struct, a struct returned out of a function, an element read out of an array of structs, and a global. Also fixed, and all three were found by the sweep rather than by reading: a unit-typed call in statement position was compiled to an expression nobody emitted, so (load-xs) silently did not happen; an arrow body that starts with a brace is a block, so a zeroed array of structs was a syntax error; and a bounds message must carry the index expression's location, not the form's, because that is the one emit.ml passes to check_at. Render reads an Option's tag as field 0 and a union's as field 0, which is the LLVM layout and not this one, so both are answered here rather than refused. fdefers is dropped rather than refused: nothing in the dialect can start a transfer, so the transfer exit path is unreachable, and refusing it would have refused every program that writes a plain defer.
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.