konpeito 0.2.0 → 0.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85de87c8abf1bbf3ecaf6303ba4fd875f16016099a38734c55c108f123fa4086
4
- data.tar.gz: 843f4b6d722d68526d031734c035c9126c520d9869a2488259dbf39ce1eade84
3
+ metadata.gz: 9b3a19c0259a27b1d396334b57f827f784b5aeebe022d714fb68a5b8e40537f8
4
+ data.tar.gz: a248d9e0166ef1643bb5e0a15922432cdcbd7c6758dcc6c97aa1ecebd36c48a2
5
5
  SHA512:
6
- metadata.gz: eaf22b341a9135b2d81fcda350790330a78f2da9255771833676047086fbcd861919295f8cf7f4967f380cc62c8ea882499cd6b89a863b86bbd1c53e6e6f2b9e
7
- data.tar.gz: ab456db3432eca45ff8b658b80c07d38cdbec33c1e4cd6e379d59dbb0cf8f3384b6f715b3bac61f7d120dfb25e479fe86b65dc4c44ad5c7e86ab18703ff6e6a9
6
+ metadata.gz: 5c8ea1d3161e5173f30714897b5921faa0c9aa330e65e0ad7188ac166d730a6f35ff0ee3239ce010b4c33904b0dcd60370545bf37fe33279617688c1051e609e
7
+ data.tar.gz: 83a0fee1954e7cb1870f378ddb0031ce9885183688b23f1782f20770252e12176ed2255da83deda37bd1411fe64fc137835eb8f78ecc5ad20d603558d8de0c08
data/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to Konpeito will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2026-02-20
9
+
10
+ ### Added
11
+ - Conformance test framework (`spec/conformance/`) for verifying LLVM and JVM backend output against CRuby reference
12
+
13
+ ### Fixed
14
+ - Code generation: if/unless truthiness evaluation for non-boolean values (phi type mixing)
15
+ - Code generation: method argument count mismatch in certain call patterns
16
+ - Code generation: block yield / `block_given?` interaction with monomorphizer inconsistent call sites
17
+
8
18
  ## [0.2.0] - 2026-02-19
9
19
 
10
20
  ### Added
@@ -121,6 +131,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
121
131
  - `%a{extern}` - external C struct wrappers
122
132
  - `%a{simd}` - SIMD vectorization
123
133
 
134
+ [0.2.1]: https://github.com/i2y/konpeito/compare/v0.2.0...v0.2.1
124
135
  [0.2.0]: https://github.com/i2y/konpeito/compare/v0.1.3...v0.2.0
125
136
  [0.1.3]: https://github.com/i2y/konpeito/compare/v0.1.2...v0.1.3
126
137
  [0.1.2]: https://github.com/i2y/konpeito/compare/v0.1.1...v0.1.2
data/Rakefile CHANGED
@@ -8,4 +8,19 @@ Rake::TestTask.new(:test) do |t|
8
8
  t.test_files = FileList["test/**/*_test.rb"]
9
9
  end
10
10
 
11
+ desc "Run conformance tests against Ruby/Native/JVM backends"
12
+ task :conformance do
13
+ ruby "spec/conformance/runner.rb", *ARGV.drop_while { |a| a != "--" }.drop(1)
14
+ end
15
+
16
+ desc "Run conformance tests (native backend only)"
17
+ task "conformance:native" do
18
+ ruby "spec/conformance/runner.rb", "--native-only"
19
+ end
20
+
21
+ desc "Run conformance tests (JVM backend only)"
22
+ task "conformance:jvm" do
23
+ ruby "spec/conformance/runner.rb", "--jvm-only"
24
+ end
25
+
11
26
  task default: :test
@@ -176,6 +176,10 @@ module Konpeito
176
176
  callee.params.each_with_index do |param, i|
177
177
  if call_inst.args[i]
178
178
  param_map[param.name] = call_inst.args[i]
179
+ elsif param.default_value
180
+ # Use the default value for missing optional args
181
+ default_hir = prism_to_hir_literal(param.default_value)
182
+ param_map[param.name] = default_hir if default_hir
179
183
  end
180
184
  end
181
185
  # Also map keyword arguments by name
@@ -224,6 +228,33 @@ module Konpeito
224
228
  result_instructions
225
229
  end
226
230
 
231
+ # Convert a Prism AST default value node to an HIR literal node.
232
+ # Assigns a result_var so the LLVM generator can generate it properly.
233
+ def prism_to_hir_literal(prism_node)
234
+ @default_var_counter ||= 0
235
+ @default_var_counter += 1
236
+ rv = "_default_#{@default_var_counter}"
237
+
238
+ case prism_node
239
+ when Prism::IntegerNode
240
+ HIR::IntegerLit.new(value: prism_node.value, result_var: rv)
241
+ when Prism::FloatNode
242
+ HIR::FloatLit.new(value: prism_node.value, result_var: rv)
243
+ when Prism::StringNode
244
+ HIR::StringLit.new(value: prism_node.unescaped, result_var: rv)
245
+ when Prism::SymbolNode
246
+ HIR::SymbolLit.new(value: prism_node.value, result_var: rv)
247
+ when Prism::NilNode
248
+ HIR::NilLit.new(result_var: rv)
249
+ when Prism::TrueNode
250
+ HIR::BoolLit.new(value: true, result_var: rv)
251
+ when Prism::FalseNode
252
+ HIR::BoolLit.new(value: false, result_var: rv)
253
+ else
254
+ nil
255
+ end
256
+ end
257
+
227
258
  def clone_and_rename(inst, prefix, param_map)
228
259
  case inst
229
260
  when HIR::LoadLocal
@@ -191,6 +191,12 @@ module Konpeito
191
191
  # global variables referenced inside blocks have fields available.
192
192
  prescan_global_variables(hir_program)
193
193
 
194
+ # Pre-scan: detect functions called with inconsistent argument types
195
+ # across call sites, and widen those params to :value (Object).
196
+ # This prevents JVM VerifyError when e.g. assert_equal is called with
197
+ # both Integer and String arguments at different sites.
198
+ prescan_call_site_arg_types(hir_program)
199
+
194
200
  # Generate module interfaces FIRST (before classes that may implement them)
195
201
  hir_program.modules.each do |module_def|
196
202
  @block_methods = []
@@ -340,6 +346,80 @@ module Konpeito
340
346
  end
341
347
  end
342
348
 
349
+ # Pre-scan: detect functions called with inconsistent argument types.
350
+ # When a function like assert_equal(expected, actual, desc) is called with
351
+ # Integer args at one site and String args at another, the JVM needs a single
352
+ # method descriptor. If HM inference resolves params to a concrete type (e.g. :i64)
353
+ # based on the first call site, later call sites with different types cause VerifyError.
354
+ # This pre-scan detects such cases and marks params for widening to :value (Object).
355
+ def prescan_call_site_arg_types(hir_program)
356
+ # Collect argument types at each call site per function
357
+ call_arg_types = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = Set.new } }
358
+
359
+ # Scan all functions (top-level + class methods are both in functions list)
360
+ # Note: top-level method calls may have SelfRef receiver (implicit self),
361
+ # so we check for both nil and SelfRef receivers.
362
+ hir_program.functions.each do |func|
363
+ each_instruction_recursive(func.body) do |inst|
364
+ next unless inst.is_a?(HIR::Call)
365
+ next if inst.receiver && !inst.receiver.is_a?(HIR::SelfRef)
366
+ target = inst.method_name.to_s
367
+ inst.args.each_with_index do |arg, i|
368
+ call_arg_types[target][i] << static_arg_type(arg)
369
+ end
370
+ end
371
+ end
372
+
373
+ # Build widened params map: func_name => Set of param indices that need widening
374
+ @widened_params = {}
375
+ hir_program.functions.each do |func|
376
+ func_name = func.name.to_s
377
+ sites = call_arg_types[func_name]
378
+ next if sites.empty?
379
+
380
+ widened = Set.new
381
+ func.params.each_with_index do |param, i|
382
+ site_types = sites[i]
383
+ next if site_types.nil? || site_types.size <= 1
384
+
385
+ # Multiple different types at call sites — widen to :value
386
+ widened << i
387
+ end
388
+
389
+ @widened_params[func_name] = widened unless widened.empty?
390
+ end
391
+ end
392
+
393
+ # Infer the JVM type tag of an HIR node statically (without @variable_types).
394
+ # Used by prescan_call_site_arg_types.
395
+ def static_arg_type(node)
396
+ case node
397
+ when HIR::IntegerLit then :i64
398
+ when HIR::FloatLit then :double
399
+ when HIR::StringLit then :string
400
+ when HIR::BoolLit then :i8
401
+ when HIR::NilLit then :value
402
+ else
403
+ if node.respond_to?(:type) && node.type
404
+ konpeito_type_to_tag(node.type)
405
+ else
406
+ :value
407
+ end
408
+ end
409
+ end
410
+
411
+ # Get the effective param type for a function, applying widening if needed.
412
+ # Widening occurs when different call sites pass different types for the same param.
413
+ def widened_param_type(func, param, index)
414
+ widened = @widened_params && @widened_params[func.name.to_s]
415
+ if widened && widened.include?(index)
416
+ :value
417
+ else
418
+ param_type(param)
419
+ end
420
+ end
421
+
422
+
343
423
  # Returns the complete JSON IR as a Hash
344
424
  def to_json_ir
345
425
  { "classes" => @class_defs }
@@ -481,9 +561,9 @@ module Konpeito
481
561
  ret_type = function_return_type(func)
482
562
  @current_function_return_type = ret_type
483
563
 
484
- # Allocate parameter slots
485
- func.params.each do |param|
486
- type = param_type(param)
564
+ # Allocate parameter slots (use widened types for functions called with mixed arg types)
565
+ func.params.each_with_index do |param, i|
566
+ type = widened_param_type(func, param, i)
487
567
  allocate_slot(param.name, type)
488
568
  # *args (rest param) is a Ruby Array, **kwargs (keyword_rest) is a Hash
489
569
  if param.rest || param.keyword_rest
@@ -1033,6 +1113,16 @@ module Konpeito
1033
1113
  @variable_native_array_element_type[target_var] = @variable_native_array_element_type[source_var] if @variable_native_array_element_type[source_var]
1034
1114
  @variable_array_element_types[target_var] = @variable_array_element_types[source_var] if @variable_array_element_types[source_var]
1035
1115
  @variable_is_class_ref[target_var] = @variable_is_class_ref[source_var] if @variable_is_class_ref[source_var]
1116
+ elsif value.is_a?(HIR::IntegerLit) || value.is_a?(HIR::FloatLit) ||
1117
+ value.is_a?(HIR::StringLit) || value.is_a?(HIR::BoolLit) ||
1118
+ value.is_a?(HIR::NilLit) || value.is_a?(HIR::SymbolLit)
1119
+ # Literal value (e.g. from inlined default parameter) — generate inline
1120
+ loaded_type = infer_type_from_hir(value) || :value
1121
+ type = reconcile_store_type(target_var, loaded_type)
1122
+ ensure_slot(target_var, type)
1123
+ instructions.concat(load_value(value, type))
1124
+ instructions << store_instruction(target_var, type)
1125
+ @variable_types[target_var] = type
1036
1126
  else
1037
1127
  # Value should already be on the stack or in a temp var
1038
1128
  source_var = value.respond_to?(:result_var) ? value.result_var : nil
@@ -3112,15 +3202,19 @@ module Konpeito
3112
3202
  "name" => "<init>", "descriptor" => "()V" }
3113
3203
  end
3114
3204
  elsif i < args.size
3115
- param_t = param_type(param)
3205
+ param_t = widened_param_type(target_func, param, i)
3116
3206
  instructions.concat(load_value(args[i], param_t))
3117
3207
  # Unbox if loaded type is :value but function expects primitive
3118
3208
  loaded_t = infer_loaded_type(args[i])
3119
3209
  instructions.concat(unbox_if_needed(loaded_t, param_t))
3120
3210
  else
3121
3211
  # Optional parameter not provided at call site — push default value
3122
- param_t = param_type(param)
3123
- instructions.concat(default_value_instructions(param_t))
3212
+ param_t = widened_param_type(target_func, param, i)
3213
+ if param.default_value
3214
+ instructions.concat(prism_default_to_jvm(param.default_value, param_t))
3215
+ else
3216
+ instructions.concat(default_value_instructions(param_t))
3217
+ end
3124
3218
  end
3125
3219
  end
3126
3220
 
@@ -3339,10 +3433,9 @@ module Konpeito
3339
3433
  # boolean: ifeq jumps to else (false = 0)
3340
3434
  instructions << { "op" => "ifeq", "target" => else_label }
3341
3435
  when :i64
3342
- # long: compare with 0
3343
- instructions << { "op" => "lconst_0" }
3344
- instructions << { "op" => "lcmp" }
3345
- instructions << { "op" => "ifeq", "target" => else_label }
3436
+ # Ruby: all integers (including 0) are truthy.
3437
+ # Pop the loaded long value and always fall through to then.
3438
+ instructions << { "op" => "pop2" }
3346
3439
  when :value
3347
3440
  # Ruby truthiness: null (nil) and Boolean.FALSE (false) are falsy.
3348
3441
  # A simple ifnull misses Boolean.FALSE, causing && short-circuit bugs.
@@ -10985,6 +11078,63 @@ module Konpeito
10985
11078
  end
10986
11079
  end
10987
11080
 
11081
+ # Convert a Prism default value node to JVM bytecode instructions.
11082
+ # Used when optional parameters are missing at a call site.
11083
+ def prism_default_to_jvm(prism_node, expected_type)
11084
+ case prism_node
11085
+ when Prism::IntegerNode
11086
+ val = prism_node.value
11087
+ insts = if val == 0
11088
+ [{ "op" => "lconst_0" }]
11089
+ elsif val == 1
11090
+ [{ "op" => "lconst_1" }]
11091
+ else
11092
+ [{ "op" => "ldc2_w", "value" => val, "type" => "long" }]
11093
+ end
11094
+ if expected_type == :value
11095
+ insts << { "op" => "invokestatic", "owner" => "java/lang/Long",
11096
+ "name" => "valueOf", "descriptor" => "(J)Ljava/lang/Long;" }
11097
+ end
11098
+ insts
11099
+ when Prism::FloatNode
11100
+ val = prism_node.value
11101
+ insts = if val == 0.0
11102
+ [{ "op" => "dconst_0" }]
11103
+ elsif val == 1.0
11104
+ [{ "op" => "dconst_1" }]
11105
+ else
11106
+ [{ "op" => "ldc2_w", "value" => val, "type" => "double" }]
11107
+ end
11108
+ if expected_type == :value
11109
+ insts << { "op" => "invokestatic", "owner" => "java/lang/Double",
11110
+ "name" => "valueOf", "descriptor" => "(D)Ljava/lang/Double;" }
11111
+ end
11112
+ insts
11113
+ when Prism::StringNode
11114
+ [{ "op" => "ldc", "value" => prism_node.unescaped }]
11115
+ when Prism::SymbolNode
11116
+ [{ "op" => "ldc", "value" => prism_node.value }]
11117
+ when Prism::NilNode
11118
+ [{ "op" => "aconst_null" }]
11119
+ when Prism::TrueNode
11120
+ insts = [{ "op" => "iconst", "value" => 1 }]
11121
+ if expected_type == :value
11122
+ insts << { "op" => "invokestatic", "owner" => "java/lang/Boolean",
11123
+ "name" => "valueOf", "descriptor" => "(Z)Ljava/lang/Boolean;" }
11124
+ end
11125
+ insts
11126
+ when Prism::FalseNode
11127
+ insts = [{ "op" => "iconst", "value" => 0 }]
11128
+ if expected_type == :value
11129
+ insts << { "op" => "invokestatic", "owner" => "java/lang/Boolean",
11130
+ "name" => "valueOf", "descriptor" => "(Z)Ljava/lang/Boolean;" }
11131
+ end
11132
+ insts
11133
+ else
11134
+ default_value_instructions(expected_type)
11135
+ end
11136
+ end
11137
+
10988
11138
  def default_value_instructions(type)
10989
11139
  case type
10990
11140
  when :i64 then [{ "op" => "lconst_0" }]
@@ -11539,8 +11689,8 @@ module Konpeito
11539
11689
  end
11540
11690
 
11541
11691
  def method_descriptor(func)
11542
- params_desc = func.params.map do |param|
11543
- t = param_type(param)
11692
+ params_desc = func.params.each_with_index.map do |param, i|
11693
+ t = widened_param_type(func, param, i)
11544
11694
  t = :value if t == :void # Nil/void is not valid as JVM param type
11545
11695
  type_to_descriptor(t)
11546
11696
  end.join
@@ -40,6 +40,7 @@ module Konpeito
40
40
  @dibuilder = nil # DIBuilder for debug info
41
41
  @profiler = nil # Profiler for instrumentation
42
42
  @variadic_functions = {} # Track functions with **kwargs or *args
43
+ @comparison_result_vars = Set.new # Track variables holding comparison results (0/1 boolean)
43
44
 
44
45
  # Register all NativeClass types from RBS upfront
45
46
  register_native_classes_from_rbs
@@ -719,8 +720,8 @@ module Konpeito
719
720
  var.linkage = :external
720
721
  end
721
722
 
722
- # rb_eArgumentError global
723
- @rb_eArgumentError = @mod.globals.add(value_type, "rb_eArgumentError") do |var|
723
+ # rb_eArgError global (CRuby's ArgumentError class)
724
+ @rb_eArgumentError = @mod.globals.add(value_type, "rb_eArgError") do |var|
724
725
  var.linkage = :external
725
726
  end
726
727
 
@@ -968,6 +969,12 @@ module Konpeito
968
969
  # Insert profiling entry probe after parameter setup
969
970
  insert_profile_entry_probe(hir_func)
970
971
 
972
+ # Track blocks with Return terminators so phi nodes can skip them
973
+ @return_blocks = Set.new
974
+ hir_func.body.each do |hir_block|
975
+ @return_blocks << hir_block.label if hir_block.terminator.is_a?(HIR::Return)
976
+ end
977
+
971
978
  # Generate code for each block
972
979
  hir_func.body.each do |hir_block|
973
980
  generate_block(func, hir_block)
@@ -1205,9 +1212,7 @@ module Konpeito
1205
1212
 
1206
1213
  # Determine default value
1207
1214
  default_value = if param.default_value
1208
- # Has explicit default - we need to generate the default value
1209
- # For now, use Qnil as placeholder; proper default handling requires more work
1210
- @qnil
1215
+ generate_keyword_default_value(param.default_value)
1211
1216
  else
1212
1217
  # Required keyword - use Qundef to detect missing
1213
1218
  @qundef
@@ -1277,6 +1282,14 @@ module Konpeito
1277
1282
  @builder.store(kwargs_hash, alloca)
1278
1283
  end
1279
1284
  end
1285
+
1286
+ # After keyword processing, remap entry block to current position.
1287
+ # Required keyword params create branch blocks (kwarg_missing/kwarg_ok),
1288
+ # leaving the builder positioned in a continuation block rather than
1289
+ # the original entry block. Function body code must be generated there.
1290
+ if keyword_params.any? { |p| !p.default_value }
1291
+ @blocks[hir_func.body.first.label] = @builder.insert_block
1292
+ end
1280
1293
  end # End of variadic_info else block
1281
1294
 
1282
1295
  # Insert profiling entry probe after parameter setup
@@ -1286,6 +1299,13 @@ module Konpeito
1286
1299
  # Blocks with phi nodes referencing results from other blocks must come after those blocks
1287
1300
  sorted_blocks = sort_blocks_by_phi_dependencies(hir_func.body)
1288
1301
 
1302
+ # Track blocks with Return terminators so phi nodes can skip them
1303
+ # (a block that returns doesn't branch to the merge block)
1304
+ @return_blocks = Set.new
1305
+ sorted_blocks.each do |hir_block|
1306
+ @return_blocks << hir_block.label if hir_block.terminator.is_a?(HIR::Return)
1307
+ end
1308
+
1289
1309
  # Generate code for each block in dependency order
1290
1310
  sorted_blocks.each do |hir_block|
1291
1311
  generate_block(func, hir_block)
@@ -2244,6 +2264,14 @@ module Konpeito
2244
2264
  target_type = source_type
2245
2265
  # Need to recreate alloca with the new unboxed type
2246
2266
  @variable_allocas.delete(var_name)
2267
+ elsif %i[double i64 i8].include?(target_type) && source_type == :value && inst.value.is_a?(HIR::Phi)
2268
+ # Downgrade from unboxed to VALUE type for phi nodes with mixed types.
2269
+ # This happens when a phi with mixed types (e.g., bool + int from &&/||)
2270
+ # stores into a variable that was pre-allocated as unboxed based on static
2271
+ # type analysis. The phi resolves to :value at codegen time, so we must
2272
+ # widen the variable to :value to avoid unsafe unboxing (e.g., rb_num2long on Qfalse).
2273
+ target_type = :value
2274
+ @variable_allocas.delete(var_name)
2247
2275
  end
2248
2276
 
2249
2277
  value_to_store = convert_value(value, source_type, target_type)
@@ -2279,6 +2307,13 @@ module Konpeito
2279
2307
 
2280
2308
  @variables[var_name] = value_to_store
2281
2309
  @variable_types[var_name] = target_type
2310
+
2311
+ # Propagate comparison result flag through variable assignments
2312
+ src_var = inst.value.respond_to?(:result_var) ? inst.value.result_var : nil
2313
+ if src_var && @comparison_result_vars.include?(src_var)
2314
+ @comparison_result_vars.add(var_name)
2315
+ end
2316
+
2282
2317
  value_to_store
2283
2318
  end
2284
2319
 
@@ -2726,7 +2761,10 @@ module Konpeito
2726
2761
  # Skip direct call optimizations when splat args are present
2727
2762
  has_splat = inst.args.any? { |a| a.is_a?(HIR::SplatArg) }
2728
2763
 
2729
- unless has_splat
2764
+ # Skip direct call when a block is passed — direct LLVM calls bypass
2765
+ # CRuby's call frame, so rb_yield/rb_block_given_p inside the callee
2766
+ # would fail with LocalJumpError. Use rb_block_call instead.
2767
+ unless has_splat || inst.block
2730
2768
  # Check for monomorphized function call (direct call optimization)
2731
2769
  if (specialized_target = inst.instance_variable_get(:@specialized_target))
2732
2770
  result = generate_direct_call(inst, specialized_target)
@@ -2781,6 +2819,14 @@ module Konpeito
2781
2819
  end
2782
2820
  end
2783
2821
 
2822
+ # If a block is passed to a user-defined function, use rb_block_call
2823
+ # so CRuby sets up the block context for rb_yield/rb_block_given_p.
2824
+ if inst.block
2825
+ result = generate_rb_block_call_for_user_func(inst)
2826
+ @variables[inst.result_var] = result if inst.result_var
2827
+ return result
2828
+ end
2829
+
2784
2830
  # Get receiver as Ruby VALUE (box if needed)
2785
2831
  receiver = get_value_as_ruby(inst.receiver)
2786
2832
 
@@ -3899,6 +3945,13 @@ module Konpeito
3899
3945
  @builder.phi(value_type, phi_incoming, "#{method_sym}_result")
3900
3946
  end
3901
3947
 
3948
+ # Generate rb_block_call for user-defined functions that receive a block.
3949
+ # This ensures CRuby sets up the block context so rb_yield/rb_block_given_p
3950
+ # work correctly inside the callee.
3951
+ def generate_rb_block_call_for_user_func(inst)
3952
+ generate_rb_block_call(inst, nil)
3953
+ end
3954
+
3902
3955
  # Fallback: generate rb_block_call
3903
3956
  def generate_rb_block_call(inst, builtin)
3904
3957
  # Get receiver as Ruby VALUE
@@ -5172,6 +5225,7 @@ module Konpeito
5172
5225
  if inst.result_var
5173
5226
  @variables[inst.result_var] = result
5174
5227
  @variable_types[inst.result_var] = :i64
5228
+ @comparison_result_vars.add(inst.result_var)
5175
5229
  end
5176
5230
  result
5177
5231
  end
@@ -5231,6 +5285,7 @@ module Konpeito
5231
5285
  if inst.result_var
5232
5286
  @variables[inst.result_var] = result
5233
5287
  @variable_types[inst.result_var] = :i64
5288
+ @comparison_result_vars.add(inst.result_var)
5234
5289
  end
5235
5290
  result
5236
5291
  end
@@ -6329,8 +6384,8 @@ module Konpeito
6329
6384
  # yield with no arguments - pass Qnil
6330
6385
  @builder.call(@rb_yield, @qnil)
6331
6386
  elsif inst.args.size == 1
6332
- # yield with single argument
6333
- arg_value = get_value(inst.args.first)
6387
+ # yield with single argument — must be boxed VALUE for rb_yield
6388
+ arg_value = get_value_as_ruby(inst.args.first)
6334
6389
  @builder.call(@rb_yield, arg_value)
6335
6390
  else
6336
6391
  # yield with multiple arguments - use rb_yield_values2
@@ -6340,7 +6395,7 @@ module Konpeito
6340
6395
  argv = @builder.alloca(LLVM::Array(value_type, inst.args.size))
6341
6396
 
6342
6397
  inst.args.each_with_index do |arg, i|
6343
- arg_value = get_value(arg)
6398
+ arg_value = get_value_as_ruby(arg)
6344
6399
  ptr = @builder.gep(argv, [LLVM::Int32.from_i(0), LLVM::Int32.from_i(i)])
6345
6400
  @builder.store(arg_value, ptr)
6346
6401
  end
@@ -7040,6 +7095,31 @@ module Konpeito
7040
7095
  @builder.select(is_true, @qtrue, @qfalse)
7041
7096
  end
7042
7097
 
7098
+ # Generate LLVM value for keyword argument default from Prism AST node
7099
+ def generate_keyword_default_value(prism_node)
7100
+ case prism_node
7101
+ when Prism::StringNode
7102
+ str_ptr = @builder.global_string_pointer(prism_node.unescaped)
7103
+ @builder.call(@rb_str_new_cstr, str_ptr)
7104
+ when Prism::IntegerNode
7105
+ @builder.call(@rb_int2inum, LLVM::Int64.from_i(prism_node.value))
7106
+ when Prism::FloatNode
7107
+ @builder.call(@rb_float_new, LLVM::Double.from_f(prism_node.value))
7108
+ when Prism::NilNode
7109
+ @qnil
7110
+ when Prism::TrueNode
7111
+ @qtrue
7112
+ when Prism::FalseNode
7113
+ @qfalse
7114
+ when Prism::SymbolNode
7115
+ sym_ptr = @builder.global_string_pointer(prism_node.value)
7116
+ sym_id = @builder.call(@rb_intern, sym_ptr)
7117
+ @builder.call(@rb_id2sym, sym_id)
7118
+ else
7119
+ @qnil
7120
+ end
7121
+ end
7122
+
7043
7123
  # Convert Ruby VALUE to i1 boolean
7044
7124
  def ruby_to_bool(value)
7045
7125
  # In Ruby, only nil and false are falsy
@@ -7701,6 +7781,9 @@ module Konpeito
7701
7781
  inst.incoming.each do |label, hir_value|
7702
7782
  llvm_block = @blocks[label]
7703
7783
  next unless llvm_block
7784
+ # Skip blocks with Return terminators — they don't branch to the
7785
+ # merge block, so they cannot be predecessors in the phi node.
7786
+ next if @return_blocks&.include?(label)
7704
7787
  llvm_value, type_tag = get_value_with_type(hir_value)
7705
7788
  incoming_data << [llvm_block, llvm_value, type_tag]
7706
7789
  end
@@ -7755,11 +7838,25 @@ module Konpeito
7755
7838
 
7756
7839
  is_truthy = case cond_type
7757
7840
  when :i64
7758
- # For i64 (from comparison), non-zero is truthy
7759
- @builder.icmp(:ne, condition, LLVM::Int64.from_i(0))
7841
+ if comparison_result?(term.condition)
7842
+ # Comparison result (0=false, 1=true): use C-style truthiness
7843
+ @builder.icmp(:ne, condition, LLVM::Int64.from_i(0))
7844
+ else
7845
+ # Ruby: all integers (including 0) are truthy.
7846
+ # Box to VALUE and use Ruby truthiness (RTEST).
7847
+ boxed = @builder.call(@rb_int2inum, condition)
7848
+ ruby_to_bool(boxed)
7849
+ end
7760
7850
  when :double
7761
- # For double, non-zero is truthy
7762
- @builder.fcmp(:one, condition, LLVM::Double.from_f(0.0))
7851
+ if comparison_result?(term.condition)
7852
+ # Comparison result: use C-style truthiness
7853
+ @builder.fcmp(:one, condition, LLVM::Double.from_f(0.0))
7854
+ else
7855
+ # Ruby: all floats (including 0.0) are truthy.
7856
+ # Box to VALUE and use Ruby truthiness (RTEST).
7857
+ boxed = @builder.call(@rb_float_new, condition)
7858
+ ruby_to_bool(boxed)
7859
+ end
7763
7860
  when :i8
7764
7861
  # For i8 (Bool field), non-zero is truthy
7765
7862
  @builder.icmp(:ne, condition, LLVM::Int8.from_i(0))
@@ -7779,6 +7876,25 @@ module Konpeito
7779
7876
  end
7780
7877
  end
7781
7878
 
7879
+ # Check if an HIR condition node represents a comparison result (boolean 0/1)
7880
+ # rather than a raw integer/float value.
7881
+ # In Ruby, only nil and false are falsy — integers (including 0) and floats are always truthy.
7882
+ COMPARISON_METHODS = %w[== != < > <= >=].freeze
7883
+
7884
+ def comparison_result?(hir_condition)
7885
+ case hir_condition
7886
+ when HIR::Call
7887
+ COMPARISON_METHODS.include?(hir_condition.method_name)
7888
+ when HIR::LoadLocal
7889
+ var_name = hir_condition.var&.name
7890
+ @comparison_result_vars.include?(var_name)
7891
+ when HIR::Instruction
7892
+ hir_condition.result_var && @comparison_result_vars.include?(hir_condition.result_var)
7893
+ else
7894
+ false
7895
+ end
7896
+ end
7897
+
7782
7898
  # Generate return for a native method (unboxed return value)
7783
7899
  def generate_native_return(term)
7784
7900
  method_name = @current_hir_func&.name&.to_sym
@@ -186,8 +186,15 @@ module Konpeito
186
186
  # Group call sites by (function, types)
187
187
  grouped = @call_sites.group_by { |cs| [cs[:target], cs[:types]] }
188
188
 
189
+ # Detect functions called with inconsistent arg types across call sites.
190
+ # If the same param position receives different types at different sites
191
+ # (e.g., assert_equal called with both Integer and String as first arg),
192
+ # monomorphization is not useful — skip the function entirely.
193
+ skip_functions = detect_inconsistent_call_sites
194
+
189
195
  grouped.each do |(func_name, types), sites|
190
196
  next if types.all? { |t| t == TypeChecker::Types::UNTYPED }
197
+ next if skip_functions.include?(func_name.to_s)
191
198
 
192
199
  type_suffix = types.map { |t| type_to_suffix(t) }.join("_")
193
200
  specialized = "#{func_name}_#{type_suffix}"
@@ -211,6 +218,30 @@ module Konpeito
211
218
  consolidate_union_dispatches
212
219
  end
213
220
 
221
+ # Detect functions where different call sites pass different types for
222
+ # the same parameter position. These functions should not be monomorphized
223
+ # because specialized variants would have incompatible signatures.
224
+ def detect_inconsistent_call_sites
225
+ # Group non-union call sites by target function
226
+ by_func = @call_sites.reject { |cs| cs[:union_dispatch] }
227
+ .group_by { |cs| cs[:target].to_s }
228
+
229
+ skip = Set.new
230
+ by_func.each do |func_name, sites|
231
+ next if sites.size <= 1
232
+ # Check each param position for type consistency
233
+ max_arity = sites.map { |s| s[:types].size }.max
234
+ max_arity.times do |i|
235
+ types_at_i = sites.map { |s| s[:types][i]&.to_s }.compact.uniq
236
+ if types_at_i.size > 1
237
+ skip.add(func_name)
238
+ break
239
+ end
240
+ end
241
+ end
242
+ skip
243
+ end
244
+
214
245
  # Group union call sites by their original (pre-expansion) call
215
246
  def consolidate_union_dispatches
216
247
  @union_dispatches = {}
@@ -2068,17 +2068,46 @@ module Konpeito
2068
2068
  def infer_and(node)
2069
2069
  left_type = infer(node.left)
2070
2070
  right_type = infer(node.right)
2071
- # a && b: if a is falsy, result is a (nil/false); otherwise result is b
2072
- # Return right_type as it's the most common use case
2073
- right_type
2071
+ # a && b: if a is falsy, result is a; if a is truthy, result is b
2072
+ if left_type == right_type
2073
+ right_type
2074
+ elsif always_falsy_type?(left_type)
2075
+ left_type
2076
+ elsif always_truthy_type?(left_type)
2077
+ right_type
2078
+ else
2079
+ Types::Union.new([left_type, right_type])
2080
+ end
2074
2081
  end
2075
2082
 
2076
2083
  def infer_or(node)
2077
2084
  left_type = infer(node.left)
2078
2085
  right_type = infer(node.right)
2079
- # a || b: if a is truthy, result is a; otherwise result is b
2080
- # Return left_type as it's the most common use case
2081
- left_type
2086
+ # a || b: if a is truthy, result is a; if a is falsy, result is b
2087
+ if left_type == right_type
2088
+ left_type
2089
+ elsif always_truthy_type?(left_type)
2090
+ left_type
2091
+ elsif always_falsy_type?(left_type)
2092
+ right_type
2093
+ else
2094
+ Types::Union.new([left_type, right_type])
2095
+ end
2096
+ end
2097
+
2098
+ # NilClass and FalseClass are always falsy in Ruby
2099
+ def always_falsy_type?(type)
2100
+ type.is_a?(Types::NilType) ||
2101
+ (type.is_a?(Types::ClassInstance) && type.name == :FalseClass)
2102
+ end
2103
+
2104
+ # Everything except nil, false, and bool is always truthy in Ruby
2105
+ def always_truthy_type?(type)
2106
+ return false if type.is_a?(Types::NilType)
2107
+ return false if type.is_a?(Types::BoolType)
2108
+ return false if type.is_a?(Types::ClassInstance) && type.name == :FalseClass
2109
+ return false if type.is_a?(Types::Union)
2110
+ true
2082
2111
  end
2083
2112
 
2084
2113
  # Compound assignment operators
@@ -2095,15 +2124,36 @@ module Konpeito
2095
2124
  existing = lookup(node.name)
2096
2125
  var_type = existing ? existing.instantiate : TypeVar.new
2097
2126
  value_type = infer(node.value)
2098
- # x ||= val: result is either existing value or val
2099
- var_type
2127
+ # x ||= val: if x is falsy, x becomes val; if truthy, x stays
2128
+ result = if var_type == value_type
2129
+ var_type
2130
+ elsif always_falsy_type?(var_type)
2131
+ value_type
2132
+ elsif always_truthy_type?(var_type)
2133
+ var_type
2134
+ else
2135
+ Types::Union.new([var_type, value_type])
2136
+ end
2137
+ bind(node.name, result)
2138
+ result
2100
2139
  end
2101
2140
 
2102
2141
  def infer_local_variable_and_write(node)
2103
2142
  existing = lookup(node.name)
2104
2143
  var_type = existing ? existing.instantiate : TypeVar.new
2105
2144
  value_type = infer(node.value)
2106
- value_type
2145
+ # x &&= val: if x is truthy, x becomes val; if falsy, x stays
2146
+ result = if var_type == value_type
2147
+ value_type
2148
+ elsif always_truthy_type?(var_type)
2149
+ value_type
2150
+ elsif always_falsy_type?(var_type)
2151
+ var_type
2152
+ else
2153
+ Types::Union.new([var_type, value_type])
2154
+ end
2155
+ bind(node.name, result)
2156
+ result
2107
2157
  end
2108
2158
 
2109
2159
  def infer_instance_variable_write(node)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Konpeito
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: konpeito
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yasushi Itoh