mt-lang 0.3.34 → 0.3.38

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/docs/index.html +5 -5
  4. data/docs/language-design.md +3 -3
  5. data/docs/language-manual.md +1 -1
  6. data/lib/milk_tea/base.rb +1 -1
  7. data/lib/milk_tea/bindings/bindgen/type_mapper.rb +12 -2
  8. data/lib/milk_tea/bindings/bindgen.rb +5 -0
  9. data/lib/milk_tea/core/c_backend/aggregate_utils.rb +4 -0
  10. data/lib/milk_tea/core/c_backend/expressions.rb +25 -3
  11. data/lib/milk_tea/core/c_backend/reinterpret.rb +2 -2
  12. data/lib/milk_tea/core/c_backend/runtime_helpers.rb +15 -2
  13. data/lib/milk_tea/core/c_backend/type_collectors.rb +39 -2
  14. data/lib/milk_tea/core/c_backend.rb +12 -0
  15. data/lib/milk_tea/core/compile_time.rb +109 -74
  16. data/lib/milk_tea/core/lexer.rb +12 -0
  17. data/lib/milk_tea/core/lowering/block.rb +6 -7
  18. data/lib/milk_tea/core/lowering/declarations.rb +40 -1
  19. data/lib/milk_tea/core/lowering/loops.rb +25 -1
  20. data/lib/milk_tea/core/lowering/resolve.rb +13 -6
  21. data/lib/milk_tea/core/lowering/utils.rb +6 -2
  22. data/lib/milk_tea/core/lowering.rb +12 -0
  23. data/lib/milk_tea/core/module_loader.rb +59 -37
  24. data/lib/milk_tea/core/parser/statements.rb +35 -7
  25. data/lib/milk_tea/core/parser.rb +12 -0
  26. data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +4 -1
  27. data/lib/milk_tea/core/semantic_analyzer/attributes.rb +2 -1
  28. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +8 -20
  29. data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +2 -1
  30. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +12 -2
  31. data/lib/milk_tea/core/semantic_analyzer/statements.rb +10 -10
  32. data/lib/milk_tea/core/semantic_analyzer/top_level.rb +10 -12
  33. data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +14 -5
  34. data/lib/milk_tea/core/semantic_analyzer.rb +6 -31
  35. data/lib/milk_tea/lsp/server/code_actions.rb +12 -2
  36. data/lib/milk_tea/lsp/server/formatting.rb +172 -58
  37. data/lib/milk_tea/lsp/server/inlay_hints.rb +33 -11
  38. data/lib/milk_tea/lsp/server/selection_range.rb +4 -4
  39. data/lib/milk_tea/lsp/server/semantic_tokens.rb +9 -5
  40. data/lib/milk_tea/lsp/server/type_hierarchy.rb +5 -5
  41. data/lib/milk_tea/packages/manifest.rb +9 -0
  42. data/lib/milk_tea/tooling/linter/visitors.rb +4 -1
  43. data/std/box2d.mt +14 -14
  44. data/std/c/box2d.mt +19 -19
  45. metadata +3 -3
@@ -101,11 +101,21 @@ module MilkTea
101
101
 
102
102
  def find_method_by_receiver_name(module_binding, receiver_type, name)
103
103
  module_binding.methods.each do |key, methods|
104
- return methods[name] if key.is_a?(receiver_type.class) && key.name == receiver_type.name && methods.key?(name)
104
+ next unless key.is_a?(receiver_type.class)
105
+ next unless key.name == receiver_type.name
106
+ next unless same_type_module?(key, receiver_type)
107
+ return methods[name] if methods.key?(name)
105
108
  end
106
109
  nil
107
110
  end
108
111
 
112
+ def same_type_module?(type_a, type_b)
113
+ module_a = receiver_type_module_name(type_a)
114
+ module_b = receiver_type_module_name(type_b)
115
+ return true if module_a.nil? || module_b.nil?
116
+ module_a == module_b
117
+ end
118
+
109
119
  def reachable_module_binding_for_type(receiver_type)
110
120
  module_name = receiver_type_module_name(receiver_type)
111
121
  return nil unless module_name
@@ -709,7 +719,7 @@ module MilkTea
709
719
 
710
720
  def sized_layout_type?(type)
711
721
  case type
712
- when Types::Primitive, Types::Struct, Types::StructInstance, Types::Union, Types::Enum, Types::Flags, Types::Variant, Types::Span, Types::StringView, Types::Task, Types::Event, Types::Subscription
722
+ when Types::Primitive, Types::Struct, Types::StructInstance, Types::Union, Types::Enum, Types::Flags, Types::Variant, Types::Tuple, Types::Span, Types::StringView, Types::Task, Types::Event, Types::Subscription
713
723
  true
714
724
  when Types::Nullable
715
725
  true
@@ -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
 
@@ -173,7 +175,7 @@ module MilkTea
173
175
 
174
176
  if (type_ref = type_ref_from_specialization(callee))
175
177
  specialized_type = resolve_type_ref(type_ref)
176
- return if specialized_type.is_a?(Types::Struct) || result_type?(specialized_type)
178
+ return if specialized_type.is_a?(Types::Struct) || task_type?(specialized_type) || specialized_type.is_a?(Types::Vector) || specialized_type.is_a?(Types::Matrix) || specialized_type.is_a?(Types::Quaternion) || specialized_type.is_a?(Types::Simd)
177
179
  end
178
180
  end
179
181
 
@@ -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
@@ -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
@@ -16,7 +16,8 @@ module MilkTea
16
16
  # Enrich with hierarchical children from AST
17
17
  ast = @workspace.get_ast(uri)
18
18
  if ast && result
19
- enrich_with_children(result, ast)
19
+ facts = @workspace.get_facts(uri)
20
+ enrich_with_children(result, ast, facts)
20
21
  end
21
22
 
22
23
  module_name = resolve_outline_module_name(uri)
@@ -74,17 +75,42 @@ module MilkTea
74
75
  children
75
76
  end
76
77
 
77
- def enrich_with_children(symbols, ast)
78
+ def enrich_with_children(symbols, ast, facts)
78
79
  removed_local_names = []
79
80
  removed_method_names = []
80
81
  removed_nested_type_names = []
81
82
  removed_event_names = []
82
83
  name_index = symbols.each_with_object(Hash.new { |h, k| h[k] = [] }) { |s, h| h[s[:name]] << s }
83
84
 
84
- ast.declarations&.each do |decl|
85
+ flatten_module_declarations(ast.declarations).each do |decl|
85
86
  removed_nested_type_names.concat(collect_nested_type_names(decl)) if decl.is_a?(AST::StructDecl)
86
87
 
87
88
  case decl
89
+ when AST::VarDecl
90
+ parent = name_index[decl.name]&.find { |s| symbol_line(s) == (decl.line || 0) }
91
+ next unless parent
92
+
93
+ detail = decl.type ? type_detail_string(decl.type) : nil
94
+ detail ||= resolved_local_type_detail(decl, facts)
95
+ parent[:detail] = detail if detail
96
+
97
+ when AST::TypeAliasDecl
98
+ parent = name_index[decl.name]&.find { |s| symbol_line(s) == (decl.line || 0) }
99
+ next unless parent
100
+
101
+ if (detail = type_detail_string(decl.target))
102
+ parent[:detail] = "= #{detail}"
103
+ end
104
+
105
+ when AST::ExternFunctionDecl, AST::ForeignFunctionDecl
106
+ parent = name_index[decl.name]&.find { |s| symbol_line(s) == (decl.line || 0) }
107
+ next unless parent
108
+
109
+ detail_parts = []
110
+ detail_parts << 'async' if decl.respond_to?(:async) && decl.async
111
+ detail_parts << "-> #{type_detail_string(decl.return_type) || 'void'}"
112
+ parent[:detail] = detail_parts.join(' ')
113
+
88
114
  when AST::EventDecl
89
115
  parent = name_index[decl.name]&.find { |s| symbol_line(s) == (decl.line || 0) }
90
116
  next unless parent
@@ -100,26 +126,11 @@ module MilkTea
100
126
  detail_parts = []
101
127
  detail_parts << 'const' if decl.respond_to?(:const) && decl.const
102
128
  detail_parts << 'async' if decl.respond_to?(:async) && decl.async
103
- if (ret = type_detail_string(decl.return_type))
104
- detail_parts << "-> #{ret}"
105
- end
106
- parent[:detail] = detail_parts.join(' ') unless detail_parts.empty?
129
+ detail_parts << "-> #{type_detail_string(decl.return_type) || 'void'}"
130
+ parent[:detail] = detail_parts.join(' ')
107
131
 
108
132
  locals = collect_local_decls(decl.body)
109
- next unless locals&.any?
110
-
111
- parent[:children] ||= []
112
- parent_children = parent[:children]
113
- locals.each do |local|
114
- next unless local.name
115
-
116
- child = local_decl_symbol(local)
117
- next unless child
118
-
119
- parent_children << child unless parent_children.any? { |pc| pc[:name] == child[:name] }
120
- removed_local_names << local.name
121
- removed_local_names.concat(descendant_names(child))
122
- end
133
+ append_local_children(parent, locals, facts, removed_local_names)
123
134
  when AST::ExtendingBlock
124
135
  type_name_str = decl.type_name.name.parts.join('.')
125
136
 
@@ -153,20 +164,7 @@ module MilkTea
153
164
  removed_method_names << child[:name] if child[:kind] == 6
154
165
 
155
166
  locals = collect_local_decls(method.respond_to?(:body) ? method.body : nil)
156
- next unless locals&.any?
157
-
158
- child[:children] ||= []
159
- child_children = child[:children]
160
- locals.each do |local|
161
- next unless local.name
162
-
163
- local_child = local_decl_symbol(local)
164
- next unless local_child
165
-
166
- child_children << local_child unless child_children.any? { |pc| pc[:name] == local_child[:name] }
167
- removed_local_names << local.name
168
- removed_local_names.concat(descendant_names(local_child))
169
- end
167
+ append_local_children(child, locals, facts, removed_local_names)
170
168
  end
171
169
  when AST::ConstDecl
172
170
  parent = name_index[decl.name]&.find { |s| symbol_line(s) == (decl.line || 0) }
@@ -179,25 +177,13 @@ module MilkTea
179
177
  next unless decl.block_body
180
178
 
181
179
  locals = collect_local_decls(decl.block_body)
182
- next unless locals&.any?
183
-
184
- parent[:children] ||= []
185
- parent_children = parent[:children]
186
- locals.each do |local|
187
- next unless local.name
188
-
189
- child = local_decl_symbol(local)
190
- next unless child
191
-
192
- parent_children << child unless parent_children.any? { |pc| pc[:name] == child[:name] }
193
- removed_local_names << local.name
194
- removed_local_names.concat(descendant_names(child))
195
- end
180
+ append_local_children(parent, locals, facts, removed_local_names)
196
181
  else
197
182
  parent_name = child_parent_name(decl)
198
183
  parent = parent_name ? name_index[parent_name]&.find { |s| symbol_line(s) == (decl.line || 0) } : nil
199
184
  next unless parent
200
185
 
186
+ detail_parts = []
201
187
  if decl.is_a?(AST::StructDecl) && decl.implements&.any?
202
188
  ifaces = decl.implements.map { |i|
203
189
  base = i.respond_to?(:parts) ? i.parts.join('.') : i.name.parts.join('.')
@@ -210,9 +196,19 @@ module MilkTea
210
196
  end
211
197
  "#{base}#{args}"
212
198
  }.join(', ')
213
- parent[:detail] = "(#{ifaces})"
199
+ detail_parts << "(#{ifaces})"
200
+ end
201
+
202
+ if decl.respond_to?(:backing_type) && decl.backing_type && (bt = type_detail_string(decl.backing_type))
203
+ detail_parts << ": #{bt}"
204
+ end
205
+
206
+ if (generic = generic_signature_detail(decl))
207
+ detail_parts << generic
214
208
  end
215
209
 
210
+ parent[:detail] = detail_parts.join(' ') unless detail_parts.empty?
211
+
216
212
  type_params = expand_generic_type_params(decl)
217
213
  if type_params&.any?
218
214
  parent[:children] ||= []
@@ -374,14 +370,78 @@ module MilkTea
374
370
  line = (a.line) ? a.line : default_line
375
371
  return nil unless a.respond_to?(:name) && a.name && line
376
372
  col = a.column ? a.column : 1
373
+
374
+ detail = nil
375
+ if a.respond_to?(:fields) && a.fields&.any?
376
+ fields = a.fields.map { |f| "#{f.name}: #{type_detail_string(f.type)}" }.join(', ')
377
+ detail = "(#{fields})"
378
+ end
379
+
377
380
  {
378
381
  name: a.name, kind: 22,
382
+ detail: detail,
379
383
  range: { start: { line: line - 1, character: 0 }, end: { line: line, character: 0 } },
380
384
  selectionRange: {
381
385
  start: { line: line - 1, character: col - 1 },
382
386
  end: { line: line - 1, character: col - 1 + a.name.length },
383
387
  },
384
- }
388
+ }.compact
389
+ end
390
+
391
+ # Renders the generic parameter clause of a type declaration, e.g.
392
+ # `[A, B]` for `struct Pair[A, B]` or `[@a]` for `struct Buffer[@a]`.
393
+ def generic_signature_detail(decl)
394
+ parts = []
395
+ if decl.respond_to?(:lifetime_params) && decl.lifetime_params&.any?
396
+ parts.concat(decl.lifetime_params.map(&:to_s))
397
+ end
398
+ if decl.respond_to?(:type_params) && decl.type_params&.any?
399
+ parts.concat(decl.type_params.map { |tp| tp.respond_to?(:name) ? tp.name : tp.to_s })
400
+ end
401
+ parts.empty? ? nil : "[#{parts.join(', ')}]"
402
+ end
403
+
404
+ # Module-level `when` branches are compile-time conditionals; the token
405
+ # symbol scan lists their declarations, so flatten the branch bodies so
406
+ # the enrichment below can type them like ordinary top-level decls.
407
+ def flatten_module_declarations(declarations)
408
+ (declarations || []).flat_map do |decl|
409
+ if decl.is_a?(AST::WhenStmt)
410
+ (decl.branches || []).flat_map { |b| flatten_module_declarations(b.body) } +
411
+ flatten_module_declarations(decl.else_body)
412
+ else
413
+ [decl]
414
+ end
415
+ end
416
+ end
417
+
418
+ # Adds local declaration children to an outline symbol. Destructure
419
+ # locals (`let Vec2(x, y) = ...`) introduce a spurious flat variable
420
+ # symbol named after the destructure type; that symbol is collected for
421
+ # removal instead of being shown as a typed child.
422
+ def append_local_children(container, locals, facts, removed_local_names)
423
+ return unless locals&.any?
424
+
425
+ container[:children] ||= []
426
+ children = container[:children]
427
+ locals.each do |local|
428
+ if local.respond_to?(:destructure_bindings) && local.destructure_bindings
429
+ type_name = local.destructure_type_name
430
+ if type_name
431
+ name = type_name.is_a?(Array) ? type_name.join('.') : type_name.to_s
432
+ removed_local_names << name unless removed_local_names.include?(name)
433
+ end
434
+ next
435
+ end
436
+ next unless local.name
437
+
438
+ child = local_decl_symbol(local, facts:)
439
+ next unless child
440
+
441
+ children << child unless children.any? { |pc| pc[:name] == child[:name] }
442
+ removed_local_names << local.name
443
+ removed_local_names.concat(descendant_names(child))
444
+ end
385
445
  end
386
446
 
387
447
  def collect_local_decls(body)
@@ -417,7 +477,7 @@ module MilkTea
417
477
  end
418
478
  end
419
479
 
420
- def local_decl_symbol(decl)
480
+ def local_decl_symbol(decl, facts: nil)
421
481
  return nil unless decl.name
422
482
  return nil if decl.name == '_'
423
483
 
@@ -429,8 +489,9 @@ module MilkTea
429
489
  if decl.respond_to?(:value) && decl.value.is_a?(AST::ProcExpr)
430
490
  detail ||= proc_signature_detail(decl.value)
431
491
  proc_locals = collect_local_decls(decl.value.body)
432
- children = proc_locals.filter_map { |l| local_decl_symbol(l) }
492
+ children = proc_locals.filter_map { |l| local_decl_symbol(l, facts:) }
433
493
  end
494
+ detail ||= resolved_local_type_detail(decl, facts)
434
495
 
435
496
  {
436
497
  name: decl.name, kind: 13,
@@ -441,6 +502,60 @@ module MilkTea
441
502
  }.compact
442
503
  end
443
504
 
505
+ # Resolves the declared type of an inferred local from semantic facts.
506
+ # Prefers the sema binding type (which reflects let/var ... else: and
507
+ # other flow refinement), falling back to the initializer expression
508
+ # type recorded during checking. Returns nil when facts are unavailable
509
+ # or the binding is missing.
510
+ def resolved_local_type_detail(decl, facts)
511
+ return nil unless facts
512
+ return nil unless facts.respond_to?(:binding_resolution) && facts.binding_resolution
513
+
514
+ binding_id = facts.binding_resolution.declaration_binding_ids[decl.object_id]
515
+ if binding_id
516
+ type = facts.binding_resolution.binding_types[binding_id]
517
+ return nil if type.is_a?(Types::Error)
518
+ return short_type_detail(type) if type
519
+ end
520
+
521
+ return nil unless decl.respond_to?(:value) && decl.value
522
+ return nil unless facts.respond_to?(:resolved_expr_types)
523
+
524
+ node_id = facts.respond_to?(:ast) && facts.ast ? facts.ast.node_ids[decl.value.object_id] : nil
525
+ return nil unless node_id
526
+
527
+ type = facts.resolved_expr_types[node_id]
528
+ return nil if type.is_a?(Types::Error)
529
+
530
+ short_type_detail(type)
531
+ end
532
+
533
+ # Renders a resolved semantic type for outline display without module
534
+ # qualifiers (e.g. std.deque.Deque[int] as Deque[int]). Falls back to
535
+ # the canonical #to_s when a type has no usable short form.
536
+ def short_type_detail(type)
537
+ return nil unless type
538
+
539
+ case type
540
+ when Types::Nullable
541
+ "#{short_type_detail(type.base)}?"
542
+ when Types::Span
543
+ "span[#{short_type_detail(type.element_type)}]"
544
+ when Types::SoA
545
+ "SoA[#{short_type_detail(type.element_type)}, #{type.count}]"
546
+ when Types::StructInstance, Types::VariantInstance, Types::GenericInstance
547
+ args = type.arguments.map { |arg| short_type_arg(arg) }.join(', ')
548
+ args.empty? ? type.name : "#{type.name}[#{args}]"
549
+ else
550
+ name = type.respond_to?(:name) ? type.name.to_s : ''
551
+ name.empty? ? type.to_s : name
552
+ end
553
+ end
554
+
555
+ def short_type_arg(arg)
556
+ arg.is_a?(Types::LiteralTypeArg) ? arg.value.to_s : short_type_detail(arg)
557
+ end
558
+
444
559
  def descendant_names(symbol)
445
560
  return [] unless symbol[:children]
446
561
 
@@ -487,9 +602,7 @@ module MilkTea
487
602
  detail_parts << 'mut' if m.respond_to?(:kind) && m.kind == :editable_function
488
603
  detail_parts << 'static' if m.respond_to?(:kind) && m.kind == :static_function
489
604
  detail_parts << 'async' if m.respond_to?(:async) && m.async
490
- if (ret = type_detail_string(m.return_type))
491
- detail_parts << "-> #{ret}"
492
- end
605
+ detail_parts << "-> #{type_detail_string(m.return_type) || 'void'}"
493
606
  {
494
607
  name: m.name, kind: 6,
495
608
  range: { start: { line: m.line - 1, character: 0 }, end: { line: (m.respond_to?(:end_line) && m.end_line ? m.end_line : m.line), character: 0 } },
@@ -507,10 +620,11 @@ module MilkTea
507
620
  case type
508
621
  when AST::TypeRef
509
622
  type.to_s
510
- when AST::ProcType
623
+ when AST::FunctionType, AST::ProcType
511
624
  params = (type.params || []).map { |p| type_detail_string(p.type) }.join(', ')
512
625
  ret = type_detail_string(type.return_type) || 'void'
513
- "proc(#{params}) -> #{ret}"
626
+ keyword = type.is_a?(AST::FunctionType) ? 'fn' : 'proc'
627
+ "#{keyword}(#{params}) -> #{ret}"
514
628
  when AST::TupleType
515
629
  base = "(#{(type.element_types || []).map { |t| type_detail_string(t) }.join(', ')})"
516
630
  type.nullable ? "#{base}?" : base