2540 lines
120 KiB
Plaintext
2540 lines
120 KiB
Plaintext
; 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
|