Landin library reference source

core/diag/diag.ldn

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)