mt-lang 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/AUTHORS +3 -0
  4. data/Gemfile +19 -0
  5. data/Gemfile.lock +82 -0
  6. data/LICENSE +21 -0
  7. data/README.md +1208 -0
  8. data/Rakefile +332 -0
  9. data/bin/mtc +8 -0
  10. data/bin/profile-mtc-checks +133 -0
  11. data/bin/tracy-profiler +0 -0
  12. data/docs/build-guide.md +536 -0
  13. data/docs/index.html +2778 -0
  14. data/docs/language-design.md +1519 -0
  15. data/docs/language-manual.md +1534 -0
  16. data/lib/milk_tea/base.rb +49 -0
  17. data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
  18. data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
  19. data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
  20. data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
  21. data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
  22. data/lib/milk_tea/bindings/bindgen.rb +207 -0
  23. data/lib/milk_tea/bindings/cli.rb +121 -0
  24. data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
  25. data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
  26. data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
  27. data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
  28. data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
  29. data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
  30. data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
  31. data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
  32. data/lib/milk_tea/bindings/steamworks.rb +629 -0
  33. data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
  34. data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
  35. data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
  36. data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
  37. data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
  38. data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
  39. data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
  40. data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
  41. data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
  42. data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
  43. data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
  44. data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
  45. data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
  46. data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
  47. data/lib/milk_tea/bindings.rb +23 -0
  48. data/lib/milk_tea/core/ast.rb +311 -0
  49. data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
  50. data/lib/milk_tea/core/binding_types.rb +18 -0
  51. data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
  52. data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
  53. data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
  54. data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
  55. data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
  56. data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
  57. data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
  58. data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
  59. data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
  60. data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
  61. data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
  62. data/lib/milk_tea/core/c_backend/statements.rb +563 -0
  63. data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
  64. data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
  65. data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
  66. data/lib/milk_tea/core/c_backend.rb +287 -0
  67. data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
  68. data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
  69. data/lib/milk_tea/core/compile_time.rb +329 -0
  70. data/lib/milk_tea/core/control_flow/builder.rb +582 -0
  71. data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
  72. data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
  73. data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
  74. data/lib/milk_tea/core/control_flow/graph.rb +90 -0
  75. data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
  76. data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
  77. data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
  78. data/lib/milk_tea/core/control_flow/termination.rb +31 -0
  79. data/lib/milk_tea/core/control_flow.rb +34 -0
  80. data/lib/milk_tea/core/cst.rb +48 -0
  81. data/lib/milk_tea/core/cst_builder.rb +19 -0
  82. data/lib/milk_tea/core/ir.rb +85 -0
  83. data/lib/milk_tea/core/keywords.rb +97 -0
  84. data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
  85. data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
  86. data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
  87. data/lib/milk_tea/core/lexer/indentation.rb +62 -0
  88. data/lib/milk_tea/core/lexer/numbers.rb +96 -0
  89. data/lib/milk_tea/core/lexer/recovery.rb +32 -0
  90. data/lib/milk_tea/core/lexer/strings.rb +167 -0
  91. data/lib/milk_tea/core/lexer/symbols.rb +71 -0
  92. data/lib/milk_tea/core/lexer/trivia.rb +53 -0
  93. data/lib/milk_tea/core/lexer.rb +430 -0
  94. data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
  95. data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
  96. data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
  97. data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
  98. data/lib/milk_tea/core/lowering/async.rb +714 -0
  99. data/lib/milk_tea/core/lowering/block.rb +1052 -0
  100. data/lib/milk_tea/core/lowering/calls.rb +1565 -0
  101. data/lib/milk_tea/core/lowering/declarations.rb +214 -0
  102. data/lib/milk_tea/core/lowering/dyn.rb +206 -0
  103. data/lib/milk_tea/core/lowering/events.rb +1054 -0
  104. data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
  105. data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
  106. data/lib/milk_tea/core/lowering/functions.rb +242 -0
  107. data/lib/milk_tea/core/lowering/loops.rb +1087 -0
  108. data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
  109. data/lib/milk_tea/core/lowering/proc.rb +419 -0
  110. data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
  111. data/lib/milk_tea/core/lowering/scans.rb +220 -0
  112. data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
  113. data/lib/milk_tea/core/lowering/utils.rb +1453 -0
  114. data/lib/milk_tea/core/lowering.rb +378 -0
  115. data/lib/milk_tea/core/module_binder.rb +181 -0
  116. data/lib/milk_tea/core/module_loader/errors.rb +21 -0
  117. data/lib/milk_tea/core/module_loader.rb +479 -0
  118. data/lib/milk_tea/core/module_path_resolver.rb +153 -0
  119. data/lib/milk_tea/core/module_roots.rb +88 -0
  120. data/lib/milk_tea/core/parser/attributes.rb +71 -0
  121. data/lib/milk_tea/core/parser/blocks.rb +119 -0
  122. data/lib/milk_tea/core/parser/declarations.rb +749 -0
  123. data/lib/milk_tea/core/parser/expressions.rb +624 -0
  124. data/lib/milk_tea/core/parser/recovery.rb +131 -0
  125. data/lib/milk_tea/core/parser/statements.rb +756 -0
  126. data/lib/milk_tea/core/parser/types.rb +271 -0
  127. data/lib/milk_tea/core/parser.rb +400 -0
  128. data/lib/milk_tea/core/prelude_installer.rb +29 -0
  129. data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
  130. data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
  131. data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
  132. data/lib/milk_tea/core/pretty_printer.rb +17 -0
  133. data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
  134. data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
  135. data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
  136. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
  137. data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
  138. data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
  139. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
  140. data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
  141. data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
  142. data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
  143. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
  144. data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
  145. data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
  146. data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
  147. data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
  148. data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
  149. data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
  150. data/lib/milk_tea/core/token.rb +26 -0
  151. data/lib/milk_tea/core/token_stream.rb +30 -0
  152. data/lib/milk_tea/core/types/layout.rb +243 -0
  153. data/lib/milk_tea/core/types/predicates.rb +609 -0
  154. data/lib/milk_tea/core/types/registry.rb +83 -0
  155. data/lib/milk_tea/core/types/types.rb +1696 -0
  156. data/lib/milk_tea/core/types/visitor.rb +442 -0
  157. data/lib/milk_tea/core.rb +25 -0
  158. data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
  159. data/lib/milk_tea/dap/protocol.rb +58 -0
  160. data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
  161. data/lib/milk_tea/dap/server/debug_map.rb +291 -0
  162. data/lib/milk_tea/dap/server/handlers.rb +374 -0
  163. data/lib/milk_tea/dap/server/launch.rb +261 -0
  164. data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
  165. data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
  166. data/lib/milk_tea/dap/server/utilities.rb +160 -0
  167. data/lib/milk_tea/dap/server/wire.rb +55 -0
  168. data/lib/milk_tea/dap/server.rb +131 -0
  169. data/lib/milk_tea/dap/session.rb +152 -0
  170. data/lib/milk_tea/dap.rb +6 -0
  171. data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
  172. data/lib/milk_tea/lsp/diagnostics.rb +611 -0
  173. data/lib/milk_tea/lsp/protocol.rb +104 -0
  174. data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
  175. data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
  176. data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
  177. data/lib/milk_tea/lsp/server/completion.rb +1099 -0
  178. data/lib/milk_tea/lsp/server/configuration.rb +167 -0
  179. data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
  180. data/lib/milk_tea/lsp/server/definition.rb +779 -0
  181. data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
  182. data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
  183. data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
  184. data/lib/milk_tea/lsp/server/formatting.rb +575 -0
  185. data/lib/milk_tea/lsp/server/hover.rb +1465 -0
  186. data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
  187. data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
  188. data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
  189. data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
  190. data/lib/milk_tea/lsp/server/progress.rb +47 -0
  191. data/lib/milk_tea/lsp/server/references.rb +433 -0
  192. data/lib/milk_tea/lsp/server/rename.rb +598 -0
  193. data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
  194. data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
  195. data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
  196. data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
  197. data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
  198. data/lib/milk_tea/lsp/server/utilities.rb +520 -0
  199. data/lib/milk_tea/lsp/server.rb +415 -0
  200. data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
  201. data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
  202. data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
  203. data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
  204. data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
  205. data/lib/milk_tea/lsp/workspace/store.rb +154 -0
  206. data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
  207. data/lib/milk_tea/lsp/workspace.rb +127 -0
  208. data/lib/milk_tea/lsp.rb +13 -0
  209. data/lib/milk_tea/packages/atomic_write.rb +63 -0
  210. data/lib/milk_tea/packages/dependency_solver.rb +194 -0
  211. data/lib/milk_tea/packages/graph.rb +139 -0
  212. data/lib/milk_tea/packages/lock.rb +421 -0
  213. data/lib/milk_tea/packages/manager_cli.rb +485 -0
  214. data/lib/milk_tea/packages/manifest.rb +356 -0
  215. data/lib/milk_tea/packages/manifest_editor.rb +134 -0
  216. data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
  217. data/lib/milk_tea/packages/registry_store.rb +420 -0
  218. data/lib/milk_tea/packages/services.rb +41 -0
  219. data/lib/milk_tea/packages/source_cache.rb +71 -0
  220. data/lib/milk_tea/packages/source_fetcher.rb +124 -0
  221. data/lib/milk_tea/packages/source_resolver.rb +389 -0
  222. data/lib/milk_tea/packages/version.rb +164 -0
  223. data/lib/milk_tea/packages.rb +16 -0
  224. data/lib/milk_tea/tooling/asset_pack.rb +141 -0
  225. data/lib/milk_tea/tooling/build.rb +1124 -0
  226. data/lib/milk_tea/tooling/build_cache.rb +240 -0
  227. data/lib/milk_tea/tooling/cli.rb +2688 -0
  228. data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
  229. data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
  230. data/lib/milk_tea/tooling/debug_map.rb +248 -0
  231. data/lib/milk_tea/tooling/docs_app.rb +369 -0
  232. data/lib/milk_tea/tooling/error_formatter.rb +86 -0
  233. data/lib/milk_tea/tooling/formatter.rb +787 -0
  234. data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
  235. data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
  236. data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
  237. data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
  238. data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
  239. data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
  240. data/lib/milk_tea/tooling/linter/rules.rb +593 -0
  241. data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
  242. data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
  243. data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
  244. data/lib/milk_tea/tooling/linter.rb +798 -0
  245. data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
  246. data/lib/milk_tea/tooling/public/css/docs.css +166 -0
  247. data/lib/milk_tea/tooling/public/js/docs.js +94 -0
  248. data/lib/milk_tea/tooling/run.rb +408 -0
  249. data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
  250. data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
  251. data/lib/milk_tea/tooling/views/404.erb +7 -0
  252. data/lib/milk_tea/tooling/views/index.erb +87 -0
  253. data/lib/milk_tea/tooling/views/layout.erb +69 -0
  254. data/lib/milk_tea/tooling/views/module.erb +97 -0
  255. data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
  256. data/lib/milk_tea/tooling.rb +20 -0
  257. data/lib/milk_tea.rb +8 -0
  258. metadata +426 -0
@@ -0,0 +1,479 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "module_loader/errors"
4
+ require_relative "module_path_resolver"
5
+ require_relative "module_binder"
6
+ require_relative "async_runtime_installer"
7
+ require_relative "prelude_installer"
8
+
9
+ module MilkTea
10
+ class ModuleLoader
11
+ Program = Data.define(:root_path, :root_analysis, :analyses_by_path, :analyses_by_module_name)
12
+ PLATFORM_SUFFIXES = {
13
+ "linux" => :linux,
14
+ "windows" => :windows,
15
+ "wasm" => :wasm,
16
+ }.freeze
17
+
18
+ def self.load_file(path, platform: nil)
19
+ new(platform:).load_file(path)
20
+ end
21
+
22
+ def self.check_file(path, platform: nil)
23
+ new(platform:).check_file(path)
24
+ end
25
+
26
+ def self.check_program(path, platform: nil)
27
+ new(platform:).check_program(path)
28
+ end
29
+
30
+ def self.normalize_platform_name(value)
31
+ return nil if value.nil? || value.to_s.strip.empty?
32
+
33
+ case value.to_s.strip.downcase
34
+ when "linux"
35
+ :linux
36
+ when "windows", "win", "win32"
37
+ :windows
38
+ when "wasm", "web", "html5", "browser"
39
+ :wasm
40
+ when "darwin", "macos", "osx"
41
+ :darwin
42
+ else
43
+ raise ArgumentError, "unknown platform #{value}; expected linux|windows|wasm|darwin"
44
+ end
45
+ end
46
+
47
+ def self.platform_suffix_for_path(path)
48
+ match = File.basename(path.to_s).match(/\.(linux|windows|wasm)\.mt\z/)
49
+ return nil unless match
50
+
51
+ PLATFORM_SUFFIXES.fetch(match[1])
52
+ end
53
+
54
+ def self.effective_platform_for_path(path, platform_override: nil, host_platform: nil)
55
+ normalized_override = normalize_platform_name(platform_override)
56
+ return normalized_override if normalized_override
57
+
58
+ suffix_platform = platform_suffix_for_path(path)
59
+ return suffix_platform if suffix_platform
60
+
61
+ manifest_platform = PackageManifest.load(path).platform
62
+ return manifest_platform if manifest_platform
63
+
64
+ normalize_platform_name(host_platform || default_host_platform)
65
+ rescue PackageManifestError
66
+ normalize_platform_name(host_platform || default_host_platform)
67
+ end
68
+
69
+ def self.resolve_source_path(path, platform: nil, error_class: nil)
70
+ expanded_path = File.expand_path(path.to_s)
71
+ normalized_platform = platform.nil? ? nil : normalize_platform_name(platform)
72
+ pinned_platform = platform_suffix_for_path(expanded_path)
73
+
74
+ if pinned_platform
75
+ if normalized_platform && normalized_platform != pinned_platform
76
+ raise_platform_conflict!(expanded_path, pinned_platform, normalized_platform, error_class:)
77
+ end
78
+ return expanded_path
79
+ end
80
+
81
+ return expanded_path unless normalized_platform && expanded_path.end_with?(".mt")
82
+
83
+ variant_path = expanded_path.sub(/\.mt\z/, ".#{normalized_platform}.mt")
84
+ return variant_path if File.file?(variant_path)
85
+
86
+ expanded_path
87
+ end
88
+
89
+ def self.default_host_platform
90
+ MilkTea.host_platform
91
+ end
92
+
93
+ def initialize(module_roots: [MilkTea.root], package_graph: nil, shared_cache: nil, source_overrides: nil, platform: nil)
94
+ @module_roots = module_roots.map { |root| File.expand_path(root.to_s) }
95
+ @ast_cache = {}
96
+ @parse_cache = {}
97
+ @analysis_cache = {}
98
+ @collecting_analysis_cache = {}
99
+ @collecting_path_errors = {}
100
+ @checking_paths = []
101
+ @platform = self.class.normalize_platform_name(platform)
102
+ @package_graph = package_graph
103
+ @package_manifest_cache = {}
104
+ @shared_cache = shared_cache # Hash or nil; mutated in-place to persist across calls
105
+ @source_overrides = normalize_source_overrides(source_overrides)
106
+
107
+ @path_resolver = ModulePathResolver.new(
108
+ module_roots: @module_roots,
109
+ platform: @platform,
110
+ package_graph: @package_graph,
111
+ source_overrides: @source_overrides,
112
+ package_manifest_cache: @package_manifest_cache,
113
+ )
114
+ @binder = ModuleBinder.new
115
+ @async_runtime_installer = AsyncRuntimeInstaller.new(
116
+ resolve_module_path: @path_resolver.method(:resolve_module_path),
117
+ check_block: ->(path, collecting) { collecting ? check_path_collecting_errors(path) : check_path(path) },
118
+ bind_block: @binder.method(:module_binding),
119
+ )
120
+ @prelude_installer = PreludeInstaller.new(
121
+ resolve_module_path: @path_resolver.method(:resolve_module_path),
122
+ check_block: ->(path, collecting) { collecting ? check_path_collecting_errors(path) : check_path(path) },
123
+ bind_block: @binder.method(:module_binding),
124
+ )
125
+ end
126
+
127
+ def load_file(path)
128
+ resolved_path = self.class.resolve_source_path(path, platform: @platform, error_class: ModuleLoadError)
129
+ @ast_cache[resolved_path] ||= parse_file(resolved_path)
130
+ end
131
+
132
+ def check_file(path)
133
+ check_program(path).root_analysis
134
+ end
135
+
136
+ def check_program(path)
137
+ Types::Registry.reset!
138
+ requested_path = File.expand_path(path)
139
+ previous_platform = @platform
140
+ @platform ||= self.class.platform_suffix_for_path(requested_path)
141
+ root_path = self.class.resolve_source_path(requested_path, platform: @platform, error_class: ModuleLoadError)
142
+
143
+ check_program_parallel(root_path)
144
+
145
+ root_analysis = @analysis_cache.fetch(root_path)
146
+ analyses_by_module_name = @analysis_cache.each_value.each_with_object({}) do |analysis, modules|
147
+ next unless analysis.module_name
148
+
149
+ modules[analysis.module_name] = analysis
150
+ end
151
+
152
+ Program.new(
153
+ root_path:,
154
+ root_analysis:,
155
+ analyses_by_path: @analysis_cache.dup.freeze,
156
+ analyses_by_module_name: analyses_by_module_name.freeze,
157
+ )
158
+ ensure
159
+ @platform = previous_platform
160
+ end
161
+
162
+ def check_program_parallel(root_path)
163
+ # Phase 1: Parse all transitive modules (sequential)
164
+ parse_all(root_path)
165
+
166
+ # Phase 2: Build dependency graph from parsed ASTs
167
+ graph = {}
168
+ @parse_cache.each_key do |resolved_path|
169
+ ast = @parse_cache[resolved_path]
170
+ deps = ast.imports.map do |import|
171
+ @path_resolver.resolve_module_path(import.path.to_s, importer_path: resolved_path, importer_module_name: ast.module_name.to_s)
172
+ end
173
+ graph[resolved_path] = deps
174
+ end
175
+
176
+ # Phase 3: Topological sort into independent levels
177
+ levels = topo_sort_levels(graph)
178
+
179
+ # Phase 4: Check each level (parallel within level, sequential across levels)
180
+ levels.each do |level_paths|
181
+ if level_paths.length == 1
182
+ check_path(level_paths.first)
183
+ else
184
+ check_level_parallel(level_paths)
185
+ end
186
+ end
187
+ end
188
+
189
+ def parse_all(resolved_path)
190
+ return if @parse_cache.key?(resolved_path)
191
+
192
+ if @checking_paths.include?(resolved_path)
193
+ raise ModuleLoadError.new("cyclic import detected", path: resolved_path)
194
+ end
195
+
196
+ @checking_paths << resolved_path
197
+ ast = load_file(resolved_path)
198
+ @parse_cache[resolved_path] = ast
199
+
200
+ ast.imports.each do |import|
201
+ import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path: resolved_path, importer_module_name: ast.module_name.to_s)
202
+ parse_all(import_path)
203
+ end
204
+ ensure
205
+ @checking_paths.pop if @checking_paths.last == resolved_path
206
+ end
207
+
208
+ def topo_sort_levels(graph)
209
+ in_degree = {}
210
+ graph.each_key { |node| in_degree[node] = 0 }
211
+ graph.each_value do |deps|
212
+ deps.each { |dep| in_degree[dep] = (in_degree[dep] || 0) + 1 }
213
+ end
214
+
215
+ levels = []
216
+ remaining = graph.keys.to_set
217
+ until remaining.empty?
218
+ level = remaining.select { |node| (in_degree[node] || 0) == 0 }
219
+ break if level.empty?
220
+
221
+ levels << level
222
+ level.each do |node|
223
+ remaining.delete(node)
224
+ (graph[node] || []).each { |dep| in_degree[dep] -= 1 }
225
+ end
226
+ end
227
+ levels
228
+ end
229
+
230
+ def check_level_parallel(paths)
231
+ threads = paths.map do |resolved_path|
232
+ Thread.new do
233
+ Thread.current[:resolved_path] = resolved_path
234
+ begin
235
+ analysis = check_path(resolved_path)
236
+ Thread.current[:analysis] = analysis
237
+ rescue ModuleLoadError, PackageLockError, SemanticError => e
238
+ Thread.current[:error] = e
239
+ end
240
+ end
241
+ end
242
+
243
+ threads.each(&:join)
244
+
245
+ paths.zip(threads).each do |resolved_path, t|
246
+ raise t[:error] if t[:error]
247
+ end
248
+ end
249
+
250
+ def imported_modules_for_ast(ast, importer_path: nil)
251
+ modules = {}
252
+
253
+ ast.imports.each do |import|
254
+ import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path:, importer_module_name: ast.module_name.to_s)
255
+ import_analysis = check_path(import_path)
256
+ modules[import.path.to_s] = @binder.module_binding(import_analysis)
257
+ end
258
+
259
+ @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: false)
260
+ @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: false)
261
+ modules.freeze
262
+ end
263
+
264
+ def build_global_import_index(ast)
265
+ index = {}
266
+ current_imports = ast.imports.map { |import| import.path.to_s }.to_set
267
+
268
+ @analysis_cache.each_value do |analysis|
269
+ next unless analysis
270
+ next unless analysis.module_name
271
+
272
+ mod_name = analysis.module_name.to_s
273
+ next if current_imports.include?(mod_name)
274
+ next if mod_name == ast.module_name.to_s
275
+
276
+ types = analysis.respond_to?(:types) ? analysis.types : {}
277
+ types.each_key do |type_name|
278
+ type_str = type_name.to_s
279
+ index[type_str] ||= []
280
+ index[type_str] << mod_name unless index[type_str].include?(mod_name)
281
+ end
282
+ end
283
+
284
+ index
285
+ end
286
+
287
+ def imported_modules_for_ast_collecting_errors(ast, importer_path: nil)
288
+ modules = {}
289
+ errors = []
290
+
291
+ ast.imports.each do |import|
292
+ begin
293
+ import_path = @path_resolver.resolve_module_path(import.path.to_s, importer_path:, importer_module_name: ast.module_name.to_s)
294
+ import_analysis = check_path_collecting_errors(import_path)
295
+ modules[import.path.to_s] = @binder.module_binding(import_analysis)
296
+ rescue ModuleLoadError, PackageLockError, SemanticError => e
297
+ errors << ImportResolutionError.new(import:, error: e)
298
+ end
299
+ end
300
+
301
+ begin
302
+ @async_runtime_installer.install_async_runtime_dependency!(ast, modules, importer_path:, collecting_errors: true)
303
+ rescue ModuleLoadError, PackageLockError => e
304
+ errors << ImportResolutionError.new(import: nil, error: e)
305
+ end
306
+
307
+ begin
308
+ @prelude_installer.install_prelude_modules!(ast, modules, importer_path:, collecting_errors: true)
309
+ rescue ModuleLoadError, PackageLockError => e
310
+ errors << ImportResolutionError.new(import: nil, error: e)
311
+ end
312
+
313
+ ImportResolution.new(modules: modules.freeze, errors: errors.freeze)
314
+ end
315
+
316
+ # Errors collected per analyzed import path during collecting-mode checks.
317
+ # Populated by #check_path_collecting_errors; used by the CLI `check` command
318
+ # to surface errors in a single file's imported modules (otherwise only
319
+ # reported when that module is checked directly).
320
+ def collecting_path_errors
321
+ @collecting_path_errors
322
+ end
323
+
324
+ private
325
+
326
+ def self.raise_platform_conflict!(path, pinned_platform, active_platform, error_class: nil)
327
+ if error_class == ModuleLoadError
328
+ raise ModuleLoadError.new("source file targets platform #{pinned_platform}; active platform is #{active_platform}", path:)
329
+ end
330
+
331
+ message = "source file #{path} targets platform #{pinned_platform}; active platform is #{active_platform}"
332
+ raise(error_class || ArgumentError, message)
333
+ end
334
+ private_class_method :raise_platform_conflict!
335
+
336
+ def check_path(path)
337
+ resolved_path = self.class.resolve_source_path(path, platform: @platform, error_class: ModuleLoadError)
338
+ shared_cache_mtime = nil
339
+ shared_cache_mtime_checked = false
340
+
341
+ return @analysis_cache[resolved_path] if @analysis_cache.key?(resolved_path)
342
+
343
+ if use_shared_cache?
344
+ entry = @shared_cache[resolved_path]
345
+ if entry
346
+ shared_cache_mtime_checked = true
347
+ shared_cache_mtime = File.mtime(resolved_path).to_f rescue nil
348
+ if shared_cache_mtime && entry[:mtime] == shared_cache_mtime
349
+ @analysis_cache[resolved_path] = entry[:analysis]
350
+ return entry[:analysis]
351
+ end
352
+ end
353
+ end
354
+
355
+ if @checking_paths.include?(resolved_path)
356
+ raise ModuleLoadError.new("cyclic import detected", path: resolved_path)
357
+ end
358
+
359
+ @checking_paths << resolved_path
360
+ ast = load_file(resolved_path)
361
+ imported_modules = imported_modules_for_ast(ast, importer_path: resolved_path)
362
+
363
+ global_index = build_global_import_index(ast)
364
+ analysis = SemanticAnalyzer.check(ast, imported_modules:, path: resolved_path, global_import_index: global_index)
365
+ @analysis_cache[resolved_path] = analysis
366
+
367
+ if use_shared_cache?
368
+ mtime = if shared_cache_mtime_checked
369
+ shared_cache_mtime
370
+ else
371
+ File.mtime(resolved_path).to_f rescue nil
372
+ end
373
+ @shared_cache[resolved_path] = { mtime: mtime, analysis: analysis } if mtime
374
+ end
375
+
376
+ analysis
377
+ ensure
378
+ @checking_paths.pop if @checking_paths.last == resolved_path
379
+ end
380
+
381
+ def check_path_collecting_errors(path)
382
+ resolved_path = self.class.resolve_source_path(path, platform: @platform, error_class: ModuleLoadError)
383
+
384
+ return @analysis_cache[resolved_path] if @analysis_cache.key?(resolved_path)
385
+ return @collecting_analysis_cache[resolved_path] if @collecting_analysis_cache.key?(resolved_path)
386
+
387
+ if use_shared_cache?
388
+ entry = @shared_cache[resolved_path]
389
+ if entry
390
+ mtime = File.mtime(resolved_path).to_f rescue nil
391
+ if mtime && entry[:mtime] == mtime
392
+ @analysis_cache[resolved_path] = entry[:analysis]
393
+ return entry[:analysis]
394
+ end
395
+ end
396
+ end
397
+
398
+ if @checking_paths.include?(resolved_path)
399
+ raise ModuleLoadError.new("cyclic import detected", path: resolved_path)
400
+ end
401
+
402
+ @checking_paths << resolved_path
403
+ ast = load_file(resolved_path)
404
+ imported_modules = imported_modules_for_ast_collecting_errors(ast, importer_path: resolved_path).modules
405
+ result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules:, path: resolved_path)
406
+ analysis = result[:analysis]
407
+ raise(result[:errors].first || ModuleLoadError.new("module analysis unavailable", path: resolved_path)) unless analysis
408
+
409
+ @collecting_path_errors[resolved_path] = result[:errors]
410
+ @collecting_analysis_cache[resolved_path] = analysis
411
+ analysis
412
+ ensure
413
+ @checking_paths.pop if @checking_paths.last == resolved_path
414
+ end
415
+
416
+ def parse_file(path)
417
+ source = @source_overrides.fetch(path) { File.read(path) }
418
+ ast = Parser.parse(source, path: path)
419
+ inferred_module_name = inferred_module_name_for_path(path)
420
+ AST::SourceFile.new(
421
+ module_name: AST::QualifiedName.new(inferred_module_name.split(".")),
422
+ module_kind: ast.module_kind,
423
+ imports: ast.imports,
424
+ directives: ast.directives,
425
+ declarations: ast.declarations,
426
+ line: ast.line,
427
+ node_ids: ast.node_ids,
428
+ )
429
+ rescue Errno::ENOENT
430
+ raise ModuleLoadError.new("source file not found", path: path)
431
+ rescue Errno::EISDIR
432
+ raise ModuleLoadError.new("expected a source file, got a directory", path: path)
433
+ end
434
+
435
+ def normalize_source_overrides(source_overrides)
436
+ return {} unless source_overrides
437
+
438
+ source_overrides.each_with_object({}) do |(path, source), overrides|
439
+ overrides[File.expand_path(path.to_s)] = source.to_s
440
+ end
441
+ end
442
+
443
+ def inferred_module_name_for_path(path)
444
+ manifest = begin
445
+ PackageManifest.load(path)
446
+ rescue PackageManifestError
447
+ nil
448
+ end
449
+
450
+ if manifest && path_within_root?(path, manifest.source_root)
451
+ return module_name_for_path(path, manifest.source_root)
452
+ end
453
+
454
+ matching_root = @module_roots
455
+ .select { |root| path_within_root?(path, root) }
456
+ .max_by(&:length)
457
+ return module_name_for_path(path, matching_root) if matching_root
458
+
459
+ File.basename(path).sub(/\.(linux|windows|wasm)\.mt\z/, ".mt").sub(/\.mt\z/, "")
460
+ end
461
+
462
+ def module_name_for_path(path, root)
463
+ relative_path = path.delete_prefix(File.expand_path(root) + File::SEPARATOR)
464
+ relative_path = File.basename(path) if relative_path == path
465
+ relative_path = relative_path.sub(/\.(linux|windows|wasm)\.mt\z/, '.mt')
466
+ relative_path.sub(/\.mt\z/, '').split(File::SEPARATOR).join('.')
467
+ end
468
+
469
+ def path_within_root?(path, root)
470
+ normalized_path = File.expand_path(path)
471
+ normalized_root = File.expand_path(root)
472
+ normalized_path == normalized_root || normalized_path.start_with?(normalized_root + File::SEPARATOR)
473
+ end
474
+
475
+ def use_shared_cache?
476
+ @shared_cache && @source_overrides.empty?
477
+ end
478
+ end
479
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class ModulePathResolver
5
+ def initialize(module_roots:, platform:, package_graph: nil, source_overrides: nil, package_manifest_cache: nil)
6
+ @module_roots = module_roots.map { |root| File.expand_path(root.to_s) }
7
+ @platform = platform
8
+ @package_graph = package_graph
9
+ @source_overrides = source_overrides || {}
10
+ @package_manifest_cache = package_manifest_cache || {}
11
+ end
12
+
13
+ def resolve_module_path(module_name, importer_path: nil, importer_module_name: nil)
14
+ package_candidate = resolve_package_module_path(module_name, importer_path:)
15
+ return package_candidate if package_candidate
16
+
17
+ relative_path = File.join(*module_name.split(".")) + ".mt"
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)
21
+
22
+ allowed = import_allowed?(module_name, importer_path, path)
23
+ blocked ||= !allowed
24
+ allowed
25
+ end
26
+ raise ModuleLoadError.new("package dependency not declared", path: module_name) if blocked
27
+ unless candidate
28
+ message = namespace_hint_for_missing_module(module_name, importer_path:, importer_module_name:) || "module not found"
29
+ raise ModuleLoadError.new(message, path: module_name)
30
+ end
31
+
32
+ File.expand_path(candidate)
33
+ end
34
+
35
+ def source_path_available?(path)
36
+ resolved_path = File.expand_path(path.to_s)
37
+ @source_overrides.key?(resolved_path) || File.file?(resolved_path)
38
+ end
39
+
40
+ private
41
+
42
+ def resolve_package_module_path(module_name, importer_path: nil)
43
+ return nil unless @package_graph && importer_path
44
+
45
+ importer_package = @package_graph.package_for_path(importer_path)
46
+ return nil unless importer_package
47
+
48
+ relative_path = File.join(*module_name.split(".")) + ".mt"
49
+ candidates = []
50
+ if package_namespace_match?(module_name, importer_package.manifest.package_name)
51
+ candidates << [
52
+ importer_package.manifest.package_name,
53
+ File.join(importer_package.manifest.source_root, relative_path),
54
+ ]
55
+ end
56
+
57
+ importer_package.edges.each do |edge|
58
+ next unless edge.node && package_namespace_match?(module_name, edge.dependency.name)
59
+
60
+ candidates << [
61
+ edge.dependency.name,
62
+ File.join(edge.node.manifest.source_root, relative_path),
63
+ ]
64
+ end
65
+
66
+ return nil if candidates.empty?
67
+
68
+ best_namespace_length = candidates.map { |namespace, _path| namespace.length }.max
69
+ matching_candidates = candidates.select { |namespace, _path| namespace.length == best_namespace_length }
70
+ if matching_candidates.length > 1
71
+ raise ModuleLoadError.new("ambiguous package dependency import", path: module_name)
72
+ end
73
+
74
+ resolved_path = ModuleLoader.resolve_source_path(matching_candidates.first.last, platform: @platform)
75
+ raise ModuleLoadError.new("module not found", path: module_name) unless source_path_available?(resolved_path)
76
+
77
+ File.expand_path(resolved_path)
78
+ end
79
+
80
+ def import_allowed?(module_name, importer_path, candidate_path)
81
+ if @package_graph
82
+ return import_allowed_by_graph?(module_name, importer_path, candidate_path)
83
+ end
84
+
85
+ importer_manifest = package_manifest_for_path(importer_path)
86
+ return true unless importer_manifest
87
+
88
+ candidate_manifest = package_manifest_for_path(candidate_path)
89
+ return true unless candidate_manifest
90
+ return true if candidate_manifest.manifest_path == importer_manifest.manifest_path
91
+
92
+ dependency = importer_manifest.dependencies.find { |entry| entry.name == candidate_manifest.package_name }
93
+ return false unless dependency
94
+
95
+ package_namespace_match?(module_name, dependency.name)
96
+ end
97
+
98
+ def import_allowed_by_graph?(module_name, importer_path, candidate_path)
99
+ importer_package = @package_graph.package_for_path(importer_path)
100
+ return true unless importer_package
101
+
102
+ candidate_package = @package_graph.package_for_path(candidate_path)
103
+ return true unless candidate_package
104
+ return true if candidate_package.manifest.manifest_path == importer_package.manifest.manifest_path
105
+
106
+ dependency = importer_package.edges.find do |edge|
107
+ edge.node && edge.node.manifest.package_name == candidate_package.manifest.package_name
108
+ end
109
+ return false unless dependency
110
+
111
+ package_namespace_match?(module_name, dependency.dependency.name)
112
+ end
113
+
114
+ def package_manifest_for_path(path)
115
+ return nil unless path
116
+
117
+ package_root = ModuleRoots.package_root_for_path(path)
118
+ return nil unless package_root
119
+
120
+ manifest_path = File.join(package_root, "package.toml")
121
+ return @package_manifest_cache[manifest_path] if @package_manifest_cache.key?(manifest_path)
122
+
123
+ @package_manifest_cache[manifest_path] = PackageManifest.load(path)
124
+ rescue PackageManifestError
125
+ @package_manifest_cache[manifest_path] = nil if manifest_path
126
+ nil
127
+ end
128
+
129
+ def package_namespace_match?(module_name, package_name)
130
+ module_name == package_name || module_name.start_with?("#{package_name}.")
131
+ end
132
+
133
+ def namespace_hint_for_missing_module(module_name, importer_path:, importer_module_name:)
134
+ return nil unless importer_path && importer_module_name
135
+ return nil unless entry_module_namespace_like?(importer_path, importer_module_name)
136
+ return nil unless module_name.start_with?("#{importer_module_name}.")
137
+
138
+ sibling_import = module_name.delete_prefix("#{importer_module_name}.")
139
+ sibling_path = File.join(File.dirname(importer_path), *sibling_import.split(".")) + ".mt"
140
+ resolved_sibling_path = ModuleLoader.resolve_source_path(sibling_path, platform: @platform)
141
+ return nil unless source_path_available?(resolved_sibling_path)
142
+
143
+ namespaced_path = File.join(File.dirname(importer_path), importer_module_name, *sibling_import.split(".")) + ".mt"
144
+ "module not found; entry module '#{importer_module_name}' does not create an import namespace for sibling files. Import '#{sibling_import}' instead, or move the module to #{namespaced_path}"
145
+ end
146
+
147
+ def entry_module_namespace_like?(importer_path, importer_module_name)
148
+ return false if importer_module_name.include?(".")
149
+
150
+ File.basename(importer_path).match?(/\Amain(?:\.(linux|windows|wasm))?\.mt\z/)
151
+ end
152
+ end
153
+ end