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,389 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module MilkTea
6
+ class PackageSourceResolverError < StandardError; end
7
+
8
+ class PackageSourceResolver
9
+ attr_reader :resolved_registry_versions
10
+
11
+ RegistryDependencyKey = Data.define(:parent_source_key, :dependency_name)
12
+
13
+ class SourceIdentity
14
+ def kind
15
+ raise NotImplementedError, "#{self.class} must implement #kind"
16
+ end
17
+
18
+ def lock_attributes
19
+ raise NotImplementedError, "#{self.class} must implement #lock_attributes"
20
+ end
21
+
22
+ def cache_key
23
+ nil
24
+ end
25
+
26
+ def cacheable?
27
+ !cache_key.nil?
28
+ end
29
+ end
30
+
31
+ class PathIdentity < SourceIdentity
32
+ attr_reader :path
33
+
34
+ def initialize(path)
35
+ @path = File.expand_path(path)
36
+ end
37
+
38
+ def kind
39
+ :path
40
+ end
41
+
42
+ def lock_attributes
43
+ {
44
+ "source_path" => @path,
45
+ }
46
+ end
47
+ end
48
+
49
+ class RegistryIdentity < SourceIdentity
50
+ attr_reader :package_name, :version
51
+
52
+ def initialize(package_name:, version:)
53
+ @package_name = normalize_required(package_name, "registry package name")
54
+ @version = normalize_required(version, "registry package version")
55
+ end
56
+
57
+ def kind
58
+ :registry
59
+ end
60
+
61
+ def lock_attributes
62
+ {
63
+ "registry_package" => @package_name,
64
+ "registry_version" => @version,
65
+ }
66
+ end
67
+
68
+ def cache_key
69
+ "registry/#{@package_name}@#{@version}"
70
+ end
71
+
72
+ private
73
+
74
+ def normalize_required(value, label)
75
+ text = value.to_s.strip
76
+ raise PackageSourceResolverError, "#{label} cannot be empty" if text.empty?
77
+ if text.include?(File::SEPARATOR) || (File::ALT_SEPARATOR && text.include?(File::ALT_SEPARATOR))
78
+ raise PackageSourceResolverError, "#{label} cannot contain path separators"
79
+ end
80
+
81
+ text
82
+ end
83
+ end
84
+
85
+ class GitIdentity < SourceIdentity
86
+ attr_reader :url, :revision, :subdir
87
+
88
+ def initialize(url:, revision:, subdir: nil)
89
+ @url = normalize_required(url, "git source url")
90
+ @revision = normalize_required(revision, "git source revision")
91
+ @subdir = normalize_optional(subdir)
92
+ end
93
+
94
+ def kind
95
+ :git
96
+ end
97
+
98
+ def lock_attributes
99
+ attributes = {
100
+ "git_url" => @url,
101
+ "git_rev" => @revision,
102
+ }
103
+ attributes["git_subdir"] = @subdir if @subdir
104
+ attributes
105
+ end
106
+
107
+ def cache_key
108
+ digest = Digest::SHA256.hexdigest([@url, @revision, @subdir].join("\0"))
109
+ "git/#{digest}"
110
+ end
111
+
112
+ private
113
+
114
+ def normalize_required(value, label)
115
+ text = value.to_s.strip
116
+ raise PackageSourceResolverError, "#{label} cannot be empty" if text.empty?
117
+
118
+ text
119
+ end
120
+
121
+ def normalize_optional(value)
122
+ text = value.to_s.strip
123
+ return nil if text.empty?
124
+ raise PackageSourceResolverError, "git source subdir cannot be absolute" if text.start_with?("/", "\\")
125
+ raise PackageSourceResolverError, "git source subdir cannot be absolute" if text.match?(/\A[A-Za-z]:[\\\/]/)
126
+
127
+ segments = text.split(/[\\\/]+/).reject(&:empty?)
128
+ normalized_segments = []
129
+ segments.each do |segment|
130
+ next if segment == "."
131
+ raise PackageSourceResolverError, "git source subdir cannot escape the repository root" if segment == ".."
132
+
133
+ normalized_segments << segment
134
+ end
135
+
136
+ normalized = normalized_segments.join("/")
137
+ normalized.empty? ? nil : normalized
138
+ end
139
+ end
140
+
141
+ Source = Data.define(:identity, :local_root) do
142
+ def kind
143
+ identity.kind
144
+ end
145
+
146
+ def lock_attributes
147
+ identity.lock_attributes
148
+ end
149
+
150
+ def cache_key
151
+ identity.cache_key
152
+ end
153
+
154
+ def cacheable?
155
+ identity.cacheable?
156
+ end
157
+ end
158
+
159
+ ResolvedPackage = Data.define(:manifest, :source)
160
+
161
+ def self.source_key_for_identity(identity)
162
+ return "path:#{File.expand_path(identity.path)}" if identity.is_a?(PathIdentity)
163
+ return "#{identity.kind}:#{identity.cache_key}" if identity.cache_key
164
+
165
+ raise PackageSourceResolverError, "unsupported source identity #{identity.class} for registry dependency key"
166
+ end
167
+
168
+ def self.registry_dependency_key(parent_source_key:, dependency_name:)
169
+ RegistryDependencyKey.new(
170
+ parent_source_key: parent_source_key.to_s,
171
+ dependency_name: dependency_name.to_s,
172
+ )
173
+ end
174
+
175
+ def initialize(source_cache: PackageSourceCache.new, remote_resolution: :reject, source_fetcher: nil, resolved_registry_versions: nil)
176
+ @source_cache = source_cache
177
+ @remote_resolution = remote_resolution
178
+ @source_fetcher = source_fetcher
179
+ @resolved_registry_versions = normalize_resolved_registry_versions(resolved_registry_versions)
180
+ end
181
+
182
+ def supports_dependency_solving?
183
+ @remote_resolution != :reject
184
+ end
185
+
186
+ def with_resolved_registry_versions(resolved_registry_versions)
187
+ self.class.new(
188
+ source_cache: @source_cache,
189
+ remote_resolution: @remote_resolution,
190
+ source_fetcher: @source_fetcher,
191
+ resolved_registry_versions:,
192
+ )
193
+ end
194
+
195
+ def source_for_manifest(manifest)
196
+ Source.new(
197
+ identity: PathIdentity.new(manifest.root_dir),
198
+ local_root: manifest.root_dir,
199
+ )
200
+ end
201
+
202
+ def identity_from_lock(entry, lock_path)
203
+ source_kind = entry["source_kind"].to_s
204
+
205
+ case source_kind
206
+ when "path"
207
+ source_path = entry["source_path"].to_s
208
+ raise PackageSourceResolverError, "package.lock missing source_path in #{lock_path}" if source_path.empty?
209
+
210
+ PathIdentity.new(source_path)
211
+ when "registry"
212
+ RegistryIdentity.new(
213
+ package_name: entry["registry_package"],
214
+ version: entry["registry_version"],
215
+ )
216
+ when "git"
217
+ GitIdentity.new(
218
+ url: entry["git_url"],
219
+ revision: entry["git_rev"],
220
+ subdir: entry["git_subdir"],
221
+ )
222
+ else
223
+ raise PackageSourceResolverError,
224
+ "unsupported package source_kind #{source_kind.inspect} in #{lock_path}; supported kinds are path, registry, and git"
225
+ end
226
+ end
227
+
228
+ def resolve(dependency, parent_manifest:, parent_source: nil)
229
+ if dependency.path
230
+ manifest = PackageManifest.load(dependency.path)
231
+ validate_fixed_dependency_version_requirement!(dependency, manifest, parent_manifest:)
232
+ return ResolvedPackage.new(manifest:, source: source_for_manifest(manifest))
233
+ end
234
+
235
+ return resolve_registry_dependency(dependency, parent_manifest:, parent_source:) if dependency.registry?
236
+ return resolve_git_dependency(dependency, parent_manifest:) if dependency.git
237
+
238
+ raise PackageSourceResolverError,
239
+ "dependency #{dependency.name} in #{parent_manifest.manifest_path} has no supported source resolver"
240
+ end
241
+
242
+ def source_from_lock(entry, lock_path)
243
+ identity = identity_from_lock(entry, lock_path)
244
+ source_for_identity(identity, lock_path)
245
+ rescue PackageSourceCacheError => e
246
+ raise PackageSourceResolverError, e.message
247
+ end
248
+
249
+ private
250
+
251
+ def resolve_registry_dependency(dependency, parent_manifest:, parent_source: nil)
252
+ if dependency.exact_registry_version?
253
+ identity = RegistryIdentity.new(package_name: dependency.name, version: dependency.version)
254
+ return resolve_cache_backed_dependency(identity, dependency:, parent_manifest:)
255
+ end
256
+
257
+ parent_source_key = registry_dependency_parent_source_key(parent_manifest:, parent_source:)
258
+ dependency_key = self.class.registry_dependency_key(parent_source_key:, dependency_name: dependency.name)
259
+ resolved_version = @resolved_registry_versions[dependency_key] || @resolved_registry_versions[dependency.name]
260
+ if resolved_version
261
+ unless dependency.version_req.matches?(resolved_version)
262
+ raise PackageSourceResolverError,
263
+ "dependency #{dependency.name} in #{parent_manifest.manifest_path} resolved version #{resolved_version.inspect}, which does not satisfy #{dependency.version_req}"
264
+ end
265
+
266
+ identity = RegistryIdentity.new(package_name: dependency.name, version: resolved_version)
267
+ return resolve_cache_backed_dependency(identity, dependency:, parent_manifest:)
268
+ end
269
+
270
+ raise PackageSourceResolverError,
271
+ "dependency #{dependency.name} in #{parent_manifest.manifest_path} uses version requirement #{dependency.version_req}, but registry dependency solving is not implemented yet; use an exact version for now"
272
+ end
273
+
274
+ def resolve_git_dependency(dependency, parent_manifest:)
275
+ identity = GitIdentity.new(
276
+ url: dependency.git,
277
+ revision: dependency.git_rev,
278
+ subdir: dependency.git_subdir,
279
+ )
280
+
281
+ resolve_cache_backed_dependency(identity, dependency:, parent_manifest:)
282
+ end
283
+
284
+ def resolve_cache_backed_dependency(identity, dependency:, parent_manifest:)
285
+ context_label = "dependency #{dependency.name} in #{parent_manifest.manifest_path}"
286
+ source = case @remote_resolution
287
+ when :reject
288
+ raise PackageSourceResolverError,
289
+ "#{context_label} uses #{identity.kind} resolution, but live dependency resolution only supports local path dependencies; run mtc deps lock and then use --locked or --frozen"
290
+ when :cache
291
+ source_for_identity(identity, context_label)
292
+ when :materialize
293
+ unless @source_fetcher
294
+ raise PackageSourceResolverError,
295
+ "#{context_label} requested #{identity.kind} materialization without a package source fetcher"
296
+ end
297
+
298
+ @source_fetcher.materialize_identity(package_name: dependency.name, identity:)
299
+ source_for_identity(identity, context_label)
300
+ else
301
+ raise PackageSourceResolverError, "unknown remote resolution mode #{@remote_resolution.inspect}"
302
+ end
303
+
304
+ manifest = PackageManifest.load(source.local_root)
305
+ if manifest.package_name != dependency.name
306
+ raise PackageSourceResolverError,
307
+ "#{context_label} resolved package #{manifest.package_name} at #{manifest.manifest_path}"
308
+ end
309
+
310
+ if identity.is_a?(RegistryIdentity) && manifest.package_version != identity.version
311
+ raise PackageSourceResolverError,
312
+ "#{context_label} resolved version #{manifest.package_version.inspect} at #{manifest.manifest_path}; expected #{identity.version.inspect}"
313
+ end
314
+
315
+ ResolvedPackage.new(manifest:, source:)
316
+ rescue PackageManifestError => e
317
+ raise PackageSourceResolverError, e.message
318
+ end
319
+
320
+ def validate_fixed_dependency_version_requirement!(dependency, manifest, parent_manifest:)
321
+ return unless dependency.version_req
322
+
323
+ package_version = manifest.package_version
324
+ if package_version.nil?
325
+ raise PackageSourceResolverError,
326
+ "dependency #{dependency.name} in #{parent_manifest.manifest_path} requires version #{dependency.version_req}, but #{manifest.manifest_path} does not declare package.version"
327
+ end
328
+
329
+ return if dependency.version_req.matches?(package_version)
330
+
331
+ raise PackageSourceResolverError,
332
+ "dependency #{dependency.name} in #{parent_manifest.manifest_path} resolved version #{package_version.inspect} at #{manifest.manifest_path}, which does not satisfy #{dependency.version_req}"
333
+ end
334
+
335
+ def source_for_identity(identity, context_label)
336
+ local_root = if identity.is_a?(PathIdentity)
337
+ identity.path
338
+ else
339
+ manifest_path = @source_cache.manifest_path_for(identity)
340
+ unless File.file?(manifest_path)
341
+ raise PackageSourceResolverError,
342
+ "package source_kind #{identity.kind.inspect} in #{context_label} is not materialized in the source cache: expected #{manifest_path}"
343
+ end
344
+
345
+ @source_cache.materialized_root_for(identity)
346
+ end
347
+
348
+ Source.new(
349
+ identity:,
350
+ local_root:,
351
+ )
352
+ end
353
+
354
+ def registry_dependency_parent_source_key(parent_manifest:, parent_source:)
355
+ identity = if parent_source
356
+ parent_source.identity
357
+ else
358
+ source_for_manifest(parent_manifest).identity
359
+ end
360
+
361
+ self.class.source_key_for_identity(identity)
362
+ end
363
+
364
+ def normalize_resolved_registry_versions(versions)
365
+ return {} unless versions
366
+
367
+ versions.each_with_object({}) do |(key, version), resolved|
368
+ normalized_key = case key
369
+ when RegistryDependencyKey
370
+ self.class.registry_dependency_key(
371
+ parent_source_key: key.parent_source_key,
372
+ dependency_name: key.dependency_name,
373
+ )
374
+ when Array
375
+ if key.length != 2
376
+ raise PackageSourceResolverError,
377
+ "registry dependency key must contain parent manifest path and dependency name"
378
+ end
379
+
380
+ self.class.registry_dependency_key(parent_source_key: key[0], dependency_name: key[1])
381
+ else
382
+ key.to_s
383
+ end
384
+
385
+ resolved[normalized_key] = version.to_s
386
+ end
387
+ end
388
+ end
389
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class PackageVersionError < StandardError; end
5
+
6
+ class PackageVersion
7
+ include Comparable
8
+
9
+ VERSION_PATTERN = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\z/
10
+
11
+ attr_reader :major, :minor, :patch
12
+
13
+ def self.parse(value, label: "package version")
14
+ return value if value.is_a?(self)
15
+
16
+ text = value.to_s.strip
17
+ raise PackageVersionError, "#{label} cannot be empty" if text.empty?
18
+
19
+ match = VERSION_PATTERN.match(text)
20
+ unless match
21
+ raise PackageVersionError,
22
+ "#{label} #{text.inspect} must use semantic version format MAJOR.MINOR.PATCH"
23
+ end
24
+
25
+ new(match[1].to_i, match[2].to_i, match[3].to_i)
26
+ end
27
+
28
+ def initialize(major, minor, patch)
29
+ @major = Integer(major)
30
+ @minor = Integer(minor)
31
+ @patch = Integer(patch)
32
+ end
33
+
34
+ def <=>(other)
35
+ return nil unless other.is_a?(PackageVersion)
36
+
37
+ [major, minor, patch] <=> [other.major, other.minor, other.patch]
38
+ end
39
+
40
+ def to_s
41
+ "#{major}.#{minor}.#{patch}"
42
+ end
43
+ end
44
+
45
+ class PackageVersionReq
46
+ Constraint = Data.define(:operator, :version) do
47
+ def matches?(candidate)
48
+ case operator
49
+ when :eq
50
+ candidate == version
51
+ when :gt
52
+ candidate > version
53
+ when :gte
54
+ candidate >= version
55
+ when :lt
56
+ candidate < version
57
+ when :lte
58
+ candidate <= version
59
+ else
60
+ raise PackageVersionError, "unsupported package version operator #{operator.inspect}"
61
+ end
62
+ end
63
+
64
+ def to_s
65
+ prefix = case operator
66
+ when :eq then "="
67
+ when :gt then ">"
68
+ when :gte then ">="
69
+ when :lt then "<"
70
+ when :lte then "<="
71
+ else
72
+ raise PackageVersionError, "unsupported package version operator #{operator.inspect}"
73
+ end
74
+
75
+ "#{prefix}#{version}"
76
+ end
77
+ end
78
+
79
+ attr_reader :constraints
80
+
81
+ def self.parse(value, label: "package version requirement")
82
+ text = value.to_s.strip
83
+ raise PackageVersionError, "#{label} cannot be empty" if text.empty?
84
+
85
+ segments = text.split(",", -1).map(&:strip)
86
+ if segments.empty? || segments.any?(&:empty?)
87
+ raise PackageVersionError, "#{label} #{text.inspect} contains an empty constraint"
88
+ end
89
+
90
+ constraints = segments.flat_map do |segment|
91
+ parse_segment(segment, label:)
92
+ end
93
+
94
+ new(constraints)
95
+ end
96
+
97
+ def self.parse_segment(segment, label:)
98
+ case segment
99
+ when /\A\^(.+)\z/
100
+ version = PackageVersion.parse(Regexp.last_match(1), label:)
101
+ [Constraint.new(:gte, version), Constraint.new(:lt, caret_upper_bound(version))]
102
+ when /\A~(.+)\z/
103
+ version = PackageVersion.parse(Regexp.last_match(1), label:)
104
+ [Constraint.new(:gte, version), Constraint.new(:lt, tilde_upper_bound(version))]
105
+ when /\A(>=|<=|>|<|=)\s*(.+)\z/
106
+ version = PackageVersion.parse(Regexp.last_match(2), label:)
107
+ [Constraint.new(operator_for(Regexp.last_match(1)), version)]
108
+ else
109
+ [Constraint.new(:eq, PackageVersion.parse(segment, label:))]
110
+ end
111
+ end
112
+ private_class_method :parse_segment
113
+
114
+ def self.operator_for(token)
115
+ case token
116
+ when "=" then :eq
117
+ when ">" then :gt
118
+ when ">=" then :gte
119
+ when "<" then :lt
120
+ when "<=" then :lte
121
+ else
122
+ raise PackageVersionError, "unsupported package version operator #{token.inspect}"
123
+ end
124
+ end
125
+ private_class_method :operator_for
126
+
127
+ def self.caret_upper_bound(version)
128
+ if version.major.positive?
129
+ PackageVersion.new(version.major + 1, 0, 0)
130
+ elsif version.minor.positive?
131
+ PackageVersion.new(0, version.minor + 1, 0)
132
+ else
133
+ PackageVersion.new(0, 0, version.patch + 1)
134
+ end
135
+ end
136
+ private_class_method :caret_upper_bound
137
+
138
+ def self.tilde_upper_bound(version)
139
+ PackageVersion.new(version.major, version.minor + 1, 0)
140
+ end
141
+ private_class_method :tilde_upper_bound
142
+
143
+ def initialize(constraints)
144
+ @constraints = constraints.freeze
145
+ end
146
+
147
+ def exact?
148
+ constraints.length == 1 && constraints.first.operator == :eq
149
+ end
150
+
151
+ def exact_version
152
+ constraints.first.version if exact?
153
+ end
154
+
155
+ def matches?(version)
156
+ candidate = PackageVersion.parse(version)
157
+ constraints.all? { |constraint| constraint.matches?(candidate) }
158
+ end
159
+
160
+ def to_s
161
+ constraints.map(&:to_s).join(", ")
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "packages/version"
4
+ require_relative "packages/manifest"
5
+ require_relative "packages/source_cache"
6
+ require_relative "packages/registry_store"
7
+ require_relative "packages/source_resolver"
8
+ require_relative "packages/source_fetcher"
9
+ require_relative "packages/registry_metadata_provider"
10
+ require_relative "packages/dependency_solver"
11
+ require_relative "packages/graph"
12
+ require_relative "packages/atomic_write"
13
+ require_relative "packages/lock"
14
+ require_relative "packages/manifest_editor"
15
+ require_relative "packages/services"
16
+ require_relative "packages/manager_cli"
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tempfile"
5
+
6
+ module MilkTea
7
+ class AssetPackError < StandardError; end
8
+
9
+ class AssetPack
10
+ MAGIC = "MTAP".b.freeze
11
+ VERSION = 1
12
+ HEADER_FLAGS = 0
13
+ ENTRY_FLAGS_RAW = 0
14
+ HEADER_FORMAT = "a4vvVQ<Q<".freeze
15
+ ENTRY_PREFIX_FORMAT = "VVQ<Q<Q<".freeze
16
+ HEADER_SIZE = [MAGIC, VERSION, HEADER_FLAGS, 0, 0, 0].pack(HEADER_FORMAT).bytesize
17
+ ENTRY_PREFIX_SIZE = [0, ENTRY_FLAGS_RAW, 0, 0, 0].pack(ENTRY_PREFIX_FORMAT).bytesize
18
+
19
+ SourceEntry = Data.define(:source_path, :logical_path, :stored_size, :unpacked_size, :flags)
20
+ Entry = Data.define(:source_path, :logical_path, :data_offset, :stored_size, :unpacked_size, :flags)
21
+
22
+ def self.write(output_path, source_paths)
23
+ new(output_path, source_paths).write
24
+ end
25
+
26
+ def initialize(output_path, source_paths)
27
+ @output_path = File.expand_path(output_path)
28
+ @source_paths = source_paths.map { |source_path| File.expand_path(source_path) }
29
+ end
30
+
31
+ def write
32
+ ensure_output_does_not_overlap_sources!
33
+
34
+ source_entries = collect_source_entries
35
+ index_size = source_entries.sum { |entry| ENTRY_PREFIX_SIZE + entry.logical_path.bytesize }
36
+ data_offset = HEADER_SIZE + index_size
37
+ entries = assign_offsets(source_entries, data_offset)
38
+
39
+ FileUtils.mkdir_p(File.dirname(@output_path))
40
+ Tempfile.create(["milk-tea-asset-pack", ".bin"], File.dirname(@output_path)) do |file|
41
+ file.binmode
42
+ write_header(file, entries.length, index_size, data_offset)
43
+ write_index(file, entries)
44
+ write_data(file, entries)
45
+ file.flush
46
+ file.close
47
+
48
+ FileUtils.rm_f(@output_path)
49
+ FileUtils.mv(file.path, @output_path)
50
+ end
51
+
52
+ @output_path
53
+ end
54
+
55
+ private
56
+
57
+ def ensure_output_does_not_overlap_sources!
58
+ @source_paths.each do |source_path|
59
+ if File.directory?(source_path)
60
+ next unless path_within?(@output_path, source_path)
61
+
62
+ raise AssetPackError, "asset pack output would be written inside source tree: #{@output_path}"
63
+ end
64
+
65
+ next unless @output_path == source_path
66
+
67
+ raise AssetPackError, "asset pack output would overwrite source file: #{@output_path}"
68
+ end
69
+ end
70
+
71
+ def collect_source_entries
72
+ @source_paths.flat_map do |source_path|
73
+ collect_source_path(source_path, File.basename(source_path))
74
+ end.sort_by(&:logical_path)
75
+ end
76
+
77
+ def collect_source_path(source_path, logical_path)
78
+ stat = File.lstat(source_path)
79
+
80
+ if stat.directory?
81
+ Dir.children(source_path).sort.flat_map do |child|
82
+ collect_source_path(File.join(source_path, child), join_logical_path(logical_path, child))
83
+ end
84
+ elsif stat.file?
85
+ file_size = File.size(source_path)
86
+ [SourceEntry.new(source_path:, logical_path:, stored_size: file_size, unpacked_size: file_size, flags: ENTRY_FLAGS_RAW)]
87
+ else
88
+ raise AssetPackError, "asset pack supports only regular files and directories: #{source_path}"
89
+ end
90
+ rescue Errno::ENOENT
91
+ raise AssetPackError, "asset pack source not found: #{source_path}"
92
+ end
93
+
94
+ def assign_offsets(source_entries, first_data_offset)
95
+ next_offset = first_data_offset
96
+
97
+ source_entries.map do |entry|
98
+ packed_entry = Entry.new(
99
+ source_path: entry.source_path,
100
+ logical_path: entry.logical_path,
101
+ data_offset: next_offset,
102
+ stored_size: entry.stored_size,
103
+ unpacked_size: entry.unpacked_size,
104
+ flags: entry.flags,
105
+ )
106
+ next_offset += entry.stored_size
107
+ packed_entry
108
+ end
109
+ end
110
+
111
+ def write_header(io, entry_count, index_size, data_offset)
112
+ io.write([MAGIC, VERSION, HEADER_FLAGS, entry_count, index_size, data_offset].pack(HEADER_FORMAT))
113
+ end
114
+
115
+ def write_index(io, entries)
116
+ entries.each do |entry|
117
+ path_bytes = entry.logical_path.b
118
+ io.write([path_bytes.bytesize, entry.flags, entry.data_offset, entry.stored_size, entry.unpacked_size].pack(ENTRY_PREFIX_FORMAT))
119
+ io.write(path_bytes)
120
+ end
121
+ end
122
+
123
+ def write_data(io, entries)
124
+ entries.each do |entry|
125
+ File.open(entry.source_path, "rb") do |source_file|
126
+ IO.copy_stream(source_file, io)
127
+ end
128
+ end
129
+ end
130
+
131
+ def join_logical_path(left, right)
132
+ "#{left}/#{right}"
133
+ end
134
+
135
+ def path_within?(path, root)
136
+ normalized_path = File.expand_path(path)
137
+ normalized_root = File.expand_path(root)
138
+ normalized_path == normalized_root || normalized_path.start_with?(normalized_root + File::SEPARATOR)
139
+ end
140
+ end
141
+ end