Update README

This commit is contained in:
Joseph Ferano 2026-09-14 07:34:26 +07:00
parent 6ac11be5b1
commit be39f32cb6

324
README.md
View File

@ -1,233 +1,145 @@
<div align="center">
<img src="assets/flan-logo.svg" alt="Flan" width="360">
<img src="assets/flan-logo.svg" alt="Flan" width="300">
# flan<span>.</span>
# Flan
**A statically typed Lisp for making games.**
Clojure's brackets. C's memory. No garbage collector. A REPL into the running process.
**A statically typed Lisp for native games and interactive development.**
</div>
---
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.
Flan is what happens when you want Odin's memory model and Common Lisp's debugger
at the same time and refuse to pick.
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.
There are no object headers, so a Flan struct **is** its C struct. There is no
collector, so nothing runs between your frames that you did not write. And the
program you are running is not a build artifact you replace — it is a thing you
can edit while it is still going.
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.
```
Edit the code, keep the sand.
```
## What it has
## The twenty-millisecond loop
- 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.
Run `flan dev sand.flan`. A window opens, sand falls, and a socket appears next to
your source file. Open Emacs, hit `C-c C-z`, and you are attached to the live process.
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.
Now put your cursor in a function and press `C-c C-c`.
## Quick start
That function is recompiled and installed into the running program at its next frame
boundary. The whole round trip is about **19 milliseconds**, of which the actual
hot-swap — `dlopen` plus `dlsym` — is **0.04 ms**. The rest is the compiler doing
its job. It reads your *buffer*, not your saved file, so there is no ceremony.
The window does not blink. The grid does not reset. Your sand keeps falling.
| | |
|---|---|
| `C-c C-c` | recompile this function into the live program |
| `C-x C-e` | run this expression **inside** the running process — `(len enemies)` returns the real number |
| `C-u C-c C-c` | set a breakpoint without editing the buffer |
| `C-c C-m` | expand a macro, one step or to the fixpoint |
| `C-c C-b` | open the break loop when something goes wrong |
| `C-c C-r` | a REPL, scoped to the program |
## When it breaks, it does not die
Most languages give you two options when something goes wrong: crash, or have
guessed in advance what you wanted. Flan has Common Lisp's third option.
```
flan: unhandled Missing — stopped, not dead.
0. restart: carry-on
1. restart: use-placeholder
```
The program is sitting on the frame where the error happened, with everything it
had still in scope, waiting for you to decide. Pick a restart and it carries on —
it never unwound, so there is nothing to reconstruct.
Handlers run **on the signalling frame, without unwinding**, which means you can
also just… not have an error:
```lisp
;; A handler that returns normally accumulates and lets the signaller run on.
(handler-bind [(AssetMissing [c] (set seen (+ seen (i64 (.id c)))))]
(load-all))
```
No monad. No `Result` threading. No early return. The signaller carries on.
A breakpoint, incidentally, is not a feature. `pause` is an ordinary function that
signals a `Pause` condition, and a breakpoint is just a condition nobody handled.
## The language
**Four container types, four honest ownership stories.** `[n T]` is a fixed array
and it is a *value* — it copies. `[T]` is a slice: a pointer and a length that owns
nothing. `(Vec T)` and `(Map K V)` own their storage and move rather than copy.
```lisp
;; No initialiser means all-bytes-zero, so this lives in BSS and costs nothing.
(defvar grid [rows [cols i32]])
(let [row (slice (at grid 1) 0 cols)] ; ptr+len, borrows
(set (at row 0) 5)
(println (at grid 1 0))) ; 5 — the same storage
(set grid (zeroed)) ; a memset, not an allocation
```
**`defer` is a compile-time construct**, copied into the exit paths. Innermost
first. There is no runtime stack of thunks to pay for.
```lisp
(defn work [n i32] i32
(defer (println "second"))
(defer (println "first"))
(when (< n 0)
(return 0)) ; runs both defers above it
n)
```
**Generics are monomorphised, and checked once.** The body is verified abstractly at
the definition, so an unsupported operation is an error where you wrote it — not at
whichever call site happened to pass a type that worked.
```lisp
(defn clamp-to [x $t lo $t hi $t] $t
{:where (ordered? $t)}
(min (max x lo) hi))
```
**Macros are compiled, dlopened, and called.** There is no interpreter in this
project and there is not going to be one — compiling is the only way a form is ever
run, so there is no second evaluator to disagree with the first. Running a file that
calls a macro means the compiler built a shared object and loaded it into *itself*
before parsing your first line.
**The FFI is one line per function.** No wrapper, no `shim.c`:
```lisp
(declare-c init-window [width i32 height i32 title string] "InitWindow")
(declare-c window-should-close? [] bool "WindowShouldClose")
```
`Color` crosses by value. `Vector2` comes back by value. The compiler writes the
flattening C so you don't. There are **more than 470** raylib bindings in the box, and
**29** of raylib's own examples ported.
**Keywords are enums with an integer's ABI:**
```lisp
(defenum Key [space 32 escape 256 left 263 right 262])
(key-pressed? :space) ; resolved at compile time; a typo is an error here
```
## The demo
`sand.flan` is a falling-sand toy: 180×120 grains at 120fps. Hold the mouse and
sand pours out of the cursor, release and the colour cycles, `R` clears it.
It is 206 lines, and it deliberately uses almost nothing — no `Vec`, no `Map`, no
generics, no macros of its own, no allocator beyond the stack and static storage.
The grid is `[rows [cols u32]]`: flat, unboxed, in BSS, exactly `rows*cols*4` bytes.
The same memory the Odin port has. Nothing in the frame loop allocates.
The same file is also the regression test. `sand-headless.flan` imports it as a
package and — because the linker follows what the program actually reaches — pulls
in neither a window nor libraylib. It runs N frames and hashes the grid:
```
15595743031174623232
```
That number is byte-identical on native x86-64 and on wasm32-wasi, at `-O2` and at
`-O0`. The random number generator is written in Flan rather than borrowed from
libc precisely so that it would be.
## Two backends that agree
There is the LLVM backend, and there is a second one — about 3,400 lines of OCaml
that emits x86-64 machine code directly, byte by byte, because `llc` was most of
those 19 milliseconds and that was annoying.
Every program in the test corpus produces **byte-identical stdout, stderr and exit
status under both backends**. Not similar. Identical. There is a script that checks
this and it is the only reason anyone trusts the second one.
It also has a rule, stated in capital letters in two separate file headers:
> There is still no aggregate classifier and there must not be one.
## What works, and what doesn't
**Works, and is tested at `-O2`, `-O0` and `--dev`** — often under Valgrind and
ASan too: structs, unions, enums, `match`, generics, `Option`, macros, packages,
conditions and restarts, `defer`, arenas and explicit allocators, all four
containers, handles and pools, bounds checking, arithmetic errors as conditions,
the raylib FFI, UTF-8 strings, compile-time `embed`, the whole Emacs loop, a
wasm32 target, and DWARF debug info you can step through in lldb.
**Designed, refused by name, not built yet:** `(Result T E)` and `try`, `errdefer`,
`handler-case`, `find-restart`, user-written allocators, threading macros, and
`await`. The compiler will tell you which milestone each belongs to rather than
producing a confusing parse error.
**Honest limits.** Redefinition cannot patch a frame that is currently executing and
resume at the same instruction — its register allocation belonged to the old
compilation. "Resume" means re-entering from a restart. And changing a function's
*signature* is rejected rather than applied; changing its *body* is always safe,
because old code is never unloaded.
## Try it
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.
```sh
dune build # the compiler
dune test # 232 checks
flan run sand.flan # falling sand
flan dev sand.flan # falling sand you can edit
dune build
dune exec ./bin/main.exe -- run web/examples/hello.flan
```
Then in Emacs: `M-x flan-dev`, or `C-c C-z` to attach to the one you just started.
To build a standalone native executable:
## Reading further
```sh
dune exec ./bin/main.exe -- build web/examples/hello.flan -o hello
./hello
```
- [`plan.org`](plan.org) — the design and the open decisions
- [`spec-memory.md`](spec-memory.md) — ownership, containers, generics
- [`spec-conditions.md`](spec-conditions.md) — what a restart actually is
- [`emacs/MANUAL.md`](emacs/MANUAL.md) — every key binding and what it does
- [`web/index.html`](web/index.html) — the language reference, including a table of
everything that is *not* implemented and the exact words the compiler uses to
refuse it
- [`docs/`](docs/) — the reports and the history, with an index saying which of
them are still true
The falling-sand demo uses raylib:
## Licence
```sh
dune exec ./bin/main.exe -- run sand.flan
```
MIT. See [`LICENSE`](LICENSE).
Once you are iterating regularly, put the built executable on your `PATH` if
you want to use the shorter `flan` commands shown below.
The `vendor/` directory carries other people's work under their own terms —
raylib is zlib/libpng, and the committed `raylib-5.5.h` stays under it.
## The live development loop
---
Start a long-lived development session:
<div align="center">
```sh
flan dev sand.flan
```
Flan is a custard.
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.
</div>
To set up the mode:
```elisp
(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](emacs/MANUAL.md).
## A small example
```lisp
(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](web/examples/restart.flan) for a runnable version.
## Commands
```text
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](web/index.html) — language reference and fuller examples.
- [spec-memory.md](spec-memory.md) — ownership, containers, and generics.
- [spec-conditions.md](spec-conditions.md) — conditions, handlers, and restarts.
- [emacs/MANUAL.md](emacs/MANUAL.md) — the interactive editor workflow.
- [docs/BUILT.md](docs/BUILT.md) — implementation rationale.
- [NEXT.md](NEXT.md) — current work and known limits.
## License
Flan is released under the [MIT License](LICENSE). Third-party material under
`vendor/` is distributed under its own licenses.