The renderer check greps a line out of NEXT.md, and NEXT.md is a scratch document
that gets rewritten. The line went, the grep went empty, and the empty-needle guard
did its job and said so -- into a report nobody was reading. Behind that noise the
page was genuinely wrong: the colon-to-dot sweep rewrote every field label in the
corpus and lib/render.ml writes .field today, so the inspector block on the page had
been showing {:x 1.5 :y 0} for a renderer that prints {.x 1.5 .y 0}. test_repl.ml's
fixtures are the authority and they are dots throughout, with an enum member still a
colon; the page now matches. The anchor moves to test/programs/raylib-imported.flan,
which dune test builds and runs, so it cannot quietly stop saying it.
The LLVM excerpt beside it moved too: a --dev main pushes a condition frame before
anything else now, which shifted the SSA numbering by one. The three quoted lines are
what flan emit --dev prints today, with the frame push marked as elided rather than
silently dropped.
2100 lines
100 KiB
HTML
2100 lines
100 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; }
|
||
|
||
/* Tabs. The panels are visible by default and only become switchable once
|
||
the script adds .tabbed to <main> — with no script the page is the one long
|
||
document it has always been, which is also what a printer and a text browser
|
||
get. */
|
||
.tabs {
|
||
display: flex; flex-wrap: wrap; gap: .4rem;
|
||
margin: 0 0 2rem; padding: 0 0 .6rem;
|
||
border-bottom: 1px solid var(--rule);
|
||
}
|
||
.tabs button {
|
||
font: inherit; font-size: .93rem; cursor: pointer;
|
||
background: none; border: 1px solid transparent; border-radius: 4px;
|
||
padding: .3rem .7rem; color: var(--muted);
|
||
}
|
||
.tabs button:hover { color: var(--link); }
|
||
.tabs button[aria-selected="true"] {
|
||
color: var(--text); background: var(--panel); border-color: var(--rule);
|
||
}
|
||
.tabs button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||
main:not(.tabbed) .tabs { display: none; }
|
||
main.tabbed .panel[hidden] { display: none; }
|
||
|
||
.toc-group {
|
||
margin: 1.1rem 0 .3rem; font-size: .72rem; letter-spacing: .09em;
|
||
text-transform: uppercase; color: var(--muted); opacity: .75;
|
||
}
|
||
.toc-group:first-child { margin-top: 0; }
|
||
|
||
.lede { font-size: 1.1rem; }
|
||
.note { color: var(--muted); font-size: .94rem; }
|
||
|
||
/* The table of contents: a bar with a toggle when the window is narrow, a
|
||
fixed column beside the text when there is room for one. The toggle is a
|
||
checkbox so that no script is needed to open it. */
|
||
.toc-toggle { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||
.toc-button {
|
||
display: block; cursor: pointer; user-select: none;
|
||
position: sticky; top: 0; z-index: 2;
|
||
padding: .6rem 1.25rem;
|
||
background: var(--panel); border-bottom: 1px solid var(--rule);
|
||
color: var(--muted); font-size: .92rem; letter-spacing: .02em;
|
||
}
|
||
.toc-button::before { content: "\2261\00a0\00a0"; }
|
||
.toc-toggle:focus-visible + .toc-button { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||
nav.toc {
|
||
display: none;
|
||
background: var(--panel); border-bottom: 1px solid var(--rule);
|
||
padding: .75rem 1.25rem 1.1rem;
|
||
}
|
||
.toc-toggle:checked ~ nav.toc { display: block; }
|
||
.toc-title {
|
||
display: none; /* the toggle already says "Contents" */
|
||
margin: 0 0 .5rem; font-size: .78rem; letter-spacing: .09em;
|
||
text-transform: uppercase; color: var(--muted);
|
||
}
|
||
nav.toc ul { list-style: none; padding: 0; margin: 0; }
|
||
nav.toc li { margin: 0; font-size: .93rem; }
|
||
nav.toc a {
|
||
display: block; padding: .2rem 0; color: var(--muted);
|
||
text-decoration: none; border-left: 2px solid transparent; padding-left: .6rem;
|
||
}
|
||
nav.toc a:hover { color: var(--link); border-left-color: var(--accent); }
|
||
h2, h3 { scroll-margin-top: 3.5rem; }
|
||
|
||
/* Nineteen entries one to a line fills a phone screen, so the opened panel
|
||
goes to two columns as soon as there is room for them. */
|
||
@media (min-width: 30rem) and (max-width: 65.99rem) {
|
||
nav.toc ul { columns: 2; column-gap: 1.5rem; }
|
||
nav.toc li { break-inside: avoid; }
|
||
}
|
||
footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--rule); color: var(--muted); font-size: .92rem; }
|
||
|
||
/* Wide enough for both: the contents become a column of their own, scrolling
|
||
independently of the page, and the text moves over to sit beside it. */
|
||
@media (min-width: 66rem) {
|
||
.toc-button { display: none; }
|
||
nav.toc {
|
||
display: block;
|
||
position: fixed; top: 0; left: 0; bottom: 0;
|
||
width: 15rem; overflow-y: auto; overscroll-behavior: contain;
|
||
background: none; border-bottom: 0; border-right: 1px solid var(--rule);
|
||
padding: 3.6rem 1rem 2rem 1.5rem;
|
||
}
|
||
main { margin-left: 15rem; }
|
||
.toc-title { display: block; }
|
||
h2, h3 { scroll-margin-top: 1rem; }
|
||
}
|
||
|
||
.c { color: var(--c-comment); font-style: italic; }
|
||
.s { color: var(--c-string); }
|
||
.f { color: var(--c-form); font-weight: 600; }
|
||
.n { color: var(--c-num); }
|
||
.t { color: var(--c-type); }
|
||
.k { color: var(--c-kw); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<input type="checkbox" id="toc-toggle" class="toc-toggle">
|
||
<label class="toc-button" for="toc-toggle">Contents</label>
|
||
|
||
<nav class="toc" aria-label="Contents">
|
||
<p class="toc-title">Contents</p>
|
||
<p class="toc-group">Language</p>
|
||
<ul>
|
||
<li><a href="#what">What Flan is</a></li>
|
||
<li><a href="#start">Getting started</a></li>
|
||
<li><a href="#values">Values and memory</a></li>
|
||
<li><a href="#types">Types</a></li>
|
||
<li><a href="#structs">Structs and enums</a></li>
|
||
<li><a href="#functions">Functions</a></li>
|
||
<li><a href="#control">Control flow</a></li>
|
||
<li><a href="#defer">defer</a></li>
|
||
<li><a href="#arrays">Arrays and slices</a></li>
|
||
<li><a href="#generics">Generics</a></li>
|
||
<li><a href="#printing">Printing</a></li>
|
||
<li><a href="#prelude">The prelude</a></li>
|
||
<li><a href="#packages">Packages</a></li>
|
||
</ul>
|
||
<p class="toc-group">Conditions</p>
|
||
<ul>
|
||
<li><a href="#conditions">Conditions and restarts</a></li>
|
||
<li><a href="#break">The break loop</a></li>
|
||
</ul>
|
||
<p class="toc-group">The FFI</p>
|
||
<ul>
|
||
<li><a href="#ffi">The FFI</a></li>
|
||
</ul>
|
||
<p class="toc-group">The dev loop</p>
|
||
<ul>
|
||
<li><a href="#devloop">The dev loop</a></li>
|
||
<li><a href="#emacs">Emacs</a></li>
|
||
</ul>
|
||
<p class="toc-group">Builds</p>
|
||
<ul>
|
||
<li><a href="#targets">Targets and builds</a></li>
|
||
<li><a href="#notyet">Not implemented yet</a></li>
|
||
<li><a href="#reading">Further reading</a></li>
|
||
</ul>
|
||
</nav>
|
||
|
||
<main>
|
||
<div class="wrap">
|
||
|
||
<div class="tabs" role="tablist" aria-label="Sections">
|
||
<button type="button" role="tab" data-panel="panel-language" aria-selected="true">Language</button>
|
||
<button type="button" role="tab" data-panel="panel-conditions" aria-selected="false">Conditions</button>
|
||
<button type="button" role="tab" data-panel="panel-ffi" aria-selected="false">The FFI</button>
|
||
<button type="button" role="tab" data-panel="panel-devloop" aria-selected="false">The dev loop</button>
|
||
<button type="button" role="tab" data-panel="panel-builds" aria-selected="false">Builds</button>
|
||
</div>
|
||
|
||
<header>
|
||
<svg class="mark" viewBox="0 0 118 44" role="img" aria-label="Flan">
|
||
<!-- The dot is a full stop in the same run, so it stays beside the n
|
||
whichever serif the reader actually has. -->
|
||
<text x="0" y="33" font-family="Iowan Old Style, Palatino, Georgia, serif"
|
||
font-size="38" font-weight="600" letter-spacing="1"
|
||
fill="currentColor">flan<tspan fill="var(--accent)">.</tspan></text>
|
||
</svg>
|
||
<p class="tagline">A statically typed Lisp for game development. Clojure's brackets,
|
||
C's memory and value model, no garbage collector.</p>
|
||
</header>
|
||
|
||
<p class="lede">Flan compiles s-expressions to LLVM IR and then to a native binary.
|
||
There are no object headers, so a Flan struct is exactly its C struct. There is no
|
||
collector, so nothing runs between frames that you did not write. And a running
|
||
program can be edited: a function recompiled in Emacs is installed into the live
|
||
process at its next frame boundary, in about twenty milliseconds.</p>
|
||
|
||
<p>This page describes the compiler <em>as it is</em>, not as it is planned. Where
|
||
something is designed but not built, it says so and gives the message the compiler
|
||
prints for it. Every Flan program on this page is a file in <code>web/examples/</code>
|
||
with its output recorded beside it; <code>sh web/examples/check.sh</code> runs them all
|
||
and compares, and <code>quotes.sh</code> re-derives the blocks that are transcripts
|
||
rather than programs.</p>
|
||
|
||
<div class="panel" id="panel-language">
|
||
|
||
<h2 id="what">What Flan is</h2>
|
||
|
||
<p>A minimal Lisp for games. In one line: Odin with a Lisp frontend and a live REPL.
|
||
Types are mandatory and inference makes them feel optional; memory is manual; the
|
||
frontend is OCaml, and the default backend writes LLVM IR as text and hands it to
|
||
<code>clang</code>. There is a second one, off by default, that emits x86-64 by hand —
|
||
see <a href="#targets">targets and builds</a>.</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 import-c <header.h> [package.flan...] [clang flags...]
|
||
flan generate-c <package-dir>
|
||
flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] [--debug] [--sanitize] [--x86] [--target=wasm32-wasi|web]
|
||
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. <code>run</code> builds to a temporary file and execs it.</p>
|
||
|
||
<p>The smallest program:</p>
|
||
|
||
<pre><code>(defn main [] ()
|
||
(println "hello from flan"))</code></pre>
|
||
|
||
<p>The entry point is <code>(defn main [args [string]] i32)</code>. The parameter is
|
||
optional — omitting <code>args</code> means the program ignores argv — and the return
|
||
type is not: <code>()</code> is unit, and a <code>main</code> that returns it exits 0.</p>
|
||
|
||
<h2 id="values">Values and memory</h2>
|
||
|
||
<p>There is no garbage collector and no hidden allocation. Every local is a stack
|
||
slot; reading one is a load, assigning to one is a store, and a store of an aggregate
|
||
<em>is</em> the copy. A struct is its C layout with nothing added.</p>
|
||
|
||
<p>Four rules carry most of the model:</p>
|
||
|
||
<ul>
|
||
<li><strong>Zero is initialisation.</strong> A declaration with no initialiser is
|
||
all-bytes-zero, so a global array is BSS and costs nothing to start. A struct
|
||
literal that omits a field zeroes it. <code>(zeroed)</code> re-zeroes something
|
||
later — a memset, not an allocation.</li>
|
||
<li><strong>Fixed arrays are values.</strong> <code>[n T]</code> is inline storage
|
||
and copies on assignment and on pass-by-value.</li>
|
||
<li><strong>Slices are views.</strong> <code>[T]</code> is ptr+len and owns nothing.
|
||
Copying a slice copies the view, never the elements.</li>
|
||
<li><strong>Pointers are visible.</strong> <code>(addr x)</code> takes the address of
|
||
any assignable place and gives <code>(Ptr T)</code>. It does not extend anything's
|
||
lifetime, and keeping one past its frame is your contract to honour — there is no
|
||
borrow checker.</li>
|
||
</ul>
|
||
|
||
<p>A value struct is shared mutably by passing its address down the call chain. No
|
||
heap is involved.</p>
|
||
|
||
<p>Places — the forms <code>set</code> accepts — are a fixed list, not an extensible
|
||
<code>setf</code>:</p>
|
||
|
||
<pre><code>(defstruct Enemy [hp i32 name string])
|
||
|
||
(defvar spawned i32)
|
||
(defconst room-size 4)
|
||
(defvar room [room-size i32])
|
||
|
||
;; `set` takes a fixed list of forms, not an extensible setf.
|
||
(defn main [] ()
|
||
(let [e (Enemy {.hp 10 .name "slime"})
|
||
p (addr e)]
|
||
(set spawned (+ spawned 1)) ; a local or a defvar
|
||
(set (.hp e) 7) ; a struct field
|
||
(set (.hp p) 8) ; through a (Ptr Enemy) — derefs one level
|
||
(set (at room 2) 5) ; a fixed array or slice element
|
||
(set (deref p) (Enemy {.hp 3 .name "wisp"})) ; a whole-object store
|
||
|
||
(print (.hp e)) (println "")
|
||
(println (.name e))
|
||
(print (at room 2)) (println "")
|
||
(print spawned) (println "")))</code></pre>
|
||
|
||
<pre><code class="sh">3
|
||
wisp
|
||
5
|
||
1</code></pre>
|
||
|
||
<p><code>.field</code> and <code>at</code> dereference exactly one pointer level, so
|
||
<code>(set (.hp p) 8)</code> above is legal when <code>p</code> is a
|
||
<code>(Ptr Enemy)</code>. The whole-object store through <code>p</code> overwrote
|
||
<code>e</code> itself, so <code>hp</code> reads 3 and not 8.</p>
|
||
|
||
<h3>Bounds are checked</h3>
|
||
|
||
<pre><code>(defconst xs [3 i32] [1 2 3])
|
||
|
||
;; (at xs 7) with a literal index does not reach the backend at all: check.ml
|
||
;; rejects it. This one goes through a local, so it is the runtime check that
|
||
;; catches it — the same message, and the program stops where it happened.
|
||
(defn main [] ()
|
||
(let [i 7]
|
||
(println "before")
|
||
(print (at xs i))
|
||
(println "unreachable")))</code></pre>
|
||
|
||
<pre><code class="sh">$ flan run bounds.flan
|
||
before
|
||
bounds.flan:9:19: index 7 is out of bounds for length 3
|
||
$ echo $?
|
||
134</code></pre>
|
||
|
||
<p><code>at</code> and <code>slice</code> emit a comparison and a branch to a cold
|
||
block that names the source location and stops. Checks are on by default and are not
|
||
tied to the optimisation level. <code>--no-bounds-checks</code> turns them off.
|
||
Measured cost on a
|
||
50-million-iteration dependency chain over a 1024-element array: 0.11–0.12s checked
|
||
against 0.12–0.13s unchecked.</p>
|
||
|
||
<h3>So is arithmetic that has no answer</h3>
|
||
|
||
<p>Three integer operations have no right result, and each of them used to be a bare
|
||
<code>SIGFPE</code> or an undefined value: a divide or remainder by zero, the one division
|
||
that overflows (<code>INT64_MIN / -1</code>, whose true quotient is one past the top of
|
||
the type), and a float-to-integer cast whose value does not fit. All three now signal
|
||
<code>ArithError</code>, the way a bad index signals <code>BoundsError</code>.</p>
|
||
|
||
<pre><code>;; Three integer operations have no right answer. Each used to be a bare SIGFPE
|
||
;; or an undefined value; each signals ArithError now. The divisor goes through
|
||
;; a global so that constant folding cannot answer it before the backend does.
|
||
(defvar zero i32 0)
|
||
|
||
(defn main [] ()
|
||
(println "before")
|
||
(println (/ 10 zero))
|
||
(println "unreachable"))</code></pre>
|
||
|
||
<pre><code class="sh">$ flan run arith.flan
|
||
before
|
||
arith.flan:8:12: divide by zero: (/ 10 0)
|
||
$ echo $?
|
||
134
|
||
|
||
$ flan run cast.flan
|
||
cast.flan:7:17: this value does not fit the integer type it is cast to, which
|
||
holds [-2147483648 2147483647]</code></pre>
|
||
|
||
<pre><code>;; A float-to-integer cast whose value does not fit. The condition it violated
|
||
;; is reported as the range the destination type can hold, which is the same
|
||
;; shape BoundsError uses for a slice: the violated condition, written out.
|
||
(defvar big f64 1e30)
|
||
|
||
(defn main [] ()
|
||
(println (i32 big)))</code></pre>
|
||
|
||
<p>A Lisp that stops naming the file and the line beats one that dies with
|
||
<code>SIGFPE</code>, and a program that genuinely does not care installs a handler once at
|
||
startup and never thinks about it again. Float division is deliberately left alone: IEEE
|
||
already answers it, with an infinity or a NaN.</p>
|
||
|
||
<p><strong>No restart is established at the failing operation</strong>, which is the same
|
||
decision <code>BoundsError</code> made and for the same reason. A restart frame is
|
||
allocated by the <code>restart-case</code> that offers it, on that frame's own stack, so
|
||
nothing below the program can push one on its behalf; a <code>use-value</code> at a
|
||
division would mean an <code>alloca</code> and a push-and-pop emitted at every division in
|
||
every checked build, and what it would buy is a silently different answer. What answers a
|
||
division by zero is the restart the program already had — a frame loop's
|
||
<code>continue</code> — which is on the stack and reachable from a handler or from the
|
||
break loop without anything being pushed at the failure.</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>(Vec T)</code></td><td>growable, owning — <em>moves</em> on assignment</td><td>ptr + len + cap + its allocator</td></tr>
|
||
<tr><td><code>(Map K V)</code></td><td>open addressing, owning — <em>moves</em>. The only map spelling: braces in type position are not a type</td><td>data + len + log2cap + its allocator</td></tr>
|
||
<tr><td><code>(Pool T)</code></td><td>generational slab storage, owning — <em>moves</em></td><td>items + slots + its allocator</td></tr>
|
||
<tr><td><code>(Handle T)</code></td><td>a reference into a pool that reports a dead referent</td><td>index and generation packed into an <code>i64</code></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><code>(Fn [T ...] R)</code></td><td>a function value</td><td>a pointer</td></tr>
|
||
<tr><td><code>Allocator</code></td><td>an opaque builtin: a proc, its data and a capability set</td><td>a pointer to that</td></tr>
|
||
<tr><td><code>$t</code></td><td>a type variable — see <a href="#generics">generics</a></td><td>whatever it is instantiated at</td></tr>
|
||
<tr><td>a struct</td><td>value type</td><td>fields in declaration order</td></tr>
|
||
<tr><td>a union</td><td><code>defunion</code>, matched by case</td><td>tag + the widest payload</td></tr>
|
||
<tr><td>an enum</td><td>its own type in the checker</td><td><code>i32</code></td></tr>
|
||
<tr><td><code>()</code></td><td>one value, zero size</td><td>empty</td></tr>
|
||
<tr><td><code>Never</code></td><td>fits anywhere; nothing has it</td><td>empty</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p><strong>There is no implicit widening.</strong> Both operands of a binary operator
|
||
have one type, and every conversion is written as a cast:</p>
|
||
|
||
<pre><code>(defn main [] i32
|
||
(let [n 40 ; i32, inferred
|
||
big (i64 n) ; every widening is written
|
||
x 1.5] ; f64
|
||
(print (+ big 2)) (println "")
|
||
(print (* x 2.5)) (println "")
|
||
(print (bit-xor (<< 1 8) 255)) (println "")
|
||
0))</code></pre>
|
||
|
||
<pre><code class="sh">42
|
||
3.75
|
||
511</code></pre>
|
||
|
||
<p>An untyped integer literal is <code>i32</code> and an untyped float literal is
|
||
<code>f64</code>, so <code>(defconst gravity f32 0.05)</code> names the type when
|
||
something narrower is wanted. One caveat: a whole-numbered float prints without its
|
||
fraction, so <code>3.0</code> comes out as <code>3</code>.</p>
|
||
|
||
<p>Arithmetic wraps. Shifts are bounded two ways: a literal count at or past the
|
||
operand's width is a compile error, and a computed one is masked to the width.
|
||
<code>>></code> is arithmetic on a signed type and logical on an unsigned
|
||
one.</p>
|
||
|
||
<p>An index converts from a narrower integer and never from a wider one. A
|
||
<code>u32</code> index is fine — anything above 2<sup>31</sup> truncates to a negative
|
||
<code>i32</code> and the unsigned bounds check rejects it. An <code>i64</code> index is
|
||
refused:</p>
|
||
|
||
<pre><code class="sh">an index is an i32, and i64 is wider — write (i32 …), because a value that does
|
||
not fit truncates to one that does and would read the wrong element without
|
||
tripping the bounds check</code></pre>
|
||
|
||
<h2 id="structs">Structs and enums</h2>
|
||
|
||
<p>A <code>defstruct</code> is a list of inline name/type pairs. A struct literal names
|
||
its fields, and omitted fields are zeroed.</p>
|
||
|
||
<pre><code>(defstruct Cursor
|
||
[src [u8] ; a non-owning slice
|
||
pos i32]) ; no initialiser means zeroed
|
||
|
||
(defn peek [c (Ptr Cursor)] u8
|
||
(if (< (.pos c) (len (.src c)))
|
||
(at (.src c) (.pos c))
|
||
0))
|
||
|
||
(defn advance [c (Ptr Cursor)] ()
|
||
(set (.pos c) (+ (.pos c) 1))) ; field access derefs one level
|
||
|
||
(defn main [] ()
|
||
(let [c (Cursor {.src (bytes "hi")})] ; pos omitted, so pos is 0
|
||
(print (peek (addr c))) (println "")
|
||
(advance (addr c))
|
||
(print (peek (addr c))) (println "")))</code></pre>
|
||
|
||
<pre><code class="sh">104
|
||
105</code></pre>
|
||
|
||
<p>Those are the bytes <code>h</code> and <code>i</code>. There is no character type — a
|
||
byte is a <code>u8</code> — but there is a byte literal, so <code>\h</code> is 104 and
|
||
<code>\space</code> is 32, and the prelude's <code>digit?</code> reads as
|
||
<code>(and (>= b \0) (<= b \9))</code>. To see a byte as a letter rather than as a
|
||
number, print a slice of them: <code>print</code> writes a <code>[u8]</code> as
|
||
its bytes.</p>
|
||
|
||
<p>An enum is an <code>i32</code> at run time and its own type in the checker. A
|
||
keyword at a call site resolves against the parameter's enum type at compile time, so a
|
||
typo is an error there rather than a wrong number later.</p>
|
||
|
||
<pre><code>(defenum Key
|
||
[space 32 escape 256 left 263 right 262])
|
||
|
||
(defn key-name [k Key] string
|
||
(cond
|
||
(= k :space) "space"
|
||
(= k :escape) "escape"
|
||
:else "an arrow"))
|
||
|
||
(defn main [] ()
|
||
;; :space resolves against the parameter's enum at compile time.
|
||
;; A typo is an error here, not a wrong number later.
|
||
(println (key-name :space))
|
||
(println (key-name :left)))</code></pre>
|
||
|
||
<pre><code class="sh">space
|
||
an arrow</code></pre>
|
||
|
||
<p>A keyword means nothing where no enum is expected. There is no keyword type to fall
|
||
back on, and there is no way to name a member other than as a keyword in a position
|
||
that expects that enum.</p>
|
||
|
||
<p>Struct and enum names share one top-level namespace with functions, globals and type
|
||
aliases. A second declaration of a name is rejected whatever kind either one is.</p>
|
||
|
||
<h2 id="functions">Functions</h2>
|
||
|
||
<p><code>(defn name [param Type ...] ReturnType body ...)</code>. The parameters are
|
||
inline name/type pairs, as in <code>let</code> and <code>defstruct</code>. The return
|
||
type is always written, and a function that returns nothing writes <code>()</code>,
|
||
which is unit. There is no separate <code>declare</code> form for a function with a
|
||
body — <code>declare</code> is kept only where there is none.</p>
|
||
|
||
<p>The slot used to be optional, and the parser decided return-type-versus-body by
|
||
looking the name up in a table of the file's types. It was sound only because one
|
||
top-level namespace means a name cannot be both a type and a value, and it was
|
||
silently wrong twice — once reading <code>(Rune {.code 65})</code> at the head of a
|
||
body as the function's return type. Writing the type removes the guess, and a
|
||
mistyped one now says <em>did you mean f64</em> rather than <em>unknown name</em>.</p>
|
||
|
||
<p>Top-level names are order-independent within a package, so mutually recursive
|
||
functions need no forward declaration. Globals come in two kinds:</p>
|
||
|
||
<pre><code>(defconst cell-size 5) ; a compile-time constant
|
||
(defconst gravity f32 0.05) ; with its type named
|
||
(defvar current-color i32) ; zeroed storage
|
||
(defconst rows 3)
|
||
(defconst cols 4)
|
||
(defvar grid [rows [cols u32]]) ; BSS, rows*cols*4 bytes
|
||
|
||
(defn main [] ()
|
||
(print cell-size) (println "")
|
||
(print gravity) (println "")
|
||
(print current-color) (println "")
|
||
(print (at grid 2 3)) (println ""))</code></pre>
|
||
|
||
<pre><code class="sh">5
|
||
0.05
|
||
0
|
||
0</code></pre>
|
||
|
||
<p>A <code>defconst</code> the checker consumed — an array length, for instance — is
|
||
part of the shape of the program. One it did not is only ever bytes in memory. The
|
||
two reload differently; see <a href="#devloop">the dev loop</a>.</p>
|
||
|
||
<p><code>let</code> binds name/value pairs and takes no type annotation, so a constant
|
||
whose type matters is named at the top level rather than written inline.</p>
|
||
|
||
<h2 id="control">Control flow</h2>
|
||
|
||
<p><code>if</code>, <code>when</code>, <code>unless</code>, <code>cond</code>,
|
||
<code>do</code>, <code>and</code>, <code>or</code>, <code>not</code>,
|
||
<code>while</code>, <code>until</code>, <code>dotimes</code>, <code>loop</code>/<code>recur</code>,
|
||
<code>break</code>, <code>continue</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 i)
|
||
(print " ")
|
||
(set i (- i 1)))
|
||
(println "")))
|
||
|
||
(defn first-even [s [i32]] (Option i32)
|
||
(dotimes [i (len s)]
|
||
(when (= 0 (% (at s i) 2))
|
||
(return (Some (at s i)))))
|
||
None)
|
||
|
||
(defn main [] ()
|
||
(println (classify -3))
|
||
(countdown 4)
|
||
(unless false
|
||
(println "unless runs when the test is false"))
|
||
(match (first-even (slice nums 0 (len nums)))
|
||
(Some n) (do (print n) (println ""))
|
||
None (println "none")))</code></pre>
|
||
|
||
<pre><code class="sh">negative
|
||
4 3 2 1
|
||
unless runs when the test is false
|
||
8</code></pre>
|
||
|
||
<p><code>dotimes</code> evaluates its bound once into a hidden slot before the loop, so
|
||
a body that changes it cannot change the trip count, and the loop variable is not
|
||
assignable.</p>
|
||
|
||
<p><code>break</code> and <code>continue</code> leave or restart the innermost loop, and
|
||
take a <strong>label</strong> when that is not the one meant. <code>loop</code> and
|
||
<code>recur</code> are the functional shape beside them: <code>loop</code> is an
|
||
<em>expression</em>, its value is the value of its body, and <code>recur</code> rebinds
|
||
every name at once and jumps rather than calls — ten million iterations do not grow the
|
||
stack.</p>
|
||
|
||
<pre><code>(defn gcd [a i32 b i32] i32
|
||
(loop [x a y b] ; recur rebinds every name at once
|
||
(if (= y 0) x (recur y (% x y)))))
|
||
|
||
(defn main [] ()
|
||
(println (gcd 84 36))
|
||
|
||
;; loop is an expression: its value is the value of the body.
|
||
(println (loop [i 0 acc 0]
|
||
(if (= i 5) acc (recur (+ i 1) (+ acc i)))))
|
||
|
||
;; continue in a dotimes advances the counter on the skipped iteration too.
|
||
(let [sum 0]
|
||
(dotimes [k 5]
|
||
(when (= k 2) (continue))
|
||
(set sum (+ sum k)))
|
||
(println sum))
|
||
|
||
;; A label says which loop. Unlabelled, break leaves the innermost.
|
||
(dotimes :outer [a 3]
|
||
(dotimes [b 3]
|
||
(when (= b 2) (break :outer))
|
||
(println b))))</code></pre>
|
||
|
||
<pre><code class="sh">12
|
||
10
|
||
8
|
||
0
|
||
1</code></pre>
|
||
|
||
<p><code>return</code> is still the way out of a <em>function</em> from inside a loop,
|
||
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 a <code>defunion</code>, and on nothing else. <code>some</code> unwraps
|
||
<code>Some</code> and early-returns <code>None</code> from the enclosing function.</p>
|
||
|
||
<pre><code>(defconst nums [4 i32] [4 8 15 16])
|
||
|
||
;; `some` unwraps Some and early-returns None from *this* function.
|
||
(defn doubled-first [s [i32]] (Option i32)
|
||
(Some (* 2 (some (index-of s 15)))))
|
||
|
||
(defn main [] ()
|
||
(match (doubled-first (slice nums 0 4))
|
||
(Some i) (do (print i) (println "")) ; 4
|
||
None (println "not found"))
|
||
(match (index-of (slice nums 0 4) 99)
|
||
(Some i) (do (print i) (println ""))
|
||
None (println "not found")))</code></pre>
|
||
|
||
<pre><code class="sh">4
|
||
not found</code></pre>
|
||
|
||
<h2 id="defer">defer</h2>
|
||
|
||
<p>A <code>defer</code> runs at function exit, innermost first. An explicit
|
||
<code>return</code> runs the ones registered above it — a defer written below a return
|
||
has not executed yet and must not fire.</p>
|
||
|
||
<pre><code>(defn work [n i32] i32
|
||
(defer (println "second"))
|
||
(defer (println "first")) ; innermost-first at exit
|
||
(when (< n 0)
|
||
(return 0)) ; runs both defers above it
|
||
(println "body")
|
||
n)
|
||
|
||
(defn main [] ()
|
||
(print (work 3))
|
||
(println ""))</code></pre>
|
||
|
||
<pre><code class="sh">body
|
||
first
|
||
second
|
||
3</code></pre>
|
||
|
||
<p><code>defer</code> is function-scoped: it is copied into every exit path of the
|
||
function, so it always registers and always runs at function exit. It is therefore
|
||
<strong>rejected</strong> inside a loop or a branch, where "always registers" would be a
|
||
lie. A <code>let</code> is fine, and that is not an exception — a <code>let</code> is not
|
||
a frame here, its bindings are function slots like any other and nothing is released at
|
||
scope exit, so a <code>defer</code> written in one has exactly the function's extent.
|
||
That is the shape the permission exists for: acquire, defer the release beside it, then
|
||
use it.</p>
|
||
|
||
<pre><code class="sh">defer is not allowed inside a branch — a defer is copied into every exit path of
|
||
the function, so it always registers and always runs at function exit. Write it at
|
||
the top level of the function body, or in a let that is (a let has the function's
|
||
extent, because nothing is released at scope exit)</code></pre>
|
||
|
||
<h2 id="arrays">Arrays and slices</h2>
|
||
|
||
<p><code>at</code> indexes a fixed array or a slice, and takes any number of
|
||
indices, so <code>(at grid r c)</code> indexes a two-dimensional fixed array directly.
|
||
It is a place: <code>(set (at grid r c) v)</code> and <code>(addr (at grid r c))</code> both work.
|
||
<code>len</code> works on a fixed array, a slice or a string.
|
||
<code>(slice s lo hi)</code> takes a half-open range and never copies.</p>
|
||
|
||
<pre><code>(defconst rows 3)
|
||
(defconst cols 4)
|
||
|
||
;; A fixed array is a value: inline storage, copies on assignment.
|
||
(defconst palette [4 u32] [0xE6B800FF 0x3B6E8CFF 0xA83232FF 0xCC6B1FFF])
|
||
|
||
;; No initialiser means all-bytes-zero, so this is BSS and costs nothing.
|
||
(defvar grid [rows [cols i32]])
|
||
|
||
(defn main [] ()
|
||
(set (at grid 1 2) 7)
|
||
(print (at grid 1 2)) (println "") ; 7
|
||
(print (len palette)) (println "") ; 4
|
||
|
||
;; A slice is ptr+len and non-owning: it views the array, it does not copy it.
|
||
(let [row (slice (at grid 1) 0 cols)]
|
||
(set (at row 0) 5)
|
||
(print (at grid 1 0)) (println "") ; 5 — the same storage
|
||
(print (sum-i32 row)) (println "")) ; 12
|
||
|
||
;; (zeroed) is a memset, not an allocation.
|
||
(set grid (zeroed))
|
||
(print (at grid 1 2)) (println "")) ; 0</code></pre>
|
||
|
||
<pre><code class="sh">7
|
||
4
|
||
5
|
||
12
|
||
0</code></pre>
|
||
|
||
<p>A reversed range — <code>lo</code> greater than <code>hi</code> — traps, rather than
|
||
yielding a huge unsigned length.</p>
|
||
|
||
<h2 id="generics">Generics</h2>
|
||
|
||
<p>Parametric polymorphism by monomorphisation: one body is written, and every call
|
||
site gets a copy compiled at the types it passed. There are no type classes, no
|
||
dictionaries and nothing decided at run time.</p>
|
||
|
||
<p><strong>A type variable is written <code>$t</code> where a type goes</strong> — a
|
||
parameter's type, the return type, or nested inside a type constructor such as
|
||
<code>[$t]</code> or <code>(Vec $t)</code>. The sigil is not a binding-site-only
|
||
spelling: every type position writes it. <strong>Bare <code>t</code> is the same
|
||
variable in <em>expression</em> position</strong>, where a builtin takes the name of a
|
||
type as an argument — <code>(vec-new t)</code>, <code>(map-new t i32)</code>,
|
||
<code>(pool-new t)</code>, and the cast <code>(t x)</code>.</p>
|
||
|
||
<pre><code>(defn ident [x $t] $t x) ; needs nothing declared
|
||
|
||
(defn twice [x $t] $t ; + - * / % need numeric?
|
||
{:where (numeric? $t)}
|
||
(+ x x))
|
||
|
||
(defn clamp-to [x $t lo $t hi $t] $t ; < <= > >= min max need ordered?
|
||
{:where (ordered? $t)}
|
||
(min (max x lo) hi))
|
||
|
||
(defn first-or [s [$t] d $t] $t ; the variable inside a slice type
|
||
{:where (copyable? $t)}
|
||
(if (= (len s) 0) d (at s 0)))
|
||
|
||
(defn one-of [x $t] (Vec $t) ; bare t is the type-name argument
|
||
{:where (copyable? $t)}
|
||
(let [v (vec-new t)]
|
||
(push v x)
|
||
v))
|
||
|
||
(defn main [] ()
|
||
(println (ident 3))
|
||
(println (ident "text"))
|
||
(println (twice 1.5))
|
||
(println (clamp-to 12 0 10))
|
||
(let [ns [5 3 9 1]
|
||
one (one-of 4.5)]
|
||
(println (first-or (slice ns 0 4) -1))
|
||
(println (at (as-slice one) 0))
|
||
(free one)))</code></pre>
|
||
|
||
<pre><code class="sh">3
|
||
text
|
||
3
|
||
10
|
||
5
|
||
4.5</code></pre>
|
||
|
||
<h3>The body is checked once, abstractly</h3>
|
||
|
||
<p>A generic body is checked <strong>with nothing substituted</strong>, so an operator
|
||
the variable is not declared to support is refused at the definition rather than at
|
||
whichever call site first reaches a type that happens to work. That is deliberately
|
||
<em>not</em> Odin's rule, which checks a polymorphic body per instantiation:</p>
|
||
|
||
<pre><code class="sh">+ over the type variable t is refused: a type variable supports only what it is
|
||
declared to support, and nothing here says t is numeric?. Write {:where (numeric?
|
||
$t)} at the head of the body, or take the operation as a parameter — a
|
||
(Fn [t t] ...) — and call it here</code></pre>
|
||
|
||
<p>What makes that liveable is a <code>where</code> clause, written as a Clojure-style
|
||
map at the head of the body — <code>{:where (ordered? $t)}</code>, or a vector when
|
||
there is more than one: <code>{:where [(copyable? $t) (copyable? $u)]}</code>. There
|
||
are five predicates, and each gates builtins the compiler already has:</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>Predicate</th><th>What it admits</th></tr>
|
||
<tr><td><code>numeric?</code></td><td><code>+</code> <code>-</code> <code>*</code> <code>/</code> <code>%</code>, and a cast <code>(t x)</code></td></tr>
|
||
<tr><td><code>ordered?</code></td><td><code><</code> <code><=</code> <code>></code> <code>>=</code> <code>min</code> <code>max</code></td></tr>
|
||
<tr><td><code>equal?</code></td><td><code>=</code> and <code>!=</code></td></tr>
|
||
<tr><td><code>hashable?</code></td><td>the variable as a <code>Map</code> key — <code>(map-new t V)</code>, <code>get</code>, <code>put</code>, <code>has-key?</code></td></tr>
|
||
<tr><td><code>copyable?</code></td><td>reading the value more than once; <code>Pool</code> and <code>Vec</code> element positions</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p>They entail each other in one direction, so one clause usually does:
|
||
<code>numeric?</code> gives <code>ordered?</code>, <code>ordered?</code> gives
|
||
<code>equal?</code>, and any of the four gives <code>copyable?</code>. A
|
||
<code>sort!</code> that compares its elements and reads them twice declares
|
||
<code>ordered?</code> and nothing else.</p>
|
||
|
||
<p><strong>A type variable is move-only by default</strong>, and
|
||
<code>copyable?</code> is the opt-out. <code>Types.is_move_only</code> of a variable is
|
||
not decidable abstractly — the same variable is <code>i32</code> at one instantiation
|
||
and <code>(Vec i32)</code> at the next — so the checker assumes the stricter rule,
|
||
which can only refuse a program that would have been fine and never admit one that
|
||
double-frees. It is Rust's <code>T: Copy</code>, with the difference that the compiler
|
||
answers the question rather than a user implementing a trait. So
|
||
<code>(defn twice [x $t] $t (+ x x))</code> does not merely want
|
||
<code>numeric?</code>; reading <code>x</code> a second time is a use after move:</p>
|
||
|
||
<pre><code class="sh">x was moved at twice.flan:1:26 and cannot be used again — t is move-only, so
|
||
binding, passing or returning one transfers ownership and the source binding is
|
||
dead afterwards (spec-memory.md). That rule is what makes a double free
|
||
unrepresentable; (clone x) if you wanted a second one</code></pre>
|
||
|
||
<p>Each instantiation then checks the concrete type against what the signature declared,
|
||
and refuses the <em>call site</em> when it does not answer:</p>
|
||
|
||
<pre><code class="sh">this call instantiates twice at $t = bool, and bool does not answer numeric? —
|
||
which twice requires, being written {:where (numeric? $t)}. The requirement is the
|
||
signature's, so the refusal is here, at the call that asked for the type: pass one
|
||
the predicate admits</code></pre>
|
||
|
||
<p>Two forms are deferred to the instantiation rather than settled abstractly, because
|
||
their legality is only decidable after substituting: <code>println</code> over a
|
||
variable, which selects the structural printer per copy, and the <code>Map</code>
|
||
operations over a variable key, whose hash and equality are concrete symbols chosen from
|
||
the concrete key type. The <code>Map</code> half is what <code>hashable?</code> buys —
|
||
without the clause, the type <code>(Map $t i32)</code> is refused where it is
|
||
<em>written</em>, and with it the refusal moves to the call site that names an
|
||
unhashable key.</p>
|
||
|
||
<p>This is not a type class and the difference is worth keeping straight. A type class
|
||
carries <em>implementations</em>, selected per instance and extensible by anyone, and
|
||
needs dictionaries and coherence rules. A predicate carries nothing — it gates a builtin
|
||
that already exists. The ceiling is that nobody can supply a <code><</code> of their
|
||
own; every operation the prelude and the containers need is a primitive, so it does not
|
||
bind. <code>test/programs/generics.flan</code> exercises the whole of it.</p>
|
||
|
||
<h2 id="printing">Printing</h2>
|
||
|
||
<p><code>println</code> prints a value and a newline; <code>print</code> is the same
|
||
walk without the newline. There is one of each and they take any type, but neither is a
|
||
function and neither is overloading: the compiler walks the argument's type where the
|
||
call is written and emits the printing for it. Nothing is decided at run time — a Flan
|
||
value carries no header, so nothing at run time could say what it is — and there is no
|
||
user-supplied printer to choose between.</p>
|
||
|
||
<pre><code>(defenum Key [space 32 left 263])
|
||
(defstruct Enemy [hp i32 name string key Key])
|
||
|
||
(defn look-up [k Key] (Option i32)
|
||
(if (= k :space) (Some 32) None))
|
||
|
||
(defn main [] ()
|
||
(println 42) ; an i32, uncast
|
||
(println 1.5)
|
||
(println (Enemy {.hp 3 .name "wisp" .key :left}))
|
||
(println (look-up :space))
|
||
(println (look-up :left))
|
||
(print "no newline: ") (println true))</code></pre>
|
||
|
||
<pre><code class="sh">42
|
||
1.5
|
||
(Enemy {.hp 3 .name "wisp" .key :left})
|
||
(some 32)
|
||
none
|
||
no newline: true</code></pre>
|
||
|
||
<p>The walk covers every integer and float type, <code>bool</code>, <code>()</code>,
|
||
<code>string</code>, <code>[u8]</code>, enums, <code>Ptr</code>, <code>Option</code>,
|
||
structs, unions, fixed arrays and slices. An owning container has no printer for its
|
||
contents and comes back as a marker instead — <code><vec></code>,
|
||
<code><pool></code>, <code><allocator></code> — while a <code>Handle</code>
|
||
shows its index and generation, and a <code>Map</code> has no printer at all.
|
||
An enum member comes back as its name: the value is
|
||
an <code>i32</code> by the time the backend sees it, so the name is recovered here from
|
||
the checker's table, and a value outside the declared members falls through to the
|
||
number, which is what you would want to see. A <code>Ptr</code> prints as
|
||
<code><ptr></code> and is never followed — it is the one thing that could make
|
||
the walk cycle, and dereferencing a pointer on someone else's behalf is not safe.</p>
|
||
|
||
<p>A string prints raw at the top level and quoted-and-escaped inside a structure.
|
||
Those are not in conflict: <code>(println "hello")</code> has to print
|
||
<code>hello</code> or it is useless, and the <code>name</code> field above has to be
|
||
quoted or it could not be told from the punctuation around it.</p>
|
||
|
||
<p>The walk is bounded in depth and in span, so a deeply nested value or a
|
||
<code>[100 [100 u32]]</code> grid prints <code>...</code> rather than a screenful — and,
|
||
since the walk is unrolled at compile time, rather than putting ten thousand printing
|
||
sites in the module.</p>
|
||
|
||
<p>That the walk takes the argument's own type matters more here than it would in a
|
||
language that widens implicitly. Nothing widens implicitly in this one, so a printer
|
||
that named a type would need a cast written at every call — and a <code>u64</code>
|
||
above 2<sup>63</sup> put through a signed one comes out negative. <code>print</code>
|
||
takes the value as it is and prints the number it holds.</p>
|
||
|
||
<h2 id="prelude">The prelude</h2>
|
||
|
||
<p>The prelude is written in Flan, all but five lines of it, and prepended to every
|
||
program, so nothing in it needs importing. It holds no printing of its own:
|
||
<code>print</code> and <code>println</code> are the compiler's, and
|
||
<code>write-stdout</code> — the one output primitive — is what they are written
|
||
over.</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>Group</th><th>Names</th></tr>
|
||
<tr><td>slice algorithms, over one type variable</td><td><code>swap!</code>, <code>reverse!</code>, <code>sort!</code>, <code>sort-by!</code>, <code>index-of</code>, <code>min-of</code>, <code>max-of</code>, <code>map!</code>, <code>reduce</code>, <code>filter</code></td></tr>
|
||
<tr><td>the per-type layer that stays</td><td><code>sum-i32</code>, <code>sum-f32</code> — the element and the accumulator are different types, which one variable cannot say</td></tr>
|
||
<tr><td>bytes</td><td><code>bytes=?</code>, <code>bytes<?</code>, <code>bytes-ci=?</code>, <code>starts-with?</code>, <code>ends-with?</code>, <code>index-of-bytes</code>, <code>trim</code>, <code>digit?</code>, <code>space?</code>, <code>sort-bytes!</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>split</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>to-lower</code>, <code>to-upper</code></td></tr>
|
||
<tr><td>building bytes</td><td><code>append!</code>, <code>append-i64!</code>, <code>append-f64!</code>, <code>concat</code>, <code>join</code>, <code>repeat-bytes</code>, <code>replace-bytes</code>, <code>slices-new</code>, <code>format-f64</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>clamp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, and the five <code>declare</code>s: <code>sqrt-f32</code>, <code>sin-f32</code>, <code>cos-f32</code>, <code>atan2-f32</code>, <code>pow-f32</code></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>
|
||
<tr><td>forms, for macros</td><td><code>form-nil</code>, <code>form-cons</code>, <code>form-append</code>, <code>form-rest</code>, <code>form-items</code>, <code>form-pair</code>, <code>form-sym?</code>, <code>form-is-sym?</code>, <code>gensym</code>, and <code>unless</code> and <code>into</code>, which are macros written here rather than special forms</td></tr>
|
||
<tr><td>the rest</td><td><code>pause</code>, which signals the <code>Pause</code> condition the break loop stops on, and <code>embed-find</code></td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p><strong>One family, not one per type.</strong> The slice algorithms used to be
|
||
<code>sort-i32!</code> beside <code>sort-f32!</code> beside <code>sort-bytes!</code>, and
|
||
generics collapsed them: <code>sort!</code> is written once and instantiated at whatever
|
||
element type the call passes. <code>sum-i32</code> and <code>sum-f32</code> are what did
|
||
<em>not</em> collapse, and they are the honest exception — each widens its element into a
|
||
different accumulator, which one variable cannot express.</p>
|
||
|
||
<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>Five functions in the file are not Flan, and they are libm's:
|
||
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code> and the same line for
|
||
<code>sinf</code>, <code>cosf</code>, <code>atan2f</code> and <code>powf</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. <strong>The other four are not</strong>: IEEE-754 requires
|
||
nothing of <code>sinf</code>, <code>cosf</code>, <code>atan2f</code> or
|
||
<code>powf</code>, and glibc, musl and wasi-libc do differ in the last bit — so the
|
||
byte-identical-hash property the RNG exists for does not survive a hash routed through
|
||
any of them. Every link carries <code>-lm</code>.</p>
|
||
|
||
<p>The primitives underneath are few — a primitive is the only thing implemented
|
||
twice per backend: <code>argv</code>,
|
||
<code>write-stdout</code>, <code>exit</code>, <code>len</code>, <code>at</code>,
|
||
<code>slice</code>, <code>bytes</code>, <code>bytes->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 (g/length v))
|
||
(println "")))</code></pre>
|
||
|
||
<pre><code class="sh">5</code></pre>
|
||
|
||
<p>There is one form and one meaning: <code>(import alias "path")</code>, and everything
|
||
from the package is qualified <code>alias/name</code>. There is no unqualified-import
|
||
mode, no <code>:refer</code>, no <code>ns</code> form and no per-file namespace object.
|
||
A path with no collection prefix is relative to the importing file;
|
||
<code>vendor:</code> and <code>core:</code> are collections, resolved by walking up from
|
||
the importing file until a directory of that name is found.</p>
|
||
|
||
<p>Importing is a rename. Every top-level name the package declares becomes
|
||
<code>alias/name</code>, and every use of one — in a type, in a body, in a struct
|
||
literal, in an array length — is rewritten to match. Nothing downstream knows a package
|
||
existed.</p>
|
||
|
||
<p>Three more rules:</p>
|
||
|
||
<ul>
|
||
<li><strong>A package may be a single <code>.flan</code> file</strong> named outright,
|
||
for the program that is also a library. <code>sand.flan</code> shares a directory with
|
||
three other loose programs, so naming its directory would import all four.</li>
|
||
<li><strong>A package may import a package</strong>, and the qualification flattens to
|
||
the inner alias: raylib imported by a package that is itself imported is still
|
||
<code>rl/…</code>. A directory is keyed by its real path and read once, so a cycle
|
||
ends there. The same directory under two different aliases is refused.</li>
|
||
<li><strong><code>main</code> is not exported.</strong> A package carrying one would
|
||
collide with the importer's, and <code>main</code> is a reachability root, so an
|
||
imported one would keep everything it calls alive. Writing <code>sand/main</code> is
|
||
refused at the line that wrote it.</li>
|
||
</ul>
|
||
|
||
<p>Visibility is that one rule and no more: there is no package-private marker for
|
||
anything other than <code>main</code> yet.</p>
|
||
|
||
<p>A package may carry the C it binds to. Every <code>.c</code> file in the directory is
|
||
compiled into the build, and a file named <code>link</code> lists extra linker
|
||
arguments. Whether those reach the build is decided after checking, from the program
|
||
rather than from the import list: the compiler starts at <code>main</code>, follows every
|
||
call, and a package none of whose externs survive contributes no C and no linker
|
||
argument, so a file that imports raylib still builds for wasm32.</p>
|
||
|
||
</div>
|
||
|
||
<div class="panel" id="panel-conditions">
|
||
|
||
<h2 id="conditions">Conditions and restarts</h2>
|
||
|
||
<p>A condition is a struct. There is no class hierarchy; matching is by type. The
|
||
signalling end says <em>here is something notable, here is the data</em>, and an outer
|
||
caller decides what to do about it — or decides nothing, in which case the signaller
|
||
carries on.</p>
|
||
|
||
<pre><code>(signal c) ; (). Handler returns -> 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 [p T ...] CLAUSE) ...)
|
||
|
||
(invoke-restart 'name arg ...) ; Never. Innermost frame offering the name wins.</code></pre>
|
||
|
||
<p><code>signal</code> has type <code>()</code>, always. A handler that returns
|
||
normally leaves the signaller to carry on — the accumulation case:</p>
|
||
|
||
<pre><code>(defstruct AssetMissing [id i32])
|
||
|
||
(defvar seen i64)
|
||
|
||
(defn load-all [] ()
|
||
(signal (AssetMissing {.id 1})) ; () — the caller carries on
|
||
(signal (AssetMissing {.id 2})))
|
||
|
||
(defn main [] ()
|
||
(load-all) ; no handler: a no-op
|
||
(print seen) (println "") ; 0
|
||
|
||
;; A handler that returns normally accumulates and lets the signaller run on.
|
||
(handler-bind [(AssetMissing [c] (set seen (+ seen (i64 (.id c)))))]
|
||
(load-all))
|
||
(print seen) (println "")) ; 3</code></pre>
|
||
|
||
<pre><code class="sh">0
|
||
3</code></pre>
|
||
|
||
<p>A handler that invokes a restart instead transfers control outward to the
|
||
<code>restart-case</code> that offers the name, and the clause's value becomes that
|
||
form's value. Every <code>defer</code> between the invoke and the target runs, innermost
|
||
first, before the clause body starts.</p>
|
||
|
||
<pre><code>(defstruct AssetMissing [id i32])
|
||
|
||
(defvar cleanups i64)
|
||
|
||
(defn load [n i32] i32
|
||
(signal (AssetMissing {.id n}))
|
||
100)
|
||
|
||
(defn middle [n i32] i32
|
||
(defer (set cleanups (+ cleanups 1))) ; runs on the transfer too
|
||
(+ (load n) 1))
|
||
|
||
(defn fetch [n i32] i32
|
||
(restart-case (middle n) ; its value if nothing transfers
|
||
(use-placeholder [] -1)
|
||
(retry [] 7)))
|
||
|
||
(defn main [] ()
|
||
(print (fetch 1)) (println "") ; 101 — nothing handled it
|
||
|
||
(handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
|
||
(print (fetch 2)) (println "")) ; -1
|
||
|
||
(print cleanups) (println "")) ; 2 — the defer ran both times</code></pre>
|
||
|
||
<pre><code class="sh">101
|
||
-1
|
||
2</code></pre>
|
||
|
||
<p>A clause may take parameters, which is how the answer comes from <em>outside</em> the
|
||
frame that offers the restart:</p>
|
||
|
||
<pre><code>(defn supplied [n i32] i32
|
||
(restart-case (middle n)
|
||
(use-value [v i32] (* v 2)) ; the handler supplies v
|
||
(retry [] 7)))
|
||
|
||
(defn main [] ()
|
||
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value 21))]
|
||
(println (supplied 7)))) ; 42</code></pre>
|
||
|
||
<pre><code class="sh">42</code></pre>
|
||
|
||
<p>Restart lookup walks the dynamic restart stack from innermost outward and takes the
|
||
first frame offering the name, so an inner <code>restart-case</code> shadows an outer
|
||
one for the duration of its body. An inner parser's <code>skip-form</code> is found
|
||
before an outer one's.</p>
|
||
|
||
<h3>How a transfer is lowered</h3>
|
||
|
||
<p>A transfer is not platform unwinding. Every Flan signature carries a transfer channel
|
||
— one pointer appended as an out-parameter — which <code>invoke-restart</code> writes
|
||
and every call site checks. A callee writes the target into its caller's slot; each
|
||
frame checks, runs its defers and returns early. The disassembly is the release one plus
|
||
a guard after each call.</p>
|
||
|
||
<p>Three consequences:</p>
|
||
|
||
<ul>
|
||
<li><strong>wasm32 works with no exception proposal</strong>, and native and wasm
|
||
builds of the same program agree.</li>
|
||
<li><strong>Every function carries the channel, release builds included.</strong> A
|
||
hot-reload cell holds a bare pointer, so the honest answer to "what can this call?" is
|
||
"anything". A later optimisation may stop a function checking the channel; it may not
|
||
drop the parameter.</li>
|
||
<li><strong>A transfer cannot cross a foreign frame.</strong> A handler installed
|
||
across an FFI boundary must return normally.</li>
|
||
</ul>
|
||
|
||
<h3>Gotchas</h3>
|
||
|
||
<ul>
|
||
<li><strong>A handler closes over nothing.</strong> A clause is lifted into a function
|
||
of its own, because it runs from wherever the signal was. Accumulate into a global, or
|
||
put the value on the condition. A reference to an enclosing local is refused for that
|
||
reason rather than reported as an unknown name.</li>
|
||
<li><strong>A restart is not a transaction.</strong> Control resumes at the
|
||
<code>restart-case</code> and runs forward from there, so a <code>retry</code> repeats
|
||
every side effect between it and the target. Nothing rolls back — a global the frame
|
||
already set stays set, and is set again. Common Lisp has exactly this property and
|
||
offers no help either.
|
||
<p>So the author chooses where the retry boundary is. A <code>restart-case</code> at the
|
||
top of a frame re-runs everything including mutations already applied; one placed after
|
||
the mutations re-runs only what follows. Put the restart before anything mutates, make
|
||
the retried section idempotent, or snapshot what will be re-applied.
|
||
<code>test/programs/frame-rollback.flan</code> is the worked example of the snapshot, and
|
||
the ordering in it is the part worth reading: restore <em>in the restart clause</em> and
|
||
not in a <code>defer</code>, because a defer runs on the ordinary return path too and
|
||
that version silently rolls back the frames that succeeded.</p>
|
||
<p>This matters more here than in most Lisps because the intended use is a
|
||
<em>game loop</em>, where the plan is to skip a frame and carry on rather than die. Now
|
||
that a bad index signals <code>BoundsError</code> and a bad division signals
|
||
<code>ArithError</code> instead of ending the process, abandoning a frame and retrying
|
||
it is a real thing to do — and that is exactly the case a non-idempotent mutation
|
||
spoils.</p></li>
|
||
<li><strong>An unknown restart name is a hard stop</strong> — a located runtime error.
|
||
There is no <code>find-restart</code> to test with yet.</li>
|
||
<li><strong>No supertype</strong>, so nothing can say "any condition".</li>
|
||
<li><strong><code>signal</code> cannot hand a value back.</strong> Deliberate: the
|
||
alternative forces every signal site to declare a default value and a result type.</li>
|
||
<li><strong><code>return</code> is refused inside a <code>handler-bind</code> or
|
||
<code>restart-case</code> body</strong>, and so is <code>invoke-restart</code> inside a
|
||
<code>defer</code>. In each case a bare exit would leave frames on the stack pointing
|
||
into a function that has gone.</li>
|
||
<li><strong>A restart's parameters are checked at run time</strong>, count then
|
||
spelling, because a restart is found by name on a dynamic stack and neither end can see
|
||
the other. Lookup is by the name alone and the signature is checked after it, so an
|
||
inner <code>(use-value [s string] …)</code> shadows an outer
|
||
<code>(use-value [v i32] …)</code> and <code>(invoke-restart 'use-value 21)</code>
|
||
stops the program even though the outer clause would have taken it. The clause's
|
||
parameters are slots of the function that wrote it, and the invoker fills a buffer that
|
||
function owns — by the time a clause runs, the invoking frame has gone.</li>
|
||
</ul>
|
||
|
||
<h2 id="break">The break loop</h2>
|
||
|
||
<p>An <code>error</code> nothing handles does not kill the program. It stops on the
|
||
frame that erred, with nothing unwound, so the condition and every restart between there
|
||
and the top are still live:</p>
|
||
|
||
<pre><code class="sh">flan: unhandled Missing — stopped, not dead.
|
||
0. restart: retry
|
||
1. restart: use-placeholder</code></pre>
|
||
|
||
<p>From there you fix the function, install it, and take a restart. The numbers are
|
||
how one is taken: a restart is chosen by position, because an inner one may shadow an
|
||
outer one of the same name and a name alone could not tell you which you were getting.
|
||
Control never left the erring frame, so <code>retry</code> calls through the
|
||
indirection cell and reaches the new body. Installing while stopped is allowed; there is no frame in progress.</p>
|
||
|
||
<p>The break loop lives in <code>vendor/agent</code>, which is an optional package. A
|
||
program that does not import it leaves the hook null and stops the old way — the message
|
||
and an exit status of 134:</p>
|
||
|
||
<pre><code>(defstruct Missing [id i32])
|
||
|
||
(defn load [n i32] i32
|
||
(restart-case
|
||
(do (error (Missing {.id n})) ; Never — only a transfer gets past
|
||
0)
|
||
(use-placeholder [] -1)
|
||
(retry [] 7)))
|
||
|
||
(defn main [] ()
|
||
(print (load 1))
|
||
(println ""))</code></pre>
|
||
|
||
<pre><code class="sh">$ flan run boom.flan
|
||
unhandled Missing
|
||
$ echo $?
|
||
134</code></pre>
|
||
|
||
</div>
|
||
|
||
<div class="panel" id="panel-ffi">
|
||
|
||
<h2 id="ffi">The FFI</h2>
|
||
|
||
<p>There are two declaration forms, and they are two forms rather than one because no
|
||
structural rule could tell them apart.</p>
|
||
|
||
<p><code>declare</code> names a C symbol in a signature Flan can already spell. Nothing
|
||
is generated; a Flan string crosses as ptr+len, exactly as it is stored.</p>
|
||
|
||
<pre><code>(declare cos-f64 [x f64] f64 "cos")
|
||
|
||
(defn main [] ()
|
||
(print (cos-f64 0.0)) (println ""))</code></pre>
|
||
|
||
<pre><code class="sh">1</code></pre>
|
||
|
||
<p>That is 1.0, printed by the same rule as before.</p>
|
||
|
||
<p><code>declare-c</code> names the C library's own function in the C library's own
|
||
signature, and the compiler writes the wrapper. This is what raylib's package is made
|
||
of — one line per binding:</p>
|
||
|
||
<pre><code>(declare-c unload-texture [texture Texture2D] "UnloadTexture")
|
||
|
||
(declare-c draw-texture
|
||
[texture Texture2D x i32 y i32 tint Color]
|
||
"DrawTexture")</code></pre>
|
||
|
||
<p><strong>An aggregate's calling convention is not part of its layout.</strong> On
|
||
x86-64, clang gives raylib's own prototypes
|
||
<code><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>A build needs the shared library to be linkable and <em>not</em> the
|
||
<code>-devel</code> package to be installed: every declaration a package uses is written
|
||
down in the package itself, so nothing has to go looking for a system header.
|
||
<strong>Guaranteed:</strong> the C typedef
|
||
and the Flan struct come from the same <code>defstruct</code>, so they cannot disagree,
|
||
and clang type-checks the wrapper against the generated prototype.
|
||
<strong>Trusted:</strong> that the <code>defstruct</code> matches the library's real
|
||
struct, and that the <code>declare-c</code> signature is the function's real signature.
|
||
A scalar's width now carries ABI weight — <code>f64</code> where the library says
|
||
<code>float</code> emits <code>double</code>, and the library reads garbage. That
|
||
trusted half is what the generator below checks.</p>
|
||
|
||
<h3>Generated bindings, committed</h3>
|
||
|
||
<p>Writing a binding per function by hand does not scale past the ones a program
|
||
happens to call, so most of raylib's package is not written by hand. A package
|
||
directory may carry a <code>headers</code> file naming the library's own C header;
|
||
<code>flan generate-c <package-dir></code> reads it with
|
||
<code>clang -Xclang -ast-dump=json</code>, turns every function it can represent into
|
||
the same <code>declare-c</code> line a person would have written, and writes them to
|
||
<code>generated.flan</code> in the package — which is <em>committed</em>.</p>
|
||
|
||
<pre><code class="sh">$ flan generate-c vendor/raylib
|
||
wrote vendor/raylib/generated.flan: 268 declarations, 117 refused, of 581 functions
|
||
in vendor/raylib/raylib-5.5.h.
|
||
Every defstruct, every hand-written declare-c and every mapped
|
||
constant agrees with it.</code></pre>
|
||
|
||
<p>Committing the output is what makes the declarations a dependency of the repository
|
||
rather than of the machine: every build gets all of them, they are greppable, and they
|
||
show up in a diff when the library moves. The argument is not caching — the clang dump
|
||
is already cached on disk and in memory.</p>
|
||
|
||
<p><strong>The header is committed too</strong>, at
|
||
<code>vendor/raylib/raylib-5.5.h</code>, and <code>headers</code> names it by path with
|
||
no environment variable in front of it. That line used to be
|
||
<code>?${FLAN_RAYLIB_H}</code> — optional, on the argument that requiring a header would
|
||
cost everyone the no-<code>-devel</code> property in order to give the check to whoever
|
||
had one. Committing the header dissolved that argument, because nobody needs
|
||
<code>raylib-devel</code> to have a file that ships with the repository. So the check now
|
||
runs on <strong>every</strong> build, and delete the header and the build says so by
|
||
name rather than going quiet. What being optional actually cost was found the hard way:
|
||
a gitignored web directory meant several working trees were checking against nothing and
|
||
were not told, and a check that silently does not run is worse than no check.</p>
|
||
|
||
<p><strong>Regeneration is the stronger check.</strong> An ordinary build re-reads every
|
||
declaration against the header, but only the hand-written ones can disagree — the
|
||
generated half came out of that header and agrees with it by construction. The one
|
||
function that <em>writes</em> the file therefore compares first and <em>refuses to
|
||
write</em> when the package and the header disagree: every <code>defstruct</code>
|
||
against the header's record, and every hand-written <code>declare-c</code> against the
|
||
header's signature. Pointed at a raylib 5.1-dev header while the package is written for
|
||
5.5, it reports ten real differences and writes nothing — which is exactly the silent
|
||
version skew a generated file would otherwise bake in and make look reviewed.</p>
|
||
|
||
<p>This is also why the hand-written bindings are kept rather than replaced by generated
|
||
ones. Everything the generator emits agrees with the header by construction, so diffing
|
||
generated output against the header it came from proves nothing; the hand-written lines
|
||
were transcribed by a person, so they are the only declarations a header can actually
|
||
contradict. All ten of those differences came from them.</p>
|
||
|
||
<p>A committed generated file cannot be hand-corrected — the next regeneration destroys
|
||
the edit without telling anybody — so the corrections live in a <code>bindings</code>
|
||
file beside <code>headers</code>, which is read <em>while</em> the declarations are made.
|
||
Two directives:</p>
|
||
|
||
<pre><code class="sh"># raylib's own malloc/realloc/free, which would be a second untracked heap
|
||
# behind three innocuous Flan names.
|
||
exclude Mem*
|
||
|
||
# The kebab rule gives is-window-ready. Lisp spells a predicate with a ?.
|
||
name IsWindowReady window-ready?</code></pre>
|
||
|
||
<p>An excluded function still says it was excluded rather than going quiet, and a name
|
||
override changes only the Flan face — the C symbol is kept verbatim in the declaration,
|
||
so it is still what is called and still what the check compares. Renaming is also the
|
||
way out of a collision: <code>Spin2D</code> and <code>spin2d</code> both kebab to
|
||
<code>spin-2d</code>, so neither takes the name, because which one won would otherwise
|
||
depend on the order the header happens to declare them in.</p>
|
||
|
||
<p>Anything neither directive can express is a hand-written <code>declare-c</code> in the
|
||
package's own source, which wins over the generated file and is left alone by the
|
||
generator. That is the escape hatch for a signature the importer gets wrong and for a
|
||
Flan face the header cannot describe — raylib keeps two, each a raw binding wrapped by a
|
||
Flan function of the same name, one taking a slice and one answering with an
|
||
<code>Option</code>.</p>
|
||
|
||
<p>Everything the boundary cannot represent is refused by name with the reason, rather
|
||
than half-supported: an <code>Option</code>, a union, a fixed array, a map, a returned
|
||
string, a callback, a slice parameter, an unknown type, an unrepresentable struct field,
|
||
and two Flan names for one C symbol. An aggregate in a plain <code>declare</code> is
|
||
refused too, so the narrow boundary cannot quietly acquire one.</p>
|
||
|
||
<p>One known edge: a REPL redefinition that introduces a <em>new</em>
|
||
<code>declare-c</code> cannot work, because the reload path compiles no C and the wrapper
|
||
would not exist in the running process. Editing the body of a function that calls an
|
||
existing binding is unaffected.</p>
|
||
|
||
</div>
|
||
|
||
<div class="panel" id="panel-devloop">
|
||
|
||
<h2 id="devloop">The dev loop</h2>
|
||
|
||
<p>Edit the code, keep the sand.</p>
|
||
|
||
<pre><code class="sh">$ flan dev sand.flan</code></pre>
|
||
|
||
<p>That builds the program, launches it, holds a session beside it, and listens on
|
||
<code>.flan-dev.sock</code> next to the source. From Emacs, <kbd>C-c C-c</kbd> on a
|
||
function recompiles it and installs it into the running process at that process's next
|
||
frame boundary. The window does not blink and the grid does not reset.</p>
|
||
|
||
<h3>How it works</h3>
|
||
|
||
<p>Four pieces, each runnable on its own.</p>
|
||
|
||
<p><strong>The reload primitive.</strong> <code>llc</code> → <code>ld -shared</code> →
|
||
<code>dlopen</code> → call. Measured in this codebase:</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>Step</th><th>Cost</th></tr>
|
||
<tr><td>emitting the redefinition's IR</td><td>below the timer (<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, so the
|
||
dev path never invokes it. The driver forks a second process and re-does argument and
|
||
target resolution; codegen is not the cost.</p>
|
||
|
||
<p><strong>Indirection cells.</strong> Loading a new body is not installing it. A call
|
||
bound at link time cannot be made to notice one, so a <code>--dev</code> build routes
|
||
every Flan-to-Flan call through a cell — a mutable global holding the address of the
|
||
function that is current.</p>
|
||
|
||
<p>Here is the <code>defer</code> example from <a href="#defer">above</a> — its
|
||
<code>main</code> calls <code>work</code> — through <code>flan emit --dev</code>:</p>
|
||
|
||
<pre><code class="llvm">@"flan.cell.work" = global ptr @"flan.work"
|
||
|
||
define {} @"flan.main"(ptr %xfer) {
|
||
entry:
|
||
;; the frame push, elided — see conditions, above
|
||
%t8 = alloca %slice
|
||
%t2 = load ptr, ptr @"flan.cell.work"
|
||
%t3 = call i32 %t2(i32 3, ptr %xfer)</code></pre>
|
||
|
||
<p>The call site loads the cell rather than naming <code>@"flan.work"</code>
|
||
directly. The signature carries <code>ptr %xfer</code> — the transfer channel from
|
||
<a href="#conditions">conditions</a>, on every Flan function, release builds
|
||
included. <code>println</code> is not in there: it is not a Flan function, so there is
|
||
no call to route and no cell for it.</p>
|
||
|
||
<p>Redefinition is then one store, below a microsecond. Three rules follow. A
|
||
redefinition module declares every <em>global</em> external, so globals live in the host
|
||
and survive a reload. Every other function is a <code>declare</code>, so a redefined
|
||
<code>settle</code> calls the host's <code>move-grain</code> rather than freezing a
|
||
private copy of it. And nothing is ever <code>dlclose</code>d: a cell holds an address
|
||
inside a module's text, so unloading it would leave call sites pointing at unmapped
|
||
memory. Old code is never unloaded, so a thread mid-execution finishes safely in the old
|
||
version.</p>
|
||
|
||
<p><strong>The agent.</strong> <code>vendor/agent</code> is a package like any other: a
|
||
listener thread, a single-producer ring, and three calls. This is the whole of
|
||
<code>agent.flan</code> — no aggregate crosses the boundary, so a plain
|
||
<code>declare</code> does it and there is no shim.</p>
|
||
|
||
<pre><code>(declare start-raw [path string] i32 "flan_agent_start")
|
||
(declare poll-raw [] i32 "flan_agent_poll")
|
||
(declare wait-raw [ms i32] i32 "flan_agent_wait")
|
||
|
||
(defn start [path string] i32 (start-raw path))
|
||
(defn poll [] i32 (poll-raw))
|
||
(defn wait [ms i32] i32 (wait-raw ms))</code></pre>
|
||
|
||
<p><code>(agent/start path)</code> listens on a unix socket, once, at startup.
|
||
<code>(agent/poll)</code> installs whatever has arrived and returns how many.
|
||
<code>(agent/wait ms)</code> is the same but waits for something first. A headless test
|
||
uses it so that a reload is deterministic rather than a race against the frame rate.</p>
|
||
|
||
<p>Loading and installing are separate. <code>dlopen</code> relocates
|
||
a module and takes the loader lock — milliseconds, unbounded — so it happens on the
|
||
listener thread. Installing is one store per function and must not land while a redefined
|
||
function is on the stack, so it happens on the game thread, at the top of the frame, when
|
||
the program asks. A game loop calls <code>agent/poll</code> at the top of its frame and
|
||
ignores the result.</p>
|
||
|
||
<p><strong>The session and the daemon.</strong> <code>flan dev</code> holds the
|
||
declarations the running process was built from plus every change accepted since, and it
|
||
owns the build, so its rules describe the process that is actually running. Re-checking
|
||
the whole program on every evaluation costs under 10ms, less than the <code>llc</code>
|
||
that follows. An evaluation is transactional: a form that fails to check mutates
|
||
nothing.</p>
|
||
|
||
<p>The protocol is one s-expression per message, length-framed by a decimal byte count.
|
||
It is not nREPL: <code>eval</code> there is string-in/string-out with no slot for
|
||
<em>which form, from which file</em>, and once the editor client is ours too there is no
|
||
CIDER to be compatible with.</p>
|
||
|
||
<h3>What a running process cannot be told</h3>
|
||
|
||
<p>Some changes are refused with a reason rather than loaded, because the alternative is
|
||
a silent mismatch against memory the process has already laid out:</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>Change</th><th>What it would have broken</th></tr>
|
||
<tr><td>a function's signature</td><td>a cell is a bare pointer; every call site compiled before the change still passes the old arguments through it</td></tr>
|
||
<tr><td>a global's type</td><td>the storage exists and has a shape — reuse reads at the wrong offsets, replacement discards the state the reload exists to preserve</td></tr>
|
||
<tr><td>a struct's fields</td><td>the values the process is holding have the old layout</td></tr>
|
||
<tr><td>a <code>defconst</code> the checker consumed</td><td>it is in the shape of the program — <code>(defconst rows (/ h c))</code> decides <code>grid</code>'s type before anything else resolves</td></tr>
|
||
<tr><td>a <code>defenum</code> member</td><td><code>:space</code> is erased to an <code>i32</code> literal in the caller, so it is folded there too</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p>A <code>defvar</code>'s <em>initial value</em> is deliberately not on that list:
|
||
its storage holds state the program moved past long ago. And a <code>defconst</code>
|
||
the checker never consumed can be changed, so a colour table can be tuned live while an
|
||
array length stays refused. A dev build emits those as mutable globals, so LLVM cannot
|
||
fold a read of one.</p>
|
||
|
||
<p>The signature row is a stopgap. The design is versioned functions with their own
|
||
trampolines, so that new callers resolve the new version while existing ones keep the
|
||
old. None of the three parts exists yet, and the alternative to refusing is a silent
|
||
argument mismatch.</p>
|
||
|
||
<h3>Evaluating an expression</h3>
|
||
|
||
<p><kbd>C-x C-e</kbd> is a different primitive from redefining a name. There is no name
|
||
to install a body into, so the expression is wrapped in a thunk with nowhere to be called
|
||
from. The module says <em>run this once</em>, and the agent calls it after the install, on
|
||
the game thread, at a frame boundary. An expression reading the program's state therefore
|
||
sees a consistent one.</p>
|
||
|
||
<p>Nothing is marshalled back, because nothing could be: a Flan value carries no header,
|
||
so no code at run time can say what it is. The compiler knows the type and renders it
|
||
there, in the thunk. What comes back looks like this:</p>
|
||
|
||
<pre><code class="sh">big 18446744073709551615
|
||
col :blue
|
||
(.pos b) (V {.x 1.5 .y 0})
|
||
b (Blob {.id 7 .name "sandy \"quoted\"" .pos (V {.x 1.5 .y 0}) .tags [ 0 42 0]})
|
||
(slice (.tags b) 0 3) [ 0 42 0]
|
||
(rl/get-color 0x11223344) (rl/Color {.r 17 .g 34 .b 51 .a 68})
|
||
sim/grid [ [ 0 0 0 0 0 0 0 0 ...] [ 0 ... ] ...]</code></pre>
|
||
|
||
<p>A pointer is never followed; it renders as <code><ptr></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. An owning
|
||
container renders as a marker rather than its contents — <code><vec></code>,
|
||
<code><pool></code>, <code><allocator></code>, <code><handle 3:1></code>
|
||
— and a function value renders as its signature, because the inspector reaches every local
|
||
of a stopped frame and a frame holding one has to render rather than refuse. A
|
||
<code>Map</code> is what still refuses by name.</p>
|
||
|
||
<p>The thunk's module is unloaded afterwards. Nothing points into its text once it has
|
||
returned. Sixteen expression evaluations retain zero mappings, where each redefinition
|
||
retains three, permanently and correctly.</p>
|
||
|
||
<h2 id="emacs">Emacs</h2>
|
||
|
||
<p><code>emacs/flan-mode.el</code> derives from <code>prog-mode</code> with
|
||
<code>lisp-mode</code>'s syntax table, so sexp motion, paren matching and indentation are
|
||
already right. It adds Flan's brackets — <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.</p>
|
||
|
||
<h3>Setting up</h3>
|
||
|
||
<p>Put <code>emacs/</code> on your load path and require the mode. Nothing else is needed:
|
||
the client, the REPL, the inspector and the conditions buffer all load on first use.</p>
|
||
|
||
<pre><code class="sh">(add-to-list 'load-path "~/Development/flan/emacs")
|
||
(require 'flan-mode)
|
||
(require 'flan-dape) ; optional — lldb, and the only thing that binds C-c C-g</code></pre>
|
||
|
||
<p><code>flan-dape.el</code> is separate on purpose, so <code>flan-mode</code> works
|
||
without dape installed. You also need <code>flan</code> on your <code>PATH</code>, or
|
||
<code>flan-dev-command</code> pointed at it.</p>
|
||
|
||
<h3>Starting a program</h3>
|
||
|
||
<p><code>M-x flan-dev</code> runs <code>flan dev</code> on a file, waits for it to come up
|
||
and connects. <kbd>C-c C-z</kbd> attaches to one that is already running, looking for
|
||
<code>.flan-dev.sock</code> upward from the buffer, so from anywhere in the project it
|
||
finds the one program you have going. <kbd>C-c C-q</kbd> disconnects and leaves it
|
||
running; <code>M-x flan-dev-quit</code> stops it, but only one this Emacs started — a
|
||
daemon you launched in a terminal is not Emacs' to kill, and it says so rather than doing
|
||
something surprising.</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>Key</th><th>Does</th></tr>
|
||
<tr><td><kbd>C-c C-c</kbd></td><td>the top-level form at point, recompiled and installed</td></tr>
|
||
<tr><td><kbd>C-u C-c C-c</kbd></td><td>…and mark it, so the program stops at the form point is inside (<kbd>C-u C-u</kbd>: on entry)</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-u C-x C-e</kbd></td><td>…and stop <em>at</em> it instead of printing its value</td></tr>
|
||
<tr><td><kbd>C-c C-z</kbd> / <kbd>C-c C-q</kbd></td><td>connect (finds <code>.flan-dev.sock</code> upward) / disconnect</td></tr>
|
||
<tr><td><kbd>C-c C-o</kbd></td><td>the running program's own output, in <code>*flan-output*</code></td></tr>
|
||
<tr><td><kbd>C-c C-r</kbd></td><td>a prompt on the running program (<code>*flan-repl*</code>)</td></tr>
|
||
<tr><td><kbd>C-c C-b</kbd></td><td>a stopped program: the condition, the restarts, the stack</td></tr>
|
||
<tr><td><kbd>C-c C-M-b</kbd></td><td>the same restarts, as a one-key prompt</td></tr>
|
||
<tr><td><kbd>C-c C-i</kbd></td><td>inspect a value, navigating into its fields</td></tr>
|
||
<tr><td><kbd>C-c C-m</kbd></td><td>what the macro call at point expands to, one step; <kbd>C-u</kbd> first for all the way</td></tr>
|
||
<tr><td><kbd>C-c C-a</kbd></td><td>disassemble a function; <kbd>C-u</kbd> first for its LLVM IR</td></tr>
|
||
<tr><td><kbd>C-c C-g</kbd></td><td>debug under lldb, through dape — bound only once <code>flan-dape.el</code> is loaded, so <code>flan-mode</code> works without dape installed</td></tr>
|
||
<tr><td><kbd>C-c C-d</kbd></td><td>what the running program currently defines</td></tr>
|
||
<tr><td><kbd>C-c C-v</kbd></td><td>help on the name at point</td></tr>
|
||
<tr><td><kbd>C-c C-x</kbd></td><td>rebuild, relaunch and reconnect</td></tr>
|
||
<tr><td><kbd>M-.</kbd> / <kbd>M-,</kbd></td><td>where a name is written, through an <code>xref</code> backend</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p>Some commands have no key. <code>M-x flan-dev</code> starts a program and
|
||
<code>M-x flan-dev-quit</code> stops it. <code>M-x flan-watch</code> opens the watch
|
||
buffer, which shows values <em>while the program runs</em> rather than while it is
|
||
stopped, and <code>M-x flan-watch-ghost-mode</code> shows the same values inline at the
|
||
call that wrote each one. <code>M-x flan-inspect-address</code> roots an inspection at a
|
||
raw address rather than at an expression. <code>M-x flan-allocations</code> and
|
||
<code>M-x flan-leaks</code> read the allocation registry: every block it recorded grouped
|
||
by type, and the same walk with the dead left out.</p>
|
||
|
||
<p><kbd>C-c C-k</kbd> sends one module rather than a form at a time on purpose: a
|
||
<code>defvar</code> and the function that uses it have to arrive in the same load, or the
|
||
first refers to storage that does not exist yet.</p>
|
||
|
||
<p><kbd>C-x C-e</kbd> compiles the expression before point and runs it <em>inside</em> the
|
||
running program — the actual process, with its actual state, not a copy. Both it and
|
||
<kbd>C-c C-c</kbd> work on buffer text rather than the saved file, and a change installs at
|
||
the next frame boundary.</p>
|
||
|
||
<h3>The buffers, and their own keys</h3>
|
||
|
||
<p>Three buffers have keymaps of their own. The conditions buffer
|
||
(<kbd>C-c C-b</kbd>) shows the condition, then the restarts, then the stack — in that
|
||
order, because the decision in front of you is which restart to take and the stack is only
|
||
the explanation for it.</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>Buffer</th><th>Keys</th></tr>
|
||
<tr><td>conditions</td>
|
||
<td><kbd>RET</kbd> take the restart at point · <kbd>0</kbd>–<kbd>9</kbd> take one by number ·
|
||
<kbd>TAB</kbd>/<kbd>n</kbd>, <kbd>S-TAB</kbd>/<kbd>p</kbd> move ·
|
||
<kbd>f</kbd> fold a frame · <kbd>i</kbd> inspect a local ·
|
||
<kbd>a</kbd> abort · <kbd>g</kbd> re-read · <kbd>q</kbd> close</td></tr>
|
||
<tr><td>inspector</td>
|
||
<td><kbd>RET</kbd> into the field at point · <kbd>l</kbd> back out ·
|
||
<kbd>g</kbd> re-read · <kbd>TAB</kbd>/<kbd>n</kbd>, <kbd>S-TAB</kbd>/<kbd>p</kbd> move ·
|
||
<kbd>q</kbd> close</td></tr>
|
||
<tr><td>repl</td>
|
||
<td><code>comint</code>, plus <kbd>C-c C-o</kbd>, <kbd>C-c C-d</kbd> and <kbd>C-c C-q</kbd></td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p>A restart is taken by <em>position</em>, which is why the list is numbered: a name
|
||
resolves to the innermost frame offering it, so an outer <code>retry</code> shadowed by an
|
||
inner one is real, is on the list, and cannot be reached by name. One that genuinely cannot
|
||
be taken is drawn and refused with the reason rather than quietly omitted.</p>
|
||
|
||
<p>The inspector is unlike most: the view is never stale, because every step re-reads the
|
||
program as it is now. The cost is that <strong>the root expression runs again on every
|
||
step</strong> — going into a field sends <code>(.pos b)</code> where the last one sent
|
||
<code>b</code>, which is harmless, but inspecting <code>(spawn-enemy)</code> spawns one per
|
||
keystroke. That is why there is no auto-refresh and why <kbd>g</kbd> is a key you press.</p>
|
||
|
||
<p>The repl is program-scoped rather than buffer-scoped, so in sand you write
|
||
<code>sim/settle</code> and not <code>settle</code>. <code>*flan-output*</code> is separate
|
||
from it, because the program's stdout belongs to the program.</p>
|
||
|
||
<h3>When a change is refused</h3>
|
||
|
||
<p>Two different things wear the same refusal today, and only one of them is the design.</p>
|
||
|
||
<p><strong>A changed signature is a placeholder refusal.</strong> The intended behaviour,
|
||
and what <a href="#reading">plan.org</a> specifies, is that a signature change makes a new
|
||
version of the function: new callers resolve it, existing callers and any stored
|
||
<code>Fn</code> value stay safely on the old one, and the session <em>warns</em> at each
|
||
tracked stale caller site so you know what to re-evaluate. Nothing should have to restart.
|
||
That needs function versions, trampolines and caller tracking, none of which are built yet
|
||
— so until they are, the session refuses rather than letting a cell hand old arguments
|
||
to a new body. The refusal is a limitation with a date on it, not a rule.</p>
|
||
|
||
<p><strong>A changed struct layout is the genuinely hard case</strong>, and is rejected
|
||
while live values of that struct exist: storage already allocated has the old shape, and a
|
||
new body would read its fields at the wrong offsets with nothing to say so. plan.org keeps
|
||
this one as a rejection, and gives managed classes an explicit migration at a frame
|
||
boundary as the eventual way through.</p>
|
||
|
||
<p><kbd>C-c C-x</kbd> rebuilds, relaunches and reconnects, and is the way out while the
|
||
above is true. It costs the program's state, which is why it is a key you press rather than
|
||
something <kbd>C-c C-c</kbd> falls back to.</p>
|
||
|
||
<h3>Around the edges</h3>
|
||
|
||
<p>eldoc, completion and <kbd>M-.</kbd> all read one cached reply rather than asking per
|
||
keystroke, refreshed at the two moments the answer can have changed: on connect, and after
|
||
an evaluation the daemon accepted. The modeline says whether there is a program on the
|
||
other end, and says <code>stopped</code> when there is one sitting in the break loop — a
|
||
stopped program looks exactly like a running one from anywhere else in Emacs.</p>
|
||
|
||
<p>An error comes back with a location and the client draws an overlay there, cleared the
|
||
next time that buffer's evaluation is accepted. Under <kbd>C-c C-g</kbd> lldb needs no
|
||
plugin to read a Flan value, since a struct is its C struct; locals show under their real
|
||
names, and a shadowed one appears as <code>v~2</code> while plain <code>v</code> still
|
||
answers with the outer binding.</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>Setting</th><th>Default</th><th>What it is</th></tr>
|
||
<tr><td><code>flan-dev-command</code></td><td><code>"flan"</code></td><td>the compiler binary</td></tr>
|
||
<tr><td><code>flan-dev-socket-name</code></td><td><code>".flan-dev.sock"</code></td><td>what <kbd>C-c C-z</kbd> searches for</td></tr>
|
||
<tr><td><code>flan-dev-echo-result</code></td><td><code>t</code></td><td>print <kbd>C-x C-e</kbd>'s value in the echo area</td></tr>
|
||
<tr><td><code>flan-dev-names-shown</code></td><td><code>4</code></td><td>how many names to list before counting them</td></tr>
|
||
<tr><td><code>flan-dev-output-buffer</code></td><td><code>"*flan-output*"</code></td><td>where the program's output goes</td></tr>
|
||
<tr><td><code>flan-dev-poll-interval</code></td><td><code>1.0</code></td><td>seconds between checks for whether it stopped</td></tr>
|
||
<tr><td><code>flan-dev-start-timeout</code></td><td><code>60</code></td><td>seconds to wait for a program to come up</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="panel" id="panel-builds">
|
||
|
||
<h2 id="targets">Targets and builds</h2>
|
||
|
||
<p>Native x86-64 is the development target. <code>--target=wasm32-wasi</code> produces a
|
||
module, and the headless sand acceptance program prints the same 64-bit hash under it as
|
||
it does natively:</p>
|
||
|
||
<pre><code class="sh">$ flan run test/programs/sand-headless.flan
|
||
15595743031174623232
|
||
$ flan build test/programs/sand-headless.flan --target=wasm32-wasi -o sand.wasm
|
||
$ node --no-warnings test/wasm-run.mjs sand.wasm
|
||
15595743031174623232</code></pre>
|
||
|
||
<p>The RNG is written in Flan rather than called from libc for that number: a grid hash
|
||
is only a regression test if the sequence is byte-identical on both targets. It holds at
|
||
<code>-O2</code> and at <code>-O0</code>. The wasm side needs a
|
||
<code>wasm32</code> builtins archive — from wasi-sdk, or emscripten's substituting for
|
||
it — and the compiler names every path it looked in when it cannot find one.</p>
|
||
|
||
<p>Dev and release builds are deliberately different. <code>--dev</code> means
|
||
indirection cells and <code>-rdynamic</code>, which exports those cells for a loaded
|
||
module to bind to. Release builds call directly, emit constants as constants, and get all
|
||
the folding back. Dev builds are not pruned by reachability: what a REPL may redefine
|
||
next is not a function of what has been called so far.</p>
|
||
|
||
<p><strong>There is a second backend, and it is off by default.</strong>
|
||
<code>flan build --x86</code> lowers the checked program to x86-64 by hand —
|
||
<code>lib/x86.ml</code>, writing an assembly file directly — instead of going through
|
||
LLVM. It is the <em>dev</em> backend: LLVM stays the default and stays the release path,
|
||
and the two never meet in one process, which is what lets the hand-written one pick its
|
||
own internal calling convention (every aggregate by pointer, no eightbyte rule, no
|
||
classifier) and match SysV only at the C boundary, where the shim has already flattened
|
||
every struct.</p>
|
||
|
||
<p>It <strong>refuses by name</strong> anything it does not lower, so a build that
|
||
succeeds is one it really compiled rather than one it half-compiled. Conditions were the
|
||
visible gap once and are not any more: the transfer channel, the guard after every call,
|
||
bounds and arithmetic failures, indirection cells, redefinition modules and DWARF line
|
||
tables all landed, and what is left refused is narrow — an aggregate crossing the C
|
||
boundary is the one worth naming, because closing it would mean the eightbyte classifier
|
||
this backend is built on not having.</p>
|
||
|
||
<p>What holds it honest is that every program in the corpus is built both ways and the
|
||
two are compared byte for byte on stdout, stderr and exit status — not on a disassembly,
|
||
which has read perfectly beside a wrong answer more than once. <code>spike/x86/survey.sh</code>
|
||
is the script, and it currently reports <strong>103 MATCH, 0 DIFFER, 0 refused by
|
||
name</strong>, with 38 programs skipped because they do not compile on either side, have
|
||
no <code>main</code>, or run forever. <code>dune build @x86</code> runs it as part of the
|
||
build, so a refusal cannot sit unnoticed.</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. Both backends emit it, though not the same
|
||
amount: the hand-written one writes a compile unit, a subprogram per function and a line
|
||
table out as bytes, because <code>.loc</code> cannot work against a file whose
|
||
instructions are <code>.byte</code> blobs, so <code>--x86 --debug</code> gives a
|
||
backtrace naming Flan files, functions and lines while <code>print x</code> says the name
|
||
is not in the current context.</p>
|
||
|
||
<p>Some things are refused by name rather than half-supported, and both cross-target
|
||
refusals say why:</p>
|
||
|
||
<pre><code class="sh">$ flan run hello.flan --target=wasm32-wasi
|
||
flan run: --target is refused — a cross-built module is not something this host
|
||
can exec. Use flan build --target=... and a wasm runtime.
|
||
|
||
$ flan build hello.flan --dev --target=wasm32-wasi
|
||
wasm32: --dev is native only — the reload path is dlopen, which wasm32 has no
|
||
equivalent of</code></pre>
|
||
|
||
<p>Build time for <code>calc-me.flan</code> is about 110ms, of which the frontend — read,
|
||
parse, load, check, emit — is under 10ms. Every C translation unit goes through an object
|
||
cache keyed by a digest of the source text, the compiler, the optimisation level and the
|
||
target flags, so it never needs invalidating by hand.</p>
|
||
|
||
<h2 id="notyet">Not implemented yet</h2>
|
||
|
||
<p>The house rule is that anything which binds a name, alters control flow, or is not
|
||
yet implemented must be recognised explicitly and rejected. Each of these refuses by
|
||
name, with the milestone it belongs to, and the tests assert on the reason.</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>You write</th><th>The compiler says</th></tr>
|
||
<tr><td><code>(Result T E)</code></td><td>(Result T E) 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><code>'sym</code> as a value</td><td>a quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org)</td></tr>
|
||
<tr><td>a bare lowercase type name</td><td>generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)</td></tr>
|
||
<tr><td><code>errdefer</code></td><td>errdefer is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||
<tr><td><code>await</code></td><td>await is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||
<tr><td><code>handler-case</code></td><td>handler-case is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||
<tr><td><code>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||
<tr><td>a union as a <code>declare</code> parameter</td><td>a parameter of g is U, which cannot cross to C directly — pass (Ptr U) and let the shim read it</td></tr>
|
||
<tr><td>a user-written allocator</td><td>a user-written allocator is not implemented yet, and a defn's name in that position …</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p>Two of those rows want reading carefully. <strong>A quoted symbol works where a
|
||
restart is named</strong> — <code>(invoke-restart 'use-value 21)</code> is the ordinary
|
||
spelling — and is refused only as a value in its own right, because there is no symbol
|
||
type to give it. And <strong>the type-variable row is about the sigil</strong>: bare
|
||
<code>a</code> in type position is not a type variable and never became one;
|
||
<code>$t</code> is, and <a href="#generics">generics</a> is where it is written down.</p>
|
||
|
||
<p>Beyond that list, and just as true: there is no overloading; <code>defer</code> is
|
||
refused inside a loop or a branch (a <code>let</code> is fine — it has the function's
|
||
extent); <code>find-restart</code> and <code>compute-restarts</code> are blocked on a
|
||
<code>Restart</code> type rather than on effort; a restart with parameters cannot be
|
||
taken from the break loop, which aims at a frame by position and has nothing to fill them
|
||
with; 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>One of these is settled rather than pending. There is <strong>no interpreter</strong>
|
||
and there is not going to be one: compiling is the only way a form is ever run. The
|
||
instrumentation-based step debugger that wanted one is cut, and compiled redefinition at
|
||
~19ms is perceptually instant for expression evaluation too. That is also how macros run
|
||
— a <code>defmacro</code> is compiled into a shared object and dlopened into the
|
||
compiler before the file that calls it is expanded, so there is no second evaluator to
|
||
disagree with the first.</p>
|
||
|
||
<h2 id="reading">Further reading</h2>
|
||
|
||
<p>The repository's own documents, in the order they are worth reading:</p>
|
||
|
||
<ul>
|
||
<li><code>plan.org</code> — the design, the build sequence, and the open decisions.</li>
|
||
<li><code>NEXT.md</code> — the project's memory, and the authority on what is actually
|
||
built.</li>
|
||
<li><code>spec-memory.md</code> — ownership, containers, places, generics, function
|
||
values. Frozen.</li>
|
||
<li><code>spec-conditions.md</code> — conditions and restarts, operational semantics.
|
||
Frozen.</li>
|
||
<li><code>conditions.org</code> — a cheatsheet for driving conditions.</li>
|
||
<li><code>calc-me.flan</code>, <code>sand.flan</code>, <code>test/programs/</code> —
|
||
real programs that run.</li>
|
||
</ul>
|
||
|
||
</div>
|
||
|
||
<footer>
|
||
<p>Flan is a custard. This page describes the compiler on branch
|
||
<code>dev-loop</code>; where a document and the compiler disagree, the compiler is what
|
||
is written here.</p>
|
||
</footer>
|
||
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
// Tabs. The panels are plain markup and stay visible until this runs, so the
|
||
// page without a script is the one long document it was before. A link into a
|
||
// panel that is not showing switches to it first -- every #anchor on the page,
|
||
// and every link from outside, has to keep working.
|
||
(function () {
|
||
var main = document.querySelector("main");
|
||
var bar = document.querySelector(".tabs");
|
||
if (!main || !bar) return;
|
||
var tabs = Array.prototype.slice.call(bar.querySelectorAll("button"));
|
||
var panels = tabs.map(function (t) { return document.getElementById(t.dataset.panel); });
|
||
if (panels.some(function (p) { return !p; })) return; // markup moved; leave it long
|
||
main.classList.add("tabbed");
|
||
|
||
function show(i, focus) {
|
||
tabs.forEach(function (t, j) {
|
||
t.setAttribute("aria-selected", j === i ? "true" : "false");
|
||
panels[j].hidden = j !== i;
|
||
});
|
||
if (focus) tabs[i].focus();
|
||
}
|
||
|
||
function panelOf(id) {
|
||
var el = id && document.getElementById(id);
|
||
while (el && el !== document.body) {
|
||
if (el.classList && el.classList.contains("panel")) return el.id;
|
||
el = el.parentElement;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function toHash() {
|
||
var want = panelOf(location.hash.slice(1));
|
||
if (!want) return false;
|
||
var i = panels.findIndex(function (p) { return p.id === want; });
|
||
if (i < 0) return false;
|
||
show(i);
|
||
var t = document.getElementById(location.hash.slice(1));
|
||
if (t) t.scrollIntoView();
|
||
return true;
|
||
}
|
||
|
||
tabs.forEach(function (t, i) {
|
||
t.addEventListener("click", function () { show(i); });
|
||
// Left and right move between tabs, which is what a tablist is expected
|
||
// to do once it says it is one.
|
||
t.addEventListener("keydown", function (e) {
|
||
var d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
|
||
if (!d) return;
|
||
e.preventDefault();
|
||
show((i + d + tabs.length) % tabs.length, true);
|
||
});
|
||
});
|
||
|
||
window.addEventListener("hashchange", toHash);
|
||
if (!toHash()) show(0);
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
// A small hand-written highlighter for the Flan blocks. One pass, no library.
|
||
(function () {
|
||
var FORMS = new Set(("defn defstruct defenum defunion defconst defvar defalias " +
|
||
"declare declare-c import package let if when unless cond do and or not while " +
|
||
"until dotimes match set return some try defer signal error handler-bind " +
|
||
"restart-case invoke-restart fn quote defmacro gensym").split(" "));
|
||
// Capitalised names are types; these are the ones that are not capitalised.
|
||
var TYPES = new Set(("i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 bool string").split(" "));
|
||
var TOKEN = /(;[^\n]*)|("(?:\\.|[^"\\])*")|(\\[A-Za-z0-9]+)|(:[A-Za-z][\w?!*<>=+-]*)|(\b0[xX][0-9a-fA-F]+\b|\b\d[\d.]*\b)|([A-Za-z][\w*?!<>=./+-]*)/g;
|
||
function esc(s) {
|
||
return s.replace(/&/g, "&").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>
|