carray 3.0.1 → 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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  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 +90 -41
  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 +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  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 +194 -42
  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 +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -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 +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  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 +551 -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 +88 -75
  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 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  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 +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. 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,
@@ -528,7 +541,24 @@ int ca_iter_state_init_l2 (ca_iter_state *st,
528
541
  source carries a mask (= ca_has_mask(src)), or NULL otherwise.
529
542
  The mask layout matches the value layout (= same iteration order
530
543
  and same n). Step 6+: kernels use the CA_FOR_EACH_UNMASKED macro
531
- family (carray.h) to skip masked cells. */
544
+ family (carray.h) to skip masked cells.
545
+
546
+ The mask is an input. What it points at is a copy taken when the
547
+ walk began, so writing through it changes nothing -- not even under
548
+ CA_KERNEL_WRITE, where the value half of the same yield is often a
549
+ live alias. It also cannot express the thing an author would most
550
+ want it for: an unmasked source yields NULL, so there is nowhere to
551
+ record that a cell has become undefined. A kernel authors its
552
+ output mask on its own output array instead:
553
+
554
+ if ( op_mask == NULL ) {
555
+ ca_create_mask(co);
556
+ op_mask = (boolean8_t *) co->mask->ptr;
557
+ }
558
+ op_mask[out_i] = 1;
559
+
560
+ which is what every kernel in carray does (carray_hold.c, and the
561
+ reduction kernels mkkernel emits). */
532
562
  int ca_iter_state_next_slab (ca_iter_state *st,
533
563
  char **out_ptr,
534
564
  boolean8_t **out_mask,
@@ -538,12 +568,9 @@ int ca_iter_state_next_slab (ca_iter_state *st,
538
568
  *out_mask / *out_n / *out_stride_bytes when a slab is yielded;
539
569
  returns 0 when the walk is complete. The kernel walks `*out_n`
540
570
  elements by stepping `*out_stride_bytes` between consecutive
541
- elements starting at `*out_ptr`; the mask uses the **same stride
542
- semantics** when non-NULL (= each mask byte at offset i * stride is
543
- conceptually paired with the value at ptr + i * stride_bytes, but
544
- since mask is boolean8_t == 1 byte, mask stride is 1 byte when the
545
- value stride is bytes, and proportional otherwise). Only valid
546
- 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. */
547
574
  int ca_iter_state_next_slab_strided (ca_iter_state *st,
548
575
  char **out_ptr,
549
576
  boolean8_t **out_mask,
@@ -589,20 +616,85 @@ int ca_iter_state_next_slab_axes (ca_iter_state *st,
589
616
  next_slab_strided + kernel invocation; the alias_mode branch lives
590
617
  inside the state machine so the caller never has to inspect it.
591
618
 
592
- Behaviour (proposal PROPOSAL_T1_WRITE_SEMANTICS.md §(b)):
619
+ Behaviour:
593
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
594
623
  - alias path (scratch_ptr == NULL): no-op — kernel wrote to
595
- parent directly through alias_ptr (case A semantics)
596
- - scratch path (L1 only by step-4 invariant): scatter back via
597
- ca_sync_data(src, scratch). L2 scratch is structurally
598
- unreachable in step 4 (CAStride only) and is guarded by an
599
- 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 */
600
628
  void ca_iter_state_sync_slab (ca_iter_state *st);
601
629
 
602
- /* Release any resources held by `st` and detach parent. Safe to call
603
- 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). */
604
633
  void ca_iter_state_finish (ca_iter_state *st);
605
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
+
665
+ /* Raise unless rc is CA_ITER_OK. The block macros call this on the value
666
+ ca_iter_state_init_l1 / _l2 returned, so an author who never looks at a
667
+ return code still hears about a request the iterator would not serve.
668
+ Returns rc when there is nothing to report, so it composes inside the
669
+ macros' comma expressions. */
670
+ int ca_iter_check_init (int rc);
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
+
606
698
  /* ---- Phase C C.3: kernel author block macros ------------------------
607
699
  (PROPOSAL_CAPSTONE_PHASE_C.md D3.1 (A) do/while/for + D3.2 (C) 2 kinds)
608
700
 
@@ -617,17 +709,27 @@ void ca_iter_state_finish (ca_iter_state *st);
617
709
  surrounding scope.
618
710
  - `flags` arg propagates to init_l2 (= CA_KERNEL_WRITE supported).
619
711
  `sync_slab` is called automatically after each iteration; it's a
620
- no-op when WRITE flag is absent.
621
- - Init failure (ca_iter_state_init_l2 returns CA_ITER_ERR_*) is
622
- silently discarded: the body runs zero times and finish is still
623
- called. Production kernels that need explicit error messages
624
- (e.g., sum_ki's rc=%d raise) should drop down to the raw API
625
- instead of using this macro.
712
+ no-op when WRITE flag is absent. The mask cursor is an input even
713
+ under WRITE -- see ca_iter_state_next_slab above for why, and for
714
+ where an output mask is written instead.
715
+ - Init failure (ca_iter_state_init_l2 returns CA_ITER_ERR_*) raises:
716
+ the macro passes the code to ca_iter_check_init, which reports what
717
+ the iterator declined to do. The body does not run and finish is
718
+ not reached, which is safe because a failed init allocates nothing.
719
+ Kernels that want to handle a refusal rather than propagate it
720
+ should drop down to the raw API and read the code themselves.
626
721
  - `break;` from inside the body exits the loop AND triggers finish
627
722
  correctly (= outer for's "increment" clause runs once on natural
628
- 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
629
725
  the body LEAKS resources (scratch_ptr, parent attach) — drop to
630
- raw API if early return is needed.
726
+ raw API if early return is needed. A body that raises leaks the
727
+ same way: the engine releases what it holds when the walk itself
728
+ raises (a gather or a write-back through the source's slots), but
729
+ the body runs in the caller's frame, where it has no hold on it.
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.
631
733
  - Macros are not statement-equivalent (= they expand to nested for
632
734
  constructs). Don't follow them with `else` etc. */
633
735
 
@@ -760,11 +862,12 @@ void ca_iter_state_finish (ca_iter_state *st);
760
862
  always-constant argument). CA_SLAB_AXES is still FROZEN, because
761
863
  raw-API kernels pass it to ca_iter_state_init_l2 directly. */
762
864
  #define CA_FOR_EACH_SLAB(st, ca, axes, naxes, flags, p, m) \
763
- for ( int __caf_init = (ca_iter_state_init_l2(&(st), (ca), CA_SLAB_AXES, \
764
- (axes), (naxes), (flags)), \
865
+ for ( int __caf_init = (ca_iter_check_init( \
866
+ ca_iter_state_init_l2(&(st), (ca), CA_SLAB_AXES, \
867
+ (axes), (naxes), (flags))), \
765
868
  1); \
766
869
  __caf_init; \
767
- __caf_init = 0, ca_iter_state_finish(&(st)) ) \
870
+ __caf_init = 0, ca_iter_state_close(&(st)) ) \
768
871
  for ( ; ca_iter_state_next_slab_axes(&(st), &(p), &(m)); \
769
872
  ca_iter_state_sync_slab(&(st)) )
770
873
 
@@ -777,21 +880,24 @@ void ca_iter_state_finish (ca_iter_state *st);
777
880
  Shape mismatch between ca_in / ca_out is NOT validated by the macro
778
881
  — caller responsibility (= typically output is `rb_ca_template_with_type`
779
882
  of input, guaranteeing same shape). Init failure on either iter
780
- silently skips the body. */
883
+ raises — see CA_FOR_EACH_SLAB above. */
781
884
  /* Policy fixed to CA_SLAB_AXES internally — see CA_FOR_EACH_SLAB above. */
782
885
  #define CA_FOR_EACH_SLAB_INOUT(st_in, st_out, ca_in, ca_out, \
783
886
  axes, naxes, \
784
887
  p_in, p_out, m_in, m_out) \
785
888
  for ( int __cafi_init = ( \
786
- ca_iter_state_init_l2(&(st_in), (ca_in), CA_SLAB_AXES, \
787
- (axes), (naxes), 0), \
788
- ca_iter_state_init_l2(&(st_out), (ca_out), CA_SLAB_AXES, \
789
- (axes), (naxes), CA_KERNEL_WRITE), \
889
+ ca_iter_check_init( \
890
+ ca_iter_state_init_l2(&(st_in), (ca_in), CA_SLAB_AXES, \
891
+ (axes), (naxes), 0)), \
892
+ ca_iter_check_init( \
893
+ ca_iter_state_init_l2_paired(&(st_out), &(st_in), (ca_out), \
894
+ CA_SLAB_AXES, \
895
+ (axes), (naxes), CA_KERNEL_WRITE)), \
790
896
  1); \
791
897
  __cafi_init; \
792
898
  __cafi_init = 0, \
793
- ca_iter_state_finish(&(st_in)), \
794
- ca_iter_state_finish(&(st_out)) ) \
899
+ ca_iter_state_close(&(st_in)), \
900
+ ca_iter_state_close(&(st_out)) ) \
795
901
  for ( ; ca_iter_state_next_slab_axes(&(st_in), &(p_in), &(m_in)) && \
796
902
  ca_iter_state_next_slab_axes(&(st_out), &(p_out), &(m_out)); \
797
903
  ca_iter_state_sync_slab(&(st_out)) )
@@ -809,78 +915,75 @@ void ca_iter_state_finish (ca_iter_state *st);
809
915
  The CA_KERNEL_FIBER_CONTIG flag is auto-set; engine gathers strided
810
916
  fibers into per-state scratch when slab_strides[0] != bytes (= F.1a/b).
811
917
 
812
- `axis` is evaluated ONCE into a stack-local int8 buffer of static
813
- storage scope; `ca`/`ca_in`/`ca_out` are evaluated ONCE in init.
814
- `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.
815
926
 
816
927
  Same constraints as CA_FOR_EACH_SLAB family:
817
- - `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).
818
930
  - `return;` from body LEAKS scratch / parent attach -- use raw API.
819
931
  - Macros are NOT statement-equivalent (nested for); no trailing else.
820
932
 
821
- INOUT forms (form 2 / 4) require STRICT FULL SHAPE EQUALITY of
822
- `ca_in` and `ca_out` (= ndim + every dim[k] match). Mismatch is a
823
- silent-corruption seam (= short-circuit fiber-count drop, k-th
824
- pairing corruption); init_l2 does not validate it itself, so the
825
- macros runtime-assert shape equality and skip body on mismatch.
826
- 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. */
827
937
 
828
938
  #define CA_FOR_EACH_FIBER(st, ca, axis, flags, p, n) \
829
939
  for ( int __cff_init = ( \
830
- ca_iter_state_init_l2(&(st), (ca), CA_SLAB_AXES, \
831
- (int8_t[]){(int8_t)(axis)}, 1, \
832
- (flags) | CA_KERNEL_FIBER_CONTIG), \
940
+ ca_iter_check_init( \
941
+ ca_iter_state_init_l2(&(st), (ca), CA_SLAB_AXES, \
942
+ (int8_t[]){(int8_t)(axis)}, 1, \
943
+ (flags) | CA_KERNEL_FIBER_CONTIG)), \
833
944
  (n) = (st).slab_dims[0], \
834
945
  1); \
835
946
  __cff_init; \
836
- __cff_init = 0, ca_iter_state_finish(&(st)) ) \
947
+ __cff_init = 0, ca_iter_state_close(&(st)) ) \
837
948
  for ( ; ca_iter_state_next_slab_axes(&(st), &(p), NULL); \
838
949
  ca_iter_state_sync_slab(&(st)) )
839
950
 
840
951
  #define CA_FOR_EACH_FIBER_MASKED(st, ca, axis, flags, p, n, m) \
841
952
  for ( int __cffm_init = ( \
842
- ca_iter_state_init_l2(&(st), (ca), CA_SLAB_AXES, \
843
- (int8_t[]){(int8_t)(axis)}, 1, \
844
- (flags) | CA_KERNEL_FIBER_CONTIG), \
953
+ ca_iter_check_init( \
954
+ ca_iter_state_init_l2(&(st), (ca), CA_SLAB_AXES, \
955
+ (int8_t[]){(int8_t)(axis)}, 1, \
956
+ (flags) | CA_KERNEL_FIBER_CONTIG)), \
845
957
  (n) = (st).slab_dims[0], \
846
958
  1); \
847
959
  __cffm_init; \
848
- __cffm_init = 0, ca_iter_state_finish(&(st)) ) \
960
+ __cffm_init = 0, ca_iter_state_close(&(st)) ) \
849
961
  for ( ; ca_iter_state_next_slab_axes(&(st), &(p), &(m)); \
850
962
  ca_iter_state_sync_slab(&(st)) )
851
963
 
852
- /* INOUT form 2 (NO_MASK). Output gets CA_KERNEL_WRITE auto-set.
853
-
854
- STRICT FULL SHAPE EQUALITY (rev4 §2.3): the inner for-condition
855
- re-evaluates ca_in->ndim == ca_out->ndim and dim[axis] equality (=
856
- minimal seam coverage given the macro can't loop over k). Full
857
- per-axis equality lives one level up in the init-time short-circuit
858
- below: we compare elements + ndim + axis dim, which catches the
859
- common silent-corruption seam (= e.g. (3,5) vs (4,5) axis=1 with
860
- matching fiber length but different fiber count). Comprehensive
861
- per-dim check is the caller's responsibility for now (= simpler than
862
- building a per-dim k loop into a macro; ext authors can drop to raw
863
- 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). */
864
966
  #define CA_FOR_EACH_FIBER_INOUT(st_in, st_out, ca_in, ca_out, axis, \
865
967
  flags, p_in, p_out, n) \
866
968
  for ( int __cffi_init = ( \
867
- ca_iter_state_init_l2(&(st_in), (ca_in), CA_SLAB_AXES, \
868
- (int8_t[]){(int8_t)(axis)}, 1, \
869
- (flags) | CA_KERNEL_FIBER_CONTIG), \
870
- ca_iter_state_init_l2(&(st_out), (ca_out), CA_SLAB_AXES, \
871
- (int8_t[]){(int8_t)(axis)}, 1, \
872
- ((flags) | CA_KERNEL_FIBER_CONTIG \
873
- | CA_KERNEL_WRITE)), \
969
+ ca_iter_check_same_shape((ca_in), (ca_out)), \
970
+ ca_iter_check_init( \
971
+ ca_iter_state_init_l2(&(st_in), (ca_in), CA_SLAB_AXES, \
972
+ (int8_t[]){(int8_t)(axis)}, 1, \
973
+ (flags) | CA_KERNEL_FIBER_CONTIG)), \
974
+ ca_iter_check_init( \
975
+ ca_iter_state_init_l2_paired(&(st_out), &(st_in), (ca_out), \
976
+ CA_SLAB_AXES, \
977
+ (int8_t[]){(int8_t)(axis)}, 1, \
978
+ ((flags) | CA_KERNEL_FIBER_CONTIG \
979
+ | CA_KERNEL_WRITE))), \
874
980
  (n) = (st_in).slab_dims[0], \
875
981
  1); \
876
982
  __cffi_init; \
877
983
  __cffi_init = 0, \
878
- ca_iter_state_finish(&(st_in)), \
879
- ca_iter_state_finish(&(st_out)) ) \
880
- for ( ; (st_in).src->ndim == (st_out).src->ndim \
881
- && (st_in).src->elements == (st_out).src->elements \
882
- && (st_in).slab_dims[0] == (st_out).slab_dims[0] \
883
- && 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) \
884
987
  && ca_iter_state_next_slab_axes(&(st_out), &(p_out), NULL); \
885
988
  ca_iter_state_sync_slab(&(st_in)), \
886
989
  ca_iter_state_sync_slab(&(st_out)) )
@@ -888,27 +991,95 @@ void ca_iter_state_finish (ca_iter_state *st);
888
991
  #define CA_FOR_EACH_FIBER_INOUT_MASKED(st_in, st_out, ca_in, ca_out, axis, \
889
992
  flags, p_in, p_out, n, m) \
890
993
  for ( int __cffim_init = ( \
891
- ca_iter_state_init_l2(&(st_in), (ca_in), CA_SLAB_AXES, \
892
- (int8_t[]){(int8_t)(axis)}, 1, \
893
- (flags) | CA_KERNEL_FIBER_CONTIG), \
894
- ca_iter_state_init_l2(&(st_out), (ca_out), CA_SLAB_AXES, \
895
- (int8_t[]){(int8_t)(axis)}, 1, \
896
- ((flags) | CA_KERNEL_FIBER_CONTIG \
897
- | CA_KERNEL_WRITE)), \
994
+ ca_iter_check_same_shape((ca_in), (ca_out)), \
995
+ ca_iter_check_init( \
996
+ ca_iter_state_init_l2(&(st_in), (ca_in), CA_SLAB_AXES, \
997
+ (int8_t[]){(int8_t)(axis)}, 1, \
998
+ (flags) | CA_KERNEL_FIBER_CONTIG)), \
999
+ ca_iter_check_init( \
1000
+ ca_iter_state_init_l2_paired(&(st_out), &(st_in), (ca_out), \
1001
+ CA_SLAB_AXES, \
1002
+ (int8_t[]){(int8_t)(axis)}, 1, \
1003
+ ((flags) | CA_KERNEL_FIBER_CONTIG \
1004
+ | CA_KERNEL_WRITE))), \
898
1005
  (n) = (st_in).slab_dims[0], \
899
1006
  1); \
900
1007
  __cffim_init; \
901
1008
  __cffim_init = 0, \
902
- ca_iter_state_finish(&(st_in)), \
903
- ca_iter_state_finish(&(st_out)) ) \
904
- for ( ; (st_in).src->ndim == (st_out).src->ndim \
905
- && (st_in).src->elements == (st_out).src->elements \
906
- && (st_in).slab_dims[0] == (st_out).slab_dims[0] \
907
- && 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)) \
908
1012
  && ca_iter_state_next_slab_axes(&(st_out), &(p_out), NULL); \
909
1013
  ca_iter_state_sync_slab(&(st_in)), \
910
1014
  ca_iter_state_sync_slab(&(st_out)) )
911
1015
 
1016
+ /* PAIR forms: two sources read together along the same axis.
1017
+
1018
+ The INOUT forms cover input + output. These cover input + input, which
1019
+ is what a routine taking two vectors of the same length wants -- a
1020
+ correlation, a dot product, a distance. Neither state gets
1021
+ CA_KERNEL_WRITE, and `flags` must not carry it: use the INOUT forms to
1022
+ write.
1023
+
1024
+ Both fibers are contig-delivered, so the pair may be handed straight to
1025
+ a C routine that walks it itself. The MASKED form yields BOTH mask
1026
+ cursors: which cells a pair of fibers may be used at is a question about
1027
+ both of them, and the INOUT forms answer only about the input.
1028
+
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. */
1034
+
1035
+ #define CA_FOR_EACH_FIBER_PAIR(st_a, st_b, ca_a, ca_b, axis, \
1036
+ flags, p_a, p_b, n) \
1037
+ for ( int __cffp_init = ( \
1038
+ ca_iter_check_same_shape((ca_a), (ca_b)), \
1039
+ ca_iter_check_init( \
1040
+ ca_iter_state_init_l2(&(st_a), (ca_a), CA_SLAB_AXES, \
1041
+ (int8_t[]){(int8_t)(axis)}, 1, \
1042
+ (flags) | CA_KERNEL_FIBER_CONTIG)), \
1043
+ ca_iter_check_init( \
1044
+ ca_iter_state_init_l2_paired(&(st_b), &(st_a), (ca_b), \
1045
+ CA_SLAB_AXES, \
1046
+ (int8_t[]){(int8_t)(axis)}, 1, \
1047
+ (flags) | CA_KERNEL_FIBER_CONTIG)), \
1048
+ (n) = (st_a).slab_dims[0], \
1049
+ 1); \
1050
+ __cffp_init; \
1051
+ __cffp_init = 0, \
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) \
1055
+ && ca_iter_state_next_slab_axes(&(st_b), &(p_b), NULL); \
1056
+ ca_iter_state_sync_slab(&(st_a)), \
1057
+ ca_iter_state_sync_slab(&(st_b)) )
1058
+
1059
+ #define CA_FOR_EACH_FIBER_PAIR_MASKED(st_a, st_b, ca_a, ca_b, axis, \
1060
+ flags, p_a, p_b, n, m_a, m_b) \
1061
+ for ( int __cffpm_init = ( \
1062
+ ca_iter_check_same_shape((ca_a), (ca_b)), \
1063
+ ca_iter_check_init( \
1064
+ ca_iter_state_init_l2(&(st_a), (ca_a), CA_SLAB_AXES, \
1065
+ (int8_t[]){(int8_t)(axis)}, 1, \
1066
+ (flags) | CA_KERNEL_FIBER_CONTIG)), \
1067
+ ca_iter_check_init( \
1068
+ ca_iter_state_init_l2_paired(&(st_b), &(st_a), (ca_b), \
1069
+ CA_SLAB_AXES, \
1070
+ (int8_t[]){(int8_t)(axis)}, 1, \
1071
+ (flags) | CA_KERNEL_FIBER_CONTIG)), \
1072
+ (n) = (st_a).slab_dims[0], \
1073
+ 1); \
1074
+ __cffpm_init; \
1075
+ __cffpm_init = 0, \
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)) \
1079
+ && ca_iter_state_next_slab_axes(&(st_b), &(p_b), &(m_b)); \
1080
+ ca_iter_state_sync_slab(&(st_a)), \
1081
+ ca_iter_state_sync_slab(&(st_b)) )
1082
+
912
1083
  /* ---- Phase D: per-data_type reduction macro suite ----------------------- */
913
1084
 
914
1085
  /* CA_SLAB_REDUCE_T(T, ...): generic per-data_type slab reduction. T is the
@@ -2244,7 +2415,7 @@ void ca_iter_state_finish (ca_iter_state *st);
2244
2415
  Both states must share slab geometry (= same slab_ndim and slab_dims),
2245
2416
  typically by initialising both with the same policy + axes on
2246
2417
  shape-equal CArrays. Mask handling is **not** done by this macro —
2247
- if your input is masked, see §6.2 mask propagation discussion in
2418
+ if your input is masked, see §8 Mask handling in
2248
2419
  docs/authoring/HOW_TO_WRITE_KERNEL.md. Caller is responsible for invoking
2249
2420
  ca_iter_state_sync_slab on the output state after each slab.
2250
2421