Merge branch 'worktree-agent-a1b90b5d487331391' into dev-loop
This commit is contained in:
commit
045b0a46b9
42
BUILT.md
42
BUILT.md
@ -227,7 +227,7 @@ form, no new `decl_kind`, no reader or parser change. A C symbol the package
|
||||
already binds by hand is left alone, so `declare-c` stays the escape hatch and
|
||||
stays the thing that wins.
|
||||
|
||||
A leading `?` makes a line optional, and `vendor/raylib` uses it. "No raylib
|
||||
A leading `?` makes a line optional, and `vendor/raylib` used it. "No raylib
|
||||
headers are needed" is a real property — a build needs libraylib linkable, not
|
||||
raylib-devel installed — and requiring a header would take it from everyone in
|
||||
order to give the check to whoever has one. Unset `FLAN_RAYLIB_H` and the build
|
||||
@ -235,6 +235,12 @@ is exactly what it was; set it and every signature is checked. A path that is
|
||||
*set and wrong* is an error naming it, because silently behaving as though
|
||||
nobody had opted in is the difference between an opt-in and a trap.
|
||||
|
||||
*(Superseded. The `?` marker is still what it says here, but `vendor/raylib` no
|
||||
longer uses it: the header is committed at `vendor/raylib/raylib-5.5.h`, the
|
||||
line names it directly, and the check runs on every build. See "The header is
|
||||
committed too" below, which is where the argument in this paragraph is
|
||||
answered.)*
|
||||
|
||||
#### What the diff found
|
||||
|
||||
The evidence the whole lane exists for. Against raylib **5.5** — the version
|
||||
@ -489,6 +495,34 @@ nothing and the read is purely the check. It is a *better* check than before:
|
||||
425 declarations rather than 172, because `generated.flan` is a package file
|
||||
like any other and is checked like one.
|
||||
|
||||
#### The header is committed too, and the check is no longer opt-in
|
||||
|
||||
The paragraph above is superseded: there is no `FLAN_RAYLIB_H`, and the header
|
||||
read is not conditional on anything. The header is committed at
|
||||
`vendor/raylib/raylib-5.5.h` and `headers` names that path directly, so the
|
||||
check runs on **every** build.
|
||||
|
||||
**What dissolved the opt-in argument is the commit, not a change of mind about
|
||||
the property.** "A build needs libraylib linkable and not raylib-devel
|
||||
installed" is still true and still the reason the opt-in existed — but nobody
|
||||
needs raylib-devel to have a file that ships with the repository, so requiring
|
||||
the header costs nobody anything. The trade the `?` was paying for stopped
|
||||
existing.
|
||||
|
||||
**What being optional actually cost was found the hard way**, and the story is in
|
||||
`vendor/raylib/headers` rather than repeated here. The short of it: the header a
|
||||
tree happened to have lived under a gitignored directory, so several parallel
|
||||
lanes were checking against nothing and were not told. **A check that silently
|
||||
does not run is worse than no check** — the failure mode of an opt-in is not
|
||||
"the check does not run", it is "the check does not run and the output looks the
|
||||
same".
|
||||
|
||||
There is no environment variable for the path any more. To check against a
|
||||
different header, edit the line or replace the file. The check still runs over
|
||||
every declaration in the package, hand-written and generated alike, and only the
|
||||
hand-written half can actually disagree — for the reason the section above gives,
|
||||
which is unchanged.
|
||||
|
||||
**`bindings`, beside `headers`, is what survives regeneration.** A committed
|
||||
generated file cannot be hand-corrected — the next run overwrites it and the
|
||||
edit is destroyed without anybody being told, which is the worst shape an edit
|
||||
@ -4678,9 +4712,11 @@ and how big, and a 16px tile drawn at 4x is a dest four times the source. `draw-
|
||||
scale; `draw-texture-ex` has the scale and no source. Neither half draws a tile.
|
||||
|
||||
It *was* reachable — with `FLAN_RAYLIB_H` exported the importer brings it in with 250-odd others — and that is the
|
||||
finding worth keeping. `vendor/raylib/headers` keeps the import opt-in on purpose, so a build needs libraylib linkable
|
||||
finding worth keeping. `vendor/raylib/headers` kept the import opt-in on purpose, so a build needs libraylib linkable
|
||||
and not raylib-devel installed. That property is worth keeping and it means **the default build had no draw call for a
|
||||
grid-based game**. The rule that follows: *a raylib function on a game's per-frame path is hand-written in
|
||||
grid-based game**. (The opt-in is gone — the header and the generated bindings are both committed now, so the default
|
||||
build has every declaration. The rule below is unaffected: it is about where a per-frame call is *written*, not about
|
||||
what the import happens to reach.) The rule that follows: *a raylib function on a game's per-frame path is hand-written in
|
||||
`raylib.flan` and checked against the header; it is not left to the import.* The import widens the surface; it must
|
||||
not be load-bearing.
|
||||
|
||||
|
||||
@ -227,7 +227,11 @@ would become an integer at the call site. A migration is therefore not a deletio
|
||||
kept list of the lines whose face is deliberately nicer than the header's.
|
||||
|
||||
**Answered: no, and the blocker is not the one above.** Vendoring stopped being the question once the *output* was
|
||||
committed rather than the header — no header is needed at any build. The count was also smaller than feared: 136 of
|
||||
committed rather than the header — no header is needed at any build. (Vendoring then happened anyway, and for the
|
||||
other reason: `vendor/raylib/raylib-5.5.h` is committed, `headers` names it with no `${...}` in front of it, and the
|
||||
signature check runs on every build. What that settled was not "can we build without a header" — the committed
|
||||
bindings had already settled that — but "does the check actually run", and behind an opt-in the answer in several
|
||||
worktrees was no.) The count was also smaller than feared: 136 of
|
||||
the 172 are exactly what the rule produces, and the other 36 are expressible as `name` overrides in `bindings`.
|
||||
|
||||
What actually decides it is that migration would gut the check. Everything the generator emits agrees with the header
|
||||
|
||||
45
PORTING.md
45
PORTING.md
@ -58,10 +58,13 @@ draw, the palette, and the tile riding the cursor are all of the drawing there i
|
||||
`Rectangle`, `Vector2` and `Color`, all four of which `raylib.flan` already describes, so
|
||||
it is a `declare-c` beside `draw-texture-rec` and nothing else.
|
||||
|
||||
**Why it did not already land.** It is importable — with `FLAN_RAYLIB_H` exported it
|
||||
comes in with the other 256. But `vendor/raylib/headers` says the header is opt-in on
|
||||
**Why it did not already land.** It was importable — with `FLAN_RAYLIB_H` exported it
|
||||
came in with the other 256. But `vendor/raylib/headers` made the header opt-in on
|
||||
purpose, so that a build needs libraylib linkable and *not* raylib-devel installed. That
|
||||
property is worth keeping, and it means the default build of this game has no renderer.
|
||||
property is worth keeping, and it meant the default build of this game had no renderer.
|
||||
*(Both halves of that changed after this was written: the generated bindings and the
|
||||
header are committed, so the default build has every declaration and the header check is
|
||||
not opt-in. The rule below survives it — see the note at the end of §1.)*
|
||||
The general rule this exposes is worth more than the one line: **a function the game
|
||||
calls every frame should be hand-written and checked against the header, not left to the
|
||||
import.** The import widens the surface; it should not be load-bearing.
|
||||
@ -112,10 +115,20 @@ carve reads out of a 2×1 image and the case goes red.
|
||||
**And the rule Tier 0 item 4 asked for, written down:** *a raylib function on a game's
|
||||
per-frame path is hand-written in `raylib.flan` and checked against the header; it is
|
||||
not left to the opt-in import.* The import widens the surface and is worth having, but a
|
||||
build that does not have `FLAN_RAYLIB_H` set is the default build, and the default build
|
||||
build that did not have `FLAN_RAYLIB_H` set was the default build, and the default build
|
||||
has to be able to draw. The test that goes with the rule is a link check, which is cheap
|
||||
and is all a GL-context call can have.
|
||||
|
||||
**Update — the justification moved, the rule did not.** There is no `FLAN_RAYLIB_H` any
|
||||
more. `vendor/raylib/generated.flan` holds every declaration the importer produced and is
|
||||
committed; `vendor/raylib/raylib-5.5.h` is committed beside it and `headers` names it
|
||||
directly, so the signature check runs on every build rather than on the builds that had a
|
||||
variable exported. The original argument — "the default build has no header, so it has no
|
||||
`DrawTexturePro`" — is therefore dead. The rule outlives it for a different reason: a
|
||||
per-frame call hand-written in `raylib.flan` is one a person transcribed, and the
|
||||
hand-written lines are the only declarations in the package a header can actually
|
||||
contradict. Everything generated agrees with the header by construction.
|
||||
|
||||
---
|
||||
|
||||
## 2. What looks like a gap and is not
|
||||
@ -254,8 +267,11 @@ project has queued generics for.
|
||||
`Rectangle`s. Twenty elements, once, at load.
|
||||
- `game.clj` `init-game`'s `mapv` over four `[keyword path]` pairs to textures. Four
|
||||
elements, once.
|
||||
- The prelude is monomorphic per element type (`sort-i32!`, `map-f32!`, `reduce-i32`), so
|
||||
anything over `[r c]` pairs has no helper at all.
|
||||
- The prelude was monomorphic per element type (`sort-i32!`, `map-f32!`, `reduce-i32`), so
|
||||
anything over `[r c]` pairs had no helper at all. *(Generics landed after this was
|
||||
written: those families are one `sort!`, `map!` and `reduce` now, each over a type
|
||||
variable, so a helper does exist for a slice of pairs. It does not change the count
|
||||
below — three loops, five lines each, at load time.)*
|
||||
|
||||
All three are `dotimes` with a `push`. Five lines each, at load time, run once.
|
||||
|
||||
@ -355,7 +371,7 @@ and Flan rejects it at compile time. Part of that rollback machinery is answerin
|
||||
dynamic-language problem and is not a gap. The out-of-bounds path is the part that
|
||||
survives the port, and it is the part that matters.
|
||||
|
||||
### The watch buffer has no equivalent
|
||||
### The watch buffer has no equivalent — *closed, Tier 1 item 5*
|
||||
|
||||
`watch.clj` is a pull-based watch: the game drops a snapshot of labelled values into an
|
||||
atom once per frame, and Emacs polls it on its own timer, so the watch rate is decoupled
|
||||
@ -377,6 +393,15 @@ shows you a number while you drag the mouse.
|
||||
Whether `flan dev`'s `eval` against a running program could serve it — it does not gate on
|
||||
`Running` the way `globals_op` does — I did not test, and should not be assumed.
|
||||
|
||||
**Closed.** Both halves are built. `M-x flan-watch` is the buffer, fed by a table the
|
||||
*program* writes and Emacs reads as memory — pushed rather than polled, so the values are
|
||||
as fresh as the last frame and are still there while the program is stopped.
|
||||
`watch-num-i64` / `watch-num-f64` are the `spy-num` half, keeping count/min/max/last/mean
|
||||
per slot with no formatting on the write path. `M-x flan-watch-ghost-mode` shows the same
|
||||
values inline at the call that wrote each one. The finding above stands as the reason the
|
||||
inspector was not the answer: it is stopped-only, and this is a different tool. See Tier 1
|
||||
item 5 and `emacs/MANUAL.md`.
|
||||
|
||||
### Offline image tooling
|
||||
|
||||
`sprite_atlas.clj` `auto-select-tiles` carves the atlas into 16x16 subimages and keeps the
|
||||
@ -509,8 +534,10 @@ not compete for the same slot.
|
||||
|
||||
4. ~~**Decide the rule the first item exposes.**~~ Decided and written into §1: *a raylib
|
||||
function on a game's per-frame path is hand-written in `raylib.flan` and checked
|
||||
against the header, not left to the opt-in import.* The default build has no
|
||||
`FLAN_RAYLIB_H` and the default build has to be able to draw.
|
||||
against the header, not left to the opt-in import.* The default build had no
|
||||
`FLAN_RAYLIB_H` and the default build has to be able to draw. (That justification is
|
||||
obsolete — the bindings and the header are both committed now — but the rule stands on
|
||||
the other leg: see the update at the end of §1.)
|
||||
|
||||
### Tier 1 — language and tooling, in the order that unblocks the most of this game
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ its newest release does.
|
||||
| `zig` | 2026-09-12 | Comptime, build system, error handling, C interop. |
|
||||
| `jank` | 2026-09-11 | A Clojure with native interop and a REPL into a running process. |
|
||||
| `janet` | 2025-09-29 | A small embeddable Lisp. `varfn` and its reload story. |
|
||||
| `raylib` | 2026-07-16 | The C library the FFI binds. **Note:** `vendor/raylib/web/raylib-5.5/` in *this* repo is the vendored 5.5 the bindings are checked against; the clone may be a different version. |
|
||||
| `raylib` | 2026-07-16 | The C library the FFI binds. **Note:** `vendor/raylib/raylib-5.5.h` in *this* repo is the committed header the bindings are checked against on every build; the clone may be a different version. It used to be whatever `vendor/raylib/web/` happened to hold, which is gitignored and therefore absent from a fresh worktree — the reason the header is tracked now. |
|
||||
|
||||
### Odin, specifically
|
||||
|
||||
@ -25,7 +25,9 @@ Most of what this repository copies. Places already cited in the notes:
|
||||
|
||||
- `base/runtime/core.odin:369` — `Map_Info`. The type-erased container runtime: `rawptr` keys, cell
|
||||
info for size and alignment, hash and equality as function pointers. This is why `Vec` and `Map`
|
||||
here need no generics, and it is the thing **not** to convert when generics lands.
|
||||
here need no generics, and it is the thing that was **not** converted when generics
|
||||
landed — `Vec` and `Map` are still type-erased, and a `$t` only reaches them as an
|
||||
element or key type.
|
||||
- `base/runtime/dynamic_map_internal.odin` — the map algorithm `(Map K V)` follows.
|
||||
- `base/runtime/dynamic_array_internal.odin` — the same for `Vec`.
|
||||
- `src/check_expr.cpp:470-510` — `find_or_generate_polymorphic_procedure`. The instantiation
|
||||
|
||||
@ -45,7 +45,8 @@ stack and neither end can see the other.
|
||||
#+end_src
|
||||
|
||||
~defer~ between the invoke and the target runs, innermost first, before the
|
||||
clause body. ~errdefer~ does not.
|
||||
clause body. (~errdefer~ would not, and is in *Not yet* below — it is refused by
|
||||
name today.)
|
||||
|
||||
* The break loop
|
||||
|
||||
@ -64,8 +65,8 @@ above it, or ~abort~.
|
||||
|
||||
* Not yet
|
||||
|
||||
~handler-case~ · ~find-restart~ · ~compute-restarts~ · a clause's report
|
||||
string. Each refused by name with its reason.
|
||||
~handler-case~ · ~find-restart~ · ~compute-restarts~ · ~errdefer~ · a clause's
|
||||
report string. Each refused by name with its reason.
|
||||
|
||||
A restart *with parameters* cannot be taken from the break loop: it aims at a
|
||||
frame by position and has nothing to fill the parameters in with, so the clause
|
||||
@ -91,6 +92,10 @@ the agent's socket instead.
|
||||
frame re-runs everything, mutations included; 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.
|
||||
~test/programs/frame-rollback.flan~ is the worked example of the snapshot,
|
||||
including the ordering that matters: restore *in the restart clause*, not in a
|
||||
~defer~ — a defer runs on the ordinary return path too, so that version silently
|
||||
rolls back the frames that succeeded.
|
||||
|
||||
This bites harder here than in most Lisps because the point is a *game loop*
|
||||
— skip the frame, carry on, don't die. Since a bad index signals
|
||||
|
||||
@ -355,6 +355,31 @@ never appeared would send you looking for a bug in the program. A name is
|
||||
truncated at 31 bytes and a rendered value at 192, with an ellipsis where a
|
||||
value was clipped.
|
||||
|
||||
**A number sampled thousands of times a frame — `watch-num-i64`,
|
||||
`watch-num-f64`.** The scalar entry points keep one value per name, and from a
|
||||
hot inner loop that is nearly useless: you see whichever of the 91,200 cells ran
|
||||
last. These two are the other half. A slot keeps **count, min, max, last and
|
||||
mean**, and the buffer renders them as one line:
|
||||
|
||||
```flan
|
||||
(declare-c watch-num-i64 [name string x i64] i32 "flan_dev_watch_num_i64")
|
||||
(declare-c watch-num-f64 [name string x f64] i32 "flan_dev_watch_num_f64")
|
||||
|
||||
(watch-num-i64 "cell" (at grid i))
|
||||
```
|
||||
|
||||
The slot renders as `n=… min=… max=… last=… mean=…`, one line per name.
|
||||
|
||||
Two entry points rather than one so a program need not cast at the call site.
|
||||
The write path does **no formatting** — a sample is a load, five compares and
|
||||
the slot's seqlock — and the listener thread renders once per editor tick, which
|
||||
is the whole reason this exists rather than a second `watch-i64`. **The window is
|
||||
since the editor's last tick**, not since the program started: a min and a max
|
||||
over a whole session reach the session's extremes within seconds and then never
|
||||
move again, so the two most useful of the five would go dead exactly when you
|
||||
start playing. A whole number prints as one, because a spy on an array index
|
||||
reading `66.0000` sends you looking for a rounding bug that is not there.
|
||||
|
||||
**Scalars only, so far.** `i64`, `u64`, `f64` and `string` have entry points; a
|
||||
struct or a slice does not. That is not an oversight in the runtime — a Flan
|
||||
value carries no header, so rendering one is a walk over its *type* at compile
|
||||
|
||||
60
plan.org
60
plan.org
@ -205,14 +205,21 @@ and on a managed ~class~ instance. An ordinary ~struct~ never carries one.
|
||||
the parser guessed between a return type and a body form from a table of type
|
||||
names; the guess was silently wrong twice, so the slot is mandatory.
|
||||
- Every type notation reads as exactly one data item: ~[f32]~, ~[4 f32]~,
|
||||
~(Vec f32)~, ~{string i32}~, ~(Ptr World)~, ~(Fn [f32] bool)~, ~(Option a)~,
|
||||
~(Handle a)~.
|
||||
~(Vec f32)~, ~(Map string i32)~, ~(Ptr World)~, ~(Fn [f32] bool)~,
|
||||
~(Option $t)~, ~(Handle $t)~. The map spelling was ~{string i32}~ once and is
|
||||
not any more: braces in type position are refused by name. The ~where~ clause
|
||||
below is a brace form at the head of a body, so keeping the brace type would
|
||||
have put ~(defn f [...] {string i32} {:where ...} body)~ in the language — two
|
||||
braces in a row meaning different things.
|
||||
- ~i8..i64~, ~u8..u64~, ~f32~, ~f64~ as real machine types; wrapping arithmetic.
|
||||
- Vector width 128-bit. Fixed arrays with component-wise ops and swizzles.
|
||||
- Parametric polymorphism by monomorphisation (Odin's model, no type classes, no
|
||||
HKTs). A type variable is written ~$t~ at its binding site in a signature and
|
||||
bare ~t~ at a use. This is what makes ~map~/~filter~/~reduce~ and the monomorphic
|
||||
containers work.
|
||||
HKTs). A type variable is written ~$t~ wherever a *type* goes — parameter,
|
||||
return type, or nested as ~[$t]~ or ~(Vec $t)~ — and bare ~t~ wherever a type's
|
||||
*name* is an argument in expression position: ~(vec-new t)~, ~(map-new t i32)~,
|
||||
~(pool-new t)~, and the cast ~(t x)~. This is what makes
|
||||
~map!~/~filter~/~reduce~ and the monomorphic containers work; it collapsed the
|
||||
prelude's per-type families into one function each.
|
||||
A generic body is checked *abstractly*, with nothing substituted, so ~=~, ~<~,
|
||||
~+~ and ~hash~ over an unconstrained variable are rejected at the definition
|
||||
rather than at whichever call site first instantiates it. **This is not Odin's
|
||||
@ -220,11 +227,28 @@ and on a managed ~class~ instance. An ordinary ~struct~ never carries one.
|
||||
decisions are independent even though this line once bundled them.
|
||||
What makes the rejection liveable is a ~where~ clause over compile-time type
|
||||
predicates, which is Odin's (~core/slice/slice.odin:289~,
|
||||
~where intrinsics.type_is_ordered(T)~). ~(sort! xs)~ declares ~ordered?~ of its
|
||||
~where intrinsics.type_is_ordered(T)~). It is written as a Clojure-style map at
|
||||
the head of the body — ~{:where (ordered? $t)}~, or a vector for more than one,
|
||||
~{:where [(copyable? $t) (copyable? $u)]}~ — on the precedent of Clojure's
|
||||
~{:pre ... :post ...}~, and because a bare ~{}~ in expression position is
|
||||
already refused so nothing else it could be. ~sort!~ declares ~ordered?~ of its
|
||||
variable, the abstract pass then allows ~<~ in the body, and each instantiation
|
||||
checks the concrete type satisfies the predicate and refuses the call site if it
|
||||
does not. Roughly four predicates are wanted here — ~ordered?~, ~equal?~,
|
||||
~hashable?~, ~numeric?~ — against Odin's forty-one.
|
||||
does not. There are *five* predicates — ~ordered?~, ~equal?~, ~hashable?~,
|
||||
~numeric?~, ~copyable?~ — against Odin's forty-one, and they entail one another
|
||||
in one direction, so one clause usually does: ~numeric?~ gives ~ordered?~,
|
||||
~ordered?~ gives ~equal?~, and any of the four gives ~copyable?~.
|
||||
~copyable?~ has no Odin counterpart, because Odin has no move semantics and a
|
||||
~$T~ there never has to answer the question. *A type variable is move-only by
|
||||
default* and ~copyable?~ is the opt-out: whether a variable is move-only is not
|
||||
decidable abstractly — ~i32~ at one instantiation, ~(Vec i32)~ at the next — so
|
||||
the checker takes the stricter rule, which can only refuse a program that would
|
||||
have been fine and never admit one that double-frees. The prior art is Rust's
|
||||
~T: Copy~, differing in that the compiler answers the question rather than a
|
||||
user implementing a trait.
|
||||
~hashable?~ is what lets a variable *key a map*: without it the type
|
||||
~(Map $t i32)~ is refused where it is written, and with it the refusal moves to
|
||||
the call site that names an unhashable key.
|
||||
This is not a type class and the difference is worth keeping straight: a type
|
||||
class carries *implementations* selected per instance and extensible by anyone,
|
||||
and needs dictionaries and coherence rules. A predicate carries nothing — it
|
||||
@ -232,11 +256,15 @@ and on a managed ~class~ instance. An ordinary ~struct~ never carries one.
|
||||
a user-defined ~<~; every operation the prelude and the containers need is a
|
||||
primitive, so it does not bind. Compile-time interfaces, if they are ever
|
||||
wanted, come after the base checker is stable.
|
||||
~println~ is a compiler-provided exception and needs an explicit allow-list of
|
||||
~println~ is a compiler-provided exception and is on an explicit allow-list of
|
||||
forms the abstract pass defers to instantiation: whether a printer exists for a
|
||||
variable is only decidable once it is substituted. Keep the list short — every
|
||||
member moves a refusal from the definition to a call site, which is what the
|
||||
abstract rule exists to avoid.
|
||||
variable is only decidable once it is substituted. The ~Map~ operations over a
|
||||
variable key are the other member, for the same reason — the hash and the
|
||||
equality are concrete symbols chosen from the concrete key type — and that one
|
||||
is paid for by ~hashable?~ being written in the signature, so the refusal it
|
||||
moves still lands against a requirement somebody wrote down. Keep the list
|
||||
short: every member moves a refusal from the definition to a call site, which
|
||||
is what the abstract rule exists to avoid.
|
||||
- Function values split three ways (spec-memory.md): ~(Fn [T1 T2] R)~ is a plain
|
||||
pointer with no environment — the only kind that crosses FFI or sits in a reload
|
||||
cell; a *non-escaping* ~fn~ captures enclosing locals by value into a stack
|
||||
@ -850,8 +878,12 @@ marked.
|
||||
- Dev backend → interpreter for milestone 2 certainly. Whether it /survives/
|
||||
milestone 3 is open, not settled — see Compilation.
|
||||
- ~set~ on places → a fixed list of assignable forms, not ~setf~.
|
||||
- Loop story → imperative ~while~/~for~ with ~break~/~continue~ and ~return~;
|
||||
~loop~/~recur~ only if it later earns its place. sand.flan is ported.
|
||||
- Loop story → imperative ~while~/~until~/~dotimes~ with ~break~/~continue~ and
|
||||
~return~; ~loop~/~recur~ only if it later earns its place. It did: both are
|
||||
built. ~break~ and ~continue~ take a label for the loop that is not the
|
||||
innermost, and ~loop~ is an *expression* whose value is its body's, with
|
||||
~recur~ rebinding every name at once and jumping rather than calling.
|
||||
sand.flan is ported.
|
||||
- Generic parameters → inferred at call sites, no explicit instantiation. No type
|
||||
classes. An operator over a variable with no ~where~ clause asserting it is
|
||||
rejected at the definition; a ~where~ predicate is what admits it.
|
||||
|
||||
438
web/index.html
438
web/index.html
@ -229,6 +229,7 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
|
||||
<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>
|
||||
@ -297,8 +298,9 @@ rather than programs.</p>
|
||||
|
||||
<p>A minimal Lisp for games. In one line: Odin with a Lisp frontend and a live REPL.
|
||||
Types are mandatory and inference makes them feel optional; memory is manual; the
|
||||
frontend is OCaml, the backend writes LLVM IR as text and hands it to
|
||||
<code>clang</code>.</p>
|
||||
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>
|
||||
|
||||
@ -326,7 +328,9 @@ $ ./_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] [--debug] [--target=wasm32-wasi]
|
||||
flan import-c <header.h> [package.flan...] [clang flags...]
|
||||
flan generate-c <package-dir>
|
||||
flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] [--debug] [--sanitize] [--x86] [--target=wasm32-wasi|web]
|
||||
flan run <file.flan> [args...]
|
||||
flan reload <program.flan> <forms.flan> [-o out.so]
|
||||
flan dev <program.flan> [-s socket]</code></pre>
|
||||
@ -444,9 +448,17 @@ 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>(Handle T)</code></td><td>a reference into a pool that reports a dead referent</td><td>index and generation packed into an <code>i64</code></td></tr>
|
||||
<tr><td><code>(Ptr T)</code></td><td>raw pointer</td><td>a pointer</td></tr>
|
||||
<tr><td><code>(Option T)</code></td><td><code>Some</code> / <code>None</code></td><td>tag byte + T</td></tr>
|
||||
<tr><td><code>(Fn [T ...] R)</code></td><td>a function value</td><td>a pointer</td></tr>
|
||||
<tr><td><code>Allocator</code></td><td>an opaque builtin: a proc, its data and a capability set</td><td>a pointer to that</td></tr>
|
||||
<tr><td><code>$t</code></td><td>a type variable — see <a href="#generics">generics</a></td><td>whatever it is instantiated at</td></tr>
|
||||
<tr><td>a struct</td><td>value type</td><td>fields in declaration order</td></tr>
|
||||
<tr><td>a union</td><td><code>defunion</code>, matched by case</td><td>tag + the widest payload</td></tr>
|
||||
<tr><td>an enum</td><td>its own type in the checker</td><td><code>i32</code></td></tr>
|
||||
<tr><td><code>()</code></td><td>one value, zero size</td><td>empty</td></tr>
|
||||
<tr><td><code>Never</code></td><td>fits anywhere; nothing has it</td><td>empty</td></tr>
|
||||
@ -597,7 +609,8 @@ whose type matters is named at the top level rather than written inline.</p>
|
||||
|
||||
<p><code>if</code>, <code>when</code>, <code>unless</code>, <code>cond</code>,
|
||||
<code>do</code>, <code>and</code>, <code>or</code>, <code>not</code>,
|
||||
<code>while</code>, <code>until</code>, <code>dotimes</code>, <code>return</code>,
|
||||
<code>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>
|
||||
|
||||
@ -641,33 +654,64 @@ unless runs when the test is false
|
||||
a body that changes it cannot change the trip count, and the loop variable is not
|
||||
assignable.</p>
|
||||
|
||||
<p>Loops are imperative, with <code>while</code>, <code>until</code> and
|
||||
<code>return</code>. There is no <code>loop</code>/<code>recur</code>. There is no
|
||||
<code>break</code> or <code>continue</code> yet either; both refuse by name:</p>
|
||||
<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 class="sh">break is not implemented yet (see the build sequence in plan.org)</code></pre>
|
||||
<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)))))
|
||||
|
||||
<p>An early exit out of a loop is <code>return</code>, as <code>first-even</code> does
|
||||
above.</p>
|
||||
(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 nothing else today. <code>some</code> unwraps <code>Some</code> and early-returns
|
||||
<code>None</code> from the enclosing function.</p>
|
||||
on a <code>defunion</code>, and on nothing else. <code>some</code> unwraps
|
||||
<code>Some</code> and early-returns <code>None</code> from the enclosing function.</p>
|
||||
|
||||
<pre><code>(defconst nums [4 i32] [4 8 15 16])
|
||||
|
||||
;; `some` unwraps Some and early-returns None from *this* function.
|
||||
(defn doubled-first [s [i32]] (Option i32)
|
||||
(Some (* 2 (some (index-of-i32 s 15)))))
|
||||
(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-i32 (slice nums 0 4) 99)
|
||||
(match (index-of (slice nums 0 4) 99)
|
||||
(Some i) (do (print i) (println ""))
|
||||
None (println "not found")))</code></pre>
|
||||
|
||||
@ -697,10 +741,19 @@ first
|
||||
second
|
||||
3</code></pre>
|
||||
|
||||
<p><code>defer</code> is function-scoped and is <strong>rejected</strong> inside a
|
||||
<code>let</code>, a loop or a branch. Block scoping it is not done:</p>
|
||||
<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 must be a top-level form in a function body — block-scoped defer is not implemented yet (milestone 4)</code></pre>
|
||||
<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>
|
||||
|
||||
@ -743,6 +796,131 @@ It is a place: <code>(set (at grid r c) v)</code> and <code>(addr (at grid r c))
|
||||
<p>A reversed range — <code>lo</code> greater than <code>hi</code> — traps, rather than
|
||||
yielding a huge unsigned length.</p>
|
||||
|
||||
<h2 id="generics">Generics</h2>
|
||||
|
||||
<p>Parametric polymorphism by monomorphisation: one body is written, and every call
|
||||
site gets a copy compiled at the types it passed. There are no type classes, no
|
||||
dictionaries and nothing decided at run time.</p>
|
||||
|
||||
<p><strong>A type variable is written <code>$t</code> where a type goes</strong> — a
|
||||
parameter's type, the return type, or nested inside a type constructor such as
|
||||
<code>[$t]</code> or <code>(Vec $t)</code>. The sigil is not a binding-site-only
|
||||
spelling: every type position writes it. <strong>Bare <code>t</code> is the same
|
||||
variable in <em>expression</em> position</strong>, where a builtin takes the name of a
|
||||
type as an argument — <code>(vec-new t)</code>, <code>(map-new t i32)</code>,
|
||||
<code>(pool-new t)</code>, and the cast <code>(t x)</code>.</p>
|
||||
|
||||
<pre><code>(defn ident [x $t] $t x) ; needs nothing declared
|
||||
|
||||
(defn twice [x $t] $t ; + - * / % need numeric?
|
||||
{:where (numeric? $t)}
|
||||
(+ x x))
|
||||
|
||||
(defn clamp-to [x $t lo $t hi $t] $t ; < <= > >= min max need ordered?
|
||||
{:where (ordered? $t)}
|
||||
(min (max x lo) hi))
|
||||
|
||||
(defn first-or [s [$t] d $t] $t ; the variable inside a slice type
|
||||
{:where (copyable? $t)}
|
||||
(if (= (len s) 0) d (at s 0)))
|
||||
|
||||
(defn one-of [x $t] (Vec $t) ; bare t is the type-name argument
|
||||
{:where (copyable? $t)}
|
||||
(let [v (vec-new t)]
|
||||
(push v x)
|
||||
v))
|
||||
|
||||
(defn main [] ()
|
||||
(println (ident 3))
|
||||
(println (ident "text"))
|
||||
(println (twice 1.5))
|
||||
(println (clamp-to 12 0 10))
|
||||
(let [ns [5 3 9 1]
|
||||
one (one-of 4.5)]
|
||||
(println (first-or (slice ns 0 4) -1))
|
||||
(println (at (as-slice one) 0))
|
||||
(free one)))</code></pre>
|
||||
|
||||
<pre><code class="sh">3
|
||||
text
|
||||
3
|
||||
10
|
||||
5
|
||||
4.5</code></pre>
|
||||
|
||||
<h3>The body is checked once, abstractly</h3>
|
||||
|
||||
<p>A generic body is checked <strong>with nothing substituted</strong>, so an operator
|
||||
the variable is not declared to support is refused at the definition rather than at
|
||||
whichever call site first reaches a type that happens to work. That is deliberately
|
||||
<em>not</em> Odin's rule, which checks a polymorphic body per instantiation:</p>
|
||||
|
||||
<pre><code class="sh">+ over the type variable t is refused: a type variable supports only what it is
|
||||
declared to support, and nothing here says t is numeric?. Write {:where (numeric?
|
||||
$t)} at the head of the body, or take the operation as a parameter — a
|
||||
(Fn [t t] ...) — and call it here</code></pre>
|
||||
|
||||
<p>What makes that liveable is a <code>where</code> clause, written as a Clojure-style
|
||||
map at the head of the body — <code>{:where (ordered? $t)}</code>, or a vector when
|
||||
there is more than one: <code>{:where [(copyable? $t) (copyable? $u)]}</code>. There
|
||||
are five predicates, and each gates builtins the compiler already has:</p>
|
||||
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<tr><th>Predicate</th><th>What it admits</th></tr>
|
||||
<tr><td><code>numeric?</code></td><td><code>+</code> <code>-</code> <code>*</code> <code>/</code> <code>%</code>, and a cast <code>(t x)</code></td></tr>
|
||||
<tr><td><code>ordered?</code></td><td><code><</code> <code><=</code> <code>></code> <code>>=</code> <code>min</code> <code>max</code></td></tr>
|
||||
<tr><td><code>equal?</code></td><td><code>=</code> and <code>!=</code></td></tr>
|
||||
<tr><td><code>hashable?</code></td><td>the variable as a <code>Map</code> key — <code>(map-new t V)</code>, <code>get</code>, <code>put</code>, <code>has-key?</code></td></tr>
|
||||
<tr><td><code>copyable?</code></td><td>reading the value more than once; <code>Pool</code> and <code>Vec</code> element positions</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>They entail each other in one direction, so one clause usually does:
|
||||
<code>numeric?</code> gives <code>ordered?</code>, <code>ordered?</code> gives
|
||||
<code>equal?</code>, and any of the four gives <code>copyable?</code>. A
|
||||
<code>sort!</code> that compares its elements and reads them twice declares
|
||||
<code>ordered?</code> and nothing else.</p>
|
||||
|
||||
<p><strong>A type variable is move-only by default</strong>, and
|
||||
<code>copyable?</code> is the opt-out. <code>Types.is_move_only</code> of a variable is
|
||||
not decidable abstractly — the same variable is <code>i32</code> at one instantiation
|
||||
and <code>(Vec i32)</code> at the next — so the checker assumes the stricter rule,
|
||||
which can only refuse a program that would have been fine and never admit one that
|
||||
double-frees. It is Rust's <code>T: Copy</code>, with the difference that the compiler
|
||||
answers the question rather than a user implementing a trait. So
|
||||
<code>(defn twice [x $t] $t (+ x x))</code> does not merely want
|
||||
<code>numeric?</code>; reading <code>x</code> a second time is a use after move:</p>
|
||||
|
||||
<pre><code class="sh">x was moved at twice.flan:1:26 and cannot be used again — t is move-only, so
|
||||
binding, passing or returning one transfers ownership and the source binding is
|
||||
dead afterwards (spec-memory.md). That rule is what makes a double free
|
||||
unrepresentable; (clone x) if you wanted a second one</code></pre>
|
||||
|
||||
<p>Each instantiation then checks the concrete type against what the signature declared,
|
||||
and refuses the <em>call site</em> when it does not answer:</p>
|
||||
|
||||
<pre><code class="sh">this call instantiates twice at $t = bool, and bool does not answer numeric? —
|
||||
which twice requires, being written {:where (numeric? $t)}. The requirement is the
|
||||
signature's, so the refusal is here, at the call that asked for the type: pass one
|
||||
the predicate admits</code></pre>
|
||||
|
||||
<p>Two forms are deferred to the instantiation rather than settled abstractly, because
|
||||
their legality is only decidable after substituting: <code>println</code> over a
|
||||
variable, which selects the structural printer per copy, and the <code>Map</code>
|
||||
operations over a variable key, whose hash and equality are concrete symbols chosen from
|
||||
the concrete key type. The <code>Map</code> half is what <code>hashable?</code> buys —
|
||||
without the clause, the type <code>(Map $t i32)</code> is refused where it is
|
||||
<em>written</em>, and with it the refusal moves to the call site that names an
|
||||
unhashable key.</p>
|
||||
|
||||
<p>This is not a type class and the difference is worth keeping straight. A type class
|
||||
carries <em>implementations</em>, selected per instance and extensible by anyone, and
|
||||
needs dictionaries and coherence rules. A predicate carries nothing — it gates a builtin
|
||||
that already exists. The ceiling is that nobody can supply a <code><</code> of their
|
||||
own; every operation the prelude and the containers need is a primitive, so it does not
|
||||
bind. <code>test/programs/generics.flan</code> exercises the whole of it.</p>
|
||||
|
||||
<h2 id="printing">Printing</h2>
|
||||
|
||||
<p><code>println</code> prints a value and a newline; <code>print</code> is the same
|
||||
@ -775,7 +953,11 @@ 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, fixed arrays and slices. An enum member comes back as its name: the value is
|
||||
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
|
||||
@ -800,7 +982,7 @@ 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 one line of it, and prepended to every
|
||||
<p>The prelude is written in Flan, all but five lines of it, and prepended to every
|
||||
program, so nothing in it needs importing. It holds no printing of its own:
|
||||
<code>print</code> and <code>println</code> are the compiler's, and
|
||||
<code>write-stdout</code> — the one output primitive — is what they are written
|
||||
@ -809,16 +991,27 @@ over.</p>
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<tr><th>Group</th><th>Names</th></tr>
|
||||
<tr><td>slices of <code>i32</code></td><td><code>swap-i32!</code>, <code>reverse-i32!</code>, <code>sort-i32!</code>, <code>index-of-i32</code>, <code>min-i32</code>, <code>max-i32</code>, <code>sum-i32</code></td></tr>
|
||||
<tr><td>bytes</td><td><code>bytes=?</code>, <code>starts-with?</code>, <code>ends-with?</code>, <code>index-of-byte</code>, <code>index-of-bytes</code>, <code>trim</code>, <code>digit?</code>, <code>space?</code></td></tr>
|
||||
<tr><td>slice algorithms, over one type variable</td><td><code>swap!</code>, <code>reverse!</code>, <code>sort!</code>, <code>sort-by!</code>, <code>index-of</code>, <code>min-of</code>, <code>max-of</code>, <code>map!</code>, <code>reduce</code>, <code>filter</code></td></tr>
|
||||
<tr><td>the per-type layer that stays</td><td><code>sum-i32</code>, <code>sum-f32</code> — the element and the accumulator are different types, which one variable cannot say</td></tr>
|
||||
<tr><td>bytes</td><td><code>bytes=?</code>, <code>bytes<?</code>, <code>bytes-ci=?</code>, <code>starts-with?</code>, <code>ends-with?</code>, <code>index-of-bytes</code>, <code>trim</code>, <code>digit?</code>, <code>space?</code>, <code>sort-bytes!</code></td></tr>
|
||||
<tr><td>parsing</td><td><code>parse-i64</code>, <code>parse-f64</code></td></tr>
|
||||
<tr><td>text</td><td><code>split-on-byte</code>, <code>split-next!</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>bytes-ci=?</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>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>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>clamp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, and the five <code>declare</code>s: <code>sqrt-f32</code>, <code>sin-f32</code>, <code>cos-f32</code>, <code>atan2-f32</code>, <code>pow-f32</code></td></tr>
|
||||
<tr><td>random</td><td><code>rand-seed</code>, <code>rand-u32</code>, <code>rand-f32</code>, <code>rand-i32-range</code>, <code>rand-f32-range</code></td></tr>
|
||||
<tr><td>forms, for macros</td><td><code>form-nil</code>, <code>form-cons</code>, <code>form-append</code>, <code>form-rest</code>, <code>form-items</code>, <code>form-pair</code>, <code>form-sym?</code>, <code>form-is-sym?</code>, <code>gensym</code>, and <code>unless</code> and <code>into</code>, which are macros written here rather than special forms</td></tr>
|
||||
<tr><td>the rest</td><td><code>pause</code>, which signals the <code>Pause</code> condition the break loop stops on, and <code>embed-find</code></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p><strong>One family, not one per type.</strong> The slice algorithms used to be
|
||||
<code>sort-i32!</code> beside <code>sort-f32!</code> beside <code>sort-bytes!</code>, and
|
||||
generics collapsed them: <code>sort!</code> is written once and instantiated at whatever
|
||||
element type the call passes. <code>sum-i32</code> and <code>sum-f32</code> are what did
|
||||
<em>not</em> collapse, and they are the honest exception — each widens its element into a
|
||||
different accumulator, which one variable cannot express.</p>
|
||||
|
||||
<p><strong>The RNG is ours, not libc's</strong> — PCG-XSH-RR 32, written in Flan —
|
||||
because a grid hash is only a regression test if the sequence is byte-identical on
|
||||
native and on wasm32. <strong>The parsers are ours too</strong>:
|
||||
@ -826,16 +1019,18 @@ native and on wasm32. <strong>The parsers are ours too</strong>:
|
||||
<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> 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. 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. <code>(println 1)</code> is <code>unknown function println</code>.</p>
|
||||
<p>Five functions in the file are not Flan, and they are libm's:
|
||||
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code> and the same line for
|
||||
<code>sinf</code>, <code>cosf</code>, <code>atan2f</code> and <code>powf</code>. Every
|
||||
other number here is reachable from the four operations and a cast; a square root is
|
||||
not, and the usual trick of seeding Newton's method from the exponent bits needs a
|
||||
bit-cast between <code>f32</code> and <code>u32</code> that the language does not have.
|
||||
IEEE-754 makes <code>sqrt</code> correctly rounded, so libm gives the same bit pattern
|
||||
on both targets anyway. <strong>The other four are not</strong>: IEEE-754 requires
|
||||
nothing of <code>sinf</code>, <code>cosf</code>, <code>atan2f</code> or
|
||||
<code>powf</code>, and glibc, musl and wasi-libc do differ in the last bit — so the
|
||||
byte-identical-hash property the RNG exists for does not survive a hash routed through
|
||||
any of them. Every link carries <code>-lm</code>.</p>
|
||||
|
||||
<p>The primitives underneath are few — a primitive is the only thing implemented
|
||||
twice per backend: <code>argv</code>,
|
||||
@ -930,9 +1125,9 @@ carries on.</p>
|
||||
(handler-bind [(Type [c] body ...) ...] body ...) ; match by type, no hierarchy
|
||||
|
||||
(restart-case BODY ; BODY and every clause have the same type = the form's
|
||||
(name [] CLAUSE) ...)
|
||||
(name [p T ...] CLAUSE) ...)
|
||||
|
||||
(invoke-restart 'name) ; Never. Innermost frame offering the name wins.</code></pre>
|
||||
(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>
|
||||
@ -991,6 +1186,20 @@ first, before the clause body starts.</p>
|
||||
-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
|
||||
@ -1032,7 +1241,11 @@ a guard after each call.</p>
|
||||
<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.</p>
|
||||
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> instead of ending the process,
|
||||
@ -1047,7 +1260,14 @@ a guard after each call.</p>
|
||||
<code>restart-case</code> body</strong>, and so is <code>invoke-restart</code> inside a
|
||||
<code>defer</code>. In each case a bare exit would leave frames on the stack pointing
|
||||
into a function that has gone.</li>
|
||||
<li><strong>Restarts take no parameters</strong> in this version.</li>
|
||||
<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>
|
||||
@ -1151,8 +1371,9 @@ void flan_shim_get_mouse_position_5ad0e205(flan_ty_Vector2_1bebc5ae *out) {
|
||||
*out = GetMousePosition();
|
||||
}</code></pre>
|
||||
|
||||
<p>No library header is read during a build, deliberately, so a build needs the shared
|
||||
library to be linkable and not the <code>-devel</code> package to be installed.
|
||||
<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.
|
||||
@ -1172,24 +1393,38 @@ directory may carry a <code>headers</code> file naming the library's own C heade
|
||||
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">$ export FLAN_RAYLIB_H=/path/to/raylib-5.5/src/raylib.h
|
||||
$ flan generate-c vendor/raylib
|
||||
wrote vendor/raylib/generated.flan: 253 declarations, 156 refused, of 581 functions.
|
||||
Every defstruct and every hand-written declare-c agrees with it.</code></pre>
|
||||
<pre><code class="sh">$ flan generate-c vendor/raylib
|
||||
wrote vendor/raylib/generated.flan: 269 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 rather than generating at build time is what keeps the
|
||||
no-header property honest: the declarations are in the repository, so 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>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>Regeneration is the check.</strong> The cost of committing the output is that
|
||||
nothing compares the bindings against reality on every build any more, so the one
|
||||
function that writes the file 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><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
|
||||
@ -1387,8 +1622,12 @@ 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.
|
||||
<code>Map</code>, function values and type variables refuse by name.</p>
|
||||
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
|
||||
@ -1430,8 +1669,10 @@ something surprising.</p>
|
||||
<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>
|
||||
@ -1447,6 +1688,15 @@ something surprising.</p>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>Some commands have no key. <code>M-x flan-dev</code> starts a program and
|
||||
<code>M-x flan-dev-quit</code> stops it. <code>M-x flan-watch</code> opens the watch
|
||||
buffer, which shows values <em>while the program runs</em> rather than while it is
|
||||
stopped, and <code>M-x flan-watch-ghost-mode</code> shows the same values inline at the
|
||||
call that wrote each one. <code>M-x flan-inspect-address</code> roots an inspection at a
|
||||
raw address rather than at an expression. <code>M-x flan-allocations</code> and
|
||||
<code>M-x flan-leaks</code> read the allocation registry: every block it recorded grouped
|
||||
by type, and the same walk with the dead left out.</p>
|
||||
|
||||
<p><kbd>C-c C-k</kbd> sends one module rather than a form at a time on purpose: a
|
||||
<code>defvar</code> and the function that uses it have to arrive in the same load, or the
|
||||
first refers to storage that does not exist yet.</p>
|
||||
@ -1573,6 +1823,23 @@ module to bind to. Release builds call directly, emit constants as constants, an
|
||||
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 covers a subset of the IR and <strong>refuses the rest by name</strong>, so a
|
||||
build that succeeds is one it really compiled rather than one it half-compiled.
|
||||
Conditions are the visible gap — anything reaching the transfer channel is refused:</p>
|
||||
|
||||
<pre><code class="sh">$ flan build test/programs/algorithms.flan --x86
|
||||
Fatal error: exception Flan.X86.Unsupported("restart-case needs the transfer
|
||||
channel, which this backend does not emit a guard for")</code></pre>
|
||||
|
||||
<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
|
||||
@ -1604,41 +1871,42 @@ name, with the milestone it belongs to, and the tests assert on the reason.</p>
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<tr><th>You write</th><th>The compiler says</th></tr>
|
||||
<tr><td><code>(Vec T)</code></td><td>(Vec T) is not implemented yet — milestone 6 (see plan.org)</td></tr>
|
||||
<tr><td><code>(Map K V)</code></td><td>(Map K V) is not implemented yet — milestone 6 (see plan.org)</td></tr>
|
||||
<tr><td><code>(Result T E)</code></td><td>(Result T E) is not implemented yet — milestone 6 (see plan.org)</td></tr>
|
||||
<tr><td><code>(Handle T)</code></td><td>(Handle T) is not implemented yet — milestone 6 (see plan.org)</td></tr>
|
||||
<tr><td><code>(try …)</code></td><td>try (Result) is not implemented yet — milestone 6 (see plan.org)</td></tr>
|
||||
<tr><td>a union type</td><td>the union type Shape is not implemented yet — milestone 6 (see plan.org)</td></tr>
|
||||
<tr><td><code>(Fn [T] R)</code></td><td>a function type is not implemented yet — milestone 5 (see plan.org)</td></tr>
|
||||
<tr><td><code>(fn [x i32] …)</code></td><td>calling something other than a named function is not implemented yet — milestone 5 (see plan.org)</td></tr>
|
||||
<tr><td>a type variable</td><td>generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)</td></tr>
|
||||
<tr><td><code>'sym</code></td><td>a quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org)</td></tr>
|
||||
<tr><td><code>(defmacro …)</code></td><td>parses, but is not expanded: running a macro means compiling it and loading it into the compiler, which is not wired up yet</td></tr>
|
||||
<tr><td><code>`(a ~b)</code></td><td>is read, but not expanded: macro expansion is not wired up yet</td></tr>
|
||||
<tr><td><code>handler-case</code></td><td>handler-case is not implemented yet</td></tr>
|
||||
<tr><td><code>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet</td></tr>
|
||||
<tr><td><code>'sym</code> as a value</td><td>a quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org)</td></tr>
|
||||
<tr><td>a bare lowercase type name</td><td>generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)</td></tr>
|
||||
<tr><td><code>errdefer</code></td><td>errdefer is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||||
<tr><td><code>await</code></td><td>await is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||||
<tr><td><code>handler-case</code></td><td>handler-case is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||||
<tr><td><code>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||||
<tr><td>a union as a <code>declare</code> parameter</td><td>a parameter of g is U, which cannot cross to C directly — pass (Ptr U) and let the shim read it</td></tr>
|
||||
<tr><td>a user-written allocator</td><td>a user-written allocator is not implemented yet, and a defn's name in that position …</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>Beyond that list, and just as true: there is no allocator and no
|
||||
<code>context</code>; there is no <code>println</code> and no overloading; restarts take
|
||||
no parameters; <code>match</code> works on an <code>Option</code> and nothing else;
|
||||
<code>defer</code> is function-scoped; there is no package-private marker other than
|
||||
<code>main</code> not being exported; there are no threads in the language; and the
|
||||
managed <code>class</code> facility that plan.org describes is a plan and not a feature.</p>
|
||||
<p>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>The vocabulary in <code>spec-memory.md</code> is normative but largely unbuilt:
|
||||
<code>clone</code>, <code>as-slice</code>, <code>push</code>, <code>get</code>,
|
||||
<code>put</code>, <code>resolve</code> and allocator-aware operations belong to
|
||||
<code>Vec</code> and <code>Map</code>, and arrive with them.</p>
|
||||
<p>Beyond that list, and just as true: there is no overloading; <code>defer</code> is
|
||||
refused inside a loop or a branch (a <code>let</code> is fine — it has the function's
|
||||
extent); <code>find-restart</code> and <code>compute-restarts</code> are blocked on a
|
||||
<code>Restart</code> type rather than on effort; a restart with parameters cannot be
|
||||
taken from the break loop, which aims at a frame by position and has nothing to fill them
|
||||
with; there is no package-private marker other than <code>main</code> not being exported;
|
||||
there are no threads in the language; and the managed <code>class</code> facility that
|
||||
plan.org describes is a plan and not a feature.</p>
|
||||
|
||||
<p>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
|
||||
<code>Form</code> to exist as a Flan union value first.</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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user