carray 2.0.0 → 3.0.0

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