2035 lines
107 KiB
Org Mode
2035 lines
107 KiB
Org Mode
#+TITLE: Flan
|
|
#+TODO: TODO WAIT | DONE CANCELLED
|
|
#+ARCHIVE: ::* Archive
|
|
|
|
Every decision, open question and known gap, one =**= heading each under the
|
|
subsystem it belongs to. A DONE entry says what was decided and what that rules
|
|
out, in a few lines — never the argument and never the measurements. Where the
|
|
reasoning will not fit, it is in =docs/BUILT.md= and the entry is one line
|
|
pointing at it. A CANCELLED entry carries the one-line reason, because an idea
|
|
rejected without a record is an idea that gets re-proposed.
|
|
|
|
* Language surface
|
|
|
|
** DONE Dynamic-first, and the dyn half of the language
|
|
CLOSED: [2026-09-20]
|
|
An unannotated parameter or return is =dyn=: a NaN-boxed value over a mark-sweep
|
|
heap, with =--no-gc= refusing residual dyn by location. The typed language is
|
|
unchanged underneath it.
|
|
|
|
** DONE The dynamic paths mimic Clojure, the static paths mimic Odin
|
|
CLOSED: [2026-09-19]
|
|
A tiebreaker for features, not a licence for meanings. A question the dyn side
|
|
has that Clojure has answered takes Clojure's answer; on the static side Odin's;
|
|
for conditions, Common Lisp. Rules out using it to settle anything about what an
|
|
operation computes.
|
|
|
|
** DONE Arithmetic semantics do not fork across the two spaces
|
|
CLOSED: [2026-09-20]
|
|
One operator, one meaning, both sides. =/= truncates toward zero and =%= follows
|
|
the dividend everywhere. Rules out Clojure's flooring =mod= as a dyn-side-only
|
|
behaviour of =%=; a flooring mod would be a second, separately named operation
|
|
available to both.
|
|
|
|
** DONE The return slot stays mandatory
|
|
CLOSED: [2026-09-19]
|
|
A =defn= writes its return type, and unit is =()=. The parse ambiguity a missing
|
|
slot would open is real, and =()= does not collapse into =dyn=. Rules out the
|
|
optional return slot.
|
|
|
|
** DONE def, defonce and defconst are the three forms
|
|
CLOSED: [2026-09-20]
|
|
=def= is Common Lisp's =defparameter= and re-initialises on every run; =defonce=
|
|
is CL's =defvar= under Clojure's name and keeps its value; =defconst= is folded.
|
|
A =def='s initialiser is always lifted, zero and literal included, so editing the
|
|
form and pressing =C-c C-c= reaches the same storage. Rules out the
|
|
value-dependent =defvar= that was sketched — "re-initialise only if it would come
|
|
out different" was never needed: a dev build emits a =defconst= as a mutable
|
|
global, so a folded constant is tunable live without any dependency tracking.
|
|
|
|
** DONE The third element of a defvar decides
|
|
CLOSED: [2026-09-20]
|
|
A type there is the zeroed static global; a value there is a dyn global
|
|
initialised from it. The same dispatch the parameter vector already makes. A
|
|
symbol that is neither gets a refusal naming both readings.
|
|
|
|
** DONE A macro has a parameter list, and [args] is the first argument
|
|
CLOSED: [2026-09-20]
|
|
A =defmacro= takes real parameters with destructuring and =&= variadics, checked
|
|
at the call before expansion; the whole argument list is spelled =[& args]=.
|
|
Rules out a legacy mode where one bare parameter keeps the old meaning, which
|
|
would make =[a]= and =[a b]= mean unrelated things.
|
|
|
|
** CANCELLED Map destructuring in a macro's parameter list
|
|
CLOSED: [2026-09-20]
|
|
A macro's argument is a =Form=, whose map case is a flat run of alternating forms
|
|
with no field names, so the pattern cannot be translated without being given a new
|
|
meaning and none of the candidates is obviously the one anyone wants. Vectors and
|
|
=&= are the 95% case and are built.
|
|
|
|
** DONE A macro fails at its call site in its own words
|
|
=compile-error= is a builtin reachable from a macro body, so a macro reports what
|
|
is wrong where it was written rather than aborting the compile with no location.
|
|
The prelude's own =unless= has not been converted and still answers a bare
|
|
undefined name.
|
|
|
|
** TODO gensym's counter restarts in a second module
|
|
The counter lives in the loaded module and a module is dlopened once per compiler
|
|
process, so it is process-wide in practice — but the rounds already build more
|
|
than one module for a program whose macros call macros. Seed it from the module's
|
|
index.
|
|
|
|
** TODO A quasiquote inside a quasiquote is refused
|
|
Nothing counts nesting levels — not the reader, deliberately, and not the
|
|
desugaring. Only a macro that writes a macro wants one.
|
|
|
|
** TODO until, cond, when and dotimes are still special forms in parse.ml
|
|
=until= and =cond= are free to move to the prelude whenever somebody wants them.
|
|
=when= and =dotimes= are not: the prelude uses them 29 and 12 times, so moving
|
|
either makes the prelude depend on the macro the macro module has to compile the
|
|
prelude to get. =cond= also has a =(cond a)= refusal a macro cannot produce.
|
|
|
|
** DONE Macros are imported from a package
|
|
The old refusal claimed collecting a package's macros needed a second import
|
|
resolver at the =Form= level. It did not: the file being compiled is parsed before
|
|
=Load= runs too, so =Load.program= takes forms and uses the one resolver that
|
|
always existed.
|
|
|
|
** DONE A prelude function may call a prelude macro
|
|
It was a cycle, not an ordering — the arity error came from the =Check.program=
|
|
inside =Macro.compile=, where expansion is off. "A prelude macro may not call a
|
|
macro" still stands and names itself when violated.
|
|
|
|
** DONE The expander design: running a macro means dlopening it
|
|
There is no interpreter, so the compiler builds a shared object and dlopens it
|
|
into itself. A call inside a quasiquote is output, not a compile-order dependency,
|
|
and quasiquote is desugared before the walk, which is load-bearing.
|
|
|
|
** DONE (comment ...) is a prelude macro
|
|
CLOSED: [2026-09-20]
|
|
A macro's arguments are raw =Form= and are never checked as expressions, so what
|
|
is inside never has to be a program — it obeys the reader's rules only. =#_= is
|
|
the other spelling and they are not rivals: =#_= discards one form and works in
|
|
argument position.
|
|
|
|
** DONE inc, dec, ++ and --
|
|
CLOSED: [2026-09-20]
|
|
Four prelude macros, generic for free. A word for the pure pair and C's
|
|
punctuation for the mutating pair. =(++ PLACE)= expands to =(set PLACE (+ PLACE
|
|
1))=, so a place with a side effect in it is evaluated twice — not fixable without
|
|
a reference type the language does not have.
|
|
|
|
** DONE Type-limit constants
|
|
CLOSED: [2026-09-20]
|
|
=i8..i64= and =u8..u64= max and min, =f32=/=f64= max, min-positive and epsilon,
|
|
each carrying its type. Kebab and the type's own name — =i32-max=, not =INT_MAX=.
|
|
There is no =f32-min=, because what a caller wants is the smallest positive value
|
|
and the name has to say which.
|
|
|
|
** DONE The two byte fills: (filled BYTE) and (dead-beef)
|
|
CLOSED: [2026-09-20]
|
|
Two builtins rather than one — "why not both?" — spelled the way =(zeroed)= is,
|
|
taking the type expected of them, so a place is filled with =set= and there is
|
|
no place-taking form to learn beside it. A pattern's ascending bytes are its
|
|
big-endian bytes, which is how the hex literal reads. Two and not one with a
|
|
wider operand because the intrinsic takes a single repeated byte: the byte fill
|
|
is one instruction and the four-byte pattern is a loop on both backends.
|
|
|
|
** TODO There is no literal for an infinity or a NaN
|
|
=lib/reader.ml= has no literal for either, and =float_repr= prints =inf= and =nan=
|
|
as words the reader will not read back. =(/ 1.0 0.0)= is the only route to an
|
|
infinity, and the constant folder is integers only, so it cannot be a =defconst=.
|
|
Closing it needs a reader literal or a float-capable folding pass.
|
|
|
|
** TODO A u64 constant above 2^63 cannot be written in decimal
|
|
The reader reads a decimal integer literal as a signed 64-bit number; hex is read
|
|
as a bit pattern and works. The same limit has a second face: a cast's argument is
|
|
checked against the default type, so =(u64 2935910691)= is refused for not fitting
|
|
in an =i32=.
|
|
|
|
** DONE {.row .col} binds same-named locals
|
|
CLOSED: [2026-09-20]
|
|
A new arm at the top of =dmap= in =lib/parse.ml=, before the pair arm. Works in
|
|
=let= and nowhere else. The note claiming there was no grammar collision was wrong
|
|
— a bare dotted symbol in head position already parsed as something else and
|
|
failed later at the use.
|
|
|
|
** DONE An empty body where a body is optional
|
|
CLOSED: [2026-09-20]
|
|
=(when test)=, =(fn [])= and a =defn= returning =()= with no body all parse.
|
|
Relaxing =fn= opened a hole the checker had to close — an empty =fn= body at a
|
|
non-unit want is refused, because without that the call read a return value
|
|
nothing wrote.
|
|
|
|
** CANCELLED () as a unit value in expression position
|
|
CLOSED: [2026-09-20]
|
|
=()= stays the type-position spelling of =Unit= and has no value-position meaning.
|
|
Empty forms doing the right thing covers the need that made it look attractive,
|
|
and =()= stays unspoken-for in case the language grows lists.
|
|
|
|
** CANCELLED // for forced truncating division
|
|
=/= on two integers already truncates toward zero, and casting a float division
|
|
truncates too, so =(i32 (/ a b))= is the spelling. Python's =//= is floor
|
|
division, which is a different operation from the one that was wanted.
|
|
|
|
** DONE builtin/ is a reserved qualifier
|
|
CLOSED: [2026-09-20]
|
|
=builtin/length= is the builtin whatever else the file has decided =length= means,
|
|
and it is legal whether or not anything is shadowed. It wears the package
|
|
qualifier's spelling deliberately, because =builtin= is reserved rather than
|
|
resolved — every other qualifier in a finished program comes from an import alias.
|
|
|
|
** DONE println is variadic
|
|
CLOSED: [2026-09-20]
|
|
Clojure's semantics — every argument in order, one space between each pair,
|
|
=println= ends the line, =(println)= is the newline alone. Typed and dyn arguments
|
|
mix in one call because each gets its own printer and both share stdio's buffer.
|
|
|
|
** DONE The six comparisons take two operands or more
|
|
CLOSED: [2026-09-21]
|
|
=<= =<== =>= =>== and === chain adjacently; =!== asks about every pair, Common
|
|
Lisp's =/=. Asking whether a sequence increases is a question about neighbours;
|
|
asking whether values are all different is a question about the set. =(< x)= stays
|
|
refused, and =%= and the shifts stay two operands.
|
|
|
|
** DONE dotimes counts from where you say
|
|
CLOSED: [2026-09-21]
|
|
Three arities, with =stop= exclusive in every one, so =(dotimes [i 0 n])= is
|
|
=(dotimes [i n])= — one rule, not two. A negative step counts down; a literal step
|
|
of zero is refused at compile time and a computed zero runs no times at all, which
|
|
is terminating and deterministic.
|
|
|
|
** DONE slice takes three arities, and at/slice reach a string
|
|
CLOSED: [2026-09-20]
|
|
=(slice a)= and =(slice a n)= are written out into the three-argument form. A
|
|
string slice answers a string rather than a =[u8]=, because a byte slice is
|
|
writable-looking and these bytes are not the program's to write.
|
|
|
|
** DONE One slice; as-slice is gone
|
|
CLOSED: [2026-09-21]
|
|
The input type already determines the semantics completely, so the second name
|
|
expressed no choice, and it warned at the moment nothing is wrong — the danger
|
|
arrives at the push. A =Vec= a call returned is accepted where an array a call
|
|
returned is refused: one dangles and one only leaks, and leaking is defined
|
|
behaviour here.
|
|
|
|
** TODO (clone slice) as the general spelling of what (bytes s) does
|
|
Not built because of the who-frees question =bytes= answers by leaning on
|
|
free-all and arena-destroy. =flan_bytes_dup= is already the lowering, so if slices
|
|
grow a =clone= the two should share it.
|
|
|
|
** DONE The count is length, and len is a name a program can have
|
|
CLOSED: [2026-09-21]
|
|
One arm in the checker and one row in the builtin table. A call to an undefined
|
|
=len= is refused after every table and after the shadowing guard, so a program
|
|
with its own =len= never sees the refusal.
|
|
|
|
** CANCELLED Counting abandonments
|
|
CLOSED: [2026-09-21]
|
|
An =abandoned= verb was drafted and dropped. The wait already ends when the
|
|
thunk breaks and the restart's own reply says what taking it did; a third
|
|
telling read by nobody is how a wire grows a verb whose answer drifts from what
|
|
happened.
|
|
|
|
** DONE The randomness surface
|
|
CLOSED: [2026-09-21]
|
|
=(rand-int)=, =(rand)=, =(rand-bool)=, =(rand-int-range lo hi)=,
|
|
=(rand-float-range lo hi)=. The generator is PCG-RXS-M-XS 64, so every call costs
|
|
one draw and a seeded run is reproducible — but the permutation is a bijection of
|
|
the state, so someone holding one result can predict the rest. Fine for a grid,
|
|
not for a key.
|
|
|
|
** DONE A value-producing array constructor
|
|
CLOSED: [2026-09-20]
|
|
=(array-fill [n ...] v)= and =(array-gen [n ...] f)= build the whole array as a
|
|
value, so one can be a =def='s initialiser. Ranks are spelled flat; there is no
|
|
nested bracket syntax. Row-major order is a promise, and the fill value and the
|
|
generator are each evaluated once, before any loop runs.
|
|
|
|
** DONE (array 4 T) is the zeroed array constructor
|
|
A =let= binding takes no type, so =(let [pts [4 rl/Vector2]] ...)= reads the
|
|
bracket as a two-element array literal. =(zeroed [4 rl/Vector2])= was proposed
|
|
first and rejected on how it reads — unambiguous to the parser, still looks like a
|
|
two-element vector to a person.
|
|
|
|
** DONE A defining form owns what happens on a re-run
|
|
CLOSED: [2026-09-20]
|
|
Each computed global's initialiser guards itself with its own flag, per global
|
|
rather than per startup function, and the daemon has no policy about globals at
|
|
all. Dev builds only; a release build's output is byte-identical.
|
|
|
|
** DONE defenum autoincrement
|
|
CLOSED: [2026-09-17]
|
|
C's rule: a member with no value is the previous plus one, and the first is zero.
|
|
An explicitly written duplicate is an intended alias and allowed; one produced by
|
|
autoincrement walking into another member's value is refused, naming both.
|
|
|
|
** DONE Every mapped raylib enum carries a Flan-side prefix
|
|
CLOSED: [2026-09-21]
|
|
An optional third column on the enum line in =vendor/raylib/bindings=, stripped
|
|
before the C prefix is applied. A reading choice and not a collision fix — a
|
|
keyword resolves against the expected type and against nothing else, so two enums
|
|
could always share a member spelling. What the prefix buys is the call site read
|
|
on its own.
|
|
|
|
** TODO match over enums
|
|
Fully desugarable and wanted, blocked only on =Ast.pattern= needing a keyword
|
|
case.
|
|
|
|
** DONE defdata is the tagged sum, defunion is C's untagged one
|
|
CLOSED: [2026-09-17]
|
|
The old tagged =defunion= is renamed =defdata=; =defunion= becomes the C-style
|
|
untagged one, serving the FFI and type punning, and is verified against the header
|
|
where one exists.
|
|
|
|
** DONE A defdata carries its tag, and case order is part of the contract
|
|
Tags are declaration order from zero, so a zeroed value is the first declared
|
|
case. =Option= is a two-case sum wearing a special coat, so the existing arms grew
|
|
a second subject rather than a second path. A non-exhaustive match is refused,
|
|
never defaulted.
|
|
|
|
** DONE An imported defdata keeps working across a package boundary
|
|
Both the tagged and the untagged form import now. It was never a blocker for
|
|
=Form=, because the prelude is parsed and prepended into the same flat namespace
|
|
before collection runs.
|
|
|
|
** TODO (Result T E) and try
|
|
Another sum type, queued after sum values landed, and still refused by name.
|
|
=docs/PORTING.md= records that it has no customer in the game's code.
|
|
|
|
** CANCELLED (Handle T) and the pool
|
|
CLOSED: [2026-09-18]
|
|
Built, then removed. Two containers are enough — a generational slab is a library
|
|
over a =Vec= — and everything copies as its header once move-only is gone. The
|
|
=(Ptr T)=-from-resolve hazard and the pool-as-enumeration argument for managed
|
|
classes go with it.
|
|
|
|
** CANCELLED drop, and unwind-protect with it
|
|
CLOSED: [2026-09-17]
|
|
=drop= runs code somewhere the reader is not looking, which is the C++ behaviour
|
|
the author does not want; it would not cover the motivating case, since =Image=
|
|
and =Texture2D= are raylib's types; and its one real advantage, cascading through
|
|
a container, was the case =Handle= made rare. =with-cleanup=/=unwind-protect= was
|
|
put and rejected as awkward with several resources. =defer= is the answer, and the
|
|
raylib begin/end pairs that seemed to motivate it are a macro problem.
|
|
|
|
** DONE defer may be written in a let
|
|
A =let= at the top level of a function body has exactly the function's extent, so
|
|
a =defer= in one always registers. A loop body and a branch are still refused by
|
|
name: =defer= is a compile-time construct with the cleanup copied into every exit
|
|
path, so "maybe registered" is not expressible.
|
|
|
|
** DONE edn reads into a struct and answers a dynamic value
|
|
CLOSED: [2026-09-17]
|
|
Two projects, not one, and the dynamic half goes through an allocator rather than
|
|
through a teardown operation. =(edn/read bytes)= answers a =dyn= — the separate
|
|
=Value= union it was first written against is gone. Rules out destructors and
|
|
finalizers: free-all takes the region.
|
|
|
|
** DONE A data file's struct is derived while the program is compiled
|
|
The typed half arrived as a provider macro, =(edn/defedn Name "path.edn")= and
|
|
=defjson= beside it, rather than as the =(read-edn T bytes)= spelling it was asked
|
|
for. A macro may read a file at the path the call site is written at, and there is
|
|
no run-time type information to do it any other way.
|
|
|
|
** DONE Assets are embedded at compile time
|
|
CLOSED: [2026-09-12]
|
|
Odin's answer. =(embed "p")= is a compiler feature, so it needs no build flags, no
|
|
linker arguments and no per-target packaging, and it works identically on desktop
|
|
and web. Rules out a linker-flag or per-target packaging answer, which a single
|
|
file could not have used anyway — =Load= gives link flags only to directory
|
|
packages.
|
|
|
|
** DONE slurp and barf are the file surface
|
|
CLOSED: [2026-09-12]
|
|
=(slurp path)= reads a whole file into a =(Vec u8)=; it had to wait for an
|
|
allocator, because the length is not known until the file is read.
|
|
|
|
** DONE Writing a file is desktop-only and signals on web
|
|
CLOSED: [2026-09-12]
|
|
Flan has no conditional compilation, so "isolate this to desktop" is not
|
|
expressible and a build-time refusal would be unusable. =barf= on web signals
|
|
=FileError= under a restart and the program decides. Rules out both the silent
|
|
no-op — which is how a save file disappears with nothing said — and the
|
|
build-time refusal.
|
|
|
|
** TODO Conditions get a parent link, not class inheritance
|
|
A condition type may name a parent where it is declared, and handler matching
|
|
walks that static chain. It buys the hierarchy conditions most lack — a catch-all
|
|
"any file error" handler — at compile-time cost only. Rules out the class answer:
|
|
a class condition allocates at the signal site, inverts the lifetime rule, and
|
|
lets a layout change under a standing handler frame. Not built.
|
|
|
|
** TODO Can a condition be a class?
|
|
Unanswered, and the shape that probably wins is both — a struct condition stays
|
|
what it is, a class condition allocates and matches by walking its class chain.
|
|
Three costs, one serious: signalling would allocate on the failure path.
|
|
|
|
** DONE handler-case
|
|
CLOSED: [2026-09-19]
|
|
=(handler-case BODY [(T [c] ...)])= — body first, clauses after. It is a
|
|
=handler-bind= whose clause invokes a restart the form established around itself,
|
|
so no backend work was needed. A clause runs at the form, which is why it sees the
|
|
establishing function's locals where a =handler-bind= clause cannot.
|
|
|
|
** DONE A restart that abandons the evaluation
|
|
CLOSED: [2026-09-21]
|
|
An expression that signalled used to offer an empty restart list, leaving =abort=,
|
|
which takes the compiler, the session and the game. Abandoning drops the
|
|
expression; it does not undo it, and every surface says so. At a trap there is no
|
|
transfer channel, so nothing can be abandoned, and that is correct.
|
|
|
|
** TODO A restart-case clause has no report string
|
|
The field is cheap and the accessor is cheap, but the only consumer is the break
|
|
loop's listing, so it would ship as a field nothing read. It belongs with the
|
|
listing work.
|
|
|
|
** WAIT find-restart and compute-restarts
|
|
Blocked on a type, not on effort: the spec gives them =(Option Restart)= and a
|
|
list, and there is no =Restart= type and no list type to return one in. The
|
|
minibuffer prompt never needed them — it reads the snapshot over the agent's
|
|
socket.
|
|
|
|
** DONE into fuses at compile time because it is a macro
|
|
Not transducers and not Rust's iterators, both of which compose at runtime and
|
|
need function values and allocation. A macro writes the call straight into the
|
|
loop body, so the function name is syntax and never a value. What it gives up is
|
|
building a transformation at runtime, which is close to useless in a game.
|
|
Reductions deliberately do not share the form.
|
|
|
|
** DONE loop and recur
|
|
Tail position is a permission that is withdrawn rather than a pre-pass. =loop= is
|
|
itself a barrier for =break= and =continue=, because a loop answers with the value
|
|
of its body so a jump out has no value to give — which is also why it takes no
|
|
label. Neither backend learned anything: a =loop= is a =let=, a =While= on true,
|
|
and two jumps. Mutual recursion still needs real tail calls and is out of scope.
|
|
|
|
** DONE break and continue, with loop labels
|
|
Labels are Odin's, in the head position, where a keyword is unambiguous because a
|
|
loop condition is never one. It is not a =goto= — control can only leave a loop it
|
|
is already inside.
|
|
|
|
** DONE The second tier of the standard library
|
|
The prelude had 44 allocation-free functions because there was nothing to allocate
|
|
from. =append=, =concat=, =join=, =split=, =format-f64=, =atan2=, =pow= and
|
|
=clamp= are the tier that returns new storage.
|
|
|
|
** DONE The Clojure patterns deliberately not copied
|
|
Four rules. The thing being operated on comes first, everywhere, so there is no
|
|
second threading macro. A membership test says which thing it tests. A =?= name
|
|
answers yes or no and a finder answers the thing or nothing. Composition reads
|
|
left to right. Rules out inheriting Clojure's =contains?=, =some= and =comp=
|
|
shapes.
|
|
|
|
** DONE A pointer from C needs a length before it can be indexed
|
|
CLOSED: [2026-09-13]
|
|
=(slice-from-ptr p n)=: the caller states the length and owns being right about
|
|
it. The alternative weighed — a per-binding declaration naming which argument
|
|
carries the count — cannot reach a count that is a sibling field. No marker on the
|
|
name; =ptr= is the marker, it owns nothing, and =free= refuses it.
|
|
|
|
** TODO A string cannot be returned from C
|
|
A string crosses as a parameter only — a C function that returns one returns
|
|
something Flan has no owner for. It is what makes =GetGamepadName= unbindable, and
|
|
the same rule refuses =TextFormat=, which is variadic and so has no honest
|
|
signature either.
|
|
|
|
** TODO Model, Mesh and FilePathList want a defstruct, and a callback wants the other direction
|
|
=Ray= and =BoundingBox= have their defstructs now. =FilePathList= is a =char**=
|
|
and blocks the drop-files example; =Model= and =Mesh= are ordinary widening.
|
|
Function-pointer parameters — =SetTraceLogCallback=, the audio stream processors —
|
|
are the callback direction of the FFI and nothing has needed it yet.
|
|
|
|
** DONE raymath is written in Flan, because static inline has no symbol
|
|
CLOSED: [2026-09-13]
|
|
=Clamp=, =Vector2Add= and the rest exist only in the header, so =declare-c= has
|
|
nothing to name. raymath's semantics exactly, including =normalize='s zero-length
|
|
guard. The C-shim alternative was rejected: it buys identical arithmetic for a
|
|
compilation unit in the build and a second place raylib's semantics are written
|
|
down.
|
|
|
|
** TODO rlgl's matrix stack is unbound
|
|
=core_2d_camera_mouse_zoom= is skipped for want of it — a different reason from
|
|
the raymath one.
|
|
|
|
** CANCELLED cstring as a type
|
|
Odin has no string-to-cstring conversion at all; it pays the same copy the shim
|
|
already makes. The one thing it buys is the return direction, and nothing in
|
|
=vendor/raylib= returns a string.
|
|
|
|
** CANCELLED rune as a type
|
|
Odin's is a 4-byte integer distinguished by a flag, so =i32= is the same thing.
|
|
Non-ASCII text was blocked on font loading, not on the string layer, and fonts are
|
|
bound.
|
|
|
|
** DONE Two function types: Fn captures, CFn cannot
|
|
CLOSED: [2026-09-21]
|
|
=(Fn [T ...] R)= is ={code, env}=, two words; =(CFn [T ...] R)= is the bare
|
|
address, one. Capture is by value into a stack environment, non-escaping only, and
|
|
an ordinary =defn= declares no environment and is byte-for-byte what it was. The
|
|
static side does not pay for the dynamic side. Rejected names: Closure, Proc, Fun,
|
|
Func, Fnptr.
|
|
|
|
** TODO Escaping closures, allocated on the GC side
|
|
The second half of "do both". What changes is where the environment points — a
|
|
frame slot today, a collector allocation then — and the escape check goes away
|
|
with it, along with the refusals on returning, storing, pointing at and pushing a
|
|
capturing value. Two things for it to know: a widening thunk's environment holds a
|
|
code pointer rather than a GC object, and capturing a dyn stays refused until a
|
|
synthesised environment has a descriptor.
|
|
|
|
** TODO CFn and C's calling convention
|
|
A Flan function's signature ends with the transfer channel and a C caller knows
|
|
nothing about one, so a =CFn= is not a C callback today. Under a future
|
|
=--no-conditions= flag a =CFn= signature could drop the channel and reach C's
|
|
exact convention, which is the direction the author is interested in.
|
|
|
|
** DONE defclass is a named dyn map with a shape tag
|
|
CLOSED: [2026-09-20]
|
|
An instance is a dyn map with its class in the object header, so =get=, =put=,
|
|
=has-key?= and =length= need no new operation. CLOS dispatch and Clojure's
|
|
arbitrary dispatch are one mechanism: a class dispatcher is the shape tag of the
|
|
first argument used as the dispatch function. Method bodies are inlined into one
|
|
dispatcher function, so a generic is one top-level name and one cell — adding a
|
|
method to a running program is an ordinary redefinition.
|
|
|
|
** CANCELLED Class features deferred, each with its reason
|
|
CLOSED: [2026-09-20]
|
|
Inheritance, multi-argument dispatch, =:before=/=:after=/=:around= and
|
|
=call-next-method=, named-slot construction, unknown-slot checking, computed
|
|
dispatch values. With single dispatch on literal values there is no specificity
|
|
question, and inheritance or multiple dispatch would create one. Unknown-slot
|
|
checking needs class-typed tracking the dyn side deliberately does not have.
|
|
|
|
** TODO update-instance-for-redefined-class, the user hook
|
|
Left out of v1 because name matching is the half that makes redefinition usable
|
|
and the hook is what makes it expressive. The obvious spelling is a generic riding
|
|
the dispatch that exists, and the migration already computes both the added and
|
|
the discarded lists. Rolling a failed migration back becomes a real question the
|
|
day this lands.
|
|
|
|
** DONE The module system stays directory-as-package
|
|
Several files in one directory are one module; a loose file is a module of one,
|
|
and no =package= line is required or accepted. Confirmed against Odin, which
|
|
requires the declaration despite the same rule, so the line buys only the ability
|
|
to disagree with the directory name. Acyclic imports are kept deliberately — a
|
|
definite package order is what the macro expander needs.
|
|
|
|
** DONE A name imported through a package keeps the inner alias
|
|
If =area/= imports =shape=, the type is =shape/Box= in the finished program and
|
|
never =area/shape/Box=. Forced rather than chosen: a directory reached along two
|
|
routes must arrive under one set of names or the checker sees every declaration
|
|
twice. The price is that the same directory under two aliases is refused, naming
|
|
both.
|
|
|
|
** TODO A package cannot mark a name private
|
|
=rl/get-color-raw= is callable from outside its package. The refusal machinery
|
|
takes a second rule in one line; the blocker is that there is no way for a package
|
|
to *say* a name is private, and adding one is a parser change the author has to
|
|
choose a spelling for. Every lane has skipped it for that reason.
|
|
|
|
** CANCELLED A struct version word, so a redefined layout keeps working
|
|
CLOSED: [2026-09-20]
|
|
Dropped rather than deferred. A shape still being discovered lives on the dyn side
|
|
as a =defclass=; a typed =defstruct= is a commitment to a layout, and changing a
|
|
commitment restarts the process. SBCL's push-through-and-invalidate is cheap only
|
|
because its instances carry headers. =docs/SBCL-REDEFINITION-NOTES.md= is the
|
|
reading behind it.
|
|
|
|
** WAIT An F#-ish indentation surface beside s-expressions
|
|
Deferred 2026-09-19 with no spike queued, blocked on evidence: the author will write
|
|
imperative Flan as it stands and see whether the parens still grate. If it is ever
|
|
built, it is one AST with the existing forms unchanged and a second reader in
|
|
front. Rules out Parinfer, wisp and sweet-expressions, and a simplified in-paren
|
|
syntax — all thin the parens without removing them.
|
|
|
|
** TODO The shims in sand.flan can go
|
|
=sand.flan= defines =dyn->f64= and =dyn->u32=, one-line functions whose only job
|
|
is that their parameter slot unboxes. Every call site can write =(f64 d)= and
|
|
=(u32 d)= now. The author's file, not a lane's to edit.
|
|
|
|
** DONE sand.flan's game-data.edn initialiser no longer aborts the headless import
|
|
A global initialiser that read a file at startup failed wherever the working
|
|
directory was not the author's. It is wrapped in a =handler-case= with a
|
|
=FileError= clause; away from that directory the data reads as nil.
|
|
|
|
** DONE An error in a macro's body is reported where it was written
|
|
CLOSED: [2026-09-21]
|
|
A form a macro splices through keeps its own line, and the =note:= naming the
|
|
macro points at the call. The wire =Form= still carries no location: the
|
|
marshalled payload pointer is the identity key instead, which is SBCL's
|
|
=*source-paths*= with an address standing in for =eq=. A node the macro built
|
|
inherits the nearest located ancestor; only a wholly macro-built subtree falls
|
|
back to the call site. Costs about 2µs a call. Rules out putting a =loc= field
|
|
on the wire, and rules out structural matching of the expansion against the
|
|
arguments, which can pick the wrong one of two equal subtrees.
|
|
|
|
** TODO A declared name may carry the $ sigil
|
|
=(defn $foo [x i32] i32 ...)= is accepted and =($foo 3)= calls it; so is
|
|
=(defstruct $S [a i32])=, whose type can then be written nowhere. The character is
|
|
reserved in every type position and in no name. Refusing it in a declared name
|
|
would close it properly, and that is a decision about the spelling.
|
|
|
|
* Checker
|
|
|
|
** DONE The ownership flow analysis is repealed
|
|
CLOSED: [2026-09-18]
|
|
Static use-after-move and double-free checking is gone; types, allocators and the
|
|
dev build's generation checks are the net instead. An unsound checker is worse
|
|
than none, because it is believed. Rules out a borrow checker returning as
|
|
anything but an additive pass. =docs/BUILT.md= carries the shape of what replaced
|
|
it.
|
|
|
|
** DONE Move-only is gone with it
|
|
CLOSED: [2026-09-18]
|
|
Everything copies as its header, the =copyable?= predicate is off the list, and
|
|
the owning-field refusals on structs and sums are lifted. The region rule stands.
|
|
The earlier ruling that a move-only global's lifetime is the process's — so
|
|
reading one is always a borrow — has no subject left.
|
|
|
|
** DONE Implicit numeric widening is legal; narrowing stays a hard error
|
|
CLOSED: [2026-09-20]
|
|
A conversion is admitted exactly when no value of the source can come out as a
|
|
different number. No second type-checking mode and no flag, which was the
|
|
objection to the =-Wconversion= middle ground that was asked for. Integer into
|
|
float is exact-only, containers are invariant, and =dyn= is not in the lattice.
|
|
|
|
** DONE abs is one generic, and a bound joins to the wider type
|
|
CLOSED: [2026-09-20]
|
|
Numeric scalars bound to one type variable resolve to the join of them all, which
|
|
walks back the same day's "widening does not cross a generic binding". A joinless
|
|
pair is deferred and re-asked against the final binding, which is what makes
|
|
acceptance order-independent. =integer?= exists because =numeric?= admits floats,
|
|
where the branch spelling of =abs= hands back a negative zero.
|
|
|
|
** DONE A conversion is legal at a bounded variable when it is legal at every type the bound admits
|
|
CLOSED: [2026-09-21]
|
|
A machine-type target needs =numeric?=; an enum target needs =integer?=;
|
|
=ordered?=, =equal?= and =hashable?= admit nothing. A predicate gates an operation
|
|
by what it claims, not by the set it happens to denote this week — which is why
|
|
=ordered?= is refused even though every type it admits today converts.
|
|
|
|
** TODO There is now no generic enum to integer conversion
|
|
Recorded as a loss. The one spelling that worked did so by not asking about the
|
|
operand at all, so removing it was still right. =enum?= is the eventual answer —
|
|
it would entail =ordered?= and =equal?= and not =numeric?=, so the cast rule
|
|
becomes a disjunction and the refusal has to name whichever the reader meant. Each
|
|
part of that is a decision and the author has not been asked.
|
|
|
|
** TODO The Ptr and union arms of the fill boundary are relaxable
|
|
What may be byte-filled is numbers, and structs and fixed arrays of numbers.
|
|
A =Ptr= is refused so the rule stays one sentence, and an untagged union
|
|
because the walk goes over a struct's fields rather than a union's members.
|
|
Both are named in the decision as the arms to relax first if it is reopened,
|
|
and a poisoned pointer is arguably the useful case.
|
|
|
|
** TODO A compound constant expression at a bounded type variable
|
|
=(+ x (+ 1 2))= at a bounded variable is refused where =(+ x 3)= works — the
|
|
literal arm admits a bare constant and nothing folds the compound first.
|
|
Walk-backable, so it waits until a body actually wants it.
|
|
|
|
** DONE Generics by monomorphisation, checked abstractly, with where predicates
|
|
CLOSED: [2026-09-13]
|
|
A =where= clause tells the abstract pass what it may assume, so the body checks at
|
|
the definition and the call stays =(sort xs)=. Not a type class — a predicate
|
|
carries nothing and gates a builtin the compiler already has. The fork that said
|
|
an unconstrained =+= over a type variable must be rejected turned out to be false,
|
|
and it is not what Odin does. The five predicates are =ordered?=, =equal?=,
|
|
=hashable?=, =numeric?= and =integer?=.
|
|
|
|
** DONE A type variable takes a $ sigil
|
|
CLOSED: [2026-09-13]
|
|
=$t= binds in a parameter vector and bare =t= reads it. Three reasons, against
|
|
plan.org's "lowercase names are variables, capitalised are concrete": there is no
|
|
binding site without a sigil; introducing one is invisible, so a mistyped type
|
|
made a function *more* permissive; and =[n t]= gave absence opposite meanings on
|
|
the two sides of the bracket.
|
|
|
|
** DONE A type variable may be written with its sigil at a use
|
|
CLOSED: [2026-09-21]
|
|
=(vec-new $t)=, =(map-new $k $v)= and =($t x)= work; every type position already
|
|
did. The feature was specified correctly and three membership tests asked about
|
|
the name as written, where the tables are keyed on the bare name.
|
|
|
|
** DONE Milestone 5 was mostly already there
|
|
CLOSED: [2026-09-20]
|
|
What the lane added was a written integer zero standing where a =numeric?=-bounded
|
|
variable stands — legal because every type =numeric?= admits is an integer or a
|
|
float — the widening boundary, and a refusal for instantiating a type variable at
|
|
=dyn=, which names =defgeneric=/=defmethod= as the other spelling. A float literal
|
|
is still refused at a =numeric?= variable, since the predicate covers both halves.
|
|
|
|
** DONE hashable? gates the type and not the operations
|
|
CLOSED: [2026-09-13]
|
|
=put=, =get=, =has-key?=, =reserve=, =clone= and =map-remove= are deferred to the
|
|
instantiation, joining =print= and =println=. The membership rule is not a
|
|
headcount: either the operation cannot fail after substituting, or a declared
|
|
predicate gives its failure somewhere to land. A generic that does not declare the
|
|
predicate gets no deferral.
|
|
|
|
** DONE The runaway instantiation cap names the chain
|
|
CLOSED: [2026-09-21]
|
|
A structural occurs-check stops an instantiation that asks for a copy of itself,
|
|
and the refusal prints the chain of instantiations that got there rather than the
|
|
depth it gave up at. The bare depth number is a backstop that also prints the
|
|
chain. Before any of it, the compiler hung rather than failed, which wedges =C-c
|
|
C-c= with nothing to show.
|
|
|
|
** TODO Generic types
|
|
=(defstruct Pair [a $t b $t])= cannot be spelled, and neither can a length
|
|
parameter. =Types.Named= is a bare string with no room for parameters; giving it
|
|
some changes the type, the layout calculator, both backends, the renderer and the
|
|
DWARF path. Same price for one as for both. Decided and unblocked, deliberately
|
|
not started — it is a language feature under a freeze, and it was stopped once
|
|
already for that reason. The motivating case is Odin's =Small_Array=: a
|
|
fixed-capacity array with a count and no allocation.
|
|
|
|
** TODO A value predicate over a length parameter
|
|
Odin's =where N >= 0= is a predicate over a value, not a type, and a =where=
|
|
clause here admits nothing but type predicates. Whether it should take value
|
|
predicates over a length parameter deserves answering deliberately rather than
|
|
falling out of the implementation.
|
|
|
|
** TODO "In instantiation of" notes
|
|
A refusal inside a copy points at the generic's source with no note naming the
|
|
call site that asked for that type. The data is there — =instantiation_origin=
|
|
exists and the session already uses it — and wiring it into every failure under an
|
|
instantiation is a lane of its own.
|
|
|
|
** TODO Generics across a real compilation-unit boundary
|
|
It works today because =Load= flattens imports before checking. A package boundary
|
|
that ever becomes a real unit boundary needs the generic's body to cross it, which
|
|
separate compilation cannot do — which is why C++ puts templates in headers.
|
|
|
|
** DONE Collapsing the prelude buys 27 to 15, not 27 to 6
|
|
CLOSED: [2026-09-13]
|
|
The honest number. =sum-*= would widen into a type-level function, which is a
|
|
constraint system or an associated type; =append-i64= and =append-f64= are two
|
|
different primitives and choosing between them per instantiation is compile-time
|
|
overloading. Five do not collapse and should not.
|
|
|
|
** DONE A bare {.field v} takes its type from the position it stands in
|
|
CLOSED: [2026-09-20]
|
|
The refusal moved out of the parser, where it could not see the enclosing =defn='s
|
|
return type, into the checker, which reads the type name off the expectation.
|
|
Refused with no want, at a =dyn= want and at a non-struct want. A =dyn= want keeps
|
|
the dyn map literal — a =.field=-keyed brace is not being given a second meaning.
|
|
|
|
** DONE (Cell 1 2) is positional, and its arity is exact
|
|
CLOSED: [2026-09-20]
|
|
Positional construction gives every field or it is refused. Not a retreat from
|
|
zero-is-initialisation: a positional list cannot say which field it left out, and
|
|
which field a short list omits depends on a declaration order the author is free
|
|
to change. The field-reorder hazard is accepted as the price, with refactoring
|
|
tooling named as the eventual answer.
|
|
|
|
** DONE Shadowing a builtin is legal, and the definition wins
|
|
CLOSED: [2026-09-20]
|
|
Clojure's model: allow shadowing, but warn. Builtin-wins had never been a rule —
|
|
it was the implementation trying the builtin arms first. The shadow reaches
|
|
exactly the file the definition was written in, decided by the file and not by the
|
|
enclosing function's name, because a global initialiser has no enclosing name but
|
|
does have a file.
|
|
|
|
** DONE int and float are builtin aliases
|
|
CLOSED: [2026-09-20]
|
|
Exactly those two, spelled as machine types rather than prelude aliases because
|
|
the cast check does not look in the alias table and =(int x)= had to have a
|
|
reading. Every message still says =i32=. =integer=, =long=, =double=, =uint= and
|
|
=str= keep the teaching refusal, and any other unrecognised lowercase name is
|
|
still a type variable.
|
|
|
|
** DONE A defconst is a compiler const
|
|
CLOSED: [2026-09-20]
|
|
A =defconst='s initialiser has to be a compile-time constant, and the refusal is
|
|
the checker's so both backends refuse the same program. Integer arithmetic is
|
|
folded before the check sees it and the folder is integers only, so =(defconst
|
|
half f64 (/ 1.0 2.0))= is refused.
|
|
|
|
** DONE Container globals start zeroed, and a defconst container is refused
|
|
CLOSED: [2026-09-18]
|
|
A global initialiser is a compile-time constant, a container's only constant is
|
|
the empty one, and a constant is not an assignable place.
|
|
|
|
** DONE A numeric cast opens a dyn box
|
|
CLOSED: [2026-09-20]
|
|
Every numeric cast takes a =dyn= operand. The same kind unboxes; a cross kind
|
|
coerces with a once-per-site warning; a non-number traps. The cross-kind case
|
|
coerces rather than trapping, which overrides the tempting rule of matching the
|
|
parameter boundary. It is lowered as a branch into two ordinary casts so nothing
|
|
has a second opinion about range and NaN, which is what keeps the two backends in
|
|
step.
|
|
|
|
** DONE nil crosses at (Option T) and nowhere else
|
|
CLOSED: [2026-09-20]
|
|
=nil= and =None= are the same value at an =(Option T)= boundary. At a bare =T= it
|
|
is a compile-time refusal where the checker can see it and a run-time trap where
|
|
it cannot. =(Some nil)= and =(Option (Option T))= are unconstructible.
|
|
|
|
** DONE Typed = and != reach strings
|
|
CLOSED: [2026-09-20]
|
|
Bytewise, with length and same-pointer fast paths on both backends. Equality only —
|
|
ordering a string needs a collation nobody has chosen.
|
|
|
|
** DONE dyn truthiness in if, when, cond, and, or, not and while
|
|
CLOSED: [2026-09-20]
|
|
=nil= and =false= are false and everything else is true, on the dyn side only;
|
|
typed conditions stay strict =bool=. =and= and =or= hand back the operand that
|
|
decided them, Clojure's rule, through a desugaring that evaluates each test once.
|
|
|
|
** TODO A bool arm and a dyn arm joining as dyn
|
|
With both arms of a desugared =and=/=or= holding real values, a non-bool =dyn= on
|
|
the losing side meets the strict =bool= boundary and traps —
|
|
=(or false (box "s"))= is the case. Whether a =bool= arm and a =dyn= arm should
|
|
join as =dyn= is the author's call and is not settled.
|
|
|
|
** TODO A truthiness failure re-runs the whole failing subtree
|
|
The retry exists to keep a refused literal's message unchanged and re-runs the
|
|
subtree rather than the leaf, which is exponential in nested =not= depth on a
|
|
program that does not type-check. Moot for anything that compiles; only the
|
|
daemon's half-typed recompiles could feel it. A cheaper retry was tried and
|
|
shelved because it changes which literal gets the nicer message.
|
|
|
|
** TODO and's last operand gets a misdirected caret
|
|
=(println (and true true (vec-new i32)))= puts the caret on the second =true=. The
|
|
last operand of an =and= is the then arm and the then arm is typed first, so the
|
|
mismatch is blamed on the else arm, which carries the previous operand's location.
|
|
The fix is preferring the arm that is not a compiler temp when deciding whom to
|
|
blame. Three others were considered and rejected: relabelling the else arm reads
|
|
backwards, a bool sentinel reverts the =or= fix, and inverting the condition costs
|
|
a =not= per operand.
|
|
|
|
** TODO Signature pairing's cold-rebuild edge
|
|
Whether a parameter vector reads as one annotated parameter or two dyn ones
|
|
depends on what type names exist, so adding a type can silently re-pair an
|
|
existing signature between compiles. A changed-pairing warning was proposed and
|
|
not queued.
|
|
|
|
** DONE A typed container crosses into dyn as a view, and only from permanent storage
|
|
CLOSED: [2026-09-20]
|
|
The descriptor is pointer, length and element type — a slice plus the piece a
|
|
slice is missing. A =Vec= view holds the address of the =Vec='s own header and
|
|
reads pointer and length live, so a reallocating push cannot go stale. Elements
|
|
are =i64=, =f64= and =bool= only. Rules out a heap-held header and anything behind
|
|
a =(Ptr T)=.
|
|
|
|
** DONE A view of a Vec goes stale at the push, and the warning is at the push
|
|
CLOSED: [2026-09-21]
|
|
What was built is the sentence, not a diagnostic. =docs/BUILT.md= beside the =Vec=
|
|
surface table, and =spec-memory.md= under Borrowing, carry it.
|
|
|
|
** CANCELLED A live view at the push, detected cheaply
|
|
CLOSED: [2026-09-21]
|
|
Investigated and not built. =(reserve v 100)= followed by a view and a push is
|
|
correct code, so any per-push flag is a false positive by the language's own
|
|
semantics; the refined version needs liveness across control flow, which is the
|
|
flow tracking that was repealed.
|
|
|
|
** TODO Catching a use-after-release statically
|
|
Open, and for the first time with evidence available: the epoch trap is built, and
|
|
there is a =Vec= to write real arena programs with, so whether the escapes that
|
|
actually occur are lexical can now be answered. The next thing to look at, not the
|
|
next thing to build.
|
|
|
|
** TODO A fixed array of structs or of strings is not a map key
|
|
Refused by name, narrower than the spec's key set; a struct holding the array
|
|
works. It needs the per-element walk a struct key gets, driven by a loop rather
|
|
than a field list.
|
|
|
|
** TODO map-keys and map-values cannot be prelude functions
|
|
Iteration is built; the remaining refusal is generics. A =defn= has to name its
|
|
types and =(defn map-keys [m (Map K V)] (Vec K))= has no =K=. The loop is three
|
|
lines at the call site, where =K= is known.
|
|
|
|
** TODO (vec-new [u8]) is refused
|
|
The element type must be a bare symbol naming a type, so a =(Vec [u8])= can only
|
|
be made where the context names it. The fix is letting it take a type expression —
|
|
the same parser that already reads =[u8]= in a parameter list.
|
|
|
|
** TODO An array literal cannot say it is [f32]
|
|
A float literal defaults to =f64=, an array literal has no context, and a =let=
|
|
has no annotation. Same shape as =(vec-new [u8])= and probably the same fix.
|
|
|
|
** TODO A let binding takes no type annotation
|
|
Everything under the surface is there — the binding carries a type slot and the
|
|
checker consumes it as the want — and only the way it is written is open, because
|
|
=let= is a flat list of pairs and cannot disambiguate by count. No longer the
|
|
blocker it was, since =(array 4 T)= answers the case that raised it. plan.org's
|
|
rule is "annotate function signatures, infer locals", so a general annotation is a
|
|
deliberate absence.
|
|
|
|
** TODO A read-only slice type
|
|
=bytes-view= is read-only by convention only — the type system cannot say a =[u8]=
|
|
may not be stored through, so a trap on read-only memory is the enforcement. A
|
|
read-only slice type, or provenance, is what would move that refusal to compile
|
|
time.
|
|
|
|
** TODO Writing through a string literal
|
|
=(let [s (bytes-view "Hi")] (set (at s 0) \h))= stores into read-only memory at
|
|
=-O0= and is deleted as undefined at =-O2= — same source, and which way it fails
|
|
depends on a flag. Narrowed when =(bytes s)= started copying, so the common
|
|
spelling no longer reaches the edge. Emitting literals as mutable globals is not a
|
|
fix: it moves which flag misbehaves and costs their read-only placement.
|
|
|
|
** TODO (slice d 1) over a dyn string is refused where (at d i) works
|
|
The typed and dyn spaces disagree about a spelling, which the standing rule
|
|
forbids. A dyn slice should exist.
|
|
|
|
** TODO (slice "abc" 0 99) is not refused at compile time
|
|
A string type carries no length, so there is nothing to compare the bound against
|
|
— consistent with a slice of a slice. A missed nicety rather than a hole; the
|
|
runtime check still catches it.
|
|
|
|
** TODO An owning temporary as into's source leaks
|
|
The macro binds a non-name source to a name the caller cannot reach and cannot
|
|
know whether the type owns anything. A call in that position should borrow, and
|
|
=drop= is what would close it.
|
|
|
|
** TODO Notes on the type-mismatch errors
|
|
The most common error class, and it has no second place to point at, because the
|
|
function table records parameter types and a return type and no locations. A small
|
|
change to what collection records.
|
|
|
|
** DONE An error is a value, and there is more than one of them
|
|
The span went into the location type itself as an exclusive end defaulting to the
|
|
start, so every refusal site kept its meaning and a location nobody widened is a
|
|
zero-width span at a point. Macro provenance went the same way. Deliberately not
|
|
collecting: the reader (there is no resynchronising a paren stream), the first
|
|
checker pass, =Load= and the shim.
|
|
|
|
** DONE An index converts from a narrower integer and never from a wider one
|
|
An =i64= index is refused because 2^32+5 truncates to 5 and would read the wrong
|
|
element with no trap at all. A =u32= index works; anything above 2^31 truncates to
|
|
a negative =i32= and the unsigned bounds check rejects it.
|
|
|
|
** DONE There is one top-level namespace
|
|
One pass rejects a second declaration of a name whatever kind either one is. The
|
|
environment's tables are per-kind, so only a function was ever checked for a
|
|
duplicate — a =defn= beside a =defonce= of the same name type checked and then
|
|
died in the backend as a redefinition of a symbol, a message with no source
|
|
location.
|
|
|
|
** DONE A u64 literal is its 64-bit pattern
|
|
The cost of accepting the pattern is that a negative decimal literal is accepted
|
|
as a =u64=, because the reader records the value and not how it was written.
|
|
Narrower unsigned types keep the strict check, which is where a typo like =300=
|
|
for a =u8= shows up.
|
|
|
|
** DONE A folded constant does not skip the range check
|
|
The folding pass makes its own call to the range test, because a global's
|
|
initialiser has to be a compile-time constant and only that pass knows this one
|
|
is.
|
|
|
|
** DONE A defn must state its return type, and an unknown one says so
|
|
=(defn f [] f65 0.0)= says *unknown type f65 — did you mean f64?*. The
|
|
unconditional return slot removed the guess, and the pre-pass that collected a
|
|
file's type names went with it.
|
|
|
|
** DONE Anything that binds a name or alters control flow is recognised explicitly
|
|
The house rule that caught two misparse bugs: anything that binds a name, alters
|
|
control flow, or is not yet implemented must be recognised by name and rejected if
|
|
unsupported. Rules out silently falling through to a generic arm.
|
|
|
|
** DONE The escape was real: a value the compiler builds trips no function-value refusal
|
|
All four function-value refusals were about surface syntax, so =Allocator= could
|
|
be a builtin opaque type with no user-writable constructor and the containers
|
|
needed nothing from generics. The compiler already did exactly this twice — the
|
|
lifted handler clause and the dev build's indirect call.
|
|
|
|
** DONE Function values, with no capture
|
|
=map=, =filter=, =reduce= and a comparator-taking =sort= arrived with no generics
|
|
at all, which is what the diagnosis predicted. A =map= that *changes* the element
|
|
type is the one shape that did not come with them: one copy per ordered pair of
|
|
types rather than per type.
|
|
|
|
** TODO CFn in a struct or a fixed array
|
|
A zeroed function value is a null pointer, so a function value is refused in any
|
|
position zero-initialisation would conjure one — =CFn= included. An =(Option
|
|
(CFn ...))= field is already legal. A table of function pointers is exactly what
|
|
=CFn= is for, and the objection is about zero-initialisation rather than about
|
|
capture.
|
|
|
|
** DONE Structural compatibility is identical layout
|
|
Same fields, same types, same order, so structural compatibility is "the same
|
|
memory" — no copy, no reordering, no adaptor. Writability is the question that
|
|
decided it: read-only structural access could gather fields into a temporary and
|
|
ignore order, writable access has to alias the real storage. Flexible field order
|
|
waits for classes deliberately, because a class owns its layout and a =Vector2=
|
|
should not pay for identity and metadata. Not implemented.
|
|
|
|
* Backends
|
|
|
|
** DONE The x86 backend tracks LLVM at -O0
|
|
CLOSED: [2026-09-20]
|
|
A construct LLVM compiles, this backend compiles, and the two agree on what the
|
|
program observably does. "LLVM takes this and x86 does not" is by itself a defect
|
|
report, not a discussion. The ruling was made by a typed float =%=, which compiled
|
|
under LLVM and died at build time on x86; the fix calls the same function LLVM's
|
|
code generator calls, so agreement is by construction.
|
|
|
|
** DONE The x86 backend is the dev daemon's default
|
|
CLOSED: [2026-09-14]
|
|
=flan dev= picks it and =--llvm= leaves it; every other command is LLVM by
|
|
default. That split is what keeps the calling convention licensed — a dev build
|
|
compiled entirely by one backend, a release build entirely by the other.
|
|
=--debug= takes LLVM's side on its own, because an x86 redefinition module carries
|
|
no line table.
|
|
|
|
** DONE A crossed reload is refused by an ABI marker
|
|
CLOSED: [2026-09-14]
|
|
Each backend defines a marker symbol its own modules reference, and the pair is
|
|
refused at =dlopen= naming both. Before it, an x86 host given an LLVM module died
|
|
at the first redefined function taking a struct and nothing said why.
|
|
|
|
** DONE The x86 redefinition emitter
|
|
CLOSED: [2026-09-14]
|
|
The counterpart to the LLVM one, which is what the whole backend exercise was for:
|
|
until it existed the backend built whole programs and could not serve a single
|
|
=C-c C-c=. It emits no line table, which is why =--debug= goes to LLVM.
|
|
|
|
** DONE Conditions, bounds checks and the cell on the x86 backend
|
|
The transfer guard, the landing pads, the per-function transfer exit, the
|
|
restart machinery and the two bounds checks are written from the specification
|
|
rather than ported, and one cell per function makes every call site redefinable.
|
|
Every program in the corpus that compiles, has a =main= and terminates agrees with
|
|
the LLVM build down to stderr. =docs/BUILT.md=, "The hand-written x86 backend, and
|
|
the four measurements behind it", is the account.
|
|
|
|
** TODO Nothing pins the LLVM side at -O0 when the two backends are compared
|
|
The survey builds both sides at the default =-O2=, so a construct LLVM folds is
|
|
compared as a constant rather than as a lowering. That is how the float =%= gap
|
|
survived. Two things would close it: an =-O0= pass of the sweep, and something
|
|
that walks the two backends' primitive match arms mechanically. Neither is queued.
|
|
|
|
** TODO (uninit) and unreachable differ between the backends, and the language has not said what they mean
|
|
=(uninit)= is stable garbage — whatever the stack slot held — rather than
|
|
=poison=, and an exhausted match is =ud2=, a defined SIGILL, rather than undefined
|
|
behaviour. Both are deliberate and both are now written down, but the *language*
|
|
still has not defined what reading an uninitialised value means, which is the item.
|
|
|
|
** DONE f64 to i64 out of range, and INT64_MIN / -1
|
|
CLOSED: [2026-09-14]
|
|
Carried through three backend reports as "a language decision, not backend work",
|
|
then taken: =ArithError= replaced =SIGFPE= for a divide by zero, for
|
|
=INT64_MIN / -1= and for an out-of-range float-to-int cast, and both backends
|
|
agree on every case. Float division is deliberately unguarded, because IEEE
|
|
already answers it.
|
|
|
|
** DONE Rt with an aggregate return was never a gap
|
|
CLOSED: [2026-09-13]
|
|
Every aggregate-valued runtime result crosses through an out-pointer, so the
|
|
refusal is unreachable and building a hidden-pointer convention behind it would
|
|
have been wrong — that path is the C boundary, where a 16-byte slice comes back in
|
|
two registers. Rules out writing a SysV classifier for it.
|
|
|
|
** DONE An assignment is whole or it never happened, on x86 too
|
|
CLOSED: [2026-09-21]
|
|
Two bugs. An aggregate was built in its destination, so a signal part-way through
|
|
left it part-written — worst for a sum case, whose destination is zeroed first; it
|
|
is built into a frame temporary and copied over now, unless lowering is bound to
|
|
reach the end. Separately, the transfer exit zeroed an aggregate return value,
|
|
which for an aggregate is the caller's storage; it zeroes scalars only.
|
|
|
|
** TODO A global initialiser still builds an aggregate straight into its destination on x86
|
|
The statement-level assignment goes through a frame temporary now, so the
|
|
half-write is closed where a program can observe it. The release build's
|
|
globals-init path was not converted and still writes in place.
|
|
|
|
** TODO An aggregate built in place can read its own destination
|
|
=(set p (P {.a (.b p) .b (.a p)}))= answers =2 1= under LLVM and =2 2= under
|
|
=--x86=: lowering asks whether it can transfer and not whether it can alias. The
|
|
fix has a shape already — the same temporary, chosen by an aliasing question.
|
|
Whether the two become one predicate or two is the lane to decide.
|
|
|
|
** TODO The aggregate temporary is an unrooted buffer while it is filled
|
|
No root table names it. Harmless only while a dyn field in a struct was refused,
|
|
and per-type descriptors have since lifted that. Whoever relies on a dyn field
|
|
reaching this path has to root the buffer, or a collection running inside the
|
|
construction will not see what has been built so far.
|
|
|
|
** TODO Marking through a descriptor an x86 reload module emitted
|
|
The module links and runs. What is not proved is a collection running while a live
|
|
instance of a dyn-holding struct sits in a frame of a body that module delivered.
|
|
For the next sweep rather than for a lane.
|
|
|
|
** TODO A sliced string loses the trailing NUL
|
|
The x86 backend emits a NUL after every string constant and the LLVM one does not,
|
|
so a =declare-c= wrapper leaning on the courtesy is already backend-dependent as
|
|
well as slice-dependent. The contract is pointer and length, and nothing promised
|
|
otherwise.
|
|
|
|
** TODO Frame descriptions are gated on --debug
|
|
They are correct in every build and free at runtime, and a release build is where
|
|
a crash would most want them. One =if= in three places.
|
|
|
|
** TODO A !DILexicalBlock per Let
|
|
Inside nested =let=s that bind the same name, a debugger still answers with the
|
|
outer one. The disambiguating suffix makes both visible, which is not the same as
|
|
making the answer right. It needs block structure the typed IR does not carry, and
|
|
the variable declarations moved out of the entry block.
|
|
|
|
** TODO UBSan sees no Flan code, and no flag changes that
|
|
UBSan's checks are branches clang's C frontend emits inline, not a pass, so shift
|
|
undefined behaviour, alignment and a NaN float-to-int cast are unreached. Either
|
|
the emitter grows those checks behind the flag — a compiler feature of the same
|
|
shape as the bounds checks — or they belong to the checker. Not decided.
|
|
|
|
** DONE A JS backend is a dialect, not a second machine
|
|
CLOSED: [2026-09-17]
|
|
Object mapping, not linear memory: a Flan struct becomes a plain JS object. That
|
|
means garbage collection, so no pointers, no manual free, no arena and no
|
|
allocator — some Flan programs will not compile to JS, and that is named up front
|
|
rather than discovered. Reader conditionals are Clojure's inline ones, not
|
|
file-level target naming. Rules out asm.js-style linear memory, which is what wasm
|
|
exists to replace.
|
|
|
|
** WAIT The JS backend is held behind the dev loop
|
|
Held 2026-09-17. wasm32 already reaches the browser, and a third backend beside the two that exist
|
|
is the largest item on the list. In the meantime =lib/js.ml= compares string views
|
|
by identity and answers string equality wrongly; the option on the table when it
|
|
is picked up is a loud refusal in that arm rather than an implementation, so the
|
|
dialect says it cannot do this instead of saying something false.
|
|
|
|
** TODO The web target does not reach four things
|
|
Nothing has been opened in a browser — =node sand.js= gets as far as =glfwInit=
|
|
before dying on =window is not defined=, which proves the module is live and
|
|
proves nothing about the canvas. Asyncify's cost is quoted rather than measured,
|
|
no frame time on web has been taken, raylib's audio and threads on web are
|
|
untried, and a wasi build reaching raylib fails on undefined symbols because the
|
|
raylib link line is tagged native.
|
|
|
|
** DONE A package's .c files can be addressed to a target
|
|
A tag in the name before the extension — =flan_agent.web.c= replaces
|
|
=flan_agent.c= on a web build. Refusing the agent package on web was the
|
|
honest-looking option and is ruled out by arithmetic: there is no conditional
|
|
compilation, the flagship program calls into the agent unconditionally, and
|
|
=Reach= cannot prune a package something reachable calls into. A refusal is only
|
|
honest when the caller has a way to not ask.
|
|
|
|
** TODO The IR and the disassembly are not annotated with the source
|
|
The emitter writes =.ll= as text and every typed IR node carries a location, so an
|
|
IR comment costs nothing and cannot break anything. The disassembly half is
|
|
=objdump=, which the daemon already shells out to without =-S= or =-l=. Settled in
|
|
conversation: =-O0= gets the full annotation and =-O2= gets nothing or whatever
|
|
best-effort mapping falls out, so =--debug= forcing =-O0= is fine. Writing a
|
|
disassembler stays off the table; richer annotation of objdump's output needs only
|
|
what the daemon already holds, which is where SBCL's advantage actually comes
|
|
from.
|
|
|
|
* Runtime
|
|
|
|
** DONE An index out of range is a condition
|
|
CLOSED: [2026-09-13]
|
|
A failed bounds check signals =BoundsError=; a handler can answer it, a
|
|
=restart-case= catches the transfer, and an unanswered one dies with the location
|
|
and the index. =Vec='s checks are plumbed the same way, since indexing an array
|
|
and indexing a =Vec= are one form. Rules out exiting on the spot.
|
|
|
|
** DONE No restart is established at the failing index
|
|
CLOSED: [2026-09-13]
|
|
=retry= exists for allocation and for files because those attempts are repeatable;
|
|
nothing a handler can do makes index 51 valid for a length-50 array. =use-value=
|
|
for the index would cost every indexing operation a restart frame. What answers a
|
|
bad index is the program's own =continue=.
|
|
|
|
** DONE ArithError signals and dies, with no restart offered
|
|
The runtime cannot push a restart frame on a program's behalf — a restart frame is
|
|
allocated in the =restart-case= that offers it. Rules out =use-value= at the
|
|
failing operation. Only a =saturate= restart on the cast arm alone was left as a
|
|
question, and it is not written anywhere.
|
|
|
|
** DONE slice-from-ptr's runtime refusal names the promise
|
|
CLOSED: [2026-09-14]
|
|
It used to reuse the slice error and report a range and a length the caller
|
|
never wrote. =slice-from-ptr= is the one form where the compiler cannot check
|
|
the thing that matters, so its refusal is where the promise is spelled out. The
|
|
check is signed on purpose: a negative length sign-extended is a huge unsigned
|
|
value an unsigned compare waves through.
|
|
|
|
** DONE A dying program uses _exit
|
|
=exit= runs the atexit chain and the ELF destructors, which want the loader
|
|
lock the agent's listener thread may hold inside =dlopen=, so a program that
|
|
should die could hang. Unconditionally rather than only under =--dev=.
|
|
|
|
** TODO A Map's bounds check and the stale-container failure still die
|
|
Deliberate for the stale case — the region the container lived in was released and
|
|
there is no frame to go back to that would not read freed memory. The Map path was
|
|
left alone rather than converted half-way.
|
|
|
|
** DONE Six trap paths park instead of killing the session
|
|
CLOSED: [2026-09-18]
|
|
A trap in a dev session stops for inspection rather than taking the daemon with
|
|
it. Two of the six refuse the resume, because there is nothing to resume into. A
|
|
standalone build dies as before.
|
|
|
|
** DONE bytes copies, string constants trap, and a segfault parks
|
|
CLOSED: [2026-09-20]
|
|
An in-place sort over =(bytes "INSERTIONSORT")= wrote into a string constant and
|
|
took the whole session down. =(bytes s)= allocates a writable copy through the
|
|
allocator surface — never a hidden malloc — and =(bytes-view s)= is the old free
|
|
reinterpret. A dev build installs a SIGSEGV handler that parks in the break loop;
|
|
it needs =SA_NODEFER=, because the handler is the park and never returns.
|
|
|
|
** DONE Allocators, (Vec T) and StorageExhausted
|
|
Three amendments to a frozen spec: free-all is retain-capacity with
|
|
=arena-destroy= beside it; the allocator context is a dynamic variable rather than
|
|
a calling-convention parameter; and the =Vec= header is the same size in every
|
|
build, because a layout that changes with a build flag can disagree silently
|
|
across the reload boundary. One addition: a budget, because =retry= needs a
|
|
handler that can make the same request succeed.
|
|
|
|
** TODO The Vec generation word has no reader
|
|
It is bumped on reallocation and read by nothing. The stale-slice trap it exists
|
|
for needs a slice that can carry the Vec's identity, and a slice is pointer and
|
|
length — so either slices grow a word in a dev build or the trap does not exist.
|
|
Today it does not.
|
|
|
|
** TODO The allocator's budget is not in the spec
|
|
=alloc-budget= and =set-alloc-budget= exist and the spec does not mention them.
|
|
Worth folding in or replacing with a growable arena.
|
|
|
|
** TODO The Vec header is not the size the spec fixes
|
|
Five words in every build rather than the spec's four, and for a stated reason: a
|
|
redefinition module is built separately from its host and nothing makes the two
|
|
agree on a struct size. Give the reload path a way to carry the build flags and
|
|
this falls out.
|
|
|
|
** TODO arena-destroy under a live view reads freed memory
|
|
The epoch check that makes free-all safe under a live view does not survive
|
|
=arena-destroy=, which frees the block holding the epoch. It happens to trap in
|
|
practice because the freed block still holds the bumped value. A question about
|
|
=arena-destroy='s ordering, not about views, and the typed side has the same shape.
|
|
|
|
** DONE Map removal costs a backward-shift loop
|
|
Removal landed, with the loop the spec predicted as its cost. Deferring it was
|
|
what had kept the implementation free of tombstones and of Odin's
|
|
backward-shift loop; taking it is taking the loop.
|
|
|
|
** TODO The Map is slower than CPython's dict at a million entries
|
|
Keys, values and hashes are three separate runs, so a lookup that misses
|
|
everything costs three cache misses where a compact dict costs two. One byte of
|
|
metadata a slot — the Swiss-table arrangement — is the known answer and is not
|
|
built. The crossover is somewhere between ten thousand and a million and nobody
|
|
has found it.
|
|
|
|
** DONE dyn maps and interned keywords
|
|
CLOSED: [2026-09-20]
|
|
Keywords are interned and immortal, so equality is pointer equality and there is
|
|
no collector object to attribute to one.
|
|
|
|
** DONE Per-type descriptors make a dyn field in a struct markable
|
|
CLOSED: [2026-09-20]
|
|
A struct or a condition holding a =dyn= carries a descriptor naming the byte
|
|
offsets the collector must follow, emitted by both backends and by both
|
|
redefinition emitters. The stopgap refusal on a dyn field in a struct is lifted.
|
|
|
|
** DONE A redefined defclass migrates its instances lazily
|
|
CLOSED: [2026-09-20]
|
|
CLHS 4.3.6 minus the user hook. Nothing is enumerated and no heap is walked — the
|
|
redefinition is constant time and each instance pays once, at its next touch.
|
|
Neither printer migrates, so a stale instance shows its old slots to the editor
|
|
until something touches it. The registry is advisory: a key the class never
|
|
declared is dropped by the next migration, which is data loss with no enforcement
|
|
behind it.
|
|
|
|
** TODO A class registry keeps one slot list per class, not one per layout version
|
|
"A redefined class's old instances stay resolvable" needs every version's metadata
|
|
retained for as long as any instance holds it, the way nothing is ever
|
|
=dlclose=d. What exists is one current slot list and one generation per class, and
|
|
migration is lazy and additive.
|
|
|
|
** DONE A spin is not patience: the registry's slot read
|
|
CLOSED: [2026-09-21]
|
|
Sixty-four bare re-reads of one word finish in about two microseconds, so against
|
|
a descheduled writer the old budget was not small — it was zero wall clock. There
|
|
was no timeout to widen; what was added is the first wall-clock patience the slot
|
|
read ever had. A refusal now means the table genuinely would not hold still.
|
|
|
|
** DONE The 4K result cap is not a transport buffer and stays
|
|
The bound is the buffer the *game thread* writes into, so a growable one means the
|
|
frame thread calling realloc, and that breaks the seqlock, which assumes the
|
|
address it copies from does not move. Removing it is a redesign of the read and
|
|
belongs with moving the read to a frame boundary. Rules out deleting it as
|
|
transport machinery.
|
|
|
|
** DONE The snapshot copying and generation stamping stay
|
|
It was never about two address spaces — it is about two threads, and there are
|
|
still two. The break loop polls, a thunk it runs is arbitrary Flan that pushes and
|
|
pops the live restart list, and the generation stamp keeps a nested break from
|
|
claiming a choice made against the outer one. Rules out deleting them with the
|
|
transport.
|
|
|
|
** TODO The seqlock's losing race has no test
|
|
The result read copies into the caller's buffer and checks the counter either
|
|
side; nothing drives the case where the counter moves. The one threaded test races
|
|
the registry table instead.
|
|
|
|
** TODO The snapshot generation's racing stale claim has no test
|
|
The nested-break case is tested deterministically now. What is still absent is the
|
|
race: landing a request inside a two-millisecond poll from outside the process. It
|
|
wants a hook the test can drive, not a sleep.
|
|
|
|
** TODO SNAP_MAX and SNAP_NAMES are read rather than tested
|
|
Three of the four named buffers have evidence. Only this pair is still read rather
|
|
than driven, and sixty-five nested =restart-case=s are a lot of program for a
|
|
clamp.
|
|
|
|
** CANCELLED Probing for an interior overrun under memcheck
|
|
CLOSED: [2026-09-12]
|
|
An arena is one malloc and a Map's four runs are one allocation, so an interior
|
|
overrun is not observable, not merely unreported. No client request fixes it.
|
|
Recorded so it is not re-proposed as a gap in the sweep.
|
|
|
|
** DONE ASan does not see an uninitialised read; valgrind does
|
|
CLOSED: [2026-09-12]
|
|
memcheck needs no instrumentation — it works on the binary, so hand-written IR
|
|
arrives on the same footing as clang's C, which is why it was reachable where MSan
|
|
was not. Rules out treating a clean sanitizer run as evidence about stack
|
|
lifetime.
|
|
|
|
** DONE The memcheck half of the allocation registry
|
|
CLOSED: [2026-09-14]
|
|
The registry knows a free-all killed everything in a region, so a later read
|
|
through a pointer into it is answerable; memcheck is told the same fact, so the
|
|
same read is reported. The two stay two claims — different tools reaching
|
|
different people.
|
|
|
|
** TODO The leak question across the corpus
|
|
Both sweeps run with leak checking off, because allocate-once-never-free is this
|
|
runtime's design and a leak check produces a suppression list. A green sweep
|
|
therefore says nothing about who frees the newly allocating =(bytes s)=. Worth
|
|
asking on purpose one day, across the whole corpus and not one program.
|
|
|
|
** TODO An unhandled condition has no location
|
|
The error entry point takes five integer arguments, which fills the argument
|
|
registers; a location pair makes seven, so the x86 backend would need stack
|
|
argument passing at a call site whose register file is exactly full. The dev-side
|
|
half is different work: the trap hook hands control to a session in-process with
|
|
the compiler, which can read the source.
|
|
|
|
** TODO trap_oom has no site
|
|
It is reached from the allocator, which has no site to be given. The range trap
|
|
already carries the location pair and every caller passes null, so giving =at=,
|
|
=set-at= and =push= a site is a call-site change rather than another round of
|
|
signature churn.
|
|
|
|
** TODO A restart has no location
|
|
The restart frame is mirrored across both backends and the runtime, so giving
|
|
=continue= a file, line and column means two fields, stores in both backends, an
|
|
accessor, the snapshot copying it and the buffer printing it. A cross-backend ABI
|
|
change; do it as one lane, not as a rider. A site for user =error= calls is the
|
|
same lane if the frame is being touched anyway.
|
|
|
|
** TODO handler-case's own restart is listed in a break loop under it
|
|
The restart the form makes up for itself is on the restart stack like any other.
|
|
Hiding it means a new field in the frame layout written out in both backends and
|
|
the runtime. Choosing it is refused loudly rather than answered wrongly, so this is
|
|
cosmetic.
|
|
|
|
** TODO A formatted number does not outlive its frame
|
|
The conversion buffer is one frame slot per call site, so returning a string built
|
|
from it returns a view of storage the return has just released, and pushing one
|
|
pushes an element aliasing that slot. Neither shape is refused. Copy the bytes for
|
|
anything that outlives the expression that made them, which is what =append-i64=
|
|
and =append-f64= do.
|
|
|
|
** DONE A shift count is bounded two different ways
|
|
A literal count out of range is rejected by the checker; a computed one is masked
|
|
to the operand's width minus one. A shift by the operand's own width is poison in
|
|
LLVM rather than a wrong number — =(<< 1 32)= compiled to a bare return. The mask
|
|
is what the hardware does anyway and is folded away when the count is constant.
|
|
|
|
** DONE The linked-list frame beat an array with a stack pointer
|
|
The opposite of what the escaping-alloca argument predicts, and the measurement
|
|
that first said otherwise was comparing a 40-frame binary with a 600-frame one.
|
|
That is why every number in =docs/BUILT.md= is a minimum of nine runs.
|
|
|
|
** TODO runtime/flan_dyn_stub.c is dead
|
|
No dune rule mentions it, no module refers to it, no test links it, and it does
|
|
not compile — two conflicting-type errors against its own header. It is maintained
|
|
by accident: one lane added a function to it, which is duplicity on the same side
|
|
of the same capability. The recommendation is delete, and the author added the
|
|
file, so it is his call.
|
|
|
|
* Dev loop
|
|
|
|
** DONE The dev loop, step 1: the reload primitive
|
|
A list of top-level forms is recompiled and installed into a running process, and
|
|
call sites compiled before those forms existed follow them through an indirection
|
|
cell.
|
|
|
|
** DONE The dev loop, step 2: a name the process was never built with
|
|
A =defn= or a =defonce= the process was not built with can be added and then
|
|
redefined again, through a by-name cell in the dev registry.
|
|
|
|
** DONE The dev loop, step 3: the agent installs at a frame boundary
|
|
The program takes a redefinition over the agent socket and installs it between
|
|
frames.
|
|
|
|
** DONE The compiler is a thread inside the program
|
|
CLOSED: [2026-09-14]
|
|
One binary that is the compiled program and the whole OCaml compiler. The program
|
|
keeps =main()= and the compiler comes up on a side thread beside the agent's
|
|
listener, rather than the program being loaded into the daemon — macOS needs a
|
|
window on the main thread, and the agent was already a server inside the program.
|
|
Crash isolation is given up knowingly. =docs/BUILT.md=, "One process", is the
|
|
account, and the embedding spike that made it safe to commit to is under it.
|
|
|
|
** DONE Transport and code generation were independent, and transport was the larger prize
|
|
CLOSED: [2026-09-14]
|
|
Merging the processes removed the transport; a new backend removes code
|
|
generation. Neither implied the other, and the two were repeatedly conflated. The
|
|
order held: spike the embedding, merge while keeping the existing build path
|
|
exactly as it was, measure what was left, and only then choose a backend.
|
|
|
|
** DONE Re-runnable main after the window closes
|
|
CLOSED: [2026-09-17]
|
|
A finished program parks instead of dying, and a daemon op wakes it and re-enters
|
|
=main= on the same thread, because a window belongs to the thread that opened it.
|
|
Globals are not reset between runs — the process never died. Rules out a fresh
|
|
process per run.
|
|
|
|
** TODO Re-run does not work under --two-process
|
|
A finished child process is genuinely gone, so there is nothing to wake. Re-run is
|
|
merged-build only, and since the default backend runs merged it is no longer the
|
|
blocked case.
|
|
|
|
** DONE An accepted re-run reads as running
|
|
CLOSED: [2026-09-21]
|
|
A caller that asked for a re-run and then waited for the program to park was
|
|
answered by the park it had just ended. The state is set under the lock that
|
|
accepted the request, so the state is the decision rather than a report of it.
|
|
Rules out teaching the tests to wait on something else — the runtime refuses a
|
|
second re-run precisely because the first is committed, so the two answers
|
|
disagreed about the same fact.
|
|
|
|
** DONE C-x C-e installs a top-level form
|
|
CLOSED: [2026-09-17]
|
|
Context-aware: a top-level form compiles and installs, anything else evaluates as
|
|
an expression. =C-c C-c= stays the explicit alias.
|
|
|
|
** DONE C-x C-e answers against a parked program
|
|
CLOSED: [2026-09-17]
|
|
An expression evaluates against a parked program by draining the agent's ring from
|
|
the park, rather than by loosening what a frame boundary is. Safe because a parked
|
|
program has no concurrency at all.
|
|
|
|
** DONE Evaluating a def assigns
|
|
CLOSED: [2026-09-21]
|
|
=def= is Common Lisp's =defparameter=, and evaluating a =defparameter= assigns.
|
|
The previous reading — a promise about the next re-run — was wrong for the reason
|
|
the form is named after. Both events happen: an evaluation that assigns and a load
|
|
that re-initialises. =defonce= and =defconst= are unchanged.
|
|
|
|
** DONE An evaluated expression that signals says so at once
|
|
CLOSED: [2026-09-20]
|
|
The wait recognised only a pause, so every other stop fell through to a timeout
|
|
arm whose sentences were about something else. "A stop entered after the module
|
|
was delivered" is a generation number rather than a name, so evaluating from
|
|
inside a break into a thunk that stops on the same condition class is settled by
|
|
comparing two integers.
|
|
|
|
** TODO Whose break it is, which no counter answers
|
|
A game loop that signals during the build or the wait bumps the generation exactly
|
|
as a thunk would. The machine-readable fields stay right; what is wrong is the
|
|
sentence. The per-frame program-or-eval label is computed by the daemon from
|
|
ownership, not from anything in the frame, so this is not the shadow-stack gap it
|
|
was once written down as. Not queued — the window is narrow.
|
|
|
|
** DONE The first evaluation no longer stalls behind the agent socket
|
|
The accept loop used to sit behind a ten-second wait for the agent socket, so a
|
|
program that binds its socket late — or not at all — looked ready and answered
|
|
nothing. The wait is no longer in front of the accept loop.
|
|
|
|
** DONE The parked-program note is said once per park
|
|
A finished program is always parked, so re-evaluating =main= after every run
|
|
printed the whole note again. The daemon remembers whether it has said it for this
|
|
park.
|
|
|
|
** DONE A session ends when no editor has held the socket for a grace period
|
|
CLOSED: [2026-09-18]
|
|
Two graces — five minutes parked, thirty seconds live — because a parked program
|
|
is invisible and a live one may be a window somebody is watching. Armed only after
|
|
a first client has connected, so a headless daemon is untouched.
|
|
|
|
** DONE Parked orphans exit with their daemon
|
|
CLOSED: [2026-09-18]
|
|
A child outliving its daemon is killed by the kernel. Found in passing that half
|
|
the observed orphans were merged daemons whose editor had vanished, which the
|
|
grace above answers.
|
|
|
|
** DONE The daemon survives a client that closes mid-reply
|
|
CLOSED: [2026-09-14]
|
|
A reply written into a socket whose reader had gone used to kill the process, and
|
|
in a merged build that process is the program, the compiler and the listener at
|
|
once. It reported as a connection refusal on a path that plainly existed, which
|
|
misdirected two investigations.
|
|
|
|
** TODO The daemon leaves its temp directory behind
|
|
About seven megabytes a session, and nothing removes it. Two deliberate non-goals
|
|
when it is fixed: not on a crash, because the directory is the post-mortem, and
|
|
never another session's directory, because a stale pid is not proof of anything.
|
|
The agent socket goes with it.
|
|
|
|
** DONE (agent/start) takes no argument, and binds before main
|
|
CLOSED: [2026-09-20]
|
|
The zero-argument form takes the daemon's socket where there is one and an
|
|
announced path where there is not, bound by a constructor before =main=. =Reach=
|
|
prunes a package nothing calls into, so the constructor reaches only a program
|
|
that polls or waits. Full invisibility — a dev build linking the agent whether or
|
|
not the source says so — needs the package force-linked and is a decision about
|
|
what =--dev= means.
|
|
|
|
** TODO FLAN_AGENT_SOCKET in a shell's environment steals the socket
|
|
Binding unlinks the path first, and before the constructor that unlink was reached
|
|
only by an explicit call. A sentence about the shape of the gate rather than an
|
|
observed problem: only the daemon sets the variable and it never runs release
|
|
builds. The fix, if it is ever felt, is a narrower gate.
|
|
|
|
** TODO The daemon's "has not called (agent/start ...)" note is unreachable
|
|
Unreachable, not merely unexercised: the one state it was true of is closed by the
|
|
constructor. Retiring it is the author's call over a lane that merged days ago, so
|
|
it is left in place saying a true thing about a state nothing can be in.
|
|
|
|
** DONE The allocation registry
|
|
CLOSED: [2026-09-13]
|
|
One insert per allocation, always on in a dev build, no opt-out. Nothing
|
|
per-region, no range recording, no per-allocator opt-out. It does not cover stack
|
|
locals and globals, which the shadow stack and the static type table answer by
|
|
name; a stack address is deliberately not in the table.
|
|
|
|
** DONE The shadow stack
|
|
CLOSED: [2026-09-12]
|
|
The route to a backtrace and to locals together, dev-only so a shipped game pays
|
|
nothing. Chosen over DWARF deliberately: DWARF owes a lexical block per =let=
|
|
before shadowed locals are even honest, and that buys locals in lldb rather than in
|
|
the break loop.
|
|
|
|
** DONE The x86 backend pushes shadow-stack frames
|
|
It does, in every dev build. The prose in the daemon that rewrites the agent's
|
|
reply to say otherwise is now false and should go with whoever next touches it.
|
|
|
|
** DONE A restart is not a transaction
|
|
If a frame mutates a global and then signals, taking a =retry= re-runs the
|
|
mutation. Nothing rolls back, and Common Lisp offers no help either. The discipline
|
|
is that the author chooses where the retry boundary is, and it matters more here
|
|
because the intended use is a game loop.
|
|
|
|
** DONE The break loop's display pass
|
|
CLOSED: [2026-09-20]
|
|
Off a dogfooding session that hit a bounds error with no line number and
|
|
unreadable field names. The headline reads the condition's fields inline, the
|
|
buffer draws the source line with a caret, compiler temps are hidden from the
|
|
locals listing, and a shadowed restart is takeable by index.
|
|
|
|
** DONE The break buffer opens by itself when the program stops
|
|
The client already knew the moment it happened. Three questions settled with it:
|
|
it displays rather than takes focus, =(pause)= is not special, and the behaviour is
|
|
a defcustom.
|
|
|
|
** DONE The condition itself is on the wire
|
|
The agent keeps the condition pointer beside its name and a verb hands it back, so
|
|
the editor can render the condition's own fields rather than only its class.
|
|
|
|
** TODO A restart's source location and arity are not on the wire
|
|
The restart frame is =prev=, a name id, a name and a length. A backtrace and
|
|
locals landed out of the shadow stack and needed no debug information; these did
|
|
not come with them.
|
|
|
|
** TODO The editor half of a typed restart
|
|
The language half is in — a restart clause takes parameters and =invoke-restart=
|
|
passes them. What is missing is the half only an editor can do: arity and signature
|
|
on the frame, the restart listing carrying the signature, and the daemon compiling
|
|
each argument against the declared type and writing the values into the frame's
|
|
buffer before aiming the channel.
|
|
|
|
** TODO The type identity of a local is not qualified
|
|
Settled for conditions and for structs, because =Load= qualifies every declaration
|
|
at import. Still open for locals, where the debug information gives a bare name and
|
|
nothing qualifies it.
|
|
|
|
** TODO The render-thunk-per-inspection design
|
|
An inspection still compiles a thunk per request. A redesign rather than a
|
|
deletion, and its own lane: it is what unblocks the inspector retaining a value.
|
|
|
|
** TODO The watch design is reopened
|
|
Push was chosen partly because polling costs a compile, and that premise weakened
|
|
when the processes merged. The table-and-read design stands; whether it should stay
|
|
pushed is open.
|
|
|
|
** TODO A watch over a struct or a slice
|
|
Scalars work today through four runtime entry points and need no compiler change.
|
|
A struct or a slice needs a compile-time walk over its type — one arm beside
|
|
=print=.
|
|
|
|
** DONE Two ways to root a walk
|
|
The inspector takes a frame and a slot index as well as an expression. An index is
|
|
the thing a listing can hand back where an address is not something an editor
|
|
should hold. The one prediction that did not survive contact: walking between the
|
|
two modes was listed as a cost and is not one, because a stack entry carries its
|
|
own root.
|
|
|
|
** DONE C-c C-c on a generic installs its instantiations
|
|
Only instantiations reach the function list, so a generic name used to report that
|
|
it had installed nothing at all. The session expands to instantiations before
|
|
reporting.
|
|
|
|
** TODO Signature generations and stale-caller warnings
|
|
The biggest hole in "you never restart the program". A changed signature is
|
|
refused outright today and that is a placeholder, not the design. It needs function
|
|
versions, a trampoline per version, and caller tracking good enough to name the
|
|
sites; the cell gives the indirection, and what is missing is that a cell holds one
|
|
bare pointer with no signature, so there is nowhere to put a second version.
|
|
|
|
** DONE A module carrying a string literal is never unloaded
|
|
The transient rule is that a module retaining nothing may go, and a string literal
|
|
counts as something retained — which silently stopped every module carrying one
|
|
from ever being unloaded. That is why frame descriptors got their own counter.
|
|
|
|
** DONE A redefinition delivered while parked installs on the next re-run
|
|
The park used to drain the agent ring only when something had asked it to poll,
|
|
and a plain redefine does not, so every generation ran one re-run later than
|
|
whoever pressed the key expected. The drain happens in front of the exit now.
|
|
|
|
** TODO A dyn value from eval-expr never reaches the reply's value field
|
|
It renders to the program's own stdout and arrives on a later reply's output
|
|
instead. Where a dyn expression's value should surface is a question about the
|
|
editor protocol.
|
|
|
|
** DONE Memory diagnostics on demand
|
|
CLOSED: [2026-09-20]
|
|
Two classes of allocation, surfaced on demand and never changing what compiles.
|
|
Precision over completeness: a site that does not allocate is never marked, and
|
|
=vec-new=, =map-new=, dyn arithmetic, keywords and dyn push and put are
|
|
deliberately silent, each for its own reason.
|
|
|
|
** TODO A debug tracking allocator over the raylib boundary
|
|
ASan's leak detection covers memory instrumented code allocated — the Flan
|
|
allocator, already clean. It does not cover a leaked texture, because that memory
|
|
belongs to uninstrumented raylib. Every raylib call goes through a generated
|
|
wrapper, so a dev build can count acquisitions against releases there and report
|
|
what is still held at exit, by name.
|
|
|
|
** DONE --dev --sanitize was unbuildable, and nothing built it
|
|
CLOSED: [2026-09-21]
|
|
clang's sanitizer pass faulted on a constructor table naming a function the
|
|
module only declares; the table names a local definition that calls the two
|
|
now. A configuration nothing builds can be broken for a month, and this one
|
|
was, so the sweep builds dev programs twice under the existing alias.
|
|
|
|
** DONE test_dev's abort rows raced and killed the binary
|
|
CLOSED: [2026-09-20]
|
|
What says an abort worked is the waitpid underneath it, not the reply — the
|
|
program exits from its own thread while the reply is composed on another, and
|
|
nothing orders the two. The read raised on a closed socket and the test binary
|
|
exited 1 with no failure line, which is the worst shape a failure can have when
|
|
a lane is judged on the exit status.
|
|
|
|
** TODO A program driven by a real flan dev daemon under a sanitizer
|
|
The daemon builds its host through its own path and the CLI has no way to pass a
|
|
sanitizer flag to it. Named as the check worth adding next; a day rather than an
|
|
hour. The x86 backend is not a gap here — that pair is refused by name, because
|
|
there is no sanitizer pass over hand-written assembly.
|
|
|
|
** TODO A transient signal 11 on a globals daemon
|
|
Seen once, never reproduced, on a daemon whose fixture had just gained a host
|
|
global =Vec= and a run-time-new one. A reproduction under load would settle it.
|
|
|
|
** TODO test_dev daemons fail to bind under load
|
|
Daemons exiting with status 1 or 2 before binding their socket, across most of the
|
|
file at once, while the load average is high. No stale socket or leftover daemon
|
|
afterwards; green on a quiet machine. Distinct from the registry race and the
|
|
stale-park re-run flake, both of which are fixed. A daemon that dies before it
|
|
binds died on the compiler's side, before any program it built has run a line.
|
|
|
|
** DONE A test binary that hangs is killed by its own alarm
|
|
A reader branch that forgets to advance loops for ever and the suite waits as long
|
|
as it is left to; in CI that is a job the runner kills with nothing named. The
|
|
alarm is generous on purpose, because an alarm that fires on a slow machine is a
|
|
flake and a flake is how a watchdog gets deleted.
|
|
|
|
** DONE A forked acceptance failure reaches the exit status
|
|
The fork pool is drained before anything reads the failure count, and a nonzero
|
|
count is an exit status. A red row used to be able to print and pass.
|
|
|
|
* Editor
|
|
|
|
** DONE The syntax table and the font-lock lists are read off the parser
|
|
CLOSED: [2026-09-21]
|
|
The special-form list is the heads the parser dispatches on, the builtin list is
|
|
the checker's, and the constants are the bare symbols; =defmacro= is a definer.
|
|
Three kinds of name had been mixed into one list, so =push= was drawn like =let=.
|
|
|
|
** DONE A user-defined macro is highlighted from the live session
|
|
CLOSED: [2026-09-21]
|
|
The definitions op gained a macro kind and the mode consumes the live cache as a
|
|
font-lock source through a matcher function over a hash table. Macro-ness had been
|
|
erased by check time, so the op was extended rather than the editor made to guess.
|
|
CIDER over nREPL is the precedent. The rules carry no override flag, so the static
|
|
table wins by mechanism and a program defining its own =length= cannot repaint the
|
|
builtin.
|
|
|
|
** DONE A discarded form is drawn as a comment
|
|
A syntax-propertize function gives a =#_= span the comment class, clojure-mode's
|
|
approach, including chained discards. The compiler side was never the gap.
|
|
|
|
** DONE A binding vector indents name-under-name
|
|
CLOSED: [2026-09-21]
|
|
The indenter is ported from clojure-mode's source rather than derived from it, so
|
|
brackets-mean-binding and pairs-align come with it. =defn= parameter lists and
|
|
=restart-case= clause parameters were the same shape and were fixed in the same
|
|
pass. Deriving from clojure-mode at runtime stays rejected: it would add an
|
|
external dependency to a mode that ships in this repository and needs nothing
|
|
beyond stock Emacs, and that community is mid-transition to a tree-sitter mode.
|
|
|
|
** TODO 373 lines in 20 files still reindent differently
|
|
Concentrated in four files, untouched by the Emacs pass and untouched before it.
|
|
The indenter and the hand-formatting there disagree about shapes nothing has
|
|
looked at.
|
|
|
|
** DONE C-c C-i inspects the expression at point
|
|
No prompt, because the expression is already written in the buffer. =C-u= opens
|
|
the minibuffer instead, pre-filled, and so does having nothing at point to take.
|
|
|
|
** DONE An evaluation's value is shown at the end of the line and echoed
|
|
Both, not one or the other. The overlay used to suppress the echo on the argument
|
|
that saying one number twice teaches a reader to skip both; in practice the echo is
|
|
the half still there after the next keystroke takes the overlay down.
|
|
|
|
** DONE Two streams and one tool list
|
|
CLOSED: [2026-09-20]
|
|
The daemon's log is one buffer under =compilation-minor-mode=, the REPL is the
|
|
working stream, and a third buffer holds the errors and the memory sites. The
|
|
separate output buffer is gone — the program's output already rides every reply,
|
|
so both destinations were editor-side routing all along.
|
|
|
|
** DONE A breakpoint is marked from the editor, without editing the buffer
|
|
=C-u= before an evaluation marks a form so the program stops when it runs, for
|
|
three targets: the top-level form, the last expression, and the form point is
|
|
inside. The mark is a separate field the daemon splices after parsing, where
|
|
locations are already attached, rather than text spliced into the source — which
|
|
would shift every line and column after it. It sticks, like Clojure's, until the
|
|
form is evaluated again plainly.
|
|
|
|
** DONE A backtrace and a frame's locals have an editor surface
|
|
Both were daemon ops with nothing calling them. One command shows the backtrace
|
|
with the selected frame's locals.
|
|
|
|
** TODO Hex, binary and an address on a primitive in the inspector
|
|
The last item of the Emacs batch besides the break buffer, and independent of it.
|
|
|
|
** TODO A defclass is not on the definitions list as a type, and a sum's cases are not drawn
|
|
A =defclass= is expanded away before the checker — a dyn map and a shape tag by
|
|
then — so there is no class table to read. A sum's cases are one symbol each and
|
|
the daemon answers with the type's name only. =CFn= also wants adding to the type
|
|
rule.
|
|
|
|
** CANCELLED A flycheck checker, and a structured JSON report
|
|
CLOSED: [2026-09-20]
|
|
The workflow is compile-at-the-end, not live linting. Not built per the
|
|
specification's own branch — the flag is the command and the printed shape is the
|
|
error pattern, so anyone who wants one has the four lines, and the manual carries
|
|
them.
|
|
|
|
** TODO defclass slots take types, checked on write
|
|
=(defclass State [pause bool step bool])= reads as four untyped slots and
|
|
reports a duplicate =bool=. Wanted: the slot list is name/type pairs, as CLOS
|
|
does it. The type is a declaration about the values and not a layout — an
|
|
instance stays a map, so redefinition and lazy migration are unchanged. SBCL
|
|
checks it on write (=src/pcl/slots.lisp:160=, the typecheck before the store),
|
|
which is where the bad value is, so =put= is the site here.
|
|
|
|
Open: what migration does with a stored value that no longer fits a changed
|
|
slot type, and whether an untyped slot stays legal (it should — =dyn= is a type
|
|
and writing nothing should mean it).
|
|
|
|
** TODO println takes up to a second to appear
|
|
Output is drained by =flan--poll= at =flan-poll-interval=, 1.0s
|
|
(=emacs/flan.el:541=). A reply carries whatever was buffered when it was
|
|
composed, so anything the program prints after that waits for the next tick.
|
|
Polling faster costs a request a second for nothing most of the time; the
|
|
daemon pushing on its own connection is the other shape. Decide which.
|
|
|
|
** TODO set writes a class slot; put is for maps
|
|
=put= exists because an absent map key has no location to store into, which is
|
|
why =(get m k)= is refused as a place (=lib/parse.ml:1159=). A class instance is
|
|
not in that situation: its slots are fixed by the =defclass=, so a declared slot
|
|
always exists and =(set (get state :pause) true)= is a field store like
|
|
=(set (.velocity g) 0.0)=. Make =set= take it, and leave =put= to maps, where
|
|
insertion is real. Writing an undeclared slot through =set= is then a refusal
|
|
naming the class.
|
|
|
|
** TODO update: change a place by applying a function to it
|
|
=(set (.velocity g) (inc (.velocity g)))= names the place twice. Clojure's
|
|
=update= would be a macro over the same two steps, for a struct field and a
|
|
class slot alike.
|
|
|
|
Blocked on the double-evaluation question, which =++=, =--= and any
|
|
compound assignment share: =(update (at grid (next-index) c) inc)= evaluates
|
|
=(next-index)= twice, and a place with a side effect is then wrong rather than
|
|
slow. Either places get a general single-evaluation rule — bind every
|
|
subexpression of a place to a temp once, which is what C's compound assignment
|
|
does — or the language says a place must be side-effect free and refuses
|
|
otherwise. The first is the real fix and it is a change to how every place
|
|
lowers, not to one macro.
|
|
|
|
** TODO A session eval reported (CFn [] ()) does not cross into dyn yet
|
|
At =sand.flan:46:20=, the =:pause= in =(when (get state :pause) (return))=,
|
|
where =state= is a =defclass= instance with a =pause= slot. =(CFn [] ())= is
|
|
the prelude's =pause='s own type, so a keyword looks to have resolved to the
|
|
function of that name. Not reproduced: the file type checks, =flan reload= of
|
|
the same form builds, and a minimal =defclass= + =get= + =return= program
|
|
compiles. So it is the session path against an installed program, and what is
|
|
missing is what that daemon had installed at the time.
|
|
|
|
** TODO A digit does not take the restart RET takes
|
|
Pressing =0= left the program stopped; RET on the same line resumed it. Both
|
|
end in =flan-cnr-take=, but the digit path (=flan-cnr-take-number=,
|
|
=flan-cnr.el:559=) scans from =point-min= for the line whose =flan-cnr-index=
|
|
matches and calls =take= inside a =save-excursion=. Not reproduced yet — needs
|
|
a non-raylib program stopped under a test daemon.
|
|
|
|
** TODO Eval in the frame, from the break loop
|
|
An expression is evaluated at a frame boundary, so it sees globals and not the
|
|
stopped frame's locals — which are the values anyone stopped there wants. Wants
|
|
SLIME's eval-in-frame: pick a frame, and the expression is checked and run with
|
|
its slots in scope. The slots are already on the frame and already readable
|
|
(=flan_dev_frame_slot=); what is missing is checking an expression against that
|
|
frame's names and types.
|
|
|
|
** TODO The stack lists prelude frames
|
|
=0: pause <prelude>:151:7= is the breakpoint the author wrote, not a step in
|
|
their program. Prelude frames want hiding by default, with a key to show them.
|
|
|
|
** TODO There is no stepper
|
|
=(pause)= stops and offers restarts, frames, locals and the inspector, but
|
|
nothing advances a form at a time. CIDER instruments a form and steps the
|
|
instrumented copy; the equivalent here is a dev-build-only instrumented
|
|
redefinition, which the cell indirection already makes deliverable. Open:
|
|
whether stepping suspends the frame loop, and what it does to a game's clock.
|
|
|
|
** TODO A NaN cast says "does not fit", which reads as too big
|
|
=runtime/flan_rt.c:1008= covers every out-of-range float with one sentence, so
|
|
=(i32 nan)= reports the =i32= bounds as if the value had overshot them. NaN and
|
|
the infinities convert to no integer at all and want saying so by name. Found
|
|
by filling a struct holding an =f32= with =(filled 0xFF)=, where every bit set
|
|
is NaN.
|
|
|
|
** TODO The break buffer prints fields, not the sentence the runtime wrote
|
|
=ArithError — op 4, lhs -2147483648, rhs 2147483647= where the runtime's own
|
|
sentence is "this value does not fit the integer type it is cast to"
|
|
(=runtime/flan_rt.c:986=). Worse for a dyn trap: =DynType= has no struct at all,
|
|
so the buffer says "no struct is named DynType" while =flan_dyn.c:799= has
|
|
written the operation, both tags and both values to stderr. The sentences exist
|
|
and go to the daemon buffer; the break buffer wants them on the wire.
|
|
=ArithError='s =op= being a bare number is the same gap — it is an enum spelled
|
|
as =i32=.
|
|
|
|
** TODO A backtrace frame names the function, not the call
|
|
=fninfo= (=lib/emit.ml:185=) holds one static =loc=, the =defn='s own, and
|
|
=flan_frame= (=runtime/flan_dev.c:1011=) adds no per-call location — so two
|
|
calls to the same function from one caller are indistinguishable in the stack.
|
|
Wants the caller storing its call site into the frame before the call, which is
|
|
a field and a store on every dev-build call.
|
|
|
|
** TODO The condition buffer cannot jump to the source
|
|
It prints the source line and carets for the stop (=flan-cnr.el:197=) and lists
|
|
frames, but no key opens the file at that line. Wants RET-on-a-frame, or =M-.=,
|
|
and =next-error= over the frame list.
|
|
|
|
** TODO loop's bindings should be sequential, like let's
|
|
=check_loop= (=lib/check.ml:4741=) checks every initialiser before binding any,
|
|
so =(loop [curr-r r next-r (inc curr-r)] ...)= cannot see =curr-r= and the
|
|
refusal reads as an unknown name. Every binding form is sequential — there is
|
|
no =let*= here and there is not going to be one. Check the other binding forms
|
|
for the same gap while fixing it.
|
|
|
|
** TODO C-c C-c reports one error, not every error in the form
|
|
Whole-file paths use =Check.program_all= and report every bad declaration. The
|
|
daemon asks for the sink off (=lib/loc.ml:185=) and gets one exception, so a
|
|
function with three bad expressions takes three round trips. The sink is
|
|
per-phase; making it per-form would need a resync point inside a body.
|
|
|
|
** TODO A session should start before a program compiles
|
|
=flan dev= builds the program first, so a =main= that does not compile gives no
|
|
session. Want SBCL/nREPL order: empty image, then load a file into it. Needs a
|
|
host built with no user program, and a load-file op — the reload machinery
|
|
already builds a file as a module. Open: what =flan-rerun= does with no =main=,
|
|
and a half-loaded file. =C-c C-k= is taken by the inspector.
|
|
|
|
** TODO The daemon buffer is navigable but not coloured
|
|
=*flan*= is all plain text. =compilation-minor-mode= is on (=emacs/flan.el:822=)
|
|
so =next-error= works, but a minor mode installs no font-lock. Open: whether the
|
|
program's output should look different from the compiler's.
|
|
|
|
** TODO compilation-mode steps over the notes
|
|
Nothing sets the skip threshold, so =next-error= walks the errors and steps over
|
|
the notes, which are still parsed, coloured and clickable. Labelling them as
|
|
warnings would make them navigable and is refused: a note is not a warning.
|
|
|
|
* Docs and the repository
|
|
|
|
** DONE The reference page says what the language is
|
|
CLOSED: [2026-09-21]
|
|
A correction pass over =web/index.html=, which had been asserting that there is
|
|
no collector, that ownership is tracked statically, that types are mandatory,
|
|
that =Vec= and =Map= move on assignment, and — under "What it is not" — that
|
|
there is no dynamic typing. A =dyn= section, function values, the =slice=
|
|
arities and the three defining forms were added. Two claims are still wrong and
|
|
are carried into the Diátaxis restructure: a =dyn= is NaN-boxed rather than
|
|
always on the heap, and =bytes-view= write-through is undefined behaviour
|
|
rather than a guaranteed trap.
|
|
|
|
** DONE @page was green over a needle it could never derive
|
|
CLOSED: [2026-09-21]
|
|
=test/dune='s page rule never named =vendor/edn=, which =sand.flan= imports, so
|
|
the headless program could not resolve the package and the quote check reported
|
|
it as drift rather than as a missing dependency. One =glob_files=.
|
|
|
|
** TODO The reference, the tutorial, the how-to and the explanation
|
|
The four Diátaxis kinds, one org file each and nothing outside them:
|
|
=reference.org= absorbing =spec-memory.md=, =spec-conditions.md= and
|
|
=conditions.org=; =howto.org= from =emacs/MANUAL.md=; =explanation.org= from
|
|
=docs/BUILT.md= and the spikes; =tutorial.org=, which does not exist yet.
|
|
Published with =ox-publish= the way =~/Development/ferano.io= does it, from a
|
|
dune rule so a broken docs change fails the build, with examples as
|
|
=:tangle= blocks so the code on the page is the file =@page= compiles.
|
|
=CLAUDE.md= stays markdown because Claude Code loads it by that name.
|
|
|
|
** DONE dune test stays fast and the slow checks stay opt-in
|
|
CLOSED: [2026-09-14]
|
|
The suite is run constantly, including by every lane, so a second added to the
|
|
default run is paid hundreds of times. Rules out attaching the sanitizer, valgrind
|
|
or backend sweeps to the default run.
|
|
|
|
** DONE A lane runs dune test and nothing more
|
|
CLOSED: [2026-09-19]
|
|
The corpus sweeps run once after several lanes have landed, and their fixes are
|
|
dispatched as one batch. A survey walks the whole corpus, so a lane touching a
|
|
handful of programs was paying the full cost to learn nothing about the rest. The
|
|
consequence accepted is that a lane is reviewed on its code.
|
|
|
|
** DONE A check nothing runs rots, so every check has an alias and CI runs them
|
|
The page's example checker and the backend survey each rotted for days or weeks
|
|
because neither ran unless somebody remembered. Both are aliases now, inside the
|
|
umbrella alias CI runs on every push. Both are too slow for the default suite,
|
|
which is the tension that caused it.
|
|
|
|
** DONE Compiler messages are written for a first-timer
|
|
CLOSED: [2026-09-21]
|
|
Two rules, not one. A message says what is wrong and what to write, and stops. And
|
|
it says it to someone holding this compiler and nothing else — no prior spelling,
|
|
no milestone number, no rename framed as a rename. Every suggestion a message
|
|
prints must compile, and an assertion about the compiler's own invariants is
|
|
prefixed and says it is a compiler bug.
|
|
|
|
** DONE The diagnostics pass
|
|
CLOSED: [2026-09-20]
|
|
Graded against the contract: show the code with the caret, say what was
|
|
understood, say what conflicts, name the fix. Two behaviour changes came with it —
|
|
an unannotated two-name parameter vector compiles as two dyn parameters, and a
|
|
=defn= named after a builtin stopped being unreachable.
|
|
|
|
** TODO Three structural diagnostics questions are still the author's
|
|
Printing the stable kind at the end of the first line, the understood-then-
|
|
conflicted clause order as a writing rule, and non-cascading multiple errors. Each
|
|
needs a decision rather than work.
|
|
|
|
** TODO docs/BUILT.md still describes (Handle T) and the pool as built
|
|
It reads as shipped fact for a type the checker has no constructor for and the
|
|
runtime has no code for, including an enumeration primitive in the present tense.
|
|
The pool was removed on 2026-09-18.
|
|
|
|
** TODO The daemon still tells an editor the x86 backend pushes no frames
|
|
It does push them, in every dev build. The reply is rewritten from a claim that
|
|
stopped being true.
|
|
|
|
** TODO web/index.html still claims there is no implicit widening
|
|
Two places. Left alone deliberately — the website has its own rewrite lane.
|
|
|
|
** TODO plan.org's Types section lists a predicate that no longer exists
|
|
It still describes =copyable?= and "a type variable is move-only by default",
|
|
both of which the ownership repeal removed. Left alone deliberately by the
|
|
generics lane as the repeal lane's sentence to retire.
|
|
|
|
** TODO plan.org cites the wrong mechanism for jank's relinking bug
|
|
The real cause was a process-teardown race; jank calls through vars, which are
|
|
already indirection cells. We are safe from the repro because we compile out of
|
|
process, not because of cells. A normative document citing the wrong mechanism
|
|
protects the wrong invariant.
|
|
|
|
** TODO docs/SPIKE-GENERICS.md lists landed work as remaining
|
|
Three items are under "Mechanical" as remaining work and have landed. A dated
|
|
report going stale at a live claim.
|
|
|
|
** TODO Two citations in spec-memory.md do not land where they say
|
|
One is off by a line in a reference clone; the other names an arm that is not the
|
|
one the claim is about. The claim behind the second is true and cited in the wrong
|
|
place, and a third companion citation in the same section is stale too.
|
|
|
|
** TODO The sand hash is quoted as prose in five places besides its assertion
|
|
The assertion carries the current number; the prose copies do not all. Whoever
|
|
re-takes the number has that list.
|
|
|
|
** DONE sand.flan is two programs
|
|
CLOSED: [2026-09-13]
|
|
The windowed one is never executed here; the headless one is the acceptance case
|
|
and runs at both optimisation levels, as a dev build, and on wasm32. The physics is
|
|
untouched on purpose — the three reference implementations disagree there, so
|
|
parity does not name a target.
|
|
|
|
** DONE The brush is embedded, not loaded from a path
|
|
Deliberate, because a path-based load is the one shape the browser cannot have.
|
|
Said so it is not later read as an accident. It holds for the shipped programs
|
|
rather than for the repository — a test fixture still loads an image from a path.
|
|
|
|
** DONE A package under test/programs needs a glob line in four places
|
|
CLOSED: [2026-09-25]
|
|
One =corpus= alias in =test/dune= holds =(source_tree programs)= and the
|
|
workspace files the corpus imports; the tests, =test_web=, =@sanitize=,
|
|
=@valgrind=, =@x86= and =@js= depend on it, and =@page= takes the same
|
|
=source_tree=. A package added under =programs/= needs no line anywhere. A build
|
|
that raises is a =FAIL= line in every sweep: the acceptance pool, the two
|
|
sanitizer binaries, and a =MISSING= count that fails both survey scripts.
|
|
|
|
** DONE The macro programs are not in the sanitizer sweep
|
|
CLOSED: [2026-09-25]
|
|
The five that run — =macros=, =macro-params=, =macro-unless=, =pkg-macro=,
|
|
=prelude-macros= — are rows in =test_sanitize.ml='s list; the refused ones stay
|
|
out with the other negative cases. The list stays explicit rather than a glob.
|
|
Not yet run under the sweep: that waits for the batched =@sanitize=.
|
|
|
|
** TODO The mutation pass has not been re-run
|
|
Sixty mutations, nineteen of which left the whole suite green; all nineteen are
|
|
closed, each re-planted and watched fail against the new test. What is open is
|
|
that the pass has not been run again, so nineteen is the old number.
|
|
|
|
** TODO bin/main.ml spells the compile pipeline out by hand
|
|
Two places each do a load, a check and a link feeding the builder, where the tests
|
|
go through one helper. The CLI loads through its own path and checks with a
|
|
different entry point, so this is not a matter of calling the test module from the
|
|
binary — closing it means the pipeline moving into the library.
|
|
|
|
** TODO Build.executable returns only its output path
|
|
The daemon recovers the host's IR file by recomputing the working directory. One
|
|
line away: return the path rather than recomputing it.
|
|
|
|
** TODO The 2MB OFL font is not vendored
|
|
One example wants a font that is OFL and redistributable; it says on screen when
|
|
it is missing and runs either way. A call about the repository, not about the
|
|
port.
|
|
|
|
** DONE old-ocaml/ and the built executables are untracked on purpose
|
|
The executables are what a build drops beside their sources. =old-ocaml/= is the
|
|
pre-rewrite frontend, kept as reference and excluded from the build; its contents
|
|
are also in git history.
|
|
|
|
** DONE A lane that stops mid-repair says which pieces it ran
|
|
A handoff once said one of five hand-offs was dropping a value; four of the five
|
|
were never written at all, and the first step it recommended could not have worked
|
|
because there was no comparison to print.
|
|
|
|
** CANCELLED Examining branch emacs-batch-a63bd
|
|
The branch no longer exists, so there is nothing left to look at. git log is the
|
|
record.
|