flan/web/index.html
Joseph Ferano 042b2ce4d0 A restart is not a transaction, and nothing rolls back
If a frame mutates a global and then signals, taking a retry re-runs the
mutation. Control resumes at the restart-case and runs forward; nothing is
undone. Common Lisp has the same property and offers no help either, so this
is written down rather than fixed.

The discipline is that the author chooses where the retry boundary is: a
restart-case above the mutations re-runs them, one below re-runs only what
follows. Put the restart before anything mutates, make the retried section
idempotent, or snapshot what will be re-applied.

It matters more here than in most Lisps because the intended use is a game
loop, and a bad index signalling BoundsError rather than ending the process
made abandoning and retrying a frame an ordinary thing to do.

conditions.org and web/index.html already carried the mechanical half as a
one-line gotcha; those are rewritten in place rather than gaining a second
bullet beside them. spec-conditions.md takes it in section 5, which already
enumerates what a transfer does and does not do. No numbered case changed
meaning.
2026-09-13 09:14:46 +07:00

1765 lines
80 KiB
HTML
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.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flan</title>
<meta name="description" content="Flan — a statically typed, GC-free Lisp for game development.">
<style>
:root {
--bg: #fdf8ee;
--panel: #f6ecd8;
--code-bg: #fbf3e2;
--rule: #e4d5b8;
--text: #3a2a18;
--muted: #7b6444;
--accent: #b07a12;
--link: #96601a;
--c-comment: #93795a;
--c-string: #6d7f3a;
--c-form: #a8621a;
--c-num: #8a5a2b;
--c-type: #4d7684;
--c-kw: #96604a;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #1b1611;
--panel: #261f18;
--code-bg: #231d17;
--rule: #3b3229;
--text: #efe3cd;
--muted: #b09c7e;
--accent: #e0b15e;
--link: #e0b15e;
--c-comment: #8b7c62;
--c-string: #adc06b;
--c-form: #e0b15e;
--c-num: #d9a465;
--c-type: #8bb6c6;
--c-kw: #d79c86;
}
}
:root[data-theme="dark"] {
--bg: #1b1611;
--panel: #261f18;
--code-bg: #231d17;
--rule: #3b3229;
--text: #efe3cd;
--muted: #b09c7e;
--accent: #e0b15e;
--link: #e0b15e;
--c-comment: #8b7c62;
--c-string: #adc06b;
--c-form: #e0b15e;
--c-num: #d9a465;
--c-type: #8bb6c6;
--c-kw: #d79c86;
}
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font: 17px/1.65 "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
}
main { display: block; }
.wrap { max-width: 44rem; margin: 0 auto; padding: 0 1.25rem 6rem; }
header { padding: 3.5rem 0 2rem; }
.mark { display: block; height: 2.6rem; width: auto; }
.tagline { color: var(--muted); font-size: 1.05rem; margin: .9rem 0 0; }
h2 {
font-size: 1.45rem; line-height: 1.25; font-weight: 600;
margin: 3.2rem 0 .8rem; padding-top: 1.6rem;
border-top: 1px solid var(--rule);
}
h3 { font-size: 1.08rem; font-weight: 600; margin: 2rem 0 .5rem; }
h2:target, h3:target { color: var(--accent); }
p, ul, ol, table { margin: 0 0 1rem; }
ul, ol { padding-left: 1.3rem; }
li { margin: .25rem 0; }
a { color: var(--link); text-decoration-thickness: 1px; text-underline-offset: 2px; }
code, pre, kbd {
font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, Consolas, monospace;
font-size: .875em;
}
p code, li code, td code, h3 code {
background: var(--panel);
border-radius: 3px;
padding: .1em .32em;
}
kbd {
background: var(--panel); border: 1px solid var(--rule);
border-radius: 3px; padding: .05em .35em; white-space: nowrap;
}
pre {
background: var(--code-bg);
border: 1px solid var(--rule);
border-left: 3px solid var(--accent);
border-radius: 4px;
padding: .85rem 1rem;
overflow-x: auto;
line-height: 1.55;
margin: 0 0 1.15rem;
}
pre code { background: none; padding: 0; font-size: .8125rem; }
table { border-collapse: collapse; width: 100%; font-size: .94rem; }
th, td { text-align: left; padding: .38rem .6rem .38rem 0; border-bottom: 1px solid var(--rule); vertical-align: top; }
th { font-weight: 600; color: var(--muted); }
.scroll { overflow-x: auto; margin: 0 0 1.15rem; }
/* Tabs. The panels are visible by default and only become switchable once
the script adds .tabbed to <main> — with no script the page is the one long
document it has always been, which is also what a printer and a text browser
get. */
.tabs {
display: flex; flex-wrap: wrap; gap: .4rem;
margin: 0 0 2rem; padding: 0 0 .6rem;
border-bottom: 1px solid var(--rule);
}
.tabs button {
font: inherit; font-size: .93rem; cursor: pointer;
background: none; border: 1px solid transparent; border-radius: 4px;
padding: .3rem .7rem; color: var(--muted);
}
.tabs button:hover { color: var(--link); }
.tabs button[aria-selected="true"] {
color: var(--text); background: var(--panel); border-color: var(--rule);
}
.tabs button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
main:not(.tabbed) .tabs { display: none; }
main.tabbed .panel[hidden] { display: none; }
.toc-group {
margin: 1.1rem 0 .3rem; font-size: .72rem; letter-spacing: .09em;
text-transform: uppercase; color: var(--muted); opacity: .75;
}
.toc-group:first-child { margin-top: 0; }
.lede { font-size: 1.1rem; }
.note { color: var(--muted); font-size: .94rem; }
/* The table of contents: a bar with a toggle when the window is narrow, a
fixed column beside the text when there is room for one. The toggle is a
checkbox so that no script is needed to open it. */
.toc-toggle { position: absolute; width: 1px; height: 1px; opacity: 0; }
.toc-button {
display: block; cursor: pointer; user-select: none;
position: sticky; top: 0; z-index: 2;
padding: .6rem 1.25rem;
background: var(--panel); border-bottom: 1px solid var(--rule);
color: var(--muted); font-size: .92rem; letter-spacing: .02em;
}
.toc-button::before { content: "\2261\00a0\00a0"; }
.toc-toggle:focus-visible + .toc-button { outline: 2px solid var(--accent); outline-offset: -2px; }
nav.toc {
display: none;
background: var(--panel); border-bottom: 1px solid var(--rule);
padding: .75rem 1.25rem 1.1rem;
}
.toc-toggle:checked ~ nav.toc { display: block; }
.toc-title {
display: none; /* the toggle already says "Contents" */
margin: 0 0 .5rem; font-size: .78rem; letter-spacing: .09em;
text-transform: uppercase; color: var(--muted);
}
nav.toc ul { list-style: none; padding: 0; margin: 0; }
nav.toc li { margin: 0; font-size: .93rem; }
nav.toc a {
display: block; padding: .2rem 0; color: var(--muted);
text-decoration: none; border-left: 2px solid transparent; padding-left: .6rem;
}
nav.toc a:hover { color: var(--link); border-left-color: var(--accent); }
h2, h3 { scroll-margin-top: 3.5rem; }
/* Nineteen entries one to a line fills a phone screen, so the opened panel
goes to two columns as soon as there is room for them. */
@media (min-width: 30rem) and (max-width: 65.99rem) {
nav.toc ul { columns: 2; column-gap: 1.5rem; }
nav.toc li { break-inside: avoid; }
}
footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--rule); color: var(--muted); font-size: .92rem; }
/* Wide enough for both: the contents become a column of their own, scrolling
independently of the page, and the text moves over to sit beside it. */
@media (min-width: 66rem) {
.toc-button { display: none; }
nav.toc {
display: block;
position: fixed; top: 0; left: 0; bottom: 0;
width: 15rem; overflow-y: auto; overscroll-behavior: contain;
background: none; border-bottom: 0; border-right: 1px solid var(--rule);
padding: 3.6rem 1rem 2rem 1.5rem;
}
main { margin-left: 15rem; }
.toc-title { display: block; }
h2, h3 { scroll-margin-top: 1rem; }
}
.c { color: var(--c-comment); font-style: italic; }
.s { color: var(--c-string); }
.f { color: var(--c-form); font-weight: 600; }
.n { color: var(--c-num); }
.t { color: var(--c-type); }
.k { color: var(--c-kw); }
</style>
</head>
<body>
<input type="checkbox" id="toc-toggle" class="toc-toggle">
<label class="toc-button" for="toc-toggle">Contents</label>
<nav class="toc" aria-label="Contents">
<p class="toc-title">Contents</p>
<p class="toc-group">Language</p>
<ul>
<li><a href="#what">What Flan is</a></li>
<li><a href="#start">Getting started</a></li>
<li><a href="#values">Values and memory</a></li>
<li><a href="#types">Types</a></li>
<li><a href="#structs">Structs and enums</a></li>
<li><a href="#functions">Functions</a></li>
<li><a href="#control">Control flow</a></li>
<li><a href="#defer">defer</a></li>
<li><a href="#arrays">Arrays and slices</a></li>
<li><a href="#printing">Printing</a></li>
<li><a href="#prelude">The prelude</a></li>
<li><a href="#packages">Packages</a></li>
</ul>
<p class="toc-group">Conditions</p>
<ul>
<li><a href="#conditions">Conditions and restarts</a></li>
<li><a href="#break">The break loop</a></li>
</ul>
<p class="toc-group">The FFI</p>
<ul>
<li><a href="#ffi">The FFI</a></li>
</ul>
<p class="toc-group">The dev loop</p>
<ul>
<li><a href="#devloop">The dev loop</a></li>
<li><a href="#emacs">Emacs</a></li>
</ul>
<p class="toc-group">Builds</p>
<ul>
<li><a href="#targets">Targets and builds</a></li>
<li><a href="#notyet">Not implemented yet</a></li>
<li><a href="#reading">Further reading</a></li>
</ul>
</nav>
<main>
<div class="wrap">
<div class="tabs" role="tablist" aria-label="Sections">
<button type="button" role="tab" data-panel="panel-language" aria-selected="true">Language</button>
<button type="button" role="tab" data-panel="panel-conditions" aria-selected="false">Conditions</button>
<button type="button" role="tab" data-panel="panel-ffi" aria-selected="false">The FFI</button>
<button type="button" role="tab" data-panel="panel-devloop" aria-selected="false">The dev loop</button>
<button type="button" role="tab" data-panel="panel-builds" aria-selected="false">Builds</button>
</div>
<header>
<svg class="mark" viewBox="0 0 118 44" role="img" aria-label="Flan">
<!-- The dot is a full stop in the same run, so it stays beside the n
whichever serif the reader actually has. -->
<text x="0" y="33" font-family="Iowan Old Style, Palatino, Georgia, serif"
font-size="38" font-weight="600" letter-spacing="1"
fill="currentColor">flan<tspan fill="var(--accent)">.</tspan></text>
</svg>
<p class="tagline">A statically typed Lisp for game development. Clojure's brackets,
C's memory and value model, no garbage collector.</p>
</header>
<p class="lede">Flan compiles s-expressions to LLVM IR and then to a native binary.
There are no object headers, so a Flan struct is exactly its C struct. There is no
collector, so nothing runs between frames that you did not write. And a running
program can be edited: a function recompiled in Emacs is installed into the live
process at its next frame boundary, in about twenty milliseconds.</p>
<p>This page describes the compiler <em>as it is</em>, not as it is planned. Where
something is designed but not built, it says so and gives the message the compiler
prints for it. Every Flan program on this page is a file in <code>web/examples/</code>
with its output recorded beside it; <code>sh web/examples/check.sh</code> runs them all
and compares, and <code>quotes.sh</code> re-derives the blocks that are transcripts
rather than programs.</p>
<div class="panel" id="panel-language">
<h2 id="what">What Flan is</h2>
<p>A minimal Lisp for games. In one line: Odin with a Lisp frontend and a live REPL.
Types are mandatory and inference makes them feel optional; memory is manual; the
frontend is OCaml, the backend writes LLVM IR as text and hands it to
<code>clang</code>.</p>
<p>What it is not:</p>
<ul>
<li><strong>Not a Common Lisp and not a Clojure.</strong> No numeric tower, no CLOS,
no <code>format</code>, no lazy seqs, no persistent collections, no JVM.</li>
<li><strong>No immutable collection types.</strong> Structure sharing destroys clear
ownership, and clear ownership is the only thing that removes the need for a
collector. Value structs that copy on assignment replace them.</li>
<li><strong>No dynamic typing.</strong> A tag word on every value is exactly the
header cost that dropping the GC was meant to avoid.</li>
<li><strong>No consoles</strong>, and no live-image development at SBCL's level.</li>
</ul>
<h2 id="start">Getting started</h2>
<p>You need OCaml with dune, and a <code>clang</code> on <code>PATH</code>. Build the
compiler, then run something:</p>
<pre><code class="sh">$ dune build
$ ./_build/default/bin/main.exe run calc-me.flan "1 + 2 * (3 - 0.5) / 2"
3.5</code></pre>
<p>Call that binary <code>flan</code>. Its subcommands:</p>
<pre><code class="sh">$ flan
usage: flan (read|parse|check|emit|shim) &lt;file.flan&gt;...
flan build &lt;file.flan&gt; [-o out] [--no-bounds-checks] [--dev] [--debug] [--target=wasm32-wasi]
flan run &lt;file.flan&gt; [args...]
flan reload &lt;program.flan&gt; &lt;forms.flan&gt; [-o out.so]
flan dev &lt;program.flan&gt; [-s socket]</code></pre>
<p><code>read</code>, <code>parse</code>, <code>check</code>, <code>emit</code> and
<code>shim</code> each stop the pipeline one stage further along and print what it
produced. <code>run</code> builds to a temporary file and execs it.</p>
<p>The smallest program:</p>
<pre><code>(defn main [] ()
(println "hello from flan"))</code></pre>
<p>The entry point is <code>(defn main [args [string]] i32)</code>. The parameter is
optional — omitting <code>args</code> means the program ignores argv — and the return
type is not: <code>()</code> is unit, and a <code>main</code> that returns it exits 0.</p>
<h2 id="values">Values and memory</h2>
<p>There is no garbage collector and no hidden allocation. Every local is a stack
slot; reading one is a load, assigning to one is a store, and a store of an aggregate
<em>is</em> the copy. A struct is its C layout with nothing added.</p>
<p>Four rules carry most of the model:</p>
<ul>
<li><strong>Zero is initialisation.</strong> A declaration with no initialiser is
all-bytes-zero, so a global array is BSS and costs nothing to start. A struct
literal that omits a field zeroes it. <code>(zeroed)</code> re-zeroes something
later — a memset, not an allocation.</li>
<li><strong>Fixed arrays are values.</strong> <code>[n T]</code> is inline storage
and copies on assignment and on pass-by-value.</li>
<li><strong>Slices are views.</strong> <code>[T]</code> is ptr+len and owns nothing.
Copying a slice copies the view, never the elements.</li>
<li><strong>Pointers are visible.</strong> <code>(addr x)</code> takes the address of
any assignable place and gives <code>(Ptr T)</code>. It does not extend anything's
lifetime, and keeping one past its frame is your contract to honour — there is no
borrow checker.</li>
</ul>
<p>A value struct is shared mutably by passing its address down the call chain. No
heap is involved.</p>
<p>Places — the forms <code>set</code> accepts — are a fixed list, not an extensible
<code>setf</code>:</p>
<pre><code>(defstruct Enemy [hp i32 name string])
(defvar spawned i32)
(defconst room-size 4)
(defvar room [room-size i32])
;; `set` takes a fixed list of forms, not an extensible setf.
(defn main [] ()
(let [e (Enemy {.hp 10 .name "slime"})
p (addr e)]
(set spawned (+ spawned 1)) ; a local or a defvar
(set (.hp e) 7) ; a struct field
(set (.hp p) 8) ; through a (Ptr Enemy) — derefs one level
(set (at room 2) 5) ; a fixed array or slice element
(set (deref p) (Enemy {.hp 3 .name "wisp"})) ; a whole-object store
(print (.hp e)) (println "")
(println (.name e))
(print (at room 2)) (println "")
(print spawned) (println "")))</code></pre>
<pre><code class="sh">3
wisp
5
1</code></pre>
<p><code>.field</code> and <code>at</code> dereference exactly one pointer level, so
<code>(set (.hp p) 8)</code> above is legal when <code>p</code> is a
<code>(Ptr Enemy)</code>. The whole-object store through <code>p</code> overwrote
<code>e</code> itself, so <code>hp</code> reads 3 and not 8.</p>
<h3>Bounds are checked</h3>
<pre><code>(defconst xs [3 i32] [1 2 3])
;; (at xs 7) with a literal index does not reach the backend at all: check.ml
;; rejects it. This one goes through a local, so it is the runtime check that
;; catches it — the same message, and the program stops where it happened.
(defn main [] ()
(let [i 7]
(println "before")
(print (at xs i))
(println "unreachable")))</code></pre>
<pre><code class="sh">$ flan run bounds.flan
before
bounds.flan:9:19: index 7 is out of bounds for length 3
$ echo $?
134</code></pre>
<p><code>at</code> and <code>slice</code> emit a comparison and a branch to a cold
block that names the source location and stops. Checks are on by default and are not
tied to the optimisation level. <code>--no-bounds-checks</code> turns them off.
Measured cost on a
50-million-iteration dependency chain over a 1024-element array: 0.110.12s checked
against 0.120.13s unchecked.</p>
<h2 id="types">Types</h2>
<p>Types are annotated at function boundaries and inferred everywhere else. Every type
notation reads as exactly one data item.</p>
<div class="scroll">
<table>
<tr><th>Notation</th><th>Meaning</th><th>Layout</th></tr>
<tr><td><code>i8</code><code>i64</code>, <code>u8</code><code>u64</code></td><td>machine integers, wrapping arithmetic</td><td>the obvious one</td></tr>
<tr><td><code>f32</code>, <code>f64</code></td><td>floats</td><td><code>float</code>, <code>double</code></td></tr>
<tr><td><code>bool</code></td><td></td><td><code>i1</code></td></tr>
<tr><td><code>string</code></td><td>a byte slice with no NUL</td><td>ptr + len</td></tr>
<tr><td><code>[T]</code></td><td>slice, non-owning</td><td>ptr + len</td></tr>
<tr><td><code>[n T]</code></td><td>fixed array, a value</td><td>n inline items</td></tr>
<tr><td><code>(Ptr T)</code></td><td>raw pointer</td><td>a pointer</td></tr>
<tr><td><code>(Option T)</code></td><td><code>Some</code> / <code>None</code></td><td>tag byte + T</td></tr>
<tr><td>a struct</td><td>value type</td><td>fields in declaration order</td></tr>
<tr><td>an enum</td><td>its own type in the checker</td><td><code>i32</code></td></tr>
<tr><td><code>()</code></td><td>one value, zero size</td><td>empty</td></tr>
<tr><td><code>Never</code></td><td>fits anywhere; nothing has it</td><td>empty</td></tr>
</table>
</div>
<p><strong>There is no implicit widening.</strong> Both operands of a binary operator
have one type, and every conversion is written as a cast:</p>
<pre><code>(defn main [] i32
(let [n 40 ; i32, inferred
big (i64 n) ; every widening is written
x 1.5] ; f64
(print (+ big 2)) (println "")
(print (* x 2.5)) (println "")
(print (bit-xor (&lt;&lt; 1 8) 255)) (println "")
0))</code></pre>
<pre><code class="sh">42
3.75
511</code></pre>
<p>An untyped integer literal is <code>i32</code> and an untyped float literal is
<code>f64</code>, so <code>(defconst gravity f32 0.05)</code> names the type when
something narrower is wanted. One caveat: a whole-numbered float prints without its
fraction, so <code>3.0</code> comes out as <code>3</code>.</p>
<p>Arithmetic wraps. Shifts are bounded two ways: a literal count at or past the
operand's width is a compile error, and a computed one is masked to the width.
<code>&gt;&gt;</code> is arithmetic on a signed type and logical on an unsigned
one.</p>
<p>An index converts from a narrower integer and never from a wider one. A
<code>u32</code> index is fine — anything above 2<sup>31</sup> truncates to a negative
<code>i32</code> and the unsigned bounds check rejects it. An <code>i64</code> index is
refused:</p>
<pre><code class="sh">an index is an i32, and i64 is wider — write (i32 …), because a value that does
not fit truncates to one that does and would read the wrong element without
tripping the bounds check</code></pre>
<h2 id="structs">Structs and enums</h2>
<p>A <code>defstruct</code> is a list of inline name/type pairs. A struct literal names
its fields, and omitted fields are zeroed.</p>
<pre><code>(defstruct Cursor
[src [u8] ; a non-owning slice
pos i32]) ; no initialiser means zeroed
(defn peek [c (Ptr Cursor)] u8
(if (&lt; (.pos c) (len (.src c)))
(at (.src c) (.pos c))
0))
(defn advance [c (Ptr Cursor)] ()
(set (.pos c) (+ (.pos c) 1))) ; field access derefs one level
(defn main [] ()
(let [c (Cursor {.src (bytes "hi")})] ; pos omitted, so pos is 0
(print (peek (addr c))) (println "")
(advance (addr c))
(print (peek (addr c))) (println "")))</code></pre>
<pre><code class="sh">104
105</code></pre>
<p>Those are the bytes <code>h</code> and <code>i</code>. There is no character type — a
byte is a <code>u8</code> — but there is a byte literal, so <code>\h</code> is 104 and
<code>\space</code> is 32, and the prelude's <code>digit?</code> reads as
<code>(and (&gt;= b \0) (&lt;= b \9))</code>. To see a byte as a letter rather than as a
number, print a slice of them: <code>print</code> writes a <code>[u8]</code> as
its bytes.</p>
<p>An enum is an <code>i32</code> at run time and its own type in the checker. A
keyword at a call site resolves against the parameter's enum type at compile time, so a
typo is an error there rather than a wrong number later.</p>
<pre><code>(defenum Key
[space 32 escape 256 left 263 right 262])
(defn key-name [k Key] string
(cond
(= k :space) "space"
(= k :escape) "escape"
:else "an arrow"))
(defn main [] ()
;; :space resolves against the parameter's enum at compile time.
;; A typo is an error here, not a wrong number later.
(println (key-name :space))
(println (key-name :left)))</code></pre>
<pre><code class="sh">space
an arrow</code></pre>
<p>A keyword means nothing where no enum is expected. There is no keyword type to fall
back on, and there is no way to name a member other than as a keyword in a position
that expects that enum.</p>
<p>Struct and enum names share one top-level namespace with functions, globals and type
aliases. A second declaration of a name is rejected whatever kind either one is.</p>
<h2 id="functions">Functions</h2>
<p><code>(defn name [param Type ...] ReturnType body ...)</code>. The parameters are
inline name/type pairs, as in <code>let</code> and <code>defstruct</code>. The return
type is always written, and a function that returns nothing writes <code>()</code>,
which is unit. There is no separate <code>declare</code> form for a function with a
body — <code>declare</code> is kept only where there is none.</p>
<p>The slot used to be optional, and the parser decided return-type-versus-body by
looking the name up in a table of the file's types. It was sound only because one
top-level namespace means a name cannot be both a type and a value, and it was
silently wrong twice — once reading <code>(Rune {.code 65})</code> at the head of a
body as the function's return type. Writing the type removes the guess, and a
mistyped one now says <em>did you mean f64</em> rather than <em>unknown name</em>.</p>
<p>Top-level names are order-independent within a package, so mutually recursive
functions need no forward declaration. Globals come in two kinds:</p>
<pre><code>(defconst cell-size 5) ; a compile-time constant
(defconst gravity f32 0.05) ; with its type named
(defvar current-color i32) ; zeroed storage
(defconst rows 3)
(defconst cols 4)
(defvar grid [rows [cols u32]]) ; BSS, rows*cols*4 bytes
(defn main [] ()
(print cell-size) (println "")
(print gravity) (println "")
(print current-color) (println "")
(print (at grid 2 3)) (println ""))</code></pre>
<pre><code class="sh">5
0.05
0
0</code></pre>
<p>A <code>defconst</code> the checker consumed — an array length, for instance — is
part of the shape of the program. One it did not is only ever bytes in memory. The
two reload differently; see <a href="#devloop">the dev loop</a>.</p>
<p><code>let</code> binds name/value pairs and takes no type annotation, so a constant
whose type matters is named at the top level rather than written inline.</p>
<h2 id="control">Control flow</h2>
<p><code>if</code>, <code>when</code>, <code>unless</code>, <code>cond</code>,
<code>do</code>, <code>and</code>, <code>or</code>, <code>not</code>,
<code>while</code>, <code>until</code>, <code>dotimes</code>, <code>return</code>,
<code>match</code>. <code>and</code> and <code>or</code> short-circuit.
<code>:else</code> is <code>cond</code>'s catch-all.</p>
<pre><code>(defconst nums [5 i32] [1 3 8 9 10])
(defn classify [n i32] string
(cond
(&lt; n 0) "negative"
(= n 0) "zero"
:else "positive"))
(defn countdown [n i32] ()
(let [i n]
(while (&gt; i 0)
(print i)
(print " ")
(set i (- i 1)))
(println "")))
(defn first-even [s [i32]] (Option i32)
(dotimes [i (len s)]
(when (= 0 (% (at s i) 2))
(return (Some (at s i)))))
None)
(defn main [] ()
(println (classify -3))
(countdown 4)
(unless false
(println "unless runs when the test is false"))
(match (first-even (slice nums 0 (len nums)))
(Some n) (do (print n) (println ""))
None (println "none")))</code></pre>
<pre><code class="sh">negative
4 3 2 1
unless runs when the test is false
8</code></pre>
<p><code>dotimes</code> evaluates its bound once into a hidden slot before the loop, so
a body that changes it cannot change the trip count, and the loop variable is not
assignable.</p>
<p>Loops are imperative, with <code>while</code>, <code>until</code> and
<code>return</code>. There is no <code>loop</code>/<code>recur</code>. There is no
<code>break</code> or <code>continue</code> yet either; both refuse by name:</p>
<pre><code class="sh">break is not implemented yet (see the build sequence in plan.org)</code></pre>
<p>An early exit out of a loop is <code>return</code>, as <code>first-even</code> does
above.</p>
<h3>Option, <code>match</code> and <code>some</code></h3>
<p><code>(Option T)</code> is how absence is spelled: a lookup miss, an empty
collection, the end of a stream. <code>match</code> works on an <code>Option</code> and
on nothing else today. <code>some</code> unwraps <code>Some</code> and early-returns
<code>None</code> from the enclosing function.</p>
<pre><code>(defconst nums [4 i32] [4 8 15 16])
;; `some` unwraps Some and early-returns None from *this* function.
(defn doubled-first [s [i32]] (Option i32)
(Some (* 2 (some (index-of-i32 s 15)))))
(defn main [] ()
(match (doubled-first (slice nums 0 4))
(Some i) (do (print i) (println "")) ; 4
None (println "not found"))
(match (index-of-i32 (slice nums 0 4) 99)
(Some i) (do (print i) (println ""))
None (println "not found")))</code></pre>
<pre><code class="sh">4
not found</code></pre>
<h2 id="defer">defer</h2>
<p>A <code>defer</code> runs at function exit, innermost first. An explicit
<code>return</code> runs the ones registered above it — a defer written below a return
has not executed yet and must not fire.</p>
<pre><code>(defn work [n i32] i32
(defer (println "second"))
(defer (println "first")) ; innermost-first at exit
(when (&lt; n 0)
(return 0)) ; runs both defers above it
(println "body")
n)
(defn main [] ()
(print (work 3))
(println ""))</code></pre>
<pre><code class="sh">body
first
second
3</code></pre>
<p><code>defer</code> is function-scoped and is <strong>rejected</strong> inside a
<code>let</code>, a loop or a branch. Block scoping it is not done:</p>
<pre><code class="sh">defer must be a top-level form in a function body — block-scoped defer is not implemented yet (milestone 4)</code></pre>
<h2 id="arrays">Arrays and slices</h2>
<p><code>at</code> indexes a fixed array or a slice, and takes any number of
indices, so <code>(at grid r c)</code> indexes a two-dimensional fixed array directly.
It is a place: <code>(set (at grid r c) v)</code> and <code>(addr (at grid r c))</code> both work.
<code>len</code> works on a fixed array, a slice or a string.
<code>(slice s lo hi)</code> takes a half-open range and never copies.</p>
<pre><code>(defconst rows 3)
(defconst cols 4)
;; A fixed array is a value: inline storage, copies on assignment.
(defconst palette [4 u32] [0xE6B800FF 0x3B6E8CFF 0xA83232FF 0xCC6B1FFF])
;; No initialiser means all-bytes-zero, so this is BSS and costs nothing.
(defvar grid [rows [cols i32]])
(defn main [] ()
(set (at grid 1 2) 7)
(print (at grid 1 2)) (println "") ; 7
(print (len palette)) (println "") ; 4
;; A slice is ptr+len and non-owning: it views the array, it does not copy it.
(let [row (slice (at grid 1) 0 cols)]
(set (at row 0) 5)
(print (at grid 1 0)) (println "") ; 5 — the same storage
(print (sum-i32 row)) (println "")) ; 12
;; (zeroed) is a memset, not an allocation.
(set grid (zeroed))
(print (at grid 1 2)) (println "")) ; 0</code></pre>
<pre><code class="sh">7
4
5
12
0</code></pre>
<p>A reversed range — <code>lo</code> greater than <code>hi</code> — traps, rather than
yielding a huge unsigned length.</p>
<h2 id="printing">Printing</h2>
<p><code>println</code> prints a value and a newline; <code>print</code> is the same
walk without the newline. There is one of each and they take any type, but neither is a
function and neither is overloading: the compiler walks the argument's type where the
call is written and emits the printing for it. Nothing is decided at run time — a Flan
value carries no header, so nothing at run time could say what it is — and there is no
user-supplied printer to choose between.</p>
<pre><code>(defenum Key [space 32 left 263])
(defstruct Enemy [hp i32 name string key Key])
(defn look-up [k Key] (Option i32)
(if (= k :space) (Some 32) None))
(defn main [] ()
(println 42) ; an i32, uncast
(println 1.5)
(println (Enemy {.hp 3 .name "wisp" .key :left}))
(println (look-up :space))
(println (look-up :left))
(print "no newline: ") (println true))</code></pre>
<pre><code class="sh">42
1.5
(Enemy {:hp 3 :name "wisp" :key :left})
(some 32)
none
no newline: true</code></pre>
<p>The walk covers every integer and float type, <code>bool</code>, <code>()</code>,
<code>string</code>, <code>[u8]</code>, enums, <code>Ptr</code>, <code>Option</code>,
structs, fixed arrays and slices. An enum member comes back as its name: the value is
an <code>i32</code> by the time the backend sees it, so the name is recovered here from
the checker's table, and a value outside the declared members falls through to the
number, which is what you would want to see. A <code>Ptr</code> prints as
<code>&lt;ptr&gt;</code> and is never followed — it is the one thing that could make
the walk cycle, and dereferencing a pointer on someone else's behalf is not safe.</p>
<p>A string prints raw at the top level and quoted-and-escaped inside a structure.
Those are not in conflict: <code>(println "hello")</code> has to print
<code>hello</code> or it is useless, and the <code>name</code> field above has to be
quoted or it could not be told from the punctuation around it.</p>
<p>The walk is bounded in depth and in span, so a deeply nested value or a
<code>[100 [100 u32]]</code> grid prints <code>...</code> rather than a screenful — and,
since the walk is unrolled at compile time, rather than putting ten thousand printing
sites in the module.</p>
<p>That the walk takes the argument's own type matters more here than it would in a
language that widens implicitly. Nothing widens implicitly in this one, so a printer
that named a type would need a cast written at every call — and a <code>u64</code>
above 2<sup>63</sup> put through a signed one comes out negative. <code>print</code>
takes the value as it is and prints the number it holds.</p>
<h2 id="prelude">The prelude</h2>
<p>The prelude is written in Flan, all but one line of it, and prepended to every
program, so nothing in it needs importing. It holds no printing of its own:
<code>print</code> and <code>println</code> are the compiler's, and
<code>write-stdout</code> — the one output primitive — is what they are written
over.</p>
<div class="scroll">
<table>
<tr><th>Group</th><th>Names</th></tr>
<tr><td>slices of <code>i32</code></td><td><code>swap-i32!</code>, <code>reverse-i32!</code>, <code>sort-i32!</code>, <code>index-of-i32</code>, <code>min-i32</code>, <code>max-i32</code>, <code>sum-i32</code></td></tr>
<tr><td>bytes</td><td><code>bytes=?</code>, <code>starts-with?</code>, <code>ends-with?</code>, <code>index-of-byte</code>, <code>index-of-bytes</code>, <code>trim</code>, <code>digit?</code>, <code>space?</code></td></tr>
<tr><td>parsing</td><td><code>parse-i64</code>, <code>parse-f64</code></td></tr>
<tr><td>text</td><td><code>split-on-byte</code>, <code>split-next!</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>bytes-ci=?</code></td></tr>
<tr><td>UTF-8</td><td><code>decode-rune</code>, <code>rune-at</code>, <code>rune-count</code>, <code>rune-size</code>, <code>rune-start?</code>, <code>valid-utf8?</code>, <code>encode-rune!</code></td></tr>
<tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, and <code>sqrt-f32</code>, which is the one <code>declare</code> in the file</td></tr>
<tr><td>random</td><td><code>rand-seed</code>, <code>rand-u32</code>, <code>rand-f32</code>, <code>rand-i32-range</code>, <code>rand-f32-range</code></td></tr>
</table>
</div>
<p><strong>The RNG is ours, not libc's</strong> — PCG-XSH-RR 32, written in Flan —
because a grid hash is only a regression test if the sequence is byte-identical on
native and on wasm32. <strong>The parsers are ours too</strong>:
<code>strtoll</code> answers 0 for <code>""</code>, 0 for
<code>"abc"</code> and 12 for <code>"12x"</code>, which are three wrong answers a caller
cannot tell from a real 12.</p>
<p><code>sqrt-f32</code> is the one function in the file that is not Flan:
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code>. Every other number
here is reachable from the four operations and a cast; a square root is not, and the
usual trick of seeding Newton's method from the exponent bits needs a bit-cast between
<code>f32</code> and <code>u32</code> that the language does not have. IEEE-754 makes
<code>sqrt</code> correctly rounded, so libm gives the same bit pattern on both
targets anyway. Every link carries <code>-lm</code>.</p>
<p>There is no <code>println</code>. There is no overloading yet, so each printer names
its type. <code>(println 1)</code> is <code>unknown function println</code>.</p>
<p>The primitives underneath are few — a primitive is the only thing implemented
twice per backend: <code>argv</code>,
<code>write-stdout</code>, <code>exit</code>, <code>len</code>, <code>at</code>,
<code>slice</code>, <code>bytes</code>, <code>bytes-&gt;f64</code>,
<code>bytes-&gt;i64</code>, <code>f64-&gt;bytes</code>, <code>i64-&gt;bytes</code>,
<code>addr</code>, and arithmetic, comparison and casts.</p>
<h2 id="packages">Packages</h2>
<p><strong>The directory is the package.</strong> Every file in a directory shares one
top-level scope; files within a package do not import each other, and their order does
not matter. The package declaration is optional and the name is inferred from the
directory, so a loose file in a scratch directory is a package of one with no manifest
and no ceremony.</p>
<pre><code>;; geom/vec.flan — no package declaration: the name comes from the directory.
(defstruct V2 [x f32 y f32])
(defn add [a V2 b V2] V2
(V2 {.x (+ (.x a) (.x b)) .y (+ (.y a) (.y b))}))</code></pre>
<pre><code>;; geom/len.flan — a second file in the same directory shares one top-level
;; scope: it does not import vec.flan, and the order of the two does not matter.
(defn length [v V2] f32
(sqrt-f32 (+ (* (.x v) (.x v)) (* (.y v) (.y v)))))</code></pre>
<pre><code>;; pkg.flan — the directory is the package, and everything it declares
;; arrives qualified by the alias this import chose.
(import g "geom")
(defn main [] ()
(let [v (g/add (g/V2 {.x 3.0 .y 0.0})
(g/V2 {.x 0.0 .y 4.0}))]
(print (g/length v))
(println "")))</code></pre>
<pre><code class="sh">5</code></pre>
<p>There is one form and one meaning: <code>(import alias "path")</code>, and everything
from the package is qualified <code>alias/name</code>. There is no unqualified-import
mode, no <code>:refer</code>, no <code>ns</code> form and no per-file namespace object.
A path with no collection prefix is relative to the importing file;
<code>vendor:</code> and <code>core:</code> are collections, resolved by walking up from
the importing file until a directory of that name is found.</p>
<p>Importing is a rename. Every top-level name the package declares becomes
<code>alias/name</code>, and every use of one — in a type, in a body, in a struct
literal, in an array length — is rewritten to match. Nothing downstream knows a package
existed.</p>
<p>Three more rules:</p>
<ul>
<li><strong>A package may be a single <code>.flan</code> file</strong> named outright,
for the program that is also a library. <code>sand.flan</code> shares a directory with
three other loose programs, so naming its directory would import all four.</li>
<li><strong>A package may import a package</strong>, and the qualification flattens to
the inner alias: raylib imported by a package that is itself imported is still
<code>rl/…</code>. A directory is keyed by its real path and read once, so a cycle
ends there. The same directory under two different aliases is refused.</li>
<li><strong><code>main</code> is not exported.</strong> A package carrying one would
collide with the importer's, and <code>main</code> is a reachability root, so an
imported one would keep everything it calls alive. Writing <code>sand/main</code> is
refused at the line that wrote it.</li>
</ul>
<p>Visibility is that one rule and no more: there is no package-private marker for
anything other than <code>main</code> yet.</p>
<p>A package may carry the C it binds to. Every <code>.c</code> file in the directory is
compiled into the build, and a file named <code>link</code> lists extra linker
arguments. Whether those reach the build is decided after checking, from the program
rather than from the import list: the compiler starts at <code>main</code>, follows every
call, and a package none of whose externs survive contributes no C and no linker
argument, so a file that imports raylib still builds for wasm32.</p>
</div>
<div class="panel" id="panel-conditions">
<h2 id="conditions">Conditions and restarts</h2>
<p>A condition is a struct. There is no class hierarchy; matching is by type. The
signalling end says <em>here is something notable, here is the data</em>, and an outer
caller decides what to do about it — or decides nothing, in which case the signaller
carries on.</p>
<pre><code>(signal c) ; (). Handler returns -&gt; carry on. No handler -&gt; no-op.
(error c) ; Never. Only a transfer gets past; else the program stops.
(handler-bind [(Type [c] body ...) ...] body ...) ; match by type, no hierarchy
(restart-case BODY ; BODY and every clause have the same type = the form's
(name [] CLAUSE) ...)
(invoke-restart 'name) ; Never. Innermost frame offering the name wins.</code></pre>
<p><code>signal</code> has type <code>()</code>, always. A handler that returns
normally leaves the signaller to carry on — the accumulation case:</p>
<pre><code>(defstruct AssetMissing [id i32])
(defvar seen i64)
(defn load-all [] ()
(signal (AssetMissing {.id 1})) ; () — the caller carries on
(signal (AssetMissing {.id 2})))
(defn main [] ()
(load-all) ; no handler: a no-op
(print seen) (println "") ; 0
;; A handler that returns normally accumulates and lets the signaller run on.
(handler-bind [(AssetMissing [c] (set seen (+ seen (i64 (.id c)))))]
(load-all))
(print seen) (println "")) ; 3</code></pre>
<pre><code class="sh">0
3</code></pre>
<p>A handler that invokes a restart instead transfers control outward to the
<code>restart-case</code> that offers the name, and the clause's value becomes that
form's value. Every <code>defer</code> between the invoke and the target runs, innermost
first, before the clause body starts.</p>
<pre><code>(defstruct AssetMissing [id i32])
(defvar cleanups i64)
(defn load [n i32] i32
(signal (AssetMissing {.id n}))
100)
(defn middle [n i32] i32
(defer (set cleanups (+ cleanups 1))) ; runs on the transfer too
(+ (load n) 1))
(defn fetch [n i32] i32
(restart-case (middle n) ; its value if nothing transfers
(use-placeholder [] -1)
(retry [] 7)))
(defn main [] ()
(print (fetch 1)) (println "") ; 101 — nothing handled it
(handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
(print (fetch 2)) (println "")) ; -1
(print cleanups) (println "")) ; 2 — the defer ran both times</code></pre>
<pre><code class="sh">101
-1
2</code></pre>
<p>Restart lookup walks the dynamic restart stack from innermost outward and takes the
first frame offering the name, so an inner <code>restart-case</code> shadows an outer
one for the duration of its body. An inner parser's <code>skip-form</code> is found
before an outer one's.</p>
<h3>How a transfer is lowered</h3>
<p>A transfer is not platform unwinding. Every Flan signature carries a transfer channel
— one pointer appended as an out-parameter — which <code>invoke-restart</code> writes
and every call site checks. A callee writes the target into its caller's slot; each
frame checks, runs its defers and returns early. The disassembly is the release one plus
a guard after each call.</p>
<p>Three consequences:</p>
<ul>
<li><strong>wasm32 works with no exception proposal</strong>, and native and wasm
builds of the same program agree.</li>
<li><strong>Every function carries the channel, release builds included.</strong> A
hot-reload cell holds a bare pointer, so the honest answer to "what can this call?" is
"anything". A later optimisation may stop a function checking the channel; it may not
drop the parameter.</li>
<li><strong>A transfer cannot cross a foreign frame.</strong> A handler installed
across an FFI boundary must return normally.</li>
</ul>
<h3>Gotchas</h3>
<ul>
<li><strong>A handler closes over nothing.</strong> A clause is lifted into a function
of its own, because it runs from wherever the signal was. Accumulate into a global, or
put the value on the condition. A reference to an enclosing local is refused for that
reason rather than reported as an unknown name.</li>
<li><strong>A restart is not a transaction.</strong> Control resumes at the
<code>restart-case</code> and runs forward from there, so a <code>retry</code> repeats
every side effect between it and the target. Nothing rolls back — a global the frame
already set stays set, and is set again. Common Lisp has exactly this property and
offers no help either.
<p>So the author chooses where the retry boundary is. A <code>restart-case</code> at the
top of a frame re-runs everything including mutations already applied; one placed after
the mutations re-runs only what follows. Put the restart before anything mutates, make
the retried section idempotent, or snapshot what will be re-applied.</p>
<p>This matters more here than in most Lisps because the intended use is a
<em>game loop</em>, where the plan is to skip a frame and carry on rather than die. Now
that a bad index signals <code>BoundsError</code> instead of ending the process,
abandoning a frame and retrying it is a real thing to do — and that is exactly the case
a non-idempotent mutation spoils.</p></li>
<li><strong>An unknown restart name is a hard stop</strong> — a located runtime error.
There is no <code>find-restart</code> to test with yet.</li>
<li><strong>No supertype</strong>, so nothing can say "any condition".</li>
<li><strong><code>signal</code> cannot hand a value back.</strong> Deliberate: the
alternative forces every signal site to declare a default value and a result type.</li>
<li><strong><code>return</code> is refused inside a <code>handler-bind</code> or
<code>restart-case</code> body</strong>, and so is <code>invoke-restart</code> inside a
<code>defer</code>. In each case a bare exit would leave frames on the stack pointing
into a function that has gone.</li>
<li><strong>Restarts take no parameters</strong> in this version.</li>
</ul>
<h2 id="break">The break loop</h2>
<p>An <code>error</code> nothing handles does not kill the program. It stops on the
frame that erred, with nothing unwound, so the condition and every restart between there
and the top are still live:</p>
<pre><code class="sh">flan: unhandled Missing — stopped, not dead.
0. restart: retry
1. restart: use-placeholder</code></pre>
<p>From there you fix the function, install it, and take a restart. The numbers are
how one is taken: a restart is chosen by position, because an inner one may shadow an
outer one of the same name and a name alone could not tell you which you were getting.
Control never left the erring frame, so <code>retry</code> calls through the
indirection cell and reaches the new body. Installing while stopped is allowed; there is no frame in progress.</p>
<p>The break loop lives in <code>vendor/agent</code>, which is an optional package. A
program that does not import it leaves the hook null and stops the old way — the message
and an exit status of 134:</p>
<pre><code>(defstruct Missing [id i32])
(defn load [n i32] i32
(restart-case
(do (error (Missing {.id n})) ; Never — only a transfer gets past
0)
(use-placeholder [] -1)
(retry [] 7)))
(defn main [] ()
(print (load 1))
(println ""))</code></pre>
<pre><code class="sh">$ flan run boom.flan
unhandled Missing
$ echo $?
134</code></pre>
</div>
<div class="panel" id="panel-ffi">
<h2 id="ffi">The FFI</h2>
<p>There are two declaration forms, and they are two forms rather than one because no
structural rule could tell them apart.</p>
<p><code>declare</code> names a C symbol in a signature Flan can already spell. Nothing
is generated; a Flan string crosses as ptr+len, exactly as it is stored.</p>
<pre><code>(declare cos-f64 [x f64] f64 "cos")
(defn main [] ()
(print (cos-f64 0.0)) (println ""))</code></pre>
<pre><code class="sh">1</code></pre>
<p>That is 1.0, printed by the same rule as before.</p>
<p><code>declare-c</code> names the C library's own function in the C library's own
signature, and the compiler writes the wrapper. This is what raylib's package is made
of — one line per binding:</p>
<pre><code>(declare-c unload-texture [texture Texture2D] "UnloadTexture")
(declare-c draw-texture
[texture Texture2D x i32 y i32 tint Color]
"DrawTexture")</code></pre>
<p><strong>An aggregate's calling convention is not part of its layout.</strong> On
x86-64, clang gives raylib's own prototypes
<code>&lt;2 x float&gt;</code> for a returned <code>Vector2</code>, <code>i32</code> for
a <code>Color</code> argument, and <code>{ i64, i64 }</code> for a returned
<code>Rectangle</code> — none of which is the struct's own LLVM type, and arm64 and
wasm32 classify differently again. Reproducing that in the backend would be three
classifiers to write and keep correct forever, and a mistake would show up as a field
full of garbage rather than as a link error.</p>
<p>So the boundary has one wrapper per binding, each flattening the aggregates: a struct
returns through an out-pointer, a struct argument is passed by pointer, and clang
classifies all of it, per target, for free. <code>flan shim &lt;file&gt;</code> prints
the whole generated file, of which this is the end — the rest is the typedefs
and a comment saying not to edit it:</p>
<pre><code>(defstruct Vector2 [x f32 y f32])
(declare-c get-mouse-position [] Vector2 "GetMousePosition")</code></pre>
<pre><code class="c">typedef struct flan_ty_Vector2_1bebc5ae_s flan_ty_Vector2_1bebc5ae;
struct flan_ty_Vector2_1bebc5ae_s { /* Vector2 */
float x;
float y;
};
/* get-mouse-position */
extern flan_ty_Vector2_1bebc5ae GetMousePosition(void);
void flan_shim_get_mouse_position_5ad0e205(flan_ty_Vector2_1bebc5ae *out) {
*out = GetMousePosition();
}</code></pre>
<p>No library header is read during a build, deliberately, so a build needs the shared
library to be linkable and not the <code>-devel</code> package to be installed.
<strong>Guaranteed:</strong> the C typedef
and the Flan struct come from the same <code>defstruct</code>, so they cannot disagree,
and clang type-checks the wrapper against the generated prototype.
<strong>Trusted:</strong> that the <code>defstruct</code> matches the library's real
struct, and that the <code>declare-c</code> signature is the function's real signature.
A scalar's width now carries ABI weight — <code>f64</code> where the library says
<code>float</code> emits <code>double</code>, and the library reads garbage. That
trusted half is what the generator below checks.</p>
<h3>Generated bindings, committed</h3>
<p>Writing a binding per function by hand does not scale past the ones a program
happens to call, so most of raylib's package is not written by hand. A package
directory may carry a <code>headers</code> file naming the library's own C header;
<code>flan generate-c &lt;package-dir&gt;</code> reads it with
<code>clang -Xclang -ast-dump=json</code>, turns every function it can represent into
the same <code>declare-c</code> line a person would have written, and writes them to
<code>generated.flan</code> in the package — which is <em>committed</em>.</p>
<pre><code class="sh">$ export FLAN_RAYLIB_H=/path/to/raylib-5.5/src/raylib.h
$ flan generate-c vendor/raylib
wrote vendor/raylib/generated.flan: 253 declarations, 156 refused, of 581 functions.
Every defstruct and every hand-written declare-c agrees with it.</code></pre>
<p>Committing the output rather than generating at build time is what keeps the
no-header property honest: the declarations are in the repository, so every build gets
all of them, they are greppable, and they show up in a diff when the library moves. The
argument is not caching — the clang dump is already cached on disk and in memory.</p>
<p><strong>Regeneration is the check.</strong> The cost of committing the output is that
nothing compares the bindings against reality on every build any more, so the one
function that writes the file compares first and <em>refuses to write</em> when the
package and the header disagree: every <code>defstruct</code> against the header's
record, and every hand-written <code>declare-c</code> against the header's signature.
Pointed at a raylib 5.1-dev header while the package is written for 5.5, it reports ten
real differences and writes nothing — which is exactly the silent version skew a
generated file would otherwise bake in and make look reviewed.</p>
<p>This is also why the hand-written bindings are kept rather than replaced by generated
ones. Everything the generator emits agrees with the header by construction, so diffing
generated output against the header it came from proves nothing; the hand-written lines
were transcribed by a person, so they are the only declarations a header can actually
contradict. All ten of those differences came from them.</p>
<p>A committed generated file cannot be hand-corrected — the next regeneration destroys
the edit without telling anybody — so the corrections live in a <code>bindings</code>
file beside <code>headers</code>, which is read <em>while</em> the declarations are made.
Two directives:</p>
<pre><code class="sh"># raylib's own malloc/realloc/free, which would be a second untracked heap
# behind three innocuous Flan names.
exclude Mem*
# The kebab rule gives is-window-ready. Lisp spells a predicate with a ?.
name IsWindowReady window-ready?</code></pre>
<p>An excluded function still says it was excluded rather than going quiet, and a name
override changes only the Flan face — the C symbol is kept verbatim in the declaration,
so it is still what is called and still what the check compares. Renaming is also the
way out of a collision: <code>Spin2D</code> and <code>spin2d</code> both kebab to
<code>spin-2d</code>, so neither takes the name, because which one won would otherwise
depend on the order the header happens to declare them in.</p>
<p>Anything neither directive can express is a hand-written <code>declare-c</code> in the
package's own source, which wins over the generated file and is left alone by the
generator. That is the escape hatch for a signature the importer gets wrong and for a
Flan face the header cannot describe — raylib keeps two, each a raw binding wrapped by a
Flan function of the same name, one taking a slice and one answering with an
<code>Option</code>.</p>
<p>Everything the boundary cannot represent is refused by name with the reason, rather
than half-supported: an <code>Option</code>, a union, a fixed array, a map, a returned
string, a callback, a slice parameter, an unknown type, an unrepresentable struct field,
and two Flan names for one C symbol. An aggregate in a plain <code>declare</code> is
refused too, so the narrow boundary cannot quietly acquire one.</p>
<p>One known edge: a REPL redefinition that introduces a <em>new</em>
<code>declare-c</code> cannot work, because the reload path compiles no C and the wrapper
would not exist in the running process. Editing the body of a function that calls an
existing binding is unaffected.</p>
</div>
<div class="panel" id="panel-devloop">
<h2 id="devloop">The dev loop</h2>
<p>Edit the code, keep the sand.</p>
<pre><code class="sh">$ flan dev sand.flan</code></pre>
<p>That builds the program, launches it, holds a session beside it, and listens on
<code>.flan-dev.sock</code> next to the source. From Emacs, <kbd>C-c C-c</kbd> on a
function recompiles it and installs it into the running process at that process's next
frame boundary. The window does not blink and the grid does not reset.</p>
<h3>How it works</h3>
<p>Four pieces, each runnable on its own.</p>
<p><strong>The reload primitive.</strong> <code>llc</code><code>ld -shared</code>
<code>dlopen</code> → call. Measured in this codebase:</p>
<div class="scroll">
<table>
<tr><th>Step</th><th>Cost</th></tr>
<tr><td>emitting the redefinition's IR</td><td>below the timer (&lt;0.1ms)</td></tr>
<tr><td><code>llc -O2 -filetype=obj</code></td><td>1517ms</td></tr>
<tr><td><code>ld -shared</code></td><td>3ms</td></tr>
<tr><td><code>dlopen</code> + <code>dlsym</code></td><td>0.04ms</td></tr>
</table>
</div>
<p>About 19ms end to end. The <code>clang</code> driver on the same IR is 50ms, so the
dev path never invokes it. The driver forks a second process and re-does argument and
target resolution; codegen is not the cost.</p>
<p><strong>Indirection cells.</strong> Loading a new body is not installing it. A call
bound at link time cannot be made to notice one, so a <code>--dev</code> build routes
every Flan-to-Flan call through a cell — a mutable global holding the address of the
function that is current.</p>
<p>Here is the <code>defer</code> example from <a href="#defer">above</a> — its
<code>main</code> calls <code>work</code> — through <code>flan emit --dev</code>:</p>
<pre><code class="llvm">@"flan.cell.work" = global ptr @"flan.work"
define {} @"flan.main"(ptr %xfer) {
entry:
%t7 = alloca %slice
%t1 = load ptr, ptr @"flan.cell.work"
%t2 = call i32 %t1(i32 3, ptr %xfer)</code></pre>
<p>The call site loads the cell rather than naming <code>@"flan.work"</code>
directly. The signature carries <code>ptr %xfer</code> — the transfer channel from
<a href="#conditions">conditions</a>, on every Flan function, release builds
included. <code>println</code> is not in there: it is not a Flan function, so there is
no call to route and no cell for it.</p>
<p>Redefinition is then one store, below a microsecond. Three rules follow. A
redefinition module declares every <em>global</em> external, so globals live in the host
and survive a reload. Every other function is a <code>declare</code>, so a redefined
<code>settle</code> calls the host's <code>move-grain</code> rather than freezing a
private copy of it. And nothing is ever <code>dlclose</code>d: a cell holds an address
inside a module's text, so unloading it would leave call sites pointing at unmapped
memory. Old code is never unloaded, so a thread mid-execution finishes safely in the old
version.</p>
<p><strong>The agent.</strong> <code>vendor/agent</code> is a package like any other: a
listener thread, a single-producer ring, and three calls. This is the whole of
<code>agent.flan</code> — no aggregate crosses the boundary, so a plain
<code>declare</code> does it and there is no shim.</p>
<pre><code>(declare start-raw [path string] i32 "flan_agent_start")
(declare poll-raw [] i32 "flan_agent_poll")
(declare wait-raw [ms i32] i32 "flan_agent_wait")
(defn start [path string] i32 (start-raw path))
(defn poll [] i32 (poll-raw))
(defn wait [ms i32] i32 (wait-raw ms))</code></pre>
<p><code>(agent/start path)</code> listens on a unix socket, once, at startup.
<code>(agent/poll)</code> installs whatever has arrived and returns how many.
<code>(agent/wait ms)</code> is the same but waits for something first. A headless test
uses it so that a reload is deterministic rather than a race against the frame rate.</p>
<p>Loading and installing are separate. <code>dlopen</code> relocates
a module and takes the loader lock — milliseconds, unbounded — so it happens on the
listener thread. Installing is one store per function and must not land while a redefined
function is on the stack, so it happens on the game thread, at the top of the frame, when
the program asks. A game loop calls <code>agent/poll</code> at the top of its frame and
ignores the result.</p>
<p><strong>The session and the daemon.</strong> <code>flan dev</code> holds the
declarations the running process was built from plus every change accepted since, and it
owns the build, so its rules describe the process that is actually running. Re-checking
the whole program on every evaluation costs under 10ms, less than the <code>llc</code>
that follows. An evaluation is transactional: a form that fails to check mutates
nothing.</p>
<p>The protocol is one s-expression per message, length-framed by a decimal byte count.
It is not nREPL: <code>eval</code> there is string-in/string-out with no slot for
<em>which form, from which file</em>, and once the editor client is ours too there is no
CIDER to be compatible with.</p>
<h3>What a running process cannot be told</h3>
<p>Some changes are refused with a reason rather than loaded, because the alternative is
a silent mismatch against memory the process has already laid out:</p>
<div class="scroll">
<table>
<tr><th>Change</th><th>What it would have broken</th></tr>
<tr><td>a function's signature</td><td>a cell is a bare pointer; every call site compiled before the change still passes the old arguments through it</td></tr>
<tr><td>a global's type</td><td>the storage exists and has a shape — reuse reads at the wrong offsets, replacement discards the state the reload exists to preserve</td></tr>
<tr><td>a struct's fields</td><td>the values the process is holding have the old layout</td></tr>
<tr><td>a <code>defconst</code> the checker consumed</td><td>it is in the shape of the program — <code>(defconst rows (/ h c))</code> decides <code>grid</code>'s type before anything else resolves</td></tr>
<tr><td>a <code>defenum</code> member</td><td><code>:space</code> is erased to an <code>i32</code> literal in the caller, so it is folded there too</td></tr>
</table>
</div>
<p>A <code>defvar</code>'s <em>initial value</em> is deliberately not on that list:
its storage holds state the program moved past long ago. And a <code>defconst</code>
the checker never consumed can be changed, so a colour table can be tuned live while an
array length stays refused. A dev build emits those as mutable globals, so LLVM cannot
fold a read of one.</p>
<p>The signature row is a stopgap. The design is versioned functions with their own
trampolines, so that new callers resolve the new version while existing ones keep the
old. None of the three parts exists yet, and the alternative to refusing is a silent
argument mismatch.</p>
<h3>Evaluating an expression</h3>
<p><kbd>C-x C-e</kbd> is a different primitive from redefining a name. There is no name
to install a body into, so the expression is wrapped in a thunk with nowhere to be called
from. The module says <em>run this once</em>, and the agent calls it after the install, on
the game thread, at a frame boundary. An expression reading the program's state therefore
sees a consistent one.</p>
<p>Nothing is marshalled back, because nothing could be: a Flan value carries no header,
so no code at run time can say what it is. The compiler knows the type and renders it
there, in the thunk. What comes back looks like this:</p>
<pre><code class="sh">big 18446744073709551615
col :blue
(.pos b) (V {:x 1.5 :y 0})
b (Blob {:id 7 :name "sandy \"quoted\"" :pos (V {:x 1.5 :y 0}) :tags [ 0 42 0]})
(slice (.tags b) 0 3) [ 0 42 0]
(rl/get-color 0x11223344) (rl/Color {:r 17 :g 34 :b 51 :a 68})
sim/grid [ [ 0 0 0 0 0 0 0 0 ...] [ 0 ... ] ...]</code></pre>
<p>A pointer is never followed; it renders as <code>&lt;ptr&gt;</code>. Following one
would make the walk cycle, and dereferencing a pointer a REPL was handed is not safe.
The walk is bounded at depth 4 and 8 elements, and the output truncates at 4K.
<code>Map</code>, function values and type variables refuse by name.</p>
<p>The thunk's module is unloaded afterwards. Nothing points into its text once it has
returned. Sixteen expression evaluations retain zero mappings, where each redefinition
retains three, permanently and correctly.</p>
<h2 id="emacs">Emacs</h2>
<p><code>emacs/flan-mode.el</code> derives from <code>prog-mode</code> with
<code>lisp-mode</code>'s syntax table, so sexp motion, paren matching and indentation are
already right. It adds Flan's brackets &mdash; <code>[</code> and <code>{</code> are brackets,
not symbol characters, since every binding list and every type is written with them &mdash; and
the characters a Flan name may contain. <code>emacs/flan-dev.el</code> is the client;
there is no parser in it.</p>
<h3>Setting up</h3>
<p>Put <code>emacs/</code> on your load path and require the mode. Nothing else is needed:
the client, the REPL, the inspector and the conditions buffer all load on first use.</p>
<pre><code class="sh">(add-to-list 'load-path "~/Development/flan/emacs")
(require 'flan-mode)
(require 'flan-dape) ; optional — lldb, and the only thing that binds C-c C-g</code></pre>
<p><code>flan-dape.el</code> is separate on purpose, so <code>flan-mode</code> works
without dape installed. You also need <code>flan</code> on your <code>PATH</code>, or
<code>flan-dev-command</code> pointed at it.</p>
<h3>Starting a program</h3>
<p><code>M-x flan-dev</code> runs <code>flan dev</code> on a file, waits for it to come up
and connects. <kbd>C-c C-z</kbd> attaches to one that is already running, looking for
<code>.flan-dev.sock</code> upward from the buffer, so from anywhere in the project it
finds the one program you have going. <kbd>C-c C-q</kbd> disconnects and leaves it
running; <code>M-x flan-dev-quit</code> stops it, but only one this Emacs started &mdash; a
daemon you launched in a terminal is not Emacs' to kill, and it says so rather than doing
something surprising.</p>
<div class="scroll">
<table>
<tr><th>Key</th><th>Does</th></tr>
<tr><td><kbd>C-c C-c</kbd></td><td>the top-level form at point, recompiled and installed</td></tr>
<tr><td><kbd>C-c C-k</kbd></td><td>the whole buffer, as <em>one</em> module</td></tr>
<tr><td><kbd>C-x C-e</kbd></td><td>the expression before point, evaluated in the running program</td></tr>
<tr><td><kbd>C-c C-z</kbd> / <kbd>C-c C-q</kbd></td><td>connect (finds <code>.flan-dev.sock</code> upward) / disconnect</td></tr>
<tr><td><kbd>C-c C-o</kbd></td><td>the running program's own output, in <code>*flan-output*</code></td></tr>
<tr><td><kbd>C-c C-r</kbd></td><td>a prompt on the running program (<code>*flan-repl*</code>)</td></tr>
<tr><td><kbd>C-c C-b</kbd></td><td>a stopped program: the condition, the restarts, the stack</td></tr>
<tr><td><kbd>C-c C-M-b</kbd></td><td>the same restarts, as a one-key prompt</td></tr>
<tr><td><kbd>C-c C-i</kbd></td><td>inspect a value, navigating into its fields</td></tr>
<tr><td><kbd>C-c C-a</kbd></td><td>disassemble a function; <kbd>C-u</kbd> first for its LLVM IR</td></tr>
<tr><td><kbd>C-c C-g</kbd></td><td>debug under lldb, through dape &mdash; bound only once <code>flan-dape.el</code> is loaded, so <code>flan-mode</code> works without dape installed</td></tr>
<tr><td><kbd>C-c C-d</kbd></td><td>what the running program currently defines</td></tr>
<tr><td><kbd>C-c C-v</kbd></td><td>help on the name at point</td></tr>
<tr><td><kbd>C-c C-x</kbd></td><td>rebuild, relaunch and reconnect</td></tr>
<tr><td><kbd>M-.</kbd> / <kbd>M-,</kbd></td><td>where a name is written, through an <code>xref</code> backend</td></tr>
</table>
</div>
<p><kbd>C-c C-k</kbd> sends one module rather than a form at a time on purpose: a
<code>defvar</code> and the function that uses it have to arrive in the same load, or the
first refers to storage that does not exist yet.</p>
<p><kbd>C-x C-e</kbd> compiles the expression before point and runs it <em>inside</em> the
running program &mdash; the actual process, with its actual state, not a copy. Both it and
<kbd>C-c C-c</kbd> work on buffer text rather than the saved file, and a change installs at
the next frame boundary.</p>
<h3>The buffers, and their own keys</h3>
<p>Three buffers have keymaps of their own. The conditions buffer
(<kbd>C-c C-b</kbd>) shows the condition, then the restarts, then the stack &mdash; in that
order, because the decision in front of you is which restart to take and the stack is only
the explanation for it.</p>
<div class="scroll">
<table>
<tr><th>Buffer</th><th>Keys</th></tr>
<tr><td>conditions</td>
<td><kbd>RET</kbd> take the restart at point &middot; <kbd>0</kbd>&ndash;<kbd>9</kbd> take one by number &middot;
<kbd>TAB</kbd>/<kbd>n</kbd>, <kbd>S-TAB</kbd>/<kbd>p</kbd> move &middot;
<kbd>f</kbd> fold a frame &middot; <kbd>i</kbd> inspect a local &middot;
<kbd>a</kbd> abort &middot; <kbd>g</kbd> re-read &middot; <kbd>q</kbd> close</td></tr>
<tr><td>inspector</td>
<td><kbd>RET</kbd> into the field at point &middot; <kbd>l</kbd> back out &middot;
<kbd>g</kbd> re-read &middot; <kbd>TAB</kbd>/<kbd>n</kbd>, <kbd>S-TAB</kbd>/<kbd>p</kbd> move &middot;
<kbd>q</kbd> close</td></tr>
<tr><td>repl</td>
<td><code>comint</code>, plus <kbd>C-c C-o</kbd>, <kbd>C-c C-d</kbd> and <kbd>C-c C-q</kbd></td></tr>
</table>
</div>
<p>A restart is taken by <em>position</em>, which is why the list is numbered: a name
resolves to the innermost frame offering it, so an outer <code>retry</code> shadowed by an
inner one is real, is on the list, and cannot be reached by name. One that genuinely cannot
be taken is drawn and refused with the reason rather than quietly omitted.</p>
<p>The inspector is unlike most: the view is never stale, because every step re-reads the
program as it is now. The cost is that <strong>the root expression runs again on every
step</strong> &mdash; going into a field sends <code>(.pos b)</code> where the last one sent
<code>b</code>, which is harmless, but inspecting <code>(spawn-enemy)</code> spawns one per
keystroke. That is why there is no auto-refresh and why <kbd>g</kbd> is a key you press.</p>
<p>The repl is program-scoped rather than buffer-scoped, so in sand you write
<code>sim/settle</code> and not <code>settle</code>. <code>*flan-output*</code> is separate
from it, because the program's stdout belongs to the program.</p>
<h3>When a change is refused</h3>
<p>Two different things wear the same refusal today, and only one of them is the design.</p>
<p><strong>A changed signature is a placeholder refusal.</strong> The intended behaviour,
and what <a href="#reading">plan.org</a> specifies, is that a signature change makes a new
version of the function: new callers resolve it, existing callers and any stored
<code>Fn</code> value stay safely on the old one, and the session <em>warns</em> at each
tracked stale caller site so you know what to re-evaluate. Nothing should have to restart.
That needs function versions, trampolines and caller tracking, none of which are built yet
&mdash; so until they are, the session refuses rather than letting a cell hand old arguments
to a new body. The refusal is a limitation with a date on it, not a rule.</p>
<p><strong>A changed struct layout is the genuinely hard case</strong>, and is rejected
while live values of that struct exist: storage already allocated has the old shape, and a
new body would read its fields at the wrong offsets with nothing to say so. plan.org keeps
this one as a rejection, and gives managed classes an explicit migration at a frame
boundary as the eventual way through.</p>
<p><kbd>C-c C-x</kbd> rebuilds, relaunches and reconnects, and is the way out while the
above is true. It costs the program's state, which is why it is a key you press rather than
something <kbd>C-c C-c</kbd> falls back to.</p>
<h3>Around the edges</h3>
<p>eldoc, completion and <kbd>M-.</kbd> all read one cached reply rather than asking per
keystroke, refreshed at the two moments the answer can have changed: on connect, and after
an evaluation the daemon accepted. The modeline says whether there is a program on the
other end, and says <code>stopped</code> when there is one sitting in the break loop &mdash; a
stopped program looks exactly like a running one from anywhere else in Emacs.</p>
<p>An error comes back with a location and the client draws an overlay there, cleared the
next time that buffer's evaluation is accepted. Under <kbd>C-c C-g</kbd> lldb needs no
plugin to read a Flan value, since a struct is its C struct; locals show under their real
names, and a shadowed one appears as <code>v~2</code> while plain <code>v</code> still
answers with the outer binding.</p>
<div class="scroll">
<table>
<tr><th>Setting</th><th>Default</th><th>What it is</th></tr>
<tr><td><code>flan-dev-command</code></td><td><code>"flan"</code></td><td>the compiler binary</td></tr>
<tr><td><code>flan-dev-socket-name</code></td><td><code>".flan-dev.sock"</code></td><td>what <kbd>C-c C-z</kbd> searches for</td></tr>
<tr><td><code>flan-dev-echo-result</code></td><td><code>t</code></td><td>print <kbd>C-x C-e</kbd>'s value in the echo area</td></tr>
<tr><td><code>flan-dev-names-shown</code></td><td><code>4</code></td><td>how many names to list before counting them</td></tr>
<tr><td><code>flan-dev-output-buffer</code></td><td><code>"*flan-output*"</code></td><td>where the program's output goes</td></tr>
<tr><td><code>flan-dev-poll-interval</code></td><td><code>1.0</code></td><td>seconds between checks for whether it stopped</td></tr>
<tr><td><code>flan-dev-start-timeout</code></td><td><code>60</code></td><td>seconds to wait for a program to come up</td></tr>
</table>
</div>
</div>
<div class="panel" id="panel-builds">
<h2 id="targets">Targets and builds</h2>
<p>Native x86-64 is the development target. <code>--target=wasm32-wasi</code> produces a
module, and the headless sand acceptance program prints the same 64-bit hash under it as
it does natively:</p>
<pre><code class="sh">$ flan run test/programs/sand-headless.flan
15595743031174623232
$ flan build test/programs/sand-headless.flan --target=wasm32-wasi -o sand.wasm
$ node --no-warnings test/wasm-run.mjs sand.wasm
15595743031174623232</code></pre>
<p>The RNG is written in Flan rather than called from libc for that number: a grid hash
is only a regression test if the sequence is byte-identical on both targets. It holds at
<code>-O2</code> and at <code>-O0</code>. The wasm side needs a
<code>wasm32</code> builtins archive — from wasi-sdk, or emscripten's substituting for
it — and the compiler names every path it looked in when it cannot find one.</p>
<p>Dev and release builds are deliberately different. <code>--dev</code> means
indirection cells and <code>-rdynamic</code>, which exports those cells for a loaded
module to bind to. Release builds call directly, emit constants as constants, and get all
the folding back. Dev builds are not pruned by reachability: what a REPL may redefine
next is not a function of what has been called so far.</p>
<p><code>--debug</code> is a third flag beside <code>--dev</code> and the optimisation
level. <code>--dev</code> asks whether you can redefine the program while it runs;
<code>--debug</code> asks whether you can stop it and read it. It emits DWARF, sets
<code>-O0</code>, and is refused by name for wasm32. lldb needs no plugin to read a
Flan struct: the struct is its C struct.</p>
<p>Some things are refused by name rather than half-supported, and both cross-target
refusals say why:</p>
<pre><code class="sh">$ flan run hello.flan --target=wasm32-wasi
flan run: --target is refused — a cross-built module is not something this host
can exec. Use flan build --target=... and a wasm runtime.
$ flan build hello.flan --dev --target=wasm32-wasi
wasm32: --dev is native only — the reload path is dlopen, which wasm32 has no
equivalent of</code></pre>
<p>Build time for <code>calc-me.flan</code> is about 110ms, of which the frontend — read,
parse, load, check, emit — is under 10ms. Every C translation unit goes through an object
cache keyed by a digest of the source text, the compiler, the optimisation level and the
target flags, so it never needs invalidating by hand.</p>
<h2 id="notyet">Not implemented yet</h2>
<p>The house rule is that anything which binds a name, alters control flow, or is not
yet implemented must be recognised explicitly and rejected. Each of these refuses by
name, with the milestone it belongs to, and the tests assert on the reason.</p>
<div class="scroll">
<table>
<tr><th>You write</th><th>The compiler says</th></tr>
<tr><td><code>(Vec T)</code></td><td>(Vec T) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(Map K V)</code></td><td>(Map K V) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(Result T E)</code></td><td>(Result T E) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(Handle T)</code></td><td>(Handle T) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(try …)</code></td><td>try (Result) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td>a union type</td><td>the union type Shape is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(Fn [T] R)</code></td><td>a function type is not implemented yet — milestone 5 (see plan.org)</td></tr>
<tr><td><code>(fn [x i32] …)</code></td><td>calling something other than a named function is not implemented yet — milestone 5 (see plan.org)</td></tr>
<tr><td>a type variable</td><td>generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)</td></tr>
<tr><td><code>'sym</code></td><td>a quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(defmacro …)</code></td><td>parses, but is not expanded: running a macro means compiling it and loading it into the compiler, which is not wired up yet</td></tr>
<tr><td><code>`(a ~b)</code></td><td>is read, but not expanded: macro expansion is not wired up yet</td></tr>
<tr><td><code>handler-case</code></td><td>handler-case is not implemented yet</td></tr>
<tr><td><code>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet</td></tr>
</table>
</div>
<p>Beyond that list, and just as true: there is no allocator and no
<code>context</code>; there is no <code>println</code> and no overloading; restarts take
no parameters; <code>match</code> works on an <code>Option</code> and nothing else;
<code>defer</code> is function-scoped; there is no package-private marker other than
<code>main</code> not being exported; there are no threads in the language; and the
managed <code>class</code> facility that plan.org describes is a plan and not a feature.</p>
<p>The vocabulary in <code>spec-memory.md</code> is normative but largely unbuilt:
<code>clone</code>, <code>as-slice</code>, <code>push</code>, <code>get</code>,
<code>put</code>, <code>resolve</code> and allocator-aware operations belong to
<code>Vec</code> and <code>Map</code>, and arrive with them.</p>
<p>Two of these are settled rather than pending. There is <strong>no interpreter</strong>
and there is not going to be one: the compiled path is the only backend. The instrumentation-based step debugger that wanted one is cut,
and compiled redefinition at ~19ms is perceptually instant for expression evaluation too.
And the <strong>macro expander</strong> is blocked on unions rather than on itself — a
macro is a function from <code>Form</code> to <code>Form</code>, which needs
<code>Form</code> to exist as a Flan union value first.</p>
<h2 id="reading">Further reading</h2>
<p>The repository's own documents, in the order they are worth reading:</p>
<ul>
<li><code>plan.org</code> — the design, the build sequence, and the open decisions.</li>
<li><code>NEXT.md</code> — the project's memory, and the authority on what is actually
built.</li>
<li><code>spec-memory.md</code> — ownership, containers, places, generics, function
values. Frozen.</li>
<li><code>spec-conditions.md</code> — conditions and restarts, operational semantics.
Frozen.</li>
<li><code>conditions.org</code> — a cheatsheet for driving conditions.</li>
<li><code>calc-me.flan</code>, <code>sand.flan</code>, <code>test/programs/</code>
real programs that run.</li>
</ul>
</div>
<footer>
<p>Flan is a custard. This page describes the compiler on branch
<code>dev-loop</code>; where a document and the compiler disagree, the compiler is what
is written here.</p>
</footer>
</div>
</main>
<script>
// Tabs. The panels are plain markup and stay visible until this runs, so the
// page without a script is the one long document it was before. A link into a
// panel that is not showing switches to it first -- every #anchor on the page,
// and every link from outside, has to keep working.
(function () {
var main = document.querySelector("main");
var bar = document.querySelector(".tabs");
if (!main || !bar) return;
var tabs = Array.prototype.slice.call(bar.querySelectorAll("button"));
var panels = tabs.map(function (t) { return document.getElementById(t.dataset.panel); });
if (panels.some(function (p) { return !p; })) return; // markup moved; leave it long
main.classList.add("tabbed");
function show(i, focus) {
tabs.forEach(function (t, j) {
t.setAttribute("aria-selected", j === i ? "true" : "false");
panels[j].hidden = j !== i;
});
if (focus) tabs[i].focus();
}
function panelOf(id) {
var el = id && document.getElementById(id);
while (el && el !== document.body) {
if (el.classList && el.classList.contains("panel")) return el.id;
el = el.parentElement;
}
return null;
}
function toHash() {
var want = panelOf(location.hash.slice(1));
if (!want) return false;
var i = panels.findIndex(function (p) { return p.id === want; });
if (i < 0) return false;
show(i);
var t = document.getElementById(location.hash.slice(1));
if (t) t.scrollIntoView();
return true;
}
tabs.forEach(function (t, i) {
t.addEventListener("click", function () { show(i); });
// Left and right move between tabs, which is what a tablist is expected
// to do once it says it is one.
t.addEventListener("keydown", function (e) {
var d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
if (!d) return;
e.preventDefault();
show((i + d + tabs.length) % tabs.length, true);
});
});
window.addEventListener("hashchange", toHash);
if (!toHash()) show(0);
})();
</script>
<script>
// A small hand-written highlighter for the Flan blocks. One pass, no library.
(function () {
var FORMS = new Set(("defn defstruct defenum defunion defconst defvar defalias " +
"declare declare-c import package let if when unless cond do and or not while " +
"until dotimes match set return some try defer signal error handler-bind " +
"restart-case invoke-restart fn quote defmacro gensym").split(" "));
// Capitalised names are types; these are the ones that are not capitalised.
var TYPES = new Set(("i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 bool string").split(" "));
var TOKEN = /(;[^\n]*)|("(?:\\.|[^"\\])*")|(\\[A-Za-z0-9]+)|(:[A-Za-z][\w?!*<>=+-]*)|(\b0[xX][0-9a-fA-F]+\b|\b\d[\d.]*\b)|([A-Za-z][\w*?!<>=./+-]*)/g;
function esc(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
document.querySelectorAll("pre > code").forEach(function (el) {
var lang = el.className;
if (lang && lang !== "flan") return; // sh, c and llvm blocks are left alone
el.innerHTML = esc(el.textContent).replace(TOKEN,
function (m, comment, str, chr, kw, num, word) {
if (comment) return '<span class="c">' + m + "</span>";
if (str) return '<span class="s">' + m + "</span>";
if (chr) return '<span class="s">' + m + "</span>";
if (kw) return '<span class="k">' + m + "</span>";
if (num) return '<span class="n">' + m + "</span>";
if (word) {
if (FORMS.has(word)) return '<span class="f">' + m + "</span>";
if (TYPES.has(word) || /^[A-Z]/.test(word))
return '<span class="t">' + m + "</span>";
}
return m;
});
});
})();
</script>
</body>
</html>