diff --git a/TODO.org b/TODO.org index 072d5f74..0c205182 100644 --- a/TODO.org +++ b/TODO.org @@ -25,6 +25,7 @@ prelude and vendor rewritten in .fln; tests and examples converted with =flan co then =flan convert=, the .flan source path and =flan-mode= removed. Nothing tracks what .flan can no longer say. Step 1 is done: the name rule with the =is-=/=has-= renames through the prelude, vendor and raylib, and =T?=, =??=, =x!=, =if let g = x= and =a?.b=. +Step 2's prelude is done: =lib/prelude.fln=, read by the indented reader. ** DONE x? tests presence, and narrows CLOSED: [2026-09-26] Decided (133): =x?= is a bool; =if x?=, =elif x?=, =while x?= and the rest of an =and= diff --git a/docs/BUILT.md b/docs/BUILT.md index f79380da..4353f511 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -7527,7 +7527,7 @@ prints freed memory. ## String is a prelude struct, and the checker is its wall -`String` is `(defstruct String [bytes (Vec u8)])` in `lib/prelude.ml`, so its +`String` is `struct String(bytes: Vec(u8))` in `lib/prelude.fln`, so its allocator, free, retry on exhaustion and dev-registry notes are the Vec's and neither backend has a String of its own. What makes it always valid UTF-8 is `lib/check.ml`'s String section: outside the prelude the field and the diff --git a/emacs/flan-cnr.el b/emacs/flan-cnr.el index 7a944600..a293d949 100644 --- a/emacs/flan-cnr.el +++ b/emacs/flan-cnr.el @@ -166,7 +166,7 @@ often `pause' itself, and `P' shows them.") (defconst flan-cnr-breakpoint "Pause" "The condition `(pause)' signals. -`lib/prelude.ml' writes it as an ordinary struct under a `restart-case', so +`lib/prelude.fln' writes it as an ordinary struct under a `restart-case', so nothing in the compiler knows a breakpoint from an error and this buffer is the first place that can tell the difference. Named here rather than spelled at its use, because it is a fact about the prelude.") diff --git a/emacs/flan.el b/emacs/flan.el index 1ab1531b..c0fab618 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -1528,7 +1528,7 @@ definition, and RET on a frame or on the stop in the break buffer." (user-error "flan: the daemon gave %s an unreadable location: %s" subject loc)) ((string-match-p "\\`<.*>\\'" (nth 0 parts)) - ;; The prelude is a string inside the compiler (lib/prelude.ml) and + ;; The prelude is a string inside the compiler (lib/prelude.fln) and ;; names itself ; anything in angle brackets is a placeholder ;; the frontend made up, not a path. (user-error "flan: %s is defined in %s, which is not a file on disk" diff --git a/lib/body_macros.ml b/lib/body_macros.ml index 6b3496fb..b2fd8fda 100644 --- a/lib/body_macros.ml +++ b/lib/body_macros.ml @@ -179,7 +179,7 @@ let add (t : t) ~qualify forms = adds nothing. *) let table ?file (forms : Form.t list) : t = let t = create () in - let prelude = try Reader.read_all ~file:Prelude.file Prelude.source with _ -> [] in + let prelude = try Prelude.read () with _ -> [] in add t ~qualify:Fun.id prelude; (match file with | None -> () diff --git a/lib/dune b/lib/dune index be79d9f4..c85ada35 100644 --- a/lib/dune +++ b/lib/dune @@ -64,3 +64,17 @@ (echo "|c}\n\nlet agent_source = {c|\n") (cat %{workspace_root}/vendor/agent/flan_agent.c) (echo "|c}\n"))))) + +; The prelude, carried the same way: lib/prelude.fln is the source, read by +; the indented reader at every build (lib/prelude.ml). No newline after the +; opening quote, so a location's line is the file's line. +(rule + (target prelude_src.ml) + (deps prelude.fln) + (action + (with-stdout-to + prelude_src.ml + (progn + (echo "let source = {fln|") + (cat prelude.fln) + (echo "|fln}\n"))))) diff --git a/lib/prelude.fln b/lib/prelude.fln new file mode 100644 index 00000000..a9de6860 --- /dev/null +++ b/lib/prelude.fln @@ -0,0 +1,2539 @@ +; 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.flan's shape — and they are on the restart +; stack and reachable from a handler or from the break loop without anything +; being pushed here. That is plan.org's "restarts go at the resync point, +; once", with allocation and file failure as the named exceptions and this on +; the default side of the rule. +struct BoundsError(low: i64, high: i64, length: i64) :parent Error + +; What an arithmetic operation with no answer signals. Three situations, and +; until now none of them had a defined behaviour: a divide or remainder by +; zero, which was a raw SIGFPE with no message and no location; the one +; division that overflows, (/ most-negative -1), whose true quotient is one +; past the top of the type and which `idiv` also makes a SIGFPE; and a float +; to integer cast whose value does not fit, where x86 produces a fixed +; "integer indefinite" and LLVM calls the whole thing undefined and may fold +; it to anything. +; +; They signal, for the reason spelled out over BoundsError and for one more +; that is specific to these: a SIGFPE cannot be caught and resumed, so the +; only way to get a message naming the file and the line is to test *before* +; the instruction. Once that branch is being paid for, making it a condition +; rather than a die costs nothing further, and a program that genuinely does +; not care installs a handler once at startup and never thinks about it again. +; +; Same shape as the two above, and for the same reasons: fixed numeric +; fields, no rendered message, nothing that allocates. It is signalled from +; the runtime — flan_arith_error in runtime/flan_rt.c — so **these three +; fields are a C struct that has to agree with this one field for field**, +; the same hand-kept agreement flan_bounds_cond keeps with BoundsError. +; +; `op` is an ArithOp, an i32 at run time, which is what lets the runtime fill +; it in from C; the members' numbers are flan_rt.c's FLAN_ARITH_* codes: +; +; :div-zero (/ a 0) :rem-zero (% a 0) +; :div-overflow (/ min -1) :rem-overflow (% min -1) +; :cast-range a float to integer cast whose value does not fit +; :cast-nan a float to integer cast of NaN +; :cast-inf a float to integer cast of an infinity +; +; `lhs` and `rhs` are the two operands for the first four and the +; destination type's representable range for the casts — the violated condition +; written as a range, which is what flan_slice_promise_error already does +; with BoundsError's fields. Two meanings over two fields rather than two +; condition types, so that a handler writes one clause and not five. The +; value that did not fit is not carried, because it is a float and these +; fields are not; what the handler needs in order to say something useful is +; the range it missed. +; +; The fields are the low 64 bits of whatever they hold. A u64 operand above +; 2^63 therefore reads back negative, which is the same reinterpretation +; every i64 field in every condition here makes and is not worth a fourth +; field to fix. +; +; **No restart is established at the failing operation**, which is +; BoundsError's decision and not StorageExhausted's. The sketch this started +; from asked for `use-value`, and the implementation is what ruled it out: a +; restart frame is allocated by the restart-case that offers it, on its own +; stack, and a transfer carries that frame's address (runtime/flan_rt.c, the +; restart stack). The runtime cannot hold one on a program's behalf, so +; `use-value` here would mean an alloca and a restart frame emitted at every +; division in every checked build — the identical cost refused for indexing +; a few lines above, buying a silently different answer. What answers a +; division by zero is the restart the program already established, a frame +; loop's `continue`, which is reachable from a handler without anything being +; pushed here. +enum ArithOp + div-zero = 0 + rem-zero = 1 + div-overflow = 2 + rem-overflow = 3 + cast-range = 4 + cast-nan = 5 + cast-inf = 6 + +struct ArithError(op: ArithOp, lhs: i64, rhs: i64) :parent Error + +; A call that was compiled against one signature, reaching a function that +; now has another. It exists only in a dev build: there every call to a Flan +; function goes through a cell, the cell carries the signature its body was +; compiled with, and a function redefined with other parameters or another +; return installs anyway — so a caller compiled before the change finds the +; two different at the call and signals this instead of passing arguments +; the new body does not take. A release build has no cells and never +; signals it. +; +; `callee` is the function called, `compiled` the signature the call site +; was compiled for and `current` the one the function has now, both written +; the way a defn writes them: "[i32 i32] i64". Evaluating the caller again +; compiles it against `current`, and the call works from then on. +; +; Signalled from the runtime — flan_stale_call in runtime/flan_rt.c — so +; **these three fields are a C struct that has to agree with this one**, the +; agreement flan_bounds_cond keeps with BoundsError. No restart is +; established at the call, BoundsError's decision for BoundsError's reason: +; nothing a handler supplies makes the old arguments fit the new body. +struct StaleCall(callee: str, compiled: str, current: str) :parent Error + +; A call through a (CFn ...) that holds no function. A CFn may be a struct +; field, a fixed array's element or a global, and each of those starts out +; zeroed, which for a function value is no address at all. Every call +; through one tests first and signals this instead of jumping to nothing. +; `type` is the value's type as written, "(CFn [i32] i32)". +; +; Signalled from the runtime — flan_null_call in runtime/flan_rt.c — so +; **this field is a C struct that has to agree with this one**. No restart is +; established at the call, BoundsError's decision for BoundsError's reason. +struct NullCall(type: str) :parent Error + +; What a generic function signals when no method answers. `generic` is the +; name written at the defgeneric or defmulti, and `value` is what the +; dispatch actually produced -- the class of the first argument for a +; defgeneric, whatever the body answered for a defmulti. A miss is nil for +; the common case of a value that is not an instance at all. +; +; A condition and not a trap, and that is the decision rather than the +; obvious default: Common Lisp signals here, and a dispatch that missed is +; something a program can be written to answer -- a default object, a log +; line, a fallback -- which a trap would take away. `handler-case` around +; the call is the shape, and a method written for `:else` is the other +; answer, in the generic rather than at the call. +; +; `value` is dyn, which is the one field type no other condition here has. +; It is the honest one: a dispatch value is whatever the dispatch answered +; and there is no narrower type it has. The collector reaches it through the +; per-type descriptor a struct with a dyn field carries. +; +; No restart is established at the miss, which is BoundsError's decision +; taken for BoundsError's reason -- see the note above it. +struct NoMethod(generic: str, value) :parent Error + +; A breakpoint. (pause) stops the program where it stands and hands it to the +; break loop, with the whole stack under it readable — C-c C-b lists the +; frames, TAB opens one, and taking `continue` resumes at the call. +; +; It is spelled `pause` and not `break` because `break` is reserved for +; leaving a loop (parse.ml refuses it by name, with the milestone), and a +; breakpoint and a loop exit in the same word would be the worst kind of +; collision: both are legal in the same place and mean opposite things. +; +; Nothing in the compiler knows about this. It is `error` under a +; `restart-case`, which is exactly what a breakpoint is in a language that +; already has conditions: the break loop is entered because nothing handled +; the condition, and `continue` is an ordinary restart whose body is empty, so +; taking it returns here and the caller carries on. A handler-bind above it +; can therefore also intercept a Pause and decline to stop, which is the +; behaviour a release build wants and gets for free. +struct Pause + +fn pause() -> () + restart-case + error(Pause{}) + restart continue() + () + +; The stepper's stop, which C-c C-s puts before each form of a defn's body +; (Ast.instrument_step). It is (pause) with an answer: next goes on stepping +; and continue runs the rest of the call, and the instrumented body keeps +; that answer in a local of its own. Like Pause it is not under Error. +; Named so a program's own step or Step is not what the instrumented body +; calls. +struct StepPoint + +fn step-point() -> bool + restart-case + error(StepPoint{}) + restart next() "stop at the next form" + true + restart continue() "run the rest of this call" + false + +; A seeded PRNG in Flan rather than libc's, because a grid hash is only a +; regression test if the sequence is byte-identical on native and wasm32 +; (plan.org, RNG is ours). PCG-RXS-M-XS 64: one u64 LCG step per draw, and +; the whole 64-bit state permuted down to a 64-bit result by an xorshift +; whose distance is read off the state's top five bits, a multiply, and a +; final xorshift. +; +; A call to any of the five below is one draw — one LCG step — and never two, +; and that is the property the whole file is arranged around: a program's +; position in the sequence depends on how many numbers it asked for and never +; on which of the five it asked for. The one call that is not a draw is the +; one that is not a number either: a range with nothing in it answers lo +; without touching the generator, which is said again where it happens. +; +; The permutation is a bijection of the state, which is the price of getting +; 64 output bits out of 64 state bits: someone holding one result can run it +; backwards to the state and predict every number after it. That is fine for +; a grid, a spawn point or a shuffle and is not fine for a key or a nonce, +; and there is nothing here that pretends otherwise. +once rand-state: u64 = 6364136223846793005 + +fn rand-seed(seed: u64) -> () + rand-state = seed * 6364136223846793005 + 1442695040888963407 + +; One draw, all 64 bits of it, every one of them equally likely. This is the +; unbiased full-width draw and the three functions below are the three ways of +; asking for less than all of it. +fn rand-int() -> u64 + let s = rand-state + rand-state = s * 6364136223846793005 + 1442695040888963407 + ; The shift distance is (top five bits of s) + 5, so it is between 5 and + ; 36 and a 64-bit shift by it is always defined. The 32-bit version of + ; this generator had to mask its rotate because a shift by 32 is poison in + ; LLVM; at this width there is no such case to guard. + ; + ; The multiplier is written in hex, which is how it is written everywhere + ; it appears: in decimal it is 12605985483714917081. Either spelling is + ; a u64 literal and nothing else. It is written here rather than given a + ; name of its own: a + ; prelude constant is a name in every program, and this is an + ; implementation number that nothing outside these four lines wants. + let w = ((s >> ((s >> 59) + 5)) ^^ s) * 0xAEF17502108EF2D9 + (w >> 43) ^^ w + +; In [0, 1), on one draw. The top 53 bits of the draw over 2^53 exactly: 53 is +; the whole mantissa of an f64, so every representable value in the range can +; come up and each is as likely as the format allows. The division is exact +; and so is the conversion — an integer below 2^53 is an f64 with no rounding +; — so the result never reaches 1.0. +; +; The top bits and not the bottom ones, for the reason rand-bool gives. +fn rand() -> f64 = f64(rand-int() >> 11) / 9007199254740992.0 + +; True half the time, on one draw. The bit taken is the draw's top one. +; +; Which bit to take is a question about what the permutation is doing. Every +; bit of a *draw* is sound; the weak bits belong to the state underneath, +; which is a plain LCG — bit 0 of one modulo 2^64 alternates 0, 1, 0, 1 for +; ever, and the low bits above it have periods barely longer. The output's +; low bits are the ones whose soundness rests entirely on the multiply and +; the two xorshifts having scrambled those in; the output's top bits are +; carried there by the multiply out of the whole width of the state, so they +; do not depend on any one state bit and least of all on a weak one. (They +; are not a copy of the state's top bit either: output bit 63 agrees with +; state bit 63 about half the time, which is what a permutation doing its job +; looks like.) Taking the top costs nothing and asks less of the permutation, +; so that is what it takes. +fn rand-bool() -> bool = rand-int() >> 63 == 1 + +; ── Slice algorithms, all in place ──────────────────────────────────── +; +; One family per element type, because there are no generics: each of these +; is a *copy* per element type, and the set below is i32 (what indices, ids +; and tile values are), f32 (what positions, velocities and weights are) and +; [const u8] (what a field coming out of `split` is). +; +; A slice is ptr+len and non-owning, so these mutate the storage they were +; handed: sorting (slice grid 4 9) sorts those five elements of grid and +; leaves the rest alone. That was originally forced — there was no allocator +; to return a new sequence from — and it stays the right shape now that there +; is one, because sorting a thing you already own should not allocate. The +; allocating tier is further down, and a caller sorts a Vec by sorting +; (slice v). +; +; **map, filter, reduce and a sort taking a comparator are here now**, in a +; section of their own after the f32 family. They were blocked on *function +; values* and not on generics, which is why they arrived without generics: +; a (Fn [T ...] R) is an ordinary parameter type. What they are still one +; copy per element type for *is* generics — sum-i32 and sum-f32 are the same +; shape and the same argument — so the set is the same i32 and f32 the rest of +; this family covers. + +; ── One family, over one type variable ──────────────────────────────── +; +; What used to be a copy per element type. A [$t] binds a type variable in +; the signature and every call site instantiates the body at the types it +; passes, so [(sort xs)] over a [i32] and over a [f32] are two emitted +; bodies from one written one. +; +; **Two things in the signatures are not decoration.** +; +; [{:where (is-ordered $t)}] is what lets the body write [<] at all. A type +; variable supports only what it is declared to support — an unconstrained +; one is refused at the *definition*, not at some later call site — and +; [is-ordered] is the predicate that admits [<], [<=], [>], [>=], [min] and +; [max]. It admits [=] too: every type the language orders is a number or an +; enum, so it is equatable. +; +; There is no [copyable?] any more. Since the repeal every value copies — +; a container copies as its header, the copies alias one buffer, and what +; the copies then do is the program's business, as it is in Odin. A body +; that reads an element into a local and writes it into another slot is +; duplicating a header when the element owns storage, and nothing here +; says otherwise any more: that sentence moved from a predicate into this +; comment, which is where Odin keeps it too. +; +; **What did not collapse, and why it should not.** [sum-i32] and [sum-f32] +; widen their element into [i64] and [f64]; "the wider type $t accumulates +; into" is a type-level function, which is a constraint system of a different +; kind, and a generic [sum] that took its accumulator and its [+] would just +; be [reduce]. [append-i64] and [append-f64] are two different primitives. +; [sort-bytes] needs [is-bytes-less] rather than [<] — a [[u8]] is not [is-ordered] +; and cannot be — so it is [sort-by] with the comparison written in, and it +; keeps its name because the stability contract in its comment is worth +; keeping attached to something. + +fn swap(s: [$t], i: i32, j: i32) -> () + let t = s[i] + s[i] = s[j] + s[j] = t + +fn reverse(s: [$t]) -> () + let i = 0 + j = length(s) - 1 + while i < j + swap(s, i, j) + i += 1 + j -= 1 + +; Insertion sort: in place, no recursion, no auxiliary array — quicksort +; would want a stack and mergesort a buffer, and neither exists. Ascending, +; and stable, though with no payload type to carry that is not yet +; observable. +; +; One caveat that only arises at f32: **a NaN in the input makes the order +; undefined.** Every comparison against a NaN is false, so the insertion loop +; never moves one and never moves anything past one; what comes out is sorted +; within each run between NaNs and not sorted across them. That is what C's +; qsort with a naive comparator does too, and the only fix is not to have +; NaNs in the array — there is no ordering of the reals a NaN sits anywhere +; in. +fn sort(s: [$t]) -> () where is-ordered($t) + let i = 1 + while i < length(s) + let j = i + ; `and` short-circuits, which is load-bearing: at j = 0 the left test + ; fails and (at s -1) is never evaluated, so this does not trap. + while j > 0 and s[j - 1] > s[j] + swap(s, j - 1, j) + j -= 1 + i += 1 + +; The same insertion sort, with the one comparison it had written in replaced +; by the one it is told. is-before answers "does a come before b", so passing +; (fn [a b] (< a b)) is ascending and reversing it is descending — and a +; caller wanting a key rather than an order writes the comparison. +; +; It is stable exactly as sort is: the loop stops the moment is-before says +; no, so equal elements never swap past each other. A is-before that is not a +; strict weak ordering — one answering true for both (a b) and (b a) — is the +; caller's mistake and shows up as an order, not as a loop: the inner while +; is bounded by j reaching 0 whatever the comparison says. +; +; This one needs no [is-ordered]: the comparison it cannot have is the +; comparison it is given. It is the shape every generic had to take before +; predicates existed, and it stays because passing a comparison is a real +; thing to want and not only a workaround. +fn sort-by(s: [$t], is-before: Fn($t, $t) -> bool) -> () + let i = 1 + while i < length(s) + let j = i + while j > 0 and is-before(s[j], s[j - 1]) + swap(s, j - 1, j) + j -= 1 + i += 1 + +; The first index holding x. None rather than -1, because Option is what the +; language has and a sentinel index is the bug this avoids. +fn index-of(s: [const $t], x: $t) -> Option(i32) where is-equal($t) + for i in range(length(s)) + if s[i] == x + return Some(i) + None + +; None for an empty slice: there is no least i32 that is also an honest +; answer, and returning one would be a value the caller cannot tell from a +; real element. A NaN in the input is not special-cased and propagates the +; way it does through the builtins — the comparison fails, so the running +; value simply does not change. +; +; Named min-of rather than min because [min] and [max] are builtins over two +; or more numbers, and a defn cannot shadow a builtin: nothing shadows [+] +; either. These reduce a slice, which is a different operation with a +; different arity, so the different name is honest rather than a workaround. +; A type's own limits are (min-value T) and (max-value T). +fn min-of(s: [const $t]) -> Option($t) where is-ordered($t) + if length(s) == 0 + None + else + let m = s[0] + for i in range(length(s)) + m = min(m, s[i]) + Some(m) + +fn max-of(s: [const $t]) -> Option($t) where is-ordered($t) + if length(s) == 0 + None + else + let m = s[0] + for i in range(length(s)) + m = max(m, s[i]) + Some(m) + +; ── Opening an Option without writing the match ─────────────────────── +; +; Everything above answers an (Option $t), and until now `match` was the only +; thing that could open one. That is the right *primitive* — it is the form +; that makes the empty case unforgettable — and it is the wrong thing to write +; when the empty case is one word: +; +; (match (parse-i64 s) (Some v) v None (i64 0)) +; (or-else (parse-i64 s) (i64 0)) +; +; **Neither takes a {:where}, and that is a decision rather than an +; oversight.** A predicate buys an *operation* on the variable — [is-ordered] +; is what lets sort write `<` — and these perform no operation on their +; payload at all: they move it out of the Option, or they look at the tag and +; never touch the payload. That is the one move [ident] in +; test/programs/generics.flan makes, which needs nothing declared, so these +; instantiate at every type including the ones that own storage. +; +; Two, and the ones that were declined are worth naming because a reader will +; look for them: +; +; none? (not (is-some o)) is the whole of it, and this file already +; refuses a wrapper whose only method is the thing it wraps +; — see the Builder entry under "Still refused". +; an unwrap that Refused for the reason file-size below is an Option in the +; signals on None first place: absence is a reply and not a fault, and +; making it a condition puts a handler search on the +; ordinary path. Whether an empty Option is an error is the +; *caller's* question, and the caller has handler-bind if +; the answer is yes. +; a lazy or-else Would need a (Fn [] $t) — sort-by's shape, available the +; day something wants it. A macro would get laziness for +; free and need no generics, and costs more than it buys +; here: a prelude macro drops every prelude defn that +; depends on it out of a macro-module build (see the +; bootstrap hook at the foot of this file), and a macro has +; no way to report a malformed call. + +; This is Java's `Optional.orElse`, hyphenated: eager, and it answers the +; payload's type rather than another Option. Worth saying which, because Rust +; spells something else `or_else` — there it takes a closure and answers an +; `Option`, so borrowing that name for this behaviour would be the wrong +; loan twice over. Java's own lazy sibling is `orElseGet`, which is the one +; declined above. +; +; **At a $t that owns storage the result is a header copy, and the branch not +; taken is still the caller's to free.** Since the copyable? repeal every +; value copies as its header and the copies alias one buffer (see the section +; comment above), so (or-else o d) over a (Vec u8) hands back a second header +; onto o's block or onto d's — and the one it did not choose was never +; released by anything here. That is the same contract `at` on a slice of Vecs +; has; it is written down here because an "or a default" reads like it +; consumes the default and it does not. +fn or-else(o: Option($t), d: $t) -> $t + match o + Some(v) -> v + None -> d + +; Clojure's `is-some`, spelled with the is- a predicate takes. It is what makes a `when` or a `cond` possible +; at all — or-else can say "this or that" and cannot say "only if there is +; one" — and it is the honest shape for the case where the payload is not +; wanted, which a match would still have to bind a name for. +fn is-some(o: Option($t)) -> bool + match o + Some(_v) -> true + None -> false + +; map-in-place writes back into the slice it was handed, for the same reason sort +; does — a slice is non-owning, and transforming a thing you already own +; should not allocate. A map that produces a *different* element type is not +; here: it is two type variables and a second signature, and nothing has +; wanted it. +fn map-in-place(s: [$t], f: Fn($t) -> $t) -> () + for i in range(length(s)) + s[i] = f(s[i]) + +; The general fold, of which sum-i32 is the special case with the + written +; in. The accumulator comes first in the step, which is the order that reads +; as (f acc x) and the order Odin's slice.reduce uses. +fn reduce(s: [const $t], init: $t, f: Fn($t, $t) -> $t) -> $t + let acc = init + for i in range(length(s)) + acc = f(acc, s[i]) + acc + +; A new Vec holding the elements the predicate kept, in the order they were +; in. Owned by the caller: (free v), or let a (free-all a) take the region. +; +; This is the one that proves the containers and the generics compose. It +; allocates — (vec-new $t), push, returns (Vec $t) — and the type-erased Vec +; runtime needed no change at all, because SizeOf and AlignOf are computed at +; the instantiation site, where the element type is concrete. +fn filter(s: [const $t], is-keep: Fn($t) -> bool) -> Vec($t) + let v = vec-new($t) + for i in range(length(s)) + if is-keep(s[i]) + push(v, s[i]) + v + +; A map's keys, and its values, as a new Vec the caller owns. In block order, +; which is the hash's and not the insertion's — sort what comes back if the +; order matters. A string key is copied as the view it is, so the Vec reads +; the map's own key bytes and is good for as long as they are. +fn map-keys(m: Map($k, $v)) -> Vec($k) where is-hashable($k) + let out = vec-new($k) + cur = i64(0) + key: $k = zeroed() + while map-next(m, addr(cur), addr(key)) + push(out, key) + out + +fn map-values(m: Map($k, $v)) -> Vec($v) where is-hashable($k) + ; Walked by key and read back with get, because a place to copy a value + ; into would have to be zeroed first, and a function value cannot be. + let out = vec-new($v) + cur = i64(0) + key: $k = zeroed() + while map-next(m, addr(cur), addr(key)) + match get(m, key) + Some(val) -> push(out, val) + None -> () + out + +; ── The sign questions, over every numeric type at once ─────────────── +; +; The family the whole of generics was asked for. Three questions about a +; number's sign, one body each, answering at i8 through u64 and at both +; float widths — where without a type variable they would be three functions +; per width, which is why they were never written at all. +; +; What makes them writable is not the type variable on its own: it is that a +; written 0 may stand where $t stands. That needs the {:where (is-numeric $t)} +; clause and nothing weaker, because the bound is what promises the literal +; has a meaning at every type the variable can become. An unconstrained +; variable is refused, and so is [is-ordered] — it admits an enum, which holds +; no number. +; +; The comparison is the clause's too: [is-numeric] entails [is-ordered], so one +; predicate on the line gives the body both the < it writes and the 0 it +; writes it against. +; +; **The unsigned instantiations are not mistakes.** (is-neg (u8 3)) is false at +; every u8 and the copy is a constant, which a reader may find odd in the +; emitted code and which is exactly right: a generic is copied per written +; type, and the body says what it says at each of them. Refusing the copy +; would mean a bound that spells "signed", and there is no such predicate. +fn is-pos(x: $t) -> bool where is-numeric($t) = x > 0 + +fn is-neg(x: $t) -> bool where is-numeric($t) = x < 0 + +; Named is-zero rather than =0 because it reads as the question it is. The +; float instantiations answer true for both zeros, since -0.0 = 0.0 is what +; IEEE says and this does not second-guess it. +fn is-zero(x: $t) -> bool where is-numeric($t) = x == 0 + +; ── The per-type layer that stays ───────────────────────────────────── +; +; sum is the one shape a type variable cannot express, and it is worth being +; precise about why rather than leaving two near-identical functions looking +; like an oversight. Each of these *widens*: sum-i32 accumulates in i64 and +; sum-f32 in f64, because summing a screenful into the element's own type is +; how a total silently wraps or absorbs. The per-element casts no longer have +; to be written to say so — an i32 widens into an i64 by itself — and they +; stay because what these two functions exist to show is +; that the accumulator is a different type from the element. "The wider +; type $t accumulates into" is a function from types to types — an associated +; type, or a constraint system of a kind {:where} is not — and a generic sum +; that took its accumulator and its + as parameters would be reduce, which is +; above. + +; Accumulates in i64, because summing a screenful of i32 into an i32 is how a +; total silently wraps. The per-element (i64 ...) would happen on its own now; +; it is written to keep the accumulator's type visible at the line that feeds +; it. +fn sum-i32(s: [const i32]) -> i64 + let t = i64(0) + for i in range(length(s)) + t += i64(s[i]) + t + +; Accumulates in f64 and widens each element explicitly, which is sum-i32's +; argument in its floating form and a stronger one: summing a screenful of +; f32 in f32 does not wrap, it *absorbs* — once the running total is large +; enough, adding a small element rounds to no change at all, and the answer +; is silently short rather than obviously wrong. An f64 accumulator has 29 +; more bits of mantissa and pushes that failure out of reach of any array a +; game holds. +fn sum-f32(s: [const f32]) -> f64 + let t = 0.0 + for i in range(length(s)) + t += f64(s[i]) + t + +; ── Bytes ───────────────────────────────────────────────────────────── +; +; Over [const u8] and not over string, so (bytes-view s) is what a caller writes +; and one copy of each serves strings and byte slices both, writable or not — which is as close to a +; generic as a language without them gets. Nothing here allocates: every +; result is a bool, an index, or a number. + +fn is-bytes-equal(a: [const u8], b: [const u8]) -> bool + if length(a) != length(b) + false + else + for i in range(length(a)) + if a[i] != b[i] + return false + true + +; The length test comes first and `and` short-circuits, so the slice is only +; built once it is known to be in bounds — otherwise a prefix longer than the +; string would trap rather than answer false. +fn has-prefix(s: [const u8], p: [const u8]) -> bool + length(p) <= length(s) and is-bytes-equal(slice(s, 0, length(p)), p) + +fn has-suffix(s: [const u8], p: [const u8]) -> bool + length(p) <= length(s) and is-bytes-equal(slice(s, length(s) - length(p), length(s)), p) + +; The whole slice is an integer, or it is None. bytes->i64 is strtoll, which +; answers 0 for "" and for "abc" and stops at the first junk byte in "12x" — +; three wrong answers a caller cannot tell from a real 12. This is also the +; one that has to be Flan rather than the primitive: strtoll is locale- and +; libc-dependent, and a parser in the language gives the same answer on +; wasm32 as on native for the same reason rand does. +; Overflow wraps, as all arithmetic here does; it is not reported. +fn parse-i64(s: [const u8]) -> Option(i64) + let i = 0 + n = i64(0) + neg = false + if length(s) == 0 + return None + if s[0] == \- or s[0] == \+ + neg = s[0] == \- + i = 1 + if i == length(s) + return None ; a lone sign is not a number + while i < length(s) + let b = s[i] + if b < \0 or b > \9 + return None + n = n * 10 + i64(b - \0) + i += 1 + if neg then Some(0 - n) else Some(n) + +; ── The limits of each numeric type ─────────────────────────────────── +; +; What C spells INT_MAX and FLT_MAX, and what nothing here could reach for: +; cimport pulls in declared functions, structs and typedefs, never a #define, +; so limits.h and float.h have no way in. These are written out instead, once, +; where every program already sees them. +; +; Kebab-case and the type's own name, like ns-per-second above: i32-max, not +; I32_MAX and not INT_MAX. The type prefix is the type as the language spells +; it, so the constant for a u8 is u8-max and there is nothing to translate. +; +; Each carries its type, which is the point of them — i32-max is an i32 and +; putting it where a u8 is wanted is a type error rather than a silent 255. +; That also means the pair for a type is the pair the *language* has, so +; u8-min is here beside u16-min and u32-min and u64-min, all of them zero: a +; family with a hole in it is worse than four lines that say nothing +; surprising, and code generated over a list of type names needs the hole +; filled. +; +; u64-max is written in hex and it has to be. The reader parses a decimal +; integer into an i64, and 18446744073709551615 does not fit one; the hex +; spelling is read as the 64-bit pattern it names, which is what a u64 +; literal is here (see [Check.in_range], which accepts any pattern at 64 bits +; unsigned for exactly this reason). i64-min's decimal spelling *does* fit, +; since it is i64's own least value, so it is written the ordinary way. +const i8-max: i8 = 127 +const i8-min: i8 = -128 +const i16-max: i16 = 32767 +const i16-min: i16 = -32768 +const i32-max: i32 = 2147483647 +const i32-min: i32 = -2147483648 +const i64-max: i64 = 9223372036854775807 +const i64-min: i64 = -9223372036854775808 + +const u8-max: u8 = 255 +const u8-min: u8 = 0 +const u16-max: u16 = 65535 +const u16-min: u16 = 0 +const u32-max: u32 = 4294967295 +const u32-min: u32 = 0 +const u64-max: u64 = 0xFFFFFFFFFFFFFFFF +const u64-min: u64 = 0 + +; The floats are three questions and not two, which is why there is no +; f32-min here to sit beside f32-max. +; +; A float's least value is just the negation of its greatest — (- f32-max) +; — so a constant for it would say nothing the language cannot. What a caller +; actually reaches for under the name "min" is the smallest positive one, and +; that is a different number entirely. Naming it f32-min would make the two +; readings collide at the worst possible place, so the name says which it is: +; f32-min-positive, the smallest *normal* positive value, as Rust's +; MIN_POSITIVE does. Below it the subnormals run further down still, trading +; mantissa bits for exponent range; nothing here names one, because a program +; that wants the last subnormal wants to say so. +; +; The epsilons are the gap from 1.0 to the next representable value above it — +; 2^-23 and 2^-52, the mantissa widths — and not "the smallest number you can +; add to anything". That distinction is the whole reason a comparison written +; (< (abs (- a b)) f64-epsilon) is wrong for any a and b of interesting size, +; and the reason this is named epsilon and not tolerance. +; +; Every decimal below is the shortest one that round-trips to the exact value +; intended, and each is pinned against an independent derivation in +; test/programs/limits.flan rather than trusted. The infinities and NaNs, +; f64-inf, f64-nan, f32-inf and f32-nan, are not here: no literal writes one, +; so the checker supplies them (Check.special_float). +const f32-max: f32 = 3.4028234663852886e38 +const f64-max: f64 = 1.7976931348623157e308 +const f32-min-positive: f32 = 1.1754943508222875e-38 +const f64-min-positive: f64 = 2.2250738585072014e-308 +const f32-epsilon: f32 = 1.1920928955078125e-07 +const f64-epsilon: f64 = 2.220446049250313e-16 + +; ── Numbers ─────────────────────────────────────────────────────────── +; +; Only the ones that encode a decision. abs is (max x (- 0 x)); a wrapper over +; that is a function emitted into every program to save a caller nothing. The +; one honest caveat on that abs: at the least representable integer it +; answers itself, because the negation wraps. That is what every two's-complement abs does, a +; function here would do it too, and the only fix is not to hand it that +; value — so it is written down rather than wrapped. +; +; The float abs is the same one-liner, (max x (- 0.0 x)), and it is not +; wrapped for the same reason — but the caveat above does not carry over, so +; it is not inherited by silence. f32 negation is exact at every value, there +; is no least representable float that negates to itself, and the two edge +; inputs both come out right: -0.0 answers +0.0 (the max picks the subtracted +; side, since neither zero is greater than the other), and a NaN answers a +; NaN (every comparison fails, so the same max picks the subtracted side, +; which is still a NaN). There is nothing left for a function to fix. + +; clamp is a macro and not a function, and the reason is the objection above +; turned around rather than dropped. min and max are builtins that work at +; every numeric type; a clamp *function* cannot, because there are no +; generics, so it would be one copy per type — a clamp-i32, a clamp-f32, a +; clamp-i64 — each emitted into every program to save a caller eleven +; characters. A macro is type-agnostic for free and emits nothing at all: what +; the program contains after expansion is the (min hi (max lo x)) the caller +; would have written. +; +; Each of x, lo and hi appears exactly once in the expansion, so nothing here +; is evaluated twice and an argument with a side effect behaves as it reads. +; +; lo above hi is not checked, and the answer there is hi — the outer min wins. +; That is the same rule Odin's clamp follows and there is nowhere better to +; put a complaint: a macro has no error facility (see `unless` at the foot of +; this file), so a diagnostic would have to be a run-time one, in the one +; construct whose whole point is that it costs nothing at run time. +macro clamp(& args) + if length(args) != 3 + quote + clamp-takes-a-value-a-low-and-a-high() + else + quote + min(~(args[2]), max(~(args[1]), ~(args[0]))) + +; Zero for zero, and zero for NaN — neither is positive nor negative, so +; neither comparison fires. A caller that needs to know which it got should +; be testing for NaN, not reading a sign. +fn sign-f32(x: f32) -> f32 + if x > 0.0 + 1.0 + elif x < 0.0 + -1.0 + else + 0.0 + +; Written as the weighted sum and not as a + t*(b - a): the second form does +; not return b exactly at t = 1.0 once rounding is involved, and a position +; that does not arrive is the bug an interpolation gets reported for. +fn lerp(a: f32, b: f32, t: f32) -> f32 = (1.0 - t) * a + t * b + +; ── More of the RNG ─────────────────────────────────────────────────── +; +; Each is one rand-int, as rand and rand-bool above are, so the sequence a +; program consumes is fixed by how many numbers it asked for and not by which +; of the five it asked for. The exception is the one range that has no number +; in it to give: an empty or reversed range answers lo without drawing, so a +; program that asks for one is where it was. Neither touches the generator +; otherwise. + +; [lo, hi), at i64 — the width of the draw, so that no range is out of reach. +; An empty or reversed range answers lo, a defined value rather than a +; remainder by zero, which is immediate undefined behaviour and not a wrong +; number. hi - lo is computed with wrapping arithmetic and read as a u64, so +; the span is right even for a range as wide as the whole of i64. +; +; One draw when there is a number to draw, and therefore modulo bias: the low +; (2^64 % span) values of the range come up very slightly more often — for any +; span a program is likely to ask for, too slightly to measure. Rejection +; sampling would remove it and would consume an unpredictable number of draws, +; which is the one thing this generator exists not to do. +; +; An index is an i32 here (length answers one), so indexing with this reads +; (at xs (i32 (rand-int-range 0 (i64 (length xs))))). +fn rand-int-range(lo: i64, hi: i64) -> i64 + if hi <= lo then lo else lo + i64(rand-int() % u64(hi - lo)) + +; [lo, hi), because rand never reaches 1.0. One draw, and f64 because rand is. +fn rand-float-range(lo: f64, hi: f64) -> f64 = lo + rand() * (hi - lo) + +; ── Rounding, and the one thing that is not Flan ────────────────────── +; +; All three answer an f32 and take the f32 path, because that is what a +; position, a tile coordinate and a velocity are here. f64 versions wait for +; a program that wants them, for the same reason the f32 slice algorithms do. +; +; The cast to i32 truncates toward zero, which is the only rounding mode the +; language has, so each of these is that cast plus the correction the mode +; does not make. Three inputs would make the cast itself undefined and each +; is named before it happens: NaN (which fails every comparison, so it is +; tested for by (not (= x x)) and nothing else), and the two infinities, +; which are caught by the magnitude test. Above 2^23 an f32 has no fractional +; bits left at all, so returning x there is not an approximation — it is the +; answer — and it doubles as the guard that keeps the cast inside i32. + +; Zero is returned as itself rather than through the cast, which would turn +; -0.0 into +0.0. That is one line for a value most callers never look at, +; and it is here because floorf is specified to return it: a sign of zero is +; how a caller recovers which side a position approached from once the +; magnitude has already been rounded away. +fn floor-f32(x: f32) -> f32 + if not (x == x) or x >= 8388608.0 or x <= -8388608.0 or x == 0.0 + x + else + let t = f32(i32(x)) + if t > x then t - 1.0 else t + +; One deviation from C's ceilf, written down rather than branched around: +; between -1.0 and 0.0 this answers +0.0 where IEEE asks for -0.0, because +; the outer (- 0.0 …) is a subtraction and not a negation. Nothing here reads +; the sign of a zero; a caller that does should test the input instead. +fn ceil-f32(x: f32) -> f32 = 0.0 - floor-f32(0.0 - x) + +; Half away from zero, which is C's round and not the even-tie rule: -2.5 +; goes to -3. Written as floor of the *magnitude* and mirrored, because +; (floor-f32 (+ x 0.5)) is wrong twice over — it is half-*up* rather than +; half-away for negatives, and at the largest f32 below 0.5 the addition +; itself rounds to 1.0 and answers 1 for a number under a half. +fn round-f32(x: f32) -> f32 + let m = if x < 0.0 then 0.0 - x else x + f = floor-f32(m) + r = if m - f >= 0.5 then f + 1.0 else f + if x < 0.0 then 0.0 - r else r + +; sqrt is the one function in this file that is not Flan, and it is a +; `declare` rather than a body for a reason that is not laziness. Every other +; number here is reachable from the four operations and a cast; a square root +; is not. Newton's method needs a starting guess, a good one comes from +; reinterpreting the exponent bits, and the language has no bit-cast between +; f32 and u32 — only value-preserving casts. Without it the iteration needs a +; scaling loop to normalise, converges slowly from a poor guess, and produces +; a result that is *close*, which is exactly what a standard library must not +; hand back. IEEE-754 makes sqrt correctly rounded, so libm's answer is the +; same bit pattern on native and on wasm32 — the byte-identical property that +; keeps rand-int in Flan is, for this one, an argument for going out to C. +; +; The cost is one `declare` line in every module, which LLVM drops where it +; is unused, and one -lm on every link, which build.ml now passes. That flag +; is not optional and not obvious: at -O2 LLVM folds most sqrtf calls into +; the hardware instruction and nothing is left to resolve, so this appears to +; link without it and then fails at -O0, where the call survives. +; +; The better fix belongs to the compiler and not here: llvm.sqrt.f32 as a +; builtin in check.ml and emit.ml is one instruction with no symbol at all. +declare(sqrt-f32, [x f32], f32, "sqrtf") + +; sin and cos go out to libm too, and the argument is *not* the one above — +; it is weaker, and which way it is weaker is the thing to know before +; calling them. IEEE-754 requires sqrt to be correctly rounded, which is why +; sqrtf's answer is the same bit pattern wherever it runs. It requires +; nothing of the kind for sinf and cosf: each implementation is free to be a +; fraction of an ulp off in its own direction, and glibc, musl and wasi-libc +; do differ. So these two are the one place in this file where native and +; wasm32 may not agree bit for bit, and a program whose output is hashed +; across targets — the sand grid of plan.org's "RNG is ours", which is why +; rand-int above is written in Flan and not called out of libc — must not +; route that hash through a sine. +; +; They are here anyway, because the alternative on offer today is worse: a +; caller that wants an angle writes the same two `declare` lines at the top +; of its own file (examples/core-input-gestures-testbed.flan 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.flan, `(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.flan, 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 diff --git a/lib/prelude.ml b/lib/prelude.ml index 8a227ded..6391f187 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -5,10 +5,11 @@ above it is Flan. That is what keeps a second backend cheap — a primitive is the only thing implemented twice. - It lives here as a string rather than as a file, and every program gets it - prepended by [Check.build_program] whether it asked for one or not. That is - a real limit and it is stated as one: the prelude cannot be read as Flan, - extended, or replaced without rebuilding the compiler. There *is* a package + Its source is [lib/prelude.fln], in the indented syntax, embedded as a + string at build time (the rule in lib/dune) and read with [Indent_reader]; + every program gets it prepended by [Check.build_program] whether it asked + for one or not. That is a real limit and it is stated as one: the prelude + cannot be extended or replaced without rebuilding the compiler. There *is* a package loader now — [lib/load.ml], and [vendor/raylib] is a package that uses it — so the obstacle is no longer the mechanism. What is missing is the decision about what a [core:] package would mean for a program that imports nothing, @@ -42,2488 +43,7 @@ on its own, but not at that one -- a [u64] widens into nothing at all, and the printer it was being forced through was the wrong one. *) -let source = {flan| -;; 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. -(defstruct 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. -(defstruct StorageExhausted :parent Error [bytes i64 align i64 allocator i64]) - -;; 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.flan'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. -(defstruct BoundsError :parent Error [low i64 high i64 length i64]) - -;; 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. -(defenum ArithOp - [div-zero 0 rem-zero 1 div-overflow 2 rem-overflow 3 - cast-range 4 cast-nan 5 cast-inf 6]) - -(defstruct ArithError :parent Error [op ArithOp lhs i64 rhs i64]) - -;; 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. -(defstruct StaleCall :parent Error [callee str compiled str current str]) - -;; 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. -(defstruct NullCall :parent Error [type str]) - -;; 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. -(defstruct NoMethod :parent Error [generic str value dyn]) - -;; 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. -(defstruct Pause []) - -(defn pause [] () - (restart-case (error (Pause {})) - (continue [] (do)))) -;; 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. -(defstruct StepPoint []) - -(defn step-point [] bool - (restart-case (error (StepPoint {})) - (next [] :report "stop at the next form" true) - (continue [] :report "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. -(defonce rand-state u64 6364136223846793005) - -(defn rand-seed [seed u64] () - (set 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. -(defn rand-int [] u64 - (let [s rand-state] - (set 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 (* (bit-xor (>> s (+ (>> s 59) 5)) s) 0xAEF17502108EF2D9)] - (bit-xor (>> 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. -(defn 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. -(defn 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. - -(defn swap [s [$t] i i32 j i32] () - (let [t (at s i)] - (set (at s i) (at s j)) - (set (at s j) t))) - -(defn reverse [s [$t]] () - (let [i 0 - j (- (length s) 1)] - (while (< i j) - (swap s i j) - (set i (+ i 1)) - (set j (- 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. -(defn 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 (and (> j 0) (> (at s (- j 1)) (at s j))) - (swap s (- j 1) j) - (set j (- j 1)))) - (set i (+ 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. -(defn sort-by [s [$t] is-before (Fn [$t $t] bool)] () - (let [i 1] - (while (< i (length s)) - (let [j i] - (while (and (> j 0) (is-before (at s j) (at s (- j 1)))) - (swap s (- j 1) j) - (set j (- j 1)))) - (set i (+ 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. -(defn index-of [s [const $t] x $t] (Option i32) - {:where (is-equal $t)} - (dotimes [i (length s)] - (when (= (at 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). -(defn min-of [s [const $t]] (Option $t) - {:where (is-ordered $t)} - (if (= (length s) 0) - None - (let [m (at s 0)] - (dotimes [i (length s)] - (set m (min m (at s i)))) - (Some m)))) - -(defn max-of [s [const $t]] (Option $t) - {:where (is-ordered $t)} - (if (= (length s) 0) - None - (let [m (at s 0)] - (dotimes [i (length s)] - (set m (max m (at s i)))) - (Some m)))) - -;; ── Opening an Option without writing the match ─────────────────────── -;; -;; Everything above answers an (Option $t), and until now `match` was the only -;; thing that could open one. That is the right *primitive* — it is the form -;; that makes the empty case unforgettable — and it is the wrong thing to write -;; when the empty case is one word: -;; -;; (match (parse-i64 s) (Some v) v None (i64 0)) -;; (or-else (parse-i64 s) (i64 0)) -;; -;; **Neither takes a {:where}, and that is a decision rather than an -;; oversight.** A predicate buys an *operation* on the variable — [is-ordered] -;; is what lets sort write `<` — and these perform no operation on their -;; payload at all: they move it out of the Option, or they look at the tag and -;; never touch the payload. That is the one move [ident] in -;; test/programs/generics.flan makes, which needs nothing declared, so these -;; instantiate at every type including the ones that own storage. -;; -;; Two, and the ones that were declined are worth naming because a reader will -;; look for them: -;; -;; none? (not (is-some o)) is the whole of it, and this file already -;; refuses a wrapper whose only method is the thing it wraps -;; — see the Builder entry under "Still refused". -;; an unwrap that Refused for the reason file-size below is an Option in the -;; signals on None first place: absence is a reply and not a fault, and -;; making it a condition puts a handler search on the -;; ordinary path. Whether an empty Option is an error is the -;; *caller's* question, and the caller has handler-bind if -;; the answer is yes. -;; a lazy or-else Would need a (Fn [] $t) — sort-by's shape, available the -;; day something wants it. A macro would get laziness for -;; free and need no generics, and costs more than it buys -;; here: a prelude macro drops every prelude defn that -;; depends on it out of a macro-module build (see the -;; bootstrap hook at the foot of this file), and a macro has -;; no way to report a malformed call. - -;; This is Java's `Optional.orElse`, hyphenated: eager, and it answers the -;; payload's type rather than another Option. Worth saying which, because Rust -;; spells something else `or_else` — there it takes a closure and answers an -;; `Option`, so borrowing that name for this behaviour would be the wrong -;; loan twice over. Java's own lazy sibling is `orElseGet`, which is the one -;; declined above. -;; -;; **At a $t that owns storage the result is a header copy, and the branch not -;; taken is still the caller's to free.** Since the copyable? repeal every -;; value copies as its header and the copies alias one buffer (see the section -;; comment above), so (or-else o d) over a (Vec u8) hands back a second header -;; onto o's block or onto d's — and the one it did not choose was never -;; released by anything here. That is the same contract `at` on a slice of Vecs -;; has; it is written down here because an "or a default" reads like it -;; consumes the default and it does not. -(defn 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. -(defn 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. -(defn map-in-place [s [$t] f (Fn [$t] $t)] () - (dotimes [i (length s)] - (set (at s i) (f (at 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. -(defn reduce [s [const $t] init $t f (Fn [$t $t] $t)] $t - (let [acc init] - (dotimes [i (length s)] - (set acc (f acc (at 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. -(defn filter [s [const $t] is-keep (Fn [$t] bool)] (Vec $t) - (let [v (vec-new $t)] - (dotimes [i (length s)] - (when (is-keep (at s i)) - (push v (at 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. -(defn map-keys [m (Map $k $v)] (Vec $k) - {:where (is-hashable $k)} - (let [out (vec-new $k) - cur (i64 0) - key (the $k (zeroed))] - (while (map-next m (addr cur) (addr key)) - (push out key)) - out)) - -(defn 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 (the $k (zeroed))] - (while (map-next m (addr cur) (addr key)) - (match (get m key) - (Some val) (push out val) - None (do))) - 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. -(defn is-pos [x $t] bool - {:where (is-numeric $t)} - (> x 0)) - -(defn 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. -(defn 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. -(defn sum-i32 [s [const i32]] i64 - (let [t (i64 0)] - (dotimes [i (length s)] - (set t (+ t (i64 (at 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. -(defn sum-f32 [s [const f32]] f64 - (let [t 0.0] - (dotimes [i (length s)] - (set t (+ t (f64 (at 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. - -(defn is-bytes-equal [a [const u8] b [const u8]] bool - (if (!= (length a) (length b)) - false - (do - (dotimes [i (length a)] - (when (!= (at a i) (at 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. -(defn has-prefix [s [const u8] p [const u8]] bool - (and (<= (length p) (length s)) - (is-bytes-equal (slice s 0 (length p)) p))) - -(defn has-suffix [s [const u8] p [const u8]] bool - (and (<= (length p) (length s)) - (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. -(defn parse-i64 [s [const u8]] (Option i64) - (let [i 0 - n (i64 0) - neg false] - (when (= (length s) 0) - (return None)) - (when (or (= (at s 0) \-) (= (at s 0) \+)) - (set neg (= (at s 0) \-)) - (set i 1)) - (when (= i (length s)) - (return None)) ; a lone sign is not a number - (while (< i (length s)) - (let [b (at s i)] - (when (or (< b \0) (> b \9)) - (return None)) - (set n (+ (* n 10) (i64 (- b \0))))) - (set i (+ i 1))) - (if neg (Some (- 0 n)) (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. -(defconst i8-max i8 127) -(defconst i8-min i8 -128) -(defconst i16-max i16 32767) -(defconst i16-min i16 -32768) -(defconst i32-max i32 2147483647) -(defconst i32-min i32 -2147483648) -(defconst i64-max i64 9223372036854775807) -(defconst i64-min i64 -9223372036854775808) - -(defconst u8-max u8 255) -(defconst u8-min u8 0) -(defconst u16-max u16 65535) -(defconst u16-min u16 0) -(defconst u32-max u32 4294967295) -(defconst u32-min u32 0) -(defconst u64-max u64 0xFFFFFFFFFFFFFFFF) -(defconst 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). -(defconst f32-max f32 3.4028234663852886e38) -(defconst f64-max f64 1.7976931348623157e308) -(defconst f32-min-positive f32 1.1754943508222875e-38) -(defconst f64-min-positive f64 2.2250738585072014e-308) -(defconst f32-epsilon f32 1.1920928955078125e-07) -(defconst 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. -(defmacro clamp [& args] - (if (!= (length args) 3) - `(clamp-takes-a-value-a-low-and-a-high) - `(min ~(at args 2) (max ~(at args 1) ~(at 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. -(defn sign-f32 [x f32] f32 - (cond - (> x 0.0) 1.0 - (< 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. -(defn 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))))). -(defn rand-int-range [lo i64 hi i64] i64 - (if (<= hi lo) - lo - (+ lo (i64 (% (rand-int) (u64 (- hi lo))))))) - -;; [lo, hi), because rand never reaches 1.0. One draw, and f64 because rand is. -(defn 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. -(defn floor-f32 [x f32] f32 - (if (or (not (= x x)) - (>= x 8388608.0) - (<= x -8388608.0) - (= x 0.0)) - x - (let [t (f32 (i32 x))] - (if (> t x) (- t 1.0) 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. -(defn 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. -(defn round-f32 [x f32] f32 - (let [m (if (< x 0.0) (- 0.0 x) x) - f (floor-f32 m) - r (if (>= (- m f) 0.5) (+ f 1.0) f)] - (if (< x 0.0) (- 0.0 r) 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.flan 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. -(defn abs [x $t] $t - {:where (is-integer $t)} - (if (< x 0) (- 0 x) 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. -(defconst pi-f32 f32 3.14159265358979323846) -(defconst pi-f64 f64 3.14159265358979323846) -(defconst tau-f32 f32 6.28318530717958647692) -(defconst 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.flan, `(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. -(defconst ns-per-microsecond i64 1000) -(defconst ns-per-millisecond i64 1000000) -(defconst ns-per-second i64 1000000000) - -(defn monotonic-seconds [] f64 - (/ (f64 (monotonic-ns)) 1000000000.0)) - -(defn 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") - -(defn 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") - -(defn getenv [name str] (Option [u8]) - (let [n (i64 0) - p (getenv-raw name (addr n))] - (if (< n 0) - None - (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. - -(defn is-digit [b u8] bool - (and (>= b \0) (<= b \9))) - -(defn is-space [b u8] bool - (or (= b \space) (= b \tab) (= b \newline) (= 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. -(defn index-of-bytes [s [const u8] p [const u8]] (Option i32) - (when (> (length p) (length s)) - (return None)) - (let [last (- (length s) (length p)) - i 0] - (while (<= i last) - (when (is-bytes-equal (slice s i (+ i (length p))) p) - (return (Some i))) - (set 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. -(defn trim [s [const u8]] [const u8] - (let [lo 0 - hi (length s)] - (while (and (< lo hi) (is-space (at s lo))) - (set lo (+ lo 1))) - (while (and (< lo hi) (is-space (at s (- hi 1)))) - (set hi (- 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. -(defn parse-f64 [s [const u8]] (Option f64) - (let [i 0 - digits 0] - (when (or (= (length s) 0) (> (length s) 511)) - (return None)) - (when (or (= (at s 0) \-) (= (at s 0) \+)) - (set i 1)) - (while (and (< i (length s)) (is-digit (at s i))) - (set i (+ i 1)) - (set digits (+ digits 1))) - (when (and (< i (length s)) (= (at s i) \.)) - (set i (+ i 1)) - (while (and (< i (length s)) (is-digit (at s i))) - (set i (+ i 1)) - (set digits (+ digits 1)))) - (when (= digits 0) - (return None)) ; "." and "+" and "e5" are not numbers - (when (and (< i (length s)) (or (= (at s i) \e) (= (at s i) \E))) - (set i (+ i 1)) - (when (and (< i (length s)) (or (= (at s i) \-) (= (at s i) \+))) - (set i (+ i 1))) - (let [e 0] - (while (and (< i (length s)) (is-digit (at s i))) - (set i (+ i 1)) - (set e (+ e 1))) - (when (= 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)) (Some (bytes->f64 s)) 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. -(defstruct Rune [code i32 width i32 ok bool]) - -(defn is-rune-start [b u8] bool - (!= (bit-and b 0xc0) 0x80)) - -(defn decode-rune [s [const u8]] Rune - (when (= (length s) 0) - (return (Rune {.code 0 .width 0 .ok false}))) - (let [b0 (at s 0)] - (when (< 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)] - (cond - (< b0 0xc2) (set size 0) - (<= b0 0xdf) (set size 2) - (= b0 0xe0) (do (set size 3) (set lo (u8 0xa0))) - (<= b0 0xec) (set size 3) - (= b0 0xed) (do (set size 3) (set hi (u8 0x9f))) - (<= b0 0xef) (set size 3) - (= b0 0xf0) (do (set size 4) (set lo (u8 0x90))) - (<= b0 0xf3) (set size 4) - (= b0 0xf4) (do (set size 4) (set hi (u8 0x8f))) - :else (set size 0)) - (when (= 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. - (when (> size (length s)) - (return (Rune {.code 0 .width 1 .ok false}))) - (let [b1 (at s 1)] - (when (or (< b1 lo) (> b1 hi)) - (return (Rune {.code 0 .width 1 .ok false}))) - (when (= size 2) - (return (Rune {.code (bit-or (<< (i32 (bit-and b0 0x1f)) 6) - (i32 (bit-and b1 0x3f))) - .width 2 .ok true}))) - (let [b2 (at s 2)] - (when (or (< b2 0x80) (> b2 0xbf)) - (return (Rune {.code 0 .width 1 .ok false}))) - (when (= size 3) - (return (Rune {.code (bit-or (bit-or (<< (i32 (bit-and b0 0x0f)) 12) - (<< (i32 (bit-and b1 0x3f)) 6)) - (i32 (bit-and b2 0x3f))) - .width 3 .ok true}))) - (let [b3 (at s 3)] - (when (or (< b3 0x80) (> b3 0xbf)) - (return (Rune {.code 0 .width 1 .ok false}))) - (Rune {.code (bit-or (bit-or (<< (i32 (bit-and b0 0x07)) 18) - (bit-or (<< (i32 (bit-and b1 0x3f)) 12) - (<< (i32 (bit-and b2 0x3f)) 6))) - (i32 (bit-and 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. -(defn rune-at [s [const u8] i i32] (Option char) - (if (or (< i 0) (>= i (length s))) - None - (let [r (decode-rune (slice s i (length s)))] - (if (.ok r) (Some (char (.code r))) 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. -(defn rune-count [s [const u8]] i32 - (let [i 0 - n 0] - (while (< i (length s)) - (let [r (decode-rune (slice s i (length s)))] - (set i (+ i (.width r))) - (set n (+ n 1)))) - n)) - -(defn is-valid-utf8 [s [const u8]] bool - (let [i 0] - (while (< i (length s)) - (let [r (decode-rune (slice s i (length s)))] - (when (not (.ok r)) - (return false)) - (set i (+ i (.width r))))) - 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. -(defn rune-size [code i32] (Option i32) - (cond - (< code 0) None - (<= code 0x7f) (Some 1) - (<= code 0x7ff) (Some 2) - (and (>= code 0xd800) (<= code 0xdfff)) None - (<= code 0xffff) (Some 3) - (<= 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. -(defn encode-rune [dst [u8] code i32] (Option i32) - (match (rune-size code) - None None - (Some w) - (if (> w (length dst)) - None - (do - (cond - (= w 1) - (set (at dst 0) (u8 code)) - (= w 2) - (do (set (at dst 0) (u8 (bit-or 0xc0 (>> code 6)))) - (set (at dst 1) (u8 (bit-or 0x80 (bit-and code 0x3f))))) - (= w 3) - (do (set (at dst 0) (u8 (bit-or 0xe0 (>> code 12)))) - (set (at dst 1) (u8 (bit-or 0x80 (bit-and (>> code 6) 0x3f)))) - (set (at dst 2) (u8 (bit-or 0x80 (bit-and code 0x3f))))) - :else - (do (set (at dst 0) (u8 (bit-or 0xf0 (>> code 18)))) - (set (at dst 1) (u8 (bit-or 0x80 (bit-and (>> code 12) 0x3f)))) - (set (at dst 2) (u8 (bit-or 0x80 (bit-and (>> code 6) 0x3f)))) - (set (at dst 3) (u8 (bit-or 0x80 (bit-and 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. -(defstruct Split [rest [const u8] sep u8 more bool]) - -(defn split-on-byte [s [const u8] sep u8] Split - (Split {.rest s .sep sep .more true})) - -(defn split-next [it (Ptr Split)] (Option [const u8]) - (when (not (.more it)) - (return None)) - (match (index-of (.rest it) (.sep it)) - (Some i) - (let [field (slice (.rest it) 0 i)] - (set (.rest it) (slice (.rest it) (+ i 1) (length (.rest it)))) - (Some field)) - None - (let [field (.rest it)] - (set (.more it) false) - (set (.rest it) - (slice (.rest it) (length (.rest it)) (length (.rest it)))) - (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. -(defn lower-ascii [b u8] u8 - (if (and (>= b \A) (<= b \Z)) (+ b 32) b)) - -(defn upper-ascii [b u8] u8 - (if (and (>= b \a) (<= b \z)) (- b 32) 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. -(defn is-bytes-ci-equal [a [const u8] b [const u8]] bool - (if (!= (length a) (length b)) - false - (do - (dotimes [i (length a)] - (when (!= (lower-ascii (at a i)) (lower-ascii (at 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. -(defn is-bytes-less [a [const u8] b [const u8]] bool - (let [n (min (length a) (length b))] - (dotimes [i n] - (when (!= (at a i) (at b i)) - (return (< (at a i) (at 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. -(defn 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. -(defstruct 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. -(defstruct Runes [rest [const u8]]) - -(defn runes-next [it (Ptr Runes)] (Option char) - (if (= (length (.rest it)) 0) - None - (let [r (decode-rune (.rest it))] - (set (.rest it) (slice (.rest it) (.width r) (length (.rest it)))) - (Some (char (if (.ok r) (.code r) 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. -(defn append-i64 [b (Ptr (Vec u8)) n i64] () - (append b (i64->bytes n))) - -(defn 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. -(defn concat [parts [const [const u8]]] String - (let [b (vec-new u8)] - (dotimes [i (length parts)] - (append (addr b) (at 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. -(defn join [parts [const [const u8]] sep [const u8]] String - (let [b (vec-new u8)] - (dotimes [i (length parts)] - (when (> i 0) - (append (addr b) sep)) - (append (addr b) (at parts i))) - (bytes->string b))) - -(defn repeat-bytes [s [const u8] n i32] String - (let [b (vec-new u8)] - (dotimes [i 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. -(defn to-lower [s [const u8]] String - (let [b (vec-new u8)] - (dotimes [i (length s)] - (push b (lower-ascii (at s i)))) - (bytes->string b))) - -(defn to-upper [s [const u8]] String - (let [b (vec-new u8)] - (dotimes [i (length s)] - (push b (upper-ascii (at 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. -(defn 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) - (while (< i (length s)) - (match (index-of-bytes (slice s i (length s)) from) - (Some k) - (do - (append (addr b) (slice s i (+ i k))) - (append (addr b) to) - (set i (+ i k (length from)))) - None - (do - (append (addr b) (slice s i (length s))) - (set 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". -(defn 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. -(defn 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 (set 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. -(defn format-f64 [x f64 prec i32] String - (let [b (vec-new u8) - p (clamp prec 0 9)] - (cond - (not (= x x)) - (append (addr b) (bytes-view "nan")) - - (and (= x (* x 2.0)) (!= x 0.0)) - (append (addr b) (bytes-view (if (< x 0.0) "-inf" "inf"))) - - :else - (let [neg (< x 0.0) - m (if neg (- 0.0 x) x)] - (if (>= m 9.0e18) - (append (addr b) (f64->bytes x)) - (let [scale (i64 1)] - (dotimes [i p] - (set scale (* 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". - (when (>= fr scale) - (set fr 0) - (set ip (+ 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. - (when neg - (push b \-)) - (append-i64 (addr b) ip) - (when (> 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)] - (dotimes [i (- 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. -(defstruct 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"). -(defn embed-find [files [EmbedFile] name str] (Option [const u8]) - (dotimes [i (length files)] - (when (is-bytes-equal (bytes-view (.name (at files i))) (bytes-view name)) - (return (Some (.data (at files i)))))) - 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. -(defstruct FileError :parent Error [path str op i32 reason i32]) - -(defconst file-op-read i32 0) -(defconst file-op-write i32 1) -(defconst file-op-delete i32 2) -(defconst file-op-rename i32 3) -(defconst file-op-mkdir i32 4) - -(defconst file-missing i32 1) -(defconst file-denied i32 2) -(defconst 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. -(defconst 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. -(defn 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. -(defn file-size [path str] (Option i64) - (let [n (i64 0)] - (if (= (file-stat-raw path (addr n)) 1) (Some n) 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") - -(defn macro-slurp [path str] (Option [u8]) - (let [n (i64 0) - p (macro-slurp-raw path (addr n))] - (if (< n 0) - None - (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. -(defdata 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. -(defn form-nil [] [Form] - (let [v (vec-new Form)] - (slice v))) - -(defn form-cons [x Form rest [Form]] [Form] - (let [v (vec-new Form)] - (push v x) - (dotimes [i (length rest)] - (push v (at rest i))) - (slice v))) - -(defn form-append [a [Form] b [Form]] [Form] - (let [v (vec-new Form)] - (dotimes [i (length a)] - (push v (at a i))) - (dotimes [i (length b)] - (push v (at 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. -(defn form-rest [xs [Form] from i32] [Form] - (let [v (vec-new Form) - i from] - (while (< i (length xs)) - (push v (at xs i)) - (set 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). -(defn form-wrap-each [head str xs [Form]] [Form] - (let [v (vec-new Form)] - (dotimes [i (length xs)] - (push v (Form.List {.xs (form-pair (Form.Sym {.s head}) (at 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. -(defn 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") - -(defn gensym [] Form - (let [v (vec-new u8)] - (push v 126) ; ~ - (push v 103) ; g - (let [d (i64->bytes (gensym-next))] - (dotimes [i (length d)] - (push v (at 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. -(defmacro unless [& args] - (if (< (length args) 1) - `(unless-takes-a-test) - `(if (not ~(at 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 ...). -(defmacro until [& args] - (let [labelled (and (> (length args) 0) - (match (at args 0) (Form.Kw k) true _ false)) - from (if labelled 1 0)] - (if (>= from (length args)) - `(compile-error - "until is (until test body ...), or (until :label test body ...)") - (if labelled - `(while ~(at args 0) (not ~(at args 1)) ~@(form-rest args 2)) - `(while (not ~(at 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. -(defmacro comment [& args] - `(do)) - -;; ── 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. -(defmacro inc [& args] - (if (!= (length args) 1) - `(inc-takes-one-number) - `(+ ~(at args 0) 1))) - -(defmacro dec [& args] - (if (!= (length args) 1) - `(dec-takes-one-number) - `(- ~(at args 0) 1))) - -(defmacro ++ [& args] - (if (!= (length args) 1) - `(++-takes-one-place) - (let [g (gensym)] - `(~(Form.Sym {.s "update~"}) ~(at args 0) ~g (+ ~g 1))))) - -(defmacro -- [& args] - (if (!= (length args) 1) - `(---takes-one-place) - (let [g (gensym)] - `(~(Form.Sym {.s "update~"}) ~(at 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. -(defmacro update [& args] - (if (< (length args) 2) - `(update-takes-a-place-and-a-function) - (let [g (gensym)] - `(~(Form.Sym {.s "update~"}) ~(at args 0) ~g - (~(at 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. -(defn into-wrap [ts [Form] dst Form x Form] Form - (loop [k (length ts) body `(push ~dst ~x)] - (if (= k 0) - body - (let [t (at ts (- k 1)) - items (form-items t)] - (if (!= (length items) 2) - `(into-transform-is-map-or-filter-of-one-function ~t) - (let [head (at items 0) - f (at items 1)] - (cond - (is-form-named head "map") (recur (- k 1) `(let [~x (~f ~x)] ~body)) - (is-form-named head "filter") (recur (- k 1) `(when (~f ~x) ~body)) - :else `(into-transform-is-map-or-filter ~t)))))))) - -;; Whether any transform in the chain is a (map f). -(defn has-map-step [ts [Form]] bool - (loop [k 0] - (cond - (= k (length ts)) false - (let [items (form-items (at ts k))] - (and (> (length items) 0) (is-form-named (at items 0) "map"))) true - :else (recur (+ k 1))))) - -;; 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. -(defn form-items [f Form] [Form] - (match f - (Form.List xs) xs - _ (form-nil))) - -(defn 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.flan, where a lone () argument is a -;; body that was not written rather than a body of one form. -(defn is-form-empty-list [f Form] bool - (match f - (Form.List xs) (= (length xs) 0) - _ false)) - -(defn is-form-sym [f Form] bool - (match f - (Form.Sym s) true - _ false)) - -(defn 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. -(defmacro into [& args] - (if (< (length args) 2) - `(into-takes-a-source-a-destination-and-transforms) - (let [from (at args 0) - named? (is-form-sym from) - src (if named? from (gensym)) - bind (if named? (form-nil) (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. - shares (if (has-map-step (form-rest args 2)) - (form-nil) - (form-cons `(into-copies-elements ~src ~(at args 1) ~@(form-rest args 2)) - (form-nil))) - dst (gensym) - x (gensym) - i (gensym)] - `(let [~dst ~(at args 1) ~@bind] - ~@shares - (dotimes [~i (length ~src)] - (let [~x (at ~src ~i)] - ~(into-wrap (form-rest args 2) dst x))) - ~dst)))) - -|flan} +let source = Prelude_src.source let file = "" @@ -2544,4 +64,13 @@ let file = "" from here. *) let bootstrap : (Form.t list -> Form.t list) ref = ref (fun fs -> fs) -let forms () = !bootstrap (Reader.read_all ~file source) +(* The prelude's forms as written, before [bootstrap]. The file keeps the + name [] rather than [prelude.fln]: nothing can open it, and a + diagnostic about a prelude form spells its code as the paren syntax does, + which is what [Source.indented_at] gives for a name that is not [.fln]. + Read once per process: the indented reader over the whole prelude is + milliseconds, several readers want it per build, and a form is immutable. *) +let parsed = lazy (Indent_reader.read_all ~file source) +let read () = Lazy.force parsed + +let forms () = !bootstrap (read ()) diff --git a/lib/source.ml b/lib/source.ml index ba670faf..32000108 100644 --- a/lib/source.ml +++ b/lib/source.ml @@ -3,7 +3,8 @@ ([Reader]). Both give the same [Form.t], so nothing past this point knows which one a file was written in, and a program may mix them freely. - Only program sources come through here. The prelude, the wire protocol and + Only program sources come through here. The prelude is indented text + embedded in the compiler and read by [Prelude.read]; the wire protocol and the registry's spellings are paren text the compiler writes itself, and read it with [Reader] directly. *)