mt-lang 0.3.20 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83f7a9e4ec79d81bd7d2e64ebb469871fecb5e4b3f888c932061d5fa265b7544
4
- data.tar.gz: c08d508785143d338a6a47adfcedcf605b30e8e9990913a44178344643944178
3
+ metadata.gz: 6dc76fcf0acde63fa4060157d6c6864dedcc32b0f5c2cd60422e9948d9861dab
4
+ data.tar.gz: 0b1bf5aee4d6be4d0f1968a7dacaf35e37557336a5911834a17324d23bd60ba3
5
5
  SHA512:
6
- metadata.gz: 95818de5161127d240aacd92b2f8a727e29d1962c11bd9161faa0f0286ac701d2b328352d7ac51bdab23ed2b76a8ccaa7e698490c4bbe5b99edb9452de76cbac
7
- data.tar.gz: 0d188ebbabd4f175a201ea61c7cde3393a93b9deed991fbb639935b125e565b3b679f16c0fb72c4f96939475d8b6a199362e66920d65e49dd60e6dbc248df372
6
+ metadata.gz: 1b34537660a1c2424b3f829ff6ef18d35de4366fe76d0a0938216261b1f9c7c56d8fce2c8a0ac1162f17ddc8c1a2c565f9e08f2f5f0056624f4959d35833cabf
7
+ data.tar.gz: 9b6b00b5b761ab3128c617acb8398c64b829df1e2d6984661bc6e8bfb2081b9fa408fd5e8774e1b540884a59caef4eef146c3a05f0d176cb62019471b4e28b88
data/lib/milk_tea/base.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module MilkTea
6
- VERSION = "0.3.20"
6
+ VERSION = "0.3.22"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -413,7 +413,8 @@ module MilkTea
413
413
  read_identifiers(expression.expression, names, reads_info)
414
414
  expression.arms.each do |arm|
415
415
  read_identifiers(arm.pattern, names, reads_info)
416
- read_identifiers(arm.value, names, reads_info)
416
+ arm_value = arm.is_a?(AST::MatchExprArm) ? arm.value : arm.body
417
+ read_identifiers(arm_value, names, reads_info)
417
418
  end
418
419
  when AST::MatchStmt
419
420
  read_identifiers(expression.expression, names, reads_info)
@@ -1506,7 +1506,7 @@ module MilkTea
1506
1506
  case argument
1507
1507
  when AST::TypeRef
1508
1508
  resolve_type_argument_ref(argument, type_params:)
1509
- when AST::FunctionType, AST::ProcType
1509
+ when AST::FunctionType, AST::ProcType, AST::TupleType
1510
1510
  resolve_type_ref(argument, type_params:)
1511
1511
  when AST::IntegerLiteral, AST::FloatLiteral
1512
1512
  Types::LiteralTypeArg.new(argument.value)
@@ -20,19 +20,24 @@ module MilkTea
20
20
  when AST::StructDecl, AST::UnionDecl, AST::VariantDecl, AST::EnumDecl, AST::FlagsDecl, AST::OpaqueDecl, AST::TypeAliasDecl
21
21
  type_declarations[declaration.name] = declaration
22
22
  target = exported_declaration?(analysis, declaration) ? types : private_types
23
- target[declaration.name] = analysis.types.fetch(declaration.name)
23
+ resolved_type = analysis.types[declaration.name]
24
+ target[declaration.name] = resolved_type if resolved_type
24
25
  when AST::InterfaceDecl
25
26
  target = exported_declaration?(analysis, declaration) ? interfaces : private_interfaces
26
- target[declaration.name] = analysis.interfaces.fetch(declaration.name)
27
+ resolved_iface = analysis.interfaces[declaration.name]
28
+ target[declaration.name] = resolved_iface if resolved_iface
27
29
  when AST::AttributeDecl
28
30
  target = exported_declaration?(analysis, declaration) ? attributes : private_attributes
29
- target[declaration.name] = analysis.attributes.fetch(declaration.name)
31
+ resolved_attr = analysis.attributes[declaration.name]
32
+ target[declaration.name] = resolved_attr if resolved_attr
30
33
  when AST::ConstDecl, AST::VarDecl, AST::EventDecl
31
34
  target = exported_declaration?(analysis, declaration) ? values : private_values
32
- target[declaration.name] = analysis.values.fetch(declaration.name)
35
+ resolved_val = analysis.values[declaration.name]
36
+ target[declaration.name] = resolved_val if resolved_val
33
37
  when AST::FunctionDef, AST::ExternFunctionDecl, AST::ForeignFunctionDecl
34
38
  target = exported_declaration?(analysis, declaration) ? functions : private_functions
35
- target[declaration.name] = analysis.functions.fetch(declaration.name)
39
+ resolved_func = analysis.functions[declaration.name]
40
+ target[declaration.name] = resolved_func if resolved_func
36
41
  end
37
42
  end
38
43
 
@@ -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
163
167
 
164
- check_program_parallel(root_path)
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
165
174
 
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
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
 
@@ -328,6 +401,15 @@ module MilkTea
328
401
  modules = {}
329
402
  errors = []
330
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
412
+
331
413
  ast.imports.each do |import|
332
414
  begin
333
415
  import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path:, importer_module_name: ast.module_name.to_s)
@@ -340,7 +422,8 @@ module MilkTea
340
422
  end
341
423
  rescue ModuleLoadError, PackageLockError, SemanticError => e
342
424
  raise unless collecting
343
- errors << ImportResolutionError.new(import:, error: e)
425
+
426
+ handle_circular_import_in_collecting_mode(import, import_path, modules, errors, e)
344
427
  end
345
428
  end
346
429
 
@@ -361,6 +444,8 @@ module MilkTea
361
444
  return ImportResolution.new(modules: modules.freeze, errors: errors.freeze) if collecting
362
445
 
363
446
  modules.freeze
447
+ ensure
448
+ @import_resolve_depth -= 1 if @import_resolve_depth
364
449
  end
365
450
 
366
451
  def build_global_import_index(ast)
@@ -521,6 +606,74 @@ module MilkTea
521
606
  normalized_path == normalized_root || normalized_path.start_with?(normalized_root + File::SEPARATOR)
522
607
  end
523
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
+
524
677
  def create_forward_binding(ast)
525
678
  module_name = ast.module_name.to_s
526
679
  types = {}
@@ -528,9 +681,13 @@ module MilkTea
528
681
  ast.declarations.each do |decl|
529
682
  case decl
530
683
  when AST::StructDecl
531
- 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))
532
687
  when AST::VariantDecl
533
- 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))
534
691
  when AST::EnumDecl
535
692
  types[decl.name] = Types::Enum.new(decl.name, module_name:)
536
693
  when AST::FlagsDecl
@@ -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,7 +123,7 @@ 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
- end
126
+ end
123
127
 
124
128
  def package_namespace_match?(module_name, package_name)
125
129
  module_name == package_name || module_name.start_with?("#{package_name}.")
@@ -640,7 +640,7 @@ module MilkTea
640
640
 
641
641
  def definite_type_argument?(value)
642
642
  case value
643
- when AST::FunctionType
643
+ when AST::FunctionType, AST::TupleType
644
644
  true
645
645
  when AST::TypeRef
646
646
  known_type_like_name?(value.name.parts.first)
@@ -58,6 +58,20 @@ module MilkTea
58
58
  imported_candidates << [module_binding, imported_method] if imported_method
59
59
  end
60
60
 
61
+ if imported_candidates.empty?
62
+ # Identity-based lookup failed. Try a name-based fallback across
63
+ # all imports, but only take the first match — not all candidates —
64
+ # to avoid false "ambiguous method" errors when two modules both
65
+ # extend the same receiver type with the same method name.
66
+ @ctx.imports.each_value do |module_binding|
67
+ method = find_method_by_receiver_name(module_binding, dispatch_receiver_type, name)
68
+ if method
69
+ imported_candidates << [module_binding, method]
70
+ break
71
+ end
72
+ end
73
+ end
74
+
61
75
  if imported_candidates.empty?
62
76
  owner_module = reachable_module_binding_for_type(receiver_type)
63
77
  return nil unless owner_module
@@ -77,17 +91,25 @@ module MilkTea
77
91
  def module_binding_method(module_binding, receiver_type, dispatch_receiver_type, name)
78
92
  method = module_binding.methods.fetch(receiver_type, {})[name]
79
93
  method ||= module_binding.methods.fetch(dispatch_receiver_type, {})[name] unless dispatch_receiver_type == receiver_type
94
+ method ||= find_method_by_receiver_name(module_binding, dispatch_receiver_type, name)
80
95
  method
81
96
  end
82
97
 
83
- def reachable_module_binding_for_type(receiver_type)
84
- module_name = receiver_type_module_name(receiver_type)
85
- return nil unless module_name
86
- return nil if module_name == @ctx.module_name
87
-
88
- find_reachable_imported_module(module_name)
98
+ def find_method_by_receiver_name(module_binding, receiver_type, name)
99
+ module_binding.methods.each do |key, methods|
100
+ return methods[name] if key.is_a?(receiver_type.class) && key.name == receiver_type.name && methods.key?(name)
101
+ end
102
+ nil
89
103
  end
90
104
 
105
+ def reachable_module_binding_for_type(receiver_type)
106
+ module_name = receiver_type_module_name(receiver_type)
107
+ return nil unless module_name
108
+ return nil if module_name == @ctx.module_name
109
+
110
+ find_reachable_imported_module(module_name)
111
+ end
112
+
91
113
  def receiver_type_module_name(receiver_type)
92
114
  return receiver_type_module_name(receiver_type.base) if receiver_type.is_a?(Types::Nullable)
93
115
  return receiver_type.module_name if receiver_type.respond_to?(:module_name)
@@ -399,7 +421,7 @@ module MilkTea
399
421
  case argument
400
422
  when AST::TypeRef
401
423
  resolve_type_argument_ref(argument, type_params:, type_param_constraints:)
402
- when AST::FunctionType, AST::ProcType
424
+ when AST::FunctionType, AST::ProcType, AST::TupleType
403
425
  resolve_type_ref(argument, type_params:, type_param_constraints:)
404
426
  when AST::IntegerLiteral, AST::FloatLiteral
405
427
  Types::LiteralTypeArg.new(argument.value)
@@ -822,6 +822,18 @@ module MilkTea
822
822
  self
823
823
  end
824
824
 
825
+ def eql?(other)
826
+ other.is_a?(GenericStructDefinition) && other.name == name &&
827
+ other.type_params == type_params && other.module_name == module_name &&
828
+ other.external == external && other.packed == packed && other.alignment == alignment
829
+ end
830
+
831
+ alias == eql?
832
+
833
+ def hash
834
+ [self.class, name, type_params, module_name, external, packed, alignment].hash
835
+ end
836
+
825
837
  def set_layout(packed:, alignment:)
826
838
  @packed = packed
827
839
  @alignment = alignment
@@ -853,23 +865,6 @@ module MilkTea
853
865
  name
854
866
  end
855
867
 
856
- def eql?(other)
857
- other.class == self.class &&
858
- other.name == name &&
859
- other.type_params == type_params &&
860
- other.module_name == module_name &&
861
- other.external == external &&
862
- other.packed == packed &&
863
- other.alignment == alignment &&
864
- other.linkage_name == linkage_name
865
- end
866
-
867
- alias == eql?
868
-
869
- def hash
870
- [self.class, name, type_params, module_name, external, packed, alignment, linkage_name].hash
871
- end
872
-
873
868
  def instantiate(arguments)
874
869
  raise ArgumentError, "#{name} expects #{type_params.length} type arguments, got #{arguments.length}" unless arguments.length == type_params.length
875
870
 
@@ -179,6 +179,19 @@ module MilkTea
179
179
  platform: effective_platform,
180
180
  )
181
181
  module_name = loader.send(:inferred_module_name_for_path, path) rescue nil
182
+
183
+ # Run the full two-pass program check first so that @analysis_cache
184
+ # has fully-checked analyses for all transitive modules. When the
185
+ # import resolution below encounters a cycle member, it finds the
186
+ # cached analysis and resolves correctly. If the program check fails
187
+ # (e.g. missing module), fall through to the standard resolution
188
+ # path so that prelude modules and regular errors are still reported.
189
+ begin
190
+ loader.check_program_collecting(path)
191
+ rescue ModuleLoadError, PackageLockError
192
+ # best-effort — standard path below handles diagnostics
193
+ end
194
+
182
195
  resolution_result = loader.imported_modules_for_ast_collecting_errors(ast, importer_path: path)
183
196
  unresolved_import_paths = []
184
197
 
@@ -76,20 +76,11 @@ module MilkTea
76
76
  def check_single_reporting_all(path, locked: false)
77
77
  loader = make_module_loader(path, locked:, platform: ModuleLoader.default_host_platform)
78
78
  resolved_path = File.expand_path(path)
79
- ast = loader.load_file(resolved_path)
80
- module_name = ast.module_name.to_s
81
79
 
82
- import_result = loader.imported_modules_for_ast_collecting_errors(ast, importer_path: resolved_path)
83
- errors = import_result.errors.dup
84
-
85
- analysis = nil
86
- begin
87
- result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules: import_result.modules, path: resolved_path)
88
- errors.concat(result[:errors])
89
- analysis = result[:analysis]
90
- rescue SemanticError => e
91
- errors << e
92
- end
80
+ result = loader.check_program_collecting(path)
81
+ errors = result[:errors]
82
+ analysis = result[:root_analysis]
83
+ module_name = result[:module_name]
93
84
 
94
85
  if analysis && errors.empty?
95
86
  source = read_source_file(path)
@@ -97,9 +88,8 @@ module MilkTea
97
88
  errors.concat(warnings)
98
89
  end
99
90
 
100
- closure_errors = loader.collecting_path_errors.values.flatten.compact
101
- [errors, module_name, closure_errors]
102
- rescue ModuleLoadError, PackageLockError, SemanticError => e
91
+ [errors, module_name, []]
92
+ rescue ModuleLoadError, PackageLockError => e
103
93
  [[e], nil, []]
104
94
  end
105
95
 
@@ -72,11 +72,15 @@ module MilkTea
72
72
  # ── Token serialisation ───────────────────────────────────────────
73
73
 
74
74
  def emit_token(token, buf)
75
- buf << "(token :type " << emit_token_type(token.type)
75
+ buf << "(token :type :" << token.type.to_s.tr("_", "-")
76
76
  buf << " :lexeme "
77
77
  emit_string(token.lexeme, buf)
78
78
  buf << " :literal "
79
- emit_value(token.literal, buf)
79
+ if token.type == :float
80
+ buf << token.lexeme
81
+ else
82
+ emit_value(token.literal, buf)
83
+ end
80
84
  buf << " :line " << token.line.to_s
81
85
  buf << " :column " << token.column.to_s
82
86
  buf << " :start_offset " << token.start_offset.to_s
@@ -84,10 +88,6 @@ module MilkTea
84
88
  buf << ")"
85
89
  end
86
90
 
87
- def emit_token_type(sym)
88
- ":" + sym.to_s.tr("_", "-")
89
- end
90
-
91
91
  # ── Types serialisation ───────────────────────────────────────────
92
92
 
93
93
  TYPES_SHORT_NAMES = {
@@ -337,7 +337,7 @@ module MilkTea
337
337
  end
338
338
 
339
339
  def format_float(value)
340
- s = sprintf("%.17g", value)
340
+ s = sprintf("%g", value)
341
341
  s.include?(".") || s.include?("e") ? s : s + ".0"
342
342
  end
343
343
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mt-lang
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.20
4
+ version: 0.3.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Long (Teefan) Tran
@@ -625,7 +625,7 @@ metadata:
625
625
  homepage_uri: https://teefan.github.io/mt-lang/
626
626
  source_code_uri: https://github.com/teefan/mt-lang
627
627
  post_install_message: |
628
- Milk Tea 0.3.20 installed!
628
+ Milk Tea 0.3.22 installed!
629
629
 
630
630
  System requirements:
631
631
  - A C compiler (gcc or clang) must be available on PATH