Nineteen entries at the top of a long page are scrolled past once and then unreachable. A fixed column stays put and scrolls on its own. Below 66rem there is not room for two columns, so it collapses to a bar with a toggle — a checkbox, so the page still needs no script to navigate. The wordmark's dot was a circle at a guessed x, which drifted from the n whenever the reader's serif was not the one it was measured against. It is a full stop in the same text run now.
1402 lines
63 KiB
HTML
1402 lines
63 KiB
HTML
<!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; }
|
||
|
||
.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>
|
||
<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="#prelude">The prelude</a></li>
|
||
<li><a href="#packages">Packages</a></li>
|
||
<li><a href="#conditions">Conditions and restarts</a></li>
|
||
<li><a href="#break">The break loop</a></li>
|
||
<li><a href="#ffi">The FFI</a></li>
|
||
<li><a href="#devloop">The dev loop</a></li>
|
||
<li><a href="#emacs">Emacs</a></li>
|
||
<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">
|
||
|
||
<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. If the page and the compiler disagree, one of those two goes
|
||
red.</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.
|
||
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) <file.flan>...
|
||
flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] [--debug] [--target=wasm32-wasi]
|
||
flan run <file.flan> [args...]
|
||
flan reload <program.flan> <forms.flan> [-o out.so]
|
||
flan dev <program.flan> [-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>
|
||
|
||
<p>The smallest program:</p>
|
||
|
||
<pre><code>(defn main []
|
||
(print-line "hello from flan"))</code></pre>
|
||
|
||
<p>The entry point is <code>(defn main [args [string]] i32)</code>. Both the parameter
|
||
and the return type are optional: omitting <code>args</code> means the program ignores
|
||
argv, and omitting the return type means <code>Unit</code> and an exit status of 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>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>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-i64 (i64 (.hp e))) (newline)
|
||
(print-line (.name e))
|
||
(print-i64 (i64 (at room 2))) (newline)
|
||
(print-i64 (i64 spawned)) (newline)))</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, 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>
|
||
|
||
<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]
|
||
(print-line "before")
|
||
(print-i64 (i64 (at xs i)))
|
||
(print-line "unreachable")))</code></pre>
|
||
|
||
<pre><code class="sh">$ flan run bounds.flan
|
||
before
|
||
bounds.flan:9:28: 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, 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
|
||
50-million-iteration dependency chain over a 1024-element array: 0.11–0.12s checked
|
||
against 0.12–0.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>Unit</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-i64 (+ big 2)) (newline)
|
||
(print-f64 (* x 2.5)) (newline)
|
||
(print-i64 (i64 (bit-xor (<< 1 8) 255))) (newline)
|
||
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 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>
|
||
|
||
<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>>></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>
|
||
|
||
<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 (< (.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-i64 (i64 (peek (addr c)))) (newline)
|
||
(advance (addr c))
|
||
(print-i64 (i64 (peek (addr c)))) (newline)))</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 (>= b \0) (<= 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>
|
||
|
||
<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.
|
||
(print-line (key-name :space))
|
||
(print-line (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>. An omitted
|
||
return type means <code>Unit</code>. 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>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-i64 (i64 cell-size)) (newline)
|
||
(print-f64 (f64 gravity)) (newline)
|
||
(print-i64 (i64 current-color)) (newline)
|
||
(print-i64 (i64 (at grid 2 3))) (newline))</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, which
|
||
matters for reloading; 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
|
||
(< n 0) "negative"
|
||
(= n 0) "zero"
|
||
:else "positive"))
|
||
|
||
(defn countdown [n i32]
|
||
(let [i n]
|
||
(while (> i 0)
|
||
(print-i64 (i64 i))
|
||
(print-str " ")
|
||
(set i (- i 1)))
|
||
(newline)))
|
||
|
||
(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 []
|
||
(print-line (classify -3))
|
||
(countdown 4)
|
||
(unless false
|
||
(print-line "unless runs when the test is false"))
|
||
(match (first-even (slice nums 0 (len nums)))
|
||
(Some n) (do (print-i64 (i64 n)) (newline))
|
||
None (print-line "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, which is what keeps a recursive descent
|
||
parser readable.</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-i64 (i64 i)) (newline)) ; 4
|
||
None (print-line "not found"))
|
||
(match (index-of-i32 (slice nums 0 4) 99)
|
||
(Some i) (do (print-i64 (i64 i)) (newline))
|
||
None (print-line "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 (print-line "second"))
|
||
(defer (print-line "first")) ; innermost-first at exit
|
||
(when (< n 0)
|
||
(return 0)) ; runs both defers above it
|
||
(print-line "body")
|
||
n)
|
||
|
||
(defn main []
|
||
(print-i64 (i64 (work 3)))
|
||
(newline))</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, rather than accepted with surprising scope. Block
|
||
scoping it is real work and 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> and <code>nth</code> are the same operation and take any number of
|
||
indices, so <code>(at grid r c)</code> indexes a two-dimensional fixed array directly.
|
||
<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-i64 (i64 (at grid 1 2))) (newline) ; 7
|
||
(print-i64 (i64 (len palette))) (newline) ; 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-i64 (i64 (at grid 1 0))) (newline) ; 5 — the same storage
|
||
(print-i64 (sum-i32 row)) (newline)) ; 12
|
||
|
||
;; (zeroed) is a memset, not an allocation.
|
||
(set grid (zeroed))
|
||
(print-i64 (i64 (at grid 1 2))) (newline)) ; 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="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. Printing is deliberately not a primitive:
|
||
<code>write-stdout</code> is the one output primitive and everything above it is
|
||
ordinary Flan.</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>Group</th><th>Names</th></tr>
|
||
<tr><td>output</td><td><code>print-str</code>, <code>print-bytes</code>, <code>print-i64</code>, <code>print-f64</code>, <code>print-line</code>, <code>newline</code></td></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>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
|
||
<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
|
||
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>
|
||
|
||
<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>
|
||
|
||
<p>The primitives underneath are few by design, because a primitive is the only thing
|
||
that gets 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->f64</code>,
|
||
<code>bytes->i64</code>, <code>f64->bytes</code>, <code>i64->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-f64 (f64 (g/length v)))
|
||
(newline)))</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 that are easier to know than to discover:</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, which is also
|
||
what ends a cycle. 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. That is what lets one file import raylib and still build for wasm32.</p>
|
||
|
||
<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) ; Unit. Handler returns -> carry on. No handler -> 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>Unit</code>, always. That is the accumulation
|
||
case, and it is worth having on its own because it alters no control flow:</p>
|
||
|
||
<pre><code>(defstruct AssetMissing [id i32])
|
||
|
||
(defvar seen i64)
|
||
|
||
(defn load-all []
|
||
(signal (AssetMissing {:id 1})) ; Unit — the caller carries on
|
||
(signal (AssetMissing {:id 2})))
|
||
|
||
(defn main []
|
||
(load-all) ; no handler: a no-op
|
||
(print-i64 seen) (newline) ; 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-i64 seen) (newline)) ; 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-i64 (i64 (fetch 1))) (newline) ; 101 — nothing handled it
|
||
|
||
(handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
|
||
(print-i64 (i64 (fetch 2))) (newline)) ; -1
|
||
|
||
(print-i64 cleanups) (newline)) ; 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. That is what makes "restarts go at the resync point"
|
||
composable.</p>
|
||
|
||
<h3>How it is lowered, and why that matters</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 to know:</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>
|
||
<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 re-runs whatever sits between it and the target.</strong> Control
|
||
resumes at the <code>restart-case</code>, so a <code>retry</code> repeats the side
|
||
effects after it. Put the <code>restart-case</code> where re-entry is safe.</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.
|
||
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>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-i64 (i64 (load 1)))
|
||
(newline))</code></pre>
|
||
|
||
<pre><code class="sh">$ flan run boom.flan
|
||
unhandled Missing
|
||
$ echo $?
|
||
134</code></pre>
|
||
|
||
<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-f64 (cos-f64 0.0)) (newline))</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>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
|
||
<code><2 x float></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 <file></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, 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
|
||
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.</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>
|
||
|
||
<h2 id="devloop">The dev loop</h2>
|
||
|
||
<p>This is the thesis of the project: 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 of which can be run 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 (<0.1ms)</td></tr>
|
||
<tr><td><code>llc -O2 -filetype=obj</code></td><td>15–17ms</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, 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><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 whole of <code>hello.flan</code> through
|
||
<code>flan emit --dev</code>, which is the shortest thing that shows it:</p>
|
||
|
||
<pre><code class="llvm">@"flan.cell.print-line" = global ptr @"flan.print-line"
|
||
|
||
define {} @"flan.main"(ptr %xfer) {
|
||
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>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><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, which is what a
|
||
headless test uses 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
|
||
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 — 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>
|
||
|
||
<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:
|
||
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
|
||
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>
|
||
|
||
<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>
|
||
|
||
<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><ptr></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>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>
|
||
|
||
<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 — <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>
|
||
|
||
<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>what a <em>stopped</em> program is offering, and which to take</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 — the way out when a reload is refused</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>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 — 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. The <code>repl</code> buffer is
|
||
<code>comint</code>-derived and every line goes through the same request <kbd>C-x C-e</kbd>
|
||
uses; it is program-scoped, so in sand you write <code>sim/settle</code> and not
|
||
<code>settle</code>.</p>
|
||
|
||
<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
|
||
-2851001042534928384
|
||
$ flan build test/programs/sand-headless.flan --target=wasm32-wasi -o sand.wasm
|
||
$ node --no-warnings test/wasm-run.mjs sand.wasm
|
||
-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
|
||
<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>
|
||
|
||
<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. 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>
|
||
|
||
<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 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,
|
||
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>
|
||
|
||
<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>
|
||
// 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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
}
|
||
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>
|