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,1124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "rubygems/package"
6
+ require "tempfile"
7
+ require "zlib"
8
+
9
+ require_relative "asset_pack"
10
+ require_relative "build_cache"
11
+ require_relative "debug_map"
12
+
13
+ module MilkTea
14
+ class BuildError < StandardError; end
15
+
16
+ class Build
17
+ FrontendModule = Data.define(:name, :kind, :link_libraries, :compiler_flags, :uses_parallel_for)
18
+
19
+ class RubyFrontend
20
+ def compile(path:, module_roots:, package_graph:, platform:, emit_line_directives:, binary_path:, debug_guards: false)
21
+ program = ModuleLoader.new(module_roots:, package_graph:, platform:).check_program(path)
22
+ Build.frontend_build_artifacts(program, emit_line_directives:, binary_path:, debug_guards:)
23
+ end
24
+ end
25
+
26
+ DEFAULT_WASM_SHELL_TEMPLATE_PATH = File.expand_path("templates/wasm_shell.html", __dir__).freeze
27
+ WASM_SHELL_SCRIPT_PLACEHOLDER = "{{{ SCRIPT }}}".freeze
28
+ WASM_SHELL_CANVAS_PLACEHOLDER = "{{{ MILK_TEA_CANVAS }}}".freeze
29
+ WASM_SHELL_OUTPUT_PLACEHOLDER = "{{{ MILK_TEA_OUTPUT }}}".freeze
30
+ WASM_SHELL_BOOTSTRAP_PLACEHOLDER = "{{{ MILK_TEA_BOOTSTRAP }}}".freeze
31
+ WASM_SHELL_TEMPLATE = File.read(DEFAULT_WASM_SHELL_TEMPLATE_PATH).freeze
32
+ WASM_SHELL_CANVAS_TEMPLATE = <<~HTML.freeze
33
+ <canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
34
+ HTML
35
+ WASM_SHELL_OUTPUT_TEMPLATE = <<~HTML.freeze
36
+ <pre id="output"></pre>
37
+ HTML
38
+ WASM_SHELL_BOOTSTRAP_TEMPLATE = <<~HTML.freeze
39
+ <script>
40
+ var Module = {
41
+ print: (function() {
42
+ var element = document.getElementById('output');
43
+ if (element) element.textContent = '';
44
+ return function(text) {
45
+ if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
46
+ console.log(text);
47
+ if (element) {
48
+ element.textContent += text + "\\n";
49
+ element.scrollTop = element.scrollHeight;
50
+ }
51
+ };
52
+ })(),
53
+ printErr: (function() {
54
+ var element = document.getElementById('output');
55
+ return function(text) {
56
+ if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
57
+ console.error(text);
58
+ if (element) {
59
+ element.textContent += "[err] " + text + "\\n";
60
+ element.scrollTop = element.scrollHeight;
61
+ }
62
+ };
63
+ })(),
64
+ canvas: (function() {
65
+ return document.getElementById('canvas');
66
+ })()
67
+ };
68
+ </script>
69
+ HTML
70
+
71
+ Result = Data.define(:output_path, :c_path, :compiler, :link_flags, :profile, :platform, :bundle_root, :archive_path, :cached)
72
+
73
+ def self.build(path, output_path: nil, cc: ENV.fetch("CC", "cc"), keep_c_path: nil, raw_bindings: nil, module_roots: nil, package_graph: nil, frontend: nil, debug: false, profile: nil, platform: nil, bundle: false, archive: false, no_cache: false, sanitize: false, kind: :executable, debug_guards: nil)
74
+ raw_bindings ||= default_raw_bindings
75
+ new(path, output_path:, cc:, keep_c_path:, raw_bindings:, module_roots:, package_graph:, frontend:, debug:, profile:, platform:, bundle:, archive:, no_cache:, sanitize:, kind:, debug_guards:).build
76
+ end
77
+
78
+ def self.clean(path, output_path: nil, profile: nil, platform: nil, bundle: false, archive: false)
79
+ new(
80
+ path,
81
+ output_path:,
82
+ cc: ENV.fetch("CC", "cc"),
83
+ keep_c_path: nil,
84
+ raw_bindings: nil,
85
+ module_roots: nil,
86
+ debug: false,
87
+ profile:,
88
+ platform:,
89
+ bundle:,
90
+ archive:
91
+ ).clean
92
+ end
93
+
94
+ def self.frontend_build_artifacts(program, emit_line_directives: false, binary_path: nil, debug_guards: false)
95
+ ir_program = program.is_a?(IR::Program) ? program : Lowering.lower(program)
96
+ ensure_program_has_entrypoint!(program, ir_program)
97
+ compiled_c = CBackend.emit(ir_program, emit_line_directives:, debug_guards:)
98
+ saved_c = emit_line_directives ? nil : compiled_c
99
+
100
+ {
101
+ ir_program: ir_program,
102
+ compiled_c: compiled_c,
103
+ saved_c: saved_c,
104
+ debug_map: binary_path ? DebugMap.from_ir(ir_program, binary_path:) : nil,
105
+ modules: frontend_modules(program),
106
+ }
107
+ end
108
+
109
+ def self.frontend_modules(program)
110
+ return [].freeze if program.is_a?(IR::Program)
111
+
112
+ program.analyses_by_module_name.keys.sort.map do |module_name|
113
+ analysis = program.analyses_by_module_name.fetch(module_name)
114
+ FrontendModule.new(
115
+ name: module_name,
116
+ kind: analysis.module_kind,
117
+ link_libraries: analysis.directives.grep(AST::LinkDirective).map(&:value).freeze,
118
+ compiler_flags: analysis.directives.grep(AST::CompilerFlagDirective).map(&:value).freeze,
119
+ uses_parallel_for: analysis.uses_parallel_for,
120
+ )
121
+ end.freeze
122
+ end
123
+ private_class_method :frontend_modules
124
+
125
+ def self.default_raw_bindings(root: MilkTea.root)
126
+ require_relative "../bindings"
127
+
128
+ RawBindings.default_registry(root:)
129
+ end
130
+ private_class_method :default_raw_bindings
131
+
132
+ def initialize(path, output_path:, cc:, keep_c_path:, raw_bindings:, module_roots: nil, package_graph: nil, frontend: nil, debug: false, profile: nil, platform: nil, bundle: false, archive: false, no_cache: false, sanitize: false, kind: :executable, debug_guards: nil)
133
+ @kind = case kind
134
+ when :executable, :static, :shared then kind
135
+ else raise BuildError, "unknown build kind #{kind}; expected executable|static|shared"
136
+ end
137
+ @explicit_output_path = !output_path.nil?
138
+ resolved_source = use_package_build_for?(path)
139
+ if resolved_source
140
+ @package_build = true
141
+ manifest = resolved_source[:manifest]
142
+ @source_path = manifest.source_path
143
+ @project_root = manifest.root_dir
144
+ @package_name = manifest.package_name
145
+ @archive = archive
146
+ @bundle = bundle || archive
147
+ if manifest.package_kind == :library && @kind == :executable
148
+ raise BuildError, "cannot build library package #{manifest.package_name} as an executable"
149
+ end
150
+ unless @source_path
151
+ raise BuildError, "application package #{manifest.package_name} has no build entry; set build.entry or create src/main.mt"
152
+ end
153
+ @profile = normalize_profile(profile || manifest.profile || (debug ? :debug : :debug))
154
+ @platform = normalize_platform(platform || manifest.platform || host_platform)
155
+ @resolved_source_path = ModuleLoader.resolve_source_path(@source_path, platform: @platform, error_class: BuildError)
156
+ validate_bundle_mode!
157
+ @manifest_output_path = manifest.output_path
158
+ if @bundle
159
+ @bundle_root = File.expand_path(output_path || manifest.output_path || default_package_bundle_root)
160
+ @output_path = File.join(@bundle_root, "#{@package_name}#{artifact_extension}")
161
+ else
162
+ @bundle_root = nil
163
+ resolved_output = output_path || manifest.output_path || default_package_output_path
164
+ @output_path = normalize_output_path(File.expand_path(resolved_output))
165
+ end
166
+ @assets_paths = manifest.assets_paths
167
+ @html_template_path = manifest.html_template_path
168
+ else
169
+ @package_build = false
170
+ @source_path = File.expand_path(path)
171
+ @project_root = File.dirname(@source_path)
172
+ @package_name = File.basename(@project_root).tr("-", "_")
173
+ @archive = archive
174
+ @bundle = bundle || archive
175
+ @profile = normalize_profile(profile || (debug ? :debug : :debug))
176
+ @platform = normalize_platform(platform || host_platform)
177
+ @resolved_source_path = ModuleLoader.resolve_source_path(@source_path, platform: @platform, error_class: BuildError)
178
+ validate_bundle_mode!
179
+ @manifest_output_path = nil
180
+ @bundle_root = nil
181
+ @output_path = normalize_output_path(File.expand_path(output_path || default_source_output_path(@source_path)))
182
+ @assets_paths = []
183
+ @html_template_path = nil
184
+ end
185
+ @cc = resolve_compiler(cc)
186
+ @keep_c_path = keep_c_path ? File.expand_path(keep_c_path) : nil
187
+ @raw_bindings = raw_bindings
188
+ @package_graph = package_graph
189
+ @module_roots = (module_roots || @package_graph&.source_roots || MilkTea::ModuleRoots.roots_for_path(@source_path)).dup
190
+ @frontend = frontend || RubyFrontend.new
191
+ @no_cache = no_cache || sanitize
192
+ @sanitize = sanitize
193
+ @debug = debug
194
+ @debug_guards = debug_guards
195
+ end
196
+
197
+ def use_package_build_for?(path)
198
+ manifest = PackageManifest.load(path)
199
+ return nil unless manifest&.source_path
200
+
201
+ manifest_source = File.expand_path(manifest.source_path, manifest.root_dir)
202
+ given_source = File.expand_path(path)
203
+ return nil unless manifest_source == given_source || File.directory?(path)
204
+
205
+ { manifest: }
206
+ rescue PackageManifestError => e
207
+ if PackageManifest.manifest_exists_for?(path)
208
+ raise BuildError, e.message
209
+ end
210
+ nil
211
+ end
212
+
213
+ def clean
214
+ target_path = clean_target_path
215
+ if File.directory?(target_path)
216
+ FileUtils.rm_rf(target_path)
217
+ else
218
+ clean_output_artifacts(target_path)
219
+ clean_staged_runtime_assets(target_path)
220
+ FileUtils.rm_f(DebugMap.sidecar_path_for(@output_path))
221
+ end
222
+ clean_bundle_archive
223
+ clean_cache
224
+ target_path
225
+ end
226
+
227
+ def clean_cache
228
+ cache_dir = File.join(MilkTea.data_root.to_s, "tmp", "mtc-cache")
229
+ FileUtils.rm_rf(cache_dir) if File.exist?(cache_dir)
230
+ end
231
+
232
+ def build
233
+ Types::Registry.reset!
234
+ ensure_compiler_available!
235
+ ensure_supported_backend!
236
+
237
+ return build_static_library if @kind == :static
238
+ return build_shared_library if @kind == :shared
239
+
240
+ if @frontend.is_a?(RubyFrontend) && !@no_cache
241
+ build_cached
242
+ else
243
+ build_uncached
244
+ end
245
+ end
246
+
247
+ private
248
+
249
+ def build_static_library
250
+ cache = @no_cache ? nil : BuildCache.new(root: MilkTea.root)
251
+ emit_line_directives = line_directives_required?
252
+ _source_files, program, compiled_c, saved_c, frontend_modules, _ir_program = prepare_program(cache:, emit_line_directives:)
253
+ prepare_bindings(frontend_modules)
254
+
255
+ object_path = compile_or_cache_object(cache, compiled_c || saved_c)
256
+ archive_path = @output_path
257
+ command = darwin_target? ? ["libtool", "-static", "-o", archive_path, object_path] : ["ar", "rcs", archive_path, object_path]
258
+ stdout, stderr, status = Open3.capture3(*command)
259
+ raise BuildError, "archiver failed:\n#{stdout}#{stderr}" unless status.success?
260
+
261
+ generate_header(program) if program
262
+ Result.new(output_path: @output_path, c_path: nil, compiler: @cc, link_flags: [], profile: @profile, platform: @platform, bundle_root: nil, archive_path: nil, cached: false)
263
+ end
264
+
265
+ def build_shared_library
266
+ cache = @no_cache ? nil : BuildCache.new(root: MilkTea.root)
267
+ emit_line_directives = line_directives_required?
268
+ _source_files, program, compiled_c, saved_c, frontend_modules, _ir_program = prepare_program(cache:, emit_line_directives:)
269
+ prepare_bindings(frontend_modules)
270
+ compiler_flags = collect_compiler_flags(frontend_modules)
271
+ link_flags = collect_link_flags(frontend_modules)
272
+
273
+ object_path = compile_or_cache_object(cache, compiled_c || saved_c, pic: true)
274
+ shared_flags = darwin_target? ? ["-dynamiclib"] : ["-shared"]
275
+ command = [@cc, *shared_flags, "-o", @output_path, object_path, *link_flags, *compiler_flags]
276
+ stdout, stderr, status = Open3.capture3(*command)
277
+ raise BuildError, "shared library link failed:\n#{stdout}#{stderr}" unless status.success?
278
+
279
+ generate_header(program) if program
280
+ Result.new(output_path: @output_path, c_path: nil, compiler: @cc, link_flags:, profile: @profile, platform: @platform, bundle_root: nil, archive_path: nil, cached: false)
281
+ end
282
+
283
+ def prepare_program(cache:, emit_line_directives:)
284
+ loader = ModuleLoader.new(
285
+ module_roots: @module_roots,
286
+ package_graph: @package_graph,
287
+ shared_cache: cache&.shared_analysis_cache,
288
+ platform: @platform,
289
+ )
290
+ program = loader.check_program(@resolved_source_path)
291
+
292
+ # Check program-level cache for C source
293
+ source_files = program.analyses_by_path.keys.each_with_object({}) { |p, h| h[p] = File.read(p, mode: "rb") }
294
+ if cache
295
+ key = cache.program_key(source_files: source_files.to_a)
296
+ cached = cache.fetch_program(key)
297
+ if cached
298
+ frontend_modules = cached.frontend_modules
299
+ return [source_files, program, cached.c_source, cached.c_source, frontend_modules, nil]
300
+ end
301
+ end
302
+
303
+ ir_program = Lowering.lower(program)
304
+ dbg = debug_guards_enabled?
305
+ saved_c = CBackend.emit(ir_program, emit_line_directives: false, debug_guards: dbg)
306
+ compiled_c = emit_line_directives ? CBackend.emit(ir_program, emit_line_directives: true, debug_guards: dbg) : saved_c
307
+
308
+ frontend_modules = Build.send(:frontend_modules, program)
309
+
310
+ if cache
311
+ cache.store_program(key, c_source: compiled_c, frontend_modules:)
312
+ end
313
+
314
+ [source_files, program, compiled_c, saved_c, frontend_modules, ir_program]
315
+ end
316
+
317
+ def compile_or_cache_object(cache, source, pic: false)
318
+ return compile_to_object(source, pic:) unless cache
319
+
320
+ hasher = Digest::SHA256.new
321
+ hasher << source << "\0"
322
+ hasher << (pic ? "pic" : "nopic") << "\0"
323
+ hasher << @cc << "\0"
324
+ key = hasher.hexdigest
325
+
326
+ object_path = File.join(cache_cache_dir, "objects", key[0, 2], key, "object.o")
327
+ return object_path if File.exist?(object_path)
328
+
329
+ FileUtils.mkdir_p(File.dirname(object_path))
330
+ compile_to_object_at(source, object_path, pic:)
331
+ object_path
332
+ end
333
+
334
+ def compile_to_object_at(source, output_path, pic: false)
335
+ c_path = "#{output_path}.c"
336
+ File.write(c_path, source)
337
+ profile_flags = profile_compiler_flags
338
+ pic_flags = pic ? ["-fPIC"] : []
339
+ std_c_include_flag = "-I#{MilkTea.root.join('std/c')}"
340
+ command = [@cc, "-std=c11", "-c", *pic_flags, *profile_flags, std_c_include_flag, c_path, "-o", output_path]
341
+ stdout, stderr, status = Open3.capture3(*command)
342
+ FileUtils.rm_f(c_path)
343
+ raise BuildError, "C compilation failed:\n#{stdout}#{stderr}" unless status.success?
344
+ output_path
345
+ end
346
+
347
+ def cache_cache_dir
348
+ File.join(MilkTea.data_root.to_s, "tmp", "mtc-cache")
349
+ end
350
+
351
+ def darwin_target?
352
+ @platform == :darwin || RUBY_PLATFORM =~ /darwin/
353
+ end
354
+
355
+ def build_cached
356
+ cache = BuildCache.new(root: MilkTea.root)
357
+ emit_line_directives = line_directives_required?
358
+ @cached = true
359
+
360
+ program = load_program_with_cache(cache)
361
+
362
+ compiled_c, frontend_modules, debug_map_source = compile_frontend(program, cache, emit_line_directives)
363
+
364
+ prepare_bindings(frontend_modules)
365
+ compiler_flags = collect_compiler_flags(frontend_modules)
366
+ link_flags = collect_link_flags(frontend_modules)
367
+ debug_map_path = DebugMap.sidecar_path_for(@output_path)
368
+
369
+ FileUtils.mkdir_p(File.dirname(@output_path))
370
+
371
+ saved_c = compile_frontend_saved_c(compiled_c, emit_line_directives)
372
+
373
+ if @keep_c_path
374
+ saved_c ||= compiled_c unless emit_line_directives
375
+ if emit_line_directives && saved_c.nil?
376
+ saved_c = CBackend.emit(Lowering.lower(program), emit_line_directives: false, debug_guards: false)
377
+ end
378
+ raise BuildError, "frontend did not provide saved C output for --keep-c debug build" if saved_c.nil?
379
+
380
+ write_c_file(@keep_c_path, saved_c)
381
+ compile_and_link_cached(compiled_c, compiler_flags, link_flags, cache)
382
+ if debug_map_source
383
+ DebugMap.from_ir(debug_map_source, binary_path: @output_path).write(debug_map_path)
384
+ end
385
+ stage_runtime_assets
386
+ archive_path = write_bundle_archive
387
+ return Result.new(output_path: @output_path, c_path: @keep_c_path, compiler: @cc, link_flags:, profile: @profile, platform: @platform, bundle_root: @bundle_root, archive_path:, cached: @cached)
388
+ end
389
+
390
+ compile_and_link_cached(compiled_c, compiler_flags, link_flags, cache)
391
+
392
+ if debug_map_source
393
+ debug_map = debug_map_source.is_a?(IR::Program) ? DebugMap.from_ir(debug_map_source, binary_path: @output_path) : debug_map_source
394
+ debug_map.write(debug_map_path)
395
+ end
396
+ stage_runtime_assets
397
+ archive_path = write_bundle_archive
398
+
399
+ Result.new(output_path: @output_path, c_path: nil, compiler: @cc, link_flags:, profile: @profile, platform: @platform, bundle_root: @bundle_root, archive_path:, cached: @cached)
400
+ end
401
+
402
+ def build_uncached
403
+ emit_line_directives = line_directives_required?
404
+ artifacts = @frontend.compile(
405
+ path: @resolved_source_path,
406
+ module_roots: @module_roots,
407
+ package_graph: @package_graph,
408
+ platform: @platform,
409
+ emit_line_directives: emit_line_directives,
410
+ binary_path: @output_path,
411
+ debug_guards: debug_guards_enabled?,
412
+ )
413
+ ir_program = artifacts[:ir_program]
414
+ compiled_c = artifacts.fetch(:compiled_c)
415
+ saved_c = artifacts[:saved_c]
416
+ debug_map = artifacts.fetch(:debug_map)
417
+ frontend_modules = artifacts.fetch(:modules)
418
+ prepare_bindings(frontend_modules)
419
+ compiler_flags = collect_compiler_flags(frontend_modules)
420
+ link_flags = collect_link_flags(frontend_modules)
421
+ debug_map_path = DebugMap.sidecar_path_for(@output_path)
422
+
423
+ FileUtils.mkdir_p(File.dirname(@output_path))
424
+
425
+ if @keep_c_path
426
+ saved_c ||= compiled_c unless emit_line_directives
427
+ saved_c ||= CBackend.emit(ir_program, emit_line_directives: false, debug_guards: false) if emit_line_directives && ir_program
428
+ raise BuildError, "frontend did not provide saved C output for --keep-c debug build" if saved_c.nil?
429
+
430
+ write_c_file(@keep_c_path, saved_c)
431
+ if emit_line_directives
432
+ compile_generated_c(compiled_c, compiler_flags, link_flags)
433
+ else
434
+ compile(@keep_c_path, compiler_flags, link_flags)
435
+ end
436
+ debug_map.write(debug_map_path)
437
+ stage_runtime_assets
438
+ archive_path = write_bundle_archive
439
+ return Result.new(output_path: @output_path, c_path: @keep_c_path, compiler: @cc, link_flags:, profile: @profile, platform: @platform, bundle_root: @bundle_root, archive_path:, cached: false)
440
+ end
441
+
442
+ compile_generated_c(compiled_c, compiler_flags, link_flags)
443
+
444
+ debug_map.write(debug_map_path)
445
+ stage_runtime_assets
446
+ archive_path = write_bundle_archive
447
+
448
+ Result.new(output_path: @output_path, c_path: nil, compiler: @cc, link_flags:, profile: @profile, platform: @platform, bundle_root: @bundle_root, archive_path:, cached: false)
449
+ end
450
+
451
+ def clean_target_path
452
+ if @package_build && !@explicit_output_path && @manifest_output_path.nil?
453
+ return File.join(@project_root, "build", "dist") if @bundle
454
+
455
+ File.join(@project_root, "build")
456
+ elsif @bundle
457
+ @bundle_root
458
+ else
459
+ @output_path
460
+ end
461
+ end
462
+
463
+ def validate_bundle_mode!
464
+ return unless @bundle
465
+
466
+ raise BuildError, "bundle mode requires a package build" unless @package_build
467
+ raise BuildError, "bundle mode is supported only for native package builds" if target_wasm?
468
+ end
469
+
470
+ def load_program_with_cache(cache)
471
+ loader = ModuleLoader.new(
472
+ module_roots: @module_roots,
473
+ package_graph: @package_graph,
474
+ shared_cache: cache.shared_analysis_cache,
475
+ platform: @platform,
476
+ )
477
+ loader.check_program(@resolved_source_path)
478
+ end
479
+
480
+ def compile_frontend(program, cache, emit_line_directives)
481
+ source_files = program.analyses_by_path.keys.each_with_object({}) do |path, hash|
482
+ hash[path] = File.read(path, mode: "rb")
483
+ end
484
+ key = cache.program_key(source_files: source_files.to_a)
485
+
486
+ cached = cache.fetch_program(key)
487
+ if cached
488
+ return [cached.c_source, cached.frontend_modules, nil]
489
+ end
490
+
491
+ @cached = false
492
+
493
+ method_defs_hash = compute_method_defs_hash(program)
494
+ previous_method_defs = cache.fetch_method_defs
495
+ if previous_method_defs && previous_method_defs != method_defs_hash
496
+ cache.invalidate_all_module_ir
497
+ end
498
+
499
+ module_changes = detect_module_changes(program, cache, source_files)
500
+ if module_changes.any? { |_, v| v == :changed }
501
+ changed = module_changes.select { |_, v| v == :changed }.keys
502
+ warn " #{changed.length} of #{module_changes.length} module(s) changed" if $VERBOSE || ENV["MTC_VERBOSE"]
503
+ end
504
+
505
+ rgraph = reverse_import_graph(program)
506
+ changed_names = module_changes.select { |_, v| v == :changed }.map { |k, _| k.to_s }
507
+ invalidated = {}
508
+ changed_names.each { |mod| invalidated[mod] = true }
509
+ changed_names.each do |mod|
510
+ (rgraph[mod] || []).each { |dep| invalidated[dep] = true }
511
+ end
512
+
513
+ preload = {}
514
+ preload_synthetics = {}
515
+ module_changes.each do |module_name, status|
516
+ next unless status == :unchanged
517
+ next if invalidated.key?(module_name.to_s)
518
+
519
+ path = program.analyses_by_path.keys.find { |p| program.analyses_by_path[p].module_name.to_s == module_name.to_s }
520
+ next unless path
521
+
522
+ module_content = source_files[path]
523
+ next unless module_content
524
+
525
+ path_key = cache.module_key(path, module_content)
526
+ cached_data = cache.fetch_module_ir(path_key)
527
+ next unless cached_data
528
+
529
+ if cached_data.is_a?(Array)
530
+ ir, synths = cached_data
531
+ preload[module_name.to_s] = ir
532
+ preload_synthetics[module_name.to_s] = synths || {}
533
+ end
534
+ end
535
+
536
+ ir_program, modules, per_module_synthetics = Lowering.lower_incremental(
537
+ program,
538
+ cached: preload.empty? ? nil : preload,
539
+ cached_synthetics: preload_synthetics.empty? ? nil : preload_synthetics,
540
+ )
541
+
542
+ Build.ensure_program_has_entrypoint!(program, ir_program)
543
+ compiled_c = CBackend.emit(ir_program, emit_line_directives:, debug_guards: debug_guards_enabled?)
544
+
545
+ frontend_modules = Build.send(:frontend_modules, program)
546
+
547
+ cache.store_program(key, c_source: compiled_c, frontend_modules:)
548
+
549
+ source_files.each do |path, content|
550
+ analysis = program.analyses_by_path[path]
551
+ next unless analysis
552
+ next if preload.key?(analysis.module_name.to_s)
553
+
554
+ path_key = cache.module_key(path, content)
555
+ fragment = modules[analysis.module_name]
556
+ synths = per_module_synthetics[analysis.module_name]
557
+ cache.store_module_ir(path_key, fragment, synthetics: synths) if fragment
558
+ end
559
+
560
+ cache.store_method_defs(method_defs_hash)
561
+ update_module_caches(program, cache, source_files)
562
+
563
+ [compiled_c, frontend_modules, ir_program]
564
+ end
565
+
566
+ def reverse_import_graph(program)
567
+ graph = Hash.new { |h, k| h[k] = [] }
568
+ program.analyses_by_path.each do |path, analysis|
569
+ module_name = analysis.module_name.to_s
570
+ ast_imports = analysis.ast.respond_to?(:imports) ? analysis.ast.imports : []
571
+ ast_imports.each do |import|
572
+ imported_name = import.path.to_s
573
+ graph[imported_name] << module_name
574
+ end
575
+ end
576
+ graph
577
+ end
578
+
579
+ def compute_method_defs_hash(program)
580
+ hasher = Digest::SHA256.new
581
+ program.analyses_by_path.values.each do |analysis|
582
+ analysis.ast.declarations.grep(AST::ExtendingBlock).each do |block|
583
+ type_name = block.type_name.respond_to?(:name) ? block.type_name.name.parts.join(".") : block.type_name.to_s
584
+ block.methods.each do |method|
585
+ hasher << analysis.module_name.to_s << "\0"
586
+ hasher << type_name << "\0"
587
+ hasher << method.kind.to_s << "\0" << method.name << "\0"
588
+ end
589
+ end
590
+ end
591
+ hasher.hexdigest
592
+ end
593
+
594
+ def detect_module_changes(program, cache, source_files)
595
+ changes = {}
596
+
597
+ program.analyses_by_path.each do |path, analysis|
598
+ module_name = analysis.module_name.to_s
599
+ content = source_files[path]
600
+ next unless content
601
+
602
+ module_key = cache.module_key(path, content)
603
+ state = cache.fetch_module_state(module_key)
604
+
605
+ if state.nil? || state[:source_key] != module_key
606
+ changes[module_name] = :changed
607
+ else
608
+ changes[module_name] = :unchanged
609
+ end
610
+ end
611
+
612
+ changes
613
+ end
614
+
615
+ def update_module_caches(program, cache, source_files)
616
+ program.analyses_by_path.each do |path, analysis|
617
+ content = source_files[path]
618
+ next unless content
619
+
620
+ module_name = analysis.module_name.to_s
621
+ module_key = cache.module_key(path, content)
622
+ ast_imports = analysis.ast.respond_to?(:imports) ? analysis.ast.imports : []
623
+ deps = ast_imports.map { |import| import.path.to_s }
624
+
625
+ cache.store_module_state(
626
+ module_key,
627
+ module_name: module_name,
628
+ source_key: module_key,
629
+ dependencies: deps,
630
+ )
631
+ end
632
+ end
633
+
634
+ def compile_frontend_saved_c(compiled_c, emit_line_directives)
635
+ return nil if emit_line_directives
636
+
637
+ compiled_c
638
+ end
639
+
640
+ def compile_and_link_cached(compiled_c, compiler_flags, link_flags, cache)
641
+ binary_key = cache.binary_key(c_source: compiled_c, cc: @cc, compiler_flags:, link_flags:)
642
+ cached_binary = cache.fetch_binary(binary_key)
643
+
644
+ if cached_binary
645
+ FileUtils.cp(cached_binary, @output_path)
646
+ return
647
+ end
648
+
649
+ @cached = false
650
+ compile_generated_c(compiled_c, compiler_flags, link_flags)
651
+ cache.store_binary(binary_key, @output_path)
652
+ end
653
+
654
+ def self.ensure_program_has_entrypoint!(program, ir_program)
655
+ return if ir_program.functions.any?(&:entry_point)
656
+ return if program.is_a?(IR::Program)
657
+
658
+ if program.root_analysis.functions.key?("main")
659
+ raise BuildError, "root main is not a valid executable entrypoint; expected `function main() -> int|void`, `function main(argc: int, argv: ptr[cstr]) -> int|void`, `function main(argc: int, argv: ptr[ptr[char]]) -> int|void`, or `function main(args: span[str]) -> int|void`"
660
+ end
661
+
662
+ raise BuildError, "no executable entrypoint found; define `main` with one of the supported executable signatures"
663
+ end
664
+
665
+ def package_manifest_required_for?(path)
666
+ expanded_path = File.expand_path(path)
667
+ return true if File.directory?(expanded_path)
668
+ return true if File.basename(expanded_path) == "package.toml"
669
+
670
+ current = File.dirname(expanded_path)
671
+ loop do
672
+ return true if File.file?(File.join(current, "package.toml"))
673
+
674
+ parent = File.dirname(current)
675
+ break if parent == current
676
+
677
+ current = parent
678
+ end
679
+
680
+ false
681
+ end
682
+
683
+ def default_source_output_path(source_path)
684
+ base = source_path.sub(/\.mt\z/, "")
685
+ target_wasm? ? "#{base}.html" : base
686
+ end
687
+
688
+ def default_package_output_path
689
+ build_root = File.join(@project_root, "build")
690
+ File.join(build_root, "bin", @platform.to_s, @profile.to_s, "#{@package_name}#{artifact_extension}")
691
+ end
692
+
693
+ def default_package_bundle_root
694
+ File.join(@project_root, "build", "dist", @platform.to_s, @profile.to_s, @package_name)
695
+ end
696
+
697
+ def normalize_profile(value)
698
+ case value.to_s
699
+ when "", "debug", "dev"
700
+ :debug
701
+ when "release", "rel"
702
+ :release
703
+ else
704
+ raise BuildError, "unknown profile #{value}; expected debug|release"
705
+ end
706
+ end
707
+
708
+ def normalize_platform(value)
709
+ case value.to_s
710
+ when "", "linux"
711
+ :linux
712
+ when "windows", "win", "win32"
713
+ :windows
714
+ when "wasm", "web", "html5", "browser"
715
+ :wasm
716
+ when "darwin", "macos", "osx"
717
+ :darwin
718
+ else
719
+ raise BuildError, "unknown platform #{value}; expected linux|windows|wasm|darwin"
720
+ end
721
+ end
722
+
723
+ def normalize_output_path(path)
724
+ return path unless target_wasm?
725
+
726
+ extension = File.extname(path)
727
+ return "#{path}.html" if extension.empty?
728
+ return path if extension == ".html"
729
+
730
+ raise BuildError, "wasm output path must end with .html or omit the extension: #{path}"
731
+ end
732
+
733
+ def resolve_compiler(requested_cc)
734
+ return ENV.fetch("EMCC", "emcc") if target_wasm? && requested_cc == ENV.fetch("CC", "cc")
735
+
736
+ requested_cc
737
+ end
738
+
739
+ def prepare_bindings(frontend_modules)
740
+ frontend_modules.each do |mod|
741
+ next unless mod.kind == :raw_module
742
+
743
+ binding = @raw_bindings.find_by_module_name(mod.name)
744
+ next unless binding
745
+
746
+ binding.prepare!(cc: @cc, platform: @platform)
747
+ end
748
+ rescue RawBindings::Error => e
749
+ raise BuildError, e.message
750
+ end
751
+
752
+ def write_c_file(path, source)
753
+ FileUtils.mkdir_p(File.dirname(path))
754
+ File.write(path, source)
755
+ end
756
+
757
+ def compile_generated_c(source, compiler_flags, link_flags)
758
+ Tempfile.create(["milk-tea-build", ".c"]) do |file|
759
+ file.write(source)
760
+ file.flush
761
+ file.close
762
+
763
+ compile(file.path, compiler_flags, link_flags)
764
+ end
765
+ end
766
+
767
+ def ensure_compiler_available!
768
+ return if compiler_available?(@cc)
769
+
770
+ raise BuildError, "C compiler not found: #{@cc}"
771
+ end
772
+
773
+ def ensure_supported_backend!
774
+ if target_wasm?
775
+ return if emscripten_backend?(@cc)
776
+
777
+ raise BuildError, "unsupported C compiler backend for wasm target: #{@cc}; use Emscripten emcc"
778
+ end
779
+
780
+ return unless msvc_style_backend?(@cc)
781
+
782
+ raise BuildError, "unsupported C compiler backend for native target: #{@cc}; use a clang/gcc-style compiler driver instead of cl.exe or clang-cl"
783
+ end
784
+
785
+ def collect_link_flags(frontend_modules)
786
+ flags = frontend_modules.each_with_object([]) do |mod, flags|
787
+ next unless mod.kind == :raw_module
788
+
789
+ binding = @raw_bindings.find_by_module_name(mod.name)
790
+ if binding
791
+ binding.link_flags(platform: @platform).grep(/\A-L/).each do |flag|
792
+ flags << flag unless flags.include?(flag)
793
+ end
794
+ end
795
+
796
+ mod.link_libraries.each do |link_library|
797
+ flag = "-l#{link_library}"
798
+ flags << flag unless flags.include?(flag)
799
+ end
800
+
801
+ if binding
802
+ binding.link_flags(platform: @platform).reject { |flag| flag.start_with?("-L") }.each do |flag|
803
+ flags << flag unless flags.include?(flag)
804
+ end
805
+ end
806
+ end
807
+
808
+ if frontend_modules.any?(&:uses_parallel_for) && !flags.include?("-luv")
809
+ libuv_binding = @raw_bindings.find_by_module_name("std.c.libuv")
810
+ if libuv_binding
811
+ libuv_binding.link_flags(platform: @platform).each do |flag|
812
+ flags << flag unless flags.include?(flag)
813
+ end
814
+ end
815
+ flags << "-luv" unless flags.include?("-luv")
816
+ end
817
+
818
+ flags
819
+ end
820
+
821
+ def collect_compiler_flags(frontend_modules)
822
+ std_c_include_flag = "-I#{MilkTea.root.join('std/c')}"
823
+
824
+ flags = frontend_modules.each_with_object([]) do |mod, result|
825
+ next unless mod.kind == :raw_module
826
+
827
+ if mod.name.start_with?("std.c.")
828
+ result << std_c_include_flag unless result.include?(std_c_include_flag)
829
+ end
830
+
831
+ mod.compiler_flags.each do |compiler_flag|
832
+ result << compiler_flag unless result.include?(compiler_flag)
833
+ end
834
+
835
+ binding = @raw_bindings.find_by_module_name(mod.name)
836
+ next unless binding
837
+
838
+ binding.build_flags(platform: @platform).each do |flag|
839
+ result << flag unless result.include?(flag)
840
+ end
841
+ end
842
+
843
+ if frontend_modules.any?(&:uses_parallel_for)
844
+ flags << std_c_include_flag unless flags.include?(std_c_include_flag)
845
+ libuv_binding = @raw_bindings.find_by_module_name("std.c.libuv")
846
+ if libuv_binding
847
+ libuv_binding.build_flags(platform: @platform).each do |flag|
848
+ flags << flag unless flags.include?(flag)
849
+ end
850
+ end
851
+ end
852
+
853
+ flags
854
+ rescue RawBindings::Error => e
855
+ raise BuildError, e.message
856
+ end
857
+
858
+ def compile(c_path, compiler_flags, link_flags)
859
+ return compile_wasm(c_path, compiler_flags, link_flags) if target_wasm?
860
+
861
+ profile_flags = profile_compiler_flags
862
+ command = [@cc, "-std=c11", *sanitize_flags, *profile_flags, *compiler_flags, c_path, "-o", @output_path, *link_flags]
863
+ stdout, stderr, status = Open3.capture3(*command)
864
+ return if status.success?
865
+
866
+ details = [stdout, stderr].reject(&:empty?).join
867
+ raise BuildError, details.empty? ? "C compiler failed" : "C compiler failed:\n#{details}"
868
+ rescue Errno::ENOENT
869
+ raise BuildError, "C compiler not found: #{@cc}"
870
+ end
871
+
872
+ def sanitize_flags
873
+ @sanitize ? %w[-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer] : []
874
+ end
875
+
876
+ def host_platform
877
+ MilkTea.host_platform
878
+ end
879
+
880
+ def clean_output_artifacts(target_path)
881
+ FileUtils.rm_f(target_path)
882
+ return unless target_wasm? && File.extname(target_path) == ".html"
883
+
884
+ base_path = target_path.sub(/\.html\z/, "")
885
+ %w[.js .wasm .data .worker.js .symbols .wasm.map].each do |extension|
886
+ FileUtils.rm_f("#{base_path}#{extension}")
887
+ end
888
+ end
889
+
890
+ def clean_bundle_archive
891
+ return unless @archive
892
+
893
+ FileUtils.rm_f(bundle_archive_path)
894
+ end
895
+
896
+ def clean_staged_runtime_assets(target_path)
897
+ runtime_asset_mappings_for(target_path).each do |_source_path, staged_path|
898
+ FileUtils.rm_rf(staged_path)
899
+ end
900
+
901
+ FileUtils.rm_f(runtime_asset_pack_path_for(target_path)) if runtime_assets_packed?
902
+ end
903
+
904
+ def compile_wasm(c_path, compiler_flags, link_flags)
905
+ profile_flags = profile_compiler_flags
906
+ preload_flags = wasm_asset_flags
907
+ module_api_flags = ["-sINCOMING_MODULE_JS_API=canvas,print,printErr"]
908
+
909
+ with_wasm_shell_file do |shell_path|
910
+ command = [@cc, "-std=c11", *profile_flags, *compiler_flags, c_path, "-o", @output_path, "--shell-file", shell_path, *module_api_flags, *preload_flags, *link_flags]
911
+ stdout, stderr, status = Open3.capture3(*command)
912
+ return if status.success?
913
+
914
+ details = [stdout, stderr].reject(&:empty?).join
915
+ raise BuildError, details.empty? ? "C compiler failed" : "C compiler failed:\n#{details}"
916
+ end
917
+ rescue Errno::ENOENT
918
+ raise BuildError, "C compiler not found: #{@cc}"
919
+ end
920
+
921
+ def with_wasm_shell_file
922
+ Tempfile.create(["milk-tea-shell", ".html"]) do |file|
923
+ file.write(render_wasm_shell_template)
924
+ file.flush
925
+ file.close
926
+ yield file.path
927
+ end
928
+ end
929
+
930
+ def render_wasm_shell_template
931
+ template_source = load_wasm_shell_template_source
932
+ validate_wasm_shell_placeholder!(template_source, WASM_SHELL_SCRIPT_PLACEHOLDER, "Emscripten {{{ SCRIPT }}}")
933
+ validate_wasm_shell_placeholder!(template_source, WASM_SHELL_CANVAS_PLACEHOLDER, "Milk Tea {{{ MILK_TEA_CANVAS }}}")
934
+ validate_wasm_shell_placeholder!(template_source, WASM_SHELL_OUTPUT_PLACEHOLDER, "Milk Tea {{{ MILK_TEA_OUTPUT }}}")
935
+ validate_wasm_shell_placeholder!(template_source, WASM_SHELL_BOOTSTRAP_PLACEHOLDER, "Milk Tea {{{ MILK_TEA_BOOTSTRAP }}}")
936
+
937
+ template_source
938
+ .sub(WASM_SHELL_CANVAS_PLACEHOLDER, WASM_SHELL_CANVAS_TEMPLATE)
939
+ .sub(WASM_SHELL_OUTPUT_PLACEHOLDER, WASM_SHELL_OUTPUT_TEMPLATE)
940
+ .sub(WASM_SHELL_BOOTSTRAP_PLACEHOLDER, WASM_SHELL_BOOTSTRAP_TEMPLATE)
941
+ end
942
+
943
+ def load_wasm_shell_template_source
944
+ return WASM_SHELL_TEMPLATE.dup unless @html_template_path
945
+
946
+ File.read(@html_template_path)
947
+ end
948
+
949
+ def validate_wasm_shell_placeholder!(template_source, placeholder, label)
950
+ count = template_source.scan(Regexp.new(Regexp.escape(placeholder))).length
951
+ return if count == 1
952
+
953
+ template_path = @html_template_path || DEFAULT_WASM_SHELL_TEMPLATE_PATH
954
+ raise BuildError, "wasm HTML template must contain #{label} exactly once: #{template_path}"
955
+ end
956
+
957
+ def wasm_asset_flags
958
+ return [] unless target_wasm? && !@assets_paths.empty?
959
+
960
+ @assets_paths.flat_map do |assets_path|
961
+ mount_path = "/#{File.basename(assets_path)}"
962
+ ["--preload-file", "#{assets_path}@#{mount_path}"]
963
+ end
964
+ end
965
+
966
+ def stage_runtime_assets
967
+ if runtime_assets_packed?
968
+ runtime_asset_mappings_for(@output_path).each do |_source_path, staged_path|
969
+ FileUtils.rm_rf(staged_path)
970
+ end
971
+
972
+ begin
973
+ AssetPack.write(runtime_asset_pack_path_for(@output_path), @assets_paths)
974
+ rescue AssetPackError => e
975
+ raise BuildError, e.message
976
+ end
977
+
978
+ return
979
+ end
980
+
981
+ runtime_asset_mappings_for(@output_path).each do |source_path, staged_path|
982
+ ensure_runtime_assets_do_not_overlap_source!(source_path, staged_path)
983
+ FileUtils.rm_rf(staged_path)
984
+ FileUtils.mkdir_p(File.dirname(staged_path))
985
+
986
+ if File.directory?(source_path)
987
+ FileUtils.cp_r(source_path, File.dirname(staged_path))
988
+ else
989
+ FileUtils.cp(source_path, File.dirname(staged_path))
990
+ end
991
+ end
992
+ end
993
+
994
+ def write_bundle_archive
995
+ return nil unless @archive
996
+
997
+ archive_path = bundle_archive_path
998
+ FileUtils.rm_f(archive_path)
999
+ FileUtils.mkdir_p(File.dirname(archive_path))
1000
+
1001
+ Tempfile.create(["milk-tea-bundle", ".tar"]) do |tar_file|
1002
+ Gem::Package::TarWriter.new(tar_file) do |tar|
1003
+ add_archive_tree(tar, @bundle_root, File.basename(@bundle_root))
1004
+ end
1005
+
1006
+ tar_file.flush
1007
+ tar_file.rewind
1008
+
1009
+ Zlib::GzipWriter.open(archive_path) do |gzip|
1010
+ IO.copy_stream(tar_file, gzip)
1011
+ end
1012
+ end
1013
+
1014
+ archive_path
1015
+ end
1016
+
1017
+ def bundle_archive_path
1018
+ "#{@bundle_root}.tar.gz"
1019
+ end
1020
+
1021
+ def add_archive_tree(tar, source_path, archive_path)
1022
+ stat = File.lstat(source_path)
1023
+
1024
+ if stat.directory?
1025
+ tar.mkdir(archive_path, stat.mode & 0o777)
1026
+ Dir.children(source_path).sort.each do |child|
1027
+ add_archive_tree(tar, File.join(source_path, child), File.join(archive_path, child))
1028
+ end
1029
+ elsif stat.file?
1030
+ tar.add_file(archive_path, stat.mode & 0o777) do |io|
1031
+ File.open(source_path, "rb") do |file|
1032
+ IO.copy_stream(file, io)
1033
+ end
1034
+ end
1035
+ end
1036
+ end
1037
+
1038
+ def runtime_asset_mappings_for(target_path)
1039
+ return [] if @assets_paths.empty?
1040
+ return [] if target_wasm?
1041
+
1042
+ @assets_paths.filter_map do |assets_path|
1043
+ staged_path = File.join(File.dirname(target_path), File.basename(assets_path))
1044
+ next if staged_path == assets_path
1045
+
1046
+ [assets_path, staged_path]
1047
+ end
1048
+ end
1049
+
1050
+ def runtime_assets_packed?
1051
+ @bundle && !target_wasm? && !@assets_paths.empty?
1052
+ end
1053
+
1054
+ def runtime_asset_pack_path_for(target_path)
1055
+ File.join(File.dirname(target_path), "assets.mtpack")
1056
+ end
1057
+
1058
+ def ensure_runtime_assets_do_not_overlap_source!(source_path, staged_path)
1059
+ return unless File.directory?(source_path)
1060
+ return unless path_within?(staged_path, source_path)
1061
+
1062
+ raise BuildError, "native runtime asset output would be written inside build.assets source tree: #{staged_path}"
1063
+ end
1064
+
1065
+ def path_within?(path, root)
1066
+ normalized_path = File.expand_path(path)
1067
+ normalized_root = File.expand_path(root)
1068
+ normalized_path == normalized_root || normalized_path.start_with?(normalized_root + File::SEPARATOR)
1069
+ end
1070
+
1071
+ def target_windows?
1072
+ @platform == :windows
1073
+ end
1074
+
1075
+ def target_wasm?
1076
+ @platform == :wasm
1077
+ end
1078
+
1079
+ def artifact_extension
1080
+ return ".html" if target_wasm?
1081
+ return ".exe" if target_windows?
1082
+
1083
+ ""
1084
+ end
1085
+
1086
+ def profile_compiler_flags
1087
+ return ["-g", "-O0"] if @debug || @profile == :debug
1088
+
1089
+ ["-O3", "-DNDEBUG"]
1090
+ end
1091
+
1092
+ def line_directives_required?
1093
+ @debug || @profile == :debug
1094
+ end
1095
+
1096
+ def debug_guards_enabled?
1097
+ return @debug_guards unless @debug_guards.nil?
1098
+
1099
+ @profile == :debug
1100
+ end
1101
+
1102
+ def compiler_available?(compiler)
1103
+ return File.file?(compiler) && File.executable?(compiler) if compiler.include?(File::SEPARATOR)
1104
+
1105
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |entry|
1106
+ candidate = File.join(entry, compiler)
1107
+ File.file?(candidate) && File.executable?(candidate)
1108
+ end
1109
+ end
1110
+
1111
+ def compiler_command_name(compiler)
1112
+ File.basename(compiler).downcase.sub(/\.(?:exe|bat|cmd|com|sh|py)\z/, "")
1113
+ end
1114
+
1115
+ def emscripten_backend?(compiler)
1116
+ compiler_command_name(compiler).split(/[-_.]/).include?("emcc")
1117
+ end
1118
+
1119
+ def msvc_style_backend?(compiler)
1120
+ tokens = compiler_command_name(compiler).split(/[-_.]/)
1121
+ tokens.last == "cl" || tokens.last(2) == %w[clang cl]
1122
+ end
1123
+ end
1124
+ end