carray 3.0.2 → 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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +3 -4
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +225 -0
  6. data/ext/ca_axis_group.c +177 -99
  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 +66 -71
  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 +663 -214
  14. data/ext/ca_kernel_iterator.h +231 -167
  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 +132 -22
  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 +1 -0
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +96 -25
  25. data/ext/ca_obj_face.c +406 -64
  26. data/ext/ca_obj_face.h +92 -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 +21 -21
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +189 -107
  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 +543 -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 +47 -49
  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 -118
  46. data/ext/ca_obj_stride.c +195 -34
  47. data/ext/ca_obj_string.c +17 -18
  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 +149 -27
  54. data/ext/ca_op_ipower.c +24 -11
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_sort_kernels.h +70 -0
  58. data/ext/ca_sweep_engine.c +102 -79
  59. data/ext/ca_sweep_engine.h +7 -20
  60. data/ext/ca_triop_dispatch.c +4 -1
  61. data/ext/ca_triop_dispatch.h +5 -2
  62. data/ext/caframe_csv_reader.c +463 -0
  63. data/ext/carray.h +70 -27
  64. data/ext/carray_access.c +455 -211
  65. data/ext/carray_address_basis.c +177 -36
  66. data/ext/carray_attribute.c +45 -3
  67. data/ext/carray_bincount.c +32 -2
  68. data/ext/carray_broadcast.c +46 -14
  69. data/ext/carray_call_cfunc.c +0 -28
  70. data/ext/carray_cast.c +1444 -209
  71. data/ext/carray_cast_func.rb +34 -21
  72. data/ext/carray_conversion.c +197 -73
  73. data/ext/carray_copy.c +32 -11
  74. data/ext/carray_core.c +514 -122
  75. data/ext/carray_count.c +140 -87
  76. data/ext/carray_element.c +18 -27
  77. data/ext/carray_factorize.c +569 -277
  78. data/ext/carray_generate.c +76 -40
  79. data/ext/carray_histogram.c +152 -94
  80. data/ext/carray_hold.c +2 -1
  81. data/ext/carray_index_classifier.c +63 -96
  82. data/ext/carray_internal.h +385 -3
  83. data/ext/carray_lazy.c +834 -79
  84. data/ext/carray_loop.c +81 -65
  85. data/ext/carray_mask.c +682 -316
  86. data/ext/carray_math_kernel.h +1 -0
  87. data/ext/carray_median_percentile.c +221 -76
  88. data/ext/carray_memory_view.c +254 -248
  89. data/ext/carray_operator.c +1089 -359
  90. data/ext/carray_order.c +133 -69
  91. data/ext/carray_partition.c +102 -56
  92. data/ext/carray_random.c +273 -239
  93. data/ext/carray_scatter.c +308 -65
  94. data/ext/carray_segment.c +257 -0
  95. data/ext/carray_slab.c +170 -70
  96. data/ext/carray_sort.c +215 -218
  97. data/ext/carray_sort_kernel.c +8 -1
  98. data/ext/carray_struct.c +99 -21
  99. data/ext/carray_test.c +167 -64
  100. data/ext/carray_time_text.c +1182 -0
  101. data/ext/carray_utils.c +363 -98
  102. data/ext/extconf.rb +27 -2
  103. data/ext/mk_call_cfunc.rb +0 -2
  104. data/ext/mk_pow5_table.rb +35 -0
  105. data/ext/mkkernel.rb +1076 -846
  106. data/ext/ruby_carray.c +7 -1
  107. data/ext/version.h +4 -4
  108. data/ext/version.rb +4 -2
  109. data/lib/carray/attribute.rb +39 -0
  110. data/lib/carray/autoload_carray.rb +19 -0
  111. data/lib/carray/autoload_method_extension.rb +6 -0
  112. data/lib/carray/axis_group.rb +174 -26
  113. data/lib/carray/basics.rb +91 -25
  114. data/lib/carray/bincount_nd.rb +6 -0
  115. data/lib/carray/block_iterator.rb +118 -48
  116. data/lib/carray/boolean_reduce.rb +21 -29
  117. data/lib/carray/categorical.rb +94 -26
  118. data/lib/carray/categorical_iterator.rb +333 -826
  119. data/lib/carray/complex.rb +36 -29
  120. data/lib/carray/conditional.rb +94 -7
  121. data/lib/carray/const_string.rb +46 -0
  122. data/lib/carray/frame/concat.rb +157 -6
  123. data/lib/carray/frame/convert.rb +13 -7
  124. data/lib/carray/frame/csv_parser.rb +589 -82
  125. data/lib/carray/frame/describe.rb +79 -0
  126. data/lib/carray/frame/frame.rb +124 -25
  127. data/lib/carray/frame/group.rb +189 -63
  128. data/lib/carray/frame/io.rb +412 -72
  129. data/lib/carray/frame/join.rb +134 -46
  130. data/lib/carray/frame/records.rb +37 -16
  131. data/lib/carray/frame/reshape.rb +448 -0
  132. data/lib/carray/frame/sort.rb +26 -2
  133. data/lib/carray/frame/verbs.rb +715 -48
  134. data/lib/carray/frame.rb +2 -0
  135. data/lib/carray/fuse_source.rb +111 -2
  136. data/lib/carray/fusion.rb +229 -38
  137. data/lib/carray/inspect.rb +29 -11
  138. data/lib/carray/iterator.rb +14 -2
  139. data/lib/carray/lazy.rb +54 -30
  140. data/lib/carray/mask_gap_fill.rb +66 -52
  141. data/lib/carray/math.rb +32 -19
  142. data/lib/carray/meld_reduce.rb +40 -7
  143. data/lib/carray/methods/align_addr.rb +8 -4
  144. data/lib/carray/methods/bincount.rb +2 -2
  145. data/lib/carray/methods/bit_string.rb +14 -3
  146. data/lib/carray/methods/composition.rb +16 -10
  147. data/lib/carray/methods/factorize.rb +2 -2
  148. data/lib/carray/methods/gather_nd.rb +13 -2
  149. data/lib/carray/methods/insert_block.rb +9 -14
  150. data/lib/carray/methods/is_in.rb +117 -4
  151. data/lib/carray/methods/join.rb +1 -1
  152. data/lib/carray/methods/locate_addr.rb +177 -36
  153. data/lib/carray/methods/mode.rb +18 -1
  154. data/lib/carray/methods/repeat.rb +1 -20
  155. data/lib/carray/methods/resize.rb +21 -23
  156. data/lib/carray/methods/snap.rb +34 -12
  157. data/lib/carray/methods/unique.rb +11 -2
  158. data/lib/carray/record_order.rb +208 -0
  159. data/lib/carray/runtime.rb +14 -11
  160. data/lib/carray/segment_iterator.rb +990 -0
  161. data/lib/carray/serialize.rb +197 -27
  162. data/lib/carray/slab_iterator.rb +51 -27
  163. data/lib/carray/stack.rb +10 -4
  164. data/lib/carray/string_operation_extension.rb +4 -3
  165. data/lib/carray/struct.rb +142 -51
  166. data/lib/carray/struct_builder.rb +110 -14
  167. data/lib/carray/table.rb +2 -2
  168. data/lib/carray/time.rb +517 -103
  169. data/lib/carray/window_iterator.rb +127 -72
  170. data/lib/carray.rb +2 -2
  171. data/yard-stubs/ca_obj_array.rb +4 -4
  172. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  173. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  174. data/yard-stubs/ca_obj_fake.rb +1 -1
  175. data/yard-stubs/ca_obj_field.rb +1 -1
  176. data/yard-stubs/ca_obj_refer.rb +3 -3
  177. data/yard-stubs/ca_obj_roll.rb +0 -1
  178. data/yard-stubs/ca_obj_stride.rb +10 -7
  179. data/yard-stubs/ca_obj_transpose.rb +1 -1
  180. data/yard-stubs/ca_obj_window.rb +1 -2
  181. data/yard-stubs/carray_access.rb +5 -4
  182. data/yard-stubs/carray_broadcast.rb +1 -1
  183. data/yard-stubs/carray_cast.rb +78 -43
  184. data/yard-stubs/carray_conversion.rb +13 -1
  185. data/yard-stubs/carray_copy.rb +8 -3
  186. data/yard-stubs/carray_core.rb +3 -3
  187. data/yard-stubs/carray_count.rb +9 -0
  188. data/yard-stubs/carray_lazy.rb +5 -1
  189. data/yard-stubs/carray_loop.rb +4 -1
  190. data/yard-stubs/carray_mask.rb +34 -10
  191. data/yard-stubs/carray_math.rb +25 -13
  192. data/yard-stubs/carray_median_percentile.rb +10 -6
  193. data/yard-stubs/carray_order.rb +34 -13
  194. data/yard-stubs/carray_scatter.rb +10 -3
  195. data/yard-stubs/carray_segment.rb +72 -0
  196. data/yard-stubs/carray_slab.rb +2 -1
  197. data/yard-stubs/carray_sort.rb +43 -3
  198. data/yard-stubs/carray_test.rb +10 -0
  199. data/yard-stubs/carray_utils.rb +0 -26
  200. metadata +11 -19
  201. data/ext/ca_for_each_element.h +0 -239
  202. data/ext/carray_build_flags.h +0 -3
  203. data/ext/carray_config.h +0 -28
  204. data/ext/carray_kernels_bincmp.c +0 -4446
  205. data/ext/carray_kernels_binop.c +0 -11001
  206. data/ext/carray_kernels_init.c +0 -1131
  207. data/ext/carray_kernels_map.c +0 -3467
  208. data/ext/carray_kernels_moncmp.c +0 -2097
  209. data/ext/carray_kernels_monop.c +0 -18313
  210. data/ext/carray_kernels_reduce_aggregate.c +0 -26005
  211. data/ext/carray_kernels_reduce_boolean.c +0 -330
  212. data/ext/carray_kernels_reduce_cumulative.c +0 -14862
  213. data/ext/carray_kernels_reduce_extreme.c +0 -17494
  214. data/ext/carray_kernels_reduce_variance.c +0 -3910
  215. data/ext/carray_kernels_scan.c +0 -3693
  216. data/ext/carray_kernels_search.c +0 -32218
  217. data/ext/carray_kernels_sort.c +0 -10626
  218. data/ext/carray_kernels_triop.c +0 -1392
data/ext/mkkernel.rb CHANGED
@@ -406,8 +406,8 @@ module MkKernel
406
406
  # only, output (an axis-local index) needs no re-lift.
407
407
  # :relift -- value-returning kernels (min/max): descend, then re-lift the
408
408
  # output back into the Face -- a full-reduction scalar via the
409
- # registered storage_to_scalar hook, a per-axis CArray via
410
- # rb_ca_face_template (carries the subclass tail, e.g. unit).
409
+ # registered storage_to_element hook, a per-axis CArray via
410
+ # ca_face_relift_entity (carries the subclass tail, e.g. unit).
411
411
  raise "reduce: unknown face_gate #{face_gate} (expected nil, :strip, or :relift)" \
412
412
  unless [nil, :strip, :relift].include?(face_gate)
413
413
  # object_escape: a Symbol naming a Ruby instance method that handles the
@@ -659,41 +659,6 @@ module MkKernel
659
659
  }
660
660
  end
661
661
 
662
- # Register a map kernel (element-wise transform: input shape == output
663
- # shape, one cell at a time). Each generated helper walks the input
664
- # and output slabs in lockstep using CA_SLAB_MAP_T(T_LOAD, T_OUT, ...).
665
- #
666
- # DSL:
667
- # MkKernel.map :name,
668
- # source: MkKernel::ALL_NUMERIC,
669
- # output: :f64 | :preserve,
670
- # expr: "r = sqrt((double) v)", # binds v (input) + r (output)
671
- # fallback: :wrap_to_f64 | :raise
672
- #
673
- # Generated method takes no arguments (`a.sqrt_ki` not `a.sqrt_ki(0)`)
674
- # since the operation is per-cell across the whole array.
675
- def self.map(name, source:, output:, expr:, fallback: :raise)
676
- raise "duplicate kernel #{name}" if KERNELS.any? { |k| k[:name] == name }
677
- source.each do |s|
678
- raise "unknown source data_type #{s}" unless DTYPES.key?(s)
679
- end
680
- raise "unknown output #{output}" unless output == :preserve || DTYPES.key?(output) || output.is_a?(Hash)
681
- raise "unknown fallback #{fallback}" unless %i[raise wrap_to_f64].include?(fallback)
682
- if output == :preserve && fallback == :wrap_to_f64
683
- raise "#{name}: :preserve + :wrap_to_f64 is a semantic conflict " \
684
- "(fallback path produces f64 output, contradicting :preserve)"
685
- end
686
-
687
- KERNELS << {
688
- kind: :map,
689
- name: name,
690
- source: source,
691
- output: output,
692
- expr: expr,
693
- fallback: fallback,
694
- }
695
- end
696
-
697
662
  # Register a scan kernel (cumulative / prefix-scan: input shape ==
698
663
  # output shape, running accumulator along one axis). The generated
699
664
  # method takes exactly one axis argument (e.g., `a.cumsum_ki(0)`).
@@ -704,7 +669,11 @@ module MkKernel
704
669
  # output: :f64 | :preserve,
705
670
  # init: "0", # initial acc value (per fiber)
706
671
  # step: "acc += v; r = acc", # binds v (T_LOAD), r (T_OUT), acc
707
- # 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.
708
677
  #
709
678
  # Init supports the same special tokens as reduce: T_LIMIT_HI /
710
679
  # T_LIMIT_LO / T_ACC / T_OUT. Each scan slab (= fiber along the
@@ -738,13 +707,13 @@ module MkKernel
738
707
  # the reduction contract (empty max / min = UNDEF, no
739
708
  # identity) and the axis-group scan family.
740
709
  def self.scan(name, source:, output:, init:, step:, fallback: :raise,
741
- acc_type: nil, axis_default: nil, empty: nil)
710
+ acc_type: nil, axis_default: nil, empty: nil, face_gate: nil)
742
711
  raise "duplicate kernel #{name}" if KERNELS.any? { |k| k[:name] == name }
743
712
  source.each do |s|
744
713
  raise "unknown source data_type #{s}" unless DTYPES.key?(s)
745
714
  end
746
715
  raise "unknown output #{output}" unless output == :preserve || DTYPES.key?(output) || output.is_a?(Hash)
747
- raise "unknown fallback #{fallback}" unless %i[raise wrap_to_f64].include?(fallback)
716
+ raise "unknown fallback #{fallback}" unless %i[raise wrap_to_f64 mask_count].include?(fallback)
748
717
  if output == :preserve && fallback == :wrap_to_f64
749
718
  raise "#{name}: :preserve + :wrap_to_f64 is a semantic conflict"
750
719
  end
@@ -760,6 +729,13 @@ module MkKernel
760
729
  if empty == :undef && acc_type == :load_type
761
730
  raise "#{name}: empty: :undef is not supported with acc_type: :load_type"
762
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
763
739
 
764
740
  KERNELS << {
765
741
  kind: :scan,
@@ -772,6 +748,7 @@ module MkKernel
772
748
  acc_type: acc_type,
773
749
  axis_default: axis_default,
774
750
  empty: empty,
751
+ face_gate: face_gate,
775
752
  }
776
753
  end
777
754
 
@@ -1446,6 +1423,12 @@ module MkKernel
1446
1423
  true
1447
1424
  end
1448
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
+
1449
1432
  def self.reduce_macro_suffix(k, src = nil)
1450
1433
  # CA_OBJECT cannot ride the SIMD-licensed macros (= _PLUS / _MIN / _MAX
1451
1434
  # / _STAR), which assume C operators (= acc is a VALUE, so
@@ -1624,9 +1607,11 @@ module MkKernel
1624
1607
  require "fileutils"
1625
1608
  FileUtils.mkdir_p(outdir)
1626
1609
  tags = file_tags
1610
+ written = ["carray_kernels_init.c"]
1627
1611
  tags.each do |kind, subgroup|
1628
1612
  suffix = file_suffix(kind, subgroup)
1629
1613
  path = File.join(outdir, "carray_kernels_#{suffix}.c")
1614
+ written << File.basename(path)
1630
1615
  File.open(path, "w") do |io|
1631
1616
  io.puts header
1632
1617
  KERNELS.each do |k|
@@ -1640,6 +1625,11 @@ module MkKernel
1640
1625
  File.open(File.join(outdir, "carray_kernels_init.c"), "w") do |io|
1641
1626
  emit_aggregator_init(io, tags)
1642
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
1643
1633
  end
1644
1634
 
1645
1635
  # Which file a kernel belongs to. The three alias kinds live with the kind
@@ -1658,7 +1648,6 @@ module MkKernel
1658
1648
  def self.emit_kernel_dispatch(io, k)
1659
1649
  case k[:kind]
1660
1650
  when :reduce then emit_reduce(io, k)
1661
- when :map then emit_map(io, k)
1662
1651
  when :scan then emit_scan(io, k)
1663
1652
  when :sort then emit_sort(io, k)
1664
1653
  when :search then emit_search(io, k)
@@ -1675,7 +1664,7 @@ module MkKernel
1675
1664
  end
1676
1665
  end
1677
1666
 
1678
- KINDS = [:reduce, :map, :scan, :sort, :search,
1667
+ KINDS = [:reduce, :scan, :sort, :search,
1679
1668
  :monop, :binop, :triop, :moncmp, :bincmp].freeze
1680
1669
 
1681
1670
  # Sub-split map for the reduce kind. As one file, reduce accounted for 71%
@@ -1686,17 +1675,16 @@ module MkKernel
1686
1675
  # rather than the guess that monop and binop would dominate. A kernel added
1687
1676
  # later needs an entry here; emit asserts, so a missing one is loud.
1688
1677
  REDUCE_SUB_GROUPS = {
1689
- # aggregate: arithmetic folds (sum / prod / mean and their strict,
1690
- # safe and weighted variants)
1678
+ # aggregate: arithmetic folds (sum / prod / mean and their weighted
1679
+ # variants)
1691
1680
  sum: :aggregate, prod: :aggregate, mean: :aggregate,
1692
- sum_strict: :aggregate, mean_safe: :aggregate,
1693
1681
  wsum: :aggregate, wmean: :aggregate,
1694
1682
  # extreme: order-based (min / max, argmin / argmax, minmax)
1695
1683
  min: :extreme, max: :extreme, minmax: :extreme,
1696
1684
  argmin: :extreme, argmax: :extreme,
1697
1685
  argmin_addr: :extreme, argmax_addr: :extreme,
1698
1686
  # cumulative: counter accumulation (the count family and accumulate)
1699
- count: :cumulative, accumulate: :cumulative,
1687
+ accumulate: :cumulative,
1700
1688
  count_true: :cumulative, count_false: :cumulative,
1701
1689
  count_equal: :cumulative,
1702
1690
  # variance family: multi-state (Chan / Welford), and the one that
@@ -1784,11 +1772,116 @@ module MkKernel
1784
1772
  base + HEADER_BLOCKS.join("\n")
1785
1773
  end
1786
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
+
1787
1880
  def self.emit_reduce(io, k)
1788
1881
  io.puts
1789
1882
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
1790
1883
  k[:source].each do |src|
1791
- emit_reduce_native(io, k, src)
1884
+ emit_native_for(io, k, src) { |o| emit_reduce_native(o, k, src) }
1792
1885
  end
1793
1886
  emit_reduce_dispatch(io, k)
1794
1887
  end
@@ -1860,6 +1953,11 @@ module MkKernel
1860
1953
 
1861
1954
  view_flat = (k[:semantics] == :view_flat)
1862
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
+
1863
1961
  io.puts
1864
1962
  io.puts <<~C
1865
1963
  static VALUE
@@ -1868,8 +1966,7 @@ module MkKernel
1868
1966
  C
1869
1967
 
1870
1968
  if streamable
1871
- emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var, acc_init,
1872
- decls, reduce_stmt, finish_expr, extra_args, src)
1969
+ emit_reduce_streaming_call(io, k, src)
1873
1970
  end
1874
1971
 
1875
1972
  # L.1 / L.3 / L.4 (PROPOSAL_REDUCTION_LOOP_INTERCHANGE):
@@ -1917,12 +2014,22 @@ module MkKernel
1917
2014
  else
1918
2015
  mixed_buf = false
1919
2016
  end
1920
- 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] &&
1921
2022
  buf_count >= 1 && induction_count <= 1 && other_count == 0 &&
1922
2023
  !mixed_buf
1923
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
1924
2030
  emit_reduce_loop_interchange(io, k, si, oi, ruby_wrap, src,
1925
2031
  acc_var, acc_init, reduce_stmt, finish_expr)
2032
+ io.puts " }" if li_gate
1926
2033
  end
1927
2034
 
1928
2035
  if view_flat
@@ -1932,7 +2039,7 @@ module MkKernel
1932
2039
  # (slab walks the whole array in view-flat row-major); other
1933
2040
  # naxes are rejected at entry.
1934
2041
  io.puts " if ( naxes != 1 && naxes != ca->ndim ) {"
1935
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: view_flat semantics requires single-axis or full reduction (got naxes=%d for ndim=%d)", (int)naxes, (int)ca->ndim);]
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);]
1936
2043
  io.puts " }"
1937
2044
  end
1938
2045
 
@@ -1946,7 +2053,7 @@ module MkKernel
1946
2053
  slab_axes, naxes, 0);
1947
2054
  if ( rc != CA_ITER_OK ) {
1948
2055
  rb_raise(rb_eRuntimeError,
1949
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
2056
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
1950
2057
  }
1951
2058
  char *p;
1952
2059
  boolean8_t *m;
@@ -1984,7 +2091,7 @@ module MkKernel
1984
2091
  io.puts " slab_axes, naxes, 0);"
1985
2092
  io.puts " if ( rcw != CA_ITER_OK ) {"
1986
2093
  io.puts " ca_iter_state_finish(&st);"
1987
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: weights kernel_iterator init failed rc=%d", rcw);]
2094
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: weights kernel_iterator init failed rc=%d", ca_calling_method_name(), rcw);]
1988
2095
  io.puts " }"
1989
2096
  io.puts " char *p_w;"
1990
2097
  end
@@ -1999,7 +2106,7 @@ module MkKernel
1999
2106
  io.puts " if ( ! ca_iter_state_next_slab_axes(&st_w, &p_w, NULL) ) {"
2000
2107
  io.puts " ca_iter_state_finish(&st);"
2001
2108
  io.puts " ca_iter_state_finish(&st_w);"
2002
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: weights iter exhausted early (shape invariant violated)");]
2109
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: weights iter exhausted early (shape invariant violated)", ca_calling_method_name());]
2003
2110
  io.puts " }"
2004
2111
  else
2005
2112
  io.puts " while ( ca_iter_state_next_slab_axes(&st, &p, &m) ) {"
@@ -2035,6 +2142,11 @@ module MkKernel
2035
2142
  if k[:all_nan_result] == :undef && FLOAT_DTYPES.include?(src)
2036
2143
  all_nan_flag = "__anf_all_nan"
2037
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 );"
2038
2150
  end
2039
2151
  emit_all_nan_fixup(io, k, src, si, pairs,
2040
2152
  "masked_cnt < st.slab_elements",
@@ -2042,24 +2154,12 @@ module MkKernel
2042
2154
  end
2043
2155
  trigger = case k[:mask_policy]
2044
2156
  when :strict then "masked_cnt > 0"
2045
- 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)"
2046
2158
  when :min_count
2047
- # min_count < 0 (sentinel) => legacy default (all_masked).
2048
- # min_count >= 0 => need at least min_count valid cells
2049
- # (valid_cnt = slab_elements - masked_cnt).
2050
- #
2051
- # ERI.0: identity_on_empty kernels do NOT fire UNDEF for the
2052
- # default (min_count < 0) zero-contribution case; the trigger
2053
- # is false there, so the else branch writes finish (= acc =
2054
- # identity) for empty / all-masked slabs. The explicit
2055
- # min_count >= 0 opt-in ("require K valid cells") is unchanged.
2056
- if k[:identity_on_empty]
2057
- "(min_count < 0 ? 0 " \
2058
- ": st.slab_elements - masked_cnt < min_count)"
2059
- else
2060
- "(min_count < 0 ? masked_cnt == st.slab_elements " \
2061
- ": st.slab_elements - masked_cnt < min_count)"
2062
- end
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})"
2063
2163
  end
2064
2164
  trigger = "(#{trigger}) || #{all_nan_flag}" if all_nan_flag
2065
2165
  finish_emit = view_flat \
@@ -2166,7 +2266,7 @@ module MkKernel
2166
2266
  io.puts " ca_iter_state st;"
2167
2267
  io.puts " int rc = ca_iter_state_init_l2(&st, ca, CA_SLAB_AXES, slab_axes, naxes, 0);"
2168
2268
  io.puts " if ( rc != CA_ITER_OK ) {"
2169
- 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);]
2170
2270
  io.puts " }"
2171
2271
  io.puts " char *p;"
2172
2272
  io.puts " boolean8_t *m;"
@@ -2215,16 +2315,9 @@ module MkKernel
2215
2315
  io.puts " sidx[sk] = 0;"
2216
2316
  io.puts " }"
2217
2317
  io.puts " }"
2218
- # ERI.0: a count over nothing is 0, not UNDEF -- so the default
2219
- # (min_count < 0) never fires the mask for :count_equal. An explicit
2220
- # min_count: still does.
2221
- if counting
2222
- io.puts " if ( min_count < 0 ? 0"
2223
- io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2224
- else
2225
- io.puts " if ( min_count < 0 ? masked_cnt == st.slab_elements"
2226
- io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2227
- end
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}) ) {"
2228
2321
  io.puts " if ( ! op_mask ) {"
2229
2322
  io.puts " ca_create_mask(co);"
2230
2323
  io.puts " op_mask = (boolean8_t *) co->mask->ptr;"
@@ -2277,10 +2370,8 @@ module MkKernel
2277
2370
  # Layout per slab:
2278
2371
  # Pass 1: sum = Σ x_i via CA_SLAB_REDUCE_T_PLUS_EX (SIMD :plus)
2279
2372
  # masked_cnt is populated as a side effect.
2280
- # mask trigger:
2281
- # min_count < 0 (default) -> UNDEF iff all-masked, OR (for
2282
- # :n_minus_1 divisor) if n_valid < 2.
2283
- # min_count >= 0 -> UNDEF iff n_valid < min_count.
2373
+ # mask trigger: ca_too_few_present(n_valid, min_count, 1 or 2), 2 for
2374
+ # the :n_minus_1 divisor.
2284
2375
  # Pass 2: M2 = Σ (x_i - mean)² via the same macro with a centred
2285
2376
  # REDUCE stmt.
2286
2377
  # Finish: variance = M2 / DIVISOR (n or n-1); stddev variant applies
@@ -2387,9 +2478,9 @@ module MkKernel
2387
2478
  finish_expr = if k[:output_transform] == :sqrt_clamp
2388
2479
  if src == :object
2389
2480
  # Object stddev outputs f64 (BigMath.sqrt is user's job).
2390
- "sqrt(fmax(NUM2DBL(#{inner_var}), 0.0))"
2481
+ "ca_sqrt_clamp(NUM2DBL(#{inner_var}))"
2391
2482
  else
2392
- "sqrt(fmax(#{inner_var}, 0.0))"
2483
+ "ca_sqrt_clamp(#{inner_var})"
2393
2484
  end
2394
2485
  else
2395
2486
  inner_var
@@ -2417,13 +2508,9 @@ module MkKernel
2417
2508
  # min_count sentinel arg is always present (all four kernels use it).
2418
2509
  extra_args = ", ca_size_t min_count"
2419
2510
 
2420
- # The default (min_count < 0) trigger fires when all cells are
2421
- # masked; for sample-variance/stddev we also need n_valid >= 2 to
2422
- # avoid division by zero, but the legacy behaviour returned 0
2423
- # (not UNDEF) for n_valid < required_min. Preserve that: the
2424
- # default trigger only checks the all-masked case; the sub-required
2425
- # case falls through to the "return zero" branch below.
2426
- default_trigger = "masked_cnt == st.slab_elements"
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.
2427
2514
 
2428
2515
  # Reduce-call plumbing. :object keeps the scalar rb_funcall loop
2429
2516
  # (CA_SLAB_REDUCE_T_EX). Every other src uses the 8-way accumulator
@@ -2464,7 +2551,7 @@ module MkKernel
2464
2551
  slab_axes, naxes, 0);
2465
2552
  if ( rc != CA_ITER_OK ) {
2466
2553
  rb_raise(rb_eRuntimeError,
2467
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
2554
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
2468
2555
  }
2469
2556
  char *p;
2470
2557
  boolean8_t *m;
@@ -2476,7 +2563,7 @@ module MkKernel
2476
2563
  ca_size_t masked_cnt = 0;
2477
2564
  #{p1_reduce_call}
2478
2565
  ca_size_t n_valid = st.slab_elements - masked_cnt;
2479
- int __trigger = (min_count < 0 ? #{default_trigger} : n_valid < min_count);
2566
+ int __trigger = ca_too_few_present(n_valid, min_count, #{required_min});
2480
2567
  if ( __trigger ) {
2481
2568
  if ( ! op_mask ) {
2482
2569
  ca_create_mask(co);
@@ -2484,11 +2571,6 @@ module MkKernel
2484
2571
  }
2485
2572
  op_mask[out_i] = 1;
2486
2573
  op[out_i++] = (#{out_c}) #{out_zero_typed};
2487
- } else if ( n_valid < #{required_min} ) {
2488
- /* Legacy degenerate case (e.g. sample variance with 1 unmasked
2489
- cell): return zero rather than UNDEF, matching pre-migration
2490
- behaviour of `cnt > 1 ? formula : 0`. */
2491
- op[out_i++] = (#{out_c}) #{out_zero_typed};
2492
2574
  } else {
2493
2575
  #{mean_setup}
2494
2576
  /* Pass 2: M2 = Σ (x - mean)² */
@@ -2660,8 +2742,9 @@ module MkKernel
2660
2742
  exposes ca->ptr as that buffer at O(1) cost (no materialise). */
2661
2743
  if ( naxes == 1
2662
2744
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2663
- && ca->mask == NULL
2664
- && ca_attach_is_alias(ca) ) {
2745
+ && ! ca_has_mask(ca)
2746
+ && ca_attach_is_alias(ca)
2747
+ && ca_fast_paths_enabled() ) {
2665
2748
  int8_t __li_ax = slab_axes[0];
2666
2749
  ca_size_t __li_M = ca->dim[__li_ax];
2667
2750
  ca_size_t __li_INNER = 1;
@@ -2675,7 +2758,7 @@ module MkKernel
2675
2758
  CArray *co;
2676
2759
  GetCArray(vout, co);
2677
2760
  #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
2678
- 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 */
2679
2762
  const #{si[:c]} *p_root = (const #{si[:c]} *) ca->ptr;
2680
2763
 
2681
2764
  /* Per-buffer stack accumulators (one per :plus state, sized
@@ -2709,9 +2792,10 @@ module MkKernel
2709
2792
  if ( naxes == 1
2710
2793
  && slab_axes[0] >= 1
2711
2794
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2712
- && ca->mask == NULL
2795
+ && ! ca_has_mask(ca)
2713
2796
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2714
- && ((CAStack *) ca)->k_axis == 0 ) {
2797
+ && ((CAStack *) ca)->k_axis == 0
2798
+ && ca_fast_paths_enabled() ) {
2715
2799
  CAStack *__li_st = (CAStack *) ca;
2716
2800
  int8_t __li_ax = slab_axes[0];
2717
2801
  ca_size_t __li_M = ca->dim[__li_ax];
@@ -2724,7 +2808,7 @@ module MkKernel
2724
2808
  core reads parent->ptr without mask awareness). */
2725
2809
  int __li_ok = 1;
2726
2810
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2727
- if ( __li_st->parents[__kk]->mask != NULL ) { __li_ok = 0; break; }
2811
+ if ( ca_has_mask(__li_st->parents[__kk]) ) { __li_ok = 0; break; }
2728
2812
  }
2729
2813
 
2730
2814
  if ( __li_ok && __li_INNER >= 64 && __li_M * __li_INNER >= 1024 ) {
@@ -2739,7 +2823,7 @@ module MkKernel
2739
2823
  ca_size_t __li_parent_out = __li_OUTER * __li_INNER;
2740
2824
  const ca_size_t __li_TILE = 512;
2741
2825
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2742
- ca_attach(__li_st->parents[__kk]);
2826
+ ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2743
2827
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2744
2828
  #{oi[:c]} *__li_op_k = op + (ca_size_t) __kk * __li_parent_out;
2745
2829
  #{core.call("p_root", "__li_op_k", "__li_OUTER")}
@@ -2772,11 +2856,12 @@ module MkKernel
2772
2856
  if ( naxes == 1
2773
2857
  && slab_axes[0] >= 1
2774
2858
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2775
- && ca->mask == NULL
2859
+ && ! ca_has_mask(ca)
2776
2860
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2777
2861
  && ((CAStack *) ca)->k_axis > 0
2778
2862
  && ( slab_axes[0] > ((CAStack *) ca)->k_axis
2779
- || slab_axes[0] + 1 == ((CAStack *) ca)->k_axis ) ) {
2863
+ || slab_axes[0] + 1 == ((CAStack *) ca)->k_axis )
2864
+ && ca_fast_paths_enabled() ) {
2780
2865
  CAStack *__li_st = (CAStack *) ca;
2781
2866
  int8_t __li_ax = slab_axes[0];
2782
2867
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -2792,7 +2877,7 @@ module MkKernel
2792
2877
  /* Bail to the generic path if any parent carries a mask. */
2793
2878
  int __li_ok = 1;
2794
2879
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2795
- if ( __li_st->parents[__kk]->mask != NULL ) { __li_ok = 0; break; }
2880
+ if ( ca_has_mask(__li_st->parents[__kk]) ) { __li_ok = 0; break; }
2796
2881
  }
2797
2882
 
2798
2883
  if ( __li_ok && __li_INNER >= 64 && __li_M * __li_INNER >= 1024 ) {
@@ -2833,7 +2918,7 @@ module MkKernel
2833
2918
  ca_size_t __li_plane_elems = __li_M * __li_INNER;
2834
2919
  const ca_size_t __li_TILE = 512;
2835
2920
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2836
- ca_attach(__li_st->parents[__kk]);
2921
+ ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2837
2922
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2838
2923
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
2839
2924
 
@@ -2915,10 +3000,11 @@ module MkKernel
2915
3000
  if ( naxes == 1
2916
3001
  && slab_axes[0] >= 1
2917
3002
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2918
- && ca->mask == NULL
3003
+ && ! ca_has_mask(ca)
2919
3004
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2920
3005
  && ((CAStack *) ca)->k_axis > 0
2921
- && slab_axes[0] + 1 < ((CAStack *) ca)->k_axis ) {
3006
+ && slab_axes[0] + 1 < ((CAStack *) ca)->k_axis
3007
+ && ca_fast_paths_enabled() ) {
2922
3008
  CAStack *__li_st = (CAStack *) ca;
2923
3009
  int8_t __li_ax = slab_axes[0];
2924
3010
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -2936,7 +3022,7 @@ module MkKernel
2936
3022
  /* Bail to the generic path if any parent carries a mask. */
2937
3023
  int __li_ok = 1;
2938
3024
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2939
- if ( __li_st->parents[__kk]->mask != NULL ) { __li_ok = 0; break; }
3025
+ if ( ca_has_mask(__li_st->parents[__kk]) ) { __li_ok = 0; break; }
2940
3026
  }
2941
3027
 
2942
3028
  if ( __li_ok && __li_eff_INNER >= 32 && __li_M * __li_eff_INNER >= 1024 ) {
@@ -2990,7 +3076,7 @@ module MkKernel
2990
3076
 
2991
3077
  const ca_size_t __li_TILE = 512;
2992
3078
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2993
- ca_attach(__li_st->parents[__kk]);
3079
+ ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2994
3080
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2995
3081
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
2996
3082
 
@@ -3097,7 +3183,7 @@ module MkKernel
3097
3183
  slab_axes, naxes, 0);
3098
3184
  if ( rc != CA_ITER_OK ) {
3099
3185
  rb_raise(rb_eRuntimeError,
3100
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
3186
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
3101
3187
  }
3102
3188
  char *p;
3103
3189
  boolean8_t *m;
@@ -3116,10 +3202,7 @@ module MkKernel
3116
3202
  emit_all_nan_fixup(io, k, src, si, limit_init_state_vars(k, si, oi, src),
3117
3203
  "masked_cnt < st.slab_elements", indent: " ")
3118
3204
  end
3119
- # Same trigger as single-output :min_count: legacy default (all_masked)
3120
- # when min_count < 0, otherwise need at least min_count valid cells.
3121
- trigger = "(min_count < 0 ? masked_cnt == st.slab_elements " \
3122
- ": st.slab_elements - masked_cnt < min_count)"
3205
+ trigger = "ca_too_few_present(st.slab_elements - masked_cnt, min_count, 1)"
3123
3206
  io.puts " if ( #{trigger} ) {"
3124
3207
  io.puts " if ( ! op_mask_a ) {"
3125
3208
  io.puts " ca_create_mask(co_a);"
@@ -3153,10 +3236,11 @@ module MkKernel
3153
3236
  # pair of [1,...,1] entities instead.
3154
3237
  io.puts " if ( naxes == ca->ndim && !keep_axis ) {"
3155
3238
  if min_count
3156
- # Full reduction: if the sole output cell is masked, return CA_UNDEF.
3157
- # (Both op_mask_a and op_mask_b share the same bit since both
3158
- # reductions consume the same slab, so checking _a is sufficient.)
3159
- io.puts " if ( op_mask_a && op_mask_a[0] ) return CA_UNDEF;"
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);"
3160
3244
  end
3161
3245
  io.puts " return rb_assoc_new(#{ruby_wrap}(op_a[0]), #{ruby_wrap}(op_b[0]));"
3162
3246
  io.puts " }"
@@ -3164,42 +3248,40 @@ module MkKernel
3164
3248
  io.puts " }"
3165
3249
  end
3166
3250
 
3167
- # P.4.5.3b/c: streaming chunked reduce path emitter. Generates an
3168
- # early-return branch at the top of name_ki_native_<src> that pulls
3169
- # the lazy view in fixed-size chunks via ca_xfer_stride instead of
3170
- # full-materialising via SRC_ATTACH. Falls through to the existing
3171
- # path when conditions aren't met.
3172
- #
3173
- # Strategy: chunk along the outermost axis. inner = prod(dim[1..]);
3174
- # rows_per_chunk = max(1, target_elems / inner). 1-D is the natural
3175
- # sub-case (inner == 1, rows = target_elems).
3176
- #
3177
- # Memory peak: O(max(target_elems, inner) * sizeof T) instead of O(N).
3178
- # For N-D, this drops the outermost dim from the materialised buffer,
3179
- # which is typically the dominant factor (e.g. 1000x10000 f64 reduction:
3180
- # 80MB -> 80KB). Wall-clock perf at large N matches or beats
3181
- # SRC_ATTACH because each chunk fits in L1d (or L2 for wide last dims).
3182
- #
3183
- # Restrictions handled by short-circuit checks (= falls through to
3184
- # SRC_ATTACH path):
3185
- # - axis kwarg (= partial reduction): naxes != ca->ndim
3186
- # (structural blocker: CABinOp/CABinCmp xfer_stride rebuilds
3187
- # right_strides from counts, so non-rectangular slab pulls
3188
- # produce wrong values for the right operand. See proposal
3189
- # §future-work / partial streaming blocker.)
3190
- # - non-lazy source: !ca_is_lazy_view(ca)
3191
- # - mask present: ca_has_mask(ca)
3251
+ # The walk of the streaming reduce (see emit_reduce_streaming_call).
3192
3252
  def self.emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var,
3193
3253
  acc_init, decls, reduce_stmt,
3194
- finish_expr, extra_args, src)
3254
+ finish_expr, src)
3195
3255
  name = k[:name]
3196
- min_count = (k[:mask_policy] == :min_count)
3197
3256
  has_mp = !k[:mask_policy].nil?
3257
+ fn = "#{name}_ki_stream_#{src}"
3198
3258
 
3199
- io.puts " /* P.4.5.3b/c streaming chunked reduce: full reduction over lazy unmasked. */"
3200
- io.puts " /* P.4.5.3c (N-D): outer-axis chunking, 1-D is the natural sub-case. */"
3201
- io.puts " if ( naxes == ca->ndim && ca->ndim >= 1 && !keep_axis &&"
3202
- io.puts " ca_is_lazy_view(ca) && ! ca_has_mask(ca) ) {"
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
3203
3285
  io.puts " ca_size_t __inner = 1;"
3204
3286
  io.puts " int __k;"
3205
3287
  io.puts " for ( __k = 1; __k < ca->ndim; __k++ ) __inner *= ca->dim[__k];"
@@ -3237,7 +3319,6 @@ module MkKernel
3237
3319
  io.puts " ca_size_t masked_cnt = 0; /* unused on streaming (no mask) */"
3238
3320
  io.puts " (void) masked_cnt;"
3239
3321
  end
3240
- io.puts " ca_lazy_arena_enter();"
3241
3322
  if si[:c] == "VALUE"
3242
3323
  # Object lane: the chunk holds VALUEs pulled from the source, and
3243
3324
  # the fold below calls rb_funcall per cell. A collection there
@@ -3247,6 +3328,7 @@ module MkKernel
3247
3328
  else
3248
3329
  io.puts " #{si[:c]} *__chunk = (#{si[:c]} *) ca_lazy_arena_acquire(__chunk_elems * sizeof(#{si[:c]}));"
3249
3330
  end
3331
+ io.puts " __ctx->chunk = __chunk;"
3250
3332
  io.puts " while ( __outer_off < __outer ) {"
3251
3333
  io.puts " ca_size_t __r = (__outer - __outer_off < __rows) ? (__outer - __outer_off) : __rows;"
3252
3334
  io.puts " ca_size_t __n = __r * __inner;"
@@ -3261,21 +3343,84 @@ module MkKernel
3261
3343
  io.puts " }"
3262
3344
  io.puts " __outer_off += __r;"
3263
3345
  io.puts " }"
3264
- io.puts " ca_lazy_arena_release(__chunk);"
3265
- io.puts " ca_lazy_arena_exit();"
3266
3346
  if all_nan_stream
3267
3347
  io.puts " if ( ! __anf_seen && ca->elements > 0 ) #{acc_var} = (#{si[:c]}) NAN;"
3268
3348
  end
3269
- if has_mp
3270
- # Streaming path has no mask source, so masked_cnt is 0; min_count
3271
- # / strict / all_masked triggers all evaluate to false except
3272
- # min_count when explicit > 0 is set on an all-clean source (= no
3273
- # trigger). Output is the finish expression directly.
3274
- io.puts " return #{ruby_wrap}((#{oi[:c]}) (#{finish_expr}));"
3275
- else
3276
- io.puts " return #{ruby_wrap}((#{oi[:c]}) (#{finish_expr}));"
3277
- end
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);"
3397
+ io.puts " }"
3398
+ end
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);"
3278
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")
3279
3424
  end
3280
3425
 
3281
3426
  def self.emit_reduce_dispatch(io, k)
@@ -3303,12 +3448,13 @@ module MkKernel
3303
3448
  io.puts " GetCArray(self, src);"
3304
3449
 
3305
3450
  if k[:object_escape]
3306
- # CA_OBJECT escape: forward argc/argv (weights + trailing options hash,
3307
- # untouched) to the Ruby composition helper. Intercept before any arg
3308
- # parsing so the helper owns the full original signature.
3309
- io.puts " if ( src->data_type == CA_OBJECT ) {"
3310
- io.puts " return rb_funcallv(self, rb_intern(\"#{k[:object_escape]}\"), argc, argv);"
3311
- io.puts " }"
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;"
3312
3458
  end
3313
3459
 
3314
3460
  if k[:face_gate]
@@ -3342,7 +3488,18 @@ module MkKernel
3342
3488
  io.puts " int keep_axis = RTEST(rkeep_axis);"
3343
3489
  io.puts " int8_t slab_axes[CA_RANK_MAX];"
3344
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)
3345
3501
 
3502
+ weights_io = nil
3346
3503
  if has_aarg
3347
3504
  # Weight storage type: :promote materializes weights at the f64
3348
3505
  # computation type (so a float weight against an int source is not
@@ -3357,7 +3514,7 @@ module MkKernel
3357
3514
  # at dispatch (= simple, uniform kernel path). rev5 strict acceptance
3358
3515
  # mirrors PROPOSAL_LINEAR_INTERP_PER_FIBER_MATCHED rev5 in spirit.
3359
3516
  io.puts " if ( argc < 1 ) {"
3360
- 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());]
3361
3518
  io.puts " }"
3362
3519
  io.puts " volatile VALUE rweights = argv[0];"
3363
3520
  io.puts " argc--; argv++;"
@@ -3370,98 +3527,103 @@ module MkKernel
3370
3527
  io.puts " rweights = rb_funcall(rweights, rb_intern(\"[]\"), 1, INT2NUM(0));"
3371
3528
  io.puts " }"
3372
3529
  io.puts " }"
3373
- io.puts " volatile VALUE vcw;"
3374
- io.puts " CArray *cw;"
3375
- io.puts " if ( ! rb_obj_is_carray(rweights) ) {"
3376
- io.puts " /* W-A1: Ruby Numeric scalar -> full-shape constant weight CArray */"
3377
- io.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3378
- io.puts " GetCArray(vcw, cw);"
3379
- io.puts " ca_attach(cw);"
3380
- io.puts " rb_ca_obj2ptr(vcw, rweights, cw->ptr); /* fill first cell */"
3381
- io.puts " /* broadcast first cell to all elements (= constant fill) */"
3382
- io.puts " {"
3383
- io.puts " char *p0 = cw->ptr;"
3384
- io.puts " for ( ca_size_t i = 1; i < cw->elements; i++ ) {"
3385
- io.puts " memcpy(cw->ptr + i * cw->bytes, p0, cw->bytes);"
3386
- io.puts " }"
3387
- io.puts " }"
3388
- io.puts " }"
3389
- io.puts " else {"
3390
- io.puts " /* CArray val: coerce to the weight storage type (f64 for :promote, so a"
3391
- io.puts " float weight against an int source is not truncated) */"
3392
- io.puts " vcw = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3393
- io.puts " CArray *cv;"
3394
- io.puts " GetCArray(vcw, cv);"
3395
- io.puts ""
3396
- io.puts " /* rev5 form detection: A3 (same ndim) -> A2 (1-D axis-broadcast) -> raise */"
3397
- io.puts " if ( cv->ndim == src->ndim ) {"
3398
- io.puts " /* W-A3 commit-or-raise: shape strict match */"
3399
- io.puts " for ( int8_t i = 0; i < src->ndim; i++ ) {"
3400
- io.puts " if ( cv->dim[i] != src->dim[i] ) {"
3401
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: w shape mismatch (W-A3 candidate, dim[%d]=%ld != self.dim[%d]=%ld; expected scalar / [M=self.dim[axes[0]]] / self.shape)", (int)i, (long)cv->dim[i], (int)i, (long)src->dim[i]);]
3402
- io.puts " }"
3403
- io.puts " }"
3404
- io.puts " /* W-A3 path: use cv directly (= existing) */"
3405
- io.puts " cw = cv;"
3406
- io.puts " ca_attach(cw);"
3407
- io.puts " }"
3408
- io.puts " else if ( naxes == 1 && cv->ndim == 1 && cv->dim[0] == src->dim[slab_axes[0]] ) {"
3409
- io.puts " /* W-A2: 1-D axis-broadcast -> materialize to full shape */"
3410
- io.puts " ca_attach(cv);"
3411
- io.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3412
- io.puts " GetCArray(vcw, cw);"
3413
- io.puts " ca_attach(cw);"
3414
- io.puts " /* broadcast cv (1-D, length src->dim[slab_axes[0]]) along reduce axis */"
3415
- io.puts " {"
3416
- io.puts " int8_t baxis = slab_axes[0];"
3417
- io.puts " ca_size_t inner_stride = 1;"
3418
- io.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3419
- io.puts " ca_size_t axis_n = src->dim[baxis];"
3420
- io.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3421
- io.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3422
- io.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3423
- io.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3424
- io.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3425
- io.puts " memcpy(cw->ptr + flat * cw->bytes, cv->ptr + a * cv->bytes, cw->bytes);"
3426
- io.puts " }"
3427
- io.puts " }"
3428
- io.puts " }"
3429
- io.puts " }"
3430
- io.puts " /* Propagate cv mask via W-A3 overlay path below (= mask materialize) */"
3431
- io.puts " if ( ca_has_mask(cv) ) {"
3432
- io.puts " /* Allocate cw mask + broadcast cv mask along axis */"
3433
- io.puts " ca_create_mask(cw);"
3434
- io.puts " boolean8_t *cw_m = (boolean8_t *)cw->mask->ptr;"
3435
- io.puts " boolean8_t *cv_m = (boolean8_t *)cv->mask->ptr;"
3436
- io.puts " int8_t baxis = slab_axes[0];"
3437
- io.puts " ca_size_t inner_stride = 1;"
3438
- io.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3439
- io.puts " ca_size_t axis_n = src->dim[baxis];"
3440
- io.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3441
- io.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3442
- io.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3443
- io.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3444
- io.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3445
- io.puts " cw_m[flat] = cv_m[a];"
3446
- io.puts " }"
3447
- io.puts " }"
3448
- io.puts " }"
3449
- io.puts " }"
3450
- io.puts " ca_detach(cv);"
3451
- io.puts " }"
3452
- io.puts " else {"
3453
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: w shape not accepted (val.ndim=%d, val.dim[0]=%ld; expected scalar / [M=%ld] / self.shape)", (int)cv->ndim, (long)(cv->ndim >= 1 ? cv->dim[0] : 0), (long)(naxes == 1 ? src->dim[slab_axes[0]] : -1));]
3454
- io.puts " }"
3455
- io.puts " }"
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 " }"
3456
3614
  # Mask overlay (= legacy W-A3 path; if the materialized cw carries a mask, overlay it)
3457
- io.puts " volatile VALUE __vsrc_keep = self; /* GC guard */ (void) __vsrc_keep;"
3458
- io.puts " if ( ca_has_mask(cw) ) {"
3459
- io.puts " CArray *src_copy = ca_copy(src);"
3460
- io.puts " VALUE vsrc_copy = ca_wrap_struct(src_copy);"
3461
- io.puts " ca_copy_mask_overlay(src_copy, src_copy->elements, 1, cw);"
3462
- io.puts " src = src_copy;"
3463
- io.puts " self = vsrc_copy;"
3464
- io.puts " }"
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
3465
3627
  end
3466
3628
 
3467
3629
  if has_varg
@@ -3469,7 +3631,7 @@ module MkKernel
3469
3631
  # runtime value_arg (e.g. `a.count_equal(5, 0)` -- 5 is value_arg,
3470
3632
  # 0 is axis). Per-src cast happens inside the switch case below.
3471
3633
  io.puts " if ( argc < 1 ) {"
3472
- 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());]
3473
3635
  io.puts " }"
3474
3636
  io.puts " volatile VALUE rval = argv[0];"
3475
3637
  io.puts " argc--; argv++;"
@@ -3477,16 +3639,16 @@ module MkKernel
3477
3639
 
3478
3640
  if min_count
3479
3641
  # rmin_count / rfval were scanned up front (PoC scan-merge); here we
3480
- # only interpret them. Sentinel min_count = -1 means "no opt given"
3481
- # -> :all_masked semantics.
3482
- io.puts " ca_size_t min_count = -1;"
3483
- io.puts " if ( ! NIL_P(rmin_count) ) {"
3484
- io.puts " ca_size_t mc_user = NUM2SIZE(rmin_count);"
3485
- io.puts " if ( mc_user < 0 ) {"
3486
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: :min_count must be non-negative, got %lld", (long long) mc_user);]
3487
- io.puts " }"
3488
- io.puts " /* :min_count = 0 collapses to legacy default (any 1 valid). */"
3489
- io.puts " if ( mc_user > 0 ) min_count = mc_user;"
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;"
3490
3652
  io.puts " }"
3491
3653
  io.puts " VALUE result;"
3492
3654
  elsif has_aarg
@@ -3502,9 +3664,23 @@ module MkKernel
3502
3664
  # axis is allowed. Variadic `a.sum(0, 1)` raises here with a migration
3503
3665
  # hint pointing at the kwarg form.
3504
3666
  io.puts " if ( argc > 0 ) {"
3505
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: positional axis arguments are no longer accepted (got %d); use axis: kwarg, e.g. a.#{name}(axis: 0) or a.#{name}(axis: [0, 1])", argc);]
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());]
3506
3668
  io.puts " }"
3507
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
+
3508
3684
  io.puts " switch ( src->data_type ) {"
3509
3685
  k[:source].each do |s|
3510
3686
  si = DTYPES[s]
@@ -3515,8 +3691,14 @@ module MkKernel
3515
3691
  # A fixlen query is a runtime-width byte blob with no
3516
3692
  # scalar cast; pack it the way the search family does,
3517
3693
  # which NUL-pads a short String to the cell width.
3518
- " char *value_arg = ALLOCA_N(char, src->bytes);\n" \
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" \
3519
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"
3520
3702
  else
3521
3703
  " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n"
3522
3704
  end
@@ -3525,13 +3707,14 @@ module MkKernel
3525
3707
  io.print varg_decl unless varg_decl.empty?
3526
3708
  if use_result_var
3527
3709
  io.puts " result = #{name}_ki_native_#{s}(self, src, slab_axes, naxes, keep_axis#{extra_call});"
3710
+ io.puts " RB_GC_GUARD(self);"
3528
3711
  io.puts " break;"
3529
3712
  else
3530
- 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}));"
3531
3714
  end
3532
3715
  io.puts " }"
3533
3716
  else
3534
- 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));"
3535
3718
  end
3536
3719
  end
3537
3720
  case k[:fallback]
@@ -3544,7 +3727,8 @@ module MkKernel
3544
3727
  io.puts " switch ( src->data_type ) {"
3545
3728
  io.puts " case CA_BOOLEAN: break; /* wrap: count-of-trues semantic */"
3546
3729
  io.puts " default:"
3547
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")}, or boolean)", ca_type_name[src->data_type]);]
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]);]
3548
3732
  io.puts " }"
3549
3733
  io.puts " VALUE vsrc = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));"
3550
3734
  io.puts " CArray *casted;"
@@ -3556,42 +3740,44 @@ module MkKernel
3556
3740
  end
3557
3741
  if use_result_var
3558
3742
  io.puts " result = #{name}_ki_native_f64(vsrc, casted, slab_axes, naxes, keep_axis#{extra_call});"
3743
+ io.puts " RB_GC_GUARD(vsrc);"
3559
3744
  io.puts " break;"
3560
3745
  else
3561
- 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}));"
3562
3747
  end
3563
3748
  io.puts " }"
3564
3749
  when :raise
3565
3750
  io.puts " default:"
3566
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
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]);]
3567
3753
  end
3568
3754
  io.puts " }"
3569
3755
 
3570
- if min_count
3571
- # Apply fill_value substitution: CA_UNDEF (full reduction all-masked)
3572
- # or per-axis CArray with mask bits.
3573
- io.puts " if ( ! NIL_P(rfval) ) {"
3574
- io.puts " if ( result == CA_UNDEF ) {"
3575
- io.puts " result = rfval;"
3576
- io.puts " } else if ( TYPE(result) != T_FLOAT && rb_obj_is_kind_of(result, rb_cCArray) ) {"
3577
- io.puts " CArray *cr;"
3578
- io.puts " GetCArray(result, cr);"
3579
- io.puts " if ( ca_has_mask(cr) ) {"
3580
- io.puts " result = rb_ca_mask_fill_copy(result, rfval);"
3581
- io.puts " }"
3582
- io.puts " }"
3583
- io.puts " }"
3584
- end
3585
-
3586
- # array_arg cleanup: detach weights (paired with ca_attach above).
3756
+ # array_arg cleanup: detach weights (paired with ca_attach above),
3757
+ # ahead of the fill_value substitution, which can raise.
3587
3758
  if has_aarg
3588
3759
  io.puts " ca_detach(cw);"
3589
3760
  end
3590
3761
 
3591
- 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
3592
3773
  # Descended through emit_reduce_face_gate: re-lift the storage result
3593
3774
  # back into the Face before returning.
3594
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
3595
3781
  end
3596
3782
 
3597
3783
  if use_result_var
@@ -3602,117 +3788,13 @@ module MkKernel
3602
3788
  io.puts "}"
3603
3789
  end
3604
3790
 
3605
- # ---- map emitter ------------------------------------------------------
3606
-
3607
- def self.emit_map(io, k)
3608
- io.puts
3609
- io.puts "/* ===== #{k[:name]}_ki ============================================ */"
3610
- k[:source].each do |src|
3611
- emit_map_native(io, k, src)
3612
- end
3613
- emit_map_dispatch(io, k)
3614
- end
3615
-
3616
- def self.emit_map_native(io, k, src)
3617
- si = DTYPES[src]
3618
- oi = output_info(k, src)
3619
- name = k[:name]
3620
- expr = resolve_expr(k[:expr], oi, src)
3621
-
3622
- io.puts
3623
- io.puts <<~C
3624
- static VALUE
3625
- #{name}_ki_native_#{src} (VALUE self, CArray *ca)
3626
- {
3627
- VALUE vout = rb_ca_template_with_type(self, INT2NUM(#{oi[:ca]}),
3628
- INT2NUM(sizeof(#{oi[:c]})));
3629
- CArray *co;
3630
- GetCArray(vout, co);
3631
-
3632
- /* All-axes slab = whole array as one K-D walk via CA_SLAB_AXES.
3633
- CA_SLAB_WHOLE uses the flat next_slab path, which doesn't
3634
- populate slab_dims; CA_SLAB_AXES with naxes == ndim is the
3635
- K-D-walkable form. */
3636
- int8_t slab_axes[CA_RANK_MAX];
3637
- int8_t naxes = ca->ndim;
3638
- for ( int8_t k = 0; k < naxes; k++ ) slab_axes[k] = k;
3639
-
3640
- ca_iter_state st_in, st_out;
3641
- int rc;
3642
- rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,
3643
- slab_axes, naxes, 0);
3644
- if ( rc != CA_ITER_OK ) {
3645
- rb_raise(rb_eRuntimeError,
3646
- "#{name}_ki: input init failed rc=%d", rc);
3647
- }
3648
- rc = ca_iter_state_init_l2(&st_out, co, CA_SLAB_AXES,
3649
- slab_axes, naxes, CA_KERNEL_WRITE);
3650
- if ( rc != CA_ITER_OK ) {
3651
- ca_iter_state_finish(&st_in);
3652
- rb_raise(rb_eRuntimeError,
3653
- "#{name}_ki: output init failed rc=%d", rc);
3654
- }
3655
-
3656
- char *pi, *po;
3657
- boolean8_t *mi, *mo;
3658
- while ( ca_iter_state_next_slab_axes(&st_in, &pi, &mi) &&
3659
- ca_iter_state_next_slab_axes(&st_out, &po, &mo) ) {
3660
- CA_SLAB_MAP_T(#{si[:c]}, #{oi[:c]}, st_in, pi, st_out, po, #{expr});
3661
- ca_iter_state_sync_slab(&st_out);
3662
- }
3663
- ca_iter_state_finish(&st_in);
3664
- ca_iter_state_finish(&st_out);
3665
- return vout;
3666
- }
3667
- C
3668
- end
3669
-
3670
- def self.emit_map_dispatch(io, k)
3671
- name = k[:name]
3672
- io.puts
3673
- io.puts "static VALUE"
3674
- io.puts "rb_ca_#{name}_ki (VALUE self)"
3675
- io.puts "{"
3676
- io.puts " CArray *src;"
3677
- io.puts " GetCArray(self, src);"
3678
- io.puts " switch ( src->data_type ) {"
3679
- k[:source].each do |s|
3680
- si = DTYPES[s]
3681
- io.puts " case #{si[:ca]}: return #{name}_ki_native_#{s}(self, src);"
3682
- end
3683
- case k[:fallback]
3684
- when :wrap_to_f64
3685
- io.puts " default: {"
3686
- io.puts " /* Phase E: only wrap data_types that have a meaningful float64"
3687
- io.puts " representation. Complex / fixlen / object would lose"
3688
- io.puts " structure (e.g. complex -> float strips imaginary part)"
3689
- io.puts " so reject explicitly, matching legacy DataTypeError. */"
3690
- io.puts " switch ( src->data_type ) {"
3691
- io.puts " case CA_BOOLEAN: break; /* wrap: count-of-trues semantic */"
3692
- io.puts " default:"
3693
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")}, or boolean)", ca_type_name[src->data_type]);]
3694
- io.puts " }"
3695
- io.puts " VALUE vsrc = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));"
3696
- io.puts " CArray *casted;"
3697
- io.puts " GetCArray(vsrc, casted);"
3698
- io.puts " return #{name}_ki_native_f64(vsrc, casted);"
3699
- io.puts " }"
3700
- when :raise
3701
- io.puts " default:"
3702
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
3703
- end
3704
- io.puts " }"
3705
- io.puts " return Qnil; /* unreachable */"
3706
- io.puts "}"
3707
- end
3708
-
3709
3791
  # ---- scan emitter -----------------------------------------------------
3710
3792
 
3711
3793
  def self.emit_scan(io, k)
3712
3794
  io.puts
3713
3795
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
3714
3796
  k[:source].each do |src|
3715
- emit_scan_native(io, k, src)
3797
+ emit_native_for(io, k, src) { |o| emit_scan_native(o, k, src) }
3716
3798
  end
3717
3799
  emit_scan_dispatch(io, k)
3718
3800
  end
@@ -3783,14 +3865,14 @@ module MkKernel
3783
3865
  slab_axes, 1, 0);
3784
3866
  if ( rc != CA_ITER_OK ) {
3785
3867
  rb_raise(rb_eRuntimeError,
3786
- "#{name}_ki: input init failed rc=%d", rc);
3868
+ "%s: input init failed rc=%d", ca_calling_method_name(), rc);
3787
3869
  }
3788
- 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,
3789
3871
  slab_axes, 1, CA_KERNEL_WRITE);
3790
3872
  if ( rc != CA_ITER_OK ) {
3791
3873
  ca_iter_state_finish(&st_in);
3792
3874
  rb_raise(rb_eRuntimeError,
3793
- "#{name}_ki: output init failed rc=%d", rc);
3875
+ "%s: output init failed rc=%d", ca_calling_method_name(), rc);
3794
3876
  }
3795
3877
  #{mask_setup}
3796
3878
  char *pi, *po;
@@ -3815,16 +3897,38 @@ module MkKernel
3815
3897
  # multi-axis scan is semantically ambiguous (flatten-and-cumsum vs
3816
3898
  # 2D prefix sum); users must chain explicitly. Arity flips from 1
3817
3899
  # (positional Integer axis) to -1 (kwarg dispatcher).
3818
- io.puts "static VALUE"
3819
- io.puts "rb_ca_#{name}_ki (int argc, VALUE *argv, VALUE self)"
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
3820
3923
  io.puts "{"
3821
3924
  io.puts " CArray *src;"
3822
3925
  io.puts " GetCArray(self, src);"
3926
+ emit_lazy_evaluated(io)
3823
3927
  io.puts " volatile VALUE ropt = rb_pop_options(&argc, &argv);"
3824
3928
  io.puts " volatile VALUE raxis = Qnil;"
3825
3929
  io.puts " rb_scan_options(ropt, \"axis\", &raxis);"
3826
3930
  io.puts " if ( argc > 0 ) {"
3827
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: positional axis is no longer accepted (got %d); use axis: kwarg, e.g. a.#{name}(axis: 0)", argc);]
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);]
3828
3932
  io.puts " }"
3829
3933
  if k[:axis_default] == :flatten
3830
3934
  # Legacy compat: nil/omitted axis: flatten source and scan axis 0.
@@ -3836,7 +3940,7 @@ module MkKernel
3836
3940
  io.puts " switch ( fsrc->data_type ) {"
3837
3941
  k[:source].each do |s|
3838
3942
  si = DTYPES[s]
3839
- 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));"
3840
3944
  end
3841
3945
  case k[:fallback]
3842
3946
  when :wrap_to_f64
@@ -3844,34 +3948,35 @@ module MkKernel
3844
3948
  io.puts " VALUE vsrc = rb_ca_wrap_readonly(vflat, INT2NUM(CA_FLOAT64));"
3845
3949
  io.puts " CArray *casted;"
3846
3950
  io.puts " GetCArray(vsrc, casted);"
3847
- io.puts " return #{name}_ki_native_f64(vsrc, casted, 0);"
3951
+ io.puts " CA_RETURN_KEEPING(vsrc, #{name}_ki_native_f64(vsrc, casted, 0));"
3848
3952
  io.puts " }"
3849
3953
  when :raise
3850
3954
  io.puts " default:"
3851
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[fsrc->data_type]);]
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);"
3852
3960
  end
3853
3961
  io.puts " }"
3854
3962
  io.puts " }"
3855
3963
  else
3856
3964
  io.puts " if ( NIL_P(raxis) ) {"
3857
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis: kwarg is required (single axis Integer; multi-axis scan is semantically ambiguous)");]
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());]
3858
3966
  io.puts " }"
3859
3967
  end
3860
3968
  io.puts " if ( TYPE(raxis) == T_ARRAY ) {"
3861
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis: must be Integer (got Array); multi-axis scan is semantically ambiguous, chain explicitly: a.#{name}(axis: 0).#{name}(axis: 1)");]
3862
- io.puts " }"
3863
- io.puts " if ( ! rb_obj_is_kind_of(raxis, rb_cInteger) ) {"
3864
- io.puts %Q[ rb_raise(rb_eTypeError, "#{name}_ki: axis: must be Integer (got %"PRIsVALUE")", rb_obj_class(raxis));]
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());]
3865
3970
  io.puts " }"
3866
- io.puts " int axis = NUM2INT(raxis);"
3971
+ io.puts " int axis = (int) ca_axis_integer(raxis, NULL);"
3867
3972
  io.puts " if ( axis < 0 ) axis += src->ndim;"
3868
3973
  io.puts " if ( axis < 0 || axis >= src->ndim ) {"
3869
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(raxis), src->ndim);]
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);]
3870
3975
  io.puts " }"
3871
3976
  io.puts " switch ( src->data_type ) {"
3872
3977
  k[:source].each do |s|
3873
3978
  si = DTYPES[s]
3874
- 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));"
3875
3980
  end
3876
3981
  case k[:fallback]
3877
3982
  when :wrap_to_f64
@@ -3879,11 +3984,15 @@ module MkKernel
3879
3984
  io.puts " VALUE vsrc = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));"
3880
3985
  io.puts " CArray *casted;"
3881
3986
  io.puts " GetCArray(vsrc, casted);"
3882
- io.puts " return #{name}_ki_native_f64(vsrc, casted, axis);"
3987
+ io.puts " CA_RETURN_KEEPING(vsrc, #{name}_ki_native_f64(vsrc, casted, axis));"
3883
3988
  io.puts " }"
3884
3989
  when :raise
3885
3990
  io.puts " default:"
3886
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
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));"
3887
3996
  end
3888
3997
  io.puts " }"
3889
3998
  io.puts " return Qnil; /* unreachable */"
@@ -3898,7 +4007,7 @@ module MkKernel
3898
4007
  k[:source].each do |src|
3899
4008
  emit_sort_cmp(io, k, src)
3900
4009
  emit_sort_quickselect(io, k, src) if k[:algorithm] == :partition
3901
- emit_sort_native(io, k, src)
4010
+ emit_native_for(io, k, src) { |o| emit_sort_native(o, k, src) }
3902
4011
  end
3903
4012
  emit_sort_dispatch(io, k)
3904
4013
  end
@@ -4030,14 +4139,21 @@ module MkKernel
4030
4139
  # String, custom class with <=> defined) works. Raises Ruby
4031
4140
  # TypeError mid-sort if a cell pair is uncomparable -- consistent
4032
4141
  # with Array#sort behavior.
4033
- io.puts %Q{ VALUE r = rb_funcall(pa->v, rb_intern("<=>"), 1, pb->v);}
4034
- io.puts " if ( NIL_P(r) ) {"
4035
- io.puts %Q{ rb_raise(rb_eArgError, "#{name}_ki: comparison of %s with %s failed",}
4036
- io.puts " rb_obj_classname(pa->v), rb_obj_classname(pb->v));"
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;"
4037
4156
  io.puts " }"
4038
- io.puts " int c = NUM2INT(r);"
4039
- io.puts " if ( c > 0 ) return 1;"
4040
- io.puts " if ( c < 0 ) return -1;"
4041
4157
  else
4042
4158
  if is_fp
4043
4159
  # NaN policy :end -- NaN sorts to the end (matches qcmp_f_type in
@@ -4168,19 +4284,32 @@ module MkKernel
4168
4284
  io.puts " rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,"
4169
4285
  io.puts " slab_axes, 1, 0);"
4170
4286
  io.puts " if ( rc != CA_ITER_OK ) {"
4171
- 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);]
4172
4288
  io.puts " }"
4173
- 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,"
4174
4290
  io.puts " slab_axes, 1, CA_KERNEL_WRITE);"
4175
4291
  io.puts " if ( rc != CA_ITER_OK ) {"
4176
4292
  io.puts " ca_iter_state_finish(&st_in);"
4177
- 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);]
4178
4294
  io.puts " }"
4179
4295
  io.puts
4180
4296
  io.puts " ca_size_t fiber_n = st_in.slab_elements;"
4181
- io.puts " #{name}_pair_#{src} *buf ="
4182
- io.puts " xmalloc(sizeof(#{name}_pair_#{src}) * (size_t) fiber_n);"
4183
- if has_kind
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
4184
4313
  # PROPOSAL_PORTABLE_TEXTBOOK_SORT §9.5.2 Option γ: aux pair buffer
4185
4314
  # for ca_sort_merge_pair_<src> (= ping-pong bottom-up mergesort).
4186
4315
  # Allocated once outside the fiber loop, reused across fibers.
@@ -4325,16 +4454,15 @@ module MkKernel
4325
4454
  io.puts " */"
4326
4455
  end
4327
4456
  if is_object
4328
- # PROPOSAL_MKKERNEL_OBJECT_DTYPE_BRANCH Phase 2: CA_OBJECT routes to
4329
- # libc qsort with the emitted _cmp_object (rb_funcall(<=>) based).
4330
- # qsort isn't stable, but our cmp tie-breaks on the original index,
4331
- # so the result is effectively stable. do_stable is ignored: both
4332
- # paths use the same qsort + cmp here. NaN partition is skipped
4333
- # (Ruby Float NaN handling deferred -- cells of type Float with NaN
4334
- # would compare via Float#<=> which returns nil for NaN, raising
4335
- # ArgumentError in the cmp helper).
4336
- io.puts " (void) do_stable; /* CA_OBJECT: qsort handles both stable / quick */"
4337
- io.puts " qsort(#{buf_expr}, (size_t) #{sort_n_expr}, sizeof(#{name}_pair_#{src}), #{name}_cmp_#{src});"
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});"
4338
4466
  elsif is_fixlen
4339
4467
  io.puts " /* CA_FIXLEN: libc qsort with the memcmp comparator. qsort isn't"
4340
4468
  io.puts " stable, but #{name}_cmp_#{src} tie-breaks on the original index,"
@@ -4381,7 +4509,7 @@ module MkKernel
4381
4509
  io.puts " if ( r > 0 ) {"
4382
4510
  io.puts " if ( dense ) {"
4383
4511
  if is_object
4384
- 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++;"
4385
4513
  elsif is_fixlen
4386
4514
  io.puts " if ( memcmp(buf[r].vp, buf[r - 1].vp, (size_t) buf[r].nb) != 0 ) out_r++;"
4387
4515
  else
@@ -4427,9 +4555,14 @@ module MkKernel
4427
4555
  end
4428
4556
 
4429
4557
  io.puts " }"
4430
- io.puts " xfree(buf);"
4431
- if has_kind
4432
- 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
4433
4566
  end
4434
4567
  io.puts " ca_iter_state_finish(&st_in);"
4435
4568
  io.puts " ca_iter_state_finish(&st_out);"
@@ -4451,10 +4584,7 @@ module MkKernel
4451
4584
  io.puts " CArray *_sc;"
4452
4585
  io.puts " GetCArray(_st, _sc);"
4453
4586
  io.puts " if ( _sc->data_type != CA_FIXLEN && ! _orderable ) {"
4454
- io.puts %Q[ rb_raise(rb_eArgError,]
4455
- io.puts %Q[ "#{name}_ki: Face-typed input (%s) is not orderable "]
4456
- io.puts %Q[ "by storage; use ca.parent to descend to storage",]
4457
- io.puts %Q[ rb_obj_classname(_face));]
4587
+ io.puts %Q[ ca_face_refuse_order(ca_calling_method_name(), _face, "order");]
4458
4588
  io.puts " }"
4459
4589
  io.puts " self = _st;"
4460
4590
  io.puts " src = _sc;"
@@ -4477,10 +4607,7 @@ module MkKernel
4477
4607
  io.puts " CArray *_sc;"
4478
4608
  io.puts " GetCArray(_st, _sc);"
4479
4609
  io.puts " if ( _sc->data_type != CA_FIXLEN && ! _orderable ) {"
4480
- io.puts %Q[ rb_raise(rb_eArgError,]
4481
- io.puts %Q[ "#{name}_ki: Face-typed input (%s) is not orderable "]
4482
- io.puts %Q[ "by storage; use ca.parent to descend to storage",]
4483
- io.puts %Q[ rb_obj_classname(_face));]
4610
+ io.puts %Q[ ca_face_refuse_order(ca_calling_method_name(), _face, "order");]
4484
4611
  io.puts " }"
4485
4612
  if relift
4486
4613
  io.puts " _reduce_face = _face;"
@@ -4491,32 +4618,37 @@ module MkKernel
4491
4618
  end
4492
4619
 
4493
4620
  # Re-lift a value-returning reduction result back into the Face captured by
4494
- # emit_reduce_face_gate. Full-reduction scalar -> storage_to_scalar hook;
4495
- # per-axis CArray -> rb_ca_face_template (carries the subclass tail).
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).
4496
4623
  # All-masked full reductions leave CA_UNDEF untouched.
4497
4624
  def self.emit_reduce_face_relift(io)
4498
- io.puts " if ( _reduce_face != Qnil && result != CA_UNDEF && result != Qnil ) {"
4499
- io.puts " if ( rb_obj_is_kind_of(result, rb_cCArray) ) {"
4500
- io.puts " /* per-axis: re-lift the storage result as a Face of the same"
4501
- io.puts " subclass; the template memcpy carries the subclass tail. */"
4502
- io.puts " CArray *_rca;"
4503
- io.puts " GetCArray(result, _rca);"
4504
- io.puts " VALUE _lifted = rb_ca_face_template(_reduce_face, _rca, _rca->dim);"
4505
- io.puts " rb_ca_set_parent(_lifted, result); /* pin storage result as GC root */"
4506
- io.puts " if ( ca_has_mask(_rca) ) {"
4507
- io.puts " /* rb_ca_face_template resets mask to NULL; carry the reduced"
4508
- io.puts " mask bits (all-masked slabs) onto the lifted Face. */"
4509
- io.puts " CArray *_lca;"
4510
- io.puts " GetCArray(_lifted, _lca);"
4511
- io.puts " ca_copy_mask(_lca, _rca);"
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);"
4633
+ io.puts " }"
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);"
4512
4638
  io.puts " }"
4513
- io.puts " result = _lifted;"
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);"
4514
4646
  io.puts " }"
4515
4647
  io.puts " else {"
4516
4648
  io.puts " /* full reduction: decode the scalar via the registered hook. */"
4517
4649
  io.puts " CArray *_fca;"
4518
4650
  io.puts " GetCArray(_reduce_face, _fca);"
4519
- 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);"
4520
4652
  io.puts " }"
4521
4653
  io.puts " }"
4522
4654
  end
@@ -4528,7 +4660,7 @@ module MkKernel
4528
4660
  has_kind = !partition # :full and :rank gain kind: dispatch (Option γ)
4529
4661
  mask_sentinel = (k[:mask_self] == :sentinel)
4530
4662
  # bind_ruby: false kernels are consumed by other .c files at the C
4531
- # level (= rb_ca_sorted_view in carray_order.c calls rb_ca_sort_addr_ki).
4663
+ # level (carray_sort.c / carray_partition.c call the _mp twins).
4532
4664
  # Emit extern linkage so the symbol is reachable across translation
4533
4665
  # units. bind_ruby: true kernels stay static (only referenced by
4534
4666
  # Init_carray_kernels in this file).
@@ -4540,6 +4672,10 @@ module MkKernel
4540
4672
  # partition_index / rank_index, which are consumed at the C level
4541
4673
  # by carray_order.c (= ordering helpers like nlargest, order).
4542
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)
4543
4679
  # PROPOSAL_PORTABLE_TEXTBOOK_SORT §9.5.2 Option γ:
4544
4680
  # For algorithm :full / :rank, dispatch emits THREE C-level entries:
4545
4681
  # rb_ca_<name>_ki_quick (self, vaxis) -> do_stable=0
@@ -4576,6 +4712,7 @@ module MkKernel
4576
4712
  io.puts "{"
4577
4713
  io.puts " CArray *src;"
4578
4714
  io.puts " GetCArray(self, src);"
4715
+ emit_lazy_evaluated(io)
4579
4716
  io.puts " /* Face gate: descend to storage (fixlen -> memcmp default,"
4580
4717
  io.puts " numeric -> ORDERABLE required). The strip also makes the"
4581
4718
  io.puts " switch below and every index/addr template output build on"
@@ -4585,23 +4722,24 @@ module MkKernel
4585
4722
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
4586
4723
  io.puts " masked elements would break the ordering invariant. */"
4587
4724
  io.puts " if ( ca_has_mask(src) ) {"
4588
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: masked input not supported (use ca.value or ca.strip_mask(fill))");]
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());]
4589
4726
  io.puts " }"
4590
4727
  end
4591
- io.puts " int axis = NUM2INT(vaxis);"
4728
+ io.puts " int axis = (int) ca_axis_integer(vaxis, NULL);"
4592
4729
  io.puts " if ( axis < 0 ) axis += src->ndim;"
4593
4730
  io.puts " if ( axis < 0 || axis >= src->ndim ) {"
4594
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(vaxis), src->ndim);]
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);]
4595
4732
  io.puts " }"
4596
4733
  io.puts " switch ( src->data_type ) {"
4597
4734
  k[:source].each do |s|
4598
4735
  si = DTYPES[s]
4599
4736
  native_extra = mask_sentinel ? ", masked_last" : ""
4600
4737
  native_extra += rank ? ", dense" : ""
4601
- 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}));"
4602
4739
  end
4603
4740
  io.puts " default:"
4604
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
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]);]
4605
4743
  io.puts " }"
4606
4744
  io.puts " return Qnil; /* unreachable */"
4607
4745
  io.puts "}"
@@ -4614,30 +4752,33 @@ module MkKernel
4614
4752
  dispatch_call_last = mask_sentinel ? ", 1" : ""
4615
4753
  dispatch_call_last += rank ? ", 0" : ""
4616
4754
 
4617
- # _ki_quick: arity 2 (self, vaxis). Linkage = storage.
4618
- io.puts
4619
- io.puts "#{storage}VALUE"
4620
- io.puts "rb_ca_#{name}_ki_quick (VALUE self, VALUE vaxis)"
4621
- io.puts "{"
4622
- io.puts " return #{name}_ki_dispatch(self, vaxis, 0#{dispatch_call_last});"
4623
- io.puts "}"
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 "}"
4624
4763
 
4625
- # _ki_stable: arity 2 (self, vaxis). Linkage = storage.
4626
- io.puts
4627
- io.puts "#{storage}VALUE"
4628
- io.puts "rb_ca_#{name}_ki_stable (VALUE self, VALUE vaxis)"
4629
- io.puts "{"
4630
- io.puts " return #{name}_ki_dispatch(self, vaxis, 1#{dispatch_call_last});"
4631
- io.puts "}"
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 "}"
4632
4771
 
4633
- # _ki: arity 2 alias of _ki_quick (= preserves existing arity-1
4634
- # Ruby binding and existing C extern callers).
4635
- io.puts
4636
- io.puts "#{storage}VALUE"
4637
- io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis)"
4638
- io.puts "{"
4639
- io.puts " return rb_ca_#{name}_ki_quick(self, vaxis);"
4640
- io.puts "}"
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
4641
4782
 
4642
4783
  if mask_sentinel && k[:bind_ruby] == false
4643
4784
  # _mp twins: explicit masked_last, non-Ruby (plain C int, no
@@ -4659,16 +4800,16 @@ module MkKernel
4659
4800
  end
4660
4801
 
4661
4802
  if rank
4662
- # _quick_dense: explicit dense (0 :ordinal / 1 :dense), extern for
4663
- # carray_order.c's `order` method: kwarg (order is CA_INT64-family
4664
- # sugar over rank_index, defined in a different file, so it needs
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
4665
4806
  # a non-static entry point the same way the mask_self: :sentinel
4666
4807
  # kernels' _mp twins do).
4667
4808
  io.puts
4668
4809
  io.puts "VALUE"
4669
- 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)"
4670
4811
  io.puts "{"
4671
- io.puts " return #{name}_ki_dispatch(self, vaxis, 0, dense);"
4812
+ io.puts " return #{name}_ki_dispatch(self, vaxis, stable, dense);"
4672
4813
  io.puts "}"
4673
4814
  end
4674
4815
  else
@@ -4683,6 +4824,7 @@ module MkKernel
4683
4824
  io.puts "{"
4684
4825
  io.puts " CArray *src;"
4685
4826
  io.puts " GetCArray(self, src);"
4827
+ emit_lazy_evaluated(io)
4686
4828
  io.puts " /* Face gate (partition): same as the sort dispatcher --"
4687
4829
  io.puts " fixlen -> memcmp default, numeric -> ORDERABLE required. */"
4688
4830
  emit_sort_face_gate(io, name)
@@ -4690,38 +4832,42 @@ module MkKernel
4690
4832
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
4691
4833
  io.puts " masked elements would break the ordering invariant. */"
4692
4834
  io.puts " if ( ca_has_mask(src) ) {"
4693
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: masked input not supported (use ca.value or ca.strip_mask(fill))");]
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());]
4694
4836
  io.puts " }"
4695
4837
  end
4696
- io.puts " int axis = NUM2INT(vaxis);"
4838
+ io.puts " int axis = (int) ca_axis_integer(vaxis, NULL);"
4697
4839
  io.puts " if ( axis < 0 ) axis += src->ndim;"
4698
4840
  io.puts " if ( axis < 0 || axis >= src->ndim ) {"
4699
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(vaxis), src->ndim);]
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);]
4700
4842
  io.puts " }"
4701
- 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;"
4702
4845
  io.puts " ca_size_t axis_n = src->dim[axis];"
4703
4846
  io.puts " if ( kth < 0 ) kth += axis_n;"
4704
4847
  io.puts " if ( kth < 0 || kth >= axis_n ) {"
4705
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: kth %lld out of range for axis size %lld", (long long) NUM2SIZE(vkth), (long long) axis_n);]
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);]
4706
4849
  io.puts " }"
4707
4850
  io.puts " switch ( src->data_type ) {"
4708
4851
  k[:source].each do |s|
4709
4852
  si = DTYPES[s]
4710
4853
  native_extra = mask_sentinel ? ", masked_last" : ""
4711
- 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}));"
4712
4855
  end
4713
4856
  io.puts " default:"
4714
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
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]);]
4715
4859
  io.puts " }"
4716
4860
  io.puts " return Qnil; /* unreachable */"
4717
4861
  io.puts "}"
4718
4862
 
4719
- io.puts
4720
- io.puts "#{storage}VALUE"
4721
- io.puts "rb_ca_#{name}_ki (VALUE self, VALUE vaxis, VALUE vkth)"
4722
- io.puts "{"
4723
- io.puts " return #{name}_ki_dispatch(self, vaxis, vkth#{mask_sentinel ? ", 1" : ""});"
4724
- io.puts "}"
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
4725
4871
 
4726
4872
  if mask_sentinel && k[:bind_ruby] == false
4727
4873
  # _mp twin: explicit masked_last, extern for carray_partition.c's
@@ -4739,10 +4885,10 @@ module MkKernel
4739
4885
  # wrapper). When public_method: is set, emit a -1-argc
4740
4886
  # entry that parses `axis:` (and `kind:` for has_kind, and
4741
4887
  # `masked_position:` for mask_self: :sentinel, and `method:` for
4742
- # :rank) kwarg with default 0 / :quick / :last / :ordinal and
4743
- # dispatches to the appropriate positional entry. For :partition
4744
- # algorithm, kth is accepted as a single positional argument
4745
- # before the kwarg.
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.
4746
4892
  if k[:public_method]
4747
4893
  legacy_name = k[:public_method].to_s
4748
4894
  io.puts
@@ -4769,7 +4915,21 @@ module MkKernel
4769
4915
  scan_targets << "&rmasked_position" if mask_sentinel
4770
4916
  scan_targets << "&rmethod" if rank
4771
4917
  io.puts " rb_scan_options(ropt, \"#{scan_keys.join(",")}\", #{scan_targets.join(", ")});"
4772
- 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 " }"
4773
4933
  if rank
4774
4934
  # method: :ordinal (default) / :dense -> dense int. :ordinal
4775
4935
  # assigns every cell a distinct rank (ties broken by original
@@ -4777,37 +4937,15 @@ module MkKernel
4777
4937
  # the standard "dense rank" used to compose rank as a sort_addr
4778
4938
  # priority key without silently dropping lower-priority keys on
4779
4939
  # ties (see MASKED_POSITION's sibling proposal thread).
4780
- io.puts " int dense = 0; /* default method: :ordinal */"
4781
- io.puts " if ( ! NIL_P(rmethod) ) {"
4782
- io.puts " static ID sym_ordinal = 0, sym_dense = 0;"
4783
- io.puts " if ( ! sym_ordinal ) sym_ordinal = rb_intern(\"ordinal\");"
4784
- io.puts " if ( ! sym_dense ) sym_dense = rb_intern(\"dense\");"
4785
- io.puts " ID method_id = SYM2ID(rmethod);"
4786
- io.puts " if ( method_id == sym_ordinal ) dense = 0;"
4787
- io.puts " else if ( method_id == sym_dense ) dense = 1;"
4788
- io.puts " else {"
4789
- io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: unknown method %s (expected :ordinal or :dense)", rb_id2name(method_id));]
4790
- io.puts " }"
4791
- io.puts " }"
4940
+ io.puts " int dense = NIL_P(rmethod) ? 0 : ca_symbol_choice(rmethod, \"method\", \"ordinal\", \"dense\", NULL);"
4792
4941
  end
4793
4942
  if mask_sentinel
4794
4943
  # masked_position: :last (default) / :first -> masked_last int.
4795
4944
  # Shared by both the has_kind and :partition branches below.
4796
- io.puts " int masked_last = 1; /* default masked_position: :last */"
4797
- io.puts " if ( ! NIL_P(rmasked_position) ) {"
4798
- io.puts " static ID sym_first = 0, sym_last = 0;"
4799
- io.puts " if ( ! sym_first ) sym_first = rb_intern(\"first\");"
4800
- io.puts " if ( ! sym_last ) sym_last = rb_intern(\"last\");"
4801
- io.puts " ID mp_id = SYM2ID(rmasked_position);"
4802
- io.puts " if ( mp_id == sym_last ) masked_last = 1;"
4803
- io.puts " else if ( mp_id == sym_first ) masked_last = 0;"
4804
- io.puts " else {"
4805
- io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: unknown masked_position %s (expected :first or :last)", rb_id2name(mp_id));]
4806
- io.puts " }"
4807
- io.puts " }"
4945
+ io.puts " int masked_last = NIL_P(rmasked_position) ? 1 : ca_symbol_choice(rmasked_position, \"masked_position\", \"first\", \"last\", NULL);"
4808
4946
  end
4809
4947
  if partition
4810
- # partition_index(kth, axis: 0, masked_position: :last) -- kth
4948
+ # partition_index(kth, axis: nil, masked_position: :last) -- kth
4811
4949
  # positional, axis/masked_position kwargs.
4812
4950
  io.puts " if ( argc != 1 ) {"
4813
4951
  io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: wrong number of positional arguments (given %d, expected 1: kth)", argc);]
@@ -4818,28 +4956,27 @@ module MkKernel
4818
4956
  io.puts " return rb_ca_#{name}_ki(self, raxis, argv[0]);"
4819
4957
  end
4820
4958
  else
4821
- # sort_index(axis: 0, kind: :quick, masked_position: :last) --
4959
+ # sort_index(axis: nil, kind: :quick, masked_position: :last) --
4822
4960
  # no positional, kwargs only.
4823
4961
  io.puts " if ( argc != 0 ) {"
4824
4962
  io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: positional args no longer accepted (given %d args); use axis:/kind: kwargs", argc);]
4825
4963
  io.puts " }"
4826
4964
  # kind: dispatch (:quick default, :stable optional).
4827
- io.puts " int do_stable = 0;"
4828
- io.puts " if ( ! NIL_P(rkind) ) {"
4829
- io.puts " static ID sym_quick = 0, sym_stable = 0;"
4830
- io.puts " if ( ! sym_quick ) sym_quick = rb_intern(\"quick\");"
4831
- io.puts " if ( ! sym_stable ) sym_stable = rb_intern(\"stable\");"
4832
- io.puts " ID kind_id = SYM2ID(rkind);"
4833
- io.puts " if ( kind_id == sym_quick ) do_stable = 0;"
4834
- io.puts " else if ( kind_id == sym_stable ) do_stable = 1;"
4835
- io.puts " else {"
4836
- io.puts %Q[ rb_raise(rb_eArgError, "#{legacy_name}: unknown kind %s (expected :quick or :stable)", rb_id2name(kind_id));]
4837
- io.puts " }"
4838
- io.puts " }"
4965
+ io.puts " int do_stable = NIL_P(rkind) ? 0 : ca_symbol_choice(rkind, \"kind\", \"quick\", \"stable\", NULL);"
4839
4966
  if mask_sentinel
4840
4967
  io.puts " return #{name}_ki_dispatch(self, raxis, do_stable, masked_last);"
4841
4968
  elsif rank
4842
- 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;"
4843
4980
  else
4844
4981
  io.puts " return do_stable ? rb_ca_#{name}_ki_stable(self, raxis)"
4845
4982
  io.puts " : rb_ca_#{name}_ki_quick(self, raxis);"
@@ -4880,7 +5017,7 @@ module MkKernel
4880
5017
  io.puts
4881
5018
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
4882
5019
  k[:source].each do |src|
4883
- emit_search_native(io, k, src)
5020
+ emit_native_for(io, k, src) { |o| emit_search_native(o, k, src) }
4884
5021
  end
4885
5022
  emit_search_dispatch(io, k)
4886
5023
  end
@@ -4903,7 +5040,7 @@ module MkKernel
4903
5040
  # body: Hash form picks per-data_type-kind branch (= :int / :float /
4904
5041
  # :object / :fixlen); String form is uniform.
4905
5042
  # PROPOSAL_MKKERNEL_OBJECT_DTYPE_BRANCH Phase 4: :object branch added for
4906
- # CA_OBJECT (equality via rb_equal, ordering via rb_funcall(<=>)).
5043
+ # CA_OBJECT (equality via ca_obj_equal, ordering via rb_funcall(<=>)).
4907
5044
  # PROPOSAL_SEARCH_SEMANTICS_UNIFY S2: :fixlen branch added for CA_FIXLEN
4908
5045
  # (the cell is a runtime-width byte blob; comparison is memcmp over
4909
5046
  # ca->bytes, the same total order fixlen bincmp uses).
@@ -4937,7 +5074,7 @@ module MkKernel
4937
5074
  <<~C.chomp
4938
5075
  int eps_is_default = NIL_P(reps);
4939
5076
  double user_eps = eps_is_default ? 0.0 : NUM2DBL(reps);
4940
- double query_eps = eps_is_default ? (double)(#{eps_def}) * fabs((double)query_val) : user_eps;
5077
+ double query_eps = eps_is_default ? (isfinite((double)query_val) ? (double)(#{eps_def}) * fabs((double)query_val) : 0.0) : user_eps;
4941
5078
  C
4942
5079
  elsif has_eps && no_eps
4943
5080
  "(void) reps; /* #{is_fixlen ? "CA_FIXLEN" : "CA_OBJECT"}: eps ignored, exact match only */\n double query_eps = 0.0; (void) query_eps;"
@@ -4952,7 +5089,7 @@ module MkKernel
4952
5089
  ""
4953
5090
  end
4954
5091
  eps_percell = if has_eps && !no_eps
4955
- "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;"
4956
5093
  else
4957
5094
  "double query_eps = 0.0; (void) query_eps;"
4958
5095
  end
@@ -4964,14 +5101,36 @@ module MkKernel
4964
5101
  # and let rb_ca_obj2ptr pack it; `query_val` is then a char* the memcmp
4965
5102
  # body reads (mirrors the legacy flat bsearch ALLOCA_N path).
4966
5103
  query_setup_a = if is_fixlen
4967
- "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" \
4968
5106
  " rb_ca_obj2ptr(self, rval, query_val);"
4969
5107
  else
5108
+ (INT_DTYPES.include?(src) ? "ca_check_integer_fits(rval, #{si[:ca]});\n " : "") +
4970
5109
  "#{si[:c]} query_val_buf;\n" \
4971
5110
  " rb_ca_obj2ptr(self, rval, &query_val_buf);\n" \
4972
5111
  " #{si[:c]} query_val = query_val_buf;"
4973
5112
  end
4974
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
+
4975
5134
  # case B/C query coercion. numeric/object: wrap-readonly to self's
4976
5135
  # data_type when they differ. fixlen: require a CA_FIXLEN query of the
4977
5136
  # same byte width (no scalar coercion exists for blobs), so the per-cell
@@ -4981,7 +5140,7 @@ module MkKernel
4981
5140
  (void) vsrc_val; /* fixlen: no wrap-readonly coercion */
4982
5141
  if ( cv->data_type != CA_FIXLEN || cv->bytes != ca->bytes ) {
4983
5142
  rb_raise(rb_eArgError,
4984
- "#{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);
4985
5144
  }
4986
5145
  C
4987
5146
  else
@@ -5140,7 +5299,7 @@ module MkKernel
5140
5299
  slab_axes, 1, 0);
5141
5300
  if ( rc != CA_ITER_OK ) {
5142
5301
  rb_raise(rb_eRuntimeError,
5143
- "#{name}_ki: kernel_iterator init failed rc=%d", rc);
5302
+ "%s: kernel_iterator init failed rc=%d", ca_calling_method_name(), rc);
5144
5303
  }
5145
5304
  C
5146
5305
 
@@ -5222,7 +5381,7 @@ module MkKernel
5222
5381
  # transformation in two more inner loops.
5223
5382
  io.puts <<~C
5224
5383
  /* view_flat semantics + CArray val: NotImpError (deferred). */
5225
- 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());
5226
5385
  C
5227
5386
  end
5228
5387
 
@@ -5245,7 +5404,9 @@ module MkKernel
5245
5404
  */
5246
5405
  CArray *cv;
5247
5406
  GetCArray(rval, cv);
5248
- 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;
5249
5410
  #{coerce_bc}
5250
5411
 
5251
5412
  /* base_shape = self.shape with axis removed. base_to_self[bk] =
@@ -5280,13 +5441,13 @@ module MkKernel
5280
5441
  if ( i == axis ) continue;
5281
5442
  if ( cv->dim[i] != ca->dim[i] ) {
5282
5443
  rb_raise(rb_eArgError,
5283
- "#{name}_ki: val shape mismatch (A3 candidate, axis %d non-target dim[%d] = %ld != self.dim[%d] = %ld; expected scalar / [M] / base_shape (ndim>=2) / self.shape with axis dim free)",
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(),
5284
5445
  axis, (int) i, (long) cv->dim[i], (int) i, (long) ca->dim[i]);
5285
5446
  }
5286
5447
  }
5287
5448
  if ( cv->dim[axis] < 1 ) {
5288
5449
  rb_raise(rb_eArgError,
5289
- "#{name}_ki: empty query (val.dim[%d] = 0)", axis);
5450
+ "%s: empty query (val.dim[%d] = 0)", ca_calling_method_name(), axis);
5290
5451
  }
5291
5452
  cv_form = CV_FORM_A3;
5292
5453
  cv_form_set = 1;
@@ -5296,7 +5457,7 @@ module MkKernel
5296
5457
  /* A2: 1-D shared M-query (M >= 1); placed above A2.5 in rev5 */
5297
5458
  if ( cv->dim[0] < 1 ) {
5298
5459
  rb_raise(rb_eArgError,
5299
- "#{name}_ki: empty query (val.dim[0] = 0)");
5460
+ "%s: empty query (val.dim[0] = 0)", ca_calling_method_name());
5300
5461
  }
5301
5462
  cv_form = CV_FORM_A2;
5302
5463
  cv_form_set = 1;
@@ -5317,7 +5478,7 @@ module MkKernel
5317
5478
 
5318
5479
  if ( ! cv_form_set ) {
5319
5480
  rb_raise(rb_eArgError,
5320
- "#{name}_ki: val shape not accepted (val.ndim = %d; expected scalar / [M] / base_shape (ndim>=2, = %d) / self.shape with axis dim free (ndim = %d))",
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(),
5321
5482
  (int) cv->ndim, (int) base_ndim, (int) ca->ndim);
5322
5483
  }
5323
5484
 
@@ -5352,9 +5513,9 @@ module MkKernel
5352
5513
  #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
5353
5514
  #{op_mask_decl}
5354
5515
 
5516
+ #{entity_bc}
5355
5517
  /* Attach self + val; rev4 uses raw row-major byte addressing. */
5356
- ca_attach(ca);
5357
- ca_attach(cv);
5518
+ ca_attach_n(2, ca, cv); /* window: numeric bodies do not raise; the object lane reads entities */
5358
5519
 
5359
5520
  /* self_byte_stride[j] = bytes to advance along self axis j (row-major). */
5360
5521
  ca_size_t self_byte_stride[CA_RANK_MAX];
@@ -5482,25 +5643,18 @@ module MkKernel
5482
5643
  io.puts "{"
5483
5644
  io.puts " CArray *src;"
5484
5645
  io.puts " VALUE self_ref = self; /* pre-strip Face, for to_comparable */"
5485
- io.puts " int self_face_comparable = 0;"
5486
5646
  io.puts " int self_was_face = 0;"
5487
5647
  io.puts " GetCArray(self, src);"
5648
+ emit_lazy_evaluated(io)
5488
5649
  io.puts " /* Face gate (self): descend a Face reference to storage so the"
5489
5650
  io.puts " search runs on the numeric storage. ORDERABLE licenses the"
5490
- io.puts " ordered/searchable descent; COMPARABLE additionally licenses"
5491
- io.puts " stripping a Face query for direct comparison (see the query"
5492
- io.puts " gate below). A unit-bearing Face (CATime) is ORDERABLE"
5493
- io.puts " but not COMPARABLE, so its query is reconciled by to_comparable"
5494
- io.puts " -- no per-Face Ruby override needed. */"
5651
+ io.puts " ordered/searchable descent; the query gate below brings the"
5652
+ io.puts " query into the same space. */"
5495
5653
  io.puts " if ( ca_is_face(src) ) {"
5496
5654
  io.puts " if ( ! ca_test_flag(src, CA_FLAG_FACE_ORDERABLE_STORAGE) ) {"
5497
- io.puts %Q[ rb_raise(rb_eArgError,]
5498
- io.puts %Q[ "#{name}_ki: Face-typed input (%s) is not orderable "]
5499
- io.puts %Q[ "by storage; use ca.parent to descend to storage",]
5500
- io.puts %Q[ rb_obj_classname(self));]
5655
+ io.puts %Q[ ca_face_refuse_order(ca_calling_method_name(), self, "order");]
5501
5656
  io.puts " }"
5502
5657
  io.puts " self_was_face = 1;"
5503
- io.puts " self_face_comparable = ca_test_flag(src, CA_FLAG_FACE_COMPARABLE_STORAGE);"
5504
5658
  io.puts " self = rb_ca_strip_face_value(self);"
5505
5659
  io.puts " GetCArray(self, src);"
5506
5660
  io.puts " }"
@@ -5514,64 +5668,61 @@ module MkKernel
5514
5668
  io.puts " VALUE raxis = argv[1];"
5515
5669
  io.puts " VALUE reps = (argc >= 3) ? argv[2] : Qnil;"
5516
5670
  end
5517
- io.puts " /* Query gate (generic, PROPOSAL_FACE_ORDERING_GATE + reference"
5518
- io.puts " flip PROPOSAL_TO_COMPARABLE_RECEIVER_FLIP): the reference Face"
5519
- io.puts " reconciles the query into its own space. A COMPARABLE self is"
5520
- io.puts " directly comparable -> strip a Face query, take a plain query"
5521
- io.puts " as-is. A non-COMPARABLE Face reference calls its own"
5522
- io.puts " to_comparable(query) for ANY query type (Face CArray, our"
5523
- io.puts " Scalar, a Ruby Time / DateTime, ...), then strips; the"
5524
- io.puts " reference raises if it cannot reconcile the query. Putting the"
5525
- io.puts " conversion on the reference (always one of our classes) means a"
5526
- io.puts " foreign query (a core Time) needs no monkey-patch, and a new"
5527
- io.puts " Face author writes one to_comparable, not one per query type. */"
5528
- io.puts " {"
5529
- io.puts " int rval_is_face = 0;"
5530
- io.puts " if ( rb_obj_is_kind_of(rval, rb_cCArray) ) {"
5531
- io.puts " CArray *rv_ca;"
5532
- io.puts " TypedData_Get_Struct(rval, CArray, &carray_data_type, rv_ca);"
5533
- io.puts " rval_is_face = ca_is_face(rv_ca);"
5534
- io.puts " }"
5535
- io.puts " if ( self_face_comparable ) {"
5536
- io.puts " if ( rval_is_face ) {"
5537
- io.puts %Q[ rval = ca_face_operand_descend(rval, "#{name}_ki");]
5538
- io.puts " }"
5539
- io.puts " } else if ( self_was_face ) {"
5540
- io.puts " if ( rb_respond_to(self_ref, rb_intern(\"to_comparable\")) ) {"
5541
- io.puts " rval = rb_funcall(self_ref, rb_intern(\"to_comparable\"), 1, rval);"
5542
- io.puts " rval = rb_ca_strip_face_value(rval);"
5543
- io.puts " } else {"
5544
- io.puts %Q[ rb_raise(rb_eArgError,]
5545
- io.puts %Q[ "#{name}_ki: non-comparable Face reference (%s) has no "]
5546
- io.puts %Q[ "to_comparable to reconcile the query; use ca.parent "]
5547
- io.puts %Q[ "to search the hidden storage explicitly",]
5548
- io.puts %Q[ rb_obj_classname(self_ref));]
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);"
5549
5695
  io.puts " }"
5550
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 " }"
5551
5701
  io.puts " }"
5552
5702
 
5553
5703
  case k[:mask_self]
5554
5704
  when :raise
5555
5705
  io.puts " /* mask_self: :raise -- global reject if self has any masked element. */"
5556
5706
  io.puts " if ( ca_is_any_masked(src) ) {"
5557
- io.puts %Q[ rb_raise(rb_eRuntimeError, "#{name}_ki: self should not have any masked elements");]
5707
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: self should not have any masked elements", ca_calling_method_name());]
5558
5708
  io.puts " }"
5559
5709
  when :skip, :ignore
5560
5710
  # body handles mask_in or ignores it
5561
5711
  end
5562
5712
 
5563
- io.puts " int axis = NUM2INT(raxis);"
5713
+ io.puts " int axis = (int) ca_axis_integer(raxis, NULL);"
5564
5714
  io.puts " if ( axis < 0 ) axis += src->ndim;"
5565
5715
  io.puts " if ( axis < 0 || axis >= src->ndim ) {"
5566
- io.puts %Q[ rb_raise(rb_eArgError, "#{name}_ki: axis %d out of range for ndim %d", NUM2INT(raxis), src->ndim);]
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);]
5567
5717
  io.puts " }"
5568
5718
  io.puts " switch ( src->data_type ) {"
5569
5719
  k[:source].each do |s|
5570
5720
  si = DTYPES[s]
5571
- 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}));"
5572
5722
  end
5573
5723
  io.puts " default:"
5574
- io.puts %Q[ rb_raise(rb_eCADataTypeError, "#{name}_ki: source data_type :%s not supported (expected one of: #{k[:source].join(", ")})", ca_type_name[src->data_type]);]
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]);]
5575
5726
  io.puts " }"
5576
5727
  io.puts " return Qnil; /* unreachable */"
5577
5728
  io.puts "}"
@@ -5821,11 +5972,15 @@ module MkKernel
5821
5972
  int8_t in_dt;
5822
5973
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca);
5823
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
+ }
5824
5979
  if ( ca_monop_#{name}_out_data_type[in_dt] != in_dt ) {
5825
- rb_raise(rb_eRuntimeError,
5826
- "#{name}!: in-place form is not available when output "
5827
- "data_type differs from input (input data_type %d -> output data_type %d); "
5828
- "use non-bang form", in_dt, ca_monop_#{name}_out_data_type[in_dt]);
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]]);
5829
5984
  }
5830
5985
  return rb_ca_call_monop_bang(self, ca_monop_#{name});
5831
5986
  }
@@ -6079,6 +6234,10 @@ module MkKernel
6079
6234
  io.print <<~C
6080
6235
  VALUE rb_ca_#{name} (VALUE self, VALUE other2, VALUE other3)
6081
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
6082
6241
 
6083
6242
  VALUE rb_ca_#{name}_bang (VALUE self, VALUE other2, VALUE other3)
6084
6243
  { return rb_ca_call_triop_bang(self, other2, other3, ca_triop_#{name}); }
@@ -6350,15 +6509,56 @@ module MkKernel
6350
6509
  # caller substitutes its own operands. Object-lane bodies are left out,
6351
6510
  # since they call back into the interpreter and cannot be compiled apart
6352
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.
6353
6515
  # ---------------------------------------------------------------------
6354
6516
 
6355
6517
  BODY_TABLE_DTYPES = %i[i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6356
6518
  bool cmplx64 cmplx128].freeze
6357
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
+
6358
6558
  def self.body_table_rows
6359
- rows = []
6559
+ rows = EXTRA_BODY_ROWS.map(&:dup) + cast_body_rows
6360
6560
  KERNELS.each do |k|
6361
- next unless %i[monop binop triop].include?(k[:kind])
6561
+ next unless %i[monop binop triop moncmp bincmp].include?(k[:kind])
6362
6562
  BODY_TABLE_DTYPES.each do |src|
6363
6563
  body = monop_expr_for(k, src)
6364
6564
  next unless body.is_a?(String)
@@ -6370,8 +6570,10 @@ module MkKernel
6370
6570
  rows
6371
6571
  end
6372
6572
 
6573
+ # The block forms keep gsub from reading the backslashes in the
6574
+ # replacement as references to the match.
6373
6575
  def self.c_string_literal(text)
6374
- '"' + text.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", '\\n') + '"'
6576
+ '"' + text.gsub("\\") { "\\\\" }.gsub('"') { '\\"' }.gsub("\n") { '\\n' } + '"'
6375
6577
  end
6376
6578
 
6377
6579
  def self.emit_kernel_bodies(io)
@@ -6420,6 +6622,17 @@ module MkKernel
6420
6622
  }
6421
6623
  return Qnil;
6422
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
+ }
6423
6636
  C
6424
6637
  end
6425
6638
 
@@ -6441,6 +6654,8 @@ module MkKernel
6441
6654
  end
6442
6655
  io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6443
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);"
6444
6659
  io.puts "}"
6445
6660
  end
6446
6661
 
@@ -6455,6 +6670,8 @@ module MkKernel
6455
6670
  KERNELS.each { |k| emit_init_line(io, k) }
6456
6671
  io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6457
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);"
6458
6675
  io.puts "}"
6459
6676
  end
6460
6677
 
@@ -6515,7 +6732,6 @@ module MkKernel
6515
6732
  end
6516
6733
  arity = case k[:kind]
6517
6734
  when :reduce then -1 # axis: kwarg + optional weights/value positional
6518
- when :map then 0 # no args
6519
6735
  when :scan then -1 # axis: kwarg (Integer required)
6520
6736
  when :sort
6521
6737
  # SO.3+ (rev8): :partition algorithm takes (axis, kth) = arity 2
@@ -6540,7 +6756,7 @@ module MkKernel
6540
6756
  # reduce: default false (E.8 retire), opt-in via bind_ruby: true
6541
6757
  # (used by CF.1 count_equal smoke + future kernel testing)
6542
6758
  # sort: default true, opt-out via bind_ruby: false
6543
- # map/scan: always bound (no opt-out)
6759
+ # scan: always bound (no opt-out)
6544
6760
  bind = case k[:kind]
6545
6761
  when :reduce then k[:bind_ruby] == true
6546
6762
  when :sort then k[:bind_ruby] != false
@@ -6642,7 +6858,7 @@ MkKernel.reduce :min,
6642
6858
  # bool acc is u64 (numeric output); cast the boolean8_t
6643
6859
  # load to match and avoid a signed/unsigned compare.
6644
6860
  bool: "acc = ((uint64_t) v < acc) ? (uint64_t) v : acc",
6645
- object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v;' },
6861
+ object: 'if (acc == Qundef || ca_obj_extremum_replaces(v, acc, rb_intern("<"))) acc = v;' },
6646
6862
  reduction_kind: :min, # SL.1.2
6647
6863
  all_nan_result: :nan,
6648
6864
  # CA_FIXLEN: memcmp lexicographic min (byte order == the fixlen sort
@@ -6655,8 +6871,8 @@ MkKernel.reduce :min,
6655
6871
  fallback: :raise,
6656
6872
  mask_policy: :min_count, # Phase E
6657
6873
  # An ORDERABLE Face descends to numeric storage, then the result is
6658
- # re-lifted into the Face (scalar via storage_to_scalar, per-axis via
6659
- # rb_ca_face_template). This replaces per-Face lib overrides.
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.
6660
6876
  face_gate: :relift,
6661
6877
  public_method: true # Phase E: rebind "min" -> rb_ca_min_ki
6662
6878
 
@@ -6667,7 +6883,7 @@ MkKernel.reduce :max,
6667
6883
  # 0/1); the boolean-returning twin is `any` (= bool max).
6668
6884
  reduce: { numeric: "acc = (v > acc) ? v : acc",
6669
6885
  bool: "acc = ((uint64_t) v > acc) ? (uint64_t) v : acc",
6670
- object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v;' },
6886
+ object: 'if (acc == Qundef || ca_obj_extremum_replaces(v, acc, rb_intern(">"))) acc = v;' },
6671
6887
  reduction_kind: :max, # SL.1.2
6672
6888
  all_nan_result: :nan,
6673
6889
  # CA_FIXLEN: memcmp lexicographic max (byte order == the fixlen sort order).
@@ -6680,20 +6896,6 @@ MkKernel.reduce :max,
6680
6896
  face_gate: :relift,
6681
6897
  public_method: true # Phase E: rebind "max" -> rb_ca_max_ki
6682
6898
 
6683
- # count_ki — count unmasked elements per slab. The macro skips masked
6684
- # cells, so REDUCE fires only on contributing cells; `acc += 1` ignores
6685
- # the value entirely (cast to void to silence unused-variable warnings).
6686
- # When the input has no mask the macro takes a faster path that loads
6687
- # `v` but our REDUCE doesn't use it -- compiler optimises the load away.
6688
- MkKernel.reduce :count,
6689
- init: "0",
6690
- reduce: "(void)v; acc += 1",
6691
- reduction_kind: :plus, # SL.1.4
6692
- source: MkKernel::ALL_NUMERIC,
6693
- output: :i64,
6694
- ruby_scalar: :LL2NUM,
6695
- fallback: :wrap_to_f64
6696
-
6697
6899
  # all / any reduce: bool input -> bool output, axis-aware. Replaces the
6698
6900
  # flat-only hand-written rb_ca_all_equal_p / rb_ca_any_equal_p /
6699
6901
  # all_close? / any_close? / all_equiv? / any_equiv? in carray_stat.c
@@ -6710,6 +6912,15 @@ MkKernel.reduce :count,
6710
6912
  # modern CPUs. For dramatically lopsided inputs (= mostly-false `all`
6711
6913
  # or mostly-true `any`) a profile-driven early-break variant can be
6712
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
+
6713
6924
  MkKernel.header_block <<~C
6714
6925
  /* BOOL2VAL: bool -> Ruby (Qtrue/Qfalse). Used as ruby_scalar wrapper
6715
6926
  for the all/any flat-reduction Ruby surface so `a.all` / `a.any`
@@ -6893,7 +7104,7 @@ MkKernel.reduce :minmax,
6893
7104
  # CA_OBJECT minmax via Qundef sentinel + first-cell-init.
6894
7105
  # One sentinel guards both lo + hi (they go Qundef
6895
7106
  # together on init, get set together on first reduce).
6896
- object: 'if (lo == Qundef) { lo = v; hi = v; } else { if (RTEST(rb_funcall(v, rb_intern("<"), 1, lo))) lo = v; if (RTEST(rb_funcall(v, rb_intern(">"), 1, hi))) hi = v; }' },
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; }' },
6897
7108
  outputs: 2,
6898
7109
  all_nan_result: :nan,
6899
7110
  finish: { min: "lo", max: "hi" },
@@ -6904,6 +7115,7 @@ MkKernel.reduce :minmax,
6904
7115
  ruby_scalar: :auto,
6905
7116
  fallback: :raise,
6906
7117
  mask_policy: :min_count, # FM.1.5: match existing min / max behaviour
7118
+ face_gate: :relift,
6907
7119
  public_method: :minmax # bind public name `minmax` -> rb_ca_minmax_ki
6908
7120
  # (= surface-name override, no actual legacy)
6909
7121
 
@@ -6925,14 +7137,19 @@ MkKernel.reduce :minmax,
6925
7137
  # replace the legacy `min_addr` / `max_addr` retired in E.7 stat_proc
6926
7138
  # retire (commit f5c7ecd). 3.0 breaking: name change from `*_addr` to
6927
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).
6928
7145
 
6929
7146
  MkKernel.reduce :argmin,
6930
7147
  state: { best_v: :load_type, best_i: :int64_t },
6931
7148
  init: { best_v: { numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qundef" },
6932
- best_i: "0" },
6933
- reduce: { numeric: "if (v < best_v) { best_v = v; best_i = idx; }",
6934
- bool: "if (v < best_v) { best_v = v; best_i = idx; }",
6935
- object: 'if (best_v == Qundef) { best_v = v; best_i = idx; } else if (RTEST(rb_funcall(v, rb_intern("<"), 1, best_v))) { best_v = v; best_i = idx; }' },
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; }' },
6936
7153
  finish: "best_i",
6937
7154
  # CA_FIXLEN: position of the memcmp-lexicographic min (first-wins on ties).
6938
7155
  fixlen: :argmin,
@@ -6950,10 +7167,10 @@ MkKernel.reduce :argmin,
6950
7167
  MkKernel.reduce :argmax,
6951
7168
  state: { best_v: :load_type, best_i: :int64_t },
6952
7169
  init: { best_v: { numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qundef" },
6953
- best_i: "0" },
6954
- reduce: { numeric: "if (v > best_v) { best_v = v; best_i = idx; }",
6955
- bool: "if (v > best_v) { best_v = v; best_i = idx; }",
6956
- object: 'if (best_v == Qundef) { best_v = v; best_i = idx; } else if (RTEST(rb_funcall(v, rb_intern(">"), 1, best_v))) { best_v = v; best_i = idx; }' },
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; }' },
6957
7174
  finish: "best_i",
6958
7175
  # CA_FIXLEN: position of the memcmp-lexicographic max (first-wins on ties).
6959
7176
  fixlen: :argmax,
@@ -6995,10 +7212,10 @@ MkKernel.reduce :argmax,
6995
7212
  MkKernel.reduce :argmin_addr,
6996
7213
  state: { best_v: :load_type, best_i: :int64_t },
6997
7214
  init: { best_v: { numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qundef" },
6998
- best_i: "0" },
6999
- reduce: { numeric: "if (v < best_v) { best_v = v; best_i = idx; }",
7000
- bool: "if (v < best_v) { best_v = v; best_i = idx; }",
7001
- object: 'if (best_v == Qundef) { best_v = v; best_i = idx; } else if (RTEST(rb_funcall(v, rb_intern("<"), 1, best_v))) { best_v = v; best_i = idx; }' },
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; }' },
7002
7219
  finish: "best_i",
7003
7220
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7004
7221
  output: :i64,
@@ -7013,10 +7230,10 @@ MkKernel.reduce :argmin_addr,
7013
7230
  MkKernel.reduce :argmax_addr,
7014
7231
  state: { best_v: :load_type, best_i: :int64_t },
7015
7232
  init: { best_v: { numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qundef" },
7016
- best_i: "0" },
7017
- reduce: { numeric: "if (v > best_v) { best_v = v; best_i = idx; }",
7018
- bool: "if (v > best_v) { best_v = v; best_i = idx; }",
7019
- object: 'if (best_v == Qundef) { best_v = v; best_i = idx; } else if (RTEST(rb_funcall(v, rb_intern(">"), 1, best_v))) { best_v = v; best_i = idx; }' },
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; }' },
7020
7237
  finish: "best_i",
7021
7238
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7022
7239
  output: :i64,
@@ -7028,36 +7245,6 @@ MkKernel.reduce :argmax_addr,
7028
7245
  all_nan_result: :undef,
7029
7246
  public_method: :max_addr
7030
7247
 
7031
- # ---- mask_policy demos ------------------------------------------------
7032
-
7033
- # sum_strict_ki -- :strict policy. Any masked input cell makes the
7034
- # output cell UNDEF. Use case: "this is sensor data, any gap invalidates
7035
- # the aggregate" (= NaN-propagation semantics). Sibling of
7036
- # sum_ki which silently treats masked as 0.
7037
- MkKernel.reduce :sum_strict,
7038
- init: "0",
7039
- reduce: "acc += v",
7040
- source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7041
- output: { numeric: :f64, complex: :cmplx128 },
7042
- ruby_scalar: :auto,
7043
- fallback: :raise,
7044
- mask_policy: :strict
7045
-
7046
- # mean_safe_ki -- :all_masked policy. Output UNDEF only when every
7047
- # input cell is masked, otherwise mean over visible cells. Use case:
7048
- # "tolerate gaps but flag if nothing is observed". Sibling of mean_ki
7049
- # which returns 0.0 for all-masked slabs.
7050
- MkKernel.reduce :mean_safe,
7051
- state: { acc: :acc_type, cnt: :int64_t },
7052
- init: { acc: "0", cnt: "0" },
7053
- reduce: "(acc += v, cnt++)",
7054
- finish: "cnt ? acc / (T_OUT) cnt : 0",
7055
- source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7056
- output: { numeric: :f64, complex: :cmplx128 },
7057
- ruby_scalar: :auto,
7058
- fallback: :raise,
7059
- mask_policy: :all_masked
7060
-
7061
7248
  # accumulate -- sum that preserves input data_type (legacy rb_ca_accum,
7062
7249
  # retired in E.7). Same machinery as sum_ki but output: :preserve so
7063
7250
  # int8 stays int8 (will overflow on long arrays = legacy behaviour).
@@ -7153,10 +7340,11 @@ MkKernel.reduce :count_false,
7153
7340
 
7154
7341
  MkKernel.reduce :count_equal,
7155
7342
  init: "0",
7156
- # The object lane compares with rb_equal (= Ruby ==), not the C ==, which
7157
- # on two VALUEs would ask whether they are the same object.
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.
7158
7346
  reduce: { numeric: "if (v == value_arg) acc += 1",
7159
- object: "if (RTEST(rb_equal(v, value_arg))) acc += 1" },
7347
+ object: "if (ca_obj_equal(v, value_arg)) acc += 1" },
7160
7348
  reduction_kind: :plus, # SL.1.4 (conditional predication; clang predicates safely under reduction(+:acc))
7161
7349
  source: MkKernel::ALL_NUMERIC + [:object, :fixlen],
7162
7350
  fixlen: :count_equal, # bespoke memcmp walk (a blob has no scalar C type)
@@ -7248,69 +7436,6 @@ MkKernel.reduce :wmean,
7248
7436
  bind_ruby: true,
7249
7437
  public_method: true
7250
7438
 
7251
- # ---- map kernels (element-wise transforms) ----------------------------
7252
-
7253
- # Float-output transcendentals -- input widens to double inside the
7254
- # expression, output is float64. The :wrap_to_f64 fallback handles
7255
- # any other numeric data_type (int16, bool, etc.) by promoting to f64 first.
7256
-
7257
- MkKernel.map :sqrt,
7258
- source: MkKernel::ALL_NUMERIC,
7259
- output: :f64,
7260
- expr: "r = sqrt((double) v)",
7261
- fallback: :wrap_to_f64
7262
-
7263
- MkKernel.map :sin,
7264
- source: MkKernel::ALL_NUMERIC,
7265
- output: :f64,
7266
- expr: "r = sin((double) v)",
7267
- fallback: :wrap_to_f64
7268
-
7269
- MkKernel.map :cos,
7270
- source: MkKernel::ALL_NUMERIC,
7271
- output: :f64,
7272
- expr: "r = cos((double) v)",
7273
- fallback: :wrap_to_f64
7274
-
7275
- MkKernel.map :exp,
7276
- source: MkKernel::ALL_NUMERIC,
7277
- output: :f64,
7278
- expr: "r = exp((double) v)",
7279
- fallback: :wrap_to_f64
7280
-
7281
- MkKernel.map :log,
7282
- source: MkKernel::ALL_NUMERIC,
7283
- output: :f64,
7284
- expr: "r = log((double) v)",
7285
- fallback: :wrap_to_f64
7286
-
7287
- # Source-data_type-preserving transforms -- arithmetic ops that don't need
7288
- # floating-point. Fallback raises rather than silently widening.
7289
-
7290
- MkKernel.map :square,
7291
- source: MkKernel::ALL_NUMERIC,
7292
- output: :preserve,
7293
- expr: "r = v * v",
7294
- fallback: :raise
7295
-
7296
- MkKernel.map :abs,
7297
- # Signed-only: `v < 0` is always false on unsigned types (compiler
7298
- # warning + identity result). Use `negate` semantics for unsigned
7299
- # only if you genuinely want wrap-around.
7300
- source: MkKernel::SIGNED_NUMERIC,
7301
- output: :preserve,
7302
- expr: "r = (v < 0) ? -v : v",
7303
- fallback: :raise
7304
-
7305
- MkKernel.map :negate,
7306
- # Signed-only: -v on unsigned types wraps (C semantics), which is
7307
- # mathematically wrong for "negate". If you want bitwise inversion
7308
- # on unsigned, use a different kernel (~v).
7309
- source: MkKernel::SIGNED_NUMERIC,
7310
- output: :preserve,
7311
- expr: "r = -v",
7312
- fallback: :raise
7313
-
7314
7439
  # ---- scan kernels (cumulative / prefix scan along one axis) ----------
7315
7440
 
7316
7441
  # cumsum / cumprod: data_type-conditional output (numeric -> f64 widening,
@@ -7360,13 +7485,14 @@ MkKernel.scan :cumprod,
7360
7485
  # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7361
7486
  # and keep the limit init and the plain compare.
7362
7487
  MkKernel.scan :cummax,
7488
+ face_gate: :relift,
7363
7489
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7364
7490
  output: { bool: :u64, default: :preserve },
7365
7491
  init: { float: "NAN", numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qnil" },
7366
7492
  step: { float: "acc = fmax(acc, v); r = acc",
7367
7493
  numeric: "if (v > acc) acc = v; r = acc",
7368
7494
  bool: "if ((uint64_t) v > acc) acc = v; r = acc",
7369
- object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v; r = acc' },
7495
+ object: 'if (acc == Qnil || ca_obj_extremum_replaces(v, acc, rb_intern(">"))) acc = v; r = acc' },
7370
7496
  fallback: :raise,
7371
7497
  axis_default: :flatten,
7372
7498
  empty: :undef
@@ -7379,26 +7505,30 @@ MkKernel.scan :cummax,
7379
7505
  # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7380
7506
  # and keep the limit init and the plain compare.
7381
7507
  MkKernel.scan :cummin,
7508
+ face_gate: :relift,
7382
7509
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7383
7510
  output: { bool: :u64, default: :preserve },
7384
7511
  init: { float: "NAN", numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qnil" },
7385
7512
  step: { float: "acc = fmin(acc, v); r = acc",
7386
7513
  numeric: "if (v < acc) acc = v; r = acc",
7387
7514
  bool: "if ((uint64_t) v < acc) acc = v; r = acc",
7388
- object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v; r = acc' },
7515
+ object: 'if (acc == Qnil || ca_obj_extremum_replaces(v, acc, rb_intern("<"))) acc = v; r = acc' },
7389
7516
  fallback: :raise,
7390
7517
  axis_default: :flatten,
7391
7518
  empty: :undef
7392
7519
 
7393
7520
  # cumcount: running count of unmasked cells. Output data_type = int64.
7394
7521
  # `(void) v` silences unused-variable warning for the unmasked path
7395
- # (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).
7396
7526
  MkKernel.scan :cumcount,
7397
7527
  source: MkKernel::ALL_NUMERIC,
7398
7528
  output: :i64,
7399
7529
  init: "0",
7400
7530
  step: "(void) v; r = ++acc",
7401
- fallback: :wrap_to_f64,
7531
+ fallback: :mask_count,
7402
7532
  axis_default: :flatten
7403
7533
 
7404
7534
  # uniq_scan: per-axis adjacent-compare scan returning a boolean
@@ -7446,7 +7576,7 @@ MkKernel.sort :sort_index,
7446
7576
  nan_policy: :end,
7447
7577
  fallback: :raise,
7448
7578
  mask_self: :sentinel, # masked cells sort to masked_position: :first/:last (default :last)
7449
- public_method: :sort_index, # binds public sort_index(axis: 0, masked_position: :last) via _kw trampoline
7579
+ public_method: :sort_index, # binds public sort_index(axis: nil, masked_position: :last) via _kw trampoline
7450
7580
  c_callable: true # extern linkage for carray_order.c (nlargest / order family)
7451
7581
 
7452
7582
  # sort_addr: per-axis argsort returning view-flat addresses (=
@@ -7454,7 +7584,7 @@ MkKernel.sort :sort_index,
7454
7584
  # sort_index but the kernel writes view-flat addresses instead of
7455
7585
  # fiber-local indices, so the output can feed directly into
7456
7586
  # ca_remap_new as a CARemap.idx. Internal-only (bind_ruby: false):
7457
- # used by `a.sort(axis: k)` at the C level via rb_ca_sort_addr_ki.
7587
+ # used by `a.sort(axis: k)` at the C level via rb_ca_sort_addr_ki_*_mp.
7458
7588
  # rev3 of PROPOSAL_SORT_AXIS defers `sort_addr(axis: k)` as a public
7459
7589
  # method to a future phase; here we just need the kernel machinery.
7460
7590
  MkKernel.sort :sort_addr,
@@ -7481,7 +7611,7 @@ MkKernel.sort :partition_index,
7481
7611
  fallback: :raise,
7482
7612
  algorithm: :partition,
7483
7613
  mask_self: :sentinel, # masked cells sort to masked_position: :first/:last (default :last)
7484
- public_method: :partition_index, # binds partition_index(kth, axis: 0, masked_position: :last) via _kw trampoline
7614
+ public_method: :partition_index, # binds partition_index(kth, axis: nil, masked_position: :last) via _kw trampoline
7485
7615
  c_callable: true # extern linkage for carray_order.c (nlargest / nsmallest)
7486
7616
 
7487
7617
  # partition_addr: view-flat-address variant of partition_index, parallel
@@ -7523,7 +7653,7 @@ MkKernel.sort :rank_index,
7523
7653
  fallback: :raise,
7524
7654
  algorithm: :rank,
7525
7655
  mask_self: :skip,
7526
- 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
7527
7657
  c_callable: true # extern linkage for carray_order.c (order method)
7528
7658
 
7529
7659
  # ---- search family ------------------------------------------------------
@@ -7560,7 +7690,7 @@ MkKernel.search :find_value_index,
7560
7690
  result = (ca_size_t) -1;
7561
7691
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7562
7692
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7563
- if ( rb_equal(v, query_val) ) { result = i; break; }
7693
+ if ( ca_obj_equal(v, query_val) ) { result = i; break; }
7564
7694
  }
7565
7695
  C
7566
7696
  fixlen: <<~C,
@@ -7626,19 +7756,18 @@ MkKernel.search :bsearch,
7626
7756
  C
7627
7757
  object: <<~C,
7628
7758
  /* CA_OBJECT bsearch: ordering via rb_funcall(<=>). Assumes the
7629
- slab is sorted by the same comparator used to build it. NaN
7630
- guards skipped (= no float-NaN concept for arbitrary objects;
7631
- a Float-cell NaN would raise from <=>). */
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. */
7632
7761
  result = (ca_size_t) -1;
7633
7762
  ca_size_t lo = 0;
7634
- ca_size_t hi = slab_n;
7763
+ ca_size_t hi = CA_OBJ_ISNAN(query_val) ? 0 : slab_n; /* NaN matches nothing */
7635
7764
  while ( lo < hi ) {
7636
7765
  ca_size_t mid = lo + (hi - lo) / 2;
7637
7766
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7767
+ if ( CA_OBJ_ISNAN(v) ) { hi = mid; continue; } /* NaN sorts last */
7638
7768
  VALUE r = rb_funcall(v, rb_intern("<=>"), 1, query_val);
7639
7769
  if ( NIL_P(r) ) {
7640
- rb_raise(rb_eArgError, "bsearch: comparison of %s with %s failed",
7641
- rb_obj_classname(v), rb_obj_classname(query_val));
7770
+ ca_obj_compare_refused(v, query_val);
7642
7771
  }
7643
7772
  int c = NUM2INT(r);
7644
7773
  if ( c < 0 ) lo = mid + 1;
@@ -7675,7 +7804,7 @@ MkKernel.search :bsearch,
7675
7804
  #
7676
7805
  # Per-data_type body: int uses exact `v == query_val` (eps ignored); float
7677
7806
  # uses `fabs(v - query_val) <= query_eps` (eps_default = FLT_EPSILON*|val|
7678
- # or DBL_EPSILON*|val|); object uses rb_equal (eps ignored); fixlen uses
7807
+ # or DBL_EPSILON*|val|); object uses ca_obj_equal (eps ignored); fixlen uses
7679
7808
  # memcmp exact match (eps ignored). Sole search implementation: the
7680
7809
  # no-axis (flat) surface routes here via flatten + axis 0
7681
7810
  # (PROPOSAL_SEARCH_SEMANTICS_UNIFY S2/S3; the legacy flat scan was removed).
@@ -7697,18 +7826,18 @@ MkKernel.search :search,
7697
7826
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7698
7827
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7699
7828
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7700
- 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; }
7701
7830
  }
7702
7831
  C
7703
7832
  object: <<~C,
7704
- /* CA_OBJECT search: exact equality via rb_equal. eps is silently
7833
+ /* CA_OBJECT search: exact equality via ca_obj_equal. eps is silently
7705
7834
  ignored (= no meaningful tolerance on arbitrary Ruby objects). */
7706
7835
  result = (ca_size_t) -1;
7707
7836
  (void) query_eps;
7708
7837
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7709
7838
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7710
7839
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7711
- if ( rb_equal(v, query_val) ) { result = i; break; }
7840
+ if ( ca_obj_equal(v, query_val) ) { result = i; break; }
7712
7841
  }
7713
7842
  C
7714
7843
  fixlen: <<~C,
@@ -7747,13 +7876,16 @@ MkKernel.search :search_nearest,
7747
7876
  output: :ca_size,
7748
7877
  body: {
7749
7878
  int: <<~C,
7879
+ /* The distance is exact in uint64: a double loses the difference
7880
+ between neighbouring int64 values above 2**53. */
7750
7881
  result = (ca_size_t) -1;
7751
- double best_dist = INFINITY;
7882
+ uint64_t best_dist = 0;
7752
7883
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7753
7884
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7754
7885
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7755
- double dist = fabs((double)v - (double)query_val);
7756
- if ( dist < best_dist ) { best_dist = dist; result = i; }
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; }
7757
7889
  }
7758
7890
  (void) query_eps; /* unused: search_nearest has no eps semantics */
7759
7891
  C
@@ -7763,8 +7895,11 @@ MkKernel.search :search_nearest,
7763
7895
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7764
7896
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7765
7897
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7766
- double dist = fabs((double)v - (double)query_val);
7767
- if ( dist < best_dist ) { best_dist = dist; result = i; }
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; }
7768
7903
  }
7769
7904
  (void) query_eps; /* unused: search_nearest has no eps semantics */
7770
7905
  C
@@ -7783,20 +7918,22 @@ MkKernel.search :search_nearest,
7783
7918
  int nearest_by_distance = rb_respond_to(query_val, nearest_id);
7784
7919
  if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
7785
7920
  rb_raise(rb_eCADataTypeError,
7786
- "search_nearest: nearest needs a distance, and %s is neither a "
7921
+ "%s: nearest needs a distance, and %s is neither a "
7787
7922
  "number nor answers #distance (define one on the stored "
7788
7923
  "objects, or use search / bsearch for an exact match)",
7789
- rb_obj_classname(query_val));
7924
+ ca_calling_method_name(), rb_obj_classname(query_val));
7790
7925
  }
7791
7926
  result = (ca_size_t) -1;
7792
7927
  VALUE best = Qnil;
7793
7928
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7794
7929
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7795
7930
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7796
- VALUE dist = nearest_by_distance
7931
+ VALUE dist = ca_obj_equal(v, query_val) ? INT2FIX(0)
7932
+ : nearest_by_distance
7797
7933
  ? rb_funcall(query_val, nearest_id, 1, v)
7798
7934
  : rb_funcall(rb_funcall(query_val, '-', 1, v),
7799
7935
  rb_intern("abs"), 0);
7936
+ if ( CA_OBJ_ISNAN(dist) ) continue; /* a NaN is no distance, as in float */
7800
7937
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
7801
7938
  best = dist; result = i;
7802
7939
  }
@@ -7859,14 +7996,14 @@ MkKernel.search :bsearch_addr,
7859
7996
  object: <<~C,
7860
7997
  result = (ca_size_t) -1;
7861
7998
  ca_size_t lo = 0;
7862
- ca_size_t hi = slab_n;
7999
+ ca_size_t hi = CA_OBJ_ISNAN(query_val) ? 0 : slab_n; /* NaN matches nothing */
7863
8000
  while ( lo < hi ) {
7864
8001
  ca_size_t mid = lo + (hi - lo) / 2;
7865
8002
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
8003
+ if ( CA_OBJ_ISNAN(v) ) { hi = mid; continue; } /* NaN sorts last */
7866
8004
  VALUE r = rb_funcall(v, rb_intern("<=>"), 1, query_val);
7867
8005
  if ( NIL_P(r) ) {
7868
- rb_raise(rb_eArgError, "bsearch_addr: comparison of %s with %s failed",
7869
- rb_obj_classname(v), rb_obj_classname(query_val));
8006
+ ca_obj_compare_refused(v, query_val);
7870
8007
  }
7871
8008
  int c = NUM2INT(r);
7872
8009
  if ( c < 0 ) lo = mid + 1;
@@ -7913,7 +8050,7 @@ MkKernel.search :search_addr,
7913
8050
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7914
8051
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7915
8052
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7916
- 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; }
7917
8054
  }
7918
8055
  C
7919
8056
  object: <<~C,
@@ -7922,7 +8059,7 @@ MkKernel.search :search_addr,
7922
8059
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7923
8060
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7924
8061
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7925
- if ( rb_equal(v, query_val) ) { result = i; break; }
8062
+ if ( ca_obj_equal(v, query_val) ) { result = i; break; }
7926
8063
  }
7927
8064
  C
7928
8065
  fixlen: <<~C,
@@ -7948,13 +8085,16 @@ MkKernel.search :search_nearest_addr,
7948
8085
  output: :ca_size,
7949
8086
  body: {
7950
8087
  int: <<~C,
8088
+ /* The distance is exact in uint64: a double loses the difference
8089
+ between neighbouring int64 values above 2**53. */
7951
8090
  result = (ca_size_t) -1;
7952
- double best_dist = INFINITY;
8091
+ uint64_t best_dist = 0;
7953
8092
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7954
8093
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7955
8094
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7956
- double dist = fabs((double)v - (double)query_val);
7957
- if ( dist < best_dist ) { best_dist = dist; result = i; }
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; }
7958
8098
  }
7959
8099
  (void) query_eps;
7960
8100
  C
@@ -7964,8 +8104,11 @@ MkKernel.search :search_nearest_addr,
7964
8104
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7965
8105
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7966
8106
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7967
- double dist = fabs((double)v - (double)query_val);
7968
- if ( dist < best_dist ) { best_dist = dist; result = i; }
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; }
7969
8112
  }
7970
8113
  (void) query_eps;
7971
8114
  C
@@ -7984,20 +8127,22 @@ MkKernel.search :search_nearest_addr,
7984
8127
  int nearest_by_distance = rb_respond_to(query_val, nearest_id);
7985
8128
  if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
7986
8129
  rb_raise(rb_eCADataTypeError,
7987
- "search_nearest_addr: nearest needs a distance, and %s is neither a "
8130
+ "%s: nearest needs a distance, and %s is neither a "
7988
8131
  "number nor answers #distance (define one on the stored "
7989
8132
  "objects, or use search / bsearch for an exact match)",
7990
- rb_obj_classname(query_val));
8133
+ ca_calling_method_name(), rb_obj_classname(query_val));
7991
8134
  }
7992
8135
  result = (ca_size_t) -1;
7993
8136
  VALUE best = Qnil;
7994
8137
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7995
8138
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7996
8139
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7997
- VALUE dist = nearest_by_distance
8140
+ VALUE dist = ca_obj_equal(v, query_val) ? INT2FIX(0)
8141
+ : nearest_by_distance
7998
8142
  ? rb_funcall(query_val, nearest_id, 1, v)
7999
8143
  : rb_funcall(rb_funcall(query_val, '-', 1, v),
8000
8144
  rb_intern("abs"), 0);
8145
+ if ( CA_OBJ_ISNAN(dist) ) continue; /* a NaN is no distance, as in float */
8001
8146
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
8002
8147
  best = dist; result = i;
8003
8148
  }
@@ -8047,7 +8192,11 @@ MkKernel.search :linear_section_binary,
8047
8192
  body: <<~C,
8048
8193
  result = NAN;
8049
8194
  if ( slab_n <= 1 ) {
8050
- 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
+ }
8051
8200
  }
8052
8201
  else {
8053
8202
  double y0 = *(double *)(slab_ptr + 0 * slab_stride);
@@ -8135,7 +8284,11 @@ MkKernel.search :linear_section_linear,
8135
8284
  body: <<~C,
8136
8285
  result = NAN;
8137
8286
  if ( slab_n <= 1 ) {
8138
- 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
+ }
8139
8292
  }
8140
8293
  else {
8141
8294
  ca_size_t x1 = 0;
@@ -8265,9 +8418,10 @@ MkKernel.monop :abs_i,
8265
8418
  # (= used by .real chain composition); abs is the user-facing op that
8266
8419
  # returns the real-valued magnitude entity.
8267
8420
  MkKernel.monop :abs,
8268
- source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
8269
- 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 },
8270
8423
  expr: {
8424
+ [:object] => '(#2) = rb_funcall((#1), rb_intern("abs"), 0);',
8271
8425
  MkKernel::SINT_SMALL_DTYPES => "(#2) = abs(#1);",
8272
8426
  MkKernel::SINT64_DTYPES => "(#2) = llabs(#1);",
8273
8427
  MkKernel::UINT_DTYPES => "(#2) = (#1);",
@@ -8383,7 +8537,9 @@ MkKernel.monop :imag_i,
8383
8537
  MkKernel.monop :arg_i,
8384
8538
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES,
8385
8539
  expr: {
8386
- [: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));",
8387
8543
  [:f64] => "(#2) = carg((cmplx128_t)(#1));",
8388
8544
  [:cmplx64] => "(#2) = cargf(#1);",
8389
8545
  [:cmplx128] => "(#2) = carg(#1);",
@@ -8439,7 +8595,9 @@ MkKernel.monfunc :round,
8439
8595
  MkKernel.monfunc :rcp,
8440
8596
  source: MkKernel::MATH_NUMERIC + [:object],
8441
8597
  expr: {
8442
- 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;",
8443
8601
  float: "(#2) = 1/(#1);",
8444
8602
  [:cmplx64] => "(#2) = op_crcp_cmplx64(#1);",
8445
8603
  [:cmplx128] => "(#2) = 1/(#1);",
@@ -8548,11 +8706,13 @@ end
8548
8706
 
8549
8707
  # expm1, log1p: float + object only (C99 doesn't standardise complex
8550
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.
8551
8711
  {
8552
8712
  expm1: "expm1",
8553
8713
  log1p: "log1p",
8554
8714
  }.each do |op, c_fn|
8555
- MkKernel.monfunc op,
8715
+ MkKernel.monfunc op, cmath: false,
8556
8716
  source: MkKernel::FLOAT_DTYPES + [:object],
8557
8717
  expr: {
8558
8718
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
@@ -8737,14 +8897,15 @@ MkKernel.monfunc :rad_pi,
8737
8897
  # NaN the result is NaN. Distinct kernel — cannot be
8738
8898
  # aliased onto pmax.
8739
8899
  #
8740
- # Integer / object branches are identical across all three (no NaN
8741
- # 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.
8742
8903
  MkKernel.binop :pmax,
8743
8904
  source: MkKernel::ALL_NUMERIC + [:object],
8744
8905
  expr: {
8745
8906
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8746
8907
  float: "(#3) = fmax(#1, #2);",
8747
- 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);',
8748
8909
  }
8749
8910
 
8750
8911
  MkKernel.binop :pmin,
@@ -8752,26 +8913,28 @@ MkKernel.binop :pmin,
8752
8913
  expr: {
8753
8914
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8754
8915
  float: "(#3) = fmin(#1, #2);",
8755
- 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);',
8756
8917
  }
8757
8918
 
8758
8919
  # NaN-propagate variants. Float
8759
8920
  # branch tests both operands for NaN and short-circuits to NaN; non-
8760
- # 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.
8761
8924
  MkKernel.binop :maximum,
8762
8925
  source: MkKernel::ALL_NUMERIC + [:object],
8763
8926
  expr: {
8764
8927
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8765
- float: "(#3) = isnan(#1) ? (#1) : (isnan(#2) ? (#2) : ((#1) > (#2) ? (#1) : (#2)));",
8766
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("max"), 0);',
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);',
8767
8930
  }
8768
8931
 
8769
8932
  MkKernel.binop :minimum,
8770
8933
  source: MkKernel::ALL_NUMERIC + [:object],
8771
8934
  expr: {
8772
8935
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8773
- float: "(#3) = isnan(#1) ? (#1) : (isnan(#2) ? (#2) : ((#1) < (#2) ? (#1) : (#2)));",
8774
- object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("min"), 0);',
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);',
8775
8938
  }
8776
8939
 
8777
8940
  # + and - work on the parts independently, so one generic expression
@@ -8812,6 +8975,11 @@ MkKernel.binop :mul,
8812
8975
  # sign, so they keep the bare C form. Float `/` is true division and is
8813
8976
  # unchanged (matching Ruby `Float#/`); the identity above therefore holds
8814
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.
8815
8983
  MkKernel.binop :div,
8816
8984
  op: "/",
8817
8985
  source: MkKernel::MATH_NUMERIC + [:object],
@@ -8819,7 +8987,8 @@ MkKernel.binop :div,
8819
8987
  MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) / (#2);",
8820
8988
  int: %{
8821
8989
  if ((#2)==0) {ca_zerodiv();};
8822
- {
8990
+ if ((#2)==-1) { (#3) = (<type>) (0 - (uint64_t) (#1)); }
8991
+ else {
8823
8992
  <type> _q = (#1) / (#2);
8824
8993
  if ( (#1) % (#2) != 0 && (((#1) < 0) != ((#2) < 0)) ) { _q -= 1; }
8825
8994
  (#3) = _q;
@@ -8845,11 +9014,21 @@ MkKernel.binop :quo_i,
8845
9014
  object: '(#3) = rb_funcall((#1), rb_intern("quo"), 1, (#2));',
8846
9015
  }
8847
9016
 
9017
+ # rcp_mul: `a.rcp_mul(b)` is `b / a`, and divides as `/` does.
8848
9018
  MkKernel.binop :rcp_mul,
8849
9019
  op: "rcp_mul",
8850
9020
  source: MkKernel::MATH_NUMERIC + [:object],
8851
9021
  expr: {
8852
- 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
+ },
8853
9032
  float: "(#3) = (#2) / (#1);",
8854
9033
  [:cmplx64] => "(#3) = op_cdiv_cmplx64((#2), (#1));",
8855
9034
  [:cmplx128] => "(#3) = (#2) / (#1);",
@@ -8871,7 +9050,8 @@ MkKernel.binop :mod,
8871
9050
  MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
8872
9051
  int: %{
8873
9052
  if ((#2)==0) {ca_zerodiv();};
8874
- {
9053
+ if ((#2)==-1) { (#3) = 0; }
9054
+ else {
8875
9055
  <type> _r = (#1) % (#2);
8876
9056
  if ( _r != 0 && ((_r < 0) != ((#2) < 0)) ) { _r += (#2); }
8877
9057
  (#3) = _r;
@@ -8899,7 +9079,8 @@ MkKernel.binop :mod,
8899
9079
  MkKernel.binop :fmod,
8900
9080
  source: MkKernel::ALL_NUMERIC + [:object],
8901
9081
  expr: {
8902
- 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);",
8903
9084
  object: '(#3) = rb_funcall((#1), rb_intern("remainder"), 1, (#2));',
8904
9085
  }.merge(MkKernel.float_widths("(#3) = fmod<f>((#1), (#2));"))
8905
9086
 
@@ -8932,11 +9113,29 @@ MkKernel.binop :bit_xor_i,
8932
9113
  object: '(#3) = rb_funcall((#1), rb_intern("^"), 1, (#2));',
8933
9114
  }
8934
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.
8935
9123
  MkKernel.binop :bit_lshift,
8936
9124
  op: "<<",
8937
9125
  source: MkKernel::INT_DTYPES + [:object],
8938
9126
  expr: {
8939
- 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
+ },
8940
9139
  object: '(#3) = rb_funcall((#1), rb_intern("<<"), 1, (#2));',
8941
9140
  }
8942
9141
 
@@ -8944,7 +9143,18 @@ MkKernel.binop :bit_rshift,
8944
9143
  op: ">>",
8945
9144
  source: MkKernel::INT_DTYPES + [:object],
8946
9145
  expr: {
8947
- 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
+ },
8948
9158
  object: '(#3) = rb_funcall((#1), rb_intern(">>"), 1, (#2));',
8949
9159
  }
8950
9160
 
@@ -9014,16 +9224,16 @@ C
9014
9224
  # fma: fused multiply-add — `self * op2 + op3` in a single rounding step
9015
9225
  # (= C99 `fma(x, y, z)` and HW FMA3/FMA4 / ARM FMA instructions). More
9016
9226
  # accurate than the equivalent two-op `a*b + c` expression which rounds
9017
- # 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,
9018
9229
  # Newton iteration.
9019
9230
  MkKernel.triop :fma,
9020
9231
  source: MkKernel::MATH_NUMERIC + [:object],
9021
9232
  expr: {
9022
9233
  int: "(#4) = (#1) * (#2) + (#3);",
9023
- float: "(#4) = fma((#1), (#2), (#3));",
9024
9234
  complex: "(#4) = (#1) * (#2) + (#3);",
9025
9235
  object: '(#4) = rb_funcall(rb_funcall((#1), rb_intern("*"), 1, (#2)), rb_intern("+"), 1, (#3));',
9026
- }
9236
+ }.merge(MkKernel.float_widths("(#4) = fma<f>((#1), (#2), (#3));"))
9027
9237
 
9028
9238
  # fms: fused multiply-subtract — `self * op2 - op3` in a single rounding.
9029
9239
  # Useful in cross products and complex multiplication primitives.
@@ -9031,11 +9241,10 @@ MkKernel.triop :fms,
9031
9241
  source: MkKernel::MATH_NUMERIC + [:object],
9032
9242
  expr: {
9033
9243
  int: "(#4) = (#1) * (#2) - (#3);",
9034
- # fma(a, b, -c) = a*b - c with single rounding (= C99 / IEEE 754 hint).
9035
- float: "(#4) = fma((#1), (#2), -(#3));",
9036
9244
  complex: "(#4) = (#1) * (#2) - (#3);",
9037
9245
  object: '(#4) = rb_funcall(rb_funcall((#1), rb_intern("*"), 1, (#2)), rb_intern("-"), 1, (#3));',
9038
- }
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));"))
9039
9248
 
9040
9249
  # clip: per-element clamp into [lo, hi]. 3.0 migration: was a hand-
9041
9250
  # written eager-only method in ext/carray_generate.c that only accepted
@@ -9074,6 +9283,20 @@ MkKernel.triop :clip,
9074
9283
  '(#4) = _v; }',
9075
9284
  }
9076
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
+
9077
9300
  MkKernel.binop :power,
9078
9301
  op: :power,
9079
9302
  source: MkKernel::MATH_NUMERIC + [:object],
@@ -9151,7 +9374,11 @@ MkKernel.moncmp :is_nan,
9151
9374
  int: "(#2) = 0;",
9152
9375
  float: "(#2) = isnan(#1);",
9153
9376
  complex: "(#2) = (isnan(creal(#1)) || isnan(cimag(#1)));",
9154
- 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));',
9155
9382
  }
9156
9383
 
9157
9384
  MkKernel.moncmp :is_inf,
@@ -9160,7 +9387,7 @@ MkKernel.moncmp :is_inf,
9160
9387
  int: "(#2) = 0;",
9161
9388
  float: "(#2) = isinf(#1);",
9162
9389
  complex: "(#2) = (isinf(creal(#1)) || isinf(cimag(#1)));",
9163
- object: '(#2) = rb_funcall((#1), rb_intern("infinite?"), 0);',
9390
+ object: '(#2) = RTEST(rb_funcall((#1), rb_intern("infinite?"), 0));',
9164
9391
  }
9165
9392
 
9166
9393
  MkKernel.moncmp :is_finite,
@@ -9169,7 +9396,7 @@ MkKernel.moncmp :is_finite,
9169
9396
  int: "(#2) = 1;",
9170
9397
  float: "(#2) = isfinite(#1);",
9171
9398
  complex: "(#2) = (isfinite(creal(#1)) && isfinite(cimag(#1)));",
9172
- object: '(#2) = rb_funcall((#1), rb_intern("finite?"), 0);',
9399
+ object: '(#2) = RTEST(rb_funcall((#1), rb_intern("finite?"), 0));',
9173
9400
  }
9174
9401
 
9175
9402
  # is_invalid = !is_finite (MS.1 mask SET family redesign): NaN or Inf
@@ -9199,7 +9426,10 @@ MkKernel.moncmp :signbit,
9199
9426
  MkKernel::SINT_DTYPES => "(#2) = ((#1) < 0);",
9200
9427
  MkKernel::UINT_DTYPES => "(#2) = 0;",
9201
9428
  float: "(#2) = signbit(#1);",
9202
- 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);',
9203
9433
  }
9204
9434
 
9205
9435
  # ---- P.5b.4: bincmp family (predicates returning bool) ----------------