A dev cell carries its body's signature word beside the body, every call through a cell (and every function value taken from one) compares it with the word the site was compiled for, and the session lists the stale callers by file and line on the reply. Both backends, both installers; release builds have neither the word nor the compare.
2529 lines
126 KiB
HTML
2529 lines
126 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="#dyn">dyn</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, and a collector only <code>dyn</code> values reach.</p>
|
||
</header>
|
||
|
||
<p class="lede">Flan compiles s-expressions to LLVM IR and then to a native binary.
|
||
A typed value carries no header, so a Flan struct is exactly its C struct, and a
|
||
program that writes <code>dyn</code> nowhere has no collector in it — 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 written at function boundaries and inferred everywhere else, and a
|
||
parameter left unwritten is <a href="#dyn"><code>dyn</code></a>; 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> No persistent maps or vectors
|
||
and no structure sharing. Value structs that copy on assignment replace them.</li>
|
||
<li><strong>Not dynamically typed by default.</strong> A type is written or
|
||
inferred, and a value of one carries no tag. <code>dyn</code> is the opt-in, and
|
||
the tag and the collector are what it costs — see <a href="#dyn">dyn</a>.</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 check <file.flan>... [--warn-memory]
|
||
flan emit <file.flan> [--x86] [--dev] [--debug] [--no-bounds-checks]
|
||
flan import-c <header.h> [package.flan...] [clang flags...]
|
||
flan generate-c <package-dir>
|
||
flan build <file.flan> [-o out] [-O0|-O1|-O2|-O3] [--no-bounds-checks] [--dev] [--debug] [--sanitize] [--x86] [--warn-memory] [--target=wasm32-wasi|web|js]
|
||
flan run <file.flan> [build flags...] [--] [program args...]
|
||
flan reload <program.flan> <forms.flan> [-o out.so] [--x86]
|
||
flan dev <program.flan> [-s socket] [--x86]</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><code>flan emit --x86</code> prints the hand-written backend's assembly where plain
|
||
<code>emit</code> prints LLVM IR, and it is <em>annotated</em>: each run of bytes is
|
||
headed by the Flan form that produced it and its source position, and each function by a
|
||
map of its frame saying which displacement is which parameter, which is a named local,
|
||
and below which offset everything is a reusable temporary. Reading a listing of this
|
||
backend without that map is reading <code>-0x20(%rbp)</code> with no key. The comments
|
||
cost nothing in the object — the assembler discards them — but they are
|
||
emitted only by <code>emit</code>, never by a build, so the assembly a build hands to
|
||
<code>clang</code> is the same text it always was. <code>spike/x86/dump.sh</code> puts all
|
||
four lowerings of one function side by side, which is what <kbd>C-c C-l</kbd> shows in
|
||
the editor.</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 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. The one value that is not laid out this
|
||
way is a <code>dyn</code>, which is allocated by the runtime and collected; writing
|
||
the type is how a program asks for that, and <a href="#dyn">dyn</a> is where it is
|
||
described.</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])
|
||
|
||
(defonce spawned i32)
|
||
(defconst room-size 4)
|
||
(defonce 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 defonce
|
||
(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.
|
||
(defonce 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.
|
||
(defonce 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 — copies as its header, so the copies alias one buffer</td><td>ptr + len + cap + its allocator</td></tr>
|
||
<tr><td><code>(Map K V)</code></td><td>open addressing, owning, copies the same way. 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, copies the same way</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, which may have captured</td><td>a code address and an environment pointer</td></tr>
|
||
<tr><td><code>(CFn [T ...] R)</code></td><td>a function value that cannot capture — the <code>C</code> is what a C function pointer would need, not a way to reach C today</td><td>a pointer</td></tr>
|
||
<tr><td><code>dyn</code></td><td>a value the runtime knows the type of and the checker does not — see <a href="#dyn">dyn</a></td><td>one word, on a collected heap</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 tagged data type</td><td><code>defdata</code>, matched by case</td><td>tag + the widest payload</td></tr>
|
||
<tr><td>an untagged union</td><td><code>defunion</code>, C's: read any member, no tag</td><td>the widest member, at the strictest alignment</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>A conversion that cannot change the number is implicit; any other is
|
||
written.</strong> An <code>i32</code> goes where an <code>i64</code> or an <code>f64</code>
|
||
is wanted, and an <code>f32</code> where an <code>f64</code> is. An <code>i64</code> into an
|
||
<code>i32</code>, or an <code>i32</code> into an <code>f32</code>, is an error until it is
|
||
written as a cast:</p>
|
||
|
||
<pre><code>(defn main [] i32
|
||
(let [n 40 ; i32, inferred
|
||
big (i64 n) ; a cast is a call named for the type
|
||
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>
|
||
|
||
<h3>Operators take a run of operands</h3>
|
||
|
||
<p>Two operands is the floor and there is no ceiling.
|
||
<code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>min</code>,
|
||
<code>max</code> and the three bitwise combining operators fold left over as many
|
||
arguments as they are given. <code>%</code> and the shifts are not in that set and
|
||
take two: a chain of remainders or of shifts has no reading agreed on in advance, so
|
||
there an arity error is the useful answer. The orderings — <code><</code>, <code><=</code>, <code>></code>,
|
||
<code>>=</code> — <em>chain</em>: each neighbouring pair is compared and every
|
||
pair has to hold, so <code>(< 1 2 3)</code> asks whether the run is increasing.
|
||
<code>=</code> chains the same way. <code>!=</code> does not: it asks whether the
|
||
operands are <strong>all distinct</strong>, comparing every pair rather than the
|
||
neighbouring ones, so <code>(!= 1 2 1)</code> is false.</p>
|
||
|
||
<pre><code>;; The arithmetic operators and the comparisons both take a run of operands.
|
||
;; The orderings chain: each neighbouring pair is compared, and every pair has
|
||
;; to hold. != is the one that does not chain — it asks whether the operands
|
||
;; are all distinct, so a value repeated anywhere in the run makes it false.
|
||
(defn main [] ()
|
||
(println (+ 1 2 3 4))
|
||
(println (< 1 2 3))
|
||
(println (< 1 3 2))
|
||
(println (= 4 4 4))
|
||
(println (!= 1 2 3))
|
||
(println (!= 1 2 1)))</code></pre>
|
||
|
||
<pre><code class="sh">10
|
||
true
|
||
false
|
||
true
|
||
true
|
||
false</code></pre>
|
||
|
||
<p><strong>Why the two differ.</strong> "Is this sequence increasing" and "are these
|
||
values all different" are the two questions a run of operands is actually asked, and
|
||
they need different comparisons. Chaining <code>!=</code> would answer neither: it
|
||
would be true of <code>1 2 1</code>, which is not a run of distinct values.</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="dyn">dyn</h2>
|
||
|
||
<p>A <code>dyn</code> is one machine word whose contents the runtime knows and the
|
||
type checker does not. Every other type on this page is decided where it is written;
|
||
this one is decided when the program runs, and the runtime keeps a tag beside the
|
||
value to decide it with.</p>
|
||
|
||
<p>Writing <code>dyn</code> is how a value opts in. It is written like any other type
|
||
— a parameter's type, a return type, a global's type — and a parameter left with no
|
||
type written is a <code>dyn</code>, which is the shortest way to ask for one. Those
|
||
values, and only those, live on a collected heap. A program that writes it nowhere
|
||
pays nothing: no tag, no allocation, no collector.</p>
|
||
|
||
<pre><code>;; Writing dyn is how a value opts in. The parameter has no type written, so
|
||
;; it is a dyn; the return type says dyn too, and both ends of twice are then
|
||
;; a word the runtime knows the contents of and the checker does not.
|
||
(defn twice [x] dyn
|
||
(+ x x))
|
||
|
||
;; A numeric cast opens the box. (i64 d) compiles for any dyn d, and what the
|
||
;; box actually holds is answered when the program runs.
|
||
(defn main [] ()
|
||
(println (twice 21))
|
||
(println (twice 1.5))
|
||
(let [d (twice 21)]
|
||
(println (+ (i64 d) 1))))</code></pre>
|
||
|
||
<pre><code class="sh">42
|
||
3
|
||
43</code></pre>
|
||
|
||
<p>One <code>twice</code> is compiled, not one per argument type, and the
|
||
<code>+</code> in it is the runtime's addition over two tagged words. The second
|
||
call prints <code>3</code> rather than <code>3.0</code> by the whole-number rule
|
||
in <a href="#types">types</a>, not by anything <code>dyn</code> does.</p>
|
||
|
||
<p>The return type is always written, <code>dyn</code> included. The parameter vector
|
||
is where the types may be left out, and a vector of bare names is read as names: in
|
||
<code>(defn f [x y] ...)</code>, <code>y</code> is a second <code>dyn</code>
|
||
parameter when nothing declares a type called <code>y</code>, and is
|
||
<code>x</code>'s type when something does. The pairing is decided after every file is
|
||
loaded and every macro has expanded, so the set of type names it consults is the
|
||
whole set — but it is the set at a point in time, and writing a
|
||
<code>(defstruct y ...)</code> elsewhere changes <code>f</code>'s signature with no
|
||
edit to <code>f</code>.</p>
|
||
|
||
<p>A global left without a type is a <code>dyn</code> too:
|
||
<code>(def x 5)</code> declares one and <code>(def x i32 5)</code> does not. See
|
||
<a href="#functions">functions</a> for the three global forms.</p>
|
||
|
||
<h3>A cast opens the box</h3>
|
||
|
||
<p>A numeric cast is how a <code>dyn</code> comes back to a type the checker can
|
||
see. <code>(i64 d)</code> compiles for any <code>dyn</code> <code>d</code>, because
|
||
the question of what the box holds is not one the checker can answer; the question
|
||
moves to run time, where <code>flan_dyn_cast_kind</code> answers it. A box holding
|
||
something the cast cannot take stops the program where it happened, with the file
|
||
and the line, the way a bad index does.</p>
|
||
|
||
<h3>--no-gc</h3>
|
||
|
||
<p><code>flan build --no-gc</code> names every <code>dyn</code> in the program, with
|
||
its location, and refuses to build. That is how a program has "this carries no
|
||
collector" checked rather than believed. It is a refusal and not a different
|
||
lowering: nothing downstream is told the flag was given, so the output of a build
|
||
that passes it is byte for byte the output of a build that does not.</p>
|
||
|
||
<pre><code class="sh">$ flan build dyn.flan --no-gc
|
||
dyn.flan:4:7: parameter 1 of twice holds a dyn, and --no-gc says this program
|
||
carries no collector. A dyn value is one the runtime allocates and the collector
|
||
owns, so there is nothing smaller to compile it to — write the type</code></pre>
|
||
|
||
<p>The check runs before reachability, so a <code>dyn</code> in a function nothing
|
||
calls is still a <code>dyn</code> somebody wrote. A refusal that came and went as the
|
||
program was edited elsewhere would not be worth having.</p>
|
||
|
||
<h3>Maps, vectors and keywords</h3>
|
||
|
||
<p>Braces and brackets in expression position write dyn literals:
|
||
<code>{:a 1 :b "two"}</code> is a dyn map and <code>[1 2 3]</code> is a dyn vector.
|
||
<code>get</code>, <code>put</code>, <code>has-key?</code>, <code>at</code> and
|
||
<code>length</code> read and write them, the same names the typed
|
||
<code>Map</code> and <code>Vec</code> answer to. A keyword is a value here rather
|
||
than only a way to name an enum member: keywords are interned, so comparing two is
|
||
comparing two pointers.</p>
|
||
|
||
<p>The absent dyn value is written <code>nil</code>. It is not <code>()</code> —
|
||
unit carries nothing for a dyn word to hold, and boxing it is refused — and
|
||
<code>(Some nil)</code> cannot be built, because a present absence would make
|
||
<code>nil</code> and <code>None</code> the same case of an
|
||
<code>(Option dyn)</code>.</p>
|
||
|
||
<h3>Classes and generic functions</h3>
|
||
|
||
<p>A class is a named dyn map with a shape tag. <code>defclass</code> names its
|
||
slots, which carry no types; the constructor is the class's own name and is
|
||
positional; and <code>class-of</code> answers the tag, or <code>nil</code> for
|
||
anything that is not an instance. The slots are map keys, so nothing was added to
|
||
read or write one.</p>
|
||
|
||
<p>Dispatch comes in the two styles and they are one mechanism.
|
||
<code>defgeneric</code> dispatches on the class of the first argument, which is
|
||
CLOS's rule. <code>defmulti</code> takes a body whose value is the dispatch value,
|
||
which is Clojure's. Either way a <code>defmethod</code> names the value it answers
|
||
for — a class name, a keyword, a string, an integer, <code>true</code>,
|
||
<code>false</code>, or <code>:else</code> for the arm everything falls through to,
|
||
which is last whatever order it was written in. The generic states the return type
|
||
once, for every method; a method has no return slot; and every parameter of both is
|
||
<code>dyn</code>, written or not.</p>
|
||
|
||
<pre><code>;; A class is a named dyn map with a shape tag. Its slots are names and
|
||
;; carry no types, and its constructor is the class's own name, positional.
|
||
(defclass point [x y])
|
||
(defclass circle [r])
|
||
|
||
;; CLOS-style: the generic states the return type once, and each method
|
||
;; dispatches on the class of its first argument.
|
||
(defgeneric area [self] dyn)
|
||
(defmethod area point [p] (* (get p :x) (get p :y)))
|
||
(defmethod area circle [c] (* 3 (get c :r) (get c :r)))
|
||
|
||
;; Clojure-style: the generic's body is the dispatch value, and a method
|
||
;; names the value it answers for. :else is the arm everything falls to.
|
||
(defmulti describe [x] dyn (get x :kind))
|
||
(defmethod describe :square [s] (get s :side))
|
||
(defmethod describe :else [s] "something else")
|
||
|
||
(defn main [] ()
|
||
(let [p (point 3 4)]
|
||
(println (area p))
|
||
(println (area (circle 2)))
|
||
;; A slot is a map key: get, put and has-key? are how one is read.
|
||
(println (get p :y))
|
||
;; class-of answers the tag, and nil for anything that is not an instance.
|
||
(println (class-of p))
|
||
(println (class-of 7))
|
||
(println p)
|
||
(println (describe {:kind :square :side 9}))
|
||
(println (describe {:kind :blob}))))</code></pre>
|
||
|
||
<pre><code class="sh">12
|
||
12
|
||
4
|
||
:point
|
||
nil
|
||
#point{ :x 3 :y 4}
|
||
9
|
||
something else</code></pre>
|
||
|
||
<p>An instance renders as <code>#point{ :x 3 :y 4}</code>, Clojure's spelling for a
|
||
record, and the tag is why two instances of one class compare by their slots while an
|
||
instance is never equal to a plain map with the same entries. A dispatch that matches
|
||
no method signals <code>NoMethod</code>, carrying the generic's name and the value
|
||
the dispatch produced. It is a condition rather than a trap because a miss is
|
||
something a program can be written to answer; no restart is established at it, which
|
||
is <code>BoundsError</code>'s decision taken for <code>BoundsError</code>'s
|
||
reason.</p>
|
||
|
||
<p><strong>A generic is one function.</strong> The method bodies are inlined into a
|
||
chain in its body rather than lifted into functions of their own, so adding a method
|
||
to a running program is the ordinary redefinition of one name, through the cell the
|
||
call site already goes through. A method does declare a name of its own —
|
||
<code>area@:circle</code> — which is what makes re-evaluating one a replacement and
|
||
evaluating a new one an append; no function is emitted under it.</p>
|
||
|
||
<p>The four forms are a pass over the whole declaration list rather than macros:
|
||
<code>lib/classes.ml</code> runs where <code>declare-c</code>'s shim generation runs.
|
||
A macro sees one form, and a method may be written above its generic, below it, or
|
||
arrive at a reload an hour later.</p>
|
||
|
||
<h3>Rationale: an explicit root stack</h3>
|
||
|
||
<p>The collector is mark-sweep, and it finds its roots from a shadow stack the
|
||
compiler pushes to rather than by scanning the C stack. Scanning is what a collector
|
||
normally does and it is cheaper to write, but it needs to know where the stack is and
|
||
what on it might be a pointer. wasm32 does not let a program look at its own call
|
||
stack at all, and the language builds for wasm32. An explicit root stack is the same
|
||
code on every target, which is what makes the collector work there.</p>
|
||
|
||
<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) (length (.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-view "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>There are two ways to see a string's bytes and the difference is whether anything
|
||
is allocated. <code>(bytes-view s)</code> is the string's own storage seen as a
|
||
<code>[u8]</code> and costs nothing; it aliases the string, so a literal's view points
|
||
into <code>.rodata</code> and writing through it traps. <code>(bytes s)</code> and
|
||
<code>(bytes s allocator)</code> make a writable copy through the allocator — never a
|
||
hidden <code>malloc</code>, which is the rule every allocating operation follows. The
|
||
example above wants a view and takes one.</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 three kinds:</p>
|
||
|
||
<div class="scroll">
|
||
<table>
|
||
<tr><th>Form</th><th>What it is</th><th>What a re-run does</th></tr>
|
||
<tr><td><code>defconst</code></td><td>a compile-time constant</td><td>nothing to do</td></tr>
|
||
<tr><td><code>defonce</code></td><td>storage, initialised once</td><td>keeps the value it has</td></tr>
|
||
<tr><td><code>def</code></td><td>storage</td><td>runs the initialiser again</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p>Each names its own type, and the type is what makes the pair legible: a
|
||
<code>defonce</code> is for state the program builds up and a <code>def</code> is for
|
||
a value the source decides, so re-evaluating a file leaves the first alone and stores
|
||
into the second. The three forms decide that between them; the dev session does not.
|
||
There is no <code>defvar</code> — the compiler catches the name and gives both
|
||
spellings rather than guessing which was meant.</p>
|
||
|
||
<p>A third element that is not a type is the value of a <strong>dyn</strong> global:
|
||
<code>(def x 5)</code> declares a <code>dyn</code>, and <code>(def x i32 5)</code>
|
||
declares an <code>i32</code>. The same holds for <code>defonce</code>.</p>
|
||
|
||
<pre><code>(defconst cell-size 5) ; a compile-time constant
|
||
(defconst gravity f32 0.05) ; with its type named
|
||
(defonce current-color i32) ; zeroed storage
|
||
(defconst rows 3)
|
||
(defconst cols 4)
|
||
(defonce 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>
|
||
|
||
<h3>Function values</h3>
|
||
|
||
<p>There are two function types and the difference between them is what a value of
|
||
each one <em>is</em>, not what it may do. <code>(Fn [T ...] R)</code> is a code
|
||
address and the environment it is called with: two words, and it may have captured.
|
||
<code>(CFn [T ...] R)</code> is the bare address: one word, no environment, and
|
||
therefore nothing that can capture.</p>
|
||
|
||
<p><code>fn</code> writes a function value inline. It takes its parameter and return
|
||
types from the position it is written in, so it goes in an argument slot whose
|
||
parameter names them; a bare <code>(let [f (fn [x] x)])</code> is refused saying so.
|
||
An <code>fn</code> sees the locals it was written among and copies the ones it uses,
|
||
and that is what makes it an <code>Fn</code>. A name declared by <code>defn</code>
|
||
captures nothing and fits either type.</p>
|
||
|
||
<pre><code>;; An Fn is {code, env}: it may have captured, and the caller neither knows
|
||
;; nor cares. A CFn is the bare code address, one word, and cannot capture.
|
||
(defn apply-fn [f (Fn [i32] i32) x i32] i32 (f x))
|
||
(defn apply-cfn [f (CFn [i32] i32) x i32] i32 (f x))
|
||
|
||
(defn bump [x i32] i32 (+ x 1))
|
||
|
||
(defn main [] ()
|
||
(let [n 10]
|
||
;; An fn takes its types from the position it is written in.
|
||
(println (apply-fn (fn [x] (+ x n)) 5)) ; captures n
|
||
(println (apply-fn bump 5)) ; a defn captures nothing
|
||
(println (apply-cfn bump 5)) ; so it fits either position
|
||
;; Widening is one way: a CFn goes where an Fn is wanted.
|
||
(println (apply-fn (fn [x] (* x 2)) 21))))</code></pre>
|
||
|
||
<pre><code class="sh">15
|
||
6
|
||
6
|
||
42</code></pre>
|
||
|
||
<p><strong>Widening is one way.</strong> A <code>CFn</code> is accepted where an
|
||
<code>Fn</code> is wanted — the environment word is filled in and nothing is lost.
|
||
The reverse cannot work: an <code>Fn</code> put into a <code>CFn</code> would have
|
||
nowhere to keep its captures. An <code>fn</code> written into a <code>CFn</code>
|
||
position is checked as an <code>Fn</code> and then refused if the finished body
|
||
turns out to capture, which is the only point at which that is decidable:</p>
|
||
|
||
<pre><code class="sh">this fn captures n, so it is a (Fn [i32] i32) and not a (CFn [i32] i32): a CFn
|
||
is the bare address, one word, with nowhere for the copies to live. Widen the
|
||
position to Fn, or pass n in as a parameter</code></pre>
|
||
|
||
<p><strong>Rationale: two types rather than one.</strong> A uniform environment would
|
||
tax every function in every program for something most of them never use. With two
|
||
types an ordinary <code>defn</code> keeps exactly the signature it always had, and
|
||
the <code>C</code> in <code>CFn</code> names where the one-word form is going: a
|
||
value with no environment is the only kind that could ever be a C function pointer.
|
||
It is not a capability that exists today — a <code>declare</code> cannot take a
|
||
function type at all, because a Flan signature ends with the transfer channel and a
|
||
C caller knows nothing about one.</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 (length 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 (length 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> takes one, two or three bounds:
|
||
<code>(dotimes [i n])</code> counts 0 to n-1, <code>(dotimes [i start stop])</code> counts
|
||
start to stop-1, and <code>(dotimes [i start stop step])</code> steps by step. The stop is
|
||
exclusive in every form — so <code>(dotimes [i 0 n])</code> is <code>(dotimes [i n])</code> —
|
||
and a negative step counts down: <code>(dotimes [i 9 -1 -1])</code> is 9 down to 0. A step
|
||
of 0 written as a literal is a compile error; one that is only known at run time runs the
|
||
loop no times.</p>
|
||
|
||
<p>Each bound is evaluated exactly once, before the loop and left to right — the start
|
||
into the counter itself, the stop into a hidden slot, and the step into one too unless it
|
||
is a literal. So a body that changes what a bound came from 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>defdata</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>length</code> works on a fixed array, a slice, a string, a <code>Vec</code> or a
|
||
<code>Map</code>.</p>
|
||
|
||
<p><code>slice</code> takes the view and never copies. It has three arities —
|
||
<code>(slice a)</code>, <code>(slice a lo)</code> and <code>(slice a lo hi)</code> —
|
||
and the short ones are written out into the long one, so nothing is added at run
|
||
time: <code>lo</code> is 0 and <code>hi</code> is the length, which an array already
|
||
folds to a constant and a slice or a string is carrying anyway. The range is
|
||
half-open. A string slices to a string rather than to a <code>[u8]</code>, because
|
||
the result views the same bytes and is read-only for the same reason the source
|
||
is.</p>
|
||
|
||
<p><strong>One name, because the input type decides the semantics.</strong> There
|
||
used to be a second, <code>as-slice</code>, for the <code>Vec</code> alone. A
|
||
<code>Vec</code> can only be borrowed and an array can only be viewed, and no call
|
||
site picks between the two, so the second name expressed nothing. Writing it now says
|
||
there is no <code>as-slice</code> and names the one that exists.</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.
|
||
(defonce grid [rows [cols i32]])
|
||
|
||
(defn main [] ()
|
||
(set (at grid 1 2) 7)
|
||
(print (at grid 1 2)) (println "") ; 7
|
||
(print (length 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 (equal? $t)}
|
||
(if (= (length s) 0) d (at s 0)))
|
||
|
||
(defn one-of [x $t] (Vec $t) ; bare t is the type-name argument
|
||
(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 (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 [(ordered? $t) (hashable? $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>integer?</code></td><td><code>bit-and</code> <code>bit-or</code> <code>bit-xor</code> <code><<</code> <code>>></code> — every integer type, no float</td></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>
|
||
</table>
|
||
</div>
|
||
|
||
<p>They entail each other in one direction, so one clause usually does:
|
||
<code>integer?</code> gives <code>numeric?</code>, <code>numeric?</code> gives
|
||
<code>ordered?</code>, and <code>ordered?</code> gives <code>equal?</code>. A
|
||
<code>sort</code> that compares its elements declares <code>ordered?</code> and
|
||
nothing else, and the prelude's <code>abs</code> declares <code>integer?</code>
|
||
alone — the bound is what keeps its integer body away from the floats, whose
|
||
<code>abs-f32</code>/<code>abs-f64</code> are libm's sign-bit clear.</p>
|
||
|
||
<p><strong>Every value copies.</strong> There used to be a fifth predicate,
|
||
<code>copyable?</code>, gating a second read of a move-only variable; the move
|
||
concept was repealed on 2026-09-18 — a container copies as its header, the
|
||
copies alias one buffer, and which free runs is the program's business, as it
|
||
is in Odin — so the predicate went with it.</p>
|
||
|
||
<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
|
||
typed 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. A <code>dyn</code> is the one
|
||
argument whose printing <em>is</em> decided at run time, because it is the one value
|
||
that carries a tag to decide it with.</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>, <code>dyn</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 for the types that do not
|
||
widen into one another. A printer that named one type, say <code>i64</code>, would take
|
||
an <code>i32</code> as it is but need a cast for a <code>u64</code> — and a
|
||
<code>u64</code> above 2<sup>63</sup> put through that cast 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 its <code>declare</code> lines, 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-in-place</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>, <code>abs</code> (generic over every integer width), the constants <code>pi-f32</code>, <code>pi-f64</code>, <code>tau-f32</code>, <code>tau-f64</code>, and libm through a <code>declare</code> at both widths: <code>sqrt</code>, <code>abs</code>, <code>floor</code>, <code>ceil</code>, <code>round</code>, <code>fmod</code>, <code>sin</code>, <code>cos</code>, <code>tan</code>, <code>asin</code>, <code>acos</code>, <code>atan</code>, <code>atan2</code>, <code>log</code>, <code>log2</code>, <code>log10</code>, <code>exp</code>, <code>pow</code>, <code>hypot</code>, <code>cbrt</code> — each spelled <code>-f32</code> or <code>-f64</code></td></tr>
|
||
<tr><td>time</td><td><code>monotonic-ns</code>, <code>monotonic-seconds</code>, <code>unix-ns</code>, <code>unix-seconds</code>, <code>sleep-ns</code>, <code>sleep-seconds</code>, and <code>ns-per-second</code> and its two smaller siblings</td></tr>
|
||
<tr><td>files</td><td><code>file-exists?</code> and <code>file-size</code>, which answer a value; <code>slurp</code>, <code>barf</code>, <code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code>, which signal <code>FileError</code> under <code>retry</code> and <code>use-value</code></td></tr>
|
||
<tr><td>the operating system</td><td><code>getenv</code>, which answers an <code>(Option [u8])</code> viewing the process environment</td></tr>
|
||
<tr><td>random</td><td><code>rand-seed</code>, <code>rand-int</code> (a <code>u64</code>), <code>rand</code> (an <code>f64</code> in [0, 1)), <code>rand-bool</code>, <code>rand-int-range</code> (an <code>i64</code>), <code>rand-float-range</code>. A seeded PRNG written in Flan, one draw per call, so the same seed gives the same numbers on every target. Reproducible and predictable are the same property: a holder of one result can work back to the state and know the rest. Right for a grid or a shuffle, wrong for a key</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>The maths in the file is not Flan, and it is libm's:
|
||
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code> and the same line for thirty-odd
|
||
more. 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.
|
||
A <code>declare</code> is also the cheapest thing in the language to add — a line, a
|
||
symbol already on the link, and nothing in either backend — which is why the surface is
|
||
now the whole family at both widths rather than the five it started as.</p>
|
||
|
||
<p><strong>One split is worth knowing before calling any of them.</strong> IEEE-754
|
||
specifies <code>sqrt</code>, <code>fabs</code>, <code>floor</code>, <code>ceil</code>,
|
||
<code>round</code> and <code>fmod</code> as exact or correctly rounded, so those give the
|
||
same bit pattern under glibc, musl and wasi-libc. <strong>It requires nothing of the
|
||
rest</strong> — <code>sin</code>, <code>cos</code>, <code>tan</code>, the inverses, the
|
||
logarithms, <code>exp</code>, <code>pow</code>, <code>hypot</code>, <code>cbrt</code> —
|
||
and the three libms do differ in the last bit, so the byte-identical-hash property the
|
||
RNG exists for does not survive a value routed through any of them. Every link carries
|
||
<code>-lm</code>.</p>
|
||
|
||
<p><strong>The clock is two clocks and they are not interchangeable.</strong>
|
||
<code>monotonic-ns</code> measures: it never goes backwards, nothing adjusts it, and its
|
||
zero is the first time the program reads it, so it is meaningless alone and correct as a
|
||
difference. <code>unix-ns</code> dates: nanoseconds since 1970, which is what goes in a
|
||
save file or a log line, and which can jump in either direction when the system clock is
|
||
set. Odin draws the same line — <code>Tick</code> against <code>Time</code> in
|
||
<code>core/time</code> — and the nanosecond integer is the primitive on both sides, with
|
||
the <code>-seconds</code> faces derived from it. The <code>f64</code> of seconds is the
|
||
shape raylib's <code>get-time</code> already answers with, so the two mix; it stays
|
||
integer-exact in nanoseconds for a hundred days of process life, which is why the
|
||
monotonic origin is the first read and not boot.</p>
|
||
|
||
<p><strong>The file surface is split by whether a handler could do anything.</strong>
|
||
<code>file-exists?</code> and <code>file-size</code> answer a <code>bool</code> and an
|
||
<code>(Option i64)</code>: absence is the reply, not a fault, and a condition would make
|
||
the ordinary case pay for a handler search. <code>slurp</code>, <code>barf</code>,
|
||
<code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code> signal
|
||
<code>FileError</code> instead, under the two restarts Common Lisp establishes for a
|
||
file error — <code>retry</code>, because the handler may have just made the directory,
|
||
and <code>use-value</code> with another path. Nothing here returns an error code, which
|
||
is the same rule allocation follows. Streaming, stdin and directory listings are not
|
||
here; a whole file at a time is the surface.</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>length</code>, <code>at</code>,
|
||
<code>slice</code>, <code>bytes</code>, <code>bytes-view</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><strong>A function declared with <code>defn-</code> is private to its package.</strong>
|
||
It is a <code>defn</code> in every other respect, and every file in the package's
|
||
directory can call it. For a package imported by naming a single file, the package is
|
||
that file alone, and the other files in its directory cannot call it. A use from
|
||
anywhere else — a call, or the name passed as a value — is refused at compile time,
|
||
and the message names the function and where it may be used. Code written by one of
|
||
the package's own macros counts as the package's, wherever the macro is called. Only
|
||
functions have a private form.</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])
|
||
|
||
(defonce 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])
|
||
|
||
(defonce 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 can be given any body, 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 cosh-f64 [x f64] f64 "cosh")
|
||
|
||
(defn main [] ()
|
||
(print (cosh-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. It is <code>cosh</code> and not
|
||
<code>cos</code> because <a href="#prelude">the prelude</a> already declares
|
||
<code>cos-f64</code>, and a second declaration of a name is refused with both sites
|
||
named — which is the other half of what this example shows. The prelude is where the
|
||
common libm calls live; a <code>declare</code> is how you reach one it does not
|
||
name.</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 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>defonce</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>A function's signature is not on the list either. A dev cell carries the signature
|
||
its body was compiled with, and every call through it compares that against the
|
||
signature the caller was compiled for. A changed signature installs, the reply names
|
||
every caller compiled against the old one by file and line, and a stale caller that
|
||
reaches the call stops on a <code>StaleCall</code> condition instead of passing the old
|
||
arguments. Evaluating the caller again clears it. <code>main</code> is the exception:
|
||
the startup code that calls it was built with the program.</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 for all but one type nothing could be: a typed
|
||
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. A <code>dyn</code> is rendered
|
||
there too, from its tag. 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.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-command</code> pointed at it.</p>
|
||
|
||
<h3>Starting a program</h3>
|
||
|
||
<p><code>M-x flan</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-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-l</kbd></td><td>every lowering of a function at once: IR, <code>-O0</code>, <code>-O2</code>, and the hand-written backend</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</code> starts a program and
|
||
<code>M-x flan-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>defonce</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.</p>
|
||
|
||
<p>A <a href="#dyn"><code>defclass</code></a> is the shape that does evolve, and the
|
||
reason is the whole difference between the two: an instance carries a header naming
|
||
its class and a flat struct does not. Redefining one re-registers the class and
|
||
bumps a generation counter, which is O(1) and walks no heap; every live instance
|
||
migrates at its next touch. Slots matched by name keep their values, a gained slot
|
||
appears as <code>nil</code>, a dropped one goes, the object is the same object, and
|
||
<code>class-of</code> still answers the same tag, so every method still reaches it.
|
||
That is CLHS 4.3.6's protocol without the user hook, which is not built.</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-command</code></td><td><code>"flan"</code></td><td>the compiler binary</td></tr>
|
||
<tr><td><code>flan-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-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-names-shown</code></td><td><code>4</code></td><td>how many names to list before counting them</td></tr>
|
||
<tr><td><code>flan-output-buffer</code></td><td><code>"*flan-output*"</code></td><td>where the program's output goes</td></tr>
|
||
<tr><td><code>flan-poll-interval</code></td><td><code>1.0</code></td><td>seconds between checks for whether it stopped</td></tr>
|
||
<tr><td><code>flan-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
|
||
7623511701454096497
|
||
$ flan build test/programs/sand-headless.flan --target=wasm32-wasi -o sand.wasm
|
||
$ node --no-warnings test/wasm-run.mjs sand.wasm
|
||
7623511701454096497</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><strong>It serves the editor now, which is what it was written for.</strong>
|
||
<code>flan dev --x86</code> and <code>flan reload --x86</code> compile a redefinition
|
||
through the hand-written backend, and the round trip is about half what LLVM's is —
|
||
<code>llc</code> was nearly all of it, and an assembler is not:</p>
|
||
|
||
<table>
|
||
<tr><th></th><th>LLVM</th><th><code>--x86</code></th></tr>
|
||
<tr><td>code generation</td><td><code>llc</code> 42–48ms</td><td><code>as</code> 8.0–8.4ms</td></tr>
|
||
<tr><td>link</td><td><code>ld</code> 9.7–11.4ms</td><td><code>ld</code> 8.9–9.3ms</td></tr>
|
||
<tr><td><code>C-c C-c</code>, at the socket</td><td>62–66ms</td><td><strong>27–30ms</strong></td></tr>
|
||
<tr><td><code>C-x C-e</code></td><td>60–61ms</td><td><strong>24–25ms</strong></td></tr>
|
||
</table>
|
||
|
||
<p>Both columns were measured on one machine, whose <code>llc</code> is slower than the
|
||
one the 15–17ms above came from; the comparison is the point and not the absolute
|
||
numbers. What the headline hides is where the time went: <code>ld</code> did not move and
|
||
is now half the x86 build, and about 9ms of the round trip is the frontend checking the
|
||
program again — a seventh of the LLVM loop but a third of this one. The next millisecond
|
||
is in the linker rather than the code generator.</p>
|
||
|
||
<p><strong>It requires <code>--two-process</code>, and refuses the merged daemon by
|
||
name.</strong> A merged host is linked <code>-rdynamic</code>, so it exports every Flan
|
||
body it contains, and those interpose the prelude of the LLVM-built macro module the
|
||
compiler loads into <em>itself</em> to expand macros. An LLVM caller lands in an x86 body
|
||
and the process dies during the first expansion, before the program has started. The
|
||
marker symbol that refuses a crossed redefinition module does not catch this, and was
|
||
never meant to: a macro module deliberately carries none. Hidden visibility on a macro
|
||
module's Flan bodies is the fix and is not built.</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>unknown type a. A lowercase name is a type variable only where a defn signature introduced it — write $a in the parameter vector to introduce one, and a reads it from there</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>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;
|
||
and there are no threads in the language. The class facility plan.org describes is
|
||
built — see <a href="#dyn">dyn</a> — and what is not built of it is the named-slot
|
||
constructor spelling and the user-written migration hook.</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>TODO.org</code> — the project's memory, and the authority on what is actually
|
||
built.</li>
|
||
<li><code>spec-memory.md</code> — containers, places, generics, function values,
|
||
allocators, and the 2026-09-18 repeal of static ownership tracking.</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 <code>master</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 defn- defstruct defenum defdata defunion defconst defonce def 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 " +
|
||
"defclass defgeneric defmulti defmethod").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 dyn").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>
|