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
|
already binds by hand is left alone, so `declare-c` stays the escape hatch and
|
||||||
stays the thing that wins.
|
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
|
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
|
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
|
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
|
*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.
|
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
|
#### What the diff found
|
||||||
|
|
||||||
The evidence the whole lane exists for. Against raylib **5.5** — the version
|
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
|
425 declarations rather than 172, because `generated.flan` is a package file
|
||||||
like any other and is checked like one.
|
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
|
**`bindings`, beside `headers`, is what survives regeneration.** A committed
|
||||||
generated file cannot be hand-corrected — the next run overwrites it and the
|
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
|
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.
|
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
|
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
|
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
|
`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.
|
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.
|
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
|
**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`.
|
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
|
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
|
`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.
|
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
|
**Why it did not already land.** It was importable — with `FLAN_RAYLIB_H` exported it
|
||||||
comes in with the other 256. But `vendor/raylib/headers` says the header is opt-in on
|
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
|
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
|
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
|
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.
|
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
|
**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
|
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
|
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
|
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.
|
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
|
## 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.
|
`Rectangle`s. Twenty elements, once, at load.
|
||||||
- `game.clj` `init-game`'s `mapv` over four `[keyword path]` pairs to textures. Four
|
- `game.clj` `init-game`'s `mapv` over four `[keyword path]` pairs to textures. Four
|
||||||
elements, once.
|
elements, once.
|
||||||
- The prelude is monomorphic per element type (`sort-i32!`, `map-f32!`, `reduce-i32`), so
|
- The prelude was monomorphic per element type (`sort-i32!`, `map-f32!`, `reduce-i32`), so
|
||||||
anything over `[r c]` pairs has no helper at all.
|
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.
|
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
|
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.
|
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
|
`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
|
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
|
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.
|
`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
|
### Offline image tooling
|
||||||
|
|
||||||
`sprite_atlas.clj` `auto-select-tiles` carves the atlas into 16x16 subimages and keeps the
|
`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
|
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
|
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
|
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.
|
`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
|
### 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. |
|
| `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. |
|
| `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. |
|
| `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
|
### 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
|
- `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`
|
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_map_internal.odin` — the map algorithm `(Map K V)` follows.
|
||||||
- `base/runtime/dynamic_array_internal.odin` — the same for `Vec`.
|
- `base/runtime/dynamic_array_internal.odin` — the same for `Vec`.
|
||||||
- `src/check_expr.cpp:470-510` — `find_or_generate_polymorphic_procedure`. The instantiation
|
- `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
|
#+end_src
|
||||||
|
|
||||||
~defer~ between the invoke and the target runs, innermost first, before the
|
~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
|
* The break loop
|
||||||
|
|
||||||
@ -64,8 +65,8 @@ above it, or ~abort~.
|
|||||||
|
|
||||||
* Not yet
|
* Not yet
|
||||||
|
|
||||||
~handler-case~ · ~find-restart~ · ~compute-restarts~ · a clause's report
|
~handler-case~ · ~find-restart~ · ~compute-restarts~ · ~errdefer~ · a clause's
|
||||||
string. Each refused by name with its reason.
|
report string. Each refused by name with its reason.
|
||||||
|
|
||||||
A restart *with parameters* cannot be taken from the break loop: it aims at a
|
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
|
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
|
frame re-runs everything, mutations included; one placed after the mutations
|
||||||
re-runs only what follows. Put the restart before anything mutates, make the
|
re-runs only what follows. Put the restart before anything mutates, make the
|
||||||
retried section idempotent, or snapshot what will be re-applied.
|
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*
|
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
|
— 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
|
truncated at 31 bytes and a rendered value at 192, with an ellipsis where a
|
||||||
value was clipped.
|
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
|
**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
|
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
|
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
|
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.
|
names; the guess was silently wrong twice, so the slot is mandatory.
|
||||||
- Every type notation reads as exactly one data item: ~[f32]~, ~[4 f32]~,
|
- Every type notation reads as exactly one data item: ~[f32]~, ~[4 f32]~,
|
||||||
~(Vec f32)~, ~{string i32}~, ~(Ptr World)~, ~(Fn [f32] bool)~, ~(Option a)~,
|
~(Vec f32)~, ~(Map string i32)~, ~(Ptr World)~, ~(Fn [f32] bool)~,
|
||||||
~(Handle a)~.
|
~(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.
|
- ~i8..i64~, ~u8..u64~, ~f32~, ~f64~ as real machine types; wrapping arithmetic.
|
||||||
- Vector width 128-bit. Fixed arrays with component-wise ops and swizzles.
|
- Vector width 128-bit. Fixed arrays with component-wise ops and swizzles.
|
||||||
- Parametric polymorphism by monomorphisation (Odin's model, no type classes, no
|
- 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
|
HKTs). A type variable is written ~$t~ wherever a *type* goes — parameter,
|
||||||
bare ~t~ at a use. This is what makes ~map~/~filter~/~reduce~ and the monomorphic
|
return type, or nested as ~[$t]~ or ~(Vec $t)~ — and bare ~t~ wherever a type's
|
||||||
containers work.
|
*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 ~=~, ~<~,
|
A generic body is checked *abstractly*, with nothing substituted, so ~=~, ~<~,
|
||||||
~+~ and ~hash~ over an unconstrained variable are rejected at the definition
|
~+~ 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
|
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.
|
decisions are independent even though this line once bundled them.
|
||||||
What makes the rejection liveable is a ~where~ clause over compile-time type
|
What makes the rejection liveable is a ~where~ clause over compile-time type
|
||||||
predicates, which is Odin's (~core/slice/slice.odin:289~,
|
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
|
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
|
checks the concrete type satisfies the predicate and refuses the call site if it
|
||||||
does not. Roughly four predicates are wanted here — ~ordered?~, ~equal?~,
|
does not. There are *five* predicates — ~ordered?~, ~equal?~, ~hashable?~,
|
||||||
~hashable?~, ~numeric?~ — against Odin's forty-one.
|
~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
|
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,
|
class carries *implementations* selected per instance and extensible by anyone,
|
||||||
and needs dictionaries and coherence rules. A predicate carries nothing — it
|
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
|
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
|
primitive, so it does not bind. Compile-time interfaces, if they are ever
|
||||||
wanted, come after the base checker is stable.
|
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
|
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
|
variable is only decidable once it is substituted. The ~Map~ operations over a
|
||||||
member moves a refusal from the definition to a call site, which is what the
|
variable key are the other member, for the same reason — the hash and the
|
||||||
abstract rule exists to avoid.
|
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
|
- 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
|
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
|
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/
|
- Dev backend → interpreter for milestone 2 certainly. Whether it /survives/
|
||||||
milestone 3 is open, not settled — see Compilation.
|
milestone 3 is open, not settled — see Compilation.
|
||||||
- ~set~ on places → a fixed list of assignable forms, not ~setf~.
|
- ~set~ on places → a fixed list of assignable forms, not ~setf~.
|
||||||
- Loop story → imperative ~while~/~for~ with ~break~/~continue~ and ~return~;
|
- Loop story → imperative ~while~/~until~/~dotimes~ with ~break~/~continue~ and
|
||||||
~loop~/~recur~ only if it later earns its place. sand.flan is ported.
|
~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
|
- Generic parameters → inferred at call sites, no explicit instantiation. No type
|
||||||
classes. An operator over a variable with no ~where~ clause asserting it is
|
classes. An operator over a variable with no ~where~ clause asserting it is
|
||||||
rejected at the definition; a ~where~ predicate is what admits it.
|
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="#control">Control flow</a></li>
|
||||||
<li><a href="#defer">defer</a></li>
|
<li><a href="#defer">defer</a></li>
|
||||||
<li><a href="#arrays">Arrays and slices</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="#printing">Printing</a></li>
|
||||||
<li><a href="#prelude">The prelude</a></li>
|
<li><a href="#prelude">The prelude</a></li>
|
||||||
<li><a href="#packages">Packages</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.
|
<p>A minimal Lisp for games. In one line: Odin with a Lisp frontend and a live REPL.
|
||||||
Types are mandatory and inference makes them feel optional; memory is manual; the
|
Types are 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
|
frontend is OCaml, and the default backend writes LLVM IR as text and hands it to
|
||||||
<code>clang</code>.</p>
|
<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>
|
<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
|
<pre><code class="sh">$ flan
|
||||||
usage: flan (read|parse|check|emit|shim) <file.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 run <file.flan> [args...]
|
||||||
flan reload <program.flan> <forms.flan> [-o out.so]
|
flan reload <program.flan> <forms.flan> [-o out.so]
|
||||||
flan dev <program.flan> [-s socket]</code></pre>
|
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>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>[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>[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>(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>(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 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>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>()</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>
|
<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>,
|
<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>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>match</code>. <code>and</code> and <code>or</code> short-circuit.
|
||||||
<code>:else</code> is <code>cond</code>'s catch-all.</p>
|
<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
|
a body that changes it cannot change the trip count, and the loop variable is not
|
||||||
assignable.</p>
|
assignable.</p>
|
||||||
|
|
||||||
<p>Loops are imperative, with <code>while</code>, <code>until</code> and
|
<p><code>break</code> and <code>continue</code> leave or restart the innermost loop, and
|
||||||
<code>return</code>. There is no <code>loop</code>/<code>recur</code>. There is no
|
take a <strong>label</strong> when that is not the one meant. <code>loop</code> and
|
||||||
<code>break</code> or <code>continue</code> yet either; both refuse by name:</p>
|
<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
|
(defn main [] ()
|
||||||
above.</p>
|
(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>
|
<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
|
<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
|
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
|
on a <code>defunion</code>, and on nothing else. <code>some</code> unwraps
|
||||||
<code>None</code> from the enclosing function.</p>
|
<code>Some</code> and early-returns <code>None</code> from the enclosing function.</p>
|
||||||
|
|
||||||
<pre><code>(defconst nums [4 i32] [4 8 15 16])
|
<pre><code>(defconst nums [4 i32] [4 8 15 16])
|
||||||
|
|
||||||
;; `some` unwraps Some and early-returns None from *this* function.
|
;; `some` unwraps Some and early-returns None from *this* function.
|
||||||
(defn doubled-first [s [i32]] (Option i32)
|
(defn doubled-first [s [i32]] (Option i32)
|
||||||
(Some (* 2 (some (index-of-i32 s 15)))))
|
(Some (* 2 (some (index-of s 15)))))
|
||||||
|
|
||||||
(defn main [] ()
|
(defn main [] ()
|
||||||
(match (doubled-first (slice nums 0 4))
|
(match (doubled-first (slice nums 0 4))
|
||||||
(Some i) (do (print i) (println "")) ; 4
|
(Some i) (do (print i) (println "")) ; 4
|
||||||
None (println "not found"))
|
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 ""))
|
(Some i) (do (print i) (println ""))
|
||||||
None (println "not found")))</code></pre>
|
None (println "not found")))</code></pre>
|
||||||
|
|
||||||
@ -697,10 +741,19 @@ first
|
|||||||
second
|
second
|
||||||
3</code></pre>
|
3</code></pre>
|
||||||
|
|
||||||
<p><code>defer</code> is function-scoped and is <strong>rejected</strong> inside a
|
<p><code>defer</code> is function-scoped: it is copied into every exit path of the
|
||||||
<code>let</code>, a loop or a branch. Block scoping it is not done:</p>
|
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>
|
<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
|
<p>A reversed range — <code>lo</code> greater than <code>hi</code> — traps, rather than
|
||||||
yielding a huge unsigned length.</p>
|
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>
|
<h2 id="printing">Printing</h2>
|
||||||
|
|
||||||
<p><code>println</code> prints a value and a newline; <code>print</code> is the same
|
<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>,
|
<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>,
|
<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
|
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
|
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
|
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>
|
<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:
|
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>print</code> and <code>println</code> are the compiler's, and
|
||||||
<code>write-stdout</code> — the one output primitive — is what they are written
|
<code>write-stdout</code> — the one output primitive — is what they are written
|
||||||
@ -809,16 +991,27 @@ over.</p>
|
|||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>Group</th><th>Names</th></tr>
|
<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>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>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>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>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>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>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>
|
</table>
|
||||||
</div>
|
</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 —
|
<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
|
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>:
|
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
|
<code>"abc"</code> and 12 for <code>"12x"</code>, which are three wrong answers a caller
|
||||||
cannot tell from a real 12.</p>
|
cannot tell from a real 12.</p>
|
||||||
|
|
||||||
<p><code>sqrt-f32</code> is the one function in the file that is not Flan:
|
<p>Five functions in the file are not Flan, and they are libm's:
|
||||||
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code>. Every other number
|
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code> and the same line for
|
||||||
here is reachable from the four operations and a cast; a square root is not, and the
|
<code>sinf</code>, <code>cosf</code>, <code>atan2f</code> and <code>powf</code>. Every
|
||||||
usual trick of seeding Newton's method from the exponent bits needs a bit-cast between
|
other number here is reachable from the four operations and a cast; a square root is
|
||||||
<code>f32</code> and <code>u32</code> that the language does not have. IEEE-754 makes
|
not, and the usual trick of seeding Newton's method from the exponent bits needs a
|
||||||
<code>sqrt</code> correctly rounded, so libm gives the same bit pattern on both
|
bit-cast between <code>f32</code> and <code>u32</code> that the language does not have.
|
||||||
targets anyway. Every link carries <code>-lm</code>.</p>
|
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
|
||||||
<p>There is no <code>println</code>. There is no overloading yet, so each printer names
|
nothing of <code>sinf</code>, <code>cosf</code>, <code>atan2f</code> or
|
||||||
its type. <code>(println 1)</code> is <code>unknown function println</code>.</p>
|
<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
|
<p>The primitives underneath are few — a primitive is the only thing implemented
|
||||||
twice per backend: <code>argv</code>,
|
twice per backend: <code>argv</code>,
|
||||||
@ -930,9 +1125,9 @@ carries on.</p>
|
|||||||
(handler-bind [(Type [c] body ...) ...] body ...) ; match by type, no hierarchy
|
(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
|
(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
|
<p><code>signal</code> has type <code>()</code>, always. A handler that returns
|
||||||
normally leaves the signaller to carry on — the accumulation case:</p>
|
normally leaves the signaller to carry on — the accumulation case:</p>
|
||||||
@ -991,6 +1186,20 @@ first, before the clause body starts.</p>
|
|||||||
-1
|
-1
|
||||||
2</code></pre>
|
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
|
<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
|
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
|
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
|
<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
|
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 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
|
<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
|
<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,
|
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>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
|
<code>defer</code>. In each case a bare exit would leave frames on the stack pointing
|
||||||
into a function that has gone.</li>
|
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>
|
</ul>
|
||||||
|
|
||||||
<h2 id="break">The break loop</h2>
|
<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();
|
*out = GetMousePosition();
|
||||||
}</code></pre>
|
}</code></pre>
|
||||||
|
|
||||||
<p>No library header is read during a build, deliberately, so a build needs the shared
|
<p>A build needs the shared library to be linkable and <em>not</em> the
|
||||||
library to be linkable and not the <code>-devel</code> package to be installed.
|
<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
|
<strong>Guaranteed:</strong> the C typedef
|
||||||
and the Flan struct come from the same <code>defstruct</code>, so they cannot disagree,
|
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.
|
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
|
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>
|
<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
|
<pre><code class="sh">$ flan generate-c vendor/raylib
|
||||||
$ flan generate-c vendor/raylib
|
wrote vendor/raylib/generated.flan: 269 declarations, 117 refused, of 581 functions
|
||||||
wrote vendor/raylib/generated.flan: 253 declarations, 156 refused, of 581 functions.
|
in vendor/raylib/raylib-5.5.h.
|
||||||
Every defstruct and every hand-written declare-c agrees with it.</code></pre>
|
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
|
<p>Committing the output is what makes the declarations a dependency of the repository
|
||||||
no-header property honest: the declarations are in the repository, so every build gets
|
rather than of the machine: every build gets all of them, they are greppable, and they
|
||||||
all of them, they are greppable, and they show up in a diff when the library moves. The
|
show up in a diff when the library moves. The argument is not caching — the clang dump
|
||||||
argument is not caching — the clang dump is already cached on disk and in memory.</p>
|
is already cached on disk and in memory.</p>
|
||||||
|
|
||||||
<p><strong>Regeneration is the check.</strong> The cost of committing the output is that
|
<p><strong>The header is committed too</strong>, at
|
||||||
nothing compares the bindings against reality on every build any more, so the one
|
<code>vendor/raylib/raylib-5.5.h</code>, and <code>headers</code> names it by path with
|
||||||
function that writes the file compares first and <em>refuses to write</em> when the
|
no environment variable in front of it. That line used to be
|
||||||
package and the header disagree: every <code>defstruct</code> against the header's
|
<code>?${FLAN_RAYLIB_H}</code> — optional, on the argument that requiring a header would
|
||||||
record, and every hand-written <code>declare-c</code> against the header's signature.
|
cost everyone the no-<code>-devel</code> property in order to give the check to whoever
|
||||||
Pointed at a raylib 5.1-dev header while the package is written for 5.5, it reports ten
|
had one. Committing the header dissolved that argument, because nobody needs
|
||||||
real differences and writes nothing — which is exactly the silent version skew a
|
<code>raylib-devel</code> to have a file that ships with the repository. So the check now
|
||||||
generated file would otherwise bake in and make look reviewed.</p>
|
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
|
<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
|
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
|
<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.
|
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.
|
The walk is bounded at depth 4 and 8 elements, and the output truncates at 4K. An owning
|
||||||
<code>Map</code>, function values and type variables refuse by name.</p>
|
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
|
<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
|
returned. Sixteen expression evaluations retain zero mappings, where each redefinition
|
||||||
@ -1430,8 +1669,10 @@ something surprising.</p>
|
|||||||
<table>
|
<table>
|
||||||
<tr><th>Key</th><th>Does</th></tr>
|
<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-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-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-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-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-o</kbd></td><td>the running program's own output, in <code>*flan-output*</code></td></tr>
|
||||||
<tr><td><kbd>C-c C-r</kbd></td><td>a prompt on the running program (<code>*flan-repl*</code>)</td></tr>
|
<tr><td><kbd>C-c C-r</kbd></td><td>a prompt on the running program (<code>*flan-repl*</code>)</td></tr>
|
||||||
@ -1447,6 +1688,15 @@ something surprising.</p>
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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
|
<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
|
<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>
|
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
|
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>
|
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
|
<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;
|
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>--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">
|
<div class="scroll">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>You write</th><th>The compiler says</th></tr>
|
<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>(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><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>'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><code>(Fn [T] R)</code></td><td>a function type is not implemented yet — milestone 5 (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>(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><code>errdefer</code></td><td>errdefer is not implemented yet (see the build sequence in 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>await</code></td><td>await is not implemented yet (see the build sequence in 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>handler-case</code></td><td>handler-case is not implemented yet (see the build sequence in 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>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet (see the build sequence in plan.org)</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>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><code>handler-case</code></td><td>handler-case is not implemented yet</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>
|
||||||
<tr><td><code>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Beyond that list, and just as true: there is no allocator and no
|
<p>Two of those rows want reading carefully. <strong>A quoted symbol works where a
|
||||||
<code>context</code>; there is no <code>println</code> and no overloading; restarts take
|
restart is named</strong> — <code>(invoke-restart 'use-value 21)</code> is the ordinary
|
||||||
no parameters; <code>match</code> works on an <code>Option</code> and nothing else;
|
spelling — and is refused only as a value in its own right, because there is no symbol
|
||||||
<code>defer</code> is function-scoped; there is no package-private marker other than
|
type to give it. And <strong>the type-variable row is about the sigil</strong>: bare
|
||||||
<code>main</code> not being exported; there are no threads in the language; and the
|
<code>a</code> in type position is not a type variable and never became one;
|
||||||
managed <code>class</code> facility that plan.org describes is a plan and not a feature.</p>
|
<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:
|
<p>Beyond that list, and just as true: there is no overloading; <code>defer</code> is
|
||||||
<code>clone</code>, <code>as-slice</code>, <code>push</code>, <code>get</code>,
|
refused inside a loop or a branch (a <code>let</code> is fine — it has the function's
|
||||||
<code>put</code>, <code>resolve</code> and allocator-aware operations belong to
|
extent); <code>find-restart</code> and <code>compute-restarts</code> are blocked on a
|
||||||
<code>Vec</code> and <code>Map</code>, and arrive with them.</p>
|
<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>
|
<p>One of these is 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 there is not going to be one: compiling is the only way a form is ever run. The
|
||||||
and compiled redefinition at ~19ms is perceptually instant for expression evaluation too.
|
instrumentation-based step debugger that wanted one is cut, and compiled redefinition at
|
||||||
And the <strong>macro expander</strong> is blocked on unions rather than on itself — a
|
~19ms is perceptually instant for expression evaluation too. That is also how macros run
|
||||||
macro is a function from <code>Form</code> to <code>Form</code>, which needs
|
— a <code>defmacro</code> is compiled into a shared object and dlopened into the
|
||||||
<code>Form</code> to exist as a Flan union value first.</p>
|
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>
|
<h2 id="reading">Further reading</h2>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user