diff --git a/FIX.org b/FIX.org index 35c940a2..596956cd 100644 --- a/FIX.org +++ b/FIX.org @@ -6353,3 +6353,76 @@ out on ~program_asked~, so a re-run installs what is queued and then enters ~main~. That is ~lib/dev.ml~, which another lane holds, so it is written down here rather than done. The test row asserts the behaviour as it is, with the extra re-run spelled out. +* The reference page says what the language is, 2026-09-21 + +~web/index.html~ had drifted behind the compiler, mostly in one direction: it +described the language as it was before ~dyn~ existed and before ownership +tracking was repealed. A correction pass, not a rewrite — structure, section +ids and voice kept, claims fixed. + +What was false. The tagline, the lede and ~Values and memory~ each said there +is no garbage collector; the ~What it is not~ list refused immutable +collections because "clear ownership is the only thing that removes the need +for a collector" and refused dynamic typing outright; the types table gave +~Vec~, ~Map~ and ~Pool~ as moving on assignment, which the 2026-09-18 repeal +removed; ~Printing~ and the dev loop's expression evaluation both said a Flan +value carries no header with no exception for a ~dyn~; the usage block was +missing ~-O0..-O3~, ~--warn-memory~ and the ~js~ target and still gave ~flan +run~'s old argument shape; the structs example quoted ~(bytes "hi")~ where the +file beside it says ~bytes-view~; the ~Not implemented yet~ table quoted a +milestone-5 message for a bare lowercase type name that the compiler no longer +prints; ~Further reading~ called spec-memory.md frozen and about ownership; +the footer named the retired ~dev-loop~ branch. + +What was added. A ~dyn~ section after ~Types~ (id ~dyn~, one TOC entry, no +existing id touched): one word the runtime knows the contents of and the +checker does not, writing the type is the opt-in, only those values are +collected, a numeric cast moves the question to run time where +~flan_dyn_cast_kind~ answers it, ~--no-gc~ names every ~dyn~ and refuses, and +the roots are an explicit shadow stack because wasm32 cannot be made to scan +its own. Also: the operator arities (the orderings chain, ~!=~ is +all-distinct, ~%~ and the shifts take two), ~slice~'s three arities and its +string case with the ~as-slice~ note, ~def~/~defonce~/~defconst~ as a table of +what a re-run does, an untyped global being a ~dyn~, the parameter-vector +pairing rule, ~bytes~ against ~bytes-view~, and a ~Function values~ subsection +covering ~fn~, capture, ~Fn~ against ~CFn~ and the one-way widening. + +The class facility was the other thing the page got wrong, and it got it +wrong the other way round: ~Not implemented yet~ said the managed ~class~ +plan.org describes "is a plan and not a feature", and the Emacs section said +managed classes were the eventual way through a changed layout. Both landed +on 2026-09-20. So the ~dyn~ section carries ~Classes and generic functions~ — +the four forms, the two dispatch styles as one mechanism, ~:else~, +~NoMethod~, the ~#point{ :x 3 :y 4}~ rendering, a generic being one function +so a method reaches the call sites already compiled — and the Emacs section +says a ~defclass~ is the shape that evolves, with the lazy per-instance +migration and why a flat ~defstruct~ cannot have it. What is left unbuilt of +it, and now named as such, is the named-slot constructor spelling and the +user migration hook. The ~dyn~ section also gained maps, vectors, keywords +and ~nil~, since a class's slots are map keys and none of that was on the +page either. + +Four new example programs under ~web/examples/~ — compare.flan, dyn.flan, +fnvalues.flan, classes.flan — and a ~no-gc~ needle in quotes.sh over +dyn.flan, so the refusal the page quotes is re-derived like every other +transcript on it. + +** @page was green over a needle that could not be derived +~sand hash~ had been reporting "nothing to compare against" since sand.flan +grew ~(import edn "vendor:edn")~: the alias's deps never named ~vendor/edn~, +so the headless program could not resolve the package in ~_build~ and the +needle came up empty. quotes.sh reports an empty needle as a failure, which is +why it was visible at all, but it reads as "whatever this quotes has moved" +rather than as a missing dependency. One ~glob_files~ in test/dune. + +** Not fixed, because it cannot be checked from here +The ~Targets and builds~ section still says survey.sh reports 103 MATCH. +FIX.org's 2026-09-20 session close says 130. Running @x86 was out of scope for +this pass, so the number is left as it stands rather than replaced with one +nobody ran. + +** The top-level usage does not mention --no-gc +~flan build --no-gc~ works and ~flan build~'s own usage line lists it; +the usage that bare ~flan~ prints does not. The page quotes the latter +verbatim, so it does not name the flag either. A compiler change, not a page +change. diff --git a/test/dune b/test/dune index dc7ff098..ebcf20fd 100644 --- a/test/dune +++ b/test/dune @@ -350,6 +350,11 @@ ; alias was green only because `dune test` had already put the file in ; _build, and in CI it sits inside @checks where the same thing hides it. (file %{workspace_root}/sand.flan) + ; And the package sand.flan imports, for the same reason: without it the + ; headless program cannot resolve vendor:edn in _build and the sand-hash + ; needle comes up empty, which quotes.sh reports as "whatever this quotes + ; has moved" rather than as a missing dependency. + (glob_files %{workspace_root}/vendor/edn/*) ; breakdemo.flan is built --dev, so it imports the agent; raylib is here for ; the binding line quotes.sh greps out of it. (glob_files %{workspace_root}/vendor/agent/*) diff --git a/web/examples/classes.flan b/web/examples/classes.flan new file mode 100644 index 00000000..b79f5b5f --- /dev/null +++ b/web/examples/classes.flan @@ -0,0 +1,29 @@ +;; A class is a named dyn map with a shape tag. Its slots are names and +;; carry no types, and its constructor is the class's own name, positional. +(defclass point [x y]) +(defclass circle [r]) + +;; CLOS-style: the generic states the return type once, and each method +;; dispatches on the class of its first argument. +(defgeneric area [self] dyn) +(defmethod area point [p] (* (get p :x) (get p :y))) +(defmethod area circle [c] (* 3 (get c :r) (get c :r))) + +;; Clojure-style: the generic's body is the dispatch value, and a method +;; names the value it answers for. :else is the arm everything falls to. +(defmulti describe [x] dyn (get x :kind)) +(defmethod describe :square [s] (get s :side)) +(defmethod describe :else [s] "something else") + +(defn main [] () + (let [p (point 3 4)] + (println (area p)) + (println (area (circle 2))) + ;; A slot is a map key: get, put and has-key? are how one is read. + (println (get p :y)) + ;; class-of answers the tag, and nil for anything that is not an instance. + (println (class-of p)) + (println (class-of 7)) + (println p) + (println (describe {:kind :square :side 9})) + (println (describe {:kind :blob})))) diff --git a/web/examples/classes.out b/web/examples/classes.out new file mode 100644 index 00000000..2cc45bd2 --- /dev/null +++ b/web/examples/classes.out @@ -0,0 +1,9 @@ +12 +12 +4 +:point +nil +#point{ :x 3 :y 4} +9 +something else +exit 0 diff --git a/web/examples/compare.flan b/web/examples/compare.flan new file mode 100644 index 00000000..4467878f --- /dev/null +++ b/web/examples/compare.flan @@ -0,0 +1,11 @@ +;; The arithmetic operators and the comparisons both take a run of operands. +;; The orderings chain: each neighbouring pair is compared, and every pair has +;; to hold. != is the one that does not chain — it asks whether the operands +;; are all distinct, so a value repeated anywhere in the run makes it false. +(defn main [] () + (println (+ 1 2 3 4)) + (println (< 1 2 3)) + (println (< 1 3 2)) + (println (= 4 4 4)) + (println (!= 1 2 3)) + (println (!= 1 2 1))) diff --git a/web/examples/compare.out b/web/examples/compare.out new file mode 100644 index 00000000..6043f657 --- /dev/null +++ b/web/examples/compare.out @@ -0,0 +1,7 @@ +10 +true +false +true +true +false +exit 0 diff --git a/web/examples/dyn.flan b/web/examples/dyn.flan new file mode 100644 index 00000000..74e6f46f --- /dev/null +++ b/web/examples/dyn.flan @@ -0,0 +1,13 @@ +;; Writing dyn is how a value opts in. The parameter has no type written, so +;; it is a dyn; the return type says dyn too, and both ends of twice are then +;; a word the runtime knows the contents of and the checker does not. +(defn twice [x] dyn + (+ x x)) + +;; A numeric cast opens the box. (i64 d) compiles for any dyn d, and what the +;; box actually holds is answered when the program runs. +(defn main [] () + (println (twice 21)) + (println (twice 1.5)) + (let [d (twice 21)] + (println (+ (i64 d) 1)))) diff --git a/web/examples/dyn.out b/web/examples/dyn.out new file mode 100644 index 00000000..6aa57159 --- /dev/null +++ b/web/examples/dyn.out @@ -0,0 +1,4 @@ +42 +3 +43 +exit 0 diff --git a/web/examples/fnvalues.flan b/web/examples/fnvalues.flan new file mode 100644 index 00000000..fa3ce997 --- /dev/null +++ b/web/examples/fnvalues.flan @@ -0,0 +1,15 @@ +;; An Fn is {code, env}: it may have captured, and the caller neither knows +;; nor cares. A CFn is the bare code address, one word, and cannot capture. +(defn apply-fn [f (Fn [i32] i32) x i32] i32 (f x)) +(defn apply-cfn [f (CFn [i32] i32) x i32] i32 (f x)) + +(defn bump [x i32] i32 (+ x 1)) + +(defn main [] () + (let [n 10] + ;; An fn takes its types from the position it is written in. + (println (apply-fn (fn [x] (+ x n)) 5)) ; captures n + (println (apply-fn bump 5)) ; a defn captures nothing + (println (apply-cfn bump 5)) ; so it fits either position + ;; Widening is one way: a CFn goes where an Fn is wanted. + (println (apply-fn (fn [x] (* x 2)) 21)))) diff --git a/web/examples/fnvalues.out b/web/examples/fnvalues.out new file mode 100644 index 00000000..0779ade3 --- /dev/null +++ b/web/examples/fnvalues.out @@ -0,0 +1,5 @@ +15 +6 +6 +42 +exit 0 diff --git a/web/examples/quotes.sh b/web/examples/quotes.sh index 9d48eb28..d2b5d314 100644 --- a/web/examples/quotes.sh +++ b/web/examples/quotes.sh @@ -47,6 +47,14 @@ want "calc-me" "$("$FLAN" run "$root/calc-me.flan" '1 + 2 * (3 - 0.5) / 2')" # that the two agree; only the native half is cheap enough to check here. want "sand hash" "$("$FLAN" run "$root/test/programs/sand-headless.flan")" +# --no-gc, which is the page's claim that a program can have "this carries no +# collector" checked rather than believed. dyn.flan holds one on purpose, so +# the flag must refuse it; the page quotes the first line of what it says. +nogc=${TMPDIR:-/tmp}/flan-page-nogc.$$ +want "no-gc" "$("$FLAN" build "$here/dyn.flan" --no-gc -o "$nogc" 2>&1 \ + | sed 's/^[^ ]*: //' | sed -n 1p)" +rm -f "$nogc" + # The two cross-target refusals, in the compiler's own words. want "run --target" "$("$FLAN" run "$here/hello.flan" --target=wasm32-wasi 2>&1)" diff --git a/web/index.html b/web/index.html index be27bf4d..c79bc99e 100644 --- a/web/index.html +++ b/web/index.html @@ -224,6 +224,7 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
A statically typed Lisp for game development. Clojure's brackets, - C's memory and value model, no garbage collector.
+ C's memory and value model, and a collector onlydyn values reach.
Flan compiles s-expressions to LLVM IR and then to a native binary. -There are no object headers, so a Flan struct is exactly its C struct. There is no -collector, so nothing runs between frames that you did not write. And a running -program can be edited: a function recompiled in Emacs is installed into the live -process at its next frame boundary, in about twenty milliseconds.
+A typed value carries no header, so a Flan struct is exactly its C struct, and a +program that writesdyn nowhere has no collector in it — nothing runs
+between frames that you did not write. And a running program can be edited: a
+function recompiled in Emacs is installed into the live process at its next frame
+boundary, in about twenty milliseconds.
This page describes the compiler as it is, not as it is planned. Where something is designed but not built, it says so and gives the message the compiler @@ -297,7 +299,8 @@ rather than programs.
A minimal Lisp for games. In one line: Odin with a Lisp frontend and a live REPL.
-Types are mandatory and inference makes them feel optional; memory is manual; the
+Types are written at function boundaries and inferred everywhere else, and a
+parameter left unwritten is dyn; memory is manual; the
frontend is OCaml, and the default backend writes LLVM IR as text and hands it to
clang. There is a second one, off by default, that emits x86-64 by hand —
see targets and builds.
format, no lazy seqs, no persistent collections, no JVM.dyn is the opt-in, and
+ the tag and the collector are what it costs — see dyn.() is unit, and a main that returns it ex
There is no garbage collector and no hidden allocation. Every local is a stack -slot; reading one is a load, assigning to one is a store, and a store of an aggregate -is the copy. A struct is its C layout with nothing added.
+There is no hidden allocation. Every local is a stack slot; reading one is a load,
+assigning to one is a store, and a store of an aggregate is the copy. A
+struct is its C layout with nothing added. The one value that is not laid out this
+way is a dyn, which is allocated by the runtime and collected; writing
+the type is how a program asks for that, and dyn is where it is
+described.
Four rules carry most of the model:
@@ -513,14 +519,15 @@ notation reads as exactly one data item.string[T][n T](Vec T)(Map K V)(Pool T)(Vec T)(Map K V)(Pool T)(Handle T)i64(Ptr T)(Option T)Some / None(Fn [T ...] R)(CFn [T ...] R)C is what a C function pointer would need, not a way to reach C todaydynAllocator$tThere is no implicit widening. Both operands of a binary operator -have one type, and every conversion is written as a cast:
+There is no implicit widening. Every operand of an arithmetic or +comparison form has one type, and every conversion is written as a cast:
(defn main [] i32
(let [n 40 ; i32, inferred
@@ -558,6 +565,44 @@ operand's width is a compile error, and a computed one is masked to the width.
>> is arithmetic on a signed type and logical on an unsigned
one.
+Operators take a run of operands
+
+Two operands is the floor and there is no ceiling.
++, -, *, /, min,
+max and the three bitwise combining operators fold left over as many
+arguments as they are given. % and the shifts are not in that set and
+take two: a chain of remainders or of shifts has no reading agreed on in advance, so
+there an arity error is the useful answer. The orderings — <, <=, >,
+>= — chain: each neighbouring pair is compared and every
+pair has to hold, so (< 1 2 3) asks whether the run is increasing.
+= chains the same way. != does not: it asks whether the
+operands are all distinct, comparing every pair rather than the
+neighbouring ones, so (!= 1 2 1) is false.
+
+;; The arithmetic operators and the comparisons both take a run of operands.
+;; The orderings chain: each neighbouring pair is compared, and every pair has
+;; to hold. != is the one that does not chain — it asks whether the operands
+;; are all distinct, so a value repeated anywhere in the run makes it false.
+(defn main [] ()
+ (println (+ 1 2 3 4))
+ (println (< 1 2 3))
+ (println (< 1 3 2))
+ (println (= 4 4 4))
+ (println (!= 1 2 3))
+ (println (!= 1 2 1)))
+
+10
+true
+false
+true
+true
+false
+
+Why the two differ. "Is this sequence increasing" and "are these
+values all different" are the two questions a run of operands is actually asked, and
+they need different comparisons. Chaining != would answer neither: it
+would be true of 1 2 1, which is not a run of distinct values.
+
An index converts from a narrower integer and never from a wider one. A
u32 index is fine — anything above 231 truncates to a negative
i32 and the unsigned bounds check rejects it. An i64 index is
@@ -567,6 +612,185 @@ refused:
not fit truncates to one that does and would read the wrong element without
tripping the bounds check
+A dyn is one machine word whose contents the runtime knows and the
+type checker does not. Every other type on this page is decided where it is written;
+this one is decided when the program runs, and the runtime keeps a tag beside the
+value to decide it with.
Writing dyn is how a value opts in. It is written like any other type
+— a parameter's type, a return type, a global's type — and a parameter left with no
+type written is a dyn, which is the shortest way to ask for one. Those
+values, and only those, live on a collected heap. A program that writes it nowhere
+pays nothing: no tag, no allocation, no collector.
;; Writing dyn is how a value opts in. The parameter has no type written, so
+;; it is a dyn; the return type says dyn too, and both ends of twice are then
+;; a word the runtime knows the contents of and the checker does not.
+(defn twice [x] dyn
+ (+ x x))
+
+;; A numeric cast opens the box. (i64 d) compiles for any dyn d, and what the
+;; box actually holds is answered when the program runs.
+(defn main [] ()
+ (println (twice 21))
+ (println (twice 1.5))
+ (let [d (twice 21)]
+ (println (+ (i64 d) 1))))
+
+42
+3
+43
+
+One twice is compiled, not one per argument type, and the
++ in it is the runtime's addition over two tagged words. The second
+call prints 3 rather than 3.0 by the whole-number rule
+in types, not by anything dyn does.
The return type is always written, dyn included. The parameter vector
+is where the types may be left out, and a vector of bare names is read as names: in
+(defn f [x y] ...), y is a second dyn
+parameter when nothing declares a type called y, and is
+x's type when something does. The pairing is decided after every file is
+loaded and every macro has expanded, so the set of type names it consults is the
+whole set — but it is the set at a point in time, and writing a
+(defstruct y ...) elsewhere changes f's signature with no
+edit to f.
A global left without a type is a dyn too:
+(def x 5) declares one and (def x i32 5) does not. See
+functions for the three global forms.
A numeric cast is how a dyn comes back to a type the checker can
+see. (i64 d) compiles for any dyn d, because
+the question of what the box holds is not one the checker can answer; the question
+moves to run time, where flan_dyn_cast_kind answers it. A box holding
+something the cast cannot take stops the program where it happened, with the file
+and the line, the way a bad index does.
flan build --no-gc names every dyn in the program, with
+its location, and refuses to build. That is how a program has "this carries no
+collector" checked rather than believed. It is a refusal and not a different
+lowering: nothing downstream is told the flag was given, so the output of a build
+that passes it is byte for byte the output of a build that does not.
$ flan build dyn.flan --no-gc
+dyn.flan:4:7: parameter 1 of twice holds a dyn, and --no-gc says this program
+carries no collector. A dyn value is one the runtime allocates and the collector
+owns, so there is nothing smaller to compile it to — write the type
+
+The check runs before reachability, so a dyn in a function nothing
+calls is still a dyn somebody wrote. A refusal that came and went as the
+program was edited elsewhere would not be worth having.
Braces and brackets in expression position write dyn literals:
+{:a 1 :b "two"} is a dyn map and [1 2 3] is a dyn vector.
+get, put, has-key?, at and
+length read and write them, the same names the typed
+Map and Vec answer to. A keyword is a value here rather
+than only a way to name an enum member: keywords are interned, so comparing two is
+comparing two pointers.
The absent dyn value is written nil. It is not () —
+unit carries nothing for a dyn word to hold, and boxing it is refused — and
+(Some nil) cannot be built, because a present absence would make
+nil and None the same case of an
+(Option dyn).
A class is a named dyn map with a shape tag. defclass names its
+slots, which carry no types; the constructor is the class's own name and is
+positional; and class-of answers the tag, or nil for
+anything that is not an instance. The slots are map keys, so nothing was added to
+read or write one.
Dispatch comes in the two styles and they are one mechanism.
+defgeneric dispatches on the class of the first argument, which is
+CLOS's rule. defmulti takes a body whose value is the dispatch value,
+which is Clojure's. Either way a defmethod names the value it answers
+for — a class name, a keyword, a string, an integer, true,
+false, or :else for the arm everything falls through to,
+which is last whatever order it was written in. The generic states the return type
+once, for every method; a method has no return slot; and every parameter of both is
+dyn, written or not.
;; A class is a named dyn map with a shape tag. Its slots are names and
+;; carry no types, and its constructor is the class's own name, positional.
+(defclass point [x y])
+(defclass circle [r])
+
+;; CLOS-style: the generic states the return type once, and each method
+;; dispatches on the class of its first argument.
+(defgeneric area [self] dyn)
+(defmethod area point [p] (* (get p :x) (get p :y)))
+(defmethod area circle [c] (* 3 (get c :r) (get c :r)))
+
+;; Clojure-style: the generic's body is the dispatch value, and a method
+;; names the value it answers for. :else is the arm everything falls to.
+(defmulti describe [x] dyn (get x :kind))
+(defmethod describe :square [s] (get s :side))
+(defmethod describe :else [s] "something else")
+
+(defn main [] ()
+ (let [p (point 3 4)]
+ (println (area p))
+ (println (area (circle 2)))
+ ;; A slot is a map key: get, put and has-key? are how one is read.
+ (println (get p :y))
+ ;; class-of answers the tag, and nil for anything that is not an instance.
+ (println (class-of p))
+ (println (class-of 7))
+ (println p)
+ (println (describe {:kind :square :side 9}))
+ (println (describe {:kind :blob}))))
+
+12
+12
+4
+:point
+nil
+#point{ :x 3 :y 4}
+9
+something else
+
+An instance renders as #point{ :x 3 :y 4}, Clojure's spelling for a
+record, and the tag is why two instances of one class compare by their slots while an
+instance is never equal to a plain map with the same entries. A dispatch that matches
+no method signals NoMethod, carrying the generic's name and the value
+the dispatch produced. It is a condition rather than a trap because a miss is
+something a program can be written to answer; no restart is established at it, which
+is BoundsError's decision taken for BoundsError's
+reason.
A generic is one function. The method bodies are inlined into a
+chain in its body rather than lifted into functions of their own, so adding a method
+to a running program is the ordinary redefinition of one name, through the cell the
+call site already goes through. A method does declare a name of its own —
+area@:circle — which is what makes re-evaluating one a replacement and
+evaluating a new one an append; no function is emitted under it.
The four forms are a pass over the whole declaration list rather than macros:
+lib/classes.ml runs where declare-c's shim generation runs.
+A macro sees one form, and a method may be written above its generic, below it, or
+arrive at a reload an hour later.
The collector is mark-sweep, and it finds its roots from a shadow stack the +compiler pushes to rather than by scanning the C stack. Scanning is what a collector +normally does and it is cheaper to write, but it needs to know where the stack is and +what on it might be a pointer. wasm32 does not let a program look at its own call +stack at all, and the language builds for wasm32. An explicit root stack is the same +code on every target, which is what makes the collector work there.
+A defstruct is a list of inline name/type pairs. A struct literal names
@@ -585,7 +809,7 @@ its fields, and omitted fields are zeroed.
u8 — but there is a byte literal, so \h is
number, print a slice of them: print writes a [u8] as
its bytes.
+There are two ways to see a string's bytes and the difference is whether anything
+is allocated. (bytes-view s) is the string's own storage seen as a
+[u8] and costs nothing; it aliases the string, so a literal's view points
+into .rodata and writing through it traps. (bytes s) and
+(bytes s allocator) make a writable copy through the allocator — never a
+hidden malloc, which is the rule every allocating operation follows. The
+example above wants a view and takes one.
An enum is an i32 at run time and its own type in the checker. A
keyword at a call site resolves against the parameter's enum type at compile time, so a
typo is an error there rather than a wrong number later.
Top-level names are order-independent within a package, so mutually recursive -functions need no forward declaration. Globals come in two kinds:
+functions need no forward declaration. Globals come in three kinds: + +| Form | What it is | What a re-run does |
|---|---|---|
defconst | a compile-time constant | nothing to do |
defonce | storage, initialised once | keeps the value it has |
def | storage | runs the initialiser again |
Each names its own type, and the type is what makes the pair legible: a
+defonce is for state the program builds up and a def is for
+a value the source decides, so re-evaluating a file leaves the first alone and stores
+into the second. The three forms decide that between them; the dev session does not.
+There is no defvar — the compiler catches the name and gives both
+spellings rather than guessing which was meant.
A third element that is not a type is the value of a dyn global:
+(def x 5) declares a dyn, and (def x i32 5)
+declares an i32. The same holds for defonce.
(defconst cell-size 5) ; a compile-time constant
(defconst gravity f32 0.05) ; with its type named
@@ -672,6 +924,62 @@ two reload differently; see the dev loop.
let binds name/value pairs and takes no type annotation, so a constant
whose type matters is named at the top level rather than written inline.
+Function values
+
+There are two function types and the difference between them is what a value of
+each one is, not what it may do. (Fn [T ...] R) is a code
+address and the environment it is called with: two words, and it may have captured.
+(CFn [T ...] R) is the bare address: one word, no environment, and
+therefore nothing that can capture.
+
+fn writes a function value inline. It takes its parameter and return
+types from the position it is written in, so it goes in an argument slot whose
+parameter names them; a bare (let [f (fn [x] x)]) is refused saying so.
+An fn sees the locals it was written among and copies the ones it uses,
+and that is what makes it an Fn. A name declared by defn
+captures nothing and fits either type.
+
+;; An Fn is {code, env}: it may have captured, and the caller neither knows
+;; nor cares. A CFn is the bare code address, one word, and cannot capture.
+(defn apply-fn [f (Fn [i32] i32) x i32] i32 (f x))
+(defn apply-cfn [f (CFn [i32] i32) x i32] i32 (f x))
+
+(defn bump [x i32] i32 (+ x 1))
+
+(defn main [] ()
+ (let [n 10]
+ ;; An fn takes its types from the position it is written in.
+ (println (apply-fn (fn [x] (+ x n)) 5)) ; captures n
+ (println (apply-fn bump 5)) ; a defn captures nothing
+ (println (apply-cfn bump 5)) ; so it fits either position
+ ;; Widening is one way: a CFn goes where an Fn is wanted.
+ (println (apply-fn (fn [x] (* x 2)) 21))))
+
+15
+6
+6
+42
+
+Widening is one way. A CFn is accepted where an
+Fn is wanted — the environment word is filled in and nothing is lost.
+The reverse cannot work: an Fn put into a CFn would have
+nowhere to keep its captures. An fn written into a CFn
+position is checked as an Fn and then refused if the finished body
+turns out to capture, which is the only point at which that is decidable:
+
+this fn captures n, so it is a (Fn [i32] i32) and not a (CFn [i32] i32): a CFn
+is the bare address, one word, with nowhere for the copies to live. Widen the
+position to Fn, or pass n in as a parameter
+
+Rationale: two types rather than one. A uniform environment would
+tax every function in every program for something most of them never use. With two
+types an ordinary defn keeps exactly the signature it always had, and
+the C in CFn names where the one-word form is going: a
+value with no environment is the only kind that could ever be a C function pointer.
+It is not a capability that exists today — a declare cannot take a
+function type at all, because a Flan signature ends with the transfer channel and a
+C caller knows nothing about one.
+
Control flow
if, when, unless, cond,
@@ -836,8 +1144,23 @@ extent, because nothing is released at scope exit)
at indexes a fixed array or a slice, and takes any number of
indices, so (at grid r c) indexes a two-dimensional fixed array directly.
It is a place: (set (at grid r c) v) and (addr (at grid r c)) both work.
-length works on a fixed array, a slice or a string.
-(slice s lo hi) takes a half-open range and never copies.
length works on a fixed array, a slice, a string, a Vec or a
+Map.
+
+slice takes the view and never copies. It has three arities —
+(slice a), (slice a lo) and (slice a lo hi) —
+and the short ones are written out into the long one, so nothing is added at run
+time: lo is 0 and hi is the length, which an array already
+folds to a constant and a slice or a string is carrying anyway. The range is
+half-open. A string slices to a string rather than to a [u8], because
+the result views the same bytes and is read-only for the same reason the source
+is.
One name, because the input type decides the semantics. There
+used to be a second, as-slice, for the Vec alone. A
+Vec can only be borrowed and an array can only be viewed, and no call
+site picks between the two, so the second name expressed nothing. Writing it now says
+there is no as-slice and names the one that exists.
(defconst rows 3)
(defconst cols 4)
@@ -994,9 +1317,11 @@ bind. test/programs/generics.flan exercises the whole of it.
println prints a value and a newline; print is the same
walk without the newline. There is one of each and they take any type, but neither is a
function and neither is overloading: the compiler walks the argument's type where the
-call is written and emits the printing for it. Nothing is decided at run time — a Flan
-value carries no header, so nothing at run time could say what it is — and there is no
-user-supplied printer to choose between.
+call is written and emits the printing for it. Nothing is decided at run time — a
+typed Flan value carries no header, so nothing at run time could say what it is — and
+there is no user-supplied printer to choose between. A dyn is the one
+argument whose printing is decided at run time, because it is the one value
+that carries a tag to decide it with.
(defenum Key [space 32 left 263])
(defstruct Enemy [hp i32 name string key Key])
@@ -1020,8 +1345,8 @@ none
no newline: true
The walk covers every integer and float type, bool, (),
-string, [u8], enums, Ptr, Option,
-structs, unions, fixed arrays and slices. An owning container has no printer for its
+string, [u8], dyn, enums, Ptr,
+Option, structs, unions, fixed arrays and slices. An owning container has no printer for its
contents and comes back as a marker instead — <vec>,
<pool>, <allocator> — while a Handle
shows its index and generation, and a Map has no printer at all.
@@ -1135,7 +1460,7 @@ here; a whole file at a time is the surface.
The primitives underneath are few — a primitive is the only thing implemented
twice per backend: argv,
write-stdout, exit, length, at,
-slice, bytes, bytes->f64,
+slice, bytes, bytes-view, bytes->f64,
bytes->i64, f64->bytes, i64->bytes,
addr, and arithmetic, comparison and casts.
@@ -1715,9 +2040,10 @@ from. The module says run this once, and the agent calls it after the i
the game thread, at a frame boundary. An expression reading the program's state therefore
sees a consistent one.
-Nothing is marshalled back, because nothing could be: a Flan value carries no header,
-so no code at run time can say what it is. The compiler knows the type and renders it
-there, in the thunk. What comes back looks like this:
+Nothing is marshalled back, because for all but one type nothing could be: a typed
+Flan value carries no header, so no code at run time can say what it is. The compiler
+knows the type and renders it there, in the thunk. A dyn is rendered
+there too, from its tag. What comes back looks like this:
big 18446744073709551615
col :blue
@@ -1869,9 +2195,16 @@ to a new body. The refusal is a limitation with a date on it, not a rule.
A changed struct layout is the genuinely hard case, and is rejected
while live values of that struct exist: storage already allocated has the old shape, and a
-new body would read its fields at the wrong offsets with nothing to say so. plan.org keeps
-this one as a rejection, and gives managed classes an explicit migration at a frame
-boundary as the eventual way through.
+new body would read its fields at the wrong offsets with nothing to say so.
+
+A defclass is the shape that does evolve, and the
+reason is the whole difference between the two: an instance carries a header naming
+its class and a flat struct does not. Redefining one re-registers the class and
+bumps a generation counter, which is O(1) and walks no heap; every live instance
+migrates at its next touch. Slots matched by name keep their values, a gained slot
+appears as nil, a dropped one goes, the object is the same object, and
+class-of still answers the same tag, so every method still reaches it.
+That is CLHS 4.3.6's protocol without the user hook, which is not built.
C-c C-x rebuilds, relaunches and reconnects, and is the way out while the
above is true. It costs the program's state, which is why it is a key you press rather than
@@ -2025,7 +2358,7 @@ name, with the milestone it belongs to, and the tests assert on the reason.
(Result T E)(Result T E) is not implemented yet — milestone 6 (see plan.org)
(try …)try (Result) is not implemented yet — milestone 6 (see plan.org)
'sym as a valuea quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org)
-a bare lowercase type name generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)
+a bare lowercase type name unknown type a. A lowercase name is a type variable only where a defn signature introduced it — write $a in the parameter vector to introduce one, and a reads it from there
errdefererrdefer is not implemented yet (see the build sequence in plan.org)
awaitawait is not implemented yet (see the build sequence in plan.org)
find-restart, compute-restarts… is not implemented yet (see the build sequence in plan.org)
@@ -2047,8 +2380,9 @@ extent); find-restart and compute-restarts are blocked
Restart type rather than on effort; a restart with parameters cannot be
taken from the break loop, which aims at a frame by position and has nothing to fill them
with; there is no package-private marker other than main not being exported;
-there are no threads in the language; and the managed class facility that
-plan.org describes is a plan and not a feature.
+and there are no threads in the language. The class facility plan.org describes is
+built — see dyn — and what is not built of it is the named-slot
+constructor spelling and the user-written migration hook.
One of these is settled rather than pending. There is no interpreter
and there is not going to be one: compiling is the only way a form is ever run. The
@@ -2066,8 +2400,8 @@ disagree with the first.
plan.org — the design, the build sequence, and the open decisions.
NEXT.md — the project's memory, and the authority on what is actually
built.
- spec-memory.md — ownership, containers, places, generics, function
- values. Frozen.
+ spec-memory.md — containers, places, generics, function values,
+ allocators, and the 2026-09-18 repeal of static ownership tracking.
spec-conditions.md — conditions and restarts, operational semantics.
Frozen.
conditions.org — a cheatsheet for driving conditions.
@@ -2078,9 +2412,9 @@ disagree with the first.
@@ -2148,12 +2482,13 @@ disagree with the first.