carray 3.0.1 → 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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +520 -0
  3. data/README.md +2 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +33 -4
  6. data/ext/ca_axis_group.c +202 -96
  7. data/ext/ca_categorical_iterator.c +108 -54
  8. data/ext/ca_kernel_iterator.c +317 -51
  9. data/ext/ca_kernel_iterator.h +142 -35
  10. data/ext/ca_obj_array.c +62 -20
  11. data/ext/ca_obj_block.c +4 -4
  12. data/ext/ca_obj_const_string.c +85 -26
  13. data/ext/ca_obj_face.c +24 -0
  14. data/ext/ca_obj_face.h +15 -0
  15. data/ext/ca_obj_fixlen_string.c +18 -5
  16. data/ext/ca_obj_meld.c +123 -25
  17. data/ext/ca_obj_object.c +8 -0
  18. data/ext/ca_obj_select.c +49 -34
  19. data/ext/ca_obj_stack.c +3 -8
  20. data/ext/ca_obj_stride.c +72 -1
  21. data/ext/ca_obj_string.c +8 -4
  22. data/ext/ca_obj_window.c +8 -2
  23. data/ext/ca_op_ipower.c +1 -2
  24. data/ext/ca_rng_normal.h +42 -0
  25. data/ext/ca_rng_xoshiro256pp.h +105 -0
  26. data/ext/ca_sweep_engine.c +307 -143
  27. data/ext/ca_sweep_engine.h +26 -5
  28. data/ext/carray.h +21 -2
  29. data/ext/carray_access.c +32 -20
  30. data/ext/carray_address_basis.c +590 -0
  31. data/ext/carray_broadcast.c +3 -3
  32. data/ext/carray_call_cfunc.c +667 -483
  33. data/ext/carray_cast.c +115 -41
  34. data/ext/carray_copy.c +55 -30
  35. data/ext/carray_core.c +83 -3
  36. data/ext/carray_count.c +9 -10
  37. data/ext/carray_factorize.c +46 -25
  38. data/ext/carray_internal.h +17 -0
  39. data/ext/carray_kernels_reduce_aggregate.c +168 -0
  40. data/ext/carray_kernels_reduce_cumulative.c +270 -1
  41. data/ext/carray_kernels_reduce_extreme.c +554 -8
  42. data/ext/carray_kernels_scan.c +4 -4
  43. data/ext/carray_kernels_search.c +94 -14
  44. data/ext/carray_loop.c +7 -1
  45. data/ext/carray_mask.c +23 -8
  46. data/ext/carray_median_percentile.c +55 -0
  47. data/ext/carray_operator.c +4 -4
  48. data/ext/carray_order.c +1 -1
  49. data/ext/carray_random.c +384 -40
  50. data/ext/carray_slab.c +13 -0
  51. data/ext/carray_sort.c +20 -22
  52. data/ext/mk_call_cfunc.rb +103 -116
  53. data/ext/mkkernel.rb +297 -29
  54. data/ext/ruby_carray.c +10 -1
  55. data/ext/version.h +4 -4
  56. data/lib/carray/autoload_carray.rb +5 -3
  57. data/lib/carray/autoload_method_extension.rb +12 -0
  58. data/lib/carray/axis_group.rb +77 -0
  59. data/lib/carray/basics.rb +4 -0
  60. data/lib/carray/block_iterator.rb +92 -16
  61. data/lib/carray/categorical.rb +150 -33
  62. data/lib/carray/categorical_iterator.rb +207 -80
  63. data/lib/carray/const_string.rb +131 -27
  64. data/lib/carray/construct.rb +40 -0
  65. data/lib/carray/data_type_extension.rb +3 -0
  66. data/lib/carray/data_type_limits.rb +91 -0
  67. data/lib/carray/fixlen_string.rb +1 -1
  68. data/lib/carray/frame/csv_parser.rb +11 -4
  69. data/lib/carray/frame/frame.rb +81 -10
  70. data/lib/carray/frame/group.rb +36 -3
  71. data/lib/carray/frame/io.rb +67 -15
  72. data/lib/carray/frame/records.rb +18 -4
  73. data/lib/carray/frame/verbs.rb +14 -11
  74. data/lib/carray/inspect.rb +42 -9
  75. data/lib/carray/iterator.rb +143 -0
  76. data/lib/carray/lazy.rb +0 -37
  77. data/lib/carray/mask_gap_fill.rb +3 -1
  78. data/lib/carray/methods/discovery_along.rb +74 -0
  79. data/lib/carray/methods/factorize.rb +50 -0
  80. data/lib/carray/methods/is_in.rb +13 -2
  81. data/lib/carray/methods/locate_addr.rb +75 -2
  82. data/lib/carray/methods/mask_duplicates.rb +35 -1
  83. data/lib/carray/methods/nunique.rb +22 -1
  84. data/lib/carray/methods/repeat.rb +110 -0
  85. data/lib/carray/methods/unique.rb +41 -1
  86. data/lib/carray/rng.rb +86 -0
  87. data/lib/carray/slab_iterator.rb +58 -13
  88. data/lib/carray/string_operation_extension.rb +5 -1
  89. data/lib/carray/time.rb +18 -2
  90. data/lib/carray/window_iterator.rb +142 -20
  91. data/lib/carray.rb +2 -0
  92. data/yard-stubs/ca_obj_block.rb +2 -7
  93. data/yard-stubs/ca_obj_window.rb +10 -2
  94. data/yard-stubs/carray_access.rb +1 -1
  95. data/yard-stubs/carray_broadcast.rb +1 -1
  96. data/yard-stubs/carray_core.rb +0 -80
  97. data/yard-stubs/carray_count.rb +7 -2
  98. data/yard-stubs/carray_lazy.rb +205 -0
  99. data/yard-stubs/carray_math.rb +1486 -3
  100. data/yard-stubs/carray_median_percentile.rb +16 -2
  101. data/yard-stubs/carray_order.rb +9 -69
  102. data/yard-stubs/carray_slab.rb +9 -7
  103. data/yard-stubs/carray_sort.rb +7 -5
  104. metadata +9 -1
@@ -0,0 +1,105 @@
1
+ /* ---------------------------------------------------------------------------
2
+
3
+ ca_rng_xoshiro256pp.h -- the xoshiro256++ generator, as text
4
+
5
+ This file is not a header in the usual sense. It carries no include
6
+ guard and includes nothing, because it is read two ways:
7
+
8
+ - ext/carray_random.c #includes it, and the extension compiles it.
9
+ That is what `CArray#random!(rng: r)` runs.
10
+
11
+ - CArray::Rng::SOURCE[:xoshiro256pp] is this file's text, read at
12
+ runtime. carray-jit pastes it into the translation unit it
13
+ generates for a kernel, beside its own `static inline` helpers.
14
+ That is what a kernel's `random(rng:)` runs.
15
+
16
+ One text, so the two cannot drift: a kernel that draws after
17
+ `random!` continues the same sequence because it is running the same
18
+ code, not because two implementations were checked against each
19
+ other. Anything added here has to stay pasteable -- `static inline`,
20
+ no directives, and nothing beyond <stdint.h>, which both sides have.
21
+
22
+ xoshiro256++ 1.0 by David Blackman and Sebastiano Vigna, released to
23
+ the public domain (https://prng.di.unimi.it/xoshiro256plusplus.c).
24
+ The state is seeded through splitmix64, as its authors prescribe.
25
+
26
+ The state is held as int64_t[4] rather than uint64_t[4] so that it is
27
+ a plain CA_INT64 array a caller can look at. C says an object may be
28
+ read through the corresponding signed or unsigned type, so the cast
29
+ below is the language's own allowance and not a reinterpretation.
30
+
31
+ --------------------------------------------------------------------------- */
32
+
33
+ /* splitmix64: the state seeder. A single 64-bit seed is a poor state
34
+ for xoshiro on its own -- an all-but-zero state takes many draws to
35
+ escape -- so the seed is stretched through a generator whose output
36
+ is well mixed from the first call. */
37
+ static inline uint64_t
38
+ ca_splitmix64_next (uint64_t *x)
39
+ {
40
+ uint64_t z = (*x += 0x9E3779B97F4A7C15ULL);
41
+ z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
42
+ z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
43
+ return z ^ (z >> 31);
44
+ }
45
+
46
+ /* Fill a state from one seed. */
47
+ static inline void
48
+ ca_xoshiro256pp_seed (int64_t *s, uint64_t seed)
49
+ {
50
+ uint64_t *u = (uint64_t *) s;
51
+ uint64_t x = seed;
52
+ u[0] = ca_splitmix64_next(&x);
53
+ u[1] = ca_splitmix64_next(&x);
54
+ u[2] = ca_splitmix64_next(&x);
55
+ u[3] = ca_splitmix64_next(&x);
56
+ }
57
+
58
+ static inline uint64_t
59
+ ca_xoshiro256pp_rotl (uint64_t x, int k)
60
+ {
61
+ return (x << k) | (x >> (64 - k));
62
+ }
63
+
64
+ /* One draw: 64 random bits, the state advanced. */
65
+ static inline uint64_t
66
+ ca_xoshiro256pp_next (int64_t *s)
67
+ {
68
+ uint64_t *u = (uint64_t *) s;
69
+ const uint64_t result = ca_xoshiro256pp_rotl(u[0] + u[3], 23) + u[0];
70
+ const uint64_t t = u[1] << 17;
71
+
72
+ u[2] ^= u[0];
73
+ u[3] ^= u[1];
74
+ u[1] ^= u[2];
75
+ u[0] ^= u[3];
76
+ u[2] ^= t;
77
+ u[3] = ca_xoshiro256pp_rotl(u[3], 45);
78
+
79
+ return result;
80
+ }
81
+
82
+ /* A double in [0, 1). The top 53 bits are the ones taken: that is the
83
+ whole mantissa, so no two draws collide merely because the generator
84
+ handed back fewer bits than a double can hold. */
85
+ static inline double
86
+ ca_xoshiro256pp_next_real (int64_t *s)
87
+ {
88
+ return (double) (ca_xoshiro256pp_next(s) >> 11) * 0x1.0p-53;
89
+ }
90
+
91
+ /* One standard normal, which is two draws. ca_rng_normal is what turns
92
+ them into one, and it is in a file of its own because it belongs to no
93
+ generator.
94
+
95
+ The two draws are taken into locals rather than written as two
96
+ arguments: C does not say which order a call's arguments are
97
+ evaluated in, and these two are not interchangeable -- they advance a
98
+ state. */
99
+ static inline double
100
+ ca_xoshiro256pp_next_normal (int64_t *s)
101
+ {
102
+ const double u1 = ca_xoshiro256pp_next_real(s);
103
+ const double u2 = ca_xoshiro256pp_next_real(s);
104
+ return ca_rng_normal(u1, u2);
105
+ }
@@ -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,65 +305,12 @@ ca_sweep_release (ca_sweep_state_t *st)
158
305
 
159
306
  /* ===== Chunked path implementation ===== */
160
307
 
161
- void
162
- ca_sweep_acquire_chunked (ca_sweep_state_t *st)
308
+ static VALUE
309
+ ca_sweep_acquire_chunked_body (VALUE arg)
163
310
  {
311
+ ca_sweep_acquire_ctx_t *ctx = (ca_sweep_acquire_ctx_t *) arg;
312
+ ca_sweep_state_t *st = ctx->st;
164
313
  int k_op;
165
- int any_input_mask = 0;
166
- CArray *shape_donor = NULL;
167
-
168
- if ((int) strlen(st->fsync) != st->n_ops) {
169
- rb_raise(rb_eRuntimeError,
170
- "[BUG] invalid length of fsync arg in %s (expected %d)",
171
- st->src_label ? st->src_label : "ca_sweep_acquire_chunked",
172
- st->n_ops);
173
- }
174
-
175
- for (k_op = 0; k_op < st->n_ops; k_op++) {
176
- st->base[k_op] = NULL;
177
- st->base_orig[k_op] = NULL;
178
- st->owned_buf[k_op] = NULL;
179
- st->attached[k_op] = 0;
180
- }
181
- st->m0 = NULL;
182
- st->mask_scratch = NULL;
183
- st->n_kernel = 1;
184
- st->chunk_off = 0;
185
- st->chunk_n = 0;
186
- st->chunk_n_max = 0;
187
- st->inner = 1;
188
- st->chunked_state = 0;
189
-
190
- /* compute broadcast shape (n_kernel + strides) from operand shapes.
191
- * scalar operands collapse to stride 0; non-scalar operands must agree
192
- * on element count. shape_donor is the first non-scalar operand and
193
- * defines the chunking inner-axis size. */
194
- for (k_op = 0; k_op < st->n_ops; k_op++) {
195
- CArray *ca = st->cx[k_op];
196
- if (ca_is_scalar(ca)) {
197
- st->stride[k_op] = 0;
198
- } else {
199
- st->stride[k_op] = ca->bytes;
200
- if (st->n_kernel == 1) {
201
- st->n_kernel = ca->elements;
202
- shape_donor = ca;
203
- } else if (ca->elements != st->n_kernel) {
204
- rb_raise(rb_eRuntimeError, "data size mismatch in operation");
205
- }
206
- }
207
- }
208
-
209
- /* chunk-size policy: inner = donor's product of dims[1..]; chunk_n_max
210
- * = compute_n on donor->bytes (= type-dependent 32KB target). */
211
- if (shape_donor) {
212
- st->inner = ca_chunk_inner_size(shape_donor);
213
- st->chunk_n_max = ca_chunk_compute_n(st->n_kernel, st->inner,
214
- shape_donor->bytes);
215
- } else {
216
- /* all-scalar: single 1-cell chunk */
217
- st->inner = 1;
218
- st->chunk_n_max = 1;
219
- }
220
314
 
221
315
  /* per-operand acquire:
222
316
  * OUTPUT (fsync == '1') : ca_attach + base_orig = ca->ptr (legitimate)
@@ -226,14 +320,11 @@ ca_sweep_acquire_chunked (ca_sweep_state_t *st)
226
320
  */
227
321
  for (k_op = 0; k_op < st->n_ops; k_op++) {
228
322
  CArray *ca = st->cx[k_op];
229
- if (st->fsync[k_op] == '1') {
323
+ if (st->fsync[k_op] == '1' || ca_is_scalar(ca) ||
324
+ ca_attach_is_alias(ca)) {
230
325
  ca_attach(ca);
231
- st->base_orig[k_op] = (char *) ca->ptr;
232
326
  st->attached[k_op] = 1;
233
- } else if (ca_is_scalar(ca) || ca_attach_is_alias(ca)) {
234
- ca_attach(ca);
235
327
  st->base_orig[k_op] = (char *) ca->ptr;
236
- st->attached[k_op] = 1;
237
328
  } else {
238
329
  /* non-alias non-scalar INPUT: arena scratch sized for chunk_n_max */
239
330
  ca_size_t scratch_bytes = st->chunk_n_max * ca->bytes;
@@ -251,19 +342,7 @@ ca_sweep_acquire_chunked (ca_sweep_state_t *st)
251
342
  * scaling with the operand, which is the thing this path exists to avoid:
252
343
  * at 1 byte per cell it is an eighth of an f64 operand, but an eighth of
253
344
  * unbounded is still unbounded. */
254
- for (k_op = 0; k_op < st->n_ops; k_op++) {
255
- if (st->fsync[k_op] == '0' && ca_has_mask(st->cx[k_op])) {
256
- any_input_mask = 1;
257
- break;
258
- }
259
- }
260
- if (any_input_mask && st->no_mask) {
261
- rb_raise(rb_eRuntimeError,
262
- "%s: masked INPUT not allowed in NO_MASK form "
263
- "(use the *_MASKED form to handle masked cells explicitly)",
264
- st->src_label ? st->src_label : "ca_sweep_acquire_chunked");
265
- }
266
- if (any_input_mask) {
345
+ if (ctx->any_input_mask) {
267
346
  st->m0 = (boolean8_t *) ca_lazy_arena_acquire(st->chunk_n_max);
268
347
  st->mask_scratch = (boolean8_t *) ca_lazy_arena_acquire(st->chunk_n_max);
269
348
  memset(st->m0, 0, st->chunk_n_max);
@@ -283,6 +362,68 @@ ca_sweep_acquire_chunked (ca_sweep_state_t *st)
283
362
  * once more in release), never at acquire. Author per-cell m_out writes
284
363
  * land in m0 during the chunk loop and must be captured after that loop
285
364
  * has run, not before it. */
365
+ return Qnil;
366
+ }
367
+
368
+ void
369
+ ca_sweep_acquire_chunked (ca_sweep_state_t *st)
370
+ {
371
+ ca_sweep_acquire_ctx_t ctx;
372
+ int k_op;
373
+ int tag = 0;
374
+ CArray *shape_donor = NULL;
375
+
376
+ if ((int) strlen(st->fsync) != st->n_ops) {
377
+ rb_raise(rb_eRuntimeError,
378
+ "[BUG] invalid length of fsync arg in %s (expected %d)",
379
+ st->src_label ? st->src_label : "ca_sweep_acquire_chunked",
380
+ st->n_ops);
381
+ }
382
+
383
+ for (k_op = 0; k_op < st->n_ops; k_op++) {
384
+ st->base[k_op] = NULL;
385
+ st->base_orig[k_op] = NULL;
386
+ st->owned_buf[k_op] = NULL;
387
+ st->attached[k_op] = 0;
388
+ }
389
+ st->m0 = NULL;
390
+ st->mask_scratch = NULL;
391
+ st->chunk_off = 0;
392
+ st->chunk_n = 0;
393
+ st->chunk_n_max = 0;
394
+ st->inner = 1;
395
+ st->chunked_state = 0;
396
+
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);
400
+
401
+ /* chunk-size policy: inner = donor's product of dims[1..]; chunk_n_max
402
+ * = compute_n on donor->bytes (= type-dependent 32KB target). */
403
+ if (shape_donor) {
404
+ st->inner = ca_chunk_inner_size(shape_donor);
405
+ st->chunk_n_max = ca_chunk_compute_n(st->n_kernel, st->inner,
406
+ shape_donor->bytes);
407
+ } else {
408
+ /* all-scalar: single 1-cell chunk */
409
+ st->inner = 1;
410
+ st->chunk_n_max = 1;
411
+ }
412
+
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
+ }
286
427
  }
287
428
 
288
429
  /* OR one INPUT operand's mask for the current chunk into m0. Gathered via
@@ -329,11 +470,40 @@ ca_sweep_flush_chunk_mask (ca_sweep_state_t *st)
329
470
  }
330
471
  }
331
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];
495
+ }
496
+ }
497
+
498
+ if (st->m0) ca_sweep_gather_chunk_mask(st, off, n);
499
+ return Qnil;
500
+ }
501
+
332
502
  int
333
503
  ca_sweep_next_chunk (ca_sweep_state_t *st)
334
504
  {
335
- int k_op;
336
505
  ca_size_t off, n;
506
+ int tag = 0;
337
507
 
338
508
  if (st->chunked_state == 0) {
339
509
  /* first chunk */
@@ -356,24 +526,16 @@ ca_sweep_next_chunk (ca_sweep_state_t *st)
356
526
  if (off + n > st->n_kernel) n = st->n_kernel - off;
357
527
  st->chunk_n = n;
358
528
 
359
- /* set up base[] for the upcoming chunk */
360
- for (k_op = 0; k_op < st->n_ops; k_op++) {
361
- CArray *ca = st->cx[k_op];
362
- if (st->stride[k_op] == 0) {
363
- /* scalar: stride 0, base is the single-cell ptr (base_orig) */
364
- st->base[k_op] = st->base_orig[k_op];
365
- } else if (st->base_orig[k_op]) {
366
- /* alias INPUT or OUTPUT: walk through ca->ptr by chunk_off */
367
- st->base[k_op] = st->base_orig[k_op] + off * st->stride[k_op];
368
- } else {
369
- /* non-alias INPUT: per-chunk gather into owned_buf (arena) */
370
- ca_chunked_gather(ca, off, n, st->owned_buf[k_op]);
371
- st->base[k_op] = st->owned_buf[k_op];
372
- }
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);
373
537
  }
374
538
 
375
- if (st->m0) ca_sweep_gather_chunk_mask(st, off, n);
376
-
377
539
  return 1;
378
540
  }
379
541
 
@@ -381,6 +543,8 @@ void
381
543
  ca_sweep_release_chunked (ca_sweep_state_t *st)
382
544
  {
383
545
  int k_op;
546
+ /* a walk that raised has already given everything back */
547
+ if (st->chunked_state == 3) return;
384
548
  /* The final chunk has no next_chunk call to flush it, so it is flushed
385
549
  * here. For INOUT_MASKED forms this is what captures the author's
386
550
  * per-cell m_out writes over that last chunk. */