Landin library reference source

core/text/builder.ldn

1--  A builder writes typed pieces into bytes: a caller's buffer, or storage
2--  an allocator the caller names grows.  Every append either writes its
3--  whole piece or reports `no_space` and leaves the builder as it was.
4
5import core/mem
6
7builder_value: type = struct
8    buffer: []mut u8
9    used: usize
10    owned: bool
11    growth: variant
12        bounded |
13        grows: (memory: any mem.allocator)
14    end growth
15end builder_value
16
17--- Text being built: the bytes written so far, in a buffer the caller gave
18--- `over` or in storage that `growing` takes from an allocator.  A value
19--- the caller keeps and edits, passing it `inout` to every append; `used`
20--- lends what it holds.  It is bytes, not `utf8`: an append of bytes or of
21--- a host string writes them as they are.
22public builder: type = builder_value
23
24--- A builder over the caller's `buffer`, empty, which never grows: an
25--- append that does not fit reports `no_space`.  The buffer's earlier
26--- contents are overwritten as pieces are appended.
27public over: (buffer: []mut u8) -> (result: builder from buffer) =
28    result = (buffer: buffer, used: 0, owned: false, growth: bounded)
29end over
30
31--- An empty builder that takes its storage from `memory` as it needs it,
32--- doubling what it holds, and reports `no_space` only when the allocator
33--- refuses.  Build the erased allocator from the address of its handle's
34--- binding, `text.growing(any(addr heap))`, and keep the handle and its
35--- state alive while the builder is used.  `release` gives the storage
36--- back: there are no destructors, so a builder dropped without it keeps
37--- its storage.
38public growing: (memory: any mem.allocator) -> (result: builder from memory) =
39    nothing: []mut u8 = []
40    result = (buffer: nothing, used: 0, owned: false,
41              growth: grows(memory: memory))
42end growing
43
44--- The bytes written so far, lent from the builder until it is next
45--- changed.
46public used: (source: builder) -> (bytes: []u8 from source) =
47    bytes = source.buffer[0..<source.used]
48end used
49
50--- How many bytes have been written: the offset the next piece begins at,
51--- which `align_left` and `align_right` take as a piece's start.
52public length: (source: builder) -> (count: usize) =
53    count = source.used
54end length
55
56--- How many bytes the builder holds before it next fills: its buffer's
57--- length, which grows for a builder from `growing`.
58public capacity: (source: builder) -> (count: usize) =
59    room := source.buffer
60    count = lenof room
61end capacity
62
63--- Forget everything written, keeping the buffer for the next text.
64public clear: (inout target: builder) -> none =
65    target.used = 0
66end clear
67
68--- Keep only the first `count` bytes written; a count at or past the
69--- length changes nothing.  A `formattable` conformance that appends
70--- several pieces puts the builder back with this, to an earlier `length`,
71--- when a later piece reports `no_space`.
72public truncate: (inout target: builder, count: usize) -> none =
73    if count < target.used then
74        target.used = count
75    end if
76end truncate
77
78--- Give a growing builder's storage back to its allocator and leave it
79--- empty; it grows again from nothing if appended to.  A builder `over` a
80--- caller's buffer has nothing to give back and is only cleared.  End every
81--- use of `used` first.
82public release: (inout target: builder) -> none =
83    target.used = 0
84    return when not target.owned
85    room := target.buffer
86    extent: usize = lenof room
87    block: ptr mut u8 = addr target.buffer[0]
88    match target.growth
89        bounded: return
90        grows(memory): memory.free(block, extent)
91    end match
92    nothing: []mut u8 = []
93    target.buffer = nothing
94    target.owned = false
95end release
96
97--  A writable view of `size` bytes at `block`, which an allocator returned.
98--  The block borrows nothing [0810], so neither does the view.
99view: (block: ptr mut u8, size: usize) -> (result: []mut u8) =
100    first: ptr mut [1]u8 = ptr(usize(block))
101    result = first.val[0..<1]
102    unchecked begin
103        result = result[0..<size]
104    end unchecked
105end view
106
107--  Make room for `extra` more bytes after the used ones.  A bounded builder
108--  without the room reports `no_space`; a growing one doubles, in place if
109--  the allocator can, otherwise into a fresh block the used bytes are copied
110--  to before the old one is given back, and reports `no_space` when the
111--  allocator refuses.  A failure changes nothing.
112room: (inout target: builder, extra: usize) -> none ! no_space =
113    current := target.buffer
114    capacity_now: usize = lenof current
115    return when extra <= capacity_now - target.used
116    maximum: usize = 0 -% 1
117    fail no_space when extra > maximum - target.used
118    wanted: usize = target.used + extra
119    match target.growth
120        bounded: fail no_space
121        grows(memory): begin
122            mut size: usize = 16
123            if capacity_now > maximum / 2 then
124                size = maximum
125            elsif capacity_now * 2 > size then
126                size = capacity_now * 2
127            end if
128            if size < wanted then
129                size = wanted
130            end if
131            if target.owned then
132                old: ptr mut u8 = addr target.buffer[0]
133                if memory.grow(old, capacity_now, size, 1) then
134                    target.buffer = view(old, size)
135                    return
136                end if
137            end if
138            block: ptr mut u8 = memory.alloc(size, 1) else (problem)
139                _ = problem
140                fail no_space
141            end
142            fresh: []mut u8 = view(block, size)
143            mem.copy(fresh, target.buffer[0..<target.used]) else (problem)
144                _ = problem
145                memory.free(block, size)
146                fail no_space
147            end
148            if target.owned then
149                old: ptr mut u8 = addr target.buffer[0]
150                memory.free(old, capacity_now)
151            end if
152            target.buffer = fresh
153            target.owned = true
154        end
155    end match
156end room
157
158--- Append bytes as they are.
159public append: (inout into: builder, bytes: []u8) -> none ! no_space =
160    count: usize = lenof bytes
161    return when count == 0
162    try room(into, count)
163    ends: usize = into.used + count
164    mem.copy(into.buffer[into.used..<ends], bytes) else (problem)
165        _ = problem
166        fail no_space
167    end
168    into.used = ends
169end append
170
171--- Append one byte.
172public append_byte: (inout into: builder, value: u8) -> none ! no_space =
173    try room(into, 1)
174    into.buffer[into.used] = value
175    inc into.used
176end append_byte
177
178--- Append text's encoded bytes.
179public append_text: (inout into: builder, value: utf8) -> none ! no_space =
180    try append(into, bytes(value))
181end append_text
182
183--- Append a host string's bytes, as the system gave them: a name that is
184--- not UTF-8 is written as it is.  Check it with `from_host` first where
185--- the output must be text.
186public append_host: (inout into: builder, value: host_string)
187                    -> none ! no_space =
188    try append(into, value.bytes)
189end append_host
190
191--- Append a `u32` in decimal: its digits, with no sign, padding or
192--- separator.  Write `u8` and `u16` through it.
193public append_u32: (inout into: builder, value: u32) -> none ! no_space =
194    next: usize = write_u32(into.buffer, into.used, value) else (problem)
195        _ = problem
196        try room(into, 10)
197        into.used = try write_u32(into.buffer, into.used, value)
198        return
199    end
200    into.used = next
201end append_u32
202
203--- Append a `u64` in decimal.
204public append_u64: (inout into: builder, value: u64) -> none ! no_space =
205    next: usize = write_u64(into.buffer, into.used, value) else (problem)
206        _ = problem
207        try room(into, 20)
208        into.used = try write_u64(into.buffer, into.used, value)
209        return
210    end
211    into.used = next
212end append_u64
213
214--- Append a `usize` in decimal, at the target's width.
215public append_usize: (inout into: builder, value: usize) -> none ! no_space =
216    next: usize = write_usize(into.buffer, into.used, value) else (problem)
217        _ = problem
218        try room(into, 20)
219        into.used = try write_usize(into.buffer, into.used, value)
220        return
221    end
222    into.used = next
223end append_usize
224
225--- Append an `i32` in decimal: a `-` before a negative value's digits and
226--- nothing before any other's.  Write `i8` and `i16` through it.
227public append_i32: (inout into: builder, value: i32) -> none ! no_space =
228    if value >= 0 then
229        try append_u32(into, u32(value))
230        return
231    end if
232    start: usize = into.used
233    try append_byte(into, 45)
234    magnitude: u32 = u32(0 - (value + 1)) + 1
235    append_u32(into, magnitude) else (problem)
236        into.used = start
237        fail problem
238    end
239end append_i32
240
241--- Append an `i64` in decimal, signed as `append_i32` is.
242public append_i64: (inout into: builder, value: i64) -> none ! no_space =
243    if value >= 0 then
244        try append_u64(into, u64(value))
245        return
246    end if
247    start: usize = into.used
248    try append_byte(into, 45)
249    magnitude: u64 = u64(0 - (value + 1)) + 1
250    append_u64(into, magnitude) else (problem)
251        into.used = start
252        fail problem
253    end
254end append_i64
255
256--- Append an `isize` in decimal, signed as `append_i32` is.
257public append_isize: (inout into: builder, value: isize) -> none ! no_space =
258    if value >= 0 then
259        try append_usize(into, usize(value))
260        return
261    end if
262    start: usize = into.used
263    try append_byte(into, 45)
264    magnitude: usize = usize(0 - (value + 1)) + 1
265    append_usize(into, magnitude) else (problem)
266        into.used = start
267        fail problem
268    end
269end append_isize
270
271hex_digits: []u8 = "0123456789abcdef"
272
273--- Append a `u32` in hexadecimal: lower-case digits, as few as the value
274--- needs, and no `0x`, which is a piece of its own.
275public append_hex_u32: (inout into: builder, value: u32) -> none ! no_space =
276    mut count: usize = 1
277    mut scan: u32 = value >> 4
278    while scan <> 0 do
279        inc count
280        scan = scan >> 4
281    end while
282    try room(into, count)
283    mut remaining: u32 = value
284    mut at: usize = into.used + count
285    while at > into.used do
286        dec at
287        into.buffer[at] = hex_digits[usize(remaining & 15)]
288        remaining = remaining >> 4
289    end while
290    into.used += count
291end append_hex_u32
292
293--- Append a `u64` in hexadecimal, as `append_hex_u32` does.
294public append_hex_u64: (inout into: builder, value: u64) -> none ! no_space =
295    mut count: usize = 1
296    mut scan: u64 = value >> 4
297    while scan <> 0 do
298        inc count
299        scan = scan >> 4
300    end while
301    try room(into, count)
302    mut remaining: u64 = value
303    mut at: usize = into.used + count
304    while at > into.used do
305        dec at
306        into.buffer[at] = hex_digits[usize(remaining & 15)]
307        remaining = remaining >> 4
308    end while
309    into.used += count
310end append_hex_u64
311
312--- Append a `usize` in hexadecimal, as `append_hex_u32` does.
313public append_hex_usize: (inout into: builder, value: usize)
314                         -> none ! no_space =
315    mut count: usize = 1
316    mut scan: usize = value >> 4
317    while scan <> 0 do
318        inc count
319        scan = scan >> 4
320    end while
321    try room(into, count)
322    mut remaining: usize = value
323    mut at: usize = into.used + count
324    while at > into.used do
325        dec at
326        into.buffer[at] = hex_digits[remaining & 15]
327        remaining = remaining >> 4
328    end while
329    into.used += count
330end append_hex_usize
331
332--- Right-align the piece written since `start`, an earlier `length`, in a
333--- field `width` bytes wide: `fill` goes before it until the field is
334--- full.  A piece as wide as the field or wider is left as it is.  The fill
335--- goes before the whole piece, a sign included, so a zero fill suits an
336--- unsigned value.  A `start` past the length is a checked-range trap, as a
337--- slice's would be.
338public align_right: (inout into: builder, start: usize, width: usize,
339                     fill: u8) -> none ! no_space =
340    piece: usize = into.used - start
341    return when piece >= width
342    padding: usize = width - piece
343    try room(into, padding)
344    ends: usize = into.used + padding
345    moved: usize = start + padding
346    mem.copy(into.buffer[moved..<ends], into.buffer[start..<into.used])
347        else (problem)
348        _ = problem
349        fail no_space
350    end
351    mem.fill(into.buffer[start..<moved], fill)
352    into.used = ends
353end align_right
354
355--- Left-align the piece written since `start` in a field `width` bytes
356--- wide: `fill` goes after it until the field is full.  A piece as wide as
357--- the field or wider is left as it is.
358public align_left: (inout into: builder, start: usize, width: usize,
359                    fill: u8) -> none ! no_space =
360    piece: usize = into.used - start
361    return when piece >= width
362    padding: usize = width - piece
363    try room(into, padding)
364    ends: usize = into.used + padding
365    mem.fill(into.buffer[into.used..<ends], fill)
366    into.used = ends
367end align_left