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,490 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ module LSP
5
+ class Workspace
6
+ module WorkspaceUtilities
7
+ def shared_module_cache
8
+ @shared_module_cache
9
+ end
10
+
11
+ def workspace_root_path=(path)
12
+ @workspace_root_path = normalize_workspace_root_path(path)
13
+ end
14
+
15
+ def workspace_root_path
16
+ @workspace_root_path
17
+ end
18
+
19
+ def dependency_resolution_mode=(mode)
20
+ normalized = DependencyResolution.normalize_mode(mode)
21
+ return if @dependency_resolution_mode == normalized
22
+
23
+ @dependency_resolution_mode = normalized
24
+ @facts_state_mutex.synchronize do
25
+ @facts_cache_mutex.synchronize do
26
+ @shared_module_cache.clear
27
+ @facts_cache.clear
28
+ @tooling_snapshot_cache.clear
29
+ @diagnostics_cache.clear
30
+ @last_good_facts_cache.clear
31
+ @last_good_tooling_snapshot_cache.clear
32
+ clear_dependency_index
33
+ end
34
+ end
35
+ end
36
+
37
+ def dependency_resolution_mode
38
+ @dependency_resolution_mode
39
+ end
40
+
41
+ def platform_override=(platform)
42
+ normalized = platform.nil? ? nil : ModuleLoader.normalize_platform_name(platform)
43
+ return if @platform_override == normalized
44
+
45
+ @platform_override = normalized
46
+ @facts_state_mutex.synchronize do
47
+ @facts_cache_mutex.synchronize do
48
+ @shared_module_cache.clear
49
+ @facts_cache.clear
50
+ @tooling_snapshot_cache.clear
51
+ @diagnostics_cache.clear
52
+ @last_good_facts_cache.clear
53
+ @last_good_tooling_snapshot_cache.clear
54
+ clear_dependency_index
55
+ end
56
+ end
57
+ end
58
+
59
+ def platform_override
60
+ @platform_override
61
+ end
62
+
63
+ def strict_current_root_diagnostics_enabled=(enabled)
64
+ normalized = !!enabled
65
+ return if @strict_current_root_diagnostics_enabled == normalized
66
+
67
+ @strict_current_root_diagnostics_enabled = normalized
68
+ @facts_state_mutex.synchronize do
69
+ @facts_cache_mutex.synchronize do
70
+ @diagnostics_cache.clear
71
+ end
72
+ end
73
+ end
74
+
75
+ def strict_current_root_diagnostics_enabled
76
+ @strict_current_root_diagnostics_enabled
77
+ end
78
+
79
+ # Scan text up to the cursor to find the innermost open function call context.
80
+ # Returns { name:, active_parameter: } or nil if not inside a call.
81
+ def find_call_context(uri, lsp_line, lsp_char)
82
+ content = get_content(uri)
83
+ return nil if content.empty?
84
+
85
+ lines = content.split("\n", -1)
86
+ cursor_line = lines[lsp_line] || ''
87
+ prefix = lsp_line.positive? ? lines[0...lsp_line].join("\n") + "\n" : ''
88
+ text = prefix + cursor_line[0...lsp_char]
89
+
90
+ depth = 0
91
+ active_param = 0
92
+ i = text.length - 1
93
+ paren_pos = nil
94
+
95
+ while i >= 0
96
+ ch = text[i]
97
+ case ch
98
+ when ')', ']'
99
+ depth += 1
100
+ when '['
101
+ return nil if depth.zero?
102
+ depth -= 1
103
+ when '('
104
+ if depth.zero?
105
+ paren_pos = i
106
+ break
107
+ end
108
+ depth -= 1
109
+ when ','
110
+ active_param += 1 if depth.zero?
111
+ end
112
+ i -= 1
113
+ end
114
+
115
+ return nil unless paren_pos
116
+
117
+ # Find identifier immediately before the '('
118
+ j = paren_pos - 1
119
+ j -= 1 while j >= 0 && (text[j] == ' ' || text[j] == "\t")
120
+ return nil if j < 0 || text[j] !~ /[A-Za-z0-9_]/
121
+
122
+ end_j = j
123
+ j -= 1 while j > 0 && text[j - 1] =~ /[A-Za-z0-9_]/
124
+ name = text[j..end_j]
125
+ return nil if name.empty?
126
+
127
+ { name: name, active_parameter: active_param }
128
+ rescue StandardError => e
129
+ warn "LSP call context error #{uri}: #{e.message}"
130
+ nil
131
+ end
132
+
133
+ # ── Position helpers ────────────────────────────────────────────────────
134
+
135
+ # Find the identifier/keyword token under the cursor.
136
+ # lsp_line and lsp_character are 0-based (LSP convention).
137
+ def find_token_at(uri, lsp_line, lsp_character)
138
+ tokens = get_tokens(uri)
139
+ return nil if tokens.nil?
140
+
141
+ # Tokens use 1-based line/column
142
+ target_line = lsp_line + 1
143
+ target_char = lsp_character + 1
144
+
145
+ tokens.find do |tok|
146
+ next false if [:newline, :indent, :dedent, :eof].include?(tok.type)
147
+
148
+ token_contains_position?(tok, target_line, target_char)
149
+ end
150
+ end
151
+
152
+ def token_contains_position?(token, target_line, target_char)
153
+ segments = token.lexeme.split("\n", -1)
154
+ end_line = token.line + segments.length - 1
155
+ return false if target_line < token.line || target_line > end_line
156
+
157
+ if segments.length == 1
158
+ return token.column <= target_char && target_char < (token.column + segments.first.length)
159
+ end
160
+
161
+ if target_line == token.line
162
+ return token.column <= target_char && target_char <= (token.column + segments.first.length - 1)
163
+ end
164
+
165
+ target_char <= segments.fetch(target_line - token.line).length
166
+ end
167
+
168
+ # Returns the receiver name before '.' if the cursor is in a dot-access
169
+ # context, e.g. for "vec.len|" returns "vec". Returns nil otherwise.
170
+ # lsp_char is the 0-based cursor character position (LSP convention).
171
+ def find_dot_receiver(uri, lsp_line, lsp_char)
172
+ receiver_path = find_dot_receiver_path(uri, lsp_line, lsp_char)
173
+ return nil unless receiver_path
174
+
175
+ receiver_path.split('.').last
176
+ rescue StandardError
177
+ nil
178
+ end
179
+
180
+ def find_dot_receiver_path(uri, lsp_line, lsp_char)
181
+ content = get_content(uri)
182
+ lines = content.split("\n", -1)
183
+ line_str = lines[lsp_line] || ''
184
+
185
+ idx = [lsp_char - 1, line_str.length - 1].min
186
+
187
+ # Walk back past the hovered identifier to its dot
188
+ idx -= 1 while idx >= 0 && line_str[idx] =~ /[A-Za-z0-9_]/
189
+ return nil if idx < 0 || line_str[idx] != '.'
190
+
191
+ segments = []
192
+ left = idx - 1
193
+ loop do
194
+ # Skip brackets and their contents, then collect the identifier before them
195
+ if line_str[left] == ']'
196
+ depth = 1
197
+ left -= 1
198
+ while left >= 0 && depth > 0
199
+ depth += 1 if line_str[left] == ']'
200
+ depth -= 1 if line_str[left] == '['
201
+ left -= 1 if depth > 0 && left > 0
202
+ end
203
+ left -= 1 # skip past opening bracket
204
+ end
205
+
206
+ # Walk back over identifier chars
207
+ start = left
208
+ start -= 1 while start >= 0 && line_str[start] =~ /[A-Za-z0-9_]/
209
+ segments.unshift(line_str[start + 1..left]) if start < left
210
+
211
+ break unless start >= 0 && line_str[start] == '.'
212
+
213
+ left = start - 1
214
+ end
215
+
216
+ segments.any? ? segments.join('.') : nil
217
+ rescue StandardError
218
+ nil
219
+ end
220
+
221
+ # Find the name identifier token immediately after a definition keyword
222
+ # (def, struct, union, enum, flags, variant, type, const, var) for the given name.
223
+ # Returns the identifier Token, or nil if not found.
224
+ def find_definition_token(uri, name, before_line: nil, before_char: nil)
225
+ tokens = get_tokens(uri)
226
+ return nil if tokens.nil?
227
+
228
+ nearest = nil
229
+ tokens.each_cons(2) do |kw_tok, id_tok|
230
+ next unless DEFINITION_KEYWORDS.include?(kw_tok.type)
231
+ next unless id_tok.type == :identifier && id_tok.lexeme == name
232
+
233
+ if before_line
234
+ next if id_tok.line > before_line
235
+ next if id_tok.line == before_line && before_char && id_tok.column >= before_char
236
+ end
237
+
238
+ if nearest.nil? || id_tok.line > nearest.line || (id_tok.line == nearest.line && id_tok.column > nearest.column)
239
+ nearest = id_tok
240
+ end
241
+ end
242
+
243
+ return nearest if nearest
244
+
245
+ tokens.each_cons(2) do |kw_tok, id_tok|
246
+ next unless DEFINITION_KEYWORDS.include?(kw_tok.type)
247
+ next unless id_tok.type == :identifier && id_tok.lexeme == name
248
+
249
+ return id_tok
250
+ end
251
+ nil
252
+ end
253
+
254
+ private
255
+
256
+ # ── Symbol extraction (token-based, no AST position requirement) ────────
257
+
258
+ def extract_symbols_from_tokens(uri)
259
+ tokens = get_tokens(uri)
260
+ return [] if tokens.nil?
261
+
262
+ symbols = []
263
+ tokens.each_cons(2) do |kw_tok, id_tok|
264
+ next unless DEFINITION_KEYWORDS.include?(kw_tok.type)
265
+ next unless id_tok.type == :identifier
266
+
267
+ kind = case kw_tok.type
268
+ when :function then 'function'
269
+ when :struct then 'struct'
270
+ when :union then 'union'
271
+ when :enum then 'enum'
272
+ when :flags then 'flags'
273
+ when :variant then 'variant'
274
+ when :type then 'type_alias'
275
+ when :const then 'constant'
276
+ when :let then 'variable'
277
+ when :var then 'variable'
278
+ when :event then 'event'
279
+ when :extending then 'struct'
280
+ when :opaque then 'struct'
281
+ when :interface then 'interface'
282
+ end
283
+
284
+ symbols << {
285
+ name: id_tok.lexeme,
286
+ kind: kind,
287
+ line: id_tok.line,
288
+ column: id_tok.column
289
+ } unless kind == 'variable' && id_tok.lexeme == '_'
290
+ end
291
+ symbols
292
+ end
293
+
294
+ def file_backed_source_overrides
295
+ open_documents = @document_state_mutex.synchronize do
296
+ @open_documents.dup
297
+ end
298
+
299
+ open_documents.each_with_object({}) do |(uri, content), overrides|
300
+ path = uri_to_path(uri)
301
+ next unless path
302
+
303
+ overrides[File.expand_path(path)] = content
304
+ end
305
+ end
306
+
307
+ def extract_doc_comments_for_definitions(uri)
308
+ content = get_content(uri)
309
+ return {} if content.empty?
310
+
311
+ tokens = get_tokens(uri)
312
+ return {} if tokens.nil?
313
+
314
+ lines = content.split("\n", -1)
315
+ docs_by_location = {}
316
+
317
+ tokens.each_cons(2) do |kw_tok, id_tok|
318
+ next unless DEFINITION_KEYWORDS.include?(kw_tok.type)
319
+ next unless id_tok.type == :identifier
320
+
321
+ docs = extract_doc_comment_for_line(lines, id_tok.line - 1)
322
+ next unless docs
323
+
324
+ docs_by_location[doc_comment_key(id_tok.line, id_tok.column)] = docs
325
+ end
326
+
327
+ docs_by_location
328
+ end
329
+
330
+ def extract_doc_comment_for_line(lines, declaration_line)
331
+ index = declaration_line - 1
332
+ return nil if index.negative?
333
+
334
+ docs = []
335
+ while index >= 0
336
+ stripped = lines[index].to_s.strip
337
+ break if stripped.empty?
338
+ break unless stripped.start_with?(DOC_COMMENT_PREFIX)
339
+
340
+ docs << {
341
+ line: index + 1,
342
+ text: stripped.sub(/\A##\s?/, ''),
343
+ }
344
+ index -= 1
345
+ end
346
+
347
+ return nil if docs.empty?
348
+
349
+ build_doc_comment_data(docs.reverse)
350
+ end
351
+
352
+ def build_doc_comment_data(lines)
353
+ raw_lines = lines.map { |entry| entry[:text].to_s }
354
+ raw_markdown = raw_lines.join("\n")
355
+
356
+ tags = {
357
+ params: [],
358
+ returns: nil,
359
+ throws: [],
360
+ see: [],
361
+ }
362
+ tag_errors = []
363
+ body_lines = []
364
+
365
+ lines.each do |entry|
366
+ line_text = entry[:text].to_s
367
+ match = line_text.match(DOC_TAG_PATTERN)
368
+ unless match
369
+ body_lines << line_text
370
+ next
371
+ end
372
+
373
+ tag_name = match[1].to_s.downcase
374
+ payload = match[2].to_s.strip
375
+
376
+ case tag_name
377
+ when 'param'
378
+ if payload.empty?
379
+ tag_errors << {
380
+ line: entry[:line],
381
+ message: 'doc tag @param requires a parameter name',
382
+ }
383
+ next
384
+ end
385
+
386
+ name_match = payload.match(/\A([A-Za-z_][A-Za-z0-9_]*)(?:\s+(.*))?\z/)
387
+ unless name_match
388
+ tag_errors << {
389
+ line: entry[:line],
390
+ message: 'doc tag @param has an invalid parameter name',
391
+ }
392
+ next
393
+ end
394
+
395
+ tags[:params] << {
396
+ name: name_match[1],
397
+ text: name_match[2].to_s.strip,
398
+ line: entry[:line],
399
+ }
400
+ when 'return', 'returns'
401
+ tags[:returns] = {
402
+ text: payload,
403
+ line: entry[:line],
404
+ }
405
+ when 'throws', 'throw'
406
+ tags[:throws] << {
407
+ text: payload,
408
+ line: entry[:line],
409
+ }
410
+ when 'see'
411
+ tags[:see] << {
412
+ text: payload,
413
+ line: entry[:line],
414
+ }
415
+ else
416
+ tag_errors << {
417
+ line: entry[:line],
418
+ message: "unknown doc tag @#{tag_name}",
419
+ }
420
+ end
421
+ end
422
+
423
+ body_markdown = body_lines.join("\n").strip
424
+ summary_lines = body_markdown.split("\n").slice_before { |line| line.strip.empty? }.first || []
425
+ summary = summary_lines.join("\n").strip
426
+ summary = nil if summary.empty?
427
+
428
+ {
429
+ raw_markdown: raw_markdown,
430
+ summary: summary,
431
+ body_markdown: body_markdown,
432
+ tags: tags,
433
+ tag_errors: tag_errors,
434
+ }
435
+ end
436
+
437
+ def doc_comment_key(line, column)
438
+ "#{line}:#{column}"
439
+ end
440
+
441
+ # ── Offset utilities ────────────────────────────────────────────────────
442
+
443
+ # Convert a 0-based LSP (line, character) pair into a byte offset.
444
+ # LSP positions use UTF-16 code units for +character+.
445
+ def line_char_to_offset(content, line, char)
446
+ lines = content.split("\n", -1)
447
+ clamped_line = [[line.to_i, 0].max, lines.length - 1].min
448
+
449
+ preceding = if clamped_line.zero?
450
+ ''
451
+ else
452
+ lines[0...clamped_line].join("\n") + "\n"
453
+ end
454
+
455
+ line_text = lines[clamped_line] || ''
456
+ target_units = [char.to_i, 0].max
457
+
458
+ utf16_units_seen = 0
459
+ byte_index = 0
460
+ line_text.each_char do |ch|
461
+ codepoint = ch.ord
462
+ units = codepoint > 0xFFFF ? 2 : 1
463
+ break if utf16_units_seen + units > target_units
464
+
465
+ utf16_units_seen += units
466
+ byte_index += ch.bytesize
467
+ end
468
+
469
+ within_line = line_text.byteslice(0, byte_index).to_s
470
+
471
+ (preceding + within_line).bytesize
472
+ end
473
+
474
+ def uri_to_path(uri)
475
+ parsed = URI.parse(uri)
476
+ return nil unless parsed.scheme == 'file'
477
+
478
+ CGI.unescape(parsed.path)
479
+ rescue URI::InvalidURIError
480
+ nil
481
+ end
482
+
483
+ def path_to_uri(path)
484
+ escaped_path = path.split('/').map { |seg| CGI.escape(seg).gsub('+', '%20') }.join('/')
485
+ "file://#{escaped_path}"
486
+ end
487
+ end
488
+ end
489
+ end
490
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi/escape'
4
+ require 'set'
5
+ require 'thread'
6
+ require 'uri'
7
+
8
+ require_relative 'workspace/store'
9
+ require_relative 'workspace/caches'
10
+ require_relative 'workspace/analysis'
11
+ require_relative 'workspace/dependency_graph'
12
+ require_relative 'workspace/definition_index'
13
+ require_relative 'workspace/collection'
14
+ require_relative 'workspace/utilities'
15
+
16
+ module MilkTea
17
+ module LSP
18
+ # Manages open documents, AST cache, token cache, semantic facts cache, and symbol index.
19
+ # Supports incremental document edits and workspace-wide indexing.
20
+ class Workspace
21
+ DOCUMENT_SOURCES = %w[active-editor visible-editor background-document].freeze
22
+ PERF_LOG_THRESHOLD_MS = 1000
23
+
24
+ # Token types that introduce a named definition, in order of precedence.
25
+ #
26
+ # NOTE: this list is intentionally minimal. Multi-keyword prefixes such as
27
+ # 'const function', 'async function', 'external function', 'foreign function',
28
+ # and visibility modifiers ('public') are captured by the adjacent-token scan
29
+ # in extract_symbols_from_tokens because the scan relies on every pair of
30
+ # adjacent tokens, and the inner keyword (e.g. :function) is always followed
31
+ # by the identifier. If the lexer ever merges a compound keyword into a
32
+ # single token type (e.g. :const_function), it must be added here.
33
+ DEFINITION_KEYWORDS = %i[function struct union enum flags variant type const var let extending opaque interface event].freeze
34
+ DOC_COMMENT_PREFIX = '##'
35
+ DOC_TAG_PATTERN = /\A\s*@([A-Za-z_][A-Za-z0-9_-]*)(?:\s+(.*))?\z/
36
+ DEFINITION_LINE_PREFIX = /^(?:\s)*(?:(?:public|foreign|external)\s+)*(?:function|struct|union|enum|flags|variant|type|const|var|let|extending|opaque|interface|event)\s+/m
37
+ DEFINITION_NAME_REGEX = /^\s*(?:(?:public|foreign|external)\s+)*(?:function|struct|union|enum|flags|variant|type|const|var|let|extending|opaque|interface|event)\s+([A-Za-z_][A-Za-z0-9_]*)\b/
38
+
39
+ def initialize
40
+ @workspace_root_path = nil
41
+ @dependency_resolution_mode = :auto
42
+ @platform_override = nil
43
+ @strict_current_root_diagnostics_enabled = false
44
+ @open_documents = {} # uri -> content String from didOpen/didChange
45
+ @indexed_documents = {} # uri -> content String loaded from disk index
46
+ @document_sources = {} # uri -> source string from the editor client
47
+ @document_state_mutex = Mutex.new
48
+ @tokens_cache = {} # uri -> [Token]
49
+ @last_good_tokens_cache = {} # uri -> last known-good [Token]
50
+ @ast_cache = {} # uri -> AST::SourceFile (nil on parse failure)
51
+ @facts_cache = {} # uri -> SemanticAnalyzer::Facts (projection of cached tooling snapshot facts)
52
+ @tooling_snapshot_cache = {} # uri -> SemanticAnalyzer::ToolingSnapshot (facts may be nil on structural failure)
53
+ @symbols_cache = {} # uri -> [{name, kind, line, column}]
54
+ @doc_comments_cache = {} # uri -> {"line:column" => structured_doc_comment_hash}
55
+ @last_good_facts_cache = {} # uri -> last SemanticAnalyzer::Facts that succeeded
56
+ @last_good_tooling_snapshot_cache = {} # uri -> last SemanticAnalyzer::ToolingSnapshot with facts that succeeded
57
+ @document_module_names = {} # uri -> module name string (populated from last-good facts)
58
+ @shared_module_cache = {}
59
+ @facts_cache_mutex = Mutex.new
60
+ @facts_generation = Hash.new(0)
61
+ @facts_state_mutex = Mutex.new
62
+ # Diagnostics cache: uri -> { content_hash:, diagnostics: }
63
+ @diagnostics_cache = {}
64
+ @dependency_module_name_by_uri = {}
65
+ @dependency_imports_by_uri = {}
66
+ @reverse_import_dependents = Hash.new { |hash, key| hash[key] = Set.new }
67
+ @full_reverse_index_built = false
68
+ @full_reverse_index_built = false
69
+ # Definition index: name -> { uri:, token: } — built lazily from symbols cache.
70
+ # Caches known matching definitions without forcing a full-workspace index
71
+ # build on the first global lookup.
72
+ @definition_index = {} # name -> [{ uri:, token: Token }]
73
+ @definition_miss_cache = Set.new
74
+ @definition_candidate_uris = Hash.new { |hash, key| hash[key] = Set.new }
75
+ @definition_names_by_uri = {}
76
+ @definition_cache_mutex = Mutex.new
77
+ @definition_warmup_queue = Queue.new
78
+ @definition_warmup_enqueued = Set.new
79
+ @definition_warmup_thread = nil
80
+ # Identifier index: name -> [{uri:, line:, col:}] — lazily populated from tokens
81
+ @identifier_index = {}
82
+ @identifier_index_mutex = Mutex.new
83
+ @indexed_uris = Set.new
84
+ end
85
+
86
+ def reset
87
+ @workspace_root_path = nil
88
+ @dependency_resolution_mode = :auto
89
+ @platform_override = nil
90
+ @strict_current_root_diagnostics_enabled = false
91
+ @indexed_documents.clear
92
+ @tokens_cache.clear
93
+ @last_good_tokens_cache.clear
94
+ @ast_cache.clear
95
+ @facts_cache.clear
96
+ @tooling_snapshot_cache.clear
97
+ @symbols_cache.clear
98
+ @doc_comments_cache.clear
99
+ @last_good_facts_cache.clear
100
+ @last_good_tooling_snapshot_cache.clear
101
+ @document_module_names.clear
102
+ @shared_module_cache.clear
103
+ @definition_index.clear
104
+ @identifier_index.clear
105
+ @full_reverse_index_built = false
106
+ @indexed_uris.clear
107
+ @definition_miss_cache.clear
108
+ @definition_candidate_uris.clear
109
+ @definition_names_by_uri.clear
110
+ @definition_warmup_enqueued.clear
111
+ @dependency_module_name_by_uri.clear
112
+ @dependency_imports_by_uri.clear
113
+ @reverse_import_dependents.clear
114
+ @diagnostics_cache.clear
115
+ @facts_generation.clear
116
+ end
117
+
118
+ include WorkspaceStore
119
+ include WorkspaceCaches
120
+ include WorkspaceAnalysis
121
+ include WorkspaceDependencyGraph
122
+ include WorkspaceDefinitionIndex
123
+ include WorkspaceCollection
124
+ include WorkspaceUtilities
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ module LSP
5
+ # LSP implementation - language server protocol support
6
+ end
7
+ end
8
+
9
+ require_relative 'lsp/protocol'
10
+ require_relative 'lsp/dependency_resolution'
11
+ require_relative 'lsp/workspace'
12
+ require_relative 'lsp/diagnostics'
13
+ require_relative 'lsp/server'
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tempfile"
5
+
6
+ module MilkTea
7
+ module PackageAtomicWrite
8
+ module_function
9
+
10
+ def write(path, content, binmode: false)
11
+ open(path, binmode:) do |file|
12
+ file.write(content)
13
+ end
14
+ end
15
+
16
+ def open(path, binmode: false)
17
+ expanded_path = File.expand_path(path)
18
+ directory = File.dirname(expanded_path)
19
+
20
+ FileUtils.mkdir_p(directory)
21
+
22
+ Tempfile.create(["milk-tea-package", File.extname(expanded_path)], directory) do |file|
23
+ file.binmode if binmode
24
+ yield file
25
+
26
+ unless file.closed?
27
+ file.flush
28
+ file.fsync
29
+ file.close
30
+ end
31
+
32
+ replace(file.path, expanded_path)
33
+ end
34
+
35
+ expanded_path
36
+ end
37
+
38
+ def replace(source_path, destination_path)
39
+ File.rename(source_path, destination_path)
40
+ rescue Errno::EACCES, Errno::EEXIST, Errno::EPERM
41
+ replace_via_backup(source_path, destination_path)
42
+ end
43
+
44
+ def replace_via_backup(source_path, destination_path)
45
+ backup_path = "#{destination_path}.bak.#{$$}.#{Thread.current.object_id}"
46
+
47
+ File.rename(destination_path, backup_path) if File.exist?(destination_path)
48
+
49
+ begin
50
+ File.rename(source_path, destination_path)
51
+ rescue SystemCallError
52
+ if File.exist?(backup_path) && !File.exist?(destination_path)
53
+ File.rename(backup_path, destination_path)
54
+ end
55
+ raise
56
+ end
57
+
58
+ File.delete(backup_path) if File.exist?(backup_path)
59
+ rescue Errno::ENOENT
60
+ nil
61
+ end
62
+ end
63
+ end