Skip to content
← Back

src/analysis/command_despatcher.ghul

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