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.
The author: "I think I prefer length over len, because then I'll use len as
the variable name". One arm in check.ml, one row in the table beside it, and
every (len x) in lib, test, examples, vendor, spike, docs, web, emacs,
plan.org and NEXT.md rewritten.
Shadowing and builtin/ had already taken most of the sting out: a (defn len
...) was legal and won in its own file, and builtin/len reached past it. What
was left is that len was still a builtin — the defn earned a warning, and a
wrapper had to say builtin/ at every inner call. Now there is nothing under
the short name: len is an ordinary identifier in every position, which is
what (let [len (length xs)] ...) wants.
length takes over as shadowing's worked example rather than the feature
losing one. shadow-builtin.flan, builtin-qualified.flan, pkgs/shadowed and the
builtin/ rows in test_flan move to it and go on testing shadowing.
A call to a len nothing defines is answered where an unknown function is,
after every table and after the shadowing guard, so a program with its own len
never reaches it. The sentence is said rather than guessed at — len and length
are three edits apart and the did-you-mean's net is one — and the call is
written back out through spell_arg, as-slice's spelling lifted out of it and
now shared, so what is printed compiles.
sand.flan:33 still calls the old name and is the author's to change; until it
does, test_acceptance and test_session abort there. Both were run green
against a copy with that one line changed. FIX.org says so.
Rebased onto dev-loop. Three conflicts were additive and both sides are
kept: FIX.org's two appended sections, want_map's diagnostics argument
against the class_sync inserted beside it, and test_dev.ml's agent-socket
block against this lane's migration block, whose comment no longer says
"the block above" now that something sits between.
The fourth is the one the auto-merge hid. flan_dyn_class_def's argument
check was written against the pre-diagnostics trap1 and merged clean into
a tree where trap1 takes a location first, so the class name would have
been read as a length. dune build does not compile flan_dyn.c, so the
green build said nothing; caught by compiling a program.
Two pins in test_session.ml asserted "flan_dyn_class_def" against the IR
text, which every module contains because emit.ml declares every runtime
entry point in all of them. Both now assert the call and the packed slot
list. Checked by mutation: with the thunk suppressed the old needles pass
and the new ones fail, along with the daemon's slot count.
Also disclosed: say_render is the second raw reader beside render, and
neither syncs, so a stale instance shows its old slots in the inspector
until something touches it. That is the editor-facing consequence of
keeping the printers printers, and it is now in the runtime comment and in
FIX.org rather than left to be met. And the stale-caller walk says in as
many words that it is a tripwire, unreachable on purpose, not a filter to
be tidied away.
CLHS 4.3.6's update protocol, minus the user hook, on the dyn side's
defclass. Redefining a class used to be silent: a class is sugar for a
constructor defn, so the edit replaced a body and the instances already in
the program kept their old keys for ever.
Three pieces. A registry in flan_dyn.c holding each class's current slot
list and a generation, made only of interned kw_entry pointers so the
collector has nothing to trace in it and no root to push for it. A uint32
generation on the instance, fitted into the padding kind and mark leave in
front of len's alignment — sizeof(flan_obj) is 48 with it and was 48
without, and flan_dyn_obj_size is there so a later field that moves it
fails a test. And a registration thunk per reload, run by the agent
through flan_reload_call after the module's bodies are published: it has
to be a thunk, because the case this exists for is a class redefined and
not constructed.
Migration is lazy, at want_map, len's map arm and dyn_equal's. Slots kept
by name, gained slots nil, dropped slots gone, identity preserved, entries
rebuilt in the class's order so a migrated instance is indistinguishable
from a fresh one. Equality migrates both operands first, so it is over the
class as it is now.
The session had to stop refusing the constructor's signature change, and
does so only for a defclass and only when no compiled caller is left
behind. The checker gets there first in practice; the walk in eval holds
the reason locally rather than inheriting it.
The registry is advisory: a class instance is an open map, so a key a raw
put wrote that the class never declared is dropped by the next migration.
FIX.org says that plainly rather than pretending enforcement.
F1 was the blocker and it was the worst kind of fault this pass can have: the
condition message told the reader to write (not= x 0), and not= does not
exist — the operator is !=. Applying the compiler's own advice got 'unknown
function not= — did you mean not?'. Both branches say != now, and all three
— the named form, the float zero, and the unnamed one — were checked by
compiling the sentence the compiler prints.
F5: a typo of a declared capitalised name got the generics lecture. (Piont 1
2) with Point declared was told that a capitalised name given type arguments
is milestone 5 work, which is a confident answer about a feature nobody was
reaching for. The did-you-mean runs first and, for a capitalised head only,
asks the type tables as well; the generics sentence is left for a head that
resembles nothing.
F2: flan_dyn_cast_kind had the site live and passed NULL on the trapping
path — the one entry point on this side that had a location and threw it
away. The acceptance row now pins the prefix it prints.
F3: the case-typo row used (data ...), which is not a top-level form, so it
refused as an unknown top-level form and the needle 'unknown' matched that
rather than the rule. Rewritten with defdata, and as a pair: a capitalised
head gets no accessor advice, a lowercase one does. Both halves were checked
to fail when perturbed.
F4: an end-to-end pin for the headline. programs/dyn-trap-site.flan is
compiled, run, and its stderr read for the file:line:col in front of the
sentence, on both backends and at -O0. Proven live: three failures when the
expected line is wrong.
F8: usize and size_t stay off the foreign-spelling list, and the comment now
says why — the honest answer is pointer-width, which is u64 here and u32 on
wasm32, and a tree that builds both cannot name one of them.
F10 pins the fourth dot shape. F6 moves the not-reached reasons out of the
commit bodies and into FIX.org, where they can be read without git.
The dyn arithmetic and ordering entry points printed their sentence with no
file, no line and no column, which in a dynamic-first language is the type
error arriving from nowhere. flan_rt.c's bounds and arithmetic traps have
taken an emitter-threaded (loc, loclen) pair since they were written, and
[flan_dyn_cast_kind] is the fresh precedent on the dyn side; this is the same
pair, threaded through [arith], [want_nums] and [order] to the five
arithmetic and four ordering entry points. [eq] never traps and takes none.
The three trap printers take the pair and print nothing for a NULL loc, so
every other call site in the file — and test/dyn_ops.c, which calls the
runtime directly and has no source position — keeps its sentence byte for
byte. [trap_oom] is left alone: it is reached from [gc_alloc], which has no
site to be given and would have had to grow one on every allocation path in
the file for no reader's benefit.
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.
flan.abi.require was spelled by hand in both backends, which is the one
job Mangle has. Moved; emit and x86 produce byte-identical output on the
reload path either way.
Four comments in the dyn-cast code asserted things that are not true.
The warning's location prefix now reads like every other loc-bearing
runtime diagnostic instead of inventing a shape. widen's contract says
what cast_dyn actually does with it. The thread-safety note names the
torn {ptr,len} overread rather than a duplicated line, and says why no
lock. The site table's borrowed loc pointer names what keeps it valid.
The memory op's note claimed a completeness it does not have: dyn push
and put may allocate and are deliberately silent. Said so, in the note,
in the classifier, and in FIX.org where the decision belongs.
The documented flycheck form only matched warnings, so a real error
made it say the checker returned non-zero and found nothing.
flan-clear-memory cleared one buffer where the toggle clears all.
Two comments claimed test/dyn_ops.c calls every function flan_dyn.h
declares; six are declared and never called there.
flan_dyn_stub.c's deadness is written into FIX.org for the author to
decide on. Not deleted here.