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
@@ -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,14 +667,165 @@ 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. */
556
673
  int
557
- ca_iter_state_init_l1 (ca_iter_state *st,
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
+
693
+ int
694
+ ca_iter_check_init (int rc)
695
+ {
696
+ const char *why;
697
+
698
+ if ( rc == CA_ITER_OK ) return rc;
699
+
700
+ switch ( rc ) {
701
+ case CA_ITER_ERR_NOT_CHEAP:
702
+ why = "the iterator does not recognise this kind of source";
703
+ break;
704
+ case CA_ITER_ERR_POLICY:
705
+ why = "the slab policy or the axes given do not apply to this source";
706
+ break;
707
+ case CA_ITER_ERR_FLAGS:
708
+ why = "this flag combination is not supported for this source";
709
+ break;
710
+ case CA_ITER_ERR_READONLY:
711
+ why = "the array is read-only and the kernel asked to write";
712
+ break;
713
+ case CA_ITER_ERR_MASK:
714
+ why = "the source carries a mask";
715
+ break;
716
+ case CA_ITER_ERR_MASK_NOT_ALLOWED:
717
+ why = "the kernel declared CA_KERNEL_NO_MASK and the source carries a mask";
718
+ break;
719
+ default:
720
+ why = "the request was refused";
721
+ break;
722
+ }
723
+ rb_raise(rb_eRuntimeError, "kernel iterator: %s (rc=%d)", why, rc);
724
+ return rc; /* not reached */
725
+ }
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
+
813
+ static int
814
+ ca_iter_state_init_l1_unprotected (ca_iter_state *st,
558
815
  struct _CArray *src,
559
816
  ca_slab_policy_t policy,
560
817
  int8_t *axes,
561
818
  int8_t naxes,
562
819
  uint32_t flags)
563
820
  {
821
+ /* Leave the state defined before anything can return. Every error
822
+ return below hands control back to a caller that may not look at the
823
+ code, so what it is left holding has to be a state that iterates zero
824
+ times rather than whatever its stack frame happened to contain. */
825
+ if ( st == NULL ) return CA_ITER_ERR_FLAGS;
826
+ memset(st, 0, sizeof(*st));
827
+ if ( src == NULL ) return CA_ITER_ERR_FLAGS;
828
+
564
829
  /* PROPOSAL_CAFACE_PHASE_2 F.2.6 + PROPOSAL_LAZY_MARKER_LIFT Phase 0:
565
830
  storage-identical wrapper strip at entry (= same rationale as init_l2
566
831
  below). Strip before validate_inputs.
@@ -586,7 +851,6 @@ ca_iter_state_init_l1 (ca_iter_state *st,
586
851
 
587
852
  uint8_t src_kind = ca_iter_classify_source(src);
588
853
 
589
- memset(st, 0, sizeof(*st));
590
854
  st->src = src;
591
855
  st->src_kind = src_kind;
592
856
  st->level = 1;
@@ -594,7 +858,6 @@ ca_iter_state_init_l1 (ca_iter_state *st,
594
858
  st->ndim = src->ndim;
595
859
  st->flags = flags;
596
860
  st->bytes = src->bytes;
597
- st->axes = axes;
598
861
  st->naxes = naxes;
599
862
 
600
863
  st->slab_n = src->elements;
@@ -612,11 +875,7 @@ ca_iter_state_init_l1 (ca_iter_state *st,
612
875
  Decouples kernel_iterator from per-view attach/sync lifecycle
613
876
  and inherits xfer reform improvements (transform-fused, etc.)
614
877
  automatically. */
615
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
616
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0 ? st->scratch_cap : 1);
617
- if ( src->elements > 0 ) {
618
- ca_xfer_all(src, st->scratch_ptr, CA_XFER_GET);
619
- }
878
+ ki_gather_view(st, src, flags, 0);
620
879
  st->alias_mode = CA_ITER_ALIAS_NONE; /* scratch-owned, no src.detach */
621
880
  st->alias_ptr = st->scratch_ptr;
622
881
  st->composed_strides[0] = src->bytes;
@@ -677,12 +936,11 @@ ca_iter_state_init_l1 (ca_iter_state *st,
677
936
  if ( ca_func[src->obj_type].attach == ca_window_func.attach ) {
678
937
  bound_fill = ((CAWindow *) src)->fill;
679
938
  }
680
- st->scratch_ptr = ca_axis_dispatch_attach(parent,
681
- st->parent_axis_dims,
682
- raw_descs, raw_ndim,
683
- src->bytes,
684
- st->total_elements,
685
- 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);
686
944
  st->alias_mode = CA_ITER_ALIAS_NONE;
687
945
  st->alias_ptr = st->scratch_ptr;
688
946
 
@@ -712,12 +970,7 @@ ca_iter_state_init_l1 (ca_iter_state *st,
712
970
  to ca_stride_func_copy_data which composes leaf strides up to
713
971
  the root entity and gathers in one pass — no per-intermediate
714
972
  view materialise. */
715
- ca_size_t cap = src->elements * src->bytes;
716
- st->scratch_cap = cap;
717
- st->scratch_ptr = xmalloc(cap > 0 ? cap : 1);
718
- if ( cap > 0 ) {
719
- ca_copy_data(src, st->scratch_ptr);
720
- }
973
+ ki_gather_view(st, src, flags, 1);
721
974
  st->alias_mode = CA_ITER_ALIAS_NONE;
722
975
  st->alias_ptr = st->scratch_ptr;
723
976
  }
@@ -740,14 +993,22 @@ ca_iter_state_init_l1 (ca_iter_state *st,
740
993
  return CA_ITER_OK;
741
994
  }
742
995
 
743
- int
744
- ca_iter_state_init_l2 (ca_iter_state *st,
996
+ static int
997
+ ca_iter_state_init_l2_unprotected (ca_iter_state *st,
745
998
  struct _CArray *src,
746
999
  ca_slab_policy_t policy,
747
1000
  int8_t *axes,
748
1001
  int8_t naxes,
749
1002
  uint32_t flags)
750
1003
  {
1004
+ /* Leave the state defined before anything can return. Every error
1005
+ return below hands control back to a caller that may not look at the
1006
+ code, so what it is left holding has to be a state that iterates zero
1007
+ times rather than whatever its stack frame happened to contain. */
1008
+ if ( st == NULL ) return CA_ITER_ERR_FLAGS;
1009
+ memset(st, 0, sizeof(*st));
1010
+ if ( src == NULL ) return CA_ITER_ERR_FLAGS;
1011
+
751
1012
  /* PROPOSAL_CAFACE_PHASE_2 F.2.6 (= MEMO §3.5 kernel_iterator entry strip)
752
1013
  + PROPOSAL_LAZY_MARKER_LIFT Phase 0.
753
1014
 
@@ -815,7 +1076,7 @@ ca_iter_state_init_l2 (ca_iter_state *st,
815
1076
  and use per-fiber ca_xfer_stride which routes to the view's
816
1077
  X.1 OOB-fused per-region fast path. */
817
1078
  if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1
818
- && raw_ndim == src->ndim ) {
1079
+ && raw_ndim == src->ndim && ca_fast_paths_enabled() ) {
819
1080
  int8_t fiber_ax = axes[0];
820
1081
  ca_size_t row_byte_strides[CA_RANK_MAX];
821
1082
  {
@@ -828,7 +1089,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
828
1089
  if ( ca_iter_should_per_fiber_fused(src, src_kind, fiber_ax,
829
1090
  row_byte_strides[fiber_ax],
830
1091
  flags) ) {
831
- memset(st, 0, sizeof(*st));
832
1092
  st->src = src;
833
1093
  st->src_kind = src_kind;
834
1094
  st->level = 2;
@@ -836,7 +1096,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
836
1096
  st->ndim = src->ndim;
837
1097
  st->flags = flags;
838
1098
  st->bytes = src->bytes;
839
- st->axes = axes;
840
1099
  st->naxes = naxes;
841
1100
 
842
1101
  st->alias_mode = CA_ITER_ALIAS_PER_FIBER_FUSED;
@@ -910,19 +1169,29 @@ ca_iter_state_init_l2 (ca_iter_state *st,
910
1169
  return CA_ITER_ERR_POLICY;
911
1170
  }
912
1171
  int slab_has_non_stride = 0;
1172
+ int slab_has_index = 0;
913
1173
  for ( k = 0; k < src->ndim; k++ ) {
914
1174
  if ( in_slab[k] && raw_descs[k].kind != CA_AXIS_KIND_STRIDE ) {
915
1175
  slab_has_non_stride = 1;
916
- break;
1176
+ if ( raw_descs[k].kind == CA_AXIS_KIND_INDEX ) {
1177
+ slab_has_index = 1;
1178
+ }
917
1179
  }
918
1180
  }
919
- 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 ) {
920
1190
  /* C.1 scope: READ-only. WRITE = future sub-step (C.1c). */
921
1191
  if ( flags & CA_KERNEL_WRITE ) {
922
1192
  return CA_ITER_ERR_FLAGS;
923
1193
  }
924
1194
 
925
- memset(st, 0, sizeof(*st));
926
1195
  st->src = src;
927
1196
  st->src_kind = CA_ITER_SRC_DESCRIPTOR;
928
1197
  st->level = 2;
@@ -930,7 +1199,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
930
1199
  st->ndim = src->ndim;
931
1200
  st->flags = flags;
932
1201
  st->bytes = src->bytes;
933
- st->axes = axes;
934
1202
  st->naxes = naxes;
935
1203
 
936
1204
  CArray *parent = CAVIEW(src)->parent;
@@ -1030,7 +1298,7 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1030
1298
  full-view materialise). */
1031
1299
  ca_size_t slab_bytes = st->slab_elements * src->bytes;
1032
1300
  st->scratch_cap = slab_bytes > 0 ? slab_bytes : 1;
1033
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap);
1301
+ st->scratch_ptr = (char *) ki_data_alloc(src->data_type, st->scratch_cap);
1034
1302
  st->alias_ptr = st->scratch_ptr; /* refilled per slab in next_slab_axes */
1035
1303
 
1036
1304
  /* Specialisation (B-1b, C.1b): if innermost slab axis is STRIDE
@@ -1038,7 +1306,7 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1038
1306
  manual gather path (= outer + non-innermost-slab hoist,
1039
1307
  inner pure STRIDE linear memcpy). Else (innermost INDEX /
1040
1308
  SHIFT-anywhere): use the engine fallback (A). */
1041
- int specialised_eligible = (sp > 0);
1309
+ int specialised_eligible = (sp > 0) && ca_fast_paths_enabled();
1042
1310
  if ( specialised_eligible ) {
1043
1311
  int8_t inner_view_ax = st->slab_axes_buf[sp - 1];
1044
1312
  if ( raw_descs[inner_view_ax].kind != CA_AXIS_KIND_STRIDE ) {
@@ -1090,8 +1358,7 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1090
1358
  }
1091
1359
  }
1092
1360
 
1093
- if ( outer_has_shift ) {
1094
- memset(st, 0, sizeof(*st));
1361
+ if ( outer_has_shift || whole_view ) {
1095
1362
  st->src = src;
1096
1363
  st->src_kind = CA_ITER_SRC_DESCRIPTOR;
1097
1364
  st->level = 2;
@@ -1099,7 +1366,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1099
1366
  st->ndim = src->ndim;
1100
1367
  st->flags = flags;
1101
1368
  st->bytes = src->bytes;
1102
- st->axes = axes;
1103
1369
  st->naxes = naxes;
1104
1370
 
1105
1371
  CArray *parent = CAVIEW(src)->parent;
@@ -1113,10 +1379,12 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1113
1379
  bound_fill = ((CAWindow *) src)->fill;
1114
1380
  }
1115
1381
  st->scratch_cap = src->elements * src->bytes;
1116
- st->scratch_ptr = ca_axis_dispatch_attach(parent, raw_pdims,
1117
- local_descs, src->ndim,
1118
- src->bytes, src->elements,
1119
- 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;
1120
1388
  st->alias_mode = CA_ITER_ALIAS_NONE;
1121
1389
  st->alias_ptr = st->scratch_ptr;
1122
1390
 
@@ -1185,7 +1453,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1185
1453
  return CA_ITER_OK;
1186
1454
  }
1187
1455
 
1188
- memset(st, 0, sizeof(*st));
1189
1456
  st->src = src;
1190
1457
  st->src_kind = CA_ITER_SRC_DESCRIPTOR; /* Phase B alias path */
1191
1458
  st->level = 2;
@@ -1193,7 +1460,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1193
1460
  st->ndim = src->ndim;
1194
1461
  st->flags = flags;
1195
1462
  st->bytes = src->bytes;
1196
- st->axes = axes;
1197
1463
  st->naxes = naxes;
1198
1464
 
1199
1465
  /* Parent row-major byte strides for offset computation. */
@@ -1333,7 +1599,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1333
1599
  in_slab[ax] = 1;
1334
1600
  }
1335
1601
 
1336
- memset(st, 0, sizeof(*st));
1337
1602
  st->src = src;
1338
1603
  st->src_kind = CA_ITER_SRC_ATTACH;
1339
1604
  st->level = 2;
@@ -1341,7 +1606,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1341
1606
  st->ndim = src->ndim;
1342
1607
  st->flags = flags;
1343
1608
  st->bytes = src->bytes;
1344
- st->axes = axes;
1345
1609
  st->naxes = naxes;
1346
1610
 
1347
1611
  /* Row-major byte / element strides on the attached view buffer.
@@ -1386,7 +1650,9 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1386
1650
  k_axis..N-2]; either way outer_idx[m] = parent axis m). */
1387
1651
  if ( ca_func[src->obj_type].attach == ca_stack_func.attach
1388
1652
  && !ca_has_mask(src)
1389
- && naxes == 1 && axes[0] == ((CAStack *) src)->k_axis ) {
1653
+ && !(flags & CA_KERNEL_WRITE)
1654
+ && naxes == 1 && axes[0] == ((CAStack *) src)->k_axis
1655
+ && ca_fast_paths_enabled() ) {
1390
1656
  CAStack *stack = (CAStack *) src;
1391
1657
  int8_t parent_ndim = src->ndim - 1;
1392
1658
  int8_t sp = 0, op = 0;
@@ -1435,12 +1701,15 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1435
1701
  guarantees uniform shape (MEMO §3.2) so one set of parent
1436
1702
  native byte strides covers all K (= parents[0]->dim is
1437
1703
  canonical). */
1438
- 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;
1439
1707
  st->stack_parent_ptrs =
1440
1708
  (char **) xmalloc(stack->n_parents * sizeof(char *));
1441
1709
  for ( int32_t kk = 0; kk < stack->n_parents; kk++ ) {
1442
- ca_attach(stack->parents[kk]);
1710
+ ca_attach(stack->parents[kk]); /* window: finish detaches the counted parents */
1443
1711
  st->stack_parent_ptrs[kk] = (char *) stack->parents[kk]->ptr;
1712
+ st->stack_n_parents = kk + 1;
1444
1713
  }
1445
1714
  {
1446
1715
  ca_size_t s = src->bytes;
@@ -1452,7 +1721,7 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1452
1721
 
1453
1722
  /* Slab-sized scratch (= K * bytes for slab_axes == [0]). */
1454
1723
  st->scratch_cap = (ca_size_t) st->slab_elements * src->bytes;
1455
- 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);
1456
1725
 
1457
1726
  /* Tile cache (pilot/castack-axis0-loop-interchange): refill TILE
1458
1727
  fibers per K parent reads, serve next TILE-1 fibers from L1d.
@@ -1467,7 +1736,7 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1467
1736
  st->stack_tile_cap = tile;
1468
1737
  st->stack_tile_pos = 0;
1469
1738
  st->stack_tile_have = 0; /* force refill on first call */
1470
- st->stack_tile_cache = (char *) xmalloc(K * tile * bytes);
1739
+ st->stack_tile_cache = (char *) ki_data_alloc(src->data_type, K * tile * bytes);
1471
1740
  }
1472
1741
 
1473
1742
  st->alias_mode = CA_ITER_ALIAS_STACK;
@@ -1501,7 +1770,9 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1501
1770
  s - 1); only the K stack axis (s == k_axis) has no parent
1502
1771
  counterpart. */
1503
1772
  if ( ca_func[src->obj_type].attach == ca_stack_func.attach
1504
- && naxes >= 1 && !in_slab[((CAStack *) src)->k_axis] ) {
1773
+ && !(flags & CA_KERNEL_WRITE)
1774
+ && naxes >= 1 && !in_slab[((CAStack *) src)->k_axis]
1775
+ && ca_fast_paths_enabled() ) {
1505
1776
  CAStack *stack = (CAStack *) src;
1506
1777
  int8_t k_axis = stack->k_axis;
1507
1778
  int8_t parent_ndim = src->ndim - 1;
@@ -1582,25 +1853,42 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1582
1853
 
1583
1854
  /* Attach K parents + cache ptr aliases. Symmetric with rev3
1584
1855
  Case B; for entity parents attach is O(1). */
1585
- 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;
1586
1859
  st->stack_parent_ptrs =
1587
1860
  (char **) xmalloc(stack->n_parents * sizeof(char *));
1588
1861
  for ( int32_t kk = 0; kk < stack->n_parents; kk++ ) {
1589
- ca_attach(stack->parents[kk]);
1862
+ ca_attach(stack->parents[kk]); /* window: finish detaches the counted parents */
1590
1863
  st->stack_parent_ptrs[kk] = (char *) stack->parents[kk]->ptr;
1864
+ st->stack_n_parents = kk + 1;
1591
1865
  }
1592
1866
 
1593
1867
  /* If CAStack carries mask (= horizontal propagation already
1594
- applied at create_mask), attach K parent masks + cache ptr
1595
- aliases. next_slab_axes aliases parent->mask[k]->ptr +
1596
- 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. */
1597
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. */
1598
1880
  st->stack_parent_mask_ptrs =
1599
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 *));
1600
1884
  for ( int32_t kk = 0; kk < stack->n_parents; kk++ ) {
1601
- ca_attach(stack->parents[kk]->mask);
1602
- st->stack_parent_mask_ptrs[kk] =
1603
- (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);
1604
1892
  }
1605
1893
  } else {
1606
1894
  st->stack_parent_mask_ptrs = NULL;
@@ -1620,7 +1908,8 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1620
1908
  is called on demand in next_slab_axes). Predicate is stubbed
1621
1909
  in F.6.1 (returns 0); F.6.2+ enables specific source kinds. */
1622
1910
  int per_fiber_fused = 0;
1623
- if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1 ) {
1911
+ if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1
1912
+ && ca_fast_paths_enabled() ) {
1624
1913
  int8_t fiber_ax = axes[0];
1625
1914
  ca_size_t fiber_ax_stride = row_byte_strides[fiber_ax];
1626
1915
  if ( ca_iter_should_per_fiber_fused(src, src_kind, fiber_ax,
@@ -1638,11 +1927,7 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1638
1927
  if ( !per_fiber_fused ) {
1639
1928
  /* 2026-05-31 refactor: iter-owns scratch via ca_xfer_all (was
1640
1929
  ca_attach(src) + alias src->ptr). */
1641
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
1642
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0 ? st->scratch_cap : 1);
1643
- if ( src->elements > 0 ) {
1644
- ca_xfer_all(src, st->scratch_ptr, CA_XFER_GET);
1645
- }
1930
+ ki_gather_view(st, src, flags, 0);
1646
1931
  st->alias_mode = CA_ITER_ALIAS_NONE;
1647
1932
  st->alias_ptr = st->scratch_ptr;
1648
1933
  }
@@ -1721,7 +2006,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1721
2006
  / CATile / CARoll). Same shape as L1 SRC_ATTACH: iterator-owns
1722
2007
  scratch + xfer_all GET/PUT. Yield as one 1-D strided slab. */
1723
2008
  if ( src_kind == CA_ITER_SRC_ATTACH ) {
1724
- memset(st, 0, sizeof(*st));
1725
2009
  st->src = src;
1726
2010
  st->src_kind = CA_ITER_SRC_ATTACH;
1727
2011
  st->level = 2;
@@ -1729,14 +2013,9 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1729
2013
  st->ndim = 1; /* logical 1-D L2 layout */
1730
2014
  st->flags = flags;
1731
2015
  st->bytes = src->bytes;
1732
- st->axes = axes;
1733
2016
  st->naxes = naxes;
1734
2017
 
1735
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
1736
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0 ? st->scratch_cap : 1);
1737
- if ( src->elements > 0 ) {
1738
- ca_xfer_all(src, st->scratch_ptr, CA_XFER_GET);
1739
- }
2018
+ ki_gather_view(st, src, flags, 0);
1740
2019
  st->alias_mode = CA_ITER_ALIAS_NONE;
1741
2020
  st->alias_ptr = st->scratch_ptr;
1742
2021
  st->composed_strides[0] = src->bytes;
@@ -1773,7 +2052,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1773
2052
  int8_t nd = raw_ndim;
1774
2053
  int8_t k;
1775
2054
 
1776
- memset(st, 0, sizeof(*st));
1777
2055
  st->src = src;
1778
2056
  st->src_kind = CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE;
1779
2057
  st->level = 2;
@@ -1781,7 +2059,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1781
2059
  st->ndim = nd;
1782
2060
  st->flags = flags;
1783
2061
  st->bytes = src->bytes;
1784
- st->axes = axes;
1785
2062
  st->naxes = naxes;
1786
2063
 
1787
2064
  /* Persist descs / parent_axis_dims in state; build parent row-major
@@ -1864,7 +2141,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1864
2141
  /* raw_descs / raw_pdims / raw_ndim were populated by route_source
1865
2142
  above so we skip the local describe_axes call. Kept locals
1866
2143
  named raw_* to match the original code. */
1867
- memset(st, 0, sizeof(*st));
1868
2144
  st->src = src;
1869
2145
  st->src_kind = CA_ITER_SRC_DESCRIPTOR;
1870
2146
  st->level = 2;
@@ -1872,7 +2148,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1872
2148
  st->ndim = src->ndim;
1873
2149
  st->flags = flags;
1874
2150
  st->bytes = src->bytes;
1875
- st->axes = axes;
1876
2151
  st->naxes = naxes;
1877
2152
 
1878
2153
  memcpy(st->parent_axis_dims, raw_pdims, raw_ndim * sizeof(ca_size_t));
@@ -1899,12 +2174,11 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1899
2174
  if ( ca_func[src->obj_type].attach == ca_window_func.attach ) {
1900
2175
  bound_fill = ((CAWindow *) src)->fill;
1901
2176
  }
1902
- st->scratch_ptr = ca_axis_dispatch_attach(parent,
1903
- st->parent_axis_dims,
1904
- raw_descs, raw_ndim,
1905
- src->bytes,
1906
- st->total_elements,
1907
- 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);
1908
2182
 
1909
2183
  /* Lay out as a single 1-D L2 strided slab: ptr = scratch,
1910
2184
  n = total_elements, stride = bytes. next_slab_strided reads
@@ -1938,7 +2212,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1938
2212
  }
1939
2213
 
1940
2214
  /* === CAStride / entity L2 path === */
1941
- memset(st, 0, sizeof(*st));
1942
2215
  st->src = src;
1943
2216
  st->src_kind = CA_ITER_SRC_CASTRIDE;
1944
2217
  st->level = 2;
@@ -1946,7 +2219,6 @@ ca_iter_state_init_l2 (ca_iter_state *st,
1946
2219
  st->ndim = src->ndim;
1947
2220
  st->flags = flags;
1948
2221
  st->bytes = src->bytes;
1949
- st->axes = axes;
1950
2222
  st->naxes = naxes;
1951
2223
 
1952
2224
  /* Compute composed strides + base. For entity / CAStride contig we
@@ -2062,18 +2334,13 @@ ca_iter_state_init_l2 (ca_iter_state *st,
2062
2334
  }
2063
2335
 
2064
2336
  if ( use_strided ) {
2065
- st->root = root;
2066
- ca_attach(root);
2337
+ ca_attach(root); /* window: finish detaches st->root */
2338
+ st->root = root; /* recorded once attached */
2067
2339
  st->alias_mode = CA_ITER_ALIAS_STRIDED;
2068
2340
  st->alias_ptr = (char *) root->ptr;
2069
2341
  } else if ( use_view_scratch ) {
2070
2342
  st->root = NULL;
2071
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
2072
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0
2073
- ? st->scratch_cap : 1);
2074
- if ( src->elements > 0 ) {
2075
- ca_copy_data(src, st->scratch_ptr);
2076
- }
2343
+ ki_gather_view(st, src, flags, 1);
2077
2344
  st->alias_mode = CA_ITER_ALIAS_NONE;
2078
2345
  st->alias_ptr = st->scratch_ptr;
2079
2346
  } else {
@@ -2116,18 +2383,13 @@ ca_iter_state_init_l2 (ca_iter_state *st,
2116
2383
  st->chunk_size = st->slab_n;
2117
2384
 
2118
2385
  if ( use_strided ) {
2119
- st->root = root;
2120
- ca_attach(root);
2386
+ ca_attach(root); /* window: finish detaches st->root */
2387
+ st->root = root; /* recorded once attached */
2121
2388
  st->alias_mode = CA_ITER_ALIAS_STRIDED;
2122
2389
  st->alias_ptr = (char *) root->ptr;
2123
2390
  } else if ( use_view_scratch ) {
2124
2391
  st->root = NULL;
2125
- st->scratch_cap = (ca_size_t) src->elements * src->bytes;
2126
- st->scratch_ptr = (char *) xmalloc(st->scratch_cap > 0
2127
- ? st->scratch_cap : 1);
2128
- if ( src->elements > 0 ) {
2129
- ca_copy_data(src, st->scratch_ptr);
2130
- }
2392
+ ki_gather_view(st, src, flags, 1);
2131
2393
  st->alias_mode = CA_ITER_ALIAS_NONE;
2132
2394
  st->alias_ptr = st->scratch_ptr;
2133
2395
  } else {
@@ -2136,11 +2398,22 @@ ca_iter_state_init_l2 (ca_iter_state *st,
2136
2398
  st->alias_mode = CA_ITER_ALIAS_CONTIG;
2137
2399
  st->alias_ptr = (char *) src->ptr;
2138
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
+ }
2139
2412
  return CA_ITER_OK;
2140
2413
  }
2141
2414
 
2142
- int
2143
- ca_iter_state_next_slab (ca_iter_state *st,
2415
+ static int
2416
+ ca_iter_state_next_slab_unprotected (ca_iter_state *st,
2144
2417
  char **out_ptr,
2145
2418
  boolean8_t **out_mask,
2146
2419
  ca_size_t *out_n)
@@ -2162,8 +2435,8 @@ ca_iter_state_next_slab (ca_iter_state *st,
2162
2435
  return 1;
2163
2436
  }
2164
2437
 
2165
- int
2166
- ca_iter_state_next_slab_strided (ca_iter_state *st,
2438
+ static int
2439
+ ca_iter_state_next_slab_strided_unprotected (ca_iter_state *st,
2167
2440
  char **out_ptr,
2168
2441
  boolean8_t **out_mask,
2169
2442
  ca_size_t *out_n,
@@ -2231,8 +2504,57 @@ ca_iter_state_next_slab_strided (ca_iter_state *st,
2231
2504
  return 1;
2232
2505
  }
2233
2506
 
2234
- int
2235
- ca_iter_state_next_slab_axes (ca_iter_state *st,
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
+
2556
+ static int
2557
+ ca_iter_state_next_slab_axes_unprotected (ca_iter_state *st,
2236
2558
  char **out_ptr,
2237
2559
  boolean8_t **out_mask)
2238
2560
  {
@@ -2330,7 +2652,11 @@ ca_iter_state_next_slab_axes (ca_iter_state *st,
2330
2652
  }
2331
2653
 
2332
2654
  if ( out_ptr ) *out_ptr = st->scratch_ptr;
2333
- 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
+ }
2334
2660
 
2335
2661
  st->slabs_emitted += 1;
2336
2662
  for ( int8_t m = st->outer_ndim - 1; m >= 0; m-- ) {
@@ -2366,11 +2692,15 @@ ca_iter_state_next_slab_axes (ca_iter_state *st,
2366
2692
  mask_off += st->outer_idx[m] * st->stack_parent_mask_strides[parent_ax];
2367
2693
  }
2368
2694
 
2369
- 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
+ }
2370
2699
  if ( out_mask ) {
2371
- *out_mask = st->stack_parent_mask_ptrs
2372
- ? st->stack_parent_mask_ptrs[k] + mask_off
2373
- : 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]);
2374
2704
  }
2375
2705
 
2376
2706
  st->slabs_emitted += 1;
@@ -2521,7 +2851,11 @@ ca_iter_state_next_slab_axes (ca_iter_state *st,
2521
2851
  }
2522
2852
 
2523
2853
  if ( out_ptr ) *out_ptr = st->scratch_ptr;
2524
- 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
+ }
2525
2859
 
2526
2860
  st->slabs_emitted += 1;
2527
2861
  /* Advance outer_idx row-major (innermost outer axis ticks first). */
@@ -2582,8 +2916,8 @@ ca_iter_state_next_slab_axes (ca_iter_state *st,
2582
2916
  ca_size_t counts[CA_RANK_MAX];
2583
2917
 
2584
2918
  if ( st->fiber_data_scratch_cap < need ) {
2585
- if ( st->fiber_data_scratch ) xfree(st->fiber_data_scratch);
2586
- 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);
2587
2921
  st->fiber_data_scratch_cap = need;
2588
2922
  }
2589
2923
 
@@ -2661,72 +2995,14 @@ ca_iter_state_next_slab_axes (ca_iter_state *st,
2661
2995
  Capture last_data_off BEFORE the outer_idx advance below; sync_slab
2662
2996
  consumes it for WRITE scatter. See header field doc + PROPOSAL
2663
2997
  §4.3.2 hazard comment in sync_slab. */
2664
- char *yield_ptr;
2665
- if ( (st->flags & CA_KERNEL_FIBER_CONTIG)
2666
- && st->naxes == 1 && st->slab_ndim == 1 ) {
2667
- ca_size_t n = st->slab_dims[0];
2668
- ca_size_t data_step = st->slab_strides[0];
2669
- ca_size_t bytes = st->bytes;
2670
- char *src_data = st->alias_ptr + data_off;
2671
-
2672
- st->last_data_off = data_off;
2673
-
2674
- if ( data_step == (ca_size_t) bytes ) {
2675
- /* Fast path: fiber is already contig (= innermost-axis or stride
2676
- coincidentally == bytes). No gather needed. */
2677
- yield_ptr = src_data;
2678
- } else {
2679
- /* Per-fiber gather via the typed-store inline helper
2680
- (ca_iter_substrate.h). For bytes in {1,2,4,8} this uses a
2681
- compiler-vectorize-friendly `*dp++ = v; sp += step` loop;
2682
- other sizes fall back to per-element memcpy. Lazy-alloc
2683
- scratch sized to max fiber bytes (slab_dims[0] constant per
2684
- walk → single alloc in practice). */
2685
- ca_size_t need = n * bytes;
2686
- if ( st->fiber_data_scratch_cap < need ) {
2687
- if ( st->fiber_data_scratch ) xfree(st->fiber_data_scratch);
2688
- st->fiber_data_scratch = (char *) xmalloc(need);
2689
- st->fiber_data_scratch_cap = need;
2690
- }
2691
- ca_stride_gather_run(st->fiber_data_scratch, src_data,
2692
- bytes, n, data_step);
2693
- yield_ptr = st->fiber_data_scratch;
2694
- }
2695
- } else {
2696
- yield_ptr = st->alias_ptr + data_off;
2697
- }
2698
-
2699
- /* PROPOSAL_FIBER_DELIVERY F.1b: per-fiber contig mask delivery.
2700
- Symmetric to F.1a data path above. When the source has a mask
2701
- (= alias_mask != NULL) and the fiber's mask is not innermost-contig
2702
- (= slab_mask_strides[0] != 1), gather it into fiber_mask_scratch
2703
- so the author can write m[i] without stride math. Mask is
2704
- read-only here (= L2 WRITE never propagates to mask state), so no
2705
- scatter is needed in sync_slab. */
2706
- boolean8_t *yield_mask;
2707
- if ( (st->flags & CA_KERNEL_FIBER_CONTIG)
2708
- && st->naxes == 1 && st->slab_ndim == 1
2709
- && st->alias_mask != NULL ) {
2710
- ca_size_t n = st->slab_dims[0];
2711
- ca_size_t mask_step = st->slab_mask_strides[0];
2712
- boolean8_t *src_mask = st->alias_mask + mask_off;
2713
-
2714
- if ( mask_step == 1 ) {
2715
- yield_mask = src_mask;
2716
- } else {
2717
- if ( st->fiber_mask_scratch_cap < (ca_size_t) n ) {
2718
- if ( st->fiber_mask_scratch ) xfree(st->fiber_mask_scratch);
2719
- st->fiber_mask_scratch = (boolean8_t *) xmalloc(n);
2720
- st->fiber_mask_scratch_cap = n;
2721
- }
2722
- for ( ca_size_t i = 0; i < n; i++ ) {
2723
- st->fiber_mask_scratch[i] = src_mask[i * mask_step];
2724
- }
2725
- yield_mask = st->fiber_mask_scratch;
2726
- }
2727
- } else {
2728
- yield_mask = st->alias_mask ? st->alias_mask + mask_off : NULL;
2729
- }
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]);
2730
3006
 
2731
3007
  if ( out_ptr ) *out_ptr = yield_ptr;
2732
3008
  if ( out_mask ) *out_mask = yield_mask;
@@ -2742,38 +3018,22 @@ ca_iter_state_next_slab_axes (ca_iter_state *st,
2742
3018
  return 1;
2743
3019
  }
2744
3020
 
2745
- void
2746
- ca_iter_state_sync_slab (ca_iter_state *st)
3021
+ static void
3022
+ ca_iter_state_sync_slab_unprotected (ca_iter_state *st)
2747
3023
  {
2748
3024
  /* READ walk: nothing to sync. */
2749
3025
  if ( st == NULL || !(st->flags & CA_KERNEL_WRITE) ) return;
2750
3026
 
2751
- /* PROPOSAL_CASTACK_LOOP_INTERCHANGE Vector A rev2: STACK path is
2752
- READ-only in initial scope. Kernel writes into scratch would not
2753
- be valid to scatter back via xfer_all PUT (= scratch is slab-sized
2754
- not whole-view), so the SRC_ATTACH PUT below would be a semantic
2755
- mismatch. Per-slab scatter via direct per-parent memcpy is a
2756
- future extension once a WRITE-using kernel materialises. */
3027
+ /* Neither stack mode can be reached with WRITE set: both are declined
3028
+ at init, which sends a write destination down the generic SRC_ATTACH
3029
+ path instead. They cannot serve a write themselves -- ALIAS_STACK
3030
+ hands out a tile cache that finish frees, and STACK_OUTER_K aliases
3031
+ parents[k]->ptr, which is the attach buffer rather than the parent's
3032
+ own memory whenever the parent is a view. Scattering per parent
3033
+ would let the fast paths carry writes too; until then this branch is
3034
+ just the assertion that they are not asked to. */
2757
3035
  if ( st->alias_mode == CA_ITER_ALIAS_STACK
2758
3036
  || st->alias_mode == CA_ITER_ALIAS_STACK_OUTER_K ) {
2759
- /* STACK_OUTER_K (P.2 Case A) is READ-only scope: slabs are direct
2760
- aliases into parents[k]->ptr, kernel WRITE would scatter into
2761
- parent memory which is out of scope (see proposal R1). Skip
2762
- sync. */
2763
- st->write_dirty = 0;
2764
- return;
2765
- }
2766
-
2767
- /* SRC_ATTACH path (step 9 + 2026-05-31 refactor): kernel wrote into
2768
- iterator-owned scratch (= scratch_ptr). Push back via xfer_all PUT
2769
- which routes through the view's xfer_all slot -- handles CAFake
2770
- (cast back), CAByteSwap (swap back), CABitfield/CABitarray (bit
2771
- pack back), CAReduce (broadcast across reduce window). Inherits
2772
- transform-fused / partial materialise / etc. automatically. */
2773
- if ( st->src_kind == CA_ITER_SRC_ATTACH ) {
2774
- if ( st->src->elements > 0 ) {
2775
- ca_xfer_all(st->src, st->scratch_ptr, CA_XFER_PUT);
2776
- }
2777
3037
  st->write_dirty = 0;
2778
3038
  return;
2779
3039
  }
@@ -2852,12 +3112,27 @@ ca_iter_state_sync_slab (ca_iter_state *st)
2852
3112
  ca_stride_scatter_run(dst, st->fiber_data_scratch,
2853
3113
  bytes, n, data_step);
2854
3114
  }
2855
- /* Fall through to any subsequent src_kind scatter (= harmless: for
2856
- the alias paths reached here, scratch_ptr is NULL and the switch
2857
- below early-returns). But for SRC_DESCRIPTOR / SRC_ATTACH that
2858
- use the per-slab materialise path, fiber_data_scratch stays NULL
2859
- (those paths use scratch_ptr and the PER_SLAB(_HOIST) yield), so
2860
- this block does not fire. */
3115
+ /* Fall through. The gather above is not specific to a src_kind --
3116
+ it fires for any source whose fiber is not innermost-contig -- so
3117
+ for SRC_ATTACH this has just written the fiber back into the
3118
+ whole-view scratch, and the push-back below carries it to the
3119
+ source. For the alias paths scratch_ptr is NULL and the switch
3120
+ below early-returns. */
3121
+ }
3122
+
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.
3126
+
3127
+ This has to stay below the two per-fiber blocks: consulting
3128
+ src_kind first would push back the whole-view buffer while the
3129
+ fiber the author actually wrote sat unscattered in
3130
+ fiber_data_scratch, and would push back a NULL one for
3131
+ PER_FIBER_FUSED, which owns no whole-view buffer at all. */
3132
+ if ( st->src_kind == CA_ITER_SRC_ATTACH ) {
3133
+ ki_scratch_push_changed(st);
3134
+ st->write_dirty = 0;
3135
+ return;
2861
3136
  }
2862
3137
 
2863
3138
  /* alias path: kernel wrote through alias_ptr into parent directly
@@ -2874,11 +3149,10 @@ ca_iter_state_sync_slab (ca_iter_state *st)
2874
3149
  case CA_ITER_SRC_CASTRIDE:
2875
3150
  /* Two producers of CAStride + scratch: L1 non-contig, and the L2
2876
3151
  non-entity-root path (init_l2's use_view_scratch). Both gathered
2877
- with ca_copy_data into a view row-major buffer, so both scatter
2878
- back the same way — ca_sync_data routes through the view's
2879
- xfer_all(PUT), which asks the root for the region it owns rather
2880
- than writing a whole-root materialise back. */
2881
- 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);
2882
3156
  break;
2883
3157
 
2884
3158
  case CA_ITER_SRC_DESCRIPTOR: {
@@ -2911,12 +3185,17 @@ ca_iter_state_sync_slab (ca_iter_state *st)
2911
3185
  st->write_dirty = 0;
2912
3186
  }
2913
3187
 
3188
+ static VALUE ca_iter_finish_sync (VALUE arg);
3189
+
2914
3190
  void
2915
3191
  ca_iter_state_finish (ca_iter_state *st)
2916
3192
  {
2917
3193
  if ( st == NULL || st->src == NULL ) {
2918
3194
  return;
2919
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;
2920
3199
  /* composed_strides is inline — no free needed. outer_idx is heap
2921
3200
  for L2 multi-d sources (NULL on L1 paths and on L2 0/1-d). */
2922
3201
  if ( st->outer_idx ) {
@@ -2933,7 +3212,7 @@ ca_iter_state_finish (ca_iter_state *st)
2933
3212
  ca_detach.
2934
3213
  - else (alias paths): src was attached at init, detach. */
2935
3214
  if ( st->scratch_ptr ) {
2936
- xfree(st->scratch_ptr);
3215
+ ki_data_free(st->scratch_ptr, st->scratch_cap);
2937
3216
  st->scratch_ptr = NULL;
2938
3217
  st->scratch_cap = 0;
2939
3218
  }
@@ -2943,7 +3222,7 @@ ca_iter_state_finish (ca_iter_state *st)
2943
3222
  }
2944
3223
  /* PROPOSAL_FIBER_DELIVERY F.1a/F.1b: per-fiber scratch lifecycle. */
2945
3224
  if ( st->fiber_data_scratch ) {
2946
- xfree(st->fiber_data_scratch);
3225
+ ki_data_free(st->fiber_data_scratch, st->fiber_data_scratch_cap);
2947
3226
  st->fiber_data_scratch = NULL;
2948
3227
  st->fiber_data_scratch_cap = 0;
2949
3228
  }
@@ -2965,24 +3244,34 @@ ca_iter_state_finish (ca_iter_state *st)
2965
3244
  }
2966
3245
  /* pilot/castack-axis0-loop-interchange: free tile cache. */
2967
3246
  if ( st->stack_tile_cache ) {
2968
- xfree(st->stack_tile_cache);
3247
+ ki_data_free(st->stack_tile_cache, 0);
2969
3248
  st->stack_tile_cache = NULL;
2970
3249
  st->stack_tile_cap = 0;
2971
3250
  st->stack_tile_pos = 0;
2972
3251
  st->stack_tile_have = 0;
2973
3252
  }
2974
3253
  if ( st->stack_parent_mask_ptrs ) {
2975
- CAStack *stack = (CAStack *) st->src;
2976
3254
  for ( int32_t kk = 0; kk < st->stack_n_parents; kk++ ) {
2977
- ca_detach(stack->parents[kk]->mask);
3255
+ if ( st->stack_parent_mask_ptrs[kk] ) {
3256
+ xfree(st->stack_parent_mask_ptrs[kk]);
3257
+ }
2978
3258
  }
2979
3259
  xfree(st->stack_parent_mask_ptrs);
2980
3260
  st->stack_parent_mask_ptrs = NULL;
2981
3261
  }
2982
3262
  st->stack_n_parents = 0;
2983
3263
  if ( st->root ) {
2984
- 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;
2985
3268
  st->root = NULL;
3269
+ if ( st->flags & CA_KERNEL_WRITE ) {
3270
+ ca_sync_detach(root);
3271
+ }
3272
+ else {
3273
+ ca_detach(root);
3274
+ }
2986
3275
  } else if ( st->alias_mode == CA_ITER_ALIAS_CONTIG
2987
3276
  || st->alias_mode == CA_ITER_ALIAS_STRIDED ) {
2988
3277
  /* alias path (CONTIG/STRIDED for CAStride family): we attached
@@ -2992,10 +3281,429 @@ ca_iter_state_finish (ca_iter_state *st)
2992
3281
  NONE, scratch_ptr xfree'd above), so no src.detach needed. */
2993
3282
  ca_detach(st->src);
2994
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
+ }
2995
3292
  st->src = NULL;
2996
3293
  st->alias_ptr = NULL;
2997
3294
  }
2998
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
+
3313
+ /* ---- Raise-safe entry points ----------------------------------------
3314
+ *
3315
+ * A walk reads its source, and a source that converts as it is read (a
3316
+ * lazy view over an object array, say) raises when it meets a cell it
3317
+ * cannot convert. The caller's loop then never reaches
3318
+ * ca_iter_state_finish -- the block macros put it in the `for`
3319
+ * increment, which a raise jumps over, and it is past the raise in
3320
+ * hand-written walks too. So the walk finishes itself on the way out.
3321
+ *
3322
+ * Every entry point that can raise is called through rb_protect here,
3323
+ * and ca_iter_state_finish is idempotent (it clears st->src), so the
3324
+ * caller's own finish after an ordinary walk stays correct.
3325
+ *
3326
+ * Not covered here: a kernel body that raises. It runs in the caller's
3327
+ * frame, between two calls of these functions, where the engine has no
3328
+ * hold on it. A body that can raise runs through ca_iter_ensure below.
3329
+ *
3330
+ * init runs protected always -- once per walk, next to a materialise.
3331
+ * next_slab / sync_slab are called once per slab, in walks whose whole
3332
+ * per-fiber cost can be a few nanoseconds, so they are protected only
3333
+ * when this walk can raise at all: when it gathers each fiber through
3334
+ * the source's own transfer slots, or when it writes scratch back
3335
+ * through them. Every other shape hands out pointers into memory that
3336
+ * init already materialised, and moves it with memcpy. */
3337
+
3338
+ /* Private flags bit, kept in ca_iter_state.flags rather than a field of
3339
+ its own: the state is stack-allocated by ext authors through the block
3340
+ macros, so growing it would overrun states built against an older
3341
+ header. */
3342
+ #define CA_ITER_FLAG_WALK_MAY_RAISE 0x80000000u
3343
+
3344
+
3345
+ typedef struct {
3346
+ ca_iter_state *st;
3347
+ CArray *src;
3348
+ ca_slab_policy_t policy;
3349
+ int8_t *axes;
3350
+ int8_t naxes;
3351
+ uint32_t flags;
3352
+ char **out_ptr;
3353
+ boolean8_t **out_mask;
3354
+ ca_size_t *out_n;
3355
+ ca_size_t *out_stride_bytes;
3356
+ int rc;
3357
+ } ca_iter_call_t;
3358
+
3359
+ /* Whether next_slab / sync_slab on this walk need the protection. */
3360
+ static void
3361
+ ca_iter_mark_walk_may_raise (ca_iter_state *st)
3362
+ {
3363
+ if ( st == NULL ) return;
3364
+ if ( st->alias_mode == CA_ITER_ALIAS_PER_FIBER_FUSED ) {
3365
+ /* gathers each fiber through the source's xfer_stride slot */
3366
+ st->flags |= CA_ITER_FLAG_WALK_MAY_RAISE;
3367
+ }
3368
+ else if ( (st->flags & CA_KERNEL_WRITE)
3369
+ && (st->scratch_ptr != NULL
3370
+ || st->src_kind == CA_ITER_SRC_ATTACH) ) {
3371
+ /* writes back out of iterator-owned scratch, through the source's
3372
+ xfer_all / sync_data slot. A write into aliased memory needs no
3373
+ push-back at all: sync_slab returns at once. */
3374
+ st->flags |= CA_ITER_FLAG_WALK_MAY_RAISE;
3375
+ }
3376
+ }
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. */
3381
+ static void
3382
+ ca_iter_protect (ca_iter_state *st, VALUE (*body)(VALUE), VALUE arg)
3383
+ {
3384
+ int tag = 0;
3385
+ rb_protect(body, arg, &tag);
3386
+ if ( tag ) {
3387
+ ca_iter_state *pair = (ca_iter_state *) st->pair;
3388
+ ca_iter_state_finish(st);
3389
+ if ( pair != NULL ) {
3390
+ ca_iter_state_finish(pair);
3391
+ }
3392
+ rb_jump_tag(tag);
3393
+ }
3394
+ }
3395
+
3396
+ static VALUE
3397
+ ca_iter_call_init_l1 (VALUE arg)
3398
+ {
3399
+ ca_iter_call_t *c = (ca_iter_call_t *) arg;
3400
+ c->rc = ca_iter_state_init_l1_unprotected(c->st, c->src, c->policy,
3401
+ c->axes, c->naxes, c->flags);
3402
+ return Qnil;
3403
+ }
3404
+
3405
+ static VALUE
3406
+ ca_iter_call_init_l2 (VALUE arg)
3407
+ {
3408
+ ca_iter_call_t *c = (ca_iter_call_t *) arg;
3409
+ c->rc = ca_iter_state_init_l2_unprotected(c->st, c->src, c->policy,
3410
+ c->axes, c->naxes, c->flags);
3411
+ return Qnil;
3412
+ }
3413
+
3414
+ static VALUE
3415
+ ca_iter_call_next_slab (VALUE arg)
3416
+ {
3417
+ ca_iter_call_t *c = (ca_iter_call_t *) arg;
3418
+ c->rc = ca_iter_state_next_slab_unprotected(c->st, c->out_ptr,
3419
+ c->out_mask, c->out_n);
3420
+ return Qnil;
3421
+ }
3422
+
3423
+ static VALUE
3424
+ ca_iter_call_next_slab_strided (VALUE arg)
3425
+ {
3426
+ ca_iter_call_t *c = (ca_iter_call_t *) arg;
3427
+ c->rc = ca_iter_state_next_slab_strided_unprotected(c->st, c->out_ptr,
3428
+ c->out_mask, c->out_n,
3429
+ c->out_stride_bytes);
3430
+ return Qnil;
3431
+ }
3432
+
3433
+ static VALUE
3434
+ ca_iter_call_next_slab_axes (VALUE arg)
3435
+ {
3436
+ ca_iter_call_t *c = (ca_iter_call_t *) arg;
3437
+ c->rc = ca_iter_state_next_slab_axes_unprotected(c->st, c->out_ptr,
3438
+ c->out_mask);
3439
+ return Qnil;
3440
+ }
3441
+
3442
+ static VALUE
3443
+ ca_iter_call_sync_slab (VALUE arg)
3444
+ {
3445
+ ca_iter_call_t *c = (ca_iter_call_t *) arg;
3446
+ ca_iter_state_sync_slab_unprotected(c->st);
3447
+ return Qnil;
3448
+ }
3449
+
3450
+ int
3451
+ ca_iter_state_init_l1 (ca_iter_state *st,
3452
+ struct _CArray *src,
3453
+ ca_slab_policy_t policy,
3454
+ int8_t *axes,
3455
+ int8_t naxes,
3456
+ uint32_t flags)
3457
+ {
3458
+ ca_iter_call_t c;
3459
+ c.st = st; c.src = src; c.policy = policy;
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
+ }
3464
+ ca_iter_protect(st, ca_iter_call_init_l1, (VALUE) &c);
3465
+ if ( c.rc == CA_ITER_OK ) ca_iter_mark_walk_may_raise(st);
3466
+ return c.rc;
3467
+ }
3468
+
3469
+ int
3470
+ ca_iter_state_init_l2 (ca_iter_state *st,
3471
+ struct _CArray *src,
3472
+ ca_slab_policy_t policy,
3473
+ int8_t *axes,
3474
+ int8_t naxes,
3475
+ uint32_t flags)
3476
+ {
3477
+ ca_iter_call_t c;
3478
+ c.st = st; c.src = src; c.policy = policy;
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
+ }
3483
+ ca_iter_protect(st, ca_iter_call_init_l2, (VALUE) &c);
3484
+ if ( c.rc == CA_ITER_OK ) ca_iter_mark_walk_may_raise(st);
3485
+ return c.rc;
3486
+ }
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
+
3506
+ int
3507
+ ca_iter_state_next_slab (ca_iter_state *st,
3508
+ char **out_ptr,
3509
+ boolean8_t **out_mask,
3510
+ ca_size_t *out_n)
3511
+ {
3512
+ ca_iter_call_t c;
3513
+ if ( st == NULL || !(st->flags & CA_ITER_FLAG_WALK_MAY_RAISE) ) {
3514
+ return ca_iter_mark_dirty(st,
3515
+ ca_iter_state_next_slab_unprotected(st, out_ptr, out_mask, out_n));
3516
+ }
3517
+ c.st = st; c.out_ptr = out_ptr; c.out_mask = out_mask; c.out_n = out_n;
3518
+ c.rc = 0;
3519
+ ca_iter_protect(st, ca_iter_call_next_slab, (VALUE) &c);
3520
+ return ca_iter_mark_dirty(st, c.rc);
3521
+ }
3522
+
3523
+ int
3524
+ ca_iter_state_next_slab_strided (ca_iter_state *st,
3525
+ char **out_ptr,
3526
+ boolean8_t **out_mask,
3527
+ ca_size_t *out_n,
3528
+ ca_size_t *out_stride_bytes)
3529
+ {
3530
+ ca_iter_call_t c;
3531
+ if ( st == NULL || !(st->flags & CA_ITER_FLAG_WALK_MAY_RAISE) ) {
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));
3535
+ }
3536
+ c.st = st; c.out_ptr = out_ptr; c.out_mask = out_mask; c.out_n = out_n;
3537
+ c.out_stride_bytes = out_stride_bytes; c.rc = 0;
3538
+ ca_iter_protect(st, ca_iter_call_next_slab_strided, (VALUE) &c);
3539
+ return ca_iter_mark_dirty(st, c.rc);
3540
+ }
3541
+
3542
+ int
3543
+ ca_iter_state_next_slab_axes (ca_iter_state *st,
3544
+ char **out_ptr,
3545
+ boolean8_t **out_mask)
3546
+ {
3547
+ ca_iter_call_t c;
3548
+ if ( st == NULL || !(st->flags & CA_ITER_FLAG_WALK_MAY_RAISE) ) {
3549
+ return ca_iter_mark_dirty(st,
3550
+ ca_iter_state_next_slab_axes_unprotected(st, out_ptr, out_mask));
3551
+ }
3552
+ c.st = st; c.out_ptr = out_ptr; c.out_mask = out_mask; c.rc = 0;
3553
+ ca_iter_protect(st, ca_iter_call_next_slab_axes, (VALUE) &c);
3554
+ return ca_iter_mark_dirty(st, c.rc);
3555
+ }
3556
+
3557
+ void
3558
+ ca_iter_state_sync_slab (ca_iter_state *st)
3559
+ {
3560
+ ca_iter_call_t c;
3561
+ if ( st == NULL || !(st->flags & CA_ITER_FLAG_WALK_MAY_RAISE) ) {
3562
+ ca_iter_state_sync_slab_unprotected(st);
3563
+ return;
3564
+ }
3565
+ c.st = st;
3566
+ ca_iter_protect(st, ca_iter_call_sync_slab, (VALUE) &c);
3567
+ }
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
+
2999
3707
  #ifdef CARRAY_DEV_BUILD
3000
3708
  /* ============================================================
3001
3709
  * smoke surface (dev-only, stripped in release)
@@ -4152,9 +4860,13 @@ rb_caf_slab_bench_sort_copy_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4152
4860
  TypedData_Get_Struct(vout, CArray, &carray_data_type, out);
4153
4861
  slab_axes[0] = (int8_t) axis;
4154
4862
 
4863
+ if ( axis < 0 || axis >= src->ndim ) {
4864
+ rb_raise(rb_eArgError, "caf_slab_bench_sort_copy_f64: axis out of range");
4865
+ }
4155
4866
  ca_size_t fiber_n = src->dim[axis];
4156
4867
  ca_size_t bytes = sizeof(double);
4157
- 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);
4158
4870
 
4159
4871
  CA_FOR_EACH_SLAB_INOUT(st_in, st_out, src, out,
4160
4872
  slab_axes, 1,
@@ -4186,7 +4898,7 @@ rb_caf_slab_bench_sort_copy_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4186
4898
  }
4187
4899
  }
4188
4900
  }
4189
- xfree(buf);
4901
+ ALLOCV_END(holder);
4190
4902
  return vout;
4191
4903
  }
4192
4904
 
@@ -4236,7 +4948,8 @@ rb_caf_bench_per_fiber_xfer_sum_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4236
4948
  }
4237
4949
 
4238
4950
  /* Fiber-sized contig scratch. */
4239
- char *buf = (char *) xmalloc(fiber_n * src->bytes);
4951
+ volatile VALUE holder;
4952
+ char *buf = ALLOCV_N(char, holder, fiber_n * src->bytes);
4240
4953
 
4241
4954
  for ( ca_size_t f = 0; f < total_fibers; f++ ) {
4242
4955
  /* Build region: counts = 1 on all non-axis, fiber_n on axis;
@@ -4264,7 +4977,7 @@ rb_caf_bench_per_fiber_xfer_sum_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4264
4977
  }
4265
4978
  }
4266
4979
 
4267
- xfree(buf);
4980
+ ALLOCV_END(holder);
4268
4981
  return rb_float_new(total);
4269
4982
  }
4270
4983
 
@@ -4273,6 +4986,8 @@ rb_caf_bench_per_fiber_xfer_sum_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4273
4986
  void
4274
4987
  Init_ca_kernel_iterator (void)
4275
4988
  {
4989
+ ki_kept_key = rb_ractor_local_storage_ptr_newkey(&ki_kept_type);
4990
+
4276
4991
  #ifdef CARRAY_DEV_BUILD
4277
4992
  /* ==== smoke surface registrations (dev-only, stripped in release) ====
4278
4993
  * See PROPOSAL_SMOKE_DEV_BUILD_GATE.md. All `t1_smoke_*`, `caf_smoke_*`,