Skip to content
← Back

src/logging/logger.ghul

1
namespace Logging is
2
use System.Exception
3
use System.NotImplementedException
4
use Ghul.Disposable
5
6
use Collections.LIST
7
use Collections.STACK
8
9
use IO.Std
10
use IO.TextWriter
11
use IO.StringWriter
12
13
use Source
14
15
struct DEBUG_THEN_EXIT: Disposable is
16
_initialized: bool
17
_depth: int
18
19
init(depth: int) is
20
_initialized = true
21
_depth = depth
22
si
23
24
init() is
25
// not debugging, so dispose doesn't need to do anything
26
si
27
28
dispose() is
29
if _initialized then
30
while _debug_depth > _depth do
31
debug_exit()
32
od
33
fi
34
35
_initialized = false
36
si
37
si
38
39
_debug_depth: int
40
41
debug_enter() -> DEBUG_THEN_EXIT is
42
debug_always(">>>")
43
let result = DEBUG_THEN_EXIT(_debug_depth)
44
45
_debug_depth = _debug_depth + 1
46
47
return result
48
si
49
50
in_debug() -> bool => _debug_depth > 0
51
52
debug_enter(want_debug: bool) -> DEBUG_THEN_EXIT =>
53
if want_debug then
54
debug_enter()
55
else
56
DEBUG_THEN_EXIT()
57
fi
58
59
debug_exit() is
60
_debug_depth = _debug_depth - 1
61
debug_always("<<<")
62
si
63
64
debug_exit(want_debug: bool) is
65
if want_debug then
66
debug_exit()
67
fi
68
si
69
70
debug_reset() is
71
_debug_depth = 0
72
si
73
74
debug_indent() is
75
if _debug_depth > 0 then
76
_debug_depth = _debug_depth + 1
77
fi
78
si
79
80
debug_unindent() is
81
if _debug_depth > 0 then
82
_debug_depth = _debug_depth - 1
83
fi
84
si
85
86
debug(message: string) is
87
if _debug_depth > 0 then
88
debug_always(message)
89
fi
90
si
91
92
debug_always(message: string) is
93
for m in message.replace_line_endings("\n").split(['\n']) do
94
for i in 0.._debug_depth do
95
IO.Std.error.write(" ")
96
od
97
98
IO.Std.error.write_line(m)
99
od
100
101
IO.Std.error.flush()
102
si
103
104
trait Logger is
105
is_poisoned: bool
106
has_consumed_error: bool
107
has_consumed_any: bool
108
109
error_count: int
110
any_errors: bool
111
112
// The source paths that currently hold at least one error-severity
113
// diagnostic. The analysis-mode incremental interface path uses
114
// this as a conservative dependent set: a body that failed to bind
115
// a name recorded no reference, so reference-based dependency
116
// scans cannot see it, yet an interface edit may be exactly what
117
// cures (or should re-report) its error.
118
paths_with_errors: Collections.Iterable[string]
119
120
// Every source path with a retained diagnostics list at the
121
// current speculation depth — includes a path checked and found
122
// clean, since `clear` creates the (possibly empty) list before
123
// any diagnostic is added for it.
124
diagnostic_paths: Collections.Iterable[string]
125
126
// Every diagnostic message recorded for `path` at the current
127
// speculation depth, in report order. Walked directly by
128
// DIAGNOSTICS_COLLECTOR — no text formatter in between.
129
diagnostics_for(path: string) -> Collections.Iterable[DIAGNOSTIC_MESSAGE]
130
131
// True iff this speculation level recorded no errors and didn't
132
// consume any wild / inferred / placeholder types. Used by the
133
// iterative inference loop in COMPILE_EXPRESSIONS to detect
134
// convergence: a body walk that was clean is one we don't need
135
// to retry.
136
is_clean: bool
137
138
depth: int
139
140
speculate()
141
roll_back() -> DIAGNOSTICS_STATE
142
commit()
143
mark() -> int
144
release(mark: int)
145
146
speculate_then_commit() -> LOGGER_SPECULATE_THEN_COMMIT
147
speculate_then_backtrack() -> LOGGER_SPECULATE_THEN_BACKTRACK
148
mark_then_release() -> MARK_THEN_RELEASE
149
150
// True while an analysis-mode (language-service) request is
151
// being served, false during batch compilation.
152
is_analysis: bool
153
154
// The set of files the client currently has open. Replaced whole by
155
// each client update. Editor-only hints are suppressed at the source
156
// for any file not in this set, since they are invisible for a file
157
// the user is not viewing. Returns the paths whose open state
158
// changed, so the caller can invalidate the expression walk of each
159
// - and only of each: no other file's hint output was gated on the
160
// part of the set that moved.
161
set_open_files(paths: Collections.Iterable[string]) -> Collections.List[string]
162
is_file_open(path: string) -> bool
163
164
start_analysis()
165
end_analysis()
166
167
set_is_compiling_expressions(value: bool)
168
169
merge(state: DIAGNOSTICS_STATE)
170
171
exception(location: LOCATION, exception: Exception, message: string)
172
// Report an exception thrown by work that may have left
173
// speculation states stacked. The stack is unwound to `mark`
174
// first, so the diagnostic lands in the state the caller keeps
175
// rather than in one it is about to discard.
176
exception(mark: int, location: LOCATION, exception: Exception, message: string)
177
fatal(location: LOCATION, message: string)
178
error(location: LOCATION, message: string)
179
// Same as error(location, message), plus a second location this
180
// diagnostic refers to (a prior declaration, an overridden
181
// member) - rendered by a capable client as a jump-to link, and
182
// appended to the line by the text formatters. The primary
183
// `message` must still name the thing the related location
184
// points at; the related location says where, not what.
185
error(location: LOCATION, message: string, related_location: LOCATION, related_message: string)
186
// Same as error(location, message, related_location,
187
// related_message), plus several related locations - for a
188
// diagnostic whose counterpart sites are peers (a duplicate
189
// declaration reported at every site) rather than one
190
// authoritative prior declaration.
191
error(location: LOCATION, message: string, related: LIST[RELATED_LOCATION])
192
// A lexer-produced error, written to the base diagnostics state so it
193
// survives parser speculation rollback (see DIAGNOSTICS_STORE).
194
lexer_error(location: LOCATION, message: string)
195
// An error about the compilation environment rather than the source,
196
// written to the base diagnostics state like a lexer error and for the
197
// same reason: it is produced once, by work that is cached, so it
198
// cannot duplicate, and whichever speculative branch happened to
199
// trigger that work must not be able to discard it.
200
environment_error(location: LOCATION, message: string)
201
warn(location: LOCATION, message: string)
202
warn(location: LOCATION, code: string, message: string)
203
// Same as warn(location, code, message), plus a related location -
204
// see error(location, message, related_location, related_message).
205
warn(location: LOCATION, code: string, message: string, related_location: LOCATION, related_message: string)
206
// Same as warn(location, code, message), plus several related
207
// locations - for a diagnostic that points at more than one other
208
// declaration at once (every field a constructor leaves
209
// unassigned, say).
210
warn(location: LOCATION, code: string, message: string, related: LIST[RELATED_LOCATION])
211
poison(location: LOCATION, message: string)
212
info(location: LOCATION, message: string)
213
info(location: LOCATION, code: string, message: string)
214
hint(location: LOCATION, message: string)
215
hint(location: LOCATION, code: string, message: string)
216
// Same as hint(location, code, message), plus a related location -
217
// see error(location, message, related_location, related_message).
218
hint(location: LOCATION, code: string, message: string, related_location: LOCATION, related_message: string)
219
220
// Record an editor-only inlay hint at `location`: a terse `label`
221
// (the ghost text) plus an optional `detail` (hover tooltip). Never
222
// published as a diagnostic; surfaced only by the INLAY_HINTS query.
223
// Callers gate on `want_hint_for` first, exactly as for a hint.
224
inlay(location: LOCATION, code: string, label: string, detail: string?)
225
226
// Every inlay recorded for `path` in the current state, in
227
// emission order. Consulted by the INLAY_HINTS handler.
228
inlays_for(path: string) -> Collections.Iterable[INLAY]
229
230
// Drop the inlay carriers recorded for `path` whose code begins
231
// with `code`. A pass re-run against one file calls this first so
232
// it replaces its carriers rather than stacking a second copy;
233
// a prefix because one pass emits suffixed codes for variants of
234
// one carrier (an accessor's half) alongside its base code.
235
clear_inlays(path: string, code: string)
236
237
238
// Whether an inlay kind (an INLAY_KINDS name) is collected.
239
// Kinds are enabled and disabled process-wide by the driver's
240
// `--inlay <kind>` / `--no-inlay <kind>` flags; a disabled kind
241
// is never recorded, so it costs nothing to collect or serve.
242
// The default is "on", so a logger that does not track kinds
243
// keeps every hint.
244
is_inlay_kind_enabled(kind: string) -> bool => true
245
246
// Turn a kind on or off for this process. A logger without
247
// per-kind state ignores these.
248
enable_inlay_kind(kind: string) is si
249
250
disable_inlay_kind(kind: string) is si
251
252
// Record a statement boundary the parser inferred - a terminator
253
// left off at end of line - at `location`. Called from the
254
// parser's terminator sites; a logger that does not serve the
255
// terminator inlay kind (it is disabled, or the logger has no
256
// per-file state) records nothing.
257
note_inferred_terminator(location: LOCATION) is si
258
259
// The inferred-terminator positions recorded for `path`, as
260
// LOCATION-packed ints in source order. Served by the
261
// INLAY_HINTS handler when the terminator kind is enabled.
262
inferred_terminators_for(path: string) -> Collections.List[int] =>
263
Collections.LIST[int]()
264
265
// Suppress every subsequent warn/info/hint carrying this code.
266
// Accumulating — calling twice with different codes silences both.
267
// Populated by `--suppress` / `--no-warn-*` from the driver.
268
suppress(code: string)
269
270
// Remove a code from the global suppression set. Populated by
271
// `--warn <slug,slug,...>` from the driver; the way to enable a
272
// warning that is suppressed by default (redundant-semicolon).
273
unsuppress(code: string)
274
275
is_suppressed(code: string?) -> bool
276
277
// Promote warnings to errors. `set_all_warnings_are_errors(true)`
278
// routes every subsequent `warn(...)` to error level; `promote_to_error(code)`
279
// does the same only for warns carrying that slug. Populated by
280
// `--warn-as-error <slug,slug,...>` from the driver, with the literal
281
// slug `all` mapping to `set_all_warnings_are_errors(true)`.
282
// Suppression wins: a warning silenced by `suppress` or by an
283
// `@suppress(...)` region is never resurrected as an error.
284
set_all_warnings_are_errors(value: bool)
285
promote_to_error(code: string)
286
287
// Demote warnings carrying this code to editor-only hints.
288
// Populated by `--warn-as-hint <slug,slug,...>` from the driver.
289
// A demoted warn is emitted as a hint under the same gate as a
290
// native hint: dropped entirely in batch compilation, surfaced in
291
// analysis mode only for a file the client has open. Suppression
292
// still wins: a code that is both suppressed and demoted is
293
// dropped. Demotion wins over promotion to error.
294
demote_to_hint(code: string)
295
296
// Demote warnings carrying this code to info-level diagnostics.
297
// Populated by `--warn-as-info <slug,slug,...>` from the driver.
298
// Unlike a hint, info stays a normal batch-visible diagnostic:
299
// it is not subject to the editor-only gate. Suppression still
300
// wins; a code demoted to a hint takes precedence over info.
301
demote_to_info(code: string)
302
303
// Whether a hint at `location` should be generated: true while
304
// serving an analysis request for a file the client has open. The
305
// single gate every hint site consults before doing any
306
// hint-specific work.
307
want_hint_for(location: LOCATION?) -> bool
308
309
// Lexical-scope suppression: anything emitted at a location
310
// covered by `region` carrying `code` is dropped, on top of the
311
// global `suppress` set. Populated by the `@suppress(...)`
312
// pragma collector. The location-aware `is_suppressed` consults
313
// both the global set and the registered regions. Regions are
314
// stored per source file so analysis-mode re-walks can clear
315
// exactly the file being re-walked.
316
register_suppression_region(region: LOCATION, code: string)
317
clear_suppression_regions(path: string)
318
clear_suppression_regions()
319
is_suppressed(code: string?, location: LOCATION) -> bool
320
321
poison(location: LOCATION)
322
mark_consumed_error()
323
mark_consumed_any()
324
325
// Convenience: mark consumed-any iff `consumed` is true.
326
// Removes the `if add_*(...) then logger.mark_consumed_any() fi`
327
// boilerplate at sites that record progress against a
328
// Variable accumulator.
329
mark_consumed_any_if(consumed: bool)
330
331
clear_consumed_error()
332
clear_consumed_any()
333
334
write_poison_messages()
335
336
clear(path: string, analysis_only: bool)
337
338
clear_global_declaration_diagnostics(path: string)
339
clear_expression_diagnostics(path: string)
340
clear_expression_diagnostics_within(path: string, span: Source.LOCATION)
341
342
write_all_diagnostics(writer: TextWriter, formatter: DiagnosticFormatter)
343
si
344
345
struct LOGGER_SPECULATE_THEN_COMMIT: Disposable is
346
_logger: Logger?
347
348
has_consumed_any: bool => _logger!.has_consumed_any
349
has_consumed_error: bool => _logger!.has_consumed_error
350
351
any_errors: bool => _logger!.any_errors
352
353
is_speculating: bool => _logger != null
354
355
init(logger: Logger) is
356
_logger = logger
357
_logger.speculate()
358
si
359
360
backtrack() -> DIAGNOSTICS_STATE is
361
let result = _logger!.roll_back()
362
_logger = null
363
return result
364
si
365
366
backtrack_if_speculating() -> DIAGNOSTICS_STATE? is
367
if _logger != null then
368
return backtrack()
369
else
370
return null
371
fi
372
si
373
374
backtrack_and_restart() -> DIAGNOSTICS_STATE is
375
let logger = _logger!
376
let result = logger.roll_back()
377
logger.speculate()
378
return result
379
si
380
381
commit() is
382
_logger!.commit()
383
_logger = null
384
si
385
386
cancel() is
387
_logger = null
388
si
389
390
dispose() is
391
if _logger != null then
392
_logger.commit()
393
_logger = null
394
fi
395
si
396
si
397
398
struct LOGGER_SPECULATE_THEN_BACKTRACK: Disposable is
399
_logger: Logger?
400
401
has_consumed_any: bool => _logger!.has_consumed_any
402
has_consumed_error: bool => _logger!.has_consumed_error
403
404
any_errors: bool => _logger!.any_errors
405
406
is_speculating: bool => _logger != null
407
408
init(logger: Logger) is
409
_logger = logger
410
_logger.speculate()
411
si
412
413
backtrack() -> DIAGNOSTICS_STATE is
414
let result = _logger!.roll_back()
415
_logger = null
416
return result
417
si
418
419
backtrack_and_restart() -> DIAGNOSTICS_STATE is
420
let logger = _logger!
421
let result = logger.roll_back()
422
logger.speculate()
423
return result
424
si
425
426
commit() is
427
_logger!.commit()
428
_logger = null
429
si
430
431
cancel() is
432
_logger = null
433
si
434
435
dispose() is
436
if _logger != null then
437
_logger.roll_back()
438
_logger = null
439
fi
440
si
441
si
442
443
struct MARK_THEN_RELEASE: Disposable is
444
_logger: Logger
445
_mark: int
446
447
init(logger: Logger) is
448
_logger = logger
449
_mark = _logger.mark()
450
si
451
452
dispose() is
453
_logger.release(_mark)
454
si
455
si
456
si