konpeito 0.2.4 → 0.3.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.
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Konpeito
4
- # Known standard library names that can be loaded at runtime via rb_require
5
- # This list is derived from Ruby's bundled RBS stdlib definitions
4
+ # Native extension file extensions to check when scanning gem directories
5
+ NATIVE_EXTENSIONS = %w[.bundle .so .dll .dylib].freeze
6
+
7
+ # Known standard library names that can be loaded at runtime via rb_require.
8
+ # This list acts as a fast path; dynamic gem detection via Gem.path covers the rest.
6
9
  KNOWN_STDLIB_LIBRARIES = %w[
7
- json fileutils find pathname tempfile timeout uri yaml
10
+ json fileutils find pathname tempfile timeout uri yaml tmpdir
8
11
  digest openssl socket stringio csv date time set
9
12
  net/http net/https net/ftp net/smtp net/pop net/imap
10
13
  securerandom base64 benchmark erb logger optparse
@@ -184,9 +187,9 @@ module Konpeito
184
187
  @cache_manager.add_dependency(path, dep_path)
185
188
  end
186
189
  resolve_file(dep_path)
187
- elsif req[:type] == :require && stdlib_library?(req[:name])
188
- # Known stdlib library - will be loaded at runtime via rb_require
189
- log " Detected stdlib require: #{req[:name]}"
190
+ elsif req[:type] == :require && runtime_loadable?(req[:name])
191
+ # Stdlib or installed gem will be loaded at runtime via rb_require
192
+ log " Detected runtime require: #{req[:name]}"
190
193
  @stdlib_requires << req[:name]
191
194
  elsif req[:type] == :require_relative
192
195
  # Check if this points to a native extension (.bundle/.so/.dll)
@@ -207,10 +210,10 @@ module Konpeito
207
210
  )
208
211
  end
209
212
  else
210
- # Unknown require - likely a gem that must be required at runtime
213
+ # Unknown require that cannot be resolved statically or found in gems.
211
214
  raise DependencyError.new(
212
215
  "Cannot resolve require: #{req[:name]}. " \
213
- "If this is a gem, use require at runtime in Ruby code instead of compile-time.",
216
+ "If this is a gem, ensure it is installed (`gem install #{req[:name]}`).",
214
217
  from_file: path,
215
218
  line: req[:line],
216
219
  missing_file: req[:name]
@@ -283,7 +286,27 @@ module Konpeito
283
286
  %w[.bundle .so .dll .dylib].any? { |ext| File.exist?("#{base}#{ext}") }
284
287
  end
285
288
 
286
- # Check if a name is a known stdlib library
289
+ # Check whether a require target can be loaded at runtime (stdlib or installed gem).
290
+ # This replaces the hardcoded KNOWN_STDLIB_LIBRARIES approach with dynamic detection.
291
+ def runtime_loadable?(name)
292
+ # Fast path: known stdlib names (avoids gem lookup overhead for common stdlib)
293
+ return true if stdlib_library?(name)
294
+
295
+ # Dynamic gem detection: scan all GEM_PATH directories for a matching lib file.
296
+ # Using Gem.path (not Gem.find_files) because bundler restricts Gem.find_files
297
+ # to only activated gems, while Gem.path covers all installed gem homes.
298
+ name_rb = name.end_with?(".rb") ? name : "#{name}.rb"
299
+ Gem.path.any? do |gem_home|
300
+ Dir.glob(File.join(gem_home, "gems", "*", "lib")).any? do |lib_dir|
301
+ File.exist?(File.join(lib_dir, name_rb)) ||
302
+ NATIVE_EXTENSIONS.any? { |ext| File.exist?(File.join(lib_dir, "#{name}#{ext}")) }
303
+ end
304
+ end
305
+ rescue LoadError, NameError
306
+ false
307
+ end
308
+
309
+ # Check if a name is a known stdlib library (fast path, kept for backwards compat)
287
310
  def stdlib_library?(name)
288
311
  # Normalize: convert dashes to underscores for matching
289
312
  normalized = name.tr("-", "_")
@@ -134,6 +134,10 @@ module Konpeito
134
134
  end
135
135
 
136
136
  def visit_program(typed_node)
137
+ # Scan top-level statements for include/extend/prepend before visiting
138
+ # These need to be executed in the Init function before class definitions
139
+ scan_toplevel_includes(typed_node.children.first)
140
+
137
141
  # Create a main function for top-level code
138
142
  main_func = Function.new(
139
143
  name: "__main__",
@@ -541,6 +545,26 @@ module Konpeito
541
545
  NilLit.new
542
546
  end
543
547
 
548
+ # Scan top-level statements for include/extend/prepend calls
549
+ # These must be executed in Init before class definitions so that
550
+ # constants from included modules are available for superclass resolution.
551
+ def scan_toplevel_includes(statements_node)
552
+ return unless statements_node&.children
553
+
554
+ statements_node.children.each do |child|
555
+ if include_statement?(child)
556
+ module_name = extract_include_module_name(child)
557
+ @program.toplevel_includes << [:include, module_name] if module_name
558
+ elsif extend_statement?(child)
559
+ module_name = extract_include_module_name(child)
560
+ @program.toplevel_includes << [:extend, module_name] if module_name
561
+ elsif prepend_statement?(child)
562
+ module_name = extract_include_module_name(child)
563
+ @program.toplevel_includes << [:prepend, module_name] if module_name
564
+ end
565
+ end
566
+ end
567
+
544
568
  # Check if a node is an include statement
545
569
  def include_statement?(typed_node)
546
570
  return false unless typed_node.node_type == :call
@@ -781,7 +805,11 @@ module Konpeito
781
805
 
782
806
  method_names = []
783
807
  singleton_method_names = []
808
+ module_function_method_names = []
809
+ module_private_method_names = Set.new
784
810
  module_constants = {}
811
+ # Track current visibility state within this module body
812
+ module_visibility = :public
785
813
 
786
814
  # Visit body within module context
787
815
  old_module = @current_module
@@ -802,6 +830,12 @@ module Konpeito
802
830
  func.is_instance_method = false if func && func.name.to_s == method_name
803
831
  else
804
832
  method_names << method_name
833
+ case module_visibility
834
+ when :module_function
835
+ module_function_method_names << method_name
836
+ when :private
837
+ module_private_method_names << method_name
838
+ end
805
839
  end
806
840
  elsif child.node_type == :singleton_class
807
841
  # class << self in module - collect singleton methods
@@ -818,13 +852,29 @@ module Konpeito
818
852
  end
819
853
  end
820
854
  elsif child.node_type == :constant_write
821
- # Handle constant assignment within module
855
+ # Handle constant assignment within module.
856
+ # Collect literal value for C Init optimization (simple constants only).
857
+ # Also emit StoreConstant into __main__ for non-literal values (Mutex.new,
858
+ # {}, File.join(...), etc.) so they get initialized at runtime via rb_const_set.
859
+ # Simple literals are already set in C Init, so we skip them here to avoid
860
+ # "already initialized constant" warnings.
822
861
  const_name = child.node.name.to_s
823
- module_constants[const_name] = visit_literal_value(child.children.first)
862
+ lit_val = visit_literal_value(child.children.first)
863
+ module_constants[const_name] = lit_val
864
+ # Only emit for non-literal values (literals handled by C Init)
865
+ unless lit_val.is_a?(HIR::IntegerLit) || lit_val.is_a?(HIR::FloatLit) ||
866
+ lit_val.is_a?(HIR::StringLit) || lit_val.is_a?(HIR::SymbolLit) ||
867
+ lit_val.is_a?(HIR::BoolLit) || lit_val.is_a?(HIR::NilLit)
868
+ visit(child)
869
+ end
824
870
  elsif child.node_type == :class_variable_write
825
871
  # Handle class variable initialization within module
826
872
  # (modules can have class variables too)
827
873
  visit(child)
874
+ elsif module_visibility_modifier?(child)
875
+ # module_function / private / public / protected as bare calls — compilation
876
+ # directive only, must NOT emit a runtime rb_funcallv call into __main__.
877
+ module_visibility = extract_module_visibility(child)
828
878
  else
829
879
  visit(child)
830
880
  end
@@ -834,17 +884,45 @@ module Konpeito
834
884
 
835
885
  @current_module = old_module
836
886
 
837
- module_def = ModuleDef.new(
838
- name: name,
839
- methods: method_names,
840
- singleton_methods: singleton_method_names,
841
- constants: module_constants
842
- )
843
-
844
- @program.modules << module_def
887
+ # Merge into existing ModuleDef if this module was already opened (multi-file projects)
888
+ existing_module_def = @program.modules.find { |m| m.name == name }
889
+ if existing_module_def
890
+ existing_module_def.methods.concat(method_names)
891
+ existing_module_def.singleton_methods.concat(singleton_method_names)
892
+ existing_module_def.module_function_methods.concat(module_function_method_names)
893
+ existing_module_def.private_methods.merge(module_private_method_names)
894
+ existing_module_def.constants.merge!(module_constants)
895
+ else
896
+ module_def = ModuleDef.new(
897
+ name: name,
898
+ methods: method_names,
899
+ singleton_methods: singleton_method_names,
900
+ constants: module_constants
901
+ )
902
+ module_def.module_function_methods.concat(module_function_method_names)
903
+ module_def.private_methods.merge(module_private_method_names)
904
+ @program.modules << module_def
905
+ end
845
906
  NilLit.new
846
907
  end
847
908
 
909
+ # Check if a node is a bare visibility modifier inside a module body
910
+ # (module_function, private, public, protected with no arguments and no receiver)
911
+ def module_visibility_modifier?(typed_node)
912
+ return false unless typed_node.node_type == :call
913
+ return false unless typed_node.node.receiver.nil?
914
+ name = typed_node.node.name.to_s
915
+ return false unless %w[module_function private public protected].include?(name)
916
+ # Must be bare (no arguments) or have only symbol arguments (per-method form)
917
+ true
918
+ end
919
+
920
+ # Extract the visibility symbol from a visibility modifier call node
921
+ def extract_module_visibility(typed_node)
922
+ name = typed_node.node.name.to_s
923
+ name == "module_function" ? :module_function : name.to_sym
924
+ end
925
+
848
926
  # Check if a def node is a singleton method (def self.method)
849
927
  def singleton_method?(typed_node)
850
928
  return false unless typed_node.node_type == :def
@@ -2098,6 +2176,7 @@ module Konpeito
2098
2176
  # Get arguments and keyword arguments
2099
2177
  args = []
2100
2178
  keyword_args = {}
2179
+ keyword_splat = nil
2101
2180
  args_child = typed_node.children.find { |c| c.node_type == :arguments }
2102
2181
  if args_child
2103
2182
  args_child.children.each do |arg|
@@ -2117,6 +2196,15 @@ module Konpeito
2117
2196
  # Fallback: create typed node for the value
2118
2197
  keyword_args[key_name] = visit_node_directly(elem.value)
2119
2198
  end
2199
+ elsif elem.is_a?(Prism::AssocSplatNode) && elem.value
2200
+ # **hash at call site — capture the splatted hash expression
2201
+ typed_assoc = arg.children[ei]
2202
+ splat_expr = if typed_assoc && typed_assoc.children && !typed_assoc.children.empty?
2203
+ visit(typed_assoc.children.first)
2204
+ else
2205
+ visit_node_directly(elem.value)
2206
+ end
2207
+ keyword_splat = splat_expr
2120
2208
  end
2121
2209
  end
2122
2210
  elsif arg.node.is_a?(Prism::SplatNode)
@@ -2211,6 +2299,7 @@ module Konpeito
2211
2299
  args: args,
2212
2300
  block: block,
2213
2301
  keyword_args: keyword_args,
2302
+ keyword_splat: keyword_splat,
2214
2303
  type: typed_node.type,
2215
2304
  result_var: result_var,
2216
2305
  safe_navigation: is_safe_nav
@@ -5990,13 +6079,84 @@ module Konpeito
5990
6079
 
5991
6080
  # Detect variables from outer scope that are used in the block
5992
6081
  def detect_captured_variables(outer_vars)
5993
- # For now, capture all outer variables that exist
5994
- # A more sophisticated approach would analyze actual usage
5995
- outer_vars.keys.map do |name|
6082
+ # Only capture outer variables that are actually referenced (read or written)
6083
+ # in the block body or any nested block bodies within it.
6084
+ referenced = Set.new
6085
+ block_body = @current_function.body
6086
+ scan_block_body_for_captures(block_body, outer_vars, referenced)
6087
+ referenced.map do |name|
5996
6088
  Capture.new(name: name, type: outer_vars[name].type)
5997
6089
  end
5998
6090
  end
5999
6091
 
6092
+ # Recursively scan basic blocks for variable references to outer scope,
6093
+ # including nested block definitions (e.g., mutex.synchronize { ... })
6094
+ def scan_block_body_for_captures(blocks, outer_vars, referenced)
6095
+ blocks.each do |bb|
6096
+ bb.instructions.each do |inst|
6097
+ scan_instruction_for_captures(inst, outer_vars, referenced)
6098
+ end
6099
+ # Also check terminator for variable references
6100
+ if bb.terminator
6101
+ scan_instruction_for_captures(bb.terminator, outer_vars, referenced)
6102
+ end
6103
+ end
6104
+ end
6105
+
6106
+ # Recursively scan a single HIR instruction (and its sub-expressions) for captures
6107
+ def scan_instruction_for_captures(node, outer_vars, referenced)
6108
+ return unless node.is_a?(Instruction)
6109
+ case node
6110
+ when LoadLocal
6111
+ referenced << node.var.name if outer_vars.key?(node.var.name)
6112
+ when StoreLocal
6113
+ referenced << node.var.name if outer_vars.key?(node.var.name)
6114
+ scan_instruction_for_captures(node.value, outer_vars, referenced) if node.value.is_a?(Instruction)
6115
+ when Call
6116
+ scan_instruction_for_captures(node.receiver, outer_vars, referenced) if node.receiver
6117
+ node.args.each { |a| scan_instruction_for_captures(a, outer_vars, referenced) }
6118
+ if node.block && node.block.respond_to?(:body) && node.block.body
6119
+ scan_block_body_for_captures(node.block.body, outer_vars, referenced)
6120
+ end
6121
+ when ThreadNew, ProcNew, FiberNew, RactorNew
6122
+ if node.respond_to?(:block_def) && node.block_def
6123
+ scan_block_body_for_captures(node.block_def.body, outer_vars, referenced)
6124
+ end
6125
+ else
6126
+ # Generic fallback: scan sub-expressions and nested block bodies
6127
+ # for instruction types like MutexSynchronize, BeginRescue, etc.
6128
+ if node.respond_to?(:block_def) && node.block_def
6129
+ scan_block_body_for_captures(node.block_def.body, outer_vars, referenced)
6130
+ end
6131
+ # Scan known sub-expression attributes that may reference outer variables
6132
+ [:mutex, :cv, :queue, :value, :receiver, :ractor, :port, :max_size, :key].each do |attr|
6133
+ if node.respond_to?(attr)
6134
+ sub = node.send(attr)
6135
+ scan_instruction_for_captures(sub, outer_vars, referenced) if sub.is_a?(Instruction)
6136
+ end
6137
+ end
6138
+ # Scan args array if present
6139
+ if node.respond_to?(:args) && node.args.is_a?(Array)
6140
+ node.args.each { |a| scan_instruction_for_captures(a, outer_vars, referenced) }
6141
+ end
6142
+ # Scan nested HIR blocks (BeginRescue try/rescue/else/ensure blocks)
6143
+ if node.respond_to?(:try_hir_blocks) && node.try_hir_blocks
6144
+ scan_block_body_for_captures(node.try_hir_blocks, outer_vars, referenced)
6145
+ end
6146
+ if node.respond_to?(:rescue_clauses) && node.rescue_clauses
6147
+ node.rescue_clauses.each do |clause|
6148
+ scan_block_body_for_captures(clause.body, outer_vars, referenced) if clause.respond_to?(:body) && clause.body
6149
+ end
6150
+ end
6151
+ if node.respond_to?(:else_blocks) && node.else_blocks
6152
+ scan_block_body_for_captures(node.else_blocks, outer_vars, referenced)
6153
+ end
6154
+ if node.respond_to?(:ensure_blocks) && node.ensure_blocks
6155
+ scan_block_body_for_captures(node.ensure_blocks, outer_vars, referenced)
6156
+ end
6157
+ end
6158
+ end
6159
+
6000
6160
  def build_block_params(params_node)
6001
6161
  result = []
6002
6162
  params_node.parameters&.requireds&.each do |param|
@@ -6624,14 +6784,56 @@ module Konpeito
6624
6784
  ensure_child = typed_node.children.find { |c| c.node_type == :ensure }
6625
6785
 
6626
6786
  result_var = new_temp_var
6787
+ has_rescue = rescue_child != nil
6627
6788
 
6628
- # Collect try instructions (without creating new blocks)
6789
+ # When rescue clauses are present, collect the try body into a separate HIR block
6790
+ # list using BlockBodyCollector so that control flow (if/else etc.) inside the try
6791
+ # body does NOT bleed into the outer function's basic block graph.
6792
+ # This prevents invalid phi-node predecessors in the rescue try callback.
6793
+ try_hir_blocks = nil
6629
6794
  try_instructions = []
6630
- if statements_child
6795
+
6796
+ if has_rescue && statements_child
6797
+ saved_function = @current_function
6798
+ saved_current_blk = @current_block
6799
+
6800
+ @block_counter += 1
6801
+ try_entry = BasicBlock.new(label: "rescue_try_entry_#{@block_counter}")
6802
+ try_hir_blocks = [try_entry]
6803
+ @current_function = BlockBodyCollector.new(try_hir_blocks)
6804
+ set_current_block(try_entry)
6805
+
6631
6806
  statements_child.children.each do |stmt|
6632
6807
  inst = visit(stmt)
6633
6808
  try_instructions << inst if inst
6634
6809
  end
6810
+
6811
+ # Close the last open block with a Return
6812
+ unless @current_block.terminator
6813
+ last_val = try_instructions.last
6814
+ @current_block.set_terminator(Return.new(value: last_val || NilLit.new))
6815
+ end
6816
+
6817
+ @current_function = saved_function
6818
+ set_current_block(saved_current_blk)
6819
+ elsif statements_child
6820
+ # No rescue clauses: inline (original behaviour)
6821
+ # Capture ALL emitted instructions (not just return values) because
6822
+ # visit() for assignment nodes emits StoreLocal but returns the value.
6823
+ # Guard: visit() may change @current_block (if/else, while, etc.),
6824
+ # in which case fall back to original behavior.
6825
+ statements_child.children.each do |stmt|
6826
+ saved_block = @current_block
6827
+ before_idx = @current_block.instructions.size
6828
+ inst = visit(stmt)
6829
+ if @current_block.equal?(saved_block) && @current_block.instructions.size >= before_idx
6830
+ @current_block.instructions[before_idx..]&.each do |emitted|
6831
+ try_instructions << emitted
6832
+ end
6833
+ else
6834
+ try_instructions << inst if inst
6835
+ end
6836
+ end
6635
6837
  end
6636
6838
 
6637
6839
  # Record instruction count after try body - everything emitted after this
@@ -6644,26 +6846,43 @@ module Konpeito
6644
6846
  rescue_clauses = build_rescue_clauses(rescue_child)
6645
6847
  end
6646
6848
 
6647
- # Collect else instructions
6849
+ # Collect else instructions - capture ALL emitted instructions
6850
+ # (visit() for assignments emits StoreLocal but returns the value)
6648
6851
  else_instructions = []
6649
6852
  if else_child
6650
6853
  else_statements = else_child.children.find { |c| c.node_type == :statements }
6651
6854
  if else_statements
6652
6855
  else_statements.children.each do |stmt|
6856
+ saved_block = @current_block
6857
+ before_idx = @current_block.instructions.size
6653
6858
  inst = visit(stmt)
6654
- else_instructions << inst if inst
6859
+ if @current_block.equal?(saved_block) && @current_block.instructions.size >= before_idx
6860
+ @current_block.instructions[before_idx..]&.each do |emitted|
6861
+ else_instructions << emitted
6862
+ end
6863
+ else
6864
+ else_instructions << inst if inst
6865
+ end
6655
6866
  end
6656
6867
  end
6657
6868
  end
6658
6869
 
6659
- # Collect ensure instructions
6870
+ # Collect ensure instructions - capture ALL emitted instructions
6660
6871
  ensure_instructions = []
6661
6872
  if ensure_child
6662
6873
  ensure_statements = ensure_child.children.find { |c| c.node_type == :statements }
6663
6874
  if ensure_statements
6664
6875
  ensure_statements.children.each do |stmt|
6876
+ saved_block = @current_block
6877
+ before_idx = @current_block.instructions.size
6665
6878
  inst = visit(stmt)
6666
- ensure_instructions << inst if inst
6879
+ if @current_block.equal?(saved_block) && @current_block.instructions.size >= before_idx
6880
+ @current_block.instructions[before_idx..]&.each do |emitted|
6881
+ ensure_instructions << emitted
6882
+ end
6883
+ else
6884
+ ensure_instructions << inst if inst
6885
+ end
6667
6886
  end
6668
6887
  end
6669
6888
  end
@@ -6671,12 +6890,13 @@ module Konpeito
6671
6890
  # Collect ALL instruction object_ids emitted during rescue/else/ensure visitation
6672
6891
  # This includes sub-expression instructions (e.g. StringLit for literal args)
6673
6892
  non_try_ids = Set.new
6674
- @current_block.instructions[non_try_start_idx..].each do |i|
6893
+ (@current_block.instructions[non_try_start_idx..] || []).each do |i|
6675
6894
  non_try_ids << i.object_id
6676
6895
  end
6677
6896
 
6678
6897
  inst = BeginRescue.new(
6679
6898
  try_blocks: try_instructions,
6899
+ try_hir_blocks: try_hir_blocks,
6680
6900
  rescue_clauses: rescue_clauses,
6681
6901
  else_blocks: else_instructions,
6682
6902
  ensure_blocks: ensure_instructions,
@@ -6710,13 +6930,23 @@ module Konpeito
6710
6930
  exception_var = current.node.reference.name.to_s
6711
6931
  end
6712
6932
 
6713
- # Collect body instructions
6933
+ # Collect body instructions - capture ALL emitted instructions
6934
+ # (visit() for assignments emits StoreLocal but returns the value)
6935
+ # Guard: visit() may change @current_block (if/else, while, etc.)
6714
6936
  body_instructions = []
6715
6937
  statements_child = current.children.find { |c| c.node_type == :statements }
6716
6938
  if statements_child
6717
6939
  statements_child.children.each do |stmt|
6940
+ saved_block = @current_block
6941
+ before_idx = @current_block.instructions.size
6718
6942
  inst = visit(stmt)
6719
- body_instructions << inst if inst
6943
+ if @current_block.equal?(saved_block) && @current_block.instructions.size >= before_idx
6944
+ @current_block.instructions[before_idx..]&.each do |emitted|
6945
+ body_instructions << emitted
6946
+ end
6947
+ else
6948
+ body_instructions << inst if inst
6949
+ end
6720
6950
  end
6721
6951
  end
6722
6952
 
@@ -6735,7 +6965,10 @@ module Konpeito
6735
6965
  end
6736
6966
 
6737
6967
  def visit_case(typed_node)
6738
- # Handle case/when/else statements
6968
+ # Handle case/when/else statements by creating proper HIR basic blocks,
6969
+ # like visit_if does. This avoids the CaseStatement-embedding approach which
6970
+ # caused when-body instructions to execute unconditionally in the entry block.
6971
+ #
6739
6972
  # case x
6740
6973
  # when 1 then "one"
6741
6974
  # when 2, 3 then "small"
@@ -6753,62 +6986,141 @@ module Konpeito
6753
6986
  end
6754
6987
  end
6755
6988
 
6756
- # Record instruction count after predicate - everything after is when/else sub-instructions
6757
- sub_start_idx = @current_block.instructions.size
6989
+ # Create merge block
6990
+ merge_block = new_block("case_merge")
6991
+ incoming = {} # { hir_block_label => result_hir_node }
6992
+
6993
+ when_nodes = typed_node.children.select { |c| c.node_type == :when }
6994
+ has_else = typed_node.children.any? { |c| c.node_type == :else }
6995
+
6996
+ when_nodes.each_with_index do |when_child, i|
6997
+ # Extract conditions and body statements
6998
+ conditions = []
6999
+ body_stmts = nil
7000
+ when_child.children.each do |child|
7001
+ if child.node_type == :statements
7002
+ body_stmts = child
7003
+ else
7004
+ # Evaluate condition in the current check block
7005
+ cond = visit(child)
7006
+ conditions << cond
7007
+ end
7008
+ end
7009
+
7010
+ body_block = new_block("when_body")
7011
+ is_last_when = (i == when_nodes.size - 1)
7012
+ next_block = new_block(is_last_when && !has_else ? "when_else" : "when_check")
6758
7013
 
6759
- # Build when clauses
6760
- when_clauses = []
6761
- typed_node.children.select { |c| c.node_type == :when }.each do |when_child|
6762
- when_clause = build_when_clause(when_child)
6763
- when_clauses << when_clause
7014
+ # Generate condition checks with OR logic for multiple conditions
7015
+ if predicate.nil?
7016
+ # No predicate: treat conditions as boolean (truthy) directly
7017
+ if conditions.empty?
7018
+ @current_block.set_terminator(Jump.new(target: body_block.label))
7019
+ else
7020
+ # OR chain: if any condition is truthy, execute body
7021
+ remaining = conditions.dup
7022
+ last_cond = remaining.pop
7023
+ remaining.each do |cond|
7024
+ or_check_block = new_block("when_or_check")
7025
+ @current_block.set_terminator(Branch.new(
7026
+ condition: cond,
7027
+ then_block: body_block.label,
7028
+ else_block: or_check_block.label
7029
+ ))
7030
+ set_current_block(or_check_block)
7031
+ end
7032
+ @current_block.set_terminator(Branch.new(
7033
+ condition: last_cond,
7034
+ then_block: body_block.label,
7035
+ else_block: next_block.label
7036
+ ))
7037
+ end
7038
+ else
7039
+ # With predicate: use === comparison
7040
+ if conditions.empty?
7041
+ @current_block.set_terminator(Jump.new(target: body_block.label))
7042
+ else
7043
+ remaining = conditions.dup
7044
+ last_cond = remaining.pop
7045
+ remaining.each do |cond|
7046
+ check_var = new_temp_var
7047
+ check = CaseEqualityCheck.new(condition: cond, predicate: predicate, result_var: check_var)
7048
+ emit(check)
7049
+ or_check_block = new_block("when_or_check")
7050
+ @current_block.set_terminator(Branch.new(
7051
+ condition: check,
7052
+ then_block: body_block.label,
7053
+ else_block: or_check_block.label
7054
+ ))
7055
+ set_current_block(or_check_block)
7056
+ end
7057
+ check_var = new_temp_var
7058
+ check = CaseEqualityCheck.new(condition: last_cond, predicate: predicate, result_var: check_var)
7059
+ emit(check)
7060
+ @current_block.set_terminator(Branch.new(
7061
+ condition: check,
7062
+ then_block: body_block.label,
7063
+ else_block: next_block.label
7064
+ ))
7065
+ end
7066
+ end
7067
+
7068
+ # Generate body in body_block
7069
+ set_current_block(body_block)
7070
+ body_result = NilLit.new
7071
+ if body_stmts
7072
+ body_stmts.children.each do |stmt|
7073
+ result = visit(stmt)
7074
+ body_result = result if result
7075
+ end
7076
+ end
7077
+ body_exit = @current_block
7078
+ unless @current_block.terminator
7079
+ @current_block.set_terminator(Jump.new(target: merge_block.label))
7080
+ end
7081
+ incoming[body_exit.label] = body_result
7082
+
7083
+ # Move to next check block
7084
+ set_current_block(next_block)
6764
7085
  end
6765
7086
 
6766
- # Build else body
6767
- else_body = nil
7087
+ # Else body (in the last check/else block = @current_block)
6768
7088
  else_child = typed_node.children.find { |c| c.node_type == :else }
7089
+ else_result = NilLit.new
6769
7090
  if else_child
6770
- else_body = []
6771
- else_statements = else_child.children.find { |c| c.node_type == :statements }
6772
- if else_statements
6773
- else_statements.children.each do |stmt|
6774
- inst = visit(stmt)
6775
- else_body << inst if inst
7091
+ else_stmts = else_child.children.find { |c| c.node_type == :statements }
7092
+ if else_stmts
7093
+ else_stmts.children.each do |stmt|
7094
+ result = visit(stmt)
7095
+ else_result = result if result
6776
7096
  end
6777
7097
  end
6778
7098
  end
6779
-
6780
- # Collect ALL instruction IDs emitted during when/else visitation
6781
- sub_ids = Set.new
6782
- @current_block.instructions[sub_start_idx..].each do |i|
6783
- sub_ids << i.object_id
7099
+ else_exit = @current_block
7100
+ unless @current_block.terminator
7101
+ @current_block.set_terminator(Jump.new(target: merge_block.label))
6784
7102
  end
7103
+ incoming[else_exit.label] = else_result
6785
7104
 
6786
- inst = CaseStatement.new(
6787
- predicate: predicate,
6788
- when_clauses: when_clauses,
6789
- else_body: else_body,
6790
- type: typed_node.type,
6791
- result_var: result_var
6792
- )
6793
- inst.sub_instruction_ids = sub_ids
6794
- emit(inst)
6795
- inst
7105
+ # Merge block with phi node
7106
+ set_current_block(merge_block)
7107
+ phi = Phi.new(incoming: incoming, type: typed_node.type, result_var: result_var)
7108
+ emit(phi)
7109
+ phi
6796
7110
  end
6797
7111
 
6798
7112
  def build_when_clause(when_typed_node)
6799
- # Extract conditions and body from when clause
7113
+ # Extract conditions and body from when clause (kept for backward compatibility)
6800
7114
  conditions = []
6801
7115
  body = []
6802
7116
 
6803
7117
  when_typed_node.children.each do |child|
6804
7118
  if child.node_type == :statements
6805
- # This is the body
6806
7119
  child.children.each do |stmt|
6807
7120
  inst = visit(stmt)
6808
7121
  body << inst if inst
6809
7122
  end
6810
7123
  else
6811
- # This is a condition
6812
7124
  conditions << visit(child)
6813
7125
  end
6814
7126
  end
@@ -7470,7 +7782,7 @@ module Konpeito
7470
7782
 
7471
7783
  def new_temp_var
7472
7784
  @var_counter += 1
7473
- "t#{@var_counter}"
7785
+ "__t#{@var_counter}"
7474
7786
  end
7475
7787
  end
7476
7788
  end