71 Commits

Author SHA1 Message Date
c1916ed819 e as g is any test of a condition's and chain, binding g for the rest of the chain and the block. 2026-09-26 17:07:01 +07:00
d252eee2b2 In .fln x? tests that a value is present and narrows a local Option to its payload in the block it guards, and e? as g names what a test found. 2026-09-26 15:48:36 +07:00
178c129c97 A .fln name carries no ? or !, and .fln has T?, x ?? d, x!, if let over a plain name and a?.b chaining. 2026-09-26 14:54:58 +07:00
7db1a81d1f Builtins, prelude, vendor and raylib predicates are named with is- or has- and no name carries a ? or !. 2026-09-26 14:34:36 +07:00
5ee433a38f A kept when answers an Option, get is a checked lookup over arrays, slices, Vecs and dyn, and if let reads as a two-arm match in both syntaxes. 2026-09-26 12:12:48 +07:00
267db2cd91 The typed read-only text type is spelled str, and string is refused with that fix. 2026-09-26 05:22:24 +07:00
2135f6d37f Merge branch 'master' into worktree-agent-a46233a32d1912c78 2026-09-25 23:27:55 +07:00
ccdadeeb6e A mark or a step spliced into a program that defines its own pause or step-point calls the prelude's 2026-09-25 22:34:28 +07:00
0fc258c434 Merge branch 'master' into worktree-agent-a46233a32d1912c78 2026-09-25 21:11:17 +07:00
69eb11ce9b Return types read off the body and generic structs live together 2026-09-25 20:51:38 +07:00
111ee66a4b A match arm can be a number, char or string literal compared with =, over a number, a string or a dyn, with a _ arm required 2026-09-25 20:51:24 +07:00
ad40123b25 Master's error recovery and return types read off the body live together, and two ways out of an inferred body that cannot share a type are refused with both named 2026-09-25 19:58:31 +07:00
ff88b9dad6 A defn whose return slot is _ (or a .fln fn with no arrow) takes its return type from its own body, recursion through such functions is refused by name, and a stale caller of one is told which line changed its type 2026-09-25 17:09:13 +07:00
2f321fd061 Merge master into the generics lane 2026-09-25 17:03:13 +07:00
4054c4921b A defstruct whose fields introduce $t or an array length $n is a generic struct, each application of it an ordinary struct copy that generic functions bind against, and a printed call is evaluated once 2026-09-25 16:08:14 +07:00
9a91440486 C-c C-s installs a defn that stops before each form of its body, and the break buffer steps it with s and runs the rest of the call with c 2026-09-25 15:43:24 +07:00
610e9fcf9d A parent's handler reads a name and message made in context/temp, whole and good until the frame ends, and the handler-case landing no longer holds a buffer of its own 2026-09-25 14:36:46 +07:00
b2eb81f7bf A slice or pointer can be read-only, and what it reaches cannot be written through it 2026-09-25 13:39:04 +07:00
f3d969618d A class slot can carry a type, and set writes a declared slot 2026-09-25 13:32:16 +07:00
3e09a1f722 Merge branch 'master' into worktree-agent-ac750a5e83cab251b 2026-09-25 13:18:42 +07:00
9d6694fd92 Merge master: an evaluated expression's lifted bodies are placed with its thunk 2026-09-25 13:18:40 +07:00
9aeee9d676 Merge branch 'master' into worktree-agent-a6ffe579d55d1892b 2026-09-25 13:16:58 +07:00
0358f6637c Merge branch 'master' into worktree-agent-ad097f67134ba0552 2026-09-25 13:15:36 +07:00
bfd176cce9 Merge master: a handler clause runs without its own handler in force, over the condition descriptor, and StaleCall is an Error 2026-09-25 12:50:01 +07:00
aedd50c646 Merge branch 'master' into worktree-agent-ac750a5e83cab251b
# Conflicts:
#	lib/check.ml
2026-09-25 12:17:58 +07:00
1d219b526c Merge master into the typed class slots lane 2026-09-25 12:14:42 +07:00
73dfaacbc8 A condition type may name a parent and a handler for Error catches every built-in error, a signal passes one descriptor carrying its chain, name, sentence and site, and the break loop shows the sentence the runtime wrote 2026-09-25 12:05:13 +07:00
6e8cc52bc7 Merge branch 'master' into worktree-agent-ad097f67134ba0552 2026-09-25 12:02:48 +07:00
3892f1ed34 Merge master 2026-09-25 11:50:29 +07:00
c671bba490 (the T e) gives any expression its type, and an array literal nothing names is typed when its elements agree and a dyn vector when they mix 2026-09-25 11:47:28 +07:00
2ff1da88da A slice can be read-only: bytes-view answers a [const u8], and a store through one is refused at compile time 2026-09-25 11:43:41 +07:00
2682214499 A defclass slot may declare a type that every store into it checks, and set writes a declared slot 2026-09-25 11:40:08 +07:00
680c12e686 A restart frame carries where its clause is written, its :report sentence and whether a handler-case made it up, and the break loop shows the first two and hides the third 2026-09-25 11:35:12 +07:00
389e93ff09 A capturing fn's environment belongs to the collector, so a closure may outlive the frame that made it 2026-09-25 11:13:57 +07:00
e9e151e1a4 A match over an enum names its members as keywords and is refused when it misses one 2026-09-25 10:33:22 +07:00
11dc230974 A defn- is private to its module, and the module's own macros may call it 2026-09-25 08:57:20 +07:00
793b0eea9e A defn- is private to its module, the package's own macros may write calls to it, and the refusal names the fix 2026-09-25 08:55:37 +07:00
cb0be62087 An integer written at or above 2^63 is a u64 and nothing else, and vec-new reads a type from its argument only when the builtin is what was called 2026-09-25 07:43:24 +07:00
6fd456c0a2 A function declared with defn- is callable only from the files of its own package 2026-09-25 07:42:40 +07:00
aa629da2e5 vec-new and map-new take a type expression where they take a type, so (vec-new [u8]) makes a Vec of byte slices 2026-09-25 07:10:48 +07:00
57fe91f303 Five records become one, and every citation lands somewhere
FIX.org, NEXT.md, DISCUSS.org, docs/DISCUSS.md and the session handoff at the
root are one TODO.org now: 293 entries under seven subsystem headings, each
carrying an org keyword that says where it stands. A DONE entry is a few lines
saying what was decided and what that rules out; the reasoning that would not
compress — the embedding spike and the four reports the hand-written x86
backend was built from — moved into docs/BUILT.md instead, and its entries
point there in one line.

Every entry was checked against the tree before it got a keyword, and the
prose was wrong in both directions. Things the deleted files called open were
built: the first-evaluation stall, main being redefinable, macro parameter
lists, the type-limit constants, the array constructors, the byte fills,
inc/dec, the discard's fontification, the Emacs buffers, rt_die's _exit, the
backtrace surface, and the acceptance failure that could print and still exit
zero. Things they called done were not: the backend reports' no-plan buckets
had gone stale in the other direction, the value-dependent defvar was
superseded rather than built, and macro-expansion source locations are on an
unmerged lane, so that entry is NEXT and names the branch.

Every comment that cited one of the five by name now cites a heading that
exists, in TODO.org or in docs/BUILT.md. The session reports under
docs/handoffs/ keep naming the files they worked on, because rewriting them
would falsify what those sessions did; each carries a note saying where the
content went.
2026-09-21 21:05:48 +07:00
2572f0a537 An fn sees the locals it was written among, and Fn says so in its type
spec-memory.md's case 2, capture by value into a stack environment, and
the calling convention the author's rulings asked for.

    (Fn  [i32] i32)   captures; {code, env}; the common case
    (CFn [i32] i32)   the bare address; one word; cannot capture

A local of the enclosing function that an fn names is copied into a
struct the checker synthesises, held in a slot of that function's frame,
and the value carries its address; the lifted body reads the copies back
into named slots of its own, once, at entry.  So the name in the body
means what the local held at the instant the value was made --
fn-capture.flan changes the local through a pointer after the value
exists and the fn still answers with the old one.

Two types rather than a uniform environment parameter: "while it's dyn
first, static side should never have to pay the price for the existence
of the dyn side... if you fully opt out, for instance, using --no-gc
flag, then we should be operating under Odin/C semantics and never paying
any runtime costs."  The environment is declared by exactly the bodies an
(Fn ...) value can reach -- a lifted literal in an Fn position, every
handler clause, and the widening thunks -- and by nothing else.  An
ordinary defn emits the signature it always did; calc-me and fourteen
corpus programs were diffed to say so.

CFn, because the C carries information: a value with no environment is
the only kind that could ever cross to C, and under the --no-conditions
direction FIX.org records it becomes literally a C function pointer.  It
is not that today -- a declare cannot take a function type at all -- and
crossable's refusal says so where a reader would otherwise be misled.
Nobody needs CFn: Fn accepts everything, and the commonest reason to
reach for the narrow one is that a *named* function handed to an Fn pays
a hop through the widening thunk where a CFn is a direct call.

That thunk is one small function per distinct signature widened, which
reads the bare address back out of the environment and calls it.  The
cheaper trick -- the environment last, ignored by a body that never
declared it -- is legal under SysV and is a trap under wasm32's
call_indirect, which compares the signature at the call.  Every indirect
call is exactly typed now.

A handler clause captures the same way and is sound with nothing left
over: its frame is popped by the body that pushed it.  What is refused
there is a *store* into a captured name -- it is a copy, and writing to
it would leave the local as it was.

And the other half, which is what "non-escaping" means: a value carrying
an environment may be called, passed down and let-bound, and may not be
returned, stored, pointed at or pushed into a container.  A parameter of
type Fn is treated as one, which answers "passed to something that stores
it" with no interprocedural analysis -- the store is refused inside the
callee.  Everything of type CFn is clean for free, which is the second
thing having two types buys.  Every refusal names case 3, the environment
the collector owns.

Two pre-existing bugs fell out on the way.  A lifted fn asked for Fnval,
so `flan reload' on any function containing an fn literal died at llc
with an undefined cell; it takes Flanfn now, which is the choice a
handler clause always made.  And a redefinition module now carries its
own hidden copy of every thunk it names, which is the same bug shape
caught before it shipped.
2026-09-21 13:41:44 +07:00
2d8d9cd5a8 dotimes counts from where you say, and can count down
"Is there a way to do dotimes or a loop in reverse?" — the answer was a
hand-written let plus set. Now it is (dotimes [i 9 -1 -1]).

Three arities: [i n], [i start stop], [i start stop step]. The stop is
exclusive in all of them, so [i 0 n] is [i n] — one rule, not two — and a
negative step counts down, testing with > instead of <.

A literal step of 0 is refused where it is written. One that is only a value
cannot be, so the condition asks the sign first and 0 falls out of it as a
loop that runs no times: terminating and deterministic, and free, because a
literal step still emits the single comparison it always did.

Each bound is evaluated once, left to right, before the counter exists: the
start into the counter, the stop into the hidden slot it always had, the
step into one of its own unless it is a literal.

Still a special form, still a Let and a While with the step in the latch, so
neither backend learned anything — the new program prints the same thing
under --x86 and at -O0. load.ml's Form-level walk had to learn more than one
bound for the same reason parse.ml did; it is part of this feature and not a
bug that was sitting there, because before this a three-bound dotimes was a
parse error long before that walk could reach it.
2026-09-21 08:09:53 +07:00
a4c6b996ff def re-runs its initialiser, and defvar is renamed defonce
The trio the author decided on 2026-09-20 is now all built: def is CL's
defparameter — its initialiser runs on every daemon re-run, unguarded, so
an edited initialiser repaints the same storage on C-c C-c plus re-run —
defonce (Clojure's name for CL's defvar, per the author) initialises once
behind the .init~once. flag, and defconst stays the image.

One parse arm reads both forms; the difference is Ast.reinit, carried to
Tast.global's grerun. Emit.startup_plan gives a def no guard flag, and
Check.check_global lifts every def initialiser — zero and literal
included — into global/<n>, so the host's startup reaches it through the
function cell and a re-evaluated def swaps it (Session's def_inits;
Emit.redefinition declares the cell for a non-sibling target). The old
defvar spelling is refused with the rename and both compiling spellings,
and every program, test, doc and editor list is swept — except sand.flan,
the author's live WIP, whose seven defvar lines are flagged in FIX.org
and keep its three dependent tests red on this branch.
2026-09-21 07:12:04 +07:00
d4def945a9 An array is a value you can write, not a place you have to fill first
DISCUSS.org's "need a value-producing array constructor": the author
wanted grid filled with 255 as part of its declaration and could not
write it. (array n T) produces the zeroed array only, and dotimes is
Unit, so it can mutate a place that already exists but cannot be the
initialiser expression -- which has to produce the whole value in one
go. The grid was declared zeroed and filled in main instead.

Two forms, both expressions, both any rank:

  (array-fill [rows cols] 255)   every element that value
  (array-gen  [rows cols] cell)  every element (cell i j)

Spelled apart rather than one form dispatching on the third element's
type, because an array *of* function values is a thing to want and one
form would have to decide whether (array-fill [4] f) meant four copies
of f or four calls of it.

The dimensions are read in Parse, and that is the whole reason they are
recognised there: handed through as an ordinary call, [rows cols] is an
array literal of two names, and where those names are defconsts it is a
perfectly good two-element array of integers -- the wrong reading, and a
silent one. Read in Parse they are the same len the [n T] type spelling
takes, resolved by the same array_len, with one extra condition of their
own: the fill counts in i32 like every index in the language, so a
dimension no i32 can reach has no loop that could end.

The lowering is a loop over a slot, not an aggregate. Tast.Arr is the
node the backends have and both build it element by element from a list
as long as the array; a fill of [600 [800 u8]] is half a million
elements and there is no list to be had. So these bind the array to a
slot, zero it, run one While per dimension writing through Set of a
Pindex, and answer with the slot -- While, Set and Pindex, which is the
argument check_loop already makes for recur. Nothing new reaches a
backend and all three get the form with no edit. The value stays
value-like: the slot is the form's own, and the Local at the end copies
out the way any array-typed expression does.

Row-major is pinned, not incidental: the first dimension is the
outermost loop, and a generator that counts observes it. The fill value
and the generator value are each bound once before any loop starts, so
(array-fill [n] (next-id)) is one call and n copies of its answer.

What falls out for the defvar the note was written about, and neither
half is a carve-out:

  (defvar grid [rows [cols u8]] (array-fill [rows cols] 255))

is the spelling that works -- a typed global with a computed
initialiser, which is the startup-lifted path with the init-once guard
that defvar already had, so the fill runs once and the value survives a
re-run like any other computed one. The three-element spelling means
what the 2026-09-20 rule says it means: not a type, so a dyn global, and
a typed fixed array crosses into dyn only as a view of storage that
outlives the view. A freshly built array is a temporary, so it is
refused -- by the element rule where the elements are themselves an
array, by the lifetime rule where they are one of the three scalars a
view carries. Both refusals are the ones any other temporary gets.

The type an array-fill builds never goes through resolve, so resolve's
own guard is asked again where it is built: a fixed array of function
values would be zeroed, and a zeroed function value is a null pointer.
2026-09-20 18:34:52 +07:00
c1b18cb181 Two struct spellings the parser was never able to decide
A bare {.field v} had its refusal in Parse.expr, before any checking, so a
defn whose return type was the only place the struct's name appeared could
not build one. The refusal moves to Check: Parse builds an Ast.Bare out of
the same struct_fields the named form uses, and check_bare reads the type
name off the expectation and hands that very list to check_struct. ZII, the
unknown-field refusal and the duplicate-field refusal are therefore not
copies of the named form's rules but the named form's rules.

Braces at a dyn want are the dyn map literal and stay exactly that. A
.field-keyed brace was never part of that spelling, and at a dyn want it is
refused by name rather than given a second meaning.

(Cell 1 2) is the other half, and it is character-for-character an ordinary
call, so only the symbol table separates them. It is decided on the last arm
of named_call, after a local of function type, a generic and the function
table -- so a defclass constructor, which is a real defn, resolves above it
and is untouched. Arity is exact: ZII is what the braces do, and a positional
list cannot say which field it left out, so it is not allowed to leave one
out. The refusal names the first field it did not reach and points at the
spelling that does mean "zero the rest".

Both are gone before any backend sees them -- Tast.Make either way -- and the
three acceptance rows print the same lines to say so.
2026-09-20 18:12:45 +07:00
5a62770e52 Classes and generic functions, milestone 2's last item
A defclass is a named dyn map with a shape tag, and a generic function
dispatches on it two ways: CLOS's, where the dispatch value is the class
of the first argument, and Clojure's, where a body computes it. They are
one mechanism and not two — a class dispatcher is (class-of arg0) as the
dispatch function, which is what lets a method written for the class
point and one written for the value :point be the same branch.

    (defclass point [x y])
    (point 3 4)                 ; the constructor, positional
    (class-of p)                ; :point, or nil for anything else
    (defgeneric area [self] dyn)
    (defmethod area point [p] (* (get p :x) (get p :y)))
    (defmulti describe [x] dyn (get x :kind))
    (defmethod describe :square [s] ...)
    (defmethod describe :else [s] ...)

A slot is a key in the instance's own map, so get, put and has-key? are
how one is read and written and no operation was added for any of it.
What the class adds is the tag, and the tag lives in the object's header
rather than in a reserved entry — the queue's note said a reserved key
and this departs from it, because a key would be counted by len, walked
by the renderer and compared by equality, so every instance would answer
a length one larger than its slot count and print a key nobody wrote. A
header field cannot be reached by get or put at all, so no user key can
collide with it. It costs nothing: the map arm of flan_obj's union grows
to the size the view arm already had, and sizeof(flan_obj) is unchanged.
It needs no tracing either — the tag is an interned keyword entry, which
is immortal and is not a collector object.

The tag shows up in exactly three places: class-of answers it, equality
compares it (two instances of one class compare by their slots; an
instance and a plain map with the same entries do not, which is
Clojure's answer for a record beside a map), and both renderers print it
— #point{ :x 1 :y 2}, Clojure's own spelling.

None of the four forms reaches the checker. lib/classes.ml turns the
whole declaration list into ordinary defns at the top of build_program,
the way Shim.expand already turns a declare-c into a declare plus a
defn: a class becomes its constructor, a generic becomes one function
whose body binds the dispatch value and compares it down a chain, and a
method becomes a branch of that chain. It is a pass and not a macro
because a macro sees one form and the generic's body is not decidable
until every method is in hand — a method may be written above its
generic, below it, or arrive at a reload an hour later.

That last case is why the method bodies are inlined rather than lifted.
A generic is exactly one top-level name, so adding a method to a running
program is the ordinary redefinition of one function, through the cell
every call site already goes through. session.ml names the generic
alongside the method's own declaration name for that reason. The cost,
recorded rather than hidden: a method is not separately callable and is
not a frame of its own.

A dispatch that finds no method signals NoMethod, a prelude struct
carrying the generic's name and the dispatch value that missed. A
condition and not a trap, because a miss is something a program can be
written to answer, and handler-case around the call is the shape. Its
value field is dyn, the first condition here with one; the per-type
descriptor an item-2 struct carries is what the collector reaches it by.
No restart is established at the miss, which is BoundsError's decision
taken for BoundsError's reason.

Both backends, identically: the two new runtime entry points are
declared in emit.ml and the x86 backend needs nothing, since a dyn call
is a dyn call there. Deferred and written down in FIX.org: inheritance,
multi-argument dispatch, :before/:after/:around, named-slot
construction, unknown-slot checking, and computed dispatch values.
2026-09-20 15:36:13 +07:00
Joseph Ferano
9321547822 A defvar's third element decides: a type is the static, anything else is dyn 2026-09-20 15:00:15 +07:00
a251dca67f handler-case is a handler-bind plus a transfer, and nothing more
The unwinding handler, which spec-conditions.md named and left unwritten while
it asked whether the thing should be a macro over the two operators that were
already here. It should. (handler-case B [(T [c] A)]) is checked as
(restart-case (handler-bind [(T [c] (invoke-restart 'R c))] B) (R [c T] A))
with R a name the form makes up for itself, which is Common Lisp's own
definition of the operator and means neither backend needed a line.

What that buys is not economy, it is the correctness of the parts nobody can
see. The defers between the signal and the form run, and the allocator a
with-allocator rebound is put back, because a transfer already does both for
every frame it leaves. The body and every clause agree on one type, because a
restart-case's body and clauses already do, and a clause that disagrees is
refused with the same message an if with disagreeing arms gets. A condition no
clause lists installs no frame that matches it and goes on outward untouched.
A clause sees the establishing function's locals, which a handler-bind clause
cannot, because a restart clause runs where it was written.

The body comes first and the clauses after it, the opposite of handler-bind's
order: one reads as something put around a body and the other as a body with
answers hung off the end of it. The restart the two halves meet over is named
after the function and numbered within it, and it has to be unique per form,
because two nested handler-cases sharing a name would have the inner frame
shadow the outer one and land a condition at the wrong place.

The refusals name handler-case rather than the machinery underneath it, which
is why check_handler_bind and the restart clauses now take the word the reader
wrote. A break loop entered under a handler-case still lists the made-up
restart, and taking it there is refused loudly rather than answered wrongly;
hiding it would mean a field in a frame layout spelled out in three files.

The survey program runs the same under LLVM, at -O0 and under --x86: normal
completion, a caught condition, one nobody listed passing through with the body
carrying on, both defers on the way out, the two nestings against handler-bind,
a clause that signals and is caught outside the form it belongs to, and a
with-allocator whose restore is on the transfer path.
2026-09-19 20:53:31 +07:00
ab82e46119 Dyn maps, keywords and nil land: milestone 2's first item
The dyn runtime gets a map object and an interned keyword, alongside the
vec it already had. {:a 1 :b s} is a map literal wherever a struct
literal isn't — the parser tells the two apart by whether the first form
in the braces is a .field symbol — and a bracket literal builds the
runtime's own vec rather than a typed array wherever a dyn is wanted, which
is what lets a map literal's values nest arrays and maps freely. get, put,
len and has-key? all learn a dyn-map arm alongside the typed-map one they
already had, and (keyword s) builds the same interned value a :foo literal
does, for a name that only exists at run time. nil is now a literal, the
dyn absence value that get answers for a key a map does not hold.

On the runtime side, flan_dyn.c gets an OBJ_MAP that shares the vec's
storage arm and doubles its accounting, a linear-scan intern table for
keywords that makes equality an identity compare, and structural map
equality by lookup rather than position. The marker traces a map's
interleaved keys and values the same way it already traced a vec.

edn/read and its callers move off the old (Option Value) union entirely:
a document is plain dyn now, sets are dyn maps to true, and arena-edn.flan
is retired along with the union it demonstrated. The acceptance suite's
edn-read and json rows were recaptured against the new shape, and a new
dyn-map.flan program exercises the map and keyword operations end to end,
including a 200k-iteration churn loop against a rooted map that runs
GC for real, across the LLVM, -O0 and x86 rows, and under the sanitizer.

Keywords are dyn everywhere an enum isn't expected, which changed what a
couple of existing checker tests actually see refused; both were updated
to the sentence the checker gives now rather than the one it used to.
2026-09-19 19:44:56 +07:00