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/ca_axis_group.c CHANGED
@@ -8,7 +8,7 @@
8
8
  reduce dispatch, GroupLabels) is wired in ca_group_iter.c (the `[]` gate and
9
9
  the iterator) + `axis_group` (lib/carray/axis_group.rb). Both kernels are
10
10
  written in the general form: several group axes + rank-N categorical (an N-D
11
- codes map) via a per-slab-element composite code, native dtype dispatch,
11
+ codes map) via a per-slab-element composite code, native data type dispatch,
12
12
  mask support. They take pre-built code bundles, so they are independent of
13
13
  how the classifier is constructed.
14
14
 
@@ -40,7 +40,8 @@
40
40
  enum {
41
41
  GR_SUM = 0, GR_PROD, GR_MEAN, GR_MIN, GR_MAX,
42
42
  GR_VARIANCE, GR_STDDEV, GR_COUNT, GR_ALL, GR_ANY,
43
- GR_VARIANCEP, GR_STDDEVP, GR_MINADDR, GR_MAXADDR
43
+ GR_VARIANCEP, GR_STDDEVP, GR_MINADDR, GR_MAXADDR,
44
+ GR_ACCUM
44
45
  };
45
46
 
46
47
  static int
@@ -48,6 +49,7 @@ group_op_code (VALUE vop)
48
49
  {
49
50
  ID id = SYM2ID(vop);
50
51
  if ( id == rb_intern("sum") ) return GR_SUM;
52
+ else if ( id == rb_intern("accumulate") ) return GR_ACCUM;
51
53
  else if ( id == rb_intern("prod") ) return GR_PROD;
52
54
  else if ( id == rb_intern("mean") ) return GR_MEAN;
53
55
  else if ( id == rb_intern("min") ) return GR_MIN;
@@ -72,14 +74,14 @@ group_op_code (VALUE vop)
72
74
  group code, applies the mask, and folds the value into whichever
73
75
  accumulator buffers are non-NULL. Accumulation is always in `double` (or
74
76
  counts in ca_size_t), so the op-finalisation below is type-agnostic — only
75
- the LOAD is monomorphised per dtype, keeping the inner loop autovectorisable
77
+ the LOAD is monomorphised per data type, keeping the inner loop autovectorisable
76
78
  while avoiding a forced float64 materialise of the (large) source. */
77
79
 
78
80
  /* GROUP_WALK(T, ACCUM): one CA_FOR_EACH_SLAB pass. For each slab element it
79
81
  computes the composite group code, applies the mask, then runs ACCUM with
80
82
  `v` (the element, widened to double) and `o` (the output flat index =
81
83
  code * band + band_flat) in scope. ACCUM is the only per-op-varying part,
82
- so the dtype is monomorphised once per type while sum / mean / variance /
84
+ so the data type is monomorphised once per type while sum / mean / variance /
83
85
  ... reuse the same walk. */
84
86
  /* Sentinel o-code for a slab element whose composite code is out of range
85
87
  (excluded categorical), stored in the precomputed plan below. */
@@ -108,16 +110,20 @@ group_op_code (VALUE vop)
108
110
  ca_size_t *gw_ocode = NULL, *gw_doff = NULL, *gw_moff = NULL, \
109
111
  *gw_gaddr = NULL; \
110
112
  int gw_ready = 0; \
113
+ /* freed by the unwind too: the walk can raise between here and the \
114
+ releases below (a gather that cannot convert a cell, and in the object \
115
+ lane a callback into Ruby). */ \
116
+ volatile VALUE gw_h1 = 0, gw_h2 = 0, gw_h3 = 0, gw_h4 = 0; \
111
117
  int gw_need_addr = ( op == GR_MINADDR || op == GR_MAXADDR ); \
112
118
  CA_FOR_EACH_SLAB(st, ca, axes, (int8_t) ngroup, CA_KERNEL_READ, p, m) { \
113
119
  int8_t sndim = st.slab_ndim; \
114
120
  ca_size_t SE = st.slab_elements; \
115
121
  if ( SE > 0 ) { \
116
122
  if ( ! gw_ready ) { \
117
- gw_ocode = ALLOC_N(ca_size_t, SE); \
118
- gw_doff = ALLOC_N(ca_size_t, SE); \
119
- gw_moff = ALLOC_N(ca_size_t, SE); \
120
- gw_gaddr = ALLOC_N(ca_size_t, SE); \
123
+ gw_ocode = ALLOCV_N(ca_size_t, gw_h1, SE); \
124
+ gw_doff = ALLOCV_N(ca_size_t, gw_h2, SE); \
125
+ gw_moff = ALLOCV_N(ca_size_t, gw_h3, SE); \
126
+ gw_gaddr = ALLOCV_N(ca_size_t, gw_h4, SE); \
121
127
  ca_size_t sidx[CA_RANK_MAX]; \
122
128
  for ( int8_t k = 0; k < sndim; k++ ) sidx[k] = 0; \
123
129
  for ( ca_size_t e = 0; e < SE; e++ ) { \
@@ -156,22 +162,96 @@ group_op_code (VALUE vop)
156
162
  ca_size_t oc = gw_ocode[e]; \
157
163
  if ( oc == GW_SKIP ) continue; \
158
164
  if ( m && m[ gw_moff[e] ] ) continue; \
159
- double v = (double) ( *(T *)(p + gw_doff[e]) ); \
165
+ T rv = *(T *)(p + gw_doff[e]); \
166
+ double v = (double) rv; \
160
167
  ca_size_t o = oc + b; \
161
168
  ca_size_t gaddr = gw_gaddr[e]; \
162
169
  ACCUM; \
163
- (void) v; (void) o; (void) gaddr; \
170
+ (void) rv; (void) v; (void) o; (void) gaddr; \
164
171
  } \
165
172
  } \
166
173
  b++; \
167
174
  } \
168
- if ( gw_ocode ) xfree(gw_ocode); \
169
- if ( gw_doff ) xfree(gw_doff); \
170
- if ( gw_moff ) xfree(gw_moff); \
171
- if ( gw_gaddr ) xfree(gw_gaddr); \
175
+ ALLOCV_END(gw_h1); ALLOCV_END(gw_h2); \
176
+ ALLOCV_END(gw_h3); ALLOCV_END(gw_h4); \
172
177
  } while (0)
173
178
 
174
- /* Run one walk over every supported native dtype. Dispatched on the source
179
+ /* GROUP_DISPATCH_T(ACCUM_T): the same walk, for an accumulator that needs to
180
+ name the source type -- one that keeps its answer in that type instead of
181
+ widening to double. ACCUM_T is invoked as ACCUM_T(T). */
182
+ #define GROUP_DISPATCH_T(ACCUM_T) \
183
+ switch ( ca->data_type ) { \
184
+ case CA_BOOLEAN: GROUP_WALK(boolean8_t, ACCUM_T(boolean8_t)); break; \
185
+ case CA_INT8: GROUP_WALK(int8_t, ACCUM_T(int8_t)); break; \
186
+ case CA_UINT8: GROUP_WALK(uint8_t, ACCUM_T(uint8_t)); break; \
187
+ case CA_INT16: GROUP_WALK(int16_t, ACCUM_T(int16_t)); break; \
188
+ case CA_UINT16: GROUP_WALK(uint16_t, ACCUM_T(uint16_t)); break; \
189
+ case CA_INT32: GROUP_WALK(int32_t, ACCUM_T(int32_t)); break; \
190
+ case CA_UINT32: GROUP_WALK(uint32_t, ACCUM_T(uint32_t)); break; \
191
+ case CA_INT64: GROUP_WALK(int64_t, ACCUM_T(int64_t)); break; \
192
+ case CA_UINT64: GROUP_WALK(uint64_t, ACCUM_T(uint64_t)); break; \
193
+ case CA_FLOAT32: GROUP_WALK(float, ACCUM_T(float)); break; \
194
+ case CA_FLOAT64: GROUP_WALK(double, ACCUM_T(double)); break; \
195
+ default: break; \
196
+ }
197
+
198
+ /* An extremum keeps the source data type: its magnitude never grows, so
199
+ widening to double buys nothing and costs exactness -- an int64 past 2^53
200
+ came back rounded, and a genuinely larger value could lose the comparison
201
+ to a smaller one that rounded to the same double. The scan siblings
202
+ (GROUP_SCAN_EXTREMUM_WALK) already held a native accumulator; these are the
203
+ reduce twins.
204
+
205
+ A NaN loses every contest, as it does in CArray's own min / max: it is held
206
+ only while nothing else has landed, the first number displaces it, and a
207
+ group of nothing but NaN answers NaN (for a position, UNDEF). `v` is the
208
+ widened load, so testing it for NaN is the same question for every source
209
+ type and costs an integer body nothing. seen_num[] says whether a number
210
+ has landed; cnt[] still says whether anything has. */
211
+ #define GMINMAX(T, A, CMP) \
212
+ do { \
213
+ A *acc = (A *) co->ptr; \
214
+ A av = (A) rv; \
215
+ if ( v == v ) { \
216
+ if ( ! seen_num[o] ) { acc[o] = av; seen_num[o] = 1; } \
217
+ else if ( av CMP acc[o] ) acc[o] = av; \
218
+ } else if ( cnt[o] == 0 ) acc[o] = av; \
219
+ cnt[o] += 1; \
220
+ } while (0)
221
+
222
+ /* Boolean answers as its 0/1 numeric storage, which is what CArray#min / #max
223
+ do with a boolean array -- the boolean-returning twins are all / any. Every
224
+ other type answers in its own. */
225
+ #define GROUP_DISPATCH_EXTREMUM(CMP) \
226
+ switch ( ca->data_type ) { \
227
+ case CA_BOOLEAN: GROUP_WALK(boolean8_t, GMINMAX(boolean8_t, uint64_t, CMP)); break; \
228
+ case CA_INT8: GROUP_WALK(int8_t, GMINMAX(int8_t, int8_t, CMP)); break; \
229
+ case CA_UINT8: GROUP_WALK(uint8_t, GMINMAX(uint8_t, uint8_t, CMP)); break; \
230
+ case CA_INT16: GROUP_WALK(int16_t, GMINMAX(int16_t, int16_t, CMP)); break; \
231
+ case CA_UINT16: GROUP_WALK(uint16_t, GMINMAX(uint16_t, uint16_t, CMP)); break; \
232
+ case CA_INT32: GROUP_WALK(int32_t, GMINMAX(int32_t, int32_t, CMP)); break; \
233
+ case CA_UINT32: GROUP_WALK(uint32_t, GMINMAX(uint32_t, uint32_t, CMP)); break; \
234
+ case CA_INT64: GROUP_WALK(int64_t, GMINMAX(int64_t, int64_t, CMP)); break; \
235
+ case CA_UINT64: GROUP_WALK(uint64_t, GMINMAX(uint64_t, uint64_t, CMP)); break; \
236
+ case CA_FLOAT32: GROUP_WALK(float, GMINMAX(float, float, CMP)); break; \
237
+ case CA_FLOAT64: GROUP_WALK(double, GMINMAX(double, double, CMP)); break; \
238
+ default: break; \
239
+ }
240
+
241
+ #define GMINMAXADDR(T, CMP, ADDR) \
242
+ do { \
243
+ T *acc = (T *) xbuf; \
244
+ if ( v == v && ( ! seen_num[o] || rv CMP acc[o] ) ) { \
245
+ acc[o] = rv; \
246
+ ADDR[o] = (int64_t) ( band_addr[b] + gaddr ); \
247
+ seen_num[o] = 1; \
248
+ } \
249
+ cnt[o] += 1; \
250
+ } while (0)
251
+ #define GMINADDR(T) GMINMAXADDR(T, <, mnaddr)
252
+ #define GMAXADDR(T) GMINMAXADDR(T, >, mxaddr)
253
+
254
+ /* Run one walk over every supported native data type. Dispatched on the source
175
255
  data_type so the inner loop stays monomorphic (no forced float64 cast). */
176
256
  #define GROUP_DISPATCH(ACCUM) \
177
257
  switch ( ca->data_type ) { \
@@ -189,6 +269,19 @@ group_op_code (VALUE vop)
189
269
  default: break; \
190
270
  }
191
271
 
272
+ /* `accumulate` is the one op that folds in the SOURCE's own type instead of in
273
+ double, so it wraps at that width exactly as CArray#accumulate does. It
274
+ folds straight into the (zeroed) output — whose data type is the source's —
275
+ so an empty group already holds the additive identity 0 and needs no mask.
276
+ The type cannot travel inside a GROUP_WALK ACCUM argument (an argument's own
277
+ tokens are not substituted for the macro's parameters), so the walk below
278
+ names its type twice. */
279
+ #define GACC_ADD(T) ( ((T *) co->ptr)[o] += *(T *)(p + gw_doff[e]) )
280
+ /* A boolean accumulate is XOR parity, matching the core: the result stays
281
+ boolean, so a second `true` has nowhere to carry into. */
282
+ #define GACC_XOR \
283
+ ( ((boolean8_t *) co->ptr)[o] ^= (*(boolean8_t *)(p + gw_doff[e]) ? 1 : 0) )
284
+
192
285
  /* __axis_group_reduce__(group_axes, bundles, op) — group-reduces self along
193
286
  * the union of `group_axes` (ascending source-axis indices = the slab) into
194
287
  * composite groups described by `bundles`, preserving the band (= non-grouped)
@@ -240,7 +333,6 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
240
333
  int8_t axes[CA_RANK_MAX];
241
334
  char is_group[CA_RANK_MAX];
242
335
  for ( int8_t i = 0; i < src->ndim; i++ ) is_group[i] = 0;
243
- ca_size_t group_prod = 1;
244
336
  for ( long i = 0; i < ngroup; i++ ) {
245
337
  int a = NUM2INT(RARRAY_AREF(vgaxes, i));
246
338
  if ( a < 0 || a >= src->ndim ) {
@@ -254,7 +346,6 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
254
346
  }
255
347
  is_group[a] = 1;
256
348
  axes[i] = (int8_t) a;
257
- group_prod *= src->dim[a];
258
349
  }
259
350
 
260
351
  /* --- bundles: small per-group code tables (metadata, kept alive) --- */
@@ -288,12 +379,25 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
288
379
  rb_raise(rb_eArgError, "axis_group_reduce: bundle k must be positive");
289
380
  }
290
381
 
291
- /* int32 view of the codes (metadata-sized, O(consumed-axis dims)) */
382
+ /* The code table is copied out and the source let go of at once, rather
383
+ than held attached for the length of the walk. Everything between here
384
+ and the end of the walk can raise -- a validation just below, a gather
385
+ that cannot convert a cell, an object-lane callback into Ruby -- and a
386
+ raise jumps over every detach, leaving the source materialised with
387
+ nothing left to release it. The copy rides a temporary buffer that the
388
+ unwind collects. */
292
389
  VALUE v32 = rb_ca_wrap_readonly(vcodes, INT2NUM(CA_INT32));
293
390
  rb_ary_push((VALUE) keep, v32);
294
391
  GetCArray(v32, bundle_ca[bi]);
295
- ca_attach(bundle_ca[bi]);
296
- bundle_codes[bi] = (int32_t *) bundle_ca[bi]->ptr;
392
+ {
393
+ size_t nbytes = (size_t) bundle_ca[bi]->elements * sizeof(int32_t);
394
+ VALUE vbuf = rb_str_tmp_new((long) nbytes);
395
+ rb_ary_push((VALUE) keep, vbuf);
396
+ ca_attach(bundle_ca[bi]);
397
+ memcpy(RSTRING_PTR(vbuf), bundle_ca[bi]->ptr, nbytes);
398
+ ca_detach(bundle_ca[bi]);
399
+ bundle_codes[bi] = (int32_t *) RSTRING_PTR(vbuf);
400
+ }
297
401
 
298
402
  int nb = (int) RARRAY_LEN(vbaxes);
299
403
  if ( nb <= 0 || nb > src->ndim ) {
@@ -348,63 +452,88 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
348
452
  }
349
453
  }
350
454
 
351
- /* --- band layout + output shape [K_total, *band_dims] --- */
352
- ca_size_t band = (group_prod > 0) ? (src->elements / group_prod) : 0;
455
+ /* --- band layout + output shape [K_total, *band_dims] ---
456
+ band is the product of the band dims, taken directly rather than as
457
+ src->elements / group_prod: a zero-length *group* axis makes group_prod
458
+ zero, and the division has to answer something. Answering 0 made nout 0
459
+ while the output really had K_total x band cells, so every finalisation
460
+ loop below ran zero times and the freshly zeroed buffer went back to the
461
+ caller unmasked -- a mean, a min and a variance all reported as 0.0.
462
+ Taken directly, nout == co->elements for every shape, and the group with
463
+ no cell in it takes the same empty-group path as any other. */
464
+ ca_size_t band = 1;
353
465
  ca_size_t odim[CA_RANK_MAX];
354
466
  int8_t ondim = 1;
355
467
  odim[0] = K_total;
356
468
  for ( int8_t i = 0; i < src->ndim; i++ ) {
357
- if ( ! is_group[i] ) odim[ondim++] = src->dim[i];
469
+ if ( ! is_group[i] ) { odim[ondim++] = src->dim[i]; band *= src->dim[i]; }
358
470
  }
359
471
  ca_size_t nout = K_total * band;
360
472
 
361
- /* --- supported dtype gate (before any allocation) --- */
473
+ /* --- supported data type gate (before any allocation) --- */
362
474
  ca = src;
363
475
  switch ( src->data_type ) {
364
476
  case CA_BOOLEAN: case CA_INT8: case CA_UINT8: case CA_INT16: case CA_UINT16:
365
477
  case CA_INT32: case CA_UINT32: case CA_INT64: case CA_UINT64:
366
478
  case CA_FLOAT32: case CA_FLOAT64: break;
367
479
  default:
368
- for ( int bi = 0; bi < n_bundles; bi++ ) ca_detach(bundle_ca[bi]);
369
480
  rb_raise(rb_eRuntimeError,
370
481
  "axis_group_reduce: unsupported source data_type %d",
371
482
  src->data_type);
372
483
  }
373
484
 
374
- /* output dtype per op */
485
+ /* output data type per op */
375
486
  int8_t out_dt = CA_FLOAT64;
376
487
  if ( op == GR_COUNT ) out_dt = CA_INT64;
377
488
  else if ( op == GR_MINADDR || op == GR_MAXADDR ) out_dt = CA_INT64;
378
489
  else if ( op == GR_ALL || op == GR_ANY ) out_dt = CA_BOOLEAN;
490
+ else if ( op == GR_ACCUM ) out_dt = src->data_type;
491
+ else if ( op == GR_MIN || op == GR_MAX )
492
+ out_dt = ( src->data_type == CA_BOOLEAN ) ? CA_UINT64 : src->data_type;
379
493
  VALUE vout = rb_carray_new(out_dt, ondim, odim, 0, NULL);
380
494
  GetCArray(vout, co);
381
495
 
382
496
  /* --- accumulator buffers (only those the op needs; all O(nout)) --- */
383
- ca_size_t *cnt = ALLOC_N(ca_size_t, nout); MEMZERO(cnt, ca_size_t, nout);
384
- double *sum = NULL, *sumsq = NULL, *prod = NULL, *mn = NULL, *mx = NULL;
497
+ /* Scratch that the unwind collects. A raise anywhere in the walk -- an
498
+ object-lane callback into Ruby, a gather that cannot convert a cell --
499
+ jumps over every release below, so these ride ALLOCV rather than plain
500
+ malloc: the temporary buffer behind each holder is freed whether the
501
+ function returns or unwinds. */
502
+ volatile VALUE h_cnt = 0, h_sum = 0, h_sumsq = 0, h_prod = 0, h_seen = 0,
503
+ h_xbuf = 0, h_nz = 0, h_mnaddr = 0, h_mxaddr = 0, h_band = 0;
504
+ ca_size_t *cnt = ALLOCV_N(ca_size_t, h_cnt, nout);
505
+ MEMZERO(cnt, ca_size_t, nout);
506
+ double *sum = NULL, *sumsq = NULL, *prod = NULL;
507
+ boolean8_t *seen_num = NULL; /* has a number (not a NaN) landed here? */
508
+ char *xbuf = NULL; /* running extremum, in the source type */
385
509
  ca_size_t *nz = NULL;
386
510
  int64_t *mnaddr = NULL, *mxaddr = NULL; /* flat source addr of min / max */
387
511
  ca_size_t *band_addr = NULL; /* raveled addr of each band cell */
388
512
  if ( op == GR_SUM || op == GR_MEAN || op == GR_VARIANCE || op == GR_STDDEV ||
389
513
  op == GR_VARIANCEP || op == GR_STDDEVP ) {
390
- sum = ALLOC_N(double, nout); MEMZERO(sum, double, nout);
514
+ sum = ALLOCV_N(double, h_sum, nout); MEMZERO(sum, double, nout);
391
515
  }
392
516
  if ( op == GR_PROD ) {
393
- prod = ALLOC_N(double, nout);
517
+ prod = ALLOCV_N(double, h_prod, nout);
394
518
  for ( ca_size_t o = 0; o < nout; o++ ) prod[o] = 1.0;
395
519
  }
396
- if ( op == GR_MIN || op == GR_MINADDR ) {
397
- mn = ALLOC_N(double, nout);
398
- for ( ca_size_t o = 0; o < nout; o++ ) mn[o] = HUGE_VAL;
520
+ if ( op == GR_MIN || op == GR_MAX ||
521
+ op == GR_MINADDR || op == GR_MAXADDR ) {
522
+ seen_num = ALLOCV_N(boolean8_t, h_seen, nout);
523
+ MEMZERO(seen_num, boolean8_t, nout);
524
+ }
525
+ if ( op == GR_MIN || op == GR_MAX ) {
526
+ /* the walk writes the extremum straight into the output, in its own type */
527
+ MEMZERO(co->ptr, char, (size_t) nout * co->bytes);
399
528
  }
400
- if ( op == GR_MAX || op == GR_MAXADDR ) {
401
- mx = ALLOC_N(double, nout);
402
- for ( ca_size_t o = 0; o < nout; o++ ) mx[o] = -HUGE_VAL;
529
+ if ( op == GR_MINADDR || op == GR_MAXADDR ) {
530
+ xbuf = ALLOCV_N(char, h_xbuf, (size_t) nout * src->bytes);
531
+ MEMZERO(xbuf, char, (size_t) nout * src->bytes);
403
532
  }
404
- if ( op == GR_MINADDR ) { mnaddr = ALLOC_N(int64_t, nout); MEMZERO(mnaddr, int64_t, nout); }
405
- if ( op == GR_MAXADDR ) { mxaddr = ALLOC_N(int64_t, nout); MEMZERO(mxaddr, int64_t, nout); }
533
+ if ( op == GR_MINADDR ) { mnaddr = ALLOCV_N(int64_t, h_mnaddr, nout); MEMZERO(mnaddr, int64_t, nout); }
534
+ if ( op == GR_MAXADDR ) { mxaddr = ALLOCV_N(int64_t, h_mxaddr, nout); MEMZERO(mxaddr, int64_t, nout); }
406
535
  if ( op == GR_ALL || op == GR_ANY ) {
407
- nz = ALLOC_N(ca_size_t, nout); MEMZERO(nz, ca_size_t, nout);
536
+ nz = ALLOCV_N(ca_size_t, h_nz, nout); MEMZERO(nz, ca_size_t, nout);
408
537
  }
409
538
 
410
539
  /* min_addr / max_addr need the flat raveled source address of each cell.
@@ -420,7 +549,7 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
420
549
  int band_axis[CA_RANK_MAX]; int nband_axes = 0;
421
550
  for ( int8_t k = 0; k < src->ndim; k++ )
422
551
  if ( ! is_group[k] ) band_axis[nband_axes++] = k;
423
- band_addr = ALLOC_N(ca_size_t, band > 0 ? band : 1);
552
+ band_addr = ALLOCV_N(ca_size_t, h_band, band > 0 ? band : 1);
424
553
  for ( ca_size_t bb = 0; bb < band; bb++ ) {
425
554
  ca_size_t rem = bb, addr = 0;
426
555
  for ( int j = nband_axes - 1; j >= 0; j-- ) { /* last band axis fastest */
@@ -432,7 +561,7 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
432
561
  }
433
562
  }
434
563
 
435
- /* --- compute pass(es), native dtype dispatch, no forced float64 cast ---
564
+ /* --- compute pass(es), native data type dispatch, no forced float64 cast ---
436
565
  variance / stddev use a centred two-pass (= matches CArray's own
437
566
  variance, avoids the one-pass sumsq cancellation that breaks ε-close
438
567
  for small near-constant groups). Pass 1 fills sum + cnt; sum is then
@@ -443,34 +572,35 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
443
572
  GROUP_DISPATCH( cnt[o] += 1; sum[o] += v; );
444
573
  for ( ca_size_t o = 0; o < nout; o++ )
445
574
  if ( cnt[o] > 0 ) sum[o] /= (double) cnt[o]; /* sum -> mean */
446
- sumsq = ALLOC_N(double, nout); MEMZERO(sumsq, double, nout);
575
+ sumsq = ALLOCV_N(double, h_sumsq, nout); MEMZERO(sumsq, double, nout);
447
576
  GROUP_DISPATCH( { double _d = v - sum[o]; sumsq[o] += _d * _d; } );
448
577
  }
449
- else if ( op == GR_MINADDR ) {
450
- GROUP_DISPATCH(
451
- cnt[o] += 1;
452
- if ( v < mn[o] ) {
453
- ca_size_t faddr = band_addr[b] + gaddr;
454
- mn[o] = v; mnaddr[o] = (int64_t) faddr;
455
- }
456
- );
457
- }
458
- else if ( op == GR_MAXADDR ) {
459
- GROUP_DISPATCH(
460
- cnt[o] += 1;
461
- if ( v > mx[o] ) {
462
- ca_size_t faddr = band_addr[b] + gaddr;
463
- mx[o] = v; mxaddr[o] = (int64_t) faddr;
464
- }
465
- );
578
+ else if ( op == GR_MINADDR ) { GROUP_DISPATCH_T(GMINADDR); }
579
+ else if ( op == GR_MAXADDR ) { GROUP_DISPATCH_T(GMAXADDR); }
580
+ else if ( op == GR_MIN ) { GROUP_DISPATCH_EXTREMUM(<); }
581
+ else if ( op == GR_MAX ) { GROUP_DISPATCH_EXTREMUM(>); }
582
+ else if ( op == GR_ACCUM ) {
583
+ MEMZERO(co->ptr, char, (size_t) nout * co->bytes);
584
+ switch ( ca->data_type ) {
585
+ case CA_BOOLEAN: GROUP_WALK(boolean8_t, GACC_XOR); break;
586
+ case CA_INT8: GROUP_WALK(int8_t, GACC_ADD(int8_t)); break;
587
+ case CA_UINT8: GROUP_WALK(uint8_t, GACC_ADD(uint8_t)); break;
588
+ case CA_INT16: GROUP_WALK(int16_t, GACC_ADD(int16_t)); break;
589
+ case CA_UINT16: GROUP_WALK(uint16_t, GACC_ADD(uint16_t)); break;
590
+ case CA_INT32: GROUP_WALK(int32_t, GACC_ADD(int32_t)); break;
591
+ case CA_UINT32: GROUP_WALK(uint32_t, GACC_ADD(uint32_t)); break;
592
+ case CA_INT64: GROUP_WALK(int64_t, GACC_ADD(int64_t)); break;
593
+ case CA_UINT64: GROUP_WALK(uint64_t, GACC_ADD(uint64_t)); break;
594
+ case CA_FLOAT32: GROUP_WALK(float, GACC_ADD(float)); break;
595
+ case CA_FLOAT64: GROUP_WALK(double, GACC_ADD(double)); break;
596
+ default: break;
597
+ }
466
598
  }
467
599
  else {
468
600
  GROUP_DISPATCH(
469
601
  cnt[o] += 1;
470
602
  if ( sum ) sum[o] += v;
471
603
  if ( prod ) prod[o] *= v;
472
- if ( mn ) { if ( v < mn[o] ) mn[o] = v; }
473
- if ( mx ) { if ( v > mx[o] ) mx[o] = v; }
474
604
  if ( nz ) { if ( v != 0.0 ) nz[o] += 1; }
475
605
  );
476
606
  }
@@ -498,10 +628,20 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
498
628
  int64_t *out = (int64_t *) co->ptr;
499
629
  int64_t *addr = ( op == GR_MINADDR ) ? mnaddr : mxaddr;
500
630
  for ( ca_size_t o = 0; o < nout; o++ ) {
501
- if ( cnt[o] == 0 ) { out[o] = 0; MARK_UNDEF(o); } /* empty -> UNDEF */
631
+ /* Nothing present, or nothing but NaN: either way no cell here won,
632
+ and the core answers UNDEF for both. */
633
+ if ( cnt[o] == 0 || ! seen_num[o] ) { out[o] = 0; MARK_UNDEF(o); }
502
634
  else out[o] = addr[o];
503
635
  }
504
636
  }
637
+ else if ( op == GR_ACCUM ) {
638
+ /* already folded in place, in the source's own type; empty groups hold 0 */
639
+ }
640
+ else if ( op == GR_MIN || op == GR_MAX ) {
641
+ /* the walk wrote the extremum in place, in the source's own type; a group
642
+ with no cell in it has nothing to report */
643
+ for ( ca_size_t o = 0; o < nout; o++ ) if ( cnt[o] == 0 ) MARK_UNDEF(o);
644
+ }
505
645
  else if ( op == GR_ALL ) {
506
646
  boolean8_t *out = (boolean8_t *) co->ptr; /* empty -> true (vacuous) */
507
647
  for ( ca_size_t o = 0; o < nout; o++ )
@@ -522,12 +662,6 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
522
662
  if ( cnt[o] == 0 ) { out[o] = 0.0; MARK_UNDEF(o); }
523
663
  else out[o] = sum[o] / (double) cnt[o];
524
664
  break;
525
- case GR_MIN:
526
- if ( cnt[o] == 0 ) { out[o] = 0.0; MARK_UNDEF(o); } else out[o] = mn[o];
527
- break;
528
- case GR_MAX:
529
- if ( cnt[o] == 0 ) { out[o] = 0.0; MARK_UNDEF(o); } else out[o] = mx[o];
530
- break;
531
665
  case GR_VARIANCE:
532
666
  case GR_STDDEV:
533
667
  if ( cnt[o] == 0 ) { out[o] = 0.0; MARK_UNDEF(o); }
@@ -546,17 +680,10 @@ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
546
680
  }
547
681
  #undef MARK_UNDEF
548
682
 
549
- for ( int bi = 0; bi < n_bundles; bi++ ) ca_detach(bundle_ca[bi]);
550
- xfree(cnt);
551
- if ( sum ) xfree(sum);
552
- if ( sumsq ) xfree(sumsq);
553
- if ( prod ) xfree(prod);
554
- if ( mn ) xfree(mn);
555
- if ( mx ) xfree(mx);
556
- if ( nz ) xfree(nz);
557
- if ( mnaddr ) xfree(mnaddr);
558
- if ( mxaddr ) xfree(mxaddr);
559
- if ( band_addr ) xfree(band_addr);
683
+ ALLOCV_END(h_cnt); ALLOCV_END(h_sum); ALLOCV_END(h_sumsq);
684
+ ALLOCV_END(h_prod); ALLOCV_END(h_seen); ALLOCV_END(h_xbuf);
685
+ ALLOCV_END(h_nz); ALLOCV_END(h_mnaddr); ALLOCV_END(h_mxaddr);
686
+ ALLOCV_END(h_band);
560
687
 
561
688
  RB_GC_GUARD(keep);
562
689
  return vout;
@@ -722,10 +849,10 @@ group_scan_build_plan (ca_iter_state *st, boolean8_t *m,
722
849
  }
723
850
 
724
851
  /* GROUP_SCAN_EXTREMUM_WALK(T, CMP): running extremum (cummax / cummin). The
725
- extremum keeps the source dtype (its magnitude never grows), so it holds a
852
+ extremum keeps the source data type (its magnitude never grows), so it holds a
726
853
  native T accumulator, not a widened double. The first member of a group
727
854
  emits its own value: a per-group `seen` byte initialises the accumulator
728
- lazily on first hit — no sentinel like HUGE_VAL, which an integer dtype could
855
+ lazily on first hit — no sentinel like HUGE_VAL, which an integer data type could
729
856
  not represent. CMP is > for max, < for min: a later member replaces the
730
857
  running extremum when `rv CMP acc`. A cell masked within its group holds the
731
858
  current extremum once a member has been seen (output NOT masked, like sum);
@@ -740,7 +867,8 @@ group_scan_build_plan (ca_iter_state *st, boolean8_t *m,
740
867
  boolean8_t *m; \
741
868
  ca_size_t b = 0; \
742
869
  int ready = 0; \
743
- T *acce = ALLOC_N(T, K_total); \
870
+ volatile VALUE h_acce = 0; /* freed by the unwind if the walk raises */ \
871
+ T *acce = ALLOCV_N(T, h_acce, K_total); \
744
872
  T *outp = (T *) co->ptr; \
745
873
  CA_FOR_EACH_SLAB(st, ca, axes, (int8_t) ngroup, CA_KERNEL_READ, p, m) { \
746
874
  ca_size_t SE = st.slab_elements; \
@@ -773,7 +901,7 @@ group_scan_build_plan (ca_iter_state *st, boolean8_t *m,
773
901
  } \
774
902
  b++; \
775
903
  } \
776
- xfree(acce); \
904
+ ALLOCV_END(h_acce); \
777
905
  } while (0)
778
906
 
779
907
  #define GROUP_SCAN_EXTREMUM_DISPATCH(CMP) \
@@ -863,18 +991,18 @@ group_scan_op_code (VALUE vop)
863
991
  * its group up to and including that cell, in row-major position order along
864
992
  * the grouped axes (per band).
865
993
 
866
- op / output dtype:
994
+ op / output data type:
867
995
  :cumsum -> float64, inclusive within-group running sum.
868
996
  :cumprod -> float64, inclusive within-group running product (init 1.0;
869
997
  float64 like cumsum since the product grows).
870
- :cummax -> source dtype, running within-group maximum (extrema do not
871
- grow magnitude, so the dtype is preserved; int stays int).
872
- :cummin -> source dtype, running within-group minimum.
998
+ :cummax -> source data type, running within-group maximum (extrema do not
999
+ grow magnitude, so the data type is preserved; int stays int).
1000
+ :cummin -> source data type, running within-group minimum.
873
1001
  :cumcount -> int64, 1-based within-group running count of present cells
874
1002
  (matching the core CArray#cumcount): the first present member
875
1003
  of a group emits 1, the next 2, ...
876
1004
  cumsum / cumprod keep float64 (matching the reduce siblings sum / prod);
877
- integer-preserving sum / prod is a deliberate non-goal (overflow / dtype
1005
+ integer-preserving sum / prod is a deliberate non-goal (overflow / data type
878
1006
  consistency), as on the reduce side. A CA_OBJECT source emits a CA_OBJECT
879
1007
  result for cumsum / cumprod / cummax / cummin (cumcount stays int64).
880
1008
 
@@ -954,11 +1082,25 @@ rb_ca_axis_group_scan (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
954
1082
  rb_raise(rb_eArgError, "axis_group_scan: bundle k must be positive");
955
1083
  }
956
1084
 
1085
+ /* The code table is copied out and the source let go of at once, rather
1086
+ than held attached for the length of the walk. Everything between here
1087
+ and the end of the walk can raise -- a validation just below, a gather
1088
+ that cannot convert a cell, an object-lane callback into Ruby -- and a
1089
+ raise jumps over every detach, leaving the source materialised with
1090
+ nothing left to release it. The copy rides a temporary buffer that the
1091
+ unwind collects. */
957
1092
  VALUE v32 = rb_ca_wrap_readonly(vcodes, INT2NUM(CA_INT32));
958
1093
  rb_ary_push((VALUE) keep, v32);
959
1094
  GetCArray(v32, bundle_ca[bi]);
960
- ca_attach(bundle_ca[bi]);
961
- bundle_codes[bi] = (int32_t *) bundle_ca[bi]->ptr;
1095
+ {
1096
+ size_t nbytes = (size_t) bundle_ca[bi]->elements * sizeof(int32_t);
1097
+ VALUE vbuf = rb_str_tmp_new((long) nbytes);
1098
+ rb_ary_push((VALUE) keep, vbuf);
1099
+ ca_attach(bundle_ca[bi]);
1100
+ memcpy(RSTRING_PTR(vbuf), bundle_ca[bi]->ptr, nbytes);
1101
+ ca_detach(bundle_ca[bi]);
1102
+ bundle_codes[bi] = (int32_t *) RSTRING_PTR(vbuf);
1103
+ }
962
1104
 
963
1105
  int nb = (int) RARRAY_LEN(vbaxes);
964
1106
  if ( nb <= 0 || nb > src->ndim ) {
@@ -1011,21 +1153,20 @@ rb_ca_axis_group_scan (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
1011
1153
 
1012
1154
  ca_size_t band = (group_prod > 0) ? (src->elements / group_prod) : 0;
1013
1155
 
1014
- /* --- supported dtype gate (CA_OBJECT handled by its own lane below) --- */
1156
+ /* --- supported data type gate (CA_OBJECT handled by its own lane below) --- */
1015
1157
  ca = src;
1016
1158
  switch ( src->data_type ) {
1017
1159
  case CA_BOOLEAN: case CA_INT8: case CA_UINT8: case CA_INT16: case CA_UINT16:
1018
1160
  case CA_INT32: case CA_UINT32: case CA_INT64: case CA_UINT64:
1019
1161
  case CA_FLOAT32: case CA_FLOAT64: case CA_OBJECT: break;
1020
1162
  default:
1021
- for ( int bi = 0; bi < n_bundles; bi++ ) ca_detach(bundle_ca[bi]);
1022
1163
  rb_raise(rb_eRuntimeError,
1023
1164
  "axis_group_scan: unsupported source data_type %d",
1024
1165
  src->data_type);
1025
1166
  }
1026
1167
 
1027
- /* --- output dtype per op: cumcount int64; cumsum / cumprod float64 (object
1028
- source -> object); cummax / cummin preserve the source dtype (object ->
1168
+ /* --- output data type per op: cumcount int64; cumsum / cumprod float64 (object
1169
+ source -> object); cummax / cummin preserve the source data type (object ->
1029
1170
  object). --- */
1030
1171
  int8_t out_dt;
1031
1172
  if ( op == GS_CUMCOUNT ) { out_dt = CA_INT64; }
@@ -1055,7 +1196,8 @@ rb_ca_axis_group_scan (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
1055
1196
  int band_axis[CA_RANK_MAX]; int nband_axes = 0;
1056
1197
  for ( int8_t k = 0; k < src->ndim; k++ )
1057
1198
  if ( ! is_group[k] ) band_axis[nband_axes++] = k;
1058
- ca_size_t *band_addr = ALLOC_N(ca_size_t, band > 0 ? band : 1);
1199
+ volatile VALUE h_band2 = 0;
1200
+ ca_size_t *band_addr = ALLOCV_N(ca_size_t, h_band2, band > 0 ? band : 1);
1059
1201
  for ( ca_size_t bb = 0; bb < band; bb++ ) {
1060
1202
  ca_size_t rem = bb, addr = 0;
1061
1203
  for ( int j = nband_axes - 1; j >= 0; j-- ) {
@@ -1072,13 +1214,20 @@ rb_ca_axis_group_scan (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
1072
1214
  member / object identity init) are all tiny; acco (object running VALUE) is
1073
1215
  allocated only for the object arithmetic / extremum ops. */
1074
1216
  ca_size_t plan_n = (group_prod > 0) ? group_prod : 1;
1075
- ca_size_t *sw_code = ALLOC_N(ca_size_t, plan_n);
1076
- ca_size_t *sw_doff = ALLOC_N(ca_size_t, plan_n);
1077
- ca_size_t *sw_moff = ALLOC_N(ca_size_t, plan_n);
1078
- ca_size_t *sw_addr = ALLOC_N(ca_size_t, plan_n);
1079
- double *accd = ALLOC_N(double, K_total);
1080
- ca_size_t *accn = ALLOC_N(ca_size_t, K_total);
1081
- char *seen = ALLOC_N(char, K_total);
1217
+ /* Scratch that the unwind collects. The object lane calls back into Ruby
1218
+ for every cell, so a raise in the middle of the walk is ordinary here --
1219
+ an operand that will not coerce, a <=> that answers nil -- and it jumps
1220
+ over every release below. ALLOCV frees the buffer behind each holder
1221
+ whether the function returns or unwinds. */
1222
+ volatile VALUE h_code = 0, h_doff = 0, h_moff = 0, h_addr = 0,
1223
+ h_accd = 0, h_accn = 0, h_seen = 0, h_acco = 0;
1224
+ ca_size_t *sw_code = ALLOCV_N(ca_size_t, h_code, plan_n);
1225
+ ca_size_t *sw_doff = ALLOCV_N(ca_size_t, h_doff, plan_n);
1226
+ ca_size_t *sw_moff = ALLOCV_N(ca_size_t, h_moff, plan_n);
1227
+ ca_size_t *sw_addr = ALLOCV_N(ca_size_t, h_addr, plan_n);
1228
+ double *accd = ALLOCV_N(double, h_accd, K_total);
1229
+ ca_size_t *accn = ALLOCV_N(ca_size_t, h_accn, K_total);
1230
+ char *seen = ALLOCV_N(char, h_seen, K_total);
1082
1231
  VALUE *acco = NULL;
1083
1232
  double acc_init = ( op == GS_CUMPROD ) ? 1.0 : 0.0;
1084
1233
 
@@ -1101,7 +1250,7 @@ rb_ca_axis_group_scan (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
1101
1250
  }
1102
1251
  else { /* object running VALUE */
1103
1252
  VALUE *outo = (VALUE *) co->ptr;
1104
- acco = ALLOC_N(VALUE, K_total);
1253
+ acco = ALLOCV_N(VALUE, h_acco, K_total);
1105
1254
  switch ( op ) {
1106
1255
  case GS_CUMSUM:
1107
1256
  /* Identity 0 (matching the core object cumsum): a group's acc is lazily
@@ -1153,7 +1302,7 @@ rb_ca_axis_group_scan (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
1153
1302
  }
1154
1303
  }
1155
1304
  }
1156
- else { /* native dtype dispatch */
1305
+ else { /* native data type dispatch */
1157
1306
  switch ( op ) {
1158
1307
  case GS_CUMSUM: {
1159
1308
  double *outd = (double *) co->ptr;
@@ -1183,16 +1332,9 @@ rb_ca_axis_group_scan (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
1183
1332
  }
1184
1333
  #undef MARK_OUT_UNDEF
1185
1334
 
1186
- for ( int bi = 0; bi < n_bundles; bi++ ) ca_detach(bundle_ca[bi]);
1187
- xfree(band_addr);
1188
- xfree(sw_code);
1189
- xfree(sw_doff);
1190
- xfree(sw_moff);
1191
- xfree(sw_addr);
1192
- xfree(accd);
1193
- xfree(accn);
1194
- xfree(seen);
1195
- if ( acco ) xfree(acco);
1335
+ ALLOCV_END(h_band2); ALLOCV_END(h_code); ALLOCV_END(h_doff);
1336
+ ALLOCV_END(h_moff); ALLOCV_END(h_addr); ALLOCV_END(h_accd);
1337
+ ALLOCV_END(h_accn); ALLOCV_END(h_seen); ALLOCV_END(h_acco);
1196
1338
 
1197
1339
  RB_GC_GUARD(keep);
1198
1340
  return vout;