The reference page describes the language that exists, dyn and classes included

This commit is contained in:
Joseph Ferano 2026-09-21 19:34:08 +07:00
parent bcfcf130f7
commit 23e272a24f
12 changed files with 562 additions and 48 deletions

73
FIX.org
View File

@ -6353,3 +6353,76 @@ out on ~program_asked~, so a re-run installs what is queued and then enters
~main~. That is ~lib/dev.ml~, which another lane holds, so it is written down
here rather than done. The test row asserts the behaviour as it is, with the
extra re-run spelled out.
* The reference page says what the language is, 2026-09-21
~web/index.html~ had drifted behind the compiler, mostly in one direction: it
described the language as it was before ~dyn~ existed and before ownership
tracking was repealed. A correction pass, not a rewrite — structure, section
ids and voice kept, claims fixed.
What was false. The tagline, the lede and ~Values and memory~ each said there
is no garbage collector; the ~What it is not~ list refused immutable
collections because "clear ownership is the only thing that removes the need
for a collector" and refused dynamic typing outright; the types table gave
~Vec~, ~Map~ and ~Pool~ as moving on assignment, which the 2026-09-18 repeal
removed; ~Printing~ and the dev loop's expression evaluation both said a Flan
value carries no header with no exception for a ~dyn~; the usage block was
missing ~-O0..-O3~, ~--warn-memory~ and the ~js~ target and still gave ~flan
run~'s old argument shape; the structs example quoted ~(bytes "hi")~ where the
file beside it says ~bytes-view~; the ~Not implemented yet~ table quoted a
milestone-5 message for a bare lowercase type name that the compiler no longer
prints; ~Further reading~ called spec-memory.md frozen and about ownership;
the footer named the retired ~dev-loop~ branch.
What was added. A ~dyn~ section after ~Types~ (id ~dyn~, one TOC entry, no
existing id touched): one word the runtime knows the contents of and the
checker does not, writing the type is the opt-in, only those values are
collected, a numeric cast moves the question to run time where
~flan_dyn_cast_kind~ answers it, ~--no-gc~ names every ~dyn~ and refuses, and
the roots are an explicit shadow stack because wasm32 cannot be made to scan
its own. Also: the operator arities (the orderings chain, ~!=~ is
all-distinct, ~%~ and the shifts take two), ~slice~'s three arities and its
string case with the ~as-slice~ note, ~def~/~defonce~/~defconst~ as a table of
what a re-run does, an untyped global being a ~dyn~, the parameter-vector
pairing rule, ~bytes~ against ~bytes-view~, and a ~Function values~ subsection
covering ~fn~, capture, ~Fn~ against ~CFn~ and the one-way widening.
The class facility was the other thing the page got wrong, and it got it
wrong the other way round: ~Not implemented yet~ said the managed ~class~
plan.org describes "is a plan and not a feature", and the Emacs section said
managed classes were the eventual way through a changed layout. Both landed
on 2026-09-20. So the ~dyn~ section carries ~Classes and generic functions~ —
the four forms, the two dispatch styles as one mechanism, ~:else~,
~NoMethod~, the ~#point{ :x 3 :y 4}~ rendering, a generic being one function
so a method reaches the call sites already compiled — and the Emacs section
says a ~defclass~ is the shape that evolves, with the lazy per-instance
migration and why a flat ~defstruct~ cannot have it. What is left unbuilt of
it, and now named as such, is the named-slot constructor spelling and the
user migration hook. The ~dyn~ section also gained maps, vectors, keywords
and ~nil~, since a class's slots are map keys and none of that was on the
page either.
Four new example programs under ~web/examples/~ — compare.flan, dyn.flan,
fnvalues.flan, classes.flan — and a ~no-gc~ needle in quotes.sh over
dyn.flan, so the refusal the page quotes is re-derived like every other
transcript on it.
** @page was green over a needle that could not be derived
~sand hash~ had been reporting "nothing to compare against" since sand.flan
grew ~(import edn "vendor:edn")~: the alias's deps never named ~vendor/edn~,
so the headless program could not resolve the package in ~_build~ and the
needle came up empty. quotes.sh reports an empty needle as a failure, which is
why it was visible at all, but it reads as "whatever this quotes has moved"
rather than as a missing dependency. One ~glob_files~ in test/dune.
** Not fixed, because it cannot be checked from here
The ~Targets and builds~ section still says survey.sh reports 103 MATCH.
FIX.org's 2026-09-20 session close says 130. Running @x86 was out of scope for
this pass, so the number is left as it stands rather than replaced with one
nobody ran.
** The top-level usage does not mention --no-gc
~flan build --no-gc~ works and ~flan build~'s own usage line lists it;
the usage that bare ~flan~ prints does not. The page quotes the latter
verbatim, so it does not name the flag either. A compiler change, not a page
change.

View File

@ -350,6 +350,11 @@
; alias was green only because `dune test` had already put the file in
; _build, and in CI it sits inside @checks where the same thing hides it.
(file %{workspace_root}/sand.flan)
; And the package sand.flan imports, for the same reason: without it the
; headless program cannot resolve vendor:edn in _build and the sand-hash
; needle comes up empty, which quotes.sh reports as "whatever this quotes
; has moved" rather than as a missing dependency.
(glob_files %{workspace_root}/vendor/edn/*)
; breakdemo.flan is built --dev, so it imports the agent; raylib is here for
; the binding line quotes.sh greps out of it.
(glob_files %{workspace_root}/vendor/agent/*)

29
web/examples/classes.flan Normal file
View File

@ -0,0 +1,29 @@
;; 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}))))

9
web/examples/classes.out Normal file
View File

@ -0,0 +1,9 @@
12
12
4
:point
nil
#point{ :x 3 :y 4}
9
something else
exit 0

11
web/examples/compare.flan Normal file
View File

@ -0,0 +1,11 @@
;; 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)))

7
web/examples/compare.out Normal file
View File

@ -0,0 +1,7 @@
10
true
false
true
true
false
exit 0

13
web/examples/dyn.flan Normal file
View File

@ -0,0 +1,13 @@
;; 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))))

4
web/examples/dyn.out Normal file
View File

@ -0,0 +1,4 @@
42
3
43
exit 0

View File

@ -0,0 +1,15 @@
;; 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))))

View File

@ -0,0 +1,5 @@
15
6
6
42
exit 0

View File

@ -47,6 +47,14 @@ want "calc-me" "$("$FLAN" run "$root/calc-me.flan" '1 + 2 * (3 - 0.5) / 2')"
# that the two agree; only the native half is cheap enough to check here.
want "sand hash" "$("$FLAN" run "$root/test/programs/sand-headless.flan")"
# --no-gc, which is the page's claim that a program can have "this carries no
# collector" checked rather than believed. dyn.flan holds one on purpose, so
# the flag must refuse it; the page quotes the first line of what it says.
nogc=${TMPDIR:-/tmp}/flan-page-nogc.$$
want "no-gc" "$("$FLAN" build "$here/dyn.flan" --no-gc -o "$nogc" 2>&1 \
| sed 's/^[^ ]*: //' | sed -n 1p)"
rm -f "$nogc"
# The two cross-target refusals, in the compiler's own words.
want "run --target" "$("$FLAN" run "$here/hello.flan" --target=wasm32-wasi 2>&1)"

View File

@ -224,6 +224,7 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
<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>
@ -276,14 +277,15 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
fill="currentColor">flan<tspan fill="var(--accent)">.</tspan></text>
</svg>
<p class="tagline">A statically typed Lisp for game development. Clojure's brackets,
C's memory and value model, no garbage collector.</p>
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.
There are no object headers, so a Flan struct is exactly its C struct. There is no
collector, so nothing runs between frames that you did not write. And a running
program can be edited: a function recompiled in Emacs is installed into the live
process at its next frame boundary, in about twenty milliseconds.</p>
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
@ -297,7 +299,8 @@ rather than programs.</p>
<h2 id="what">What Flan is</h2>
<p>A minimal Lisp for games. In one line: Odin with a Lisp frontend and a live REPL.
Types are mandatory and inference makes them feel optional; memory is manual; the
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>
@ -307,11 +310,11 @@ see <a href="#targets">targets and builds</a>.</p>
<ul>
<li><strong>Not a Common Lisp and not a Clojure.</strong> No numeric tower, no CLOS,
no <code>format</code>, no lazy seqs, no persistent collections, no JVM.</li>
<li><strong>No immutable collection types.</strong> Structure sharing destroys clear
ownership, and clear ownership is the only thing that removes the need for a
collector. Value structs that copy on assignment replace them.</li>
<li><strong>No dynamic typing.</strong> A tag word on every value is exactly the
header cost that dropping the GC was meant to avoid.</li>
<li><strong>No 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>
@ -332,8 +335,8 @@ usage: flan (read|parse|check|emit|shim) &lt;file.flan&gt;...
flan emit &lt;file.flan&gt; [--x86] [--dev] [--debug] [--no-bounds-checks]
flan import-c &lt;header.h&gt; [package.flan...] [clang flags...]
flan generate-c &lt;package-dir&gt;
flan build &lt;file.flan&gt; [-o out] [--no-bounds-checks] [--dev] [--debug] [--sanitize] [--x86] [--target=wasm32-wasi|web]
flan run &lt;file.flan&gt; [args...]
flan build &lt;file.flan&gt; [-o out] [-O0|-O1|-O2|-O3] [--no-bounds-checks] [--dev] [--debug] [--sanitize] [--x86] [--warn-memory] [--target=wasm32-wasi|web|js]
flan run &lt;file.flan&gt; [build flags...] [--] [program args...]
flan reload &lt;program.flan&gt; &lt;forms.flan&gt; [-o out.so] [--x86]
flan dev &lt;program.flan&gt; [-s socket] [--x86]</code></pre>
@ -364,9 +367,12 @@ type is not: <code>()</code> is unit, and a <code>main</code> that returns it ex
<h2 id="values">Values and memory</h2>
<p>There is no garbage collector and no hidden allocation. Every local is a stack
slot; reading one is a load, assigning to one is a store, and a store of an aggregate
<em>is</em> the copy. A struct is its C layout with nothing added.</p>
<p>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>
@ -513,14 +519,15 @@ notation reads as exactly one data item.</p>
<tr><td><code>string</code></td><td>a byte slice with no NUL</td><td>ptr + len</td></tr>
<tr><td><code>[T]</code></td><td>slice, non-owning</td><td>ptr + len</td></tr>
<tr><td><code>[n T]</code></td><td>fixed array, a value</td><td>n inline items</td></tr>
<tr><td><code>(Vec T)</code></td><td>growable, owning — <em>moves</em> on assignment</td><td>ptr + len + cap + its allocator</td></tr>
<tr><td><code>(Map K V)</code></td><td>open addressing, owning — <em>moves</em>. The only map spelling: braces in type position are not a type</td><td>data + len + log2cap + its allocator</td></tr>
<tr><td><code>(Pool T)</code></td><td>generational slab storage, owning — <em>moves</em></td><td>items + slots + its allocator</td></tr>
<tr><td><code>(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>
@ -532,8 +539,8 @@ notation reads as exactly one data item.</p>
</table>
</div>
<p><strong>There is no implicit widening.</strong> Both operands of a binary operator
have one type, and every conversion is written as a cast:</p>
<p><strong>There is no implicit widening.</strong> Every operand of an arithmetic or
comparison form has one type, and every conversion is written as a cast:</p>
<pre><code>(defn main [] i32
(let [n 40 ; i32, inferred
@ -558,6 +565,44 @@ operand's width is a compile error, and a computed one is masked to the width.
<code>&gt;&gt;</code> is arithmetic on a signed type and logical on an unsigned
one.</p>
<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>&lt;</code>, <code>&lt;=</code>, <code>&gt;</code>,
<code>&gt;=</code> — <em>chain</em>: each neighbouring pair is compared and every
pair has to hold, so <code>(&lt; 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 (&lt; 1 2 3))
(println (&lt; 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
@ -567,6 +612,185 @@ refused:</p>
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
@ -585,7 +809,7 @@ its fields, and omitted fields are zeroed.</p>
(set (.pos c) (+ (.pos c) 1))) ; field access derefs one level
(defn main [] ()
(let [c (Cursor {.src (bytes "hi")})] ; pos omitted, so pos is 0
(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>
@ -600,6 +824,14 @@ byte is a <code>u8</code> — but there is a byte literal, so <code>\h</code> is
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>
@ -645,7 +877,27 @@ body as the function's return type. Writing the type removes the guess, and a
mistyped one now says <em>did you mean f64</em> rather than <em>unknown name</em>.</p>
<p>Top-level names are order-independent within a package, so mutually recursive
functions need no forward declaration. Globals come in two kinds:</p>
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
@ -672,6 +924,62 @@ 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>,
@ -836,8 +1144,23 @@ extent, because nothing is released at scope exit)</code></pre>
<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 or a string.
<code>(slice s lo hi)</code> takes a half-open range and never copies.</p>
<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)
@ -994,9 +1317,11 @@ bind. <code>test/programs/generics.flan</code> exercises the whole of it.</p>
<p><code>println</code> prints a value and a newline; <code>print</code> is the same
walk without the newline. There is one of each and they take any type, but neither is a
function and neither is overloading: the compiler walks the argument's type where the
call is written and emits the printing for it. Nothing is decided at run time — a Flan
value carries no header, so nothing at run time could say what it is — and there is no
user-supplied printer to choose between.</p>
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])
@ -1020,8 +1345,8 @@ none
no newline: true</code></pre>
<p>The walk covers every integer and float type, <code>bool</code>, <code>()</code>,
<code>string</code>, <code>[u8]</code>, enums, <code>Ptr</code>, <code>Option</code>,
structs, unions, fixed arrays and slices. An owning container has no printer for its
<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>&lt;vec&gt;</code>,
<code>&lt;pool&gt;</code>, <code>&lt;allocator&gt;</code> — while a <code>Handle</code>
shows its index and generation, and a <code>Map</code> has no printer at all.
@ -1135,7 +1460,7 @@ 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-&gt;f64</code>,
<code>slice</code>, <code>bytes</code>, <code>bytes-view</code>, <code>bytes-&gt;f64</code>,
<code>bytes-&gt;i64</code>, <code>f64-&gt;bytes</code>, <code>i64-&gt;bytes</code>,
<code>addr</code>, and arithmetic, comparison and casts.</p>
@ -1715,9 +2040,10 @@ from. The module says <em>run this once</em>, and the agent calls it after the i
the game thread, at a frame boundary. An expression reading the program's state therefore
sees a consistent one.</p>
<p>Nothing is marshalled back, because nothing could be: a Flan value carries no header,
so no code at run time can say what it is. The compiler knows the type and renders it
there, in the thunk. What comes back looks like this:</p>
<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
@ -1869,9 +2195,16 @@ to a new body. The refusal is a limitation with a date on it, not a rule.</p>
<p><strong>A changed struct layout is the genuinely hard case</strong>, and is rejected
while live values of that struct exist: storage already allocated has the old shape, and a
new body would read its fields at the wrong offsets with nothing to say so. plan.org keeps
this one as a rejection, and gives managed classes an explicit migration at a frame
boundary as the eventual way through.</p>
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
@ -2025,7 +2358,7 @@ name, with the milestone it belongs to, and the tests assert on the reason.</p>
<tr><td><code>(Result T E)</code></td><td>(Result T E) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(try …)</code></td><td>try (Result) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>'sym</code> as a value</td><td>a quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td>a bare lowercase type name</td><td>generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)</td></tr>
<tr><td>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>
@ -2047,8 +2380,9 @@ extent); <code>find-restart</code> and <code>compute-restarts</code> are blocked
<code>Restart</code> type rather than on effort; a restart with parameters cannot be
taken from the break loop, which aims at a frame by position and has nothing to fill them
with; there is no package-private marker other than <code>main</code> not being exported;
there are no threads in the language; and the managed <code>class</code> facility that
plan.org describes is a plan and not a feature.</p>
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
@ -2066,8 +2400,8 @@ disagree with the first.</p>
<li><code>plan.org</code> — the design, the build sequence, and the open decisions.</li>
<li><code>NEXT.md</code> — the project's memory, and the authority on what is actually
built.</li>
<li><code>spec-memory.md</code> — ownership, containers, places, generics, function
values. Frozen.</li>
<li><code>spec-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>
@ -2078,9 +2412,9 @@ disagree with the first.</p>
</div>
<footer>
<p>Flan is a custard. This page describes the compiler on branch
<code>dev-loop</code>; where a document and the compiler disagree, the compiler is what
is written here.</p>
<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>
@ -2148,12 +2482,13 @@ disagree with the first.</p>
<script>
// A small hand-written highlighter for the Flan blocks. One pass, no library.
(function () {
var FORMS = new Set(("defn defstruct defenum defdata defunion defconst defonce defalias " +
var FORMS = new Set(("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").split(" "));
"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").split(" "));
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");