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