The trio the author decided on 2026-09-20 is now all built: def is CL's defparameter — its initialiser runs on every daemon re-run, unguarded, so an edited initialiser repaints the same storage on C-c C-c plus re-run — defonce (Clojure's name for CL's defvar, per the author) initialises once behind the .init~once. flag, and defconst stays the image. One parse arm reads both forms; the difference is Ast.reinit, carried to Tast.global's grerun. Emit.startup_plan gives a def no guard flag, and Check.check_global lifts every def initialiser — zero and literal included — into global/<n>, so the host's startup reaches it through the function cell and a re-evaluated def swaps it (Session's def_inits; Emit.redefinition declares the cell for a non-sibling target). The old defvar spelling is refused with the rename and both compiling spellings, and every program, test, doc and editor list is swept — except sand.flan, the author's live WIP, whose seven defvar lines are flagged in FIX.org and keep its three dependent tests red on this branch.
108 lines
5.1 KiB
Plaintext
108 lines
5.1 KiB
Plaintext
;;;; 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.
|
|
(defonce seen i64)
|
|
(defonce last-reason i32)
|
|
(defonce 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-view "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-view "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)
|