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 +4 -4
- data/CHANGELOG.md +11 -0
- data/Rakefile +15 -0
- data/lib/konpeito/codegen/inliner.rb +31 -0
- data/lib/konpeito/codegen/jvm_generator.rb +162 -12
- data/lib/konpeito/codegen/llvm_generator.rb +129 -13
- data/lib/konpeito/codegen/monomorphizer.rb +31 -0
- data/lib/konpeito/type_checker/hm_inferrer.rb +59 -9
- data/lib/konpeito/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b3a19c0259a27b1d396334b57f827f784b5aeebe022d714fb68a5b8e40537f8
|
|
4
|
+
data.tar.gz: a248d9e0166ef1643bb5e0a15922432cdcbd7c6758dcc6c97aa1ecebd36c48a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
486
|
-
type =
|
|
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 =
|
|
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 =
|
|
3123
|
-
|
|
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
|
-
#
|
|
3343
|
-
|
|
3344
|
-
instructions << { "op" => "
|
|
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 =
|
|
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
|
-
#
|
|
723
|
-
@rb_eArgumentError = @mod.globals.add(value_type, "
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
7759
|
-
|
|
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
|
-
|
|
7762
|
-
|
|
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
|
|
2072
|
-
|
|
2073
|
-
|
|
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;
|
|
2080
|
-
|
|
2081
|
-
|
|
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:
|
|
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
|
-
|
|
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)
|
data/lib/konpeito/version.rb
CHANGED