flan/lib/prelude.fln

2540 lines
120 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

; 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<T>`, 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