diff --git a/README.md b/README.md index b8e62a1..1507413 100644 --- a/README.md +++ b/README.md @@ -1,233 +1,145 @@
-Flan +Flan -# flan. +# 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.**
---- +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. -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: -
+```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. -
+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 type-check a program +flan run [args...] build and run it +flan build [-o out] [options] build a native executable +flan dev [-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.