; The prelude: every program is compiled with these definitions in front of it. ; lib/prelude.ml says how the compiler carries and reads this file. ; The root every built-in error descends from. A condition type names its ; parent where it is declared — (defstruct FileError :parent Error [...]) — ; and a handler for a type answers every condition below it, so one handler ; for Error catches any error: ; ; (handler-case (run) [(Error [e] (println (.name e)) (println (.message e)))]) ; ; A handler that matched through a parent is handed the condition's name and ; a sentence saying what went wrong, not the condition's own fields: the ; handler's type is the parent's, and the fields are the child's. That is ; why a parent has exactly these two fields, and why a type declared with a ; parent and no field vector — a category, (defstruct Category :parent ; Error) — gets them. The sentence has no values in it; a handler for the ; condition's own type reads those from its fields. A program's own ; condition has an empty sentence, since its fields say what it is. ; ; (pause) and warnings are not under Error: a breakpoint is not a failure. struct Error(name: str, message: str) ; The condition every allocating operation signals when the allocator cannot ; satisfy a request — spec-memory.md, "Allocation failure". It is here rather ; than built by the checker because it is an ordinary value struct and the ; checker already knows how to build one of those; nothing about it is ; special except who signals it. ; ; Fixed numeric fields and no rendered message, because formatting would ; allocate and this is the one path that must not. :allocator is the ; allocator's address, which is its identity — the same thing the epoch hangs ; off — so a handler can tell which region ran out. Rendering happens in the ; handler or the break loop, where a working allocator is known. struct StorageExhausted(bytes: i64, align: i64, allocator: i64) :parent Error ; What an out-of-range index signals. Same shape as StorageExhausted and for ; the same reasons: fixed numeric fields, no rendered message, nothing that ; allocates — the condition is built on the failing frame's stack and the ; formatting is the handler's or the break loop's job, where a working ; allocator is known. ; ; It is signalled with `error`, from the runtime rather than from Flan: ; flan_bounds_error, flan_slice_error and flan_slice_promise_error in ; runtime/flan_rt.c — the last of those is (slice-from p n), whose ; message is about the caller's promise because there is no container to ; report, and which fills these fields with (0, n, 0): the condition it ; violated, 0 <= n, written as a range. Between them they are where every ; bounds check branches. **The three fields there are a C struct that ; has to agree with this one field for field**, the same hand-kept agreement ; flan_name_id keeps with Check.type_id. ; ; `low` and `high` are the same index for an (at xs i), and the two ends of ; the range for a (slice xs lo hi). One condition type rather than two, ; because a handler that wants to survive a bad index should not have to ; write two clauses to cover the two ways of writing one. ; ; **Nothing establishes a restart at the failing site**, which is the ; difference from StorageExhausted and from FileError. Those offer `retry` ; because their attempt is repeatable: free something, or supply another path, ; and the same operation succeeds the second time. Nothing a handler can do ; makes index 51 valid for a length-50 array, so there is no attempt to ; re-run. `use-value` for the index is the near miss and is not built: it ; would put an alloca and a restart frame on every indexing operation, and ; what it buys is a *different element*, silently. ; ; The restarts that matter are the ones the program already established — a ; frame loop's `continue`, sand.fln's shape — and they are on the restart ; stack and reachable from a handler or from the break loop without anything ; being pushed here. That is plan.org's "restarts go at the resync point, ; once", with allocation and file failure as the named exceptions and this on ; the default side of the rule. struct BoundsError(low: i64, high: i64, length: i64) :parent Error ; What an arithmetic operation with no answer signals. Three situations, and ; until now none of them had a defined behaviour: a divide or remainder by ; zero, which was a raw SIGFPE with no message and no location; the one ; division that overflows, (/ most-negative -1), whose true quotient is one ; past the top of the type and which `idiv` also makes a SIGFPE; and a float ; to integer cast whose value does not fit, where x86 produces a fixed ; "integer indefinite" and LLVM calls the whole thing undefined and may fold ; it to anything. ; ; They signal, for the reason spelled out over BoundsError and for one more ; that is specific to these: a SIGFPE cannot be caught and resumed, so the ; only way to get a message naming the file and the line is to test *before* ; the instruction. Once that branch is being paid for, making it a condition ; rather than a die costs nothing further, and a program that genuinely does ; not care installs a handler once at startup and never thinks about it again. ; ; Same shape as the two above, and for the same reasons: fixed numeric ; fields, no rendered message, nothing that allocates. It is signalled from ; the runtime — flan_arith_error in runtime/flan_rt.c — so **these three ; fields are a C struct that has to agree with this one field for field**, ; the same hand-kept agreement flan_bounds_cond keeps with BoundsError. ; ; `op` is an ArithOp, an i32 at run time, which is what lets the runtime fill ; it in from C; the members' numbers are flan_rt.c's FLAN_ARITH_* codes: ; ; :div-zero (/ a 0) :rem-zero (% a 0) ; :div-overflow (/ min -1) :rem-overflow (% min -1) ; :cast-range a float to integer cast whose value does not fit ; :cast-nan a float to integer cast of NaN ; :cast-inf a float to integer cast of an infinity ; ; `lhs` and `rhs` are the two operands for the first four and the ; destination type's representable range for the casts — the violated condition ; written as a range, which is what flan_slice_promise_error already does ; with BoundsError's fields. Two meanings over two fields rather than two ; condition types, so that a handler writes one clause and not five. The ; value that did not fit is not carried, because it is a float and these ; fields are not; what the handler needs in order to say something useful is ; the range it missed. ; ; The fields are the low 64 bits of whatever they hold. A u64 operand above ; 2^63 therefore reads back negative, which is the same reinterpretation ; every i64 field in every condition here makes and is not worth a fourth ; field to fix. ; ; **No restart is established at the failing operation**, which is ; BoundsError's decision and not StorageExhausted's. The sketch this started ; from asked for `use-value`, and the implementation is what ruled it out: a ; restart frame is allocated by the restart-case that offers it, on its own ; stack, and a transfer carries that frame's address (runtime/flan_rt.c, the ; restart stack). The runtime cannot hold one on a program's behalf, so ; `use-value` here would mean an alloca and a restart frame emitted at every ; division in every checked build — the identical cost refused for indexing ; a few lines above, buying a silently different answer. What answers a ; division by zero is the restart the program already established, a frame ; loop's `continue`, which is reachable from a handler without anything being ; pushed here. enum ArithOp div-zero = 0 rem-zero = 1 div-overflow = 2 rem-overflow = 3 cast-range = 4 cast-nan = 5 cast-inf = 6 struct ArithError(op: ArithOp, lhs: i64, rhs: i64) :parent Error ; A call that was compiled against one signature, reaching a function that ; now has another. It exists only in a dev build: there every call to a Flan ; function goes through a cell, the cell carries the signature its body was ; compiled with, and a function redefined with other parameters or another ; return installs anyway — so a caller compiled before the change finds the ; two different at the call and signals this instead of passing arguments ; the new body does not take. A release build has no cells and never ; signals it. ; ; `callee` is the function called, `compiled` the signature the call site ; was compiled for and `current` the one the function has now, both written ; the way a defn writes them: "[i32 i32] i64". Evaluating the caller again ; compiles it against `current`, and the call works from then on. ; ; Signalled from the runtime — flan_stale_call in runtime/flan_rt.c — so ; **these three fields are a C struct that has to agree with this one**, the ; agreement flan_bounds_cond keeps with BoundsError. No restart is ; established at the call, BoundsError's decision for BoundsError's reason: ; nothing a handler supplies makes the old arguments fit the new body. struct StaleCall(callee: str, compiled: str, current: str) :parent Error ; A call through a (CFn ...) that holds no function. A CFn may be a struct ; field, a fixed array's element or a global, and each of those starts out ; zeroed, which for a function value is no address at all. Every call ; through one tests first and signals this instead of jumping to nothing. ; `type` is the value's type as written, "(CFn [i32] i32)". ; ; Signalled from the runtime — flan_null_call in runtime/flan_rt.c — so ; **this field is a C struct that has to agree with this one**. No restart is ; established at the call, BoundsError's decision for BoundsError's reason. struct NullCall(type: str) :parent Error ; What a generic function signals when no method answers. `generic` is the ; name written at the defgeneric or defmulti, and `value` is what the ; dispatch actually produced -- the class of the first argument for a ; defgeneric, whatever the body answered for a defmulti. A miss is nil for ; the common case of a value that is not an instance at all. ; ; A condition and not a trap, and that is the decision rather than the ; obvious default: Common Lisp signals here, and a dispatch that missed is ; something a program can be written to answer -- a default object, a log ; line, a fallback -- which a trap would take away. `handler-case` around ; the call is the shape, and a method written for `:else` is the other ; answer, in the generic rather than at the call. ; ; `value` is dyn, which is the one field type no other condition here has. ; It is the honest one: a dispatch value is whatever the dispatch answered ; and there is no narrower type it has. The collector reaches it through the ; per-type descriptor a struct with a dyn field carries. ; ; No restart is established at the miss, which is BoundsError's decision ; taken for BoundsError's reason -- see the note above it. struct NoMethod(generic: str, value) :parent Error ; A breakpoint. (pause) stops the program where it stands and hands it to the ; break loop, with the whole stack under it readable — C-c C-b lists the ; frames, TAB opens one, and taking `continue` resumes at the call. ; ; It is spelled `pause` and not `break` because `break` is reserved for ; leaving a loop (parse.ml refuses it by name, with the milestone), and a ; breakpoint and a loop exit in the same word would be the worst kind of ; collision: both are legal in the same place and mean opposite things. ; ; Nothing in the compiler knows about this. It is `error` under a ; `restart-case`, which is exactly what a breakpoint is in a language that ; already has conditions: the break loop is entered because nothing handled ; the condition, and `continue` is an ordinary restart whose body is empty, so ; taking it returns here and the caller carries on. A handler-bind above it ; can therefore also intercept a Pause and decline to stop, which is the ; behaviour a release build wants and gets for free. struct Pause fn pause() -> () restart-case error(Pause{}) restart continue() () ; The stepper's stop, which C-c C-s puts before each form of a defn's body ; (Ast.instrument_step). It is (pause) with an answer: next goes on stepping ; and continue runs the rest of the call, and the instrumented body keeps ; that answer in a local of its own. Like Pause it is not under Error. ; Named so a program's own step or Step is not what the instrumented body ; calls. struct StepPoint fn step-point() -> bool restart-case error(StepPoint{}) restart next() "stop at the next form" true restart continue() "run the rest of this call" false ; A seeded PRNG in Flan rather than libc's, because a grid hash is only a ; regression test if the sequence is byte-identical on native and wasm32 ; (plan.org, RNG is ours). PCG-RXS-M-XS 64: one u64 LCG step per draw, and ; the whole 64-bit state permuted down to a 64-bit result by an xorshift ; whose distance is read off the state's top five bits, a multiply, and a ; final xorshift. ; ; A call to any of the five below is one draw — one LCG step — and never two, ; and that is the property the whole file is arranged around: a program's ; position in the sequence depends on how many numbers it asked for and never ; on which of the five it asked for. The one call that is not a draw is the ; one that is not a number either: a range with nothing in it answers lo ; without touching the generator, which is said again where it happens. ; ; The permutation is a bijection of the state, which is the price of getting ; 64 output bits out of 64 state bits: someone holding one result can run it ; backwards to the state and predict every number after it. That is fine for ; a grid, a spawn point or a shuffle and is not fine for a key or a nonce, ; and there is nothing here that pretends otherwise. once rand-state: u64 = 6364136223846793005 fn rand-seed(seed: u64) -> () rand-state = seed * 6364136223846793005 + 1442695040888963407 ; One draw, all 64 bits of it, every one of them equally likely. This is the ; unbiased full-width draw and the three functions below are the three ways of ; asking for less than all of it. fn rand-int() -> u64 let s = rand-state rand-state = s * 6364136223846793005 + 1442695040888963407 ; The shift distance is (top five bits of s) + 5, so it is between 5 and ; 36 and a 64-bit shift by it is always defined. The 32-bit version of ; this generator had to mask its rotate because a shift by 32 is poison in ; LLVM; at this width there is no such case to guard. ; ; The multiplier is written in hex, which is how it is written everywhere ; it appears: in decimal it is 12605985483714917081. Either spelling is ; a u64 literal and nothing else. It is written here rather than given a ; name of its own: a ; prelude constant is a name in every program, and this is an ; implementation number that nothing outside these four lines wants. let w = ((s >> ((s >> 59) + 5)) ^^ s) * 0xAEF17502108EF2D9 (w >> 43) ^^ w ; In [0, 1), on one draw. The top 53 bits of the draw over 2^53 exactly: 53 is ; the whole mantissa of an f64, so every representable value in the range can ; come up and each is as likely as the format allows. The division is exact ; and so is the conversion — an integer below 2^53 is an f64 with no rounding ; — so the result never reaches 1.0. ; ; The top bits and not the bottom ones, for the reason rand-bool gives. fn rand() -> f64 = f64(rand-int() >> 11) / 9007199254740992.0 ; True half the time, on one draw. The bit taken is the draw's top one. ; ; Which bit to take is a question about what the permutation is doing. Every ; bit of a *draw* is sound; the weak bits belong to the state underneath, ; which is a plain LCG — bit 0 of one modulo 2^64 alternates 0, 1, 0, 1 for ; ever, and the low bits above it have periods barely longer. The output's ; low bits are the ones whose soundness rests entirely on the multiply and ; the two xorshifts having scrambled those in; the output's top bits are ; carried there by the multiply out of the whole width of the state, so they ; do not depend on any one state bit and least of all on a weak one. (They ; are not a copy of the state's top bit either: output bit 63 agrees with ; state bit 63 about half the time, which is what a permutation doing its job ; looks like.) Taking the top costs nothing and asks less of the permutation, ; so that is what it takes. fn rand-bool() -> bool = rand-int() >> 63 == 1 ; ── Slice algorithms, all in place ──────────────────────────────────── ; ; One family per element type, because there are no generics: each of these ; is a *copy* per element type, and the set below is i32 (what indices, ids ; and tile values are), f32 (what positions, velocities and weights are) and ; [const u8] (what a field coming out of `split` is). ; ; A slice is ptr+len and non-owning, so these mutate the storage they were ; handed: sorting (slice grid 4 9) sorts those five elements of grid and ; leaves the rest alone. That was originally forced — there was no allocator ; to return a new sequence from — and it stays the right shape now that there ; is one, because sorting a thing you already own should not allocate. The ; allocating tier is further down, and a caller sorts a Vec by sorting ; (slice v). ; ; **map, filter, reduce and a sort taking a comparator are here now**, in a ; section of their own after the f32 family. They were blocked on *function ; values* and not on generics, which is why they arrived without generics: ; a (Fn [T ...] R) is an ordinary parameter type. What they are still one ; copy per element type for *is* generics — sum-i32 and sum-f32 are the same ; shape and the same argument — so the set is the same i32 and f32 the rest of ; this family covers. ; ── One family, over one type variable ──────────────────────────────── ; ; What used to be a copy per element type. A [$t] binds a type variable in ; the signature and every call site instantiates the body at the types it ; passes, so [(sort xs)] over a [i32] and over a [f32] are two emitted ; bodies from one written one. ; ; **Two things in the signatures are not decoration.** ; ; [{:where (is-ordered $t)}] is what lets the body write [<] at all. A type ; variable supports only what it is declared to support — an unconstrained ; one is refused at the *definition*, not at some later call site — and ; [is-ordered] is the predicate that admits [<], [<=], [>], [>=], [min] and ; [max]. It admits [=] too: every type the language orders is a number or an ; enum, so it is equatable. ; ; There is no [copyable?] any more. Since the repeal every value copies — ; a container copies as its header, the copies alias one buffer, and what ; the copies then do is the program's business, as it is in Odin. A body ; that reads an element into a local and writes it into another slot is ; duplicating a header when the element owns storage, and nothing here ; says otherwise any more: that sentence moved from a predicate into this ; comment, which is where Odin keeps it too. ; ; **What did not collapse, and why it should not.** [sum-i32] and [sum-f32] ; widen their element into [i64] and [f64]; "the wider type $t accumulates ; into" is a type-level function, which is a constraint system of a different ; kind, and a generic [sum] that took its accumulator and its [+] would just ; be [reduce]. [append-i64] and [append-f64] are two different primitives. ; [sort-bytes] needs [is-bytes-less] rather than [<] — a [[u8]] is not [is-ordered] ; and cannot be — so it is [sort-by] with the comparison written in, and it ; keeps its name because the stability contract in its comment is worth ; keeping attached to something. fn swap(s: [$t], i: i32, j: i32) -> () let t = s[i] s[i] = s[j] s[j] = t fn reverse(s: [$t]) -> () let i = 0 j = length(s) - 1 while i < j swap(s, i, j) i += 1 j -= 1 ; Insertion sort: in place, no recursion, no auxiliary array — quicksort ; would want a stack and mergesort a buffer, and neither exists. Ascending, ; and stable, though with no payload type to carry that is not yet ; observable. ; ; One caveat that only arises at f32: **a NaN in the input makes the order ; undefined.** Every comparison against a NaN is false, so the insertion loop ; never moves one and never moves anything past one; what comes out is sorted ; within each run between NaNs and not sorted across them. That is what C's ; qsort with a naive comparator does too, and the only fix is not to have ; NaNs in the array — there is no ordering of the reals a NaN sits anywhere ; in. fn sort(s: [$t]) -> () where is-ordered($t) let i = 1 while i < length(s) let j = i ; `and` short-circuits, which is load-bearing: at j = 0 the left test ; fails and (at s -1) is never evaluated, so this does not trap. while j > 0 and s[j - 1] > s[j] swap(s, j - 1, j) j -= 1 i += 1 ; The same insertion sort, with the one comparison it had written in replaced ; by the one it is told. is-before answers "does a come before b", so passing ; (fn [a b] (< a b)) is ascending and reversing it is descending — and a ; caller wanting a key rather than an order writes the comparison. ; ; It is stable exactly as sort is: the loop stops the moment is-before says ; no, so equal elements never swap past each other. A is-before that is not a ; strict weak ordering — one answering true for both (a b) and (b a) — is the ; caller's mistake and shows up as an order, not as a loop: the inner while ; is bounded by j reaching 0 whatever the comparison says. ; ; This one needs no [is-ordered]: the comparison it cannot have is the ; comparison it is given. It is the shape every generic had to take before ; predicates existed, and it stays because passing a comparison is a real ; thing to want and not only a workaround. fn sort-by(s: [$t], is-before: Fn($t, $t) -> bool) -> () let i = 1 while i < length(s) let j = i while j > 0 and is-before(s[j], s[j - 1]) swap(s, j - 1, j) j -= 1 i += 1 ; The first index holding x. None rather than -1, because Option is what the ; language has and a sentinel index is the bug this avoids. fn index-of(s: [const $t], x: $t) -> Option(i32) where is-equal($t) for i in range(length(s)) if s[i] == x return Some(i) None ; None for an empty slice: there is no least i32 that is also an honest ; answer, and returning one would be a value the caller cannot tell from a ; real element. A NaN in the input is not special-cased and propagates the ; way it does through the builtins — the comparison fails, so the running ; value simply does not change. ; ; Named min-of rather than min because [min] and [max] are builtins over two ; or more numbers, and a defn cannot shadow a builtin: nothing shadows [+] ; either. These reduce a slice, which is a different operation with a ; different arity, so the different name is honest rather than a workaround. ; A type's own limits are (min-value T) and (max-value T). fn min-of(s: [const $t]) -> Option($t) where is-ordered($t) if length(s) == 0 None else let m = s[0] for i in range(length(s)) m = min(m, s[i]) Some(m) fn max-of(s: [const $t]) -> Option($t) where is-ordered($t) if length(s) == 0 None else let m = s[0] for i in range(length(s)) m = max(m, s[i]) Some(m) ; ── Opening an Option without writing the match ─────────────────────── ; ; Everything above answers an (Option $t), and until now `match` was the only ; thing that could open one. That is the right *primitive* — it is the form ; that makes the empty case unforgettable — and it is the wrong thing to write ; when the empty case is one word: ; ; (match (parse-i64 s) (Some v) v None (i64 0)) ; (or-else (parse-i64 s) (i64 0)) ; ; **Neither takes a {:where}, and that is a decision rather than an ; oversight.** A predicate buys an *operation* on the variable — [is-ordered] ; is what lets sort write `<` — and these perform no operation on their ; payload at all: they move it out of the Option, or they look at the tag and ; never touch the payload. That is the one move [ident] in ; test/programs/generics.flan makes, which needs nothing declared, so these ; instantiate at every type including the ones that own storage. ; ; Two, and the ones that were declined are worth naming because a reader will ; look for them: ; ; none? (not (is-some o)) is the whole of it, and this file already ; refuses a wrapper whose only method is the thing it wraps ; — see the Builder entry under "Still refused". ; an unwrap that Refused for the reason file-size below is an Option in the ; signals on None first place: absence is a reply and not a fault, and ; making it a condition puts a handler search on the ; ordinary path. Whether an empty Option is an error is the ; *caller's* question, and the caller has handler-bind if ; the answer is yes. ; a lazy or-else Would need a (Fn [] $t) — sort-by's shape, available the ; day something wants it. A macro would get laziness for ; free and need no generics, and costs more than it buys ; here: a prelude macro drops every prelude defn that ; depends on it out of a macro-module build (see the ; bootstrap hook at the foot of this file), and a macro has ; no way to report a malformed call. ; This is Java's `Optional.orElse`, hyphenated: eager, and it answers the ; payload's type rather than another Option. Worth saying which, because Rust ; spells something else `or_else` — there it takes a closure and answers an ; `Option`, so borrowing that name for this behaviour would be the wrong ; loan twice over. Java's own lazy sibling is `orElseGet`, which is the one ; declined above. ; ; **At a $t that owns storage the result is a header copy, and the branch not ; taken is still the caller's to free.** Since the copyable? repeal every ; value copies as its header and the copies alias one buffer (see the section ; comment above), so (or-else o d) over a (Vec u8) hands back a second header ; onto o's block or onto d's — and the one it did not choose was never ; released by anything here. That is the same contract `at` on a slice of Vecs ; has; it is written down here because an "or a default" reads like it ; consumes the default and it does not. fn or-else(o: Option($t), d: $t) -> $t match o Some(v) -> v None -> d ; Clojure's `is-some`, spelled with the is- a predicate takes. It is what makes a `when` or a `cond` possible ; at all — or-else can say "this or that" and cannot say "only if there is ; one" — and it is the honest shape for the case where the payload is not ; wanted, which a match would still have to bind a name for. fn is-some(o: Option($t)) -> bool match o Some(_v) -> true None -> false ; map-in-place writes back into the slice it was handed, for the same reason sort ; does — a slice is non-owning, and transforming a thing you already own ; should not allocate. A map that produces a *different* element type is not ; here: it is two type variables and a second signature, and nothing has ; wanted it. fn map-in-place(s: [$t], f: Fn($t) -> $t) -> () for i in range(length(s)) s[i] = f(s[i]) ; The general fold, of which sum-i32 is the special case with the + written ; in. The accumulator comes first in the step, which is the order that reads ; as (f acc x) and the order Odin's slice.reduce uses. fn reduce(s: [const $t], init: $t, f: Fn($t, $t) -> $t) -> $t let acc = init for i in range(length(s)) acc = f(acc, s[i]) acc ; A new Vec holding the elements the predicate kept, in the order they were ; in. Owned by the caller: (free v), or let a (free-all a) take the region. ; ; This is the one that proves the containers and the generics compose. It ; allocates — (vec-new $t), push, returns (Vec $t) — and the type-erased Vec ; runtime needed no change at all, because SizeOf and AlignOf are computed at ; the instantiation site, where the element type is concrete. fn filter(s: [const $t], is-keep: Fn($t) -> bool) -> Vec($t) let v = vec-new($t) for i in range(length(s)) if is-keep(s[i]) push(v, s[i]) v ; A map's keys, and its values, as a new Vec the caller owns. In block order, ; which is the hash's and not the insertion's — sort what comes back if the ; order matters. A string key is copied as the view it is, so the Vec reads ; the map's own key bytes and is good for as long as they are. fn map-keys(m: Map($k, $v)) -> Vec($k) where is-hashable($k) let out = vec-new($k) cur = i64(0) key: $k = zeroed() while map-next(m, addr(cur), addr(key)) push(out, key) out fn map-values(m: Map($k, $v)) -> Vec($v) where is-hashable($k) ; Walked by key and read back with get, because a place to copy a value ; into would have to be zeroed first, and a function value cannot be. let out = vec-new($v) cur = i64(0) key: $k = zeroed() while map-next(m, addr(cur), addr(key)) match get(m, key) Some(val) -> push(out, val) None -> () out ; ── The sign questions, over every numeric type at once ─────────────── ; ; The family the whole of generics was asked for. Three questions about a ; number's sign, one body each, answering at i8 through u64 and at both ; float widths — where without a type variable they would be three functions ; per width, which is why they were never written at all. ; ; What makes them writable is not the type variable on its own: it is that a ; written 0 may stand where $t stands. That needs the {:where (is-numeric $t)} ; clause and nothing weaker, because the bound is what promises the literal ; has a meaning at every type the variable can become. An unconstrained ; variable is refused, and so is [is-ordered] — it admits an enum, which holds ; no number. ; ; The comparison is the clause's too: [is-numeric] entails [is-ordered], so one ; predicate on the line gives the body both the < it writes and the 0 it ; writes it against. ; ; **The unsigned instantiations are not mistakes.** (is-neg (u8 3)) is false at ; every u8 and the copy is a constant, which a reader may find odd in the ; emitted code and which is exactly right: a generic is copied per written ; type, and the body says what it says at each of them. Refusing the copy ; would mean a bound that spells "signed", and there is no such predicate. fn is-pos(x: $t) -> bool where is-numeric($t) = x > 0 fn is-neg(x: $t) -> bool where is-numeric($t) = x < 0 ; Named is-zero rather than =0 because it reads as the question it is. The ; float instantiations answer true for both zeros, since -0.0 = 0.0 is what ; IEEE says and this does not second-guess it. fn is-zero(x: $t) -> bool where is-numeric($t) = x == 0 ; ── The per-type layer that stays ───────────────────────────────────── ; ; sum is the one shape a type variable cannot express, and it is worth being ; precise about why rather than leaving two near-identical functions looking ; like an oversight. Each of these *widens*: sum-i32 accumulates in i64 and ; sum-f32 in f64, because summing a screenful into the element's own type is ; how a total silently wraps or absorbs. The per-element casts no longer have ; to be written to say so — an i32 widens into an i64 by itself — and they ; stay because what these two functions exist to show is ; that the accumulator is a different type from the element. "The wider ; type $t accumulates into" is a function from types to types — an associated ; type, or a constraint system of a kind {:where} is not — and a generic sum ; that took its accumulator and its + as parameters would be reduce, which is ; above. ; Accumulates in i64, because summing a screenful of i32 into an i32 is how a ; total silently wraps. The per-element (i64 ...) would happen on its own now; ; it is written to keep the accumulator's type visible at the line that feeds ; it. fn sum-i32(s: [const i32]) -> i64 let t = i64(0) for i in range(length(s)) t += i64(s[i]) t ; Accumulates in f64 and widens each element explicitly, which is sum-i32's ; argument in its floating form and a stronger one: summing a screenful of ; f32 in f32 does not wrap, it *absorbs* — once the running total is large ; enough, adding a small element rounds to no change at all, and the answer ; is silently short rather than obviously wrong. An f64 accumulator has 29 ; more bits of mantissa and pushes that failure out of reach of any array a ; game holds. fn sum-f32(s: [const f32]) -> f64 let t = 0.0 for i in range(length(s)) t += f64(s[i]) t ; ── Bytes ───────────────────────────────────────────────────────────── ; ; Over [const u8] and not over string, so (bytes-view s) is what a caller writes ; and one copy of each serves strings and byte slices both, writable or not — which is as close to a ; generic as a language without them gets. Nothing here allocates: every ; result is a bool, an index, or a number. fn is-bytes-equal(a: [const u8], b: [const u8]) -> bool if length(a) != length(b) false else for i in range(length(a)) if a[i] != b[i] return false true ; The length test comes first and `and` short-circuits, so the slice is only ; built once it is known to be in bounds — otherwise a prefix longer than the ; string would trap rather than answer false. fn has-prefix(s: [const u8], p: [const u8]) -> bool length(p) <= length(s) and is-bytes-equal(slice(s, 0, length(p)), p) fn has-suffix(s: [const u8], p: [const u8]) -> bool length(p) <= length(s) and is-bytes-equal(slice(s, length(s) - length(p), length(s)), p) ; The whole slice is an integer, or it is None. bytes->i64 is strtoll, which ; answers 0 for "" and for "abc" and stops at the first junk byte in "12x" — ; three wrong answers a caller cannot tell from a real 12. This is also the ; one that has to be Flan rather than the primitive: strtoll is locale- and ; libc-dependent, and a parser in the language gives the same answer on ; wasm32 as on native for the same reason rand does. ; Overflow wraps, as all arithmetic here does; it is not reported. fn parse-i64(s: [const u8]) -> Option(i64) let i = 0 n = i64(0) neg = false if length(s) == 0 return None if s[0] == \- or s[0] == \+ neg = s[0] == \- i = 1 if i == length(s) return None ; a lone sign is not a number while i < length(s) let b = s[i] if b < \0 or b > \9 return None n = n * 10 + i64(b - \0) i += 1 if neg then Some(0 - n) else Some(n) ; ── The limits of each numeric type ─────────────────────────────────── ; ; What C spells INT_MAX and FLT_MAX, and what nothing here could reach for: ; cimport pulls in declared functions, structs and typedefs, never a #define, ; so limits.h and float.h have no way in. These are written out instead, once, ; where every program already sees them. ; ; Kebab-case and the type's own name, like ns-per-second above: i32-max, not ; I32_MAX and not INT_MAX. The type prefix is the type as the language spells ; it, so the constant for a u8 is u8-max and there is nothing to translate. ; ; Each carries its type, which is the point of them — i32-max is an i32 and ; putting it where a u8 is wanted is a type error rather than a silent 255. ; That also means the pair for a type is the pair the *language* has, so ; u8-min is here beside u16-min and u32-min and u64-min, all of them zero: a ; family with a hole in it is worse than four lines that say nothing ; surprising, and code generated over a list of type names needs the hole ; filled. ; ; u64-max is written in hex and it has to be. The reader parses a decimal ; integer into an i64, and 18446744073709551615 does not fit one; the hex ; spelling is read as the 64-bit pattern it names, which is what a u64 ; literal is here (see [Check.in_range], which accepts any pattern at 64 bits ; unsigned for exactly this reason). i64-min's decimal spelling *does* fit, ; since it is i64's own least value, so it is written the ordinary way. const i8-max: i8 = 127 const i8-min: i8 = -128 const i16-max: i16 = 32767 const i16-min: i16 = -32768 const i32-max: i32 = 2147483647 const i32-min: i32 = -2147483648 const i64-max: i64 = 9223372036854775807 const i64-min: i64 = -9223372036854775808 const u8-max: u8 = 255 const u8-min: u8 = 0 const u16-max: u16 = 65535 const u16-min: u16 = 0 const u32-max: u32 = 4294967295 const u32-min: u32 = 0 const u64-max: u64 = 0xFFFFFFFFFFFFFFFF const u64-min: u64 = 0 ; The floats are three questions and not two, which is why there is no ; f32-min here to sit beside f32-max. ; ; A float's least value is just the negation of its greatest — (- f32-max) ; — so a constant for it would say nothing the language cannot. What a caller ; actually reaches for under the name "min" is the smallest positive one, and ; that is a different number entirely. Naming it f32-min would make the two ; readings collide at the worst possible place, so the name says which it is: ; f32-min-positive, the smallest *normal* positive value, as Rust's ; MIN_POSITIVE does. Below it the subnormals run further down still, trading ; mantissa bits for exponent range; nothing here names one, because a program ; that wants the last subnormal wants to say so. ; ; The epsilons are the gap from 1.0 to the next representable value above it — ; 2^-23 and 2^-52, the mantissa widths — and not "the smallest number you can ; add to anything". That distinction is the whole reason a comparison written ; (< (abs (- a b)) f64-epsilon) is wrong for any a and b of interesting size, ; and the reason this is named epsilon and not tolerance. ; ; Every decimal below is the shortest one that round-trips to the exact value ; intended, and each is pinned against an independent derivation in ; test/programs/limits.flan rather than trusted. The infinities and NaNs, ; f64-inf, f64-nan, f32-inf and f32-nan, are not here: no literal writes one, ; so the checker supplies them (Check.special_float). const f32-max: f32 = 3.4028234663852886e38 const f64-max: f64 = 1.7976931348623157e308 const f32-min-positive: f32 = 1.1754943508222875e-38 const f64-min-positive: f64 = 2.2250738585072014e-308 const f32-epsilon: f32 = 1.1920928955078125e-07 const f64-epsilon: f64 = 2.220446049250313e-16 ; ── Numbers ─────────────────────────────────────────────────────────── ; ; Only the ones that encode a decision. abs is (max x (- 0 x)); a wrapper over ; that is a function emitted into every program to save a caller nothing. The ; one honest caveat on that abs: at the least representable integer it ; answers itself, because the negation wraps. That is what every two's-complement abs does, a ; function here would do it too, and the only fix is not to hand it that ; value — so it is written down rather than wrapped. ; ; The float abs is the same one-liner, (max x (- 0.0 x)), and it is not ; wrapped for the same reason — but the caveat above does not carry over, so ; it is not inherited by silence. f32 negation is exact at every value, there ; is no least representable float that negates to itself, and the two edge ; inputs both come out right: -0.0 answers +0.0 (the max picks the subtracted ; side, since neither zero is greater than the other), and a NaN answers a ; NaN (every comparison fails, so the same max picks the subtracted side, ; which is still a NaN). There is nothing left for a function to fix. ; clamp is a macro and not a function, and the reason is the objection above ; turned around rather than dropped. min and max are builtins that work at ; every numeric type; a clamp *function* cannot, because there are no ; generics, so it would be one copy per type — a clamp-i32, a clamp-f32, a ; clamp-i64 — each emitted into every program to save a caller eleven ; characters. A macro is type-agnostic for free and emits nothing at all: what ; the program contains after expansion is the (min hi (max lo x)) the caller ; would have written. ; ; Each of x, lo and hi appears exactly once in the expansion, so nothing here ; is evaluated twice and an argument with a side effect behaves as it reads. ; ; lo above hi is not checked, and the answer there is hi — the outer min wins. ; That is the same rule Odin's clamp follows and there is nowhere better to ; put a complaint: a macro has no error facility (see `unless` at the foot of ; this file), so a diagnostic would have to be a run-time one, in the one ; construct whose whole point is that it costs nothing at run time. macro clamp(& args) if length(args) != 3 quote clamp-takes-a-value-a-low-and-a-high() else quote min(~(args[2]), max(~(args[1]), ~(args[0]))) ; Zero for zero, and zero for NaN — neither is positive nor negative, so ; neither comparison fires. A caller that needs to know which it got should ; be testing for NaN, not reading a sign. fn sign-f32(x: f32) -> f32 if x > 0.0 1.0 elif x < 0.0 -1.0 else 0.0 ; Written as the weighted sum and not as a + t*(b - a): the second form does ; not return b exactly at t = 1.0 once rounding is involved, and a position ; that does not arrive is the bug an interpolation gets reported for. fn lerp(a: f32, b: f32, t: f32) -> f32 = (1.0 - t) * a + t * b ; ── More of the RNG ─────────────────────────────────────────────────── ; ; Each is one rand-int, as rand and rand-bool above are, so the sequence a ; program consumes is fixed by how many numbers it asked for and not by which ; of the five it asked for. The exception is the one range that has no number ; in it to give: an empty or reversed range answers lo without drawing, so a ; program that asks for one is where it was. Neither touches the generator ; otherwise. ; [lo, hi), at i64 — the width of the draw, so that no range is out of reach. ; An empty or reversed range answers lo, a defined value rather than a ; remainder by zero, which is immediate undefined behaviour and not a wrong ; number. hi - lo is computed with wrapping arithmetic and read as a u64, so ; the span is right even for a range as wide as the whole of i64. ; ; One draw when there is a number to draw, and therefore modulo bias: the low ; (2^64 % span) values of the range come up very slightly more often — for any ; span a program is likely to ask for, too slightly to measure. Rejection ; sampling would remove it and would consume an unpredictable number of draws, ; which is the one thing this generator exists not to do. ; ; An index is an i32 here (length answers one), so indexing with this reads ; (at xs (i32 (rand-int-range 0 (i64 (length xs))))). fn rand-int-range(lo: i64, hi: i64) -> i64 if hi <= lo then lo else lo + i64(rand-int() % u64(hi - lo)) ; [lo, hi), because rand never reaches 1.0. One draw, and f64 because rand is. fn rand-float-range(lo: f64, hi: f64) -> f64 = lo + rand() * (hi - lo) ; ── Rounding, and the one thing that is not Flan ────────────────────── ; ; All three answer an f32 and take the f32 path, because that is what a ; position, a tile coordinate and a velocity are here. f64 versions wait for ; a program that wants them, for the same reason the f32 slice algorithms do. ; ; The cast to i32 truncates toward zero, which is the only rounding mode the ; language has, so each of these is that cast plus the correction the mode ; does not make. Three inputs would make the cast itself undefined and each ; is named before it happens: NaN (which fails every comparison, so it is ; tested for by (not (= x x)) and nothing else), and the two infinities, ; which are caught by the magnitude test. Above 2^23 an f32 has no fractional ; bits left at all, so returning x there is not an approximation — it is the ; answer — and it doubles as the guard that keeps the cast inside i32. ; Zero is returned as itself rather than through the cast, which would turn ; -0.0 into +0.0. That is one line for a value most callers never look at, ; and it is here because floorf is specified to return it: a sign of zero is ; how a caller recovers which side a position approached from once the ; magnitude has already been rounded away. fn floor-f32(x: f32) -> f32 if not (x == x) or x >= 8388608.0 or x <= -8388608.0 or x == 0.0 x else let t = f32(i32(x)) if t > x then t - 1.0 else t ; One deviation from C's ceilf, written down rather than branched around: ; between -1.0 and 0.0 this answers +0.0 where IEEE asks for -0.0, because ; the outer (- 0.0 …) is a subtraction and not a negation. Nothing here reads ; the sign of a zero; a caller that does should test the input instead. fn ceil-f32(x: f32) -> f32 = 0.0 - floor-f32(0.0 - x) ; Half away from zero, which is C's round and not the even-tie rule: -2.5 ; goes to -3. Written as floor of the *magnitude* and mirrored, because ; (floor-f32 (+ x 0.5)) is wrong twice over — it is half-*up* rather than ; half-away for negatives, and at the largest f32 below 0.5 the addition ; itself rounds to 1.0 and answers 1 for a number under a half. fn round-f32(x: f32) -> f32 let m = if x < 0.0 then 0.0 - x else x f = floor-f32(m) r = if m - f >= 0.5 then f + 1.0 else f if x < 0.0 then 0.0 - r else r ; sqrt is the one function in this file that is not Flan, and it is a ; `declare` rather than a body for a reason that is not laziness. Every other ; number here is reachable from the four operations and a cast; a square root ; is not. Newton's method needs a starting guess, a good one comes from ; reinterpreting the exponent bits, and the language has no bit-cast between ; f32 and u32 — only value-preserving casts. Without it the iteration needs a ; scaling loop to normalise, converges slowly from a poor guess, and produces ; a result that is *close*, which is exactly what 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 — the byte-identical property that ; keeps rand-int in Flan is, for this one, an argument for going out to C. ; ; The cost is one `declare` line in every module, which LLVM drops where it ; is unused, and one -lm on every link, which build.ml now passes. That flag ; is not optional and not obvious: at -O2 LLVM folds most sqrtf calls into ; the hardware instruction and nothing is left to resolve, so this appears to ; link without it and then fails at -O0, where the call survives. ; ; The better fix belongs to the compiler and not here: llvm.sqrt.f32 as a ; builtin in check.ml and emit.ml is one instruction with no symbol at all. declare(sqrt-f32, [x f32], f32, "sqrtf") ; sin and cos go out to libm too, and the argument is *not* the one above — ; it is weaker, and which way it is weaker is the thing to know before ; calling them. IEEE-754 requires sqrt to be correctly rounded, which is why ; sqrtf's answer is the same bit pattern wherever it runs. It requires ; nothing of the kind for sinf and cosf: each implementation is free to be a ; fraction of an ulp off in its own direction, and glibc, musl and wasi-libc ; do differ. So these two are the one place in this file where native and ; wasm32 may not agree bit for bit, and a program whose output is hashed ; across targets — the sand grid of plan.org's "RNG is ours", which is why ; rand-int above is written in Flan and not called out of libc — must not ; route that hash through a sine. ; ; They are here anyway, because the alternative on offer today is worse: a ; caller that wants an angle writes the same two `declare` lines at the top ; of its own file (examples/core-input-gestures-testbed.fln did, before ; this), which is the identical libm call with the identical caveat and ; nobody's name on it. One copy with the caveat written down beats a copy per ; file with none. ; ; The fix, if a program ever does need trig that agrees across targets, is a ; body rather than a declare: Cody-Waite reduction onto [-pi/4, pi/4] and a ; minimax polynomial, which is reachable from the four operations and ; floor-f32 and would therefore be exactly as reproducible as rand-int. That ; is a numerics job with its own accuracy budget, and it waits for a program ; that needs it. declare(sin-f32, [x f32], f32, "sinf") declare(cos-f32, [x f32], f32, "cosf") ; atan2 and pow inherit the paragraph above in full, and not the sqrt one. ; IEEE-754 requires nothing of atan2f or powf either, so these are the third ; and fourth places in this file where native and wasm32 may disagree in the ; last bit, and the sand-grid rule stands unchanged: a hash compared across ; targets must not be routed through any of the four. ; ; They are here for the reason the trig pair is. Without them a program that ; wants a heading or a falloff curve writes the identical two declare lines at ; the top of its own file, which is the same libm call with the same caveat ; and nobody's name on it. ; ; atan2's y comes first, as it does in C, and the order is the answer rather ; than a convention: knowing the quadrant of (y, x) is the whole of what it ; has over (atan (/ y x)), and it is recovered from the two signs. It is ; defined at x = 0, where the division is not. ; ; One caveat of pow-f32's own, because it is the one that gets reported as a ; bug: it is not exact at integer exponents. powf goes through a logarithm, ; so (pow-f32 10.0 2.0) is 100.0 or the float next to it depending on the ; libm, and an index computed by casting that to i32 is off by one on the ; wrong side. A small integer power is a multiplication, and should be ; written as one. declare(atan2-f32, [y f32 x f32], f32, "atan2f") declare(pow-f32, [x f32 y f32], f32, "powf") ; ── The rest of libm, and both widths ───────────────────────────────── ; ; The five above were the whole of it for a long time, and the reason they ; were is the reason the rest are here now: every one of these is a line, a ; symbol that is already on the link, and nothing in the compiler. A program ; that wanted a logarithm wrote the `declare` at the top of its own file — ; which is the identical call with none of the caveats written down and ; nobody's name on it. ; ; **The f64 half is not decoration.** f32 is what a position and a colour ; are, and f64 is what a *measurement* is: the clock below is nanoseconds in ; an i64 and seconds in an f64, parse-f64 and format-f64 are both f64, and a ; sum over more than a few thousand f32 terms has already lost the low bits ; the answer was about. Having only the f32 face forced a cast down and back ; at every one of those boundaries, and a cast down is where the precision ; went. ; ; The split below is the one the sqrt paragraph draws, applied to the whole ; family, and it is the only thing here worth knowing before calling: ; ; **Exact on every target.** IEEE-754 specifies these as exact operations ; or as correctly rounded, so the answer is the same bit pattern under ; glibc, musl and wasi-libc, and a hash taken across targets may be routed ; through them. sqrt, fabs, floor, ceil, round, fmod. ; ; **Not.** IEEE-754 requires nothing of these and the three libms do ; differ in the last bit. The sand-grid rule from the sin/cos paragraph ; above covers all of them without change: a value compared across targets ; must not have been through one. Everything else here. declare(sqrt-f64, [x f64], f64, "sqrt") ; Magnitude, and the f32/f64 pair is libm's because fabs is a sign-bit clear ; that the compiler folds into one instruction — cheaper than the branch a ; Flan body would be, and right for -0.0 and for NaN, which a (< x 0.0) test ; is not: -0.0 is not less than zero, so the branch returns it unchanged and ; hands back a negative zero from a function named abs. declare(abs-f32, [x f32], f32, "fabsf") declare(abs-f64, [x f64], f64, "fabs") ; The f64 faces of the three rounding functions floor-f32, ceil-f32 and ; round-f32 are libm's rather than Flan's, and that is not an inconsistency. ; Those three are Flan because of a cast: (i32 x) is the whole of floor-f32's ; body, and it works precisely because every f32 with a fractional part fits ; in an i32. At f64 it does not — the exact range runs to 2^53 and i64's cast ; would have to carry its own guard — so the trick that made them free is not ; available and the libm call is both shorter and exact. declare(floor-f64, [x f64], f64, "floor") declare(ceil-f64, [x f64], f64, "ceil") declare(round-f64, [x f64], f64, "round") ; Remainder, and it is C's fmod and not a modulo: the sign follows the ; *dividend*, so (fmod-f32 -1.0 3.0) is -1.0 and not 2.0. An angle wrapped ; into [0, tau) therefore needs the add-and-fmod-again that every wrap ; function has, and this is the line where that is written down rather than ; discovered. It is exact — the result is the true remainder, representable ; by construction — so it belongs to the first group above. declare(fmod-f32, [x f32 y f32], f32, "fmodf") declare(fmod-f64, [x f64 y f64], f64, "fmod") ; The trigonometric family, in full and at both widths. tan is separate from ; (/ (sin-f32 x) (cos-f32 x)) for the reason atan2 is separate from a ; division: near pi/2 the quotient is a ratio of two small errors and tanf ; is not. declare(tan-f32, [x f32], f32, "tanf") declare(sin-f64, [x f64], f64, "sin") declare(cos-f64, [x f64], f64, "cos") declare(tan-f64, [x f64], f64, "tan") ; The inverses. asin and acos answer NaN outside [-1, 1] rather than ; clamping, which is what catches a dot product that drifted to 1.0000001 ; through rounding — clamp it at the call site, on purpose, and the drift is ; visible instead of silently becoming an angle of zero. declare(asin-f32, [x f32], f32, "asinf") declare(acos-f32, [x f32], f32, "acosf") declare(atan-f32, [x f32], f32, "atanf") declare(asin-f64, [x f64], f64, "asin") declare(acos-f64, [x f64], f64, "acos") declare(atan-f64, [x f64], f64, "atan") declare(atan2-f64, [y f64 x f64], f64, "atan2") ; Logarithms and the exponential. log is the natural one, as in C and unlike ; the spreadsheet convention — log2 and log10 are the other two and are named ; for their bases, so nothing here is ambiguous. log2 is not (/ (log x) ; (log 2.0)): it is exact at every power of two, which is the whole reason a ; bit-width or an octave is computed with it. ; ; All four answer -inf at zero and NaN below it rather than signalling. A ; condition per logarithm would cost a handler search on a path whose callers ; are loops over samples, and NaN is the value that propagates to wherever ; the caller does check. declare(log-f32, [x f32], f32, "logf") declare(log2-f32, [x f32], f32, "log2f") declare(log10-f32, [x f32], f32, "log10f") declare(exp-f32, [x f32], f32, "expf") declare(log-f64, [x f64], f64, "log") declare(log2-f64, [x f64], f64, "log2") declare(log10-f64, [x f64], f64, "log10") declare(exp-f64, [x f64], f64, "exp") declare(pow-f64, [x f64 y f64], f64, "pow") ; hypot over (sqrt-f32 (+ (* x x) (* y y))) because the obvious form ; overflows on inputs the answer does not: the square of an f32 above ~1.8e19 ; is infinity, so a distance between two far-apart points comes back inf when ; the distance itself is perfectly representable. hypotf scales first. It ; costs more than the naive form and is worth it exactly when the naive form ; is wrong. declare(hypot-f32, [x f32 y f32], f32, "hypotf") declare(hypot-f64, [x f64 y f64], f64, "hypot") ; Cube root, and it is here because (pow-f32 x 0.33333334) is not it: pow ; goes through a logarithm, which is undefined for a negative base, so the ; obvious spelling answers NaN for every negative number where cbrt answers ; the negative root. declare(cbrt-f32, [x f32], f32, "cbrtf") declare(cbrt-f64, [x f64], f64, "cbrt") ; Integer magnitude, one body for every integer width. The per-width pair — ; abs-i32 and abs-i64 — waited here on a bound that spells "an integer ; type", and is-integer is that bound, so they collapsed into this on ; 2026-09-20. TODO.org, "abs is one generic, and a bound joins to the wider ; type". ; ; **The bound is is-integer and not is-numeric, and that is the whole design.** ; is-numeric admits f32 and f64, and this body is the wrong abs for a float: ; (< -0.0 0) is false, so it hands back a negative zero from a function ; named abs. There is no float-safe spelling of the body either — (max x ; (- 0 x)) picks whichever zero sits in the wrong slot, since -0.0 and 0.0 ; compare equal. The right float abs is a sign-bit clear, which is libm's ; fabs, declared above as abs-f32 and abs-f64; a caller with a float writes ; those, and (abs 1.5) is refused with the bound named rather than shadowing ; them with a quiet wrong answer. One capability, one spelling per side of ; the integer/float line — not one per width, which is what this collapse ; ends. ; ; The most negative value of each width has no positive counterpart, and this ; does not special-case it: the subtraction is the same subtraction written ; anywhere else and meets whatever the build's overflow rule is. Saturating ; to the maximum would be a wrong answer returned quietly, which is the one ; thing this file does not do. The unsigned instantiations are the identity, ; for the reason is-pos gives about its own: a generic is copied per written ; type, and at a u32 the body says what it says. fn abs(x: $t) -> $t where is-integer($t) if x < 0 then 0 - x else x ; pi and tau at both widths, because a defconst has a type and a cast between ; them is where digits go missing. tau is 2pi and is written out rather than ; multiplied, so the f32 one is the nearest f32 to tau and not twice the ; nearest f32 to pi — which is the same number here and is not guaranteed to ; be for the derived form in general. ; ; Both are given to more digits than either width holds. That is deliberate: ; the literal is rounded once, by the compiler, to the nearest value of the ; declared type, which is the best available answer and is the same answer on ; both targets. const pi-f32: f32 = 3.14159265358979323846 const pi-f64: f64 = 3.14159265358979323846 const tau-f32: f32 = 6.28318530717958647692 const tau-f64: f64 = 6.28318530717958647692 ; ── The clock ───────────────────────────────────────────────────────── ; ; Until this section nothing in the language could tell the time. A game got ; one from raylib and a program that was not a game had none at all, which ; made "how long did that take" unanswerable in a tool — the half of daily ; use that has no window. ; ; **Two clocks, and they are not interchangeable.** This is the whole of what ; a caller has to know, and the names are chosen so that picking the wrong ; one reads wrong: ; ; `monotonic-…` measures. It never goes backwards, it is not moved by NTP ; or by a user setting the clock, and its zero is arbitrary — the first ; time the program reads it. It is meaningless on its own and correct as a ; difference. ; ; `unix-…` dates. Seconds (or nanoseconds) since 1970-01-01 UTC, which is ; what goes in a file, a log line or a save. It *can* jump, forwards or ; backwards, so a duration computed from two readings of it can be ; negative, and timing anything with it is the bug this pair exists to make ; hard to write. ; ; Odin draws exactly this line and this is its shape: core/time/time.odin has ; `Time` for the date and `Tick` for the measurement, both an i64 of ; nanoseconds, and core/time/time_linux.odin implements them as REALTIME and ; MONOTONIC. The nanosecond integer is the primitive there and the f64 of ; seconds is derived, which is why it is derived here too — three C functions, ; six names. ; ; **Which face to use.** The i64 of nanoseconds is exact and is what a ; difference should be taken in. The f64 of seconds is what a frame loop ; wants, and it is the shape raylib's `get-time` already answers with ; (vendor/raylib/raylib.fln, `(declare-c get-time [] f64 "GetTime")`), so the ; two mix without a conversion at every site. The monotonic origin is latched ; at the first read rather than being boot — see runtime/flan_rt.c — so that ; the f64 stays integer-exact in nanoseconds for a hundred days of process ; life, which a boot-relative clock on a long-lived machine does not. declare(monotonic-ns, [], i64, "flan_monotonic_ns") declare(unix-ns, [], i64, "flan_unix_ns") ; Nanoseconds, so the caller writes the unit rather than counting zeroes, and ; so that a duration in the language is one type rather than a per-unit ; family. Odin spells the same idea as `Duration` constants in core/time. const ns-per-microsecond: i64 = 1000 const ns-per-millisecond: i64 = 1000000 const ns-per-second: i64 = 1000000000 fn monotonic-seconds() -> f64 = f64(monotonic-ns()) / 1000000000.0 fn unix-seconds() -> f64 = f64(unix-ns()) / 1000000000.0 ; **Not a frame limiter.** A sleep asks the operating system to stop this ; thread for *at least* the time given and says nothing about the upper ; bound: a default Linux kernel wakes a sleeper on the timer tick after the ; deadline, so a request for one millisecond commonly returns after rather ; more, and the error is on the late side every time. A frame loop that ; sleeps a fixed slice per frame therefore runs slow and drifts; the shape ; that works is to sleep until a deadline computed from `monotonic-ns` and to ; recompute it from the same clock each turn, so that a long frame is ; absorbed instead of accumulated. ; ; A zero or negative request returns immediately rather than being refused, ; which is what a deadline that has already passed produces and is not an ; error — see flan_sleep_ns for why, and for the EINTR loop that keeps a ; signal from cutting the wait short. declare(sleep-ns, [ns i64], (), "flan_sleep_ns") fn sleep-seconds(s: f64) -> () = sleep-ns(i64(s * 1000000000.0)) ; ── The environment ─────────────────────────────────────────────────── ; ; One lookup, and `argv` and `exit` are the rest of the OS surface. Setting a ; variable is not here and is not an omission: `setenv` mutates a table the ; slice below views, and nothing in the language can spawn the process that ; would be the only reason to set one. ; ; **The result borrows.** It is a view of the process environment, not a copy: ; it needs no allocator and no free, and it stays valid because there is no ; writer — that is the same promise `slice-from` asks a caller to make, ; kept here once so that no caller has to. A program that wants to hold the ; value past a point where that reasoning stops being obvious should copy it ; into a Vec, which `concat` of one part already does. ; ; `None` and an empty `Some` are different answers and both occur: an unset ; variable is None, and `FOO=` set to nothing is `(Some [])`. A caller that ; wants to treat them alike says so. ; ; The absent case rides in the length and not in the pointer, because there is ; no null test to write here — a (Ptr T) in this language always addresses ; something. flan_getenv answers a length of -1 and a pointer at a valid empty ; string, so the test below is arithmetic and the pointer is never dereferenced ; on the absent path. declare(getenv-raw, [name str out-len Ptr(i64)], Ptr(u8), "flan_getenv") fn getenv(name: str) -> Option([u8]) let n = i64(0) p = getenv-raw(name, addr(n)) if n < 0 then None else Some(slice-from(p, i32(n))) ; ── Byte classes ────────────────────────────────────────────────────── ; ; ASCII only, and deliberately: a byte is a byte here, there is no code point ; type, and a UTF-8 continuation byte is not a digit under any locale. Both ; exist because something below needs them — parse-f64 the first, trim the ; second — and both are what a caller writing a tokenizer reaches for anyway. fn is-digit(b: u8) -> bool = b >= \0 and b <= \9 fn is-space(b: u8) -> bool b == \space or b == \tab or b == \newline or b == \return ; ── More of the bytes family ────────────────────────────────────────── ; Substring search, first occurrence. The length test is first and returns ; before the loop, so a needle longer than the haystack answers None rather ; than building a slice that runs off the end. An empty needle is Some 0, ; which is the answer that makes (index-of-bytes s p) agree with ; (has-prefix s p) on every p. ; ; Naive, O(n·m), and that is the deliberate choice: Boyer–Moore wants a skip ; table, which is an array sized by the needle, which is an allocation. fn index-of-bytes(s: [const u8], p: [const u8]) -> Option(i32) if length(p) > length(s) return None let last = length(s) - length(p) i = 0 while i <= last if is-bytes-equal(slice(s, i, i + length(p)), p) return Some(i) i += 1 None ; Returns a slice *of the input*, which is the whole reason trim can exist ; without an allocator: there is no new storage, only a narrower view of the ; caller's. It follows that the result dies with its owner, and that trimming ; does not modify anything. ; ; The two loops both test (< lo hi), so an all-whitespace input walks lo up ; to hi and stops there, and the result is the empty slice. Without that test ; lo would pass hi and (slice s lo hi) would be a reversed range, which traps. fn trim(s: [const u8]) -> [const u8] let lo = 0 hi = length(s) while lo < hi and is-space(s[lo]) lo += 1 while lo < hi and is-space(s[hi - 1]) hi -= 1 slice(s, lo, hi) ; The grammar is Flan's and the rounding is libc's, which is a split and not ; a dodge. parse-i64 is entirely Flan because strtoll's *answers* are wrong ; for a caller — 0 for "", 0 for "abc", 12 for "12x" — and reproducing ; correct-to-the-last-bit decimal-to-binary conversion is a different problem ; from rejecting junk. So this validates the whole slice first, and only a ; slice that is entirely a number is handed to bytes->f64; every string this ; returns Some for is one strtod converts exactly, correctly rounded, and ; identically everywhere, because that much IEEE-754 requires. ; ; The locale worry that keeps parse-i64 in Flan does apply to strtod's ; decimal point — and is moot here because nothing in the runtime calls ; setlocale, so the program stays in the C locale for its whole life. If that ; ever stops being true this function is the thing that breaks. ; ; Accepts [+-]? digits [. digits] [eE [+-] digits], needing at least one ; mantissa digit; refuses "", ".", "1e", "nan", "0x10", " 1" and "1 ". The ; 511 cap is flan_bytes_to_f64's buffer: past it the shim truncates, and a ; validator that said yes to 600 digits would be approving a different ; number than the one strtod reads. fn parse-f64(s: [const u8]) -> Option(f64) let i = 0 digits = 0 if length(s) == 0 or length(s) > 511 return None if s[0] == \- or s[0] == \+ i = 1 while i < length(s) and is-digit(s[i]) i += 1 digits += 1 if i < length(s) and s[i] == \. i += 1 while i < length(s) and is-digit(s[i]) i += 1 digits += 1 if digits == 0 return None ; "." and "+" and "e5" are not numbers if i < length(s) and (s[i] == \e or s[i] == \E) i += 1 if i < length(s) and (s[i] == \- or s[i] == \+) i += 1 let e = 0 while i < length(s) and is-digit(s[i]) i += 1 e += 1 if e == 0 return None ; a lone exponent marker ; Trailing junk is the case strtod is silent about, so the position has ; to land exactly on the end. if i == length(s) then Some(bytes->f64(s)) else None ; ── UTF-8 ───────────────────────────────────────────────────────────── ; ; Ported from Odin's core/unicode/utf8/utf8.odin, which is the one corner of ; a string library that is allocation-free by construction: decoding is ; classification, and every answer it gives is a number. Everything else in ; Odin's core/strings takes `allocator := context.allocator`, which is why ; this corner came first and the rest waited; most of that rest is ported now ; and lives in the building section below. core/fmt is still absent, and the ; reason it stays absent is not allocation — see the refusal list at the foot ; of this file. ; ; Odin's 256-entry accept_sizes table becomes a cond over the lead byte here. ; The table is the cache-friendly form and the cond is the one you can check ; by reading, and nothing in a game decodes UTF-8 in a hot loop — DrawText ; hands the bytes straight to raylib. ; ; The four rules that table encodes, and which a hand-written decoder gets ; wrong one at a time: ; ; 0x80..0xc1 never a lead byte. 0x80..0xbf are continuation bytes, and ; 0xc0 and 0xc1 could only ever begin an *overlong* two-byte ; spelling of an ASCII character — the encoding that lets ; "\xc0\xaf" smuggle a "/" past a check for one. ; 0xe0 second byte 0xa0..0xbf and not 0x80..0xbf; the low half is ; the overlong three-byte range. ; 0xed second byte 0x80..0x9f. The high half is U+D800..U+DFFF, ; the UTF-16 surrogates, which are not scalar values. ; 0xf0, 0xf4 second byte 0x90..0xbf and 0x80..0x8f: overlong below, ; and past U+10FFFF above. 0xf5..0xff lead nothing at all. ; ; The codec works on a code point as an i32, the number it is built from ; with shifts. What hands a character to a caller — rune-at and runes-next — ; hands back a char, converted once decoding has made it a scalar value, ; so nothing that walks text has to treat a number as a character. ; One deliberate divergence from Odin, and it is the parse-i64 argument over ; again. Odin's decode_rune answers RUNE_ERROR — U+FFFD — for malformed ; bytes, and U+FFFD is a perfectly real code point that a well-formed string ; may contain, so a caller cannot tell a decoded replacement character from a ; failure to decode. This carries `ok` instead, and leaves `code` 0 when it ; is false. ; ; `width` is 1 on a malformed byte and 0 only for an empty input. That is ; Odin's rule and it is load-bearing rather than cosmetic: every loop below ; advances by `width`, so a 0 there on a bad byte is an infinite loop, not a ; wrong number. struct Rune(code: i32, width: i32, ok: bool) fn is-rune-start(b: u8) -> bool = (b && 0xc0) != 0x80 fn decode-rune(s: [const u8]) -> Rune if length(s) == 0 return Rune{.code 0, .width 0, .ok false} let b0 = s[0] if b0 < 0x80 return Rune{.code i32(b0), .width 1, .ok true} ; size 0 means "this byte cannot lead"; lo/hi are the *second* byte's ; accepted range, which is the only place the overlong and surrogate ; rules live. Bytes three and four are always 0x80..0xbf. let size = 0 lo = u8(0x80) hi = u8(0xbf) if b0 < 0xc2 size = 0 elif b0 <= 0xdf size = 2 elif b0 == 0xe0 size = 3 lo = u8(0xa0) elif b0 <= 0xec size = 3 elif b0 == 0xed size = 3 hi = u8(0x9f) elif b0 <= 0xef size = 3 elif b0 == 0xf0 size = 4 lo = u8(0x90) elif b0 <= 0xf3 size = 4 elif b0 == 0xf4 size = 4 hi = u8(0x8f) else size = 0 if size == 0 return Rune{.code 0, .width 1, .ok false} ; A sequence cut off by the end of the slice. Width 1, so a caller ; scanning a buffer boundary makes progress instead of stalling. if size > length(s) return Rune{.code 0, .width 1, .ok false} let b1 = s[1] if b1 < lo or b1 > hi return Rune{.code 0, .width 1, .ok false} if size == 2 return Rune{.code (i32(b0 && 0x1f) << 6) || i32(b1 && 0x3f), .width 2, .ok true} let b2 = s[2] if b2 < 0x80 or b2 > 0xbf return Rune{.code 0, .width 1, .ok false} if size == 3 return Rune{.code ((i32(b0 && 0x0f) << 12) || (i32(b1 && 0x3f) << 6)) || i32(b2 && 0x3f), .width 3, .ok true} let b3 = s[3] if b3 < 0x80 or b3 > 0xbf return Rune{.code 0, .width 1, .ok false} Rune{.code ((i32(b0 && 0x07) << 18) || ((i32(b1 && 0x3f) << 12) || (i32(b2 && 0x3f) << 6))) || i32(b3 && 0x3f), .width 4, .ok true} ; Decode at a byte offset. None when the offset is not on a rune boundary or ; the bytes there are malformed, which is stricter than Odin's rune_at — that ; one hands back RUNE_ERROR and the caller carries on with a wrong character. fn rune-at(s: [const u8], i: i32) -> Option(char) if i < 0 or i >= length(s) None else let r = decode-rune(slice(s, i, length(s))) if r.ok then Some(char(r.code)) else None ; Counted through decode-rune rather than through a second walk of its own. ; Odin keeps a separate rune_count_in_bytes that re-implements the size ; table; two copies of that classification is two places for the surrogate ; rule to be right in only one of them. ; ; A malformed byte counts as one, which is what a replacement-character ; renderer would draw, so this agrees with what the screen shows. fn rune-count(s: [const u8]) -> i32 let i = 0 n = 0 while i < length(s) let r = decode-rune(slice(s, i, length(s))) i += r.width n += 1 n fn is-valid-utf8(s: [const u8]) -> bool let i = 0 while i < length(s) let r = decode-rune(slice(s, i, length(s))) if not r.ok return false i += r.width true ; How many bytes this code point encodes to, or None if it is not a scalar ; value. Odin's rune_size answers -1 for the refusals; a sentinel index is ; exactly what index-of avoids above, so this is an Option like the rest ; of the file. fn rune-size(code: i32) -> Option(i32) if code < 0 None elif code <= 0x7f Some(1) elif code <= 0x7ff Some(2) elif code >= 0xd800 and code <= 0xdfff None elif code <= 0xffff Some(3) elif code <= 0x10ffff Some(4) else None ; Encoding is the one operation here whose result is not a slice of its ; input, because the bytes it makes existed nowhere before. With no allocator ; the only shape left is Odin's own allocation-free one — strings.Builder ; built by builder_from_bytes over a caller's backing array (builder.odin, ; builder_from_bytes: "Uses Nil Allocator - Does NOT allocate") — reduced to ; its essential case: write into a buffer the caller owns, and say how much ; was written. ; ; None rather than a partial write when the buffer is short, and None rather ; than Odin's silent substitution of U+FFFD for an invalid rune. Odin's ; encode_rune rewrites a surrogate or an out-of-range value to the ; replacement character and reports success; the caller then finds three ; bytes of U+FFFD in its buffer and no indication that it asked for something ; else. Nothing is written at all when this answers None. fn encode-rune(dst: [u8], code: i32) -> Option(i32) match rune-size(code) None -> None Some(w) -> if w > length(dst) None else if w == 1 dst[0] = u8(code) elif w == 2 dst[0] = u8(0xc0 || code >> 6) dst[1] = u8(0x80 || (code && 0x3f)) elif w == 3 dst[0] = u8(0xe0 || code >> 12) dst[1] = u8(0x80 || ((code >> 6) && 0x3f)) dst[2] = u8(0x80 || (code && 0x3f)) else dst[0] = u8(0xf0 || code >> 18) dst[1] = u8(0x80 || ((code >> 12) && 0x3f)) dst[2] = u8(0x80 || ((code >> 6) && 0x3f)) dst[3] = u8(0x80 || (code && 0x3f)) Some(w) ; ── Splitting ───────────────────────────────────────────────────────── ; ; The iterator, which owns nothing. `split` returning a sequence of fields has ; to allocate that sequence, and it does — it is in the building section below ; — but this stays the right call whenever you do not want to own the result: ; it is Odin's split_by_byte_iterator (strings.odin), a cursor holding the ; rest of the input and handing back one field at a time. Every field is a ; slice *of the caller's bytes*; nothing is copied, nothing is owned, and ; there is no free to remember. `split` is built on exactly this. ; ; One divergence, and it is a wart of Odin's rather than a decision. Odin's ; iterator stops on an empty final field, so "a,b," iterates a and b and the ; trailing empty field is lost — while Odin's own allocating strings.split ; returns ["a", "b", ""] for the same input. The two disagree. This follows ; split: n separators always yield n+1 fields, an empty input yields one ; empty field, and `rest` is exhausted only after the last one is taken. That ; is the rule you can state without exceptions, and the one a caller counting ; comma-separated columns needs. struct Split(rest: [const u8], sep: u8, more: bool) fn split-on-byte(s: [const u8], sep: u8) -> Split Split{.rest s, .sep sep, .more true} fn split-next(it: Ptr(Split)) -> Option([const u8]) if not it.more return None match index-of(it.rest, it.sep) Some(i) -> let field = slice(it.rest, 0, i) it.rest = slice(it.rest, i + 1, length(it.rest)) Some(field) None -> let field = it.rest it.more = false it.rest = slice(it.rest, length(it.rest), length(it.rest)) Some(field) ; ── ASCII case ──────────────────────────────────────────────────────── ; ; Byte in, byte out, and *not* a function over a slice. Odin's to_lower and ; to_upper both allocate a new string (core/strings/conversion.odin) and so do ; the ones in the building section below; these are the forms that allocate ; nothing, and they stay the right call when a copy is not wanted — folding a ; comparison over two inputs beats lowering both and comparing. What is *not* ; on offer is the third shape, lowering a [u8] in place: the text a caller ; has is most often a (bytes-view s), which is a [const u8] because a string ; literal's bytes are in read-only memory, and an in-place lower could not ; take it. (bytes s) is the writable copy; a caller that owns its buffer ; writes the two-line loop itself. ; ; ASCII only, and only the 26 letters: case outside ASCII is not a byte ; operation at all — it is per-code-point, it is not length-preserving (ß ; upcases to SS), and it is locale-dependent (Turkish dotless ı). A byte ; table that pretended otherwise would be wrong in the quiet way. fn lower-ascii(b: u8) -> u8 if b >= \A and b <= \Z then b + 32 else b fn upper-ascii(b: u8) -> u8 if b >= \a and b <= \z then b - 32 else b ; Case-insensitive comparison as a fold over both inputs, which is the useful ; half of to_lower and needs no storage at all: comparing two lowered copies ; is what a caller wanted, and this is that answer without either copy. fn is-bytes-ci-equal(a: [const u8], b: [const u8]) -> bool if length(a) != length(b) false else for i in range(length(a)) if lower-ascii(a[i]) != lower-ascii(b[i]) return false true ; ── Ordering byte slices, and sorting them ──────────────────────────── ; ; The third element type the slice family covers, and the one a caller of ; `split` actually has: a [[const u8]] of fields, wanting to come out in order. ; ; The order is bytewise-lexicographic — memcmp's, and the one every sane ; sorted format uses. It is explicitly *not* alphabetical and not a collation: ; "Zebra" sorts before "apple" because 'Z' is 90 and 'a' is 97, and a ; non-ASCII byte sorts by its UTF-8 encoding, which for code points happens to ; agree with code-point order and for anything a human would call alphabetical ; does not. A locale-aware comparison is not a byte operation at all, for the ; same reasons the ASCII-case note above gives. ; ; The comparison is over u8 and therefore unsigned, which is the bug a version ; written over a signed byte type has: 0x80 would compare *below* 0x00 and ; every multi-byte character would sort before every ASCII one. ; ; A prefix sorts before what extends it — "ab" before "abc" — which falls out ; of running to the shorter length and then comparing lengths, and is the case ; a loop written to (length a) alone reads off the end for. fn is-bytes-less(a: [const u8], b: [const u8]) -> bool let n = min(length(a), length(b)) for i in range(n) if a[i] != b[i] return a[i] < b[i] length(a) < length(b) ; sort-by with the comparison written in, over the same in-place contract: ; the *slices* move, never the bytes they point at, so this sorts a [[const u8]] of ; fields borrowed from one buffer without touching the buffer. Stable, and ; here that is observable — two equal fields are two distinct slices of ; different parts of the input, and a caller can see which one came first. ; ; It keeps a name of its own rather than collapsing into sort, and the ; reason is the point of the predicates: a [u8] is not ordered (is-ordered) and cannot ; be, because < is defined on machine numbers and comparing two slices ; lexicographically is a loop and not an instruction. is-bytes-less is that loop. ; So this is the shape a generic takes when the operation it needs is not a ; primitive: pass it in. fn sort-bytes(s: [[const u8]]) -> () sort-by(s, fn(a, b) => is-bytes-less(a, b)) ; ── Building bytes, which is the tier that needed an allocator ──────── ; ; Everything above this line is slice-based and allocation-free, because when ; it was written there was nothing to allocate from. Everything below it ; *returns new storage*, which is the whole difference, and there are three ; rules that hold for all of it. ; ; **The result is owned and the caller frees it.** Each of these hands back a ; String — or, for split, a (Vec [const u8]) — which is move-only: it goes with the call that ; takes it, and nothing is released at scope exit — not at the end of a let, ; not at the end of a function (spec-memory.md, "When storage is released"). ; A caller writes (free v) or lets a (free-all a) take the whole region. ; ; **The allocator is the context's, and `with-allocator` is the override.** ; spec-memory.md makes allocation use the current implicit allocator and ; forbids falling back to a hidden global one. (vec-new) and (map-new) take an ; optional trailing allocator because the checker builds them; a Flan defn has ; fixed arity and cannot, so the choice here was an allocator parameter on ; every one of these signatures or none. None: a caller wanting a frame arena ; writes (with-allocator a (join parts sep)) and the Vec records the arena, so ; the free and the clone never need it named again. ; ; **The text builders check what they answer**, with (bytes->string b), and ; that check stops the program at this file's line. A call the program writes ; is checked first, at its own line, on the text it passes (check.ml, ; [prechecked_call]); the check here is for a builder reached through a ; function value, which has no call site to check at. ; ; **No Result, anywhere.** Running out of storage signals StorageExhausted ; under a `retry` restart and no allocating operation returns an error ; (spec-memory.md, "Allocation failure"), so these signatures say what they ; produce and nothing about how they might fail. ; String: owned, growable, always valid UTF-8. The bytes live in a (Vec u8), ; so the allocator, the free, the retry on exhaustion and the dev build's ; registry are all the Vec's. What the struct adds is the promise, and the ; checker keeps it (check.ml, [string_call]): outside this file the field ; cannot be named and the struct cannot be built, so every byte arrives ; through append, insert, string-new or bytes->string, each of which ; checks text it cannot prove valid at the site that stores it. ; ; A zeroed String is the empty one: a zeroed Vec adopts the context ; allocator on its first append. struct String(bytes: Vec(u8)) ; A cursor over the code points of some UTF-8 bytes, which owns nothing: the ; shape split-on-byte has. (runes s) makes one over a str, a String or a ; [const u8], and runes-next hands back one char at a time. A ; malformed byte in a str or a [const u8] comes back as U+FFFD and counts ; as one, as rune-count counts it; a String has none. struct Runes(rest: [const u8]) fn runes-next(it: Ptr(Runes)) -> Option(char) if length(it.rest) == 0 None else let r = decode-rune(it.rest) it.rest = slice(it.rest, r.width, length(it.rest)) Some(char(if r.ok then r.code else 0xfffd)) ; append — onto a String, or a run of bytes onto a (Vec u8) — is the ; checker's (check.ml, "append"), because what it takes decides what it ; does: a str, a String or a code point onto a String, a [const u8] onto a ; (Vec u8). ; The two number appends. Outside the prelude i64->bytes and f64->bytes copy ; their text into the temp allocator; inside it they answer a view of the ; frame slot they render into (check.ml, the "i64->bytes" arm), so these ; append the text without an allocation per number. fn append-i64(b: Ptr(Vec(u8)), n: i64) -> () = append(b, i64->bytes(n)) fn append-f64(b: Ptr(Vec(u8)), x: f64) -> () = append(b, f64->bytes(x)) ; concat and join. Both take a slice of slices, which is the shape a caller ; already has: an array literal of them, [(bytes-view "a") (bytes-view b)], slices to a ; [[const u8]] and copies nothing. The outer slice is const too, which is what ; lets a [[u8]] in as well: nothing here can store a read-only slice into it. ; ; join with an empty separator is concat, and concat is here anyway because ; the empty (bytes-view "") a caller would have to write is the kind of argument ; that reads like a mistake at the call site. fn concat(parts: [const [const u8]]) -> String let b = vec-new(u8) for i in range(length(parts)) append(addr(b), parts[i]) bytes->string(b) ; n parts yield n-1 separators, and the empty slice of parts yields the empty ; result rather than a leading separator — which is the off-by-one a join ; written as "append part then separator, then chop the tail" gets wrong on ; exactly that input, because there is no tail to chop. fn join(parts: [const [const u8]], sep: [const u8]) -> String let b = vec-new(u8) for i in range(length(parts)) if i > 0 append(addr(b), sep) append(addr(b), parts[i]) bytes->string(b) fn repeat-bytes(s: [const u8], n: i32) -> String let b = vec-new(u8) for i in range(n) append(addr(b), s) bytes->string(b) ; The allocating halves of the ASCII case pair. The note above lower-ascii ; says why there is no in-place one; these write only bytes of their own. fn to-lower(s: [const u8]) -> String let b = vec-new(u8) for i in range(length(s)) push(b, lower-ascii(s[i])) bytes->string(b) fn to-upper(s: [const u8]) -> String let b = vec-new(u8) for i in range(length(s)) push(b, upper-ascii(s[i])) bytes->string(b) ; Every non-overlapping occurrence, left to right, which is the rule that ; makes (replace-bytes (bytes-view "aaa") (bytes-view "aa") (bytes-view "b")) answer "ba" and ; not "bb" or "b". ; ; An empty `from` matches nothing and the result is a copy of the input. The ; alternative reading — that it matches at every position — is what turns this ; into an infinite loop, and Odin's replace guards the same case for the same ; reason. ; ; The guard is an `if` and not an early `(return b)`, which is not a style ; choice: returning a Vec *moves* it, and the move analysis is a dead set over ; the whole function, so a `return b` on one branch kills the binding for the ; `b` at the foot of the other. One exit, one move. fn replace-bytes(s: [const u8], from: [const u8], to: [const u8]) -> String let b = vec-new(u8) i = 0 if length(from) == 0 append(addr(b), s) else while i < length(s) match index-of-bytes(slice(s, i, length(s)), from) Some(k) -> append(addr(b), slice(s, i, i + k)) append(addr(b), to) i = i + k + length(from) None -> append(addr(b), slice(s, i, length(s))) i = length(s) bytes->string(b) ; A (Vec [u8]) cannot be written at a let, and this one-line function is where ; the type is said instead. (vec-new) takes its element type as a *bare ; symbol* — check.ml's vec_new_elem resolves one name and nothing else — so ; (vec-new [u8]) is not accepted, and a let has no type annotation to say it ; the other way. A return type does say it. That is a compiler gap rather than ; a language decision, and it is written down in TODO.org, "(vec-new [u8]) is ; refused". fn slices-new() -> Vec([const u8]) = vec-new() ; split, which the file used to refuse by name. The fields are slices *of the ; input* and not copies, so nothing here owns bytes and the result dies with ; whatever `s` pointed at — a (Vec (Vec u8)) is the shape that would own them ; and it is refused outright, because a Vec's elements are copied and released ; bytewise and an owner cannot survive that. ; ; The rule is split-on-byte's, unchanged and worth restating: n separators ; always yield n+1 fields, so the empty input yields one empty field and a ; trailing separator yields a trailing empty one. That is Odin's allocating ; strings.split and not Odin's iterator, which disagree with each other. fn split(s: [const u8], sep: u8) -> Vec([const u8]) let v = slices-new() it = split-on-byte(s, sep) going = true while going match split-next(addr(it)) Some(f) -> push(v, f) None -> going = false v ; ── A number with a precision ───────────────────────────────────────── ; ; The one formatting job the runtime cannot do. f64->bytes is snprintf "%g", ; which is six significant digits and switches to exponent notation on its ; own: a frame time of 0.0166667 is what a caller wanted two decimals of, and ; 1.23457e+06 is what a score looks like once it passes a million. There is no ; precision to pass it. ; ; This returns a Vec. Its i64->bytes calls answer frame-slot views, since this ; is the prelude, and each is copied into the Vec before the next is made. ; ; Half away from zero, the same rule round-f32 follows, applied at the last ; digit kept. That is not bit-for-bit printf: printf rounds the *binary* value ; to nearest-even at the decimal digit, and this rounds the decimal expansion ; half-up, so a value sitting exactly on a half — 0.999995 at five places — ; comes out 1.00000 here and may come out 0.99999 there. Choosing the rule the ; rest of this file already uses beats matching a libc whose answer is not the ; same on every target anyway. ; ; Precision is clamped to 0..9 rather than refused. 10^9 is the largest power ; of ten that leaves room in the f64 product below, and a precision argument ; is almost always a literal, so a refusal would be a run-time condition for a ; mistake visible in the source. ; ; It is the `clamp` macro two hundred lines up, and this is the call that ; proves a prelude function may call a prelude macro — which it could not ; until macro.ml grew its bootstrap reduction. The cycle it breaks: a macro ; module is compiled *from* the prelude, so a prelude function calling a macro ; would have to be compiled into the very module that expands it. For that one ; build the prelude drops every defn that reaches a macro, this one included. ; A prelude *macro* may still not call a macro, and says so by name. ; ; Three inputs do not have decimal expansions and are named before the cast ; that would be undefined on them: NaN, which fails every comparison and is ; therefore tested with (not (= x x)) and nothing else, and the two ; infinities, which are the values satisfying (= x (* x 2.0)) away from zero. ; A magnitude past 9e18 has no fractional bits left at all and would not fit ; in the i64 the integer part is carried in, so it falls back to f64->bytes — ; which is the honest answer there rather than an approximation of one. ; ; -0.0 prints as "0.00": the sign test is (< x 0.0), which -0.0 fails. A ; caller that needs the sign of a zero should not be reading it out of text. fn format-f64(x: f64, prec: i32) -> String let b = vec-new(u8) p = clamp(prec, 0, 9) if not (x == x) append(addr(b), bytes-view("nan")) elif x == x * 2.0 and x != 0.0 append(addr(b), bytes-view(if x < 0.0 then "-inf" else "inf")) else let neg = x < 0.0 m = if neg then 0.0 - x else x if m >= 9.0e18 append(addr(b), f64->bytes(x)) else let scale = i64(1) for i in range(p) scale *= 10 ; The split is exact: (i64 m) truncates toward zero and m is ; non-negative here, and the subtraction of an integer from the ; float it came from is exact at every magnitude an f64 can hold. ; Only the scaling below rounds, and it rounds a value already ; under 1. let ip = i64(m) fr = i64((m - f64(ip)) * f64(scale) + 0.5) ; The carry, which is the bug this shape is otherwise written ; with: 0.999995 at five places scales to exactly 100000, which ; is not a fraction at all — it is the next integer, and without ; this line it prints as "0.100000". if fr >= scale fr = 0 ip += 1 ; The sign goes on separately, because the integer part is a ; magnitude: -0.5 at one place has an integer part of 0, and ; i64->bytes of 0 has no sign to carry. if neg push(b, \-) append-i64(addr(b), ip) if p > 0 push(b, \.) ; Left-padded with zeros to exactly p digits. fr is under ; scale by the carry above, so it never needs more, and ; without the padding 1.005 at three places prints "1.5". let d = i64->bytes(fr) for i in range(p - length(d)) push(b, \0) append(addr(b), d) bytes->string(b) ; ── Still refused, and what the reason is now ───────────────────────── ; ; This list used to be one sentence long — every entry needed to produce bytes ; that did not exist in its input, and there was no allocator. That sentence ; stopped being true when `Vec` landed, and most of the list has moved up into ; the building section above: join, concat, split, to-lower, to-upper, repeat ; and replace are all written now. Bytes become text two ways: (str (slice v)) ; views them, free and unchecked, and (bytes->string v) takes the Vec over ; as a String once it has checked the bytes are UTF-8. ; ; What is left is refused for four *different* reasons, which is why they are ; named separately rather than under one heading. ; ; pad, center Nothing. These are three lines each over repeat-bytes ; and concat, and they are absent only because no ; caller has asked. Write them when one does. ; format, sprintf A format *string* — Odin's fmt.aprintf family. It ; needs variadic arguments of mixed type, which is a ; function-value and generics question, not an ; allocation one. format-f64 above is the piece of it ; that was actually wanted, and `print`/`println` are ; already the structural walk over any one value. ; map that changes the Generics, and only that. map-in-place, filter, reduce and ; element type sort-by landed the day function values did — see ; "The ones that take a function" above — at i32 and ; f32, the two element types the rest of that family ; covers. A map from [i32] to [f32] is the one shape ; that did not come with them, because it is one copy ; per *ordered pair* of types rather than per type, ; which is where a per-type family stops being honest. ; ; Builder A String is one: append onto it. ; ── Files: embedding, slurp and barf ────────────────────────────────── ; ; One entry per file in an (embed-dir "...") — Odin's Load_Directory_File ; (base/runtime/core.odin), which is the same two fields for the same reason: ; a directory embed is only useful if you can find one file in it by the name ; it had on disk. ; ; `data` points into the program's own .rodata, exactly as a string literal ; does, so an embed costs nothing at run time and nothing at startup. It is ; also read-only, so `data` is a [const u8] and a store through it is refused ; at compile time. To get a writable copy, copy the bytes into a Vec. struct EmbedFile(name: str, data: [const u8]) ; A linear scan, deliberately. A directory embed is tens of entries, the scan ; is over names already in cache-warm .rodata, and the alternative — a ; compile-time perfect hash — is a build-time map with its own failure modes ; that nothing here has asked for. If a program ever embeds thousands of ; files, sort-and-bisect is the next step and it does not change this type. ; ; It takes a slice rather than the array (embed-dir) answers, because an array ; length is part of its type and there are no generics: write ; (embed-find (slice assets 0 (length assets)) "brush.png"). fn embed-find(files: [EmbedFile], name: str) -> Option([const u8]) for i in range(length(files)) if is-bytes-equal(bytes-view(files[i].name), bytes-view(name)) return Some(files[i].data) None ; The condition slurp and barf signal — spec-conditions.md, and the same shape ; StorageExhausted has: a value struct on the signalling frame's stack, fixed ; fields, no rendered message. `path` is the path that failed, which is a ; string literal or a string the handler itself supplied, so naming it costs ; no allocation either. ; ; One type rather than a family, because conditions have no hierarchy today ; (spec-conditions.md §1) and a family would need one handler clause per ; member to say "any file error". The parent link TODO.org, "Conditions get a ; parent link, not class inheritance", decides on is the ; answer to that, and it is not built; when it is, these reasons can become ; types without any call site changing. struct FileError(path: str, op: i32, reason: i32) :parent Error const file-op-read: i32 = 0 const file-op-write: i32 = 1 const file-op-delete: i32 = 2 const file-op-rename: i32 = 3 const file-op-mkdir: i32 = 4 const file-missing: i32 = 1 const file-denied: i32 = 2 const file-io: i32 = 3 ; What `barf` signals on the web target, every time. Decision 2: writing is ; desktop-only, and it signals rather than refusing at build time (Flan has no ; conditional compilation, so isolating code to desktop is not expressible) or ; silently doing nothing (which is how a save file disappears with nothing ; said). `delete-file`, `rename-file` and `make-directory` carry the same ; decision: all three change the filesystem, so all three signal this on the ; web rather than quietly succeeding into a filesystem the page throws away. const file-unsupported: i32 = 4 ; The two file questions that are not failures, and they are prelude ; functions rather than builtins because of that: nothing here needs a ; restart, so nothing here needs the compiler. ; ; That is the line the whole file surface is drawn on. `slurp`, `barf`, ; `delete-file`, `rename-file` and `make-directory` can fail in ways a ; handler can *answer* — make the parent and retry, supply another path — so ; each signals FileError with those two restarts. "Is it there" and "how big ; is it" have no such answer: absence is the reply, not a fault, and a ; condition would make the ordinary case cost a handler search. declare(file-stat-raw, [path str out-size Ptr(i64)], i8, "flan_file_stat") ; True for anything the path resolves to — a file, a directory, a device — ; because that is what the question asks and a caller wanting "and it is a ; regular file" is asking a second question this does not pretend to answer. ; ; **It is a reading and not a guarantee.** Between this answering true and the ; next line opening the file, anything may have removed it; the race is ; unavoidable and is the reason `slurp` signals rather than requiring this ; first. Reach for it when the answer is the point — choosing a config path, ; deciding whether to write a default — and not as a guard in front of an ; operation that already reports its own failure properly. fn file-exists(path: str) -> bool let n = i64(0) file-stat-raw(path, addr(n)) == 1 ; None for a path that does not resolve, which folds every reason into one ; answer — that is the trade a caller makes by asking a question with no ; restart on it. A caller that needs to tell "missing" from "denied" wants ; `slurp`, whose FileError carries the reason. fn file-size(path: str) -> Option(i64) let n = i64(0) if file-stat-raw(path, addr(n)) == 1 then Some(n) else None ; ── Reading a file while a macro runs ───────────────────────────────── ; ; The one thing a macro needed that it could not write for itself. A macro is ; compiled and dlopened into the compiler, so `slurp` was always callable from ; one; what was missing is that a macro has no idea where its call site is, ; and so no way to resolve a path the way `(embed "assets/x.edn")` resolves ; one — relative to the directory of the source file the form is written in. ; ; This is that rule, and it is the *same* rule: the compiler pokes the call ; site's directory into the runtime before every expansion (runtime/flan_rt.c, ; "Reading a file while a macro runs", and lib/macro.ml's expand_form), and a ; relative path is joined to it. An absolute path is taken as written. ; ; **None rather than a condition**, which is the whole reason this is not ; `slurp`. A condition signalled inside an expansion is signalled *in the ; compiler*, through the macro module's own copy of the runtime, and that is ; the failure `Build.macro_module`'s hidden-visibility note measured: it takes ; the process down instead of parking it. Absence arriving as an answer is ; what lets a type provider say "there is no file at that path" as a refusal ; with a location, which is the sentence its author wanted anyway. ; ; **Outside a macro it is still a read**, with the path relative to the ; process rather than to any source file — nothing else knows better, and ; every program links this runtime. It is not a file API and `slurp` is; this ; exists so a macro can look at data at compile time. declare(macro-slurp-raw, [path str out-len Ptr(i64)], Ptr(u8), "flan_macro_slurp") fn macro-slurp(path: str) -> Option([u8]) let n = i64(0) p = macro-slurp-raw(path, addr(n)) if n < 0 then None else Some(slice-from(p, i32(n))) ; ── Form: what a macro takes and what it answers ────────────────────── ; ; The reader's output, mirrored on the Flan side, because a macro is a ; function [Form] -> Form and there is no interpreter: running one means ; compiling it and dlopening it into the compiler. So the compiler and the ; loaded macro have to agree on the *layout* of a Form, not merely on its ; shape. lib/form.ml is the other half of this declaration and the two are ; edited together. ; ; It mirrors Form.value and not Form.t: there is no `loc` field. A macro ; cannot invent a source location and should not carry one, so locations stay ; on the compiler's side. It still knows where a form came from: every case ; but the three that fit inside the payload holds a pointer into memory the ; compiler allocated, and lib/expand.ml keeps a table from that address to the ; line the form was read on. A form a macro splices through comes back holding ; that pointer, so an error on it is reported where it was written; a node the ; macro built is reported at the call. ; ; Case order is the tag order (docs/BUILT.md, data types), so this list is a layout ; contract with lib/expand.ml's marshaller and may not be reordered. data Form Sym(s: str) Kw(s: str) Int(i: i64) Float(x: f64) Str(s: str) Byte(b: i32) List(xs: [Form]) Vec(xs: [Form]) Map(xs: [Form]) ; The list-building surface quasiquote desugars into. Three functions and no ; more: `form-nil` starts one, `form-cons` puts a form on the front, and ; `form-append` is what ~@ splices with. Everything else — a vector literal, ; a length, an index — is already the language's. ; ; Each allocates a fresh (Vec Form) and hands back a borrow of it that ; outlives the call. That is a leak, on purpose: a macro runs inside the ; compiler, its result is read after it returns, and the whole expansion is ; bounded by the size of the program being compiled. `drop` is what would ; change this, and it does not exist. fn form-nil() -> [Form] let v = vec-new(Form) slice(v) fn form-cons(x: Form, rest: [Form]) -> [Form] let v = vec-new(Form) push(v, x) for i in range(length(rest)) push(v, rest[i]) slice(v) fn form-append(a: [Form], b: [Form]) -> [Form] let v = vec-new(Form) for i in range(length(a)) push(v, a[i]) for i in range(length(b)) push(v, b[i]) slice(v) ; The rest of a macro's arguments, which is what a variadic body is: a macro ; takes one parameter, the slice of the forms at its call site. fn form-rest(xs: [Form], from: i32) -> [Form] let v = vec-new(Form) i = from while i < length(xs) push(v, xs[i]) i += 1 slice(v) ; (head x) for every x, which is what ~~@xs splices into an inner template: ; one unquote per element, as SBCL's unquote* builds (src/code/backq.lisp). fn form-wrap-each(head: str, xs: [Form]) -> [Form] let v = vec-new(Form) for i in range(length(xs)) push(v, Form.List{.xs form-pair(Form.Sym{.s head}, xs[i])}) slice(v) ; The elements of a vector form, which is what a [ ] pattern in a macro's ; parameter list unwraps. The other arm is unreachable from a generated ; binding -- lib/expand.ml's check_call refuses a non-vector argument at the ; call site, before the macro runs -- and is here because a macro picking a ; form apart by hand has the same question and no such guarantee. fn form-vec-items(f: Form) -> [Form] match f Form.Vec(xs) -> xs _ -> form-nil() ; A name no reader can produce. `~` is a delimiter now (it opens an unquote), ; so no symbol coming out of read_all can contain one, and a gensym therefore ; cannot collide with a name someone wrote. Non-hygienic expansion with an ; explicit gensym is the settled decision (plan.org, open decision 2); this is ; the escape hatch that makes it liveable. ; ; The counter is C data in the runtime, flan_gensym_n, because a build loads ; more than one macro module — one per round when macros call macros, and ; another for every expansion in a session — and each links its own copy of ; the runtime. lib/macro.ml keeps the count across them: it writes it into ; the module before every macro call and reads it back after, so no two ; modules in one compiler process draw the same name. declare(gensym-next, [], i64, "flan_gensym_next") fn gensym() -> Form let v = vec-new(u8) push(v, 126) ; ~ push(v, 103) ; g let d = i64->bytes(gensym-next()) for i in range(length(d)) push(v, d[i]) Form.Sym{.s str(slice(v))} ; ── The first special form to stop being one ────────────────────────── ; ; plan.org milestone 5 says when, unless, until, cond and dotimes are special ; forms only until macros land. This is the one that moved, and it is here to ; show that the move is possible and cheap, not because it was the most ; valuable of the five: it is the one no other part of the prelude uses, so ; moving it cannot make the prelude depend on the expander that compiles it. ; ; The expansion is exactly what parse.ml built by hand until now -- an if over ; (not test) with the body in a do -- so every test written against the ; special form is a test of this, unchanged. ; ; The one thing the compiler could say and this cannot is a reason. A macro ; has no error facility: it runs inside the compiler and anything it signals ; aborts the compile with no location. So a malformed (unless) answers a name ; nothing defines, and the report is "unknown name unless-takes-a-test" at the ; call site, which is the right place and the wrong sentence. That is the next ; thing a macro needs; TODO.org, "A macro fails at its call site in its own ; words", is where that landed. ; ; An empty body is allowed, and expands to the (do) it always would have: ; (unless test) is a guard whose body has not been written yet, which is a ; state a program passes through on the way to being finished, and refusing it ; bought nothing. `when` in lib/parse.ml is the same change; the two are ; halves of one form and only a restriction they both carried would be worth ; keeping. A test is still required, because there is nothing to negate ; without one. macro unless(& args) if length(args) < 1 quote unless-takes-a-test() else quote if(not ~(args[0])): do: ~@(form-rest(args, 1)) ; ── until ───────────────────────────────────────────────────────────── ; ; (until test body ...) is (while (not test) body ...), and a label written ; first stays first: (until :outer test body ...). macro until(& args) let labelled = length(args) > 0 and match(args[0], Form.Kw(k), true, _, false) let from = if labelled then 1 else 0 if from >= length(args) quote compile-error("until is (until test body ...), or (until :label test body ...)") else if labelled quote while(~(args[0]), not ~(args[1]), ~@(form-rest(args, 2))) else quote while not ~(args[0]) ~@(form-rest(args, 1)) ; ── comment ─────────────────────────────────────────────────────────── ; ; (comment (whatever you like)) is nothing at all, and the "whatever you like" ; is the whole feature. A macro's arguments arrive as raw Form and are never ; checked as expressions, so what is inside can name functions that do not ; exist, call them at the wrong arity, or add a string to a number: none of it ; is ever looked at, because this answers (do) without reading a single ; argument. That is Clojure's (comment ...) exactly, and it is what ; cannot ; do — a commented-out block stops being a form, so an editor can no longer ; move over it, indent it or send it to the REPL, and a discarded one still ; can. ; ; The one thing it does require is that the contents READ: balanced ; delimiters and legal tokens, since the reader runs before any macro does. ; An unterminated string inside a (comment ...) is still an unterminated ; string. ; ; #_ is the other spelling and they are not rivals: #_ discards the one form ; after it and is the reader's, so it works in any position including inside ; another form's arguments; this is a form of its own and takes any number, ; which is what a block of parked code wants. Built in rather than left to ; every project, because a name this standard should mean the same thing in ; all of them. macro comment(& args) quote () ; ── inc/dec and ++/-- ───────────────────────────────────────────────── ; ; Two pairs, and the split between them is the whole design. inc and dec ; answer a number and change nothing; ++ and -- change a place and answer ; whatever `set` answers. The spelling says which: a word for the pure one, a ; punctuation pair borrowed from C for the one with the effect, so ; (inc i) in an argument and (++ i) as a statement never get confused for one ; another the way C's i++ and i+1 do. ; ; Generic for free, all four of them, because + and - already are: (inc x) is ; (+ x 1) with the literal taking whichever numeric type x has — i8 through ; i64, u8 through u64, f32, f64, and a dyn — and none of that is this macro's ; business. There is no per-type family here and there is no `where` clause, ; because a macro does not have a type at all; the expansion is checked at the ; call site as if it had been written there. ; ; **++ and -- evaluate the place once.** Each index, key and pointer in the ; place is bound to a temp before the read, so (++ (at arr (next-index))) ; calls next-index once and reads and writes the same element — C's rule for ; compound assignment. They are update with + and -, spelled as the form ; update~ that update itself expands into (a prelude macro may not call a ; macro); lib/parse.ml's [modify] is where the place is taken apart. macro inc(& args) if length(args) != 1 quote inc-takes-one-number() else quote ~(args[0]) + 1 macro dec(& args) if length(args) != 1 quote dec-takes-one-number() else quote ~(args[0]) - 1 macro ++(& args) if length(args) != 1 quote ++-takes-one-place() else let g = gensym() quote ~(Form.Sym{.s "update~"})(~(args[0]), ~g, ~g + 1) macro --(& args) if length(args) != 1 quote ---takes-one-place() else let g = gensym() quote ~(Form.Sym{.s "update~"})(~(args[0]), ~g, ~g - 1) ; ── update: change a place by applying a function to it ──────────────── ; ; (update (.velocity g) inc) ; (update (at grid r c) + 10) ; ; (update place f args ...) stores (f old args ...) back into the place, where ; old is what the place held. f is written as the head of a call, so it may be ; a function, an operator or a macro such as inc. Every place set takes is a ; place here too — a name, a field, an element, a deref, a class slot — and ; the place is evaluated once, as ++ says above. It answers what set answers. macro update(& args) if length(args) < 2 quote update-takes-a-place-and-a-function() else let g = gensym() quote ~(Form.Sym{.s "update~"})(~(args[0]), ~g, ~(args[1])(~g, ~@(form-rest(args, 2)))) ; ── into: a fused transformation, and not a transducer ──────────────── ; ; (into xs (vec-new i32) (map double) (filter is-even)) ; ; Source, destination, then any number of transforms. It reads as a sentence ; — take this, put it there, doing these — and the variadic tail has to trail ; anyway, which is the mechanical reason the transforms cannot sit in the ; middle. ; ; **A macro, not transducers.** Transducers compose at run time: function ; values, closures, an allocation, and a chain of indirect calls per element. ; Rust has no transducers either — it has iterators, which fuse at compile ; time through monomorphisation, and that needs generics. A macro reaches the ; same place with neither. (map double) expands to (double x) written straight ; into the loop body, so the function name is *syntax* and never a value: ; there is no intermediate collection at any step, no closure, no generics and ; nothing to inline. What it gives up is building a transformation at run time ; and passing it around, which is transducers' actual selling point and is ; close to useless in a game. ; ; **The destination is in the form on purpose.** Every collecting operation ; here allocates from an explicit allocator, which is a frozen rule in ; spec-memory.md. A ->> chain would hide where the result goes; naming the ; destination means this macro knows its type, emits the right loop, and the ; rule is honoured by construction. `(vec-new i32 a)` names an allocator here ; as it does anywhere else, because the destination form is written out ; untouched. ; ; **The destination is a Vec**, because push is what fills it. A Map ; destination is refused by push, which says "push takes a (Vec T)" and names ; the real problem; there is no second lowering for it and no reason to invent ; one before something wants it. ; ; **Reductions do not share this form**, and that was the open question. (into ; xs 0 (map cost) (sum)) reads oddly because zero is not a collection, and the ; oddness is the tell: the whole reason the destination sits in the form is ; that it is the allocation, and a seed is not one. Keeping `into` to ; collections means the destination is always honest about what it is. A ; reducing macro of the same shape is a separate form when something wants it. ; The chain, built from the inside out: the innermost form is the push, and ; each transform wraps whatever the transforms after it produced. Walked in ; reverse for that reason, which is what the loop's two names are. ; ; One element name throughout, shadowed by each (map f) stage. A let binding's ; value is checked before the name is bound, so (let [x (f x)] ...) reads the ; outer x and binds the inner one — that is the language's rule and not an ; accident of the compiler; the debug-info suffix `x~2` exists precisely so a ; debugger does not lie about which is which. The name is a gensym, so it ; cannot collide with anything at the call site. ; ; A transform that is neither map nor filter expands to a call to a name ; nothing defines, which is how a macro reports anything at all: it has no ; error facility, so the report is "unknown name" at the call site — the right ; place and the wrong sentence. The bad transform is passed along so that at ; least it is named. fn into-wrap(ts: [Form], dst: Form, x: Form) -> Form let k = length(ts) body = quasiquote(push(~dst, ~x)) while k > 0 let t = ts[k - 1] items = form-items(t) if length(items) != 2 return quasiquote(into-transform-is-map-or-filter-of-one-function(~t)) else let head = items[0] f = items[1] if is-form-named(head, "map") body = quasiquote(let([~x ~f(~x)], ~body)) elif is-form-named(head, "filter") body = quasiquote(when ~f(~x) then ~body) else return quasiquote(into-transform-is-map-or-filter(~t)) k -= 1 body ; Whether any transform in the chain is a (map f). fn has-map-step(ts: [Form]) -> bool for k in range(length(ts)) let items = form-items(ts[k]) if length(items) > 0 and is-form-named(items[0], "map") return true false ; The items of a list form, and the empty slice for anything else — a ; non-list transform falls into the arity complaint above rather than needing ; a case of its own. fn form-items(f: Form) -> [Form] match f Form.List(xs) -> xs _ -> form-nil() fn is-form-named(f: Form, name: str) -> bool match f Form.Sym(s) -> is-bytes-equal(bytes-view(s), bytes-view(name)) _ -> false ; Whether a form is the empty list, (). [form-items] cannot answer this: it ; returns the empty slice for a non-list too, so "no items" and "not a list" ; arrive the same. A macro that has to tell `()` from a name needs the ; difference — see vendor/raylib/modes.fln, where a lone () argument is a ; body that was not written rather than a body of one form. fn is-form-empty-list(f: Form) -> bool match f Form.List(xs) -> length(xs) == 0 _ -> false fn is-form-sym(f: Form) -> bool match f Form.Sym(s) -> true _ -> false fn form-pair(a: Form, b: Form) -> [Form] form-cons(a, form-cons(b, form-nil())) ; A source that is already a name is used as it is, and a source that is ; anything else is bound to one. Both halves matter. ; ; Binding it is what a source that is a *call* needs: (length s) and (at s i) ; have to be the same s, and without the binding the call would be made twice ; per element. ; ; Not binding a name is what everything else needs. A (Vec T) is move-only, so ; (let [s v] ...) would hand v's ownership to the macro's own binding and the ; caller would find v dead after an (into v ...) that only read it — and a ; fixed array would be *copied* into the binding, once per into. Neither is ; what was written. length and at borrow, so used directly the source is only ; read. A source that is a call and produces a Vec is still consumed, which is ; right: nobody else is holding it. macro into(& args) if length(args) < 2 quote into-takes-a-source-a-destination-and-transforms() else let from = args[0] is-named = is-form-sym(from) src = if is-named then from else gensym() let bind = if is-named then form-nil() else form-pair(src, from) ; With no (map f) in the chain every element pushed is a source ; element as it stands, which copies only its header: the checker ; refuses that for an element that owns storage. The destination and ; the transforms ride along unevaluated, to be written back in the fix. let shares = if has-map-step(form-rest(args, 2)) form-nil() else form-cons(quasiquote(into-copies-elements(~src, ~(args[1]), ~@(form-rest(args, 2)))), form-nil()) let dst = gensym() x = gensym() i = gensym() quote let([~dst ~(args[1]) ~@bind]): ~@shares for ~i in range(length(~src)) let ~x = ~src[~i] ~(into-wrap(form-rest(args, 2), dst, x)) ~dst