Appearance
| 1 | namespace Analysis is | |
| 2 | use System.Exception | |
| 3 | use IO.Std | |
| 4 | ||
| 5 | use Collections.Iterable | |
| 6 | ||
| 7 | use Pair = Collections.KeyValuePair | |
| 8 | ||
| 9 | use Ghul.Pipes | |
| 10 | ||
| 11 | use IoC | |
| 12 | use Logging | |
| 13 | use Source | |
| 14 | use Compiler | |
| 15 | ||
| 16 | class COMMAND_DESPATCHER( | |
| 17 | _reader: REQUEST_READER, | |
| 18 | _writer: IO.TextWriter, | |
| 19 | _timers: TIMERS, | |
| 20 | _log: IO.TextWriter, | |
| 21 | _watchdog: WATCHDOG, | |
| 22 | _full_compiler: FULL_COMPILER, | |
| 23 | _want_stats_report: bool, | |
| 24 | _idle_timeout_seconds: int | |
| 25 | ) is | |
| 26 | _command_map: Collections.MAP[System.Type,CommandHandler] | |
| 27 | ||
| 28 | // The request variants that may be answered part-way through a | |
| 29 | // compile. See serve_pending_queries for what qualifies. | |
| 30 | _interleavable: Collections.SET[System.Type] | |
| 31 | ||
| 32 | _last_report_time: System.DateTime | |
| 33 | ||
| 34 | _listening: bool | |
| 35 | ||
| 36 | // Guards against re-entering the interleave: a query served from | |
| 37 | // inside a compile can reach a handler that compiles in turn, | |
| 38 | // and its file boundaries would otherwise start another round. | |
| 39 | _serving_pending: bool | |
| 40 | ||
| 41 | init(..) is | |
| 42 | _last_report_time = System.DateTime.now | |
| 43 | _command_map = Collections.MAP[System.Type,CommandHandler]() | |
| 44 | _interleavable = Collections.SET[System.Type]() | |
| 45 | ||
| 46 | _reader.start() | |
| 47 | si | |
| 48 | ||
| 49 | // The compiler's own version, sourced from the entry assembly's | |
| 50 | // AssemblyInformationalVersionAttribute (same source as the driver's | |
| 51 | // startup log line). Exposed in the LISTEN frame so a client can | |
| 52 | // reject a version older than the minimum it supports; a bad | |
| 53 | // version-negotiation error is a lot clearer than a mysterious hang. | |
| 54 | current_compiler_version() -> string? static is | |
| 55 | let attribute = | |
| 56 | System.Reflection.Assembly.get_entry_assembly()! | |
| 57 | .get_custom_attributes(typeof System.Reflection.AssemblyInformationalVersionAttribute, false) | |
| 58 | |> map(a => cast System.Reflection.AssemblyInformationalVersionAttribute?(a)!) | |
| 59 | |> first() | |
| 60 | ||
| 61 | if attribute? then | |
| 62 | return attribute.informational_version | |
| 63 | fi | |
| 64 | ||
| 65 | return null | |
| 66 | si | |
| 67 | ||
| 68 | // Register a handler keyed by the request variant type. A deserialized | |
| 69 | // request routes to its handler by runtime type; the variant's | |
| 70 | // unqualified class name (e.g. `HOVER`, `FORMAT_RANGE`) labels the | |
| 71 | // per-command timer in the stats report. | |
| 72 | add_handler(request_variant: System.Type, command_handler: CommandHandler) is | |
| 73 | add_handler(request_variant, command_handler, false) | |
| 74 | si | |
| 75 | ||
| 76 | // As above, with `can_interleave` marking the variant as one | |
| 77 | // serve_pending_queries may answer part-way through a compile. | |
| 78 | add_handler(request_variant: System.Type, command_handler: CommandHandler, can_interleave: bool) is | |
| 79 | assert !_command_map.contains_key(request_variant) else "replacing command handler for {request_variant.name}" | |
| 80 | ||
| 81 | _command_map[request_variant] = command_handler | |
| 82 | ||
| 83 | if can_interleave then | |
| 84 | _interleavable.add(request_variant) | |
| 85 | fi | |
| 86 | si | |
| 87 | ||
| 88 | // Wait for the next frame the reader thread queued, exiting | |
| 89 | // instead if none arrives within the idle timeout. | |
| 90 | // | |
| 91 | // Idle-exit lives here rather than in WATCHDOG because it is decided | |
| 92 | // by the absence of work rather than by anything a compile revealed, | |
| 93 | // and because this is the one place the analyser is provably between | |
| 94 | // requests: exiting from anywhere else could truncate a response. | |
| 95 | read_next_frame() -> QueuedFrame is | |
| 96 | let frame = | |
| 97 | _reader.take( | |
| 98 | if _idle_timeout_seconds <= 0 then | |
| 99 | 0 | |
| 100 | else | |
| 101 | _idle_timeout_seconds * 1000 | |
| 102 | fi | |
| 103 | ) | |
| 104 | ||
| 105 | if isa QueuedFrame.TIMED_OUT(frame) then | |
| 106 | exit_idle() | |
| 107 | fi | |
| 108 | ||
| 109 | return frame | |
| 110 | si | |
| 111 | ||
| 112 | // Announce the exit and go. Distinct from WATCHDOG.recycle, which | |
| 113 | // asks the client to relaunch immediately: this asks it to wait | |
| 114 | // until it has something to ask, and the exit status says the same | |
| 115 | // thing to a client that was not reading at the time. | |
| 116 | exit_idle() is | |
| 117 | _log.write_line("ghūl: exiting after {_idle_timeout_seconds}s idle") | |
| 118 | _log.flush() | |
| 119 | ||
| 120 | Protocol.JSON_PROTOCOL.write_response( | |
| 121 | _writer, | |
| 122 | Protocol.Response.EXIT("idle for {_idle_timeout_seconds}s") | |
| 123 | ) | |
| 124 | ||
| 125 | System.Environment.exit(0) | |
| 126 | si | |
| 127 | ||
| 128 | poll() -> bool is | |
| 129 | if !_listening then | |
| 130 | let listen_capabilities = Collections.LIST[string]() | |
| 131 | // The incremental body re-walk is on by default; this | |
| 132 | // capability tells a client the analyser supports it. | |
| 133 | listen_capabilities.add("incremental-analysis") | |
| 134 | ||
| 135 | // Quick fixes are answered by the `code_actions` request | |
| 136 | // rather than attached to reported diagnostics. A client | |
| 137 | // that does not see this must not send the request: the | |
| 138 | // analyser it is talking to would reject the frame, and | |
| 139 | // read its own fixes out of the diagnostics instead. | |
| 140 | listen_capabilities.add("code-actions") | |
| 141 | ||
| 142 | // An edit can be sent as the span that changed rather | |
| 143 | // than as the file's whole text. A client that does not | |
| 144 | // see this must not send the request: unknown JSON | |
| 145 | // members are ignored, so an analyser without the | |
| 146 | // capability reads a delta as an edit whose source is | |
| 147 | // empty and blanks the file. | |
| 148 | listen_capabilities.add("edit-deltas") | |
| 149 | ||
| 150 | // A query sent while a compile is running is answered | |
| 151 | // during it rather than after it. Nothing about the wire | |
| 152 | // changes - a client that ignores this and waits for its | |
| 153 | // response as before is served exactly as it was - but a | |
| 154 | // client that pipelines queries against a compile can | |
| 155 | // know it will get answers rather than a stall. | |
| 156 | listen_capabilities.add("mid-compile-queries") | |
| 157 | ||
| 158 | // An edit written while a compile is running cuts that | |
| 159 | // compile short instead of queueing behind it. A client | |
| 160 | // that does not see this must hold its edits until the | |
| 161 | // compile's response arrives: the analyser it is talking | |
| 162 | // to would run the whole compile computing diagnostics | |
| 163 | // the edit has already made stale. | |
| 164 | listen_capabilities.add("compile-abort") | |
| 165 | ||
| 166 | // Inlay hints can be asked for by range. Unknown JSON | |
| 167 | // members are ignored, so a range sent to an analyser | |
| 168 | // without the capability is simply dropped and the answer | |
| 169 | // comes back whole-file; the capability lets a client | |
| 170 | // that checks avoid paying transport for hints outside | |
| 171 | // the viewport it asked about. | |
| 172 | listen_capabilities.add("inlay-hint-ranges") | |
| 173 | ||
| 174 | // A client adding references to a running analyser: an | |
| 175 | // analyser without it fails the request as unknown. | |
| 176 | listen_capabilities.add("add-references") | |
| 177 | ||
| 178 | Protocol.JSON_PROTOCOL.write_response(_writer, Protocol.Response.LISTEN(listen_capabilities, current_compiler_version())) | |
| 179 | ||
| 180 | _listening = true | |
| 181 | fi | |
| 182 | ||
| 183 | let frame = read_next_frame() | |
| 184 | ||
| 185 | if let parse_error: QueuedFrame.PARSE_ERROR = frame then | |
| 186 | // A frame the analyser couldn't deserialize is a protocol | |
| 187 | // mismatch, not analyser instability - typically a client | |
| 188 | // sending a request variant the analyser doesn't know | |
| 189 | // (older analyser + newer client). Answer with a discrete | |
| 190 | // error frame so the client can surface it, and keep the | |
| 191 | // process alive to serve subsequent requests. | |
| 192 | _log.write_line("ghūl: could not parse request: {parse_error.message}") | |
| 193 | ||
| 194 | Protocol.JSON_PROTOCOL.write_response( | |
| 195 | _writer, | |
| 196 | Protocol.Response.ERROR("parse", parse_error.message, "") | |
| 197 | ) | |
| 198 | ||
| 199 | _watchdog.on_operation_complete(_writer) | |
| 200 | ||
| 201 | return true | |
| 202 | fi | |
| 203 | ||
| 204 | let queued = cast QueuedFrame.REQUEST?(frame) | |
| 205 | ||
| 206 | if !queued? then | |
| 207 | _log.write_line("ghūl: reader is at end") | |
| 208 | _log.flush() | |
| 209 | ||
| 210 | return false | |
| 211 | fi | |
| 212 | ||
| 213 | let request = queued.request | |
| 214 | ||
| 215 | despatch(request) | |
| 216 | ||
| 217 | _watchdog.on_operation_complete(_writer) | |
| 218 | ||
| 219 | if _want_stats_report then | |
| 220 | let elapsed_since_last_report = System.DateTime.now.subtract(_last_report_time) | |
| 221 | ||
| 222 | if elapsed_since_last_report.total_seconds > 60.0D then | |
| 223 | _last_report_time = System.DateTime.now | |
| 224 | ||
| 225 | _log.write(_timers) | |
| 226 | fi | |
| 227 | fi | |
| 228 | ||
| 229 | return true | |
| 230 | si | |
| 231 | ||
| 232 | // Route one request to its handler and time it. Reached both from | |
| 233 | // poll and, for the interleavable variants, from | |
| 234 | // serve_pending_queries. | |
| 235 | despatch(request: Protocol.Request) is | |
| 236 | let request_variant = request.get_type() | |
| 237 | let command_name = request_variant.name | |
| 238 | ||
| 239 | if !_command_map.contains_key(request_variant) then | |
| 240 | // Registered variants with no handler are also a protocol | |
| 241 | // mismatch - the request kind exists on the wire but this | |
| 242 | // build doesn't route it anywhere. Same treatment as an | |
| 243 | // unparseable frame: emit an error and keep going. | |
| 244 | _log.write_line("ghūl: no handler found for command: '{command_name}'") | |
| 245 | ||
| 246 | Protocol.JSON_PROTOCOL.write_response( | |
| 247 | _writer, | |
| 248 | Protocol.Response.ERROR("unknown_command", "no handler registered for request kind {command_name}", command_name) | |
| 249 | ) | |
| 250 | ||
| 251 | return | |
| 252 | fi | |
| 253 | ||
| 254 | let handler = _command_map[request_variant] | |
| 255 | ||
| 256 | _timers.start(command_name) | |
| 257 | handler.handle(request, _writer) | |
| 258 | _timers.finish(command_name) | |
| 259 | si | |
| 260 | ||
| 261 | // Whether an edit is waiting on the pipe, superseding whatever is | |
| 262 | // being computed now. Read at the same per-file boundaries the | |
| 263 | // queries are served at, by a compile that can abandon its walk. | |
| 264 | is_superseded() -> bool => | |
| 265 | _reader.has_pending( | |
| 266 | r => | |
| 267 | isa Protocol.Request.EDIT(r) \/ | |
| 268 | isa Protocol.Request.EDIT_DELTA(r) | |
| 269 | ) | |
| 270 | ||
| 271 | // Answer the queries already waiting on the pipe, from the state | |
| 272 | // the analyser holds right now, and return to what was being done. | |
| 273 | // | |
| 274 | // Called at the per-file boundaries of the compile-expressions | |
| 275 | // walk, and only there. That window is the one place mid-compile | |
| 276 | // state is as consistent as it is between requests: every file's | |
| 277 | // declarations, ancestries and types are resolved before the walk | |
| 278 | // starts, and the scope and namespace stacks are back at their | |
| 279 | // base between files. Earlier passes of a from-scratch rebuild | |
| 280 | // are repopulating the symbol table and answer nothing reliably. | |
| 281 | // | |
| 282 | // Only the variants registered as interleavable are taken, and | |
| 283 | // what qualifies is a query that fails safe: one whose answer is | |
| 284 | // either right or absent. A whole-project query - references, | |
| 285 | // implementations, rename - would instead answer from a use map | |
| 286 | // that is still being rebuilt, and an incomplete set of edit | |
| 287 | // sites reads as a complete one. Those keep waiting. | |
| 288 | // | |
| 289 | // Recompiles are suspended for the duration, so a query that | |
| 290 | // misses answers from current state rather than starting a | |
| 291 | // second compile inside this one. | |
| 292 | serve_pending_queries() is | |
| 293 | if _serving_pending then | |
| 294 | return | |
| 295 | fi | |
| 296 | ||
| 297 | _serving_pending = true | |
| 298 | ||
| 299 | let symbol_table = IoC.CONTAINER.instance.symbol_table | |
| 300 | let scope_mark = symbol_table.mark_scope_stack() | |
| 301 | ||
| 302 | _full_compiler.suspend_recompiles = true | |
| 303 | ||
| 304 | try | |
| 305 | while let request = _reader.take_if(r => _interleavable.contains(r.get_type())) do | |
| 306 | _timers.bump("interleaved-query") | |
| 307 | ||
| 308 | despatch(request) | |
| 309 | od | |
| 310 | finally | |
| 311 | // A handler that walks a file - completion, signature | |
| 312 | // help - pushes scopes as it goes, and one that threw | |
| 313 | // part-way through would otherwise leave them for the | |
| 314 | // rest of the compile to trip over. | |
| 315 | symbol_table.release_scope_stack(scope_mark) | |
| 316 | IoC.CONTAINER.instance.namespaces.pop_all_namespaces() | |
| 317 | ||
| 318 | _full_compiler.suspend_recompiles = false | |
| 319 | ||
| 320 | _serving_pending = false | |
| 321 | yrt | |
| 322 | si | |
| 323 | si | |
| 324 | si |