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,611 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi/escape'
4
+ require 'uri'
5
+
6
+ module MilkTea
7
+ module LSP
8
+ # Collects parse and semantic errors and formats them as LSP Diagnostics
9
+ class Diagnostics
10
+ class << self
11
+ attr_accessor :protocol, :position_encoding
12
+ end
13
+ @position_encoding = 'utf-16'
14
+
15
+ def self.collect(uri, content, shared_module_cache: nil, source_overrides: nil, workspace_root_path: nil, dependency_resolution_mode: :auto, platform_override: nil, sema_snapshot: nil, strict_current_root_diagnostics: false, lint_tier: :full)
16
+ total_start = perf_logging? ? monotonic_time : nil
17
+ diagnostics = []
18
+ sema_facts = nil
19
+ unresolved_import_paths = []
20
+ normalized_lint_tier = Linter.normalize_lint_tier(lint_tier)
21
+ parse_ms = 0.0
22
+ imports_ms = 0.0
23
+ sema_ms = 0.0
24
+ strict_root_ms = 0.0
25
+ lint_ms = 0.0
26
+ lint_profile = total_start ? Linter::Profile.new : nil
27
+ path = uri_to_path(uri)
28
+ resolution = DependencyResolution.resolve(path, mode: dependency_resolution_mode)
29
+ effective_platform = effective_platform_for_path(path, platform_override:)
30
+
31
+ # Parse
32
+ begin
33
+ parse_start = total_start ? monotonic_time : nil
34
+ ast = if path && File.file?(path)
35
+ parse_result = Parser.parse_collecting_errors(content, path: uri)
36
+ parse_result.errors.each { |error| diagnostics << format_error(error) }
37
+ parse_result.ast
38
+ else
39
+ Parser.parse(content, path: uri)
40
+ end
41
+ parse_ms = elapsed_ms(parse_start) if parse_start
42
+ return { diagnostics: diagnostics, facts: sema_facts, sema_snapshot: sema_snapshot } unless ast
43
+
44
+ if resolution.error_message
45
+ diagnostics << format_error(SemanticError.new(resolution.error_message, line: 1, column: 1))
46
+ return { diagnostics: diagnostics, facts: sema_facts, sema_snapshot: sema_snapshot }
47
+ end
48
+
49
+ conflict_error = root_platform_conflict_error(path, effective_platform)
50
+ if conflict_error
51
+ diagnostics << format_error(SemanticError.new(conflict_error.message, line: 1, column: 1))
52
+ return { diagnostics: diagnostics, facts: sema_facts, sema_snapshot: sema_snapshot }
53
+ end
54
+
55
+ imports_start = total_start ? monotonic_time : nil
56
+ imported_modules = resolve_imported_modules(
57
+ uri,
58
+ ast,
59
+ diagnostics,
60
+ resolution:,
61
+ effective_platform:,
62
+ shared_module_cache: shared_module_cache,
63
+ source_overrides: source_overrides,
64
+ workspace_root_path: workspace_root_path,
65
+ content: content,
66
+ )
67
+ imports_ms = elapsed_ms(imports_start) if imports_start
68
+ unresolved_import_paths = imported_modules.fetch(:unresolved_import_paths)
69
+ inferred_module_name = imported_modules[:module_name]
70
+
71
+ # Wrap AST with module name before sema so facts.module_name is non-nil.
72
+ unless ast.respond_to?(:module_name) && ast.module_name
73
+ mn_parts = inferred_module_name ? inferred_module_name.split('.') : []
74
+ ast = AST::SourceFile.new(
75
+ module_name: mn_parts.any? ? AST::QualifiedName.new(parts: mn_parts) : nil,
76
+ module_kind: ast.respond_to?(:module_kind) ? ast.module_kind : nil,
77
+ imports: ast.respond_to?(:imports) ? (ast.imports || []) : [],
78
+ directives: ast.respond_to?(:directives) ? (ast.directives || []) : [],
79
+ declarations: ast.respond_to?(:declarations) ? (ast.declarations || []) : [],
80
+ line: ast.respond_to?(:line) ? ast.line : 1,
81
+ node_ids: ast.respond_to?(:node_ids) ? ast.node_ids : {},
82
+ )
83
+ end
84
+
85
+ # Semantic analysis — collect errors from all functions, not just first.
86
+ begin
87
+ sema_start = total_start ? monotonic_time : nil
88
+ sema_snapshot ||= SemanticAnalyzer.tooling_snapshot(ast, imported_modules: imported_modules.fetch(:modules), path: path, allow_missing_imports: true)
89
+ sema_facts = sema_snapshot.facts
90
+ sema_snapshot.diagnostics.reject { |diagnostic| redundant_unknown_import_diagnostic?(diagnostic, unresolved_import_paths) }
91
+ .each { |diagnostic| diagnostics << format_tooling_diagnostic(diagnostic, stage: 'sema') }
92
+ sema_ms = elapsed_ms(sema_start) if sema_start
93
+ rescue StandardError => e
94
+ log_diagnostics_warning("Error collecting diagnostics: #{e.message}")
95
+ end
96
+
97
+ begin
98
+ if strict_current_root_diagnostics && normalized_lint_tier == :full
99
+ strict_root_start = total_start ? monotonic_time : nil
100
+ collect_strict_root_diagnostics(
101
+ path,
102
+ ast,
103
+ sema_facts,
104
+ diagnostics,
105
+ resolution:,
106
+ effective_platform:,
107
+ shared_module_cache:,
108
+ source_overrides:,
109
+ workspace_root_path:,
110
+ ).each do |diagnostic|
111
+ diagnostics << diagnostic
112
+ end
113
+ strict_root_ms = elapsed_ms(strict_root_start) if strict_root_start
114
+ end
115
+ rescue StandardError => e
116
+ log_diagnostics_warning("Error collecting strict root diagnostics: #{e.message}")
117
+ end
118
+ rescue MilkTea::LexError => e
119
+ diagnostics << format_error(e)
120
+ rescue StandardError => e
121
+ log_diagnostics_warning("Error collecting diagnostics: #{e.message}")
122
+ end
123
+
124
+ # Lint warnings (severity: 2 = Warning)
125
+ begin
126
+ lint_start = total_start ? monotonic_time : nil
127
+ Linter.lint_source(
128
+ content,
129
+ path: uri,
130
+ sema_facts: sema_facts,
131
+ unresolved_import_paths: unresolved_import_paths,
132
+ profile: lint_profile,
133
+ lint_tier: normalized_lint_tier,
134
+ ).each do |w|
135
+ diagnostics << format_warning(w, content: content)
136
+ end
137
+ lint_ms = elapsed_ms(lint_start) if lint_start
138
+ rescue MilkTea::LexError, MilkTea::ParseError
139
+ # Best-effort only while the user is mid-edit; lex/parse failures are
140
+ # already reported by the main diagnostics path.
141
+ rescue StandardError => e
142
+ log_diagnostics_warning("Error collecting lint diagnostics: #{e.message}")
143
+ end
144
+
145
+ { diagnostics: diagnostics, facts: sema_facts, sema_snapshot: sema_snapshot }
146
+ ensure
147
+ if total_start
148
+ lint_breakdown = lint_profile&.summary(limit: 12)
149
+ detail = "uri=#{uri} lint_tier=#{normalized_lint_tier} diagnostics=#{diagnostics.length} facts=#{sema_facts ? 'ok' : 'nil'} stages_ms=parse:#{parse_ms},imports:#{imports_ms},sema:#{sema_ms},strict_root:#{strict_root_ms},lint:#{lint_ms}"
150
+ detail += " lint_passes_ms=#{lint_breakdown}" unless lint_breakdown.nil? || lint_breakdown.empty?
151
+ log_perf_breakdown(
152
+ 'diagnostics.collect',
153
+ elapsed_ms(total_start),
154
+ detail,
155
+ )
156
+ end
157
+ end
158
+
159
+ private
160
+
161
+ def self.resolve_imported_modules(uri, ast, diagnostics, resolution:, effective_platform:, shared_module_cache: nil, source_overrides: nil, workspace_root_path: nil, content: nil)
162
+ path = uri_to_path(uri)
163
+ return { modules: {}, unresolved_import_paths: [], module_name: nil } unless path && File.file?(path)
164
+
165
+ loader = ModuleLoader.new(
166
+ module_roots: module_roots_for_path(path, locked: resolution.locked, workspace_root_path: workspace_root_path),
167
+ package_graph: load_package_graph(path, locked: resolution.locked),
168
+ shared_cache: shared_module_cache,
169
+ source_overrides: source_overrides,
170
+ platform: effective_platform,
171
+ )
172
+ module_name = loader.send(:inferred_module_name_for_path, path) rescue nil
173
+ resolution_result = loader.imported_modules_for_ast_collecting_errors(ast, importer_path: path)
174
+ unresolved_import_paths = []
175
+
176
+ resolution_result.errors.each do |entry|
177
+ if entry.error.is_a?(ModuleLoadError)
178
+ diagnostics << format_import_error(entry.error, entry.import, content: content)
179
+ unresolved_import_paths << entry.import.path.to_s if entry.import
180
+ else
181
+ diagnostics << format_error(SemanticError.new(entry.error.message))
182
+ end
183
+ end
184
+
185
+ { modules: resolution_result.modules, unresolved_import_paths: unresolved_import_paths.uniq, module_name: module_name }
186
+ rescue ModuleLoadError, PackageLockError => e
187
+ if e.is_a?(ModuleLoadError)
188
+ import = ast.imports.find { |candidate| candidate.path.to_s == e.path }
189
+ diagnostics << format_import_error(e, import, content: content)
190
+ else
191
+ diagnostics << format_error(SemanticError.new(e.message))
192
+ end
193
+ { modules: {}, unresolved_import_paths: [], module_name: nil }
194
+ end
195
+
196
+ def self.redundant_unknown_import_diagnostic?(diagnostic, unresolved_import_paths)
197
+ return false unless diagnostic&.message.to_s.start_with?("unknown import ")
198
+
199
+ unresolved_import_paths.include?(diagnostic.message.delete_prefix("unknown import "))
200
+ end
201
+
202
+ def self.uri_to_path(uri)
203
+ parsed = URI.parse(uri)
204
+ return nil unless parsed.scheme == "file"
205
+
206
+ CGI.unescape(parsed.path)
207
+ rescue URI::InvalidURIError
208
+ nil
209
+ end
210
+
211
+ def self.perf_logging?
212
+ @perf_logging ||= !ENV.fetch('MILK_TEA_LSP_PERF', nil).to_s.empty?
213
+ end
214
+
215
+ def self.perf_verbose?
216
+ @perf_verbose ||= ENV.fetch('MILK_TEA_LSP_PERF', nil).to_s == 'verbose'
217
+ end
218
+
219
+ def self.perf_breakdown_logging?(elapsed_ms_value)
220
+ perf_logging? && (perf_verbose? || elapsed_ms_value > Workspace::PERF_LOG_THRESHOLD_MS)
221
+ end
222
+
223
+ def self.monotonic_time
224
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
225
+ end
226
+
227
+ def self.elapsed_ms(start_time)
228
+ ((monotonic_time - start_time) * 1000).round(1)
229
+ end
230
+
231
+ def self.log_perf_breakdown(name, elapsed_ms_value, detail)
232
+ return unless perf_breakdown_logging?(elapsed_ms_value)
233
+
234
+ warn "[LSP perf] breakdown #{name} #{elapsed_ms_value}ms #{detail}"
235
+ end
236
+
237
+ def self.load_package_graph(path, locked: false)
238
+ @pkg_graph_cache ||= {}
239
+ cache_key = [path, locked]
240
+ return @pkg_graph_cache[cache_key] if @pkg_graph_cache.key?(cache_key)
241
+
242
+ @pkg_graph_cache[cache_key] = PackageGraph.load(path, locked:)
243
+ rescue PackageManifestError
244
+ @pkg_graph_cache[cache_key] = nil
245
+ end
246
+ private_class_method :load_package_graph
247
+
248
+ def self.effective_platform_for_path(path, platform_override: nil)
249
+ return nil unless path
250
+
251
+ ModuleLoader.effective_platform_for_path(path, platform_override:)
252
+ end
253
+ private_class_method :effective_platform_for_path
254
+
255
+ def self.root_platform_conflict_error(path, effective_platform)
256
+ return nil unless path && File.file?(path)
257
+ return nil unless ModuleLoader.platform_suffix_for_path(path)
258
+
259
+ ModuleLoader.resolve_source_path(path, platform: effective_platform, error_class: ModuleLoadError)
260
+ nil
261
+ rescue ModuleLoadError => e
262
+ e
263
+ end
264
+ private_class_method :root_platform_conflict_error
265
+
266
+ def self.collect_strict_root_diagnostics(path, ast, sema_facts, existing_diagnostics, resolution:, effective_platform:, shared_module_cache: nil, source_overrides: nil, workspace_root_path: nil)
267
+ return [] unless strict_root_check_eligible?(path, ast, sema_facts, existing_diagnostics)
268
+
269
+ loader = ModuleLoader.new(
270
+ module_roots: module_roots_for_path(path, locked: resolution.locked, workspace_root_path: workspace_root_path),
271
+ package_graph: load_package_graph(path, locked: resolution.locked),
272
+ shared_cache: shared_module_cache,
273
+ source_overrides: source_overrides,
274
+ platform: effective_platform,
275
+ )
276
+ program = loader.check_program(path)
277
+ Build.frontend_build_artifacts(program)
278
+ []
279
+ rescue ModuleLoadError, PackageLockError, SemanticError, LoweringError, BuildError => e
280
+ [format_strict_root_error(e, path: path)]
281
+ end
282
+ private_class_method :collect_strict_root_diagnostics
283
+
284
+ def self.strict_root_check_eligible?(path, ast, sema_facts, existing_diagnostics)
285
+ return false unless path && File.file?(path)
286
+ return false unless sema_facts
287
+ return false if ast&.module_kind == :external
288
+
289
+ existing_diagnostics.none? do |diagnostic|
290
+ diagnostic[:severity] == 1 && diagnostic.dig(:data, :stage) != 'lint'
291
+ end
292
+ end
293
+ private_class_method :strict_root_check_eligible?
294
+
295
+ def self.format_strict_root_error(error, path:)
296
+ diagnostic = Diagnostic.new(
297
+ path: path,
298
+ line: error.respond_to?(:line) && error.line ? error.line : 1,
299
+ column: error.respond_to?(:column) && error.column ? error.column : 1,
300
+ code: strict_root_diagnostic_code(error),
301
+ message: "strict current-root check failed: #{error.message}",
302
+ severity: :error,
303
+ )
304
+ format_tooling_diagnostic(diagnostic, stage: 'strict-root')
305
+ end
306
+ private_class_method :format_strict_root_error
307
+
308
+ def self.module_roots_for_path(path, locked:, workspace_root_path: nil)
309
+ roots = MilkTea::ModuleRoots.roots_for_path(path, locked: locked)
310
+ return roots unless workspace_root_applicable_for?(path, workspace_root_path)
311
+
312
+ [File.expand_path(workspace_root_path), *roots].uniq
313
+ end
314
+ private_class_method :module_roots_for_path
315
+
316
+ def self.workspace_root_applicable_for?(path, workspace_root_path)
317
+ return false if path.nil? || workspace_root_path.nil? || workspace_root_path.to_s.strip.empty?
318
+
319
+ expanded_root = File.expand_path(workspace_root_path)
320
+ return false unless File.directory?(expanded_root)
321
+
322
+ expanded_path = File.expand_path(path)
323
+ expanded_path == expanded_root || expanded_path.start_with?(expanded_root + File::SEPARATOR)
324
+ end
325
+ private_class_method :workspace_root_applicable_for?
326
+
327
+ def self.strict_root_diagnostic_code(error)
328
+ case error
329
+ when MilkTea::ModuleLoadError
330
+ 'import/load-error'
331
+ when MilkTea::SemanticError
332
+ 'sema/error'
333
+ when MilkTea::LoweringError
334
+ 'lowering/error'
335
+ when MilkTea::BuildError
336
+ 'build/error'
337
+ when MilkTea::PackageLockError
338
+ 'package/lock-error'
339
+ else
340
+ 'tooling/error'
341
+ end
342
+ end
343
+ private_class_method :strict_root_diagnostic_code
344
+
345
+ def self.format_warning(warning, content: nil)
346
+ format_tooling_diagnostic(warning.to_diagnostic, content:, stage: 'lint')
347
+ end
348
+
349
+ def self.format_tooling_diagnostic(diagnostic, content: nil, stage:)
350
+ line = diagnostic.line.to_i
351
+ line_index = [line - 1, 0].max
352
+ start_char, end_char = extract_warning_range(diagnostic, content)
353
+
354
+ lsp_severity = case diagnostic.severity
355
+ when :error then 1
356
+ when :warning then 2
357
+ when :info then 3
358
+ when :hint then 4
359
+ else 2
360
+ end
361
+ {
362
+ range: {
363
+ start: { line: line_index, character: start_char },
364
+ end: { line: line_index, character: end_char }
365
+ },
366
+ severity: lsp_severity,
367
+ code: diagnostic.code,
368
+ message: diagnostic_message_to_client(diagnostic.message.to_s, diagnostic.code),
369
+ source: 'milk-tea',
370
+ data: {
371
+ stage: stage
372
+ }
373
+ }
374
+ end
375
+
376
+ # Returns [start_char, end_char] in 0-based LSP coordinates.
377
+ # Falls back to [0, 1] when a precise token span cannot be inferred.
378
+ def self.extract_warning_range(warning, content)
379
+ if warning.respond_to?(:column) && warning.column
380
+ start_char = [warning.column.to_i - 1, 0].max
381
+ len = warning.respond_to?(:length) ? warning.length.to_i : 0
382
+ len = 1 if len <= 0
383
+ return [start_char, start_char + len]
384
+ end
385
+
386
+ return [0, 1] unless content && warning.line
387
+
388
+ lines = content.split("\n", -1)
389
+ line_text = lines[warning.line - 1]
390
+ return [0, 1] unless line_text
391
+
392
+ token_name = warning.respond_to?(:symbol_name) ? warning.symbol_name : nil
393
+ token_name ||= extract_quoted_name(warning.message.to_s)
394
+ return [0, 1] unless token_name && !token_name.empty?
395
+
396
+ # Prefer whole-token matches so names like "arg" don't match inside "argv".
397
+ token_match = line_text.match(/\b#{Regexp.escape(token_name)}\b/)
398
+ return [token_match.begin(0), token_match.end(0)] if token_match
399
+
400
+ # Fallback for non-word symbols if any rule introduces them.
401
+ index = line_text.index(token_name)
402
+ return [index, index + token_name.length] if index
403
+
404
+ [0, 1]
405
+ rescue StandardError
406
+ [0, 1]
407
+ end
408
+
409
+ def self.extract_quoted_name(message)
410
+ match = message.match(/'([^']+)'/)
411
+ match && match[1]
412
+ end
413
+
414
+ def self.format_error(error)
415
+ line = extract_line(error)
416
+ column = extract_column(error)
417
+ length = extract_length(error)
418
+ start_char = [column.to_i - 1, 0].max
419
+ end_char = start_char + [length.to_i, 1].max
420
+ code = diagnostic_code(error)
421
+
422
+ diagnostic = {
423
+ range: {
424
+ start: {
425
+ line: (line.to_i - 1),
426
+ character: start_char
427
+ },
428
+ end: {
429
+ line: (line.to_i - 1),
430
+ character: end_char
431
+ }
432
+ },
433
+ severity: 1, # Error
434
+ code: code,
435
+ message: diagnostic_message_to_client(error.message.to_s, code),
436
+ source: 'milk-tea',
437
+ data: {
438
+ stage: diagnostic_stage(error)
439
+ }
440
+ }
441
+ if error.respond_to?(:suggestion) && error.suggestion
442
+ diagnostic[:data][:suggestion] = error.suggestion
443
+ end
444
+ diagnostic
445
+ end
446
+
447
+ def self.format_import_error(error, import, content: nil)
448
+ return format_error(SemanticError.new(error.message)) unless import
449
+
450
+ column, length = import_path_span(import, content)
451
+ line_index = [import.line.to_i - 1, 0].max
452
+ start_char = [column.to_i - 1, 0].max
453
+ end_char = start_char + [length.to_i, 1].max
454
+ code = diagnostic_code(error)
455
+
456
+ {
457
+ range: {
458
+ start: {
459
+ line: line_index,
460
+ character: start_char,
461
+ },
462
+ end: {
463
+ line: line_index,
464
+ character: end_char,
465
+ }
466
+ },
467
+ severity: 1,
468
+ code: code,
469
+ message: diagnostic_message_to_client(error.message.to_s, code),
470
+ source: 'milk-tea',
471
+ data: {
472
+ stage: diagnostic_stage(error)
473
+ }
474
+ }
475
+ end
476
+
477
+ def self.diagnostic_code(error)
478
+ case error
479
+ when MilkTea::LexError
480
+ 'lex/error'
481
+ when MilkTea::ParseError
482
+ 'parse/error'
483
+ when MilkTea::ModuleLoadError
484
+ 'import/load-error'
485
+ when MilkTea::SemanticError
486
+ 'sema/error'
487
+ else
488
+ 'tooling/error'
489
+ end
490
+ end
491
+
492
+ def self.diagnostic_stage(error)
493
+ case error
494
+ when MilkTea::LexError
495
+ 'lex'
496
+ when MilkTea::ParseError
497
+ 'parse'
498
+ when MilkTea::ModuleLoadError
499
+ 'import'
500
+ when MilkTea::SemanticError
501
+ 'sema'
502
+ else
503
+ 'tooling'
504
+ end
505
+ end
506
+
507
+ def self.import_path_span(import, content)
508
+ return [import.column || 1, import.length || 1] unless content && import.line
509
+
510
+ line_text = content.split("\n", -1)[import.line - 1]
511
+ return [import.column || 1, import.length || 1] unless line_text
512
+
513
+ path_text = import.path.to_s
514
+ index = line_text.index(path_text)
515
+ return [index + 1, path_text.length] if index
516
+
517
+ [import.column || 1, import.length || 1]
518
+ rescue StandardError
519
+ [import.column || 1, import.length || 1]
520
+ end
521
+
522
+ def self.extract_line(error)
523
+ case error
524
+ when MilkTea::LexError
525
+ error.line || 1
526
+ when MilkTea::ParseError
527
+ error.token&.line || 1
528
+ when MilkTea::SemanticError
529
+ error.line || 1
530
+ else
531
+ 1
532
+ end
533
+ end
534
+
535
+ def self.extract_column(error)
536
+ case error
537
+ when MilkTea::LexError
538
+ error.column || 1
539
+ when MilkTea::ParseError
540
+ error.token&.column || 1
541
+ when MilkTea::SemanticError
542
+ error.column || 1
543
+ else
544
+ 1
545
+ end
546
+ end
547
+
548
+ def self.extract_length(error)
549
+ case error
550
+ when MilkTea::ParseError
551
+ error.token&.lexeme.to_s.length
552
+ when MilkTea::SemanticError
553
+ error.length || 1
554
+ else
555
+ 1
556
+ end
557
+ end
558
+
559
+ def self.log_diagnostics_warning(message)
560
+ return unless @protocol
561
+
562
+ @protocol.write_notification('window/logMessage', {
563
+ type: 2,
564
+ message: message,
565
+ })
566
+ rescue StandardError
567
+ nil
568
+ end
569
+
570
+ def self.diagnostic_message_to_client(message, code)
571
+ return message unless message.is_a?(String)
572
+
573
+ code_spans = []
574
+ if code.to_s.start_with?('sema')
575
+ message.scan(/`([^`]+)`/) { |m| code_spans << m[0] }
576
+ message.scan(/'([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)'/) { |m| code_spans << m[0] unless code_spans.include?(m[0]) }
577
+ end
578
+
579
+ return message if code_spans.empty?
580
+
581
+ markdown = message.gsub(/`([^`]+)`/, '`\1`')
582
+ { kind: 'markdown', value: markdown }
583
+ end
584
+
585
+ def self.client_char_to_internal(line_text, client_char)
586
+ return client_char if @position_encoding == 'utf-16'
587
+
588
+ case @position_encoding
589
+ when 'utf-8'
590
+ bytes_seen = 0
591
+ utf16_count = 0
592
+ line_text.each_char do |ch|
593
+ break if bytes_seen >= client_char
594
+ bytes_seen += ch.bytesize
595
+ utf16_count += ch.ord > 0xFFFF ? 2 : 1
596
+ end
597
+ utf16_count
598
+ when 'utf-32'
599
+ utf16_count = 0
600
+ line_text.each_char.with_index do |ch, idx|
601
+ break if idx >= client_char
602
+ utf16_count += ch.ord > 0xFFFF ? 2 : 1
603
+ end
604
+ utf16_count
605
+ else
606
+ client_char
607
+ end
608
+ end
609
+ end
610
+ end
611
+ end