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 ff16762..30d6435 100644
--- a/lib/prelude.ml
+++ b/lib/prelude.ml
@@ -1691,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)
@@ -1699,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 33c0424..db27a46 100644
--- a/runtime/flan_rt.c
+++ b/runtime/flan_rt.c
@@ -2871,3 +2871,123 @@ const uint8_t *flan_getenv(const uint8_t *name, int64_t n, int64_t *len) {
*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
decode-rune, rune-at, rune-count, rune-size, rune-start?, valid-utf8?, encode-rune!sign-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 -f64monotonic-ns, monotonic-seconds, unix-ns, unix-seconds, sleep-ns, sleep-seconds, and ns-per-second and its two smaller siblingsfile-exists? and file-size, which answer a value; slurp, barf, delete-file, rename-file and make-directory, which signal FileError under retry and use-valuegetenv, which answers an (Option [u8]) viewing the process environmentrand-seed, rand-u32, rand-f32, rand-i32-range, rand-f32-rangeform-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 formsget-time already answers with, so the two mix; it st
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,
write-stdout, exit, len, at,