konpeito 0.6.0 → 0.7.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: c6e49cc678968c9afbf41e3483ce19ac9d0f506bf18b31def5de3468fdef18b3
4
- data.tar.gz: f6f85c8b6d646c56ace6b35338ea66adeabc53eec3a21b50b0953346145b0d59
3
+ metadata.gz: 50ad4b5d35d1b8abd26ee9d941a901b99e281eb9cc7d2deed777f3ebf12b2308
4
+ data.tar.gz: fc91a16a60718a8a910776ab56f01a09d49ec2ee78e71082965392f1dee332e1
5
5
  SHA512:
6
- metadata.gz: cc766128892195b93c5acf173b7a84c3ee86415932b9d477a952a824cb9167b15c7e63b74ab894dcd7e8213e731b5c6b2dfd1f2ef829c2e380583d7e85f1536b
7
- data.tar.gz: 692f32eee6feb583b24b7f468fbd659c784f01c41ecde853272966c13bdda0032c47e8d55d0d0f0d084dd9493f0df50d9f0f483245f7d9f1a8ef98f43bbb6c70
6
+ metadata.gz: c2efa11a609aba145beef6d2b722028f657a6a4d5469864241cbdde1b43dc494b9cf814379788a8ede8d4715d0aceab76df3d64a1c6b14913a3e2f84d0787f1f
7
+ data.tar.gz: 77860386e45ac9a5b78ead9b0f02dfc3d759668fe5ffc3d97c72c4932ca297778d27192c6a698ad264f7641a988b07905dbf6a15c32f10320678999e89a604dc
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-03-14
11
+
12
+ ### Added
13
+ - **KUI declarative UI framework**: Pure Ruby DSL for building GUI/TUI apps with a single codebase. Wraps Clay+Raylib (GUI) or ClayTUI (TUI). Widgets: `vpanel`, `hpanel`, `fixed_panel`, `label`, `label_num`, `button`, `menu_item`, `spacer`, `divider`, `progress_bar`. Theme system and unified key event abstraction.
14
+ - **ClayTUI stdlib**: Terminal UI backend using Clay layout engine + termbox2 rendering. Auto-detected via `ClayTUI` module reference.
15
+ - **KonpeitoShell stdlib**: Shell execution (`exec`, `system`), environment variables (`getenv`, `setenv`), and file I/O (`read_file`, `write_file`, `append_file`, `file_exists`) for mruby backend.
16
+ - **RPG framework stdlib**: Reusable RPG game components (tilemap, sprites, camera, battle system) for raylib-based games.
17
+ - **Stdlib auto-detection expansion**: `KonpeitoJSON`, `KonpeitoHTTP`, `KonpeitoCrypto`, `KonpeitoCompression` added to `STDLIB_MODULE_MAP` for automatic RBS injection when referenced in source code.
18
+ - **KUI auto-path resolution**: `require "kui_gui"` / `require "kui_tui"` works without `-I` flags — KUI stdlib directory is automatically included in compiler search paths.
19
+ - **KUI example apps**: Counter (GUI + TUI), minimal hello, multi-page dashboard with sidebar navigation.
20
+ - **DQ RPG demo with Clay UI**: Battle scenes, shop menus, and status displays using Clay layout integration.
21
+ - Keyword arguments documented as supported in mruby backend.
22
+
23
+ ### Fixed
24
+ - **mruby GC crash in native callbacks**: `rb_block_call` stored raw C stack pointer (`data2`) in proc env — GC tried to dereference it as `RBasic*` causing crash in `gc_mark_children`. Fixed by storing `mrb_nil_value()` instead; the native callback path reads `data2` from a global, not from proc env.
25
+ - **TUI rendering flicker**: Added per-frame string pool to `clay_tui_native.c` (64KB static buffer) to prevent Clay from holding pointers to GC-managed mruby heap memory that could be invalidated between frames.
26
+ - **Documentation references**: Fixed CLAUDE.md referencing non-existent `docs/architecture.md`.
27
+
28
+ ### Changed
29
+ - Tutorials (EN/JA) updated with KUI framework sections and all stdlib module documentation (Shell, JSON, HTTP, Crypto, Compression).
30
+
10
31
  ## [0.6.0] - 2026-03-11
11
32
 
12
33
  ### Added
@@ -69,7 +69,7 @@ module Konpeito
69
69
  end
70
70
  end
71
71
 
72
- attr_reader :profiler, :variadic_functions, :alias_renamed_methods
72
+ attr_reader :profiler, :variadic_functions, :keyword_param_functions, :alias_renamed_methods
73
73
 
74
74
  def generate(hir_program)
75
75
  @hir_program = hir_program
@@ -1126,6 +1126,17 @@ module Konpeito
1126
1126
  init_builder.store(init_builder.call(mrb_undef_fn), @mrb_qundef_global)
1127
1127
  init_builder.ret_void
1128
1128
 
1129
+ # GC arena management functions (prevent arena overflow in loops)
1130
+ @konpeito_gc_arena_save = @mod.functions.add(
1131
+ "konpeito_gc_arena_save", [], LLVM::Int32
1132
+ )
1133
+ @konpeito_gc_arena_restore = @mod.functions.add(
1134
+ "konpeito_gc_arena_restore", [LLVM::Int32], LLVM.Void
1135
+ )
1136
+ @konpeito_gc_protect_value = @mod.functions.add(
1137
+ "konpeito_gc_protect_value", [value_type], LLVM.Void
1138
+ )
1139
+
1129
1140
  # Clear @qnil etc. so the accessor methods know to load from globals
1130
1141
  @qnil = nil
1131
1142
  @qtrue = nil
@@ -1728,6 +1739,16 @@ module Konpeito
1728
1739
  end
1729
1740
  end
1730
1741
 
1742
+ # For mruby: save GC arena index so we can restore before return.
1743
+ # This prevents arena overflow when LLVM functions call each other
1744
+ # directly (bypassing mruby dispatch's arena management).
1745
+ @mruby_gc_arena_alloca = nil
1746
+ if @runtime == :mruby
1747
+ @mruby_gc_arena_alloca = @builder.alloca(LLVM::Int32, "_gc_arena")
1748
+ arena_idx = @builder.call(@konpeito_gc_arena_save)
1749
+ @builder.store(arena_idx, @mruby_gc_arena_alloca)
1750
+ end
1751
+
1731
1752
  # Insert profiling entry probe after parameter setup
1732
1753
  insert_profile_entry_probe(hir_func)
1733
1754
 
@@ -1753,6 +1774,21 @@ module Konpeito
1753
1774
  func
1754
1775
  end
1755
1776
 
1777
+ # Emit a return instruction with mruby GC arena management.
1778
+ # For the mruby backend, restores the arena to the saved index and
1779
+ # protects the return value from GC before returning.
1780
+ def emit_ret(value)
1781
+ if @mruby_gc_arena_alloca
1782
+ idx = @builder.load2(LLVM::Int32, @mruby_gc_arena_alloca, "_arena_r")
1783
+ @builder.call(@konpeito_gc_arena_restore, idx)
1784
+ # Protect the return value so it survives the arena restore
1785
+ if value.type == value_type
1786
+ @builder.call(@konpeito_gc_protect_value, value)
1787
+ end
1788
+ end
1789
+ @builder.ret(value)
1790
+ end
1791
+
1756
1792
  # Topologically sort blocks based on phi dependencies
1757
1793
  # Ensures blocks are generated after the blocks their phi nodes reference
1758
1794
  def sort_blocks_by_phi_dependencies(blocks)
@@ -4843,6 +4879,9 @@ module Konpeito
4843
4879
  saved_allocas = @variable_allocas.dup
4844
4880
  saved_in_block_callback = @in_block_callback
4845
4881
  saved_block_callback_self = @block_callback_self
4882
+ saved_mrb_cache = save_mrb_constant_cache
4883
+ saved_current_function = @current_function
4884
+ saved_gc_arena_alloca = @mruby_gc_arena_alloca
4846
4885
 
4847
4886
  # Create entry block for callback
4848
4887
  entry = callback_func.basic_blocks.append("entry")
@@ -4850,10 +4889,16 @@ module Konpeito
4850
4889
 
4851
4890
  # Reset variable tracking for callback scope.
4852
4891
  # Set @in_block_callback so nested proc creation uses GC-safe escape-cells mode.
4892
+ # Reset mruby constant cache — LLVM values are scoped per function, so
4893
+ # cached %qnil/%qtrue from the parent function are invalid here.
4894
+ # Clear GC arena alloca — callbacks don't own their own arena scope.
4853
4895
  @in_block_callback = true
4896
+ @mruby_gc_arena_alloca = nil
4854
4897
  @variables = {}
4855
4898
  @variable_types = {}
4856
4899
  @variable_allocas = {}
4900
+ reset_mrb_constant_cache
4901
+ @current_function = callback_func
4857
4902
 
4858
4903
  # Setup captured variable access through data2 pointer
4859
4904
  unless captures.empty?
@@ -5132,6 +5177,9 @@ module Konpeito
5132
5177
  @variable_allocas = saved_allocas
5133
5178
  @in_block_callback = saved_in_block_callback
5134
5179
  @block_callback_self = saved_block_callback_self
5180
+ restore_mrb_constant_cache(saved_mrb_cache)
5181
+ @current_function = saved_current_function
5182
+ @mruby_gc_arena_alloca = saved_gc_arena_alloca
5135
5183
 
5136
5184
  callback_func
5137
5185
  end
@@ -5583,6 +5631,7 @@ module Konpeito
5583
5631
  saved_block_callback_self = @block_callback_self
5584
5632
  saved_in_thread_callback = @in_thread_callback
5585
5633
  saved_thread_boxed_vars = @thread_boxed_vars
5634
+ saved_gc_arena_alloca = @mruby_gc_arena_alloca
5586
5635
 
5587
5636
  # Create entry block for callback
5588
5637
  entry = callback_func.basic_blocks.append("entry")
@@ -5599,6 +5648,7 @@ module Konpeito
5599
5648
  @in_block_callback = false
5600
5649
  @block_callback_self = nil
5601
5650
  @in_thread_callback = true
5651
+ @mruby_gc_arena_alloca = nil
5602
5652
 
5603
5653
  unless captures.empty?
5604
5654
  declare_free
@@ -5735,6 +5785,7 @@ module Konpeito
5735
5785
  @block_callback_self = saved_block_callback_self
5736
5786
  @in_thread_callback = saved_in_thread_callback
5737
5787
  @thread_boxed_vars = saved_thread_boxed_vars
5788
+ @mruby_gc_arena_alloca = saved_gc_arena_alloca
5738
5789
 
5739
5790
  callback_func
5740
5791
  end
@@ -8134,11 +8185,13 @@ module Konpeito
8134
8185
  saved_block_callback_self = @block_callback_self
8135
8186
  saved_rescue_escape_array = @rescue_escape_array
8136
8187
  saved_rescue_escape_indices = @rescue_escape_indices
8188
+ saved_gc_arena_alloca = @mruby_gc_arena_alloca
8137
8189
 
8138
8190
  # Reset variable tracking for callback scope
8139
8191
  @variables = {}
8140
8192
  @variable_types = {}
8141
8193
  @variable_allocas = {}
8194
+ @mruby_gc_arena_alloca = nil
8142
8195
 
8143
8196
  if try_hir_blocks && !try_hir_blocks.empty?
8144
8197
  # Structured try body: process HIR BasicBlock list (handles control flow inside try).
@@ -8231,6 +8284,7 @@ module Konpeito
8231
8284
  @block_callback_self = saved_block_callback_self
8232
8285
  @rescue_escape_array = saved_rescue_escape_array
8233
8286
  @rescue_escape_indices = saved_rescue_escape_indices
8287
+ @mruby_gc_arena_alloca = saved_gc_arena_alloca
8234
8288
 
8235
8289
  callback_func
8236
8290
  end
@@ -8252,6 +8306,7 @@ module Konpeito
8252
8306
  saved_block_callback_self = @block_callback_self
8253
8307
  saved_rescue_escape_array = @rescue_escape_array
8254
8308
  saved_rescue_escape_indices = @rescue_escape_indices
8309
+ saved_gc_arena_alloca = @mruby_gc_arena_alloca
8255
8310
 
8256
8311
  # In normal mode: params[0] = self. In escape mode, set @block_callback_self after unpacking.
8257
8312
  @block_callback_self = callback_func.params[0] unless escape_var_names
@@ -8264,6 +8319,7 @@ module Konpeito
8264
8319
  @variables = {}
8265
8320
  @variable_types = {}
8266
8321
  @variable_allocas = {}
8322
+ @mruby_gc_arena_alloca = nil
8267
8323
 
8268
8324
  # CRuby rescue callback: (VALUE data2, VALUE exception)
8269
8325
  exception_val = callback_func.params[1]
@@ -8393,6 +8449,7 @@ module Konpeito
8393
8449
  @block_callback_self = saved_block_callback_self
8394
8450
  @rescue_escape_array = saved_rescue_escape_array
8395
8451
  @rescue_escape_indices = saved_rescue_escape_indices
8452
+ @mruby_gc_arena_alloca = saved_gc_arena_alloca
8396
8453
 
8397
8454
  callback_func
8398
8455
  end
@@ -8532,6 +8589,7 @@ module Konpeito
8532
8589
  @block_callback_self = saved_block_callback_self
8533
8590
  @rescue_escape_array = saved_rescue_escape_array
8534
8591
  @rescue_escape_indices = saved_rescue_escape_indices
8592
+ @mruby_gc_arena_alloca = saved_gc_arena_alloca
8535
8593
 
8536
8594
  callback_func
8537
8595
  end
@@ -9593,9 +9651,9 @@ module Konpeito
9593
9651
  value, type_tag = get_value_with_type(term.value)
9594
9652
  # Box the value before returning to Ruby
9595
9653
  boxed = convert_value(value, type_tag, :value)
9596
- @builder.ret(boxed)
9654
+ emit_ret(boxed)
9597
9655
  else
9598
- @builder.ret(qnil)
9656
+ emit_ret(qnil)
9599
9657
  end
9600
9658
  when HIR::Branch
9601
9659
  condition, cond_type = get_value_with_type(term.condition)
@@ -8,6 +8,10 @@ module Konpeito
8
8
  module Codegen
9
9
  # Generates standalone executable from LLVM module using mruby runtime
10
10
  class MRubyBackend
11
+ # Stdlib modules that should be auto-defined in mruby init code
12
+ # even when no ModuleDef exists in HIR (cfunc-only modules).
13
+ STDLIB_MODULES = %w[Raylib Clay ClayTUI KonpeitoShell].freeze
14
+
11
15
  attr_reader :llvm_generator, :output_file, :module_name, :rbs_loader, :debug
12
16
 
13
17
  def initialize(llvm_generator, output_file:, module_name: nil, rbs_loader: nil, debug: false, extra_c_files: [],
@@ -62,6 +66,10 @@ module Konpeito
62
66
  clay_objs = ensure_clay_compiled
63
67
  extra_obj_files.concat(clay_objs)
64
68
 
69
+ # Compile vendored termbox2 library if ClayTUI is used
70
+ tb2_objs = ensure_termbox_compiled
71
+ extra_obj_files.concat(tb2_objs)
72
+
65
73
  obj_files = [obj_file, init_obj_file, helpers_obj_file] + extra_obj_files
66
74
 
67
75
  # Link into standalone executable
@@ -278,7 +286,9 @@ module Konpeito
278
286
  lines << ""
279
287
 
280
288
  # Define modules
289
+ defined_module_names = []
281
290
  hir.modules.each do |module_def|
291
+ defined_module_names << module_def.name.to_s
282
292
  module_var = "m#{module_def.name}"
283
293
  lines << " struct RClass *#{module_var} = mrb_define_module(mrb, \"#{module_def.name}\");"
284
294
 
@@ -319,6 +329,18 @@ module Konpeito
319
329
  end
320
330
  end
321
331
 
332
+ # Auto-define stdlib modules that have cfunc methods but no ModuleDef in HIR.
333
+ # This ensures that even if a dynamic dispatch fallback occurs (e.g., due to
334
+ # a method name mismatch), the module constant exists at runtime.
335
+ if @rbs_loader
336
+ STDLIB_MODULES.each do |mod_name|
337
+ next if defined_module_names.include?(mod_name)
338
+ next unless @rbs_loader.has_cfunc_methods?(mod_name.to_sym)
339
+
340
+ lines << " mrb_define_module(mrb, \"#{mod_name}\");"
341
+ end
342
+ end
343
+
322
344
  # Define NativeClasses with mrb_data_type
323
345
  native_classes.each do |class_name, class_type|
324
346
  lines.concat(generate_native_class_init(class_name, class_type))
@@ -434,8 +456,21 @@ module Konpeito
434
456
  else
435
457
  arity = llvm_func.params.size - 1 # Subtract self
436
458
 
459
+ # Check if this function has only keyword args (no positional params).
460
+ # If so, the kwargs hash arg is optional — caller may pass 0 args
461
+ # when no keywords are specified (e.g., `footer do ... end`).
462
+ func_base_name = mangled_name.sub(/\Arn_/, "")
463
+ kw_info = llvm_generator.keyword_param_functions[func_base_name] ||
464
+ llvm_generator.keyword_param_functions[func_base_name.to_sym]
465
+ kwargs_only = kw_info && kw_info[:regular_count] == 0 && arity == 1
466
+
437
467
  lines << "static mrb_value #{wrapper_name}(mrb_state *mrb, mrb_value self) {"
438
- if arity > 0
468
+ if kwargs_only
469
+ # Single optional kwargs hash arg — default to empty hash
470
+ lines << " mrb_value a0 = mrb_nil_value(), _block = mrb_nil_value();"
471
+ lines << " mrb_get_args(mrb, \"|o&\", &a0, &_block);"
472
+ lines << " if (mrb_nil_p(a0)) a0 = mrb_hash_new(mrb);"
473
+ elsif arity > 0
439
474
  lines << " mrb_value #{(0...arity).map { |i| "a#{i}" }.join(', ')}, _block;"
440
475
  format_str = "o" * arity + "&"
441
476
  args_list = (0...arity).map { |i| "&a#{i}" }.join(", ") + ", &_block"
@@ -933,6 +968,36 @@ module Konpeito
933
968
  )
934
969
  end
935
970
 
971
+ # Include termbox2 if ClayTUI is used
972
+ clay_tui_used = @extra_c_files.any? { |f| File.basename(f).include?("clay_tui") }
973
+ if clay_tui_used
974
+ sections << license_section(
975
+ "termbox2",
976
+ "MIT",
977
+ "Copyright (c) 2021 termbox developers",
978
+ "https://github.com/termbox/termbox2",
979
+ <<~MIT
980
+ Permission is hereby granted, free of charge, to any person obtaining a copy
981
+ of this software and associated documentation files (the "Software"), to deal
982
+ in the Software without restriction, including without limitation the rights
983
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
984
+ copies of the Software, and to permit persons to whom the Software is
985
+ furnished to do so, subject to the following conditions:
986
+
987
+ The above copyright notice and this permission notice shall be included in all
988
+ copies or substantial portions of the Software.
989
+
990
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
991
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
992
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
993
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
994
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
995
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
996
+ SOFTWARE.
997
+ MIT
998
+ )
999
+ end
1000
+
936
1001
  # Include raylib if linked
937
1002
  ffi_libs = @rbs_loader&.all_ffi_libraries || []
938
1003
  if ffi_libs.any? { |lib| lib.to_s.include?("raylib") }
@@ -1220,15 +1285,46 @@ module Konpeito
1220
1285
  [clay_impl_obj]
1221
1286
  end
1222
1287
 
1288
+ # Compile vendored termbox2 implementation if ClayTUI stdlib is used
1289
+ # Returns array of object file paths
1290
+ def ensure_termbox_compiled
1291
+ clay_tui_used = @extra_c_files.any? { |f| File.basename(f).include?("clay_tui") }
1292
+ return [] unless clay_tui_used
1293
+
1294
+ tb2_dir = File.expand_path("../../../vendor/termbox2", __dir__)
1295
+ tb2_impl_c = File.join(tb2_dir, "termbox2_impl.c")
1296
+ tb2_impl_obj = File.join(tb2_dir, "termbox2_impl.o")
1297
+
1298
+ return [] unless File.exist?(tb2_impl_c)
1299
+
1300
+ cc, cc_flags = cross_cc_with_flags
1301
+
1302
+ # Compile termbox2_impl.c (only if stale)
1303
+ unless File.exist?(tb2_impl_obj) && File.mtime(tb2_impl_obj) > File.mtime(tb2_impl_c)
1304
+ cmd = [*cc, "-c", "-O2"]
1305
+ cmd.concat(cc_flags)
1306
+ cmd += ["-o", tb2_impl_obj, tb2_impl_c]
1307
+ system(*cmd) or return []
1308
+ end
1309
+
1310
+ [tb2_impl_obj]
1311
+ end
1312
+
1223
1313
  def ffi_include_flags
1224
1314
  flags = []
1225
1315
 
1226
- # Always add vendored Clay include path if Clay stdlib is used
1316
+ # Always add vendored Clay include path if Clay/ClayTUI stdlib is used
1227
1317
  clay_dir = File.expand_path("../../../vendor/clay", __dir__)
1228
1318
  if @extra_c_files.any? { |f| File.basename(f).include?("clay") } && Dir.exist?(clay_dir)
1229
1319
  flags << "-I#{clay_dir}"
1230
1320
  end
1231
1321
 
1322
+ # Add vendored termbox2 include path if ClayTUI stdlib is used
1323
+ tb2_dir = File.expand_path("../../../vendor/termbox2", __dir__)
1324
+ if @extra_c_files.any? { |f| File.basename(f).include?("clay_tui") } && Dir.exist?(tb2_dir)
1325
+ flags << "-I#{tb2_dir}"
1326
+ end
1327
+
1232
1328
  if cross_compiling?
1233
1329
  # When cross-compiling, use cross library include paths
1234
1330
  flags << "-I#{@cross_libs_dir}/../include" if @cross_libs_dir && Dir.exist?("#{@cross_libs_dir}/../include")
@@ -43,18 +43,50 @@ mrb_state *konpeito_get_mrb_state(void) {
43
43
  /* ================================================================
44
44
  * Block stack for rb_yield / rb_block_given_p / rb_block_proc
45
45
  *
46
- * mruby requires explicit block values, unlike CRuby's implicit
47
- * "current block" context. We maintain a stack of block values
48
- * that wrapper functions push/pop around LLVM function calls.
46
+ * We maintain TWO parallel stacks:
47
+ *
48
+ * 1. konpeito_block_stack: mruby Proc VALUE (for rb_block_proc,
49
+ * rb_block_given_p, and fallback rb_yield via mrb_funcall).
50
+ *
51
+ * 2. konpeito_native_cb_stack: native function pointer + data pairs
52
+ * (for rb_yield to call callbacks directly without re-entering
53
+ * mrb_vm_exec, which corrupts mruby's callinfo on nested yields).
54
+ *
55
+ * rb_block_call pushes both the native callback AND the mruby proc.
56
+ * The mruby method wrapper pushes only the mruby proc (native_cb=NULL).
57
+ * rb_yield prefers the native callback when available.
49
58
  * ================================================================ */
50
59
 
60
+ typedef mrb_value (*cruby_block_callback_t)(mrb_value, mrb_value, int, const mrb_value*);
61
+
62
+ typedef struct {
63
+ cruby_block_callback_t func;
64
+ mrb_value data2;
65
+ } konpeito_native_cb_entry;
66
+
51
67
  #define KONPEITO_MAX_BLOCK_STACK 64
52
68
  static mrb_value konpeito_block_stack[KONPEITO_MAX_BLOCK_STACK];
69
+ static konpeito_native_cb_entry konpeito_native_cb_stack[KONPEITO_MAX_BLOCK_STACK];
53
70
  static int konpeito_block_stack_depth = 0;
54
71
 
72
+ /* Pending native callback: set by rb_block_call before mrb_funcall_with_block,
73
+ * consumed by konpeito_push_block in the wrapper. */
74
+ static cruby_block_callback_t konpeito_pending_native_func = NULL;
75
+ static mrb_value konpeito_pending_native_data2;
76
+
55
77
  void konpeito_push_block(mrb_value block) {
56
78
  if (konpeito_block_stack_depth < KONPEITO_MAX_BLOCK_STACK) {
57
- konpeito_block_stack[konpeito_block_stack_depth++] = block;
79
+ konpeito_block_stack[konpeito_block_stack_depth] = block;
80
+ /* If rb_block_call set a pending native callback, attach it here.
81
+ * This ensures the wrapper's push includes the native callback info. */
82
+ if (konpeito_pending_native_func) {
83
+ konpeito_native_cb_stack[konpeito_block_stack_depth].func = konpeito_pending_native_func;
84
+ konpeito_native_cb_stack[konpeito_block_stack_depth].data2 = konpeito_pending_native_data2;
85
+ konpeito_pending_native_func = NULL;
86
+ } else {
87
+ konpeito_native_cb_stack[konpeito_block_stack_depth].func = NULL;
88
+ }
89
+ konpeito_block_stack_depth++;
58
90
  }
59
91
  }
60
92
 
@@ -71,6 +103,37 @@ static mrb_value konpeito_get_block(void) {
71
103
  return mrb_nil_value();
72
104
  }
73
105
 
106
+ static konpeito_native_cb_entry *konpeito_get_native_cb(void) {
107
+ if (konpeito_block_stack_depth > 0) {
108
+ konpeito_native_cb_entry *entry = &konpeito_native_cb_stack[konpeito_block_stack_depth - 1];
109
+ if (entry->func) return entry;
110
+ }
111
+ return NULL;
112
+ }
113
+
114
+ /* ================================================================
115
+ * GC arena management
116
+ *
117
+ * mruby objects created in LLVM-generated code live on the C stack,
118
+ * invisible to mruby's GC. The arena protects them as roots.
119
+ * Each generated function saves the arena index at entry and restores
120
+ * it before returning, preventing arena overflow.
121
+ * ================================================================ */
122
+
123
+ int konpeito_gc_arena_save(void) {
124
+ return mrb_gc_arena_save(konpeito_mrb_state);
125
+ }
126
+
127
+ void konpeito_gc_arena_restore(int idx) {
128
+ mrb_gc_arena_restore(konpeito_mrb_state, idx);
129
+ }
130
+
131
+ void konpeito_gc_protect_value(mrb_value val) {
132
+ if (!mrb_immediate_p(val)) {
133
+ mrb_gc_protect(konpeito_mrb_state, val);
134
+ }
135
+ }
136
+
74
137
  /* ================================================================
75
138
  * mruby constant values (Qnil, Qtrue, Qfalse, Qundef equivalents)
76
139
  * These are stored as mrb_value but exposed as uint64_t (VALUE) to
@@ -141,23 +204,31 @@ double rb_num2dbl(mrb_value v) {
141
204
 
142
205
  /* CRuby: VALUE rb_str_new(const char *ptr, long len) */
143
206
  mrb_value rb_str_new(const char *ptr, long len) {
144
- return mrb_str_new(konpeito_mrb_state, ptr, (mrb_int)len);
207
+ mrb_value s = mrb_str_new(konpeito_mrb_state, ptr, (mrb_int)len);
208
+ mrb_gc_protect(konpeito_mrb_state, s);
209
+ return s;
145
210
  }
146
211
 
147
212
  /* CRuby: VALUE rb_utf8_str_new(const char *ptr, long len) */
148
213
  /* mruby doesn't distinguish encodings */
149
214
  mrb_value rb_utf8_str_new(const char *ptr, long len) {
150
- return mrb_str_new(konpeito_mrb_state, ptr, (mrb_int)len);
215
+ mrb_value s = mrb_str_new(konpeito_mrb_state, ptr, (mrb_int)len);
216
+ mrb_gc_protect(konpeito_mrb_state, s);
217
+ return s;
151
218
  }
152
219
 
153
220
  /* CRuby: VALUE rb_str_new_cstr(const char *ptr) */
154
221
  mrb_value rb_str_new_cstr(const char *ptr) {
155
- return mrb_str_new_cstr(konpeito_mrb_state, ptr);
222
+ mrb_value s = mrb_str_new_cstr(konpeito_mrb_state, ptr);
223
+ mrb_gc_protect(konpeito_mrb_state, s);
224
+ return s;
156
225
  }
157
226
 
158
227
  /* CRuby: VALUE rb_str_dup(VALUE str) */
159
228
  mrb_value rb_str_dup(mrb_value str) {
160
- return mrb_str_dup(konpeito_mrb_state, str);
229
+ mrb_value s = mrb_str_dup(konpeito_mrb_state, str);
230
+ mrb_gc_protect(konpeito_mrb_state, s);
231
+ return s;
161
232
  }
162
233
 
163
234
  /* CRuby: VALUE rb_str_hash(VALUE str) */
@@ -230,7 +301,9 @@ mrb_value rb_reg_new_str(mrb_value str, int64_t options) {
230
301
 
231
302
  /* CRuby: VALUE rb_ary_new_capa(long capa) */
232
303
  mrb_value rb_ary_new_capa(int64_t capa) {
233
- return mrb_ary_new_capa(konpeito_mrb_state, (mrb_int)capa);
304
+ mrb_value a = mrb_ary_new_capa(konpeito_mrb_state, (mrb_int)capa);
305
+ mrb_gc_protect(konpeito_mrb_state, a);
306
+ return a;
234
307
  }
235
308
 
236
309
  /* CRuby: VALUE rb_ary_push(VALUE ary, VALUE item) */
@@ -256,12 +329,16 @@ void rb_ary_store(mrb_value ary, int64_t idx, mrb_value val) {
256
329
 
257
330
  /* CRuby: VALUE rb_ary_new(void) */
258
331
  mrb_value rb_ary_new(void) {
259
- return mrb_ary_new(konpeito_mrb_state);
332
+ mrb_value a = mrb_ary_new(konpeito_mrb_state);
333
+ mrb_gc_protect(konpeito_mrb_state, a);
334
+ return a;
260
335
  }
261
336
 
262
337
  /* CRuby: VALUE rb_ary_new_from_values(long n, const VALUE *elts) */
263
338
  mrb_value rb_ary_new_from_values(int64_t n, const mrb_value *elts) {
264
- return mrb_ary_new_from_values(konpeito_mrb_state, (mrb_int)n, elts);
339
+ mrb_value a = mrb_ary_new_from_values(konpeito_mrb_state, (mrb_int)n, elts);
340
+ mrb_gc_protect(konpeito_mrb_state, a);
341
+ return a;
265
342
  }
266
343
 
267
344
  /* CRuby: VALUE rb_ary_subseq(VALUE ary, long beg, long len) */
@@ -305,7 +382,9 @@ mrb_value rb_ary_delete_at(mrb_value ary, int64_t pos) {
305
382
 
306
383
  /* CRuby: VALUE rb_hash_new(void) */
307
384
  mrb_value rb_hash_new(void) {
308
- return mrb_hash_new(konpeito_mrb_state);
385
+ mrb_value h = mrb_hash_new(konpeito_mrb_state);
386
+ mrb_gc_protect(konpeito_mrb_state, h);
387
+ return h;
309
388
  }
310
389
 
311
390
  /* CRuby: VALUE rb_hash_aset(VALUE hash, VALUE key, VALUE val) */
@@ -412,17 +491,33 @@ mrb_value rb_obj_class(mrb_value obj) {
412
491
  /* CRuby: VALUE rb_yield(VALUE val) */
413
492
  mrb_value rb_yield(mrb_value val) {
414
493
  mrb_state *mrb = konpeito_mrb_state;
494
+
495
+ /* Prefer direct native callback to avoid mrb_vm_exec re-entry issues */
496
+ konpeito_native_cb_entry *native = konpeito_get_native_cb();
497
+ if (native) {
498
+ mrb_value argv[1];
499
+ argv[0] = val;
500
+ return native->func(val, native->data2, 1, argv);
501
+ }
502
+
415
503
  mrb_value block = konpeito_get_block();
416
504
  if (mrb_nil_p(block)) {
417
505
  mrb_raise(mrb, E_RUNTIME_ERROR, "no block given (yield)");
418
506
  return mrb_nil_value();
419
507
  }
420
- return mrb_funcall(konpeito_mrb_state, block, "call", 1, val);
508
+ return mrb_funcall(mrb, block, "call", 1, val);
421
509
  }
422
510
 
423
511
  /* CRuby: VALUE rb_yield_values2(int argc, const VALUE *argv) */
424
512
  mrb_value rb_yield_values2(int argc, const mrb_value *argv) {
425
513
  mrb_state *mrb = konpeito_mrb_state;
514
+
515
+ konpeito_native_cb_entry *native = konpeito_get_native_cb();
516
+ if (native) {
517
+ mrb_value yielded = (argc > 0) ? argv[0] : mrb_nil_value();
518
+ return native->func(yielded, native->data2, argc, argv);
519
+ }
520
+
426
521
  mrb_value block = konpeito_get_block();
427
522
  if (mrb_nil_p(block)) {
428
523
  mrb_raise(mrb, E_RUNTIME_ERROR, "no block given (yield)");
@@ -456,8 +551,6 @@ mrb_value rb_block_proc(void) {
456
551
  * the original callback pointer and data in the proc's environment.
457
552
  * ================================================================ */
458
553
 
459
- typedef mrb_value (*cruby_block_callback_t)(mrb_value, mrb_value, int, const mrb_value*);
460
-
461
554
  /* Adapter: called as mruby cfunc, delegates to CRuby-style callback */
462
555
  static mrb_value konpeito_cfunc_adapter(mrb_state *mrb, mrb_value self) {
463
556
  mrb_value cb_ptr_val = mrb_proc_cfunc_env_get(mrb, 0);
@@ -481,15 +574,29 @@ mrb_value rb_block_call(mrb_value obj, mrb_sym mid, int argc, const mrb_value *a
481
574
  return mrb_funcall_argv(konpeito_mrb_state, obj, mid, argc, argv);
482
575
  }
483
576
 
484
- /* Create a proc that wraps the CRuby callback via environment */
577
+ /* Create a proc that wraps the CRuby callback via environment.
578
+ * This is needed for rb_block_proc() and as fallback for rb_yield()
579
+ * when the block was not pushed via rb_block_call (e.g., mruby Proc). */
485
580
  mrb_value env[2];
486
581
  env[0] = mrb_cptr_value(konpeito_mrb_state, proc);
487
- env[1] = data2;
582
+ /* Store nil instead of data2 in the proc env. data2 may be a raw C
583
+ * stack pointer (pointer-to-alloca capture mode) which is NOT a valid
584
+ * mrb_value. If GC marks the proc's env it would try to dereference
585
+ * that raw pointer as an RBasic* → crash in gc_mark_children.
586
+ * The native callback path reads data2 from konpeito_pending_native_data2
587
+ * (set below), so env[1] is never read in practice. */
588
+ env[1] = mrb_nil_value();
488
589
  struct RProc *block_proc = mrb_proc_new_cfunc_with_env(
489
590
  konpeito_mrb_state, konpeito_cfunc_adapter, 2, env);
490
591
  mrb_value block_val = mrb_obj_value(block_proc);
491
592
 
492
- return mrb_funcall_with_block(konpeito_mrb_state, obj, mid, argc, argv, block_val);
593
+ /* Set pending native callback so the wrapper's konpeito_push_block call
594
+ * will attach the native function pointer. rb_yield will then call the
595
+ * native callback directly, bypassing mrb_vm_exec re-entry. */
596
+ konpeito_pending_native_func = (cruby_block_callback_t)proc;
597
+ konpeito_pending_native_data2 = data2;
598
+ mrb_value result = mrb_funcall_with_block(konpeito_mrb_state, obj, mid, argc, argv, block_val);
599
+ return result;
493
600
  }
494
601
 
495
602
  /* CRuby: VALUE rb_block_call_kw(...) */
@@ -116,8 +116,14 @@ module Konpeito
116
116
 
117
117
  def parse
118
118
  log "Resolving dependencies for #{source_file}..."
119
+ # Include KUI stdlib in search paths so `require "kui_gui"` / `require "kui_tui"`
120
+ # resolves automatically without needing `-I` for the KUI directory.
121
+ kui_stdlib_dir = File.expand_path("stdlib/kui", __dir__)
122
+ all_require_paths = @require_paths.dup
123
+ all_require_paths << kui_stdlib_dir unless all_require_paths.include?(kui_stdlib_dir)
124
+
119
125
  resolver = DependencyResolver.new(
120
- base_paths: @require_paths,
126
+ base_paths: all_require_paths,
121
127
  verbose: verbose,
122
128
  cache_manager: @cache_manager
123
129
  )
@@ -653,7 +659,13 @@ module Konpeito
653
659
  # Map of stdlib modules: module name pattern => stdlib directory name
654
660
  STDLIB_MODULE_MAP = {
655
661
  "Raylib" => "raylib",
656
- "Clay" => "clay"
662
+ "Clay" => "clay",
663
+ "ClayTUI" => "clay_tui",
664
+ "KonpeitoShell" => "shell",
665
+ "KonpeitoJSON" => "json",
666
+ "KonpeitoHTTP" => "http",
667
+ "KonpeitoCrypto" => "crypto",
668
+ "KonpeitoCompression" => "compression"
657
669
  }.freeze
658
670
 
659
671
  # Scan AST for known stdlib module references and auto-add their RBS paths