Appearance
| 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 |