konpeito 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/lib/konpeito/codegen/cruby_backend.rb +7 -0
  4. data/lib/konpeito/codegen/jvm_generator.rb +160 -40
  5. data/lib/konpeito/codegen/llvm_generator.rb +236 -58
  6. data/lib/konpeito/hir/builder.rb +61 -27
  7. data/lib/konpeito/hir/nodes.rb +2 -0
  8. data/lib/konpeito/version.rb +1 -1
  9. data/tools/konpeito-asm/build.sh +1 -0
  10. data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +12 -0
  11. data/tools/konpeito-asm/src/konpeito/runtime/KMatchData.java +50 -1
  12. data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +721 -8
  13. metadata +1 -20
  14. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
  15. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
  16. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
  17. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
  18. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFiber.class +0 -0
  19. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
  20. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
  21. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
  22. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
  23. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
  24. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMatchData.class +0 -0
  25. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
  26. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
  27. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
  28. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRubyException.class +0 -0
  29. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
  30. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
  31. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
  32. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
@@ -155,6 +155,16 @@ module Konpeito
155
155
  end
156
156
 
157
157
  # Declare a native function signature
158
+ # Infer native param type symbol from HIR param type (used when no RBS method sig is available,
159
+ # e.g. for monomorphized functions like dot_Vec2 whose name has no matching method entry).
160
+ def native_param_type_sym_from_hir(param)
161
+ 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
164
+ # Everything else (NativeClass, unknown) → use pointer (else branch in caller)
165
+ :OtherNativeClass
166
+ end
167
+
158
168
  def declare_native_function(hir_func, native_class_type)
159
169
  struct_type = get_or_create_native_class_struct(native_class_type)
160
170
  method_sig = native_class_type.methods[hir_func.name.to_sym]
@@ -163,11 +173,14 @@ module Konpeito
163
173
  param_types = [LLVM::Pointer(struct_type)]
164
174
 
165
175
  hir_func.params.each_with_index do |param, i|
166
- param_type_sym = method_sig&.param_types&.[](i) || :Float64
176
+ # Prefer RBS-derived type; fall back to HIR param.type for monomorphized functions
177
+ # (monomorphized funcs like dot_Vec2 have no matching method_sig entry)
178
+ param_type_sym = method_sig&.param_types&.[](i) ||
179
+ native_param_type_sym_from_hir(param)
167
180
  llvm_type = case param_type_sym
168
181
  when :Int64 then LLVM::Int64
169
182
  when :Float64 then LLVM::Double
170
- else LLVM::Pointer(LLVM::Int8) # Other NativeClass
183
+ else LLVM::Pointer(LLVM::Int8) # Other NativeClass or value type
171
184
  end
172
185
  param_types << llvm_type
173
186
  end
@@ -639,6 +652,10 @@ module Konpeito
639
652
  # void rb_define_method(VALUE klass, const char *name, VALUE (*func)(...), int argc)
640
653
  @rb_define_method = @mod.functions.add("rb_define_method", [value_type, ptr_type, ptr_type, LLVM::Int32], LLVM.Void)
641
654
 
655
+ # void rb_define_singleton_method(VALUE obj, const char *name, VALUE (*func)(...), int argc)
656
+ @rb_define_singleton_method = @mod.functions.add("rb_define_singleton_method",
657
+ [value_type, ptr_type, ptr_type, LLVM::Int32], LLVM.Void)
658
+
642
659
  # Instance variable functions
643
660
  # VALUE rb_ivar_get(VALUE obj, ID id)
644
661
  @rb_ivar_get = @mod.functions.add("rb_ivar_get", [value_type, id_type], value_type)
@@ -1050,7 +1067,8 @@ module Konpeito
1050
1067
  # Store parameters (already unboxed)
1051
1068
  hir_func.params.each_with_index do |param, i|
1052
1069
  param_value = func.params[i + 1] # +1 to skip self
1053
- param_type_sym = method_sig&.param_types&.[](i) || :Float64
1070
+ param_type_sym = method_sig&.param_types&.[](i) ||
1071
+ native_param_type_sym_from_hir(param)
1054
1072
 
1055
1073
  # Determine type tag based on parameter type
1056
1074
  type_tag = case param_type_sym
@@ -2057,9 +2075,10 @@ module Konpeito
2057
2075
  end
2058
2076
 
2059
2077
  def generate_string_lit(inst)
2060
- # Create global string constant
2078
+ # Create global string constant with UTF-8 encoding (Ruby default for string literals)
2061
2079
  str_ptr = @builder.global_string_pointer(inst.value)
2062
- ruby_str = @builder.call(@rb_str_new_cstr, str_ptr)
2080
+ len = LLVM::Int64.from_i(inst.value.bytesize)
2081
+ ruby_str = @builder.call(@rb_utf8_str_new, str_ptr, len)
2063
2082
  @variables[inst.result_var] = ruby_str if inst.result_var
2064
2083
  ruby_str
2065
2084
  end
@@ -4354,7 +4373,9 @@ module Konpeito
4354
4373
 
4355
4374
  # Generate a callback function for rb_block_call
4356
4375
  # Signature: VALUE func(VALUE yielded_arg, VALUE data2, int argc, VALUE *argv, VALUE blockarg)
4357
- def generate_block_callback(block_def, method_name, captures = [], capture_types = {})
4376
+ # escape_cells_mode: when true, data2 is a Ruby Array VALUE (GC-safe heap)
4377
+ # used for procs created inside block callbacks (nested lambda capture safety)
4378
+ def generate_block_callback(block_def, method_name, captures = [], capture_types = {}, escape_cells_mode: false)
4358
4379
  return nil unless block_def
4359
4380
 
4360
4381
  # Create a unique name for the callback
@@ -4373,35 +4394,59 @@ module Konpeito
4373
4394
  saved_vars = @variables.dup
4374
4395
  saved_types = @variable_types.dup
4375
4396
  saved_allocas = @variable_allocas.dup
4397
+ saved_in_block_callback = @in_block_callback
4376
4398
 
4377
4399
  # Create entry block for callback
4378
4400
  entry = callback_func.basic_blocks.append("entry")
4379
4401
  @builder.position_at_end(entry)
4380
4402
 
4381
- # Reset variable tracking for callback scope
4403
+ # Reset variable tracking for callback scope.
4404
+ # Set @in_block_callback so nested proc creation uses GC-safe escape-cells mode.
4405
+ @in_block_callback = true
4382
4406
  @variables = {}
4383
4407
  @variable_types = {}
4384
4408
  @variable_allocas = {}
4385
4409
 
4386
4410
  # Setup captured variable access through data2 pointer
4387
- # data2 is a pointer to array of VALUE* (pointers to captured variables)
4388
4411
  unless captures.empty?
4389
- ptr_type = LLVM::Pointer(value_type)
4390
- array_type = LLVM::Array(ptr_type, captures.size)
4391
- captures_ptr = @builder.int2ptr(callback_func.params[1],
4392
- LLVM::Pointer(array_type), "captures_ptr")
4393
-
4394
- captures.each_with_index do |capture, i|
4395
- # Get pointer to the pointer (VALUE**)
4396
- elem_ptr_ptr = @builder.gep2(array_type, captures_ptr,
4397
- [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)], "cap_#{capture.name}_ptr_ptr")
4398
- # Load the pointer to the variable (VALUE*)
4399
- elem_ptr = @builder.load2(ptr_type, elem_ptr_ptr, "cap_#{capture.name}_ptr")
4400
-
4401
- # Store in variable_allocas so LoadLocal/StoreLocal can use it
4402
- @variable_allocas[capture.name] = elem_ptr
4403
- # Preserve the original type from outer scope
4404
- @variable_types[capture.name] = capture_types[capture.name] || :value
4412
+ if escape_cells_mode
4413
+ # data2 is a Ruby Array VALUE (GC-safe heap snapshot).
4414
+ # Read each capture via rb_ary_entry(data2, i) and store in a local alloca.
4415
+ # This mode is used for procs created inside block callbacks to avoid
4416
+ # dangling stack pointers when the outer callback returns.
4417
+ ary_val = callback_func.params[1] # already VALUE (Ruby Array)
4418
+ captures.each_with_index do |capture, i|
4419
+ val = @builder.call(@rb_ary_entry, ary_val,
4420
+ LLVM::Int64.from_i(i), "esc_#{capture.name}")
4421
+ # Store in an alloca so LoadLocal/StoreLocal can use it normally.
4422
+ # Writes to this alloca stay local (no back-write to the GC array),
4423
+ # which is correct for snapshots from a returned outer scope.
4424
+ cap_alloca = @builder.alloca(value_type, "esc_alloca_#{capture.name}")
4425
+ @builder.store(val, cap_alloca)
4426
+ @variable_allocas[capture.name] = cap_alloca
4427
+ @variable_types[capture.name] = capture_types[capture.name] || :value
4428
+ end
4429
+ else
4430
+ # data2 is a pointer to array of VALUE* (pointers to captured variables).
4431
+ # Used for procs created in method scope — the outer stack stays alive
4432
+ # for the proc's lifetime, so dangling is not a concern.
4433
+ ptr_type = LLVM::Pointer(value_type)
4434
+ array_type = LLVM::Array(ptr_type, captures.size)
4435
+ captures_ptr = @builder.int2ptr(callback_func.params[1],
4436
+ LLVM::Pointer(array_type), "captures_ptr")
4437
+
4438
+ captures.each_with_index do |capture, i|
4439
+ # Get pointer to the pointer (VALUE**)
4440
+ elem_ptr_ptr = @builder.gep2(array_type, captures_ptr,
4441
+ [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)], "cap_#{capture.name}_ptr_ptr")
4442
+ # Load the pointer to the variable (VALUE*)
4443
+ elem_ptr = @builder.load2(ptr_type, elem_ptr_ptr, "cap_#{capture.name}_ptr")
4444
+
4445
+ # Store in variable_allocas so LoadLocal/StoreLocal can use it
4446
+ @variable_allocas[capture.name] = elem_ptr
4447
+ # Preserve the original type from outer scope
4448
+ @variable_types[capture.name] = capture_types[capture.name] || :value
4449
+ end
4405
4450
  end
4406
4451
  end
4407
4452
 
@@ -4619,6 +4664,7 @@ module Konpeito
4619
4664
  @variables = saved_vars
4620
4665
  @variable_types = saved_types
4621
4666
  @variable_allocas = saved_allocas
4667
+ @in_block_callback = saved_in_block_callback
4622
4668
 
4623
4669
  callback_func
4624
4670
  end
@@ -4635,27 +4681,57 @@ module Konpeito
4635
4681
  capture_types[cap.name] = @variable_types[cap.name]
4636
4682
  end
4637
4683
 
4638
- # Create the callback function (same as block callback)
4639
- callback_func = generate_block_callback(block_def, "proc", captures, capture_types)
4684
+ # Choose capture strategy based on whether we are inside a block callback.
4685
+ #
4686
+ # When @in_block_callback is true (i.e. this proc is being created inside
4687
+ # another lambda/block callback), the outer callback's stack will be freed
4688
+ # before this inner proc is ever called. Using pointer-to-stack-alloca would
4689
+ # leave dangling pointers. Instead we snapshot the current VALUES into a
4690
+ # GC-tracked Ruby Array so they survive after the outer callback returns.
4691
+ #
4692
+ # When @in_block_callback is false (method scope), the outer stack stays
4693
+ # alive for the proc's lifetime, so we use the pointer-to-alloca approach
4694
+ # which also supports cross-scope mutation (e.g. `x = 0; inc = -> { x += 1 }`).
4695
+ use_escape_cells = @in_block_callback && !captures.empty?
4640
4696
 
4641
- # Setup captures data if there are any
4697
+ # Create the callback function
4698
+ callback_func = generate_block_callback(block_def, "proc", captures, capture_types,
4699
+ escape_cells_mode: use_escape_cells)
4700
+
4701
+ # Setup captures data
4642
4702
  if captures.empty?
4643
4703
  captures_data = LLVM::Int64.from_i(0) # NULL for no captures
4704
+ elsif use_escape_cells
4705
+ # Build a Ruby Array holding a snapshot of each captured VALUE.
4706
+ # data2 in the callback receives this Array VALUE directly — the GC
4707
+ # keeps it (and the stored VALUEs) alive regardless of stack lifetime.
4708
+ esc_ary = @builder.call(@rb_ary_new_capa, LLVM::Int64.from_i(captures.size),
4709
+ "esc_ary")
4710
+ captures.each do |capture|
4711
+ # Load current VALUE from the alloca (or use 0/Qnil if missing)
4712
+ val = if @variable_allocas[capture.name]
4713
+ @builder.load2(value_type, @variable_allocas[capture.name],
4714
+ "esc_load_#{capture.name}")
4715
+ else
4716
+ @qnil
4717
+ end
4718
+ @builder.call(@rb_ary_push, esc_ary, val)
4719
+ end
4720
+ # Pass the Ruby Array VALUE directly as data2 (cast to i64 / VALUE)
4721
+ captures_data = esc_ary
4644
4722
  else
4645
- # Allocate array of pointers to captured variables
4723
+ # Allocate array of pointers to captured variables (on stack — safe for
4724
+ # method-scope procs because the method outlives the proc calls).
4646
4725
  ptr_type = LLVM::Pointer(value_type)
4647
4726
  array_type = LLVM::Array(ptr_type, captures.size)
4648
4727
  captures_array = @builder.alloca(array_type, "proc_captures")
4649
4728
 
4650
4729
  captures.each_with_index do |capture, i|
4651
- # Get the alloca for this captured variable
4652
4730
  alloca = @variable_allocas[capture.name]
4731
+ ptr = @builder.gep(captures_array, [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)])
4653
4732
  if alloca
4654
- ptr = @builder.gep(captures_array, [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)])
4655
4733
  @builder.store(alloca, ptr)
4656
4734
  else
4657
- # Variable not found, store null
4658
- ptr = @builder.gep(captures_array, [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)])
4659
4735
  @builder.store(LLVM::Pointer(value_type).null, ptr)
4660
4736
  end
4661
4737
  end
@@ -4666,6 +4742,11 @@ module Konpeito
4666
4742
  # Call rb_proc_new to create the Proc object
4667
4743
  result = @builder.call(@rb_proc_new, callback_func, captures_data)
4668
4744
 
4745
+ # Override lambda? and arity on the Proc singleton to match lambda semantics.
4746
+ # rb_proc_new always creates a regular Proc (lambda?=false, arity=-1).
4747
+ # We define singleton methods so the proc behaves like a lambda.
4748
+ attach_lambda_singleton_methods(result, block_def.params.size)
4749
+
4669
4750
  if inst.result_var
4670
4751
  @variables[inst.result_var] = result
4671
4752
  @variable_types[inst.result_var] = :value
@@ -4674,6 +4755,44 @@ module Konpeito
4674
4755
  result
4675
4756
  end
4676
4757
 
4758
+ # Attach lambda? and arity singleton methods to a Proc created via rb_proc_new.
4759
+ # This makes the proc behave like a true Ruby lambda for introspection purposes.
4760
+ def attach_lambda_singleton_methods(proc_val, param_count)
4761
+ saved_block = @builder.insert_block
4762
+
4763
+ # --- Shared lambda_true helper (created once per module) ---
4764
+ @lambda_true_func ||= begin
4765
+ f = @mod.functions.add("__konpeito_lambda_true", [value_type], value_type)
4766
+ bb = f.basic_blocks.append("entry")
4767
+ @builder.position_at_end(bb)
4768
+ @builder.ret(@qtrue)
4769
+ f
4770
+ end
4771
+
4772
+ # --- Per-arity helper (arity_N) ---
4773
+ arity_func_name = "__konpeito_proc_arity_#{param_count}"
4774
+ arity_func = @mod.functions[arity_func_name] || begin
4775
+ f = @mod.functions.add(arity_func_name, [value_type], value_type)
4776
+ bb = f.basic_blocks.append("entry")
4777
+ @builder.position_at_end(bb)
4778
+ arity_ruby = @builder.call(@rb_int2inum, LLVM::Int64.from_i(param_count), "arity_val")
4779
+ @builder.ret(arity_ruby)
4780
+ f
4781
+ end
4782
+
4783
+ @builder.position_at_end(saved_block)
4784
+
4785
+ # rb_define_singleton_method(obj, name, func, argc)
4786
+ # argc=0 means the Ruby method takes no arguments (VALUE self only in C)
4787
+ name_lambda_q = @builder.global_string_pointer("lambda?", "str_lambda_q")
4788
+ name_arity = @builder.global_string_pointer("arity", "str_arity")
4789
+
4790
+ @builder.call(@rb_define_singleton_method, proc_val, name_lambda_q,
4791
+ @lambda_true_func, LLVM::Int32.from_i(0))
4792
+ @builder.call(@rb_define_singleton_method, proc_val, name_arity,
4793
+ arity_func, LLVM::Int32.from_i(0))
4794
+ end
4795
+
4677
4796
  # Generate a call to a Proc object (from ProcCall HIR instruction)
4678
4797
  def generate_proc_call(inst)
4679
4798
  # Get the Proc value
@@ -4998,31 +5117,85 @@ module Konpeito
4998
5117
  end
4999
5118
  end
5000
5119
 
5120
+ # Pre-allocate allocas for block-local variables
5121
+ block_local_vars = collect_local_variables_in_block(block_def)
5122
+ capture_names = captures.map(&:name)
5123
+ block_local_vars.each do |var_name, var_type|
5124
+ next if capture_names.include?(var_name)
5125
+ next if @variable_allocas[var_name]
5126
+ llvm_type, type_tag = llvm_type_for_ruby_type(var_type)
5127
+ alloca = @builder.alloca(llvm_type, "blk_#{var_name}")
5128
+ @variable_allocas[var_name] = alloca
5129
+ @variable_types[var_name] = type_tag
5130
+ end
5131
+
5001
5132
  # Compile block body
5002
- result = @qnil
5003
- result_type = :value
5004
- last_inst = nil
5005
- block_def.body.each do |basic_block|
5006
- basic_block.instructions.each do |hir_inst|
5007
- result = generate_instruction(hir_inst)
5008
- last_inst = hir_inst
5133
+ if block_def.body.size > 1
5134
+ # Multi-block body (contains while/if control flow)
5135
+ saved_blocks = @blocks.dup
5136
+ saved_return_blocks = @return_blocks
5137
+ saved_loop_stack = @loop_stack
5138
+
5139
+ @return_blocks = Set.new
5140
+ @loop_stack = [] if @loop_stack.nil?
5141
+
5142
+ block_def.body.each do |hir_block|
5143
+ llvm_block = callback_func.basic_blocks.append(hir_block.label)
5144
+ @blocks[hir_block.label] = llvm_block
5009
5145
  end
5010
- end
5011
5146
 
5012
- # Determine result type from last instruction
5013
- if last_inst.is_a?(HIR::StoreLocal)
5014
- var_name = last_inst.var.name
5015
- result_type = @variable_types[var_name] || :value
5016
- elsif last_inst && last_inst.respond_to?(:result_var) && last_inst.result_var
5017
- result_type = @variable_types[last_inst.result_var] || :value
5018
- end
5147
+ block_def.body.each do |hir_block|
5148
+ @return_blocks << hir_block.label if hir_block.terminator.is_a?(HIR::Return)
5149
+ end
5019
5150
 
5020
- # Return the result, converting to VALUE if necessary
5021
- result = @qnil if result.nil?
5022
- if result.is_a?(LLVM::Value) && result_type != :value
5023
- result = convert_value(result, result_type, :value)
5151
+ # Jump from entry to first HIR block
5152
+ @builder.br(@blocks[block_def.body.first.label])
5153
+
5154
+ # Generate each block (instructions + terminators)
5155
+ block_def.body.each do |hir_block|
5156
+ generate_block(callback_func, hir_block)
5157
+ end
5158
+
5159
+ # Ensure all blocks have terminators
5160
+ callback_func.basic_blocks.each do |llvm_block|
5161
+ next if llvm_block.name == "entry"
5162
+ last_instr = llvm_block.instructions.last
5163
+ has_terminator = last_instr && (last_instr.opcode == :ret || last_instr.opcode == :br ||
5164
+ last_instr.opcode == :cond_br || last_instr.opcode == :switch ||
5165
+ last_instr.opcode == :unreachable)
5166
+ unless has_terminator
5167
+ @builder.position_at_end(llvm_block)
5168
+ @builder.ret(last_instr || @qnil)
5169
+ end
5170
+ end
5171
+
5172
+ @blocks = saved_blocks
5173
+ @return_blocks = saved_return_blocks
5174
+ @loop_stack = saved_loop_stack
5175
+ else
5176
+ result = @qnil
5177
+ result_type = :value
5178
+ last_inst = nil
5179
+ block_def.body.each do |basic_block|
5180
+ basic_block.instructions.each do |hir_inst|
5181
+ result = generate_instruction(hir_inst)
5182
+ last_inst = hir_inst
5183
+ end
5184
+ end
5185
+
5186
+ if last_inst.is_a?(HIR::StoreLocal)
5187
+ var_name = last_inst.var.name
5188
+ result_type = @variable_types[var_name] || :value
5189
+ elsif last_inst && last_inst.respond_to?(:result_var) && last_inst.result_var
5190
+ result_type = @variable_types[last_inst.result_var] || :value
5191
+ end
5192
+
5193
+ result = @qnil if result.nil?
5194
+ if result.is_a?(LLVM::Value) && result_type != :value
5195
+ result = convert_value(result, result_type, :value)
5196
+ end
5197
+ @builder.ret(result.is_a?(LLVM::Value) ? result : @qnil)
5024
5198
  end
5025
- @builder.ret(result.is_a?(LLVM::Value) ? result : @qnil)
5026
5199
 
5027
5200
  # Restore builder state
5028
5201
  @builder.position_at_end(saved_block) if saved_block
@@ -6584,7 +6757,11 @@ module Konpeito
6584
6757
  # Set type tag if this is a native method call
6585
6758
  # Check if we're calling a method on self that's a NativeClass method
6586
6759
  if @current_native_class && inst.result_var
6587
- method_sig = @current_native_class.methods[func_name.to_sym]
6760
+ # Try both the (possibly monomorphized) func_name and the original method name.
6761
+ # Monomorphized names like "rn_Vec2_dot_Vec2" won't match the methods hash key
6762
+ # ":dot", so fall back to inst.method_name to find the right method signature.
6763
+ method_sig = @current_native_class.methods[func_name.to_sym] ||
6764
+ @current_native_class.methods[inst.method_name.to_sym]
6588
6765
  if method_sig
6589
6766
  result_type_tag = case method_sig.return_type
6590
6767
  when :Int64 then :i64
@@ -6989,17 +7166,18 @@ module Konpeito
6989
7166
  end
6990
7167
  result = @builder.phi(value_type, phi_incoming)
6991
7168
  when :method
6992
- # Use rb_respond_to to check if method exists
6993
- rb_respond_to = @mod.functions["rb_respond_to"] || @mod.functions.add(
6994
- "rb_respond_to",
6995
- [value_type, value_type],
7169
+ # Use rb_obj_respond_to with include_private=1 to check if method exists
7170
+ # (rb_respond_to only checks public methods, but defined? should find private methods too)
7171
+ rb_obj_respond_to = @mod.functions["rb_obj_respond_to"] || @mod.functions.add(
7172
+ "rb_obj_respond_to",
7173
+ [value_type, value_type, LLVM::Int32],
6996
7174
  LLVM::Int32
6997
7175
  )
6998
7176
  # Check on main object (self)
6999
7177
  self_val = @variables["self"] || @builder.load2(value_type, @rb_cObject, "rb_cObject_self")
7000
7178
  name_ptr = @builder.global_string_pointer(inst.name)
7001
7179
  name_id = @builder.call(@rb_intern, name_ptr)
7002
- is_defined = @builder.call(rb_respond_to, self_val, name_id)
7180
+ is_defined = @builder.call(rb_obj_respond_to, self_val, name_id, LLVM::Int32.from_i(1))
7003
7181
  is_true = @builder.icmp(:ne, is_defined, LLVM::Int32.from_i(0))
7004
7182
 
7005
7183
  defined_bb = func.basic_blocks.append("defined_method_yes")
@@ -1704,6 +1704,13 @@ module Konpeito
1704
1704
  # Determine scope (module/class context)
1705
1705
  scope = @current_module || @current_class
1706
1706
 
1707
+ # Track top-level constants (scope == nil) for static Init registration in native backend.
1708
+ # __main__ is never called from Init, so top-level constants must be set explicitly.
1709
+ if scope.nil?
1710
+ literal_node = visit_literal_value(typed_node.children.first)
1711
+ @program.toplevel_constants << [name, literal_node]
1712
+ end
1713
+
1707
1714
  inst = StoreConstant.new(name: name, value: value, scope: scope, type: typed_node.type)
1708
1715
  emit(inst)
1709
1716
  value
@@ -2133,38 +2140,65 @@ module Konpeito
2133
2140
  block = visit_block_def(block_child)
2134
2141
  end
2135
2142
 
2136
- # Handle &blk block argument reference (e.g., arr.map(&blk))
2143
+ # Handle &blk block argument reference (e.g., arr.map(&blk)) or
2144
+ # &:symbol Symbol#to_proc (e.g., arr.map(&:upcase))
2137
2145
  unless block
2138
2146
  block_arg_child = typed_node.children.find { |c| c.node_type == :block_argument }
2139
2147
  if block_arg_child
2140
2148
  blk_node = block_arg_child.node
2141
2149
  if blk_node.respond_to?(:expression) && blk_node.expression
2142
- blk_name = blk_node.expression.name.to_s
2143
- # Create a wrapper BlockDef: { |__block_arg_param| blk.call(__block_arg_param) }
2144
- # Load blk from captures inside the block body (not from outer scope)
2145
- param_name = "__block_arg_param"
2146
- param = Param.new(name: param_name, type: TypeChecker::Types::UNTYPED)
2147
- param_var = LocalVar.new(name: param_name, type: TypeChecker::Types::UNTYPED)
2148
- param_load = LoadLocal.new(var: param_var, type: TypeChecker::Types::UNTYPED, result_var: new_temp_var)
2149
- blk_local_var = LocalVar.new(name: blk_name, type: TypeChecker::Types::UNTYPED)
2150
- blk_load = LoadLocal.new(var: blk_local_var, type: TypeChecker::Types::UNTYPED, result_var: new_temp_var)
2151
- call_inst = Call.new(
2152
- receiver: blk_load,
2153
- method_name: "call",
2154
- args: [param_load],
2155
- type: TypeChecker::Types::UNTYPED,
2156
- result_var: new_temp_var
2157
- )
2158
- # Wrap in a BasicBlock (BlockDef.body expects Array[BasicBlock])
2159
- bb = BasicBlock.new(label: "block_arg_body")
2160
- bb.add_instruction(blk_load)
2161
- bb.add_instruction(param_load)
2162
- bb.add_instruction(call_inst)
2163
- block = BlockDef.new(
2164
- params: [param],
2165
- body: [bb],
2166
- captures: [Capture.new(name: blk_name, type: TypeChecker::Types::UNTYPED)]
2167
- )
2150
+ expr = blk_node.expression
2151
+
2152
+ if expr.is_a?(Prism::SymbolNode)
2153
+ # &:method_name — Symbol#to_proc: create { |x| x.method_name }
2154
+ sym_method = expr.value.to_s
2155
+ param_name = "__sym_proc_param"
2156
+ param = Param.new(name: param_name, type: TypeChecker::Types::UNTYPED)
2157
+ param_var = LocalVar.new(name: param_name, type: TypeChecker::Types::UNTYPED)
2158
+ param_load = LoadLocal.new(var: param_var, type: TypeChecker::Types::UNTYPED, result_var: new_temp_var)
2159
+ call_inst = Call.new(
2160
+ receiver: param_load,
2161
+ method_name: sym_method,
2162
+ args: [],
2163
+ type: TypeChecker::Types::UNTYPED,
2164
+ result_var: new_temp_var
2165
+ )
2166
+ bb = BasicBlock.new(label: "sym_proc_body")
2167
+ bb.add_instruction(param_load)
2168
+ bb.add_instruction(call_inst)
2169
+ block = BlockDef.new(
2170
+ params: [param],
2171
+ body: [bb],
2172
+ captures: []
2173
+ )
2174
+ else
2175
+ blk_name = expr.name.to_s
2176
+ # Create a wrapper BlockDef: { |__block_arg_param| blk.call(__block_arg_param) }
2177
+ # Load blk from captures inside the block body (not from outer scope)
2178
+ param_name = "__block_arg_param"
2179
+ param = Param.new(name: param_name, type: TypeChecker::Types::UNTYPED)
2180
+ param_var = LocalVar.new(name: param_name, type: TypeChecker::Types::UNTYPED)
2181
+ param_load = LoadLocal.new(var: param_var, type: TypeChecker::Types::UNTYPED, result_var: new_temp_var)
2182
+ blk_local_var = LocalVar.new(name: blk_name, type: TypeChecker::Types::UNTYPED)
2183
+ blk_load = LoadLocal.new(var: blk_local_var, type: TypeChecker::Types::UNTYPED, result_var: new_temp_var)
2184
+ call_inst = Call.new(
2185
+ receiver: blk_load,
2186
+ method_name: "call",
2187
+ args: [param_load],
2188
+ type: TypeChecker::Types::UNTYPED,
2189
+ result_var: new_temp_var
2190
+ )
2191
+ # Wrap in a BasicBlock (BlockDef.body expects Array[BasicBlock])
2192
+ bb = BasicBlock.new(label: "block_arg_body")
2193
+ bb.add_instruction(blk_load)
2194
+ bb.add_instruction(param_load)
2195
+ bb.add_instruction(call_inst)
2196
+ block = BlockDef.new(
2197
+ params: [param],
2198
+ body: [bb],
2199
+ captures: [Capture.new(name: blk_name, type: TypeChecker::Types::UNTYPED)]
2200
+ )
2201
+ end
2168
2202
  end
2169
2203
  end
2170
2204
  end
@@ -37,12 +37,14 @@ module Konpeito
37
37
  # Program is the top-level container
38
38
  class Program < Node
39
39
  attr_reader :functions, :classes, :modules
40
+ attr_accessor :toplevel_constants # Array of [name, literal_node] for top-level constants
40
41
 
41
42
  def initialize(functions: [], classes: [], modules: [])
42
43
  super(type: TypeChecker::Types::NIL)
43
44
  @functions = functions
44
45
  @classes = classes
45
46
  @modules = modules
47
+ @toplevel_constants = []
46
48
  end
47
49
  end
48
50
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Konpeito
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
5
5
  end
@@ -45,6 +45,7 @@ mkdir -p "$BUILD_DIR/konpeito/runtime"
45
45
  "$SCRIPT_DIR/src/konpeito/runtime/KRactorPort.java" \
46
46
  "$SCRIPT_DIR/src/konpeito/runtime/KMatchData.java" \
47
47
  "$SCRIPT_DIR/src/konpeito/runtime/KFiber.java" \
48
+ "$SCRIPT_DIR/src/konpeito/runtime/KRubyException.java" \
48
49
  "$SCRIPT_DIR/src/konpeito/runtime/RubyDispatch.java"
49
50
 
50
51
  # Copy runtime classes to a separate directory for JAR bundling
@@ -14,6 +14,7 @@ import java.util.*;
14
14
  */
15
15
  public class KHash<K, V> implements Map<K, V> {
16
16
  private final LinkedHashMap<K, V> data;
17
+ private boolean frozen = false;
17
18
 
18
19
  // ========================================================================
19
20
  // Constructors
@@ -31,6 +32,17 @@ public class KHash<K, V> implements Map<K, V> {
31
32
  // Ruby-specific methods
32
33
  // ========================================================================
33
34
 
35
+ /** Ruby: hash.freeze — makes this hash immutable */
36
+ public KHash<K, V> freeze() {
37
+ this.frozen = true;
38
+ return this;
39
+ }
40
+
41
+ /** Ruby: hash.frozen? */
42
+ public boolean isFrozen() {
43
+ return frozen;
44
+ }
45
+
34
46
  /** Ruby: hash.deconstruct_keys(keys) — returns self (for pattern matching) */
35
47
  @SuppressWarnings("unchecked")
36
48
  public KHash<K, V> deconstruct_keys(Object keys) {