flan/README.md

232 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<div align="center">
<img src="assets/flan-logo.svg" alt="Flan" width="360">
# flan<span>.</span>
**A statically typed Lisp for making games.**
Clojure's brackets. C's memory. No garbage collector. A REPL into the running process.
</div>
---
Flan is what happens when you want Odin's memory model and Common Lisp's debugger
at the same time and refuse to pick.
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.
```
Edit the code, keep the sand.
```
## The twenty-millisecond loop
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.
Now put your cursor in a function and press `C-c C-c`.
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
```sh
dune build # the compiler
dune test # 232 checks
flan run sand.flan # falling sand
flan dev sand.flan # falling sand you can edit
```
Then in Emacs: `M-x flan-dev`, or `C-c C-z` to attach to the one you just started.
## Reading further
- [`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
## Licence
MIT. See [`LICENSE`](LICENSE).
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.
---
<div align="center">
Flan is a custard.
</div>