Landin library reference source

core/io/io.ldn

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