konpeito 0.1.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 (180) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +75 -0
  4. data/CONTRIBUTING.md +123 -0
  5. data/LICENSE +21 -0
  6. data/README.md +257 -0
  7. data/Rakefile +11 -0
  8. data/bin/konpeito +6 -0
  9. data/konpeito.gemspec +43 -0
  10. data/lib/konpeito/ast/typed_ast.rb +620 -0
  11. data/lib/konpeito/ast/visitor.rb +78 -0
  12. data/lib/konpeito/cache/cache_manager.rb +230 -0
  13. data/lib/konpeito/cache/dependency_graph.rb +192 -0
  14. data/lib/konpeito/cache.rb +8 -0
  15. data/lib/konpeito/cli/base_command.rb +187 -0
  16. data/lib/konpeito/cli/build_command.rb +220 -0
  17. data/lib/konpeito/cli/check_command.rb +104 -0
  18. data/lib/konpeito/cli/config.rb +231 -0
  19. data/lib/konpeito/cli/deps_command.rb +128 -0
  20. data/lib/konpeito/cli/doctor_command.rb +340 -0
  21. data/lib/konpeito/cli/fmt_command.rb +199 -0
  22. data/lib/konpeito/cli/init_command.rb +312 -0
  23. data/lib/konpeito/cli/lsp_command.rb +40 -0
  24. data/lib/konpeito/cli/run_command.rb +150 -0
  25. data/lib/konpeito/cli/test_command.rb +248 -0
  26. data/lib/konpeito/cli/watch_command.rb +212 -0
  27. data/lib/konpeito/cli.rb +301 -0
  28. data/lib/konpeito/codegen/builtin_methods.rb +229 -0
  29. data/lib/konpeito/codegen/cruby_backend.rb +1090 -0
  30. data/lib/konpeito/codegen/debug_info.rb +352 -0
  31. data/lib/konpeito/codegen/inliner.rb +486 -0
  32. data/lib/konpeito/codegen/jvm_backend.rb +197 -0
  33. data/lib/konpeito/codegen/jvm_generator.rb +13412 -0
  34. data/lib/konpeito/codegen/llvm_generator.rb +13191 -0
  35. data/lib/konpeito/codegen/loop_optimizer.rb +363 -0
  36. data/lib/konpeito/codegen/monomorphizer.rb +359 -0
  37. data/lib/konpeito/codegen/profile_runtime.c +341 -0
  38. data/lib/konpeito/codegen/profiler.rb +99 -0
  39. data/lib/konpeito/compiler.rb +592 -0
  40. data/lib/konpeito/dependency_resolver.rb +296 -0
  41. data/lib/konpeito/diagnostics/collector.rb +127 -0
  42. data/lib/konpeito/diagnostics/diagnostic.rb +237 -0
  43. data/lib/konpeito/diagnostics/renderer.rb +144 -0
  44. data/lib/konpeito/formatter/formatter.rb +1214 -0
  45. data/lib/konpeito/hir/builder.rb +7167 -0
  46. data/lib/konpeito/hir/nodes.rb +2465 -0
  47. data/lib/konpeito/lsp/document_manager.rb +820 -0
  48. data/lib/konpeito/lsp/server.rb +183 -0
  49. data/lib/konpeito/lsp/transport.rb +38 -0
  50. data/lib/konpeito/parser/prism_adapter.rb +65 -0
  51. data/lib/konpeito/platform.rb +103 -0
  52. data/lib/konpeito/profile/report.rb +136 -0
  53. data/lib/konpeito/rbs_inline/preprocessor.rb +199 -0
  54. data/lib/konpeito/stdlib/compression/compression.rb +72 -0
  55. data/lib/konpeito/stdlib/compression/compression.rbs +60 -0
  56. data/lib/konpeito/stdlib/compression/compression_native.c +415 -0
  57. data/lib/konpeito/stdlib/compression/extconf.rb +19 -0
  58. data/lib/konpeito/stdlib/crypto/crypto.rb +85 -0
  59. data/lib/konpeito/stdlib/crypto/crypto.rbs +74 -0
  60. data/lib/konpeito/stdlib/crypto/crypto_native.c +312 -0
  61. data/lib/konpeito/stdlib/crypto/extconf.rb +40 -0
  62. data/lib/konpeito/stdlib/http/extconf.rb +19 -0
  63. data/lib/konpeito/stdlib/http/http.rb +125 -0
  64. data/lib/konpeito/stdlib/http/http.rbs +57 -0
  65. data/lib/konpeito/stdlib/http/http_native.c +440 -0
  66. data/lib/konpeito/stdlib/json/extconf.rb +17 -0
  67. data/lib/konpeito/stdlib/json/json.rb +44 -0
  68. data/lib/konpeito/stdlib/json/json.rbs +33 -0
  69. data/lib/konpeito/stdlib/json/json_native.c +286 -0
  70. data/lib/konpeito/stdlib/ui/extconf.rb +216 -0
  71. data/lib/konpeito/stdlib/ui/konpeito_ui_native.cpp +1625 -0
  72. data/lib/konpeito/stdlib/ui/konpeito_ui_native.h +162 -0
  73. data/lib/konpeito/stdlib/ui/ui.rb +318 -0
  74. data/lib/konpeito/stdlib/ui/ui.rbs +247 -0
  75. data/lib/konpeito/type_checker/annotation_parser.rb +67 -0
  76. data/lib/konpeito/type_checker/hm_inferrer.rb +2565 -0
  77. data/lib/konpeito/type_checker/inferrer.rb +565 -0
  78. data/lib/konpeito/type_checker/rbs_loader.rb +1621 -0
  79. data/lib/konpeito/type_checker/type_resolver.rb +276 -0
  80. data/lib/konpeito/type_checker/types.rb +1434 -0
  81. data/lib/konpeito/type_checker/unification.rb +323 -0
  82. data/lib/konpeito/ui/animation/animated_state.rb +80 -0
  83. data/lib/konpeito/ui/animation/easing.rb +59 -0
  84. data/lib/konpeito/ui/animation/value_tween.rb +66 -0
  85. data/lib/konpeito/ui/app.rb +379 -0
  86. data/lib/konpeito/ui/box.rb +38 -0
  87. data/lib/konpeito/ui/castella.rb +70 -0
  88. data/lib/konpeito/ui/castella_native.rb +76 -0
  89. data/lib/konpeito/ui/chart/area_chart.rb +305 -0
  90. data/lib/konpeito/ui/chart/bar_chart.rb +288 -0
  91. data/lib/konpeito/ui/chart/base_chart.rb +210 -0
  92. data/lib/konpeito/ui/chart/chart_helpers.rb +79 -0
  93. data/lib/konpeito/ui/chart/gauge_chart.rb +171 -0
  94. data/lib/konpeito/ui/chart/heatmap_chart.rb +222 -0
  95. data/lib/konpeito/ui/chart/line_chart.rb +289 -0
  96. data/lib/konpeito/ui/chart/pie_chart.rb +219 -0
  97. data/lib/konpeito/ui/chart/scales.rb +77 -0
  98. data/lib/konpeito/ui/chart/scatter_chart.rb +303 -0
  99. data/lib/konpeito/ui/chart/stacked_bar_chart.rb +276 -0
  100. data/lib/konpeito/ui/column.rb +271 -0
  101. data/lib/konpeito/ui/core.rb +2199 -0
  102. data/lib/konpeito/ui/dsl.rb +443 -0
  103. data/lib/konpeito/ui/frame.rb +171 -0
  104. data/lib/konpeito/ui/frame_native.rb +494 -0
  105. data/lib/konpeito/ui/markdown/ast.rb +124 -0
  106. data/lib/konpeito/ui/markdown/mermaid/layout.rb +387 -0
  107. data/lib/konpeito/ui/markdown/mermaid/models.rb +232 -0
  108. data/lib/konpeito/ui/markdown/mermaid/parser.rb +519 -0
  109. data/lib/konpeito/ui/markdown/mermaid/renderer.rb +336 -0
  110. data/lib/konpeito/ui/markdown/parser.rb +805 -0
  111. data/lib/konpeito/ui/markdown/renderer.rb +639 -0
  112. data/lib/konpeito/ui/markdown/theme.rb +165 -0
  113. data/lib/konpeito/ui/render_node.rb +260 -0
  114. data/lib/konpeito/ui/row.rb +207 -0
  115. data/lib/konpeito/ui/spacer.rb +18 -0
  116. data/lib/konpeito/ui/style.rb +799 -0
  117. data/lib/konpeito/ui/theme.rb +563 -0
  118. data/lib/konpeito/ui/themes/material.rb +35 -0
  119. data/lib/konpeito/ui/themes/tokyo_night.rb +6 -0
  120. data/lib/konpeito/ui/widgets/button.rb +103 -0
  121. data/lib/konpeito/ui/widgets/calendar.rb +1034 -0
  122. data/lib/konpeito/ui/widgets/checkbox.rb +119 -0
  123. data/lib/konpeito/ui/widgets/container.rb +91 -0
  124. data/lib/konpeito/ui/widgets/data_table.rb +667 -0
  125. data/lib/konpeito/ui/widgets/divider.rb +29 -0
  126. data/lib/konpeito/ui/widgets/image.rb +105 -0
  127. data/lib/konpeito/ui/widgets/input.rb +485 -0
  128. data/lib/konpeito/ui/widgets/markdown.rb +57 -0
  129. data/lib/konpeito/ui/widgets/modal.rb +163 -0
  130. data/lib/konpeito/ui/widgets/multiline_input.rb +968 -0
  131. data/lib/konpeito/ui/widgets/multiline_text.rb +180 -0
  132. data/lib/konpeito/ui/widgets/net_image.rb +100 -0
  133. data/lib/konpeito/ui/widgets/progress_bar.rb +70 -0
  134. data/lib/konpeito/ui/widgets/radio_buttons.rb +93 -0
  135. data/lib/konpeito/ui/widgets/slider.rb +133 -0
  136. data/lib/konpeito/ui/widgets/switch.rb +84 -0
  137. data/lib/konpeito/ui/widgets/tabs.rb +157 -0
  138. data/lib/konpeito/ui/widgets/text.rb +110 -0
  139. data/lib/konpeito/ui/widgets/tree.rb +426 -0
  140. data/lib/konpeito/version.rb +5 -0
  141. data/lib/konpeito.rb +109 -0
  142. data/test_native_array.rb +172 -0
  143. data/test_native_array_class.rb +197 -0
  144. data/test_native_class.rb +151 -0
  145. data/tools/konpeito-asm/build.sh +65 -0
  146. data/tools/konpeito-asm/lib/asm-9.7.1.jar +0 -0
  147. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
  148. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
  149. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
  150. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
  151. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
  152. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
  153. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
  154. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
  155. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
  156. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
  157. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
  158. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
  159. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
  160. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
  161. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
  162. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
  163. data/tools/konpeito-asm/src/ClassIntrospector.java +312 -0
  164. data/tools/konpeito-asm/src/KonpeitoAssembler.java +659 -0
  165. data/tools/konpeito-asm/src/konpeito/runtime/KArray.java +390 -0
  166. data/tools/konpeito-asm/src/konpeito/runtime/KCompression.java +168 -0
  167. data/tools/konpeito-asm/src/konpeito/runtime/KConditionVariable.java +48 -0
  168. data/tools/konpeito-asm/src/konpeito/runtime/KCrypto.java +151 -0
  169. data/tools/konpeito-asm/src/konpeito/runtime/KFile.java +100 -0
  170. data/tools/konpeito-asm/src/konpeito/runtime/KHTTP.java +113 -0
  171. data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +228 -0
  172. data/tools/konpeito-asm/src/konpeito/runtime/KJSON.java +405 -0
  173. data/tools/konpeito-asm/src/konpeito/runtime/KMath.java +54 -0
  174. data/tools/konpeito-asm/src/konpeito/runtime/KRactor.java +244 -0
  175. data/tools/konpeito-asm/src/konpeito/runtime/KRactorPort.java +53 -0
  176. data/tools/konpeito-asm/src/konpeito/runtime/KSizedQueue.java +49 -0
  177. data/tools/konpeito-asm/src/konpeito/runtime/KThread.java +49 -0
  178. data/tools/konpeito-asm/src/konpeito/runtime/KTime.java +53 -0
  179. data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +416 -0
  180. metadata +267 -0
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Konpeito
4
+ module Codegen
5
+ # Optimizes loops in HIR by:
6
+ # 1. Detecting natural loop structures (while/until)
7
+ # 2. Hoisting loop-invariant instructions to a preheader block
8
+ # 3. Recognizing known-pure method calls that can be safely moved
9
+ #
10
+ # This complements LLVM's opt passes which cannot optimize opaque
11
+ # rb_funcallv calls. HIR-level LICM can hoist calls like .length/.size
12
+ # that we know are side-effect-free.
13
+ class LoopOptimizer
14
+ # Methods known to be pure (no side effects, same result for same args)
15
+ PURE_METHODS = Set.new(%i[
16
+ length size count frozen? nil? empty?
17
+ first last class is_a? kind_of? instance_of?
18
+ equal? respond_to? object_id hash
19
+ abs ceil floor round truncate
20
+ even? odd? zero? positive? negative?
21
+ integer? float? finite? infinite? nan?
22
+ to_i to_f to_r to_c
23
+ min max minmax
24
+ ascii_only? encoding bytesize
25
+ key? has_key? include? member?
26
+ keys values
27
+ ]).freeze
28
+
29
+ attr_reader :hoisted_count
30
+
31
+ def initialize(hir_program)
32
+ @hir_program = hir_program
33
+ @hoisted_count = 0
34
+ end
35
+
36
+ def optimize
37
+ @hir_program.functions.each do |func|
38
+ optimize_function(func)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Represents a natural loop in the CFG
45
+ LoopInfo = Struct.new(:header, :body_blocks, :exit_block, :preheader_source, keyword_init: true)
46
+
47
+ def optimize_function(func)
48
+ block_map = build_block_map(func)
49
+ loops = detect_loops(func, block_map)
50
+
51
+ loops.each do |loop_info|
52
+ hoist_invariants(func, loop_info, block_map)
53
+ end
54
+ end
55
+
56
+ # Build a map from block label to BasicBlock
57
+ def build_block_map(func)
58
+ map = {}
59
+ func.body.each { |block| map[block.label] = block }
60
+ map
61
+ end
62
+
63
+ # Detect natural loops by finding back edges (Jump from body → cond)
64
+ def detect_loops(func, block_map)
65
+ loops = []
66
+
67
+ func.body.each_with_index do |block, idx|
68
+ # Look for the pattern:
69
+ # block_before → Jump(while_cond)
70
+ # while_cond → Branch(while_body, while_exit)
71
+ # while_body → Jump(while_cond) [back edge]
72
+ next unless block.label =~ /^(while|until)_cond/
73
+
74
+ cond_block = block
75
+ term = cond_block.terminator
76
+ next unless term.is_a?(HIR::Branch)
77
+
78
+ # Determine body and exit based on loop type
79
+ if cond_block.label.start_with?("while_cond")
80
+ body_label = term.then_block
81
+ exit_label = term.else_block
82
+ else # until_cond - condition is inverted
83
+ body_label = term.else_block
84
+ exit_label = term.then_block
85
+ end
86
+
87
+ body_block = block_map[body_label]
88
+ next unless body_block
89
+
90
+ # Verify back edge exists (body → cond)
91
+ back_edge = has_back_edge?(body_block, cond_block.label, block_map)
92
+ next unless back_edge
93
+
94
+ # Find the block that jumps to cond (preheader source)
95
+ preheader = find_preheader_source(func, cond_block.label, body_label, block_map)
96
+
97
+ # Collect all blocks that form the loop body
98
+ body_blocks = collect_loop_body_blocks(cond_block.label, body_label, block_map)
99
+
100
+ loops << LoopInfo.new(
101
+ header: cond_block,
102
+ body_blocks: body_blocks,
103
+ exit_block: block_map[exit_label],
104
+ preheader_source: preheader
105
+ )
106
+ end
107
+
108
+ loops
109
+ end
110
+
111
+ # Check if a block (or any successor within the loop) has a back edge to header
112
+ def has_back_edge?(block, header_label, block_map)
113
+ return true if block.terminator.is_a?(HIR::Jump) && block.terminator.target == header_label
114
+ if block.terminator.is_a?(HIR::Branch)
115
+ return true if block.terminator.then_block == header_label
116
+ return true if block.terminator.else_block == header_label
117
+ end
118
+ false
119
+ end
120
+
121
+ # Find the block that jumps to the loop header (not from within the loop)
122
+ def find_preheader_source(func, header_label, body_label, block_map)
123
+ func.body.each do |block|
124
+ next if block.label == body_label
125
+ next if block.label == header_label
126
+ next if block.label =~ /^(while|until)_body/ && block.terminator.is_a?(HIR::Jump) && block.terminator.target == header_label
127
+
128
+ if block.terminator.is_a?(HIR::Jump) && block.terminator.target == header_label
129
+ return block
130
+ end
131
+ end
132
+ nil
133
+ end
134
+
135
+ # Collect all basic blocks that are part of the loop body
136
+ def collect_loop_body_blocks(header_label, body_label, block_map)
137
+ body_block = block_map[body_label]
138
+ return [body_block] unless body_block
139
+
140
+ # Simple: just return the body block for now
141
+ # For nested structures (if/else inside loop), we'd need to trace all reachable blocks
142
+ # that eventually jump back to the header
143
+ blocks = [body_block]
144
+ visited = Set.new([header_label, body_label])
145
+
146
+ # BFS to find all blocks within the loop
147
+ queue = []
148
+ term = body_block.terminator
149
+ if term.is_a?(HIR::Branch)
150
+ queue << term.then_block unless visited.include?(term.then_block)
151
+ queue << term.else_block unless visited.include?(term.else_block)
152
+ end
153
+
154
+ while (label = queue.shift)
155
+ next if visited.include?(label)
156
+ visited << label
157
+
158
+ block = block_map[label]
159
+ next unless block
160
+
161
+ # Check if this block is within the loop (eventually reaches header)
162
+ if block_reaches_header?(block, header_label, block_map, Set.new)
163
+ blocks << block
164
+ term = block.terminator
165
+ if term.is_a?(HIR::Jump)
166
+ queue << term.target unless visited.include?(term.target)
167
+ elsif term.is_a?(HIR::Branch)
168
+ queue << term.then_block unless visited.include?(term.then_block)
169
+ queue << term.else_block unless visited.include?(term.else_block)
170
+ end
171
+ end
172
+ end
173
+
174
+ blocks
175
+ end
176
+
177
+ # Check if a block can reach the loop header (part of the loop)
178
+ def block_reaches_header?(block, header_label, block_map, visited)
179
+ return false if visited.include?(block.label)
180
+ visited << block.label
181
+
182
+ term = block.terminator
183
+ return false unless term
184
+
185
+ if term.is_a?(HIR::Jump)
186
+ return true if term.target == header_label
187
+ next_block = block_map[term.target]
188
+ return next_block && block_reaches_header?(next_block, header_label, block_map, visited)
189
+ elsif term.is_a?(HIR::Branch)
190
+ return true if term.then_block == header_label || term.else_block == header_label
191
+ then_block = block_map[term.then_block]
192
+ else_block = block_map[term.else_block]
193
+ (then_block && block_reaches_header?(then_block, header_label, block_map, visited)) ||
194
+ (else_block && block_reaches_header?(else_block, header_label, block_map, visited))
195
+ else
196
+ false
197
+ end
198
+ end
199
+
200
+ # Hoist loop-invariant instructions from loop body to preheader
201
+ def hoist_invariants(func, loop_info, block_map)
202
+ return unless loop_info.preheader_source
203
+
204
+ # Collect variables modified inside the loop (cond + body blocks)
205
+ modified_vars = collect_modified_vars(loop_info)
206
+
207
+ # Collect variables used in the loop condition
208
+ cond_vars = collect_referenced_vars_in_block(loop_info.header)
209
+
210
+ # Find invariant instructions in the loop condition block
211
+ # (most common: arr.length in `while i < arr.length`)
212
+ invariants_from_cond = find_invariant_instructions(
213
+ loop_info.header, modified_vars, loop_info
214
+ )
215
+
216
+ # Find invariant instructions in body blocks
217
+ invariants_from_body = []
218
+ loop_info.body_blocks.each do |body_block|
219
+ invariants_from_body.concat(
220
+ find_invariant_instructions(body_block, modified_vars, loop_info)
221
+ )
222
+ end
223
+
224
+ all_invariants = invariants_from_cond + invariants_from_body
225
+ return if all_invariants.empty?
226
+
227
+ # Move invariant instructions to the preheader
228
+ preheader = loop_info.preheader_source
229
+ all_invariants.each do |inv|
230
+ source_block = inv[:block]
231
+ inst = inv[:instruction]
232
+
233
+ # Remove from source block
234
+ source_block.instructions.delete(inst)
235
+
236
+ # Add to preheader (before terminator, which is conceptually at the end)
237
+ preheader.instructions << inst
238
+
239
+ @hoisted_count += 1
240
+ end
241
+ end
242
+
243
+ # Collect all variables that are written to inside the loop
244
+ def collect_modified_vars(loop_info)
245
+ modified = Set.new
246
+
247
+ all_blocks = [loop_info.header] + loop_info.body_blocks
248
+ all_blocks.each do |block|
249
+ block.instructions.each do |inst|
250
+ case inst
251
+ when HIR::StoreLocal
252
+ modified << inst.var.to_s
253
+ when HIR::StoreInstanceVar
254
+ modified << inst.name.to_s
255
+ when HIR::StoreClassVar
256
+ modified << inst.name.to_s
257
+ end
258
+
259
+ # Instructions that produce results are also "modified"
260
+ if inst.respond_to?(:result_var) && inst.result_var
261
+ modified << inst.result_var.to_s
262
+ end
263
+ end
264
+ end
265
+
266
+ modified
267
+ end
268
+
269
+ # Collect variables referenced in a block's instructions
270
+ def collect_referenced_vars_in_block(block)
271
+ vars = Set.new
272
+ block.instructions.each do |inst|
273
+ collect_vars_from_instruction(inst, vars)
274
+ end
275
+ # Also check terminator condition
276
+ if block.terminator.is_a?(HIR::Branch) && block.terminator.condition
277
+ collect_vars_from_instruction(block.terminator.condition, vars)
278
+ end
279
+ vars
280
+ end
281
+
282
+ # Collect variable references from an instruction
283
+ def collect_vars_from_instruction(inst, vars)
284
+ case inst
285
+ when HIR::LoadLocal
286
+ vars << inst.var.to_s
287
+ when HIR::LoadInstanceVar
288
+ vars << inst.name.to_s
289
+ when HIR::Call
290
+ collect_vars_from_instruction(inst.receiver, vars) if inst.receiver
291
+ inst.args.each { |arg| collect_vars_from_instruction(arg, vars) }
292
+ end
293
+ end
294
+
295
+ # Find instructions that are invariant (don't depend on loop-modified vars)
296
+ def find_invariant_instructions(block, modified_vars, loop_info)
297
+ invariants = []
298
+
299
+ block.instructions.each do |inst|
300
+ next unless invariant_instruction?(inst, modified_vars, loop_info)
301
+
302
+ invariants << { block: block, instruction: inst }
303
+ end
304
+
305
+ invariants
306
+ end
307
+
308
+ # Check if an instruction is loop-invariant
309
+ def invariant_instruction?(inst, modified_vars, loop_info)
310
+ case inst
311
+ when HIR::Call
312
+ # Only hoist calls to known-pure methods
313
+ return false unless PURE_METHODS.include?(inst.method_name.to_sym)
314
+
315
+ # Check that the receiver doesn't depend on loop-modified vars
316
+ return false unless operand_invariant?(inst.receiver, modified_vars)
317
+
318
+ # Check that all args don't depend on loop-modified vars
319
+ return false unless inst.args.all? { |arg| operand_invariant?(arg, modified_vars) }
320
+
321
+ # Don't hoist if the result_var is used as a store target in the loop
322
+ # (it shouldn't be, but safety check)
323
+ if inst.result_var
324
+ return false if modified_vars.include?(inst.result_var.to_s)
325
+ end
326
+
327
+ true
328
+
329
+ when HIR::Literal, HIR::IntegerLit, HIR::FloatLit, HIR::StringLit,
330
+ HIR::SymbolLit, HIR::NilLit, HIR::BoolLit
331
+ # Literals are always invariant, but they're cheap to compute
332
+ # Only hoist if they have a result_var that's used in the loop
333
+ false # Not worth hoisting literals
334
+
335
+ else
336
+ false
337
+ end
338
+ end
339
+
340
+ # Check if an operand (receiver/arg) doesn't depend on loop-modified vars
341
+ def operand_invariant?(operand, modified_vars)
342
+ case operand
343
+ when HIR::LoadLocal
344
+ !modified_vars.include?(operand.var.to_s)
345
+ when HIR::LoadInstanceVar
346
+ !modified_vars.include?(operand.name.to_s)
347
+ when HIR::SelfRef
348
+ true # self doesn't change
349
+ when HIR::Literal, HIR::IntegerLit, HIR::FloatLit,
350
+ HIR::StringLit, HIR::SymbolLit, HIR::NilLit, HIR::BoolLit
351
+ true # Literals are invariant
352
+ when HIR::Call
353
+ # Nested call: check recursively
354
+ PURE_METHODS.include?(operand.method_name.to_sym) &&
355
+ operand_invariant?(operand.receiver, modified_vars) &&
356
+ operand.args.all? { |arg| operand_invariant?(arg, modified_vars) }
357
+ else
358
+ false # Unknown operand types are assumed variant
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end