diff --git a/lib/check.ml b/lib/check.ml index 13c9203..46c3f10 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -4777,6 +4777,70 @@ and named_call ctx ~want loc name args = [ file_guard ctx loc ~path_slot:ps ~op:1 steps ]))) | _ -> assert false) + (* ── the three that change the filesystem ────────────────────────── + [delete-file], [rename-file] and [make-directory] are [barf]'s shape with + a different runtime call, and they are here rather than as prelude + [declare]s for the one thing a declare cannot do: signal [FileError] with + the two restarts the compiler emits. A declare could only answer a bool, + and "the delete failed, here is a boolean" is the shape decision 5 exists + to keep out of this language — a handler that made the parent directory + and wants [retry], or that has another path and wants [use-value], has + nothing to hold onto. + + Each answers [()] and not a bool for the same reason [barf] does: the + failure is the condition, so a return value would only ever be true. The + questions that are *not* failures — does this exist, how big is it — + answer a value instead, and those two are prelude functions over one + [declare] because nothing about them needs a restart. + + [op] continues the FileError numbering the prelude names: 0 read, 1 write, + and 2, 3, 4 here. A handler matching on it is matching on the prelude's + [file-op-delete] and friends, not on a literal. *) + | "delete-file" | "make-directory" -> + arity loc name 1 args; + let sym, op = + if String.equal name "delete-file" then "flan_file_delete", 2 + else "flan_file_mkdir", 4 + in + let path = check ctx ~want:Types.String (List.hd args) in + let ps = fresh_slot ctx Types.String in + let steps try_ = + [ try_ (rt loc (Types.Int Types.I8) sym + [ mk loc Types.String (Tast.Local ps) ]) ] + in + expect loc ~want + (mk loc Types.Unit + (Tast.Let ([ (ps, path) ], + [ file_guard ctx loc ~path_slot:ps ~op steps ]))) + + (* Two paths and one restart slot, so the guard holds the *source*: a + [use-value] renames a different file to the same destination. That is the + direction a handler can act on — the destination it asked for is the one + thing it already knows — and it is written down here because the other + reading is equally plausible until somebody says which it is. + + The destination is bound before the loop, exactly as [barf] binds its + data, so a retry re-attempts the rename and not the expression that + computed where to. *) + | "rename-file" -> + arity loc name 2 args; + (match args with + | [ from_; to_ ] -> + let from_ = check ctx ~want:Types.String from_ in + let to_ = check ctx ~want:Types.String to_ in + let ps = fresh_slot ctx Types.String in + let ds = fresh_slot ctx Types.String in + let steps try_ = + [ try_ (rt loc (Types.Int Types.I8) "flan_file_rename" + [ mk loc Types.String (Tast.Local ps); + mk loc Types.String (Tast.Local ds) ]) ] + in + expect loc ~want + (mk loc Types.Unit + (Tast.Let ([ (ps, from_); (ds, to_) ], + [ file_guard ctx loc ~path_slot:ps ~op:3 steps ]))) + | _ -> assert false) + (* ── containers ────────────────────────────────────────────────── *) (* [at] and [len] were already the names for a fixed array and a slice, so a Vec extends them rather than adding a parallel pair — which is the diff --git a/lib/emit.ml b/lib/emit.ml index c9e771a..7f7eb1e 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -2809,6 +2809,14 @@ declare i64 @flan_hash_combine(i64, i64) ; here. `embed` needs none of these: it is a compile-time constant. declare i8 @flan_file_size(ptr, i64, ptr) declare i8 @flan_file_write(ptr, i64, ptr, i64) +; The three that change the filesystem. flan_file_stat is not here for the +; reason flan_file_read is not: nothing emitted calls it. It is reached from +; the prelude through a `declare`, because file-exists? and file-size answer a +; value rather than signalling and so need none of the guard machinery these +; three do. +declare i8 @flan_file_delete(ptr, i64) +declare i8 @flan_file_rename(ptr, i64, ptr, i64) +declare i8 @flan_file_mkdir(ptr, i64) declare i64 @flan_file_fail_reason() declare i8 @flan_slurp_into(ptr, ptr, i64) |} diff --git a/lib/prelude.ml b/lib/prelude.ml index 3ed65e6..30d6435 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -676,6 +676,258 @@ let source = {flan| (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 per width because there are no generics over the +;; numeric types and min and max are builtins rather than functions, so a +;; single abs is not expressible today. +;; +;; 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. +(defn abs-i32 [x i32] i32 + (if (< x 0) (- 0 x) x)) + +(defn abs-i64 [x i64] i64 + (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-ptr` 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 string out-len (Ptr i64)] (Ptr u8) "flan_getenv") + +(defn getenv [name string] (Option [u8]) + (let [n (i64 0) + p (getenv-raw name (addr n))] + (if (< n 0) + None + (Some (slice-from-ptr p (i32 n)))))) + ;; ── Byte classes ────────────────────────────────────────────────────── ;; ;; ASCII only, and deliberately: a byte is a byte here, there is no code point @@ -1439,6 +1691,9 @@ let source = {flan| (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) @@ -1447,9 +1702,45 @@ let source = {flan| ;; 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). +;; 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 string 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 string] 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 string] (Option i64) + (let [n (i64 0)] + (if (= (file-stat-raw path (addr n)) 1) (Some n) None))) + ;; ── Form: what a macro takes and what it answers ────────────────────── ;; ;; The reader's output, mirrored on the Flan side, because a macro is a diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index 2ca6ff4..db27a46 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -2749,3 +2749,245 @@ int8_t flan_slurp_into(flan_vec *v, const uint8_t *path, int64_t n) { v->len = got; return 1; } + +/* ── Time, and the environment ───────────────────────────────────────── + * + * Three clock primitives and one environment lookup, added as a block at the + * end so that the two halves of this file — the milestone-2 ABI above and the + * host services below — stay separable. is included here rather than + * at the top for the reason is: an include beside the only section + * that needs it says which section that is. + * + * The rule the whole file is written to applies hardest here: a primitive is + * the only thing implemented twice, so the seconds-valued faces of all three + * (monotonic-seconds, unix-seconds, sleep-seconds) are Flan in the prelude, + * over these. Odin draws the same line — core/time/time_linux.odin is exactly + * _now, _tick_now, _sleep and _yield over clock_gettime and nanosleep, and + * duration_seconds is derived arithmetic in core/time/time.odin. */ + +#include + +/* The two clocks are kept apart on purpose, because the mistake they invite is + * using one for the other's job. + * + * MONOTONIC never goes backwards and is not adjusted by NTP or by the user + * setting the clock, which is what makes it the one to *measure* with: a frame + * time taken across a daylight-saving change is still a frame time. It has no + * meaning as a date — its zero is arbitrary — so it can only ever be + * subtracted from another reading of itself. + * + * REALTIME is the date, and it is the one that jumps: it can move backwards, + * and a duration computed from two readings of it can be negative. It is here + * to answer "when", not "how long". */ + +/* The origin is the first read of this clock in the process, not boot, and + * that is a decision rather than an accident. + * + * The reason is the f64 face above it. CLOCK_MONOTONIC counts from boot, so on + * a machine up a hundred days the raw value is past 2^53 nanoseconds — beyond + * where an f64 holds consecutive integers — and (monotonic-seconds) would + * quietly lose sub-microsecond resolution depending on how long the *machine* + * had been running, which is the worst kind of bug to be handed. Latched to + * first read, the f64 stays integer-exact for a hundred days of *process* + * life, and nothing this language builds runs that long without a restart. + * + * It also matches what a game already expects: raylib's GetTime is seconds + * since InitWindow, not seconds since boot, and the two now mix without a + * caller having to notice one of them is a much larger number. + * + * A plain static and no atomics, because the language has no threads. If it + * ever gets them, the worst a race here can do is latch two origins a few + * nanoseconds apart, which costs a reading that is early by that much and + * cannot make the clock run backwards. */ +static int64_t flan_mono_origin; +static int flan_mono_armed; + +static int64_t flan_clock_ns(clockid_t which) { + struct timespec ts; + /* A failure here is not reachable with a constant clock id the platform + * has, and there is no channel to report it on that a caller could act on: + * the answer to "what time is it" cannot be a condition without every + * reading of it costing a handler search. A zeroed timespec is what a + * failure reads as, and for MONOTONIC that is the origin. */ + if (clock_gettime(which, &ts) != 0) { ts.tv_sec = 0; ts.tv_nsec = 0; } + return (int64_t)ts.tv_sec * 1000000000 + (int64_t)ts.tv_nsec; +} + +int64_t flan_monotonic_ns(void) { + int64_t now = flan_clock_ns(CLOCK_MONOTONIC); + if (!flan_mono_armed) { flan_mono_armed = 1; flan_mono_origin = now; } + return now - flan_mono_origin; +} + +int64_t flan_unix_ns(void) { return flan_clock_ns(CLOCK_REALTIME); } + +/* A negative or zero request returns at once rather than being refused: the + * caller that computed "sleep until the frame's deadline" and arrived late + * wants to carry on, not to be told it is late, and that is by far the most + * common way this is called. + * + * The EINTR loop is the reason this is C and not two Flan lines over a raw + * nanosleep: a signal — a profiler's timer, the dev loop's own — otherwise + * cuts the wait short and the caller's frame pacing wobbles for reasons + * nothing in the program explains. The remaining time comes back in the same + * timespec, so resuming is a second call with no arithmetic. Odin's _sleep in + * core/time/time_linux.odin loops on EINTR for the same reason. + * + * On emscripten this is still nanosleep, which there spins rather than yields: + * the sleep is the length asked for, and it burns a core and blocks the frame + * doing it. Correct, and not what a browser build should be reaching for — a + * web frame loop waits by returning to the browser, not by sleeping. */ +void flan_sleep_ns(int64_t ns) { + struct timespec ts; + if (ns <= 0) return; + ts.tv_sec = (time_t)(ns / 1000000000); + ts.tv_nsec = (long)(ns % 1000000000); + while (nanosleep(&ts, &ts) != 0 && errno == EINTR) { } +} + +/* getenv, with the absent case carried in the length rather than in the + * pointer, so that the Flan side never has to compare a pointer against null — + * a test the language does not offer, since a (Ptr T) only ever arrives from a + * declare and nothing in the type says it may be nothing. Absent is *len = -1 + * and a pointer to a valid empty string; present is *len >= 0 and the + * environment's own bytes, which (slice-from-ptr) then views. + * + * The bytes are the process environment's and are not copied. They outlive the + * call — nothing in this language can call setenv or spawn a process, so there + * is no writer — and they are not the caller's to free. The prelude's `getenv` + * says so where a caller will read it. + * + * A name with an embedded NUL reads as absent rather than as the shorter name + * before it, which is flan_path_cstr's rule and for its reason: the name + * looked up must be the name written. */ +const uint8_t *flan_getenv(const uint8_t *name, int64_t n, int64_t *len) { + static const char empty[1] = { 0 }; + char buf[FLAN_PATH_MAX]; + const char *v; + *len = -1; + if (!flan_path_cstr(name, n, buf)) return (const uint8_t *)empty; + v = getenv(buf); + if (!v) return (const uint8_t *)empty; + *len = (int64_t)strlen(v); + return (const uint8_t *)v; +} + +/* ── The rest of the file surface ────────────────────────────────────── + * + * Four more POSIX-shaped calls under the same rules as flan_file_size, + * flan_file_read and flan_file_write above: a path as ptr+len, 1 or 0, and the + * reason in flan_file_fail where the compiler's file_guard reads it. Nothing + * here holds a descriptor between calls, so a second target implements four + * functions and inherits the Flan that sits on them. + * + * The errno mapping is flan_errno_reason's and is not extended. Its three + * buckets — missing, denied, io — are what a *handler* can act on: retry after + * making the directory, use-value with another path, or give up. EEXIST and + * ENOTEMPTY land in io along with everything else, and that is the honest + * place for them until conditions have a hierarchy to hang a fourth reason + * off (see the FileError note in the prelude). */ + +#include +#include + +/* One call behind both file-exists? and file-size, because they are one + * question: stat answers whether the path resolves and how big it is in the + * same breath, and two entry points would be two chances for them to disagree. + * + * stat and not the fopen-plus-ftell that flan_file_size uses. That one is + * shaped by slurp's needs — it is about to read the file, so opening it is the + * test that matters — and it is wrong as a general size: fopen on a directory + * succeeds on Linux and ftell then answers a number that is not a file size. + * The two coexist deliberately and answer different questions. */ +int8_t flan_file_stat(const uint8_t *path, int64_t n, int64_t *size) { + char buf[FLAN_PATH_MAX]; + struct stat st; + *size = 0; + if (!flan_path_cstr(path, n, buf)) { + flan_file_fail = FLAN_FILE_MISSING; + return 0; + } + errno = 0; + if (stat(buf, &st) != 0) { flan_file_fail = flan_errno_reason(); return 0; } + *size = (int64_t)st.st_size; + flan_file_fail = FLAN_FILE_OK; + return 1; +} + +/* The three that change the filesystem, and they carry flan_file_write's + * decision 2 unchanged: on the web they signal, every time, with the path in + * the condition. Not a build-time refusal, because Flan has no conditional + * compilation and "isolate this to desktop" is therefore not expressible in + * source; and not a silent no-op, because that is how a save directory fails + * to appear with nothing said. */ + +int8_t flan_file_delete(const uint8_t *path, int64_t n) { +#if defined(__EMSCRIPTEN__) + (void)path; (void)n; + flan_file_fail = FLAN_FILE_UNSUPPORTED; + return 0; +#else + char buf[FLAN_PATH_MAX]; + if (!flan_path_cstr(path, n, buf)) { + flan_file_fail = FLAN_FILE_MISSING; + return 0; + } + errno = 0; + /* remove(), so that an empty directory is deletable by the same call a file + * is — it is unlink or rmdir depending on what the path names, which is the + * distinction a caller of a language with one `delete-file` does not want to + * have to make. A non-empty directory fails, and that is deliberate: + * recursive deletion is a loop the caller writes and sees. */ + if (remove(buf) != 0) { flan_file_fail = flan_errno_reason(); return 0; } + flan_file_fail = FLAN_FILE_OK; + return 1; +#endif +} + +/* Two paths, so two conversions, and the failure of either is reported as a + * missing path — the same answer flan_path_cstr's refusal gets everywhere + * else. rename() is atomic within one filesystem and fails with EXDEV across + * two rather than copying, which lands in the io bucket; a caller that wants + * a move across devices writes slurp and barf, and sees that it did. */ +int8_t flan_file_rename(const uint8_t *from, int64_t fn, const uint8_t *to, + int64_t tn) { +#if defined(__EMSCRIPTEN__) + (void)from; (void)fn; (void)to; (void)tn; + flan_file_fail = FLAN_FILE_UNSUPPORTED; + return 0; +#else + char a[FLAN_PATH_MAX], b[FLAN_PATH_MAX]; + if (!flan_path_cstr(from, fn, a) || !flan_path_cstr(to, tn, b)) { + flan_file_fail = FLAN_FILE_MISSING; + return 0; + } + errno = 0; + if (rename(a, b) != 0) { flan_file_fail = flan_errno_reason(); return 0; } + flan_file_fail = FLAN_FILE_OK; + return 1; +#endif +} + +/* 0777 and not 0755, because the process umask is what decides: a program that + * hardcodes 0755 has overridden a user's umask for no reason it could know. + * One level only — an intervening directory that does not exist is ENOENT, + * which reaches the caller as `missing` and is answerable by a handler that + * makes the parent and takes `retry`, which is the restart that path exists + * for. */ +int8_t flan_file_mkdir(const uint8_t *path, int64_t n) { +#if defined(__EMSCRIPTEN__) + (void)path; (void)n; + flan_file_fail = FLAN_FILE_UNSUPPORTED; + return 0; +#else + char buf[FLAN_PATH_MAX]; + if (!flan_path_cstr(path, n, buf)) { + flan_file_fail = FLAN_FILE_MISSING; + return 0; + } + errno = 0; + if (mkdir(buf, 0777) != 0) { flan_file_fail = flan_errno_reason(); return 0; } + flan_file_fail = FLAN_FILE_OK; + return 1; +#endif +} diff --git a/test/programs/files.flan b/test/programs/files.flan new file mode 100644 index 0000000..adf8178 --- /dev/null +++ b/test/programs/files.flan @@ -0,0 +1,107 @@ +;;;; The file surface beyond slurp and barf: file-exists?, file-size, +;;;; delete-file, rename-file and make-directory. +;;;; +;;;; The split down the middle of that list is the whole design and this file +;;;; is arranged to show it. The two that ask a *question* — is it there, how +;;;; big is it — answer a value, because absence is a reply and not a fault; +;;;; they are prelude functions over one declare and the compiler knows +;;;; nothing about them. The three that *change* the filesystem answer () and +;;;; signal FileError with the two restarts slurp and barf already establish, +;;;; because each of their failures is one a handler can act on: make the +;;;; parent directory and retry, or supply another path. +;;;; +;;;; Everything is made and removed inside this program, so it leaves the +;;;; directory as it found it — checked at the end rather than assumed. + +;; Handlers cannot see the locals of the function that established them, so the +;; observations are globals, as in slurp.flan. +(defvar seen i64) +(defvar last-reason i32) +(defvar last-op i32) + +(defn main [] i32 + ;; ── The questions ───────────────────────────────────────────────── + (println (file-exists? "programs/assets/a.txt")) ; true + (println (file-exists? "programs/assets/nope")) ; false + ;; A directory resolves, which is what the name asks and not "is a regular + ;; file" — a caller wanting the narrower question is asking a second one. + (println (file-exists? "programs/assets")) ; true + + (match (file-size "programs/assets/a.txt") + (Some n) (println n) ; 13 + None (println "missing")) + ;; None folds every reason into one answer, which is the trade a question + ;; with no restart on it makes. + (match (file-size "programs/assets/nope") + (Some n) (println n) + None (println "none")) + + ;; ── make-directory, rename-file, delete-file ────────────────────── + (make-directory "files-tmp") + (println (file-exists? "files-tmp")) ; true + + (barf "files-tmp/one.txt" (bytes "0123456789")) + (match (file-size "files-tmp/one.txt") + (Some n) (println n) ; 10 + None (println "missing")) + + (rename-file "files-tmp/one.txt" "files-tmp/two.txt") + (println (file-exists? "files-tmp/one.txt")) ; false + (println (file-exists? "files-tmp/two.txt")) ; true + + (delete-file "files-tmp/two.txt") + (println (file-exists? "files-tmp/two.txt")) ; false + + ;; ── retry, after the handler made the parent ────────────────────── + ;; The restart this family exists for. Writing into a directory that is not + ;; there is ENOENT, which arrives as `missing`; the handler makes the + ;; directory and takes `retry`, and the second attempt succeeds. Nothing in + ;; the failing code knows any of that happened. + (handler-bind + [(FileError [c] + (set seen (+ seen 1)) + (set last-reason (.reason c)) + (set last-op (.op c)) + (make-directory "files-tmp/sub") + (invoke-restart 'retry))] + (barf "files-tmp/sub/deep.txt" (bytes "deep"))) + (println seen) ; 1 + (println (= last-reason file-missing)) ; true + (println (= last-op file-op-write)) ; true + (println (file-exists? "files-tmp/sub/deep.txt")) ; true + + ;; ── use-value, on a delete ──────────────────────────────────────── + ;; The same restart slurp's read offers, on an operation that writes: the + ;; handler names a path that is there and the delete resumes against it. + (set seen 0) + (handler-bind + [(FileError [c] + (set seen (+ seen 1)) + (set last-op (.op c)) + (invoke-restart 'use-value "files-tmp/sub/deep.txt"))] + (delete-file "files-tmp/sub/not-there.txt")) + (println seen) ; 1 + (println (= last-op file-op-delete)) ; true + (println (file-exists? "files-tmp/sub/deep.txt")) ; false + + ;; ── A non-empty directory does not delete ───────────────────────── + ;; remove() is unlink or rmdir depending on what the path names, so an empty + ;; directory goes by the same call a file does — and a full one does not, + ;; which is deliberate: a recursive delete is a loop the caller writes and + ;; sees. Here the handler declines to answer, which is what an unhandled + ;; condition would do, so it counts and lets the program carry on by + ;; supplying the child path instead. + (set seen 0) + (handler-bind + [(FileError [c] + (set seen (+ seen 1)) + (set last-op (.op c)) + (invoke-restart 'use-value "files-tmp/sub"))] + (delete-file "files-tmp")) + (println seen) ; 1 + (println (= last-op file-op-delete)) ; true + + ;; And now it is empty, so it goes. + (delete-file "files-tmp") + (println (file-exists? "files-tmp")) ; false + 0) diff --git a/test/programs/math3.flan b/test/programs/math3.flan new file mode 100644 index 0000000..7f8a381 --- /dev/null +++ b/test/programs/math3.flan @@ -0,0 +1,92 @@ +;;;; The rest of libm, at both widths — what math.flan and math2.flan left out. +;;;; +;;;; The rule those two set holds here unchanged and is the only reason this +;;;; file looks the way it does: none of these is correctly rounded under +;;;; IEEE-754 except sqrt, fabs, the rounding three and fmod, so every value +;;;; below is one whose answer is exact in binary — zero, one, a power of two, +;;;; a perfect square, a perfect cube. A case that pinned glibc's last bit +;;;; would pass here and fail on wasi-libc. +;;;; +;;;; The acceptance table builds this at -O0 as well, and that run is the one +;;;; that matters: at -O2 LLVM folds a libm call over two literals and leaves +;;;; no symbol to resolve, which is how a missing -lm hid the first time. + +(defn show [x f32] () + (print x) + (print " ")) + +(defn show64 [x f64] () + (print x) + (print " ")) + +(defn main [] i32 + ;; The f32 half. tan, the three inverses, the three logarithms and exp. + (show (tan-f32 0.0)) ; 0 + (show (asin-f32 0.0)) ; 0 + (show (acos-f32 1.0)) ; 0 + (show (atan-f32 0.0)) ; 0 + (show (log-f32 1.0)) ; 0 + (show (log2-f32 8.0)) ; 3 + (show (log10-f32 1000.0)) ; 3 + (show (exp-f32 0.0)) ; 1 + (println "") + + (show (fmod-f32 7.0 4.0)) ; 3 + ;; The sign follows the dividend and not the divisor, which is the line a + ;; caller reaching for a modulo gets wrong. Written down as a case. + (show (fmod-f32 -1.0 3.0)) ; -1 + (show (hypot-f32 3.0 4.0)) ; 5 + (show (cbrt-f32 27.0)) ; 3 + ;; The negative root, where (pow-f32 x 0.33333334) would be NaN: pow goes + ;; through a logarithm and cbrt does not. + (show (cbrt-f32 -8.0)) ; -2 + (show (abs-f32 -2.5)) ; 2.5 + (println "") + + ;; The f64 half, at the same exact values. This block is the whole point of + ;; the f64 face existing: before it, every one of these was a cast down to + ;; f32 and back, and the cast down is where the precision went. + (show64 (sqrt-f64 16.0)) ; 4 + (show64 (sin-f64 0.0)) ; 0 + (show64 (cos-f64 0.0)) ; 1 + (show64 (tan-f64 0.0)) ; 0 + (show64 (asin-f64 0.0)) ; 0 + (show64 (acos-f64 1.0)) ; 0 + (show64 (atan-f64 0.0)) ; 0 + (show64 (atan2-f64 0.0 1.0)) ; 0 + (println "") + + (show64 (log-f64 1.0)) ; 0 + (show64 (log2-f64 1024.0)) ; 10 + (show64 (log10-f64 100.0)) ; 2 + (show64 (exp-f64 0.0)) ; 1 + (show64 (pow-f64 2.0 10.0)) ; 1024 + (show64 (fmod-f64 7.0 4.0)) ; 3 + (show64 (hypot-f64 3.0 4.0)) ; 5 + (show64 (cbrt-f64 8.0)) ; 2 + (show64 (abs-f64 -1.5)) ; 1.5 + (println "") + + ;; The f64 rounding family, which is libm's where the f32 one is Flan's — + ;; and it agrees with the Flan one where they overlap: half away from zero, + ;; so -2.5 goes to -3 and not to -2. + (show64 (floor-f64 -2.5)) ; -3 + (show64 (ceil-f64 -2.5)) ; -2 + (show64 (round-f64 -2.5)) ; -3 + (show64 (round-f64 2.5)) ; 3 + (println "") + + ;; Integer magnitude, one per width. + (print (abs-i32 -7)) (print " ") ; 7 + (print (abs-i64 (i64 -7))) (print " ") ; 7 + (print (abs-i32 7)) (print " ") ; 7 + ;; tau is 2pi at both widths. Pinning the relation rather than the digits is + ;; what catches a constant written to too few of them. + (print (= tau-f32 (* 2.0 pi-f32))) (print " ") + (print (= tau-f64 (* 2.0 pi-f64))) + (println "") + + ;; pi is the one value here that can be pinned without pinning a libm: it is + ;; a literal the compiler rounds, so it is the same on every target. + (println (and (> pi-f64 3.14159265) (< pi-f64 3.14159266))) + 0) diff --git a/test/programs/time.flan b/test/programs/time.flan new file mode 100644 index 0000000..6836c24 --- /dev/null +++ b/test/programs/time.flan @@ -0,0 +1,71 @@ +;;;; The clock and the environment. +;;;; +;;;; Every line of output here is an invariant and not a reading, and that is +;;;; forced rather than chosen: this file is in the corpus @x86 builds twice +;;;; and diffs, and the acceptance table matches its stdout exactly, so a +;;;; timestamp or an elapsed count would fail a correct compiler on the second +;;;; run. What is left is what a clock actually has to promise — that it does +;;;; not go backwards, that a sleep does not return early, that the two faces +;;;; of one clock describe one instant — and those are the properties worth +;;;; pinning anyway. A test that asserted "this took under 3ms" would be a +;;;; test of the machine's load. + +(defn main [] i32 + ;; Monotonic, twice. The whole contract in one line: it never goes + ;; backwards. Equal is allowed and is not a bug — two reads inside one tick + ;; of a coarse timer are the same nanosecond. + (let [t1 (monotonic-ns) + t2 (monotonic-ns)] + (println (>= t2 t1))) + + ;; And the origin is the first read rather than boot, so the first readings + ;; a program takes are small. Bounded rather than pinned: the number is + ;; whatever this process spent between the calls above and this one, which + ;; is not a second on any machine that can run the suite at all. + (println (< (monotonic-ns) ns-per-second)) + + ;; The f64 face is the i64 one divided, and what is checked is that the two + ;; describe the same instant: a later reading in seconds is at or past an + ;; earlier reading in nanoseconds converted the same way. A clock whose two + ;; faces came from different sources fails this. + (let [a (/ (f64 (monotonic-ns)) 1000000000.0) + b (monotonic-seconds)] + (println (>= b a))) + + ;; The wall clock is a date, so the invariant is a date one: it is after + ;; 2020 and before 2100. That pins the epoch and the unit at once — a clock + ;; counting microseconds, or counting from boot, fails both halves. + (let [now (unix-seconds)] + (println (and (> now 1577836800.0) (< now 4102444800.0)))) + + ;; Sleep is specified as *at least*, so at-least is what is asserted; the + ;; upper bound belongs to the scheduler and not to this language. Two + ;; milliseconds because the shortest sleep a default kernel actually + ;; performs is a timer tick, and a shorter request would make this a test of + ;; how that kernel was configured. + (let [before (monotonic-ns)] + (sleep-ns (* 2 ns-per-millisecond)) + (println (>= (- (monotonic-ns) before) (* 2 ns-per-millisecond)))) + + ;; Zero and negative return at once rather than being refused, which is what + ;; a deadline already passed produces. That they return at all is the + ;; assertion; nothing here is timed. + (sleep-ns 0) + (sleep-ns -1) + (sleep-seconds 0.0) + (println "slept") + + ;; ── The environment ────────────────────────────────────────────── + + ;; A variable nothing sets. None is the answer, and it is a different answer + ;; from a variable set to nothing. + (match (getenv "FLAN_NO_SUCH_VARIABLE_AT_ALL") + (Some v) (println "unexpectedly set") + None (println "unset")) + + ;; PATH is set for every process that gets as far as running this, and the + ;; only portable thing about its contents is that there are some. + (match (getenv "PATH") + (Some v) (println (> (len v) 0)) + None (println "no PATH")) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index a99ab31..3b51865 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -268,6 +268,32 @@ let () = outputs "atan2, pow and clamp" "programs/math2.flan" math2_out; outputs ~opt:"-O0" "atan2, pow and clamp, -O0" "programs/math2.flan" math2_out; + (* The rest of libm, at both widths. Same rule as math2 above and for the + same reason — every value is exact in binary — and the -O0 pass is + doing the same job: at -O2 LLVM folds a libm call over two literals and + leaves no symbol to resolve, so that run is the one proving all thirty + new declares actually link. *) + let math3_out = + "0 0 0 0 0 3 3 1 \n\ + 3 -1 5 3 -2 2.5 \n\ + 4 0 1 0 0 0 0 0 \n\ + 0 10 2 1 1024 3 5 2 1.5 \n\ + -3 -2 -3 3 \n\ + 7 7 7 true true\n\ + true\n" + in + outputs "the rest of libm, both widths" "programs/math3.flan" math3_out; + outputs ~opt:"-O0" "the rest of libm, both widths, -O0" + "programs/math3.flan" math3_out; + (* The clock and the environment. Every line of that program's output is + an invariant — a monotonicity, a date range, a sleep that did not + return early — and not a reading, because the same file is in the + corpus @x86 builds twice and diffs, so a timestamp would fail a correct + compiler on its second run. *) + let time_out = "true\ntrue\ntrue\ntrue\ntrue\nslept\nunset\ntrue\n" in + outputs "the clock and getenv" "programs/time.flan" time_out; + outputs ~opt:"-O0" "the clock and getenv, -O0" "programs/time.flan" + time_out; (* index-of-bytes, trim, the byte classes and parse-f64. The search cases are the ones that separate a correct loop from a lucky one: a match only at the end, "aab" in "aaab" (where the first byte matches twice @@ -824,6 +850,37 @@ let () = end; (try Sys.remove exe with Sys_error _ -> ()); + (* The rest of the file surface. What is being checked as much as the + calls is the line drawn through them: file-exists? and file-size answer + a value because absence is a reply and not a fault, and the three that + change the filesystem signal FileError with the same two restarts slurp + and barf establish. Both restarts are taken here on operations that + write - retry after the handler made the parent directory, and + use-value on a delete - which is what the pair is for and what a bool + return could not have offered. + + The program makes and removes its own tree, so the cleanup below is for + a run that failed part way through and not for a passing one. *) + let clean_dir () = + List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) + [ "files-tmp/sub/deep.txt"; "files-tmp/one.txt"; "files-tmp/two.txt" ]; + List.iter (fun d -> try Unix.rmdir d with Unix.Unix_error _ -> ()) + [ "files-tmp/sub"; "files-tmp" ] + in + let files_out = + "true\nfalse\ntrue\n13\nnone\ntrue\n10\nfalse\ntrue\nfalse\n\ + 1\ntrue\ntrue\ntrue\n1\ntrue\nfalse\n1\ntrue\nfalse\n" + in + clean_dir (); + outputs "the rest of the file surface" "programs/files.flan" files_out; + clean_dir (); + outputs ~opt:"-O0" "the rest of the file surface, -O0" "programs/files.flan" + files_out; + clean_dir (); + outputs ~dev:true "the rest of the file surface, dev" "programs/files.flan" + files_out; + clean_dir (); + (* The epoch trap: a container whose allocator has been released. This is spec-memory.md's shipping answer to "Open: catching a use-after-release statically" — detection, loud and immediate, rather than a static rule diff --git a/test/test_sanitize.ml b/test/test_sanitize.ml index 12090cd..971d768 100644 --- a/test/test_sanitize.ml +++ b/test/test_sanitize.ml @@ -128,9 +128,14 @@ let corpus = "programs/edn.flan", []; "programs/enum-compare.flan", []; "programs/error.flan", []; + (* Makes and removes its own tree, so the two runs of the sweep see the + same directory; the new C here is three more path buffers, which is + exactly what this tool is for. *) + "programs/files.flan", []; "programs/handles.flan", []; "programs/machine.flan", []; "programs/math.flan", []; + "programs/math3.flan", []; "programs/pkg-diamond.flan", []; "programs/pkg-return.flan", []; "programs/pkg-shared.flan", []; @@ -143,6 +148,10 @@ let corpus = "programs/slices.flan", []; "programs/string-of-bytes.flan", []; "programs/text.flan", []; + (* The clock and getenv. getenv hands back a slice viewing the process + environment and never a copy, so a report here would be the one that + matters. *) + "programs/time.flan", []; "programs/unit-main.flan", []; "programs/utf8.flan", []; "programs/values.flan", []; diff --git a/test/test_valgrind.ml b/test/test_valgrind.ml index e58a06e..3577948 100644 --- a/test/test_valgrind.ml +++ b/test/test_valgrind.ml @@ -215,6 +215,7 @@ let corpus = "programs/error.flan", []; "programs/exhausted.flan", []; "programs/exhausted-unhandled.flan", []; + "programs/files.flan", []; "programs/free-all-refused.flan", []; "programs/handles.flan", []; "programs/machine.flan", []; @@ -222,6 +223,7 @@ let corpus = "programs/map-stale-region.flan", []; "programs/maps.flan", []; "programs/math.flan", []; + "programs/math3.flan", []; "programs/pool-stale-region.flan", []; "programs/pkg-macro.flan", []; "programs/pkg-diamond.flan", []; @@ -241,6 +243,7 @@ let corpus = "programs/stale-region.flan", []; "programs/string-of-bytes.flan", []; "programs/text.flan", []; + "programs/time.flan", []; "programs/datas.flan", []; "programs/unit-main.flan", []; "programs/utf8.flan", []; diff --git a/web/examples/ffi.flan b/web/examples/ffi.flan index d34ca00..6429f95 100644 --- a/web/examples/ffi.flan +++ b/web/examples/ffi.flan @@ -1,6 +1,11 @@ ;; A plain `declare` names a C symbol in a signature Flan can already spell: ;; no aggregate crosses, so no wrapper is generated. -(declare cos-f64 [x f64] f64 "cos") +;; +;; cosh and not cos, because the prelude already declares cos-f64 and a second +;; declaration of a name is refused with both sites named. That refusal is the +;; useful half of this example: the prelude is where the common libm calls +;; live, and a `declare` is how you reach one it does not name. +(declare cosh-f64 [x f64] f64 "cosh") (defn main [] () - (print (cos-f64 0.0)) (println "")) + (print (cosh-f64 0.0)) (println "")) diff --git a/web/index.html b/web/index.html index a4ab6ec..8860532 100644 --- a/web/index.html +++ b/web/index.html @@ -1047,7 +1047,7 @@ takes the value as it is and prints the number it holds.

The prelude

-

The prelude is written in Flan, all but five lines of it, and prepended to every +

The prelude is written in Flan, all but its declare lines, and prepended to every program, so nothing in it needs importing. It holds no printing of its own: print and println are the compiler's, and write-stdout — the one output primitive — is what they are written @@ -1063,7 +1063,10 @@ over.

textsplit-on-byte, split-next!, split, lower-ascii, upper-ascii, to-lower, to-upper building bytesappend!, append-i64!, append-f64!, concat, join, repeat-bytes, replace-bytes, slices-new, format-f64 UTF-8decode-rune, rune-at, rune-count, rune-size, rune-start?, valid-utf8?, encode-rune! -numberssign-f32, lerp, clamp, floor-f32, ceil-f32, round-f32, and the five declares: sqrt-f32, sin-f32, cos-f32, atan2-f32, pow-f32 +numberssign-f32, lerp, clamp, floor-f32, ceil-f32, round-f32, abs-i32, abs-i64, the constants pi-f32, pi-f64, tau-f32, tau-f64, and libm through a declare at both widths: sqrt, abs, floor, ceil, round, fmod, sin, cos, tan, asin, acos, atan, atan2, log, log2, log10, exp, pow, hypot, cbrt — each spelled -f32 or -f64 +timemonotonic-ns, monotonic-seconds, unix-ns, unix-seconds, sleep-ns, sleep-seconds, and ns-per-second and its two smaller siblings +filesfile-exists? and file-size, which answer a value; slurp, barf, delete-file, rename-file and make-directory, which signal FileError under retry and use-value +the operating systemgetenv, which answers an (Option [u8]) viewing the process environment randomrand-seed, rand-u32, rand-f32, rand-i32-range, rand-f32-range forms, for macrosform-nil, form-cons, form-append, form-rest, form-items, form-pair, form-sym?, form-is-sym?, gensym, and unless and into, which are macros written here rather than special forms the restpause, which signals the Pause condition the break loop stops on, and embed-find @@ -1084,18 +1087,47 @@ native and on wasm32. The parsers are ours too: "abc" and 12 for "12x", which are three wrong answers a caller cannot tell from a real 12.

-

Five functions in the file are not Flan, and they are libm's: -(declare sqrt-f32 [x f32] f32 "sqrtf") and the same line for -sinf, cosf, atan2f and powf. Every -other number here is reachable from the four operations and a cast; a square root is -not, and the usual trick of seeding Newton's method from the exponent bits needs a -bit-cast between f32 and u32 that the language does not have. -IEEE-754 makes sqrt correctly rounded, so libm gives the same bit pattern -on both targets anyway. The other four are not: IEEE-754 requires -nothing of sinf, cosf, atan2f or -powf, and glibc, musl and wasi-libc do differ in the last bit — so the -byte-identical-hash property the RNG exists for does not survive a hash routed through -any of them. Every link carries -lm.

+

The maths in the file is not Flan, and it is libm's: +(declare sqrt-f32 [x f32] f32 "sqrtf") and the same line for thirty-odd +more. Every other number here is reachable from the four operations and a cast; a square +root is not, and the usual trick of seeding Newton's method from the exponent bits needs +a bit-cast between f32 and u32 that the language does not have. +A declare is also the cheapest thing in the language to add — a line, a +symbol already on the link, and nothing in either backend — which is why the surface is +now the whole family at both widths rather than the five it started as.

+ +

One split is worth knowing before calling any of them. IEEE-754 +specifies sqrt, fabs, floor, ceil, +round and fmod as exact or correctly rounded, so those give the +same bit pattern under glibc, musl and wasi-libc. It requires nothing of the +restsin, cos, tan, the inverses, the +logarithms, exp, pow, hypot, cbrt — +and the three libms do differ in the last bit, so the byte-identical-hash property the +RNG exists for does not survive a value routed through any of them. Every link carries +-lm.

+ +

The clock is two clocks and they are not interchangeable. +monotonic-ns measures: it never goes backwards, nothing adjusts it, and its +zero is the first time the program reads it, so it is meaningless alone and correct as a +difference. unix-ns dates: nanoseconds since 1970, which is what goes in a +save file or a log line, and which can jump in either direction when the system clock is +set. Odin draws the same line — Tick against Time in +core/time — and the nanosecond integer is the primitive on both sides, with +the -seconds faces derived from it. The f64 of seconds is the +shape raylib's get-time already answers with, so the two mix; it stays +integer-exact in nanoseconds for a hundred days of process life, which is why the +monotonic origin is the first read and not boot.

+ +

The file surface is split by whether a handler could do anything. +file-exists? and file-size answer a bool and an +(Option i64): absence is the reply, not a fault, and a condition would make +the ordinary case pay for a handler search. slurp, barf, +delete-file, rename-file and make-directory signal +FileError instead, under the two restarts Common Lisp establishes for a +file error — retry, because the handler may have just made the directory, +and use-value with another path. Nothing here returns an error code, which +is the same rule allocation follows. Streaming, stdin and directory listings are not +here; a whole file at a time is the surface.

The primitives underneath are few — a primitive is the only thing implemented twice per backend: argv, @@ -1386,14 +1418,19 @@ structural rule could tell them apart.

declare names a C symbol in a signature Flan can already spell. Nothing is generated; a Flan string crosses as ptr+len, exactly as it is stored.

-
(declare cos-f64 [x f64] f64 "cos")
+
(declare cosh-f64 [x f64] f64 "cosh")
 
 (defn main [] ()
-  (print (cos-f64 0.0)) (println ""))
+ (print (cosh-f64 0.0)) (println ""))
1
-

That is 1.0, printed by the same rule as before.

+

That is 1.0, printed by the same rule as before. It is cosh and not +cos because the prelude already declares +cos-f64, and a second declaration of a name is refused with both sites +named — which is the other half of what this example shows. The prelude is where the +common libm calls live; a declare is how you reach one it does not +name.

declare-c names the C library's own function in the C library's own signature, and the compiler writes the wrapper. This is what raylib's package is made