2405 lines
132 KiB
Org Mode
2405 lines
132 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
|
|
=(- 0.0 f64-inf)=: the decision wrote =(- f64-inf)=, and there is no unary minus.
|
|
Rules out Clojure's =##Inf= reader literal.
|
|
|
|
** TODO (!= x x) is false for a NaN
|
|
=!== on floats is LLVM's ordered =one= on both backends (=lib/emit.ml= =fcmp_op=,
|
|
=lib/x86.ml= =float_cc=), so =(!= f64-nan f64-nan)= is =false= where C, Odin and
|
|
IEEE 754 say =true=; =(not (= x x))= is the only NaN test that works. Changing it
|
|
to =une= is a decision about what =!== means.
|
|
|
|
** 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 negative
|
|
decimal is still a =u64= bit pattern. 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.
|
|
|
|
** NEXT (clone slice) as the general spelling of what (bytes s) does
|
|
Decided 2026-09-25: =(clone xs)= copies any slice into the context allocator, sharing =flan_bytes_dup='s lowering with =bytes=. The allocator's region answers who frees it.
|
|
Not built because of the who-frees question =bytes= answers by leaning on
|
|
free-all and arena-destroy. =flan_bytes_dup= is already the lowering, so if slices
|
|
grow a =clone= the two should share it.
|
|
|
|
** DONE The count is length, and len is a name a program can have
|
|
CLOSED: [2026-09-21]
|
|
One arm in the checker and one row in the builtin table. A call to an undefined
|
|
=len= is refused after every table and after the shadowing guard, so a program
|
|
with its own =len= never sees the refusal.
|
|
|
|
** CANCELLED Counting abandonments
|
|
CLOSED: [2026-09-21]
|
|
An =abandoned= verb was drafted and dropped. The wait already ends when the
|
|
thunk breaks and the restart's own reply says what taking it did; a third
|
|
telling read by nobody is how a wire grows a verb whose answer drifts from what
|
|
happened.
|
|
|
|
** DONE The randomness surface
|
|
CLOSED: [2026-09-21]
|
|
=(rand-int)=, =(rand)=, =(rand-bool)=, =(rand-int-range lo hi)=,
|
|
=(rand-float-range lo hi)=. The generator is PCG-RXS-M-XS 64, so every call costs
|
|
one draw and a seeded run is reproducible — but the permutation is a bijection of
|
|
the state, so someone holding one result can predict the rest. Fine for a grid,
|
|
not for a key.
|
|
|
|
** DONE A value-producing array constructor
|
|
CLOSED: [2026-09-20]
|
|
=(array-fill [n ...] v)= and =(array-gen [n ...] f)= build the whole array as a
|
|
value, so one can be a =def='s initialiser. Ranks are spelled flat; there is no
|
|
nested bracket syntax. Row-major order is a promise, and the fill value and the
|
|
generator are each evaluated once, before any loop runs.
|
|
|
|
** DONE (array 4 T) is the zeroed array constructor
|
|
A =let= binding takes no type, so =(let [pts [4 rl/Vector2]] ...)= reads the
|
|
bracket as a two-element array literal. =(zeroed [4 rl/Vector2])= was proposed
|
|
first and rejected on how it reads — unambiguous to the parser, still looks like a
|
|
two-element vector to a person.
|
|
|
|
** DONE A defining form owns what happens on a re-run
|
|
CLOSED: [2026-09-20]
|
|
Each computed global's initialiser guards itself with its own flag, per global
|
|
rather than per startup function, and the daemon has no policy about globals at
|
|
all. Dev builds only; a release build's output is byte-identical.
|
|
|
|
** DONE defenum autoincrement
|
|
CLOSED: [2026-09-17]
|
|
C's rule: a member with no value is the previous plus one, and the first is zero.
|
|
An explicitly written duplicate is an intended alias and allowed; one produced by
|
|
autoincrement walking into another member's value is refused, naming both.
|
|
|
|
** DONE Every mapped raylib enum carries a Flan-side prefix
|
|
CLOSED: [2026-09-21]
|
|
An optional third column on the enum line in =vendor/raylib/bindings=, stripped
|
|
before the C prefix is applied. A reading choice and not a collision fix — a
|
|
keyword resolves against the expected type and against nothing else, so two enums
|
|
could always share a member spelling. What the prefix buys is the call site read
|
|
on its own.
|
|
|
|
** 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.
|
|
|
|
** NEXT Conditions get a parent link, not class inheritance
|
|
Decided 2026-09-25: build it, with a root =Error= every built-in error descends from, so one handler catches any error. A catch-all handler gets the condition's name and the runtime's sentence, not its fields. =(pause)= and warnings are not under =Error=.
|
|
A condition type may name a parent where it is declared, and handler matching
|
|
walks that static chain. It buys the hierarchy conditions most lack — a catch-all
|
|
"any file error" handler — at compile-time cost only. Rules out the class answer:
|
|
a class condition allocates at the signal site, inverts the lifetime rule, and
|
|
lets a layout change under a standing handler frame. Not built.
|
|
|
|
** 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.
|
|
|
|
** TODO A restart-case clause has no report string
|
|
The field is cheap and the accessor is cheap, but the only consumer is the break
|
|
loop's listing, so it would ship as a field nothing read. It belongs with the
|
|
listing work.
|
|
|
|
** WAIT find-restart and compute-restarts
|
|
Blocked on a type, not on effort: the spec gives them =(Option Restart)= and a
|
|
list, and there is no =Restart= type and no list type to return one in. The
|
|
minibuffer prompt never needed them — it reads the snapshot over the agent's
|
|
socket.
|
|
|
|
** DONE into fuses at compile time because it is a macro
|
|
Not transducers and not Rust's iterators, both of which compose at runtime and
|
|
need function values and allocation. A macro writes the call straight into the
|
|
loop body, so the function name is syntax and never a value. What it gives up is
|
|
building a transformation at runtime, which is close to useless in a game.
|
|
Reductions deliberately do not share the form.
|
|
|
|
** DONE loop and recur
|
|
Tail position is a permission that is withdrawn rather than a pre-pass. =loop= is
|
|
itself a barrier for =break= and =continue=, because a loop answers with the value
|
|
of its body so a jump out has no value to give — which is also why it takes no
|
|
label. Neither backend learned anything: a =loop= is a =let=, a =While= on true,
|
|
and two jumps. Mutual recursion still needs real tail calls and is out of scope.
|
|
|
|
** DONE break and continue, with loop labels
|
|
Labels are Odin's, in the head position, where a keyword is unambiguous because a
|
|
loop condition is never one. It is not a =goto= — control can only leave a loop it
|
|
is already inside.
|
|
|
|
** DONE The second tier of the standard library
|
|
The prelude had 44 allocation-free functions because there was nothing to allocate
|
|
from. =append=, =concat=, =join=, =split=, =format-f64=, =atan2=, =pow= and
|
|
=clamp= are the tier that returns new storage.
|
|
|
|
** DONE The Clojure patterns deliberately not copied
|
|
Four rules. The thing being operated on comes first, everywhere, so there is no
|
|
second threading macro. A membership test says which thing it tests. A =?= name
|
|
answers yes or no and a finder answers the thing or nothing. Composition reads
|
|
left to right. Rules out inheriting Clojure's =contains?=, =some= and =comp=
|
|
shapes.
|
|
|
|
** DONE A pointer from C needs a length before it can be indexed
|
|
CLOSED: [2026-09-13]
|
|
=(slice-from-ptr p n)=: the caller states the length and owns being right about
|
|
it. The alternative weighed — a per-binding declaration naming which argument
|
|
carries the count — cannot reach a count that is a sibling field. No marker on the
|
|
name; =ptr= is the marker, it owns nothing, and =free= refuses it.
|
|
|
|
** 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.
|
|
|
|
** NEXT Escaping closures, allocated on the GC side
|
|
Decided 2026-09-25: start it. It must work on wasm32.
|
|
The second half of "do both". What changes is where the environment points — a
|
|
frame slot today, a collector allocation then — and the escape check goes away
|
|
with it, along with the refusals on returning, storing, pointing at and pushing a
|
|
capturing value. Two things for it to know: a widening thunk's environment holds a
|
|
code pointer rather than a GC object, and capturing a dyn stays refused until a
|
|
synthesised environment has a descriptor.
|
|
|
|
** 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.
|
|
|
|
** NEXT update-instance-for-redefined-class, the user hook
|
|
Decided 2026-09-25: build it after typed class slots land, shaped for the REPL — written and installed from a live session as a one-time "here is how to migrate this", without restarting. It receives the instance with the added and discarded slots and their old values, and runs at each instance's lazy migration. A hook that signals parks in the break buffer with a restart that falls back to name-matching migration.
|
|
Left out of v1 because name matching is the half that makes redefinition usable
|
|
and the hook is what makes it expressive. The obvious spelling is a generic riding
|
|
the dispatch that exists, and the migration already computes both the added and
|
|
the discarded lists. Rolling a failed migration back becomes a real question the
|
|
day this lands.
|
|
|
|
** DONE The module system stays directory-as-package
|
|
Several files in one directory are one module; a loose file is a module of one,
|
|
and no =package= line is required or accepted. Confirmed against Odin, which
|
|
requires the declaration despite the same rule, so the line buys only the ability
|
|
to disagree with the directory name. Acyclic imports are kept deliberately — a
|
|
definite package order is what the macro expander needs.
|
|
|
|
** DONE A name imported through a package keeps the inner alias
|
|
If =area/= imports =shape=, the type is =shape/Box= in the finished program and
|
|
never =area/shape/Box=. Forced rather than chosen: a directory reached along two
|
|
routes must arrive under one set of names or the checker sees every declaration
|
|
twice. The price is that the same directory under two aliases is refused, naming
|
|
both.
|
|
|
|
** 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.
|
|
|
|
** WAIT An F#-ish indentation surface beside s-expressions
|
|
Deferred 2026-09-19 with no spike queued, blocked on evidence: the author will write
|
|
imperative Flan as it stands and see whether the parens still grate. If it is ever
|
|
built, it is one AST with the existing forms unchanged and a second reader in
|
|
front. Rules out Parinfer, wisp and sweet-expressions, and a simplified in-paren
|
|
syntax — all thin the parens without removing them.
|
|
|
|
** TODO The shims in sand.flan can go
|
|
=sand.flan= defines =dyn->f64= and =dyn->u32=, one-line functions whose only job
|
|
is that their parameter slot unboxes. Every call site can write =(f64 d)= and
|
|
=(u32 d)= now. The author's file, not a lane's to edit.
|
|
|
|
** DONE sand.flan's game-data.edn initialiser no longer aborts the headless import
|
|
A global initialiser that read a file at startup failed wherever the working
|
|
directory was not the author's. It is wrapped in a =handler-case= with a
|
|
=FileError= clause; away from that directory the data reads as nil.
|
|
|
|
** DONE An error in a macro's body is reported where it was written
|
|
CLOSED: [2026-09-21]
|
|
A form a macro splices through keeps its own line, and the =note:= naming the
|
|
macro points at the call. The wire =Form= still carries no location: the
|
|
marshalled payload pointer is the identity key instead, which is SBCL's
|
|
=*source-paths*= with an address standing in for =eq=. A node the macro built
|
|
inherits the nearest located ancestor; only a wholly macro-built subtree falls
|
|
back to the call site. Costs about 2µs a call. Rules out putting a =loc= field
|
|
on the wire, and rules out structural matching of the expansion against the
|
|
arguments, which can pick the wrong one of two equal subtrees.
|
|
|
|
** 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]
|
|
With nothing outside an array literal naming its element type, the first
|
|
element's type is the want for the rest, so =[(f32 1.0) 2.5]= is a =[2 f32]=. A
|
|
refusal of a later element carries a note at the first saying it set the type.
|
|
Rules out a =1.0f= suffix for now.
|
|
|
|
** NEXT A let binding takes no type annotation
|
|
Decided 2026-09-25: =(the T expr)=, Common Lisp's special operator, gives any expression its want; checked at compile time like any other want, and it compiles to nothing. =let= is unchanged. On a =dyn= operand it is refused, naming the cast. The refusals that say "annotate the binding" — =None=, an empty =[]=, and =(zeroed)=/=(filled)=/=(dead-beef)= with no want — suggest it instead, because today their suggestion cannot compile.
|
|
Everything under the surface is there — the binding carries a type slot and the
|
|
checker consumes it as the want — and only the way it is written is open, because
|
|
=let= is a flat list of pairs and cannot disambiguate by count. No longer the
|
|
blocker it was, since =(array 4 T)= answers the case that raised it. plan.org's
|
|
rule is "annotate function signatures, infer locals", so a general annotation is a
|
|
deliberate absence.
|
|
|
|
** NEXT A read-only slice type
|
|
Decided 2026-09-25: =[const u8]=, Zig's spelling in Flan's brackets. =bytes-view= answers one and a =set= through it is a compile error; a =[T]= converts to =[const T]= and not back, and the prelude's read-only functions take it. =const= is reserved as a name, since =[n T]= accepts a constant's name for =n=.
|
|
=bytes-view= is read-only by convention only — the type system cannot say a =[u8]=
|
|
may not be stored through, so a trap on read-only memory is the enforcement. A
|
|
read-only slice type, or provenance, is what would move that refusal to compile
|
|
time.
|
|
|
|
** NEXT Writing through a string literal
|
|
Decided 2026-09-25: closed by the read-only slice type above.
|
|
=(let [s (bytes-view "Hi")] (set (at s 0) \h))= stores into read-only memory at
|
|
=-O0= and is deleted as undefined at =-O2= — same source, and which way it fails
|
|
depends on a flag. Narrowed when =(bytes s)= started copying, so the common
|
|
spelling no longer reaches the edge. Emitting literals as mutable globals is not a
|
|
fix: it moves which flag misbehaves and costs their read-only placement.
|
|
|
|
** TODO (slice d 1) over a dyn string is refused where (at d i) works
|
|
The typed and dyn spaces disagree about a spelling, which the standing rule
|
|
forbids. A dyn slice should exist.
|
|
|
|
** TODO (slice "abc" 0 99) is not refused at compile time
|
|
A string type carries no length, so there is nothing to compare the bound against
|
|
— consistent with a slice of a slice. A missed nicety rather than a hole; the
|
|
runtime check still catches it.
|
|
|
|
** 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
|
|
The cost of accepting the pattern is that a negative decimal literal is accepted
|
|
as a =u64=, because the reader records the value and not how it was written.
|
|
Narrower unsigned types keep the strict check, which is where a typo like =300=
|
|
for a =u8= shows up.
|
|
|
|
** DONE A folded constant does not skip the range check
|
|
The folding pass makes its own call to the range test, because a global's
|
|
initialiser has to be a compile-time constant and only that pass knows this one
|
|
is.
|
|
|
|
** DONE A defn must state its return type, and an unknown one says so
|
|
=(defn f [] f65 0.0)= says *unknown type f65 — did you mean f64?*. The
|
|
unconditional return slot removed the guess, and the pre-pass that collected a
|
|
file's type names went with it.
|
|
|
|
** DONE Anything that binds a name or alters control flow is recognised explicitly
|
|
The house rule that caught two misparse bugs: anything that binds a name, alters
|
|
control flow, or is not yet implemented must be recognised by name and rejected if
|
|
unsupported. Rules out silently falling through to a generic arm.
|
|
|
|
** DONE The escape was real: a value the compiler builds trips no function-value refusal
|
|
All four function-value refusals were about surface syntax, so =Allocator= could
|
|
be a builtin opaque type with no user-writable constructor and the containers
|
|
needed nothing from generics. The compiler already did exactly this twice — the
|
|
lifted handler clause and the dev build's indirect call.
|
|
|
|
** DONE Function values, with no capture
|
|
=map=, =filter=, =reduce= and a comparator-taking =sort= arrived with no generics
|
|
at all, which is what the diagnosis predicted. A =map= that *changes* the element
|
|
type is the one shape that did not come with them: one copy per ordered pair of
|
|
types rather than per type.
|
|
|
|
** 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.
|
|
|
|
** NEXT (max-value T) and (min-value T)
|
|
Decided 2026-09-25: the type-limit constants as a form taking a type, Odin's
|
|
max(T), valid at any numeric type or a numeric?-bounded variable. For a float,
|
|
min-of is the most negative finite value.
|
|
|
|
** NEXT (Ptr const T), the pointer beside [const T]
|
|
Decided 2026-09-25: addr through a read-only slice gives a (Ptr const T), which
|
|
nothing writes through; (Ptr T) widens to it and never back; a C parameter
|
|
declared const T* takes one. Closes the addr hole in [const T].
|
|
|
|
* 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.
|
|
For the next sweep rather than for a lane.
|
|
|
|
** NEXT A sliced string loses the trailing NUL
|
|
Decided 2026-09-25: both backends emit a NUL after every string literal, and a declare-c wrapper passes a literal argument to C without the copy it makes for any other string. A string is still pointer and length; no slice is promised a NUL. Rules out a NUL guarantee on every string.
|
|
The x86 backend emits a NUL after every string constant and the LLVM one does not,
|
|
so a =declare-c= wrapper leaning on the courtesy is already backend-dependent as
|
|
well as slice-dependent. The contract is pointer and length, and nothing promised
|
|
otherwise.
|
|
|
|
** 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.
|
|
|
|
** NEXT A temporary allocator, wiped each frame
|
|
Decided 2026-09-25: Odin's context.temp_allocator. i64->bytes, f64->bytes and
|
|
other quick formatting allocate from it, so a number drawn every frame no longer
|
|
leaks from the default allocator. A dev build wipes it at each frame boundary;
|
|
otherwise the program calls (free-temp) once per frame. Text kept past the frame
|
|
is cloned.
|
|
|
|
* 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.
|
|
|
|
** NEXT The Vec generation word has no reader
|
|
Decided 2026-09-25: remove the word, and in a dev build fill a Vec's old buffer with the dead-beef pattern when a push moves it, so a stale slice reads visibly wrong values. No slice layout change; a release build is untouched. Rules out a dev-only word on every slice.
|
|
It is bumped on reallocation and read by nothing. The stale-slice trap it exists
|
|
for needs a slice that can carry the Vec's identity, and a slice is pointer and
|
|
length — so either slices grow a word in a dev build or the trap does not exist.
|
|
Today it does not.
|
|
|
|
** 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. An =Allocator=
|
|
value kept past its destroy names the new arena once its header is reused.
|
|
Rules out freeing the header while any container may hold it. The
|
|
=DestroyedAllocator= trap prints no site: the allocator procedure is given none. 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 minus the user hook. Nothing is enumerated and no heap is walked — the
|
|
redefinition is constant time and each instance pays once, at its next touch.
|
|
Neither printer migrates, so a stale instance shows its old slots to the editor
|
|
until something touches it. The registry is advisory: a key the class never
|
|
declared is dropped by the next migration, which is data loss with no enforcement
|
|
behind it.
|
|
|
|
** 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.
|
|
|
|
** TODO An unhandled condition has no location
|
|
The error entry point takes five integer arguments, which fills the argument
|
|
registers; a location pair makes seven, so the x86 backend would need stack
|
|
argument passing at a call site whose register file is exactly full. The dev-side
|
|
half is different work: the trap hook hands control to a session in-process with
|
|
the compiler, which can read the source.
|
|
|
|
** 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.
|
|
|
|
** TODO A restart has no location
|
|
The restart frame is mirrored across both backends and the runtime, so giving
|
|
=continue= a file, line and column means two fields, stores in both backends, an
|
|
accessor, the snapshot copying it and the buffer printing it. A cross-backend ABI
|
|
change; do it as one lane, not as a rider. A site for user =error= calls is the
|
|
same lane if the frame is being touched anyway.
|
|
|
|
** TODO handler-case's own restart is listed in a break loop under it
|
|
The restart the form makes up for itself is on the restart stack like any other.
|
|
Hiding it means a new field in the frame layout written out in both backends and
|
|
the runtime. Choosing it is refused loudly rather than answered wrongly, so this is
|
|
cosmetic.
|
|
|
|
** NEXT A formatted number does not outlive its frame
|
|
Decided 2026-09-25: the conversion's bytes are always copied into the context allocator, so the string outlives the frame. Rules out refusing the escape, which needs flow tracking.
|
|
The conversion buffer is one frame slot per call site, so returning a string built
|
|
from it returns a view of storage the return has just released, and pushing one
|
|
pushes an element aliasing that slot. Neither shape is refused. Copy the bytes for
|
|
anything that outlives the expression that made them, which is what =append-i64=
|
|
and =append-f64= do.
|
|
|
|
** DONE A shift count is bounded two different ways
|
|
A literal count out of range is rejected by the checker; a computed one is masked
|
|
to the operand's width minus one. A shift by the operand's own width is poison in
|
|
LLVM rather than a wrong number — =(<< 1 32)= compiled to a bare return. The mask
|
|
is what the hardware does anyway and is folded away when the count is constant.
|
|
|
|
** DONE The linked-list frame beat an array with a stack pointer
|
|
The opposite of what the escaping-alloca argument predicts, and the measurement
|
|
that first said otherwise was comparing a 40-frame binary with a 600-frame one.
|
|
That is why every number in =docs/BUILT.md= is a minimum of nine runs.
|
|
|
|
** 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.
|
|
|
|
** NEXT A destroyed arena always traps, even after its record is reused
|
|
Decided 2026-09-25: an Allocator value is two words, the record and the
|
|
incarnation it was made for; every use compares the incarnation, so a destroyed
|
|
arena traps whether or not a later arena-new reused its record. Rules out
|
|
static tracking of destroy, which is move semantics.
|
|
|
|
** NEXT A mixed array literal with no want is a dyn vector
|
|
Decided 2026-09-25: with nothing expected of it, an array literal whose elements
|
|
agree (numbers widening together) is typed; one whose elements mix — [10 "Hi"],
|
|
[nil 1] — is a dyn vector. (the [T] ...) forces a typed one, and a want from
|
|
context still wins. Replaces the first-element carry-over.
|
|
|
|
* Dev loop
|
|
|
|
** DONE The dev loop, step 1: the reload primitive
|
|
A list of top-level forms is recompiled and installed into a running process, and
|
|
call sites compiled before those forms existed follow them through an indirection
|
|
cell.
|
|
|
|
** DONE The dev loop, step 2: a name the process was never built with
|
|
A =defn= or a =defonce= the process was not built with can be added and then
|
|
redefined again, through a by-name cell in the dev registry.
|
|
|
|
** DONE The dev loop, step 3: the agent installs at a frame boundary
|
|
The program takes a redefinition over the agent socket and installs it between
|
|
frames.
|
|
|
|
** DONE The compiler is a thread inside the program
|
|
CLOSED: [2026-09-14]
|
|
One binary that is the compiled program and the whole OCaml compiler. The program
|
|
keeps =main()= and the compiler comes up on a side thread beside the agent's
|
|
listener, rather than the program being loaded into the daemon — macOS needs a
|
|
window on the main thread, and the agent was already a server inside the program.
|
|
Crash isolation is given up knowingly. =docs/BUILT.md=, "One process", is the
|
|
account, and the embedding spike that made it safe to commit to is under it.
|
|
|
|
** DONE Transport and code generation were independent, and transport was the larger prize
|
|
CLOSED: [2026-09-14]
|
|
Merging the processes removed the transport; a new backend removes code
|
|
generation. Neither implied the other, and the two were repeatedly conflated. The
|
|
order held: spike the embedding, merge while keeping the existing build path
|
|
exactly as it was, measure what was left, and only then choose a backend.
|
|
|
|
** DONE Re-runnable main after the window closes
|
|
CLOSED: [2026-09-17]
|
|
A finished program parks instead of dying, and a daemon op wakes it and re-enters
|
|
=main= on the same thread, because a window belongs to the thread that opened it.
|
|
Globals are not reset between runs — the process never died. Rules out a fresh
|
|
process per run.
|
|
|
|
** NEXT Re-run does not work under --two-process
|
|
Decided 2026-09-25: re-run under =--two-process= starts a fresh child, installed redefinitions included, and says that globals start over because the process is new.
|
|
A finished child process is genuinely gone, so there is nothing to wake. Re-run is
|
|
merged-build only, and since the default backend runs merged it is no longer the
|
|
blocked case.
|
|
|
|
** DONE An accepted re-run reads as running
|
|
CLOSED: [2026-09-21]
|
|
A caller that asked for a re-run and then waited for the program to park was
|
|
answered by the park it had just ended. The state is set under the lock that
|
|
accepted the request, so the state is the decision rather than a report of it.
|
|
Rules out teaching the tests to wait on something else — the runtime refuses a
|
|
second re-run precisely because the first is committed, so the two answers
|
|
disagreed about the same fact.
|
|
|
|
** DONE C-x C-e installs a top-level form
|
|
CLOSED: [2026-09-17]
|
|
Context-aware: a top-level form compiles and installs, anything else evaluates as
|
|
an expression. =C-c C-c= stays the explicit alias.
|
|
|
|
** DONE C-x C-e answers against a parked program
|
|
CLOSED: [2026-09-17]
|
|
An expression evaluates against a parked program by draining the agent's ring from
|
|
the park, rather than by loosening what a frame boundary is. Safe because a parked
|
|
program has no concurrency at all.
|
|
|
|
** DONE Evaluating a def assigns
|
|
CLOSED: [2026-09-21]
|
|
=def= is Common Lisp's =defparameter=, and evaluating a =defparameter= assigns.
|
|
The previous reading — a promise about the next re-run — was wrong for the reason
|
|
the form is named after. Both events happen: an evaluation that assigns and a load
|
|
that re-initialises. =defonce= and =defconst= are unchanged.
|
|
|
|
** DONE An evaluated expression that signals says so at once
|
|
CLOSED: [2026-09-20]
|
|
The wait recognised only a pause, so every other stop fell through to a timeout
|
|
arm whose sentences were about something else. "A stop entered after the module
|
|
was delivered" is a generation number rather than a name, so evaluating from
|
|
inside a break into a thunk that stops on the same condition class is settled by
|
|
comparing two integers.
|
|
|
|
** NEXT Whose break it is, which no counter answers
|
|
Decided 2026-09-25: fix it. A stop records whether the thread that stopped was running the evaluation's thunk or the program's own code, so the sentence is decided by the frame and not by the generation counter.
|
|
A game loop that signals during the build or the wait bumps the generation exactly
|
|
as a thunk would. The machine-readable fields stay right; what is wrong is the
|
|
sentence. The per-frame program-or-eval label is computed by the daemon from
|
|
ownership, not from anything in the frame, so this is not the shadow-stack gap it
|
|
was once written down as. Not queued — the window is narrow.
|
|
|
|
** DONE The first evaluation no longer stalls behind the agent socket
|
|
The accept loop used to sit behind a ten-second wait for the agent socket, so a
|
|
program that binds its socket late — or not at all — looked ready and answered
|
|
nothing. The wait is no longer in front of the accept loop.
|
|
|
|
** DONE The parked-program note is said once per park
|
|
A finished program is always parked, so re-evaluating =main= after every run
|
|
printed the whole note again. The daemon remembers whether it has said it for this
|
|
park.
|
|
|
|
** DONE A session ends when no editor has held the socket for a grace period
|
|
CLOSED: [2026-09-18]
|
|
Two graces — five minutes parked, thirty seconds live — because a parked program
|
|
is invisible and a live one may be a window somebody is watching. Armed only after
|
|
a first client has connected, so a headless daemon is untouched.
|
|
|
|
** DONE Parked orphans exit with their daemon
|
|
CLOSED: [2026-09-18]
|
|
A child outliving its daemon is killed by the kernel. Found in passing that half
|
|
the observed orphans were merged daemons whose editor had vanished, which the
|
|
grace above answers.
|
|
|
|
** DONE The daemon survives a client that closes mid-reply
|
|
CLOSED: [2026-09-14]
|
|
A reply written into a socket whose reader had gone used to kill the process, and
|
|
in a merged build that process is the program, the compiler and the listener at
|
|
once. It reported as a connection refusal on a path that plainly existed, which
|
|
misdirected two investigations.
|
|
|
|
** 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.
|
|
|
|
** NEXT FLAN_AGENT_SOCKET in a shell's environment steals the socket
|
|
Decided 2026-09-25: narrow the gate. The daemon also exports its own pid, and the constructor binds the socket only when that pid is the program's parent (or the program itself, in a merged build).
|
|
Binding unlinks the path first, and before the constructor that unlink was reached
|
|
only by an explicit call. A sentence about the shape of the gate rather than an
|
|
observed problem: only the daemon sets the variable and it never runs release
|
|
builds. The fix, if it is ever felt, is a narrower gate.
|
|
|
|
** 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.
|
|
|
|
** TODO A restart's source location and arity are not on the wire
|
|
The restart frame is =prev=, a name id, a name and a length. A backtrace and
|
|
locals landed out of the shadow stack and needed no debug information; these did
|
|
not come with them.
|
|
|
|
** TODO The editor half of a typed restart
|
|
The language half is in — a restart clause takes parameters and =invoke-restart=
|
|
passes them. What is missing is the half only an editor can do: arity and signature
|
|
on the frame, the restart listing carrying the signature, and the daemon compiling
|
|
each argument against the declared type and writing the values into the frame's
|
|
buffer before aiming the channel.
|
|
|
|
** 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.
|
|
|
|
** NEXT A program driven by a real flan dev daemon under a sanitizer
|
|
Decided 2026-09-25: =flan dev --sanitize= builds the host under ASan/UBSan on the LLVM backend (refused by name with =--x86=), and the @sanitize alias gains a case driving a real session through reloads and a break.
|
|
The daemon builds its host through its own path and the CLI has no way to pass a
|
|
sanitizer flag to it. Named as the check worth adding next; a day rather than an
|
|
hour. The x86 backend is not a gap here — that pair is refused by name, because
|
|
there is no sanitizer pass over hand-written assembly.
|
|
|
|
** 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.
|
|
|
|
** TODO An x86 dev session's dyn global sometimes reads wrong after an allocating thunk
|
|
test_dev's =--x86: after a thunk that allocates (cycle 1) the parked program's dyn
|
|
global reads "kept"= failed once in a full =dune test= on 2026-09-25 and passed three
|
|
direct reruns. Intermittent and GC-shaped: a dyn global read after a collection a
|
|
C-x C-e thunk triggered. Needs reproducing under load and fixing.
|
|
|
|
* 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.
|
|
|
|
** NEXT defclass slots take types, checked on write
|
|
Decided 2026-09-25: slots are name/type pairs checked on write; an untyped slot stays legal and holds any =dyn=. A migration keeps a stored value that no longer fits the new type, warns once, and the next write is checked. One lane with the =set= entry below.
|
|
=(defclass State [pause bool step bool])= reads as four untyped slots and
|
|
reports a duplicate =bool=. Wanted: the slot list is name/type pairs, as CLOS
|
|
does it. The type is a declaration about the values and not a layout — an
|
|
instance stays a map, so redefinition and lazy migration are unchanged. SBCL
|
|
checks it on write (=src/pcl/slots.lisp:160=, the typecheck before the store),
|
|
which is where the bad value is, so =put= is the site here.
|
|
|
|
Open: what migration does with a stored value that no longer fits a changed
|
|
slot type, and whether an untyped slot stays legal (it should — =dyn= is a type
|
|
and writing nothing should mean it).
|
|
|
|
** 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".
|
|
|
|
** NEXT set writes a class slot; put is for maps
|
|
Decided 2026-09-25: as written; one lane with typed slots.
|
|
=put= exists because an absent map key has no location to store into, which is
|
|
why =(get m k)= is refused as a place (=lib/parse.ml:1159=). A class instance is
|
|
not in that situation: its slots are fixed by the =defclass=, so a declared slot
|
|
always exists and =(set (get state :pause) true)= is a field store like
|
|
=(set (.velocity g) 0.0)=. Make =set= take it, and leave =put= to maps, where
|
|
insertion is real. Writing an undeclared slot through =set= is then a refusal
|
|
naming the class.
|
|
|
|
** 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.
|
|
|
|
** TODO The break buffer prints fields, not the sentence the runtime wrote
|
|
=ArithError — op 4, lhs -2147483648, rhs 2147483647= where the runtime's own
|
|
sentence is "this value does not fit the integer type it is cast to"
|
|
(=runtime/flan_rt.c:986=). Worse for a dyn trap: =DynType= has no struct at all,
|
|
so the buffer says "no struct is named DynType" while =flan_dyn.c:799= has
|
|
written the operation, both tags and both values to stderr. The sentences exist
|
|
and go to the daemon buffer; the break buffer wants them on the wire.
|
|
=ArithError='s =op= being a bare number is the same gap — it is an enum spelled
|
|
as =i32=.
|
|
|
|
** TODO A backtrace frame names the function, not the call
|
|
=fninfo= (=lib/emit.ml:185=) holds one static =loc=, the =defn='s own, and
|
|
=flan_frame= (=runtime/flan_dev.c:1011=) adds no per-call location — so two
|
|
calls to the same function from one caller are indistinguishable in the stack.
|
|
Wants the caller storing its call site into the frame before the call, which is
|
|
a field and a store on every dev-build call.
|
|
|
|
** 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.
|