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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 968753a2cfabd2d2b237dd5125d6ae0818848a3f7243e7fdbb408a50dc802f48
4
- data.tar.gz: 724dcd3309cb7e42fbd2cf7e3ffa0b6b1eac07840a8ba46affcad3c0bab6a21a
3
+ metadata.gz: eb29f99bc1ae9fcdcf859aaea0f04974db8a8f42ddfa989c5a07d6e722d04a5e
4
+ data.tar.gz: f3ae88ae17967ee5241737159a53781fcdc33894f6ab122af4cc2e58a8ef56f9
5
5
  SHA512:
6
- metadata.gz: e23cbc46176d84f51b1706e1125eae4f02a1ff31f6e9f64bc49856c79c8061a1aae6441cac1ee638c1a7eb8b0884cb235a40d11c509783e8e82b502d0b77e477
7
- data.tar.gz: 3ead6d2c4b880aae4f687cfabf4f2c75c07f3b1939ed610a20ca0b1af301ff27153acfc25007720aaad772ae2581cb55c11558d7c6e9d9a16876012911081394
6
+ metadata.gz: 8a267485ba1745ee5ebcf07be2d9235e4f8e6acb112292132943c2a484ec680f0af59ada1c0417326ad5765e1baeade52bcaf0b7d32de8bc1a1dcda92128391d
7
+ data.tar.gz: e162318a4fbe08e1022218a800ece9f8b6b6d2ad362b63ee8348ffc369772ce3f5f477432a6df08af69fd90b429f2f93fbaafb0619f5ab5bfcf5521a40c83780
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ 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.4] - 2026-02-28
9
+
10
+ ### Added
11
+ - Conformance test suite expanded to 87 spec files (10 new specs: array_functional, frozen_immutable, hash_transform, integer_step, kernel_format, object_protocol, proc_curry, range_advanced, regexp_matchdata, string_succ_ord)
12
+ - JVM runtime: updated KFiber, KMatchData, KRactor, KRactorPort classes for new conformance specs
13
+
14
+ ### Fixed
15
+ - Native backend: `lambda?` and `arity` singleton methods now correctly attached to Proc objects via `rb_define_singleton_method`, achieving 87/87 native conformance
16
+ - Native backend: NativeClass method chaining — monomorphized call results (e.g. `dot_Vec2`) now correctly tagged as `:double`/`:i64` in `@variable_types`, preventing `rb_num2dbl` from being called on an already-unboxed double (was causing segfault in `normalized_dot` benchmark)
17
+ - Benchmark: `slice_bench.rb` RBS overload syntax updated from duplicate `def []:` to union `|` syntax (RBS 3.x compatibility)
18
+
19
+ ### Changed
20
+ - `.gitignore`: `*.class` pattern extended to cover all subdirectories (was only matching root-level `.class` files)
21
+
8
22
  ## [0.2.3] - 2026-02-24
9
23
 
10
24
  ### Added
@@ -162,6 +176,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
162
176
  - `%a{extern}` - external C struct wrappers
163
177
  - `%a{simd}` - SIMD vectorization
164
178
 
179
+ [0.2.4]: https://github.com/i2y/konpeito/compare/v0.2.3...v0.2.4
180
+ [0.2.3]: https://github.com/i2y/konpeito/compare/v0.2.2...v0.2.3
165
181
  [0.2.2]: https://github.com/i2y/konpeito/compare/v0.2.1...v0.2.2
166
182
  [0.2.1]: https://github.com/i2y/konpeito/compare/v0.2.0...v0.2.1
167
183
  [0.2.0]: https://github.com/i2y/konpeito/compare/v0.1.3...v0.2.0
@@ -403,6 +403,13 @@ module Konpeito
403
403
  end
404
404
  end
405
405
 
406
+ # Register top-level constants on rb_cObject (e.g., MY_CONST = 42 at top level)
407
+ hir.toplevel_constants.each do |const_name, value_node|
408
+ c_value = hir_literal_to_c_value(value_node)
409
+ next unless c_value
410
+ lines << " rb_const_set(rb_cObject, rb_intern(\"#{const_name}\"), #{c_value});"
411
+ end
412
+
406
413
  # Define top-level methods on Object
407
414
  hir.functions.each do |func_def|
408
415
  next if func_def.owner_class
@@ -4491,7 +4491,7 @@ module Konpeito
4491
4491
  # Extract elements for remaining params
4492
4492
  remaining_params = target_func.params[splat_index..]
4493
4493
  remaining_params.each_with_index do |param, j|
4494
- next if param.rest || param.keyword_rest
4494
+ next if param.rest || param.keyword_rest || param.block
4495
4495
  param_t = widened_param_type(target_func, param, splat_index + j)
4496
4496
  # Load array, push index, call get(int)
4497
4497
  instructions << load_instruction(splat_temp, :value)
@@ -4503,8 +4503,9 @@ module Konpeito
4503
4503
  end
4504
4504
  else
4505
4505
  # Normal argument loading (no splat)
4506
- # Load arguments
4506
+ # Load arguments (skip block params — they are handled as KBlock below)
4507
4507
  target_func.params.each_with_index do |param, i|
4508
+ next if param.block
4508
4509
  if param.rest
4509
4510
  # Rest parameter (*args): collect remaining arguments into a KArray
4510
4511
  rest_args = args[i..]
@@ -4547,13 +4548,18 @@ module Konpeito
4547
4548
  end
4548
4549
  end
4549
4550
 
4550
- # If target is a yield-containing function, determine descriptor and pass null block
4551
+ # If target is a yield-containing function, determine descriptor and pass block (or null)
4551
4552
  target_has_yield = @yield_functions.include?(actual_target) || @yield_functions.include?(method_name)
4552
4553
  if target_has_yield
4553
4554
  kblock_iface = yield_function_kblock_interface(target_func)
4554
4555
  desc = method_descriptor_with_block(target_func, kblock_iface)
4555
- # Pass null as block argument (no block provided)
4556
- instructions << { "op" => "aconst_null" }
4556
+ # If caller provides a block, compile it and pass as KBlock; otherwise pass null
4557
+ if inst.block
4558
+ block_insts = compile_block_arg_for_instance_call(inst.block)
4559
+ instructions.concat(block_insts)
4560
+ else
4561
+ instructions << { "op" => "aconst_null" }
4562
+ end
4557
4563
  else
4558
4564
  desc = method_descriptor(target_func)
4559
4565
  end
@@ -9167,7 +9173,7 @@ module Konpeito
9167
9173
 
9168
9174
  # Build method descriptor with KBlock parameter for yield-containing functions
9169
9175
  def method_descriptor_with_block(func, kblock_iface)
9170
- params_desc = func.params.map { |p|
9176
+ params_desc = func.params.reject { |p| p.block }.map { |p|
9171
9177
  t = param_type(p)
9172
9178
  t = :value if t == :void # Nil/void is not valid as JVM param type
9173
9179
  type_to_descriptor(t)
@@ -9287,6 +9293,7 @@ module Konpeito
9287
9293
 
9288
9294
  # Load regular arguments (using the target function's param types)
9289
9295
  target_func.params.each_with_index do |param, i|
9296
+ next if param.block # block params handled as KBlock separately
9290
9297
  if param.keyword_rest
9291
9298
  # Keyword rest parameter (**kwargs): build a KHash from keyword_args
9292
9299
  if inst.respond_to?(:keyword_args) && inst.has_keyword_args?
@@ -9530,7 +9537,7 @@ module Konpeito
9530
9537
  # because they may receive Regexp (Pattern) arguments that can't be handled inline.
9531
9538
  %w[length size upcase downcase include? start_with? end_with? strip
9532
9539
  reverse empty? split chars lines bytes
9533
- replace freeze frozen? to_i to_f []].include?(name)
9540
+ replace to_i to_f []].include?(name)
9534
9541
  end
9535
9542
 
9536
9543
  # Check if a method argument is a Regexp (Pattern) type
@@ -10223,7 +10230,10 @@ module Konpeito
10223
10230
  instructions << { "op" => "lcmp" }
10224
10231
  instructions << { "op" => "ifne", "target" => found_label }
10225
10232
  else
10226
- instructions << { "op" => "ifnonnull", "target" => found_label }
10233
+ # Use RubyDispatch.isTruthy to handle null (nil) and Boolean.FALSE
10234
+ instructions << { "op" => "invokestatic", "owner" => "konpeito/runtime/RubyDispatch",
10235
+ "name" => "isTruthy", "descriptor" => "(Ljava/lang/Object;)Z" }
10236
+ instructions << { "op" => "ifne", "target" => found_label }
10227
10237
  end
10228
10238
  end
10229
10239
 
@@ -10577,7 +10587,10 @@ module Konpeito
10577
10587
  instructions << { "op" => "lcmp" }
10578
10588
  instructions << { "op" => "ifeq", "target" => skip_label }
10579
10589
  else
10580
- instructions << { "op" => "ifnull", "target" => skip_label }
10590
+ # Use RubyDispatch.isTruthy to correctly handle null (nil) and Boolean.FALSE
10591
+ instructions << { "op" => "invokestatic", "owner" => "konpeito/runtime/RubyDispatch",
10592
+ "name" => "isTruthy", "descriptor" => "(Ljava/lang/Object;)Z" }
10593
+ instructions << { "op" => "ifeq", "target" => skip_label }
10581
10594
  end
10582
10595
  else
10583
10596
  instructions << { "op" => "goto", "target" => skip_label }
@@ -10692,7 +10705,10 @@ module Konpeito
10692
10705
  instructions << { "op" => "lcmp" }
10693
10706
  instructions << { "op" => "ifne", "target" => skip_label }
10694
10707
  else
10695
- instructions << { "op" => "ifnonnull", "target" => skip_label }
10708
+ # Use RubyDispatch.isTruthy to handle null (nil) and Boolean.FALSE
10709
+ instructions << { "op" => "invokestatic", "owner" => "konpeito/runtime/RubyDispatch",
10710
+ "name" => "isTruthy", "descriptor" => "(Ljava/lang/Object;)Z" }
10711
+ instructions << { "op" => "ifne", "target" => skip_label }
10696
10712
  end
10697
10713
  end
10698
10714
 
@@ -10933,7 +10949,10 @@ module Konpeito
10933
10949
  instructions << { "op" => "lcmp" }
10934
10950
  instructions << { "op" => "ifne", "target" => found_label }
10935
10951
  else
10936
- instructions << { "op" => "ifnonnull", "target" => found_label }
10952
+ # Use RubyDispatch.isTruthy to handle null (nil) and Boolean.FALSE
10953
+ instructions << { "op" => "invokestatic", "owner" => "konpeito/runtime/RubyDispatch",
10954
+ "name" => "isTruthy", "descriptor" => "(Ljava/lang/Object;)Z" }
10955
+ instructions << { "op" => "ifne", "target" => found_label }
10937
10956
  end
10938
10957
  when :all
10939
10958
  # all?: if falsy → found (false)
@@ -10944,7 +10963,10 @@ module Konpeito
10944
10963
  instructions << { "op" => "lcmp" }
10945
10964
  instructions << { "op" => "ifeq", "target" => found_label }
10946
10965
  else
10947
- instructions << { "op" => "ifnull", "target" => found_label }
10966
+ # Use RubyDispatch.isTruthy to handle null (nil) and Boolean.FALSE
10967
+ instructions << { "op" => "invokestatic", "owner" => "konpeito/runtime/RubyDispatch",
10968
+ "name" => "isTruthy", "descriptor" => "(Ljava/lang/Object;)Z" }
10969
+ instructions << { "op" => "ifeq", "target" => found_label }
10948
10970
  end
10949
10971
  when :none
10950
10972
  # none?: if truthy → found (false)
@@ -10955,7 +10977,10 @@ module Konpeito
10955
10977
  instructions << { "op" => "lcmp" }
10956
10978
  instructions << { "op" => "ifne", "target" => found_label }
10957
10979
  else
10958
- instructions << { "op" => "ifnonnull", "target" => found_label }
10980
+ # Use RubyDispatch.isTruthy to handle null (nil) and Boolean.FALSE
10981
+ instructions << { "op" => "invokestatic", "owner" => "konpeito/runtime/RubyDispatch",
10982
+ "name" => "isTruthy", "descriptor" => "(Ljava/lang/Object;)Z" }
10983
+ instructions << { "op" => "ifne", "target" => found_label }
10959
10984
  end
10960
10985
  end
10961
10986
  end
@@ -11056,10 +11081,18 @@ module Konpeito
11056
11081
  if last_result_var
11057
11082
  last_type = @variable_types[last_result_var] || :value
11058
11083
  instructions << load_instruction(last_result_var, last_type)
11059
- if last_type == :i8
11084
+ case last_type
11085
+ when :i8
11086
+ instructions << { "op" => "ifeq", "target" => skip_label }
11087
+ when :i64
11088
+ instructions << { "op" => "lconst_0" }
11089
+ instructions << { "op" => "lcmp" }
11060
11090
  instructions << { "op" => "ifeq", "target" => skip_label }
11061
11091
  else
11062
- instructions << { "op" => "ifnull", "target" => skip_label }
11092
+ # Use RubyDispatch.isTruthy to correctly handle null (nil) and Boolean.FALSE
11093
+ instructions << { "op" => "invokestatic", "owner" => "konpeito/runtime/RubyDispatch",
11094
+ "name" => "isTruthy", "descriptor" => "(Ljava/lang/Object;)Z" }
11095
+ instructions << { "op" => "ifeq", "target" => skip_label }
11063
11096
  end
11064
11097
  end
11065
11098
 
@@ -11234,7 +11267,7 @@ module Konpeito
11234
11267
  generate_hash_each_inline(receiver, _block_def, result_var)
11235
11268
  end
11236
11269
  when "fetch"
11237
- generate_hash_fetch(receiver, args, result_var)
11270
+ generate_hash_fetch(receiver, args, result_var, _block_def)
11238
11271
  when "merge"
11239
11272
  generate_hash_merge(receiver, args, result_var)
11240
11273
  when "merge!", "update"
@@ -11431,24 +11464,116 @@ module Konpeito
11431
11464
 
11432
11465
  # -- Hash methods --
11433
11466
 
11434
- def generate_hash_fetch(receiver, args, result_var)
11467
+ def generate_hash_fetch(receiver, args, result_var, block_def = nil)
11435
11468
  instructions = []
11436
- instructions.concat(load_khash_receiver(receiver))
11437
- instructions.concat(load_and_box_for_collection(args[0]))
11438
- if args.size > 1
11439
- instructions.concat(load_and_box_for_collection(args[1]))
11469
+ if block_def && args.size == 1
11470
+ # h.fetch(key) { |k| ... } — call block when key is missing
11471
+ @block_counter = (@block_counter || 0) + 1
11472
+ hash_var = "__fetch_hash_#{@block_counter}"
11473
+ key_var = "__fetch_key_#{@block_counter}"
11474
+ val_var = "__fetch_val_#{@block_counter}"
11475
+ found_label = new_label("fetch_found")
11476
+ end_label = new_label("fetch_end")
11477
+ ensure_slot(hash_var, :value)
11478
+ ensure_slot(key_var, :value)
11479
+ ensure_slot(val_var, :value)
11480
+
11481
+ instructions.concat(load_khash_receiver(receiver))
11482
+ instructions << store_instruction(hash_var, :value)
11483
+ @variable_types[hash_var] = :value
11484
+
11485
+ instructions.concat(load_and_box_for_collection(args[0]))
11486
+ instructions << store_instruction(key_var, :value)
11487
+ @variable_types[key_var] = :value
11488
+
11489
+ # val = hash.get(key)
11490
+ instructions << load_instruction(hash_var, :value)
11491
+ instructions << { "op" => "checkcast", "type" => KHASH_CLASS }
11492
+ instructions << load_instruction(key_var, :value)
11493
+ instructions << { "op" => "invokeinterface", "owner" => "java/util/Map",
11494
+ "name" => "get", "descriptor" => "(Ljava/lang/Object;)Ljava/lang/Object;" }
11495
+ instructions << store_instruction(val_var, :value)
11496
+ @variable_types[val_var] = :value
11497
+
11498
+ # if val != null OR hash.containsKey(key) → found
11499
+ instructions << load_instruction(val_var, :value)
11500
+ instructions << { "op" => "ifnonnull", "target" => found_label }
11501
+
11502
+ # also check containsKey for explicit null values
11503
+ instructions << load_instruction(hash_var, :value)
11504
+ instructions << { "op" => "checkcast", "type" => KHASH_CLASS }
11505
+ instructions << load_instruction(key_var, :value)
11506
+ instructions << { "op" => "invokeinterface", "owner" => "java/util/Map",
11507
+ "name" => "containsKey", "descriptor" => "(Ljava/lang/Object;)Z" }
11508
+ instructions << { "op" => "ifne", "target" => found_label }
11509
+
11510
+ # Key not found: execute block with key
11511
+ block_param = block_def.params.first
11512
+ elem_var = block_param ? block_param.name.to_s : "__fetch_blk_k_#{@block_counter}"
11513
+ saved_outer = save_outer_var_for_block_param(elem_var)
11514
+ ensure_slot(elem_var, :value)
11515
+ instructions << load_instruction(key_var, :value)
11516
+ instructions << store_instruction(elem_var, :value)
11517
+ @variable_types[elem_var] = :value
11518
+
11519
+ last_result_var = nil
11520
+ block_def.body.each do |bb|
11521
+ @current_block_label = bb.label.to_s
11522
+ bb.instructions.each do |block_inst|
11523
+ instructions.concat(generate_instruction(block_inst))
11524
+ last_result_var = block_inst.result_var if block_inst.respond_to?(:result_var) && block_inst.result_var
11525
+ end
11526
+ end
11527
+ restore_outer_var_after_block(elem_var, saved_outer)
11528
+
11529
+ if last_result_var
11530
+ last_type = @variable_types[last_result_var] || :value
11531
+ instructions << load_instruction(last_result_var, last_type)
11532
+ case last_type
11533
+ when :i64
11534
+ instructions << { "op" => "invokestatic", "owner" => "java/lang/Long",
11535
+ "name" => "valueOf", "descriptor" => "(J)Ljava/lang/Long;" }
11536
+ when :double
11537
+ instructions << { "op" => "invokestatic", "owner" => "java/lang/Double",
11538
+ "name" => "valueOf", "descriptor" => "(D)Ljava/lang/Double;" }
11539
+ when :i8
11540
+ instructions << { "op" => "invokestatic", "owner" => "java/lang/Boolean",
11541
+ "name" => "valueOf", "descriptor" => "(Z)Ljava/lang/Boolean;" }
11542
+ end
11543
+ else
11544
+ instructions << { "op" => "aconst_null" }
11545
+ end
11546
+ instructions << { "op" => "goto", "target" => end_label }
11547
+
11548
+ # Found: return val
11549
+ instructions << { "op" => "label", "name" => found_label }
11550
+ instructions << load_instruction(val_var, :value)
11551
+
11552
+ instructions << { "op" => "label", "name" => end_label }
11553
+ if result_var
11554
+ ensure_slot(result_var, :value)
11555
+ instructions << store_instruction(result_var, :value)
11556
+ @variable_types[result_var] = :value
11557
+ end
11558
+ instructions
11440
11559
  else
11441
- instructions << { "op" => "aconst_null" }
11442
- end
11443
- instructions << { "op" => "invokevirtual", "owner" => KHASH_CLASS,
11444
- "name" => "fetch",
11445
- "descriptor" => "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;" }
11446
- if result_var
11447
- ensure_slot(result_var, :value)
11448
- instructions << store_instruction(result_var, :value)
11449
- @variable_types[result_var] = :value
11560
+ instructions.concat(load_khash_receiver(receiver))
11561
+ instructions.concat(load_and_box_for_collection(args[0]))
11562
+ if args.size > 1
11563
+ instructions.concat(load_and_box_for_collection(args[1]))
11564
+ else
11565
+ instructions << { "op" => "aconst_null" }
11566
+ end
11567
+ instructions << { "op" => "invokevirtual", "owner" => KHASH_CLASS,
11568
+ "name" => "fetch",
11569
+ "descriptor" => "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;" }
11570
+ if result_var
11571
+ ensure_slot(result_var, :value)
11572
+ instructions << store_instruction(result_var, :value)
11573
+ @variable_types[result_var] = :value
11574
+ end
11575
+ instructions
11450
11576
  end
11451
- instructions
11452
11577
  end
11453
11578
 
11454
11579
  def generate_hash_merge(receiver, args, result_var)
@@ -12307,14 +12432,6 @@ module Konpeito
12307
12432
  generate_string_bytes(receiver, result_var)
12308
12433
  when "replace"
12309
12434
  generate_string_replace(receiver, args, result_var)
12310
- when "freeze"
12311
- # No-op on JVM (strings are immutable), just return the receiver
12312
- generate_string_passthrough(receiver, result_var)
12313
- when "frozen?"
12314
- # On JVM, String#frozen? defaults to true since Java strings are immutable.
12315
- # This means "unfrozen" string tests will fail, but "freeze then frozen?" tests will pass.
12316
- # A proper solution would require wrapping strings in a mutable container.
12317
- generate_string_always_true(result_var)
12318
12435
  when "count"
12319
12436
  generate_string_count(receiver, args, result_var)
12320
12437
  when "tr"
@@ -13687,7 +13804,10 @@ module Konpeito
13687
13804
  instructions << { "op" => "lcmp" }
13688
13805
  instructions << { "op" => "ifeq", "target" => skip_label }
13689
13806
  else
13690
- instructions << { "op" => "ifnull", "target" => skip_label }
13807
+ # Use RubyDispatch.isTruthy to correctly handle null (nil) and Boolean.FALSE
13808
+ instructions << { "op" => "invokestatic", "owner" => "konpeito/runtime/RubyDispatch",
13809
+ "name" => "isTruthy", "descriptor" => "(Ljava/lang/Object;)Z" }
13810
+ instructions << { "op" => "ifeq", "target" => skip_label }
13691
13811
  end
13692
13812
  else
13693
13813
  instructions << { "op" => "goto", "target" => skip_label }