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
@@ -30,6 +30,106 @@
30
30
 
31
31
  #include <assert.h>
32
32
  #include <string.h>
33
+ #include <ruby/ractor.h>
34
+
35
+ /* Private flags bit, kept in ca_iter_state.flags like
36
+ CA_ITER_FLAG_WALK_MAY_RAISE below: src is a copy the walk owns (see
37
+ ca_iter_reopen_on_copy), freed when the walk finishes. */
38
+ #define CA_ITER_FLAG_OWNS_SRC 0x40000000u
39
+
40
+ /* Data scratch. A scratch of object cells holds VALUEs no Ruby object
41
+ owns while the kernel calls Ruby for each cell, so it is filled with
42
+ Qnil and held for the GC until finish releases it.
43
+
44
+ One large scratch is kept when a walk frees it and handed to the next
45
+ walk that fits in it: a fresh allocation of that size costs the page
46
+ faults of touching it, which for a reduction is several times the
47
+ reduction itself. The kept buffer is owned by nobody, and a walk that
48
+ takes it owns it alone until it frees it again. Each Ractor keeps its
49
+ own (ractor-local storage), since walks in different Ractors run at
50
+ the same time. */
51
+
52
+ #define KI_KEEP_MIN ((ca_size_t) 1 << 20)
53
+ #define KI_KEEP_MAX ((ca_size_t) 64 << 20)
54
+
55
+ typedef struct {
56
+ void *p;
57
+ ca_size_t bytes;
58
+ } ki_kept_t;
59
+
60
+ static rb_ractor_local_key_t ki_kept_key;
61
+
62
+ static void
63
+ ki_kept_free (void *ptr)
64
+ {
65
+ ki_kept_t *k = (ki_kept_t *) ptr;
66
+ if ( k ) {
67
+ if ( k->p ) ruby_xfree(k->p);
68
+ ruby_xfree(k);
69
+ }
70
+ }
71
+
72
+ static const struct rb_ractor_local_storage_type ki_kept_type = {
73
+ NULL, ki_kept_free
74
+ };
75
+
76
+ static ki_kept_t *
77
+ ki_kept_slot (void)
78
+ {
79
+ ki_kept_t *k = (ki_kept_t *) rb_ractor_local_storage_ptr(ki_kept_key);
80
+ if ( k == NULL ) {
81
+ k = ALLOC(ki_kept_t);
82
+ k->p = NULL;
83
+ k->bytes = 0;
84
+ rb_ractor_local_storage_ptr_set(ki_kept_key, k);
85
+ }
86
+ return k;
87
+ }
88
+
89
+ static void *
90
+ ki_data_alloc (int8_t data_type, ca_size_t bytes)
91
+ {
92
+ void *p = NULL;
93
+ if ( bytes >= KI_KEEP_MIN ) {
94
+ ki_kept_t *k = ki_kept_slot();
95
+ if ( k->p && bytes <= k->bytes ) {
96
+ p = k->p;
97
+ k->p = NULL;
98
+ k->bytes = 0;
99
+ }
100
+ }
101
+ if ( p == NULL ) {
102
+ p = xmalloc(bytes > 0 ? bytes : 1);
103
+ }
104
+ if ( data_type == CA_OBJECT ) {
105
+ VALUE *v = (VALUE *) p;
106
+ ca_size_t n = bytes / (ca_size_t) sizeof(VALUE);
107
+ ca_size_t i;
108
+ for ( i = 0; i < n; i++ ) v[i] = Qnil;
109
+ ca_gc_hold_buffer(p, n);
110
+ }
111
+ return p;
112
+ }
113
+
114
+ /* `bytes` is the size p was allocated with (0 when unknown: never kept). */
115
+ static void
116
+ ki_data_free (void *p, ca_size_t bytes)
117
+ {
118
+ ca_gc_release_buffer(p);
119
+ if ( bytes >= KI_KEEP_MIN && bytes <= KI_KEEP_MAX ) {
120
+ ki_kept_t *k = ki_kept_slot();
121
+ if ( bytes > k->bytes ) {
122
+ void *old = k->p;
123
+ k->p = p;
124
+ k->bytes = bytes;
125
+ if ( old ) {
126
+ xfree(old);
127
+ }
128
+ return;
129
+ }
130
+ }
131
+ xfree(p);
132
+ }
33
133
 
34
134
  /* Defined in carray_core.c. (ca_is_readonly / ca_has_mask /
35
135
  ca_sync_data are already declared in carray.h.) */
@@ -103,6 +203,7 @@ extern ca_operation_function_t ca_monop_func;
103
203
  extern ca_operation_function_t ca_binop_func;
104
204
  extern ca_operation_function_t ca_bincmp_func;
105
205
  extern ca_operation_function_t ca_moncmp_func;
206
+ extern ca_operation_function_t ca_triop_func;
106
207
  extern CArray *ca_remap_new (CArray *ref, CArray *idx);
107
208
 
108
209
  /* ca_reduce_new is declared in carray.h; rb_cCAReduce defined in
@@ -236,6 +337,13 @@ ca_iter_classify_source (CArray *src)
236
337
  if ( attach == ca_binop_func.attach ) return CA_ITER_SRC_ATTACH;
237
338
  if ( attach == ca_bincmp_func.attach ) return CA_ITER_SRC_ATTACH;
238
339
  if ( attach == ca_moncmp_func.attach ) return CA_ITER_SRC_ATTACH;
340
+ if ( attach == ca_triop_func.attach ) return CA_ITER_SRC_ATTACH;
341
+
342
+ /* Anything else is a view class installed from outside that did not
343
+ register. Attaching is the one thing every view can do, and it is
344
+ what a registration would declare, so the array is delivered that way
345
+ rather than refused. */
346
+ if ( attach != NULL ) return CA_ITER_SRC_ATTACH;
239
347
 
240
348
  return CA_ITER_SRC_NONE;
241
349
  }
@@ -270,6 +378,18 @@ ca_iter_route_source (CArray *src,
270
378
  uint8_t kind = ca_iter_classify_source(src);
271
379
  if ( kind != CA_ITER_SRC_DESCRIPTOR ) return kind;
272
380
 
381
+ /* The descriptor walk reads the parent's memory, which such a parent
382
+ only has after a whole materialise -- however few cells the view
383
+ selects. Deliver through the view's own transfer instead: SRC_ATTACH
384
+ gathers the view into scratch with ca_xfer_all, and the view asks its
385
+ parent for the selected cells alone. */
386
+ {
387
+ CArray *parent = ((CAView *) src)->parent;
388
+ if ( ! parent->ptr && ca_parent_lends_no_memory(parent) ) {
389
+ return CA_ITER_SRC_ATTACH;
390
+ }
391
+ }
392
+
273
393
  /* Descriptor source: describe_axes + inspect innermost. */
274
394
  ca_iter_describe_axes(src, out_descs, out_parent_dims, out_ndim);
275
395
  if ( ca_axis_dispatch_is_innermost_stride(out_descs, *out_ndim) ) {
@@ -485,39 +605,33 @@ ca_iter_validate_inputs (ca_iter_state *st,
485
605
  if ( policy != CA_SLAB_WHOLE && policy != CA_SLAB_AXES )
486
606
  return CA_ITER_ERR_POLICY;
487
607
 
488
- /* Step 6: accept CA_KERNEL_WRITE and CA_KERNEL_NO_MASK.
489
- CA_KERNEL_CHUNK_HINT is reserved for T2 — still rejected.
490
- NO_MASK enforcement (= reject masked source if NO_MASK is set)
491
- lands in step 7; step 6 accepts the flag but does not enforce.
492
- CA_KERNEL_FIBER_CONTIG (PROPOSAL_FIBER_DELIVERY F.1a) accepted —
493
- activates per-fiber contig delivery for naxes==1 in next_slab_axes. */
608
+ /* Accepted: CA_KERNEL_WRITE, CA_KERNEL_NO_MASK (enforced below) and
609
+ CA_KERNEL_FIBER_CONTIG (per-fiber contig delivery for naxes==1 in
610
+ next_slab_axes). CA_KERNEL_CHUNK_HINT is reserved and rejected. */
494
611
  const uint32_t accepted = CA_KERNEL_WRITE | CA_KERNEL_NO_MASK
495
612
  | CA_KERNEL_FIBER_CONTIG;
496
613
  if ( flags & ~accepted ) return CA_ITER_ERR_FLAGS;
497
614
 
498
- /* WRITE on a readonly view (CARepeat stride-0 / value_array /
499
- CAWrap readonly) — explicit reject, would otherwise SEGV on
500
- write. */
615
+ /* WRITE on a readonly view (CARepeat stride-0 / a lazy expression /
616
+ a read-only CAWrap / any view of a frozen or read-only array) --
617
+ explicit reject, would otherwise SEGV on write. */
501
618
  if ( (flags & CA_KERNEL_WRITE) && ca_is_readonly(src) ) {
502
619
  return CA_ITER_ERR_READONLY;
503
620
  }
504
621
 
505
- /* Step 6: mask is default-borne (bakeoff #5). The step-4
506
- CA_ITER_ERR_MASK gate is lifted (= masked sources are accepted).
507
- Step 7: enforce CA_KERNEL_NO_MASK as an explicit kernel-character
508
- declaration — if a kernel says "I cannot handle mask" and a
509
- masked source is handed in, reject with a dedicated error code
510
- so the caller can decide whether to peel via .value /
511
- .strip_mask(fill) or pick a mask-aware kernel. */
622
+ /* Masked sources are accepted; the mask travels with the slab, so
623
+ nothing returns CA_ITER_ERR_MASK. CA_KERNEL_NO_MASK is the kernel
624
+ declaring it cannot handle a mask: a masked source is then refused
625
+ with a dedicated code, so the caller can decide whether to peel via
626
+ .value / .strip_mask(fill) or pick a mask-aware kernel. */
512
627
  if ( (flags & CA_KERNEL_NO_MASK) && ca_has_mask(src) ) {
513
628
  return CA_ITER_ERR_MASK_NOT_ALLOWED;
514
629
  }
515
630
 
516
- /* Gate: classify source. sub-step 5.1 routing accepts entity +
517
- CAStride family (step 1-4) and CSA + CAGrid (step 5.1). Other
518
- descriptor views (CASelect / CAMapping / CAWindow / CAShift)
519
- and overlay views (CAFake / ...) still reject — handled in
520
- 5.2 and Phase 2 respectively. */
631
+ /* Gate: classify source. Entity, CAStride family, descriptor views
632
+ and SRC_ATTACH views are accepted; a source the classifier does not
633
+ recognise (an externally installed view that has not registered a
634
+ kind) is refused. */
521
635
  if ( ca_iter_classify_source(src) == CA_ITER_SRC_NONE ) {
522
636
  return CA_ITER_ERR_NOT_CHEAP;
523
637
  }
@@ -553,6 +667,29 @@ ca_iter_strip_storage_wrapper (CArray *src)
553
667
  return src;
554
668
  }
555
669
 
670
+ /* Raise unless a and b have the same shape. The INOUT and PAIR fiber
671
+ macros call it before opening either walk, so a refusal leaves nothing
672
+ to close. */
673
+ int
674
+ ca_iter_check_same_shape (struct _CArray *a, struct _CArray *b)
675
+ {
676
+ int8_t k;
677
+ int same = ( a->ndim == b->ndim );
678
+ for ( k = 0; same && k < a->ndim; k++ ) {
679
+ if ( a->dim[k] != b->dim[k] ) same = 0;
680
+ }
681
+ if ( ! same ) {
682
+ VALUE sa = rb_ary_new(), sb = rb_ary_new();
683
+ for ( k = 0; k < a->ndim; k++ ) rb_ary_push(sa, SIZE2NUM(a->dim[k]));
684
+ for ( k = 0; k < b->ndim; k++ ) rb_ary_push(sb, SIZE2NUM(b->dim[k]));
685
+ rb_raise(rb_eArgError,
686
+ "kernel iterator: the two arrays of a paired walk "
687
+ "differ in shape (%"PRIsVALUE" vs %"PRIsVALUE")",
688
+ rb_inspect(sa), rb_inspect(sb));
689
+ }
690
+ return 0;
691
+ }
692
+
556
693
  int
557
694
  ca_iter_check_init (int rc)
558
695
  {
@@ -562,7 +699,7 @@ ca_iter_check_init (int rc)
562
699
 
563
700
  switch ( rc ) {
564
701
  case CA_ITER_ERR_NOT_CHEAP:
565
- why = "the source cannot be read without materialising it";
702
+ why = "the iterator does not recognise this kind of source";
566
703
  break;
567
704
  case CA_ITER_ERR_POLICY:
568
705
  why = "the slab policy or the axes given do not apply to this source";
@@ -587,6 +724,92 @@ ca_iter_check_init (int rc)
587
724
  return rc; /* not reached */
588
725
  }
589
726
 
727
+ /* Whole-view scratch. The view is gathered into the walk's scratch,
728
+ by the view's own transfer (by_copy = 0) or by ca_copy_data (by_copy =
729
+ 1, the CAStride compose-fold). A WRITE walk sends back only the cells
730
+ the kernel changed (ki_scratch_push_changed), so the scratch is twice
731
+ the view and its second half keeps what was gathered: a cell the
732
+ kernel left alone must not be sent back, for it may be another copy
733
+ of a cell the kernel did write (a tile, a stack of one array with
734
+ itself) or a cell a conversion view would not bring back unchanged. */
735
+ static void
736
+ ki_gather_view (ca_iter_state *st, CArray *src, uint32_t flags, int by_copy)
737
+ {
738
+ ca_size_t view_bytes = (ca_size_t) src->elements * src->bytes;
739
+ st->scratch_cap = (flags & CA_KERNEL_WRITE) ? 2 * view_bytes : view_bytes;
740
+ st->scratch_ptr = (char *) ki_data_alloc(src->data_type, st->scratch_cap);
741
+ if ( src->elements > 0 ) {
742
+ if ( by_copy ) {
743
+ ca_copy_data(src, st->scratch_ptr);
744
+ }
745
+ else {
746
+ ca_xfer_all(src, st->scratch_ptr, CA_XFER_GET);
747
+ }
748
+ if ( flags & CA_KERNEL_WRITE ) {
749
+ memcpy(st->scratch_ptr + view_bytes, st->scratch_ptr, view_bytes);
750
+ }
751
+ }
752
+ }
753
+
754
+ /* Send back the cells of the current slab (the whole view for a WHOLE
755
+ walk) that differ from what was gathered, and record them as sent.
756
+ The slab of a CA_SLAB_AXES walk starts at last_data_off in the
757
+ scratch, which is laid out row-major over the view, so a cell's
758
+ offset divided by the cell size is its address in the view. */
759
+ static void
760
+ ki_scratch_push_changed (ca_iter_state *st)
761
+ {
762
+ CArray *src = st->src;
763
+ ca_size_t bytes = src->bytes;
764
+ ca_size_t view_bytes = (ca_size_t) src->elements * bytes;
765
+ char *now = st->scratch_ptr;
766
+ char *was = st->scratch_ptr + view_bytes;
767
+ int whole = ! ( st->policy == CA_SLAB_AXES && st->level == 2 );
768
+ ca_size_t n = whole ? src->elements : st->slab_elements;
769
+ ca_size_t idx[CA_RANK_MAX];
770
+ ca_size_t *addrs;
771
+ char *vals;
772
+ ca_size_t n_changed = 0;
773
+ ca_size_t i;
774
+ int8_t k;
775
+ volatile VALUE h_addrs = 0, h_vals = 0;
776
+
777
+ if ( n <= 0 ) return;
778
+
779
+ addrs = ALLOCV_N(ca_size_t, h_addrs, n);
780
+ vals = (char *) ALLOCV(h_vals, n * bytes);
781
+ for ( k = 0; k < st->slab_ndim; k++ ) idx[k] = 0;
782
+
783
+ for ( i = 0; i < n; i++ ) {
784
+ ca_size_t off;
785
+ if ( whole ) {
786
+ off = i * bytes;
787
+ }
788
+ else {
789
+ off = st->last_data_off;
790
+ for ( k = 0; k < st->slab_ndim; k++ ) off += idx[k] * st->slab_strides[k];
791
+ for ( k = st->slab_ndim - 1; k >= 0; k-- ) {
792
+ if ( ++idx[k] < st->slab_dims[k] ) break;
793
+ idx[k] = 0;
794
+ }
795
+ }
796
+ if ( memcmp(now + off, was + off, bytes) != 0 ) {
797
+ memcpy(was + off, now + off, bytes);
798
+ memcpy(vals + n_changed * bytes, now + off, bytes);
799
+ addrs[n_changed++] = off / bytes;
800
+ }
801
+ }
802
+
803
+ if ( whole && n_changed == n ) {
804
+ ca_xfer_all(src, now, CA_XFER_PUT);
805
+ }
806
+ else if ( n_changed > 0 ) {
807
+ ca_xfer_addrs(src, n_changed, addrs, vals, CA_XFER_PUT);
808
+ }
809
+ ALLOCV_END(h_vals);
810
+ ALLOCV_END(h_addrs);
811
+ }
812
+
590
813
  static int
591
814
  ca_iter_state_init_l1_unprotected (ca_iter_state *st,
592
815
  struct _CArray *src,
@@ -601,6 +824,7 @@ ca_iter_state_init_l1_unprotected (ca_iter_state *st,
601
824
  times rather than whatever its stack frame happened to contain. */
602
825
  if ( st == NULL ) return CA_ITER_ERR_FLAGS;
603
826
  memset(st, 0, sizeof(*st));
827
+ if ( src == NULL ) return CA_ITER_ERR_FLAGS;
604
828
 
605
829
  /* PROPOSAL_CAFACE_PHASE_2 F.2.6 + PROPOSAL_LAZY_MARKER_LIFT Phase 0:
606
830
  storage-identical wrapper strip at entry (= same rationale as init_l2
@@ -634,7 +858,6 @@ ca_iter_state_init_l1_unprotected (ca_iter_state *st,
634
858
  st->ndim = src->ndim;
635
859
  st->flags = flags;
636
860
  st->bytes = src->bytes;
637
- st->axes = axes;
638
861
  st->naxes = naxes;
639
862
 
640
863
  st->slab_n = src->elements;
@@ -652,11 +875,7 @@ ca_iter_state_init_l1_unprotected (ca_iter_state *st,
652
875
  Decouples kernel_iterator from per-view attach/sync lifecycle
653
876
  and inherits xfer reform improvements (transform-fused, etc.)
654
877
  automatically. */
655
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
656
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0 ? st->scratch_cap : 1);
657
- if ( src->elements > 0 ) {
658
- ca_xfer_all(src, st->scratch_ptr, CA_XFER_GET);
659
- }
878
+ ki_gather_view(st, src, flags, 0);
660
879
  st->alias_mode = CA_ITER_ALIAS_NONE; /* scratch-owned, no src.detach */
661
880
  st->alias_ptr = st->scratch_ptr;
662
881
  st->composed_strides[0] = src->bytes;
@@ -717,12 +936,11 @@ ca_iter_state_init_l1_unprotected (ca_iter_state *st,
717
936
  if ( ca_func[src->obj_type].attach == ca_window_func.attach ) {
718
937
  bound_fill = ((CAWindow *) src)->fill;
719
938
  }
720
- st->scratch_ptr = ca_axis_dispatch_attach(parent,
721
- st->parent_axis_dims,
722
- raw_descs, raw_ndim,
723
- src->bytes,
724
- st->total_elements,
725
- bound_fill);
939
+ st->scratch_cap = st->total_elements * src->bytes;
940
+ st->scratch_ptr = (char *) ki_data_alloc(src->data_type, st->scratch_cap);
941
+ ca_axis_dispatch_gather(parent, st->parent_axis_dims, raw_descs, raw_ndim,
942
+ src->bytes, st->total_elements, bound_fill,
943
+ st->scratch_ptr);
726
944
  st->alias_mode = CA_ITER_ALIAS_NONE;
727
945
  st->alias_ptr = st->scratch_ptr;
728
946
 
@@ -752,12 +970,7 @@ ca_iter_state_init_l1_unprotected (ca_iter_state *st,
752
970
  to ca_stride_func_copy_data which composes leaf strides up to
753
971
  the root entity and gathers in one pass — no per-intermediate
754
972
  view materialise. */
755
- ca_size_t cap = src->elements * src->bytes;
756
- st->scratch_cap = cap;
757
- st->scratch_ptr = xmalloc(cap > 0 ? cap : 1);
758
- if ( cap > 0 ) {
759
- ca_copy_data(src, st->scratch_ptr);
760
- }
973
+ ki_gather_view(st, src, flags, 1);
761
974
  st->alias_mode = CA_ITER_ALIAS_NONE;
762
975
  st->alias_ptr = st->scratch_ptr;
763
976
  }
@@ -794,6 +1007,7 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
794
1007
  times rather than whatever its stack frame happened to contain. */
795
1008
  if ( st == NULL ) return CA_ITER_ERR_FLAGS;
796
1009
  memset(st, 0, sizeof(*st));
1010
+ if ( src == NULL ) return CA_ITER_ERR_FLAGS;
797
1011
 
798
1012
  /* PROPOSAL_CAFACE_PHASE_2 F.2.6 (= MEMO §3.5 kernel_iterator entry strip)
799
1013
  + PROPOSAL_LAZY_MARKER_LIFT Phase 0.
@@ -862,7 +1076,7 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
862
1076
  and use per-fiber ca_xfer_stride which routes to the view's
863
1077
  X.1 OOB-fused per-region fast path. */
864
1078
  if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1
865
- && raw_ndim == src->ndim ) {
1079
+ && raw_ndim == src->ndim && ca_fast_paths_enabled() ) {
866
1080
  int8_t fiber_ax = axes[0];
867
1081
  ca_size_t row_byte_strides[CA_RANK_MAX];
868
1082
  {
@@ -882,7 +1096,6 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
882
1096
  st->ndim = src->ndim;
883
1097
  st->flags = flags;
884
1098
  st->bytes = src->bytes;
885
- st->axes = axes;
886
1099
  st->naxes = naxes;
887
1100
 
888
1101
  st->alias_mode = CA_ITER_ALIAS_PER_FIBER_FUSED;
@@ -956,13 +1169,24 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
956
1169
  return CA_ITER_ERR_POLICY;
957
1170
  }
958
1171
  int slab_has_non_stride = 0;
1172
+ int slab_has_index = 0;
959
1173
  for ( k = 0; k < src->ndim; k++ ) {
960
1174
  if ( in_slab[k] && raw_descs[k].kind != CA_AXIS_KIND_STRIDE ) {
961
1175
  slab_has_non_stride = 1;
962
- break;
1176
+ if ( raw_descs[k].kind == CA_AXIS_KIND_INDEX ) {
1177
+ slab_has_index = 1;
1178
+ }
963
1179
  }
964
1180
  }
965
- if ( slab_has_non_stride ) {
1181
+ /* A slab with a SHIFT axis that leaves out the innermost view axis
1182
+ would be gathered one strided cell at a time, once per slab. Read
1183
+ walks take the whole-view path below instead: the view is gathered
1184
+ once, row by row, and the slabs are walked in the copy. */
1185
+ int whole_view = slab_has_non_stride && ! slab_has_index
1186
+ && ! in_slab[src->ndim - 1]
1187
+ && ! ( flags & CA_KERNEL_WRITE )
1188
+ && ca_fast_paths_enabled();
1189
+ if ( slab_has_non_stride && ! whole_view ) {
966
1190
  /* C.1 scope: READ-only. WRITE = future sub-step (C.1c). */
967
1191
  if ( flags & CA_KERNEL_WRITE ) {
968
1192
  return CA_ITER_ERR_FLAGS;
@@ -975,7 +1199,6 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
975
1199
  st->ndim = src->ndim;
976
1200
  st->flags = flags;
977
1201
  st->bytes = src->bytes;
978
- st->axes = axes;
979
1202
  st->naxes = naxes;
980
1203
 
981
1204
  CArray *parent = CAVIEW(src)->parent;
@@ -1075,7 +1298,7 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1075
1298
  full-view materialise). */
1076
1299
  ca_size_t slab_bytes = st->slab_elements * src->bytes;
1077
1300
  st->scratch_cap = slab_bytes > 0 ? slab_bytes : 1;
1078
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap);
1301
+ st->scratch_ptr = (char *) ki_data_alloc(src->data_type, st->scratch_cap);
1079
1302
  st->alias_ptr = st->scratch_ptr; /* refilled per slab in next_slab_axes */
1080
1303
 
1081
1304
  /* Specialisation (B-1b, C.1b): if innermost slab axis is STRIDE
@@ -1083,7 +1306,7 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1083
1306
  manual gather path (= outer + non-innermost-slab hoist,
1084
1307
  inner pure STRIDE linear memcpy). Else (innermost INDEX /
1085
1308
  SHIFT-anywhere): use the engine fallback (A). */
1086
- int specialised_eligible = (sp > 0);
1309
+ int specialised_eligible = (sp > 0) && ca_fast_paths_enabled();
1087
1310
  if ( specialised_eligible ) {
1088
1311
  int8_t inner_view_ax = st->slab_axes_buf[sp - 1];
1089
1312
  if ( raw_descs[inner_view_ax].kind != CA_AXIS_KIND_STRIDE ) {
@@ -1135,7 +1358,7 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1135
1358
  }
1136
1359
  }
1137
1360
 
1138
- if ( outer_has_shift ) {
1361
+ if ( outer_has_shift || whole_view ) {
1139
1362
  st->src = src;
1140
1363
  st->src_kind = CA_ITER_SRC_DESCRIPTOR;
1141
1364
  st->level = 2;
@@ -1143,7 +1366,6 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1143
1366
  st->ndim = src->ndim;
1144
1367
  st->flags = flags;
1145
1368
  st->bytes = src->bytes;
1146
- st->axes = axes;
1147
1369
  st->naxes = naxes;
1148
1370
 
1149
1371
  CArray *parent = CAVIEW(src)->parent;
@@ -1157,10 +1379,12 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1157
1379
  bound_fill = ((CAWindow *) src)->fill;
1158
1380
  }
1159
1381
  st->scratch_cap = src->elements * src->bytes;
1160
- st->scratch_ptr = ca_axis_dispatch_attach(parent, raw_pdims,
1161
- local_descs, src->ndim,
1162
- src->bytes, src->elements,
1163
- bound_fill);
1382
+ st->scratch_ptr = (char *) ki_data_alloc(src->data_type, st->scratch_cap);
1383
+ ca_axis_dispatch_gather(parent, raw_pdims, local_descs, src->ndim,
1384
+ src->bytes, src->elements, bound_fill,
1385
+ st->scratch_ptr);
1386
+ /* sync_slab scatters the whole scratch back by this count. */
1387
+ st->total_elements = src->elements;
1164
1388
  st->alias_mode = CA_ITER_ALIAS_NONE;
1165
1389
  st->alias_ptr = st->scratch_ptr;
1166
1390
 
@@ -1236,7 +1460,6 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1236
1460
  st->ndim = src->ndim;
1237
1461
  st->flags = flags;
1238
1462
  st->bytes = src->bytes;
1239
- st->axes = axes;
1240
1463
  st->naxes = naxes;
1241
1464
 
1242
1465
  /* Parent row-major byte strides for offset computation. */
@@ -1383,7 +1606,6 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1383
1606
  st->ndim = src->ndim;
1384
1607
  st->flags = flags;
1385
1608
  st->bytes = src->bytes;
1386
- st->axes = axes;
1387
1609
  st->naxes = naxes;
1388
1610
 
1389
1611
  /* Row-major byte / element strides on the attached view buffer.
@@ -1429,7 +1651,8 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1429
1651
  if ( ca_func[src->obj_type].attach == ca_stack_func.attach
1430
1652
  && !ca_has_mask(src)
1431
1653
  && !(flags & CA_KERNEL_WRITE)
1432
- && naxes == 1 && axes[0] == ((CAStack *) src)->k_axis ) {
1654
+ && naxes == 1 && axes[0] == ((CAStack *) src)->k_axis
1655
+ && ca_fast_paths_enabled() ) {
1433
1656
  CAStack *stack = (CAStack *) src;
1434
1657
  int8_t parent_ndim = src->ndim - 1;
1435
1658
  int8_t sp = 0, op = 0;
@@ -1478,12 +1701,15 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1478
1701
  guarantees uniform shape (MEMO §3.2) so one set of parent
1479
1702
  native byte strides covers all K (= parents[0]->dim is
1480
1703
  canonical). */
1481
- st->stack_n_parents = stack->n_parents;
1704
+ /* stack_n_parents counts the parents attached so far: an attach
1705
+ that raises leaves finish with exactly those to detach. */
1706
+ st->stack_n_parents = 0;
1482
1707
  st->stack_parent_ptrs =
1483
1708
  (char **) xmalloc(stack->n_parents * sizeof(char *));
1484
1709
  for ( int32_t kk = 0; kk < stack->n_parents; kk++ ) {
1485
- ca_attach(stack->parents[kk]);
1710
+ ca_attach(stack->parents[kk]); /* window: finish detaches the counted parents */
1486
1711
  st->stack_parent_ptrs[kk] = (char *) stack->parents[kk]->ptr;
1712
+ st->stack_n_parents = kk + 1;
1487
1713
  }
1488
1714
  {
1489
1715
  ca_size_t s = src->bytes;
@@ -1495,7 +1721,7 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1495
1721
 
1496
1722
  /* Slab-sized scratch (= K * bytes for slab_axes == [0]). */
1497
1723
  st->scratch_cap = (ca_size_t) st->slab_elements * src->bytes;
1498
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0 ? st->scratch_cap : 1);
1724
+ st->scratch_ptr = (char *) ki_data_alloc(src->data_type, st->scratch_cap);
1499
1725
 
1500
1726
  /* Tile cache (pilot/castack-axis0-loop-interchange): refill TILE
1501
1727
  fibers per K parent reads, serve next TILE-1 fibers from L1d.
@@ -1510,7 +1736,7 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1510
1736
  st->stack_tile_cap = tile;
1511
1737
  st->stack_tile_pos = 0;
1512
1738
  st->stack_tile_have = 0; /* force refill on first call */
1513
- st->stack_tile_cache = (char *) xmalloc(K * tile * bytes);
1739
+ st->stack_tile_cache = (char *) ki_data_alloc(src->data_type, K * tile * bytes);
1514
1740
  }
1515
1741
 
1516
1742
  st->alias_mode = CA_ITER_ALIAS_STACK;
@@ -1545,7 +1771,8 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1545
1771
  counterpart. */
1546
1772
  if ( ca_func[src->obj_type].attach == ca_stack_func.attach
1547
1773
  && !(flags & CA_KERNEL_WRITE)
1548
- && naxes >= 1 && !in_slab[((CAStack *) src)->k_axis] ) {
1774
+ && naxes >= 1 && !in_slab[((CAStack *) src)->k_axis]
1775
+ && ca_fast_paths_enabled() ) {
1549
1776
  CAStack *stack = (CAStack *) src;
1550
1777
  int8_t k_axis = stack->k_axis;
1551
1778
  int8_t parent_ndim = src->ndim - 1;
@@ -1626,25 +1853,42 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1626
1853
 
1627
1854
  /* Attach K parents + cache ptr aliases. Symmetric with rev3
1628
1855
  Case B; for entity parents attach is O(1). */
1629
- st->stack_n_parents = stack->n_parents;
1856
+ /* stack_n_parents counts the parents attached so far: an attach
1857
+ that raises leaves finish with exactly those to detach. */
1858
+ st->stack_n_parents = 0;
1630
1859
  st->stack_parent_ptrs =
1631
1860
  (char **) xmalloc(stack->n_parents * sizeof(char *));
1632
1861
  for ( int32_t kk = 0; kk < stack->n_parents; kk++ ) {
1633
- ca_attach(stack->parents[kk]);
1862
+ ca_attach(stack->parents[kk]); /* window: finish detaches the counted parents */
1634
1863
  st->stack_parent_ptrs[kk] = (char *) stack->parents[kk]->ptr;
1864
+ st->stack_n_parents = kk + 1;
1635
1865
  }
1636
1866
 
1637
1867
  /* If CAStack carries mask (= horizontal propagation already
1638
- applied at create_mask), attach K parent masks + cache ptr
1639
- aliases. next_slab_axes aliases parent->mask[k]->ptr +
1640
- mask_off for the same slab. */
1868
+ applied at create_mask), copy the K parent masks as they are
1869
+ at the start of the walk; next_slab_axes points into the copy
1870
+ of parent k at mask_off. A copy, as on every other path: the
1871
+ cursor is the walk's own, and a write to it reaches nothing. */
1641
1872
  if ( ca_has_mask(src) ) {
1873
+ /* The stack's mask may still read stand-ins for parents that
1874
+ have gained a mask since; read through to those. */
1875
+ ca_multi_parent_mask_refresh(src->mask, stack->parents,
1876
+ stack->mask_standins,
1877
+ stack->n_parents, 0);
1878
+ /* Zero-filled: a slot is set once its copy is made, and finish
1879
+ frees the copies whose slot is set. */
1642
1880
  st->stack_parent_mask_ptrs =
1643
1881
  (boolean8_t **) xmalloc(stack->n_parents * sizeof(boolean8_t *));
1882
+ memset(st->stack_parent_mask_ptrs, 0,
1883
+ stack->n_parents * sizeof(boolean8_t *));
1644
1884
  for ( int32_t kk = 0; kk < stack->n_parents; kk++ ) {
1645
- ca_attach(stack->parents[kk]->mask);
1646
- st->stack_parent_mask_ptrs[kk] =
1647
- (boolean8_t *) stack->parents[kk]->mask->ptr;
1885
+ CArray *pm = ((CAStack *) src->mask)->parents[kk];
1886
+ ca_size_t n = pm->elements;
1887
+ boolean8_t *copy = (boolean8_t *) xmalloc(n > 0 ? n : 1);
1888
+ st->stack_parent_mask_ptrs[kk] = copy;
1889
+ ca_attach(pm); /* window: only a memcpy before the detach */
1890
+ memcpy(copy, pm->ptr, n);
1891
+ ca_detach(pm);
1648
1892
  }
1649
1893
  } else {
1650
1894
  st->stack_parent_mask_ptrs = NULL;
@@ -1664,7 +1908,8 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1664
1908
  is called on demand in next_slab_axes). Predicate is stubbed
1665
1909
  in F.6.1 (returns 0); F.6.2+ enables specific source kinds. */
1666
1910
  int per_fiber_fused = 0;
1667
- if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1 ) {
1911
+ if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1
1912
+ && ca_fast_paths_enabled() ) {
1668
1913
  int8_t fiber_ax = axes[0];
1669
1914
  ca_size_t fiber_ax_stride = row_byte_strides[fiber_ax];
1670
1915
  if ( ca_iter_should_per_fiber_fused(src, src_kind, fiber_ax,
@@ -1682,11 +1927,7 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1682
1927
  if ( !per_fiber_fused ) {
1683
1928
  /* 2026-05-31 refactor: iter-owns scratch via ca_xfer_all (was
1684
1929
  ca_attach(src) + alias src->ptr). */
1685
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
1686
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0 ? st->scratch_cap : 1);
1687
- if ( src->elements > 0 ) {
1688
- ca_xfer_all(src, st->scratch_ptr, CA_XFER_GET);
1689
- }
1930
+ ki_gather_view(st, src, flags, 0);
1690
1931
  st->alias_mode = CA_ITER_ALIAS_NONE;
1691
1932
  st->alias_ptr = st->scratch_ptr;
1692
1933
  }
@@ -1772,14 +2013,9 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1772
2013
  st->ndim = 1; /* logical 1-D L2 layout */
1773
2014
  st->flags = flags;
1774
2015
  st->bytes = src->bytes;
1775
- st->axes = axes;
1776
2016
  st->naxes = naxes;
1777
2017
 
1778
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
1779
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0 ? st->scratch_cap : 1);
1780
- if ( src->elements > 0 ) {
1781
- ca_xfer_all(src, st->scratch_ptr, CA_XFER_GET);
1782
- }
2018
+ ki_gather_view(st, src, flags, 0);
1783
2019
  st->alias_mode = CA_ITER_ALIAS_NONE;
1784
2020
  st->alias_ptr = st->scratch_ptr;
1785
2021
  st->composed_strides[0] = src->bytes;
@@ -1823,7 +2059,6 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1823
2059
  st->ndim = nd;
1824
2060
  st->flags = flags;
1825
2061
  st->bytes = src->bytes;
1826
- st->axes = axes;
1827
2062
  st->naxes = naxes;
1828
2063
 
1829
2064
  /* Persist descs / parent_axis_dims in state; build parent row-major
@@ -1913,7 +2148,6 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1913
2148
  st->ndim = src->ndim;
1914
2149
  st->flags = flags;
1915
2150
  st->bytes = src->bytes;
1916
- st->axes = axes;
1917
2151
  st->naxes = naxes;
1918
2152
 
1919
2153
  memcpy(st->parent_axis_dims, raw_pdims, raw_ndim * sizeof(ca_size_t));
@@ -1940,12 +2174,11 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1940
2174
  if ( ca_func[src->obj_type].attach == ca_window_func.attach ) {
1941
2175
  bound_fill = ((CAWindow *) src)->fill;
1942
2176
  }
1943
- st->scratch_ptr = ca_axis_dispatch_attach(parent,
1944
- st->parent_axis_dims,
1945
- raw_descs, raw_ndim,
1946
- src->bytes,
1947
- st->total_elements,
1948
- bound_fill);
2177
+ st->scratch_cap = st->total_elements * src->bytes;
2178
+ st->scratch_ptr = (char *) ki_data_alloc(src->data_type, st->scratch_cap);
2179
+ ca_axis_dispatch_gather(parent, st->parent_axis_dims, raw_descs, raw_ndim,
2180
+ src->bytes, st->total_elements, bound_fill,
2181
+ st->scratch_ptr);
1949
2182
 
1950
2183
  /* Lay out as a single 1-D L2 strided slab: ptr = scratch,
1951
2184
  n = total_elements, stride = bytes. next_slab_strided reads
@@ -1986,7 +2219,6 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
1986
2219
  st->ndim = src->ndim;
1987
2220
  st->flags = flags;
1988
2221
  st->bytes = src->bytes;
1989
- st->axes = axes;
1990
2222
  st->naxes = naxes;
1991
2223
 
1992
2224
  /* Compute composed strides + base. For entity / CAStride contig we
@@ -2102,18 +2334,13 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
2102
2334
  }
2103
2335
 
2104
2336
  if ( use_strided ) {
2105
- st->root = root;
2106
- ca_attach(root);
2337
+ ca_attach(root); /* window: finish detaches st->root */
2338
+ st->root = root; /* recorded once attached */
2107
2339
  st->alias_mode = CA_ITER_ALIAS_STRIDED;
2108
2340
  st->alias_ptr = (char *) root->ptr;
2109
2341
  } else if ( use_view_scratch ) {
2110
2342
  st->root = NULL;
2111
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
2112
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0
2113
- ? st->scratch_cap : 1);
2114
- if ( src->elements > 0 ) {
2115
- ca_copy_data(src, st->scratch_ptr);
2116
- }
2343
+ ki_gather_view(st, src, flags, 1);
2117
2344
  st->alias_mode = CA_ITER_ALIAS_NONE;
2118
2345
  st->alias_ptr = st->scratch_ptr;
2119
2346
  } else {
@@ -2156,18 +2383,13 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
2156
2383
  st->chunk_size = st->slab_n;
2157
2384
 
2158
2385
  if ( use_strided ) {
2159
- st->root = root;
2160
- ca_attach(root);
2386
+ ca_attach(root); /* window: finish detaches st->root */
2387
+ st->root = root; /* recorded once attached */
2161
2388
  st->alias_mode = CA_ITER_ALIAS_STRIDED;
2162
2389
  st->alias_ptr = (char *) root->ptr;
2163
2390
  } else if ( use_view_scratch ) {
2164
2391
  st->root = NULL;
2165
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
2166
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0
2167
- ? st->scratch_cap : 1);
2168
- if ( src->elements > 0 ) {
2169
- ca_copy_data(src, st->scratch_ptr);
2170
- }
2392
+ ki_gather_view(st, src, flags, 1);
2171
2393
  st->alias_mode = CA_ITER_ALIAS_NONE;
2172
2394
  st->alias_ptr = st->scratch_ptr;
2173
2395
  } else {
@@ -2176,6 +2398,17 @@ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
2176
2398
  st->alias_mode = CA_ITER_ALIAS_CONTIG;
2177
2399
  st->alias_ptr = (char *) src->ptr;
2178
2400
  }
2401
+
2402
+ /* Mask: gathered in view row-major order, as on the AXES path above;
2403
+ next_slab_strided hands out alias_mask + slab * slab_n. */
2404
+ if ( ca_has_mask(src) ) {
2405
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
2406
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
2407
+ if ( src->elements > 0 ) {
2408
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
2409
+ }
2410
+ st->alias_mask = st->scratch_mask;
2411
+ }
2179
2412
  return CA_ITER_OK;
2180
2413
  }
2181
2414
 
@@ -2271,6 +2504,55 @@ ca_iter_state_next_slab_strided_unprotected (ca_iter_state *st,
2271
2504
  return 1;
2272
2505
  }
2273
2506
 
2507
+ /* CA_KERNEL_FIBER_CONTIG: a fiber (a walk with one slab axis) reaches the
2508
+ kernel as a contiguous run of cells and of mask flags. Each path of
2509
+ next_slab_axes finds the fiber's first cell and the step along it, and
2510
+ hands both through these two functions, which copy a fiber with any
2511
+ other step into the walk's fiber scratch. Other walks get the pointer
2512
+ back unchanged. */
2513
+ static int
2514
+ ki_fiber_contig_wanted (const ca_iter_state *st)
2515
+ {
2516
+ return (st->flags & CA_KERNEL_FIBER_CONTIG)
2517
+ && st->naxes == 1 && st->slab_ndim == 1;
2518
+ }
2519
+
2520
+ static char *
2521
+ ki_fiber_contig_data (ca_iter_state *st, char *data, ca_size_t step)
2522
+ {
2523
+ ca_size_t n = st->slab_dims[0];
2524
+ ca_size_t bytes = st->bytes;
2525
+ ca_size_t need = n * bytes;
2526
+ if ( ! ki_fiber_contig_wanted(st) || step == bytes ) {
2527
+ return data;
2528
+ }
2529
+ if ( st->fiber_data_scratch_cap < need ) {
2530
+ if ( st->fiber_data_scratch ) ki_data_free(st->fiber_data_scratch, st->fiber_data_scratch_cap);
2531
+ st->fiber_data_scratch = (char *) ki_data_alloc(st->src->data_type, need);
2532
+ st->fiber_data_scratch_cap = need;
2533
+ }
2534
+ ca_stride_gather_run(st->fiber_data_scratch, data, bytes, n, step);
2535
+ return st->fiber_data_scratch;
2536
+ }
2537
+
2538
+ static boolean8_t *
2539
+ ki_fiber_contig_mask (ca_iter_state *st, boolean8_t *mask, ca_size_t step)
2540
+ {
2541
+ ca_size_t n = st->slab_dims[0];
2542
+ if ( mask == NULL || ! ki_fiber_contig_wanted(st) || step == 1 ) {
2543
+ return mask;
2544
+ }
2545
+ if ( st->fiber_mask_scratch_cap < n ) {
2546
+ if ( st->fiber_mask_scratch ) xfree(st->fiber_mask_scratch);
2547
+ st->fiber_mask_scratch = (boolean8_t *) xmalloc(n > 0 ? n : 1);
2548
+ st->fiber_mask_scratch_cap = n;
2549
+ }
2550
+ for ( ca_size_t i = 0; i < n; i++ ) {
2551
+ st->fiber_mask_scratch[i] = mask[i * step];
2552
+ }
2553
+ return st->fiber_mask_scratch;
2554
+ }
2555
+
2274
2556
  static int
2275
2557
  ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2276
2558
  char **out_ptr,
@@ -2370,7 +2652,11 @@ ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2370
2652
  }
2371
2653
 
2372
2654
  if ( out_ptr ) *out_ptr = st->scratch_ptr;
2373
- if ( out_mask ) *out_mask = st->scratch_mask ? st->scratch_mask + mask_off : NULL;
2655
+ if ( out_mask ) {
2656
+ *out_mask = ki_fiber_contig_mask(st,
2657
+ st->scratch_mask ? st->scratch_mask + mask_off : NULL,
2658
+ st->slab_mask_strides[0]);
2659
+ }
2374
2660
 
2375
2661
  st->slabs_emitted += 1;
2376
2662
  for ( int8_t m = st->outer_ndim - 1; m >= 0; m-- ) {
@@ -2406,11 +2692,15 @@ ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2406
2692
  mask_off += st->outer_idx[m] * st->stack_parent_mask_strides[parent_ax];
2407
2693
  }
2408
2694
 
2409
- if ( out_ptr ) *out_ptr = st->stack_parent_ptrs[k] + parent_off;
2695
+ if ( out_ptr ) {
2696
+ *out_ptr = ki_fiber_contig_data(st, st->stack_parent_ptrs[k] + parent_off,
2697
+ st->slab_strides[0]);
2698
+ }
2410
2699
  if ( out_mask ) {
2411
- *out_mask = st->stack_parent_mask_ptrs
2412
- ? st->stack_parent_mask_ptrs[k] + mask_off
2413
- : NULL;
2700
+ *out_mask = ki_fiber_contig_mask(st,
2701
+ st->stack_parent_mask_ptrs
2702
+ ? st->stack_parent_mask_ptrs[k] + mask_off : NULL,
2703
+ st->slab_mask_strides[0]);
2414
2704
  }
2415
2705
 
2416
2706
  st->slabs_emitted += 1;
@@ -2561,7 +2851,11 @@ ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2561
2851
  }
2562
2852
 
2563
2853
  if ( out_ptr ) *out_ptr = st->scratch_ptr;
2564
- if ( out_mask ) *out_mask = st->scratch_mask ? st->scratch_mask + mask_off : NULL;
2854
+ if ( out_mask ) {
2855
+ *out_mask = ki_fiber_contig_mask(st,
2856
+ st->scratch_mask ? st->scratch_mask + mask_off : NULL,
2857
+ st->slab_mask_strides[0]);
2858
+ }
2565
2859
 
2566
2860
  st->slabs_emitted += 1;
2567
2861
  /* Advance outer_idx row-major (innermost outer axis ticks first). */
@@ -2622,8 +2916,8 @@ ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2622
2916
  ca_size_t counts[CA_RANK_MAX];
2623
2917
 
2624
2918
  if ( st->fiber_data_scratch_cap < need ) {
2625
- if ( st->fiber_data_scratch ) xfree(st->fiber_data_scratch);
2626
- st->fiber_data_scratch = (char *) xmalloc(need > 0 ? need : 1);
2919
+ if ( st->fiber_data_scratch ) ki_data_free(st->fiber_data_scratch, st->fiber_data_scratch_cap);
2920
+ st->fiber_data_scratch = (char *) ki_data_alloc(st->src->data_type, need);
2627
2921
  st->fiber_data_scratch_cap = need;
2628
2922
  }
2629
2923
 
@@ -2701,72 +2995,14 @@ ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2701
2995
  Capture last_data_off BEFORE the outer_idx advance below; sync_slab
2702
2996
  consumes it for WRITE scatter. See header field doc + PROPOSAL
2703
2997
  §4.3.2 hazard comment in sync_slab. */
2704
- char *yield_ptr;
2705
- if ( (st->flags & CA_KERNEL_FIBER_CONTIG)
2706
- && st->naxes == 1 && st->slab_ndim == 1 ) {
2707
- ca_size_t n = st->slab_dims[0];
2708
- ca_size_t data_step = st->slab_strides[0];
2709
- ca_size_t bytes = st->bytes;
2710
- char *src_data = st->alias_ptr + data_off;
2711
-
2712
- st->last_data_off = data_off;
2713
-
2714
- if ( data_step == (ca_size_t) bytes ) {
2715
- /* Fast path: fiber is already contig (= innermost-axis or stride
2716
- coincidentally == bytes). No gather needed. */
2717
- yield_ptr = src_data;
2718
- } else {
2719
- /* Per-fiber gather via the typed-store inline helper
2720
- (ca_iter_substrate.h). For bytes in {1,2,4,8} this uses a
2721
- compiler-vectorize-friendly `*dp++ = v; sp += step` loop;
2722
- other sizes fall back to per-element memcpy. Lazy-alloc
2723
- scratch sized to max fiber bytes (slab_dims[0] constant per
2724
- walk → single alloc in practice). */
2725
- ca_size_t need = n * bytes;
2726
- if ( st->fiber_data_scratch_cap < need ) {
2727
- if ( st->fiber_data_scratch ) xfree(st->fiber_data_scratch);
2728
- st->fiber_data_scratch = (char *) xmalloc(need);
2729
- st->fiber_data_scratch_cap = need;
2730
- }
2731
- ca_stride_gather_run(st->fiber_data_scratch, src_data,
2732
- bytes, n, data_step);
2733
- yield_ptr = st->fiber_data_scratch;
2734
- }
2735
- } else {
2736
- yield_ptr = st->alias_ptr + data_off;
2737
- }
2738
-
2739
- /* PROPOSAL_FIBER_DELIVERY F.1b: per-fiber contig mask delivery.
2740
- Symmetric to F.1a data path above. When the source has a mask
2741
- (= alias_mask != NULL) and the fiber's mask is not innermost-contig
2742
- (= slab_mask_strides[0] != 1), gather it into fiber_mask_scratch
2743
- so the author can write m[i] without stride math. Mask is
2744
- read-only here (= L2 WRITE never propagates to mask state), so no
2745
- scatter is needed in sync_slab. */
2746
- boolean8_t *yield_mask;
2747
- if ( (st->flags & CA_KERNEL_FIBER_CONTIG)
2748
- && st->naxes == 1 && st->slab_ndim == 1
2749
- && st->alias_mask != NULL ) {
2750
- ca_size_t n = st->slab_dims[0];
2751
- ca_size_t mask_step = st->slab_mask_strides[0];
2752
- boolean8_t *src_mask = st->alias_mask + mask_off;
2753
-
2754
- if ( mask_step == 1 ) {
2755
- yield_mask = src_mask;
2756
- } else {
2757
- if ( st->fiber_mask_scratch_cap < (ca_size_t) n ) {
2758
- if ( st->fiber_mask_scratch ) xfree(st->fiber_mask_scratch);
2759
- st->fiber_mask_scratch = (boolean8_t *) xmalloc(n);
2760
- st->fiber_mask_scratch_cap = n;
2761
- }
2762
- for ( ca_size_t i = 0; i < n; i++ ) {
2763
- st->fiber_mask_scratch[i] = src_mask[i * mask_step];
2764
- }
2765
- yield_mask = st->fiber_mask_scratch;
2766
- }
2767
- } else {
2768
- yield_mask = st->alias_mask ? st->alias_mask + mask_off : NULL;
2769
- }
2998
+ /* sync_slab finds the slab here: the fiber it scatters back, and the
2999
+ cells of a whole-view scratch it compares. */
3000
+ st->last_data_off = data_off;
3001
+ char *yield_ptr = ki_fiber_contig_data(st, st->alias_ptr + data_off,
3002
+ st->slab_strides[0]);
3003
+ boolean8_t *yield_mask = ki_fiber_contig_mask(st,
3004
+ st->alias_mask ? st->alias_mask + mask_off : NULL,
3005
+ st->slab_mask_strides[0]);
2770
3006
 
2771
3007
  if ( out_ptr ) *out_ptr = yield_ptr;
2772
3008
  if ( out_mask ) *out_mask = yield_mask;
@@ -2884,12 +3120,9 @@ ca_iter_state_sync_slab_unprotected (ca_iter_state *st)
2884
3120
  below early-returns. */
2885
3121
  }
2886
3122
 
2887
- /* SRC_ATTACH path (step 9 + 2026-05-31 refactor): kernel wrote into
2888
- iterator-owned scratch (= scratch_ptr). Push back via xfer_all PUT
2889
- which routes through the view's xfer_all slot -- handles CAFake
2890
- (cast back), CAByteSwap (swap back), CABitfield/CABitarray (bit
2891
- pack back), CAReduce (broadcast across reduce window). Inherits
2892
- transform-fused / partial materialise / etc. automatically.
3123
+ /* SRC_ATTACH path: the kernel wrote into the iterator's whole-view
3124
+ scratch. The changed cells go back through the view's own transfer,
3125
+ which converts, packs or scatters them as the view requires.
2893
3126
 
2894
3127
  This has to stay below the two per-fiber blocks: consulting
2895
3128
  src_kind first would push back the whole-view buffer while the
@@ -2897,9 +3130,7 @@ ca_iter_state_sync_slab_unprotected (ca_iter_state *st)
2897
3130
  fiber_data_scratch, and would push back a NULL one for
2898
3131
  PER_FIBER_FUSED, which owns no whole-view buffer at all. */
2899
3132
  if ( st->src_kind == CA_ITER_SRC_ATTACH ) {
2900
- if ( st->src->elements > 0 ) {
2901
- ca_xfer_all(st->src, st->scratch_ptr, CA_XFER_PUT);
2902
- }
3133
+ ki_scratch_push_changed(st);
2903
3134
  st->write_dirty = 0;
2904
3135
  return;
2905
3136
  }
@@ -2918,11 +3149,10 @@ ca_iter_state_sync_slab_unprotected (ca_iter_state *st)
2918
3149
  case CA_ITER_SRC_CASTRIDE:
2919
3150
  /* Two producers of CAStride + scratch: L1 non-contig, and the L2
2920
3151
  non-entity-root path (init_l2's use_view_scratch). Both gathered
2921
- with ca_copy_data into a view row-major buffer, so both scatter
2922
- back the same way — ca_sync_data routes through the view's
2923
- xfer_all(PUT), which asks the root for the region it owns rather
2924
- than writing a whole-root materialise back. */
2925
- ca_sync_data(st->src, st->scratch_ptr);
3152
+ with ca_copy_data into a view row-major buffer; the changed
3153
+ cells go back through the view's own transfer, which asks the
3154
+ root for the region it owns. */
3155
+ ki_scratch_push_changed(st);
2926
3156
  break;
2927
3157
 
2928
3158
  case CA_ITER_SRC_DESCRIPTOR: {
@@ -2955,12 +3185,17 @@ ca_iter_state_sync_slab_unprotected (ca_iter_state *st)
2955
3185
  st->write_dirty = 0;
2956
3186
  }
2957
3187
 
3188
+ static VALUE ca_iter_finish_sync (VALUE arg);
3189
+
2958
3190
  void
2959
3191
  ca_iter_state_finish (ca_iter_state *st)
2960
3192
  {
2961
3193
  if ( st == NULL || st->src == NULL ) {
2962
3194
  return;
2963
3195
  }
3196
+ /* A slab still handed out is dropped: finish abandons it. The block
3197
+ macros close with ca_iter_state_close, which writes it back first. */
3198
+ st->write_dirty = 0;
2964
3199
  /* composed_strides is inline — no free needed. outer_idx is heap
2965
3200
  for L2 multi-d sources (NULL on L1 paths and on L2 0/1-d). */
2966
3201
  if ( st->outer_idx ) {
@@ -2977,7 +3212,7 @@ ca_iter_state_finish (ca_iter_state *st)
2977
3212
  ca_detach.
2978
3213
  - else (alias paths): src was attached at init, detach. */
2979
3214
  if ( st->scratch_ptr ) {
2980
- xfree(st->scratch_ptr);
3215
+ ki_data_free(st->scratch_ptr, st->scratch_cap);
2981
3216
  st->scratch_ptr = NULL;
2982
3217
  st->scratch_cap = 0;
2983
3218
  }
@@ -2987,7 +3222,7 @@ ca_iter_state_finish (ca_iter_state *st)
2987
3222
  }
2988
3223
  /* PROPOSAL_FIBER_DELIVERY F.1a/F.1b: per-fiber scratch lifecycle. */
2989
3224
  if ( st->fiber_data_scratch ) {
2990
- xfree(st->fiber_data_scratch);
3225
+ ki_data_free(st->fiber_data_scratch, st->fiber_data_scratch_cap);
2991
3226
  st->fiber_data_scratch = NULL;
2992
3227
  st->fiber_data_scratch_cap = 0;
2993
3228
  }
@@ -3009,24 +3244,34 @@ ca_iter_state_finish (ca_iter_state *st)
3009
3244
  }
3010
3245
  /* pilot/castack-axis0-loop-interchange: free tile cache. */
3011
3246
  if ( st->stack_tile_cache ) {
3012
- xfree(st->stack_tile_cache);
3247
+ ki_data_free(st->stack_tile_cache, 0);
3013
3248
  st->stack_tile_cache = NULL;
3014
3249
  st->stack_tile_cap = 0;
3015
3250
  st->stack_tile_pos = 0;
3016
3251
  st->stack_tile_have = 0;
3017
3252
  }
3018
3253
  if ( st->stack_parent_mask_ptrs ) {
3019
- CAStack *stack = (CAStack *) st->src;
3020
3254
  for ( int32_t kk = 0; kk < st->stack_n_parents; kk++ ) {
3021
- ca_detach(stack->parents[kk]->mask);
3255
+ if ( st->stack_parent_mask_ptrs[kk] ) {
3256
+ xfree(st->stack_parent_mask_ptrs[kk]);
3257
+ }
3022
3258
  }
3023
3259
  xfree(st->stack_parent_mask_ptrs);
3024
3260
  st->stack_parent_mask_ptrs = NULL;
3025
3261
  }
3026
3262
  st->stack_n_parents = 0;
3027
3263
  if ( st->root ) {
3028
- ca_detach(st->root);
3264
+ /* A write walk wrote into the root's attach buffer, which is the root
3265
+ itself only for an entity: a view root (a transpose, a non-contig
3266
+ block, a view lending no memory) gave a buffer of its own. */
3267
+ CArray *root = st->root;
3029
3268
  st->root = NULL;
3269
+ if ( st->flags & CA_KERNEL_WRITE ) {
3270
+ ca_sync_detach(root);
3271
+ }
3272
+ else {
3273
+ ca_detach(root);
3274
+ }
3030
3275
  } else if ( st->alias_mode == CA_ITER_ALIAS_CONTIG
3031
3276
  || st->alias_mode == CA_ITER_ALIAS_STRIDED ) {
3032
3277
  /* alias path (CONTIG/STRIDED for CAStride family): we attached
@@ -3036,10 +3281,35 @@ ca_iter_state_finish (ca_iter_state *st)
3036
3281
  NONE, scratch_ptr xfree'd above), so no src.detach needed. */
3037
3282
  ca_detach(st->src);
3038
3283
  }
3284
+ if ( st->flags & CA_ITER_FLAG_OWNS_SRC ) {
3285
+ CArray *own = st->src;
3286
+ st->flags &= ~CA_ITER_FLAG_OWNS_SRC;
3287
+ if ( ca_is_object_type(own) ) {
3288
+ ca_gc_release_buffer(own->ptr);
3289
+ }
3290
+ ca_free(own);
3291
+ }
3039
3292
  st->src = NULL;
3040
3293
  st->alias_ptr = NULL;
3041
3294
  }
3042
3295
 
3296
+ /* Finish a walk the way the block macros leave it: a slab of a write walk
3297
+ still handed out -- the body left with `break` -- is written back
3298
+ first. A raise from the write-back is passed on once the walk is
3299
+ finished. */
3300
+ void
3301
+ ca_iter_state_close (ca_iter_state *st)
3302
+ {
3303
+ int tag = 0;
3304
+ if ( st != NULL && st->src != NULL && st->write_dirty ) {
3305
+ rb_protect(ca_iter_finish_sync, (VALUE) st, &tag);
3306
+ }
3307
+ ca_iter_state_finish(st);
3308
+ if ( tag ) {
3309
+ rb_jump_tag(tag);
3310
+ }
3311
+ }
3312
+
3043
3313
  /* ---- Raise-safe entry points ----------------------------------------
3044
3314
  *
3045
3315
  * A walk reads its source, and a source that converts as it is read (a
@@ -3053,10 +3323,9 @@ ca_iter_state_finish (ca_iter_state *st)
3053
3323
  * and ca_iter_state_finish is idempotent (it clears st->src), so the
3054
3324
  * caller's own finish after an ordinary walk stays correct.
3055
3325
  *
3056
- * Not covered: a kernel body that raises. It runs in the caller's
3326
+ * Not covered here: a kernel body that raises. It runs in the caller's
3057
3327
  * frame, between two calls of these functions, where the engine has no
3058
- * hold on it. An object-lane kernel calling back into Ruby is the case
3059
- * to watch.
3328
+ * hold on it. A body that can raise runs through ca_iter_ensure below.
3060
3329
  *
3061
3330
  * init runs protected always -- once per walk, next to a materialise.
3062
3331
  * next_slab / sync_slab are called once per slab, in walks whose whole
@@ -3072,6 +3341,7 @@ ca_iter_state_finish (ca_iter_state *st)
3072
3341
  header. */
3073
3342
  #define CA_ITER_FLAG_WALK_MAY_RAISE 0x80000000u
3074
3343
 
3344
+
3075
3345
  typedef struct {
3076
3346
  ca_iter_state *st;
3077
3347
  CArray *src;
@@ -3105,13 +3375,20 @@ ca_iter_mark_walk_may_raise (ca_iter_state *st)
3105
3375
  }
3106
3376
  }
3107
3377
 
3378
+ /* When body raises, finish st -- and the state it is paired with (see
3379
+ ca_iter_state_init_l2_paired), which is open in the same frame and
3380
+ would otherwise be jumped over. */
3108
3381
  static void
3109
3382
  ca_iter_protect (ca_iter_state *st, VALUE (*body)(VALUE), VALUE arg)
3110
3383
  {
3111
3384
  int tag = 0;
3112
3385
  rb_protect(body, arg, &tag);
3113
3386
  if ( tag ) {
3387
+ ca_iter_state *pair = (ca_iter_state *) st->pair;
3114
3388
  ca_iter_state_finish(st);
3389
+ if ( pair != NULL ) {
3390
+ ca_iter_state_finish(pair);
3391
+ }
3115
3392
  rb_jump_tag(tag);
3116
3393
  }
3117
3394
  }
@@ -3181,6 +3458,9 @@ ca_iter_state_init_l1 (ca_iter_state *st,
3181
3458
  ca_iter_call_t c;
3182
3459
  c.st = st; c.src = src; c.policy = policy;
3183
3460
  c.axes = axes; c.naxes = naxes; c.flags = flags; c.rc = CA_ITER_OK;
3461
+ if ( st != NULL ) {
3462
+ st->pair = NULL; /* read by ca_iter_protect if the init raises */
3463
+ }
3184
3464
  ca_iter_protect(st, ca_iter_call_init_l1, (VALUE) &c);
3185
3465
  if ( c.rc == CA_ITER_OK ) ca_iter_mark_walk_may_raise(st);
3186
3466
  return c.rc;
@@ -3197,11 +3477,32 @@ ca_iter_state_init_l2 (ca_iter_state *st,
3197
3477
  ca_iter_call_t c;
3198
3478
  c.st = st; c.src = src; c.policy = policy;
3199
3479
  c.axes = axes; c.naxes = naxes; c.flags = flags; c.rc = CA_ITER_OK;
3480
+ if ( st != NULL ) {
3481
+ st->pair = NULL; /* read by ca_iter_protect if the init raises */
3482
+ }
3200
3483
  ca_iter_protect(st, ca_iter_call_init_l2, (VALUE) &c);
3201
3484
  if ( c.rc == CA_ITER_OK ) ca_iter_mark_walk_may_raise(st);
3202
3485
  return c.rc;
3203
3486
  }
3204
3487
 
3488
+ /* A slab handed out by a write walk is dirty until sync_slab writes it
3489
+ back; finish writes back one the caller left with `break`. */
3490
+ static int
3491
+ ca_iter_mark_dirty (ca_iter_state *st, int rc)
3492
+ {
3493
+ if ( rc && st != NULL && (st->flags & CA_KERNEL_WRITE) ) {
3494
+ st->write_dirty = 1;
3495
+ }
3496
+ return rc;
3497
+ }
3498
+
3499
+ static VALUE
3500
+ ca_iter_finish_sync (VALUE arg)
3501
+ {
3502
+ ca_iter_state_sync_slab_unprotected((ca_iter_state *) arg);
3503
+ return Qnil;
3504
+ }
3505
+
3205
3506
  int
3206
3507
  ca_iter_state_next_slab (ca_iter_state *st,
3207
3508
  char **out_ptr,
@@ -3210,12 +3511,13 @@ ca_iter_state_next_slab (ca_iter_state *st,
3210
3511
  {
3211
3512
  ca_iter_call_t c;
3212
3513
  if ( st == NULL || !(st->flags & CA_ITER_FLAG_WALK_MAY_RAISE) ) {
3213
- return ca_iter_state_next_slab_unprotected(st, out_ptr, out_mask, out_n);
3514
+ return ca_iter_mark_dirty(st,
3515
+ ca_iter_state_next_slab_unprotected(st, out_ptr, out_mask, out_n));
3214
3516
  }
3215
3517
  c.st = st; c.out_ptr = out_ptr; c.out_mask = out_mask; c.out_n = out_n;
3216
3518
  c.rc = 0;
3217
3519
  ca_iter_protect(st, ca_iter_call_next_slab, (VALUE) &c);
3218
- return c.rc;
3520
+ return ca_iter_mark_dirty(st, c.rc);
3219
3521
  }
3220
3522
 
3221
3523
  int
@@ -3227,13 +3529,14 @@ ca_iter_state_next_slab_strided (ca_iter_state *st,
3227
3529
  {
3228
3530
  ca_iter_call_t c;
3229
3531
  if ( st == NULL || !(st->flags & CA_ITER_FLAG_WALK_MAY_RAISE) ) {
3230
- return ca_iter_state_next_slab_strided_unprotected(st, out_ptr, out_mask,
3231
- out_n, out_stride_bytes);
3532
+ return ca_iter_mark_dirty(st,
3533
+ ca_iter_state_next_slab_strided_unprotected(st, out_ptr, out_mask,
3534
+ out_n, out_stride_bytes));
3232
3535
  }
3233
3536
  c.st = st; c.out_ptr = out_ptr; c.out_mask = out_mask; c.out_n = out_n;
3234
3537
  c.out_stride_bytes = out_stride_bytes; c.rc = 0;
3235
3538
  ca_iter_protect(st, ca_iter_call_next_slab_strided, (VALUE) &c);
3236
- return c.rc;
3539
+ return ca_iter_mark_dirty(st, c.rc);
3237
3540
  }
3238
3541
 
3239
3542
  int
@@ -3243,11 +3546,12 @@ ca_iter_state_next_slab_axes (ca_iter_state *st,
3243
3546
  {
3244
3547
  ca_iter_call_t c;
3245
3548
  if ( st == NULL || !(st->flags & CA_ITER_FLAG_WALK_MAY_RAISE) ) {
3246
- return ca_iter_state_next_slab_axes_unprotected(st, out_ptr, out_mask);
3549
+ return ca_iter_mark_dirty(st,
3550
+ ca_iter_state_next_slab_axes_unprotected(st, out_ptr, out_mask));
3247
3551
  }
3248
3552
  c.st = st; c.out_ptr = out_ptr; c.out_mask = out_mask; c.rc = 0;
3249
3553
  ca_iter_protect(st, ca_iter_call_next_slab_axes, (VALUE) &c);
3250
- return c.rc;
3554
+ return ca_iter_mark_dirty(st, c.rc);
3251
3555
  }
3252
3556
 
3253
3557
  void
@@ -3262,6 +3566,144 @@ ca_iter_state_sync_slab (ca_iter_state *st)
3262
3566
  ca_iter_protect(st, ca_iter_call_sync_slab, (VALUE) &c);
3263
3567
  }
3264
3568
 
3569
+ typedef struct {
3570
+ ca_iter_state *st;
3571
+ CArray *src;
3572
+ CArray *copy;
3573
+ } ca_iter_reopen_t;
3574
+
3575
+ static VALUE
3576
+ ca_iter_reopen_copy (VALUE arg)
3577
+ {
3578
+ ca_iter_reopen_t *r = (ca_iter_reopen_t *) arg;
3579
+ r->copy = ca_copy(r->src);
3580
+ return Qnil;
3581
+ }
3582
+
3583
+ /* A read walk whose source lives in the root a paired write walk writes
3584
+ into would read cells the kernel has already written: an aliased fiber
3585
+ is the written memory itself, and a gathered one is read from the root
3586
+ after earlier fibers were written back. Such a read walk is reopened
3587
+ on a copy of its source, taken before anything is written and owned by
3588
+ the walk. Returns the init's rc; a raise finishes `first` first. */
3589
+ static int
3590
+ ca_iter_reopen_on_copy (ca_iter_state *first)
3591
+ {
3592
+ ca_iter_reopen_t r;
3593
+ ca_iter_call_t c;
3594
+ int8_t axes[CA_RANK_MAX];
3595
+ int8_t naxes = first->naxes;
3596
+ uint32_t flags = first->flags & ~(CA_ITER_FLAG_WALK_MAY_RAISE
3597
+ | CA_ITER_FLAG_OWNS_SRC);
3598
+ ca_slab_policy_t policy = first->policy;
3599
+ int tag = 0;
3600
+
3601
+ memcpy(axes, first->slab_axes_buf, (size_t) naxes);
3602
+ r.st = first; r.src = first->src; r.copy = NULL;
3603
+ rb_protect(ca_iter_reopen_copy, (VALUE) &r, &tag);
3604
+ if ( tag ) {
3605
+ ca_iter_state_finish(first);
3606
+ rb_jump_tag(tag);
3607
+ }
3608
+ if ( ca_is_object_type(r.copy) ) {
3609
+ /* the walk overwrites the cells that held these VALUEs */
3610
+ ca_gc_hold_buffer(r.copy->ptr, r.copy->elements);
3611
+ }
3612
+ ca_iter_state_finish(first);
3613
+
3614
+ c.st = first; c.src = r.copy; c.policy = policy;
3615
+ c.axes = axes; c.naxes = naxes; c.flags = flags; c.rc = CA_ITER_OK;
3616
+ rb_protect(ca_iter_call_init_l2, (VALUE) &c, &tag);
3617
+ if ( tag || c.rc != CA_ITER_OK ) {
3618
+ ca_iter_state_finish(first);
3619
+ if ( ca_is_object_type(r.copy) ) {
3620
+ ca_gc_release_buffer(r.copy->ptr);
3621
+ }
3622
+ ca_free(r.copy);
3623
+ if ( tag ) {
3624
+ rb_jump_tag(tag);
3625
+ }
3626
+ return c.rc;
3627
+ }
3628
+ first->flags |= CA_ITER_FLAG_OWNS_SRC;
3629
+ ca_iter_mark_walk_may_raise(first);
3630
+ return CA_ITER_OK;
3631
+ }
3632
+
3633
+ int
3634
+ ca_iter_state_init_l2_paired (ca_iter_state *st,
3635
+ ca_iter_state *first,
3636
+ struct _CArray *src,
3637
+ ca_slab_policy_t policy,
3638
+ int8_t *axes,
3639
+ int8_t naxes,
3640
+ uint32_t flags)
3641
+ {
3642
+ ca_iter_call_t c;
3643
+ int tag = 0;
3644
+ if ( (flags & CA_KERNEL_WRITE) && first->src != NULL &&
3645
+ ! (first->flags & CA_KERNEL_WRITE) &&
3646
+ policy == CA_SLAB_AXES && first->policy == CA_SLAB_AXES &&
3647
+ ca_shares_root(first->src, src) ) {
3648
+ int rc = ca_iter_reopen_on_copy(first);
3649
+ if ( rc != CA_ITER_OK ) {
3650
+ return rc;
3651
+ }
3652
+ }
3653
+ c.st = st; c.src = src; c.policy = policy;
3654
+ c.axes = axes; c.naxes = naxes; c.flags = flags; c.rc = CA_ITER_OK;
3655
+ rb_protect(ca_iter_call_init_l2, (VALUE) &c, &tag);
3656
+ if ( tag ) {
3657
+ ca_iter_state_finish(st);
3658
+ ca_iter_state_finish(first);
3659
+ rb_jump_tag(tag);
3660
+ }
3661
+ if ( c.rc != CA_ITER_OK ) {
3662
+ ca_iter_state_finish(first);
3663
+ return c.rc;
3664
+ }
3665
+ ca_iter_mark_walk_may_raise(st);
3666
+ st->pair = first;
3667
+ first->pair = st;
3668
+ return c.rc;
3669
+ }
3670
+
3671
+ typedef struct {
3672
+ int32_t n;
3673
+ ca_iter_state *states;
3674
+ } ca_iter_ensure_t;
3675
+
3676
+ /* Finishes the walks in reverse order of their slots, as nested windows
3677
+ close. ca_iter_state_finish only detaches and frees, so it cannot
3678
+ raise in turn. */
3679
+ static VALUE
3680
+ ca_iter_ensure_close (VALUE arg)
3681
+ {
3682
+ ca_iter_ensure_t *e = (ca_iter_ensure_t *) arg;
3683
+ for ( int32_t i = e->n - 1; i >= 0; i-- ) {
3684
+ ca_iter_state_finish(&e->states[i]);
3685
+ }
3686
+ return Qnil;
3687
+ }
3688
+
3689
+ VALUE
3690
+ ca_iter_ensure (int32_t n, ca_iter_state *states,
3691
+ VALUE (*body)(VALUE), VALUE arg)
3692
+ {
3693
+ ca_iter_ensure_t e;
3694
+ if ( n < 0 ) {
3695
+ rb_raise(rb_eArgError, "ca_iter_ensure: negative state count %d", (int) n);
3696
+ }
3697
+ /* Unopened: finish returns at once on a state whose src is NULL, and
3698
+ init clears the whole state before it takes anything. */
3699
+ for ( int32_t i = 0; i < n; i++ ) {
3700
+ states[i].src = NULL;
3701
+ }
3702
+ e.n = n;
3703
+ e.states = states;
3704
+ return rb_ensure(body, arg, ca_iter_ensure_close, (VALUE) &e);
3705
+ }
3706
+
3265
3707
  #ifdef CARRAY_DEV_BUILD
3266
3708
  /* ============================================================
3267
3709
  * smoke surface (dev-only, stripped in release)
@@ -4418,9 +4860,13 @@ rb_caf_slab_bench_sort_copy_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4418
4860
  TypedData_Get_Struct(vout, CArray, &carray_data_type, out);
4419
4861
  slab_axes[0] = (int8_t) axis;
4420
4862
 
4863
+ if ( axis < 0 || axis >= src->ndim ) {
4864
+ rb_raise(rb_eArgError, "caf_slab_bench_sort_copy_f64: axis out of range");
4865
+ }
4421
4866
  ca_size_t fiber_n = src->dim[axis];
4422
4867
  ca_size_t bytes = sizeof(double);
4423
- char *buf = (char *) xmalloc(fiber_n * bytes);
4868
+ volatile VALUE holder; /* the walk can raise while buf is held */
4869
+ char *buf = ALLOCV_N(char, holder, fiber_n * bytes);
4424
4870
 
4425
4871
  CA_FOR_EACH_SLAB_INOUT(st_in, st_out, src, out,
4426
4872
  slab_axes, 1,
@@ -4452,7 +4898,7 @@ rb_caf_slab_bench_sort_copy_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4452
4898
  }
4453
4899
  }
4454
4900
  }
4455
- xfree(buf);
4901
+ ALLOCV_END(holder);
4456
4902
  return vout;
4457
4903
  }
4458
4904
 
@@ -4502,7 +4948,8 @@ rb_caf_bench_per_fiber_xfer_sum_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4502
4948
  }
4503
4949
 
4504
4950
  /* Fiber-sized contig scratch. */
4505
- char *buf = (char *) xmalloc(fiber_n * src->bytes);
4951
+ volatile VALUE holder;
4952
+ char *buf = ALLOCV_N(char, holder, fiber_n * src->bytes);
4506
4953
 
4507
4954
  for ( ca_size_t f = 0; f < total_fibers; f++ ) {
4508
4955
  /* Build region: counts = 1 on all non-axis, fiber_n on axis;
@@ -4530,7 +4977,7 @@ rb_caf_bench_per_fiber_xfer_sum_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4530
4977
  }
4531
4978
  }
4532
4979
 
4533
- xfree(buf);
4980
+ ALLOCV_END(holder);
4534
4981
  return rb_float_new(total);
4535
4982
  }
4536
4983
 
@@ -4539,6 +4986,8 @@ rb_caf_bench_per_fiber_xfer_sum_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4539
4986
  void
4540
4987
  Init_ca_kernel_iterator (void)
4541
4988
  {
4989
+ ki_kept_key = rb_ractor_local_storage_ptr_newkey(&ki_kept_type);
4990
+
4542
4991
  #ifdef CARRAY_DEV_BUILD
4543
4992
  /* ==== smoke surface registrations (dev-only, stripped in release) ====
4544
4993
  * See PROPOSAL_SMOKE_DEV_BUILD_GATE.md. All `t1_smoke_*`, `caf_smoke_*`,