carray 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +90 -41
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +194 -42
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +551 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +88 -75
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. data/ext/carray_kernels_triop.c +0 -1392
data/ext/mkkernel.rb CHANGED
@@ -360,6 +360,7 @@ module MkKernel
360
360
  face_gate: nil,
361
361
  object_escape: nil,
362
362
  identity_on_empty: false,
363
+ all_nan_result: nil,
363
364
  outputs: 1,
364
365
  # Two-pass centred algorithm (variance / stddev family).
365
366
  # When algorithm: :two_pass_centred is set, `state / init /
@@ -405,8 +406,8 @@ module MkKernel
405
406
  # only, output (an axis-local index) needs no re-lift.
406
407
  # :relift -- value-returning kernels (min/max): descend, then re-lift the
407
408
  # output back into the Face -- a full-reduction scalar via the
408
- # registered storage_to_scalar hook, a per-axis CArray via
409
- # rb_ca_face_template (carries the subclass tail, e.g. unit).
409
+ # registered storage_to_element hook, a per-axis CArray via
410
+ # ca_face_relift_entity (carries the subclass tail, e.g. unit).
410
411
  raise "reduce: unknown face_gate #{face_gate} (expected nil, :strip, or :relift)" \
411
412
  unless [nil, :strip, :relift].include?(face_gate)
412
413
  # object_escape: a Symbol naming a Ruby instance method that handles the
@@ -465,6 +466,30 @@ module MkKernel
465
466
  end
466
467
  end
467
468
  end
469
+ # all_nan_result: what an extreme-value reduction answers when every
470
+ # contributing cell was NaN. The `(v < acc) ? v : acc` reduce body is
471
+ # false for NaN, so acc is never updated and the init (+/-INFINITY)
472
+ # leaks out as the answer -- a value that is neither the minimum of
473
+ # anything nor a missing marker, and indistinguishable from data that
474
+ # genuinely held only +INFINITY.
475
+ #
476
+ # :nan -- answer NaN (C99 fmin folded: a lone non-NaN wins, two
477
+ # NaNs give NaN). For kernels whose output can hold it.
478
+ # :undef -- answer UNDEF, by folding into the mask_policy trigger.
479
+ # For kernels whose output is a position (argmin family):
480
+ # an integer output cannot hold NaN, and index 0 would be
481
+ # a lie about where the minimum is.
482
+ #
483
+ # Only the float srcs act on it; integers and boolean have no NaN and
484
+ # their init is a legitimate value.
485
+ unless all_nan_result.nil? || %i[nan undef].include?(all_nan_result)
486
+ raise "#{name}: all_nan_result #{all_nan_result.inspect} invalid " \
487
+ "(expected :nan, :undef, or nil)"
488
+ end
489
+ if all_nan_result == :undef && mask_policy.nil?
490
+ raise "#{name}: all_nan_result: :undef needs a mask_policy " \
491
+ "(UNDEF is written through the mask_policy trigger)"
492
+ end
468
493
  if value_arg
469
494
  raise "#{name}: value_arg must be a Hash" unless value_arg.is_a?(Hash)
470
495
  raise "#{name}: value_arg: target must be :T_IN" unless value_arg[:target] == :T_IN
@@ -478,16 +503,29 @@ module MkKernel
478
503
  # selects direction (:min / :max) and output kind:
479
504
  # :min / :max -> extremum blob, output data_type = CA_FIXLEN
480
505
  # :argmin / :argmax -> position of the extremum, output i64
506
+ # :count_equal -> how many cells memcmp-equal value_arg, output i64
481
507
  # Author must also list :fixlen in source: (parallel to how :object
482
508
  # opts in via source: + an :object body).
509
+ #
510
+ # :count_equal is the one mode that takes a value_arg, and takes it as a
511
+ # byte blob rather than a scalar: the dispatcher packs the query with
512
+ # rb_ca_obj2ptr into a ca->bytes buffer, the same way the search family
513
+ # already does for a fixlen query (which is what makes a short query
514
+ # NUL-pad to the cell width instead of never matching).
483
515
  if fixlen
484
- unless %i[min max argmin argmax].include?(fixlen)
485
- raise "#{name}: fixlen: must be :min / :max / :argmin / :argmax (got #{fixlen.inspect})"
516
+ unless %i[min max argmin argmax count_equal].include?(fixlen)
517
+ raise "#{name}: fixlen: must be :min / :max / :argmin / :argmax / :count_equal (got #{fixlen.inspect})"
486
518
  end
487
519
  raise "#{name}: fixlen: requires :fixlen in source:" unless source.include?(:fixlen)
488
520
  raise "#{name}: fixlen: requires mask_policy: :min_count" unless mask_policy == :min_count
489
521
  raise "#{name}: fixlen: requires outputs: 1" unless outputs == 1
490
- raise "#{name}: fixlen: does not support value_arg / array_arg" if value_arg || array_arg
522
+ raise "#{name}: fixlen: does not support array_arg" if array_arg
523
+ if value_arg && fixlen != :count_equal
524
+ raise "#{name}: fixlen: only :count_equal takes a value_arg"
525
+ end
526
+ if fixlen == :count_equal && !value_arg
527
+ raise "#{name}: fixlen: :count_equal requires value_arg"
528
+ end
491
529
  elsif source.include?(:fixlen)
492
530
  raise "#{name}: source includes :fixlen but no fixlen: spec given"
493
531
  end
@@ -612,6 +650,7 @@ module MkKernel
612
650
  face_gate: face_gate,
613
651
  object_escape: object_escape,
614
652
  identity_on_empty: identity_on_empty,
653
+ all_nan_result: all_nan_result,
615
654
  outputs: outputs,
616
655
  algorithm: algorithm,
617
656
  divisor: divisor,
@@ -620,41 +659,6 @@ module MkKernel
620
659
  }
621
660
  end
622
661
 
623
- # Register a map kernel (element-wise transform: input shape == output
624
- # shape, one cell at a time). Each generated helper walks the input
625
- # and output slabs in lockstep using CA_SLAB_MAP_T(T_LOAD, T_OUT, ...).
626
- #
627
- # DSL:
628
- # MkKernel.map :name,
629
- # source: MkKernel::ALL_NUMERIC,
630
- # output: :f64 | :preserve,
631
- # expr: "r = sqrt((double) v)", # binds v (input) + r (output)
632
- # fallback: :wrap_to_f64 | :raise
633
- #
634
- # Generated method takes no arguments (`a.sqrt_ki` not `a.sqrt_ki(0)`)
635
- # since the operation is per-cell across the whole array.
636
- def self.map(name, source:, output:, expr:, fallback: :raise)
637
- raise "duplicate kernel #{name}" if KERNELS.any? { |k| k[:name] == name }
638
- source.each do |s|
639
- raise "unknown source data_type #{s}" unless DTYPES.key?(s)
640
- end
641
- raise "unknown output #{output}" unless output == :preserve || DTYPES.key?(output) || output.is_a?(Hash)
642
- raise "unknown fallback #{fallback}" unless %i[raise wrap_to_f64].include?(fallback)
643
- if output == :preserve && fallback == :wrap_to_f64
644
- raise "#{name}: :preserve + :wrap_to_f64 is a semantic conflict " \
645
- "(fallback path produces f64 output, contradicting :preserve)"
646
- end
647
-
648
- KERNELS << {
649
- kind: :map,
650
- name: name,
651
- source: source,
652
- output: output,
653
- expr: expr,
654
- fallback: fallback,
655
- }
656
- end
657
-
658
662
  # Register a scan kernel (cumulative / prefix-scan: input shape ==
659
663
  # output shape, running accumulator along one axis). The generated
660
664
  # method takes exactly one axis argument (e.g., `a.cumsum_ki(0)`).
@@ -665,7 +669,11 @@ module MkKernel
665
669
  # output: :f64 | :preserve,
666
670
  # init: "0", # initial acc value (per fiber)
667
671
  # step: "acc += v; r = acc", # binds v (T_LOAD), r (T_OUT), acc
668
- # fallback: :wrap_to_f64 | :raise
672
+ # fallback: :wrap_to_f64 | :raise | :mask_count
673
+ #
674
+ # :mask_count is for a scan that reads only the mask (cumcount): a data
675
+ # type outside source: is answered as is_not_masked.cumsum(axis:) in
676
+ # int64, so the values are never converted.
669
677
  #
670
678
  # Init supports the same special tokens as reduce: T_LIMIT_HI /
671
679
  # T_LIMIT_LO / T_ACC / T_OUT. Each scan slab (= fiber along the
@@ -699,13 +707,13 @@ module MkKernel
699
707
  # the reduction contract (empty max / min = UNDEF, no
700
708
  # identity) and the axis-group scan family.
701
709
  def self.scan(name, source:, output:, init:, step:, fallback: :raise,
702
- acc_type: nil, axis_default: nil, empty: nil)
710
+ acc_type: nil, axis_default: nil, empty: nil, face_gate: nil)
703
711
  raise "duplicate kernel #{name}" if KERNELS.any? { |k| k[:name] == name }
704
712
  source.each do |s|
705
713
  raise "unknown source data_type #{s}" unless DTYPES.key?(s)
706
714
  end
707
715
  raise "unknown output #{output}" unless output == :preserve || DTYPES.key?(output) || output.is_a?(Hash)
708
- raise "unknown fallback #{fallback}" unless %i[raise wrap_to_f64].include?(fallback)
716
+ raise "unknown fallback #{fallback}" unless %i[raise wrap_to_f64 mask_count].include?(fallback)
709
717
  if output == :preserve && fallback == :wrap_to_f64
710
718
  raise "#{name}: :preserve + :wrap_to_f64 is a semantic conflict"
711
719
  end
@@ -721,6 +729,13 @@ module MkKernel
721
729
  if empty == :undef && acc_type == :load_type
722
730
  raise "#{name}: empty: :undef is not supported with acc_type: :load_type"
723
731
  end
732
+ # face_gate: :relift descends an ORDERABLE Face to its storage, scans
733
+ # the storage and puts the Face back on the result (the reduce family's
734
+ # policy). Only a scan whose output is the source's own values may take
735
+ # it: a running sum of instants is not an instant.
736
+ unless face_gate.nil? || face_gate == :relift
737
+ raise "#{name}: unknown face_gate #{face_gate.inspect} (nil | :relift)"
738
+ end
724
739
 
725
740
  KERNELS << {
726
741
  kind: :scan,
@@ -733,6 +748,7 @@ module MkKernel
733
748
  acc_type: acc_type,
734
749
  axis_default: axis_default,
735
750
  empty: empty,
751
+ face_gate: face_gate,
736
752
  }
737
753
  end
738
754
 
@@ -1351,6 +1367,68 @@ module MkKernel
1351
1367
  io.puts "#undef #{expr_macro}"
1352
1368
  end
1353
1369
 
1370
+ # State vars that start at the type's limit (T_LIMIT_HI / T_LIMIT_LO)
1371
+ # and so can leak their init as an answer. A position state (argmin's
1372
+ # best_i, init "0") is deliberately not one of them: index 0 is a value
1373
+ # a real minimum can legitimately produce.
1374
+ def self.limit_init_state_vars(k, si, oi, src)
1375
+ return [] unless k[:state]
1376
+ k[:state].keys.filter_map do |var|
1377
+ token = pick_family_string(k[:init][var], src, "init")
1378
+ next unless %w[T_LIMIT_HI T_LIMIT_LO].include?(token)
1379
+ [var, resolve_init_expr(k[:init][var], oi, si, src)]
1380
+ end
1381
+ end
1382
+
1383
+ # Emit the all-NaN fix-up for an extreme-value reduction (see the
1384
+ # all_nan_result: validation in MkKernel.reduce for why it exists).
1385
+ #
1386
+ # The per-cell reduce body is left alone: it stays the SIMD-licensed
1387
+ # ternary, so the 8-way horizontal split (reduce_8way_eligible?) and
1388
+ # the `reduction(min:acc)` clause are untouched, and ordinary data pays
1389
+ # one comparison per slab. Carrying a "saw a number" flag as a second
1390
+ # DSL state var was the alternative and costs more than it looks: it
1391
+ # takes the kernel over reduce_8way_eligible?'s single-state gate and
1392
+ # drops min / max back onto the legacy single-accumulator macro.
1393
+ #
1394
+ # acc still sitting at its init means either nothing updated it or the
1395
+ # data genuinely held only +/-INFINITY. Those are told apart by
1396
+ # walking the slab once more for a non-NaN cell -- O(n), and only for
1397
+ # a slab whose answer came out equal to the init.
1398
+ #
1399
+ # Returns true when it emitted anything.
1400
+ def self.emit_all_nan_fixup(io, k, src, si, pairs, valid_guard,
1401
+ indent: " ", flag_var: nil)
1402
+ mode = k[:all_nan_result]
1403
+ return false unless mode
1404
+ return false unless FLOAT_DTYPES.include?(src)
1405
+ cond = pairs.map { |var, init| "#{var} == (#{init})" }.join(" && ")
1406
+ cond = "(#{cond}) && #{valid_guard}" if valid_guard
1407
+ io.puts "#{indent}/* all-NaN fix-up (all_nan_result: #{mode.inspect}). */"
1408
+ io.puts "#{indent}if ( #{cond} ) {"
1409
+ io.puts "#{indent} int64_t __anf_seen = 0;"
1410
+ io.puts "#{indent} ca_size_t __anf_mc = 0;"
1411
+ io.puts "#{indent} CA_SLAB_REDUCE_T_EX(#{si[:c]}, st, p, m, __anf_seen, 0, " \
1412
+ "__anf_seen |= (v == v), __anf_mc);"
1413
+ io.puts "#{indent} (void) __anf_mc;"
1414
+ io.puts "#{indent} if ( ! __anf_seen ) {"
1415
+ if mode == :nan
1416
+ pairs.each { |var, _| io.puts "#{indent} #{var} = (#{si[:c]}) NAN;" }
1417
+ else
1418
+ raise "#{k[:name]}: all_nan_result: :undef needs flag_var" unless flag_var
1419
+ io.puts "#{indent} #{flag_var} = 1;"
1420
+ end
1421
+ io.puts "#{indent} }"
1422
+ io.puts "#{indent}}"
1423
+ true
1424
+ end
1425
+
1426
+ # The CArray names of the data types a kernel loads, for its refusal
1427
+ # message ("int8, uint8, ..."), not the generator's own short keys.
1428
+ def self.type_names(sources)
1429
+ sources.map { |s| s == :fixlen ? "fixlen" : DTYPES[s][:ca].sub(/\ACA_/, "").downcase }.join(", ")
1430
+ end
1431
+
1354
1432
  def self.reduce_macro_suffix(k, src = nil)
1355
1433
  # CA_OBJECT cannot ride the SIMD-licensed macros (= _PLUS / _MIN / _MAX
1356
1434
  # / _STAR), which assume C operators (= acc is a VALUE, so
@@ -1529,9 +1607,11 @@ module MkKernel
1529
1607
  require "fileutils"
1530
1608
  FileUtils.mkdir_p(outdir)
1531
1609
  tags = file_tags
1610
+ written = ["carray_kernels_init.c"]
1532
1611
  tags.each do |kind, subgroup|
1533
1612
  suffix = file_suffix(kind, subgroup)
1534
1613
  path = File.join(outdir, "carray_kernels_#{suffix}.c")
1614
+ written << File.basename(path)
1535
1615
  File.open(path, "w") do |io|
1536
1616
  io.puts header
1537
1617
  KERNELS.each do |k|
@@ -1545,6 +1625,11 @@ module MkKernel
1545
1625
  File.open(File.join(outdir, "carray_kernels_init.c"), "w") do |io|
1546
1626
  emit_aggregator_init(io, tags)
1547
1627
  end
1628
+ # extconf compiles every carray_kernels_*.c it finds, so a file left
1629
+ # by a kind or sub-group that no longer exists has to go.
1630
+ Dir.glob(File.join(outdir, "carray_kernels_*.c")).each do |f|
1631
+ File.delete(f) unless written.include?(File.basename(f))
1632
+ end
1548
1633
  end
1549
1634
 
1550
1635
  # Which file a kernel belongs to. The three alias kinds live with the kind
@@ -1563,7 +1648,6 @@ module MkKernel
1563
1648
  def self.emit_kernel_dispatch(io, k)
1564
1649
  case k[:kind]
1565
1650
  when :reduce then emit_reduce(io, k)
1566
- when :map then emit_map(io, k)
1567
1651
  when :scan then emit_scan(io, k)
1568
1652
  when :sort then emit_sort(io, k)
1569
1653
  when :search then emit_search(io, k)
@@ -1580,7 +1664,7 @@ module MkKernel
1580
1664
  end
1581
1665
  end
1582
1666
 
1583
- KINDS = [:reduce, :map, :scan, :sort, :search,
1667
+ KINDS = [:reduce, :scan, :sort, :search,
1584
1668
  :monop, :binop, :triop, :moncmp, :bincmp].freeze
1585
1669
 
1586
1670
  # Sub-split map for the reduce kind. As one file, reduce accounted for 71%
@@ -1591,17 +1675,16 @@ module MkKernel
1591
1675
  # rather than the guess that monop and binop would dominate. A kernel added
1592
1676
  # later needs an entry here; emit asserts, so a missing one is loud.
1593
1677
  REDUCE_SUB_GROUPS = {
1594
- # aggregate: arithmetic folds (sum / prod / mean and their strict,
1595
- # safe and weighted variants)
1678
+ # aggregate: arithmetic folds (sum / prod / mean and their weighted
1679
+ # variants)
1596
1680
  sum: :aggregate, prod: :aggregate, mean: :aggregate,
1597
- sum_strict: :aggregate, mean_safe: :aggregate,
1598
1681
  wsum: :aggregate, wmean: :aggregate,
1599
1682
  # extreme: order-based (min / max, argmin / argmax, minmax)
1600
1683
  min: :extreme, max: :extreme, minmax: :extreme,
1601
1684
  argmin: :extreme, argmax: :extreme,
1602
1685
  argmin_addr: :extreme, argmax_addr: :extreme,
1603
1686
  # cumulative: counter accumulation (the count family and accumulate)
1604
- count: :cumulative, accumulate: :cumulative,
1687
+ accumulate: :cumulative,
1605
1688
  count_true: :cumulative, count_false: :cumulative,
1606
1689
  count_equal: :cumulative,
1607
1690
  # variance family: multi-state (Chan / Welford), and the one that
@@ -1689,11 +1772,116 @@ module MkKernel
1689
1772
  base + HEADER_BLOCKS.join("\n")
1690
1773
  end
1691
1774
 
1775
+ # Emits the native function of one source data type. The object lane
1776
+ # calls Ruby for every cell, so any cell can raise in the middle of a walk;
1777
+ # its function is rebuilt around ca_iter_ensure (see guard_object_native).
1778
+ # Every other lane is emitted as the emitter writes it.
1779
+ def self.emit_native_for(io, k, src)
1780
+ if src == :object
1781
+ buf = StringIO.new
1782
+ yield buf
1783
+ io.print guard_object_native(buf.string, "#{k[:name]}_ki_native_object")
1784
+ else
1785
+ yield io
1786
+ end
1787
+ end
1788
+
1789
+ # Index of the brace that closes the one at text[open], skipping string
1790
+ # and character literals and comments.
1791
+ def self.matching_brace(text, open)
1792
+ depth = 0
1793
+ i = open
1794
+ while i < text.length
1795
+ c = text[i]
1796
+ case c
1797
+ when '"', "'"
1798
+ i += 1
1799
+ i += (text[i] == "\\") ? 2 : 1 while i < text.length && text[i] != c
1800
+ when "/"
1801
+ if text[i + 1] == "/"
1802
+ i = text.index("\n", i) || text.length
1803
+ elsif text[i + 1] == "*"
1804
+ i = (text.index("*/", i + 2) || text.length) + 1
1805
+ end
1806
+ when "{"
1807
+ depth += 1
1808
+ when "}"
1809
+ depth -= 1
1810
+ return i if depth == 0
1811
+ end
1812
+ i += 1
1813
+ end
1814
+ raise "mkkernel: unbalanced braces"
1815
+ end
1816
+
1817
+ # Rebuilds the native function +fn+ found in +text+ so that its walks are
1818
+ # finished however it leaves. The body moves into a function of its own
1819
+ # that ca_iter_ensure runs; each `ca_iter_state` it declared becomes a
1820
+ # slot of a frame kept by the caller, since a state in the body's own
1821
+ # frame is gone once the body raises. The body text is kept as written:
1822
+ # the parameters are unpacked from the frame under their own names, and
1823
+ # each state name is defined to its slot for the length of the body.
1824
+ def self.guard_object_native(text, fn)
1825
+ header = /^static VALUE\n#{fn} \(([^)]*)\)\n\{\n/
1826
+ m = header.match(text) or raise "mkkernel: #{fn} not found"
1827
+ close = matching_brace(text, m.end(0) - 2)
1828
+ body = text[m.end(0)...close]
1829
+ rest = text[(close + 1)..]
1830
+
1831
+ states = []
1832
+ body = body.gsub(/^[ \t]*ca_iter_state[ \t]+(\w+(?:[ \t]*,[ \t]*\w+)*)[ \t]*;[ \t]*\n/) do
1833
+ states.concat($1.split(/[ \t]*,[ \t]*/))
1834
+ ""
1835
+ end
1836
+ states.uniq!
1837
+ return text if states.empty?
1838
+
1839
+ params = m[1].split(",").map do |p|
1840
+ pm = p.strip.match(/\A(.*?)(\w+)\z/) or raise "mkkernel: #{fn} parameter #{p}"
1841
+ [pm[1].strip, pm[2]]
1842
+ end
1843
+ frame = "#{fn}_frame"
1844
+
1845
+ out = StringIO.new
1846
+ out.print m.pre_match
1847
+ out.puts "/* The object lane calls Ruby for every cell, so a walk can raise part"
1848
+ out.puts " way. Its states live in the caller's frame, and ca_iter_ensure"
1849
+ out.puts " finishes them however the body leaves. */"
1850
+ out.puts "typedef struct {"
1851
+ out.puts " ca_iter_state states[#{states.size}];"
1852
+ params.each { |type, name| out.puts " #{type} #{name};".sub(/\* /, "*") }
1853
+ out.puts "} #{frame};"
1854
+ out.puts
1855
+ out.puts "static VALUE"
1856
+ out.puts "#{fn}_body (VALUE arg)"
1857
+ out.puts "{"
1858
+ out.puts " #{frame} *fr = (#{frame} *) arg;"
1859
+ params.each do |type, name|
1860
+ out.puts " #{type} #{name} = fr->#{name};".sub(/\* /, "*")
1861
+ out.puts " (void) #{name};"
1862
+ end
1863
+ states.each_with_index { |s, i| out.puts "#define #{s} (fr->states[#{i}])" }
1864
+ out.print body.sub(/[ \t]*\z/, "")
1865
+ out.puts unless body.match?(/\n[ \t]*\z/)
1866
+ states.each { |s| out.puts "#undef #{s}" }
1867
+ out.puts "}"
1868
+ out.puts
1869
+ out.puts "static VALUE"
1870
+ out.puts "#{fn} (#{m[1]})"
1871
+ out.puts "{"
1872
+ out.puts " #{frame} fr;"
1873
+ params.each { |_, name| out.puts " fr.#{name} = #{name};" }
1874
+ out.puts " return ca_iter_ensure(#{states.size}, fr.states, #{fn}_body, (VALUE) &fr);"
1875
+ out.puts "}"
1876
+ out.print rest
1877
+ out.string
1878
+ end
1879
+
1692
1880
  def self.emit_reduce(io, k)
1693
1881
  io.puts
1694
1882
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
1695
1883
  k[:source].each do |src|
1696
- emit_reduce_native(io, k, src)
1884
+ emit_native_for(io, k, src) { |o| emit_reduce_native(o, k, src) }
1697
1885
  end
1698
1886
  emit_reduce_dispatch(io, k)
1699
1887
  end
@@ -1765,6 +1953,11 @@ module MkKernel
1765
1953
 
1766
1954
  view_flat = (k[:semantics] == :view_flat)
1767
1955
 
1956
+ if streamable
1957
+ emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var, acc_init,
1958
+ decls, reduce_stmt, finish_expr, src)
1959
+ end
1960
+
1768
1961
  io.puts
1769
1962
  io.puts <<~C
1770
1963
  static VALUE
@@ -1773,8 +1966,7 @@ module MkKernel
1773
1966
  C
1774
1967
 
1775
1968
  if streamable
1776
- emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var, acc_init,
1777
- decls, reduce_stmt, finish_expr, extra_args)
1969
+ emit_reduce_streaming_call(io, k, src)
1778
1970
  end
1779
1971
 
1780
1972
  # L.1 / L.3 / L.4 (PROPOSAL_REDUCTION_LOOP_INTERCHANGE):
@@ -1822,12 +2014,22 @@ module MkKernel
1822
2014
  else
1823
2015
  mixed_buf = false
1824
2016
  end
1825
- li_eligible = !k[:array_arg] && !k[:value_arg] &&
2017
+ # The object lane is left out: its cell is a Ruby call, so the
2018
+ # interchange's contiguous SIMD loads buy nothing, and each output cell
2019
+ # accumulates in the same order on the generic walk.
2020
+ li_eligible = src != :object &&
2021
+ !k[:array_arg] && !k[:value_arg] &&
1826
2022
  buf_count >= 1 && induction_count <= 1 && other_count == 0 &&
1827
2023
  !mixed_buf
1828
2024
  if li_eligible
2025
+ # The interchange folds every cell of an unmasked source, so it can
2026
+ # answer only when the reduced axis holds at least min_count cells;
2027
+ # otherwise the general walk writes the UNDEF (or fill_value) cells.
2028
+ li_gate = k[:mask_policy] == :min_count
2029
+ io.puts " if ( naxes != 1 || ! ca_too_few_present(ca->dim[slab_axes[0]], min_count, 0) ) {" if li_gate
1829
2030
  emit_reduce_loop_interchange(io, k, si, oi, ruby_wrap, src,
1830
2031
  acc_var, acc_init, reduce_stmt, finish_expr)
2032
+ io.puts " }" if li_gate
1831
2033
  end
1832
2034
 
1833
2035
  if view_flat
@@ -1837,7 +2039,7 @@ module MkKernel
1837
2039
  # (slab walks the whole array in view-flat row-major); other
1838
2040
  # naxes are rejected at entry.
1839
2041
  io.puts " if ( naxes != 1 && naxes != ca->ndim ) {"
1840
- 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);]
2042
+ 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);]
1841
2043
  io.puts " }"
1842
2044
  end
1843
2045
 
@@ -1851,7 +2053,7 @@ module MkKernel
1851
2053
  slab_axes, naxes, 0);
1852
2054
  if ( rc != CA_ITER_OK ) {
1853
2055
  rb_raise(rb_eRuntimeError,
1854
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
2056
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
1855
2057
  }
1856
2058
  char *p;
1857
2059
  boolean8_t *m;
@@ -1889,7 +2091,7 @@ module MkKernel
1889
2091
  io.puts " slab_axes, naxes, 0);"
1890
2092
  io.puts " if ( rcw != CA_ITER_OK ) {"
1891
2093
  io.puts " ca_iter_state_finish(&st);"
1892
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: weights kernel_iterator init failed rc=%d", rcw);]
2094
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: weights kernel_iterator init failed rc=%d", ca_calling_method_name(), rcw);]
1893
2095
  io.puts " }"
1894
2096
  io.puts " char *p_w;"
1895
2097
  end
@@ -1904,7 +2106,7 @@ module MkKernel
1904
2106
  io.puts " if ( ! ca_iter_state_next_slab_axes(&st_w, &p_w, NULL) ) {"
1905
2107
  io.puts " ca_iter_state_finish(&st);"
1906
2108
  io.puts " ca_iter_state_finish(&st_w);"
1907
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: weights iter exhausted early (shape invariant violated)");]
2109
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: weights iter exhausted early (shape invariant violated)", ca_calling_method_name());]
1908
2110
  io.puts " }"
1909
2111
  else
1910
2112
  io.puts " while ( ca_iter_state_next_slab_axes(&st, &p, &m) ) {"
@@ -1934,27 +2136,32 @@ module MkKernel
1934
2136
  emit_reduce_slab_call(io, k, src, si, oi, suffix, acc_var, acc_init,
1935
2137
  reduce_stmt, "masked_cnt", indent: " ")
1936
2138
  end
2139
+ all_nan_flag = nil
2140
+ if k[:all_nan_result]
2141
+ pairs = limit_init_state_vars(k, si, oi, src)
2142
+ if k[:all_nan_result] == :undef && FLOAT_DTYPES.include?(src)
2143
+ all_nan_flag = "__anf_all_nan"
2144
+ io.puts " int #{all_nan_flag} = 0;"
2145
+ elsif k[:all_nan_result] == :undef && src == :object
2146
+ # The object body takes no NaN cell, so a slab of present NaN
2147
+ # cells leaves the position unset.
2148
+ all_nan_flag = "__anf_all_nan"
2149
+ io.puts " int #{all_nan_flag} = ( masked_cnt < st.slab_elements && (#{finish_expr}) < 0 );"
2150
+ end
2151
+ emit_all_nan_fixup(io, k, src, si, pairs,
2152
+ "masked_cnt < st.slab_elements",
2153
+ indent: " ", flag_var: all_nan_flag)
2154
+ end
1937
2155
  trigger = case k[:mask_policy]
1938
2156
  when :strict then "masked_cnt > 0"
1939
- when :all_masked then "masked_cnt == st.slab_elements"
2157
+ when :all_masked then "ca_too_few_present(st.slab_elements - masked_cnt, -1, 1)"
1940
2158
  when :min_count
1941
- # min_count < 0 (sentinel) => legacy default (all_masked).
1942
- # min_count >= 0 => need at least min_count valid cells
1943
- # (valid_cnt = slab_elements - masked_cnt).
1944
- #
1945
- # ERI.0: identity_on_empty kernels do NOT fire UNDEF for the
1946
- # default (min_count < 0) zero-contribution case; the trigger
1947
- # is false there, so the else branch writes finish (= acc =
1948
- # identity) for empty / all-masked slabs. The explicit
1949
- # min_count >= 0 opt-in ("require K valid cells") is unchanged.
1950
- if k[:identity_on_empty]
1951
- "(min_count < 0 ? 0 " \
1952
- ": st.slab_elements - masked_cnt < min_count)"
1953
- else
1954
- "(min_count < 0 ? masked_cnt == st.slab_elements " \
1955
- ": st.slab_elements - masked_cnt < min_count)"
1956
- end
2159
+ # An identity_on_empty kernel needs no cell (the else
2160
+ # branch writes finish = acc = identity for an empty slab).
2161
+ "ca_too_few_present(st.slab_elements - masked_cnt, min_count, " \
2162
+ "#{k[:identity_on_empty] ? 0 : 1})"
1957
2163
  end
2164
+ trigger = "(#{trigger}) || #{all_nan_flag}" if all_nan_flag
1958
2165
  finish_emit = view_flat \
1959
2166
  ? "(transform_active ? (outer_off + ((ca_size_t)(#{finish_expr})) * axis_vstride) : ((ca_size_t)(#{finish_expr})))" \
1960
2167
  : "(#{finish_expr})"
@@ -2037,13 +2244,15 @@ module MkKernel
2037
2244
  # the numeric argmin's best_i. mask_policy is always :min_count here.
2038
2245
  def self.emit_reduce_native_fixlen(io, k, src)
2039
2246
  name = k[:name]
2247
+ counting = k[:fixlen] == :count_equal
2040
2248
  want_max = %i[max argmax].include?(k[:fixlen])
2041
- index_out = %i[argmin argmax].include?(k[:fixlen])
2249
+ index_out = %i[argmin argmax].include?(k[:fixlen]) || counting
2042
2250
  cmp = want_max ? ">" : "<"
2251
+ varg = counting ? ", const char *value_arg" : ""
2043
2252
 
2044
2253
  io.puts
2045
2254
  io.puts "static VALUE"
2046
- io.puts "#{name}_ki_native_fixlen (VALUE self, CArray *ca, int8_t *slab_axes, int8_t naxes, int keep_axis, ca_size_t min_count)"
2255
+ io.puts "#{name}_ki_native_fixlen (VALUE self, CArray *ca, int8_t *slab_axes, int8_t naxes, int keep_axis#{varg}, ca_size_t min_count)"
2047
2256
  io.puts "{"
2048
2257
  io.puts " ca_size_t K = ca->bytes; /* uniform fixlen byte width */"
2049
2258
  if index_out
@@ -2057,16 +2266,20 @@ module MkKernel
2057
2266
  io.puts " ca_iter_state st;"
2058
2267
  io.puts " int rc = ca_iter_state_init_l2(&st, ca, CA_SLAB_AXES, slab_axes, naxes, 0);"
2059
2268
  io.puts " if ( rc != CA_ITER_OK ) {"
2060
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: kernel_iterator init failed rc=%d", rc);]
2269
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);]
2061
2270
  io.puts " }"
2062
2271
  io.puts " char *p;"
2063
2272
  io.puts " boolean8_t *m;"
2064
2273
  io.puts " ca_size_t out_i = 0;"
2065
2274
  io.puts " boolean8_t *op_mask = NULL; /* lazily allocated on first UNDEF */"
2066
2275
  io.puts " while ( ca_iter_state_next_slab_axes(&st, &p, &m) ) {"
2067
- io.puts " const char *best = NULL;"
2068
- io.puts " ca_size_t best_i = 0;"
2069
- io.puts " (void) best_i;" unless index_out # value output ignores the index
2276
+ if counting
2277
+ io.puts " int64_t cnt = 0;"
2278
+ else
2279
+ io.puts " const char *best = NULL;"
2280
+ io.puts " ca_size_t best_i = 0;"
2281
+ io.puts " (void) best_i;" unless index_out # value output ignores the index
2282
+ end
2070
2283
  io.puts " ca_size_t masked_cnt = 0;"
2071
2284
  io.puts " int8_t sndim = st.slab_ndim;"
2072
2285
  io.puts " ca_size_t sidx[CA_RANK_MAX] = { 0 };"
@@ -2082,12 +2295,18 @@ module MkKernel
2082
2295
  io.puts " }"
2083
2296
  io.puts " else {"
2084
2297
  io.puts " const char *q = (const char *) p + doff;"
2085
- io.puts " if ( best == NULL ) {"
2086
- io.puts " best = q; best_i = idx;"
2087
- io.puts " }"
2088
- io.puts " else if ( memcmp(q, best, (size_t) K) #{cmp} 0 ) {"
2089
- io.puts " best = q; best_i = idx;"
2090
- io.puts " }"
2298
+ if counting
2299
+ io.puts " if ( memcmp(q, value_arg, (size_t) K) == 0 ) {"
2300
+ io.puts " cnt++;"
2301
+ io.puts " }"
2302
+ else
2303
+ io.puts " if ( best == NULL ) {"
2304
+ io.puts " best = q; best_i = idx;"
2305
+ io.puts " }"
2306
+ io.puts " else if ( memcmp(q, best, (size_t) K) #{cmp} 0 ) {"
2307
+ io.puts " best = q; best_i = idx;"
2308
+ io.puts " }"
2309
+ end
2091
2310
  io.puts " }"
2092
2311
  io.puts " /* row-major odometer (innermost slab axis fastest) so idx"
2093
2312
  io.puts " matches CA_SLAB_REDUCE_T's flat slab index. */"
@@ -2096,8 +2315,9 @@ module MkKernel
2096
2315
  io.puts " sidx[sk] = 0;"
2097
2316
  io.puts " }"
2098
2317
  io.puts " }"
2099
- io.puts " if ( min_count < 0 ? masked_cnt == st.slab_elements"
2100
- io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2318
+ # A count over nothing is 0, not UNDEF: it needs no cell.
2319
+ io.puts " if ( ca_too_few_present(st.slab_elements - masked_cnt, min_count, " \
2320
+ "#{counting ? 0 : 1}) ) {"
2101
2321
  io.puts " if ( ! op_mask ) {"
2102
2322
  io.puts " ca_create_mask(co);"
2103
2323
  io.puts " op_mask = (boolean8_t *) co->mask->ptr;"
@@ -2111,7 +2331,9 @@ module MkKernel
2111
2331
  io.puts " out_i++;"
2112
2332
  io.puts " }"
2113
2333
  io.puts " else {"
2114
- if index_out
2334
+ if counting
2335
+ io.puts " ((int64_t *) op)[out_i] = cnt;"
2336
+ elsif index_out
2115
2337
  io.puts " ((int64_t *) op)[out_i] = (int64_t) best_i;"
2116
2338
  else
2117
2339
  io.puts " memcpy(op + out_i * K, best, (size_t) K);"
@@ -2148,10 +2370,8 @@ module MkKernel
2148
2370
  # Layout per slab:
2149
2371
  # Pass 1: sum = Σ x_i via CA_SLAB_REDUCE_T_PLUS_EX (SIMD :plus)
2150
2372
  # masked_cnt is populated as a side effect.
2151
- # mask trigger:
2152
- # min_count < 0 (default) -> UNDEF iff all-masked, OR (for
2153
- # :n_minus_1 divisor) if n_valid < 2.
2154
- # min_count >= 0 -> UNDEF iff n_valid < min_count.
2373
+ # mask trigger: ca_too_few_present(n_valid, min_count, 1 or 2), 2 for
2374
+ # the :n_minus_1 divisor.
2155
2375
  # Pass 2: M2 = Σ (x_i - mean)² via the same macro with a centred
2156
2376
  # REDUCE stmt.
2157
2377
  # Finish: variance = M2 / DIVISOR (n or n-1); stddev variant applies
@@ -2258,9 +2478,9 @@ module MkKernel
2258
2478
  finish_expr = if k[:output_transform] == :sqrt_clamp
2259
2479
  if src == :object
2260
2480
  # Object stddev outputs f64 (BigMath.sqrt is user's job).
2261
- "sqrt(fmax(NUM2DBL(#{inner_var}), 0.0))"
2481
+ "ca_sqrt_clamp(NUM2DBL(#{inner_var}))"
2262
2482
  else
2263
- "sqrt(fmax(#{inner_var}, 0.0))"
2483
+ "ca_sqrt_clamp(#{inner_var})"
2264
2484
  end
2265
2485
  else
2266
2486
  inner_var
@@ -2288,13 +2508,9 @@ module MkKernel
2288
2508
  # min_count sentinel arg is always present (all four kernels use it).
2289
2509
  extra_args = ", ca_size_t min_count"
2290
2510
 
2291
- # The default (min_count < 0) trigger fires when all cells are
2292
- # masked; for sample-variance/stddev we also need n_valid >= 2 to
2293
- # avoid division by zero, but the legacy behaviour returned 0
2294
- # (not UNDEF) for n_valid < required_min. Preserve that: the
2295
- # default trigger only checks the all-masked case; the sub-required
2296
- # case falls through to the "return zero" branch below.
2297
- default_trigger = "masked_cnt == st.slab_elements"
2511
+ # Fewer than required_min valid cells (2 for the n-1 divisor) leave the
2512
+ # statistic undefined, so the cell is UNDEF: the sample variance of one
2513
+ # value is 0/0.
2298
2514
 
2299
2515
  # Reduce-call plumbing. :object keeps the scalar rb_funcall loop
2300
2516
  # (CA_SLAB_REDUCE_T_EX). Every other src uses the 8-way accumulator
@@ -2335,7 +2551,7 @@ module MkKernel
2335
2551
  slab_axes, naxes, 0);
2336
2552
  if ( rc != CA_ITER_OK ) {
2337
2553
  rb_raise(rb_eRuntimeError,
2338
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
2554
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
2339
2555
  }
2340
2556
  char *p;
2341
2557
  boolean8_t *m;
@@ -2347,7 +2563,7 @@ module MkKernel
2347
2563
  ca_size_t masked_cnt = 0;
2348
2564
  #{p1_reduce_call}
2349
2565
  ca_size_t n_valid = st.slab_elements - masked_cnt;
2350
- int __trigger = (min_count < 0 ? #{default_trigger} : n_valid < min_count);
2566
+ int __trigger = ca_too_few_present(n_valid, min_count, #{required_min});
2351
2567
  if ( __trigger ) {
2352
2568
  if ( ! op_mask ) {
2353
2569
  ca_create_mask(co);
@@ -2355,11 +2571,6 @@ module MkKernel
2355
2571
  }
2356
2572
  op_mask[out_i] = 1;
2357
2573
  op[out_i++] = (#{out_c}) #{out_zero_typed};
2358
- } else if ( n_valid < #{required_min} ) {
2359
- /* Legacy degenerate case (e.g. sample variance with 1 unmasked
2360
- cell): return zero rather than UNDEF, matching pre-migration
2361
- behaviour of `cnt > 1 ? formula : 0`. */
2362
- op[out_i++] = (#{out_c}) #{out_zero_typed};
2363
2574
  } else {
2364
2575
  #{mean_setup}
2365
2576
  /* Pass 2: M2 = Σ (x - mean)² */
@@ -2439,6 +2650,49 @@ module MkKernel
2439
2650
  buf_decls = plus_info.map do |pi|
2440
2651
  " #{pi[:c_type]} #{pi[:buf]}[512];\n"
2441
2652
  end.join
2653
+
2654
+ # all-NaN fix-up for the tiled core (see emit_all_nan_fixup for the
2655
+ # reduction counterpart). Each output cell of a tile has its own
2656
+ # accumulator, so the check is per column: a column whose accumulator
2657
+ # is still at the init either saw only NaN or only +/-INFINITY.
2658
+ #
2659
+ # The `_Pragma("omp simd")` j-loop is left alone -- carrying a
2660
+ # "saw a number" flag beside the accumulator would double its loads
2661
+ # and stores on every cell of every column. Instead the tile is
2662
+ # tested once after the M loop (tile_len comparisons against M *
2663
+ # tile_len already done), and only a tile that holds a suspicious
2664
+ # column re-reads its rows.
2665
+ li_nan_fixup =
2666
+ if k[:all_nan_result] == :nan && FLOAT_DTYPES.include?(src) &&
2667
+ plus_info.size == 1
2668
+ pi = plus_info.first
2669
+ seen_buf = "__li_buf_seen"
2670
+ lambda do |m_step|
2671
+ <<~C.rstrip
2672
+ {
2673
+ int __li_susp = 0;
2674
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2675
+ if ( #{pi[:buf]}[__j] == (#{pi[:c_type]}) (#{pi[:init]}) ) { __li_susp = 1; break; }
2676
+ }
2677
+ if ( __li_susp ) {
2678
+ int8_t #{seen_buf}[512];
2679
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) #{seen_buf}[__j] = 0;
2680
+ for ( ca_size_t __li_i = 0; __li_i < __li_M; __li_i++ ) {
2681
+ const #{si[:c]} *__li_row = __li_plane + __li_i * #{m_step} + __li_tile;
2682
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2683
+ #{seen_buf}[__j] |= (__li_row[__j] == __li_row[__j]);
2684
+ }
2685
+ }
2686
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2687
+ if ( ! #{seen_buf}[__j] ) #{pi[:buf]}[__j] = (#{pi[:c_type]}) NAN;
2688
+ }
2689
+ }
2690
+ }
2691
+ C
2692
+ end
2693
+ else
2694
+ lambda { |_m_step| "" }
2695
+ end
2442
2696
  init_loops = plus_info.map do |pi|
2443
2697
  " for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {\n" \
2444
2698
  " #{pi[:buf]}[__j] = (#{pi[:c_type]}) (#{pi[:init]});\n" \
@@ -2468,6 +2722,7 @@ module MkKernel
2468
2722
  (void) v;
2469
2723
  }
2470
2724
  }
2725
+ #{li_nan_fixup.call("__li_INNER")}
2471
2726
  for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2472
2727
  #{opv}[__li_o * __li_INNER + __li_tile + __j] = (#{oi[:c]}) (#{finish_li});
2473
2728
  }
@@ -2487,8 +2742,9 @@ module MkKernel
2487
2742
  exposes ca->ptr as that buffer at O(1) cost (no materialise). */
2488
2743
  if ( naxes == 1
2489
2744
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2490
- && ca->mask == NULL
2491
- && ca_attach_is_alias(ca) ) {
2745
+ && ! ca_has_mask(ca)
2746
+ && ca_attach_is_alias(ca)
2747
+ && ca_fast_paths_enabled() ) {
2492
2748
  int8_t __li_ax = slab_axes[0];
2493
2749
  ca_size_t __li_M = ca->dim[__li_ax];
2494
2750
  ca_size_t __li_INNER = 1;
@@ -2502,7 +2758,7 @@ module MkKernel
2502
2758
  CArray *co;
2503
2759
  GetCArray(vout, co);
2504
2760
  #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
2505
- ca_attach(ca); /* O(1): entity no-op or alias attach */
2761
+ ca_attach(ca); /* window: numeric lanes only, nothing raises inside; O(1) alias attach */
2506
2762
  const #{si[:c]} *p_root = (const #{si[:c]} *) ca->ptr;
2507
2763
 
2508
2764
  /* Per-buffer stack accumulators (one per :plus state, sized
@@ -2536,9 +2792,10 @@ module MkKernel
2536
2792
  if ( naxes == 1
2537
2793
  && slab_axes[0] >= 1
2538
2794
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2539
- && ca->mask == NULL
2795
+ && ! ca_has_mask(ca)
2540
2796
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2541
- && ((CAStack *) ca)->k_axis == 0 ) {
2797
+ && ((CAStack *) ca)->k_axis == 0
2798
+ && ca_fast_paths_enabled() ) {
2542
2799
  CAStack *__li_st = (CAStack *) ca;
2543
2800
  int8_t __li_ax = slab_axes[0];
2544
2801
  ca_size_t __li_M = ca->dim[__li_ax];
@@ -2551,7 +2808,7 @@ module MkKernel
2551
2808
  core reads parent->ptr without mask awareness). */
2552
2809
  int __li_ok = 1;
2553
2810
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2554
- if ( __li_st->parents[__kk]->mask != NULL ) { __li_ok = 0; break; }
2811
+ if ( ca_has_mask(__li_st->parents[__kk]) ) { __li_ok = 0; break; }
2555
2812
  }
2556
2813
 
2557
2814
  if ( __li_ok && __li_INNER >= 64 && __li_M * __li_INNER >= 1024 ) {
@@ -2566,7 +2823,7 @@ module MkKernel
2566
2823
  ca_size_t __li_parent_out = __li_OUTER * __li_INNER;
2567
2824
  const ca_size_t __li_TILE = 512;
2568
2825
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2569
- ca_attach(__li_st->parents[__kk]);
2826
+ ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2570
2827
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2571
2828
  #{oi[:c]} *__li_op_k = op + (ca_size_t) __kk * __li_parent_out;
2572
2829
  #{core.call("p_root", "__li_op_k", "__li_OUTER")}
@@ -2599,11 +2856,12 @@ module MkKernel
2599
2856
  if ( naxes == 1
2600
2857
  && slab_axes[0] >= 1
2601
2858
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2602
- && ca->mask == NULL
2859
+ && ! ca_has_mask(ca)
2603
2860
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2604
2861
  && ((CAStack *) ca)->k_axis > 0
2605
2862
  && ( slab_axes[0] > ((CAStack *) ca)->k_axis
2606
- || slab_axes[0] + 1 == ((CAStack *) ca)->k_axis ) ) {
2863
+ || slab_axes[0] + 1 == ((CAStack *) ca)->k_axis )
2864
+ && ca_fast_paths_enabled() ) {
2607
2865
  CAStack *__li_st = (CAStack *) ca;
2608
2866
  int8_t __li_ax = slab_axes[0];
2609
2867
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -2619,7 +2877,7 @@ module MkKernel
2619
2877
  /* Bail to the generic path if any parent carries a mask. */
2620
2878
  int __li_ok = 1;
2621
2879
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2622
- if ( __li_st->parents[__kk]->mask != NULL ) { __li_ok = 0; break; }
2880
+ if ( ca_has_mask(__li_st->parents[__kk]) ) { __li_ok = 0; break; }
2623
2881
  }
2624
2882
 
2625
2883
  if ( __li_ok && __li_INNER >= 64 && __li_M * __li_INNER >= 1024 ) {
@@ -2660,7 +2918,7 @@ module MkKernel
2660
2918
  ca_size_t __li_plane_elems = __li_M * __li_INNER;
2661
2919
  const ca_size_t __li_TILE = 512;
2662
2920
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2663
- ca_attach(__li_st->parents[__kk]);
2921
+ ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2664
2922
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2665
2923
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
2666
2924
 
@@ -2688,6 +2946,7 @@ module MkKernel
2688
2946
  (void) v;
2689
2947
  }
2690
2948
  }
2949
+ #{li_nan_fixup.call("__li_INNER")}
2691
2950
  for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2692
2951
  __li_op_k[__li_tile + __j] = (#{oi[:c]}) (#{finish_li});
2693
2952
  }
@@ -2741,10 +3000,11 @@ module MkKernel
2741
3000
  if ( naxes == 1
2742
3001
  && slab_axes[0] >= 1
2743
3002
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2744
- && ca->mask == NULL
3003
+ && ! ca_has_mask(ca)
2745
3004
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2746
3005
  && ((CAStack *) ca)->k_axis > 0
2747
- && slab_axes[0] + 1 < ((CAStack *) ca)->k_axis ) {
3006
+ && slab_axes[0] + 1 < ((CAStack *) ca)->k_axis
3007
+ && ca_fast_paths_enabled() ) {
2748
3008
  CAStack *__li_st = (CAStack *) ca;
2749
3009
  int8_t __li_ax = slab_axes[0];
2750
3010
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -2762,7 +3022,7 @@ module MkKernel
2762
3022
  /* Bail to the generic path if any parent carries a mask. */
2763
3023
  int __li_ok = 1;
2764
3024
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2765
- if ( __li_st->parents[__kk]->mask != NULL ) { __li_ok = 0; break; }
3025
+ if ( ca_has_mask(__li_st->parents[__kk]) ) { __li_ok = 0; break; }
2766
3026
  }
2767
3027
 
2768
3028
  if ( __li_ok && __li_eff_INNER >= 32 && __li_M * __li_eff_INNER >= 1024 ) {
@@ -2816,7 +3076,7 @@ module MkKernel
2816
3076
 
2817
3077
  const ca_size_t __li_TILE = 512;
2818
3078
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2819
- ca_attach(__li_st->parents[__kk]);
3079
+ ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2820
3080
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2821
3081
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
2822
3082
 
@@ -2846,6 +3106,7 @@ module MkKernel
2846
3106
  (void) v;
2847
3107
  }
2848
3108
  }
3109
+ #{li_nan_fixup.call("__li_M_stride")}
2849
3110
  for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2850
3111
  __li_op_k[__li_tile + __j] = (#{oi[:c]}) (#{finish_li});
2851
3112
  }
@@ -2922,7 +3183,7 @@ module MkKernel
2922
3183
  slab_axes, naxes, 0);
2923
3184
  if ( rc != CA_ITER_OK ) {
2924
3185
  rb_raise(rb_eRuntimeError,
2925
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
3186
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
2926
3187
  }
2927
3188
  char *p;
2928
3189
  boolean8_t *m;
@@ -2937,10 +3198,11 @@ module MkKernel
2937
3198
  if min_count
2938
3199
  io.puts " ca_size_t masked_cnt = 0;"
2939
3200
  io.puts " CA_SLAB_REDUCE_T_EX(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt}, masked_cnt);"
2940
- # Same trigger as single-output :min_count: legacy default (all_masked)
2941
- # when min_count < 0, otherwise need at least min_count valid cells.
2942
- trigger = "(min_count < 0 ? masked_cnt == st.slab_elements " \
2943
- ": st.slab_elements - masked_cnt < min_count)"
3201
+ if k[:all_nan_result]
3202
+ emit_all_nan_fixup(io, k, src, si, limit_init_state_vars(k, si, oi, src),
3203
+ "masked_cnt < st.slab_elements", indent: " ")
3204
+ end
3205
+ trigger = "ca_too_few_present(st.slab_elements - masked_cnt, min_count, 1)"
2944
3206
  io.puts " if ( #{trigger} ) {"
2945
3207
  io.puts " if ( ! op_mask_a ) {"
2946
3208
  io.puts " ca_create_mask(co_a);"
@@ -2974,10 +3236,11 @@ module MkKernel
2974
3236
  # pair of [1,...,1] entities instead.
2975
3237
  io.puts " if ( naxes == ca->ndim && !keep_axis ) {"
2976
3238
  if min_count
2977
- # Full reduction: if the sole output cell is masked, return CA_UNDEF.
2978
- # (Both op_mask_a and op_mask_b share the same bit since both
2979
- # reductions consume the same slab, so checking _a is sufficient.)
2980
- io.puts " if ( op_mask_a && op_mask_a[0] ) return CA_UNDEF;"
3239
+ # Full reduction: if the sole output cell is masked, both answers are
3240
+ # UNDEF, still as a pair -- the shape of the answer does not depend on
3241
+ # the data. (Both op_mask_a and op_mask_b share the same bit since
3242
+ # both reductions consume the same slab, so checking _a is sufficient.)
3243
+ io.puts " if ( op_mask_a && op_mask_a[0] ) return rb_assoc_new(CA_UNDEF, CA_UNDEF);"
2981
3244
  end
2982
3245
  io.puts " return rb_assoc_new(#{ruby_wrap}(op_a[0]), #{ruby_wrap}(op_b[0]));"
2983
3246
  io.puts " }"
@@ -2985,42 +3248,40 @@ module MkKernel
2985
3248
  io.puts " }"
2986
3249
  end
2987
3250
 
2988
- # P.4.5.3b/c: streaming chunked reduce path emitter. Generates an
2989
- # early-return branch at the top of name_ki_native_<src> that pulls
2990
- # the lazy view in fixed-size chunks via ca_xfer_stride instead of
2991
- # full-materialising via SRC_ATTACH. Falls through to the existing
2992
- # path when conditions aren't met.
2993
- #
2994
- # Strategy: chunk along the outermost axis. inner = prod(dim[1..]);
2995
- # rows_per_chunk = max(1, target_elems / inner). 1-D is the natural
2996
- # sub-case (inner == 1, rows = target_elems).
2997
- #
2998
- # Memory peak: O(max(target_elems, inner) * sizeof T) instead of O(N).
2999
- # For N-D, this drops the outermost dim from the materialised buffer,
3000
- # which is typically the dominant factor (e.g. 1000x10000 f64 reduction:
3001
- # 80MB -> 80KB). Wall-clock perf at large N matches or beats
3002
- # SRC_ATTACH because each chunk fits in L1d (or L2 for wide last dims).
3003
- #
3004
- # Restrictions handled by short-circuit checks (= falls through to
3005
- # SRC_ATTACH path):
3006
- # - axis kwarg (= partial reduction): naxes != ca->ndim
3007
- # (structural blocker: CABinOp/CABinCmp xfer_stride rebuilds
3008
- # right_strides from counts, so non-rectangular slab pulls
3009
- # produce wrong values for the right operand. See proposal
3010
- # §future-work / partial streaming blocker.)
3011
- # - non-lazy source: !ca_is_lazy_view(ca)
3012
- # - mask present: ca_has_mask(ca)
3251
+ # The walk of the streaming reduce (see emit_reduce_streaming_call).
3013
3252
  def self.emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var,
3014
3253
  acc_init, decls, reduce_stmt,
3015
- finish_expr, extra_args)
3254
+ finish_expr, src)
3016
3255
  name = k[:name]
3017
- min_count = (k[:mask_policy] == :min_count)
3018
3256
  has_mp = !k[:mask_policy].nil?
3257
+ fn = "#{name}_ki_stream_#{src}"
3019
3258
 
3020
- io.puts " /* P.4.5.3b/c streaming chunked reduce: full reduction over lazy unmasked. */"
3021
- io.puts " /* P.4.5.3c (N-D): outer-axis chunking, 1-D is the natural sub-case. */"
3022
- io.puts " if ( naxes == ca->ndim && ca->ndim >= 1 && !keep_axis &&"
3023
- io.puts " ca_is_lazy_view(ca) && ! ca_has_mask(ca) ) {"
3259
+ # The walk runs under ca_lazy_arena_protect: the transfer from the lazy
3260
+ # source and the object lane's fold can both raise, and the arena must
3261
+ # be left however the walk ends. So it is a function of its own, with
3262
+ # the native's arguments carried in a context whose first member is
3263
+ # the chunk (released by ca_lazy_arena_release_held).
3264
+ io.puts
3265
+ io.puts "typedef struct {"
3266
+ io.puts " void *chunk;"
3267
+ io.puts " CArray *ca;"
3268
+ io.puts " #{si[:c]} value_arg;" if k[:value_arg]
3269
+ io.puts " ca_size_t min_count;" if k[:mask_policy] == :min_count
3270
+ io.puts "} #{fn}_t;"
3271
+ io.puts
3272
+ io.puts "static VALUE"
3273
+ io.puts "#{fn} (VALUE arg)"
3274
+ io.puts "{"
3275
+ io.puts " #{fn}_t *__ctx = (#{fn}_t *) arg;"
3276
+ io.puts " CArray *ca = __ctx->ca;"
3277
+ if k[:value_arg]
3278
+ io.puts " #{si[:c]} value_arg = __ctx->value_arg;"
3279
+ io.puts " (void) value_arg;"
3280
+ end
3281
+ if k[:mask_policy] == :min_count
3282
+ io.puts " ca_size_t min_count = __ctx->min_count;"
3283
+ io.puts " (void) min_count;"
3284
+ end
3024
3285
  io.puts " ca_size_t __inner = 1;"
3025
3286
  io.puts " int __k;"
3026
3287
  io.puts " for ( __k = 1; __k < ca->ndim; __k++ ) __inner *= ca->dim[__k];"
@@ -3043,6 +3304,14 @@ module MkKernel
3043
3304
  # acc_var requires explicit init (= the macro normally does this).
3044
3305
  # Other state vars in decls already include `= init` per line.
3045
3306
  io.puts " #{acc_var} = (#{acc_init});"
3307
+ # all-NaN fix-up, streaming variant. The other two paths confirm a
3308
+ # suspicious answer by re-reading the data; here re-reading means
3309
+ # evaluating the lazy chain a second time, so the flag rides along in
3310
+ # the chunk loop instead. The loop already pays for producing each
3311
+ # cell, which is what makes one more compare affordable here and not
3312
+ # in the tiled core.
3313
+ all_nan_stream = k[:all_nan_result] == :nan && FLOAT_DTYPES.include?(src)
3314
+ io.puts " int64_t __anf_seen = 0;" if all_nan_stream
3046
3315
  if has_mp
3047
3316
  # Mask-policy reductions need masked_cnt to satisfy the macro/
3048
3317
  # finish_expr signature. On streaming we have no mask, so it's
@@ -3050,7 +3319,6 @@ module MkKernel
3050
3319
  io.puts " ca_size_t masked_cnt = 0; /* unused on streaming (no mask) */"
3051
3320
  io.puts " (void) masked_cnt;"
3052
3321
  end
3053
- io.puts " ca_lazy_arena_enter();"
3054
3322
  if si[:c] == "VALUE"
3055
3323
  # Object lane: the chunk holds VALUEs pulled from the source, and
3056
3324
  # the fold below calls rb_funcall per cell. A collection there
@@ -3060,6 +3328,7 @@ module MkKernel
3060
3328
  else
3061
3329
  io.puts " #{si[:c]} *__chunk = (#{si[:c]} *) ca_lazy_arena_acquire(__chunk_elems * sizeof(#{si[:c]}));"
3062
3330
  end
3331
+ io.puts " __ctx->chunk = __chunk;"
3063
3332
  io.puts " while ( __outer_off < __outer ) {"
3064
3333
  io.puts " ca_size_t __r = (__outer - __outer_off < __rows) ? (__outer - __outer_off) : __rows;"
3065
3334
  io.puts " ca_size_t __n = __r * __inner;"
@@ -3070,23 +3339,90 @@ module MkKernel
3070
3339
  io.puts " for ( __i = 0; __i < __n; __i++ ) {"
3071
3340
  io.puts " #{si[:c]} v = __chunk[__i];"
3072
3341
  io.puts " #{reduce_stmt};"
3342
+ io.puts " __anf_seen |= (v == v);" if all_nan_stream
3073
3343
  io.puts " }"
3074
3344
  io.puts " __outer_off += __r;"
3075
3345
  io.puts " }"
3076
- io.puts " ca_lazy_arena_release(__chunk);"
3077
- io.puts " ca_lazy_arena_exit();"
3078
- if has_mp
3079
- # Streaming path has no mask source, so masked_cnt is 0; min_count
3080
- # / strict / all_masked triggers all evaluate to false except
3081
- # min_count when explicit > 0 is set on an all-clean source (= no
3082
- # trigger). Output is the finish expression directly.
3083
- io.puts " return #{ruby_wrap}((#{oi[:c]}) (#{finish_expr}));"
3084
- else
3085
- io.puts " return #{ruby_wrap}((#{oi[:c]}) (#{finish_expr}));"
3346
+ if all_nan_stream
3347
+ io.puts " if ( ! __anf_seen && ca->elements > 0 ) #{acc_var} = (#{si[:c]}) NAN;"
3086
3348
  end
3349
+ # Streaming has no mask source, so masked_cnt is 0 and none of the
3350
+ # mask-policy triggers fire: the answer is the finish expression.
3351
+ io.puts " return #{ruby_wrap}((#{oi[:c]}) (#{finish_expr}));"
3352
+ io.puts "}"
3353
+ end
3354
+
3355
+ # The streaming branch at the top of name_ki_native_<src>: a flat
3356
+ # reduction of an unmasked lazy view pulls the source in fixed-size
3357
+ # chunks via ca_xfer_stride instead of materialising it whole. Falls
3358
+ # through to the existing path when the conditions are not met.
3359
+ #
3360
+ # Strategy: chunk along the outermost axis. inner = prod(dim[1..]);
3361
+ # rows_per_chunk = max(1, target_elems / inner). 1-D is the natural
3362
+ # sub-case (inner == 1, rows = target_elems).
3363
+ #
3364
+ # Memory peak: O(max(target_elems, inner) * sizeof T) instead of O(N).
3365
+ # For N-D, this drops the outermost dim from the materialised buffer,
3366
+ # which is typically the dominant factor (e.g. 1000x10000 f64 reduction:
3367
+ # 80MB -> 80KB). Wall-clock perf at large N matches or beats
3368
+ # SRC_ATTACH because each chunk fits in L1d (or L2 for wide last dims).
3369
+ #
3370
+ # Restrictions handled by short-circuit checks (= falls through to
3371
+ # SRC_ATTACH path):
3372
+ # - axis kwarg (= partial reduction): naxes != ca->ndim
3373
+ # (structural blocker: CABinOp/CABinCmp xfer_stride rebuilds
3374
+ # right_strides from counts, so non-rectangular slab pulls
3375
+ # produce wrong values for the right operand.)
3376
+ # - non-lazy source: !ca_is_lazy_view(ca)
3377
+ # - mask present: ca_has_mask(ca)
3378
+ # - nothing to fold, or fewer cells than min_count asks for: the walk
3379
+ # has no mask, so it answers the finish expression, which is right
3380
+ # only when the mask policy cannot fire. An empty source and a
3381
+ # min_count above the cell count are the two ways it can without a
3382
+ # mask, and the full path answers them.
3383
+ def self.emit_reduce_streaming_call(io, k, src)
3384
+ fn = "#{k[:name]}_ki_stream_#{src}"
3385
+ io.puts " if ( naxes == ca->ndim && ca->ndim >= 1 && !keep_axis &&"
3386
+ io.puts " ca->elements > 0 &&"
3387
+ io.puts " ! ca_too_few_present(ca->elements, min_count, 0) &&" if k[:mask_policy] == :min_count
3388
+ io.puts " ca_is_lazy_view(ca) && ! ca_has_mask(ca) &&"
3389
+ io.puts " ca_fast_paths_enabled() ) {"
3390
+ io.puts " #{fn}_t __ctx;"
3391
+ io.puts " __ctx.chunk = NULL;"
3392
+ io.puts " __ctx.ca = ca;"
3393
+ io.puts " __ctx.value_arg = value_arg;" if k[:value_arg]
3394
+ io.puts " __ctx.min_count = min_count;" if k[:mask_policy] == :min_count
3395
+ io.puts " return ca_lazy_arena_protect(#{fn}, ca_lazy_arena_release_held,"
3396
+ io.puts " (VALUE) &__ctx);"
3087
3397
  io.puts " }"
3088
3398
  end
3089
3399
 
3400
+ # Where the source is a lazy expression the kernel is about to compute
3401
+ # whole, a registered expression evaluator may compute it instead; the
3402
+ # dispatcher then goes on with that array. `unless_streaming` is the C
3403
+ # condition under which the reduction streams the expression in chunks
3404
+ # rather than making it whole -- there it is left to stream, since asking
3405
+ # would make it whole and take that much more memory.
3406
+ def self.emit_lazy_evaluated(io, unless_streaming: nil)
3407
+ cond = "ca_is_lazy_view(src)"
3408
+ cond += " && ! ( #{unless_streaming} )" if unless_streaming
3409
+ io.puts " if ( #{cond} ) {"
3410
+ io.puts " volatile VALUE evaluated = ca_lazy_evaluated(self);"
3411
+ io.puts " if ( ! NIL_P(evaluated) ) {"
3412
+ io.puts " self = evaluated;"
3413
+ io.puts " GetCArray(self, src);"
3414
+ io.puts " }"
3415
+ io.puts " }"
3416
+ end
3417
+
3418
+ # Whether a reduction's native walk has the streaming branch
3419
+ # (emit_reduce_streaming_call): the conditions emit_reduce_native checks
3420
+ # before it emits one.
3421
+ def self.reduce_streams?(k)
3422
+ k[:outputs] != 2 && k[:algorithm] != :two_pass_centred &&
3423
+ !k[:array_arg] && !k[:reduce].to_s.include?("idx")
3424
+ end
3425
+
3090
3426
  def self.emit_reduce_dispatch(io, k)
3091
3427
  name = k[:name]
3092
3428
  min_count = (k[:mask_policy] == :min_count)
@@ -3112,12 +3448,13 @@ module MkKernel
3112
3448
  io.puts " GetCArray(self, src);"
3113
3449
 
3114
3450
  if k[:object_escape]
3115
- # CA_OBJECT escape: forward argc/argv (weights + trailing options hash,
3116
- # untouched) to the Ruby composition helper. Intercept before any arg
3117
- # parsing so the helper owns the full original signature.
3118
- io.puts " if ( src->data_type == CA_OBJECT ) {"
3119
- io.puts " return rb_funcallv(self, rb_intern(\"#{k[:object_escape]}\"), argc, argv);"
3120
- io.puts " }"
3451
+ # CA_OBJECT escape: the Ruby composition helper gets the original
3452
+ # argc/argv (weights + trailing options hash, untouched). The escape
3453
+ # itself comes after the options and axes are checked below, so a
3454
+ # wrong keyword or axis is reported here, in this method's name, as
3455
+ # on the numeric path.
3456
+ io.puts " int escape_argc = argc;"
3457
+ io.puts " VALUE *escape_argv = argv;"
3121
3458
  end
3122
3459
 
3123
3460
  if k[:face_gate]
@@ -3151,7 +3488,18 @@ module MkKernel
3151
3488
  io.puts " int keep_axis = RTEST(rkeep_axis);"
3152
3489
  io.puts " int8_t slab_axes[CA_RANK_MAX];"
3153
3490
  io.puts " int8_t naxes = rb_ca_parse_reduce_axes_kw(raxis, src, slab_axes);"
3491
+ if k[:object_escape]
3492
+ io.puts " if ( src->data_type == CA_OBJECT ) {"
3493
+ if min_count
3494
+ io.puts " ca_min_count_arg(rmin_count, NULL);"
3495
+ end
3496
+ io.puts " return rb_funcallv(self, rb_intern(\"#{k[:object_escape]}\"), escape_argc, escape_argv);"
3497
+ io.puts " }"
3498
+ end
3499
+ emit_lazy_evaluated(io, unless_streaming:
3500
+ reduce_streams?(k) ? "naxes == src->ndim && src->ndim >= 1 && ! keep_axis && ! ca_has_mask(src)" : nil)
3154
3501
 
3502
+ weights_io = nil
3155
3503
  if has_aarg
3156
3504
  # Weight storage type: :promote materializes weights at the f64
3157
3505
  # computation type (so a float weight against an int source is not
@@ -3166,7 +3514,7 @@ module MkKernel
3166
3514
  # at dispatch (= simple, uniform kernel path). rev5 strict acceptance
3167
3515
  # mirrors PROPOSAL_LINEAR_INTERP_PER_FIBER_MATCHED rev5 in spirit.
3168
3516
  io.puts " if ( argc < 1 ) {"
3169
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: missing weights argument (1+ required, got 0)");]
3517
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: missing weights argument (1+ required, got 0)", ca_calling_method_name());]
3170
3518
  io.puts " }"
3171
3519
  io.puts " volatile VALUE rweights = argv[0];"
3172
3520
  io.puts " argc--; argv++;"
@@ -3179,98 +3527,103 @@ module MkKernel
3179
3527
  io.puts " rweights = rb_funcall(rweights, rb_intern(\"[]\"), 1, INT2NUM(0));"
3180
3528
  io.puts " }"
3181
3529
  io.puts " }"
3182
- io.puts " volatile VALUE vcw;"
3183
- io.puts " CArray *cw;"
3184
- io.puts " if ( ! rb_obj_is_carray(rweights) ) {"
3185
- io.puts " /* W-A1: Ruby Numeric scalar -> full-shape constant weight CArray */"
3186
- io.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3187
- io.puts " GetCArray(vcw, cw);"
3188
- io.puts " ca_attach(cw);"
3189
- io.puts " rb_ca_obj2ptr(vcw, rweights, cw->ptr); /* fill first cell */"
3190
- io.puts " /* broadcast first cell to all elements (= constant fill) */"
3191
- io.puts " {"
3192
- io.puts " char *p0 = cw->ptr;"
3193
- io.puts " for ( ca_size_t i = 1; i < cw->elements; i++ ) {"
3194
- io.puts " memcpy(cw->ptr + i * cw->bytes, p0, cw->bytes);"
3195
- io.puts " }"
3196
- io.puts " }"
3197
- io.puts " }"
3198
- io.puts " else {"
3199
- io.puts " /* CArray val: coerce to the weight storage type (f64 for :promote, so a"
3200
- io.puts " float weight against an int source is not truncated) */"
3201
- io.puts " vcw = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3202
- io.puts " CArray *cv;"
3203
- io.puts " GetCArray(vcw, cv);"
3204
- io.puts ""
3205
- io.puts " /* rev5 form detection: A3 (same ndim) -> A2 (1-D axis-broadcast) -> raise */"
3206
- io.puts " if ( cv->ndim == src->ndim ) {"
3207
- io.puts " /* W-A3 commit-or-raise: shape strict match */"
3208
- io.puts " for ( int8_t i = 0; i < src->ndim; i++ ) {"
3209
- io.puts " if ( cv->dim[i] != src->dim[i] ) {"
3210
- 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]);]
3211
- io.puts " }"
3212
- io.puts " }"
3213
- io.puts " /* W-A3 path: use cv directly (= existing) */"
3214
- io.puts " cw = cv;"
3215
- io.puts " ca_attach(cw);"
3216
- io.puts " }"
3217
- io.puts " else if ( naxes == 1 && cv->ndim == 1 && cv->dim[0] == src->dim[slab_axes[0]] ) {"
3218
- io.puts " /* W-A2: 1-D axis-broadcast -> materialize to full shape */"
3219
- io.puts " ca_attach(cv);"
3220
- io.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3221
- io.puts " GetCArray(vcw, cw);"
3222
- io.puts " ca_attach(cw);"
3223
- io.puts " /* broadcast cv (1-D, length src->dim[slab_axes[0]]) along reduce axis */"
3224
- io.puts " {"
3225
- io.puts " int8_t baxis = slab_axes[0];"
3226
- io.puts " ca_size_t inner_stride = 1;"
3227
- io.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3228
- io.puts " ca_size_t axis_n = src->dim[baxis];"
3229
- io.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3230
- io.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3231
- io.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3232
- io.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3233
- io.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3234
- io.puts " memcpy(cw->ptr + flat * cw->bytes, cv->ptr + a * cv->bytes, cw->bytes);"
3235
- io.puts " }"
3236
- io.puts " }"
3237
- io.puts " }"
3238
- io.puts " }"
3239
- io.puts " /* Propagate cv mask via W-A3 overlay path below (= mask materialize) */"
3240
- io.puts " if ( ca_has_mask(cv) ) {"
3241
- io.puts " /* Allocate cw mask + broadcast cv mask along axis */"
3242
- io.puts " ca_create_mask(cw);"
3243
- io.puts " boolean8_t *cw_m = (boolean8_t *)cw->mask->ptr;"
3244
- io.puts " boolean8_t *cv_m = (boolean8_t *)cv->mask->ptr;"
3245
- io.puts " int8_t baxis = slab_axes[0];"
3246
- io.puts " ca_size_t inner_stride = 1;"
3247
- io.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3248
- io.puts " ca_size_t axis_n = src->dim[baxis];"
3249
- io.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3250
- io.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3251
- io.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3252
- io.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3253
- io.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3254
- io.puts " cw_m[flat] = cv_m[a];"
3255
- io.puts " }"
3256
- io.puts " }"
3257
- io.puts " }"
3258
- io.puts " }"
3259
- io.puts " ca_detach(cv);"
3260
- io.puts " }"
3261
- io.puts " else {"
3262
- 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));]
3263
- io.puts " }"
3264
- io.puts " }"
3530
+ # The weights are attached from here to the end of the call, so they
3531
+ # are made only once every check that can raise has passed; see the
3532
+ # data type check below.
3533
+ wio = StringIO.new
3534
+ wio.puts " volatile VALUE vcw;"
3535
+ wio.puts " CArray *cw;"
3536
+ wio.puts " if ( ! rb_obj_is_carray(rweights) ) {"
3537
+ wio.puts " /* W-A1: Ruby Numeric scalar -> full-shape constant weight CArray */"
3538
+ wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3539
+ wio.puts " GetCArray(vcw, cw); /* a new entity: its ptr needs no attach */"
3540
+ wio.puts " rb_ca_obj2ptr(vcw, rweights, cw->ptr); /* fill first cell */"
3541
+ wio.puts " /* broadcast first cell to all elements (= constant fill) */"
3542
+ wio.puts " {"
3543
+ wio.puts " char *p0 = cw->ptr;"
3544
+ wio.puts " for ( ca_size_t i = 1; i < cw->elements; i++ ) {"
3545
+ wio.puts " memcpy(cw->ptr + i * cw->bytes, p0, cw->bytes);"
3546
+ wio.puts " }"
3547
+ wio.puts " }"
3548
+ wio.puts " }"
3549
+ wio.puts " else {"
3550
+ wio.puts " /* CArray val: coerce to the weight storage type (f64 for :promote, so a"
3551
+ wio.puts " float weight against an int source is not truncated) */"
3552
+ wio.puts " vcw = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3553
+ wio.puts " CArray *cv;"
3554
+ wio.puts " GetCArray(vcw, cv);"
3555
+ wio.puts ""
3556
+ wio.puts " /* rev5 form detection: A3 (same ndim) -> A2 (1-D axis-broadcast) -> raise */"
3557
+ wio.puts " if ( cv->ndim == src->ndim ) {"
3558
+ wio.puts " /* W-A3 commit-or-raise: shape strict match */"
3559
+ wio.puts " for ( int8_t i = 0; i < src->ndim; i++ ) {"
3560
+ wio.puts " if ( cv->dim[i] != src->dim[i] ) {"
3561
+ 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]);]
3562
+ wio.puts " }"
3563
+ wio.puts " }"
3564
+ wio.puts " /* W-A3 path: use cv directly (= existing) */"
3565
+ wio.puts " cw = cv;"
3566
+ wio.puts " }"
3567
+ wio.puts " else if ( naxes == 1 && cv->ndim == 1 && cv->dim[0] == src->dim[slab_axes[0]] ) {"
3568
+ wio.puts " /* W-A2: 1-D axis-broadcast -> materialize to full shape */"
3569
+ wio.puts " ca_attach(cv); /* window: nothing raises inside */"
3570
+ wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3571
+ wio.puts " GetCArray(vcw, cw);"
3572
+ wio.puts " /* broadcast cv (1-D, length src->dim[slab_axes[0]]) along reduce axis */"
3573
+ wio.puts " {"
3574
+ wio.puts " int8_t baxis = slab_axes[0];"
3575
+ wio.puts " ca_size_t inner_stride = 1;"
3576
+ wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3577
+ wio.puts " ca_size_t axis_n = src->dim[baxis];"
3578
+ wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3579
+ wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3580
+ wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3581
+ wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3582
+ wio.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3583
+ wio.puts " memcpy(cw->ptr + flat * cw->bytes, cv->ptr + a * cv->bytes, cw->bytes);"
3584
+ wio.puts " }"
3585
+ wio.puts " }"
3586
+ wio.puts " }"
3587
+ wio.puts " }"
3588
+ wio.puts " /* Propagate cv mask via W-A3 overlay path below (= mask materialize) */"
3589
+ wio.puts " if ( ca_has_mask(cv) ) {"
3590
+ wio.puts " /* Allocate cw mask + broadcast cv mask along axis */"
3591
+ wio.puts " ca_create_mask(cw);"
3592
+ wio.puts " boolean8_t *cw_m = (boolean8_t *)cw->mask->ptr;"
3593
+ wio.puts " boolean8_t *cv_m = (boolean8_t *)cv->mask->ptr;"
3594
+ wio.puts " int8_t baxis = slab_axes[0];"
3595
+ wio.puts " ca_size_t inner_stride = 1;"
3596
+ wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3597
+ wio.puts " ca_size_t axis_n = src->dim[baxis];"
3598
+ wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3599
+ wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3600
+ wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3601
+ wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3602
+ wio.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3603
+ wio.puts " cw_m[flat] = cv_m[a];"
3604
+ wio.puts " }"
3605
+ wio.puts " }"
3606
+ wio.puts " }"
3607
+ wio.puts " }"
3608
+ wio.puts " ca_detach(cv);"
3609
+ wio.puts " }"
3610
+ wio.puts " else {"
3611
+ 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));]
3612
+ wio.puts " }"
3613
+ wio.puts " }"
3265
3614
  # Mask overlay (= legacy W-A3 path; if the materialized cw carries a mask, overlay it)
3266
- io.puts " volatile VALUE __vsrc_keep = self; /* GC guard */ (void) __vsrc_keep;"
3267
- io.puts " if ( ca_has_mask(cw) ) {"
3268
- io.puts " CArray *src_copy = ca_copy(src);"
3269
- io.puts " VALUE vsrc_copy = ca_wrap_struct(src_copy);"
3270
- io.puts " ca_copy_mask_overlay(src_copy, src_copy->elements, 1, cw);"
3271
- io.puts " src = src_copy;"
3272
- io.puts " self = vsrc_copy;"
3273
- io.puts " }"
3615
+ wio.puts " volatile VALUE __vsrc_keep = self; /* GC guard */ (void) __vsrc_keep;"
3616
+ wio.puts " if ( ca_has_mask(cw) ) {"
3617
+ wio.puts " CArray *src_copy = ca_copy(src);"
3618
+ wio.puts " VALUE vsrc_copy = ca_wrap_struct(src_copy);"
3619
+ wio.puts " ca_copy_mask_overlay(src_copy, src_copy->elements, 1, cw);"
3620
+ wio.puts " src = src_copy;"
3621
+ wio.puts " self = vsrc_copy;"
3622
+ wio.puts " }"
3623
+ # Attached last, after the overlay's copy of the source (which can
3624
+ # raise), and detached right after the switch.
3625
+ wio.puts " ca_attach(cw); /* window: nothing raises inside; the kernels are numeric */"
3626
+ weights_io = wio
3274
3627
  end
3275
3628
 
3276
3629
  if has_varg
@@ -3278,7 +3631,7 @@ module MkKernel
3278
3631
  # runtime value_arg (e.g. `a.count_equal(5, 0)` -- 5 is value_arg,
3279
3632
  # 0 is axis). Per-src cast happens inside the switch case below.
3280
3633
  io.puts " if ( argc < 1 ) {"
3281
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: missing value argument (1+ required, got 0)");]
3634
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: missing value argument (1+ required, got 0)", ca_calling_method_name());]
3282
3635
  io.puts " }"
3283
3636
  io.puts " volatile VALUE rval = argv[0];"
3284
3637
  io.puts " argc--; argv++;"
@@ -3286,16 +3639,16 @@ module MkKernel
3286
3639
 
3287
3640
  if min_count
3288
3641
  # rmin_count / rfval were scanned up front (PoC scan-merge); here we
3289
- # only interpret them. Sentinel min_count = -1 means "no opt given"
3290
- # -> :all_masked semantics.
3291
- io.puts " ca_size_t min_count = -1;"
3292
- io.puts " if ( ! NIL_P(rmin_count) ) {"
3293
- io.puts " ca_size_t mc_user = NUM2SIZE(rmin_count);"
3294
- io.puts " if ( mc_user < 0 ) {"
3295
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: :min_count must be non-negative, got %lld", (long long) mc_user);]
3296
- io.puts " }"
3297
- io.puts " /* :min_count = 0 collapses to legacy default (any 1 valid). */"
3298
- io.puts " if ( mc_user > 0 ) min_count = mc_user;"
3642
+ # only interpret them; min_count is -1 when not given.
3643
+ io.puts " ca_size_t min_count = ca_min_count_arg(rmin_count, NULL);"
3644
+ # fill_value: UNDEF leaves the result undefined, as no fill_value does.
3645
+ # A full reduction with a fill keeps its axes so the fill goes into a
3646
+ # typed cell, as it does per axis; ca_reduce_fill takes the cell out.
3647
+ io.puts " if ( rfval == CA_UNDEF ) rfval = Qnil;"
3648
+ io.puts " int fill_whole = 0;"
3649
+ io.puts " if ( ! NIL_P(rfval) && naxes == src->ndim && ! keep_axis ) {"
3650
+ io.puts " keep_axis = 1;"
3651
+ io.puts " fill_whole = 1;"
3299
3652
  io.puts " }"
3300
3653
  io.puts " VALUE result;"
3301
3654
  elsif has_aarg
@@ -3311,26 +3664,57 @@ module MkKernel
3311
3664
  # axis is allowed. Variadic `a.sum(0, 1)` raises here with a migration
3312
3665
  # hint pointing at the kwarg form.
3313
3666
  io.puts " if ( argc > 0 ) {"
3314
- 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);]
3667
+ 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());]
3315
3668
  io.puts " }"
3316
3669
 
3670
+ if weights_io
3671
+ # Refuse an unsupported source before the weights are attached: the
3672
+ # switch below would raise with them held.
3673
+ if k[:fallback] == :raise
3674
+ io.puts " switch ( src->data_type ) {"
3675
+ io.puts " " + k[:source].map { |s| "case #{DTYPES[s][:ca]}:" }.join(" ") + " break;"
3676
+ io.puts " default:"
3677
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3678
+ 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]);]
3679
+ io.puts " }"
3680
+ end
3681
+ io.print weights_io.string
3682
+ end
3683
+
3317
3684
  io.puts " switch ( src->data_type ) {"
3318
3685
  k[:source].each do |s|
3319
3686
  si = DTYPES[s]
3320
3687
  # Per-src value_arg cast: NUM2LL / NUM2ULL / NUM2DBL -> (T_IN).
3321
- varg_decl = has_varg ? " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n" : ""
3688
+ varg_decl = if !has_varg
3689
+ ""
3690
+ elsif s == :fixlen
3691
+ # A fixlen query is a runtime-width byte blob with no
3692
+ # scalar cast; pack it the way the search family does,
3693
+ # which NUL-pads a short String to the cell width.
3694
+ # ALLOCV, not the stack: a cell can be wide. The holder
3695
+ # keeps the buffer until the GC takes it.
3696
+ " volatile VALUE value_arg_holder = 0;\n" \
3697
+ " char *value_arg = ALLOCV_N(char, value_arg_holder, src->bytes);\n" \
3698
+ " rb_ca_obj2ptr(self, rval, value_arg);\n"
3699
+ elsif INT_DTYPES.include?(s)
3700
+ " ca_check_integer_fits(rval, #{si[:ca]});\n" \
3701
+ " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n"
3702
+ else
3703
+ " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n"
3704
+ end
3322
3705
  if use_result_var || has_varg
3323
3706
  io.puts " case #{si[:ca]}: {"
3324
3707
  io.print varg_decl unless varg_decl.empty?
3325
3708
  if use_result_var
3326
3709
  io.puts " result = #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis#{extra_call});"
3710
+ io.puts " RB_GC_GUARD(self);"
3327
3711
  io.puts " break;"
3328
3712
  else
3329
- io.puts " return #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis#{extra_call});"
3713
+ io.puts " CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis#{extra_call}));"
3330
3714
  end
3331
3715
  io.puts " }"
3332
3716
  else
3333
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis);"
3717
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis));"
3334
3718
  end
3335
3719
  end
3336
3720
  case k[:fallback]
@@ -3343,7 +3727,8 @@ module MkKernel
3343
3727
  io.puts " switch ( src->data_type ) {"
3344
3728
  io.puts " case CA_BOOLEAN: break; /* wrap: count-of-trues semantic */"
3345
3729
  io.puts " default:"
3346
- 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]);]
3730
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3731
+ 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]);]
3347
3732
  io.puts " }"
3348
3733
  io.puts " VALUE vsrc = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));"
3349
3734
  io.puts " CArray *casted;"
@@ -3355,42 +3740,44 @@ module MkKernel
3355
3740
  end
3356
3741
  if use_result_var
3357
3742
  io.puts " result = #{name}_ki_native_f64(vsrc, casted, slab_axes, naxes, keep_axis#{extra_call});"
3743
+ io.puts " RB_GC_GUARD(vsrc);"
3358
3744
  io.puts " break;"
3359
3745
  else
3360
- io.puts " return #{name}_ki_native_f64(vsrc, casted, slab_axes, naxes, keep_axis#{extra_call});"
3746
+ io.puts " CA_RETURN_KEEPING(vsrc, #{name}_ki_native_f64(vsrc, casted, slab_axes, naxes, keep_axis#{extra_call}));"
3361
3747
  end
3362
3748
  io.puts " }"
3363
3749
  when :raise
3364
3750
  io.puts " default:"
3365
- 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]);]
3751
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3752
+ 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]);]
3366
3753
  end
3367
3754
  io.puts " }"
3368
3755
 
3369
- if min_count
3370
- # Apply fill_value substitution: CA_UNDEF (full reduction all-masked)
3371
- # or per-axis CArray with mask bits.
3372
- io.puts " if ( ! NIL_P(rfval) ) {"
3373
- io.puts " if ( result == CA_UNDEF ) {"
3374
- io.puts " result = rfval;"
3375
- io.puts " } else if ( TYPE(result) != T_FLOAT && rb_obj_is_kind_of(result, rb_cCArray) ) {"
3376
- io.puts " CArray *cr;"
3377
- io.puts " GetCArray(result, cr);"
3378
- io.puts " if ( ca_has_mask(cr) ) {"
3379
- io.puts " result = rb_ca_mask_fill_copy(result, rfval);"
3380
- io.puts " }"
3381
- io.puts " }"
3382
- io.puts " }"
3383
- end
3384
-
3385
- # array_arg cleanup: detach weights (paired with ca_attach above).
3756
+ # array_arg cleanup: detach weights (paired with ca_attach above),
3757
+ # ahead of the fill_value substitution, which can raise.
3386
3758
  if has_aarg
3387
3759
  io.puts " ca_detach(cw);"
3388
3760
  end
3389
3761
 
3390
- if k[:face_gate] == :relift
3762
+ relift = (k[:face_gate] == :relift)
3763
+ if min_count
3764
+ # Apply fill_value through the one rule the per-axis result uses. A
3765
+ # Face's fill is a value of the Face, so it goes in after the re-lift
3766
+ # below, as an assignment to the Face would put it.
3767
+ io.puts relift ? " if ( ! NIL_P(rfval) && _reduce_face == Qnil ) {" : " if ( ! NIL_P(rfval) ) {"
3768
+ io.puts " result = ca_reduce_fill(result, rfval, fill_whole);"
3769
+ io.puts " }"
3770
+ end
3771
+
3772
+ if relift
3391
3773
  # Descended through emit_reduce_face_gate: re-lift the storage result
3392
3774
  # back into the Face before returning.
3393
3775
  emit_reduce_face_relift(io)
3776
+ if min_count
3777
+ io.puts " if ( ! NIL_P(rfval) && _reduce_face != Qnil ) {"
3778
+ io.puts " result = ca_reduce_fill(result, rfval, fill_whole);"
3779
+ io.puts " }"
3780
+ end
3394
3781
  end
3395
3782
 
3396
3783
  if use_result_var
@@ -3401,117 +3788,13 @@ module MkKernel
3401
3788
  io.puts "}"
3402
3789
  end
3403
3790
 
3404
- # ---- map emitter ------------------------------------------------------
3405
-
3406
- def self.emit_map(io, k)
3407
- io.puts
3408
- io.puts "/* ===== #{k[:name]}_ki ============================================ */"
3409
- k[:source].each do |src|
3410
- emit_map_native(io, k, src)
3411
- end
3412
- emit_map_dispatch(io, k)
3413
- end
3414
-
3415
- def self.emit_map_native(io, k, src)
3416
- si = DTYPES[src]
3417
- oi = output_info(k, src)
3418
- name = k[:name]
3419
- expr = resolve_expr(k[:expr], oi, src)
3420
-
3421
- io.puts
3422
- io.puts <<~C
3423
- static VALUE
3424
- #{name}_ki_native_#{src} (VALUE self, CArray *ca)
3425
- {
3426
- VALUE vout = rb_ca_template_with_type(self, INT2NUM(#{oi[:ca]}),
3427
- INT2NUM(sizeof(#{oi[:c]})));
3428
- CArray *co;
3429
- GetCArray(vout, co);
3430
-
3431
- /* All-axes slab = whole array as one K-D walk via CA_SLAB_AXES.
3432
- CA_SLAB_WHOLE uses the flat next_slab path, which doesn't
3433
- populate slab_dims; CA_SLAB_AXES with naxes == ndim is the
3434
- K-D-walkable form. */
3435
- int8_t slab_axes[CA_RANK_MAX];
3436
- int8_t naxes = ca->ndim;
3437
- for ( int8_t k = 0; k < naxes; k++ ) slab_axes[k] = k;
3438
-
3439
- ca_iter_state st_in, st_out;
3440
- int rc;
3441
- rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,
3442
- slab_axes, naxes, 0);
3443
- if ( rc != CA_ITER_OK ) {
3444
- rb_raise(rb_eRuntimeError,
3445
- "#{name}_ki: input init failed rc=%d", rc);
3446
- }
3447
- rc = ca_iter_state_init_l2(&st_out, co, CA_SLAB_AXES,
3448
- slab_axes, naxes, CA_KERNEL_WRITE);
3449
- if ( rc != CA_ITER_OK ) {
3450
- ca_iter_state_finish(&st_in);
3451
- rb_raise(rb_eRuntimeError,
3452
- "#{name}_ki: output init failed rc=%d", rc);
3453
- }
3454
-
3455
- char *pi, *po;
3456
- boolean8_t *mi, *mo;
3457
- while ( ca_iter_state_next_slab_axes(&st_in, &pi, &mi) &&
3458
- ca_iter_state_next_slab_axes(&st_out, &po, &mo) ) {
3459
- CA_SLAB_MAP_T(#{si[:c]}, #{oi[:c]}, st_in, pi, st_out, po, #{expr});
3460
- ca_iter_state_sync_slab(&st_out);
3461
- }
3462
- ca_iter_state_finish(&st_in);
3463
- ca_iter_state_finish(&st_out);
3464
- return vout;
3465
- }
3466
- C
3467
- end
3468
-
3469
- def self.emit_map_dispatch(io, k)
3470
- name = k[:name]
3471
- io.puts
3472
- io.puts "static VALUE"
3473
- io.puts "rb_ca_#{name}_ki (VALUE self)"
3474
- io.puts "{"
3475
- io.puts " CArray *src;"
3476
- io.puts " GetCArray(self, src);"
3477
- io.puts " switch ( src->data_type ) {"
3478
- k[:source].each do |s|
3479
- si = DTYPES[s]
3480
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src);"
3481
- end
3482
- case k[:fallback]
3483
- when :wrap_to_f64
3484
- io.puts " default: {"
3485
- io.puts " /* Phase E: only wrap data_types that have a meaningful float64"
3486
- io.puts " representation. Complex / fixlen / object would lose"
3487
- io.puts " structure (e.g. complex -> float strips imaginary part)"
3488
- io.puts " so reject explicitly, matching legacy DataTypeError. */"
3489
- io.puts " switch ( src->data_type ) {"
3490
- io.puts " case CA_BOOLEAN: break; /* wrap: count-of-trues semantic */"
3491
- io.puts " default:"
3492
- 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]);]
3493
- io.puts " }"
3494
- io.puts " VALUE vsrc = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));"
3495
- io.puts " CArray *casted;"
3496
- io.puts " GetCArray(vsrc, casted);"
3497
- io.puts " return #{name}_ki_native_f64(vsrc, casted);"
3498
- io.puts " }"
3499
- when :raise
3500
- io.puts " default:"
3501
- 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]);]
3502
- end
3503
- io.puts " }"
3504
- io.puts " return Qnil; /* unreachable */"
3505
- io.puts "}"
3506
- end
3507
-
3508
3791
  # ---- scan emitter -----------------------------------------------------
3509
3792
 
3510
3793
  def self.emit_scan(io, k)
3511
3794
  io.puts
3512
3795
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
3513
3796
  k[:source].each do |src|
3514
- emit_scan_native(io, k, src)
3797
+ emit_native_for(io, k, src) { |o| emit_scan_native(o, k, src) }
3515
3798
  end
3516
3799
  emit_scan_dispatch(io, k)
3517
3800
  end
@@ -3582,14 +3865,14 @@ module MkKernel
3582
3865
  slab_axes, 1, 0);
3583
3866
  if ( rc != CA_ITER_OK ) {
3584
3867
  rb_raise(rb_eRuntimeError,
3585
- "#{name}_ki: input init failed rc=%d", rc);
3868
+ "%s: input init failed rc=%d", ca_calling_method_name(), rc);
3586
3869
  }
3587
- rc = ca_iter_state_init_l2(&st_out, co, CA_SLAB_AXES,
3870
+ rc = ca_iter_state_init_l2_paired(&st_out, &st_in, co, CA_SLAB_AXES,
3588
3871
  slab_axes, 1, CA_KERNEL_WRITE);
3589
3872
  if ( rc != CA_ITER_OK ) {
3590
3873
  ca_iter_state_finish(&st_in);
3591
3874
  rb_raise(rb_eRuntimeError,
3592
- "#{name}_ki: output init failed rc=%d", rc);
3875
+ "%s: output init failed rc=%d", ca_calling_method_name(), rc);
3593
3876
  }
3594
3877
  #{mask_setup}
3595
3878
  char *pi, *po;
@@ -3614,16 +3897,38 @@ module MkKernel
3614
3897
  # multi-axis scan is semantically ambiguous (flatten-and-cumsum vs
3615
3898
  # 2D prefix sum); users must chain explicitly. Arity flips from 1
3616
3899
  # (positional Integer axis) to -1 (kwarg dispatcher).
3617
- io.puts "static VALUE"
3618
- io.puts "rb_ca_#{name}_ki (int argc, VALUE *argv, VALUE self)"
3900
+ if k[:face_gate]
3901
+ # The scan proper runs on the storage; the bound entry below gates the
3902
+ # Face and re-lifts the result.
3903
+ io.puts "static VALUE rb_ca_#{name}_ki_storage (int argc, VALUE *argv, VALUE self);"
3904
+ io.puts
3905
+ io.puts "static VALUE"
3906
+ io.puts "rb_ca_#{name}_ki (int argc, VALUE *argv, VALUE self)"
3907
+ io.puts "{"
3908
+ io.puts " CArray *src;"
3909
+ io.puts " VALUE result;"
3910
+ io.puts " GetCArray(self, src);"
3911
+ emit_reduce_face_gate(io, name, true)
3912
+ io.puts " result = rb_ca_#{name}_ki_storage(argc, argv, self);"
3913
+ emit_reduce_face_relift(io)
3914
+ io.puts " return result;"
3915
+ io.puts "}"
3916
+ io.puts
3917
+ io.puts "static VALUE"
3918
+ io.puts "rb_ca_#{name}_ki_storage (int argc, VALUE *argv, VALUE self)"
3919
+ else
3920
+ io.puts "static VALUE"
3921
+ io.puts "rb_ca_#{name}_ki (int argc, VALUE *argv, VALUE self)"
3922
+ end
3619
3923
  io.puts "{"
3620
3924
  io.puts " CArray *src;"
3621
3925
  io.puts " GetCArray(self, src);"
3926
+ emit_lazy_evaluated(io)
3622
3927
  io.puts " volatile VALUE ropt = rb_pop_options(&argc, &argv);"
3623
3928
  io.puts " volatile VALUE raxis = Qnil;"
3624
3929
  io.puts " rb_scan_options(ropt, \"axis\", &raxis);"
3625
3930
  io.puts " if ( argc > 0 ) {"
3626
- 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);]
3931
+ 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);]
3627
3932
  io.puts " }"
3628
3933
  if k[:axis_default] == :flatten
3629
3934
  # Legacy compat: nil/omitted axis: flatten source and scan axis 0.
@@ -3635,7 +3940,7 @@ module MkKernel
3635
3940
  io.puts " switch ( fsrc->data_type ) {"
3636
3941
  k[:source].each do |s|
3637
3942
  si = DTYPES[s]
3638
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(vflat, fsrc, 0);"
3943
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(vflat, #{name}_ki_native_#{s}(vflat, fsrc, 0));"
3639
3944
  end
3640
3945
  case k[:fallback]
3641
3946
  when :wrap_to_f64
@@ -3643,34 +3948,35 @@ module MkKernel
3643
3948
  io.puts " VALUE vsrc = rb_ca_wrap_readonly(vflat, INT2NUM(CA_FLOAT64));"
3644
3949
  io.puts " CArray *casted;"
3645
3950
  io.puts " GetCArray(vsrc, casted);"
3646
- io.puts " return #{name}_ki_native_f64(vsrc, casted, 0);"
3951
+ io.puts " CA_RETURN_KEEPING(vsrc, #{name}_ki_native_f64(vsrc, casted, 0));"
3647
3952
  io.puts " }"
3648
3953
  when :raise
3649
3954
  io.puts " default:"
3650
- 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]);]
3955
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3956
+ 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]);]
3957
+ when :mask_count
3958
+ io.puts " default:"
3959
+ io.puts " return rb_ca_scan_mask_count(self, Qnil);"
3651
3960
  end
3652
3961
  io.puts " }"
3653
3962
  io.puts " }"
3654
3963
  else
3655
3964
  io.puts " if ( NIL_P(raxis) ) {"
3656
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis: kwarg is required (single axis Integer; multi-axis scan is semantically ambiguous)");]
3965
+ 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());]
3657
3966
  io.puts " }"
3658
3967
  end
3659
3968
  io.puts " if ( TYPE(raxis) == T_ARRAY ) {"
3660
- 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)");]
3969
+ 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());]
3661
3970
  io.puts " }"
3662
- io.puts " if ( ! rb_obj_is_kind_of(raxis, rb_cInteger) ) {"
3663
- io.puts %Q[ rb_raise(rb_eTypeError, "#{name}_ki: axis: must be Integer (got %"PRIsVALUE")", rb_obj_class(raxis));]
3664
- io.puts " }"
3665
- io.puts " int axis = NUM2INT(raxis);"
3971
+ io.puts " int axis = (int) ca_axis_integer(raxis, NULL);"
3666
3972
  io.puts " if ( axis < 0 ) axis += src->ndim;"
3667
3973
  io.puts " if ( axis < 0 || axis >= src->ndim ) {"
3668
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(raxis), src->ndim);]
3974
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: axis %d out of range for ndim %d", ca_calling_method_name(), NUM2INT(raxis), src->ndim);]
3669
3975
  io.puts " }"
3670
3976
  io.puts " switch ( src->data_type ) {"
3671
3977
  k[:source].each do |s|
3672
3978
  si = DTYPES[s]
3673
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, axis);"
3979
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, axis));"
3674
3980
  end
3675
3981
  case k[:fallback]
3676
3982
  when :wrap_to_f64
@@ -3678,11 +3984,15 @@ module MkKernel
3678
3984
  io.puts " VALUE vsrc = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));"
3679
3985
  io.puts " CArray *casted;"
3680
3986
  io.puts " GetCArray(vsrc, casted);"
3681
- io.puts " return #{name}_ki_native_f64(vsrc, casted, axis);"
3987
+ io.puts " CA_RETURN_KEEPING(vsrc, #{name}_ki_native_f64(vsrc, casted, axis));"
3682
3988
  io.puts " }"
3683
3989
  when :raise
3684
3990
  io.puts " default:"
3685
- 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]);]
3991
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
3992
+ 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]);]
3993
+ when :mask_count
3994
+ io.puts " default:"
3995
+ io.puts " return rb_ca_scan_mask_count(self, INT2NUM(axis));"
3686
3996
  end
3687
3997
  io.puts " }"
3688
3998
  io.puts " return Qnil; /* unreachable */"
@@ -3697,7 +4007,7 @@ module MkKernel
3697
4007
  k[:source].each do |src|
3698
4008
  emit_sort_cmp(io, k, src)
3699
4009
  emit_sort_quickselect(io, k, src) if k[:algorithm] == :partition
3700
- emit_sort_native(io, k, src)
4010
+ emit_native_for(io, k, src) { |o| emit_sort_native(o, k, src) }
3701
4011
  end
3702
4012
  emit_sort_dispatch(io, k)
3703
4013
  end
@@ -3829,14 +4139,21 @@ module MkKernel
3829
4139
  # String, custom class with <=> defined) works. Raises Ruby
3830
4140
  # TypeError mid-sort if a cell pair is uncomparable -- consistent
3831
4141
  # with Array#sort behavior.
3832
- io.puts %Q{ VALUE r = rb_funcall(pa->v, rb_intern("<=>"), 1, pb->v);}
3833
- io.puts " if ( NIL_P(r) ) {"
3834
- io.puts %Q{ rb_raise(rb_eArgError, "#{name}_ki: comparison of %s with %s failed",}
3835
- io.puts " rb_obj_classname(pa->v), rb_obj_classname(pb->v));"
4142
+ # A Float NaN sorts after every other value, as it does in a float
4143
+ # array; two NaNs fall to the index tie-break.
4144
+ io.puts " int nan_a = CA_OBJ_ISNAN(pa->v);"
4145
+ io.puts " int nan_b = CA_OBJ_ISNAN(pb->v);"
4146
+ io.puts " if ( nan_a && !nan_b ) return 1;"
4147
+ io.puts " if ( nan_b && !nan_a ) return -1;"
4148
+ io.puts " if ( !nan_a ) {"
4149
+ io.puts %Q{ VALUE r = rb_funcall(pa->v, rb_intern("<=>"), 1, pb->v);}
4150
+ io.puts " if ( NIL_P(r) ) {"
4151
+ io.puts " ca_obj_compare_refused(pa->v, pb->v);"
4152
+ io.puts " }"
4153
+ io.puts " int c = NUM2INT(r);"
4154
+ io.puts " if ( c > 0 ) return 1;"
4155
+ io.puts " if ( c < 0 ) return -1;"
3836
4156
  io.puts " }"
3837
- io.puts " int c = NUM2INT(r);"
3838
- io.puts " if ( c > 0 ) return 1;"
3839
- io.puts " if ( c < 0 ) return -1;"
3840
4157
  else
3841
4158
  if is_fp
3842
4159
  # NaN policy :end -- NaN sorts to the end (matches qcmp_f_type in
@@ -3967,19 +4284,32 @@ module MkKernel
3967
4284
  io.puts " rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,"
3968
4285
  io.puts " slab_axes, 1, 0);"
3969
4286
  io.puts " if ( rc != CA_ITER_OK ) {"
3970
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: input init failed rc=%d", rc);]
4287
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: input init failed rc=%d", ca_calling_method_name(), rc);]
3971
4288
  io.puts " }"
3972
- io.puts " rc = ca_iter_state_init_l2(&st_out, co, CA_SLAB_AXES,"
4289
+ io.puts " rc = ca_iter_state_init_l2_paired(&st_out, &st_in, co, CA_SLAB_AXES,"
3973
4290
  io.puts " slab_axes, 1, CA_KERNEL_WRITE);"
3974
4291
  io.puts " if ( rc != CA_ITER_OK ) {"
3975
4292
  io.puts " ca_iter_state_finish(&st_in);"
3976
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: output init failed rc=%d", rc);]
4293
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: output init failed rc=%d", ca_calling_method_name(), rc);]
3977
4294
  io.puts " }"
3978
4295
  io.puts
3979
4296
  io.puts " ca_size_t fiber_n = st_in.slab_elements;"
3980
- io.puts " #{name}_pair_#{src} *buf ="
3981
- io.puts " xmalloc(sizeof(#{name}_pair_#{src}) * (size_t) fiber_n);"
3982
- if has_kind
4297
+ if src == :object
4298
+ # The object comparator calls <=>, so the sort can raise part way.
4299
+ # ALLOCV frees these buffers whether the function returns or unwinds.
4300
+ # A full sort also needs aux: ca_sort_merge_pair_object is a mergesort.
4301
+ io.puts " volatile VALUE hbuf = 0, haux = 0;"
4302
+ io.puts " #{name}_pair_#{src} *buf = ALLOCV_N(#{name}_pair_#{src}, hbuf, fiber_n);"
4303
+ if partition
4304
+ io.puts " (void) haux;"
4305
+ else
4306
+ io.puts " #{name}_pair_#{src} *aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4307
+ end
4308
+ else
4309
+ io.puts " #{name}_pair_#{src} *buf ="
4310
+ io.puts " xmalloc(sizeof(#{name}_pair_#{src}) * (size_t) fiber_n);"
4311
+ end
4312
+ if has_kind && src != :object
3983
4313
  # PROPOSAL_PORTABLE_TEXTBOOK_SORT §9.5.2 Option γ: aux pair buffer
3984
4314
  # for ca_sort_merge_pair_<src> (= ping-pong bottom-up mergesort).
3985
4315
  # Allocated once outside the fiber loop, reused across fibers.
@@ -4124,16 +4454,15 @@ module MkKernel
4124
4454
  io.puts " */"
4125
4455
  end
4126
4456
  if is_object
4127
- # PROPOSAL_MKKERNEL_OBJECT_DTYPE_BRANCH Phase 2: CA_OBJECT routes to
4128
- # libc qsort with the emitted _cmp_object (rb_funcall(<=>) based).
4129
- # qsort isn't stable, but our cmp tie-breaks on the original index,
4130
- # so the result is effectively stable. do_stable is ignored: both
4131
- # paths use the same qsort + cmp here. NaN partition is skipped
4132
- # (Ruby Float NaN handling deferred -- cells of type Float with NaN
4133
- # would compare via Float#<=> which returns nil for NaN, raising
4134
- # ArgumentError in the cmp helper).
4135
- io.puts " (void) do_stable; /* CA_OBJECT: qsort handles both stable / quick */"
4136
- io.puts " qsort(#{buf_expr}, (size_t) #{sort_n_expr}, sizeof(#{name}_pair_#{src}), #{name}_cmp_#{src});"
4457
+ # CA_OBJECT sorts with the emitted _cmp_object (rb_funcall(<=>)
4458
+ # based) through ca_sort_merge_pair_object rather than libc qsort: the
4459
+ # comparator raises on a pair that does not compare, and the raise
4460
+ # must not unwind through libc's frames. The cmp tie-breaks on the
4461
+ # original index, so the order is the same for both kinds, and
4462
+ # do_stable is ignored. NaN partition is skipped (Float#<=> answers
4463
+ # nil for NaN, which the comparator reports as ArgumentError).
4464
+ io.puts " (void) do_stable; /* CA_OBJECT: one stable sort serves both kinds */"
4465
+ 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});"
4137
4466
  elsif is_fixlen
4138
4467
  io.puts " /* CA_FIXLEN: libc qsort with the memcmp comparator. qsort isn't"
4139
4468
  io.puts " stable, but #{name}_cmp_#{src} tie-breaks on the original index,"
@@ -4180,7 +4509,7 @@ module MkKernel
4180
4509
  io.puts " if ( r > 0 ) {"
4181
4510
  io.puts " if ( dense ) {"
4182
4511
  if is_object
4183
- io.puts " if ( ! rb_equal(buf[r].v, buf[r - 1].v) ) out_r++;"
4512
+ io.puts " if ( ! ca_obj_equal(buf[r].v, buf[r - 1].v) ) out_r++;"
4184
4513
  elsif is_fixlen
4185
4514
  io.puts " if ( memcmp(buf[r].vp, buf[r - 1].vp, (size_t) buf[r].nb) != 0 ) out_r++;"
4186
4515
  else
@@ -4226,9 +4555,14 @@ module MkKernel
4226
4555
  end
4227
4556
 
4228
4557
  io.puts " }"
4229
- io.puts " xfree(buf);"
4230
- if has_kind
4231
- io.puts " if ( aux ) xfree(aux);"
4558
+ if src == :object
4559
+ io.puts " ALLOCV_END(hbuf);"
4560
+ io.puts " ALLOCV_END(haux);"
4561
+ else
4562
+ io.puts " xfree(buf);"
4563
+ if has_kind
4564
+ io.puts " if ( aux ) xfree(aux);"
4565
+ end
4232
4566
  end
4233
4567
  io.puts " ca_iter_state_finish(&st_in);"
4234
4568
  io.puts " ca_iter_state_finish(&st_out);"
@@ -4250,10 +4584,7 @@ module MkKernel
4250
4584
  io.puts " CArray *_sc;"
4251
4585
  io.puts " GetCArray(_st, _sc);"
4252
4586
  io.puts " if ( _sc->data_type != CA_FIXLEN && ! _orderable ) {"
4253
- io.puts %Q[ rb_raise(rb_eArgError,]
4254
- io.puts %Q[ "#{name}_ki: Face-typed input (%s) is not orderable "]
4255
- io.puts %Q[ "by storage; use ca.parent to descend to storage",]
4256
- io.puts %Q[ rb_obj_classname(_face));]
4587
+ io.puts %Q[ ca_face_refuse_order(ca_calling_method_name(), _face, "order");]
4257
4588
  io.puts " }"
4258
4589
  io.puts " self = _st;"
4259
4590
  io.puts " src = _sc;"
@@ -4276,10 +4607,7 @@ module MkKernel
4276
4607
  io.puts " CArray *_sc;"
4277
4608
  io.puts " GetCArray(_st, _sc);"
4278
4609
  io.puts " if ( _sc->data_type != CA_FIXLEN && ! _orderable ) {"
4279
- io.puts %Q[ rb_raise(rb_eArgError,]
4280
- io.puts %Q[ "#{name}_ki: Face-typed input (%s) is not orderable "]
4281
- io.puts %Q[ "by storage; use ca.parent to descend to storage",]
4282
- io.puts %Q[ rb_obj_classname(_face));]
4610
+ io.puts %Q[ ca_face_refuse_order(ca_calling_method_name(), _face, "order");]
4283
4611
  io.puts " }"
4284
4612
  if relift
4285
4613
  io.puts " _reduce_face = _face;"
@@ -4290,32 +4618,37 @@ module MkKernel
4290
4618
  end
4291
4619
 
4292
4620
  # Re-lift a value-returning reduction result back into the Face captured by
4293
- # emit_reduce_face_gate. Full-reduction scalar -> storage_to_scalar hook;
4294
- # per-axis CArray -> rb_ca_face_template (carries the subclass tail).
4621
+ # emit_reduce_face_gate. Full-reduction scalar -> storage_to_element hook;
4622
+ # per-axis CArray -> ca_face_relift_entity (tail, mask and copy_state).
4295
4623
  # All-masked full reductions leave CA_UNDEF untouched.
4296
4624
  def self.emit_reduce_face_relift(io)
4297
- io.puts " if ( _reduce_face != Qnil && result != CA_UNDEF && result != Qnil ) {"
4298
- io.puts " if ( rb_obj_is_kind_of(result, rb_cCArray) ) {"
4299
- io.puts " /* per-axis: re-lift the storage result as a Face of the same"
4300
- io.puts " subclass; the template memcpy carries the subclass tail. */"
4301
- io.puts " CArray *_rca;"
4302
- io.puts " GetCArray(result, _rca);"
4303
- io.puts " VALUE _lifted = rb_ca_face_template(_reduce_face, _rca, _rca->dim);"
4304
- io.puts " rb_ca_set_parent(_lifted, result); /* pin storage result as GC root */"
4305
- io.puts " if ( ca_has_mask(_rca) ) {"
4306
- io.puts " /* rb_ca_face_template resets mask to NULL; carry the reduced"
4307
- io.puts " mask bits (all-masked slabs) onto the lifted Face. */"
4308
- io.puts " CArray *_lca;"
4309
- io.puts " GetCArray(_lifted, _lca);"
4310
- io.puts " ca_copy_mask(_lca, _rca);"
4625
+ io.puts " if ( _reduce_face != Qnil && RB_TYPE_P(result, T_ARRAY) ) {"
4626
+ io.puts " /* a multi-output reduction (minmax): re-lift each output. */"
4627
+ io.puts " long _i;"
4628
+ io.puts " for ( _i = 0; _i < RARRAY_LEN(result); _i++ ) {"
4629
+ io.puts " VALUE _r = rb_ary_entry(result, _i);"
4630
+ io.puts " if ( _r == CA_UNDEF || _r == Qnil ) continue;"
4631
+ io.puts " if ( rb_obj_is_kind_of(_r, rb_cCArray) ) {"
4632
+ io.puts " _r = ca_face_relift_entity(_reduce_face, _r);"
4311
4633
  io.puts " }"
4312
- io.puts " result = _lifted;"
4634
+ io.puts " else {"
4635
+ io.puts " CArray *_fca;"
4636
+ io.puts " GetCArray(_reduce_face, _fca);"
4637
+ io.puts " CA_FACE_STORAGE_TO_ELEMENT_IF_FACE(_r, _reduce_face, _fca);"
4638
+ io.puts " }"
4639
+ io.puts " rb_ary_store(result, _i, _r);"
4640
+ io.puts " }"
4641
+ io.puts " }"
4642
+ io.puts " else if ( _reduce_face != Qnil && result != CA_UNDEF && result != Qnil ) {"
4643
+ io.puts " if ( rb_obj_is_kind_of(result, rb_cCArray) ) {"
4644
+ io.puts " /* per-axis: re-lift the storage result as a Face of the same class. */"
4645
+ io.puts " result = ca_face_relift_entity(_reduce_face, result);"
4313
4646
  io.puts " }"
4314
4647
  io.puts " else {"
4315
4648
  io.puts " /* full reduction: decode the scalar via the registered hook. */"
4316
4649
  io.puts " CArray *_fca;"
4317
4650
  io.puts " GetCArray(_reduce_face, _fca);"
4318
- io.puts " CA_FACE_STORAGE_TO_SCALAR_IF_FACE(result, _reduce_face, _fca);"
4651
+ io.puts " CA_FACE_STORAGE_TO_ELEMENT_IF_FACE(result, _reduce_face, _fca);"
4319
4652
  io.puts " }"
4320
4653
  io.puts " }"
4321
4654
  end
@@ -4327,7 +4660,7 @@ module MkKernel
4327
4660
  has_kind = !partition # :full and :rank gain kind: dispatch (Option γ)
4328
4661
  mask_sentinel = (k[:mask_self] == :sentinel)
4329
4662
  # bind_ruby: false kernels are consumed by other .c files at the C
4330
- # level (= rb_ca_sorted_view in carray_order.c calls rb_ca_sort_addr_ki).
4663
+ # level (carray_sort.c / carray_partition.c call the _mp twins).
4331
4664
  # Emit extern linkage so the symbol is reachable across translation
4332
4665
  # units. bind_ruby: true kernels stay static (only referenced by
4333
4666
  # Init_carray_kernels in this file).
@@ -4339,6 +4672,10 @@ module MkKernel
4339
4672
  # partition_index / rank_index, which are consumed at the C level
4340
4673
  # by carray_order.c (= ordering helpers like nlargest, order).
4341
4674
  storage = (k[:bind_ruby] == false || k[:c_callable]) ? "" : "static "
4675
+ # bind_ruby: false + mask_self: :sentinel kernels are reached only
4676
+ # through their _mp twins, so the fixed-masked_last entries below
4677
+ # would have no caller.
4678
+ mp_only = (k[:mask_self] == :sentinel && k[:bind_ruby] == false)
4342
4679
  # PROPOSAL_PORTABLE_TEXTBOOK_SORT §9.5.2 Option γ:
4343
4680
  # For algorithm :full / :rank, dispatch emits THREE C-level entries:
4344
4681
  # rb_ca_<name>_ki_quick (self, vaxis) -> do_stable=0
@@ -4375,6 +4712,7 @@ module MkKernel
4375
4712
  io.puts "{"
4376
4713
  io.puts " CArray *src;"
4377
4714
  io.puts " GetCArray(self, src);"
4715
+ emit_lazy_evaluated(io)
4378
4716
  io.puts " /* Face gate: descend to storage (fixlen -> memcmp default,"
4379
4717
  io.puts " numeric -> ORDERABLE required). The strip also makes the"
4380
4718
  io.puts " switch below and every index/addr template output build on"
@@ -4384,23 +4722,24 @@ module MkKernel
4384
4722
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
4385
4723
  io.puts " masked elements would break the ordering invariant. */"
4386
4724
  io.puts " if ( ca_has_mask(src) ) {"
4387
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: masked input not supported (use ca.value or ca.strip_mask(fill))");]
4725
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: masked input not supported (use ca.value or ca.strip_mask(fill))", ca_calling_method_name());]
4388
4726
  io.puts " }"
4389
4727
  end
4390
- io.puts " int axis = NUM2INT(vaxis);"
4728
+ io.puts " int axis = (int) ca_axis_integer(vaxis, NULL);"
4391
4729
  io.puts " if ( axis < 0 ) axis += src->ndim;"
4392
4730
  io.puts " if ( axis < 0 || axis >= src->ndim ) {"
4393
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(vaxis), src->ndim);]
4731
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: axis %d out of range for ndim %d", ca_calling_method_name(), NUM2INT(vaxis), src->ndim);]
4394
4732
  io.puts " }"
4395
4733
  io.puts " switch ( src->data_type ) {"
4396
4734
  k[:source].each do |s|
4397
4735
  si = DTYPES[s]
4398
4736
  native_extra = mask_sentinel ? ", masked_last" : ""
4399
4737
  native_extra += rank ? ", dense" : ""
4400
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, axis, do_stable#{native_extra});"
4738
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, axis, do_stable#{native_extra}));"
4401
4739
  end
4402
4740
  io.puts " default:"
4403
- 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]);]
4741
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
4742
+ 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]);]
4404
4743
  io.puts " }"
4405
4744
  io.puts " return Qnil; /* unreachable */"
4406
4745
  io.puts "}"
@@ -4413,30 +4752,33 @@ module MkKernel
4413
4752
  dispatch_call_last = mask_sentinel ? ", 1" : ""
4414
4753
  dispatch_call_last += rank ? ", 0" : ""
4415
4754
 
4416
- # _ki_quick: arity 2 (self, vaxis). Linkage = storage.
4417
- io.puts
4418
- io.puts "#{storage}VALUE"
4419
- io.puts "rb_ca_#{name}_ki_quick (VALUE self, VALUE vaxis)"
4420
- io.puts "{"
4421
- io.puts " return #{name}_ki_dispatch(self, vaxis, 0#{dispatch_call_last});"
4422
- io.puts "}"
4755
+ unless mp_only
4756
+ # _ki_quick: arity 2 (self, vaxis). Linkage = storage.
4757
+ io.puts
4758
+ io.puts "#{storage}VALUE"
4759
+ io.puts "rb_ca_#{name}_ki_quick (VALUE self, VALUE vaxis)"
4760
+ io.puts "{"
4761
+ io.puts " return #{name}_ki_dispatch(self, vaxis, 0#{dispatch_call_last});"
4762
+ io.puts "}"
4423
4763
 
4424
- # _ki_stable: arity 2 (self, vaxis). Linkage = storage.
4425
- io.puts
4426
- io.puts "#{storage}VALUE"
4427
- io.puts "rb_ca_#{name}_ki_stable (VALUE self, VALUE vaxis)"
4428
- io.puts "{"
4429
- io.puts " return #{name}_ki_dispatch(self, vaxis, 1#{dispatch_call_last});"
4430
- io.puts "}"
4764
+ # _ki_stable: arity 2 (self, vaxis). Linkage = storage.
4765
+ io.puts
4766
+ io.puts "#{storage}VALUE"
4767
+ io.puts "rb_ca_#{name}_ki_stable (VALUE self, VALUE vaxis)"
4768
+ io.puts "{"
4769
+ io.puts " return #{name}_ki_dispatch(self, vaxis, 1#{dispatch_call_last});"
4770
+ io.puts "}"
4431
4771
 
4432
- # _ki: arity 2 alias of _ki_quick (= preserves existing arity-1
4433
- # Ruby binding and existing C extern callers).
4434
- io.puts
4435
- io.puts "#{storage}VALUE"
4436
- io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis)"
4437
- io.puts "{"
4438
- io.puts " return rb_ca_#{name}_ki_quick(self, vaxis);"
4439
- io.puts "}"
4772
+ # _ki: arity 2 alias of _ki_quick (= preserves existing arity-1
4773
+ # Ruby binding and existing C extern callers).
4774
+ io.puts
4775
+ io.puts "#{storage}VALUE"
4776
+ io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis)"
4777
+ io.puts "{"
4778
+ io.puts " return rb_ca_#{name}_ki_quick(self, vaxis);"
4779
+ io.puts "}"
4780
+
4781
+ end
4440
4782
 
4441
4783
  if mask_sentinel && k[:bind_ruby] == false
4442
4784
  # _mp twins: explicit masked_last, non-Ruby (plain C int, no
@@ -4458,16 +4800,16 @@ module MkKernel
4458
4800
  end
4459
4801
 
4460
4802
  if rank
4461
- # _quick_dense: explicit dense (0 :ordinal / 1 :dense), extern for
4462
- # carray_order.c's `order` method: kwarg (order is CA_INT64-family
4463
- # sugar over rank_index, defined in a different file, so it needs
4803
+ # _kind_dense: explicit kind (0 :quick / 1 :stable) and dense
4804
+ # (0 :ordinal / 1 :dense), extern for carray_order.c's `order`
4805
+ # (sugar over rank_index, defined in a different file, so it needs
4464
4806
  # a non-static entry point the same way the mask_self: :sentinel
4465
4807
  # kernels' _mp twins do).
4466
4808
  io.puts
4467
4809
  io.puts "VALUE"
4468
- io.puts "rb_ca_#{name}_ki_quick_dense (VALUE self, VALUE vaxis, int dense)"
4810
+ io.puts "rb_ca_#{name}_ki_kind_dense (VALUE self, VALUE vaxis, int stable, int dense)"
4469
4811
  io.puts "{"
4470
- io.puts " return #{name}_ki_dispatch(self, vaxis, 0, dense);"
4812
+ io.puts " return #{name}_ki_dispatch(self, vaxis, stable, dense);"
4471
4813
  io.puts "}"
4472
4814
  end
4473
4815
  else
@@ -4482,6 +4824,7 @@ module MkKernel
4482
4824
  io.puts "{"
4483
4825
  io.puts " CArray *src;"
4484
4826
  io.puts " GetCArray(self, src);"
4827
+ emit_lazy_evaluated(io)
4485
4828
  io.puts " /* Face gate (partition): same as the sort dispatcher --"
4486
4829
  io.puts " fixlen -> memcmp default, numeric -> ORDERABLE required. */"
4487
4830
  emit_sort_face_gate(io, name)
@@ -4489,38 +4832,42 @@ module MkKernel
4489
4832
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
4490
4833
  io.puts " masked elements would break the ordering invariant. */"
4491
4834
  io.puts " if ( ca_has_mask(src) ) {"
4492
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: masked input not supported (use ca.value or ca.strip_mask(fill))");]
4835
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: masked input not supported (use ca.value or ca.strip_mask(fill))", ca_calling_method_name());]
4493
4836
  io.puts " }"
4494
4837
  end
4495
- io.puts " int axis = NUM2INT(vaxis);"
4838
+ io.puts " int axis = (int) ca_axis_integer(vaxis, NULL);"
4496
4839
  io.puts " if ( axis < 0 ) axis += src->ndim;"
4497
4840
  io.puts " if ( axis < 0 || axis >= src->ndim ) {"
4498
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(vaxis), src->ndim);]
4841
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: axis %d out of range for ndim %d", ca_calling_method_name(), NUM2INT(vaxis), src->ndim);]
4499
4842
  io.puts " }"
4500
- io.puts " ca_size_t kth = NUM2SIZE(vkth);"
4843
+ io.puts " ca_size_t kth_raw = (ca_size_t) ca_integer_arg(vkth, \"kth\", NULL);"
4844
+ io.puts " ca_size_t kth = kth_raw;"
4501
4845
  io.puts " ca_size_t axis_n = src->dim[axis];"
4502
4846
  io.puts " if ( kth < 0 ) kth += axis_n;"
4503
4847
  io.puts " if ( kth < 0 || kth >= axis_n ) {"
4504
- 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);]
4848
+ 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);]
4505
4849
  io.puts " }"
4506
4850
  io.puts " switch ( src->data_type ) {"
4507
4851
  k[:source].each do |s|
4508
4852
  si = DTYPES[s]
4509
4853
  native_extra = mask_sentinel ? ", masked_last" : ""
4510
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, axis, kth#{native_extra});"
4854
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, axis, kth#{native_extra}));"
4511
4855
  end
4512
4856
  io.puts " default:"
4513
- 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]);]
4857
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
4858
+ 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]);]
4514
4859
  io.puts " }"
4515
4860
  io.puts " return Qnil; /* unreachable */"
4516
4861
  io.puts "}"
4517
4862
 
4518
- io.puts
4519
- io.puts "#{storage}VALUE"
4520
- io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis, VALUE vkth)"
4521
- io.puts "{"
4522
- io.puts " return #{name}_ki_dispatch(self, vaxis, vkth#{mask_sentinel ? ", 1" : ""});"
4523
- io.puts "}"
4863
+ unless mp_only
4864
+ io.puts
4865
+ io.puts "#{storage}VALUE"
4866
+ io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis, VALUE vkth)"
4867
+ io.puts "{"
4868
+ io.puts " return #{name}_ki_dispatch(self, vaxis, vkth#{mask_sentinel ? ", 1" : ""});"
4869
+ io.puts "}"
4870
+ end
4524
4871
 
4525
4872
  if mask_sentinel && k[:bind_ruby] == false
4526
4873
  # _mp twin: explicit masked_last, extern for carray_partition.c's
@@ -4538,10 +4885,10 @@ module MkKernel
4538
4885
  # wrapper). When public_method: is set, emit a -1-argc
4539
4886
  # entry that parses `axis:` (and `kind:` for has_kind, and
4540
4887
  # `masked_position:` for mask_self: :sentinel, and `method:` for
4541
- # :rank) kwarg with default 0 / :quick / :last / :ordinal and
4542
- # dispatches to the appropriate positional entry. For :partition
4543
- # algorithm, kth is accepted as a single positional argument
4544
- # before the kwarg.
4888
+ # :rank) kwarg with default nil (= whole array) / :quick / :last /
4889
+ # :ordinal and dispatches to the appropriate positional entry.
4890
+ # For :partition algorithm, kth is accepted as a single positional
4891
+ # argument before the kwarg.
4545
4892
  if k[:public_method]
4546
4893
  legacy_name = k[:public_method].to_s
4547
4894
  io.puts
@@ -4568,7 +4915,21 @@ module MkKernel
4568
4915
  scan_targets << "&rmasked_position" if mask_sentinel
4569
4916
  scan_targets << "&rmethod" if rank
4570
4917
  io.puts " rb_scan_options(ropt, \"#{scan_keys.join(",")}\", #{scan_targets.join(", ")});"
4571
- io.puts " if ( NIL_P(raxis) ) raxis = INT2NUM(0); /* default axis */"
4918
+ # No axis means the whole array, as for sort: flatten and walk the
4919
+ # single fiber. rank_index keeps self's shape (a rank belongs to a
4920
+ # cell, as for order); the position lists keep the flat shape.
4921
+ io.puts " volatile VALUE whole_src = Qnil;" if rank
4922
+ io.puts " if ( NIL_P(raxis) ) { /* no axis: the whole array */"
4923
+ io.puts " whole_src = self;" if rank
4924
+ # The flattened view of a lazy expression is not one itself, so the
4925
+ # dispatcher would not see an expression to hand over: ask here.
4926
+ io.puts " {"
4927
+ io.puts " volatile VALUE evaluated = ca_lazy_evaluated(self);"
4928
+ io.puts " if ( ! NIL_P(evaluated) ) self = evaluated;"
4929
+ io.puts " }"
4930
+ io.puts " self = rb_ca_flatten(self);"
4931
+ io.puts " raxis = INT2NUM(0);"
4932
+ io.puts " }"
4572
4933
  if rank
4573
4934
  # method: :ordinal (default) / :dense -> dense int. :ordinal
4574
4935
  # assigns every cell a distinct rank (ties broken by original
@@ -4576,37 +4937,15 @@ module MkKernel
4576
4937
  # the standard "dense rank" used to compose rank as a sort_addr
4577
4938
  # priority key without silently dropping lower-priority keys on
4578
4939
  # ties (see MASKED_POSITION's sibling proposal thread).
4579
- io.puts " int dense = 0; /* default method: :ordinal */"
4580
- io.puts " if ( ! NIL_P(rmethod) ) {"
4581
- io.puts " static ID sym_ordinal = 0, sym_dense = 0;"
4582
- io.puts " if ( ! sym_ordinal ) sym_ordinal = rb_intern(\"ordinal\");"
4583
- io.puts " if ( ! sym_dense ) sym_dense = rb_intern(\"dense\");"
4584
- io.puts " ID method_id = SYM2ID(rmethod);"
4585
- io.puts " if ( method_id == sym_ordinal ) dense = 0;"
4586
- io.puts " else if ( method_id == sym_dense ) dense = 1;"
4587
- io.puts " else {"
4588
- io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: unknown method %s (expected :ordinal or :dense)", rb_id2name(method_id));]
4589
- io.puts " }"
4590
- io.puts " }"
4940
+ io.puts " int dense = NIL_P(rmethod) ? 0 : ca_symbol_choice(rmethod, \"method\", \"ordinal\", \"dense\", NULL);"
4591
4941
  end
4592
4942
  if mask_sentinel
4593
4943
  # masked_position: :last (default) / :first -> masked_last int.
4594
4944
  # Shared by both the has_kind and :partition branches below.
4595
- io.puts " int masked_last = 1; /* default masked_position: :last */"
4596
- io.puts " if ( ! NIL_P(rmasked_position) ) {"
4597
- io.puts " static ID sym_first = 0, sym_last = 0;"
4598
- io.puts " if ( ! sym_first ) sym_first = rb_intern(\"first\");"
4599
- io.puts " if ( ! sym_last ) sym_last = rb_intern(\"last\");"
4600
- io.puts " ID mp_id = SYM2ID(rmasked_position);"
4601
- io.puts " if ( mp_id == sym_last ) masked_last = 1;"
4602
- io.puts " else if ( mp_id == sym_first ) masked_last = 0;"
4603
- io.puts " else {"
4604
- io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: unknown masked_position %s (expected :first or :last)", rb_id2name(mp_id));]
4605
- io.puts " }"
4606
- io.puts " }"
4945
+ io.puts " int masked_last = NIL_P(rmasked_position) ? 1 : ca_symbol_choice(rmasked_position, \"masked_position\", \"first\", \"last\", NULL);"
4607
4946
  end
4608
4947
  if partition
4609
- # partition_index(kth, axis: 0, masked_position: :last) -- kth
4948
+ # partition_index(kth, axis: nil, masked_position: :last) -- kth
4610
4949
  # positional, axis/masked_position kwargs.
4611
4950
  io.puts " if ( argc != 1 ) {"
4612
4951
  io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: wrong number of positional arguments (given %d, expected 1: kth)", argc);]
@@ -4617,28 +4956,27 @@ module MkKernel
4617
4956
  io.puts " return rb_ca_#{name}_ki(self, raxis, argv[0]);"
4618
4957
  end
4619
4958
  else
4620
- # sort_index(axis: 0, kind: :quick, masked_position: :last) --
4959
+ # sort_index(axis: nil, kind: :quick, masked_position: :last) --
4621
4960
  # no positional, kwargs only.
4622
4961
  io.puts " if ( argc != 0 ) {"
4623
4962
  io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: positional args no longer accepted (given %d args); use axis:/kind: kwargs", argc);]
4624
4963
  io.puts " }"
4625
4964
  # kind: dispatch (:quick default, :stable optional).
4626
- io.puts " int do_stable = 0;"
4627
- io.puts " if ( ! NIL_P(rkind) ) {"
4628
- io.puts " static ID sym_quick = 0, sym_stable = 0;"
4629
- io.puts " if ( ! sym_quick ) sym_quick = rb_intern(\"quick\");"
4630
- io.puts " if ( ! sym_stable ) sym_stable = rb_intern(\"stable\");"
4631
- io.puts " ID kind_id = SYM2ID(rkind);"
4632
- io.puts " if ( kind_id == sym_quick ) do_stable = 0;"
4633
- io.puts " else if ( kind_id == sym_stable ) do_stable = 1;"
4634
- io.puts " else {"
4635
- io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: unknown kind %s (expected :quick or :stable)", rb_id2name(kind_id));]
4636
- io.puts " }"
4637
- io.puts " }"
4965
+ io.puts " int do_stable = NIL_P(rkind) ? 0 : ca_symbol_choice(rkind, \"kind\", \"quick\", \"stable\", NULL);"
4638
4966
  if mask_sentinel
4639
4967
  io.puts " return #{name}_ki_dispatch(self, raxis, do_stable, masked_last);"
4640
4968
  elsif rank
4641
- io.puts " return #{name}_ki_dispatch(self, raxis, do_stable, dense);"
4969
+ io.puts " VALUE out = #{name}_ki_dispatch(self, raxis, do_stable, dense);"
4970
+ io.puts " if ( ! NIL_P(whole_src) ) {"
4971
+ io.puts " CArray *src;"
4972
+ io.puts " VALUE shape_argv[CA_RANK_MAX];"
4973
+ io.puts " GetCArray(whole_src, src);"
4974
+ io.puts " for ( int k = 0; k < src->ndim; k++ ) {"
4975
+ io.puts " shape_argv[k] = SIZE2NUM(src->dim[k]);"
4976
+ io.puts " }"
4977
+ io.puts " out = rb_ca_reshape((int) src->ndim, shape_argv, out);"
4978
+ io.puts " }"
4979
+ io.puts " return out;"
4642
4980
  else
4643
4981
  io.puts " return do_stable ? rb_ca_#{name}_ki_stable(self, raxis)"
4644
4982
  io.puts " : rb_ca_#{name}_ki_quick(self, raxis);"
@@ -4679,7 +5017,7 @@ module MkKernel
4679
5017
  io.puts
4680
5018
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
4681
5019
  k[:source].each do |src|
4682
- emit_search_native(io, k, src)
5020
+ emit_native_for(io, k, src) { |o| emit_search_native(o, k, src) }
4683
5021
  end
4684
5022
  emit_search_dispatch(io, k)
4685
5023
  end
@@ -4702,7 +5040,7 @@ module MkKernel
4702
5040
  # body: Hash form picks per-data_type-kind branch (= :int / :float /
4703
5041
  # :object / :fixlen); String form is uniform.
4704
5042
  # PROPOSAL_MKKERNEL_OBJECT_DTYPE_BRANCH Phase 4: :object branch added for
4705
- # CA_OBJECT (equality via rb_equal, ordering via rb_funcall(<=>)).
5043
+ # CA_OBJECT (equality via ca_obj_equal, ordering via rb_funcall(<=>)).
4706
5044
  # PROPOSAL_SEARCH_SEMANTICS_UNIFY S2: :fixlen branch added for CA_FIXLEN
4707
5045
  # (the cell is a runtime-width byte blob; comparison is memcmp over
4708
5046
  # ca->bytes, the same total order fixlen bincmp uses).
@@ -4736,7 +5074,7 @@ module MkKernel
4736
5074
  <<~C.chomp
4737
5075
  int eps_is_default = NIL_P(reps);
4738
5076
  double user_eps = eps_is_default ? 0.0 : NUM2DBL(reps);
4739
- double query_eps = eps_is_default ? (double)(#{eps_def}) * fabs((double)query_val) : user_eps;
5077
+ double query_eps = eps_is_default ? (isfinite((double)query_val) ? (double)(#{eps_def}) * fabs((double)query_val) : 0.0) : user_eps;
4740
5078
  C
4741
5079
  elsif has_eps && no_eps
4742
5080
  "(void) reps; /* #{is_fixlen ? "CA_FIXLEN" : "CA_OBJECT"}: eps ignored, exact match only */\n double query_eps = 0.0; (void) query_eps;"
@@ -4751,7 +5089,7 @@ module MkKernel
4751
5089
  ""
4752
5090
  end
4753
5091
  eps_percell = if has_eps && !no_eps
4754
- "double query_eps = eps_is_default ? (double)(#{eps_def}) * fabs((double)query_val) : user_eps;"
5092
+ "double query_eps = eps_is_default ? (isfinite((double)query_val) ? (double)(#{eps_def}) * fabs((double)query_val) : 0.0) : user_eps;"
4755
5093
  else
4756
5094
  "double query_eps = 0.0; (void) query_eps;"
4757
5095
  end
@@ -4763,14 +5101,36 @@ module MkKernel
4763
5101
  # and let rb_ca_obj2ptr pack it; `query_val` is then a char* the memcmp
4764
5102
  # body reads (mirrors the legacy flat bsearch ALLOCA_N path).
4765
5103
  query_setup_a = if is_fixlen
4766
- "char *query_val = ALLOCA_N(char, ca->bytes);\n" \
5104
+ "volatile VALUE query_val_holder = 0;\n" \
5105
+ " char *query_val = ALLOCV_N(char, query_val_holder, ca->bytes);\n" \
4767
5106
  " rb_ca_obj2ptr(self, rval, query_val);"
4768
5107
  else
5108
+ (INT_DTYPES.include?(src) ? "ca_check_integer_fits(rval, #{si[:ca]});\n " : "") +
4769
5109
  "#{si[:c]} query_val_buf;\n" \
4770
5110
  " rb_ca_obj2ptr(self, rval, &query_val_buf);\n" \
4771
5111
  " #{si[:c]} query_val = query_val_buf;"
4772
5112
  end
4773
5113
 
5114
+ # The object lane calls Ruby for every cell of the case B/C walk, which
5115
+ # runs between a direct attach and detach of self and val; a raise there
5116
+ # would leave both attached. An entity holds no attach, so the lane
5117
+ # reads an entity copy of whichever is a view.
5118
+ entity_bc = if is_object
5119
+ <<~C.chomp
5120
+ volatile VALUE vca_entity = self;
5121
+ if ( ! ca_is_entity(ca) ) {
5122
+ vca_entity = rb_ca_copy(self);
5123
+ GetCArray(vca_entity, ca);
5124
+ }
5125
+ if ( ! ca_is_entity(cv) ) {
5126
+ vsrc_val = rb_ca_copy(vsrc_val);
5127
+ GetCArray(vsrc_val, cv);
5128
+ }
5129
+ C
5130
+ else
5131
+ ""
5132
+ end
5133
+
4774
5134
  # case B/C query coercion. numeric/object: wrap-readonly to self's
4775
5135
  # data_type when they differ. fixlen: require a CA_FIXLEN query of the
4776
5136
  # same byte width (no scalar coercion exists for blobs), so the per-cell
@@ -4780,7 +5140,7 @@ module MkKernel
4780
5140
  (void) vsrc_val; /* fixlen: no wrap-readonly coercion */
4781
5141
  if ( cv->data_type != CA_FIXLEN || cv->bytes != ca->bytes ) {
4782
5142
  rb_raise(rb_eArgError,
4783
- "#{name}_ki: CA_FIXLEN query must be CA_FIXLEN of the same byte width (= %ld)", (long) ca->bytes);
5143
+ "%s: CA_FIXLEN query must be CA_FIXLEN of the same byte width (= %ld)", ca_calling_method_name(), (long) ca->bytes);
4784
5144
  }
4785
5145
  C
4786
5146
  else
@@ -4939,7 +5299,7 @@ module MkKernel
4939
5299
  slab_axes, 1, 0);
4940
5300
  if ( rc != CA_ITER_OK ) {
4941
5301
  rb_raise(rb_eRuntimeError,
4942
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
5302
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
4943
5303
  }
4944
5304
  C
4945
5305
 
@@ -5021,7 +5381,7 @@ module MkKernel
5021
5381
  # transformation in two more inner loops.
5022
5382
  io.puts <<~C
5023
5383
  /* view_flat semantics + CArray val: NotImpError (deferred). */
5024
- rb_raise(rb_eNotImpError, "#{name}_ki: view_flat semantics with CArray val (broadcast) not yet supported");
5384
+ rb_raise(rb_eNotImpError, "%s: view_flat semantics with CArray val (broadcast) not yet supported", ca_calling_method_name());
5025
5385
  C
5026
5386
  end
5027
5387
 
@@ -5044,7 +5404,9 @@ module MkKernel
5044
5404
  */
5045
5405
  CArray *cv;
5046
5406
  GetCArray(rval, cv);
5047
- VALUE vsrc_val = rval;
5407
+ /* volatile: once the query is wrapped below, only cv is used, and
5408
+ the wrapper must stay reachable while its cells are read. */
5409
+ volatile VALUE vsrc_val = rval;
5048
5410
  #{coerce_bc}
5049
5411
 
5050
5412
  /* base_shape = self.shape with axis removed. base_to_self[bk] =
@@ -5079,13 +5441,13 @@ module MkKernel
5079
5441
  if ( i == axis ) continue;
5080
5442
  if ( cv->dim[i] != ca->dim[i] ) {
5081
5443
  rb_raise(rb_eArgError,
5082
- "#{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)",
5444
+ "%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(),
5083
5445
  axis, (int) i, (long) cv->dim[i], (int) i, (long) ca->dim[i]);
5084
5446
  }
5085
5447
  }
5086
5448
  if ( cv->dim[axis] < 1 ) {
5087
5449
  rb_raise(rb_eArgError,
5088
- "#{name}_ki: empty query (val.dim[%d] = 0)", axis);
5450
+ "%s: empty query (val.dim[%d] = 0)", ca_calling_method_name(), axis);
5089
5451
  }
5090
5452
  cv_form = CV_FORM_A3;
5091
5453
  cv_form_set = 1;
@@ -5095,7 +5457,7 @@ module MkKernel
5095
5457
  /* A2: 1-D shared M-query (M >= 1); placed above A2.5 in rev5 */
5096
5458
  if ( cv->dim[0] < 1 ) {
5097
5459
  rb_raise(rb_eArgError,
5098
- "#{name}_ki: empty query (val.dim[0] = 0)");
5460
+ "%s: empty query (val.dim[0] = 0)", ca_calling_method_name());
5099
5461
  }
5100
5462
  cv_form = CV_FORM_A2;
5101
5463
  cv_form_set = 1;
@@ -5116,7 +5478,7 @@ module MkKernel
5116
5478
 
5117
5479
  if ( ! cv_form_set ) {
5118
5480
  rb_raise(rb_eArgError,
5119
- "#{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))",
5481
+ "%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(),
5120
5482
  (int) cv->ndim, (int) base_ndim, (int) ca->ndim);
5121
5483
  }
5122
5484
 
@@ -5151,9 +5513,9 @@ module MkKernel
5151
5513
  #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
5152
5514
  #{op_mask_decl}
5153
5515
 
5516
+ #{entity_bc}
5154
5517
  /* Attach self + val; rev4 uses raw row-major byte addressing. */
5155
- ca_attach(ca);
5156
- ca_attach(cv);
5518
+ ca_attach_n(2, ca, cv); /* window: numeric bodies do not raise; the object lane reads entities */
5157
5519
 
5158
5520
  /* self_byte_stride[j] = bytes to advance along self axis j (row-major). */
5159
5521
  ca_size_t self_byte_stride[CA_RANK_MAX];
@@ -5281,25 +5643,18 @@ module MkKernel
5281
5643
  io.puts "{"
5282
5644
  io.puts " CArray *src;"
5283
5645
  io.puts " VALUE self_ref = self; /* pre-strip Face, for to_comparable */"
5284
- io.puts " int self_face_comparable = 0;"
5285
5646
  io.puts " int self_was_face = 0;"
5286
5647
  io.puts " GetCArray(self, src);"
5648
+ emit_lazy_evaluated(io)
5287
5649
  io.puts " /* Face gate (self): descend a Face reference to storage so the"
5288
5650
  io.puts " search runs on the numeric storage. ORDERABLE licenses the"
5289
- io.puts " ordered/searchable descent; COMPARABLE additionally licenses"
5290
- io.puts " stripping a Face query for direct comparison (see the query"
5291
- io.puts " gate below). A unit-bearing Face (CATime) is ORDERABLE"
5292
- io.puts " but not COMPARABLE, so its query is reconciled by to_comparable"
5293
- io.puts " -- no per-Face Ruby override needed. */"
5651
+ io.puts " ordered/searchable descent; the query gate below brings the"
5652
+ io.puts " query into the same space. */"
5294
5653
  io.puts " if ( ca_is_face(src) ) {"
5295
5654
  io.puts " if ( ! ca_test_flag(src, CA_FLAG_FACE_ORDERABLE_STORAGE) ) {"
5296
- io.puts %Q[ rb_raise(rb_eArgError,]
5297
- io.puts %Q[ "#{name}_ki: Face-typed input (%s) is not orderable "]
5298
- io.puts %Q[ "by storage; use ca.parent to descend to storage",]
5299
- io.puts %Q[ rb_obj_classname(self));]
5655
+ io.puts %Q[ ca_face_refuse_order(ca_calling_method_name(), self, "order");]
5300
5656
  io.puts " }"
5301
5657
  io.puts " self_was_face = 1;"
5302
- io.puts " self_face_comparable = ca_test_flag(src, CA_FLAG_FACE_COMPARABLE_STORAGE);"
5303
5658
  io.puts " self = rb_ca_strip_face_value(self);"
5304
5659
  io.puts " GetCArray(self, src);"
5305
5660
  io.puts " }"
@@ -5313,64 +5668,61 @@ module MkKernel
5313
5668
  io.puts " VALUE raxis = argv[1];"
5314
5669
  io.puts " VALUE reps = (argc >= 3) ? argv[2] : Qnil;"
5315
5670
  end
5316
- io.puts " /* Query gate (generic, PROPOSAL_FACE_ORDERING_GATE + reference"
5317
- io.puts " flip PROPOSAL_TO_COMPARABLE_RECEIVER_FLIP): the reference Face"
5318
- io.puts " reconciles the query into its own space. A COMPARABLE self is"
5319
- io.puts " directly comparable -> strip a Face query, take a plain query"
5320
- io.puts " as-is. A non-COMPARABLE Face reference calls its own"
5321
- io.puts " to_comparable(query) for ANY query type (Face CArray, our"
5322
- io.puts " Scalar, a Ruby Time / DateTime, ...), then strips; the"
5323
- io.puts " reference raises if it cannot reconcile the query. Putting the"
5324
- io.puts " conversion on the reference (always one of our classes) means a"
5325
- io.puts " foreign query (a core Time) needs no monkey-patch, and a new"
5326
- io.puts " Face author writes one to_comparable, not one per query type. */"
5327
- io.puts " {"
5328
- io.puts " int rval_is_face = 0;"
5329
- io.puts " if ( rb_obj_is_kind_of(rval, rb_cCArray) ) {"
5330
- io.puts " CArray *rv_ca;"
5331
- io.puts " TypedData_Get_Struct(rval, CArray, &carray_data_type, rv_ca);"
5332
- io.puts " rval_is_face = ca_is_face(rv_ca);"
5333
- io.puts " }"
5334
- io.puts " if ( self_face_comparable ) {"
5335
- io.puts " if ( rval_is_face ) {"
5336
- io.puts " rval = rb_ca_strip_face_value(rval);"
5337
- io.puts " }"
5338
- io.puts " } else if ( self_was_face ) {"
5339
- io.puts " if ( rb_respond_to(self_ref, rb_intern(\"to_comparable\")) ) {"
5340
- io.puts " rval = rb_funcall(self_ref, rb_intern(\"to_comparable\"), 1, rval);"
5341
- io.puts " rval = rb_ca_strip_face_value(rval);"
5342
- io.puts " } else {"
5343
- io.puts %Q[ rb_raise(rb_eArgError,]
5344
- io.puts %Q[ "#{name}_ki: non-comparable Face reference (%s) has no "]
5345
- io.puts %Q[ "to_comparable to reconcile the query; use ca.parent "]
5346
- io.puts %Q[ "to search the hidden storage explicitly",]
5347
- io.puts %Q[ rb_obj_classname(self_ref));]
5671
+ io.puts " /* Query gate: the reference Face reconciles the query into its own"
5672
+ io.puts " space (ca_face_reconcile_operand). */"
5673
+ io.puts " if ( self_was_face ) {"
5674
+ io.puts " rval = ca_face_reconcile_operand(self_ref, rval, ca_calling_method_name());"
5675
+ io.puts " }"
5676
+
5677
+ # The query and the reference are compared in the type they share, as
5678
+ # `a.eq(q)` compares, not with the query cast to the reference's type:
5679
+ # 3.9 must not match 3, a float64 query must not be rounded to a float32
5680
+ # reference, and -1 must not read as 255. A scalar takes the type the
5681
+ # operators give it (ca_promote_scalar_type); a CArray query the type
5682
+ # the two match in (ca_value_match_type). The reference is read through
5683
+ # a cast view of that type. An Integer scalar keeps the reference's type
5684
+ # and is refused when it does not fit.
5685
+ io.puts " if ( ca_is_integer_type(src) || ca_is_float_type(src) || src->data_type == CA_BOOLEAN ) {"
5686
+ io.puts " int8_t qt = src->data_type;"
5687
+ io.puts " if ( RB_FLOAT_TYPE_P(rval) ) {"
5688
+ io.puts " qt = ca_promote_scalar_type(src->data_type, rval, CA_FLOAT64);"
5689
+ io.puts " } else if ( rb_obj_is_kind_of(rval, rb_cCArray) ) {"
5690
+ io.puts " CArray *qv;"
5691
+ io.puts " TypedData_Get_Struct(rval, CArray, &carray_data_type, qv);"
5692
+ io.puts " if ( ca_is_integer_type(qv) || ca_is_float_type(qv) || qv->data_type == CA_BOOLEAN ) {"
5693
+ io.puts " qt = ca_value_match_type(ca_promote_type(src->data_type, qv->data_type),"
5694
+ io.puts " src->data_type, qv->data_type);"
5348
5695
  io.puts " }"
5349
5696
  io.puts " }"
5697
+ io.puts " if ( qt != src->data_type ) {"
5698
+ io.puts " self = rb_ca_wrap_readonly(self, INT2NUM(qt));"
5699
+ io.puts " GetCArray(self, src);"
5700
+ io.puts " }"
5350
5701
  io.puts " }"
5351
5702
 
5352
5703
  case k[:mask_self]
5353
5704
  when :raise
5354
5705
  io.puts " /* mask_self: :raise -- global reject if self has any masked element. */"
5355
5706
  io.puts " if ( ca_is_any_masked(src) ) {"
5356
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: self should not have any masked elements");]
5707
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: self should not have any masked elements", ca_calling_method_name());]
5357
5708
  io.puts " }"
5358
5709
  when :skip, :ignore
5359
5710
  # body handles mask_in or ignores it
5360
5711
  end
5361
5712
 
5362
- io.puts " int axis = NUM2INT(raxis);"
5713
+ io.puts " int axis = (int) ca_axis_integer(raxis, NULL);"
5363
5714
  io.puts " if ( axis < 0 ) axis += src->ndim;"
5364
5715
  io.puts " if ( axis < 0 || axis >= src->ndim ) {"
5365
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(raxis), src->ndim);]
5716
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: axis %d out of range for ndim %d", ca_calling_method_name(), NUM2INT(raxis), src->ndim);]
5366
5717
  io.puts " }"
5367
5718
  io.puts " switch ( src->data_type ) {"
5368
5719
  k[:source].each do |s|
5369
5720
  si = DTYPES[s]
5370
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src, rval, axis#{extra_arg});"
5721
+ io.puts " case #{si[:ca]}: CA_RETURN_KEEPING(self, #{name}_ki_native_#{s}(self, src, rval, axis#{extra_arg}));"
5371
5722
  end
5372
5723
  io.puts " default:"
5373
- 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]);]
5724
+ io.puts %Q[ ca_face_refuse_numeric(self, ca_calling_method_name(), rb_eCADataTypeError);]
5725
+ 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]);]
5374
5726
  io.puts " }"
5375
5727
  io.puts " return Qnil; /* unreachable */"
5376
5728
  io.puts "}"
@@ -5620,11 +5972,15 @@ module MkKernel
5620
5972
  int8_t in_dt;
5621
5973
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca);
5622
5974
  in_dt = ca->data_type;
5975
+ if ( ca_monop_#{name}_out_data_type[in_dt] <= CA_NONE ) {
5976
+ rb_raise(rb_eCADataTypeError,
5977
+ "#{name}!: source data_type :%s not supported", ca_type_name[in_dt]);
5978
+ }
5623
5979
  if ( ca_monop_#{name}_out_data_type[in_dt] != in_dt ) {
5624
- rb_raise(rb_eRuntimeError,
5625
- "#{name}!: in-place form is not available when output "
5626
- "data_type differs from input (input data_type %d -> output data_type %d); "
5627
- "use non-bang form", in_dt, ca_monop_#{name}_out_data_type[in_dt]);
5980
+ rb_raise(rb_eCADataTypeError,
5981
+ "#{name}!: #{name} of :%s is :%s, which cannot be written back in place; "
5982
+ "use #{name}", ca_type_name[in_dt],
5983
+ ca_type_name[ca_monop_#{name}_out_data_type[in_dt]]);
5628
5984
  }
5629
5985
  return rb_ca_call_monop_bang(self, ca_monop_#{name});
5630
5986
  }
@@ -5878,6 +6234,10 @@ module MkKernel
5878
6234
  io.print <<~C
5879
6235
  VALUE rb_ca_#{name} (VALUE self, VALUE other2, VALUE other3)
5880
6236
  { return rb_ca_call_triop(self, other2, other3, ca_triop_#{name}); }
6237
+ C
6238
+ # bang: false kernels get no bang method, so no bang entry either.
6239
+ return if k[:bang] == false
6240
+ io.print <<~C
5881
6241
 
5882
6242
  VALUE rb_ca_#{name}_bang (VALUE self, VALUE other2, VALUE other3)
5883
6243
  { return rb_ca_call_triop_bang(self, other2, other3, ca_triop_#{name}); }
@@ -6149,15 +6509,56 @@ module MkKernel
6149
6509
  # caller substitutes its own operands. Object-lane bodies are left out,
6150
6510
  # since they call back into the interpreter and cannot be compiled apart
6151
6511
  # from it.
6512
+ #
6513
+ # A comparison is keyed by the data type it compares, not by what it
6514
+ # answers, which is always boolean.
6152
6515
  # ---------------------------------------------------------------------
6153
6516
 
6154
6517
  BODY_TABLE_DTYPES = %i[i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6155
6518
  bool cmplx64 cmplx128].freeze
6156
6519
 
6520
+ # Kernels written outside this generator that a lazy expression can still
6521
+ # hold. A Float or Complex array to an Integer power is CABinOp's ipow
6522
+ # (ext/ca_op_ipower.c, which applies op_powi_<type> cell by cell); its
6523
+ # body is that call.
6524
+ EXTRA_BODY_ROWS = %w[float32 float64 cmplx64 cmplx128].map { |t|
6525
+ ["binop", "ipow", t, "(#3) = op_powi_<type>((#1), (#2));"]
6526
+ }.freeze
6527
+
6528
+ # The helpers the bodies call that are not in the C standard library:
6529
+ # the part of ca_op_powi.h between its markers, as it stands.
6530
+ def self.kernel_helpers
6531
+ text = File.read(File.join(__dir__, "ca_op_powi.h"))
6532
+ text[%r{/\* BEGIN kernel helpers \*/\n(.*)/\* END kernel helpers \*/}m, 1] or
6533
+ raise "ca_op_powi.h: kernel helper markers not found"
6534
+ end
6535
+
6536
+ # A lazy cast node (CAMonOp with op_id CAST_BASE + target) converts cell
6537
+ # by cell through ca_cast_block, which for a numeric target is a plain C
6538
+ # conversion (ext/carray_cast_func.rb). It is keyed as cast_<target> at
6539
+ # the source's data type. Left out: a boolean target, which checks its
6540
+ # input and raises, and a float or complex source to an integer target,
6541
+ # where an out-of-range value or NaN has no defined C result for a
6542
+ # second compilation to agree on.
6543
+ CAST_SOURCES = %i[bool i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6544
+ cmplx64 cmplx128].freeze
6545
+ CAST_TARGETS = %i[i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6546
+ cmplx64 cmplx128].freeze
6547
+ CAST_INEXACT = %i[f32 f64 cmplx64 cmplx128].freeze
6548
+
6549
+ def self.cast_body_rows
6550
+ CAST_SOURCES.product(CAST_TARGETS).filter_map { |src, dst|
6551
+ next if CAST_INEXACT.include?(src) && !CAST_INEXACT.include?(dst)
6552
+ name = DTYPES[dst][:ca].sub(/\ACA_/, "").downcase
6553
+ ["monop", "cast_#{name}", DTYPES[src][:ca].sub(/\ACA_/, "").downcase,
6554
+ "(#2) = (#{DTYPES[dst][:c]})(#1);"]
6555
+ }
6556
+ end
6557
+
6157
6558
  def self.body_table_rows
6158
- rows = []
6559
+ rows = EXTRA_BODY_ROWS.map(&:dup) + cast_body_rows
6159
6560
  KERNELS.each do |k|
6160
- next unless %i[monop binop triop].include?(k[:kind])
6561
+ next unless %i[monop binop triop moncmp bincmp].include?(k[:kind])
6161
6562
  BODY_TABLE_DTYPES.each do |src|
6162
6563
  body = monop_expr_for(k, src)
6163
6564
  next unless body.is_a?(String)
@@ -6169,8 +6570,10 @@ module MkKernel
6169
6570
  rows
6170
6571
  end
6171
6572
 
6573
+ # The block forms keep gsub from reading the backslashes in the
6574
+ # replacement as references to the match.
6172
6575
  def self.c_string_literal(text)
6173
- '"' + text.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", '\\n') + '"'
6576
+ '"' + text.gsub("\\") { "\\\\" }.gsub('"') { '\\"' }.gsub("\n") { '\\n' } + '"'
6174
6577
  end
6175
6578
 
6176
6579
  def self.emit_kernel_bodies(io)
@@ -6219,6 +6622,17 @@ module MkKernel
6219
6622
  }
6220
6623
  return Qnil;
6221
6624
  }
6625
+
6626
+ /* CArray.__kernel_helpers__ -> String: the C a body above may call
6627
+ beyond the standard library, for the caller to put ahead of it. */
6628
+ static const char ca_kernel_helpers[] =
6629
+ #{c_string_literal(kernel_helpers)};
6630
+
6631
+ static VALUE
6632
+ rb_ca_s_kernel_helpers (VALUE klass)
6633
+ {
6634
+ return rb_str_new_cstr(ca_kernel_helpers);
6635
+ }
6222
6636
  C
6223
6637
  end
6224
6638
 
@@ -6240,6 +6654,8 @@ module MkKernel
6240
6654
  end
6241
6655
  io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6242
6656
  io.puts " rb_ca_s_kernel_body, 3);"
6657
+ io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_helpers__\","
6658
+ io.puts " rb_ca_s_kernel_helpers, 0);"
6243
6659
  io.puts "}"
6244
6660
  end
6245
6661
 
@@ -6254,6 +6670,8 @@ module MkKernel
6254
6670
  KERNELS.each { |k| emit_init_line(io, k) }
6255
6671
  io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6256
6672
  io.puts " rb_ca_s_kernel_body, 3);"
6673
+ io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_helpers__\","
6674
+ io.puts " rb_ca_s_kernel_helpers, 0);"
6257
6675
  io.puts "}"
6258
6676
  end
6259
6677
 
@@ -6314,7 +6732,6 @@ module MkKernel
6314
6732
  end
6315
6733
  arity = case k[:kind]
6316
6734
  when :reduce then -1 # axis: kwarg + optional weights/value positional
6317
- when :map then 0 # no args
6318
6735
  when :scan then -1 # axis: kwarg (Integer required)
6319
6736
  when :sort
6320
6737
  # SO.3+ (rev8): :partition algorithm takes (axis, kth) = arity 2
@@ -6339,7 +6756,7 @@ module MkKernel
6339
6756
  # reduce: default false (E.8 retire), opt-in via bind_ruby: true
6340
6757
  # (used by CF.1 count_equal smoke + future kernel testing)
6341
6758
  # sort: default true, opt-out via bind_ruby: false
6342
- # map/scan: always bound (no opt-out)
6759
+ # scan: always bound (no opt-out)
6343
6760
  bind = case k[:kind]
6344
6761
  when :reduce then k[:bind_ruby] == true
6345
6762
  when :sort then k[:bind_ruby] != false
@@ -6441,8 +6858,9 @@ MkKernel.reduce :min,
6441
6858
  # bool acc is u64 (numeric output); cast the boolean8_t
6442
6859
  # load to match and avoid a signed/unsigned compare.
6443
6860
  bool: "acc = ((uint64_t) v < acc) ? (uint64_t) v : acc",
6444
- object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v;' },
6861
+ object: 'if (acc == Qundef || ca_obj_extremum_replaces(v, acc, rb_intern("<"))) acc = v;' },
6445
6862
  reduction_kind: :min, # SL.1.2
6863
+ all_nan_result: :nan,
6446
6864
  # CA_FIXLEN: memcmp lexicographic min (byte order == the fixlen sort
6447
6865
  # order); the numeric reduce/init above are unused for fixlen (bespoke
6448
6866
  # slab walk, see the fixlen: option in MkKernel.reduce).
@@ -6453,8 +6871,8 @@ MkKernel.reduce :min,
6453
6871
  fallback: :raise,
6454
6872
  mask_policy: :min_count, # Phase E
6455
6873
  # An ORDERABLE Face descends to numeric storage, then the result is
6456
- # re-lifted into the Face (scalar via storage_to_scalar, per-axis via
6457
- # rb_ca_face_template). This replaces per-Face lib overrides.
6874
+ # re-lifted into the Face (scalar via storage_to_element, per-axis via
6875
+ # ca_face_relift_entity). This replaces per-Face lib overrides.
6458
6876
  face_gate: :relift,
6459
6877
  public_method: true # Phase E: rebind "min" -> rb_ca_min_ki
6460
6878
 
@@ -6465,8 +6883,9 @@ MkKernel.reduce :max,
6465
6883
  # 0/1); the boolean-returning twin is `any` (= bool max).
6466
6884
  reduce: { numeric: "acc = (v > acc) ? v : acc",
6467
6885
  bool: "acc = ((uint64_t) v > acc) ? (uint64_t) v : acc",
6468
- object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v;' },
6886
+ object: 'if (acc == Qundef || ca_obj_extremum_replaces(v, acc, rb_intern(">"))) acc = v;' },
6469
6887
  reduction_kind: :max, # SL.1.2
6888
+ all_nan_result: :nan,
6470
6889
  # CA_FIXLEN: memcmp lexicographic max (byte order == the fixlen sort order).
6471
6890
  fixlen: :max,
6472
6891
  source: MkKernel::ALL_NUMERIC + [:bool, :object, :fixlen],
@@ -6477,20 +6896,6 @@ MkKernel.reduce :max,
6477
6896
  face_gate: :relift,
6478
6897
  public_method: true # Phase E: rebind "max" -> rb_ca_max_ki
6479
6898
 
6480
- # count_ki — count unmasked elements per slab. The macro skips masked
6481
- # cells, so REDUCE fires only on contributing cells; `acc += 1` ignores
6482
- # the value entirely (cast to void to silence unused-variable warnings).
6483
- # When the input has no mask the macro takes a faster path that loads
6484
- # `v` but our REDUCE doesn't use it -- compiler optimises the load away.
6485
- MkKernel.reduce :count,
6486
- init: "0",
6487
- reduce: "(void)v; acc += 1",
6488
- reduction_kind: :plus, # SL.1.4
6489
- source: MkKernel::ALL_NUMERIC,
6490
- output: :i64,
6491
- ruby_scalar: :LL2NUM,
6492
- fallback: :wrap_to_f64
6493
-
6494
6899
  # all / any reduce: bool input -> bool output, axis-aware. Replaces the
6495
6900
  # flat-only hand-written rb_ca_all_equal_p / rb_ca_any_equal_p /
6496
6901
  # all_close? / any_close? / all_equiv? / any_equiv? in carray_stat.c
@@ -6507,6 +6912,15 @@ MkKernel.reduce :count,
6507
6912
  # modern CPUs. For dramatically lopsided inputs (= mostly-false `all`
6508
6913
  # or mostly-true `any`) a profile-driven early-break variant can be
6509
6914
  # added later as a separate kernel.
6915
+ MkKernel.header_block <<~C
6916
+ /* The square root of a variance: an ε-negative from rounding becomes 0,
6917
+ a NaN stays NaN (fmax(NaN, 0.0) is 0.0). */
6918
+ #ifndef CARRAY_SQRT_CLAMP_DEFINED
6919
+ #define CARRAY_SQRT_CLAMP_DEFINED
6920
+ static inline double ca_sqrt_clamp (double v) { return sqrt(v < 0.0 ? 0.0 : v); }
6921
+ #endif
6922
+ C
6923
+
6510
6924
  MkKernel.header_block <<~C
6511
6925
  /* BOOL2VAL: bool -> Ruby (Qtrue/Qfalse). Used as ruby_scalar wrapper
6512
6926
  for the all/any flat-reduction Ruby surface so `a.all` / `a.any`
@@ -6690,8 +7104,9 @@ MkKernel.reduce :minmax,
6690
7104
  # CA_OBJECT minmax via Qundef sentinel + first-cell-init.
6691
7105
  # One sentinel guards both lo + hi (they go Qundef
6692
7106
  # together on init, get set together on first reduce).
6693
- 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; }' },
7107
+ 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; }' },
6694
7108
  outputs: 2,
7109
+ all_nan_result: :nan,
6695
7110
  finish: { min: "lo", max: "hi" },
6696
7111
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
6697
7112
  # bool: u64 (Integer 0/1) so minmax returns [0/1, 0/1], not
@@ -6700,6 +7115,7 @@ MkKernel.reduce :minmax,
6700
7115
  ruby_scalar: :auto,
6701
7116
  fallback: :raise,
6702
7117
  mask_policy: :min_count, # FM.1.5: match existing min / max behaviour
7118
+ face_gate: :relift,
6703
7119
  public_method: :minmax # bind public name `minmax` -> rb_ca_minmax_ki
6704
7120
  # (= surface-name override, no actual legacy)
6705
7121
 
@@ -6721,14 +7137,19 @@ MkKernel.reduce :minmax,
6721
7137
  # replace the legacy `min_addr` / `max_addr` retired in E.7 stat_proc
6722
7138
  # retire (commit f5c7ecd). 3.0 breaking: name change from `*_addr` to
6723
7139
  # `*_index` is intentional.
7140
+ #
7141
+ # best_i starts at -1 (no cell taken yet), so the first unmasked non-NaN
7142
+ # cell is taken even when it equals the starting limit (an all-+Infinity
7143
+ # or all-INT_MAX fiber). `v == v` keeps a NaN from being taken first; a
7144
+ # fiber of NaN only is left to the all-NaN fixup (UNDEF).
6724
7145
 
6725
7146
  MkKernel.reduce :argmin,
6726
7147
  state: { best_v: :load_type, best_i: :int64_t },
6727
7148
  init: { best_v: { numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qundef" },
6728
- best_i: "0" },
6729
- reduce: { numeric: "if (v < best_v) { best_v = v; best_i = idx; }",
6730
- bool: "if (v < best_v) { best_v = v; best_i = idx; }",
6731
- 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; }' },
7149
+ best_i: "-1" },
7150
+ reduce: { numeric: "if (v < best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7151
+ bool: "if (v < best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7152
+ object: 'if (!CA_OBJ_ISNAN(v) && (best_v == Qundef || ca_obj_cmp(v, best_v) < 0)) { best_v = v; best_i = idx; }' },
6732
7153
  finish: "best_i",
6733
7154
  # CA_FIXLEN: position of the memcmp-lexicographic min (first-wins on ties).
6734
7155
  fixlen: :argmin,
@@ -6740,15 +7161,16 @@ MkKernel.reduce :argmin,
6740
7161
  # An ORDERABLE Face descends to its numeric storage (position output needs
6741
7162
  # no re-lift; the axis-local index is identical for Face and storage).
6742
7163
  face_gate: :strip,
7164
+ all_nan_result: :undef,
6743
7165
  public_method: :min_index
6744
7166
 
6745
7167
  MkKernel.reduce :argmax,
6746
7168
  state: { best_v: :load_type, best_i: :int64_t },
6747
7169
  init: { best_v: { numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qundef" },
6748
- best_i: "0" },
6749
- reduce: { numeric: "if (v > best_v) { best_v = v; best_i = idx; }",
6750
- bool: "if (v > best_v) { best_v = v; best_i = idx; }",
6751
- 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; }' },
7170
+ best_i: "-1" },
7171
+ reduce: { numeric: "if (v > best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7172
+ bool: "if (v > best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7173
+ object: 'if (!CA_OBJ_ISNAN(v) && (best_v == Qundef || ca_obj_cmp(v, best_v) > 0)) { best_v = v; best_i = idx; }' },
6752
7174
  finish: "best_i",
6753
7175
  # CA_FIXLEN: position of the memcmp-lexicographic max (first-wins on ties).
6754
7176
  fixlen: :argmax,
@@ -6758,6 +7180,7 @@ MkKernel.reduce :argmax,
6758
7180
  fallback: :raise,
6759
7181
  mask_policy: :min_count,
6760
7182
  face_gate: :strip,
7183
+ all_nan_result: :undef,
6761
7184
  public_method: :max_index
6762
7185
 
6763
7186
  # ---- argmin_addr / argmax_addr (view-flat address variants) ---------
@@ -6789,10 +7212,10 @@ MkKernel.reduce :argmax,
6789
7212
  MkKernel.reduce :argmin_addr,
6790
7213
  state: { best_v: :load_type, best_i: :int64_t },
6791
7214
  init: { best_v: { numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qundef" },
6792
- best_i: "0" },
6793
- reduce: { numeric: "if (v < best_v) { best_v = v; best_i = idx; }",
6794
- bool: "if (v < best_v) { best_v = v; best_i = idx; }",
6795
- 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; }' },
7215
+ best_i: "-1" },
7216
+ reduce: { numeric: "if (v < best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7217
+ bool: "if (v < best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7218
+ object: 'if (!CA_OBJ_ISNAN(v) && (best_v == Qundef || ca_obj_cmp(v, best_v) < 0)) { best_v = v; best_i = idx; }' },
6796
7219
  finish: "best_i",
6797
7220
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
6798
7221
  output: :i64,
@@ -6801,15 +7224,16 @@ MkKernel.reduce :argmin_addr,
6801
7224
  mask_policy: :min_count,
6802
7225
  semantics: :view_flat,
6803
7226
  face_gate: :strip,
7227
+ all_nan_result: :undef,
6804
7228
  public_method: :min_addr
6805
7229
 
6806
7230
  MkKernel.reduce :argmax_addr,
6807
7231
  state: { best_v: :load_type, best_i: :int64_t },
6808
7232
  init: { best_v: { numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qundef" },
6809
- best_i: "0" },
6810
- reduce: { numeric: "if (v > best_v) { best_v = v; best_i = idx; }",
6811
- bool: "if (v > best_v) { best_v = v; best_i = idx; }",
6812
- 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; }' },
7233
+ best_i: "-1" },
7234
+ reduce: { numeric: "if (v > best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7235
+ bool: "if (v > best_v || (best_i < 0 && v == v)) { best_v = v; best_i = idx; }",
7236
+ object: 'if (!CA_OBJ_ISNAN(v) && (best_v == Qundef || ca_obj_cmp(v, best_v) > 0)) { best_v = v; best_i = idx; }' },
6813
7237
  finish: "best_i",
6814
7238
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
6815
7239
  output: :i64,
@@ -6818,38 +7242,9 @@ MkKernel.reduce :argmax_addr,
6818
7242
  mask_policy: :min_count,
6819
7243
  semantics: :view_flat,
6820
7244
  face_gate: :strip,
7245
+ all_nan_result: :undef,
6821
7246
  public_method: :max_addr
6822
7247
 
6823
- # ---- mask_policy demos ------------------------------------------------
6824
-
6825
- # sum_strict_ki -- :strict policy. Any masked input cell makes the
6826
- # output cell UNDEF. Use case: "this is sensor data, any gap invalidates
6827
- # the aggregate" (= NaN-propagation semantics). Sibling of
6828
- # sum_ki which silently treats masked as 0.
6829
- MkKernel.reduce :sum_strict,
6830
- init: "0",
6831
- reduce: "acc += v",
6832
- source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
6833
- output: { numeric: :f64, complex: :cmplx128 },
6834
- ruby_scalar: :auto,
6835
- fallback: :raise,
6836
- mask_policy: :strict
6837
-
6838
- # mean_safe_ki -- :all_masked policy. Output UNDEF only when every
6839
- # input cell is masked, otherwise mean over visible cells. Use case:
6840
- # "tolerate gaps but flag if nothing is observed". Sibling of mean_ki
6841
- # which returns 0.0 for all-masked slabs.
6842
- MkKernel.reduce :mean_safe,
6843
- state: { acc: :acc_type, cnt: :int64_t },
6844
- init: { acc: "0", cnt: "0" },
6845
- reduce: "(acc += v, cnt++)",
6846
- finish: "cnt ? acc / (T_OUT) cnt : 0",
6847
- source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
6848
- output: { numeric: :f64, complex: :cmplx128 },
6849
- ruby_scalar: :auto,
6850
- fallback: :raise,
6851
- mask_policy: :all_masked
6852
-
6853
7248
  # accumulate -- sum that preserves input data_type (legacy rb_ca_accum,
6854
7249
  # retired in E.7). Same machinery as sum_ki but output: :preserve so
6855
7250
  # int8 stays int8 (will overflow on long arrays = legacy behaviour).
@@ -6945,9 +7340,14 @@ MkKernel.reduce :count_false,
6945
7340
 
6946
7341
  MkKernel.reduce :count_equal,
6947
7342
  init: "0",
6948
- reduce: "if (v == value_arg) acc += 1",
7343
+ # The object lane compares with ca_obj_equal (= Ruby ==, a NaN equal to
7344
+ # nothing), not the C ==, which on two VALUEs would ask whether they are
7345
+ # the same object.
7346
+ reduce: { numeric: "if (v == value_arg) acc += 1",
7347
+ object: "if (ca_obj_equal(v, value_arg)) acc += 1" },
6949
7348
  reduction_kind: :plus, # SL.1.4 (conditional predication; clang predicates safely under reduction(+:acc))
6950
- source: MkKernel::ALL_NUMERIC,
7349
+ source: MkKernel::ALL_NUMERIC + [:object, :fixlen],
7350
+ fixlen: :count_equal, # bespoke memcmp walk (a blob has no scalar C type)
6951
7351
  output: :i64,
6952
7352
  ruby_scalar: :LL2NUM,
6953
7353
  fallback: :raise,
@@ -7036,69 +7436,6 @@ MkKernel.reduce :wmean,
7036
7436
  bind_ruby: true,
7037
7437
  public_method: true
7038
7438
 
7039
- # ---- map kernels (element-wise transforms) ----------------------------
7040
-
7041
- # Float-output transcendentals -- input widens to double inside the
7042
- # expression, output is float64. The :wrap_to_f64 fallback handles
7043
- # any other numeric data_type (int16, bool, etc.) by promoting to f64 first.
7044
-
7045
- MkKernel.map :sqrt,
7046
- source: MkKernel::ALL_NUMERIC,
7047
- output: :f64,
7048
- expr: "r = sqrt((double) v)",
7049
- fallback: :wrap_to_f64
7050
-
7051
- MkKernel.map :sin,
7052
- source: MkKernel::ALL_NUMERIC,
7053
- output: :f64,
7054
- expr: "r = sin((double) v)",
7055
- fallback: :wrap_to_f64
7056
-
7057
- MkKernel.map :cos,
7058
- source: MkKernel::ALL_NUMERIC,
7059
- output: :f64,
7060
- expr: "r = cos((double) v)",
7061
- fallback: :wrap_to_f64
7062
-
7063
- MkKernel.map :exp,
7064
- source: MkKernel::ALL_NUMERIC,
7065
- output: :f64,
7066
- expr: "r = exp((double) v)",
7067
- fallback: :wrap_to_f64
7068
-
7069
- MkKernel.map :log,
7070
- source: MkKernel::ALL_NUMERIC,
7071
- output: :f64,
7072
- expr: "r = log((double) v)",
7073
- fallback: :wrap_to_f64
7074
-
7075
- # Source-data_type-preserving transforms -- arithmetic ops that don't need
7076
- # floating-point. Fallback raises rather than silently widening.
7077
-
7078
- MkKernel.map :square,
7079
- source: MkKernel::ALL_NUMERIC,
7080
- output: :preserve,
7081
- expr: "r = v * v",
7082
- fallback: :raise
7083
-
7084
- MkKernel.map :abs,
7085
- # Signed-only: `v < 0` is always false on unsigned types (compiler
7086
- # warning + identity result). Use `negate` semantics for unsigned
7087
- # only if you genuinely want wrap-around.
7088
- source: MkKernel::SIGNED_NUMERIC,
7089
- output: :preserve,
7090
- expr: "r = (v < 0) ? -v : v",
7091
- fallback: :raise
7092
-
7093
- MkKernel.map :negate,
7094
- # Signed-only: -v on unsigned types wraps (C semantics), which is
7095
- # mathematically wrong for "negate". If you want bitwise inversion
7096
- # on unsigned, use a different kernel (~v).
7097
- source: MkKernel::SIGNED_NUMERIC,
7098
- output: :preserve,
7099
- expr: "r = -v",
7100
- fallback: :raise
7101
-
7102
7439
  # ---- scan kernels (cumulative / prefix scan along one axis) ----------
7103
7440
 
7104
7441
  # cumsum / cumprod: data_type-conditional output (numeric -> f64 widening,
@@ -7140,37 +7477,58 @@ MkKernel.scan :cumprod,
7140
7477
  # init Qnil is the "no running extremum yet" sentinel, also never leaked
7141
7478
  # (unseen cells are masked). First unmasked cell adopts v as acc;
7142
7479
  # subsequent unmasked cells compare via rb_funcall(:>) / rb_funcall(:<).
7480
+ # The float lane starts at NaN and folds with C99 fmin / fmax, whose
7481
+ # rule is exactly the one wanted: a lone number beats NaN, two NaNs give
7482
+ # NaN. So a prefix that has seen only NaN answers NaN instead of leaking
7483
+ # +/-INFINITY -- the running form of the rule min / max follow
7484
+ # (all_nan_result:). fmin / fmax are exact on a float at either width,
7485
+ # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7486
+ # and keep the limit init and the plain compare.
7143
7487
  MkKernel.scan :cummax,
7488
+ face_gate: :relift,
7144
7489
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7145
7490
  output: { bool: :u64, default: :preserve },
7146
- init: { numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qnil" },
7147
- step: { numeric: "if (v > acc) acc = v; r = acc",
7491
+ init: { float: "NAN", numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qnil" },
7492
+ step: { float: "acc = fmax(acc, v); r = acc",
7493
+ numeric: "if (v > acc) acc = v; r = acc",
7148
7494
  bool: "if ((uint64_t) v > acc) acc = v; r = acc",
7149
- object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v; r = acc' },
7495
+ object: 'if (acc == Qnil || ca_obj_extremum_replaces(v, acc, rb_intern(">"))) acc = v; r = acc' },
7150
7496
  fallback: :raise,
7151
7497
  axis_default: :flatten,
7152
7498
  empty: :undef
7153
7499
 
7500
+ # The float lane starts at NaN and folds with C99 fmin / fmax, whose
7501
+ # rule is exactly the one wanted: a lone number beats NaN, two NaNs give
7502
+ # NaN. So a prefix that has seen only NaN answers NaN instead of leaking
7503
+ # +/-INFINITY -- the running form of the rule min / max follow
7504
+ # (all_nan_result:). fmin / fmax are exact on a float at either width,
7505
+ # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7506
+ # and keep the limit init and the plain compare.
7154
7507
  MkKernel.scan :cummin,
7508
+ face_gate: :relift,
7155
7509
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7156
7510
  output: { bool: :u64, default: :preserve },
7157
- init: { numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qnil" },
7158
- step: { numeric: "if (v < acc) acc = v; r = acc",
7511
+ init: { float: "NAN", numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qnil" },
7512
+ step: { float: "acc = fmin(acc, v); r = acc",
7513
+ numeric: "if (v < acc) acc = v; r = acc",
7159
7514
  bool: "if ((uint64_t) v < acc) acc = v; r = acc",
7160
- object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v; r = acc' },
7515
+ object: 'if (acc == Qnil || ca_obj_extremum_replaces(v, acc, rb_intern("<"))) acc = v; r = acc' },
7161
7516
  fallback: :raise,
7162
7517
  axis_default: :flatten,
7163
7518
  empty: :undef
7164
7519
 
7165
7520
  # cumcount: running count of unmasked cells. Output data_type = int64.
7166
7521
  # `(void) v` silences unused-variable warning for the unmasked path
7167
- # (compiler DCEs the v load when STEP doesn't reference it).
7522
+ # (compiler DCEs the v load when STEP doesn't reference it). The count
7523
+ # reads only the mask, so a data type outside source: (object, complex,
7524
+ # boolean, fixlen, a Face) is counted off the mask without converting its
7525
+ # values (fallback: :mask_count).
7168
7526
  MkKernel.scan :cumcount,
7169
7527
  source: MkKernel::ALL_NUMERIC,
7170
7528
  output: :i64,
7171
7529
  init: "0",
7172
7530
  step: "(void) v; r = ++acc",
7173
- fallback: :wrap_to_f64,
7531
+ fallback: :mask_count,
7174
7532
  axis_default: :flatten
7175
7533
 
7176
7534
  # uniq_scan: per-axis adjacent-compare scan returning a boolean
@@ -7218,7 +7576,7 @@ MkKernel.sort :sort_index,
7218
7576
  nan_policy: :end,
7219
7577
  fallback: :raise,
7220
7578
  mask_self: :sentinel, # masked cells sort to masked_position: :first/:last (default :last)
7221
- public_method: :sort_index, # binds public sort_index(axis: 0, masked_position: :last) via _kw trampoline
7579
+ public_method: :sort_index, # binds public sort_index(axis: nil, masked_position: :last) via _kw trampoline
7222
7580
  c_callable: true # extern linkage for carray_order.c (nlargest / order family)
7223
7581
 
7224
7582
  # sort_addr: per-axis argsort returning view-flat addresses (=
@@ -7226,7 +7584,7 @@ MkKernel.sort :sort_index,
7226
7584
  # sort_index but the kernel writes view-flat addresses instead of
7227
7585
  # fiber-local indices, so the output can feed directly into
7228
7586
  # ca_remap_new as a CARemap.idx. Internal-only (bind_ruby: false):
7229
- # used by `a.sort(axis: k)` at the C level via rb_ca_sort_addr_ki.
7587
+ # used by `a.sort(axis: k)` at the C level via rb_ca_sort_addr_ki_*_mp.
7230
7588
  # rev3 of PROPOSAL_SORT_AXIS defers `sort_addr(axis: k)` as a public
7231
7589
  # method to a future phase; here we just need the kernel machinery.
7232
7590
  MkKernel.sort :sort_addr,
@@ -7253,7 +7611,7 @@ MkKernel.sort :partition_index,
7253
7611
  fallback: :raise,
7254
7612
  algorithm: :partition,
7255
7613
  mask_self: :sentinel, # masked cells sort to masked_position: :first/:last (default :last)
7256
- public_method: :partition_index, # binds partition_index(kth, axis: 0, masked_position: :last) via _kw trampoline
7614
+ public_method: :partition_index, # binds partition_index(kth, axis: nil, masked_position: :last) via _kw trampoline
7257
7615
  c_callable: true # extern linkage for carray_order.c (nlargest / nsmallest)
7258
7616
 
7259
7617
  # partition_addr: view-flat-address variant of partition_index, parallel
@@ -7295,7 +7653,7 @@ MkKernel.sort :rank_index,
7295
7653
  fallback: :raise,
7296
7654
  algorithm: :rank,
7297
7655
  mask_self: :skip,
7298
- public_method: :rank_index, # binds public rank_index(axis: 0) via _kw trampoline
7656
+ public_method: :rank_index, # binds public rank_index(axis: nil) via _kw trampoline
7299
7657
  c_callable: true # extern linkage for carray_order.c (order method)
7300
7658
 
7301
7659
  # ---- search family ------------------------------------------------------
@@ -7332,7 +7690,7 @@ MkKernel.search :find_value_index,
7332
7690
  result = (ca_size_t) -1;
7333
7691
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7334
7692
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7335
- if ( rb_equal(v, query_val) ) { result = i; break; }
7693
+ if ( ca_obj_equal(v, query_val) ) { result = i; break; }
7336
7694
  }
7337
7695
  C
7338
7696
  fixlen: <<~C,
@@ -7398,19 +7756,18 @@ MkKernel.search :bsearch,
7398
7756
  C
7399
7757
  object: <<~C,
7400
7758
  /* CA_OBJECT bsearch: ordering via rb_funcall(<=>). Assumes the
7401
- slab is sorted by the same comparator used to build it. NaN
7402
- guards skipped (= no float-NaN concept for arbitrary objects;
7403
- a Float-cell NaN would raise from <=>). */
7759
+ slab is sorted by the same comparator used to build it, which puts
7760
+ a Float NaN last; a NaN query matches nothing, as in float. */
7404
7761
  result = (ca_size_t) -1;
7405
7762
  ca_size_t lo = 0;
7406
- ca_size_t hi = slab_n;
7763
+ ca_size_t hi = CA_OBJ_ISNAN(query_val) ? 0 : slab_n; /* NaN matches nothing */
7407
7764
  while ( lo < hi ) {
7408
7765
  ca_size_t mid = lo + (hi - lo) / 2;
7409
7766
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7767
+ if ( CA_OBJ_ISNAN(v) ) { hi = mid; continue; } /* NaN sorts last */
7410
7768
  VALUE r = rb_funcall(v, rb_intern("<=>"), 1, query_val);
7411
7769
  if ( NIL_P(r) ) {
7412
- rb_raise(rb_eArgError, "bsearch: comparison of %s with %s failed",
7413
- rb_obj_classname(v), rb_obj_classname(query_val));
7770
+ ca_obj_compare_refused(v, query_val);
7414
7771
  }
7415
7772
  int c = NUM2INT(r);
7416
7773
  if ( c < 0 ) lo = mid + 1;
@@ -7447,7 +7804,7 @@ MkKernel.search :bsearch,
7447
7804
  #
7448
7805
  # Per-data_type body: int uses exact `v == query_val` (eps ignored); float
7449
7806
  # uses `fabs(v - query_val) <= query_eps` (eps_default = FLT_EPSILON*|val|
7450
- # or DBL_EPSILON*|val|); object uses rb_equal (eps ignored); fixlen uses
7807
+ # or DBL_EPSILON*|val|); object uses ca_obj_equal (eps ignored); fixlen uses
7451
7808
  # memcmp exact match (eps ignored). Sole search implementation: the
7452
7809
  # no-axis (flat) surface routes here via flatten + axis 0
7453
7810
  # (PROPOSAL_SEARCH_SEMANTICS_UNIFY S2/S3; the legacy flat scan was removed).
@@ -7469,18 +7826,18 @@ MkKernel.search :search,
7469
7826
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7470
7827
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7471
7828
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7472
- if ( fabs((double)(v - query_val)) <= query_eps ) { result = i; break; }
7829
+ if ( v == query_val || fabs((double)(v - query_val)) <= query_eps ) { result = i; break; }
7473
7830
  }
7474
7831
  C
7475
7832
  object: <<~C,
7476
- /* CA_OBJECT search: exact equality via rb_equal. eps is silently
7833
+ /* CA_OBJECT search: exact equality via ca_obj_equal. eps is silently
7477
7834
  ignored (= no meaningful tolerance on arbitrary Ruby objects). */
7478
7835
  result = (ca_size_t) -1;
7479
7836
  (void) query_eps;
7480
7837
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7481
7838
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7482
7839
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7483
- if ( rb_equal(v, query_val) ) { result = i; break; }
7840
+ if ( ca_obj_equal(v, query_val) ) { result = i; break; }
7484
7841
  }
7485
7842
  C
7486
7843
  fixlen: <<~C,
@@ -7519,13 +7876,16 @@ MkKernel.search :search_nearest,
7519
7876
  output: :ca_size,
7520
7877
  body: {
7521
7878
  int: <<~C,
7879
+ /* The distance is exact in uint64: a double loses the difference
7880
+ between neighbouring int64 values above 2**53. */
7522
7881
  result = (ca_size_t) -1;
7523
- double best_dist = INFINITY;
7882
+ uint64_t best_dist = 0;
7524
7883
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7525
7884
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7526
7885
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7527
- double dist = fabs((double)v - (double)query_val);
7528
- if ( dist < best_dist ) { best_dist = dist; result = i; }
7886
+ uint64_t dist = ( v >= query_val ) ? (uint64_t) v - (uint64_t) query_val
7887
+ : (uint64_t) query_val - (uint64_t) v;
7888
+ if ( result == (ca_size_t) -1 || dist < best_dist ) { best_dist = dist; result = i; }
7529
7889
  }
7530
7890
  (void) query_eps; /* unused: search_nearest has no eps semantics */
7531
7891
  C
@@ -7535,20 +7895,45 @@ MkKernel.search :search_nearest,
7535
7895
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7536
7896
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7537
7897
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7538
- double dist = fabs((double)v - (double)query_val);
7539
- if ( dist < best_dist ) { best_dist = dist; result = i; }
7898
+ /* An equal value is at distance 0 even when both are infinite
7899
+ (Inf - Inf is NaN); a NaN distance never wins. */
7900
+ double dist = ( v == query_val ) ? 0.0 : fabs((double)v - (double)query_val);
7901
+ if ( dist != dist ) continue;
7902
+ if ( result == (ca_size_t) -1 || dist < best_dist ) { best_dist = dist; result = i; }
7540
7903
  }
7541
7904
  (void) query_eps; /* unused: search_nearest has no eps semantics */
7542
7905
  C
7543
7906
  object: <<~C,
7544
7907
  /* CA_OBJECT nearest: minimum of query_val.distance(cell), compared
7545
7908
  with `<` (matches the legacy flat proc_nearest_addr_VALUE). */
7909
+ /* Nearest needs a metric. #distance is the protocol the 2.0 flat
7910
+ kernel used, back when Numeric#distance was a monkey patch; it
7911
+ is an opt-in refinement now, and a refinement does not reach an
7912
+ rb_funcall from C -- so a number reaching here answers no more
7913
+ than a String does. Measure a number the way #distance itself
7914
+ does, keep #distance for anything that defines a real one, and
7915
+ refuse the rest by name rather than let a bare NoMethodError out
7916
+ of the loop below. */
7917
+ ID nearest_id = rb_intern("distance");
7918
+ int nearest_by_distance = rb_respond_to(query_val, nearest_id);
7919
+ if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
7920
+ rb_raise(rb_eCADataTypeError,
7921
+ "%s: nearest needs a distance, and %s is neither a "
7922
+ "number nor answers #distance (define one on the stored "
7923
+ "objects, or use search / bsearch for an exact match)",
7924
+ ca_calling_method_name(), rb_obj_classname(query_val));
7925
+ }
7546
7926
  result = (ca_size_t) -1;
7547
7927
  VALUE best = Qnil;
7548
7928
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7549
7929
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7550
7930
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7551
- VALUE dist = rb_funcall(query_val, rb_intern("distance"), 1, v);
7931
+ VALUE dist = ca_obj_equal(v, query_val) ? INT2FIX(0)
7932
+ : nearest_by_distance
7933
+ ? rb_funcall(query_val, nearest_id, 1, v)
7934
+ : rb_funcall(rb_funcall(query_val, '-', 1, v),
7935
+ rb_intern("abs"), 0);
7936
+ if ( CA_OBJ_ISNAN(dist) ) continue; /* a NaN is no distance, as in float */
7552
7937
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
7553
7938
  best = dist; result = i;
7554
7939
  }
@@ -7611,14 +7996,14 @@ MkKernel.search :bsearch_addr,
7611
7996
  object: <<~C,
7612
7997
  result = (ca_size_t) -1;
7613
7998
  ca_size_t lo = 0;
7614
- ca_size_t hi = slab_n;
7999
+ ca_size_t hi = CA_OBJ_ISNAN(query_val) ? 0 : slab_n; /* NaN matches nothing */
7615
8000
  while ( lo < hi ) {
7616
8001
  ca_size_t mid = lo + (hi - lo) / 2;
7617
8002
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
8003
+ if ( CA_OBJ_ISNAN(v) ) { hi = mid; continue; } /* NaN sorts last */
7618
8004
  VALUE r = rb_funcall(v, rb_intern("<=>"), 1, query_val);
7619
8005
  if ( NIL_P(r) ) {
7620
- rb_raise(rb_eArgError, "bsearch_addr: comparison of %s with %s failed",
7621
- rb_obj_classname(v), rb_obj_classname(query_val));
8006
+ ca_obj_compare_refused(v, query_val);
7622
8007
  }
7623
8008
  int c = NUM2INT(r);
7624
8009
  if ( c < 0 ) lo = mid + 1;
@@ -7665,7 +8050,7 @@ MkKernel.search :search_addr,
7665
8050
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7666
8051
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7667
8052
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7668
- if ( fabs((double)v - (double)query_val) <= query_eps ) { result = i; break; }
8053
+ if ( v == query_val || fabs((double)v - (double)query_val) <= query_eps ) { result = i; break; }
7669
8054
  }
7670
8055
  C
7671
8056
  object: <<~C,
@@ -7674,7 +8059,7 @@ MkKernel.search :search_addr,
7674
8059
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7675
8060
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7676
8061
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7677
- if ( rb_equal(v, query_val) ) { result = i; break; }
8062
+ if ( ca_obj_equal(v, query_val) ) { result = i; break; }
7678
8063
  }
7679
8064
  C
7680
8065
  fixlen: <<~C,
@@ -7700,13 +8085,16 @@ MkKernel.search :search_nearest_addr,
7700
8085
  output: :ca_size,
7701
8086
  body: {
7702
8087
  int: <<~C,
8088
+ /* The distance is exact in uint64: a double loses the difference
8089
+ between neighbouring int64 values above 2**53. */
7703
8090
  result = (ca_size_t) -1;
7704
- double best_dist = INFINITY;
8091
+ uint64_t best_dist = 0;
7705
8092
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7706
8093
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7707
8094
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7708
- double dist = fabs((double)v - (double)query_val);
7709
- if ( dist < best_dist ) { best_dist = dist; result = i; }
8095
+ uint64_t dist = ( v >= query_val ) ? (uint64_t) v - (uint64_t) query_val
8096
+ : (uint64_t) query_val - (uint64_t) v;
8097
+ if ( result == (ca_size_t) -1 || dist < best_dist ) { best_dist = dist; result = i; }
7710
8098
  }
7711
8099
  (void) query_eps;
7712
8100
  C
@@ -7716,20 +8104,45 @@ MkKernel.search :search_nearest_addr,
7716
8104
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7717
8105
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7718
8106
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7719
- double dist = fabs((double)v - (double)query_val);
7720
- if ( dist < best_dist ) { best_dist = dist; result = i; }
8107
+ /* An equal value is at distance 0 even when both are infinite
8108
+ (Inf - Inf is NaN); a NaN distance never wins. */
8109
+ double dist = ( v == query_val ) ? 0.0 : fabs((double)v - (double)query_val);
8110
+ if ( dist != dist ) continue;
8111
+ if ( result == (ca_size_t) -1 || dist < best_dist ) { best_dist = dist; result = i; }
7721
8112
  }
7722
8113
  (void) query_eps;
7723
8114
  C
7724
8115
  object: <<~C,
7725
8116
  /* CA_OBJECT nearest (view_flat addr): minimum of
7726
8117
  query_val.distance(cell), compared with `<`. */
8118
+ /* Nearest needs a metric. #distance is the protocol the 2.0 flat
8119
+ kernel used, back when Numeric#distance was a monkey patch; it
8120
+ is an opt-in refinement now, and a refinement does not reach an
8121
+ rb_funcall from C -- so a number reaching here answers no more
8122
+ than a String does. Measure a number the way #distance itself
8123
+ does, keep #distance for anything that defines a real one, and
8124
+ refuse the rest by name rather than let a bare NoMethodError out
8125
+ of the loop below. */
8126
+ ID nearest_id = rb_intern("distance");
8127
+ int nearest_by_distance = rb_respond_to(query_val, nearest_id);
8128
+ if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
8129
+ rb_raise(rb_eCADataTypeError,
8130
+ "%s: nearest needs a distance, and %s is neither a "
8131
+ "number nor answers #distance (define one on the stored "
8132
+ "objects, or use search / bsearch for an exact match)",
8133
+ ca_calling_method_name(), rb_obj_classname(query_val));
8134
+ }
7727
8135
  result = (ca_size_t) -1;
7728
8136
  VALUE best = Qnil;
7729
8137
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7730
8138
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7731
8139
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7732
- VALUE dist = rb_funcall(query_val, rb_intern("distance"), 1, v);
8140
+ VALUE dist = ca_obj_equal(v, query_val) ? INT2FIX(0)
8141
+ : nearest_by_distance
8142
+ ? rb_funcall(query_val, nearest_id, 1, v)
8143
+ : rb_funcall(rb_funcall(query_val, '-', 1, v),
8144
+ rb_intern("abs"), 0);
8145
+ if ( CA_OBJ_ISNAN(dist) ) continue; /* a NaN is no distance, as in float */
7733
8146
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
7734
8147
  best = dist; result = i;
7735
8148
  }
@@ -7779,7 +8192,11 @@ MkKernel.search :linear_section_binary,
7779
8192
  body: <<~C,
7780
8193
  result = NAN;
7781
8194
  if ( slab_n <= 1 ) {
7782
- result = 0.0;
8195
+ /* A one-point axis has no segment: only the point itself is in
8196
+ * range, at 0. An empty one has nothing in range. */
8197
+ if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8198
+ result = 0.0;
8199
+ }
7783
8200
  }
7784
8201
  else {
7785
8202
  double y0 = *(double *)(slab_ptr + 0 * slab_stride);
@@ -7867,7 +8284,11 @@ MkKernel.search :linear_section_linear,
7867
8284
  body: <<~C,
7868
8285
  result = NAN;
7869
8286
  if ( slab_n <= 1 ) {
7870
- result = 0.0;
8287
+ /* A one-point axis has no segment: only the point itself is in
8288
+ * range, at 0. An empty one has nothing in range. */
8289
+ if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8290
+ result = 0.0;
8291
+ }
7871
8292
  }
7872
8293
  else {
7873
8294
  ca_size_t x1 = 0;
@@ -7997,9 +8418,10 @@ MkKernel.monop :abs_i,
7997
8418
  # (= used by .real chain composition); abs is the user-facing op that
7998
8419
  # returns the real-valued magnitude entity.
7999
8420
  MkKernel.monop :abs,
8000
- source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
8001
- output: { numeric: :preserve, complex: :real_of_source },
8421
+ source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES + [:object],
8422
+ output: { numeric: :preserve, complex: :real_of_source, object: :preserve },
8002
8423
  expr: {
8424
+ [:object] => '(#2) = rb_funcall((#1), rb_intern("abs"), 0);',
8003
8425
  MkKernel::SINT_SMALL_DTYPES => "(#2) = abs(#1);",
8004
8426
  MkKernel::SINT64_DTYPES => "(#2) = llabs(#1);",
8005
8427
  MkKernel::UINT_DTYPES => "(#2) = (#1);",
@@ -8115,7 +8537,9 @@ MkKernel.monop :imag_i,
8115
8537
  MkKernel.monop :arg_i,
8116
8538
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES,
8117
8539
  expr: {
8118
- [:f32] => "(#2) = cargf((cmplx64_t)(#1));",
8540
+ # A real cell's argument is 0, pi or NaN. Taken in double and rounded,
8541
+ # as the eager arg does; cargf returns pi one ulp low.
8542
+ [:f32] => "(#2) = carg((cmplx128_t)(#1));",
8119
8543
  [:f64] => "(#2) = carg((cmplx128_t)(#1));",
8120
8544
  [:cmplx64] => "(#2) = cargf(#1);",
8121
8545
  [:cmplx128] => "(#2) = carg(#1);",
@@ -8171,7 +8595,9 @@ MkKernel.monfunc :round,
8171
8595
  MkKernel.monfunc :rcp,
8172
8596
  source: MkKernel::MATH_NUMERIC + [:object],
8173
8597
  expr: {
8174
- int: "if ((#1)==0) {ca_zerodiv();}; (#2) = 1/(#1);",
8598
+ MkKernel::UINT_DTYPES => "if ((#1)==0) {ca_zerodiv();}; (#2) = 1/(#1);",
8599
+ # 1 / x floored: 1 for 1, -1 for every negative x, 0 otherwise.
8600
+ int: "if ((#1)==0) {ca_zerodiv();}; (#2) = ((#1)==1) ? 1 : ((#1) < 0) ? -1 : 0;",
8175
8601
  float: "(#2) = 1/(#1);",
8176
8602
  [:cmplx64] => "(#2) = op_crcp_cmplx64(#1);",
8177
8603
  [:cmplx128] => "(#2) = 1/(#1);",
@@ -8280,11 +8706,13 @@ end
8280
8706
 
8281
8707
  # expm1, log1p: float + object only (C99 doesn't standardise complex
8282
8708
  # variants). Widening monfunc — integer input auto-casts to f64.
8709
+ # CAMath.expm1 / log1p are defined in lib/carray/math.rb instead: Ruby's
8710
+ # Math has neither, so the generated front-end could not take a Numeric.
8283
8711
  {
8284
8712
  expm1: "expm1",
8285
8713
  log1p: "log1p",
8286
8714
  }.each do |op, c_fn|
8287
- MkKernel.monfunc op,
8715
+ MkKernel.monfunc op, cmath: false,
8288
8716
  source: MkKernel::FLOAT_DTYPES + [:object],
8289
8717
  expr: {
8290
8718
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
@@ -8469,14 +8897,15 @@ MkKernel.monfunc :rad_pi,
8469
8897
  # NaN the result is NaN. Distinct kernel — cannot be
8470
8898
  # aliased onto pmax.
8471
8899
  #
8472
- # Integer / object branches are identical across all three (no NaN
8473
- # concept). Only the float branch differs.
8900
+ # Integer branches are identical across all three (no NaN). The float
8901
+ # and object branches differ: pmax / pmin let a NaN lose (an object cell's
8902
+ # Float NaN included), maximum / minimum let it win.
8474
8903
  MkKernel.binop :pmax,
8475
8904
  source: MkKernel::ALL_NUMERIC + [:object],
8476
8905
  expr: {
8477
8906
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8478
8907
  float: "(#3) = fmax(#1, #2);",
8479
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("max"), 0);',
8908
+ object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern(">")) ? (#2) : (#1);',
8480
8909
  }
8481
8910
 
8482
8911
  MkKernel.binop :pmin,
@@ -8484,26 +8913,28 @@ MkKernel.binop :pmin,
8484
8913
  expr: {
8485
8914
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8486
8915
  float: "(#3) = fmin(#1, #2);",
8487
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("min"), 0);',
8916
+ object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern("<")) ? (#2) : (#1);',
8488
8917
  }
8489
8918
 
8490
8919
  # NaN-propagate variants. Float
8491
8920
  # branch tests both operands for NaN and short-circuits to NaN; non-
8492
- # float branches reuse the comparison form.
8921
+ # float branches reuse the comparison form. Of two zeros, maximum is +0
8922
+ # and minimum is -0 in either order: a bare comparison would answer with
8923
+ # whichever came second.
8493
8924
  MkKernel.binop :maximum,
8494
8925
  source: MkKernel::ALL_NUMERIC + [:object],
8495
8926
  expr: {
8496
8927
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8497
- float: "(#3) = isnan(#1) ? (#1) : (isnan(#2) ? (#2) : ((#1) > (#2) ? (#1) : (#2)));",
8498
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("max"), 0);',
8928
+ float: "(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) > (#2) || ((#1) == (#2) && !signbit(#1))) ? (#1) : (#2);",
8929
+ object: '(#3) = CA_OBJ_ISNAN(#1) ? (#1) : CA_OBJ_ISNAN(#2) ? (#2) : ca_obj_cmp((#2), (#1)) > 0 ? (#2) : (#1);',
8499
8930
  }
8500
8931
 
8501
8932
  MkKernel.binop :minimum,
8502
8933
  source: MkKernel::ALL_NUMERIC + [:object],
8503
8934
  expr: {
8504
8935
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8505
- float: "(#3) = isnan(#1) ? (#1) : (isnan(#2) ? (#2) : ((#1) < (#2) ? (#1) : (#2)));",
8506
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("min"), 0);',
8936
+ float: "(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) < (#2) || ((#1) == (#2) && signbit(#1))) ? (#1) : (#2);",
8937
+ object: '(#3) = CA_OBJ_ISNAN(#1) ? (#1) : CA_OBJ_ISNAN(#2) ? (#2) : ca_obj_cmp((#2), (#1)) < 0 ? (#2) : (#1);',
8507
8938
  }
8508
8939
 
8509
8940
  # + and - work on the parts independently, so one generic expression
@@ -8544,6 +8975,11 @@ MkKernel.binop :mul,
8544
8975
  # sign, so they keep the bare C form. Float `/` is true division and is
8545
8976
  # unchanged (matching Ruby `Float#/`); the identity above therefore holds
8546
8977
  # for integers only, exactly as in Ruby.
8978
+ #
8979
+ # A signed divisor of -1 is taken apart from C's `/`: MIN / -1 does not
8980
+ # fit, and the hardware's answer to it differs (x86 traps, ARM returns
8981
+ # MIN). The quotient is the negation wrapped like any other overflow, so
8982
+ # MIN / -1 is MIN and MIN % -1 is 0 on every machine.
8547
8983
  MkKernel.binop :div,
8548
8984
  op: "/",
8549
8985
  source: MkKernel::MATH_NUMERIC + [:object],
@@ -8551,7 +8987,8 @@ MkKernel.binop :div,
8551
8987
  MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) / (#2);",
8552
8988
  int: %{
8553
8989
  if ((#2)==0) {ca_zerodiv();};
8554
- {
8990
+ if ((#2)==-1) { (#3) = (<type>) (0 - (uint64_t) (#1)); }
8991
+ else {
8555
8992
  <type> _q = (#1) / (#2);
8556
8993
  if ( (#1) % (#2) != 0 && (((#1) < 0) != ((#2) < 0)) ) { _q -= 1; }
8557
8994
  (#3) = _q;
@@ -8577,11 +9014,21 @@ MkKernel.binop :quo_i,
8577
9014
  object: '(#3) = rb_funcall((#1), rb_intern("quo"), 1, (#2));',
8578
9015
  }
8579
9016
 
9017
+ # rcp_mul: `a.rcp_mul(b)` is `b / a`, and divides as `/` does.
8580
9018
  MkKernel.binop :rcp_mul,
8581
9019
  op: "rcp_mul",
8582
9020
  source: MkKernel::MATH_NUMERIC + [:object],
8583
9021
  expr: {
8584
- int: "if ((#1)==0) {ca_zerodiv();}; (#3) = (#2) / (#1);",
9022
+ MkKernel::UINT_DTYPES => "if ((#1)==0) {ca_zerodiv();}; (#3) = (#2) / (#1);",
9023
+ int: %{
9024
+ if ((#1)==0) {ca_zerodiv();};
9025
+ if ((#1)==-1) { (#3) = (<type>) (0 - (uint64_t) (#2)); }
9026
+ else {
9027
+ <type> _q = (#2) / (#1);
9028
+ if ( (#2) % (#1) != 0 && (((#2) < 0) != ((#1) < 0)) ) { _q -= 1; }
9029
+ (#3) = _q;
9030
+ }
9031
+ },
8585
9032
  float: "(#3) = (#2) / (#1);",
8586
9033
  [:cmplx64] => "(#3) = op_cdiv_cmplx64((#2), (#1));",
8587
9034
  [:cmplx128] => "(#3) = (#2) / (#1);",
@@ -8603,7 +9050,8 @@ MkKernel.binop :mod,
8603
9050
  MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
8604
9051
  int: %{
8605
9052
  if ((#2)==0) {ca_zerodiv();};
8606
- {
9053
+ if ((#2)==-1) { (#3) = 0; }
9054
+ else {
8607
9055
  <type> _r = (#1) % (#2);
8608
9056
  if ( _r != 0 && ((_r < 0) != ((#2) < 0)) ) { _r += (#2); }
8609
9057
  (#3) = _r;
@@ -8631,7 +9079,8 @@ MkKernel.binop :mod,
8631
9079
  MkKernel.binop :fmod,
8632
9080
  source: MkKernel::ALL_NUMERIC + [:object],
8633
9081
  expr: {
8634
- int: "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
9082
+ MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
9083
+ int: "if ((#2)==0) {ca_zerodiv();}; (#3) = ((#2)==-1) ? 0 : (#1) % (#2);",
8635
9084
  object: '(#3) = rb_funcall((#1), rb_intern("remainder"), 1, (#2));',
8636
9085
  }.merge(MkKernel.float_widths("(#3) = fmod<f>((#1), (#2));"))
8637
9086
 
@@ -8664,11 +9113,29 @@ MkKernel.binop :bit_xor_i,
8664
9113
  object: '(#3) = rb_funcall((#1), rb_intern("^"), 1, (#2));',
8665
9114
  }
8666
9115
 
9116
+ # Shifts take the meaning Integer#<< and #>> give them: a negative count
9117
+ # shifts the other way, and a count of the width or more shifts every bit
9118
+ # out (0, or -1 for a negative value shifted right). C leaves both, and a
9119
+ # left shift of a negative value, undefined. The value is widened to 64
9120
+ # bits and shifted there -- unsigned for a left shift -- and the result
9121
+ # narrowed back, which keeps the low bits a shift within the width gives.
9122
+ # The bodies are self-contained, since carray-jit compiles them as they are.
8667
9123
  MkKernel.binop :bit_lshift,
8668
9124
  op: "<<",
8669
9125
  source: MkKernel::INT_DTYPES + [:object],
8670
9126
  expr: {
8671
- int: "(#3) = (#1) << (#2);",
9127
+ MkKernel::UINT_DTYPES => "(#3) = ((uint64_t) (#2) >= 8 * sizeof(<type>)) ? 0 : (<type>) ((uint64_t) (#1) << (#2));",
9128
+ int: %{
9129
+ {
9130
+ int64_t _x = (int64_t) (#1), _n = (int64_t) (#2), _r;
9131
+ const int64_t _w = (int64_t) (8 * sizeof(<type>));
9132
+ if ( _n >= _w ) _r = 0;
9133
+ else if ( _n <= -_w ) _r = _x < 0 ? -1 : 0;
9134
+ else if ( _n < 0 ) _r = _x >> -_n;
9135
+ else _r = (int64_t) ((uint64_t) _x << _n);
9136
+ (#3) = (<type>) _r;
9137
+ }
9138
+ },
8672
9139
  object: '(#3) = rb_funcall((#1), rb_intern("<<"), 1, (#2));',
8673
9140
  }
8674
9141
 
@@ -8676,7 +9143,18 @@ MkKernel.binop :bit_rshift,
8676
9143
  op: ">>",
8677
9144
  source: MkKernel::INT_DTYPES + [:object],
8678
9145
  expr: {
8679
- int: "(#3) = (#1) >> (#2);",
9146
+ MkKernel::UINT_DTYPES => "(#3) = ((uint64_t) (#2) >= 8 * sizeof(<type>)) ? 0 : (<type>) ((uint64_t) (#1) >> (#2));",
9147
+ int: %{
9148
+ {
9149
+ int64_t _x = (int64_t) (#1), _n = (int64_t) (#2), _r;
9150
+ const int64_t _w = (int64_t) (8 * sizeof(<type>));
9151
+ if ( _n >= _w ) _r = _x < 0 ? -1 : 0;
9152
+ else if ( _n <= -_w ) _r = 0;
9153
+ else if ( _n < 0 ) _r = (int64_t) ((uint64_t) _x << -_n);
9154
+ else _r = _x >> _n;
9155
+ (#3) = (<type>) _r;
9156
+ }
9157
+ },
8680
9158
  object: '(#3) = rb_funcall((#1), rb_intern(">>"), 1, (#2));',
8681
9159
  }
8682
9160
 
@@ -8746,16 +9224,16 @@ C
8746
9224
  # fma: fused multiply-add — `self * op2 + op3` in a single rounding step
8747
9225
  # (= C99 `fma(x, y, z)` and HW FMA3/FMA4 / ARM FMA instructions). More
8748
9226
  # accurate than the equivalent two-op `a*b + c` expression which rounds
8749
- # twice. Used in Horner evaluation, linear interpolation, dot product,
9227
+ # twice. float32 calls fmaf: fma in double followed by a narrowing is a
9228
+ # second rounding, and can land 1 ulp off. Used in Horner evaluation, linear interpolation, dot product,
8750
9229
  # Newton iteration.
8751
9230
  MkKernel.triop :fma,
8752
9231
  source: MkKernel::MATH_NUMERIC + [:object],
8753
9232
  expr: {
8754
9233
  int: "(#4) = (#1) * (#2) + (#3);",
8755
- float: "(#4) = fma((#1), (#2), (#3));",
8756
9234
  complex: "(#4) = (#1) * (#2) + (#3);",
8757
9235
  object: '(#4) = rb_funcall(rb_funcall((#1), rb_intern("*"), 1, (#2)), rb_intern("+"), 1, (#3));',
8758
- }
9236
+ }.merge(MkKernel.float_widths("(#4) = fma<f>((#1), (#2), (#3));"))
8759
9237
 
8760
9238
  # fms: fused multiply-subtract — `self * op2 - op3` in a single rounding.
8761
9239
  # Useful in cross products and complex multiplication primitives.
@@ -8763,11 +9241,10 @@ MkKernel.triop :fms,
8763
9241
  source: MkKernel::MATH_NUMERIC + [:object],
8764
9242
  expr: {
8765
9243
  int: "(#4) = (#1) * (#2) - (#3);",
8766
- # fma(a, b, -c) = a*b - c with single rounding (= C99 / IEEE 754 hint).
8767
- float: "(#4) = fma((#1), (#2), -(#3));",
8768
9244
  complex: "(#4) = (#1) * (#2) - (#3);",
8769
9245
  object: '(#4) = rb_funcall(rb_funcall((#1), rb_intern("*"), 1, (#2)), rb_intern("-"), 1, (#3));',
8770
- }
9246
+ # fma(a, b, -c) = a*b - c with single rounding (= C99 / IEEE 754 hint).
9247
+ }.merge(MkKernel.float_widths("(#4) = fma<f>((#1), (#2), -(#3));"))
8771
9248
 
8772
9249
  # clip: per-element clamp into [lo, hi]. 3.0 migration: was a hand-
8773
9250
  # written eager-only method in ext/carray_generate.c that only accepted
@@ -8806,6 +9283,20 @@ MkKernel.triop :clip,
8806
9283
  '(#4) = _v; }',
8807
9284
  }
8808
9285
 
9286
+ # select: the per-cell choice a lazy `then_else` makes (#1 is the
9287
+ # condition, converted to the branches' common type; #2 / #3 are the
9288
+ # branches). No Ruby method: CArray#then_else builds the node.
9289
+ MkKernel.triop :select,
9290
+ bind: false,
9291
+ bang: false,
9292
+ source: MkKernel::MATH_NUMERIC + [:bool],
9293
+ expr: {
9294
+ int: "(#4) = (#1) ? (#2) : (#3);",
9295
+ float: "(#4) = (#1) ? (#2) : (#3);",
9296
+ complex: "(#4) = (#1) ? (#2) : (#3);",
9297
+ bool: "(#4) = (#1) ? (#2) : (#3);",
9298
+ }
9299
+
8809
9300
  MkKernel.binop :power,
8810
9301
  op: :power,
8811
9302
  source: MkKernel::MATH_NUMERIC + [:object],
@@ -8883,7 +9374,11 @@ MkKernel.moncmp :is_nan,
8883
9374
  int: "(#2) = 0;",
8884
9375
  float: "(#2) = isnan(#1);",
8885
9376
  complex: "(#2) = (isnan(creal(#1)) || isnan(cimag(#1)));",
8886
- object: '(#2) = rb_funcall((#1), rb_intern("nan?"), 0);',
9377
+ # A Float answers by its bits; an Integer or a Rational is never NaN;
9378
+ # anything else answers through its own #nan?.
9379
+ object: '(#2) = RB_FLOAT_TYPE_P(#1) ? isnan(RFLOAT_VALUE(#1)) : '\
9380
+ '(RB_INTEGER_TYPE_P(#1) || RB_TYPE_P((#1), T_RATIONAL)) ? 0 : '\
9381
+ 'RTEST(rb_funcall((#1), rb_intern("nan?"), 0));',
8887
9382
  }
8888
9383
 
8889
9384
  MkKernel.moncmp :is_inf,
@@ -8892,7 +9387,7 @@ MkKernel.moncmp :is_inf,
8892
9387
  int: "(#2) = 0;",
8893
9388
  float: "(#2) = isinf(#1);",
8894
9389
  complex: "(#2) = (isinf(creal(#1)) || isinf(cimag(#1)));",
8895
- object: '(#2) = rb_funcall((#1), rb_intern("infinite?"), 0);',
9390
+ object: '(#2) = RTEST(rb_funcall((#1), rb_intern("infinite?"), 0));',
8896
9391
  }
8897
9392
 
8898
9393
  MkKernel.moncmp :is_finite,
@@ -8901,7 +9396,7 @@ MkKernel.moncmp :is_finite,
8901
9396
  int: "(#2) = 1;",
8902
9397
  float: "(#2) = isfinite(#1);",
8903
9398
  complex: "(#2) = (isfinite(creal(#1)) && isfinite(cimag(#1)));",
8904
- object: '(#2) = rb_funcall((#1), rb_intern("finite?"), 0);',
9399
+ object: '(#2) = RTEST(rb_funcall((#1), rb_intern("finite?"), 0));',
8905
9400
  }
8906
9401
 
8907
9402
  # is_invalid = !is_finite (MS.1 mask SET family redesign): NaN or Inf
@@ -8931,7 +9426,10 @@ MkKernel.moncmp :signbit,
8931
9426
  MkKernel::SINT_DTYPES => "(#2) = ((#1) < 0);",
8932
9427
  MkKernel::UINT_DTYPES => "(#2) = 0;",
8933
9428
  float: "(#2) = signbit(#1);",
8934
- object: '(#2) = (RTEST(rb_funcall((#1), rb_intern("negative?"), 0)) ? 1 : 0);',
9429
+ # A Float answers by its sign bit (-0.0 and -NaN are negative), as
9430
+ # the float branch does; anything else through #negative?.
9431
+ object: '(#2) = RB_FLOAT_TYPE_P(#1) ? (signbit(RFLOAT_VALUE(#1)) != 0) : '\
9432
+ '(RTEST(rb_funcall((#1), rb_intern("negative?"), 0)) ? 1 : 0);',
8935
9433
  }
8936
9434
 
8937
9435
  # ---- P.5b.4: bincmp family (predicates returning bool) ----------------