Compare commits

..

No commits in common. "9dddcac821fc3c3a1bdd623cc45e612ba8148db0" and "682cb745acce9ae80ebcfd346191383fffb96c16" have entirely different histories.

3 changed files with 0 additions and 299 deletions

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024-2026 Joseph Ferano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

231
README.md
View File

@ -1,231 +0,0 @@
<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>

View File

@ -1,47 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 360" width="520" height="360" role="img" aria-labelledby="t d">
<title id="t">Flan</title>
<desc id="d">A caramel flan held between two parentheses.</desc>
<defs>
<linearGradient id="custard" x1="0" y1="0" x2="0.15" y2="1">
<stop offset="0" stop-color="#FDEBBC"/>
<stop offset="0.5" stop-color="#F5D286"/>
<stop offset="1" stop-color="#DDA84D"/>
</linearGradient>
<linearGradient id="caramel" gradientUnits="userSpaceOnUse" x1="200" y1="122" x2="300" y2="248">
<stop offset="0" stop-color="#DE8C2A"/>
<stop offset="0.55" stop-color="#B96418"/>
<stop offset="1" stop-color="#8C4410"/>
</linearGradient>
<radialGradient id="sheen" cx="0.32" cy="0.28" r="0.55">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.6"/>
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- the parens: tall, clearly punctuation, holding the flan between them -->
<g fill="none" stroke="#B96418" stroke-width="21" stroke-linecap="round">
<path d="M108 46 C 30 130, 30 232, 108 316"/>
<path d="M412 46 C 490 130, 490 232, 412 316"/>
</g>
<!-- sauce pooled under the flan -->
<ellipse cx="260" cy="286" rx="132" ry="20" fill="#9E5314"/>
<!-- custard body -->
<path d="M172 150 L150 268 A110 26 0 0 0 370 268 L348 150 Z" fill="url(#custard)"/>
<ellipse cx="260" cy="268" rx="110" ry="26" fill="#D9A94F"/>
<!-- caramel top -->
<ellipse cx="260" cy="150" rx="88" ry="25" fill="url(#caramel)"/>
<!-- three drips, tapering to a rounded tip and starting up inside the
glaze so they hang off the rim instead of floating below it -->
<g fill="url(#caramel)">
<path d="M181 146 C181 190 188 208 192 216 C196 208 203 186 203 136 Z"/>
<path d="M250 152 C250 210 257 238 262 246 C267 238 274 210 274 152 Z"/>
<path d="M322 144 C322 184 328 200 332 208 C336 200 342 184 342 144 Z"/>
</g>
<!-- highlight on the glaze -->
<ellipse cx="224" cy="143" rx="38" ry="10" fill="url(#sheen)"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB