A let-bound local printed as s0 under lldb. Parameters were fine, because
the driver recovered their names from the AST and handed them down in
pnames; everything else was a slot index, since Check knew the name in its
scope list and dropped it at allocation.
Tast.fn now carries snames beside slots, Check fills it in at bind, and
Emit prefers it over pnames. A slot the compiler invented keeps s<index>:
fresh_slot takes the name as an optional argument, so dotimes' hidden
bound and the pair min and max evaluate into say nothing and get None
without any of their call sites changing. Naming those something plausible
would put a variable in the debugger that is not in the file.
Shadowing needed deciding rather than assuming. Every DILocalVariable is
scoped to the subprogram — the typed IR has no block structure to build a
DILexicalBlock from — so two slots called v landed in one flat scope, and
lldb answered p v with the outer one while the body computed with the
inner, which it did not list at all. A debugger confident and wrong is the
one outcome worse than s0, so a repeat of a name already bound in this
function gets a ~2 suffix: ~ is the reader's delimiter and cannot occur in
a source symbol, so v~2 is unambiguous and visibly the compiler's. It is a
way of not lying, not a way of being right; scoping properly means a
lexical block per Let and the declares moved out of the entry block.
(lldb) breakpoint set --file debug.flan --line 20
(lldb) frame variable
(Cell *) c = 0x00007fffffffd970
(int) n = 41
(int) bump = 42
The test breaks after the binding on purpose. A name breakpoint stops on
the function's first line, before the let has stored anything, and a
variable is nominally in scope from entry — so the name is checked there
and the value only where it means something.
A stopped program was said not to have installed what was delivered. The
commonest way to stop is to install a body and have it error, so that
asserted non-installation in precisely the case where the body is running;
the daemon cannot read a cell back either way, and now says that. What is
certain is only that nothing further installs until it resumes.
And the source location came from the session rather than from the build it
was showing. Session.eval replaces the checked program the moment a form
checks — before the build, before delivery — so an evaluation that checked
and then failed to build left a reply showing the host's code, saying
nothing had been delivered, and pointing at a buffer whose code never
landed. A daemon whose llc is [false] is the whole test.
They came back as "unknown function break", which reads as a typo rather than
as a missing feature. plan.org's loop story is settled as imperative while/for
with break, continue and return, so these are named, planned and absent - and
they alter control flow, which is the first thing the house rule says must be
recognised explicitly rather than left to fall through to a call.
Found by the lane writing the documentation site, which had to describe the
loop forms and discovered two of them were neither implemented nor refused.
encode-rune! says nothing is written when it answers None, and every None case
in the table passed that claim without testing it: an encoder that lays the
lead byte down and only then notices the buffer is short returns None exactly
as a correct one does. So a known byte goes into scratch, a refused encoding
is asked for, and the byte is read back. Storing before the length test turns
the line from 65 -1 65 -1 65 into 65 -1 0 -1 0.
The read-only claim beside lower-ascii was reasoned from the emitted linkage
rather than observed, and observing it was worse than the guess. With
(set (at (bytes "Hi") 0) \h): at -O0 the store is emitted against the constant
and the program takes SIGSEGV; at -O2 LLVM deletes it as undefined behaviour
and the program prints "Hi" and exits 0. The same source either dies or
silently does nothing depending on a flag. The comment now says that instead
of predicting a segfault.
The parser decides "return type or first body form?" from the set of type
names the file declares, and an import is resolved after parsing - so a
package's structs cannot be in that set by construction. (defn mk [] rl/Vector2
...) therefore read the return type as the body and failed with "unknown name
rl/Vector2", which names the symptom and not the cause.
The signal is the alias plus the capital, and both halves are needed. An alias
is syntactically obvious and the same pre-pass collects it. A bare capitalised
symbol is never a value in this language - a struct or union constructor is
(Name {...}), a List, and an enum member is a keyword - so the hazard the
surrounding comment warns about, a body form eaten as a return type, has no
form of this shape to eat. A lowercase qualified name stays an expression,
which is what rl/get-color has to be.
Found by the raylib lane, which hit it on rl/Vector2 and reported it rather
than reaching into a file it did not own.
An editor could see the IR of a whole file and nothing at all of what the
running process is executing. The daemon built every module it sent, so
objdump on the right object is the disassembly and the retained .ll is the
IR; the only hard part is which module owns a name after N reloads, and a
table filled on accepted delivery answers it.
What it deliberately does not claim is that the code shown is installed.
The agent takes a module path and answers ok when it has queued one; there
is no verb that reads a cell back, so :basis spells out which of the three
things is true — the host's body, still certain because nothing was ever
delivered; queued and awaiting a frame boundary; or queued while the
program is stopped and therefore certainly not installed yet.
From SBCL: offsets from the function's start rather than addresses into a
file, and L0.. labels on branch targets. Not source interleaving, which
needs line tables this build does not emit, so the reply says so.
A module's .ll is deleted by the build and the host's lives in a working
directory named after the process rather than the module, so ten reloads
in there is nothing left on disk that says what a given function was
compiled from. The daemon owns the build and is the only thing that could
have kept it, so it keeps it: one .ll beside each .so, and a table from
function name to the last module that carried a body for it.
Odin's core/strings and all of core/fmt take an allocator; core/unicode/utf8
does not, because decoding is classification and every answer is a number.
That line is where the port stops, and the refusals at the foot of the file
say so by name rather than leaving a caller to find out.
The accept_sizes table becomes a cond over the lead byte. Its four awkward
rows are the ones a hand-written decoder gets wrong one at a time, so they are
written out: 0xc0/0xc1 lead nothing, 0xe0 and 0xf0 have a raised second-byte
floor against overlongs, 0xed has a lowered ceiling against the surrogates.
Two divergences from Odin, both the parse-i64 argument again. A malformed
sequence carries ok:false instead of decoding to U+FFFD, which is a real code
point a caller cannot tell from a failure; and encode-rune! answers None
rather than silently substituting U+FFFD for a rune it was not given. Width
stays 1 on a bad byte, which is Odin's rule and load-bearing: every loop here
advances by it, and a 0 would hang rather than answer wrong.
split cannot return a sequence it would have to own, so the cursor is what
survives. It follows the allocating strings.split rather than Odin's own
iterator, which drops a trailing empty field and disagrees with it.
Case conversion is byte-wise and not in place: a literal is emitted into
read-only memory, so lowering (bytes "Hi") would type check and segfault.
Two ways to write a match over an enum and two different refusals, neither
of them true. (match k :lo ...) died in the parser with "expected a pattern,
found :hi" — which arm it named depended on cons evaluation order, and it
never mentioned enums. (match k lo ...) died in the checker blaming milestone
2, which is not what stands in the way.
What stands in the way is worth writing down, because the feature is close.
An enum is an i32 at run time and its members are all known, so the arms are
a chain of (= k :member) and the exhaustiveness check falls out of env.enums
— a desugaring, no new IR node, the same shape as everything else this lane
landed. What is missing is a case in Ast.pattern for a keyword, and load.ml
matches that type exhaustively with no wildcard, so the variant cannot be
added from a session that does not own the file. One line, for whoever does.
That is also why destructuring went through a call to an unspellable name
instead: a name in call position is an open namespace check.ml already owns,
whereas tagging Pctor with ":lo" would put a second meaning into a field
another file destructures as a constructor.
The struct-tail case in the acceptance program is unrelated housekeeping: the
corpus slices arrays of i32, u8 and f32 and nothing wider, so nothing else
proves the desugared (slice xs n (len xs)) gets a struct's stride right.
--debug is a third flag beside --dev and the optimisation level because it
answers a third question. --dev is "can I redefine this while it runs";
--debug is "can I stop it and read it". Either is useful without the other,
and a REPL session that is not being stepped should not pay for DWARF.
Not implied by -O0 in particular, for a reason already written down in this
file: the acceptance table runs the same programs at -O0 and -O2 to compare
the emitted IR against what mem2reg makes of it. If -O0 pulled in debug info,
every one of those comparisons would be against a different module.
It does imply -O0 downwards, and sets it. The whole mechanism is an
llvm.dbg.declare hanging off an alloca, and mem2reg deletes the alloca.
Refused for wasm32 by name. The member offsets in the DWARF are computed for
the host — ptr is 8 bytes — and wasm32's pointer is 4, so a slice's len sits
at byte 8 there and byte 16 here. Emitting the host numbers would hand a
debugger a confident wrong answer for every slice and every struct holding
one, which is the exact failure this project keeps meeting at the FFI
boundary. Silence would be worse than the refusal.
-g reaches the C compiles too, and joins compile_c's digest key with it, or
an object built without it would be served to a build that asked for it.
plan.org says Flan is Clojure's brackets and a small slice of its API, and
(let [{:keys [x y]} p] ...) is one of the most-used parts of that surface.
A struct is Flan's map, so {:keys [x y]} and {inner :field} read fields off
one; [a b] and [a b & rest] read a fixed array.
It desugars in parse.ml into the Let bindings and Field accesses that already
exist — the same trade dotimes makes. Ast.binding carries a name and nothing
else, so nothing downstream learns that a pattern exists: not Load's renaming,
not Check, not a backend. That is not only taste. Load matches Ast.pattern
exhaustively and Shim builds Ast.binding literally, and neither file is
editable from here, so an AST variant was never on the table.
The value goes into a temporary first. A pattern over a call must call it
once, and (let [{:keys [p]} p] ...) must read the old p rather than the one
it is halfway through rebinding. The temporaries are named with a ~, which
the reader treats as a delimiter, so no source symbol can collide with one.
The arity is the one thing the parser cannot settle — it is a type — so the
pattern's shape travels to check.ml as destructure~nth, which knows how many
elements the value has and lowers to an ordinary at.
Every Tast node carries a Loc and nothing ever used one outside an error
message, so a Flan program under a debugger was a wall of addresses. This
emits DWARF for them.
The reason it is a few hundred lines and not a few thousand is the layout.
A Flan struct is its C struct, every slot is an alloca and there are no tag
words, so there is nothing to describe *about Flan* — DW_LANG_C99 and the
machine types are the honest answer, and lldb's own C support is then exactly
right for a Flan value.
Two things are load-bearing and neither is obvious:
Debug Info Version in llvm.module.flags. Without it LLVM drops every scrap of
debug metadata with no diagnostic at all, so the build succeeds and the
debugger shows nothing and there is no thread to pull.
A !dbg on every instruction, not only the ones that want a line. The verifier
rejects a call without a location inside a function that has debug info, and
this file emits calls from a dozen places — the bounds failure, the handler
push and pop, the transfer guards — none of which would have remembered to
ask. So the location lives on the per-function state and `ins` appends it.
The member offsets are computed here rather than handed to LLVM, which is the
one place in this backend that happens and so the one place a layout bug can
hide. !DIDerivedType takes offset: as an integer literal; the ptrtoint-of-gep
form this file uses elsewhere for a size is not accepted in metadata. The
acceptance test therefore checks each one against LLVM's own getelementptr
answer for the same struct type, not against a table written by the same hand.
Local names are the gap. The typed IR refers to slots by index and records no
names — Check has them and drops them — so a parameter gets its source name,
recovered by the driver from declarations already in hand, and everything else
gets s<index>, which is the slot it actually is. Closing that means Tast
carrying the name.
The transient marker said nothing outside the module points into it once the
call returns - true of its text, silent about its data. A string literal is
emitted into the evaluating module's own image and an expression may store one
anywhere: C-x C-e on (set msg "tuned") left a program global pointing into the
mapping the agent was about to drop. The next thunk can be mapped at the same
address, so what comes back is silent garbage rather than a fault, and nothing
in the compiler refused it.
The third condition is that the module emitted no string constants. Then there
is nothing in its image anyone could still be pointing at. One that did keeps
its mapping, which costs a page and is the bargain every redefinition already
makes.
Found by reading jank, which has met the neighbouring hazard from the other
side: its notes are explicit that nothing is ever unloaded, and the one place
Flan makes an exception is the one place the rule had a hole.
vendor/raylib has no C in it any more: shim.c is deleted and its 84 wrappers
are emitted from declare-c, which names the library's function in the library's
own signature. The reason the shim exists is unchanged - a small struct's
calling convention is a per-target classification and clang reproduces it for
free - but writing it by hand has stopped.
declare-c is a second form rather than a change to declare, because the two make
opposite claims about the same shape: (declare start-raw [path string] ...) says
the symbol takes ptr+len, and (declare-c init-window [... title string] ...)
says it takes a NUL-terminated char*. No structural rule separates them, so the
author says which.
The merge needed two fixes that neither lane could have found alone.
Load's uses-walker matches decl_kind exhaustively and did not know DeclareC, so
the reachability work and the generator did not compile together.
And the generated C is now emitted in parts keyed by the wrapper's own C symbol,
not as one translation unit. Reach.link drops the bindings nothing reachable
calls; a single TU holding every wrapper referenced every raylib symbol, so
sand-headless - which deliberately links no libraylib, and is the reason Reach
exists - failed at the link with undefined references to GetTime and its
neighbours. The first attempt keyed the parts by Flan name and broke the other
way, dropping a wrapper that was called: the flattened declaration is named
foo-c when a Flan wrapper is generated over it and foo when none is needed, so
the Flan name is not one thing. The wrapper's C symbol is what the declaration
binds in both branches.
Worth recording how close that came to passing: the acceptance suite died with
an exception rather than printing FAIL, so a grep for failures counted zero and
the suite looked green. Only the count of reporting suites - ten where there had
been eleven - showed it.
Two gaps in what was claimed. The first is prose: "the typedef follows the
defstruct" answers field order and field types but says nothing about
padding, which reads like the remaining hazard. It is not one. Every field
type the generator admits has the same layout under LLVM as under C, and
emit.ml writes no datalayout, so clang applies the target's own rules to
both halves; everything where they could diverge — an array, a slice, an
Option, a map, a union — is already refused at the field.
The second is real. The flattened declaration's name is invented by
appending -c, so a hand-written foo-c beside (declare-c foo ...) came out
as the checker complaining that a name not in the file was declared twice.
Refused now where it happens, naming both and saying to rename one.
84 hand-written C wrappers is the shape of a job the compiler should be
doing. The reason the shim exists is unchanged and is not negotiable: a
small aggregate's calling convention is a per-target classification, not
part of its layout, and reproducing x86-64, arm64 and wasm32 inside
emit.ml is three classifiers to keep correct forever, where a mistake
reads as a field full of garbage rather than as a link error. clang does
it, per target, for free. So the C stays; the typing of it stops.
declare-c names the library's own function in the library's own
signature, and Shim emits the typedefs, the extern prototype, the
flattening wrapper and the flattened declaration the Flan side calls.
It is a second form rather than a change to declare because no
structural rule can separate them: (declare start-raw [path string] i32
"flan_agent_start") means the symbol takes ptr+len, and (declare-c
init-window [w i32 h i32 title string] "InitWindow") means it takes a
NUL-terminated char *. Same shape, opposite claims. declare is
untouched, so sqrtf and vendor/agent keep working unedited.
The generated C rides on Tast.program rather than beside it, so the CLI,
the REPL and the acceptance table all carry it without being told about
it. `flan shim` prints it, because a wrong binding is wrong in a wrapper
that is otherwise on no disk anywhere.
Clojure's backtick, tilde and tilde-at rather than Common Lisp's comma forms:
is_delimiter already treats a comma as whitespace and every binding vector in
the corpus assumes it, so freeing the comma would rewrite more of the language
than macros are worth. They read as (quasiquote x), (unquote x) and
(unquote-splicing x), the way 'x already reads as (quote x) - the reader stays
dumb and the meaning is resolved later.
The backtick previously read as an ordinary symbol character, which is exactly
the failure the reader's own header warns about for the apostrophe. Both sigils
are delimiters now, so a~b is two things and can never be one name.
defmacro validates its shape before refusing, because a malformed one and a
well-formed one are different mistakes and deserve different sentences. The
three new reader names are refused by name too, or they would fall through to
Call and come back as unknown name quasiquote. unquote outside a quasiquote is
refused as a mistake rather than as a milestone, since the reader cannot know
where it is.
Nothing is stored: no Ast.Defmacro and no macro table. A new decl variant would
have forced edits to four files other agents hold this session, and a
process-global registry spanning the prelude parse, the package parses and
hundreds of test snippets would make results order-dependent. The storage shape
is the expander author's first decision anyway.
The design note records what the expander needs, and the blocker worth knowing:
a macro is [Form] -> Form, so Form has to be a Flan union whose layout the
compiler and the loaded macro agree on exactly, and union values are milestone
6.
Found by a read-only audit of emit.ml's failwith sites, each of which is a claim
that the checker guarantees something. Three of those claims were false, and
every one failed in the shape NEXT.md calls the worst available: type checks,
then dies with no source location.
An enum comparison is lowered now rather than refused. Types.is_comparable
already admits an enum, so the checker was stating an intent the backend never
honoured - (= k :a) is the first thing anyone writes with an enum, and it raised
Failure("comparison on K"). An enum is an i32 at run time, so all six
operators are an icmp. Signed, because (defenum K [a -1]) is accepted and an
unsigned compare would call -1 the largest member.
A union in a type position is refused instead. Constructing a union value and
reading a field of one were already refused, so nothing could ever be done with
such a value - only the declaration got through, and it reached clang as a
reference to an undefined %"U", which is a link error naming an emitted symbol
with the source location long gone.
A function type annotation is refused too. The function *value* was refused
where it is written; the annotation was refused nowhere, so (defn f [g (Fn []
i32)]) died with "no layout for". It now sits beside the Map line directly
above it, which is the same shape of not-yet.
The audit also found the sentence that covered the last two: NEXT.md and
check.ml's header both claim unions and function values are rejected by name.
That is true of values and false of types, which is exactly the gap the two
findings lived in.
(defmacro m [x] x) answered "unknown top-level form (defmacro ...)" —
refused, but not by name and with no reason, because the refusal list
only covered expressions. Now it checks the shape and then refuses,
which are two different mistakes and get two different reasons: a
defmacro with no body is a typo, a defmacro with a body is a feature
that is not here.
The reader's new sigils made this urgent rather than tidy. quasiquote,
unquote and unquote-splicing are now real heads arriving at the parser,
and without a case each they would fall through to Call and come back
from the checker as "unknown name quasiquote" — which tells you nothing
about what is missing. unquote and unquote-splicing are refused as
mistakes rather than as milestones: they mean nothing outside a
quasiquote and the reader cannot notice, because it does not track
where it is.
gensym is neither a reader token nor a special form — it is a function a
macro body calls while the macro runs, and there is nowhere for it to
run. Refused by name so it does not arrive as an unknown one.
package_of matched the file being edited against a package's directory, which
is right for every package that is one — and answers "not a package" for one
that is a single file, because the file's directory is not the file. A form
typed into sand.flan with the headless driver running would have spliced as a
bare step, the evaluation would have said ok, and the program would have gone
on calling the step it already had. That is the exact silent failure the
function exists to prevent, so it now matches the file too.
`(a b) came back as the unknown name "`" — precisely the failure the
reader's own header warns about for the apostrophe, one sigil over and
still open. Same fix: the sigil reads as a wrapper and the reader stays
dumb about what it means.
Clojure's ` ~ ~@ rather than Common Lisp's ` , ,@ because a comma is
already whitespace here, and every binding vector in the corpus is
written assuming that. Changing is_delim to free up the comma would
rewrite more of the language than macros are worth.
Backtick and tilde join is_delimiter so that a~b is two things and can
never be one name. No symbol in the corpus contains either character, so
closing the class costs nothing now and would cost a migration later.
NEXT.md described a packaging system with no visibility, no nesting and a link
that ignored the program, and explained sand's two files by it. All four are
now wrong. The Packages section says what the rules are; a new section says how
the link is decided and why the pruning has to take the functions as well as
the flags; and the sand section keeps the part that still stands — the headless
test needs no window on any target, which is a reason for two entry points and
never was a reason for two files.
The comments in load.ml and session.ml that used sim.flan to explain package
qualification now use vendor/agent, which is the package left with a defn in
it.
Three limitations, and the same program wanted all three gone.
An imported package's own imports were refused by name. They are resolved now,
and the qualification flattens to the inner alias: raylib imported by a package
that is itself imported is still rl/..., never sand/rl/.... That is forced, not
chosen — a directory reached along two routes has to arrive under one set of
names or the checker sees every declaration twice — and it is what lets the
dedupe work. A directory is keyed by its real path and read once, which also
ends a cycle: a package that imports itself meets its own entry and contributes
nothing the second time, and since the namespace is flat, mutually dependent
packages simply work. The same directory under two different aliases is
refused, because both cannot be true at once.
main is not exported. A package carrying one would collide with the importer's
the moment anything imported it, so a program could never be a package; and
main is a root, so an imported one keeps everything it calls reachable — for a
raylib front-end, the whole library, on the target that cannot link it. Writing
sand/main is refused at the line that wrote it rather than left to the checker,
which would only say the name is unknown. That is true and useless: the name is
missing on purpose and the message should say which purpose. A package that
calls its own main is refused too — it would silently get the importer's.
And a package may be a single .flan file named outright. sand.flan shares the
repository root with three other loose programs, so naming its directory would
import all four; moving it into a directory of its own would be arranging the
tree around a limitation. A file carries no .c and no link file — those belong
to a directory, and a package that needs them has one.
A package handed over its .c files and its `link` arguments the moment it was
imported, whatever the importing program did with it. That is what made sand's
two halves two files: anything naming vendor:raylib linked libraylib on every
target, and on wasm32 that link cannot succeed, so the headless run could not
so much as mention the package the interactive one needs.
Reach.link answers it from the checked program instead. Start at main and at
the globals that run before it, follow every call — including the Handled
frames, where a lifted handler clause is reached by address and by nothing
else — and keep what is reached. A package none of whose externs survive
contributes no C and no linker argument.
Dropping the flags alone would only move the failure: the bodies that called
into raylib would still be emitted, and wasm-ld would fail on the symbols
rather than on the argument. So the same walk prunes the functions and externs
too. Only those — globals, structs and unions stay, because an unreferenced
global is bytes in BSS and a dropped one is a silently different program.
Dev builds keep everything. What a REPL may redefine next is not a function of
what has been called so far.
The wasi-sdk candidate had an LLVM version in it, which moves release to
release — so the path advertised as the proper article would have matched only
by coincidence, while the emscripten one beside it was derived. Both are
derived now.
calc-me on wasm32 covers what the other three cases cannot: flan_argv hands
Flan an array of flan_slice built in C, so what it pins is the element stride
of a ptr+len pair — 16 bytes native, 12 on wasm32 — rather than a field
offset. It is also the claim in this file's own header, that the table runs on
the second target, honoured for the first time.
flan emit refuses --target rather than stripping it. The IR really is
target-free, so ignoring it is correct and silence about it is not.
f32 only, and each refusal by name: clamp and abs stay compositions of the
min/max builtins, split-at wants a pair type there is no way to spell, and the
f64 and other-element-type copies wait for a program that wants them.
-lm goes on every link, after the objects. The default --as-needed drops a
library named before the object that wants it, and at -O2 LLVM folds most sqrtf
calls into the hardware instruction so nothing has to resolve - which makes the
flag look unnecessary until the -O0 build emits the call and fails to link. That
is how it was found, on the -O0 acceptance run.
sqrt is libm's rather than Newton's, because there is no bit cast between f32
and u32 to seed a guess from, and IEEE-754 makes sqrt correctly rounded so
libm is bit-identical across targets anyway. llvm.sqrt.f32 as a builtin would be
better still - one instruction, no symbol, no link flag - and belongs to
whoever next touches check.ml.
The finding worth keeping is a test that came back green when it should have
been red: nothing in the table could observe floor's zero guard, because
(ceil-f32 0.0) is +0.0 either way. (floor-f32 -0.0) is the only case where it
shows, and the prelude comment had claimed the wrong justification for it.
wasm32-wasi needs a sysroot clang does not know about and a builtins archive
Fedora does not ship, and both have to reach the C compiles as well as the
link — flan_rt.c includes <stdio.h> and never got past it. The flags are
computed once and the whole list, not just the triple, is in the object cache
key: repointing a sysroot must not be served a stale .o.
Fedora ships no wasm libclang_rt.builtins.a and clang's resource directory is
root-owned, so a shadow one is built under the object cache with the archive
under the name clang looks for. The archive substituted is emscripten's
libcompiler_rt.a, a different triple built by a different clang; wasi-sdk is
the proper article and the comment says so, because a session reading "wasm32
works" should know which joint is glued. Nothing found means a refusal naming
every path tried.
The entry point is the other thing no triple tells you: wasi-libc calls
__main_argc_argv, the .ll says @main, and the mismatch links clean and then
traps on a weak stub. Two lines of C bridge it, and the asm label in them is
why the shim is not an infinite self-call.
--dev and Build.shared are refused for the target rather than half-supported:
both are dlopen, which wasm32 has no equivalent of.
floor, ceil and round over f32, which is what a position and a tile coordinate
are here. The only rounding mode available is the cast's truncation toward
zero, so each of these is that cast plus the correction the mode does not
make, and the content is which inputs make the cast itself undefined. NaN
fails every comparison, so it needs its own (not (= x x)) and nothing else
finds it; the infinities fall out of the magnitude test; and above 2^23 an f32
has no fractional bits left, which makes returning the input there the exact
answer and also the guard that keeps the cast inside i32.
round is half away from zero, written as floor of the magnitude and mirrored.
The obvious (floor-f32 (+ x 0.5)) is wrong twice: half-up rather than
half-away, so -2.5 comes out -2, and at the largest f32 below 0.5 the addition
alone rounds to 1.0 and answers 1 for a number under a half. Both are in the
table, which is why every case there is a negative or a half.
sqrt is the decision in this commit and it goes out to libm, which is a change
to the release link and so is said out loud. Every other number in the prelude
is reachable from the four operations and a cast; a square root is not.
Newton's method needs a starting guess, the good guess comes from
reinterpreting the exponent bits, and the language has only value-preserving
casts - no bit-cast between f32 and u32. Without one the iteration needs a
scaling loop to normalise and still produces a result that is merely close,
which is the one thing a standard library must not hand back. IEEE-754 makes
sqrt correctly rounded, so libm's answer is the same bit pattern on native and
on wasm32; for this function the byte-identical argument points at C rather
than away from it.
The cost is -lm on every link, and its placement matters. It goes after the
objects, not in the leading flags, because --as-needed drops a library named
before the object that wants it. Worse, at -O2 LLVM folds most sqrtf calls
into the hardware instruction and the symbol never has to resolve - so this
looked linked before the flag existed and failed only at -O0, which is exactly
why the table runs both. Untested against --target=wasm32: wasi-libc ships
libm.a as a stub because the symbols live in libc, so it should be inert
there, but nothing here exercises it.
The better fix is not in this lane. llvm.sqrt.f32 as a builtin in check.ml and
emit.ml is one instruction, no symbol and no flag, and it belongs to whoever
owns the compiler.
Every check of `abort' so far was of it being refused while the program runs.
The accepted path -- the one that ends a program -- was code that had never
run and answered `ok'. So the break block breaks its program once more, by
installing a `step' that errors into the loop that calls it, and takes the
exit: the daemon owns the program's lifetime, so no `close' is sent and the
daemon coming down on its own is the assertion.
And `restart' refuses a name with a control character in it. The agent's
contract is one line per request; a newline in a name is a second request
smuggled into the first. `completing-read' with require-match cannot produce
one, but the guarantee belongs to the end holding the socket, and an editor
is not the only thing that can speak to it.
Finishing the text family the previous lane started. All three are over [u8]
and none of them allocates, which is what decides their shapes.
trim answers a slice of its input. That is the only shape available without an
allocator, and it is also the better one: there is no new storage, only a
narrower view of the caller's, so the result dies with its owner and trimming
modifies nothing. Both loops test (< lo hi), because an all-whitespace input
otherwise walks lo past hi and (slice s lo hi) traps on a reversed range - the
same trap the bounds table already asserts on. That input is in the case list.
index-of-bytes is naive and stays naive. Boyer-Moore wants a skip table sized
by the needle, which is an array, which is an allocation. The empty needle
answers Some 0 so that index-of-bytes and starts-with? agree on every needle,
and the length test returns before the loop so a needle longer than the
haystack cannot build a window off the end.
parse-f64 splits the work where the two halves actually differ: the grammar is
Flan's and the rounding is libc's. parse-i64 is entirely Flan because strtoll's
answers are wrong for a caller - 0 for "", 0 for "abc", 12 for "12x" - and not
because decimal-to-binary conversion is suspect. Reimplementing correctly
rounded conversion is a different and much larger problem than rejecting junk,
and IEEE-754 already guarantees strtod gives the same bits everywhere. So this
validates the whole slice and only a slice that is entirely a number reaches
bytes->f64. Every refusal in the table - "", "abc", "1x", ".", "1e", " 1",
"1 ", "0x10", "nan" - is a plausible number out of strtod.
Two caveats, both written into the source rather than discovered later. The
locale worry that keeps parse-i64 in Flan does apply to strtod's decimal point,
and is moot only because nothing in the runtime calls setlocale; if that stops
being true this is what breaks. And the length is capped at 511 because
flan_bytes_to_f64 truncates there - a validator that approved 600 digits would
be approving a different number than the one strtod reads.
digit? and space? exist because parse-f64 and trim need them, and calc-me loses
its own byte-identical digit?. One top-level namespace makes the second
definition an error rather than a shadow, which is the rule doing its job: two
copies that later drift apart is exactly what it prevents.
The break loop was reachable from a raw socket. This is the half that makes
it reachable from an editor, and it all follows from one fact: a program
stops at a moment nobody asked about.
So the state is learned twice, on purpose. It rides on every reply, beside
the program's output and for the same reason -- the likeliest instant for a
program to stop is the one just after an evaluation, which is a reply the
client is already reading, and learning it a second later from a poll would
mean learning it after the echo area had said the evaluation was fine. And a
timer asks anyway, once a second with `describe', because a program that
stops in a frame of its own game loop produces no reply at all and folding
state into replies that never come says nothing. The timer never reconnects
-- that would quietly erase the `lost' state that exists to be seen -- and
skips while a request is in flight, since accept-process-output runs timers
and a poll firing inside a read would eat that read's reply.
Three ops: `break' for the restart names, `restart' and `abort'. The
annotation owns :stopped and :condition rather than the ops, so one place in
the daemon decides whether the program is stopped and the poll and the prompt
cannot disagree. "ok" from `restart' means accepted, not resumed: the choice
is validated against the stopped stack and taken when that thread next comes
round, so it says so and the client clears its own flag rather than polling
once, finding it stopped, and re-opening the prompt it just answered.
The agent grew one verb, `status', answered in both states. Everything else
the break loop offers is refused while running, rightly; but the question an
editor asks without already knowing had to have an answer either way or there
would be nothing to poll.
And flan_agent_poll had to become re-entrant, which was a bug rather than an
addition. A C-x C-e thunk may itself error, and the break loop that catches
it polls again from inside that call. The old loop cached both indices and
stored tail at the end, rewinding over everything the nested poll consumed --
re-running the thunk that had just stopped the program, which is an unbounded
recursion of breaks. Each job is now claimed before it is run. test_dev.ml
evaluates an expression that errors and resumes it, which fails against the
old shape.
Finishing the 2D lane's unfinished work: the collision family was written and
had no tests when the session ended. It is the best material a headless table
gets, since every one of these is pure and needs no GL context.
Two plausible tests in a row turned out to check nothing, and that is the part
worth keeping. A struct round trip is symmetric and passes for any field order -
the texture lane found that one. The second is subtler: no axis-aligned geometry
can pin Vector2's fields, because exchanging x and y is a reflection that is
applied on the way in and undone on the way out. Swapping the shim's own typedef
leaves every collision case passing. Distances never even see it.
What does pin Vector2 is the rotated camera, because a rotation is not
axis-aligned and does not commute with the reflection. That case is load-bearing
and the comment now says so, because the collision cases look like they cover
the same ground and do not.
What the new cases do pin is Rectangle, completely: swapping width and height
turns three of the four predicates the wrong way. Verified by doing it.
collision-lines answers (Option Vector2) rather than a bool and an
out-parameter, because raylib leaves the out-parameter untouched when the
segments do not meet and a caller who forgets reads whatever was there.
The standard library's first real content beyond printers. Everything is
in-place over a slice, because there is no allocator and nowhere to put a copy
- and a slice aliases its owner's storage, so sorting one sorts the original.
No generics means no single sort. Each is spelled per concrete type, which is a
cost paid deliberately rather than worked around.
parse-i64 is written in Flan rather than bound to strtoll, which answers 0 for
an empty string, for a string of letters, and for a genuine zero, and reports
overflow through errno.
bytes->i64 is strtoll behind a primitive, and strtoll returns 0 for "", for
"abc", for a lone "-", and for the "12" in "12x". None of those is
distinguishable from a real 0, so any program that parses input it did not
write is already wrong and has no way to find out. parse-i64 takes the whole
slice or refuses it and says so with None. It is also the version that answers
the same on wasm32: strtoll is libc's and locale-sensitive, which is the same
argument that put the PRNG in the prelude rather than leaving it to rand().
The byte predicates are over [u8] rather than over string on purpose. (bytes s)
is one call at the call site, and in exchange one copy of each function serves
strings and byte slices both — which is as near a generic as this gets. Each
tests its length before it slices, and `and` short-circuits, so a prefix longer
than the subject answers false instead of tripping the slice bounds check.
sign-f32 and lerp are the only two numeric helpers here, because they are the
only two that decide something. clamp is (min hi (max lo x)) and abs is
(max x (- 0 x)) over builtins that already exist — a prelude wrapper is a
function emitted into every program to save a caller nothing. sign-f32 answers
0.0 for NaN, which is a choice and is written down. lerp is the weighted sum
and not a + t*(b - a): the latter does not land on b exactly at t = 1.0, and a
position that never quite arrives is what interpolation gets bug reports for.
floor, ceil and round are deliberately absent. (f32 (i32 x)) is fptosi, which
is poison out of range, and shipping that as a documented limitation is the
same class of bug NEXT.md already records twice under Sharp edges. Correct
lowering is llvm.floor.f32 in emit.ml, which is not this lane. sqrt is absent
for a different reason: it is an extern to libm, and what libm means on wasm32
is a decision the FFI owns, not the prelude.
rand-i32-range answers lo for an empty or reversed range rather than dividing
by zero, which is immediate undefined behaviour and not merely a wrong number.
Both range functions draw exactly one rand-u32 and neither changes it, so the
sand hash still pins the generator; the new test pins the derivations off a
fixed seed, which nothing else would have caught.
A sequence library normally returns new sequences. There is no allocator, so
every one of these mutates the storage it was handed and a slice is the handle
that makes that useful: (slice grid 4 9) is ptr+len into grid, so sorting it
sorts those five elements and leaves the rest of grid alone. The test asserts
exactly that — it sorts a subslice and prints the whole owning array — because
it is the property that would die silently the day a slice parameter started
being copied rather than passed by value, and -O2's mem2reg would hide it.
Insertion sort rather than anything faster. Quicksort wants a stack and
mergesort wants a buffer, and neither exists; insertion sort needs a swap and
two indices. It is also the only one of the three whose inner loop is short
enough to read, which matters more than the asymptotics on the slice sizes a
frame loop actually sorts. The `and` guarding it short-circuits, and that is
load-bearing: at j = 0 the left test fails and (at s -1) is never evaluated,
so the bounds check never fires.
Over [i32] and nothing else. There are no generics, so a second element type
is a second copy of all seven functions emitted into every program that links
the prelude, and i32 is the type indices, ids and tile values already have.
An f32 set waits for a program that wants one.
min-i32 and max-i32 return (Option i32) rather than a sentinel because there
is no i32 that means "the slice was empty" and is not also a possible element.
sum-i32 accumulates in i64 and widens each element explicitly — there is no
implicit widening anywhere, and an i32 total over a screenful of i32 is how a
sum wraps without anyone noticing.
spec-conditions.md §2, and the reason the transfer was worth building. An
unhandled error runs a hook instead of rt_die(), on the frame that erred with
nothing unwound, lists the restarts between there and the top, and waits.
A hook rather than a direct call because the loop lives in vendor/agent, which
is an optional package, and flan_rt.c is the release runtime - a program with no
agent leaves it null and dies the way it always did. The hook resumes by writing
a restart into the transfer channel, which is the channel an invoke-restart
writes and reaches the same guard, so choosing from the break loop and choosing
from a handler are one act lowered once. §6 needed no change.
The break loop is the poll loop, run from the error rather than from the frame
boundary. That is load-bearing: an expression evaluated while stopped is a
module the listener queues and the game thread runs, so a loop that did not
drain that queue would hang C-x C-e exactly when it is wanted most. Installing
while stopped is allowed, which contradicts the rule that a redefined function
must not be swapped while it is on the stack - that rule is about mid-frame
consistency and there is no frame in progress here. The old body keeps running
and a retry reaches the new one through the cell, which is the whole point.
A restart frame carries its name now, beside the hash. Matching never needs it;
showing someone their choices does, and nothing at run time can turn a hash back
into a name.
A choice is checked on the listener thread against a stack the stopped game
thread is holding still. Answering ok and finding out on the game thread that
nothing offers that name would report success for something that cannot happen.
The test errors twice and takes a different restart each time, so a loop that
always resumed the same way fails it.
The extern-boundary refusal and a restart-case one each had a run of spaces
mid-sentence where an OCaml line continuation had been lost. The textures lane
found the first while reading the FFI rules; the second was mine, from the same
editing mistake in the same week.
The restart-case one turned out to be unreachable as well. Each clause is
checked with the type the form has already settled on, so a clause that
disagrees is refused by expect where it is written - the dedicated message
could only have fired if that check did not, and it does. Removed rather than
reworded; a message nothing can produce is worse than no message, because the
next person tries to work out what reaches it.
All three want the same three facts about a name — what it is, what it looks
like, and where it was written — so the daemon answers all three in one
`defs` reply and the client keeps the last one.
`defs` is its own op rather than more fields on `describe`. `describe` is what
an editor *polls*: it is how the program's output gets drained, and the
existing tests ask it in loops. Signatures riding on that would be paid for
every time anyone glanced at the output buffer. This is asked once on connect
and again after each accepted install, which is exactly when the answer can
have changed — so a `defn` typed a second ago completes.
It is a cache rather than a request per keystroke because of where these are
called from: eldoc fires on an idle timer and completion inside redisplay, and
neither may block on a socket or signal.
Three refusals rather than three guesses. A global has no location because
`Tast.global` carries no `Loc`, and searching the buffer for "(defvar ticks"
instead would find the wrong one in a program of several files. The prelude is
a string inside the compiler, so its location names a file nobody can visit. A
short name that could be several of the program's package-qualified ones is
ambiguous, and picking would be a guess about which function you meant — a
name that is the tail of exactly *one* is not a guess, and resolves.
Functions the checker invented — a lifted handler-bind clause, which carries
an `fparent` — are left out entirely: nobody wrote that name, so completing it
is noise and jumping to it is meaningless.
And the daemon now makes its own source path absolute before building, because
every location it reports derives from it. `flan dev src/game.flan` from a
project root answered `src/game.flan:12:7`, which an editor can only resolve by
guessing what it was relative to.
lib/dev.ml is the only compiler file touched: a `defs` op, its three list
builders, and the one `realpath` in `start`. Nothing existing changed shape —
`describe`, `eval` and `eval-expr` answer byte for byte what they did.
plan.org's hot reload section now says a signature-changing redefinition should
make a new internal function version with its own trampoline: new code resolves
the name to it, existing callers and stored Fn values keep the old one safely,
and the session warns at every tracked caller site still on the old signature.
session.ml refuses the change outright, with a reason that reads like the final
answer.
None of the three parts exists - no function versions, no trampolines, and no
record of which source location called what - so the refusal stays, because the
alternative to refusing is not the new design, it is a silent argument
mismatch. What changes here is only that the code and NEXT.md now say which one
it is, so the next person reads it as the stopgap it is.
plan.org now says a top-level function value is a stable trampoline over the
indirection cell and never the address of a particular body, so that a stored
callback observes a redefinition. A pushed handler frame breaks that rule and
should: it is not a Fn value, nothing in the language can name it, and it is
live only for the duration of the handler-bind body - so a reload landing while
it is on the stack finds the clause it pushed still valid, which is the whole of
old code is never unloaded.
The consequence worth knowing is that a handler already on the stack does not
pick up a redefinition of its own clause; the next entry to the handler-bind
pushes the new one. Recorded at the store in emit.ml and in NEXT.md, because
when Fn values arrive this is the one place that stores a body address on
purpose and must not be swept up with the rest.
The refusal said milestone 5, which lumped it in with escaping closures - and
plan.org has just deferred those until a concrete use case appears. A handler
frame does not outlive the function that pushed it, so what a handler clause
needs is spec-memory.md's case 2, a non-escaping fn capturing by value into a
stack environment, which is settled rather than deferred.
Worth recording before anyone schedules it, because open decision #5 says in as
many words that without this conditions are not worth building, and the
deferral it just received does not apply to it.
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.
spec-conditions.md §2. The same lookup as signal, and the difference is
entirely what happens when the walk ends: signal returns Unit and the
signalling function carries on, error has type Never and the program stops.
Only a transfer gets past it, so emit puts a guard after the call and then
unreachable - and flan_error cannot be marked noreturn for the same reason, it
does return, on exactly one path.
Being Never is what lets it stand where a value was expected, which is the
fall-through shape §1's load-texture example needs and the reason it is worth
having before the break loop rather than after. An unhandled one names the
condition on stderr and dies the way every other trap does; flan_error is where
the dev-build break loop will go.
The two spellings share one AST and IR node with a kind beside them, the same
shape Ast.unwrap already uses for some and try, because they differ in one
decision and nothing else. test/programs/error.flan is the unhandled case,
asserted on the exit code and the reason rather than through the outputs table,
which only has room for a program that exits 0.
vendor/agent/flan_agent.c calls flan_dev_result_get, which lives in flan_dev.c,
which build.ml compiled only for a dev build - so flan build sand.flan died at
the link with an undefined symbol. A regression from 7ce1d09, where C-x C-e
gave the agent a result to report.
A package's C sources are collected whatever main does, so the agent's C is in
every build that imports it. flan_dev.c is now compiled into all of them.
Nothing in a release build reaches it: the compiler emits a registry lookup
only for a name the host was not built with, and without cells there is no such
name. The table is BSS, so the cost is address space rather than binary size,
and -rdynamic and the cells are still what --dev means.
test_agent.ml now links the same program both ways. It runs only the dev one -
with no cells the agent refuses every module, so linking is the whole claim.
error, find-restart and compute-restarts are named in spec-conditions.md and had
no case in parse.ml, so each fell through to Call and came back as unknown name
- the very shape the house rule exists to prevent, and the one that makes a
missing feature look like a typo.
Three message strings from the previous commit had their line continuations
collapsed into runs of spaces. Rewrapped; no change to what they say.