Five more: file-exists?, file-size, delete-file, rename-file and make-directory. The interesting thing is not the list, it is the line drawn through it. file-exists? and file-size answer a value -- a bool and an (Option i64) -- and are prelude functions over one declare that the compiler knows nothing about. Absence is the reply to those two questions and not a fault, so a condition would make the ordinary case pay for a handler search, and there is no restart a handler could take that would turn "it is not there" into a different answer. delete-file, rename-file and make-directory answer () and signal FileError, and they are check.ml builtins for the one thing a declare cannot do: they go through file_guard, so each failure arrives under retry and use-value. Those are restarts a handler really can take -- make the parent directory and retry, or supply another path -- which is exactly the case a bool return throws away. op continues the prelude's numbering as 2, 3 and 4. One C function behind the two questions rather than two, because they are one question: stat answers whether the path resolves and how big it is in the same breath. It is stat and not flan_file_size's fopen-plus-ftell, which is shaped by slurp being about to read the file and 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 and answer different questions. rename holds the source in the guard's path slot, so a use-value renames a different file to the same destination. Both readings are plausible until somebody says which, so check.ml says which. The errno mapping is not extended. Its three buckets are what a handler can act on; EEXIST and ENOTEMPTY land in io with everything else, and that is honest until conditions have a hierarchy to hang a fourth reason off. All three carry barf's decision 2 unchanged: they change the filesystem, so on the web they signal rather than succeeding quietly into a filesystem the page throws away. Not here, and not half-parsed either: a directory listing, which needs an allocating builtin and a Vec of owned strings, and streaming IO. Neither has a name to trip over. programs/files.flan makes and removes its own tree and takes both restarts on operations that write. The runtime additions continue the block at the end of flan_rt.c.
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.
|
|
(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)
|