Landin library reference source

hosted/io/io.ldn

1--  Linux/Darwin libc-backed provider of every core/io concept: reader,
2--  writer, files and process, and so of their composition, world.
3--  Import this module only at a hosted authority root.
4
5import core/io
6
7extern(c) _landin_host_argument_count: () -> (count: usize)
8extern(c) _landin_host_argument_table: () -> (table: ptr ptr u8)
9extern(c) _landin_host_argument_at_from: (table: ptr ptr u8, index: usize)
10                                         -> (data: ptr u8 from table)
11extern(c) _landin_host_text_length: (data: ptr u8) -> (length: usize)
12extern(c) _landin_host_open_read: (path: ptr u8) -> (descriptor: i32)
13extern(c) _landin_host_open_write: (path: ptr u8) -> (descriptor: i32)
14extern(c) _landin_host_read: (descriptor: i32, data: ptr mut u8,
15                              length: usize) -> (read_count: usize)
16extern(c) _landin_host_write: (descriptor: i32, data: ptr u8,
17                               length: usize) -> (write_count: usize)
18extern(c) _landin_host_close: (descriptor: i32) -> (status: i32)
19extern(c) _landin_host_errno: () -> (number: i32)
20
21--  Values shared by Linux and Darwin (historical names). Kept at the only
22--  provider that interprets libc errno, rather than leaking into its users.
23linux_eperm: i32 = 1
24linux_enoent: i32 = 2
25linux_eintr: i32 = 4
26linux_eacces: i32 = 13
27linux_erofs: i32 = 30
28
29system_value: type = struct
30    arguments: ptr ptr u8
31    argument_length: usize
32    failure_errno: i32
33end system_value
34
35--- Hosted implementation of all four I/O capabilities and `io.world`, backed
36--- by libc. File identifiers belong to this provider; borrowed arguments
37--- refer to process storage.
38public system: type = system_value
39
40--- Return errno captured for the terminal failing provider call. Read it
41--- immediately after failure; it is provider state, not a portable error
42--- category.
43---
44--- Zero means that the most recent open/read/write/close did not report a
45--- failing libc call.  A nonzero value is the exact errno captured for the
46--- terminal call before the provider made any other host call.  Retriable
47--- EINTR attempts are internal and do not replace a previously reported
48--- operation with a failure.
49public last_errno: (state: system) -> (number: i32) =
50    number = state.failure_errno
51end last_errno
52
53system_open_read: (self: ptr mut system, path: ptr u8)
54                  -> (opened: io.file) ! io.not_found | io.no_access | io.io_failed =
55    self.val.failure_errno = 0
56    loop do
57        descriptor: i32 = _landin_host_open_read(path)
58        if descriptor >= 0 then
59            opened = io.file(descriptor)
60            return
61        end if
62
63        --  Nothing may call into the host between the failed operation and
64        --  this capture.  Linux open reports EINTR before publishing a file
65        --  descriptor, so retrying this same request is safe.
66        number: i32 = _landin_host_errno()
67        if number <> linux_eintr then
68            self.val.failure_errno = number
69            fail io.not_found when number == linux_enoent
70            fail io.no_access when number == linux_eperm or number == linux_eacces
71              or number == linux_erofs
72            fail io.io_failed
73        end if
74    end loop
75end system_open_read
76
77system_open_write: (self: ptr mut system, path: ptr u8)
78                   -> (opened: io.file)
79                   ! io.not_found | io.no_access | io.io_failed =
80    self.val.failure_errno = 0
81    loop do
82        descriptor: i32 = _landin_host_open_write(path)
83        if descriptor >= 0 then
84            opened = io.file(descriptor)
85            return
86        end if
87
88        --  The bridge's write-open request is fixed across attempts.  An
89        --  interrupted open has not returned a descriptor, so retry it; every
90        --  other errno is retained before mapping to the stable atom set.
91        number: i32 = _landin_host_errno()
92        if number <> linux_eintr then
93            self.val.failure_errno = number
94            fail io.not_found when number == linux_enoent
95            fail io.no_access when number == linux_eperm or number == linux_eacces
96              or number == linux_erofs
97            fail io.io_failed
98        end if
99    end loop
100end system_open_write
101
102--  A stable-path identity check, not an atomic check-and-open operation.
103--  Every input lookup failure refuses; only an absent output is allowed.
104system_same_file: (self: ptr mut system, left: ptr u8, right: ptr u8)
105                  -> (same: bool) ! io.io_failed =
106    self.val.failure_errno = 0
107    fail io.io_failed when alignof native_stat <> 8
108      or sizeof native_stat <> native_stat_size
109    mut first: native_stat = zeroed
110    mut second: native_stat = zeroed
111    if stat(left, addr first) <> 0 then
112        self.val.failure_errno = _landin_host_errno()
113        fail io.io_failed
114    end if
115    if stat(right, addr second) <> 0 then
116        number: i32 = _landin_host_errno()
117        if number == linux_enoent then
118            same = false
119            return
120        end if
121        self.val.failure_errno = number
122        fail io.io_failed
123    end if
124    same = first.device == second.device and first.inode == second.inode
125end system_same_file
126
127system_close: (self: ptr mut system, sink opened: io.file)
128              -> none ! io.io_failed =
129    self.val.failure_errno = 0
130    status: i32 = _landin_host_close(i32(opened))
131    if status <> 0 then
132        --  Capture first, then report.  A failed Linux close may already have
133        --  released the descriptor; retrying could close an unrelated reused
134        --  descriptor, so EINTR is deliberately terminal here.
135        self.val.failure_errno = _landin_host_errno()
136        fail io.io_failed
137    end if
138end system_close
139
140system_read: (self: ptr mut system, opened: io.file, into: []mut u8)
141             -> (count: usize) ! io.io_failed =
142    self.val.failure_errno = 0
143    if lenof into == 0 then
144        count = 0
145        return
146    end if
147    base: ptr mut u8 = addr into[0]
148    failed: usize = 0 -% 1
149    loop do
150        read_count: usize = _landin_host_read
151            (i32(opened), base, lenof into)
152        if read_count <> failed then
153            fail io.io_failed when read_count > lenof into
154            --  Zero from a nonempty read is genuine EOF, not stalled progress.
155            count = read_count
156            return
157        end if
158
159        --  Linux read returns EINTR only before transferring a byte, so the
160        --  same buffer and extent may be retried.  A positive partial read is
161        --  returned above and is never discarded by an unnecessary retry.
162        number: i32 = _landin_host_errno()
163        if number <> linux_eintr then
164            self.val.failure_errno = number
165            fail io.io_failed
166        end if
167    end loop
168end system_read
169
170write_all: (self: ptr mut system, descriptor: i32, bytes: []u8)
171           -> none ! io.io_failed =
172    failed: usize = 0 -% 1
173    mut written: usize = 0
174    while written < lenof bytes do
175        remaining: []u8 = bytes[written..<lenof bytes]
176        count: usize = lenof remaining
177        base: ptr u8 = addr remaining[0]
178        mut accepted: usize = failed
179        loop do
180            accepted = _landin_host_write(descriptor, base, count)
181            break when accepted <> failed
182
183            --  Linux reports EINTR only before this attempt transfers data.
184            --  Retry the same remaining suffix; a preceding positive partial
185            --  write has already advanced `written` and is never repeated.
186            number: i32 = _landin_host_errno()
187            if number <> linux_eintr then
188                self.val.failure_errno = number
189                fail io.io_failed
190            end if
191        end loop
192        --  A nonempty zero-progress success cannot complete write-all, and an
193        --  oversized count violates the host seam.  Neither fabricates errno.
194        fail io.io_failed when accepted == 0 or accepted > count
195        written += accepted
196    end while
197end write_all
198
199system_write: (self: ptr mut system, opened: io.file, bytes: []u8)
200              -> none ! io.io_failed =
201    self.val.failure_errno = 0
202    return when lenof bytes == 0
203    try write_all(self, i32(opened), bytes)
204end system_write
205
206system_write_some: (self: ptr mut system, opened: io.file, bytes: []u8)
207                   -> (count: usize) ! io.io_failed =
208    self.val.failure_errno = 0
209    if lenof bytes == 0 then
210        count = 0
211        return
212    end if
213    failed: usize = 0 -% 1
214    base: ptr u8 = addr bytes[0]
215    --  Keep a single host request within a representable, bounded extent;
216    --  the caller can send the remaining suffix after this progress.
217    mut requested: usize = lenof bytes
218    if requested > 65536 then requested = 65536 end if
219    loop do
220        count = _landin_host_write(i32(opened), base, requested)
221        if count <> failed then
222            fail io.io_failed when count == 0 or count > requested
223            return
224        end if
225        --  EINTR transfers nothing on this attempt. A positive short write
226        --  returns immediately so its exact prefix reaches the caller.
227        number: i32 = _landin_host_errno()
228        if number <> linux_eintr then
229            self.val.failure_errno = number
230            fail io.io_failed
231        end if
232    end loop
233end system_write_some
234
235system_out: (self: ptr system) -> (stream: io.file) =
236    _ = self.val.argument_length
237    stream = io.file(1)
238end system_out
239
240system_err: (self: ptr system) -> (stream: io.file) =
241    _ = self.val.argument_length
242    stream = io.file(2)
243end system_err
244
245system_argument_count: (self: ptr system) -> (count: usize) =
246    count = self.val.argument_length
247end system_argument_count
248
249system_argument_table: (self: ptr system)
250                       -> (table: ptr ptr u8 from self) =
251    table = self.val.arguments
252end system_argument_table
253
254system_argument: (self: ptr system, index: usize)
255                 -> (value: io.argument from self) ! io.no_argument =
256    fail io.no_argument when index >= self.val.argument_length
257    table: ptr ptr u8 = system_argument_table(self)
258    data: ptr u8 = _landin_host_argument_at_from(table, index)
259    length: usize = _landin_host_text_length(data)
260    value = (data: data, length: length)
261end system_argument
262
263system is io.reader (read: system_read)
264system is io.writer (write: system_write, write_some: system_write_some)
265system is io.files
266    (open_read: system_open_read, open_write: system_open_write,
267     same_file: system_same_file, close: system_close)
268system is io.process
269    (out: system_out, err: system_err,
270     argument_count: system_argument_count, argument: system_argument)
271system is io.world ()
272
273--- Create the hosted I/O provider and expose process arguments and standard
274--- streams. Regular open-write operations create or truncate files; close
275--- regular files explicitly.
276public host: () -> (state: system) =
277    state = (arguments: _landin_host_argument_table(),
278             argument_length: _landin_host_argument_count(),
279             failure_errno: 0)
280end host