carray 3.0.0 → 3.0.2

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.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +845 -3
  4. data/{NEWS.md → CHANGELOG.v1.md} +1 -1
  5. data/README.md +33 -33
  6. data/carray.gemspec +12 -8
  7. data/ext/ca_axis_dispatch.c +33 -4
  8. data/ext/ca_axis_group.c +256 -114
  9. data/ext/ca_binop_dispatch.c +5 -6
  10. data/ext/ca_binop_dispatch.h +0 -7
  11. data/ext/ca_categorical_iterator.c +121 -67
  12. data/ext/ca_for_each_element.h +6 -8
  13. data/ext/ca_group_iter.c +1 -1
  14. data/ext/ca_kernel_iterator.c +386 -74
  15. data/ext/ca_kernel_iterator.h +145 -45
  16. data/ext/ca_obj_array.c +62 -20
  17. data/ext/ca_obj_bincmp.c +57 -27
  18. data/ext/ca_obj_binop.c +86 -30
  19. data/ext/ca_obj_block.c +4 -4
  20. data/ext/ca_obj_const_string.c +88 -27
  21. data/ext/ca_obj_face.c +104 -0
  22. data/ext/ca_obj_face.h +48 -1
  23. data/ext/ca_obj_fake.c +11 -0
  24. data/ext/ca_obj_fixlen_string.c +21 -6
  25. data/ext/ca_obj_grid.c +11 -3
  26. data/ext/ca_obj_meld.c +125 -22
  27. data/ext/ca_obj_moncmp.c +33 -14
  28. data/ext/ca_obj_monop.c +19 -7
  29. data/ext/ca_obj_object.c +107 -4
  30. data/ext/ca_obj_record.c +3 -1
  31. data/ext/ca_obj_refer.c +30 -6
  32. data/ext/ca_obj_roll.c +13 -5
  33. data/ext/ca_obj_select.c +49 -34
  34. data/ext/ca_obj_select_axis.c +16 -6
  35. data/ext/ca_obj_shift.c +3 -3
  36. data/ext/ca_obj_stack.c +3 -8
  37. data/ext/ca_obj_stride.c +178 -24
  38. data/ext/ca_obj_string.c +11 -5
  39. data/ext/ca_obj_tile.c +13 -5
  40. data/ext/ca_obj_time.c +3 -1
  41. data/ext/ca_obj_timedelta.c +3 -1
  42. data/ext/ca_obj_transpose.c +1 -1
  43. data/ext/ca_obj_triop.c +62 -23
  44. data/ext/ca_obj_window.c +94 -19
  45. data/ext/ca_op_cmplx64.h +123 -0
  46. data/ext/ca_op_ipower.c +1 -5
  47. data/ext/ca_rng_normal.h +42 -0
  48. data/ext/ca_rng_xoshiro256pp.h +105 -0
  49. data/ext/ca_sort_kernels.h +5 -5
  50. data/ext/ca_sweep_engine.c +362 -155
  51. data/ext/ca_sweep_engine.h +39 -9
  52. data/ext/ca_transform_common.c +7 -0
  53. data/ext/carray.h +148 -132
  54. data/ext/carray_access.c +83 -49
  55. data/ext/carray_address_basis.c +590 -0
  56. data/ext/carray_bincount.c +8 -8
  57. data/ext/carray_broadcast.c +102 -9
  58. data/ext/carray_build_flags.h +3 -0
  59. data/ext/carray_call_cfunc.c +2010 -335
  60. data/ext/carray_call_cfunc.h +153 -0
  61. data/ext/carray_cast.c +184 -72
  62. data/ext/carray_conversion.c +22 -22
  63. data/ext/carray_copy.c +67 -32
  64. data/ext/carray_core.c +140 -13
  65. data/ext/carray_count.c +10 -11
  66. data/ext/carray_element.c +7 -7
  67. data/ext/carray_factorize.c +72 -51
  68. data/ext/carray_hold.c +1 -1
  69. data/ext/carray_index_classifier.c +7 -21
  70. data/ext/carray_internal.h +36 -3
  71. data/ext/carray_kernels_bincmp.c +1 -0
  72. data/ext/carray_kernels_binop.c +320 -298
  73. data/ext/carray_kernels_init.c +1095 -0
  74. data/ext/carray_kernels_map.c +1 -0
  75. data/ext/carray_kernels_moncmp.c +1 -0
  76. data/ext/carray_kernels_monop.c +412 -411
  77. data/ext/carray_kernels_reduce_aggregate.c +256 -87
  78. data/ext/carray_kernels_reduce_boolean.c +1 -0
  79. data/ext/carray_kernels_reduce_cumulative.c +324 -54
  80. data/ext/carray_kernels_reduce_extreme.c +605 -58
  81. data/ext/carray_kernels_reduce_variance.c +1 -0
  82. data/ext/carray_kernels_scan.c +5 -4
  83. data/ext/carray_kernels_search.c +187 -106
  84. data/ext/carray_kernels_sort.c +1 -0
  85. data/ext/carray_kernels_triop.c +1 -0
  86. data/ext/carray_lazy.c +175 -5
  87. data/ext/carray_loop.c +7 -1
  88. data/ext/carray_mask.c +29 -13
  89. data/ext/carray_median_percentile.c +56 -1
  90. data/ext/carray_memory_view.c +46 -21
  91. data/ext/carray_operator.c +50 -53
  92. data/ext/carray_order.c +1 -1
  93. data/ext/carray_partition.c +4 -4
  94. data/ext/carray_random.c +391 -47
  95. data/ext/carray_scatter.c +1 -1
  96. data/ext/carray_slab.c +13 -0
  97. data/ext/carray_sort.c +22 -24
  98. data/ext/carray_sort_kernel.c +10 -10
  99. data/ext/carray_test.c +1 -1
  100. data/ext/extconf.rb +18 -0
  101. data/ext/mk_call_cfunc.rb +323 -145
  102. data/ext/mkkernel.rb +651 -129
  103. data/ext/ruby_carray.c +28 -17
  104. data/ext/version.h +4 -4
  105. data/lib/carray/autoload_carray.rb +24 -3
  106. data/lib/carray/autoload_method_extension.rb +14 -1
  107. data/lib/carray/axis_group.rb +84 -7
  108. data/lib/carray/basics.rb +63 -59
  109. data/lib/carray/bincount_nd.rb +12 -12
  110. data/lib/carray/block_iterator.rb +111 -25
  111. data/lib/carray/categorical.rb +154 -37
  112. data/lib/carray/categorical_iterator.rb +300 -151
  113. data/lib/carray/conditional.rb +14 -14
  114. data/lib/carray/const_string.rb +131 -27
  115. data/lib/carray/construct.rb +61 -0
  116. data/lib/carray/core_extensions.rb +62 -5
  117. data/lib/carray/data_type_extension.rb +21 -1
  118. data/lib/carray/data_type_limits.rb +91 -0
  119. data/lib/carray/fixlen_string.rb +1 -1
  120. data/lib/carray/frame/concat.rb +3 -3
  121. data/lib/carray/frame/convert.rb +1 -1
  122. data/lib/carray/frame/csv_parser.rb +11 -4
  123. data/lib/carray/frame/frame.rb +85 -21
  124. data/lib/carray/frame/group.rb +36 -3
  125. data/lib/carray/frame/io.rb +224 -15
  126. data/lib/carray/frame/records.rb +18 -4
  127. data/lib/carray/frame/sort.rb +1 -1
  128. data/lib/carray/frame/verbs.rb +32 -12
  129. data/lib/carray/fuse_source.rb +123 -0
  130. data/lib/carray/fusion.rb +218 -0
  131. data/lib/carray/histogram.rb +16 -16
  132. data/lib/carray/inspect.rb +43 -16
  133. data/lib/carray/iterator.rb +147 -3
  134. data/lib/carray/lazy.rb +88 -73
  135. data/lib/carray/mask_gap_fill.rb +3 -1
  136. data/lib/carray/meld_reduce.rb +2 -2
  137. data/lib/carray/methods/align_addr.rb +1 -1
  138. data/lib/carray/methods/composition.rb +1 -1
  139. data/lib/carray/methods/discovery_along.rb +74 -0
  140. data/lib/carray/methods/factorize.rb +50 -0
  141. data/lib/carray/methods/is_in.rb +25 -14
  142. data/lib/carray/methods/locate_addr.rb +81 -3
  143. data/lib/carray/methods/mask_duplicates.rb +36 -2
  144. data/lib/carray/methods/meshgrid.rb +4 -5
  145. data/lib/carray/methods/mode.rb +2 -2
  146. data/lib/carray/methods/nunique.rb +23 -2
  147. data/lib/carray/methods/repeat.rb +110 -0
  148. data/lib/carray/methods/snap.rb +7 -2
  149. data/lib/carray/methods/unique.rb +44 -4
  150. data/lib/carray/methods/value_counts.rb +2 -2
  151. data/lib/carray/rng.rb +86 -0
  152. data/lib/carray/runtime.rb +0 -19
  153. data/lib/carray/slab_iterator.rb +77 -19
  154. data/lib/carray/string_operation_extension.rb +10 -6
  155. data/lib/carray/time.rb +906 -491
  156. data/lib/carray/window_iterator.rb +421 -27
  157. data/lib/carray.rb +7 -5
  158. data/yard-stubs/ca_obj_array.rb +385 -0
  159. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  160. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  161. data/yard-stubs/ca_obj_block.rb +68 -0
  162. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  163. data/yard-stubs/ca_obj_fake.rb +31 -0
  164. data/yard-stubs/ca_obj_farray.rb +32 -0
  165. data/yard-stubs/ca_obj_field.rb +45 -0
  166. data/yard-stubs/ca_obj_grid.rb +35 -0
  167. data/yard-stubs/ca_obj_refer.rb +72 -0
  168. data/yard-stubs/ca_obj_roll.rb +45 -0
  169. data/yard-stubs/ca_obj_shift.rb +43 -0
  170. data/yard-stubs/ca_obj_stride.rb +181 -0
  171. data/yard-stubs/ca_obj_tile.rb +29 -0
  172. data/yard-stubs/ca_obj_transpose.rb +40 -0
  173. data/yard-stubs/ca_obj_window.rb +57 -0
  174. data/yard-stubs/carray_access.rb +131 -0
  175. data/yard-stubs/carray_attribute.rb +246 -0
  176. data/yard-stubs/carray_broadcast.rb +37 -0
  177. data/yard-stubs/carray_cast.rb +489 -0
  178. data/yard-stubs/carray_class.rb +65 -0
  179. data/yard-stubs/carray_conversion.rb +76 -0
  180. data/yard-stubs/carray_copy.rb +79 -0
  181. data/yard-stubs/carray_core.rb +34 -0
  182. data/yard-stubs/carray_count.rb +84 -0
  183. data/yard-stubs/carray_element.rb +108 -0
  184. data/yard-stubs/carray_generate.rb +66 -0
  185. data/yard-stubs/carray_lazy.rb +228 -0
  186. data/yard-stubs/carray_loop.rb +140 -0
  187. data/yard-stubs/carray_mask.rb +259 -0
  188. data/yard-stubs/carray_math.rb +1615 -0
  189. data/yard-stubs/carray_mathfunc.rb +45 -0
  190. data/yard-stubs/carray_median_percentile.rb +103 -0
  191. data/yard-stubs/carray_memory_view.rb +163 -0
  192. data/yard-stubs/carray_order.rb +252 -0
  193. data/yard-stubs/carray_random.rb +89 -0
  194. data/yard-stubs/carray_scatter.rb +106 -0
  195. data/yard-stubs/carray_slab.rb +59 -0
  196. data/yard-stubs/carray_sort.rb +165 -0
  197. data/yard-stubs/carray_test.rb +85 -0
  198. data/yard-stubs/carray_undef.rb +64 -0
  199. data/yard-stubs/carray_utils.rb +97 -0
  200. data/yard-stubs/ruby_carray.rb +193 -0
  201. metadata +67 -10
  202. data/ext/ca_obj_unbound_repeat.c +0 -496
data/ext/mk_call_cfunc.rb CHANGED
@@ -32,6 +32,7 @@
32
32
  # --- spec -------------------------------------------------------------------
33
33
 
34
34
  RAW_ARITIES = (1..7).to_a # ca_call_cfunc_1 .. _7
35
+ SLAB_ARITIES = (1..7).to_a # ca_call_cslab_1 .. _7
35
36
  TYPED_PAIRS = [ # ca_call_cfunc_<m>_<n> (m outputs, n inputs)
36
37
  [1, 1], [1, 2], [1, 3], [1, 4], [1, 5], [1, 6],
37
38
  [2, 1], [2, 2], [2, 3], [2, 4],
@@ -97,6 +98,28 @@ def sig_typed_r(m, n)
97
98
  "VALUE\nca_call_cfunc_#{m}_#{n}_r (#{typed_params(m, n, r: true)})"
98
99
  end
99
100
 
101
+ # The typed slab family declares its callback as ca_cslab_t rather than an
102
+ # arity-shaped function pointer: a slab callback takes its operands through
103
+ # `base`, so its signature does not grow with M + N.
104
+ def typed_slab_params(m, n, r: false)
105
+ dty = ->(k) { m == 1 ? "dty" : "dty#{k + 1}" }
106
+ out_data_type_params = (0...m).map { |k| "int8_t #{dty.(k)}" }
107
+ in_data_type_params = (0...n).map { |k| "int8_t dtx#{k + 1}" }
108
+ in_value_params = (0...n).map { |k| "volatile VALUE rx#{k + 1}" }
109
+ func_sig = r ? "ca_cslab_r_t slabfunc" : "ca_cslab_t slabfunc"
110
+ params = out_data_type_params + in_data_type_params + [func_sig] + in_value_params
111
+ params << "void *userdata" if r
112
+ params.join(", ")
113
+ end
114
+
115
+ def sig_typed_slab(m, n)
116
+ "VALUE\nca_call_cslab_#{m}_#{n} (#{typed_slab_params(m, n)})"
117
+ end
118
+
119
+ def sig_typed_slab_r(m, n)
120
+ "VALUE\nca_call_cslab_#{m}_#{n}_r (#{typed_slab_params(m, n, r: true)})"
121
+ end
122
+
100
123
  # Re-indent each non-blank line of `s` with `prefix`. `<<~` strips the
101
124
  # common leading whitespace from a heredoc, which would leave our body
102
125
  # at column 0; we want a 2-space body indent inside the function brace.
@@ -105,83 +128,61 @@ def indent(s, prefix = " ")
105
128
  end
106
129
 
107
130
  # --- raw ca_call_cfunc_N ----------------------------------------------------
131
+ #
132
+ # The per-cell loop runs in a walker function of its own, handed to
133
+ # ca_sweep_run: a callback that raises part way through gives back the
134
+ # operands and scratch the engine holds, rather than leaving them behind.
135
+ #
136
+ # The `_r` variant (reentrant: + void *userdata at signature tail) passes a
137
+ # trailing `void *userdata` through to every per-cell `func(...)`
138
+ # invocation as its last argument. Naming follows POSIX convention
139
+ # (qsort_r / bsearch_r / strtok_r), where `_r` marks a reentrant form that
140
+ # takes a thunk so the callback no longer depends on file-static / global
141
+ # state. Use when the callback needs to share state with the caller (e.g.
142
+ # accumulators, configuration flags, library plan handles) without
143
+ # resorting to file-static plumbing.
144
+ def emit_raw_common(n, r:)
145
+ name = r ? "ca_call_cfunc_#{n}_r" : "ca_call_cfunc_#{n}"
146
+ fsig = r ? void_p_list_r(n) : void_p_list(n)
147
+ call = "c->func(#{args_p(n)}#{r ? ", c->userdata" : ""})"
108
148
 
109
- def emit_raw(n)
110
- $src.puts sig_raw(n)
111
- $src.puts "{"
112
- $src.puts indent(<<~END_C)
113
- CArray *cx[#{n}];
114
- char *base[#{n}];
115
- ca_size_t stride[#{n}];
116
- char *owned_buf[#{n}];
117
- int attached[#{n}];
118
- ca_sweep_state_t state;
119
- int k_op;
120
-
121
- END_C
122
- # extract CArray* from VALUE
123
- (0...n).each do |k|
124
- $src.puts " TypedData_Get_Struct(rcx#{k}, CArray, &carray_data_type, cx[#{k}]);"
125
- end
126
- $src.puts ""
127
149
  $src.puts <<~END_C
128
- /* sweep engine: per-operand acquire (alias / xmalloc + ca_xfer_all),
129
- broadcast shape check, mask OR across INPUTs, mask propagate to
130
- OUTPUTs. Lifecycle template lives in ext/ca_sweep_engine.{c,h}. */
131
- state.n_ops = #{n};
132
- state.fsync = fsync;
133
- state.cx = cx;
134
- state.base = base;
135
- state.stride = stride;
136
- state.owned_buf = owned_buf;
137
- state.attached = attached;
138
- state.no_mask = 0;
139
- state.src_label = "ca_call_cfunc_#{n}";
140
-
141
- ca_sweep_acquire(&state);
142
-
143
- /* inner loop: advance per-cell ptrs and invoke user kernel func */
144
- {
145
- char *p[#{n}];
146
- ca_size_t k;
147
- if ( state.m0 ) {
148
- for ( k = 0; k < state.n_kernel; k++ ) {
149
- if ( ! state.m0[k] ) {
150
- for ( k_op = 0; k_op < #{n}; k_op++ ) {
151
- p[k_op] = base[k_op] + k * stride[k_op];
152
- }
153
- func(#{args_p(n)});
154
- }
155
- }
156
- } else {
157
- for ( k = 0; k < state.n_kernel; k++ ) {
150
+ typedef struct {
151
+ ca_sweep_state_t *st;
152
+ void (*func)(#{fsig});
153
+ void *userdata;
154
+ } #{name}_ctx_t;
155
+
156
+ static VALUE
157
+ #{name}_walk (VALUE arg)
158
+ {
159
+ #{name}_ctx_t *c = (#{name}_ctx_t *) arg;
160
+ ca_sweep_state_t *st = c->st;
161
+ char *p[#{n}];
162
+ ca_size_t k;
163
+ int k_op;
164
+ if ( st->m0 ) {
165
+ for ( k = 0; k < st->n_kernel; k++ ) {
166
+ if ( ! st->m0[k] ) {
158
167
  for ( k_op = 0; k_op < #{n}; k_op++ ) {
159
- p[k_op] = base[k_op] + k * stride[k_op];
168
+ p[k_op] = st->base[k_op] + k * st->stride[k_op];
160
169
  }
161
- func(#{args_p(n)});
170
+ #{call};
162
171
  }
163
172
  }
173
+ } else {
174
+ for ( k = 0; k < st->n_kernel; k++ ) {
175
+ for ( k_op = 0; k_op < #{n}; k_op++ ) {
176
+ p[k_op] = st->base[k_op] + k * st->stride[k_op];
177
+ }
178
+ #{call};
179
+ }
164
180
  }
165
-
166
- ca_sweep_release(&state);
167
-
168
- return rcx0;
181
+ return Qnil;
169
182
  }
170
183
 
171
184
  END_C
172
- end
173
-
174
- # --- raw ca_call_cfunc_N_r (reentrant: + void *userdata at signature tail) --
175
- #
176
- # Variant of emit_raw with a trailing `void *userdata` parameter passed
177
- # through to every per-cell `func(...)` invocation as its last argument.
178
- # Naming follows POSIX convention (qsort_r / bsearch_r / strtok_r), where
179
- # `_r` marks a reentrant form that takes a thunk so the callback no longer
180
- # depends on file-static / global state. Use when the callback needs to
181
- # share state with the caller (e.g. accumulators, configuration flags,
182
- # library plan handles) without resorting to file-static plumbing.
183
- def emit_raw_r(n)
184
- $src.puts sig_raw_r(n)
185
+ $src.puts(r ? sig_raw_r(n) : sig_raw(n))
185
186
  $src.puts "{"
186
187
  $src.puts indent(<<~END_C)
187
188
  CArray *cx[#{n}];
@@ -190,7 +191,7 @@ def emit_raw_r(n)
190
191
  char *owned_buf[#{n}];
191
192
  int attached[#{n}];
192
193
  ca_sweep_state_t state;
193
- int k_op;
194
+ #{name}_ctx_t ctx;
194
195
 
195
196
  END_C
196
197
  (0...n).each do |k|
@@ -198,8 +199,9 @@ def emit_raw_r(n)
198
199
  end
199
200
  $src.puts ""
200
201
  $src.puts <<~END_C
201
- /* sweep engine: same lifecycle as ca_call_cfunc_#{n}; the difference is
202
- the per-cell `func(...)` call has `userdata` as its last argument. */
202
+ /* sweep engine: per-operand acquire (alias / xmalloc + ca_xfer_all),
203
+ operand pairing, mask OR across INPUTs, mask propagate to OUTPUTs.
204
+ Lifecycle template lives in ext/ca_sweep_engine.{c,h}. */
203
205
  state.n_ops = #{n};
204
206
  state.fsync = fsync;
205
207
  state.cx = cx;
@@ -208,33 +210,14 @@ def emit_raw_r(n)
208
210
  state.owned_buf = owned_buf;
209
211
  state.attached = attached;
210
212
  state.no_mask = 0;
211
- state.src_label = "ca_call_cfunc_#{n}_r";
213
+ state.src_label = "#{name}";
212
214
 
213
215
  ca_sweep_acquire(&state);
214
216
 
215
- {
216
- char *p[#{n}];
217
- ca_size_t k;
218
- if ( state.m0 ) {
219
- for ( k = 0; k < state.n_kernel; k++ ) {
220
- if ( ! state.m0[k] ) {
221
- for ( k_op = 0; k_op < #{n}; k_op++ ) {
222
- p[k_op] = base[k_op] + k * stride[k_op];
223
- }
224
- func(#{args_p_r(n)});
225
- }
226
- }
227
- } else {
228
- for ( k = 0; k < state.n_kernel; k++ ) {
229
- for ( k_op = 0; k_op < #{n}; k_op++ ) {
230
- p[k_op] = base[k_op] + k * stride[k_op];
231
- }
232
- func(#{args_p_r(n)});
233
- }
234
- }
235
- }
236
-
237
- ca_sweep_release(&state);
217
+ ctx.st = &state;
218
+ ctx.func = func;
219
+ ctx.userdata = #{r ? "userdata" : "NULL"};
220
+ ca_sweep_run(&state, #{name}_walk, (VALUE) &ctx);
238
221
 
239
222
  return rcx0;
240
223
  }
@@ -242,21 +225,25 @@ def emit_raw_r(n)
242
225
  END_C
243
226
  end
244
227
 
245
- # --- typed ca_call_cfunc_M_N ------------------------------------------------
228
+ def emit_raw(n) ; emit_raw_common(n, r: false); end
229
+ def emit_raw_r(n) ; emit_raw_common(n, r: true); end
246
230
 
247
- def emit_typed(m, n)
248
- total = m + n
249
- # Naming convention (matches legacy ext/carray_call_cfunc.c byte-for-byte):
250
- # M==1 : output data_type = `dty`, output var = `ry`
251
- # M >1 : output data_types = `dty1..dtyM`, output vars = `ry1..ryM`
231
+ # --- typed dispatchers: M outputs, N inputs ---------------------------------
232
+ #
233
+ # The four typed emitters (cfunc / cfunc_r / cslab / cslab_r) differ in one
234
+ # line -- which lower-level family they delegate to -- and are identical in
235
+ # the wrapping, template allocation and return handling around it. That
236
+ # shared part lives in emit_typed_body so a change to the coercion rules is
237
+ # made once rather than four times, which is the same reason this file is a
238
+ # generator at all.
239
+ #
240
+ # Naming convention (matches legacy ext/carray_call_cfunc.c byte-for-byte):
241
+ # M==1 : output data_type = `dty`, output var = `ry`
242
+ # M >1 : output data_types = `dty1..dtyM`, output vars = `ry1..ryM`
243
+ def emit_typed_body(m, n)
252
244
  dty = ->(k) { m == 1 ? "dty" : "dty#{k + 1}" }
253
245
  ry = ->(k) { m == 1 ? "ry" : "ry#{k + 1}" }
254
246
 
255
- fsync_str = "1" * m + "0" * n
256
-
257
- $src.puts sig_typed(m, n)
258
- $src.puts "{"
259
-
260
247
  # output VALUE declarations
261
248
  out_decl = (0...m).map { |k| "#{ry.(k)} = Qnil" }.join(", ")
262
249
  $src.puts " volatile VALUE #{out_decl};"
@@ -284,9 +271,9 @@ def emit_typed(m, n)
284
271
  end
285
272
  $src.puts ""
286
273
 
287
- # delegate to lower-level ca_call_cfunc_(m+n)
274
+ # delegate to the lower-level family
288
275
  args = (0...m).map { |k| ry.(k) } + (0...n).map { |k| "rx#{k + 1}" }
289
- $src.puts " ca_call_cfunc_#{total}(mathfunc, \"#{fsync_str}\", #{args.join(", ")});"
276
+ yield args.join(", ")
290
277
  $src.puts ""
291
278
 
292
279
  # return: scalar-fetch each output if rank-0, then assemble
@@ -305,62 +292,156 @@ def emit_typed(m, n)
305
292
  $src.puts ""
306
293
  end
307
294
 
295
+ def fsync_literal(m, n)
296
+ "1" * m + "0" * n
297
+ end
298
+
299
+ def emit_typed(m, n)
300
+ $src.puts sig_typed(m, n)
301
+ $src.puts "{"
302
+ emit_typed_body(m, n) do |args|
303
+ $src.puts " ca_call_cfunc_#{m + n}(mathfunc, \"#{fsync_literal(m, n)}\", #{args});"
304
+ end
305
+ end
306
+
308
307
  # --- typed ca_call_cfunc_M_N_r (reentrant variant) -------------------------
309
308
  #
310
309
  # Same as emit_typed but the callback signature and outer function gain a
311
310
  # trailing `void *userdata`; the inner delegate calls ca_call_cfunc_(M+N)_r
312
311
  # and forwards `userdata` through.
313
312
  def emit_typed_r(m, n)
314
- total = m + n
315
- dty = ->(k) { m == 1 ? "dty" : "dty#{k + 1}" }
316
- ry = ->(k) { m == 1 ? "ry" : "ry#{k + 1}" }
313
+ $src.puts sig_typed_r(m, n)
314
+ $src.puts "{"
315
+ emit_typed_body(m, n) do |args|
316
+ $src.puts " ca_call_cfunc_#{m + n}_r(mathfunc, \"#{fsync_literal(m, n)}\", #{args}, userdata);"
317
+ end
318
+ end
317
319
 
318
- fsync_str = "1" * m + "0" * n
320
+ # --- typed ca_call_cslab_M_N (chunked variant) -----------------------------
321
+ #
322
+ # The same convenience over the slab family: the caller declares the data
323
+ # types its callback works in and gets back a freshly allocated output,
324
+ # rather than allocating one and matching dtypes by hand.
325
+ #
326
+ # This is where chunking pays most, and not by coincidence. Coercion is
327
+ # what this layer is for, and rb_ca_wrap_readonly implements it as a lazy
328
+ # readonly cast view -- which is never attach-alias, so it is exactly the
329
+ # operand kind the whole-buffer path materialises with
330
+ # xmalloc(elements * bytes). Declaring CA_DOUBLE over an int32 array
331
+ # therefore costs a full converted copy through ca_call_cfunc_M_N and one
332
+ # chunk of arena scratch through ca_call_cslab_M_N. The typical use of the
333
+ # typed layer is the case chunking was built for.
334
+ def emit_typed_slab(m, n)
335
+ $src.puts sig_typed_slab(m, n)
336
+ $src.puts "{"
337
+ emit_typed_body(m, n) do |args|
338
+ $src.puts " ca_call_cslab_#{m + n}(slabfunc, \"#{fsync_literal(m, n)}\", #{args});"
339
+ end
340
+ end
319
341
 
320
- $src.puts sig_typed_r(m, n)
342
+ def emit_typed_slab_r(m, n)
343
+ $src.puts sig_typed_slab_r(m, n)
321
344
  $src.puts "{"
345
+ emit_typed_body(m, n) do |args|
346
+ $src.puts " ca_call_cslab_#{m + n}_r(slabfunc, \"#{fsync_literal(m, n)}\", #{args}, userdata);"
347
+ end
348
+ end
322
349
 
323
- out_decl = (0...m).map { |k| "#{ry.(k)} = Qnil" }.join(", ")
324
- $src.puts " volatile VALUE #{out_decl};"
325
- $src.puts ""
350
+ # --- slab ca_call_cslab_N ---------------------------------------------------
351
+ #
352
+ # The chunked counterpart of ca_call_cfunc_N. Two things differ, and they
353
+ # are the same thing seen from two sides:
354
+ #
355
+ # memory -- ca_sweep_acquire_chunked holds one arena scratch of ~32KB per
356
+ # non-alias INPUT for the whole walk and re-gathers it per chunk, where
357
+ # ca_sweep_acquire materialises each such operand whole. Peak input
358
+ # memory stops scaling with the operand.
359
+ #
360
+ # speed -- the callback is handed a whole chunk rather than one cell, so
361
+ # the indirect call is paid once per few thousand cells instead of once
362
+ # per cell. Measured on `y = a + b*2.0` over 10M doubles: 1.48 ns per
363
+ # element through a per-cell callback against 0.34 ns for the same
364
+ # arithmetic in a loop the compiler can see. A slab callback keeps the
365
+ # loop, so it keeps the 0.34, and it is the shape a vectoriser can work
366
+ # with -- a per-cell callback defeats one by construction.
367
+ #
368
+ # The callback signature does not depend on the arity, because the operands
369
+ # reach it through `base` / `stride` rather than as separate parameters.
370
+ # One typedef therefore serves every ca_call_cslab_N.
371
+ #
372
+ # Masked cells are NOT skipped for the author, as the per-cell form skips
373
+ # them: a slab has no way to leave a hole. The chunk's slice of the mask
374
+ # arrives as `m0` instead -- one byte per cell, indexed 0..n-1 alongside the
375
+ # data, or NULL when no INPUT carried a mask.
376
+ def sig_slab(n)
377
+ "VALUE\nca_call_cslab_#{n} (ca_cslab_t func, const char *fsync,\n #{value_param_list(n)})"
378
+ end
326
379
 
380
+ def sig_slab_r(n)
381
+ "VALUE\nca_call_cslab_#{n}_r (ca_cslab_r_t func, const char *fsync,\n #{value_param_list(n)},\n void *userdata)"
382
+ end
383
+
384
+ def emit_slab_body(n, name, walker, userdata)
385
+ $src.puts indent(<<~END_C)
386
+ CArray *cx[#{n}];
387
+ char *base[#{n}];
388
+ char *base_orig[#{n}];
389
+ ca_size_t stride[#{n}];
390
+ char *owned_buf[#{n}];
391
+ int attached[#{n}];
392
+ ca_sweep_state_t state;
393
+ ca_cslab_ctx_t ctx;
394
+
395
+ END_C
327
396
  (0...n).each do |k|
328
- $src.puts " rx#{k + 1} = rb_ca_wrap_readonly(rx#{k + 1}, INT2NUM(dtx#{k + 1}));"
397
+ $src.puts " TypedData_Get_Struct(rcx#{k}, CArray, &carray_data_type, cx[#{k}]);"
329
398
  end
330
399
  $src.puts ""
400
+ $src.puts <<~END_C
401
+ /* sweep engine, chunked path: per-operand acquire (attach for OUTPUT
402
+ and alias INPUT, arena chunk scratch for non-alias INPUT), broadcast
403
+ shape check, mask OR across INPUTs, mask propagate to OUTPUTs at
404
+ release. Lifecycle template lives in ext/ca_sweep_engine.{c,h}.
405
+ base_orig is the field the whole-buffer path leaves zero; the
406
+ chunked path walks it, so it has to be given here. */
407
+ state.n_ops = #{n};
408
+ state.fsync = fsync;
409
+ state.cx = cx;
410
+ state.base = base;
411
+ state.base_orig = base_orig;
412
+ state.stride = stride;
413
+ state.owned_buf = owned_buf;
414
+ state.attached = attached;
415
+ state.no_mask = 0;
416
+ state.src_label = "#{name}";
331
417
 
332
- (0...m).each do |out_k|
333
- cur_dty = dty.(out_k)
334
- conds = (0...n).map { |in_k| "#{cur_dty} != dtx#{in_k + 1}" }
335
- wrapped = (0...n).map { |in_k|
336
- "rb_ca_wrap_readonly(rx#{in_k + 1}, INT2NUM(#{cur_dty}))"
418
+ ca_sweep_acquire_chunked(&state);
419
+
420
+ ctx.st = &state;
421
+ ctx.func = #{userdata == "NULL" ? "func" : "NULL"};
422
+ ctx.func_r = #{userdata == "NULL" ? "NULL" : "func"};
423
+ ctx.userdata = #{userdata};
424
+ ca_sweep_run_chunked(&state, #{walker}, (VALUE) &ctx);
425
+
426
+ return rcx0;
337
427
  }
338
- plain = (0...n).map { |in_k| "rx#{in_k + 1}" }
339
- $src.puts " if ( #{conds.join(" || ")} ) {"
340
- $src.puts " #{ry.(out_k)} = rb_ca_template_n(#{n}, #{wrapped.join(", ")});"
341
- $src.puts " } else {"
342
- $src.puts " #{ry.(out_k)} = rb_ca_template_n(#{n}, #{plain.join(", ")});"
343
- $src.puts " }"
344
- end
345
- $src.puts ""
346
428
 
347
- args = (0...m).map { |k| ry.(k) } + (0...n).map { |k| "rx#{k + 1}" }
348
- $src.puts " ca_call_cfunc_#{total}_r(mathfunc, \"#{fsync_str}\", #{args.join(", ")}, userdata);"
349
- $src.puts ""
429
+ END_C
430
+ end
350
431
 
351
- (0...m).each do |k|
352
- $src.puts " if ( rb_ca_is_scalar(#{ry.(k)}) ) {"
353
- $src.puts " #{ry.(k)} = rb_ca_fetch_addr(#{ry.(k)}, 0);"
354
- $src.puts " }"
355
- end
356
- if m == 1
357
- $src.puts " return #{ry.(0)};"
358
- else
359
- list = (0...m).map { |k| ry.(k) }.join(", ")
360
- $src.puts " return rb_ary_new3(#{m}, #{list});"
361
- end
362
- $src.puts "}"
363
- $src.puts ""
432
+ def emit_slab(n)
433
+ $src.puts sig_slab(n)
434
+ $src.puts "{"
435
+ emit_slab_body(n, "ca_call_cslab_#{n}", "ca_cslab_walk", "NULL")
436
+ end
437
+
438
+ # Variant of emit_slab with a trailing `void *userdata` parameter passed
439
+ # through to every chunk invocation as its last argument, following the same
440
+ # POSIX `_r` convention as ca_call_cfunc_N_r.
441
+ def emit_slab_r(n)
442
+ $src.puts sig_slab_r(n)
443
+ $src.puts "{"
444
+ emit_slab_body(n, "ca_call_cslab_#{n}_r", "ca_cslab_r_walk", "userdata")
364
445
  end
365
446
 
366
447
  # --- header declaration emitters --------------------------------------------
@@ -369,6 +450,10 @@ def decl_raw(n) ; $hdr.puts sig_raw(n) + ";"; $hdr.puts ""; end
369
450
  def decl_raw_r(n) ; $hdr.puts sig_raw_r(n) + ";"; $hdr.puts ""; end
370
451
  def decl_typed(m, n) ; $hdr.puts sig_typed(m, n) + ";"; $hdr.puts ""; end
371
452
  def decl_typed_r(m, n) ; $hdr.puts sig_typed_r(m, n) + ";"; $hdr.puts ""; end
453
+ def decl_typed_slab(m, n) ; $hdr.puts sig_typed_slab(m, n) + ";"; $hdr.puts ""; end
454
+ def decl_typed_slab_r(m, n) ; $hdr.puts sig_typed_slab_r(m, n) + ";"; $hdr.puts ""; end
455
+ def decl_slab(n) ; $hdr.puts sig_slab(n) + ";"; $hdr.puts ""; end
456
+ def decl_slab_r(n) ; $hdr.puts sig_slab_r(n) + ";"; $hdr.puts ""; end
372
457
 
373
458
  # --- main -------------------------------------------------------------------
374
459
 
@@ -406,6 +491,18 @@ $src.puts <<~END_C
406
491
  * operand invariant established in PROPOSAL_EAGER_ELEMENTWISE_NO_ATTACH
407
492
  * and extended in PROPOSAL_EAGER_SLOWPATH_CHUNKING_ARENA).
408
493
  *
494
+ * Slab variants `ca_call_cslab_N` / `ca_call_cslab_N_r` take the chunked
495
+ * path instead (ca_sweep_acquire_chunked / ca_sweep_next_chunk /
496
+ * ca_sweep_release_chunked) and hand the callback a whole chunk -- base /
497
+ * stride per operand, a cell count, and the chunk's slice of the mask --
498
+ * rather than one cell. A non-alias INPUT is then re-gathered into a
499
+ * ~32KB arena scratch per chunk rather than materialised whole, so input
500
+ * memory peak stops scaling with the operand; and the indirect call is
501
+ * paid once per chunk, so the author's inner loop is one the compiler can
502
+ * vectorise. Use cfunc when the per-cell body is what you have (a math
503
+ * function to wrap); use cslab when the operand is large, virtual, or the
504
+ * body is worth vectorising.
505
+ *
409
506
  * L0.1 (PROPOSAL_L0_AUTHOR_SURFACE, 2026-06-11): the per-operand acquire
410
507
  * + broadcast check + mask OR + release lifecycle is now factored out
411
508
  * into ext/ca_sweep_engine.{c,h} (ca_sweep_acquire / ca_sweep_release). This
@@ -417,6 +514,46 @@ $src.puts <<~END_C
417
514
  #include "ca_sweep_engine.h"
418
515
  #include <string.h>
419
516
 
517
+ /* Chunk walk for the ca_call_cslab_N family, run by ca_sweep_run_chunked
518
+ so that a callback raising part way through gives back what the engine
519
+ holds. Hands the author one chunk at a time. base[] is rewritten per
520
+ chunk by ca_sweep_next_chunk -- for a non-alias INPUT it points at the
521
+ arena scratch the chunk was just gathered into, which is packed, so
522
+ stride[] is the element size and the author's inner loop sees
523
+ contiguous data. m0 is the chunk's iteration mask, or NULL when no
524
+ INPUT operand carried one: chunk-sized and re-gathered per chunk, so it
525
+ is already the slice -- one byte per cell, indexed 0..chunk_n-1
526
+ alongside base[] and stride[]. The arity does not appear here: the
527
+ operands reach the callback through base[] / stride[]. */
528
+ typedef struct {
529
+ ca_sweep_state_t *st;
530
+ ca_cslab_t func; /* ca_call_cslab_N */
531
+ ca_cslab_r_t func_r; /* ca_call_cslab_N_r */
532
+ void *userdata;
533
+ } ca_cslab_ctx_t;
534
+
535
+ static VALUE
536
+ ca_cslab_walk (VALUE arg)
537
+ {
538
+ ca_cslab_ctx_t *c = (ca_cslab_ctx_t *) arg;
539
+ ca_sweep_state_t *st = c->st;
540
+ while ( ca_sweep_next_chunk(st) ) {
541
+ c->func(st->base, st->stride, st->chunk_n, st->m0);
542
+ }
543
+ return Qnil;
544
+ }
545
+
546
+ static VALUE
547
+ ca_cslab_r_walk (VALUE arg)
548
+ {
549
+ ca_cslab_ctx_t *c = (ca_cslab_ctx_t *) arg;
550
+ ca_sweep_state_t *st = c->st;
551
+ while ( ca_sweep_next_chunk(st) ) {
552
+ c->func_r(st->base, st->stride, st->chunk_n, st->m0, c->userdata);
553
+ }
554
+ return Qnil;
555
+ }
556
+
420
557
  END_C
421
558
 
422
559
  $hdr.puts <<~END_H
@@ -438,6 +575,18 @@ $hdr.puts <<~END_H
438
575
  #ifndef CARRAY_CALL_CFUNC_H
439
576
  #define CARRAY_CALL_CFUNC_H
440
577
 
578
+ /* Chunk callback for the ca_call_cslab_N family. `base` and `stride` are
579
+ one entry per operand, in the order the operands were passed; `n` is the
580
+ number of cells in this chunk; `m0` is the chunk's slice of the mask,
581
+ one byte per cell, or NULL when no INPUT carried a mask. The arity does
582
+ not appear in the signature -- the operands arrive through `base` -- so
583
+ one typedef serves every arity. */
584
+ typedef void (*ca_cslab_t) (char **base, ca_size_t *stride, ca_size_t n,
585
+ const boolean8_t *m0);
586
+
587
+ typedef void (*ca_cslab_r_t) (char **base, ca_size_t *stride, ca_size_t n,
588
+ const boolean8_t *m0, void *userdata);
589
+
441
590
  END_H
442
591
 
443
592
  RAW_ARITIES.each do |n|
@@ -474,6 +623,35 @@ TYPED_PAIRS.each do |m, n|
474
623
  decl_typed_r(m, n)
475
624
  end
476
625
 
626
+ $src.puts "/* -------------------------------------------------------------------- */"
627
+ $src.puts "/* Slab variants: the chunked counterpart of ca_call_cfunc_N. The */"
628
+ $src.puts "/* callback is handed a whole chunk (base / stride / count / mask */"
629
+ $src.puts "/* slice) rather than one cell, and non-alias INPUTs are gathered */"
630
+ $src.puts "/* into a ~32KB arena scratch per chunk instead of materialised */"
631
+ $src.puts "/* whole, so input memory peak stops scaling with the operand. */"
632
+ $src.puts "/* -------------------------------------------------------------------- */"
633
+ $src.puts ""
634
+
635
+ SLAB_ARITIES.each do |n|
636
+ emit_slab(n)
637
+ decl_slab(n)
638
+ end
639
+
640
+ SLAB_ARITIES.each do |n|
641
+ emit_slab_r(n)
642
+ decl_slab_r(n)
643
+ end
644
+
645
+ TYPED_PAIRS.each do |m, n|
646
+ emit_typed_slab(m, n)
647
+ decl_typed_slab(m, n)
648
+ end
649
+
650
+ TYPED_PAIRS.each do |m, n|
651
+ emit_typed_slab_r(m, n)
652
+ decl_typed_slab_r(m, n)
653
+ end
654
+
477
655
  $hdr.puts "#endif /* CARRAY_CALL_CFUNC_H */"
478
656
 
479
657
  $src.close