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,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "pathname"
6
+
7
+ module MilkTea
8
+ class DebugMap
9
+ VERSION = 1
10
+
11
+ Entry = Data.define(:name, :linkage_name, :line) do
12
+ def to_h
13
+ payload = {
14
+ "name" => name,
15
+ "cName" => linkage_name,
16
+ }
17
+ payload["line"] = line if line
18
+ payload
19
+ end
20
+ end
21
+
22
+ Function = Data.define(:name, :linkage_name, :source_path, :line, :params, :locals) do
23
+ def to_h(base_dir: nil)
24
+ payload = {
25
+ "name" => name,
26
+ "cName" => linkage_name,
27
+ "params" => params.map(&:to_h),
28
+ "locals" => locals.map(&:to_h),
29
+ }
30
+ serialized_source_path = DebugMap.path_for_payload(source_path, base_dir)
31
+ payload["sourcePath"] = serialized_source_path if serialized_source_path
32
+ payload["line"] = line if line
33
+ payload
34
+ end
35
+ end
36
+
37
+ attr_reader :binary_path, :program_source_path, :functions
38
+
39
+ def initialize(binary_path:, program_source_path:, functions:)
40
+ @binary_path = binary_path ? File.expand_path(binary_path) : nil
41
+ @program_source_path = program_source_path ? File.expand_path(program_source_path) : nil
42
+ @functions = functions
43
+ @functions_by_c_name = functions.each_with_object({}) do |function, memo|
44
+ memo[function.linkage_name] ||= function
45
+ end
46
+ end
47
+
48
+ def self.sidecar_path_for(binary_path)
49
+ "#{File.expand_path(binary_path)}.mtdbg.json"
50
+ end
51
+
52
+ def self.load(path)
53
+ resolved_path = File.expand_path(path)
54
+ base_dir = File.dirname(resolved_path)
55
+ payload = JSON.parse(File.read(resolved_path))
56
+ functions = Array(payload["functions"]).map do |function|
57
+ Function.new(
58
+ name: function.fetch("name"),
59
+ linkage_name: function.fetch("cName"),
60
+ source_path: path_from_payload(function["sourcePath"], base_dir),
61
+ line: function["line"],
62
+ params: load_entries(function["params"]),
63
+ locals: load_entries(function["locals"])
64
+ )
65
+ end
66
+
67
+ new(
68
+ binary_path: path_from_payload(payload["binaryPath"], base_dir),
69
+ program_source_path: path_from_payload(payload["programSourcePath"], base_dir),
70
+ functions:
71
+ )
72
+ end
73
+
74
+ def self.load_for_binary(binary_path)
75
+ path = sidecar_path_for(binary_path)
76
+ return nil unless File.file?(path)
77
+
78
+ load(path)
79
+ rescue JSON::ParserError
80
+ nil
81
+ end
82
+
83
+ def self.from_ir(ir_program, binary_path:)
84
+ functions = ir_program.functions.map do |function|
85
+ source_path = first_statement_source_path(function.body) || ir_program.source_path
86
+ Function.new(
87
+ name: function.name.to_s,
88
+ linkage_name: function.linkage_name.to_s,
89
+ source_path: source_path ? File.expand_path(source_path) : nil,
90
+ line: first_statement_line(function.body),
91
+ params: function.params.map { |param| Entry.new(name: param.name.to_s, linkage_name: param.linkage_name.to_s, line: nil) },
92
+ locals: collect_locals(function.body)
93
+ )
94
+ end
95
+
96
+ new(binary_path:, program_source_path: ir_program.source_path, functions:)
97
+ end
98
+
99
+ def write(path)
100
+ resolved_path = File.expand_path(path)
101
+ base_dir = File.dirname(resolved_path)
102
+ FileUtils.mkdir_p(base_dir)
103
+ File.write(resolved_path, JSON.pretty_generate(to_h(base_dir:)) + "\n")
104
+ end
105
+
106
+ def function_for_c_name(c_name)
107
+ @functions_by_c_name[c_name.to_s]
108
+ end
109
+
110
+ def variable_for(function_c_name, c_name)
111
+ function = function_for_c_name(function_c_name)
112
+ return nil unless function
113
+
114
+ function.params.find { |entry| entry.linkage_name == c_name.to_s } ||
115
+ function.locals.find { |entry| entry.linkage_name == c_name.to_s }
116
+ end
117
+
118
+ def source_variable_for(function_c_name, source_name)
119
+ function = function_for_c_name(function_c_name)
120
+ return nil unless function
121
+
122
+ matches = (function.params + function.locals).select { |entry| entry.name == source_name.to_s }
123
+ return nil if matches.empty?
124
+
125
+ return matches.first if matches.map(&:linkage_name).uniq.one?
126
+
127
+ nil
128
+ end
129
+
130
+ def to_h(base_dir: nil)
131
+ payload = {
132
+ "version" => VERSION,
133
+ "functions" => functions.map { |function| function.to_h(base_dir:) },
134
+ }
135
+ serialized_binary_path = self.class.path_for_payload(binary_path, base_dir)
136
+ payload["binaryPath"] = serialized_binary_path if serialized_binary_path
137
+ serialized_program_source_path = self.class.path_for_payload(program_source_path, base_dir)
138
+ payload["programSourcePath"] = serialized_program_source_path if serialized_program_source_path
139
+ payload
140
+ end
141
+
142
+ def self.path_for_payload(path, base_dir)
143
+ return nil unless path
144
+ return path.tr("\\", "/") unless base_dir
145
+
146
+ expanded_path = File.expand_path(path)
147
+ begin
148
+ Pathname.new(expanded_path).relative_path_from(Pathname.new(base_dir)).to_s.tr("\\", "/")
149
+ rescue ArgumentError
150
+ expanded_path.tr("\\", "/")
151
+ end
152
+ end
153
+
154
+ def self.path_from_payload(path, base_dir)
155
+ return nil unless path
156
+ return path if path.empty?
157
+ return path if absolute_path_string?(path)
158
+ return path unless base_dir
159
+
160
+ File.expand_path(path, base_dir)
161
+ end
162
+
163
+ def self.absolute_path_string?(path)
164
+ path.start_with?("/") || path.start_with?("\\\\") || path.match?(/\A[A-Za-z]:[\\\/]/)
165
+ end
166
+
167
+ class << self
168
+ private
169
+
170
+ def load_entries(entries)
171
+ Array(entries).map do |entry|
172
+ Entry.new(name: entry.fetch("name"), linkage_name: entry.fetch("cName"), line: entry["line"])
173
+ end
174
+ end
175
+
176
+ def collect_locals(statements, locals = [])
177
+ Array(statements).each do |statement|
178
+ case statement
179
+ when IR::LocalDecl
180
+ locals << Entry.new(name: statement.name.to_s, linkage_name: statement.linkage_name.to_s, line: statement.line)
181
+ when IR::BlockStmt, IR::WhileStmt
182
+ collect_locals(statement.body, locals)
183
+ when IR::ForStmt
184
+ collect_locals([statement.init], locals)
185
+ collect_locals(statement.body, locals)
186
+ collect_locals([statement.post], locals)
187
+ when IR::IfStmt
188
+ collect_locals(statement.then_body, locals)
189
+ collect_locals(statement.else_body, locals)
190
+ when IR::SwitchStmt
191
+ statement.cases.each do |switch_case|
192
+ collect_locals(switch_case.body, locals)
193
+ end
194
+ end
195
+ end
196
+
197
+ locals
198
+ end
199
+
200
+ def first_statement_source_path(statements)
201
+ Array(statements).each do |statement|
202
+ if statement.respond_to?(:source_path) && statement.source_path
203
+ return statement.source_path
204
+ end
205
+
206
+ nested = nested_statements(statement)
207
+ nested.each do |branch|
208
+ source_path = first_statement_source_path(branch)
209
+ return source_path if source_path
210
+ end
211
+ end
212
+
213
+ nil
214
+ end
215
+
216
+ def first_statement_line(statements)
217
+ Array(statements).each do |statement|
218
+ if statement.respond_to?(:line) && statement.line
219
+ return statement.line
220
+ end
221
+
222
+ nested = nested_statements(statement)
223
+ nested.each do |branch|
224
+ line = first_statement_line(branch)
225
+ return line if line
226
+ end
227
+ end
228
+
229
+ nil
230
+ end
231
+
232
+ def nested_statements(statement)
233
+ case statement
234
+ when IR::BlockStmt, IR::WhileStmt
235
+ [statement.body]
236
+ when IR::ForStmt
237
+ [[statement.init], statement.body, [statement.post]]
238
+ when IR::IfStmt
239
+ [statement.then_body, statement.else_body]
240
+ when IR::SwitchStmt
241
+ statement.cases.map(&:body)
242
+ else
243
+ []
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,369 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sinatra/base"
4
+ require "json"
5
+ require "open3"
6
+
7
+ module MilkTea
8
+ class DocsApp < Sinatra::Base
9
+ SNAPSHOT_CACHE = {}
10
+
11
+ MODULE_CATEGORIES = {
12
+ "Collections" => %w[vec deque binary_heap priority_queue ordered_set ordered_map map set linked_map linked_set counter multiset queue stack],
13
+ "Memory" => %w[mem cell],
14
+ "Text & Format" => %w[string str fmt cstring encoding base64],
15
+ "Math & Random" => %w[math random],
16
+ "Data & Serialization" => %w[binary bytes json toml tar gzip zstd crypto hash],
17
+ "Concurrency" => %w[async jobs sync thread],
18
+ "Files & I/O" => %w[fs path stdio],
19
+ "System" => %w[ctype errno process time c],
20
+ "Network & HTTP" => %w[net http uri cookie curl],
21
+ "Game & Graphics" => %w[raylib box2d flecs enet cgltf cjson],
22
+ "Algorithms & AI" => %w[fsm behavior_tree goap],
23
+ "Database & Matching" => %w[sqlite3 pcre2],
24
+ "Utilities" => %w[cli terminal span spatial asset_pack],
25
+ }.freeze
26
+
27
+ configure do
28
+ set :views, File.expand_path("views", __dir__)
29
+ set :public_folder, File.expand_path("public", __dir__)
30
+ set :static, true
31
+ set :logging, false
32
+ set :show_exceptions, false
33
+ end
34
+
35
+ helpers do
36
+ def stdlib_root
37
+ @stdlib_root ||= File.expand_path("../../../std", __dir__)
38
+ end
39
+
40
+ def reference_index
41
+ @reference_index ||= File.expand_path("../../../docs/index.html", __dir__)
42
+ end
43
+
44
+ def h(text)
45
+ Rack::Utils.escape_html(text.to_s)
46
+ end
47
+
48
+ def active?(path)
49
+ request.path_info == path ? "active" : ""
50
+ end
51
+
52
+ def stdlib_modules
53
+ @stdlib_modules ||= discover_modules.freeze
54
+ end
55
+
56
+ def categorized_modules
57
+ @categorized_modules ||= begin
58
+ cats = {}
59
+ uncategorized = []
60
+
61
+ stdlib_modules.each do |mod|
62
+ cat = MODULE_CATEGORIES.find { |_, names| names.include?(mod[:name]) }&.first
63
+ if cat
64
+ (cats[cat] ||= []) << mod
65
+ else
66
+ uncategorized << mod
67
+ end
68
+ end
69
+
70
+ cats["Other"] = uncategorized unless uncategorized.empty?
71
+ cats
72
+ end.freeze
73
+ end
74
+
75
+ def snapshot_available?
76
+ !!snapshot_script_path
77
+ end
78
+
79
+ def snapshot_for(source_path)
80
+ return nil unless snapshot_available?
81
+
82
+ cache_key = "#{source_path}:#{File.mtime(source_path).to_i}"
83
+ cached = SNAPSHOT_CACHE[cache_key]
84
+ return cached if cached
85
+
86
+ html = run_snapshot(source_path)
87
+ SNAPSHOT_CACHE[cache_key] = html if html
88
+ html
89
+ end
90
+
91
+ private
92
+
93
+ def snapshot_script_path
94
+ return @snapshot_script_path if defined?(@snapshot_script_path)
95
+
96
+ candidate = File.expand_path("../../../bindings/vscode/scripts/snapshot.js", __dir__)
97
+ @snapshot_script_path = File.file?(candidate) ? candidate : nil
98
+ end
99
+
100
+ def snapshot_theme_path
101
+ return @snapshot_theme_path if defined?(@snapshot_theme_path)
102
+
103
+ candidate = File.expand_path("../../../bindings/vscode/themes/2026-dark.json", __dir__)
104
+ @snapshot_theme_path = File.file?(candidate) ? candidate : nil
105
+ end
106
+
107
+ def run_snapshot(source_path)
108
+ script = snapshot_script_path
109
+ theme = snapshot_theme_path
110
+ return nil unless script && theme
111
+
112
+ stdout, stderr, status = Open3.capture3("node", script, source_path, "-t", theme)
113
+
114
+ unless status.success?
115
+ warn("snapshot failed for #{source_path}: #{stderr}")
116
+ return nil
117
+ end
118
+
119
+ stdout
120
+ end
121
+
122
+ private
123
+
124
+ def discover_modules
125
+ mods = []
126
+ root = stdlib_root
127
+ return mods unless File.directory?(root)
128
+
129
+ dir_names = Dir.glob(File.join(root, "*")).filter_map do |entry|
130
+ next unless File.directory?(entry)
131
+ name = File.basename(entry)
132
+ mts = Dir.glob(File.join(entry, "*.mt"))
133
+ next if mts.empty?
134
+ mods << { name:, path: entry, kind: :dir }
135
+ name
136
+ end
137
+
138
+ Dir.glob(File.join(root, "*.mt")).sort.each do |file|
139
+ name = File.basename(file, ".mt")
140
+ next if name.end_with?(".linux", ".windows", ".wasm")
141
+ next if dir_names.include?(name)
142
+ mods << { name:, path: file, kind: :file }
143
+ end
144
+
145
+ mods
146
+ end
147
+
148
+ def extract_doc_comment(source)
149
+ lines = source.lines
150
+ doc_lines = []
151
+ in_doc = false
152
+
153
+ lines.each do |line|
154
+ if line.start_with?("##")
155
+ doc_lines << line.sub(/^## ?/, "").rstrip
156
+ in_doc = true
157
+ elsif in_doc && line.strip.empty?
158
+ break
159
+ elsif in_doc
160
+ break
161
+ end
162
+ end
163
+
164
+ doc_lines.join("\n").strip
165
+ end
166
+
167
+ def extract_declarations(source)
168
+ decls = []
169
+ extend_parent = nil
170
+ extend_indent = nil
171
+ doc_lines = []
172
+
173
+ source.each_line do |line|
174
+ stripped = line.strip
175
+ indent = line[/\A */].length
176
+
177
+ if stripped.start_with?("##")
178
+ doc_lines << stripped.sub(/^## ?/, "").rstrip
179
+ next
180
+ end
181
+
182
+ next if stripped.empty? || stripped.start_with?("#") || stripped.start_with?("import")
183
+
184
+ if extend_parent && indent <= extend_indent
185
+ extend_parent = nil
186
+ extend_indent = nil
187
+ end
188
+
189
+ kind, name = classify_declaration(stripped)
190
+
191
+ unless kind && name
192
+ doc_lines = []
193
+ next
194
+ end
195
+
196
+ if kind == "extend"
197
+ extend_parent = name
198
+ extend_indent = indent
199
+ next
200
+ end
201
+
202
+ doc = doc_lines.empty? ? nil : doc_lines.join("\n")
203
+ doc_lines = []
204
+
205
+ if extend_parent && indent > extend_indent
206
+ decls << { kind:, name:, line: stripped, parent: extend_parent, doc: }
207
+ else
208
+ decls << { kind:, name:, line: stripped, doc: }
209
+ end
210
+ end
211
+ decls
212
+ end
213
+
214
+ def classify_declaration(stripped)
215
+ case stripped
216
+ when /\A(public\s+)?(?:(?:external|foreign|async|const|static|editable)\s+)*function\s+(\w+)/
217
+ ["func", $2]
218
+ when /\A(public\s+)?const\s+(\w+)/
219
+ ["const", $2]
220
+ when /\A(public\s+)?struct\s+(\w+)/
221
+ ["struct", $2]
222
+ when /\A(public\s+)?enum\s+(\w+)/
223
+ ["enum", $2]
224
+ when /\A(public\s+)?flags\s+(\w+)/
225
+ ["flags", $2]
226
+ when /\A(public\s+)?variant\s+(\w+)/
227
+ ["variant", $2]
228
+ when /\A(public\s+)?union\s+(\w+)/
229
+ ["union", $2]
230
+ when /\A(public\s+)?opaque\s+(\w+)/
231
+ ["opaque", $2]
232
+ when /\A(public\s+)?type\s+(\w+)/
233
+ ["type", $2]
234
+ when /\A(public\s+)?interface\s+(\w+)/
235
+ ["interface", $2]
236
+ when /\A(public\s+)?event\s+(\w+)/
237
+ ["event", $2]
238
+ when /\Aextending\s+([^\[:\s]+)/
239
+ ["extend", $1]
240
+ when /\A(public\s+)?attribute\[/
241
+ ["attr", stripped[/attribute\[.*?\]\s+(\w+)/, 1]]
242
+ end
243
+ end
244
+
245
+ def read_module_source(path)
246
+ File.read(path)
247
+ rescue Errno::ENOENT
248
+ nil
249
+ end
250
+
251
+ def module_files(mod)
252
+ if mod[:kind] == :dir
253
+ Dir.glob(File.join(mod[:path], "*.mt")).sort.filter_map do |f|
254
+ basename = File.basename(f, ".mt")
255
+ next if basename.end_with?(".linux", ".windows", ".wasm")
256
+ { name: basename, path: f }
257
+ end
258
+ else
259
+ [{ name: mod[:name], path: mod[:path] }]
260
+ end
261
+ end
262
+
263
+ def resolve_source_path(mod, file_name)
264
+ if mod[:kind] == :dir
265
+ candidate = File.join(mod[:path], "#{file_name}.mt")
266
+ File.file?(candidate) ? candidate : nil
267
+ elsif file_name == mod[:name]
268
+ mod[:path]
269
+ end
270
+ end
271
+ end
272
+
273
+ # ---- Routes ----
274
+
275
+ get "/" do
276
+ content_type :html
277
+ File.read(reference_index)
278
+ end
279
+
280
+ get "/reference" do
281
+ redirect "/reference/"
282
+ end
283
+
284
+ get "/reference/" do
285
+ content_type :html
286
+ File.read(reference_index)
287
+ end
288
+
289
+ get "/stdlib" do
290
+ @breadcrumbs = [["Reference", "/"], ["Standard Library", nil]]
291
+ @modules = stdlib_modules.map do |mod|
292
+ primary = if mod[:kind] == :dir
293
+ File.join(mod[:path], "#{mod[:name]}.mt")
294
+ else
295
+ mod[:path]
296
+ end
297
+ source = read_module_source(primary)
298
+ doc = source ? extract_doc_comment(source) : ""
299
+ decl_count = source ? extract_declarations(source).size : 0
300
+ mod.merge(doc:, decl_count:)
301
+ end
302
+ erb :stdlib
303
+ end
304
+
305
+ get "/stdlib/:name" do
306
+ @name = params[:name]
307
+ mod = stdlib_modules.find { |m| m[:name] == @name }
308
+ halt 404 unless mod
309
+
310
+ @breadcrumbs = [["Reference", "/"], ["Standard Library", "/stdlib"], [@name, nil]]
311
+ @mod = mod
312
+ @files = module_files(mod).filter_map do |f|
313
+ source = read_module_source(f[:path])
314
+ next unless source
315
+ { name: f[:name], doc: extract_doc_comment(source), decls: extract_declarations(source) }
316
+ end
317
+
318
+ erb :module
319
+ end
320
+
321
+ get "/stdlib/:name/source/:file" do
322
+ mod = stdlib_modules.find { |m| m[:name] == params[:name] }
323
+ halt 404 unless mod
324
+
325
+ file_path = resolve_source_path(mod, params[:file])
326
+ halt 404 unless file_path && File.file?(file_path)
327
+ content_type "text/plain"
328
+ File.read(file_path)
329
+ end
330
+
331
+ get "/stdlib/:name/source/:file/hl" do
332
+ mod = stdlib_modules.find { |m| m[:name] == params[:name] }
333
+ halt 404 unless mod
334
+
335
+ file_path = resolve_source_path(mod, params[:file])
336
+ halt 404 unless file_path && File.file?(file_path)
337
+
338
+ html = snapshot_for(file_path)
339
+ unless html
340
+ content_type "text/plain"
341
+ halt 503, "Syntax highlighting unavailable (node.js or snapshot dependencies not found)"
342
+ end
343
+
344
+ content_type :html
345
+ html
346
+ end
347
+
348
+ get "/api/search" do
349
+ content_type :json
350
+ query = (params[:q] || "").downcase.strip
351
+ return "[]" if query.empty? || query.length < 2
352
+
353
+ results = stdlib_modules.filter_map do |mod|
354
+ next unless mod[:name].downcase.include?(query)
355
+
356
+ primary = mod[:kind] == :dir ? File.join(mod[:path], "#{mod[:name]}.mt") : mod[:path]
357
+ source = read_module_source(primary)
358
+ doc_line = source ? extract_doc_comment(source).lines.first&.strip : nil
359
+
360
+ { name: mod[:name], kind: mod[:kind].to_s, summary: doc_line }
361
+ end
362
+ results.to_json
363
+ end
364
+
365
+ error 404 do
366
+ erb :"404"
367
+ end
368
+ end
369
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ module ErrorFormatter
5
+ def self.format(error, path: nil, source: nil, color: true)
6
+ error_message = error.respond_to?(:message) ? error.message : error.to_s
7
+ return error_message unless error.respond_to?(:line) && error.line
8
+ return error_message unless error.respond_to?(:column) && error.column
9
+
10
+ path ||= error.respond_to?(:path) ? error.path : nil
11
+ return error_message unless path
12
+
13
+ line = error.line.to_i
14
+ column = error.column.to_i
15
+ length = error.respond_to?(:length) ? error.length.to_i : 0
16
+ severity = error.respond_to?(:severity) ? error.severity : :error
17
+ code = error.respond_to?(:code) ? error.code : nil
18
+
19
+ source ||= read_source(path)
20
+ return "#{severity_label(severity)}: #{error_message} at #{path}:#{line}:#{column}" unless source
21
+
22
+ source_lines = source.split("\n", -1)
23
+ source_line = source_lines[line - 1] || ""
24
+ stripped = source_line.gsub(/\t/, " ")
25
+
26
+ indent = " " * [column - 1, 0].max
27
+ highlight = if length && length > 1
28
+ "^" + "~" * (length - 1)
29
+ else
30
+ "^"
31
+ end
32
+
33
+ bold = color ? "\e[1m" : ""
34
+ red = color ? "\e[31m" : ""
35
+ yellow = color ? "\e[33m" : ""
36
+ cyan = color ? "\e[36m" : ""
37
+ reset = color ? "\e[0m" : ""
38
+
39
+ sev_color, sev_text = case severity
40
+ when :error then [red, "error"]
41
+ when :warning then [yellow, "warning"]
42
+ when :info then [cyan, "info"]
43
+ when :hint then [cyan, "hint"]
44
+ else [red, "error"]
45
+ end
46
+
47
+ code_text = code ? "#{cyan}[#{code}]#{reset}" : ""
48
+
49
+ [
50
+ "#{bold}#{sev_color}#{sev_text}#{code_text}#{reset}: #{error_message}",
51
+ " #{bold}-->#{reset} #{path}:#{line}:#{column}",
52
+ " #{bold}|#{reset}",
53
+ "#{line.to_s.rjust(5)} #{bold}|#{reset} #{stripped}",
54
+ " #{bold}|#{reset} #{sev_color}#{indent}#{highlight}#{reset}",
55
+ *error_suggestion_lines(error, color, bold, reset),
56
+ ].join("\n")
57
+ end
58
+
59
+ def self.error_suggestion_lines(error, color, bold, reset)
60
+ suggestion = error.respond_to?(:suggestion) ? error.suggestion : nil
61
+ return [] unless suggestion
62
+
63
+ suggestions = suggestion.is_a?(Array) ? suggestion : [suggestion]
64
+ suggestions.map do |s|
65
+ label = color ? "#{bold}= help:#{reset}" : "= help:"
66
+ " #{label} #{s}"
67
+ end
68
+ end
69
+
70
+ def self.read_source(path)
71
+ File.read(path)
72
+ rescue Errno::ENOENT, Errno::EISDIR
73
+ nil
74
+ end
75
+
76
+ def self.severity_label(severity)
77
+ case severity
78
+ when :error then "error"
79
+ when :warning then "warning"
80
+ when :info then "info"
81
+ when :hint then "hint"
82
+ else "error"
83
+ end
84
+ end
85
+ end
86
+ end