Class: MilkTea::ModuleLoader
- Inherits:
-
Object
- Object
- MilkTea::ModuleLoader
- Defined in:
- lib/milk_tea/core/module_loader.rb
Defined Under Namespace
Classes: ImportResolution, ImportResolutionError, Program
Constant Summary collapse
- PLATFORM_SUFFIXES =
{ "linux" => :linux, "windows" => :windows, "wasm" => :wasm, }.freeze
Class Method Summary collapse
- .check_file(path, platform: nil) ⇒ Object
- .check_program(path, platform: nil) ⇒ Object
- .default_host_platform ⇒ Object
- .effective_platform_for_path(path, platform_override: nil, host_platform: nil) ⇒ Object
- .load_file(path, platform: nil) ⇒ Object
- .normalize_platform_name(value) ⇒ Object
- .platform_suffix_for_path(path) ⇒ Object
- .raise_platform_conflict!(path, pinned_platform, active_platform, error_class: nil) ⇒ Object
- .resolve_source_path(path, platform: nil, error_class: nil) ⇒ Object
Instance Method Summary collapse
- #build_global_import_index(ast) ⇒ Object
- #build_program(root_path) ⇒ Object
- #capture_analysis_for_cycle_member(resolved_path) ⇒ Object
- #check_file(path) ⇒ Object
- #check_level_parallel(paths) ⇒ Object
- #check_module_cache(path, extra_cache: nil) ⇒ Object
- #check_path(path) ⇒ Object
- #check_path_collecting_errors(path) ⇒ Object
- #check_program(path) ⇒ Object
-
#check_program_collecting(path) ⇒ Object
Variant of #check_program that collects errors instead of raising so diagnostic paths (check, debug, LSP) can report all issues at once.
- #check_program_parallel(root_path, collecting_errors: nil) ⇒ Object
-
#collecting_path_errors ⇒ Object
Errors collected per analyzed import path during collecting-mode checks.
- #create_forward_binding(ast) ⇒ Object
- #dfs_reachable_from(current, target, graph, visited) ⇒ Object
- #handle_circular_import_in_collecting_mode(import, import_path, modules, errors, error) ⇒ Object
- #imported_modules_for_ast(ast, importer_path: nil) ⇒ Object
- #imported_modules_for_ast_collecting_errors(ast, importer_path: nil) ⇒ Object
- #inferred_module_name_for_path(path) ⇒ Object
-
#initialize(module_roots: [MilkTea.root], package_graph: nil, shared_cache: nil, source_overrides: nil, platform: nil) ⇒ ModuleLoader
constructor
A new instance of ModuleLoader.
- #load_file(path) ⇒ Object
- #module_name_for_path(path, root) ⇒ Object
- #node_in_cycle?(node, graph) ⇒ Boolean
- #normalize_source_overrides(source_overrides) ⇒ Object
- #parse_all(resolved_path) ⇒ Object
- #parse_file(path) ⇒ Object
- #path_within_root?(path, root) ⇒ Boolean
- #populate_full_forward_bindings(cycle_members) ⇒ Object
- #resolve_imports_for_ast(ast, importer_path:, collecting: false) ⇒ Object
- #topo_sort_levels(graph) ⇒ Object
- #update_shared_cache(resolved_path, analysis) ⇒ Object
- #use_shared_cache? ⇒ Boolean
- #with_check_context(path, &block) ⇒ Object
Constructor Details
#initialize(module_roots: [MilkTea.root], package_graph: nil, shared_cache: nil, source_overrides: nil, platform: nil) ⇒ ModuleLoader
Returns a new instance of ModuleLoader.
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/milk_tea/core/module_loader.rb', line 113 def initialize(module_roots: [MilkTea.root], package_graph: nil, shared_cache: nil, source_overrides: nil, platform: nil) @module_roots = module_roots.map { |root| File.(root.to_s) } @ast_cache = {} @parse_cache = {} @analysis_cache = {} @collecting_analysis_cache = {} @collecting_path_errors = {} @checking_paths = [] @forward_bindings = {} @platform = self.class.normalize_platform_name(platform) @package_graph = package_graph @package_manifest_cache = {} @shared_cache = shared_cache # Hash or nil; mutated in-place to persist across calls @source_overrides = normalize_source_overrides(source_overrides) @path_resolver = ModulePathResolver.new( module_roots: @module_roots, platform: @platform, package_graph: @package_graph, source_overrides: @source_overrides, package_manifest_cache: @package_manifest_cache, ) @binder = ModuleBinder.new @async_runtime_installer = AsyncRuntimeInstaller.new( resolve_module_path: @path_resolver.method(:resolve_module_path), check_block: ->(path, collecting) { collecting ? check_path_collecting_errors(path) : check_path(path) }, bind_block: @binder.method(:module_binding), ) @prelude_installer = PreludeInstaller.new( resolve_module_path: @path_resolver.method(:resolve_module_path), check_block: ->(path, collecting) { collecting ? check_path_collecting_errors(path) : check_path(path) }, bind_block: @binder.method(:module_binding), ) end |
Class Method Details
.check_file(path, platform: nil) ⇒ Object
33 34 35 |
# File 'lib/milk_tea/core/module_loader.rb', line 33 def self.check_file(path, platform: nil) new(platform:).check_file(path) end |
.check_program(path, platform: nil) ⇒ Object
37 38 39 |
# File 'lib/milk_tea/core/module_loader.rb', line 37 def self.check_program(path, platform: nil) new(platform:).check_program(path) end |
.default_host_platform ⇒ Object
100 101 102 |
# File 'lib/milk_tea/core/module_loader.rb', line 100 def self.default_host_platform MilkTea.host_platform end |
.effective_platform_for_path(path, platform_override: nil, host_platform: nil) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/milk_tea/core/module_loader.rb', line 65 def self.effective_platform_for_path(path, platform_override: nil, host_platform: nil) normalized_override = normalize_platform_name(platform_override) return normalized_override if normalized_override suffix_platform = platform_suffix_for_path(path) return suffix_platform if suffix_platform manifest_platform = PackageManifest.load(path).platform return manifest_platform if manifest_platform normalize_platform_name(host_platform || default_host_platform) rescue PackageManifestError normalize_platform_name(host_platform || default_host_platform) end |
.load_file(path, platform: nil) ⇒ Object
29 30 31 |
# File 'lib/milk_tea/core/module_loader.rb', line 29 def self.load_file(path, platform: nil) new(platform:).load_file(path) end |
.normalize_platform_name(value) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/milk_tea/core/module_loader.rb', line 41 def self.normalize_platform_name(value) return nil if value.nil? || value.to_s.strip.empty? case value.to_s.strip.downcase when "linux" :linux when "windows", "win", "win32" :windows when "wasm", "web", "html5", "browser" :wasm when "darwin", "macos", "osx" :darwin else raise ArgumentError, "unknown platform #{value}; expected linux|windows|wasm|darwin" end end |
.platform_suffix_for_path(path) ⇒ Object
58 59 60 61 62 63 |
# File 'lib/milk_tea/core/module_loader.rb', line 58 def self.platform_suffix_for_path(path) match = File.basename(path.to_s).match(/\.(linux|windows|wasm)\.mt\z/) return nil unless match PLATFORM_SUFFIXES.fetch(match[1]) end |
.raise_platform_conflict!(path, pinned_platform, active_platform, error_class: nil) ⇒ Object
104 105 106 107 108 109 110 111 |
# File 'lib/milk_tea/core/module_loader.rb', line 104 def self.raise_platform_conflict!(path, pinned_platform, active_platform, error_class: nil) if error_class == ModuleLoadError raise ModuleLoadError.new("source file targets platform #{pinned_platform}; active platform is #{active_platform}", path:) end = "source file #{path} targets platform #{pinned_platform}; active platform is #{active_platform}" raise(error_class || ArgumentError, ) end |
.resolve_source_path(path, platform: nil, error_class: nil) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/milk_tea/core/module_loader.rb', line 80 def self.resolve_source_path(path, platform: nil, error_class: nil) = File.(path.to_s) normalized_platform = platform.nil? ? nil : normalize_platform_name(platform) pinned_platform = platform_suffix_for_path() if pinned_platform if normalized_platform && normalized_platform != pinned_platform raise_platform_conflict!(, pinned_platform, normalized_platform, error_class:) end return end return unless normalized_platform && .end_with?(".mt") variant_path = .sub(/\.mt\z/, ".#{normalized_platform}.mt") return variant_path if File.file?(variant_path) end |
Instance Method Details
#build_global_import_index(ast) ⇒ Object
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 |
# File 'lib/milk_tea/core/module_loader.rb', line 451 def build_global_import_index(ast) index = {} current_imports = ast.imports.map { |import| import.path.to_s }.to_set @analysis_cache.each_value do |analysis| next unless analysis next unless analysis.module_name mod_name = analysis.module_name.to_s next if current_imports.include?(mod_name) next if mod_name == ast.module_name.to_s types = analysis.respond_to?(:types) ? analysis.types : {} types.each_key do |type_name| type_str = type_name.to_s index[type_str] ||= [] index[type_str] << mod_name unless index[type_str].include?(mod_name) end end index end |
#build_program(root_path) ⇒ Object
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/milk_tea/core/module_loader.rb', line 187 def build_program(root_path) root_analysis = @analysis_cache.fetch(root_path) analyses_by_module_name = @analysis_cache.each_value.each_with_object({}) do |analysis, modules| next unless analysis.module_name modules[analysis.module_name] = analysis end Program.new( root_path:, root_analysis:, analyses_by_path: @analysis_cache.dup.freeze, analyses_by_module_name: analyses_by_module_name.freeze, ) end |
#capture_analysis_for_cycle_member(resolved_path) ⇒ Object
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 |
# File 'lib/milk_tea/core/module_loader.rb', line 625 def capture_analysis_for_cycle_member(resolved_path) return if @analysis_cache[resolved_path] ast = @parse_cache[resolved_path] return unless ast @checking_paths << resolved_path import_result = resolve_imports_for_ast(ast, importer_path: resolved_path, collecting: true) result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules: import_result.modules, path: resolved_path) @analysis_cache[resolved_path] = result[:analysis] if result[:analysis] rescue StandardError # Analysis capture is best-effort. ensure @checking_paths.pop end |
#check_file(path) ⇒ Object
153 154 155 |
# File 'lib/milk_tea/core/module_loader.rb', line 153 def check_file(path) check_program(path).root_analysis end |
#check_level_parallel(paths) ⇒ Object
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 |
# File 'lib/milk_tea/core/module_loader.rb', line 372 def check_level_parallel(paths) threads = paths.map do |resolved_path| Thread.new do Thread.current[:resolved_path] = resolved_path begin analysis = check_path(resolved_path) Thread.current[:analysis] = analysis rescue ModuleLoadError, PackageLockError, SemanticError => e Thread.current[:error] = e end end end threads.each(&:join) paths.zip(threads).each do |resolved_path, t| raise t[:error] if t[:error] end end |
#check_module_cache(path, extra_cache: nil) ⇒ Object
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 |
# File 'lib/milk_tea/core/module_loader.rb', line 522 def check_module_cache(path, extra_cache: nil) resolved_path = self.class.resolve_source_path(path, platform: @platform, error_class: ModuleLoadError) return [resolved_path, nil, @analysis_cache[resolved_path]] if @analysis_cache.key?(resolved_path) return [resolved_path, nil, extra_cache[resolved_path]] if extra_cache&.key?(resolved_path) if use_shared_cache? entry = @shared_cache[resolved_path] if entry mtime = File.mtime(resolved_path).to_f rescue nil if mtime && entry[:mtime] == mtime @analysis_cache[resolved_path] = entry[:analysis] return [resolved_path, nil, entry[:analysis]] end end end ast = load_file(resolved_path) [resolved_path, ast, nil] end |
#check_path(path) ⇒ Object
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 |
# File 'lib/milk_tea/core/module_loader.rb', line 482 def check_path(path) resolved_path, ast, cached = check_module_cache(path) return cached if cached if @checking_paths.include?(resolved_path) && !@forward_bindings.key?(resolved_path) raise ModuleLoadError.new("circular import not resolvable: module #{@parse_cache[resolved_path]&.module_name || File.basename(path)}", path: resolved_path) end @checking_paths << resolved_path imported_modules = imported_modules_for_ast(ast, importer_path: resolved_path) global_index = build_global_import_index(ast) analysis = SemanticAnalyzer.check(ast, imported_modules:, path: resolved_path, global_import_index: global_index) @analysis_cache[resolved_path] = analysis update_shared_cache(resolved_path, analysis) analysis ensure @checking_paths.pop end |
#check_path_collecting_errors(path) ⇒ Object
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 |
# File 'lib/milk_tea/core/module_loader.rb', line 501 def check_path_collecting_errors(path) resolved_path, ast, cached = check_module_cache(path, extra_cache: @collecting_analysis_cache) return cached if cached if @checking_paths.include?(resolved_path) && !@forward_bindings.key?(resolved_path) raise ModuleLoadError.new("circular import not resolvable: module #{@parse_cache[resolved_path]&.module_name || File.basename(path)}", path: resolved_path) end @checking_paths << resolved_path imported_modules = imported_modules_for_ast_collecting_errors(ast, importer_path: resolved_path).modules result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules:, path: resolved_path) analysis = result[:analysis] raise(result[:errors].first || ModuleLoadError.new("module analysis unavailable", path: resolved_path)) unless analysis @collecting_path_errors[resolved_path] = result[:errors] @collecting_analysis_cache[resolved_path] = analysis analysis ensure @checking_paths.pop end |
#check_program(path) ⇒ Object
168 169 170 171 172 173 |
# File 'lib/milk_tea/core/module_loader.rb', line 168 def check_program(path) with_check_context(path) do |root_path| check_program_parallel(root_path) build_program(root_path) end end |
#check_program_collecting(path) ⇒ Object
Variant of #check_program that collects errors instead of raising so diagnostic paths (check, debug, LSP) can report all issues at once. Returns { root_analysis: Analysis|nil, errors: [SemanticError], module_name: String|nil }.
178 179 180 181 182 183 184 185 |
# File 'lib/milk_tea/core/module_loader.rb', line 178 def check_program_collecting(path) with_check_context(path) do |root_path| errors = [] check_program_parallel(root_path, collecting_errors: errors) root_analysis = @analysis_cache[root_path] { root_analysis: root_analysis, errors: errors, module_name: root_analysis&.module_name } end end |
#check_program_parallel(root_path, collecting_errors: nil) ⇒ Object
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 |
# File 'lib/milk_tea/core/module_loader.rb', line 221 def check_program_parallel(root_path, collecting_errors: nil) # Phase 1: Parse all transitive modules (sequential) parse_all(root_path) # Phase 2: Build dependency graph from parsed ASTs graph = {} @parse_cache.each_key do |resolved_path| ast = @parse_cache[resolved_path] deps = ast.imports.map do |import| @path_resolver.resolve_module_path(import.path.to_s, importer_path: resolved_path, importer_module_name: ast.module_name.to_s) end graph[resolved_path] = deps end # Phase 3: Topological sort into independent levels. # Nodes that cannot be sorted are candidates for cycle membership, # but not all of them are actually part of a cycle — some are just # dependencies downstream from the cycle. We separate true cycle # members (nodes reachable from themselves) from tail nodes. levels = topo_sort_levels(graph) all_checked = levels.flatten.to_set unsorted = graph.keys.reject { |p| all_checked.include?(p) } # Among unsorted nodes, identify true cycle members: nodes that can # reach themselves through the graph (belong to a strongly connected # component of size > 1). Tail nodes that only depend on cycle members # but have no path back to themselves are NOT cycle members. cycle_members = unsorted.select { |node| node_in_cycle?(node, graph) } tail_members = unsorted - cycle_members # Pre-register forward bindings only for true cycle members cycle_members.each do |resolved_path| ast = @parse_cache[resolved_path] @forward_bindings[resolved_path] = create_forward_binding(ast) end # Phase 4: Check cycle members with iterative refinement # Pass 1: Check with forward bindings — registers type declarations. # SemanticError is expected (forward types lack fields/constructors) # but ModuleLoadError indicates a broken dependency graph. # Do NOT collect Pass 1 errors — they are temporary failures caused # by incomplete forward bindings that Pass 2 resolves. cycle_members.each do |resolved_path| check_path(resolved_path) rescue SemanticError # Forward types can't satisfy constructors / functions / methods. # Re-check in collecting mode to capture the analysis for population. capture_analysis_for_cycle_member(resolved_path) end # Update forward type objects in-place with field/arm info from the # real analyses. This way, all references to forward types (function # return types, struct field types, etc.) automatically see the full # type information without needing to replace the objects themselves. cycle_members.each do |resolved_path| analysis = @analysis_cache[resolved_path] next unless analysis fw_binding = @forward_bindings[resolved_path] next unless fw_binding analysis.types.each do |name, real_type| fw_type = fw_binding.types[name] next unless fw_type if real_type.respond_to?(:fields) && fw_type.respond_to?(:define_fields) fw_type.define_fields(real_type.fields) unless real_type.fields.empty? end if real_type.respond_to?(:arms) && fw_type.respond_to?(:define_arms) fw_type.define_arms(real_type.arms) unless real_type.arms.empty? end if real_type.respond_to?(:members) && fw_type.respond_to?(:define_members) member_names = real_type.members unless member_names.empty? fw_type.define_members(real_type.backing_type, member_names) values = member_names.each_with_object({}) { |n, h| h[n] = real_type.member_value(n) } fw_type.define_member_values(values) end end end end # Replace forward bindings with fully populated bindings so that # Pass 2 import resolution sees functions, methods, values, and # interfaces — not just type skeletons. populate_full_forward_bindings(cycle_members) # Pass 2: Re-check with populated bindings — now cycle members see # complete module information for each other. cycle_members.each do |resolved_path| previous = @analysis_cache.delete(resolved_path) check_path(resolved_path) rescue SemanticError => e @analysis_cache[resolved_path] = previous if previous collecting_errors << e if collecting_errors end @forward_bindings.clear # Phase 5: Check acyclic modules (they see fully-checked analyses for all imports). # Also check tail members — nodes that were in the unsorted set but # are not themselves part of a cycle; they just depended on cycle members. (levels + [tail_members]).each do |level_paths| if level_paths.length == 1 check_path(level_paths.first) elsif level_paths.any? check_level_parallel(level_paths) end rescue SemanticError => e raise unless collecting_errors collecting_errors << e end end |
#collecting_path_errors ⇒ Object
Errors collected per analyzed import path during collecting-mode checks.
Populated by #check_path_collecting_errors; used by the CLI check command
to surface errors in a single file's imported modules (otherwise only
reported when that module is checked directly).
478 479 480 |
# File 'lib/milk_tea/core/module_loader.rb', line 478 def collecting_path_errors @collecting_path_errors end |
#create_forward_binding(ast) ⇒ Object
677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 |
# File 'lib/milk_tea/core/module_loader.rb', line 677 def create_forward_binding(ast) module_name = ast.module_name.to_s types = {} ast.declarations.each do |decl| case decl when AST::StructDecl types[decl.name] = decl.type_params.empty? ? Types::Struct.new(decl.name, module_name:) : Types::GenericStructDefinition.new(decl.name, decl.type_params.map(&:name)) when AST::VariantDecl types[decl.name] = decl.type_params.empty? ? Types::Variant.new(decl.name, module_name:) : Types::GenericVariantDefinition.new(decl.name, decl.type_params.map(&:name)) when AST::EnumDecl types[decl.name] = Types::Enum.new(decl.name, module_name:) when AST::FlagsDecl types[decl.name] = Types::Flags.new(decl.name, module_name:) when AST::OpaqueDecl types[decl.name] = Types::Opaque.new(decl.name, module_name:, external: false) when AST::UnionDecl types[decl.name] = Types::Union.new(decl.name, module_name:) end end ModuleBinding.new( name: module_name, types:, type_declarations: {}, interfaces: {}, attributes: {}, attribute_applications: {}, values: {}, functions: {}, methods: {}, implemented_interfaces: {}, imports: {}, private_types: {}, private_interfaces: {}, private_attributes: {}, private_values: {}, private_functions: {}, private_methods: {}, private_implemented_interfaces: {}, ) end |
#dfs_reachable_from(current, target, graph, visited) ⇒ Object
213 214 215 216 217 218 219 |
# File 'lib/milk_tea/core/module_loader.rb', line 213 def dfs_reachable_from(current, target, graph, visited) return false if visited[current] return true if current == target visited[current] = true (graph[current] || []).any? { |neighbor| dfs_reachable_from(neighbor, target, graph, visited) } end |
#handle_circular_import_in_collecting_mode(import, import_path, modules, errors, error) ⇒ Object
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 |
# File 'lib/milk_tea/core/module_loader.rb', line 609 def handle_circular_import_in_collecting_mode(import, import_path, modules, errors, error) # When a circular import is detected in collecting mode, create a # forward binding for the target module so the importing module can # at minimum resolve type references. Without this the import is # entirely absent, which causes downstream crashes in module_binding. if import_path && error.is_a?(ModuleLoadError) && error..start_with?("circular import") circular_ast = @parse_cache[import_path] || load_file(import_path) if circular_ast @forward_bindings[import_path] ||= create_forward_binding(circular_ast) modules[import.path.to_s] = @forward_bindings[import_path] end end errors << ImportResolutionError.new(import:, error:) end |
#imported_modules_for_ast(ast, importer_path: nil) ⇒ Object
392 393 394 |
# File 'lib/milk_tea/core/module_loader.rb', line 392 def imported_modules_for_ast(ast, importer_path: nil) resolve_imports_for_ast(ast, importer_path:, collecting: false) end |
#imported_modules_for_ast_collecting_errors(ast, importer_path: nil) ⇒ Object
396 397 398 |
# File 'lib/milk_tea/core/module_loader.rb', line 396 def imported_modules_for_ast_collecting_errors(ast, importer_path: nil) resolve_imports_for_ast(ast, importer_path:, collecting: true) end |
#inferred_module_name_for_path(path) ⇒ Object
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 |
# File 'lib/milk_tea/core/module_loader.rb', line 577 def inferred_module_name_for_path(path) manifest = begin PackageManifest.load(path) rescue PackageManifestError nil end if manifest && path_within_root?(path, manifest.source_root) return module_name_for_path(path, manifest.source_root) end matching_root = @module_roots .select { |root| path_within_root?(path, root) } .max_by(&:length) return module_name_for_path(path, matching_root) if matching_root File.basename(path).sub(/\.(linux|windows|wasm)\.mt\z/, ".mt").sub(/\.mt\z/, "") end |
#load_file(path) ⇒ Object
148 149 150 151 |
# File 'lib/milk_tea/core/module_loader.rb', line 148 def load_file(path) resolved_path = self.class.resolve_source_path(path, platform: @platform, error_class: ModuleLoadError) @ast_cache[resolved_path] ||= parse_file(resolved_path) end |
#module_name_for_path(path, root) ⇒ Object
596 597 598 599 600 601 |
# File 'lib/milk_tea/core/module_loader.rb', line 596 def module_name_for_path(path, root) relative_path = path.delete_prefix(File.(root) + File::SEPARATOR) relative_path = File.basename(path) if relative_path == path relative_path = relative_path.sub(/\.(linux|windows|wasm)\.mt\z/, '.mt') relative_path.sub(/\.mt\z/, '').split(File::SEPARATOR).join('.') end |
#node_in_cycle?(node, graph) ⇒ Boolean
203 204 205 206 207 208 209 210 211 |
# File 'lib/milk_tea/core/module_loader.rb', line 203 def node_in_cycle?(node, graph) # Start from each immediate successor to avoid the trivial self-path successors = graph[node] || [] successors.each do |next_node| next unless graph.key?(next_node) return true if dfs_reachable_from(next_node, node, graph, {}) end false end |
#normalize_source_overrides(source_overrides) ⇒ Object
569 570 571 572 573 574 575 |
# File 'lib/milk_tea/core/module_loader.rb', line 569 def normalize_source_overrides(source_overrides) return {} unless source_overrides source_overrides.each_with_object({}) do |(path, source), overrides| overrides[File.(path.to_s)] = source.to_s end end |
#parse_all(resolved_path) ⇒ Object
335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/milk_tea/core/module_loader.rb', line 335 def parse_all(resolved_path) return if @parse_cache.key?(resolved_path) @checking_paths << resolved_path ast = load_file(resolved_path) @parse_cache[resolved_path] = ast ast.imports.each do |import| import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path: resolved_path, importer_module_name: ast.module_name.to_s) parse_all(import_path) end ensure @checking_paths.pop end |
#parse_file(path) ⇒ Object
550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 |
# File 'lib/milk_tea/core/module_loader.rb', line 550 def parse_file(path) source = @source_overrides.fetch(path) { File.read(path) } ast = Parser.parse(source, path: path) inferred_module_name = inferred_module_name_for_path(path) AST::SourceFile.new( module_name: AST::QualifiedName.new(inferred_module_name.split(".")), module_kind: ast.module_kind, imports: ast.imports, directives: ast.directives, declarations: ast.declarations, line: ast.line, node_ids: ast.node_ids, ) rescue Errno::ENOENT raise ModuleLoadError.new("source file not found", path: path) rescue Errno::EISDIR raise ModuleLoadError.new("expected a source file, got a directory", path: path) end |
#path_within_root?(path, root) ⇒ Boolean
603 604 605 606 607 |
# File 'lib/milk_tea/core/module_loader.rb', line 603 def path_within_root?(path, root) normalized_path = File.(path) normalized_root = File.(root) normalized_path == normalized_root || normalized_path.start_with?(normalized_root + File::SEPARATOR) end |
#populate_full_forward_bindings(cycle_members) ⇒ Object
641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 |
# File 'lib/milk_tea/core/module_loader.rb', line 641 def populate_full_forward_bindings(cycle_members) cycle_members.each do |resolved_path| analysis = @analysis_cache[resolved_path] next unless analysis fw_binding = @forward_bindings[resolved_path] next unless fw_binding full_binding = @binder.module_binding(analysis) merged_types = fw_binding.types.merge( full_binding.types.reject { |name, _| fw_binding.types.key?(name) } ) @forward_bindings[resolved_path] = ModuleBinding.new( name: full_binding.name, types: merged_types, type_declarations: full_binding.type_declarations, interfaces: full_binding.interfaces, attributes: full_binding.attributes, attribute_applications: full_binding.attribute_applications, values: full_binding.values, functions: full_binding.functions, methods: full_binding.methods, implemented_interfaces: full_binding.implemented_interfaces, imports: full_binding.imports, private_types: full_binding.private_types, private_interfaces: full_binding.private_interfaces, private_attributes: full_binding.private_attributes, private_values: full_binding.private_values, private_functions: full_binding.private_functions, private_methods: full_binding.private_methods, private_implemented_interfaces: full_binding.private_implemented_interfaces, ) end end |
#resolve_imports_for_ast(ast, importer_path:, collecting: false) ⇒ Object
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 |
# File 'lib/milk_tea/core/module_loader.rb', line 400 def resolve_imports_for_ast(ast, importer_path:, collecting: false) modules = {} errors = [] if @import_resolve_depth @import_resolve_depth += 1 if @import_resolve_depth > 80 raise SemanticError.new("import resolution depth exceeded") end else @import_resolve_depth = 1 end ast.imports.each do |import| begin import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path:, importer_module_name: ast.module_name.to_s) if @forward_bindings.key?(import_path) modules[import.path.to_s] = @forward_bindings[import_path] else import_analysis = collecting ? check_path_collecting_errors(import_path) : check_path(import_path) modules[import.path.to_s] = @binder.module_binding(import_analysis) end rescue ModuleLoadError, PackageLockError, SemanticError => e raise unless collecting handle_circular_import_in_collecting_mode(import, import_path, modules, errors, e) end end begin @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: collecting) rescue ModuleLoadError, PackageLockError => e raise unless collecting errors << ImportResolutionError.new(import: nil, error: e) end begin @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: collecting) rescue ModuleLoadError, PackageLockError => e raise unless collecting errors << ImportResolutionError.new(import: nil, error: e) end return ImportResolution.new(modules: modules.freeze, errors: errors.freeze) if collecting modules.freeze ensure @import_resolve_depth -= 1 if @import_resolve_depth end |
#topo_sort_levels(graph) ⇒ Object
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/milk_tea/core/module_loader.rb', line 350 def topo_sort_levels(graph) in_degree = {} graph.each_key { |node| in_degree[node] = 0 } graph.each_value do |deps| deps.each { |dep| in_degree[dep] = (in_degree[dep] || 0) + 1 } end levels = [] remaining = graph.keys.to_set until remaining.empty? level = remaining.select { |node| (in_degree[node] || 0) == 0 } break if level.empty? levels << level level.each do |node| remaining.delete(node) (graph[node] || []).each { |dep| in_degree[dep] -= 1 } end end levels end |
#update_shared_cache(resolved_path, analysis) ⇒ Object
543 544 545 546 547 548 |
# File 'lib/milk_tea/core/module_loader.rb', line 543 def update_shared_cache(resolved_path, analysis) return unless use_shared_cache? mtime = File.mtime(resolved_path).to_f rescue nil @shared_cache[resolved_path] = { mtime:, analysis: } if mtime end |
#use_shared_cache? ⇒ Boolean
713 714 715 |
# File 'lib/milk_tea/core/module_loader.rb', line 713 def use_shared_cache? @shared_cache && @source_overrides.empty? end |
#with_check_context(path, &block) ⇒ Object
157 158 159 160 161 162 163 164 165 166 |
# File 'lib/milk_tea/core/module_loader.rb', line 157 def with_check_context(path, &block) Types::Registry.reset! requested_path = File.(path) previous_platform = @platform @platform ||= self.class.platform_suffix_for_path(requested_path) root_path = self.class.resolve_source_path(requested_path, platform: @platform, error_class: ModuleLoadError) block.call(root_path) ensure @platform = previous_platform end |