konpeito 0.4.2 → 0.5.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: 18af8ec6ef271ba48955db2f05fc19b70529a318494bced5fe4e2a83febc3544
4
- data.tar.gz: 076eb7d785e5bff312c1ec5b8c093c6fc1db034eabe397d5cc1d32d57438e332
3
+ metadata.gz: 453b0bb32e4ef66b8b6020d262200e070caf01fb483cff4a0ebd6ceba46495b3
4
+ data.tar.gz: 7b38fb2807e1649150a0febae5defcb8f964967a715770db1de857d6409f88b3
5
5
  SHA512:
6
- metadata.gz: 30fe2f9fc64bd7c4d4cff705e8a3e46bef719e947efb6d81418ba51e54ccaad89f5cc6e145fb28a5d84960c864abcc4a284cf2868e81da2b161102619cbaae08
7
- data.tar.gz: 7653068f209f65ae14e1258b0cf17c5b9b3f956a2af006d3af87375eeebc7dcaf694f8384184c8b84e98fedb3485b32ce9374d6cd148ced09bda7e6dbcc228d8
6
+ metadata.gz: afa5fd10db773c8e1bb3c399a39cf140b1c7577271420750368551ee80cab864683d9455860ddba185b0d53cc221df42b5beb3a7334fbc67b56747e4a7bd6e0f
7
+ data.tar.gz: 7fbe956b09d489f9af313c0fa6600f81960cb02cb72d95f912a4d50d9f658b4b7bff35528d43b73c223d011d0aa5c35f6c63bf09ce5f1873c14605cd7274395e
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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
+ ## [Unreleased]
9
+
10
+ ## [0.5.0] - 2026-03-10
11
+
12
+ ### Added
13
+ - **Clay UI layout stdlib**: Flexbox-style UI layout library for the mruby backend. `module Clay` auto-detected like raylib — 40+ `%a{cfunc}` bindings covering lifecycle, element construction, text, borders, scrolling, floating elements, and bulk rendering via the official raylib renderer. Vendored Clay v0.14 under `vendor/clay/`.
14
+ - **Clay UI demo**: `examples/mruby_clay_ui/clay_demo.rb` — sidebar + main content layout with TTF fonts
15
+ - **Memory Match game**: `examples/mruby_clay_ui/memory_game.rb` — card matching game using Clay layout system with module NativeArray game state
16
+ - **Module NativeArray**: fixed-size global arrays shared across functions via RBS module instance variables (`@field: NativeArray[T, N]`). Compiles to LLVM global arrays — no C wrapper file needed. Available on LLVM (CRuby) and mruby backends.
17
+ - **Inline RBS module blocks**: `# @rbs module Foo ... # @rbs end` comment blocks are now extracted by the preprocessor and emitted as raw RBS, enabling module NativeArray declarations without separate `.rbs` files or empty `module Foo; end` stubs.
18
+ - **Space Invaders example rewrite**: `examples/mruby_space_invaders/` rewritten to use module NativeArray with inline RBS — single `.rb` file, no C wrapper or separate `.rbs` needed
19
+ - **Third-party license file**: `THIRD_PARTY_LICENSES.md` summarizing vendored library licenses (yyjson, Clay)
20
+
21
+ ### Fixed
22
+ - **NativeArray GEP stride**: fix `gep` → `gep2` for all NativeArray element access (14 call sites). With LLVM opaque pointers, `gep` used array type as stride instead of element type, causing memory corruption and SIGSEGV crashes.
23
+
8
24
  ## [0.4.2] - 2026-03-10
9
25
 
10
26
  ### Fixed
@@ -0,0 +1,26 @@
1
+ # Third-Party Licenses
2
+
3
+ Konpeito vendors the following third-party libraries. Each library's license
4
+ file is kept alongside its source under `vendor/`.
5
+
6
+ ---
7
+
8
+ ## yyjson
9
+
10
+ - **Location:** `vendor/yyjson/`
11
+ - **License:** MIT
12
+ - **Copyright:** (c) 2020 YaoYuan
13
+ - **URL:** https://github.com/ibireme/yyjson
14
+
15
+ See `vendor/yyjson/LICENSE` for the full license text.
16
+
17
+ ---
18
+
19
+ ## Clay
20
+
21
+ - **Location:** `vendor/clay/`
22
+ - **License:** zlib/libpng
23
+ - **Copyright:** (c) 2024 Nic Barker
24
+ - **URL:** https://github.com/nicbarker/clay
25
+
26
+ See `vendor/clay/LICENSE` for the full license text.
@@ -996,6 +996,13 @@ module Konpeito
996
996
  flags.concat(yyjson_objs)
997
997
  end
998
998
 
999
+ # Add clay object files if Clay stdlib is used
1000
+ clay_used = @rbs_loader&.cfunc_methods&.any? { |k, _| k.start_with?("Clay.") }
1001
+ if clay_used
1002
+ clay_objs = ensure_clay_compiled
1003
+ flags.concat(clay_objs)
1004
+ end
1005
+
999
1006
  if @rbs_loader
1000
1007
  stdlib_ui_dir = File.expand_path("../stdlib/ui", __dir__)
1001
1008
 
@@ -1051,6 +1058,25 @@ module Konpeito
1051
1058
  [yyjson_obj, wrapper_obj]
1052
1059
  end
1053
1060
 
1061
+ # Compile vendored clay.h implementation if Clay stdlib is used
1062
+ # Returns array of object file paths
1063
+ def ensure_clay_compiled
1064
+ clay_dir = File.expand_path("../../../vendor/clay", __dir__)
1065
+ clay_impl_c = File.join(clay_dir, "clay_impl.c")
1066
+ clay_impl_obj = File.join(clay_dir, "clay_impl.o")
1067
+
1068
+ return [] unless File.exist?(clay_impl_c)
1069
+
1070
+ cc = find_llvm_tool("clang") || "cc"
1071
+
1072
+ unless File.exist?(clay_impl_obj) && File.mtime(clay_impl_obj) > File.mtime(clay_impl_c)
1073
+ cmd = [cc, "-c", "-O2", "-fPIC", "-o", clay_impl_obj, clay_impl_c]
1074
+ system(*cmd) or return []
1075
+ end
1076
+
1077
+ [clay_impl_obj]
1078
+ end
1079
+
1054
1080
  def ruby_link_flags
1055
1081
  # Get Ruby's linker flags for extensions
1056
1082
  [
@@ -532,6 +532,16 @@ module Konpeito
532
532
  new_array = transform_value(inst.array, prefix, param_map)
533
533
  HIR::MultiWriteExtract.new(array: new_array, index: inst.index, type: inst.type, result_var: new_result)
534
534
 
535
+ when HIR::ModuleNativeArrayRef
536
+ new_result = inst.result_var ? prefix + inst.result_var : nil
537
+ HIR::ModuleNativeArrayRef.new(
538
+ module_name: inst.module_name,
539
+ field_name: inst.field_name,
540
+ element_type: inst.element_type,
541
+ size: inst.size,
542
+ result_var: new_result
543
+ )
544
+
535
545
  else
536
546
  # For other instructions, just return as-is with renamed result
537
547
  inst
@@ -100,6 +100,9 @@ module Konpeito
100
100
  # Scan HIR to register NativeClassTypes before code generation
101
101
  scan_for_native_class_types(hir_program)
102
102
 
103
+ # Declare LLVM globals for module-level NativeArray fields
104
+ declare_module_native_arrays(hir_program)
105
+
103
106
  # Pre-scan: detect parameters that can receive nil at call sites
104
107
  @nil_possible_params = scan_nil_possible_params(hir_program)
105
108
 
@@ -2182,6 +2185,8 @@ module Konpeito
2182
2185
  generate_sized_queue_push(inst)
2183
2186
  when HIR::SizedQueuePop
2184
2187
  generate_sized_queue_pop(inst)
2188
+ when HIR::ModuleNativeArrayRef
2189
+ generate_module_native_array_ref(inst)
2185
2190
  when HIR::NativeArrayAlloc
2186
2191
  generate_native_array_alloc(inst)
2187
2192
  when HIR::NativeArrayGet
@@ -9841,8 +9846,8 @@ module Konpeito
9841
9846
  # Normalize negative indices
9842
9847
  index_i64 = normalize_native_index(index_i64, inst.receiver)
9843
9848
 
9844
- # GEP to get element pointer
9845
- elem_ptr = @builder.gep(array_ptr, [index_i64], "elem_ptr")
9849
+ # GEP to get element pointer (use gep2 with element type for correct stride)
9850
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [index_i64], "elem_ptr")
9846
9851
 
9847
9852
  # Load element
9848
9853
  elem_value = @builder.load2(llvm_elem_type, elem_ptr, "elem")
@@ -9872,8 +9877,9 @@ module Konpeito
9872
9877
  target_type = element_type == :Int64 ? :i64 : :double
9873
9878
  converted_value = convert_value(store_value, value_type, target_type)
9874
9879
 
9875
- # GEP to get element pointer
9876
- elem_ptr = @builder.gep(array_ptr, [index_i64], "elem_ptr")
9880
+ # GEP to get element pointer (use gep2 with element type for correct stride)
9881
+ llvm_elem_type = native_array_element_llvm_type(element_type)
9882
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [index_i64], "elem_ptr")
9877
9883
 
9878
9884
  # Store element
9879
9885
  @builder.store(converted_value, elem_ptr)
@@ -9893,46 +9899,19 @@ module Konpeito
9893
9899
  len_value = @variables["#{receiver_var}_len"]
9894
9900
  return index_i64 unless len_value
9895
9901
 
9896
- func = @builder.insert_block.parent
9902
+ # Use branchless select instead of conditional branches to avoid
9903
+ # creating new basic blocks that break surrounding PHI node predecessors.
9897
9904
  is_negative = @builder.icmp(:slt, index_i64, LLVM::Int64.from_i(0))
9898
-
9899
- neg_bb = func.basic_blocks.append("idx_neg")
9900
- pos_bb = func.basic_blocks.append("idx_pos")
9901
- merge_bb = func.basic_blocks.append("idx_merge")
9902
-
9903
- @builder.cond(is_negative, neg_bb, pos_bb)
9904
-
9905
- @builder.position_at_end(neg_bb)
9906
9905
  normalized = @builder.add(len_value, index_i64, "neg_idx")
9907
- @builder.br(merge_bb)
9908
-
9909
- @builder.position_at_end(pos_bb)
9910
- @builder.br(merge_bb)
9911
-
9912
- @builder.position_at_end(merge_bb)
9913
- @builder.phi(LLVM::Int64, { neg_bb => normalized, pos_bb => index_i64 })
9906
+ @builder.select(is_negative, normalized, index_i64, "norm_idx")
9914
9907
  end
9915
9908
 
9916
9909
  # Normalize negative index for StaticArray (compile-time known size)
9917
9910
  def normalize_static_index(index_i64, size)
9918
- func = @builder.insert_block.parent
9911
+ # Use branchless select to avoid creating new basic blocks
9919
9912
  is_negative = @builder.icmp(:slt, index_i64, LLVM::Int64.from_i(0))
9920
-
9921
- neg_bb = func.basic_blocks.append("sidx_neg")
9922
- pos_bb = func.basic_blocks.append("sidx_pos")
9923
- merge_bb = func.basic_blocks.append("sidx_merge")
9924
-
9925
- @builder.cond(is_negative, neg_bb, pos_bb)
9926
-
9927
- @builder.position_at_end(neg_bb)
9928
9913
  normalized = @builder.add(LLVM::Int64.from_i(size), index_i64, "neg_sidx")
9929
- @builder.br(merge_bb)
9930
-
9931
- @builder.position_at_end(pos_bb)
9932
- @builder.br(merge_bb)
9933
-
9934
- @builder.position_at_end(merge_bb)
9935
- @builder.phi(LLVM::Int64, { neg_bb => normalized, pos_bb => index_i64 })
9914
+ @builder.select(is_negative, normalized, index_i64, "norm_sidx")
9936
9915
  end
9937
9916
 
9938
9917
  # Generate NativeArray length access: arr.length
@@ -9990,7 +9969,7 @@ module Konpeito
9990
9969
  @builder.position_at_end(loop_body)
9991
9970
 
9992
9971
  # Get element via GEP + load (no rb_ary_entry!)
9993
- elem_ptr = @builder.gep(array_ptr, [current_idx], "elem_ptr")
9972
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [current_idx], "elem_ptr")
9994
9973
  elem_value = @builder.load2(llvm_elem_type, elem_ptr, "elem")
9995
9974
 
9996
9975
  # Set up block parameter (unboxed!)
@@ -10040,7 +10019,7 @@ module Konpeito
10040
10019
  convert_value(val, val_type, type_tag)
10041
10020
  else
10042
10021
  # Use first element as initial, start from index 1
10043
- elem_ptr = @builder.gep(array_ptr, [LLVM::Int64.from_i(0)], "first_ptr")
10022
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [LLVM::Int64.from_i(0)], "first_ptr")
10044
10023
  @builder.load2(llvm_elem_type, elem_ptr, "first_elem")
10045
10024
  end
10046
10025
 
@@ -10072,7 +10051,7 @@ module Konpeito
10072
10051
 
10073
10052
  # Load accumulator and element (both unboxed)
10074
10053
  acc_value = @builder.load2(llvm_elem_type, acc_alloca, "acc")
10075
- elem_ptr = @builder.gep(array_ptr, [current_idx], "elem_ptr")
10054
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [current_idx], "elem_ptr")
10076
10055
  elem_value = @builder.load2(llvm_elem_type, elem_ptr, "elem")
10077
10056
 
10078
10057
  # Set up block parameters
@@ -10172,7 +10151,7 @@ module Konpeito
10172
10151
  @builder.position_at_end(loop_body)
10173
10152
 
10174
10153
  # Load element (unboxed)
10175
- elem_ptr = @builder.gep(array_ptr, [current_idx], "elem_ptr")
10154
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [current_idx], "elem_ptr")
10176
10155
  elem_value = @builder.load2(llvm_elem_type, elem_ptr, "elem")
10177
10156
 
10178
10157
  # Set up block parameter
@@ -10255,7 +10234,7 @@ module Konpeito
10255
10234
  @builder.position_at_end(loop_body)
10256
10235
 
10257
10236
  # Load element (unboxed)
10258
- elem_ptr = @builder.gep(array_ptr, [current_idx], "elem_ptr")
10237
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [current_idx], "elem_ptr")
10259
10238
  elem_value = @builder.load2(llvm_elem_type, elem_ptr, "elem")
10260
10239
 
10261
10240
  # Set up block parameter
@@ -10353,7 +10332,7 @@ module Konpeito
10353
10332
  @builder.position_at_end(loop_body)
10354
10333
 
10355
10334
  # Load element and store to alloca for later use
10356
- elem_ptr = @builder.gep(array_ptr, [current_idx], "elem_ptr")
10335
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [current_idx], "elem_ptr")
10357
10336
  elem_value = @builder.load2(llvm_elem_type, elem_ptr, "elem")
10358
10337
  @builder.store(elem_value, elem_alloca)
10359
10338
 
@@ -10445,7 +10424,7 @@ module Konpeito
10445
10424
  @builder.position_at_end(loop_body)
10446
10425
 
10447
10426
  # Load element
10448
- elem_ptr = @builder.gep(array_ptr, [current_idx], "elem_ptr")
10427
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [current_idx], "elem_ptr")
10449
10428
  elem_value = @builder.load2(llvm_elem_type, elem_ptr, "elem")
10450
10429
 
10451
10430
  # Set up block parameter
@@ -10553,7 +10532,7 @@ module Konpeito
10553
10532
 
10554
10533
  # Load accumulator and element
10555
10534
  acc_value = @builder.load2(llvm_elem_type, acc_alloca, "acc")
10556
- elem_ptr = @builder.gep(array_ptr, [current_idx], "elem_ptr")
10535
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [current_idx], "elem_ptr")
10557
10536
  elem_value = @builder.load2(llvm_elem_type, elem_ptr, "elem")
10558
10537
 
10559
10538
  # Add (unboxed)
@@ -10614,7 +10593,7 @@ module Konpeito
10614
10593
  # Non-empty: initialize with first element
10615
10594
  @builder.position_at_end(non_empty_block)
10616
10595
  result_alloca = @builder.alloca(llvm_elem_type, "na_minmax_result")
10617
- first_ptr = @builder.gep(array_ptr, [LLVM::Int64.from_i(0)], "first_ptr")
10596
+ first_ptr = @builder.gep2(llvm_elem_type, array_ptr, [LLVM::Int64.from_i(0)], "first_ptr")
10618
10597
  first_elem = @builder.load2(llvm_elem_type, first_ptr, "first")
10619
10598
  @builder.store(first_elem, result_alloca)
10620
10599
 
@@ -10634,7 +10613,7 @@ module Konpeito
10634
10613
 
10635
10614
  # Load current result and element
10636
10615
  current_result = @builder.load2(llvm_elem_type, result_alloca, "current")
10637
- elem_ptr = @builder.gep(array_ptr, [current_idx], "elem_ptr")
10616
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [current_idx], "elem_ptr")
10638
10617
  elem_value = @builder.load2(llvm_elem_type, elem_ptr, "elem")
10639
10618
 
10640
10619
  # Compare and select
@@ -10704,11 +10683,16 @@ module Konpeito
10704
10683
  # Get the array pointer from a receiver HIR value
10705
10684
  def get_native_array_ptr(receiver)
10706
10685
  case receiver
10686
+ when HIR::LoadLocal
10687
+ # LoadLocal may have result_var=nil (e.g., from inliner transform_value).
10688
+ # Use the variable name to look up the array pointer.
10689
+ @variables[receiver.var.name] || (receiver.result_var && @variables[receiver.result_var]) ||
10690
+ raise("NativeArray LoadLocal var '#{receiver.var.name}' not in @variables")
10707
10691
  when HIR::Instruction
10708
10692
  if receiver.result_var
10709
10693
  @variables[receiver.result_var]
10710
10694
  else
10711
- raise "NativeArray receiver has no result_var"
10695
+ raise "NativeArray receiver has no result_var: #{receiver.class}"
10712
10696
  end
10713
10697
  when String
10714
10698
  @variables[receiver]
@@ -10720,6 +10704,8 @@ module Konpeito
10720
10704
  # Get the variable name from a receiver for length lookup
10721
10705
  def get_receiver_var_name(receiver)
10722
10706
  case receiver
10707
+ when HIR::LoadLocal
10708
+ receiver.var.name
10723
10709
  when HIR::Instruction
10724
10710
  receiver.result_var
10725
10711
  when String
@@ -10729,6 +10715,62 @@ module Konpeito
10729
10715
  end
10730
10716
  end
10731
10717
 
10718
+ # ========================================
10719
+ # Module-level NativeArray (LLVM global variables)
10720
+ # ========================================
10721
+
10722
+ # Declare LLVM global arrays for all modules with NativeArray fields
10723
+ def declare_module_native_arrays(hir)
10724
+ @module_native_arrays ||= {}
10725
+
10726
+ hir.modules.each do |module_def|
10727
+ next unless module_def.native_array_fields&.any?
10728
+
10729
+ module_def.native_array_fields.each do |field_name, info|
10730
+ elem_type = info[:element_type]
10731
+ size = info[:size]
10732
+ llvm_elem_type = elem_type == :Int64 ? LLVM::Int64 : LLVM::Double
10733
+
10734
+ global_name = "konpeito_#{module_def.name}_#{field_name}"
10735
+ array_type = LLVM::Type.array(llvm_elem_type, size)
10736
+
10737
+ global = @mod.globals.add(array_type, global_name)
10738
+ global.initializer = LLVM::Constant.null(array_type)
10739
+ global.linkage = :internal
10740
+
10741
+ @module_native_arrays["#{module_def.name}_#{field_name}"] = {
10742
+ global: global,
10743
+ element_type: elem_type,
10744
+ size: size,
10745
+ llvm_elem_type: llvm_elem_type,
10746
+ array_type: array_type
10747
+ }
10748
+ end
10749
+ end
10750
+ end
10751
+
10752
+ # Generate a reference to a module-level NativeArray global
10753
+ def generate_module_native_array_ref(inst)
10754
+ key = "#{inst.module_name}_#{inst.field_name}"
10755
+ info = @module_native_arrays[key]
10756
+ raise "Unknown module NativeArray: #{key}" unless info
10757
+
10758
+ # GEP to get pointer to first element of global array
10759
+ array_ptr = @builder.gep2(
10760
+ info[:array_type], info[:global],
10761
+ [LLVM::Int64.from_i(0), LLVM::Int64.from_i(0)],
10762
+ "#{inst.field_name}_ptr"
10763
+ )
10764
+
10765
+ if inst.result_var
10766
+ @variables[inst.result_var] = array_ptr
10767
+ @variable_types[inst.result_var] = :native_array
10768
+ @variables["#{inst.result_var}_len"] = LLVM::Int64.from_i(inst.size)
10769
+ end
10770
+
10771
+ array_ptr
10772
+ end
10773
+
10732
10774
  # Allocate a NativeArray on the stack
10733
10775
  # Returns a pointer to contiguous memory + stores length
10734
10776
  def generate_native_array_alloc(inst)
@@ -10778,8 +10820,8 @@ module Konpeito
10778
10820
  index_value, index_type = get_value_with_type(inst.index)
10779
10821
  index_i64 = index_type == :i64 ? index_value : @builder.call(@rb_num2long, index_value)
10780
10822
 
10781
- # GEP to get element pointer
10782
- elem_ptr = @builder.gep(array_ptr, [index_i64], "elem_ptr")
10823
+ # GEP to get element pointer (use gep2 with element type for correct stride)
10824
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [index_i64], "elem_ptr")
10783
10825
 
10784
10826
  if native_array_has_class_element?(element_type)
10785
10827
  # For NativeClass elements, return pointer to struct (for field access)
@@ -10820,8 +10862,9 @@ module Konpeito
10820
10862
  target_type = element_type == :Int64 ? :i64 : :double
10821
10863
  converted_value = convert_value(store_value, value_type, target_type)
10822
10864
 
10823
- # GEP to get element pointer
10824
- elem_ptr = @builder.gep(array_ptr, [index_i64], "elem_ptr")
10865
+ # GEP to get element pointer (use gep2 with element type for correct stride)
10866
+ llvm_elem_type = native_array_element_llvm_type(element_type)
10867
+ elem_ptr = @builder.gep2(llvm_elem_type, array_ptr, [index_i64], "elem_ptr")
10825
10868
 
10826
10869
  # Store element
10827
10870
  @builder.store(converted_value, elem_ptr)
@@ -12919,8 +12962,8 @@ module Konpeito
12919
12962
  # Get JSON element at index
12920
12963
  elem_val = @builder.call(@yyjson_arr_get, root, idx, "json_elem")
12921
12964
 
12922
- # Get pointer to NativeClass struct in the array
12923
- struct_ptr = @builder.gep(array_ptr, [idx], "elem_struct_ptr")
12965
+ # Get pointer to NativeClass struct in the array (use gep2 for correct stride)
12966
+ struct_ptr = @builder.gep2(llvm_elem_type, array_ptr, [idx], "elem_struct_ptr")
12924
12967
 
12925
12968
  # Parse fields from JSON object into struct
12926
12969
  fields = element_class.fields
@@ -58,6 +58,10 @@ module Konpeito
58
58
  extra_obj_files << extra_obj
59
59
  end
60
60
 
61
+ # Compile vendored Clay library if used
62
+ clay_objs = ensure_clay_compiled
63
+ extra_obj_files.concat(clay_objs)
64
+
61
65
  obj_files = [obj_file, init_obj_file, helpers_obj_file] + extra_obj_files
62
66
 
63
67
  # Link into standalone executable
@@ -901,6 +905,34 @@ module Konpeito
901
905
  )
902
906
  end
903
907
 
908
+ # Include Clay if used
909
+ clay_used = @extra_c_files.any? { |f| File.basename(f).include?("clay") }
910
+ if clay_used
911
+ sections << license_section(
912
+ "Clay",
913
+ "zlib/libpng",
914
+ "Copyright (c) 2024 Nic Barker",
915
+ "https://github.com/nicbarker/clay",
916
+ <<~ZLIB
917
+ This software is provided 'as-is', without any express or implied warranty.
918
+ In no event will the authors be held liable for any damages arising from the
919
+ use of this software.
920
+
921
+ Permission is granted to anyone to use this software for any purpose,
922
+ including commercial applications, and to alter it and redistribute it freely,
923
+ subject to the following restrictions:
924
+
925
+ 1. The origin of this software must not be misrepresented; you must not claim
926
+ that you wrote the original software. If you use this software in a product,
927
+ an acknowledgment in the product documentation would be appreciated but is
928
+ not required.
929
+ 2. Altered source versions must be plainly marked as such, and must not be
930
+ misrepresented as being the original software.
931
+ 3. This notice may not be removed or altered from any source distribution.
932
+ ZLIB
933
+ )
934
+ end
935
+
904
936
  # Include raylib if linked
905
937
  ffi_libs = @rbs_loader&.all_ffi_libraries || []
906
938
  if ffi_libs.any? { |lib| lib.to_s.include?("raylib") }
@@ -1160,8 +1192,41 @@ module Konpeito
1160
1192
  end
1161
1193
  end
1162
1194
 
1195
+ # Compile vendored clay.h implementation if Clay stdlib is used
1196
+ # Returns array of object file paths
1197
+ def ensure_clay_compiled
1198
+ clay_used = @extra_c_files.any? { |f| File.basename(f).include?("clay") }
1199
+ return [] unless clay_used
1200
+
1201
+ clay_dir = File.expand_path("../../../vendor/clay", __dir__)
1202
+ clay_impl_c = File.join(clay_dir, "clay_impl.c")
1203
+ clay_impl_obj = File.join(clay_dir, "clay_impl.o")
1204
+
1205
+ return [] unless File.exist?(clay_impl_c)
1206
+
1207
+ cc, cc_flags = cross_cc_with_flags
1208
+ cflags = cross_compiling? ? Platform.cross_mruby_cflags(@cross_mruby_dir) : (Platform.mruby_cflags rescue "-O2")
1209
+
1210
+ # Compile clay_impl.c (only if stale)
1211
+ unless File.exist?(clay_impl_obj) && File.mtime(clay_impl_obj) > File.mtime(clay_impl_c)
1212
+ cmd = [*cc, "-c", "-O2"]
1213
+ cmd.concat(cc_flags)
1214
+ cmd += ["-o", clay_impl_obj, clay_impl_c]
1215
+ system(*cmd) or return []
1216
+ end
1217
+
1218
+ [clay_impl_obj]
1219
+ end
1220
+
1163
1221
  def ffi_include_flags
1164
1222
  flags = []
1223
+
1224
+ # Always add vendored Clay include path if Clay stdlib is used
1225
+ clay_dir = File.expand_path("../../../vendor/clay", __dir__)
1226
+ if @extra_c_files.any? { |f| File.basename(f).include?("clay") } && Dir.exist?(clay_dir)
1227
+ flags << "-I#{clay_dir}"
1228
+ end
1229
+
1165
1230
  if cross_compiling?
1166
1231
  # When cross-compiling, use cross library include paths
1167
1232
  flags << "-I#{@cross_libs_dir}/../include" if @cross_libs_dir && Dir.exist?("#{@cross_libs_dir}/../include")
@@ -652,7 +652,8 @@ module Konpeito
652
652
 
653
653
  # Map of stdlib modules: module name pattern => stdlib directory name
654
654
  STDLIB_MODULE_MAP = {
655
- "Raylib" => "raylib"
655
+ "Raylib" => "raylib",
656
+ "Clay" => "clay"
656
657
  }.freeze
657
658
 
658
659
  # Scan AST for known stdlib module references and auto-add their RBS paths
@@ -694,6 +695,7 @@ module Konpeito
694
695
  end
695
696
 
696
697
  # Auto-include stdlib C files based on FFI libraries detected by RBS loader
698
+ # and vendored stdlibs detected by module reference
697
699
  def inject_stdlib_c_files(extra_c_files)
698
700
  return extra_c_files unless @rbs_loader
699
701
 
@@ -716,6 +718,21 @@ module Konpeito
716
718
  end
717
719
  end
718
720
 
721
+ # Also include vendored stdlib C files detected by module reference
722
+ # (for stdlibs without %a{ffi} that use vendored C libraries)
723
+ @rbs_paths.each do |rbs_path|
724
+ STDLIB_MODULE_MAP.each_value do |stdlib_name|
725
+ stdlib_rbs = File.expand_path("stdlib/#{stdlib_name}/#{stdlib_name}.rbs", __dir__)
726
+ next unless rbs_path == stdlib_rbs
727
+
728
+ stdlib_c = File.expand_path("stdlib/#{stdlib_name}/#{stdlib_name}_native.c", __dir__)
729
+ if File.exist?(stdlib_c) && !extra_c_files.include?(stdlib_c)
730
+ extra_c_files << stdlib_c
731
+ log "Auto-including stdlib C wrapper: #{stdlib_name}" if verbose
732
+ end
733
+ end
734
+ end
735
+
719
736
  extra_c_files
720
737
  end
721
738
  end
@@ -43,11 +43,34 @@ module Konpeito
43
43
 
44
44
  def build(typed_ast)
45
45
  visit(typed_ast)
46
+ ensure_module_native_array_defs
46
47
  @program
47
48
  end
48
49
 
49
50
  private
50
51
 
52
+ # Ensure ModuleDefs exist for all modules with NativeArray fields from RBS.
53
+ # When using inline RBS (rbs_inline), the Ruby source may not contain
54
+ # `module Foo; end`, so visit_module is never called and no ModuleDef
55
+ # is created. This method fills in the missing ModuleDefs.
56
+ def ensure_module_native_array_defs
57
+ return unless @rbs_loader
58
+
59
+ @rbs_loader.each_native_module do |module_name, mod_type|
60
+ next unless mod_type.native_array_fields&.any?
61
+ next if @program.modules.any? { |m| m.name == module_name.to_s }
62
+
63
+ module_def = ModuleDef.new(
64
+ name: module_name.to_s,
65
+ methods: [],
66
+ singleton_methods: [],
67
+ constants: {}
68
+ )
69
+ module_def.native_array_fields = mod_type.native_array_fields
70
+ @program.modules << module_def
71
+ end
72
+ end
73
+
51
74
  def visit(typed_node)
52
75
  return nil unless typed_node
53
76
 
@@ -884,6 +907,13 @@ module Konpeito
884
907
 
885
908
  @current_module = old_module
886
909
 
910
+ # Get NativeArray fields from RBS if available
911
+ mod_native_array_fields = {}
912
+ if @rbs_loader
913
+ mod_type = @rbs_loader.native_module_type(name)
914
+ mod_native_array_fields = mod_type.native_array_fields if mod_type
915
+ end
916
+
887
917
  # Merge into existing ModuleDef if this module was already opened (multi-file projects)
888
918
  existing_module_def = @program.modules.find { |m| m.name == name }
889
919
  if existing_module_def
@@ -892,6 +922,7 @@ module Konpeito
892
922
  existing_module_def.module_function_methods.concat(module_function_method_names)
893
923
  existing_module_def.private_methods.merge(module_private_method_names)
894
924
  existing_module_def.constants.merge!(module_constants)
925
+ existing_module_def.native_array_fields.merge!(mod_native_array_fields)
895
926
  else
896
927
  module_def = ModuleDef.new(
897
928
  name: name,
@@ -901,6 +932,7 @@ module Konpeito
901
932
  )
902
933
  module_def.module_function_methods.concat(module_function_method_names)
903
934
  module_def.private_methods.merge(module_private_method_names)
935
+ module_def.native_array_fields = mod_native_array_fields
904
936
  @program.modules << module_def
905
937
  end
906
938
  NilLit.new
@@ -1798,6 +1830,11 @@ module Konpeito
1798
1830
  def visit_call(typed_node)
1799
1831
  node = typed_node.node
1800
1832
 
1833
+ # Check for module-level NativeArray field access (e.g., Inv.gs)
1834
+ if (mod_arr_info = module_native_array_access?(typed_node))
1835
+ return visit_module_native_array_ref(mod_arr_info)
1836
+ end
1837
+
1801
1838
  # Check for NativeArray.new(size) pattern
1802
1839
  if native_array_new_call?(typed_node)
1803
1840
  return visit_native_array_new(typed_node)
@@ -2366,6 +2403,40 @@ module Konpeito
2366
2403
  receiver_child.node.name.to_s == "NativeArray"
2367
2404
  end
2368
2405
 
2406
+ # Check if this is a module-level NativeArray field access (e.g., Inv.gs)
2407
+ # Returns field info hash or nil
2408
+ def module_native_array_access?(typed_node)
2409
+ return nil unless @rbs_loader
2410
+
2411
+ # Receiver must be a constant read (e.g., Inv)
2412
+ receiver_child = typed_node.children.first
2413
+ return nil unless receiver_child&.node_type == :constant_read
2414
+
2415
+ module_name = receiver_child.node.name.to_s
2416
+ mod_type = @rbs_loader.native_module_type(module_name)
2417
+ return nil unless mod_type
2418
+
2419
+ field_name = typed_node.node.name.to_s.to_sym
2420
+ field_info = mod_type.lookup_native_array_field(field_name)
2421
+ return nil unless field_info
2422
+
2423
+ { module_name: module_name, field_name: field_name, **field_info }
2424
+ end
2425
+
2426
+ # Emit a ModuleNativeArrayRef for module-level NativeArray access
2427
+ def visit_module_native_array_ref(info)
2428
+ result_var = new_temp_var
2429
+ inst = ModuleNativeArrayRef.new(
2430
+ module_name: info[:module_name],
2431
+ field_name: info[:field_name],
2432
+ element_type: info[:element_type],
2433
+ size: info[:size],
2434
+ result_var: result_var
2435
+ )
2436
+ emit(inst)
2437
+ inst
2438
+ end
2439
+
2369
2440
  # Check if this is a NativeArray element access: arr[i] where arr is NativeArray[NativeClass]
2370
2441
  def native_array_element_access?(typed_node)
2371
2442
  method_name = typed_node.node.name.to_s