src/Fs.bend
mylsm-lsm-store@0.5.0.0 · 145 lines · 0 definitions
1import Base
2import bend-kit-bytes@0.3.2.0/bytes.bend as StorageBytes
3import bend-kit-files@0.1.1.0/files.bend as PackedFiles
4import mylsm-lsm-store@0.5.0.0/src/FsPolicy.bend as Policy
5
6# Audited host effects (spec §3.3). Thin syscall wrappers with .c + .js
7# twins and matching intended semantics. No laws — validated empirically.
8
9# Read at most 1 MiB as packed words. Short reads are valid progress; zero is EOF.
10def read_words(
11 file: File,
12 max: U32
13) ->
14 IO(File & Result<&1, &1, U32 & String, U32 & Array<U32>>):
15 PackedFiles.read.words(file, max)
16
17# Write all bytes from a packed word array, retaining the file handle on errors.
18def write_words(
19 file: File,
20 len: U32,
21 words: Array<U32>
22) ->
23 IO(File & Result<&1, &1, U32 & String, Unit>):
24 PackedFiles.write.words(file, len, words)
25
26def read_bytes.wrap(
27 result: Result<&1, &1, U32 & String, U32 & Array<U32>>
28) ->
29 Result<&1, &1, U32 & String, StorageBytes.Bytes>:
30 match result:
31 case Fail{err}:
32 Fail{err}
33 case Done{(len, words)}:
34 Done{StorageBytes.Bytes{len, words}}
35
36def read_bytes.pair(
37 pair: File & Result<&1, &1, U32 & String, U32 & Array<U32>>
38) ->
39 File & Result<&1, &1, U32 & String, StorageBytes.Bytes>:
40 match pair:
41 case (handle, result):
42 (handle, read_bytes.wrap(result))
43
44# Read one bounded packed chunk; nonempty short reads are not EOF.
45def read_bytes(
46 file: File,
47 max: U32
48) ->
49 IO(File & Result<&1, &1, U32 & String, StorageBytes.Bytes>):
50 do IO<File & Result<&1, &1, U32 & String, StorageBytes.Bytes>>:
51 pair : File & Result<&1, &1, U32 & String, U32 & Array<U32>> <- read_words(file, max)
52 return read_bytes.pair(pair)
53
54# Write the significant bytes of a packed buffer through the pinned host effect.
55def write_bytes(
56 file: File,
57 bytes: StorageBytes.Bytes
58) ->
59 IO(File & Result<&1, &1, U32 & String, Unit>):
60 StorageBytes.Bytes{len, buf} = bytes
61 write_words(file, len, buf)
62
63# Sync file contents to stable storage.
64def fsync(path: String) -> IO(Result<&1, &1, U32 & String, Unit>):
65 import "./effs/fsync.c"
66 import "./effs/fsync.js"
67
68# Handle truncate in the filesystem effects.
69def truncate(path: String, offset: Nat) -> IO(Result<&1, &1, U32 & String, Unit>):
70 import "./effs/truncate.c"
71 import "./effs/truncate.js"
72
73# Atomically rename a filesystem path.
74def rename(old: String, new: String) -> IO(Result<&1, &1, U32 & String, Unit>):
75 PackedFiles.rename(old, new)
76
77# Remove a file or empty directory.
78def remove(path: String) -> IO(Result<&1, &1, U32 & String, Unit>):
79 PackedFiles.remove(path)
80
81# Return the number of entries available through the indexed directory API.
82def read_dir_count(path: String) -> IO(Result<&1, &1, U32 & String, Nat>):
83 import "./effs/read_dir.c"
84 import "./effs/read_dir.js"
85
86# Fetch one directory entry by index.
87def read_dir_at(path: String, idx: Nat) -> IO(Result<&1, &1, U32 & String, String>):
88 import "./effs/read_dir.c"
89 import "./effs/read_dir.js"
90
91# Create one directory.
92def make_dir(path: String) -> IO(Result<&1, &1, U32 & String, Unit>):
93 import "./effs/make_dir.c"
94 import "./effs/make_dir.js"
95
96# Set POSIX permission bits.
97def chmod(path: String, bits: U32) -> IO(Result<&1, &1, U32 & String, Unit>):
98 import "./effs/chmod.c"
99 import "./effs/chmod.js"
100
101# Check whether a filesystem path exists.
102def exists.result(result: Result<&1, &1, U32 & String, PackedFiles.Info>) -> Result<&1, &1, U32 & String, Bool>:
103 match result:
104 case Fail{(+code, message)}:
105 Policy.missing_path_result(U32.is_eq(code, 2), code, message)
106 case Done{_}:
107 Done{True{}}
108
109# Returns whether a path exists and preserves other stat errors.
110def exists(path: String) -> IO(Result<&1, &1, U32 & String, Bool>):
111 do IO<Result<&1, &1, U32 & String, Bool>>:
112 result : Result<&1, &1, U32 & String, PackedFiles.Info> <- PackedFiles.stat(path)
113 return exists.result(result)
114
115# Read-only byte size for Bend-side accounting (no shell wc needed).
116def file_size(path: String) -> IO(Result<&1, &1, U32 & String, Nat>):
117 import "./effs/file_size.c"
118 import "./effs/file_size.js"
119
120# Pure accumulator step (leaf: matches structurally, never recurses).
121# Past-end fetches answer "" (SNil{}), which is skipped.
122def grab_name(nm: String, acc: List<String>) -> List<String>:
123 match nm:
124 case SNil{}:
125 acc
126 case SCon{_, _}:
127 Con{nm, acc}
128
129# Fuel-bounded sweep 0..count-1. Startup-only use (spec §10 below): the
130# directory is stable during recovery, so count-then-fetch is exact.
131def read_go(fuel: Nat, +path: String, +idx: Nat, acc: List<String>) -> IO(List<String>):
132 match fuel:
133 case 0n:
134 IO.pure(List<String>, List.reverse(&1, String, acc))
135 case 1n+f:
136 do IO<List<String>>:
137 nm : String <- IO.try(String, read_dir_at(path, idx))
138 read_go(f, path, Nat.add(idx, 1n), grab_name(nm, acc))
139
140# List directory names using the bounded count-then-fetch host API.
141def read_dir(+path: String) -> IO(Result<&1, &1, U32 & String, List<String>>):
142 do IO<Result<&1, &1, U32 & String, List<String>>>:
143 n : Nat <- IO.try(Nat, read_dir_count(path))
144 names : List<String> <- read_go(Nat.add(n, 1n), path, 0n, Nil{})
145 return Done{names}