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
@@ -186,6 +186,7 @@ module MilkTea
186
186
  mutable: false,
187
187
  pointer: false,
188
188
  const_value: element,
189
+ substitute_const_value: true,
189
190
  )
190
191
  emit_stmts, other_stmts = statement.body.partition { |s| s.is_a?(AST::EmitStmt) }
191
192
  emit_stmts.each do |emit_stmt|
@@ -486,13 +487,11 @@ module MilkTea
486
487
  expected_type: return_type,
487
488
  contextual_int_to_float: contextual_int_to_float_target?(return_type),
488
489
  ) : nil
489
- if prepared_cleanups.any? && cstr_trackable_type?(return_type)
490
- raise LoweringError.new("formatted string temporaries cannot be returned as borrowed text; use std.fmt.format(f\"...\") when ownership must escape",
491
- line: statement.line, column: statement.column, path: @ctx.current_analysis_path)
492
- end
493
-
494
490
  prepared_cleanup_list = prepared_cleanups.flat_map(&:itself)
495
- if prepared_cleanup_list.any? && return_type.is_a?(Types::Struct) && struct_contains_string_field?(return_type)
491
+ if prepared_cleanup_list.any? && (cstr_trackable_type?(return_type) ||
492
+ (return_type.is_a?(Types::Struct) && struct_contains_string_field?(return_type)))
493
+ # A dynamic f-string temp owns its heap buffer; returning it transfers
494
+ # ownership to the caller, so the format release must not run.
496
495
  prepared_cleanup_list = prepared_cleanup_list.reject { |stmt| stmt.is_a?(IR::ExpressionStmt) && stmt.expression.is_a?(IR::Call) && stmt.expression.callee == "mt_format_str_release" }
497
496
  end
498
497
  cleanup = prepared_cleanup_list + cleanup_statements(local_defers, active_defers)
@@ -793,7 +792,7 @@ module MilkTea
793
792
  else_body: nil,
794
793
  )
795
794
  end
796
- local_defers.concat(prepared_cleanups)
795
+ local_defers.concat(reject_format_releases_for_assignment(prepared_cleanups, storage_type))
797
796
  if contains_proc_storage_type?(storage_type)
798
797
  local_value = IR::Name.new(name: linkage_name, type: storage_type, pointer: false)
799
798
  local_defers << lower_proc_nullable_release_statements(local_value, storage_type)
@@ -23,6 +23,7 @@ module MilkTea
23
23
  expanded_declarations.grep(AST::ConstDecl).filter_map do |decl|
24
24
  type = @ctx.values.fetch(decl.name).type
25
25
  const_value = @ctx.values.fetch(decl.name).const_value
26
+ ensure_registered_storage_type_types(type)
26
27
 
27
28
  next if type == Types::BUILTIN_TYPE_META_TYPE
28
29
 
@@ -35,7 +36,7 @@ module MilkTea
35
36
  value = lower_const_value_literal(type, const_value)
36
37
  end
37
38
  elsif decl.block_body || decl.value.is_a?(AST::ExpressionList)
38
- raise LoweringError.new("constant #{decl.name} has no compile-time value", line: decl.line, column: decl.column)
39
+ raise LoweringError.new("constant #{decl.name} has no compile-time value", line: decl.line, column: decl.column, path: @ctx.current_analysis_path)
39
40
  else
40
41
  value = lower_static_storage_initializer(decl.value, env: empty_env, expected_type: type)
41
42
  if (decl.value.is_a?(AST::Call) || decl.value.is_a?(AST::Specialization)) && static_initializer_ir_has_call?(value)
@@ -78,6 +79,43 @@ module MilkTea
78
79
  end
79
80
  end
80
81
 
82
+ ## Register C struct typedefs for every tuple type reachable from a
83
+ ## module-storage type (consts and globals). `ensure_tuple_struct` is
84
+ ## normally called while lowering tuple *expressions*, so a tuple that
85
+ ## only appears in static storage never gets its `mt_tuple_...` typedef
86
+ ## emitted; the C compiler then fails with "unknown type name".
87
+ def ensure_registered_storage_type_types(type, seen = {})
88
+ return if type.nil? || seen[type]
89
+
90
+ seen[type] = true
91
+ if type.is_a?(Types::Tuple)
92
+ ensure_tuple_struct(type)
93
+ type.element_types.each { |element_type| ensure_registered_storage_type_types(element_type, seen) }
94
+ end
95
+
96
+ case type
97
+ when Types::Nullable
98
+ ensure_registered_storage_type_types(type.base, seen)
99
+ when Types::Span
100
+ ensure_registered_storage_type_types(type.element_type, seen)
101
+ when Types::Task
102
+ ensure_registered_storage_type_types(type.result_type, seen)
103
+ when Types::GenericInstance, Types::StructInstance, Types::VariantInstance
104
+ type.arguments.each do |argument|
105
+ ensure_registered_storage_type_types(argument, seen) unless argument.is_a?(Types::LiteralTypeArg)
106
+ end
107
+ end
108
+
109
+ if type.respond_to?(:fields) && type.fields
110
+ type.fields.each_value { |field_type| ensure_registered_storage_type_types(field_type, seen) }
111
+ end
112
+ if type.respond_to?(:arms)
113
+ type.arms.each_value do |arm_fields|
114
+ arm_fields.each_value { |field_type| ensure_registered_storage_type_types(field_type, seen) }
115
+ end
116
+ end
117
+ end
118
+
81
119
  def lower_const_value_literal(type, const_value)
82
120
  case const_value
83
121
  when Integer
@@ -130,6 +168,7 @@ module MilkTea
130
168
  next unless decl.is_a?(AST::VarDecl) || decl.is_a?(AST::EventDecl)
131
169
 
132
170
  type = @ctx.values.fetch(decl.name).type
171
+ ensure_registered_storage_type_types(type)
133
172
  ensure_event_runtime(type) if type.is_a?(Types::Event)
134
173
  value = if decl.is_a?(AST::VarDecl) && decl.value
135
174
  lower_static_storage_initializer(decl.value, env: empty_env, expected_type: type)
@@ -643,6 +643,8 @@ module MilkTea
643
643
  end
644
644
  end
645
645
 
646
+ written_scalar_captures = captures.select { |c| written_names.include?(c.name) && !array_capture_names.include?(c.name) }
647
+
646
648
  @artifacts.synthetic_structs << IR::StructDecl.new(
647
649
  name: cap_struct_c_name, linkage_name: cap_struct_c_name,
648
650
  fields: cap_fields, packed: false, alignment: nil,
@@ -674,6 +676,14 @@ module MilkTea
674
676
 
675
677
  rewritten_body = array_capture_names.empty? ? block_body : rewrite_pfor_array_captures(block_body, array_capture_names)
676
678
  worker_body.concat(rewritten_body)
679
+
680
+ written_scalar_captures.each do |c|
681
+ worker_body << IR::Assignment.new(
682
+ target: IR::Member.new(receiver: cap_name_ir, member: c.name, type: c.type),
683
+ operator: "=",
684
+ value: IR::Name.new(name: c.name, type: c.type, pointer: false),
685
+ )
686
+ end
677
687
  end
678
688
 
679
689
  @artifacts.synthetic_functions << IR::Function.new(
@@ -715,7 +725,7 @@ module MilkTea
715
725
  )
716
726
  end
717
727
 
718
- { worker_c_name:, cap_local_name:, cap_struct_type:, cap_init:, capture_names: Set.new(captures.map(&:name)), written_names:, captureless: }
728
+ { worker_c_name:, cap_local_name:, cap_struct_type:, cap_init:, capture_names: Set.new(captures.map(&:name)), written_names:, written_scalar_captures: captureless ? [] : written_scalar_captures, captureless: }
719
729
  end
720
730
 
721
731
  validate_pfor_write_conflicts!(block_infos)
@@ -768,6 +778,20 @@ module MilkTea
768
778
  type: void_type,
769
779
  ))
770
780
 
781
+ block_infos.each do |info|
782
+ info[:written_scalar_captures].each do |c|
783
+ call_site << IR::Assignment.new(
784
+ target: IR::Name.new(name: c.name, type: c.type, pointer: false),
785
+ operator: "=",
786
+ value: IR::Member.new(
787
+ receiver: IR::Name.new(name: info[:cap_local_name], type: info[:cap_struct_type], pointer: false),
788
+ member: c.name,
789
+ type: c.type,
790
+ ),
791
+ )
792
+ end
793
+ end
794
+
771
795
  IR::BlockStmt.new(body: call_site)
772
796
  end
773
797
 
@@ -745,7 +745,7 @@ module MilkTea
745
745
  end
746
746
  return function_type_for_name(expression.name) if @ctx.functions.key?(expression.name)
747
747
 
748
- raise LoweringError.new("unknown identifier #{expression.name}", line: expression.line, column: expression.column)
748
+ raise LoweringError.new("unknown identifier #{expression.name}", line: expression.line, column: expression.column, path: @ctx.current_analysis_path)
749
749
  when AST::MemberAccess
750
750
  if (type_expr = resolve_type_expression(expression.receiver))
751
751
  member_type = resolve_type_member(type_expr, expression.member)
@@ -784,7 +784,7 @@ module MilkTea
784
784
  end
785
785
 
786
786
  return receiver_type.field(expression.member) if receiver_type.respond_to?(:field)
787
- raise LoweringError.new("unknown member #{expression.member}", line: expression.line, column: expression.column)
787
+ raise LoweringError.new("unknown member #{expression.member}", line: expression.line, column: expression.column, path: @ctx.current_analysis_path)
788
788
  when AST::IndexAccess
789
789
  receiver_type = infer_expression_type(expression.receiver, env:)
790
790
  index_type = infer_expression_type(expression.index, env:)
@@ -1677,7 +1677,15 @@ module MilkTea
1677
1677
  evaluate_attribute_arg_call(expression.arguments, env:)
1678
1678
  else
1679
1679
  callee_name = expression.callee.callee.is_a?(AST::Identifier) ? expression.callee.callee.name : nil
1680
- if callee_name
1680
+ if callee_name == "array"
1681
+ values = []
1682
+ expression.arguments.each do |argument|
1683
+ val = compile_time_const_value(argument.value, env:)
1684
+ return nil unless val
1685
+ values << val
1686
+ end
1687
+ values
1688
+ elsif callee_name
1681
1689
  func = @ctx.functions[callee_name]
1682
1690
  if func&.ast&.respond_to?(:const) && func.ast.const
1683
1691
  evaluate_const_function_body_lower(func, expression.arguments)
@@ -1842,9 +1850,8 @@ module MilkTea
1842
1850
 
1843
1851
  evaluator = ConstFnLowerEvaluator.new(self)
1844
1852
  ctx = CompileTime::BlockContext.new(evaluator, initial_variables: initial_vars)
1845
- ctx.evaluate_block(func.ast.body, scopes: nil)
1846
- rescue CompileTime::ReturnValue => e
1847
- e.value
1853
+ result = ctx.evaluate_block(func.ast.body, scopes: nil)
1854
+ result.is_a?(CompileTime::ReturnOutcome) ? result.value : result
1848
1855
  end
1849
1856
 
1850
1857
  class ConstFnLowerEvaluator
@@ -459,8 +459,8 @@ module MilkTea
459
459
  end
460
460
  end
461
461
 
462
- def local_binding(type:, linkage_name:, mutable:, pointer:, storage_type: nil, projection: nil, cstr_backed: false, cstr_list_backed: false, const_value: nil)
463
- { type:, storage_type: storage_type || type, linkage_name:, mutable:, pointer:, projection:, cstr_backed:, cstr_list_backed:, const_value: }
462
+ def local_binding(type:, linkage_name:, mutable:, pointer:, storage_type: nil, projection: nil, cstr_backed: false, cstr_list_backed: false, const_value: nil, substitute_const_value: false)
463
+ { type:, storage_type: storage_type || type, linkage_name:, mutable:, pointer:, projection:, cstr_backed:, cstr_list_backed:, const_value:, substitute_const_value: }
464
464
  end
465
465
 
466
466
  def callable_type?(type)
@@ -904,6 +904,10 @@ module MilkTea
904
904
  visible_type = binding[:type]
905
905
  projection = binding[:projection]
906
906
 
907
+ if binding[:substitute_const_value] && !binding[:const_value].nil?
908
+ return lower_const_value_literal(visible_type, binding[:const_value])
909
+ end
910
+
907
911
  if projection == :result_success_value
908
912
  local_ref = IR::Name.new(name: binding[:linkage_name], type: storage_type, pointer: binding[:pointer])
909
913
  return variant_binding_projection_expression(local_ref, storage_type, "success", "value", visible_type)
@@ -59,6 +59,18 @@ module MilkTea
59
59
  def code
60
60
  "lowering/internal"
61
61
  end
62
+
63
+ def to_diagnostic(path: nil)
64
+ Diagnostic.new(
65
+ path: @path || path,
66
+ line: @line,
67
+ column: @column,
68
+ length: nil,
69
+ code: code,
70
+ message: message,
71
+ severity: :error,
72
+ )
73
+ end
62
74
  end
63
75
 
64
76
  module Lowering
@@ -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
@@ -297,7 +297,7 @@ module MilkTea
297
297
  expression = nil
298
298
  arms = []
299
299
  expression = parse_expression
300
- arms = parse_match_arms(arms)
300
+ arms = normalize_mixed_match_arms(parse_match_arms(arms))
301
301
  if arms.first&.is_a?(AST::MatchExprArm)
302
302
  expr = AST::MatchExpr.new(expression:, arms:, line:, column: token.column, length: token.lexeme.length)
303
303
  AST::ExpressionStmt.new(expression: expr, line:)
@@ -311,12 +311,13 @@ module MilkTea
311
311
  recovered_arms = synchronize_to_match_arm_boundary
312
312
  target_line = line
313
313
  if recovered_arms
314
+ combined_arms = normalize_mixed_match_arms(arms + recovered_arms)
314
315
  stmt =
315
- if recovered_arms.first&.is_a?(AST::MatchExprArm)
316
- expr = AST::MatchExpr.new(expression: expression || recovery_error_expr(e), arms: arms + recovered_arms, line: target_line, column: token.column, length: token.lexeme.length)
316
+ if combined_arms.first&.is_a?(AST::MatchExprArm)
317
+ expr = AST::MatchExpr.new(expression: expression || recovery_error_expr(e), arms: combined_arms, line: target_line, column: token.column, length: token.lexeme.length)
317
318
  AST::ExpressionStmt.new(expression: expr, line: target_line)
318
319
  else
319
- AST::MatchStmt.new(expression: expression || recovery_error_expr(e), arms: arms + recovered_arms, line: target_line, column: token.column, length: token.lexeme.length)
320
+ AST::MatchStmt.new(expression: expression || recovery_error_expr(e), arms: combined_arms, line: target_line, column: token.column, length: token.lexeme.length)
320
321
  end
321
322
  return stmt
322
323
  end
@@ -335,6 +336,33 @@ module MilkTea
335
336
  arms
336
337
  end
337
338
 
339
+ ## A statement match may mix inline value arms (`pattern: expr`) with
340
+ ## block arms. The inline form is a single expression statement, so when
341
+ ## any arm is a block the inline arms are rewritten to block arms with a
342
+ ## single expression-statement body. This keeps MatchStmt arms
343
+ ## homogeneous (all MatchArm) and MatchExpr arms homogeneous (all
344
+ ## MatchExprArm); the sema / control-flow / lowering consumers rely on
345
+ ## that invariant.
346
+ def normalize_mixed_match_arms(arms)
347
+ return arms unless arms.any? { |arm| arm.is_a?(AST::MatchExprArm) } && arms.any? { |arm| arm.is_a?(AST::MatchArm) }
348
+
349
+ arms.map do |arm|
350
+ if arm.is_a?(AST::MatchExprArm)
351
+ AST::MatchArm.new(
352
+ pattern: arm.pattern,
353
+ binding_name: arm.binding_name,
354
+ binding_line: arm.binding_line,
355
+ binding_column: arm.binding_column,
356
+ body: [AST::ExpressionStmt.new(expression: arm.value, line: arm.line)],
357
+ line: arm.line,
358
+ column: arm.column,
359
+ )
360
+ else
361
+ arm
362
+ end
363
+ end
364
+ end
365
+
338
366
  def parse_match_arm_body(arms = [])
339
367
  skip_newlines
340
368
  until check(:dedent) || eof?
@@ -652,7 +680,7 @@ module MilkTea
652
680
  token = previous
653
681
  line = token.line
654
682
  discriminant = parse_expression
655
- branches = parse_match_arms([])
683
+ branches = normalize_mixed_match_arms(parse_match_arms([]))
656
684
  else_body = if check(:else)
657
685
  if check_next(:newline) || check_next(:indent)
658
686
  parse_else_branch_body
@@ -734,14 +762,14 @@ module MilkTea
734
762
  line = token.line
735
763
  arms = []
736
764
  expression = parse_expression
737
- arms = parse_match_arms(arms)
765
+ arms = normalize_mixed_match_arms(parse_match_arms(arms))
738
766
  AST::MatchStmt.new(expression:, arms:, inline: true, line:, column: token.column, length: token.lexeme.length)
739
767
  rescue ParseError => e
740
768
  raise unless @recovery_errors
741
769
 
742
770
  @recovery_errors << e
743
771
  recovered_arms = synchronize_to_match_arm_boundary
744
- return AST::MatchStmt.new(expression: expression || recovery_error_expr(e), arms: arms + recovered_arms, inline: true, line:, column: token.column, length: token.lexeme.length) if recovered_arms
772
+ return AST::MatchStmt.new(expression: expression || recovery_error_expr(e), arms: normalize_mixed_match_arms(arms + recovered_arms), inline: true, line:, column: token.column, length: token.lexeme.length) if recovered_arms
745
773
 
746
774
  recovery_error_stmt(e)
747
775
  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
@@ -211,7 +211,8 @@ module MilkTea
211
211
  when IR::NullableSpanIndex
212
212
  "nullable_span_index<#{expression.receiver_type}>(#{render_expression(expression.receiver)}, #{render_expression(expression.index)})"
213
213
  when IR::Call
214
- wrap("#{expression.callee}(#{expression.arguments.map { |argument| render_expression(argument) }.join(', ')})", parent_precedence, POSTFIX_PRECEDENCE)
214
+ callee_text = expression.callee.is_a?(String) ? expression.callee : render_expression(expression.callee)
215
+ wrap("#{callee_text}(#{expression.arguments.map { |argument| render_expression(argument) }.join(', ')})", parent_precedence, POSTFIX_PRECEDENCE)
215
216
  when IR::Unary
216
217
  operand = render_expression(expression.operand, UNARY_PRECEDENCE)
217
218
  text = expression.operator == "not" ? "not #{operand}" : "#{expression.operator}#{operand}"
@@ -258,6 +259,8 @@ module MilkTea
258
259
  end
259
260
  when IR::ArrayLiteral
260
261
  "#{expression.type}(#{expression.elements.map { |element| render_expression(element) }.join(', ')})"
262
+ when IR::SimdLaneWith
263
+ "#{render_expression(expression.src)}.with(#{render_expression(expression.index)}, #{render_expression(expression.value)})"
261
264
  when IR::Assignment
262
265
  "#{render_expression(expression.target)} #{expression.operator} #{render_expression(expression.value)}"
263
266
  else
@@ -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