Compare commits
3 Commits
682cb745ac
...
9dddcac821
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dddcac821 | |||
| 96b1bba265 | |||
| 71495a7fc8 |
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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
Normal file
231
README.md
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
<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>
|
||||||
47
assets/flan-logo.svg
Normal file
47
assets/flan-logo.svg
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
Loading…
x
Reference in New Issue
Block a user