Merge branch 'web-prose' into dev-loop
This commit is contained in:
commit
692403998b
@ -56,7 +56,8 @@ for pair in \
|
||||
'fnty:(defn f [g (Fn [i32] i32)] i32 (g 1))' \
|
||||
'quoted:(defn f [] i32 (quote a))' \
|
||||
'deferblock:(defn f [] i32 (let [x 1] (defer (print-line "a")) x))' \
|
||||
'i64index:(defconst xs [3 i32] [1 2 3]) (defn main [] i32 (let [i (i64 1)] (at xs i)))'
|
||||
'i64index:(defconst xs [3 i32] [1 2 3]) (defn main [] i32 (let [i (i64 1)] (at xs i)))' \
|
||||
'break:(defn main [] (let [i 0] (while (< i 3) (break))))'
|
||||
do
|
||||
name=${pair%%:*}; src=${pair#*:}
|
||||
printf '%s\n' "$src" > "$here/.q.flan"
|
||||
|
||||
371
web/index.html
371
web/index.html
@ -67,6 +67,7 @@ body {
|
||||
color: var(--text);
|
||||
font: 17px/1.65 "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
|
||||
}
|
||||
main { display: block; }
|
||||
.wrap { max-width: 44rem; margin: 0 auto; padding: 0 1.25rem 6rem; }
|
||||
|
||||
header { padding: 3.5rem 0 2rem; }
|
||||
@ -117,12 +118,63 @@ th { font-weight: 600; color: var(--muted); }
|
||||
|
||||
.lede { font-size: 1.1rem; }
|
||||
.note { color: var(--muted); font-size: .94rem; }
|
||||
nav.toc { background: var(--panel); border-radius: 5px; padding: 1rem 1.25rem; margin: 2rem 0 0; }
|
||||
nav.toc ul { list-style: none; padding: 0; margin: 0; columns: 2; column-gap: 2rem; }
|
||||
nav.toc li { margin: .18rem 0; font-size: .93rem; break-inside: avoid; }
|
||||
|
||||
/* The table of contents: a bar with a toggle when the window is narrow, a
|
||||
fixed column beside the text when there is room for one. The toggle is a
|
||||
checkbox so that no script is needed to open it. */
|
||||
.toc-toggle { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||
.toc-button {
|
||||
display: block; cursor: pointer; user-select: none;
|
||||
position: sticky; top: 0; z-index: 2;
|
||||
padding: .6rem 1.25rem;
|
||||
background: var(--panel); border-bottom: 1px solid var(--rule);
|
||||
color: var(--muted); font-size: .92rem; letter-spacing: .02em;
|
||||
}
|
||||
.toc-button::before { content: "\2261\00a0\00a0"; }
|
||||
.toc-toggle:focus-visible + .toc-button { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
nav.toc {
|
||||
display: none;
|
||||
background: var(--panel); border-bottom: 1px solid var(--rule);
|
||||
padding: .75rem 1.25rem 1.1rem;
|
||||
}
|
||||
.toc-toggle:checked ~ nav.toc { display: block; }
|
||||
.toc-title {
|
||||
display: none; /* the toggle already says "Contents" */
|
||||
margin: 0 0 .5rem; font-size: .78rem; letter-spacing: .09em;
|
||||
text-transform: uppercase; color: var(--muted);
|
||||
}
|
||||
nav.toc ul { list-style: none; padding: 0; margin: 0; }
|
||||
nav.toc li { margin: 0; font-size: .93rem; }
|
||||
nav.toc a {
|
||||
display: block; padding: .2rem 0; color: var(--muted);
|
||||
text-decoration: none; border-left: 2px solid transparent; padding-left: .6rem;
|
||||
}
|
||||
nav.toc a:hover { color: var(--link); border-left-color: var(--accent); }
|
||||
h2, h3 { scroll-margin-top: 3.5rem; }
|
||||
|
||||
/* Nineteen entries one to a line fills a phone screen, so the opened panel
|
||||
goes to two columns as soon as there is room for them. */
|
||||
@media (min-width: 30rem) and (max-width: 65.99rem) {
|
||||
nav.toc ul { columns: 2; column-gap: 1.5rem; }
|
||||
nav.toc li { break-inside: avoid; }
|
||||
}
|
||||
footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--rule); color: var(--muted); font-size: .92rem; }
|
||||
|
||||
@media (max-width: 34rem) { nav.toc ul { columns: 1; } }
|
||||
/* Wide enough for both: the contents become a column of their own, scrolling
|
||||
independently of the page, and the text moves over to sit beside it. */
|
||||
@media (min-width: 66rem) {
|
||||
.toc-button { display: none; }
|
||||
nav.toc {
|
||||
display: block;
|
||||
position: fixed; top: 0; left: 0; bottom: 0;
|
||||
width: 15rem; overflow-y: auto; overscroll-behavior: contain;
|
||||
background: none; border-bottom: 0; border-right: 1px solid var(--rule);
|
||||
padding: 3.6rem 1rem 2rem 1.5rem;
|
||||
}
|
||||
main { margin-left: 15rem; }
|
||||
.toc-title { display: block; }
|
||||
h2, h3 { scroll-margin-top: 1rem; }
|
||||
}
|
||||
|
||||
.c { color: var(--c-comment); font-style: italic; }
|
||||
.s { color: var(--c-string); }
|
||||
@ -133,34 +185,11 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<svg class="mark" viewBox="0 0 210 44" role="img" aria-label="Flan">
|
||||
<text x="0" y="33" font-family="Iowan Old Style, Palatino, Georgia, serif"
|
||||
font-size="38" font-weight="600" letter-spacing="1.5"
|
||||
fill="currentColor">flan</text>
|
||||
<circle cx="128" cy="28" r="3.4" fill="var(--accent)"/>
|
||||
</svg>
|
||||
<p class="tagline">A statically typed Lisp for game development. Clojure's brackets,
|
||||
C's memory and value model, no garbage collector.</p>
|
||||
</header>
|
||||
|
||||
<p class="lede">Flan compiles s-expressions to LLVM IR and then to a native binary.
|
||||
There are no object headers, so a Flan struct is exactly its C struct. There is no
|
||||
collector, so nothing runs between frames that you did not write. And a running
|
||||
program can be edited: a function recompiled in Emacs is installed into the live
|
||||
process at its next frame boundary, in about twenty milliseconds.</p>
|
||||
|
||||
<p>This page describes the compiler <em>as it is</em>, not as it is planned. Where
|
||||
something is designed but not built, it says so and gives the message the compiler
|
||||
prints for it. Every Flan program on this page is a file in <code>web/examples/</code>
|
||||
with its output recorded beside it; <code>sh web/examples/check.sh</code> runs them all
|
||||
and compares, and <code>quotes.sh</code> re-derives the blocks that are transcripts
|
||||
rather than programs. If the page and the compiler disagree, one of those two goes
|
||||
red.</p>
|
||||
<input type="checkbox" id="toc-toggle" class="toc-toggle">
|
||||
<label class="toc-button" for="toc-toggle">Contents</label>
|
||||
|
||||
<nav class="toc" aria-label="Contents">
|
||||
<p class="toc-title">Contents</p>
|
||||
<ul>
|
||||
<li><a href="#what">What Flan is</a></li>
|
||||
<li><a href="#start">Getting started</a></li>
|
||||
@ -184,6 +213,34 @@ red.</p>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<svg class="mark" viewBox="0 0 118 44" role="img" aria-label="Flan">
|
||||
<!-- The dot is a full stop in the same run, so it stays beside the n
|
||||
whichever serif the reader actually has. -->
|
||||
<text x="0" y="33" font-family="Iowan Old Style, Palatino, Georgia, serif"
|
||||
font-size="38" font-weight="600" letter-spacing="1"
|
||||
fill="currentColor">flan<tspan fill="var(--accent)">.</tspan></text>
|
||||
</svg>
|
||||
<p class="tagline">A statically typed Lisp for game development. Clojure's brackets,
|
||||
C's memory and value model, no garbage collector.</p>
|
||||
</header>
|
||||
|
||||
<p class="lede">Flan compiles s-expressions to LLVM IR and then to a native binary.
|
||||
There are no object headers, so a Flan struct is exactly its C struct. There is no
|
||||
collector, so nothing runs between frames that you did not write. And a running
|
||||
program can be edited: a function recompiled in Emacs is installed into the live
|
||||
process at its next frame boundary, in about twenty milliseconds.</p>
|
||||
|
||||
<p>This page describes the compiler <em>as it is</em>, not as it is planned. Where
|
||||
something is designed but not built, it says so and gives the message the compiler
|
||||
prints for it. Every Flan program on this page is a file in <code>web/examples/</code>
|
||||
with its output recorded beside it; <code>sh web/examples/check.sh</code> runs them all
|
||||
and compares, and <code>quotes.sh</code> re-derives the blocks that are transcripts
|
||||
rather than programs.</p>
|
||||
|
||||
<h2 id="what">What Flan is</h2>
|
||||
|
||||
<p>A minimal Lisp for games. In one line: Odin with a Lisp frontend and a live REPL.
|
||||
@ -217,15 +274,14 @@ $ ./_build/default/bin/main.exe run calc-me.flan "1 + 2 * (3 - 0.5) / 2"
|
||||
|
||||
<pre><code class="sh">$ flan
|
||||
usage: flan (read|parse|check|emit|shim) <file.flan>...
|
||||
flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] [--target=wasm32-wasi]
|
||||
flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] [--debug] [--target=wasm32-wasi]
|
||||
flan run <file.flan> [args...]
|
||||
flan reload <program.flan> <forms.flan> [-o out.so]
|
||||
flan dev <program.flan> [-s socket]</code></pre>
|
||||
|
||||
<p><code>read</code>, <code>parse</code>, <code>check</code>, <code>emit</code> and
|
||||
<code>shim</code> each stop the pipeline one stage further along and print what it
|
||||
produced, which is how you find out what the compiler thinks of a form.
|
||||
<code>run</code> builds to a temporary file and execs it.</p>
|
||||
produced. <code>run</code> builds to a temporary file and execs it.</p>
|
||||
|
||||
<p>The smallest program:</p>
|
||||
|
||||
@ -259,8 +315,8 @@ slot; reading one is a load, assigning to one is a store, and a store of an aggr
|
||||
borrow checker.</li>
|
||||
</ul>
|
||||
|
||||
<p>That last point is what makes a heap unnecessary for a great deal of code: a value
|
||||
struct is shared mutably by passing its address down the call chain.</p>
|
||||
<p>A value struct is shared mutably by passing its address down the call chain. No
|
||||
heap is involved.</p>
|
||||
|
||||
<p>Places — the forms <code>set</code> accepts — are a fixed list, not an extensible
|
||||
<code>setf</code>:</p>
|
||||
@ -291,10 +347,10 @@ wisp
|
||||
5
|
||||
1</code></pre>
|
||||
|
||||
<p><code>.field</code> and <code>at</code> dereference exactly one pointer level, which
|
||||
is why <code>(set (.hp p) 8)</code> above is legal when <code>p</code> is a
|
||||
<code>(Ptr Enemy)</code>. Note the last two stores: the whole-object store through
|
||||
<code>p</code> overwrote <code>e</code> itself, so <code>hp</code> reads 3 and not 8.</p>
|
||||
<p><code>.field</code> and <code>at</code> dereference exactly one pointer level, so
|
||||
<code>(set (.hp p) 8)</code> above is legal when <code>p</code> is a
|
||||
<code>(Ptr Enemy)</code>. The whole-object store through <code>p</code> overwrote
|
||||
<code>e</code> itself, so <code>hp</code> reads 3 and not 8.</p>
|
||||
|
||||
<h3>Bounds are checked</h3>
|
||||
|
||||
@ -317,9 +373,8 @@ $ echo $?
|
||||
|
||||
<p><code>at</code> and <code>slice</code> emit a comparison and a branch to a cold
|
||||
block that names the source location and stops. Checks are on by default and are not
|
||||
tied to the optimisation level, which is what lets the acceptance table run the same
|
||||
programs at <code>-O0</code> and <code>-O2</code> with identical checks;
|
||||
<code>--no-bounds-checks</code> turns them off. Measured cost on a
|
||||
tied to the optimisation level. <code>--no-bounds-checks</code> turns them off.
|
||||
Measured cost on a
|
||||
50-million-iteration dependency chain over a 1024-element array: 0.11–0.12s checked
|
||||
against 0.12–0.13s unchecked.</p>
|
||||
|
||||
@ -364,19 +419,18 @@ have one type, and every conversion is written as a cast:</p>
|
||||
|
||||
<p>An untyped integer literal is <code>i32</code> and an untyped float literal is
|
||||
<code>f64</code>, so <code>(defconst gravity f32 0.05)</code> names the type when
|
||||
something narrower is wanted. One caveat worth knowing before it surprises you: a
|
||||
whole-numbered float prints without its fraction, so <code>3.0</code> comes out as
|
||||
<code>3</code>.</p>
|
||||
something narrower is wanted. One caveat: a whole-numbered float prints without its
|
||||
fraction, so <code>3.0</code> comes out as <code>3</code>.</p>
|
||||
|
||||
<p>Arithmetic wraps. Shifts are bounded two ways: a literal count at or past the
|
||||
operand's width is a compile error, and a computed one is masked to the width, which
|
||||
is what the hardware does anyway. <code>>></code> is arithmetic on a signed type
|
||||
and logical on an unsigned one.</p>
|
||||
operand's width is a compile error, and a computed one is masked to the width.
|
||||
<code>>></code> is arithmetic on a signed type and logical on an unsigned
|
||||
one.</p>
|
||||
|
||||
<p>An index converts from a narrower integer and never from a wider one. A
|
||||
<code>u32</code> index is fine — anything above 2<sup>31</sup> truncates to a negative
|
||||
<code>i32</code> and the unsigned bounds check rejects it. An <code>i64</code> index is
|
||||
refused, and the message is worth reading because it is the shape of most of them:</p>
|
||||
refused:</p>
|
||||
|
||||
<pre><code class="sh">an index is an i32, and i64 is wider — write (i32 …), because a value that does
|
||||
not fit truncates to one that does and would read the wrong element without
|
||||
@ -414,10 +468,9 @@ byte is a <code>u8</code> — but there is a byte literal, so <code>\h</code> is
|
||||
<code>(and (>= b \0) (<= b \9))</code>. To see a byte as a letter rather than as a
|
||||
number, print a slice of them with <code>print-bytes</code>.</p>
|
||||
|
||||
<p>An enum is an <code>i32</code> at run time and its own type in the checker. That is
|
||||
what makes a keyword at a call site useful: <code>:space</code> resolves against the
|
||||
parameter's enum type at compile time, and a typo is an error there rather than a wrong
|
||||
number later.</p>
|
||||
<p>An enum is an <code>i32</code> at run time and its own type in the checker. A
|
||||
keyword at a call site resolves against the parameter's enum type at compile time, so a
|
||||
typo is an error there rather than a wrong number later.</p>
|
||||
|
||||
<pre><code>(defenum Key
|
||||
[space 32 escape 256 left 263 right 262])
|
||||
@ -473,8 +526,8 @@ functions need no forward declaration. Globals come in two kinds:</p>
|
||||
0</code></pre>
|
||||
|
||||
<p>A <code>defconst</code> the checker consumed — an array length, for instance — is
|
||||
part of the shape of the program. One it did not is only ever bytes in memory, which
|
||||
matters for reloading; see <a href="#devloop">the dev loop</a>.</p>
|
||||
part of the shape of the program. One it did not is only ever bytes in memory. The
|
||||
two reload differently; see <a href="#devloop">the dev loop</a>.</p>
|
||||
|
||||
<p><code>let</code> binds name/value pairs and takes no type annotation, so a constant
|
||||
whose type matters is named at the top level rather than written inline.</p>
|
||||
@ -528,18 +581,20 @@ a body that changes it cannot change the trip count, and the loop variable is no
|
||||
assignable.</p>
|
||||
|
||||
<p>Loops are imperative, with <code>while</code>, <code>until</code> and
|
||||
<code>return</code>. There is no <code>loop</code>/<code>recur</code>, and there is no
|
||||
<code>break</code> or <code>continue</code> either — both are planned and neither
|
||||
exists, so today they report as <code>unknown function break</code>. An early exit out
|
||||
of a loop is <code>return</code>, as <code>first-even</code> does above.</p>
|
||||
<code>return</code>. There is no <code>loop</code>/<code>recur</code>. There is no
|
||||
<code>break</code> or <code>continue</code> yet either; both refuse by name:</p>
|
||||
|
||||
<pre><code class="sh">break is not implemented yet (see the build sequence in plan.org)</code></pre>
|
||||
|
||||
<p>An early exit out of a loop is <code>return</code>, as <code>first-even</code> does
|
||||
above.</p>
|
||||
|
||||
<h3>Option, <code>match</code> and <code>some</code></h3>
|
||||
|
||||
<p><code>(Option T)</code> is how absence is spelled: a lookup miss, an empty
|
||||
collection, the end of a stream. <code>match</code> works on an <code>Option</code> and
|
||||
on nothing else today. <code>some</code> unwraps <code>Some</code> and early-returns
|
||||
<code>None</code> from the enclosing function, which is what keeps a recursive descent
|
||||
parser readable.</p>
|
||||
<code>None</code> from the enclosing function.</p>
|
||||
|
||||
<pre><code>(defconst nums [4 i32] [4 8 15 16])
|
||||
|
||||
@ -582,8 +637,7 @@ second
|
||||
3</code></pre>
|
||||
|
||||
<p><code>defer</code> is function-scoped and is <strong>rejected</strong> inside a
|
||||
<code>let</code>, a loop or a branch, rather than accepted with surprising scope. Block
|
||||
scoping it is real work and is not done:</p>
|
||||
<code>let</code>, a loop or a branch. Block scoping it is not done:</p>
|
||||
|
||||
<pre><code class="sh">defer must be a top-level form in a function body — block-scoped defer is not implemented yet (milestone 4)</code></pre>
|
||||
|
||||
@ -641,33 +695,33 @@ ordinary Flan.</p>
|
||||
<tr><td>slices of <code>i32</code></td><td><code>swap-i32!</code>, <code>reverse-i32!</code>, <code>sort-i32!</code>, <code>index-of-i32</code>, <code>min-i32</code>, <code>max-i32</code>, <code>sum-i32</code></td></tr>
|
||||
<tr><td>bytes</td><td><code>bytes=?</code>, <code>starts-with?</code>, <code>ends-with?</code>, <code>index-of-byte</code>, <code>index-of-bytes</code>, <code>trim</code>, <code>digit?</code>, <code>space?</code></td></tr>
|
||||
<tr><td>parsing</td><td><code>parse-i64</code>, <code>parse-f64</code></td></tr>
|
||||
<tr><td>text</td><td><code>split-on-byte</code>, <code>split-next!</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>bytes-ci=?</code></td></tr>
|
||||
<tr><td>UTF-8</td><td><code>decode-rune</code>, <code>rune-at</code>, <code>rune-count</code>, <code>rune-size</code>, <code>rune-start?</code>, <code>valid-utf8?</code>, <code>encode-rune!</code></td></tr>
|
||||
<tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, and <code>sqrt-f32</code>, which is the one <code>declare</code> in the file</td></tr>
|
||||
<tr><td>random</td><td><code>rand-seed</code>, <code>rand-u32</code>, <code>rand-f32</code>, <code>rand-i32-range</code>, <code>rand-f32-range</code></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>Two deliberate choices in there. <strong>The RNG is ours, not libc's</strong> —
|
||||
PCG-XSH-RR 32, written in Flan — because a grid hash is only a regression test if the
|
||||
sequence is byte-identical on native and on wasm32. And <strong>the parsers are ours
|
||||
too</strong>: <code>strtoll</code> answers 0 for <code>""</code>, 0 for
|
||||
<p><strong>The RNG is ours, not libc's</strong> — PCG-XSH-RR 32, written in Flan —
|
||||
because a grid hash is only a regression test if the sequence is byte-identical on
|
||||
native and on wasm32. <strong>The parsers are ours too</strong>:
|
||||
<code>strtoll</code> answers 0 for <code>""</code>, 0 for
|
||||
<code>"abc"</code> and 12 for <code>"12x"</code>, which are three wrong answers a caller
|
||||
cannot tell from a real 12.</p>
|
||||
|
||||
<p><code>sqrt-f32</code> goes the other way, and is the one function in the file that
|
||||
is not Flan: <code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code>. Every other number
|
||||
<p><code>sqrt-f32</code> is the one function in the file that is not Flan:
|
||||
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code>. Every other number
|
||||
here is reachable from the four operations and a cast; a square root is not, and the
|
||||
usual trick of seeding Newton's method from the exponent bits needs a bit-cast between
|
||||
<code>f32</code> and <code>u32</code> that the language does not have. IEEE-754 makes
|
||||
<code>sqrt</code> correctly rounded, so libm gives the same bit pattern on both targets
|
||||
anyway — the very property that keeps the RNG in Flan is, for this one, the argument
|
||||
for going out to C. It is also why every link carries <code>-lm</code>.</p>
|
||||
<code>sqrt</code> correctly rounded, so libm gives the same bit pattern on both
|
||||
targets anyway. Every link carries <code>-lm</code>.</p>
|
||||
|
||||
<p>There is no <code>println</code>. There is no overloading yet, so each printer names
|
||||
its type. The names are the compiler's answer too: <code>(println 1)</code> is
|
||||
<code>unknown function println</code>.</p>
|
||||
its type. <code>(println 1)</code> is <code>unknown function println</code>.</p>
|
||||
|
||||
<p>The primitives underneath are few by design, because a primitive is the only thing
|
||||
that gets implemented twice per backend: <code>argv</code>,
|
||||
<p>The primitives underneath are few — a primitive is the only thing implemented
|
||||
twice per backend: <code>argv</code>,
|
||||
<code>write-stdout</code>, <code>exit</code>, <code>len</code>, <code>at</code>,
|
||||
<code>slice</code>, <code>bytes</code>, <code>bytes->f64</code>,
|
||||
<code>bytes->i64</code>, <code>f64->bytes</code>, <code>i64->bytes</code>,
|
||||
@ -716,7 +770,7 @@ the importing file until a directory of that name is found.</p>
|
||||
literal, in an array length — is rewritten to match. Nothing downstream knows a package
|
||||
existed.</p>
|
||||
|
||||
<p>Three more rules that are easier to know than to discover:</p>
|
||||
<p>Three more rules:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>A package may be a single <code>.flan</code> file</strong> named outright,
|
||||
@ -724,8 +778,8 @@ existed.</p>
|
||||
three other loose programs, so naming its directory would import all four.</li>
|
||||
<li><strong>A package may import a package</strong>, and the qualification flattens to
|
||||
the inner alias: raylib imported by a package that is itself imported is still
|
||||
<code>rl/…</code>. A directory is keyed by its real path and read once, which is also
|
||||
what ends a cycle. The same directory under two different aliases is refused.</li>
|
||||
<code>rl/…</code>. A directory is keyed by its real path and read once, so a cycle
|
||||
ends there. The same directory under two different aliases is refused.</li>
|
||||
<li><strong><code>main</code> is not exported.</strong> A package carrying one would
|
||||
collide with the importer's, and <code>main</code> is a reachability root, so an
|
||||
imported one would keep everything it calls alive. Writing <code>sand/main</code> is
|
||||
@ -740,7 +794,7 @@ compiled into the build, and a file named <code>link</code> lists extra linker
|
||||
arguments. Whether those reach the build is decided after checking, from the program
|
||||
rather than from the import list: the compiler starts at <code>main</code>, follows every
|
||||
call, and a package none of whose externs survive contributes no C and no linker
|
||||
argument. That is what lets one file import raylib and still build for wasm32.</p>
|
||||
argument, so a file that imports raylib still builds for wasm32.</p>
|
||||
|
||||
<h2 id="conditions">Conditions and restarts</h2>
|
||||
|
||||
@ -759,8 +813,8 @@ carries on.</p>
|
||||
|
||||
(invoke-restart 'name) ; Never. Innermost frame offering the name wins.</code></pre>
|
||||
|
||||
<p><code>signal</code> has type <code>Unit</code>, always. That is the accumulation
|
||||
case, and it is worth having on its own because it alters no control flow:</p>
|
||||
<p><code>signal</code> has type <code>Unit</code>, always. A handler that returns
|
||||
normally leaves the signaller to carry on — the accumulation case:</p>
|
||||
|
||||
<pre><code>(defstruct AssetMissing [id i32])
|
||||
|
||||
@ -818,10 +872,10 @@ first, before the clause body starts.</p>
|
||||
|
||||
<p>Restart lookup walks the dynamic restart stack from innermost outward and takes the
|
||||
first frame offering the name, so an inner <code>restart-case</code> shadows an outer
|
||||
one for the duration of its body. That is what makes "restarts go at the resync point"
|
||||
composable.</p>
|
||||
one for the duration of its body. An inner parser's <code>skip-form</code> is found
|
||||
before an outer one's.</p>
|
||||
|
||||
<h3>How it is lowered, and why that matters</h3>
|
||||
<h3>How a transfer is lowered</h3>
|
||||
|
||||
<p>A transfer is not platform unwinding. Every Flan signature carries a transfer channel
|
||||
— one pointer appended as an out-parameter — which <code>invoke-restart</code> writes
|
||||
@ -829,12 +883,11 @@ and every call site checks. A callee writes the target into its caller's slot; e
|
||||
frame checks, runs its defers and returns early. The disassembly is the release one plus
|
||||
a guard after each call.</p>
|
||||
|
||||
<p>Three consequences to know:</p>
|
||||
<p>Three consequences:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>wasm32 works with no exception proposal</strong>, and native and wasm
|
||||
builds of the same program agree, which is the property the acceptance table exists to
|
||||
check.</li>
|
||||
builds of the same program agree.</li>
|
||||
<li><strong>Every function carries the channel, release builds included.</strong> A
|
||||
hot-reload cell holds a bare pointer, so the honest answer to "what can this call?" is
|
||||
"anything". A later optimisation may stop a function checking the channel; it may not
|
||||
@ -875,11 +928,9 @@ and the top are still live:</p>
|
||||
restart: retry
|
||||
restart: use-placeholder</code></pre>
|
||||
|
||||
<p>From there you fix the function, install it, and take a restart — and because control
|
||||
never left the erring frame, <code>retry</code> calls through the indirection cell and
|
||||
reaches the new body. Installing while stopped is allowed: the rule against swapping a
|
||||
function that is on the stack is about mid-frame consistency, and there is no frame in
|
||||
progress here.</p>
|
||||
<p>From there you fix the function, install it, and take a restart. Control never left
|
||||
the erring frame, so <code>retry</code> calls through the indirection cell and reaches
|
||||
the new body. Installing while stopped is allowed; there is no frame in progress.</p>
|
||||
|
||||
<p>The break loop lives in <code>vendor/agent</code>, which is an optional package. A
|
||||
program that does not import it leaves the hook null and stops the old way — the message
|
||||
@ -930,8 +981,8 @@ of — one line per binding:</p>
|
||||
[texture Texture2D x i32 y i32 tint Color]
|
||||
"DrawTexture")</code></pre>
|
||||
|
||||
<p>The reason for the wrapper is that <strong>an aggregate's calling convention is not
|
||||
part of its layout</strong>. On x86-64, clang gives raylib's own prototypes
|
||||
<p><strong>An aggregate's calling convention is not part of its layout.</strong> On
|
||||
x86-64, clang gives raylib's own prototypes
|
||||
<code><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
|
||||
@ -963,8 +1014,8 @@ void flan_shim_get_mouse_position_5ad0e205(flan_ty_Vector2_1bebc5ae *out) {
|
||||
}</code></pre>
|
||||
|
||||
<p>No library header is read, deliberately, so a build needs the shared library to be
|
||||
linkable and not the <code>-devel</code> package to be installed. What follows from that
|
||||
is what the generator can and cannot promise. <strong>Guaranteed:</strong> the C typedef
|
||||
linkable and not the <code>-devel</code> package to be installed.
|
||||
<strong>Guaranteed:</strong> the C typedef
|
||||
and the Flan struct come from the same <code>defstruct</code>, so they cannot disagree,
|
||||
and clang type-checks the wrapper against the generated prototype.
|
||||
<strong>Trusted:</strong> that the <code>defstruct</code> matches the library's real
|
||||
@ -985,7 +1036,7 @@ existing binding is unaffected.</p>
|
||||
|
||||
<h2 id="devloop">The dev loop</h2>
|
||||
|
||||
<p>This is the thesis of the project: edit the code, keep the sand.</p>
|
||||
<p>Edit the code, keep the sand.</p>
|
||||
|
||||
<pre><code class="sh">$ flan dev sand.flan</code></pre>
|
||||
|
||||
@ -996,7 +1047,7 @@ frame boundary. The window does not blink and the grid does not reset.</p>
|
||||
|
||||
<h3>How it works</h3>
|
||||
|
||||
<p>Four pieces, each of which can be run on its own.</p>
|
||||
<p>Four pieces, each runnable on its own.</p>
|
||||
|
||||
<p><strong>The reload primitive.</strong> <code>llc</code> → <code>ld -shared</code> →
|
||||
<code>dlopen</code> → call. Measured in this codebase:</p>
|
||||
@ -1011,9 +1062,9 @@ frame boundary. The window does not blink and the grid does not reset.</p>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>About 19ms end to end. The <code>clang</code> driver on the same IR is 50ms, which is
|
||||
why the dev path never invokes it: the driver forks a second process and re-does
|
||||
argument and target resolution, and codegen is not the cost.</p>
|
||||
<p>About 19ms end to end. The <code>clang</code> driver on the same IR is 50ms, so the
|
||||
dev path never invokes it. The driver forks a second process and re-does argument and
|
||||
target resolution; codegen is not the cost.</p>
|
||||
|
||||
<p><strong>Indirection cells.</strong> Loading a new body is not installing it. A call
|
||||
bound at link time cannot be made to notice one, so a <code>--dev</code> build routes
|
||||
@ -1021,7 +1072,7 @@ every Flan-to-Flan call through a cell — a mutable global holding the address
|
||||
function that is current.</p>
|
||||
|
||||
<p>Here is the whole of <code>hello.flan</code> through
|
||||
<code>flan emit --dev</code>, which is the shortest thing that shows it:</p>
|
||||
<code>flan emit --dev</code>:</p>
|
||||
|
||||
<pre><code class="llvm">@"flan.cell.print-line" = global ptr @"flan.print-line"
|
||||
|
||||
@ -1030,20 +1081,19 @@ entry:
|
||||
%t1 = load ptr, ptr @"flan.cell.print-line"
|
||||
%t2 = call {} %t1(%slice { ptr @".str.36", i64 15 }, ptr %xfer)</code></pre>
|
||||
|
||||
<p>Two things are visible there at once. The call site loads the cell rather than naming
|
||||
<code>@"flan.print-line"</code> directly, and the signature carries <code>ptr %xfer</code>
|
||||
— the transfer channel from <a href="#conditions">conditions</a>, which every Flan
|
||||
function has, release builds included.</p>
|
||||
<p>The call site loads the cell rather than naming <code>@"flan.print-line"</code>
|
||||
directly. The signature carries <code>ptr %xfer</code> — the transfer channel from
|
||||
<a href="#conditions">conditions</a>, on every Flan function, release builds
|
||||
included.</p>
|
||||
|
||||
<p>Redefinition is then one store, below a microsecond, which is what makes a
|
||||
frame-boundary swap a non-event. Three rules fall out of it. A redefinition module
|
||||
declares every <em>global</em> external, so globals live in the host and survive a reload
|
||||
— that is what "keep the sand" means. Every other function is a <code>declare</code>, so
|
||||
a redefined <code>settle</code> calls the host's <code>move-grain</code> rather than
|
||||
freezing a private copy of it. And nothing is ever <code>dlclose</code>d: a cell holds an
|
||||
address inside a module's text, so unloading it would leave call sites pointing at
|
||||
unmapped memory. Old code is never unloaded, which is also why a thread mid-execution
|
||||
finishes safely in the old version.</p>
|
||||
<p>Redefinition is then one store, below a microsecond. Three rules follow. A
|
||||
redefinition module declares every <em>global</em> external, so globals live in the host
|
||||
and survive a reload. Every other function is a <code>declare</code>, so a redefined
|
||||
<code>settle</code> calls the host's <code>move-grain</code> rather than freezing a
|
||||
private copy of it. And nothing is ever <code>dlclose</code>d: a cell holds an address
|
||||
inside a module's text, so unloading it would leave call sites pointing at unmapped
|
||||
memory. Old code is never unloaded, so a thread mid-execution finishes safely in the old
|
||||
version.</p>
|
||||
|
||||
<p><strong>The agent.</strong> <code>vendor/agent</code> is a package like any other: a
|
||||
listener thread, a single-producer ring, and three calls. This is the whole of
|
||||
@ -1060,11 +1110,10 @@ listener thread, a single-producer ring, and three calls. This is the whole of
|
||||
|
||||
<p><code>(agent/start path)</code> listens on a unix socket, once, at startup.
|
||||
<code>(agent/poll)</code> installs whatever has arrived and returns how many.
|
||||
<code>(agent/wait ms)</code> is the same but waits for something first, which is what a
|
||||
headless test uses so that a reload is deterministic rather than a race against the
|
||||
frame rate.</p>
|
||||
<code>(agent/wait ms)</code> is the same but waits for something first. A headless test
|
||||
uses it so that a reload is deterministic rather than a race against the frame rate.</p>
|
||||
|
||||
<p>The split between loading and installing is the design. <code>dlopen</code> relocates
|
||||
<p>Loading and installing are separate. <code>dlopen</code> relocates
|
||||
a module and takes the loader lock — milliseconds, unbounded — so it happens on the
|
||||
listener thread. Installing is one store per function and must not land while a redefined
|
||||
function is on the stack, so it happens on the game thread, at the top of the frame, when
|
||||
@ -1073,10 +1122,10 @@ ignores the result.</p>
|
||||
|
||||
<p><strong>The session and the daemon.</strong> <code>flan dev</code> holds the
|
||||
declarations the running process was built from plus every change accepted since, and it
|
||||
owns the build — which is what makes its rules describe the process that is actually
|
||||
running rather than a guess about it. Re-checking the whole program on every evaluation
|
||||
costs under 10ms, less than the <code>llc</code> that follows, and it makes an evaluation
|
||||
transactional for free: a form that fails to check mutates nothing.</p>
|
||||
owns the build, so its rules describe the process that is actually running. Re-checking
|
||||
the whole program on every evaluation costs under 10ms, less than the <code>llc</code>
|
||||
that follows. An evaluation is transactional: a form that fails to check mutates
|
||||
nothing.</p>
|
||||
|
||||
<p>The protocol is one s-expression per message, length-framed by a decimal byte count.
|
||||
It is not nREPL: <code>eval</code> there is string-in/string-out with no slot for
|
||||
@ -1100,23 +1149,23 @@ a silent mismatch against memory the process has already laid out:</p>
|
||||
</div>
|
||||
|
||||
<p>A <code>defvar</code>'s <em>initial value</em> is deliberately not on that list:
|
||||
refusing to change it would be refusing the whole point. And a <code>defconst</code> the
|
||||
checker never consumed can be changed, which is how a colour table gets tuned live while
|
||||
an array length stays refused — a dev build emits those as mutable globals so LLVM cannot
|
||||
its storage holds state the program moved past long ago. And a <code>defconst</code>
|
||||
the checker never consumed can be changed, so a colour table can be tuned live while an
|
||||
array length stays refused. A dev build emits those as mutable globals, so LLVM cannot
|
||||
fold a read of one.</p>
|
||||
|
||||
<p>The signature row is a stopgap and the message should not be read as the final answer.
|
||||
The design is versioned functions with their own trampolines, so that new callers resolve
|
||||
the new version while existing ones keep the old; none of the three parts exists yet, and
|
||||
the alternative to refusing is not the new design, it is a silent argument mismatch.</p>
|
||||
<p>The signature row is a stopgap. The design is versioned functions with their own
|
||||
trampolines, so that new callers resolve the new version while existing ones keep the
|
||||
old. None of the three parts exists yet, and the alternative to refusing is a silent
|
||||
argument mismatch.</p>
|
||||
|
||||
<h3>Evaluating an expression</h3>
|
||||
|
||||
<p><kbd>C-x C-e</kbd> is a different primitive from redefining a name. There is no name
|
||||
to install a body into, so the expression is wrapped in a thunk with nowhere to be called
|
||||
from; the module says <em>run this once</em>, and the agent calls it after the install, on
|
||||
the game thread, at a frame boundary — so an expression reading the program's state sees
|
||||
a point the program agrees is consistent.</p>
|
||||
from. The module says <em>run this once</em>, and the agent calls it after the install, on
|
||||
the game thread, at a frame boundary. An expression reading the program's state therefore
|
||||
sees a consistent one.</p>
|
||||
|
||||
<p>Nothing is marshalled back, because nothing could be: a Flan value carries no header,
|
||||
so no code at run time can say what it is. The compiler knows the type and renders it
|
||||
@ -1130,15 +1179,14 @@ b (Blob {:id 7 :name "sandy \"quoted\"" :pos (V {:x 1.5
|
||||
(rl/get-color 0x11223344) (rl/Color {:r 17 :g 34 :b 51 :a 68})
|
||||
sim/grid [ [ 0 0 0 0 0 0 0 0 ...] [ 0 ... ] ...]</code></pre>
|
||||
|
||||
<p>A pointer is never followed — it renders as <code><ptr></code> — because it is
|
||||
the only thing that could make the walk cycle, and dereferencing one a REPL was handed is
|
||||
not a safe thing to do on someone's behalf. The walk is bounded at depth 4 and 8 elements,
|
||||
and the output truncates at 4K. <code>Map</code>, function values and type variables
|
||||
refuse by name.</p>
|
||||
<p>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.
|
||||
<code>Map</code>, function values and type variables refuse by name.</p>
|
||||
|
||||
<p>The thunk's module is unloaded afterwards, which is the one case where that is safe:
|
||||
nothing points into its text once it has returned. Sixteen expression evaluations retain
|
||||
zero mappings, where each redefinition retains three, permanently and correctly.</p>
|
||||
<p>The thunk's module is unloaded afterwards. Nothing points into its text once it has
|
||||
returned. Sixteen expression evaluations retain zero mappings, where each redefinition
|
||||
retains three, permanently and correctly.</p>
|
||||
|
||||
<h2 id="emacs">Emacs</h2>
|
||||
|
||||
@ -1147,7 +1195,7 @@ zero mappings, where each redefinition retains three, permanently and correctly.
|
||||
already right. It adds Flan's brackets — <code>[</code> and <code>{</code> are brackets,
|
||||
not symbol characters, since every binding list and every type is written with them — and
|
||||
the characters a Flan name may contain. <code>emacs/flan-dev.el</code> is the client;
|
||||
there is no parser in it, which is the point of the protocol choice.</p>
|
||||
there is no parser in it.</p>
|
||||
|
||||
<div class="scroll">
|
||||
<table>
|
||||
@ -1189,22 +1237,28 @@ module, and the headless sand acceptance program prints the same 64-bit hash und
|
||||
it does natively:</p>
|
||||
|
||||
<pre><code class="sh">$ flan run test/programs/sand-headless.flan
|
||||
2256461126764447066
|
||||
-2851001042534928384
|
||||
$ flan build test/programs/sand-headless.flan --target=wasm32-wasi -o sand.wasm
|
||||
$ node --no-warnings test/wasm-run.mjs sand.wasm
|
||||
2256461126764447066</code></pre>
|
||||
-2851001042534928384</code></pre>
|
||||
|
||||
<p>That number is the whole point of writing the RNG in Flan rather than calling libc's:
|
||||
a grid hash is only a regression test if the sequence is byte-identical on both targets.
|
||||
It holds at <code>-O2</code> and at <code>-O0</code>. The wasm side needs a
|
||||
<p>The RNG is written in Flan rather than called from libc for that number: a grid hash
|
||||
is only a regression test if the sequence is byte-identical on both targets. It holds at
|
||||
<code>-O2</code> and at <code>-O0</code>. The wasm side needs a
|
||||
<code>wasm32</code> builtins archive — from wasi-sdk, or emscripten's substituting for
|
||||
it — and the compiler names every path it looked in when it cannot find one.</p>
|
||||
|
||||
<p>Dev and release builds are deliberately different. <code>--dev</code> means
|
||||
indirection cells and <code>-rdynamic</code>, which is what exports the cells for a loaded
|
||||
module to bind to; release builds call directly, emit constants as constants, and get all
|
||||
the folding back. Dev builds are not pruned by reachability, because what a REPL may
|
||||
redefine next is not a function of what has been called so far.</p>
|
||||
indirection cells and <code>-rdynamic</code>, which exports those cells for a loaded
|
||||
module to bind to. Release builds call directly, emit constants as constants, and get all
|
||||
the folding back. Dev builds are not pruned by reachability: what a REPL may redefine
|
||||
next is not a function of what has been called so far.</p>
|
||||
|
||||
<p><code>--debug</code> is a third flag beside <code>--dev</code> and the optimisation
|
||||
level. <code>--dev</code> asks whether you can redefine the program while it runs;
|
||||
<code>--debug</code> asks whether you can stop it and read it. It emits DWARF, sets
|
||||
<code>-O0</code>, and is refused by name for wasm32. lldb needs no plugin to read a
|
||||
Flan struct: the struct is its C struct.</p>
|
||||
|
||||
<p>Some things are refused by name rather than half-supported, and both cross-target
|
||||
refusals say why:</p>
|
||||
@ -1224,10 +1278,9 @@ target flags, so it never needs invalidating by hand.</p>
|
||||
|
||||
<h2 id="notyet">Not implemented yet</h2>
|
||||
|
||||
<p>The house rule is that anything which binds a name, alters control flow, or is not yet
|
||||
implemented must be recognised explicitly and rejected. So these are not missing features
|
||||
you discover as a strange type error — each refuses by name, with the milestone it belongs
|
||||
to, and the tests assert on the reason.</p>
|
||||
<p>The house rule is that anything which binds a name, alters control flow, or is not
|
||||
yet implemented must be recognised explicitly and rejected. Each of these refuses by
|
||||
name, with the milestone it belongs to, and the tests assert on the reason.</p>
|
||||
|
||||
<div class="scroll">
|
||||
<table>
|
||||
@ -1261,9 +1314,8 @@ managed <code>class</code> facility that plan.org describes is a plan and not a
|
||||
<code>put</code>, <code>resolve</code> and allocator-aware operations belong to
|
||||
<code>Vec</code> and <code>Map</code>, and arrive with them.</p>
|
||||
|
||||
<p>Two notes on what <em>is</em> settled, because their absence reads like an oversight.
|
||||
There is <strong>no interpreter</strong> and there is not going to be one: the compiled
|
||||
path is the only backend. The instrumentation-based step debugger that wanted one is cut,
|
||||
<p>Two of these are settled rather than pending. There is <strong>no interpreter</strong>
|
||||
and there is not going to be one: the compiled path is the only backend. The instrumentation-based step debugger that wanted one is cut,
|
||||
and compiled redefinition at ~19ms is perceptually instant for expression evaluation too.
|
||||
And the <strong>macro expander</strong> is blocked on unions rather than on itself — a
|
||||
macro is a function from <code>Form</code> to <code>Form</code>, which needs
|
||||
@ -1293,6 +1345,7 @@ macro is a function from <code>Form</code> to <code>Form</code>, which needs
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// A small hand-written highlighter for the Flan blocks. One pass, no library.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user