carray 3.0.2 → 3.0.3

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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +3 -4
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +225 -0
  6. data/ext/ca_axis_group.c +177 -99
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +66 -71
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +663 -214
  14. data/ext/ca_kernel_iterator.h +231 -167
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +132 -22
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +1 -0
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +96 -25
  25. data/ext/ca_obj_face.c +406 -64
  26. data/ext/ca_obj_face.h +92 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +21 -21
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +189 -107
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +543 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +47 -49
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -118
  46. data/ext/ca_obj_stride.c +195 -34
  47. data/ext/ca_obj_string.c +17 -18
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +149 -27
  54. data/ext/ca_op_ipower.c +24 -11
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_sort_kernels.h +70 -0
  58. data/ext/ca_sweep_engine.c +102 -79
  59. data/ext/ca_sweep_engine.h +7 -20
  60. data/ext/ca_triop_dispatch.c +4 -1
  61. data/ext/ca_triop_dispatch.h +5 -2
  62. data/ext/caframe_csv_reader.c +463 -0
  63. data/ext/carray.h +70 -27
  64. data/ext/carray_access.c +455 -211
  65. data/ext/carray_address_basis.c +177 -36
  66. data/ext/carray_attribute.c +45 -3
  67. data/ext/carray_bincount.c +32 -2
  68. data/ext/carray_broadcast.c +46 -14
  69. data/ext/carray_call_cfunc.c +0 -28
  70. data/ext/carray_cast.c +1444 -209
  71. data/ext/carray_cast_func.rb +34 -21
  72. data/ext/carray_conversion.c +197 -73
  73. data/ext/carray_copy.c +32 -11
  74. data/ext/carray_core.c +514 -122
  75. data/ext/carray_count.c +140 -87
  76. data/ext/carray_element.c +18 -27
  77. data/ext/carray_factorize.c +569 -277
  78. data/ext/carray_generate.c +76 -40
  79. data/ext/carray_histogram.c +152 -94
  80. data/ext/carray_hold.c +2 -1
  81. data/ext/carray_index_classifier.c +63 -96
  82. data/ext/carray_internal.h +385 -3
  83. data/ext/carray_lazy.c +834 -79
  84. data/ext/carray_loop.c +81 -65
  85. data/ext/carray_mask.c +682 -316
  86. data/ext/carray_math_kernel.h +1 -0
  87. data/ext/carray_median_percentile.c +221 -76
  88. data/ext/carray_memory_view.c +254 -248
  89. data/ext/carray_operator.c +1089 -359
  90. data/ext/carray_order.c +133 -69
  91. data/ext/carray_partition.c +102 -56
  92. data/ext/carray_random.c +273 -239
  93. data/ext/carray_scatter.c +308 -65
  94. data/ext/carray_segment.c +257 -0
  95. data/ext/carray_slab.c +170 -70
  96. data/ext/carray_sort.c +215 -218
  97. data/ext/carray_sort_kernel.c +8 -1
  98. data/ext/carray_struct.c +99 -21
  99. data/ext/carray_test.c +167 -64
  100. data/ext/carray_time_text.c +1182 -0
  101. data/ext/carray_utils.c +363 -98
  102. data/ext/extconf.rb +27 -2
  103. data/ext/mk_call_cfunc.rb +0 -2
  104. data/ext/mk_pow5_table.rb +35 -0
  105. data/ext/mkkernel.rb +1076 -846
  106. data/ext/ruby_carray.c +7 -1
  107. data/ext/version.h +4 -4
  108. data/ext/version.rb +4 -2
  109. data/lib/carray/attribute.rb +39 -0
  110. data/lib/carray/autoload_carray.rb +19 -0
  111. data/lib/carray/autoload_method_extension.rb +6 -0
  112. data/lib/carray/axis_group.rb +174 -26
  113. data/lib/carray/basics.rb +91 -25
  114. data/lib/carray/bincount_nd.rb +6 -0
  115. data/lib/carray/block_iterator.rb +118 -48
  116. data/lib/carray/boolean_reduce.rb +21 -29
  117. data/lib/carray/categorical.rb +94 -26
  118. data/lib/carray/categorical_iterator.rb +333 -826
  119. data/lib/carray/complex.rb +36 -29
  120. data/lib/carray/conditional.rb +94 -7
  121. data/lib/carray/const_string.rb +46 -0
  122. data/lib/carray/frame/concat.rb +157 -6
  123. data/lib/carray/frame/convert.rb +13 -7
  124. data/lib/carray/frame/csv_parser.rb +589 -82
  125. data/lib/carray/frame/describe.rb +79 -0
  126. data/lib/carray/frame/frame.rb +124 -25
  127. data/lib/carray/frame/group.rb +189 -63
  128. data/lib/carray/frame/io.rb +412 -72
  129. data/lib/carray/frame/join.rb +134 -46
  130. data/lib/carray/frame/records.rb +37 -16
  131. data/lib/carray/frame/reshape.rb +448 -0
  132. data/lib/carray/frame/sort.rb +26 -2
  133. data/lib/carray/frame/verbs.rb +715 -48
  134. data/lib/carray/frame.rb +2 -0
  135. data/lib/carray/fuse_source.rb +111 -2
  136. data/lib/carray/fusion.rb +229 -38
  137. data/lib/carray/inspect.rb +29 -11
  138. data/lib/carray/iterator.rb +14 -2
  139. data/lib/carray/lazy.rb +54 -30
  140. data/lib/carray/mask_gap_fill.rb +66 -52
  141. data/lib/carray/math.rb +32 -19
  142. data/lib/carray/meld_reduce.rb +40 -7
  143. data/lib/carray/methods/align_addr.rb +8 -4
  144. data/lib/carray/methods/bincount.rb +2 -2
  145. data/lib/carray/methods/bit_string.rb +14 -3
  146. data/lib/carray/methods/composition.rb +16 -10
  147. data/lib/carray/methods/factorize.rb +2 -2
  148. data/lib/carray/methods/gather_nd.rb +13 -2
  149. data/lib/carray/methods/insert_block.rb +9 -14
  150. data/lib/carray/methods/is_in.rb +117 -4
  151. data/lib/carray/methods/join.rb +1 -1
  152. data/lib/carray/methods/locate_addr.rb +177 -36
  153. data/lib/carray/methods/mode.rb +18 -1
  154. data/lib/carray/methods/repeat.rb +1 -20
  155. data/lib/carray/methods/resize.rb +21 -23
  156. data/lib/carray/methods/snap.rb +34 -12
  157. data/lib/carray/methods/unique.rb +11 -2
  158. data/lib/carray/record_order.rb +208 -0
  159. data/lib/carray/runtime.rb +14 -11
  160. data/lib/carray/segment_iterator.rb +990 -0
  161. data/lib/carray/serialize.rb +197 -27
  162. data/lib/carray/slab_iterator.rb +51 -27
  163. data/lib/carray/stack.rb +10 -4
  164. data/lib/carray/string_operation_extension.rb +4 -3
  165. data/lib/carray/struct.rb +142 -51
  166. data/lib/carray/struct_builder.rb +110 -14
  167. data/lib/carray/table.rb +2 -2
  168. data/lib/carray/time.rb +517 -103
  169. data/lib/carray/window_iterator.rb +127 -72
  170. data/lib/carray.rb +2 -2
  171. data/yard-stubs/ca_obj_array.rb +4 -4
  172. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  173. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  174. data/yard-stubs/ca_obj_fake.rb +1 -1
  175. data/yard-stubs/ca_obj_field.rb +1 -1
  176. data/yard-stubs/ca_obj_refer.rb +3 -3
  177. data/yard-stubs/ca_obj_roll.rb +0 -1
  178. data/yard-stubs/ca_obj_stride.rb +10 -7
  179. data/yard-stubs/ca_obj_transpose.rb +1 -1
  180. data/yard-stubs/ca_obj_window.rb +1 -2
  181. data/yard-stubs/carray_access.rb +5 -4
  182. data/yard-stubs/carray_broadcast.rb +1 -1
  183. data/yard-stubs/carray_cast.rb +78 -43
  184. data/yard-stubs/carray_conversion.rb +13 -1
  185. data/yard-stubs/carray_copy.rb +8 -3
  186. data/yard-stubs/carray_core.rb +3 -3
  187. data/yard-stubs/carray_count.rb +9 -0
  188. data/yard-stubs/carray_lazy.rb +5 -1
  189. data/yard-stubs/carray_loop.rb +4 -1
  190. data/yard-stubs/carray_mask.rb +34 -10
  191. data/yard-stubs/carray_math.rb +25 -13
  192. data/yard-stubs/carray_median_percentile.rb +10 -6
  193. data/yard-stubs/carray_order.rb +34 -13
  194. data/yard-stubs/carray_scatter.rb +10 -3
  195. data/yard-stubs/carray_segment.rb +72 -0
  196. data/yard-stubs/carray_slab.rb +2 -1
  197. data/yard-stubs/carray_sort.rb +43 -3
  198. data/yard-stubs/carray_test.rb +10 -0
  199. data/yard-stubs/carray_utils.rb +0 -26
  200. metadata +11 -19
  201. data/ext/ca_for_each_element.h +0 -239
  202. data/ext/carray_build_flags.h +0 -3
  203. data/ext/carray_config.h +0 -28
  204. data/ext/carray_kernels_bincmp.c +0 -4446
  205. data/ext/carray_kernels_binop.c +0 -11001
  206. data/ext/carray_kernels_init.c +0 -1131
  207. data/ext/carray_kernels_map.c +0 -3467
  208. data/ext/carray_kernels_moncmp.c +0 -2097
  209. data/ext/carray_kernels_monop.c +0 -18313
  210. data/ext/carray_kernels_reduce_aggregate.c +0 -26005
  211. data/ext/carray_kernels_reduce_boolean.c +0 -330
  212. data/ext/carray_kernels_reduce_cumulative.c +0 -14862
  213. data/ext/carray_kernels_reduce_extreme.c +0 -17494
  214. data/ext/carray_kernels_reduce_variance.c +0 -3910
  215. data/ext/carray_kernels_scan.c +0 -3693
  216. data/ext/carray_kernels_search.c +0 -32218
  217. data/ext/carray_kernels_sort.c +0 -10626
  218. data/ext/carray_kernels_triop.c +0 -1392
@@ -15,7 +15,8 @@
15
15
  CA_SLAB_REDUCE_* / _MAP_* / _SCAN_* suites, CA_L2_FOR_EACH,
16
16
  CA_*_UNMASKED helpers)
17
17
  - the raw-API entry points the macros expand to: ca_iter_state_
18
- init_l2 / next_slab_axes / sync_slab / finish
18
+ init_l2 / init_l2_paired / next_slab_axes / sync_slab / finish /
19
+ close, ca_iter_check_init, ca_iter_check_same_shape
19
20
  - the enum/status tokens authors write literally: CA_SLAB_AXES,
20
21
  CA_KERNEL_WRITE, CA_KERNEL_NO_MASK, CA_ITER_OK, CA_ITER_ERR_*
21
22
  - the slab-delivery representation = the ca_iter_state fields a
@@ -41,21 +42,6 @@
41
42
  devel/MEMO_KERNEL_ITERATOR_OOP_PATH.vev4.md for the planned engine
42
43
  overhaul this contract insulates against.)
43
44
 
44
- Step 1 scope (this commit):
45
- - struct definition (subset used by step 1; chunk_pos / scratch /
46
- descs / outer_idx fields are present but unused, reserved for
47
- later steps per proposal §3.1)
48
- - CA_SLAB_WHOLE policy only (single slab = whole array)
49
- - alias path only: ca_attach_is_alias(src) must hold, else init
50
- returns CA_ITER_ERR_NOT_CHEAP (renamed from ca_attach_is_cheap
51
- in T1 step 9.4a)
52
- - READ-only, no mask handling, no WRITE sync
53
-
54
- Later steps will fill in scratch materialize (step 2), L2 strided
55
- (step 3), WRITE + sync_slab (step 4), descriptor 6-view connection
56
- via P3 ca_axis_dispatch_for_each_slab (step 5), mask + macros
57
- (step 6), NO_MASK enforcement (step 7).
58
-
59
45
  --------------------------------------------------------------------------- */
60
46
 
61
47
  #ifndef CA_KERNEL_ITERATOR_H
@@ -66,9 +52,10 @@
66
52
 
67
53
  /* ---- slab policy (proposal §2.2) ------------------------------------- */
68
54
  typedef enum {
69
- CA_SLAB_FREE = 0, /* engine-chosen chunk (S1 max merge). step 2+. */
70
- CA_SLAB_AXES = 1, /* user-pinned axes inside the slab. step 5+. */
71
- CA_SLAB_WHOLE = 2 /* whole array in one slab. step 1 default. */
55
+ CA_SLAB_FREE = 0, /* engine-chosen chunk. Not implemented:
56
+ init returns CA_ITER_ERR_POLICY. */
57
+ CA_SLAB_AXES = 1, /* user-pinned axes inside the slab (init_l2) */
58
+ CA_SLAB_WHOLE = 2 /* whole array in one slab */
72
59
  } ca_slab_policy_t;
73
60
 
74
61
  /* ---- kernel flags (proposal §2.2) ------------------------------------ */
@@ -93,9 +80,11 @@ typedef enum {
93
80
  #define CA_ITER_ALIAS_NONE 0 /* scratch materialize. step 2+. */
94
81
  #define CA_ITER_ALIAS_CONTIG 1 /* parent.ptr+offset direct slab. */
95
82
  #define CA_ITER_ALIAS_STRIDED 2 /* L2 stride-aware. step 3+. */
96
- #define CA_ITER_ALIAS_ATTACH 3 /* SRC_ATTACH path: view's own
97
- ca_attach materialised src->ptr,
98
- sync via ca_sync(src). step 9+. */
83
+ #define CA_ITER_ALIAS_ATTACH 3 /* Not set by the engine. A
84
+ SRC_ATTACH walk runs with
85
+ CA_ITER_ALIAS_NONE over an
86
+ iterator-owned scratch filled and
87
+ written back with ca_xfer_all. */
99
88
  #define CA_ITER_ALIAS_PER_SLAB 4 /* Phase C T3 fallback: per-slab
100
89
  materialise via ca_axis_dispatch_gather
101
90
  with subset descriptor (= caller-built
@@ -120,34 +109,28 @@ typedef enum {
120
109
  mask in fiber_mask_scratch
121
110
  (= rev2 §3.3, (data,mask)
122
111
  pair). */
123
- #define CA_ITER_ALIAS_STACK_OUTER_K 8 /* PROPOSAL_CASTACK_XFER_OPT_LAYERING
124
- P.2 Case A (2026-06-18): CAStack
125
- source + CA_SLAB_AXES with axis 0
126
- NOT in slab (= K-axis in outer
127
- iter, e.g. view.mean(axis: 1) /
128
- view.mean(axis: 2)). Each slab
129
- corresponds to a region inside
130
- ONE parent selected by
131
- outer_idx[K_outer_pos]. init_l2
132
- attaches K parents (+ K parent
133
- masks if present) and caches
134
- their ptrs + uniform parent
135
- native byte strides; next_slab_
136
- axes aliases parents[k]->ptr +
137
- parent_off directly (= zero
138
- copy, zero scratch). parent
139
- entity case = eager-equivalent
140
- memory bandwidth. Mask aliases
141
- parent->mask similarly. Scoped
142
- to slab_axes that exclude axis 0
143
- (= axis 0 must be an outer iter
144
- axis). */
112
+ #define CA_ITER_ALIAS_STACK_OUTER_K 8 /* CAStack source + CA_SLAB_AXES
113
+ whose slab leaves out the stack
114
+ axis (k_axis), which is then an
115
+ outer axis. Each slab lies inside
116
+ ONE parent, selected by
117
+ outer_idx[stack_k_outer_pos].
118
+ init_l2 attaches the K parents and
119
+ caches their ptrs and parent byte
120
+ strides; next_slab_axes reads
121
+ parents[k]->ptr + parent_off
122
+ directly. The parents' masks are
123
+ copied at init and the cursor
124
+ points into the copy. A fiber
125
+ that is not contiguous in its
126
+ parent is gathered (FIBER_CONTIG).
127
+ READ only. */
145
128
  #define CA_ITER_ALIAS_STACK 7 /* PROPOSAL_CASTACK_LOOP_INTERCHANGE
146
129
  Vector A rev2 (direct per-parent
147
130
  ptr access): CAStack source +
148
131
  CA_SLAB_AXES with slab_axes ==
149
- [0] (= K-axis-only slab, e.g.
150
- view.mean(axis: 0) / sum(axis: 0)).
132
+ [k_axis] (= the stack axis alone,
133
+ e.g. view.mean(axis: k_axis)).
151
134
  init_l2 attaches K parents up
152
135
  front (= O(1) per entity parent),
153
136
  caches parent->ptr aliases +
@@ -192,19 +175,30 @@ typedef enum {
192
175
 
193
176
  /* ---- error codes ----------------------------------------------------- */
194
177
  #define CA_ITER_OK 0
195
- #define CA_ITER_ERR_NOT_CHEAP 1 /* src needs materialize, step 2+ */
178
+ #define CA_ITER_ERR_NOT_CHEAP 1 /* the classifier does not recognise
179
+ the source (a view with no attach
180
+ slot; any other external obj_type
181
+ is delivered by attaching it) */
196
182
  #define CA_ITER_ERR_POLICY 2 /* policy not implemented yet */
197
- #define CA_ITER_ERR_FLAGS 3 /* flag combination unsupported */
183
+ #define CA_ITER_ERR_FLAGS 3 /* flag combination unsupported;
184
+ also returned for a NULL state
185
+ or source */
198
186
  #define CA_ITER_ERR_READONLY 4 /* WRITE on readonly view (CARepeat etc.) */
199
- #define CA_ITER_ERR_MASK 5 /* masked source — step 4-5 only, lifted in step 6 */
200
- #define CA_ITER_ERR_MASK_NOT_ALLOWED 6 /* NO_MASK flag set on a masked source (step 7) */
187
+ #define CA_ITER_ERR_MASK 5 /* reserved: no entry point returns
188
+ it (masked sources are accepted) */
189
+ #define CA_ITER_ERR_MASK_NOT_ALLOWED 6 /* NO_MASK flag set on a masked source */
201
190
  /* ---- source kind (step 5+, internal routing) ------------------------- */
202
191
  #define CA_ITER_SRC_NONE 0
203
192
  #define CA_ITER_SRC_CASTRIDE 1 /* entity / CAStride family (step 1-4) */
204
- #define CA_ITER_SRC_DESCRIPTOR 2 /* CSA / CAGrid / CASelect / CAMapping / CAWindow / CAShift (step 5+) */
205
- #define CA_ITER_SRC_ATTACH 3 /* CAFake / CAByteSwap / CABitfield /
206
- CABitarray / CAReduce — view's own
207
- ca_attach materialises (step 9+). */
193
+ #define CA_ITER_SRC_DESCRIPTOR 2 /* CASelectAxis / CAGrid / CASelect /
194
+ CAWindow / CAShift: per-axis
195
+ descriptors (describe_axes) */
196
+ #define CA_ITER_SRC_ATTACH 3 /* every other view the classifier
197
+ knows (conversion views, CATile,
198
+ CARoll, CAStack, CAMeld, CARemap,
199
+ CAObject, the lazy views) and any
200
+ registered external obj_type: the
201
+ view's own transfer gathers it */
208
202
  #define CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE 4
209
203
  /* F-2 (PROPOSAL_F2_KERNEL_ITERATOR_ALIAS
210
204
  rev6): descriptor view whose innermost
@@ -220,13 +214,13 @@ typedef enum {
220
214
  The classifier recognises the core's own view classes by comparing
221
215
  their operation table against a list compiled into the engine. A view
222
216
  class installed by a companion gem through ca_install_obj_type matches
223
- nothing on that list, so without this hook it classifies as
224
- CA_ITER_SRC_NONE and every kernel that goes through the iterator
225
- refuses the array with CA_ITER_ERR_NOT_CHEAP. An external author
226
- declares the routing here instead, once, in the class's Init:
217
+ nothing on that list; it is delivered as CA_ITER_SRC_ATTACH (the view's
218
+ own attach gathers it), and so is one that registers here, once, in the
219
+ class's Init:
227
220
 
228
221
  ca_iter_register_source_kind(CA_OBJ_MY_VIEW, CA_ITER_SRC_ATTACH);
229
222
 
223
+ Registering states the routing rather than changing it.
230
224
  CA_ITER_SRC_ATTACH is the only kind that may be registered. It is the
231
225
  one whose contract an external class can meet on its own: func_attach
232
226
  materialises (or aliases) src->ptr and func_sync scatters back — the
@@ -257,7 +251,10 @@ typedef struct {
257
251
  ca_slab_policy_t policy;
258
252
  int8_t ndim;
259
253
  int8_t naxes; /* [step 5+] AXES policy axis count */
260
- int8_t *axes; /* [step 5+] [naxes] */
254
+ void *pair; /* the other state of a two-state
255
+ walk (ca_iter_state_init_l2_paired),
256
+ else NULL; finished with this one
257
+ when the walk raises */
261
258
  uint32_t flags;
262
259
  ca_size_t bytes; /* element size */
263
260
 
@@ -273,9 +270,9 @@ typedef struct {
273
270
  ca_size_t composed_strides[CA_RANK_MAX]; /* byte units */
274
271
  ca_size_t composed_base; /* byte offset from root->ptr */
275
272
 
276
- /* --- descriptor framework cache (step 5+, descriptor sources) ---
277
- For CSA / CAGrid / CASelect / CAMapping / CAWindow / CAShift,
278
- init_l1_descriptor runs the view's *_describe_axes once then
273
+ /* --- descriptor framework cache (descriptor sources) ---
274
+ For CASelectAxis / CAGrid / CASelect / CAWindow / CAShift, init runs
275
+ the view's *_describe_axes once then
279
276
  reuses the P3 substrate (ca_axis_dispatch_prepare / _layout /
280
277
  _classify_prefix) to derive the slab layout. Cached inline so
281
278
  next_slab walks the prefix axes without re-doing the analysis. */
@@ -315,8 +312,8 @@ typedef struct {
315
312
  uint8_t write_dirty;
316
313
 
317
314
  /* --- CA_SLAB_AXES policy fields (Phase A capstone, T1) ---
318
- Populated when init_l2 is called with policy = CA_SLAB_AXES against
319
- a SRC_CASTRIDE source. The kernel reads slab metadata directly
315
+ Populated when init_l2 is called with policy = CA_SLAB_AXES, for
316
+ every kind of source. The kernel reads slab metadata directly
320
317
  from these fields (= per-walk metadata, unchanged across slabs).
321
318
  outer_axes / outer_dims / outer_strides drive the prefix walk;
322
319
  slab_axes_buf / slab_dims / slab_strides describe the K-D slab
@@ -355,14 +352,14 @@ typedef struct {
355
352
  gather and gathers each fiber here before yielding. Reused across
356
353
  fibers; size grows to max slab_dims[0] * bytes.
357
354
 
358
- last_data_off is captured by next_slab_axes(k) BEFORE the outer_idx
359
- advance, then consumed by sync_slab(k) to compute the dst base for
360
- WRITE scatter (= rebuilding from outer_idx in sync would duplicate
361
- next_slab_axes logic; see PROPOSAL §4.3.2 hazard comment).
355
+ last_data_off is the byte offset of the current slab from alias_ptr,
356
+ set by next_slab_axes on its default path BEFORE the outer_idx
357
+ advance. sync_slab reads it for the fiber it scatters back and for
358
+ the cells of a whole-view scratch it compares; the slab iterator's
359
+ map_slab reads it to mark the output's mask.
362
360
 
363
- Phase C T3 paths (CA_ITER_ALIAS_PER_SLAB / _HOIST) yield from
364
- scratch_ptr (= already contig per-slab materialise) and do NOT
365
- touch these fields. */
361
+ The per-slab paths (CA_ITER_ALIAS_PER_SLAB / _HOIST) and the stack
362
+ paths do not set it. */
366
363
  char *fiber_data_scratch;
367
364
  ca_size_t fiber_data_scratch_cap;
368
365
  ca_size_t last_data_off;
@@ -410,12 +407,10 @@ typedef struct {
410
407
  here lets next_slab_axes compute mask_off without downcasting to
411
408
  CAStack (= AC3 layering goal). */
412
409
  ca_size_t stack_parent_mask_strides[CA_RANK_MAX];
413
- /* --- PROPOSAL_CASTACK_XFER_OPT_LAYERING P.2 Case A (2026-06-18) --- */
414
410
  /* When alias_mode == CA_ITER_ALIAS_STACK_OUTER_K and the CAStack
415
- source carries a mask, init_l2 attaches K parent masks and caches
416
- their ptrs here for parent->mask alias delivery alongside
417
- parents[k]->ptr. NULL when source has no mask. Owned by iter
418
- (xfree in finish). */
411
+ source carries a mask, init_l2 copies the K parent masks here, read
412
+ alongside parents[k]->ptr. NULL when the source has no mask. Owned
413
+ by the iterator (xfree in finish). */
419
414
  boolean8_t **stack_parent_mask_ptrs; /* [n_parents] or NULL */
420
415
  /* K axis position within the outer iter axis list. Set by init_l2
421
416
  when alias_mode == CA_ITER_ALIAS_STACK_OUTER_K; next_slab_axes uses
@@ -461,10 +456,12 @@ typedef struct {
461
456
  Not implemented in Phase 1 — falls back to L1 semantics so the predicate
462
457
  stays well-defined for callers that probe ahead.
463
458
 
464
- Descriptor framework views (CAGrid / CASelect / CAMapping / CAWindow /
465
- CAShift / CSA) and overlay views (CAFake / CAByteSwap / CABitfield /
466
- CABitarray) return 0 at every level; their alias story lands in
467
- step 5 (descriptor connection via ca_axis_dispatch_for_each_slab).
459
+ Descriptor views (CASelectAxis / CAGrid / CASelect / CAWindow /
460
+ CAShift) and conversion views (CAFake / CAByteSwap / CABitfield /
461
+ CABitarray) return 0 at every level. init_l2 still reads a
462
+ descriptor view in place when its innermost axis is a stride
463
+ (CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE); this predicate does not
464
+ report that case.
468
465
 
469
466
  ca_attach_is_alias is retained as the level=1 oracle for the Tier A
470
467
  (PROPOSAL_DELEGATE_COPY_DATA) defer site; callers that already use
@@ -481,19 +478,21 @@ int ca_iter_can_alias (void *ap, int level);
481
478
  src->ptr, stride implicit = bytes)
482
479
  - CAStride family non-contig: scratch path (ca_copy_data
483
480
  compose-fold gather into a malloc'd buffer)
484
- - other sources: CA_ITER_ERR_NOT_CHEAP
481
+ - descriptor views (CAGrid / CASelect / CAMapping / CAWindow /
482
+ CAShift / CASelectAxis): gathered into a scratch buffer
483
+ - SRC_ATTACH views (overlays, lazy views, CAObject, CAStack, ...):
484
+ iterator-owned scratch filled through ca_xfer_all
485
+ - sources the classifier does not recognise: CA_ITER_ERR_NOT_CHEAP
485
486
 
486
- policy: only CA_SLAB_WHOLE accepted in step 1-3.
487
- flags: must be 0 (READ) until step 4 / 7.
487
+ policy: CA_SLAB_WHOLE only (CA_SLAB_AXES and CA_SLAB_FREE return
488
+ CA_ITER_ERR_POLICY).
489
+ flags: any of CA_KERNEL_WRITE / CA_KERNEL_NO_MASK /
490
+ CA_KERNEL_FIBER_CONTIG; other bits return CA_ITER_ERR_FLAGS.
488
491
 
489
492
  On success returns CA_ITER_OK; on error returns CA_ITER_ERR_*
490
493
  without claiming resources (finish need not be called).
491
494
 
492
- Pair with ca_iter_state_next_slab. Step 3 split init into
493
- level-specific entry points so each setup path stays focused
494
- (proposal §3.2 rev: original single-init was relaxed when L2 setup
495
- diverged enough to warrant its own state initialiser; see
496
- ROADMAP/CHANGELOG rev). */
495
+ Pair with ca_iter_state_next_slab. */
497
496
  int ca_iter_state_init_l1 (ca_iter_state *st,
498
497
  struct _CArray *src,
499
498
  ca_slab_policy_t policy,
@@ -509,9 +508,23 @@ int ca_iter_state_init_l1 (ca_iter_state *st,
509
508
  - CAStride non-contig: alias_mode = STRIDED, multi-slab walk over
510
509
  prefix axes, each yield carries native inner stride_bytes. No
511
510
  scratch.
512
- - other sources: CA_ITER_ERR_NOT_CHEAP
513
-
514
- Other args match init_l1. Pair with ca_iter_state_next_slab_strided. */
511
+ - descriptor views: aliased when the innermost axis is a STRIDE
512
+ axis, otherwise gathered into scratch (per slab under
513
+ CA_SLAB_AXES when a slab axis is an INDEX or SHIFT axis)
514
+ - SRC_ATTACH views: iterator-owned scratch filled through
515
+ ca_xfer_all and written back the same way
516
+ - sources the classifier does not recognise: CA_ITER_ERR_NOT_CHEAP
517
+
518
+ policy: CA_SLAB_WHOLE or CA_SLAB_AXES (CA_SLAB_FREE returns
519
+ CA_ITER_ERR_POLICY, as do out-of-range or duplicate axes, and
520
+ CA_SLAB_AXES over a descriptor view whose descriptor rank
521
+ differs from the view's rank).
522
+ flags: as init_l1. CA_KERNEL_WRITE is refused with
523
+ CA_ITER_ERR_FLAGS when a CA_SLAB_AXES slab axis is an INDEX
524
+ or SHIFT axis of a descriptor view.
525
+
526
+ Pair with ca_iter_state_next_slab_strided (CA_SLAB_WHOLE) or
527
+ ca_iter_state_next_slab_axes (CA_SLAB_AXES). */
515
528
  int ca_iter_state_init_l2 (ca_iter_state *st,
516
529
  struct _CArray *src,
517
530
  ca_slab_policy_t policy,
@@ -555,12 +568,9 @@ int ca_iter_state_next_slab (ca_iter_state *st,
555
568
  *out_mask / *out_n / *out_stride_bytes when a slab is yielded;
556
569
  returns 0 when the walk is complete. The kernel walks `*out_n`
557
570
  elements by stepping `*out_stride_bytes` between consecutive
558
- elements starting at `*out_ptr`; the mask uses the **same stride
559
- semantics** when non-NULL (= each mask byte at offset i * stride is
560
- conceptually paired with the value at ptr + i * stride_bytes, but
561
- since mask is boolean8_t == 1 byte, mask stride is 1 byte when the
562
- value stride is bytes, and proportional otherwise). Only valid
563
- when init_l2 was used. */
571
+ elements starting at `*out_ptr`. The mask, when non-NULL, is
572
+ contiguous whatever the value stride: mask[i] pairs with the value
573
+ at ptr + i * stride_bytes. Only valid when init_l2 was used. */
564
574
  int ca_iter_state_next_slab_strided (ca_iter_state *st,
565
575
  char **out_ptr,
566
576
  boolean8_t **out_mask,
@@ -606,20 +616,52 @@ int ca_iter_state_next_slab_axes (ca_iter_state *st,
606
616
  next_slab_strided + kernel invocation; the alias_mode branch lives
607
617
  inside the state machine so the caller never has to inspect it.
608
618
 
609
- Behaviour (proposal PROPOSAL_T1_WRITE_SEMANTICS.md §(b)):
619
+ Behaviour:
610
620
  - !(flags & CA_KERNEL_WRITE): no-op (READ walk)
621
+ - per-fiber gather (CA_KERNEL_FIBER_CONTIG on a strided fiber):
622
+ the fiber is scattered back from its scratch first
611
623
  - alias path (scratch_ptr == NULL): no-op — kernel wrote to
612
- parent directly through alias_ptr (case A semantics)
613
- - scratch path (L1 only by step-4 invariant): scatter back via
614
- ca_sync_data(src, scratch). L2 scratch is structurally
615
- unreachable in step 4 (CAStride only) and is guarded by an
616
- assert; step 5 re-evaluation noted in the proposal. */
624
+ parent directly through alias_ptr
625
+ - scratch path, at L1 or L2: written back through the source —
626
+ ca_xfer_all for SRC_ATTACH, ca_sync_data for CAStride, the
627
+ descriptor scatter for descriptor views */
617
628
  void ca_iter_state_sync_slab (ca_iter_state *st);
618
629
 
619
- /* Release any resources held by `st` and detach parent. Safe to call
620
- exactly once after a successful init (either level). */
630
+ /* Release any resources held by `st` and detach parent. Idempotent:
631
+ a second call is a no-op. Also safe after a failed init, which holds
632
+ nothing to release (the call is then not required). */
621
633
  void ca_iter_state_finish (ca_iter_state *st);
622
634
 
635
+ /* Open `st` as the second state of a walk whose first state is already
636
+ open -- an input and an output walked together. Takes what
637
+ ca_iter_state_init_l2 takes, plus `first`.
638
+
639
+ The two are then finished together when either walk raises (a gather
640
+ or a write-back through a source's slots): each is open in the
641
+ caller's frame, and the raise would jump over the other's finish. If
642
+ opening `st` raises, or is declined (the code is returned), `first` is
643
+ finished here, so the caller has nothing left to close.
644
+
645
+ When `st` writes (CA_KERNEL_WRITE), `first` reads, both walk by
646
+ CA_SLAB_AXES, and their arrays live in the same root, `first` is
647
+ reopened on a copy of its array taken before anything is written, so
648
+ the kernel reads the values the input held when the walk began. Two
649
+ walks opened separately with ca_iter_state_init_l2 know nothing of
650
+ each other: a read walk over memory a write walk writes into must be
651
+ given a copy by the caller. */
652
+ int ca_iter_state_init_l2_paired (ca_iter_state *st,
653
+ ca_iter_state *first,
654
+ struct _CArray *src,
655
+ ca_slab_policy_t policy,
656
+ int8_t *axes,
657
+ int8_t naxes,
658
+ uint32_t flags);
659
+
660
+ /* Finish a walk as the block macros leave it: when the body left a write
661
+ walk with `break`, the slab it was in is written back before the walk is
662
+ finished. ca_iter_state_finish itself abandons such a slab. */
663
+ void ca_iter_state_close (ca_iter_state *st);
664
+
623
665
  /* Raise unless rc is CA_ITER_OK. The block macros call this on the value
624
666
  ca_iter_state_init_l1 / _l2 returned, so an author who never looks at a
625
667
  return code still hears about a request the iterator would not serve.
@@ -627,6 +669,32 @@ void ca_iter_state_finish (ca_iter_state *st);
627
669
  macros' comma expressions. */
628
670
  int ca_iter_check_init (int rc);
629
671
 
672
+ /* Raise ArgumentError unless the two arrays have the same shape. The
673
+ INOUT and PAIR fiber macros call this before opening either walk. */
674
+ int ca_iter_check_same_shape (struct _CArray *a, struct _CArray *b);
675
+
676
+ /* Run body(arg) so that the walks in states[0..n) are finished however the
677
+ body leaves: by returning, by `break`, or by raising.
678
+
679
+ A walk releases what it holds in ca_iter_state_finish, and a body that
680
+ raises jumps over the finish -- the block macros put it in the `for`
681
+ increment, and a hand-written walk has it after the loop. A body that
682
+ calls back into Ruby (an object-lane kernel) can raise at any cell, so
683
+ it runs through this function.
684
+
685
+ The states belong to the caller's frame, not to the body's: after a
686
+ raise, the body's frame is gone, and the cleanup runs where it was.
687
+ Pass them to the body inside `arg` and open each one there, with
688
+ ca_iter_state_init_* or a block macro (`CA_FOR_EACH_SLAB(c->st, ...)`).
689
+ A state declared inside the body is not covered.
690
+
691
+ The states are marked unopened before the body runs, so one the body
692
+ never opens costs nothing, and the body may finish a walk itself:
693
+ finishing is idempotent. Returns what the body returns; a raise propagates
694
+ after the walks are finished. */
695
+ VALUE ca_iter_ensure (int32_t n, ca_iter_state *states,
696
+ VALUE (*body)(VALUE), VALUE arg);
697
+
630
698
  /* ---- Phase C C.3: kernel author block macros ------------------------
631
699
  (PROPOSAL_CAPSTONE_PHASE_C.md D3.1 (A) do/while/for + D3.2 (C) 2 kinds)
632
700
 
@@ -652,13 +720,16 @@ int ca_iter_check_init (int rc);
652
720
  should drop down to the raw API and read the code themselves.
653
721
  - `break;` from inside the body exits the loop AND triggers finish
654
722
  correctly (= outer for's "increment" clause runs once on natural
655
- exit; `break` from the inner while breaks both). `return` inside
723
+ exit; `break` from the inner while breaks both). In a write walk
724
+ finish writes back the slab the body was in. `return` inside
656
725
  the body LEAKS resources (scratch_ptr, parent attach) — drop to
657
726
  raw API if early return is needed. A body that raises leaks the
658
727
  same way: the engine releases what it holds when the walk itself
659
728
  raises (a gather or a write-back through the source's slots), but
660
729
  the body runs in the caller's frame, where it has no hold on it.
661
- An object-lane body calling back into Ruby is the case to watch.
730
+ A body that can raise -- one that calls back into Ruby, as an
731
+ object-lane kernel does -- runs through ca_iter_ensure, with the
732
+ state in the caller's frame.
662
733
  - Macros are not statement-equivalent (= they expand to nested for
663
734
  constructs). Don't follow them with `else` etc. */
664
735
 
@@ -796,7 +867,7 @@ int ca_iter_check_init (int rc);
796
867
  (axes), (naxes), (flags))), \
797
868
  1); \
798
869
  __caf_init; \
799
- __caf_init = 0, ca_iter_state_finish(&(st)) ) \
870
+ __caf_init = 0, ca_iter_state_close(&(st)) ) \
800
871
  for ( ; ca_iter_state_next_slab_axes(&(st), &(p), &(m)); \
801
872
  ca_iter_state_sync_slab(&(st)) )
802
873
 
@@ -819,13 +890,14 @@ int ca_iter_check_init (int rc);
819
890
  ca_iter_state_init_l2(&(st_in), (ca_in), CA_SLAB_AXES, \
820
891
  (axes), (naxes), 0)), \
821
892
  ca_iter_check_init( \
822
- ca_iter_state_init_l2(&(st_out), (ca_out), CA_SLAB_AXES, \
893
+ ca_iter_state_init_l2_paired(&(st_out), &(st_in), (ca_out), \
894
+ CA_SLAB_AXES, \
823
895
  (axes), (naxes), CA_KERNEL_WRITE)), \
824
896
  1); \
825
897
  __cafi_init; \
826
898
  __cafi_init = 0, \
827
- ca_iter_state_finish(&(st_in)), \
828
- ca_iter_state_finish(&(st_out)) ) \
899
+ ca_iter_state_close(&(st_in)), \
900
+ ca_iter_state_close(&(st_out)) ) \
829
901
  for ( ; ca_iter_state_next_slab_axes(&(st_in), &(p_in), &(m_in)) && \
830
902
  ca_iter_state_next_slab_axes(&(st_out), &(p_out), &(m_out)); \
831
903
  ca_iter_state_sync_slab(&(st_out)) )
@@ -843,21 +915,25 @@ int ca_iter_check_init (int rc);
843
915
  The CA_KERNEL_FIBER_CONTIG flag is auto-set; engine gathers strided
844
916
  fibers into per-state scratch when slab_strides[0] != bytes (= F.1a/b).
845
917
 
846
- `axis` is evaluated ONCE into a stack-local int8 buffer of static
847
- storage scope; `ca`/`ca_in`/`ca_out` are evaluated ONCE in init.
848
- `n` is set to the fiber length (= slab_dims[0], constant per walk).
918
+ `axis`, `ca`, `ca_in` and `ca_out` may be evaluated more than once:
919
+ pass variables, not expressions with side effects. `n` is set to the
920
+ fiber length (= slab_dims[0], constant per walk).
921
+
922
+ The forms without MASKED in the name hand out no mask cursor: a masked
923
+ source is walked with its masked cells read as values. Pass
924
+ CA_KERNEL_NO_MASK to have a masked source refused, or use a MASKED
925
+ form to see the mask.
849
926
 
850
927
  Same constraints as CA_FOR_EACH_SLAB family:
851
- - `break;` from body exits cleanly (finish runs).
928
+ - `break;` from body exits cleanly (finish runs, and a write walk
929
+ writes back the fiber it was in).
852
930
  - `return;` from body LEAKS scratch / parent attach -- use raw API.
853
931
  - Macros are NOT statement-equivalent (nested for); no trailing else.
854
932
 
855
- INOUT forms (form 2 / 4) require STRICT FULL SHAPE EQUALITY of
856
- `ca_in` and `ca_out` (= ndim + every dim[k] match). Mismatch is a
857
- silent-corruption seam (= short-circuit fiber-count drop, k-th
858
- pairing corruption); init_l2 does not validate it itself, so the
859
- macros runtime-assert shape equality and skip body on mismatch.
860
- Authors that need broadcasting must drop to raw API. */
933
+ INOUT forms (form 2 / 4) require `ca_in` and `ca_out` to have the same
934
+ shape (ndim and every dim[k]), and raise ArgumentError before opening
935
+ either walk when they do not. Authors that need broadcasting must
936
+ drop to raw API. */
861
937
 
862
938
  #define CA_FOR_EACH_FIBER(st, ca, axis, flags, p, n) \
863
939
  for ( int __cff_init = ( \
@@ -868,7 +944,7 @@ int ca_iter_check_init (int rc);
868
944
  (n) = (st).slab_dims[0], \
869
945
  1); \
870
946
  __cff_init; \
871
- __cff_init = 0, ca_iter_state_finish(&(st)) ) \
947
+ __cff_init = 0, ca_iter_state_close(&(st)) ) \
872
948
  for ( ; ca_iter_state_next_slab_axes(&(st), &(p), NULL); \
873
949
  ca_iter_state_sync_slab(&(st)) )
874
950
 
@@ -881,31 +957,23 @@ int ca_iter_check_init (int rc);
881
957
  (n) = (st).slab_dims[0], \
882
958
  1); \
883
959
  __cffm_init; \
884
- __cffm_init = 0, ca_iter_state_finish(&(st)) ) \
960
+ __cffm_init = 0, ca_iter_state_close(&(st)) ) \
885
961
  for ( ; ca_iter_state_next_slab_axes(&(st), &(p), &(m)); \
886
962
  ca_iter_state_sync_slab(&(st)) )
887
963
 
888
- /* INOUT form 2 (NO_MASK). Output gets CA_KERNEL_WRITE auto-set.
889
-
890
- STRICT FULL SHAPE EQUALITY (rev4 §2.3): the inner for-condition
891
- re-evaluates ca_in->ndim == ca_out->ndim and dim[axis] equality (=
892
- minimal seam coverage given the macro can't loop over k). Full
893
- per-axis equality lives one level up in the init-time short-circuit
894
- below: we compare elements + ndim + axis dim, which catches the
895
- common silent-corruption seam (= e.g. (3,5) vs (4,5) axis=1 with
896
- matching fiber length but different fiber count). Comprehensive
897
- per-dim check is the caller's responsibility for now (= simpler than
898
- building a per-dim k loop into a macro; ext authors can drop to raw
899
- API for arbitrary broadcasting designs). */
964
+ /* INOUT form 2. Output gets CA_KERNEL_WRITE auto-set. The shapes are
965
+ checked first (ca_iter_check_same_shape). */
900
966
  #define CA_FOR_EACH_FIBER_INOUT(st_in, st_out, ca_in, ca_out, axis, \
901
967
  flags, p_in, p_out, n) \
902
968
  for ( int __cffi_init = ( \
969
+ ca_iter_check_same_shape((ca_in), (ca_out)), \
903
970
  ca_iter_check_init( \
904
971
  ca_iter_state_init_l2(&(st_in), (ca_in), CA_SLAB_AXES, \
905
972
  (int8_t[]){(int8_t)(axis)}, 1, \
906
973
  (flags) | CA_KERNEL_FIBER_CONTIG)), \
907
974
  ca_iter_check_init( \
908
- ca_iter_state_init_l2(&(st_out), (ca_out), CA_SLAB_AXES, \
975
+ ca_iter_state_init_l2_paired(&(st_out), &(st_in), (ca_out), \
976
+ CA_SLAB_AXES, \
909
977
  (int8_t[]){(int8_t)(axis)}, 1, \
910
978
  ((flags) | CA_KERNEL_FIBER_CONTIG \
911
979
  | CA_KERNEL_WRITE))), \
@@ -913,12 +981,9 @@ int ca_iter_check_init (int rc);
913
981
  1); \
914
982
  __cffi_init; \
915
983
  __cffi_init = 0, \
916
- ca_iter_state_finish(&(st_in)), \
917
- ca_iter_state_finish(&(st_out)) ) \
918
- for ( ; (st_in).src->ndim == (st_out).src->ndim \
919
- && (st_in).src->elements == (st_out).src->elements \
920
- && (st_in).slab_dims[0] == (st_out).slab_dims[0] \
921
- && ca_iter_state_next_slab_axes(&(st_in), &(p_in), NULL) \
984
+ ca_iter_state_close(&(st_in)), \
985
+ ca_iter_state_close(&(st_out)) ) \
986
+ for ( ; ca_iter_state_next_slab_axes(&(st_in), &(p_in), NULL) \
922
987
  && ca_iter_state_next_slab_axes(&(st_out), &(p_out), NULL); \
923
988
  ca_iter_state_sync_slab(&(st_in)), \
924
989
  ca_iter_state_sync_slab(&(st_out)) )
@@ -926,12 +991,14 @@ int ca_iter_check_init (int rc);
926
991
  #define CA_FOR_EACH_FIBER_INOUT_MASKED(st_in, st_out, ca_in, ca_out, axis, \
927
992
  flags, p_in, p_out, n, m) \
928
993
  for ( int __cffim_init = ( \
994
+ ca_iter_check_same_shape((ca_in), (ca_out)), \
929
995
  ca_iter_check_init( \
930
996
  ca_iter_state_init_l2(&(st_in), (ca_in), CA_SLAB_AXES, \
931
997
  (int8_t[]){(int8_t)(axis)}, 1, \
932
998
  (flags) | CA_KERNEL_FIBER_CONTIG)), \
933
999
  ca_iter_check_init( \
934
- ca_iter_state_init_l2(&(st_out), (ca_out), CA_SLAB_AXES, \
1000
+ ca_iter_state_init_l2_paired(&(st_out), &(st_in), (ca_out), \
1001
+ CA_SLAB_AXES, \
935
1002
  (int8_t[]){(int8_t)(axis)}, 1, \
936
1003
  ((flags) | CA_KERNEL_FIBER_CONTIG \
937
1004
  | CA_KERNEL_WRITE))), \
@@ -939,12 +1006,9 @@ int ca_iter_check_init (int rc);
939
1006
  1); \
940
1007
  __cffim_init; \
941
1008
  __cffim_init = 0, \
942
- ca_iter_state_finish(&(st_in)), \
943
- ca_iter_state_finish(&(st_out)) ) \
944
- for ( ; (st_in).src->ndim == (st_out).src->ndim \
945
- && (st_in).src->elements == (st_out).src->elements \
946
- && (st_in).slab_dims[0] == (st_out).slab_dims[0] \
947
- && ca_iter_state_next_slab_axes(&(st_in), &(p_in), &(m)) \
1009
+ ca_iter_state_close(&(st_in)), \
1010
+ ca_iter_state_close(&(st_out)) ) \
1011
+ for ( ; ca_iter_state_next_slab_axes(&(st_in), &(p_in), &(m)) \
948
1012
  && ca_iter_state_next_slab_axes(&(st_out), &(p_out), NULL); \
949
1013
  ca_iter_state_sync_slab(&(st_in)), \
950
1014
  ca_iter_state_sync_slab(&(st_out)) )
@@ -962,31 +1026,32 @@ int ca_iter_check_init (int rc);
962
1026
  cursors: which cells a pair of fibers may be used at is a question about
963
1027
  both of them, and the INOUT forms answer only about the input.
964
1028
 
965
- Shape agreement is guarded the way the INOUT forms guard it (ndim,
966
- elements, fiber length), and the body is skipped on mismatch. The two
967
- sources may be the same array. */
1029
+ The two sources must have the same shape, as for the INOUT forms:
1030
+ ca_iter_check_same_shape raises ArgumentError before either walk is
1031
+ opened. The two sources may be the same array. The second state is
1032
+ opened paired with the first, so a refusal or a raise on either side
1033
+ closes both. */
968
1034
 
969
1035
  #define CA_FOR_EACH_FIBER_PAIR(st_a, st_b, ca_a, ca_b, axis, \
970
1036
  flags, p_a, p_b, n) \
971
1037
  for ( int __cffp_init = ( \
1038
+ ca_iter_check_same_shape((ca_a), (ca_b)), \
972
1039
  ca_iter_check_init( \
973
1040
  ca_iter_state_init_l2(&(st_a), (ca_a), CA_SLAB_AXES, \
974
1041
  (int8_t[]){(int8_t)(axis)}, 1, \
975
1042
  (flags) | CA_KERNEL_FIBER_CONTIG)), \
976
1043
  ca_iter_check_init( \
977
- ca_iter_state_init_l2(&(st_b), (ca_b), CA_SLAB_AXES, \
1044
+ ca_iter_state_init_l2_paired(&(st_b), &(st_a), (ca_b), \
1045
+ CA_SLAB_AXES, \
978
1046
  (int8_t[]){(int8_t)(axis)}, 1, \
979
1047
  (flags) | CA_KERNEL_FIBER_CONTIG)), \
980
1048
  (n) = (st_a).slab_dims[0], \
981
1049
  1); \
982
1050
  __cffp_init; \
983
1051
  __cffp_init = 0, \
984
- ca_iter_state_finish(&(st_a)), \
985
- ca_iter_state_finish(&(st_b)) ) \
986
- for ( ; (st_a).src->ndim == (st_b).src->ndim \
987
- && (st_a).src->elements == (st_b).src->elements \
988
- && (st_a).slab_dims[0] == (st_b).slab_dims[0] \
989
- && ca_iter_state_next_slab_axes(&(st_a), &(p_a), NULL) \
1052
+ ca_iter_state_close(&(st_a)), \
1053
+ ca_iter_state_close(&(st_b)) ) \
1054
+ for ( ; ca_iter_state_next_slab_axes(&(st_a), &(p_a), NULL) \
990
1055
  && ca_iter_state_next_slab_axes(&(st_b), &(p_b), NULL); \
991
1056
  ca_iter_state_sync_slab(&(st_a)), \
992
1057
  ca_iter_state_sync_slab(&(st_b)) )
@@ -994,24 +1059,23 @@ int ca_iter_check_init (int rc);
994
1059
  #define CA_FOR_EACH_FIBER_PAIR_MASKED(st_a, st_b, ca_a, ca_b, axis, \
995
1060
  flags, p_a, p_b, n, m_a, m_b) \
996
1061
  for ( int __cffpm_init = ( \
1062
+ ca_iter_check_same_shape((ca_a), (ca_b)), \
997
1063
  ca_iter_check_init( \
998
1064
  ca_iter_state_init_l2(&(st_a), (ca_a), CA_SLAB_AXES, \
999
1065
  (int8_t[]){(int8_t)(axis)}, 1, \
1000
1066
  (flags) | CA_KERNEL_FIBER_CONTIG)), \
1001
1067
  ca_iter_check_init( \
1002
- ca_iter_state_init_l2(&(st_b), (ca_b), CA_SLAB_AXES, \
1068
+ ca_iter_state_init_l2_paired(&(st_b), &(st_a), (ca_b), \
1069
+ CA_SLAB_AXES, \
1003
1070
  (int8_t[]){(int8_t)(axis)}, 1, \
1004
1071
  (flags) | CA_KERNEL_FIBER_CONTIG)), \
1005
1072
  (n) = (st_a).slab_dims[0], \
1006
1073
  1); \
1007
1074
  __cffpm_init; \
1008
1075
  __cffpm_init = 0, \
1009
- ca_iter_state_finish(&(st_a)), \
1010
- ca_iter_state_finish(&(st_b)) ) \
1011
- for ( ; (st_a).src->ndim == (st_b).src->ndim \
1012
- && (st_a).src->elements == (st_b).src->elements \
1013
- && (st_a).slab_dims[0] == (st_b).slab_dims[0] \
1014
- && ca_iter_state_next_slab_axes(&(st_a), &(p_a), &(m_a)) \
1076
+ ca_iter_state_close(&(st_a)), \
1077
+ ca_iter_state_close(&(st_b)) ) \
1078
+ for ( ; ca_iter_state_next_slab_axes(&(st_a), &(p_a), &(m_a)) \
1015
1079
  && ca_iter_state_next_slab_axes(&(st_b), &(p_b), &(m_b)); \
1016
1080
  ca_iter_state_sync_slab(&(st_a)), \
1017
1081
  ca_iter_state_sync_slab(&(st_b)) )
@@ -2351,7 +2415,7 @@ int ca_iter_check_init (int rc);
2351
2415
  Both states must share slab geometry (= same slab_ndim and slab_dims),
2352
2416
  typically by initialising both with the same policy + axes on
2353
2417
  shape-equal CArrays. Mask handling is **not** done by this macro —
2354
- if your input is masked, see §6.2 mask propagation discussion in
2418
+ if your input is masked, see §8 Mask handling in
2355
2419
  docs/authoring/HOW_TO_WRITE_KERNEL.md. Caller is responsible for invoking
2356
2420
  ca_iter_state_sync_slab on the output state after each slab.
2357
2421