konpeito 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d4e25daf950c4a2c5c60e4a646f4c0f74a959e1557761feb3327245ec995bee
4
- data.tar.gz: f0e5571b58a2316a729c15872be3e0bfa9cba03573f96f51b79e4b384e2af675
3
+ metadata.gz: 945682bc2b93e30dbea1e57f3d05b1613c780b8301d1b66fe6b763191727ca64
4
+ data.tar.gz: 38f644805520a6885c5d583f499fbec84eca513d48c9ca0a7350e469821a8587
5
5
  SHA512:
6
- metadata.gz: 6c7fd35077041ff22737b3b72a101771f597ebe813a61fbc47b2c5bd29245afd4df77d60d1a0fef50005631fbed9932bb6d9599a9f5d50ed8f833a3691157a36
7
- data.tar.gz: 539a02d11cebfc1ad35e0df912fa49043aa8f4cd310088ee5c2f419d6cd14c59a1ed5ec603fa7974fb947f95a6c42d390516f54849d727400707cb218a52484b
6
+ metadata.gz: 92ad6de982c1d40d9fd821899a32fdde41b5aa3d3bfc618f02feff51f9e8a26ff61b24173f54ef4a0781035ec75898cdb9e2dafec584d06d5f94e8b60f166e1f
7
+ data.tar.gz: 477c494b53513b16f99de04a91410cdea9d8a30c64b5e14d8f436cae1d14fc48323948c9d42bf4791bdfa3e77e7cffca6e856fe25c9d5a39f60b523134a3f27c
data/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ 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.10.0] - 2026-03-18
9
+
10
+ ### Added
11
+ - **mruby 4 RC2 support**: Version detection (`mruby_version`, `mruby_major_version`
12
+ in Platform), C-level compatibility guards (`KONPEITO_MRUBY4` macro,
13
+ `_Static_assert(sizeof(mrb_value)==8)`, `MRB_NO_BOXING` error), runtime version
14
+ check in MRubyBackend, mruby version display in `konpeito doctor`, and CI matrix
15
+ for mruby 3.x / 4.x
16
+ - **User-defined generics**: RBS `class Stack[T]` type parameters extracted and
17
+ propagated through HIR ClassDef, HM inferrer (fresh TypeVars on `.new`), and
18
+ monomorphizer (class-level specialization generating `Stack_Integer` etc.)
19
+ - **Cosmos demoscene demo** (`examples/mruby_cosmos/`): Procedural graphics showcase
20
+ with sine lookup table, starfield, orbital particles, glowing nebula, rotating
21
+ wireframes, pulse rings, aurora wave, and CRT scanlines
22
+
23
+ ### Changed
24
+ - mruby version requirement updated from "3.x" to "3.x or 4.x" in docs
25
+ - Monomorphizer `RBS_TYPE_PARAMS` check replaced with dynamic `unresolved_type_param?`
26
+ that includes user-defined class type parameters
27
+
8
28
  ## [0.9.1] - 2026-03-16
9
29
 
10
30
  ### Fixed
@@ -411,6 +431,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
411
431
  - `%a{extern}` - external C struct wrappers
412
432
  - `%a{simd}` - SIMD vectorization
413
433
 
434
+ [0.10.0]: https://github.com/i2y/konpeito/compare/v0.9.1...v0.10.0
414
435
  [0.9.1]: https://github.com/i2y/konpeito/compare/v0.9.0...v0.9.1
415
436
  [0.9.0]: https://github.com/i2y/konpeito/compare/v0.8.0...v0.9.0
416
437
  [0.8.0]: https://github.com/i2y/konpeito/compare/v0.7.1...v0.8.0
@@ -189,6 +189,15 @@ module Konpeito
189
189
  hint: "Install: #{Platform.mruby_install_hint}"
190
190
  }
191
191
 
192
+ # mruby version
193
+ mruby_ver = Platform.mruby_version
194
+ checks << {
195
+ name: "mruby version",
196
+ detail: mruby_ver || "unknown",
197
+ status: mruby_ver ? :ok : :warning,
198
+ hint: "Supported: mruby 3.x or 4.x"
199
+ }
200
+
192
201
  # mruby headers
193
202
  cflags = Platform.mruby_cflags
194
203
  inc_dir = cflags.split.find { |f| f.start_with?("-I") }&.sub(/^-I/, "")
@@ -14,7 +14,7 @@ module Konpeito
14
14
  # identity("hello") # generates identity_String
15
15
  #
16
16
  class Monomorphizer
17
- attr_reader :specializations, :call_sites, :union_dispatches
17
+ attr_reader :specializations, :call_sites, :union_dispatches, :class_specializations
18
18
 
19
19
  def initialize(hir_program, type_info)
20
20
  @hir_program = hir_program
@@ -24,6 +24,7 @@ module Konpeito
24
24
  @union_call_sites = [] # Calls requiring runtime dispatch
25
25
  @union_dispatches = {} # { [func_name, original_types] => dispatch_info }
26
26
  @generated_functions = {}
27
+ @class_specializations = {} # { [class_name, type_args_strs] => specialized_class_name }
27
28
  end
28
29
 
29
30
  # Analyze the HIR program to find monomorphization opportunities
@@ -34,6 +35,9 @@ module Konpeito
34
35
 
35
36
  # Determine which specializations to generate
36
37
  determine_specializations
38
+
39
+ # Collect and generate class-level specializations for generic classes
40
+ analyze_class_specializations
37
41
  end
38
42
 
39
43
  # Apply monomorphization transformations
@@ -41,6 +45,9 @@ module Konpeito
41
45
  # Generate specialized function copies
42
46
  generate_specialized_functions
43
47
 
48
+ # Generate specialized class definitions
49
+ generate_specialized_classes
50
+
44
51
  # Rewrite call sites to use specialized functions
45
52
  rewrite_call_sites
46
53
  end
@@ -197,7 +204,7 @@ module Konpeito
197
204
  next if types.any? { |t| t == TypeChecker::Types::UNTYPED || t.is_a?(TypeChecker::Types::Untyped) }
198
205
  next if skip_functions.include?(func_name.to_s)
199
206
  # Skip if any type is an unresolved RBS type parameter (Elem, K, V, etc.)
200
- next if types.any? { |t| t.is_a?(TypeChecker::Types::ClassInstance) && RBS_TYPE_PARAMS.include?(t.name) }
207
+ next if types.any? { |t| t.is_a?(TypeChecker::Types::ClassInstance) && unresolved_type_param?(t.name) }
201
208
 
202
209
  type_suffix = types.map { |t| type_to_suffix(t) }.join("_")
203
210
  specialized = "#{func_name}_#{type_suffix}"
@@ -329,15 +336,27 @@ module Konpeito
329
336
  end
330
337
 
331
338
  # RBS type parameter names that should not be used as monomorphized suffixes
332
- # These are unresolved generic type variables, not concrete Ruby classes
333
- RBS_TYPE_PARAMS = Set.new(%w[Elem K V U T S R E A B C D N M].map(&:to_sym)).freeze
339
+ # These are unresolved generic type variables, not concrete Ruby classes.
340
+ # User-defined type params (from RBS class declarations) are also checked dynamically.
341
+ BUILTIN_RBS_TYPE_PARAMS = Set.new(%w[Elem K V U T S R E A B C D N M].map(&:to_sym)).freeze
342
+
343
+ def unresolved_type_param?(type_name)
344
+ return true if BUILTIN_RBS_TYPE_PARAMS.include?(type_name)
345
+ # Check user-defined class type params
346
+ if @type_info.respond_to?(:class_type_params)
347
+ @type_info.class_type_params.each_value do |param_map|
348
+ return true if param_map.key?(type_name)
349
+ end
350
+ end
351
+ false
352
+ end
334
353
 
335
354
  def type_to_suffix(type)
336
355
  case type
337
356
  when TypeChecker::Types::ClassInstance
338
357
  # If the type name is an unresolved RBS type parameter (Elem, K, V, etc.),
339
358
  # treat it as untyped to avoid generating rb_const_get("Elem") at runtime
340
- if RBS_TYPE_PARAMS.include?(type.name)
359
+ if unresolved_type_param?(type.name)
341
360
  "Any"
342
361
  else
343
362
  type.name.to_s
@@ -422,6 +441,96 @@ module Konpeito
422
441
  term
423
442
  end
424
443
 
444
+ # Analyze generic classes and collect concrete type instantiations from .new call sites
445
+ def analyze_class_specializations
446
+ generic_classes = @hir_program.classes.select { |c| c.type_params && !c.type_params.empty? }
447
+ return if generic_classes.empty?
448
+
449
+ generic_names = generic_classes.map(&:name).to_set
450
+
451
+ # Scan all functions for ClassName.new calls that produce typed ClassInstance
452
+ @hir_program.functions.each do |func|
453
+ func.body.each do |block|
454
+ block.instructions.each do |inst|
455
+ next unless inst.is_a?(HIR::Call) && inst.method_name.to_s == "new"
456
+ next unless inst.type.is_a?(TypeChecker::Types::ClassInstance)
457
+ next unless generic_names.include?(inst.type.name.to_s)
458
+
459
+ type_args = inst.type.type_args
460
+ next if type_args.empty?
461
+
462
+ # Resolve type args through unifier
463
+ resolved_args = type_args.map { |ta| @type_info.unifier.apply(ta) }
464
+ # Skip if any arg is still unresolved
465
+ next if resolved_args.any? { |ta| ta.is_a?(TypeChecker::TypeVar) || ta == TypeChecker::Types::UNTYPED }
466
+
467
+ class_name = inst.type.name.to_s
468
+ arg_strs = resolved_args.map(&:to_s)
469
+ key = [class_name, arg_strs]
470
+ next if @class_specializations.key?(key)
471
+
472
+ suffix = resolved_args.map { |t| type_to_suffix(t) }.join("_")
473
+ @class_specializations[key] = "#{class_name}_#{suffix}"
474
+ end
475
+ end
476
+ end
477
+ end
478
+
479
+ # Generate specialized ClassDef and methods for each class instantiation
480
+ def generate_specialized_classes
481
+ @class_specializations.each do |(class_name, type_arg_strs), specialized_name|
482
+ original_class = @hir_program.classes.find { |c| c.name == class_name }
483
+ next unless original_class
484
+
485
+ # Build substitution: type_param_name → concrete type string
486
+ substitution = {}
487
+ original_class.type_params.each_with_index do |tp, i|
488
+ substitution[tp.to_s] = type_arg_strs[i]
489
+ end
490
+
491
+ # Create specialized ClassDef with concrete ivar types
492
+ specialized_class = HIR::ClassDef.new(
493
+ name: specialized_name,
494
+ superclass: original_class.superclass,
495
+ method_names: original_class.method_names.dup,
496
+ instance_vars: original_class.instance_vars.dup,
497
+ included_modules: original_class.included_modules.dup
498
+ )
499
+ specialized_class.type_params = [] # Concrete — no type params
500
+
501
+ # Substitute ivar types
502
+ original_class.instance_var_types.each do |ivar_name, field_tag|
503
+ specialized_class.instance_var_types[ivar_name] =
504
+ if substitution.key?(field_tag.to_s)
505
+ substitution[field_tag.to_s].to_sym
506
+ else
507
+ field_tag
508
+ end
509
+ end
510
+
511
+ @hir_program.classes << specialized_class
512
+
513
+ # Copy and rename methods
514
+ original_class.method_names.each do |method_name|
515
+ original_func = @hir_program.functions.find do |f|
516
+ f.owner_class == class_name && f.name.to_s == method_name
517
+ end
518
+ next unless original_func
519
+
520
+ specialized_body = deep_clone_blocks(original_func.body)
521
+ specialized_func = HIR::Function.new(
522
+ name: method_name,
523
+ params: original_func.params.dup,
524
+ body: specialized_body,
525
+ return_type: original_func.return_type,
526
+ is_instance_method: original_func.is_instance_method,
527
+ owner_class: specialized_name
528
+ )
529
+ @hir_program.functions << specialized_func
530
+ end
531
+ end
532
+ end
533
+
425
534
  def rewrite_call_sites
426
535
  # Track which calls have already been processed (for Union calls that appear multiple times)
427
536
  processed_calls = Set.new
@@ -31,7 +31,21 @@ module Konpeito
31
31
  !!@cross_target
32
32
  end
33
33
 
34
+ # Check mruby version compatibility. Raises for unsupported versions, warns for future versions.
35
+ def check_mruby_compatibility
36
+ major = Platform.mruby_major_version
37
+ return unless major # unknown version — skip check
38
+
39
+ if major < 3
40
+ raise "mruby #{Platform.mruby_version} is not supported. Konpeito requires mruby 3.x or 4.x."
41
+ elsif major > 4
42
+ warn "Warning: mruby #{Platform.mruby_version} is newer than tested. Konpeito is tested with mruby 3.x and 4.x."
43
+ end
44
+ end
45
+
34
46
  def generate
47
+ check_mruby_compatibility unless cross_compiling?
48
+
35
49
  ir_file = "#{output_base}.ll"
36
50
  obj_file = "#{output_base}.o"
37
51
  init_c_file = "#{output_base}_mruby_init.c"
@@ -14,6 +14,7 @@
14
14
 
15
15
  #include <mruby.h>
16
16
  #include <mruby/value.h>
17
+ #include <mruby/version.h>
17
18
  #include <mruby/class.h>
18
19
  #include <mruby/data.h>
19
20
  #include <mruby/string.h>
@@ -29,6 +30,26 @@
29
30
  #include <stdarg.h>
30
31
  #include <stdint.h>
31
32
 
33
+ /* ================================================================
34
+ * mruby version compatibility
35
+ * ================================================================ */
36
+
37
+ /* Detect mruby 4.x */
38
+ #if defined(MRUBY_RELEASE_MAJOR) && MRUBY_RELEASE_MAJOR >= 4
39
+ #define KONPEITO_MRUBY4 1
40
+ #else
41
+ #define KONPEITO_MRUBY4 0
42
+ #endif
43
+
44
+ /* Reject boxing modes where mrb_value is not 64-bit.
45
+ * Konpeito LLVM IR treats mrb_value as i64 — this must hold. */
46
+ #ifdef MRB_NO_BOXING
47
+ #error "Konpeito requires mrb_value to be 64-bit (NaN-boxing or word-boxing). MRB_NO_BOXING is not supported."
48
+ #endif
49
+
50
+ _Static_assert(sizeof(mrb_value) == sizeof(uint64_t),
51
+ "Konpeito requires sizeof(mrb_value) == 8 (64-bit)");
52
+
32
53
  /* ================================================================
33
54
  * Global mrb_state pointer (set by main() in generated init code)
34
55
  * All CRuby-compatible wrappers below use this to access mruby.
@@ -40,6 +61,15 @@ mrb_state *konpeito_get_mrb_state(void) {
40
61
  return konpeito_mrb_state;
41
62
  }
42
63
 
64
+ /* Return the mruby version description string */
65
+ const char *konpeito_mruby_version(void) {
66
+ #ifdef MRUBY_DESCRIPTION
67
+ return MRUBY_DESCRIPTION;
68
+ #else
69
+ return "mruby (unknown version)";
70
+ #endif
71
+ }
72
+
43
73
  /* ================================================================
44
74
  * Block stack for rb_yield / rb_block_given_p / rb_block_proc
45
75
  *
@@ -450,6 +450,11 @@ module Konpeito
450
450
  )
451
451
  # Mark as reopened if it's a known Ruby core class
452
452
  class_def.reopened = true if RUBY_CORE_CLASSES.include?(name)
453
+ # Propagate type_params from RBS (e.g., class Stack[T])
454
+ if @rbs_loader&.respond_to?(:user_class_type_params)
455
+ tp = @rbs_loader.user_class_type_params[name]
456
+ class_def.type_params = tp if tp
457
+ end
453
458
  end
454
459
 
455
460
  # Visit body within class context
@@ -65,6 +65,7 @@ module Konpeito
65
65
  attr_accessor :instance_var_types # HM-inferred ivar types: { "name" => :Integer, "age" => :String, ... }
66
66
  attr_accessor :body_constants # Array of [name, value_node] for constants defined in class body
67
67
  attr_accessor :body_class_vars # Array of [name, value_node] for class variables initialized in class body
68
+ attr_accessor :type_params # Array of type parameter names (e.g., [:T] for class Stack[T])
68
69
 
69
70
  def initialize(name:, superclass: nil, method_names: [], instance_vars: [], included_modules: [], extended_modules: [], prepended_modules: [])
70
71
  super(type: TypeChecker::Types::NIL)
@@ -83,6 +84,7 @@ module Konpeito
83
84
  @instance_var_types = {}
84
85
  @body_constants = []
85
86
  @body_class_vars = []
87
+ @type_params = []
86
88
  end
87
89
  end
88
90
 
@@ -145,6 +145,47 @@ module Konpeito
145
145
  File.exist?(File.join(inc_dir, "mruby.h"))
146
146
  end
147
147
 
148
+ # Get mruby version string (e.g., "3.4.0" or "4.0.0-rc2")
149
+ def self.mruby_version
150
+ # Try mruby-config --version first (mruby 4.x supports this)
151
+ config = find_mruby_config
152
+ if config
153
+ version = `#{config} --version 2>/dev/null`.strip
154
+ # Only accept if it looks like a version string (not help text)
155
+ return version if version.match?(/\A\d+\.\d+/)
156
+ end
157
+
158
+ # Fallback: parse mruby/version.h
159
+ version_h = find_mruby_version_header
160
+ if version_h
161
+ content = File.read(version_h)
162
+ major = content[/MRUBY_RELEASE_MAJOR\s+(\d+)/, 1]
163
+ minor = content[/MRUBY_RELEASE_MINOR\s+(\d+)/, 1]
164
+ patch = content[/MRUBY_RELEASE_TEENY\s+(\d+)/, 1]
165
+ return "#{major}.#{minor}.#{patch}" if major
166
+ end
167
+
168
+ nil
169
+ end
170
+
171
+ # Find mruby/version.h from include paths
172
+ def self.find_mruby_version_header
173
+ cflags = mruby_cflags
174
+ cflags.split.select { |f| f.start_with?("-I") }.each do |flag|
175
+ inc_dir = flag.sub(/^-I/, "")
176
+ path = File.join(inc_dir, "mruby", "version.h")
177
+ return path if File.exist?(path)
178
+ end
179
+ nil
180
+ end
181
+
182
+ # Get mruby major version number (e.g., 3 or 4)
183
+ def self.mruby_major_version
184
+ version = mruby_version
185
+ return nil unless version
186
+ version.split(".").first.to_i
187
+ end
188
+
148
189
  # Find zig compiler (used for cross-compilation)
149
190
  def self.find_zig
150
191
  find_executable("zig")
@@ -9,7 +9,7 @@ module Konpeito
9
9
  # Uses Algorithm W with constraint generation and unification
10
10
  class HMInferrer
11
11
  attr_reader :errors, :unifier, :node_types, :diagnostics, :ivar_types, :inference_errors,
12
- :unresolved_type_warnings
12
+ :unresolved_type_warnings, :class_type_params
13
13
 
14
14
  def initialize(rbs_loader = nil, file_path: nil, source: nil)
15
15
  @rbs_loader = rbs_loader
@@ -35,6 +35,7 @@ module Konpeito
35
35
  @keyword_param_vars = {} # func_key => { :param_name => TypeVar }
36
36
  @unresolved_type_warnings = [] # Warnings for types that survived inference
37
37
  @polymorphic_methods = {} # qualified_key => TypeScheme for instance methods
38
+ @class_type_params = {} # class_name => { :T => TypeVar, ... } for generic classes
38
39
  end
39
40
 
40
41
  # Main entry: infer types for a program AST
@@ -903,6 +904,22 @@ module Konpeito
903
904
  if init_type
904
905
  unify_call_args(init_type, arg_types)
905
906
  end
907
+
908
+ # For generic classes (e.g., class Stack[T] in RBS), create fresh TypeVars
909
+ # so that Stack.new → Stack[T'] where T' is unified from usage
910
+ class_name_str = receiver_type.name.to_s
911
+ rbs_type_params = @rbs_loader&.respond_to?(:user_class_type_params) &&
912
+ @rbs_loader.user_class_type_params[class_name_str]
913
+ if rbs_type_params && !rbs_type_params.empty?
914
+ fresh_vars = rbs_type_params.map { TypeVar.new }
915
+ # Store mapping so instance method resolution can build substitution
916
+ @class_type_params[class_name_str] ||= {}
917
+ rbs_type_params.each_with_index do |tp_name, i|
918
+ @class_type_params[class_name_str][tp_name] = fresh_vars[i]
919
+ end
920
+ return Types::ClassInstance.new(receiver_type.name, fresh_vars)
921
+ end
922
+
906
923
  return Types::ClassInstance.new(receiver_type.name)
907
924
  end
908
925
 
@@ -9,7 +9,7 @@ module Konpeito
9
9
  # Loads and manages RBS type definitions
10
10
  class RBSLoader
11
11
  attr_reader :environment, :native_classes, :native_modules, :boxed_classes, :cfunc_methods, :ffi_libraries,
12
- :extern_classes, :simd_classes, :jvm_classes
12
+ :extern_classes, :simd_classes, :jvm_classes, :user_class_type_params
13
13
 
14
14
  def initialize
15
15
  @environment = nil
@@ -22,6 +22,7 @@ module Konpeito
22
22
  @extern_classes = {} # class_name -> ExternClassType (external C struct wrappers)
23
23
  @simd_classes = {} # class_name -> SIMDClassType (SIMD vector classes)
24
24
  @jvm_classes = {} # "Java::Util::ArrayList" -> { jvm_internal_name:, methods:, static_methods: }
25
+ @user_class_type_params = {} # "Stack" => [:T], "Pair" => [:A, :B]
25
26
  @rbs_file_contents = {} # path -> content
26
27
  @temp_dirs = [] # Temp directories for single RBS files
27
28
  @definition_builder = nil # Cached DefinitionBuilder
@@ -574,6 +575,11 @@ module Konpeito
574
575
  return
575
576
  end
576
577
 
578
+ # Extract type parameters (e.g., class Stack[T] → [:T])
579
+ if decl.type_params && !decl.type_params.empty?
580
+ @user_class_type_params[class_name.to_s] = decl.type_params.map { |tp| tp.name.to_sym }
581
+ end
582
+
577
583
  annotations = AnnotationParser.parse_all(decl.annotations)
578
584
 
579
585
  # Skip built-in types
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Konpeito
4
- VERSION = "0.9.1"
4
+ VERSION = "0.10.0"
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.9.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yasushi Itoh