carray 2.0.1 → 3.0.1

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 (386) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -25
  3. data/CHANGELOG.md +338 -0
  4. data/{NEWS.md → CHANGELOG.v1.md} +3 -0
  5. data/LICENSE +1 -1
  6. data/README.md +120 -36
  7. data/carray.gemspec +32 -30
  8. data/ext/ca_array_pool.c +91 -0
  9. data/ext/ca_axis_descriptor.h +186 -0
  10. data/ext/ca_axis_dispatch.c +924 -0
  11. data/ext/ca_axis_group.c +1244 -0
  12. data/ext/ca_bincmp_dispatch.c +76 -0
  13. data/ext/ca_bincmp_dispatch.h +85 -0
  14. data/ext/ca_binop_dispatch.c +124 -0
  15. data/ext/ca_binop_dispatch.h +152 -0
  16. data/ext/ca_categorical_iterator.c +1375 -0
  17. data/ext/ca_compare.c +94 -0
  18. data/ext/ca_compare.h +26 -0
  19. data/ext/ca_composite_dispatch.c +414 -0
  20. data/ext/ca_composite_dispatch.h +116 -0
  21. data/ext/ca_for_buffer.h +96 -0
  22. data/ext/ca_for_each_element.h +239 -0
  23. data/ext/ca_group_iter.c +304 -0
  24. data/ext/ca_iter_substrate.h +325 -0
  25. data/ext/ca_kernel_iterator.c +4367 -0
  26. data/ext/ca_kernel_iterator.h +2596 -0
  27. data/ext/ca_moncmp_dispatch.c +37 -0
  28. data/ext/ca_moncmp_dispatch.h +62 -0
  29. data/ext/ca_monop_dispatch.c +200 -0
  30. data/ext/ca_monop_dispatch.h +235 -0
  31. data/ext/ca_obj_array.c +355 -359
  32. data/ext/ca_obj_bincmp.c +839 -0
  33. data/ext/ca_obj_binop.c +948 -0
  34. data/ext/ca_obj_bitarray.c +369 -164
  35. data/ext/ca_obj_bitfield.c +294 -234
  36. data/ext/ca_obj_block.c +189 -711
  37. data/ext/ca_obj_byte_swap.c +766 -0
  38. data/ext/ca_obj_const_string.c +967 -0
  39. data/ext/ca_obj_face.c +750 -0
  40. data/ext/ca_obj_face.h +279 -0
  41. data/ext/ca_obj_fake.c +239 -100
  42. data/ext/ca_obj_farray.c +54 -441
  43. data/ext/ca_obj_field.c +82 -529
  44. data/ext/ca_obj_fixlen_string.c +308 -0
  45. data/ext/ca_obj_grid.c +866 -440
  46. data/ext/ca_obj_meld.c +1039 -0
  47. data/ext/ca_obj_moncmp.c +588 -0
  48. data/ext/ca_obj_monop.c +1123 -0
  49. data/ext/ca_obj_object.c +866 -296
  50. data/ext/ca_obj_record.c +470 -0
  51. data/ext/ca_obj_reduce.c +97 -82
  52. data/ext/ca_obj_refer.c +593 -459
  53. data/ext/ca_obj_remap.c +475 -0
  54. data/ext/ca_obj_repeat.c +92 -477
  55. data/ext/ca_obj_roll.c +624 -0
  56. data/ext/ca_obj_select.c +344 -296
  57. data/ext/ca_obj_select_axis.c +1306 -0
  58. data/ext/ca_obj_shift.c +231 -793
  59. data/ext/ca_obj_source.c +78 -0
  60. data/ext/ca_obj_stack.c +1173 -0
  61. data/ext/ca_obj_stride.c +2584 -0
  62. data/ext/ca_obj_string.c +270 -0
  63. data/ext/ca_obj_tile.c +622 -0
  64. data/ext/ca_obj_time.c +548 -0
  65. data/ext/ca_obj_timedelta.c +437 -0
  66. data/ext/ca_obj_transpose.c +62 -516
  67. data/ext/ca_obj_triop.c +785 -0
  68. data/ext/ca_obj_window.c +1202 -565
  69. data/ext/ca_op_byte_swap.c +175 -0
  70. data/ext/ca_op_cmplx64.h +123 -0
  71. data/ext/ca_op_ipower.c +316 -0
  72. data/ext/ca_op_powi.h +88 -0
  73. data/ext/ca_sort_kernels.h +132 -0
  74. data/ext/ca_sweep_engine.c +473 -0
  75. data/ext/ca_sweep_engine.h +166 -0
  76. data/ext/ca_transform_common.c +235 -0
  77. data/ext/ca_triop_dispatch.c +55 -0
  78. data/ext/ca_triop_dispatch.h +62 -0
  79. data/ext/carray.h +810 -420
  80. data/ext/carray_access.c +873 -731
  81. data/ext/carray_attribute.c +98 -329
  82. data/ext/carray_bincount.c +255 -0
  83. data/ext/carray_broadcast.c +376 -0
  84. data/ext/carray_build_flags.h +3 -0
  85. data/ext/carray_call_cfunc.c +2897 -874
  86. data/ext/carray_call_cfunc.h +313 -0
  87. data/ext/carray_cast.c +1264 -315
  88. data/ext/carray_cast_func.rb +81 -40
  89. data/ext/carray_class.c +53 -63
  90. data/ext/carray_config.h +28 -0
  91. data/ext/carray_conversion.c +350 -346
  92. data/ext/carray_copy.c +168 -270
  93. data/ext/carray_core.c +1396 -206
  94. data/ext/carray_count.c +312 -0
  95. data/ext/carray_data_type.c +43 -19
  96. data/ext/carray_element.c +585 -213
  97. data/ext/carray_factorize.c +2542 -0
  98. data/ext/carray_generate.c +230 -559
  99. data/ext/carray_histogram.c +490 -0
  100. data/ext/carray_hold.c +228 -0
  101. data/ext/carray_index_classifier.c +1021 -0
  102. data/ext/carray_index_classifier.h +27 -0
  103. data/ext/carray_internal.h +136 -0
  104. data/ext/carray_kernels_bincmp.c +4446 -0
  105. data/ext/carray_kernels_binop.c +11001 -0
  106. data/ext/carray_kernels_init.c +1131 -0
  107. data/ext/carray_kernels_map.c +3467 -0
  108. data/ext/carray_kernels_moncmp.c +2097 -0
  109. data/ext/carray_kernels_monop.c +18313 -0
  110. data/ext/carray_kernels_reduce_aggregate.c +25837 -0
  111. data/ext/carray_kernels_reduce_boolean.c +330 -0
  112. data/ext/carray_kernels_reduce_cumulative.c +14593 -0
  113. data/ext/carray_kernels_reduce_extreme.c +16948 -0
  114. data/ext/carray_kernels_reduce_variance.c +3910 -0
  115. data/ext/carray_kernels_scan.c +3693 -0
  116. data/ext/carray_kernels_search.c +32138 -0
  117. data/ext/carray_kernels_sort.c +10626 -0
  118. data/ext/carray_kernels_triop.c +1392 -0
  119. data/ext/carray_lazy.c +737 -0
  120. data/ext/carray_loop.c +88 -200
  121. data/ext/carray_mask.c +853 -158
  122. data/ext/carray_math_kernel.h +120 -0
  123. data/ext/carray_mathfunc.c +10 -241
  124. data/ext/carray_median_percentile.c +1257 -0
  125. data/ext/carray_memory_view.c +1650 -0
  126. data/ext/carray_operator.c +1525 -320
  127. data/ext/carray_order.c +664 -1394
  128. data/ext/carray_partition.c +416 -0
  129. data/ext/carray_random.c +518 -0
  130. data/ext/carray_scatter.c +357 -0
  131. data/ext/carray_slab.c +1219 -0
  132. data/ext/carray_slab.h +84 -0
  133. data/ext/carray_sort.c +829 -0
  134. data/ext/carray_sort_kernel.c +620 -0
  135. data/ext/carray_struct.c +695 -0
  136. data/ext/carray_test.c +343 -229
  137. data/ext/carray_undef.c +34 -17
  138. data/ext/carray_utils.c +175 -74
  139. data/ext/extconf.rb +234 -55
  140. data/ext/mk_call_cfunc.rb +671 -0
  141. data/ext/mkkernel.rb +9096 -0
  142. data/ext/ruby_carray.c +211 -108
  143. data/ext/version.h +4 -14
  144. data/ext/version.rb +5 -13
  145. data/lib/carray/arrow_tensor.rb +401 -0
  146. data/lib/carray/attribute.rb +166 -0
  147. data/lib/carray/autoload_carray.rb +239 -0
  148. data/lib/carray/autoload_method_extension.rb +45 -0
  149. data/lib/carray/axis_group.rb +711 -0
  150. data/lib/carray/basics.rb +481 -0
  151. data/lib/carray/bincount_nd.rb +358 -0
  152. data/lib/carray/block_iterator.rb +614 -0
  153. data/lib/carray/boolean_reduce.rb +109 -0
  154. data/lib/carray/categorical.rb +561 -0
  155. data/lib/carray/categorical_iterator.rb +1084 -0
  156. data/lib/carray/complex.rb +150 -0
  157. data/lib/carray/conditional.rb +216 -0
  158. data/lib/carray/const_string.rb +228 -0
  159. data/lib/carray/construct.rb +160 -328
  160. data/lib/carray/core_extensions.rb +297 -0
  161. data/lib/carray/data_type_extension.rb +250 -0
  162. data/lib/carray/fixlen_string.rb +95 -0
  163. data/lib/carray/frame/concat.rb +132 -0
  164. data/lib/carray/frame/convert.rb +95 -0
  165. data/lib/carray/frame/csv_parser.rb +211 -0
  166. data/lib/carray/frame/frame.rb +642 -0
  167. data/lib/carray/frame/group.rb +186 -0
  168. data/lib/carray/frame/io.rb +321 -0
  169. data/lib/carray/frame/join.rb +248 -0
  170. data/lib/carray/frame/records.rb +99 -0
  171. data/lib/carray/frame/sort.rb +113 -0
  172. data/lib/carray/frame/verbs.rb +316 -0
  173. data/lib/carray/frame.rb +16 -0
  174. data/lib/carray/fuse_source.rb +123 -0
  175. data/lib/carray/fusion.rb +218 -0
  176. data/lib/carray/histogram.rb +512 -0
  177. data/lib/carray/inspect.rb +37 -26
  178. data/lib/carray/iterator.rb +58 -349
  179. data/lib/carray/lazy.rb +941 -0
  180. data/lib/carray/mask_gap_fill.rb +200 -0
  181. data/lib/carray/math.rb +78 -342
  182. data/lib/carray/meld_reduce.rb +289 -0
  183. data/lib/carray/methods/align_addr.rb +116 -0
  184. data/lib/carray/methods/bin.rb +128 -0
  185. data/lib/carray/methods/bincount.rb +87 -0
  186. data/lib/carray/methods/bit_string.rb +92 -0
  187. data/lib/carray/methods/broadcast.rb +63 -0
  188. data/lib/carray/methods/choose.rb +39 -0
  189. data/lib/carray/methods/composition.rb +280 -0
  190. data/lib/carray/methods/gather_nd.rb +206 -0
  191. data/lib/carray/methods/index.rb +39 -0
  192. data/lib/carray/methods/insert_block.rb +99 -0
  193. data/lib/carray/methods/is_in.rb +141 -0
  194. data/lib/carray/methods/join.rb +90 -0
  195. data/lib/carray/methods/locate_addr.rb +52 -0
  196. data/lib/carray/methods/mask_duplicates.rb +41 -0
  197. data/lib/carray/methods/meshgrid.rb +90 -0
  198. data/lib/carray/methods/mode.rb +126 -0
  199. data/lib/carray/methods/nunique.rb +46 -0
  200. data/lib/carray/methods/resize.rb +56 -0
  201. data/lib/carray/methods/snap.rb +161 -0
  202. data/lib/carray/methods/string_format.rb +57 -0
  203. data/lib/carray/methods/unique.rb +47 -0
  204. data/lib/carray/methods/value_counts.rb +71 -0
  205. data/lib/carray/mkmf.rb +124 -101
  206. data/lib/carray/runtime.rb +89 -0
  207. data/lib/carray/serialize.rb +478 -167
  208. data/lib/carray/slab_iterator.rb +305 -0
  209. data/lib/carray/stack.rb +291 -0
  210. data/lib/carray/string.rb +56 -180
  211. data/lib/carray/string_operation_extension.rb +289 -0
  212. data/lib/carray/struct.rb +335 -323
  213. data/lib/carray/struct_builder.rb +697 -0
  214. data/lib/carray/table.rb +41 -2
  215. data/lib/carray/time.rb +2654 -38
  216. data/lib/carray/window_iterator.rb +927 -0
  217. data/lib/carray.rb +55 -57
  218. data/yard-stubs/ca_obj_array.rb +385 -0
  219. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  220. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  221. data/yard-stubs/ca_obj_block.rb +73 -0
  222. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  223. data/yard-stubs/ca_obj_fake.rb +31 -0
  224. data/yard-stubs/ca_obj_farray.rb +32 -0
  225. data/yard-stubs/ca_obj_field.rb +45 -0
  226. data/yard-stubs/ca_obj_grid.rb +35 -0
  227. data/yard-stubs/ca_obj_refer.rb +72 -0
  228. data/yard-stubs/ca_obj_roll.rb +45 -0
  229. data/yard-stubs/ca_obj_shift.rb +43 -0
  230. data/yard-stubs/ca_obj_stride.rb +181 -0
  231. data/yard-stubs/ca_obj_tile.rb +29 -0
  232. data/yard-stubs/ca_obj_transpose.rb +40 -0
  233. data/yard-stubs/ca_obj_window.rb +49 -0
  234. data/yard-stubs/carray_access.rb +131 -0
  235. data/yard-stubs/carray_attribute.rb +246 -0
  236. data/yard-stubs/carray_broadcast.rb +37 -0
  237. data/yard-stubs/carray_cast.rb +489 -0
  238. data/yard-stubs/carray_class.rb +65 -0
  239. data/yard-stubs/carray_conversion.rb +76 -0
  240. data/yard-stubs/carray_copy.rb +79 -0
  241. data/yard-stubs/carray_core.rb +114 -0
  242. data/yard-stubs/carray_count.rb +79 -0
  243. data/yard-stubs/carray_element.rb +108 -0
  244. data/yard-stubs/carray_generate.rb +66 -0
  245. data/yard-stubs/carray_lazy.rb +23 -0
  246. data/yard-stubs/carray_loop.rb +140 -0
  247. data/yard-stubs/carray_mask.rb +259 -0
  248. data/yard-stubs/carray_math.rb +132 -0
  249. data/yard-stubs/carray_mathfunc.rb +45 -0
  250. data/yard-stubs/carray_median_percentile.rb +89 -0
  251. data/yard-stubs/carray_memory_view.rb +163 -0
  252. data/yard-stubs/carray_order.rb +312 -0
  253. data/yard-stubs/carray_random.rb +89 -0
  254. data/yard-stubs/carray_scatter.rb +106 -0
  255. data/yard-stubs/carray_slab.rb +57 -0
  256. data/yard-stubs/carray_sort.rb +163 -0
  257. data/yard-stubs/carray_test.rb +85 -0
  258. data/yard-stubs/carray_undef.rb +64 -0
  259. data/yard-stubs/carray_utils.rb +97 -0
  260. data/yard-stubs/ruby_carray.rb +193 -0
  261. metadata +220 -138
  262. data/Rakefile +0 -51
  263. data/TODO.md +0 -18
  264. data/ext/ca_iter_block.c +0 -257
  265. data/ext/ca_iter_dimension.c +0 -299
  266. data/ext/ca_iter_window.c +0 -214
  267. data/ext/ca_obj_mapping.c +0 -644
  268. data/ext/ca_obj_unbound_repeat.c +0 -529
  269. data/ext/carray_iterator.c +0 -641
  270. data/ext/carray_math.rb +0 -850
  271. data/ext/carray_numeric.c +0 -259
  272. data/ext/carray_sort_addr.c +0 -254
  273. data/ext/carray_stat.c +0 -2100
  274. data/ext/carray_stat_proc.rb +0 -1999
  275. data/ext/mkmath.rb +0 -741
  276. data/ext/ruby_ccomplex.c +0 -509
  277. data/ext/ruby_float_func.c +0 -86
  278. data/lib/carray/array.rb +0 -8
  279. data/lib/carray/autoload/autoload_base.rb +0 -19
  280. data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
  281. data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
  282. data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
  283. data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
  284. data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
  285. data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
  286. data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
  287. data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
  288. data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
  289. data/lib/carray/autoload/autoload_gem_random.rb +0 -8
  290. data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
  291. data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
  292. data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
  293. data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
  294. data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
  295. data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
  296. data/lib/carray/autoload/autoload_object_link.rb +0 -1
  297. data/lib/carray/autoload/autoload_object_pack.rb +0 -2
  298. data/lib/carray/autoload.rb +0 -141
  299. data/lib/carray/basic.rb +0 -191
  300. data/lib/carray/broadcast.rb +0 -101
  301. data/lib/carray/compose.rb +0 -315
  302. data/lib/carray/convert.rb +0 -115
  303. data/lib/carray/info.rb +0 -110
  304. data/lib/carray/io/imagemagick.rb +0 -235
  305. data/lib/carray/mask.rb +0 -102
  306. data/lib/carray/math/histogram.rb +0 -177
  307. data/lib/carray/math/recurrence.rb +0 -93
  308. data/lib/carray/object/ca_obj_iterator.rb +0 -50
  309. data/lib/carray/object/ca_obj_link.rb +0 -50
  310. data/lib/carray/object/ca_obj_pack.rb +0 -99
  311. data/lib/carray/obsolete.rb +0 -256
  312. data/lib/carray/ordering.rb +0 -181
  313. data/lib/carray/testing.rb +0 -51
  314. data/lib/carray/transform.rb +0 -109
  315. data/mailmap +0 -1
  316. data/misc/Methods.ja.md +0 -182
  317. data/misc/NOTE +0 -51
  318. data/spec/Classes/CABitfield_spec.rb +0 -58
  319. data/spec/Classes/CABlockIterator_spec.rb +0 -114
  320. data/spec/Classes/CABlock_spec.rb +0 -205
  321. data/spec/Classes/CAField_spec.rb +0 -39
  322. data/spec/Classes/CAGrid_spec.rb +0 -75
  323. data/spec/Classes/CAMap_spec.rb +0 -0
  324. data/spec/Classes/CAMapping_spec.rb +0 -105
  325. data/spec/Classes/CAObject_attribute_spec.rb +0 -33
  326. data/spec/Classes/CAObject_spec.rb +0 -33
  327. data/spec/Classes/CARefer_spec.rb +0 -93
  328. data/spec/Classes/CARepeat_spec.rb +0 -65
  329. data/spec/Classes/CASelect_spec.rb +0 -22
  330. data/spec/Classes/CAShift_spec.rb +0 -16
  331. data/spec/Classes/CAStruct_spec.rb +0 -71
  332. data/spec/Classes/CATranspose_spec.rb +0 -60
  333. data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
  334. data/spec/Classes/CAWindow_spec.rb +0 -54
  335. data/spec/Classes/CAWrap_spec.rb +0 -8
  336. data/spec/Classes/CArray_spec.rb +0 -184
  337. data/spec/Classes/CScalar_spec.rb +0 -55
  338. data/spec/Features/feature_130_spec.rb +0 -19
  339. data/spec/Features/feature_attributes_spec.rb +0 -280
  340. data/spec/Features/feature_boolean_spec.rb +0 -98
  341. data/spec/Features/feature_broadcast.rb +0 -116
  342. data/spec/Features/feature_cast_function.rb +0 -19
  343. data/spec/Features/feature_cast_spec.rb +0 -33
  344. data/spec/Features/feature_class_spec.rb +0 -84
  345. data/spec/Features/feature_complex_spec.rb +0 -42
  346. data/spec/Features/feature_composite_spec.rb +0 -124
  347. data/spec/Features/feature_convert_spec.rb +0 -46
  348. data/spec/Features/feature_copy_spec.rb +0 -123
  349. data/spec/Features/feature_creation_spec.rb +0 -84
  350. data/spec/Features/feature_element_spec.rb +0 -144
  351. data/spec/Features/feature_extream_spec.rb +0 -54
  352. data/spec/Features/feature_generate_spec.rb +0 -74
  353. data/spec/Features/feature_index_spec.rb +0 -69
  354. data/spec/Features/feature_mask_spec.rb +0 -580
  355. data/spec/Features/feature_math_spec.rb +0 -97
  356. data/spec/Features/feature_order_spec.rb +0 -146
  357. data/spec/Features/feature_ref_store_spec.rb +0 -209
  358. data/spec/Features/feature_serialization_spec.rb +0 -125
  359. data/spec/Features/feature_stat_spec.rb +0 -397
  360. data/spec/Features/feature_virtual_spec.rb +0 -48
  361. data/spec/Features/method_eq_spec.rb +0 -81
  362. data/spec/Features/method_is_nan_spec.rb +0 -12
  363. data/spec/Features/method_map_spec.rb +0 -54
  364. data/spec/Features/method_max_with.rb +0 -20
  365. data/spec/Features/method_min_with.rb +0 -19
  366. data/spec/Features/method_ne_spec.rb +0 -18
  367. data/spec/Features/method_project_spec.rb +0 -188
  368. data/spec/Features/method_ref_spec.rb +0 -27
  369. data/spec/Features/method_round_spec.rb +0 -11
  370. data/spec/Features/method_s_linspace_spec.rb +0 -48
  371. data/spec/Features/method_s_span_spec.rb +0 -14
  372. data/spec/Features/method_seq_spec.rb +0 -47
  373. data/spec/Features/method_sort_with.rb +0 -43
  374. data/spec/Features/method_sorted_with.rb +0 -29
  375. data/spec/Features/method_span_spec.rb +0 -42
  376. data/spec/Features/method_wrap_readonly_spec.rb +0 -43
  377. data/spec/UnitTest/test_CAVirtual.rb +0 -214
  378. data/spec/spec_all.rb +0 -10
  379. data/utils/ca_ase.rb +0 -21
  380. data/utils/ca_methods.rb +0 -15
  381. data/utils/cast_checker.rb +0 -30
  382. data/utils/convert_test.rb +0 -73
  383. data/utils/extract_yard.rb +0 -22
  384. data/utils/guess_shape.rb +0 -76
  385. data/utils/monkey_patch_methods.rb +0 -62
  386. data/utils/remove_resource_fork.sh +0 -5
@@ -0,0 +1,4367 @@
1
+ /* ---------------------------------------------------------------------------
2
+
3
+ T1 kernel_iterator MVP — Phase 1 step 1 + step 2 + step 3 implementation.
4
+
5
+ Scope after step 3 (PROPOSAL_T1_KERNEL_ITERATOR.md §10.3):
6
+ - ca_iter_state struct + init / next_slab / next_slab_strided /
7
+ finish state machine
8
+ - CA_SLAB_WHOLE policy only (step 5+ adds AXES / FREE)
9
+ - level == 1 (L1, contig kernel):
10
+ - entity / CAStride contig: alias path (single slab, alias_ptr
11
+ = parent->ptr, stride implicit = bytes)
12
+ - CAStride family non-contig: scratch path (ca_copy_data
13
+ compose-fold gather into a malloc'd buffer)
14
+ - other sources: CA_ITER_ERR_NOT_CHEAP
15
+ - level == 2 (L2, strided kernel):
16
+ - entity / CAStride contig: alias path (single slab,
17
+ stride_bytes = bytes)
18
+ - CAStride family non-contig: alias_strided path — no scratch,
19
+ per-outer-prefix yield with native inner stride_bytes
20
+ - other sources: CA_ITER_ERR_NOT_CHEAP
21
+ - READ-only (flags == 0). WRITE = step 4, NO_MASK = step 7.
22
+
23
+ ---------------------------------------------------------------------------- */
24
+
25
+ #include "carray.h"
26
+ #include "carray_internal.h" /* per-obj_type view constructors */
27
+ #include "ca_kernel_iterator.h"
28
+ #include "ca_monop_dispatch.h" /* P.6.2.d: ca_monop_view_is_single_cast for F.6.2 gate */
29
+ #include "ca_obj_face.h" /* PROPOSAL_CAFACE_PHASE_2 F.2.6 — ca_strip_face for SRC_* entry */
30
+
31
+ #include <assert.h>
32
+ #include <string.h>
33
+
34
+ /* Defined in carray_core.c. (ca_is_readonly / ca_has_mask /
35
+ ca_sync_data are already declared in carray.h.) */
36
+ extern int ca_attach_is_alias (void *ap);
37
+ extern int ca_root_lends_no_memory (void *ap);
38
+
39
+ /* CAStride family attach-fn marker (step 1-4). */
40
+ extern ca_operation_function_t ca_stride_func;
41
+
42
+ /* Descriptor framework view ops (step 5+). Each view's describe_axes
43
+ emits the per-axis descriptor + parent dim snapshot consumed by
44
+ the P3 ca_axis_dispatch_* substrate. CAShift uses ca_window_func
45
+ (Phase G typedef pattern, ca_shift_func is a copy with only free /
46
+ clone / create_mask overridden) so it routes via attach pointer
47
+ match against ca_window_func.attach. */
48
+ extern ca_operation_function_t ca_select_axis_func;
49
+ extern ca_operation_function_t ca_grid_func;
50
+ extern ca_operation_function_t ca_select_func;
51
+ extern ca_operation_function_t ca_window_func;
52
+ /* ca_mapping_func retired in R.3 (PROPOSAL_CAMAPPING_REMOVAL). */
53
+
54
+ /* SRC_ATTACH 5 view (step 9). Each defines its own attach that
55
+ materialises src->ptr via view-specific transform (cast / swap /
56
+ bit unpack / reduction). kernel_iterator treats them uniformly:
57
+ ca_attach(src) → kernel sees src->ptr as contig → on WRITE,
58
+ ca_sync(src) lets the view's sync_data scatter back. */
59
+ extern ca_operation_function_t ca_fake_func;
60
+ extern ca_operation_function_t ca_byte_swap_func;
61
+ extern ca_operation_function_t ca_bitfield_func;
62
+ extern ca_operation_function_t ca_bitarray_func;
63
+ extern ca_operation_function_t ca_reduce_func;
64
+ extern ca_operation_function_t ca_object_func;
65
+
66
+ /* F-2 follow-up (2026-05-26): connect CATile / CARoll to kernel_iterator
67
+ via the SRC_ATTACH pattern. Per-cell modulo wrap does not fit the
68
+ descriptor framework's kind enum {STRIDE, INDEX, SHIFT}, so the
69
+ innermost-STRIDE L2 alias path is not used; but the view-specific
70
+ func_attach (embed-region gather) materialises src->ptr and func_sync
71
+ scatters back — structurally identical to SRC_ATTACH. Fills the gap
72
+ in the "deliver" principle (= these were previously rejected via
73
+ SRC_NONE). */
74
+ extern ca_operation_function_t ca_tile_func;
75
+ extern ca_operation_function_t ca_roll_func;
76
+
77
+ /* PROPOSAL_CASTACK.md Phase 3 (2026-06-18): CAStack via SRC_ATTACH.
78
+ func_attach materialises src->ptr via per-parent ca_xfer_all GET
79
+ (= K * parent.bytes alloc, caller responsibility per MEMO §3.4);
80
+ func_sync scatters back via xfer_all PUT. kernel sees a flat
81
+ contig slab over the stacked output of shape (K, *parent_shape). */
82
+ extern ca_operation_function_t ca_stack_func;
83
+ extern ca_operation_function_t ca_meld_func;
84
+
85
+ /* M.6 (PROPOSAL_CAREMAP_INTERNAL.md §5.2): CARemap is internal-only
86
+ (no Ruby class constant) but participates in kernel_iterator as an
87
+ SRC_ATTACH source. Per-element gather has no STRIDE structure to
88
+ preserve, so SRC_ATTACH (scratch materialise via ca_remap_func_attach)
89
+ is the natural acceptance path. func_attach allocates a fresh
90
+ buffer and runs xfer_all(GET); kernel sees a flat contig slab.
91
+ On WRITE, ca_sync routes through xfer_all(PUT). */
92
+ extern ca_operation_function_t ca_remap_func;
93
+
94
+ /* PROPOSAL_LAZY_ELEMENTWISE_VIEW Phase 4.5 P.4.5.1 (2026-06-07): lazy
95
+ element-wise view family. CAMonOp (Phase 1) / CABinOp (Phase 2) /
96
+ CABinCmp + CAMonCmp (Phase 4) all expose a func_attach that pulls
97
+ the lazy tree's materialise into a fresh contig buffer; kernel sees
98
+ a flat slab. Same SRC_ATTACH structural pattern as CAFake et al,
99
+ so a 4-line addition to the classify_source list opens all 22
100
+ mkkernel-generated reduction ops (sum / count / mean / variance /
101
+ argmin / ...) to lazy operands — `(a.lazy + b).sum` now works. */
102
+ extern ca_operation_function_t ca_monop_func;
103
+ extern ca_operation_function_t ca_binop_func;
104
+ extern ca_operation_function_t ca_bincmp_func;
105
+ extern ca_operation_function_t ca_moncmp_func;
106
+ extern CArray *ca_remap_new (CArray *ref, CArray *idx);
107
+
108
+ /* ca_reduce_new is declared in carray.h; rb_cCAReduce defined in
109
+ ca_obj_reduce.c — needed by 9.3 smoke helpers since CAReduce has
110
+ no public Ruby surface. */
111
+ extern VALUE rb_cCAReduce;
112
+ extern void ca_select_axis_describe_axes (void *ap, ca_axis_desc_t *out,
113
+ ca_size_t *out_parent_dims);
114
+ extern void ca_grid_describe_axes (void *ap, ca_axis_desc_t *out,
115
+ ca_size_t *out_parent_dims);
116
+ extern void ca_select_describe_axes (void *ap, ca_axis_desc_t *out,
117
+ ca_size_t *out_parent_dims);
118
+ extern void ca_window_describe_axes (void *ap, ca_axis_desc_t *out,
119
+ ca_size_t *out_parent_dims);
120
+
121
+ /* ---- helpers -------------------------------------------------------- */
122
+
123
+ /* True iff src is a CAStride-family view (contig or not). Entity
124
+ arrays are not CAStride family — they're tested separately. */
125
+ static int
126
+ ca_iter_is_castride_family (CArray *src)
127
+ {
128
+ if ( src == NULL ) return 0;
129
+ return ca_func[src->obj_type].attach == ca_stride_func.attach;
130
+ }
131
+
132
+ /* Source kinds declared by view classes installed from outside the core
133
+ (ca_install_obj_type). Sized to CA_OBJ_TYPE_MAX so any obj_type can be
134
+ indexed directly; file-scope zero-init leaves unregistered slots at
135
+ CA_ITER_SRC_NONE (= 0), which is exactly "the classifier decides".
136
+ See ca_kernel_iterator.h for the contract an external class accepts by
137
+ registering. */
138
+ static uint8_t ca_iter_registered_source_kind[CA_OBJ_TYPE_MAX];
139
+
140
+ void
141
+ ca_iter_register_source_kind (int obj_type, uint8_t kind)
142
+ {
143
+ if ( obj_type < 0 || obj_type >= CA_OBJ_TYPE_MAX ) {
144
+ rb_raise(rb_eArgError,
145
+ "ca_iter_register_source_kind: obj_type %d out of range",
146
+ obj_type);
147
+ }
148
+ if ( kind != CA_ITER_SRC_ATTACH ) {
149
+ rb_raise(rb_eArgError,
150
+ "ca_iter_register_source_kind: only CA_ITER_SRC_ATTACH (%d) "
151
+ "may be registered, got %d",
152
+ CA_ITER_SRC_ATTACH, (int) kind);
153
+ }
154
+ ca_iter_registered_source_kind[obj_type] = kind;
155
+ }
156
+
157
+ /* Classify the source by routing kind (proposal §1 strategy table).
158
+ Returns CA_ITER_SRC_NONE for sources not yet supported. */
159
+ static uint8_t
160
+ ca_iter_classify_source (CArray *src)
161
+ {
162
+ if ( src == NULL ) return CA_ITER_SRC_NONE;
163
+ if ( ca_is_entity(src) ) return CA_ITER_SRC_CASTRIDE;
164
+ if ( ca_iter_is_castride_family(src) ) return CA_ITER_SRC_CASTRIDE;
165
+
166
+ /* Externally installed obj_types declare their routing (2026-08-07).
167
+ Placed after the two structural cases and before the built-in list:
168
+ entity and CAStride-family sources are recognised from the struct
169
+ itself and are read directly, so a registration must not be able to
170
+ divert them onto a materialising path; everything below is a lookup
171
+ of one operation table against another, and a class that registered
172
+ is answering exactly that question about itself. */
173
+ {
174
+ uint8_t kind = ca_iter_registered_source_kind[src->obj_type];
175
+ if ( kind != CA_ITER_SRC_NONE ) return kind;
176
+ }
177
+
178
+ /* Descriptor framework views (step 5.1: CSA + CAGrid; 5.2: + CASelect /
179
+ CAMapping / CAWindow / CAShift). CAShift uses ca_window_func
180
+ for attach (Phase G typedef pattern) so it matches the CAWindow
181
+ check below. */
182
+ void *attach = ca_func[src->obj_type].attach;
183
+ if ( attach == ca_select_axis_func.attach ) return CA_ITER_SRC_DESCRIPTOR;
184
+ if ( attach == ca_grid_func.attach ) return CA_ITER_SRC_DESCRIPTOR;
185
+ if ( attach == ca_select_func.attach ) return CA_ITER_SRC_DESCRIPTOR;
186
+ if ( attach == ca_window_func.attach ) return CA_ITER_SRC_DESCRIPTOR; /* + CAShift */
187
+
188
+ /* Step 9: SRC_ATTACH 5 view. Each view-specific attach materialises
189
+ src->ptr via per-element transform; kernel sees a flat contig slab. */
190
+ if ( attach == ca_fake_func.attach ) return CA_ITER_SRC_ATTACH;
191
+ if ( attach == ca_byte_swap_func.attach ) return CA_ITER_SRC_ATTACH;
192
+ if ( attach == ca_bitfield_func.attach ) return CA_ITER_SRC_ATTACH;
193
+ if ( attach == ca_bitarray_func.attach ) return CA_ITER_SRC_ATTACH;
194
+ if ( attach == ca_reduce_func.attach ) return CA_ITER_SRC_ATTACH;
195
+ /* Step 11: CAObject — Ruby callback per-element bridge. Same
196
+ SRC_ATTACH structural pattern (func_attach materialises via Ruby
197
+ copy_data, func_sync scatters back via Ruby sync_data, CA_FLAG_
198
+ READ_ONLY auto-rejects WRITE via ca_is_readonly). Bench gate
199
+ n/a (Ruby callback overhead structurally dominant). */
200
+ if ( attach == ca_object_func.attach ) return CA_ITER_SRC_ATTACH;
201
+
202
+ /* F-2 follow-up: CATile / CARoll via SRC_ATTACH (embed-region gather
203
+ materialises src->ptr; ca_sync scatters back per view-specific
204
+ semantics — CATile tile decomposition, CARoll cyclic permutation). */
205
+ if ( attach == ca_tile_func.attach ) return CA_ITER_SRC_ATTACH;
206
+ if ( attach == ca_roll_func.attach ) return CA_ITER_SRC_ATTACH;
207
+
208
+ /* Phase 3 (PROPOSAL_CASTACK.md): CAStack via SRC_ATTACH. func_attach
209
+ materialises src->ptr via per-parent ca_xfer_all GET (= K * parent
210
+ bytes alloc, caller responsibility per MEMO §3.4); func_sync
211
+ scatters back via xfer_all PUT. kernel sees a flat contig slab
212
+ over the stacked output. Routine ndim mismatch is irrelevant
213
+ here (= attach delivers a self-owned buffer of shape (K, *parent
214
+ shape), kernel reads/writes that buffer). */
215
+ if ( attach == ca_stack_func.attach ) return CA_ITER_SRC_ATTACH;
216
+
217
+ /* CAMeld — ragged concatenate along an existing axis. func_attach
218
+ materialises via K per-parent xfer_all GET into a contig buffer at
219
+ seg_offset[k] * tail_bytes offsets; func_sync scatters back. Same
220
+ SRC_ATTACH structural pattern as CAStack; reduce hot paths bypass
221
+ this via the per-parent decompose in lib/carray/meld_reduce.rb. */
222
+ if ( attach == ca_meld_func.attach ) return CA_ITER_SRC_ATTACH;
223
+
224
+ /* M.6: CARemap — per-element gather, internal-only. Same SRC_ATTACH
225
+ structural pattern (func_attach materialises via xfer_all GET,
226
+ func_sync via xfer_all PUT). */
227
+ if ( attach == ca_remap_func.attach ) return CA_ITER_SRC_ATTACH;
228
+
229
+ /* Phase 4.5 P.4.5.1: lazy view family — CAMonOp / CABinOp / CABinCmp /
230
+ CAMonCmp. func_attach pulls the lazy chain materialise (arena-pooled
231
+ scratches under the hood from Phase 3); kernel sees a flat contig
232
+ slab. Read-only (= CA_FLAG_READ_ONLY) so WRITE auto-rejects at
233
+ the ca_is_readonly check. Opens 22 mkkernel-generated reduction
234
+ ops to lazy operands. */
235
+ if ( attach == ca_monop_func.attach ) return CA_ITER_SRC_ATTACH;
236
+ if ( attach == ca_binop_func.attach ) return CA_ITER_SRC_ATTACH;
237
+ if ( attach == ca_bincmp_func.attach ) return CA_ITER_SRC_ATTACH;
238
+ if ( attach == ca_moncmp_func.attach ) return CA_ITER_SRC_ATTACH;
239
+
240
+ return CA_ITER_SRC_NONE;
241
+ }
242
+
243
+ /* F-2 (PROPOSAL_F2_KERNEL_ITERATOR_ALIAS rev6): route a source by
244
+ running classify_source and, for descriptor-routed views, also calling
245
+ describe_axes to inspect the innermost axis kind. Returns the refined
246
+ src_kind (= SRC_DESCRIPTOR_L2_ALIASABLE iff innermost axis is STRIDE,
247
+ else SRC_DESCRIPTOR). out_descs / out_parent_dims / out_ndim are
248
+ populated for descriptor sources so the caller (init_l1 / init_l2)
249
+ does not have to re-call describe_axes. For non-descriptor sources
250
+ (CASTRIDE / ATTACH / NONE) the out_* arguments are not touched and
251
+ classify_source's verdict is returned as-is.
252
+
253
+ Cost analysis: classify_source is O(1) pointer compares. For
254
+ SRC_DESCRIPTOR candidates we add one describe_axes call (O(ndim) with
255
+ ndim ≤ CA_RANK_MAX = 16) plus one innermost-axis kind compare.
256
+ Per-walk overhead = 1 describe_axes call (descriptor sources only),
257
+ which init was going to do anyway -- so the routing is net zero cost
258
+ compared to the pre-rev6 path where classify_source + init both
259
+ computed describe_axes redundantly. See prep doc rev2 §4.1.1. */
260
+ static uint8_t ca_iter_classify_source (CArray *src);
261
+ static void ca_iter_describe_axes (CArray *src, ca_axis_desc_t *,
262
+ ca_size_t *, int8_t *);
263
+
264
+ static uint8_t
265
+ ca_iter_route_source (CArray *src,
266
+ ca_axis_desc_t *out_descs,
267
+ ca_size_t *out_parent_dims,
268
+ int8_t *out_ndim)
269
+ {
270
+ uint8_t kind = ca_iter_classify_source(src);
271
+ if ( kind != CA_ITER_SRC_DESCRIPTOR ) return kind;
272
+
273
+ /* Descriptor source: describe_axes + inspect innermost. */
274
+ ca_iter_describe_axes(src, out_descs, out_parent_dims, out_ndim);
275
+ if ( ca_axis_dispatch_is_innermost_stride(out_descs, *out_ndim) ) {
276
+ return CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE;
277
+ }
278
+ return CA_ITER_SRC_DESCRIPTOR;
279
+ }
280
+
281
+ /* Dispatch to the view's describe_axes. Routing keyed on the
282
+ shared attach pointer (CAShift shares with CAWindow per Phase G
283
+ typedef). */
284
+ static void
285
+ ca_iter_describe_axes (CArray *src,
286
+ ca_axis_desc_t *out_descs,
287
+ ca_size_t *out_parent_dims,
288
+ int8_t *out_ndim)
289
+ {
290
+ void *attach = ca_func[src->obj_type].attach;
291
+ if ( attach == ca_select_axis_func.attach ) {
292
+ ca_select_axis_describe_axes(src, out_descs, out_parent_dims);
293
+ *out_ndim = src->ndim;
294
+ return;
295
+ }
296
+ if ( attach == ca_grid_func.attach ) {
297
+ ca_grid_describe_axes(src, out_descs, out_parent_dims);
298
+ *out_ndim = src->ndim;
299
+ return;
300
+ }
301
+ if ( attach == ca_select_func.attach ) {
302
+ ca_select_describe_axes(src, out_descs, out_parent_dims);
303
+ /* CASelect emits 1-D INDEX descriptor per its describe_axes
304
+ contract. src->ndim should reflect the selector-flattened
305
+ view shape; engine reads ndim from this argument. */
306
+ *out_ndim = src->ndim;
307
+ return;
308
+ }
309
+ if ( attach == ca_window_func.attach ) {
310
+ ca_window_describe_axes(src, out_descs, out_parent_dims);
311
+ *out_ndim = src->ndim;
312
+ return;
313
+ }
314
+ /* unreachable: validate_inputs already gated through ca_iter_classify_source */
315
+ *out_ndim = 0;
316
+ }
317
+
318
+ /* Public alias eligibility predicate (proposal §11.3 case (a)). */
319
+ int
320
+ ca_iter_can_alias (void *ap, int level)
321
+ {
322
+ CArray *ca = (CArray *) ap;
323
+ if ( ca == NULL ) return 0;
324
+
325
+ switch ( level ) {
326
+ case 1:
327
+ return ca_attach_is_alias(ca);
328
+
329
+ case 2:
330
+ if ( ca_is_entity(ca) ) return 1;
331
+ return ca_iter_is_castride_family(ca);
332
+
333
+ default:
334
+ /* L3 (and any future level) — not implemented in Phase 1; L1
335
+ fallback so the predicate stays well-defined for callers that
336
+ probe ahead. */
337
+ return ca_attach_is_alias(ca);
338
+ }
339
+ }
340
+
341
+ /* Build row-major byte strides for an entity-shape (CA_RANK_MAX). */
342
+ static void
343
+ ca_iter_build_rowmajor_strides (ca_size_t *strides,
344
+ const ca_size_t *dim,
345
+ int8_t ndim,
346
+ ca_size_t bytes)
347
+ {
348
+ ca_size_t s = bytes;
349
+ int8_t k;
350
+ for ( k = ndim - 1; k >= 0; k-- ) {
351
+ strides[k] = s;
352
+ s *= dim[k];
353
+ }
354
+ }
355
+
356
+ /* ---- PROPOSAL_FIBER_PER_SOURCE_PATH F.6.1 dispatch predicate -------- */
357
+
358
+ /* Hybrid (rev2 Q2): coarse src_kind branch + view-specific func pointer
359
+ probe + fiber-axis effective stride check (Q3).
360
+ F.6.1 substrate: returns 0 (= disabled) for all sources. F.6.2+
361
+ (CAFake / CAByteSwap / CAShift / CAWindow / etc.) progressively enable
362
+ specific source kinds with bench-driven justification.
363
+
364
+ The fiber_axis_stride argument is the effective byte stride of the
365
+ fiber axis on the view (= what next_slab_axes would yield as
366
+ slab_strides[0]). Q3-equivalence: fiber_axis_stride == src->bytes
367
+ means the fiber is parent-memory contig, which is the precondition
368
+ for X.1/X.4 per-region fused paths to deliver a 1-pass result.
369
+
370
+ Called from init_l2 SRC_ATTACH branch right before the whole-view
371
+ ca_xfer_all GET. When returning 1, caller skips xfer_all GET and
372
+ sets alias_mode = CA_ITER_ALIAS_PER_FIBER_FUSED with fiber dispatch
373
+ state populated. */
374
+ static int
375
+ ca_iter_should_per_fiber_fused (CArray *src,
376
+ int src_kind,
377
+ int8_t fiber_axis,
378
+ ca_size_t fiber_axis_stride,
379
+ uint32_t flags)
380
+ {
381
+ (void) fiber_axis;
382
+ (void) flags;
383
+
384
+ /* F.6.2: CAFake / CAByteSwap (transform-fused, X.4 per-region).
385
+ Enable when fiber-axis effective stride == view cell bytes
386
+ (= Q3: fiber is parent-memory contig run, so ca_xfer_stride
387
+ routes into ca_xfer_stride_transform_fused inner-contig fast
388
+ path for 1-pass per-fiber delivery).
389
+
390
+ F.5 bench: innermost-axis fiber 30-35% faster than whole-view
391
+ materialise (CAFake) / 16-18% (CAByteSwap). Non-innermost (=
392
+ fiber_axis_stride != bytes) was a loser, so the stride gate
393
+ keeps current path for that case.
394
+
395
+ Phase 6 P.6.2.d (Q13 α): single-cast CAMonOp (= post-migration
396
+ successor of CAFake numeric path) is recognised via
397
+ ca_monop_view_is_single_cast and routed through the same F.6.2
398
+ fast path. Chain CAMonOp (depth ≥ 2) is excluded — its
399
+ materialise path (= arena-pooled scratches from Phase 3) is
400
+ structurally different from the X.4 inner-contig fast path. */
401
+ void *attach = ca_func[src->obj_type].attach;
402
+ if ( src_kind == CA_ITER_SRC_ATTACH ) {
403
+ if ( attach == ca_fake_func.attach
404
+ || attach == ca_byte_swap_func.attach
405
+ || ca_monop_view_is_single_cast(src) ) {
406
+ return fiber_axis_stride == (ca_size_t) src->bytes;
407
+ }
408
+ }
409
+
410
+ /* F.6.3: CAShift / CAWindow with OOB-fused materialise path (X.1).
411
+ Per-fiber ca_xfer_stride wins decisively when the view would
412
+ otherwise fall into a per-slab gather / whole-view materialise
413
+ path (= SRC_DESCRIPTOR, e.g. CAShift always, CAWindow with SHIFT
414
+ axes). But when the view qualifies for the L2 alias fast path
415
+ (SRC_DESCRIPTOR_L2_ALIASABLE, = interior-only CAWindow with F-1
416
+ STRIDE promotion), the existing alias path is already optimal
417
+ (parent.ptr + composed offsets, zero materialise). Per-fiber
418
+ dispatch in that case adds ca_xfer_stride per-call overhead with
419
+ no payoff -- F.6.3 bench measured 0.35-0.82x (regression). Gate
420
+ on SRC_DESCRIPTOR only. */
421
+ if ( src_kind == CA_ITER_SRC_DESCRIPTOR ) {
422
+ /* Both CAWindow and CAShift match ca_window_func.attach (Phase G
423
+ typedef pattern: ca_shift_func = ca_window_func). */
424
+ if ( attach == ca_window_func.attach ) {
425
+ return 1;
426
+ }
427
+ }
428
+
429
+ /* F.6.4 audit (devel/bench_f6_4_audit.rb): CSA (CASelectAxis) /
430
+ CAGrid / CASelect / CAMapping all kept on the whole-view
431
+ materialise path. Per-fiber ca_xfer_stride is a LOSER for
432
+ these views (1.25-4.7x slower across all axis positions
433
+ measured) because the descriptor framework's
434
+ ca_axis_dispatch_gather is already a single batched per-axis
435
+ kind gather; splitting into N per-fiber calls loses the
436
+ batched throughput. Structurally different from
437
+ CAFake/CAByteSwap (transform-fused at byte level, X.4) and
438
+ CAShift/CAWindow OOB (per-region fused with bound check, X.1):
439
+ descriptor framework views do not have a per-region fused fast
440
+ path designed for per-fiber entry.
441
+
442
+ Note (rev4): bench split fiber-axis kind shows STRIDE fiber
443
+ 1.25x slower vs INDEX fiber 4-5x slower. STRIDE-fiber case
444
+ is close to parity and could conceivably reach win with finer
445
+ gating (= bypass per-axis kind classification when the outer
446
+ prefix is homogeneous STRIDE). Not pursued at current 1.25x
447
+ deficit; recorded as a future possible refinement should an
448
+ application motivate it. See proposal §3.1 SRC_DESCRIPTOR
449
+ (CSA, CAGrid, ...) inline comment.
450
+
451
+ F.6.6 audit (devel/bench_f6_6_bit_audit.rb): CABitarray /
452
+ CABitfield kept on current path. Bench measured via
453
+ bits.fake(:float64) (= production CAFake-wrap hot path) shows
454
+ per-fiber 1.23-5.86x slower on the F.6.2-gate-off axes.
455
+
456
+ The CAFake wrap overhead applies equally to both per-fiber and
457
+ whole-view paths (same total cell-cast cost), so the 5x ratio
458
+ is essentially the CABitarray whole-view-vs-per-fiber ratio
459
+ itself. Phase A bench note "xfer_stride 0.98x parity" is a
460
+ single-call comparison: N per-fiber calls cumulatively cost
461
+ N x parity > 1 x xfer_all (+ N dispatch overhead + per-call
462
+ bit alignment math). This reasoning generalises to direct
463
+ CABitarray / CABitfield source (= when a non-f64 fiber smoke
464
+ is added in a future phase) -- no separate audit needed for
465
+ that case. Predicate stays unchanged.
466
+
467
+ F.6.7 will audit CAReduce. */
468
+ return 0;
469
+ }
470
+
471
+ /* ---- state machine -------------------------------------------------- */
472
+
473
+ /* Shared input validation for init_l1 / init_l2. Returns CA_ITER_OK
474
+ if the (src, policy, flags) tuple is acceptable, else the
475
+ corresponding CA_ITER_ERR_*. */
476
+ static int
477
+ ca_iter_validate_inputs (ca_iter_state *st,
478
+ struct _CArray *src,
479
+ ca_slab_policy_t policy,
480
+ uint32_t flags)
481
+ {
482
+ if ( st == NULL || src == NULL ) return CA_ITER_ERR_FLAGS;
483
+ /* Phase A capstone: CA_SLAB_AXES accepted (init_l2 path only — init_l1
484
+ does not implement CA_SLAB_AXES yet, see init_l1's policy gate). */
485
+ if ( policy != CA_SLAB_WHOLE && policy != CA_SLAB_AXES )
486
+ return CA_ITER_ERR_POLICY;
487
+
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. */
494
+ const uint32_t accepted = CA_KERNEL_WRITE | CA_KERNEL_NO_MASK
495
+ | CA_KERNEL_FIBER_CONTIG;
496
+ if ( flags & ~accepted ) return CA_ITER_ERR_FLAGS;
497
+
498
+ /* WRITE on a readonly view (CARepeat stride-0 / value_array /
499
+ CAWrap readonly) — explicit reject, would otherwise SEGV on
500
+ write. */
501
+ if ( (flags & CA_KERNEL_WRITE) && ca_is_readonly(src) ) {
502
+ return CA_ITER_ERR_READONLY;
503
+ }
504
+
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. */
512
+ if ( (flags & CA_KERNEL_NO_MASK) && ca_has_mask(src) ) {
513
+ return CA_ITER_ERR_MASK_NOT_ALLOWED;
514
+ }
515
+
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. */
521
+ if ( ca_iter_classify_source(src) == CA_ITER_SRC_NONE ) {
522
+ return CA_ITER_ERR_NOT_CHEAP;
523
+ }
524
+ return CA_ITER_OK;
525
+ }
526
+
527
+ /* Storage-identical wrapper strip for the kernel-compute entry.
528
+
529
+ A Face layers a semantic identifier and a CALazyMarker layers "read this
530
+ as the leaf of a lazy chain"; neither changes the storage, so routing and
531
+ alias decisions belong to what they wrap. ca_strip_face answers this for
532
+ Face alone and is read that way in ~60 other places, so the marker is
533
+ added here rather than inside it.
534
+
535
+ The marker is NOT classified as a source in its own right. Routing it to
536
+ CA_ITER_SRC_ATTACH would work but would allocate elements * bytes and pull
537
+ the whole array through ca_xfer_all first -- a.lazy.sum(axis: 0) would copy
538
+ what a.sum(axis: 0) aliases. Descending instead lets the parent be
539
+ classified on its own merits, which costs nothing.
540
+
541
+ Unlike Face, nothing re-wraps the result: a reduction over a lazy marker
542
+ yields a plain entity, not a lazy view.
543
+
544
+ CAREFUL: a marker carries CA_FLAG_READ_ONLY and its parent usually does
545
+ not, so the WRITE rejection has to happen against the wrapper. The caller
546
+ does that before stripping. */
547
+ static CArray *
548
+ ca_iter_strip_storage_wrapper (CArray *src)
549
+ {
550
+ while ( src && ( ca_is_face(src) || ca_is_lazy_marker(src) ) ) {
551
+ src = ((CAView *) src)->parent;
552
+ }
553
+ return src;
554
+ }
555
+
556
+ int
557
+ ca_iter_state_init_l1 (ca_iter_state *st,
558
+ struct _CArray *src,
559
+ ca_slab_policy_t policy,
560
+ int8_t *axes,
561
+ int8_t naxes,
562
+ uint32_t flags)
563
+ {
564
+ /* PROPOSAL_CAFACE_PHASE_2 F.2.6 + PROPOSAL_LAZY_MARKER_LIFT Phase 0:
565
+ storage-identical wrapper strip at entry (= same rationale as init_l2
566
+ below). Strip before validate_inputs.
567
+
568
+ The WRITE rejection is taken against the wrapper, not what it wraps: a
569
+ CALazyMarker is read-only while its parent is not, and stripping first
570
+ would let a destructive kernel through to the parent. validate_inputs
571
+ re-checks the stripped source, which is harmless. */
572
+ if ( (flags & CA_KERNEL_WRITE) && ca_is_readonly(src) ) {
573
+ return CA_ITER_ERR_READONLY;
574
+ }
575
+ if ( ca_is_face(src) || ca_is_lazy_marker(src) ) {
576
+ src = ca_iter_strip_storage_wrapper(src);
577
+ }
578
+
579
+ int rc = ca_iter_validate_inputs(st, src, policy, flags);
580
+ if ( rc != CA_ITER_OK ) return rc;
581
+
582
+ /* Phase A capstone: CA_SLAB_AXES is L2-only (kernels needing K-D slab
583
+ yield use init_l2). init_l1 rejects so callers don't get a silent
584
+ misdispatch. */
585
+ if ( policy == CA_SLAB_AXES ) return CA_ITER_ERR_POLICY;
586
+
587
+ uint8_t src_kind = ca_iter_classify_source(src);
588
+
589
+ memset(st, 0, sizeof(*st));
590
+ st->src = src;
591
+ st->src_kind = src_kind;
592
+ st->level = 1;
593
+ st->policy = policy;
594
+ st->ndim = src->ndim;
595
+ st->flags = flags;
596
+ st->bytes = src->bytes;
597
+ st->axes = axes;
598
+ st->naxes = naxes;
599
+
600
+ st->slab_n = src->elements;
601
+ st->total_slabs = 1;
602
+ st->slabs_emitted = 0;
603
+ st->chunk_size = st->slab_n;
604
+
605
+ if ( src_kind == CA_ITER_SRC_ATTACH ) {
606
+ /* === SRC_ATTACH path (step 9 + 2026-05-31 refactor): CAFake /
607
+ CAByteSwap / CABitfield / CABitarray / CAReduce =================
608
+ View's xfer_all delivers materialised data through the unified
609
+ dispatch surface. Previously used `ca_attach(src) + src->ptr`
610
+ (= view's attach slot) and `ca_sync(src)` for WRITE; now uses
611
+ `ca_xfer_all(src, scratch, GET/PUT)` with iterator-owned scratch.
612
+ Decouples kernel_iterator from per-view attach/sync lifecycle
613
+ and inherits xfer reform improvements (transform-fused, etc.)
614
+ 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
+ }
620
+ st->alias_mode = CA_ITER_ALIAS_NONE; /* scratch-owned, no src.detach */
621
+ st->alias_ptr = st->scratch_ptr;
622
+ st->composed_strides[0] = src->bytes;
623
+ st->composed_base = 0;
624
+ st->outer_idx = NULL;
625
+ /* Step 6: mask gather (xfer_all on mask -- ca_copy_data is a thin
626
+ wrapper, this just removes the extra hop). */
627
+ if ( ca_has_mask(src) ) {
628
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
629
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
630
+ if ( src->elements > 0 ) {
631
+ ca_xfer_all(src->mask, (char *) st->scratch_mask, CA_XFER_GET);
632
+ }
633
+ st->alias_mask = st->scratch_mask;
634
+ }
635
+ return CA_ITER_OK;
636
+ }
637
+
638
+ if ( src_kind == CA_ITER_SRC_DESCRIPTOR ) {
639
+ /* === descriptor path (step 5.1: CSA + CAGrid) ======================
640
+ Reuse P3 substrate: describe_axes → prepare → layout. For now
641
+ always materialise via ca_axis_dispatch_attach into scratch and
642
+ yield one contig slab (alias_mode = NONE). The view-transparency
643
+ principle (proposal §0): kernel sees a flat slab regardless of
644
+ per-axis kind; INDEX / SHIFT mix is handled inside the engine. */
645
+ ca_axis_desc_t raw_descs[CA_RANK_MAX];
646
+ int8_t raw_ndim = 0;
647
+ ca_iter_describe_axes(src, raw_descs, st->parent_axis_dims, &raw_ndim);
648
+ /* Cache the post-merge layout for next_slab / sync_slab. */
649
+ ca_axis_dispatch_prepare(st->parent_axis_dims, raw_descs, raw_ndim,
650
+ st->bytes, st->descs, st->pstrides,
651
+ st->mdim, &st->desc_ndim);
652
+ ca_axis_dispatch_layout(st->descs, st->pstrides, st->mdim,
653
+ st->desc_ndim, st->bytes,
654
+ &st->slab_start, &st->slab_bytes_desc,
655
+ &st->slab_base);
656
+ if ( st->slab_start > 0 ) {
657
+ ca_axis_dispatch_classify_prefix(st->descs, st->pstrides,
658
+ st->slab_start, st->prefix);
659
+ }
660
+ st->total_elements = src->elements;
661
+
662
+ /* Materialise via the engine. ca_axis_dispatch_attach handles
663
+ parent attach + scratch alloc + gather (INDEX axes included);
664
+ we own the resulting buffer. Descriptor views inherit from
665
+ CAView so parent is at the CAVIEW prefix slot. */
666
+ CArray *parent = CAVIEW(src)->parent;
667
+ ca_attach(parent); /* engine reads parent->ptr */
668
+ st->root = parent; /* finish() detaches */
669
+ st->scratch_cap = src->elements * src->bytes;
670
+ /* Engine ndim = descriptor ndim from describe_axes (not view.ndim).
671
+ Critical for CAMapping where view.ndim > 1 but the descriptor is
672
+ 1-D INDEX gather.
673
+ bound_fill = CAWindow/CAShift fill value (Tier 2.B SHIFT-axis
674
+ OOB cell), NULL for non-window views (CSA / CAGrid / CASelect /
675
+ CAMapping don't have OOB semantics — engine sees no SHIFT axes). */
676
+ const void *bound_fill = NULL;
677
+ if ( ca_func[src->obj_type].attach == ca_window_func.attach ) {
678
+ bound_fill = ((CAWindow *) src)->fill;
679
+ }
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);
686
+ st->alias_mode = CA_ITER_ALIAS_NONE;
687
+ st->alias_ptr = st->scratch_ptr;
688
+
689
+ /* Step 6: gather mask via ca_copy_data on src->mask (works for
690
+ all descriptor views — mask propagates through their
691
+ func_copy_data via the descriptor framework's own mask
692
+ handling). */
693
+ if ( ca_has_mask(src) ) {
694
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
695
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
696
+ if ( src->elements > 0 ) {
697
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
698
+ }
699
+ st->alias_mask = st->scratch_mask;
700
+ }
701
+ return CA_ITER_OK;
702
+ }
703
+
704
+ /* === CAStride path (step 1-4) ============================== */
705
+ if ( ca_iter_can_alias(src, 1) ) {
706
+ /* alias path: entity or CAStride contig */
707
+ st->alias_mode = CA_ITER_ALIAS_CONTIG;
708
+ ca_attach(src);
709
+ st->alias_ptr = (char *) src->ptr;
710
+ } else {
711
+ /* scratch path: CAStride family non-contig. ca_copy_data routes
712
+ to ca_stride_func_copy_data which composes leaf strides up to
713
+ the root entity and gathers in one pass — no per-intermediate
714
+ 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
+ }
721
+ st->alias_mode = CA_ITER_ALIAS_NONE;
722
+ st->alias_ptr = st->scratch_ptr;
723
+ }
724
+
725
+ /* Step 6: gather mask into scratch_mask if source carries one.
726
+ We always gather into a contig boolean8_t buffer for uniformity
727
+ across alias / scratch / descriptor paths — the kernel sees a
728
+ simple `boolean8_t *` aligned with the value slab. Future
729
+ optimisation: alias mask directly for CAStride contig. Mask is
730
+ informational; kernel uses CA_FOR_EACH_UNMASKED macros to skip
731
+ masked cells. */
732
+ if ( ca_has_mask(src) ) {
733
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
734
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
735
+ if ( src->elements > 0 ) {
736
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
737
+ }
738
+ st->alias_mask = st->scratch_mask;
739
+ }
740
+ return CA_ITER_OK;
741
+ }
742
+
743
+ int
744
+ ca_iter_state_init_l2 (ca_iter_state *st,
745
+ struct _CArray *src,
746
+ ca_slab_policy_t policy,
747
+ int8_t *axes,
748
+ int8_t naxes,
749
+ uint32_t flags)
750
+ {
751
+ /* PROPOSAL_CAFACE_PHASE_2 F.2.6 (= MEMO §3.5 kernel_iterator entry strip)
752
+ + PROPOSAL_LAZY_MARKER_LIFT Phase 0.
753
+
754
+ A Face only layers a semantic identifier and a CALazyMarker only layers
755
+ "leaf of a lazy chain"; storage is identical to the parent either way, so
756
+ strip at the kernel-compute entry and descend. Routing and alias
757
+ decisions belong to the parent. Strip before validate_inputs —
758
+ classify_source rejects both as knows-no.
759
+
760
+ The Face identifier is re-wrapped onto the result by the caller's lift
761
+ hook (= primary operators / reductions / etc.). The marker is not:
762
+ a reduction over a lazy marker yields a plain entity.
763
+
764
+ WRITE is rejected against the wrapper, before the strip — a marker is
765
+ read-only while its parent is not. validate_inputs re-checks the
766
+ stripped source, which is harmless. */
767
+ if ( (flags & CA_KERNEL_WRITE) && ca_is_readonly(src) ) {
768
+ return CA_ITER_ERR_READONLY;
769
+ }
770
+ if ( ca_is_face(src) || ca_is_lazy_marker(src) ) {
771
+ src = ca_iter_strip_storage_wrapper(src);
772
+ }
773
+
774
+ int rc = ca_iter_validate_inputs(st, src, policy, flags);
775
+ if ( rc != CA_ITER_OK ) return rc;
776
+
777
+ /* F-2 (rev6): route_source returns the refined src_kind and (for
778
+ descriptor sources) populates raw_descs / raw_pdims / raw_ndim so we
779
+ don't re-call describe_axes when SRC_DESCRIPTOR_L2_ALIASABLE upgrades
780
+ into the alias branch. Non-descriptor sources leave the out_*
781
+ buffers untouched. */
782
+ ca_axis_desc_t raw_descs[CA_RANK_MAX];
783
+ ca_size_t raw_pdims[CA_RANK_MAX];
784
+ int8_t raw_ndim = 0;
785
+ uint8_t src_kind = ca_iter_route_source(src, raw_descs, raw_pdims, &raw_ndim);
786
+
787
+ /* Phase B capstone (T2): CA_SLAB_AXES + descriptor view (= SRC_DESCRIPTOR
788
+ and SRC_DESCRIPTOR_L2_ALIASABLE), accept when slab axes are all-STRIDE
789
+ kind and no outer SHIFT. Other combinations:
790
+ - slab has INDEX/SHIFT kind → reject (Phase C T3 scope)
791
+ - outer has SHIFT axis → reject (B.1.5 materialise downgrade scope)
792
+ - SRC_ATTACH → reject (overlay views; no kind structure)
793
+ SRC_CASTRIDE still goes through the Phase A branch below. */
794
+ if ( policy == CA_SLAB_AXES ) {
795
+ if ( src_kind == CA_ITER_SRC_DESCRIPTOR
796
+ || src_kind == CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE ) {
797
+ /* Validate axes early (range / duplicate). */
798
+ if ( axes == NULL || naxes <= 0 || naxes > src->ndim ) {
799
+ return CA_ITER_ERR_POLICY;
800
+ }
801
+ int8_t in_slab[CA_RANK_MAX];
802
+ int8_t k;
803
+ for ( k = 0; k < CA_RANK_MAX; k++ ) in_slab[k] = 0;
804
+ for ( k = 0; k < naxes; k++ ) {
805
+ int8_t ax = axes[k];
806
+ if ( ax < 0 || ax >= src->ndim ) return CA_ITER_ERR_POLICY;
807
+ if ( in_slab[ax] ) return CA_ITER_ERR_POLICY;
808
+ in_slab[ax] = 1;
809
+ }
810
+
811
+ /* PROPOSAL_FIBER_PER_SOURCE_PATH F.6.3 hook (= parallel to
812
+ F.6.2 SRC_ATTACH hook): for descriptor views CAWindow /
813
+ CAShift (predicate returns 1 unconditionally per F.5 always-
814
+ win bench), skip materialise (alias / C.1 PER_SLAB / B.1.5)
815
+ and use per-fiber ca_xfer_stride which routes to the view's
816
+ X.1 OOB-fused per-region fast path. */
817
+ if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1
818
+ && raw_ndim == src->ndim ) {
819
+ int8_t fiber_ax = axes[0];
820
+ ca_size_t row_byte_strides[CA_RANK_MAX];
821
+ {
822
+ ca_size_t b = src->bytes;
823
+ for ( int8_t kk = src->ndim - 1; kk >= 0; kk-- ) {
824
+ row_byte_strides[kk] = b;
825
+ b *= src->dim[kk];
826
+ }
827
+ }
828
+ if ( ca_iter_should_per_fiber_fused(src, src_kind, fiber_ax,
829
+ row_byte_strides[fiber_ax],
830
+ flags) ) {
831
+ memset(st, 0, sizeof(*st));
832
+ st->src = src;
833
+ st->src_kind = src_kind;
834
+ st->level = 2;
835
+ st->policy = policy;
836
+ st->ndim = src->ndim;
837
+ st->flags = flags;
838
+ st->bytes = src->bytes;
839
+ st->axes = axes;
840
+ st->naxes = naxes;
841
+
842
+ st->alias_mode = CA_ITER_ALIAS_PER_FIBER_FUSED;
843
+ st->alias_ptr = NULL;
844
+ st->fiber_axis = fiber_ax;
845
+ for ( int8_t kk = 0; kk < src->ndim; kk++ ) {
846
+ st->fiber_native_strides[kk] = row_byte_strides[kk];
847
+ }
848
+
849
+ /* Row-major element strides for mask (= identity element
850
+ index strides over src->dim). */
851
+ ca_size_t row_elem_strides[CA_RANK_MAX];
852
+ {
853
+ ca_size_t e = 1;
854
+ for ( int8_t kk = src->ndim - 1; kk >= 0; kk-- ) {
855
+ row_elem_strides[kk] = e;
856
+ e *= src->dim[kk];
857
+ }
858
+ }
859
+
860
+ int8_t sp = 0, op = 0;
861
+ st->slab_elements = 1;
862
+ for ( int8_t kk = 0; kk < src->ndim; kk++ ) {
863
+ if ( in_slab[kk] ) {
864
+ st->slab_axes_buf[sp] = kk;
865
+ st->slab_dims[sp] = src->dim[kk];
866
+ st->slab_strides[sp] = row_byte_strides[kk];
867
+ st->slab_mask_strides[sp] = row_elem_strides[kk];
868
+ st->slab_elements *= src->dim[kk];
869
+ sp++;
870
+ } else {
871
+ st->outer_axes[op] = kk;
872
+ st->outer_dims[op] = src->dim[kk];
873
+ st->outer_strides[op] = row_byte_strides[kk];
874
+ st->outer_mask_strides[op] = row_elem_strides[kk];
875
+ op++;
876
+ }
877
+ }
878
+ st->slab_ndim = sp;
879
+ st->outer_ndim = op;
880
+ st->desc_ndim = 0;
881
+
882
+ ca_size_t total = 1;
883
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) total *= st->outer_dims[m];
884
+ st->total_slabs = total;
885
+ st->slab_n = st->slab_elements;
886
+ st->slabs_emitted = 0;
887
+ st->chunk_size = st->slab_n;
888
+
889
+ if ( st->outer_ndim > 0 ) {
890
+ CA_ASSUME(st->outer_ndim <= CA_RANK_MAX); /* bound alloc over rank */
891
+ st->outer_idx = ALLOC_N(ca_size_t, st->outer_ndim);
892
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) st->outer_idx[m] = 0;
893
+ } else {
894
+ st->outer_idx = NULL;
895
+ }
896
+
897
+ /* Mask: per-fiber gather via next_slab_axes; no whole-view
898
+ materialise (rev2 §3.3, (data,mask) pair travels together). */
899
+ st->alias_mask = NULL;
900
+ return CA_ITER_OK;
901
+ }
902
+ }
903
+
904
+ /* Phase C T3 (C.1): slab axis has INDEX or SHIFT kind → per-slab
905
+ materialise fallback path (D1.1 (B) + D1.2 (A)). CAMapping-style
906
+ views (raw_ndim != src->ndim) are out of C.1 scope — descriptor
907
+ exposes a flat 1-D INDEX gather that doesn't expose the view-
908
+ axis partition the user gave via slab_axes. Reject explicitly. */
909
+ if ( raw_ndim != src->ndim ) {
910
+ return CA_ITER_ERR_POLICY;
911
+ }
912
+ int slab_has_non_stride = 0;
913
+ for ( k = 0; k < src->ndim; k++ ) {
914
+ if ( in_slab[k] && raw_descs[k].kind != CA_AXIS_KIND_STRIDE ) {
915
+ slab_has_non_stride = 1;
916
+ break;
917
+ }
918
+ }
919
+ if ( slab_has_non_stride ) {
920
+ /* C.1 scope: READ-only. WRITE = future sub-step (C.1c). */
921
+ if ( flags & CA_KERNEL_WRITE ) {
922
+ return CA_ITER_ERR_FLAGS;
923
+ }
924
+
925
+ memset(st, 0, sizeof(*st));
926
+ st->src = src;
927
+ st->src_kind = CA_ITER_SRC_DESCRIPTOR;
928
+ st->level = 2;
929
+ st->policy = policy;
930
+ st->ndim = src->ndim;
931
+ st->flags = flags;
932
+ st->bytes = src->bytes;
933
+ st->axes = axes;
934
+ st->naxes = naxes;
935
+
936
+ CArray *parent = CAVIEW(src)->parent;
937
+ ca_attach(parent);
938
+ st->root = parent;
939
+
940
+ /* Persist raw_descs / pdims / pstrides for per-slab subset
941
+ construction (PER_SLAB fallback) or for the hoisted manual
942
+ gather (PER_SLAB_HOIST specialised path). desc_ndim =
943
+ src->ndim so has_descs branch in next_slab_axes can read
944
+ st->descs (though T3 path takes its own branch before that
945
+ check). */
946
+ memcpy(st->descs, raw_descs, src->ndim * sizeof(ca_axis_desc_t));
947
+ memcpy(st->parent_axis_dims, raw_pdims, src->ndim * sizeof(ca_size_t));
948
+ {
949
+ ca_size_t s = src->bytes;
950
+ for ( k = src->ndim - 1; k >= 0; k-- ) {
951
+ st->pstrides[k] = s;
952
+ s *= raw_pdims[k];
953
+ }
954
+ }
955
+ st->desc_ndim = src->ndim;
956
+
957
+ /* View-row-major element strides (= mask scratch layout, since
958
+ mask is gathered whole-view once at init). */
959
+ ca_size_t row_elem_strides[CA_RANK_MAX];
960
+ {
961
+ ca_size_t e = 1;
962
+ for ( k = src->ndim - 1; k >= 0; k-- ) {
963
+ row_elem_strides[k] = e;
964
+ e *= src->dim[k];
965
+ }
966
+ }
967
+
968
+ /* Row-major byte strides over SLAB axes only (= scratch_ptr
969
+ layout, since scratch_ptr is sized slab_elements × bytes
970
+ and refilled per outer iter as a contig row-major slab). */
971
+ ca_size_t slab_data_strides[CA_RANK_MAX];
972
+ {
973
+ ca_size_t b = src->bytes;
974
+ ca_size_t e_slab = 1;
975
+ for ( k = src->ndim - 1; k >= 0; k-- ) {
976
+ if ( in_slab[k] ) {
977
+ slab_data_strides[k] = b;
978
+ b *= raw_descs[k].count;
979
+ e_slab *= raw_descs[k].count;
980
+ } else {
981
+ slab_data_strides[k] = 0; /* unused for outer axes */
982
+ }
983
+ }
984
+ st->slab_elements = e_slab;
985
+ }
986
+
987
+ int8_t sp = 0, op = 0;
988
+ for ( k = 0; k < src->ndim; k++ ) {
989
+ if ( in_slab[k] ) {
990
+ st->slab_axes_buf[sp] = k;
991
+ st->slab_dims[sp] = raw_descs[k].count;
992
+ st->slab_strides[sp] = slab_data_strides[k];
993
+ /* Slab mask stride is view-row-major elem stride along
994
+ this view axis (mask scratch is whole-view layout). */
995
+ st->slab_mask_strides[sp] = row_elem_strides[k];
996
+ sp++;
997
+ } else {
998
+ st->outer_axes[op] = k;
999
+ st->outer_dims[op] = raw_descs[k].count;
1000
+ /* T3 path: outer data offset is always 0 (scratch refilled
1001
+ per slab, ptr = scratch_ptr). */
1002
+ st->outer_strides[op] = 0;
1003
+ /* Outer mask offset uses view-row-major elem stride into
1004
+ whole-view mask scratch. */
1005
+ st->outer_mask_strides[op] = row_elem_strides[k];
1006
+ op++;
1007
+ }
1008
+ }
1009
+ st->slab_ndim = sp;
1010
+ st->outer_ndim = op;
1011
+ st->composed_base = 0; /* unused for T3 fallback */
1012
+
1013
+ ca_size_t total = 1;
1014
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) total *= st->outer_dims[m];
1015
+ st->total_slabs = total;
1016
+ st->slab_n = st->slab_elements;
1017
+ st->slabs_emitted = 0;
1018
+ st->chunk_size = st->slab_n;
1019
+
1020
+ if ( st->outer_ndim > 0 ) {
1021
+ CA_ASSUME(st->outer_ndim <= CA_RANK_MAX); /* bound alloc over rank */
1022
+ st->outer_idx = ALLOC_N(ca_size_t, st->outer_ndim);
1023
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) st->outer_idx[m] = 0;
1024
+ } else {
1025
+ st->outer_idx = NULL;
1026
+ }
1027
+
1028
+ /* Alloc per-slab data scratch (D1.1 (B): one max-slab buffer
1029
+ refilled per outer iter, instead of per-iter alloc/free or
1030
+ full-view materialise). */
1031
+ ca_size_t slab_bytes = st->slab_elements * src->bytes;
1032
+ st->scratch_cap = slab_bytes > 0 ? slab_bytes : 1;
1033
+ st->scratch_ptr = (char *) xmalloc(st->scratch_cap);
1034
+ st->alias_ptr = st->scratch_ptr; /* refilled per slab in next_slab_axes */
1035
+
1036
+ /* Specialisation (B-1b, C.1b): if innermost slab axis is STRIDE
1037
+ and no SHIFT axes anywhere in the view, use the hoisted
1038
+ manual gather path (= outer + non-innermost-slab hoist,
1039
+ inner pure STRIDE linear memcpy). Else (innermost INDEX /
1040
+ SHIFT-anywhere): use the engine fallback (A). */
1041
+ int specialised_eligible = (sp > 0);
1042
+ if ( specialised_eligible ) {
1043
+ int8_t inner_view_ax = st->slab_axes_buf[sp - 1];
1044
+ if ( raw_descs[inner_view_ax].kind != CA_AXIS_KIND_STRIDE ) {
1045
+ specialised_eligible = 0;
1046
+ }
1047
+ }
1048
+ if ( specialised_eligible ) {
1049
+ /* Any SHIFT axis (slab or outer) → fallback to engine for
1050
+ clean bound_fill semantics in C.1b scope. */
1051
+ for ( k = 0; k < src->ndim; k++ ) {
1052
+ if ( raw_descs[k].kind == CA_AXIS_KIND_SHIFT ) {
1053
+ specialised_eligible = 0;
1054
+ break;
1055
+ }
1056
+ }
1057
+ }
1058
+ st->alias_mode = specialised_eligible
1059
+ ? CA_ITER_ALIAS_PER_SLAB_HOIST
1060
+ : CA_ITER_ALIAS_PER_SLAB;
1061
+
1062
+ /* Mask: gather whole view mask once into scratch_mask (= same
1063
+ strategy as B.1.5). Per-slab mask offset is computed via
1064
+ outer_idx × outer_mask_strides in next_slab_axes. */
1065
+ if ( ca_has_mask(src) ) {
1066
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
1067
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
1068
+ if ( src->elements > 0 ) {
1069
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
1070
+ }
1071
+ st->alias_mask = st->scratch_mask;
1072
+ }
1073
+
1074
+ return CA_ITER_OK;
1075
+ }
1076
+ /* slab is all-STRIDE: fall through to B.1.5 / Phase B paths. */
1077
+ /* Phase B.1.5: outer SHIFT axis → materialise downgrade. Alias
1078
+ can't deliver OOB cells (= need fill_value), and the view-family
1079
+ surface prioritises delivering the cells over avoiding a copy, so
1080
+ we materialise the entire view into a row-major scratch
1081
+ buffer via ca_axis_dispatch_attach (= same engine as the
1082
+ existing SRC_DESCRIPTOR L2 NONE path), then walk it with
1083
+ Phase A-style row-major K-D strides. Slab / outer partition
1084
+ applies to view-axes; the scratch IS the flat row-major view. */
1085
+ int outer_has_shift = 0;
1086
+ for ( k = 0; k < src->ndim; k++ ) {
1087
+ if ( !in_slab[k] && raw_descs[k].kind == CA_AXIS_KIND_SHIFT ) {
1088
+ outer_has_shift = 1;
1089
+ break;
1090
+ }
1091
+ }
1092
+
1093
+ if ( outer_has_shift ) {
1094
+ memset(st, 0, sizeof(*st));
1095
+ st->src = src;
1096
+ st->src_kind = CA_ITER_SRC_DESCRIPTOR;
1097
+ st->level = 2;
1098
+ st->policy = policy;
1099
+ st->ndim = src->ndim;
1100
+ st->flags = flags;
1101
+ st->bytes = src->bytes;
1102
+ st->axes = axes;
1103
+ st->naxes = naxes;
1104
+
1105
+ CArray *parent = CAVIEW(src)->parent;
1106
+ ca_attach(parent);
1107
+ st->root = parent;
1108
+
1109
+ ca_axis_desc_t local_descs[CA_RANK_MAX];
1110
+ memcpy(local_descs, raw_descs, src->ndim * sizeof(ca_axis_desc_t));
1111
+ const void *bound_fill = NULL;
1112
+ if ( ca_func[src->obj_type].attach == ca_window_func.attach ) {
1113
+ bound_fill = ((CAWindow *) src)->fill;
1114
+ }
1115
+ 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);
1120
+ st->alias_mode = CA_ITER_ALIAS_NONE;
1121
+ st->alias_ptr = st->scratch_ptr;
1122
+
1123
+ /* Row-major byte / element strides on the scratch (= view layout). */
1124
+ ca_size_t row_byte_strides[CA_RANK_MAX];
1125
+ ca_size_t row_elem_strides[CA_RANK_MAX];
1126
+ {
1127
+ ca_size_t b = src->bytes, e = 1;
1128
+ for ( k = src->ndim - 1; k >= 0; k-- ) {
1129
+ row_byte_strides[k] = b;
1130
+ row_elem_strides[k] = e;
1131
+ b *= src->dim[k];
1132
+ e *= src->dim[k];
1133
+ }
1134
+ }
1135
+
1136
+ int8_t sp = 0, op = 0;
1137
+ st->slab_elements = 1;
1138
+ for ( k = 0; k < src->ndim; k++ ) {
1139
+ if ( in_slab[k] ) {
1140
+ st->slab_axes_buf[sp] = k;
1141
+ st->slab_dims[sp] = src->dim[k];
1142
+ st->slab_strides[sp] = row_byte_strides[k];
1143
+ st->slab_mask_strides[sp] = row_elem_strides[k];
1144
+ st->slab_elements *= src->dim[k];
1145
+ sp++;
1146
+ } else {
1147
+ st->outer_axes[op] = k;
1148
+ st->outer_dims[op] = src->dim[k];
1149
+ st->outer_strides[op] = row_byte_strides[k];
1150
+ st->outer_mask_strides[op] = row_elem_strides[k];
1151
+ op++;
1152
+ }
1153
+ }
1154
+ st->slab_ndim = sp;
1155
+ st->outer_ndim = op;
1156
+ st->composed_base = 0;
1157
+ /* desc_ndim = 0: next_slab_axes treats outer like Phase A
1158
+ (no INDEX lookup), correct because scratch is row-major
1159
+ contig (= STRIDE everywhere). */
1160
+ st->desc_ndim = 0;
1161
+
1162
+ ca_size_t total = 1;
1163
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) total *= st->outer_dims[m];
1164
+ st->total_slabs = total;
1165
+ st->slab_n = st->slab_elements;
1166
+ st->slabs_emitted = 0;
1167
+ st->chunk_size = st->slab_n;
1168
+
1169
+ if ( st->outer_ndim > 0 ) {
1170
+ CA_ASSUME(st->outer_ndim <= CA_RANK_MAX); /* bound alloc over rank */
1171
+ st->outer_idx = ALLOC_N(ca_size_t, st->outer_ndim);
1172
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) st->outer_idx[m] = 0;
1173
+ } else {
1174
+ st->outer_idx = NULL;
1175
+ }
1176
+
1177
+ if ( ca_has_mask(src) ) {
1178
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
1179
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
1180
+ if ( src->elements > 0 ) {
1181
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
1182
+ }
1183
+ st->alias_mask = st->scratch_mask;
1184
+ }
1185
+ return CA_ITER_OK;
1186
+ }
1187
+
1188
+ memset(st, 0, sizeof(*st));
1189
+ st->src = src;
1190
+ st->src_kind = CA_ITER_SRC_DESCRIPTOR; /* Phase B alias path */
1191
+ st->level = 2;
1192
+ st->policy = policy;
1193
+ st->ndim = src->ndim;
1194
+ st->flags = flags;
1195
+ st->bytes = src->bytes;
1196
+ st->axes = axes;
1197
+ st->naxes = naxes;
1198
+
1199
+ /* Parent row-major byte strides for offset computation. */
1200
+ ca_size_t pstrides[CA_RANK_MAX];
1201
+ {
1202
+ ca_size_t s = src->bytes;
1203
+ for ( k = src->ndim - 1; k >= 0; k-- ) {
1204
+ pstrides[k] = s;
1205
+ s *= raw_pdims[k];
1206
+ }
1207
+ }
1208
+ /* View row-major element strides (= mask scratch layout). */
1209
+ ca_size_t row_elem_strides[CA_RANK_MAX];
1210
+ {
1211
+ ca_size_t s = 1;
1212
+ for ( k = src->ndim - 1; k >= 0; k-- ) {
1213
+ row_elem_strides[k] = s;
1214
+ s *= src->dim[k];
1215
+ }
1216
+ }
1217
+
1218
+ /* Partition + populate slab / outer metadata. Slab axes contribute
1219
+ to a constant slab_base (since they're all STRIDE: start +
1220
+ step*idx, where the base is start*pstride summed). Outer axes
1221
+ drive per-slab offset via direct (start + step*idx)*pstride or
1222
+ indices[idx]*pstride (= no classify_prefix engine, since outer
1223
+ axes here are not a prefix of raw_descs[] — they're a
1224
+ complement of slab_axes). */
1225
+ int8_t sp = 0, op = 0;
1226
+ ca_size_t slab_base = 0;
1227
+ st->slab_elements = 1;
1228
+ for ( k = 0; k < src->ndim; k++ ) {
1229
+ if ( in_slab[k] ) {
1230
+ /* STRIDE-kind slab axis: start*pstride goes to slab_base,
1231
+ step*pstride is the per-cell byte stride. */
1232
+ slab_base += raw_descs[k].start * pstrides[k];
1233
+ st->slab_axes_buf[sp] = k;
1234
+ st->slab_dims[sp] = raw_descs[k].count;
1235
+ st->slab_strides[sp] = raw_descs[k].step * pstrides[k];
1236
+ st->slab_mask_strides[sp] = row_elem_strides[k];
1237
+ st->slab_elements *= raw_descs[k].count;
1238
+ sp++;
1239
+ } else {
1240
+ /* Outer axis: STRIDE or INDEX (SHIFT was rejected above).
1241
+ Store axis index + view's outer dim/stride for descriptor
1242
+ walk in next_slab_axes. We reuse outer_strides/outer_mask
1243
+ _strides as the OUTER walk metadata; outer-axis kind is
1244
+ carried implicitly via raw_descs[outer_axes[m]] in the
1245
+ state struct's descs[] field (populated below). */
1246
+ st->outer_axes[op] = k;
1247
+ st->outer_dims[op] = raw_descs[k].count;
1248
+ /* For STRIDE: per-axis stride = step * pstride; INDEX axes
1249
+ use indices[] from raw_descs and need a 0 stride here
1250
+ (next_slab_axes branches on descs[].kind to compute the
1251
+ real offset). We persist the full raw_descs in st->descs
1252
+ so the next_slab_axes implementation can dispatch. */
1253
+ if ( raw_descs[k].kind == CA_AXIS_KIND_STRIDE ) {
1254
+ st->outer_strides[op] = raw_descs[k].step * pstrides[k];
1255
+ /* Bake in start*pstride into slab_base for STRIDE outer too
1256
+ so per-cell offset is purely step*idx. */
1257
+ slab_base += raw_descs[k].start * pstrides[k];
1258
+ } else {
1259
+ /* INDEX kind: outer_strides[m] = pstrides[k] (element-unit
1260
+ from indices[]). We DON'T pre-bake into slab_base for
1261
+ INDEX axes; per-iter offset = indices[idx[m]] * pstrides[k]
1262
+ is computed in next_slab_axes. */
1263
+ st->outer_strides[op] = pstrides[k];
1264
+ }
1265
+ st->outer_mask_strides[op] = row_elem_strides[k];
1266
+ op++;
1267
+ }
1268
+ }
1269
+ st->slab_ndim = sp;
1270
+ st->outer_ndim = op;
1271
+ st->composed_base = slab_base;
1272
+
1273
+ /* Persist raw_descs / pstrides in state so next_slab_axes can
1274
+ distinguish STRIDE vs INDEX outer per axis. */
1275
+ memcpy(st->descs, raw_descs, src->ndim * sizeof(ca_axis_desc_t));
1276
+ memcpy(st->parent_axis_dims, raw_pdims, src->ndim * sizeof(ca_size_t));
1277
+ memcpy(st->pstrides, pstrides, src->ndim * sizeof(ca_size_t));
1278
+ st->desc_ndim = src->ndim;
1279
+
1280
+ ca_size_t total = 1;
1281
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) total *= st->outer_dims[m];
1282
+ st->total_slabs = total;
1283
+ st->slab_n = st->slab_elements;
1284
+ st->slabs_emitted = 0;
1285
+ st->chunk_size = st->slab_n;
1286
+
1287
+ if ( st->outer_ndim > 0 ) {
1288
+ st->outer_idx = ALLOC_N(ca_size_t, st->outer_ndim);
1289
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) st->outer_idx[m] = 0;
1290
+ } else {
1291
+ st->outer_idx = NULL;
1292
+ }
1293
+
1294
+ /* Attach parent for alias path. */
1295
+ CArray *parent = CAVIEW(src)->parent;
1296
+ ca_attach(parent);
1297
+ st->root = parent;
1298
+ st->alias_mode = CA_ITER_ALIAS_STRIDED;
1299
+ st->alias_ptr = (char *) parent->ptr;
1300
+
1301
+ /* Mask gather (= view row-major boolean8_t). */
1302
+ if ( ca_has_mask(src) ) {
1303
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
1304
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
1305
+ if ( src->elements > 0 ) {
1306
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
1307
+ }
1308
+ st->alias_mask = st->scratch_mask;
1309
+ }
1310
+ return CA_ITER_OK;
1311
+ }
1312
+ if ( src_kind == CA_ITER_SRC_ATTACH ) {
1313
+ /* Phase B.5: CA_SLAB_AXES + SRC_ATTACH (= CAFake / CAByteSwap /
1314
+ CABitfield / CABitarray / CAReduce). These overlay views
1315
+ materialise via their own ca_attach (= no descriptor framework
1316
+ path), so we let ca_attach(src) populate src->ptr in view
1317
+ row-major layout and walk it with Phase A-style K-D strides.
1318
+
1319
+ The most common entry path here is wrap_readonly(int_src,
1320
+ CA_FLOAT64) → CAFake, enabling sum_ki etc. to accept any
1321
+ numeric data_type source via auto-cast (= D2.2 confirmed in
1322
+ rev2 sparring). */
1323
+ if ( axes == NULL || naxes <= 0 || naxes > src->ndim ) {
1324
+ return CA_ITER_ERR_POLICY;
1325
+ }
1326
+ int8_t in_slab[CA_RANK_MAX];
1327
+ int8_t k;
1328
+ for ( k = 0; k < CA_RANK_MAX; k++ ) in_slab[k] = 0;
1329
+ for ( k = 0; k < naxes; k++ ) {
1330
+ int8_t ax = axes[k];
1331
+ if ( ax < 0 || ax >= src->ndim ) return CA_ITER_ERR_POLICY;
1332
+ if ( in_slab[ax] ) return CA_ITER_ERR_POLICY;
1333
+ in_slab[ax] = 1;
1334
+ }
1335
+
1336
+ memset(st, 0, sizeof(*st));
1337
+ st->src = src;
1338
+ st->src_kind = CA_ITER_SRC_ATTACH;
1339
+ st->level = 2;
1340
+ st->policy = policy;
1341
+ st->ndim = src->ndim;
1342
+ st->flags = flags;
1343
+ st->bytes = src->bytes;
1344
+ st->axes = axes;
1345
+ st->naxes = naxes;
1346
+
1347
+ /* Row-major byte / element strides on the attached view buffer.
1348
+ Computed first so the F.6.1 predicate can inspect fiber-axis
1349
+ effective stride before deciding whether to materialise. */
1350
+ ca_size_t row_byte_strides[CA_RANK_MAX];
1351
+ ca_size_t row_elem_strides[CA_RANK_MAX];
1352
+ {
1353
+ ca_size_t b = src->bytes, e = 1;
1354
+ for ( k = src->ndim - 1; k >= 0; k-- ) {
1355
+ row_byte_strides[k] = b;
1356
+ row_elem_strides[k] = e;
1357
+ b *= src->dim[k];
1358
+ e *= src->dim[k];
1359
+ }
1360
+ }
1361
+
1362
+ /* PROPOSAL_CASTACK_LOOP_INTERCHANGE Vector A rev4 (direct per-
1363
+ parent ptr access path + tile cache). Engages when:
1364
+ - source.attach is ca_stack_func.attach (= CAStack identity)
1365
+ - no mask (= horizontal mask propagation per-slab gather is
1366
+ out of scope; falls back to SRC_ATTACH whole-view path)
1367
+ - naxes == 1 && axes[0] == k_axis (= K-axis-only slab, the
1368
+ demand-driving case = reduce along the stacked axis like
1369
+ view.mean(axis: k_axis))
1370
+ init attaches K parents up front (O(1) per entity parent),
1371
+ caches parent->ptr aliases + uniform parent-native byte strides,
1372
+ allocates a slab-sized scratch (= K * bytes) plus a tile cache
1373
+ sized to fit the L1d budget. next_slab_axes refills TILE fibers
1374
+ per K contig parent reads (rev4 2026-06-19) and serves the next
1375
+ TILE-1 calls from the L1d-resident cache, beating SRC_ATTACH
1376
+ across all measured sizes. No size-threshold gate: rev3's gate
1377
+ was a perf trade-off justification that the tile cache erased.
1378
+
1379
+ K.3 (PROPOSAL_CASTACK_K_AXIS, 2026-06-20): engage predicate
1380
+ generalised from `axes[0] == 0` to `axes[0] == k_axis`. The
1381
+ tile cache mechanism + outer_idx -> parent axis mapping are
1382
+ already k_axis-agnostic: outer_idx[m] maps to parent axis m
1383
+ regardless of k_axis position (= for k_axis = 0 outer is stack
1384
+ axes 1..N-1 = parent axes 0..N-2; for k_axis > 0 outer is stack
1385
+ axes [0..k_axis-1, k_axis+1..N-1] = parent axes [0..k_axis-1,
1386
+ k_axis..N-2]; either way outer_idx[m] = parent axis m). */
1387
+ if ( ca_func[src->obj_type].attach == ca_stack_func.attach
1388
+ && !ca_has_mask(src)
1389
+ && naxes == 1 && axes[0] == ((CAStack *) src)->k_axis ) {
1390
+ CAStack *stack = (CAStack *) src;
1391
+ int8_t parent_ndim = src->ndim - 1;
1392
+ int8_t sp = 0, op = 0;
1393
+
1394
+ st->slab_elements = 1;
1395
+ for ( k = 0; k < src->ndim; k++ ) {
1396
+ if ( in_slab[k] ) {
1397
+ st->slab_axes_buf[sp] = k;
1398
+ st->slab_dims[sp] = src->dim[k];
1399
+ st->slab_elements *= src->dim[k];
1400
+ sp++;
1401
+ } else {
1402
+ st->outer_axes[op] = k;
1403
+ st->outer_dims[op] = src->dim[k];
1404
+ st->outer_strides[op] = row_byte_strides[k]; /* unused on STACK path */
1405
+ st->outer_mask_strides[op] = row_elem_strides[k]; /* unused on STACK path */
1406
+ op++;
1407
+ }
1408
+ }
1409
+ st->slab_ndim = sp;
1410
+ st->outer_ndim = op;
1411
+ st->composed_base = 0;
1412
+ st->desc_ndim = 0;
1413
+ /* slab_strides on STACK path: slab_axes == [0], scratch is
1414
+ packed contig over K elements (= K * bytes), so single-axis
1415
+ slab walk uses bytes stride. */
1416
+ st->slab_strides[0] = src->bytes;
1417
+ st->slab_mask_strides[0] = 1; /* harmless on no-mask path */
1418
+
1419
+ ca_size_t total = 1;
1420
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) total *= st->outer_dims[m];
1421
+ st->total_slabs = total;
1422
+ st->slab_n = st->slab_elements;
1423
+ st->slabs_emitted = 0;
1424
+ st->chunk_size = st->slab_n;
1425
+
1426
+ if ( st->outer_ndim > 0 ) {
1427
+ CA_ASSUME(st->outer_ndim <= CA_RANK_MAX); /* bound alloc over rank */
1428
+ st->outer_idx = ALLOC_N(ca_size_t, st->outer_ndim);
1429
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) st->outer_idx[m] = 0;
1430
+ } else {
1431
+ st->outer_idx = NULL;
1432
+ }
1433
+
1434
+ /* Attach K parents and cache their ptr aliases. CAStack
1435
+ guarantees uniform shape (MEMO §3.2) so one set of parent
1436
+ native byte strides covers all K (= parents[0]->dim is
1437
+ canonical). */
1438
+ st->stack_n_parents = stack->n_parents;
1439
+ st->stack_parent_ptrs =
1440
+ (char **) xmalloc(stack->n_parents * sizeof(char *));
1441
+ for ( int32_t kk = 0; kk < stack->n_parents; kk++ ) {
1442
+ ca_attach(stack->parents[kk]);
1443
+ st->stack_parent_ptrs[kk] = (char *) stack->parents[kk]->ptr;
1444
+ }
1445
+ {
1446
+ ca_size_t s = src->bytes;
1447
+ for ( int8_t kk = parent_ndim - 1; kk >= 0; kk-- ) {
1448
+ st->stack_parent_strides[kk] = s;
1449
+ s *= stack->parents[0]->dim[kk];
1450
+ }
1451
+ }
1452
+
1453
+ /* Slab-sized scratch (= K * bytes for slab_axes == [0]). */
1454
+ 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);
1456
+
1457
+ /* Tile cache (pilot/castack-axis0-loop-interchange): refill TILE
1458
+ fibers per K parent reads, serve next TILE-1 fibers from L1d.
1459
+ TILE budget = ~32 KB (half of M2 L1d). Empirically tested
1460
+ values; clamped to [8, 64] to keep the refill loop tight. */
1461
+ {
1462
+ ca_size_t K = (ca_size_t) stack->n_parents;
1463
+ ca_size_t bytes = (ca_size_t) src->bytes;
1464
+ ca_size_t tile = K > 0 ? (32 * 1024) / (K * bytes) : 0;
1465
+ if ( tile < 8 ) tile = 8;
1466
+ if ( tile > 64 ) tile = 64;
1467
+ st->stack_tile_cap = tile;
1468
+ st->stack_tile_pos = 0;
1469
+ st->stack_tile_have = 0; /* force refill on first call */
1470
+ st->stack_tile_cache = (char *) xmalloc(K * tile * bytes);
1471
+ }
1472
+
1473
+ st->alias_mode = CA_ITER_ALIAS_STACK;
1474
+ st->alias_ptr = NULL; /* no whole-view buffer */
1475
+ st->alias_mask = NULL;
1476
+ return CA_ITER_OK;
1477
+ }
1478
+
1479
+ /* PROPOSAL_CASTACK_XFER_OPT_LAYERING P.2 Case A (2026-06-18):
1480
+ CAStack source + slab_axes excludes axis 0 (= K-axis stays in
1481
+ outer iter, e.g. view.mean(axis: 1), view.mean(axis: 2),
1482
+ view.mean(axis: 1, 2)). Each slab corresponds to a region
1483
+ inside ONE parent selected by outer_idx[K_outer_pos]. Engine
1484
+ pre-attaches K parents (+ K parent masks if mask present),
1485
+ caches their ptrs, and next_slab_axes aliases parents[k]->ptr +
1486
+ parent_off directly -- no scratch, no materialise, parent
1487
+ entity case = eager-equivalent memory bandwidth.
1488
+
1489
+ Layering: engine detects CAStack identity by function-pointer
1490
+ comparison and accesses parents[] / mask via ca_func[STACK]
1491
+ operation table where possible. CAStack downcast occurs only
1492
+ for the n_parents / parents[] read in init_l2 (= same scope as
1493
+ rev3 STACK; AC3 forbids parents[] walk in next_slab_axes only).
1494
+ Q1 disposition: option (i) minimal-diff inline in SLAB_AXES
1495
+ branch -- evaluated as smallest diff with acceptable layering
1496
+ (proposal §3.1). */
1497
+ /* STACK_OUTER_K: K-axis stays in outer iter (slab carved out of
1498
+ one parent at a time). K.3 (2026-06-20): engage predicate +
1499
+ stack-axis -> parent-axis mapping generalised for arbitrary
1500
+ k_axis. stack axis s maps to parent axis (s if s < k_axis else
1501
+ s - 1); only the K stack axis (s == k_axis) has no parent
1502
+ counterpart. */
1503
+ if ( ca_func[src->obj_type].attach == ca_stack_func.attach
1504
+ && naxes >= 1 && !in_slab[((CAStack *) src)->k_axis] ) {
1505
+ CAStack *stack = (CAStack *) src;
1506
+ int8_t k_axis = stack->k_axis;
1507
+ int8_t parent_ndim = src->ndim - 1;
1508
+ int8_t sp = 0, op = 0;
1509
+
1510
+ st->slab_elements = 1;
1511
+ for ( k = 0; k < src->ndim; k++ ) {
1512
+ if ( in_slab[k] ) {
1513
+ st->slab_axes_buf[sp] = k;
1514
+ st->slab_dims[sp] = src->dim[k];
1515
+ st->slab_elements *= src->dim[k];
1516
+ /* slab_strides[sp] = parent native byte stride at parent
1517
+ axis (k if k < k_axis else k - 1). k == k_axis cannot
1518
+ appear here -- the engage predicate above excludes it. */
1519
+ sp++;
1520
+ } else {
1521
+ st->outer_axes[op] = k;
1522
+ st->outer_dims[op] = src->dim[k];
1523
+ st->outer_strides[op] = row_byte_strides[k];
1524
+ st->outer_mask_strides[op] = row_elem_strides[k];
1525
+ op++;
1526
+ }
1527
+ }
1528
+ st->slab_ndim = sp;
1529
+ st->outer_ndim = op;
1530
+ st->composed_base = 0;
1531
+ st->desc_ndim = 0;
1532
+
1533
+ /* Locate the K-axis (= stack axis k_axis) within outer_axes. */
1534
+ st->stack_k_outer_pos = -1;
1535
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) {
1536
+ if ( st->outer_axes[m] == k_axis ) {
1537
+ st->stack_k_outer_pos = m;
1538
+ break;
1539
+ }
1540
+ }
1541
+
1542
+ /* Uniform parent native byte strides + parent element strides
1543
+ for mask (= MEMO §3.2 uniform shape across parents,
1544
+ parents[0]->dim is canonical). Mask strides cached so
1545
+ next_slab_axes can compute mask_off without downcasting
1546
+ (= AC3 layering goal). */
1547
+ {
1548
+ ca_size_t sb = src->bytes;
1549
+ ca_size_t se = 1;
1550
+ for ( int8_t kk = parent_ndim - 1; kk >= 0; kk-- ) {
1551
+ st->stack_parent_strides[kk] = sb;
1552
+ st->stack_parent_mask_strides[kk] = se;
1553
+ sb *= stack->parents[0]->dim[kk];
1554
+ se *= stack->parents[0]->dim[kk];
1555
+ }
1556
+ }
1557
+
1558
+ /* Fill slab_strides + slab_mask_strides for slab axes (all of
1559
+ which are parent inner axes since K is in outer). stack ax
1560
+ -> parent ax: s if s < k_axis else s - 1. */
1561
+ for ( int8_t s_i = 0; s_i < st->slab_ndim; s_i++ ) {
1562
+ int8_t stack_ax = st->slab_axes_buf[s_i];
1563
+ int8_t parent_ax = (stack_ax < k_axis) ? stack_ax : (stack_ax - 1);
1564
+ st->slab_strides[s_i] = st->stack_parent_strides[parent_ax];
1565
+ st->slab_mask_strides[s_i] = st->stack_parent_mask_strides[parent_ax];
1566
+ }
1567
+
1568
+ ca_size_t total = 1;
1569
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) total *= st->outer_dims[m];
1570
+ st->total_slabs = total;
1571
+ st->slab_n = st->slab_elements;
1572
+ st->slabs_emitted = 0;
1573
+ st->chunk_size = st->slab_n;
1574
+
1575
+ if ( st->outer_ndim > 0 ) {
1576
+ CA_ASSUME(st->outer_ndim <= CA_RANK_MAX); /* bound alloc over rank */
1577
+ st->outer_idx = ALLOC_N(ca_size_t, st->outer_ndim);
1578
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) st->outer_idx[m] = 0;
1579
+ } else {
1580
+ st->outer_idx = NULL;
1581
+ }
1582
+
1583
+ /* Attach K parents + cache ptr aliases. Symmetric with rev3
1584
+ Case B; for entity parents attach is O(1). */
1585
+ st->stack_n_parents = stack->n_parents;
1586
+ st->stack_parent_ptrs =
1587
+ (char **) xmalloc(stack->n_parents * sizeof(char *));
1588
+ for ( int32_t kk = 0; kk < stack->n_parents; kk++ ) {
1589
+ ca_attach(stack->parents[kk]);
1590
+ st->stack_parent_ptrs[kk] = (char *) stack->parents[kk]->ptr;
1591
+ }
1592
+
1593
+ /* 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. */
1597
+ if ( ca_has_mask(src) ) {
1598
+ st->stack_parent_mask_ptrs =
1599
+ (boolean8_t **) xmalloc(stack->n_parents * sizeof(boolean8_t *));
1600
+ 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;
1604
+ }
1605
+ } else {
1606
+ st->stack_parent_mask_ptrs = NULL;
1607
+ }
1608
+
1609
+ st->scratch_ptr = NULL; /* no scratch: aliasing parent->ptr */
1610
+ st->scratch_cap = 0;
1611
+ st->alias_mode = CA_ITER_ALIAS_STACK_OUTER_K;
1612
+ st->alias_ptr = NULL;
1613
+ st->alias_mask = NULL;
1614
+ return CA_ITER_OK;
1615
+ }
1616
+
1617
+ /* PROPOSAL_FIBER_PER_SOURCE_PATH F.6.1 hook: try per-fiber fused
1618
+ dispatch before whole-view materialise. When predicate fires,
1619
+ skip scratch alloc + xfer_all GET (= per-fiber ca_xfer_stride
1620
+ is called on demand in next_slab_axes). Predicate is stubbed
1621
+ in F.6.1 (returns 0); F.6.2+ enables specific source kinds. */
1622
+ int per_fiber_fused = 0;
1623
+ if ( (flags & CA_KERNEL_FIBER_CONTIG) && naxes == 1 ) {
1624
+ int8_t fiber_ax = axes[0];
1625
+ ca_size_t fiber_ax_stride = row_byte_strides[fiber_ax];
1626
+ if ( ca_iter_should_per_fiber_fused(src, src_kind, fiber_ax,
1627
+ fiber_ax_stride, flags) ) {
1628
+ per_fiber_fused = 1;
1629
+ st->alias_mode = CA_ITER_ALIAS_PER_FIBER_FUSED;
1630
+ st->alias_ptr = NULL; /* no whole-view buffer */
1631
+ st->fiber_axis = fiber_ax;
1632
+ for ( int8_t kk = 0; kk < src->ndim; kk++ ) {
1633
+ st->fiber_native_strides[kk] = row_byte_strides[kk];
1634
+ }
1635
+ }
1636
+ }
1637
+
1638
+ if ( !per_fiber_fused ) {
1639
+ /* 2026-05-31 refactor: iter-owns scratch via ca_xfer_all (was
1640
+ 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
+ }
1646
+ st->alias_mode = CA_ITER_ALIAS_NONE;
1647
+ st->alias_ptr = st->scratch_ptr;
1648
+ }
1649
+
1650
+ int8_t sp = 0, op = 0;
1651
+ st->slab_elements = 1;
1652
+ for ( k = 0; k < src->ndim; k++ ) {
1653
+ if ( in_slab[k] ) {
1654
+ st->slab_axes_buf[sp] = k;
1655
+ st->slab_dims[sp] = src->dim[k];
1656
+ st->slab_strides[sp] = row_byte_strides[k];
1657
+ st->slab_mask_strides[sp] = row_elem_strides[k];
1658
+ st->slab_elements *= src->dim[k];
1659
+ sp++;
1660
+ } else {
1661
+ st->outer_axes[op] = k;
1662
+ st->outer_dims[op] = src->dim[k];
1663
+ st->outer_strides[op] = row_byte_strides[k];
1664
+ st->outer_mask_strides[op] = row_elem_strides[k];
1665
+ op++;
1666
+ }
1667
+ }
1668
+ st->slab_ndim = sp;
1669
+ st->outer_ndim = op;
1670
+ st->composed_base = 0;
1671
+ st->desc_ndim = 0; /* row-major STRIDE everywhere */
1672
+
1673
+ ca_size_t total = 1;
1674
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) total *= st->outer_dims[m];
1675
+ st->total_slabs = total;
1676
+ st->slab_n = st->slab_elements;
1677
+ st->slabs_emitted = 0;
1678
+ st->chunk_size = st->slab_n;
1679
+
1680
+ if ( st->outer_ndim > 0 ) {
1681
+ st->outer_idx = ALLOC_N(ca_size_t, st->outer_ndim);
1682
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) st->outer_idx[m] = 0;
1683
+ } else {
1684
+ st->outer_idx = NULL;
1685
+ }
1686
+
1687
+ if ( ca_has_mask(src) ) {
1688
+ if ( per_fiber_fused ) {
1689
+ /* F.6.1 rev2 §3.3: mask travels with data per-fiber. Mask
1690
+ buffer is delivered through fiber_mask_scratch by
1691
+ next_slab_axes; no whole-view materialise. */
1692
+ st->alias_mask = NULL;
1693
+ } else {
1694
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
1695
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
1696
+ if ( src->elements > 0 ) {
1697
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
1698
+ }
1699
+ st->alias_mask = st->scratch_mask;
1700
+ }
1701
+ }
1702
+ return CA_ITER_OK;
1703
+ }
1704
+ if ( src_kind != CA_ITER_SRC_CASTRIDE ) {
1705
+ /* Any other unclassified kind. */
1706
+ return CA_ITER_ERR_POLICY;
1707
+ }
1708
+ }
1709
+
1710
+ /* F-2 minimal scope: SHIFT outer axes need OOB fill-slab support which
1711
+ is not yet implemented; downgrade to materialise (= existing
1712
+ SRC_DESCRIPTOR path) when present. Future work: allocate a 1-slab
1713
+ fill scratch and yield it on OOB iterations (see rev6 §3.5 deferred). */
1714
+ if ( src_kind == CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE
1715
+ && ca_axis_dispatch_outer_has_shift(raw_descs, raw_ndim) ) {
1716
+ src_kind = CA_ITER_SRC_DESCRIPTOR;
1717
+ }
1718
+
1719
+ /* Step 9 + 2026-05-31 refactor: L2 dispatch over SRC_ATTACH sources
1720
+ (CAFake / CAByteSwap / CABitfield / CABitarray / CAReduce / CAObject
1721
+ / CATile / CARoll). Same shape as L1 SRC_ATTACH: iterator-owns
1722
+ scratch + xfer_all GET/PUT. Yield as one 1-D strided slab. */
1723
+ if ( src_kind == CA_ITER_SRC_ATTACH ) {
1724
+ memset(st, 0, sizeof(*st));
1725
+ st->src = src;
1726
+ st->src_kind = CA_ITER_SRC_ATTACH;
1727
+ st->level = 2;
1728
+ st->policy = policy;
1729
+ st->ndim = 1; /* logical 1-D L2 layout */
1730
+ st->flags = flags;
1731
+ st->bytes = src->bytes;
1732
+ st->axes = axes;
1733
+ st->naxes = naxes;
1734
+
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
+ }
1740
+ st->alias_mode = CA_ITER_ALIAS_NONE;
1741
+ st->alias_ptr = st->scratch_ptr;
1742
+ st->composed_strides[0] = src->bytes;
1743
+ st->composed_base = 0;
1744
+ st->slab_n = src->elements;
1745
+ st->total_slabs = 1;
1746
+ st->slabs_emitted = 0;
1747
+ st->chunk_size = st->slab_n;
1748
+ st->outer_idx = NULL;
1749
+
1750
+ if ( ca_has_mask(src) ) {
1751
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
1752
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
1753
+ if ( src->elements > 0 ) {
1754
+ ca_xfer_all(src->mask, (char *) st->scratch_mask, CA_XFER_GET);
1755
+ }
1756
+ st->alias_mask = st->scratch_mask;
1757
+ }
1758
+ return CA_ITER_OK;
1759
+ }
1760
+
1761
+ /* F-2 (rev6, PROPOSAL_F2_KERNEL_ITERATOR_ALIAS): descriptor L2 alias
1762
+ path. Eligibility (route_source verdict): innermost descriptor axis
1763
+ is STRIDE kind, no outer SHIFT axis (downgraded above when present).
1764
+ Setup: ca_attach(parent), alias_ptr = parent->ptr, no scratch alloc.
1765
+ Each outer-prefix iteration yields a strided slab
1766
+ slab_ptr = parent->ptr + inner_byte_start + outer_prefix_offset
1767
+ slab_n = descs[ndim-1].count
1768
+ slab_stride = descs[ndim-1].step * pstrides[ndim-1]
1769
+ where outer_prefix_offset is computed via ca_axis_dispatch_prefix_offset
1770
+ on the pre-classified prefix[]. next_slab_strided branches on
1771
+ src_kind to dispatch to this offset formula. */
1772
+ if ( src_kind == CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE ) {
1773
+ int8_t nd = raw_ndim;
1774
+ int8_t k;
1775
+
1776
+ memset(st, 0, sizeof(*st));
1777
+ st->src = src;
1778
+ st->src_kind = CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE;
1779
+ st->level = 2;
1780
+ st->policy = policy;
1781
+ st->ndim = nd;
1782
+ st->flags = flags;
1783
+ st->bytes = src->bytes;
1784
+ st->axes = axes;
1785
+ st->naxes = naxes;
1786
+
1787
+ /* Persist descs / parent_axis_dims in state; build parent row-major
1788
+ pstrides so prefix_offset / inner offset share the same byte space. */
1789
+ memcpy(st->descs, raw_descs, nd * sizeof(ca_axis_desc_t));
1790
+ memcpy(st->parent_axis_dims, raw_pdims, nd * sizeof(ca_size_t));
1791
+ st->desc_ndim = nd;
1792
+ {
1793
+ ca_size_t s = src->bytes;
1794
+ for ( k = nd - 1; k >= 0; k-- ) {
1795
+ st->pstrides[k] = s;
1796
+ s *= raw_pdims[k];
1797
+ }
1798
+ }
1799
+
1800
+ /* Inner slab parameters from innermost STRIDE descriptor axis. */
1801
+ ca_size_t inner_start = raw_descs[nd - 1].start * st->pstrides[nd - 1];
1802
+ ca_size_t inner_stride = raw_descs[nd - 1].step * st->pstrides[nd - 1];
1803
+ ca_size_t inner_n = raw_descs[nd - 1].count;
1804
+
1805
+ /* Outer prefix classify (skipped for 1-D source since no outer). */
1806
+ if ( nd > 1 ) {
1807
+ ca_axis_dispatch_classify_prefix(st->descs, st->pstrides, nd - 1, st->prefix);
1808
+ }
1809
+
1810
+ /* Total slabs = Π descs[0..nd-2].count (= src->elements / inner_n). */
1811
+ ca_size_t outer_total = 1;
1812
+ for ( k = 0; k < nd - 1; k++ ) outer_total *= raw_descs[k].count;
1813
+ st->total_slabs = outer_total;
1814
+ st->slab_n = inner_n;
1815
+ st->slabs_emitted = 0;
1816
+ st->chunk_size = inner_n;
1817
+ st->total_elements = src->elements;
1818
+
1819
+ /* composed_base holds inner_byte_start; composed_strides[nd-1] holds
1820
+ inner stride so next_slab_strided's existing inner_st extraction
1821
+ (= composed_strides[nd-1]) works without per-iter recomputation.
1822
+ Outer slots of composed_strides are unused by the descriptor
1823
+ branch in next_slab_strided (= prefix[] drives the offset). */
1824
+ st->composed_base = inner_start;
1825
+ st->composed_strides[nd - 1] = inner_stride;
1826
+
1827
+ if ( nd > 1 ) {
1828
+ CA_ASSUME(nd <= CA_RANK_MAX); /* with nd > 1: nd-1 in [1, CA_RANK_MAX-1] */
1829
+ st->outer_idx = ALLOC_N(ca_size_t, nd - 1);
1830
+ for ( k = 0; k < nd - 1; k++ ) st->outer_idx[k] = 0;
1831
+ } else {
1832
+ st->outer_idx = NULL;
1833
+ }
1834
+
1835
+ CArray *parent = CAVIEW(src)->parent;
1836
+ ca_attach(parent);
1837
+ st->root = parent;
1838
+ st->alias_mode = CA_ITER_ALIAS_STRIDED;
1839
+ st->alias_ptr = (char *) parent->ptr;
1840
+
1841
+ /* Mask: gather into scratch_mask (same as SRC_DESCRIPTOR path).
1842
+ Data alias + mask materialise is per rev6 §4.4 deferred to F-4.c. */
1843
+ if ( ca_has_mask(src) ) {
1844
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
1845
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
1846
+ if ( src->elements > 0 ) {
1847
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
1848
+ }
1849
+ st->alias_mask = st->scratch_mask;
1850
+ }
1851
+ return CA_ITER_OK;
1852
+ }
1853
+
1854
+ /* Sub-step 5.3: L2 dispatch over descriptor sources. The view-family
1855
+ surface prioritises delivering the cells over avoiding a copy: always
1856
+ materialise into a
1857
+ scratch buffer via ca_axis_dispatch_attach and yield a single
1858
+ strided slab (stride = bytes). CASelect/CAMapping always reach
1859
+ here, CSA/CAGrid/CAWindow/CAShift when INDEX/SHIFT axes are
1860
+ present. F-2 (rev6) routes innermost-STRIDE descriptor cases to
1861
+ the L2 alias path above; this block now handles remaining mixed
1862
+ cases (innermost INDEX, outer SHIFT temporarily, etc.). */
1863
+ if ( src_kind == CA_ITER_SRC_DESCRIPTOR ) {
1864
+ /* raw_descs / raw_pdims / raw_ndim were populated by route_source
1865
+ above so we skip the local describe_axes call. Kept locals
1866
+ named raw_* to match the original code. */
1867
+ memset(st, 0, sizeof(*st));
1868
+ st->src = src;
1869
+ st->src_kind = CA_ITER_SRC_DESCRIPTOR;
1870
+ st->level = 2;
1871
+ st->policy = policy;
1872
+ st->ndim = src->ndim;
1873
+ st->flags = flags;
1874
+ st->bytes = src->bytes;
1875
+ st->axes = axes;
1876
+ st->naxes = naxes;
1877
+
1878
+ memcpy(st->parent_axis_dims, raw_pdims, raw_ndim * sizeof(ca_size_t));
1879
+ ca_axis_dispatch_prepare(st->parent_axis_dims, raw_descs, raw_ndim,
1880
+ st->bytes, st->descs, st->pstrides,
1881
+ st->mdim, &st->desc_ndim);
1882
+ ca_axis_dispatch_layout(st->descs, st->pstrides, st->mdim,
1883
+ st->desc_ndim, st->bytes,
1884
+ &st->slab_start, &st->slab_bytes_desc,
1885
+ &st->slab_base);
1886
+ if ( st->slab_start > 0 ) {
1887
+ ca_axis_dispatch_classify_prefix(st->descs, st->pstrides,
1888
+ st->slab_start, st->prefix);
1889
+ }
1890
+ st->total_elements = src->elements;
1891
+
1892
+ /* Materialise via the engine into a scratch buffer (contig
1893
+ layout = single strided run, stride = bytes). */
1894
+ CArray *parent = CAVIEW(src)->parent;
1895
+ ca_attach(parent);
1896
+ st->root = parent;
1897
+ st->scratch_cap = src->elements * src->bytes;
1898
+ const void *bound_fill = NULL;
1899
+ if ( ca_func[src->obj_type].attach == ca_window_func.attach ) {
1900
+ bound_fill = ((CAWindow *) src)->fill;
1901
+ }
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);
1908
+
1909
+ /* Lay out as a single 1-D L2 strided slab: ptr = scratch,
1910
+ n = total_elements, stride = bytes. next_slab_strided reads
1911
+ inner_stride from composed_strides[ndim - 1] and skips the
1912
+ outer loop when outer_idx == NULL, so a 1-D logical layout
1913
+ (st->ndim = 1, composed_strides[0] = bytes) yields exactly one
1914
+ contig run. Note st->ndim diverges from src->ndim here — the
1915
+ iterator's logical ndim is 1, but the view itself can be N-D
1916
+ (the materialised buffer is flat). */
1917
+ st->ndim = 1;
1918
+ st->alias_mode = CA_ITER_ALIAS_NONE;
1919
+ st->alias_ptr = st->scratch_ptr;
1920
+ st->composed_strides[0] = src->bytes;
1921
+ st->composed_base = 0;
1922
+ st->slab_n = src->elements;
1923
+ st->total_slabs = 1;
1924
+ st->slabs_emitted = 0;
1925
+ st->chunk_size = st->slab_n;
1926
+ st->outer_idx = NULL;
1927
+
1928
+ /* Step 6: mask gather, same as L1 descriptor branch. */
1929
+ if ( ca_has_mask(src) ) {
1930
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
1931
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
1932
+ if ( src->elements > 0 ) {
1933
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
1934
+ }
1935
+ st->alias_mask = st->scratch_mask;
1936
+ }
1937
+ return CA_ITER_OK;
1938
+ }
1939
+
1940
+ /* === CAStride / entity L2 path === */
1941
+ memset(st, 0, sizeof(*st));
1942
+ st->src = src;
1943
+ st->src_kind = CA_ITER_SRC_CASTRIDE;
1944
+ st->level = 2;
1945
+ st->policy = policy;
1946
+ st->ndim = src->ndim;
1947
+ st->flags = flags;
1948
+ st->bytes = src->bytes;
1949
+ st->axes = axes;
1950
+ st->naxes = naxes;
1951
+
1952
+ /* Compute composed strides + base. For entity / CAStride contig we
1953
+ synthesise row-major byte strides so next_slab_strided's offset
1954
+ math is uniform across alias modes; for CAStride non-contig we
1955
+ compose leaf strides up to the root entity via the substrate. */
1956
+ int8_t nd = src->ndim;
1957
+ int use_strided = 0;
1958
+ int use_view_scratch = 0;
1959
+ CArray *root = NULL;
1960
+
1961
+ if ( ca_iter_can_alias(src, 1) ) {
1962
+ /* Entity or CAStride contig: stride = row-major bytes, base = 0. */
1963
+ ca_iter_build_rowmajor_strides(st->composed_strides,
1964
+ src->dim, nd, src->bytes);
1965
+ st->composed_base = 0;
1966
+ } else {
1967
+ /* CAStride non-contig: leaf->root compose. */
1968
+ ca_size_t cs[CA_RANK_MAX];
1969
+ ca_size_t base;
1970
+ ca_stride_compose_to_root((CAStride *) src, &root, cs, &base);
1971
+ if ( !ca_root_lends_no_memory(root) ) {
1972
+ memcpy(st->composed_strides, cs, nd * sizeof(ca_size_t));
1973
+ st->composed_base = base;
1974
+ use_strided = 1;
1975
+ } else {
1976
+ /* Root holds nothing to read through: reaching root->ptr costs one
1977
+ whole-root materialise no matter how few cells the view touches —
1978
+ the same cliff the xfer path avoids by asking for a region. Take
1979
+ the view's own region protocol instead: ca_copy_data walks the
1980
+ leaf's request up the chain, so a 1000x1000 slice of a 2014x3040
1981
+ CAObject asks for exactly that block. The gathered buffer is view
1982
+ row-major, so the slab arithmetic below is the contig-alias case
1983
+ unchanged. */
1984
+ root = NULL;
1985
+ ca_iter_build_rowmajor_strides(st->composed_strides,
1986
+ src->dim, nd, src->bytes);
1987
+ st->composed_base = 0;
1988
+ use_view_scratch = 1;
1989
+ }
1990
+ }
1991
+
1992
+ /* Phase A: CA_SLAB_AXES branch. Partition axes into slab vs outer,
1993
+ populate per-axis dims / strides (both data byte strides from
1994
+ composed_strides[] and mask element strides from view row-major
1995
+ dim products), allocate outer_idx if outer_ndim > 0, attach data
1996
+ buffer (alias or strided), gather mask if present.
1997
+
1998
+ Layout invariant: slab_axes_buf and outer_axes are both stored
1999
+ sort-ascending (in source-axis order), so multi-axis CA_SLAB_AXES
2000
+ is canonical regardless of the user's input order. */
2001
+ if ( policy == CA_SLAB_AXES ) {
2002
+ /* Validate axes input. */
2003
+ if ( axes == NULL || naxes <= 0 || naxes > nd ) {
2004
+ return CA_ITER_ERR_POLICY;
2005
+ }
2006
+ int8_t in_slab[CA_RANK_MAX];
2007
+ int8_t k;
2008
+ for ( k = 0; k < CA_RANK_MAX; k++ ) in_slab[k] = 0;
2009
+ for ( k = 0; k < naxes; k++ ) {
2010
+ int8_t ax = axes[k];
2011
+ if ( ax < 0 || ax >= nd ) return CA_ITER_ERR_POLICY;
2012
+ if ( in_slab[ax] ) return CA_ITER_ERR_POLICY; /* duplicate */
2013
+ in_slab[ax] = 1;
2014
+ }
2015
+
2016
+ /* View row-major element strides (= mask scratch layout strides). */
2017
+ ca_size_t row_elem_strides[CA_RANK_MAX];
2018
+ {
2019
+ ca_size_t s = 1;
2020
+ for ( k = nd - 1; k >= 0; k-- ) {
2021
+ row_elem_strides[k] = s;
2022
+ s *= src->dim[k];
2023
+ }
2024
+ }
2025
+
2026
+ /* Partition axes (ascending order). */
2027
+ int8_t sp = 0, op = 0;
2028
+ st->slab_elements = 1;
2029
+ for ( k = 0; k < nd; k++ ) {
2030
+ if ( in_slab[k] ) {
2031
+ st->slab_axes_buf[sp] = k;
2032
+ st->slab_dims[sp] = src->dim[k];
2033
+ st->slab_strides[sp] = st->composed_strides[k];
2034
+ st->slab_mask_strides[sp] = row_elem_strides[k];
2035
+ st->slab_elements *= src->dim[k];
2036
+ sp++;
2037
+ } else {
2038
+ st->outer_axes[op] = k;
2039
+ st->outer_dims[op] = src->dim[k];
2040
+ st->outer_strides[op] = st->composed_strides[k];
2041
+ st->outer_mask_strides[op] = row_elem_strides[k];
2042
+ op++;
2043
+ }
2044
+ }
2045
+ st->slab_ndim = sp;
2046
+ st->outer_ndim = op;
2047
+
2048
+ /* total_slabs = Π outer_dims. Empty product (all-axes case) = 1
2049
+ → single slab = whole array (D1.4 WHOLE-equivalent). */
2050
+ ca_size_t total = 1;
2051
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) total *= st->outer_dims[m];
2052
+ st->total_slabs = total;
2053
+ st->slab_n = st->slab_elements; /* mirror to legacy field */
2054
+ st->slabs_emitted = 0;
2055
+ st->chunk_size = st->slab_n;
2056
+
2057
+ if ( st->outer_ndim > 0 ) {
2058
+ st->outer_idx = ALLOC_N(ca_size_t, st->outer_ndim);
2059
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) st->outer_idx[m] = 0;
2060
+ } else {
2061
+ st->outer_idx = NULL;
2062
+ }
2063
+
2064
+ if ( use_strided ) {
2065
+ st->root = root;
2066
+ ca_attach(root);
2067
+ st->alias_mode = CA_ITER_ALIAS_STRIDED;
2068
+ st->alias_ptr = (char *) root->ptr;
2069
+ } else if ( use_view_scratch ) {
2070
+ 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
+ }
2077
+ st->alias_mode = CA_ITER_ALIAS_NONE;
2078
+ st->alias_ptr = st->scratch_ptr;
2079
+ } else {
2080
+ st->root = NULL;
2081
+ ca_attach(src);
2082
+ st->alias_mode = CA_ITER_ALIAS_CONTIG;
2083
+ st->alias_ptr = (char *) src->ptr;
2084
+ }
2085
+
2086
+ /* Mask: gather to scratch_mask in view row-major order (= same as
2087
+ L1 path). The mask layout matches view->dim row-major, which is
2088
+ what slab_mask_strides / outer_mask_strides walk. */
2089
+ if ( ca_has_mask(src) ) {
2090
+ ca_size_t mcap = src->elements > 0 ? src->elements : 1;
2091
+ st->scratch_mask = (boolean8_t *) xmalloc(mcap);
2092
+ if ( src->elements > 0 ) {
2093
+ ca_copy_data(src->mask, (char *) st->scratch_mask);
2094
+ }
2095
+ st->alias_mask = st->scratch_mask;
2096
+ }
2097
+ return CA_ITER_OK;
2098
+ }
2099
+
2100
+ /* Outer prefix axes [0..ndim-2] drive total_slabs; innermost axis is
2101
+ the slab. 0-d / 1-d sources collapse to a single yield. */
2102
+ if ( nd <= 1 ) {
2103
+ st->total_slabs = 1;
2104
+ st->slab_n = src->elements;
2105
+ st->outer_idx = NULL;
2106
+ } else {
2107
+ ca_size_t total = 1;
2108
+ int8_t k;
2109
+ for ( k = 0; k < nd - 1; k++ ) total *= src->dim[k];
2110
+ st->total_slabs = total;
2111
+ st->slab_n = src->dim[nd - 1];
2112
+ st->outer_idx = ALLOC_N(ca_size_t, nd - 1);
2113
+ for ( k = 0; k < nd - 1; k++ ) st->outer_idx[k] = 0;
2114
+ }
2115
+ st->slabs_emitted = 0;
2116
+ st->chunk_size = st->slab_n;
2117
+
2118
+ if ( use_strided ) {
2119
+ st->root = root;
2120
+ ca_attach(root);
2121
+ st->alias_mode = CA_ITER_ALIAS_STRIDED;
2122
+ st->alias_ptr = (char *) root->ptr;
2123
+ } else if ( use_view_scratch ) {
2124
+ 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
+ }
2131
+ st->alias_mode = CA_ITER_ALIAS_NONE;
2132
+ st->alias_ptr = st->scratch_ptr;
2133
+ } else {
2134
+ st->root = NULL;
2135
+ ca_attach(src);
2136
+ st->alias_mode = CA_ITER_ALIAS_CONTIG;
2137
+ st->alias_ptr = (char *) src->ptr;
2138
+ }
2139
+ return CA_ITER_OK;
2140
+ }
2141
+
2142
+ int
2143
+ ca_iter_state_next_slab (ca_iter_state *st,
2144
+ char **out_ptr,
2145
+ boolean8_t **out_mask,
2146
+ ca_size_t *out_n)
2147
+ {
2148
+ if ( st == NULL || st->level != 1
2149
+ || st->slabs_emitted >= st->total_slabs ) {
2150
+ if ( out_ptr ) *out_ptr = NULL;
2151
+ if ( out_mask ) *out_mask = NULL;
2152
+ if ( out_n ) *out_n = 0;
2153
+ return 0;
2154
+ }
2155
+ /* L1 WHOLE policy: a single slab whose ptr is either parent->ptr
2156
+ (alias) or the scratch buffer. alias_mask is populated in init
2157
+ when the source carries a mask (NULL otherwise). */
2158
+ if ( out_ptr ) *out_ptr = st->alias_ptr;
2159
+ if ( out_mask ) *out_mask = st->alias_mask;
2160
+ if ( out_n ) *out_n = st->slab_n;
2161
+ st->slabs_emitted += 1;
2162
+ return 1;
2163
+ }
2164
+
2165
+ int
2166
+ ca_iter_state_next_slab_strided (ca_iter_state *st,
2167
+ char **out_ptr,
2168
+ boolean8_t **out_mask,
2169
+ ca_size_t *out_n,
2170
+ ca_size_t *out_stride_bytes)
2171
+ {
2172
+ if ( st == NULL || st->level != 2
2173
+ || st->slabs_emitted >= st->total_slabs ) {
2174
+ if ( out_ptr ) *out_ptr = NULL;
2175
+ if ( out_mask ) *out_mask = NULL;
2176
+ if ( out_n ) *out_n = 0;
2177
+ if ( out_stride_bytes ) *out_stride_bytes = 0;
2178
+ return 0;
2179
+ }
2180
+
2181
+ int8_t nd = st->ndim;
2182
+ ca_size_t inner_st = (nd > 0)
2183
+ ? st->composed_strides[nd - 1]
2184
+ : st->bytes;
2185
+ ca_size_t off = st->composed_base;
2186
+ int8_t k;
2187
+
2188
+ /* Sum outer prefix offset. For F-2 descriptor L2 alias the prefix can
2189
+ contain INDEX axes (and, once OOB fill_slab lands, SHIFT axes), so
2190
+ we delegate to the pre-classified prefix[] engine. Otherwise (=
2191
+ CAStride / entity outer = STRIDE-only by construction) use the
2192
+ direct sum that has been the L2 inner loop since step 3. For
2193
+ 0-d / 1-d sources, the loop / call is a no-op and off stays at
2194
+ composed_base. */
2195
+ if ( st->outer_idx != NULL ) {
2196
+ if ( st->src_kind == CA_ITER_SRC_DESCRIPTOR_L2_ALIASABLE ) {
2197
+ int oob = 0;
2198
+ off += ca_axis_dispatch_prefix_offset(st->prefix, st->outer_idx,
2199
+ nd - 1, &oob);
2200
+ /* oob unreachable: outer SHIFT was downgraded in init_l2. */
2201
+ } else {
2202
+ for ( k = 0; k < nd - 1; k++ ) {
2203
+ off += st->outer_idx[k] * st->composed_strides[k];
2204
+ }
2205
+ }
2206
+ }
2207
+
2208
+ if ( out_ptr ) *out_ptr = st->alias_ptr + off;
2209
+ /* mask layout mirrors value layout, but mask is contig boolean8_t
2210
+ when alias_mask is a scratch buffer (step 6 baseline = always
2211
+ gather mask into scratch_mask for uniformity). Per-slab mask
2212
+ offset = i in the outer cursor (= slabs_emitted at this point). */
2213
+ if ( out_mask ) {
2214
+ *out_mask = st->alias_mask
2215
+ ? st->alias_mask + st->slabs_emitted * st->slab_n
2216
+ : NULL;
2217
+ }
2218
+ if ( out_n ) *out_n = st->slab_n;
2219
+ if ( out_stride_bytes ) *out_stride_bytes = inner_st;
2220
+
2221
+ st->slabs_emitted += 1;
2222
+
2223
+ /* Advance outer_idx row-major (least-significant axis innermost,
2224
+ so we tick outer_idx[ndim-2] first). */
2225
+ if ( st->outer_idx != NULL ) {
2226
+ for ( k = nd - 2; k >= 0; k-- ) {
2227
+ if ( ++st->outer_idx[k] < st->src->dim[k] ) break;
2228
+ st->outer_idx[k] = 0;
2229
+ }
2230
+ }
2231
+ return 1;
2232
+ }
2233
+
2234
+ int
2235
+ ca_iter_state_next_slab_axes (ca_iter_state *st,
2236
+ char **out_ptr,
2237
+ boolean8_t **out_mask)
2238
+ {
2239
+ if ( st == NULL || st->policy != CA_SLAB_AXES || st->level != 2
2240
+ || st->slabs_emitted >= st->total_slabs ) {
2241
+ if ( out_ptr ) *out_ptr = NULL;
2242
+ if ( out_mask ) *out_mask = NULL;
2243
+ return 0;
2244
+ }
2245
+
2246
+ /* Phase C T3 specialised path (B-1b, C.1b, 2026-05-27): innermost
2247
+ slab axis is STRIDE, no SHIFT axes anywhere. Hoist outer + non-
2248
+ innermost-slab axes (= per-cell switch evaluated once per slab row),
2249
+ inner = pure STRIDE linear memcpy (= SIMD-friendly contig run, no
2250
+ engine per-cell dispatch). Target: 1.5-1.8x win for INDEX slab
2251
+ with innermost STRIDE (= grid / select sparse projection use cases). */
2252
+ if ( st->alias_mode == CA_ITER_ALIAS_PER_SLAB_HOIST ) {
2253
+ int8_t inner_view_ax = st->slab_axes_buf[st->slab_ndim - 1];
2254
+ ca_size_t inner_count = st->descs[inner_view_ax].count;
2255
+ ca_size_t inner_pstride = st->pstrides[inner_view_ax];
2256
+ ca_size_t inner_byte_step = st->descs[inner_view_ax].step * inner_pstride;
2257
+ ca_size_t inner_byte_base = st->descs[inner_view_ax].start * inner_pstride;
2258
+ ca_size_t bytes = st->bytes;
2259
+
2260
+ /* Outer contribution (hoisted, computed once per next_slab_axes
2261
+ call): walks outer_axes with their kind-specific offset. No
2262
+ SHIFT here (init ruled out SHIFT-anywhere). */
2263
+ ca_size_t outer_off = 0;
2264
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) {
2265
+ int8_t ax = st->outer_axes[m];
2266
+ ca_size_t pos = st->outer_idx[m];
2267
+ if ( st->descs[ax].kind == CA_AXIS_KIND_STRIDE ) {
2268
+ outer_off += (st->descs[ax].start + pos * st->descs[ax].step)
2269
+ * st->pstrides[ax];
2270
+ } else { /* INDEX */
2271
+ outer_off += st->descs[ax].indices[pos] * st->pstrides[ax];
2272
+ }
2273
+ }
2274
+
2275
+ /* Non-innermost slab axes: walk row-major via single linear cursor.
2276
+ For sp = 1 (single slab axis = innermost STRIDE), the outer
2277
+ row-major loop runs once with nonin_off = 0. */
2278
+ int8_t nonin_n = st->slab_ndim - 1;
2279
+ int8_t nonin_view_ax[CA_RANK_MAX];
2280
+ ca_size_t nonin_count[CA_RANK_MAX];
2281
+ ca_size_t nonin_idx[CA_RANK_MAX];
2282
+ ca_size_t nonin_total = 1;
2283
+ for ( int8_t s = 0; s < nonin_n; s++ ) {
2284
+ nonin_view_ax[s] = st->slab_axes_buf[s];
2285
+ nonin_count[s] = st->descs[nonin_view_ax[s]].count;
2286
+ nonin_idx[s] = 0;
2287
+ nonin_total *= nonin_count[s];
2288
+ }
2289
+
2290
+ char *parent_base = (char *) st->root->ptr + outer_off + inner_byte_base;
2291
+ for ( ca_size_t nlin = 0; nlin < nonin_total; nlin++ ) {
2292
+ /* Compute non-innermost-slab contribution at current nonin_idx. */
2293
+ ca_size_t nonin_off = 0;
2294
+ for ( int8_t s = 0; s < nonin_n; s++ ) {
2295
+ int8_t ax = nonin_view_ax[s];
2296
+ ca_size_t pos = nonin_idx[s];
2297
+ if ( st->descs[ax].kind == CA_AXIS_KIND_STRIDE ) {
2298
+ nonin_off += (st->descs[ax].start + pos * st->descs[ax].step)
2299
+ * st->pstrides[ax];
2300
+ } else { /* INDEX */
2301
+ nonin_off += st->descs[ax].indices[pos] * st->pstrides[ax];
2302
+ }
2303
+ }
2304
+
2305
+ /* Inner loop: pure STRIDE linear copy of inner_count cells.
2306
+ For unit-bytes step == bytes (= contig run) the compiler can
2307
+ hoist this into a single memcpy. Otherwise per-cell memcpy
2308
+ with linear stride (= SIMD-friendly). */
2309
+ char *dst = st->scratch_ptr + nlin * inner_count * bytes;
2310
+ char *src_base = parent_base + nonin_off;
2311
+ if ( inner_byte_step == (ca_size_t) bytes ) {
2312
+ memcpy(dst, src_base, inner_count * bytes);
2313
+ } else {
2314
+ for ( ca_size_t i = 0; i < inner_count; i++ ) {
2315
+ memcpy(dst + i * bytes, src_base + i * inner_byte_step, bytes);
2316
+ }
2317
+ }
2318
+
2319
+ /* Advance nonin_idx row-major (last axis ticks fastest). */
2320
+ for ( int8_t s = nonin_n - 1; s >= 0; s-- ) {
2321
+ if ( ++nonin_idx[s] < nonin_count[s] ) break;
2322
+ nonin_idx[s] = 0;
2323
+ }
2324
+ }
2325
+
2326
+ /* Mask offset (= same as fallback path, whole-view mask scratch). */
2327
+ ca_size_t mask_off = 0;
2328
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) {
2329
+ mask_off += st->outer_idx[m] * st->outer_mask_strides[m];
2330
+ }
2331
+
2332
+ if ( out_ptr ) *out_ptr = st->scratch_ptr;
2333
+ if ( out_mask ) *out_mask = st->scratch_mask ? st->scratch_mask + mask_off : NULL;
2334
+
2335
+ st->slabs_emitted += 1;
2336
+ for ( int8_t m = st->outer_ndim - 1; m >= 0; m-- ) {
2337
+ if ( ++st->outer_idx[m] < st->outer_dims[m] ) break;
2338
+ st->outer_idx[m] = 0;
2339
+ }
2340
+ return 1;
2341
+ }
2342
+
2343
+ /* PROPOSAL_CASTACK_XFER_OPT_LAYERING P.2 Case A (2026-06-18): CAStack
2344
+ source + K-axis (k_axis) in outer iter. Each slab aliases a region
2345
+ inside parents[k]->ptr where k = outer_idx[K_outer_pos]. Parent
2346
+ inner byte offset = Σ outer_idx[m] * stack_parent_strides[parent_ax]
2347
+ over all outer axes except the K-axis itself. Mask: parallel alias
2348
+ into parents[k]->mask->ptr + mask_off. Zero copy / zero scratch /
2349
+ parent entity bandwidth.
2350
+
2351
+ K.3 (2026-06-20): parent_ax derivation generalised for arbitrary
2352
+ k_axis -- stack axis s != k_axis maps to parent axis s if
2353
+ s < k_axis else s - 1. */
2354
+ if ( st->alias_mode == CA_ITER_ALIAS_STACK_OUTER_K ) {
2355
+ int8_t kpos = st->stack_k_outer_pos;
2356
+ int32_t k = (int32_t) st->outer_idx[kpos];
2357
+ int8_t k_axis = ((CAStack *) st->src)->k_axis;
2358
+
2359
+ ca_size_t parent_off = 0; /* byte offset within parent data */
2360
+ ca_size_t mask_off = 0; /* element offset within parent mask */
2361
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) {
2362
+ if ( m == kpos ) continue;
2363
+ int8_t stack_ax = st->outer_axes[m];
2364
+ int8_t parent_ax = (stack_ax < k_axis) ? stack_ax : (stack_ax - 1);
2365
+ parent_off += st->outer_idx[m] * st->stack_parent_strides[parent_ax];
2366
+ mask_off += st->outer_idx[m] * st->stack_parent_mask_strides[parent_ax];
2367
+ }
2368
+
2369
+ if ( out_ptr ) *out_ptr = st->stack_parent_ptrs[k] + parent_off;
2370
+ if ( out_mask ) {
2371
+ *out_mask = st->stack_parent_mask_ptrs
2372
+ ? st->stack_parent_mask_ptrs[k] + mask_off
2373
+ : NULL;
2374
+ }
2375
+
2376
+ st->slabs_emitted += 1;
2377
+ for ( int8_t m = st->outer_ndim - 1; m >= 0; m-- ) {
2378
+ if ( ++st->outer_idx[m] < st->outer_dims[m] ) break;
2379
+ st->outer_idx[m] = 0;
2380
+ }
2381
+ return 1;
2382
+ }
2383
+
2384
+ /* PROPOSAL_CASTACK_LOOP_INTERCHANGE Vector A rev2: CAStack direct
2385
+ per-parent ptr access path. For each outer iter, compute the
2386
+ parent inner byte offset (= same across all K parents, uniform
2387
+ shape), then K-fold direct memcpy gather from cached parent ptrs
2388
+ into scratch. No ca_xfer_stride dispatch; per-cell cost = pointer
2389
+ arith + memcpy(bytes). For f64 the memcpy(8) compiles to a single
2390
+ mov, so the inner loop is tight.
2391
+
2392
+ Note: scope-narrowed to slab_axes == [0] at init, so outer_axes are
2393
+ stack axes 1..N-1 = parent axes 0..parent_ndim-1. outer_idx[m]
2394
+ maps directly to parent axis (m) (since the m-th outer axis is
2395
+ stack axis m+1 = parent axis m). */
2396
+ if ( st->alias_mode == CA_ITER_ALIAS_STACK ) {
2397
+ /* pilot/castack-axis0-loop-interchange: tile cache. Outer iter
2398
+ walks parent storage in row-major order so parent_off increments
2399
+ by `bytes` per call monotonically. Refill TILE fibers at once
2400
+ via K contig parent streams (= TILE consecutive cells from each
2401
+ parent), transposed into cache[t][k] layout. Subsequent (TILE-1)
2402
+ next_slab calls alias into the cache. */
2403
+ if ( st->stack_tile_pos >= st->stack_tile_have ) {
2404
+ ca_size_t bytes = st->bytes;
2405
+ ca_size_t parent_off = 0;
2406
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) {
2407
+ parent_off += st->outer_idx[m] * st->stack_parent_strides[m];
2408
+ }
2409
+ ca_size_t want = st->stack_tile_cap;
2410
+ ca_size_t remaining = st->total_slabs - st->slabs_emitted;
2411
+ if ( want > remaining ) want = remaining;
2412
+ st->stack_tile_have = want;
2413
+ st->stack_tile_pos = 0;
2414
+
2415
+ char **pptrs = st->stack_parent_ptrs;
2416
+ int32_t K = st->stack_n_parents;
2417
+ char *cache = st->stack_tile_cache;
2418
+ ca_size_t stride_t = (ca_size_t) K * bytes; /* cache[t][*] stride */
2419
+
2420
+ if ( bytes == 8 ) {
2421
+ /* f64 / i64 hot path: store one cell per inner iter, compiler
2422
+ can keep `src` in a vector reg and stream cleanly. */
2423
+ for ( int32_t kk = 0; kk < K; kk++ ) {
2424
+ const uint64_t *src = (const uint64_t *)
2425
+ (pptrs[kk] + parent_off);
2426
+ uint64_t *dst = (uint64_t *) (cache + (ca_size_t) kk * bytes);
2427
+ for ( ca_size_t t = 0; t < want; t++ ) {
2428
+ *(uint64_t *)((char *) dst + t * stride_t) = src[t];
2429
+ }
2430
+ }
2431
+ } else if ( bytes == 4 ) {
2432
+ for ( int32_t kk = 0; kk < K; kk++ ) {
2433
+ const uint32_t *src = (const uint32_t *)
2434
+ (pptrs[kk] + parent_off);
2435
+ uint32_t *dst = (uint32_t *) (cache + (ca_size_t) kk * bytes);
2436
+ for ( ca_size_t t = 0; t < want; t++ ) {
2437
+ *(uint32_t *)((char *) dst + t * stride_t) = src[t];
2438
+ }
2439
+ }
2440
+ } else {
2441
+ for ( int32_t kk = 0; kk < K; kk++ ) {
2442
+ const char *src = pptrs[kk] + parent_off;
2443
+ char *dst = cache + (ca_size_t) kk * bytes;
2444
+ for ( ca_size_t t = 0; t < want; t++ ) {
2445
+ memcpy(dst + t * stride_t, src + t * bytes, bytes);
2446
+ }
2447
+ }
2448
+ }
2449
+ }
2450
+
2451
+ if ( out_ptr ) {
2452
+ *out_ptr = st->stack_tile_cache
2453
+ + st->stack_tile_pos
2454
+ * (ca_size_t) st->stack_n_parents * st->bytes;
2455
+ }
2456
+ if ( out_mask ) *out_mask = NULL;
2457
+
2458
+ st->stack_tile_pos += 1;
2459
+ st->slabs_emitted += 1;
2460
+ for ( int8_t m = st->outer_ndim - 1; m >= 0; m-- ) {
2461
+ if ( ++st->outer_idx[m] < st->outer_dims[m] ) break;
2462
+ st->outer_idx[m] = 0;
2463
+ }
2464
+ return 1;
2465
+ }
2466
+
2467
+ /* Phase C T3 fallback (C.1, 2026-05-27): per-slab materialise via
2468
+ ca_axis_dispatch_gather with a subset descriptor built by pinning
2469
+ outer axes at the current outer_idx position (D1.2 (A): caller-side
2470
+ subset construction, engine API unchanged). scratch_ptr is sized
2471
+ for one max slab and reused across iters (D1.1 (B)). */
2472
+ if ( st->alias_mode == CA_ITER_ALIAS_PER_SLAB ) {
2473
+ ca_axis_desc_t subset_descs[CA_RANK_MAX];
2474
+ memcpy(subset_descs, st->descs, st->ndim * sizeof(ca_axis_desc_t));
2475
+
2476
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) {
2477
+ int8_t ax = st->outer_axes[m];
2478
+ ca_size_t pos = st->outer_idx[m];
2479
+ switch ( st->descs[ax].kind ) {
2480
+ case CA_AXIS_KIND_STRIDE:
2481
+ subset_descs[ax].start = st->descs[ax].start
2482
+ + pos * st->descs[ax].step;
2483
+ subset_descs[ax].step = 0;
2484
+ subset_descs[ax].count = 1;
2485
+ break;
2486
+ case CA_AXIS_KIND_INDEX:
2487
+ /* Borrow into the indices[] array at offset pos; count=1
2488
+ means engine reads indices[0] which is original indices[pos].
2489
+ No allocation, no mutation of the producer's array. */
2490
+ subset_descs[ax].indices = &st->descs[ax].indices[pos];
2491
+ subset_descs[ax].count = 1;
2492
+ break;
2493
+ case CA_AXIS_KIND_SHIFT:
2494
+ /* SHIFT outer pinned at pos: collapse to a count=1 axis at the
2495
+ projected start. size0 / policy unchanged so engine's
2496
+ bound check + bound_fill writeback still applies if the
2497
+ projected position is OOB. */
2498
+ subset_descs[ax].start = st->descs[ax].start
2499
+ + pos * st->descs[ax].step;
2500
+ subset_descs[ax].step = 0;
2501
+ subset_descs[ax].count = 1;
2502
+ break;
2503
+ }
2504
+ }
2505
+
2506
+ /* CAWindow fill value capture (= same lookup as B.1.5 init). */
2507
+ const void *bound_fill = NULL;
2508
+ if ( ca_func[st->src->obj_type].attach == ca_window_func.attach ) {
2509
+ bound_fill = ((CAWindow *) st->src)->fill;
2510
+ }
2511
+
2512
+ ca_axis_dispatch_gather(st->root, st->parent_axis_dims, subset_descs,
2513
+ st->ndim, st->bytes, st->slab_elements,
2514
+ bound_fill, st->scratch_ptr);
2515
+
2516
+ /* Mask offset into whole-view scratch_mask (T3 path keeps mask
2517
+ layout view-row-major to match slab_mask_strides). */
2518
+ ca_size_t mask_off = 0;
2519
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) {
2520
+ mask_off += st->outer_idx[m] * st->outer_mask_strides[m];
2521
+ }
2522
+
2523
+ if ( out_ptr ) *out_ptr = st->scratch_ptr;
2524
+ if ( out_mask ) *out_mask = st->scratch_mask ? st->scratch_mask + mask_off : NULL;
2525
+
2526
+ st->slabs_emitted += 1;
2527
+ /* Advance outer_idx row-major (innermost outer axis ticks first). */
2528
+ for ( int8_t m = st->outer_ndim - 1; m >= 0; m-- ) {
2529
+ if ( ++st->outer_idx[m] < st->outer_dims[m] ) break;
2530
+ st->outer_idx[m] = 0;
2531
+ }
2532
+ return 1;
2533
+ }
2534
+
2535
+ /* Per-slab base offsets via outer_idx walk. Data offset uses
2536
+ outer_strides (byte units); mask offset uses outer_mask_strides
2537
+ (element units, view row-major). Both are zero when outer_ndim
2538
+ == 0 (= all-axes WHOLE-equivalent case).
2539
+
2540
+ STRIDE outer axes (Phase A SRC_CASTRIDE, and Phase B
2541
+ SRC_DESCRIPTOR STRIDE-kind axes) use outer_idx[m] directly as the
2542
+ multiplier. INDEX outer axes (Phase B SRC_DESCRIPTOR INDEX-kind)
2543
+ need an indices[] lookup: multiplier = descs[axis].indices[outer_idx[m]].
2544
+ Mask offset uses outer_idx[m] directly in both cases (= mask
2545
+ scratch is in view row-major order, outer_idx walks view-axis
2546
+ positions). */
2547
+ ca_size_t data_off = st->composed_base;
2548
+ ca_size_t mask_off = 0;
2549
+ int has_descs = (st->src_kind == CA_ITER_SRC_DESCRIPTOR
2550
+ && st->desc_ndim > 0);
2551
+ for ( int8_t m = 0; m < st->outer_ndim; m++ ) {
2552
+ ca_size_t multiplier = st->outer_idx[m];
2553
+ if ( has_descs ) {
2554
+ int8_t ax = st->outer_axes[m];
2555
+ if ( st->descs[ax].kind == CA_AXIS_KIND_INDEX ) {
2556
+ multiplier = st->descs[ax].indices[st->outer_idx[m]];
2557
+ }
2558
+ }
2559
+ data_off += multiplier * st->outer_strides[m];
2560
+ mask_off += st->outer_idx[m] * st->outer_mask_strides[m];
2561
+ }
2562
+
2563
+ /* PROPOSAL_FIBER_PER_SOURCE_PATH F.6.1: per-fiber fused dispatch.
2564
+ When init_l2 selected CA_ITER_ALIAS_PER_FIBER_FUSED, there is no
2565
+ whole-view buffer to alias from. Build the fiber region from
2566
+ outer_idx + fiber_axis and call ca_xfer_stride(src, ..., GET)
2567
+ directly into fiber_data_scratch. For fused-aware views (X.1
2568
+ OOB-fused / X.4 transform-fused) this routes to a 1-pass per-
2569
+ region path.
2570
+
2571
+ last_data_off carries the fiber region's outer footprint so
2572
+ sync_slab can reconstruct the same region for WRITE PUT. Encode
2573
+ it as the linear outer_idx position scaled by fiber_axis stride
2574
+ equivalence; for PER_FIBER_FUSED sync_slab reads outer_idx state
2575
+ directly rather than data_off so the value is informational. */
2576
+ if ( st->alias_mode == CA_ITER_ALIAS_PER_FIBER_FUSED ) {
2577
+ ca_size_t fiber_n = st->slab_dims[0];
2578
+ ca_size_t need = fiber_n * st->bytes;
2579
+ int8_t fiber_ax = st->fiber_axis;
2580
+ int8_t nd = st->ndim;
2581
+ ca_size_t starts[CA_RANK_MAX];
2582
+ ca_size_t counts[CA_RANK_MAX];
2583
+
2584
+ 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);
2587
+ st->fiber_data_scratch_cap = need;
2588
+ }
2589
+
2590
+ /* Build fiber region: fiber_axis spans the full fiber, all other
2591
+ axes pinned to outer_idx position (count=1). Cache starts[] in
2592
+ state so sync_slab can rebuild the same region for WRITE PUT
2593
+ (= captured BEFORE outer_idx advance below, same hazard pattern
2594
+ as F.1a last_data_off). */
2595
+ {
2596
+ int8_t op = 0;
2597
+ for ( int8_t k = 0; k < nd; k++ ) {
2598
+ if ( k == fiber_ax ) {
2599
+ starts[k] = 0;
2600
+ counts[k] = fiber_n;
2601
+ } else {
2602
+ starts[k] = st->outer_idx ? st->outer_idx[op] : 0;
2603
+ counts[k] = 1;
2604
+ op++;
2605
+ }
2606
+ st->fiber_region_starts[k] = starts[k];
2607
+ }
2608
+ }
2609
+
2610
+ st->last_data_off = 0; /* not used for PER_FIBER_FUSED */
2611
+
2612
+ ca_xfer_stride(st->src, starts, counts, st->fiber_native_strides,
2613
+ st->fiber_data_scratch, CA_XFER_GET);
2614
+
2615
+ char *yield_ptr = st->fiber_data_scratch;
2616
+ boolean8_t *yield_mask = NULL;
2617
+
2618
+ if ( ca_has_mask(st->src) ) {
2619
+ if ( st->fiber_mask_scratch_cap < fiber_n ) {
2620
+ if ( st->fiber_mask_scratch ) xfree(st->fiber_mask_scratch);
2621
+ st->fiber_mask_scratch = (boolean8_t *) xmalloc(fiber_n > 0 ? fiber_n : 1);
2622
+ st->fiber_mask_scratch_cap = fiber_n;
2623
+ }
2624
+ /* Mask uses element strides (= bytes 1 per cell, identity).
2625
+ Reuse fiber_native_strides scaled down by bytes for mask;
2626
+ actually mask is boolean8_t (1 byte per cell), so native
2627
+ strides over src->mask are simply Π dims (= element index
2628
+ strides). Compute on the fly. */
2629
+ ca_size_t mask_strides[CA_RANK_MAX];
2630
+ {
2631
+ ca_size_t s = 1;
2632
+ for ( int8_t k = nd - 1; k >= 0; k-- ) {
2633
+ mask_strides[k] = s;
2634
+ s *= st->src->dim[k];
2635
+ }
2636
+ }
2637
+ ca_xfer_stride(st->src->mask, starts, counts, mask_strides,
2638
+ (char *) st->fiber_mask_scratch, CA_XFER_GET);
2639
+ yield_mask = st->fiber_mask_scratch;
2640
+ }
2641
+
2642
+ if ( out_ptr ) *out_ptr = yield_ptr;
2643
+ if ( out_mask ) *out_mask = yield_mask;
2644
+
2645
+ /* Advance outer_idx (= same logic as default fall-through). */
2646
+ st->slabs_emitted += 1;
2647
+ for ( int8_t m = st->outer_ndim - 1; m >= 0; m-- ) {
2648
+ if ( ++st->outer_idx[m] < st->outer_dims[m] ) break;
2649
+ st->outer_idx[m] = 0;
2650
+ }
2651
+ return 1;
2652
+ }
2653
+
2654
+ /* PROPOSAL_FIBER_DELIVERY F.1a: per-axis fiber contig delivery.
2655
+ For naxes==1 (= single slab axis = fiber) the catalog contract
2656
+ CA_FOR_EACH_FIBER promises contig data delivery. When the fiber
2657
+ is not innermost-contig (= slab_strides[0] != bytes), the engine
2658
+ gathers the fiber into fiber_data_scratch before yielding so the
2659
+ author can write p[i] without stride math.
2660
+
2661
+ Capture last_data_off BEFORE the outer_idx advance below; sync_slab
2662
+ consumes it for WRITE scatter. See header field doc + PROPOSAL
2663
+ §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
+ }
2730
+
2731
+ if ( out_ptr ) *out_ptr = yield_ptr;
2732
+ if ( out_mask ) *out_mask = yield_mask;
2733
+
2734
+ st->slabs_emitted += 1;
2735
+
2736
+ /* Advance outer_idx row-major (innermost outer axis ticks first).
2737
+ No-op when outer_ndim == 0 (= single slab walk). */
2738
+ for ( int8_t m = st->outer_ndim - 1; m >= 0; m-- ) {
2739
+ if ( ++st->outer_idx[m] < st->outer_dims[m] ) break;
2740
+ st->outer_idx[m] = 0;
2741
+ }
2742
+ return 1;
2743
+ }
2744
+
2745
+ void
2746
+ ca_iter_state_sync_slab (ca_iter_state *st)
2747
+ {
2748
+ /* READ walk: nothing to sync. */
2749
+ if ( st == NULL || !(st->flags & CA_KERNEL_WRITE) ) return;
2750
+
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. */
2757
+ if ( st->alias_mode == CA_ITER_ALIAS_STACK
2758
+ || 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
+ st->write_dirty = 0;
2778
+ return;
2779
+ }
2780
+
2781
+ /* ======================================================================
2782
+ * !!! CORRECTNESS HAZARD - DO NOT MOVE !!!
2783
+ *
2784
+ * (PROPOSAL_FIBER_DELIVERY F.1a)
2785
+ *
2786
+ * Per-fiber scratch reuse + scatter correctness depends on the strict
2787
+ * evaluation order of the CA_FOR_EACH_FIBER_* macro sandwich:
2788
+ *
2789
+ * for ( init ; next_slab_axes(k) ; sync_slab(k) ) { body(k) }
2790
+ *
2791
+ * Concretely: sync_slab(fiber k) MUST run BEFORE next_slab_axes(k+1).
2792
+ * The invariant at sync_slab(k) time:
2793
+ *
2794
+ * - st->fiber_data_scratch holds author-written data for fiber k
2795
+ * (= body(k) just modified it, no other call has touched it since)
2796
+ * - st->last_data_off holds the source byte offset for fiber k
2797
+ * (= captured by next_slab_axes(k) before outer_idx advance)
2798
+ *
2799
+ * next_slab_axes(k+1) will overwrite BOTH (= refill scratch + advance
2800
+ * last_data_off) BEFORE body(k+1) starts. Per-fiber scratch reuse
2801
+ * (= only one buffer for all fibers) is correct ONLY because this
2802
+ * sequence holds.
2803
+ *
2804
+ * DO NOT introduce: prefetch of next_slab_axes(k+1), async sync_slab,
2805
+ * sandwich reordering, batched sync, or any pattern that breaks the
2806
+ * (next -> body -> sync -> next -> body -> sync ...) sequence. Per-
2807
+ * fiber scratch reuse becomes UB the moment this invariant is violated.
2808
+ * If lookahead / batching is needed, allocate one scratch per fiber
2809
+ * instead of reusing -- separate phase, separate design.
2810
+ * ====================================================================== */
2811
+
2812
+ /* PROPOSAL_FIBER_PER_SOURCE_PATH F.6.1: per-fiber fused WRITE PUT.
2813
+ When alias_mode == CA_ITER_ALIAS_PER_FIBER_FUSED, the author
2814
+ wrote into fiber_data_scratch and there is no whole-view buffer
2815
+ to xfer_all PUT. Rebuild the same fiber region from cached
2816
+ fiber_region_starts[] (= captured pre-advance in next_slab_axes,
2817
+ same hazard pattern as F.1a last_data_off) and call
2818
+ ca_xfer_stride(src, ..., PUT) which routes through the view's
2819
+ fused PUT path (X.1 / X.4) for 1-pass scatter back. */
2820
+ if ( st->alias_mode == CA_ITER_ALIAS_PER_FIBER_FUSED ) {
2821
+ ca_size_t fiber_n = st->slab_dims[0];
2822
+ int8_t nd = st->ndim;
2823
+ ca_size_t counts[CA_RANK_MAX];
2824
+ for ( int8_t k = 0; k < nd; k++ ) {
2825
+ counts[k] = (k == st->fiber_axis) ? fiber_n : 1;
2826
+ }
2827
+ ca_xfer_stride(st->src, st->fiber_region_starts, counts,
2828
+ st->fiber_native_strides,
2829
+ st->fiber_data_scratch, CA_XFER_PUT);
2830
+ st->write_dirty = 0;
2831
+ return;
2832
+ }
2833
+
2834
+ /* Per-fiber gather path scatter (PROPOSAL_FIBER_DELIVERY F.1a).
2835
+ When next_slab_axes gathered the fiber into fiber_data_scratch
2836
+ (= naxes==1 + slab_strides[0] != bytes), scatter it back to the
2837
+ source layout via the strided write. When the fiber was the
2838
+ contig fast path (= data_step == bytes), the author wrote directly
2839
+ into the source via alias_ptr; no scatter needed. */
2840
+ if ( (st->flags & CA_KERNEL_FIBER_CONTIG)
2841
+ && st->naxes == 1 && st->slab_ndim == 1
2842
+ && st->fiber_data_scratch != NULL ) {
2843
+ ca_size_t n = st->slab_dims[0];
2844
+ ca_size_t data_step = st->slab_strides[0];
2845
+ ca_size_t bytes = st->bytes;
2846
+ if ( data_step != (ca_size_t) bytes ) {
2847
+ char *dst = st->alias_ptr + st->last_data_off;
2848
+ /* ^ captured by next_slab_axes(k) BEFORE
2849
+ * outer_idx advance; see hazard above. */
2850
+ /* Typed scatter helper (ca_iter_substrate.h): same SIMD-friendly
2851
+ loop structure as ca_stride_gather_run, in reverse direction. */
2852
+ ca_stride_scatter_run(dst, st->fiber_data_scratch,
2853
+ bytes, n, data_step);
2854
+ }
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. */
2861
+ }
2862
+
2863
+ /* alias path: kernel wrote through alias_ptr into parent directly
2864
+ (case A semantics, PROPOSAL_T1_WRITE_SEMANTICS.md §(a)). No
2865
+ scatter needed. Applies to all CAStride alias and to
2866
+ STRIDE-only descriptor alias (= descriptor L2 alias future
2867
+ optimisation, not yet enabled — but if it lands, scratch_ptr
2868
+ stays NULL and we no-op correctly). */
2869
+ if ( st->scratch_ptr == NULL ) return;
2870
+
2871
+ /* scratch path: scatter back the materialised buffer into the
2872
+ source view. src_kind chooses the engine: */
2873
+ switch ( st->src_kind ) {
2874
+ case CA_ITER_SRC_CASTRIDE:
2875
+ /* Two producers of CAStride + scratch: L1 non-contig, and the L2
2876
+ 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);
2882
+ break;
2883
+
2884
+ case CA_ITER_SRC_DESCRIPTOR: {
2885
+ /* Sub-step 5.4: descriptor framework scatter back.
2886
+ ca_axis_dispatch_scatter is the P3-landed engine entry that
2887
+ handles per-axis kind (STRIDE / INDEX / SHIFT) gather direction
2888
+ in reverse — INDEX duplicates yield last-write-wins (R5 spec),
2889
+ SHIFT OOB cells in CAWindow FILL policy are skipped (no parent
2890
+ destination), CAShift WRAP/REFLECT bounds map back to interior
2891
+ and are written normally. Iteration order is engine-defined;
2892
+ user kernels must not rely on it.
2893
+
2894
+ scatter wants the pre-merge raw descriptors (the engine re-runs
2895
+ _prepare internally with whatever we give it). init cached
2896
+ the post-merge axes in st->descs for next_slab_strided's
2897
+ offset math; we re-call describe_axes here for the scatter
2898
+ call rather than caching a second copy in the state struct. */
2899
+ ca_axis_desc_t raw_descs[CA_RANK_MAX];
2900
+ ca_size_t raw_pdims[CA_RANK_MAX];
2901
+ int8_t raw_ndim = 0;
2902
+ ca_iter_describe_axes(st->src, raw_descs, raw_pdims, &raw_ndim);
2903
+ ca_axis_dispatch_scatter(st->root /* parent */,
2904
+ raw_pdims,
2905
+ raw_descs, raw_ndim,
2906
+ st->bytes, st->total_elements,
2907
+ st->scratch_ptr);
2908
+ break;
2909
+ }
2910
+ }
2911
+ st->write_dirty = 0;
2912
+ }
2913
+
2914
+ void
2915
+ ca_iter_state_finish (ca_iter_state *st)
2916
+ {
2917
+ if ( st == NULL || st->src == NULL ) {
2918
+ return;
2919
+ }
2920
+ /* composed_strides is inline — no free needed. outer_idx is heap
2921
+ for L2 multi-d sources (NULL on L1 paths and on L2 0/1-d). */
2922
+ if ( st->outer_idx ) {
2923
+ xfree(st->outer_idx);
2924
+ st->outer_idx = NULL;
2925
+ }
2926
+ /* Lifecycle cleanup — orders matter slightly (free scratch before
2927
+ detaching parent so the kernel iterator's resources are released
2928
+ symmetrically with init):
2929
+ - scratch_ptr: owned by iter (CAStride non-contig L1, or
2930
+ descriptor materialise via ca_axis_dispatch_attach). xfree.
2931
+ - root: descriptor parent attached at init (sub-step 5.1+) or
2932
+ CAStride L2 compose-fold root attached at init (step 3).
2933
+ ca_detach.
2934
+ - else (alias paths): src was attached at init, detach. */
2935
+ if ( st->scratch_ptr ) {
2936
+ xfree(st->scratch_ptr);
2937
+ st->scratch_ptr = NULL;
2938
+ st->scratch_cap = 0;
2939
+ }
2940
+ if ( st->scratch_mask ) {
2941
+ xfree(st->scratch_mask);
2942
+ st->scratch_mask = NULL;
2943
+ }
2944
+ /* PROPOSAL_FIBER_DELIVERY F.1a/F.1b: per-fiber scratch lifecycle. */
2945
+ if ( st->fiber_data_scratch ) {
2946
+ xfree(st->fiber_data_scratch);
2947
+ st->fiber_data_scratch = NULL;
2948
+ st->fiber_data_scratch_cap = 0;
2949
+ }
2950
+ if ( st->fiber_mask_scratch ) {
2951
+ xfree(st->fiber_mask_scratch);
2952
+ st->fiber_mask_scratch = NULL;
2953
+ st->fiber_mask_scratch_cap = 0;
2954
+ }
2955
+ /* PROPOSAL_CASTACK_LOOP_INTERCHANGE Vector A rev2 + P.2 Case A: detach
2956
+ K parents (+ K parent masks if cached) and free the cached ptr
2957
+ arrays. Symmetric with init_l2 per-parent ca_attach loop. */
2958
+ if ( st->stack_parent_ptrs ) {
2959
+ CAStack *stack = (CAStack *) st->src;
2960
+ for ( int32_t kk = 0; kk < st->stack_n_parents; kk++ ) {
2961
+ ca_detach(stack->parents[kk]);
2962
+ }
2963
+ xfree(st->stack_parent_ptrs);
2964
+ st->stack_parent_ptrs = NULL;
2965
+ }
2966
+ /* pilot/castack-axis0-loop-interchange: free tile cache. */
2967
+ if ( st->stack_tile_cache ) {
2968
+ xfree(st->stack_tile_cache);
2969
+ st->stack_tile_cache = NULL;
2970
+ st->stack_tile_cap = 0;
2971
+ st->stack_tile_pos = 0;
2972
+ st->stack_tile_have = 0;
2973
+ }
2974
+ if ( st->stack_parent_mask_ptrs ) {
2975
+ CAStack *stack = (CAStack *) st->src;
2976
+ for ( int32_t kk = 0; kk < st->stack_n_parents; kk++ ) {
2977
+ ca_detach(stack->parents[kk]->mask);
2978
+ }
2979
+ xfree(st->stack_parent_mask_ptrs);
2980
+ st->stack_parent_mask_ptrs = NULL;
2981
+ }
2982
+ st->stack_n_parents = 0;
2983
+ if ( st->root ) {
2984
+ ca_detach(st->root);
2985
+ st->root = NULL;
2986
+ } else if ( st->alias_mode == CA_ITER_ALIAS_CONTIG
2987
+ || st->alias_mode == CA_ITER_ALIAS_STRIDED ) {
2988
+ /* alias path (CONTIG/STRIDED for CAStride family): we attached
2989
+ src directly at init, so detach it here.
2990
+ Note: SRC_ATTACH used to be in this list (ALIAS_ATTACH); after
2991
+ the 2026-05-31 refactor it owns its own scratch (alias_mode =
2992
+ NONE, scratch_ptr xfree'd above), so no src.detach needed. */
2993
+ ca_detach(st->src);
2994
+ }
2995
+ st->src = NULL;
2996
+ st->alias_ptr = NULL;
2997
+ }
2998
+
2999
+ #ifdef CARRAY_DEV_BUILD
3000
+ /* ============================================================
3001
+ * smoke surface (dev-only, stripped in release)
3002
+ *
3003
+ * Gated by CARRAY_DEV_BUILD (enabled via `extconf.rb --enable-dev-build`
3004
+ * or `CARRAY_DEV=1 rake build_ext`). These helpers expose internal
3005
+ * engine state to Ruby for spec_ai regression pins. Do not consume
3006
+ * from user code.
3007
+ *
3008
+ * See devel/PROPOSAL_SMOKE_DEV_BUILD_GATE.md
3009
+ * ============================================================ */
3010
+
3011
+ /* ---- Ruby smoke surface --------------------------------------------- */
3012
+
3013
+ /* L1 smoke (steps 1+2): CArray.t1_smoke(ca) -> Hash with
3014
+ rc / slabs / total_elems / ptr_nonnull / alias_mode / data. */
3015
+ static VALUE
3016
+ rb_t1_smoke (VALUE klass, VALUE vsrc)
3017
+ {
3018
+ CArray *src;
3019
+ ca_iter_state st;
3020
+ char *p;
3021
+ ca_size_t n;
3022
+ int rc;
3023
+ int slabs = 0;
3024
+ ca_size_t total_elems = 0;
3025
+ int ptr_nonnull = 0;
3026
+ int alias_mode = CA_ITER_ALIAS_NONE;
3027
+ VALUE result;
3028
+ VALUE data;
3029
+
3030
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3031
+ data = rb_str_new(0, 0);
3032
+
3033
+ rc = ca_iter_state_init_l1(&st, src, CA_SLAB_WHOLE, NULL, 0, 0);
3034
+ if ( rc == CA_ITER_OK ) {
3035
+ while ( ca_iter_state_next_slab(&st, &p, NULL, &n) ) {
3036
+ if ( slabs == 0 && p != NULL ) ptr_nonnull = 1;
3037
+ if ( p != NULL && n > 0 ) rb_str_cat(data, p, n * st.bytes);
3038
+ total_elems += n;
3039
+ slabs++;
3040
+ }
3041
+ alias_mode = st.alias_mode;
3042
+ ca_iter_state_finish(&st);
3043
+ }
3044
+
3045
+ result = rb_hash_new();
3046
+ rb_hash_aset(result, ID2SYM(rb_intern("rc")), INT2NUM(rc));
3047
+ rb_hash_aset(result, ID2SYM(rb_intern("slabs")), INT2NUM(slabs));
3048
+ rb_hash_aset(result, ID2SYM(rb_intern("total_elems")), SIZE2NUM(total_elems));
3049
+ rb_hash_aset(result, ID2SYM(rb_intern("ptr_nonnull")), ptr_nonnull ? Qtrue : Qfalse);
3050
+ rb_hash_aset(result, ID2SYM(rb_intern("alias_mode")), INT2NUM(alias_mode));
3051
+ rb_hash_aset(result, ID2SYM(rb_intern("data")), data);
3052
+ return result;
3053
+ }
3054
+
3055
+ /* L2 smoke (step 3): CArray.t1_smoke_strided(ca) -> Hash with
3056
+ rc => Integer
3057
+ slabs => Integer (= total_slabs)
3058
+ total_elems => Integer
3059
+ alias_mode => Integer
3060
+ data => String — slab bytes reconstructed via the
3061
+ reported (ptr, n, stride_bytes) tuples, in
3062
+ iteration order; should equal view.to_ca.dump_binary
3063
+ strides => Array<Integer> — stride_bytes per yielded slab
3064
+ (constant across yields under WHOLE policy in
3065
+ step 3; surfaced for inspection)
3066
+ On error rc != OK, the walk fields are 0 / empty. */
3067
+ static VALUE
3068
+ rb_t1_smoke_strided (VALUE klass, VALUE vsrc)
3069
+ {
3070
+ CArray *src;
3071
+ ca_iter_state st;
3072
+ char *p;
3073
+ ca_size_t n, stride_bytes;
3074
+ int rc;
3075
+ int slabs = 0;
3076
+ ca_size_t total_elems = 0;
3077
+ int alias_mode = CA_ITER_ALIAS_NONE;
3078
+ VALUE result, data, strides_arr;
3079
+
3080
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3081
+ data = rb_str_new(0, 0);
3082
+ strides_arr = rb_ary_new();
3083
+
3084
+ rc = ca_iter_state_init_l2(&st, src, CA_SLAB_WHOLE, NULL, 0, 0);
3085
+ if ( rc == CA_ITER_OK ) {
3086
+ while ( ca_iter_state_next_slab_strided(&st, &p, NULL, &n, &stride_bytes) ) {
3087
+ if ( p != NULL && n > 0 ) {
3088
+ ca_size_t i;
3089
+ for ( i = 0; i < n; i++ ) {
3090
+ rb_str_cat(data, p + i * stride_bytes, st.bytes);
3091
+ }
3092
+ }
3093
+ total_elems += n;
3094
+ slabs++;
3095
+ rb_ary_push(strides_arr, SIZE2NUM(stride_bytes));
3096
+ }
3097
+ alias_mode = st.alias_mode;
3098
+ ca_iter_state_finish(&st);
3099
+ }
3100
+
3101
+ result = rb_hash_new();
3102
+ rb_hash_aset(result, ID2SYM(rb_intern("rc")), INT2NUM(rc));
3103
+ rb_hash_aset(result, ID2SYM(rb_intern("slabs")), INT2NUM(slabs));
3104
+ rb_hash_aset(result, ID2SYM(rb_intern("total_elems")), SIZE2NUM(total_elems));
3105
+ rb_hash_aset(result, ID2SYM(rb_intern("alias_mode")), INT2NUM(alias_mode));
3106
+ rb_hash_aset(result, ID2SYM(rb_intern("data")), data);
3107
+ rb_hash_aset(result, ID2SYM(rb_intern("strides")), strides_arr);
3108
+ return result;
3109
+ }
3110
+
3111
+ /* Bench-grade L2 sum kernel: total reduction via L2 iteration with no
3112
+ Ruby String materialisation in the hot loop. Use this rather than
3113
+ t1_smoke_strided when measuring the actual L2 dispatch overhead
3114
+ (the smoke variant's rb_str_cat dominates timing for moderate
3115
+ slab counts). Only supports float64 sources for now — the smoke
3116
+ API isn't a public surface and this is bench scaffolding. */
3117
+ static VALUE
3118
+ rb_t1_smoke_sum_strided_f64 (VALUE klass, VALUE vsrc)
3119
+ {
3120
+ CArray *src;
3121
+ ca_iter_state st;
3122
+ char *p;
3123
+ ca_size_t n, stride_bytes, i;
3124
+ int rc;
3125
+ double acc = 0.0;
3126
+
3127
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3128
+ if ( src->data_type != CA_FLOAT64 ) {
3129
+ rb_raise(rb_eTypeError,
3130
+ "t1_smoke_sum_strided_f64 expects a float64 source");
3131
+ }
3132
+
3133
+ rc = ca_iter_state_init_l2(&st, src, CA_SLAB_WHOLE, NULL, 0, 0);
3134
+ if ( rc != CA_ITER_OK ) {
3135
+ rb_raise(rb_eRuntimeError, "ca_iter_state_init L2 failed (rc=%d)", rc);
3136
+ }
3137
+
3138
+ while ( ca_iter_state_next_slab_strided(&st, &p, NULL, &n, &stride_bytes) ) {
3139
+ /* Step 8.1: macro picks contig fast path when stride == sizeof(double),
3140
+ falls back to strided loop otherwise. Removes the SIMD inhibition
3141
+ observed in step 5.5 §10.4.5 on descriptor materialise paths. */
3142
+ CA_L2_FOR_EACH(double, p, n, stride_bytes, dp, {
3143
+ acc += *dp;
3144
+ });
3145
+ }
3146
+ (void) i;
3147
+ ca_iter_state_finish(&st);
3148
+ return DBL2NUM(acc);
3149
+ }
3150
+
3151
+ /* WRITE smoke: in-place fill via L1. Fills every element with `val`
3152
+ using next_slab (alias direct write if cheap, scratch+sync if not).
3153
+ Returns iter rc; on rc != OK src is not modified. */
3154
+ static VALUE
3155
+ rb_t1_smoke_write_fill_f64 (VALUE klass, VALUE vsrc, VALUE vval)
3156
+ {
3157
+ CArray *src;
3158
+ ca_iter_state st;
3159
+ char *p;
3160
+ ca_size_t n, i;
3161
+ double v = NUM2DBL(vval);
3162
+ int rc;
3163
+
3164
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3165
+ if ( src->data_type != CA_FLOAT64 ) {
3166
+ rb_raise(rb_eTypeError, "expects a float64 source");
3167
+ }
3168
+
3169
+ rc = ca_iter_state_init_l1(&st, src, CA_SLAB_WHOLE, NULL, 0,
3170
+ CA_KERNEL_WRITE);
3171
+ if ( rc != CA_ITER_OK ) return INT2NUM(rc);
3172
+
3173
+ while ( ca_iter_state_next_slab(&st, &p, NULL, &n) ) {
3174
+ double *d = (double *) p;
3175
+ for ( i = 0; i < n; i++ ) d[i] = v;
3176
+ ca_iter_state_sync_slab(&st);
3177
+ }
3178
+ ca_iter_state_finish(&st);
3179
+ return INT2NUM(CA_ITER_OK);
3180
+ }
3181
+
3182
+ /* WRITE smoke: in-place fill via L2 strided dispatch. Used by the
3183
+ step 5.5 aggregate bench to round out the matrix (L1 WRITE was
3184
+ already in t1_smoke_write_fill_f64). Kernel walks the strided
3185
+ slab and writes val at every position; sync_slab scatters back. */
3186
+ static VALUE
3187
+ rb_t1_smoke_write_fill_strided_f64 (VALUE klass, VALUE vsrc, VALUE vval)
3188
+ {
3189
+ CArray *src;
3190
+ ca_iter_state st;
3191
+ char *p;
3192
+ ca_size_t n, stride, i;
3193
+ double v = NUM2DBL(vval);
3194
+ int rc;
3195
+
3196
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3197
+ if ( src->data_type != CA_FLOAT64 ) {
3198
+ rb_raise(rb_eTypeError, "expects a float64 source");
3199
+ }
3200
+
3201
+ rc = ca_iter_state_init_l2(&st, src, CA_SLAB_WHOLE, NULL, 0,
3202
+ CA_KERNEL_WRITE);
3203
+ if ( rc != CA_ITER_OK ) return INT2NUM(rc);
3204
+
3205
+ while ( ca_iter_state_next_slab_strided(&st, &p, NULL, &n, &stride) ) {
3206
+ /* Step 8.1: contig fast path via macro (stride == sizeof(double)
3207
+ on materialise scratch, which is the descriptor L2 path that
3208
+ hit +18-22% in step 5.5 — macro removes that overhead). */
3209
+ CA_L2_FOR_EACH(double, p, n, stride, dp, {
3210
+ *dp = v;
3211
+ });
3212
+ ca_iter_state_sync_slab(&st);
3213
+ }
3214
+ (void) i;
3215
+ ca_iter_state_finish(&st);
3216
+ return INT2NUM(CA_ITER_OK);
3217
+ }
3218
+
3219
+ /* WRITE smoke: partial write then ruby raise — used by exception
3220
+ safety tests. Writes the first `raise_at` elements, then raises.
3221
+ Parent is left in a partially-written state (alias path) or
3222
+ unchanged (scratch path, pre-sync). */
3223
+ static VALUE
3224
+ rb_t1_smoke_write_partial_raise_f64 (VALUE klass, VALUE vsrc,
3225
+ VALUE vval, VALUE vraise_at)
3226
+ {
3227
+ CArray *src;
3228
+ ca_iter_state st;
3229
+ char *p;
3230
+ ca_size_t n, i;
3231
+ double v = NUM2DBL(vval);
3232
+ ca_size_t raise_at = NUM2SIZET(vraise_at);
3233
+ int rc;
3234
+
3235
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3236
+ if ( src->data_type != CA_FLOAT64 ) {
3237
+ rb_raise(rb_eTypeError, "expects a float64 source");
3238
+ }
3239
+
3240
+ rc = ca_iter_state_init_l1(&st, src, CA_SLAB_WHOLE, NULL, 0,
3241
+ CA_KERNEL_WRITE);
3242
+ if ( rc != CA_ITER_OK ) {
3243
+ rb_raise(rb_eRuntimeError, "init_l1 failed (rc=%d)", rc);
3244
+ }
3245
+
3246
+ while ( ca_iter_state_next_slab(&st, &p, NULL, &n) ) {
3247
+ double *d = (double *) p;
3248
+ for ( i = 0; i < n; i++ ) {
3249
+ if ( i == raise_at ) {
3250
+ /* Raise without finishing the walk. alias path: writes 0..raise_at-1
3251
+ are now visible to parent. scratch path: scratch has the partial
3252
+ writes but sync_slab was not called, so parent is unchanged. */
3253
+ ca_iter_state_finish(&st); /* release lifecycle */
3254
+ rb_raise(rb_eRuntimeError, "kernel raise at %ld", (long) raise_at);
3255
+ }
3256
+ d[i] = v;
3257
+ }
3258
+ ca_iter_state_sync_slab(&st);
3259
+ }
3260
+ ca_iter_state_finish(&st);
3261
+ return INT2NUM(CA_ITER_OK);
3262
+ }
3263
+
3264
+ /* qsort comparator for double */
3265
+ static int
3266
+ cmp_double (const void *a, const void *b)
3267
+ {
3268
+ double da = *(const double *)a, db = *(const double *)b;
3269
+ if ( da < db ) return -1;
3270
+ if ( da > db ) return 1;
3271
+ return 0;
3272
+ }
3273
+
3274
+ /* WRITE smoke: per-row sort via L2 strided dispatch. For a 2D
3275
+ float64 src of shape [m, n], sort each of the m rows in ascending
3276
+ order. Uses next_slab_strided so each row is a strided slab; the
3277
+ kernel materialises into a tight contig scratch, qsorts, then
3278
+ writes back via the same stride. This exercises L2 WRITE
3279
+ mechanics (multi-slab walk, per-slab fill of strided cells)
3280
+ without over-engineering a strided qsort itself — that would be a
3281
+ Pattern H specialised op, out of step 4 scope (reviewer advice #3).
3282
+ */
3283
+ static VALUE
3284
+ rb_t1_smoke_sort_row_f64 (VALUE klass, VALUE vsrc)
3285
+ {
3286
+ CArray *src;
3287
+ ca_iter_state st;
3288
+ char *p;
3289
+ ca_size_t n, stride_bytes;
3290
+ int rc;
3291
+ double scratch[CA_DIM_MAX > 0 ? 4096 : 4096]; /* row-cap, see below */
3292
+
3293
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3294
+ if ( src->data_type != CA_FLOAT64 ) {
3295
+ rb_raise(rb_eTypeError, "expects a float64 source");
3296
+ }
3297
+
3298
+ rc = ca_iter_state_init_l2(&st, src, CA_SLAB_WHOLE, NULL, 0,
3299
+ CA_KERNEL_WRITE);
3300
+ if ( rc != CA_ITER_OK ) return INT2NUM(rc);
3301
+
3302
+ while ( ca_iter_state_next_slab_strided(&st, &p, NULL, &n, &stride_bytes) ) {
3303
+ if ( n > (ca_size_t) (sizeof(scratch) / sizeof(double)) ) {
3304
+ ca_iter_state_finish(&st);
3305
+ rb_raise(rb_eRuntimeError, "row too large for smoke scratch");
3306
+ }
3307
+ /* strided -> contig scratch. Step 8.1: macro handles the
3308
+ stride == bytes contig fast path automatically. */
3309
+ ca_size_t _sc_i = 0;
3310
+ CA_L2_FOR_EACH(double, p, n, stride_bytes, dp, {
3311
+ scratch[_sc_i++] = *dp;
3312
+ });
3313
+ /* in-place qsort on the contig scratch */
3314
+ qsort(scratch, n, sizeof(double), cmp_double);
3315
+ /* contig scratch -> strided (writes back to parent via alias_ptr,
3316
+ case A direct write). Step 8.1: macro contig fast path. */
3317
+ ca_size_t _wb_i = 0;
3318
+ CA_L2_FOR_EACH(double, p, n, stride_bytes, dp, {
3319
+ *dp = scratch[_wb_i++];
3320
+ });
3321
+ ca_iter_state_sync_slab(&st); /* no-op for L2 alias, by invariant */
3322
+ }
3323
+ ca_iter_state_finish(&st);
3324
+ return INT2NUM(CA_ITER_OK);
3325
+ }
3326
+
3327
+ /* Step 6 smoke: L1 walk that exposes BOTH the value slab and the
3328
+ mask slab. Used to pin "mask is delivered to kernel" semantics.
3329
+ Returns rc + mask_seen (Boolean: was out_mask non-NULL at first
3330
+ yield) + mask_bytes (String: concatenated mask bytes). */
3331
+ static VALUE
3332
+ rb_t1_smoke_with_mask (VALUE klass, VALUE vsrc)
3333
+ {
3334
+ CArray *src;
3335
+ ca_iter_state st;
3336
+ char *p;
3337
+ boolean8_t *m;
3338
+ ca_size_t n;
3339
+ int rc;
3340
+ int mask_seen = 0;
3341
+ VALUE mask_bytes, result;
3342
+
3343
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3344
+ mask_bytes = rb_str_new(0, 0);
3345
+
3346
+ rc = ca_iter_state_init_l1(&st, src, CA_SLAB_WHOLE, NULL, 0, 0);
3347
+ if ( rc != CA_ITER_OK ) {
3348
+ result = rb_hash_new();
3349
+ rb_hash_aset(result, ID2SYM(rb_intern("rc")), INT2NUM(rc));
3350
+ rb_hash_aset(result, ID2SYM(rb_intern("mask_seen")), Qfalse);
3351
+ rb_hash_aset(result, ID2SYM(rb_intern("mask_bytes")), mask_bytes);
3352
+ return result;
3353
+ }
3354
+
3355
+ while ( ca_iter_state_next_slab(&st, &p, &m, &n) ) {
3356
+ if ( m != NULL ) {
3357
+ mask_seen = 1;
3358
+ if ( n > 0 ) rb_str_cat(mask_bytes, (char *) m, n);
3359
+ }
3360
+ }
3361
+ ca_iter_state_finish(&st);
3362
+
3363
+ result = rb_hash_new();
3364
+ rb_hash_aset(result, ID2SYM(rb_intern("rc")), INT2NUM(rc));
3365
+ rb_hash_aset(result, ID2SYM(rb_intern("mask_seen")), mask_seen ? Qtrue : Qfalse);
3366
+ rb_hash_aset(result, ID2SYM(rb_intern("mask_bytes")), mask_bytes);
3367
+ return result;
3368
+ }
3369
+
3370
+ /* Bench-grade smoke for the L1 walk (CAStride + descriptor sources).
3371
+ Runs the full init / next_slab / finish cycle with no Ruby String
3372
+ materialisation in the hot loop — the kernel "consumes" the slab
3373
+ by xoring its first byte into a volatile sink (defeats dead-code
3374
+ elimination, costs nothing measurable on top of the materialise
3375
+ already done inside init). Returns total elements walked as an
3376
+ Integer. Use this when comparing against view.to_ca; the regular
3377
+ t1_smoke includes an rb_str_cat that doubles the materialise
3378
+ memcpy cost. */
3379
+ static VALUE
3380
+ rb_t1_smoke_attach (VALUE klass, VALUE vsrc)
3381
+ {
3382
+ CArray *src;
3383
+ ca_iter_state st;
3384
+ char *p;
3385
+ ca_size_t n;
3386
+ int rc;
3387
+ ca_size_t total = 0;
3388
+ volatile char sink = 0;
3389
+
3390
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3391
+ rc = ca_iter_state_init_l1(&st, src, CA_SLAB_WHOLE, NULL, 0, 0);
3392
+ if ( rc != CA_ITER_OK ) return INT2NUM(rc);
3393
+
3394
+ while ( ca_iter_state_next_slab(&st, &p, NULL, &n) ) {
3395
+ if ( p != NULL && n > 0 ) sink ^= p[0];
3396
+ total += n;
3397
+ }
3398
+ (void) sink;
3399
+ ca_iter_state_finish(&st);
3400
+ return SIZE2NUM(total);
3401
+ }
3402
+
3403
+ /* L2 bench-grade smoke (sub-step 5.3+). Same shape as t1_smoke_attach
3404
+ but uses init_l2 / next_slab_strided so descriptor sources can be
3405
+ exercised through the L2 path. */
3406
+ static VALUE
3407
+ rb_t1_smoke_attach_strided (VALUE klass, VALUE vsrc)
3408
+ {
3409
+ CArray *src;
3410
+ ca_iter_state st;
3411
+ char *p;
3412
+ ca_size_t n, stride;
3413
+ int rc;
3414
+ ca_size_t total = 0;
3415
+ volatile char sink = 0;
3416
+
3417
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3418
+ rc = ca_iter_state_init_l2(&st, src, CA_SLAB_WHOLE, NULL, 0, 0);
3419
+ if ( rc != CA_ITER_OK ) return INT2NUM(rc);
3420
+
3421
+ while ( ca_iter_state_next_slab_strided(&st, &p, NULL, &n, &stride) ) {
3422
+ if ( p != NULL && n > 0 ) sink ^= p[0];
3423
+ total += n;
3424
+ }
3425
+ (void) sink;
3426
+ ca_iter_state_finish(&st);
3427
+ return SIZE2NUM(total);
3428
+ }
3429
+
3430
+ /* step 7: minimal smoke that exposes the `flags` argument to Ruby
3431
+ tests. Just runs init_l1 with the requested flags and returns
3432
+ the rc — used to verify NO_MASK enforcement (and any future
3433
+ flag-gated rejection paths) without needing a full kernel walk. */
3434
+ static VALUE
3435
+ rb_t1_smoke_init_rc (VALUE klass, VALUE vsrc, VALUE vflags)
3436
+ {
3437
+ CArray *src;
3438
+ ca_iter_state st;
3439
+ int rc;
3440
+ uint32_t flags = (uint32_t) NUM2UINT(vflags);
3441
+
3442
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3443
+ rc = ca_iter_state_init_l1(&st, src, CA_SLAB_WHOLE, NULL, 0, flags);
3444
+ if ( rc == CA_ITER_OK ) ca_iter_state_finish(&st);
3445
+ return INT2NUM(rc);
3446
+ }
3447
+
3448
+ /* ---- Phase A capstone: CA_SLAB_AXES smoke -------------------------
3449
+ sum a float64 source over user-specified slab axes via init_l2 +
3450
+ next_slab_axes. K-D walk over slab dims using slab_strides for data
3451
+ and slab_mask_strides for mask (so non-contig CAStride sources work
3452
+ too). Returns total sum as a Float. Variadic int axes arg
3453
+ (= matches CArray#sum surface):
3454
+ CArray.t1_smoke_sum_axes_f64(ca, 0)
3455
+ CArray.t1_smoke_sum_axes_f64(ca, 0, 2)
3456
+ */
3457
+ static VALUE
3458
+ rb_t1_smoke_sum_axes_f64 (int argc, VALUE *argv, VALUE klass)
3459
+ {
3460
+ CArray *src;
3461
+ ca_iter_state st;
3462
+ char *p;
3463
+ boolean8_t *m;
3464
+ int rc;
3465
+ int8_t slab_axes[CA_RANK_MAX];
3466
+ int8_t naxes;
3467
+ double acc = 0.0;
3468
+ int i;
3469
+
3470
+ if ( argc < 2 ) {
3471
+ rb_raise(rb_eArgError, "t1_smoke_sum_axes_f64 expects (ca, axis...)");
3472
+ }
3473
+ TypedData_Get_Struct(argv[0], CArray, &carray_data_type, src);
3474
+ if ( src->data_type != CA_FLOAT64 ) {
3475
+ rb_raise(rb_eTypeError, "t1_smoke_sum_axes_f64 expects a float64 source");
3476
+ }
3477
+ naxes = (int8_t) (argc - 1);
3478
+ if ( naxes > CA_RANK_MAX ) {
3479
+ rb_raise(rb_eArgError, "too many axes");
3480
+ }
3481
+ for ( i = 0; i < naxes; i++ ) {
3482
+ slab_axes[i] = (int8_t) NUM2INT(argv[i + 1]);
3483
+ }
3484
+
3485
+ rc = ca_iter_state_init_l2(&st, src, CA_SLAB_AXES, slab_axes, naxes, 0);
3486
+ if ( rc != CA_ITER_OK ) {
3487
+ rb_raise(rb_eRuntimeError, "init_l2 CA_SLAB_AXES failed rc=%d", rc);
3488
+ }
3489
+
3490
+ while ( ca_iter_state_next_slab_axes(&st, &p, &m) ) {
3491
+ /* K-D walk over slab via outer_idx-style iteration over slab_dims.
3492
+ For simplicity / correctness, use a flat counter that decomposes
3493
+ into per-axis indices (= general-purpose walk, no SIMD pattern). */
3494
+ ca_size_t cell_count = st.slab_elements;
3495
+ ca_size_t idx[CA_RANK_MAX] = { 0 };
3496
+ for ( ca_size_t c = 0; c < cell_count; c++ ) {
3497
+ ca_size_t data_off = 0;
3498
+ ca_size_t mask_off = 0;
3499
+ for ( int8_t k = 0; k < st.slab_ndim; k++ ) {
3500
+ data_off += idx[k] * st.slab_strides[k];
3501
+ mask_off += idx[k] * st.slab_mask_strides[k];
3502
+ }
3503
+ if ( m == NULL || !m[mask_off] ) {
3504
+ acc += *(double *)(p + data_off);
3505
+ }
3506
+ /* advance idx row-major (innermost first) */
3507
+ for ( int8_t k = st.slab_ndim - 1; k >= 0; k-- ) {
3508
+ if ( ++idx[k] < st.slab_dims[k] ) break;
3509
+ idx[k] = 0;
3510
+ }
3511
+ }
3512
+ }
3513
+ ca_iter_state_finish(&st);
3514
+ return DBL2NUM(acc);
3515
+ }
3516
+
3517
+ /* ---- Phase B.3 helper smoke: rb_ca_parse_reduce_axes ---------------
3518
+ Calls the helper and returns the parsed axes as a Ruby Array of
3519
+ Integer. Lets tests inspect parsing behaviour directly.
3520
+ Signature: CArray.t1_test_parse_reduce_axes(ca, *args) */
3521
+ static VALUE
3522
+ rb_t1_test_parse_reduce_axes (int argc, VALUE *argv, VALUE klass)
3523
+ {
3524
+ CArray *ca;
3525
+ int8_t axes[CA_RANK_MAX];
3526
+ int8_t naxes;
3527
+ int i;
3528
+
3529
+ if ( argc < 1 ) {
3530
+ rb_raise(rb_eArgError,
3531
+ "t1_test_parse_reduce_axes expects (ca, *axis_args)");
3532
+ }
3533
+ TypedData_Get_Struct(argv[0], CArray, &carray_data_type, ca);
3534
+ naxes = rb_ca_parse_reduce_axes(argc - 1, argv + 1, ca, axes);
3535
+
3536
+ VALUE arr = rb_ary_new_capa(naxes);
3537
+ for ( i = 0; i < naxes; i++ ) {
3538
+ rb_ary_push(arr, INT2NUM((int) axes[i]));
3539
+ }
3540
+ return arr;
3541
+ }
3542
+
3543
+ /* ---- API harmonisation A.1 smoke: rb_ca_parse_reduce_axes_kw -------
3544
+ Calls the kwarg helper and returns the parsed axes as a Ruby Array.
3545
+ Lets tests inspect kwarg-form parsing (Qnil / Integer / Array) +
3546
+ validation parity with the variadic entry.
3547
+ Signature: CArray.test_parse_reduce_axes_kw(ca, axis: ...) */
3548
+ static VALUE
3549
+ rb_test_parse_reduce_axes_kw (int argc, VALUE *argv, VALUE klass)
3550
+ {
3551
+ CArray *ca;
3552
+ int8_t axes[CA_RANK_MAX];
3553
+ int8_t naxes;
3554
+ int i;
3555
+ VALUE ca_obj, kw_hash, axis_val = Qnil;
3556
+
3557
+ rb_scan_args(argc, argv, "1:", &ca_obj, &kw_hash);
3558
+ TypedData_Get_Struct(ca_obj, CArray, &carray_data_type, ca);
3559
+
3560
+ rb_scan_options(kw_hash, "axis", &axis_val);
3561
+
3562
+ naxes = rb_ca_parse_reduce_axes_kw(axis_val, ca, axes);
3563
+
3564
+ VALUE arr = rb_ary_new_capa(naxes);
3565
+ for ( i = 0; i < naxes; i++ ) {
3566
+ rb_ary_push(arr, INT2NUM((int) axes[i]));
3567
+ }
3568
+ return arr;
3569
+ }
3570
+
3571
+ /* ---- Phase A.3 helper smoke: rb_ca_new_reduced ---------------------
3572
+ Exposes rb_ca_new_reduced (carray_core.c) for unit testing. Returns
3573
+ the newly allocated output CArray (= tests verify shape + data_type).
3574
+ Signature: CArray.t1_test_new_reduced(ca, data_type, axis1, axis2, ...) */
3575
+ static VALUE
3576
+ rb_t1_test_new_reduced (int argc, VALUE *argv, VALUE klass)
3577
+ {
3578
+ int8_t axes[CA_RANK_MAX];
3579
+ int8_t naxes;
3580
+ int32_t data_type;
3581
+
3582
+ if ( argc < 3 ) {
3583
+ rb_raise(rb_eArgError,
3584
+ "t1_test_new_reduced expects (ca, data_type, axis...) with >= 1 axis");
3585
+ }
3586
+ /* PROPOSAL_DTYPE_SYMBOL_FLIP: accept Symbol / Integer / Class / String
3587
+ uniformly via rb_ca_guess_type so post-flip CA_* (Symbol) callers
3588
+ and legacy Integer-code callers both work. */
3589
+ data_type = (int32_t) rb_ca_guess_type(argv[1]);
3590
+ naxes = (int8_t) (argc - 2);
3591
+ if ( naxes > CA_RANK_MAX ) {
3592
+ rb_raise(rb_eArgError, "too many axes");
3593
+ }
3594
+ for ( int i = 0; i < naxes; i++ ) {
3595
+ axes[i] = (int8_t) NUM2INT(argv[2 + i]);
3596
+ }
3597
+ return rb_ca_new_reduced(argv[0], axes, naxes, data_type, 0);
3598
+ }
3599
+
3600
+ /* ---- step 9.3: CAReduce-specific smoke -----------------------------
3601
+ CAReduce has no public Ruby ctor (it's an internal class used by
3602
+ CARefer mask handling). Tests need to drive kernel_iterator over
3603
+ a CAReduce, so we expose a thin construction helper plus dedicated
3604
+ read / write smokes. All three accept a boolean parent — CAReduce
3605
+ is fixed to CA_BOOLEAN (ca_obj_reduce.c:71). */
3606
+
3607
+ static VALUE
3608
+ rb_t1_make_reduce (VALUE klass, VALUE vparent, VALUE vcount, VALUE voffset)
3609
+ {
3610
+ CArray *parent;
3611
+ TypedData_Get_Struct(vparent, CArray, &carray_data_type, parent);
3612
+ ca_size_t count = NUM2SIZE(vcount);
3613
+ ca_size_t offset = NUM2SIZE(voffset);
3614
+ CArray *r = (CArray *) ca_reduce_new(parent, count, offset);
3615
+ return ca_wrap_struct(r);
3616
+ }
3617
+
3618
+ /* READ smoke for CAReduce: returns Hash mirroring t1_smoke (rc / slabs /
3619
+ total_elems / alias_mode / data) so binary parity is checkable. */
3620
+ static VALUE
3621
+ rb_t1_smoke_reduce_read (VALUE klass, VALUE vparent, VALUE vcount, VALUE voffset)
3622
+ {
3623
+ CArray *parent;
3624
+ ca_iter_state st;
3625
+ char *p;
3626
+ ca_size_t n;
3627
+ int rc;
3628
+ int slabs = 0;
3629
+ ca_size_t total_elems = 0;
3630
+ int alias_mode = CA_ITER_ALIAS_NONE;
3631
+ VALUE result, data;
3632
+ CArray *reduce;
3633
+
3634
+ TypedData_Get_Struct(vparent, CArray, &carray_data_type, parent);
3635
+ ca_size_t count = NUM2SIZE(vcount);
3636
+ ca_size_t offset = NUM2SIZE(voffset);
3637
+ reduce = (CArray *) ca_reduce_new(parent, count, offset);
3638
+ data = rb_str_new(0, 0);
3639
+
3640
+ rc = ca_iter_state_init_l1(&st, reduce, CA_SLAB_WHOLE, NULL, 0, 0);
3641
+ if ( rc == CA_ITER_OK ) {
3642
+ while ( ca_iter_state_next_slab(&st, &p, NULL, &n) ) {
3643
+ if ( p != NULL && n > 0 ) rb_str_cat(data, p, n * st.bytes);
3644
+ total_elems += n;
3645
+ slabs++;
3646
+ }
3647
+ alias_mode = st.alias_mode;
3648
+ ca_iter_state_finish(&st);
3649
+ }
3650
+ /* reduce was allocated via ALLOC (ca_reduce_new) but not wrapped in
3651
+ a Ruby VALUE, so it would leak. ca_free dispatches to the view's
3652
+ free_object which already xfrees the struct (ca_obj_reduce.c:113). */
3653
+ ca_free(reduce);
3654
+
3655
+ result = rb_hash_new();
3656
+ rb_hash_aset(result, ID2SYM(rb_intern("rc")), INT2NUM(rc));
3657
+ rb_hash_aset(result, ID2SYM(rb_intern("slabs")), INT2NUM(slabs));
3658
+ rb_hash_aset(result, ID2SYM(rb_intern("total_elems")), SIZE2NUM(total_elems));
3659
+ rb_hash_aset(result, ID2SYM(rb_intern("alias_mode")), INT2NUM(alias_mode));
3660
+ rb_hash_aset(result, ID2SYM(rb_intern("data")), data);
3661
+ return result;
3662
+ }
3663
+
3664
+ /* WRITE broadcast smoke for CAReduce: fills the reduce view with
3665
+ `val` (boolean / 0 or 1) and lets sync_slab → ca_sync run the
3666
+ broadcast scatter back to parent. After the call parent's
3667
+ [offset..offset+elems*count) bytes should all equal val. Returns
3668
+ rc; on rc != OK parent is not modified. */
3669
+ static VALUE
3670
+ rb_t1_smoke_reduce_write_broadcast (VALUE klass, VALUE vparent,
3671
+ VALUE vcount, VALUE voffset, VALUE vval)
3672
+ {
3673
+ CArray *parent;
3674
+ ca_iter_state st;
3675
+ char *p;
3676
+ ca_size_t n, i;
3677
+ int rc;
3678
+ CArray *reduce;
3679
+ uint8_t v = (uint8_t) NUM2UINT(vval);
3680
+
3681
+ TypedData_Get_Struct(vparent, CArray, &carray_data_type, parent);
3682
+ ca_size_t count = NUM2SIZE(vcount);
3683
+ ca_size_t offset = NUM2SIZE(voffset);
3684
+ reduce = (CArray *) ca_reduce_new(parent, count, offset);
3685
+
3686
+ rc = ca_iter_state_init_l1(&st, reduce, CA_SLAB_WHOLE, NULL, 0,
3687
+ CA_KERNEL_WRITE);
3688
+ if ( rc != CA_ITER_OK ) {
3689
+ ca_free(reduce);
3690
+ return INT2NUM(rc);
3691
+ }
3692
+ while ( ca_iter_state_next_slab(&st, &p, NULL, &n) ) {
3693
+ for ( i = 0; i < n; i++ ) p[i] = v;
3694
+ ca_iter_state_sync_slab(&st); /* triggers ca_sync → broadcast */
3695
+ }
3696
+ ca_iter_state_finish(&st);
3697
+ ca_free(reduce);
3698
+ return INT2NUM(CA_ITER_OK);
3699
+ }
3700
+
3701
+ /* ---- M.6: CARemap kernel_iterator smoke helpers --------------------
3702
+ CARemap has no public Ruby ctor (internal-only per §2.2), so we
3703
+ construct it from C using the (ref, idx) args. Pattern mirrors
3704
+ rb_t1_smoke_reduce_*: build the view, run the iterator state
3705
+ machine, return the standard hash (rc / slabs / total_elems /
3706
+ alias_mode / data). Expected alias_mode = ALIAS_NONE since
3707
+ SRC_ATTACH always materialises via xfer_all into iter-owned
3708
+ scratch. */
3709
+
3710
+ static VALUE
3711
+ rb_t1_smoke_remap_read (VALUE klass, VALUE vref, VALUE vidx)
3712
+ {
3713
+ CArray *ref, *idx, *view;
3714
+ ca_iter_state st;
3715
+ char *p;
3716
+ ca_size_t n;
3717
+ int rc;
3718
+ int slabs = 0;
3719
+ ca_size_t total_elems = 0;
3720
+ int alias_mode = CA_ITER_ALIAS_NONE;
3721
+ VALUE result, data;
3722
+
3723
+ (void) klass;
3724
+ TypedData_Get_Struct(vref, CArray, &carray_data_type, ref);
3725
+ TypedData_Get_Struct(vidx, CArray, &carray_data_type, idx);
3726
+ view = ca_remap_new(ref, idx);
3727
+ data = rb_str_new(0, 0);
3728
+
3729
+ rc = ca_iter_state_init_l1(&st, view, CA_SLAB_WHOLE, NULL, 0, 0);
3730
+ if ( rc == CA_ITER_OK ) {
3731
+ while ( ca_iter_state_next_slab(&st, &p, NULL, &n) ) {
3732
+ if ( p != NULL && n > 0 ) rb_str_cat(data, p, n * st.bytes);
3733
+ total_elems += n;
3734
+ slabs++;
3735
+ }
3736
+ alias_mode = st.alias_mode;
3737
+ ca_iter_state_finish(&st);
3738
+ }
3739
+ ca_free(view);
3740
+
3741
+ result = rb_hash_new();
3742
+ rb_hash_aset(result, ID2SYM(rb_intern("rc")), INT2NUM(rc));
3743
+ rb_hash_aset(result, ID2SYM(rb_intern("slabs")), INT2NUM(slabs));
3744
+ rb_hash_aset(result, ID2SYM(rb_intern("total_elems")), SIZE2NUM(total_elems));
3745
+ rb_hash_aset(result, ID2SYM(rb_intern("alias_mode")), INT2NUM(alias_mode));
3746
+ rb_hash_aset(result, ID2SYM(rb_intern("data")), data);
3747
+ return result;
3748
+ }
3749
+
3750
+ static VALUE
3751
+ rb_t1_smoke_remap_read_strided (VALUE klass, VALUE vref, VALUE vidx)
3752
+ {
3753
+ CArray *ref, *idx, *view;
3754
+ ca_iter_state st;
3755
+ char *p;
3756
+ ca_size_t n;
3757
+ int rc;
3758
+ int slabs = 0;
3759
+ ca_size_t total_elems = 0;
3760
+ int alias_mode = CA_ITER_ALIAS_NONE;
3761
+ VALUE result, data;
3762
+
3763
+ (void) klass;
3764
+ TypedData_Get_Struct(vref, CArray, &carray_data_type, ref);
3765
+ TypedData_Get_Struct(vidx, CArray, &carray_data_type, idx);
3766
+ view = ca_remap_new(ref, idx);
3767
+ data = rb_str_new(0, 0);
3768
+
3769
+ rc = ca_iter_state_init_l2(&st, view, CA_SLAB_WHOLE, NULL, 0, 0);
3770
+ if ( rc == CA_ITER_OK ) {
3771
+ ca_size_t stride;
3772
+ while ( ca_iter_state_next_slab_strided(&st, &p, NULL, &n, &stride) ) {
3773
+ ca_size_t i;
3774
+ for ( i = 0; i < n; i++ ) {
3775
+ rb_str_cat(data, p + i * stride, st.bytes);
3776
+ }
3777
+ total_elems += n;
3778
+ slabs++;
3779
+ }
3780
+ alias_mode = st.alias_mode;
3781
+ ca_iter_state_finish(&st);
3782
+ }
3783
+ ca_free(view);
3784
+
3785
+ result = rb_hash_new();
3786
+ rb_hash_aset(result, ID2SYM(rb_intern("rc")), INT2NUM(rc));
3787
+ rb_hash_aset(result, ID2SYM(rb_intern("slabs")), INT2NUM(slabs));
3788
+ rb_hash_aset(result, ID2SYM(rb_intern("total_elems")), SIZE2NUM(total_elems));
3789
+ rb_hash_aset(result, ID2SYM(rb_intern("alias_mode")), INT2NUM(alias_mode));
3790
+ rb_hash_aset(result, ID2SYM(rb_intern("data")), data);
3791
+ return result;
3792
+ }
3793
+
3794
+ /* WRITE smoke: fill view's slab buffer with `val` (float64), let
3795
+ sync_slab → ca_sync run the scatter back to ref via xfer_all PUT. */
3796
+ static VALUE
3797
+ rb_t1_smoke_remap_write_fill_f64 (VALUE klass, VALUE vref, VALUE vidx,
3798
+ VALUE vval)
3799
+ {
3800
+ CArray *ref, *idx, *view;
3801
+ ca_iter_state st;
3802
+ char *p;
3803
+ ca_size_t n, i;
3804
+ int rc;
3805
+ double v = NUM2DBL(vval);
3806
+
3807
+ (void) klass;
3808
+ TypedData_Get_Struct(vref, CArray, &carray_data_type, ref);
3809
+ TypedData_Get_Struct(vidx, CArray, &carray_data_type, idx);
3810
+ view = ca_remap_new(ref, idx);
3811
+
3812
+ rc = ca_iter_state_init_l1(&st, view, CA_SLAB_WHOLE, NULL, 0,
3813
+ CA_KERNEL_WRITE);
3814
+ if ( rc != CA_ITER_OK ) {
3815
+ ca_free(view);
3816
+ return INT2NUM(rc);
3817
+ }
3818
+ while ( ca_iter_state_next_slab(&st, &p, NULL, &n) ) {
3819
+ double *q = (double *) p;
3820
+ for ( i = 0; i < n; i++ ) q[i] = v;
3821
+ ca_iter_state_sync_slab(&st);
3822
+ }
3823
+ ca_iter_state_finish(&st);
3824
+ ca_free(view);
3825
+ return INT2NUM(CA_ITER_OK);
3826
+ }
3827
+
3828
+ /* ---- Phase C C.3: CA_FOR_EACH_SLAB macro smoke surfaces -------------
3829
+ Smoke kernels that exercise the block macros end-to-end. Used by
3830
+ spec_ai/test_ca_for_each_slab_macros.rb to pin behavioural
3831
+ correctness of the macro expansion (= same byte-parity result as
3832
+ the raw API equivalent). */
3833
+
3834
+ /* sum reduction using CA_FOR_EACH_SLAB. innermost slab axis only,
3835
+ float64 source, accumulator = scalar (single full-reduction slab
3836
+ when naxes == src->ndim, otherwise per-outer-slab accumulator
3837
+ written to a row-major output buffer). Mirrors carray_kernel_sum
3838
+ structurally but uses the macro for lifecycle. */
3839
+ static VALUE
3840
+ rb_caf_smoke_sum_f64 (int argc, VALUE *argv, VALUE klass)
3841
+ {
3842
+ (void) klass;
3843
+ if ( argc < 2 ) {
3844
+ rb_raise(rb_eArgError, "expected (src, axis_int, ...)");
3845
+ }
3846
+
3847
+ VALUE vsrc = argv[0];
3848
+ CArray *ca;
3849
+ GetCArray(vsrc, ca);
3850
+
3851
+ int8_t slab_axes[CA_RANK_MAX];
3852
+ int8_t naxes = (int8_t) (argc - 1);
3853
+ if ( naxes < 1 || naxes > ca->ndim ) {
3854
+ rb_raise(rb_eArgError, "bad axes count");
3855
+ }
3856
+ for ( int8_t k = 0; k < naxes; k++ ) {
3857
+ slab_axes[k] = (int8_t) NUM2INT(argv[1 + k]);
3858
+ if ( slab_axes[k] < 0 ) slab_axes[k] += ca->ndim;
3859
+ }
3860
+
3861
+ /* Output: same layout policy as sum_ki (= reduced shape via
3862
+ rb_ca_new_reduced). For full reduction naxes == ndim, output
3863
+ is a 1-element CArray that we unwrap to Float below. */
3864
+ VALUE vout = rb_ca_new_reduced(vsrc, slab_axes, naxes, CA_FLOAT64, 0);
3865
+ CArray *co;
3866
+ GetCArray(vout, co);
3867
+ double *op = (double *) co->ptr;
3868
+
3869
+ ca_iter_state st;
3870
+ char *p;
3871
+ boolean8_t *m;
3872
+ ca_size_t out_i = 0;
3873
+
3874
+ CA_FOR_EACH_SLAB(st, ca, slab_axes, naxes, 0, p, m) {
3875
+ /* K-1 outer carry + innermost inner walk (= same shape as sum_ki).
3876
+ Required for slab_ndim > 1 because slab cells aren't a single
3877
+ contig run with one stride — outer slab axes have their own
3878
+ strides. */
3879
+ double acc = 0.0;
3880
+ int8_t K = st.slab_ndim;
3881
+ int8_t outer_K = K - 1;
3882
+ ca_size_t inner_n = st.slab_dims[K - 1];
3883
+ ca_size_t inner_s = st.slab_strides[K - 1];
3884
+ ca_size_t inner_ms = st.slab_mask_strides[K - 1];
3885
+
3886
+ ca_size_t outer_count = 1;
3887
+ for ( int8_t k = 0; k < outer_K; k++ ) outer_count *= st.slab_dims[k];
3888
+
3889
+ ca_size_t idx[CA_RANK_MAX] = { 0 };
3890
+ for ( ca_size_t o = 0; o < outer_count; o++ ) {
3891
+ ca_size_t data_off = 0;
3892
+ ca_size_t mask_off = 0;
3893
+ for ( int8_t k = 0; k < outer_K; k++ ) {
3894
+ data_off += idx[k] * st.slab_strides[k];
3895
+ mask_off += idx[k] * st.slab_mask_strides[k];
3896
+ }
3897
+ const char *q = p + data_off;
3898
+ if ( m == NULL ) {
3899
+ for ( ca_size_t j = 0; j < inner_n; j++ ) {
3900
+ acc += *(const double *) (q + j * inner_s);
3901
+ }
3902
+ } else {
3903
+ const boolean8_t *mm = m + mask_off;
3904
+ for ( ca_size_t j = 0; j < inner_n; j++ ) {
3905
+ if ( ! mm[j * inner_ms] ) {
3906
+ acc += *(const double *) (q + j * inner_s);
3907
+ }
3908
+ }
3909
+ }
3910
+ for ( int8_t k = outer_K - 1; k >= 0; k-- ) {
3911
+ if ( ++idx[k] < st.slab_dims[k] ) break;
3912
+ idx[k] = 0;
3913
+ }
3914
+ }
3915
+ op[out_i++] = acc;
3916
+ }
3917
+
3918
+ if ( naxes == ca->ndim ) {
3919
+ return rb_float_new(op[0]);
3920
+ }
3921
+ return vout;
3922
+ }
3923
+
3924
+ /* map kernel using CA_FOR_EACH_SLAB_INOUT. Doubles each element of
3925
+ a float64 source into a fresh same-shape output. Slab axis is
3926
+ always the innermost (= argv[1] is unused for simplicity, axis
3927
+ fixed to ca->ndim - 1). Demonstrates the 2-iter pattern. */
3928
+ static VALUE
3929
+ rb_caf_smoke_double_f64 (VALUE klass, VALUE vsrc)
3930
+ {
3931
+ (void) klass;
3932
+ CArray *ca;
3933
+ GetCArray(vsrc, ca);
3934
+ if ( ca->data_type != CA_FLOAT64 ) {
3935
+ rb_raise(rb_eTypeError, "expected float64 source");
3936
+ }
3937
+
3938
+ VALUE vout = rb_ca_template_with_type(vsrc, INT2NUM(CA_FLOAT64), Qnil);
3939
+ CArray *co;
3940
+ GetCArray(vout, co);
3941
+
3942
+ int8_t slab_axes[CA_RANK_MAX];
3943
+ int8_t naxes = 1;
3944
+ slab_axes[0] = (int8_t) (ca->ndim - 1);
3945
+
3946
+ ca_iter_state st_in, st_out;
3947
+ char *p_in, *p_out;
3948
+ boolean8_t *m_in, *m_out;
3949
+
3950
+ CA_FOR_EACH_SLAB_INOUT(st_in, st_out, ca, co,
3951
+ slab_axes, naxes,
3952
+ p_in, p_out, m_in, m_out) {
3953
+ ca_size_t n = st_in.slab_n;
3954
+ ca_size_t in_s = st_in.slab_strides[st_in.slab_ndim - 1];
3955
+ ca_size_t out_s = st_out.slab_strides[st_out.slab_ndim - 1];
3956
+ /* Both sides float64; for the typical entity output, in_s = out_s
3957
+ = sizeof(double). Mask propagation: if input is masked at cell
3958
+ j, just leave output alone (= CArray's default mask propagation
3959
+ happens via co's own mask, not our concern here for the smoke). */
3960
+ (void) m_in;
3961
+ (void) m_out;
3962
+ for ( ca_size_t j = 0; j < n; j++ ) {
3963
+ double v = *(const double *) (p_in + j * in_s);
3964
+ *(double *) (p_out + j * out_s) = v * 2.0;
3965
+ }
3966
+ }
3967
+
3968
+ return vout;
3969
+ }
3970
+
3971
+ /* ---- PROPOSAL_FIBER_DELIVERY F.2: catalog macro smokes -------------
3972
+ Exercise each of the 4 catalog forms end-to-end with float64 fibers,
3973
+ verifying contig delivery for both innermost (= stride==bytes) and
3974
+ non-innermost (= stride>bytes, gather path) axis positions. Used by
3975
+ spec_ai/test_fiber_delivery.rb (= F.3). */
3976
+
3977
+ /* form 1 (NO_MASK single): per-axis sum, returns total sum.
3978
+ Catalog: CA_FOR_EACH_FIBER. */
3979
+ static VALUE
3980
+ rb_caf_fiber_smoke_sum_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
3981
+ {
3982
+ CArray *src;
3983
+ ca_iter_state st;
3984
+ char *p;
3985
+ ca_size_t n;
3986
+ double total = 0.0;
3987
+ int axis = NUM2INT(vaxis);
3988
+
3989
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
3990
+ if ( src->data_type != CA_FLOAT64 ) {
3991
+ rb_raise(rb_eArgError, "caf_fiber_smoke_sum_f64: requires float64");
3992
+ }
3993
+ CA_FOR_EACH_FIBER(st, src, axis, CA_KERNEL_NO_MASK, p, n) {
3994
+ const double *pd = (const double *) p;
3995
+ for ( ca_size_t i = 0; i < n; i++ ) total += pd[i];
3996
+ }
3997
+ return rb_float_new(total);
3998
+ }
3999
+
4000
+ /* form 2 (NO_MASK INOUT): per-axis fiber copy * scalar.
4001
+ Catalog: CA_FOR_EACH_FIBER_INOUT. Sorts each fiber for non-trivial
4002
+ gather/scatter behavior. */
4003
+ static VALUE
4004
+ rb_caf_fiber_smoke_double_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4005
+ {
4006
+ CArray *src, *out;
4007
+ ca_iter_state st_in, st_out;
4008
+ char *pi, *po;
4009
+ ca_size_t n;
4010
+ int axis = NUM2INT(vaxis);
4011
+ VALUE vout;
4012
+
4013
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
4014
+ if ( src->data_type != CA_FLOAT64 ) {
4015
+ rb_raise(rb_eArgError, "caf_fiber_smoke_double_f64: requires float64");
4016
+ }
4017
+ vout = rb_ca_template(vsrc);
4018
+ TypedData_Get_Struct(vout, CArray, &carray_data_type, out);
4019
+
4020
+ CA_FOR_EACH_FIBER_INOUT(st_in, st_out, src, out, axis,
4021
+ CA_KERNEL_NO_MASK, pi, po, n) {
4022
+ double *dpi = (double *) pi;
4023
+ double *dpo = (double *) po;
4024
+ for ( ca_size_t i = 0; i < n; i++ ) dpo[i] = dpi[i] * 2.0;
4025
+ }
4026
+ return vout;
4027
+ }
4028
+
4029
+ /* form 3 (mask-aware single): unmasked sum.
4030
+ Catalog: CA_FOR_EACH_FIBER_MASKED. */
4031
+ static VALUE
4032
+ rb_caf_fiber_smoke_unmasked_sum_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4033
+ {
4034
+ CArray *src;
4035
+ ca_iter_state st;
4036
+ char *p;
4037
+ boolean8_t *m;
4038
+ ca_size_t n;
4039
+ double total = 0.0;
4040
+ int axis = NUM2INT(vaxis);
4041
+
4042
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
4043
+ if ( src->data_type != CA_FLOAT64 ) {
4044
+ rb_raise(rb_eArgError, "caf_fiber_smoke_unmasked_sum_f64: requires float64");
4045
+ }
4046
+ CA_FOR_EACH_FIBER_MASKED(st, src, axis, 0, p, n, m) {
4047
+ double *dp = (double *) p;
4048
+ for ( ca_size_t i = 0; i < n; i++ ) {
4049
+ if ( !m || !m[i] ) total += dp[i];
4050
+ }
4051
+ }
4052
+ return rb_float_new(total);
4053
+ }
4054
+
4055
+ /* form 4 (mask-aware INOUT): copy input but zero out masked cells in
4056
+ output. Catalog: CA_FOR_EACH_FIBER_INOUT_MASKED. */
4057
+ static VALUE
4058
+ rb_caf_fiber_smoke_zero_masked_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4059
+ {
4060
+ CArray *src, *out;
4061
+ ca_iter_state st_in, st_out;
4062
+ char *pi, *po;
4063
+ boolean8_t *m;
4064
+ ca_size_t n;
4065
+ int axis = NUM2INT(vaxis);
4066
+ VALUE vout;
4067
+
4068
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
4069
+ if ( src->data_type != CA_FLOAT64 ) {
4070
+ rb_raise(rb_eArgError, "caf_fiber_smoke_zero_masked_f64: requires float64");
4071
+ }
4072
+ vout = rb_ca_template(vsrc);
4073
+ TypedData_Get_Struct(vout, CArray, &carray_data_type, out);
4074
+
4075
+ CA_FOR_EACH_FIBER_INOUT_MASKED(st_in, st_out, src, out, axis,
4076
+ 0, pi, po, n, m) {
4077
+ double *dpi = (double *) pi;
4078
+ double *dpo = (double *) po;
4079
+ for ( ca_size_t i = 0; i < n; i++ ) {
4080
+ dpo[i] = (m && m[i]) ? 0.0 : dpi[i];
4081
+ }
4082
+ }
4083
+ return vout;
4084
+ }
4085
+
4086
+ /* F.5 bench helper: sort_copy via CA_FOR_EACH_FIBER_INOUT.
4087
+ Functional equivalent of ext/carray_order.c::rb_ca_sort_copy_axis_*
4088
+ (= hand-rolled gather/scatter with explicit slab_strides[0] loops).
4089
+ The macro form lets us A/B the author-side overhead of stride math
4090
+ vs the catalog-macro-driven engine gather/scatter. */
4091
+ static int
4092
+ caf_fiber_bench_cmp_double (const void *a, const void *b)
4093
+ {
4094
+ double da = *(const double *) a;
4095
+ double db = *(const double *) b;
4096
+ return (da > db) - (da < db);
4097
+ }
4098
+
4099
+ static VALUE
4100
+ rb_caf_fiber_bench_sort_copy_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4101
+ {
4102
+ CArray *src, *out;
4103
+ ca_iter_state st_in, st_out;
4104
+ char *pi, *po;
4105
+ ca_size_t n;
4106
+ int axis = NUM2INT(vaxis);
4107
+ VALUE vout;
4108
+
4109
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
4110
+ if ( src->data_type != CA_FLOAT64 ) {
4111
+ rb_raise(rb_eArgError, "caf_fiber_bench_sort_copy_f64: requires float64");
4112
+ }
4113
+ vout = rb_ca_template(vsrc);
4114
+ TypedData_Get_Struct(vout, CArray, &carray_data_type, out);
4115
+
4116
+ CA_FOR_EACH_FIBER_INOUT(st_in, st_out, src, out, axis,
4117
+ CA_KERNEL_NO_MASK, pi, po, n) {
4118
+ double *dpo = (double *) po;
4119
+ memcpy(po, pi, n * sizeof(double));
4120
+ #ifdef HAVE_MERGESORT
4121
+ if ( mergesort(dpo, n, sizeof(double), caf_fiber_bench_cmp_double) != 0 ) {
4122
+ qsort(dpo, n, sizeof(double), caf_fiber_bench_cmp_double);
4123
+ }
4124
+ #else
4125
+ qsort(dpo, n, sizeof(double), caf_fiber_bench_cmp_double);
4126
+ #endif
4127
+ }
4128
+ return vout;
4129
+ }
4130
+
4131
+ /* F.5 bench helper variant: sort_copy via SLAB macro + manual
4132
+ gather/scatter (= what hand-rolled sort_copy effectively does, but
4133
+ driven through the public CA_FOR_EACH_SLAB_INOUT macro instead of
4134
+ the raw next_slab_axes API). Lets us isolate macro overhead from
4135
+ gather/scatter overhead. */
4136
+ static VALUE
4137
+ rb_caf_slab_bench_sort_copy_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4138
+ {
4139
+ CArray *src, *out;
4140
+ ca_iter_state st_in, st_out;
4141
+ char *pi, *po;
4142
+ boolean8_t *mi, *mo;
4143
+ int axis = NUM2INT(vaxis);
4144
+ VALUE vout;
4145
+ int8_t slab_axes[1];
4146
+
4147
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
4148
+ if ( src->data_type != CA_FLOAT64 ) {
4149
+ rb_raise(rb_eArgError, "caf_slab_bench_sort_copy_f64: requires float64");
4150
+ }
4151
+ vout = rb_ca_template(vsrc);
4152
+ TypedData_Get_Struct(vout, CArray, &carray_data_type, out);
4153
+ slab_axes[0] = (int8_t) axis;
4154
+
4155
+ ca_size_t fiber_n = src->dim[axis];
4156
+ ca_size_t bytes = sizeof(double);
4157
+ char *buf = (char *) xmalloc(fiber_n * bytes);
4158
+
4159
+ CA_FOR_EACH_SLAB_INOUT(st_in, st_out, src, out,
4160
+ slab_axes, 1,
4161
+ pi, po, mi, mo) {
4162
+ ca_size_t n = st_in.slab_dims[0];
4163
+ ca_size_t is = st_in.slab_strides[0];
4164
+ ca_size_t os = st_out.slab_strides[0];
4165
+ /* Author-side gather (= what hand-rolled sort_copy does). */
4166
+ if ( is == (ca_size_t) bytes ) {
4167
+ memcpy(buf, pi, n * bytes);
4168
+ } else {
4169
+ for ( ca_size_t k = 0; k < n; k++ ) {
4170
+ memcpy(buf + k * bytes, pi + k * is, bytes);
4171
+ }
4172
+ }
4173
+ #ifdef HAVE_MERGESORT
4174
+ if ( mergesort(buf, n, bytes, caf_fiber_bench_cmp_double) != 0 ) {
4175
+ qsort(buf, n, bytes, caf_fiber_bench_cmp_double);
4176
+ }
4177
+ #else
4178
+ qsort(buf, n, bytes, caf_fiber_bench_cmp_double);
4179
+ #endif
4180
+ /* Author-side scatter. */
4181
+ if ( os == (ca_size_t) bytes ) {
4182
+ memcpy(po, buf, n * bytes);
4183
+ } else {
4184
+ for ( ca_size_t k = 0; k < n; k++ ) {
4185
+ memcpy(po + k * os, buf + k * bytes, bytes);
4186
+ }
4187
+ }
4188
+ }
4189
+ xfree(buf);
4190
+ return vout;
4191
+ }
4192
+
4193
+ /* F.5 follow-up bench: per-fiber fused xfer_stride direct into a contig
4194
+ scratch, skipping the kernel_iterator SRC_ATTACH whole-view materialise.
4195
+ Tests user's hypothesis: for transform views (CAFake/CAByteSwap etc.),
4196
+ does bypassing the whole-view scratch_ptr and calling ca_xfer_stride
4197
+ per-fiber (= fused 1-pass per fiber) beat the current FIBER path?
4198
+
4199
+ Scope: float64 view, sum along a single axis. No kernel_iterator
4200
+ state is created -- this directly walks fiber regions via outer
4201
+ odometer + per-fiber ca_xfer_stride call. */
4202
+ static VALUE
4203
+ rb_caf_bench_per_fiber_xfer_sum_f64 (VALUE klass, VALUE vsrc, VALUE vaxis)
4204
+ {
4205
+ CArray *src;
4206
+ int axis;
4207
+ double total = 0.0;
4208
+ ca_size_t starts[CA_RANK_MAX], counts[CA_RANK_MAX], strides[CA_RANK_MAX];
4209
+ ca_size_t outer_idx[CA_RANK_MAX];
4210
+ int8_t nd, k;
4211
+
4212
+ TypedData_Get_Struct(vsrc, CArray, &carray_data_type, src);
4213
+ if ( src->data_type != CA_FLOAT64 ) {
4214
+ rb_raise(rb_eArgError,
4215
+ "caf_bench_per_fiber_xfer_sum_f64: requires float64 view");
4216
+ }
4217
+ axis = NUM2INT(vaxis);
4218
+ nd = src->ndim;
4219
+ if ( axis < 0 || axis >= nd ) {
4220
+ rb_raise(rb_eArgError, "axis out of range");
4221
+ }
4222
+
4223
+ /* Fiber length = src->dim[axis], fiber count = product of other dims. */
4224
+ ca_size_t fiber_n = src->dim[axis];
4225
+ ca_size_t total_fibers = 1;
4226
+ for ( k = 0; k < nd; k++ ) {
4227
+ if ( k != axis ) total_fibers *= src->dim[k];
4228
+ outer_idx[k] = 0;
4229
+ }
4230
+
4231
+ /* Per-axis native byte strides (row-major) for use in ca_xfer_stride. */
4232
+ ca_size_t native[CA_RANK_MAX];
4233
+ {
4234
+ ca_size_t s = src->bytes;
4235
+ for ( k = nd - 1; k >= 0; k-- ) { native[k] = s; s *= src->dim[k]; }
4236
+ }
4237
+
4238
+ /* Fiber-sized contig scratch. */
4239
+ char *buf = (char *) xmalloc(fiber_n * src->bytes);
4240
+
4241
+ for ( ca_size_t f = 0; f < total_fibers; f++ ) {
4242
+ /* Build region: counts = 1 on all non-axis, fiber_n on axis;
4243
+ starts from outer_idx (0 on axis); strides = native bytes. */
4244
+ for ( k = 0; k < nd; k++ ) {
4245
+ starts[k] = (k == axis) ? 0 : outer_idx[k];
4246
+ counts[k] = (k == axis) ? fiber_n : 1;
4247
+ strides[k] = native[k];
4248
+ }
4249
+
4250
+ /* ONE fused xfer_stride per fiber: for CAFake/CAByteSwap this
4251
+ routes to ca_xfer_stride_transform_fused (= 1-pass cast direct
4252
+ from parent to buf, no intermediate whole-view scratch). */
4253
+ ca_xfer_stride(src, starts, counts, strides, buf, CA_XFER_GET);
4254
+
4255
+ /* Sum the fiber. */
4256
+ double *p = (double *) buf;
4257
+ for ( ca_size_t i = 0; i < fiber_n; i++ ) total += p[i];
4258
+
4259
+ /* Advance outer_idx row-major over non-axis dims. */
4260
+ for ( k = nd - 1; k >= 0; k-- ) {
4261
+ if ( k == axis ) continue;
4262
+ if ( ++outer_idx[k] < src->dim[k] ) break;
4263
+ outer_idx[k] = 0;
4264
+ }
4265
+ }
4266
+
4267
+ xfree(buf);
4268
+ return rb_float_new(total);
4269
+ }
4270
+
4271
+ #endif /* CARRAY_DEV_BUILD — end smoke surface fence */
4272
+
4273
+ void
4274
+ Init_ca_kernel_iterator (void)
4275
+ {
4276
+ #ifdef CARRAY_DEV_BUILD
4277
+ /* ==== smoke surface registrations (dev-only, stripped in release) ====
4278
+ * See PROPOSAL_SMOKE_DEV_BUILD_GATE.md. All `t1_smoke_*`, `caf_smoke_*`,
4279
+ * `caf_fiber_smoke_*`, `caf_*_bench_*`, helper smokes, and `T1_*` test
4280
+ * constants are gated here together — none of them are consumed by
4281
+ * production code (lib/ / other ext/), only by spec_ai regression pins. */
4282
+ rb_define_singleton_method(rb_cCArray, "t1_smoke",
4283
+ rb_t1_smoke, 1);
4284
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_strided",
4285
+ rb_t1_smoke_strided, 1);
4286
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_sum_strided_f64",
4287
+ rb_t1_smoke_sum_strided_f64, 1);
4288
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_write_fill_f64",
4289
+ rb_t1_smoke_write_fill_f64, 2);
4290
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_write_fill_strided_f64",
4291
+ rb_t1_smoke_write_fill_strided_f64, 2);
4292
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_write_partial_raise_f64",
4293
+ rb_t1_smoke_write_partial_raise_f64, 3);
4294
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_sort_row_f64",
4295
+ rb_t1_smoke_sort_row_f64, 1);
4296
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_attach",
4297
+ rb_t1_smoke_attach, 1);
4298
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_attach_strided",
4299
+ rb_t1_smoke_attach_strided, 1);
4300
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_with_mask",
4301
+ rb_t1_smoke_with_mask, 1);
4302
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_init_rc",
4303
+ rb_t1_smoke_init_rc, 2);
4304
+ /* Phase A capstone: CA_SLAB_AXES smoke (variadic axes) */
4305
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_sum_axes_f64",
4306
+ rb_t1_smoke_sum_axes_f64, -1);
4307
+ /* Phase A.3: rb_ca_new_reduced helper smoke */
4308
+ rb_define_singleton_method(rb_cCArray, "t1_test_new_reduced",
4309
+ rb_t1_test_new_reduced, -1);
4310
+ /* Phase B.3: rb_ca_parse_reduce_axes helper smoke */
4311
+ rb_define_singleton_method(rb_cCArray, "t1_test_parse_reduce_axes",
4312
+ rb_t1_test_parse_reduce_axes, -1);
4313
+
4314
+ /* API harmonisation A.1: rb_ca_parse_reduce_axes_kw helper smoke */
4315
+ rb_define_singleton_method(rb_cCArray, "test_parse_reduce_axes_kw",
4316
+ rb_test_parse_reduce_axes_kw, -1);
4317
+ /* CAReduce-specific (step 9.3): no public Ruby API */
4318
+ rb_define_singleton_method(rb_cCArray, "t1_make_reduce",
4319
+ rb_t1_make_reduce, 3);
4320
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_reduce_read",
4321
+ rb_t1_smoke_reduce_read, 3);
4322
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_reduce_write_broadcast",
4323
+ rb_t1_smoke_reduce_write_broadcast, 4);
4324
+
4325
+ /* M.6 CARemap kernel_iterator smokes (test-only). */
4326
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_remap_read",
4327
+ rb_t1_smoke_remap_read, 2);
4328
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_remap_read_strided",
4329
+ rb_t1_smoke_remap_read_strided, 2);
4330
+ rb_define_singleton_method(rb_cCArray, "t1_smoke_remap_write_fill_f64",
4331
+ rb_t1_smoke_remap_write_fill_f64, 3);
4332
+ /* Phase C C.3 (2026-05-27): block macro smokes */
4333
+ rb_define_singleton_method(rb_cCArray, "caf_smoke_sum_f64",
4334
+ rb_caf_smoke_sum_f64, -1);
4335
+ rb_define_singleton_method(rb_cCArray, "caf_smoke_double_f64",
4336
+ rb_caf_smoke_double_f64, 1);
4337
+ /* PROPOSAL_FIBER_DELIVERY F.2: catalog macro smokes */
4338
+ rb_define_singleton_method(rb_cCArray, "caf_fiber_smoke_sum_f64",
4339
+ rb_caf_fiber_smoke_sum_f64, 2);
4340
+ rb_define_singleton_method(rb_cCArray, "caf_fiber_smoke_double_f64",
4341
+ rb_caf_fiber_smoke_double_f64, 2);
4342
+ rb_define_singleton_method(rb_cCArray, "caf_fiber_smoke_unmasked_sum_f64",
4343
+ rb_caf_fiber_smoke_unmasked_sum_f64, 2);
4344
+ rb_define_singleton_method(rb_cCArray, "caf_fiber_smoke_zero_masked_f64",
4345
+ rb_caf_fiber_smoke_zero_masked_f64, 2);
4346
+ /* F.5 bench helper */
4347
+ rb_define_singleton_method(rb_cCArray, "caf_fiber_bench_sort_copy_f64",
4348
+ rb_caf_fiber_bench_sort_copy_f64, 2);
4349
+ rb_define_singleton_method(rb_cCArray, "caf_slab_bench_sort_copy_f64",
4350
+ rb_caf_slab_bench_sort_copy_f64, 2);
4351
+ rb_define_singleton_method(rb_cCArray, "caf_bench_per_fiber_xfer_sum_f64",
4352
+ rb_caf_bench_per_fiber_xfer_sum_f64, 2);
4353
+
4354
+ rb_define_const(rb_cCArray, "T1_ITER_OK", INT2NUM(CA_ITER_OK));
4355
+ rb_define_const(rb_cCArray, "T1_ITER_ERR_NOT_CHEAP", INT2NUM(CA_ITER_ERR_NOT_CHEAP));
4356
+ rb_define_const(rb_cCArray, "T1_ITER_ERR_POLICY", INT2NUM(CA_ITER_ERR_POLICY));
4357
+ rb_define_const(rb_cCArray, "T1_ITER_ERR_FLAGS", INT2NUM(CA_ITER_ERR_FLAGS));
4358
+ rb_define_const(rb_cCArray, "T1_ITER_ERR_READONLY", INT2NUM(CA_ITER_ERR_READONLY));
4359
+ rb_define_const(rb_cCArray, "T1_ITER_ERR_MASK", INT2NUM(CA_ITER_ERR_MASK));
4360
+ rb_define_const(rb_cCArray, "T1_ITER_ERR_MASK_NOT_ALLOWED", INT2NUM(CA_ITER_ERR_MASK_NOT_ALLOWED));
4361
+ rb_define_const(rb_cCArray, "T1_KERNEL_NO_MASK", INT2NUM(CA_KERNEL_NO_MASK));
4362
+ rb_define_const(rb_cCArray, "T1_ITER_ALIAS_NONE", INT2NUM(CA_ITER_ALIAS_NONE));
4363
+ rb_define_const(rb_cCArray, "T1_ITER_ALIAS_CONTIG", INT2NUM(CA_ITER_ALIAS_CONTIG));
4364
+ rb_define_const(rb_cCArray, "T1_ITER_ALIAS_STRIDED", INT2NUM(CA_ITER_ALIAS_STRIDED));
4365
+ rb_define_const(rb_cCArray, "T1_ITER_ALIAS_ATTACH", INT2NUM(CA_ITER_ALIAS_ATTACH));
4366
+ #endif /* CARRAY_DEV_BUILD */
4367
+ }