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.
@@ -7,6 +7,9 @@ module Konpeito
7
7
  module Codegen
8
8
  # Generates LLVM IR from HIR
9
9
  class LLVMGenerator
10
+ # Special capture name used to pass outer `self` into block callbacks.
11
+ # Allows @ivar access inside blocks to use the enclosing method's self.
12
+ BLOCK_SELF_CAPTURE = "__blk_self__"
10
13
  attr_reader :mod, :builder, :hir_program
11
14
 
12
15
  def initialize(module_name: "konpeito", monomorphizer: nil, rbs_loader: nil, debug: false, profile: false, source_file: nil)
@@ -30,6 +33,9 @@ module Konpeito
30
33
  @variables = {} # For SSA values (temps, etc.)
31
34
  @variable_allocas = {} # For local variables (alloca-based for loop safety)
32
35
  @variable_types = {} # Track unboxed types: :i64, :double, or :value
36
+ @block_exit_overrides = {} # When safe navigation expands a HIR block into multiple LLVM blocks,
37
+ # maps HIR block label → actual LLVM exit block (for phi predecessors)
38
+ @generating_block_label = nil # Label of the HIR block currently being generated
33
39
  @hir_program = nil
34
40
  @monomorphizer = monomorphizer
35
41
  @rbs_loader = rbs_loader
@@ -42,6 +48,8 @@ module Konpeito
42
48
  @variadic_functions = {} # Track functions with **kwargs or *args
43
49
  @keyword_param_functions = {} # Track functions with keyword params (for direct call kwargs passing)
44
50
  @comparison_result_vars = Set.new # Track variables holding comparison results (0/1 boolean)
51
+ @polymorphic_methods = Set.new # Method names defined in multiple classes (must not use direct call)
52
+ @hir_functions = {} # name -> HIR::Function; used to detect &blk params (proc-storing methods)
45
53
 
46
54
  # Register all NativeClass types from RBS upfront
47
55
  register_native_classes_from_rbs
@@ -126,7 +134,11 @@ module Konpeito
126
134
  @alias_renamed_methods = {} # "ClassName#old_name" -> renamed_func_name
127
135
 
128
136
  functions.each_with_index do |func, i|
129
- key = [func.owner_class, func.owner_module, func.name.to_s, func.class_method?]
137
+ # Use the mangled LLVM name as the dedup key.
138
+ # Two HIR functions with the same owner_class but different owner_module
139
+ # still produce the same LLVM symbol (e.g. rn_Style_align), so they must
140
+ # be deduplicated even if their [owner_class, owner_module, name] triples differ.
141
+ key = mangle_name(func)
130
142
  if seen.key?(key) && alias_targets["#{func.owner_class}##{func.name}"]
131
143
  # This method was aliased and is now being redefined.
132
144
  # Rename the earlier definition so the alias can point to it.
@@ -139,12 +151,20 @@ module Konpeito
139
151
  end
140
152
  seen[key] = i
141
153
  end
142
- # Keep all functions (including renamed originals)
143
- functions
154
+ # Keep only the last occurrence of each function key.
155
+ # Also keep alias-renamed originals (needed for alias resolution).
156
+ keep_indices = seen.values.to_set
157
+ functions.each_with_index do |func, i|
158
+ keep_indices << i if func.name.to_s.end_with?("$alias_orig")
159
+ end
160
+ functions.select.with_index { |_, i| keep_indices.include?(i) }
144
161
  end
145
162
 
146
163
  # Declare a function (create LLVM function without body)
147
164
  def declare_function(hir_func)
165
+ # Record HIR function for later inspection (e.g. detecting &blk params)
166
+ @hir_functions[hir_func.name.to_s] = hir_func
167
+
148
168
  native_class_type = detect_native_class_for_function(hir_func)
149
169
 
150
170
  if native_class_type
@@ -159,8 +179,8 @@ module Konpeito
159
179
  # e.g. for monomorphized functions like dot_Vec2 whose name has no matching method entry).
160
180
  def native_param_type_sym_from_hir(param)
161
181
  t = param.type
162
- return :Float64 if t == TypeChecker::Types::FLOAT || t&.name == :Float
163
- return :Int64 if t == TypeChecker::Types::INTEGER || t&.name == :Integer
182
+ return :Float64 if t == TypeChecker::Types::FLOAT || (t.is_a?(TypeChecker::Types::ClassInstance) && t.name == :Float)
183
+ return :Int64 if t == TypeChecker::Types::INTEGER || (t.is_a?(TypeChecker::Types::ClassInstance) && t.name == :Integer)
164
184
  # Everything else (NativeClass, unknown) → use pointer (else branch in caller)
165
185
  :OtherNativeClass
166
186
  end
@@ -202,6 +222,10 @@ module Konpeito
202
222
 
203
223
  mangled = mangle_name(hir_func)
204
224
  func = @mod.functions.add(mangled, param_types, return_type)
225
+ # Track methods that are defined in multiple classes — these require virtual dispatch
226
+ if (existing = @functions[hir_func.name]) && existing.name != mangled
227
+ @polymorphic_methods << hir_func.name
228
+ end
205
229
  @functions[hir_func.name] = func
206
230
  @functions[mangled] = func
207
231
  @native_method_funcs ||= {}
@@ -257,6 +281,10 @@ module Konpeito
257
281
 
258
282
  return_type = value_type
259
283
  func = @mod.functions.add(mangled, param_types, return_type)
284
+ # Track methods that are defined in multiple classes — these require virtual dispatch
285
+ if (existing = @functions[hir_func.name]) && existing.name != mangled
286
+ @polymorphic_methods << hir_func.name
287
+ end
260
288
  @functions[hir_func.name] = func
261
289
  @functions[mangled] = func
262
290
  end
@@ -504,6 +532,11 @@ module Konpeito
504
532
  # We'll use rb_funcallv for variadic args: VALUE rb_funcallv(VALUE recv, ID mid, int argc, VALUE *argv)
505
533
  @rb_funcallv = @mod.functions.add("rb_funcallv", [value_type, id_type, LLVM::Int32, LLVM::Pointer(value_type)], value_type)
506
534
 
535
+ # rb_funcallv_kw - call a method with keyword argument flag
536
+ # VALUE rb_funcallv_kw(VALUE recv, ID mid, int argc, const VALUE *argv, int kw_splat)
537
+ # kw_splat: RB_PASS_KEYWORDS (1) means last element of argv is a keyword Hash
538
+ @rb_funcallv_kw = @mod.functions.add("rb_funcallv_kw", [value_type, id_type, LLVM::Int32, LLVM::Pointer(value_type), LLVM::Int32], value_type)
539
+
507
540
  # rb_int2inum - convert C int to Ruby Integer
508
541
  @rb_int2inum = @mod.functions.add("rb_int2inum", [int_type], value_type) do |fn|
509
542
  fn.linkage = :external
@@ -699,6 +732,14 @@ module Konpeito
699
732
  LLVM::Pointer(block_callback_type), value_type],
700
733
  value_type)
701
734
 
735
+ # rb_block_call_kw - call method with block callback and keyword argument flag
736
+ # VALUE rb_block_call_kw(VALUE obj, ID mid, int argc, const VALUE *argv,
737
+ # rb_block_call_func_t proc, VALUE data2, int kw_splat)
738
+ @rb_block_call_kw = @mod.functions.add("rb_block_call_kw",
739
+ [value_type, id_type, LLVM::Int32, LLVM::Pointer(value_type),
740
+ LLVM::Pointer(block_callback_type), value_type, LLVM::Int32],
741
+ value_type)
742
+
702
743
  # Proc functions
703
744
  # VALUE rb_proc_new(rb_block_call_func_t func, VALUE val)
704
745
  # Creates a Proc object from a C callback function
@@ -1045,6 +1086,7 @@ module Konpeito
1045
1086
  @variables = {}
1046
1087
  @variable_allocas = {}
1047
1088
  @variable_types = {}
1089
+ @block_exit_overrides = {}
1048
1090
  @current_function = func
1049
1091
  @current_hir_func = hir_func
1050
1092
  @current_native_class = native_class_type # Track for self field access
@@ -1119,6 +1161,7 @@ module Konpeito
1119
1161
  @variables = {}
1120
1162
  @variable_allocas = {}
1121
1163
  @variable_types = {}
1164
+ @block_exit_overrides = {}
1122
1165
  @current_function = func
1123
1166
  @current_hir_func = hir_func
1124
1167
 
@@ -1155,6 +1198,34 @@ module Konpeito
1155
1198
  @variable_types[var_name] = type_tag
1156
1199
  end
1157
1200
 
1201
+ # Pre-scan for thread-captured variables.
1202
+ # Replace their stack allocas with heap cells at function entry so that
1203
+ # all code (loop conditions, loop bodies, after-loop) uses the same
1204
+ # shared heap location consistently. This avoids the mid-loop alloca
1205
+ # replacement issue where loop conditions read from stale stack allocas.
1206
+ thread_captured_vars = collect_thread_captured_vars(hir_func)
1207
+ @thread_boxed_vars = {} # Reset per-function (each function has its own heap cells)
1208
+ unless thread_captured_vars.empty?
1209
+ declare_malloc
1210
+ thread_captured_vars.each do |cap_name|
1211
+ next if @thread_boxed_vars[cap_name]
1212
+ next unless @variable_allocas[cap_name]
1213
+
1214
+ # Allocate a heap cell (8 bytes = sizeof(VALUE)) and replace the alloca
1215
+ cell = @builder.call(@malloc, LLVM::Int64.from_i(8), "tbox_#{cap_name}")
1216
+ cell_int = @builder.ptr2int(cell, LLVM::Int64, "tbox_#{cap_name}_int")
1217
+ cell_typed = @builder.int2ptr(cell_int, LLVM::Pointer(value_type), "tbox_#{cap_name}_ptr")
1218
+
1219
+ # Initialize with Qnil (variables haven't been assigned yet at entry)
1220
+ @builder.store(@qnil, cell_typed)
1221
+
1222
+ # Replace stack alloca with heap cell — all subsequent reads/writes
1223
+ # in the function go through this shared heap location
1224
+ @variable_allocas[cap_name] = cell_typed
1225
+ @thread_boxed_vars[cap_name] = cell_typed
1226
+ end
1227
+ end
1228
+
1158
1229
  # Separate regular, keyword, keyword_rest, and block parameters
1159
1230
  regular_params = hir_func.params.reject { |p| p.keyword || p.keyword_rest || p.rest || p.block }
1160
1231
  keyword_params = hir_func.params.select(&:keyword)
@@ -1507,67 +1578,93 @@ module Konpeito
1507
1578
  # Topologically sort blocks based on phi dependencies
1508
1579
  # Ensures blocks are generated after the blocks their phi nodes reference
1509
1580
  def sort_blocks_by_phi_dependencies(blocks)
1510
- # Build a map of block label -> block
1581
+ return blocks if blocks.size <= 1
1582
+
1511
1583
  block_map = blocks.each_with_object({}) { |b, h| h[b.label] = b }
1512
1584
 
1513
- # Build dependency graph: block -> blocks it depends on (via phi incoming values)
1514
- dependencies = {}
1585
+ # Build CFG successor edges from terminators.
1586
+ # A block's successors are the blocks it branches/jumps to.
1587
+ successors = {}
1588
+ blocks.each do |block|
1589
+ succs = []
1590
+ term = block.terminator
1591
+ case term
1592
+ when HIR::Branch
1593
+ succs << term.then_block if block_map[term.then_block]
1594
+ succs << term.else_block if block_map[term.else_block]
1595
+ when HIR::Jump
1596
+ succs << term.target if block_map[term.target]
1597
+ end
1598
+ successors[block.label] = succs
1599
+ end
1600
+
1601
+ # Also add phi-based dependencies: if a block has a phi that references a
1602
+ # result_var defined in another block, that other block should come first.
1603
+ phi_deps = Hash.new { |h, k| h[k] = Set.new }
1515
1604
  blocks.each do |block|
1516
- deps = Set.new
1517
1605
  block.instructions.each do |inst|
1518
1606
  next unless inst.is_a?(HIR::Phi)
1519
1607
 
1520
- inst.incoming.each do |label, value|
1521
- # If the incoming value is from a result_var in another block,
1522
- # we depend on that block being generated first
1608
+ inst.incoming.each do |_label, value|
1523
1609
  if value.respond_to?(:result_var) && value.result_var
1524
- # Find which block defines this result_var
1525
1610
  blocks.each do |other_block|
1526
1611
  next if other_block.label == block.label
1527
1612
 
1528
1613
  other_block.instructions.each do |other_inst|
1529
1614
  if other_inst.respond_to?(:result_var) && other_inst.result_var == value.result_var
1530
- deps << other_block.label
1615
+ phi_deps[block.label] << other_block.label
1531
1616
  end
1532
1617
  end
1533
1618
  end
1534
1619
  end
1535
1620
  end
1536
1621
  end
1537
- dependencies[block.label] = deps
1538
1622
  end
1539
1623
 
1540
- # Topological sort using Kahn's algorithm
1541
- in_degree = Hash.new(0)
1542
- dependencies.each do |block_label, deps|
1543
- deps.each do |dep_label|
1544
- in_degree[block_label] += 1 if block_map[dep_label]
1624
+ # Reverse Post-Order (RPO) traversal of the CFG.
1625
+ # This ensures a block is processed after all its predecessors (except back-edges).
1626
+ visited = Set.new
1627
+ post_order = []
1628
+
1629
+ dfs = lambda do |label|
1630
+ return if visited.include?(label)
1631
+
1632
+ visited << label
1633
+ successors[label]&.each do |succ|
1634
+ dfs.call(succ)
1545
1635
  end
1636
+ post_order << label
1546
1637
  end
1547
1638
 
1548
- # Start with blocks that have no dependencies
1549
- queue = blocks.select { |b| in_degree[b.label] == 0 }
1550
- sorted = []
1639
+ dfs.call(blocks.first.label)
1551
1640
 
1552
- while queue.any?
1553
- # Take the first available block (preserves original order when possible)
1554
- current = queue.shift
1555
- sorted << current
1641
+ # Any unreachable blocks (not visited) are appended at the end
1642
+ rpo = post_order.reverse.filter_map { |label| block_map[label] }
1643
+ remaining = blocks.select { |b| !visited.include?(b.label) }
1556
1644
 
1557
- # For each block that depends on current, decrease its in-degree
1558
- blocks.each do |block|
1559
- if dependencies[block.label]&.include?(current.label)
1560
- in_degree[block.label] -= 1
1561
- if in_degree[block.label] == 0 && !sorted.include?(block) && !queue.include?(block)
1562
- queue << block
1563
- end
1645
+ sorted = rpo + remaining
1646
+
1647
+ # Final pass: respect phi dependencies by moving blocks forward if needed.
1648
+ # If block B depends on block A (via phi), ensure A comes before B.
1649
+ changed = true
1650
+ while changed
1651
+ changed = false
1652
+ sorted.each_with_index do |block, idx|
1653
+ phi_deps[block.label].each do |dep_label|
1654
+ dep_idx = sorted.index { |b| b.label == dep_label }
1655
+ next unless dep_idx && dep_idx > idx
1656
+
1657
+ # Move dep_block before current block
1658
+ dep_block = sorted.delete_at(dep_idx)
1659
+ sorted.insert(idx, dep_block)
1660
+ changed = true
1661
+ break
1564
1662
  end
1663
+ break if changed
1565
1664
  end
1566
1665
  end
1567
1666
 
1568
- # If some blocks weren't added (cycle), add them in original order
1569
- remaining = blocks - sorted
1570
- sorted + remaining
1667
+ sorted
1571
1668
  end
1572
1669
 
1573
1670
  def collect_local_variables_with_types(hir_func)
@@ -1627,6 +1724,22 @@ module Konpeito
1627
1724
  vars
1628
1725
  end
1629
1726
 
1727
+ # Collect all variable names that are captured by any Thread.new block
1728
+ # in the function. Used for pre-boxing at function entry.
1729
+ def collect_thread_captured_vars(hir_func)
1730
+ captured = Set.new
1731
+ hir_func.body.each do |block|
1732
+ block.instructions.each do |inst|
1733
+ if inst.is_a?(HIR::ThreadNew) && inst.block_def
1734
+ inst.block_def.captures.each do |cap|
1735
+ captured << cap.name
1736
+ end
1737
+ end
1738
+ end
1739
+ end
1740
+ captured
1741
+ end
1742
+
1630
1743
  # Collect local variables used in a block definition
1631
1744
  # Similar to collect_local_variables_with_types but for block bodies
1632
1745
  def collect_local_variables_in_block(block_def)
@@ -1722,7 +1835,12 @@ module Konpeito
1722
1835
 
1723
1836
  # Get the self VALUE for the current function.
1724
1837
  # For variadic functions (arity -1), self is at params[2] instead of params[0].
1838
+ # Inside block/rescue callbacks, returns the captured self (set by callback generators).
1839
+ # Inside thread callbacks, params[0] is ptr (void*) not VALUE, so returns Qnil.
1725
1840
  def get_self_value
1841
+ return @block_callback_self if @block_callback_self
1842
+ return @qnil if @in_thread_callback
1843
+
1726
1844
  func = @builder.insert_block.parent
1727
1845
  mangled = func.name
1728
1846
  if @variadic_functions[mangled]
@@ -1736,10 +1854,15 @@ module Konpeito
1736
1854
  llvm_block = @blocks[hir_block.label]
1737
1855
  @builder.position_at_end(llvm_block)
1738
1856
 
1857
+ # Track the current HIR block label so safe navigation can record exit overrides
1858
+ saved_generating_block_label = @generating_block_label
1859
+ @generating_block_label = hir_block.label
1860
+
1739
1861
  # Collect instructions that are owned by BeginRescue nodes
1740
1862
  # These will be generated inside callbacks, not in the main function
1741
1863
  rescue_owned = collect_rescue_owned_instructions(hir_block.instructions)
1742
1864
 
1865
+
1743
1866
  # Generate instructions
1744
1867
  hir_block.instructions.each do |inst|
1745
1868
  next if rescue_owned.include?(inst.object_id)
@@ -1748,29 +1871,42 @@ module Konpeito
1748
1871
 
1749
1872
  # Generate terminator
1750
1873
  generate_terminator(hir_block.terminator) if hir_block.terminator
1874
+
1875
+ @generating_block_label = saved_generating_block_label
1751
1876
  end
1752
1877
 
1753
- # Collect all instructions that are owned by BeginRescue nodes
1754
- # These instructions will be generated inside try/rescue callbacks,
1755
- # not in the main function body
1878
+ # Collect all instructions that are owned by BeginRescue or CaseStatement nodes.
1879
+ # These instructions will be generated inside their proper blocks (callbacks or
1880
+ # when_body blocks), not in the main function body's linear pass.
1756
1881
  def collect_rescue_owned_instructions(instructions)
1757
1882
  owned = Set.new
1758
1883
  instructions.each do |inst|
1759
- next unless inst.is_a?(HIR::BeginRescue)
1884
+ if inst.is_a?(HIR::BeginRescue)
1885
+ # All sub-expression instructions emitted during rescue/else/ensure visitation
1886
+ # are tracked in non_try_instruction_ids (same approach as JVM generator)
1887
+ owned.merge(inst.non_try_instruction_ids) if inst.non_try_instruction_ids
1888
+
1889
+ # Try block instructions (skip Phi nodes — they must stay at the top of their
1890
+ # natural HIR block; generate_phi will return a cached value when called inline)
1891
+ inst.try_blocks&.each { |i| owned << i.object_id unless i.is_a?(HIR::Phi) }
1892
+
1893
+ # Rescue clause body instructions
1894
+ inst.rescue_clauses&.each do |clause|
1895
+ clause.body_blocks&.each { |i| owned << i.object_id }
1896
+ end
1760
1897
 
1761
- # Try block instructions
1762
- inst.try_blocks&.each { |i| owned << i.object_id }
1898
+ # Else block instructions
1899
+ inst.else_blocks&.each { |i| owned << i.object_id }
1763
1900
 
1764
- # Rescue clause body instructions
1765
- inst.rescue_clauses&.each do |clause|
1766
- clause.body_blocks&.each { |i| owned << i.object_id }
1901
+ # Ensure block instructions
1902
+ inst.ensure_blocks&.each { |i| owned << i.object_id }
1903
+ elsif inst.is_a?(HIR::CaseStatement)
1904
+ # When/else body instructions (and their condition expressions) are emitted
1905
+ # into the current block by build_when_clause, but they must only be generated
1906
+ # inside their proper when_body/else LLVM blocks by generate_case_statement.
1907
+ # Skip them here so they are not executed unconditionally before the dispatch.
1908
+ owned.merge(inst.sub_instruction_ids) if inst.sub_instruction_ids
1767
1909
  end
1768
-
1769
- # Else block instructions
1770
- inst.else_blocks&.each { |i| owned << i.object_id }
1771
-
1772
- # Ensure block instructions
1773
- inst.ensure_blocks&.each { |i| owned << i.object_id }
1774
1910
  end
1775
1911
  owned
1776
1912
  end
@@ -1813,6 +1949,8 @@ module Konpeito
1813
1949
  generate_store_constant(inst)
1814
1950
  when HIR::Call
1815
1951
  generate_call(inst)
1952
+ when HIR::CaseEqualityCheck
1953
+ generate_case_equality_check(inst)
1816
1954
  when HIR::SelfRef
1817
1955
  generate_self_ref(inst)
1818
1956
  when HIR::Phi
@@ -2103,9 +2241,12 @@ module Konpeito
2103
2241
  end
2104
2242
 
2105
2243
  # Generate dynamic parts first (they may need to_s conversion)
2106
- # and collect their lengths
2107
2244
  dynamic_values = []
2108
- dynamic_lengths = []
2245
+
2246
+ # Declare rb_obj_as_string (returns String as-is, calls to_s only for non-String)
2247
+ rb_obj_as_string = @mod.functions["rb_obj_as_string"] || @mod.functions.add(
2248
+ "rb_obj_as_string", [value_type], value_type
2249
+ )
2109
2250
 
2110
2251
  part_info.each do |info|
2111
2252
  next if info[:type] == :static
@@ -2116,19 +2257,13 @@ module Konpeito
2116
2257
  val = get_value_as_ruby(info[:hir])
2117
2258
 
2118
2259
  # Convert to string if needed (for non-string interpolated values)
2260
+ # rb_obj_as_string returns String as-is, avoids rb_funcallv method lookup
2119
2261
  val_type = info[:hir].respond_to?(:type) ? info[:hir].type : nil
2120
2262
  unless val_type == TypeChecker::Types::STRING
2121
- to_s_ptr = @builder.global_string_pointer("to_s")
2122
- to_s_id = @builder.call(@rb_intern, to_s_ptr)
2123
- val = @builder.call(@rb_funcallv, val, to_s_id, LLVM::Int32.from_i(0), LLVM::Pointer(value_type).null)
2263
+ val = @builder.call(rb_obj_as_string, val)
2124
2264
  end
2125
2265
 
2126
2266
  dynamic_values << val
2127
- # Get string length (returns Fixnum, need to unbox)
2128
- len_val = @builder.call(@rb_str_length, val)
2129
- # Unbox Fixnum: (value >> 1)
2130
- len_i64 = @builder.ashr(len_val, LLVM::Int64.from_i(1))
2131
- dynamic_lengths << len_i64
2132
2267
  end
2133
2268
 
2134
2269
  # If only one part, just return it
@@ -2143,14 +2278,9 @@ module Konpeito
2143
2278
  return result
2144
2279
  end
2145
2280
 
2146
- # Calculate total length: static_length + sum of dynamic lengths
2147
- total_length = LLVM::Int64.from_i(static_length)
2148
- dynamic_lengths.each do |len|
2149
- total_length = @builder.add(total_length, len)
2150
- end
2151
-
2152
- # Pre-allocate buffer with total capacity
2153
- result = @builder.call(@rb_str_buf_new, total_length)
2281
+ # Pre-allocate buffer: static length + padding for dynamic parts (avoid runtime length scan)
2282
+ total_capacity = LLVM::Int64.from_i(static_length + 128)
2283
+ result = @builder.call(@rb_str_buf_new, total_capacity)
2154
2284
 
2155
2285
  # Append each part using efficient methods
2156
2286
  dynamic_idx = 0
@@ -2571,6 +2701,15 @@ module Konpeito
2571
2701
  @variables[var_name] = value_to_store
2572
2702
  @variable_types[var_name] = target_type
2573
2703
 
2704
+ # Write-back to escape array when inside a rescue/try callback.
2705
+ # This ensures variable mutations inside callbacks are visible to the
2706
+ # outer function after rb_rescue2 returns.
2707
+ if @rescue_escape_array && @rescue_escape_indices&.key?(var_name)
2708
+ idx = @rescue_escape_indices[var_name]
2709
+ boxed = convert_value(value_to_store, target_type, :value)
2710
+ @builder.call(@rb_ary_store, @rescue_escape_array, LLVM::Int64.from_i(idx), boxed)
2711
+ end
2712
+
2574
2713
  # Propagate comparison result flag through variable assignments
2575
2714
  src_var = inst.value.respond_to?(:result_var) ? inst.value.result_var : nil
2576
2715
  if src_var && @comparison_result_vars.include?(src_var)
@@ -2600,9 +2739,17 @@ module Konpeito
2600
2739
  value = @variables[hir_value.result_var]
2601
2740
  type_tag = @variable_types[hir_value.result_var] || :value
2602
2741
  [value, type_tag]
2603
- else
2742
+ elsif @variables.key?(hir_value.var.name)
2604
2743
  # Fall back to var.name (for pattern-bound variables or inliner-created refs)
2605
2744
  var_name = hir_value.var.name
2745
+ value = @variables[var_name]
2746
+ type_tag = @variable_types[var_name] || :value
2747
+ [value, type_tag]
2748
+ else
2749
+ # Variable not yet loaded (e.g. sub-expression in rescue handler body).
2750
+ # Generate the LoadLocal instruction to load from @variable_allocas.
2751
+ generate_instruction(hir_value)
2752
+ var_name = hir_value.result_var || hir_value.var.name
2606
2753
  value = @variables[var_name] || @qnil
2607
2754
  type_tag = @variable_types[var_name] || :value
2608
2755
  [value, type_tag]
@@ -3008,6 +3155,11 @@ module Konpeito
3008
3155
  return generate_unboxed_unary(inst)
3009
3156
  end
3010
3157
 
3158
+ # Try inline String methods (empty?)
3159
+ if can_inline_string_method?(inst)
3160
+ return generate_inline_string_method(inst)
3161
+ end
3162
+
3011
3163
  # Try unboxed arithmetic optimization
3012
3164
  if can_use_unboxed_arithmetic?(inst)
3013
3165
  # generate_unboxed_arithmetic sets @variables and @variable_types internally
@@ -3036,9 +3188,11 @@ module Konpeito
3036
3188
  return result
3037
3189
  end
3038
3190
 
3039
- # Check if we can call a local function directly
3191
+ # Check if we can call a local function directly.
3192
+ # Skip if the method is defined in multiple classes (polymorphic) — such
3193
+ # calls must use rb_funcallv so the correct subclass override is invoked.
3040
3194
  local_func = @functions[inst.method_name]
3041
- if local_func && self_receiver?(inst.receiver)
3195
+ if local_func && self_receiver?(inst.receiver) && !@polymorphic_methods.include?(inst.method_name)
3042
3196
  result = generate_direct_call(inst, inst.method_name)
3043
3197
  @variables[inst.result_var] = result if inst.result_var
3044
3198
  return result
@@ -3091,6 +3245,12 @@ module Konpeito
3091
3245
  return result
3092
3246
  end
3093
3247
 
3248
+ # Try optimized String method calls before rb_funcallv fallback
3249
+ if (result = try_optimized_string_call(inst, receiver_type))
3250
+ @variables[inst.result_var] = result if inst.result_var
3251
+ return result
3252
+ end
3253
+
3094
3254
  # Get receiver as Ruby VALUE (box if needed)
3095
3255
  receiver = get_value_as_ruby(inst.receiver)
3096
3256
 
@@ -3101,7 +3261,7 @@ module Konpeito
3101
3261
  # Build keyword arguments hash if present
3102
3262
  kwargs_hash = nil
3103
3263
  if inst.has_keyword_args?
3104
- kwargs_hash = build_keyword_args_hash(inst.keyword_args)
3264
+ kwargs_hash = build_keyword_args_hash(inst.keyword_args, keyword_splat: inst.keyword_splat)
3105
3265
  end
3106
3266
 
3107
3267
  # Check if any argument is a splat
@@ -3136,7 +3296,14 @@ module Konpeito
3136
3296
  argv = @builder.bit_cast(argv, LLVM::Pointer(value_type))
3137
3297
  end
3138
3298
 
3139
- result = @builder.call(@rb_funcallv, receiver, method_id, argc, argv)
3299
+ # Use rb_funcallv_kw when keyword arguments are present so Ruby
3300
+ # correctly separates them from positional arguments (Ruby 3.0+)
3301
+ if kwargs_hash
3302
+ rb_pass_keywords = LLVM::Int32.from_i(1) # RB_PASS_KEYWORDS
3303
+ result = @builder.call(@rb_funcallv_kw, receiver, method_id, argc, argv, rb_pass_keywords)
3304
+ else
3305
+ result = @builder.call(@rb_funcallv, receiver, method_id, argc, argv)
3306
+ end
3140
3307
  end
3141
3308
 
3142
3309
  @variables[inst.result_var] = result if inst.result_var
@@ -3229,6 +3396,14 @@ module Konpeito
3229
3396
  end
3230
3397
  end
3231
3398
  end
3399
+
3400
+ # Record that the enclosing HIR block now exits through safe_merge_bb.
3401
+ # Any HIR::Phi that references the enclosing HIR block as a predecessor
3402
+ # must use safe_merge_bb instead of the original LLVM block.
3403
+ if @generating_block_label
3404
+ @block_exit_overrides[@generating_block_label] = safe_merge_bb
3405
+ end
3406
+
3232
3407
  phi
3233
3408
  end
3234
3409
 
@@ -3439,13 +3614,30 @@ module Konpeito
3439
3614
  end
3440
3615
  end
3441
3616
 
3442
- # Build a Ruby Hash from keyword arguments
3617
+ # Build a Ruby Hash from keyword arguments (and optional **hash splat)
3443
3618
  # Returns an LLVM VALUE representing the Hash
3444
- def build_keyword_args_hash(keyword_args)
3619
+ def build_keyword_args_hash(keyword_args, keyword_splat: nil)
3620
+ # If only a keyword splat with no explicit keyword args, use the hash directly
3621
+ if keyword_splat && keyword_args.empty?
3622
+ return get_value_as_ruby(keyword_splat)
3623
+ end
3624
+
3445
3625
  # Create new hash
3446
3626
  hash = @builder.call(@rb_hash_new)
3447
3627
 
3448
- # Add each keyword argument
3628
+ # Merge splatted hash if present (mixed case: **hash + explicit key: val)
3629
+ if keyword_splat
3630
+ splat_value = get_value_as_ruby(keyword_splat)
3631
+ update_id_ptr = @builder.global_string_pointer("update")
3632
+ update_id = @builder.call(@rb_intern, update_id_ptr)
3633
+ argv = @builder.alloca(LLVM::Array(value_type, 1))
3634
+ ptr = @builder.gep(argv, [LLVM::Int32.from_i(0), LLVM::Int32.from_i(0)])
3635
+ @builder.store(splat_value, ptr)
3636
+ argv_ptr = @builder.bit_cast(argv, LLVM::Pointer(value_type))
3637
+ @builder.call(@rb_funcallv, hash, update_id, LLVM::Int32.from_i(1), argv_ptr)
3638
+ end
3639
+
3640
+ # Add each explicit keyword argument
3449
3641
  keyword_args.each do |key_name, value_inst|
3450
3642
  # Convert key name to Ruby Symbol
3451
3643
  key_str_ptr = @builder.global_string_pointer(key_name.to_s)
@@ -4309,37 +4501,108 @@ module Konpeito
4309
4501
  method_ptr = @builder.global_string_pointer(inst.method_name)
4310
4502
  method_id = @builder.call(@rb_intern, method_ptr)
4311
4503
 
4312
- # Prepare arguments array (for methods like upto/downto that take args)
4313
- argc = LLVM::Int32.from_i(inst.args.size)
4504
+ # Build keyword arguments hash if present
4505
+ kwargs_hash = nil
4506
+ if inst.has_keyword_args?
4507
+ kwargs_hash = build_keyword_args_hash(inst.keyword_args, keyword_splat: inst.keyword_splat)
4508
+ end
4314
4509
 
4315
- if inst.args.empty?
4510
+ # Prepare arguments array (regular args + optional kwargs hash)
4511
+ total_args = inst.args.dup
4512
+ total_args << kwargs_hash if kwargs_hash
4513
+
4514
+ argc = LLVM::Int32.from_i(total_args.size)
4515
+
4516
+ if total_args.empty?
4316
4517
  argv = LLVM::Pointer(value_type).null
4317
4518
  else
4318
- argv = @builder.alloca(LLVM::Array(value_type, inst.args.size))
4319
- inst.args.each_with_index do |arg, i|
4320
- arg_value = get_value_as_ruby(arg)
4519
+ argv = @builder.alloca(LLVM::Array(value_type, total_args.size))
4520
+ total_args.each_with_index do |arg, i|
4521
+ arg_value = arg.is_a?(LLVM::Value) ? arg : get_value_as_ruby(arg)
4321
4522
  ptr = @builder.gep(argv, [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)])
4322
4523
  @builder.store(arg_value, ptr)
4323
4524
  end
4324
4525
  argv = @builder.bit_cast(argv, LLVM::Pointer(value_type))
4325
4526
  end
4326
4527
 
4327
- # Create capture struct for closure variables
4528
+ # Create capture struct for closure variables.
4529
+ # Also capture outer self so @ivar access inside blocks works correctly.
4328
4530
  captures = inst.block.captures.select { |c| @variable_allocas[c.name] }
4329
- capture_data = create_capture_struct(captures)
4531
+
4532
+ outer_self_val = get_self_value
4533
+ self_slot = @builder.alloca(value_type, "blk_self_slot")
4534
+ @builder.store(outer_self_val, self_slot)
4535
+ @variable_allocas[BLOCK_SELF_CAPTURE] = self_slot
4536
+ self_capture = HIR::Capture.new(name: BLOCK_SELF_CAPTURE, type: nil)
4537
+ all_captures = [self_capture] + captures
4330
4538
 
4331
4539
  # Collect type info for captured variables
4332
- capture_types = {}
4540
+ capture_types = { BLOCK_SELF_CAPTURE => :value }
4333
4541
  captures.each do |capture|
4334
4542
  capture_types[capture.name] = @variable_types[capture.name] || :value
4335
4543
  end
4336
4544
 
4337
- # Generate block callback function
4338
- block_func = generate_block_callback(inst.block, inst.method_name, captures, capture_types)
4545
+ # Determine capture strategy.
4546
+ #
4547
+ # - Builtin block iterators (each, times, map, etc.) call the block inline
4548
+ # before returning, so the stack remains alive and pointer-to-alloca is safe.
4549
+ # These arrive via generate_block_iterator_call with a non-nil `builtin`.
4550
+ #
4551
+ # - Compiled user functions that use `yield` call blocks inline → pointer-to-alloca safe,
4552
+ # and write-back through captured variable allocas works correctly.
4553
+ #
4554
+ # - Compiled user functions with a `&blk` parameter (block: true in HIR::Param) STORE the
4555
+ # block as a Proc (e.g. Button#on_click stores the block in @click_handler). The stack
4556
+ # frame of the caller will be gone when the proc is eventually called. These MUST use
4557
+ # escape-cells to avoid dangling-pointer segfaults.
4558
+ #
4559
+ # - External / unknown methods (not in the builtin table and not compiled by Konpeito)
4560
+ # may also store the block. Use escape-cells for safety.
4561
+ #
4562
+ # Additionally, when we are already inside a block callback (@in_block_callback),
4563
+ # the outer callback's stack will also be freed before any nested proc runs, so
4564
+ # escape-cells is required there too.
4565
+ hir_target = @hir_functions[inst.method_name.to_s]
4566
+ target_stores_block = hir_target&.params&.any? { |p| p.block }
4567
+ is_external_method = builtin.nil? && @functions[inst.method_name].nil?
4568
+ use_escape_cells = target_stores_block || is_external_method || @in_block_callback
4569
+
4570
+ if use_escape_cells
4571
+ esc_ary = @builder.call(@rb_ary_new_capa, LLVM::Int64.from_i(all_captures.size), "blk_esc_ary")
4572
+ all_captures.each do |cap|
4573
+ val = if @variable_allocas[cap.name]
4574
+ @builder.load2(value_type, @variable_allocas[cap.name], "blk_esc_#{cap.name}")
4575
+ else
4576
+ @qnil
4577
+ end
4578
+ @builder.call(@rb_ary_push, esc_ary, val)
4579
+ end
4339
4580
 
4340
- # Call rb_block_call with capture data
4341
- result = @builder.call(@rb_block_call,
4342
- receiver, method_id, argc, argv, block_func, capture_data)
4581
+ block_func = generate_block_callback(inst.block, inst.method_name, all_captures, capture_types,
4582
+ escape_cells_mode: true)
4583
+ if kwargs_hash
4584
+ rb_pass_keywords = LLVM::Int32.from_i(1)
4585
+ result = @builder.call(@rb_block_call_kw,
4586
+ receiver, method_id, argc, argv, block_func, esc_ary, rb_pass_keywords)
4587
+ else
4588
+ result = @builder.call(@rb_block_call,
4589
+ receiver, method_id, argc, argv, block_func, esc_ary)
4590
+ end
4591
+ else
4592
+ # Pointer-to-alloca mode: captures are stored as VALUE* pointers in a struct.
4593
+ # The callback reads/writes through those pointers, so mutations propagate back.
4594
+ capture_data = create_capture_struct(all_captures)
4595
+ block_func = generate_block_callback(inst.block, inst.method_name, all_captures, capture_types,
4596
+ escape_cells_mode: false)
4597
+ if kwargs_hash
4598
+ rb_pass_keywords = LLVM::Int32.from_i(1)
4599
+ result = @builder.call(@rb_block_call_kw,
4600
+ receiver, method_id, argc, argv, block_func, capture_data, rb_pass_keywords)
4601
+ else
4602
+ result = @builder.call(@rb_block_call,
4603
+ receiver, method_id, argc, argv, block_func, capture_data)
4604
+ end
4605
+ end
4343
4606
 
4344
4607
  result
4345
4608
  end
@@ -4395,6 +4658,7 @@ module Konpeito
4395
4658
  saved_types = @variable_types.dup
4396
4659
  saved_allocas = @variable_allocas.dup
4397
4660
  saved_in_block_callback = @in_block_callback
4661
+ saved_block_callback_self = @block_callback_self
4398
4662
 
4399
4663
  # Create entry block for callback
4400
4664
  entry = callback_func.basic_blocks.append("entry")
@@ -4450,6 +4714,14 @@ module Konpeito
4450
4714
  end
4451
4715
  end
4452
4716
 
4717
+ # Extract the captured outer self for @ivar access inside blocks.
4718
+ # BLOCK_SELF_CAPTURE is always included by generate_rb_block_call.
4719
+ @block_callback_self = nil
4720
+ if @variable_allocas[BLOCK_SELF_CAPTURE]
4721
+ @block_callback_self = @builder.load2(value_type,
4722
+ @variable_allocas[BLOCK_SELF_CAPTURE], "blk_self")
4723
+ end
4724
+
4453
4725
  # Bind block parameters to yielded values
4454
4726
  # For single-parameter blocks, use yielded_arg directly
4455
4727
  # For multi-parameter blocks, check argc at runtime:
@@ -4551,6 +4823,12 @@ module Konpeito
4551
4823
  val = @builder.load2(value_type, param_allocas[i], param.name)
4552
4824
  @variables[param.name] = val
4553
4825
  @variable_types[param.name] = :value
4826
+ # Register the alloca so that generate_store_local uses the correct
4827
+ # pointer when the block body assigns to a parameter variable
4828
+ # (e.g. `completely = true` inside an |painter, completely| block).
4829
+ # Without this, generate_store_local creates a new alloca with the
4830
+ # same name as the already-loaded i64 value, causing an LLVM type error.
4831
+ @variable_allocas[param.name] = param_allocas[i]
4554
4832
  end
4555
4833
  end
4556
4834
 
@@ -4597,8 +4875,10 @@ module Konpeito
4597
4875
  # Jump from entry to first HIR block
4598
4876
  @builder.br(@blocks[block_def.body.first.label])
4599
4877
 
4600
- # Generate each block (instructions + terminators)
4601
- block_def.body.each do |hir_block|
4878
+ # Generate each block in phi-dependency order (same as setup_function)
4879
+ # Without sorting, nested if/else phi nodes may be placed in the wrong block
4880
+ sorted_callback_blocks = sort_blocks_by_phi_dependencies(block_def.body)
4881
+ sorted_callback_blocks.each do |hir_block|
4602
4882
  generate_block(callback_func, hir_block)
4603
4883
  end
4604
4884
 
@@ -4635,7 +4915,9 @@ module Konpeito
4635
4915
  result_type = :value
4636
4916
  last_inst = nil
4637
4917
  block_def.body.each do |basic_block|
4918
+ rescue_owned = collect_rescue_owned_instructions(basic_block.instructions)
4638
4919
  basic_block.instructions.each do |hir_inst|
4920
+ next if rescue_owned.include?(hir_inst.object_id)
4639
4921
  result = generate_instruction(hir_inst)
4640
4922
  last_inst = hir_inst
4641
4923
  end
@@ -4665,6 +4947,7 @@ module Konpeito
4665
4947
  @variable_types = saved_types
4666
4948
  @variable_allocas = saved_allocas
4667
4949
  @in_block_callback = saved_in_block_callback
4950
+ @block_callback_self = saved_block_callback_self
4668
4951
 
4669
4952
  callback_func
4670
4953
  end
@@ -5025,33 +5308,58 @@ module Konpeito
5025
5308
  capture_types[cap.name] = @variable_types[cap.name]
5026
5309
  end
5027
5310
 
5311
+ # Determine if all captures have been pre-boxed to heap cells.
5312
+ # When ThreadNew is at function top-level, collect_thread_captured_vars finds it
5313
+ # and pre-boxes the captures. When ThreadNew is inside a rescue callback or
5314
+ # other nested scope, pre-boxing doesn't happen — we must use the safe value-copy
5315
+ # protocol instead (snapshot values, no bidirectional mutation).
5316
+ all_preboxed = !captures.empty? && captures.all? { |c| @thread_boxed_vars[c.name] }
5317
+
5028
5318
  # Create thread-specific callback (different signature from fiber/proc)
5029
- callback_func = generate_thread_callback(block_def, captures, capture_types)
5319
+ callback_func = generate_thread_callback(block_def, captures, capture_types, cell_protocol: all_preboxed)
5030
5320
 
5031
- # Setup captures data (pass as intptr_t, cast back in callback)
5032
5321
  if captures.empty?
5033
- captures_data = LLVM::Int64.from_i(0)
5322
+ captures_ptr = LLVM::Pointer(LLVM::Int8).null
5034
5323
  else
5035
- ptr_type = LLVM::Pointer(value_type)
5036
- array_type = LLVM::Array(ptr_type, captures.size)
5037
- captures_array = @builder.alloca(array_type, "thread_captures")
5324
+ declare_malloc
5325
+ n = captures.size
5038
5326
 
5039
- captures.each_with_index do |capture, i|
5040
- alloca = @variable_allocas[capture.name]
5041
- if alloca
5042
- ptr = @builder.gep(captures_array, [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)])
5043
- @builder.store(alloca, ptr)
5044
- else
5045
- ptr = @builder.gep(captures_array, [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)])
5046
- @builder.store(LLVM::Pointer(value_type).null, ptr)
5327
+ if all_preboxed
5328
+ # Cell-pointer protocol: pass pointers to shared heap cells.
5329
+ # Enables bidirectional mutation visibility between threads.
5330
+ buf = @builder.call(@malloc, LLVM::Int64.from_i(n * 8), "thread_ptrbuf")
5331
+ buf_int = @builder.ptr2int(buf, LLVM::Int64, "thread_ptrbuf_int")
5332
+ buf_typed = @builder.int2ptr(buf_int, LLVM::Pointer(LLVM::Pointer(value_type)), "thread_ptrbuf_typed")
5333
+
5334
+ captures.each_with_index do |capture, i|
5335
+ cell_ptr = @thread_boxed_vars[capture.name]
5336
+ elem_ptr = @builder.gep2(LLVM::Pointer(value_type), buf_typed, [LLVM::Int64.from_i(i)], "tpb_elem_#{i}")
5337
+ @builder.store(cell_ptr, elem_ptr)
5047
5338
  end
5048
- end
5049
5339
 
5050
- captures_data = @builder.ptr2int(captures_array, LLVM::Int64, "captures_int")
5051
- end
5340
+ captures_ptr = buf
5341
+ else
5342
+ # Value-copy protocol: snapshot current VALUES into the buffer.
5343
+ # Safe for async threads (no pointer dependency on caller's stack).
5344
+ # No bidirectional mutation — thread gets independent copies.
5345
+ buf_size = LLVM::Int64.from_i(n * 8)
5346
+ raw_ptr = @builder.call(@malloc, buf_size, "thread_cap_buf")
5347
+ raw_int = @builder.ptr2int(raw_ptr, LLVM::Int64, "thread_cap_int")
5348
+ val_arr_ptr = @builder.int2ptr(raw_int, LLVM::Pointer(value_type), "thread_cap_vals")
5349
+
5350
+ captures.each_with_index do |capture, i|
5351
+ val = if @variable_allocas[capture.name]
5352
+ @builder.load2(value_type, @variable_allocas[capture.name], "tc_#{capture.name}")
5353
+ else
5354
+ @qnil
5355
+ end
5356
+ elem_ptr = @builder.gep2(value_type, val_arr_ptr, [LLVM::Int64.from_i(i)], "tc_elem_#{i}")
5357
+ @builder.store(val, elem_ptr)
5358
+ end
5052
5359
 
5053
- # Call rb_thread_create - pass captures as intptr_t (void* compatible)
5054
- captures_ptr = @builder.int2ptr(captures_data, LLVM::Pointer(LLVM::Int8), "captures_ptr")
5360
+ captures_ptr = raw_ptr
5361
+ end
5362
+ end
5055
5363
  result = @builder.call(@rb_thread_create, callback_func, captures_ptr)
5056
5364
 
5057
5365
  if inst.result_var
@@ -5063,7 +5371,9 @@ module Konpeito
5063
5371
  end
5064
5372
 
5065
5373
  # Generate thread callback function with correct signature: VALUE func(void*)
5066
- def generate_thread_callback(block_def, captures = [], capture_types = {})
5374
+ # cell_protocol: true = buffer contains cell POINTERS (shared heap cells, bidirectional)
5375
+ # false = buffer contains VALUE copies (snapshot, safe for async)
5376
+ def generate_thread_callback(block_def, captures = [], capture_types = {}, cell_protocol: false)
5067
5377
  return nil unless block_def
5068
5378
 
5069
5379
  # Create a unique name for the callback
@@ -5082,38 +5392,63 @@ module Konpeito
5082
5392
  saved_types = @variable_types.dup
5083
5393
  saved_allocas = @variable_allocas.dup
5084
5394
  saved_current_function = @current_function
5395
+ saved_in_block_callback = @in_block_callback
5396
+ saved_block_callback_self = @block_callback_self
5397
+ saved_in_thread_callback = @in_thread_callback
5398
+ saved_thread_boxed_vars = @thread_boxed_vars
5085
5399
 
5086
5400
  # Create entry block for callback
5087
5401
  entry = callback_func.basic_blocks.append("entry")
5088
5402
  @builder.position_at_end(entry)
5089
5403
  @current_function = callback_func
5090
5404
 
5091
- # Reset variable tracking for callback scope
5405
+ # Reset variable tracking for callback scope.
5406
+ # Thread callbacks are independent execution contexts — reset block callback state.
5092
5407
  @variables = {}
5093
5408
  @variable_types = {}
5094
5409
  @variable_allocas = {}
5410
+ @in_block_callback = false
5411
+ @block_callback_self = nil
5412
+ @in_thread_callback = true
5095
5413
 
5096
- # Setup captured variable access through void* arg
5097
5414
  unless captures.empty?
5098
- # The void* arg contains captures array pointer (cast from intptr_t)
5099
- ptr_type = LLVM::Pointer(value_type)
5100
- array_type = LLVM::Array(ptr_type, captures.size)
5415
+ declare_free
5101
5416
 
5102
- # Convert void* (i8*) to intptr_t, then to the array pointer type
5103
- arg_int = @builder.ptr2int(callback_func.params[0], LLVM::Int64, "arg_int")
5104
- captures_ptr = @builder.int2ptr(arg_int, LLVM::Pointer(array_type), "captures_ptr")
5417
+ if cell_protocol
5418
+ # Cell-pointer protocol: buffer contains pointers to shared heap cells.
5419
+ # Reads/writes through cell pointers are visible to both sides.
5420
+ arg_int = @builder.ptr2int(callback_func.params[0], LLVM::Int64, "tc_arg_int")
5421
+ buf_typed = @builder.int2ptr(arg_int, LLVM::Pointer(LLVM::Pointer(value_type)), "tc_buf_typed")
5105
5422
 
5106
- captures.each_with_index do |capture, i|
5107
- # Get pointer to the pointer (VALUE**)
5108
- elem_ptr_ptr = @builder.gep2(array_type, captures_ptr,
5109
- [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)], "cap_#{capture.name}_ptr_ptr")
5110
- # Load the pointer to the variable (VALUE*)
5111
- elem_ptr = @builder.load2(ptr_type, elem_ptr_ptr, "cap_#{capture.name}_ptr")
5423
+ captures.each_with_index do |capture, i|
5424
+ elem_ptr = @builder.gep2(LLVM::Pointer(value_type), buf_typed, [LLVM::Int64.from_i(i)], "tc_bp_#{i}")
5425
+ cell_ptr = @builder.load2(LLVM::Pointer(value_type), elem_ptr, "tc_cell_#{capture.name}")
5426
+ @variable_allocas[capture.name] = cell_ptr
5427
+ @variable_types[capture.name] = capture_types[capture.name] || :value
5428
+ val = @builder.load2(value_type, cell_ptr, "tc_#{capture.name}_val")
5429
+ @variables[capture.name] = val
5430
+ end
5112
5431
 
5113
- # Store in variable_allocas so LoadLocal/StoreLocal can use it
5114
- @variable_allocas[capture.name] = elem_ptr
5115
- # Preserve the original type from outer scope
5116
- @variable_types[capture.name] = capture_types[capture.name] || :value
5432
+ # Free the pointer buffer (NOT the cells — they're shared)
5433
+ @builder.call(@free, callback_func.params[0])
5434
+ else
5435
+ # Value-copy protocol: buffer contains snapshot VALUE copies.
5436
+ # Load each VALUE into a local alloca, then free the buffer.
5437
+ arg_int = @builder.ptr2int(callback_func.params[0], LLVM::Int64, "tc_arg_int")
5438
+ val_arr_ptr = @builder.int2ptr(arg_int, LLVM::Pointer(value_type), "tc_vals")
5439
+
5440
+ captures.each_with_index do |capture, i|
5441
+ elem_ptr = @builder.gep2(value_type, val_arr_ptr, [LLVM::Int64.from_i(i)], "tc_elem_#{i}")
5442
+ val = @builder.load2(value_type, elem_ptr, "tc_#{capture.name}_val")
5443
+ alloca = @builder.alloca(value_type, "tc_#{capture.name}")
5444
+ @builder.store(val, alloca)
5445
+ @variable_allocas[capture.name] = alloca
5446
+ @variable_types[capture.name] = capture_types[capture.name] || :value
5447
+ @variables[capture.name] = val
5448
+ end
5449
+
5450
+ # Free the malloc'd buffer
5451
+ @builder.call(@free, callback_func.params[0])
5117
5452
  end
5118
5453
  end
5119
5454
 
@@ -5151,8 +5486,9 @@ module Konpeito
5151
5486
  # Jump from entry to first HIR block
5152
5487
  @builder.br(@blocks[block_def.body.first.label])
5153
5488
 
5154
- # Generate each block (instructions + terminators)
5155
- block_def.body.each do |hir_block|
5489
+ # Generate each block in phi-dependency order (same as setup_function)
5490
+ sorted_callback_blocks = sort_blocks_by_phi_dependencies(block_def.body)
5491
+ sorted_callback_blocks.each do |hir_block|
5156
5492
  generate_block(callback_func, hir_block)
5157
5493
  end
5158
5494
 
@@ -5177,7 +5513,9 @@ module Konpeito
5177
5513
  result_type = :value
5178
5514
  last_inst = nil
5179
5515
  block_def.body.each do |basic_block|
5516
+ rescue_owned = collect_rescue_owned_instructions(basic_block.instructions)
5180
5517
  basic_block.instructions.each do |hir_inst|
5518
+ next if rescue_owned.include?(hir_inst.object_id)
5181
5519
  result = generate_instruction(hir_inst)
5182
5520
  last_inst = hir_inst
5183
5521
  end
@@ -5203,6 +5541,10 @@ module Konpeito
5203
5541
  @variable_types = saved_types
5204
5542
  @variable_allocas = saved_allocas
5205
5543
  @current_function = saved_current_function
5544
+ @in_block_callback = saved_in_block_callback
5545
+ @block_callback_self = saved_block_callback_self
5546
+ @in_thread_callback = saved_in_thread_callback
5547
+ @thread_boxed_vars = saved_thread_boxed_vars
5206
5548
 
5207
5549
  callback_func
5208
5550
  end
@@ -5428,7 +5770,9 @@ module Konpeito
5428
5770
  last_inst = nil
5429
5771
 
5430
5772
  block_def.body.each do |basic_block|
5773
+ rescue_owned = collect_rescue_owned_instructions(basic_block.instructions)
5431
5774
  basic_block.instructions.each do |hir_inst|
5775
+ next if rescue_owned.include?(hir_inst.object_id)
5432
5776
  result = generate_instruction(hir_inst) || result
5433
5777
  last_inst = hir_inst
5434
5778
  end
@@ -6043,6 +6387,8 @@ module Konpeito
6043
6387
  @builder.fmul(left, right)
6044
6388
  when "/"
6045
6389
  @builder.fdiv(left, right)
6390
+ when "%"
6391
+ @builder.frem(left, right)
6046
6392
  else
6047
6393
  raise "Unknown float operation: #{inst.method_name}"
6048
6394
  end
@@ -6212,6 +6558,88 @@ module Konpeito
6212
6558
  end
6213
6559
  end
6214
6560
 
6561
+ # Try optimized String method calls (String#[] 2-arg, String#split with literal)
6562
+ def try_optimized_string_call(inst, receiver_type)
6563
+ resolved_type = receiver_type.is_a?(TypeChecker::TypeVar) ? resolve_type_var(receiver_type) : receiver_type
6564
+ is_string = resolved_type == TypeChecker::Types::STRING ||
6565
+ (resolved_type.is_a?(TypeChecker::Types::ClassInstance) && resolved_type.name == :String)
6566
+ return nil unless is_string
6567
+
6568
+ case inst.method_name
6569
+ when "[]"
6570
+ # String#[start, length] → rb_str_substr(str, start, length)
6571
+ return nil unless inst.args.size == 2
6572
+
6573
+ receiver = get_value_as_ruby(inst.receiver)
6574
+ start_val = get_value_as_ruby(inst.args[0])
6575
+ start_long = @builder.call(@rb_num2long, start_val)
6576
+ len_val = get_value_as_ruby(inst.args[1])
6577
+ len_long = @builder.call(@rb_num2long, len_val)
6578
+
6579
+ rb_str_substr = @mod.functions["rb_str_substr"] || @mod.functions.add(
6580
+ "rb_str_substr", [value_type, LLVM::Int64, LLVM::Int64], value_type
6581
+ )
6582
+ @builder.call(rb_str_substr, receiver, start_long, len_long)
6583
+ when "split"
6584
+ # String#split(separator) → rb_str_split(str, separator_cstr)
6585
+ return nil unless inst.args.size == 1
6586
+
6587
+ receiver = get_value_as_ruby(inst.receiver)
6588
+
6589
+ # rb_str_split takes (VALUE str, const char *sep)
6590
+ rb_str_split = @mod.functions["rb_str_split"] || @mod.functions.add(
6591
+ "rb_str_split", [value_type, LLVM::Pointer(LLVM::Int8)], value_type
6592
+ )
6593
+
6594
+ if inst.args[0].is_a?(HIR::StringLit)
6595
+ # Literal separator: use compile-time C string directly
6596
+ sep_ptr = @builder.global_string_pointer(inst.args[0].value)
6597
+ else
6598
+ # Dynamic separator: convert to C string
6599
+ sep_value = get_value_as_ruby(inst.args[0])
6600
+ sep_value_ptr = @builder.alloca(LLVM::Int64, "split_sep_ptr")
6601
+ @builder.store(sep_value, sep_value_ptr)
6602
+ sep_ptr = @builder.call(@rb_string_value_cstr, sep_value_ptr, "split_sep_cstr")
6603
+ end
6604
+ @builder.call(rb_str_split, receiver, sep_ptr)
6605
+ end
6606
+ end
6607
+
6608
+ # Inline String method constants
6609
+ INLINE_STRING_METHODS = %w[empty?].freeze
6610
+
6611
+ # Check if a method call can use inline String optimization
6612
+ def can_inline_string_method?(inst)
6613
+ return false unless inst.args.empty?
6614
+ return false unless INLINE_STRING_METHODS.include?(inst.method_name)
6615
+
6616
+ receiver_type = get_type(inst.receiver)
6617
+ receiver_type = resolve_type_var(receiver_type) if receiver_type.is_a?(TypeChecker::TypeVar)
6618
+ receiver_type == TypeChecker::Types::STRING ||
6619
+ (receiver_type.is_a?(TypeChecker::Types::ClassInstance) && receiver_type.name == :String)
6620
+ end
6621
+
6622
+ # Generate inline String method
6623
+ def generate_inline_string_method(inst)
6624
+ receiver = get_value_as_ruby(inst.receiver)
6625
+
6626
+ case inst.method_name
6627
+ when "empty?"
6628
+ # rb_str_strlen returns long (byte count), avoids rb_funcallv method lookup
6629
+ rb_str_strlen = @mod.functions["rb_str_strlen"] || @mod.functions.add(
6630
+ "rb_str_strlen", [value_type], LLVM::Int64
6631
+ )
6632
+ str_len = @builder.call(rb_str_strlen, receiver, "str_len")
6633
+ is_empty = @builder.icmp(:eq, str_len, LLVM::Int64.from_i(0), "is_empty")
6634
+ result = @builder.select(is_empty, @qtrue, @qfalse)
6635
+ if inst.result_var
6636
+ @variables[inst.result_var] = result
6637
+ @variable_types[inst.result_var] = :value
6638
+ end
6639
+ result
6640
+ end
6641
+ end
6642
+
6215
6643
  # Generate Array#[] using rb_ary_entry
6216
6644
  def generate_array_index_get(inst)
6217
6645
  receiver = get_value_as_ruby(inst.receiver)
@@ -6743,7 +7171,7 @@ module Konpeito
6743
7171
  kw_info = @keyword_param_functions[func_name.to_s] || @keyword_param_functions[func_name.to_sym]
6744
7172
  if kw_info
6745
7173
  if inst.has_keyword_args?
6746
- kwargs_hash = build_keyword_args_hash(inst.keyword_args)
7174
+ kwargs_hash = build_keyword_args_hash(inst.keyword_args, keyword_splat: inst.keyword_splat)
6747
7175
  call_args << kwargs_hash
6748
7176
  else
6749
7177
  # No keyword args provided - pass an empty hash
@@ -6783,7 +7211,7 @@ module Konpeito
6783
7211
  # Collect all arguments (regular + keyword hash if present)
6784
7212
  all_args = inst.args.map { |arg| get_value_as_ruby(arg) }
6785
7213
  if inst.has_keyword_args?
6786
- kwargs_hash = build_keyword_args_hash(inst.keyword_args)
7214
+ kwargs_hash = build_keyword_args_hash(inst.keyword_args, keyword_splat: inst.keyword_splat)
6787
7215
  all_args << kwargs_hash
6788
7216
  end
6789
7217
 
@@ -6941,23 +7369,60 @@ module Konpeito
6941
7369
  end
6942
7370
  result = scope
6943
7371
  else
6944
- # Unqualified constant - search current class/module first, then Object
7372
+ # Unqualified constant follow Ruby's nesting lookup:
7373
+ # 1. owner_class (e.g. RanmaFrame) — check with rb_const_defined_at
7374
+ # 2. owner_module (e.g. Kumiki) if constant not directly in class
7375
+ # 3. rb_cObject as final fallback
7376
+ #
7377
+ # Uses `select` to pick the lookup scope without creating new basic blocks
7378
+ # (creating blocks mid-HIR-block corrupts phi node predecessor relationships).
6945
7379
  const_name = parts.first
6946
- owner_class = @current_hir_func&.owner_class || @current_hir_func&.owner_module
7380
+ const_name_ptr = @builder.global_string_pointer(const_name)
7381
+ const_id = @builder.call(@rb_intern, const_name_ptr)
7382
+ rb_cobject_val = @builder.load2(value_type, @rb_cObject, "rb_cObject")
7383
+
7384
+ owner_class = @current_hir_func&.owner_class
7385
+ owner_module = @current_hir_func&.owner_module
7386
+
6947
7387
  if owner_class
6948
- # Look up constant in the owning class/module
6949
- owner_ptr = @builder.global_string_pointer(owner_class)
6950
- owner_id = @builder.call(@rb_intern, owner_ptr)
6951
- rb_cobject_val = @builder.load2(value_type, @rb_cObject, "rb_cObject")
6952
- owner_value = @builder.call(@rb_const_get, rb_cobject_val, owner_id)
6953
- const_name_ptr = @builder.global_string_pointer(const_name)
6954
- const_id = @builder.call(@rb_intern, const_name_ptr)
6955
- result = @builder.call(@rb_const_get, owner_value, const_id)
7388
+ # Resolve the owner class value (qualified by module if present)
7389
+ if owner_module
7390
+ owner_mod_ptr = @builder.global_string_pointer(owner_module)
7391
+ owner_mod_id = @builder.call(@rb_intern, owner_mod_ptr)
7392
+ owner_mod_val = @builder.call(@rb_const_get, rb_cobject_val, owner_mod_id)
7393
+ owner_cls_ptr = @builder.global_string_pointer(owner_class)
7394
+ owner_cls_id = @builder.call(@rb_intern, owner_cls_ptr)
7395
+ owner_cls_val = @builder.call(@rb_const_get, owner_mod_val, owner_cls_id)
7396
+ else
7397
+ owner_cls_ptr = @builder.global_string_pointer(owner_class)
7398
+ owner_cls_id = @builder.call(@rb_intern, owner_cls_ptr)
7399
+ owner_cls_val = @builder.call(@rb_const_get, rb_cobject_val, owner_cls_id)
7400
+ owner_mod_val = rb_cobject_val
7401
+ end
7402
+
7403
+ if owner_module
7404
+ # Use rb_const_defined_at to select the scope without branching:
7405
+ # if constant is defined directly in owner_class → look in owner_class
7406
+ # otherwise → look in owner_module
7407
+ @rb_const_defined_at ||= @mod.functions.add("rb_const_defined_at",
7408
+ [value_type, id_type], LLVM::Int32)
7409
+ defined_in_cls = @builder.call(@rb_const_defined_at, owner_cls_val, const_id, "def_in_cls")
7410
+ is_in_cls = @builder.icmp(:ne, defined_in_cls, LLVM::Int32.from_i(0))
7411
+ # Select lookup scope: class if defined there, module otherwise
7412
+ lookup_scope = @builder.select(is_in_cls, owner_cls_val, owner_mod_val, "const_scope")
7413
+ result = @builder.call(@rb_const_get, lookup_scope, const_id, "const_val")
7414
+ else
7415
+ result = @builder.call(@rb_const_get, owner_cls_val, const_id)
7416
+ end
7417
+
7418
+ elsif owner_module
7419
+ # Only a module context (no class), look in module
7420
+ owner_mod_ptr = @builder.global_string_pointer(owner_module)
7421
+ owner_mod_id = @builder.call(@rb_intern, owner_mod_ptr)
7422
+ owner_mod_val = @builder.call(@rb_const_get, rb_cobject_val, owner_mod_id)
7423
+ result = @builder.call(@rb_const_get, owner_mod_val, const_id)
6956
7424
  else
6957
7425
  # Top-level constant
6958
- const_name_ptr = @builder.global_string_pointer(const_name)
6959
- const_id = @builder.call(@rb_intern, const_name_ptr)
6960
- rb_cobject_val = @builder.load2(value_type, @rb_cObject, "rb_cObject")
6961
7426
  result = @builder.call(@rb_const_get, rb_cobject_val, const_id)
6962
7427
  end
6963
7428
  end
@@ -7285,7 +7750,57 @@ module Konpeito
7285
7750
  # Generate try callback function
7286
7751
  @rescue_counter ||= 0
7287
7752
  @rescue_counter += 1
7288
- try_func = generate_rescue_try_callback(inst.try_blocks, @rescue_counter)
7753
+
7754
+ # Rescue callbacks run as separate C functions that only receive a single VALUE
7755
+ # as data. Any local variables (method params, locals) in @variable_allocas are
7756
+ # NOT accessible inside those callbacks unless we pack them into an escape array.
7757
+ # This applies to ALL contexts — not just thread/block callbacks — because the
7758
+ # rescue callback is always a separate LLVM function.
7759
+ #
7760
+ # Native pointer types (NativeArray, NativeClass, ByteBuffer, etc.) cannot be
7761
+ # stored in a Ruby Array, so we skip them. Unboxed types (:i64, :double) are
7762
+ # boxed before pushing and unboxed after unpacking.
7763
+ native_pointer_types = %i[native_array native_class byte_buffer byte_slice
7764
+ native_string slice_int64 slice_float64
7765
+ static_array native_hash]
7766
+ escapable_vars = @variable_allocas.keys.select do |vname|
7767
+ !native_pointer_types.include?(@variable_types[vname])
7768
+ end
7769
+
7770
+ needs_escape = !escapable_vars.empty?
7771
+ escape_var_names = nil
7772
+ rescue_data = nil
7773
+
7774
+ if needs_escape
7775
+ escape_var_names = escapable_vars
7776
+ n = 1 + escape_var_names.size # index-0 = self
7777
+ rescue_esc_ary = @builder.call(@rb_ary_new_capa, LLVM::Int64.from_i(n), "rescue_esc_ary")
7778
+ # Push self (index 0)
7779
+ actual_self = @block_callback_self || get_self_value
7780
+ @builder.call(@rb_ary_push, rescue_esc_ary, actual_self)
7781
+ # Push captured variables (index 1..n), boxing unboxed types
7782
+ escape_var_names.each do |vname|
7783
+ var_type = @variable_types[vname] || :value
7784
+ case var_type
7785
+ when :double
7786
+ raw = @builder.load2(LLVM::Double, @variable_allocas[vname], "esc_load_#{vname}")
7787
+ val = @builder.call(@rb_float_new, raw, "esc_box_#{vname}")
7788
+ when :i64, :i8
7789
+ raw = @builder.load2(LLVM::Int64, @variable_allocas[vname], "esc_load_#{vname}")
7790
+ val = @builder.call(@rb_int2inum, raw, "esc_box_#{vname}")
7791
+ else
7792
+ val = @builder.load2(value_type, @variable_allocas[vname], "esc_load_#{vname}")
7793
+ end
7794
+ @builder.call(@rb_ary_push, rescue_esc_ary, val)
7795
+ end
7796
+ rescue_data = rescue_esc_ary
7797
+ else
7798
+ rescue_data = get_self_value
7799
+ end
7800
+
7801
+ try_func = generate_rescue_try_callback(inst.try_blocks, @rescue_counter,
7802
+ try_hir_blocks: inst.try_hir_blocks,
7803
+ escape_var_names: escape_var_names)
7289
7804
 
7290
7805
  # Collect exception classes
7291
7806
  exception_classes = []
@@ -7299,18 +7814,18 @@ module Konpeito
7299
7814
 
7300
7815
  has_else = inst.else_blocks && !inst.else_blocks.empty?
7301
7816
 
7302
- # If else blocks present, use a global flag to detect if rescue ran
7303
- # A module-level global i32 is set to 1 by the rescue callback
7304
7817
  if has_else
7305
7818
  flag_global = @mod.globals.add(LLVM::Int32, "rescue_else_flag_#{@rescue_counter}")
7306
7819
  flag_global.initializer = LLVM::Int32.from_i(0)
7307
7820
  @builder.store(LLVM::Int32.from_i(0), flag_global)
7308
7821
 
7309
- rescue_func = generate_rescue_handler_with_global_flag_callback(inst.rescue_clauses, @rescue_counter, flag_global)
7310
- args = [try_func, @qnil, rescue_func, @qnil]
7822
+ rescue_func = generate_rescue_handler_with_global_flag_callback(inst.rescue_clauses, @rescue_counter, flag_global,
7823
+ escape_var_names: escape_var_names)
7824
+ args = [try_func, rescue_data, rescue_func, rescue_data]
7311
7825
  else
7312
- rescue_func = generate_rescue_handler_callback(inst.rescue_clauses, @rescue_counter)
7313
- args = [try_func, @qnil, rescue_func, @qnil]
7826
+ rescue_func = generate_rescue_handler_callback(inst.rescue_clauses, @rescue_counter,
7827
+ escape_var_names: escape_var_names)
7828
+ args = [try_func, rescue_data, rescue_func, rescue_data]
7314
7829
  end
7315
7830
 
7316
7831
  args.concat(exception_classes)
@@ -7320,6 +7835,19 @@ module Konpeito
7320
7835
  rescue2_func = declare_rb_rescue2_variadic(exception_classes.size)
7321
7836
  result = @builder.call(rescue2_func, *args)
7322
7837
 
7838
+ # Write-back: after rb_rescue2 returns, the escape array contains updated values
7839
+ # from try/rescue callbacks (written via rb_ary_store in generate_store_local).
7840
+ # Copy them back to the outer function's allocas.
7841
+ if needs_escape && escape_var_names
7842
+ escape_var_names.each_with_index do |vname, idx|
7843
+ next unless @variable_allocas[vname]
7844
+ val = @builder.call(@rb_ary_entry, rescue_data, LLVM::Int64.from_i(idx + 1), "wb_#{vname}")
7845
+ @builder.store(val, @variable_allocas[vname])
7846
+ @variables[vname] = val
7847
+ @variable_types[vname] = :value
7848
+ end
7849
+ end
7850
+
7323
7851
  # Execute else blocks if no exception was raised
7324
7852
  if has_else
7325
7853
  flag_val = @builder.load2(LLVM::Int32, flag_global, "flag_val")
@@ -7396,53 +7924,127 @@ module Konpeito
7396
7924
  end
7397
7925
 
7398
7926
  # Generate try callback function: VALUE func(VALUE data)
7399
- def generate_rescue_try_callback(try_instructions, counter)
7927
+ # When escape_var_names is given, params[0] is a Ruby Array [self, var0, var1, ...].
7928
+ def generate_rescue_try_callback(try_instructions, counter, try_hir_blocks: nil, escape_var_names: nil)
7400
7929
  callback_name = "rescue_try_#{counter}"
7401
7930
 
7402
- # VALUE func(VALUE data)
7931
+ # VALUE func(VALUE data) — data (params[0]) = self or escape array
7403
7932
  callback_func = @mod.functions.add(callback_name, [value_type], value_type)
7404
7933
 
7405
7934
  # Save current builder state
7406
- saved_block = @builder.insert_block
7407
- saved_vars = @variables.dup
7408
- saved_types = @variable_types.dup
7409
- saved_allocas = @variable_allocas.dup
7410
-
7411
- # Create entry block for callback
7412
- entry = callback_func.basic_blocks.append("entry")
7413
- @builder.position_at_end(entry)
7935
+ saved_block = @builder.insert_block
7936
+ saved_vars = @variables.dup
7937
+ saved_types = @variable_types.dup
7938
+ saved_allocas = @variable_allocas.dup
7939
+ saved_cur_func = @current_function
7940
+ saved_return_blocks = @return_blocks
7941
+ saved_block_callback_self = @block_callback_self
7942
+ saved_rescue_escape_array = @rescue_escape_array
7943
+ saved_rescue_escape_indices = @rescue_escape_indices
7414
7944
 
7415
7945
  # Reset variable tracking for callback scope
7416
7946
  @variables = {}
7417
7947
  @variable_types = {}
7418
7948
  @variable_allocas = {}
7419
7949
 
7420
- result = @qnil
7950
+ if try_hir_blocks && !try_hir_blocks.empty?
7951
+ # Structured try body: process HIR BasicBlock list (handles control flow inside try).
7952
+ if escape_var_names
7953
+ # In escape mode, create a dedicated entry block FIRST (LLVM entry = first BB).
7954
+ # It unpacks self + local vars from the escape array, then branches to the first HIR block.
7955
+ escape_entry_bb = callback_func.basic_blocks.append("rescue_escape_entry")
7956
+ else
7957
+ # Normal mode: params[0] is self.
7958
+ @block_callback_self = callback_func.params[0]
7959
+ end
7421
7960
 
7422
- # Generate try instructions
7423
- if try_instructions && !try_instructions.empty?
7424
- try_instructions.each do |inst|
7425
- result = generate_instruction(inst)
7961
+ # Pre-create all LLVM basic blocks so forward references resolve.
7962
+ try_hir_blocks.each do |hb|
7963
+ llvm_bb = callback_func.basic_blocks.append(hb.label)
7964
+ @blocks[hb.label] = llvm_bb
7965
+ end
7966
+ # Set @current_function so generate_store_local creates allocas in the callback.
7967
+ @current_function = callback_func
7968
+ # Populate @return_blocks so generate_phi skips Return-predecessor entries.
7969
+ @return_blocks = Set.new
7970
+ try_hir_blocks.each do |hb|
7971
+ @return_blocks << hb.label if hb.terminator.is_a?(HIR::Return)
7972
+ end
7973
+
7974
+ if escape_var_names
7975
+ # Fill the escape-entry block: unpack array → allocas, then branch to first HIR block.
7976
+ @builder.position_at_end(escape_entry_bb)
7977
+ escape_ary = callback_func.params[0]
7978
+ self_val = @builder.call(@rb_ary_entry, escape_ary, LLVM::Int64.from_i(0), "rescue_esc_self")
7979
+ @block_callback_self = self_val
7980
+ escape_var_names.each_with_index do |vname, i|
7981
+ val = @builder.call(@rb_ary_entry, escape_ary, LLVM::Int64.from_i(i + 1), "rescue_esc_#{vname}")
7982
+ alloca = @builder.alloca(value_type, "rescue_esc_alloca_#{vname}")
7983
+ @builder.store(val, alloca)
7984
+ @variable_allocas[vname] = alloca
7985
+ @variable_types[vname] = :value
7986
+ end
7987
+ # Set escape write-back info so StoreLocal writes to escape array
7988
+ @rescue_escape_array = escape_ary
7989
+ @rescue_escape_indices = {}
7990
+ escape_var_names.each_with_index { |vname, i| @rescue_escape_indices[vname] = i + 1 }
7991
+ @builder.br(@blocks[try_hir_blocks.first.label])
7992
+ end
7993
+
7994
+ sorted = sort_blocks_by_phi_dependencies(try_hir_blocks)
7995
+ sorted.each { |hb| generate_block(callback_func, hb) }
7996
+ else
7997
+ # Flat try body: original behaviour (no control flow in try body).
7998
+ entry = callback_func.basic_blocks.append("entry")
7999
+ @builder.position_at_end(entry)
8000
+
8001
+ if escape_var_names
8002
+ escape_ary = callback_func.params[0]
8003
+ self_val = @builder.call(@rb_ary_entry, escape_ary, LLVM::Int64.from_i(0), "rescue_esc_self")
8004
+ @block_callback_self = self_val
8005
+ escape_var_names.each_with_index do |vname, i|
8006
+ val = @builder.call(@rb_ary_entry, escape_ary, LLVM::Int64.from_i(i + 1), "rescue_esc_#{vname}")
8007
+ alloca = @builder.alloca(value_type, "rescue_esc_alloca_#{vname}")
8008
+ @builder.store(val, alloca)
8009
+ @variable_allocas[vname] = alloca
8010
+ @variable_types[vname] = :value
8011
+ end
8012
+ # Set escape write-back info so StoreLocal writes to escape array
8013
+ @rescue_escape_array = escape_ary
8014
+ @rescue_escape_indices = {}
8015
+ escape_var_names.each_with_index { |vname, i| @rescue_escape_indices[vname] = i + 1 }
8016
+ else
8017
+ @block_callback_self = callback_func.params[0]
8018
+ end
8019
+
8020
+ result = @qnil
8021
+ if try_instructions && !try_instructions.empty?
8022
+ try_instructions.each { |inst| result = generate_instruction(inst) }
7426
8023
  end
7427
- end
7428
8024
 
7429
- # Return the result (or Qnil)
7430
- @builder.ret(result || @qnil)
8025
+ @builder.ret(result || @qnil)
8026
+ end
7431
8027
 
7432
8028
  # Restore builder state
7433
8029
  @builder.position_at_end(saved_block) if saved_block
7434
- @variables = saved_vars
7435
- @variable_types = saved_types
8030
+ @variables = saved_vars
8031
+ @variable_types = saved_types
7436
8032
  @variable_allocas = saved_allocas
8033
+ @current_function = saved_cur_func
8034
+ @return_blocks = saved_return_blocks
8035
+ @block_callback_self = saved_block_callback_self
8036
+ @rescue_escape_array = saved_rescue_escape_array
8037
+ @rescue_escape_indices = saved_rescue_escape_indices
7437
8038
 
7438
8039
  callback_func
7439
8040
  end
7440
8041
 
7441
- # Generate rescue handler callback: VALUE func(VALUE exception, VALUE data)
7442
- def generate_rescue_handler_callback(rescue_clauses, counter)
8042
+ # Generate rescue handler callback: VALUE func(VALUE data2, VALUE exception)
8043
+ # data2 (params[0]) = self or escape array (when escape_var_names is given)
8044
+ def generate_rescue_handler_callback(rescue_clauses, counter, escape_var_names: nil)
7443
8045
  callback_name = "rescue_handler_#{counter}"
7444
8046
 
7445
- # VALUE func(VALUE exception, VALUE data)
8047
+ # VALUE func(VALUE data2, VALUE exception)
7446
8048
  callback_func = @mod.functions.add(callback_name, [value_type, value_type], value_type)
7447
8049
 
7448
8050
  # Save current builder state
@@ -7450,6 +8052,12 @@ module Konpeito
7450
8052
  saved_vars = @variables.dup
7451
8053
  saved_types = @variable_types.dup
7452
8054
  saved_allocas = @variable_allocas.dup
8055
+ saved_block_callback_self = @block_callback_self
8056
+ saved_rescue_escape_array = @rescue_escape_array
8057
+ saved_rescue_escape_indices = @rescue_escape_indices
8058
+
8059
+ # In normal mode: params[0] = self. In escape mode, set @block_callback_self after unpacking.
8060
+ @block_callback_self = callback_func.params[0] unless escape_var_names
7453
8061
 
7454
8062
  # Create entry block for callback
7455
8063
  entry = callback_func.basic_blocks.append("entry")
@@ -7460,10 +8068,30 @@ module Konpeito
7460
8068
  @variable_types = {}
7461
8069
  @variable_allocas = {}
7462
8070
 
7463
- # Get exception parameter
7464
8071
  # CRuby rescue callback: (VALUE data2, VALUE exception)
7465
8072
  exception_val = callback_func.params[1]
7466
8073
 
8074
+ # In escape mode: unpack self and local variables from the escape array (params[0]).
8075
+ # escape_allocas_map holds the allocas so we can restore them after each body-block reset.
8076
+ escape_allocas_map = {}
8077
+ if escape_var_names
8078
+ escape_ary = callback_func.params[0]
8079
+ self_val = @builder.call(@rb_ary_entry, escape_ary, LLVM::Int64.from_i(0), "rescue_esc_self")
8080
+ @block_callback_self = self_val
8081
+ escape_var_names.each_with_index do |vname, i|
8082
+ val = @builder.call(@rb_ary_entry, escape_ary, LLVM::Int64.from_i(i + 1), "rescue_esc_#{vname}")
8083
+ alloca = @builder.alloca(value_type, "rescue_esc_alloca_#{vname}")
8084
+ @builder.store(val, alloca)
8085
+ @variable_allocas[vname] = alloca
8086
+ @variable_types[vname] = :value
8087
+ escape_allocas_map[vname] = alloca
8088
+ end
8089
+ # Set escape write-back info so StoreLocal writes to escape array
8090
+ @rescue_escape_array = escape_ary
8091
+ @rescue_escape_indices = {}
8092
+ escape_var_names.each_with_index { |vname, i| @rescue_escape_indices[vname] = i + 1 }
8093
+ end
8094
+
7467
8095
  # Match exception class to find correct handler
7468
8096
  if rescue_clauses && !rescue_clauses.empty?
7469
8097
  # Create blocks for each rescue clause and a fallback
@@ -7523,6 +8151,12 @@ module Konpeito
7523
8151
  @variable_types = {}
7524
8152
  @variable_allocas = {}
7525
8153
 
8154
+ # In escape mode, restore captured variable allocas (created in entry block).
8155
+ escape_allocas_map.each do |vname, alloca|
8156
+ @variable_allocas[vname] = alloca
8157
+ @variable_types[vname] = :value
8158
+ end
8159
+
7526
8160
  # Bind exception variable if specified
7527
8161
  if clause.exception_var
7528
8162
  @variables[clause.exception_var] = exception_val
@@ -7559,20 +8193,30 @@ module Konpeito
7559
8193
  @variables = saved_vars
7560
8194
  @variable_types = saved_types
7561
8195
  @variable_allocas = saved_allocas
8196
+ @block_callback_self = saved_block_callback_self
8197
+ @rescue_escape_array = saved_rescue_escape_array
8198
+ @rescue_escape_indices = saved_rescue_escape_indices
7562
8199
 
7563
8200
  callback_func
7564
8201
  end
7565
8202
 
7566
8203
  # Generate rescue handler callback that sets a global flag when called
7567
8204
  # The flag_global is an LLVM global variable that is set to 1 when rescue is invoked
7568
- def generate_rescue_handler_with_global_flag_callback(rescue_clauses, counter, flag_global)
8205
+ def generate_rescue_handler_with_global_flag_callback(rescue_clauses, counter, flag_global, escape_var_names: nil)
7569
8206
  callback_name = "rescue_handler_gflag_#{counter}"
8207
+ # VALUE func(VALUE data2, VALUE exception) — data2 (params[0]) = self or escape array
7570
8208
  callback_func = @mod.functions.add(callback_name, [value_type, value_type], value_type)
7571
8209
 
7572
8210
  saved_block = @builder.insert_block
7573
8211
  saved_vars = @variables.dup
7574
8212
  saved_types = @variable_types.dup
7575
8213
  saved_allocas = @variable_allocas.dup
8214
+ saved_block_callback_self = @block_callback_self
8215
+ saved_rescue_escape_array = @rescue_escape_array
8216
+ saved_rescue_escape_indices = @rescue_escape_indices
8217
+
8218
+ # In normal mode: params[0] = self. In escape mode, set @block_callback_self after unpacking.
8219
+ @block_callback_self = callback_func.params[0] unless escape_var_names
7576
8220
 
7577
8221
  entry = callback_func.basic_blocks.append("entry")
7578
8222
  @builder.position_at_end(entry)
@@ -7587,6 +8231,26 @@ module Konpeito
7587
8231
  # Set the global flag to 1 (rescue was invoked)
7588
8232
  @builder.store(LLVM::Int32.from_i(1), flag_global)
7589
8233
 
8234
+ # In escape mode: unpack self and local variables from the escape array.
8235
+ escape_allocas_map = {}
8236
+ if escape_var_names
8237
+ escape_ary = callback_func.params[0]
8238
+ self_val = @builder.call(@rb_ary_entry, escape_ary, LLVM::Int64.from_i(0), "rescue_esc_self")
8239
+ @block_callback_self = self_val
8240
+ escape_var_names.each_with_index do |vname, i|
8241
+ val = @builder.call(@rb_ary_entry, escape_ary, LLVM::Int64.from_i(i + 1), "rescue_esc_#{vname}")
8242
+ alloca = @builder.alloca(value_type, "rescue_esc_alloca_#{vname}")
8243
+ @builder.store(val, alloca)
8244
+ @variable_allocas[vname] = alloca
8245
+ @variable_types[vname] = :value
8246
+ escape_allocas_map[vname] = alloca
8247
+ end
8248
+ # Set escape write-back info so StoreLocal writes to escape array
8249
+ @rescue_escape_array = escape_ary
8250
+ @rescue_escape_indices = {}
8251
+ escape_var_names.each_with_index { |vname, i| @rescue_escape_indices[vname] = i + 1 }
8252
+ end
8253
+
7590
8254
  # Same rescue matching logic as generate_rescue_handler_callback
7591
8255
  if rescue_clauses && !rescue_clauses.empty?
7592
8256
  clause_blocks = rescue_clauses.map.with_index do |_, i|
@@ -7632,6 +8296,12 @@ module Konpeito
7632
8296
  @variable_types = {}
7633
8297
  @variable_allocas = {}
7634
8298
 
8299
+ # In escape mode, restore captured variable allocas after the reset.
8300
+ escape_allocas_map.each do |vname, alloca|
8301
+ @variable_allocas[vname] = alloca
8302
+ @variable_types[vname] = :value
8303
+ end
8304
+
7635
8305
  if clause.exception_var
7636
8306
  @variables[clause.exception_var] = exception_val
7637
8307
  @variable_types[clause.exception_var] = :value
@@ -7661,6 +8331,9 @@ module Konpeito
7661
8331
  @variables = saved_vars
7662
8332
  @variable_types = saved_types
7663
8333
  @variable_allocas = saved_allocas
8334
+ @block_callback_self = saved_block_callback_self
8335
+ @rescue_escape_array = saved_rescue_escape_array
8336
+ @rescue_escape_indices = saved_rescue_escape_indices
7664
8337
 
7665
8338
  callback_func
7666
8339
  end
@@ -7892,6 +8565,27 @@ module Konpeito
7892
8565
  result
7893
8566
  end
7894
8567
 
8568
+ # Generate a CaseEqualityCheck: condition === predicate (returns VALUE Qtrue/Qfalse)
8569
+ # Used by the new HIR-block-based case/when dispatch
8570
+ def generate_case_equality_check(inst)
8571
+ cond_val = get_value_as_ruby(inst.condition)
8572
+
8573
+ result = if inst.predicate.nil?
8574
+ # No predicate: treat condition itself as Ruby truthy value
8575
+ cond_val
8576
+ else
8577
+ pred_val = get_value_as_ruby(inst.predicate)
8578
+ # call_case_equality returns VALUE (Qtrue or Qfalse)
8579
+ call_case_equality(cond_val, pred_val, receiver_hir: inst.condition)
8580
+ end
8581
+
8582
+ if inst.result_var
8583
+ @variables[inst.result_var] = result
8584
+ @variable_types[inst.result_var] = :value
8585
+ end
8586
+ result
8587
+ end
8588
+
7895
8589
  # Call === (case equality) on receiver with argument
7896
8590
  # Call case equality with optimized inline comparison for basic types
7897
8591
  def call_case_equality(receiver, arg, receiver_hir: nil)
@@ -7950,8 +8644,8 @@ module Konpeito
7950
8644
  def inline_class_case_eq(klass, arg)
7951
8645
  # Use rb_obj_is_kind_of(arg, klass) which returns int (0 or non-zero)
7952
8646
  result_int = @builder.call(@rb_obj_is_kind_of, arg, klass)
7953
- # Convert int to VALUE
7954
- is_true = @builder.icmp(:ne, result_int, LLVM::Int32.from_i(0))
8647
+ # rb_obj_is_kind_of returns VALUE (i64); compare with i64 0 (Qfalse)
8648
+ is_true = @builder.icmp(:ne, result_int, LLVM::Int64.from_i(0))
7955
8649
  @builder.select(is_true, @qtrue, @qfalse)
7956
8650
  end
7957
8651
 
@@ -8638,11 +9332,19 @@ module Konpeito
8638
9332
  end
8639
9333
 
8640
9334
  def generate_phi(inst)
9335
+ # If this phi was already generated (e.g., it lives in an earlier HIR block but
9336
+ # also appears in a when_body or inline-rescue try_blocks), return the cached value.
9337
+ if inst.result_var && @variables.key?(inst.result_var)
9338
+ return @variables[inst.result_var]
9339
+ end
9340
+
8641
9341
  # Collect incoming values with their type tags for optimization
8642
9342
  incoming_data = []
8643
9343
 
8644
9344
  inst.incoming.each do |label, hir_value|
8645
- llvm_block = @blocks[label]
9345
+ # If safe navigation inside this HIR block created additional LLVM blocks,
9346
+ # use the actual exit block instead of the original HIR block's LLVM block.
9347
+ llvm_block = @block_exit_overrides[label] || @blocks[label]
8646
9348
  next unless llvm_block
8647
9349
  # Skip blocks with Return terminators — they don't branch to the
8648
9350
  # merge block, so they cannot be predecessors in the phi node.
@@ -10118,6 +10820,14 @@ module Konpeito
10118
10820
  end
10119
10821
 
10120
10822
  # Declare memchr if not already declared
10823
+ def declare_strlen
10824
+ @strlen ||= @mod.functions["strlen"] || @mod.functions.add(
10825
+ "strlen",
10826
+ [LLVM::Pointer(LLVM::Int8)],
10827
+ LLVM::Int64
10828
+ )
10829
+ end
10830
+
10121
10831
  def declare_memchr
10122
10832
  @memchr ||= @mod.functions["memchr"] || @mod.functions.add(
10123
10833
  "memchr",
@@ -11131,22 +11841,24 @@ module Konpeito
11131
11841
  # Get C string pointer
11132
11842
  data_ptr = @builder.call(@rb_string_value_cstr, value_ptr, "data_ptr")
11133
11843
 
11134
- # Get byte length using rb_str_length (returns Fixnum)
11135
- len_value = @builder.call(@rb_str_length, string_value, "str_len")
11136
- byte_len = @builder.call(@rb_num2long, len_value, "byte_len")
11844
+ # Get byte length using strlen on the C string
11845
+ # (rb_str_length returns character count, not byte count)
11846
+ declare_strlen
11847
+ byte_len = @builder.call(@strlen, data_ptr, "byte_len")
11137
11848
 
11138
- # Check ASCII-only using Ruby's ascii_only? method
11139
- ascii_only_id = @builder.call(@rb_intern, @builder.global_string_pointer("ascii_only?"))
11140
- ascii_result = @builder.call(@rb_funcallv, string_value, ascii_only_id,
11141
- LLVM::Int32.from_i(0), LLVM::Pointer(LLVM::Int64).null_pointer, "ascii_result")
11849
+ # Check ASCII-only using rb_enc_str_asciionly_p (avoids rb_funcallv method lookup)
11850
+ rb_enc_str_asciionly_p = @mod.functions["rb_enc_str_asciionly_p"] || @mod.functions.add(
11851
+ "rb_enc_str_asciionly_p", [value_type], LLVM::Int32
11852
+ )
11853
+ ascii_int = @builder.call(rb_enc_str_asciionly_p, string_value, "ascii_int")
11854
+ # rb_enc_str_asciionly_p returns non-zero for ASCII-only
11855
+ ascii_result = @builder.icmp(:ne, ascii_int, LLVM::Int32.from_i(0), "is_ascii_raw")
11142
11856
 
11143
- # Convert Ruby boolean to flags (Qtrue = 20, Qfalse = 0)
11144
11857
  # flags bit 0 = ASCII_ONLY
11145
- is_ascii_true = @builder.icmp(:eq, ascii_result, @qtrue, "is_ascii_true")
11146
- flags_val = @builder.select(is_ascii_true, LLVM::Int64.from_i(1), LLVM::Int64.from_i(0), "flags_val")
11858
+ flags_val = @builder.select(ascii_result, LLVM::Int64.from_i(1), LLVM::Int64.from_i(0), "flags_val")
11147
11859
 
11148
11860
  # For ASCII strings, char_len = byte_len; otherwise -1 (not computed)
11149
- char_len = @builder.select(is_ascii_true, byte_len, LLVM::Int64.from_i(-1), "char_len")
11861
+ char_len = @builder.select(ascii_result, byte_len, LLVM::Int64.from_i(-1), "char_len")
11150
11862
 
11151
11863
  # Allocate struct on stack
11152
11864
  ns_ptr = @builder.alloca(struct_type, "native_string")