9 Commits

Author SHA1 Message Date
e992491799 The colon belongs to keys; the prose, the page and the sweep agree now
web/index.html's Flan blocks convert and its output blocks do not, which
is the same split render.ml makes: the printed form keeps the colon until
the Emacs inspector that reads it moves too. Same in BUILT.md.

plan.org, spec-conditions.md and spec-memory.md carried struct literals in
the old spelling and now do not.

NEXT.md decision 6 is struck, and batch item 2 with it, naming what to run
at merge. BUILT.md says why the colon belongs to keys -- mostly that a map
literal wants {:key value}, and two literals sharing one syntax would have
left the reader asking the checker which it was looking at.

The sweep was not idempotent and is now: {.k :hi} -- a field already
converted, holding an enum member -- read as a destructuring pair on a
second run and ate the member. A re-run over a lane's files would have
corrupted them silently, which is exactly what the tool exists to do
safely.
2026-09-12 15:00:34 +07:00
127aef75f5 Say in plan.org that write-stdout is all that is under print 2026-09-12 05:38:57 +07:00
93231e8c9e println, the structural printer, shared with the REPL
session.ml already had this: a compile-time walk over a Tast type that
emits the calls to print a value of it, handling every concrete type the
language has. It was dev-build-only and went to flan_dev_emit, and
prelude.ml justified the per-type print-* functions by saying a real
println had to wait for milestone 5 and generics. It did not. plan.org
specifies println as compiler-provided and per concrete type, which is
not overloading: there is nothing to dispatch on at run time and no
user-supplied printer to choose between, so no type variables appear.

The walk moves to render.ml, parameterised on an emitter and a slot
allocator. The emitter is five functions rather than five extern names
because the two sides are not both extern calls -- the REPL's are, and
stdout's compose a conversion with a write. The slot allocator differs
too: the REPL builds a thunk's frame, println takes slots from the
enclosing function being checked, once per call site.

Two runtime shims, both only reachable from the walk. flan_u64_to_bytes,
because routing u64 through the signed printer makes 0xFFFF...F read as
-1, which is the one way println could disagree with the REPL about a
value both can hold. flan_escape_bytes, so a string nested in a printed
structure is quoted and escaped -- same table as flan_dev_emit_str, noted
in both, because the REPL and println must not disagree about what a
struct looks like.

A string at top level prints raw and nested prints quoted. Not a conflict:
(println "hello") has to print hello, and a struct's string field has to
be distinguishable from the punctuation around it. The split is top-level
vs nested, so it lives in check.ml and not in the walk.

Found on the way: a field of an Option had no gep in emit.ml, so the
walk's Option arm had never run -- the REPL would have failed on one too.
Option is { i8, T } with no declared name, so its layout is now spelled
out. Nothing in the surface language reaches a field of an Option; the
printer does, to read the tag without unwrapping a None.

The print-* functions stay. They print without a newline, which println
cannot express -- slices.flan's show prints elements separated by spaces
-- and they are raw where print is structural.

println.flan covers every arm at -O0 and -O2: the u64, the raw/quoted
split, both Option arms, the depth and span caps, and the slice arm's
loop twice over plus once inside a dotimes, which is where per-call-site
slot allocation would show if it were per-iteration.
2026-09-12 04:55:42 +07:00
5ea0bcae84 Remove nth, the alias that was not one
nth and at were documented as the same operation, and as reads they were:
check.ml matched "at" | "nth" in one arm. But a place is recovered in two
other spots -- parse.ml for (set ...) and place_of_expr for (addr ...) --
and both match only Sym "at". So (set (nth a i) x) and (addr (nth a i))
were refused while the at forms worked.

Two names said to be identical that disagree about writing is worse than
one name, and the asymmetry is not worth fixing in three places to keep a
synonym. at is the indexing operation; nth is gone.

The six call sites were all reads, so they rewrite directly. get/put stay
the Map pair: get returns (Option V) and is deliberately not a place.

nth-gone.flan pins the removal -- it has to fail as a name nobody defined,
not quietly resolve to at again.

destructure~nth is compiler-generated and unrelated.
2026-09-12 04:46:41 +07:00
59e9392ec8 What the class decision changes for the next session
plan.org's managed classes arrived after NEXT.md's handoff was written, so a
session reading the plan cold would take them as the next task. They are not:
plan.org's own last line on them says nothing until struct, Handle and reload
semantics work, and that belongs where the next task is named.

Three findings from reviewing it that are not in plan.org. A generic function is
a cell whose body is a dispatch table - adding a method later is the same
problem the indirection cells already solve, so the expensive half of classes is
built. Migration has to enumerate live instances, which makes the pool behind a
generational Handle the only one of the three storage options that obviously
supports it, rather than a free choice. And a numbered layout has to stay
resolvable for migrate to dispatch on, which is the same retention rule as
nothing is ever dlclosed.

Also records the open question the class facility raises for conditions: whether
a condition may be a class, what the hierarchy would buy, and the three costs -
allocation on the signal path being the serious one. It wants answering before
handler-case, since it decides whether handler matching has one path or two.

Plus three nits in the new prose: float/int are not Flan type names, the place
syntax was dotted, and the migration example set a slot the class did not have.
The tag-word sentence said any was the only place one is paid, which Error and
now a class instance both make false.
2026-09-11 17:35:06 +07:00
ac8d31ee65 Managed classes, planned and deliberately separate from structs
The struct/class split, written down before anything is built on it. A struct
stays a fixed-layout value with C's layout, which is what keeps the FFI, SoA and
wasm stories intact; a class is a separate kind with identity, metadata and an
implementation-defined representation, for the long-lived gameplay objects that
want to change shape while the program is running.

The tagline loses "no GC" for "no mandatory GC", because a small collector
confined to class instances is now an option rather than a contradiction. CLOS
goes from a flat non-goal to a bounded one: the metaclasses, method combination
and arbitrary change-class are out; exact-class single dispatch and an explicit
frame-boundary migration are in.

Migration is eager and explicit rather than CLOS's lazy-on-access, which would
put a check on every slot read. Class identity is stable and layouts are
numbered, the same shape as the function versions the hot reload section grew.

Nothing is frozen and nothing is to be built until struct, Handle and reload
semantics are working.
2026-09-11 17:34:13 +07:00
943561e765 A map entry is not a place
spec-memory.md drops (set (get m k) v) from the assignable forms: a map has an
upsert of its own, put, which either inserts or replaces, so there is no store
into a lookup - and an absent entry has no location to store into anyway.

The compiler still parsed it into an Ast.Pkey and refused it downstream as
unimplemented, milestone 6, which is the wrong reason for something that is
never arriving. The place form is gone from ast, tast, load, check and emit,
and the parser refuses the shape where it is written, with the reason and a
pointer to put.
2026-09-11 12:23:10 +07:00
5a8c2327c6 Debugging: DAP for the machine layer, nREPL for the language layer
Do not extend nREPL with registers -- the protocol is trivial, the
implementation is a whole debugger. Emit DWARF from the LLVM backend
and use lldb-dap with dape; no adapter is written. C layout with no
object headers means DWARF describes Flan structs exactly, so lldb
needs no plugin or formatters.

Reload mostly works with DAP for free: lldb watches the loader
rendezvous and re-resolves breakpoint specs against new modules. The
gotcha is our own never-unload rule -- N redefinitions leave N copies
all claiming the same source line, so one breakpoint resolves to N
locations. Needs per-generation DWARF source identity plus a reload
agent that disables superseded locations.

Cuts the instrumentation-based step debugger, the most expensive piece
of milestone 8. Knock-on: &env stops being urgent, and the shadow stack
shrinks to serving nREPL backtraces and restart enumeration.
2026-09-10 16:04:56 +07:00
e9cdbb321b Lisp based flan 2026-09-10 14:40:34 +07:00