mt-lang 0.3.37 → 0.3.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/docs/lsp-performance.md +347 -0
  3. data/lib/milk_tea/base.rb +1 -1
  4. data/lib/milk_tea/core/c_backend/reinterpret.rb +2 -2
  5. data/lib/milk_tea/core/c_backend.rb +12 -0
  6. data/lib/milk_tea/core/compile_time.rb +46 -17
  7. data/lib/milk_tea/core/lexer.rb +12 -0
  8. data/lib/milk_tea/core/lowering/resolve.rb +2 -3
  9. data/lib/milk_tea/core/lowering.rb +12 -0
  10. data/lib/milk_tea/core/module_loader.rb +59 -37
  11. data/lib/milk_tea/core/parser.rb +12 -0
  12. data/lib/milk_tea/core/semantic_analyzer/attributes.rb +2 -1
  13. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +8 -20
  14. data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +2 -1
  15. data/lib/milk_tea/core/semantic_analyzer/statements.rb +10 -10
  16. data/lib/milk_tea/core/semantic_analyzer/top_level.rb +9 -11
  17. data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +14 -5
  18. data/lib/milk_tea/core/semantic_analyzer.rb +6 -31
  19. data/lib/milk_tea/lsp/diagnostics.rb +15 -5
  20. data/lib/milk_tea/lsp/server/code_actions.rb +12 -2
  21. data/lib/milk_tea/lsp/server/completion.rb +100 -77
  22. data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +4 -6
  23. data/lib/milk_tea/lsp/server/formatting.rb +184 -60
  24. data/lib/milk_tea/lsp/server/hover.rb +321 -41
  25. data/lib/milk_tea/lsp/server/inlay_hints.rb +33 -11
  26. data/lib/milk_tea/lsp/server/lifecycle.rb +12 -0
  27. data/lib/milk_tea/lsp/server/references.rb +3 -1
  28. data/lib/milk_tea/lsp/server/selection_range.rb +4 -4
  29. data/lib/milk_tea/lsp/server/semantic_tokens.rb +63 -20
  30. data/lib/milk_tea/lsp/server/text_documents.rb +2 -2
  31. data/lib/milk_tea/lsp/server/type_hierarchy.rb +7 -7
  32. data/lib/milk_tea/lsp/server/utilities.rb +4 -0
  33. data/lib/milk_tea/lsp/server.rb +9 -0
  34. data/lib/milk_tea/lsp/workspace/analysis.rb +14 -3
  35. data/lib/milk_tea/lsp/workspace/caches.rb +17 -1
  36. data/lib/milk_tea/lsp/workspace/collection.rb +4 -0
  37. data/lib/milk_tea/lsp/workspace/module_index.rb +134 -0
  38. data/lib/milk_tea/lsp/workspace/store.rb +9 -4
  39. data/lib/milk_tea/lsp/workspace.rb +8 -0
  40. data/lib/milk_tea/packages/manifest.rb +9 -0
  41. metadata +5 -3
@@ -14,6 +14,18 @@ module MilkTea
14
14
  def code
15
15
  "module/error"
16
16
  end
17
+
18
+ def to_diagnostic(path: nil)
19
+ Diagnostic.new(
20
+ path: @path || path,
21
+ line: @line,
22
+ column: @column,
23
+ length: nil,
24
+ code: code,
25
+ message: message,
26
+ severity: :error,
27
+ )
28
+ end
17
29
  end
18
30
 
19
31
  class ModuleLoader
@@ -69,11 +81,10 @@ module MilkTea
69
81
  suffix_platform = platform_suffix_for_path(path)
70
82
  return suffix_platform if suffix_platform
71
83
 
72
- manifest_platform = PackageManifest.load(path).platform
84
+ manifest = PackageManifest.load_option(path)
85
+ manifest_platform = manifest&.platform
73
86
  return manifest_platform if manifest_platform
74
87
 
75
- normalize_platform_name(host_platform || default_host_platform)
76
- rescue PackageManifestError
77
88
  normalize_platform_name(host_platform || default_host_platform)
78
89
  end
79
90
 
@@ -167,7 +178,10 @@ module MilkTea
167
178
 
168
179
  def check_program(path)
169
180
  with_check_context(path) do |root_path|
170
- check_program_parallel(root_path)
181
+ errors = []
182
+ check_program_parallel(root_path, collecting_errors: errors)
183
+ raise errors.first if errors.any?
184
+
171
185
  build_program(root_path)
172
186
  end
173
187
  end
@@ -313,7 +327,7 @@ module MilkTea
313
327
  check_path(resolved_path)
314
328
  rescue SemanticError => e
315
329
  @analysis_cache[resolved_path] = previous if previous
316
- collecting_errors << e if collecting_errors
330
+ collecting_errors << e
317
331
  end
318
332
  @forward_bindings.clear
319
333
 
@@ -322,13 +336,14 @@ module MilkTea
322
336
  # are not themselves part of a cycle; they just depended on cycle members.
323
337
  (levels + [tail_members]).each do |level_paths|
324
338
  if level_paths.length == 1
325
- check_path(level_paths.first)
339
+ begin
340
+ check_path(level_paths.first)
341
+ rescue SemanticError => e
342
+ collecting_errors << e
343
+ end
326
344
  elsif level_paths.any?
327
- check_level_parallel(level_paths)
345
+ collecting_errors.concat(check_level_parallel(level_paths))
328
346
  end
329
- rescue SemanticError => e
330
- raise unless collecting_errors
331
- collecting_errors << e
332
347
  end
333
348
  end
334
349
 
@@ -369,28 +384,31 @@ module MilkTea
369
384
  levels
370
385
  end
371
386
 
387
+ # Checks a level of independent modules on worker threads. Each worker
388
+ # returns { ok: analysis } or { error: e } as an ordinary value; the join
389
+ # point collects the failed outcomes instead of re-raising them, so the
390
+ # caller decides whether to propagate or accumulate.
372
391
  def check_level_parallel(paths)
373
- threads = paths.map do |resolved_path|
392
+ workers = paths.map do |resolved_path|
374
393
  Thread.new do
375
- Thread.current[:resolved_path] = resolved_path
376
394
  begin
377
- analysis = check_path(resolved_path)
378
- Thread.current[:analysis] = analysis
395
+ { ok: check_path(resolved_path) }
379
396
  rescue ModuleLoadError, PackageLockError, SemanticError => e
380
- Thread.current[:error] = e
397
+ { error: e }
381
398
  end
382
399
  end
383
400
  end
384
401
 
385
- threads.each(&:join)
402
+ workers.each(&:join)
386
403
 
387
- paths.zip(threads).each do |resolved_path, t|
388
- raise t[:error] if t[:error]
389
- end
404
+ workers.map(&:value).filter_map { |result| result[:error] }
390
405
  end
391
406
 
392
407
  def imported_modules_for_ast(ast, importer_path: nil)
393
- resolve_imports_for_ast(ast, importer_path:, collecting: false)
408
+ result = resolve_imports_for_ast(ast, importer_path:, collecting: false)
409
+ raise result.errors.first.error if result.errors.any?
410
+
411
+ result.modules
394
412
  end
395
413
 
396
414
  def imported_modules_for_ast_collecting_errors(ast, importer_path: nil)
@@ -421,29 +439,27 @@ module MilkTea
421
439
  modules[import.path.to_s] = @binder.module_binding(import_analysis)
422
440
  end
423
441
  rescue ModuleLoadError, PackageLockError, SemanticError => e
424
- raise unless collecting
425
-
426
- handle_circular_import_in_collecting_mode(import, import_path, modules, errors, e)
442
+ if collecting
443
+ handle_circular_import_in_collecting_mode(import, import_path, modules, errors, e)
444
+ else
445
+ errors << ImportResolutionError.new(import:, error: e)
446
+ end
427
447
  end
428
448
  end
429
449
 
430
450
  begin
431
451
  @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: collecting)
432
452
  rescue ModuleLoadError, PackageLockError => e
433
- raise unless collecting
434
453
  errors << ImportResolutionError.new(import: nil, error: e)
435
454
  end
436
455
 
437
456
  begin
438
457
  @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: collecting)
439
458
  rescue ModuleLoadError, PackageLockError => e
440
- raise unless collecting
441
459
  errors << ImportResolutionError.new(import: nil, error: e)
442
460
  end
443
461
 
444
- return ImportResolution.new(modules: modules.freeze, errors: errors.freeze) if collecting
445
-
446
- modules.freeze
462
+ ImportResolution.new(modules: modules.freeze, errors: errors.freeze)
447
463
  ensure
448
464
  @import_resolve_depth -= 1 if @import_resolve_depth
449
465
  end
@@ -528,7 +544,7 @@ module MilkTea
528
544
  if use_shared_cache?
529
545
  entry = @shared_cache[resolved_path]
530
546
  if entry
531
- mtime = File.mtime(resolved_path).to_f rescue nil
547
+ mtime = source_mtime(resolved_path)
532
548
  if mtime && entry[:mtime] == mtime
533
549
  @analysis_cache[resolved_path] = entry[:analysis]
534
550
  return [resolved_path, nil, entry[:analysis]]
@@ -540,10 +556,19 @@ module MilkTea
540
556
  [resolved_path, ast, nil]
541
557
  end
542
558
 
559
+ # Option-style source modification time: nil when the file's mtime cannot
560
+ # be read (e.g. the file was removed between resolution and stat), so
561
+ # shared-cache reads simply miss instead of raising.
562
+ def source_mtime(path)
563
+ File.mtime(path).to_f
564
+ rescue SystemCallError
565
+ nil
566
+ end
567
+
543
568
  def update_shared_cache(resolved_path, analysis)
544
569
  return unless use_shared_cache?
545
570
 
546
- mtime = File.mtime(resolved_path).to_f rescue nil
571
+ mtime = source_mtime(resolved_path)
547
572
  @shared_cache[resolved_path] = { mtime:, analysis: } if mtime
548
573
  end
549
574
 
@@ -575,11 +600,7 @@ module MilkTea
575
600
  end
576
601
 
577
602
  def inferred_module_name_for_path(path)
578
- manifest = begin
579
- PackageManifest.load(path)
580
- rescue PackageManifestError
581
- nil
582
- end
603
+ manifest = PackageManifest.load_option(path)
583
604
 
584
605
  if manifest && path_within_root?(path, manifest.source_root)
585
606
  return module_name_for_path(path, manifest.source_root)
@@ -632,8 +653,9 @@ module MilkTea
632
653
  import_result = resolve_imports_for_ast(ast, importer_path: resolved_path, collecting: true)
633
654
  result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules: import_result.modules, path: resolved_path)
634
655
  @analysis_cache[resolved_path] = result[:analysis] if result[:analysis]
635
- rescue StandardError
636
- # Analysis capture is best-effort.
656
+ rescue ModuleLoadError, PackageLockError, SemanticError
657
+ # Analysis capture is best-effort; named load/check failures leave the
658
+ # cycle member to the pass-2 re-check instead.
637
659
  ensure
638
660
  @checking_paths.pop
639
661
  end
@@ -31,6 +31,18 @@ module MilkTea
31
31
  def code
32
32
  "parse/error"
33
33
  end
34
+
35
+ def to_diagnostic(path: nil)
36
+ Diagnostic.new(
37
+ path: @path || path,
38
+ line: line,
39
+ column: column,
40
+ length: nil,
41
+ code: code,
42
+ message: message,
43
+ severity: :error,
44
+ )
45
+ end
34
46
  end
35
47
 
36
48
  class SyntaxTokenStream
@@ -9,7 +9,8 @@ module MilkTea
9
9
  case decl
10
10
  when AST::StructDecl
11
11
  packed, alignment = check_decl_attribute_applications!(decl.attributes, target_kind: :struct, target_label: "struct #{decl.name}", target_node: decl)
12
- @ctx.types.fetch(decl.name).set_layout(packed:, alignment:)
12
+ struct_type = @ctx.types[decl.name]
13
+ struct_type.set_layout(packed:, alignment:) if struct_type
13
14
 
14
15
  decl.fields.each do |field|
15
16
  with_error_node(field) do
@@ -249,32 +249,20 @@ module MilkTea
249
249
  end
250
250
  end
251
251
 
252
- def check_functions
253
- @ctx.top_level_functions.each_value do |binding|
254
- check_function(binding)
255
- end
256
-
257
- @ctx.methods.each_value do |method_map|
258
- method_map.each_value do |binding|
259
- check_function(binding)
260
- end
261
- end
262
- end
263
-
264
252
  # Validates the body of a specialized (instantiated) function or method
265
- # binding. The owner checker may be in collecting-errors mode, which
266
- # would silently swallow body errors into @structural_errors. We
267
- # temporarily disable collect mode on the owner so the caller receives
268
- # the SemanticError directly.
253
+ # binding. The owner checker always accumulates body errors into its
254
+ # structural buffer, so this pulls out the new errors and surfaces the
255
+ # first non-tolerated one directly; tolerated ones are dropped.
269
256
  def validate_specialized_function_body!(binding)
270
257
  owner = binding.owner
271
- prev_collecting = owner.instance_variable_get(:@collecting_errors)
272
- owner.instance_variable_set(:@collecting_errors, false)
258
+ structural_errors = owner.instance_variable_get(:@structural_errors)
259
+ prev_count = structural_errors.length
273
260
  owner.check_function(binding)
261
+ new_errors = structural_errors[prev_count..].to_a
262
+ structural_errors.slice!(prev_count..) unless new_errors.empty?
263
+ new_errors.each { |error| raise error unless error.message.include?("cannot assign through immutable") }
274
264
  rescue SemanticError => e
275
265
  raise unless e.message.include?("cannot assign through immutable")
276
- ensure
277
- owner.instance_variable_set(:@collecting_errors, prev_collecting) if owner
278
266
  end
279
267
 
280
268
  # Per-function error collection used by check_collecting_errors.
@@ -9,7 +9,8 @@ module MilkTea
9
9
  next unless decl.is_a?(AST::StructDecl) || decl.is_a?(AST::OpaqueDecl)
10
10
  next if decl.implements.empty?
11
11
 
12
- receiver_type = @ctx.types.fetch(decl.name)
12
+ receiver_type = @ctx.types[decl.name]
13
+ next unless receiver_type
13
14
  resolved_interfaces = []
14
15
  seen = {}
15
16
 
@@ -34,17 +34,17 @@ module MilkTea
34
34
  end
35
35
  record_local_completion_snapshot(end_line, 1_000_000, nested_scopes)
36
36
  rescue SemanticError => e
37
- if @collecting_errors
38
- @structural_errors << e
39
- next
37
+ if e.line.nil? && statement.line
38
+ e = SemanticError.new(
39
+ e.message,
40
+ line: statement.line,
41
+ column: source_column(statement),
42
+ length: source_length(statement),
43
+ path: @path,
44
+ )
40
45
  end
41
-
42
- raise e unless e.line.nil?
43
-
44
- stmt_line = statement.line
45
- raise e if stmt_line.nil?
46
-
47
- raise_sema_error(e.message, statement)
46
+ @structural_errors << e
47
+ next
48
48
  end
49
49
  end
50
50
  end
@@ -18,7 +18,8 @@ module MilkTea
18
18
  collect_structural_error(e)
19
19
  end
20
20
  when AST::VarDecl
21
- binding = @ctx.top_level_values.fetch(decl.name)
21
+ binding = @ctx.top_level_values[decl.name]
22
+ next unless binding
22
23
  if decl.value
23
24
  validate_consuming_foreign_expression!(decl.value, scopes: [], root_allowed: false)
24
25
  validate_hoistable_foreign_expression!(decl.value, scopes: [], root_hoistable: false)
@@ -39,7 +40,8 @@ module MilkTea
39
40
  end
40
41
 
41
42
  def check_expr_const(decl)
42
- binding = @ctx.top_level_values.fetch(decl.name)
43
+ binding = @ctx.top_level_values[decl.name]
44
+ return unless binding
43
45
  validate_consuming_foreign_expression!(decl.value, scopes: [], root_allowed: false)
44
46
  validate_hoistable_foreign_expression!(decl.value, scopes: [], root_hoistable: false)
45
47
 
@@ -270,9 +272,7 @@ module MilkTea
270
272
  def evaluate_compile_time_block(statements, scopes: nil)
271
273
  ctx = CompileTime::BlockContext.new(self)
272
274
  result = ctx.evaluate_block(statements, scopes:)
273
- result
274
- rescue CompileTime::ReturnValue => e
275
- e.value
275
+ result.is_a?(CompileTime::ReturnOutcome) ? result.value : result
276
276
  rescue CompileTime::Error => e
277
277
  raise_sema_error(e.message)
278
278
  end
@@ -558,9 +558,8 @@ module MilkTea
558
558
  end
559
559
 
560
560
  ctx = CompileTime::BlockContext.new(self, initial_variables: initial_vars)
561
- ctx.evaluate_block(func.ast.body, scopes: nil)
562
- rescue CompileTime::ReturnValue => e
563
- e.value
561
+ result = ctx.evaluate_block(func.ast.body, scopes: nil)
562
+ result.is_a?(CompileTime::ReturnOutcome) ? result.value : result
564
563
  rescue CompileTime::Error => e
565
564
  raise_sema_error(e.message)
566
565
  end
@@ -589,9 +588,8 @@ module MilkTea
589
588
  end
590
589
 
591
590
  ctx = CompileTime::BlockContext.new(self, initial_variables: initial_vars)
592
- ctx.evaluate_block(func.ast.body, scopes: nil)
593
- rescue CompileTime::ReturnValue => e
594
- e.value
591
+ result = ctx.evaluate_block(func.ast.body, scopes: nil)
592
+ result.is_a?(CompileTime::ReturnOutcome) ? result.value : result
595
593
  rescue CompileTime::Error => e
596
594
  raise_sema_error(e.message)
597
595
  end
@@ -293,9 +293,15 @@ module MilkTea
293
293
 
294
294
  constraints = resolve_type_param_constraints(decl.type_params)
295
295
  if decl.is_a?(AST::InterfaceDecl)
296
- @ctx.interfaces[decl.name] = @ctx.interfaces.fetch(decl.name).with(type_param_constraints: constraints)
296
+ interface_binding = @ctx.interfaces[decl.name]
297
+ next unless interface_binding
298
+
299
+ @ctx.interfaces[decl.name] = interface_binding.with(type_param_constraints: constraints)
297
300
  else
298
- @ctx.types.fetch(decl.name).define_type_param_constraints(constraints)
301
+ type_binding = @ctx.types[decl.name]
302
+ next unless type_binding
303
+
304
+ type_binding.define_type_param_constraints(constraints)
299
305
  end
300
306
  end
301
307
  end
@@ -455,7 +461,8 @@ module MilkTea
455
461
  with_error_node(decl) do
456
462
  next unless decl.is_a?(AST::StructDecl) || decl.is_a?(AST::UnionDecl)
457
463
 
458
- struct_type = @ctx.types.fetch(decl.name)
464
+ struct_type = @ctx.types[decl.name]
465
+ next unless struct_type
459
466
  struct_type.ast_declaration = decl if struct_type.respond_to?(:ast_declaration=)
460
467
  type_params = if struct_type.is_a?(Types::GenericStructDefinition)
461
468
  seen = {}
@@ -581,7 +588,8 @@ module MilkTea
581
588
  with_error_node(decl) do
582
589
  next unless decl.is_a?(AST::EnumDecl) || decl.is_a?(AST::FlagsDecl)
583
590
 
584
- enum_type = @ctx.types.fetch(decl.name)
591
+ enum_type = @ctx.types[decl.name]
592
+ next unless enum_type
585
593
  backing_type = resolve_type_ref(decl.backing_type)
586
594
  unless backing_type.is_a?(Types::Primitive) && backing_type.integer?
587
595
  raise_sema_error("#{decl.name} backing type must be an integer primitive, got #{backing_type}")
@@ -641,7 +649,8 @@ module MilkTea
641
649
  with_error_node(decl) do
642
650
  next unless decl.is_a?(AST::VariantDecl)
643
651
 
644
- variant_type = @ctx.types.fetch(decl.name)
652
+ variant_type = @ctx.types[decl.name]
653
+ next unless variant_type
645
654
  type_params = if variant_type.is_a?(Types::GenericVariantDefinition)
646
655
  seen = {}
647
656
  variant_type.type_params.each_with_object({}) do |name, params|
@@ -187,31 +187,9 @@ module MilkTea
187
187
  end
188
188
 
189
189
  def check
190
- @completed_phases = Set.new
191
-
192
- run_phase(:install_builtin_types)
193
- run_phase(:install_builtin_attributes)
194
- run_phase(:install_imports)
195
- run_phase(:install_prelude_types, requires: [:install_imports])
196
- run_phase(:declare_named_types, requires: [:install_builtin_types, :install_imports, :install_prelude_types])
197
- run_phase(:resolve_generic_type_param_constraints, requires: [:declare_named_types])
198
- run_phase(:resolve_type_aliases, requires: [:declare_named_types])
199
- run_phase(:declare_attributes)
200
- run_phase(:predeclare_top_level_consts)
201
- run_phase(:resolve_aggregate_fields, requires: [:resolve_type_aliases, :declare_named_types])
202
- run_phase(:resolve_enum_members, requires: [:declare_named_types])
203
- run_phase(:resolve_variant_arms, requires: [:declare_named_types])
204
- run_phase(:collect_emit_declarations)
205
- run_phase(:declare_top_level_values, requires: [:resolve_aggregate_fields, :resolve_type_aliases])
206
- run_phase(:check_attribute_applications, requires: [:declare_attributes])
207
- run_phase(:declare_functions, requires: [:resolve_aggregate_fields, :resolve_enum_members, :resolve_variant_arms])
208
- run_phase(:check_interface_conformances, requires: [:declare_functions, :resolve_aggregate_fields])
209
- run_phase(:check_top_level_values, requires: [:declare_top_level_values])
210
- run_phase(:finalize_top_level_const_values, requires: [:check_top_level_values])
211
- run_phase(:check_top_level_static_asserts, requires: [:finalize_top_level_const_values])
212
- run_phase(:check_functions, requires: [:declare_functions, :resolve_aggregate_fields, :check_interface_conformances])
213
-
214
- build_analysis
190
+ result = check_collecting_errors
191
+ raise result[:errors].first unless result[:errors].empty?
192
+ result[:analysis]
215
193
  end
216
194
 
217
195
  def run_phase(name, requires: [])
@@ -291,12 +269,11 @@ module MilkTea
291
269
  end
292
270
  end
293
271
 
294
- # Like check, but collects per-function errors instead of raising at first.
295
- # Structural phases (imports, type resolution, declaration) collect errors per
296
- # declaration so that the maximum number of diagnostics are surfaced.
272
+ # Runs all sema phases and collects every error instead of stopping at
273
+ # the first one. Structural phases collect per-declaration, and
274
+ # function-body phases collect per function/method.
297
275
  # Returns { analysis: Analysis, errors: [SemanticError] }.
298
276
  def check_collecting_errors
299
- @collecting_errors = true
300
277
  @structural_errors = []
301
278
  @completed_phases = Set.new
302
279
 
@@ -353,8 +330,6 @@ module MilkTea
353
330
  end
354
331
 
355
332
  def collect_structural_error(error)
356
- raise error unless @collecting_errors
357
-
358
333
  @structural_errors << error
359
334
  end
360
335
  end
@@ -31,8 +31,10 @@ module MilkTea
31
31
  # Parse
32
32
  begin
33
33
  parse_start = total_start ? monotonic_time : nil
34
+ parse_errors = []
34
35
  ast = if path && File.file?(path)
35
36
  parse_result = Parser.parse_collecting_errors(content, path: uri)
37
+ parse_errors = parse_result.errors
36
38
  parse_result.errors.each { |error| diagnostics << format_error(error) }
37
39
  parse_result.ast
38
40
  else
@@ -63,6 +65,10 @@ module MilkTea
63
65
  source_overrides: source_overrides,
64
66
  workspace_root_path: workspace_root_path,
65
67
  content: content,
68
+ # A file whose own parse recovered with errors can poison the loader's
69
+ # program-check cache and yield no facts on the standard path; skip the
70
+ # program check and resolve imports directly for those files.
71
+ skip_program_check: !parse_errors.empty?,
66
72
  )
67
73
  imports_ms = elapsed_ms(imports_start) if imports_start
68
74
  unresolved_import_paths = imported_modules.fetch(:unresolved_import_paths)
@@ -167,7 +173,7 @@ module MilkTea
167
173
 
168
174
  private
169
175
 
170
- def self.resolve_imported_modules(uri, ast, diagnostics, resolution:, effective_platform:, shared_module_cache: nil, source_overrides: nil, workspace_root_path: nil, content: nil)
176
+ def self.resolve_imported_modules(uri, ast, diagnostics, resolution:, effective_platform:, shared_module_cache: nil, source_overrides: nil, workspace_root_path: nil, content: nil, skip_program_check: false)
171
177
  path = uri_to_path(uri)
172
178
  return { modules: {}, unresolved_import_paths: [], module_name: nil } unless path && File.file?(path)
173
179
 
@@ -186,10 +192,14 @@ module MilkTea
186
192
  # cached analysis and resolves correctly. If the program check fails
187
193
  # (e.g. missing module), fall through to the standard resolution
188
194
  # path so that prelude modules and regular errors are still reported.
189
- begin
190
- loader.check_program_collecting(path)
191
- rescue ModuleLoadError, PackageLockError
192
- # best-effort — standard path below handles diagnostics
195
+ # Skipped when the file's own parse recovered with errors: the program
196
+ # check poisons the loader cache and yields no facts on that path.
197
+ unless skip_program_check
198
+ begin
199
+ loader.check_program_collecting(path)
200
+ rescue ModuleLoadError, PackageLockError
201
+ # best-effort — standard path below handles diagnostics
202
+ end
193
203
  end
194
204
 
195
205
  resolution_result = loader.imported_modules_for_ast_collecting_errors(ast, importer_path: path)
@@ -404,6 +404,12 @@ module MilkTea
404
404
  end
405
405
 
406
406
  def handle_workspace_diagnostic(params)
407
+ progress = nil
408
+ if (work_done_token = params['workDoneToken'])
409
+ progress = create_progress_handle(@protocol, work_done_token)
410
+ progress.report(percentage: 0, message: 'Collecting workspace diagnostics...')
411
+ end
412
+
407
413
  previous_ids = params['previousResultIds'] || []
408
414
  prev_map = previous_ids.each_with_object({}) do |entry, h|
409
415
  h[entry['uri']] = entry['value'] if entry.is_a?(Hash) && entry['uri']
@@ -420,15 +426,19 @@ module MilkTea
420
426
 
421
427
  cached = @workspace_diagnostic_cache[uri]
422
428
  if cached && cached[:result_id] == prev_map[uri] && cached[:fingerprint] == fingerprint
423
- { uri: uri, kind: 'unchanged', resultId: result_id, items: [] }
429
+ { uri: uri, kind: 'unchanged', resultId: result_id, items: [], version: nil }
424
430
  else
425
431
  @workspace_diagnostic_cache[uri] = { result_id: result_id, fingerprint: fingerprint }
426
- { uri: uri, kind: 'full', resultId: result_id, items: diagnostics }
432
+ { uri: uri, kind: 'full', resultId: result_id, items: diagnostics, version: nil }
427
433
  end
428
434
  end
429
435
 
436
+ progress&.report(percentage: 100, message: "#{items.length} document#{items.length == 1 ? '' : 's'} checked")
437
+ progress&.done(message: 'Workspace diagnostics ready')
438
+
430
439
  { items: items }
431
440
  rescue StandardError => e
441
+ progress&.done(message: 'Workspace diagnostics failed')
432
442
  warn "Error in workspace/diagnostic handler: #{e.message}"
433
443
  { items: [] }
434
444
  end