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
@@ -1,16 +1,34 @@
1
1
  /* ---------------------------------------------------------------------------
2
2
 
3
- carray_operator.c
4
-
5
- This file is part of Ruby/CArray extension library.
6
-
7
- Copyright (C) 2005-2025 Hiroki Motoyoshi
3
+ Operator dispatch: the drivers behind CArray's arithmetic / comparison
4
+ operators (rb_ca_call_monop / _binop / _triop / _moncmp / _bincmp),
5
+ plus the chunked-gather and safe-mask-overlay helpers that let the eager
6
+ slow path materialise operands without attaching attach-hostile views.
8
7
 
9
8
  ---------------------------------------------------------------------------- */
10
9
 
11
10
  #include <math.h>
11
+ #include <stdarg.h>
12
12
 
13
13
  #include "carray.h"
14
+ #include "carray_internal.h" /* ca_lazy_arena_* */
15
+
16
+ /* Operand scratch for a chunked / gathered kernel run.
17
+
18
+ For CA_OBJECT the scratch holds VALUEs, and an object-lane kernel
19
+ calls rb_funcall per cell -- a collection in the middle would free
20
+ whatever the gather pulled in from a lazy operand, since those cells
21
+ exist nowhere else. The object form of the arena acquire keeps the
22
+ slot marked until it is released. */
23
+ static void *
24
+ ca_op_acquire_operand_scratch (CArray *ca, ca_size_t n_elements)
25
+ {
26
+ return ( ca->data_type == CA_OBJECT )
27
+ ? ca_lazy_arena_acquire_object(n_elements)
28
+ : ca_lazy_arena_acquire(n_elements * ca->bytes);
29
+ }
30
+
31
+ #include "ca_obj_face.h" /* ca_face_reconcile_comparison (comparison Face gate) */
14
32
 
15
33
  VALUE rb_mCAMath;
16
34
 
@@ -18,14 +36,183 @@ extern ca_binop_func_t ca_binop_mul[CA_NTYPE];
18
36
  extern ca_binop_func_t ca_binop_add[CA_NTYPE];
19
37
 
20
38
  void
21
- ca_zerodiv ()
39
+ ca_zerodiv (void)
22
40
  {
23
- #ifdef _OPENMP
24
- #pragma omp master
25
- #endif
26
41
  rb_raise(rb_eZeroDivError, "divided by 0");
27
42
  }
28
43
 
44
+ /* Chunked gather helpers for the eager slow path. Instead of an ALLOCV
45
+ full-size materialise, gather per-region via xfer_stride into arena
46
+ scratch, dropping the memory peak from O(operand_size) to O(chunk).
47
+
48
+ Policy:
49
+ - target chunk = 4096 elements (~32KB at f64), L1d-friendly
50
+ - N-D shape: outer-axis chunking, row-aligned
51
+ - mask handling: via the ca_copy_mask_overlay path
52
+
53
+ These three helpers (ca_chunk_inner_size / ca_chunk_compute_n /
54
+ ca_chunked_gather) are extern so ca_sweep_engine.c can reuse the same
55
+ chunk policy + region-gather mechanism for the sweep ELEMENT macro
56
+ family without duplicating the implementation. */
57
+
58
+ #define CA_CHUNK_TARGET_ELEMENTS 4096
59
+
60
+ ca_size_t
61
+ ca_chunk_inner_size (CArray *ca)
62
+ {
63
+ /* product of all dims except outermost; 1 for ndim <= 1 / scalar */
64
+ ca_size_t inner = 1;
65
+ int8_t k;
66
+ if (ca->ndim <= 1) return 1;
67
+ for (k = ca->ndim - 1; k >= 1; k--) inner *= ca->dim[k];
68
+ return inner;
69
+ }
70
+
71
+ ca_size_t
72
+ ca_chunk_compute_n (ca_size_t total, ca_size_t inner, ca_size_t bytes_per_cell)
73
+ {
74
+ /* Compute chunk_n in elements aligned to outer axis: a multiple of
75
+ `inner` (or = inner if target < inner = 1 row/chunk fallback).
76
+ Result is clamped to `total`. */
77
+ ca_size_t target_bytes = CA_CHUNK_TARGET_ELEMENTS * 8; /* ~32KB at f64 */
78
+ ca_size_t target_n = target_bytes / (bytes_per_cell > 0 ? bytes_per_cell : 1);
79
+ ca_size_t chunk_n;
80
+ if (target_n < 1) target_n = 1;
81
+ if (inner <= 0) inner = 1;
82
+ if (target_n >= inner) {
83
+ chunk_n = (target_n / inner) * inner; /* round down to inner multiple */
84
+ } else {
85
+ chunk_n = inner; /* 1 row/chunk fallback */
86
+ }
87
+ if (chunk_n > total) chunk_n = total;
88
+ if (chunk_n < 1) chunk_n = 1;
89
+ return chunk_n;
90
+ }
91
+
92
+ /* Gather `n` elements starting at flat-offset `off` from `ca` into `dest`
93
+ (contig native byte layout of the chunked region). Requires:
94
+ - `off` and `n` are multiples of inner = Π_{k>=1} ca->dim[k]
95
+ - `n` represents `outer_rows * inner` consecutive flat-addr cells
96
+ For ndim <= 1: simple linear region. */
97
+ void
98
+ ca_chunked_gather (CArray *ca, ca_size_t off, ca_size_t n, void *dest)
99
+ {
100
+ ca_size_t starts[CA_RANK_MAX];
101
+ ca_size_t counts[CA_RANK_MAX];
102
+ ca_size_t strides[CA_RANK_MAX];
103
+ int8_t k;
104
+ ca_size_t inner, bytes, s;
105
+
106
+ bytes = ca->bytes;
107
+
108
+ if (ca->ndim <= 1) {
109
+ /* 1-D (or scalar reified to 1-D via elements): single axis chunk */
110
+ starts[0] = (ca->ndim == 0) ? 0 : off;
111
+ counts[0] = (ca->ndim == 0) ? 1 : n;
112
+ strides[0] = bytes;
113
+ ca_xfer_stride(ca, starts, counts, strides, dest, CA_XFER_GET);
114
+ return;
115
+ }
116
+
117
+ inner = 1;
118
+ for (k = ca->ndim - 1; k >= 1; k--) inner *= ca->dim[k];
119
+
120
+ starts[0] = (inner > 0) ? off / inner : 0;
121
+ counts[0] = (inner > 0) ? n / inner : 0;
122
+ for (k = 1; k < ca->ndim; k++) {
123
+ starts[k] = 0;
124
+ counts[k] = ca->dim[k];
125
+ }
126
+
127
+ /* native contig strides for the chunked shape */
128
+ s = bytes;
129
+ for (k = ca->ndim - 1; k >= 0; k--) {
130
+ strides[k] = s;
131
+ s *= counts[k];
132
+ }
133
+ ca_xfer_stride(ca, starts, counts, strides, dest, CA_XFER_GET);
134
+ }
135
+
136
+ /* Operand mask overlay without calling ca_attach on the operand masks.
137
+
138
+ `ca_copy_mask_overlay` (carray_mask.c) attaches each operand mask via
139
+ ca_attach(cs->mask) before OR-folding into the output mask. For
140
+ attach-hostile operand masks (= mock fixture, CAStack expansion,
141
+ per-region xfer-only views) that raises.
142
+
143
+ This helper instead materialises each operand mask via ca_xfer_all into
144
+ a transient arena scratch and OR-folds byte-wise into ca_out->mask.
145
+ Memory peak: one mask scratch at a time (= elements bytes, 1 B/cell),
146
+ released between operands.
147
+
148
+ CAREFUL: `ca_out` must be a freshly templated entity (driver allocates
149
+ via ca_template_safe), so ca_out->mask->ptr is directly writable
150
+ without attach.
151
+
152
+ This gathers the full mask once; a per-chunk mask gather inside the
153
+ chunked branch is a possible future optimisation. */
154
+ void
155
+ ca_mask_overlay_safe (CArray *ca_out, int n, ...)
156
+ {
157
+ va_list ap;
158
+ CArray *slist[8];
159
+ int i, any_mask = 0;
160
+
161
+ if ( n < 0 || n > 8 ) {
162
+ rb_raise(rb_eRuntimeError, "ca_mask_overlay_safe: n out of range");
163
+ }
164
+ va_start(ap, n);
165
+ for ( i = 0; i < n; i++ ) {
166
+ slist[i] = va_arg(ap, CArray *);
167
+ if ( slist[i] && ca_has_mask(slist[i]) ) any_mask = 1;
168
+ }
169
+ va_end(ap);
170
+
171
+ if ( ! any_mask ) return;
172
+
173
+ ca_update_mask(ca_out);
174
+ {
175
+ int created_new = 0;
176
+ if ( ! ca_out->mask ) {
177
+ ca_create_mask(ca_out);
178
+ created_new = 1;
179
+ }
180
+
181
+ boolean8_t *ma = (boolean8_t *) ca_out->mask->ptr;
182
+ ca_size_t elements = ca_out->elements;
183
+ ca_size_t j;
184
+
185
+ /* Fresh mask → zero-init. Existing mask (= bang variant where
186
+ ca_out IS one of the operands) → preserve as initial accumulator
187
+ (= ca_out's own contribution is already in `ma`, OR in others).
188
+ This is structurally equivalent to ca_copy_mask_overlay_n's
189
+ behavior which OR'd into existing mask without clearing. */
190
+ if ( created_new ) memset(ma, 0, elements);
191
+
192
+ for ( i = 0; i < n; i++ ) {
193
+ CArray *cs = slist[i];
194
+ if ( ! cs ) continue;
195
+ ca_update_mask(cs);
196
+ if ( ! cs->mask ) continue;
197
+
198
+ if ( ca_is_scalar(cs) ) {
199
+ boolean8_t bit = 0;
200
+ ca_xfer_all(cs->mask, &bit, CA_XFER_GET);
201
+ if ( bit ) memset(ma, 1, elements);
202
+ } else {
203
+ void *scratch = ca_lazy_arena_acquire(elements);
204
+ boolean8_t *ms = (boolean8_t *) scratch;
205
+ ca_xfer_all(cs->mask, scratch, CA_XFER_GET);
206
+ for ( j = 0; j < elements; j++ ) ma[j] |= ms[j];
207
+ ca_lazy_arena_release(scratch);
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ /* Monop driver. ca1 is input-only (the driver does not attach it); ca2
214
+ is the output (a new entity, attach legit). fast = ca_attach_is_alias(ca1)
215
+ → 1-shot; slow = ALLOCV + ca_xfer_all without ca_func[X].attach. */
29
216
  VALUE
30
217
  rb_ca_call_monop (VALUE self, ca_monop_func_t func[])
31
218
  {
@@ -34,28 +221,123 @@ rb_ca_call_monop (VALUE self, ca_monop_func_t func[])
34
221
 
35
222
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
36
223
 
37
- if ( ca_has_mask(ca1) ) {
38
- ca2 = ca_template_safe(ca1);
39
- }
40
- else {
41
- ca2 = ca_template(ca1);
224
+ /* Boolean-as-numeric promotion: an arithmetic monop (-@ etc.) has no
225
+ boolean kernel (func[CA_BOOLEAN] == ca_monop_not_implement), so coerce
226
+ a bool input to CA_INT64 -- `-b` yields [-1, 0, ...] as its 0/1 numeric
227
+ storage. Monops that DO define a boolean kernel are left as bool. */
228
+ if ( ca1->data_type == CA_BOOLEAN && func[CA_BOOLEAN] == ca_monop_not_implement ) {
229
+ self = rb_ca_wrap_readonly(self, INT2NUM(CA_INT64));
230
+ TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
42
231
  }
43
232
 
44
- ca2 = ca_template(ca1);
233
+ ca2 = ca_has_mask(ca1) ? ca_template_safe(ca1) : ca_template(ca1);
45
234
  out = ca_wrap_struct(ca2);
46
235
 
47
- ca_attach(ca1);
48
- ca_copy_mask_overlay(ca2, ca2->elements, 1, ca1);
49
- func[ca1->data_type](ca1->elements,
50
- ( ca2->mask ) ? (boolean8_t *)ca2->mask->ptr : NULL,
51
- ca1->ptr, 1,
52
- ca2->ptr, 1);
53
- ca_detach(ca1);
236
+ ca_mask_overlay_safe(ca2, 1, ca1);
54
237
 
55
- /* unresolved unbound repeat array generates unbound repeat array again */
56
- if ( ca1->obj_type == CA_OBJ_UNBOUND_REPEAT ) {
57
- CAUnboundRepeat *cx = (CAUnboundRepeat *) ca1;
58
- out = rb_ca_ubrep_new(rb_ca_ubrep_shave(self, out), cx->rep_ndim, cx->rep_dim);
238
+ if ( ca_attach_is_alias(ca1) ) {
239
+ ca_attach(ca1);
240
+ func[ca1->data_type](ca1->elements,
241
+ ( ca2->mask ) ? (boolean8_t *)ca2->mask->ptr : NULL,
242
+ ca1->ptr, 1,
243
+ ca2->ptr, 1);
244
+ ca_detach(ca1);
245
+ }
246
+ else {
247
+ /* Strided-walk fast path: when ca1 is a non-alias CAStride-family view
248
+ that composes to a ptr-bearing root, walk strides directly into the
249
+ kernel instead of staging via ca_xfer_all + a contig-kernel pass.
250
+ Saves 2x bandwidth (no gather buffer write+read). Output ca2 is a
251
+ contig entity; per-row output offset = row_idx * inner_count *
252
+ ca2->bytes. */
253
+ int strided_path_done = 0;
254
+ {
255
+ extern ca_operation_function_t ca_stride_func;
256
+ if ( ca_func[ca1->obj_type].attach == ca_stride_func.attach ) {
257
+ CAStride *cs1 = (CAStride *) ca1;
258
+ CArray *root1;
259
+ ca_size_t strides1[CA_RANK_MAX];
260
+ ca_size_t base1;
261
+ int8_t ndim = cs1->ndim;
262
+ int8_t k;
263
+ int ok = 1;
264
+
265
+ ca_stride_compose_to_root(cs1, &root1, strides1, &base1);
266
+
267
+ if ( !root1->ptr ) ok = 0;
268
+
269
+ /* element-stride conversion: strides must be byte-multiples of bytes */
270
+ if ( ok ) {
271
+ for ( k = 0; k < ndim; k++ ) {
272
+ if ( strides1[k] % ca1->bytes != 0 ) { ok = 0; break; }
273
+ }
274
+ }
275
+ if ( ok && (base1 % ca1->bytes != 0) ) ok = 0;
276
+
277
+ if ( ok ) {
278
+ int8_t inner_axis = ndim - 1;
279
+ ca_size_t inner_n = (ndim >= 1) ? cs1->dim[inner_axis] : ca1->elements;
280
+ ca_size_t s1_inner = (ndim >= 1) ? strides1[inner_axis] / ca1->bytes : 1;
281
+ ca_size_t idx[CA_RANK_MAX];
282
+ ca_size_t out_off = 0;
283
+ ca_size_t e1_strides[CA_RANK_MAX];
284
+ ca_size_t e1_base;
285
+ char *p1_root;
286
+
287
+ for ( k = 0; k < ndim; k++ ) {
288
+ e1_strides[k] = strides1[k] / ca1->bytes;
289
+ }
290
+ e1_base = base1 / ca1->bytes;
291
+ p1_root = (char *) root1->ptr;
292
+
293
+ for ( k = 0; k < ndim; k++ ) idx[k] = 0;
294
+
295
+ if ( ndim == 0 || ndim == 1 ) {
296
+ ca_size_t n_call = (ndim == 0) ? 1 : inner_n;
297
+ func[ca1->data_type](n_call,
298
+ ( ca2->mask ) ? (boolean8_t *) ca2->mask->ptr : NULL,
299
+ p1_root + e1_base * ca1->bytes,
300
+ (ndim == 0) ? 0 : s1_inner,
301
+ ca2->ptr, 1);
302
+ }
303
+ else {
304
+ while ( 1 ) {
305
+ ca_size_t off1 = e1_base;
306
+ for ( k = 0; k < ndim - 1; k++ ) {
307
+ off1 += idx[k] * e1_strides[k];
308
+ }
309
+ func[ca1->data_type](inner_n,
310
+ ca2->mask
311
+ ? ((boolean8_t *) ca2->mask->ptr) + out_off
312
+ : NULL,
313
+ p1_root + off1 * ca1->bytes, s1_inner,
314
+ (char *) ca2->ptr + out_off * ca2->bytes, 1);
315
+ out_off += inner_n;
316
+ k = ndim - 2;
317
+ while ( k >= 0 ) {
318
+ if ( ++idx[k] < cs1->dim[k] ) break;
319
+ idx[k] = 0; k--;
320
+ }
321
+ if ( k < 0 ) break;
322
+ }
323
+ }
324
+ strided_path_done = 1;
325
+ }
326
+ }
327
+ }
328
+
329
+ if ( !strided_path_done ) {
330
+ volatile VALUE h1 = Qnil;
331
+ char *p1;
332
+ (void) h1;
333
+ p1 = ALLOCV_N(char, h1, ca1->elements * ca1->bytes);
334
+ ca_xfer_all(ca1, p1, CA_XFER_GET);
335
+ func[ca1->data_type](ca1->elements,
336
+ ( ca2->mask ) ? (boolean8_t *)ca2->mask->ptr : NULL,
337
+ p1, 1,
338
+ ca2->ptr, 1);
339
+ ALLOCV_END(h1);
340
+ }
59
341
  }
60
342
 
61
343
  return out;
@@ -81,6 +363,139 @@ rb_ca_call_monop_bang (VALUE self, ca_monop_func_t func[])
81
363
  return self;
82
364
  }
83
365
 
366
+ /* Dtype-changing monop dispatch. Allocates output of data_type
367
+ out_data_types[in_data_type]. When out_data_type == in_data_type this is identical
368
+ to rb_ca_call_monop; when they differ (e.g. abs on cmplx128 -> f64),
369
+ the output array has different cell size from input. No bang form
370
+ (= data_type change in-place is ill-defined; bang must preserve data_type). */
371
+ VALUE
372
+ rb_ca_call_monop_typed (VALUE self, ca_monop_func_t func[],
373
+ int8_t out_data_types[])
374
+ {
375
+ volatile VALUE out;
376
+ CArray *ca1, *ca2;
377
+ int8_t out_dt;
378
+
379
+ TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
380
+
381
+ out_dt = out_data_types[ca1->data_type];
382
+ if ( out_dt < 0 ) {
383
+ rb_raise(rb_eCADataTypeError,
384
+ "data_type-changing monop not implemented for input data_type %d",
385
+ ca1->data_type);
386
+ }
387
+
388
+ /* Allocate output with the per-op output data_type. Same shape as input. */
389
+ ca2 = carray_new(out_dt, ca1->ndim, ca1->dim, 0, NULL);
390
+ out = ca_wrap_struct(ca2);
391
+
392
+ /* Same fast/slow pattern as rb_ca_call_monop. ca1 input-only, ca2 output. */
393
+ ca_mask_overlay_safe(ca2, 1, ca1);
394
+
395
+ if ( ca_attach_is_alias(ca1) ) {
396
+ ca_attach(ca1);
397
+ func[ca1->data_type](ca1->elements,
398
+ ( ca2->mask ) ? (boolean8_t *)ca2->mask->ptr : NULL,
399
+ ca1->ptr, 1,
400
+ ca2->ptr, 1);
401
+ ca_detach(ca1);
402
+ }
403
+ else {
404
+ /* Same strided-walk fast path as rb_ca_call_monop, but the output ca2
405
+ may have a different cell size (ca2->bytes may differ from
406
+ ca1->bytes, e.g. abs cmplx128 -> f64). */
407
+ int strided_path_done = 0;
408
+ {
409
+ extern ca_operation_function_t ca_stride_func;
410
+ if ( ca_func[ca1->obj_type].attach == ca_stride_func.attach ) {
411
+ CAStride *cs1 = (CAStride *) ca1;
412
+ CArray *root1;
413
+ ca_size_t strides1[CA_RANK_MAX];
414
+ ca_size_t base1;
415
+ int8_t ndim = cs1->ndim;
416
+ int8_t k;
417
+ int ok = 1;
418
+
419
+ ca_stride_compose_to_root(cs1, &root1, strides1, &base1);
420
+
421
+ if ( !root1->ptr ) ok = 0;
422
+
423
+ if ( ok ) {
424
+ for ( k = 0; k < ndim; k++ ) {
425
+ if ( strides1[k] % ca1->bytes != 0 ) { ok = 0; break; }
426
+ }
427
+ }
428
+ if ( ok && (base1 % ca1->bytes != 0) ) ok = 0;
429
+
430
+ if ( ok ) {
431
+ int8_t inner_axis = ndim - 1;
432
+ ca_size_t inner_n = (ndim >= 1) ? cs1->dim[inner_axis] : ca1->elements;
433
+ ca_size_t s1_inner = (ndim >= 1) ? strides1[inner_axis] / ca1->bytes : 1;
434
+ ca_size_t idx[CA_RANK_MAX];
435
+ ca_size_t out_off = 0;
436
+ ca_size_t e1_strides[CA_RANK_MAX];
437
+ ca_size_t e1_base;
438
+ char *p1_root;
439
+
440
+ for ( k = 0; k < ndim; k++ ) {
441
+ e1_strides[k] = strides1[k] / ca1->bytes;
442
+ }
443
+ e1_base = base1 / ca1->bytes;
444
+ p1_root = (char *) root1->ptr;
445
+
446
+ for ( k = 0; k < ndim; k++ ) idx[k] = 0;
447
+
448
+ if ( ndim == 0 || ndim == 1 ) {
449
+ ca_size_t n_call = (ndim == 0) ? 1 : inner_n;
450
+ func[ca1->data_type](n_call,
451
+ ( ca2->mask ) ? (boolean8_t *) ca2->mask->ptr : NULL,
452
+ p1_root + e1_base * ca1->bytes,
453
+ (ndim == 0) ? 0 : s1_inner,
454
+ ca2->ptr, 1);
455
+ }
456
+ else {
457
+ while ( 1 ) {
458
+ ca_size_t off1 = e1_base;
459
+ for ( k = 0; k < ndim - 1; k++ ) {
460
+ off1 += idx[k] * e1_strides[k];
461
+ }
462
+ func[ca1->data_type](inner_n,
463
+ ca2->mask
464
+ ? ((boolean8_t *) ca2->mask->ptr) + out_off
465
+ : NULL,
466
+ p1_root + off1 * ca1->bytes, s1_inner,
467
+ (char *) ca2->ptr + out_off * ca2->bytes, 1);
468
+ out_off += inner_n;
469
+ k = ndim - 2;
470
+ while ( k >= 0 ) {
471
+ if ( ++idx[k] < cs1->dim[k] ) break;
472
+ idx[k] = 0; k--;
473
+ }
474
+ if ( k < 0 ) break;
475
+ }
476
+ }
477
+ strided_path_done = 1;
478
+ }
479
+ }
480
+ }
481
+
482
+ if ( !strided_path_done ) {
483
+ volatile VALUE h1 = Qnil;
484
+ char *p1;
485
+ (void) h1;
486
+ p1 = ALLOCV_N(char, h1, ca1->elements * ca1->bytes);
487
+ ca_xfer_all(ca1, p1, CA_XFER_GET);
488
+ func[ca1->data_type](ca1->elements,
489
+ ( ca2->mask ) ? (boolean8_t *)ca2->mask->ptr : NULL,
490
+ p1, 1,
491
+ ca2->ptr, 1);
492
+ ALLOCV_END(h1);
493
+ }
494
+ }
495
+
496
+ return out;
497
+ }
498
+
84
499
  int
85
500
  rb_ca_test_castable (VALUE other)
86
501
  {
@@ -104,12 +519,119 @@ rb_ca_binop_pass_to_other (VALUE self, VALUE other, ID method)
104
519
  return rb_funcall(self, method, 1, other);
105
520
  }
106
521
 
522
+ /* Gather a boolean operand's value + mask bytes into vbuf/mbuf, broadcasting
523
+ a scalar operand across n cells. Uses ca_copy_data (materialise, no
524
+ attach) so views / lazy sources are handled transparently. */
525
+ static void
526
+ kleene_gather_bool (CArray *ca, boolean8_t *vbuf, boolean8_t *mbuf, ca_size_t n)
527
+ {
528
+ if ( ca->elements == n ) {
529
+ ca_copy_data(ca, (char *) vbuf);
530
+ ca_update_mask(ca);
531
+ if ( ca->mask ) {
532
+ ca_copy_data(ca->mask, (char *) mbuf);
533
+ }
534
+ else {
535
+ memset(mbuf, 0, n);
536
+ }
537
+ }
538
+ else { /* scalar operand: gather one, broadcast */
539
+ boolean8_t v = 0, m = 0;
540
+ ca_copy_data(ca, (char *) &v);
541
+ ca_update_mask(ca);
542
+ if ( ca->mask ) {
543
+ ca_copy_data(ca->mask, (char *) &m);
544
+ }
545
+ memset(vbuf, v, n);
546
+ memset(mbuf, m, n);
547
+ }
548
+ }
549
+
550
+ /* Kleene three-valued mask fixup for boolean AND (is_or=0) / OR (is_or=1).
551
+
552
+ After the value-blind binop, a masked output cell can still be resolved by
553
+ the *known* side: `unknown | true = true`, `unknown & false = false`. This
554
+ pass forces those cells to the known result and unmasks them; genuinely
555
+ undetermined cells (U|U, U&U, U|F, U&T) keep the blind mask. All other
556
+ cells were already correct from the value kernel.
557
+
558
+ Gate: boolean data type + output has a mask (else no-op -- the hot path is
559
+ untouched, integer bitwise stays blind). `out` is a fresh entity, so
560
+ out->ptr / out->mask->ptr are writable without attach. */
561
+ VALUE
562
+ ca_kleene_bool_fixup (VALUE vout, VALUE vself, VALUE vother, int is_or)
563
+ {
564
+ CArray *out, *a, *b;
565
+ volatile VALUE va, vb;
566
+ boolean8_t *ov, *om, *av, *am, *bv, *bm;
567
+ ca_size_t n, i;
568
+
569
+ TypedData_Get_Struct(vout, CArray, &carray_data_type, out);
570
+ if ( out->data_type != CA_BOOLEAN ) {
571
+ return vout; /* integer bitwise: blind, unchanged */
572
+ }
573
+ ca_update_mask(out);
574
+ if ( ! out->mask ) {
575
+ return vout; /* no undetermined cells to resolve */
576
+ }
577
+
578
+ /* Re-normalise the operands the same way rb_ca_call_binop did, so scalar /
579
+ CScalar / view operands all present as boolean CArrays. */
580
+ va = vself; vb = vother;
581
+ rb_ca_cast_self_or_other(&va, &vb);
582
+ TypedData_Get_Struct(va, CArray, &carray_data_type, a);
583
+ TypedData_Get_Struct(vb, CArray, &carray_data_type, b);
584
+
585
+ n = out->elements;
586
+ av = ALLOC_N(boolean8_t, n); am = ALLOC_N(boolean8_t, n);
587
+ bv = ALLOC_N(boolean8_t, n); bm = ALLOC_N(boolean8_t, n);
588
+ kleene_gather_bool(a, av, am, n);
589
+ kleene_gather_bool(b, bv, bm, n);
590
+
591
+ ov = (boolean8_t *) out->ptr;
592
+ om = (boolean8_t *) out->mask->ptr;
593
+ for (i = 0; i < n; i++) {
594
+ if ( ! om[i] ) {
595
+ continue; /* cell already determined */
596
+ }
597
+ if ( is_or ) {
598
+ if ( ( ! am[i] && av[i] ) || ( ! bm[i] && bv[i] ) ) { /* known TRUE */
599
+ ov[i] = 1;
600
+ om[i] = 0;
601
+ }
602
+ }
603
+ else {
604
+ if ( ( ! am[i] && ! av[i] ) || ( ! bm[i] && ! bv[i] ) ) { /* known FALSE */
605
+ ov[i] = 0;
606
+ om[i] = 0;
607
+ }
608
+ }
609
+ }
610
+
611
+ xfree(av); xfree(am); xfree(bv); xfree(bm);
612
+ return vout;
613
+ }
614
+
615
+ /* Binop driver, split into fast / slow path.
616
+ - fast: both operands alias-attachable (= entity / cscalar / contig
617
+ CAStride) → 1-shot ca_attach_n + kernel call.
618
+ - slow: at least one operand needs materialise → use ca_xfer_all to
619
+ gather into ALLOCV scratch, never calling ca_func[X].attach
620
+ on the operand. Honors the core invariant "the driver does
621
+ not attach an input-only operand", which keeps CAStack /
622
+ CATile expansion, attach-hostile roots, and unattachable test
623
+ fixtures working.
624
+ Output `ca3` is always a new entity (write target, attach legitimate). */
107
625
  VALUE
108
626
  rb_ca_call_binop (volatile VALUE self, volatile VALUE other,
109
627
  ca_binop_func_t func[])
110
628
  {
111
629
  volatile VALUE out;
112
630
  CArray *ca1, *ca2, *ca3; /* ca3 = ca1.op(ca2) */
631
+ int self_is_scalar, other_is_scalar;
632
+ ca_size_t n_kernel;
633
+ ca_size_t i1, i2, i3;
634
+ int fast_path;
113
635
 
114
636
  /* do implicit casting and resolving unbound repeat array */
115
637
  rb_ca_cast_self_or_other(&self, &other);
@@ -117,103 +639,394 @@ rb_ca_call_binop (volatile VALUE self, volatile VALUE other,
117
639
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
118
640
  TypedData_Get_Struct(other, CArray, &carray_data_type, ca2);
119
641
 
120
- ca_attach_n(2, ca1, ca2);
642
+ /* Boolean-as-numeric promotion: an arithmetic op (+ - * / % ** ...) has
643
+ no boolean kernel, so its func[CA_BOOLEAN] slot is ca_binop_not_implement.
644
+ When both operands promoted to CA_BOOLEAN (= bool op bool), coerce them
645
+ to CA_INT64 so `b1 + b2` behaves as its 0/1 numeric storage. Signed
646
+ (not the u64 used by bool reductions) because `b1 - b2` must reach -1.
647
+ Logical ops (& | ^) DO have a boolean kernel, so this leaves them as
648
+ bool. A bool op numeric already promoted away from CA_BOOLEAN above. */
649
+ if ( ca1->data_type == CA_BOOLEAN && func[CA_BOOLEAN] == ca_binop_not_implement ) {
650
+ self = rb_ca_wrap_readonly(self, INT2NUM(CA_INT64));
651
+ other = rb_ca_wrap_readonly(other, INT2NUM(CA_INT64));
652
+ TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
653
+ TypedData_Get_Struct(other, CArray, &carray_data_type, ca2);
654
+ }
655
+
656
+ self_is_scalar = RTEST(rb_obj_is_cscalar(self));
657
+ other_is_scalar = RTEST(rb_obj_is_cscalar(other));
121
658
 
122
- /* main operation */
123
- if ( rb_obj_is_cscalar(self) ) {
124
- if ( rb_obj_is_cscalar(other) ) { /* scalar vs scalar */
125
- if ( ca_has_mask(ca1) || ca_has_mask(ca2) ) {
126
- ca3 = ca_template_safe(ca1);
659
+ /* output template + kernel n + strides (= preserve existing dispatch
660
+ matrix: scalar×scalar / scalar×array / array×scalar / array×array) */
661
+ if ( self_is_scalar && other_is_scalar ) {
662
+ ca3 = ( ca_has_mask(ca1) || ca_has_mask(ca2) ) ? ca_template_safe(ca1)
663
+ : ca_template(ca1);
664
+ n_kernel = ca1->elements;
665
+ i1 = 0; i2 = 0; i3 = 0;
666
+ }
667
+ else if ( self_is_scalar /* && !other_is_scalar */ ) {
668
+ ca3 = ( ca_has_mask(ca1) || ca_has_mask(ca2) ) ? ca_template_safe(ca2)
669
+ : ca_template(ca2);
670
+ n_kernel = ca2->elements;
671
+ i1 = 0; i2 = 1; i3 = 1;
672
+ }
673
+ else if ( other_is_scalar /* && !self_is_scalar */ ) {
674
+ ca3 = ( ca_has_mask(ca1) || ca_has_mask(ca2) ) ? ca_template_safe(ca1)
675
+ : ca_template(ca1);
676
+ n_kernel = ca1->elements;
677
+ i1 = 1; i2 = 0; i3 = 1;
678
+ }
679
+ else { /* array vs array */
680
+ if ( ca1->elements != ca2->elements ) {
681
+ rb_raise(rb_eRuntimeError, "elements mismatch (%" PRId64 " <-> %" PRId64 ")",
682
+ (ca_size_t) ca1->elements,
683
+ (ca_size_t) ca2->elements);
684
+ }
685
+ ca3 = ( ca_has_mask(ca1) || ca_has_mask(ca2) ) ? ca_template_safe(ca1)
686
+ : ca_template(ca1);
687
+ n_kernel = ca1->elements;
688
+ i1 = 1; i2 = 1; i3 = 1;
689
+ }
690
+ out = ca_wrap_struct(ca3);
691
+
692
+ /* Mask overlay via the safe variant: materialises operand masks with
693
+ ca_xfer_all instead of attaching them, so a mask on an attach-hostile
694
+ input-only operand does not raise. */
695
+ ca_mask_overlay_safe(ca3, 2, ca1, ca2);
696
+
697
+ /* path decision: alias-attachable both → fast path */
698
+ fast_path = ca_attach_is_alias(ca1) && ca_attach_is_alias(ca2);
699
+
700
+ if ( fast_path ) {
701
+ /* FAST PATH: zero behavioral change for the hot case. */
702
+ ca_attach_n(2, ca1, ca2);
703
+ func[ca1->data_type](n_kernel,
704
+ ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
705
+ ca1->ptr, i1,
706
+ ca2->ptr, i2,
707
+ ca3->ptr, i3);
708
+ ca_detach_n(2, ca1, ca2);
709
+ }
710
+ else if ( ca1 == ca2 ) {
711
+ /* SAME-OPERAND SHARING. When the same view appears on both sides
712
+ (`mt + mt`, `(view) < (view)` etc.), materialise once and share the
713
+ scratch buffer between both kernel inputs. When !fast_path &&
714
+ ca1 == ca2, ca1 must be non-alias (if it were alias, fast_path would
715
+ be true), so a single ALLOCV + ca_xfer_all suffices. */
716
+ volatile VALUE h_shared = Qnil;
717
+ char *p_shared;
718
+ (void) h_shared;
719
+
720
+ p_shared = ALLOCV_N(char, h_shared, ca1->elements * ca1->bytes);
721
+ ca_xfer_all(ca1, p_shared, CA_XFER_GET);
722
+ func[ca1->data_type](n_kernel,
723
+ ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
724
+ p_shared, i1,
725
+ p_shared, i2,
726
+ ca3->ptr, i3);
727
+ ALLOCV_END(h_shared);
728
+ }
729
+ else {
730
+ /* SLOW PATH (distinct operands): CHUNKED materialise via arena.
731
+ Memory peak: O(operand_size) → O(chunk). Per-operand decision:
732
+ - i==0 scalar → gather 1 element once (fixed, stride 0 in kernel)
733
+ - alias array → use ca->ptr + off*bytes (no copy, contig alias)
734
+ - non-alias array → per-chunk gather into arena scratch
735
+
736
+ Threshold dispatch: count the non-alias array operands. If only 1
737
+ needs per-chunk gather, chunking gives no memory-peak win (the other
738
+ operand is already alias = no scratch) but pays per-iter dispatch
739
+ overhead, so fall back to a 1-shot ALLOCV for that operand. Only
740
+ when both operands need gather (a structural 2x peak reduction) do
741
+ we keep the chunked path.
742
+
743
+ Decision matrix:
744
+ - both array & both non-alias (= mt + mt2) → CHUNKED (peak 2x win)
745
+ - one non-alias array + one alias/scalar (= mt + 3.14, mt + entity)
746
+ → 1-shot ALLOCV */
747
+ char *p1_src = NULL, *p2_src = NULL;
748
+ void *s1_arena = NULL, *s2_arena = NULL;
749
+ int gather_per_chunk1 = 0, gather_per_chunk2 = 0;
750
+ int attached1 = 0, attached2 = 0;
751
+ int8_t dt = ca1->data_type;
752
+ ca_size_t chunk_n;
753
+ ca_size_t off;
754
+ int nonalias_arrays;
755
+ int use_chunked;
756
+
757
+ nonalias_arrays = 0;
758
+ if ( i1 == 1 && !ca_attach_is_alias(ca1) ) nonalias_arrays++;
759
+ if ( i2 == 1 && !ca_attach_is_alias(ca2) ) nonalias_arrays++;
760
+ use_chunked = (nonalias_arrays >= 2);
761
+
762
+ /* Unified strided-walk path. Eligibility: both operands are array
763
+ (i==1) and at least one needs gather. Each operand must compose to
764
+ a ptr-bearing root,
765
+ either as CAStride family (via ca_stride_compose_to_root) or as an
766
+ entity (CA_OBJ_ARRAY / CA_OBJ_ARRAY_WRAP) treated as a view
767
+ CAStride with row-major byte strides + base 0. When eligible, walk
768
+ per-row directly into the kernel and skip the chunked-gather / ALLOCV
769
+ materialise pass — saves 2x bandwidth (no gather buffer write+read).
770
+ Output ca3 is contig entity; per-row output offset = row_idx *
771
+ inner_count. The use_chunked / ALLOCV blocks below remain as a
772
+ fallback (dead on the success path) but fire for views outside the
773
+ CAStride+entity family (CASelect / CAMapping / CAReduce
774
+ etc.). */
775
+ if ( i1 == 1 && i2 == 1 && nonalias_arrays >= 1 ) {
776
+ extern ca_operation_function_t ca_stride_func;
777
+ CArray *root1 = NULL, *root2 = NULL;
778
+ ca_size_t strides1[CA_RANK_MAX], strides2[CA_RANK_MAX];
779
+ ca_size_t base1 = 0, base2 = 0;
780
+ int8_t ndim = ca1->ndim;
781
+ int8_t k;
782
+ int ok = 1;
783
+
784
+ /* operand 1: CAStride family -> compose, entity -> synthesize */
785
+ if ( ca_func[ca1->obj_type].attach == ca_stride_func.attach ) {
786
+ ca_stride_compose_to_root((CAStride *) ca1, &root1, strides1, &base1);
127
787
  }
128
- else {
129
- ca3 = ca_template(ca1);
788
+ else if ( ca1->obj_type == CA_OBJ_ARRAY ||
789
+ ca1->obj_type == CA_OBJ_ARRAY_WRAP ) {
790
+ ca_size_t stride_bytes = ca1->bytes;
791
+ root1 = ca1;
792
+ base1 = 0;
793
+ for ( k = ca1->ndim - 1; k >= 0; k-- ) {
794
+ strides1[k] = stride_bytes;
795
+ stride_bytes *= ca1->dim[k];
796
+ }
797
+ }
798
+ else ok = 0;
799
+
800
+ if ( ok ) {
801
+ if ( ca_func[ca2->obj_type].attach == ca_stride_func.attach ) {
802
+ ca_stride_compose_to_root((CAStride *) ca2, &root2, strides2, &base2);
803
+ }
804
+ else if ( ca2->obj_type == CA_OBJ_ARRAY ||
805
+ ca2->obj_type == CA_OBJ_ARRAY_WRAP ) {
806
+ ca_size_t stride_bytes = ca2->bytes;
807
+ root2 = ca2;
808
+ base2 = 0;
809
+ for ( k = ca2->ndim - 1; k >= 0; k-- ) {
810
+ strides2[k] = stride_bytes;
811
+ stride_bytes *= ca2->dim[k];
812
+ }
813
+ }
814
+ else ok = 0;
130
815
  }
131
- out = ca_wrap_struct(ca3);
132
816
 
133
- ca_copy_mask_overlay(ca3, ca3->elements, 2, ca1, ca2);
134
- func[ca1->data_type](ca1->elements,
135
- ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
136
- ca1->ptr, 0,
137
- ca2->ptr, 0,
138
- ca3->ptr, 0);
817
+ if ( ok ) {
818
+ if ( !root1->ptr || !root2->ptr ) ok = 0;
819
+ if ( ca2->ndim != ndim ) ok = 0;
820
+
821
+ /* element-stride conversion: strides must be byte-multiples of bytes */
822
+ if ( ok ) {
823
+ for ( k = 0; k < ndim; k++ ) {
824
+ if ( strides1[k] % ca1->bytes != 0 ) { ok = 0; break; }
825
+ if ( strides2[k] % ca2->bytes != 0 ) { ok = 0; break; }
826
+ }
827
+ }
828
+ if ( ok && (base1 % ca1->bytes != 0 || base2 % ca2->bytes != 0) ) ok = 0;
829
+ }
830
+
831
+ if ( ok ) {
832
+ int8_t inner_axis = ndim - 1;
833
+ ca_size_t inner_n = (ndim >= 1) ? ca1->dim[inner_axis] : ca1->elements;
834
+ ca_size_t s1_inner = (ndim >= 1) ? strides1[inner_axis] / ca1->bytes : 1;
835
+ ca_size_t s2_inner = (ndim >= 1) ? strides2[inner_axis] / ca2->bytes : 1;
836
+ ca_size_t idx[CA_RANK_MAX];
837
+ ca_size_t out_off = 0;
838
+ ca_size_t e1_strides[CA_RANK_MAX], e2_strides[CA_RANK_MAX];
839
+ ca_size_t e1_base, e2_base;
840
+ char *p1_root, *p2_root;
841
+
842
+ for ( k = 0; k < ndim; k++ ) {
843
+ e1_strides[k] = strides1[k] / ca1->bytes;
844
+ e2_strides[k] = strides2[k] / ca2->bytes;
845
+ }
846
+ e1_base = base1 / ca1->bytes;
847
+ e2_base = base2 / ca2->bytes;
848
+ p1_root = (char *) root1->ptr;
849
+ p2_root = (char *) root2->ptr;
850
+
851
+ for ( k = 0; k < ndim; k++ ) idx[k] = 0;
852
+
853
+ if ( ndim == 0 || ndim == 1 ) {
854
+ ca_size_t n_call = (ndim == 0) ? 1 : inner_n;
855
+ func[dt](n_call,
856
+ ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
857
+ p1_root + e1_base * ca1->bytes, (ndim == 0) ? 0 : s1_inner,
858
+ p2_root + e2_base * ca2->bytes, (ndim == 0) ? 0 : s2_inner,
859
+ ca3->ptr, i3);
860
+ }
861
+ else {
862
+ while ( 1 ) {
863
+ ca_size_t off1 = e1_base, off2 = e2_base;
864
+ for ( k = 0; k < ndim - 1; k++ ) {
865
+ off1 += idx[k] * e1_strides[k];
866
+ off2 += idx[k] * e2_strides[k];
867
+ }
868
+ func[dt](inner_n,
869
+ ca3->mask
870
+ ? ((boolean8_t *) ca3->mask->ptr) + out_off
871
+ : NULL,
872
+ p1_root + off1 * ca1->bytes, s1_inner,
873
+ p2_root + off2 * ca2->bytes, s2_inner,
874
+ (char *) ca3->ptr + out_off * ca3->bytes, i3);
875
+ out_off += inner_n;
876
+ k = ndim - 2;
877
+ while ( k >= 0 ) {
878
+ if ( ++idx[k] < ca1->dim[k] ) break;
879
+ idx[k] = 0; k--;
880
+ }
881
+ if ( k < 0 ) break;
882
+ }
883
+ }
884
+
885
+ return out;
886
+ }
887
+ /* else fall through to the use_chunked / ALLOCV path */
139
888
  }
140
- else { /* scalar vs array */
141
- if ( ca_has_mask(ca1) || ca_has_mask(ca2) ) {
142
- ca3 = ca_template_safe(ca2);
889
+
890
+ if ( !use_chunked ) {
891
+ /* 1-shot ALLOCV path. At most 1 operand needs ALLOCV; the other is
892
+ alias-direct. */
893
+ volatile VALUE h1 = Qnil, h2 = Qnil;
894
+ char *p1, *p2;
895
+ int a1 = 0, a2 = 0;
896
+ (void) h1; (void) h2;
897
+
898
+ if ( ca_attach_is_alias(ca1) ) {
899
+ ca_attach(ca1); p1 = (char *) ca1->ptr; a1 = 1;
900
+ } else {
901
+ p1 = ALLOCV_N(char, h1, ca1->elements * ca1->bytes);
902
+ ca_xfer_all(ca1, p1, CA_XFER_GET);
143
903
  }
144
- else {
145
- ca3 = ca_template(ca2);
904
+ if ( ca_attach_is_alias(ca2) ) {
905
+ ca_attach(ca2); p2 = (char *) ca2->ptr; a2 = 1;
906
+ } else {
907
+ p2 = ALLOCV_N(char, h2, ca2->elements * ca2->bytes);
908
+ ca_xfer_all(ca2, p2, CA_XFER_GET);
146
909
  }
147
- out = ca_wrap_struct(ca3);
148
910
 
149
- ca_copy_mask_overlay(ca3, ca3->elements, 2, ca1, ca2);
150
- func[ca1->data_type](ca2->elements,
151
- ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
152
- ca1->ptr, 0,
153
- ca2->ptr, 1,
154
- ca3->ptr, 1);
911
+ func[dt](n_kernel,
912
+ ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
913
+ p1, i1,
914
+ p2, i2,
915
+ ca3->ptr, i3);
916
+
917
+ if ( a2 ) { ca_detach(ca2); } else { ALLOCV_END(h2); }
918
+ if ( a1 ) { ca_detach(ca1); } else { ALLOCV_END(h1); }
919
+
920
+ return out;
155
921
  }
156
- }
157
- else { /* array vs scalar */
158
- if ( rb_obj_is_cscalar(other) ) {
159
- if ( ca_has_mask(ca1) || ca_has_mask(ca2) ) {
160
- ca3 = ca_template_safe(ca1);
161
- }
162
- else {
163
- ca3 = ca_template(ca1);
922
+
923
+ /* Compute chunk_n: target ~32KB worth of cells, row-aligned to the
924
+ larger-inner operand's outer axis. */
925
+ {
926
+ ca_size_t inner = 1;
927
+ ca_size_t maxb = ca1->bytes > ca2->bytes ? ca1->bytes : ca2->bytes;
928
+ if (ca3->bytes > maxb) maxb = ca3->bytes;
929
+ if (i1 == 1) inner = ca_chunk_inner_size(ca1);
930
+ if (i2 == 1) {
931
+ ca_size_t inner2 = ca_chunk_inner_size(ca2);
932
+ if (inner2 > inner) inner = inner2;
164
933
  }
165
- out = ca_wrap_struct(ca3);
934
+ chunk_n = ca_chunk_compute_n(n_kernel, inner, maxb);
935
+ }
166
936
 
167
- ca_copy_mask_overlay(ca3, ca3->elements, 2, ca1, ca2);
168
- func[ca1->data_type](ca1->elements,
169
- ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
170
- ca1->ptr, 1,
171
- ca2->ptr, 0,
172
- ca3->ptr, 1);
937
+ ca_lazy_arena_enter();
938
+
939
+ /* ca1 acquire */
940
+ if ( i1 == 0 ) {
941
+ /* scalar: gather 1 element once, kernel re-reads with stride 0 */
942
+ if ( ca_attach_is_alias(ca1) ) {
943
+ ca_attach(ca1);
944
+ p1_src = (char *) ca1->ptr;
945
+ attached1 = 1;
946
+ } else {
947
+ s1_arena = ca_op_acquire_operand_scratch(ca1, 1);
948
+ ca_xfer_all(ca1, s1_arena, CA_XFER_GET);
949
+ p1_src = (char *) s1_arena;
950
+ }
951
+ } else if ( ca_attach_is_alias(ca1) ) {
952
+ ca_attach(ca1);
953
+ p1_src = (char *) ca1->ptr;
954
+ attached1 = 1;
955
+ } else {
956
+ s1_arena = ca_op_acquire_operand_scratch(ca1, chunk_n);
957
+ p1_src = (char *) s1_arena;
958
+ gather_per_chunk1 = 1;
173
959
  }
174
- else { /* array vs array */
175
- if ( ca1->elements != ca2->elements ) {
176
- rb_raise(rb_eRuntimeError, "elements mismatch (%lld <-> %lld)",
177
- (ca_size_t) ca1->elements, (ca_size_t) ca2->elements);
960
+
961
+ /* ca2 acquire (mirror) */
962
+ if ( i2 == 0 ) {
963
+ if ( ca_attach_is_alias(ca2) ) {
964
+ ca_attach(ca2);
965
+ p2_src = (char *) ca2->ptr;
966
+ attached2 = 1;
967
+ } else {
968
+ s2_arena = ca_op_acquire_operand_scratch(ca2, 1);
969
+ ca_xfer_all(ca2, s2_arena, CA_XFER_GET);
970
+ p2_src = (char *) s2_arena;
178
971
  }
179
- if ( ca_has_mask(ca1) || ca_has_mask(ca2) ) {
180
- ca3 = ca_template_safe(ca1);
972
+ } else if ( ca_attach_is_alias(ca2) ) {
973
+ ca_attach(ca2);
974
+ p2_src = (char *) ca2->ptr;
975
+ attached2 = 1;
976
+ } else {
977
+ s2_arena = ca_op_acquire_operand_scratch(ca2, chunk_n);
978
+ p2_src = (char *) s2_arena;
979
+ gather_per_chunk2 = 1;
980
+ }
981
+
982
+ /* chunk loop: walk n_kernel cells in chunk_n strides */
983
+ for ( off = 0; off < n_kernel; off += chunk_n ) {
984
+ ca_size_t n_done = (off + chunk_n > n_kernel) ? n_kernel - off : chunk_n;
985
+ char *p1, *p2;
986
+
987
+ if ( gather_per_chunk1 ) {
988
+ ca_chunked_gather(ca1, off, n_done, s1_arena);
989
+ p1 = (char *) s1_arena;
990
+ } else {
991
+ p1 = p1_src + (i1 ? off * ca1->bytes : 0);
181
992
  }
182
- else {
183
- ca3 = ca_template(ca1);
993
+ if ( gather_per_chunk2 ) {
994
+ ca_chunked_gather(ca2, off, n_done, s2_arena);
995
+ p2 = (char *) s2_arena;
996
+ } else {
997
+ p2 = p2_src + (i2 ? off * ca2->bytes : 0);
184
998
  }
185
- out = ca_wrap_struct(ca3);
186
999
 
187
- ca_copy_mask_overlay(ca3, ca3->elements, 2, ca1, ca2);
188
- func[ca1->data_type](ca1->elements,
189
- ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
190
- ca1->ptr, 1,
191
- ca2->ptr, 1,
192
- ca3->ptr, 1);
1000
+ func[dt](n_done,
1001
+ ca3->mask ? ((boolean8_t *) ca3->mask->ptr) + (i3 ? off : 0)
1002
+ : NULL,
1003
+ p1, i1,
1004
+ p2, i2,
1005
+ (char *) ca3->ptr + (i3 ? off * ca3->bytes : 0), i3);
193
1006
  }
194
- }
195
1007
 
196
- ca_detach_n(2, ca1, ca2);
1008
+ if ( s2_arena ) ca_lazy_arena_release(s2_arena);
1009
+ if ( s1_arena ) ca_lazy_arena_release(s1_arena);
1010
+ if ( attached2 ) ca_detach(ca2);
1011
+ if ( attached1 ) ca_detach(ca1);
197
1012
 
198
- /* unresolved unbound repeat array generates unbound repeat array again */
199
- if ( ca1->obj_type == CA_OBJ_UNBOUND_REPEAT ) {
200
- CAUnboundRepeat *cx = (CAUnboundRepeat *) ca1;
201
- out = rb_ca_ubrep_new(rb_ca_ubrep_shave(self, out), cx->rep_ndim, cx->rep_dim);
202
- }
203
-
204
- /* unresolved unbound repeat array generates unbound repeat array again */
205
- if ( ca2->obj_type == CA_OBJ_UNBOUND_REPEAT ) {
206
- CAUnboundRepeat *cx = (CAUnboundRepeat *) ca2;
207
- out = rb_ca_ubrep_new(rb_ca_ubrep_shave(other, out), cx->rep_ndim, cx->rep_dim);
1013
+ ca_lazy_arena_exit();
208
1014
  }
209
1015
 
210
1016
  return out;
211
1017
  }
212
1018
 
1019
+ /* Bang (in-place) variant. Invariant: the input-only operand (= other)
1020
+ must not be attached; self IS the output (write target, attach
1021
+ legitimate). Same fast/slow pattern as rb_ca_call_binop, applied to
1022
+ `other` only.
1023
+ self always goes through ca_attach + ca_sync (= write-back to root). */
213
1024
  VALUE
214
1025
  rb_ca_call_binop_bang (VALUE self, VALUE other, ca_binop_func_t func[])
215
1026
  {
216
1027
  CArray *ca1, *ca2; /* ca1.op!(ca2) */
1028
+ int self_is_scalar, other_is_scalar;
1029
+ ca_size_t i1, i2;
217
1030
 
218
1031
  rb_ca_modify(self);
219
1032
 
@@ -223,63 +1036,465 @@ rb_ca_call_binop_bang (VALUE self, VALUE other, ca_binop_func_t func[])
223
1036
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
224
1037
  TypedData_Get_Struct(other, CArray, &carray_data_type, ca2);
225
1038
 
226
- ca_attach_n(2, ca1, ca2);
1039
+ self_is_scalar = RTEST(rb_obj_is_cscalar(self));
1040
+ other_is_scalar = RTEST(rb_obj_is_cscalar(other));
227
1041
 
228
- /* main operation */
229
- if ( rb_obj_is_cscalar(self) ) {
230
- if ( rb_obj_is_cscalar(other) ) { /* scalar vs scalar */
231
- ca_copy_mask_overlay(ca1, ca1->elements, 2, ca1, ca2);
232
- func[ca1->data_type](ca1->elements,
233
- ( ca1->mask ) ? (boolean8_t *) ca1->mask->ptr : NULL,
234
- ca1->ptr, 0,
235
- ca2->ptr, 0,
236
- ca1->ptr, 0);
1042
+ /* self is the write target, so its shape is the result's by definition
1043
+ and the destination rule applies (the same one assignment uses), not
1044
+ the symmetric one a binary operation is held to. */
1045
+ if ( !self_is_scalar && !other_is_scalar ) {
1046
+ ca_broadcast_to_destination(self, &other);
1047
+ TypedData_Get_Struct(other, CArray, &carray_data_type, ca2);
1048
+ other_is_scalar = RTEST(rb_obj_is_cscalar(other));
1049
+ }
1050
+ if ( self_is_scalar && !other_is_scalar &&
1051
+ ca1->elements != ca2->elements ) {
1052
+ rb_raise(rb_eRuntimeError, "elements mismatch (%" PRId64 " <-> %" PRId64 ")",
1053
+ (ca_size_t) ca1->elements,
1054
+ (ca_size_t) ca2->elements);
1055
+ }
1056
+
1057
+ /* kernel strides: cscalar → 0 (broadcast). ca1 is both src1 (input)
1058
+ and dst (output), same stride. ca2 is input only. */
1059
+ i1 = self_is_scalar ? 0 : 1;
1060
+ i2 = other_is_scalar ? 0 : 1;
1061
+
1062
+ /* self IS the output (= write target). Always attach + ca_sync
1063
+ (= legitimate per refined invariant; self can be view e.g.
1064
+ `arr[i,nil].add!(b)`, sync writes back to root). */
1065
+ ca_attach(ca1);
1066
+ ca_mask_overlay_safe(ca1, 2, ca1, ca2);
1067
+
1068
+ /* other is input only: fast path if alias-cheap, else materialise
1069
+ via ca_xfer_all without ca_func[X].attach. */
1070
+ if ( ca_attach_is_alias(ca2) ) {
1071
+ ca_attach(ca2);
1072
+ func[ca1->data_type](ca1->elements,
1073
+ ( ca1->mask ) ? (boolean8_t *) ca1->mask->ptr : NULL,
1074
+ ca1->ptr, i1,
1075
+ ca2->ptr, i2,
1076
+ ca1->ptr, i1);
1077
+ ca_detach(ca2);
1078
+ }
1079
+ else {
1080
+ volatile VALUE h2 = Qnil;
1081
+ char *p2;
1082
+ (void) h2;
1083
+ p2 = ALLOCV_N(char, h2, ca2->elements * ca2->bytes);
1084
+ ca_xfer_all(ca2, p2, CA_XFER_GET);
1085
+ func[ca1->data_type](ca1->elements,
1086
+ ( ca1->mask ) ? (boolean8_t *) ca1->mask->ptr : NULL,
1087
+ ca1->ptr, i1,
1088
+ p2, i2,
1089
+ ca1->ptr, i1);
1090
+ ALLOCV_END(h2);
1091
+ }
1092
+
1093
+ ca_sync(ca1);
1094
+ ca_detach(ca1);
1095
+
1096
+ return self;
1097
+ }
1098
+
1099
+ /* ------------------------------------------------------------------- */
1100
+ /* Triop driver — 3 inputs, 1 output, eager-only. */
1101
+ /* */
1102
+ /* Approach: instead of enumerating the 2^3 = 8 scalar/array combos in */
1103
+ /* line as `rb_ca_call_binop` does, we reuse `ca_set_iterator(3, ...)` */
1104
+ /* from carray_call_cfunc.c which collapses any operand with */
1105
+ /* `is_scalar == true` to a stride-0 walker. Same uniform inner loop */
1106
+ /* for all 8 cases. Output is templated from the first non-scalar */
1107
+ /* operand (or self if all are scalar). */
1108
+ /* ------------------------------------------------------------------- */
1109
+
1110
+ static VALUE
1111
+ rb_ca_triop_select_template (VALUE self, VALUE other2, VALUE other3,
1112
+ CArray *ca1, CArray *ca2, CArray *ca3)
1113
+ {
1114
+ /* Template from the first non-scalar operand; fall back to self if
1115
+ all three are scalars. Mask is allocated when any operand has
1116
+ a mask. */
1117
+ int has_mask = ca_has_mask(ca1) || ca_has_mask(ca2) || ca_has_mask(ca3);
1118
+ CArray *src;
1119
+ if ( ! rb_obj_is_cscalar(self) ) src = ca1;
1120
+ else if ( ! rb_obj_is_cscalar(other2) ) src = ca2;
1121
+ else if ( ! rb_obj_is_cscalar(other3) ) src = ca3;
1122
+ else src = ca1;
1123
+ return has_mask ? ca_wrap_struct(ca_template_safe(src))
1124
+ : ca_wrap_struct(ca_template(src));
1125
+ }
1126
+
1127
+ /* Per-operand acquire/release macros for input-only operands. Used by
1128
+ the triop / bincmp drivers where 3 inputs make inline branching
1129
+ unwieldy.
1130
+
1131
+ - alias-cheap operand → ca_attach (= O(1)) + use ca->ptr directly
1132
+ - else → ALLOCV scratch + ca_xfer_all without ca_func[X].attach
1133
+
1134
+ Pair ACQUIRE / RELEASE; `h` must be a volatile VALUE declared by
1135
+ caller (= ALLOCV_END requires it even on alias-cheap branch where
1136
+ ALLOCV_N wasn't actually called, since holder stays Qnil = no-op). */
1137
+ #define EAGER_ACQUIRE_INPUT(ca_, p_, h_, attached_) do { \
1138
+ if ( ca_attach_is_alias(ca_) ) { \
1139
+ ca_attach(ca_); \
1140
+ (p_) = (char *)(ca_)->ptr; \
1141
+ (attached_) = 1; \
1142
+ } \
1143
+ else { \
1144
+ (p_) = ALLOCV_N(char, (h_), (ca_)->elements * (ca_)->bytes); \
1145
+ ca_xfer_all((ca_), (p_), CA_XFER_GET); \
1146
+ (attached_) = 0; \
1147
+ } \
1148
+ } while (0)
1149
+
1150
+ #define EAGER_RELEASE_INPUT(ca_, h_, attached_) do { \
1151
+ if ( (attached_) ) { ca_detach(ca_); } \
1152
+ else { ALLOCV_END(h_); } \
1153
+ } while (0)
1154
+
1155
+ /* triop driver. 3 inputs are input-only (the driver does not attach
1156
+ them); cao = new entity output (attach is legit). Each input
1157
+ independently uses fast (= alias) or slow (= ALLOCV + ca_xfer_all)
1158
+ path. */
1159
+ VALUE
1160
+ rb_ca_call_triop (VALUE self, VALUE other2, VALUE other3,
1161
+ ca_triop_func_t func[])
1162
+ {
1163
+ volatile VALUE out;
1164
+ CArray *ca1, *ca2, *ca3, *cao;
1165
+
1166
+ /* Pairwise data_type promotion: ((self, other2) -> common), then
1167
+ ((self', other3) -> common). Mirrors how the binop driver normalises
1168
+ two operands; for triop we apply it twice. After this, all three
1169
+ CArrays share the same data_type (and unbound-repeats are resolved). */
1170
+ rb_ca_cast_self_or_other(&self, &other2);
1171
+ rb_ca_cast_self_or_other(&self, &other3);
1172
+ rb_ca_cast_self_or_other(&other2, &other3);
1173
+ /* one more pass to re-normalise self vs other2 in case the
1174
+ other2/other3 cast widened other2 above self's data_type */
1175
+ rb_ca_cast_self_or_other(&self, &other2);
1176
+
1177
+ TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
1178
+ TypedData_Get_Struct(other2, CArray, &carray_data_type, ca2);
1179
+ TypedData_Get_Struct(other3, CArray, &carray_data_type, ca3);
1180
+
1181
+ /* Boolean-as-numeric promotion (same rule as rb_ca_call_binop): an
1182
+ arithmetic triop (fma / fms) has no boolean kernel, so its
1183
+ func[CA_BOOLEAN] slot is ca_triop_not_implement. When all three
1184
+ operands promoted to CA_BOOLEAN (= every operand boolean), coerce
1185
+ them to CA_INT64 so `a * b + c` behaves as their 0/1 numeric storage
1186
+ (signed, so a product/sum can reach negative in fms). A boolean
1187
+ mixed with a numeric already promoted away from CA_BOOLEAN via the
1188
+ pairwise casts above. */
1189
+ if ( ca1->data_type == CA_BOOLEAN && func[CA_BOOLEAN] == ca_triop_not_implement ) {
1190
+ self = rb_ca_wrap_readonly(self, INT2NUM(CA_INT64));
1191
+ other2 = rb_ca_wrap_readonly(other2, INT2NUM(CA_INT64));
1192
+ other3 = rb_ca_wrap_readonly(other3, INT2NUM(CA_INT64));
1193
+ TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
1194
+ TypedData_Get_Struct(other2, CArray, &carray_data_type, ca2);
1195
+ TypedData_Get_Struct(other3, CArray, &carray_data_type, ca3);
1196
+ }
1197
+
1198
+ /* Element-count check: all non-scalar operands must agree. */
1199
+ {
1200
+ ca_size_t n = 1;
1201
+ if ( ! rb_obj_is_cscalar(self) ) n = ca1->elements;
1202
+ if ( ! rb_obj_is_cscalar(other2) ) {
1203
+ if ( n == 1 ) n = ca2->elements;
1204
+ else if ( ca2->elements != n ) {
1205
+ rb_raise(rb_eRuntimeError, "elements mismatch in triop (op2: %" PRId64 " != %" PRId64 ")",
1206
+ (ca_size_t) ca2->elements, n);
1207
+ }
237
1208
  }
238
- else { /* scalar vs array */
239
- if ( ca1->elements != ca2->elements ) {
240
- rb_raise(rb_eRuntimeError, "elements mismatch (%lld <-> %lld)",
241
- (ca_size_t) ca1->elements, (ca_size_t) ca2->elements);
1209
+ if ( ! rb_obj_is_cscalar(other3) ) {
1210
+ if ( n == 1 ) n = ca3->elements;
1211
+ else if ( ca3->elements != n ) {
1212
+ rb_raise(rb_eRuntimeError, "elements mismatch in triop (op3: %" PRId64 " != %" PRId64 ")",
1213
+ (ca_size_t) ca3->elements, n);
242
1214
  }
243
-
244
- ca_copy_mask_overlay(ca1, ca1->elements, 2, ca1, ca2);
245
- func[ca1->data_type](ca1->elements,
246
- ( ca1->mask ) ? (boolean8_t *) ca1->mask->ptr : NULL,
247
- ca1->ptr, 0,
248
- ca2->ptr, 0,
249
- ca1->ptr, 0);
250
1215
  }
251
1216
  }
252
- else {
253
- if ( rb_obj_is_cscalar(other) ) { /* array vs scalar */
254
- ca_copy_mask_overlay(ca1, ca1->elements, 2, ca1, ca2);
255
- func[ca1->data_type](ca1->elements,
256
- ( ca1->mask ) ? (boolean8_t *) ca1->mask->ptr : NULL,
257
- ca1->ptr, 1,
258
- ca2->ptr, 0,
259
- ca1->ptr, 1);
1217
+
1218
+ out = rb_ca_triop_select_template(self, other2, other3, ca1, ca2, ca3);
1219
+ TypedData_Get_Struct(out, CArray, &carray_data_type, cao);
1220
+
1221
+ ca_mask_overlay_safe(cao, 3, ca1, ca2, ca3);
1222
+
1223
+ {
1224
+ ca_size_t s1 = rb_obj_is_cscalar(self) ? 0 : 1;
1225
+ ca_size_t s2 = rb_obj_is_cscalar(other2) ? 0 : 1;
1226
+ ca_size_t s3 = rb_obj_is_cscalar(other3) ? 0 : 1;
1227
+
1228
+ /* Binop-style threshold dispatch: count non-alias array operands;
1229
+ >= 2 → CHUNKED (memory peak amortizes), else 1-shot ALLOCV. 3-way
1230
+ same-operand sharing is not done (a rare pattern like `fma(a, a, b)`;
1231
+ for now a non-alias `a` is gathered twice — wasteful but correct). */
1232
+ int nonalias_arrays = 0;
1233
+ int use_chunked;
1234
+ if ( s1 == 1 && !ca_attach_is_alias(ca1) ) nonalias_arrays++;
1235
+ if ( s2 == 1 && !ca_attach_is_alias(ca2) ) nonalias_arrays++;
1236
+ if ( s3 == 1 && !ca_attach_is_alias(ca3) ) nonalias_arrays++;
1237
+ use_chunked = (nonalias_arrays >= 2);
1238
+
1239
+ if ( !use_chunked ) {
1240
+ volatile VALUE h1 = Qnil, h2 = Qnil, h3 = Qnil;
1241
+ char *p1, *p2, *p3;
1242
+ int attached1, attached2, attached3;
1243
+ (void) h1; (void) h2; (void) h3;
1244
+
1245
+ EAGER_ACQUIRE_INPUT(ca1, p1, h1, attached1);
1246
+ EAGER_ACQUIRE_INPUT(ca2, p2, h2, attached2);
1247
+ EAGER_ACQUIRE_INPUT(ca3, p3, h3, attached3);
1248
+
1249
+ func[ca1->data_type](cao->elements,
1250
+ ( cao->mask ) ? (boolean8_t *) cao->mask->ptr : NULL,
1251
+ p1, s1,
1252
+ p2, s2,
1253
+ p3, s3,
1254
+ cao->ptr, 1);
1255
+
1256
+ EAGER_RELEASE_INPUT(ca3, h3, attached3);
1257
+ EAGER_RELEASE_INPUT(ca2, h2, attached2);
1258
+ EAGER_RELEASE_INPUT(ca1, h1, attached1);
260
1259
  }
261
- else { /* array vs array */
262
- if ( ca1->elements != ca2->elements ) {
263
- rb_raise(rb_eRuntimeError, "elements mismatch in binop (%lld <-> %lld)",
264
- (ca_size_t) ca1->elements, (ca_size_t) ca2->elements);
1260
+ else {
1261
+ /* CHUNKED PATH: per-operand decision matrix (the binop pattern
1262
+ extended to 3 inputs). scalar/alias same as before; non-alias
1263
+ array goes through per-chunk gather. */
1264
+ char *p1_src = NULL, *p2_src = NULL, *p3_src = NULL;
1265
+ void *s1_arena = NULL, *s2_arena = NULL, *s3_arena = NULL;
1266
+ int gpc1 = 0, gpc2 = 0, gpc3 = 0; /* gather-per-chunk flags */
1267
+ int att1 = 0, att2 = 0, att3 = 0;
1268
+ int8_t dt = ca1->data_type;
1269
+ ca_size_t chunk_n;
1270
+ ca_size_t off;
1271
+ ca_size_t n_total = cao->elements;
1272
+
1273
+ {
1274
+ ca_size_t inner = 1;
1275
+ ca_size_t maxb = ca1->bytes;
1276
+ if ( ca2->bytes > maxb ) maxb = ca2->bytes;
1277
+ if ( ca3->bytes > maxb ) maxb = ca3->bytes;
1278
+ if ( cao->bytes > maxb ) maxb = cao->bytes;
1279
+ if ( s1 == 1 ) inner = ca_chunk_inner_size(ca1);
1280
+ if ( s2 == 1 ) {
1281
+ ca_size_t inn = ca_chunk_inner_size(ca2);
1282
+ if ( inn > inner ) inner = inn;
1283
+ }
1284
+ if ( s3 == 1 ) {
1285
+ ca_size_t inn = ca_chunk_inner_size(ca3);
1286
+ if ( inn > inner ) inner = inn;
1287
+ }
1288
+ chunk_n = ca_chunk_compute_n(n_total, inner, maxb);
265
1289
  }
266
1290
 
267
- ca_copy_mask_overlay(ca1, ca1->elements, 2, ca1, ca2);
268
- func[ca1->data_type](ca1->elements,
1291
+ ca_lazy_arena_enter();
1292
+
1293
+ /* ca1 acquire */
1294
+ if ( s1 == 0 ) {
1295
+ if ( ca_attach_is_alias(ca1) ) {
1296
+ ca_attach(ca1); p1_src = (char *) ca1->ptr; att1 = 1;
1297
+ } else {
1298
+ s1_arena = ca_op_acquire_operand_scratch(ca1, 1);
1299
+ ca_xfer_all(ca1, s1_arena, CA_XFER_GET);
1300
+ p1_src = (char *) s1_arena;
1301
+ }
1302
+ } else if ( ca_attach_is_alias(ca1) ) {
1303
+ ca_attach(ca1); p1_src = (char *) ca1->ptr; att1 = 1;
1304
+ } else {
1305
+ s1_arena = ca_op_acquire_operand_scratch(ca1, chunk_n);
1306
+ p1_src = (char *) s1_arena; gpc1 = 1;
1307
+ }
1308
+ /* ca2 acquire (mirror) */
1309
+ if ( s2 == 0 ) {
1310
+ if ( ca_attach_is_alias(ca2) ) {
1311
+ ca_attach(ca2); p2_src = (char *) ca2->ptr; att2 = 1;
1312
+ } else {
1313
+ s2_arena = ca_op_acquire_operand_scratch(ca2, 1);
1314
+ ca_xfer_all(ca2, s2_arena, CA_XFER_GET);
1315
+ p2_src = (char *) s2_arena;
1316
+ }
1317
+ } else if ( ca_attach_is_alias(ca2) ) {
1318
+ ca_attach(ca2); p2_src = (char *) ca2->ptr; att2 = 1;
1319
+ } else {
1320
+ s2_arena = ca_op_acquire_operand_scratch(ca2, chunk_n);
1321
+ p2_src = (char *) s2_arena; gpc2 = 1;
1322
+ }
1323
+ /* ca3 acquire (mirror) */
1324
+ if ( s3 == 0 ) {
1325
+ if ( ca_attach_is_alias(ca3) ) {
1326
+ ca_attach(ca3); p3_src = (char *) ca3->ptr; att3 = 1;
1327
+ } else {
1328
+ s3_arena = ca_op_acquire_operand_scratch(ca3, 1);
1329
+ ca_xfer_all(ca3, s3_arena, CA_XFER_GET);
1330
+ p3_src = (char *) s3_arena;
1331
+ }
1332
+ } else if ( ca_attach_is_alias(ca3) ) {
1333
+ ca_attach(ca3); p3_src = (char *) ca3->ptr; att3 = 1;
1334
+ } else {
1335
+ s3_arena = ca_op_acquire_operand_scratch(ca3, chunk_n);
1336
+ p3_src = (char *) s3_arena; gpc3 = 1;
1337
+ }
1338
+
1339
+ for ( off = 0; off < n_total; off += chunk_n ) {
1340
+ ca_size_t n_done = (off + chunk_n > n_total) ? n_total - off
1341
+ : chunk_n;
1342
+ char *p1, *p2, *p3;
1343
+
1344
+ if ( gpc1 ) { ca_chunked_gather(ca1, off, n_done, s1_arena);
1345
+ p1 = (char *) s1_arena; }
1346
+ else { p1 = p1_src + (s1 ? off * ca1->bytes : 0); }
1347
+ if ( gpc2 ) { ca_chunked_gather(ca2, off, n_done, s2_arena);
1348
+ p2 = (char *) s2_arena; }
1349
+ else { p2 = p2_src + (s2 ? off * ca2->bytes : 0); }
1350
+ if ( gpc3 ) { ca_chunked_gather(ca3, off, n_done, s3_arena);
1351
+ p3 = (char *) s3_arena; }
1352
+ else { p3 = p3_src + (s3 ? off * ca3->bytes : 0); }
1353
+
1354
+ func[dt](n_done,
1355
+ cao->mask ? ((boolean8_t *) cao->mask->ptr) + off
1356
+ : NULL,
1357
+ p1, s1,
1358
+ p2, s2,
1359
+ p3, s3,
1360
+ (char *) cao->ptr + off * cao->bytes, 1);
1361
+ }
1362
+
1363
+ if ( s3_arena ) ca_lazy_arena_release(s3_arena);
1364
+ if ( s2_arena ) ca_lazy_arena_release(s2_arena);
1365
+ if ( s1_arena ) ca_lazy_arena_release(s1_arena);
1366
+ if ( att3 ) ca_detach(ca3);
1367
+ if ( att2 ) ca_detach(ca2);
1368
+ if ( att1 ) ca_detach(ca1);
1369
+
1370
+ ca_lazy_arena_exit();
1371
+ }
1372
+ }
1373
+
1374
+ return out;
1375
+ }
1376
+
1377
+ /* triop_bang (in-place) driver. ca1 = self = output (write target,
1378
+ attach legit; keep ca_attach + ca_sync); ca2/ca3 = input only (fast/slow
1379
+ dispatch via EAGER_ACQUIRE/RELEASE). */
1380
+ VALUE
1381
+ rb_ca_call_triop_bang (VALUE self, VALUE other2, VALUE other3,
1382
+ ca_triop_func_t func[])
1383
+ {
1384
+ CArray *ca1, *ca2, *ca3;
1385
+
1386
+ rb_ca_modify(self);
1387
+
1388
+ rb_ca_cast_other(&self, &other2);
1389
+ rb_ca_cast_other(&self, &other3);
1390
+
1391
+ TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
1392
+ TypedData_Get_Struct(other2, CArray, &carray_data_type, ca2);
1393
+ TypedData_Get_Struct(other3, CArray, &carray_data_type, ca3);
1394
+
1395
+ /* self is the write target, so the destination rule applies to each
1396
+ input operand in turn (the same one assignment uses). */
1397
+ if ( ! rb_obj_is_cscalar(other2) ) {
1398
+ ca_broadcast_to_destination(self, &other2);
1399
+ TypedData_Get_Struct(other2, CArray, &carray_data_type, ca2);
1400
+ }
1401
+ if ( ! rb_obj_is_cscalar(other3) ) {
1402
+ ca_broadcast_to_destination(self, &other3);
1403
+ TypedData_Get_Struct(other3, CArray, &carray_data_type, ca3);
1404
+ }
1405
+
1406
+ /* self IS the output (= write target; attach legit per refined invariant) */
1407
+ ca_attach(ca1);
1408
+ ca_mask_overlay_safe(ca1, 3, ca1, ca2, ca3);
1409
+
1410
+ {
1411
+ ca_size_t s2 = rb_obj_is_cscalar(other2) ? 0 : 1;
1412
+ ca_size_t s3 = rb_obj_is_cscalar(other3) ? 0 : 1;
1413
+
1414
+ /* Threshold dispatch on the input-only operands (ca2 / ca3). Self
1415
+ (ca1) IS the output (attached + ca_sync as usual). >= 2 non-alias
1416
+ input arrays → chunked, else 1-shot ALLOCV. */
1417
+ int nonalias_arrays = 0;
1418
+ int use_chunked;
1419
+ if ( s2 == 1 && !ca_attach_is_alias(ca2) ) nonalias_arrays++;
1420
+ if ( s3 == 1 && !ca_attach_is_alias(ca3) ) nonalias_arrays++;
1421
+ use_chunked = (nonalias_arrays >= 2);
1422
+
1423
+ if ( !use_chunked ) {
1424
+ volatile VALUE h2 = Qnil, h3 = Qnil;
1425
+ char *p2, *p3;
1426
+ int attached2, attached3;
1427
+ (void) h2; (void) h3;
1428
+
1429
+ EAGER_ACQUIRE_INPUT(ca2, p2, h2, attached2);
1430
+ EAGER_ACQUIRE_INPUT(ca3, p3, h3, attached3);
1431
+
1432
+ func[ca1->data_type](ca1->elements,
269
1433
  ( ca1->mask ) ? (boolean8_t *) ca1->mask->ptr : NULL,
270
1434
  ca1->ptr, 1,
271
- ca2->ptr, 1,
1435
+ p2, s2,
1436
+ p3, s3,
272
1437
  ca1->ptr, 1);
1438
+
1439
+ EAGER_RELEASE_INPUT(ca3, h3, attached3);
1440
+ EAGER_RELEASE_INPUT(ca2, h2, attached2);
273
1441
  }
1442
+ else {
1443
+ /* CHUNKED PATH: both ca2 and ca3 non-alias arrays. ca1 (= self =
1444
+ output) is already attached; its ptr is contig (= ca_attach
1445
+ materialise + alias for entity, or full materialise for view).
1446
+ Write to ca1->ptr + off*bytes in chunks; sync at end. */
1447
+ void *s2_arena = NULL, *s3_arena = NULL;
1448
+ int8_t dt = ca1->data_type;
1449
+ ca_size_t chunk_n;
1450
+ ca_size_t off;
1451
+ ca_size_t n_total = ca1->elements;
1452
+
1453
+ {
1454
+ ca_size_t inner = ca_chunk_inner_size(ca1);
1455
+ ca_size_t inn2 = ca_chunk_inner_size(ca2);
1456
+ ca_size_t inn3 = ca_chunk_inner_size(ca3);
1457
+ ca_size_t maxb = ca1->bytes;
1458
+ if ( ca2->bytes > maxb ) maxb = ca2->bytes;
1459
+ if ( ca3->bytes > maxb ) maxb = ca3->bytes;
1460
+ if ( inn2 > inner ) inner = inn2;
1461
+ if ( inn3 > inner ) inner = inn3;
1462
+ chunk_n = ca_chunk_compute_n(n_total, inner, maxb);
1463
+ }
274
1464
 
1465
+ ca_lazy_arena_enter();
1466
+ s2_arena = ca_op_acquire_operand_scratch(ca2, chunk_n);
1467
+ s3_arena = ca_op_acquire_operand_scratch(ca3, chunk_n);
1468
+
1469
+ for ( off = 0; off < n_total; off += chunk_n ) {
1470
+ ca_size_t n_done = (off + chunk_n > n_total) ? n_total - off
1471
+ : chunk_n;
1472
+ ca_chunked_gather(ca2, off, n_done, s2_arena);
1473
+ ca_chunked_gather(ca3, off, n_done, s3_arena);
1474
+
1475
+ func[dt](n_done,
1476
+ ca1->mask ? ((boolean8_t *) ca1->mask->ptr) + off
1477
+ : NULL,
1478
+ (char *) ca1->ptr + off * ca1->bytes, 1,
1479
+ (char *) s2_arena, s2,
1480
+ (char *) s3_arena, s3,
1481
+ (char *) ca1->ptr + off * ca1->bytes, 1);
1482
+ }
1483
+
1484
+ ca_lazy_arena_release(s3_arena);
1485
+ ca_lazy_arena_release(s2_arena);
1486
+ ca_lazy_arena_exit();
1487
+ }
275
1488
  }
276
1489
 
277
1490
  ca_sync(ca1);
278
- ca_detach_n(2, ca1, ca2);
1491
+ ca_detach(ca1);
279
1492
 
280
1493
  return self;
281
1494
  }
282
1495
 
1496
+ /* moncmp driver. ca1 input-only (EAGER_ACQUIRE/RELEASE fast/slow), ca2 =
1497
+ new boolean entity output (attach legit). */
283
1498
  VALUE
284
1499
  rb_ca_call_moncmp (VALUE self, ca_moncmp_func_t func[])
285
1500
  {
@@ -297,30 +1512,43 @@ rb_ca_call_moncmp (VALUE self, ca_moncmp_func_t func[])
297
1512
 
298
1513
  TypedData_Get_Struct(out, CArray, &carray_data_type, ca2);
299
1514
 
300
- ca_attach(ca1);
301
- ca_copy_mask_overlay(ca2, ca2->elements, 1, ca1);
302
- func[ca1->data_type](ca1->elements,
303
- ( ca2->mask ) ? (boolean8_t *) ca2->mask->ptr : NULL,
304
- ca1->ptr, 1,
305
- (boolean8_t *) ca2->ptr, 1);
306
- ca_detach(ca1);
1515
+ ca_mask_overlay_safe(ca2, 1, ca1);
307
1516
 
308
- /* unresolved unbound repeat array generates unbound repeat array again */
309
- if ( ca1->obj_type == CA_OBJ_UNBOUND_REPEAT ) {
310
- CAUnboundRepeat *cx = (CAUnboundRepeat *) ca1;
311
- out = rb_ca_ubrep_new(rb_ca_ubrep_shave(self, out), cx->rep_ndim, cx->rep_dim);
1517
+ /* The kernel's masked branch skips masked cells, so they would keep the
1518
+ uninitialised data from rb_carray_new. Zero the output when a mask is
1519
+ present so masked cells read as 0 (matching binop's ca_template_safe
1520
+ convention) instead of exposing uninitialised memory. */
1521
+ if ( ca2->mask ) {
1522
+ MEMZERO(ca2->ptr, char, ca2->elements * ca2->bytes);
1523
+ }
1524
+
1525
+ {
1526
+ volatile VALUE h1 = Qnil;
1527
+ char *p1;
1528
+ int attached1;
1529
+ (void) h1;
1530
+
1531
+ EAGER_ACQUIRE_INPUT(ca1, p1, h1, attached1);
1532
+ func[ca1->data_type](ca1->elements,
1533
+ ( ca2->mask ) ? (boolean8_t *) ca2->mask->ptr : NULL,
1534
+ p1, 1,
1535
+ (boolean8_t *) ca2->ptr, 1);
1536
+ EAGER_RELEASE_INPUT(ca1, h1, attached1);
312
1537
  }
313
1538
 
314
1539
  return out;
315
1540
  }
316
1541
 
317
1542
 
318
- extern ca_monop_func_t ca_bincmp_eq[CA_NTYPE];
319
- extern ca_monop_func_t ca_bincmp_ne[CA_NTYPE];
1543
+ /* ca_bincmp_eq / ca_bincmp_ne are declared (correctly typed) in
1544
+ ca_bincmp_dispatch.h, reached via the carray.h umbrella. The
1545
+ UNDEF-comparison identity checks below cast to ca_bincmp_func_t
1546
+ explicitly. */
320
1547
 
321
1548
  VALUE
322
1549
  rb_ca_call_bincmp (volatile VALUE self, volatile VALUE other,
323
- ca_bincmp_func_t func[])
1550
+ ca_bincmp_func_t func[],
1551
+ double tol)
324
1552
  {
325
1553
  volatile VALUE out = Qnil;
326
1554
  CArray *ca1, *ca2, *ca3; /* ca3 = ca1.op(ca2) */
@@ -338,74 +1566,145 @@ rb_ca_call_bincmp (volatile VALUE self, volatile VALUE other,
338
1566
  }
339
1567
  }
340
1568
 
1569
+ /* Face gate: an ORDERABLE Face over numeric storage descends to storage
1570
+ (fixing the surface-fixlen memcmp mis-order) and reconciles a Face RHS
1571
+ via to_comparable (e.g. unit alignment). No-op for non-Face self and
1572
+ for fixlen-storage Faces (memcmp is already correct there). */
1573
+ ca_face_reconcile_comparison(&self, &other);
1574
+
341
1575
  /* do implicit casting and resolving unbound repeat array */
342
1576
  rb_ca_cast_self_or_other(&self, &other);
343
1577
 
344
1578
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca1);
345
1579
  TypedData_Get_Struct(other, CArray, &carray_data_type, ca2);
346
1580
 
347
- ca_attach_n(2, ca1, ca2);
1581
+ /* Same fast/slow shape as rb_ca_call_binop, but the output data_type is
1582
+ fixed boolean (rb_ca_call_binop's output data_type matches its input). */
1583
+ {
1584
+ int self_is_scalar = RTEST(rb_obj_is_cscalar(self));
1585
+ int other_is_scalar = RTEST(rb_obj_is_cscalar(other));
1586
+ ca_size_t n_kernel, i1, i2, i3;
348
1587
 
349
- /* main operation */
350
- if ( rb_obj_is_cscalar(self) ) {
351
- if ( rb_obj_is_cscalar(other) ) { /* scalar vs scalar */
1588
+ if ( self_is_scalar && other_is_scalar ) {
352
1589
  out = rb_cscalar_new(CA_BOOLEAN, 0, NULL);
353
- TypedData_Get_Struct(out, CArray, &carray_data_type, ca3);
354
-
355
- ca_copy_mask_overlay(ca3, ca3->elements, 2, ca1, ca2);
356
- func[ca1->data_type](ca1->elements,
357
- ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
358
- ca1->ptr, ca1->bytes, 0,
359
- ca2->ptr, ca2->bytes, 0,
360
- ca3->ptr, ca3->bytes, 0);
1590
+ n_kernel = ca1->elements; i1 = 0; i2 = 0; i3 = 0;
361
1591
  }
362
- else { /* scalar vs array */
1592
+ else if ( self_is_scalar /* && !other_is_scalar */ ) {
363
1593
  out = rb_carray_new(CA_BOOLEAN, ca2->ndim, ca2->dim, 0, NULL);
364
- TypedData_Get_Struct(out, CArray, &carray_data_type, ca3);
365
-
366
- ca_copy_mask_overlay(ca3, ca3->elements, 2, ca1, ca2);
367
- func[ca1->data_type](ca2->elements,
368
- ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
369
- ca1->ptr, ca1->bytes, 0,
370
- ca2->ptr, ca2->bytes, 1,
371
- ca3->ptr, ca3->bytes, 1);
1594
+ n_kernel = ca2->elements; i1 = 0; i2 = 1; i3 = 1;
372
1595
  }
373
- }
374
- else {
375
- if ( rb_obj_is_cscalar(other) ) { /* array vs scalar */
1596
+ else if ( other_is_scalar /* && !self_is_scalar */ ) {
376
1597
  out = rb_carray_new(CA_BOOLEAN, ca1->ndim, ca1->dim, 0, NULL);
377
- TypedData_Get_Struct(out, CArray, &carray_data_type, ca3);
378
-
379
- ca_copy_mask_overlay(ca3, ca3->elements, 2, ca1, ca2);
380
- func[ca1->data_type](ca1->elements,
381
- ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
382
- ca1->ptr, ca1->bytes, 1,
383
- ca2->ptr, ca2->bytes, 0,
384
- ca3->ptr, ca3->bytes, 1);
1598
+ n_kernel = ca1->elements; i1 = 1; i2 = 0; i3 = 1;
385
1599
  }
386
- else { /* array vs array */
1600
+ else {
387
1601
  if ( ca1->elements != ca2->elements ) {
388
- rb_raise(rb_eRuntimeError, "elements mismatch in bincmp (%lld <-> %lld)",
389
- (ca_size_t) ca1->elements, (ca_size_t) ca2->elements);
1602
+ rb_raise(rb_eRuntimeError, "elements mismatch in bincmp (%" PRId64 " <-> %" PRId64 ")",
1603
+ (ca_size_t) ca1->elements,
1604
+ (ca_size_t) ca2->elements);
390
1605
  }
391
1606
  out = rb_carray_new(CA_BOOLEAN, ca1->ndim, ca1->dim, 0, NULL);
392
- TypedData_Get_Struct(out, CArray, &carray_data_type, ca3);
393
-
394
- ca_copy_mask_overlay(ca3, ca3->elements, 2, ca1, ca2);
395
- func[ca1->data_type](ca1->elements,
396
- ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
397
- ca1->ptr, ca1->bytes, 1,
398
- ca2->ptr, ca2->bytes, 1,
399
- ca3->ptr, ca3->bytes, 1);
1607
+ n_kernel = ca1->elements; i1 = 1; i2 = 1; i3 = 1;
400
1608
  }
401
- }
1609
+ TypedData_Get_Struct(out, CArray, &carray_data_type, ca3);
1610
+
1611
+ ca_mask_overlay_safe(ca3, 2, ca1, ca2);
402
1612
 
403
- ca_detach_n(2, ca1, ca2);
1613
+ /* The kernel's masked branch skips masked cells, so they would keep the
1614
+ uninitialised data from rb_carray_new. Zero the output when a mask is
1615
+ present so masked cells read as 0 (matching binop's ca_template_safe
1616
+ convention) instead of exposing uninitialised memory. */
1617
+ if ( ca3->mask ) {
1618
+ MEMZERO(ca3->ptr, char, ca3->elements * ca3->bytes);
1619
+ }
404
1620
 
405
- /* unresolved unbound repeat array generates unbound repeat array again */
406
- if ( ca1->obj_type == CA_OBJ_UNBOUND_REPEAT ) {
407
- CAUnboundRepeat *cx = (CAUnboundRepeat *) ca1;
408
- out = rb_ca_ubrep_new(rb_ca_ubrep_shave(self, out), cx->rep_ndim, cx->rep_dim);
1621
+ /* SAME-OPERAND SHARING: prevents materialising the same view twice in
1622
+ cases like `view < view`. */
1623
+ if ( ca1 == ca2 && !ca_attach_is_alias(ca1) ) {
1624
+ volatile VALUE h_shared = Qnil;
1625
+ char *p_shared;
1626
+ (void) h_shared;
1627
+ p_shared = ALLOCV_N(char, h_shared, ca1->elements * ca1->bytes);
1628
+ ca_xfer_all(ca1, p_shared, CA_XFER_GET);
1629
+ func[ca1->data_type](n_kernel,
1630
+ ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
1631
+ p_shared, ca1->bytes, i1,
1632
+ p_shared, ca2->bytes, i2,
1633
+ ca3->ptr, ca3->bytes, i3,
1634
+ tol);
1635
+ ALLOCV_END(h_shared);
1636
+ }
1637
+ else {
1638
+ /* Binop-style threshold dispatch + chunked path for bincmp: only
1639
+ when both operands need per-region gather (both non-alias array)
1640
+ do we use the chunked path; else 1-shot ALLOCV for the single
1641
+ non-alias operand. */
1642
+ int nonalias_arrays = 0;
1643
+ int use_chunked;
1644
+ if ( i1 == 1 && !ca_attach_is_alias(ca1) ) nonalias_arrays++;
1645
+ if ( i2 == 1 && !ca_attach_is_alias(ca2) ) nonalias_arrays++;
1646
+ use_chunked = (nonalias_arrays >= 2);
1647
+
1648
+ if ( !use_chunked ) {
1649
+ volatile VALUE h1 = Qnil, h2 = Qnil;
1650
+ char *p1, *p2;
1651
+ int attached1, attached2;
1652
+ (void) h1; (void) h2;
1653
+
1654
+ EAGER_ACQUIRE_INPUT(ca1, p1, h1, attached1);
1655
+ EAGER_ACQUIRE_INPUT(ca2, p2, h2, attached2);
1656
+
1657
+ func[ca1->data_type](n_kernel,
1658
+ ( ca3->mask ) ? (boolean8_t *) ca3->mask->ptr : NULL,
1659
+ p1, ca1->bytes, i1,
1660
+ p2, ca2->bytes, i2,
1661
+ ca3->ptr, ca3->bytes, i3,
1662
+ tol);
1663
+
1664
+ EAGER_RELEASE_INPUT(ca2, h2, attached2);
1665
+ EAGER_RELEASE_INPUT(ca1, h1, attached1);
1666
+ }
1667
+ else {
1668
+ /* CHUNKED PATH (both operands non-alias array): per-chunk gather
1669
+ into arena scratch. Mirrors the binop chunked branch. */
1670
+ void *s1_arena = NULL, *s2_arena = NULL;
1671
+ ca_size_t b1 = ca1->bytes, b2 = ca2->bytes, b3 = ca3->bytes;
1672
+ int8_t dt = ca1->data_type;
1673
+ ca_size_t chunk_n, off;
1674
+
1675
+ {
1676
+ ca_size_t inner1 = ca_chunk_inner_size(ca1);
1677
+ ca_size_t inner2 = ca_chunk_inner_size(ca2);
1678
+ ca_size_t inner = inner1 > inner2 ? inner1 : inner2;
1679
+ ca_size_t maxb = b1 > b2 ? b1 : b2;
1680
+ if ( b3 > maxb ) maxb = b3;
1681
+ chunk_n = ca_chunk_compute_n(n_kernel, inner, maxb);
1682
+ }
1683
+
1684
+ ca_lazy_arena_enter();
1685
+ s1_arena = ca_op_acquire_operand_scratch(ca1, chunk_n);
1686
+ s2_arena = ca_op_acquire_operand_scratch(ca2, chunk_n);
1687
+
1688
+ for ( off = 0; off < n_kernel; off += chunk_n ) {
1689
+ ca_size_t n_done = (off + chunk_n > n_kernel) ? n_kernel - off
1690
+ : chunk_n;
1691
+ ca_chunked_gather(ca1, off, n_done, s1_arena);
1692
+ ca_chunked_gather(ca2, off, n_done, s2_arena);
1693
+
1694
+ func[dt](n_done,
1695
+ ca3->mask ? ((boolean8_t *) ca3->mask->ptr) + off
1696
+ : NULL,
1697
+ (char *) s1_arena, b1, i1,
1698
+ (char *) s2_arena, b2, i2,
1699
+ (char *) ca3->ptr + off * b3, b3, i3,
1700
+ tol);
1701
+ }
1702
+
1703
+ ca_lazy_arena_release(s2_arena);
1704
+ ca_lazy_arena_release(s1_arena);
1705
+ ca_lazy_arena_exit();
1706
+ }
1707
+ }
409
1708
  }
410
1709
 
411
1710
  return out;
@@ -421,15 +1720,26 @@ ca_monop_not_implement(ca_size_t n, boolean8_t *m,
421
1720
  }
422
1721
 
423
1722
  void
424
- ca_binop_not_implement(ca_size_t n, boolean8_t *m,
425
- char *ptr1, ca_size_t i1,
426
- char *ptr2, ca_size_t i2,
1723
+ ca_binop_not_implement(ca_size_t n, boolean8_t *m,
1724
+ char *ptr1, ca_size_t i1,
1725
+ char *ptr2, ca_size_t i2,
427
1726
  char *ptr3, ca_size_t i3)
428
1727
  {
429
1728
  rb_raise(rb_eCADataTypeError,
430
1729
  "invalid data_type for binop (not implemented)");
431
1730
  }
432
1731
 
1732
+ void
1733
+ ca_triop_not_implement(ca_size_t n, boolean8_t *m,
1734
+ char *ptr1, ca_size_t i1,
1735
+ char *ptr2, ca_size_t i2,
1736
+ char *ptr3, ca_size_t i3,
1737
+ char *ptr4, ca_size_t i4)
1738
+ {
1739
+ rb_raise(rb_eCADataTypeError,
1740
+ "invalid data_type for triop (not implemented)");
1741
+ }
1742
+
433
1743
  void
434
1744
  ca_moncmp_not_implement(ca_size_t n, boolean8_t *m,
435
1745
  char *ptr1, ca_size_t i1,
@@ -440,10 +1750,11 @@ ca_moncmp_not_implement(ca_size_t n, boolean8_t *m,
440
1750
  }
441
1751
 
442
1752
  void
443
- ca_bincmp_not_implement (ca_size_t n, boolean8_t *m,
444
- char *ptr1, ca_size_t b1, ca_size_t i1,
445
- char *ptr2, ca_size_t b2, ca_size_t i2,
446
- char *ptr3, ca_size_t b3, ca_size_t i3)
1753
+ ca_bincmp_not_implement (ca_size_t n, boolean8_t *m,
1754
+ char *ptr1, ca_size_t b1, ca_size_t i1,
1755
+ char *ptr2, ca_size_t b2, ca_size_t i2,
1756
+ char *ptr3, ca_size_t b3, ca_size_t i3,
1757
+ double tol)
447
1758
  {
448
1759
  rb_raise(rb_eTypeError, "invalid data_type for bincmp (not implemented)");
449
1760
  }
@@ -455,7 +1766,7 @@ ca_math_call (VALUE mod, VALUE arg, ID id)
455
1766
  return rb_funcall(arg, id, 0);
456
1767
  }
457
1768
  #ifdef HAVE_COMPLEX_H
458
- else if ( rb_obj_is_kind_of(arg, rb_cCComplex) ) {
1769
+ else if ( RB_TYPE_P(arg, T_COMPLEX) ) {
459
1770
  if ( rb_respond_to(arg, id) ) {
460
1771
  return rb_funcall(arg, id, 0);
461
1772
  }
@@ -478,7 +1789,7 @@ ca_math_call (VALUE mod, VALUE arg, ID id)
478
1789
  }
479
1790
  }
480
1791
 
481
- /* @overload coerece (other)
1792
+ /* @overload coerce (other)
482
1793
 
483
1794
  [TBD]
484
1795
  */
@@ -486,15 +1797,9 @@ ca_math_call (VALUE mod, VALUE arg, ID id)
486
1797
  static VALUE
487
1798
  rb_ca_coerce (VALUE self, VALUE other)
488
1799
  {
489
- CArray *ca;
490
- TypedData_Get_Struct(self, CArray, &carray_data_type, ca);
491
-
492
1800
  if ( rb_obj_is_carray(other) ) {
493
1801
  return Qnil;
494
1802
  }
495
- else if ( rb_respond_to(other, rb_intern("ca")) ) {
496
- return rb_ca_coerce(self, rb_funcall(other,rb_intern("ca"),0));
497
- }
498
1803
  else if ( rb_respond_to(other, rb_intern("to_ca")) ) {
499
1804
  return rb_ca_coerce(self, rb_funcall(other,rb_intern("to_ca"),0));
500
1805
  }
@@ -506,123 +1811,23 @@ rb_ca_coerce (VALUE self, VALUE other)
506
1811
  }
507
1812
 
508
1813
 
509
- /* CArray#mul_add(other, min_count, fill) */
510
-
511
- #define proc_mul_add(type, conv, to) \
512
- { \
513
- type *p1 = (type*)ca->ptr; \
514
- type *p2; \
515
- ca_size_t s2; \
516
- boolean8_t *m = mi; \
517
- type sum = 0; \
518
- ca_size_t count = 0; \
519
- ca_size_t i; \
520
- ca_set_iterator(1, cw, &p2, &s2); \
521
- if ( m ) { \
522
- count = 0; \
523
- for (i=ca->elements; i; i--, p1++, p2+=s2) { \
524
- if ( ! *m++ ) { \
525
- sum += (type)conv(*p1) * (type)conv(*p2); \
526
- } \
527
- else { \
528
- count++; \
529
- } \
530
- } \
531
- } \
532
- else { \
533
- for (i=ca->elements; i; i--, p1++, p2+=s2) { \
534
- sum += (type)conv(*p1) * (type)conv(*p2); \
535
- } \
536
- } \
537
- if ( ( ! NIL_P(rmin_count) ) && count > min_count ) { \
538
- out = ( NIL_P(rfval) ) ? CA_UNDEF : rfval;\
539
- } \
540
- else { \
541
- out = to(sum); \
542
- } \
543
- }
544
-
545
- /* @overload mul_add (weight, min_count=nil, fill_value=nil)
546
-
547
- [TBD]
1814
+ /* CArray#mul_add was retired in 3.0 — superseded by `wsum` (mkkernel
1815
+ array_arg reduction, ext/mkkernel.rb). `wsum` is the strict superset:
1816
+ - f64 accumulator (overflow-safe for integer input)
1817
+ - per-axis (`a.wsum(w, axis)`)
1818
+ - kernel_iterator universal dispatch (= mask + lazy operand)
1819
+ - 3.0-unified min_count semantic ("min valid required").
1820
+ Migration: a.mul_add(b) -> a.wsum(b)
1821
+ a.mul_add(b, mc, fill) -> a.wsum(b, min_count: mc,
1822
+ fill_value: fill)
548
1823
  */
549
1824
 
550
- static VALUE
551
- rb_ca_mul_add (int argc, VALUE *argv, volatile VALUE self)
552
- {
553
- volatile VALUE out;
554
- volatile VALUE weight = Qnil;
555
- volatile VALUE rmin_count = Qnil;
556
- volatile VALUE rfval = Qnil;
557
- CArray *ca, *cw;
558
- boolean8_t *mi = NULL;
559
- ca_size_t min_count;
560
-
561
- /* FIXME: to parse :mask_limit, :fill_value */
562
- rb_scan_args(argc, argv, "12", (VALUE *) &weight, (VALUE *) &rmin_count, (VALUE *) &rfval);
563
-
564
- /* do implicit casting and resolving unbound repeat array */
565
- rb_ca_cast_self_or_other(&self, &weight);
566
-
567
- TypedData_Get_Struct(self, CArray, &carray_data_type, ca);
568
- TypedData_Get_Struct(weight, CArray, &carray_data_type, cw);
569
-
570
- /* checking elements and data_type */
571
- ca_check_same_elements(ca, cw);
572
- ca_check_same_data_type(ca, cw);
573
-
574
- if ( ca->elements == 0 ) {
575
- return ( NIL_P(rfval) ) ? CA_UNDEF : rfval;
576
- }
577
-
578
- if ( ca_has_mask(ca) || ca_has_mask(cw) ) {
579
- mi = ca_allocate_mask_iterator(2, ca, cw);
580
- }
581
-
582
- min_count = ( NIL_P(rmin_count) || ( ! mi ) ) ?
583
- ca->elements - 1 : NUM2SIZE(rmin_count);
584
-
585
- if ( min_count < 0 ) {
586
- min_count += ca->elements;
587
- }
588
-
589
- ca_attach_n(2, ca, cw);
590
-
591
- switch ( ca->data_type ) {
592
- case CA_INT8: proc_mul_add(int8_t, ,LONG2NUM); break;
593
- case CA_UINT8: proc_mul_add(uint8_t,,ULONG2NUM); break;
594
- case CA_INT16: proc_mul_add(int16_t,,LONG2NUM); break;
595
- case CA_UINT16: proc_mul_add(uint16_t,,ULONG2NUM); break;
596
- case CA_INT32: proc_mul_add(int32_t,,LONG2NUM); break;
597
- case CA_UINT32: proc_mul_add(uint32_t,,ULONG2NUM); break;
598
- case CA_INT64: proc_mul_add(int64_t,,LL2NUM); break;
599
- case CA_UINT64: proc_mul_add(uint64_t,,ULL2NUM); break;
600
- case CA_FLOAT32: proc_mul_add(float32_t,,rb_float_new); break;
601
- case CA_FLOAT64: proc_mul_add(float64_t,,rb_float_new); break;
602
- case CA_FLOAT128: proc_mul_add(float128_t,,rb_float_new); break;
603
- #ifdef HAVE_COMPLEX_H
604
- case CA_CMPLX64: proc_mul_add(cmplx64_t,,rb_ccomplex_new); break;
605
- case CA_CMPLX128: proc_mul_add(cmplx128_t,,rb_ccomplex_new); break;
606
- case CA_CMPLX256: proc_mul_add(cmplx256_t,,rb_ccomplex_new); break;
607
- #endif
608
- /* case CA_OBJECT: proc_mul_add(VALUE,NUM2DBL,rb_float_new); break; */
609
- default: rb_raise(rb_eCADataTypeError, "invalid data type");
610
- }
611
-
612
- ca_detach_n(2, ca, cw);
613
-
614
- free(mi);
615
-
616
- return out;
617
- }
618
-
619
1825
  void
620
- Init_carray_operator ()
1826
+ Init_carray_operator (void)
621
1827
  {
622
1828
  rb_mCAMath = rb_define_module("CAMath");
623
1829
 
624
1830
  rb_define_method(rb_cCArray, "coerce", rb_ca_coerce, 1);
625
- rb_define_method(rb_cCArray, "mul_add", rb_ca_mul_add, -1);
626
1831
  }
627
1832
 
628
1833