1-- Target-neutral I/O capability, adapters and caller-backed provider.
2-- Hosted services live in hosted/io and are selected explicitly.
3
4import core/text
5
6--- The requested file or path could not be found by the provider.
7public not_found: atom
8--- The provider refused access to the requested file.
9public no_access: atom
10--- An I/O operation failed. Provider-specific diagnostic detail, if offered,
11--- must be read from that provider.
12public io_failed: atom
13--- The argument index does not identify an available argument.
14public no_argument: atom
15--- The caller's scratch buffer cannot hold all argument bytes.
16public argument_too_long: atom
17--- The path has no bytes before its terminator.
18public empty_path: atom
19--- The supplied path bytes contain an embedded NUL.
20public path_contains_nul: atom
21--- The scratch buffer cannot hold the path and its terminating NUL.
22public path_too_long: atom
23
24file_value: type = distinct i32
25
26--- Provider-defined file identifier. Use a handle only with the provider that
27--- created it and obey that provider's close rules.
28public file: type = file_value
29
30--- A borrowed pointer and byte length for one process argument. The provider
31--- owns its lifetime; use `copy_argument` for a checked copy into caller
32--- storage.
33public argument: type = struct
34 data: ptr u8
35 length: usize
36end argument
37
38--- Output capability with complete and partial writes. `write` completes all
39--- bytes or fails; failure may follow partial output. `write_some` reports
40--- positive progress for nonempty input, zero for empty input, and no
41--- progress on failure.
42---
43--- The capability is four narrow concepts over one provider type, so a
44--- provider supplies only what it has: a UART is a writer, a read-only
45--- image is a reader; system and memory providers also supply files and
46--- process entries. A consumer names the narrowest concept it uses; diagnostics
47--- stream through any io.writer. `world` composes all four for roots that
48--- have everything. An erased `any io.world` dispatches every entry, but
49--- it is not an `any io.writer`: build that from the same provider pointer
50--- where a writer is wanted [D268].
51public writer: type = concept (provider: type)
52 --- Write the entire byte slice or fail. Failure may follow partial
53 --- output; do not blindly retry the whole slice.
54 write: (self: ptr mut provider, opened: file, bytes: []u8)
55 -> none ! io_failed
56 --- Attempt one transfer. Nonempty success returns positive progress,
57 --- empty success returns zero, and failure transfers no bytes.
58 write_some: (self: ptr mut provider, opened: file, bytes: []u8)
59 -> (count: usize) ! io_failed
60end writer
61
62--- Input capability. `read` fills at most the supplied slice and returns its
63--- byte count; zero denotes end of input. The caller retains the destination
64--- storage.
65public reader: type = concept (provider: type)
66 --- Read at most the destination length and return progress; zero is end
67 --- of input.
68 read: (self: ptr mut provider, opened: file, into: []mut u8)
69 -> (count: usize) ! io_failed
70end reader
71
72--- File lifecycle and identity capability. Path pointers must be readable
73--- through a terminating NUL. A successful open returns a provider-specific
74--- handle; close consumes the handle even when reporting failure.
75public files: type = concept (provider: type)
76 --- Open a NUL-terminated path for reading; return a handle owned by this
77 --- provider.
78 open_read: (self: ptr mut provider, path: ptr u8)
79 -> (opened: file) ! not_found | no_access | io_failed
80 --- Open a NUL-terminated path for writing; creation and truncation follow
81 --- the provider contract.
82 open_write: (self: ptr mut provider, path: ptr u8)
83 -> (opened: file) ! not_found | no_access | io_failed
84 --- Compare file identity for two NUL-terminated paths.
85 same_file: (self: ptr mut provider, left: ptr u8, right: ptr u8)
86 -> (same: bool) ! io_failed
87 --- Consume a file handle, including when closure reports a failure.
88 close: (self: ptr mut provider, sink opened: file)
89 -> none ! io_failed
90end files
91
92--- Standard streams and argument access. Returned argument storage belongs to
93--- the provider; stream identifiers have provider-specific lifetime and
94--- closing rules.
95public process: type = concept (provider: type)
96 --- Return the standard-output handle.
97 out: (self: ptr provider) -> (stream: file)
98 --- Return the standard-error handle.
99 err: (self: ptr provider) -> (stream: file)
100 --- Return the count of available arguments.
101 argument_count: (self: ptr provider) -> (count: usize)
102 --- Lend an argument by index; the provider retains its backing. Report
103 --- no_argument when out of range.
104 argument: (self: ptr provider, index: usize)
105 -> (value: argument from self) ! no_argument
106end process
107
108--- Composition of reader, writer, files and process over one provider type.
109--- Consumers should request the narrowest capability they use.
110public world: type = concept (provider: type) is reader, writer, files, process
111end world
112
113-- The writer part of an erased world. `any io.world` and `any io.writer`
114-- do not convert, so a consumer holding only the world lends its writer
115-- through this adapter, which forwards both entries to the same provider.
116lent: type = struct
117 source: ptr any world
118end lent
119--- Adapter retaining an erased world's reference and forwarding the two
120--- writer entries.
121public lent_writer: type = lent
122
123--- Lend a writer adapter from an erased world. Keep the world and its
124--- underlying provider alive while the adapter is used; this does not
125--- transfer ownership.
126public writer_of: (erased: ptr any world) -> (adapter: lent from erased) =
127 adapter = (source: erased)
128end writer_of
129
130lent_write: (self: ptr mut lent, opened: file, bytes: []u8)
131 -> none ! io_failed =
132 try self.val.source.val.write(opened, bytes)
133end lent_write
134
135lent_write_some: (self: ptr mut lent, opened: file, bytes: []u8)
136 -> (count: usize) ! io_failed =
137 count = try self.val.source.val.write_some(opened, bytes)
138end lent_write_some
139
140lent is writer (write: lent_write, write_some: lent_write_some)
141
142--- Open a terminated path for reading through `files` evidence. The caller
143--- must close a successful handle using the same provider.
144public open_read: (provider: type is files, inout state: provider,
145 path: ptr u8)
146 -> (opened: file) ! not_found | no_access | io_failed =
147 opened = try provider.open_read(addr state, path)
148end open_read
149
150--- Open a terminated path for writing through `files` evidence. Creation and
151--- truncation behavior belongs to the provider; close a successful handle
152--- through that provider.
153public open_write: (provider: type is files, inout state: provider,
154 path: ptr u8)
155 -> (opened: file) ! not_found | no_access | io_failed =
156 opened = try provider.open_write(addr state, path)
157end open_write
158
159--- Ask whether two terminated paths name the same file. Delegates
160--- provider-specific identity rules and reports `io_failed` on failure.
161public same_file: (provider: type is files, inout state: provider,
162 left: ptr u8, right: ptr u8)
163 -> (same: bool) ! io_failed =
164 same = try provider.same_file(addr state, left, right)
165end same_file
166
167--- Close a provider handle. Treat it as consumed even if closing reports
168--- `io_failed`; do not retry a consumed handle.
169public close: (provider: type is files, inout state: provider,
170 sink opened: file) -> none ! io_failed =
171 try provider.close(addr state, opened)
172end close
173
174--- Read up to the destination length through `reader` evidence. Returns the
175--- number of bytes placed in the buffer, or zero at end of input.
176public read: (provider: type is reader, inout state: provider,
177 opened: file, into: []mut u8)
178 -> (count: usize) ! io_failed =
179 count = try provider.read(addr state, opened, into)
180end read
181
182--- Write the complete byte slice through `writer` evidence. `io_failed` may
183--- follow partial output; callers must not assume retrying the whole buffer
184--- is safe.
185public write: (provider: type is writer, inout state: provider,
186 opened: file, bytes: []u8) -> none ! io_failed =
187 try provider.write(addr state, opened, bytes)
188end write
189
190--- Make one write attempt and return its progress. Nonempty success is
191--- positive; empty input returns zero; failure transfers no bytes.
192public write_some: (provider: type is writer, inout state: provider,
193 opened: file, bytes: []u8) -> (count: usize) ! io_failed =
194 count = try provider.write_some(addr state, opened, bytes)
195end write_some
196
197--- Return the provider's standard-output identifier.
198public out: (provider: type is process, state: provider) -> (stream: file) =
199 stream = provider.out(addr state)
200end out
201
202--- Return the provider's standard-error identifier.
203public err: (provider: type is process, state: provider) -> (stream: file) =
204 stream = provider.err(addr state)
205end err
206
207--- Return the number of argument entries exposed by the provider.
208public argument_count: (provider: type is process, state: provider)
209 -> (count: usize) =
210 count = provider.argument_count(addr state)
211end argument_count
212
213--- Lend the indexed process argument. Reports `no_argument` for an invalid
214--- index; the provider's argument backing must outlive its use.
215public argument_at: (provider: type is process, state: ptr provider,
216 index: usize)
217 -> (value: argument from state) ! no_argument =
218 value = try provider.argument(state, index)
219end argument_at
220
221--- Copy an argument into caller scratch and lend the written prefix. Reports
222--- `argument_too_long` before any write when capacity is insufficient.
223---
224--- An argument is a foreign-shaped pointer and length, not a slice. Copy it
225--- into genuine caller-owned initialized storage before passing its bytes to
226--- ordinary slice APIs. Exact capacity is sufficient and failure precedes
227--- every write. The caller keeps the source extent readable and disjoint
228--- from scratch, with representable address arithmetic, for the duration of
229--- the copy.
230public copy_argument: (source: argument, scratch: []mut u8)
231 -> (bytes: []u8 from scratch) ! argument_too_long =
232 fail argument_too_long when source.length > lenof scratch
233 mut copied: usize = 0
234 while copied < source.length do
235 current: ptr u8 = ptr(usize(source.data) + copied)
236 scratch[copied] = current.val
237 inc copied
238 end while
239 bytes = scratch[0..<source.length]
240end copy_argument
241
242--- Copy path bytes into scratch and add a NUL terminator. Rejects empty
243--- input, embedded NUL and insufficient space in that order before any write;
244--- overlapping input and scratch are supported.
245---
246--- A files entry receives one already terminated pointer. Dynamic callers
247--- make that precondition explicit with caller-owned scratch: empty input,
248--- an embedded NUL, and insufficient terminator capacity are distinct and
249--- checked in that order. No byte is copied before all checks pass. Source
250--- and scratch may overlap; the copy preserves the original source bytes.
251public terminate_path: (source: []u8, scratch: []mut u8)
252 -> (path: ptr u8 from scratch)
253 ! empty_path | path_contains_nul | path_too_long =
254 fail empty_path when lenof source == 0
255 mut checked: usize = 0
256 while checked < lenof source do
257 fail path_contains_nul when source[checked] == 0
258 inc checked
259 end while
260 fail path_too_long when lenof source >= lenof scratch
261
262 if usize(addr scratch[0]) > usize(addr source[0]) then
263 mut remaining: usize = lenof source
264 while remaining > 0 do
265 dec remaining
266 scratch[remaining] = source[remaining]
267 end while
268 else
269 mut copied: usize = 0
270 while copied < lenof source do
271 scratch[copied] = source[copied]
272 inc copied
273 end while
274 end if
275 scratch[lenof source] = 0
276 path = addr scratch[0]
277end terminate_path
278
279--- Terminate a byte path in caller scratch, then open it for reading.
280--- Path-validation failures occur before the provider is called.
281public open_read_bytes: (provider: type is files, inout state: provider,
282 path: []u8, scratch: []mut u8)
283 -> (opened: file)
284 ! empty_path | path_contains_nul | path_too_long
285 | not_found | no_access | io_failed =
286 terminated: ptr u8 = try terminate_path(path, scratch)
287 opened = try open_read(state, terminated)
288end open_read_bytes
289
290--- Terminate a byte path in caller scratch, then open it for writing. Path
291--- validation and provider failures remain distinct.
292public open_write_bytes: (provider: type is files, inout state: provider,
293 path: []u8, scratch: []mut u8)
294 -> (opened: file)
295 ! empty_path | path_contains_nul | path_too_long
296 | not_found | no_access | io_failed =
297 terminated: ptr u8 = try terminate_path(path, scratch)
298 opened = try open_write(state, terminated)
299end open_write_bytes
300
301--- Open a UTF-8 path for reading using caller scratch for the terminator.
302--- Delegates to the byte-path adapter; UTF-8 does not excuse embedded NUL.
303---
304--- UTF-8 paths remain ordinary immutable, source-derived views. Conversion
305--- to bytes does not allocate or invent a terminator; the same caller-owned
306--- scratch and validation policy as the byte adapters applies.
307public open_read_text: (provider: type is files, inout state: provider,
308 path: utf8, scratch: []mut u8)
309 -> (opened: file)
310 ! empty_path | path_contains_nul | path_too_long
311 | not_found | no_access | io_failed =
312 raw: []u8 = text.bytes(path)
313 opened = try open_read_bytes(state, raw, scratch)
314end open_read_text
315
316--- Open a UTF-8 path for writing using caller scratch for the terminator.
317--- Delegates to the byte-path adapter.
318public open_write_text: (provider: type is files, inout state: provider,
319 path: utf8, scratch: []mut u8)
320 -> (opened: file)
321 ! empty_path | path_contains_nul | path_too_long
322 | not_found | no_access | io_failed =
323 raw: []u8 = text.bytes(path)
324 opened = try open_write_bytes(state, raw, scratch)
325end open_write_text