carray 3.0.2 → 3.0.4

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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +568 -0
  3. data/README.md +6 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +220 -0
  7. data/ext/ca_axis_group.c +201 -123
  8. data/ext/ca_binop_dispatch.c +46 -52
  9. data/ext/ca_binop_dispatch.h +1 -1
  10. data/ext/ca_categorical_iterator.c +249 -183
  11. data/ext/ca_compare.c +7 -12
  12. data/ext/ca_for_buffer.h +21 -68
  13. data/ext/ca_group_iter.c +38 -23
  14. data/ext/ca_kernel_iterator.c +1765 -2076
  15. data/ext/ca_kernel_iterator.h +584 -566
  16. data/ext/ca_monop_dispatch.c +86 -63
  17. data/ext/ca_monop_dispatch.h +5 -0
  18. data/ext/ca_obj_array.c +223 -56
  19. data/ext/ca_obj_bincmp.c +172 -138
  20. data/ext/ca_obj_binop.c +116 -230
  21. data/ext/ca_obj_bitarray.c +40 -26
  22. data/ext/ca_obj_bitfield.c +100 -68
  23. data/ext/ca_obj_block.c +19 -9
  24. data/ext/ca_obj_byte_swap.c +45 -46
  25. data/ext/ca_obj_const_string.c +269 -44
  26. data/ext/ca_obj_face.c +612 -89
  27. data/ext/ca_obj_face.h +111 -47
  28. data/ext/ca_obj_fake.c +36 -79
  29. data/ext/ca_obj_farray.c +7 -3
  30. data/ext/ca_obj_field.c +33 -8
  31. data/ext/ca_obj_fixlen_string.c +23 -24
  32. data/ext/ca_obj_grid.c +541 -210
  33. data/ext/ca_obj_meld.c +331 -111
  34. data/ext/ca_obj_moncmp.c +80 -65
  35. data/ext/ca_obj_monop.c +177 -149
  36. data/ext/ca_obj_object.c +664 -248
  37. data/ext/ca_obj_record.c +63 -23
  38. data/ext/ca_obj_reduce.c +18 -16
  39. data/ext/ca_obj_refer.c +140 -58
  40. data/ext/ca_obj_remap.c +85 -24
  41. data/ext/ca_obj_repeat.c +24 -9
  42. data/ext/ca_obj_roll.c +121 -16
  43. data/ext/ca_obj_select.c +61 -58
  44. data/ext/ca_obj_select_axis.c +73 -107
  45. data/ext/ca_obj_shift.c +32 -102
  46. data/ext/ca_obj_stack.c +372 -122
  47. data/ext/ca_obj_stride.c +348 -140
  48. data/ext/ca_obj_string.c +88 -22
  49. data/ext/ca_obj_tile.c +38 -21
  50. data/ext/ca_obj_time.c +45 -31
  51. data/ext/ca_obj_timedelta.c +43 -29
  52. data/ext/ca_obj_transpose.c +17 -6
  53. data/ext/ca_obj_triop.c +125 -141
  54. data/ext/ca_obj_window.c +330 -44
  55. data/ext/ca_op_ipower.c +24 -11
  56. data/ext/ca_op_powi.h +57 -30
  57. data/ext/ca_pow5_table.h +656 -0
  58. data/ext/ca_sort_kernels.h +70 -0
  59. data/ext/ca_sweep_engine.c +104 -81
  60. data/ext/ca_sweep_engine.h +7 -20
  61. data/ext/ca_triop_dispatch.c +4 -1
  62. data/ext/ca_triop_dispatch.h +5 -2
  63. data/ext/caframe_csv_reader.c +542 -0
  64. data/ext/carray.h +241 -92
  65. data/ext/carray_access.c +608 -276
  66. data/ext/carray_address_basis.c +494 -97
  67. data/ext/carray_attribute.c +129 -4
  68. data/ext/carray_bincount.c +39 -7
  69. data/ext/carray_broadcast.c +56 -19
  70. data/ext/carray_build_flags.h +1 -1
  71. data/ext/carray_call_cfunc.c +108 -28
  72. data/ext/carray_cast.c +1576 -223
  73. data/ext/carray_cast_func.rb +77 -24
  74. data/ext/carray_class.c +4 -3
  75. data/ext/carray_conversion.c +208 -75
  76. data/ext/carray_copy.c +82 -24
  77. data/ext/carray_core.c +646 -162
  78. data/ext/carray_count.c +171 -88
  79. data/ext/carray_element.c +94 -60
  80. data/ext/carray_factorize.c +670 -324
  81. data/ext/carray_generate.c +260 -83
  82. data/ext/carray_histogram.c +209 -106
  83. data/ext/carray_hold.c +14 -16
  84. data/ext/carray_index_classifier.c +422 -243
  85. data/ext/carray_internal.h +692 -3
  86. data/ext/carray_kernels_bincmp.c +7 -0
  87. data/ext/carray_kernels_binop.c +487 -120
  88. data/ext/carray_kernels_init.c +653 -50
  89. data/ext/carray_kernels_moncmp.c +15 -8
  90. data/ext/carray_kernels_monop.c +99 -43
  91. data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
  92. data/ext/carray_kernels_reduce_boolean.c +112 -36
  93. data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
  94. data/ext/carray_kernels_reduce_extreme.c +3032 -2892
  95. data/ext/carray_kernels_reduce_variance.c +439 -500
  96. data/ext/carray_kernels_scan.c +1097 -488
  97. data/ext/carray_kernels_search.c +3787 -2176
  98. data/ext/carray_kernels_sort.c +1200 -920
  99. data/ext/carray_kernels_triop.c +479 -7
  100. data/ext/carray_key_windows.c +216 -0
  101. data/ext/carray_lazy.c +967 -153
  102. data/ext/carray_loop.c +81 -65
  103. data/ext/carray_mask.c +910 -324
  104. data/ext/carray_math_kernel.h +1 -0
  105. data/ext/carray_median_percentile.c +299 -100
  106. data/ext/carray_memory_view.c +872 -758
  107. data/ext/carray_operator.c +1171 -364
  108. data/ext/carray_order.c +505 -133
  109. data/ext/carray_partition.c +112 -65
  110. data/ext/carray_random.c +327 -239
  111. data/ext/carray_round.c +273 -0
  112. data/ext/carray_scatter.c +346 -78
  113. data/ext/carray_segment.c +257 -0
  114. data/ext/carray_slab.c +249 -86
  115. data/ext/carray_sort.c +224 -227
  116. data/ext/carray_sort_kernel.c +8 -1
  117. data/ext/carray_struct.c +118 -39
  118. data/ext/carray_test.c +193 -69
  119. data/ext/carray_time_text.c +1182 -0
  120. data/ext/carray_undef.c +4 -0
  121. data/ext/carray_utils.c +579 -113
  122. data/ext/extconf.rb +27 -2
  123. data/ext/mk_call_cfunc.rb +19 -4
  124. data/ext/mk_pow5_table.rb +35 -0
  125. data/ext/mkkernel.rb +1202 -1014
  126. data/ext/ruby_carray.c +17 -5
  127. data/ext/version.h +4 -4
  128. data/ext/version.rb +4 -2
  129. data/lib/carray/attribute.rb +160 -17
  130. data/lib/carray/autoload_carray.rb +23 -27
  131. data/lib/carray/autoload_method_extension.rb +20 -5
  132. data/lib/carray/axis_group.rb +360 -63
  133. data/lib/carray/basics.rb +162 -28
  134. data/lib/carray/bincount_nd.rb +18 -2
  135. data/lib/carray/block_iterator.rb +231 -58
  136. data/lib/carray/boolean_reduce.rb +34 -42
  137. data/lib/carray/categorical.rb +242 -52
  138. data/lib/carray/categorical_iterator.rb +437 -827
  139. data/lib/carray/complex.rb +41 -34
  140. data/lib/carray/conditional.rb +150 -14
  141. data/lib/carray/const_string.rb +59 -4
  142. data/lib/carray/construct.rb +15 -11
  143. data/lib/carray/data_type_extension.rb +1 -0
  144. data/lib/carray/distance_limit.rb +77 -0
  145. data/lib/carray/fixlen_string.rb +7 -1
  146. data/lib/carray/frame/column_verbs.rb +70 -0
  147. data/lib/carray/frame/concat.rb +157 -6
  148. data/lib/carray/frame/convert.rb +82 -7
  149. data/lib/carray/frame/csv_parser.rb +688 -87
  150. data/lib/carray/frame/describe.rb +80 -0
  151. data/lib/carray/frame/frame.rb +369 -45
  152. data/lib/carray/frame/group.rb +631 -90
  153. data/lib/carray/frame/io.rb +506 -79
  154. data/lib/carray/frame/join.rb +411 -62
  155. data/lib/carray/frame/records.rb +116 -19
  156. data/lib/carray/frame/reshape.rb +455 -0
  157. data/lib/carray/frame/sort.rb +30 -18
  158. data/lib/carray/frame/verbs.rb +868 -61
  159. data/lib/carray/frame/windows.rb +117 -0
  160. data/lib/carray/frame.rb +4 -0
  161. data/lib/carray/fuse_source.rb +118 -2
  162. data/lib/carray/fusion.rb +257 -42
  163. data/lib/carray/histogram.rb +6 -1
  164. data/lib/carray/inspect.rb +54 -13
  165. data/lib/carray/iterator.rb +99 -9
  166. data/lib/carray/key_windows.rb +620 -0
  167. data/lib/carray/lazy.rb +216 -193
  168. data/lib/carray/mask_gap_fill.rb +112 -92
  169. data/lib/carray/math.rb +32 -19
  170. data/lib/carray/meld_reduce.rb +52 -21
  171. data/lib/carray/methods/align_addr.rb +8 -4
  172. data/lib/carray/methods/bin.rb +26 -18
  173. data/lib/carray/methods/bincount.rb +18 -2
  174. data/lib/carray/methods/bit_string.rb +15 -4
  175. data/lib/carray/methods/choose.rb +16 -3
  176. data/lib/carray/methods/composition.rb +18 -10
  177. data/lib/carray/methods/factorize.rb +2 -2
  178. data/lib/carray/methods/gather_nd.rb +25 -5
  179. data/lib/carray/methods/insert_block.rb +9 -14
  180. data/lib/carray/methods/is_between.rb +59 -0
  181. data/lib/carray/methods/is_in.rb +140 -10
  182. data/lib/carray/methods/join.rb +2 -7
  183. data/lib/carray/methods/lag.rb +49 -0
  184. data/lib/carray/methods/locate_addr.rb +182 -45
  185. data/lib/carray/methods/mode.rb +18 -1
  186. data/lib/carray/methods/nunique.rb +11 -6
  187. data/lib/carray/methods/repeat.rb +1 -20
  188. data/lib/carray/methods/resize.rb +21 -23
  189. data/lib/carray/methods/snap.rb +34 -12
  190. data/lib/carray/methods/unique.rb +11 -2
  191. data/lib/carray/methods/value_counts.rb +4 -11
  192. data/lib/carray/record_order.rb +208 -0
  193. data/lib/carray/rng.rb +3 -0
  194. data/lib/carray/runtime.rb +65 -17
  195. data/lib/carray/segment_iterator.rb +1221 -0
  196. data/lib/carray/serialize.rb +214 -30
  197. data/lib/carray/slab_iterator.rb +101 -27
  198. data/lib/carray/stack.rb +10 -4
  199. data/lib/carray/string.rb +105 -5
  200. data/lib/carray/string_operation_extension.rb +12 -4
  201. data/lib/carray/struct.rb +137 -58
  202. data/lib/carray/struct_builder.rb +124 -17
  203. data/lib/carray/time.rb +1222 -186
  204. data/lib/carray/window_iterator.rb +242 -81
  205. data/lib/carray.rb +9 -2
  206. data/yard-stubs/ca_obj_array.rb +4 -4
  207. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  208. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  209. data/yard-stubs/ca_obj_fake.rb +1 -1
  210. data/yard-stubs/ca_obj_field.rb +5 -1
  211. data/yard-stubs/ca_obj_grid.rb +21 -10
  212. data/yard-stubs/ca_obj_refer.rb +5 -4
  213. data/yard-stubs/ca_obj_roll.rb +2 -3
  214. data/yard-stubs/ca_obj_shift.rb +5 -1
  215. data/yard-stubs/ca_obj_stride.rb +10 -7
  216. data/yard-stubs/ca_obj_tile.rb +1 -1
  217. data/yard-stubs/ca_obj_transpose.rb +1 -1
  218. data/yard-stubs/ca_obj_window.rb +9 -5
  219. data/yard-stubs/carray_access.rb +11 -5
  220. data/yard-stubs/carray_attribute.rb +0 -5
  221. data/yard-stubs/carray_broadcast.rb +1 -1
  222. data/yard-stubs/carray_cast.rb +88 -43
  223. data/yard-stubs/carray_conversion.rb +15 -2
  224. data/yard-stubs/carray_copy.rb +12 -5
  225. data/yard-stubs/carray_core.rb +3 -3
  226. data/yard-stubs/carray_count.rb +9 -0
  227. data/yard-stubs/carray_element.rb +10 -4
  228. data/yard-stubs/carray_generate.rb +21 -1
  229. data/yard-stubs/carray_lazy.rb +5 -1
  230. data/yard-stubs/carray_loop.rb +4 -1
  231. data/yard-stubs/carray_mask.rb +60 -12
  232. data/yard-stubs/carray_math.rb +52 -14
  233. data/yard-stubs/carray_median_percentile.rb +15 -8
  234. data/yard-stubs/carray_memory_view.rb +40 -23
  235. data/yard-stubs/carray_order.rb +61 -29
  236. data/yard-stubs/carray_random.rb +13 -4
  237. data/yard-stubs/carray_scatter.rb +21 -9
  238. data/yard-stubs/carray_segment.rb +72 -0
  239. data/yard-stubs/carray_slab.rb +10 -2
  240. data/yard-stubs/carray_sort.rb +49 -8
  241. data/yard-stubs/carray_test.rb +15 -3
  242. data/yard-stubs/carray_utils.rb +0 -26
  243. metadata +19 -5
  244. data/ext/ca_for_each_element.h +0 -239
  245. data/ext/carray_kernels_map.c +0 -3467
  246. data/lib/carray/arrow_tensor.rb +0 -401
  247. data/lib/carray/table.rb +0 -108
data/ext/mkkernel.rb CHANGED
@@ -177,10 +177,8 @@ module MkKernel
177
177
  # expr: { object: "..." }.merge(float_widths("(#2) = sin<f>(#1);"))
178
178
  #
179
179
  # Not every double-taking call wants this. `fabs`, `fmin` / `fmax`,
180
- # `ceil` / `floor` / `trunc` and the `isnan` family are exact on a
181
- # float either way, and `round` (`floor(x + 0.5)`) is exact only in
182
- # double -- narrowing it would round `x + 0.5f` first and step the
183
- # answer at the boundary.
180
+ # `ceil` / `floor` / `trunc` / `round` and the `isnan` family are exact
181
+ # on a float either way.
184
182
  def self.float_widths(body)
185
183
  {
186
184
  [:f32] => at_width(body, "f", "float"),
@@ -406,8 +404,8 @@ module MkKernel
406
404
  # only, output (an axis-local index) needs no re-lift.
407
405
  # :relift -- value-returning kernels (min/max): descend, then re-lift the
408
406
  # output back into the Face -- a full-reduction scalar via the
409
- # registered storage_to_scalar hook, a per-axis CArray via
410
- # rb_ca_face_template (carries the subclass tail, e.g. unit).
407
+ # registered storage_to_element hook, a per-axis CArray via
408
+ # ca_face_relift_entity (carries the subclass tail, e.g. unit).
411
409
  raise "reduce: unknown face_gate #{face_gate} (expected nil, :strip, or :relift)" \
412
410
  unless [nil, :strip, :relift].include?(face_gate)
413
411
  # object_escape: a Symbol naming a Ruby instance method that handles the
@@ -659,41 +657,6 @@ module MkKernel
659
657
  }
660
658
  end
661
659
 
662
- # Register a map kernel (element-wise transform: input shape == output
663
- # shape, one cell at a time). Each generated helper walks the input
664
- # and output slabs in lockstep using CA_SLAB_MAP_T(T_LOAD, T_OUT, ...).
665
- #
666
- # DSL:
667
- # MkKernel.map :name,
668
- # source: MkKernel::ALL_NUMERIC,
669
- # output: :f64 | :preserve,
670
- # expr: "r = sqrt((double) v)", # binds v (input) + r (output)
671
- # fallback: :wrap_to_f64 | :raise
672
- #
673
- # Generated method takes no arguments (`a.sqrt_ki` not `a.sqrt_ki(0)`)
674
- # since the operation is per-cell across the whole array.
675
- def self.map(name, source:, output:, expr:, fallback: :raise)
676
- raise "duplicate kernel #{name}" if KERNELS.any? { |k| k[:name] == name }
677
- source.each do |s|
678
- raise "unknown source data_type #{s}" unless DTYPES.key?(s)
679
- end
680
- raise "unknown output #{output}" unless output == :preserve || DTYPES.key?(output) || output.is_a?(Hash)
681
- raise "unknown fallback #{fallback}" unless %i[raise wrap_to_f64].include?(fallback)
682
- if output == :preserve && fallback == :wrap_to_f64
683
- raise "#{name}: :preserve + :wrap_to_f64 is a semantic conflict " \
684
- "(fallback path produces f64 output, contradicting :preserve)"
685
- end
686
-
687
- KERNELS << {
688
- kind: :map,
689
- name: name,
690
- source: source,
691
- output: output,
692
- expr: expr,
693
- fallback: fallback,
694
- }
695
- end
696
-
697
660
  # Register a scan kernel (cumulative / prefix-scan: input shape ==
698
661
  # output shape, running accumulator along one axis). The generated
699
662
  # method takes exactly one axis argument (e.g., `a.cumsum_ki(0)`).
@@ -704,7 +667,11 @@ module MkKernel
704
667
  # output: :f64 | :preserve,
705
668
  # init: "0", # initial acc value (per fiber)
706
669
  # step: "acc += v; r = acc", # binds v (T_LOAD), r (T_OUT), acc
707
- # fallback: :wrap_to_f64 | :raise
670
+ # fallback: :wrap_to_f64 | :raise | :mask_count
671
+ #
672
+ # :mask_count is for a scan that reads only the mask (cumcount): a data
673
+ # type outside source: is answered as is_not_masked.cumsum(axis:) in
674
+ # int64, so the values are never converted.
708
675
  #
709
676
  # Init supports the same special tokens as reduce: T_LIMIT_HI /
710
677
  # T_LIMIT_LO / T_ACC / T_OUT. Each scan slab (= fiber along the
@@ -737,14 +704,19 @@ module MkKernel
737
704
  # the input is masked). Aligns core cummax / cummin with
738
705
  # the reduction contract (empty max / min = UNDEF, no
739
706
  # identity) and the axis-group scan family.
707
+ # private:
708
+ # false (default) -> bound as the public `<name>_ki`
709
+ # true -> bound only as the private method `<name>`, a part
710
+ # that lib reaches with `send` (not user surface)
740
711
  def self.scan(name, source:, output:, init:, step:, fallback: :raise,
741
- acc_type: nil, axis_default: nil, empty: nil)
712
+ acc_type: nil, axis_default: nil, empty: nil, face_gate: nil,
713
+ private: false)
742
714
  raise "duplicate kernel #{name}" if KERNELS.any? { |k| k[:name] == name }
743
715
  source.each do |s|
744
716
  raise "unknown source data_type #{s}" unless DTYPES.key?(s)
745
717
  end
746
718
  raise "unknown output #{output}" unless output == :preserve || DTYPES.key?(output) || output.is_a?(Hash)
747
- raise "unknown fallback #{fallback}" unless %i[raise wrap_to_f64].include?(fallback)
719
+ raise "unknown fallback #{fallback}" unless %i[raise wrap_to_f64 mask_count].include?(fallback)
748
720
  if output == :preserve && fallback == :wrap_to_f64
749
721
  raise "#{name}: :preserve + :wrap_to_f64 is a semantic conflict"
750
722
  end
@@ -760,6 +732,13 @@ module MkKernel
760
732
  if empty == :undef && acc_type == :load_type
761
733
  raise "#{name}: empty: :undef is not supported with acc_type: :load_type"
762
734
  end
735
+ # face_gate: :relift descends an ORDERABLE Face to its storage, scans
736
+ # the storage and puts the Face back on the result (the reduce family's
737
+ # policy). Only a scan whose output is the source's own values may take
738
+ # it: a running sum of instants is not an instant.
739
+ unless face_gate.nil? || face_gate == :relift
740
+ raise "#{name}: unknown face_gate #{face_gate.inspect} (nil | :relift)"
741
+ end
763
742
 
764
743
  KERNELS << {
765
744
  kind: :scan,
@@ -772,6 +751,8 @@ module MkKernel
772
751
  acc_type: acc_type,
773
752
  axis_default: axis_default,
774
753
  empty: empty,
754
+ face_gate: face_gate,
755
+ private: private,
775
756
  }
776
757
  end
777
758
 
@@ -967,7 +948,7 @@ module MkKernel
967
948
  # is up to the body (= value-returning search variants).
968
949
  def self.search(name, source:, output:, body:,
969
950
  no_match: "-1", no_match_check: nil, undef_no_match: false,
970
- mask_self: :raise, mask_query: :ignore, runtime_args: nil,
951
+ mask_self: :raise, runtime_args: nil,
971
952
  fallback: :raise, semantics: :fiber_local)
972
953
  raise "search: unknown semantics #{semantics} (expected :fiber_local or :view_flat)" \
973
954
  unless %i[fiber_local view_flat].include?(semantics)
@@ -979,12 +960,10 @@ module MkKernel
979
960
  raise "search: no_match must be a String" unless no_match.is_a?(String)
980
961
  raise "search: unknown mask_self #{mask_self} (expected :raise | :skip | :ignore)" \
981
962
  unless %i[raise skip ignore].include?(mask_self)
982
- # mask_query: what a masked query cell means. :ignore reads the value
983
- # under the mask (the historical behaviour). :undef treats the query as
984
- # having no answer and marks the corresponding output cell UNDEF, so an
985
- # undetermined query cannot silently become a determined result.
986
- raise "search: unknown mask_query #{mask_query} (expected :ignore | :undef)" \
987
- unless %i[ignore undef].include?(mask_query)
963
+ # A masked query cell (or a scalar UNDEF query) has no answer: its output
964
+ # cell is UNDEF, so an undetermined query cannot become a determined
965
+ # result from the value that happens to sit under the mask. There is no
966
+ # option to read under the mask.
988
967
  raise "search: unknown fallback #{fallback} (expected :raise)" \
989
968
  unless fallback == :raise
990
969
 
@@ -1049,7 +1028,6 @@ module MkKernel
1049
1028
  no_match_check: no_match_check,
1050
1029
  undef_no_match: undef_no_match,
1051
1030
  mask_self: mask_self,
1052
- mask_query: mask_query,
1053
1031
  runtime_args: runtime_args || [],
1054
1032
  fallback: fallback,
1055
1033
  semantics: semantics,
@@ -1446,6 +1424,12 @@ module MkKernel
1446
1424
  true
1447
1425
  end
1448
1426
 
1427
+ # The CArray names of the data types a kernel loads, for its refusal
1428
+ # message ("int8, uint8, ..."), not the generator's own short keys.
1429
+ def self.type_names(sources)
1430
+ sources.map { |s| s == :fixlen ? "fixlen" : DTYPES[s][:ca].sub(/\ACA_/, "").downcase }.join(", ")
1431
+ end
1432
+
1449
1433
  def self.reduce_macro_suffix(k, src = nil)
1450
1434
  # CA_OBJECT cannot ride the SIMD-licensed macros (= _PLUS / _MIN / _MAX
1451
1435
  # / _STAR), which assume C operators (= acc is a VALUE, so
@@ -1624,9 +1608,11 @@ module MkKernel
1624
1608
  require "fileutils"
1625
1609
  FileUtils.mkdir_p(outdir)
1626
1610
  tags = file_tags
1611
+ written = ["carray_kernels_init.c"]
1627
1612
  tags.each do |kind, subgroup|
1628
1613
  suffix = file_suffix(kind, subgroup)
1629
1614
  path = File.join(outdir, "carray_kernels_#{suffix}.c")
1615
+ written << File.basename(path)
1630
1616
  File.open(path, "w") do |io|
1631
1617
  io.puts header
1632
1618
  KERNELS.each do |k|
@@ -1640,6 +1626,11 @@ module MkKernel
1640
1626
  File.open(File.join(outdir, "carray_kernels_init.c"), "w") do |io|
1641
1627
  emit_aggregator_init(io, tags)
1642
1628
  end
1629
+ # extconf compiles every carray_kernels_*.c it finds, so a file left
1630
+ # by a kind or sub-group that no longer exists has to go.
1631
+ Dir.glob(File.join(outdir, "carray_kernels_*.c")).each do |f|
1632
+ File.delete(f) unless written.include?(File.basename(f))
1633
+ end
1643
1634
  end
1644
1635
 
1645
1636
  # Which file a kernel belongs to. The three alias kinds live with the kind
@@ -1658,7 +1649,6 @@ module MkKernel
1658
1649
  def self.emit_kernel_dispatch(io, k)
1659
1650
  case k[:kind]
1660
1651
  when :reduce then emit_reduce(io, k)
1661
- when :map then emit_map(io, k)
1662
1652
  when :scan then emit_scan(io, k)
1663
1653
  when :sort then emit_sort(io, k)
1664
1654
  when :search then emit_search(io, k)
@@ -1675,7 +1665,7 @@ module MkKernel
1675
1665
  end
1676
1666
  end
1677
1667
 
1678
- KINDS = [:reduce, :map, :scan, :sort, :search,
1668
+ KINDS = [:reduce, :scan, :sort, :search,
1679
1669
  :monop, :binop, :triop, :moncmp, :bincmp].freeze
1680
1670
 
1681
1671
  # Sub-split map for the reduce kind. As one file, reduce accounted for 71%
@@ -1686,17 +1676,16 @@ module MkKernel
1686
1676
  # rather than the guess that monop and binop would dominate. A kernel added
1687
1677
  # later needs an entry here; emit asserts, so a missing one is loud.
1688
1678
  REDUCE_SUB_GROUPS = {
1689
- # aggregate: arithmetic folds (sum / prod / mean and their strict,
1690
- # safe and weighted variants)
1679
+ # aggregate: arithmetic folds (sum / prod / mean and their weighted
1680
+ # variants)
1691
1681
  sum: :aggregate, prod: :aggregate, mean: :aggregate,
1692
- sum_strict: :aggregate, mean_safe: :aggregate,
1693
1682
  wsum: :aggregate, wmean: :aggregate,
1694
1683
  # extreme: order-based (min / max, argmin / argmax, minmax)
1695
1684
  min: :extreme, max: :extreme, minmax: :extreme,
1696
1685
  argmin: :extreme, argmax: :extreme,
1697
1686
  argmin_addr: :extreme, argmax_addr: :extreme,
1698
1687
  # cumulative: counter accumulation (the count family and accumulate)
1699
- count: :cumulative, accumulate: :cumulative,
1688
+ accumulate: :cumulative,
1700
1689
  count_true: :cumulative, count_false: :cumulative,
1701
1690
  count_equal: :cumulative,
1702
1691
  # variance family: multi-state (Chan / Welford), and the one that
@@ -1784,11 +1773,116 @@ module MkKernel
1784
1773
  base + HEADER_BLOCKS.join("\n")
1785
1774
  end
1786
1775
 
1776
+ # Emits the native function of one source data type. The object lane
1777
+ # calls Ruby for every cell, so any cell can raise in the middle of a walk;
1778
+ # its function is rebuilt around ca_iter_ensure (see guard_object_native).
1779
+ # Every other lane is emitted as the emitter writes it.
1780
+ def self.emit_native_for(io, k, src)
1781
+ if src == :object
1782
+ buf = StringIO.new
1783
+ yield buf
1784
+ io.print guard_object_native(buf.string, "#{k[:name]}_ki_native_object")
1785
+ else
1786
+ yield io
1787
+ end
1788
+ end
1789
+
1790
+ # Index of the brace that closes the one at text[open], skipping string
1791
+ # and character literals and comments.
1792
+ def self.matching_brace(text, open)
1793
+ depth = 0
1794
+ i = open
1795
+ while i < text.length
1796
+ c = text[i]
1797
+ case c
1798
+ when '"', "'"
1799
+ i += 1
1800
+ i += (text[i] == "\\") ? 2 : 1 while i < text.length && text[i] != c
1801
+ when "/"
1802
+ if text[i + 1] == "/"
1803
+ i = text.index("\n", i) || text.length
1804
+ elsif text[i + 1] == "*"
1805
+ i = (text.index("*/", i + 2) || text.length) + 1
1806
+ end
1807
+ when "{"
1808
+ depth += 1
1809
+ when "}"
1810
+ depth -= 1
1811
+ return i if depth == 0
1812
+ end
1813
+ i += 1
1814
+ end
1815
+ raise "mkkernel: unbalanced braces"
1816
+ end
1817
+
1818
+ # Rebuilds the native function +fn+ found in +text+ so that its walks are
1819
+ # finished however it leaves. The body moves into a function of its own
1820
+ # that ca_iter_ensure runs; each `ca_iter_state` it declared becomes a
1821
+ # slot of a frame kept by the caller, since a state in the body's own
1822
+ # frame is gone once the body raises. The body text is kept as written:
1823
+ # the parameters are unpacked from the frame under their own names, and
1824
+ # each state name is defined to its slot for the length of the body.
1825
+ def self.guard_object_native(text, fn)
1826
+ header = /^static VALUE\n#{fn} \(([^)]*)\)\n\{\n/
1827
+ m = header.match(text) or raise "mkkernel: #{fn} not found"
1828
+ close = matching_brace(text, m.end(0) - 2)
1829
+ body = text[m.end(0)...close]
1830
+ rest = text[(close + 1)..]
1831
+
1832
+ states = []
1833
+ body = body.gsub(/^[ \t]*ca_iter_state[ \t]+(\w+(?:[ \t]*,[ \t]*\w+)*)[ \t]*;[ \t]*\n/) do
1834
+ states.concat($1.split(/[ \t]*,[ \t]*/))
1835
+ ""
1836
+ end
1837
+ states.uniq!
1838
+ return text if states.empty?
1839
+
1840
+ params = m[1].split(",").map do |p|
1841
+ pm = p.strip.match(/\A(.*?)(\w+)\z/) or raise "mkkernel: #{fn} parameter #{p}"
1842
+ [pm[1].strip, pm[2]]
1843
+ end
1844
+ frame = "#{fn}_frame"
1845
+
1846
+ out = StringIO.new
1847
+ out.print m.pre_match
1848
+ out.puts "/* The object lane calls Ruby for every cell, so a walk can raise part"
1849
+ out.puts " way. Its states live in the caller's frame, and ca_iter_ensure"
1850
+ out.puts " finishes them however the body leaves. */"
1851
+ out.puts "typedef struct {"
1852
+ out.puts " ca_iter_state states[#{states.size}];"
1853
+ params.each { |type, name| out.puts " #{type} #{name};".sub(/\* /, "*") }
1854
+ out.puts "} #{frame};"
1855
+ out.puts
1856
+ out.puts "static VALUE"
1857
+ out.puts "#{fn}_body (VALUE arg)"
1858
+ out.puts "{"
1859
+ out.puts " #{frame} *fr = (#{frame} *) arg;"
1860
+ params.each do |type, name|
1861
+ out.puts " #{type} #{name} = fr->#{name};".sub(/\* /, "*")
1862
+ out.puts " (void) #{name};"
1863
+ end
1864
+ states.each_with_index { |s, i| out.puts "#define #{s} (fr->states[#{i}])" }
1865
+ out.print body.sub(/[ \t]*\z/, "")
1866
+ out.puts unless body.match?(/\n[ \t]*\z/)
1867
+ states.each { |s| out.puts "#undef #{s}" }
1868
+ out.puts "}"
1869
+ out.puts
1870
+ out.puts "static VALUE"
1871
+ out.puts "#{fn} (#{m[1]})"
1872
+ out.puts "{"
1873
+ out.puts " #{frame} fr;"
1874
+ params.each { |_, name| out.puts " fr.#{name} = #{name};" }
1875
+ out.puts " return ca_iter_ensure(#{states.size}, fr.states, #{fn}_body, (VALUE) &fr);"
1876
+ out.puts "}"
1877
+ out.print rest
1878
+ out.string
1879
+ end
1880
+
1787
1881
  def self.emit_reduce(io, k)
1788
1882
  io.puts
1789
1883
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
1790
1884
  k[:source].each do |src|
1791
- emit_reduce_native(io, k, src)
1885
+ emit_native_for(io, k, src) { |o| emit_reduce_native(o, k, src) }
1792
1886
  end
1793
1887
  emit_reduce_dispatch(io, k)
1794
1888
  end
@@ -1860,6 +1954,11 @@ module MkKernel
1860
1954
 
1861
1955
  view_flat = (k[:semantics] == :view_flat)
1862
1956
 
1957
+ if streamable
1958
+ emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var, acc_init,
1959
+ decls, reduce_stmt, finish_expr, src)
1960
+ end
1961
+
1863
1962
  io.puts
1864
1963
  io.puts <<~C
1865
1964
  static VALUE
@@ -1868,8 +1967,7 @@ module MkKernel
1868
1967
  C
1869
1968
 
1870
1969
  if streamable
1871
- emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var, acc_init,
1872
- decls, reduce_stmt, finish_expr, extra_args, src)
1970
+ emit_reduce_streaming_call(io, k, src)
1873
1971
  end
1874
1972
 
1875
1973
  # L.1 / L.3 / L.4 (PROPOSAL_REDUCTION_LOOP_INTERCHANGE):
@@ -1917,12 +2015,22 @@ module MkKernel
1917
2015
  else
1918
2016
  mixed_buf = false
1919
2017
  end
1920
- li_eligible = !k[:array_arg] && !k[:value_arg] &&
2018
+ # The object lane is left out: its cell is a Ruby call, so the
2019
+ # interchange's contiguous SIMD loads buy nothing, and each output cell
2020
+ # accumulates in the same order on the generic walk.
2021
+ li_eligible = src != :object &&
2022
+ !k[:array_arg] && !k[:value_arg] &&
1921
2023
  buf_count >= 1 && induction_count <= 1 && other_count == 0 &&
1922
2024
  !mixed_buf
1923
2025
  if li_eligible
2026
+ # The interchange folds every cell of an unmasked source, so it can
2027
+ # answer only when the reduced axis holds at least min_count cells;
2028
+ # otherwise the general walk writes the UNDEF (or fill_value) cells.
2029
+ li_gate = k[:mask_policy] == :min_count
2030
+ io.puts " if ( naxes != 1 || ! ca_too_few_present(ca->dim[slab_axes[0]], min_count, 0) ) {" if li_gate
1924
2031
  emit_reduce_loop_interchange(io, k, si, oi, ruby_wrap, src,
1925
2032
  acc_var, acc_init, reduce_stmt, finish_expr)
2033
+ io.puts " }" if li_gate
1926
2034
  end
1927
2035
 
1928
2036
  if view_flat
@@ -1932,7 +2040,7 @@ module MkKernel
1932
2040
  # (slab walks the whole array in view-flat row-major); other
1933
2041
  # naxes are rejected at entry.
1934
2042
  io.puts " if ( naxes != 1 && naxes != ca->ndim ) {"
1935
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: view_flat semantics requires single-axis or full reduction (got naxes=%d for ndim=%d)", (int)naxes, (int)ca->ndim);]
2043
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: view_flat semantics requires single-axis or full reduction (got naxes=%d for ndim=%d)", ca_calling_method_name(), (int)naxes, (int)ca->ndim);]
1936
2044
  io.puts " }"
1937
2045
  end
1938
2046
 
@@ -1946,7 +2054,7 @@ module MkKernel
1946
2054
  slab_axes, naxes, 0);
1947
2055
  if ( rc != CA_ITER_OK ) {
1948
2056
  rb_raise(rb_eRuntimeError,
1949
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
2057
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
1950
2058
  }
1951
2059
  char *p;
1952
2060
  boolean8_t *m;
@@ -1984,7 +2092,7 @@ module MkKernel
1984
2092
  io.puts " slab_axes, naxes, 0);"
1985
2093
  io.puts " if ( rcw != CA_ITER_OK ) {"
1986
2094
  io.puts " ca_iter_state_finish(&st);"
1987
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: weights kernel_iterator init failed rc=%d", rcw);]
2095
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: weights kernel_iterator init failed rc=%d", ca_calling_method_name(), rcw);]
1988
2096
  io.puts " }"
1989
2097
  io.puts " char *p_w;"
1990
2098
  end
@@ -1999,7 +2107,7 @@ module MkKernel
1999
2107
  io.puts " if ( ! ca_iter_state_next_slab_axes(&st_w, &p_w, NULL) ) {"
2000
2108
  io.puts " ca_iter_state_finish(&st);"
2001
2109
  io.puts " ca_iter_state_finish(&st_w);"
2002
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: weights iter exhausted early (shape invariant violated)");]
2110
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: weights iter exhausted early (shape invariant violated)", ca_calling_method_name());]
2003
2111
  io.puts " }"
2004
2112
  else
2005
2113
  io.puts " while ( ca_iter_state_next_slab_axes(&st, &p, &m) ) {"
@@ -2035,6 +2143,11 @@ module MkKernel
2035
2143
  if k[:all_nan_result] == :undef && FLOAT_DTYPES.include?(src)
2036
2144
  all_nan_flag = "__anf_all_nan"
2037
2145
  io.puts " int #{all_nan_flag} = 0;"
2146
+ elsif k[:all_nan_result] == :undef && src == :object
2147
+ # The object body takes no NaN cell, so a slab of present NaN
2148
+ # cells leaves the position unset.
2149
+ all_nan_flag = "__anf_all_nan"
2150
+ io.puts " int #{all_nan_flag} = ( masked_cnt < st.slab_elements && (#{finish_expr}) < 0 );"
2038
2151
  end
2039
2152
  emit_all_nan_fixup(io, k, src, si, pairs,
2040
2153
  "masked_cnt < st.slab_elements",
@@ -2042,24 +2155,12 @@ module MkKernel
2042
2155
  end
2043
2156
  trigger = case k[:mask_policy]
2044
2157
  when :strict then "masked_cnt > 0"
2045
- when :all_masked then "masked_cnt == st.slab_elements"
2158
+ when :all_masked then "ca_too_few_present(st.slab_elements - masked_cnt, -1, 1)"
2046
2159
  when :min_count
2047
- # min_count < 0 (sentinel) => legacy default (all_masked).
2048
- # min_count >= 0 => need at least min_count valid cells
2049
- # (valid_cnt = slab_elements - masked_cnt).
2050
- #
2051
- # ERI.0: identity_on_empty kernels do NOT fire UNDEF for the
2052
- # default (min_count < 0) zero-contribution case; the trigger
2053
- # is false there, so the else branch writes finish (= acc =
2054
- # identity) for empty / all-masked slabs. The explicit
2055
- # min_count >= 0 opt-in ("require K valid cells") is unchanged.
2056
- if k[:identity_on_empty]
2057
- "(min_count < 0 ? 0 " \
2058
- ": st.slab_elements - masked_cnt < min_count)"
2059
- else
2060
- "(min_count < 0 ? masked_cnt == st.slab_elements " \
2061
- ": st.slab_elements - masked_cnt < min_count)"
2062
- end
2160
+ # An identity_on_empty kernel needs no cell (the else
2161
+ # branch writes finish = acc = identity for an empty slab).
2162
+ "ca_too_few_present(st.slab_elements - masked_cnt, min_count, " \
2163
+ "#{k[:identity_on_empty] ? 0 : 1})"
2063
2164
  end
2064
2165
  trigger = "(#{trigger}) || #{all_nan_flag}" if all_nan_flag
2065
2166
  finish_emit = view_flat \
@@ -2078,20 +2179,22 @@ module MkKernel
2078
2179
  else
2079
2180
  if k[:array_arg]
2080
2181
  wc = (k[:array_arg][:data_type] == :promote) ? "double" : si[:c]
2081
- io.puts " CA_SLAB_REDUCE_ARRAY_T(#{si[:c]}, #{wc}, st, p, m, st_w, p_w, #{acc_var}, #{acc_init}, #{reduce_stmt});"
2182
+ io.puts " ca_size_t __sra_throwaway_mc = 0;"
2183
+ io.puts " CA_SLAB_REDUCE_ARRAY_T_EX(#{si[:c]}, #{wc}, st, p, m, st_w, p_w, #{acc_var}, #{acc_init}, #{reduce_stmt}, __sra_throwaway_mc);"
2184
+ io.puts " (void) __sra_throwaway_mc;"
2082
2185
  else
2083
2186
  suffix = MkKernel.reduce_macro_suffix(k, src)
2084
- # Non-mask_policy path: emit a throwaway masked_cnt for the 8-way
2085
- # macros (which always take one). Falls through to the legacy
2086
- # non-EX macro when the 8-way path is not eligible (:object src).
2087
- if MkKernel.reduce_8way_eligible?(k, src, suffix)
2187
+ # Non-mask_policy path: the slab macros always take a masked_cnt,
2188
+ # so hand them a throwaway. The generic form (suffix "", the
2189
+ # :object lane) has a wrapper that hides it.
2190
+ if suffix == ""
2191
+ io.puts " CA_SLAB_REDUCE_T(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt});"
2192
+ else
2088
2193
  io.puts " ca_size_t __sr_throwaway_mc = 0;"
2089
2194
  emit_reduce_slab_call(io, k, src, si, oi, suffix, acc_var, acc_init,
2090
2195
  reduce_stmt, "__sr_throwaway_mc",
2091
2196
  indent: " ")
2092
2197
  io.puts " (void) __sr_throwaway_mc;"
2093
- else
2094
- io.puts " CA_SLAB_REDUCE_T#{suffix}(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt});"
2095
2198
  end
2096
2199
  end
2097
2200
  finish_emit = view_flat \
@@ -2166,7 +2269,7 @@ module MkKernel
2166
2269
  io.puts " ca_iter_state st;"
2167
2270
  io.puts " int rc = ca_iter_state_init_l2(&st, ca, CA_SLAB_AXES, slab_axes, naxes, 0);"
2168
2271
  io.puts " if ( rc != CA_ITER_OK ) {"
2169
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: kernel_iterator init failed rc=%d", rc);]
2272
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);]
2170
2273
  io.puts " }"
2171
2274
  io.puts " char *p;"
2172
2275
  io.puts " boolean8_t *m;"
@@ -2215,16 +2318,9 @@ module MkKernel
2215
2318
  io.puts " sidx[sk] = 0;"
2216
2319
  io.puts " }"
2217
2320
  io.puts " }"
2218
- # ERI.0: a count over nothing is 0, not UNDEF -- so the default
2219
- # (min_count < 0) never fires the mask for :count_equal. An explicit
2220
- # min_count: still does.
2221
- if counting
2222
- io.puts " if ( min_count < 0 ? 0"
2223
- io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2224
- else
2225
- io.puts " if ( min_count < 0 ? masked_cnt == st.slab_elements"
2226
- io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2227
- end
2321
+ # A count over nothing is 0, not UNDEF: it needs no cell.
2322
+ io.puts " if ( ca_too_few_present(st.slab_elements - masked_cnt, min_count, " \
2323
+ "#{counting ? 0 : 1}) ) {"
2228
2324
  io.puts " if ( ! op_mask ) {"
2229
2325
  io.puts " ca_create_mask(co);"
2230
2326
  io.puts " op_mask = (boolean8_t *) co->mask->ptr;"
@@ -2277,10 +2373,8 @@ module MkKernel
2277
2373
  # Layout per slab:
2278
2374
  # Pass 1: sum = Σ x_i via CA_SLAB_REDUCE_T_PLUS_EX (SIMD :plus)
2279
2375
  # masked_cnt is populated as a side effect.
2280
- # mask trigger:
2281
- # min_count < 0 (default) -> UNDEF iff all-masked, OR (for
2282
- # :n_minus_1 divisor) if n_valid < 2.
2283
- # min_count >= 0 -> UNDEF iff n_valid < min_count.
2376
+ # mask trigger: ca_too_few_present(n_valid, min_count, 1 or 2), 2 for
2377
+ # the :n_minus_1 divisor.
2284
2378
  # Pass 2: M2 = Σ (x_i - mean)² via the same macro with a centred
2285
2379
  # REDUCE stmt.
2286
2380
  # Finish: variance = M2 / DIVISOR (n or n-1); stddev variant applies
@@ -2387,9 +2481,9 @@ module MkKernel
2387
2481
  finish_expr = if k[:output_transform] == :sqrt_clamp
2388
2482
  if src == :object
2389
2483
  # Object stddev outputs f64 (BigMath.sqrt is user's job).
2390
- "sqrt(fmax(NUM2DBL(#{inner_var}), 0.0))"
2484
+ "ca_sqrt_clamp(NUM2DBL(#{inner_var}))"
2391
2485
  else
2392
- "sqrt(fmax(#{inner_var}, 0.0))"
2486
+ "ca_sqrt_clamp(#{inner_var})"
2393
2487
  end
2394
2488
  else
2395
2489
  inner_var
@@ -2417,13 +2511,9 @@ module MkKernel
2417
2511
  # min_count sentinel arg is always present (all four kernels use it).
2418
2512
  extra_args = ", ca_size_t min_count"
2419
2513
 
2420
- # The default (min_count < 0) trigger fires when all cells are
2421
- # masked; for sample-variance/stddev we also need n_valid >= 2 to
2422
- # avoid division by zero, but the legacy behaviour returned 0
2423
- # (not UNDEF) for n_valid < required_min. Preserve that: the
2424
- # default trigger only checks the all-masked case; the sub-required
2425
- # case falls through to the "return zero" branch below.
2426
- default_trigger = "masked_cnt == st.slab_elements"
2514
+ # Fewer than required_min valid cells (2 for the n-1 divisor) leave the
2515
+ # statistic undefined, so the cell is UNDEF: the sample variance of one
2516
+ # value is 0/0.
2427
2517
 
2428
2518
  # Reduce-call plumbing. :object keeps the scalar rb_funcall loop
2429
2519
  # (CA_SLAB_REDUCE_T_EX). Every other src uses the 8-way accumulator
@@ -2464,7 +2554,7 @@ module MkKernel
2464
2554
  slab_axes, naxes, 0);
2465
2555
  if ( rc != CA_ITER_OK ) {
2466
2556
  rb_raise(rb_eRuntimeError,
2467
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
2557
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
2468
2558
  }
2469
2559
  char *p;
2470
2560
  boolean8_t *m;
@@ -2476,7 +2566,7 @@ module MkKernel
2476
2566
  ca_size_t masked_cnt = 0;
2477
2567
  #{p1_reduce_call}
2478
2568
  ca_size_t n_valid = st.slab_elements - masked_cnt;
2479
- int __trigger = (min_count < 0 ? #{default_trigger} : n_valid < min_count);
2569
+ int __trigger = ca_too_few_present(n_valid, min_count, #{required_min});
2480
2570
  if ( __trigger ) {
2481
2571
  if ( ! op_mask ) {
2482
2572
  ca_create_mask(co);
@@ -2484,11 +2574,6 @@ module MkKernel
2484
2574
  }
2485
2575
  op_mask[out_i] = 1;
2486
2576
  op[out_i++] = (#{out_c}) #{out_zero_typed};
2487
- } else if ( n_valid < #{required_min} ) {
2488
- /* Legacy degenerate case (e.g. sample variance with 1 unmasked
2489
- cell): return zero rather than UNDEF, matching pre-migration
2490
- behaviour of `cnt > 1 ? formula : 0`. */
2491
- op[out_i++] = (#{out_c}) #{out_zero_typed};
2492
2577
  } else {
2493
2578
  #{mean_setup}
2494
2579
  /* Pass 2: M2 = Σ (x - mean)² */
@@ -2660,8 +2745,9 @@ module MkKernel
2660
2745
  exposes ca->ptr as that buffer at O(1) cost (no materialise). */
2661
2746
  if ( naxes == 1
2662
2747
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2663
- && ca->mask == NULL
2664
- && ca_attach_is_alias(ca) ) {
2748
+ && ! ca_has_mask(ca)
2749
+ && ca_attach_is_alias(ca)
2750
+ && ca_fast_paths_enabled() ) {
2665
2751
  int8_t __li_ax = slab_axes[0];
2666
2752
  ca_size_t __li_M = ca->dim[__li_ax];
2667
2753
  ca_size_t __li_INNER = 1;
@@ -2675,7 +2761,7 @@ module MkKernel
2675
2761
  CArray *co;
2676
2762
  GetCArray(vout, co);
2677
2763
  #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
2678
- ca_attach(ca); /* O(1): entity no-op or alias attach */
2764
+ ca_attach(ca); /* window: numeric lanes only, nothing raises inside; O(1) alias attach */
2679
2765
  const #{si[:c]} *p_root = (const #{si[:c]} *) ca->ptr;
2680
2766
 
2681
2767
  /* Per-buffer stack accumulators (one per :plus state, sized
@@ -2709,9 +2795,10 @@ module MkKernel
2709
2795
  if ( naxes == 1
2710
2796
  && slab_axes[0] >= 1
2711
2797
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2712
- && ca->mask == NULL
2798
+ && ! ca_has_mask(ca)
2713
2799
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2714
- && ((CAStack *) ca)->k_axis == 0 ) {
2800
+ && ((CAStack *) ca)->k_axis == 0
2801
+ && ca_fast_paths_enabled() ) {
2715
2802
  CAStack *__li_st = (CAStack *) ca;
2716
2803
  int8_t __li_ax = slab_axes[0];
2717
2804
  ca_size_t __li_M = ca->dim[__li_ax];
@@ -2724,7 +2811,7 @@ module MkKernel
2724
2811
  core reads parent->ptr without mask awareness). */
2725
2812
  int __li_ok = 1;
2726
2813
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2727
- if ( __li_st->parents[__kk]->mask != NULL ) { __li_ok = 0; break; }
2814
+ if ( ca_has_mask(__li_st->parents[__kk]) ) { __li_ok = 0; break; }
2728
2815
  }
2729
2816
 
2730
2817
  if ( __li_ok && __li_INNER >= 64 && __li_M * __li_INNER >= 1024 ) {
@@ -2739,7 +2826,7 @@ module MkKernel
2739
2826
  ca_size_t __li_parent_out = __li_OUTER * __li_INNER;
2740
2827
  const ca_size_t __li_TILE = 512;
2741
2828
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2742
- ca_attach(__li_st->parents[__kk]);
2829
+ ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2743
2830
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2744
2831
  #{oi[:c]} *__li_op_k = op + (ca_size_t) __kk * __li_parent_out;
2745
2832
  #{core.call("p_root", "__li_op_k", "__li_OUTER")}
@@ -2772,11 +2859,12 @@ module MkKernel
2772
2859
  if ( naxes == 1
2773
2860
  && slab_axes[0] >= 1
2774
2861
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2775
- && ca->mask == NULL
2862
+ && ! ca_has_mask(ca)
2776
2863
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2777
2864
  && ((CAStack *) ca)->k_axis > 0
2778
2865
  && ( slab_axes[0] > ((CAStack *) ca)->k_axis
2779
- || slab_axes[0] + 1 == ((CAStack *) ca)->k_axis ) ) {
2866
+ || slab_axes[0] + 1 == ((CAStack *) ca)->k_axis )
2867
+ && ca_fast_paths_enabled() ) {
2780
2868
  CAStack *__li_st = (CAStack *) ca;
2781
2869
  int8_t __li_ax = slab_axes[0];
2782
2870
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -2792,7 +2880,7 @@ module MkKernel
2792
2880
  /* Bail to the generic path if any parent carries a mask. */
2793
2881
  int __li_ok = 1;
2794
2882
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2795
- if ( __li_st->parents[__kk]->mask != NULL ) { __li_ok = 0; break; }
2883
+ if ( ca_has_mask(__li_st->parents[__kk]) ) { __li_ok = 0; break; }
2796
2884
  }
2797
2885
 
2798
2886
  if ( __li_ok && __li_INNER >= 64 && __li_M * __li_INNER >= 1024 ) {
@@ -2833,7 +2921,7 @@ module MkKernel
2833
2921
  ca_size_t __li_plane_elems = __li_M * __li_INNER;
2834
2922
  const ca_size_t __li_TILE = 512;
2835
2923
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2836
- ca_attach(__li_st->parents[__kk]);
2924
+ ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2837
2925
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2838
2926
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
2839
2927
 
@@ -2915,10 +3003,11 @@ module MkKernel
2915
3003
  if ( naxes == 1
2916
3004
  && slab_axes[0] >= 1
2917
3005
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2918
- && ca->mask == NULL
3006
+ && ! ca_has_mask(ca)
2919
3007
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2920
3008
  && ((CAStack *) ca)->k_axis > 0
2921
- && slab_axes[0] + 1 < ((CAStack *) ca)->k_axis ) {
3009
+ && slab_axes[0] + 1 < ((CAStack *) ca)->k_axis
3010
+ && ca_fast_paths_enabled() ) {
2922
3011
  CAStack *__li_st = (CAStack *) ca;
2923
3012
  int8_t __li_ax = slab_axes[0];
2924
3013
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -2936,7 +3025,7 @@ module MkKernel
2936
3025
  /* Bail to the generic path if any parent carries a mask. */
2937
3026
  int __li_ok = 1;
2938
3027
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2939
- if ( __li_st->parents[__kk]->mask != NULL ) { __li_ok = 0; break; }
3028
+ if ( ca_has_mask(__li_st->parents[__kk]) ) { __li_ok = 0; break; }
2940
3029
  }
2941
3030
 
2942
3031
  if ( __li_ok && __li_eff_INNER >= 32 && __li_M * __li_eff_INNER >= 1024 ) {
@@ -2990,7 +3079,7 @@ module MkKernel
2990
3079
 
2991
3080
  const ca_size_t __li_TILE = 512;
2992
3081
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2993
- ca_attach(__li_st->parents[__kk]);
3082
+ ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2994
3083
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2995
3084
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
2996
3085
 
@@ -3097,7 +3186,7 @@ module MkKernel
3097
3186
  slab_axes, naxes, 0);
3098
3187
  if ( rc != CA_ITER_OK ) {
3099
3188
  rb_raise(rb_eRuntimeError,
3100
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
3189
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
3101
3190
  }
3102
3191
  char *p;
3103
3192
  boolean8_t *m;
@@ -3116,10 +3205,7 @@ module MkKernel
3116
3205
  emit_all_nan_fixup(io, k, src, si, limit_init_state_vars(k, si, oi, src),
3117
3206
  "masked_cnt < st.slab_elements", indent: " ")
3118
3207
  end
3119
- # Same trigger as single-output :min_count: legacy default (all_masked)
3120
- # when min_count < 0, otherwise need at least min_count valid cells.
3121
- trigger = "(min_count < 0 ? masked_cnt == st.slab_elements " \
3122
- ": st.slab_elements - masked_cnt < min_count)"
3208
+ trigger = "ca_too_few_present(st.slab_elements - masked_cnt, min_count, 1)"
3123
3209
  io.puts " if ( #{trigger} ) {"
3124
3210
  io.puts " if ( ! op_mask_a ) {"
3125
3211
  io.puts " ca_create_mask(co_a);"
@@ -3153,10 +3239,11 @@ module MkKernel
3153
3239
  # pair of [1,...,1] entities instead.
3154
3240
  io.puts " if ( naxes == ca->ndim && !keep_axis ) {"
3155
3241
  if min_count
3156
- # Full reduction: if the sole output cell is masked, return CA_UNDEF.
3157
- # (Both op_mask_a and op_mask_b share the same bit since both
3158
- # reductions consume the same slab, so checking _a is sufficient.)
3159
- io.puts " if ( op_mask_a && op_mask_a[0] ) return CA_UNDEF;"
3242
+ # Full reduction: if the sole output cell is masked, both answers are
3243
+ # UNDEF, still as a pair -- the shape of the answer does not depend on
3244
+ # the data. (Both op_mask_a and op_mask_b share the same bit since
3245
+ # both reductions consume the same slab, so checking _a is sufficient.)
3246
+ io.puts " if ( op_mask_a && op_mask_a[0] ) return rb_assoc_new(CA_UNDEF, CA_UNDEF);"
3160
3247
  end
3161
3248
  io.puts " return rb_assoc_new(#{ruby_wrap}(op_a[0]), #{ruby_wrap}(op_b[0]));"
3162
3249
  io.puts " }"
@@ -3164,42 +3251,40 @@ module MkKernel
3164
3251
  io.puts " }"
3165
3252
  end
3166
3253
 
3167
- # P.4.5.3b/c: streaming chunked reduce path emitter. Generates an
3168
- # early-return branch at the top of name_ki_native_<src> that pulls
3169
- # the lazy view in fixed-size chunks via ca_xfer_stride instead of
3170
- # full-materialising via SRC_ATTACH. Falls through to the existing
3171
- # path when conditions aren't met.
3172
- #
3173
- # Strategy: chunk along the outermost axis. inner = prod(dim[1..]);
3174
- # rows_per_chunk = max(1, target_elems / inner). 1-D is the natural
3175
- # sub-case (inner == 1, rows = target_elems).
3176
- #
3177
- # Memory peak: O(max(target_elems, inner) * sizeof T) instead of O(N).
3178
- # For N-D, this drops the outermost dim from the materialised buffer,
3179
- # which is typically the dominant factor (e.g. 1000x10000 f64 reduction:
3180
- # 80MB -> 80KB). Wall-clock perf at large N matches or beats
3181
- # SRC_ATTACH because each chunk fits in L1d (or L2 for wide last dims).
3182
- #
3183
- # Restrictions handled by short-circuit checks (= falls through to
3184
- # SRC_ATTACH path):
3185
- # - axis kwarg (= partial reduction): naxes != ca->ndim
3186
- # (structural blocker: CABinOp/CABinCmp xfer_stride rebuilds
3187
- # right_strides from counts, so non-rectangular slab pulls
3188
- # produce wrong values for the right operand. See proposal
3189
- # §future-work / partial streaming blocker.)
3190
- # - non-lazy source: !ca_is_lazy_view(ca)
3191
- # - mask present: ca_has_mask(ca)
3254
+ # The walk of the streaming reduce (see emit_reduce_streaming_call).
3192
3255
  def self.emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var,
3193
3256
  acc_init, decls, reduce_stmt,
3194
- finish_expr, extra_args, src)
3257
+ finish_expr, src)
3195
3258
  name = k[:name]
3196
- min_count = (k[:mask_policy] == :min_count)
3197
3259
  has_mp = !k[:mask_policy].nil?
3260
+ fn = "#{name}_ki_stream_#{src}"
3198
3261
 
3199
- io.puts " /* P.4.5.3b/c streaming chunked reduce: full reduction over lazy unmasked. */"
3200
- io.puts " /* P.4.5.3c (N-D): outer-axis chunking, 1-D is the natural sub-case. */"
3201
- io.puts " if ( naxes == ca->ndim && ca->ndim >= 1 && !keep_axis &&"
3202
- io.puts " ca_is_lazy_view(ca) && ! ca_has_mask(ca) ) {"
3262
+ # The walk runs under ca_lazy_arena_protect: the transfer from the lazy
3263
+ # source and the object lane's fold can both raise, and the arena must
3264
+ # be left however the walk ends. So it is a function of its own, with
3265
+ # the native's arguments carried in a context whose first member is
3266
+ # the chunk (released by ca_lazy_arena_release_held).
3267
+ io.puts
3268
+ io.puts "typedef struct {"
3269
+ io.puts " void *chunk;"
3270
+ io.puts " CArray *ca;"
3271
+ io.puts " #{si[:c]} value_arg;" if k[:value_arg]
3272
+ io.puts " ca_size_t min_count;" if k[:mask_policy] == :min_count
3273
+ io.puts "} #{fn}_t;"
3274
+ io.puts
3275
+ io.puts "static VALUE"
3276
+ io.puts "#{fn} (VALUE arg)"
3277
+ io.puts "{"
3278
+ io.puts " #{fn}_t *__ctx = (#{fn}_t *) arg;"
3279
+ io.puts " CArray *ca = __ctx->ca;"
3280
+ if k[:value_arg]
3281
+ io.puts " #{si[:c]} value_arg = __ctx->value_arg;"
3282
+ io.puts " (void) value_arg;"
3283
+ end
3284
+ if k[:mask_policy] == :min_count
3285
+ io.puts " ca_size_t min_count = __ctx->min_count;"
3286
+ io.puts " (void) min_count;"
3287
+ end
3203
3288
  io.puts " ca_size_t __inner = 1;"
3204
3289
  io.puts " int __k;"
3205
3290
  io.puts " for ( __k = 1; __k < ca->ndim; __k++ ) __inner *= ca->dim[__k];"
@@ -3237,7 +3322,6 @@ module MkKernel
3237
3322
  io.puts " ca_size_t masked_cnt = 0; /* unused on streaming (no mask) */"
3238
3323
  io.puts " (void) masked_cnt;"
3239
3324
  end
3240
- io.puts " ca_lazy_arena_enter();"
3241
3325
  if si[:c] == "VALUE"
3242
3326
  # Object lane: the chunk holds VALUEs pulled from the source, and
3243
3327
  # the fold below calls rb_funcall per cell. A collection there
@@ -3247,6 +3331,7 @@ module MkKernel
3247
3331
  else
3248
3332
  io.puts " #{si[:c]} *__chunk = (#{si[:c]} *) ca_lazy_arena_acquire(__chunk_elems * sizeof(#{si[:c]}));"
3249
3333
  end
3334
+ io.puts " __ctx->chunk = __chunk;"
3250
3335
  io.puts " while ( __outer_off < __outer ) {"
3251
3336
  io.puts " ca_size_t __r = (__outer - __outer_off < __rows) ? (__outer - __outer_off) : __rows;"
3252
3337
  io.puts " ca_size_t __n = __r * __inner;"
@@ -3261,21 +3346,84 @@ module MkKernel
3261
3346
  io.puts " }"
3262
3347
  io.puts " __outer_off += __r;"
3263
3348
  io.puts " }"
3264
- io.puts " ca_lazy_arena_release(__chunk);"
3265
- io.puts " ca_lazy_arena_exit();"
3266
3349
  if all_nan_stream
3267
3350
  io.puts " if ( ! __anf_seen && ca->elements > 0 ) #{acc_var} = (#{si[:c]}) NAN;"
3268
3351
  end
3269
- if has_mp
3270
- # Streaming path has no mask source, so masked_cnt is 0; min_count
3271
- # / strict / all_masked triggers all evaluate to false except
3272
- # min_count when explicit > 0 is set on an all-clean source (= no
3273
- # trigger). Output is the finish expression directly.
3274
- io.puts " return #{ruby_wrap}((#{oi[:c]}) (#{finish_expr}));"
3275
- else
3276
- io.puts " return #{ruby_wrap}((#{oi[:c]}) (#{finish_expr}));"
3277
- end
3352
+ # Streaming has no mask source, so masked_cnt is 0 and none of the
3353
+ # mask-policy triggers fire: the answer is the finish expression.
3354
+ io.puts " return #{ruby_wrap}((#{oi[:c]}) (#{finish_expr}));"
3355
+ io.puts "}"
3356
+ end
3357
+
3358
+ # The streaming branch at the top of name_ki_native_<src>: a flat
3359
+ # reduction of an unmasked lazy view pulls the source in fixed-size
3360
+ # chunks via ca_xfer_stride instead of materialising it whole. Falls
3361
+ # through to the existing path when the conditions are not met.
3362
+ #
3363
+ # Strategy: chunk along the outermost axis. inner = prod(dim[1..]);
3364
+ # rows_per_chunk = max(1, target_elems / inner). 1-D is the natural
3365
+ # sub-case (inner == 1, rows = target_elems).
3366
+ #
3367
+ # Memory peak: O(max(target_elems, inner) * sizeof T) instead of O(N).
3368
+ # For N-D, this drops the outermost dim from the materialised buffer,
3369
+ # which is typically the dominant factor (e.g. 1000x10000 f64 reduction:
3370
+ # 80MB -> 80KB). Wall-clock perf at large N matches or beats
3371
+ # SRC_ATTACH because each chunk fits in L1d (or L2 for wide last dims).
3372
+ #
3373
+ # Restrictions handled by short-circuit checks (= falls through to
3374
+ # SRC_ATTACH path):
3375
+ # - axis kwarg (= partial reduction): naxes != ca->ndim
3376
+ # (structural blocker: CABinOp/CABinCmp xfer_stride rebuilds
3377
+ # right_strides from counts, so non-rectangular slab pulls
3378
+ # produce wrong values for the right operand.)
3379
+ # - non-lazy source: !ca_is_lazy_view(ca)
3380
+ # - mask present: ca_has_mask(ca)
3381
+ # - nothing to fold, or fewer cells than min_count asks for: the walk
3382
+ # has no mask, so it answers the finish expression, which is right
3383
+ # only when the mask policy cannot fire. An empty source and a
3384
+ # min_count above the cell count are the two ways it can without a
3385
+ # mask, and the full path answers them.
3386
+ def self.emit_reduce_streaming_call(io, k, src)
3387
+ fn = "#{k[:name]}_ki_stream_#{src}"
3388
+ io.puts " if ( naxes == ca->ndim && ca->ndim >= 1 && !keep_axis &&"
3389
+ io.puts " ca->elements > 0 &&"
3390
+ io.puts " ! ca_too_few_present(ca->elements, min_count, 0) &&" if k[:mask_policy] == :min_count
3391
+ io.puts " ca_is_lazy_view(ca) && ! ca_has_mask(ca) &&"
3392
+ io.puts " ca_fast_paths_enabled() ) {"
3393
+ io.puts " #{fn}_t __ctx;"
3394
+ io.puts " __ctx.chunk = NULL;"
3395
+ io.puts " __ctx.ca = ca;"
3396
+ io.puts " __ctx.value_arg = value_arg;" if k[:value_arg]
3397
+ io.puts " __ctx.min_count = min_count;" if k[:mask_policy] == :min_count
3398
+ io.puts " return ca_lazy_arena_protect(#{fn}, ca_lazy_arena_release_held,"
3399
+ io.puts " (VALUE) &__ctx);"
3400
+ io.puts " }"
3401
+ end
3402
+
3403
+ # Where the source is a lazy expression the kernel is about to compute
3404
+ # whole, a registered expression evaluator may compute it instead; the
3405
+ # dispatcher then goes on with that array. `unless_streaming` is the C
3406
+ # condition under which the reduction streams the expression in chunks
3407
+ # rather than making it whole -- there it is left to stream, since asking
3408
+ # would make it whole and take that much more memory.
3409
+ def self.emit_lazy_evaluated(io, unless_streaming: nil)
3410
+ cond = "ca_is_lazy_view(src)"
3411
+ cond += " && ! ( #{unless_streaming} )" if unless_streaming
3412
+ io.puts " if ( #{cond} ) {"
3413
+ io.puts " volatile VALUE evaluated = ca_lazy_evaluated(self);"
3414
+ io.puts " if ( ! NIL_P(evaluated) ) {"
3415
+ io.puts " self = evaluated;"
3416
+ io.puts " GetCArray(self, src);"
3278
3417
  io.puts " }"
3418
+ io.puts " }"
3419
+ end
3420
+
3421
+ # Whether a reduction's native walk has the streaming branch
3422
+ # (emit_reduce_streaming_call): the conditions emit_reduce_native checks
3423
+ # before it emits one.
3424
+ def self.reduce_streams?(k)
3425
+ k[:outputs] != 2 && k[:algorithm] != :two_pass_centred &&
3426
+ !k[:array_arg] && !k[:reduce].to_s.include?("idx")
3279
3427
  end
3280
3428
 
3281
3429
  def self.emit_reduce_dispatch(io, k)
@@ -3303,12 +3451,13 @@ module MkKernel
3303
3451
  io.puts " GetCArray(self, src);"
3304
3452
 
3305
3453
  if k[:object_escape]
3306
- # CA_OBJECT escape: forward argc/argv (weights + trailing options hash,
3307
- # untouched) to the Ruby composition helper. Intercept before any arg
3308
- # parsing so the helper owns the full original signature.
3309
- io.puts " if ( src->data_type == CA_OBJECT ) {"
3310
- io.puts " return rb_funcallv(self, rb_intern(\"#{k[:object_escape]}\"), argc, argv);"
3311
- io.puts " }"
3454
+ # CA_OBJECT escape: the Ruby composition helper gets the original
3455
+ # argc/argv (weights + trailing options hash, untouched). The escape
3456
+ # itself comes after the options and axes are checked below, so a
3457
+ # wrong keyword or axis is reported here, in this method's name, as
3458
+ # on the numeric path.
3459
+ io.puts " int escape_argc = argc;"
3460
+ io.puts " VALUE *escape_argv = argv;"
3312
3461
  end
3313
3462
 
3314
3463
  if k[:face_gate]
@@ -3342,7 +3491,18 @@ module MkKernel
3342
3491
  io.puts " int keep_axis = RTEST(rkeep_axis);"
3343
3492
  io.puts " int8_t slab_axes[CA_RANK_MAX];"
3344
3493
  io.puts " int8_t naxes = rb_ca_parse_reduce_axes_kw(raxis, src, slab_axes);"
3494
+ if k[:object_escape]
3495
+ io.puts " if ( src->data_type == CA_OBJECT ) {"
3496
+ if min_count
3497
+ io.puts " ca_min_count_arg(rmin_count, NULL);"
3498
+ end
3499
+ io.puts " return rb_funcallv(self, rb_intern(\"#{k[:object_escape]}\"), escape_argc, escape_argv);"
3500
+ io.puts " }"
3501
+ end
3502
+ emit_lazy_evaluated(io, unless_streaming:
3503
+ reduce_streams?(k) ? "naxes == src->ndim && src->ndim >= 1 && ! keep_axis && ! ca_has_mask(src)" : nil)
3345
3504
 
3505
+ weights_io = nil
3346
3506
  if has_aarg
3347
3507
  # Weight storage type: :promote materializes weights at the f64
3348
3508
  # computation type (so a float weight against an int source is not
@@ -3357,7 +3517,7 @@ module MkKernel
3357
3517
  # at dispatch (= simple, uniform kernel path). rev5 strict acceptance
3358
3518
  # mirrors PROPOSAL_LINEAR_INTERP_PER_FIBER_MATCHED rev5 in spirit.
3359
3519
  io.puts " if ( argc < 1 ) {"
3360
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: missing weights argument (1+ required, got 0)");]
3520
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: missing weights argument (1+ required, got 0)", ca_calling_method_name());]
3361
3521
  io.puts " }"
3362
3522
  io.puts " volatile VALUE rweights = argv[0];"
3363
3523
  io.puts " argc--; argv++;"
@@ -3370,98 +3530,106 @@ module MkKernel
3370
3530
  io.puts " rweights = rb_funcall(rweights, rb_intern(\"[]\"), 1, INT2NUM(0));"
3371
3531
  io.puts " }"
3372
3532
  io.puts " }"
3373
- io.puts " volatile VALUE vcw;"
3374
- io.puts " CArray *cw;"
3375
- io.puts " if ( ! rb_obj_is_carray(rweights) ) {"
3376
- io.puts " /* W-A1: Ruby Numeric scalar -> full-shape constant weight CArray */"
3377
- io.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3378
- io.puts " GetCArray(vcw, cw);"
3379
- io.puts " ca_attach(cw);"
3380
- io.puts " rb_ca_obj2ptr(vcw, rweights, cw->ptr); /* fill first cell */"
3381
- io.puts " /* broadcast first cell to all elements (= constant fill) */"
3382
- io.puts " {"
3383
- io.puts " char *p0 = cw->ptr;"
3384
- io.puts " for ( ca_size_t i = 1; i < cw->elements; i++ ) {"
3385
- io.puts " memcpy(cw->ptr + i * cw->bytes, p0, cw->bytes);"
3386
- io.puts " }"
3387
- io.puts " }"
3388
- io.puts " }"
3389
- io.puts " else {"
3390
- io.puts " /* CArray val: coerce to the weight storage type (f64 for :promote, so a"
3391
- io.puts " float weight against an int source is not truncated) */"
3392
- io.puts " vcw = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3393
- io.puts " CArray *cv;"
3394
- io.puts " GetCArray(vcw, cv);"
3395
- io.puts ""
3396
- io.puts " /* rev5 form detection: A3 (same ndim) -> A2 (1-D axis-broadcast) -> raise */"
3397
- io.puts " if ( cv->ndim == src->ndim ) {"
3398
- io.puts " /* W-A3 commit-or-raise: shape strict match */"
3399
- io.puts " for ( int8_t i = 0; i < src->ndim; i++ ) {"
3400
- io.puts " if ( cv->dim[i] != src->dim[i] ) {"
3401
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: w shape mismatch (W-A3 candidate, dim[%d]=%ld != self.dim[%d]=%ld; expected scalar / [M=self.dim[axes[0]]] / self.shape)", (int)i, (long)cv->dim[i], (int)i, (long)src->dim[i]);]
3402
- io.puts " }"
3403
- io.puts " }"
3404
- io.puts " /* W-A3 path: use cv directly (= existing) */"
3405
- io.puts " cw = cv;"
3406
- io.puts " ca_attach(cw);"
3407
- io.puts " }"
3408
- io.puts " else if ( naxes == 1 && cv->ndim == 1 && cv->dim[0] == src->dim[slab_axes[0]] ) {"
3409
- io.puts " /* W-A2: 1-D axis-broadcast -> materialize to full shape */"
3410
- io.puts " ca_attach(cv);"
3411
- io.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3412
- io.puts " GetCArray(vcw, cw);"
3413
- io.puts " ca_attach(cw);"
3414
- io.puts " /* broadcast cv (1-D, length src->dim[slab_axes[0]]) along reduce axis */"
3415
- io.puts " {"
3416
- io.puts " int8_t baxis = slab_axes[0];"
3417
- io.puts " ca_size_t inner_stride = 1;"
3418
- io.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3419
- io.puts " ca_size_t axis_n = src->dim[baxis];"
3420
- io.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3421
- io.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3422
- io.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3423
- io.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3424
- io.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3425
- io.puts " memcpy(cw->ptr + flat * cw->bytes, cv->ptr + a * cv->bytes, cw->bytes);"
3426
- io.puts " }"
3427
- io.puts " }"
3428
- io.puts " }"
3429
- io.puts " }"
3430
- io.puts " /* Propagate cv mask via W-A3 overlay path below (= mask materialize) */"
3431
- io.puts " if ( ca_has_mask(cv) ) {"
3432
- io.puts " /* Allocate cw mask + broadcast cv mask along axis */"
3433
- io.puts " ca_create_mask(cw);"
3434
- io.puts " boolean8_t *cw_m = (boolean8_t *)cw->mask->ptr;"
3435
- io.puts " boolean8_t *cv_m = (boolean8_t *)cv->mask->ptr;"
3436
- io.puts " int8_t baxis = slab_axes[0];"
3437
- io.puts " ca_size_t inner_stride = 1;"
3438
- io.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3439
- io.puts " ca_size_t axis_n = src->dim[baxis];"
3440
- io.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3441
- io.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3442
- io.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3443
- io.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3444
- io.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3445
- io.puts " cw_m[flat] = cv_m[a];"
3446
- io.puts " }"
3447
- io.puts " }"
3448
- io.puts " }"
3449
- io.puts " }"
3450
- io.puts " ca_detach(cv);"
3451
- io.puts " }"
3452
- io.puts " else {"
3453
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: w shape not accepted (val.ndim=%d, val.dim[0]=%ld; expected scalar / [M=%ld] / self.shape)", (int)cv->ndim, (long)(cv->ndim >= 1 ? cv->dim[0] : 0), (long)(naxes == 1 ? src->dim[slab_axes[0]] : -1));]
3454
- io.puts " }"
3455
- io.puts " }"
3533
+ # The weights are attached from here to the end of the call, so they
3534
+ # are made only once every check that can raise has passed; see the
3535
+ # data type check below.
3536
+ wio = StringIO.new
3537
+ wio.puts " volatile VALUE vcw;"
3538
+ wio.puts " CArray *cw;"
3539
+ wio.puts " if ( ! rb_obj_is_carray(rweights) ) {"
3540
+ wio.puts " /* W-A1: Ruby Numeric scalar -> full-shape constant weight CArray */"
3541
+ wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3542
+ wio.puts " GetCArray(vcw, cw); /* a new entity: its ptr needs no attach */"
3543
+ wio.puts " rb_ca_obj2ptr(vcw, rweights, cw->ptr); /* fill first cell */"
3544
+ wio.puts " /* broadcast first cell to all elements (= constant fill) */"
3545
+ wio.puts " {"
3546
+ wio.puts " char *p0 = cw->ptr;"
3547
+ wio.puts " for ( ca_size_t i = 1; i < cw->elements; i++ ) {"
3548
+ wio.puts " memcpy(cw->ptr + i * cw->bytes, p0, cw->bytes);"
3549
+ wio.puts " }"
3550
+ wio.puts " }"
3551
+ wio.puts " }"
3552
+ wio.puts " else {"
3553
+ wio.puts " /* CArray val: coerce to the weight storage type (f64 for :promote, so a"
3554
+ wio.puts " float weight against an int source is not truncated) */"
3555
+ wio.puts " /* vcv holds the view while cv is read: W-A2 below moves vcw on to"
3556
+ wio.puts " the materialised copy before it is done with cv. */"
3557
+ wio.puts " volatile VALUE vcv = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3558
+ wio.puts " CArray *cv;"
3559
+ wio.puts " GetCArray(vcv, cv);"
3560
+ wio.puts " vcw = vcv;"
3561
+ wio.puts ""
3562
+ wio.puts " /* rev5 form detection: A3 (same ndim) -> A2 (1-D axis-broadcast) -> raise */"
3563
+ wio.puts " if ( cv->ndim == src->ndim ) {"
3564
+ wio.puts " /* W-A3 commit-or-raise: shape strict match */"
3565
+ wio.puts " for ( int8_t i = 0; i < src->ndim; i++ ) {"
3566
+ wio.puts " if ( cv->dim[i] != src->dim[i] ) {"
3567
+ wio.puts %Q[ rb_raise(rb_eArgError, "%s: weights shape mismatch (dim[%d]=%ld != self.dim[%d]=%ld; expected a scalar, a 1-D array along the reduced axis, or self.shape)", ca_calling_method_name(), (int)i, (long)cv->dim[i], (int)i, (long)src->dim[i]);]
3568
+ wio.puts " }"
3569
+ wio.puts " }"
3570
+ wio.puts " /* W-A3 path: use cv directly (= existing) */"
3571
+ wio.puts " cw = cv;"
3572
+ wio.puts " }"
3573
+ wio.puts " else if ( naxes == 1 && cv->ndim == 1 && cv->dim[0] == src->dim[slab_axes[0]] ) {"
3574
+ wio.puts " /* W-A2: 1-D axis-broadcast -> materialize to full shape */"
3575
+ wio.puts " ca_attach(cv); /* window: nothing raises inside */"
3576
+ wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3577
+ wio.puts " GetCArray(vcw, cw);"
3578
+ wio.puts " /* broadcast cv (1-D, length src->dim[slab_axes[0]]) along reduce axis */"
3579
+ wio.puts " {"
3580
+ wio.puts " int8_t baxis = slab_axes[0];"
3581
+ wio.puts " ca_size_t inner_stride = 1;"
3582
+ wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3583
+ wio.puts " ca_size_t axis_n = src->dim[baxis];"
3584
+ wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3585
+ wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3586
+ wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3587
+ wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3588
+ wio.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3589
+ wio.puts " memcpy(cw->ptr + flat * cw->bytes, cv->ptr + a * cv->bytes, cw->bytes);"
3590
+ wio.puts " }"
3591
+ wio.puts " }"
3592
+ wio.puts " }"
3593
+ wio.puts " }"
3594
+ wio.puts " /* Propagate cv mask via W-A3 overlay path below (= mask materialize) */"
3595
+ wio.puts " if ( ca_has_mask(cv) ) {"
3596
+ wio.puts " /* Allocate cw mask + broadcast cv mask along axis */"
3597
+ wio.puts " ca_create_mask(cw);"
3598
+ wio.puts " boolean8_t *cw_m = (boolean8_t *)cw->mask->ptr;"
3599
+ wio.puts " boolean8_t *cv_m = (boolean8_t *)cv->mask->ptr;"
3600
+ wio.puts " int8_t baxis = slab_axes[0];"
3601
+ wio.puts " ca_size_t inner_stride = 1;"
3602
+ wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3603
+ wio.puts " ca_size_t axis_n = src->dim[baxis];"
3604
+ wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3605
+ wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3606
+ wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3607
+ wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3608
+ wio.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3609
+ wio.puts " cw_m[flat] = cv_m[a];"
3610
+ wio.puts " }"
3611
+ wio.puts " }"
3612
+ wio.puts " }"
3613
+ wio.puts " }"
3614
+ wio.puts " ca_detach(cv);"
3615
+ wio.puts " }"
3616
+ wio.puts " else {"
3617
+ wio.puts %Q[ rb_raise(rb_eArgError, "%s: w shape not accepted (val.ndim=%d, val.dim[0]=%ld; expected scalar / [M=%ld] / self.shape)", ca_calling_method_name(), (int)cv->ndim, (long)(cv->ndim >= 1 ? cv->dim[0] : 0), (long)(naxes == 1 ? src->dim[slab_axes[0]] : -1));]
3618
+ wio.puts " }"
3619
+ wio.puts " }"
3456
3620
  # Mask overlay (= legacy W-A3 path; if the materialized cw carries a mask, overlay it)
3457
- io.puts " volatile VALUE __vsrc_keep = self; /* GC guard */ (void) __vsrc_keep;"
3458
- io.puts " if ( ca_has_mask(cw) ) {"
3459
- io.puts " CArray *src_copy = ca_copy(src);"
3460
- io.puts " VALUE vsrc_copy = ca_wrap_struct(src_copy);"
3461
- io.puts " ca_copy_mask_overlay(src_copy, src_copy->elements, 1, cw);"
3462
- io.puts " src = src_copy;"
3463
- io.puts " self = vsrc_copy;"
3464
- io.puts " }"
3621
+ wio.puts " volatile VALUE __vsrc_keep = self; /* GC guard */ (void) __vsrc_keep;"
3622
+ wio.puts " if ( ca_has_mask(cw) ) {"
3623
+ wio.puts " CArray *src_copy = ca_copy(src);"
3624
+ wio.puts " VALUE vsrc_copy = ca_wrap_struct(src_copy);"
3625
+ wio.puts " ca_copy_mask_overlay(src_copy, src_copy->elements, 1, cw);"
3626
+ wio.puts " src = src_copy;"
3627
+ wio.puts " self = vsrc_copy;"
3628
+ wio.puts " }"
3629
+ # Attached last, after the overlay's copy of the source (which can
3630
+ # raise), and detached right after the switch.
3631
+ wio.puts " ca_attach(cw); /* window: nothing raises inside; the kernels are numeric */"
3632
+ weights_io = wio
3465
3633
  end
3466
3634
 
3467
3635
  if has_varg
@@ -3469,7 +3637,7 @@ module MkKernel
3469
3637
  # runtime value_arg (e.g. `a.count_equal(5, 0)` -- 5 is value_arg,
3470
3638
  # 0 is axis). Per-src cast happens inside the switch case below.
3471
3639
  io.puts " if ( argc < 1 ) {"
3472
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: missing value argument (1+ required, got 0)");]
3640
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: missing value argument (1+ required, got 0)", ca_calling_method_name());]
3473
3641
  io.puts " }"
3474
3642
  io.puts " volatile VALUE rval = argv[0];"
3475
3643
  io.puts " argc--; argv++;"
@@ -3477,16 +3645,16 @@ module MkKernel
3477
3645
 
3478
3646
  if min_count
3479
3647
  # rmin_count / rfval were scanned up front (PoC scan-merge); here we
3480
- # only interpret them. Sentinel min_count = -1 means "no opt given"
3481
- # -> :all_masked semantics.
3482
- io.puts " ca_size_t min_count = -1;"
3483
- io.puts " if ( ! NIL_P(rmin_count) ) {"
3484
- io.puts " ca_size_t mc_user = NUM2SIZE(rmin_count);"
3485
- io.puts " if ( mc_user < 0 ) {"
3486
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: :min_count must be non-negative, got %lld", (long long) mc_user);]
3487
- io.puts " }"
3488
- io.puts " /* :min_count = 0 collapses to legacy default (any 1 valid). */"
3489
- io.puts " if ( mc_user > 0 ) min_count = mc_user;"
3648
+ # only interpret them; min_count is -1 when not given.
3649
+ io.puts " ca_size_t min_count = ca_min_count_arg(rmin_count, NULL);"
3650
+ # fill_value: UNDEF leaves the result undefined, as no fill_value does.
3651
+ # A full reduction with a fill keeps its axes so the fill goes into a
3652
+ # typed cell, as it does per axis; ca_reduce_fill takes the cell out.
3653
+ io.puts " if ( rfval == CA_UNDEF ) rfval = Qnil;"
3654
+ io.puts " int fill_whole = 0;"
3655
+ io.puts " if ( ! NIL_P(rfval) && naxes == src->ndim && ! keep_axis ) {"
3656
+ io.puts " keep_axis = 1;"
3657
+ io.puts " fill_whole = 1;"
3490
3658
  io.puts " }"
3491
3659
  io.puts " VALUE result;"
3492
3660
  elsif has_aarg
@@ -3502,9 +3670,23 @@ module MkKernel
3502
3670
  # axis is allowed. Variadic `a.sum(0, 1)` raises here with a migration
3503
3671
  # hint pointing at the kwarg form.
3504
3672
  io.puts " if ( argc > 0 ) {"
3505
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: positional axis arguments are no longer accepted (got %d); use axis: kwarg, e.g. a.#{name}(axis: 0) or a.#{name}(axis: [0, 1])", argc);]
3673
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: positional axis arguments are no longer accepted (got %d); use the axis: keyword, e.g. a.%s(axis: 0) or a.%s(axis: [0, 1])", ca_calling_method_name(), argc, ca_calling_method_name(), ca_calling_method_name());]
3506
3674
  io.puts " }"
3507
3675
 
3676
+ if weights_io
3677
+ # Refuse an unsupported source before the weights are attached: the
3678
+ # switch below would raise with them held.
3679
+ if k[:fallback] == :raise
3680
+ io.puts " switch ( src->data_type ) {"
3681
+ io.puts " " + k[:source].map { |s| "case #{DTYPES[s][:ca]}:" }.join(" ") + " break;"
3682
+ io.puts " default:"
3683
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3684
+ io.puts %Q[ rb_raise(rb_eCADataTypeError, "%s: source data_type :%s not supported (expected one of: #{MkKernel.type_names(k[:source])})", ca_calling_method_name(), ca_type_name[src->data_type]);]
3685
+ io.puts " }"
3686
+ end
3687
+ io.print weights_io.string
3688
+ end
3689
+
3508
3690
  io.puts " switch ( src->data_type ) {"
3509
3691
  k[:source].each do |s|
3510
3692
  si = DTYPES[s]
@@ -3515,8 +3697,14 @@ module MkKernel
3515
3697
  # A fixlen query is a runtime-width byte blob with no
3516
3698
  # scalar cast; pack it the way the search family does,
3517
3699
  # which NUL-pads a short String to the cell width.
3518
- " char *value_arg = ALLOCA_N(char, src->bytes);\n" \
3700
+ # ALLOCV, not the stack: a cell can be wide. The holder
3701
+ # keeps the buffer until the GC takes it.
3702
+ " volatile VALUE value_arg_holder = 0;\n" \
3703
+ " char *value_arg = ALLOCV_N(char, value_arg_holder, src->bytes);\n" \
3519
3704
  " rb_ca_obj2ptr(self, rval, value_arg);\n"
3705
+ elsif INT_DTYPES.include?(s)
3706
+ " ca_check_integer_fits(rval, #{si[:ca]});\n" \
3707
+ " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n"
3520
3708
  else
3521
3709
  " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n"
3522
3710
  end
@@ -3525,13 +3713,14 @@ module MkKernel
3525
3713
  io.print varg_decl unless varg_decl.empty?
3526
3714
  if use_result_var
3527
3715
  io.puts " result = #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis#{extra_call});"
3716
+ io.puts " RB_GC_GUARD(self);"
3528
3717
  io.puts " break;"
3529
3718
  else
3530
- io.puts " return #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis#{extra_call});"
3719
+ io.puts " CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis#{extra_call}));"
3531
3720
  end
3532
3721
  io.puts " }"
3533
3722
  else
3534
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis);"
3723
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis));"
3535
3724
  end
3536
3725
  end
3537
3726
  case k[:fallback]
@@ -3544,7 +3733,8 @@ module MkKernel
3544
3733
  io.puts " switch ( src->data_type ) {"
3545
3734
  io.puts " case CA_BOOLEAN: break; /* wrap: count-of-trues semantic */"
3546
3735
  io.puts " default:"
3547
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")}, or boolean)", ca_type_name[src->data_type]);]
3736
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3737
+ io.puts %Q[ rb_raise(rb_eCADataTypeError, "%s: source data_type :%s not supported (expected one of: #{MkKernel.type_names(k[:source])}, or boolean)", ca_calling_method_name(), ca_type_name[src->data_type]);]
3548
3738
  io.puts " }"
3549
3739
  io.puts " VALUE vsrc = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));"
3550
3740
  io.puts " CArray *casted;"
@@ -3556,42 +3746,44 @@ module MkKernel
3556
3746
  end
3557
3747
  if use_result_var
3558
3748
  io.puts " result = #{name}_ki_native_f64(vsrc, casted, slab_axes, naxes, keep_axis#{extra_call});"
3749
+ io.puts " RB_GC_GUARD(vsrc);"
3559
3750
  io.puts " break;"
3560
3751
  else
3561
- io.puts " return #{name}_ki_native_f64(vsrc, casted, slab_axes, naxes, keep_axis#{extra_call});"
3752
+ io.puts " CA_RETURN_KEEPING(vsrc, #{name}_ki_native_f64(vsrc, casted, slab_axes, naxes, keep_axis#{extra_call}));"
3562
3753
  end
3563
3754
  io.puts " }"
3564
3755
  when :raise
3565
3756
  io.puts " default:"
3566
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
3757
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3758
+ io.puts %Q[ rb_raise(rb_eCADataTypeError, "%s: source data_type :%s not supported (expected one of: #{MkKernel.type_names(k[:source])})", ca_calling_method_name(), ca_type_name[src->data_type]);]
3567
3759
  end
3568
3760
  io.puts " }"
3569
3761
 
3570
- if min_count
3571
- # Apply fill_value substitution: CA_UNDEF (full reduction all-masked)
3572
- # or per-axis CArray with mask bits.
3573
- io.puts " if ( ! NIL_P(rfval) ) {"
3574
- io.puts " if ( result == CA_UNDEF ) {"
3575
- io.puts " result = rfval;"
3576
- io.puts " } else if ( TYPE(result) != T_FLOAT && rb_obj_is_kind_of(result, rb_cCArray) ) {"
3577
- io.puts " CArray *cr;"
3578
- io.puts " GetCArray(result, cr);"
3579
- io.puts " if ( ca_has_mask(cr) ) {"
3580
- io.puts " result = rb_ca_mask_fill_copy(result, rfval);"
3581
- io.puts " }"
3582
- io.puts " }"
3583
- io.puts " }"
3584
- end
3585
-
3586
- # array_arg cleanup: detach weights (paired with ca_attach above).
3762
+ # array_arg cleanup: detach weights (paired with ca_attach above),
3763
+ # ahead of the fill_value substitution, which can raise.
3587
3764
  if has_aarg
3588
3765
  io.puts " ca_detach(cw);"
3589
3766
  end
3590
3767
 
3591
- if k[:face_gate] == :relift
3768
+ relift = (k[:face_gate] == :relift)
3769
+ if min_count
3770
+ # Apply fill_value through the one rule the per-axis result uses. A
3771
+ # Face's fill is a value of the Face, so it goes in after the re-lift
3772
+ # below, as an assignment to the Face would put it.
3773
+ io.puts relift ? " if ( ! NIL_P(rfval) && _reduce_face == Qnil ) {" : " if ( ! NIL_P(rfval) ) {"
3774
+ io.puts " result = ca_reduce_fill(result, rfval, fill_whole);"
3775
+ io.puts " }"
3776
+ end
3777
+
3778
+ if relift
3592
3779
  # Descended through emit_reduce_face_gate: re-lift the storage result
3593
3780
  # back into the Face before returning.
3594
3781
  emit_reduce_face_relift(io)
3782
+ if min_count
3783
+ io.puts " if ( ! NIL_P(rfval) && _reduce_face != Qnil ) {"
3784
+ io.puts " result = ca_reduce_fill(result, rfval, fill_whole);"
3785
+ io.puts " }"
3786
+ end
3595
3787
  end
3596
3788
 
3597
3789
  if use_result_var
@@ -3602,117 +3794,13 @@ module MkKernel
3602
3794
  io.puts "}"
3603
3795
  end
3604
3796
 
3605
- # ---- map emitter ------------------------------------------------------
3606
-
3607
- def self.emit_map(io, k)
3608
- io.puts
3609
- io.puts "/* ===== #{k[:name]}_ki ============================================ */"
3610
- k[:source].each do |src|
3611
- emit_map_native(io, k, src)
3612
- end
3613
- emit_map_dispatch(io, k)
3614
- end
3615
-
3616
- def self.emit_map_native(io, k, src)
3617
- si = DTYPES[src]
3618
- oi = output_info(k, src)
3619
- name = k[:name]
3620
- expr = resolve_expr(k[:expr], oi, src)
3621
-
3622
- io.puts
3623
- io.puts <<~C
3624
- static VALUE
3625
- #{name}_ki_native_#{src} (VALUE self, CArray *ca)
3626
- {
3627
- VALUE vout = rb_ca_template_with_type(self, INT2NUM(#{oi[:ca]}),
3628
- INT2NUM(sizeof(#{oi[:c]})));
3629
- CArray *co;
3630
- GetCArray(vout, co);
3631
-
3632
- /* All-axes slab = whole array as one K-D walk via CA_SLAB_AXES.
3633
- CA_SLAB_WHOLE uses the flat next_slab path, which doesn't
3634
- populate slab_dims; CA_SLAB_AXES with naxes == ndim is the
3635
- K-D-walkable form. */
3636
- int8_t slab_axes[CA_RANK_MAX];
3637
- int8_t naxes = ca->ndim;
3638
- for ( int8_t k = 0; k < naxes; k++ ) slab_axes[k] = k;
3639
-
3640
- ca_iter_state st_in, st_out;
3641
- int rc;
3642
- rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,
3643
- slab_axes, naxes, 0);
3644
- if ( rc != CA_ITER_OK ) {
3645
- rb_raise(rb_eRuntimeError,
3646
- "#{name}_ki: input init failed rc=%d", rc);
3647
- }
3648
- rc = ca_iter_state_init_l2(&st_out, co, CA_SLAB_AXES,
3649
- slab_axes, naxes, CA_KERNEL_WRITE);
3650
- if ( rc != CA_ITER_OK ) {
3651
- ca_iter_state_finish(&st_in);
3652
- rb_raise(rb_eRuntimeError,
3653
- "#{name}_ki: output init failed rc=%d", rc);
3654
- }
3655
-
3656
- char *pi, *po;
3657
- boolean8_t *mi, *mo;
3658
- while ( ca_iter_state_next_slab_axes(&st_in, &pi, &mi) &&
3659
- ca_iter_state_next_slab_axes(&st_out, &po, &mo) ) {
3660
- CA_SLAB_MAP_T(#{si[:c]}, #{oi[:c]}, st_in, pi, st_out, po, #{expr});
3661
- ca_iter_state_sync_slab(&st_out);
3662
- }
3663
- ca_iter_state_finish(&st_in);
3664
- ca_iter_state_finish(&st_out);
3665
- return vout;
3666
- }
3667
- C
3668
- end
3669
-
3670
- def self.emit_map_dispatch(io, k)
3671
- name = k[:name]
3672
- io.puts
3673
- io.puts "static VALUE"
3674
- io.puts "rb_ca_#{name}_ki (VALUE self)"
3675
- io.puts "{"
3676
- io.puts " CArray *src;"
3677
- io.puts " GetCArray(self, src);"
3678
- io.puts " switch ( src->data_type ) {"
3679
- k[:source].each do |s|
3680
- si = DTYPES[s]
3681
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src);"
3682
- end
3683
- case k[:fallback]
3684
- when :wrap_to_f64
3685
- io.puts " default: {"
3686
- io.puts " /* Phase E: only wrap data_types that have a meaningful float64"
3687
- io.puts " representation. Complex / fixlen / object would lose"
3688
- io.puts " structure (e.g. complex -> float strips imaginary part)"
3689
- io.puts " so reject explicitly, matching legacy DataTypeError. */"
3690
- io.puts " switch ( src->data_type ) {"
3691
- io.puts " case CA_BOOLEAN: break; /* wrap: count-of-trues semantic */"
3692
- io.puts " default:"
3693
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")}, or boolean)", ca_type_name[src->data_type]);]
3694
- io.puts " }"
3695
- io.puts " VALUE vsrc = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));"
3696
- io.puts " CArray *casted;"
3697
- io.puts " GetCArray(vsrc, casted);"
3698
- io.puts " return #{name}_ki_native_f64(vsrc, casted);"
3699
- io.puts " }"
3700
- when :raise
3701
- io.puts " default:"
3702
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
3703
- end
3704
- io.puts " }"
3705
- io.puts " return Qnil; /* unreachable */"
3706
- io.puts "}"
3707
- end
3708
-
3709
3797
  # ---- scan emitter -----------------------------------------------------
3710
3798
 
3711
3799
  def self.emit_scan(io, k)
3712
3800
  io.puts
3713
3801
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
3714
3802
  k[:source].each do |src|
3715
- emit_scan_native(io, k, src)
3803
+ emit_native_for(io, k, src) { |o| emit_scan_native(o, k, src) }
3716
3804
  end
3717
3805
  emit_scan_dispatch(io, k)
3718
3806
  end
@@ -3731,14 +3819,16 @@ module MkKernel
3731
3819
 
3732
3820
  # empty: :undef -> allocate the output mask up front when the input
3733
3821
  # carries one (a fiber can only go UNDEF on its leading masked cells,
3734
- # which exist only when the input is masked). co_root / op_mask are the
3735
- # value / mask bases used to derive the per-fiber mask base parallel to
3736
- # the aliased output value slab.
3822
+ # which exist only when the input is masked). The output is a fresh
3823
+ # row-major entity, and the walk cuts its slabs from a buffer laid out
3824
+ # the same way (the output's memory, or the walk's scratch when it
3825
+ # delivers by transfer), so a slab's offset from st_out.alias_ptr is
3826
+ # its offset in the output, and op_mask + that offset / cell size is the
3827
+ # fiber's mask base.
3737
3828
  mask_setup =
3738
3829
  if gated
3739
3830
  "\n" \
3740
3831
  " boolean8_t *op_mask = NULL;\n" \
3741
- " char *co_root = (char *) co->ptr;\n" \
3742
3832
  " if ( ca_has_mask(ca) ) {\n" \
3743
3833
  " ca_create_mask(co);\n" \
3744
3834
  " op_mask = (boolean8_t *) co->mask->ptr;\n" \
@@ -3753,7 +3843,7 @@ module MkKernel
3753
3843
  " st_out, po, #{init_expr}, #{step_stmt});"
3754
3844
  elsif gated
3755
3845
  "boolean8_t *mo_fiber = op_mask\n" \
3756
- " ? op_mask + (ca_size_t)(po - co_root) / (ca_size_t) co->bytes\n" \
3846
+ " ? op_mask + (ca_size_t)(po - st_out.alias_ptr) / (ca_size_t) co->bytes\n" \
3757
3847
  " : NULL;\n" \
3758
3848
  " CA_SLAB_SCAN_T_GATED(#{si[:c]}, #{oi[:c]}, st_in, pi, mi,\n" \
3759
3849
  " st_out, po, mo_fiber, #{init_expr}, #{step_stmt});"
@@ -3783,14 +3873,14 @@ module MkKernel
3783
3873
  slab_axes, 1, 0);
3784
3874
  if ( rc != CA_ITER_OK ) {
3785
3875
  rb_raise(rb_eRuntimeError,
3786
- "#{name}_ki: input init failed rc=%d", rc);
3876
+ "%s: input init failed rc=%d", ca_calling_method_name(), rc);
3787
3877
  }
3788
- rc = ca_iter_state_init_l2(&st_out, co, CA_SLAB_AXES,
3878
+ rc = ca_iter_state_init_l2_paired(&st_out, &st_in, co, CA_SLAB_AXES,
3789
3879
  slab_axes, 1, CA_KERNEL_WRITE);
3790
3880
  if ( rc != CA_ITER_OK ) {
3791
3881
  ca_iter_state_finish(&st_in);
3792
3882
  rb_raise(rb_eRuntimeError,
3793
- "#{name}_ki: output init failed rc=%d", rc);
3883
+ "%s: output init failed rc=%d", ca_calling_method_name(), rc);
3794
3884
  }
3795
3885
  #{mask_setup}
3796
3886
  char *pi, *po;
@@ -3815,16 +3905,38 @@ module MkKernel
3815
3905
  # multi-axis scan is semantically ambiguous (flatten-and-cumsum vs
3816
3906
  # 2D prefix sum); users must chain explicitly. Arity flips from 1
3817
3907
  # (positional Integer axis) to -1 (kwarg dispatcher).
3818
- io.puts "static VALUE"
3819
- io.puts "rb_ca_#{name}_ki (int argc, VALUE *argv, VALUE self)"
3908
+ if k[:face_gate]
3909
+ # The scan proper runs on the storage; the bound entry below gates the
3910
+ # Face and re-lifts the result.
3911
+ io.puts "static VALUE rb_ca_#{name}_ki_storage (int argc, VALUE *argv, VALUE self);"
3912
+ io.puts
3913
+ io.puts "static VALUE"
3914
+ io.puts "rb_ca_#{name}_ki (int argc, VALUE *argv, VALUE self)"
3915
+ io.puts "{"
3916
+ io.puts " CArray *src;"
3917
+ io.puts " VALUE result;"
3918
+ io.puts " GetCArray(self, src);"
3919
+ emit_reduce_face_gate(io, name, true)
3920
+ io.puts " result = rb_ca_#{name}_ki_storage(argc, argv, self);"
3921
+ emit_reduce_face_relift(io)
3922
+ io.puts " return result;"
3923
+ io.puts "}"
3924
+ io.puts
3925
+ io.puts "static VALUE"
3926
+ io.puts "rb_ca_#{name}_ki_storage (int argc, VALUE *argv, VALUE self)"
3927
+ else
3928
+ io.puts "static VALUE"
3929
+ io.puts "rb_ca_#{name}_ki (int argc, VALUE *argv, VALUE self)"
3930
+ end
3820
3931
  io.puts "{"
3821
3932
  io.puts " CArray *src;"
3822
3933
  io.puts " GetCArray(self, src);"
3934
+ emit_lazy_evaluated(io)
3823
3935
  io.puts " volatile VALUE ropt = rb_pop_options(&argc, &argv);"
3824
3936
  io.puts " volatile VALUE raxis = Qnil;"
3825
3937
  io.puts " rb_scan_options(ropt, \"axis\", &raxis);"
3826
3938
  io.puts " if ( argc > 0 ) {"
3827
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: positional axis is no longer accepted (got %d); use axis: kwarg, e.g. a.#{name}(axis: 0)", argc);]
3939
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: positional axis is no longer accepted (got %d); use axis: kwarg, e.g. a.#{name}(axis: 0)", ca_calling_method_name(), argc);]
3828
3940
  io.puts " }"
3829
3941
  if k[:axis_default] == :flatten
3830
3942
  # Legacy compat: nil/omitted axis: flatten source and scan axis 0.
@@ -3836,7 +3948,7 @@ module MkKernel
3836
3948
  io.puts " switch ( fsrc->data_type ) {"
3837
3949
  k[:source].each do |s|
3838
3950
  si = DTYPES[s]
3839
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(vflat, fsrc, 0);"
3951
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(vflat, #{name}_ki_native_#{s}(vflat, fsrc, 0));"
3840
3952
  end
3841
3953
  case k[:fallback]
3842
3954
  when :wrap_to_f64
@@ -3844,34 +3956,31 @@ module MkKernel
3844
3956
  io.puts " VALUE vsrc = rb_ca_wrap_readonly(vflat, INT2NUM(CA_FLOAT64));"
3845
3957
  io.puts " CArray *casted;"
3846
3958
  io.puts " GetCArray(vsrc, casted);"
3847
- io.puts " return #{name}_ki_native_f64(vsrc, casted, 0);"
3959
+ io.puts " CA_RETURN_KEEPING(vsrc, #{name}_ki_native_f64(vsrc, casted, 0));"
3848
3960
  io.puts " }"
3849
3961
  when :raise
3850
3962
  io.puts " default:"
3851
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[fsrc->data_type]);]
3963
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3964
+ io.puts %Q[ rb_raise(rb_eCADataTypeError, "%s: source data_type :%s not supported (expected one of: #{MkKernel.type_names(k[:source])})", ca_calling_method_name(), ca_type_name[fsrc->data_type]);]
3965
+ when :mask_count
3966
+ io.puts " default:"
3967
+ io.puts " return rb_ca_scan_mask_count(self, Qnil);"
3852
3968
  end
3853
3969
  io.puts " }"
3854
3970
  io.puts " }"
3855
3971
  else
3856
3972
  io.puts " if ( NIL_P(raxis) ) {"
3857
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis: kwarg is required (single axis Integer; multi-axis scan is semantically ambiguous)");]
3973
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: axis: is required (a single Integer; a scan over several axes is ambiguous)", ca_calling_method_name());]
3858
3974
  io.puts " }"
3859
3975
  end
3860
3976
  io.puts " if ( TYPE(raxis) == T_ARRAY ) {"
3861
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis: must be Integer (got Array); multi-axis scan is semantically ambiguous, chain explicitly: a.#{name}(axis: 0).#{name}(axis: 1)");]
3862
- io.puts " }"
3863
- io.puts " if ( ! rb_obj_is_kind_of(raxis, rb_cInteger) ) {"
3864
- io.puts %Q[ rb_raise(rb_eTypeError, "#{name}_ki: axis: must be Integer (got %"PRIsVALUE")", rb_obj_class(raxis));]
3865
- io.puts " }"
3866
- io.puts " int axis = NUM2INT(raxis);"
3867
- io.puts " if ( axis < 0 ) axis += src->ndim;"
3868
- io.puts " if ( axis < 0 || axis >= src->ndim ) {"
3869
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(raxis), src->ndim);]
3977
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: axis must be a single Integer (got Array); a scan over several axes is ambiguous, chain explicitly: a.#{name}(axis: 0).#{name}(axis: 1)", ca_calling_method_name());]
3870
3978
  io.puts " }"
3979
+ io.puts " int axis = ca_normalize_axis_arg(raxis, src->ndim, NULL);"
3871
3980
  io.puts " switch ( src->data_type ) {"
3872
3981
  k[:source].each do |s|
3873
3982
  si = DTYPES[s]
3874
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, axis);"
3983
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, axis));"
3875
3984
  end
3876
3985
  case k[:fallback]
3877
3986
  when :wrap_to_f64
@@ -3879,11 +3988,15 @@ module MkKernel
3879
3988
  io.puts " VALUE vsrc = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));"
3880
3989
  io.puts " CArray *casted;"
3881
3990
  io.puts " GetCArray(vsrc, casted);"
3882
- io.puts " return #{name}_ki_native_f64(vsrc, casted, axis);"
3991
+ io.puts " CA_RETURN_KEEPING(vsrc, #{name}_ki_native_f64(vsrc, casted, axis));"
3883
3992
  io.puts " }"
3884
3993
  when :raise
3885
3994
  io.puts " default:"
3886
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
3995
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3996
+ io.puts %Q[ rb_raise(rb_eCADataTypeError, "%s: source data_type :%s not supported (expected one of: #{MkKernel.type_names(k[:source])})", ca_calling_method_name(), ca_type_name[src->data_type]);]
3997
+ when :mask_count
3998
+ io.puts " default:"
3999
+ io.puts " return rb_ca_scan_mask_count(self, INT2NUM(axis));"
3887
4000
  end
3888
4001
  io.puts " }"
3889
4002
  io.puts " return Qnil; /* unreachable */"
@@ -3898,7 +4011,7 @@ module MkKernel
3898
4011
  k[:source].each do |src|
3899
4012
  emit_sort_cmp(io, k, src)
3900
4013
  emit_sort_quickselect(io, k, src) if k[:algorithm] == :partition
3901
- emit_sort_native(io, k, src)
4014
+ emit_native_for(io, k, src) { |o| emit_sort_native(o, k, src) }
3902
4015
  end
3903
4016
  emit_sort_dispatch(io, k)
3904
4017
  end
@@ -4025,28 +4138,15 @@ module MkKernel
4025
4138
  io.puts " const #{name}_pair_#{src} *pa = (const #{name}_pair_#{src} *) a;"
4026
4139
  io.puts " const #{name}_pair_#{src} *pb = (const #{name}_pair_#{src} *) b;"
4027
4140
  if is_object
4028
- # PROPOSAL_MKKERNEL_OBJECT_DTYPE_BRANCH Phase 2: CA_OBJECT cmp via
4029
- # Ruby Comparable (= `<=>`). Any Comparable element type (Numeric,
4030
- # String, custom class with <=> defined) works. Raises Ruby
4031
- # TypeError mid-sort if a cell pair is uncomparable -- consistent
4032
- # with Array#sort behavior.
4033
- io.puts %Q{ VALUE r = rb_funcall(pa->v, rb_intern("<=>"), 1, pb->v);}
4034
- io.puts " if ( NIL_P(r) ) {"
4035
- io.puts %Q{ rb_raise(rb_eArgError, "#{name}_ki: comparison of %s with %s failed",}
4036
- io.puts " rb_obj_classname(pa->v), rb_obj_classname(pb->v));"
4037
- io.puts " }"
4038
- io.puts " int c = NUM2INT(r);"
4039
- io.puts " if ( c > 0 ) return 1;"
4040
- io.puts " if ( c < 0 ) return -1;"
4141
+ # The object order: `<=>`, a Float NaN after every other value
4142
+ # (ca_obj_order_cmp); two NaNs fall to the index tie-break.
4143
+ io.puts " int c = ca_obj_order_cmp(pa->v, pb->v);"
4144
+ io.puts " if ( c != 0 ) return c;"
4145
+ elsif is_fp
4146
+ # The float order: NaN after every number (CA_FLOAT_ORDER_CMP).
4147
+ io.puts " int c = CA_FLOAT_ORDER_CMP(pa->v, pb->v);"
4148
+ io.puts " if ( c != 0 ) return c;"
4041
4149
  else
4042
- if is_fp
4043
- # NaN policy :end -- NaN sorts to the end (matches qcmp_f_type in
4044
- # carray_sort.c).
4045
- io.puts " int nan_a = isnan(pa->v);"
4046
- io.puts " int nan_b = isnan(pb->v);"
4047
- io.puts " if ( nan_a && !nan_b ) return 1;"
4048
- io.puts " if ( nan_b && !nan_a ) return -1;"
4049
- end
4050
4150
  io.puts " if ( pa->v > pb->v ) return 1;"
4051
4151
  io.puts " if ( pa->v < pb->v ) return -1;"
4052
4152
  end
@@ -4168,26 +4268,43 @@ module MkKernel
4168
4268
  io.puts " rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,"
4169
4269
  io.puts " slab_axes, 1, 0);"
4170
4270
  io.puts " if ( rc != CA_ITER_OK ) {"
4171
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: input init failed rc=%d", rc);]
4271
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: input init failed rc=%d", ca_calling_method_name(), rc);]
4172
4272
  io.puts " }"
4173
- io.puts " rc = ca_iter_state_init_l2(&st_out, co, CA_SLAB_AXES,"
4273
+ io.puts " rc = ca_iter_state_init_l2_paired(&st_out, &st_in, co, CA_SLAB_AXES,"
4174
4274
  io.puts " slab_axes, 1, CA_KERNEL_WRITE);"
4175
4275
  io.puts " if ( rc != CA_ITER_OK ) {"
4176
4276
  io.puts " ca_iter_state_finish(&st_in);"
4177
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: output init failed rc=%d", rc);]
4277
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: output init failed rc=%d", ca_calling_method_name(), rc);]
4178
4278
  io.puts " }"
4179
4279
  io.puts
4180
4280
  io.puts " ca_size_t fiber_n = st_in.slab_elements;"
4181
- io.puts " #{name}_pair_#{src} *buf ="
4182
- io.puts " xmalloc(sizeof(#{name}_pair_#{src}) * (size_t) fiber_n);"
4183
- if has_kind
4184
- # PROPOSAL_PORTABLE_TEXTBOOK_SORT §9.5.2 Option γ: aux pair buffer
4185
- # for ca_sort_merge_pair_<src> (= ping-pong bottom-up mergesort).
4186
- # Allocated once outside the fiber loop, reused across fibers.
4281
+ if src == :object
4282
+ # The object comparator calls <=>, so the sort can raise part way.
4283
+ # ALLOCV frees these buffers whether the function returns or unwinds.
4284
+ # A full sort also needs aux: ca_sort_merge_pair_object is a mergesort.
4285
+ io.puts " volatile VALUE hbuf = 0, haux = 0;"
4286
+ io.puts " #{name}_pair_#{src} *buf = ALLOCV_N(#{name}_pair_#{src}, hbuf, fiber_n);"
4287
+ if partition
4288
+ io.puts " (void) haux;"
4289
+ else
4290
+ io.puts " #{name}_pair_#{src} *aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4291
+ end
4292
+ else
4293
+ # ALLOCV as well: the walk that fills buf is the only thing that
4294
+ # could raise, and it does not today, but the buffers need not rely
4295
+ # on that.
4296
+ io.puts " volatile VALUE hbuf = 0, haux = 0;"
4297
+ io.puts " #{name}_pair_#{src} *buf = ALLOCV_N(#{name}_pair_#{src}, hbuf, fiber_n);"
4298
+ end
4299
+ if has_kind && src != :object
4300
+ # The aux pair buffer for ca_sort_merge_pair_<src> (a ping-pong
4301
+ # bottom-up mergesort), taken once outside the fiber loop.
4187
4302
  io.puts " #{name}_pair_#{src} *aux = NULL;"
4188
4303
  io.puts " if ( do_stable ) {"
4189
- io.puts " aux = xmalloc(sizeof(#{name}_pair_#{src}) * (size_t) fiber_n);"
4304
+ io.puts " aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4190
4305
  io.puts " }"
4306
+ elsif src != :object
4307
+ io.puts " (void) haux;"
4191
4308
  end
4192
4309
 
4193
4310
  if view_flat
@@ -4325,16 +4442,15 @@ module MkKernel
4325
4442
  io.puts " */"
4326
4443
  end
4327
4444
  if is_object
4328
- # PROPOSAL_MKKERNEL_OBJECT_DTYPE_BRANCH Phase 2: CA_OBJECT routes to
4329
- # libc qsort with the emitted _cmp_object (rb_funcall(<=>) based).
4330
- # qsort isn't stable, but our cmp tie-breaks on the original index,
4331
- # so the result is effectively stable. do_stable is ignored: both
4332
- # paths use the same qsort + cmp here. NaN partition is skipped
4333
- # (Ruby Float NaN handling deferred -- cells of type Float with NaN
4334
- # would compare via Float#<=> which returns nil for NaN, raising
4335
- # ArgumentError in the cmp helper).
4336
- io.puts " (void) do_stable; /* CA_OBJECT: qsort handles both stable / quick */"
4337
- io.puts " qsort(#{buf_expr}, (size_t) #{sort_n_expr}, sizeof(#{name}_pair_#{src}), #{name}_cmp_#{src});"
4445
+ # CA_OBJECT sorts with the emitted _cmp_object (rb_funcall(<=>)
4446
+ # based) through ca_sort_merge_pair_object rather than libc qsort: the
4447
+ # comparator raises on a pair that does not compare, and the raise
4448
+ # must not unwind through libc's frames. The cmp tie-breaks on the
4449
+ # original index, so the order is the same for both kinds, and
4450
+ # do_stable is ignored. NaN partition is skipped (Float#<=> answers
4451
+ # nil for NaN, which the comparator reports as ArgumentError).
4452
+ io.puts " (void) do_stable; /* CA_OBJECT: one stable sort serves both kinds */"
4453
+ io.puts " ca_sort_merge_pair_object((ca_pair_object *) #{buf_expr}, (ca_pair_object *) aux, (ca_size_t) #{sort_n_expr}, #{name}_cmp_#{src});"
4338
4454
  elsif is_fixlen
4339
4455
  io.puts " /* CA_FIXLEN: libc qsort with the memcmp comparator. qsort isn't"
4340
4456
  io.puts " stable, but #{name}_cmp_#{src} tie-breaks on the original index,"
@@ -4374,16 +4490,21 @@ module MkKernel
4374
4490
  io.puts " gaps ordinal would have spent on the tie are never used, so a"
4375
4491
  io.puts " later out_r stays <= the ordinal r at that position, preserving"
4376
4492
  io.puts " both the ascending order and (via the order() method's"
4377
- io.puts " (n-1)-out_r descending transform) the descending order. NaN"
4378
- io.puts " cells never tie with each other under dense (IEEE754 x != x). */"
4493
+ io.puts " descending transform) the descending order. NaN cells tie"
4494
+ io.puts " with each other, as sort_addr and the value-hash family treat"
4495
+ io.puts " every NaN as one value. */"
4379
4496
  io.puts " ca_size_t out_r = 0;"
4380
4497
  io.puts " for ( ca_size_t r = 0; r < #{rank_n}; r++ ) {"
4381
4498
  io.puts " if ( r > 0 ) {"
4382
4499
  io.puts " if ( dense ) {"
4383
4500
  if is_object
4384
- io.puts " if ( ! rb_equal(buf[r].v, buf[r - 1].v) ) out_r++;"
4501
+ io.puts " if ( ! ( ( CA_OBJ_ISNAN(buf[r].v) && CA_OBJ_ISNAN(buf[r - 1].v) )"
4502
+ io.puts " || ca_obj_equal(buf[r].v, buf[r - 1].v) ) ) out_r++;"
4385
4503
  elsif is_fixlen
4386
4504
  io.puts " if ( memcmp(buf[r].vp, buf[r - 1].vp, (size_t) buf[r].nb) != 0 ) out_r++;"
4505
+ elsif FLOAT_DTYPES.include?(src)
4506
+ io.puts " if ( buf[r].v != buf[r - 1].v"
4507
+ io.puts " && ! ( isnan(buf[r].v) && isnan(buf[r - 1].v) ) ) out_r++;"
4387
4508
  else
4388
4509
  io.puts " if ( buf[r].v != buf[r - 1].v ) out_r++;"
4389
4510
  end
@@ -4393,15 +4514,32 @@ module MkKernel
4393
4514
  io.puts " }"
4394
4515
  io.puts " *(#{oi[:c]} *) (po + buf[r].i * ostride) = (#{oi[:c]}) out_r;"
4395
4516
  io.puts " }"
4517
+ # dense == 2 (from CArray#order only): descending dense ranks, the
4518
+ # largest value 0, the NaN cells (sorted to the end) after the numbers.
4519
+ nan_test = if is_object then "CA_OBJ_ISNAN(buf[nan_start - 1].v)"
4520
+ elsif FLOAT_DTYPES.include?(src) then "isnan(buf[nan_start - 1].v)"
4521
+ end
4522
+ io.puts " if ( dense == 2 && #{rank_n} > 0 ) {"
4523
+ io.puts " ca_size_t nan_start = #{rank_n};"
4524
+ io.puts " while ( nan_start > 0 && #{nan_test} ) nan_start--;" if nan_test
4525
+ io.puts " #{oi[:c]} top = nan_start > 0"
4526
+ io.puts " ? *(#{oi[:c]} *) (po + buf[nan_start - 1].i * ostride) : (#{oi[:c]}) -1;"
4527
+ io.puts " for ( ca_size_t r = 0; r < #{rank_n}; r++ ) {"
4528
+ io.puts " #{oi[:c]} *o = (#{oi[:c]} *) (po + buf[r].i * ostride);"
4529
+ io.puts " *o = ( r < nan_start ) ? top - *o : top + 1;"
4530
+ io.puts " }"
4531
+ io.puts " }"
4396
4532
  if mask_skip
4397
4533
  io.puts " /* Mirror input mask to output (= UNDEF rank for masked"
4398
- io.puts " cells). Write via co->mask->ptr directly with the"
4399
- io.puts " output slab offset, because the iter's mo isn't"
4400
- io.puts " populated in CA_KERNEL_WRITE mode (= reduce form's"
4401
- io.puts " op_mask pattern). */"
4534
+ io.puts " cells). Write via co->mask->ptr directly, because a"
4535
+ io.puts " mask written through a WRITE walk's cursor does not"
4536
+ io.puts " reach the array. co is a fresh row-major entity and"
4537
+ io.puts " the walk cuts its slabs from a buffer laid out the"
4538
+ io.puts " same way, so the slab's offset from st_out.alias_ptr"
4539
+ io.puts " is its offset in co. */"
4402
4540
  io.puts " if ( mi && co->mask ) {"
4403
4541
  io.puts " boolean8_t *omask_base = (boolean8_t *) co->mask->ptr"
4404
- io.puts " + ((po - (char *) co->ptr) / (ca_size_t) co->bytes);"
4542
+ io.puts " + ((po - st_out.alias_ptr) / (ca_size_t) co->bytes);"
4405
4543
  io.puts " ca_size_t mostride = st_out.slab_mask_strides[0];"
4406
4544
  io.puts " for ( ca_size_t k = 0; k < fiber_n; k++ ) {"
4407
4545
  io.puts " if ( mi[k * mstride] ) omask_base[k * mostride] = 1;"
@@ -4427,43 +4565,33 @@ module MkKernel
4427
4565
  end
4428
4566
 
4429
4567
  io.puts " }"
4430
- io.puts " xfree(buf);"
4431
- if has_kind
4432
- io.puts " if ( aux ) xfree(aux);"
4433
- end
4568
+ io.puts " ALLOCV_END(hbuf);"
4569
+ io.puts " ALLOCV_END(haux);"
4434
4570
  io.puts " ca_iter_state_finish(&st_in);"
4435
4571
  io.puts " ca_iter_state_finish(&st_out);"
4436
4572
  io.puts " return vout;"
4437
4573
  io.puts "}"
4438
4574
  end
4439
4575
 
4440
- # Shared sort-family Face gate. Descends a Face to its storage: a fixlen
4441
- # storage sorts by memcmp (the default order for fixlen, the same as a plain
4442
- # fixlen array -- a struct/record Face gets a deterministic byte order, and
4443
- # opts into a semantic order separately); a numeric storage requires
4444
- # ORDERABLE so the numeric order equals the surface order, else it raises.
4445
- # `src`/`self` are rebound to the stripped storage.
4576
+ # Shared sort-family Face gate. Every kernel of the family answers
4577
+ # positions, so it runs on the array the Face's order is read from
4578
+ # (ca_order_key): the storage of an ORDERABLE Face, the Face's own dense
4579
+ # rank for a fixlen storage that is not ORDERABLE (CAConstString's offsets,
4580
+ # a record's bytes), and a refusal otherwise. `src`/`self` are rebound to
4581
+ # that array.
4446
4582
  def self.emit_sort_face_gate(io, name)
4447
4583
  io.puts " if ( ca_is_face(src) ) {"
4448
- io.puts " VALUE _face = self;"
4449
- io.puts " int _orderable = ca_test_flag(src, CA_FLAG_FACE_ORDERABLE_STORAGE);"
4450
- io.puts " VALUE _st = rb_ca_strip_face_value(self);"
4451
- io.puts " CArray *_sc;"
4452
- io.puts " GetCArray(_st, _sc);"
4453
- io.puts " if ( _sc->data_type != CA_FIXLEN && ! _orderable ) {"
4454
- io.puts %Q[ rb_raise(rb_eArgError,]
4455
- io.puts %Q[ "#{name}_ki: Face-typed input (%s) is not orderable "]
4456
- io.puts %Q[ "by storage; use ca.parent to descend to storage",]
4457
- io.puts %Q[ rb_obj_classname(_face));]
4458
- io.puts " }"
4459
- io.puts " self = _st;"
4460
- io.puts " src = _sc;"
4584
+ io.puts " self = ca_order_key(self, ca_calling_method_name());"
4585
+ io.puts " GetCArray(self, src);"
4461
4586
  io.puts " }"
4462
4587
  end
4463
4588
 
4464
- # Reduce-family Face gate. Same ORDERABLE-descent policy as
4465
- # emit_sort_face_gate (fixlen storage -> memcmp default; numeric storage
4466
- # -> ORDERABLE required, else raise), rebinding self/src to storage. When
4589
+ # Reduce-family Face gate. Descends an ORDERABLE Face to its storage and
4590
+ # refuses any other (ca_face_require_orderable): a fixlen storage is
4591
+ # reduced by memcmp only when the Face declares that its bytes are its
4592
+ # order (CAFixlenString). CAConstString and CARecord, whose bytes are no
4593
+ # order, answer min / max / minmax in Ruby and do not reach this gate.
4594
+ # self/src are rebound to storage. When
4467
4595
  # `relift` is set (value-returning kernels), the original Face is saved to
4468
4596
  # the function-scope `_reduce_face` so the dispatcher can re-lift the output.
4469
4597
  def self.emit_reduce_face_gate(io, name, relift)
@@ -4472,16 +4600,11 @@ module MkKernel
4472
4600
  end
4473
4601
  io.puts " if ( ca_is_face(src) ) {"
4474
4602
  io.puts " VALUE _face = self;"
4475
- io.puts " int _orderable = ca_test_flag(src, CA_FLAG_FACE_ORDERABLE_STORAGE);"
4476
- io.puts " VALUE _st = rb_ca_strip_face_value(self);"
4603
+ io.puts " VALUE _st;"
4477
4604
  io.puts " CArray *_sc;"
4605
+ io.puts %Q[ ca_face_require_orderable(_face, ca_calling_method_name(), "order");]
4606
+ io.puts " _st = rb_ca_strip_face_value(self);"
4478
4607
  io.puts " GetCArray(_st, _sc);"
4479
- io.puts " if ( _sc->data_type != CA_FIXLEN && ! _orderable ) {"
4480
- io.puts %Q[ rb_raise(rb_eArgError,]
4481
- io.puts %Q[ "#{name}_ki: Face-typed input (%s) is not orderable "]
4482
- io.puts %Q[ "by storage; use ca.parent to descend to storage",]
4483
- io.puts %Q[ rb_obj_classname(_face));]
4484
- io.puts " }"
4485
4608
  if relift
4486
4609
  io.puts " _reduce_face = _face;"
4487
4610
  end
@@ -4491,32 +4614,37 @@ module MkKernel
4491
4614
  end
4492
4615
 
4493
4616
  # Re-lift a value-returning reduction result back into the Face captured by
4494
- # emit_reduce_face_gate. Full-reduction scalar -> storage_to_scalar hook;
4495
- # per-axis CArray -> rb_ca_face_template (carries the subclass tail).
4617
+ # emit_reduce_face_gate. Full-reduction scalar -> storage_to_element hook;
4618
+ # per-axis CArray -> ca_face_relift_entity (tail, mask and copy_state).
4496
4619
  # All-masked full reductions leave CA_UNDEF untouched.
4497
4620
  def self.emit_reduce_face_relift(io)
4498
- io.puts " if ( _reduce_face != Qnil && result != CA_UNDEF && result != Qnil ) {"
4499
- io.puts " if ( rb_obj_is_kind_of(result, rb_cCArray) ) {"
4500
- io.puts " /* per-axis: re-lift the storage result as a Face of the same"
4501
- io.puts " subclass; the template memcpy carries the subclass tail. */"
4502
- io.puts " CArray *_rca;"
4503
- io.puts " GetCArray(result, _rca);"
4504
- io.puts " VALUE _lifted = rb_ca_face_template(_reduce_face, _rca, _rca->dim);"
4505
- io.puts " rb_ca_set_parent(_lifted, result); /* pin storage result as GC root */"
4506
- io.puts " if ( ca_has_mask(_rca) ) {"
4507
- io.puts " /* rb_ca_face_template resets mask to NULL; carry the reduced"
4508
- io.puts " mask bits (all-masked slabs) onto the lifted Face. */"
4509
- io.puts " CArray *_lca;"
4510
- io.puts " GetCArray(_lifted, _lca);"
4511
- io.puts " ca_copy_mask(_lca, _rca);"
4621
+ io.puts " if ( _reduce_face != Qnil && RB_TYPE_P(result, T_ARRAY) ) {"
4622
+ io.puts " /* a multi-output reduction (minmax): re-lift each output. */"
4623
+ io.puts " long _i;"
4624
+ io.puts " for ( _i = 0; _i < RARRAY_LEN(result); _i++ ) {"
4625
+ io.puts " VALUE _r = rb_ary_entry(result, _i);"
4626
+ io.puts " if ( _r == CA_UNDEF || _r == Qnil ) continue;"
4627
+ io.puts " if ( rb_obj_is_kind_of(_r, rb_cCArray) ) {"
4628
+ io.puts " _r = ca_face_relift_entity(_reduce_face, _r);"
4512
4629
  io.puts " }"
4513
- io.puts " result = _lifted;"
4630
+ io.puts " else {"
4631
+ io.puts " CArray *_fca;"
4632
+ io.puts " GetCArray(_reduce_face, _fca);"
4633
+ io.puts " CA_FACE_STORAGE_TO_ELEMENT_IF_FACE(_r, _reduce_face, _fca);"
4634
+ io.puts " }"
4635
+ io.puts " rb_ary_store(result, _i, _r);"
4636
+ io.puts " }"
4637
+ io.puts " }"
4638
+ io.puts " else if ( _reduce_face != Qnil && result != CA_UNDEF && result != Qnil ) {"
4639
+ io.puts " if ( rb_obj_is_kind_of(result, rb_cCArray) ) {"
4640
+ io.puts " /* per-axis: re-lift the storage result as a Face of the same class. */"
4641
+ io.puts " result = ca_face_relift_entity(_reduce_face, result);"
4514
4642
  io.puts " }"
4515
4643
  io.puts " else {"
4516
4644
  io.puts " /* full reduction: decode the scalar via the registered hook. */"
4517
4645
  io.puts " CArray *_fca;"
4518
4646
  io.puts " GetCArray(_reduce_face, _fca);"
4519
- io.puts " CA_FACE_STORAGE_TO_SCALAR_IF_FACE(result, _reduce_face, _fca);"
4647
+ io.puts " CA_FACE_STORAGE_TO_ELEMENT_IF_FACE(result, _reduce_face, _fca);"
4520
4648
  io.puts " }"
4521
4649
  io.puts " }"
4522
4650
  end
@@ -4528,7 +4656,7 @@ module MkKernel
4528
4656
  has_kind = !partition # :full and :rank gain kind: dispatch (Option γ)
4529
4657
  mask_sentinel = (k[:mask_self] == :sentinel)
4530
4658
  # bind_ruby: false kernels are consumed by other .c files at the C
4531
- # level (= rb_ca_sorted_view in carray_order.c calls rb_ca_sort_addr_ki).
4659
+ # level (carray_sort.c / carray_partition.c call the _mp twins).
4532
4660
  # Emit extern linkage so the symbol is reachable across translation
4533
4661
  # units. bind_ruby: true kernels stay static (only referenced by
4534
4662
  # Init_carray_kernels in this file).
@@ -4540,6 +4668,10 @@ module MkKernel
4540
4668
  # partition_index / rank_index, which are consumed at the C level
4541
4669
  # by carray_order.c (= ordering helpers like nlargest, order).
4542
4670
  storage = (k[:bind_ruby] == false || k[:c_callable]) ? "" : "static "
4671
+ # bind_ruby: false + mask_self: :sentinel kernels are reached only
4672
+ # through their _mp twins, so the fixed-masked_last entries below
4673
+ # would have no caller.
4674
+ mp_only = (k[:mask_self] == :sentinel && k[:bind_ruby] == false)
4543
4675
  # PROPOSAL_PORTABLE_TEXTBOOK_SORT §9.5.2 Option γ:
4544
4676
  # For algorithm :full / :rank, dispatch emits THREE C-level entries:
4545
4677
  # rb_ca_<name>_ki_quick (self, vaxis) -> do_stable=0
@@ -4576,32 +4708,30 @@ module MkKernel
4576
4708
  io.puts "{"
4577
4709
  io.puts " CArray *src;"
4578
4710
  io.puts " GetCArray(self, src);"
4579
- io.puts " /* Face gate: descend to storage (fixlen -> memcmp default,"
4580
- io.puts " numeric -> ORDERABLE required). The strip also makes the"
4581
- io.puts " switch below and every index/addr template output build on"
4582
- io.puts " the plain storage, not a face-lifted (NULL-ptr) view. */"
4711
+ emit_lazy_evaluated(io)
4712
+ io.puts " /* Face gate: run on the array the Face's order is read from."
4713
+ io.puts " It is never a Face, so the switch below and every index/addr"
4714
+ io.puts " template output build on a plain array, not a face-lifted"
4715
+ io.puts " (NULL-ptr) view. */"
4583
4716
  emit_sort_face_gate(io, name)
4584
4717
  if k[:mask_self] == :raise
4585
4718
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
4586
4719
  io.puts " masked elements would break the ordering invariant. */"
4587
4720
  io.puts " if ( ca_has_mask(src) ) {"
4588
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: masked input not supported (use ca.value or ca.strip_mask(fill))");]
4721
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: masked input not supported (use ca.value or ca.strip_mask(fill))", ca_calling_method_name());]
4589
4722
  io.puts " }"
4590
4723
  end
4591
- io.puts " int axis = NUM2INT(vaxis);"
4592
- io.puts " if ( axis < 0 ) axis += src->ndim;"
4593
- io.puts " if ( axis < 0 || axis >= src->ndim ) {"
4594
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(vaxis), src->ndim);]
4595
- io.puts " }"
4724
+ io.puts " int axis = ca_normalize_axis_arg(vaxis, src->ndim, NULL);"
4596
4725
  io.puts " switch ( src->data_type ) {"
4597
4726
  k[:source].each do |s|
4598
4727
  si = DTYPES[s]
4599
4728
  native_extra = mask_sentinel ? ", masked_last" : ""
4600
4729
  native_extra += rank ? ", dense" : ""
4601
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, axis, do_stable#{native_extra});"
4730
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, axis, do_stable#{native_extra}));"
4602
4731
  end
4603
4732
  io.puts " default:"
4604
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
4733
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
4734
+ io.puts %Q[ rb_raise(rb_eCADataTypeError, "%s: source data_type :%s not supported (expected one of: #{MkKernel.type_names(k[:source])})", ca_calling_method_name(), ca_type_name[src->data_type]);]
4605
4735
  io.puts " }"
4606
4736
  io.puts " return Qnil; /* unreachable */"
4607
4737
  io.puts "}"
@@ -4614,30 +4744,33 @@ module MkKernel
4614
4744
  dispatch_call_last = mask_sentinel ? ", 1" : ""
4615
4745
  dispatch_call_last += rank ? ", 0" : ""
4616
4746
 
4617
- # _ki_quick: arity 2 (self, vaxis). Linkage = storage.
4618
- io.puts
4619
- io.puts "#{storage}VALUE"
4620
- io.puts "rb_ca_#{name}_ki_quick (VALUE self, VALUE vaxis)"
4621
- io.puts "{"
4622
- io.puts " return #{name}_ki_dispatch(self, vaxis, 0#{dispatch_call_last});"
4623
- io.puts "}"
4747
+ unless mp_only
4748
+ # _ki_quick: arity 2 (self, vaxis). Linkage = storage.
4749
+ io.puts
4750
+ io.puts "#{storage}VALUE"
4751
+ io.puts "rb_ca_#{name}_ki_quick (VALUE self, VALUE vaxis)"
4752
+ io.puts "{"
4753
+ io.puts " return #{name}_ki_dispatch(self, vaxis, 0#{dispatch_call_last});"
4754
+ io.puts "}"
4624
4755
 
4625
- # _ki_stable: arity 2 (self, vaxis). Linkage = storage.
4626
- io.puts
4627
- io.puts "#{storage}VALUE"
4628
- io.puts "rb_ca_#{name}_ki_stable (VALUE self, VALUE vaxis)"
4629
- io.puts "{"
4630
- io.puts " return #{name}_ki_dispatch(self, vaxis, 1#{dispatch_call_last});"
4631
- io.puts "}"
4756
+ # _ki_stable: arity 2 (self, vaxis). Linkage = storage.
4757
+ io.puts
4758
+ io.puts "#{storage}VALUE"
4759
+ io.puts "rb_ca_#{name}_ki_stable (VALUE self, VALUE vaxis)"
4760
+ io.puts "{"
4761
+ io.puts " return #{name}_ki_dispatch(self, vaxis, 1#{dispatch_call_last});"
4762
+ io.puts "}"
4632
4763
 
4633
- # _ki: arity 2 alias of _ki_quick (= preserves existing arity-1
4634
- # Ruby binding and existing C extern callers).
4635
- io.puts
4636
- io.puts "#{storage}VALUE"
4637
- io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis)"
4638
- io.puts "{"
4639
- io.puts " return rb_ca_#{name}_ki_quick(self, vaxis);"
4640
- io.puts "}"
4764
+ # _ki: arity 2 alias of _ki_quick (= preserves existing arity-1
4765
+ # Ruby binding and existing C extern callers).
4766
+ io.puts
4767
+ io.puts "#{storage}VALUE"
4768
+ io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis)"
4769
+ io.puts "{"
4770
+ io.puts " return rb_ca_#{name}_ki_quick(self, vaxis);"
4771
+ io.puts "}"
4772
+
4773
+ end
4641
4774
 
4642
4775
  if mask_sentinel && k[:bind_ruby] == false
4643
4776
  # _mp twins: explicit masked_last, non-Ruby (plain C int, no
@@ -4659,16 +4792,16 @@ module MkKernel
4659
4792
  end
4660
4793
 
4661
4794
  if rank
4662
- # _quick_dense: explicit dense (0 :ordinal / 1 :dense), extern for
4663
- # carray_order.c's `order` method: kwarg (order is CA_INT64-family
4664
- # sugar over rank_index, defined in a different file, so it needs
4795
+ # _kind_dense: explicit kind (0 :quick / 1 :stable) and dense
4796
+ # (0 :ordinal / 1 :dense), extern for carray_order.c's `order`
4797
+ # (sugar over rank_index, defined in a different file, so it needs
4665
4798
  # a non-static entry point the same way the mask_self: :sentinel
4666
4799
  # kernels' _mp twins do).
4667
4800
  io.puts
4668
4801
  io.puts "VALUE"
4669
- io.puts "rb_ca_#{name}_ki_quick_dense (VALUE self, VALUE vaxis, int dense)"
4802
+ io.puts "rb_ca_#{name}_ki_kind_dense (VALUE self, VALUE vaxis, int stable, int dense)"
4670
4803
  io.puts "{"
4671
- io.puts " return #{name}_ki_dispatch(self, vaxis, 0, dense);"
4804
+ io.puts " return #{name}_ki_dispatch(self, vaxis, stable, dense);"
4672
4805
  io.puts "}"
4673
4806
  end
4674
4807
  else
@@ -4683,45 +4816,45 @@ module MkKernel
4683
4816
  io.puts "{"
4684
4817
  io.puts " CArray *src;"
4685
4818
  io.puts " GetCArray(self, src);"
4686
- io.puts " /* Face gate (partition): same as the sort dispatcher --"
4687
- io.puts " fixlen -> memcmp default, numeric -> ORDERABLE required. */"
4819
+ emit_lazy_evaluated(io)
4820
+ io.puts " /* Face gate (partition): same as the sort dispatcher. */"
4688
4821
  emit_sort_face_gate(io, name)
4689
4822
  if k[:mask_self] == :raise
4690
4823
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
4691
4824
  io.puts " masked elements would break the ordering invariant. */"
4692
4825
  io.puts " if ( ca_has_mask(src) ) {"
4693
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: masked input not supported (use ca.value or ca.strip_mask(fill))");]
4826
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: masked input not supported (use ca.value or ca.strip_mask(fill))", ca_calling_method_name());]
4694
4827
  io.puts " }"
4695
4828
  end
4696
- io.puts " int axis = NUM2INT(vaxis);"
4697
- io.puts " if ( axis < 0 ) axis += src->ndim;"
4698
- io.puts " if ( axis < 0 || axis >= src->ndim ) {"
4699
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(vaxis), src->ndim);]
4700
- io.puts " }"
4701
- io.puts " ca_size_t kth = NUM2SIZE(vkth);"
4829
+ io.puts " int axis = ca_normalize_axis_arg(vaxis, src->ndim, NULL);"
4830
+ io.puts " ca_size_t kth_raw = (ca_size_t) ca_integer_arg(vkth, \"kth\", NULL);"
4831
+ io.puts " ca_size_t kth = kth_raw;"
4702
4832
  io.puts " ca_size_t axis_n = src->dim[axis];"
4703
4833
  io.puts " if ( kth < 0 ) kth += axis_n;"
4704
4834
  io.puts " if ( kth < 0 || kth >= axis_n ) {"
4705
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: kth %lld out of range for axis size %lld", (long long) NUM2SIZE(vkth), (long long) axis_n);]
4835
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: kth %lld out of range for length %lld", ca_calling_method_name(), (long long) kth_raw, (long long) axis_n);]
4706
4836
  io.puts " }"
4707
4837
  io.puts " switch ( src->data_type ) {"
4708
4838
  k[:source].each do |s|
4709
4839
  si = DTYPES[s]
4710
4840
  native_extra = mask_sentinel ? ", masked_last" : ""
4711
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, axis, kth#{native_extra});"
4841
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, axis, kth#{native_extra}));"
4712
4842
  end
4713
4843
  io.puts " default:"
4714
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
4844
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
4845
+ io.puts %Q[ rb_raise(rb_eCADataTypeError, "%s: source data_type :%s not supported (expected one of: #{MkKernel.type_names(k[:source])})", ca_calling_method_name(), ca_type_name[src->data_type]);]
4715
4846
  io.puts " }"
4716
4847
  io.puts " return Qnil; /* unreachable */"
4717
4848
  io.puts "}"
4718
4849
 
4719
- io.puts
4720
- io.puts "#{storage}VALUE"
4721
- io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis, VALUE vkth)"
4722
- io.puts "{"
4723
- io.puts " return #{name}_ki_dispatch(self, vaxis, vkth#{mask_sentinel ? ", 1" : ""});"
4724
- io.puts "}"
4850
+ unless mp_only
4851
+ io.puts
4852
+ io.puts "#{storage}VALUE"
4853
+ io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis, VALUE vkth)"
4854
+ io.puts "{"
4855
+ io.puts " return #{name}_ki_dispatch(self, vaxis, vkth#{mask_sentinel ? ", 1" : ""});"
4856
+ io.puts "}"
4857
+ end
4725
4858
 
4726
4859
  if mask_sentinel && k[:bind_ruby] == false
4727
4860
  # _mp twin: explicit masked_last, extern for carray_partition.c's
@@ -4739,10 +4872,10 @@ module MkKernel
4739
4872
  # wrapper). When public_method: is set, emit a -1-argc
4740
4873
  # entry that parses `axis:` (and `kind:` for has_kind, and
4741
4874
  # `masked_position:` for mask_self: :sentinel, and `method:` for
4742
- # :rank) kwarg with default 0 / :quick / :last / :ordinal and
4743
- # dispatches to the appropriate positional entry. For :partition
4744
- # algorithm, kth is accepted as a single positional argument
4745
- # before the kwarg.
4875
+ # :rank) kwarg with default nil (= whole array) / :quick / :last /
4876
+ # :ordinal and dispatches to the appropriate positional entry.
4877
+ # For :partition algorithm, kth is accepted as a single positional
4878
+ # argument before the kwarg.
4746
4879
  if k[:public_method]
4747
4880
  legacy_name = k[:public_method].to_s
4748
4881
  io.puts
@@ -4769,7 +4902,21 @@ module MkKernel
4769
4902
  scan_targets << "&rmasked_position" if mask_sentinel
4770
4903
  scan_targets << "&rmethod" if rank
4771
4904
  io.puts " rb_scan_options(ropt, \"#{scan_keys.join(",")}\", #{scan_targets.join(", ")});"
4772
- io.puts " if ( NIL_P(raxis) ) raxis = INT2NUM(0); /* default axis */"
4905
+ # No axis means the whole array, as for sort: flatten and walk the
4906
+ # single fiber. rank_index keeps self's shape (a rank belongs to a
4907
+ # cell, as for order); the position lists keep the flat shape.
4908
+ io.puts " volatile VALUE whole_src = Qnil;" if rank
4909
+ io.puts " if ( NIL_P(raxis) ) { /* no axis: the whole array */"
4910
+ io.puts " whole_src = self;" if rank
4911
+ # The flattened view of a lazy expression is not one itself, so the
4912
+ # dispatcher would not see an expression to hand over: ask here.
4913
+ io.puts " {"
4914
+ io.puts " volatile VALUE evaluated = ca_lazy_evaluated(self);"
4915
+ io.puts " if ( ! NIL_P(evaluated) ) self = evaluated;"
4916
+ io.puts " }"
4917
+ io.puts " self = rb_ca_flatten(self);"
4918
+ io.puts " raxis = INT2NUM(0);"
4919
+ io.puts " }"
4773
4920
  if rank
4774
4921
  # method: :ordinal (default) / :dense -> dense int. :ordinal
4775
4922
  # assigns every cell a distinct rank (ties broken by original
@@ -4777,37 +4924,15 @@ module MkKernel
4777
4924
  # the standard "dense rank" used to compose rank as a sort_addr
4778
4925
  # priority key without silently dropping lower-priority keys on
4779
4926
  # ties (see MASKED_POSITION's sibling proposal thread).
4780
- io.puts " int dense = 0; /* default method: :ordinal */"
4781
- io.puts " if ( ! NIL_P(rmethod) ) {"
4782
- io.puts " static ID sym_ordinal = 0, sym_dense = 0;"
4783
- io.puts " if ( ! sym_ordinal ) sym_ordinal = rb_intern(\"ordinal\");"
4784
- io.puts " if ( ! sym_dense ) sym_dense = rb_intern(\"dense\");"
4785
- io.puts " ID method_id = SYM2ID(rmethod);"
4786
- io.puts " if ( method_id == sym_ordinal ) dense = 0;"
4787
- io.puts " else if ( method_id == sym_dense ) dense = 1;"
4788
- io.puts " else {"
4789
- io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: unknown method %s (expected :ordinal or :dense)", rb_id2name(method_id));]
4790
- io.puts " }"
4791
- io.puts " }"
4927
+ io.puts " int dense = NIL_P(rmethod) ? 0 : ca_symbol_choice(rmethod, \"method\", \"ordinal\", \"dense\", NULL);"
4792
4928
  end
4793
4929
  if mask_sentinel
4794
4930
  # masked_position: :last (default) / :first -> masked_last int.
4795
4931
  # Shared by both the has_kind and :partition branches below.
4796
- io.puts " int masked_last = 1; /* default masked_position: :last */"
4797
- io.puts " if ( ! NIL_P(rmasked_position) ) {"
4798
- io.puts " static ID sym_first = 0, sym_last = 0;"
4799
- io.puts " if ( ! sym_first ) sym_first = rb_intern(\"first\");"
4800
- io.puts " if ( ! sym_last ) sym_last = rb_intern(\"last\");"
4801
- io.puts " ID mp_id = SYM2ID(rmasked_position);"
4802
- io.puts " if ( mp_id == sym_last ) masked_last = 1;"
4803
- io.puts " else if ( mp_id == sym_first ) masked_last = 0;"
4804
- io.puts " else {"
4805
- io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: unknown masked_position %s (expected :first or :last)", rb_id2name(mp_id));]
4806
- io.puts " }"
4807
- io.puts " }"
4932
+ io.puts " int masked_last = NIL_P(rmasked_position) ? 1 : ca_symbol_choice(rmasked_position, \"masked_position\", \"first\", \"last\", NULL);"
4808
4933
  end
4809
4934
  if partition
4810
- # partition_index(kth, axis: 0, masked_position: :last) -- kth
4935
+ # partition_index(kth, axis: nil, masked_position: :last) -- kth
4811
4936
  # positional, axis/masked_position kwargs.
4812
4937
  io.puts " if ( argc != 1 ) {"
4813
4938
  io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: wrong number of positional arguments (given %d, expected 1: kth)", argc);]
@@ -4818,28 +4943,27 @@ module MkKernel
4818
4943
  io.puts " return rb_ca_#{name}_ki(self, raxis, argv[0]);"
4819
4944
  end
4820
4945
  else
4821
- # sort_index(axis: 0, kind: :quick, masked_position: :last) --
4946
+ # sort_index(axis: nil, kind: :quick, masked_position: :last) --
4822
4947
  # no positional, kwargs only.
4823
4948
  io.puts " if ( argc != 0 ) {"
4824
4949
  io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: positional args no longer accepted (given %d args); use axis:/kind: kwargs", argc);]
4825
4950
  io.puts " }"
4826
4951
  # kind: dispatch (:quick default, :stable optional).
4827
- io.puts " int do_stable = 0;"
4828
- io.puts " if ( ! NIL_P(rkind) ) {"
4829
- io.puts " static ID sym_quick = 0, sym_stable = 0;"
4830
- io.puts " if ( ! sym_quick ) sym_quick = rb_intern(\"quick\");"
4831
- io.puts " if ( ! sym_stable ) sym_stable = rb_intern(\"stable\");"
4832
- io.puts " ID kind_id = SYM2ID(rkind);"
4833
- io.puts " if ( kind_id == sym_quick ) do_stable = 0;"
4834
- io.puts " else if ( kind_id == sym_stable ) do_stable = 1;"
4835
- io.puts " else {"
4836
- io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: unknown kind %s (expected :quick or :stable)", rb_id2name(kind_id));]
4837
- io.puts " }"
4838
- io.puts " }"
4952
+ io.puts " int do_stable = NIL_P(rkind) ? 0 : ca_symbol_choice(rkind, \"kind\", \"quick\", \"stable\", NULL);"
4839
4953
  if mask_sentinel
4840
4954
  io.puts " return #{name}_ki_dispatch(self, raxis, do_stable, masked_last);"
4841
4955
  elsif rank
4842
- io.puts " return #{name}_ki_dispatch(self, raxis, do_stable, dense);"
4956
+ io.puts " VALUE out = #{name}_ki_dispatch(self, raxis, do_stable, dense);"
4957
+ io.puts " if ( ! NIL_P(whole_src) ) {"
4958
+ io.puts " CArray *src;"
4959
+ io.puts " VALUE shape_argv[CA_RANK_MAX];"
4960
+ io.puts " GetCArray(whole_src, src);"
4961
+ io.puts " for ( int k = 0; k < src->ndim; k++ ) {"
4962
+ io.puts " shape_argv[k] = SIZE2NUM(src->dim[k]);"
4963
+ io.puts " }"
4964
+ io.puts " out = rb_ca_reshape((int) src->ndim, shape_argv, out);"
4965
+ io.puts " }"
4966
+ io.puts " return out;"
4843
4967
  else
4844
4968
  io.puts " return do_stable ? rb_ca_#{name}_ki_stable(self, raxis)"
4845
4969
  io.puts " : rb_ca_#{name}_ki_quick(self, raxis);"
@@ -4880,7 +5004,7 @@ module MkKernel
4880
5004
  io.puts
4881
5005
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
4882
5006
  k[:source].each do |src|
4883
- emit_search_native(io, k, src)
5007
+ emit_native_for(io, k, src) { |o| emit_search_native(o, k, src) }
4884
5008
  end
4885
5009
  emit_search_dispatch(io, k)
4886
5010
  end
@@ -4903,7 +5027,7 @@ module MkKernel
4903
5027
  # body: Hash form picks per-data_type-kind branch (= :int / :float /
4904
5028
  # :object / :fixlen); String form is uniform.
4905
5029
  # PROPOSAL_MKKERNEL_OBJECT_DTYPE_BRANCH Phase 4: :object branch added for
4906
- # CA_OBJECT (equality via rb_equal, ordering via rb_funcall(<=>)).
5030
+ # CA_OBJECT (equality via ca_obj_equal, ordering via rb_funcall(<=>)).
4907
5031
  # PROPOSAL_SEARCH_SEMANTICS_UNIFY S2: :fixlen branch added for CA_FIXLEN
4908
5032
  # (the cell is a runtime-width byte blob; comparison is memcmp over
4909
5033
  # ca->bytes, the same total order fixlen bincmp uses).
@@ -4937,7 +5061,7 @@ module MkKernel
4937
5061
  <<~C.chomp
4938
5062
  int eps_is_default = NIL_P(reps);
4939
5063
  double user_eps = eps_is_default ? 0.0 : NUM2DBL(reps);
4940
- double query_eps = eps_is_default ? (double)(#{eps_def}) * fabs((double)query_val) : user_eps;
5064
+ double query_eps = eps_is_default ? (isfinite((double)query_val) ? (double)(#{eps_def}) * fabs((double)query_val) : 0.0) : user_eps;
4941
5065
  C
4942
5066
  elsif has_eps && no_eps
4943
5067
  "(void) reps; /* #{is_fixlen ? "CA_FIXLEN" : "CA_OBJECT"}: eps ignored, exact match only */\n double query_eps = 0.0; (void) query_eps;"
@@ -4952,7 +5076,7 @@ module MkKernel
4952
5076
  ""
4953
5077
  end
4954
5078
  eps_percell = if has_eps && !no_eps
4955
- "double query_eps = eps_is_default ? (double)(#{eps_def}) * fabs((double)query_val) : user_eps;"
5079
+ "double query_eps = eps_is_default ? (isfinite((double)query_val) ? (double)(#{eps_def}) * fabs((double)query_val) : 0.0) : user_eps;"
4956
5080
  else
4957
5081
  "double query_eps = 0.0; (void) query_eps;"
4958
5082
  end
@@ -4964,14 +5088,36 @@ module MkKernel
4964
5088
  # and let rb_ca_obj2ptr pack it; `query_val` is then a char* the memcmp
4965
5089
  # body reads (mirrors the legacy flat bsearch ALLOCA_N path).
4966
5090
  query_setup_a = if is_fixlen
4967
- "char *query_val = ALLOCA_N(char, ca->bytes);\n" \
5091
+ "volatile VALUE query_val_holder = 0;\n" \
5092
+ " char *query_val = ALLOCV_N(char, query_val_holder, ca->bytes);\n" \
4968
5093
  " rb_ca_obj2ptr(self, rval, query_val);"
4969
5094
  else
5095
+ (INT_DTYPES.include?(src) ? "ca_check_integer_fits(rval, #{si[:ca]});\n " : "") +
4970
5096
  "#{si[:c]} query_val_buf;\n" \
4971
5097
  " rb_ca_obj2ptr(self, rval, &query_val_buf);\n" \
4972
5098
  " #{si[:c]} query_val = query_val_buf;"
4973
5099
  end
4974
5100
 
5101
+ # The object lane calls Ruby for every cell of the case B/C walk, which
5102
+ # runs between a direct attach and detach of self and val; a raise there
5103
+ # would leave both attached. An entity holds no attach, so the lane
5104
+ # reads an entity copy of whichever is a view.
5105
+ entity_bc = if is_object
5106
+ <<~C.chomp
5107
+ volatile VALUE vca_entity = self;
5108
+ if ( ! ca_is_entity(ca) ) {
5109
+ vca_entity = rb_ca_copy(self);
5110
+ GetCArray(vca_entity, ca);
5111
+ }
5112
+ if ( ! ca_is_entity(cv) ) {
5113
+ vsrc_val = rb_ca_copy(vsrc_val);
5114
+ GetCArray(vsrc_val, cv);
5115
+ }
5116
+ C
5117
+ else
5118
+ ""
5119
+ end
5120
+
4975
5121
  # case B/C query coercion. numeric/object: wrap-readonly to self's
4976
5122
  # data_type when they differ. fixlen: require a CA_FIXLEN query of the
4977
5123
  # same byte width (no scalar coercion exists for blobs), so the per-cell
@@ -4981,7 +5127,7 @@ module MkKernel
4981
5127
  (void) vsrc_val; /* fixlen: no wrap-readonly coercion */
4982
5128
  if ( cv->data_type != CA_FIXLEN || cv->bytes != ca->bytes ) {
4983
5129
  rb_raise(rb_eArgError,
4984
- "#{name}_ki: CA_FIXLEN query must be CA_FIXLEN of the same byte width (= %ld)", (long) ca->bytes);
5130
+ "%s: CA_FIXLEN query must be CA_FIXLEN of the same byte width (= %ld)", ca_calling_method_name(), (long) ca->bytes);
4985
5131
  }
4986
5132
  C
4987
5133
  else
@@ -5016,56 +5162,41 @@ module MkKernel
5016
5162
  # flat array path. op_mask is lazily allocated on the first no-match.
5017
5163
  undef_nm = k[:undef_no_match]
5018
5164
  nm_cond = "result == (#{oi[:c]})(#{no_match})"
5019
- # mask_query: :undef -- a masked query cell yields a masked output cell,
5020
- # so an undetermined query cannot be answered from the value that happens
5021
- # to sit under the mask. Shares op_mask with undef_no_match.
5022
- undef_q = (k[:mask_query] == :undef)
5023
- # op_mask declaration (only when an output cell can be masked);
5024
- # content-only (the heredoc carries indentation).
5025
- op_mask_decl = (undef_nm || undef_q) \
5026
- ? "boolean8_t *op_mask = NULL; /* lazily allocated on the first masked output cell */" \
5027
- : "/* (no masked output cell) */"
5165
+ # A masked query cell yields a masked output cell, so an undetermined
5166
+ # query cannot be answered from the value that happens to sit under the
5167
+ # mask. Shares op_mask with undef_no_match.
5168
+ op_mask_decl = "boolean8_t *op_mask = NULL; /* lazily allocated on the first masked output cell */"
5028
5169
 
5029
5170
  # Case A scalar UNDEF query (also reached by the A1 single-element
5030
5171
  # reduction when that element is masked): every output cell is
5031
5172
  # undetermined, so short-circuit before the query is coerced to a number.
5032
- undef_query_a = if undef_q
5033
- <<~C.chomp
5034
- if ( rval == CA_UNDEF ) {
5035
- int8_t undef_axes[1] = { (int8_t) axis };
5036
- VALUE voutu = rb_ca_new_reduced(self, undef_axes, 1, #{oi[:ca]}, 0);
5037
- CArray *cou;
5038
- GetCArray(voutu, cou);
5039
- MEMZERO((#{oi[:c]} *) cou->ptr, #{oi[:c]}, cou->elements);
5040
- ca_create_mask(cou);
5041
- memset(cou->mask->ptr, 1, (size_t) cou->elements);
5042
- if ( ca->ndim == 1 ) { return Qnil; }
5043
- return voutu;
5044
- }
5045
- C
5046
- else
5047
- "/* (mask_query: :ignore) */"
5048
- end
5173
+ undef_query_a = <<~C.chomp
5174
+ if ( rval == CA_UNDEF ) {
5175
+ int8_t undef_axes[1] = { (int8_t) axis };
5176
+ VALUE voutu = rb_ca_new_reduced(self, undef_axes, 1, #{oi[:ca]}, 0);
5177
+ CArray *cou;
5178
+ GetCArray(voutu, cou);
5179
+ MEMZERO((#{oi[:c]} *) cou->ptr, #{oi[:c]}, cou->elements);
5180
+ ca_create_mask(cou);
5181
+ memset(cou->mask->ptr, 1, (size_t) cou->elements);
5182
+ if ( ca->ndim == 1 ) { return Qnil; }
5183
+ return voutu;
5184
+ }
5185
+ C
5049
5186
 
5050
5187
  # Case B/C query mask base. NULL when the query carries no mask.
5051
- cv_mask_decl_bc = undef_q \
5052
- ? "boolean8_t *cv_mask_base = ca_has_mask(cv) ? (boolean8_t *) cv->mask->ptr : NULL;" \
5053
- : "/* (mask_query: :ignore) */"
5188
+ cv_mask_decl_bc = "boolean8_t *cv_mask_base = ca_has_mask(cv) ? (boolean8_t *) cv->mask->ptr : NULL;"
5054
5189
 
5055
5190
  # Per-cell guard wrapping the body + write in case B/C. A masked query
5056
5191
  # cell skips the body entirely and marks the output cell instead.
5057
- undef_query_open_bc = if undef_q
5058
- <<~C.chomp
5059
- if ( cv_mask_base && cv_mask_base[val_offset / cv->bytes] ) {
5060
- if ( ! op_mask ) { ca_create_mask(co); op_mask = (boolean8_t *) co->mask->ptr; }
5061
- op_mask[flat] = 1;
5062
- op[flat] = (#{oi[:c]}) 0; /* undetermined query; the mask bit is what counts */
5063
- } else {
5064
- C
5065
- else
5066
- "/* (mask_query: :ignore) */"
5067
- end
5068
- undef_query_close_bc = undef_q ? "}" : "/* (mask_query: :ignore) */"
5192
+ undef_query_open_bc = <<~C.chomp
5193
+ if ( cv_mask_base && cv_mask_base[val_offset / cv->bytes] ) {
5194
+ if ( ! op_mask ) { ca_create_mask(co); op_mask = (boolean8_t *) co->mask->ptr; }
5195
+ op_mask[flat] = 1;
5196
+ op[flat] = (#{oi[:c]}) 0; /* undetermined query; the mask bit is what counts */
5197
+ } else {
5198
+ C
5199
+ undef_query_close_bc = "}"
5069
5200
 
5070
5201
  # Per-cell write block for case A (scalar query). `out_i` is the output
5071
5202
  # cursor, `co` the output CArray. When undef_no_match: a no-match masks
@@ -5140,7 +5271,7 @@ module MkKernel
5140
5271
  slab_axes, 1, 0);
5141
5272
  if ( rc != CA_ITER_OK ) {
5142
5273
  rb_raise(rb_eRuntimeError,
5143
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
5274
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
5144
5275
  }
5145
5276
  C
5146
5277
 
@@ -5222,7 +5353,7 @@ module MkKernel
5222
5353
  # transformation in two more inner loops.
5223
5354
  io.puts <<~C
5224
5355
  /* view_flat semantics + CArray val: NotImpError (deferred). */
5225
- rb_raise(rb_eNotImpError, "#{name}_ki: view_flat semantics with CArray val (broadcast) not yet supported");
5356
+ rb_raise(rb_eNotImpError, "%s: view_flat semantics with CArray val (broadcast) not yet supported", ca_calling_method_name());
5226
5357
  C
5227
5358
  end
5228
5359
 
@@ -5245,7 +5376,9 @@ module MkKernel
5245
5376
  */
5246
5377
  CArray *cv;
5247
5378
  GetCArray(rval, cv);
5248
- VALUE vsrc_val = rval;
5379
+ /* volatile: once the query is wrapped below, only cv is used, and
5380
+ the wrapper must stay reachable while its cells are read. */
5381
+ volatile VALUE vsrc_val = rval;
5249
5382
  #{coerce_bc}
5250
5383
 
5251
5384
  /* base_shape = self.shape with axis removed. base_to_self[bk] =
@@ -5280,13 +5413,13 @@ module MkKernel
5280
5413
  if ( i == axis ) continue;
5281
5414
  if ( cv->dim[i] != ca->dim[i] ) {
5282
5415
  rb_raise(rb_eArgError,
5283
- "#{name}_ki: val shape mismatch (A3 candidate, axis %d non-target dim[%d] = %ld != self.dim[%d] = %ld; expected scalar / [M] / base_shape (ndim>=2) / self.shape with axis dim free)",
5416
+ "%s: val shape mismatch (A3 candidate, axis %d non-target dim[%d] = %ld != self.dim[%d] = %ld; expected scalar / [M] / base_shape (ndim>=2) / self.shape with axis dim free)", ca_calling_method_name(),
5284
5417
  axis, (int) i, (long) cv->dim[i], (int) i, (long) ca->dim[i]);
5285
5418
  }
5286
5419
  }
5287
5420
  if ( cv->dim[axis] < 1 ) {
5288
5421
  rb_raise(rb_eArgError,
5289
- "#{name}_ki: empty query (val.dim[%d] = 0)", axis);
5422
+ "%s: empty query (val.dim[%d] = 0)", ca_calling_method_name(), axis);
5290
5423
  }
5291
5424
  cv_form = CV_FORM_A3;
5292
5425
  cv_form_set = 1;
@@ -5296,7 +5429,7 @@ module MkKernel
5296
5429
  /* A2: 1-D shared M-query (M >= 1); placed above A2.5 in rev5 */
5297
5430
  if ( cv->dim[0] < 1 ) {
5298
5431
  rb_raise(rb_eArgError,
5299
- "#{name}_ki: empty query (val.dim[0] = 0)");
5432
+ "%s: empty query (val.dim[0] = 0)", ca_calling_method_name());
5300
5433
  }
5301
5434
  cv_form = CV_FORM_A2;
5302
5435
  cv_form_set = 1;
@@ -5317,7 +5450,7 @@ module MkKernel
5317
5450
 
5318
5451
  if ( ! cv_form_set ) {
5319
5452
  rb_raise(rb_eArgError,
5320
- "#{name}_ki: val shape not accepted (val.ndim = %d; expected scalar / [M] / base_shape (ndim>=2, = %d) / self.shape with axis dim free (ndim = %d))",
5453
+ "%s: val shape not accepted (val.ndim = %d; expected scalar / [M] / base_shape (ndim>=2, = %d) / self.shape with axis dim free (ndim = %d))", ca_calling_method_name(),
5321
5454
  (int) cv->ndim, (int) base_ndim, (int) ca->ndim);
5322
5455
  }
5323
5456
 
@@ -5352,9 +5485,9 @@ module MkKernel
5352
5485
  #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
5353
5486
  #{op_mask_decl}
5354
5487
 
5488
+ #{entity_bc}
5355
5489
  /* Attach self + val; rev4 uses raw row-major byte addressing. */
5356
- ca_attach(ca);
5357
- ca_attach(cv);
5490
+ ca_attach_n(2, ca, cv); /* window: numeric bodies do not raise; the object lane reads entities */
5358
5491
 
5359
5492
  /* self_byte_stride[j] = bytes to advance along self axis j (row-major). */
5360
5493
  ca_size_t self_byte_stride[CA_RANK_MAX];
@@ -5481,28 +5614,15 @@ module MkKernel
5481
5614
  end
5482
5615
  io.puts "{"
5483
5616
  io.puts " CArray *src;"
5484
- io.puts " VALUE self_ref = self; /* pre-strip Face, for to_comparable */"
5485
- io.puts " int self_face_comparable = 0;"
5486
5617
  io.puts " int self_was_face = 0;"
5487
5618
  io.puts " GetCArray(self, src);"
5488
- io.puts " /* Face gate (self): descend a Face reference to storage so the"
5489
- io.puts " search runs on the numeric storage. ORDERABLE licenses the"
5490
- io.puts " ordered/searchable descent; COMPARABLE additionally licenses"
5491
- io.puts " stripping a Face query for direct comparison (see the query"
5492
- io.puts " gate below). A unit-bearing Face (CATime) is ORDERABLE"
5493
- io.puts " but not COMPARABLE, so its query is reconciled by to_comparable"
5494
- io.puts " -- no per-Face Ruby override needed. */"
5619
+ emit_lazy_evaluated(io)
5620
+ io.puts " /* Face gate (self): a Face reference is searched on its numeric"
5621
+ io.puts " storage, which ORDERABLE licenses; the query gate below brings"
5622
+ io.puts " the reference and the query into one space and descends. */"
5495
5623
  io.puts " if ( ca_is_face(src) ) {"
5496
- io.puts " if ( ! ca_test_flag(src, CA_FLAG_FACE_ORDERABLE_STORAGE) ) {"
5497
- io.puts %Q[ rb_raise(rb_eArgError,]
5498
- io.puts %Q[ "#{name}_ki: Face-typed input (%s) is not orderable "]
5499
- io.puts %Q[ "by storage; use ca.parent to descend to storage",]
5500
- io.puts %Q[ rb_obj_classname(self));]
5501
- io.puts " }"
5624
+ io.puts %Q[ ca_face_require_orderable(self, ca_calling_method_name(), "order");]
5502
5625
  io.puts " self_was_face = 1;"
5503
- io.puts " self_face_comparable = ca_test_flag(src, CA_FLAG_FACE_COMPARABLE_STORAGE);"
5504
- io.puts " self = rb_ca_strip_face_value(self);"
5505
- io.puts " GetCArray(self, src);"
5506
5626
  io.puts " }"
5507
5627
 
5508
5628
  if has_eps
@@ -5514,39 +5634,26 @@ module MkKernel
5514
5634
  io.puts " VALUE raxis = argv[1];"
5515
5635
  io.puts " VALUE reps = (argc >= 3) ? argv[2] : Qnil;"
5516
5636
  end
5517
- io.puts " /* Query gate (generic, PROPOSAL_FACE_ORDERING_GATE + reference"
5518
- io.puts " flip PROPOSAL_TO_COMPARABLE_RECEIVER_FLIP): the reference Face"
5519
- io.puts " reconciles the query into its own space. A COMPARABLE self is"
5520
- io.puts " directly comparable -> strip a Face query, take a plain query"
5521
- io.puts " as-is. A non-COMPARABLE Face reference calls its own"
5522
- io.puts " to_comparable(query) for ANY query type (Face CArray, our"
5523
- io.puts " Scalar, a Ruby Time / DateTime, ...), then strips; the"
5524
- io.puts " reference raises if it cannot reconcile the query. Putting the"
5525
- io.puts " conversion on the reference (always one of our classes) means a"
5526
- io.puts " foreign query (a core Time) needs no monkey-patch, and a new"
5527
- io.puts " Face author writes one to_comparable, not one per query type. */"
5528
- io.puts " {"
5529
- io.puts " int rval_is_face = 0;"
5530
- io.puts " if ( rb_obj_is_kind_of(rval, rb_cCArray) ) {"
5531
- io.puts " CArray *rv_ca;"
5532
- io.puts " TypedData_Get_Struct(rval, CArray, &carray_data_type, rv_ca);"
5533
- io.puts " rval_is_face = ca_is_face(rv_ca);"
5534
- io.puts " }"
5535
- io.puts " if ( self_face_comparable ) {"
5536
- io.puts " if ( rval_is_face ) {"
5537
- io.puts %Q[ rval = ca_face_operand_descend(rval, "#{name}_ki");]
5538
- io.puts " }"
5539
- io.puts " } else if ( self_was_face ) {"
5540
- io.puts " if ( rb_respond_to(self_ref, rb_intern(\"to_comparable\")) ) {"
5541
- io.puts " rval = rb_funcall(self_ref, rb_intern(\"to_comparable\"), 1, rval);"
5542
- io.puts " rval = rb_ca_strip_face_value(rval);"
5543
- io.puts " } else {"
5544
- io.puts %Q[ rb_raise(rb_eArgError,]
5545
- io.puts %Q[ "#{name}_ki: non-comparable Face reference (%s) has no "]
5546
- io.puts %Q[ "to_comparable to reconcile the query; use ca.parent "]
5547
- io.puts %Q[ "to search the hidden storage explicitly",]
5548
- io.puts %Q[ rb_obj_classname(self_ref));]
5549
- io.puts " }"
5637
+ io.puts " /* Query gate: the reference Face and the query are brought into one"
5638
+ io.puts " space (ca_face_reconcile_pair), then the reference descends to"
5639
+ io.puts " its storage. */"
5640
+ io.puts " if ( self_was_face ) {"
5641
+ io.puts " volatile VALUE ref = self, query = rval;"
5642
+ io.puts " ca_face_reconcile_pair(&ref, &query, ca_calling_method_name());"
5643
+ io.puts " rval = query;"
5644
+ io.puts " self = rb_ca_strip_face_value(ref);"
5645
+ io.puts " GetCArray(self, src);"
5646
+ io.puts " }"
5647
+
5648
+ # The query and the reference are compared in the type they match in,
5649
+ # as `a.eq(q)` compares (ca_query_match_type): the reference is read
5650
+ # through a cast view of that type, so the query is never cut to the
5651
+ # reference's type.
5652
+ io.puts " if ( ca_query_match_applies(src, rval) ) {"
5653
+ io.puts " int8_t qt = ca_query_match_type(src->data_type, rval);"
5654
+ io.puts " if ( qt != src->data_type ) {"
5655
+ io.puts " self = rb_ca_wrap_readonly(self, INT2NUM(qt));"
5656
+ io.puts " GetCArray(self, src);"
5550
5657
  io.puts " }"
5551
5658
  io.puts " }"
5552
5659
 
@@ -5554,24 +5661,21 @@ module MkKernel
5554
5661
  when :raise
5555
5662
  io.puts " /* mask_self: :raise -- global reject if self has any masked element. */"
5556
5663
  io.puts " if ( ca_is_any_masked(src) ) {"
5557
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: self should not have any masked elements");]
5664
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: self should not have any masked elements", ca_calling_method_name());]
5558
5665
  io.puts " }"
5559
5666
  when :skip, :ignore
5560
5667
  # body handles mask_in or ignores it
5561
5668
  end
5562
5669
 
5563
- io.puts " int axis = NUM2INT(raxis);"
5564
- io.puts " if ( axis < 0 ) axis += src->ndim;"
5565
- io.puts " if ( axis < 0 || axis >= src->ndim ) {"
5566
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(raxis), src->ndim);]
5567
- io.puts " }"
5670
+ io.puts " int axis = ca_normalize_axis_arg(raxis, src->ndim, NULL);"
5568
5671
  io.puts " switch ( src->data_type ) {"
5569
5672
  k[:source].each do |s|
5570
5673
  si = DTYPES[s]
5571
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, rval, axis#{extra_arg});"
5674
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, rval, axis#{extra_arg}));"
5572
5675
  end
5573
5676
  io.puts " default:"
5574
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
5677
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
5678
+ io.puts %Q[ rb_raise(rb_eCADataTypeError, "%s: source data_type :%s not supported (expected one of: #{MkKernel.type_names(k[:source])})", ca_calling_method_name(), ca_type_name[src->data_type]);]
5575
5679
  io.puts " }"
5576
5680
  io.puts " return Qnil; /* unreachable */"
5577
5681
  io.puts "}"
@@ -5821,11 +5925,15 @@ module MkKernel
5821
5925
  int8_t in_dt;
5822
5926
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca);
5823
5927
  in_dt = ca->data_type;
5928
+ if ( ca_monop_#{name}_out_data_type[in_dt] <= CA_NONE ) {
5929
+ rb_raise(rb_eCADataTypeError,
5930
+ "#{name}!: source data_type :%s not supported", ca_type_name[in_dt]);
5931
+ }
5824
5932
  if ( ca_monop_#{name}_out_data_type[in_dt] != in_dt ) {
5825
- rb_raise(rb_eRuntimeError,
5826
- "#{name}!: in-place form is not available when output "
5827
- "data_type differs from input (input data_type %d -> output data_type %d); "
5828
- "use non-bang form", in_dt, ca_monop_#{name}_out_data_type[in_dt]);
5933
+ rb_raise(rb_eCADataTypeError,
5934
+ "#{name}!: #{name} of :%s is :%s, which cannot be written back in place; "
5935
+ "use #{name}", ca_type_name[in_dt],
5936
+ ca_type_name[ca_monop_#{name}_out_data_type[in_dt]]);
5829
5937
  }
5830
5938
  return rb_ca_call_monop_bang(self, ca_monop_#{name});
5831
5939
  }
@@ -6079,6 +6187,10 @@ module MkKernel
6079
6187
  io.print <<~C
6080
6188
  VALUE rb_ca_#{name} (VALUE self, VALUE other2, VALUE other3)
6081
6189
  { return rb_ca_call_triop(self, other2, other3, ca_triop_#{name}); }
6190
+ C
6191
+ # bang: false kernels get no bang method, so no bang entry either.
6192
+ return if k[:bang] == false
6193
+ io.print <<~C
6082
6194
 
6083
6195
  VALUE rb_ca_#{name}_bang (VALUE self, VALUE other2, VALUE other3)
6084
6196
  { return rb_ca_call_triop_bang(self, other2, other3, ca_triop_#{name}); }
@@ -6350,15 +6462,58 @@ module MkKernel
6350
6462
  # caller substitutes its own operands. Object-lane bodies are left out,
6351
6463
  # since they call back into the interpreter and cannot be compiled apart
6352
6464
  # from it.
6465
+ #
6466
+ # A comparison is keyed by the data type it compares, not by what it
6467
+ # answers, which is always boolean.
6353
6468
  # ---------------------------------------------------------------------
6354
6469
 
6355
6470
  BODY_TABLE_DTYPES = %i[i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6356
6471
  bool cmplx64 cmplx128].freeze
6357
6472
 
6473
+ # Kernels written outside this generator that a lazy expression can still
6474
+ # hold. A Float or Complex array to an Integer power is CABinOp's ipow
6475
+ # (ext/ca_op_ipower.c, which applies op_powi_<type> cell by cell); its
6476
+ # body is that call.
6477
+ EXTRA_BODY_ROWS = %w[float32 float64 cmplx64 cmplx128].map { |t|
6478
+ ["binop", "ipow", t, "(#3) = op_powi_<type>((#1), (#2));"]
6479
+ }.freeze
6480
+
6481
+ # The helpers the bodies call that are not in the C standard library:
6482
+ # the part of ca_op_powi.h between its markers, as it stands.
6483
+ def self.kernel_helpers
6484
+ text = File.read(File.join(__dir__, "ca_op_powi.h"))
6485
+ text[%r{/\* BEGIN kernel helpers \*/\n(.*)/\* END kernel helpers \*/}m, 1] or
6486
+ raise "ca_op_powi.h: kernel helper markers not found"
6487
+ end
6488
+
6489
+ # A lazy cast node (CAMonOp with op_id CAST_BASE + target) converts cell
6490
+ # by cell through ca_cast_block, which for a numeric target is a plain C
6491
+ # conversion (ext/carray_cast_func.rb). It is keyed as cast_<target> at
6492
+ # the source's data type. Left out: a boolean target, which checks its
6493
+ # input and raises, and a float or complex source to an integer target,
6494
+ # where an out-of-range value or NaN has no defined C result for a
6495
+ # second compilation to agree on. An int64 or uint64 source to float32
6496
+ # or cmplx64 goes through double, as ca_cast_block does.
6497
+ CAST_SOURCES = %i[bool i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6498
+ cmplx64 cmplx128].freeze
6499
+ CAST_TARGETS = %i[i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6500
+ cmplx64 cmplx128].freeze
6501
+ CAST_INEXACT = %i[f32 f64 cmplx64 cmplx128].freeze
6502
+
6503
+ def self.cast_body_rows
6504
+ CAST_SOURCES.product(CAST_TARGETS).filter_map { |src, dst|
6505
+ next if CAST_INEXACT.include?(src) && !CAST_INEXACT.include?(dst)
6506
+ name = DTYPES[dst][:ca].sub(/\ACA_/, "").downcase
6507
+ via = ( %i[i64 u64].include?(src) && %i[f32 cmplx64].include?(dst) ) ? "(double)" : ""
6508
+ ["monop", "cast_#{name}", DTYPES[src][:ca].sub(/\ACA_/, "").downcase,
6509
+ "(#2) = (#{DTYPES[dst][:c]})#{via}(#1);"]
6510
+ }
6511
+ end
6512
+
6358
6513
  def self.body_table_rows
6359
- rows = []
6514
+ rows = EXTRA_BODY_ROWS.map(&:dup) + cast_body_rows
6360
6515
  KERNELS.each do |k|
6361
- next unless %i[monop binop triop].include?(k[:kind])
6516
+ next unless %i[monop binop triop moncmp bincmp].include?(k[:kind])
6362
6517
  BODY_TABLE_DTYPES.each do |src|
6363
6518
  body = monop_expr_for(k, src)
6364
6519
  next unless body.is_a?(String)
@@ -6370,8 +6525,10 @@ module MkKernel
6370
6525
  rows
6371
6526
  end
6372
6527
 
6528
+ # The block forms keep gsub from reading the backslashes in the
6529
+ # replacement as references to the match.
6373
6530
  def self.c_string_literal(text)
6374
- '"' + text.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", '\\n') + '"'
6531
+ '"' + text.gsub("\\") { "\\\\" }.gsub('"') { '\\"' }.gsub("\n") { '\\n' } + '"'
6375
6532
  end
6376
6533
 
6377
6534
  def self.emit_kernel_bodies(io)
@@ -6420,6 +6577,17 @@ module MkKernel
6420
6577
  }
6421
6578
  return Qnil;
6422
6579
  }
6580
+
6581
+ /* CArray.__kernel_helpers__ -> String: the C a body above may call
6582
+ beyond the standard library, for the caller to put ahead of it. */
6583
+ static const char ca_kernel_helpers[] =
6584
+ #{c_string_literal(kernel_helpers)};
6585
+
6586
+ static VALUE
6587
+ rb_ca_s_kernel_helpers (VALUE klass)
6588
+ {
6589
+ return rb_str_new_cstr(ca_kernel_helpers);
6590
+ }
6423
6591
  C
6424
6592
  end
6425
6593
 
@@ -6441,6 +6609,8 @@ module MkKernel
6441
6609
  end
6442
6610
  io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6443
6611
  io.puts " rb_ca_s_kernel_body, 3);"
6612
+ io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_helpers__\","
6613
+ io.puts " rb_ca_s_kernel_helpers, 0);"
6444
6614
  io.puts "}"
6445
6615
  end
6446
6616
 
@@ -6455,6 +6625,8 @@ module MkKernel
6455
6625
  KERNELS.each { |k| emit_init_line(io, k) }
6456
6626
  io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6457
6627
  io.puts " rb_ca_s_kernel_body, 3);"
6628
+ io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_helpers__\","
6629
+ io.puts " rb_ca_s_kernel_helpers, 0);"
6458
6630
  io.puts "}"
6459
6631
  end
6460
6632
 
@@ -6515,7 +6687,6 @@ module MkKernel
6515
6687
  end
6516
6688
  arity = case k[:kind]
6517
6689
  when :reduce then -1 # axis: kwarg + optional weights/value positional
6518
- when :map then 0 # no args
6519
6690
  when :scan then -1 # axis: kwarg (Integer required)
6520
6691
  when :sort
6521
6692
  # SO.3+ (rev8): :partition algorithm takes (axis, kth) = arity 2
@@ -6540,12 +6711,16 @@ module MkKernel
6540
6711
  # reduce: default false (E.8 retire), opt-in via bind_ruby: true
6541
6712
  # (used by CF.1 count_equal smoke + future kernel testing)
6542
6713
  # sort: default true, opt-out via bind_ruby: false
6543
- # map/scan: always bound (no opt-out)
6714
+ # scan: bound as <name>_ki, or only privately as <name> (private:)
6544
6715
  bind = case k[:kind]
6545
6716
  when :reduce then k[:bind_ruby] == true
6546
6717
  when :sort then k[:bind_ruby] != false
6718
+ when :scan then ! k[:private]
6547
6719
  else true
6548
6720
  end
6721
+ if k[:kind] == :scan && k[:private]
6722
+ io.puts %Q[ rb_define_private_method(rb_cCArray, "#{k[:name]}", rb_ca_#{k[:name]}_ki, #{arity});]
6723
+ end
6549
6724
  # Smoke kernels (e.g. count_equal) expose their _ki binding only
6550
6725
  # in dev builds, gated by CARRAY_DEV_BUILD. See
6551
6726
  # devel/PROPOSAL_SMOKE_DEV_BUILD_GATE.md.
@@ -6558,7 +6733,7 @@ module MkKernel
6558
6733
  # directly without a Ruby wrapper. This is paired with
6559
6734
  # axis_default: :flatten because both express the legacy-compat
6560
6735
  # convenience of these specific scan kernels.
6561
- if k[:kind] == :scan && k[:axis_default] == :flatten
6736
+ if k[:kind] == :scan && k[:axis_default] == :flatten && ! k[:private]
6562
6737
  io.puts %Q[ rb_define_method(rb_cCArray, "#{k[:name]}", rb_ca_#{k[:name]}_ki, #{arity});]
6563
6738
  end
6564
6739
  # Phase E rewire: when this kernel replaces a legacy CArray method,
@@ -6642,7 +6817,7 @@ MkKernel.reduce :min,
6642
6817
  # bool acc is u64 (numeric output); cast the boolean8_t
6643
6818
  # load to match and avoid a signed/unsigned compare.
6644
6819
  bool: "acc = ((uint64_t) v < acc) ? (uint64_t) v : acc",
6645
- object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v;' },
6820
+ object: 'if (acc == Qundef || ca_obj_extremum_replaces(v, acc, rb_intern("<"))) acc = v;' },
6646
6821
  reduction_kind: :min, # SL.1.2
6647
6822
  all_nan_result: :nan,
6648
6823
  # CA_FIXLEN: memcmp lexicographic min (byte order == the fixlen sort
@@ -6655,8 +6830,8 @@ MkKernel.reduce :min,
6655
6830
  fallback: :raise,
6656
6831
  mask_policy: :min_count, # Phase E
6657
6832
  # An ORDERABLE Face descends to numeric storage, then the result is
6658
- # re-lifted into the Face (scalar via storage_to_scalar, per-axis via
6659
- # rb_ca_face_template). This replaces per-Face lib overrides.
6833
+ # re-lifted into the Face (scalar via storage_to_element, per-axis via
6834
+ # ca_face_relift_entity). This replaces per-Face lib overrides.
6660
6835
  face_gate: :relift,
6661
6836
  public_method: true # Phase E: rebind "min" -> rb_ca_min_ki
6662
6837
 
@@ -6667,7 +6842,7 @@ MkKernel.reduce :max,
6667
6842
  # 0/1); the boolean-returning twin is `any` (= bool max).
6668
6843
  reduce: { numeric: "acc = (v > acc) ? v : acc",
6669
6844
  bool: "acc = ((uint64_t) v > acc) ? (uint64_t) v : acc",
6670
- object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v;' },
6845
+ object: 'if (acc == Qundef || ca_obj_extremum_replaces(v, acc, rb_intern(">"))) acc = v;' },
6671
6846
  reduction_kind: :max, # SL.1.2
6672
6847
  all_nan_result: :nan,
6673
6848
  # CA_FIXLEN: memcmp lexicographic max (byte order == the fixlen sort order).
@@ -6680,20 +6855,6 @@ MkKernel.reduce :max,
6680
6855
  face_gate: :relift,
6681
6856
  public_method: true # Phase E: rebind "max" -> rb_ca_max_ki
6682
6857
 
6683
- # count_ki — count unmasked elements per slab. The macro skips masked
6684
- # cells, so REDUCE fires only on contributing cells; `acc += 1` ignores
6685
- # the value entirely (cast to void to silence unused-variable warnings).
6686
- # When the input has no mask the macro takes a faster path that loads
6687
- # `v` but our REDUCE doesn't use it -- compiler optimises the load away.
6688
- MkKernel.reduce :count,
6689
- init: "0",
6690
- reduce: "(void)v; acc += 1",
6691
- reduction_kind: :plus, # SL.1.4
6692
- source: MkKernel::ALL_NUMERIC,
6693
- output: :i64,
6694
- ruby_scalar: :LL2NUM,
6695
- fallback: :wrap_to_f64
6696
-
6697
6858
  # all / any reduce: bool input -> bool output, axis-aware. Replaces the
6698
6859
  # flat-only hand-written rb_ca_all_equal_p / rb_ca_any_equal_p /
6699
6860
  # all_close? / any_close? / all_equiv? / any_equiv? in carray_stat.c
@@ -6710,6 +6871,15 @@ MkKernel.reduce :count,
6710
6871
  # modern CPUs. For dramatically lopsided inputs (= mostly-false `all`
6711
6872
  # or mostly-true `any`) a profile-driven early-break variant can be
6712
6873
  # added later as a separate kernel.
6874
+ MkKernel.header_block <<~C
6875
+ /* The square root of a variance: an ε-negative from rounding becomes 0,
6876
+ a NaN stays NaN (fmax(NaN, 0.0) is 0.0). */
6877
+ #ifndef CARRAY_SQRT_CLAMP_DEFINED
6878
+ #define CARRAY_SQRT_CLAMP_DEFINED
6879
+ static inline double ca_sqrt_clamp (double v) { return sqrt(v < 0.0 ? 0.0 : v); }
6880
+ #endif
6881
+ C
6882
+
6713
6883
  MkKernel.header_block <<~C
6714
6884
  /* BOOL2VAL: bool -> Ruby (Qtrue/Qfalse). Used as ruby_scalar wrapper
6715
6885
  for the all/any flat-reduction Ruby surface so `a.all` / `a.any`
@@ -6893,7 +7063,7 @@ MkKernel.reduce :minmax,
6893
7063
  # CA_OBJECT minmax via Qundef sentinel + first-cell-init.
6894
7064
  # One sentinel guards both lo + hi (they go Qundef
6895
7065
  # together on init, get set together on first reduce).
6896
- object: 'if (lo == Qundef) { lo = v; hi = v; } else { if (RTEST(rb_funcall(v, rb_intern("<"), 1, lo))) lo = v; if (RTEST(rb_funcall(v, rb_intern(">"), 1, hi))) hi = v; }' },
7066
+ object: 'if (lo == Qundef) { lo = v; hi = v; } else { if (ca_obj_extremum_replaces(v, lo, rb_intern("<"))) lo = v; if (ca_obj_extremum_replaces(v, hi, rb_intern(">"))) hi = v; }' },
6897
7067
  outputs: 2,
6898
7068
  all_nan_result: :nan,
6899
7069
  finish: { min: "lo", max: "hi" },
@@ -6904,6 +7074,7 @@ MkKernel.reduce :minmax,
6904
7074
  ruby_scalar: :auto,
6905
7075
  fallback: :raise,
6906
7076
  mask_policy: :min_count, # FM.1.5: match existing min / max behaviour
7077
+ face_gate: :relift,
6907
7078
  public_method: :minmax # bind public name `minmax` -> rb_ca_minmax_ki
6908
7079
  # (= surface-name override, no actual legacy)
6909
7080
 
@@ -6925,14 +7096,19 @@ MkKernel.reduce :minmax,
6925
7096
  # replace the legacy `min_addr` / `max_addr` retired in E.7 stat_proc
6926
7097
  # retire (commit f5c7ecd). 3.0 breaking: name change from `*_addr` to
6927
7098
  # `*_index` is intentional.
7099
+ #
7100
+ # best_i starts at -1 (no cell taken yet), so the first unmasked non-NaN
7101
+ # cell is taken even when it equals the starting limit (an all-+Infinity
7102
+ # or all-INT_MAX fiber). `v == v` keeps a NaN from being taken first; a
7103
+ # fiber of NaN only is left to the all-NaN fixup (UNDEF).
6928
7104
 
6929
7105
  MkKernel.reduce :argmin,
6930
7106
  state: { best_v: :load_type, best_i: :int64_t },
6931
7107
  init: { best_v: { numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qundef" },
6932
- best_i: "0" },
6933
- reduce: { numeric: "if (v < best_v) { best_v = v; best_i = idx; }",
6934
- bool: "if (v < best_v) { best_v = v; best_i = idx; }",
6935
- object: 'if (best_v == Qundef) { best_v = v; best_i = idx; } else if (RTEST(rb_funcall(v, rb_intern("<"), 1, best_v))) { best_v = v; best_i = idx; }' },
7108
+ best_i: "-1" },
7109
+ reduce: { numeric: "if (v < best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7110
+ bool: "if (v < best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7111
+ object: 'if (!CA_OBJ_ISNAN(v) && (best_v == Qundef || ca_obj_cmp(v, best_v) < 0)) { best_v = v; best_i = idx; }' },
6936
7112
  finish: "best_i",
6937
7113
  # CA_FIXLEN: position of the memcmp-lexicographic min (first-wins on ties).
6938
7114
  fixlen: :argmin,
@@ -6950,10 +7126,10 @@ MkKernel.reduce :argmin,
6950
7126
  MkKernel.reduce :argmax,
6951
7127
  state: { best_v: :load_type, best_i: :int64_t },
6952
7128
  init: { best_v: { numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qundef" },
6953
- best_i: "0" },
6954
- reduce: { numeric: "if (v > best_v) { best_v = v; best_i = idx; }",
6955
- bool: "if (v > best_v) { best_v = v; best_i = idx; }",
6956
- object: 'if (best_v == Qundef) { best_v = v; best_i = idx; } else if (RTEST(rb_funcall(v, rb_intern(">"), 1, best_v))) { best_v = v; best_i = idx; }' },
7129
+ best_i: "-1" },
7130
+ reduce: { numeric: "if (v > best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7131
+ bool: "if (v > best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7132
+ object: 'if (!CA_OBJ_ISNAN(v) && (best_v == Qundef || ca_obj_cmp(v, best_v) > 0)) { best_v = v; best_i = idx; }' },
6957
7133
  finish: "best_i",
6958
7134
  # CA_FIXLEN: position of the memcmp-lexicographic max (first-wins on ties).
6959
7135
  fixlen: :argmax,
@@ -6995,10 +7171,10 @@ MkKernel.reduce :argmax,
6995
7171
  MkKernel.reduce :argmin_addr,
6996
7172
  state: { best_v: :load_type, best_i: :int64_t },
6997
7173
  init: { best_v: { numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qundef" },
6998
- best_i: "0" },
6999
- reduce: { numeric: "if (v < best_v) { best_v = v; best_i = idx; }",
7000
- bool: "if (v < best_v) { best_v = v; best_i = idx; }",
7001
- object: 'if (best_v == Qundef) { best_v = v; best_i = idx; } else if (RTEST(rb_funcall(v, rb_intern("<"), 1, best_v))) { best_v = v; best_i = idx; }' },
7174
+ best_i: "-1" },
7175
+ reduce: { numeric: "if (v < best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7176
+ bool: "if (v < best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7177
+ object: 'if (!CA_OBJ_ISNAN(v) && (best_v == Qundef || ca_obj_cmp(v, best_v) < 0)) { best_v = v; best_i = idx; }' },
7002
7178
  finish: "best_i",
7003
7179
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7004
7180
  output: :i64,
@@ -7013,10 +7189,10 @@ MkKernel.reduce :argmin_addr,
7013
7189
  MkKernel.reduce :argmax_addr,
7014
7190
  state: { best_v: :load_type, best_i: :int64_t },
7015
7191
  init: { best_v: { numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qundef" },
7016
- best_i: "0" },
7017
- reduce: { numeric: "if (v > best_v) { best_v = v; best_i = idx; }",
7018
- bool: "if (v > best_v) { best_v = v; best_i = idx; }",
7019
- object: 'if (best_v == Qundef) { best_v = v; best_i = idx; } else if (RTEST(rb_funcall(v, rb_intern(">"), 1, best_v))) { best_v = v; best_i = idx; }' },
7192
+ best_i: "-1" },
7193
+ reduce: { numeric: "if (v > best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7194
+ bool: "if (v > best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7195
+ object: 'if (!CA_OBJ_ISNAN(v) && (best_v == Qundef || ca_obj_cmp(v, best_v) > 0)) { best_v = v; best_i = idx; }' },
7020
7196
  finish: "best_i",
7021
7197
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7022
7198
  output: :i64,
@@ -7028,36 +7204,6 @@ MkKernel.reduce :argmax_addr,
7028
7204
  all_nan_result: :undef,
7029
7205
  public_method: :max_addr
7030
7206
 
7031
- # ---- mask_policy demos ------------------------------------------------
7032
-
7033
- # sum_strict_ki -- :strict policy. Any masked input cell makes the
7034
- # output cell UNDEF. Use case: "this is sensor data, any gap invalidates
7035
- # the aggregate" (= NaN-propagation semantics). Sibling of
7036
- # sum_ki which silently treats masked as 0.
7037
- MkKernel.reduce :sum_strict,
7038
- init: "0",
7039
- reduce: "acc += v",
7040
- source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7041
- output: { numeric: :f64, complex: :cmplx128 },
7042
- ruby_scalar: :auto,
7043
- fallback: :raise,
7044
- mask_policy: :strict
7045
-
7046
- # mean_safe_ki -- :all_masked policy. Output UNDEF only when every
7047
- # input cell is masked, otherwise mean over visible cells. Use case:
7048
- # "tolerate gaps but flag if nothing is observed". Sibling of mean_ki
7049
- # which returns 0.0 for all-masked slabs.
7050
- MkKernel.reduce :mean_safe,
7051
- state: { acc: :acc_type, cnt: :int64_t },
7052
- init: { acc: "0", cnt: "0" },
7053
- reduce: "(acc += v, cnt++)",
7054
- finish: "cnt ? acc / (T_OUT) cnt : 0",
7055
- source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7056
- output: { numeric: :f64, complex: :cmplx128 },
7057
- ruby_scalar: :auto,
7058
- fallback: :raise,
7059
- mask_policy: :all_masked
7060
-
7061
7207
  # accumulate -- sum that preserves input data_type (legacy rb_ca_accum,
7062
7208
  # retired in E.7). Same machinery as sum_ki but output: :preserve so
7063
7209
  # int8 stays int8 (will overflow on long arrays = legacy behaviour).
@@ -7153,10 +7299,11 @@ MkKernel.reduce :count_false,
7153
7299
 
7154
7300
  MkKernel.reduce :count_equal,
7155
7301
  init: "0",
7156
- # The object lane compares with rb_equal (= Ruby ==), not the C ==, which
7157
- # on two VALUEs would ask whether they are the same object.
7302
+ # The object lane compares with ca_obj_equal (= Ruby ==, a NaN equal to
7303
+ # nothing), not the C ==, which on two VALUEs would ask whether they are
7304
+ # the same object.
7158
7305
  reduce: { numeric: "if (v == value_arg) acc += 1",
7159
- object: "if (RTEST(rb_equal(v, value_arg))) acc += 1" },
7306
+ object: "if (ca_obj_equal(v, value_arg)) acc += 1" },
7160
7307
  reduction_kind: :plus, # SL.1.4 (conditional predication; clang predicates safely under reduction(+:acc))
7161
7308
  source: MkKernel::ALL_NUMERIC + [:object, :fixlen],
7162
7309
  fixlen: :count_equal, # bespoke memcmp walk (a blob has no scalar C type)
@@ -7210,7 +7357,7 @@ MkKernel.reduce :wsum,
7210
7357
  shape: :rev5_strict, mask: :overlay },
7211
7358
  mask_policy: :min_count,
7212
7359
  identity_on_empty: true, # ERI.2: empty / all-masked -> 0.0 (weighted sum identity)
7213
- object_escape: :__wsum_object__, # CA_OBJECT -> (w*self).sum (runtime.rb)
7360
+ object_escape: :wsum_object, # CA_OBJECT -> (w*self).sum (runtime.rb)
7214
7361
  bind_ruby: true,
7215
7362
  public_method: true
7216
7363
 
@@ -7227,7 +7374,7 @@ MkKernel.reduce :wsum,
7227
7374
  # - min_count semantic "max masked" -> "min valid required" (Phase E)
7228
7375
  # - complex (CMPLX64/128) dropped (= ALL_NUMERIC + :raise)
7229
7376
  # - per-axis support gained
7230
- # CA_OBJECT is handled via object_escape (runtime.rb __wmean_object__), not
7377
+ # CA_OBJECT is handled via object_escape (runtime.rb wmean_object), not
7231
7378
  # the numeric kernel: weight-coerce vs object arithmetic is cleaner as a
7232
7379
  # (w*self).sum / Σw composition than a dedicated :object kernel branch.
7233
7380
 
@@ -7244,73 +7391,10 @@ MkKernel.reduce :wmean,
7244
7391
  array_arg: { name: :weights, data_type: :promote,
7245
7392
  shape: :rev5_strict, mask: :overlay },
7246
7393
  mask_policy: :min_count,
7247
- object_escape: :__wmean_object__, # CA_OBJECT -> weighted mean via runtime.rb
7394
+ object_escape: :wmean_object, # CA_OBJECT -> weighted mean via runtime.rb
7248
7395
  bind_ruby: true,
7249
7396
  public_method: true
7250
7397
 
7251
- # ---- map kernels (element-wise transforms) ----------------------------
7252
-
7253
- # Float-output transcendentals -- input widens to double inside the
7254
- # expression, output is float64. The :wrap_to_f64 fallback handles
7255
- # any other numeric data_type (int16, bool, etc.) by promoting to f64 first.
7256
-
7257
- MkKernel.map :sqrt,
7258
- source: MkKernel::ALL_NUMERIC,
7259
- output: :f64,
7260
- expr: "r = sqrt((double) v)",
7261
- fallback: :wrap_to_f64
7262
-
7263
- MkKernel.map :sin,
7264
- source: MkKernel::ALL_NUMERIC,
7265
- output: :f64,
7266
- expr: "r = sin((double) v)",
7267
- fallback: :wrap_to_f64
7268
-
7269
- MkKernel.map :cos,
7270
- source: MkKernel::ALL_NUMERIC,
7271
- output: :f64,
7272
- expr: "r = cos((double) v)",
7273
- fallback: :wrap_to_f64
7274
-
7275
- MkKernel.map :exp,
7276
- source: MkKernel::ALL_NUMERIC,
7277
- output: :f64,
7278
- expr: "r = exp((double) v)",
7279
- fallback: :wrap_to_f64
7280
-
7281
- MkKernel.map :log,
7282
- source: MkKernel::ALL_NUMERIC,
7283
- output: :f64,
7284
- expr: "r = log((double) v)",
7285
- fallback: :wrap_to_f64
7286
-
7287
- # Source-data_type-preserving transforms -- arithmetic ops that don't need
7288
- # floating-point. Fallback raises rather than silently widening.
7289
-
7290
- MkKernel.map :square,
7291
- source: MkKernel::ALL_NUMERIC,
7292
- output: :preserve,
7293
- expr: "r = v * v",
7294
- fallback: :raise
7295
-
7296
- MkKernel.map :abs,
7297
- # Signed-only: `v < 0` is always false on unsigned types (compiler
7298
- # warning + identity result). Use `negate` semantics for unsigned
7299
- # only if you genuinely want wrap-around.
7300
- source: MkKernel::SIGNED_NUMERIC,
7301
- output: :preserve,
7302
- expr: "r = (v < 0) ? -v : v",
7303
- fallback: :raise
7304
-
7305
- MkKernel.map :negate,
7306
- # Signed-only: -v on unsigned types wraps (C semantics), which is
7307
- # mathematically wrong for "negate". If you want bitwise inversion
7308
- # on unsigned, use a different kernel (~v).
7309
- source: MkKernel::SIGNED_NUMERIC,
7310
- output: :preserve,
7311
- expr: "r = -v",
7312
- fallback: :raise
7313
-
7314
7398
  # ---- scan kernels (cumulative / prefix scan along one axis) ----------
7315
7399
 
7316
7400
  # cumsum / cumprod: data_type-conditional output (numeric -> f64 widening,
@@ -7332,6 +7416,18 @@ MkKernel.scan :cumsum,
7332
7416
  fallback: :raise,
7333
7417
  axis_default: :flatten
7334
7418
 
7419
+ # The running sum in the source's own integer type, added without a range
7420
+ # check (int64 wraps, as accumulate does). CATimedelta#cumsum reads it
7421
+ # for an exact running sum of durations.
7422
+ MkKernel.scan :cumsum_in_own_type,
7423
+ source: MkKernel::INT_DTYPES,
7424
+ output: :preserve,
7425
+ init: "0",
7426
+ step: "acc += v; r = acc",
7427
+ fallback: :raise,
7428
+ axis_default: :flatten,
7429
+ private: true
7430
+
7335
7431
  MkKernel.scan :cumprod,
7336
7432
  source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES + [:bool, :object],
7337
7433
  output: { numeric: :f64, complex: :cmplx128, bool: :u64, object: :object },
@@ -7360,13 +7456,14 @@ MkKernel.scan :cumprod,
7360
7456
  # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7361
7457
  # and keep the limit init and the plain compare.
7362
7458
  MkKernel.scan :cummax,
7459
+ face_gate: :relift,
7363
7460
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7364
7461
  output: { bool: :u64, default: :preserve },
7365
7462
  init: { float: "NAN", numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qnil" },
7366
7463
  step: { float: "acc = fmax(acc, v); r = acc",
7367
7464
  numeric: "if (v > acc) acc = v; r = acc",
7368
7465
  bool: "if ((uint64_t) v > acc) acc = v; r = acc",
7369
- object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v; r = acc' },
7466
+ object: 'if (acc == Qnil || ca_obj_extremum_replaces(v, acc, rb_intern(">"))) acc = v; r = acc' },
7370
7467
  fallback: :raise,
7371
7468
  axis_default: :flatten,
7372
7469
  empty: :undef
@@ -7379,26 +7476,30 @@ MkKernel.scan :cummax,
7379
7476
  # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7380
7477
  # and keep the limit init and the plain compare.
7381
7478
  MkKernel.scan :cummin,
7479
+ face_gate: :relift,
7382
7480
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7383
7481
  output: { bool: :u64, default: :preserve },
7384
7482
  init: { float: "NAN", numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qnil" },
7385
7483
  step: { float: "acc = fmin(acc, v); r = acc",
7386
7484
  numeric: "if (v < acc) acc = v; r = acc",
7387
7485
  bool: "if ((uint64_t) v < acc) acc = v; r = acc",
7388
- object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v; r = acc' },
7486
+ object: 'if (acc == Qnil || ca_obj_extremum_replaces(v, acc, rb_intern("<"))) acc = v; r = acc' },
7389
7487
  fallback: :raise,
7390
7488
  axis_default: :flatten,
7391
7489
  empty: :undef
7392
7490
 
7393
7491
  # cumcount: running count of unmasked cells. Output data_type = int64.
7394
7492
  # `(void) v` silences unused-variable warning for the unmasked path
7395
- # (compiler DCEs the v load when STEP doesn't reference it).
7493
+ # (compiler DCEs the v load when STEP doesn't reference it). The count
7494
+ # reads only the mask, so a data type outside source: (object, complex,
7495
+ # boolean, fixlen, a Face) is counted off the mask without converting its
7496
+ # values (fallback: :mask_count).
7396
7497
  MkKernel.scan :cumcount,
7397
7498
  source: MkKernel::ALL_NUMERIC,
7398
7499
  output: :i64,
7399
7500
  init: "0",
7400
7501
  step: "(void) v; r = ++acc",
7401
- fallback: :wrap_to_f64,
7502
+ fallback: :mask_count,
7402
7503
  axis_default: :flatten
7403
7504
 
7404
7505
  # uniq_scan: per-axis adjacent-compare scan returning a boolean
@@ -7446,7 +7547,7 @@ MkKernel.sort :sort_index,
7446
7547
  nan_policy: :end,
7447
7548
  fallback: :raise,
7448
7549
  mask_self: :sentinel, # masked cells sort to masked_position: :first/:last (default :last)
7449
- public_method: :sort_index, # binds public sort_index(axis: 0, masked_position: :last) via _kw trampoline
7550
+ public_method: :sort_index, # binds public sort_index(axis: nil, masked_position: :last) via _kw trampoline
7450
7551
  c_callable: true # extern linkage for carray_order.c (nlargest / order family)
7451
7552
 
7452
7553
  # sort_addr: per-axis argsort returning view-flat addresses (=
@@ -7454,7 +7555,7 @@ MkKernel.sort :sort_index,
7454
7555
  # sort_index but the kernel writes view-flat addresses instead of
7455
7556
  # fiber-local indices, so the output can feed directly into
7456
7557
  # ca_remap_new as a CARemap.idx. Internal-only (bind_ruby: false):
7457
- # used by `a.sort(axis: k)` at the C level via rb_ca_sort_addr_ki.
7558
+ # used by `a.sort(axis: k)` at the C level via rb_ca_sort_addr_ki_*_mp.
7458
7559
  # rev3 of PROPOSAL_SORT_AXIS defers `sort_addr(axis: k)` as a public
7459
7560
  # method to a future phase; here we just need the kernel machinery.
7460
7561
  MkKernel.sort :sort_addr,
@@ -7481,7 +7582,7 @@ MkKernel.sort :partition_index,
7481
7582
  fallback: :raise,
7482
7583
  algorithm: :partition,
7483
7584
  mask_self: :sentinel, # masked cells sort to masked_position: :first/:last (default :last)
7484
- public_method: :partition_index, # binds partition_index(kth, axis: 0, masked_position: :last) via _kw trampoline
7585
+ public_method: :partition_index, # binds partition_index(kth, axis: nil, masked_position: :last) via _kw trampoline
7485
7586
  c_callable: true # extern linkage for carray_order.c (nlargest / nsmallest)
7486
7587
 
7487
7588
  # partition_addr: view-flat-address variant of partition_index, parallel
@@ -7523,7 +7624,7 @@ MkKernel.sort :rank_index,
7523
7624
  fallback: :raise,
7524
7625
  algorithm: :rank,
7525
7626
  mask_self: :skip,
7526
- public_method: :rank_index, # binds public rank_index(axis: 0) via _kw trampoline
7627
+ public_method: :rank_index, # binds public rank_index(axis: nil) via _kw trampoline
7527
7628
  c_callable: true # extern linkage for carray_order.c (order method)
7528
7629
 
7529
7630
  # ---- search family ------------------------------------------------------
@@ -7560,7 +7661,7 @@ MkKernel.search :find_value_index,
7560
7661
  result = (ca_size_t) -1;
7561
7662
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7562
7663
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7563
- if ( rb_equal(v, query_val) ) { result = i; break; }
7664
+ if ( ca_obj_equal(v, query_val) ) { result = i; break; }
7564
7665
  }
7565
7666
  C
7566
7667
  fixlen: <<~C,
@@ -7602,7 +7703,6 @@ MkKernel.search :bsearch,
7602
7703
  while ( lo < hi ) {
7603
7704
  ca_size_t mid = lo + (hi - lo) / 2;
7604
7705
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7605
- if ( isnan((double) v) ) { hi = mid; continue; }
7606
7706
  if ( v < query_val ) lo = mid + 1;
7607
7707
  else if ( v > query_val ) hi = mid;
7608
7708
  else { result = mid; break; }
@@ -7611,36 +7711,30 @@ MkKernel.search :bsearch,
7611
7711
  C
7612
7712
  float: <<~C,
7613
7713
  result = (ca_size_t) -1;
7614
- if ( ! isnan((double) query_val) ) {
7714
+ if ( ! isnan((double) query_val) ) { /* a NaN query matches nothing */
7615
7715
  ca_size_t lo = 0;
7616
7716
  ca_size_t hi = slab_n;
7617
7717
  while ( lo < hi ) {
7618
7718
  ca_size_t mid = lo + (hi - lo) / 2;
7619
7719
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7620
- if ( isnan((double) v) ) { hi = mid; continue; }
7621
- if ( v < query_val ) lo = mid + 1;
7622
- else if ( v > query_val ) hi = mid;
7720
+ int c = CA_FLOAT_ORDER_CMP(v, query_val);
7721
+ if ( c < 0 ) lo = mid + 1;
7722
+ else if ( c > 0 ) hi = mid;
7623
7723
  else { result = mid; break; }
7624
7724
  }
7625
7725
  }
7626
7726
  C
7627
7727
  object: <<~C,
7628
- /* CA_OBJECT bsearch: ordering via rb_funcall(<=>). Assumes the
7629
- slab is sorted by the same comparator used to build it. NaN
7630
- guards skipped (= no float-NaN concept for arbitrary objects;
7631
- a Float-cell NaN would raise from <=>). */
7728
+ /* CA_OBJECT bsearch: the object order (ca_obj_order_cmp), which the
7729
+ slab is assumed to be sorted by; a NaN query matches nothing, as in
7730
+ float. */
7632
7731
  result = (ca_size_t) -1;
7633
7732
  ca_size_t lo = 0;
7634
- ca_size_t hi = slab_n;
7733
+ ca_size_t hi = CA_OBJ_ISNAN(query_val) ? 0 : slab_n; /* NaN matches nothing */
7635
7734
  while ( lo < hi ) {
7636
7735
  ca_size_t mid = lo + (hi - lo) / 2;
7637
7736
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7638
- VALUE r = rb_funcall(v, rb_intern("<=>"), 1, query_val);
7639
- if ( NIL_P(r) ) {
7640
- rb_raise(rb_eArgError, "bsearch: comparison of %s with %s failed",
7641
- rb_obj_classname(v), rb_obj_classname(query_val));
7642
- }
7643
- int c = NUM2INT(r);
7737
+ int c = ca_obj_order_cmp(v, query_val);
7644
7738
  if ( c < 0 ) lo = mid + 1;
7645
7739
  else if ( c > 0 ) hi = mid;
7646
7740
  else { result = mid; break; }
@@ -7675,7 +7769,7 @@ MkKernel.search :bsearch,
7675
7769
  #
7676
7770
  # Per-data_type body: int uses exact `v == query_val` (eps ignored); float
7677
7771
  # uses `fabs(v - query_val) <= query_eps` (eps_default = FLT_EPSILON*|val|
7678
- # or DBL_EPSILON*|val|); object uses rb_equal (eps ignored); fixlen uses
7772
+ # or DBL_EPSILON*|val|); object uses ca_obj_equal (eps ignored); fixlen uses
7679
7773
  # memcmp exact match (eps ignored). Sole search implementation: the
7680
7774
  # no-axis (flat) surface routes here via flatten + axis 0
7681
7775
  # (PROPOSAL_SEARCH_SEMANTICS_UNIFY S2/S3; the legacy flat scan was removed).
@@ -7697,18 +7791,18 @@ MkKernel.search :search,
7697
7791
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7698
7792
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7699
7793
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7700
- if ( fabs((double)(v - query_val)) <= query_eps ) { result = i; break; }
7794
+ if ( v == query_val || fabs((double)(v - query_val)) <= query_eps ) { result = i; break; }
7701
7795
  }
7702
7796
  C
7703
7797
  object: <<~C,
7704
- /* CA_OBJECT search: exact equality via rb_equal. eps is silently
7798
+ /* CA_OBJECT search: exact equality via ca_obj_equal. eps is silently
7705
7799
  ignored (= no meaningful tolerance on arbitrary Ruby objects). */
7706
7800
  result = (ca_size_t) -1;
7707
7801
  (void) query_eps;
7708
7802
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7709
7803
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7710
7804
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7711
- if ( rb_equal(v, query_val) ) { result = i; break; }
7805
+ if ( ca_obj_equal(v, query_val) ) { result = i; break; }
7712
7806
  }
7713
7807
  C
7714
7808
  fixlen: <<~C,
@@ -7747,13 +7841,16 @@ MkKernel.search :search_nearest,
7747
7841
  output: :ca_size,
7748
7842
  body: {
7749
7843
  int: <<~C,
7844
+ /* The distance is exact in uint64: a double loses the difference
7845
+ between neighbouring int64 values above 2**53. */
7750
7846
  result = (ca_size_t) -1;
7751
- double best_dist = INFINITY;
7847
+ uint64_t best_dist = 0;
7752
7848
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7753
7849
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7754
7850
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7755
- double dist = fabs((double)v - (double)query_val);
7756
- if ( dist < best_dist ) { best_dist = dist; result = i; }
7851
+ uint64_t dist = ( v >= query_val ) ? (uint64_t) v - (uint64_t) query_val
7852
+ : (uint64_t) query_val - (uint64_t) v;
7853
+ if ( result == (ca_size_t) -1 || dist < best_dist ) { best_dist = dist; result = i; }
7757
7854
  }
7758
7855
  (void) query_eps; /* unused: search_nearest has no eps semantics */
7759
7856
  C
@@ -7763,8 +7860,11 @@ MkKernel.search :search_nearest,
7763
7860
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7764
7861
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7765
7862
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7766
- double dist = fabs((double)v - (double)query_val);
7767
- if ( dist < best_dist ) { best_dist = dist; result = i; }
7863
+ /* An equal value is at distance 0 even when both are infinite
7864
+ (Inf - Inf is NaN); a NaN distance never wins. */
7865
+ double dist = ( v == query_val ) ? 0.0 : fabs((double)v - (double)query_val);
7866
+ if ( dist != dist ) continue;
7867
+ if ( result == (ca_size_t) -1 || dist < best_dist ) { best_dist = dist; result = i; }
7768
7868
  }
7769
7869
  (void) query_eps; /* unused: search_nearest has no eps semantics */
7770
7870
  C
@@ -7783,20 +7883,22 @@ MkKernel.search :search_nearest,
7783
7883
  int nearest_by_distance = rb_respond_to(query_val, nearest_id);
7784
7884
  if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
7785
7885
  rb_raise(rb_eCADataTypeError,
7786
- "search_nearest: nearest needs a distance, and %s is neither a "
7886
+ "%s: nearest needs a distance, and %s is neither a "
7787
7887
  "number nor answers #distance (define one on the stored "
7788
7888
  "objects, or use search / bsearch for an exact match)",
7789
- rb_obj_classname(query_val));
7889
+ ca_calling_method_name(), rb_obj_classname(query_val));
7790
7890
  }
7791
7891
  result = (ca_size_t) -1;
7792
7892
  VALUE best = Qnil;
7793
7893
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7794
7894
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7795
7895
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7796
- VALUE dist = nearest_by_distance
7896
+ VALUE dist = ca_obj_equal(v, query_val) ? INT2FIX(0)
7897
+ : nearest_by_distance
7797
7898
  ? rb_funcall(query_val, nearest_id, 1, v)
7798
7899
  : rb_funcall(rb_funcall(query_val, '-', 1, v),
7799
7900
  rb_intern("abs"), 0);
7901
+ if ( CA_OBJ_ISNAN(dist) ) continue; /* a NaN is no distance, as in float */
7800
7902
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
7801
7903
  best = dist; result = i;
7802
7904
  }
@@ -7834,7 +7936,6 @@ MkKernel.search :bsearch_addr,
7834
7936
  while ( lo < hi ) {
7835
7937
  ca_size_t mid = lo + (hi - lo) / 2;
7836
7938
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7837
- if ( isnan((double) v) ) { hi = mid; continue; }
7838
7939
  if ( v < query_val ) lo = mid + 1;
7839
7940
  else if ( v > query_val ) hi = mid;
7840
7941
  else { result = mid; break; }
@@ -7843,15 +7944,15 @@ MkKernel.search :bsearch_addr,
7843
7944
  C
7844
7945
  float: <<~C,
7845
7946
  result = (ca_size_t) -1;
7846
- if ( ! isnan((double) query_val) ) {
7947
+ if ( ! isnan((double) query_val) ) { /* a NaN query matches nothing */
7847
7948
  ca_size_t lo = 0;
7848
7949
  ca_size_t hi = slab_n;
7849
7950
  while ( lo < hi ) {
7850
7951
  ca_size_t mid = lo + (hi - lo) / 2;
7851
7952
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7852
- if ( isnan((double) v) ) { hi = mid; continue; }
7853
- if ( v < query_val ) lo = mid + 1;
7854
- else if ( v > query_val ) hi = mid;
7953
+ int c = CA_FLOAT_ORDER_CMP(v, query_val);
7954
+ if ( c < 0 ) lo = mid + 1;
7955
+ else if ( c > 0 ) hi = mid;
7855
7956
  else { result = mid; break; }
7856
7957
  }
7857
7958
  }
@@ -7859,16 +7960,11 @@ MkKernel.search :bsearch_addr,
7859
7960
  object: <<~C,
7860
7961
  result = (ca_size_t) -1;
7861
7962
  ca_size_t lo = 0;
7862
- ca_size_t hi = slab_n;
7963
+ ca_size_t hi = CA_OBJ_ISNAN(query_val) ? 0 : slab_n; /* NaN matches nothing */
7863
7964
  while ( lo < hi ) {
7864
7965
  ca_size_t mid = lo + (hi - lo) / 2;
7865
7966
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7866
- VALUE r = rb_funcall(v, rb_intern("<=>"), 1, query_val);
7867
- if ( NIL_P(r) ) {
7868
- rb_raise(rb_eArgError, "bsearch_addr: comparison of %s with %s failed",
7869
- rb_obj_classname(v), rb_obj_classname(query_val));
7870
- }
7871
- int c = NUM2INT(r);
7967
+ int c = ca_obj_order_cmp(v, query_val);
7872
7968
  if ( c < 0 ) lo = mid + 1;
7873
7969
  else if ( c > 0 ) hi = mid;
7874
7970
  else { result = mid; break; }
@@ -7913,7 +8009,7 @@ MkKernel.search :search_addr,
7913
8009
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7914
8010
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7915
8011
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7916
- if ( fabs((double)v - (double)query_val) <= query_eps ) { result = i; break; }
8012
+ if ( v == query_val || fabs((double)v - (double)query_val) <= query_eps ) { result = i; break; }
7917
8013
  }
7918
8014
  C
7919
8015
  object: <<~C,
@@ -7922,7 +8018,7 @@ MkKernel.search :search_addr,
7922
8018
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7923
8019
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7924
8020
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7925
- if ( rb_equal(v, query_val) ) { result = i; break; }
8021
+ if ( ca_obj_equal(v, query_val) ) { result = i; break; }
7926
8022
  }
7927
8023
  C
7928
8024
  fixlen: <<~C,
@@ -7948,13 +8044,16 @@ MkKernel.search :search_nearest_addr,
7948
8044
  output: :ca_size,
7949
8045
  body: {
7950
8046
  int: <<~C,
8047
+ /* The distance is exact in uint64: a double loses the difference
8048
+ between neighbouring int64 values above 2**53. */
7951
8049
  result = (ca_size_t) -1;
7952
- double best_dist = INFINITY;
8050
+ uint64_t best_dist = 0;
7953
8051
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7954
8052
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7955
8053
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7956
- double dist = fabs((double)v - (double)query_val);
7957
- if ( dist < best_dist ) { best_dist = dist; result = i; }
8054
+ uint64_t dist = ( v >= query_val ) ? (uint64_t) v - (uint64_t) query_val
8055
+ : (uint64_t) query_val - (uint64_t) v;
8056
+ if ( result == (ca_size_t) -1 || dist < best_dist ) { best_dist = dist; result = i; }
7958
8057
  }
7959
8058
  (void) query_eps;
7960
8059
  C
@@ -7964,8 +8063,11 @@ MkKernel.search :search_nearest_addr,
7964
8063
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7965
8064
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7966
8065
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7967
- double dist = fabs((double)v - (double)query_val);
7968
- if ( dist < best_dist ) { best_dist = dist; result = i; }
8066
+ /* An equal value is at distance 0 even when both are infinite
8067
+ (Inf - Inf is NaN); a NaN distance never wins. */
8068
+ double dist = ( v == query_val ) ? 0.0 : fabs((double)v - (double)query_val);
8069
+ if ( dist != dist ) continue;
8070
+ if ( result == (ca_size_t) -1 || dist < best_dist ) { best_dist = dist; result = i; }
7969
8071
  }
7970
8072
  (void) query_eps;
7971
8073
  C
@@ -7984,20 +8086,22 @@ MkKernel.search :search_nearest_addr,
7984
8086
  int nearest_by_distance = rb_respond_to(query_val, nearest_id);
7985
8087
  if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
7986
8088
  rb_raise(rb_eCADataTypeError,
7987
- "search_nearest_addr: nearest needs a distance, and %s is neither a "
8089
+ "%s: nearest needs a distance, and %s is neither a "
7988
8090
  "number nor answers #distance (define one on the stored "
7989
8091
  "objects, or use search / bsearch for an exact match)",
7990
- rb_obj_classname(query_val));
8092
+ ca_calling_method_name(), rb_obj_classname(query_val));
7991
8093
  }
7992
8094
  result = (ca_size_t) -1;
7993
8095
  VALUE best = Qnil;
7994
8096
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7995
8097
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7996
8098
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7997
- VALUE dist = nearest_by_distance
8099
+ VALUE dist = ca_obj_equal(v, query_val) ? INT2FIX(0)
8100
+ : nearest_by_distance
7998
8101
  ? rb_funcall(query_val, nearest_id, 1, v)
7999
8102
  : rb_funcall(rb_funcall(query_val, '-', 1, v),
8000
8103
  rb_intern("abs"), 0);
8104
+ if ( CA_OBJ_ISNAN(dist) ) continue; /* a NaN is no distance, as in float */
8001
8105
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
8002
8106
  best = dist; result = i;
8003
8107
  }
@@ -8043,11 +8147,14 @@ MkKernel.search :linear_section_binary,
8043
8147
  no_match: "NAN",
8044
8148
  no_match_check: "isnan(op[0])",
8045
8149
  mask_self: :raise,
8046
- mask_query: :undef,
8047
8150
  body: <<~C,
8048
8151
  result = NAN;
8049
8152
  if ( slab_n <= 1 ) {
8050
- result = 0.0;
8153
+ /* A one-point axis has no segment: only the point itself is in
8154
+ * range, at 0. An empty one has nothing in range. */
8155
+ if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8156
+ result = 0.0;
8157
+ }
8051
8158
  }
8052
8159
  else {
8053
8160
  double y0 = *(double *)(slab_ptr + 0 * slab_stride);
@@ -8131,11 +8238,14 @@ MkKernel.search :linear_section_linear,
8131
8238
  no_match: "NAN",
8132
8239
  no_match_check: "isnan(op[0])",
8133
8240
  mask_self: :raise,
8134
- mask_query: :undef,
8135
8241
  body: <<~C,
8136
8242
  result = NAN;
8137
8243
  if ( slab_n <= 1 ) {
8138
- result = 0.0;
8244
+ /* A one-point axis has no segment: only the point itself is in
8245
+ * range, at 0. An empty one has nothing in range. */
8246
+ if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8247
+ result = 0.0;
8248
+ }
8139
8249
  }
8140
8250
  else {
8141
8251
  ca_size_t x1 = 0;
@@ -8170,7 +8280,6 @@ MkKernel.search :linear_fetch,
8170
8280
  no_match: "NAN",
8171
8281
  no_match_check: "isnan(op[0])",
8172
8282
  mask_self: :raise,
8173
- mask_query: :undef,
8174
8283
  body: <<~C,
8175
8284
  result = NAN;
8176
8285
  if ( slab_n >= 1 && query_val >= 0.0 && query_val <= (double)(slab_n - 1) ) {
@@ -8265,9 +8374,10 @@ MkKernel.monop :abs_i,
8265
8374
  # (= used by .real chain composition); abs is the user-facing op that
8266
8375
  # returns the real-valued magnitude entity.
8267
8376
  MkKernel.monop :abs,
8268
- source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
8269
- output: { numeric: :preserve, complex: :real_of_source },
8377
+ source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES + [:object],
8378
+ output: { numeric: :preserve, complex: :real_of_source, object: :preserve },
8270
8379
  expr: {
8380
+ [:object] => '(#2) = rb_funcall((#1), rb_intern("abs"), 0);',
8271
8381
  MkKernel::SINT_SMALL_DTYPES => "(#2) = abs(#1);",
8272
8382
  MkKernel::SINT64_DTYPES => "(#2) = llabs(#1);",
8273
8383
  MkKernel::UINT_DTYPES => "(#2) = (#1);",
@@ -8383,7 +8493,9 @@ MkKernel.monop :imag_i,
8383
8493
  MkKernel.monop :arg_i,
8384
8494
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES,
8385
8495
  expr: {
8386
- [:f32] => "(#2) = cargf((cmplx64_t)(#1));",
8496
+ # A real cell's argument is 0, pi or NaN. Taken in double and rounded,
8497
+ # as the eager arg does; cargf returns pi one ulp low.
8498
+ [:f32] => "(#2) = carg((cmplx128_t)(#1));",
8387
8499
  [:f64] => "(#2) = carg((cmplx128_t)(#1));",
8388
8500
  [:cmplx64] => "(#2) = cargf(#1);",
8389
8501
  [:cmplx128] => "(#2) = carg(#1);",
@@ -8432,14 +8544,18 @@ MkKernel.monfunc :round,
8432
8544
  source: MkKernel::ALL_NUMERIC + [:object],
8433
8545
  expr: {
8434
8546
  int: "(#2) = (#1);",
8435
- float: "(#2) = ((#1)>0.0) ? floor((#1)+0.5) : ((#1)<0.0) ? ceil((#1)-0.5) : (#1);",
8547
+ # Half away from zero, as Float#round. floor(x + 0.5) would round in
8548
+ # the addition: 0.49999999999999994 + 0.5 is 1.0.
8549
+ float: "(#2) = round(#1);",
8436
8550
  object: '(#2) = rb_funcall((#1), rb_intern("round"), 0);',
8437
8551
  }
8438
8552
 
8439
8553
  MkKernel.monfunc :rcp,
8440
8554
  source: MkKernel::MATH_NUMERIC + [:object],
8441
8555
  expr: {
8442
- int: "if ((#1)==0) {ca_zerodiv();}; (#2) = 1/(#1);",
8556
+ MkKernel::UINT_DTYPES => "if ((#1)==0) {ca_zerodiv();}; (#2) = 1/(#1);",
8557
+ # 1 / x floored: 1 for 1, -1 for every negative x, 0 otherwise.
8558
+ int: "if ((#1)==0) {ca_zerodiv();}; (#2) = ((#1)==1) ? 1 : ((#1) < 0) ? -1 : 0;",
8443
8559
  float: "(#2) = 1/(#1);",
8444
8560
  [:cmplx64] => "(#2) = op_crcp_cmplx64(#1);",
8445
8561
  [:cmplx128] => "(#2) = 1/(#1);",
@@ -8548,11 +8664,13 @@ end
8548
8664
 
8549
8665
  # expm1, log1p: float + object only (C99 doesn't standardise complex
8550
8666
  # variants). Widening monfunc — integer input auto-casts to f64.
8667
+ # CAMath.expm1 / log1p are defined in lib/carray/math.rb instead: Ruby's
8668
+ # Math has neither, so the generated front-end could not take a Numeric.
8551
8669
  {
8552
8670
  expm1: "expm1",
8553
8671
  log1p: "log1p",
8554
8672
  }.each do |op, c_fn|
8555
- MkKernel.monfunc op,
8673
+ MkKernel.monfunc op, cmath: false,
8556
8674
  source: MkKernel::FLOAT_DTYPES + [:object],
8557
8675
  expr: {
8558
8676
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
@@ -8737,14 +8855,15 @@ MkKernel.monfunc :rad_pi,
8737
8855
  # NaN the result is NaN. Distinct kernel — cannot be
8738
8856
  # aliased onto pmax.
8739
8857
  #
8740
- # Integer / object branches are identical across all three (no NaN
8741
- # concept). Only the float branch differs.
8858
+ # Integer branches are identical across all three (no NaN). The float
8859
+ # and object branches differ: pmax / pmin let a NaN lose (an object cell's
8860
+ # Float NaN included), maximum / minimum let it win.
8742
8861
  MkKernel.binop :pmax,
8743
8862
  source: MkKernel::ALL_NUMERIC + [:object],
8744
8863
  expr: {
8745
8864
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8746
8865
  float: "(#3) = fmax(#1, #2);",
8747
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("max"), 0);',
8866
+ object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern(">")) ? (#2) : (#1);',
8748
8867
  }
8749
8868
 
8750
8869
  MkKernel.binop :pmin,
@@ -8752,26 +8871,28 @@ MkKernel.binop :pmin,
8752
8871
  expr: {
8753
8872
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8754
8873
  float: "(#3) = fmin(#1, #2);",
8755
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("min"), 0);',
8874
+ object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern("<")) ? (#2) : (#1);',
8756
8875
  }
8757
8876
 
8758
8877
  # NaN-propagate variants. Float
8759
8878
  # branch tests both operands for NaN and short-circuits to NaN; non-
8760
- # float branches reuse the comparison form.
8879
+ # float branches reuse the comparison form. Of two zeros, maximum is +0
8880
+ # and minimum is -0 in either order: a bare comparison would answer with
8881
+ # whichever came second.
8761
8882
  MkKernel.binop :maximum,
8762
8883
  source: MkKernel::ALL_NUMERIC + [:object],
8763
8884
  expr: {
8764
8885
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8765
- float: "(#3) = isnan(#1) ? (#1) : (isnan(#2) ? (#2) : ((#1) > (#2) ? (#1) : (#2)));",
8766
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("max"), 0);',
8886
+ float: "(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) > (#2) || ((#1) == (#2) && !signbit(#1))) ? (#1) : (#2);",
8887
+ object: '(#3) = CA_OBJ_ISNAN(#1) ? (#1) : CA_OBJ_ISNAN(#2) ? (#2) : ca_obj_cmp((#2), (#1)) > 0 ? (#2) : (#1);',
8767
8888
  }
8768
8889
 
8769
8890
  MkKernel.binop :minimum,
8770
8891
  source: MkKernel::ALL_NUMERIC + [:object],
8771
8892
  expr: {
8772
8893
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8773
- float: "(#3) = isnan(#1) ? (#1) : (isnan(#2) ? (#2) : ((#1) < (#2) ? (#1) : (#2)));",
8774
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("min"), 0);',
8894
+ float: "(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) < (#2) || ((#1) == (#2) && signbit(#1))) ? (#1) : (#2);",
8895
+ object: '(#3) = CA_OBJ_ISNAN(#1) ? (#1) : CA_OBJ_ISNAN(#2) ? (#2) : ca_obj_cmp((#2), (#1)) < 0 ? (#2) : (#1);',
8775
8896
  }
8776
8897
 
8777
8898
  # + and - work on the parts independently, so one generic expression
@@ -8812,6 +8933,11 @@ MkKernel.binop :mul,
8812
8933
  # sign, so they keep the bare C form. Float `/` is true division and is
8813
8934
  # unchanged (matching Ruby `Float#/`); the identity above therefore holds
8814
8935
  # for integers only, exactly as in Ruby.
8936
+ #
8937
+ # A signed divisor of -1 is taken apart from C's `/`: MIN / -1 does not
8938
+ # fit, and the hardware's answer to it differs (x86 traps, ARM returns
8939
+ # MIN). The quotient is the negation wrapped like any other overflow, so
8940
+ # MIN / -1 is MIN and MIN % -1 is 0 on every machine.
8815
8941
  MkKernel.binop :div,
8816
8942
  op: "/",
8817
8943
  source: MkKernel::MATH_NUMERIC + [:object],
@@ -8819,7 +8945,8 @@ MkKernel.binop :div,
8819
8945
  MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) / (#2);",
8820
8946
  int: %{
8821
8947
  if ((#2)==0) {ca_zerodiv();};
8822
- {
8948
+ if ((#2)==-1) { (#3) = (<type>) (0 - (uint64_t) (#1)); }
8949
+ else {
8823
8950
  <type> _q = (#1) / (#2);
8824
8951
  if ( (#1) % (#2) != 0 && (((#1) < 0) != ((#2) < 0)) ) { _q -= 1; }
8825
8952
  (#3) = _q;
@@ -8845,11 +8972,21 @@ MkKernel.binop :quo_i,
8845
8972
  object: '(#3) = rb_funcall((#1), rb_intern("quo"), 1, (#2));',
8846
8973
  }
8847
8974
 
8975
+ # rcp_mul: `a.rcp_mul(b)` is `b / a`, and divides as `/` does.
8848
8976
  MkKernel.binop :rcp_mul,
8849
8977
  op: "rcp_mul",
8850
8978
  source: MkKernel::MATH_NUMERIC + [:object],
8851
8979
  expr: {
8852
- int: "if ((#1)==0) {ca_zerodiv();}; (#3) = (#2) / (#1);",
8980
+ MkKernel::UINT_DTYPES => "if ((#1)==0) {ca_zerodiv();}; (#3) = (#2) / (#1);",
8981
+ int: %{
8982
+ if ((#1)==0) {ca_zerodiv();};
8983
+ if ((#1)==-1) { (#3) = (<type>) (0 - (uint64_t) (#2)); }
8984
+ else {
8985
+ <type> _q = (#2) / (#1);
8986
+ if ( (#2) % (#1) != 0 && (((#2) < 0) != ((#1) < 0)) ) { _q -= 1; }
8987
+ (#3) = _q;
8988
+ }
8989
+ },
8853
8990
  float: "(#3) = (#2) / (#1);",
8854
8991
  [:cmplx64] => "(#3) = op_cdiv_cmplx64((#2), (#1));",
8855
8992
  [:cmplx128] => "(#3) = (#2) / (#1);",
@@ -8871,7 +9008,8 @@ MkKernel.binop :mod,
8871
9008
  MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
8872
9009
  int: %{
8873
9010
  if ((#2)==0) {ca_zerodiv();};
8874
- {
9011
+ if ((#2)==-1) { (#3) = 0; }
9012
+ else {
8875
9013
  <type> _r = (#1) % (#2);
8876
9014
  if ( _r != 0 && ((_r < 0) != ((#2) < 0)) ) { _r += (#2); }
8877
9015
  (#3) = _r;
@@ -8899,7 +9037,8 @@ MkKernel.binop :mod,
8899
9037
  MkKernel.binop :fmod,
8900
9038
  source: MkKernel::ALL_NUMERIC + [:object],
8901
9039
  expr: {
8902
- int: "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
9040
+ MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
9041
+ int: "if ((#2)==0) {ca_zerodiv();}; (#3) = ((#2)==-1) ? 0 : (#1) % (#2);",
8903
9042
  object: '(#3) = rb_funcall((#1), rb_intern("remainder"), 1, (#2));',
8904
9043
  }.merge(MkKernel.float_widths("(#3) = fmod<f>((#1), (#2));"))
8905
9044
 
@@ -8932,11 +9071,29 @@ MkKernel.binop :bit_xor_i,
8932
9071
  object: '(#3) = rb_funcall((#1), rb_intern("^"), 1, (#2));',
8933
9072
  }
8934
9073
 
9074
+ # Shifts take the meaning Integer#<< and #>> give them: a negative count
9075
+ # shifts the other way, and a count of the width or more shifts every bit
9076
+ # out (0, or -1 for a negative value shifted right). C leaves both, and a
9077
+ # left shift of a negative value, undefined. The value is widened to 64
9078
+ # bits and shifted there -- unsigned for a left shift -- and the result
9079
+ # narrowed back, which keeps the low bits a shift within the width gives.
9080
+ # The bodies are self-contained, since carray-jit compiles them as they are.
8935
9081
  MkKernel.binop :bit_lshift,
8936
9082
  op: "<<",
8937
9083
  source: MkKernel::INT_DTYPES + [:object],
8938
9084
  expr: {
8939
- int: "(#3) = (#1) << (#2);",
9085
+ MkKernel::UINT_DTYPES => "(#3) = ((uint64_t) (#2) >= 8 * sizeof(<type>)) ? 0 : (<type>) ((uint64_t) (#1) << (#2));",
9086
+ int: %{
9087
+ {
9088
+ int64_t _x = (int64_t) (#1), _n = (int64_t) (#2), _r;
9089
+ const int64_t _w = (int64_t) (8 * sizeof(<type>));
9090
+ if ( _n >= _w ) _r = 0;
9091
+ else if ( _n <= -_w ) _r = _x < 0 ? -1 : 0;
9092
+ else if ( _n < 0 ) _r = _x >> -_n;
9093
+ else _r = (int64_t) ((uint64_t) _x << _n);
9094
+ (#3) = (<type>) _r;
9095
+ }
9096
+ },
8940
9097
  object: '(#3) = rb_funcall((#1), rb_intern("<<"), 1, (#2));',
8941
9098
  }
8942
9099
 
@@ -8944,7 +9101,18 @@ MkKernel.binop :bit_rshift,
8944
9101
  op: ">>",
8945
9102
  source: MkKernel::INT_DTYPES + [:object],
8946
9103
  expr: {
8947
- int: "(#3) = (#1) >> (#2);",
9104
+ MkKernel::UINT_DTYPES => "(#3) = ((uint64_t) (#2) >= 8 * sizeof(<type>)) ? 0 : (<type>) ((uint64_t) (#1) >> (#2));",
9105
+ int: %{
9106
+ {
9107
+ int64_t _x = (int64_t) (#1), _n = (int64_t) (#2), _r;
9108
+ const int64_t _w = (int64_t) (8 * sizeof(<type>));
9109
+ if ( _n >= _w ) _r = _x < 0 ? -1 : 0;
9110
+ else if ( _n <= -_w ) _r = 0;
9111
+ else if ( _n < 0 ) _r = (int64_t) ((uint64_t) _x << -_n);
9112
+ else _r = _x >> _n;
9113
+ (#3) = (<type>) _r;
9114
+ }
9115
+ },
8948
9116
  object: '(#3) = rb_funcall((#1), rb_intern(">>"), 1, (#2));',
8949
9117
  }
8950
9118
 
@@ -9014,16 +9182,16 @@ C
9014
9182
  # fma: fused multiply-add — `self * op2 + op3` in a single rounding step
9015
9183
  # (= C99 `fma(x, y, z)` and HW FMA3/FMA4 / ARM FMA instructions). More
9016
9184
  # accurate than the equivalent two-op `a*b + c` expression which rounds
9017
- # twice. Used in Horner evaluation, linear interpolation, dot product,
9185
+ # twice. float32 calls fmaf: fma in double followed by a narrowing is a
9186
+ # second rounding, and can land 1 ulp off. Used in Horner evaluation, linear interpolation, dot product,
9018
9187
  # Newton iteration.
9019
9188
  MkKernel.triop :fma,
9020
9189
  source: MkKernel::MATH_NUMERIC + [:object],
9021
9190
  expr: {
9022
9191
  int: "(#4) = (#1) * (#2) + (#3);",
9023
- float: "(#4) = fma((#1), (#2), (#3));",
9024
9192
  complex: "(#4) = (#1) * (#2) + (#3);",
9025
9193
  object: '(#4) = rb_funcall(rb_funcall((#1), rb_intern("*"), 1, (#2)), rb_intern("+"), 1, (#3));',
9026
- }
9194
+ }.merge(MkKernel.float_widths("(#4) = fma<f>((#1), (#2), (#3));"))
9027
9195
 
9028
9196
  # fms: fused multiply-subtract — `self * op2 - op3` in a single rounding.
9029
9197
  # Useful in cross products and complex multiplication primitives.
@@ -9031,11 +9199,10 @@ MkKernel.triop :fms,
9031
9199
  source: MkKernel::MATH_NUMERIC + [:object],
9032
9200
  expr: {
9033
9201
  int: "(#4) = (#1) * (#2) - (#3);",
9034
- # fma(a, b, -c) = a*b - c with single rounding (= C99 / IEEE 754 hint).
9035
- float: "(#4) = fma((#1), (#2), -(#3));",
9036
9202
  complex: "(#4) = (#1) * (#2) - (#3);",
9037
9203
  object: '(#4) = rb_funcall(rb_funcall((#1), rb_intern("*"), 1, (#2)), rb_intern("-"), 1, (#3));',
9038
- }
9204
+ # fma(a, b, -c) = a*b - c with single rounding (= C99 / IEEE 754 hint).
9205
+ }.merge(MkKernel.float_widths("(#4) = fma<f>((#1), (#2), -(#3));"))
9039
9206
 
9040
9207
  # clip: per-element clamp into [lo, hi]. 3.0 migration: was a hand-
9041
9208
  # written eager-only method in ext/carray_generate.c that only accepted
@@ -9050,13 +9217,13 @@ MkKernel.triop :fms,
9050
9217
  # UNDEF-fill case can now be expressed as `out = a.clip(lo, hi);
9051
9218
  # out[a.lt(lo) | a.gt(hi)] = UNDEF`). Pinned in test.
9052
9219
  #
9053
- # Bound at the C level as `__clip_ki__` (private kernel entry). The
9220
+ # Bound at the C level as `clamp_between` (made private by lazy.rb). The
9054
9221
  # public `CArray#clip` is a Ruby wrapper in lib/carray/math.rb that
9055
9222
  # restores the rich pre-3.0 API surface (nil-bound one-sided clip +
9056
9223
  # optional fill). `clip!` is retired in 3.0 (view-by-default
9057
9224
  # convention) so the bang form is suppressed via `bang: false`.
9058
9225
  MkKernel.triop :clip,
9059
- op: "__clip_ki__",
9226
+ op: "clamp_between",
9060
9227
  bang: false,
9061
9228
  source: MkKernel::ALL_NUMERIC + [:object],
9062
9229
  expr: {
@@ -9074,6 +9241,20 @@ MkKernel.triop :clip,
9074
9241
  '(#4) = _v; }',
9075
9242
  }
9076
9243
 
9244
+ # select: the per-cell choice a lazy `then_else` makes (#1 is the
9245
+ # condition, converted to the branches' common type; #2 / #3 are the
9246
+ # branches). No Ruby method: CArray#then_else builds the node.
9247
+ MkKernel.triop :select,
9248
+ bind: false,
9249
+ bang: false,
9250
+ source: MkKernel::MATH_NUMERIC + [:bool],
9251
+ expr: {
9252
+ int: "(#4) = (#1) ? (#2) : (#3);",
9253
+ float: "(#4) = (#1) ? (#2) : (#3);",
9254
+ complex: "(#4) = (#1) ? (#2) : (#3);",
9255
+ bool: "(#4) = (#1) ? (#2) : (#3);",
9256
+ }
9257
+
9077
9258
  MkKernel.binop :power,
9078
9259
  op: :power,
9079
9260
  source: MkKernel::MATH_NUMERIC + [:object],
@@ -9151,7 +9332,11 @@ MkKernel.moncmp :is_nan,
9151
9332
  int: "(#2) = 0;",
9152
9333
  float: "(#2) = isnan(#1);",
9153
9334
  complex: "(#2) = (isnan(creal(#1)) || isnan(cimag(#1)));",
9154
- object: '(#2) = rb_funcall((#1), rb_intern("nan?"), 0);',
9335
+ # A Float answers by its bits; an Integer or a Rational is never NaN;
9336
+ # anything else answers through its own #nan?.
9337
+ object: '(#2) = RB_FLOAT_TYPE_P(#1) ? isnan(RFLOAT_VALUE(#1)) : '\
9338
+ '(RB_INTEGER_TYPE_P(#1) || RB_TYPE_P((#1), T_RATIONAL)) ? 0 : '\
9339
+ 'RTEST(rb_funcall((#1), rb_intern("nan?"), 0));',
9155
9340
  }
9156
9341
 
9157
9342
  MkKernel.moncmp :is_inf,
@@ -9160,7 +9345,7 @@ MkKernel.moncmp :is_inf,
9160
9345
  int: "(#2) = 0;",
9161
9346
  float: "(#2) = isinf(#1);",
9162
9347
  complex: "(#2) = (isinf(creal(#1)) || isinf(cimag(#1)));",
9163
- object: '(#2) = rb_funcall((#1), rb_intern("infinite?"), 0);',
9348
+ object: '(#2) = RTEST(rb_funcall((#1), rb_intern("infinite?"), 0));',
9164
9349
  }
9165
9350
 
9166
9351
  MkKernel.moncmp :is_finite,
@@ -9169,7 +9354,7 @@ MkKernel.moncmp :is_finite,
9169
9354
  int: "(#2) = 1;",
9170
9355
  float: "(#2) = isfinite(#1);",
9171
9356
  complex: "(#2) = (isfinite(creal(#1)) && isfinite(cimag(#1)));",
9172
- object: '(#2) = rb_funcall((#1), rb_intern("finite?"), 0);',
9357
+ object: '(#2) = RTEST(rb_funcall((#1), rb_intern("finite?"), 0));',
9173
9358
  }
9174
9359
 
9175
9360
  # is_invalid = !is_finite (MS.1 mask SET family redesign): NaN or Inf
@@ -9199,7 +9384,10 @@ MkKernel.moncmp :signbit,
9199
9384
  MkKernel::SINT_DTYPES => "(#2) = ((#1) < 0);",
9200
9385
  MkKernel::UINT_DTYPES => "(#2) = 0;",
9201
9386
  float: "(#2) = signbit(#1);",
9202
- object: '(#2) = (RTEST(rb_funcall((#1), rb_intern("negative?"), 0)) ? 1 : 0);',
9387
+ # A Float answers by its sign bit (-0.0 and -NaN are negative), as
9388
+ # the float branch does; anything else through #negative?.
9389
+ object: '(#2) = RB_FLOAT_TYPE_P(#1) ? (signbit(RFLOAT_VALUE(#1)) != 0) : '\
9390
+ '(RTEST(rb_funcall((#1), rb_intern("negative?"), 0)) ? 1 : 0);',
9203
9391
  }
9204
9392
 
9205
9393
  # ---- P.5b.4: bincmp family (predicates returning bool) ----------------