13 Commits

Author SHA1 Message Date
9ce51ba94e One slice over everything with elements, and the warning at the push
as-slice was a warning, not an operation. The input type already decides
which of the two things happens — a Vec can only be borrowed, an array or a
string can only be viewed, and no call site picks between them — so the second
name expressed no choice a reader could make. And it warned at the moment the
view is taken, which is the one moment nothing is wrong; the danger arrives
later, at the push. slice now takes a Vec at all three arities and as-slice
is gone.

(slice v lo) was free, and is the arity the Vec never had: the runtime already
reads a hi of -1 as "to the end", so the tail form passes the caller's lo and
the same -1 — no slot, no length read, no second evaluation. The merge is
entirely in the checker; the Vec path builds the flan_vec_as_slice call it
always built and neither backend has a line about any of it.

A Vec a call returned is refused at every arity, and not for the array's
reason. (slice (mk)) over an array dangles. (slice (make-vec)) does not — the
storage outlives the expression — but the header is a temporary, so nothing
can ever free the block. The refusal says that and names the let.

The name's own refusal sits in ordinary_call after every table, so a program
that defines an as-slice still reaches its own. It reads for somebody who has
never heard of the old name and writes the call back out, spelling each
argument that is a name or a number.

The warning moved to where it bites: BUILT.md gains a section beside the Vec
table and the push row points at it, spec-memory.md's Borrowing says the same.
Investigated and deliberately not built — a diagnostic for a live view at the
push. (reserve v 100) then a slice, a push and a read is correct code under
the contract the spec chose, so any flag on it is a false positive by the
language's own semantics rather than by an approximation. FIX.org has the
finding and the syntactic sketch that does not work.
2026-09-21 09:51:35 +07:00
2e203f64b8 bytes copies, bytes-view aliases, and a dev-session segfault parks
The INSERTIONSORT crash, all three rulings (FIX.org 2026-09-20):

- (bytes s) allocates a writable copy through the allocator surface —
  context or (bytes s a), StorageExhausted with retry, a registry note in
  dev builds (flan_bytes_dup, lowered like vec-new). (bytes-view s) is the
  old zero-cost reinterpret, renamed, read-only by convention; every
  in-repo reader swept over to it. (string b) unchanged.
- String constants were already read-only on both backends at -O0; now
  pinned — bytes-copy.flan rows on LLVM/-O0/--x86, and dies_segv rows
  asserting the write-through-view trap on both backends.
- A dev build installs a SIGSEGV/SIGBUS handler by the same dev-only
  constructor slot that arms the registry: one line naming the address and
  the innermost frame, then the trap-hook park — stopped, not dead, the
  daemon serving. No agent: message and re-raise. Release builds untouched.
  Pinned by trap_park over dev-segv.flan.
2026-09-20 23:12:42 +07:00
831cab9fb2 Review follow-ups: x86 parity, ArithError, and three refusal bugs
Five fixes off the independent review, plus the author's u8 ruling.

x86 parity: the bad-index block always ran x86 (it is flan dev's
default) and now says so with an explicit --x86; the condition render
gets an assertion under the x86 backend too, beside the LLVM one, and
a user error is pinned as carrying no site on both.

ArithError's layout is now pinned: {i32 op; i64 lhs, rhs} in C against
the prelude's defstruct, read field by field through the break loop's
render, driven from the editor through a divide under a restart-case.
That also covers condition and site on LLVM.

Three refusals that were wrong: trap_site tested the prefix "err"
and so ate any site whose path began with those letters; source_line
let Sys_error from input_line escape and take the whole break reply
with it, leaking the handle; and a condition with no fields was
reported as a name no struct has. The daemon now sends its own field
count and the buffer tells the two empties apart.

Nits taken: an over-long site is dropped rather than silently
truncated into a plausible one; the caret pads with the source line's
own tabs; the headline says when it has cut the field list;
flan-cnr-layout is live again as the single spelling of that request
rather than dead beside an inlined copy.

And the ruling: a u8 renders as 97 (\a) where a person is inspecting
and stays 97 where the program is printing.
2026-09-20 22:55:11 +07:00
Joseph Ferano
d5fed12d48 Three messages about milestone 5, from a milestone that arrived
The refusals generics obsoleted, swept. Every message that sent
somebody to a schedule now says what is actually true of the thing in
front of them.

An unknown lowercase type name used to be reported as unimplemented
generic code over a type variable. Generics are implemented, and
resolve_name consults env.tyvars and env.subst long before anything
reaches that arm -- so a lowercase name arriving there is a typo too
far from any type to guess at, or a type variable nobody introduced.
The sentence names the sigil that would introduce it.

A capitalised name given type arguments is the other half, and it is
still genuinely unbuilt: Types.Named is a bare string with no room for
parameters, and giving it some is a change to Types.t and therefore to
the layout calculator, both backends, Render and DWARF. Both sites
that reported it -- the type resolver and the value-position fork --
now say a generic *type* is not there yet and point at the generic
function that is.

Plus the prelude's side of it. pos?, neg? and zero? are three
questions about a number's sign, one body each, answering at every
numeric type -- the family the whole feature was asked for, and the
one thing the landed generics could not write until a literal was
allowed to stand at a bounded type variable.

Two collapses examined and declined, with the real reason written
where the old one was. abs stays per width because numeric? is the
only bound that admits a written 0 and it admits floats too, and the
integer body is the wrong abs for a float: it hands back a negative
zero. It waits on an integer? predicate, which is language surface.
min and max stay builtins because they are variadic and slot each
operand so it is evaluated once; a binary prelude generic would put
the double evaluation back at the call site. Their generic half was
never missing -- ordered? already admits them in any body that
declares it.
2026-09-20 20:46:35 +07:00
Joseph Ferano
879a439951 A written zero stands where a numeric type variable stands
pos? over every numeric type from one definition was the motivating
example for milestone 5 and was the one thing the landed generics could
not write: (> x 0) refused with "expected t, found the integer literal
0", because int_literal had no arm for a want that is a type variable.

It has one now, and the bound is what makes it sound rather than
optimistic. Every type numeric? admits is an integer or a float, and an
untyped integer constant is usable at all of them, so there is no
instantiation of a numeric? variable at which the literal has no
meaning. Under a weaker bound there is -- ordered? admits an enum -- so
numeric? is what is asked for and the refusal names it.

The float literal is refused at a type variable even under numeric?,
and that asymmetry is the concrete arms' own: an integer constant is
usable where a float is wanted and a float literal is never usable
where an integer is wanted, so a body written with 0.5 has no meaning
at the integer half of its own bound. Refusing at the definition is
what the abstract pass is for; the alternative is a surprise at
whichever call site first asks for i32.

The node the abstract pass builds is never emitted. Each copy
re-checks the same form with the variable substituted, and that is
where the literal is built at the concrete width and range-checked --
so (+ x 300) is fine at i32 and a refusal at u8, and u8 is where it is
refused.
2026-09-20 20:21:41 +07:00
a0f37e72a2 The ! suffix retires: a mutator is named for what it does, not marked
The !-means-mutates convention distinguished nothing — there is no
immutable counterpart to contrast with — so every mutating name drops
the mark: sort, sort-by, sort-bytes, swap, reverse, append, append-i64,
append-f64, encode-rune, split-next, map-remove, map-next, and the test
helpers beside them. Two could not simply shed it: map! is map-in-place,
because map is the into transform's word and means the non-mutating
thing; put! is put-at, because put is the Map builtin. The ?-means-asks
convention stays. Dated records keep the old spellings; watch.clj's
reset-spies! and the other Clojure names are not ours to rename.
2026-09-19 05:21:02 +07:00
f35af50f51 or-else is Java's, not Rust's, and the None it is tested against is reached
Four corrections to the prose and one to the test, none to the design.

The provenance line said "the name is Rust's or_else, the behaviour is
Rust's unwrap_or", which conflates two functions that differ in both
eagerness and return type — Rust's or_else takes a closure and answers
another Option.  Java's Optional.orElse is the exact match, and its lazy
sibling orElseGet is the one already declined a paragraph above.

The helper reaching or-else's None branch at an owning type asserted a
refusal nobody had run.  Compiled, it is "nothing here says what None is
an Option of — annotate the function's return type or the binding", so
the comment quotes that and the helper is a return type and nothing
else: its other branch was never called, in a program whose header says
every line is a claim.

read-file's comment claimed both restarts arrive unchanged and the test
runs use-value.  Narrowed to the mechanism (nothing here establishes a
handler) plus the half that is actually executed.

And edn-read.flan now says what becomes of its defn wrapper when the
computed-initialiser work lands, since that is the only thing keeping
the motivating line from being written as the defvar.
2026-09-19 04:30:57 +07:00
7a0b2b7803 An Option opens without a match, and a reader takes a path
Two things the motivating line wanted and could not have.

or-else and some? are the first prelude family over (Option $t), and the
first that declares no {:where} at all: they move the payload out or read
the tag, and neither is an operation a type variable has to be admitted
to.  So they instantiate at every type, including the ones that own
storage — where the answer is a header onto one of the two buffers and
the branch not taken is still the caller's to free, which the comment
says because "or a default" reads like it consumes the default.

none? is declined as (not (some? o)), and an unwrap that signals on None
is declined for the reason file-size is an Option at all: absence is a
reply and not a fault, and whether an empty one is an error is the
caller's question.

edn/read-file is worth having for one fact the package already argued:
every string in a Value is a copy, so the source buffer is dead the
moment read returns and nothing outside the call can be holding it.  It
slurps against the heap by name — the one allocator this package names,
because the buffer's life is inside the call and is not the caller's
tier to choose — defers the free for the transfer path, and passes
slurp's FileError straight through with both restarts armed.  Folding a
missing file into None would collapse the very distinction the Option
exists for.

There is no json/read-file and json.flan now says why: a Token's text is
a slice into the caller's buffer, so the prerequisite is a json/read
answering a self-contained document, and there is no Value type there to
answer with.

The defvar initialiser in the motivating line is still refused as
computed, so edn-read.flan writes it as a defn and says so; everything
inside the with-allocator is verbatim.
2026-09-19 04:16:37 +07:00
4a563d0e67 The move-only concept follows the flow analysis out: everything copies, and the frees are yours 2026-09-18 12:16:05 +07:00
c46fd56447 Pool and Handle leave the language: two containers are enough, and a slab is a Vec you free less often 2026-09-18 11:56:22 +07:00
762bc988fa The map operations are deferred, and the clause is what pays for it
hashable? gated the type and not the operations: a generic could take and
return a (Map $t V) and could not get or put into one. The hash and the
equality are emitted as concrete symbols chosen from the key type, and
while $t is a variable there is no symbol to name.

The five arms that reach the pair - put, get, has-key?, reserve, clone -
now check their arguments and return a placeholder of the operation's own
type when the key is a type variable: Unit for put and reserve, None for
get so the (Option V) around it still checks, false for has-key?, a zeroed
map for clone. The node is thrown away with the rest of the abstract pass
and the real one is built in the copy, exactly as println's is.

What makes that different from print's free ride is the clause. A map
operation can fail at a concrete type; it is deferred anyway because
{:where (hashable? $t)} is in the signature, so the refusal lands at the
call that asked for the type, against a requirement the author wrote down.
A generic that declares nothing gets no deferral - deferred_key checks
first, and map_type has usually refused the signature already. So the rule
for the allow-list is not a headcount: either the operation cannot fail
after substituting, or a declared predicate gives its failure somewhere to
land. The comment at the print arm says that now instead of "stays two
long".

The instantiation-time refusal names the call site, the type it asked for,
the predicate and the clause, rather than repeating the generic's name
twice.
2026-09-13 15:23:37 +07:00
de93ffc89e A cast to a type variable, and the container builtins over one
(t x) is not a name is_cast knows - t is not a machine type - so it is
its own arm, admitted by numeric? because a cast produces a number.
vec-new, pool-new and map-new all reach the one list of what names a
type, so the spike's line for vec-new had already covered the other two;
zeroed takes its type from the position it is written in. All four are
pinned in programs/generics.flan.
2026-09-13 14:52:17 +07:00
dad725afe4 The prelude's per-type families collapse: 22 functions become 10, 27 become 16
swap!, reverse!, sort!, sort-by!, index-of, min-of, max-of, map!,
reduce and filter, each written once over $t. Every call site in the
corpus moves with them.

min-of and max-of are not min and max because min and max are builtins
over two or more numbers and nothing shadows a builtin. These reduce a
slice, which is a different operation at a different arity.

sort-bytes! did not collapse into sort!, and the reason is the point of
the predicates: a [u8] is not ordered? and cannot be, because < is an
instruction and comparing two slices lexicographically is a loop. It is
sort-by! with bytes<? written in, one line, keeping its name and its
stability note. sum-i32/sum-f32 and append-i64!/append-f64! stay for the
reasons the spike gave.

Not what the notes predicted: none of the ten collapses on a signature
change alone. filter and reduce need copyable? because the checker
demands it - reduce's accumulator at (Vec i32) is a double move - and
the rest declare it because a slice of owning elements would have them
duplicating headers.
2026-09-13 14:49:11 +07:00