Merge branch 'web-prose' into dev-loop

This commit is contained in:
Joseph Ferano 2026-09-12 04:24:46 +07:00
commit 692403998b
2 changed files with 214 additions and 160 deletions

View File

@ -56,7 +56,8 @@ for pair in \
'fnty:(defn f [g (Fn [i32] i32)] i32 (g 1))' \
'quoted:(defn f [] i32 (quote a))' \
'deferblock:(defn f [] i32 (let [x 1] (defer (print-line "a")) x))' \
'i64index:(defconst xs [3 i32] [1 2 3]) (defn main [] i32 (let [i (i64 1)] (at xs i)))'
'i64index:(defconst xs [3 i32] [1 2 3]) (defn main [] i32 (let [i (i64 1)] (at xs i)))' \
'break:(defn main [] (let [i 0] (while (< i 3) (break))))'
do
name=${pair%%:*}; src=${pair#*:}
printf '%s\n' "$src" > "$here/.q.flan"

View File

@ -67,6 +67,7 @@ body {
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; }
@ -117,12 +118,63 @@ th { font-weight: 600; color: var(--muted); }
.lede { font-size: 1.1rem; }
.note { color: var(--muted); font-size: .94rem; }
nav.toc { background: var(--panel); border-radius: 5px; padding: 1rem 1.25rem; margin: 2rem 0 0; }
nav.toc ul { list-style: none; padding: 0; margin: 0; columns: 2; column-gap: 2rem; }
nav.toc li { margin: .18rem 0; font-size: .93rem; break-inside: avoid; }
/* 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; }
@media (max-width: 34rem) { nav.toc ul { columns: 1; } }
/* 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); }
@ -133,34 +185,11 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
</style>
</head>
<body>
<div class="wrap">
<header>
<svg class="mark" viewBox="0 0 210 44" role="img" aria-label="Flan">
<text x="0" y="33" font-family="Iowan Old Style, Palatino, Georgia, serif"
font-size="38" font-weight="600" letter-spacing="1.5"
fill="currentColor">flan</text>
<circle cx="128" cy="28" r="3.4" fill="var(--accent)"/>
</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. If the page and the compiler disagree, one of those two goes
red.</p>
<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>
<ul>
<li><a href="#what">What Flan is</a></li>
<li><a href="#start">Getting started</a></li>
@ -184,6 +213,34 @@ red.</p>
</ul>
</nav>
<main>
<div class="wrap">
<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>
<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.
@ -217,15 +274,14 @@ $ ./_build/default/bin/main.exe run calc-me.flan "1 + 2 * (3 - 0.5) / 2"
<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] [--target=wasm32-wasi]
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, which is how you find out what the compiler thinks of a form.
<code>run</code> builds to a temporary file and execs it.</p>
produced. <code>run</code> builds to a temporary file and execs it.</p>
<p>The smallest program:</p>
@ -259,8 +315,8 @@ slot; reading one is a load, assigning to one is a store, and a store of an aggr
borrow checker.</li>
</ul>
<p>That last point is what makes a heap unnecessary for a great deal of code: a value
struct is shared mutably by passing its address down the call chain.</p>
<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>
@ -291,10 +347,10 @@ wisp
5
1</code></pre>
<p><code>.field</code> and <code>at</code> dereference exactly one pointer level, which
is why <code>(set (.hp p) 8)</code> above is legal when <code>p</code> is a
<code>(Ptr Enemy)</code>. Note the last two stores: the whole-object store through
<code>p</code> overwrote <code>e</code> itself, so <code>hp</code> reads 3 and not 8.</p>
<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>
@ -317,9 +373,8 @@ $ echo $?
<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, which is what lets the acceptance table run the same
programs at <code>-O0</code> and <code>-O2</code> with identical checks;
<code>--no-bounds-checks</code> turns them off. Measured cost on a
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>
@ -364,19 +419,18 @@ have one type, and every conversion is written as a cast:</p>
<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 worth knowing before it surprises you: a
whole-numbered float prints without its fraction, so <code>3.0</code> comes out as
<code>3</code>.</p>
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, which
is what the hardware does anyway. <code>&gt;&gt;</code> is arithmetic on a signed type
and logical on an unsigned one.</p>
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, and the message is worth reading because it is the shape of most of them:</p>
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
@ -414,10 +468,9 @@ byte is a <code>u8</code> — but there is a byte literal, so <code>\h</code> is
<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 with <code>print-bytes</code>.</p>
<p>An enum is an <code>i32</code> at run time and its own type in the checker. That is
what makes a keyword at a call site useful: <code>:space</code> resolves against the
parameter's enum type at compile time, and a typo is an error there rather than a wrong
number later.</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])
@ -473,8 +526,8 @@ functions need no forward declaration. Globals come in two kinds:</p>
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, which
matters for reloading; see <a href="#devloop">the dev loop</a>.</p>
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>
@ -528,18 +581,20 @@ a body that changes it cannot change the trip count, and the loop variable is no
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>, and there is no
<code>break</code> or <code>continue</code> either — both are planned and neither
exists, so today they report as <code>unknown function break</code>. An early exit out
of a loop is <code>return</code>, as <code>first-even</code> does above.</p>
<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, which is what keeps a recursive descent
parser readable.</p>
<code>None</code> from the enclosing function.</p>
<pre><code>(defconst nums [4 i32] [4 8 15 16])
@ -582,8 +637,7 @@ 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, rather than accepted with surprising scope. Block
scoping it is real work and is not done:</p>
<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>
@ -641,33 +695,33 @@ ordinary Flan.</p>
<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>Two deliberate choices in there. <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. And <strong>the parsers are ours
too</strong>: <code>strtoll</code> answers 0 for <code>""</code>, 0 for
<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> goes the other way, and is the one function in the file that
is not Flan: <code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code>. Every other number
<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 — the very property that keeps the RNG in Flan is, for this one, the argument
for going out to C. It is also why every link carries <code>-lm</code>.</p>
<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. The names are the compiler's answer too: <code>(println 1)</code> is
<code>unknown function println</code>.</p>
its type. <code>(println 1)</code> is <code>unknown function println</code>.</p>
<p>The primitives underneath are few by design, because a primitive is the only thing
that gets implemented twice per backend: <code>argv</code>,
<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>,
@ -716,7 +770,7 @@ the importing file until a directory of that name is found.</p>
literal, in an array length — is rewritten to match. Nothing downstream knows a package
existed.</p>
<p>Three more rules that are easier to know than to discover:</p>
<p>Three more rules:</p>
<ul>
<li><strong>A package may be a single <code>.flan</code> file</strong> named outright,
@ -724,8 +778,8 @@ existed.</p>
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, which is also
what ends a cycle. The same directory under two different aliases is refused.</li>
<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
@ -740,7 +794,7 @@ 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. That is what lets one file import raylib and still build for wasm32.</p>
argument, so a file that imports raylib still builds for wasm32.</p>
<h2 id="conditions">Conditions and restarts</h2>
@ -759,8 +813,8 @@ carries on.</p>
(invoke-restart 'name) ; Never. Innermost frame offering the name wins.</code></pre>
<p><code>signal</code> has type <code>Unit</code>, always. That is the accumulation
case, and it is worth having on its own because it alters no control flow:</p>
<p><code>signal</code> has type <code>Unit</code>, always. A handler that returns
normally leaves the signaller to carry on — the accumulation case:</p>
<pre><code>(defstruct AssetMissing [id i32])
@ -818,10 +872,10 @@ first, before the clause body starts.</p>
<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. That is what makes "restarts go at the resync point"
composable.</p>
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 it is lowered, and why that matters</h3>
<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
@ -829,12 +883,11 @@ and every call site checks. A callee writes the target into its caller's slot; e
frame checks, runs its defers and returns early. The disassembly is the release one plus
a guard after each call.</p>
<p>Three consequences to know:</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, which is the property the acceptance table exists to
check.</li>
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
@ -875,11 +928,9 @@ and the top are still live:</p>
restart: retry
restart: use-placeholder</code></pre>
<p>From there you fix the function, install it, and take a restart — and because control
never left the erring frame, <code>retry</code> calls through the indirection cell and
reaches the new body. Installing while stopped is allowed: the rule against swapping a
function that is on the stack is about mid-frame consistency, and there is no frame in
progress here.</p>
<p>From there you fix the function, install it, and take a restart. 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
@ -930,8 +981,8 @@ of — one line per binding:</p>
[texture Texture2D x i32 y i32 tint Color]
"DrawTexture")</code></pre>
<p>The reason for the wrapper is that <strong>an aggregate's calling convention is not
part of its layout</strong>. On x86-64, clang gives raylib's own prototypes
<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
@ -963,8 +1014,8 @@ void flan_shim_get_mouse_position_5ad0e205(flan_ty_Vector2_1bebc5ae *out) {
}</code></pre>
<p>No library header is read, deliberately, so a build needs the shared library to be
linkable and not the <code>-devel</code> package to be installed. What follows from that
is what the generator can and cannot promise. <strong>Guaranteed:</strong> the C typedef
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
@ -985,7 +1036,7 @@ existing binding is unaffected.</p>
<h2 id="devloop">The dev loop</h2>
<p>This is the thesis of the project: edit the code, keep the sand.</p>
<p>Edit the code, keep the sand.</p>
<pre><code class="sh">$ flan dev sand.flan</code></pre>
@ -996,7 +1047,7 @@ frame boundary. The window does not blink and the grid does not reset.</p>
<h3>How it works</h3>
<p>Four pieces, each of which can be run on its own.</p>
<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>
@ -1011,9 +1062,9 @@ frame boundary. The window does not blink and the grid does not reset.</p>
</table>
</div>
<p>About 19ms end to end. The <code>clang</code> driver on the same IR is 50ms, which is
why the dev path never invokes it: the driver forks a second process and re-does
argument and target resolution, and codegen is not the cost.</p>
<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
@ -1021,7 +1072,7 @@ every Flan-to-Flan call through a cell — a mutable global holding the address
function that is current.</p>
<p>Here is the whole of <code>hello.flan</code> through
<code>flan emit --dev</code>, which is the shortest thing that shows it:</p>
<code>flan emit --dev</code>:</p>
<pre><code class="llvm">@"flan.cell.print-line" = global ptr @"flan.print-line"
@ -1030,20 +1081,19 @@ entry:
%t1 = load ptr, ptr @"flan.cell.print-line"
%t2 = call {} %t1(%slice { ptr @".str.36", i64 15 }, ptr %xfer)</code></pre>
<p>Two things are visible there at once. The call site loads the cell rather than naming
<code>@"flan.print-line"</code> directly, and the signature carries <code>ptr %xfer</code>
— the transfer channel from <a href="#conditions">conditions</a>, which every Flan
function has, release builds included.</p>
<p>The call site loads the cell rather than naming <code>@"flan.print-line"</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.</p>
<p>Redefinition is then one store, below a microsecond, which is what makes a
frame-boundary swap a non-event. Three rules fall out of it. A redefinition module
declares every <em>global</em> external, so globals live in the host and survive a reload
— that is what "keep the sand" means. 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, which is also why a thread mid-execution
finishes safely in the old version.</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
@ -1060,11 +1110,10 @@ listener thread, a single-producer ring, and three calls. This is the whole of
<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, which is what a
headless test uses so that a reload is deterministic rather than a race against the
frame rate.</p>
<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>The split between loading and installing is the design. <code>dlopen</code> relocates
<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
@ -1073,10 +1122,10 @@ 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 — which is what makes its rules describe the process that is actually
running rather than a guess about it. Re-checking the whole program on every evaluation
costs under 10ms, less than the <code>llc</code> that follows, and it makes an evaluation
transactional for free: a form that fails to check mutates nothing.</p>
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
@ -1100,23 +1149,23 @@ a silent mismatch against memory the process has already laid out:</p>
</div>
<p>A <code>defvar</code>'s <em>initial value</em> is deliberately not on that list:
refusing to change it would be refusing the whole point. And a <code>defconst</code> the
checker never consumed can be changed, which is how a colour table gets tuned live while
an array length stays refused — a dev build emits those as mutable globals so LLVM cannot
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 and the message should not be read as the final answer.
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 not the new design, it is a silent argument mismatch.</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 — so an expression reading the program's state sees
a point the program agrees is consistent.</p>
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
@ -1130,15 +1179,14 @@ b (Blob {:id 7 :name "sandy \"quoted\"" :pos (V {:x 1.5
(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> — because it is
the only thing that could make the walk cycle, and dereferencing one a REPL was handed is
not a safe thing to do on someone's behalf. 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>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, which is the one case where that is safe:
nothing points into its text once it has returned. Sixteen expression evaluations retain
zero mappings, where each redefinition retains three, permanently and correctly.</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>
@ -1147,7 +1195,7 @@ zero mappings, where each redefinition retains three, permanently and correctly.
already right. It adds Flan's brackets — <code>[</code> and <code>{</code> are brackets,
not symbol characters, since every binding list and every type is written with them — and
the characters a Flan name may contain. <code>emacs/flan-dev.el</code> is the client;
there is no parser in it, which is the point of the protocol choice.</p>
there is no parser in it.</p>
<div class="scroll">
<table>
@ -1189,22 +1237,28 @@ module, and the headless sand acceptance program prints the same 64-bit hash und
it does natively:</p>
<pre><code class="sh">$ flan run test/programs/sand-headless.flan
2256461126764447066
-2851001042534928384
$ flan build test/programs/sand-headless.flan --target=wasm32-wasi -o sand.wasm
$ node --no-warnings test/wasm-run.mjs sand.wasm
2256461126764447066</code></pre>
-2851001042534928384</code></pre>
<p>That number is the whole point of writing the RNG in Flan rather than calling libc's:
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
<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 is what exports the 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, because what a REPL may
redefine next is not a function of what has been called so far.</p>
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>
@ -1224,10 +1278,9 @@ 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. So these are not missing features
you discover as a strange type error — each refuses by name, with the milestone it belongs
to, and the tests assert on the reason.</p>
<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>
@ -1261,9 +1314,8 @@ managed <code>class</code> facility that plan.org describes is a plan and not a
<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 notes on what <em>is</em> settled, because their absence reads like an oversight.
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,
<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
@ -1293,6 +1345,7 @@ macro is a function from <code>Form</code> to <code>Form</code>, which needs
</footer>
</div>
</main>
<script>
// A small hand-written highlighter for the Flan blocks. One pass, no library.