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
Checks a level of independent modules on worker threads.
- #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
-
#source_mtime(path) ⇒ Object
Option-style source modification time: nil when the file's mtime cannot be read (e.g. the file was removed between resolution and stat), so shared-cache reads simply miss instead of raising.
- #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.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/milk_tea/core/module_loader.rb', line 124 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
45 46 47 |
# File 'lib/milk_tea/core/module_loader.rb', line 45 def self.check_file(path, platform: nil) new(platform:).check_file(path) end |
.check_program(path, platform: nil) ⇒ Object
49 50 51 |
# File 'lib/milk_tea/core/module_loader.rb', line 49 def self.check_program(path, platform: nil) new(platform:).check_program(path) end |
.default_host_platform ⇒ Object
111 112 113 |
# File 'lib/milk_tea/core/module_loader.rb', line 111 def self.default_host_platform MilkTea.host_platform end |
.effective_platform_for_path(path, platform_override: nil, host_platform: nil) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/milk_tea/core/module_loader.rb', line 77 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 = PackageManifest.load_option(path) manifest_platform = manifest&.platform return manifest_platform if manifest_platform normalize_platform_name(host_platform || default_host_platform) end |
.load_file(path, platform: nil) ⇒ Object
41 42 43 |
# File 'lib/milk_tea/core/module_loader.rb', line 41 def self.load_file(path, platform: nil) new(platform:).load_file(path) end |
.normalize_platform_name(value) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/milk_tea/core/module_loader.rb', line 53 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
70 71 72 73 74 75 |
# File 'lib/milk_tea/core/module_loader.rb', line 70 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
115 116 117 118 119 120 121 122 |
# File 'lib/milk_tea/core/module_loader.rb', line 115 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
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/milk_tea/core/module_loader.rb', line 91 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
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
# File 'lib/milk_tea/core/module_loader.rb', line 467 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
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/milk_tea/core/module_loader.rb', line 201 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
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 |
# File 'lib/milk_tea/core/module_loader.rb', line 646 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 ModuleLoadError, PackageLockError, SemanticError # Analysis capture is best-effort; named load/check failures leave the # cycle member to the pass-2 re-check instead. ensure @checking_paths.pop end |
#check_file(path) ⇒ Object
164 165 166 |
# File 'lib/milk_tea/core/module_loader.rb', line 164 def check_file(path) check_program(path).root_analysis end |
#check_level_parallel(paths) ⇒ Object
Checks a level of independent modules on worker threads. Each worker returns { ok: analysis } or { error: e } as an ordinary value; the join point collects the failed outcomes instead of re-raising them, so the caller decides whether to propagate or accumulate.
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 |
# File 'lib/milk_tea/core/module_loader.rb', line 391 def check_level_parallel(paths) workers = paths.map do |resolved_path| Thread.new do begin { ok: check_path(resolved_path) } rescue ModuleLoadError, PackageLockError, SemanticError => e { error: e } end end end workers.each(&:join) workers.map(&:value).filter_map { |result| result[:error] } end |
#check_module_cache(path, extra_cache: nil) ⇒ Object
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 |
# File 'lib/milk_tea/core/module_loader.rb', line 538 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 = source_mtime(resolved_path) 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
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 |
# File 'lib/milk_tea/core/module_loader.rb', line 498 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
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 |
# File 'lib/milk_tea/core/module_loader.rb', line 517 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
179 180 181 182 183 184 185 186 187 |
# File 'lib/milk_tea/core/module_loader.rb', line 179 def check_program(path) with_check_context(path) do |root_path| errors = [] check_program_parallel(root_path, collecting_errors: errors) raise errors.first if errors.any? 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 }.
192 193 194 195 196 197 198 199 |
# File 'lib/milk_tea/core/module_loader.rb', line 192 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
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 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/milk_tea/core/module_loader.rb', line 235 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 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 begin check_path(level_paths.first) rescue SemanticError => e collecting_errors << e end elsif level_paths.any? collecting_errors.concat(check_level_parallel(level_paths)) end 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).
494 495 496 |
# File 'lib/milk_tea/core/module_loader.rb', line 494 def collecting_path_errors @collecting_path_errors end |
#create_forward_binding(ast) ⇒ Object
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 |
# File 'lib/milk_tea/core/module_loader.rb', line 699 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 Bindings::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
227 228 229 230 231 232 233 |
# File 'lib/milk_tea/core/module_loader.rb', line 227 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
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 |
# File 'lib/milk_tea/core/module_loader.rb', line 630 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
407 408 409 410 411 412 |
# File 'lib/milk_tea/core/module_loader.rb', line 407 def imported_modules_for_ast(ast, importer_path: nil) result = resolve_imports_for_ast(ast, importer_path:, collecting: false) raise result.errors.first.error if result.errors.any? result.modules end |
#imported_modules_for_ast_collecting_errors(ast, importer_path: nil) ⇒ Object
414 415 416 |
# File 'lib/milk_tea/core/module_loader.rb', line 414 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
602 603 604 605 606 607 608 609 610 611 612 613 614 615 |
# File 'lib/milk_tea/core/module_loader.rb', line 602 def inferred_module_name_for_path(path) manifest = PackageManifest.load_option(path) 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
159 160 161 162 |
# File 'lib/milk_tea/core/module_loader.rb', line 159 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
617 618 619 620 621 622 |
# File 'lib/milk_tea/core/module_loader.rb', line 617 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
217 218 219 220 221 222 223 224 225 |
# File 'lib/milk_tea/core/module_loader.rb', line 217 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
594 595 596 597 598 599 600 |
# File 'lib/milk_tea/core/module_loader.rb', line 594 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
350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/milk_tea/core/module_loader.rb', line 350 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
575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 |
# File 'lib/milk_tea/core/module_loader.rb', line 575 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
624 625 626 627 628 |
# File 'lib/milk_tea/core/module_loader.rb', line 624 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
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 |
# File 'lib/milk_tea/core/module_loader.rb', line 663 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] = Bindings::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
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 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 |
# File 'lib/milk_tea/core/module_loader.rb', line 418 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 if collecting handle_circular_import_in_collecting_mode(import, import_path, modules, errors, e) else errors << ImportResolutionError.new(import:, error: e) end end end begin @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: collecting) rescue ModuleLoadError, PackageLockError => e 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 errors << ImportResolutionError.new(import: nil, error: e) end ImportResolution.new(modules: modules.freeze, errors: errors.freeze) ensure @import_resolve_depth -= 1 if @import_resolve_depth end |
#source_mtime(path) ⇒ Object
Option-style source modification time: nil when the file's mtime cannot be read (e.g. the file was removed between resolution and stat), so shared-cache reads simply miss instead of raising.
562 563 564 565 566 |
# File 'lib/milk_tea/core/module_loader.rb', line 562 def source_mtime(path) File.mtime(path).to_f rescue SystemCallError nil end |
#topo_sort_levels(graph) ⇒ Object
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/milk_tea/core/module_loader.rb', line 365 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
568 569 570 571 572 573 |
# File 'lib/milk_tea/core/module_loader.rb', line 568 def update_shared_cache(resolved_path, analysis) return unless use_shared_cache? mtime = source_mtime(resolved_path) @shared_cache[resolved_path] = { mtime:, analysis: } if mtime end |
#use_shared_cache? ⇒ Boolean
735 736 737 |
# File 'lib/milk_tea/core/module_loader.rb', line 735 def use_shared_cache? @shared_cache && @source_overrides.empty? end |
#with_check_context(path, &block) ⇒ Object
168 169 170 171 172 173 174 175 176 177 |
# File 'lib/milk_tea/core/module_loader.rb', line 168 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 |