diff --git a/README.md b/README.md new file mode 100644 index 0000000..4578cf7 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +
+ +Flan + +# flan. + +**A statically typed Lisp for making games.** +Clojure's brackets. C's memory. No garbage collector. A REPL into the running process. + +
+ +--- + +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 + +--- + +
+ +Flan is a custard. + +