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
+ class Linter
5
+ module LinterFlowRules
6
+ private
7
+
8
+ def emit_dead_assignment_warnings(stmts)
9
+ analysis = dead_assignment_analysis(stmts)
10
+ return unless analysis
11
+
12
+ graph = analysis.graph
13
+ liveness = analysis.liveness
14
+ readable_bindings = analysis.readable_bindings
15
+ locally_declared = analysis.locally_declared
16
+
17
+ graph.each_node do |node|
18
+ node.writes_info.each do |write|
19
+ next if write[:origin] == :call_argument
20
+ next if write[:origin] == :declaration
21
+
22
+ binding_key = write[:binding_key]
23
+ name = write[:name]
24
+ next unless readable_bindings.include?(binding_key)
25
+ next unless locally_declared.include?(binding_key)
26
+ next if liveness.live_out[node.id].include?(binding_key)
27
+
28
+ @warnings << Warning.new(
29
+ path: @path,
30
+ line: write[:line],
31
+ column: write[:column],
32
+ length: name.length,
33
+ code: "dead-assignment",
34
+ message: "value assigned to '#{name}' is never read",
35
+ symbol_name: name
36
+ )
37
+ end
38
+ end
39
+ end
40
+ def emit_unreachable_warnings(stmts)
41
+ analysis = statement_flow_analysis(stmts)
42
+ return unless analysis
43
+
44
+ graph = analysis.graph
45
+ reachable = analysis.reachability
46
+
47
+ graph.each_node do |node|
48
+ next if reachable.reachable_ids.include?(node.id)
49
+ next if node.kind == :exit
50
+ next unless node.statement
51
+
52
+ statement = node.statement
53
+ line = statement.respond_to?(:line) ? statement.line : nil
54
+ @warnings << Warning.new(
55
+ path: @path,
56
+ line:,
57
+ column: statement_column(statement),
58
+ length: statement_length(statement),
59
+ code: "unreachable-code",
60
+ message: "unreachable code"
61
+ )
62
+ end
63
+ end
64
+ # Detects obvious aliasing hazards: a mutable reference (ref_of / ptr_of)
65
+ # is taken from a local variable that is also written later in the same body.
66
+ def emit_borrow_warnings(stmts)
67
+ return if stmts.nil? || stmts.empty?
68
+
69
+ borrowed = collect_borrowed_names(stmts)
70
+ return if borrowed.empty?
71
+
72
+ written = collect_written_names(stmts)
73
+ (borrowed & written).each do |name|
74
+ # Find the earliest borrow site for the warning location
75
+ borrow_line, borrow_column, borrow_length = find_borrow_location(stmts, name)
76
+ @warnings << Warning.new(
77
+ path: @path,
78
+ line: borrow_line,
79
+ column: borrow_column,
80
+ length: borrow_length,
81
+ code: "borrow-and-mutate",
82
+ message: "'#{name}' is borrowed via ref_of/ptr_of and also mutated in the same scope — potential aliasing hazard",
83
+ severity: :warning,
84
+ symbol_name: name,
85
+ )
86
+ end
87
+ end
88
+ def cfg_binding_resolution
89
+ return @cfg_binding_resolution if @cfg_binding_resolution_computed
90
+
91
+ binding_resolution = @sema_facts&.binding_resolution
92
+ @cfg_binding_resolution = if binding_resolution
93
+ ControlFlow::BindingResolution.new(
94
+ identifier_binding_ids: binding_resolution.identifier_binding_ids,
95
+ declaration_binding_ids: binding_resolution.declaration_binding_ids,
96
+ mutating_argument_identifier_ids: binding_resolution.mutating_argument_identifier_ids,
97
+ )
98
+ end
99
+ @cfg_binding_resolution_computed = true
100
+ @cfg_binding_resolution
101
+ end
102
+ def statement_flow_analysis(stmts)
103
+ return nil if stmts.nil? || stmts.empty?
104
+
105
+ @statement_flow_analysis_cache[stmts.object_id] ||= begin
106
+ binding_resolution = cfg_binding_resolution
107
+ graph = profile_phase("flow.graph") do
108
+ ControlFlow::Builder.new(ignore_name: method(:ignored_binding_name?), binding_resolution:).build(stmts)
109
+ end
110
+ reachability = profile_phase("flow.reachability") { ControlFlow::Reachability.solve(graph) }
111
+ nullability = profile_phase("flow.nullability") { ControlFlow::NullabilityFlow.solve(graph) }
112
+ constant_propagation = profile_phase("flow.constant_propagation") do
113
+ ControlFlow::ConstantPropagation.solve(graph, binding_resolution:, strict_binding_ids: !binding_resolution.nil?)
114
+ end
115
+ loop_body_nodes = profile_phase("flow.loop_body_nodes") { compute_loop_body_nodes(graph) }
116
+ StatementFlowAnalysis.new(graph:, reachability:, nullability:, constant_propagation:, loop_body_nodes:)
117
+ end
118
+ end
119
+ def dead_assignment_analysis(stmts)
120
+ return nil if stmts.nil? || stmts.empty?
121
+
122
+ @dead_assignment_analysis_cache[stmts.object_id] ||= begin
123
+ binding_resolution = cfg_binding_resolution
124
+ graph = profile_phase("dead_assignment.graph") do
125
+ ControlFlow::Builder.new(
126
+ ignore_name: method(:ignored_binding_name?),
127
+ binding_resolution:,
128
+ local_decl_without_initializer_writes: true,
129
+ ).build(stmts)
130
+ end
131
+ liveness = profile_phase("dead_assignment.liveness") { ControlFlow::Liveness.solve(graph) }
132
+ locally_declared = profile_phase("dead_assignment.locals") do
133
+ graph.each_node.each_with_object(Set.new) do |node, bindings|
134
+ node.writes_info.each do |write|
135
+ bindings << write[:binding_key] if write[:origin] == :declaration
136
+ end
137
+ end
138
+ end
139
+ DeadAssignmentAnalysis.new(
140
+ graph:,
141
+ liveness:,
142
+ readable_bindings: graph.read_bindings,
143
+ locally_declared:,
144
+ )
145
+ end
146
+ end
147
+ def cfg_identifier_binding_key(identifier)
148
+ binding_resolution = cfg_binding_resolution
149
+ return identifier.name unless binding_resolution
150
+
151
+ binding_resolution.identifier_binding_ids[identifier.object_id] || identifier.name
152
+ end
153
+ def ignored_binding_name?(name)
154
+ return true unless name
155
+
156
+ name == "_" || name.start_with?("_")
157
+ end
158
+ # ── constant-condition ─────────────────────────────────────────────────
159
+ # Uses ConstantPropagation to detect conditions that are always true/false.
160
+ # Skips `while true` — it is an idiomatic infinite loop.
161
+ # Skips if conditions inside loops, since variables can change across iterations.
162
+
163
+ def emit_constant_condition_warnings(stmts)
164
+ return if stmts.nil? || stmts.empty?
165
+
166
+ analysis = statement_flow_analysis(stmts)
167
+ return unless analysis
168
+
169
+ binding_resolution = cfg_binding_resolution
170
+ graph = analysis.graph
171
+ cp = analysis.constant_propagation
172
+ loop_bodies = analysis.loop_body_nodes
173
+
174
+ graph.each_node do |node|
175
+ cond_expr, line, keyword_pattern, skip_node =
176
+ case node.kind
177
+ when :if_condition
178
+ branch = node.statement
179
+ # Skip if conditions inside loops; variables can change across iterations.
180
+ skip = loop_bodies.include?(node.id)
181
+ [branch&.condition, branch&.line || node.line, "else if|if", skip]
182
+ when :while_condition
183
+ wstmt = node.statement
184
+ # `while true` is an idiomatic infinite loop — do not warn
185
+ skip = wstmt&.condition.is_a?(AST::BooleanLiteral) && wstmt.condition.value == true
186
+ condition = wstmt&.condition
187
+ [condition, node.line, "while", skip]
188
+ else
189
+ next
190
+ end
191
+
192
+ next if skip_node || cond_expr.nil?
193
+
194
+ in_state = cp.in_states[node.id] || {}
195
+ const_val = ControlFlow::ConstantPropagation.constant_value_of(
196
+ cond_expr,
197
+ in_state,
198
+ binding_resolution:,
199
+ strict_binding_ids: !binding_resolution.nil?
200
+ )
201
+ next unless const_val == true || const_val == false
202
+
203
+ ctx = node.kind == :while_condition ? "loop condition" : "branch condition"
204
+ line, column, length = condition_span(cond_expr, line:, keyword_pattern:)
205
+ @warnings << Warning.new(
206
+ path: @path,
207
+ line:,
208
+ column:,
209
+ length:,
210
+ code: "constant-condition",
211
+ message: "#{ctx} is always #{const_val}",
212
+ severity: :warning,
213
+ symbol_name: condition_symbol_name(cond_expr)
214
+ )
215
+ end
216
+ end
217
+ # Returns the set of node IDs that are inside loops (reachable from a back-edge).
218
+ # A back-edge exists when a node is reachable from its own successors.
219
+ private def compute_loop_body_nodes(graph)
220
+ loop_nodes = Set.new
221
+
222
+ # Find all back-edges: detect cycles by checking if any successor of a node
223
+ # can reach back to that node.
224
+ graph.each_node do |node|
225
+ node.succs.each do |succ_id|
226
+ # Check if succ can reach back to node (indicating a loop/cycle).
227
+ if reachable_from?(graph, succ_id, node.id)
228
+ # Mark all nodes reachable from succ (the loop body) as inside a loop.
229
+ mark_reachable_nodes(graph, succ_id, loop_nodes)
230
+ end
231
+ end
232
+ end
233
+
234
+ loop_nodes
235
+ end
236
+
237
+ # Returns true if target_id is reachable from start_id via forward edges.
238
+ private def reachable_from?(graph, start_id, target_id)
239
+ visited = Set.new
240
+ queue = [start_id]
241
+
242
+ while queue.any?
243
+ node_id = queue.shift
244
+ return true if node_id == target_id
245
+ next if visited.include?(node_id)
246
+
247
+ visited.add(node_id)
248
+ node = graph.nodes[node_id]
249
+ node.succs.each { |succ| queue.push(succ) }
250
+ end
251
+
252
+ false
253
+ end
254
+
255
+ # Mark all nodes reachable from start_id as being inside a loop.
256
+ private def mark_reachable_nodes(graph, start_id, loop_nodes)
257
+ visited = Set.new
258
+ queue = [start_id]
259
+
260
+ while queue.any?
261
+ node_id = queue.shift
262
+ loop_nodes.add(node_id)
263
+ next if visited.include?(node_id)
264
+
265
+ visited.add(node_id)
266
+ node = graph.nodes[node_id]
267
+ node.succs.each { |succ| queue.push(succ) unless visited.include?(succ) }
268
+ end
269
+ end
270
+ # ── redundant-null-check ───────────────────────────────────────────────
271
+ # After a variable has been narrowed to non-null by a prior check,
272
+ # a subsequent `x != nil` guard is always true.
273
+
274
+ def emit_redundant_null_check_warnings(stmts)
275
+ return if stmts.nil? || stmts.empty?
276
+
277
+ analysis = statement_flow_analysis(stmts)
278
+ return unless analysis
279
+
280
+ graph = analysis.graph
281
+ nf = analysis.nullability
282
+
283
+ graph.each_node do |node|
284
+ next unless node.kind == :if_condition
285
+
286
+ branch = node.statement
287
+ next unless branch.is_a?(AST::IfBranch)
288
+
289
+ identifier = null_check_identifier(branch.condition)
290
+ next unless identifier
291
+ next if ignored_binding_name?(identifier.name)
292
+
293
+ nonnull = nf.nonnull_before(branch)
294
+ next unless nonnull.include?(cfg_identifier_binding_key(identifier))
295
+
296
+ line, column, length = condition_span(branch.condition, line: node.line, keyword_pattern: "else if|if")
297
+
298
+ @warnings << Warning.new(
299
+ path: @path,
300
+ line:,
301
+ column:,
302
+ length:,
303
+ code: "redundant-null-check",
304
+ message: "'#{identifier.name}' is already known to be non-null here — this nil check is redundant",
305
+ severity: :hint,
306
+ symbol_name: identifier.name
307
+ )
308
+ end
309
+ end
310
+
311
+ # Returns the Identifier being nil-tested if `cond` is `x != nil` or
312
+ # `nil != x`, otherwise nil.
313
+ def null_check_identifier(cond)
314
+ return nil unless cond.is_a?(AST::BinaryOp) && cond.operator == "!="
315
+
316
+ if cond.left.is_a?(AST::Identifier) && cond.right.is_a?(AST::NullLiteral)
317
+ cond.left
318
+ elsif cond.left.is_a?(AST::NullLiteral) && cond.right.is_a?(AST::Identifier)
319
+ cond.right
320
+ end
321
+ end
322
+ # ── loop-single-iteration ──────────────────────────────────────────────
323
+ # A loop whose body unconditionally exits (return/break) before the
324
+ # back-edge is taken will execute at most once.
325
+
326
+ def emit_loop_single_iteration_warnings(stmts)
327
+ return if stmts.nil? || stmts.empty?
328
+
329
+ walk_stmts_for_loop_check(stmts)
330
+ end
331
+
332
+ def walk_stmts_for_loop_check(stmts)
333
+ stmts.each do |stmt|
334
+ case stmt
335
+ when AST::WhileStmt
336
+ body = stmt.body || []
337
+ if !body.empty? && ControlFlow::Termination.loop_body_always_exits?(body)
338
+ @warnings << Warning.new(
339
+ path: @path,
340
+ line: stmt.line,
341
+ column: stmt.column,
342
+ length: stmt.length || "while".length,
343
+ code: "loop-single-iteration",
344
+ message: "loop body always exits on the first iteration - consider replacing with an 'if' block",
345
+ severity: :warning
346
+ )
347
+ end
348
+ walk_stmts_for_loop_check(body)
349
+ when AST::ForStmt
350
+ body = stmt.body || []
351
+ if !body.empty? && ControlFlow::Termination.loop_body_always_exits?(body)
352
+ @warnings << Warning.new(
353
+ path: @path,
354
+ line: stmt.line,
355
+ column: stmt.column,
356
+ length: (stmt.respond_to?(:length) ? stmt.length : nil) || "for".length,
357
+ code: "loop-single-iteration",
358
+ message: "loop body always exits on the first iteration - consider iterating directly without a loop",
359
+ severity: :warning
360
+ )
361
+ end
362
+ walk_stmts_for_loop_check(body)
363
+ when AST::IfStmt
364
+ stmt.branches.each { |b| walk_stmts_for_loop_check(b.body) }
365
+ walk_stmts_for_loop_check(stmt.else_body) if stmt.else_body
366
+ when AST::MatchStmt
367
+ stmt.arms.each { |arm| walk_stmts_for_loop_check(arm.body) }
368
+ when AST::UnsafeStmt
369
+ walk_stmts_for_loop_check(stmt.body) if stmt.body
370
+ when AST::DeferStmt
371
+ walk_stmts_for_loop_check(stmt.body) if stmt.body
372
+ when AST::WhenStmt
373
+ stmt.branches.each { |b| walk_stmts_for_loop_check(b.body) }
374
+ walk_stmts_for_loop_check(stmt.else_body) if stmt.else_body
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end