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