carray 3.0.3 → 3.0.5

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 (221) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +469 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_array_pool.c +25 -1
  6. data/ext/ca_axis_descriptor.h +27 -24
  7. data/ext/ca_axis_dispatch.c +14 -26
  8. data/ext/ca_axis_group.c +27 -27
  9. data/ext/ca_binop_dispatch.c +1 -29
  10. data/ext/ca_binop_dispatch.h +0 -1
  11. data/ext/ca_categorical_iterator.c +231 -153
  12. data/ext/ca_compare.c +9 -20
  13. data/ext/ca_composite_dispatch.c +9 -9
  14. data/ext/ca_group_iter.c +16 -17
  15. data/ext/ca_kernel_iterator.c +1240 -2000
  16. data/ext/ca_kernel_iterator.h +502 -519
  17. data/ext/ca_monop_dispatch.c +82 -59
  18. data/ext/ca_monop_dispatch.h +1 -0
  19. data/ext/ca_obj_array.c +152 -45
  20. data/ext/ca_obj_bincmp.c +25 -21
  21. data/ext/ca_obj_binop.c +16 -26
  22. data/ext/ca_obj_bitarray.c +8 -3
  23. data/ext/ca_obj_bitfield.c +63 -49
  24. data/ext/ca_obj_block.c +18 -9
  25. data/ext/ca_obj_byte_swap.c +7 -7
  26. data/ext/ca_obj_const_string.c +188 -22
  27. data/ext/ca_obj_face.c +280 -58
  28. data/ext/ca_obj_face.h +19 -0
  29. data/ext/ca_obj_fake.c +15 -29
  30. data/ext/ca_obj_farray.c +6 -3
  31. data/ext/ca_obj_field.c +30 -8
  32. data/ext/ca_obj_fixlen_string.c +2 -3
  33. data/ext/ca_obj_grid.c +455 -134
  34. data/ext/ca_obj_meld.c +172 -34
  35. data/ext/ca_obj_moncmp.c +23 -8
  36. data/ext/ca_obj_monop.c +113 -67
  37. data/ext/ca_obj_object.c +210 -52
  38. data/ext/ca_obj_record.c +15 -7
  39. data/ext/ca_obj_reduce.c +17 -8
  40. data/ext/ca_obj_refer.c +42 -35
  41. data/ext/ca_obj_remap.c +30 -9
  42. data/ext/ca_obj_repeat.c +9 -10
  43. data/ext/ca_obj_roll.c +31 -10
  44. data/ext/ca_obj_select.c +48 -14
  45. data/ext/ca_obj_select_axis.c +15 -47
  46. data/ext/ca_obj_shift.c +41 -7
  47. data/ext/ca_obj_stack.c +180 -34
  48. data/ext/ca_obj_stride.c +190 -134
  49. data/ext/ca_obj_string.c +80 -4
  50. data/ext/ca_obj_tile.c +32 -11
  51. data/ext/ca_obj_time.c +22 -6
  52. data/ext/ca_obj_timedelta.c +22 -6
  53. data/ext/ca_obj_transpose.c +13 -7
  54. data/ext/ca_obj_triop.c +26 -32
  55. data/ext/ca_obj_window.c +267 -74
  56. data/ext/ca_op_ipower.c +4 -4
  57. data/ext/ca_sort_kernels.h +6 -3
  58. data/ext/ca_sweep_engine.c +2 -2
  59. data/ext/caframe_csv_reader.c +80 -1
  60. data/ext/carray.h +214 -75
  61. data/ext/carray_access.c +200 -99
  62. data/ext/carray_address_basis.c +343 -87
  63. data/ext/carray_attribute.c +113 -6
  64. data/ext/carray_bincount.c +10 -8
  65. data/ext/carray_broadcast.c +13 -8
  66. data/ext/carray_build_flags.h +3 -0
  67. data/ext/carray_call_cfunc.c +108 -0
  68. data/ext/carray_cast.c +375 -96
  69. data/ext/carray_cast_func.rb +90 -20
  70. data/ext/carray_class.c +4 -3
  71. data/ext/carray_config.h +28 -0
  72. data/ext/carray_conversion.c +18 -9
  73. data/ext/carray_copy.c +51 -14
  74. data/ext/carray_core.c +150 -55
  75. data/ext/carray_count.c +56 -20
  76. data/ext/carray_element.c +81 -33
  77. data/ext/carray_factorize.c +138 -84
  78. data/ext/carray_generate.c +199 -52
  79. data/ext/carray_histogram.c +63 -18
  80. data/ext/carray_hold.c +12 -15
  81. data/ext/carray_index_classifier.c +388 -176
  82. data/ext/carray_internal.h +348 -29
  83. data/ext/carray_kernels_bincmp.c +4453 -0
  84. data/ext/carray_kernels_binop.c +11368 -0
  85. data/ext/carray_kernels_init.c +1734 -0
  86. data/ext/carray_kernels_moncmp.c +2104 -0
  87. data/ext/carray_kernels_monop.c +18369 -0
  88. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  89. data/ext/carray_kernels_reduce_boolean.c +406 -0
  90. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  91. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  92. data/ext/carray_kernels_reduce_variance.c +3849 -0
  93. data/ext/carray_kernels_scan.c +4302 -0
  94. data/ext/carray_kernels_search.c +33829 -0
  95. data/ext/carray_kernels_sort.c +10897 -0
  96. data/ext/carray_kernels_triop.c +1864 -0
  97. data/ext/carray_key_windows.c +220 -0
  98. data/ext/carray_lazy.c +170 -111
  99. data/ext/carray_mask.c +337 -94
  100. data/ext/carray_median_percentile.c +82 -28
  101. data/ext/carray_memory_view.c +669 -561
  102. data/ext/carray_operator.c +129 -52
  103. data/ext/carray_order.c +604 -77
  104. data/ext/carray_partition.c +28 -27
  105. data/ext/carray_random.c +85 -31
  106. data/ext/carray_round.c +273 -0
  107. data/ext/carray_scatter.c +87 -45
  108. data/ext/carray_segment.c +3 -3
  109. data/ext/carray_slab.c +123 -37
  110. data/ext/carray_sort.c +259 -51
  111. data/ext/carray_sort_kernel.c +5 -7
  112. data/ext/carray_struct.c +54 -61
  113. data/ext/carray_test.c +26 -5
  114. data/ext/carray_time_text.c +4 -2
  115. data/ext/carray_undef.c +4 -0
  116. data/ext/carray_utils.c +309 -66
  117. data/ext/mk_call_cfunc.rb +19 -2
  118. data/ext/mkkernel.rb +403 -350
  119. data/ext/ruby_carray.c +10 -4
  120. data/ext/version.h +4 -4
  121. data/lib/carray/attribute.rb +139 -22
  122. data/lib/carray/autoload_carray.rb +8 -31
  123. data/lib/carray/autoload_method_extension.rb +14 -5
  124. data/lib/carray/axis_group.rb +255 -106
  125. data/lib/carray/basics.rb +105 -17
  126. data/lib/carray/bincount_nd.rb +12 -2
  127. data/lib/carray/block_iterator.rb +126 -23
  128. data/lib/carray/boolean_reduce.rb +17 -17
  129. data/lib/carray/categorical.rb +195 -59
  130. data/lib/carray/categorical_iterator.rb +132 -29
  131. data/lib/carray/complex.rb +5 -5
  132. data/lib/carray/conditional.rb +67 -18
  133. data/lib/carray/const_string.rb +24 -5
  134. data/lib/carray/construct.rb +15 -11
  135. data/lib/carray/data_type_extension.rb +9 -1
  136. data/lib/carray/distance_limit.rb +77 -0
  137. data/lib/carray/fixlen_string.rb +48 -3
  138. data/lib/carray/frame/column_verbs.rb +71 -0
  139. data/lib/carray/frame/concat.rb +17 -3
  140. data/lib/carray/frame/convert.rb +69 -0
  141. data/lib/carray/frame/csv_parser.rb +111 -17
  142. data/lib/carray/frame/describe.rb +3 -2
  143. data/lib/carray/frame/frame.rb +252 -28
  144. data/lib/carray/frame/group.rb +603 -101
  145. data/lib/carray/frame/io.rb +117 -28
  146. data/lib/carray/frame/join.rb +312 -51
  147. data/lib/carray/frame/records.rb +83 -7
  148. data/lib/carray/frame/reshape.rb +42 -35
  149. data/lib/carray/frame/sort.rb +7 -19
  150. data/lib/carray/frame/verbs.rb +188 -48
  151. data/lib/carray/frame/windows.rb +117 -0
  152. data/lib/carray/frame.rb +2 -0
  153. data/lib/carray/fuse_source.rb +42 -6
  154. data/lib/carray/fusion.rb +40 -16
  155. data/lib/carray/histogram.rb +6 -1
  156. data/lib/carray/inspect.rb +31 -8
  157. data/lib/carray/iterator.rb +85 -7
  158. data/lib/carray/key_windows.rb +620 -0
  159. data/lib/carray/lazy.rb +217 -183
  160. data/lib/carray/mask_gap_fill.rb +63 -57
  161. data/lib/carray/meld_reduce.rb +12 -14
  162. data/lib/carray/methods/bin.rb +26 -18
  163. data/lib/carray/methods/bincount.rb +16 -0
  164. data/lib/carray/methods/bit_string.rb +1 -1
  165. data/lib/carray/methods/choose.rb +16 -3
  166. data/lib/carray/methods/composition.rb +3 -1
  167. data/lib/carray/methods/gather_nd.rb +14 -5
  168. data/lib/carray/methods/insert_block.rb +2 -2
  169. data/lib/carray/methods/is_between.rb +59 -0
  170. data/lib/carray/methods/is_in.rb +61 -37
  171. data/lib/carray/methods/join.rb +1 -6
  172. data/lib/carray/methods/lag.rb +49 -0
  173. data/lib/carray/methods/locate_addr.rb +47 -51
  174. data/lib/carray/methods/mode.rb +1 -1
  175. data/lib/carray/methods/nunique.rb +11 -6
  176. data/lib/carray/methods/resize.rb +3 -3
  177. data/lib/carray/methods/value_counts.rb +4 -11
  178. data/lib/carray/record_order.rb +3 -3
  179. data/lib/carray/rng.rb +3 -0
  180. data/lib/carray/runtime.rb +53 -8
  181. data/lib/carray/segment_iterator.rb +323 -88
  182. data/lib/carray/serialize.rb +45 -14
  183. data/lib/carray/slab_iterator.rb +50 -0
  184. data/lib/carray/stack.rb +15 -12
  185. data/lib/carray/string.rb +113 -5
  186. data/lib/carray/string_operation_extension.rb +21 -6
  187. data/lib/carray/struct.rb +9 -21
  188. data/lib/carray/struct_builder.rb +24 -11
  189. data/lib/carray/time.rb +819 -158
  190. data/lib/carray/window_iterator.rb +122 -16
  191. data/lib/carray.rb +7 -0
  192. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  193. data/yard-stubs/ca_obj_field.rb +4 -0
  194. data/yard-stubs/ca_obj_grid.rb +21 -10
  195. data/yard-stubs/ca_obj_refer.rb +2 -1
  196. data/yard-stubs/ca_obj_roll.rb +2 -2
  197. data/yard-stubs/ca_obj_shift.rb +5 -1
  198. data/yard-stubs/ca_obj_tile.rb +1 -1
  199. data/yard-stubs/ca_obj_transpose.rb +5 -4
  200. data/yard-stubs/ca_obj_window.rb +8 -3
  201. data/yard-stubs/carray_access.rb +6 -1
  202. data/yard-stubs/carray_attribute.rb +3 -6
  203. data/yard-stubs/carray_cast.rb +12 -0
  204. data/yard-stubs/carray_conversion.rb +7 -2
  205. data/yard-stubs/carray_copy.rb +4 -2
  206. data/yard-stubs/carray_count.rb +4 -4
  207. data/yard-stubs/carray_element.rb +17 -10
  208. data/yard-stubs/carray_generate.rb +21 -1
  209. data/yard-stubs/carray_mask.rb +32 -3
  210. data/yard-stubs/carray_math.rb +41 -5
  211. data/yard-stubs/carray_median_percentile.rb +5 -2
  212. data/yard-stubs/carray_memory_view.rb +40 -23
  213. data/yard-stubs/carray_order.rb +129 -22
  214. data/yard-stubs/carray_random.rb +13 -4
  215. data/yard-stubs/carray_scatter.rb +17 -10
  216. data/yard-stubs/carray_slab.rb +9 -2
  217. data/yard-stubs/carray_sort.rb +6 -5
  218. data/yard-stubs/carray_test.rb +5 -3
  219. metadata +25 -3
  220. data/lib/carray/arrow_tensor.rb +0 -401
  221. data/lib/carray/table.rb +0 -108
data/ext/mkkernel.rb CHANGED
@@ -44,26 +44,29 @@ module MkKernel
44
44
  # value_arg: dispatchers (CF.1) to cast a positional Ruby argument to
45
45
  # the kernel's T_IN type. Signed integers use NUM2LL, unsigned
46
46
  # NUM2ULL, floats NUM2DBL, with the dispatcher adding the per-type cast.
47
+ # `c` of a multi-byte type is its unaligned typedef from carray.h
48
+ # (ca_u_float64_t): a view's cells may sit at any byte, and a kernel
49
+ # reads them through `c *`.
47
50
  DTYPES = {
48
51
  i8: { c: "int8_t", ca: "CA_INT8", ruby: "LL2NUM", num2c: "NUM2LL",
49
52
  limit_hi: "INT8_MAX", limit_lo: "INT8_MIN" },
50
53
  u8: { c: "uint8_t", ca: "CA_UINT8", ruby: "ULL2NUM", num2c: "NUM2ULL",
51
54
  limit_hi: "UINT8_MAX", limit_lo: "0" },
52
- i16: { c: "int16_t", ca: "CA_INT16", ruby: "LL2NUM", num2c: "NUM2LL",
55
+ i16: { c: "ca_u_int16_t", ca: "CA_INT16", ruby: "LL2NUM", num2c: "NUM2LL",
53
56
  limit_hi: "INT16_MAX", limit_lo: "INT16_MIN" },
54
- u16: { c: "uint16_t", ca: "CA_UINT16", ruby: "ULL2NUM", num2c: "NUM2ULL",
57
+ u16: { c: "ca_u_uint16_t", ca: "CA_UINT16", ruby: "ULL2NUM", num2c: "NUM2ULL",
55
58
  limit_hi: "UINT16_MAX", limit_lo: "0" },
56
- i32: { c: "int32_t", ca: "CA_INT32", ruby: "LL2NUM", num2c: "NUM2LL",
59
+ i32: { c: "ca_u_int32_t", ca: "CA_INT32", ruby: "LL2NUM", num2c: "NUM2LL",
57
60
  limit_hi: "INT32_MAX", limit_lo: "INT32_MIN" },
58
- u32: { c: "uint32_t", ca: "CA_UINT32", ruby: "ULL2NUM", num2c: "NUM2ULL",
61
+ u32: { c: "ca_u_uint32_t", ca: "CA_UINT32", ruby: "ULL2NUM", num2c: "NUM2ULL",
59
62
  limit_hi: "UINT32_MAX", limit_lo: "0" },
60
- i64: { c: "int64_t", ca: "CA_INT64", ruby: "LL2NUM", num2c: "NUM2LL",
63
+ i64: { c: "ca_u_int64_t", ca: "CA_INT64", ruby: "LL2NUM", num2c: "NUM2LL",
61
64
  limit_hi: "INT64_MAX", limit_lo: "INT64_MIN" },
62
- u64: { c: "uint64_t", ca: "CA_UINT64", ruby: "ULL2NUM", num2c: "NUM2ULL",
65
+ u64: { c: "ca_u_uint64_t", ca: "CA_UINT64", ruby: "ULL2NUM", num2c: "NUM2ULL",
63
66
  limit_hi: "UINT64_MAX", limit_lo: "0" },
64
- f32: { c: "float", ca: "CA_FLOAT32", ruby: "rb_float_new", num2c: "NUM2DBL",
67
+ f32: { c: "ca_u_float32_t", ca: "CA_FLOAT32", ruby: "rb_float_new", num2c: "NUM2DBL",
65
68
  limit_hi: "INFINITY", limit_lo: "-INFINITY" },
66
- f64: { c: "double", ca: "CA_FLOAT64", ruby: "rb_float_new", num2c: "NUM2DBL",
69
+ f64: { c: "ca_u_float64_t", ca: "CA_FLOAT64", ruby: "rb_float_new", num2c: "NUM2DBL",
67
70
  limit_hi: "INFINITY", limit_lo: "-INFINITY" },
68
71
  # E.6a (2026-06-03): boolean as a first-class source DTYPE.
69
72
  # Intentionally NOT included in ALL_NUMERIC -- kernels opt boolean in
@@ -90,11 +93,11 @@ module MkKernel
90
93
  # CA_NTYPE coverage parity with mkmath, mkkernel needs to know the
91
94
  # complex and object data_types too. These entries are NOT included in
92
95
  # ALL_NUMERIC; pick up via MATH_TYPES / MATH_NUMERIC aliases.
93
- cmplx64: { c: "cmplx64_t", ca: "CA_CMPLX64", ruby: "CC2NUM",
96
+ cmplx64: { c: "ca_u_cmplx64_t", ca: "CA_CMPLX64", ruby: "CC2NUM",
94
97
  num2c: "NUM2CC", limit_hi: "0", limit_lo: "0" },
95
- cmplx128: { c: "cmplx128_t", ca: "CA_CMPLX128", ruby: "CC2NUM",
98
+ cmplx128: { c: "ca_u_cmplx128_t", ca: "CA_CMPLX128", ruby: "CC2NUM",
96
99
  num2c: "NUM2CC", limit_hi: "0", limit_lo: "0" },
97
- object: { c: "VALUE", ca: "CA_OBJECT", ruby: "(VALUE)",
100
+ object: { c: "VALUE", ca: "CA_OBJECT", ruby: "(VALUE)",
98
101
  num2c: "(VALUE)", limit_hi: "0", limit_lo: "0" },
99
102
  # P.5b.4: fixlen for bincmp (= match, eq/ne/gt/lt/ge/le against
100
103
  # fixed-byte strings). The c: type is `char` because fixlen kernels
@@ -177,10 +180,8 @@ module MkKernel
177
180
  # expr: { object: "..." }.merge(float_widths("(#2) = sin<f>(#1);"))
178
181
  #
179
182
  # Not every double-taking call wants this. `fabs`, `fmin` / `fmax`,
180
- # `ceil` / `floor` / `trunc` and the `isnan` family are exact on a
181
- # float either way, and `round` (`floor(x + 0.5)`) is exact only in
182
- # double -- narrowing it would round `x + 0.5f` first and step the
183
- # answer at the boundary.
183
+ # `ceil` / `floor` / `trunc` / `round` and the `isnan` family are exact
184
+ # on a float either way.
184
185
  def self.float_widths(body)
185
186
  {
186
187
  [:f32] => at_width(body, "f", "float"),
@@ -706,8 +707,13 @@ module MkKernel
706
707
  # the input is masked). Aligns core cummax / cummin with
707
708
  # the reduction contract (empty max / min = UNDEF, no
708
709
  # identity) and the axis-group scan family.
710
+ # private:
711
+ # false (default) -> bound as the public `<name>_ki`
712
+ # true -> bound only as the private method `<name>`, a part
713
+ # that lib reaches with `send` (not user surface)
709
714
  def self.scan(name, source:, output:, init:, step:, fallback: :raise,
710
- acc_type: nil, axis_default: nil, empty: nil, face_gate: nil)
715
+ acc_type: nil, axis_default: nil, empty: nil, face_gate: nil,
716
+ private: false)
711
717
  raise "duplicate kernel #{name}" if KERNELS.any? { |k| k[:name] == name }
712
718
  source.each do |s|
713
719
  raise "unknown source data_type #{s}" unless DTYPES.key?(s)
@@ -749,6 +755,7 @@ module MkKernel
749
755
  axis_default: axis_default,
750
756
  empty: empty,
751
757
  face_gate: face_gate,
758
+ private: private,
752
759
  }
753
760
  end
754
761
 
@@ -944,7 +951,7 @@ module MkKernel
944
951
  # is up to the body (= value-returning search variants).
945
952
  def self.search(name, source:, output:, body:,
946
953
  no_match: "-1", no_match_check: nil, undef_no_match: false,
947
- mask_self: :raise, mask_query: :ignore, runtime_args: nil,
954
+ mask_self: :raise, runtime_args: nil,
948
955
  fallback: :raise, semantics: :fiber_local)
949
956
  raise "search: unknown semantics #{semantics} (expected :fiber_local or :view_flat)" \
950
957
  unless %i[fiber_local view_flat].include?(semantics)
@@ -956,12 +963,10 @@ module MkKernel
956
963
  raise "search: no_match must be a String" unless no_match.is_a?(String)
957
964
  raise "search: unknown mask_self #{mask_self} (expected :raise | :skip | :ignore)" \
958
965
  unless %i[raise skip ignore].include?(mask_self)
959
- # mask_query: what a masked query cell means. :ignore reads the value
960
- # under the mask (the historical behaviour). :undef treats the query as
961
- # having no answer and marks the corresponding output cell UNDEF, so an
962
- # undetermined query cannot silently become a determined result.
963
- raise "search: unknown mask_query #{mask_query} (expected :ignore | :undef)" \
964
- unless %i[ignore undef].include?(mask_query)
966
+ # A masked query cell (or a scalar UNDEF query) has no answer: its output
967
+ # cell is UNDEF, so an undetermined query cannot become a determined
968
+ # result from the value that happens to sit under the mask. There is no
969
+ # option to read under the mask.
965
970
  raise "search: unknown fallback #{fallback} (expected :raise)" \
966
971
  unless fallback == :raise
967
972
 
@@ -1026,7 +1031,6 @@ module MkKernel
1026
1031
  no_match_check: no_match_check,
1027
1032
  undef_no_match: undef_no_match,
1028
1033
  mask_self: mask_self,
1029
- mask_query: mask_query,
1030
1034
  runtime_args: runtime_args || [],
1031
1035
  fallback: fallback,
1032
1036
  semantics: semantics,
@@ -1774,13 +1778,15 @@ module MkKernel
1774
1778
 
1775
1779
  # Emits the native function of one source data type. The object lane
1776
1780
  # 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
+ # its function is rebuilt around ca_iter_ensure (see guard_native). So is
1782
+ # every lane of a family that passes guard: true, whose walks allocate
1783
+ # (an output mask made on the first empty slab, a scratch buffer), since a
1784
+ # failed allocation raises too. Other lanes are emitted as written.
1785
+ def self.emit_native_for(io, k, src, guard: false)
1786
+ if src == :object || guard
1781
1787
  buf = StringIO.new
1782
1788
  yield buf
1783
- io.print guard_object_native(buf.string, "#{k[:name]}_ki_native_object")
1789
+ io.print guard_native(buf.string, "#{k[:name]}_ki_native_#{src}")
1784
1790
  else
1785
1791
  yield io
1786
1792
  end
@@ -1821,7 +1827,7 @@ module MkKernel
1821
1827
  # frame is gone once the body raises. The body text is kept as written:
1822
1828
  # the parameters are unpacked from the frame under their own names, and
1823
1829
  # each state name is defined to its slot for the length of the body.
1824
- def self.guard_object_native(text, fn)
1830
+ def self.guard_native(text, fn)
1825
1831
  header = /^static VALUE\n#{fn} \(([^)]*)\)\n\{\n/
1826
1832
  m = header.match(text) or raise "mkkernel: #{fn} not found"
1827
1833
  close = matching_brace(text, m.end(0) - 2)
@@ -1844,9 +1850,9 @@ module MkKernel
1844
1850
 
1845
1851
  out = StringIO.new
1846
1852
  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. */"
1853
+ out.puts "/* The walks can raise part way (a Ruby call per cell in the object"
1854
+ out.puts " lane, an allocation in any lane). Their states live in the caller's"
1855
+ out.puts " frame, and ca_iter_ensure finishes them however the body leaves. */"
1850
1856
  out.puts "typedef struct {"
1851
1857
  out.puts " ca_iter_state states[#{states.size}];"
1852
1858
  params.each { |type, name| out.puts " #{type} #{name};".sub(/\* /, "*") }
@@ -1881,7 +1887,7 @@ module MkKernel
1881
1887
  io.puts
1882
1888
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
1883
1889
  k[:source].each do |src|
1884
- emit_native_for(io, k, src) { |o| emit_reduce_native(o, k, src) }
1890
+ emit_native_for(io, k, src, guard: true) { |o| emit_reduce_native(o, k, src) }
1885
1891
  end
1886
1892
  emit_reduce_dispatch(io, k)
1887
1893
  end
@@ -2178,20 +2184,22 @@ module MkKernel
2178
2184
  else
2179
2185
  if k[:array_arg]
2180
2186
  wc = (k[:array_arg][:data_type] == :promote) ? "double" : si[:c]
2181
- io.puts " CA_SLAB_REDUCE_ARRAY_T(#{si[:c]}, #{wc}, st, p, m, st_w, p_w, #{acc_var}, #{acc_init}, #{reduce_stmt});"
2187
+ io.puts " ca_size_t __sra_throwaway_mc = 0;"
2188
+ io.puts " CA_SLAB_REDUCE_ARRAY_T_EX(#{si[:c]}, #{wc}, st, p, m, st_w, p_w, #{acc_var}, #{acc_init}, #{reduce_stmt}, __sra_throwaway_mc);"
2189
+ io.puts " (void) __sra_throwaway_mc;"
2182
2190
  else
2183
2191
  suffix = MkKernel.reduce_macro_suffix(k, src)
2184
- # Non-mask_policy path: emit a throwaway masked_cnt for the 8-way
2185
- # macros (which always take one). Falls through to the legacy
2186
- # non-EX macro when the 8-way path is not eligible (:object src).
2187
- if MkKernel.reduce_8way_eligible?(k, src, suffix)
2192
+ # Non-mask_policy path: the slab macros always take a masked_cnt,
2193
+ # so hand them a throwaway. The generic form (suffix "", the
2194
+ # :object lane) has a wrapper that hides it.
2195
+ if suffix == ""
2196
+ io.puts " CA_SLAB_REDUCE_T(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt});"
2197
+ else
2188
2198
  io.puts " ca_size_t __sr_throwaway_mc = 0;"
2189
2199
  emit_reduce_slab_call(io, k, src, si, oi, suffix, acc_var, acc_init,
2190
2200
  reduce_stmt, "__sr_throwaway_mc",
2191
2201
  indent: " ")
2192
2202
  io.puts " (void) __sr_throwaway_mc;"
2193
- else
2194
- io.puts " CA_SLAB_REDUCE_T#{suffix}(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt});"
2195
2203
  end
2196
2204
  end
2197
2205
  finish_emit = view_flat \
@@ -3549,9 +3557,12 @@ module MkKernel
3549
3557
  wio.puts " else {"
3550
3558
  wio.puts " /* CArray val: coerce to the weight storage type (f64 for :promote, so a"
3551
3559
  wio.puts " float weight against an int source is not truncated) */"
3552
- wio.puts " vcw = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3560
+ wio.puts " /* vcv holds the view while cv is read: W-A2 below moves vcw on to"
3561
+ wio.puts " the materialised copy before it is done with cv. */"
3562
+ wio.puts " volatile VALUE vcv = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3553
3563
  wio.puts " CArray *cv;"
3554
- wio.puts " GetCArray(vcw, cv);"
3564
+ wio.puts " GetCArray(vcv, cv);"
3565
+ wio.puts " vcw = vcv;"
3555
3566
  wio.puts ""
3556
3567
  wio.puts " /* rev5 form detection: A3 (same ndim) -> A2 (1-D axis-broadcast) -> raise */"
3557
3568
  wio.puts " if ( cv->ndim == src->ndim ) {"
@@ -3565,17 +3576,21 @@ module MkKernel
3565
3576
  wio.puts " cw = cv;"
3566
3577
  wio.puts " }"
3567
3578
  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 */"
3579
+ wio.puts " /* W-A2: 1-D axis-broadcast -> materialize to full shape. cw and"
3580
+ wio.puts " its mask are made before cv is attached: making them allocates. */"
3570
3581
  wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3571
3582
  wio.puts " GetCArray(vcw, cw);"
3583
+ wio.puts " if ( ca_has_mask(cv) ) {"
3584
+ wio.puts " ca_create_mask(cw);"
3585
+ wio.puts " }"
3586
+ wio.puts " ca_attach(cv); /* window: nothing raises inside */"
3572
3587
  wio.puts " /* broadcast cv (1-D, length src->dim[slab_axes[0]]) along reduce axis */"
3573
3588
  wio.puts " {"
3574
3589
  wio.puts " int8_t baxis = slab_axes[0];"
3575
3590
  wio.puts " ca_size_t inner_stride = 1;"
3576
3591
  wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3577
3592
  wio.puts " ca_size_t axis_n = src->dim[baxis];"
3578
- wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3593
+ wio.puts " ca_size_t outer = ( axis_n && inner_stride ) ? src->elements / (axis_n * inner_stride) : 0; /* a zero-length axis leaves nothing to fill */"
3579
3594
  wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3580
3595
  wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3581
3596
  wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
@@ -3587,15 +3602,14 @@ module MkKernel
3587
3602
  wio.puts " }"
3588
3603
  wio.puts " /* Propagate cv mask via W-A3 overlay path below (= mask materialize) */"
3589
3604
  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);"
3605
+ wio.puts " /* broadcast cv mask along axis */"
3592
3606
  wio.puts " boolean8_t *cw_m = (boolean8_t *)cw->mask->ptr;"
3593
3607
  wio.puts " boolean8_t *cv_m = (boolean8_t *)cv->mask->ptr;"
3594
3608
  wio.puts " int8_t baxis = slab_axes[0];"
3595
3609
  wio.puts " ca_size_t inner_stride = 1;"
3596
3610
  wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3597
3611
  wio.puts " ca_size_t axis_n = src->dim[baxis];"
3598
- wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3612
+ wio.puts " ca_size_t outer = ( axis_n && inner_stride ) ? src->elements / (axis_n * inner_stride) : 0; /* a zero-length axis leaves nothing to fill */"
3599
3613
  wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3600
3614
  wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3601
3615
  wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
@@ -3813,14 +3827,18 @@ module MkKernel
3813
3827
 
3814
3828
  # empty: :undef -> allocate the output mask up front when the input
3815
3829
  # carries one (a fiber can only go UNDEF on its leading masked cells,
3816
- # which exist only when the input is masked). co_root / op_mask are the
3817
- # value / mask bases used to derive the per-fiber mask base parallel to
3818
- # the aliased output value slab.
3830
+ # which exist only when the input is masked). The output is a fresh
3831
+ # row-major entity, and the walk cuts its slabs from a buffer laid out
3832
+ # the same way (the output's memory, or the walk's scratch when it
3833
+ # delivers by transfer), so a slab's offset from st_out.alias_ptr is
3834
+ # its offset in the output, and op_mask + that offset / cell size is the
3835
+ # fiber's mask base.
3819
3836
  mask_setup =
3820
3837
  if gated
3838
+ # Before the walks open: creating the mask allocates, and a raise
3839
+ # inside them would jump past their finish.
3821
3840
  "\n" \
3822
3841
  " boolean8_t *op_mask = NULL;\n" \
3823
- " char *co_root = (char *) co->ptr;\n" \
3824
3842
  " if ( ca_has_mask(ca) ) {\n" \
3825
3843
  " ca_create_mask(co);\n" \
3826
3844
  " op_mask = (boolean8_t *) co->mask->ptr;\n" \
@@ -3835,7 +3853,7 @@ module MkKernel
3835
3853
  " st_out, po, #{init_expr}, #{step_stmt});"
3836
3854
  elsif gated
3837
3855
  "boolean8_t *mo_fiber = op_mask\n" \
3838
- " ? op_mask + (ca_size_t)(po - co_root) / (ca_size_t) co->bytes\n" \
3856
+ " ? op_mask + (ca_size_t)(po - st_out.alias_ptr) / (ca_size_t) co->bytes\n" \
3839
3857
  " : NULL;\n" \
3840
3858
  " CA_SLAB_SCAN_T_GATED(#{si[:c]}, #{oi[:c]}, st_in, pi, mi,\n" \
3841
3859
  " st_out, po, mo_fiber, #{init_expr}, #{step_stmt});"
@@ -3859,6 +3877,7 @@ module MkKernel
3859
3877
  INIT inside the macro. */
3860
3878
  int8_t slab_axes[CA_RANK_MAX];
3861
3879
  slab_axes[0] = (int8_t) axis;
3880
+ #{mask_setup}
3862
3881
  ca_iter_state st_in, st_out;
3863
3882
  int rc;
3864
3883
  rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,
@@ -3874,7 +3893,6 @@ module MkKernel
3874
3893
  rb_raise(rb_eRuntimeError,
3875
3894
  "%s: output init failed rc=%d", ca_calling_method_name(), rc);
3876
3895
  }
3877
- #{mask_setup}
3878
3896
  char *pi, *po;
3879
3897
  boolean8_t *mi, *mo;
3880
3898
  while ( ca_iter_state_next_slab_axes(&st_in, &pi, &mi) &&
@@ -3968,11 +3986,7 @@ module MkKernel
3968
3986
  io.puts " if ( TYPE(raxis) == T_ARRAY ) {"
3969
3987
  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());]
3970
3988
  io.puts " }"
3971
- io.puts " int axis = (int) ca_axis_integer(raxis, NULL);"
3972
- io.puts " if ( axis < 0 ) axis += src->ndim;"
3973
- io.puts " if ( axis < 0 || axis >= 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);]
3975
- io.puts " }"
3989
+ io.puts " int axis = ca_normalize_axis_arg(raxis, src->ndim, NULL);"
3976
3990
  io.puts " switch ( src->data_type ) {"
3977
3991
  k[:source].each do |s|
3978
3992
  si = DTYPES[s]
@@ -4134,35 +4148,15 @@ module MkKernel
4134
4148
  io.puts " const #{name}_pair_#{src} *pa = (const #{name}_pair_#{src} *) a;"
4135
4149
  io.puts " const #{name}_pair_#{src} *pb = (const #{name}_pair_#{src} *) b;"
4136
4150
  if is_object
4137
- # PROPOSAL_MKKERNEL_OBJECT_DTYPE_BRANCH Phase 2: CA_OBJECT cmp via
4138
- # Ruby Comparable (= `<=>`). Any Comparable element type (Numeric,
4139
- # String, custom class with <=> defined) works. Raises Ruby
4140
- # TypeError mid-sort if a cell pair is uncomparable -- consistent
4141
- # with Array#sort behavior.
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;"
4156
- io.puts " }"
4151
+ # The object order: `<=>`, a Float NaN after every other value
4152
+ # (ca_obj_order_cmp); two NaNs fall to the index tie-break.
4153
+ io.puts " int c = ca_obj_order_cmp(pa->v, pb->v);"
4154
+ io.puts " if ( c != 0 ) return c;"
4155
+ elsif is_fp
4156
+ # The float order: NaN after every number (CA_FLOAT_ORDER_CMP).
4157
+ io.puts " int c = CA_FLOAT_ORDER_CMP(pa->v, pb->v);"
4158
+ io.puts " if ( c != 0 ) return c;"
4157
4159
  else
4158
- if is_fp
4159
- # NaN policy :end -- NaN sorts to the end (matches qcmp_f_type in
4160
- # carray_sort.c).
4161
- io.puts " int nan_a = isnan(pa->v);"
4162
- io.puts " int nan_b = isnan(pb->v);"
4163
- io.puts " if ( nan_a && !nan_b ) return 1;"
4164
- io.puts " if ( nan_b && !nan_a ) return -1;"
4165
- end
4166
4160
  io.puts " if ( pa->v > pb->v ) return 1;"
4167
4161
  io.puts " if ( pa->v < pb->v ) return -1;"
4168
4162
  end
@@ -4183,7 +4177,7 @@ module MkKernel
4183
4177
  if is_fixlen
4184
4178
  io.puts " buf[#{dst}].vp = pi + #{kexpr} * istride; buf[#{dst}].nb = (ca_size_t) ca->bytes;"
4185
4179
  else
4186
- io.puts " buf[#{dst}].v = *(#{si[:c]} *) (pi + #{kexpr} * istride);"
4180
+ io.puts " memcpy(&buf[#{dst}].v, pi + #{kexpr} * istride, sizeof(buf[#{dst}].v));"
4187
4181
  end
4188
4182
  end
4189
4183
 
@@ -4279,21 +4273,10 @@ module MkKernel
4279
4273
  io.puts " when input has any mask. */"
4280
4274
  io.puts " if ( ca_has_mask(ca) ) ca_create_mask(co);"
4281
4275
  end
4282
- io.puts " ca_iter_state st_in, st_out;"
4283
- io.puts " int rc;"
4284
- io.puts " rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,"
4285
- io.puts " slab_axes, 1, 0);"
4286
- io.puts " if ( rc != CA_ITER_OK ) {"
4287
- io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: input init failed rc=%d", ca_calling_method_name(), rc);]
4288
- io.puts " }"
4289
- io.puts " rc = ca_iter_state_init_l2_paired(&st_out, &st_in, co, CA_SLAB_AXES,"
4290
- io.puts " slab_axes, 1, CA_KERNEL_WRITE);"
4291
- io.puts " if ( rc != CA_ITER_OK ) {"
4292
- io.puts " ca_iter_state_finish(&st_in);"
4293
- io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: output init failed rc=%d", ca_calling_method_name(), rc);]
4294
- io.puts " }"
4295
- io.puts
4296
- io.puts " ca_size_t fiber_n = st_in.slab_elements;"
4276
+ # The buffers are taken before the walks open: an allocation that
4277
+ # raises inside them would jump past their finish. The slab is the
4278
+ # sort axis alone, so a fiber holds ca->dim[axis] cells.
4279
+ io.puts " ca_size_t fiber_n = ca->dim[axis];"
4297
4280
  if src == :object
4298
4281
  # The object comparator calls <=>, so the sort can raise part way.
4299
4282
  # ALLOCV frees these buffers whether the function returns or unwinds.
@@ -4306,19 +4289,39 @@ module MkKernel
4306
4289
  io.puts " #{name}_pair_#{src} *aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4307
4290
  end
4308
4291
  else
4309
- io.puts " #{name}_pair_#{src} *buf ="
4310
- io.puts " xmalloc(sizeof(#{name}_pair_#{src}) * (size_t) fiber_n);"
4292
+ io.puts " volatile VALUE hbuf = 0, haux = 0;"
4293
+ io.puts " #{name}_pair_#{src} *buf = ALLOCV_N(#{name}_pair_#{src}, hbuf, fiber_n);"
4311
4294
  end
4312
- if has_kind && src != :object
4313
- # PROPOSAL_PORTABLE_TEXTBOOK_SORT §9.5.2 Option γ: aux pair buffer
4314
- # for ca_sort_merge_pair_<src> (= ping-pong bottom-up mergesort).
4315
- # Allocated once outside the fiber loop, reused across fibers.
4295
+ if has_kind && %i[f32 f64].include?(src)
4296
+ # aux is the mergesort's ping-pong buffer and, before that, the
4297
+ # scratch of the NaN pre-partition, so a float kernel always takes it.
4298
+ io.puts " #{name}_pair_#{src} *aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4299
+ elsif has_kind && src != :object && src != :fixlen
4300
+ # The aux pair buffer for ca_sort_merge_pair_<src> (a ping-pong
4301
+ # bottom-up mergesort), taken once outside the fiber loop. The fixlen
4302
+ # lane sorts with qsort and needs none.
4316
4303
  io.puts " #{name}_pair_#{src} *aux = NULL;"
4317
4304
  io.puts " if ( do_stable ) {"
4318
- io.puts " aux = xmalloc(sizeof(#{name}_pair_#{src}) * (size_t) fiber_n);"
4305
+ io.puts " aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4319
4306
  io.puts " }"
4307
+ elsif src != :object
4308
+ io.puts " (void) haux;"
4320
4309
  end
4321
4310
 
4311
+ io.puts " ca_iter_state st_in, st_out;"
4312
+ io.puts " int rc;"
4313
+ io.puts " rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,"
4314
+ io.puts " slab_axes, 1, 0);"
4315
+ io.puts " if ( rc != CA_ITER_OK ) {"
4316
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: input init failed rc=%d", ca_calling_method_name(), rc);]
4317
+ io.puts " }"
4318
+ io.puts " rc = ca_iter_state_init_l2_paired(&st_out, &st_in, co, CA_SLAB_AXES,"
4319
+ io.puts " slab_axes, 1, CA_KERNEL_WRITE);"
4320
+ io.puts " if ( rc != CA_ITER_OK ) {"
4321
+ io.puts " ca_iter_state_finish(&st_in);"
4322
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: output init failed rc=%d", ca_calling_method_name(), rc);]
4323
+ io.puts " }"
4324
+
4322
4325
  if view_flat
4323
4326
  io.puts
4324
4327
  io.puts " /* :view_flat semantics: kernel computes view-flat address"
@@ -4370,7 +4373,7 @@ module MkKernel
4370
4373
  if is_fixlen
4371
4374
  io.puts " buf[k].vp = pi + k * istride; buf[k].nb = (ca_size_t) ca->bytes;"
4372
4375
  else
4373
- io.puts " buf[k].v = *(#{si[:c]} *) (pi + k * istride);"
4376
+ io.puts " memcpy(&buf[k].v, pi + k * istride, sizeof(buf[k].v));"
4374
4377
  end
4375
4378
  io.puts " buf[k].i = #{payload.call("k")};"
4376
4379
  io.puts " }"
@@ -4385,7 +4388,7 @@ module MkKernel
4385
4388
  if is_fixlen
4386
4389
  io.puts " buf[k_unmasked].vp = pi + k * istride; buf[k_unmasked].nb = (ca_size_t) ca->bytes;"
4387
4390
  else
4388
- io.puts " buf[k_unmasked].v = *(#{si[:c]} *) (pi + k * istride);"
4391
+ io.puts " memcpy(&buf[k_unmasked].v, pi + k * istride, sizeof(buf[k_unmasked].v));"
4389
4392
  end
4390
4393
  io.puts " buf[k_unmasked].i = k;"
4391
4394
  io.puts " k_unmasked++;"
@@ -4401,7 +4404,7 @@ module MkKernel
4401
4404
  if is_fixlen
4402
4405
  io.puts " buf[k].vp = pi + k * istride; buf[k].nb = (ca_size_t) ca->bytes;"
4403
4406
  else
4404
- io.puts " buf[k].v = *(#{si[:c]} *) (pi + k * istride);"
4407
+ io.puts " memcpy(&buf[k].v, pi + k * istride, sizeof(buf[k].v));"
4405
4408
  end
4406
4409
  io.puts " buf[k].i = k;"
4407
4410
  io.puts " }"
@@ -4474,7 +4477,7 @@ module MkKernel
4474
4477
  io.puts " /* P.3/P.4: NaN pre-partition for float argsort -- NaN pairs are"
4475
4478
  io.puts " pushed to the tail (= NaN-at-end policy, matches sort_copy"
4476
4479
  io.puts " AC8). Only the finite slice is sorted. */"
4477
- io.puts " ca_size_t finite_n = ca_partition_nan_pair_#{src}((ca_pair_#{src} *) #{buf_expr}, (ca_size_t) #{sort_n_expr});"
4480
+ io.puts " ca_size_t finite_n = ca_partition_nan_pair_#{src}((ca_pair_#{src} *) #{buf_expr}, (ca_size_t) #{sort_n_expr}, (ca_pair_#{src} *) aux);"
4478
4481
  io.puts " if ( do_stable ) {"
4479
4482
  io.puts " ca_sort_merge_pair_#{src}((ca_pair_#{src} *) #{buf_expr}, (ca_pair_#{src} *) aux, finite_n);"
4480
4483
  io.puts " } else {"
@@ -4502,16 +4505,21 @@ module MkKernel
4502
4505
  io.puts " gaps ordinal would have spent on the tie are never used, so a"
4503
4506
  io.puts " later out_r stays <= the ordinal r at that position, preserving"
4504
4507
  io.puts " both the ascending order and (via the order() method's"
4505
- io.puts " (n-1)-out_r descending transform) the descending order. NaN"
4506
- io.puts " cells never tie with each other under dense (IEEE754 x != x). */"
4508
+ io.puts " descending transform) the descending order. NaN cells tie"
4509
+ io.puts " with each other, as sort_addr and the value-hash family treat"
4510
+ io.puts " every NaN as one value. */"
4507
4511
  io.puts " ca_size_t out_r = 0;"
4508
4512
  io.puts " for ( ca_size_t r = 0; r < #{rank_n}; r++ ) {"
4509
4513
  io.puts " if ( r > 0 ) {"
4510
4514
  io.puts " if ( dense ) {"
4511
4515
  if is_object
4512
- io.puts " if ( ! ca_obj_equal(buf[r].v, buf[r - 1].v) ) out_r++;"
4516
+ io.puts " if ( ! ( ( CA_OBJ_ISNAN(buf[r].v) && CA_OBJ_ISNAN(buf[r - 1].v) )"
4517
+ io.puts " || ca_obj_equal(buf[r].v, buf[r - 1].v) ) ) out_r++;"
4513
4518
  elsif is_fixlen
4514
4519
  io.puts " if ( memcmp(buf[r].vp, buf[r - 1].vp, (size_t) buf[r].nb) != 0 ) out_r++;"
4520
+ elsif FLOAT_DTYPES.include?(src)
4521
+ io.puts " if ( buf[r].v != buf[r - 1].v"
4522
+ io.puts " && ! ( isnan(buf[r].v) && isnan(buf[r - 1].v) ) ) out_r++;"
4515
4523
  else
4516
4524
  io.puts " if ( buf[r].v != buf[r - 1].v ) out_r++;"
4517
4525
  end
@@ -4521,15 +4529,32 @@ module MkKernel
4521
4529
  io.puts " }"
4522
4530
  io.puts " *(#{oi[:c]} *) (po + buf[r].i * ostride) = (#{oi[:c]}) out_r;"
4523
4531
  io.puts " }"
4532
+ # dense == 2 (from CArray#order only): descending dense ranks, the
4533
+ # largest value 0, the NaN cells (sorted to the end) after the numbers.
4534
+ nan_test = if is_object then "CA_OBJ_ISNAN(buf[nan_start - 1].v)"
4535
+ elsif FLOAT_DTYPES.include?(src) then "isnan(buf[nan_start - 1].v)"
4536
+ end
4537
+ io.puts " if ( dense == 2 && #{rank_n} > 0 ) {"
4538
+ io.puts " ca_size_t nan_start = #{rank_n};"
4539
+ io.puts " while ( nan_start > 0 && #{nan_test} ) nan_start--;" if nan_test
4540
+ io.puts " #{oi[:c]} top = nan_start > 0"
4541
+ io.puts " ? *(#{oi[:c]} *) (po + buf[nan_start - 1].i * ostride) : (#{oi[:c]}) -1;"
4542
+ io.puts " for ( ca_size_t r = 0; r < #{rank_n}; r++ ) {"
4543
+ io.puts " #{oi[:c]} *o = (#{oi[:c]} *) (po + buf[r].i * ostride);"
4544
+ io.puts " *o = ( r < nan_start ) ? top - *o : top + 1;"
4545
+ io.puts " }"
4546
+ io.puts " }"
4524
4547
  if mask_skip
4525
4548
  io.puts " /* Mirror input mask to output (= UNDEF rank for masked"
4526
- io.puts " cells). Write via co->mask->ptr directly with the"
4527
- io.puts " output slab offset, because the iter's mo isn't"
4528
- io.puts " populated in CA_KERNEL_WRITE mode (= reduce form's"
4529
- io.puts " op_mask pattern). */"
4549
+ io.puts " cells). Write via co->mask->ptr directly, because a"
4550
+ io.puts " mask written through a WRITE walk's cursor does not"
4551
+ io.puts " reach the array. co is a fresh row-major entity and"
4552
+ io.puts " the walk cuts its slabs from a buffer laid out the"
4553
+ io.puts " same way, so the slab's offset from st_out.alias_ptr"
4554
+ io.puts " is its offset in co. */"
4530
4555
  io.puts " if ( mi && co->mask ) {"
4531
4556
  io.puts " boolean8_t *omask_base = (boolean8_t *) co->mask->ptr"
4532
- io.puts " + ((po - (char *) co->ptr) / (ca_size_t) co->bytes);"
4557
+ io.puts " + ((po - st_out.alias_ptr) / (ca_size_t) co->bytes);"
4533
4558
  io.puts " ca_size_t mostride = st_out.slab_mask_strides[0];"
4534
4559
  io.puts " for ( ca_size_t k = 0; k < fiber_n; k++ ) {"
4535
4560
  io.puts " if ( mi[k * mstride] ) omask_base[k * mostride] = 1;"
@@ -4555,45 +4580,33 @@ module MkKernel
4555
4580
  end
4556
4581
 
4557
4582
  io.puts " }"
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
4566
- end
4583
+ io.puts " ALLOCV_END(hbuf);"
4584
+ io.puts " ALLOCV_END(haux);"
4567
4585
  io.puts " ca_iter_state_finish(&st_in);"
4568
4586
  io.puts " ca_iter_state_finish(&st_out);"
4569
4587
  io.puts " return vout;"
4570
4588
  io.puts "}"
4571
4589
  end
4572
4590
 
4573
- # Shared sort-family Face gate. Descends a Face to its storage: a fixlen
4574
- # storage sorts by memcmp (the default order for fixlen, the same as a plain
4575
- # fixlen array -- a struct/record Face gets a deterministic byte order, and
4576
- # opts into a semantic order separately); a numeric storage requires
4577
- # ORDERABLE so the numeric order equals the surface order, else it raises.
4578
- # `src`/`self` are rebound to the stripped storage.
4591
+ # Shared sort-family Face gate. Every kernel of the family answers
4592
+ # positions, so it runs on the array the Face's order is read from
4593
+ # (ca_order_key): the storage of an ORDERABLE Face, the Face's own dense
4594
+ # rank for a fixlen storage that is not ORDERABLE (CAConstString's offsets,
4595
+ # a record's bytes), and a refusal otherwise. `src`/`self` are rebound to
4596
+ # that array.
4579
4597
  def self.emit_sort_face_gate(io, name)
4580
4598
  io.puts " if ( ca_is_face(src) ) {"
4581
- io.puts " VALUE _face = self;"
4582
- io.puts " int _orderable = ca_test_flag(src, CA_FLAG_FACE_ORDERABLE_STORAGE);"
4583
- io.puts " VALUE _st = rb_ca_strip_face_value(self);"
4584
- io.puts " CArray *_sc;"
4585
- io.puts " GetCArray(_st, _sc);"
4586
- io.puts " if ( _sc->data_type != CA_FIXLEN && ! _orderable ) {"
4587
- io.puts %Q[ ca_face_refuse_order(ca_calling_method_name(), _face, "order");]
4588
- io.puts " }"
4589
- io.puts " self = _st;"
4590
- io.puts " src = _sc;"
4599
+ io.puts " self = ca_order_key(self, ca_calling_method_name());"
4600
+ io.puts " GetCArray(self, src);"
4591
4601
  io.puts " }"
4592
4602
  end
4593
4603
 
4594
- # Reduce-family Face gate. Same ORDERABLE-descent policy as
4595
- # emit_sort_face_gate (fixlen storage -> memcmp default; numeric storage
4596
- # -> ORDERABLE required, else raise), rebinding self/src to storage. When
4604
+ # Reduce-family Face gate. Descends an ORDERABLE Face to its storage and
4605
+ # refuses any other (ca_face_require_orderable): a fixlen storage is
4606
+ # reduced by memcmp only when the Face declares that its bytes are its
4607
+ # order (CAFixlenString). CAConstString and CARecord, whose bytes are no
4608
+ # order, answer min / max / minmax in Ruby and do not reach this gate.
4609
+ # self/src are rebound to storage. When
4597
4610
  # `relift` is set (value-returning kernels), the original Face is saved to
4598
4611
  # the function-scope `_reduce_face` so the dispatcher can re-lift the output.
4599
4612
  def self.emit_reduce_face_gate(io, name, relift)
@@ -4602,13 +4615,11 @@ module MkKernel
4602
4615
  end
4603
4616
  io.puts " if ( ca_is_face(src) ) {"
4604
4617
  io.puts " VALUE _face = self;"
4605
- io.puts " int _orderable = ca_test_flag(src, CA_FLAG_FACE_ORDERABLE_STORAGE);"
4606
- io.puts " VALUE _st = rb_ca_strip_face_value(self);"
4618
+ io.puts " VALUE _st;"
4607
4619
  io.puts " CArray *_sc;"
4620
+ io.puts %Q[ ca_face_require_orderable(_face, ca_calling_method_name(), "order");]
4621
+ io.puts " _st = rb_ca_strip_face_value(self);"
4608
4622
  io.puts " GetCArray(_st, _sc);"
4609
- io.puts " if ( _sc->data_type != CA_FIXLEN && ! _orderable ) {"
4610
- io.puts %Q[ ca_face_refuse_order(ca_calling_method_name(), _face, "order");]
4611
- io.puts " }"
4612
4623
  if relift
4613
4624
  io.puts " _reduce_face = _face;"
4614
4625
  end
@@ -4713,10 +4724,10 @@ module MkKernel
4713
4724
  io.puts " CArray *src;"
4714
4725
  io.puts " GetCArray(self, src);"
4715
4726
  emit_lazy_evaluated(io)
4716
- io.puts " /* Face gate: descend to storage (fixlen -> memcmp default,"
4717
- io.puts " numeric -> ORDERABLE required). The strip also makes the"
4718
- io.puts " switch below and every index/addr template output build on"
4719
- io.puts " the plain storage, not a face-lifted (NULL-ptr) view. */"
4727
+ io.puts " /* Face gate: run on the array the Face's order is read from."
4728
+ io.puts " It is never a Face, so the switch below and every index/addr"
4729
+ io.puts " template output build on a plain array, not a face-lifted"
4730
+ io.puts " (NULL-ptr) view. */"
4720
4731
  emit_sort_face_gate(io, name)
4721
4732
  if k[:mask_self] == :raise
4722
4733
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
@@ -4725,11 +4736,7 @@ module MkKernel
4725
4736
  io.puts %Q[ rb_raise(rb_eArgError, "%s: masked input not supported (use ca.value or ca.strip_mask(fill))", ca_calling_method_name());]
4726
4737
  io.puts " }"
4727
4738
  end
4728
- io.puts " int axis = (int) ca_axis_integer(vaxis, NULL);"
4729
- io.puts " if ( axis < 0 ) axis += src->ndim;"
4730
- io.puts " if ( axis < 0 || axis >= 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);]
4732
- io.puts " }"
4739
+ io.puts " int axis = ca_normalize_axis_arg(vaxis, src->ndim, NULL);"
4733
4740
  io.puts " switch ( src->data_type ) {"
4734
4741
  k[:source].each do |s|
4735
4742
  si = DTYPES[s]
@@ -4825,8 +4832,7 @@ module MkKernel
4825
4832
  io.puts " CArray *src;"
4826
4833
  io.puts " GetCArray(self, src);"
4827
4834
  emit_lazy_evaluated(io)
4828
- io.puts " /* Face gate (partition): same as the sort dispatcher --"
4829
- io.puts " fixlen -> memcmp default, numeric -> ORDERABLE required. */"
4835
+ io.puts " /* Face gate (partition): same as the sort dispatcher. */"
4830
4836
  emit_sort_face_gate(io, name)
4831
4837
  if k[:mask_self] == :raise
4832
4838
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
@@ -4835,11 +4841,7 @@ module MkKernel
4835
4841
  io.puts %Q[ rb_raise(rb_eArgError, "%s: masked input not supported (use ca.value or ca.strip_mask(fill))", ca_calling_method_name());]
4836
4842
  io.puts " }"
4837
4843
  end
4838
- io.puts " int axis = (int) ca_axis_integer(vaxis, NULL);"
4839
- io.puts " if ( axis < 0 ) axis += src->ndim;"
4840
- io.puts " if ( axis < 0 || axis >= 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);]
4842
- io.puts " }"
4844
+ io.puts " int axis = ca_normalize_axis_arg(vaxis, src->ndim, NULL);"
4843
4845
  io.puts " ca_size_t kth_raw = (ca_size_t) ca_integer_arg(vkth, \"kth\", NULL);"
4844
4846
  io.puts " ca_size_t kth = kth_raw;"
4845
4847
  io.puts " ca_size_t axis_n = src->dim[axis];"
@@ -5017,7 +5019,7 @@ module MkKernel
5017
5019
  io.puts
5018
5020
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
5019
5021
  k[:source].each do |src|
5020
- emit_native_for(io, k, src) { |o| emit_search_native(o, k, src) }
5022
+ emit_native_for(io, k, src, guard: true) { |o| emit_search_native(o, k, src) }
5021
5023
  end
5022
5024
  emit_search_dispatch(io, k)
5023
5025
  end
@@ -5175,56 +5177,41 @@ module MkKernel
5175
5177
  # flat array path. op_mask is lazily allocated on the first no-match.
5176
5178
  undef_nm = k[:undef_no_match]
5177
5179
  nm_cond = "result == (#{oi[:c]})(#{no_match})"
5178
- # mask_query: :undef -- a masked query cell yields a masked output cell,
5179
- # so an undetermined query cannot be answered from the value that happens
5180
- # to sit under the mask. Shares op_mask with undef_no_match.
5181
- undef_q = (k[:mask_query] == :undef)
5182
- # op_mask declaration (only when an output cell can be masked);
5183
- # content-only (the heredoc carries indentation).
5184
- op_mask_decl = (undef_nm || undef_q) \
5185
- ? "boolean8_t *op_mask = NULL; /* lazily allocated on the first masked output cell */" \
5186
- : "/* (no masked output cell) */"
5180
+ # A masked query cell yields a masked output cell, so an undetermined
5181
+ # query cannot be answered from the value that happens to sit under the
5182
+ # mask. Shares op_mask with undef_no_match.
5183
+ op_mask_decl = "boolean8_t *op_mask = NULL; /* lazily allocated on the first masked output cell */"
5187
5184
 
5188
5185
  # Case A scalar UNDEF query (also reached by the A1 single-element
5189
5186
  # reduction when that element is masked): every output cell is
5190
5187
  # undetermined, so short-circuit before the query is coerced to a number.
5191
- undef_query_a = if undef_q
5192
- <<~C.chomp
5193
- if ( rval == CA_UNDEF ) {
5194
- int8_t undef_axes[1] = { (int8_t) axis };
5195
- VALUE voutu = rb_ca_new_reduced(self, undef_axes, 1, #{oi[:ca]}, 0);
5196
- CArray *cou;
5197
- GetCArray(voutu, cou);
5198
- MEMZERO((#{oi[:c]} *) cou->ptr, #{oi[:c]}, cou->elements);
5199
- ca_create_mask(cou);
5200
- memset(cou->mask->ptr, 1, (size_t) cou->elements);
5201
- if ( ca->ndim == 1 ) { return Qnil; }
5202
- return voutu;
5203
- }
5204
- C
5205
- else
5206
- "/* (mask_query: :ignore) */"
5207
- end
5188
+ undef_query_a = <<~C.chomp
5189
+ if ( rval == CA_UNDEF ) {
5190
+ int8_t undef_axes[1] = { (int8_t) axis };
5191
+ VALUE voutu = rb_ca_new_reduced(self, undef_axes, 1, #{oi[:ca]}, 0);
5192
+ CArray *cou;
5193
+ GetCArray(voutu, cou);
5194
+ MEMZERO((#{oi[:c]} *) cou->ptr, #{oi[:c]}, cou->elements);
5195
+ ca_create_mask(cou);
5196
+ memset(cou->mask->ptr, 1, (size_t) cou->elements);
5197
+ if ( ca->ndim == 1 ) { return Qnil; }
5198
+ return voutu;
5199
+ }
5200
+ C
5208
5201
 
5209
5202
  # Case B/C query mask base. NULL when the query carries no mask.
5210
- cv_mask_decl_bc = undef_q \
5211
- ? "boolean8_t *cv_mask_base = ca_has_mask(cv) ? (boolean8_t *) cv->mask->ptr : NULL;" \
5212
- : "/* (mask_query: :ignore) */"
5203
+ cv_mask_decl_bc = "boolean8_t *cv_mask_base = ca_has_mask(cv) ? (boolean8_t *) cv->mask->ptr : NULL;"
5213
5204
 
5214
5205
  # Per-cell guard wrapping the body + write in case B/C. A masked query
5215
5206
  # cell skips the body entirely and marks the output cell instead.
5216
- undef_query_open_bc = if undef_q
5217
- <<~C.chomp
5218
- if ( cv_mask_base && cv_mask_base[val_offset / cv->bytes] ) {
5219
- if ( ! op_mask ) { ca_create_mask(co); op_mask = (boolean8_t *) co->mask->ptr; }
5220
- op_mask[flat] = 1;
5221
- op[flat] = (#{oi[:c]}) 0; /* undetermined query; the mask bit is what counts */
5222
- } else {
5223
- C
5224
- else
5225
- "/* (mask_query: :ignore) */"
5226
- end
5227
- undef_query_close_bc = undef_q ? "}" : "/* (mask_query: :ignore) */"
5207
+ undef_query_open_bc = <<~C.chomp
5208
+ if ( cv_mask_base && cv_mask_base[val_offset / cv->bytes] ) {
5209
+ if ( ! op_mask ) { ca_create_mask(co); op_mask = (boolean8_t *) co->mask->ptr; }
5210
+ op_mask[flat] = 1;
5211
+ op[flat] = (#{oi[:c]}) 0; /* undetermined query; the mask bit is what counts */
5212
+ } else {
5213
+ C
5214
+ undef_query_close_bc = "}"
5228
5215
 
5229
5216
  # Per-cell write block for case A (scalar query). `out_i` is the output
5230
5217
  # cursor, `co` the output CArray. When undef_no_match: a no-match masks
@@ -5292,7 +5279,7 @@ module MkKernel
5292
5279
  CArray *co;
5293
5280
  GetCArray(vout, co);
5294
5281
  #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
5295
- #{op_mask_decl}
5282
+ #{undef_nm ? op_mask_decl : ""}
5296
5283
 
5297
5284
  ca_iter_state st;
5298
5285
  int rc = ca_iter_state_init_l2(&st, ca, CA_SLAB_AXES,
@@ -5514,6 +5501,12 @@ module MkKernel
5514
5501
  #{op_mask_decl}
5515
5502
 
5516
5503
  #{entity_bc}
5504
+ /* The output mask is made before the window opens, since making it
5505
+ allocates; it comes off again below when no cell needed it. */
5506
+ if ( #{undef_nm ? "1" : "ca_has_mask(cv)"} ) {
5507
+ ca_create_mask(co);
5508
+ op_mask = (boolean8_t *) co->mask->ptr;
5509
+ }
5517
5510
  /* Attach self + val; rev4 uses raw row-major byte addressing. */
5518
5511
  ca_attach_n(2, ca, cv); /* window: numeric bodies do not raise; the object lane reads entities */
5519
5512
 
@@ -5620,6 +5613,11 @@ module MkKernel
5620
5613
 
5621
5614
  ca_detach(ca);
5622
5615
  ca_detach(cv);
5616
+ if ( op_mask && ! memchr(op_mask, 1, (size_t) co->elements) ) {
5617
+ CArray *unused = co->mask;
5618
+ co->mask = NULL;
5619
+ ca_free(unused);
5620
+ }
5623
5621
  return vout;
5624
5622
  }
5625
5623
  C
@@ -5642,21 +5640,15 @@ module MkKernel
5642
5640
  end
5643
5641
  io.puts "{"
5644
5642
  io.puts " CArray *src;"
5645
- io.puts " VALUE self_ref = self; /* pre-strip Face, for to_comparable */"
5646
5643
  io.puts " int self_was_face = 0;"
5647
5644
  io.puts " GetCArray(self, src);"
5648
5645
  emit_lazy_evaluated(io)
5649
- io.puts " /* Face gate (self): descend a Face reference to storage so the"
5650
- io.puts " search runs on the numeric storage. ORDERABLE licenses the"
5651
- io.puts " ordered/searchable descent; the query gate below brings the"
5652
- io.puts " query into the same space. */"
5646
+ io.puts " /* Face gate (self): a Face reference is searched on its numeric"
5647
+ io.puts " storage, which ORDERABLE licenses; the query gate below brings"
5648
+ io.puts " the reference and the query into one space and descends. */"
5653
5649
  io.puts " if ( ca_is_face(src) ) {"
5654
- io.puts " if ( ! ca_test_flag(src, CA_FLAG_FACE_ORDERABLE_STORAGE) ) {"
5655
- io.puts %Q[ ca_face_refuse_order(ca_calling_method_name(), self, "order");]
5656
- io.puts " }"
5650
+ io.puts %Q[ ca_face_require_orderable(self, ca_calling_method_name(), "order");]
5657
5651
  io.puts " self_was_face = 1;"
5658
- io.puts " self = rb_ca_strip_face_value(self);"
5659
- io.puts " GetCArray(self, src);"
5660
5652
  io.puts " }"
5661
5653
 
5662
5654
  if has_eps
@@ -5667,38 +5659,38 @@ module MkKernel
5667
5659
  io.puts " VALUE rval = argv[0];"
5668
5660
  io.puts " VALUE raxis = argv[1];"
5669
5661
  io.puts " VALUE reps = (argc >= 3) ? argv[2] : Qnil;"
5662
+ io.puts " /* A tolerance is a distance: a negative one (or NaN) means nothing. */"
5663
+ io.puts " if ( ! NIL_P(reps) ) {"
5664
+ io.puts " double eps_given = NUM2DBL(reps);"
5665
+ io.puts " if ( ! ( eps_given >= 0.0 ) ) {"
5666
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: eps must be zero or positive (got %"PRIsVALUE")", ca_calling_method_name(), rb_inspect(reps));]
5667
+ io.puts " }"
5668
+ io.puts " }"
5670
5669
  end
5671
- io.puts " /* Query gate: the reference Face reconciles the query into its own"
5672
- io.puts " space (ca_face_reconcile_operand). */"
5670
+ io.puts " /* Query gate: the reference Face and the query are brought into one"
5671
+ io.puts " space (ca_face_reconcile_pair), then the reference descends to"
5672
+ io.puts " its storage. */"
5673
5673
  io.puts " if ( self_was_face ) {"
5674
- io.puts " rval = ca_face_reconcile_operand(self_ref, rval, ca_calling_method_name());"
5674
+ io.puts " volatile VALUE ref = self, query = rval;"
5675
+ io.puts " ca_face_reconcile_pair(&ref, &query, ca_calling_method_name());"
5676
+ io.puts " rval = query;"
5677
+ io.puts " self = rb_ca_strip_face_value(ref);"
5678
+ io.puts " GetCArray(self, src);"
5675
5679
  io.puts " }"
5676
5680
 
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);"
5695
- io.puts " }"
5696
- io.puts " }"
5681
+ # The query and the reference are compared in the type they match in,
5682
+ # as `a.eq(q)` compares (ca_query_match_type): the reference is read
5683
+ # through a cast view of that type, so the query is never cut to the
5684
+ # reference's type.
5685
+ io.puts " if ( ca_query_match_applies(src, rval) ) {"
5686
+ io.puts " int8_t qt = ca_query_match_type(src->data_type, rval);"
5697
5687
  io.puts " if ( qt != src->data_type ) {"
5698
5688
  io.puts " self = rb_ca_wrap_readonly(self, INT2NUM(qt));"
5699
5689
  io.puts " GetCArray(self, src);"
5700
5690
  io.puts " }"
5701
5691
  io.puts " }"
5692
+ io.puts " self = ca_fixlen_widen_for_query(self, rval);"
5693
+ io.puts " GetCArray(self, src);"
5702
5694
 
5703
5695
  case k[:mask_self]
5704
5696
  when :raise
@@ -5710,11 +5702,7 @@ module MkKernel
5710
5702
  # body handles mask_in or ignores it
5711
5703
  end
5712
5704
 
5713
- io.puts " int axis = (int) ca_axis_integer(raxis, NULL);"
5714
- io.puts " if ( axis < 0 ) axis += src->ndim;"
5715
- io.puts " if ( axis < 0 || axis >= 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);]
5717
- io.puts " }"
5705
+ io.puts " int axis = ca_normalize_axis_arg(raxis, src->ndim, NULL);"
5718
5706
  io.puts " switch ( src->data_type ) {"
5719
5707
  k[:source].each do |s|
5720
5708
  si = DTYPES[s]
@@ -5734,13 +5722,14 @@ module MkKernel
5734
5722
  # (`ca_monop_<name>_<suffix>`). mkmath uses typedef names
5735
5723
  # `float32_t` / `float64_t` here, not the raw C types `float` / `double`,
5736
5724
  # so that `ca_monop_dispatch.c`'s extern references stay ABI-stable.
5737
- # For all other data_types, the suffix matches `DTYPES[src][:c]`.
5725
+ # For all other data_types, the suffix is `DTYPES[src][:c]` without the
5726
+ # `ca_u_` of its unaligned cell typedef (ca_u_int32_t -> int32_t).
5738
5727
  def self.math_suffix(src)
5739
5728
  case src
5740
5729
  when :f32 then "float32_t"
5741
5730
  when :f64 then "float64_t"
5742
5731
  when :fixlen then "fixlen" # symbol suffix differs from c-type "char"
5743
- else DTYPES[src][:c]
5732
+ else DTYPES[src][:c].sub(/\Aca_u_/, "")
5744
5733
  end
5745
5734
  end
5746
5735
 
@@ -5819,7 +5808,8 @@ module MkKernel
5819
5808
  uses_input = raw.include?("#1")
5820
5809
  # SL.2.x phase 2 (monop emit substrate hardening, MEMO_MONOP_STRIDED_AUDIT.md):
5821
5810
  # 3-way split (mask / contig / strided) symmetric with binop SL.2.x phase 1.
5822
- # contig branch (i1==1 && i2==1) uses index form + `__restrict__` + `omp simd`.
5811
+ # contig branch (i1==1 && i2==1) uses index form + `omp simd` (`__restrict__`
5812
+ # only when input and output are apart).
5823
5813
  # strided branch uses `p += i` increment form (per-iter multiply removed).
5824
5814
  # mask branch unchanged (per-cell predicate, not SIMD-amenable).
5825
5815
  if uses_input
@@ -5827,11 +5817,11 @@ module MkKernel
5827
5817
 
5828
5818
  static void
5829
5819
  ca_monop_#{k[:name]}_#{suffix} (ca_size_t n, boolean8_t *m,
5830
- char * __restrict__ ptr1, ca_size_t i1,
5831
- char * __restrict__ ptr2, ca_size_t i2)
5820
+ char *ptr1, ca_size_t i1,
5821
+ char *ptr2, ca_size_t i2)
5832
5822
  {
5833
- #{c_type_in} * __restrict__ q1 = (#{c_type_in} *) ptr1;
5834
- #{c_type_out} * __restrict__ q2 = (#{c_type_out} *) ptr2;
5823
+ #{c_type_in} *q1 = (#{c_type_in} *) ptr1;
5824
+ #{c_type_out} *q2 = (#{c_type_out} *) ptr2;
5835
5825
  ca_size_t k;
5836
5826
  if ( m ) {
5837
5827
  #{c_type_in} *p1 = q1;
@@ -5847,17 +5837,38 @@ module MkKernel
5847
5837
  }
5848
5838
  } else if ( i1 == 1 && i2 == 1 ) {
5849
5839
  /* contig branch: SIMD-friendly form. Index-form `q[k]` exposes
5850
- the unit-stride access to the vectorizer; `__restrict__` on
5851
- q1/q2 promises no aliasing so the omp simd pragma can license
5852
- lane-parallel loads/stores. Element-wise monop has no
5853
- reassoc, so ε-close numeric contract is unaffected. */
5840
+ the unit-stride access to the vectorizer. Element-wise monop
5841
+ has no reassoc, so ε-close numeric contract is unaffected.
5842
+ The input and output are either apart, the same cells (a
5843
+ bang, or the lazy scratch applied in place), or overlapping
5844
+ otherwise; `__restrict__` is promised only when apart, since
5845
+ the body may store part of a cell before it has read it. */
5854
5846
  #{c_type_in} *p1;
5855
5847
  #{c_type_out} *p2;
5856
5848
  (void)p1; (void)p2;
5857
- #pragma omp simd
5858
- for (k=0; k<n; k++) {
5859
- p1 = q1 + k; p2 = q2 + k;
5860
- { #{expr_body} }
5849
+ uintptr_t a1 = (uintptr_t) q1, a2 = (uintptr_t) q2;
5850
+ if ( a2 + n * sizeof(*q2) <= a1 || a1 + n * sizeof(*q1) <= a2 ) {
5851
+ #{c_type_in} * __restrict__ r1 = q1;
5852
+ #{c_type_out} * __restrict__ r2 = q2;
5853
+ #pragma omp simd
5854
+ for (k=0; k<n; k++) {
5855
+ p1 = r1 + k; p2 = r2 + k;
5856
+ { #{expr_body} }
5857
+ }
5858
+ }
5859
+ else if ( a1 == a2 && sizeof(*q1) == sizeof(*q2) ) {
5860
+ /* in place: an iteration reads and writes only its own cell */
5861
+ #pragma omp simd
5862
+ for (k=0; k<n; k++) {
5863
+ p1 = q1 + k; p2 = q2 + k;
5864
+ { #{expr_body} }
5865
+ }
5866
+ }
5867
+ else {
5868
+ for (k=0; k<n; k++) {
5869
+ p1 = q1 + k; p2 = q2 + k;
5870
+ { #{expr_body} }
5871
+ }
5861
5872
  }
5862
5873
  } else {
5863
5874
  /* strided branch: numo-shape `p += s` increment form. */
@@ -6043,7 +6054,8 @@ module MkKernel
6043
6054
  expr_body = raw.gsub(/#1/, "*p1").gsub(/#2/, "*p2").gsub(/#3/, "*p3").gsub(/<type>/, suffix)
6044
6055
  # Option B+C pilot (binop emit substrate hardening, MEMO_BINOP_STRIDED_AUDIT.md):
6045
6056
  # The no-mask body is split into a contig branch (i1==i2==i3==1) using the
6046
- # `q[k]` index form with `__restrict__`-qualified pointers + `#pragma omp simd`
6057
+ # `q[k]` index form with `#pragma omp simd` (`__restrict__`-qualified only
6058
+ # when the output is apart from both inputs)
6047
6059
  # (SL.2.x-style insurance, element-wise has no reassoc -> ε-close unaffected),
6048
6060
  # and a strided branch using `p += i` increment form (numo-shape inner loop).
6049
6061
  # `i1`/`i2`/`i3` remain element units (NOT byte) -- byte-flip is Option A scope.
@@ -6053,13 +6065,13 @@ module MkKernel
6053
6065
 
6054
6066
  static void
6055
6067
  ca_binop_#{k[:name]}_#{suffix} (ca_size_t n, boolean8_t *m,
6056
- char * __restrict__ ptr1, ca_size_t i1,
6057
- char * __restrict__ ptr2, ca_size_t i2,
6058
- char * __restrict__ ptr3, ca_size_t i3)
6068
+ char *ptr1, ca_size_t i1,
6069
+ char *ptr2, ca_size_t i2,
6070
+ char *ptr3, ca_size_t i3)
6059
6071
  {
6060
- #{c_type} * __restrict__ q1 = (#{c_type} *) ptr1;
6061
- #{c_type} * __restrict__ q2 = (#{c_type} *) ptr2;
6062
- #{c_type} * __restrict__ q3 = (#{c_type} *) ptr3;
6072
+ #{c_type} *q1 = (#{c_type} *) ptr1;
6073
+ #{c_type} *q2 = (#{c_type} *) ptr2;
6074
+ #{c_type} *q3 = (#{c_type} *) ptr3;
6063
6075
  ca_size_t k;
6064
6076
  if ( m ) {
6065
6077
  #{c_type} *p1 = q1, *p2 = q2, *p3 = q3;
@@ -6075,16 +6087,41 @@ module MkKernel
6075
6087
  }
6076
6088
  } else if ( i1 == 1 && i2 == 1 && i3 == 1 ) {
6077
6089
  /* contig branch: SIMD-friendly form. Index-form `q[k]` exposes
6078
- the unit-stride access to the vectorizer; `__restrict__` on
6079
- q1/q2/q3 promises no aliasing so the omp simd pragma can
6080
- license lane-parallel loads/stores. Element-wise binop has
6081
- no reassoc, so ε-close numeric contract is unaffected. */
6090
+ the unit-stride access to the vectorizer. Element-wise binop
6091
+ has no reassoc, so ε-close numeric contract is unaffected.
6092
+ The output is apart from each input, the same cells as it (a
6093
+ bang), or overlaps it otherwise; `__restrict__` is promised
6094
+ only when apart from both, since the body may store part of a
6095
+ cell before it has read it. */
6082
6096
  #{c_type} *p1, *p2, *p3;
6097
+ uintptr_t a1 = (uintptr_t) q1, a2 = (uintptr_t) q2, a3 = (uintptr_t) q3;
6098
+ uintptr_t nb = (uintptr_t) n * sizeof(*q3);
6099
+ int apart1 = ( a3 + nb <= a1 || a1 + nb <= a3 );
6100
+ int apart2 = ( a3 + nb <= a2 || a2 + nb <= a3 );
6083
6101
  (void)p1; (void)p2; (void)p3;
6084
- #pragma omp simd
6085
- for (k=0; k<n; k++) {
6086
- p1 = q1 + k; p2 = q2 + k; p3 = q3 + k;
6087
- { #{expr_body} }
6102
+ if ( apart1 && apart2 ) {
6103
+ #{c_type} * __restrict__ r1 = q1;
6104
+ #{c_type} * __restrict__ r2 = q2;
6105
+ #{c_type} * __restrict__ r3 = q3;
6106
+ #pragma omp simd
6107
+ for (k=0; k<n; k++) {
6108
+ p1 = r1 + k; p2 = r2 + k; p3 = r3 + k;
6109
+ { #{expr_body} }
6110
+ }
6111
+ }
6112
+ else if ( ( apart1 || a1 == a3 ) && ( apart2 || a2 == a3 ) ) {
6113
+ /* in place: an iteration reads and writes only its own cell */
6114
+ #pragma omp simd
6115
+ for (k=0; k<n; k++) {
6116
+ p1 = q1 + k; p2 = q2 + k; p3 = q3 + k;
6117
+ { #{expr_body} }
6118
+ }
6119
+ }
6120
+ else {
6121
+ for (k=0; k<n; k++) {
6122
+ p1 = q1 + k; p2 = q2 + k; p3 = q3 + k;
6123
+ { #{expr_body} }
6124
+ }
6088
6125
  }
6089
6126
  } else {
6090
6127
  /* strided branch: numo-shape `p += s` increment form. */
@@ -6539,19 +6576,29 @@ module MkKernel
6539
6576
  # the source's data type. Left out: a boolean target, which checks its
6540
6577
  # input and raises, and a float or complex source to an integer target,
6541
6578
  # where an out-of-range value or NaN has no defined C result for a
6542
- # second compilation to agree on.
6579
+ # second compilation to agree on. An int64 or uint64 source to float32
6580
+ # or cmplx64 goes through double, as ca_cast_block does.
6543
6581
  CAST_SOURCES = %i[bool i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6544
6582
  cmplx64 cmplx128].freeze
6545
6583
  CAST_TARGETS = %i[i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6546
6584
  cmplx64 cmplx128].freeze
6547
6585
  CAST_INEXACT = %i[f32 f64 cmplx64 cmplx128].freeze
6548
6586
 
6587
+ # The plain C type of a data type, for a body that leaves this file
6588
+ # (CArray.__kernel_body__, a fusion plan): the unaligned typedef in :c is
6589
+ # about how a kernel reaches a cell, not about the value.
6590
+ def self.value_c(src)
6591
+ { "ca_u_float32_t" => "float", "ca_u_float64_t" => "double" }
6592
+ .fetch(DTYPES[src][:c]) { |c| c.sub(/\Aca_u_/, "") }
6593
+ end
6594
+
6549
6595
  def self.cast_body_rows
6550
6596
  CAST_SOURCES.product(CAST_TARGETS).filter_map { |src, dst|
6551
6597
  next if CAST_INEXACT.include?(src) && !CAST_INEXACT.include?(dst)
6552
6598
  name = DTYPES[dst][:ca].sub(/\ACA_/, "").downcase
6599
+ via = ( %i[i64 u64].include?(src) && %i[f32 cmplx64].include?(dst) ) ? "(double)" : ""
6553
6600
  ["monop", "cast_#{name}", DTYPES[src][:ca].sub(/\ACA_/, "").downcase,
6554
- "(#2) = (#{DTYPES[dst][:c]})(#1);"]
6601
+ "(#2) = (#{value_c(dst)})#{via}(#1);"]
6555
6602
  }
6556
6603
  end
6557
6604
 
@@ -6756,12 +6803,16 @@ module MkKernel
6756
6803
  # reduce: default false (E.8 retire), opt-in via bind_ruby: true
6757
6804
  # (used by CF.1 count_equal smoke + future kernel testing)
6758
6805
  # sort: default true, opt-out via bind_ruby: false
6759
- # scan: always bound (no opt-out)
6806
+ # scan: bound as <name>_ki, or only privately as <name> (private:)
6760
6807
  bind = case k[:kind]
6761
6808
  when :reduce then k[:bind_ruby] == true
6762
6809
  when :sort then k[:bind_ruby] != false
6810
+ when :scan then ! k[:private]
6763
6811
  else true
6764
6812
  end
6813
+ if k[:kind] == :scan && k[:private]
6814
+ io.puts %Q[ rb_define_private_method(rb_cCArray, "#{k[:name]}", rb_ca_#{k[:name]}_ki, #{arity});]
6815
+ end
6765
6816
  # Smoke kernels (e.g. count_equal) expose their _ki binding only
6766
6817
  # in dev builds, gated by CARRAY_DEV_BUILD. See
6767
6818
  # devel/PROPOSAL_SMOKE_DEV_BUILD_GATE.md.
@@ -6774,7 +6825,7 @@ module MkKernel
6774
6825
  # directly without a Ruby wrapper. This is paired with
6775
6826
  # axis_default: :flatten because both express the legacy-compat
6776
6827
  # convenience of these specific scan kernels.
6777
- if k[:kind] == :scan && k[:axis_default] == :flatten
6828
+ if k[:kind] == :scan && k[:axis_default] == :flatten && ! k[:private]
6778
6829
  io.puts %Q[ rb_define_method(rb_cCArray, "#{k[:name]}", rb_ca_#{k[:name]}_ki, #{arity});]
6779
6830
  end
6780
6831
  # Phase E rewire: when this kernel replaces a legacy CArray method,
@@ -7398,7 +7449,7 @@ MkKernel.reduce :wsum,
7398
7449
  shape: :rev5_strict, mask: :overlay },
7399
7450
  mask_policy: :min_count,
7400
7451
  identity_on_empty: true, # ERI.2: empty / all-masked -> 0.0 (weighted sum identity)
7401
- object_escape: :__wsum_object__, # CA_OBJECT -> (w*self).sum (runtime.rb)
7452
+ object_escape: :wsum_object, # CA_OBJECT -> (w*self).sum (runtime.rb)
7402
7453
  bind_ruby: true,
7403
7454
  public_method: true
7404
7455
 
@@ -7415,7 +7466,7 @@ MkKernel.reduce :wsum,
7415
7466
  # - min_count semantic "max masked" -> "min valid required" (Phase E)
7416
7467
  # - complex (CMPLX64/128) dropped (= ALL_NUMERIC + :raise)
7417
7468
  # - per-axis support gained
7418
- # CA_OBJECT is handled via object_escape (runtime.rb __wmean_object__), not
7469
+ # CA_OBJECT is handled via object_escape (runtime.rb wmean_object), not
7419
7470
  # the numeric kernel: weight-coerce vs object arithmetic is cleaner as a
7420
7471
  # (w*self).sum / Σw composition than a dedicated :object kernel branch.
7421
7472
 
@@ -7432,7 +7483,7 @@ MkKernel.reduce :wmean,
7432
7483
  array_arg: { name: :weights, data_type: :promote,
7433
7484
  shape: :rev5_strict, mask: :overlay },
7434
7485
  mask_policy: :min_count,
7435
- object_escape: :__wmean_object__, # CA_OBJECT -> weighted mean via runtime.rb
7486
+ object_escape: :wmean_object, # CA_OBJECT -> weighted mean via runtime.rb
7436
7487
  bind_ruby: true,
7437
7488
  public_method: true
7438
7489
 
@@ -7457,6 +7508,18 @@ MkKernel.scan :cumsum,
7457
7508
  fallback: :raise,
7458
7509
  axis_default: :flatten
7459
7510
 
7511
+ # The running sum in the source's own integer type, added without a range
7512
+ # check (int64 wraps, as accumulate does). CATimedelta#cumsum reads it
7513
+ # for an exact running sum of durations.
7514
+ MkKernel.scan :cumsum_in_own_type,
7515
+ source: MkKernel::INT_DTYPES,
7516
+ output: :preserve,
7517
+ init: "0",
7518
+ step: "acc += v; r = acc",
7519
+ fallback: :raise,
7520
+ axis_default: :flatten,
7521
+ private: true
7522
+
7460
7523
  MkKernel.scan :cumprod,
7461
7524
  source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES + [:bool, :object],
7462
7525
  output: { numeric: :f64, complex: :cmplx128, bool: :u64, object: :object },
@@ -7707,8 +7770,8 @@ MkKernel.search :find_value_index,
7707
7770
  mask_self: :raise,
7708
7771
  fallback: :raise
7709
7772
 
7710
- # bsearch_ki: per-slice binary search returning the matching position
7711
- # (assumes a sorted slab). NaN handling: NaN sorts to end; query == NaN
7773
+ # bsearch_ki: per-slice binary search returning the first matching position
7774
+ # (assumes a sorted slab; of equal cells, the first). NaN handling: NaN sorts to end; query == NaN
7712
7775
  # never matches. Hand-rolled strided binary search (= libc bsearch()
7713
7776
  # requires contig memory; slab_stride may be > sizeof(T_LOAD) for a
7714
7777
  # non-innermost search axis). NaN guards are dead code for integer
@@ -7732,47 +7795,41 @@ MkKernel.search :bsearch,
7732
7795
  while ( lo < hi ) {
7733
7796
  ca_size_t mid = lo + (hi - lo) / 2;
7734
7797
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7735
- if ( isnan((double) v) ) { hi = mid; continue; }
7736
7798
  if ( v < query_val ) lo = mid + 1;
7737
7799
  else if ( v > query_val ) hi = mid;
7738
- else { result = mid; break; }
7800
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7739
7801
  }
7740
7802
  }
7741
7803
  C
7742
7804
  float: <<~C,
7743
7805
  result = (ca_size_t) -1;
7744
- if ( ! isnan((double) query_val) ) {
7806
+ if ( ! isnan((double) query_val) ) { /* a NaN query matches nothing */
7745
7807
  ca_size_t lo = 0;
7746
7808
  ca_size_t hi = slab_n;
7747
7809
  while ( lo < hi ) {
7748
7810
  ca_size_t mid = lo + (hi - lo) / 2;
7749
7811
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7750
- if ( isnan((double) v) ) { hi = mid; continue; }
7751
- if ( v < query_val ) lo = mid + 1;
7752
- else if ( v > query_val ) hi = mid;
7753
- else { result = mid; break; }
7812
+ int c = CA_FLOAT_ORDER_CMP(v, query_val);
7813
+ if ( c < 0 ) lo = mid + 1;
7814
+ else if ( c > 0 ) hi = mid;
7815
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7754
7816
  }
7755
7817
  }
7756
7818
  C
7757
7819
  object: <<~C,
7758
- /* CA_OBJECT bsearch: ordering via rb_funcall(<=>). Assumes the
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. */
7820
+ /* CA_OBJECT bsearch: the object order (ca_obj_order_cmp), which the
7821
+ slab is assumed to be sorted by; a NaN query matches nothing, as in
7822
+ float. */
7761
7823
  result = (ca_size_t) -1;
7762
7824
  ca_size_t lo = 0;
7763
7825
  ca_size_t hi = CA_OBJ_ISNAN(query_val) ? 0 : slab_n; /* NaN matches nothing */
7764
7826
  while ( lo < hi ) {
7765
7827
  ca_size_t mid = lo + (hi - lo) / 2;
7766
7828
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7767
- if ( CA_OBJ_ISNAN(v) ) { hi = mid; continue; } /* NaN sorts last */
7768
- VALUE r = rb_funcall(v, rb_intern("<=>"), 1, query_val);
7769
- if ( NIL_P(r) ) {
7770
- ca_obj_compare_refused(v, query_val);
7771
- }
7772
- int c = NUM2INT(r);
7829
+ int c = ca_obj_order_cmp(v, query_val);
7773
7830
  if ( c < 0 ) lo = mid + 1;
7774
7831
  else if ( c > 0 ) hi = mid;
7775
- else { result = mid; break; }
7832
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7776
7833
  }
7777
7834
  C
7778
7835
  fixlen: <<~C,
@@ -7788,7 +7845,7 @@ MkKernel.search :bsearch,
7788
7845
  int c = memcmp(v, query_val, (size_t) ca->bytes);
7789
7846
  if ( c < 0 ) lo = mid + 1;
7790
7847
  else if ( c > 0 ) hi = mid;
7791
- else { result = mid; break; }
7848
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7792
7849
  }
7793
7850
  C
7794
7851
  },
@@ -7971,25 +8028,24 @@ MkKernel.search :bsearch_addr,
7971
8028
  while ( lo < hi ) {
7972
8029
  ca_size_t mid = lo + (hi - lo) / 2;
7973
8030
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7974
- if ( isnan((double) v) ) { hi = mid; continue; }
7975
8031
  if ( v < query_val ) lo = mid + 1;
7976
8032
  else if ( v > query_val ) hi = mid;
7977
- else { result = mid; break; }
8033
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7978
8034
  }
7979
8035
  }
7980
8036
  C
7981
8037
  float: <<~C,
7982
8038
  result = (ca_size_t) -1;
7983
- if ( ! isnan((double) query_val) ) {
8039
+ if ( ! isnan((double) query_val) ) { /* a NaN query matches nothing */
7984
8040
  ca_size_t lo = 0;
7985
8041
  ca_size_t hi = slab_n;
7986
8042
  while ( lo < hi ) {
7987
8043
  ca_size_t mid = lo + (hi - lo) / 2;
7988
8044
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7989
- if ( isnan((double) v) ) { hi = mid; continue; }
7990
- if ( v < query_val ) lo = mid + 1;
7991
- else if ( v > query_val ) hi = mid;
7992
- else { result = mid; break; }
8045
+ int c = CA_FLOAT_ORDER_CMP(v, query_val);
8046
+ if ( c < 0 ) lo = mid + 1;
8047
+ else if ( c > 0 ) hi = mid;
8048
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7993
8049
  }
7994
8050
  }
7995
8051
  C
@@ -8000,15 +8056,10 @@ MkKernel.search :bsearch_addr,
8000
8056
  while ( lo < hi ) {
8001
8057
  ca_size_t mid = lo + (hi - lo) / 2;
8002
8058
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
8003
- if ( CA_OBJ_ISNAN(v) ) { hi = mid; continue; } /* NaN sorts last */
8004
- VALUE r = rb_funcall(v, rb_intern("<=>"), 1, query_val);
8005
- if ( NIL_P(r) ) {
8006
- ca_obj_compare_refused(v, query_val);
8007
- }
8008
- int c = NUM2INT(r);
8059
+ int c = ca_obj_order_cmp(v, query_val);
8009
8060
  if ( c < 0 ) lo = mid + 1;
8010
8061
  else if ( c > 0 ) hi = mid;
8011
- else { result = mid; break; }
8062
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
8012
8063
  }
8013
8064
  C
8014
8065
  fixlen: <<~C,
@@ -8022,7 +8073,7 @@ MkKernel.search :bsearch_addr,
8022
8073
  int c = memcmp(v, query_val, (size_t) ca->bytes);
8023
8074
  if ( c < 0 ) lo = mid + 1;
8024
8075
  else if ( c > 0 ) hi = mid;
8025
- else { result = mid; break; }
8076
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
8026
8077
  }
8027
8078
  C
8028
8079
  },
@@ -8188,19 +8239,18 @@ MkKernel.search :linear_section_binary,
8188
8239
  no_match: "NAN",
8189
8240
  no_match_check: "isnan(op[0])",
8190
8241
  mask_self: :raise,
8191
- mask_query: :undef,
8192
8242
  body: <<~C,
8193
8243
  result = NAN;
8194
8244
  if ( slab_n <= 1 ) {
8195
8245
  /* A one-point axis has no segment: only the point itself is in
8196
8246
  * range, at 0. An empty one has nothing in range. */
8197
- if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8247
+ if ( slab_n == 1 && query_val == *(ca_u_float64_t *)slab_ptr ) {
8198
8248
  result = 0.0;
8199
8249
  }
8200
8250
  }
8201
8251
  else {
8202
- double y0 = *(double *)(slab_ptr + 0 * slab_stride);
8203
- double yn = *(double *)(slab_ptr + (slab_n - 1) * slab_stride);
8252
+ double y0 = *(ca_u_float64_t *)(slab_ptr + 0 * slab_stride);
8253
+ double yn = *(ca_u_float64_t *)(slab_ptr + (slab_n - 1) * slab_stride);
8204
8254
  int ascending = ( yn >= y0 ); /* grid direction (monotonic assumed) */
8205
8255
  ca_size_t x1 = 0;
8206
8256
  int found = 0;
@@ -8223,8 +8273,8 @@ MkKernel.search :linear_section_binary,
8223
8273
  double gd = (query_val - y0) / (yn - y0) * (double)(slab_n - 1);
8224
8274
  if ( gd >= 0.0 && gd < (double)(slab_n - 1) ) {
8225
8275
  ca_size_t ag = (ca_size_t) gd;
8226
- double yga = *(double *)(slab_ptr + ag * slab_stride);
8227
- double ygb = *(double *)(slab_ptr + (ag + 1) * slab_stride);
8276
+ double yga = *(ca_u_float64_t *)(slab_ptr + ag * slab_stride);
8277
+ double ygb = *(ca_u_float64_t *)(slab_ptr + (ag + 1) * slab_stride);
8228
8278
  if ( (yga - query_val) * (ygb - query_val) <= 0 ) {
8229
8279
  x1 = ag; found = 1;
8230
8280
  }
@@ -8239,7 +8289,7 @@ MkKernel.search :linear_section_binary,
8239
8289
  double ya = y0;
8240
8290
  while ( (b - a) > 1 ) {
8241
8291
  ca_size_t c = (a + b) / 2;
8242
- double yc = *(double *)(slab_ptr + c * slab_stride);
8292
+ double yc = *(ca_u_float64_t *)(slab_ptr + c * slab_stride);
8243
8293
  if ( (ya - query_val) * (yc - query_val) <= 0 ) { b = c; }
8244
8294
  else { a = c; ya = yc; }
8245
8295
  }
@@ -8248,8 +8298,8 @@ MkKernel.search :linear_section_binary,
8248
8298
  }
8249
8299
 
8250
8300
  if ( found ) {
8251
- double y1 = *(double *)(slab_ptr + x1 * slab_stride);
8252
- double y2 = *(double *)(slab_ptr + (x1 + 1) * slab_stride);
8301
+ double y1 = *(ca_u_float64_t *)(slab_ptr + x1 * slab_stride);
8302
+ double y2 = *(ca_u_float64_t *)(slab_ptr + (x1 + 1) * slab_stride);
8253
8303
  double rest = (query_val - y1) / (y2 - y1);
8254
8304
  if ( y2 != 0.0 && fabs(y2 - query_val) / fabs(y2) < DBL_EPSILON * 100 ) {
8255
8305
  result = (double)(x1 + 1);
@@ -8280,13 +8330,12 @@ MkKernel.search :linear_section_linear,
8280
8330
  no_match: "NAN",
8281
8331
  no_match_check: "isnan(op[0])",
8282
8332
  mask_self: :raise,
8283
- mask_query: :undef,
8284
8333
  body: <<~C,
8285
8334
  result = NAN;
8286
8335
  if ( slab_n <= 1 ) {
8287
8336
  /* A one-point axis has no segment: only the point itself is in
8288
8337
  * range, at 0. An empty one has nothing in range. */
8289
- if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8338
+ if ( slab_n == 1 && query_val == *(ca_u_float64_t *)slab_ptr ) {
8290
8339
  result = 0.0;
8291
8340
  }
8292
8341
  }
@@ -8294,15 +8343,15 @@ MkKernel.search :linear_section_linear,
8294
8343
  ca_size_t x1 = 0;
8295
8344
  int found = 0;
8296
8345
  for ( ca_size_t k = 0; k < slab_n - 1; k++ ) {
8297
- double yk = *(double *)(slab_ptr + k * slab_stride);
8298
- double yk1 = *(double *)(slab_ptr + (k + 1) * slab_stride);
8346
+ double yk = *(ca_u_float64_t *)(slab_ptr + k * slab_stride);
8347
+ double yk1 = *(ca_u_float64_t *)(slab_ptr + (k + 1) * slab_stride);
8299
8348
  if ( (query_val - yk) * (query_val - yk1) <= 0 ) {
8300
8349
  x1 = k; found = 1; break;
8301
8350
  }
8302
8351
  }
8303
8352
  if ( found ) {
8304
- double y1 = *(double *)(slab_ptr + x1 * slab_stride);
8305
- double y2 = *(double *)(slab_ptr + (x1 + 1) * slab_stride);
8353
+ double y1 = *(ca_u_float64_t *)(slab_ptr + x1 * slab_stride);
8354
+ double y2 = *(ca_u_float64_t *)(slab_ptr + (x1 + 1) * slab_stride);
8306
8355
  result = (query_val - y1) / (y2 - y1) + (double) x1;
8307
8356
  }
8308
8357
 
@@ -8323,15 +8372,14 @@ MkKernel.search :linear_fetch,
8323
8372
  no_match: "NAN",
8324
8373
  no_match_check: "isnan(op[0])",
8325
8374
  mask_self: :raise,
8326
- mask_query: :undef,
8327
8375
  body: <<~C,
8328
8376
  result = NAN;
8329
8377
  if ( slab_n >= 1 && query_val >= 0.0 && query_val <= (double)(slab_n - 1) ) {
8330
8378
  ca_size_t il = (ca_size_t) floor(query_val);
8331
8379
  ca_size_t iu = (ca_size_t) ceil(query_val);
8332
8380
  double w = query_val - floor(query_val);
8333
- double yl = *(double *)(slab_ptr + il * slab_stride);
8334
- double yu = *(double *)(slab_ptr + iu * slab_stride);
8381
+ double yl = *(ca_u_float64_t *)(slab_ptr + il * slab_stride);
8382
+ double yu = *(ca_u_float64_t *)(slab_ptr + iu * slab_stride);
8335
8383
  result = yu * w + yl * (1.0 - w);
8336
8384
  }
8337
8385
  C
@@ -8588,7 +8636,9 @@ MkKernel.monfunc :round,
8588
8636
  source: MkKernel::ALL_NUMERIC + [:object],
8589
8637
  expr: {
8590
8638
  int: "(#2) = (#1);",
8591
- float: "(#2) = ((#1)>0.0) ? floor((#1)+0.5) : ((#1)<0.0) ? ceil((#1)-0.5) : (#1);",
8639
+ # Half away from zero, as Float#round. floor(x + 0.5) would round in
8640
+ # the addition: 0.49999999999999994 + 0.5 is 1.0.
8641
+ float: "(#2) = round(#1);",
8592
8642
  object: '(#2) = rb_funcall((#1), rb_intern("round"), 0);',
8593
8643
  }
8594
8644
 
@@ -8889,7 +8939,7 @@ MkKernel.monfunc :rad_pi,
8889
8939
  # Pair-wise max / min — three flavours:
8890
8940
  #
8891
8941
  # pmax / pmin CArray legacy names (kept for backwards compat),
8892
- # NaN-skip semantics via C99 fmax / fmin.
8942
+ # NaN-skip semantics, as C99 fmax / fmin.
8893
8943
  # fmax / fmin Ruby-level aliases of pmax / pmin (= same kernel,
8894
8944
  # same NaN-skip behaviour). Matches the C99 library
8895
8945
  # function of the same name.
@@ -8899,12 +8949,15 @@ MkKernel.monfunc :rad_pi,
8899
8949
  #
8900
8950
  # Integer branches are identical across all three (no NaN). The float
8901
8951
  # and object branches differ: pmax / pmin let a NaN lose (an object cell's
8902
- # Float NaN included), maximum / minimum let it win.
8952
+ # Float NaN included), maximum / minimum let it win. In both flavours, of
8953
+ # two zeros the max is +0 and the min is -0 in either order. C's fmax /
8954
+ # fmin leave that choice to the library (glibc and macOS answer
8955
+ # differently), so the float branch does not call them.
8903
8956
  MkKernel.binop :pmax,
8904
8957
  source: MkKernel::ALL_NUMERIC + [:object],
8905
8958
  expr: {
8906
8959
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8907
- float: "(#3) = fmax(#1, #2);",
8960
+ float: "(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) > (#2) || ((#1) == (#2) && !signbit(#1))) ? (#1) : (#2);",
8908
8961
  object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern(">")) ? (#2) : (#1);',
8909
8962
  }
8910
8963
 
@@ -8912,7 +8965,7 @@ MkKernel.binop :pmin,
8912
8965
  source: MkKernel::ALL_NUMERIC + [:object],
8913
8966
  expr: {
8914
8967
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8915
- float: "(#3) = fmin(#1, #2);",
8968
+ float: "(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) < (#2) || ((#1) == (#2) && signbit(#1))) ? (#1) : (#2);",
8916
8969
  object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern("<")) ? (#2) : (#1);',
8917
8970
  }
8918
8971
 
@@ -9259,13 +9312,13 @@ MkKernel.triop :fms,
9259
9312
  # UNDEF-fill case can now be expressed as `out = a.clip(lo, hi);
9260
9313
  # out[a.lt(lo) | a.gt(hi)] = UNDEF`). Pinned in test.
9261
9314
  #
9262
- # Bound at the C level as `__clip_ki__` (private kernel entry). The
9315
+ # Bound at the C level as `clamp_between` (made private by lazy.rb). The
9263
9316
  # public `CArray#clip` is a Ruby wrapper in lib/carray/math.rb that
9264
9317
  # restores the rich pre-3.0 API surface (nil-bound one-sided clip +
9265
9318
  # optional fill). `clip!` is retired in 3.0 (view-by-default
9266
9319
  # convention) so the bang form is suppressed via `bang: false`.
9267
9320
  MkKernel.triop :clip,
9268
- op: "__clip_ki__",
9321
+ op: "clamp_between",
9269
9322
  bang: false,
9270
9323
  source: MkKernel::ALL_NUMERIC + [:object],
9271
9324
  expr: {