2256 lines
121 KiB
Org Mode
2256 lines
121 KiB
Org Mode
#+TITLE: Flan
|
|
#+TODO: TODO NEXT 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.
|
|
|
|
** DONE gensym's counter restarts in a second module
|
|
CLOSED: [2026-09-25]
|
|
The counter is C data in the runtime (=flan_gensym_n=), and =lib/macro.ml= writes
|
|
the compiler's own count into the module before every macro call and reads it
|
|
back after. It counts across every module a compiler process loads — each round,
|
|
the program's module, and every expansion in a session. Rules out a counter per
|
|
module, seeded or not.
|
|
|
|
** DONE A quasiquote inside a quasiquote nests
|
|
CLOSED: [2026-09-25]
|
|
=Expand.quote= counts depth the way SBCL's =*backquote-depth*= does: an unquote
|
|
belongs to the innermost quasiquote and =~~x= reaches out two levels; deeper
|
|
forms come back as data. A macro's answer is desugared again, and a top-level
|
|
expansion that defines a macro re-runs the expander, in a build and in a session.
|
|
=~~@x= splices an unquote per element, SBCL's =unquote*=. There is
|
|
no =,',x=, since =quote= takes a symbol, and a macro defined by an expansion is
|
|
not exported from a package. docs/BUILT.md, "Quasiquote runs before the walk".
|
|
|
|
** DONE A form the prelude relies on is built in; a form only programs use is a macro
|
|
CLOSED: [2026-09-25]
|
|
=cond=, =when= and =dotimes= are special forms in parse.ml; =inc=, =++=, =into=,
|
|
=unless=, =until= and =comment= are prelude macros.
|
|
|
|
** 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.
|
|
|
|
** DONE There is no literal for an infinity or a NaN
|
|
CLOSED: [2026-09-25]
|
|
=f64-inf=, =f64-nan=, =f32-inf= and =f32-nan= are names the checker supplies
|
|
(=Check.special_float=), reached only after every local, global and function has
|
|
missed, so a program's own binding of one wins. Negative infinity is
|
|
=(- f64-inf)=. Rules out Clojure's =##Inf= reader literal.
|
|
|
|
** DONE A u64 constant above 2^63 cannot be written in decimal
|
|
CLOSED: [2026-09-25]
|
|
An integer written at or above 2^63 — a decimal up to 2^64 - 1, or hex with the
|
|
top bit set — reads as =Form.UInt=, its pattern and its spelling. It is accepted
|
|
where the type is =u64=, a =(u64 ...)= cast included, and refused everywhere else
|
|
in the spelling it was written in. Hex with the top bit set was accepted as a
|
|
negative at any integer type before this; it is refused now too. A cast's
|
|
integer literal that does not fit
|
|
=i32= is checked at the cast's type; one that fits keeps the =i32= default, so
|
|
=(u32 -1)= still means what it did. A wide literal passed to a macro as an
|
|
argument comes back wide: it crosses as an =Int= with a token in the unused
|
|
second payload word (=Expand.wides=). Rules out a second integer case in the
|
|
prelude's =Form=.
|
|
|
|
** DONE A wide literal's follow-ups: an enum member, a dyn want, a macro
|
|
CLOSED: [2026-09-25]
|
|
=(defenum E [A 0xFFFFFFFFFFFFFFFF])= gets the enum range refusal in the spelling
|
|
written. A wide literal where a =dyn= is wanted names =(u64 ...)= and says the dyn
|
|
holds it as the i64 with the same bits. A wide literal passed through a macro is
|
|
refused or accepted exactly as it would be unexpanded.
|
|
|
|
** TODO A wide literal written inside a quasiquote comes back as an i64
|
|
=(defmacro w [] `(+ 1 0xFFFFFFFFFFFFFFFF))= expands to =(+ 1 -1)= and prints 0:
|
|
=Expand.quote= builds =(Form.Int {.i ...})= from the pattern, and a Form built in
|
|
Flan has no way to carry the token an argument crosses with. At a =u64= want the
|
|
pattern is the right value, so a refusal would break the one reading that works.
|
|
|
|
** 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.
|
|
|
|
** 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.
|
|
|
|
** DONE match over enums
|
|
CLOSED: [2026-09-25]
|
|
=Ast.Pkw= is the keyword pattern; =Check.check_match= resolves it against the
|
|
scrutinee's enum and lowers the match to one temporary and a chain of =if (= t
|
|
:member)=, the last arm untested. Exhaustiveness is the data type's rule: refused,
|
|
not defaulted. Rules out a new IR node for it, and a keyword arm over an Option or
|
|
a data type.
|
|
|
|
** 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.
|
|
|
|
** CANCELLED (Result T E) and try
|
|
CLOSED: [2026-09-25]
|
|
Conditions and restarts are the error mechanism, and nothing in the game's code
|
|
wants a second one.
|
|
|
|
** 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 A return runs its defers before it computes its value
|
|
CLOSED: [2026-09-25]
|
|
=(return v)= computes =v= into a slot, then runs the defers registered so far,
|
|
then returns the slot — the order falling off the end already had, and Odin's, Go's
|
|
and Zig's. One lowering in =Check=, so every backend has it. A value of type
|
|
=Never= is still returned directly, since nothing after it runs. The LLVM
|
|
emitter emits nothing after a terminator (=Emit.value= answers =poison= once the
|
|
block is closed); a bounds check in dead code used to reopen the block and
|
|
reference an operand it never wrote.
|
|
|
|
** 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.
|
|
|
|
** DONE Conditions get a parent link, not class inheritance
|
|
CLOSED: [2026-09-25]
|
|
A parent has exactly Error's fields; a handler matched through the link gets a view (name, message with the values), never the child's fields. Rules out parents with fields of their own.
|
|
|
|
** CANCELLED Can a condition be a class?
|
|
CLOSED: [2026-09-25]
|
|
A class condition allocates on the failure path. The parent link above gives the hierarchy without it.
|
|
|
|
** 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.
|
|
|
|
** 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.
|
|
|
|
** DONE A string cannot be returned from C
|
|
CLOSED: [2026-09-25]
|
|
A =declare-c= may return =string=: the text is copied into the context allocator
|
|
at the boundary, through =bytes=, and lives until that allocator's =free-all=. The
|
|
importer maps a returned =const char *= to =string= and still refuses a plain
|
|
=char *=, which the caller owns and releases through the library. =TextFormat=
|
|
stays unbound, being variadic. docs/BUILT.md, "A string returned from C is
|
|
copied into the context allocator".
|
|
|
|
** DONE Model, Mesh and FilePathList have a defstruct
|
|
CLOSED: [2026-09-25]
|
|
=Model=, =Mesh= and =Matrix= are described and the generated half widened over
|
|
them; =Model='s material, bone and pose pointers are =(Ptr u8)= until =Material=
|
|
and =BoneInfo= can be, both holding a fixed array. =FilePathList= crosses by the
|
|
returned-string rule: =dropped-files=, =directory-files= and
|
|
=directory-files-ex= copy the paths and unload raylib's list before returning.
|
|
docs/BUILT.md, "=Model=, =Mesh=, =Matrix= and =FilePathList=".
|
|
|
|
** WAIT A callback is the other direction of the FFI
|
|
Blocked on a program that needs one. =SetTraceLogCallback= and the audio stream
|
|
processors take a C function pointer, and the shim refuses a function type by
|
|
name until then.
|
|
|
|
** 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.
|
|
|
|
** DONE rlgl's matrix stack is bound
|
|
CLOSED: [2026-09-25]
|
|
=vendor/rlgl= is its own package over =rlgl-5.5.h=, binding the matrix stack by
|
|
hand and generating nothing else. A package and not more of =vendor/raylib=
|
|
because the header check is per package. =core_2d_camera_mouse_zoom= is ported
|
|
and builds. docs/BUILT.md, "rlgl is its own package".
|
|
|
|
** TODO examples/core-input-virtual-controls.flan does not build
|
|
It defines =abs-f32=, which the prelude defines too, and a second definition is
|
|
refused. Nothing builds the examples wholesale, so nothing noticed.
|
|
|
|
** 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.
|
|
|
|
** DONE Escaping closures, allocated on the GC side
|
|
CLOSED: [2026-09-25]
|
|
Only a capturing =fn= that may outlive its frame gets a collector environment; one
|
|
only called or passed down keeps its stack environment, as every handler does.
|
|
Capture stays by value, and a =Map= of function values is refused. Rules out a
|
|
tag bit on the environment word and a heap environment for every closure.
|
|
|
|
** WAIT CFn and C's calling convention
|
|
Decided 2026-09-25: waits with C callbacks, until a program needs one.
|
|
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.
|
|
|
|
** DONE update-instance-for-redefined-class, the user hook
|
|
CLOSED: [2026-09-25]
|
|
Taking =migrate-by-name= keeps the name-matched instance, not SBCL's obsolete one,
|
|
and retries nothing; a transfer from the method to a restart below it traps.
|
|
|
|
** 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.
|
|
|
|
** DONE A package cannot mark a name private
|
|
CLOSED: [2026-09-25]
|
|
=defn-= declares a function private to its module; it is a =defn= otherwise.
|
|
The module is the import boundary: every file of a directory package, or the
|
|
one file of a package imported by naming the file. A use from outside — a call
|
|
or the name as a value — is refused in =Check=, so it holds under C-c C-c too.
|
|
Code the package's own macro wrote counts as inside (SBCL's rule, not
|
|
Clojure's); what the importer wrote, passed through or from its own macro, does
|
|
not. Functions only. The edn and json internals are now =defn-=;
|
|
=rl/get-color-raw= no longer exists. =docs/BUILT.md= has the placement.
|
|
|
|
** WAIT A private scoped to one file of a directory package
|
|
Deferred until a need appears: Odin's =@(private="file")=, a function visible
|
|
to its own file only when the package is a directory. =defn-= covers the
|
|
package; a one-file package already gets file scope because the file is the
|
|
module.
|
|
|
|
** 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.
|
|
|
|
** NEXT An indented surface beside s-expressions
|
|
Decided 2026-09-25 as a test drive: a second reader, chosen by file extension,
|
|
producing the same forms. =spec-syntax.md= holds the decisions, the proposals
|
|
awaiting confirmation, and the build order. 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.
|
|
|
|
** DONE A declared name may carry the $ sigil
|
|
CLOSED: [2026-09-25]
|
|
A name that starts with =$= is refused where it is declared — every top-level
|
|
form, a struct or union field, an enum member, a data case, a class slot, and a
|
|
=let=, =:keys=, =&=, =loop=, =dotimes=, =match=, =fn=, handler clause, macro or
|
|
generic binding — saying =$= marks a
|
|
type variable and naming the bare spelling. A =defn= parameter was already
|
|
refused, as a type in a name slot.
|
|
|
|
** NEXT A container parameter the function grows is warned at
|
|
Decided 2026-09-25: Odin's behaviour stays — a Vec or Map passed by value is a
|
|
copy of its header, so growth inside the callee does not reach the caller. A
|
|
parameter the function grows (push, put, reserve, anything that can reallocate)
|
|
gets a warning at the parameter suggesting (Ptr ...).
|
|
|
|
** CANCELLED not= as a spelling of !=
|
|
CLOSED: [2026-09-25]
|
|
One spelling for one operation; != stays, and not= is refused with a suggestion
|
|
of !=.
|
|
|
|
* 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.
|
|
|
|
** NEXT There is now no generic enum to integer conversion
|
|
Decided 2026-09-25: build =enum?= as described.
|
|
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.
|
|
|
|
** DONE The Ptr and union arms of the fill boundary are relaxable
|
|
CLOSED: [2026-09-25]
|
|
A =Ptr= may be byte-filled, and an untagged union is filled over its whole
|
|
size when every member may be, its members walked as a struct's fields are; a
|
|
union with a =dyn= member is refused naming the =dyn=. Everything else the rule
|
|
refused it still refuses.
|
|
|
|
** DONE A compound constant expression at a bounded type variable
|
|
CLOSED: [2026-09-25]
|
|
Integer arithmetic over literals alone (=Check.literal_arith=) is folded to the
|
|
literal it computes wherever a type variable is wanted, so =(+ x (+ 1 2))= is
|
|
admitted exactly where =(+ x 3)= is. A defconst's name does not fold, since it
|
|
has a type of its own. The instantiation checks the form unfolded, at its
|
|
concrete type.
|
|
|
|
** 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.
|
|
|
|
** NEXT Generic types
|
|
Decided 2026-09-25: the freeze is lifted for this; build both type and length parameters.
|
|
=(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.
|
|
|
|
** WAIT A value predicate over a length parameter
|
|
Decided 2026-09-25: waits until a program wants one.
|
|
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.
|
|
|
|
** DONE A program is one compilation, so a generic's body is always visible
|
|
CLOSED: [2026-09-25]
|
|
Odin's and Zig's model: packages are never compiled separately. The cost is build
|
|
time proportional to the whole program and no binary-only packages. If separate
|
|
compilation is ever wanted, Rust's answer is the one to take — a compiled package
|
|
carries its generics' checked bodies and the user instantiates them.
|
|
|
|
** 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.
|
|
|
|
** NEXT A bool arm and a dyn arm joining as dyn
|
|
Decided 2026-09-25: they join as =dyn=, the =bool= boxed — Clojure's rule, so =(or false (box "s"))= answers ="s"=.
|
|
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.
|
|
|
|
** NEXT A truthiness failure re-runs the whole failing subtree
|
|
Decided 2026-09-25: fix it without changing any message — the retry reuses what the first pass settled for each subtree (memoised by node), so nested =not= is linear. Test with a deep nest that must fail fast and with the existing message tests unchanged.
|
|
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.
|
|
|
|
** DONE and's last operand gets a misdirected caret
|
|
CLOSED: [2026-09-25]
|
|
Already fixed by 3672da2, which blames the arm that is not a compiler temp; the
|
|
caret is on the last operand and =test/test_flan.ml= asserts its column. Rules
|
|
out relabelling the else arm, a bool sentinel, and inverting the condition.
|
|
|
|
** NEXT Signature pairing's cold-rebuild edge
|
|
Decided 2026-09-25: the type takes precedence, as today. The warning is at the parameter site: where a name in a parameter vector is read as a program-declared type but could also have been read as a parameter name, the parameter vector gets a warning naming the type and where it is declared.
|
|
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.
|
|
|
|
** NEXT Catching a use-after-release statically
|
|
Decided 2026-09-25: a study, not a build — how arena memory escapes in real Flan code, and whether a sound lexical check would catch most of it. The result goes in docs/BUILT.md; nothing is built on it without the author.
|
|
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.
|
|
|
|
** DONE (vec-new [u8]) is refused
|
|
CLOSED: [2026-09-25]
|
|
The type positions of =vec-new= and =map-new= take a type expression: brackets, or
|
|
a parenthesised =Ptr=, =Option=, =Vec=, =Map=, =Fn= or =CFn=. The arguments stay
|
|
ordinary expressions and the builtin reads the type back out of one
|
|
(=Check.type_of_expr=), so a program's own =vec-new= still gets values; only a
|
|
type an expression cannot hold, such as =(Fn [i32] ())=, is parsed as
|
|
=Ast.TypeArg=. Rules out a type expression anywhere else in expression position.
|
|
|
|
** DONE An array literal cannot say it is [f32]
|
|
CLOSED: [2026-09-25]
|
|
=(the [f32] [1 2.5])= names the element type; with nothing naming one, a literal
|
|
element takes the other elements' type. Rules out a =1.0f= suffix for now.
|
|
|
|
** DONE A let binding takes no type annotation
|
|
CLOSED: [2026-09-25]
|
|
=(the T expr)= gives any expression its want and =let= stays a flat list of
|
|
pairs. Rules out a type slot in =let=.
|
|
|
|
** DONE A read-only slice type
|
|
CLOSED: [2026-09-25]
|
|
=[const T]= and =(Ptr const T)=; a =[T]= or =(Ptr T)= converts at the top of a type or under another const one, never inside a writable one. The const is shallow: an element of a =[const [u8]]= and a Vec's buffer are writable. The address of read-only storage, a string's byte included, is a =(Ptr const T)=, and a C =const T *= parameter takes one.
|
|
|
|
** 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.
|
|
|
|
** CANCELLED An owning temporary as into's source leaks
|
|
CLOSED: [2026-09-25]
|
|
A leak is defined here, and the allocator's region reclaims the temporary the
|
|
way it reclaims every other one. Closing it needed =drop=, which is cancelled.
|
|
|
|
** 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
|
|
A negative literal fits no unsigned type, u64 included; =(u64 -1)= is how the
|
|
pattern is written, and a constant folds it. Rules out a negative decimal as a
|
|
u64's bit pattern.
|
|
|
|
** 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.
|
|
|
|
** NEXT CFn in a struct or a fixed array
|
|
Decided 2026-09-25: allowed. A call through a null =CFn= is a named runtime condition on both backends, and parks in a dev build.
|
|
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.
|
|
|
|
** TODO An error in a called generic's body is reported twice
|
|
=(defn g [x $t] u64 (nosuch x))= called once from =main= prints "unknown
|
|
function nosuch" twice at the same place and counts 2 errors — once from the
|
|
abstract pass and once from the instantiation.
|
|
|
|
** TODO A type variable is printed without its $
|
|
=Types.to_string= prints =Var t= as =t=, so a refusal reads "selection-sort
|
|
expects [t] here, found [3 i32]" where the source wrote =[$t]=.
|
|
|
|
** DONE Two refusals suggested something that does not compile
|
|
CLOSED: [2026-09-25]
|
|
=vec-new= and =map-new= with no type no longer say "or give the binding a type";
|
|
they name the type arguments alone, and =(the T expr)= joins them when it lands.
|
|
An unknown call whose near miss is a value — =(context-allocator)= against
|
|
=context/allocator=, or a global — says the name is a value written without
|
|
parentheses, and names no call at all when the call had arguments.
|
|
|
|
* 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.
|
|
|
|
** NEXT Nothing pins the LLVM side at -O0 when the two backends are compared
|
|
Decided 2026-09-25: the x86 parity survey compares against LLVM at -O0 only; -O2 is never its target. The acceptance suite's paired -O2/-O0 rows stay, being the check for undefined behaviour in emitted IR, which is a different question.
|
|
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.
|
|
|
|
** DONE Reading (uninit) before writing it is undefined behaviour
|
|
CLOSED: [2026-09-25]
|
|
Reading an =(uninit)= value before writing it is undefined behaviour, and the backends may differ on it. An exhausted match stays =ud2= on x86.
|
|
|
|
** 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.
|
|
|
|
** DONE A global initialiser builds an aggregate through the same temporary on x86
|
|
CLOSED: [2026-09-25]
|
|
Already true when the entry was written. Every computed initialiser, release
|
|
build included, reaches =emit_globals_init= as =Emit.startup_plan='s
|
|
=(set g init)= and so goes through =assign=. The only initialisers lowered
|
|
straight into their symbol are =Tast.const_init= ones, which neither transfer
|
|
nor read anything. Nothing to change.
|
|
|
|
** DONE A u64 converted to f64 is signed on x86
|
|
CLOSED: [2026-09-25]
|
|
=--x86= converts =u64= to and from =f64= and =f32= the way LLVM's =uitofp= and
|
|
=fptoui= do: the halve-and-double sequence one way, subtract 2^63 and set the top
|
|
bit the other, ties rounding to even. =test/programs/u64-float.flan= runs on both
|
|
backends. A cast that misses =u64='s range reports it as =[0 18446744073709551615]=.
|
|
|
|
** DONE An aggregate built in place never reads its own destination
|
|
CLOSED: [2026-09-25]
|
|
=assign= builds in place only when the right-hand side settles *and* reads no
|
|
storage the destination lies in; otherwise it goes through the temporary. Two
|
|
predicates, not one: =settles= is about leaving part-way, =reads= about a value
|
|
reading itself, and a read through a pointer counts as reading everything.
|
|
=test/programs/self-read.flan= runs on both backends.
|
|
|
|
** DONE The aggregate temporary is an unrooted buffer while it is filled
|
|
CLOSED: [2026-09-25]
|
|
The buffer stays unrooted. Every dyn word written into it while a sibling field
|
|
may allocate is also in a root slot of its own, pinned as that field was
|
|
computed, so the collector sees what has been built so far without a root for
|
|
the buffer.
|
|
|
|
** 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.
|
|
The same holds for a closure environment a reload module allocated: the module
|
|
builds on both backends, and nothing yet collects while one is live.
|
|
For the next sweep rather than for a lane.
|
|
|
|
** DONE A string literal crosses to C uncopied
|
|
CLOSED: [2026-09-25]
|
|
Both backends write a NUL after every literal and a declare-c passes a literal argument
|
|
uncopied. Rules out a NUL guarantee on any other string: a slice is pointer and length.
|
|
|
|
** DONE Frame descriptions are gated on --debug
|
|
CLOSED: [2026-09-25]
|
|
The x86 backend emits its =.cfi= directives in every build, redefinition modules
|
|
included, so an unwinder never has to guess at a Flan frame. Cost: about 40 bytes
|
|
of =.eh_frame= and =.eh_frame_hdr= per function, 2 KB on =json.flan='s 210 KB
|
|
binary.
|
|
|
|
** WAIT A !DILexicalBlock per Let
|
|
Decided 2026-09-25: waits until Flan is debugged in gdb or lldb; the break buffer, which reads the shadow stack, already answers correctly.
|
|
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.
|
|
|
|
** CANCELLED UBSan sees no Flan code, and no flag changes that
|
|
CLOSED: [2026-09-25]
|
|
The language defines the cases UBSan would catch itself: a computed shift count is masked, and a float-to-int cast out of range or of a NaN signals =ArithError=. Flan code produces no misaligned access.
|
|
|
|
** 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.
|
|
|
|
** DONE The IR and the disassembly are not annotated with the source
|
|
CLOSED: [2026-09-25]
|
|
Every Flan form heads the code it produced: a comment in the =.ll= (=Emit.annot=),
|
|
and in the x86 listing a per-function map from byte offsets to forms, written as
|
|
comments after =.size= (=X86.srcmap=). =flan emit= and every dev build annotate;
|
|
=--no-annotate= turns it off, and the objects are identical either way (tested).
|
|
The daemon's =C-c C-a= places the forms from what it kept of each build: the x86
|
|
map, or on LLVM the line table read with =objdump -l= plus the =.ll='s headings,
|
|
which exists only under =--debug=; an =-O2= LLVM session says so in =:note=. The
|
|
lowering buffer annotates all four sections, the two =llc= ones from a =--debug=
|
|
copy of the IR. Rules out writing a disassembler, and reading the source off disk
|
|
at disassembly time.
|
|
|
|
** DONE A temporary allocator, wiped each frame
|
|
CLOSED: [2026-09-25]
|
|
=i64->bytes= and =f64->bytes= allocate from context/temp, which grows rather than failing. A
|
|
dev build wipes it at a top-level agent poll; an expression run at a stop gets a scratch temp arena.
|
|
|
|
* 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=.
|
|
|
|
** DONE A Map's bounds check and the stale-container failure still die
|
|
CLOSED: [2026-09-25]
|
|
A Map has no bounds check: =get= and =map-remove= answer =None= for an absent key
|
|
and nothing on the map path indexes by a number, so there was nothing to convert.
|
|
The stale-container failure keeps dying — the region was released and there is no
|
|
frame to go back to that would not read freed memory. Rules out a signalled
|
|
condition on the stale path.
|
|
|
|
** 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.
|
|
|
|
** DONE A stale slice reads poison in a dev build
|
|
CLOSED: [2026-09-25]
|
|
A dev build fills a Vec's old buffer with 0xDEADBEEF when a push moves it; nothing traps.
|
|
Rules out a dev-only word on every slice, which would change the slice layout per build.
|
|
|
|
** DONE The allocator's budget is not in the spec
|
|
CLOSED: [2026-09-25]
|
|
spec-memory.md has a Budget subsection under Allocators, as built: a ceiling on
|
|
live bytes, 0 for none, that a =retry= handler raises. The failure bullet says
|
|
"raises the allocator's budget" where it said "grows the arena", since no arena
|
|
grows. A growable arena is not ruled out; nothing here asks for one.
|
|
|
|
** DONE The Vec header is not the size the spec fixes
|
|
CLOSED: [2026-09-25]
|
|
Five words in every build, the epoch included, so a release build still traps on
|
|
a container whose region was released. spec-memory.md, "Every build detects a
|
|
released region", now says so. Rules out a four-word release layout.
|
|
|
|
** DONE arena-destroy under a live view reads freed memory
|
|
CLOSED: [2026-09-25]
|
|
No ordering of the frees fixes it: the container holds a pointer to the header.
|
|
=arena-destroy= now frees the pages and the arena record and retires the
|
|
allocator header — epoch bumped, procedure trapping as =DestroyedAllocator=,
|
|
never freed — so the stale check reads live memory on every side that makes it.
|
|
The next =arena-new= takes a retired header back, epoch kept, so a loop of them
|
|
stays flat and a container made before the destroy still traps.
|
|
Rules out freeing the header while any container may hold it. See
|
|
docs/BUILT.md, "Three amendments to a frozen spec".
|
|
|
|
** 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. Superseded by the Swiss table,
|
|
which removes by tombstone and moves nothing.
|
|
|
|
** DONE The Map is slower than CPython's dict at a million entries
|
|
CLOSED: [2026-09-25]
|
|
The Map is a Swiss table: one control byte a slot, key and value side by side,
|
|
groups of eight probed as one 64-bit word, seven-eighths load, removal by
|
|
tombstone with a same-capacity rebuild to sweep them. Header, entry points and
|
|
iteration contract unchanged. Rules out Robin Hood, the backward shift and
|
|
separate key and value runs; SSE2 groups are not built. See docs/BUILT.md, "The
|
|
Map is a Swiss table".
|
|
|
|
** 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 dyn read out of a place is rooted while a sibling operand runs
|
|
CLOSED: [2026-09-25]
|
|
An operand holding a dyn word — a call's, runtime call's or primitive's
|
|
argument, a struct or array literal's element, the temporary array an =at= or
|
|
=slice= indexes — is spilled into a pushed root slot as soon as it is computed,
|
|
whenever another operand in the same list does not settle. Same
|
|
explicit-slot root stack on both backends and on wasm32; rules out anything that
|
|
scans the native stack or registers. See docs/BUILT.md, "Operands held beside a
|
|
sibling".
|
|
|
|
** DONE A redefined defclass migrates its instances lazily
|
|
CLOSED: [2026-09-20]
|
|
CLHS 4.3.6. 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.
|
|
|
|
** WAIT A class registry keeps one slot list per class, not one per layout version
|
|
Decided 2026-09-25: waits for a case name-matching migration to the current list gets wrong.
|
|
"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.
|
|
|
|
** DONE The seqlock's losing race has no test
|
|
CLOSED: [2026-09-25]
|
|
=flan_dev_result_read_hook= runs between the copy and the second counter read,
|
|
and dev_limits.c's =race= mode writes from inside that window: once (the read
|
|
retries and returns the new value), every attempt (it gives up with nothing), and
|
|
a write left open (it never copies). A hook rather than a second thread, so the
|
|
interleaving is the same on every run; rules out a timing-based stress test here.
|
|
|
|
** DONE The snapshot generation's racing stale claim has no test
|
|
CLOSED: [2026-09-25]
|
|
=flan_agent_break_poll_hook= runs on the stopped thread where a thunk from the
|
|
poll would, and test/agent_hooks.c uses it to choose at an outer break and then
|
|
nest a break on top before the outer one looks. The inner break turns past the
|
|
choice and resumes only on its own. Rules out a sleep-timed socket test for this.
|
|
|
|
** TODO A choice made at an outer break is lost to a nested one
|
|
=chosen_index=, =chosen_gen= and =chosen_ready= are one slot. A choice validated
|
|
against an outer break and met by a nested one survives the nested break's
|
|
turns, but the nested break can only resume on a choice of its own, which
|
|
overwrites it — so the outer break stays stopped after the listener answered ok
|
|
for it. test/agent_hooks.c's =stale= mode pins this as it is. A slot per
|
|
snapshot is the likely fix.
|
|
|
|
** DONE SNAP_MAX and SNAP_NAMES are read rather than tested
|
|
CLOSED: [2026-09-25]
|
|
test/agent_hooks.c drives both through programs/agent-hooks.flan, which recurses
|
|
with one restart per level: 71 restarts list 64, and 31 with 200-byte names list
|
|
20, each whole, with the terminal counting the rest and a take by index landing
|
|
in the frame it names. The slot kept back for =abandon-evaluation= under
|
|
truncation is still not driven: it needs a thunk in progress.
|
|
|
|
** 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.
|
|
|
|
** NEXT The leak question across the corpus
|
|
Decided 2026-09-25: one pass over the whole corpus with LeakSanitizer and memcheck's leak check on. Memory an allocator holds by design is set aside; memory nothing owns is a leak and is fixed. The sweeps' default stays leak-checking off.
|
|
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.
|
|
|
|
** DONE trap_oom has no site
|
|
CLOSED: [2026-09-25]
|
|
=flan_dyn_at=, =flan_dyn_set_at= and =flan_dyn_push= take the call's site as
|
|
ptr+len, like the arithmetic, and every trap they reach prints it — type, range,
|
|
a view's tag check, and push's growth failing. =trap_oom= takes a site and only
|
|
push gives one: its other callers are the collector's own allocations, which
|
|
have no line to name. A stale view's check prints the site when =at= or
|
|
=set-at= reaches it; reached from =length=, printing or equality, it has none.
|
|
|
|
** DONE A formatted number outlives its frame
|
|
CLOSED: [2026-09-25]
|
|
=i64->bytes= and =f64->bytes= copy their text into the temp allocator; the prelude and
|
|
the printer keep the frame slot. Rules out refusing the escape, which needs flow tracking.
|
|
|
|
** 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.
|
|
|
|
** DONE runtime/flan_dyn_stub.c is dead
|
|
CLOSED: [2026-09-25]
|
|
Deleted, in a sweep for dead code across the repository in which each removal
|
|
was first shown unused. flan_dyn.c is the one implementation of the flan_dyn.h
|
|
ABI; a stand-in beside it is not to come back.
|
|
|
|
** DONE A destroyed arena always traps, even after its record is reused
|
|
CLOSED: [2026-09-25]
|
|
An Allocator value is the record and the incarnation it was made for, compared on every use.
|
|
Rules out static tracking of destroy, which is move semantics. docs/BUILT.md has the cost.
|
|
|
|
** DONE A mixed array literal with no want is a dyn vector
|
|
CLOSED: [2026-09-25]
|
|
Elements that agree, numbers meeting at the wider, are typed; elements that mix
|
|
are a dyn vector, except numbers with no common type, which are refused. Rules
|
|
out the first element typing the rest.
|
|
|
|
* Dev loop
|
|
|
|
** TODO A prelude function shadowed live is reached by the prelude's own calls
|
|
A defn of a prelude function's name sent to a running =flan dev= installs into the
|
|
host's cell for that name, so the prelude's calls compiled into the host follow it;
|
|
a rebuild gives them the prelude's again, as =Check.shadow_prelude= intends.
|
|
|
|
** 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.
|
|
|
|
** 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.
|
|
|
|
** 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.
|
|
|
|
** DONE The daemon leaves its temp directory behind
|
|
CLOSED: [2026-09-25]
|
|
A session that ends cleanly — =close=, or the editor gone past the grace —
|
|
removes =flan-dev-<pid>= (program, modules, agent socket) and its own
|
|
=Build.workdir=. Kept on a crash: the accept loop raising, or a two-process
|
|
child killed by a signal. Only the two paths named for this pid are touched;
|
|
nothing sweeps other sessions' directories.
|
|
|
|
** 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 in a release build. =flan dev= links the
|
|
agent's C into every program it builds whether or not the source imports it; a
|
|
release build links it only when the program calls into it.
|
|
|
|
** DONE The daemon's "has not called (agent/start ...)" note is unreachable
|
|
CLOSED: [2026-09-25]
|
|
Retired, with the matching arm of an evaluation's timeout, because it named the
|
|
wrong cause: with the agent linked, its constructor binds the socket before
|
|
=main=, and the one way left to be unbound is a socket path over 107 bytes, which
|
|
=flan dev= refuses at start, naming TMPDIR. A program with no agent linked is
|
|
told it has none and how to add one. No reply says =(agent/start ...)= has not
|
|
been called.
|
|
|
|
** 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 daemon no longer rewrites the agent's reply to
|
|
say otherwise.
|
|
|
|
** 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.
|
|
|
|
** NEXT The type identity of a local is not qualified
|
|
Decided 2026-09-25: a local's type prints package-qualified in the break buffer and the inspector, as a field's and a condition's already do.
|
|
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.
|
|
|
|
** NEXT The render-thunk-per-inspection design
|
|
Decided 2026-09-25: the inspector reads a value through the type layouts the compiler records, with no compile per inspection, which lets it hold a value.
|
|
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.
|
|
|
|
** DONE The watch table stays pushed
|
|
CLOSED: [2026-09-25]
|
|
The watch table stays pushed, and shares the push channel program output moves to.
|
|
|
|
** DONE A watch over a struct or a slice
|
|
CLOSED: [2026-09-25]
|
|
=(watch "name" v)= is a checker arm beside =print=, sharing its render context
|
|
with the emitter aimed at the watch slot, so any value watches as it prints. The
|
|
value is evaluated once, before the table is asked whether it is armed, so the
|
|
program behaves the same with or without a watch buffer open. Outside a dev
|
|
build, and always in the JS dialect, the backend drops everything but the
|
|
value's evaluation, so a release build makes no call. The =declare-c= scalar
|
|
entry points stay. See docs/BUILT.md, "The scalar entry points, and the
|
|
form for everything else".
|
|
|
|
** 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.
|
|
|
|
** DONE Signature generations and stale-caller warnings
|
|
CLOSED: [2026-09-25]
|
|
A changed signature installs. A dev cell is three words (body, signature word,
|
|
signature text), and every call through it, and every function value taken from
|
|
it, compares the word against the one the site was compiled for; a mismatch
|
|
signals =StaleCall= and the call is not made. The reply's =:stale= lists every
|
|
compiled caller by file:line, and recompiling one clears it. The word is a hash
|
|
of the signature, so changing it back makes old callers current again. Rules
|
|
out versioned bodies and trampolines, and redirecting a value taken before the
|
|
change. =main= stays refused: its caller is startup code no cell reaches.
|
|
docs/BUILT.md, "A signature change installs".
|
|
|
|
** 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.
|
|
|
|
** DONE A dyn value from eval-expr never reaches the reply's value field
|
|
CLOSED: [2026-09-25]
|
|
The renderer's emitter has a dyn entry: =println= keeps =flan_dyn_print= to
|
|
stdout, and the REPL's renders through =flan_dyn_emit_dev= into the value
|
|
buffer, so a dyn answer is the reply's =:value= on both backends. A dyn text is
|
|
quoted there as a typed string is. Rules out a dyn value arriving on =:output=.
|
|
|
|
** 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.
|
|
|
|
** DONE A debug tracking allocator over the raylib boundary
|
|
CLOSED: [2026-09-25]
|
|
A dev build counts calls to bindings named =Unload*= against the bindings that
|
|
return the same struct type (not =Get*=, except =GetClipboardImage=), noting
|
|
the release and acquisition in the generated wrapper and the site at the call.
|
|
A resource is keyed by its first pointer field, or its =id=, so writing other
|
|
fields keeps the match. The report at exit is behind =FLAN_DEV_LEAKS=. Release
|
|
builds emit what they did before. Rules out keying on the whole value and
|
|
tracking bare pointers. docs/BUILT.md, "A dev build counts a library's
|
|
resources".
|
|
** 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.
|
|
|
|
** DONE A transient signal 11 on a globals daemon
|
|
CLOSED: [2026-09-25]
|
|
Not a segfault. The report was OCaml's signal number, and in OCaml's numbering
|
|
-11 is SIGTERM (SIGSEGV is -10). Nothing in the daemon sends itself SIGTERM, so
|
|
it was killed from outside. The test binaries now print a signal by name
|
|
(=Test_support.signal_name=); a number from =WSIGNALED= is never printed raw.
|
|
|
|
** DONE test_dev daemons fail to bind under load
|
|
CLOSED: [2026-09-25]
|
|
The failure was a full /tmp, not the load. /tmp is a tmpfs, and every session
|
|
left its =flan-dev-<pid>= build directory behind (a test_dev run leaves about
|
|
300MB); a few concurrent runs filled it and each daemon after that died on its
|
|
link with ENOSPC. The fix is the session removing its own directory on a clean
|
|
=close=, which the build-plumbing lane owns. The half-write test's abort also
|
|
goes through =aborted= now, which took the same run down with an uncaught
|
|
=Wire.Closed=.
|
|
|
|
** 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 test binary leaves nothing in the temporary directory it was given
|
|
CLOSED: [2026-09-25]
|
|
Every test binary makes a fresh =flan-t<pid>-<hex>= under its =TMPDIR=, never
|
|
adopting an existing entry, exports it as
|
|
=TMPDIR= to everything it starts, and removes it on exit, on the watchdog, and
|
|
on SIGINT or SIGTERM (=test/own_tmp.ml=). A binary exits nonzero if that
|
|
directory survives or if a =flan-<pid>= or =flan-dev-<pid>= for its own pid
|
|
appears beside it. =dune test= was already clean, since dune gives every action
|
|
a private =TMPDIR=; the directories in /tmp came from binaries run directly or
|
|
through =dune exec=. A build's empty =flan-<pid>= goes at process exit. Rules out
|
|
a sweep that deletes directories by pid liveness, and any change to a killed or
|
|
crashed session keeping its directory.
|
|
|
|
** 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.
|
|
|
|
** TODO test_dev dies on Wire.Closed after the half-write abort
|
|
Intermittent, on an unmodified tree too: the =--llvm= half-write daemon in
|
|
=test_dev.ml='s =half_written= sometimes exits before it replies to =abort=, and
|
|
=request= raises =Wire.Closed= uncaught, so test_dev ends with a fatal, no =FAIL=
|
|
line and every later row unrun.
|
|
|
|
** TODO flan build and flan run leave an empty flan-<pid> directory
|
|
=Build.workdir= is created per process and nothing removes it once the IR is
|
|
gone. The dev daemon now removes its own on a clean end; the one-shot commands do
|
|
not.
|
|
|
|
** WAIT An x86 dev session's read after an allocating thunk once answered without the value
|
|
WAIT on a recurrence; the test now prints the failing read's own reply.
|
|
The one failure's message came from a second read, which said "kept", so the global
|
|
was intact and the first read's reply lacked the value: not the collector. Not
|
|
reproduced in 350 churn-and-read cycles under 8-way load, three concurrent test_dev
|
|
runs, or a valgrind run of the cycle, which was clean.
|
|
|
|
* Editor
|
|
|
|
** TODO C-c C-l on a generic needs a session to find its copies
|
|
The lowering view compiles the file, but only the daemon's =defs= says which
|
|
functions are a generic's copies, so with no session a generic's name shows
|
|
nothing. Needs a way to ask the compiler for a file's copies of a name.
|
|
|
|
** 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.
|
|
|
|
** DONE Every tracked .flan file reindents to itself
|
|
CLOSED: [2026-09-25]
|
|
clojure-mode decided each shape. The indenter was wrong on one: a =with-= head,
|
|
and a qualified =def…= or =with-= head, now indents as a body, and a qualified
|
|
name finds its unqualified part's spec. The rest was hand formatting and was
|
|
reindented: a =cond= or =match= result on its own line sits under its test, and
|
|
an ordinary call's later arguments align under its first. A lone =;= comment
|
|
line goes to =comment-column= in every Lisp mode, so continuation comments are
|
|
written as =;;= lines above the code instead.
|
|
|
|
** 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.
|
|
|
|
** DONE Hex, binary and an address on a primitive in the inspector
|
|
CLOSED: [2026-09-25]
|
|
Hex and binary were already drawn under every integer. The slot root's reply
|
|
now carries =:addr=, the address of the place it read, a field, element or
|
|
option payload down a path included, and the inspector shows it as =at 0x…=. A
|
|
data case's field and an expression root's value are not places and carry none,
|
|
rather than the address of a copy. A stack address is not offered to
|
|
=flan-inspect-address=, since the registry does not follow one.
|
|
|
|
** DONE A defclass is not on the definitions list as a type, and a sum's cases are not drawn
|
|
CLOSED: [2026-09-25]
|
|
=defs= reads classes off the session's declarations, which still hold every
|
|
=defclass=, and lists each as kind =class= with its slots and location; its
|
|
constructor is not listed again as a =fn=. Each data case is a =case= row named
|
|
=Type.Case=, drawn whenever =data= is, and the data row's signature lists its
|
|
cases. =CFn= is in =flan-mode='s 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.
|
|
|
|
** DONE defclass slots take types, checked on write
|
|
CLOSED: [2026-09-25]
|
|
Constructor parameters stay dyn and each store checks at run time; an int widens
|
|
into a float slot only if it round-trips, and nil fits only an (Option T) slot.
|
|
|
|
** DONE println takes up to a second to appear
|
|
CLOSED: [2026-09-25]
|
|
The daemon pushes program output on a connection that asked for pushes
|
|
(=(:op "push" :on t)=), coalesced to at most one frame per 50ms, and the
|
|
watch table on the same channel at =flan-watch-interval=. Emacs reads every
|
|
frame in a process filter. The editor's watch timer and =flan-settle-hook=
|
|
are gone. The poll stays for the stop and park edges. Rules out a faster
|
|
poll and pushes to clients that did not ask for them. See docs/BUILT.md,
|
|
"Output and the watch table are pushed".
|
|
|
|
** DONE set writes a class slot; put is for maps
|
|
CLOSED: [2026-09-25]
|
|
=put= on an instance still checks a declared slot's type and still inserts an
|
|
undeclared key; only =set= refuses one, since a slot it writes has to exist.
|
|
|
|
** NEXT update: change a place by applying a function to it
|
|
Decided 2026-09-25: every place evaluates each of its subexpressions once, C's compound-assignment rule, which also fixes =++= and =--=; =update= is built on that. Rules out refusing side effects in a place.
|
|
=(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.
|
|
Also not reproduced against a live daemon (2026-09-25): an =eval= of the
|
|
=defclass=, of a =defn= doing the =get= and =return=, and of both in one form,
|
|
and an =eval-expr= of the =get=, all succeed. In a session every installed
|
|
function of no arguments returning =()= has the type =(CFn [] ())=, not only
|
|
=pause= — a bare =pause= or =tick= asked of the session says so — so the
|
|
keyword may not be what resolved. The next report wants the exact form sent.
|
|
|
|
** DONE A digit does not take the restart RET takes
|
|
CLOSED: [2026-09-25]
|
|
Evil's normal state binds =0= (beginning of line), =1=-=9= (a count) and RET
|
|
above the major mode's map, so the digit never reached =flan-cnr-take-number=.
|
|
The keys =flan-cnr-mode-map= itself binds are given to Evil's normal and
|
|
motion states in that mode; every other key, including what =special-mode-map=
|
|
binds, stays Evil's. The other special-mode buffers (inspect, watch, doc, disassembly,
|
|
diagnostics, lower) have the same exposure and are not changed.
|
|
|
|
** DONE Evil takes the keys in the other Flan buffers
|
|
CLOSED: [2026-09-25]
|
|
=flan-evil-own-keys= (flan-mode.el) gives the keys a mode's own map binds to
|
|
Evil's normal and motion states; the break, inspect, watch, doc, disassembly,
|
|
diagnostics and lower buffers all call it. The doc, disassembly and watch
|
|
maps bind =q=, and the diagnostics map binds =RET= and =q=, so those keys are
|
|
the mode's own and behave the same under Evil. Every key a mode does not bind
|
|
itself, including the rest of =special-mode-map=, stays Evil's.
|
|
|
|
** NEXT Eval in the frame, from the break loop
|
|
Decided 2026-09-25: SLIME's eval-in-frame, as described.
|
|
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.
|
|
|
|
** DONE The stack lists prelude frames
|
|
CLOSED: [2026-09-25]
|
|
A frame whose location is =<prelude>= is hidden by default, and a line in its
|
|
place counts the hidden run; =P= shows them. A hidden frame keeps its index,
|
|
because =locals= and the inspector are asked by it. The innermost frame is
|
|
shown even when it is the prelude's, unless the stop is =(pause)=, because it
|
|
is where the program stopped. Rules out renumbering the visible frames.
|
|
|
|
** NEXT There is no stepper
|
|
Decided 2026-09-25: stepping happens inside a stopped frame, so the game loop and its clock are frozen, as under =(pause)=.
|
|
=(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.
|
|
|
|
** DONE A NaN cast says "does not fit", which reads as too big
|
|
CLOSED: [2026-09-25]
|
|
Two more =ArithError= codes: 5 for a cast of NaN and 6 for a cast of an infinity,
|
|
each with its own sentence. Both backends choose the code on the cold path, so the
|
|
guard is still two compares. =lhs= and =rhs= still carry the range. Rules out
|
|
carrying the float value in the condition.
|
|
|
|
** DONE The condition buffer cannot jump to the source
|
|
CLOSED: [2026-09-25]
|
|
RET (and =v=) on a frame or on the stop's =at= line opens the file there; TAB
|
|
alone folds a frame's locals. The buffer is a =next-error= buffer, made current
|
|
when it opens, so =M-g M-n= walks the stop and then each frame with a file.
|
|
Refusals — the prelude, a relative path, a missing file — are one function
|
|
shared with =M-.=.
|
|
|
|
** DONE loop's bindings should be sequential, like let's
|
|
CLOSED: [2026-09-25]
|
|
=check_loop= binds each name before checking the next initialiser; =recur= still
|
|
rebinds all at once. No other form had the gap: =let= was already sequential,
|
|
=dotimes= binds one name, and =fn=, =defn=, =match= and the handler and restart
|
|
clauses bind parameters with no initialisers.
|
|
|
|
** NEXT C-c C-c reports one error, not every error in the form
|
|
Decided 2026-09-25: every error in the form, at any depth. A failed subexpression takes an error type that fits any want, so checking continues around it and the errors it would cause are not reported — Rust's, TypeScript's and Elm's shape. Rules out stopping at a statement boundary.
|
|
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.
|
|
|
|
** DONE A session should start before a program compiles
|
|
CLOSED: [2026-09-25]
|
|
A file with no =main= starts on a stub =main= that returns and parks; =load-file= (=C-c C-k=, already its key — the inspector stays on =C-c C-i=) keeps what compiles and lists the rest. Rules out =flan dev= with no file at all, and =--two-process= on a file with no =main=.
|
|
|
|
** NEXT The daemon buffer is navigable but not coloured
|
|
Decided 2026-09-25: errors, warnings and notes take compilation-mode's faces, and the program's own output takes a face of its own so it reads apart from the compiler's.
|
|
=*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.
|
|
|
|
** DONE compilation-mode steps over the notes
|
|
CLOSED: [2026-09-25]
|
|
The daemon buffer and the diagnostics buffer set =compilation-skip-threshold= to
|
|
0 locally, so =next-error= stops on a note as well as an error. The user's own
|
|
default is left alone. Rules out relabelling a note as a warning to make it
|
|
navigable.
|
|
|
|
* 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.
|
|
|
|
** WAIT Every diagnostic carries a stable kind at the end of its first line
|
|
Decided 2026-09-25: wanted, in the shape =... found string [type-mismatch]=. Waits
|
|
for the documentation rewrite, which is what a kind would link to. The clause
|
|
order — understood, then the conflict, then the fix — is a rule in =CLAUDE.md=,
|
|
and reporting every error in a form is its own entry under the Editor heading.
|
|
|
|
** DONE docs/BUILT.md still describes (Handle T) and the pool as built
|
|
CLOSED: [2026-09-25]
|
|
The section is a short past-tense record: that neither exists, why they went,
|
|
the decisions a library version would face, and that classes landed without the
|
|
enumeration the pool was built to give. A stale runtime comment naming
|
|
=resolve= went with it.
|
|
|
|
** DONE The daemon still tells an editor the x86 backend pushes no frames
|
|
CLOSED: [2026-09-25]
|
|
The rewrite in =Dev.ask= is gone, so an x86 session passes the agent's reply
|
|
through as an LLVM one does. The Emacs manual and the README no longer list the
|
|
inspector as something the x86 backend cannot do.
|
|
|
|
** DONE web/index.html still claims there is no implicit widening
|
|
CLOSED: [2026-09-25]
|
|
Both sentences say what is true: a conversion that cannot change the number is
|
|
implicit, any other is written. The rest of the page is left for its rewrite.
|
|
|
|
** DONE plan.org's Types section lists a predicate that no longer exists
|
|
CLOSED: [2026-09-25]
|
|
The five predicates are the checker's five, with =integer?= in place of
|
|
=copyable?=, and the section no longer names =(Handle $t)= or =pool-new=.
|
|
|
|
** DONE plan.org's Data model section still describes move-only containers
|
|
CLOSED: [2026-09-25]
|
|
The Data model section says assignment copies a container's header and the
|
|
copies alias one buffer. The memory tiers and the classes section name the pool
|
|
and generational handles as a library over a =Vec=, and the classes gate that
|
|
named =Handle= is replaced by what classes are as built. The milestone record
|
|
of what was frozen is left as history.
|
|
|
|
** DONE plan.org cites the wrong mechanism for jank's relinking bug
|
|
CLOSED: [2026-09-25]
|
|
plan.org now says the crash was a teardown race (the maintainer's diagnosis on
|
|
issue #947), that jank already calls through vars, citing the clone, and that
|
|
Flan avoids the repro by compiling out of process.
|
|
|
|
** DONE docs/SPIKE-GENERICS.md lists landed work as remaining
|
|
CLOSED: [2026-09-25]
|
|
The row is marked as landed, with the test that covers it, rather than removed;
|
|
the report stays a dated record.
|
|
|
|
** DONE Two citations in spec-memory.md do not land where they say
|
|
CLOSED: [2026-09-25]
|
|
=Map_Cell_Info= is cited at core.odin:351. The =defer= bullet cites the
|
|
=defer_ok= field and the =Ast.Defer= arm, and says what the checker accepts: a
|
|
=defer= in a top-level =let= is legal, so =(defer (free v))= for a =let=-bound
|
|
=v= is expressible and the spec no longer says otherwise.
|
|
|
|
** DONE The sand hash is quoted as prose in five places besides its assertion
|
|
CLOSED: [2026-09-25]
|
|
Four prose copies were left, not five. BUILT.md points at =sand_out= in
|
|
test_acceptance.ml instead of quoting a number. The page's two are checked by
|
|
quotes.sh against a run, so they stay. The handoff report is dated and keeps the
|
|
number it recorded.
|
|
|
|
** 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=.
|
|
|
|
** NEXT The mutation pass has not been re-run
|
|
Decided 2026-09-25: re-run it once after the second batch of 2026-09-25 merges, with the heavy sweeps, after asking the author.
|
|
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.
|
|
|
|
** DONE bin/main.ml spells the compile pipeline out by hand
|
|
CLOSED: [2026-09-25]
|
|
=lib/front.ml= holds the load, the check and the link. =flan build=, =run=,
|
|
=emit= (both backends), =check= and =shim= go through it with =~all:true= (every error,
|
|
=Loc.Errors=); =Test_support.checked= and =linked= go through it without (the
|
|
first error, =Loc.Error=). The =--no-gc= and =--warn-memory= passes stay in the
|
|
CLI as a hook that sees the program before =Reach= prunes it.
|
|
|
|
** DONE Build.executable returns only its output path
|
|
CLOSED: [2026-09-25]
|
|
It returns the output path and, under =keep=, the path of the IR or assembly
|
|
it kept; without =keep= that file is gone and the second half is =None=. The
|
|
two-process daemon moves the host's IR from the path it is given and no longer
|
|
recomputes =Build.workdir=.
|
|
|
|
** CANCELLED The 2MB OFL font is not vendored
|
|
CLOSED: [2026-09-25]
|
|
Two megabytes of history for one example that already says on screen when the
|
|
font is missing and runs without it.
|
|
|
|
** 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.
|