Landin library reference source

core/io/lines.ldn

1--  A line reader reads a file through a caller's buffer and lends it back
2--  a line at a time, as a directory lends its names.
3
4import core/mem
5
6--- A line, its line ending included, does not fit the line reader's
7--- buffer.
8public line_too_long: atom
9
10lines_value: type (provider: type) = struct
11    host: provider
12    opened: file
13    buffer: []mut u8
14    start: usize
15    filled: usize
16    finished: bool
17    skipping: bool
18end lines_value
19
20--- Reads a file a line at a time through a buffer the caller supplies: a
21--- value the caller keeps and edits, passing it `inout` to `read_line`.
22--- Each line is lent from the reader until the next read, which reuses the
23--- buffer it lies in, so a program that keeps a line copies it.
24public line_reader: type (provider: type) = lines_value(provider)
25
26--- One line, lent from its reader: its bytes without the line ending, and
27--- whether the input had already ended, in which case there is no line and
28--- `bytes` is empty.  An empty line is an empty `bytes` that has not
29--- `finished`.
30public line: type = struct
31    bytes: []u8
32    finished: bool
33end line
34
35--- A line reader over the caller's `buffer`, reading `opened` through
36--- `host`.  It holds a copy of the reader's handle; keep the handle's
37--- state and the buffer alive while it is used.  The longest line it reads
38--- is one byte shorter than the buffer, which must hold the line's `\n` as
39--- well.
40public lines_over: (provider: type is reader, host: provider, opened: file,
41                    buffer: []mut u8)
42                   -> (lines: line_reader(provider) from buffer, host) =
43    lines = (host: host, opened: opened, buffer: buffer, start: 0, filled: 0,
44             finished: false, skipping: false)
45end lines_over
46
47--  Move the unread bytes down to the front of the buffer.
48compact: (provider: type, inout lines: line_reader(provider))
49         -> none ! io_failed =
50    return when lines.start == 0
51    remaining: usize = lines.filled - lines.start
52    mem.copy(lines.buffer[0..<remaining],
53             lines.buffer[lines.start..<lines.filled]) else (problem)
54        _ = problem
55        fail io_failed
56    end
57    lines.start = 0
58    lines.filled = remaining
59end compact
60
61--  Read more after the unread bytes, moved to the front; the reader's zero
62--  marks the end of input.
63refill: (provider: type is reader, inout lines: line_reader(provider))
64        -> none ! io_failed =
65    try compact(lines)
66    room := lines.buffer
67    got: usize = try provider.read(lines.host, lines.opened,
68                                   lines.buffer[lines.filled..<lenof room])
69    if got == 0 then
70        lines.finished = true
71    else
72        lines.filled += got
73    end if
74end refill
75
76--- Read the next line, lent from the reader until the next read.  A line
77--- ends at `\n`, and a `\r` just before it is part of the ending, so text
78--- written with `\r\n` reads the same as text written with `\n`; a lone
79--- `\r` is an ordinary byte.  The last line need not end: input that ends
80--- after it gives it, and the next read is `finished`.  End of input is a
81--- line that has `finished`, as an empty read is the end of a file.
82---
83--- A line that does not fit the buffer with its ending reports
84--- `line_too_long`, and the rest of that line is skipped, so the read after
85--- it gives the line that follows.  A reader's failure is its `io_failed`,
86--- and keeps every byte already read for the next call.
87public read_line: (provider: type is reader,
88                   inout lines: line_reader(provider))
89                  -> (item: line from lines)
90                  ! line_too_long | io_failed =
91    loop do
92        if lines.skipping then
93            waiting: usize = lines.filled - lines.start
94            at: usize = mem.find(lines.buffer[lines.start..<lines.filled], 10)
95            if at < waiting then
96                lines.start += at + 1
97                lines.skipping = false
98            else
99                lines.start = 0
100                lines.filled = 0
101                if lines.finished then
102                    lines.skipping = false
103                else
104                    try refill(lines)
105                end if
106            end if
107        else
108            waiting: usize = lines.filled - lines.start
109            at: usize = mem.find(lines.buffer[lines.start..<lines.filled], 10)
110            if at < waiting then
111                begins: usize = lines.start
112                mut ends: usize = begins + at
113                lines.start = ends + 1
114                if ends > begins and lines.buffer[ends - 1] == 13 then
115                    dec ends
116                end if
117                item = (bytes: lines.buffer[begins..<ends], finished: false)
118                return
119            end if
120            if lines.finished then
121                begins: usize = lines.start
122                lines.start = lines.filled
123                item = (bytes: lines.buffer[begins..<lines.filled],
124                        finished: waiting == 0)
125                return
126            end if
127            room := lines.buffer
128            if waiting == lenof room then
129                lines.start = 0
130                lines.filled = 0
131                lines.skipping = true
132                fail line_too_long
133            end if
134            try refill(lines)
135        end if
136    end loop
137end read_line