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,380 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ module DAP
5
+ class Server
6
+ module ServerLLDBBackend
7
+ private
8
+
9
+ def using_lldb_backend?
10
+ @session.backend_kind == "lldb-dap"
11
+ end
12
+
13
+ def start_lldb_backend(args)
14
+ return if @lldb_backend
15
+
16
+ adapter_path = args["adapterPath"].to_s
17
+ adapter_command = if adapter_path.empty?
18
+ @default_adapter_command || ["lldb-dap"]
19
+ else
20
+ expanded = File.expand_path(adapter_path)
21
+ expanded.end_with?(".rb") ? [RbConfig.ruby, expanded] : [expanded]
22
+ end
23
+
24
+ @lldb_backend = if @backend_factory
25
+ build_backend_via_factory(adapter_command)
26
+ else
27
+ Backends::LLDBDAP.new(
28
+ adapter_command: adapter_command,
29
+ on_event: method(:handle_backend_event),
30
+ on_request: method(:handle_backend_request)
31
+ )
32
+ end
33
+ @lldb_backend.start!
34
+ end
35
+
36
+ def stop_lldb_backend
37
+ @backend_start_thread&.join(0.2)
38
+ @backend_start_thread = nil
39
+ @backend_auto_continue_after_configuration = false
40
+ @backend_stopped_thread_id = nil
41
+ @backend_pause_requested = false
42
+ @lldb_backend&.stop!
43
+ ensure
44
+ @lldb_backend = nil
45
+ @backend_initialized = false
46
+ @backend_capabilities = {}
47
+ reset_backend_configuration_ready
48
+ clear_debug_context
49
+ end
50
+
51
+ def ensure_backend_initialized(client_arguments)
52
+ return { "success" => true, "body" => @backend_capabilities } if @backend_initialized
53
+
54
+ response = backend_request("initialize", backend_initialize_arguments(client_arguments))
55
+ if response["success"]
56
+ @backend_initialized = true
57
+ @backend_capabilities = response["body"] || {}
58
+ end
59
+ response
60
+ end
61
+
62
+ def backend_request(command, arguments)
63
+ unless @lldb_backend
64
+ return {
65
+ "success" => false,
66
+ "message" => "lldb-dap backend is not running"
67
+ }
68
+ end
69
+
70
+ @lldb_backend.request(command, arguments)
71
+ end
72
+
73
+ def request_backend_terminate(arguments)
74
+ join_background_threads
75
+ terminate_response = backend_request("terminate", arguments)
76
+ return terminate_response if terminate_response["success"]
77
+ return terminate_response unless backend_terminate_unsupported?(terminate_response)
78
+
79
+ disconnect_response = backend_request("disconnect", arguments)
80
+ return disconnect_response unless disconnect_response["success"]
81
+
82
+ stop_lldb_backend
83
+ cleanup_tmp_dirs
84
+ unless @session.terminated?
85
+ @session.terminate!
86
+ write_event("terminated")
87
+ end
88
+ @session.request_exit!
89
+ disconnect_response
90
+ end
91
+
92
+ def start_backend_start_request(request, arguments)
93
+ @backend_start_thread = Thread.new do
94
+ Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
95
+
96
+ backend_response = backend_request(request["command"], arguments)
97
+ write_backend_response(request, backend_response)
98
+ rescue StandardError => e
99
+ write_error_response(request, "Internal error: #{e.message}")
100
+ ensure
101
+ @backend_start_thread = nil
102
+ @backend_configuration_mutex.synchronize do
103
+ @backend_configuration_condition.broadcast
104
+ end
105
+ end
106
+ end
107
+
108
+ def write_backend_response(request, backend_response)
109
+ if backend_response["success"]
110
+ write_response(request, backend_response["body"] || {})
111
+ else
112
+ write_error_response(request, backend_error_message(backend_response))
113
+ end
114
+ end
115
+
116
+ def backend_error_message(backend_response)
117
+ message = backend_response["message"].to_s
118
+ return message unless message.empty?
119
+
120
+ error_body = backend_response.dig("body", "error")
121
+ if error_body.is_a?(Hash)
122
+ format = error_body["format"].to_s
123
+ return format unless format.empty?
124
+
125
+ error_message = error_body["message"].to_s
126
+ return error_message unless error_message.empty?
127
+ end
128
+
129
+ "backend request failed"
130
+ end
131
+
132
+ def backend_terminate_unsupported?(backend_response)
133
+ message = backend_error_message(backend_response).downcase
134
+ message.include?("unknown request") || message.include?("unsupported command")
135
+ end
136
+
137
+ def build_backend_via_factory(adapter_command)
138
+ arity = @backend_factory.arity
139
+ if arity < 0 || arity >= 3
140
+ @backend_factory.call(adapter_command, method(:handle_backend_event), method(:handle_backend_request))
141
+ else
142
+ @backend_factory.call(adapter_command, method(:handle_backend_event))
143
+ end
144
+ end
145
+
146
+ def handle_backend_event(message)
147
+ event = message["event"].to_s
148
+ body = message["body"]
149
+
150
+ # The outer Milk Tea adapter owns the initialize/initialized handshake.
151
+ # Forwarding lldb-dap's own initialized event causes VS Code to repeat
152
+ # the configuration phase and emit a second configurationDone.
153
+ if event == "initialized"
154
+ mark_backend_configuration_ready
155
+ return
156
+ end
157
+
158
+ if event == "stopped"
159
+ pause_requested = @backend_pause_requested
160
+ body = rewrite_backend_stopped_event_body(body)
161
+ emit_pause_diagnostic_async(body) if pause_requested
162
+ @backend_stopped_thread_id = body && body["threadId"]
163
+ @backend_pause_requested = false
164
+ @last_forwarded_continued_signature = nil
165
+ elsif event == "continued"
166
+ signature = continued_event_signature(body)
167
+ return if signature && signature == @last_forwarded_continued_signature
168
+
169
+ @backend_stopped_thread_id = nil
170
+ @backend_pause_requested = false
171
+ @last_forwarded_continued_signature = signature
172
+ elsif event == "terminated" || event == "exited"
173
+ @backend_stopped_thread_id = nil
174
+ @backend_pause_requested = false
175
+ @last_forwarded_continued_signature = nil
176
+ end
177
+
178
+ write_event(event, body)
179
+
180
+ return unless event == "terminated" || event == "exited"
181
+
182
+ @session.terminate!
183
+ end
184
+
185
+ def handle_backend_request(message)
186
+ client_seq = @session.next_seq
187
+ queue = Queue.new
188
+ @client_request_mutex.synchronize do
189
+ @pending_client_responses[client_seq] = queue
190
+ end
191
+
192
+ write_message({
193
+ seq: client_seq,
194
+ type: "request",
195
+ command: message["command"],
196
+ arguments: message["arguments"] || {}
197
+ })
198
+
199
+ timeout_seconds = reverse_request_timeout
200
+ client_response = Timeout.timeout(timeout_seconds) { queue.pop }
201
+ build_backend_request_response(message, client_response)
202
+ rescue Timeout::Error
203
+ {
204
+ "seq" => @session.next_seq,
205
+ "type" => "response",
206
+ "request_seq" => message["seq"],
207
+ "success" => false,
208
+ "command" => message["command"],
209
+ "message" => format("client request timed out after %.1fs: %s", timeout_seconds, message["command"])
210
+ }
211
+ ensure
212
+ @client_request_mutex.synchronize do
213
+ @pending_client_responses.delete(client_seq) if defined?(client_seq)
214
+ end
215
+ end
216
+
217
+ def reverse_request_timeout
218
+ timeout = @reverse_request_timeout.to_f
219
+ timeout.positive? ? timeout : 15.0
220
+ rescue StandardError
221
+ 15.0
222
+ end
223
+
224
+ def handle_client_response(message)
225
+ queue = @client_request_mutex.synchronize do
226
+ @pending_client_responses[message["request_seq"]]
227
+ end
228
+ queue&.push(message)
229
+ end
230
+
231
+ def build_backend_request_response(request, client_response)
232
+ response = {
233
+ "seq" => @session.next_seq,
234
+ "type" => "response",
235
+ "request_seq" => request["seq"],
236
+ "success" => !!client_response["success"],
237
+ "command" => request["command"]
238
+ }
239
+ response["body"] = client_response["body"] if client_response.key?("body")
240
+ response["message"] = client_response["message"] if client_response["message"]
241
+ response
242
+ end
243
+
244
+ def backend_configuration_ready?
245
+ @backend_configuration_mutex.synchronize { @backend_configuration_ready }
246
+ end
247
+
248
+ def backend_start_pending?
249
+ thread = @backend_start_thread
250
+ thread && thread.alive?
251
+ end
252
+
253
+ def backend_configuration_pending?
254
+ backend_start_pending? && !backend_configuration_ready?
255
+ end
256
+
257
+ def wait_for_backend_configuration_ready(timeout: 30)
258
+ return true unless backend_configuration_pending?
259
+
260
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
261
+
262
+ @backend_configuration_mutex.synchronize do
263
+ while @backend_start_thread&.alive? && !@backend_configuration_ready
264
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
265
+ return false if remaining <= 0
266
+
267
+ @backend_configuration_condition.wait(@backend_configuration_mutex, remaining)
268
+ end
269
+ end
270
+
271
+ true
272
+ end
273
+
274
+ def mark_backend_configuration_ready
275
+ @backend_configuration_mutex.synchronize do
276
+ @backend_configuration_ready = true
277
+ @backend_configuration_condition.broadcast
278
+ end
279
+ end
280
+
281
+ def reset_backend_configuration_ready
282
+ @backend_configuration_mutex.synchronize do
283
+ @backend_configuration_ready = false
284
+ end
285
+ end
286
+
287
+ def backend_thread_id_for_auto_continue
288
+ response = backend_request("threads", {})
289
+ return nil unless response["success"]
290
+
291
+ threads = response.dig("body", "threads")
292
+ return nil unless threads.is_a?(Array)
293
+
294
+ first_thread = threads.first
295
+ return nil unless first_thread.is_a?(Hash)
296
+
297
+ first_thread["id"]
298
+ end
299
+
300
+ def backend_initialize_arguments(client_arguments)
301
+ {
302
+ "adapterID" => "milk-tea",
303
+ "clientID" => "milk-tea",
304
+ "linesStartAt1" => true,
305
+ "columnsStartAt1" => true,
306
+ "pathFormat" => "path"
307
+ }.merge(client_arguments)
308
+ end
309
+
310
+ def filter_breakpoint_for_backend(breakpoint)
311
+ breakpoint.each_with_object({}) do |(key, value), result|
312
+ next if key.to_s == "id" || key.to_s == "verified"
313
+
314
+ result[key.to_s] = value
315
+ end
316
+ end
317
+
318
+ def backend_continue_arguments
319
+ thread_id = @backend_stopped_thread_id || backend_thread_id_for_auto_continue
320
+ return {} unless thread_id
321
+
322
+ { "threadId" => thread_id }
323
+ end
324
+
325
+ def continued_event_signature(body)
326
+ return nil unless body.is_a?(Hash)
327
+
328
+ [
329
+ normalize_reference_key(dap_value(body, "threadId")),
330
+ dap_value(body, "allThreadsContinued") == true,
331
+ ]
332
+ end
333
+
334
+ def rewrite_backend_stopped_event_body(body)
335
+ return body unless @backend_pause_requested
336
+ return body unless body.is_a?(Hash)
337
+
338
+ reason = dap_value(body, "reason").to_s
339
+ description = dap_value(body, "description").to_s
340
+ text = dap_value(body, "text").to_s
341
+ return body unless reason == "exception"
342
+ return body unless description.match?(/\bSIGSTOP\b/) || text.match?(/\bSIGSTOP\b/)
343
+
344
+ rewritten = body.dup
345
+ dap_set(rewritten, "reason", "pause")
346
+ dap_set(rewritten, "description", "Paused") if !description.empty? || rewritten.key?("description") || rewritten.key?(:description)
347
+ dap_set(rewritten, "text", "Paused") if !text.empty? || rewritten.key?("text") || rewritten.key?(:text)
348
+ rewritten
349
+ end
350
+
351
+ def effective_adapter_capabilities
352
+ capabilities = ADAPTER_CAPABILITIES.merge(
353
+ @backend_capabilities.transform_keys { |key| key.to_sym }
354
+ )
355
+ capabilities[:supportsSetVariable] = true if backend_supports_set_variable? || backend_supports_set_expression?
356
+
357
+ backend_filters = @backend_capabilities["exceptionBreakpointFilters"]
358
+ if backend_filters.is_a?(Array) && !backend_filters.empty?
359
+ capabilities[:exceptionBreakpointFilters] = backend_filters
360
+ end
361
+
362
+ capabilities
363
+ end
364
+
365
+ def backend_supports_set_variable?
366
+ capability_enabled?("supportsSetVariable")
367
+ end
368
+
369
+ def backend_supports_set_expression?
370
+ capability_enabled?("supportsSetExpression")
371
+ end
372
+
373
+ def capability_enabled?(name)
374
+ value = @backend_capabilities[name] || @backend_capabilities[name.to_sym]
375
+ !!value
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ module DAP
5
+ class Server
6
+ module ServerPauseDiagnostics
7
+ private
8
+
9
+ def emit_pause_diagnostic_async(body)
10
+ return unless body.is_a?(Hash)
11
+
12
+ thread_id = normalize_reference_key(dap_value(body, "threadId"))
13
+ return if thread_id.nil?
14
+
15
+ track_background_thread(Thread.new(thread_id) do |diagnostic_thread_id|
16
+ stack_response = backend_request("stackTrace", {
17
+ "threadId" => diagnostic_thread_id,
18
+ "startFrame" => 0,
19
+ "levels" => 8
20
+ })
21
+
22
+ write_event("output", {
23
+ category: "console",
24
+ output: "#{pause_diagnostic_output(diagnostic_thread_id, stack_response)}\n"
25
+ })
26
+ rescue StandardError => e
27
+ write_event("output", {
28
+ category: "console",
29
+ output: "[milk-tea dap] pause top frame unavailable thread=#{diagnostic_thread_id}: #{e.message}\n"
30
+ })
31
+ end)
32
+ end
33
+
34
+ def pause_diagnostic_output(thread_id, stack_response)
35
+ unless stack_response["success"]
36
+ return "[milk-tea dap] pause focus unavailable thread=#{thread_id}: #{backend_error_message(stack_response)}"
37
+ end
38
+
39
+ frames = stack_response.dig("body", "stackFrames")
40
+ frames = frames.select { |candidate| candidate.is_a?(Hash) } if frames.is_a?(Array)
41
+ return "[milk-tea dap] pause focus unavailable thread=#{thread_id}: no stack frames" if !frames.is_a?(Array) || frames.empty?
42
+
43
+ raw_frame = frames.first
44
+ informative_index = frames.index { |frame| informative_pause_frame?(frame) }
45
+
46
+ unless informative_index
47
+ return "[milk-tea dap] pause top frame thread=#{thread_id}: #{pause_frame_summary(raw_frame, include_ip: true)}"
48
+ end
49
+
50
+ focus_frames = frames.drop(informative_index).first(3)
51
+ focus = focus_frames.map { |frame| pause_frame_summary(frame) }.join(" <- ")
52
+ raw_suffix = informative_index.zero? ? "" : " raw=#{pause_frame_summary(raw_frame, include_ip: true)}"
53
+
54
+ "[milk-tea dap] pause focus thread=#{thread_id}: #{focus}#{raw_suffix}"
55
+ end
56
+
57
+ def informative_pause_frame?(frame)
58
+ name = dap_value(frame, "name").to_s
59
+ return false if name.empty?
60
+ return false if name.match?(/\A___lldb_unnamed_symbol_/)
61
+ return false if %w[clock_nanosleep __nanosleep nanosleep].include?(name)
62
+
63
+ source = dap_value(frame, "source")
64
+ source_path = source.is_a?(Hash) ? dap_value(source, "path").to_s : ""
65
+
66
+ return true if source_path.start_with?("/usr/src/debug/")
67
+ return true if !source_path.empty? && !source_path.start_with?("/usr/lib/") && !source_path.start_with?("/lib/")
68
+
69
+ true
70
+ end
71
+
72
+ def pause_frame_summary(frame, include_ip: false)
73
+ frame_name = dap_value(frame, "name").to_s
74
+ frame_name = "(anonymous)" if frame_name.empty?
75
+
76
+ instruction_pointer = dap_value(frame, "instructionPointerReference").to_s
77
+ source = dap_value(frame, "source")
78
+ source_path = source.is_a?(Hash) ? dap_value(source, "path").to_s : ""
79
+ source_name = source.is_a?(Hash) ? dap_value(source, "name").to_s : ""
80
+ line = dap_value(frame, "line")
81
+
82
+ location_source = if !source_path.empty? && !source_path.include?("`")
83
+ source_path
84
+ elsif !source_name.empty?
85
+ source_name
86
+ elsif !source_path.empty?
87
+ source_path
88
+ else
89
+ instruction_pointer
90
+ end
91
+
92
+ location = if location_source.to_s.empty?
93
+ "(unknown)"
94
+ elsif line.to_i.positive? && !location_source.include?(":#{line}")
95
+ "#{location_source}:#{line}"
96
+ else
97
+ location_source
98
+ end
99
+
100
+ if include_ip && !instruction_pointer.empty? && location != instruction_pointer
101
+ "#{frame_name} @ #{location} ip=#{instruction_pointer}"
102
+ else
103
+ "#{frame_name} @ #{location}"
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ module DAP
5
+ class Server
6
+ module ServerUtilities
7
+ PERF_LOG_THRESHOLD_MS = 20
8
+
9
+ private
10
+
11
+ def register_handlers
12
+ @handlers["initialize"] = method(:handle_initialize)
13
+ @handlers["setBreakpoints"] = method(:handle_set_breakpoints)
14
+ @handlers["setFunctionBreakpoints"] = method(:handle_set_function_breakpoints)
15
+ @handlers["configurationDone"] = method(:handle_configuration_done)
16
+ @handlers["launch"] = method(:handle_launch)
17
+ @handlers["attach"] = method(:handle_attach)
18
+ @handlers["threads"] = method(:handle_threads)
19
+ @handlers["stackTrace"] = method(:handle_stack_trace)
20
+ @handlers["scopes"] = method(:handle_scopes)
21
+ @handlers["variables"] = method(:handle_variables)
22
+ @handlers["continue"] = method(:handle_continue)
23
+ @handlers["next"] = method(:handle_next)
24
+ @handlers["stepIn"] = method(:handle_step_in)
25
+ @handlers["stepOut"] = method(:handle_step_out)
26
+ @handlers["pause"] = method(:handle_pause)
27
+ @handlers["terminate"] = method(:handle_terminate)
28
+ @handlers["disconnect"] = method(:handle_disconnect)
29
+ @handlers["setExceptionBreakpoints"] = method(:handle_set_exception_breakpoints)
30
+ @handlers["evaluate"] = method(:handle_evaluate)
31
+ @handlers["setExpression"] = method(:handle_set_expression)
32
+ @handlers["setVariable"] = method(:handle_set_variable)
33
+ @handlers["dataBreakpointInfo"] = method(:handle_data_breakpoint_info)
34
+ @handlers["source"] = method(:handle_source)
35
+ @handlers["loadedSources"] = method(:handle_loaded_sources)
36
+ @handlers["restart"] = method(:handle_restart)
37
+ @handlers["cancel"] = method(:handle_cancel)
38
+ end
39
+
40
+ def process_message(message)
41
+ if message["type"] == "response"
42
+ handle_client_response(message)
43
+ return
44
+ end
45
+
46
+ return unless message["type"] == "request"
47
+
48
+ command = message["command"]
49
+ handler = @handlers[command]
50
+
51
+ unless command == "initialize" || @session.initialized?
52
+ write_error_response(message, "initialize request must be sent first")
53
+ return
54
+ end
55
+
56
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
57
+
58
+ if handler
59
+ handler.call(message)
60
+ elsif using_lldb_backend?
61
+ backend_response = backend_request(command, message["arguments"] || {})
62
+ write_backend_response(message, backend_response)
63
+ else
64
+ write_error_response(message, "Command '#{command}' is not supported by the process backend. Use the lldb-dap backend for full debugging features.")
65
+ end
66
+
67
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(1)
68
+ if dap_perf_logging? && (dap_perf_verbose? || elapsed_ms > PERF_LOG_THRESHOLD_MS)
69
+ detail = dap_perf_verbose? ? " #{summarize_dap_arguments(message['arguments'])}" : ""
70
+ warn "[DAP perf] #{command} #{elapsed_ms}ms#{detail}"
71
+ end
72
+ rescue StandardError => e
73
+ write_error_response(message, "Internal error: #{e.message}")
74
+ end
75
+
76
+ def dap_perf_logging?
77
+ @dap_perf_logging ||= !ENV.fetch('MILK_TEA_DAP_PERF', nil).to_s.empty?
78
+ end
79
+
80
+ def dap_perf_verbose?
81
+ @dap_perf_verbose ||= ENV.fetch('MILK_TEA_DAP_PERF', nil).to_s == 'verbose'
82
+ end
83
+
84
+ def summarize_dap_arguments(arguments)
85
+ return "" unless arguments.is_a?(Hash)
86
+
87
+ keys = arguments.keys.map(&:to_s).sort
88
+ source_path = arguments.dig('source', 'path')
89
+ frame_id = arguments['frameId']
90
+ expression = arguments['expression']
91
+
92
+ bits = []
93
+ bits << "keys=#{keys.join(',')}" unless keys.empty?
94
+ bits << "source=#{source_path}" if source_path
95
+ bits << "frameId=#{frame_id}" if frame_id
96
+ bits << "expr=#{expression.inspect}" if expression
97
+ bits.join(' ')
98
+ rescue StandardError
99
+ ""
100
+ end
101
+
102
+ def dap_value(payload, key)
103
+ return nil unless payload.is_a?(Hash)
104
+
105
+ payload[key] || payload[key.to_sym]
106
+ end
107
+
108
+ def dap_set(payload, key, value)
109
+ return unless payload.is_a?(Hash)
110
+
111
+ if payload.key?(key)
112
+ payload[key] = value
113
+ elsif payload.key?(key.to_sym)
114
+ payload[key.to_sym] = value
115
+ else
116
+ payload[key] = value
117
+ end
118
+ end
119
+
120
+ def normalize_reference_key(value)
121
+ return nil if value.nil?
122
+ return value if value.is_a?(Integer)
123
+
124
+ text = value.to_s.strip
125
+ return text.to_i if text.match?(/\A\d+\z/)
126
+
127
+ text
128
+ end
129
+
130
+ def track_background_thread(thread)
131
+ @background_threads_mutex.synchronize do
132
+ @background_threads << thread
133
+ end
134
+ thread
135
+ end
136
+
137
+ def join_background_threads(timeout: 0.5)
138
+ threads = @background_threads_mutex.synchronize do
139
+ threads = @background_threads
140
+ @background_threads = []
141
+ threads
142
+ end
143
+
144
+ threads.each do |thread|
145
+ thread.join(timeout)
146
+ rescue StandardError
147
+ nil
148
+ end
149
+ end
150
+
151
+ def cleanup_tmp_dirs
152
+ @tmp_dirs.each do |dir|
153
+ FileUtils.remove_entry(dir) if File.exist?(dir)
154
+ end
155
+ @tmp_dirs.clear
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end