Appearance
| 1 | namespace Analysis is | |
| 2 | use System.Exception; | |
| 3 | use IO.Std; | |
| 4 | ||
| 5 | use Collections.Iterable; | |
| 6 | ||
| 7 | use Pair = Collections.KeyValuePair; | |
| 8 | ||
| 9 | use 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 |