konpeito 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e06886ab2b9bd1bcc2638dcc1c0788c7a0bbe40c0416a630eb89ca8e86945a7a
4
- data.tar.gz: 3921d4b460e10864329173fcdf6ba0c925c89ad9d7959fab5ac74c640041e0e4
3
+ metadata.gz: 7d7f18a84d8cfece0c51becca2067ab474195205b4d5ce09b7a4e197b129851a
4
+ data.tar.gz: ca0f8a113919d8ca6b28e8109fd2ed031979de67e695c9aaad9f659b425dab89
5
5
  SHA512:
6
- metadata.gz: 970fff312f787c003978377350ff5cbeeccd46aab454999b1214d1c17a2e04530c2ec9149a4015b6c477cd21984b43ebf03390120cd1ce9b22670fe767845338
7
- data.tar.gz: 32b1f3c137e224a89f57df986f667a3bc4fbe9ea12997b340927ed70e7b00458b10106be3b1f827f86ff4e147e79eafc94601b4026ce225284e4fe2d06a4597c
6
+ metadata.gz: 62a1bd26cf53ddc918f1129772574021154079e492c33f48437290179fd71bdba65ca78c95890f16c224cf58930fbc879040c595580eb73a1d7e6868f0460cd9
7
+ data.tar.gz: 295d6ac004cda8c7823ad711dca5015a95c78302fd0f3c2da0796cb30ef0c638a13bac53f0c8ae439fc50556bb1262e6a304facdd4dfaa3c6b425f1d3842f85a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to Konpeito will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.1] - 2026-02-16
9
+
10
+ ### Fixed
11
+ - JVM backend: detect and resolve field type conflicts in `dedup_inherited_fields` to prevent runtime NPE when parent/child classes use same ivar with different types
12
+ - JVM backend: UI layout fixes for Castella dashboard (`.class` dispatch, `attr_accessor` field access, scrollbar thumb rendering, etc.)
13
+ - Remove duplicate `source_code_uri` from gemspec metadata
14
+ - Remove debug `puts` from data_table widget
15
+
16
+ ### Added
17
+ - Castella UI: visual properties (`bg_color`, `border_radius`, `border_color`, `border_width`) directly on Column/Row layouts, eliminating Container wrapper boilerplate
18
+
19
+ ### Changed
20
+ - Documentation: note that JVM backend might be more mature than LLVM backend
21
+
8
22
  ## [0.1.0] - 2026-02-13
9
23
 
10
24
  ### Added
@@ -72,4 +86,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
72
86
  - `%a{extern}` - external C struct wrappers
73
87
  - `%a{simd}` - SIMD vectorization
74
88
 
89
+ [0.1.1]: https://github.com/i2y/konpeito/compare/v0.1.0...v0.1.1
75
90
  [0.1.0]: https://github.com/i2y/konpeito/releases/tag/v0.1.0
@@ -187,6 +187,10 @@ module Konpeito
187
187
  # Classes are generated before main class, so we need this early.
188
188
  prescan_top_level_constants(hir_program)
189
189
 
190
+ # Pre-scan: register global variable static fields early so that
191
+ # global variables referenced inside blocks have fields available.
192
+ prescan_global_variables(hir_program)
193
+
190
194
  # Generate module interfaces FIRST (before classes that may implement them)
191
195
  hir_program.modules.each do |module_def|
192
196
  @block_methods = []
@@ -294,17 +298,43 @@ module Konpeito
294
298
  }
295
299
  end
296
300
 
301
+ # Yield every instruction in basic_blocks, recursing into block bodies
302
+ # attached to HIR::Call nodes.
303
+ def each_instruction_recursive(basic_blocks, &blk)
304
+ basic_blocks.each do |bb|
305
+ bb.instructions.each do |inst|
306
+ blk.call(inst)
307
+ if inst.is_a?(HIR::Call) && inst.block
308
+ each_instruction_recursive(inst.block.body, &blk)
309
+ end
310
+ end
311
+ end
312
+ end
313
+
297
314
  # Pre-scan top-level StoreConstant instructions to populate @constant_fields early.
298
315
  # This is needed because user classes are generated before the main class, and
299
316
  # class constructors may reference top-level constants (e.g. EXPANDING = 1).
317
+ # Recurses into block bodies so constants defined inside lambdas are also found.
300
318
  def prescan_top_level_constants(hir_program)
301
319
  hir_program.functions.each do |func|
302
320
  next if func.owner_class || func.owner_module
303
- func.body.each do |bb|
304
- bb.instructions.each do |inst|
305
- if inst.is_a?(HIR::StoreConstant) && inst.scope.nil?
306
- @constant_fields << inst.name.to_s
307
- end
321
+ each_instruction_recursive(func.body) do |inst|
322
+ if inst.is_a?(HIR::StoreConstant) && inst.scope.nil?
323
+ @constant_fields << inst.name.to_s
324
+ end
325
+ end
326
+ end
327
+ end
328
+
329
+ # Pre-scan all functions for global variable access (LoadGlobalVar/StoreGlobalVar)
330
+ # and register the corresponding static fields early. This ensures global variables
331
+ # referenced inside blocks have their fields available during code generation.
332
+ def prescan_global_variables(hir_program)
333
+ hir_program.functions.each do |func|
334
+ each_instruction_recursive(func.body) do |inst|
335
+ if inst.is_a?(HIR::LoadGlobalVar) || inst.is_a?(HIR::StoreGlobalVar)
336
+ field_name = inst.name.sub(/^\$/, "GLOBAL_")
337
+ register_global_field(field_name)
308
338
  end
309
339
  end
310
340
  end
@@ -3709,6 +3739,11 @@ module Konpeito
3709
3739
  # Remove fields from child classes that are already declared on a parent class.
3710
3740
  # Must run AFTER all classes are registered (register_class_info) since the
3711
3741
  # HIR class order may register children before parents.
3742
+ #
3743
+ # Type conflict detection: when a child class assigns a concrete but different type
3744
+ # to a field that the parent typed narrowly (e.g. parent :i64, child :string), widen
3745
+ # the parent's field to :value (Object) to prevent runtime NPE.
3746
+ # If the child type is :value (unresolved/unknown), keep the parent's concrete type.
3712
3747
  def dedup_inherited_fields
3713
3748
  @class_info.each do |class_name, info|
3714
3749
  parent = info[:super_name]
@@ -3719,6 +3754,16 @@ module Konpeito
3719
3754
  if parent_info[:fields]
3720
3755
  parent_info[:fields].each_key do |fname|
3721
3756
  if info[:fields].key?(fname)
3757
+ child_type = info[:fields][fname]
3758
+ parent_type = parent_info[:fields][fname]
3759
+ # Only widen when both types are concrete but different.
3760
+ # If child is :value (unknown), trust the parent's type.
3761
+ # If parent is :value, trust it (already wide enough).
3762
+ if child_type != parent_type && child_type != :value && parent_type != :value
3763
+ warn "[konpeito] JVM field type conflict: #{parent_name}##{fname} is :#{parent_type}, " \
3764
+ "but subclass #{class_name} uses :#{child_type}. Widening to :value (Object)."
3765
+ parent_info[:fields][fname] = :value
3766
+ end
3722
3767
  info[:fields].delete(fname)
3723
3768
  end
3724
3769
  end
@@ -5189,8 +5234,13 @@ module Konpeito
5189
5234
  end
5190
5235
  instructions = []
5191
5236
 
5192
- # Load self
5193
- instructions << { "op" => "aload", "var" => 0 }
5237
+ # Load self — use @block_self_slot if inside a block that captured self
5238
+ self_slot = @block_self_slot || 0
5239
+ instructions << { "op" => "aload", "var" => self_slot }
5240
+ # If self was captured as Object, checkcast to the expected class
5241
+ if @block_self_slot
5242
+ instructions << { "op" => "checkcast", "type" => jvm_class }
5243
+ end
5194
5244
 
5195
5245
  # Prefer registered descriptor for consistency with method definition
5196
5246
  registered = @method_descriptors["#{@current_class_name}##{method_name}"]
@@ -6478,7 +6528,8 @@ module Konpeito
6478
6528
  instructions
6479
6529
  end
6480
6530
 
6481
- # Check if a block body accesses instance variables (needs self reference).
6531
+ # Check if a block body accesses instance variables or calls methods on
6532
+ # implicit self (receiver-less calls like `kpi_card(label, value)`).
6482
6533
  # Recursively checks nested blocks (e.g., on_click inside a yield block)
6483
6534
  # so that outer blocks capture self when inner blocks need it.
6484
6535
  def block_needs_self?(block_def)
@@ -6486,6 +6537,8 @@ module Konpeito
6486
6537
  bb.instructions.any? { |inst|
6487
6538
  if inst.is_a?(HIR::LoadInstanceVar) || inst.is_a?(HIR::StoreInstanceVar)
6488
6539
  true
6540
+ elsif inst.is_a?(HIR::Call) && (inst.receiver.nil? || inst.receiver.is_a?(HIR::SelfRef))
6541
+ true
6489
6542
  elsif inst.is_a?(HIR::Call) && inst.block
6490
6543
  block_needs_self?(inst.block)
6491
6544
  else
@@ -2072,12 +2072,14 @@ module Konpeito
2072
2072
  # Check if this is a keyword hash argument
2073
2073
  if arg.node.is_a?(Prism::KeywordHashNode)
2074
2074
  # Extract keyword arguments from the hash
2075
- arg.node.elements.each do |elem|
2075
+ # arg.children are typed AssocNode wrappers; each has children[0]=key, children[1]=value
2076
+ arg.node.elements.each_with_index do |elem, ei|
2076
2077
  if elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
2077
2078
  key_name = elem.key.unescaped.to_sym
2078
- # Find the typed value from the children
2079
- value_typed = arg.children.find { |c| c.node == elem.value }
2080
- if value_typed
2079
+ # Navigate through the typed AssocNode to get the typed value child
2080
+ typed_assoc = arg.children[ei]
2081
+ if typed_assoc && typed_assoc.children && typed_assoc.children.size >= 2
2082
+ value_typed = typed_assoc.children[1]
2081
2083
  keyword_args[key_name] = visit(value_typed)
2082
2084
  else
2083
2085
  # Fallback: create typed node for the value
@@ -77,20 +77,35 @@ class Column < Layout
77
77
 
78
78
  #: (untyped painter) -> void
79
79
  def relocate_children(painter)
80
- remaining = @height
80
+ # Account for padding
81
+ inner_w = @width - @pad_left - @pad_right
82
+ inner_h = @height - @pad_top - @pad_bottom
83
+ if inner_w < 0.0
84
+ inner_w = 0.0
85
+ end
86
+ if inner_h < 0.0
87
+ inner_h = 0.0
88
+ end
89
+
90
+ remaining = inner_h
81
91
  expanding_total_flex = 0
82
92
 
83
93
  # First pass: measure FIXED/CONTENT children, collect EXPANDING
94
+ # In a Column, non-FIXED children fill the column width (for centering, word-wrap, etc.)
84
95
  i = 0
85
96
  while i < @children.length
86
97
  c = @children[i]
87
98
  if c.get_height_policy != EXPANDING
88
99
  # Set width before measure so word-wrapping can calculate line count
89
- if c.get_width_policy == EXPANDING
90
- c.resize_wh(@width, c.get_height)
100
+ if c.get_width_policy != FIXED
101
+ c.resize_wh(inner_w, c.get_height)
91
102
  end
92
103
  cs = c.measure(painter)
93
- c.resize_wh(@width, cs.height)
104
+ if c.get_width_policy == FIXED
105
+ c.resize_wh(cs.width, cs.height)
106
+ else
107
+ c.resize_wh(inner_w, cs.height)
108
+ end
94
109
  remaining = remaining - cs.height
95
110
  else
96
111
  expanding_total_flex = expanding_total_flex + c.get_flex
@@ -104,7 +119,7 @@ class Column < Layout
104
119
  end
105
120
 
106
121
  # Second pass: distribute remaining space to EXPANDING children, position all
107
- cy = @y
122
+ cy = @y + @pad_top
108
123
  if @is_scrollable
109
124
  cy = cy - @scroll_offset
110
125
  end
@@ -117,14 +132,14 @@ class Column < Layout
117
132
  if expanding_total_flex > 0 && remaining > 0.0
118
133
  h = remaining * c.get_flex / expanding_total_flex
119
134
  end
120
- c.resize_wh(@width, h)
135
+ c.resize_wh(inner_w, h)
121
136
  else
122
- # Set width for non-expanding too
123
- if c.get_width_policy == EXPANDING
124
- c.resize_wh(@width, c.get_height)
137
+ # In a Column, non-FIXED children fill the column width
138
+ if c.get_width_policy != FIXED
139
+ c.resize_wh(inner_w, c.get_height)
125
140
  end
126
141
  end
127
- c.move_xy(@x, cy)
142
+ c.move_xy(@x + @pad_left, cy)
128
143
  cy = cy + c.get_height + @spacing
129
144
  total_content_h = total_content_h + c.get_height
130
145
  total_content_h = total_content_h + @spacing if i > 0
@@ -134,7 +149,7 @@ class Column < Layout
134
149
 
135
150
  # Auto-scroll to bottom when pinned
136
151
  if @pin_bottom && @is_scrollable
137
- max_scroll = @content_height - @height
152
+ max_scroll = @content_height - inner_h
138
153
  if max_scroll > 0.0
139
154
  @scroll_offset = max_scroll
140
155
  end
@@ -143,9 +158,24 @@ class Column < Layout
143
158
 
144
159
  #: (untyped painter, bool completely) -> void
145
160
  def redraw(painter, completely)
161
+ saved_bg = $__bg_clear_color
162
+ # When this layout has a custom background and is dirty, we handle clearing
163
+ # ourselves to preserve rounded corners. Clear dirty flag so redraw_children
164
+ # won't overwrite with a solid fill_rect.
165
+ if @custom_bg && is_dirty
166
+ parent_bg = saved_bg
167
+ if parent_bg == nil || parent_bg == 0
168
+ parent_bg = $theme.bg_canvas
169
+ end
170
+ painter.fill_rect(0.0, 0.0, @width, @height, parent_bg)
171
+ set_dirty(false)
172
+ completely = true
173
+ end
174
+ draw_visual_background(painter)
146
175
  relocate_children(painter)
147
176
  redraw_children(painter, completely)
148
177
  draw_scrollbar(painter) if @is_scrollable
178
+ $__bg_clear_color = saved_bg
149
179
  end
150
180
 
151
181
  #: (untyped painter) -> void
@@ -1864,6 +1864,51 @@ class Layout < Widget
1864
1864
  def initialize
1865
1865
  super
1866
1866
  @children = []
1867
+ # Visual properties (background, border)
1868
+ @bg_color_val = 0
1869
+ @border_color_val = 0
1870
+ @custom_bg = false
1871
+ @custom_border = false
1872
+ @border_radius_val = 0.0
1873
+ @border_width_val = 1.0
1874
+ @bg_clear_color = nil
1875
+ end
1876
+
1877
+ # --- Visual Properties (method chaining) ---
1878
+
1879
+ #: (Integer c) -> Layout
1880
+ def bg_color(c)
1881
+ @bg_color_val = c
1882
+ @custom_bg = true
1883
+ self
1884
+ end
1885
+
1886
+ #: (Integer c) -> Layout
1887
+ def border_color(c)
1888
+ @border_color_val = c
1889
+ @custom_border = true
1890
+ self
1891
+ end
1892
+
1893
+ #: (Float r) -> Layout
1894
+ def border_radius(r)
1895
+ @border_radius_val = r
1896
+ self
1897
+ end
1898
+
1899
+ #: (Float w) -> Layout
1900
+ def border_width(w)
1901
+ @border_width_val = w
1902
+ self
1903
+ end
1904
+
1905
+ # Draw background and border if visual properties are set.
1906
+ #: (untyped painter) -> void
1907
+ def draw_visual_background(painter)
1908
+ if @custom_bg
1909
+ painter.fill_round_rect(0.0, 0.0, @width, @height, @border_radius_val, @bg_color_val)
1910
+ @bg_clear_color = @bg_color_val
1911
+ end
1867
1912
  end
1868
1913
 
1869
1914
  #: () -> Array
@@ -74,7 +74,17 @@ class Row < Layout
74
74
 
75
75
  #: (untyped painter) -> void
76
76
  def relocate_children(painter)
77
- remaining = @width
77
+ # Account for padding
78
+ inner_w = @width - @pad_left - @pad_right
79
+ inner_h = @height - @pad_top - @pad_bottom
80
+ if inner_w < 0.0
81
+ inner_w = 0.0
82
+ end
83
+ if inner_h < 0.0
84
+ inner_h = 0.0
85
+ end
86
+
87
+ remaining = inner_w
78
88
  expanding_total_flex = 0
79
89
 
80
90
  # First pass: measure FIXED/CONTENT children, collect EXPANDING
@@ -84,10 +94,14 @@ class Row < Layout
84
94
  if c.get_width_policy != EXPANDING
85
95
  # Set height before measure so height-dependent layouts work
86
96
  if c.get_height_policy == EXPANDING
87
- c.resize_wh(c.get_width, @height)
97
+ c.resize_wh(c.get_width, inner_h)
88
98
  end
89
99
  cs = c.measure(painter)
90
- c.resize_wh(cs.width, @height)
100
+ if c.get_height_policy == EXPANDING
101
+ c.resize_wh(cs.width, inner_h)
102
+ else
103
+ c.resize_wh(cs.width, cs.height)
104
+ end
91
105
  remaining = remaining - cs.width
92
106
  else
93
107
  expanding_total_flex = expanding_total_flex + c.get_flex
@@ -101,7 +115,7 @@ class Row < Layout
101
115
  end
102
116
 
103
117
  # Second pass: distribute remaining space, position all
104
- cx = @x
118
+ cx = @x + @pad_left
105
119
  if @is_scrollable
106
120
  cx = cx - @scroll_offset
107
121
  end
@@ -114,13 +128,13 @@ class Row < Layout
114
128
  if expanding_total_flex > 0 && remaining > 0.0
115
129
  w = remaining * c.get_flex / expanding_total_flex
116
130
  end
117
- c.resize_wh(w, @height)
131
+ c.resize_wh(w, inner_h)
118
132
  else
119
133
  if c.get_height_policy == EXPANDING
120
- c.resize_wh(c.get_width, @height)
134
+ c.resize_wh(c.get_width, inner_h)
121
135
  end
122
136
  end
123
- c.move_xy(cx, @y)
137
+ c.move_xy(cx, @y + @pad_top)
124
138
  cx = cx + c.get_width + @spacing
125
139
  total_content_w = total_content_w + c.get_width
126
140
  total_content_w = total_content_w + @spacing if i > 0
@@ -130,7 +144,7 @@ class Row < Layout
130
144
 
131
145
  # Auto-scroll to end when pinned
132
146
  if @pin_right && @is_scrollable
133
- max_scroll = @content_width - @width
147
+ max_scroll = @content_width - inner_w
134
148
  if max_scroll > 0.0
135
149
  @scroll_offset = max_scroll
136
150
  end
@@ -139,9 +153,21 @@ class Row < Layout
139
153
 
140
154
  #: (untyped painter, bool completely) -> void
141
155
  def redraw(painter, completely)
156
+ saved_bg = $__bg_clear_color
157
+ if @custom_bg && is_dirty
158
+ parent_bg = saved_bg
159
+ if parent_bg == nil || parent_bg == 0
160
+ parent_bg = $theme.bg_canvas
161
+ end
162
+ painter.fill_rect(0.0, 0.0, @width, @height, parent_bg)
163
+ set_dirty(false)
164
+ completely = true
165
+ end
166
+ draw_visual_background(painter)
142
167
  relocate_children(painter)
143
168
  redraw_children(painter, completely)
144
169
  draw_scrollbar(painter) if @is_scrollable
170
+ $__bg_clear_color = saved_bg
145
171
  end
146
172
 
147
173
  #: (untyped painter) -> void
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Konpeito
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -56,6 +56,9 @@ public class RubyDispatch {
56
56
  RUBY_NAME_ALIASES.put("to_s", new String[]{"toString"});
57
57
  RUBY_NAME_ALIASES.put("to_a", new String[]{"toArray_"});
58
58
 
59
+ // Reserved name aliases (jvm_method_name mangles these)
60
+ RUBY_NAME_ALIASES.put("k_class", new String[]{"getClass"});
61
+
59
62
  // Hash: keys/values → Ruby-specific methods returning KArray
60
63
  RUBY_NAME_ALIASES.put("keys", new String[]{"rubyKeys"});
61
64
  RUBY_NAME_ALIASES.put("values", new String[]{"rubyValues"});
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.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yasushi Itoh