1-- Diagnostics are an ordinary capability. A producer sees only `log`;
2-- callers choose whether notes are retained in bounded storage or written
3-- immediately to any io.writer.
4
5import core/io
6import core/mem
7import core/text
8
9--- Byte carrier for diagnostic severity. Use the named `warning` and `error`
10--- values.
11public severity: type = u8
12--- Warning severity; recording it does not by itself mark a log as failed.
13public warning: severity = 0
14--- Error severity; recording it marks the log as failed.
15public error: severity = 1
16
17entry_value: type = struct
18 where: usize
19 kind: severity
20 what_bytes: [256]u8
21 what_length: usize
22end entry_value
23
24--- Opaque stored diagnostic containing severity, position and a bounded
25--- message copy.
26public entry: type = entry_value
27--- Maximum byte count stored for one bounded diagnostic message. Longer
28--- messages are dropped as whole entries.
29public message_capacity: usize = 256
30
31--- Diagnostic capability. `note` records severity, position and message;
32--- `failed` reports whether an error diagnostic has been observed. I/O
33--- failures propagate separately from `note`.
34public log: type = concept (logger: type)
35 --- Record a diagnostic, copying or consuming the message during the call.
36 --- I/O failure is returned separately from error severity.
37 note: (self: ptr mut logger, where: text.position, kind: severity,
38 what: []u8) -> none ! io.io_failed
39 --- Report whether an error diagnostic has been observed.
40 failed: (self: ptr logger) -> (yes: bool)
41end log
42
43bounded_value: type (fixed capacity: usize) = struct
44 notes: [capacity]entry
45 stored: usize
46 dropped: usize
47 errors: usize
48end bounded_value
49
50--- Fixed-capacity diagnostic log. Stores entries without allocation, counts
51--- messages dropped for capacity or excessive length, and remembers errors even if
52--- their entries are dropped.
53public bounded: type (fixed capacity: usize) = bounded_value(capacity)
54
55bounded_note: (fixed capacity: usize, self: ptr mut bounded(capacity),
56 where: text.position, kind: severity, what: []u8)
57 -> none ! io.io_failed =
58 if self.val.stored < capacity and lenof what <= message_capacity then
59 -- An erased producer may pass frame bytes. Retain a copy rather
60 -- than an address into the producer's storage.
61 mut saved: entry = zeroed
62 saved.where = text.ordinal(where)
63 saved.kind = kind
64 saved.what_length = lenof what
65 mut index: usize = 0
66 while index < lenof what do
67 saved.what_bytes[index] = what[index]
68 inc index
69 end while
70 self.val.notes[self.val.stored] = saved
71 inc self.val.stored
72 else
73 inc self.val.dropped
74 end if
75 if kind == error then
76 inc self.val.errors
77 end if
78end bounded_note
79
80bounded_failed: (fixed capacity: usize, self: ptr bounded(capacity))
81 -> (yes: bool) =
82 yes = self.val.errors > 0
83end bounded_failed
84
85--- Create an empty bounded log with zero stored and dropped counts.
86public new: (fixed capacity: usize) -> (result: bounded(capacity)) =
87 result = (notes: uninit, stored: 0, dropped: 0, errors: 0)
88end new
89
90--- Return the number of retained diagnostic entries.
91---
92--- Read-only pointers let both mutable and immutable log owners query without
93--- copying the bounded note array.
94public stored: (fixed capacity: usize, value: ptr bounded(capacity))
95 -> (count: usize) =
96 count = value.val.stored
97end stored
98
99--- Return the number of entries not retained because the log was full or the
100--- message exceeded `message_capacity`.
101public dropped: (fixed capacity: usize, value: ptr bounded(capacity))
102 -> (count: usize) =
103 count = value.val.dropped
104end dropped
105
106--- Copy a retained entry by index. Reports `mem.out_of_bounds` if the index
107--- is not stored.
108public note_at: (fixed capacity: usize, value: ptr bounded(capacity), index: usize)
109 -> (result: entry) ! mem.out_of_bounds =
110 fail mem.out_of_bounds when index >= value.val.stored
111 result = value.val.notes[index]
112end note_at
113
114--- Return the caller-supplied source position associated with an entry.
115public position_of: (value: entry) -> (where: usize) =
116 where = value.where
117end position_of
118
119--- Return the severity recorded in an entry.
120public severity_of: (value: entry) -> (kind: severity) =
121 kind = value.kind
122end severity_of
123
124--- Return the number of message bytes retained in an entry.
125public message_length: (value: entry) -> (length: usize) =
126 length = value.what_length
127end message_length
128
129--- Read one retained message byte. Reports `mem.out_of_bounds` past the
130--- message length.
131public message_byte: (value: entry, index: usize)
132 -> (byte: u8) ! mem.out_of_bounds =
133 fail mem.out_of_bounds when index >= value.what_length
134 byte = value.what_bytes[index]
135end message_byte
136
137(fixed capacity: usize) bounded_value(capacity) is log
138 (note: bounded_note, failed: bounded_failed)
139
140streaming_value: type = struct
141 state: ptr any io.writer
142 where_to: ptr io.file
143 errors: usize
144end streaming_value
145
146--- Diagnostic provider borrowing a writer and destination handle. Emits
147--- severity-prefixed messages and propagates write failures to the caller.
148public streaming: type = streaming_value
149
150warning_prefix: [2]u8 = [87, 58]
151error_prefix: [2]u8 = [69, 58]
152separator: [1]u8 = [58]
153
154write_position: (state: any io.writer, where_to: io.file, value: usize)
155 -> none ! io.io_failed =
156 -- Twenty bytes hold every decimal digit of a 64-bit usize. Build
157 -- backwards so the provider receives exactly one position slice.
158 mut digits: [20]u8 = zeroed
159 mut start: usize = lenof digits
160 mut remaining: usize = value
161 loop do
162 dec start
163 digits[start] = u8(remaining % 10) + 48
164 remaining /= 10
165 break when remaining == 0
166 end loop
167 try state.write(where_to, digits[start..<lenof digits])
168end write_position
169
170--- Create a streaming logger over an erased writer and file identifier. Keep
171--- both referenced bindings and the underlying provider alive while logging.
172---
173--- A streaming log needs only a writer. A root holding an `any io.world`
174--- builds the writer from the same provider pointer; the two erased values
175--- do not convert.
176public to: (state: ptr any io.writer, where_to: ptr io.file)
177 -> (result: streaming from state, where_to) =
178 result = streaming(state: state, where_to: where_to, errors: 0)
179end to
180
181stream_note: (self: ptr mut streaming, where: text.position,
182 kind: severity, what: []u8) -> none ! io.io_failed =
183 state: ptr any io.writer = self.val.state
184 where_to: ptr io.file = self.val.where_to
185 if kind == error then
186 inc self.val.errors
187 try state.val.write(where_to.val, error_prefix[0..<2])
188 else
189 try state.val.write(where_to.val, warning_prefix[0..<2])
190 end if
191
192 try write_position(state.val, where_to.val, text.ordinal(where))
193 try state.val.write(where_to.val, separator[0..<1])
194 try state.val.write(where_to.val, what)
195end stream_note
196
197stream_failed: (self: ptr streaming) -> (yes: bool) =
198 yes = self.val.errors > 0
199end stream_failed
200
201streaming_value is log (note: stream_note, failed: stream_failed)