12 Commits

Author SHA1 Message Date
d6fc15474b The count is length, so len is a name a program can have
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.
2026-09-21 11:58:56 +07:00
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
ff2c949361 The tagged sum is defdata, and the old spelling is an error by name
Flan's tagged sum has been spelled defunion since it landed, which was
accurate right up until the language wanted C's untagged union as well.
Both cannot be called the same thing, and the tagged one is the one with
an alternative name that says what it is: a case, its fields, and a tag
that steers which case is live is a data type, not a union.

So the form is defdata everywhere -- the parser, the AST, the checker,
both backends, the prelude's Form, the editor's font-locking and imenu,
the docs and every .flan file in the tree. The internal vocabulary moves
with it: Tast.union is Tast.data, uname is dname, the tables the checker
and the emitter keep are datas. Leaving them would have inverted the
words permanently, with surface defunion meaning one thing and
env.unions meaning the other, which is exactly the kind of drift the
comments in those files exist to prevent. What did not move is case,
variant and vfields: a tagged sum still has cases, and it still has one
live at a time.

defunion is not kept as an alias. An alias would compile the day the
untagged form lands and mean the opposite of what it used to -- the same
silent misparse that made defn's return type mandatory, and worse,
because the reader would have no reason to look. The old spelling is a
named refusal instead, parse/defunion-renamed, which says what it is now
called and that the name is reserved for something else. It fires on the
head alone, so (defunion U [A B]) -- which would otherwise have parsed
cleanly as one field A of type B -- is refused with the rest.
2026-09-17 19:03:27 +07:00
0aebef62f3 The sketch's condition syntax, checked against the parser rather than recalled
handler-bind takes (Type [name] body ...) and has since it was written; the
sketch paired a type with an fn, which is the shape parse.ml names in its own
refusal message. load-level had two return types. And there is no defcondition
anywhere in the tree -- a condition type is an ordinary struct, which is what
both spec-conditions.md and conditions.org say, so the one form in this file that
introduced one was inventing it.

The header's rules went with them: lowercase-is-a-type-variable and "no sigils"
are both the pre-$t spelling, and let never took an annotation.
2026-09-14 07:36:24 +07:00
d94e864dba The syntax sketch shows syntax that compiles, and says where it does not
Every generic example in syntax-sketch.flan was written in the lowercase-variable
spelling that $t replaced, so the file whose job is to be copied from was the one
place the wrong spelling cost the most. Checking the replacements against the
compiler rather than against the sketch turned up three more, and each is now
either fixed or named:

- reduce is (reduce s init f) and its accumulator is the element type. The old
  argument order was Clojure's and the fold in centroid is not a shape it has.
- an fn cannot be written inline into a (Fn [$t $t] bool) parameter, because a
  generic body is checked with nothing substituted and the fn has no types to
  take. Inside a generic the callback is a named defn.
- (largest hps >) does not work and never did: an operator is not a name. Both
  this file and spec-memory.md asserted it.

centroid is left as it was, because component-wise arithmetic over a fixed array
is planned rather than built and an aspirational example is not a defect -- but
it now says so instead of leaving it to be found.
2026-09-14 07:36:24 +07:00
70af1966a2 Braces are no longer a type: (Map K V) is the only spelling
The author's decision, and it removes the one syntax question generics
had. A return type can no longer be written in braces, so a {...} after
the signature is unambiguously the constraint map and there is no
structural rule to explain.

The reasons for the record: the brace's value meaning and its type
meaning do not correspond the way the bracket's do - [1 2 3] is a value
whose type is [3 i32], but {.x 1} is a value whose type is a name, and a
map value is built by map-new with no braces anywhere - and dropping it
reserves {} in type position for anonymous struct types.

Braces in a type are refused with the surviving spelling named rather
than falling through to "expected a type". Types.to_string and
Cimport's source printer both print (Map K V) now, and Shim refuses the
application spelling where it used to refuse only Ast.Tmap.
2026-09-13 14:58:27 +07:00
df73f87b2f The return type stops being a guess: the slot is mandatory, unit is ()
The slot after a defn's parameters is unconditionally a type. Parse.decl no
longer takes a set of type names, and is_type_form, qualified_type, types_in,
declared_types and prelude_types are gone with the pre-pass that fed them.

What they were for: (Option f64) and (Some 1) are the same s-expression, so the
parser decided which it had by looking the head up in a set of the file's own
type names. Sound -- one top-level namespace means a name cannot be both a type
and a value -- and brittle, because the set had to be complete. It was wrong
twice in one day, the second time parsing (defn f [] (Rune {.code 65}) (bar))
as a function returning a Rune with a one-form body, silently, in every file in
the language.

Two things fall out. A type the parser could not have known -- a struct
declared further down the file, rl/Vector2 behind an unresolved alias, a
prelude type -- never needed recognising, only placing. And a mistyped type is
a mistyped type: (defn f [] f65 0.0) reaches the resolver's near-miss check and
says did you mean f64, where it used to be read as the first form of the body
and reported as an unknown name.

Unit is written (). The old spelling is refused with a message naming the new
one, the rule the colon-to-dot change followed. Internally it is still
Tname "Unit" and Types.Unit, so the resolver, the shim and the emitter did not
change; Cimport still builds Tname "Unit" for C's void without going through
the parser. Types.to_string prints () though -- that printer prints what a
person would write for every other type it knows, [i32], {K V}, (Ptr T), and
Unit was the odd one out once the source spelling moved.

Dropping prelude_types removes one of the two reasons Macro.reduce may only
drop defns: the memoised set a bootstrap build could have poisoned is gone, so
the remaining reason is the plain one.
2026-09-12 23:18:28 +07:00
26c53e0a19 Every defn in the tree states its return type, and Unit is written ()
The mechanical half, ahead of the parser change that needs it. tools/unit-return.py
fills the empty slot with () and rewrites Unit as () wherever a type is spelled --
(Fn [i32] Unit), (Map i32 Unit), a return type written out.

Deciding whether a defn already had a return type is the whole difficulty, and
the script does it the way parse.ml did: is_type_form is transcribed rather than
improved, because being identical to the parser it replaces is what makes the
sweep meaning-preserving. It is re-runnable, so the lanes that branched before
this can have the same pass at merge:

    python3 tools/unit-return.py .
    python3 tools/unit-return.py --in-strings test/test_flan.ml test/test_acceptance.ml \
        test/test_session.ml emacs/test-flan-dev.el emacs/test-flan-mode.el
    python3 tools/unit-return.py --raw-ml lib/prelude.ml
    python3 tools/unit-return.py --in-html web/index.html

-v logs every defn it saw and what it decided, which is how a sweep of 440 sites
gets reviewed at all. Embedded modes pool a file's type declarations across all
its fragments, because a snippet split across concatenation -- decls ^ "(defn f
[s [u8]] Cursor ...)" -- cannot see the names the other half declared; pooled
names count only in bare-symbol position, for the same reason the prelude's do.
A fragment that cuts off mid-form is skipped rather than guessed at. Five sites
in test_flan.ml still needed a hand, and they are in this commit.

Two things ride along because the sweep needs them: parse.ml reads a lone () as
the return type of a function with no body, which was not a shape the old
optional slot could produce; and the map refusals name () rather than Unit, since
that is now the spelling a caller wrote.
2026-09-12 23:06:40 +07:00
8e47356592 A field label is a dot now, and the colon is refused where one was
The delimiter is what disambiguates: (.x v) is a call and therefore an
access, {.x 1.0} is a brace form and therefore a construction. The colon
kept two jobs -- field label and enum member -- and this leaves it with
one, keys, which is what a map literal will want.

The old spelling is refused rather than quietly accepted, and the refusal
names the new one. Two accepted spellings is how two spellings become
permanent, and this repo rejects what it does not support and says why.

:keys keeps its colon. It names no field -- it is an instruction to the
compiler that happens to sit in the same brace -- so leaving it alone is
what lets the dot mean exactly one thing.

render.ml prints the dot too, or a struct the daemon shows would not be
Flan anyone could paste back.
2026-09-12 14:51:57 +07:00
9a820d86cd Sweep every field label from the colon spelling to the dot
The script is in tools/ rather than thrown away, because two lanes are
writing Flan in the old spelling right now and their files need the same
pass at merge.

It works on forms, not on text: a keyword becomes a dot only where it sits
in a field-label position inside a brace, so an enum member in value
position, a map key inside an EDN string and a type-position {K V} are all
left alone. :keys keeps its colon -- it names no field.
2026-09-12 14:47:54 +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
e9cdbb321b Lisp based flan 2026-09-10 14:40:34 +07:00