mt-lang 0.3.17 → 0.3.22

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/milk_tea/base.rb +1 -1
  3. data/lib/milk_tea/core/c_backend/expressions.rb +34 -23
  4. data/lib/milk_tea/core/c_backend/feature_detection.rb +25 -45
  5. data/lib/milk_tea/core/c_backend/type_collectors.rb +18 -30
  6. data/lib/milk_tea/core/c_backend/type_declaration.rb +0 -6
  7. data/lib/milk_tea/core/c_backend.rb +49 -39
  8. data/lib/milk_tea/core/compile_time.rb +98 -72
  9. data/lib/milk_tea/core/control_flow/builder.rb +2 -1
  10. data/lib/milk_tea/core/intrinsics.rb +7 -0
  11. data/lib/milk_tea/core/lexer.rb +46 -35
  12. data/lib/milk_tea/core/lowering/block.rb +9 -0
  13. data/lib/milk_tea/core/lowering/calls.rb +2 -0
  14. data/lib/milk_tea/core/lowering/declarations.rb +1 -1
  15. data/lib/milk_tea/core/lowering/functions.rb +11 -8
  16. data/lib/milk_tea/core/lowering/resolve.rb +11 -4
  17. data/lib/milk_tea/core/lowering/scans.rb +13 -18
  18. data/lib/milk_tea/core/module_binder.rb +19 -15
  19. data/lib/milk_tea/core/module_loader.rb +212 -59
  20. data/lib/milk_tea/core/module_path_resolver.rb +8 -7
  21. data/lib/milk_tea/core/parser/declarations.rb +43 -19
  22. data/lib/milk_tea/core/parser/expressions.rb +5 -8
  23. data/lib/milk_tea/core/parser/statements.rb +5 -5
  24. data/lib/milk_tea/core/parser.rb +26 -0
  25. data/lib/milk_tea/core/semantic_analyzer/calls.rb +24 -31
  26. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +20 -23
  27. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +146 -92
  28. data/lib/milk_tea/core/semantic_analyzer/statements.rb +19 -41
  29. data/lib/milk_tea/core/semantic_analyzer.rb +56 -37
  30. data/lib/milk_tea/core/types.rb +12 -17
  31. data/lib/milk_tea/lsp/diagnostics.rb +13 -0
  32. data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
  33. data/lib/milk_tea/tooling/cli/commands/bindgen.rb +11 -0
  34. data/lib/milk_tea/tooling/cli/commands/build.rb +37 -0
  35. data/lib/milk_tea/tooling/cli/commands/cache.rb +46 -0
  36. data/lib/milk_tea/tooling/cli/commands/check.rb +106 -0
  37. data/lib/milk_tea/tooling/cli/commands/command_base.rb +8 -0
  38. data/lib/milk_tea/tooling/cli/commands/completions.rb +48 -0
  39. data/lib/milk_tea/tooling/cli/commands/dap.rb +58 -0
  40. data/lib/milk_tea/tooling/cli/commands/debug.rb +77 -0
  41. data/lib/milk_tea/tooling/cli/commands/deps.rb +17 -0
  42. data/lib/milk_tea/tooling/cli/commands/docs.rb +58 -0
  43. data/lib/milk_tea/tooling/cli/commands/emit_c.rb +64 -0
  44. data/lib/milk_tea/tooling/cli/commands/format.rb +199 -0
  45. data/lib/milk_tea/tooling/cli/commands/lex.rb +46 -0
  46. data/lib/milk_tea/tooling/cli/commands/lint.rb +248 -0
  47. data/lib/milk_tea/tooling/cli/commands/lower.rb +50 -0
  48. data/lib/milk_tea/tooling/cli/commands/lsp.rb +43 -0
  49. data/lib/milk_tea/tooling/cli/commands/new.rb +26 -0
  50. data/lib/milk_tea/tooling/cli/commands/parse.rb +51 -0
  51. data/lib/milk_tea/tooling/cli/commands/run.rb +99 -0
  52. data/lib/milk_tea/tooling/cli/commands/snapshot.rb +117 -0
  53. data/lib/milk_tea/tooling/cli/commands/test.rb +557 -0
  54. data/lib/milk_tea/tooling/cli/commands/toolchain.rb +16 -0
  55. data/lib/milk_tea/tooling/cli.rb +101 -1893
  56. data/lib/milk_tea/tooling/sexpr_dumper.rb +7 -7
  57. metadata +24 -2
@@ -154,15 +154,37 @@ module MilkTea
154
154
  check_program(path).root_analysis
155
155
  end
156
156
 
157
- def check_program(path)
157
+ def with_check_context(path, &block)
158
158
  Types::Registry.reset!
159
159
  requested_path = File.expand_path(path)
160
160
  previous_platform = @platform
161
161
  @platform ||= self.class.platform_suffix_for_path(requested_path)
162
162
  root_path = self.class.resolve_source_path(requested_path, platform: @platform, error_class: ModuleLoadError)
163
+ block.call(root_path)
164
+ ensure
165
+ @platform = previous_platform
166
+ end
167
+
168
+ def check_program(path)
169
+ with_check_context(path) do |root_path|
170
+ check_program_parallel(root_path)
171
+ build_program(root_path)
172
+ end
173
+ end
163
174
 
164
- check_program_parallel(root_path)
175
+ # Variant of #check_program that collects errors instead of raising so
176
+ # diagnostic paths (check, debug, LSP) can report all issues at once.
177
+ # Returns { root_analysis: Analysis|nil, errors: [SemanticError], module_name: String|nil }.
178
+ def check_program_collecting(path)
179
+ with_check_context(path) do |root_path|
180
+ errors = []
181
+ check_program_parallel(root_path, collecting_errors: errors)
182
+ root_analysis = @analysis_cache[root_path]
183
+ { root_analysis: root_analysis, errors: errors, module_name: root_analysis&.module_name }
184
+ end
185
+ end
165
186
 
187
+ def build_program(root_path)
166
188
  root_analysis = @analysis_cache.fetch(root_path)
167
189
  analyses_by_module_name = @analysis_cache.each_value.each_with_object({}) do |analysis, modules|
168
190
  next unless analysis.module_name
@@ -176,11 +198,27 @@ module MilkTea
176
198
  analyses_by_path: @analysis_cache.dup.freeze,
177
199
  analyses_by_module_name: analyses_by_module_name.freeze,
178
200
  )
179
- ensure
180
- @platform = previous_platform
181
201
  end
182
202
 
183
- def check_program_parallel(root_path)
203
+ def node_in_cycle?(node, graph)
204
+ # Start from each immediate successor to avoid the trivial self-path
205
+ successors = graph[node] || []
206
+ successors.each do |next_node|
207
+ next unless graph.key?(next_node)
208
+ return true if dfs_reachable_from(next_node, node, graph, {})
209
+ end
210
+ false
211
+ end
212
+
213
+ def dfs_reachable_from(current, target, graph, visited)
214
+ return false if visited[current]
215
+ return true if current == target
216
+
217
+ visited[current] = true
218
+ (graph[current] || []).any? { |neighbor| dfs_reachable_from(neighbor, target, graph, visited) }
219
+ end
220
+
221
+ def check_program_parallel(root_path, collecting_errors: nil)
184
222
  # Phase 1: Parse all transitive modules (sequential)
185
223
  parse_all(root_path)
186
224
 
@@ -194,12 +232,24 @@ module MilkTea
194
232
  graph[resolved_path] = deps
195
233
  end
196
234
 
197
- # Phase 3: Topological sort into independent levels
235
+ # Phase 3: Topological sort into independent levels.
236
+ # Nodes that cannot be sorted are candidates for cycle membership,
237
+ # but not all of them are actually part of a cycle — some are just
238
+ # dependencies downstream from the cycle. We separate true cycle
239
+ # members (nodes reachable from themselves) from tail nodes.
198
240
  levels = topo_sort_levels(graph)
199
241
 
200
- # Pre-register forward bindings for cycle members
201
242
  all_checked = levels.flatten.to_set
202
- cycle_members = graph.keys.reject { |p| all_checked.include?(p) }
243
+ unsorted = graph.keys.reject { |p| all_checked.include?(p) }
244
+
245
+ # Among unsorted nodes, identify true cycle members: nodes that can
246
+ # reach themselves through the graph (belong to a strongly connected
247
+ # component of size > 1). Tail nodes that only depend on cycle members
248
+ # but have no path back to themselves are NOT cycle members.
249
+ cycle_members = unsorted.select { |node| node_in_cycle?(node, graph) }
250
+ tail_members = unsorted - cycle_members
251
+
252
+ # Pre-register forward bindings only for true cycle members
203
253
  cycle_members.each do |resolved_path|
204
254
  ast = @parse_cache[resolved_path]
205
255
  @forward_bindings[resolved_path] = create_forward_binding(ast)
@@ -209,10 +259,14 @@ module MilkTea
209
259
  # Pass 1: Check with forward bindings — registers type declarations.
210
260
  # SemanticError is expected (forward types lack fields/constructors)
211
261
  # but ModuleLoadError indicates a broken dependency graph.
262
+ # Do NOT collect Pass 1 errors — they are temporary failures caused
263
+ # by incomplete forward bindings that Pass 2 resolves.
212
264
  cycle_members.each do |resolved_path|
213
265
  check_path(resolved_path)
214
266
  rescue SemanticError
215
- # Forward types can't satisfy constructors. Pass 2 will retry.
267
+ # Forward types can't satisfy constructors / functions / methods.
268
+ # Re-check in collecting mode to capture the analysis for population.
269
+ capture_analysis_for_cycle_member(resolved_path)
216
270
  end
217
271
 
218
272
  # Update forward type objects in-place with field/arm info from the
@@ -236,26 +290,45 @@ module MilkTea
236
290
  if real_type.respond_to?(:arms) && fw_type.respond_to?(:define_arms)
237
291
  fw_type.define_arms(real_type.arms) unless real_type.arms.empty?
238
292
  end
293
+ if real_type.respond_to?(:members) && fw_type.respond_to?(:define_members)
294
+ member_names = real_type.members
295
+ unless member_names.empty?
296
+ fw_type.define_members(real_type.backing_type, member_names)
297
+ values = member_names.each_with_object({}) { |n, h| h[n] = real_type.member_value(n) }
298
+ fw_type.define_member_values(values)
299
+ end
300
+ end
239
301
  end
240
302
  end
241
303
 
242
- # Pass 2: Re-check with populated types now cycle members see
243
- # complete type information for each other.
304
+ # Replace forward bindings with fully populated bindings so that
305
+ # Pass 2 import resolution sees functions, methods, values, and
306
+ # interfaces — not just type skeletons.
307
+ populate_full_forward_bindings(cycle_members)
308
+
309
+ # Pass 2: Re-check with populated bindings — now cycle members see
310
+ # complete module information for each other.
244
311
  cycle_members.each do |resolved_path|
245
312
  previous = @analysis_cache.delete(resolved_path)
246
313
  check_path(resolved_path)
247
- rescue SemanticError
314
+ rescue SemanticError => e
248
315
  @analysis_cache[resolved_path] = previous if previous
316
+ collecting_errors << e if collecting_errors
249
317
  end
250
318
  @forward_bindings.clear
251
319
 
252
- # Phase 5: Check acyclic modules (they see fully-checked analyses for all imports)
253
- levels.each do |level_paths|
320
+ # Phase 5: Check acyclic modules (they see fully-checked analyses for all imports).
321
+ # Also check tail members — nodes that were in the unsorted set but
322
+ # are not themselves part of a cycle; they just depended on cycle members.
323
+ (levels + [tail_members]).each do |level_paths|
254
324
  if level_paths.length == 1
255
325
  check_path(level_paths.first)
256
- else
326
+ elsif level_paths.any?
257
327
  check_level_parallel(level_paths)
258
328
  end
329
+ rescue SemanticError => e
330
+ raise unless collecting_errors
331
+ collecting_errors << e
259
332
  end
260
333
  end
261
334
 
@@ -317,22 +390,62 @@ module MilkTea
317
390
  end
318
391
 
319
392
  def imported_modules_for_ast(ast, importer_path: nil)
393
+ resolve_imports_for_ast(ast, importer_path:, collecting: false)
394
+ end
395
+
396
+ def imported_modules_for_ast_collecting_errors(ast, importer_path: nil)
397
+ resolve_imports_for_ast(ast, importer_path:, collecting: true)
398
+ end
399
+
400
+ def resolve_imports_for_ast(ast, importer_path:, collecting: false)
320
401
  modules = {}
402
+ errors = []
403
+
404
+ if @import_resolve_depth
405
+ @import_resolve_depth += 1
406
+ if @import_resolve_depth > 80
407
+ raise SemanticError.new("import resolution depth exceeded")
408
+ end
409
+ else
410
+ @import_resolve_depth = 1
411
+ end
321
412
 
322
413
  ast.imports.each do |import|
323
- import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path:, importer_module_name: ast.module_name.to_s)
414
+ begin
415
+ import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path:, importer_module_name: ast.module_name.to_s)
324
416
 
325
- if @forward_bindings.key?(import_path)
326
- modules[import.path.to_s] = @forward_bindings[import_path]
327
- else
328
- import_analysis = check_path(import_path)
329
- modules[import.path.to_s] = @binder.module_binding(import_analysis)
417
+ if @forward_bindings.key?(import_path)
418
+ modules[import.path.to_s] = @forward_bindings[import_path]
419
+ else
420
+ import_analysis = collecting ? check_path_collecting_errors(import_path) : check_path(import_path)
421
+ modules[import.path.to_s] = @binder.module_binding(import_analysis)
422
+ end
423
+ rescue ModuleLoadError, PackageLockError, SemanticError => e
424
+ raise unless collecting
425
+
426
+ handle_circular_import_in_collecting_mode(import, import_path, modules, errors, e)
330
427
  end
331
428
  end
332
429
 
333
- @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: false)
334
- @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: false)
430
+ begin
431
+ @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: collecting)
432
+ rescue ModuleLoadError, PackageLockError => e
433
+ raise unless collecting
434
+ errors << ImportResolutionError.new(import: nil, error: e)
435
+ end
436
+
437
+ begin
438
+ @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: collecting)
439
+ rescue ModuleLoadError, PackageLockError => e
440
+ raise unless collecting
441
+ errors << ImportResolutionError.new(import: nil, error: e)
442
+ end
443
+
444
+ return ImportResolution.new(modules: modules.freeze, errors: errors.freeze) if collecting
445
+
335
446
  modules.freeze
447
+ ensure
448
+ @import_resolve_depth -= 1 if @import_resolve_depth
336
449
  end
337
450
 
338
451
  def build_global_import_index(ast)
@@ -358,40 +471,6 @@ module MilkTea
358
471
  index
359
472
  end
360
473
 
361
- def imported_modules_for_ast_collecting_errors(ast, importer_path: nil)
362
- modules = {}
363
- errors = []
364
-
365
- ast.imports.each do |import|
366
- begin
367
- import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path:, importer_module_name: ast.module_name.to_s)
368
-
369
- if @forward_bindings.key?(import_path)
370
- modules[import.path.to_s] = @forward_bindings[import_path]
371
- else
372
- import_analysis = check_path_collecting_errors(import_path)
373
- modules[import.path.to_s] = @binder.module_binding(import_analysis)
374
- end
375
- rescue ModuleLoadError, PackageLockError, SemanticError => e
376
- errors << ImportResolutionError.new(import:, error: e)
377
- end
378
- end
379
-
380
- begin
381
- @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: true)
382
- rescue ModuleLoadError, PackageLockError => e
383
- errors << ImportResolutionError.new(import: nil, error: e)
384
- end
385
-
386
- begin
387
- @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: true)
388
- rescue ModuleLoadError, PackageLockError => e
389
- errors << ImportResolutionError.new(import: nil, error: e)
390
- end
391
-
392
- ImportResolution.new(modules: modules.freeze, errors: errors.freeze)
393
- end
394
-
395
474
  # Errors collected per analyzed import path during collecting-mode checks.
396
475
  # Populated by #check_path_collecting_errors; used by the CLI `check` command
397
476
  # to surface errors in a single file's imported modules (otherwise only
@@ -527,6 +606,74 @@ module MilkTea
527
606
  normalized_path == normalized_root || normalized_path.start_with?(normalized_root + File::SEPARATOR)
528
607
  end
529
608
 
609
+ def handle_circular_import_in_collecting_mode(import, import_path, modules, errors, error)
610
+ # When a circular import is detected in collecting mode, create a
611
+ # forward binding for the target module so the importing module can
612
+ # at minimum resolve type references. Without this the import is
613
+ # entirely absent, which causes downstream crashes in module_binding.
614
+ if import_path && error.is_a?(ModuleLoadError) && error.message.start_with?("circular import")
615
+ circular_ast = @parse_cache[import_path] || load_file(import_path)
616
+ if circular_ast
617
+ @forward_bindings[import_path] ||= create_forward_binding(circular_ast)
618
+ modules[import.path.to_s] = @forward_bindings[import_path]
619
+ end
620
+ end
621
+
622
+ errors << ImportResolutionError.new(import:, error:)
623
+ end
624
+
625
+ def capture_analysis_for_cycle_member(resolved_path)
626
+ return if @analysis_cache[resolved_path]
627
+
628
+ ast = @parse_cache[resolved_path]
629
+ return unless ast
630
+
631
+ @checking_paths << resolved_path
632
+ import_result = resolve_imports_for_ast(ast, importer_path: resolved_path, collecting: true)
633
+ result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules: import_result.modules, path: resolved_path)
634
+ @analysis_cache[resolved_path] = result[:analysis] if result[:analysis]
635
+ rescue StandardError
636
+ # Analysis capture is best-effort.
637
+ ensure
638
+ @checking_paths.pop
639
+ end
640
+
641
+ def populate_full_forward_bindings(cycle_members)
642
+ cycle_members.each do |resolved_path|
643
+ analysis = @analysis_cache[resolved_path]
644
+ next unless analysis
645
+
646
+ fw_binding = @forward_bindings[resolved_path]
647
+ next unless fw_binding
648
+
649
+ full_binding = @binder.module_binding(analysis)
650
+ merged_types = fw_binding.types.merge(
651
+ full_binding.types.reject { |name, _| fw_binding.types.key?(name) }
652
+ )
653
+
654
+ @forward_bindings[resolved_path] = ModuleBinding.new(
655
+ name: full_binding.name,
656
+ types: merged_types,
657
+ type_declarations: full_binding.type_declarations,
658
+ interfaces: full_binding.interfaces,
659
+ attributes: full_binding.attributes,
660
+ attribute_applications: full_binding.attribute_applications,
661
+ values: full_binding.values,
662
+ functions: full_binding.functions,
663
+ methods: full_binding.methods,
664
+ implemented_interfaces: full_binding.implemented_interfaces,
665
+ imports: full_binding.imports,
666
+ private_types: full_binding.private_types,
667
+ private_interfaces: full_binding.private_interfaces,
668
+ private_attributes: full_binding.private_attributes,
669
+ private_values: full_binding.private_values,
670
+ private_functions: full_binding.private_functions,
671
+ private_methods: full_binding.private_methods,
672
+ private_implemented_interfaces: full_binding.private_implemented_interfaces,
673
+ )
674
+ end
675
+ end
676
+
530
677
  def create_forward_binding(ast)
531
678
  module_name = ast.module_name.to_s
532
679
  types = {}
@@ -534,15 +681,21 @@ module MilkTea
534
681
  ast.declarations.each do |decl|
535
682
  case decl
536
683
  when AST::StructDecl
537
- types[decl.name] = Types::Struct.new(decl.name, module_name:)
684
+ types[decl.name] = decl.type_params.empty? ?
685
+ Types::Struct.new(decl.name, module_name:) :
686
+ Types::GenericStructDefinition.new(decl.name, decl.type_params.map(&:name))
538
687
  when AST::VariantDecl
539
- types[decl.name] = Types::Variant.new(decl.name, module_name:)
688
+ types[decl.name] = decl.type_params.empty? ?
689
+ Types::Variant.new(decl.name, module_name:) :
690
+ Types::GenericVariantDefinition.new(decl.name, decl.type_params.map(&:name))
540
691
  when AST::EnumDecl
541
692
  types[decl.name] = Types::Enum.new(decl.name, module_name:)
542
693
  when AST::FlagsDecl
543
694
  types[decl.name] = Types::Flags.new(decl.name, module_name:)
544
695
  when AST::OpaqueDecl
545
696
  types[decl.name] = Types::Opaque.new(decl.name, module_name:, external: false)
697
+ when AST::UnionDecl
698
+ types[decl.name] = Types::Union.new(decl.name, module_name:)
546
699
  end
547
700
  end
548
701
 
@@ -16,19 +16,23 @@ module MilkTea
16
16
 
17
17
  relative_path = File.join(*module_name.split(".")) + ".mt"
18
18
  blocked = false
19
- candidate = @module_roots.lazy.map { |root| ModuleLoader.resolve_source_path(File.join(root, relative_path), platform: @platform) }.find do |path|
20
- next false unless source_path_available?(path)
19
+ candidate = nil
20
+ @module_roots.each do |root|
21
+ path = ModuleLoader.resolve_source_path(File.join(root, relative_path), platform: @platform)
22
+ next unless source_path_available?(path)
21
23
 
22
24
  allowed = import_allowed?(module_name, importer_path, path)
23
25
  blocked ||= !allowed
24
- allowed
26
+ if allowed
27
+ candidate = path
28
+ break
29
+ end
25
30
  end
26
31
  raise ModuleLoadError.new("package dependency not declared", path: module_name) if blocked
27
32
  unless candidate
28
33
  message = namespace_hint_for_missing_module(module_name, importer_path:, importer_module_name:) || "module not found"
29
34
  raise ModuleLoadError.new(message, path: module_name)
30
35
  end
31
-
32
36
  File.expand_path(candidate)
33
37
  end
34
38
 
@@ -119,9 +123,6 @@ module MilkTea
119
123
  return @package_manifest_cache[manifest_path] if @package_manifest_cache.key?(manifest_path)
120
124
 
121
125
  @package_manifest_cache[manifest_path] = PackageManifest.load(path)
122
- rescue PackageManifestError
123
- @package_manifest_cache[manifest_path] = nil if manifest_path
124
- nil
125
126
  end
126
127
 
127
128
  def package_namespace_match?(module_name, package_name)
@@ -18,6 +18,39 @@ module MilkTea
18
18
  AST::Import.new(path:, alias_name:, line:, column: local_column, length: local_name.length)
19
19
  end
20
20
 
21
+ DECL_KIND = {
22
+ var: { method: :parse_var_decl, reject_attrs: "var" },
23
+ type: { method: :parse_type_alias_decl, reject_attrs: "type" },
24
+ union: { method: :parse_union_decl },
25
+ enum: { method: :parse_enum_decl, enum_class: AST::EnumDecl },
26
+ flags: { method: :parse_enum_decl, enum_class: AST::FlagsDecl },
27
+ opaque: { method: :parse_opaque_decl, reject_attrs: "opaque" },
28
+ interface: { method: :parse_interface_decl, reject_attrs: "interface" },
29
+ event: { method: :parse_event_decl },
30
+ attribute: { method: :parse_attribute_decl, reject_attrs: "attribute" },
31
+ }.freeze
32
+
33
+ def dispatch_decl_kind(kind, visibility:, attributes:)
34
+ info = DECL_KIND.fetch(kind)
35
+ reject_attributes!(attributes, info[:reject_attrs]) if info[:reject_attrs]
36
+
37
+ if info[:enum_class]
38
+ send(info[:method], info[:enum_class], visibility:, attributes:)
39
+ elsif info[:reject_attrs]
40
+ send(info[:method], visibility:)
41
+ else
42
+ send(info[:method], visibility:, attributes:)
43
+ end
44
+ end
45
+
46
+ def parse_const_or_function_decl(visibility:, attributes:)
47
+ if match(:function)
48
+ parse_function_def(visibility:, const: true, attributes:)
49
+ else
50
+ parse_const_decl(visibility:, attributes:)
51
+ end
52
+ end
53
+
21
54
  def parse_declaration
22
55
  attributes = parse_attribute_applications
23
56
  visibility, visibility_token = parse_visibility
@@ -25,38 +58,29 @@ module MilkTea
25
58
  if builtin_attribute_identifier?(peek)
26
59
  raise error(peek, "layout modifiers must use attributes like @[packed] or @[align(...)]")
27
60
  elsif match(:attribute)
28
- reject_attributes!(attributes, "attribute")
29
- parse_attribute_decl(visibility:)
61
+ dispatch_decl_kind(:attribute, visibility:, attributes:)
30
62
  elsif match(:const)
31
- if match(:function)
32
- parse_function_def(visibility:, const: true, attributes:)
33
- else
34
- parse_const_decl(visibility:, attributes:)
35
- end
63
+ parse_const_or_function_decl(visibility:, attributes:)
36
64
  elsif match(:var)
37
- reject_attributes!(attributes, "var")
38
- parse_var_decl(visibility:)
65
+ dispatch_decl_kind(:var, visibility:, attributes:)
39
66
  elsif match(:event)
40
- parse_event_decl(visibility:, attributes:)
67
+ dispatch_decl_kind(:event, visibility:, attributes:)
41
68
  elsif match(:type)
42
- reject_attributes!(attributes, "type")
43
- parse_type_alias_decl(visibility:)
69
+ dispatch_decl_kind(:type, visibility:, attributes:)
44
70
  elsif match(:struct)
45
71
  parse_struct_decl(visibility:, attributes:)
46
72
  elsif match(:union)
47
- parse_union_decl(visibility:, attributes:)
73
+ dispatch_decl_kind(:union, visibility:, attributes:)
48
74
  elsif match(:enum)
49
- parse_enum_decl(AST::EnumDecl, visibility:, attributes:)
75
+ dispatch_decl_kind(:enum, visibility:, attributes:)
50
76
  elsif match(:flags)
51
- parse_enum_decl(AST::FlagsDecl, visibility:, attributes:)
77
+ dispatch_decl_kind(:flags, visibility:, attributes:)
52
78
  elsif match(:variant)
53
79
  parse_variant_decl(visibility:, attributes:)
54
80
  elsif match(:interface)
55
- reject_attributes!(attributes, "interface")
56
- parse_interface_decl(visibility:)
81
+ dispatch_decl_kind(:interface, visibility:, attributes:)
57
82
  elsif match(:opaque)
58
- reject_attributes!(attributes, "opaque")
59
- parse_opaque_decl(visibility:)
83
+ dispatch_decl_kind(:opaque, visibility:, attributes:)
60
84
  elsif match(:extending)
61
85
  reject_attributes!(attributes, "extending")
62
86
  raise error(visibility_token, "public is not allowed on extending blocks") if visibility == :public
@@ -35,14 +35,14 @@ module MilkTea
35
35
  consume(:newline, "expected newline after 'if condition:' in if expression")
36
36
  consume(:indent, "expected indented body in if expression")
37
37
  then_expression = parse_expression
38
- consume_end_of_statement unless block_expression?(then_expression)
38
+ finish_expression_statement(then_expression)
39
39
  consume(:dedent, "expected end of if expression body")
40
40
  consume(:else, "expected 'else' in if expression")
41
41
  consume(:colon, "expected ':' after 'else' in if expression")
42
42
  consume(:newline, "expected newline after 'else:' in if expression")
43
43
  consume(:indent, "expected indented else body in if expression")
44
44
  else_expression = parse_expression
45
- consume_end_of_statement unless block_expression?(else_expression)
45
+ finish_expression_statement(else_expression)
46
46
  consume(:dedent, "expected end of else expression body")
47
47
  end
48
48
 
@@ -97,7 +97,7 @@ module MilkTea
97
97
  end
98
98
  consume(:colon, "expected ':' after match expression arm pattern")
99
99
  value = parse_expression
100
- consume_end_of_statement unless block_expression?(value)
100
+ finish_expression_statement(value)
101
101
  patterns.map do |pattern|
102
102
  AST::MatchExprArm.new(
103
103
  pattern:,
@@ -549,10 +549,7 @@ module MilkTea
549
549
  end
550
550
 
551
551
  parser = self.class.new(tokens, path: @path)
552
- parser.instance_variable_set(:@known_type_names, @known_type_names.dup)
553
- parser.instance_variable_set(:@known_import_aliases, @known_import_aliases.dup)
554
- parser.instance_variable_set(:@known_generic_callable_names, @known_generic_callable_names.dup)
555
- parser.instance_variable_set(:@current_type_param_names, @current_type_param_names.dup)
552
+ parser.apply_parser_context(parser_context)
556
553
 
557
554
  expression = parser.parse_expression
558
555
  parser.skip_newlines
@@ -643,7 +640,7 @@ module MilkTea
643
640
 
644
641
  def definite_type_argument?(value)
645
642
  case value
646
- when AST::FunctionType
643
+ when AST::FunctionType, AST::TupleType
647
644
  true
648
645
  when AST::TypeRef
649
646
  known_type_like_name?(value.name.parts.first)
@@ -147,7 +147,7 @@ module MilkTea
147
147
 
148
148
  else_body = parse_block
149
149
  else
150
- consume_end_of_statement unless block_expression?(value)
150
+ finish_expression_statement(value)
151
151
  end
152
152
  else
153
153
  raise error(name_token, "local declaration without initializer requires a type") unless var_type
@@ -366,7 +366,7 @@ module MilkTea
366
366
  if match_arm_expr_form?
367
367
  consume(:colon, "expected ':' after match expression arm pattern")
368
368
  value = parse_expression
369
- consume_end_of_statement unless block_expression?(value)
369
+ finish_expression_statement(value)
370
370
  patterns.map do |pattern|
371
371
  AST::MatchExprArm.new(
372
372
  pattern:,
@@ -585,7 +585,7 @@ module MilkTea
585
585
  token = previous
586
586
  line = token.line
587
587
  value = check(:newline) ? nil : parse_expression
588
- consume_end_of_statement unless block_expression?(value)
588
+ finish_expression_statement(value)
589
589
  AST::ReturnStmt.new(value:, line:, column: token.column, length: token.lexeme.length)
590
590
  end
591
591
 
@@ -768,10 +768,10 @@ module MilkTea
768
768
  operator = previous.lexeme
769
769
  column = previous.column
770
770
  value = parse_expression
771
- consume_end_of_statement unless block_expression?(value)
771
+ finish_expression_statement(value)
772
772
  AST::Assignment.new(target: expression, operator:, value:, line:, column:)
773
773
  else
774
- consume_end_of_statement unless block_expression?(expression)
774
+ finish_expression_statement(expression)
775
775
  AST::ExpressionStmt.new(expression:, line:)
776
776
  end
777
777
  end
@@ -38,6 +38,8 @@ module MilkTea
38
38
  def initialize(ast:, errors: []) = super
39
39
  end
40
40
 
41
+ ParseContext = Data.define(:known_type_names, :known_import_aliases, :known_generic_callable_names, :current_type_param_names)
42
+
41
43
  include Parse::Blocks
42
44
  include Parse::Recovery
43
45
  include Parse::Types
@@ -250,6 +252,10 @@ module MilkTea
250
252
  consume(:newline, "expected end of statement")
251
253
  end
252
254
 
255
+ def finish_expression_statement(value)
256
+ consume_end_of_statement unless block_expression?(value)
257
+ end
258
+
253
259
  def block_expression?(expression)
254
260
  expression.is_a?(AST::ProcExpr) || expression.is_a?(AST::MatchExpr) || expression.is_a?(AST::IfExpr)
255
261
  end
@@ -304,6 +310,26 @@ module MilkTea
304
310
  @current_type_param_names = saved_names
305
311
  end
306
312
 
313
+ # Returns a ParseContext snapshot of the parser state that must propagate
314
+ # to nested parsers (e.g. format string interpolations). When adding
315
+ # parser state that must be cloned, update both this method,
316
+ # apply_parser_context, and the ParseContext Data class.
317
+ def parser_context
318
+ ParseContext.new(
319
+ known_type_names: @known_type_names.dup,
320
+ known_import_aliases: @known_import_aliases.dup,
321
+ known_generic_callable_names: @known_generic_callable_names.dup,
322
+ current_type_param_names: @current_type_param_names.dup,
323
+ )
324
+ end
325
+
326
+ def apply_parser_context(context)
327
+ @known_type_names = context.known_type_names
328
+ @known_import_aliases = context.known_import_aliases
329
+ @known_generic_callable_names = context.known_generic_callable_names
330
+ @current_type_param_names = context.current_type_param_names
331
+ end
332
+
307
333
  def seed_known_names
308
334
  MilkTea::BUILTIN_TYPE_NAMES.each { |name| @known_type_names[name] = true }
309
335