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
@@ -22,28 +22,138 @@
22
22
  #include "ca_for_buffer.h"
23
23
  #include <string.h>
24
24
 
25
+ /* "[2, 3]" for an operand's shape. */
26
+ static VALUE
27
+ ca_sweep_shape_str (CArray *ca)
28
+ {
29
+ volatile VALUE s = rb_str_new_cstr("[");
30
+ int k;
31
+ for (k = 0; k < ca->ndim; k++) {
32
+ if (k > 0) rb_str_cat_cstr(s, ", ");
33
+ rb_str_catf(s, "%lld", (long long) ca->dim[k]);
34
+ }
35
+ rb_str_cat_cstr(s, "]");
36
+ return s;
37
+ }
38
+
25
39
  void
26
- ca_sweep_acquire (ca_sweep_state_t *st)
40
+ ca_sweep_refuse_shapes (CArray *a, CArray *b)
27
41
  {
28
- int k_op;
29
- int any_input_mask = 0;
42
+ volatile VALUE sa = ca_sweep_shape_str(a);
43
+ volatile VALUE sb = ca_sweep_shape_str(b);
44
+ rb_raise(rb_eArgError,
45
+ "shape mismatch between operands (%s and %s); a scalar pairs "
46
+ "with any array, two arrays only when their shapes agree",
47
+ StringValueCStr(sa), StringValueCStr(sb));
48
+ }
30
49
 
31
- if ((int) strlen(st->fsync) != st->n_ops) {
32
- rb_raise(rb_eRuntimeError,
33
- "[BUG] invalid length of fsync arg in %s (expected %d)",
34
- st->src_label ? st->src_label : "ca_sweep_acquire",
35
- st->n_ops);
50
+ int
51
+ ca_sweep_same_shape (CArray *a, CArray *b)
52
+ {
53
+ int k;
54
+ if (a->ndim != b->ndim) return 0;
55
+ for (k = 0; k < a->ndim; k++) {
56
+ if (a->dim[k] != b->dim[k]) return 0;
36
57
  }
58
+ return 1;
59
+ }
37
60
 
61
+ /* Pair the operands: a scalar walks with stride 0, every other operand
62
+ * must have the shape of the first non-scalar one, which is returned (NULL
63
+ * when all operands are scalars). Sets stride[] and n_kernel. Allocates
64
+ * nothing, so it runs before any operand is acquired. */
65
+ static CArray *
66
+ ca_sweep_pair_operands (ca_sweep_state_t *st)
67
+ {
68
+ CArray *donor = NULL;
69
+ int k_op;
70
+ st->n_kernel = 1;
38
71
  for (k_op = 0; k_op < st->n_ops; k_op++) {
39
- st->base[k_op] = NULL;
72
+ CArray *ca = st->cx[k_op];
73
+ if (ca_is_scalar(ca)) {
74
+ st->stride[k_op] = 0;
75
+ continue;
76
+ }
77
+ st->stride[k_op] = ca->bytes;
78
+ if (!donor) {
79
+ donor = ca;
80
+ st->n_kernel = ca->elements;
81
+ } else if (!ca_sweep_same_shape(donor, ca)) {
82
+ ca_sweep_refuse_shapes(donor, ca);
83
+ }
84
+ }
85
+ return donor;
86
+ }
87
+
88
+ /* Non-zero when any INPUT operand carries a mask. Asks, allocates
89
+ * nothing. */
90
+ static int
91
+ ca_sweep_any_input_mask (ca_sweep_state_t *st)
92
+ {
93
+ int k_op;
94
+ for (k_op = 0; k_op < st->n_ops; k_op++) {
95
+ if (st->fsync[k_op] == '0' && ca_has_mask(st->cx[k_op])) return 1;
96
+ }
97
+ return 0;
98
+ }
99
+
100
+ NORETURN(static void ca_sweep_refuse_masked_input (ca_sweep_state_t *st,
101
+ const char *fallback));
102
+
103
+ static void
104
+ ca_sweep_refuse_masked_input (ca_sweep_state_t *st, const char *fallback)
105
+ {
106
+ rb_raise(rb_eRuntimeError,
107
+ "%s: masked INPUT not allowed in NO_MASK form "
108
+ "(use the *_MASKED form to handle masked cells explicitly)",
109
+ st->src_label ? st->src_label : fallback);
110
+ }
111
+
112
+ /* Give back everything acquire has taken so far, without syncing: detach
113
+ * attached operands, free owned scratch (xmalloc on the whole-buffer path,
114
+ * arena on the chunked one) and the mask buffers. Called when a read
115
+ * raises part way through, so that the raise leaves nothing attached or
116
+ * allocated behind it. */
117
+ static void
118
+ ca_sweep_abort (ca_sweep_state_t *st, int chunked)
119
+ {
120
+ int k_op;
121
+ for (k_op = st->n_ops - 1; k_op >= 0; k_op--) {
122
+ if (st->attached[k_op]) {
123
+ st->attached[k_op] = 0;
124
+ ca_detach(st->cx[k_op]);
125
+ } else if (st->owned_buf[k_op]) {
126
+ if (chunked) ca_lazy_arena_release(st->owned_buf[k_op]);
127
+ else xfree(st->owned_buf[k_op]);
128
+ }
40
129
  st->owned_buf[k_op] = NULL;
41
- st->attached[k_op] = 0;
42
130
  }
43
- st->m0 = NULL;
44
- st->n_kernel = 1;
45
- /* CAREFUL: do not reset st->no_mask here — caller sets it before
46
- * acquire and the NO_MASK guard below consumes it. */
131
+ if (st->m0) {
132
+ if (chunked) ca_lazy_arena_release(st->m0);
133
+ else xfree(st->m0);
134
+ st->m0 = NULL;
135
+ }
136
+ if (chunked && st->mask_scratch) {
137
+ ca_lazy_arena_release(st->mask_scratch);
138
+ st->mask_scratch = NULL;
139
+ }
140
+ }
141
+
142
+ typedef struct {
143
+ ca_sweep_state_t *st;
144
+ int any_input_mask;
145
+ boolean8_t *ms; /* per-operand mask staging, whole path */
146
+ } ca_sweep_acquire_ctx_t;
147
+
148
+ /* The part of acquire that takes resources and reads operands, and so can
149
+ * raise holding them. Runs under rb_protect; every buffer is recorded in
150
+ * the state (or the ctx) before the read that may raise. */
151
+ static VALUE
152
+ ca_sweep_acquire_body (VALUE arg)
153
+ {
154
+ ca_sweep_acquire_ctx_t *ctx = (ca_sweep_acquire_ctx_t *) arg;
155
+ ca_sweep_state_t *st = ctx->st;
156
+ int k_op;
47
157
 
48
158
  /* Per-operand acquire:
49
159
  * OUTPUT (fsync == '1') -> ca_attach + base = ca->ptr
@@ -53,14 +163,10 @@ ca_sweep_acquire (ca_sweep_state_t *st)
53
163
  * copy carries the values into the kernel. */
54
164
  for (k_op = 0; k_op < st->n_ops; k_op++) {
55
165
  CArray *ca = st->cx[k_op];
56
- if (st->fsync[k_op] == '1') {
166
+ if (st->fsync[k_op] == '1' || ca_attach_is_alias(ca)) {
57
167
  ca_attach(ca);
58
- st->base[k_op] = (char *) ca->ptr;
59
168
  st->attached[k_op] = 1;
60
- } else if (ca_attach_is_alias(ca)) {
61
- ca_attach(ca);
62
169
  st->base[k_op] = (char *) ca->ptr;
63
- st->attached[k_op] = 1;
64
170
  } else {
65
171
  ca_size_t bytes_total = ca->elements * ca->bytes;
66
172
  st->owned_buf[k_op] = xmalloc(bytes_total);
@@ -69,36 +175,9 @@ ca_sweep_acquire (ca_sweep_state_t *st)
69
175
  }
70
176
  }
71
177
 
72
- /* compute n_kernel (= broadcast shape) and per-cell strides */
73
- for (k_op = 0; k_op < st->n_ops; k_op++) {
74
- CArray *ca = st->cx[k_op];
75
- if (ca_is_scalar(ca)) {
76
- st->stride[k_op] = 0;
77
- } else {
78
- st->stride[k_op] = ca->bytes;
79
- if (st->n_kernel == 1) {
80
- st->n_kernel = ca->elements;
81
- } else if (ca->elements != st->n_kernel) {
82
- rb_raise(rb_eRuntimeError, "data size mismatch in operation");
83
- }
84
- }
85
- }
86
-
87
178
  /* iter mask m0 = OR of INPUT operand masks, gathered via ca_xfer_all
88
179
  so no operand mask attach happens. Stays NULL if no INPUT masks. */
89
- for (k_op = 0; k_op < st->n_ops; k_op++) {
90
- if (st->fsync[k_op] == '0' && ca_has_mask(st->cx[k_op])) {
91
- any_input_mask = 1;
92
- break;
93
- }
94
- }
95
- if (any_input_mask && st->no_mask) {
96
- rb_raise(rb_eRuntimeError,
97
- "%s: masked INPUT not allowed in NO_MASK form "
98
- "(use the *_MASKED form to handle masked cells explicitly)",
99
- st->src_label ? st->src_label : "ca_sweep_acquire");
100
- }
101
- if (any_input_mask) {
180
+ if (ctx->any_input_mask) {
102
181
  st->m0 = xmalloc(st->n_kernel);
103
182
  memset(st->m0, 0, st->n_kernel);
104
183
  for (k_op = 0; k_op < st->n_ops; k_op++) {
@@ -111,11 +190,10 @@ ca_sweep_acquire (ca_sweep_state_t *st)
111
190
  ca_xfer_all(ca->mask, &bit, CA_XFER_GET);
112
191
  if (bit) memset(st->m0, 1, st->n_kernel);
113
192
  } else {
114
- boolean8_t *ms = xmalloc(st->n_kernel);
115
193
  ca_size_t j;
116
- ca_xfer_all(ca->mask, ms, CA_XFER_GET);
117
- for (j = 0; j < st->n_kernel; j++) st->m0[j] |= ms[j];
118
- xfree(ms);
194
+ if (!ctx->ms) ctx->ms = xmalloc(st->n_kernel);
195
+ ca_xfer_all(ca->mask, ctx->ms, CA_XFER_GET);
196
+ for (j = 0; j < st->n_kernel; j++) st->m0[j] |= ctx->ms[j];
119
197
  }
120
198
  }
121
199
  }
@@ -131,6 +209,75 @@ ca_sweep_acquire (ca_sweep_state_t *st)
131
209
  memcpy(ca->mask->ptr, st->m0, st->n_kernel);
132
210
  }
133
211
  }
212
+ return Qnil;
213
+ }
214
+
215
+ void
216
+ ca_sweep_acquire (ca_sweep_state_t *st)
217
+ {
218
+ ca_sweep_acquire_ctx_t ctx;
219
+ int k_op;
220
+ int tag = 0;
221
+
222
+ if ((int) strlen(st->fsync) != st->n_ops) {
223
+ rb_raise(rb_eRuntimeError,
224
+ "[BUG] invalid length of fsync arg in %s (expected %d)",
225
+ st->src_label ? st->src_label : "ca_sweep_acquire",
226
+ st->n_ops);
227
+ }
228
+
229
+ for (k_op = 0; k_op < st->n_ops; k_op++) {
230
+ st->base[k_op] = NULL;
231
+ st->owned_buf[k_op] = NULL;
232
+ st->attached[k_op] = 0;
233
+ }
234
+ st->m0 = NULL;
235
+ /* CAREFUL: do not reset st->no_mask here — caller sets it before
236
+ * acquire and the NO_MASK guard below consumes it. */
237
+
238
+ /* Refusals come first, while nothing is held. */
239
+ ca_sweep_pair_operands(st);
240
+ ctx.st = st;
241
+ ctx.ms = NULL;
242
+ ctx.any_input_mask = ca_sweep_any_input_mask(st);
243
+ if (ctx.any_input_mask && st->no_mask) {
244
+ ca_sweep_refuse_masked_input(st, "ca_sweep_acquire");
245
+ }
246
+
247
+ rb_protect(ca_sweep_acquire_body, (VALUE) &ctx, &tag);
248
+ if (ctx.ms) xfree(ctx.ms);
249
+ if (tag) {
250
+ ca_sweep_abort(st, 0);
251
+ rb_jump_tag(tag);
252
+ }
253
+ }
254
+
255
+ void
256
+ ca_sweep_run (ca_sweep_state_t *st, VALUE (*walk)(VALUE), VALUE arg)
257
+ {
258
+ int tag = 0;
259
+ rb_protect(walk, arg, &tag);
260
+ if (tag) {
261
+ ca_sweep_abort(st, 0);
262
+ rb_jump_tag(tag);
263
+ }
264
+ ca_sweep_release(st);
265
+ }
266
+
267
+ void
268
+ ca_sweep_run_chunked (ca_sweep_state_t *st, VALUE (*walk)(VALUE), VALUE arg)
269
+ {
270
+ int tag = 0;
271
+ rb_protect(walk, arg, &tag);
272
+ if (tag) {
273
+ /* a raise inside ca_sweep_next_chunk has already given back */
274
+ if (st->chunked_state != 3) {
275
+ ca_sweep_abort(st, 1);
276
+ st->chunked_state = 3;
277
+ }
278
+ rb_jump_tag(tag);
279
+ }
280
+ ca_sweep_release_chunked(st);
134
281
  }
135
282
 
136
283
  void
@@ -158,11 +305,72 @@ ca_sweep_release (ca_sweep_state_t *st)
158
305
 
159
306
  /* ===== Chunked path implementation ===== */
160
307
 
308
+ static VALUE
309
+ ca_sweep_acquire_chunked_body (VALUE arg)
310
+ {
311
+ ca_sweep_acquire_ctx_t *ctx = (ca_sweep_acquire_ctx_t *) arg;
312
+ ca_sweep_state_t *st = ctx->st;
313
+ int k_op;
314
+
315
+ /* per-operand acquire:
316
+ * OUTPUT (fsync == '1') : ca_attach + base_orig = ca->ptr (legitimate)
317
+ * INPUT alias : ca_attach + base_orig = ca->ptr (zero-copy)
318
+ * INPUT non-alias non-scalar : arena chunk scratch of chunk_n_max bytes
319
+ * INPUT scalar : ca_attach + base_orig = ca->ptr (1 cell)
320
+ */
321
+ for (k_op = 0; k_op < st->n_ops; k_op++) {
322
+ CArray *ca = st->cx[k_op];
323
+ if (st->fsync[k_op] == '1' || ca_is_scalar(ca) ||
324
+ ca_attach_is_alias(ca)) {
325
+ ca_attach(ca);
326
+ st->attached[k_op] = 1;
327
+ st->base_orig[k_op] = (char *) ca->ptr;
328
+ } else {
329
+ /* non-alias non-scalar INPUT: arena scratch sized for chunk_n_max */
330
+ ca_size_t scratch_bytes = st->chunk_n_max * ca->bytes;
331
+ st->owned_buf[k_op] = (char *) ca_lazy_arena_acquire(scratch_bytes);
332
+ /* base_orig stays NULL -- per-chunk gather lands the data in
333
+ * owned_buf[k_op]; base[k_op] will be set to owned_buf[k_op] at
334
+ * each chunk boundary. */
335
+ }
336
+ }
337
+
338
+ /* iter mask = OR of INPUT operand masks, chunk-sized (chunk_n_max bytes)
339
+ * and re-gathered per chunk in ca_sweep_next_chunk. m0 is indexed by the
340
+ * offset within the chunk, m0[k] for k < chunk_n -- NOT by the flat cell
341
+ * index. Sizing it to n_kernel instead would leave one allocation still
342
+ * scaling with the operand, which is the thing this path exists to avoid:
343
+ * at 1 byte per cell it is an eighth of an f64 operand, but an eighth of
344
+ * unbounded is still unbounded. */
345
+ if (ctx->any_input_mask) {
346
+ st->m0 = (boolean8_t *) ca_lazy_arena_acquire(st->chunk_n_max);
347
+ st->mask_scratch = (boolean8_t *) ca_lazy_arena_acquire(st->chunk_n_max);
348
+ memset(st->m0, 0, st->chunk_n_max);
349
+ /* The OUTPUT masks have to exist before the walk starts, because each
350
+ * chunk's m0 is flushed into them as the walk passes -- there is no
351
+ * whole m0 left at release time to propagate in one go. */
352
+ for (k_op = 0; k_op < st->n_ops; k_op++) {
353
+ CArray *ca = st->cx[k_op];
354
+ if (st->fsync[k_op] != '1') continue;
355
+ ca_update_mask(ca);
356
+ if (!ca->mask) ca_create_mask(ca);
357
+ }
358
+ }
359
+
360
+ /* CAREFUL: m0 -> OUTPUT mask propagation happens per chunk, at the point
361
+ * the chunk is finished (= the top of the next ca_sweep_next_chunk, and
362
+ * once more in release), never at acquire. Author per-cell m_out writes
363
+ * land in m0 during the chunk loop and must be captured after that loop
364
+ * has run, not before it. */
365
+ return Qnil;
366
+ }
367
+
161
368
  void
162
369
  ca_sweep_acquire_chunked (ca_sweep_state_t *st)
163
370
  {
371
+ ca_sweep_acquire_ctx_t ctx;
164
372
  int k_op;
165
- int any_input_mask = 0;
373
+ int tag = 0;
166
374
  CArray *shape_donor = NULL;
167
375
 
168
376
  if ((int) strlen(st->fsync) != st->n_ops) {
@@ -179,34 +387,19 @@ ca_sweep_acquire_chunked (ca_sweep_state_t *st)
179
387
  st->attached[k_op] = 0;
180
388
  }
181
389
  st->m0 = NULL;
182
- st->n_kernel = 1;
390
+ st->mask_scratch = NULL;
183
391
  st->chunk_off = 0;
184
392
  st->chunk_n = 0;
185
393
  st->chunk_n_max = 0;
186
394
  st->inner = 1;
187
395
  st->chunked_state = 0;
188
396
 
189
- /* compute broadcast shape (n_kernel + strides) from operand shapes.
190
- * scalar operands collapse to stride 0; non-scalar operands must agree
191
- * on element count. shape_donor is the first non-scalar operand and
192
- * defines the chunking inner-axis size. */
193
- for (k_op = 0; k_op < st->n_ops; k_op++) {
194
- CArray *ca = st->cx[k_op];
195
- if (ca_is_scalar(ca)) {
196
- st->stride[k_op] = 0;
197
- } else {
198
- st->stride[k_op] = ca->bytes;
199
- if (st->n_kernel == 1) {
200
- st->n_kernel = ca->elements;
201
- shape_donor = ca;
202
- } else if (ca->elements != st->n_kernel) {
203
- rb_raise(rb_eRuntimeError, "data size mismatch in operation");
204
- }
205
- }
206
- }
397
+ /* shape_donor is the first non-scalar operand and defines the chunking
398
+ * inner-axis size. */
399
+ shape_donor = ca_sweep_pair_operands(st);
207
400
 
208
401
  /* chunk-size policy: inner = donor's product of dims[1..]; chunk_n_max
209
- * = compute_n on donor->bytes (= dtype-dependent 32KB target). */
402
+ * = compute_n on donor->bytes (= type-dependent 32KB target). */
210
403
  if (shape_donor) {
211
404
  st->inner = ca_chunk_inner_size(shape_donor);
212
405
  st->chunk_n_max = ca_chunk_compute_n(st->n_kernel, st->inner,
@@ -217,85 +410,108 @@ ca_sweep_acquire_chunked (ca_sweep_state_t *st)
217
410
  st->chunk_n_max = 1;
218
411
  }
219
412
 
220
- /* per-operand acquire:
221
- * OUTPUT (fsync == '1') : ca_attach + base_orig = ca->ptr (legitimate)
222
- * INPUT alias : ca_attach + base_orig = ca->ptr (zero-copy)
223
- * INPUT non-alias non-scalar : arena chunk scratch of chunk_n_max bytes
224
- * INPUT scalar : ca_attach + base_orig = ca->ptr (1 cell)
225
- */
413
+ /* Refusals come first, while nothing is held. */
414
+ ctx.st = st;
415
+ ctx.ms = NULL;
416
+ ctx.any_input_mask = ca_sweep_any_input_mask(st);
417
+ if (ctx.any_input_mask && st->no_mask) {
418
+ ca_sweep_refuse_masked_input(st, "ca_sweep_acquire_chunked");
419
+ }
420
+
421
+ rb_protect(ca_sweep_acquire_chunked_body, (VALUE) &ctx, &tag);
422
+ if (tag) {
423
+ ca_sweep_abort(st, 1);
424
+ st->chunked_state = 3;
425
+ rb_jump_tag(tag);
426
+ }
427
+ }
428
+
429
+ /* OR one INPUT operand's mask for the current chunk into m0. Gathered via
430
+ * ca_chunked_gather rather than attached, so no operand mask is ever
431
+ * attached -- the same invariant the whole-buffer path keeps with
432
+ * ca_xfer_all. */
433
+ static void
434
+ ca_sweep_gather_chunk_mask (ca_sweep_state_t *st, ca_size_t off, ca_size_t n)
435
+ {
436
+ int k_op;
437
+ ca_size_t j;
438
+
439
+ memset(st->m0, 0, n);
226
440
  for (k_op = 0; k_op < st->n_ops; k_op++) {
227
441
  CArray *ca = st->cx[k_op];
228
- if (st->fsync[k_op] == '1') {
229
- ca_attach(ca);
230
- st->base_orig[k_op] = (char *) ca->ptr;
231
- st->attached[k_op] = 1;
232
- } else if (ca_is_scalar(ca) || ca_attach_is_alias(ca)) {
233
- ca_attach(ca);
234
- st->base_orig[k_op] = (char *) ca->ptr;
235
- st->attached[k_op] = 1;
442
+ if (st->fsync[k_op] != '0') continue;
443
+ ca_update_mask(ca);
444
+ if (!ca->mask) continue;
445
+ if (ca_is_scalar(ca)) {
446
+ /* one cell, broadcast across the chunk */
447
+ boolean8_t bit = 0;
448
+ ca_xfer_all(ca->mask, &bit, CA_XFER_GET);
449
+ if (bit) memset(st->m0, 1, n);
236
450
  } else {
237
- /* non-alias non-scalar INPUT: arena scratch sized for chunk_n_max */
238
- ca_size_t scratch_bytes = st->chunk_n_max * ca->bytes;
239
- st->owned_buf[k_op] = (char *) ca_lazy_arena_acquire(scratch_bytes);
240
- /* base_orig stays NULL -- per-chunk gather lands the data in
241
- * owned_buf[k_op]; base[k_op] will be set to owned_buf[k_op] at
242
- * each chunk boundary. */
451
+ ca_chunked_gather(ca->mask, off, n, st->mask_scratch);
452
+ for (j = 0; j < n; j++) st->m0[j] |= st->mask_scratch[j];
243
453
  }
244
454
  }
455
+ }
245
456
 
246
- /* iter mask = OR of INPUT operand masks (full size, n_kernel bytes).
247
- * Per-chunk mask gather is a future optim; this MVP gathers once. */
457
+ /* Copy the chunk just finished out to every OUTPUT operand's mask. Runs
458
+ * after the author's loop over that chunk, so m_out writes into m0 are
459
+ * carried through. */
460
+ static void
461
+ ca_sweep_flush_chunk_mask (ca_sweep_state_t *st)
462
+ {
463
+ int k_op;
464
+ if (!st->m0 || st->chunked_state != 1 || st->chunk_n == 0) return;
248
465
  for (k_op = 0; k_op < st->n_ops; k_op++) {
249
- if (st->fsync[k_op] == '0' && ca_has_mask(st->cx[k_op])) {
250
- any_input_mask = 1;
251
- break;
252
- }
253
- }
254
- if (any_input_mask && st->no_mask) {
255
- rb_raise(rb_eRuntimeError,
256
- "%s: masked INPUT not allowed in NO_MASK form "
257
- "(use the *_MASKED form to handle masked cells explicitly)",
258
- st->src_label ? st->src_label : "ca_sweep_acquire_chunked");
466
+ CArray *ca = st->cx[k_op];
467
+ if (st->fsync[k_op] != '1') continue;
468
+ if (!ca->mask) continue;
469
+ memcpy((boolean8_t *) ca->mask->ptr + st->chunk_off, st->m0, st->chunk_n);
259
470
  }
260
- if (any_input_mask) {
261
- st->m0 = (boolean8_t *) ca_lazy_arena_acquire(st->n_kernel);
262
- memset(st->m0, 0, st->n_kernel);
263
- for (k_op = 0; k_op < st->n_ops; k_op++) {
264
- CArray *ca = st->cx[k_op];
265
- if (st->fsync[k_op] != '0') continue;
266
- ca_update_mask(ca);
267
- if (!ca->mask) continue;
268
- if (ca_is_scalar(ca)) {
269
- boolean8_t bit = 0;
270
- ca_xfer_all(ca->mask, &bit, CA_XFER_GET);
271
- if (bit) memset(st->m0, 1, st->n_kernel);
272
- } else {
273
- boolean8_t *ms = (boolean8_t *) ca_lazy_arena_acquire(st->n_kernel);
274
- ca_size_t j;
275
- ca_xfer_all(ca->mask, ms, CA_XFER_GET);
276
- for (j = 0; j < st->n_kernel; j++) st->m0[j] |= ms[j];
277
- ca_lazy_arena_release(ms);
278
- }
471
+ }
472
+
473
+ /* Set up base[] for the chunk at chunk_off / chunk_n: gather each
474
+ non-alias INPUT into its arena scratch, then the chunk's mask. */
475
+ static VALUE
476
+ ca_sweep_next_chunk_body (VALUE arg)
477
+ {
478
+ ca_sweep_state_t *st = (ca_sweep_state_t *) arg;
479
+ ca_size_t off = st->chunk_off;
480
+ ca_size_t n = st->chunk_n;
481
+ int k_op;
482
+
483
+ for (k_op = 0; k_op < st->n_ops; k_op++) {
484
+ CArray *ca = st->cx[k_op];
485
+ if (st->stride[k_op] == 0) {
486
+ /* scalar: stride 0, base is the single-cell ptr (base_orig) */
487
+ st->base[k_op] = st->base_orig[k_op];
488
+ } else if (st->base_orig[k_op]) {
489
+ /* alias INPUT or OUTPUT: walk through ca->ptr by chunk_off */
490
+ st->base[k_op] = st->base_orig[k_op] + off * st->stride[k_op];
491
+ } else {
492
+ /* non-alias INPUT: per-chunk gather into owned_buf (arena) */
493
+ ca_chunked_gather(ca, off, n, st->owned_buf[k_op]);
494
+ st->base[k_op] = st->owned_buf[k_op];
279
495
  }
280
496
  }
281
497
 
282
- /* CAREFUL: m0 -> OUTPUT mask propagation is deferred to release
283
- * time so that author per-cell m_out writes during the chunk loop
284
- * are captured in the final OUTPUT mask. Do not fold this into
285
- * acquire; doing so would clobber the author's writes. */
498
+ if (st->m0) ca_sweep_gather_chunk_mask(st, off, n);
499
+ return Qnil;
286
500
  }
287
501
 
288
502
  int
289
503
  ca_sweep_next_chunk (ca_sweep_state_t *st)
290
504
  {
291
- int k_op;
292
505
  ca_size_t off, n;
506
+ int tag = 0;
293
507
 
294
508
  if (st->chunked_state == 0) {
295
509
  /* first chunk */
296
510
  st->chunk_off = 0;
297
511
  st->chunked_state = 1;
298
512
  } else {
513
+ /* the chunk that just finished is the author's last word on its mask */
514
+ ca_sweep_flush_chunk_mask(st);
299
515
  /* advance */
300
516
  st->chunk_off += st->chunk_n;
301
517
  }
@@ -310,20 +526,14 @@ ca_sweep_next_chunk (ca_sweep_state_t *st)
310
526
  if (off + n > st->n_kernel) n = st->n_kernel - off;
311
527
  st->chunk_n = n;
312
528
 
313
- /* set up base[] for the upcoming chunk */
314
- for (k_op = 0; k_op < st->n_ops; k_op++) {
315
- CArray *ca = st->cx[k_op];
316
- if (st->stride[k_op] == 0) {
317
- /* scalar: stride 0, base is the single-cell ptr (base_orig) */
318
- st->base[k_op] = st->base_orig[k_op];
319
- } else if (st->base_orig[k_op]) {
320
- /* alias INPUT or OUTPUT: walk through ca->ptr by chunk_off */
321
- st->base[k_op] = st->base_orig[k_op] + off * st->stride[k_op];
322
- } else {
323
- /* non-alias INPUT: per-chunk gather into owned_buf (arena) */
324
- ca_chunked_gather(ca, off, n, st->owned_buf[k_op]);
325
- st->base[k_op] = st->owned_buf[k_op];
326
- }
529
+ /* The gathers read the operands and so can raise; a raise gives back
530
+ the walk's resources before it propagates, since the caller's release
531
+ is never reached. */
532
+ rb_protect(ca_sweep_next_chunk_body, (VALUE) st, &tag);
533
+ if (tag) {
534
+ ca_sweep_abort(st, 1);
535
+ st->chunked_state = 3;
536
+ rb_jump_tag(tag);
327
537
  }
328
538
 
329
539
  return 1;
@@ -333,19 +543,12 @@ void
333
543
  ca_sweep_release_chunked (ca_sweep_state_t *st)
334
544
  {
335
545
  int k_op;
336
- /* Propagate (possibly author-mutated) m0 to OUTPUT mask before
337
- * sync. For INOUT_MASKED forms this captures the author's per-cell
338
- * m_out writes; other forms behave the same as the whole-buffer
339
- * acquire-time propagation. */
340
- if (st->m0) {
341
- for (k_op = 0; k_op < st->n_ops; k_op++) {
342
- CArray *ca = st->cx[k_op];
343
- if (st->fsync[k_op] != '1') continue;
344
- ca_update_mask(ca);
345
- if (!ca->mask) ca_create_mask(ca);
346
- memcpy(ca->mask->ptr, st->m0, st->n_kernel);
347
- }
348
- }
546
+ /* a walk that raised has already given everything back */
547
+ if (st->chunked_state == 3) return;
548
+ /* The final chunk has no next_chunk call to flush it, so it is flushed
549
+ * here. For INOUT_MASKED forms this is what captures the author's
550
+ * per-cell m_out writes over that last chunk. */
551
+ ca_sweep_flush_chunk_mask(st);
349
552
  /* sync OUTPUTs (reverse order, regardless of whether chunk loop ran) */
350
553
  for (k_op = st->n_ops - 1; k_op >= 0; k_op--) {
351
554
  if (st->fsync[k_op] == '1') ca_sync(st->cx[k_op]);
@@ -363,6 +566,10 @@ ca_sweep_release_chunked (ca_sweep_state_t *st)
363
566
  ca_lazy_arena_release(st->m0);
364
567
  st->m0 = NULL;
365
568
  }
569
+ if (st->mask_scratch) {
570
+ ca_lazy_arena_release(st->mask_scratch);
571
+ st->mask_scratch = NULL;
572
+ }
366
573
  }
367
574
 
368
575
  /* ===== WHOLE_BUFFER function form (rb_ensure-protected) ===== */