A clock, the rest of libm, getenv, and the small file verbs
This commit is contained in:
commit
aaca776afd
64
lib/check.ml
64
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
|
||||
|
||||
@ -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)
|
||||
|}
|
||||
|
||||
293
lib/prelude.ml
293
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
|
||||
|
||||
@ -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. <time.h> is included here rather than
|
||||
* at the top for the reason <errno.h> 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 <time.h>
|
||||
|
||||
/* 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 <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* 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
|
||||
}
|
||||
|
||||
107
test/programs/files.flan
Normal file
107
test/programs/files.flan
Normal file
@ -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)
|
||||
92
test/programs/math3.flan
Normal file
92
test/programs/math3.flan
Normal file
@ -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)
|
||||
71
test/programs/time.flan
Normal file
71
test/programs/time.flan
Normal file
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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", [];
|
||||
|
||||
@ -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", [];
|
||||
|
||||
@ -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 ""))
|
||||
|
||||
@ -1047,7 +1047,7 @@ takes the value as it is and prints the number it holds.</p>
|
||||
|
||||
<h2 id="prelude">The prelude</h2>
|
||||
|
||||
<p>The prelude is written in Flan, all but five lines of it, and prepended to every
|
||||
<p>The prelude is written in Flan, all but its <code>declare</code> lines, and prepended to every
|
||||
program, so nothing in it needs importing. It holds no printing of its own:
|
||||
<code>print</code> and <code>println</code> are the compiler's, and
|
||||
<code>write-stdout</code> — the one output primitive — is what they are written
|
||||
@ -1063,7 +1063,10 @@ over.</p>
|
||||
<tr><td>text</td><td><code>split-on-byte</code>, <code>split-next!</code>, <code>split</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>to-lower</code>, <code>to-upper</code></td></tr>
|
||||
<tr><td>building bytes</td><td><code>append!</code>, <code>append-i64!</code>, <code>append-f64!</code>, <code>concat</code>, <code>join</code>, <code>repeat-bytes</code>, <code>replace-bytes</code>, <code>slices-new</code>, <code>format-f64</code></td></tr>
|
||||
<tr><td>UTF-8</td><td><code>decode-rune</code>, <code>rune-at</code>, <code>rune-count</code>, <code>rune-size</code>, <code>rune-start?</code>, <code>valid-utf8?</code>, <code>encode-rune!</code></td></tr>
|
||||
<tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>clamp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, and the five <code>declare</code>s: <code>sqrt-f32</code>, <code>sin-f32</code>, <code>cos-f32</code>, <code>atan2-f32</code>, <code>pow-f32</code></td></tr>
|
||||
<tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>clamp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, <code>abs-i32</code>, <code>abs-i64</code>, the constants <code>pi-f32</code>, <code>pi-f64</code>, <code>tau-f32</code>, <code>tau-f64</code>, and libm through a <code>declare</code> at both widths: <code>sqrt</code>, <code>abs</code>, <code>floor</code>, <code>ceil</code>, <code>round</code>, <code>fmod</code>, <code>sin</code>, <code>cos</code>, <code>tan</code>, <code>asin</code>, <code>acos</code>, <code>atan</code>, <code>atan2</code>, <code>log</code>, <code>log2</code>, <code>log10</code>, <code>exp</code>, <code>pow</code>, <code>hypot</code>, <code>cbrt</code> — each spelled <code>-f32</code> or <code>-f64</code></td></tr>
|
||||
<tr><td>time</td><td><code>monotonic-ns</code>, <code>monotonic-seconds</code>, <code>unix-ns</code>, <code>unix-seconds</code>, <code>sleep-ns</code>, <code>sleep-seconds</code>, and <code>ns-per-second</code> and its two smaller siblings</td></tr>
|
||||
<tr><td>files</td><td><code>file-exists?</code> and <code>file-size</code>, which answer a value; <code>slurp</code>, <code>barf</code>, <code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code>, which signal <code>FileError</code> under <code>retry</code> and <code>use-value</code></td></tr>
|
||||
<tr><td>the operating system</td><td><code>getenv</code>, which answers an <code>(Option [u8])</code> viewing the process environment</td></tr>
|
||||
<tr><td>random</td><td><code>rand-seed</code>, <code>rand-u32</code>, <code>rand-f32</code>, <code>rand-i32-range</code>, <code>rand-f32-range</code></td></tr>
|
||||
<tr><td>forms, for macros</td><td><code>form-nil</code>, <code>form-cons</code>, <code>form-append</code>, <code>form-rest</code>, <code>form-items</code>, <code>form-pair</code>, <code>form-sym?</code>, <code>form-is-sym?</code>, <code>gensym</code>, and <code>unless</code> and <code>into</code>, which are macros written here rather than special forms</td></tr>
|
||||
<tr><td>the rest</td><td><code>pause</code>, which signals the <code>Pause</code> condition the break loop stops on, and <code>embed-find</code></td></tr>
|
||||
@ -1084,18 +1087,47 @@ native and on wasm32. <strong>The parsers are ours too</strong>:
|
||||
<code>"abc"</code> and 12 for <code>"12x"</code>, which are three wrong answers a caller
|
||||
cannot tell from a real 12.</p>
|
||||
|
||||
<p>Five functions in the file are not Flan, and they are libm's:
|
||||
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code> and the same line for
|
||||
<code>sinf</code>, <code>cosf</code>, <code>atan2f</code> and <code>powf</code>. 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 <code>f32</code> and <code>u32</code> that the language does not have.
|
||||
IEEE-754 makes <code>sqrt</code> correctly rounded, so libm gives the same bit pattern
|
||||
on both targets anyway. <strong>The other four are not</strong>: IEEE-754 requires
|
||||
nothing of <code>sinf</code>, <code>cosf</code>, <code>atan2f</code> or
|
||||
<code>powf</code>, 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 <code>-lm</code>.</p>
|
||||
<p>The maths in the file is not Flan, and it is libm's:
|
||||
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code> 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 <code>f32</code> and <code>u32</code> that the language does not have.
|
||||
A <code>declare</code> 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.</p>
|
||||
|
||||
<p><strong>One split is worth knowing before calling any of them.</strong> IEEE-754
|
||||
specifies <code>sqrt</code>, <code>fabs</code>, <code>floor</code>, <code>ceil</code>,
|
||||
<code>round</code> and <code>fmod</code> as exact or correctly rounded, so those give the
|
||||
same bit pattern under glibc, musl and wasi-libc. <strong>It requires nothing of the
|
||||
rest</strong> — <code>sin</code>, <code>cos</code>, <code>tan</code>, the inverses, the
|
||||
logarithms, <code>exp</code>, <code>pow</code>, <code>hypot</code>, <code>cbrt</code> —
|
||||
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
|
||||
<code>-lm</code>.</p>
|
||||
|
||||
<p><strong>The clock is two clocks and they are not interchangeable.</strong>
|
||||
<code>monotonic-ns</code> 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. <code>unix-ns</code> 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 — <code>Tick</code> against <code>Time</code> in
|
||||
<code>core/time</code> — and the nanosecond integer is the primitive on both sides, with
|
||||
the <code>-seconds</code> faces derived from it. The <code>f64</code> of seconds is the
|
||||
shape raylib's <code>get-time</code> 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.</p>
|
||||
|
||||
<p><strong>The file surface is split by whether a handler could do anything.</strong>
|
||||
<code>file-exists?</code> and <code>file-size</code> answer a <code>bool</code> and an
|
||||
<code>(Option i64)</code>: absence is the reply, not a fault, and a condition would make
|
||||
the ordinary case pay for a handler search. <code>slurp</code>, <code>barf</code>,
|
||||
<code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code> signal
|
||||
<code>FileError</code> instead, under the two restarts Common Lisp establishes for a
|
||||
file error — <code>retry</code>, because the handler may have just made the directory,
|
||||
and <code>use-value</code> 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.</p>
|
||||
|
||||
<p>The primitives underneath are few — a primitive is the only thing implemented
|
||||
twice per backend: <code>argv</code>,
|
||||
@ -1386,14 +1418,19 @@ structural rule could tell them apart.</p>
|
||||
<p><code>declare</code> 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.</p>
|
||||
|
||||
<pre><code>(declare cos-f64 [x f64] f64 "cos")
|
||||
<pre><code>(declare cosh-f64 [x f64] f64 "cosh")
|
||||
|
||||
(defn main [] ()
|
||||
(print (cos-f64 0.0)) (println ""))</code></pre>
|
||||
(print (cosh-f64 0.0)) (println ""))</code></pre>
|
||||
|
||||
<pre><code class="sh">1</code></pre>
|
||||
|
||||
<p>That is 1.0, printed by the same rule as before.</p>
|
||||
<p>That is 1.0, printed by the same rule as before. It is <code>cosh</code> and not
|
||||
<code>cos</code> because <a href="#prelude">the prelude</a> already declares
|
||||
<code>cos-f64</code>, 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 <code>declare</code> is how you reach one it does not
|
||||
name.</p>
|
||||
|
||||
<p><code>declare-c</code> 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user