From 3e181b52b280246458a3cffa793ebaef9b6e1d49 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
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
-clang.
clang. There is a second one, off by default, that emits x86-64 by hand —
+see targets and builds.
What it is not:
@@ -326,7 +328,9 @@ $ ./_build/default/bin/main.exe run calc-me.flan "1 + 2 * (3 - 0.5) / 2"$ 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]
@@ -444,9 +448,17 @@ notation reads as exactly one data item.
string[T][n T](Vec T)(Map K V)(Pool T)(Handle T)i64(Ptr T)(Option T)Some / None(Fn [T ...] R)Allocator$tdefunion, matched by casei32()Neverif, when, unless, cond,
do, and, or, not,
-while, until, dotimes, return,
+while, until, dotimes, loop/recur,
+break, continue, return,
match. and and or short-circuit.
:else is cond's catch-all.
Loops are imperative, with while, until and
-return. There is no loop/recur. There is no
-break or continue yet either; both refuse by name:
break and continue leave or restart the innermost loop, and
+take a label when that is not the one meant. loop and
+recur are the functional shape beside them: loop is an
+expression, its value is the value of its body, and recur rebinds
+every name at once and jumps rather than calls — ten million iterations do not grow the
+stack.
break is not implemented yet (see the build sequence in plan.org)
+(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)))))
-An early exit out of a loop is return, as first-even does
-above.
+(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))))
+
+12
+10
+8
+0
+1
+
+return is still the way out of a function from inside a loop,
+as first-even does above.
match and some(Option T) is how absence is spelled: a lookup miss, an empty
collection, the end of a stream. match works on an Option and
-on nothing else today. some unwraps Some and early-returns
-None from the enclosing function.
defunion, and on nothing else. some unwraps
+Some and early-returns None from the enclosing function.
(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")))
@@ -697,10 +741,19 @@ first
second
3
-defer is function-scoped and is rejected inside a
-let, a loop or a branch. Block scoping it is not done:
defer 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
+rejected inside a loop or a branch, where "always registers" would be a
+lie. A let is fine, and that is not an exception — a let is not
+a frame here, its bindings are function slots like any other and nothing is released at
+scope exit, so a defer 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.
defer must be a top-level form in a function body — block-scoped defer is not implemented yet (milestone 4)
+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)
(set (at grid r c) v) and (addr (at grid r c))
A reversed range — lo greater than hi — traps, rather than
yielding a huge unsigned length.
+Generics
+
+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.
+
+A type variable is written $t where a type goes — a
+parameter's type, the return type, or nested inside a type constructor such as
+[$t] or (Vec $t). The sigil is not a binding-site-only
+spelling: every type position writes it. Bare t is the same
+variable in expression position, where a builtin takes the name of a
+type as an argument — (vec-new t), (map-new t i32),
+(pool-new t), and the cast (t x).
+
+(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)))
+
+3
+text
+3
+10
+5
+4.5
+
+The body is checked once, abstractly
+
+A generic body is checked with nothing substituted, 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
+not Odin's rule, which checks a polymorphic body per instantiation:
+
++ 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
+
+What makes that liveable is a where clause, written as a Clojure-style
+map at the head of the body — {:where (ordered? $t)}, or a vector when
+there is more than one: {:where [(copyable? $t) (copyable? $u)]}. There
+are five predicates, and each gates builtins the compiler already has:
+
+
+
+Predicate What it admits
+numeric?+ - * / %, and a cast (t x)
+ordered?< <= > >= min max
+equal?= and !=
+hashable?the variable as a Map key — (map-new t V), get, put, has-key?
+copyable?reading the value more than once; Pool and Vec element positions
+
+
+
+They entail each other in one direction, so one clause usually does:
+numeric? gives ordered?, ordered? gives
+equal?, and any of the four gives copyable?. A
+sort! that compares its elements and reads them twice declares
+ordered? and nothing else.
+
+A type variable is move-only by default, and
+copyable? is the opt-out. Types.is_move_only of a variable is
+not decidable abstractly — the same variable is i32 at one instantiation
+and (Vec i32) 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 T: Copy, with the difference that the compiler
+answers the question rather than a user implementing a trait. So
+(defn twice [x $t] $t (+ x x)) does not merely want
+numeric?; reading x a second time is a use after move:
+
+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
+
+Each instantiation then checks the concrete type against what the signature declared,
+and refuses the call site when it does not answer:
+
+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
+
+Two forms are deferred to the instantiation rather than settled abstractly, because
+their legality is only decidable after substituting: println over a
+variable, which selects the structural printer per copy, and the Map
+operations over a variable key, whose hash and equality are concrete symbols chosen from
+the concrete key type. The Map half is what hashable? buys —
+without the clause, 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 gates a builtin
+that already exists. The ceiling is that nobody can supply a < of their
+own; every operation the prelude and the containers need is a primitive, so it does not
+bind. test/programs/generics.flan exercises the whole of it.
+
Printing
println prints a value and a newline; print is the same
@@ -775,7 +953,11 @@ no newline: true
The walk covers every integer and float type, bool, (),
string, [u8], enums, Ptr, Option,
-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 — <vec>,
+<pool>, <allocator> — while a Handle
+shows its index and generation, and a Map has no printer at all.
+An enum member comes back as its name: the value is
an i32 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 Ptr prints as
@@ -800,7 +982,7 @@ takes the value as it is and prints the number it holds.
The prelude is written in Flan, all but one line of it, and prepended to every +
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:
print and println are the compiler's, and
write-stdout — the one output primitive — is what they are written
@@ -809,16 +991,27 @@ over.
| Group | Names |
|---|---|
slices of i32 | swap-i32!, reverse-i32!, sort-i32!, index-of-i32, min-i32, max-i32, sum-i32 |
| bytes | bytes=?, starts-with?, ends-with?, index-of-byte, index-of-bytes, trim, digit?, space? |
| slice algorithms, over one type variable | swap!, reverse!, sort!, sort-by!, index-of, min-of, max-of, map!, reduce, filter |
| the per-type layer that stays | sum-i32, sum-f32 — the element and the accumulator are different types, which one variable cannot say |
| bytes | bytes=?, bytes<?, bytes-ci=?, starts-with?, ends-with?, index-of-bytes, trim, digit?, space?, sort-bytes! |
| parsing | parse-i64, parse-f64 |
| text | split-on-byte, split-next!, lower-ascii, upper-ascii, bytes-ci=? |
| text | split-on-byte, split-next!, split, lower-ascii, upper-ascii, to-lower, to-upper |
| building bytes | append!, append-i64!, append-f64!, concat, join, repeat-bytes, replace-bytes, slices-new, format-f64 |
| UTF-8 | decode-rune, rune-at, rune-count, rune-size, rune-start?, valid-utf8?, encode-rune! |
| numbers | sign-f32, lerp, floor-f32, ceil-f32, round-f32, and sqrt-f32, which is the one declare in the file |
| numbers | sign-f32, lerp, clamp, floor-f32, ceil-f32, round-f32, and the five declares: sqrt-f32, sin-f32, cos-f32, atan2-f32, pow-f32 |
| random | rand-seed, rand-u32, rand-f32, rand-i32-range, rand-f32-range |
| forms, for macros | form-nil, form-cons, form-append, form-rest, form-items, form-pair, form-sym?, form-is-sym?, gensym, and unless and into, which are macros written here rather than special forms |
| the rest | pause, which signals the Pause condition the break loop stops on, and embed-find |
One family, not one per type. The slice algorithms used to be
+sort-i32! beside sort-f32! beside sort-bytes!, and
+generics collapsed them: sort! is written once and instantiated at whatever
+element type the call passes. sum-i32 and sum-f32 are what did
+not collapse, and they are the honest exception — each widens its element into a
+different accumulator, which one variable cannot express.
The RNG is ours, not libc's — 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. The parsers are ours too:
@@ -826,16 +1019,18 @@ native and on wasm32. The parsers are ours too:
"abc" and 12 for "12x", which are three wrong answers a caller
cannot tell from a real 12.
sqrt-f32 is the one function in the file that is not Flan:
-(declare sqrt-f32 [x f32] f32 "sqrtf"). 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
-f32 and u32 that the language does not have. IEEE-754 makes
-sqrt correctly rounded, so libm gives the same bit pattern on both
-targets anyway. Every link carries -lm.
There is no println. There is no overloading yet, so each printer names
-its type. (println 1) is unknown function println.
Five functions in the file are not Flan, and they are libm's:
+(declare sqrt-f32 [x f32] f32 "sqrtf") and the same line for
+sinf, cosf, atan2f and powf. 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 f32 and u32 that the language does not have.
+IEEE-754 makes sqrt correctly rounded, so libm gives the same bit pattern
+on both targets anyway. The other four are not: IEEE-754 requires
+nothing of sinf, cosf, atan2f or
+powf, 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 -lm.
The primitives underneath are few — a primitive is the only thing implemented
twice per backend: argv,
@@ -930,9 +1125,9 @@ carries on.
signal has type (), always. A handler that returns
normally leaves the signaller to carry on — the accumulation case:
A clause may take parameters, which is how the answer comes from outside the +frame that offers the restart:
+ +(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
+
+42
+
Restart lookup walks the dynamic restart stack from innermost outward and takes the
first frame offering the name, so an inner restart-case shadows an outer
one for the duration of its body. An inner parser's skip-form is found
@@ -1032,7 +1241,11 @@ a guard after each call.
So the author chooses where the retry boundary is. A restart-case 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.
test/programs/frame-rollback.flan is the worked example of the snapshot, and
+the ordering in it is the part worth reading: restore in the restart clause and
+not in a defer, because a defer runs on the ordinary return path too and
+that version silently rolls back the frames that succeeded.
This matters more here than in most Lisps because the intended use is a
game loop, where the plan is to skip a frame and carry on rather than die. Now
that a bad index signals BoundsError instead of ending the process,
@@ -1047,7 +1260,14 @@ a guard after each call.
restart-case body, and so is invoke-restart inside a
defer. In each case a bare exit would leave frames on the stack pointing
into a function that has gone.
- (use-value [s string] …) shadows an outer
+ (use-value [v i32] …) and (invoke-restart 'use-value 21)
+ 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.No library header is read during a build, deliberately, so a build needs the shared
-library to be linkable and not the -devel package to be installed.
+
A build needs the shared library to be linkable and not the
+-devel 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.
Guaranteed: the C typedef
and the Flan struct come from the same defstruct, so they cannot disagree,
and clang type-checks the wrapper against the generated prototype.
@@ -1172,24 +1393,38 @@ directory may carry a headers file naming the library's own C heade
the same declare-c line a person would have written, and writes them to
generated.flan in the package — which is committed.
$ 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.
+$ 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.
-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.
+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.
-Regeneration is the check. 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 refuses to write when the
-package and the header disagree: every defstruct against the header's
-record, and every hand-written declare-c 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.
The header is committed too, at
+vendor/raylib/raylib-5.5.h, and headers names it by path with
+no environment variable in front of it. That line used to be
+?${FLAN_RAYLIB_H} — optional, on the argument that requiring a header would
+cost everyone the no--devel property in order to give the check to whoever
+had one. Committing the header dissolved that argument, because nobody needs
+raylib-devel to have a file that ships with the repository. So the check now
+runs on every 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.
Regeneration is the stronger check. 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 writes the file therefore compares first and refuses to
+write when the package and the header disagree: every defstruct
+against the header's record, and every hand-written declare-c 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.
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 ... ] ...]
A pointer is never followed; it renders as <ptr>. 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.
-Map, function values and type variables refuse by name.
<vec>,
+<pool>, <allocator>, <handle 3:1>
+— 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
+Map is what still refuses by name.
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.
| Key | Does |
|---|---|
| C-c C-c | the top-level form at point, recompiled and installed |
| C-u C-c C-c | …and mark it, so the program stops at the form point is inside (C-u C-u: on entry) |
| C-c C-k | the whole buffer, as one module |
| C-x C-e | the expression before point, evaluated in the running program |
| C-u C-x C-e | …and stop at it instead of printing its value |
| C-c C-z / C-c C-q | connect (finds .flan-dev.sock upward) / disconnect |
| C-c C-o | the running program's own output, in *flan-output* |
| C-c C-r | a prompt on the running program (*flan-repl*) |
Some commands have no key. M-x flan-dev starts a program and
+M-x flan-dev-quit stops it. M-x flan-watch opens the watch
+buffer, which shows values while the program runs rather than while it is
+stopped, and M-x flan-watch-ghost-mode shows the same values inline at the
+call that wrote each one. M-x flan-inspect-address roots an inspection at a
+raw address rather than at an expression. M-x flan-allocations and
+M-x flan-leaks read the allocation registry: every block it recorded grouped
+by type, and the same walk with the dead left out.
C-c C-k sends one module rather than a form at a time on purpose: a
defvar and the function that uses it have to arrive in the same load, or the
first refers to storage that does not exist yet.
There is a second backend, and it is off by default.
+flan build --x86 lowers the checked program to x86-64 by hand —
+lib/x86.ml, writing an assembly file directly — instead of going through
+LLVM. It is the dev 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.
It covers a subset of the IR and refuses the rest by name, 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:
+ +$ 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")
+
--debug is a third flag beside --dev and the optimisation
level. --dev asks whether you can redefine the program while it runs;
--debug 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.
| You write | The compiler says |
|---|---|
(Vec T) | (Vec T) is not implemented yet — milestone 6 (see plan.org) |
(Map K V) | (Map K V) is not implemented yet — milestone 6 (see plan.org) |
(Result T E) | (Result T E) is not implemented yet — milestone 6 (see plan.org) |
(Handle T) | (Handle T) is not implemented yet — milestone 6 (see plan.org) |
(try …) | try (Result) is not implemented yet — milestone 6 (see plan.org) |
| a union type | the union type Shape is not implemented yet — milestone 6 (see plan.org) |
(Fn [T] R) | a function type is not implemented yet — milestone 5 (see plan.org) |
(fn [x i32] …) | calling something other than a named function is not implemented yet — milestone 5 (see plan.org) |
| a type variable | generic code over the type variable a is not implemented yet — milestone 5 (see plan.org) |
'sym | a quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org) |
(defmacro …) | parses, but is not expanded: running a macro means compiling it and loading it into the compiler, which is not wired up yet |
`(a ~b) | is read, but not expanded: macro expansion is not wired up yet |
handler-case | handler-case is not implemented yet |
find-restart, compute-restarts | … is not implemented yet |
'sym as a value | a quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org) |
| a bare lowercase type name | generic code over the type variable a is not implemented yet — milestone 5 (see plan.org) |
errdefer | errdefer is not implemented yet (see the build sequence in plan.org) |
await | await is not implemented yet (see the build sequence in plan.org) |
handler-case | handler-case is not implemented yet (see the build sequence in plan.org) |
find-restart, compute-restarts | … is not implemented yet (see the build sequence in plan.org) |
a union as a declare parameter | a parameter of g is U, which cannot cross to C directly — pass (Ptr U) and let the shim read it |
| a user-written allocator | a user-written allocator is not implemented yet, and a defn's name in that position … |
Beyond that list, and just as true: there is no allocator and no
-context; there is no println and no overloading; restarts take
-no parameters; match works on an Option and nothing else;
-defer is function-scoped; there is no package-private marker other than
-main not being exported; there are no threads in the language; and the
-managed class facility that plan.org describes is a plan and not a feature.
Two of those rows want reading carefully. A quoted symbol works where a
+restart is named — (invoke-restart 'use-value 21) 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 the type-variable row is about the sigil: bare
+a in type position is not a type variable and never became one;
+$t is, and generics is where it is written down.
The vocabulary in spec-memory.md is normative but largely unbuilt:
-clone, as-slice, push, get,
-put, resolve and allocator-aware operations belong to
-Vec and Map, and arrive with them.
Beyond that list, and just as true: there is no overloading; defer is
+refused inside a loop or a branch (a let is fine — it has the function's
+extent); find-restart and compute-restarts are blocked on a
+Restart 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 main not being exported;
+there are no threads in the language; and the managed class facility that
+plan.org describes is a plan and not a feature.
Two of these are settled rather than pending. There is no interpreter
-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 macro expander is blocked on unions rather than on itself — a
-macro is a function from Form to Form, which needs
-Form to exist as a Flan union value first.
One of these is settled rather than pending. There is no interpreter
+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 defmacro 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.