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