Skip to content
← Back

src/analysis/command_handlers.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 IoC
10
use Logging
11
use Source
12
use Compiler
13
14
use Ghul.Pipes
15
16
use Protocol.JSON_PROTOCOL
17
18
// Generic base for every command handler. The despatcher holds handlers as
19
// the non-generic CommandHandler trait and hands each a deserialized
20
// Protocol.Request; this base does the single cast to the concrete request
21
// type T and forwards to handle_request, so no individual handler casts.
22
class RequestHandler[T]: CommandHandler is
23
init() is si
24
25
handle(request: Protocol.Request, writer: IO.TextWriter) is
26
handle_request(cast T(request), writer)
27
si
28
29
handle_request(request: T, writer: IO.TextWriter)
30
si
31
32
class HOVER_HANDLER(
33
_watchdog: WATCHDOG,
34
_timers: TIMERS,
35
_symbol_use_locations: Semantic.SYMBOL_USE_LOCATIONS,
36
_full_compiler: FULL_COMPILER
37
): RequestHandler[Protocol.Request.HOVER] is
38
super()
39
40
handle_request(request: Protocol.Request.HOVER, writer: IO.TextWriter) is
41
let path = request.path
42
let line = request.line
43
let column = request.column
44
45
let signature: string mut = ""
46
let kind_label: string? mut = null
47
let description: string mut = ""
48
49
try
50
if !_watchdog.want_restart then
51
let hover mut = _symbol_use_locations.find_hover_use(path, line, column)
52
53
if !hover? then
54
_full_compiler.compile_all(writer, path)
55
56
hover = _symbol_use_locations.find_hover_use(path, line, column)
57
fi
58
59
if hover? then
60
signature = SIGNATURE_DOC.build(hover)
61
kind_label = hover.kind_label
62
description = hover.description
63
fi
64
fi
65
catch ex: Exception
66
debug_always("HOVER caught: {ex.get_type()} {ex.message}")
67
68
_watchdog.request_restart()
69
yrt
70
71
Std.error.flush()
72
73
JSON_PROTOCOL.write_response(writer, Protocol.Response.HOVER(signature, kind_label, description))
74
si
75
si
76
77
// Dumps every HOVER_USE recorded for one file. Unlike hover it takes no
78
// position and never falls back to a recompile; the caller drives an EDIT
79
// (and/or COMPILE) first. Lets a batch consumer — the ghul.dev example
80
// pipeline — collect a file's hovers in one response. Signatures are
81
// rendered against the same column budget hover uses, so a batch consumer
82
// lays a wide signature out over several lines exactly as an editor does.
83
class HOVERMAP_HANDLER(
84
_watchdog: WATCHDOG,
85
_symbol_use_locations: Semantic.SYMBOL_USE_LOCATIONS
86
): RequestHandler[Protocol.Request.HOVER_MAP] is
87
super()
88
89
handle_request(request: Protocol.Request.HOVER_MAP, writer: IO.TextWriter) is
90
let path = request.path
91
92
let entries = Collections.LIST[Protocol.HOVER_ENTRY]()
93
94
try
95
if !_watchdog.want_restart then
96
for entry in _symbol_use_locations.hover_uses_in_file(path) do
97
let location = entry.location
98
99
entries.add(
100
Protocol.HOVER_ENTRY(
101
location.start_line, location.start_column,
102
location.end_line, location.end_column,
103
SIGNATURE_DOC.build(entry.value),
104
entry.value.kind_label
105
)
106
)
107
od
108
fi
109
catch ex: Exception
110
debug_always("HOVERMAP caught: {ex.get_type()} {ex.message}")
111
112
_watchdog.request_restart()
113
yrt
114
115
Std.error.flush()
116
117
JSON_PROTOCOL.write_response(writer, Protocol.Response.HOVER_MAP(entries))
118
si
119
si
120
121
// Inlay-hint dump for VS Code's inlay-hints provider: reads the
122
// store's editor-only inlays recorded by the flow-narrowing sites
123
// during the last compile-expressions pass over `path`, optionally
124
// limited to a range - the editor's viewport. This is pure retrieval
125
// — no recompile — because the analyser has already run
126
// compile-expressions before serving any query for an open file.
127
class INLAY_HINTS_HANDLER(
128
_watchdog: WATCHDOG
129
): RequestHandler[Protocol.Request.INLAY_HINTS] is
130
super()
131
132
handle_request(request: Protocol.Request.INLAY_HINTS, writer: IO.TextWriter) is
133
let path = request.path
134
135
let hints = Collections.LIST[Protocol.INLAY_HINT]()
136
137
try
138
if !_watchdog.want_restart then
139
// Filter to the range before merging: both mergers
140
// group on exact position, so a range either contains
141
// a whole group or none of it. A merger that grouped
142
// by enclosing scope instead could see a group cut in
143
// half by this filter - pinned by the range tests.
144
let merged =
145
DEFINITION_VIRTUALITY_MERGER().merge(
146
NARROWING_INLAY_MERGER().merge(
147
INLAY_RANGE.filter(IoC.CONTAINER.instance.logger.inlays_for(path), request)
148
)
149
)
150
151
for i in merged do
152
hints.add(
153
Protocol.INLAY_HINT(
154
i.location.start_line,
155
i.location.start_column,
156
i.text,
157
i.detail ?? "",
158
i.code ?? ""
159
)
160
)
161
od
162
163
for t in _terminator_hints(request, path) do
164
hints.add(t)
165
od
166
fi
167
catch ex: Exception
168
debug_always("INLAY_HINTS caught: {ex.get_type()} {ex.message}")
169
170
_watchdog.request_restart()
171
yrt
172
173
Std.error.flush()
174
175
JSON_PROTOCOL.write_response(writer, Protocol.Response.INLAY_HINTS(hints))
176
si
177
178
// The inferred statement boundaries for `path`, one "∘︎" hint per
179
// boundary. These are homogeneous positions rather than
180
// text-carrying records, collected by the parser into their own
181
// sorted list - see DIAGNOSTICS_LIST. LOCATION's packed
182
// (line, column) form orders identically to the line-then-column
183
// range comparison, so the range test is a plain int comparison,
184
// and the list's source order lets the scan stop at the range's
185
// end. Served only when the terminator kind is enabled, which is
186
// also the only state in which the parser records them.
187
_terminator_hints(
188
request: Protocol.Request.INLAY_HINTS,
189
path: string
190
) -> Collections.LIST[Protocol.INLAY_HINT] static is
191
let result = Collections.LIST[Protocol.INLAY_HINT]()
192
193
let logger = IoC.CONTAINER.instance.logger
194
195
if !logger.is_inlay_kind_enabled(Logging.INLAY_KINDS.TERMINATOR) then
196
return result
197
fi
198
199
let range = INLAY_RANGE.bounds(request)
200
201
let lower: int mut = 0
202
let upper: int mut = 0
203
204
if let (start_line, start_column, end_line, end_column) = range then
205
lower = Source.LOCATION.pair(start_line, start_column)
206
upper = Source.LOCATION.pair(end_line, end_column)
207
fi
208
209
let hint = p =>
210
Protocol.INLAY_HINT(
211
Source.LOCATION.line_of(p),
212
Source.LOCATION.column_of(p),
213
"∘︎",
214
"",
215
Logging.INLAY_KINDS.TERMINATOR
216
)
217
218
for p in logger.inferred_terminators_for(path) do
219
if !range? \/ (p >= lower /\ p < upper) then
220
result.add(hint(p))
221
elif p >= upper then
222
break
223
fi
224
od
225
226
return result
227
si
228
si
229
230
// Whole-file semantic-token dump for VS Code's semantic-tokens provider:
231
// one token per recorded HOVER_USE whose kind maps to an LSP token type.
232
// A symbol with no LSP-mappable kind contributes no token. Like HOVERMAP
233
// this triggers a recompile only when the hover map is empty (first open).
234
class SEMANTICTOKENS_HANDLER(
235
_watchdog: WATCHDOG,
236
_symbol_use_locations: Semantic.SYMBOL_USE_LOCATIONS,
237
_source_file_lookup: SourceFileLookup,
238
_full_compiler: FULL_COMPILER
239
): RequestHandler[Protocol.Request.SEMANTIC_TOKENS] is
240
_classifier: SEMANTIC_TOKEN_CLASSIFIER
241
242
super()
243
244
init(..) is
245
_classifier = SEMANTIC_TOKEN_CLASSIFIER()
246
si
247
248
handle_request(request: Protocol.Request.SEMANTIC_TOKENS, writer: IO.TextWriter) is
249
let path = request.path
250
251
let tokens = Collections.LIST[Protocol.SEMANTIC_TOKEN]()
252
253
try
254
if !_watchdog.want_restart then
255
let uses mut = _symbol_use_locations.hover_uses_in_file(path)
256
257
// Unlike HOVERMAP (built for the offline ghul.dev pipeline
258
// that explicitly drives EDIT + COMPILE), this serves a live
259
// IDE that asks for tokens whenever a document opens. The
260
// hover map is only populated by a whole-project compile; on
261
// first open we get nothing back. Fall back like hover does
262
// and force one — the user otherwise sees TextMate-only
263
// coloring until the first edit triggers a debounced compile.
264
if uses.count == 0 then
265
_full_compiler.compile_all(writer, path)
266
267
uses = _symbol_use_locations.hover_uses_in_file(path)
268
fi
269
270
// Discard wide spans that strictly contain another recorded
271
// use on the same line. Desugared expressions register a
272
// hover use whose location is the original source span —
273
// useful for hover, but for semantic tokens it produces a
274
// giant token covering identifiers and operators. The
275
// strict-containment filter drops the outer use while
276
// keeping every inner identifier.
277
let useful = OUTER_SPAN_FILTER().apply(uses, u => u.location)
278
279
for entry in useful do
280
let symbol = entry.value.symbol
281
let token_type = _classifier.token_type(symbol)
282
283
if token_type? then
284
let location = entry.location
285
let modifiers = _classifier.modifiers(symbol)
286
287
// The leading underscore on a non-public member is
288
// an access marker, not part of the name. Split it
289
// into its own `modifier` token so it colours like
290
// `public`/`private`, while the rest of the name
291
// keeps its kind colour. `prefix` is 0 for public
292
// symbols and for names with no leading underscore.
293
let prefix = _classifier.access_underscore_prefix_length(symbol)
294
295
if prefix > 0 /\ location.start_line == location.end_line then
296
tokens.add(
297
Protocol.SEMANTIC_TOKEN(
298
location.start_line, location.start_column,
299
location.end_line, location.start_column + prefix - 1,
300
"modifier", ""
301
)
302
)
303
304
tokens.add(
305
Protocol.SEMANTIC_TOKEN(
306
location.start_line, location.start_column + prefix,
307
location.end_line, location.end_column,
308
token_type, modifiers
309
)
310
)
311
else
312
tokens.add(
313
Protocol.SEMANTIC_TOKEN(
314
location.start_line, location.start_column,
315
location.end_line, location.end_column,
316
token_type, modifiers
317
)
318
)
319
fi
320
fi
321
od
322
323
// Contextual modifier keywords (`init`, `open`) look like
324
// identifiers to the lexer, so tmLanguage can't safely
325
// colour them without false-positives at genuine
326
// identifier sites. The `collect-modifier-keyword-
327
// locations` post-parse pass captured every site the
328
// parser accepted; overlay a `keyword` token at each.
329
let source_file = _source_file_lookup.find_source_file(path)
330
if source_file? then
331
if let source_file.contextual_modifier_locations? then
332
for location in contextual_modifier_locations do
333
tokens.add(
334
Protocol.SEMANTIC_TOKEN(
335
location.start_line, location.start_column,
336
location.end_line, location.end_column,
337
"keyword", ""
338
)
339
)
340
od
341
fi
342
fi
343
fi
344
catch ex: Exception
345
debug_always("SEMANTICTOKENS caught: {ex.get_type()} {ex.message}")
346
347
_watchdog.request_restart()
348
yrt
349
350
Std.error.flush()
351
352
JSON_PROTOCOL.write_response(writer, Protocol.Response.SEMANTIC_TOKENS(tokens))
353
si
354
355
si
356
357
// Maps a Symbol to the LSP semantic-token type and modifier strings the
358
// VS Code client expects. Returns null token_type for kinds VS Code has no
359
// slot for; the handler then skips that token.
360
class SEMANTIC_TOKEN_CLASSIFIER is
361
init() is si
362
363
token_type(symbol: Semantic.Symbols.Symbol?) -> string? is
364
if !symbol? then
365
return null
366
fi
367
368
let s = symbol.collapse_group_if_single_member()
369
370
// Innate functions are the built-in operators (`+`, `==`,
371
// `<>`, …); they are methods underneath, but reading as
372
// operators matches how they are written and used.
373
if s.is_innate then
374
return "operator"
375
fi
376
377
case s.symbol_kind
378
when Semantic.Symbols.SymbolKind.NAMESPACE then
379
return "namespace"
380
when Semantic.Symbols.SymbolKind.CLASS then
381
return "class"
382
when Semantic.Symbols.SymbolKind.INTERFACE then
383
return "interface"
384
when Semantic.Symbols.SymbolKind.STRUCT then
385
return "struct"
386
when Semantic.Symbols.SymbolKind.ENUM then
387
return "enum"
388
when Semantic.Symbols.SymbolKind.ENUM_MEMBER then
389
return "enumMember"
390
when Semantic.Symbols.SymbolKind.TYPE_PARAMETER then
391
return "typeParameter"
392
when Semantic.Symbols.SymbolKind.METHOD then
393
return "method"
394
when Semantic.Symbols.SymbolKind.FUNCTION then
395
return "function"
396
when Semantic.Symbols.SymbolKind.PROPERTY then
397
return "property"
398
when Semantic.Symbols.SymbolKind.FIELD then
399
return "property"
400
when Semantic.Symbols.SymbolKind.VARIABLE then
401
// Arguments are classified as `variable` rather than
402
// `parameter`: the useful axis in ghūl is immutable vs
403
// `mut` (carried by the `readonly` modifier below), and
404
// `variable.readonly` is styled by mainstream themes where
405
// `parameter.readonly` typically is not — so an argument
406
// shares a local's colouring instead of diverging from it.
407
return "variable"
408
else
409
return null
410
esac
411
si
412
413
// The number of leading underscores that mark a non-public member —
414
// the prefix the handler peels into its own `modifier` token so it
415
// colours as an access modifier. Zero unless the symbol reports itself
416
// non-public (`is_workspace_visible` is the compiler's own access
417
// determination, so a public symbol whose name happens to begin with
418
// `_`, and any type — always workspace-visible — keep the underscore
419
// as part of the name). Zero too when the whole name is underscores,
420
// leaving nothing to colour by kind.
421
access_underscore_prefix_length(symbol: Semantic.Symbols.Symbol) -> int is
422
let s = symbol.collapse_group_if_single_member()
423
424
if s.is_workspace_visible then
425
return 0
426
fi
427
428
let name = s.name
429
430
let count mut = 0
431
432
while count < name.length /\ name[count] == '_' do
433
count = count + 1
434
od
435
436
if count == name.length then
437
return 0
438
fi
439
440
return count
441
si
442
443
// Comma-separated LSP modifiers, possibly empty. Emits `static` for
444
// STATIC_* subclasses and `readonly` for an immutable (non-`mut`)
445
// local or argument.
446
modifiers(symbol: Semantic.Symbols.Symbol?) -> string is
447
if !symbol? then
448
return ""
449
fi
450
451
let s = symbol.collapse_group_if_single_member()
452
453
let parts = Collections.LIST[string]()
454
455
if
456
isa Semantic.Symbols.STATIC_METHOD(s) \/
457
isa Semantic.Symbols.STATIC_PROPERTY(s) \/
458
isa Semantic.Symbols.STATIC_FIELD(s) \/
459
isa Semantic.Symbols.CONSTANT_FIELD(s)
460
then
461
parts.add("static")
462
fi
463
464
// Locals and arguments are immutable unless declared `mut`. An
465
// immutable one cannot be reassigned, so it carries the
466
// `readonly` modifier that mainstream themes already style. The
467
// `is_local` guard restricts this to locals and arguments; fields
468
// are Variable subclasses too, but are members, freely
469
// assignable, and carry no such modifier.
470
if isa Semantic.Symbols.Variable(s) /\ s.is_local /\ !s.is_mutable_marked then
471
parts.add("readonly")
472
fi
473
474
if parts.count == 0 then
475
return ""
476
fi
477
478
return string.join(",", parts)
479
si
480
si
481
482
// Append compiler LOCATIONs as LOCATION_DTOs into the target list — used
483
// by every location-list response handler.
484
append_location_dtos(into: Collections.LIST[Protocol.LOCATION_DTO], locations: Iterable[LOCATION]) is
485
for location in locations do
486
into.add(
487
Protocol.LOCATION_DTO(
488
location.file_name,
489
location.start_line,
490
location.start_column,
491
location.end_line,
492
location.end_column
493
)
494
)
495
od
496
si
497
498
class DEFINITION_HANDLER(
499
_watchdog: WATCHDOG,
500
_symbol_use_locations: Semantic.SYMBOL_USE_LOCATIONS,
501
_full_compiler: FULL_COMPILER
502
): RequestHandler[Protocol.Request.DEFINITION] is
503
super()
504
505
handle_request(request: Protocol.Request.DEFINITION, writer: IO.TextWriter) is
506
let path = request.path
507
let line = request.line
508
let column = request.column
509
510
let locations = Collections.LIST[Protocol.LOCATION_DTO]()
511
512
try
513
if !_watchdog.want_restart then
514
let symbol mut = _symbol_use_locations.find_definition_from_use(path, line, column)
515
516
if !symbol? then
517
_full_compiler.compile_all(writer, path)
518
519
symbol = _symbol_use_locations.find_definition_from_use(path, line, column)
520
fi
521
522
if symbol? /\ !symbol.is_internal /\ !symbol.is_reflected then
523
append_location_dtos(locations, [symbol.location])
524
fi
525
fi
526
catch e: Exception
527
debug_always("DEFINITION caught: {e.get_type()}: {e.message}")
528
_watchdog.request_restart()
529
yrt
530
531
Std.error.flush()
532
533
JSON_PROTOCOL.write_response(writer, Protocol.Response.DEFINITION(locations))
534
si
535
si
536
537
class DECLARATION_HANDLER(
538
_watchdog: WATCHDOG,
539
_symbol_use_locations: Semantic.SYMBOL_USE_LOCATIONS,
540
_full_compiler: FULL_COMPILER
541
): RequestHandler[Protocol.Request.DECLARATION] is
542
super()
543
544
handle_request(request: Protocol.Request.DECLARATION, writer: IO.TextWriter) is
545
let locations = Collections.LIST[Protocol.LOCATION_DTO]()
546
547
try
548
let path = request.path
549
let line = request.line
550
let column = request.column
551
552
if !_watchdog.want_restart then
553
let symbol mut = _symbol_use_locations.find_definition_from_use(path, line, column)
554
555
if !symbol? then
556
_full_compiler.compile_all(writer, path)
557
558
symbol = _symbol_use_locations.find_definition_from_use(path, line, column)
559
fi
560
561
if symbol? then
562
append_location_dtos(locations, _symbol_use_locations.find_declarations_of_symbol(symbol))
563
fi
564
fi
565
catch e: Exception
566
debug_always("DECLARATION caught: {e.get_type()}: {e.message}")
567
yrt
568
569
Std.error.flush()
570
571
JSON_PROTOCOL.write_response(writer, Protocol.Response.DECLARATION(locations))
572
si
573
si
574
575
class FULL_COMPILER(
576
_watchdog: WATCHDOG,
577
_compiler: COMPILER,
578
_source_files: Iterable[SOURCE_FILE],
579
_timers: TIMERS
580
) is
581
// Set while a query is being answered part-way through a compile.
582
// The query-miss recompile below would otherwise start a second
583
// compile inside the first, against state the outer one is in the
584
// middle of rewriting. A miss answers from what is there instead.
585
suspend_recompiles: bool public
586
587
is_compiled_through_expressions(source_file: SOURCE_FILE) -> bool =>
588
_compiler.is_compiled_through_expressions(source_file)
589
590
// True when every known file has been walked through the
591
// compile-expressions pass — the precondition for a complete
592
// cross-file use map. A single-file EDIT's rebuild leaves the
593
// non-edited files at up-to-expressions, so their expression-position
594
// uses are absent until a full recompile restores them.
595
all_compiled_through_expressions() -> bool is
596
for source_file in _source_files do
597
if !_compiler.is_compiled_through_expressions(source_file) then
598
return false
599
fi
600
od
601
602
return true
603
si
604
605
compile_all(writer: IO.TextWriter) is
606
compile_all(writer, null)
607
si
608
609
// Recompile (driven by a query miss), then emit the diagnostics as a
610
// standalone diagnostics response with phase "query" — the client
611
// drains this before reading the query's own response.
612
compile_all(writer: IO.TextWriter, only_for_file_name: string?) is
613
if suspend_recompiles then
614
return
615
fi
616
617
// A query can arrive before the first EDIT has registered any
618
// source files. Building then is not a harmless no-op: it
619
// clears the symbol table and runs the pass barriers with no
620
// files queued, so a barrier hook that touches a reflected
621
// type materializes it against a symbol table missing the
622
// innate types (Ghul.REFERENCE and friends),
623
// and the half-built symbol stays cached for the life of the
624
// process. Answer from the empty state instead; the first
625
// real EDIT performs the initial build.
626
if !(_source_files |> any(f => true)) then
627
JSON_PROTOCOL.write_response(
628
writer,
629
Protocol.Response.DIAGNOSTICS(
630
Collections.LIST[Protocol.DIAGNOSTIC](),
631
Collections.LIST[string](),
632
"query",
633
0.0D,
634
false
635
)
636
)
637
638
return
639
fi
640
641
// The retained declare/resolve state - symbols, ancestries,
642
// override links, store-free bits - reflects every file's
643
// current source (every EDIT rebuild leaves it that way; only
644
// a rebuild in flight invalidates it), and the type comparison
645
// cache keyed on those symbols stays valid. The only thing a
646
// query can be missing is a file's expression-level walk: a
647
// rebuild compiles expressions for the edited files only,
648
// leaving every other file at up-to-expressions with no
649
// use-map entries. Compile just the files that lack it instead
650
// of clearing the world; when none lack it the miss is
651
// authoritative and a recompile could not change the answer.
652
if _compiler.are_tables_current then
653
compile_missing_expressions(writer, only_for_file_name)
654
655
return
656
fi
657
658
try
659
_timers.start("compile-all")
660
661
Semantic.Types.NAMED.clear_cache()
662
663
if !_watchdog.want_restart then
664
for i in _source_files do
665
i.want_compile_up_to_expressions = true
666
667
if !only_for_file_name? \/ i.file_name =~ only_for_file_name then
668
IoC.CONTAINER.instance.logger.clear(i.file_name, true)
669
i.want_compile_expressions = true
670
else
671
IoC.CONTAINER.instance.logger.clear_global_declaration_diagnostics(i.file_name)
672
i.want_compile_expressions = false
673
fi
674
od
675
676
let clear_state = Syntax.Process.CLEAR_STATE_VISITOR()
677
for i in _source_files do
678
clear_state.apply(i.definition)
679
od
680
681
IoC.CONTAINER.instance.state_store_registry.clear_all()
682
683
IoC.CONTAINER.instance.logger.start_analysis()
684
685
_compiler.clear_symbols()
686
687
_compiler.queue(_source_files)
688
689
_compiler.build()
690
691
let diagnostics = Collections.LIST[Protocol.DIAGNOSTIC]()
692
let checked_paths = Collections.LIST[string]()
693
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, diagnostics, checked_paths)
694
JSON_PROTOCOL.write_response(
695
writer,
696
Protocol.Response.DIAGNOSTICS(diagnostics, checked_paths, "query", 0.0D, false)
697
)
698
699
_watchdog.note_full_compile()
700
701
if !only_for_file_name? then
702
_compiler.is_full_compile_needed = false
703
fi
704
fi
705
706
catch e: Exception
707
debug_always("FULL_COMPILE caught: {e.get_type()}: {e.message}")
708
709
_watchdog.request_restart()
710
finally
711
IoC.CONTAINER.instance.logger.end_analysis()
712
713
_compiler.clear_queue()
714
715
_timers.finish("compile-all")
716
yrt
717
si
718
719
// The query-miss recompile when no interface change is pending:
720
// run compile-expressions for the files - the queried file, or
721
// all files for a cross-file query - that have not been walked
722
// through expressions in the current symbol generation. The
723
// retained symbol table is not cleared and no earlier pass is
724
// re-run. When every relevant file is already compiled through
725
// expressions there is nothing to do and no response is written -
726
// the caller answers from the maps as it would on a hit.
727
compile_missing_expressions(writer: IO.TextWriter, only_for_file_name: string?) is
728
let missing = Collections.LIST[SOURCE_FILE]()
729
730
for i in _source_files do
731
if
732
(!only_for_file_name? \/ i.file_name =~ only_for_file_name) /\
733
!_compiler.is_compiled_through_expressions(i)
734
then
735
missing.add(i)
736
fi
737
od
738
739
if missing.count == 0 then
740
// Timed as a pair so the skip shows up, with a count, in
741
// the analysis stats dump.
742
_timers.start("query-miss-authoritative")
743
_timers.finish("query-miss-authoritative")
744
745
return
746
fi
747
748
try
749
_timers.start("compile-on-demand")
750
751
IoC.CONTAINER.instance.logger.start_analysis()
752
753
if !_watchdog.want_restart then
754
// No CLEAR_STATE here: a not-compiled-through file
755
// arrives straight from a full rebuild, which cleared
756
// its per-build state and then re-resolved its type
757
// expressions - exactly the state compile-expressions
758
// expects. Clearing again would wipe the resolve
759
// passes' output (body TypeExpression.type) that the
760
// walk consumes, with no pass left to restore it.
761
for i in missing do
762
// The re-walk re-reports this file's expression
763
// diagnostics; drop the previous copies so they
764
// do not double up. Parse and declaration-level
765
// diagnostics are kept - no earlier pass re-runs.
766
IoC.CONTAINER.instance.logger.clear_expression_diagnostics(i.file_name)
767
Syntax.Process.PURE_SLOTS.clear_for(i.file_name)
768
Syntax.Process.KILL_LEDGER.clear_for(i.file_name)
769
770
_compiler.compile_expressions_only(i)
771
od
772
773
let diagnostics = Collections.LIST[Protocol.DIAGNOSTIC]()
774
let checked_paths = Collections.LIST[string]()
775
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, diagnostics, checked_paths)
776
JSON_PROTOCOL.write_response(
777
writer,
778
Protocol.Response.DIAGNOSTICS(diagnostics, checked_paths, "query", 0.0D, false)
779
)
780
fi
781
catch e: Exception
782
debug_always("COMPILE_ON_DEMAND caught: {e.get_type()}: {e.message}")
783
784
_watchdog.request_restart()
785
finally
786
IoC.CONTAINER.instance.logger.end_analysis()
787
788
_timers.finish("compile-on-demand")
789
yrt
790
si
791
si
792
793
// Does a full compile of the files that have been edited, plus a partial
794
// compile (up to but not including expressions) of all other files.
795
class FILE_EDITED_HANDLER(
796
_watchdog: WATCHDOG,
797
_timers: TIMERS,
798
_compiler: COMPILER,
799
_build_flags: GLOBAL_BUILD_FLAGS,
800
library_files: Iterable[string],
801
_symbol_table: Semantic.SYMBOL_TABLE
802
): RequestHandler[Protocol.Request.EDIT], SourceFileLookup, Iterable[SOURCE_FILE] is
803
_library_files: Iterable[string]?
804
805
_source_files_by_path: Collections.MutableMap[string,SOURCE_FILE]
806
807
_is_interface_changed: bool
808
809
// The guard that last gave up on this EDIT, reported if it ends up
810
// taking the whole-project rebuild. Null when no incremental path
811
// was attempted at all.
812
_declined_reason: string?
813
814
// The pre-edit SOURCE_FILE of the most recent parse, stashed by
815
// parse_and_add_file. For an interface-preserving single-file EDIT the
816
// incremental body re-walk splices the new bodies onto it (its symbols
817
// are still valid). Null on a cold / first-parse EDIT.
818
_retained: SOURCE_FILE?
819
820
file_names: Iterable[string] => _source_files_by_path.keys
821
822
iterator: Collections.Iterator[SOURCE_FILE]
823
=> _source_files_by_path.values.iterator
824
825
super()
826
827
init(..) is
828
_source_files_by_path = Collections.MAP[string,SOURCE_FILE]()
829
si
830
831
find_source_file(path: string) -> SOURCE_FILE? =>
832
if _source_files_by_path.contains_key(path) then
833
_source_files_by_path[path]
834
else
835
null
836
fi
837
838
parse_and_add_file(path: string, reader: IO.TextReader, is_internal_file: bool) is
839
IoC.CONTAINER.instance.logger.clear(path, false)
840
841
// The re-parse replaces this file's tree; node-keyed state
842
// recorded against the old tree's nodes is reclaimed here.
843
// Any pass that later walks surviving spliced-in nodes
844
// rewrites its state before reading it, so dropping the
845
// whole file is safe on the incremental paths too.
846
IoC.CONTAINER.instance.state_store_registry.drop_file(path)
847
848
let previous = find_source_file(path)
849
850
_retained = previous
851
852
let source_file = _compiler.parse(path, reader, _build_flags.want_compile_up_to_expressions, _build_flags.want_compile_expressions, is_internal_file)
853
_source_files_by_path[path] = source_file
854
855
_compiler.post_parse([source_file])
856
857
source_file.want_compile_expressions = true
858
859
let signature = Syntax.INTERFACE_SIGNATURE()
860
source_file.definition.walk(signature)
861
source_file.interface_signature = signature.signature
862
863
if
864
!previous? \/
865
!previous.interface_signature? \/
866
previous.interface_signature !~ source_file.interface_signature
867
then
868
_is_interface_changed = true
869
fi
870
si
871
872
// Latch why an incremental path gave up, and decline. Every
873
// bail-out in the incremental paths goes through here. The first
874
// reason latched wins, and it is reported only if the edit ends up
875
// rebuilding; WORK_COUNTERS carries why.
876
_decline(reason: string) -> bool is
877
if !_declined_reason? then
878
_declined_reason = reason
879
fi
880
881
return false
882
si
883
884
_definition_signature(definition: Syntax.Trees.Definitions.Definition) -> string is
885
let signature = Syntax.INTERFACE_SIGNATURE()
886
887
definition.walk(signature)
888
889
return signature.signature
890
si
891
892
// An interface-affecting edit handled incrementally. Two shapes
893
// qualify, classified by comparing per-top-level-definition
894
// interface signatures (the whole-file signature folds post-order
895
// - a parent's kind trails its children - so neither shape is a
896
// string-prefix relation on it):
897
//
898
// - append-only: every retained definition matches the donor's
899
// at the same index and the donor has extras. The appended
900
// declarations are adopted onto the retained AST and built
901
// against the retained tables; nothing referenced them before
902
// the edit, so nothing else changes.
903
// - replace-one: same count, exactly one definition differs, and
904
// the outgoing subtree declares only functions that nothing
905
// outside this file references or overrides. The old functions
906
// are removed from their scopes, the side tables purge the old
907
// subtree's span, and the new subtree is built in its place.
908
// This is the interface-typing stream: each keystroke pause
909
// re-replaces the one declaration in flux.
910
//
911
// Everything else falls back to the whole-project rebuild. A new
912
// or renamed name can in principle capture a previously
913
// differently-resolved use in another file; that stale resolution
914
// lasts until the next full rebuild - the same class of transient
915
// imprecision the store-free bits accept.
916
try_incremental_interface_edit(
917
diagnostics: Collections.LIST[Protocol.DIAGNOSTIC],
918
checked_paths: Collections.LIST[string]
919
) -> bool is
920
let retained = _retained
921
922
if !retained? then
923
return _decline(WORK_COUNTERS.NO_RETAINED_PARSE)
924
fi
925
926
let edited_path = retained.file_name
927
let donor = _source_files_by_path[edited_path]
928
929
let donor_signature = donor.interface_signature
930
931
if !retained.interface_signature? \/ !donor_signature? then
932
return _decline(WORK_COUNTERS.MISSING_INTERFACE_SIGNATURE)
933
fi
934
935
let donor_list = cast Syntax.Trees.Definitions.LIST?(donor.definition)
936
let retained_list = cast Syntax.Trees.Definitions.LIST?(retained.definition)
937
938
if !donor_list? \/ !retained_list? then
939
return _decline(WORK_COUNTERS.FILE_ROOT_NOT_A_LIST)
940
fi
941
942
// See try_incremental_build: top-level statements put
943
// namespace-declared symbols inside a body, which none of
944
// the incremental routes re-derive.
945
if Syntax.Process.SYNTHESISE_TOP_LEVEL_ENTRY.has_top_level_entry(donor_list) \/ Syntax.Process.SYNTHESISE_TOP_LEVEL_ENTRY.has_top_level_entry(retained_list) then
946
return _decline(WORK_COUNTERS.TOP_LEVEL_STATEMENTS)
947
fi
948
949
let donor_definitions = donor_list.definitions
950
let retained_definitions = retained_list.definitions
951
let retained_count = retained_definitions.count
952
953
if donor_definitions.count == retained_count then
954
let changed_index mut = -1
955
956
for i in 0..retained_count do
957
if _definition_signature(retained_definitions[i]) !~ _definition_signature(donor_definitions[i]) then
958
if changed_index >= 0 then
959
return _decline(WORK_COUNTERS.MULTIPLE_CHANGED_DEFINITIONS)
960
fi
961
962
changed_index = i
963
fi
964
od
965
966
if changed_index < 0 then
967
// The whole-file signatures differ but no top-level
968
// definition's does - something file-level changed;
969
// let the full rebuild sort it out.
970
return _decline(WORK_COUNTERS.FILE_LEVEL_CHANGE)
971
fi
972
973
let chain = Collections.LIST[Syntax.Trees.Definitions.Definition]()
974
975
return _try_narrow_edit(retained, donor_signature, chain, retained_definitions[changed_index], donor_definitions[changed_index], diagnostics, checked_paths)
976
fi
977
978
if donor_definitions.count < retained_count then
979
return _decline(WORK_COUNTERS.FEWER_DEFINITIONS)
980
fi
981
982
for i in 0..retained_count do
983
if _definition_signature(retained_definitions[i]) !~ _definition_signature(donor_definitions[i]) then
984
return _decline(WORK_COUNTERS.RETAINED_PREFIX_MISMATCH)
985
fi
986
od
987
988
// Split the donor: the leading definitions pair with the
989
// retained skeleton; the tail is the appended declarations.
990
let appended = Collections.LIST[Syntax.Trees.Definitions.Definition]()
991
992
while donor_definitions.count > retained_count do
993
appended.insert(0, donor_definitions[donor_definitions.count - 1])
994
donor_definitions.remove_at(donor_definitions.count - 1)
995
od
996
997
if !try_incremental_build(diagnostics, checked_paths, false) then
998
// try_incremental_build re-registered the donor as the
999
// live file; give it its appended tail back so the full
1000
// rebuild sees the whole edit. It named its own decline
1001
// reason, so none is named here.
1002
for d in appended do
1003
donor_definitions.add(d)
1004
od
1005
1006
return false
1007
fi
1008
1009
for d in appended do
1010
retained_list.add(d)
1011
od
1012
1013
_compiler.build_appended(
1014
retained,
1015
Syntax.Trees.Definitions.LIST(retained_list.location, appended)
1016
)
1017
1018
retained.interface_signature = donor_signature
1019
1020
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, diagnostics, checked_paths)
1021
1022
return true
1023
si
1024
1025
// Narrow a changed definition pair down to the smallest changed
1026
// window. Containers of the same kind and name - namespaces, and
1027
// classes whose headers match - recurse into their member lists;
1028
// at each level the members split into a signature-matched common
1029
// prefix, a signature-matched common suffix, and the changed
1030
// window between them. A window consisting solely of function
1031
// definitions is replaced in place; anything else falls back to
1032
// the whole-project rebuild.
1033
_try_narrow_edit(
1034
retained: SOURCE_FILE,
1035
donor_signature: string,
1036
chain: Collections.LIST[Syntax.Trees.Definitions.Definition],
1037
retained_definition: Syntax.Trees.Definitions.Definition,
1038
donor_definition: Syntax.Trees.Definitions.Definition,
1039
diagnostics: Collections.LIST[Protocol.DIAGNOSTIC],
1040
checked_paths: Collections.LIST[string]
1041
) -> bool is
1042
if let retained_namespace: Syntax.Trees.Definitions.NAMESPACE = retained_definition then
1043
let donor_namespace = cast Syntax.Trees.Definitions.NAMESPACE?(donor_definition)
1044
1045
if !donor_namespace? \/ retained_namespace.name.name !~ donor_namespace.name.name then
1046
return _decline(WORK_COUNTERS.NAMESPACE_NAME_MISMATCH)
1047
fi
1048
1049
chain.add(retained_namespace)
1050
1051
return _try_narrow_children(retained, donor_signature, chain, retained_namespace.body, donor_namespace.body, null, diagnostics, checked_paths)
1052
fi
1053
1054
if let retained_class: Syntax.Trees.Definitions.Classy = retained_definition /\ _is_member_container(retained_class) then
1055
let donor_class = cast Syntax.Trees.Definitions.Classy?(donor_definition)
1056
1057
if
1058
!donor_class? \/
1059
retained_definition.get_type() != donor_definition.get_type() \/
1060
_class_header_signature(retained_class) !~ _class_header_signature(donor_class)
1061
then
1062
return _decline(WORK_COUNTERS.CLASS_HEADER_MISMATCH)
1063
fi
1064
1065
let scope = _symbol_table.scope_for(retained_class)
1066
let classy = if scope? then cast Semantic.Symbols.Classy?(scope.underlying_scope) else null fi
1067
1068
if !classy? then
1069
return _decline(WORK_COUNTERS.NO_CLASS_SYMBOL)
1070
fi
1071
1072
// A class with ghul-declared subclasses or trait
1073
// implementors is handled by resetting and re-pulling the
1074
// whole implementor closure at the reconcile. That covers
1075
// pull-down bookkeeping, not member cloning: a closure
1076
// member extending a constructed generic re-creates its
1077
// inherited member clones on re-pull, leaving other files'
1078
// bindings on the dead clones, so those fall back to the
1079
// whole-project rebuild.
1080
if !_implementor_closure(classy)? then
1081
return _decline(WORK_COUNTERS.UNRESETTABLE_CLOSURE)
1082
fi
1083
1084
chain.add(retained_class)
1085
1086
return _try_narrow_children(retained, donor_signature, chain, retained_class.body, donor_class.body, classy, diagnostics, checked_paths)
1087
fi
1088
1089
// Not a container the search can descend into. This is the
1090
// recursion's base case rather than a guard rejecting the
1091
// edit - the caller falls through to replacing the definition
1092
// in its enclosing member list, and names a reason if that
1093
// does not apply either. Latching one here would mask it.
1094
return false
1095
si
1096
1097
_try_narrow_children(
1098
retained: SOURCE_FILE,
1099
donor_signature: string,
1100
chain: Collections.LIST[Syntax.Trees.Definitions.Definition],
1101
retained_body: Syntax.Trees.Definitions.LIST,
1102
donor_body: Syntax.Trees.Definitions.LIST,
1103
slice_class: Semantic.Symbols.Classy?,
1104
diagnostics: Collections.LIST[Protocol.DIAGNOSTIC],
1105
checked_paths: Collections.LIST[string]
1106
) -> bool is
1107
let retained_members = retained_body.definitions
1108
let donor_members = donor_body.definitions
1109
1110
let retained_count = retained_members.count
1111
let donor_count = donor_members.count
1112
1113
let limit = if retained_count < donor_count then retained_count else donor_count fi
1114
1115
let prefix mut = 0
1116
1117
while
1118
prefix < limit /\
1119
_definition_signature(retained_members[prefix]) =~ _definition_signature(donor_members[prefix])
1120
do
1121
prefix = prefix + 1
1122
od
1123
1124
let suffix mut = 0
1125
1126
while
1127
suffix < limit - prefix /\
1128
_definition_signature(retained_members[retained_count - 1 - suffix]) =~ _definition_signature(donor_members[donor_count - 1 - suffix])
1129
do
1130
suffix = suffix + 1
1131
od
1132
1133
let retained_window = retained_count - prefix - suffix
1134
let donor_window = donor_count - prefix - suffix
1135
1136
if retained_window == 1 /\ donor_window == 1 then
1137
// A single changed member may itself be a container with a
1138
// still narrower change inside it. Recursion mutates the
1139
// chain, so work on a copy in case it declines and the
1140
// window falls through to function replacement.
1141
let inner_chain = Collections.LIST[Syntax.Trees.Definitions.Definition](chain)
1142
1143
if _try_narrow_edit(retained, donor_signature, inner_chain, retained_members[prefix], donor_members[prefix], diagnostics, checked_paths) then
1144
return true
1145
fi
1146
fi
1147
1148
return _try_replace_functions(retained, donor_signature, chain, retained_body, donor_body, prefix, retained_window, donor_window, slice_class, diagnostics, checked_paths)
1149
si
1150
1151
// Replace the changed window of a member list, when every member
1152
// in it (both outgoing and incoming) is a function definition. All
1153
// guards run before anything is mutated.
1154
_try_replace_functions(
1155
retained: SOURCE_FILE,
1156
donor_signature: string,
1157
chain: Collections.LIST[Syntax.Trees.Definitions.Definition],
1158
retained_body: Syntax.Trees.Definitions.LIST,
1159
donor_body: Syntax.Trees.Definitions.LIST,
1160
prefix: int,
1161
retained_window: int,
1162
donor_window: int,
1163
slice_class: Semantic.Symbols.Classy?,
1164
diagnostics: Collections.LIST[Protocol.DIAGNOSTIC],
1165
checked_paths: Collections.LIST[string]
1166
) -> bool is
1167
let retained_members = retained_body.definitions
1168
let donor_members = donor_body.definitions
1169
1170
// Texts for the body comparison in the identity adoption after
1171
// the reconcile: the outgoing members' spans index into the
1172
// text the retained tree was parsed from, the incoming
1173
// members' into the donor's. Captured up front because a
1174
// successful try_incremental_build below moves the donor's
1175
// text onto the retained file.
1176
let outgoing_text = retained.source_text
1177
let incoming_text = find_source_file(retained.file_name)?.source_text
1178
1179
let removed_functions = Collections.LIST[Semantic.Symbols.Function]()
1180
1181
// Set when a body elsewhere in the edited file binds an
1182
// outgoing function: those bodies must re-compile against the
1183
// reconciled symbol. Referencing bodies in other files are
1184
// collected here and re-compiled the same way, but only for a
1185
// class member edit - see the reference scan below.
1186
let needs_dependent_recompile mut = false
1187
let dependent_file_names = Collections.SET[string]()
1188
1189
for i in 0..retained_window do
1190
let member = retained_members[prefix + i]
1191
1192
let function = _function_symbol_for(member)
1193
1194
if !function? then
1195
return _decline(WORK_COUNTERS.OUTGOING_NOT_A_FUNCTION)
1196
fi
1197
1198
// A body bound to the outgoing function must re-bind to the
1199
// reconciled symbol or it stays silently bound to the dead
1200
// one. Its file's expressions are recompiled after the
1201
// reconcile: the edited file itself when the reference is a
1202
// same-file body, or the referencing file when it is a
1203
// cross-file body of a class member (see the reference scan).
1204
// Only direct references count - a call bound to an overridden
1205
// base member stays valid when an override is removed.
1206
// Override links in either direction are undone by the slice
1207
// reset, but only when there is a class to reset - at
1208
// namespace level they reject the edit.
1209
if !slice_class? then
1210
if !function.has_no_overriders then
1211
return _decline(WORK_COUNTERS.OUTGOING_HAS_OVERRIDERS)
1212
fi
1213
1214
if let overridees = function.overridees then
1215
if overridees |> any(o => true) then
1216
return _decline(WORK_COUNTERS.OUTGOING_HAS_OVERRIDEES)
1217
fi
1218
fi
1219
else
1220
// A ghul-declared overrider in a subclass couples its
1221
// own declaration to this signature: the closure
1222
// re-pull rebuilds the override links, but the
1223
// subclass's declaration-level diagnostics would not
1224
// be re-derived against the new signature.
1225
if !function.has_no_overriders then
1226
return _decline(WORK_COUNTERS.OUTGOING_HAS_OVERRIDERS)
1227
fi
1228
fi
1229
1230
for reference in IoC.CONTAINER.instance.symbol_use_locations.direct_references_to(function) do
1231
if reference.file_name =~ retained.file_name then
1232
if reference.start < member.location.start \/ reference.start > member.location.end then
1233
// A same-file body outside the edited window
1234
// binds the outgoing function.
1235
needs_dependent_recompile = true
1236
fi
1237
else
1238
// A body in another file binds the outgoing function.
1239
// Recompiling that file rebinds it only when the
1240
// owner scope resolves to one symbol across files: a
1241
// class-like scope does, so a class member's
1242
// other-file callers re-bind, but a namespace-level
1243
// function's groups are per-file and the reconcile
1244
// updates only this file's, leaving other files
1245
// resolving the outgoing symbol. Fall back to the
1246
// whole-project rebuild for a namespace-level edit,
1247
// and for a reference in a file that is not a known
1248
// source file.
1249
if !slice_class? then
1250
return _decline(WORK_COUNTERS.CROSS_FILE_NAMESPACE_REFERENCE)
1251
fi
1252
1253
if !find_source_file(reference.file_name)? then
1254
return _decline(WORK_COUNTERS.CROSS_FILE_UNKNOWN_FILE)
1255
fi
1256
1257
dependent_file_names.add(reference.file_name)
1258
fi
1259
od
1260
1261
removed_functions.add(function)
1262
od
1263
1264
for i in 0..donor_window do
1265
if !isa Syntax.Trees.Definitions.FUNCTION(donor_members[prefix + i]) then
1266
return _decline(WORK_COUNTERS.INCOMING_NOT_A_FUNCTION)
1267
fi
1268
od
1269
1270
// The implementor closure that must reset and re-pull around
1271
// the member swap. Collected before anything mutates - the
1272
// journal undo removes the implementor registrations the
1273
// collection walks - and rejected (null) while rejection can
1274
// still fall back cleanly.
1275
let hierarchy_slice =
1276
if slice_class? then
1277
_implementor_closure(slice_class)
1278
else
1279
null
1280
fi
1281
1282
if slice_class? /\ !hierarchy_slice? then
1283
return _decline(WORK_COUNTERS.UNRESETTABLE_CLOSURE)
1284
fi
1285
1286
// Trim both windows so the remaining trees pair structurally
1287
// for the ordinary body re-walk; the outgoing members' spans
1288
// purge their side-table entries through the same
1289
// reconciliation that drops re-walked body spans.
1290
let removed_definitions = Collections.LIST[Syntax.Trees.Definitions.Definition]()
1291
let added_definitions = Collections.LIST[Syntax.Trees.Definitions.Definition]()
1292
1293
let extra_purge_spans = Collections.LIST[Source.LOCATION]()
1294
1295
for i in 0..retained_window do
1296
let member = retained_members[prefix]
1297
1298
removed_definitions.add(member)
1299
extra_purge_spans.add(member.location)
1300
1301
retained_members.remove_at(prefix)
1302
od
1303
1304
for i in 0..donor_window do
1305
added_definitions.add(donor_members[prefix])
1306
donor_members.remove_at(prefix)
1307
od
1308
1309
// A decline here was named by try_incremental_build itself.
1310
if !try_incremental_build(diagnostics, checked_paths, false, extra_purge_spans) then
1311
for i in 0..retained_window do
1312
retained_members.insert(prefix + i, removed_definitions[i])
1313
od
1314
1315
for i in 0..donor_window do
1316
donor_members.insert(prefix + i, added_definitions[i])
1317
od
1318
1319
return false
1320
fi
1321
1322
// Committed. Undo the pull-downs before touching the declared
1323
// members so each journal's record still matches the state it
1324
// was made against. The whole implementor closure resets: a
1325
// subclass's own journal holds the override links, implementor
1326
// registrations and pulled-down copies that referenced the
1327
// outgoing members.
1328
if hierarchy_slice? then
1329
for hierarchy_member in hierarchy_slice do
1330
hierarchy_member.reset_pulled_down_symbols()
1331
od
1332
fi
1333
1334
1335
for f in removed_functions do
1336
_remove_function_from_owner(f)
1337
od
1338
1339
1340
for i in 0..donor_window do
1341
retained_members.insert(prefix + i, added_definitions[i])
1342
od
1343
1344
let added_list = Syntax.Trees.Definitions.LIST(retained_body.location, added_definitions)
1345
1346
// The window replacement mutated this file's member tree
1347
// beyond the body splice (which already dropped the bucket);
1348
// stated here too so the mutation owns its staleness rule
1349
// rather than leaning on the splice having run first.
1350
1351
_compiler.build_members_interface(retained, chain, added_list)
1352
1353
1354
// Before anything recompiles against the new symbols: each
1355
// incoming function adopts its outgoing counterpart's
1356
// identity, carrying the id-keyed store-free bit across the
1357
// re-creation, so the next debounced compile's refresh
1358
// compares against the bits callers were compiled with
1359
// rather than a default and does not escalate to a full
1360
// rebuild when nothing store-free-relevant changed.
1361
_adopt_replaced_identities(
1362
removed_functions,
1363
removed_definitions,
1364
added_definitions,
1365
outgoing_text,
1366
incoming_text
1367
)
1368
1369
// Re-resolve the closure's inheritance against the changed
1370
// member set: overrides, pulled-down members and implementor
1371
// registration all rebuild through the ordinary pull-down.
1372
// Order is free - pull_down_super_symbols recursively pulls a
1373
// class's ancestors first, and a class already pulled is a
1374
// no-op.
1375
if hierarchy_slice? then
1376
for hierarchy_member in hierarchy_slice do
1377
hierarchy_member.pull_down_super_symbols()
1378
od
1379
fi
1380
1381
// A body that failed to bind a name recorded no reference, so
1382
// the reference scan above cannot see it as a dependent - yet
1383
// this interface change may be exactly what cures, or should
1384
// re-report, its error. The edited file re-checks its own
1385
// errors on this edit's recompile; any other file holding an
1386
// error is marked not-compiled-through, with its expression
1387
// state cleared now (bodies elsewhere in it can bind the
1388
// outgoing symbols), so the next debounced compile's
1389
// expressions-only pass re-checks it - deferring that cost to
1390
// the debounce tick keeps the per-keystroke cost flat.
1391
for error_path in IoC.CONTAINER.instance.logger.paths_with_errors do
1392
if error_path =~ retained.file_name then
1393
needs_dependent_recompile = true
1394
elif let error_file = find_source_file(error_path) then
1395
_compiler.invalidate_file_expressions(error_file)
1396
1397
_compiler.is_full_compile_needed = true
1398
fi
1399
od
1400
1401
if needs_dependent_recompile then
1402
// A same-file body bound the outgoing function; recompile
1403
// the whole edited file's expressions so every body rebinds
1404
// to the reconciled symbol. Covers the changed members too,
1405
// so the positioned expression build is skipped.
1406
_recompile_file_expressions(retained)
1407
else
1408
_compiler.build_members_expressions(retained, chain, added_list)
1409
fi
1410
1411
// Bodies in other files that bound an outgoing function rebind
1412
// to the reconciled symbol the same way - a compile-expressions-
1413
// only recompile against the unchanged interface of each.
1414
for dependent_file_name in dependent_file_names do
1415
if let dependent = find_source_file(dependent_file_name) then
1416
_recompile_file_expressions(dependent)
1417
fi
1418
od
1419
1420
retained.interface_signature = donor_signature
1421
1422
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, diagnostics, checked_paths)
1423
1424
return true
1425
si
1426
1427
_recompile_file_expressions(source_file: SOURCE_FILE) is
1428
_compiler.recompile_file_expressions(source_file)
1429
si
1430
1431
// The header of a class-like definition - name, type parameters,
1432
// ancestors and modifiers - folded the way the interface signature
1433
// folds them, so two versions of a class can be compared ignoring
1434
// their member lists.
1435
_class_header_signature(`class: Syntax.Trees.Definitions.Classy) -> string is
1436
let signature = Syntax.INTERFACE_SIGNATURE()
1437
1438
`class.name.walk(signature)
1439
1440
if let arguments = `class.arguments then
1441
arguments.walk(signature)
1442
fi
1443
1444
if let ancestors = `class.ancestors then
1445
ancestors.walk(signature)
1446
fi
1447
1448
`class.modifiers.walk(signature)
1449
1450
return signature.signature
1451
si
1452
1453
// Whether the narrowing search can descend into this definition's
1454
// body looking for a changed member.
1455
//
1456
// Classes, structs and traits carry methods and properties, which
1457
// is what the window replacement below knows how to swap. Unions
1458
// and enums share the same `Classy` base and the same body shape,
1459
// but their bodies hold variants and enum members - the
1460
// replacement would reject every window, so descending only buys a
1461
// longer walk to the same answer.
1462
_is_member_container(definition: Syntax.Trees.Definitions.Definition) -> bool =>
1463
isa Syntax.Trees.Definitions.CLASS(definition) \/
1464
isa Syntax.Trees.Definitions.STRUCT(definition) \/
1465
isa Syntax.Trees.Definitions.TRAIT(definition)
1466
1467
_function_symbol_for(definition: Syntax.Trees.Definitions.Definition) -> Semantic.Symbols.Function? is
1468
if !isa Syntax.Trees.Definitions.FUNCTION(definition) then
1469
return null
1470
fi
1471
1472
let scope = _symbol_table.scope_for(definition)
1473
1474
if !scope? then
1475
return null
1476
fi
1477
1478
return cast Semantic.Symbols.Function?(scope.underlying_scope)
1479
si
1480
1481
// The class together with its transitive ghul-declared
1482
// subclasses and trait implementors, in discovery order. Null
1483
// when an edit to the class's members can't be handled by
1484
// resetting and re-pulling the closure: a closure member that
1485
// extends or implements a constructed generic re-creates its
1486
// inherited member clones on re-pull, and bindings elsewhere
1487
// would keep the dead clones. Reflected implementors are
1488
// skipped - they are import-lifetime and never reset.
1489
_implementor_closure(classy: Semantic.Symbols.Classy) -> Collections.LIST[Semantic.Symbols.Classy]? is
1490
let result = Collections.LIST[Semantic.Symbols.Classy]()
1491
let seen = Collections.SET[Semantic.Symbols.Classy]()
1492
1493
result.add(classy)
1494
seen.add(classy)
1495
1496
let index mut = 0
1497
1498
while index < result.count do
1499
let current = result[index]
1500
1501
index = index + 1
1502
1503
if let implementors = current.implementors then
1504
for implementor in implementors do
1505
if implementor.is_reflected then
1506
continue
1507
fi
1508
1509
if !isa Semantic.Symbols.Classy(implementor) then
1510
return null
1511
fi
1512
1513
if seen.contains(implementor) then
1514
continue
1515
fi
1516
1517
for a in 0..implementor.ancestors.count do
1518
if isa Semantic.Symbols.GENERIC(implementor.get_ancestor(a).symbol) then
1519
return null
1520
fi
1521
od
1522
1523
seen.add(implementor)
1524
result.add(implementor)
1525
od
1526
fi
1527
od
1528
1529
return result
1530
si
1531
1532
// After a reconcile replaced the changed window's members: each
1533
// incoming function that pairs with exactly one outgoing function
1534
// by name and arity adopts its identity, so the store-free bit —
1535
// keyed by symbol id — survives the re-creation. The carried bit
1536
// stays valid only while the body it was derived from is
1537
// unchanged; an incoming body that differs textually resets it
1538
// to the conservative default instead, and the next compile's
1539
// refresh re-derives it. (A changed parameter type can rebind a
1540
// call inside an unchanged body and move the derived bit; the
1541
// refresh catches the difference and escalates, the same true-up
1542
// that covers body-only edits.) Ambiguous pairings — several
1543
// same-name same-arity candidates on either side — are skipped:
1544
// the incoming function keeps its fresh identity and default bit.
1545
_adopt_replaced_identities(
1546
removed_functions: Collections.List[Semantic.Symbols.Function],
1547
removed_definitions: Collections.List[Syntax.Trees.Definitions.Definition],
1548
added_definitions: Collections.List[Syntax.Trees.Definitions.Definition],
1549
outgoing_text: string?,
1550
incoming_text: string?
1551
) is
1552
let adopted = Collections.SET[int]()
1553
1554
for i in 0..added_definitions.count do
1555
let added = added_definitions[i]
1556
1557
let incoming = _function_symbol_for(added)
1558
1559
if !incoming? then
1560
continue
1561
fi
1562
1563
let matched_index mut = -1
1564
let is_ambiguous mut = false
1565
1566
for j in 0..removed_functions.count do
1567
if adopted.contains(j) then
1568
continue
1569
fi
1570
1571
let outgoing = removed_functions[j]
1572
1573
if
1574
outgoing.name =~ incoming.name /\
1575
outgoing.arguments.count == incoming.arguments.count
1576
then
1577
if matched_index >= 0 then
1578
is_ambiguous = true
1579
break
1580
fi
1581
1582
matched_index = j
1583
fi
1584
od
1585
1586
if matched_index < 0 \/ is_ambiguous then
1587
continue
1588
fi
1589
1590
adopted.add(matched_index)
1591
1592
incoming.adopt_id(removed_functions[matched_index])
1593
1594
// the adopted id carries the outgoing symbol's proven
1595
// store-free bit; an edit that also changed the body
1596
// resets it to the conservative default, and the next
1597
// solve re-derives it
1598
if !_body_text_matches(removed_definitions[matched_index], added, outgoing_text, incoming_text) then
1599
incoming.set_proven_store_free(false)
1600
fi
1601
od
1602
si
1603
1604
// Whether two function definitions' bodies are textually
1605
// identical, each sliced from the source text its tree was
1606
// parsed from. Unavailable text or a non-function definition
1607
// compares as not matching — the conservative direction: the
1608
// carried bit resets and is re-derived.
1609
_body_text_matches(
1610
outgoing: Syntax.Trees.Definitions.Definition,
1611
incoming: Syntax.Trees.Definitions.Definition,
1612
outgoing_text: string?,
1613
incoming_text: string?
1614
) -> bool is
1615
if
1616
!isa Syntax.Trees.Definitions.FUNCTION(outgoing) \/
1617
!isa Syntax.Trees.Definitions.FUNCTION(incoming)
1618
then
1619
return false
1620
fi
1621
1622
let outgoing_body = (cast Syntax.Trees.Definitions.FUNCTION(outgoing)).body
1623
let incoming_body = (cast Syntax.Trees.Definitions.FUNCTION(incoming)).body
1624
1625
if !outgoing_body? /\ !incoming_body? then
1626
// both bodiless: nothing body-derived can have changed
1627
return true
1628
fi
1629
1630
if !outgoing_body? \/ !incoming_body? then
1631
return false
1632
fi
1633
1634
let outgoing_slice = _slice_span(outgoing_text, outgoing_body.location)
1635
let incoming_slice = _slice_span(incoming_text, incoming_body.location)
1636
1637
return outgoing_slice? /\ incoming_slice? /\ outgoing_slice =~ incoming_slice
1638
si
1639
1640
// The span's text, or null when the text is unavailable or the
1641
// span falls outside it. Lines and columns are 1-based; the end
1642
// column is taken as inclusive — the convention only has to be
1643
// applied identically to the two sides of an equality check.
1644
_slice_span(text: string?, location: Source.LOCATION) -> string? is
1645
if !text? then
1646
return null
1647
fi
1648
1649
let lines = text.replace_line_endings("\n").split(['\n'])
1650
1651
if location.start_line < 1 \/ location.end_line > lines.count \/ location.end_line < location.start_line then
1652
return null
1653
fi
1654
1655
let result = System.Text.StringBuilder()
1656
1657
for line_number in location.start_line..location.end_line + 1 do
1658
let line = lines[line_number - 1]
1659
1660
let from = if line_number == location.start_line then location.start_column - 1 else 0 fi
1661
let to mut = if line_number == location.end_line then location.end_column else line.length fi
1662
1663
if from < 0 \/ from > line.length then
1664
return null
1665
fi
1666
1667
if to > line.length then
1668
to = line.length
1669
fi
1670
1671
if to > from then
1672
result.append(line.substring(from, to - from))
1673
fi
1674
1675
if line_number < location.end_line then
1676
result.append('\n')
1677
fi
1678
od
1679
1680
return result.to_string()
1681
si
1682
1683
_remove_function_from_owner(function: Semantic.Symbols.Function) is
1684
let owner = cast Semantic.Symbols.Scoped?(function.owner)
1685
1686
if !owner? then
1687
return
1688
fi
1689
1690
let name = function.name
1691
1692
let existing = owner.find_direct(name)
1693
1694
if let group: Semantic.Symbols.FUNCTION_GROUP = existing then
1695
group.remove(function)
1696
1697
if group.is_empty then
1698
owner.remove_direct(name)
1699
fi
1700
elif existing == function then
1701
owner.remove_direct(name)
1702
fi
1703
si
1704
1705
// The incremental body re-walk for an interface-preserving single-file
1706
// EDIT: keep the retained AST registered, splice the freshly-parsed
1707
// bodies onto it, refresh its locations from the fresh parse, and
1708
// re-walk only the edited file. Returns false — restoring the fresh
1709
// parse as the registered file — if the parses do not pair or the
1710
// location refresh desyncs, so the caller falls back to a full rebuild.
1711
try_incremental_build(
1712
diagnostics: Collections.LIST[Protocol.DIAGNOSTIC],
1713
checked_paths: Collections.LIST[string]
1714
) -> bool =>
1715
try_incremental_build(diagnostics, checked_paths, true, null)
1716
1717
try_incremental_build(
1718
diagnostics: Collections.LIST[Protocol.DIAGNOSTIC],
1719
checked_paths: Collections.LIST[string],
1720
want_collect: bool
1721
) -> bool =>
1722
try_incremental_build(diagnostics, checked_paths, want_collect, null)
1723
1724
try_incremental_build(
1725
diagnostics: Collections.LIST[Protocol.DIAGNOSTIC],
1726
checked_paths: Collections.LIST[string],
1727
want_collect: bool,
1728
extra_purge_spans: Collections.List[Source.LOCATION]?
1729
) -> bool is
1730
let retained = _retained
1731
1732
if !retained? then
1733
return _decline(WORK_COUNTERS.NO_RETAINED_PARSE)
1734
fi
1735
1736
let edited_path = retained.file_name
1737
let donor = _source_files_by_path[edited_path]
1738
1739
// A top-level `let` in a synthesised entry body declares a
1740
// symbol into the file's namespace, so an edit among the
1741
// top-level statements can add, remove or retype one with no
1742
// definition's signature changing. That namespace is private
1743
// to the file, and the re-walk re-declares the entry's
1744
// variables and then re-walks every body in the file, so
1745
// nothing outside the re-walk reads a stale one. Under
1746
// --global-namespace the variables are visible to every other
1747
// namespace-less file, and only the full rebuild re-derives
1748
// those readers.
1749
if
1750
_build_flags.want_global_namespace /\
1751
(
1752
Syntax.Process.SYNTHESISE_TOP_LEVEL_ENTRY.has_top_level_entry(retained.definition) \/
1753
Syntax.Process.SYNTHESISE_TOP_LEVEL_ENTRY.has_top_level_entry(donor.definition)
1754
)
1755
then
1756
return _decline(WORK_COUNTERS.TOP_LEVEL_STATEMENTS)
1757
fi
1758
1759
// Keep the retained AST as the registered file; the fresh parse is
1760
// only a body donor.
1761
_source_files_by_path[edited_path] = retained
1762
1763
let pairing = Syntax.FUNCTION_PAIRING(retained.definition, donor.definition)
1764
1765
let pairs = pairing.pairs
1766
1767
if !pairs? then
1768
_source_files_by_path[edited_path] = donor
1769
return _decline(WORK_COUNTERS.FUNCTION_PAIRING_DESYNC)
1770
fi
1771
1772
// Capture the pre-edit spans of the bodies about to be
1773
// re-walked, before the splice overwrites the retained bodies.
1774
// A stale symbol-use / definition entry inside one of these is
1775
// re-recorded by the re-walk, so reconciliation drops it.
1776
let body_spans = Source.BODY_SPANS()
1777
1778
for pair in pairs do
1779
if pair.retained.body? then
1780
body_spans.add(pair.retained.body!.location)
1781
fi
1782
od
1783
1784
// A replaced definition's whole span purges the same way as a
1785
// re-walked body: its old entries drop and the replacement's
1786
// build re-records them.
1787
if extra_purge_spans? then
1788
for s in extra_purge_spans do
1789
body_spans.add(s)
1790
od
1791
fi
1792
1793
Syntax.BODY_SPLICE.apply(pairs)
1794
1795
// The lockstep reconciliation walk: copies the donor's correct
1796
// locations onto the retained interface and returns every
1797
// interface node's pre-edit -> post-edit correspondence. Null
1798
// if the two parses disagreed structurally — fall back.
1799
let correspondence = Syntax.Process.LOCATION_REFRESH.apply(retained.definition, donor.definition)
1800
1801
if !correspondence? then
1802
_source_files_by_path[edited_path] = donor
1803
return _decline(WORK_COUNTERS.LOCATION_REFRESH_DESYNC)
1804
fi
1805
1806
IoC.CONTAINER.instance.logger.clear(edited_path, true)
1807
IoC.CONTAINER.instance.logger.start_analysis()
1808
1809
// The retained tree now reflects the donor's content — spliced
1810
// bodies, donor locations — so it must carry the donor's text
1811
// too. Every successful incremental edit flows through here;
1812
// a stale text would let a later reconcile's body comparison
1813
// false-match a since-reverted body. The retained store-free
1814
// facts describe the pre-splice bodies for the same reason.
1815
retained.source_text = donor.source_text
1816
1817
_compiler.rewalk_bodies(retained, correspondence, body_spans)
1818
1819
if want_collect then
1820
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, diagnostics, checked_paths)
1821
fi
1822
1823
return true
1824
si
1825
1826
handle_request(request: Protocol.Request.EDIT, writer: IO.TextWriter) is
1827
_is_interface_changed = false
1828
_retained = null
1829
_declined_reason = null
1830
1831
for i in _source_files_by_path.values do
1832
i.want_compile_expressions = false
1833
od
1834
1835
let paths = Collections.LIST[string]()
1836
1837
let want_timer = request.files.count == 1 /\ !_library_files?
1838
1839
if want_timer then
1840
_timers.start(TIMERS.edit_single_timer_name)
1841
fi
1842
1843
if _library_files? then
1844
for library_file_path in _library_files do
1845
parse_and_add_file(library_file_path, IO.File.open_text(library_file_path), true)
1846
od
1847
1848
_library_files = null
1849
fi
1850
1851
try
1852
for f in request.files do
1853
parse_and_add_file(f.path, IO.StringReader(f.source), false)
1854
1855
if let source_file = find_source_file(f.path) then
1856
source_file.source_text = f.source
1857
fi
1858
1859
paths.add(f.path)
1860
od
1861
catch ex: Exception
1862
debug_always("PARSE caught: {ex.get_type()} {ex.message}")
1863
yrt
1864
1865
// Classify this EDIT for the stats dump: interface-preserving (a
1866
// following COMPILE can be skipped) vs interface-affecting.
1867
let edit_class_timer =
1868
if _is_interface_changed then
1869
"edit-interface-affecting"
1870
else
1871
"edit-interface-preserving"
1872
fi
1873
1874
if want_timer then
1875
_timers.start(edit_class_timer)
1876
fi
1877
1878
let diagnostics = Collections.LIST[Protocol.DIAGNOSTIC]()
1879
let checked_paths = Collections.LIST[string]()
1880
1881
// An interface-preserving single-file EDIT takes the incremental
1882
// body re-walk instead of the whole-project rebuild.
1883
// On by default; `--no-incremental-analysis` turns it off.
1884
//
1885
// Neither incremental path is taken while the retained tables
1886
// are out of date: added references can change how any file
1887
// resolves, and only the whole-project rebuild reflects that.
1888
let incremental_eligible =
1889
_build_flags.want_incremental_analysis /\
1890
_compiler.are_tables_current /\
1891
!_is_interface_changed /\
1892
paths.count == 1 /\
1893
_retained? /\
1894
_retained.interface_signature?
1895
1896
// An interface-affecting edit takes the incremental interface
1897
// path - appended declarations, or one replaced declaration
1898
// whose old symbols nothing else references - instead of the
1899
// whole-project rebuild; the shape classification lives in
1900
// try_incremental_interface_edit.
1901
let interface_edit_eligible =
1902
_build_flags.want_incremental_analysis /\
1903
_compiler.are_tables_current /\
1904
_is_interface_changed /\
1905
paths.count == 1 /\
1906
_retained? /\
1907
_retained.interface_signature?
1908
1909
try
1910
Semantic.Types.NAMED.clear_cache()
1911
1912
if !_watchdog.want_restart then
1913
// Which path handled this EDIT, for the stats dump:
1914
// the body re-walk, the incremental interface path, or
1915
// the whole-project fallback. Counts across a session
1916
// are the edit-mix data the incremental work is tuned
1917
// against.
1918
let handled_by_body_rewalk =
1919
incremental_eligible /\ try_incremental_build(diagnostics, checked_paths)
1920
1921
let handled_by_interface_path =
1922
!handled_by_body_rewalk /\
1923
interface_edit_eligible /\
1924
try_incremental_interface_edit(diagnostics, checked_paths)
1925
1926
if handled_by_body_rewalk then
1927
_timers.bump(WORK_COUNTERS.EDIT_PATH_BODY_REWALK)
1928
elif handled_by_interface_path then
1929
_timers.bump(WORK_COUNTERS.EDIT_PATH_INTERFACE_INCREMENTAL)
1930
1931
// The interface path splices the new bodies in
1932
// without recompiling the file's expressions,
1933
// so its expression-level state — hover uses,
1934
// reliance records — still describes the
1935
// pre-edit bodies. Mark the file stale so the
1936
// debounced COMPILE recompiles exactly this
1937
// file and trues that state up.
1938
for edited_path in paths do
1939
if let edited = find_source_file(edited_path) then
1940
_compiler.invalidate_file_expressions(edited)
1941
fi
1942
od
1943
1944
_compiler.is_full_compile_needed = true
1945
fi
1946
1947
if !handled_by_body_rewalk /\ !handled_by_interface_path then
1948
_timers.bump(WORK_COUNTERS.EDIT_PATH_FULL_REBUILD)
1949
1950
// Exactly one reason per rebuilt edit: the guard
1951
// that gave up, or not-eligible when no
1952
// incremental path was attempted (the flag is off,
1953
// the edit spans several files, or this is the
1954
// first parse of the file).
1955
WORK_COUNTERS.declined(
1956
_timers,
1957
_declined_reason ?? WORK_COUNTERS.NOT_ELIGIBLE
1958
)
1959
1960
for i in _source_files_by_path.values do
1961
i.want_compile_up_to_expressions = true
1962
1963
IoC.CONTAINER.instance.logger.clear_global_declaration_diagnostics(i.file_name)
1964
od
1965
1966
// Match the COMPILE / query-miss rebuild paths:
1967
// retained ASTs carry expression-level state from
1968
// the previous build - IR values and types that
1969
// reference the symbols clear_symbols is about to
1970
// abandon. Clear it so no later walk of a retained
1971
// tree consumes state from a dead symbol
1972
// generation.
1973
let clear_state = Syntax.Process.CLEAR_STATE_VISITOR()
1974
for i in _source_files_by_path.values do
1975
clear_state.apply(i.definition)
1976
od
1977
1978
IoC.CONTAINER.instance.state_store_registry.clear_all()
1979
1980
IoC.CONTAINER.instance.logger.start_analysis()
1981
1982
_compiler.clear_symbols(paths)
1983
1984
_compiler.queue(self)
1985
1986
// Which function the assembly enters at is settled
1987
// by the debounced COMPILE, not here: an edit
1988
// reaching this path rebuilds the whole project,
1989
// and for a file carrying top-level statements
1990
// that is every edit.
1991
_compiler.build(false)
1992
1993
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, diagnostics, checked_paths)
1994
fi
1995
fi
1996
1997
catch ex: Exception
1998
debug_always("ANALYSE caught: {ex.get_type()} {ex.message}")
1999
2000
_watchdog.request_restart()
2001
finally
2002
_compiler.clear_queue()
2003
2004
IoC.CONTAINER.instance.logger.end_analysis()
2005
2006
if want_timer then
2007
_timers.finish(TIMERS.edit_single_timer_name)
2008
_timers.finish(edit_class_timer)
2009
fi
2010
2011
let compile_needed =
2012
_is_interface_changed /\
2013
(_source_files_by_path.values |> any(i => !i.want_compile_expressions))
2014
2015
if compile_needed then
2016
_compiler.is_full_compile_needed = true
2017
fi
2018
2019
let elapsed_ms = _timers.edit_single_timer.max_average_milliseconds
2020
2021
_timers.start("edit-write-response")
2022
2023
JSON_PROTOCOL.write_response(
2024
writer,
2025
Protocol.Response.DIAGNOSTICS(diagnostics, checked_paths, "partial", elapsed_ms, compile_needed)
2026
)
2027
2028
_timers.finish("edit-write-response")
2029
yrt
2030
si
2031
si
2032
2033
// Turns each file's replaced span into that file's whole new text,
2034
// then hands the result to the ordinary edit path.
2035
//
2036
// Reconstruction rather than partial reparsing is the whole of what
2037
// this saves: a client sending a keystroke's worth of text instead of
2038
// a whole file pays for the edit it made rather than for the size of
2039
// the file it made it in. Everything downstream sees exactly what an
2040
// edit carrying the same text would.
2041
//
2042
// Keeping the two copies of a file in step is the client's job: it
2043
// sends a delta only when it knows what the analyser holds, and sends
2044
// whole text whenever it does not. `expected_length` is the assertion
2045
// that it did so, not a negotiation - splicing a span into text that
2046
// is not the text it was computed against yields a plausible file
2047
// rather than a failure, and every later delta compounds it. So a
2048
// mismatch, or a delta for a file the analyser does not hold, is
2049
// treated as the desync it is: the analyser recycles, and the
2050
// replacement is primed with full text by the same path that primes
2051
// any freshly-spawned one.
2052
class EDIT_DELTA_HANDLER(
2053
_watchdog: WATCHDOG,
2054
_file_edited_handler: FILE_EDITED_HANDLER
2055
): RequestHandler[Protocol.Request.EDIT_DELTA] is
2056
super()
2057
2058
handle_request(request: Protocol.Request.EDIT_DELTA, writer: IO.TextWriter) is
2059
let files = Collections.LIST[Protocol.EDIT_FILE]()
2060
2061
for delta in request.files do
2062
let retained = _file_edited_handler.find_source_file(delta.path)?.source_text
2063
2064
if !retained? then
2065
_desynchronized(writer, delta.path, "no retained text for this file")
2066
2067
return
2068
fi
2069
2070
if retained.length != delta.expected_length then
2071
_desynchronized(
2072
writer,
2073
delta.path,
2074
"retained text is {retained.length} characters, "
2075
"the delta expected {delta.expected_length}"
2076
)
2077
2078
return
2079
fi
2080
2081
let edited = _apply(retained, delta)
2082
2083
if !edited? then
2084
_desynchronized(
2085
writer,
2086
delta.path,
2087
"the replaced span falls outside the retained text"
2088
)
2089
2090
return
2091
fi
2092
2093
files.add(Protocol.EDIT_FILE(delta.path, edited))
2094
od
2095
2096
_file_edited_handler.handle_request(Protocol.Request.EDIT(files), writer)
2097
si
2098
2099
// Report the desync and recycle. Recycling rather than answering
2100
// is what makes this safe to get wrong in only one direction: the
2101
// analyser never carries on holding text it cannot account for,
2102
// and the process that replaces it starts from whatever the
2103
// client sends it.
2104
_desynchronized(writer: IO.TextWriter, path: string, reason: string) is
2105
Std.error.write_line("edit delta for {path} cannot be applied: {reason}")
2106
Std.error.flush()
2107
2108
_watchdog.recycle(writer, "edit delta out of step with retained text")
2109
si
2110
2111
// `text` with the delta's span replaced by its new text, or null
2112
// when the span does not lie within `text`.
2113
_apply(text: string, delta: Protocol.EDIT_DELTA_FILE) -> string? is
2114
let start = _offset_of(text, delta.start_line, delta.start_column)
2115
let stop = _offset_of(text, delta.end_line, delta.end_column)
2116
2117
if start < 0 \/ stop < start then
2118
return null
2119
fi
2120
2121
let result = System.Text.StringBuilder()
2122
2123
result.append(text.substring(0, start))
2124
result.append(delta.new_text)
2125
result.append(text.substring(stop))
2126
2127
return result.to_string()
2128
si
2129
2130
// Character offset of a 1-based line and column in `text`, or -1
2131
// when the position lies outside it. Lines are delimited by a
2132
// newline and a carriage return belongs to the line it ends, so a
2133
// column counts the characters a client's own buffer holds
2134
// whichever line endings the file uses.
2135
_offset_of(text: string, line: int, column: int) -> int static is
2136
if line < 1 \/ column < 1 then
2137
return -1
2138
fi
2139
2140
let offset mut = 0
2141
let current_line mut = 1
2142
2143
while current_line < line do
2144
let next_line_start = text.index_of('\n', offset)
2145
2146
if next_line_start < 0 then
2147
return -1
2148
fi
2149
2150
offset = next_line_start + 1
2151
current_line = current_line + 1
2152
od
2153
2154
let line_end mut = text.index_of('\n', offset)
2155
2156
if line_end < 0 then
2157
line_end = text.length
2158
fi
2159
2160
let result = offset + column - 1
2161
2162
if result > line_end then
2163
return -1
2164
fi
2165
2166
return result
2167
si
2168
si
2169
2170
// Does a full compile of all files.
2171
class COMPILE_HANDLER(
2172
_watchdog: WATCHDOG,
2173
_timers: TIMERS,
2174
_compiler: COMPILER,
2175
_source_files: Iterable[SOURCE_FILE],
2176
_build_flags: GLOBAL_BUILD_FLAGS
2177
): RequestHandler[Protocol.Request.COMPILE] is
2178
super()
2179
2180
// Whether an edit has arrived that supersedes the compile now
2181
// running. Consulted at per-file boundaries of the
2182
// expressions-only walk, which is the one branch below that can
2183
// be abandoned part-way: stopping there leaves the remaining
2184
// files at up-to-expressions with no use-map entries, which is
2185
// what a single-file edit's rebuild leaves anyway and what
2186
// is_full_compile_needed already describes. Unset outside
2187
// analysis mode, and never consulted by the whole-project
2188
// rebuild, which has no such halfway state.
2189
is_superseded: (() -> bool)? public
2190
2191
// A flipped function's callers are enumerable from the recorded
2192
// references only when calls bind the function itself through
2193
// ordinary name lookup: a named, source-declared function that
2194
// its owner's scope resolves to directly (alone or through its
2195
// overload group). Everything else - reflected imports whose
2196
// trust changed, and accessor functions reached through a
2197
// property or indexer read, which record the use against the
2198
// property symbol - has callers this scan can't see.
2199
_has_complete_references(function: Semantic.Symbols.Function) -> bool is
2200
if function.is_reflected then
2201
return false
2202
fi
2203
2204
let name = function.name
2205
2206
let owner = cast Semantic.Symbols.Scoped?(function.owner)
2207
2208
if !owner? then
2209
return false
2210
fi
2211
2212
let found = owner.find_direct(name)
2213
2214
if !found? then
2215
return false
2216
fi
2217
2218
if found == cast Semantic.Symbols.Symbol(function) then
2219
return true
2220
fi
2221
2222
if isa Semantic.Symbols.FUNCTION_GROUP(found) then
2223
return (cast Semantic.Symbols.FUNCTION_GROUP(found)).functions |> any(f => f == function)
2224
fi
2225
2226
return false
2227
si
2228
2229
_was_superseded() -> bool is
2230
let superseded = is_superseded
2231
2232
return superseded? /\ superseded()
2233
si
2234
2235
handle_request(request: Protocol.Request.COMPILE, writer: IO.TextWriter) is
2236
let diagnostics = Collections.LIST[Protocol.DIAGNOSTIC]()
2237
let checked_paths = Collections.LIST[string]()
2238
2239
let compile_needed mut = false
2240
2241
// Spans the whole handler, whichever branch it takes, so the
2242
// elapsed time reported to the client covers what the client
2243
// waited for. The per-branch timers below split that total.
2244
_timers.start(TIMERS.compile_timer_name)
2245
2246
try
2247
if !_watchdog.want_restart then
2248
if !_compiler.is_full_compile_needed then
2249
// No interface-affecting edit pending — the rebuild is
2250
// skipped. Timed under `compile-skipped` so the
2251
// skipped-vs-run split shows in the stats dump.
2252
_timers.start("compile-skipped")
2253
2254
// The incremental EDIT paths and the targeted
2255
// recompile above record reliances without
2256
// judging them; re-solve the effect relations
2257
// over the edited bodies and judge here, inside
2258
// the analysis window so the errors are
2259
// expression-level diagnostics.
2260
_timers.start("infer-effects")
2261
IoC.CONTAINER.instance.logger.start_analysis()
2262
_compiler.run_effects_pass(_source_files)
2263
_timers.finish("infer-effects")
2264
2265
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, diagnostics, checked_paths)
2266
2267
_timers.finish("compile-skipped")
2268
elif _compiler.are_tables_current then
2269
// A full compile is needed because some files'
2270
// expression-level diagnostics are stale, but the
2271
// declaration-level tables already reflect every
2272
// file's source - the interface-affecting EDIT that
2273
// raised the flag rebuilt them. Skip the redundant
2274
// clear-and-rebuild and run compile-expressions for
2275
// just the files that lack it. No CLEAR_STATE: the
2276
// rebuild cleared these files and re-resolved their
2277
// type expressions, which the expression walk
2278
// consumes.
2279
_timers.start("compile-run-expressions-only")
2280
// these bodies join the descent at the top; see
2281
// COMPILER._restart_descent
2282
Syntax.Process.KILL_LEDGER.kills_suspended = true
2283
2284
IoC.CONTAINER.instance.logger.start_analysis()
2285
2286
for i in _source_files do
2287
if _was_superseded() then
2288
break
2289
fi
2290
2291
if !_compiler.is_compiled_through_expressions(i) then
2292
// Cleared as each file is walked rather
2293
// than for every file before the walk
2294
// starts, so that abandoning the walk
2295
// cannot leave a file cleared and not
2296
// recomputed. Such a file reports as
2297
// checked with nothing against it, which
2298
// a client reads as having come back
2299
// clean and acts on by dropping the
2300
// squiggles it still has.
2301
IoC.CONTAINER.instance.logger.clear_expression_diagnostics(i.file_name)
2302
Syntax.Process.PURE_SLOTS.clear_for(i.file_name)
2303
Syntax.Process.KILL_LEDGER.clear_for(i.file_name)
2304
2305
_compiler.compile_expressions_only(i)
2306
fi
2307
od
2308
2309
if _was_superseded() then
2310
// The walk is part-done: the files it
2311
// reached are current and the rest hold what
2312
// the previous compile left, which is what
2313
// an edit's own rebuild leaves too. Nothing
2314
// is reported because the reliance judge
2315
// below never ran, so what the store holds
2316
// for the walked files is incomplete. Say a
2317
// compile is still needed; the edit that
2318
// superseded this one answers next.
2319
_timers.bump("compile-superseded")
2320
2321
compile_needed = true
2322
else
2323
// judge the reliances the expression recompiles
2324
// above just recorded, against freshly-solved
2325
// effect relations
2326
_timers.start("infer-effects")
2327
_compiler.run_effects_pass(_source_files)
2328
_timers.finish("infer-effects")
2329
2330
// No build ran on this path, so the entry
2331
// point is settled here. The branch below
2332
// does build, and settles it there.
2333
_compiler.select_entry_point(_source_files)
2334
2335
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, diagnostics, checked_paths)
2336
2337
_watchdog.note_full_compile()
2338
2339
_compiler.is_full_compile_needed = false
2340
fi
2341
2342
_timers.finish("compile-run-expressions-only")
2343
elif _source_files |> any(f => true) then
2344
// A compile can arrive before the first EDIT has
2345
// registered any source files. Building then is not
2346
// a harmless no-op: it clears the symbol table and
2347
// runs the pass barriers with nothing queued, so the
2348
// same reasoning as compile_all's empty-state guard
2349
// applies — answer from the empty state instead;
2350
// the first real EDIT performs the initial build.
2351
_timers.start("compile-run")
2352
2353
Semantic.Types.NAMED.clear_cache()
2354
2355
for i in _source_files do
2356
IoC.CONTAINER.instance.logger.clear(i.file_name, true)
2357
2358
i.want_compile_up_to_expressions = true
2359
i.want_compile_expressions = true
2360
od
2361
2362
let clear_state = Syntax.Process.CLEAR_STATE_VISITOR()
2363
for i in _source_files do
2364
clear_state.apply(i.definition)
2365
od
2366
2367
IoC.CONTAINER.instance.state_store_registry.clear_all()
2368
2369
IoC.CONTAINER.instance.logger.start_analysis()
2370
2371
_compiler.clear_symbols()
2372
2373
_compiler.queue(_source_files)
2374
2375
_compiler.build()
2376
2377
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, diagnostics, checked_paths)
2378
2379
_watchdog.note_full_compile()
2380
2381
_compiler.is_full_compile_needed = false
2382
2383
_timers.finish("compile-run")
2384
fi
2385
fi
2386
2387
catch ex: Exception
2388
debug_always("FULL COMPILE caught: {ex.get_type()} {ex.message}")
2389
2390
_watchdog.request_restart()
2391
finally
2392
IoC.CONTAINER.instance.logger.end_analysis()
2393
2394
_compiler.clear_queue()
2395
2396
_timers.finish(TIMERS.compile_timer_name)
2397
2398
let elapsed_ms = _timers.compile_timer.max_average_milliseconds
2399
2400
2401
JSON_PROTOCOL.write_response(
2402
writer,
2403
Protocol.Response.DIAGNOSTICS(diagnostics, checked_paths, "full", elapsed_ms, compile_needed)
2404
)
2405
yrt
2406
si
2407
si
2408
2409
class COMPLETION_HANDLER(
2410
_watchdog: WATCHDOG,
2411
_completer: Syntax.Process.COMPLETER,
2412
_source_file_lookup: SourceFileLookup,
2413
_full_compiler: FULL_COMPILER
2414
): RequestHandler[Protocol.Request.COMPLETE] is
2415
super()
2416
2417
handle_request(request: Protocol.Request.COMPLETE, writer: IO.TextWriter) is
2418
let path = request.path
2419
let target_line = request.line
2420
let target_column = request.column
2421
2422
let source_file = _source_file_lookup.find_source_file(path)
2423
2424
if !source_file? \/ !_full_compiler.is_compiled_through_expressions(source_file) then
2425
_full_compiler.compile_all(writer, path)
2426
fi
2427
2428
let items = Collections.LIST[Protocol.COMPLETION_ITEM]()
2429
2430
try
2431
if !_watchdog.want_restart then
2432
let results = find_completions(path, target_line, target_column)
2433
2434
// The client asked because the user typed a `.`, but
2435
// there is no member access at that position in the
2436
// text this analyser holds — so the text is older
2437
// than the client's and the enclosing scope is not
2438
// an answer to what was asked. Offering it anyway
2439
// fills the list with locals, globals, namespaces
2440
// and statement keywords, none of which can follow
2441
// a `.`.
2442
let answers_the_question =
2443
!request.is_member_trigger \/ _completer.is_member_completion
2444
2445
if results? /\ answers_the_question then
2446
for pair in results do
2447
let is_operator = Lexical.TOKENIZER.is_operator_name(pair.key)
2448
2449
items.add(
2450
Protocol.COMPLETION_ITEM(
2451
pair.key,
2452
if is_operator then
2453
cast int(Semantic.Symbols.CompletionKind.OPERATOR)
2454
else
2455
cast int(pair.value.completion_kind)
2456
fi,
2457
pair.value.signature,
2458
pair.value.kind_label,
2459
if is_operator then
2460
operator_insert_text(pair.key)
2461
else
2462
""
2463
fi
2464
)
2465
)
2466
od
2467
fi
2468
2469
if answers_the_question then
2470
for keyword in _completer.keyword_results do
2471
items.add(
2472
Protocol.COMPLETION_ITEM(
2473
keyword.name,
2474
cast int(Semantic.Symbols.CompletionKind.KEYWORD),
2475
"",
2476
"keyword",
2477
keyword.snippet
2478
)
2479
)
2480
od
2481
fi
2482
fi
2483
catch ex: Exception
2484
debug_always("COMPLETION caught: {ex.get_type()} {ex.message}")
2485
2486
_watchdog.request_restart()
2487
yrt
2488
2489
Std.error.flush()
2490
2491
JSON_PROTOCOL.write_response(writer, Protocol.Response.COMPLETION(items))
2492
si
2493
2494
// What to insert for an operator member offered after a `.`.
2495
// Inserting the bare name would put the operator's characters
2496
// straight after the dot, where the tokenizer scans them as one
2497
// run and produces a single operator token (`.=~`) rather than a
2498
// member access. The backtick escape is the spelling that always
2499
// lexes as a member name, whatever the operator is made of.
2500
//
2501
// The client renders this as an LSP snippet, so the two
2502
// characters that carry meaning there are escaped; `$` and `\`
2503
// are both operator characters, so both can genuinely occur.
2504
operator_insert_text(name: string) -> string is
2505
let buffer = System.Text.StringBuilder("`")
2506
2507
for c in name do
2508
if c == '$' \/ c == '\\' then
2509
buffer.append('\\')
2510
fi
2511
2512
buffer.append(c)
2513
od
2514
2515
return buffer.to_string()
2516
si
2517
2518
find_completions(path: string, target_line: int, target_column: int) -> Iterable[Pair[string,Semantic.Symbols.Symbol]]? is
2519
let i = _source_file_lookup.find_source_file(path)
2520
2521
if !i? then
2522
return null
2523
fi
2524
2525
return _completer.find_completions(i.definition, target_line, target_column)
2526
si
2527
si
2528
2529
class SIGNATURE_HANDLER(
2530
_watchdog: WATCHDOG,
2531
_signature_help: Syntax.Process.SIGNATURE_HELP,
2532
_source_file_lookup: SourceFileLookup
2533
): RequestHandler[Protocol.Request.SIGNATURE] is
2534
super()
2535
2536
handle_request(request: Protocol.Request.SIGNATURE, writer: IO.TextWriter) is
2537
let best_signature_index: int mut = 0
2538
let current_parameter_index: int mut = 0
2539
let signatures = Collections.LIST[Protocol.SIGNATURE_DTO]()
2540
2541
try
2542
let path = request.path
2543
let target_line = request.line
2544
let target_column = request.column
2545
2546
if !_watchdog.want_restart then
2547
let results = find_signatures(path, target_line, target_column)
2548
2549
if results? then
2550
best_signature_index = results.best_signature_index
2551
current_parameter_index = results.current_parameter_index
2552
2553
for signature in results.signatures do
2554
signatures.add(to_signature_dto(signature))
2555
od
2556
fi
2557
fi
2558
catch ex: Exception
2559
debug_always("SIGNATURE caught: {ex.get_type()} {ex.message}")
2560
2561
_watchdog.request_restart()
2562
yrt
2563
2564
Std.error.flush()
2565
2566
JSON_PROTOCOL.write_response(
2567
writer,
2568
Protocol.Response.SIGNATURE(best_signature_index, current_parameter_index, signatures)
2569
)
2570
si
2571
2572
to_signature_dto(signature: Syntax.Process.SIGNATURE) -> Protocol.SIGNATURE_DTO is
2573
let parameters = Collections.LIST[string]()
2574
2575
for parameter_description in signature.parameter_descriptions do
2576
parameters.add(parameter_description)
2577
od
2578
2579
return Protocol.SIGNATURE_DTO(signature.description, parameters)
2580
si
2581
2582
find_signatures(path: string, target_line: int, target_column: int) -> Syntax.Process.SIGNATURE_HELP_RESULT? is
2583
let i = _source_file_lookup.find_source_file(path)
2584
2585
if !i? then
2586
return null
2587
fi
2588
2589
return _signature_help.find_signatures(i.definition, target_line, target_column)
2590
si
2591
si
2592
2593
class SYMBOLS_HANDLER(
2594
_watchdog: WATCHDOG,
2595
_symbol_definition_locations: Semantic.SYMBOL_DEFINITION_LOCATIONS,
2596
_source_file_lookup: SourceFileLookup,
2597
_full_compiler: FULL_COMPILER
2598
): RequestHandler[Protocol.Request.SYMBOLS] is
2599
super()
2600
2601
handle_request(request: Protocol.Request.SYMBOLS, writer: IO.TextWriter) is
2602
let path = request.path
2603
2604
let files = Collections.LIST[Protocol.SYMBOL_FILE]()
2605
2606
try
2607
if !_watchdog.want_restart then
2608
if path? /\ path.length > 0 then
2609
// VSCode can race the initial outline request in ahead
2610
// of the first project COMPILE; without this re-compile
2611
// the outline stays empty until the user provokes one
2612
// another way (e.g. workspace symbol search).
2613
if !_symbol_definition_locations.has_definitions_for(path) then
2614
_full_compiler.compile_all(writer, path)
2615
fi
2616
2617
add_symbol_file(files, path, _symbol_definition_locations.find_definitions_from_file(path, false))
2618
else
2619
_full_compiler.compile_all(writer)
2620
2621
for i in _source_file_lookup.file_names do
2622
if i.length > 0 then
2623
add_symbol_file(files, i, _symbol_definition_locations.find_definitions_from_file(i, true))
2624
fi
2625
od
2626
fi
2627
fi
2628
catch ex: Exception
2629
debug_always("SYMBOLS caught: {ex.get_type()} {ex.message}")
2630
2631
_watchdog.request_restart()
2632
yrt
2633
2634
Std.error.flush()
2635
2636
JSON_PROTOCOL.write_response(writer, Protocol.Response.SYMBOLS(files))
2637
si
2638
2639
add_symbol_file(files: Collections.LIST[Protocol.SYMBOL_FILE], path: string, symbols: Iterable[Semantic.Symbols.Symbol]?) is
2640
let symbol_file = Protocol.SYMBOL_FILE(path)
2641
2642
if symbols? then
2643
for symbol in symbols |> filter(s => !s.is_internal) do
2644
try
2645
symbol_file.symbols.add(to_symbol_dto(symbol))
2646
catch ex: Exception
2647
yrt
2648
od
2649
fi
2650
2651
files.add(symbol_file)
2652
si
2653
2654
to_symbol_dto(symbol: Semantic.Symbols.Symbol) -> Protocol.SYMBOL_DTO =>
2655
let qualified_name = symbol.qualified_name in
2656
let qualifier = qualified_name.substring(0, qualified_name.length - symbol.name.length - 1) in
2657
Protocol.SYMBOL_DTO(
2658
symbol.search_description,
2659
cast int(symbol.symbol_kind),
2660
symbol.span.start_line,
2661
symbol.span.start_column,
2662
symbol.span.end_line,
2663
symbol.span.end_column,
2664
symbol.location.start_line,
2665
symbol.location.start_column,
2666
qualifier
2667
)
2668
si
2669
2670
class REFERENCES_HANDLER(
2671
_watchdog: WATCHDOG,
2672
_symbol_use_locations: Semantic.SYMBOL_USE_LOCATIONS,
2673
_full_compiler: FULL_COMPILER
2674
): RequestHandler[Protocol.Request.REFERENCES] is
2675
super()
2676
2677
handle_request(request: Protocol.Request.REFERENCES, writer: IO.TextWriter) is
2678
let locations = Collections.LIST[Protocol.LOCATION_DTO]()
2679
2680
try
2681
let path = request.path
2682
let line = request.line
2683
let column = request.column
2684
2685
if !_watchdog.want_restart then
2686
let symbol mut = _symbol_use_locations.find_definition_from_use(path, line, column)
2687
2688
let references mut =
2689
if symbol? then
2690
_symbol_use_locations.find_references_to_symbol(symbol)
2691
else
2692
null
2693
fi
2694
2695
// A non-local symbol's references span the whole project. A
2696
// file walked only up to expressions contributes its
2697
// type-annotation uses but none of its member-access, call,
2698
// or construction uses, so a use map built while any file
2699
// is short of expressions is silently incomplete even when
2700
// non-empty. Completeness therefore hinges on every file
2701
// reaching expressions. Locals are scoped to one function
2702
// body in the edited file, which is always fully compiled,
2703
// so an empty answer for them is authoritative.
2704
let needs_full_compile =
2705
!symbol? \/
2706
(!symbol.is_local /\ !_full_compiler.all_compiled_through_expressions())
2707
2708
if needs_full_compile then
2709
_full_compiler.compile_all(writer)
2710
2711
symbol = _symbol_use_locations.find_definition_from_use(path, line, column)
2712
2713
if symbol? then
2714
references = _symbol_use_locations.find_references_to_symbol(symbol)
2715
fi
2716
fi
2717
2718
if references? then
2719
append_location_dtos(locations, references)
2720
fi
2721
fi
2722
2723
catch ex: Exception
2724
debug_always("REFERENCES caught: {ex.get_type()} {ex.message}")
2725
yrt
2726
2727
Std.error.flush()
2728
2729
JSON_PROTOCOL.write_response(writer, Protocol.Response.REFERENCES(locations))
2730
si
2731
si
2732
2733
class TYPE_DEFINITION_HANDLER(
2734
_watchdog: WATCHDOG,
2735
_symbol_use_locations: Semantic.SYMBOL_USE_LOCATIONS,
2736
_full_compiler: FULL_COMPILER
2737
): RequestHandler[Protocol.Request.TYPE_DEFINITION] is
2738
super()
2739
2740
handle_request(request: Protocol.Request.TYPE_DEFINITION, writer: IO.TextWriter) is
2741
let path = request.path
2742
let line = request.line
2743
let column = request.column
2744
2745
let locations = Collections.LIST[Protocol.LOCATION_DTO]()
2746
2747
try
2748
if !_watchdog.want_restart then
2749
let symbol mut = _symbol_use_locations.find_definition_from_use(path, line, column)
2750
2751
if !symbol? then
2752
_full_compiler.compile_all(writer, path)
2753
2754
symbol = _symbol_use_locations.find_definition_from_use(path, line, column)
2755
fi
2756
2757
if symbol? then
2758
let type_symbol = type_symbol_of(symbol)
2759
2760
if type_symbol? /\ !type_symbol.is_internal /\ !type_symbol.is_reflected then
2761
append_location_dtos(locations, [type_symbol.location])
2762
fi
2763
fi
2764
fi
2765
catch e: Exception
2766
debug_always("TYPEDEFINITION caught: {e.get_type()}: {e.message}")
2767
_watchdog.request_restart()
2768
yrt
2769
2770
Std.error.flush()
2771
2772
JSON_PROTOCOL.write_response(writer, Protocol.Response.TYPE_DEFINITION(locations))
2773
si
2774
2775
// If the cursor sits on a type symbol itself (class/trait/struct/union/variant),
2776
// return that symbol — the type IS the type. Otherwise hop to the symbol's
2777
// declared type and return its symbol. Returns null when no useful answer
2778
// can be given.
2779
type_symbol_of(symbol: Semantic.Symbols.Symbol) -> Semantic.Symbols.Symbol? is
2780
if symbol.is_type then
2781
return symbol
2782
fi
2783
2784
let t = symbol.type
2785
2786
return t?.symbol
2787
si
2788
si
2789
2790
class IMPLEMENTATION_HANDLER(
2791
_watchdog: WATCHDOG,
2792
_symbol_use_locations: Semantic.SYMBOL_USE_LOCATIONS,
2793
_full_compiler: FULL_COMPILER
2794
): RequestHandler[Protocol.Request.IMPLEMENTATION] is
2795
super()
2796
2797
handle_request(request: Protocol.Request.IMPLEMENTATION, writer: IO.TextWriter) is
2798
let locations = Collections.LIST[Protocol.LOCATION_DTO]()
2799
2800
try
2801
let path = request.path
2802
let line = request.line
2803
let column = request.column
2804
2805
if !_watchdog.want_restart then
2806
let symbol mut = _symbol_use_locations.find_definition_from_use(path, line, column)
2807
2808
let implementations mut =
2809
if symbol? then
2810
_symbol_use_locations.find_implementations_of_symbol(symbol)
2811
else
2812
null
2813
fi
2814
2815
// Locals can't be inherited, so skip the compile_all
2816
// fallback for them. For non-locals, only recompile if
2817
// we didn't find anything — a warm cache answers
2818
// directly.
2819
let needs_full_compile =
2820
!symbol? \/
2821
(!symbol.is_local /\ (!implementations? \/ implementations |> count() == 0))
2822
2823
if needs_full_compile then
2824
_full_compiler.compile_all(writer)
2825
2826
symbol = _symbol_use_locations.find_definition_from_use(path, line, column)
2827
2828
if symbol? then
2829
implementations = _symbol_use_locations.find_implementations_of_symbol(symbol)
2830
fi
2831
fi
2832
2833
if implementations? then
2834
append_location_dtos(locations, implementations)
2835
fi
2836
fi
2837
2838
catch ex: Exception
2839
debug_always("IMPLEMENTATION caught: {ex.get_type()} {ex.message}")
2840
yrt
2841
2842
Std.error.flush()
2843
2844
JSON_PROTOCOL.write_response(writer, Protocol.Response.IMPLEMENTATION(locations))
2845
si
2846
si
2847
2848
class RENAME_REQUEST_HANDLER(
2849
_watchdog: WATCHDOG,
2850
_symbol_use_locations: Semantic.SYMBOL_USE_LOCATIONS,
2851
_full_compiler: FULL_COMPILER
2852
): RequestHandler[Protocol.Request.RENAME] is
2853
super()
2854
2855
handle_request(request: Protocol.Request.RENAME, writer: IO.TextWriter) is
2856
let edits = Collections.LIST[Protocol.RENAME_EDIT]()
2857
2858
try
2859
let path = request.path
2860
let line = request.line
2861
let column = request.column
2862
let new_name = request.new_name
2863
2864
if !_watchdog.want_restart then
2865
let symbol mut = _symbol_use_locations.find_definition_from_use(path, line, column)
2866
2867
let locations mut =
2868
if symbol? then
2869
_symbol_use_locations.find_references_to_symbol_for_rename(symbol)
2870
else
2871
null
2872
fi
2873
2874
// Locals can't be renamed across files, so the local-only
2875
// edit set is authoritative. A non-local rename spans the
2876
// whole project; a file walked only up to expressions
2877
// contributes its type-annotation uses but none of its
2878
// member-access, call, or construction uses, so the edit
2879
// set looks non-empty yet silently misses those
2880
// occurrences. Completeness hinges on every file reaching
2881
// expressions.
2882
let needs_full_compile =
2883
!symbol? \/
2884
(!symbol.is_local /\ !_full_compiler.all_compiled_through_expressions())
2885
2886
if needs_full_compile then
2887
_full_compiler.compile_all(writer)
2888
2889
symbol = _symbol_use_locations.find_definition_from_use(path, line, column)
2890
2891
if symbol? then
2892
locations = _symbol_use_locations.find_references_to_symbol_for_rename(symbol)
2893
fi
2894
fi
2895
2896
if locations? then
2897
for location in locations do
2898
edits.add(
2899
Protocol.RENAME_EDIT(
2900
location.file_name,
2901
location.start_line,
2902
location.start_column,
2903
location.end_line,
2904
location.end_column,
2905
new_name
2906
)
2907
)
2908
od
2909
fi
2910
fi
2911
2912
catch ex: Exception
2913
debug_always("RENAMEREQUEST caught: {ex.get_type()} {ex.message}")
2914
yrt
2915
2916
Std.error.flush()
2917
2918
JSON_PROTOCOL.write_response(writer, Protocol.Response.RENAME(edits))
2919
si
2920
si
2921
2922
class RESTART_HANDLER(_watchdog: WATCHDOG): RequestHandler[Protocol.Request.RESTART] is
2923
super()
2924
2925
handle_request(request: Protocol.Request.RESTART, writer: IO.TextWriter) is
2926
_watchdog.recycle(writer, "client requested restart")
2927
si
2928
si
2929
2930
// Records the client's current open-file set, replacing it wholesale.
2931
// Fire-and-forget — no response frame. Editor-only hints are produced
2932
// only for open files; see Logging.Logger.set_open_files.
2933
//
2934
// Hint gating changed for the files that were opened or closed, and
2935
// for no others, so those files' expression walks are the whole of
2936
// what this invalidates: their compile-expressions state is dropped
2937
// and their compiled-through marker cleared, which puts them in the
2938
// set the next COMPILE's expressions-only pass re-walks. Clearing the
2939
// state here rather than re-walking it defers the cost to the
2940
// debounced compile, so opening a file costs nothing at the moment it
2941
// opens.
2942
class SET_OPEN_FILES_HANDLER(
2943
_compiler: COMPILER,
2944
_source_files: SourceFileLookup
2945
): RequestHandler[Protocol.Request.SET_OPEN_FILES] is
2946
super()
2947
2948
handle_request(request: Protocol.Request.SET_OPEN_FILES, writer: IO.TextWriter) is
2949
let changed = IoC.CONTAINER.instance.logger.set_open_files(request.paths)
2950
2951
let any_invalidated mut = false
2952
2953
for path in changed do
2954
if let source_file = _source_files.find_source_file(path) then
2955
_compiler.invalidate_file_expressions(source_file)
2956
2957
any_invalidated = true
2958
fi
2959
od
2960
2961
if any_invalidated then
2962
_compiler.is_full_compile_needed = true
2963
fi
2964
si
2965
si
2966
2967
// Synthesizes quick fixes for one file over one range, on demand.
2968
//
2969
// Fixes are deliberately not attached to the diagnostics a compile
2970
// reports. Synthesis walks the whole AST of every file holding a coded
2971
// diagnostic, comparing every node against every diagnostic in that
2972
// file, so attaching them to an EDIT response costs the whole project
2973
// on every keystroke - and almost all of it is discarded, because the
2974
// user only ever asks for actions at one cursor position. Answering a
2975
// range instead makes the cost independent of both project size and
2976
// diagnostic count.
2977
//
2978
// Reads the diagnostics the last compile left in the store; it never
2979
// recompiles. A code action is a follow-up to squiggles the client is
2980
// already showing, so the store holds the same compile those came
2981
// from.
2982
class CODE_ACTIONS_HANDLER(
2983
_watchdog: WATCHDOG,
2984
_source_files: SourceFileLookup
2985
): RequestHandler[Protocol.Request.CODE_ACTIONS] is
2986
super()
2987
2988
handle_request(request: Protocol.Request.CODE_ACTIONS, writer: IO.TextWriter) is
2989
let in_range = Collections.LIST[Protocol.DIAGNOSTIC]()
2990
2991
try
2992
if !_watchdog.want_restart then
2993
let all = Collections.LIST[Protocol.DIAGNOSTIC]()
2994
let checked_paths = Collections.LIST[string]()
2995
2996
DIAGNOSTICS_COLLECTOR.collect_into(IoC.CONTAINER.instance.logger, all, checked_paths)
2997
2998
for d in all do
2999
if d.path =~ request.path /\ _overlaps(request, d) then
3000
in_range.add(d)
3001
fi
3002
od
3003
3004
let source_file = _source_files.find_source_file(request.path)
3005
3006
if in_range.count > 0 /\ source_file? then
3007
let one = Collections.LIST[SOURCE_FILE]()
3008
one.add(source_file)
3009
3010
QUICK_FIX_SYNTHESIZER.attach_fixes(in_range, one)
3011
fi
3012
fi
3013
catch ex: Exception
3014
debug_always("CODE_ACTIONS caught: {ex.get_type()} {ex.message}")
3015
3016
_watchdog.request_restart()
3017
yrt
3018
3019
Std.error.flush()
3020
3021
JSON_PROTOCOL.write_response(writer, Protocol.Response.CODE_ACTIONS(in_range))
3022
si
3023
3024
// Both ranges are 1-based with exclusive end columns, so they are
3025
// disjoint only when one ends strictly before the other starts.
3026
// Touching counts as overlapping: a cursor sitting at either end
3027
// of a squiggle is the position the user asks for a fix from, and
3028
// a bare cursor is a zero-width range that would otherwise miss
3029
// every diagnostic it is inside.
3030
_overlaps(request: Protocol.Request.CODE_ACTIONS, d: Protocol.DIAGNOSTIC) -> bool static =>
3031
!(
3032
_is_before(d.end_line, d.end_column, request.start_line, request.start_column) \/
3033
_is_before(request.end_line, request.end_column, d.start_line, d.start_column)
3034
)
3035
3036
_is_before(line: int, column: int, other_line: int, other_column: int) -> bool static =>
3037
line < other_line \/ (line == other_line /\ column < other_column)
3038
si
3039
3040
// Handles the heap_check request: an explicit request to sample the heap.
3041
// The VS Code extension sends one during a lull in editing, so the
3042
// watchdog's forced GC lands outside the latency path of an interactive
3043
// request rather than after every compile.
3044
class HEAP_CHECK_HANDLER(_watchdog: WATCHDOG): RequestHandler[Protocol.Request.HEAP_CHECK] is
3045
super()
3046
3047
handle_request(request: Protocol.Request.HEAP_CHECK, writer: IO.TextWriter) is
3048
// If the heap has grown past the recycle thresholds, check_heap
3049
// writes a RESTART response and exits; otherwise we ack so the
3050
// client's request/response loop completes.
3051
_watchdog.check_heap_on_request(writer)
3052
3053
JSON_PROTOCOL.write_response(writer, Protocol.Response.HEAP_CHECK())
3054
si
3055
si
3056
3057
// Adds assemblies to the reference set: the earlier cells of an
3058
// interactive session, which the analyser cannot see until they have
3059
// been compiled. Any file can resolve differently against the larger
3060
// set, so the retained tables no longer describe the project and the
3061
// next build is a whole-project rebuild rather than an incremental
3062
// one.
3063
class ADD_REFERENCES_HANDLER(_compiler: COMPILER): RequestHandler[Protocol.Request.ADD_REFERENCES] is
3064
super()
3065
3066
handle_request(request: Protocol.Request.ADD_REFERENCES, writer: IO.TextWriter) is
3067
let message: string? mut = null
3068
3069
try
3070
Semantic.DotNet.ADDITIONAL_REFERENCES.add(IoC.CONTAINER.instance, request.paths)
3071
catch ex: Exception
3072
message = ex.message
3073
yrt
3074
3075
_compiler.are_tables_current = false
3076
_compiler.is_full_compile_needed = true
3077
3078
JSON_PROTOCOL.write_response(writer, Protocol.Response.ADD_REFERENCES(message))
3079
si
3080
si
3081
3082
// Answers a STATS request with a snapshot of the analyser's timers,
3083
// read from the shared TIMERS instance the EDIT and COMPILE handlers
3084
// accumulate into. The edit-path tallies let a test assert which path
3085
// handled an edit - the interface-incremental counter rising by one,
3086
// and the full-rebuild counter not, is the signal that an EDIT was
3087
// served incrementally rather than by falling back.
3088
class STATS_HANDLER(_timers: TIMERS): RequestHandler[Protocol.Request.STATS] is
3089
super()
3090
3091
handle_request(request: Protocol.Request.STATS, writer: IO.TextWriter) is
3092
let entries = Collections.LIST[Protocol.STAT_ENTRY]()
3093
3094
for timer in _timers.all do
3095
entries.add(
3096
Protocol.STAT_ENTRY(
3097
timer.name,
3098
timer.execute_count,
3099
timer.moving_average_milliseconds
3100
)
3101
)
3102
od
3103
3104
JSON_PROTOCOL.write_response(writer, Protocol.Response.STATS(entries))
3105
si
3106
si
3107
3108
// Reformats a single file. The buffer is parsed fresh — no symbol table, no
3109
// dependence on prior EDIT state — and the reformatted text returned whole.
3110
// On any failure the original buffer is echoed back unchanged, so a format
3111
// request can never corrupt it.
3112
class FORMAT_HANDLER(
3113
_compiler: COMPILER,
3114
_build_flags: GLOBAL_BUILD_FLAGS
3115
): RequestHandler[Protocol.Request.FORMAT] is
3116
super()
3117
3118
handle_request(request: Protocol.Request.FORMAT, writer: IO.TextWriter) is
3119
let path = request.path
3120
let source = request.source
3121
3122
let result mut = source
3123
3124
try
3125
// Parse into a throwaway logger so a format request never
3126
// mutates the analyser's shared diagnostics store or its
3127
// speculation state stack.
3128
let format_logger = Logging.DIAGNOSTICS_STORE()
3129
3130
let source_file =
3131
_compiler.parse(
3132
path,
3133
IO.StringReader(source),
3134
_build_flags.want_compile_up_to_expressions,
3135
_build_flags.want_compile_expressions,
3136
false,
3137
format_logger
3138
)
3139
3140
if !format_logger.any_errors then
3141
let formatter =
3142
Syntax.Process.Printer.FORMATTER(source_file.trivia, 100)
3143
3144
result = formatter.format(source_file.definition)
3145
fi
3146
catch ex: Exception
3147
debug_always("FORMAT caught: {ex.get_type()} {ex.message}")
3148
yrt
3149
3150
JSON_PROTOCOL.write_response(writer, Protocol.Response.FORMAT(result))
3151
si
3152
si
3153
3154
// Reformats just the run of definitions/statements covering a requested
3155
// range. The response carries the whole-line span actually replaced and the
3156
// reformatted text; a zero span (all zeros) means nothing was formatted (the
3157
// buffer is left untouched).
3158
class FORMATRANGE_HANDLER(
3159
_compiler: COMPILER,
3160
_build_flags: GLOBAL_BUILD_FLAGS
3161
): RequestHandler[Protocol.Request.FORMAT_RANGE] is
3162
super()
3163
3164
handle_request(request: Protocol.Request.FORMAT_RANGE, writer: IO.TextWriter) is
3165
let path = request.path
3166
let start_line = request.start_line
3167
let start_column = request.start_column
3168
let end_line = request.end_line
3169
let end_column = request.end_column
3170
let source = request.source
3171
3172
let text: string mut = ""
3173
let response_start_line: int mut = 0
3174
let response_start_column: int mut = 0
3175
let response_end_line: int mut = 0
3176
let response_end_column: int mut = 0
3177
3178
try
3179
// Parse into a throwaway logger so a format request never
3180
// mutates the analyser's shared diagnostics store or its
3181
// speculation state stack.
3182
let format_logger = Logging.DIAGNOSTICS_STORE()
3183
3184
let source_file =
3185
_compiler.parse(
3186
path, IO.StringReader(source), _build_flags.want_compile_up_to_expressions, _build_flags.want_compile_expressions, false, format_logger
3187
)
3188
3189
if !format_logger.any_errors then
3190
let root = cast Syntax.Trees.Definitions.LIST?(source_file.definition)
3191
3192
if root? then
3193
let locator =
3194
Syntax.Process.Printer.RANGE_LOCATOR(
3195
start_line, start_column, end_line, end_column
3196
)
3197
3198
let target = locator.locate(root)
3199
3200
if target? then
3201
// the run's own leading indentation is not part of
3202
// the replaced span — continuation lines must be
3203
// re-indented back to it
3204
let continuation_indent = target.start_column - 1
3205
3206
let formatter =
3207
Syntax.Process.Printer.FORMATTER(
3208
_trivia_in_lines(
3209
source_file.trivia,
3210
target.start_line,
3211
target.end_line
3212
),
3213
100 - continuation_indent
3214
)
3215
3216
text = _reindent(formatter.format(target.node), continuation_indent)
3217
3218
response_start_line = target.start_line
3219
response_start_column = target.start_column
3220
response_end_line = target.end_line
3221
// exclusive end of the span to replace, one past
3222
// the run's last character (and any ';' the
3223
// statement node's location does not cover)
3224
response_end_column = _span_end_column(source, target) + 1
3225
fi
3226
fi
3227
fi
3228
catch ex: Exception
3229
debug_always("FORMATRANGE caught: {ex.get_type()} {ex.message}")
3230
yrt
3231
3232
JSON_PROTOCOL.write_response(
3233
writer,
3234
Protocol.Response.FORMAT_RANGE(response_start_line, response_start_column, response_end_line, response_end_column, text)
3235
)
3236
si
3237
3238
_trivia_in_lines(
3239
trivia: Collections.Iterable[Lexical.TRIVIA]?,
3240
start_line: int,
3241
end_line: int
3242
) -> Collections.Iterable[Lexical.TRIVIA] is
3243
let result = Collections.LIST[Lexical.TRIVIA]()
3244
3245
if trivia? then
3246
for t in trivia do
3247
let line = t.location.start_line
3248
if line >= start_line /\ line <= end_line then
3249
result.add(t)
3250
fi
3251
od
3252
fi
3253
3254
return result
3255
si
3256
3257
_char_index(source: string, line: int, column: int) -> int is
3258
let i mut = 0
3259
let current_line mut = 1
3260
while current_line < line /\ i < source.length do
3261
if source[i] == '\n' then
3262
current_line = current_line + 1
3263
fi
3264
i = i + 1
3265
od
3266
return i + column - 1
3267
si
3268
3269
// The column of the run's last character to replace. A statement's
3270
// trailing ';' is not part of its node location, so if one follows the
3271
// run it is swallowed into the span — otherwise the formatted text
3272
// (which re-emits the ';') would leave the original stranded.
3273
_span_end_column(source: string, target: Syntax.Process.Printer.RANGE_TARGET) -> int is
3274
let end_index = _char_index(source, target.end_line, target.end_column)
3275
3276
let scan mut = end_index + 1
3277
while
3278
scan < source.length /\
3279
(source[scan] == ' ' \/ source[scan] == '\t')
3280
do
3281
scan = scan + 1
3282
od
3283
3284
if scan < source.length /\ source[scan] == ';' then
3285
return target.end_column + (scan - end_index)
3286
fi
3287
3288
return target.end_column
3289
si
3290
3291
// Re-indent the formatted run for splicing back at its original
3292
// position: the first line goes in after the run's existing leading
3293
// indentation (which is not part of the replaced span), so it stays
3294
// bare; every later line is indented to that same depth. The trailing
3295
// newline the formatter appends is dropped.
3296
_reindent(body: string, continuation_indent: int) -> string is
3297
let prefix = System.Text.StringBuilder()
3298
let p mut = 0
3299
while p < continuation_indent do
3300
prefix.append(' ')
3301
p = p + 1
3302
od
3303
let prefix_string = prefix.to_string()
3304
3305
let result = System.Text.StringBuilder()
3306
let lines = Collections.LIST[string](body.split(['\n']))
3307
let index mut = 0
3308
3309
while index < lines.count do
3310
let line = lines[index]
3311
3312
if index == lines.count - 1 /\ line.length == 0 then
3313
break
3314
fi
3315
3316
if index > 0 then
3317
result.append('\n')
3318
if line.length > 0 then
3319
result.append(prefix_string)
3320
fi
3321
fi
3322
3323
result.append(line)
3324
index = index + 1
3325
od
3326
3327
return result.to_string()
3328
si
3329
si
3330
3331
// Handles describe_type: resolve a type by qualified name (dotted) and
3332
// return its kind, ancestors and members. The resolver walks name
3333
// components through find_direct starting at the symbol table's global
3334
// scope, which for imported .NET types transparently triggers dotnet
3335
// symbol loading. Trailing generic brackets are stripped so
3336
// `Collections.LIST[int]` resolves to `LIST`; generic argument
3337
// substitution isn't applied to member signatures yet.
3338
class DESCRIBE_TYPE_HANDLER(
3339
_watchdog: WATCHDOG,
3340
_symbol_table: Semantic.SYMBOL_TABLE,
3341
_full_compiler: FULL_COMPILER
3342
): RequestHandler[Protocol.Request.DESCRIBE_TYPE] is
3343
super()
3344
3345
handle_request(request: Protocol.Request.DESCRIBE_TYPE, writer: IO.TextWriter) is
3346
let resolved_name mut = ""
3347
let type_kind mut = ""
3348
let ancestors = Collections.LIST[string]()
3349
let members = Collections.LIST[Protocol.MEMBER_DTO]()
3350
3351
try
3352
if !_watchdog.want_restart then
3353
let symbol mut = resolve_type_expression(request.type_expression)
3354
3355
// A miss can mean either "unknown name" or "symbol table
3356
// not yet primed" - the initial analyser state has neither
3357
// project source EDIT'd nor .NET types loaded on demand.
3358
// Force a compile on the first miss and retry.
3359
if !symbol? then
3360
_full_compiler.compile_all(writer)
3361
symbol = resolve_type_expression(request.type_expression)
3362
fi
3363
3364
if symbol? then
3365
resolved_name = symbol.qualified_name
3366
type_kind = kind_name(symbol)
3367
3368
if let classy = cast Semantic.Symbols.Classy?(symbol) then
3369
collect_ancestors(classy, ancestors)
3370
collect_members(classy, members)
3371
else
3372
if symbol.is_namespace then
3373
collect_namespace_members(symbol, members)
3374
fi
3375
fi
3376
fi
3377
fi
3378
catch ex: Exception
3379
debug_always("DESCRIBETYPE caught: {ex.get_type()} {ex.message}")
3380
_watchdog.request_restart()
3381
yrt
3382
3383
Std.error.flush()
3384
3385
JSON_PROTOCOL.write_response(
3386
writer,
3387
Protocol.Response.DESCRIBE_TYPE(resolved_name, type_kind, ancestors, members)
3388
)
3389
si
3390
3391
// Resolve a dotted type expression. Try the .NET symbol table
3392
// directly first - `get_symbol("System.Text.StringBuilder")` loads
3393
// reflected types on demand. If that misses, walk the dotted
3394
// components through the symbol table's global scope, which finds
3395
// ghūl-declared namespaces and types. Any trailing generic-argument
3396
// brackets are dropped for the walk; the resolved symbol is the
3397
// type template itself.
3398
resolve_type_expression(expression: string?) -> Semantic.Symbols.Symbol? is
3399
if !expression? \/ expression.length == 0 then
3400
return null
3401
fi
3402
3403
let stripped = strip_generic_arguments(expression)
3404
3405
if stripped.length == 0 then
3406
return null
3407
fi
3408
3409
let dotnet_symbol_table = IoC.CONTAINER.instance.dotnet_symbol_table.value
3410
let dotnet_symbol = dotnet_symbol_table.get_symbol(stripped)
3411
3412
if dotnet_symbol? then
3413
return cast Semantic.Symbols.Symbol(dotnet_symbol)
3414
fi
3415
3416
let components = stripped.split(['.'])
3417
3418
if components.count == 0 then
3419
return null
3420
fi
3421
3422
let symbol: Semantic.Symbols.Symbol? mut = _symbol_table.global_scope.find_direct(components[0])
3423
3424
if !symbol? then
3425
return null
3426
fi
3427
3428
let index mut = 1
3429
3430
while index < components.count do
3431
symbol = symbol!.find_direct(components[index])
3432
3433
if !symbol? then
3434
return null
3435
fi
3436
3437
index = index + 1
3438
od
3439
3440
return symbol
3441
si
3442
3443
// Strip a top-level `[...]` suffix. `LIST[int]` -> `LIST`,
3444
// `Ghul.MAP[string, int]` -> `Ghul.MAP`. Anything before an inner
3445
// `[` inside a component (rare) is preserved via the substring.
3446
strip_generic_arguments(expression: string) -> string is
3447
let bracket = expression.index_of('[')
3448
3449
if bracket >= 0 then
3450
return expression.substring(0, bracket)
3451
fi
3452
3453
return expression
3454
si
3455
3456
kind_name(symbol: Semantic.Symbols.Symbol) -> string static is
3457
if symbol.is_namespace then return "namespace"; fi
3458
if symbol.is_variant then return "variant"; fi
3459
if symbol.is_union then return "union"; fi
3460
if symbol.is_trait then return "trait"; fi
3461
// ENUM_STRUCT extends STRUCT, so it must be tested first or an
3462
// enum falls through to the "struct" branch.
3463
if isa Semantic.Symbols.ENUM_STRUCT(symbol) then return "enum"; fi
3464
if isa Semantic.Symbols.STRUCT(symbol) then return "struct"; fi
3465
if symbol.is_class then return "class"; fi
3466
3467
return ""
3468
si
3469
3470
collect_ancestors(classy: Semantic.Symbols.Classy, ancestors: Collections.LIST[string]) is
3471
for ancestor_type in classy.ancestors do
3472
let name = ancestor_type.symbol.qualified_name
3473
3474
if !ancestors.contains(name) then
3475
ancestors.add(name)
3476
fi
3477
od
3478
si
3479
3480
// Members come from two sources: those declared directly on the
3481
// derived type, and those declared on an ancestor. The resolve-
3482
// overrides pass pulls inherited members into the derived's own
3483
// symbol store (wrapping methods in a FUNCTION_GROUP owned by the
3484
// derived), so `owner` can't distinguish inheritance reliably.
3485
// Instead: walk the derived's direct list first and mark those
3486
// direct; then walk each ancestor and mark any not-yet-seen
3487
// member inherited. A member reported as direct on both sides
3488
// (e.g. an interface method reflection copied into the class)
3489
// stays tagged direct.
3490
collect_members(classy: Semantic.Symbols.Classy, members: Collections.LIST[Protocol.MEMBER_DTO]) is
3491
let seen = Collections.SET[string]()
3492
3493
add_matches(classy, "", false, members, seen)
3494
3495
for ancestor_type in classy.ancestors do
3496
let ancestor = ancestor_type.symbol
3497
3498
if isa Semantic.Symbols.Classy(ancestor) then
3499
add_matches(ancestor, ancestor.qualified_name, true, members, seen)
3500
fi
3501
od
3502
si
3503
3504
// For a plain namespace, no ancestor semantics apply.
3505
collect_namespace_members(
3506
scope: Semantic.Symbols.Symbol,
3507
members: Collections.LIST[Protocol.MEMBER_DTO]
3508
) is
3509
add_matches(scope, "", false, members, Collections.SET[string]())
3510
si
3511
3512
add_matches(
3513
scope: Semantic.Symbols.Symbol,
3514
inherited_from: string,
3515
is_inherited: bool,
3516
members: Collections.LIST[Protocol.MEMBER_DTO],
3517
seen: Collections.SET[string]
3518
) is
3519
let matches = Collections.MAP[string, Semantic.Symbols.Symbol]()
3520
3521
scope.find_direct_matches("", matches)
3522
3523
for pair in matches do
3524
let name = pair.key
3525
let symbol = pair.value
3526
3527
if symbol.is_internal \/ seen.contains(name) then
3528
continue
3529
fi
3530
3531
seen.add(name)
3532
3533
members.add(
3534
Protocol.MEMBER_DTO(
3535
name,
3536
symbol.signature,
3537
symbol.kind_label,
3538
cast int(symbol.completion_kind),
3539
is_inherited,
3540
inherited_from
3541
)
3542
)
3543
od
3544
si
3545
si
3546
si