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,744 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Linter
5
+ module LinterReleaseRules
6
+ private
7
+
8
+ # Entry point — called from visit_function and proc expr in full_tier mode.
9
+ def emit_owning_release_warnings(function_or_body)
10
+ body = function_or_body.is_a?(Array) ? function_or_body : function_or_body.body
11
+ return unless body && !body.empty?
12
+
13
+ cfg_available = false
14
+ if @sema_facts
15
+ cfg_available = begin
16
+ analysis = profile_phase("rule.owning_release.cfg") { statement_flow_analysis(body) }
17
+ analysis || false
18
+ rescue StandardError
19
+ false
20
+ end
21
+ end
22
+
23
+ if cfg_available
24
+ emit_owning_release_cfg_warnings(cfg_available, body)
25
+ else
26
+ # No CFG — emit struct-type leaks via simple pattern-matching
27
+ leak_names = check_owning_release_leaks(body)
28
+ emit_owning_release_leak_warnings(leak_names)
29
+ end
30
+ # Always run own[T] checks (AST-based, works with or without CFG)
31
+ own_leaks = check_own_release_leaks(body)
32
+ emit_owning_release_leak_warnings(own_leaks)
33
+
34
+ double_names_lines = check_owning_release_double(body)
35
+ emit_owning_release_double_warnings(double_names_lines)
36
+ end
37
+
38
+ # ── Simple leak detection ──────────────────────────────────────────
39
+
40
+ def check_owning_release_leaks(stmts)
41
+ locals = owning_local_bindings(stmts)
42
+ released = all_released_names(stmts)
43
+ locals.reject { |name, _info| released.include?(name) || ownership_transferred?(stmts, name) }
44
+ end
45
+
46
+ # Check only own[T] typed leaks (separate from struct-type leaks)
47
+ def check_own_release_leaks(stmts)
48
+ own_locals = own_typed_local_bindings(stmts)
49
+ own_released = all_own_released_names(stmts)
50
+ own_locals.reject { |name, _info| own_released.include?(name) || own_transferred?(stmts, name) }
51
+ end
52
+
53
+ def emit_owning_release_leak_warnings(leak_names)
54
+ leak_names.each do |name, info|
55
+ @warnings << Warning.new(
56
+ path: @path,
57
+ line: info[:line],
58
+ column: info[:column],
59
+ length: name.length,
60
+ code: "owning-release-leak",
61
+ message: "owning binding '#{name}' of type '#{info[:type_name]}' is never released",
62
+ severity: :warning,
63
+ symbol_name: name,
64
+ )
65
+ end
66
+ end
67
+
68
+ # ── own[T] typed local detection ──────────────────────────────────
69
+
70
+ def own_typed_local_bindings(stmts)
71
+ result = {}
72
+ _collect_own_typed_locals(stmts, result)
73
+ result
74
+ end
75
+
76
+ def _collect_own_typed_locals(stmts, result)
77
+ return if stmts.nil?
78
+
79
+ stmts.each do |stmt|
80
+ case stmt
81
+ when AST::LocalDecl
82
+ type_name = resolve_local_own_pointer_type(stmt)
83
+ if type_name
84
+ result[stmt.name] = {
85
+ line: stmt.line,
86
+ column: stmt.respond_to?(:column) ? stmt.column : nil,
87
+ type_name: type_name,
88
+ }
89
+ end
90
+ when AST::IfStmt
91
+ stmt.branches.each { |b| _collect_own_typed_locals(b.body, result) }
92
+ _collect_own_typed_locals(stmt.else_body, result) if stmt.else_body
93
+ when AST::WhileStmt
94
+ _collect_own_typed_locals(stmt.body, result)
95
+ when AST::ForStmt
96
+ _collect_own_typed_locals(stmt.body, result)
97
+ when AST::MatchStmt
98
+ stmt.arms.each { |arm| _collect_own_typed_locals(arm.body, result) }
99
+ when AST::UnsafeStmt
100
+ _collect_own_typed_locals(stmt.body, result)
101
+ when AST::DeferStmt
102
+ _collect_own_typed_locals(stmt.body, result) if stmt.body.is_a?(Array)
103
+ end
104
+ end
105
+ end
106
+
107
+ def resolve_local_own_pointer_type(decl)
108
+ if is_heap_alloc_call?(decl.value)
109
+ return "own[<inferred>]"
110
+ end
111
+ type = resolve_expr_type(decl.value) if decl.value
112
+ return nil unless type
113
+ return nil unless type.is_a?(MilkTea::Types::GenericInstance)
114
+ return nil unless type.name == "own"
115
+ type.to_s
116
+ rescue StandardError
117
+ nil
118
+ end
119
+
120
+ def is_heap_alloc_call?(expr)
121
+ return false unless expr.is_a?(AST::Call)
122
+ callee = expr.callee
123
+ non_null_method_names = %w[must_alloc must_alloc_aligned
124
+ must_alloc_zeroed must_resize].to_set
125
+ case callee
126
+ when AST::MemberAccess
127
+ callee.receiver.is_a?(AST::Identifier) &&
128
+ non_null_method_names.include?(callee.member)
129
+ when AST::Specialization
130
+ inner = callee.callee
131
+ inner.is_a?(AST::MemberAccess) &&
132
+ inner.receiver.is_a?(AST::Identifier) &&
133
+ non_null_method_names.include?(inner.member)
134
+ else
135
+ false
136
+ end
137
+ end
138
+
139
+ # ── own[T] release detection ──────────────────────────────────────
140
+
141
+ def all_own_released_names(stmts)
142
+ result = Set.new
143
+ _collect_own_released_names(stmts, result)
144
+ result
145
+ end
146
+
147
+ def _collect_own_released_names(stmts, result)
148
+ return if stmts.nil?
149
+ stmts.each { |s| _collect_own_released_names_in_stmt(s, result) }
150
+ end
151
+
152
+ def _collect_own_released_names_in_stmt(stmt, result)
153
+ case stmt
154
+ when AST::ExpressionStmt
155
+ _collect_heap_release_names(stmt.expression, result)
156
+ when AST::ReturnStmt
157
+ _collect_heap_release_names(stmt.value, result) if stmt.value
158
+ when AST::LocalDecl
159
+ _collect_heap_release_names(stmt.value, result) if stmt.value
160
+ when AST::Assignment
161
+ _collect_heap_release_names(stmt.value, result)
162
+ when AST::IfStmt
163
+ stmt.branches.each { |b| _collect_own_released_names(b.body, result) }
164
+ _collect_own_released_names(stmt.else_body, result) if stmt.else_body
165
+ when AST::WhileStmt
166
+ _collect_own_released_names(stmt.body, result)
167
+ when AST::ForStmt
168
+ _collect_own_released_names(stmt.body, result)
169
+ when AST::MatchStmt
170
+ stmt.arms.each { |arm| _collect_own_released_names(arm.body, result) }
171
+ when AST::UnsafeStmt
172
+ _collect_own_released_names(stmt.body, result)
173
+ when AST::DeferStmt
174
+ if stmt.expression
175
+ _collect_heap_release_names(stmt.expression, result)
176
+ end
177
+ _collect_own_released_names(stmt.body, result) if stmt.body.is_a?(Array)
178
+ end
179
+ end
180
+
181
+ def _collect_heap_release_names(expr, result)
182
+ return if expr.nil?
183
+ if heap_release_call_on_name?(expr)
184
+ result << heap_release_target_name(expr)
185
+ end
186
+ case expr
187
+ when AST::Call
188
+ expr.arguments.each { |a| _collect_heap_release_names(a.value, result) }
189
+ _collect_heap_release_names(expr.callee, result) if expr.callee.is_a?(AST::Call)
190
+ when AST::BinaryOp
191
+ _collect_heap_release_names(expr.left, result)
192
+ _collect_heap_release_names(expr.right, result)
193
+ when AST::UnaryOp
194
+ _collect_heap_release_names(expr.operand, result)
195
+ end
196
+ end
197
+
198
+ def heap_release_call_on_name?(expr)
199
+ return false unless expr.is_a?(AST::Call)
200
+ return false unless expr.callee.is_a?(AST::MemberAccess)
201
+ return false unless %w[release release_and_null].include?(expr.callee.member)
202
+ return false unless expr.arguments.length >= 1
203
+ arg = expr.arguments.first
204
+ return false unless arg.is_a?(AST::Argument)
205
+ target_name = heap_release_target_name(expr)
206
+ target_name && !target_name.empty?
207
+ end
208
+
209
+ def heap_release_target_name(expr)
210
+ return nil unless expr.is_a?(AST::Call)
211
+ return nil unless expr.arguments.length >= 1
212
+ # For multi-arg release (e.g. tracking.release(tracker, p)), find
213
+ # the argument that names the released value by preferring the last
214
+ # direct-identifier argument.
215
+ released = nil
216
+ expr.arguments.each do |arg|
217
+ next unless arg.is_a?(AST::Argument)
218
+ value = arg.value
219
+ value = value.expression if value.is_a?(AST::UnsafeStmt)
220
+ if value.is_a?(AST::Identifier)
221
+ released = value.name
222
+ elsif value.is_a?(AST::Call) && value.callee.is_a?(AST::Identifier) && value.callee.name == "ref_of"
223
+ ref_arg = value.arguments.first
224
+ if ref_arg.is_a?(AST::Argument) && ref_arg.value.is_a?(AST::Identifier)
225
+ released = ref_arg.value.name
226
+ end
227
+ end
228
+ end
229
+ released
230
+ end
231
+
232
+ # ── own[T] ownership transfer ─────────────────────────────────────
233
+
234
+ def own_transferred?(stmts, name)
235
+ _any_stmt?(stmts) do |stmt|
236
+ if stmt.is_a?(AST::ReturnStmt) && stmt.value.is_a?(AST::Identifier) && stmt.value.name == name
237
+ true
238
+ elsif (expr = extract_expr(stmt)) && own_struct_field_transfer?(expr, name)
239
+ true
240
+ else
241
+ false
242
+ end
243
+ end
244
+ end
245
+
246
+ def own_struct_field_transfer?(expr, name)
247
+ return false unless expr.is_a?(AST::Call)
248
+ expr.arguments.any? do |arg|
249
+ arg.is_a?(AST::Argument) && arg.value.is_a?(AST::Identifier) && arg.value.name == name
250
+ end
251
+ end
252
+
253
+ def owning_local_bindings(stmts)
254
+ result = {}
255
+ _collect_owning_locals(stmts, result)
256
+ result
257
+ end
258
+
259
+ def _collect_owning_locals(stmts, result)
260
+ return if stmts.nil?
261
+
262
+ stmts.each do |stmt|
263
+ case stmt
264
+ when AST::LocalDecl
265
+ type_name = resolve_local_owning_type(stmt)
266
+ if type_name
267
+ result[stmt.name] = {
268
+ line: stmt.line,
269
+ column: stmt.respond_to?(:column) ? stmt.column : nil,
270
+ type_name: type_name,
271
+ }
272
+ end
273
+ when AST::IfStmt
274
+ stmt.branches.each { |b| _collect_owning_locals(b.body, result) }
275
+ _collect_owning_locals(stmt.else_body, result) if stmt.else_body
276
+ when AST::WhileStmt
277
+ _collect_owning_locals(stmt.body, result)
278
+ when AST::ForStmt
279
+ _collect_owning_locals(stmt.body, result)
280
+ when AST::MatchStmt
281
+ stmt.arms.each { |arm| _collect_owning_locals(arm.body, result) }
282
+ when AST::UnsafeStmt
283
+ _collect_owning_locals(stmt.body, result)
284
+ when AST::DeferStmt
285
+ _collect_owning_locals(stmt.body, result) if stmt.body.is_a?(Array)
286
+ end
287
+ end
288
+ end
289
+
290
+ # ── Simple sequential double-release ─────────────────────────────
291
+
292
+ def check_owning_release_double(stmts)
293
+ result = {}
294
+ _check_double_release_in_seq(stmts, result)
295
+ result
296
+ end
297
+
298
+ def emit_owning_release_double_warnings(double_names_lines)
299
+ double_names_lines.each do |name, lines|
300
+ @warnings << Warning.new(
301
+ path: @path,
302
+ line: lines.last,
303
+ column: nil,
304
+ length: nil,
305
+ code: "owning-release-double",
306
+ message: "owning binding '#{name}' may be released more than once",
307
+ severity: :warning,
308
+ symbol_name: name,
309
+ )
310
+ end
311
+ end
312
+
313
+ def _check_double_release_in_seq(stmts, result)
314
+ return if stmts.nil?
315
+
316
+ scope_releases = Hash.new { |hash, key| hash[key] = [] }
317
+
318
+ stmts.each do |stmt|
319
+ expr = extract_expr(stmt)
320
+ if expr && release_call_on_binding?(expr)
321
+ name = expr.callee.receiver.name
322
+ scope_releases[name] << extract_line(stmt)
323
+ elsif expr && heap_release_call_on_name?(expr)
324
+ name = heap_release_target_name(expr)
325
+ scope_releases[name] << extract_line(stmt) if name
326
+ end
327
+
328
+ scope_releases.delete(stmt.target.name) if reassigns_owning_binding?(stmt)
329
+
330
+ case stmt
331
+ when AST::UnsafeStmt
332
+ _check_double_release_in_seq(stmt.body, result)
333
+ when AST::DeferStmt
334
+ _check_double_release_in_seq(stmt.body, result) if stmt.body.is_a?(Array)
335
+ end
336
+ end
337
+
338
+ scope_releases.each do |name, lines|
339
+ result[name] = lines if lines.length > 1
340
+ end
341
+ end
342
+
343
+ # ── Type resolution ────────────────────────────────────────────────
344
+
345
+ def resolve_local_owning_type(decl)
346
+ if decl.type && decl.type.is_a?(AST::TypeRef)
347
+ base = owning_base_name(decl.type.name.to_s)
348
+ return base if owning_type_by_name?(base)
349
+ end
350
+ if decl.value
351
+ type_name = extract_initializer_type(decl.value)
352
+ return type_name if type_name && owning_type_by_name?(type_name)
353
+ end
354
+ nil
355
+ end
356
+
357
+ def extract_initializer_type(expr)
358
+ return nil unless expr.is_a?(AST::Call)
359
+ return nil unless expr.arguments.empty?
360
+
361
+ callee = expr.callee
362
+ method_name = case callee
363
+ when AST::MemberAccess then callee.member
364
+ when AST::Specialization
365
+ inner = callee.callee
366
+ inner.is_a?(AST::MemberAccess) ? inner.member : nil
367
+ else nil
368
+ end
369
+ return nil unless %w[create with_capacity empty from_str].include?(method_name)
370
+
371
+ receiver = case callee
372
+ when AST::MemberAccess then callee.receiver
373
+ when AST::Specialization then callee
374
+ else nil
375
+ end
376
+
377
+ owning_type_from_receiver(receiver)
378
+ end
379
+
380
+ def owning_type_from_receiver(receiver)
381
+ case receiver
382
+ when AST::Identifier then owning_base_name(receiver.name)
383
+ when AST::MemberAccess then owning_base_name(receiver.member)
384
+ when AST::Specialization then owning_type_from_receiver(receiver.callee)
385
+ else nil
386
+ end
387
+ end
388
+
389
+ # ── Ownership transfer detection ──────────────────────────────────
390
+
391
+ def ownership_transferred?(stmts, name)
392
+ returned_name?(stmts, name) || struct_field_transfer?(stmts, name)
393
+ end
394
+
395
+ def returned_name?(stmts, name)
396
+ _any_stmt?(stmts) do |stmt|
397
+ stmt.is_a?(AST::ReturnStmt) &&
398
+ stmt.value.is_a?(AST::Identifier) &&
399
+ stmt.value.name == name
400
+ end
401
+ end
402
+
403
+ def struct_field_transfer?(stmts, name)
404
+ _any_stmt_or_expr?(stmts) { |expr| _expr_contains_transfer?(expr, name) }
405
+ end
406
+
407
+ def _expr_contains_transfer?(expr, name)
408
+ return false unless expr.is_a?(AST::Call)
409
+
410
+ expr.arguments.any? do |arg|
411
+ next false unless arg.is_a?(AST::Argument) && arg.name
412
+
413
+ if arg.value.is_a?(AST::Identifier) && arg.value.name == name
414
+ true
415
+ elsif arg.value.is_a?(AST::Call)
416
+ _expr_contains_transfer?(arg.value, name)
417
+ else
418
+ false
419
+ end
420
+ end
421
+ end
422
+
423
+ # ── Shared AST walkers ────────────────────────────────────────────
424
+
425
+ def all_released_names(stmts)
426
+ result = Set.new
427
+ _collect_released_names(stmts, result)
428
+ result
429
+ end
430
+
431
+ def _collect_released_names(stmts, result)
432
+ return if stmts.nil?
433
+ stmts.each { |s| _collect_released_names_in_stmt(s, result) }
434
+ end
435
+
436
+ def _collect_released_names_in_stmt(stmt, result)
437
+ case stmt
438
+ when AST::ExpressionStmt
439
+ _collect_released_names_in_expr(stmt.expression, result)
440
+ when AST::ReturnStmt
441
+ _collect_released_names_in_expr(stmt.value, result) if stmt.value
442
+ when AST::LocalDecl
443
+ _collect_released_names_in_expr(stmt.value, result) if stmt.value
444
+ when AST::Assignment
445
+ _collect_released_names_in_expr(stmt.value, result)
446
+ when AST::IfStmt
447
+ stmt.branches.each { |b| _collect_released_names(b.body, result) }
448
+ _collect_released_names(stmt.else_body, result) if stmt.else_body
449
+ when AST::WhileStmt
450
+ _collect_released_names(stmt.body, result)
451
+ when AST::ForStmt
452
+ _collect_released_names(stmt.body, result)
453
+ when AST::MatchStmt
454
+ stmt.arms.each { |arm| _collect_released_names(arm.body, result) }
455
+ when AST::UnsafeStmt
456
+ _collect_released_names(stmt.body, result)
457
+ when AST::DeferStmt
458
+ _collect_released_names_in_expr(stmt.expression, result) if stmt.expression
459
+ _collect_released_names(stmt.body, result) if stmt.body.is_a?(Array)
460
+ end
461
+ end
462
+
463
+ def _collect_released_names_in_expr(expr, result)
464
+ return if expr.nil?
465
+
466
+ result << expr.callee.receiver.name if release_call_on_binding?(expr)
467
+
468
+ case expr
469
+ when AST::Call
470
+ expr.arguments.each { |a| _collect_released_names_in_expr(a.value, result) }
471
+ _collect_released_names_in_expr(expr.callee, result)
472
+ when AST::MemberAccess
473
+ _collect_released_names_in_expr(expr.receiver, result)
474
+ when AST::BinaryOp
475
+ _collect_released_names_in_expr(expr.left, result)
476
+ _collect_released_names_in_expr(expr.right, result)
477
+ when AST::UnaryOp
478
+ _collect_released_names_in_expr(expr.operand, result)
479
+ end
480
+ end
481
+
482
+ def _any_stmt?(stmts, &pred)
483
+ return false if stmts.nil?
484
+
485
+ stmts.any? do |stmt|
486
+ if pred.call(stmt)
487
+ true
488
+ else
489
+ case stmt
490
+ when AST::IfStmt
491
+ stmt.branches.any? { |b| _any_stmt?(b.body, &pred) } ||
492
+ _any_stmt?(stmt.else_body, &pred)
493
+ when AST::WhileStmt then _any_stmt?(stmt.body, &pred)
494
+ when AST::ForStmt then _any_stmt?(stmt.body, &pred)
495
+ when AST::MatchStmt then stmt.arms.any? { |arm| _any_stmt?(arm.body, &pred) }
496
+ when AST::UnsafeStmt then _any_stmt?(stmt.body, &pred)
497
+ when AST::DeferStmt then stmt.body.is_a?(Array) && _any_stmt?(stmt.body, &pred)
498
+ else false
499
+ end
500
+ end
501
+ end
502
+ end
503
+
504
+ def _any_stmt_or_expr?(stmts, &pred)
505
+ _any_stmt?(stmts) do |stmt|
506
+ expr = extract_expr(stmt)
507
+ expr && pred.call(expr)
508
+ end
509
+ end
510
+
511
+ # ── CFG-based precision analysis ──────────────────────────────────
512
+
513
+ def emit_owning_release_cfg_warnings(cfg_analysis, body)
514
+ graph = cfg_analysis.graph
515
+ reachable_ids = cfg_analysis.reachability.reachable_ids
516
+ exit_kinds = %i[exit return break_exit continue_exit].to_set.freeze
517
+ owning_locals = owning_local_bindings(body)
518
+ return if owning_locals.empty?
519
+
520
+ owning_locals.each do |name, info|
521
+ decl_id = find_decl_node_id(graph, name)
522
+ next unless decl_id && reachable_ids.include?(decl_id)
523
+
524
+ releases, consumers, reassigns = classify_cfg_nodes(graph, reachable_ids, name)
525
+
526
+ unless every_path_to_exit_has_consumer?(graph, decl_id, consumers, exit_kinds, reachable_ids)
527
+ emit_owning_release_leak_warnings({ name => info })
528
+ end
529
+
530
+ releases.each do |rel_id|
531
+ reachable_release = find_reachable_release(graph, rel_id, releases, reassigns, reachable_ids)
532
+ next unless reachable_release
533
+
534
+ node = graph.nodes[reachable_release]
535
+ @warnings << Warning.new(
536
+ path: @path,
537
+ line: node&.line,
538
+ column: nil,
539
+ length: nil,
540
+ code: "owning-release-double",
541
+ message: "owning binding '#{name}' may be released more than once",
542
+ severity: :warning,
543
+ symbol_name: name,
544
+ )
545
+ break
546
+ end
547
+ end
548
+ end
549
+
550
+ def find_decl_node_id(graph, name)
551
+ graph.each_node do |node|
552
+ next unless node.kind == :local_decl
553
+ node.writes_info.each do |write|
554
+ return node.id if write[:origin] == :declaration && write[:name] == name
555
+ end
556
+ end
557
+ nil
558
+ end
559
+
560
+ def classify_cfg_nodes(graph, reachable_ids, name)
561
+ releases = Set.new
562
+ consumers = Set.new
563
+ reassigns = Set.new
564
+
565
+ graph.each_node do |node|
566
+ next unless reachable_ids.include?(node.id)
567
+ next unless node.statement
568
+
569
+ is_defer = node.statement.is_a?(AST::DeferStmt)
570
+
571
+ if release_call_in_stmt?(node.statement, name)
572
+ consumers << node.id
573
+ releases << node.id unless is_defer
574
+ end
575
+
576
+ if return_for_name?(node.statement, name) || struct_transfer_in_stmt?(node.statement, name)
577
+ consumers << node.id
578
+ end
579
+
580
+ if node.kind == :assignment
581
+ node.writes_info.each do |write|
582
+ reassigns << node.id if write[:origin] == :assignment && write[:name] == name
583
+ end
584
+ end
585
+ end
586
+
587
+ [releases, consumers, reassigns]
588
+ end
589
+
590
+ def release_call_in_stmt?(stmt, name)
591
+ return false unless stmt
592
+
593
+ # DeferStmt: inline form `defer x.release()` or block form `defer: …`
594
+ if stmt.is_a?(AST::DeferStmt)
595
+ return true if stmt.expression && _expr_has_release?(stmt.expression, name)
596
+ return stmt.body.is_a?(Array) && stmt.body.any? { |s| release_call_in_stmt?(s, name) }
597
+ end
598
+
599
+ expr = extract_expr(stmt)
600
+ expr && _expr_has_release?(expr, name)
601
+ end
602
+
603
+ def _expr_has_release?(expr, name)
604
+ return false unless expr
605
+
606
+ return true if release_call_on_binding?(expr) && expr.callee.receiver.name == name
607
+
608
+ case expr
609
+ when AST::Call
610
+ expr.arguments.any? { |a| _expr_has_release?(a.value, name) } ||
611
+ _expr_has_release?(expr.callee, name)
612
+ when AST::MemberAccess then _expr_has_release?(expr.receiver, name)
613
+ when AST::BinaryOp
614
+ _expr_has_release?(expr.left, name) || _expr_has_release?(expr.right, name)
615
+ when AST::UnaryOp then _expr_has_release?(expr.operand, name)
616
+ else false
617
+ end
618
+ end
619
+
620
+ def return_for_name?(stmt, name)
621
+ stmt.is_a?(AST::ReturnStmt) &&
622
+ stmt.value.is_a?(AST::Identifier) &&
623
+ stmt.value.name == name
624
+ end
625
+
626
+ def struct_transfer_in_stmt?(stmt, name)
627
+ expr = extract_expr(stmt)
628
+ expr && _expr_contains_transfer?(expr, name)
629
+ end
630
+
631
+ def every_path_to_exit_has_consumer?(graph, start_id, consumers, exit_kinds, reachable_ids)
632
+ visited = Set.new
633
+ queue = (graph.nodes[start_id]&.succs || []).dup
634
+ until queue.empty?
635
+ nid = queue.shift
636
+ next if visited.include?(nid)
637
+ visited << nid
638
+ next unless reachable_ids.include?(nid)
639
+
640
+ node = graph.nodes[nid]
641
+ next unless node
642
+
643
+ if consumers.include?(nid)
644
+ nil
645
+ elsif exit_kinds.include?(node.kind)
646
+ return false
647
+ else
648
+ node.succs.each { |s| queue << s }
649
+ end
650
+ end
651
+ true
652
+ end
653
+
654
+ def find_reachable_release(graph, start_id, release_ids, reassign_ids, reachable_ids)
655
+ visited = Set.new
656
+ queue = (graph.nodes[start_id]&.succs || []).dup
657
+ until queue.empty?
658
+ nid = queue.shift
659
+ next if visited.include?(nid)
660
+ visited << nid
661
+ next unless reachable_ids.include?(nid)
662
+
663
+ return nid if release_ids.include?(nid) && nid != start_id
664
+ next if reassign_ids.include?(nid)
665
+
666
+ node = graph.nodes[nid]
667
+ next unless node
668
+
669
+ queue.concat(node.succs)
670
+ end
671
+ nil
672
+ end
673
+
674
+ # ── Helpers ───────────────────────────────────────────────────────
675
+
676
+ def release_call_on_binding?(expr)
677
+ return false unless expr.is_a?(AST::Call)
678
+ return false unless expr.callee.is_a?(AST::MemberAccess)
679
+ return false unless expr.callee.member == "release"
680
+ return false unless expr.callee.receiver.is_a?(AST::Identifier)
681
+ return false unless expr.arguments.empty?
682
+ true
683
+ end
684
+
685
+ def reassigns_owning_binding?(stmt)
686
+ return false unless stmt.is_a?(AST::Assignment)
687
+ return false unless stmt.target.is_a?(AST::Identifier)
688
+ return false unless stmt.operator == "="
689
+
690
+ @scopes.reverse_each { |scope| return true if scope.key?(stmt.target.name) }
691
+ false
692
+ end
693
+
694
+ def extract_expr(stmt)
695
+ case stmt
696
+ when AST::ExpressionStmt then stmt.expression
697
+ when AST::ReturnStmt then stmt.value
698
+ when AST::LocalDecl then stmt.value
699
+ when AST::Assignment then stmt.value
700
+ else nil
701
+ end
702
+ end
703
+
704
+ def extract_line(stmt)
705
+ stmt.respond_to?(:line) ? stmt.line : nil
706
+ end
707
+
708
+ def owning_base_name(qname)
709
+ last_dot = qname.rindex(".")
710
+ last_dot ? qname[(last_dot + 1)..] : qname
711
+ end
712
+
713
+ def owning_type_by_name?(type_name)
714
+ return false unless type_name
715
+ @owning_type_set ||= build_owning_type_set
716
+ @owning_type_set.include?(type_name)
717
+ end
718
+
719
+ def build_owning_type_set
720
+ set = Set.new
721
+ return set unless @sema_facts
722
+
723
+ all_methods = [@sema_facts.methods]
724
+ (@imported_modules || {}).each_value do |mod_binding|
725
+ all_methods << mod_binding.methods if mod_binding.respond_to?(:methods)
726
+ end
727
+
728
+ all_methods.compact.each do |methods_hash|
729
+ methods_hash.each do |type, methods|
730
+ next unless methods.is_a?(Hash) && methods.key?("release")
731
+ set << type_base_name(type)
732
+ end
733
+ end
734
+ set
735
+ end
736
+
737
+ def type_base_name(type)
738
+ return nil unless type
739
+ return type.name.to_s if type.respond_to?(:name) && type.name
740
+ type.to_s
741
+ end
742
+ end
743
+ end
744
+ end