mt-lang 0.3.16 → 0.3.20

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/docs/language-design.md +3 -4
  3. data/docs/language-manual.md +3 -3
  4. data/lib/milk_tea/base.rb +1 -1
  5. data/lib/milk_tea/core/{binding_types.rb → bindings.rb} +1 -1
  6. data/lib/milk_tea/core/c_backend/expressions.rb +34 -23
  7. data/lib/milk_tea/core/c_backend/feature_detection.rb +25 -45
  8. data/lib/milk_tea/core/c_backend/type_collectors.rb +18 -30
  9. data/lib/milk_tea/core/c_backend/type_declaration.rb +0 -6
  10. data/lib/milk_tea/core/c_backend.rb +63 -54
  11. data/lib/milk_tea/core/compile_time.rb +279 -74
  12. data/lib/milk_tea/core/control_flow/constant_propagation.rb +1 -1
  13. data/lib/milk_tea/core/{compatibility_helpers.rb → intrinsics.rb} +9 -2
  14. data/lib/milk_tea/core/lexer.rb +55 -48
  15. data/lib/milk_tea/core/lowering/block.rb +9 -0
  16. data/lib/milk_tea/core/lowering/calls.rb +2 -0
  17. data/lib/milk_tea/core/lowering/declarations.rb +1 -1
  18. data/lib/milk_tea/core/lowering/functions.rb +11 -8
  19. data/lib/milk_tea/core/lowering/resolve.rb +10 -3
  20. data/lib/milk_tea/core/lowering/scans.rb +13 -18
  21. data/lib/milk_tea/core/lowering.rb +18 -18
  22. data/lib/milk_tea/core/module_binder.rb +9 -10
  23. data/lib/milk_tea/core/module_loader.rb +64 -57
  24. data/lib/milk_tea/core/module_path_resolver.rb +1 -4
  25. data/lib/milk_tea/core/parser/declarations.rb +43 -19
  26. data/lib/milk_tea/core/parser/expressions.rb +4 -7
  27. data/lib/milk_tea/core/parser/statements.rb +5 -5
  28. data/lib/milk_tea/core/parser.rb +43 -17
  29. data/lib/milk_tea/core/semantic_analyzer/calls.rb +24 -31
  30. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +20 -23
  31. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +117 -85
  32. data/lib/milk_tea/core/semantic_analyzer/statements.rb +19 -41
  33. data/lib/milk_tea/core/semantic_analyzer.rb +57 -38
  34. data/lib/milk_tea/core.rb +6 -2
  35. data/lib/milk_tea/dap/lldb_dap_backend.rb +153 -0
  36. data/lib/milk_tea/dap/server/lldb_backend.rb +1 -1
  37. data/lib/milk_tea/dap.rb +1 -1
  38. data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
  39. data/lib/milk_tea/lsp.rb +1 -7
  40. data/lib/milk_tea/{bindings/cli.rb → tooling/bindgen_cli.rb} +1 -1
  41. data/lib/milk_tea/tooling/cli/commands/bindgen.rb +11 -0
  42. data/lib/milk_tea/tooling/cli/commands/build.rb +37 -0
  43. data/lib/milk_tea/tooling/cli/commands/cache.rb +46 -0
  44. data/lib/milk_tea/tooling/cli/commands/check.rb +116 -0
  45. data/lib/milk_tea/tooling/cli/commands/command_base.rb +8 -0
  46. data/lib/milk_tea/tooling/cli/commands/completions.rb +48 -0
  47. data/lib/milk_tea/tooling/cli/commands/dap.rb +58 -0
  48. data/lib/milk_tea/tooling/cli/commands/debug.rb +77 -0
  49. data/lib/milk_tea/tooling/cli/commands/deps.rb +17 -0
  50. data/lib/milk_tea/tooling/cli/commands/docs.rb +58 -0
  51. data/lib/milk_tea/tooling/cli/commands/emit_c.rb +64 -0
  52. data/lib/milk_tea/tooling/cli/commands/format.rb +199 -0
  53. data/lib/milk_tea/tooling/cli/commands/lex.rb +46 -0
  54. data/lib/milk_tea/tooling/cli/commands/lint.rb +248 -0
  55. data/lib/milk_tea/tooling/cli/commands/lower.rb +50 -0
  56. data/lib/milk_tea/tooling/cli/commands/lsp.rb +43 -0
  57. data/lib/milk_tea/tooling/cli/commands/new.rb +26 -0
  58. data/lib/milk_tea/tooling/cli/commands/parse.rb +51 -0
  59. data/lib/milk_tea/tooling/cli/commands/run.rb +99 -0
  60. data/lib/milk_tea/tooling/cli/commands/snapshot.rb +117 -0
  61. data/lib/milk_tea/tooling/cli/commands/test.rb +557 -0
  62. data/lib/milk_tea/tooling/cli/commands/toolchain.rb +16 -0
  63. data/lib/milk_tea/tooling/cli.rb +101 -1893
  64. data/lib/milk_tea/tooling.rb +1 -1
  65. metadata +29 -9
  66. data/lib/milk_tea/core/compile_time/const_eval.rb +0 -182
  67. data/lib/milk_tea/core/module_loader/errors.rb +0 -23
  68. data/lib/milk_tea/dap/backends/lldb_dap.rb +0 -156
  69. /data/lib/milk_tea/core/lowering/{async.rb → async/frame_builder.rb} +0 -0
@@ -1,26 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "functions"
4
+ require_relative "resolve"
5
+ require_relative "utils"
6
+
3
7
  module MilkTea
4
8
  module LowererScans
5
9
  def collect_structs
6
- @ctx.ast.declarations.each do |decl|
7
- case decl
8
- when AST::WhenStmt
9
- body = lower_when_chosen_body(decl)
10
- body&.each { |nested| collect_struct_from_decl(nested) }
11
- when AST::OpaqueDecl
12
- @ctx.opaque_types[decl.name] = @ctx.types.fetch(decl.name)
13
- when AST::StructDecl
14
- @ctx.struct_types[decl.name] = @ctx.types.fetch(decl.name)
15
- collect_nested_structs(decl)
16
- when AST::UnionDecl
17
- @ctx.union_types[decl.name] = @ctx.types.fetch(decl.name)
18
- end
19
- end
10
+ @ctx.ast.declarations.each { |decl| collect_one_struct_decl(decl) }
20
11
  end
21
12
 
22
- def collect_struct_from_decl(decl)
13
+ def collect_one_struct_decl(decl)
23
14
  case decl
15
+ when AST::WhenStmt
16
+ body = lower_when_chosen_body(decl)
17
+ body&.each { |nested| collect_one_struct_decl(nested) }
24
18
  when AST::OpaqueDecl
25
19
  @ctx.opaque_types[decl.name] = @ctx.types.fetch(decl.name)
26
20
  when AST::StructDecl
@@ -28,12 +22,13 @@ module MilkTea
28
22
  collect_nested_structs(decl)
29
23
  when AST::UnionDecl
30
24
  @ctx.union_types[decl.name] = @ctx.types.fetch(decl.name)
31
- when AST::WhenStmt
32
- body = lower_when_chosen_body(decl)
33
- body&.each { |nested| collect_struct_from_decl(nested) }
34
25
  end
35
26
  end
36
27
 
28
+ def collect_struct_from_decl(decl)
29
+ collect_one_struct_decl(decl)
30
+ end
31
+
37
32
  def lower_when_chosen_body(decl)
38
33
  val = compile_time_const_value(decl.discriminant)
39
34
  return nil if val.nil?
@@ -31,7 +31,7 @@ require_relative "lowering/functions"
31
31
  require_relative "lowering/async/analysis"
32
32
  require_relative "lowering/async/normalization"
33
33
  require_relative "lowering/async/lowering"
34
- require_relative "lowering/async"
34
+ require_relative "lowering/async/frame_builder"
35
35
  require_relative "lowering/block"
36
36
  require_relative "lowering/proc"
37
37
  require_relative "lowering/loops"
@@ -85,11 +85,27 @@ module MilkTea
85
85
  OrderResolution = Data.define(:target_type, :binding, :callee_name)
86
86
 
87
87
  class Lowerer
88
- include CompatibilityHelpers
88
+ include Intrinsics
89
89
 
90
90
  attr_accessor :bypass_sema_type_cache
91
91
  attr_reader :recorded_expr_types
92
92
 
93
+ include LowererScans
94
+ include LowererDeclarations
95
+ include LowererEvents
96
+ include LowererFunctions
97
+ include LowererAsync
98
+ include LowererBlock
99
+ include LowererProc
100
+ include LowererLoops
101
+ include LowererExpressions
102
+ include LowererCalls
103
+ include LowererForeignCstr
104
+ include LowererStrBuffer
105
+ include LowererResolve
106
+ include LowererDyn
107
+ include LowererUtils
108
+
93
109
  def initialize(program)
94
110
  @program = program
95
111
  @ctx = ModuleContext.new
@@ -358,21 +374,5 @@ module MilkTea
358
374
  ordered.concat(modules.values - ordered)
359
375
  ordered
360
376
  end
361
-
362
- include LowererScans
363
- include LowererDeclarations
364
- include LowererEvents
365
- include LowererFunctions
366
- include LowererAsync
367
- include LowererBlock
368
- include LowererProc
369
- include LowererLoops
370
- include LowererExpressions
371
- include LowererCalls
372
- include LowererForeignCstr
373
- include LowererStrBuffer
374
- include LowererResolve
375
- include LowererDyn
376
- include LowererUtils
377
377
  end
378
378
  end
@@ -136,17 +136,16 @@ module MilkTea
136
136
  imported_interface_binding?(interface, analysis.imports)
137
137
  end
138
138
 
139
+ BUILTIN_EXPORTABLE_RECEIVER_TYPES = %w[
140
+ StringView Primitive Vector Matrix Quaternion SoA Simd Span Task Dyn
141
+ ].map { |n| "Types::#{n}" }.freeze
142
+
143
+ def builtin_exportable_receiver?(receiver_type)
144
+ BUILTIN_EXPORTABLE_RECEIVER_TYPES.any? { |name| receiver_type.class.to_s.end_with?(name.split("::").last) }
145
+ end
146
+
139
147
  def exported_method_receiver?(receiver_type, analysis, exported_types)
140
- return true if receiver_type.is_a?(Types::StringView)
141
- return true if receiver_type.is_a?(Types::Primitive)
142
- return true if receiver_type.is_a?(Types::Vector)
143
- return true if receiver_type.is_a?(Types::Matrix)
144
- return true if receiver_type.is_a?(Types::Quaternion)
145
- return true if receiver_type.is_a?(Types::SoA)
146
- return true if receiver_type.is_a?(Types::Simd)
147
- return true if receiver_type.is_a?(Types::Span)
148
- return true if receiver_type.is_a?(Types::Task)
149
- return true if receiver_type.is_a?(Types::Dyn)
148
+ return true if builtin_exportable_receiver?(receiver_type)
150
149
  return true if exported_types.value?(receiver_type)
151
150
  return true if imported_receiver_type?(receiver_type, analysis.imports)
152
151
  return exported_method_receiver?(receiver_type.base, analysis, exported_types) if receiver_type.is_a?(Types::Nullable)
@@ -1,13 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "module_loader/errors"
4
- require_relative "module_path_resolver"
5
- require_relative "module_binder"
6
- require_relative "async_runtime_installer"
7
- require_relative "prelude_installer"
8
-
9
3
  module MilkTea
4
+ class ModuleLoadError < StandardError
5
+ attr_reader :path, :line, :column
6
+
7
+ def initialize(message, path:, line: nil, column: nil)
8
+ @path = path
9
+ @line = line
10
+ @column = column
11
+ super("#{message}: #{path}")
12
+ end
13
+
14
+ def code
15
+ "module/error"
16
+ end
17
+ end
18
+
10
19
  class ModuleLoader
20
+ ImportResolution = Data.define(:modules, :errors)
21
+ ImportResolutionError = Data.define(:import, :error)
11
22
  Program = Data.define(:root_path, :root_analysis, :analyses_by_path, :analyses_by_module_name)
12
23
  PLATFORM_SUFFIXES = {
13
24
  "linux" => :linux,
@@ -90,6 +101,15 @@ module MilkTea
90
101
  MilkTea.host_platform
91
102
  end
92
103
 
104
+ def self.raise_platform_conflict!(path, pinned_platform, active_platform, error_class: nil)
105
+ if error_class == ModuleLoadError
106
+ raise ModuleLoadError.new("source file targets platform #{pinned_platform}; active platform is #{active_platform}", path:)
107
+ end
108
+
109
+ message = "source file #{path} targets platform #{pinned_platform}; active platform is #{active_platform}"
110
+ raise(error_class || ArgumentError, message)
111
+ end
112
+
93
113
  def initialize(module_roots: [MilkTea.root], package_graph: nil, shared_cache: nil, source_overrides: nil, platform: nil)
94
114
  @module_roots = module_roots.map { |root| File.expand_path(root.to_s) }
95
115
  @ast_cache = {}
@@ -297,48 +317,14 @@ module MilkTea
297
317
  end
298
318
 
299
319
  def imported_modules_for_ast(ast, importer_path: nil)
300
- modules = {}
301
-
302
- ast.imports.each do |import|
303
- import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path:, importer_module_name: ast.module_name.to_s)
304
-
305
- if @forward_bindings.key?(import_path)
306
- modules[import.path.to_s] = @forward_bindings[import_path]
307
- else
308
- import_analysis = check_path(import_path)
309
- modules[import.path.to_s] = @binder.module_binding(import_analysis)
310
- end
311
- end
312
-
313
- @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: false)
314
- @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: false)
315
- modules.freeze
320
+ resolve_imports_for_ast(ast, importer_path:, collecting: false)
316
321
  end
317
322
 
318
- def build_global_import_index(ast)
319
- index = {}
320
- current_imports = ast.imports.map { |import| import.path.to_s }.to_set
321
-
322
- @analysis_cache.each_value do |analysis|
323
- next unless analysis
324
- next unless analysis.module_name
325
-
326
- mod_name = analysis.module_name.to_s
327
- next if current_imports.include?(mod_name)
328
- next if mod_name == ast.module_name.to_s
329
-
330
- types = analysis.respond_to?(:types) ? analysis.types : {}
331
- types.each_key do |type_name|
332
- type_str = type_name.to_s
333
- index[type_str] ||= []
334
- index[type_str] << mod_name unless index[type_str].include?(mod_name)
335
- end
336
- end
337
-
338
- index
323
+ def imported_modules_for_ast_collecting_errors(ast, importer_path: nil)
324
+ resolve_imports_for_ast(ast, importer_path:, collecting: true)
339
325
  end
340
326
 
341
- def imported_modules_for_ast_collecting_errors(ast, importer_path: nil)
327
+ def resolve_imports_for_ast(ast, importer_path:, collecting: false)
342
328
  modules = {}
343
329
  errors = []
344
330
 
@@ -349,27 +335,55 @@ module MilkTea
349
335
  if @forward_bindings.key?(import_path)
350
336
  modules[import.path.to_s] = @forward_bindings[import_path]
351
337
  else
352
- import_analysis = check_path_collecting_errors(import_path)
338
+ import_analysis = collecting ? check_path_collecting_errors(import_path) : check_path(import_path)
353
339
  modules[import.path.to_s] = @binder.module_binding(import_analysis)
354
340
  end
355
341
  rescue ModuleLoadError, PackageLockError, SemanticError => e
342
+ raise unless collecting
356
343
  errors << ImportResolutionError.new(import:, error: e)
357
344
  end
358
345
  end
359
346
 
360
347
  begin
361
- @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: true)
348
+ @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: collecting)
362
349
  rescue ModuleLoadError, PackageLockError => e
350
+ raise unless collecting
363
351
  errors << ImportResolutionError.new(import: nil, error: e)
364
352
  end
365
353
 
366
354
  begin
367
- @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: true)
355
+ @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: collecting)
368
356
  rescue ModuleLoadError, PackageLockError => e
357
+ raise unless collecting
369
358
  errors << ImportResolutionError.new(import: nil, error: e)
370
359
  end
371
360
 
372
- ImportResolution.new(modules: modules.freeze, errors: errors.freeze)
361
+ return ImportResolution.new(modules: modules.freeze, errors: errors.freeze) if collecting
362
+
363
+ modules.freeze
364
+ end
365
+
366
+ def build_global_import_index(ast)
367
+ index = {}
368
+ current_imports = ast.imports.map { |import| import.path.to_s }.to_set
369
+
370
+ @analysis_cache.each_value do |analysis|
371
+ next unless analysis
372
+ next unless analysis.module_name
373
+
374
+ mod_name = analysis.module_name.to_s
375
+ next if current_imports.include?(mod_name)
376
+ next if mod_name == ast.module_name.to_s
377
+
378
+ types = analysis.respond_to?(:types) ? analysis.types : {}
379
+ types.each_key do |type_name|
380
+ type_str = type_name.to_s
381
+ index[type_str] ||= []
382
+ index[type_str] << mod_name unless index[type_str].include?(mod_name)
383
+ end
384
+ end
385
+
386
+ index
373
387
  end
374
388
 
375
389
  # Errors collected per analyzed import path during collecting-mode checks.
@@ -380,15 +394,6 @@ module MilkTea
380
394
  @collecting_path_errors
381
395
  end
382
396
 
383
- def self.raise_platform_conflict!(path, pinned_platform, active_platform, error_class: nil)
384
- if error_class == ModuleLoadError
385
- raise ModuleLoadError.new("source file targets platform #{pinned_platform}; active platform is #{active_platform}", path:)
386
- end
387
-
388
- message = "source file #{path} targets platform #{pinned_platform}; active platform is #{active_platform}"
389
- raise(error_class || ArgumentError, message)
390
- end
391
-
392
397
  def check_path(path)
393
398
  resolved_path, ast, cached = check_module_cache(path)
394
399
  return cached if cached
@@ -532,6 +537,8 @@ module MilkTea
532
537
  types[decl.name] = Types::Flags.new(decl.name, module_name:)
533
538
  when AST::OpaqueDecl
534
539
  types[decl.name] = Types::Opaque.new(decl.name, module_name:, external: false)
540
+ when AST::UnionDecl
541
+ types[decl.name] = Types::Union.new(decl.name, module_name:)
535
542
  end
536
543
  end
537
544
 
@@ -119,10 +119,7 @@ module MilkTea
119
119
  return @package_manifest_cache[manifest_path] if @package_manifest_cache.key?(manifest_path)
120
120
 
121
121
  @package_manifest_cache[manifest_path] = PackageManifest.load(path)
122
- rescue PackageManifestError
123
- @package_manifest_cache[manifest_path] = nil if manifest_path
124
- nil
125
- end
122
+ end
126
123
 
127
124
  def package_namespace_match?(module_name, package_name)
128
125
  module_name == package_name || module_name.start_with?("#{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
@@ -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
@@ -155,6 +157,23 @@ module MilkTea
155
157
  next_token.nil? || %i[newline eof].include?(next_token.type)
156
158
  end
157
159
 
160
+ def advance
161
+ @current += 1 unless eof?
162
+ previous
163
+ end
164
+
165
+ def eof?
166
+ peek.type == :eof
167
+ end
168
+
169
+ def peek
170
+ @tokens[@current]
171
+ end
172
+
173
+ def previous
174
+ @tokens[@current - 1]
175
+ end
176
+
158
177
  def match(*types)
159
178
  return false unless types.any? { |type| check(type) }
160
179
 
@@ -219,23 +238,6 @@ module MilkTea
219
238
  true
220
239
  end
221
240
 
222
- def advance
223
- @current += 1 unless eof?
224
- previous
225
- end
226
-
227
- def eof?
228
- peek.type == :eof
229
- end
230
-
231
- def peek
232
- @tokens[@current]
233
- end
234
-
235
- def previous
236
- @tokens[@current - 1]
237
- end
238
-
239
241
  def error(token, message)
240
242
  ParseError.new(message, token:, path: @path)
241
243
  end
@@ -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