carray 3.0.3 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +1 -6
  7. data/ext/ca_axis_group.c +27 -27
  8. data/ext/ca_binop_dispatch.c +1 -29
  9. data/ext/ca_binop_dispatch.h +0 -1
  10. data/ext/ca_categorical_iterator.c +205 -134
  11. data/ext/ca_compare.c +7 -18
  12. data/ext/ca_group_iter.c +16 -17
  13. data/ext/ca_kernel_iterator.c +1234 -1994
  14. data/ext/ca_kernel_iterator.h +391 -437
  15. data/ext/ca_monop_dispatch.c +82 -59
  16. data/ext/ca_monop_dispatch.h +1 -0
  17. data/ext/ca_obj_array.c +96 -39
  18. data/ext/ca_obj_bincmp.c +24 -21
  19. data/ext/ca_obj_binop.c +15 -26
  20. data/ext/ca_obj_bitarray.c +3 -2
  21. data/ext/ca_obj_bitfield.c +5 -6
  22. data/ext/ca_obj_block.c +18 -9
  23. data/ext/ca_obj_byte_swap.c +5 -6
  24. data/ext/ca_obj_const_string.c +173 -19
  25. data/ext/ca_obj_face.c +238 -57
  26. data/ext/ca_obj_face.h +19 -0
  27. data/ext/ca_obj_fake.c +15 -29
  28. data/ext/ca_obj_farray.c +6 -3
  29. data/ext/ca_obj_field.c +17 -5
  30. data/ext/ca_obj_fixlen_string.c +2 -3
  31. data/ext/ca_obj_grid.c +406 -132
  32. data/ext/ca_obj_meld.c +172 -34
  33. data/ext/ca_obj_moncmp.c +23 -8
  34. data/ext/ca_obj_monop.c +67 -57
  35. data/ext/ca_obj_object.c +157 -51
  36. data/ext/ca_obj_record.c +12 -7
  37. data/ext/ca_obj_reduce.c +13 -8
  38. data/ext/ca_obj_refer.c +25 -27
  39. data/ext/ca_obj_remap.c +30 -9
  40. data/ext/ca_obj_repeat.c +9 -10
  41. data/ext/ca_obj_roll.c +6 -5
  42. data/ext/ca_obj_select.c +14 -9
  43. data/ext/ca_obj_select_axis.c +15 -47
  44. data/ext/ca_obj_shift.c +11 -1
  45. data/ext/ca_obj_stack.c +179 -34
  46. data/ext/ca_obj_stride.c +162 -115
  47. data/ext/ca_obj_string.c +71 -4
  48. data/ext/ca_obj_tile.c +6 -5
  49. data/ext/ca_obj_time.c +22 -6
  50. data/ext/ca_obj_timedelta.c +22 -6
  51. data/ext/ca_obj_transpose.c +5 -3
  52. data/ext/ca_obj_triop.c +23 -32
  53. data/ext/ca_obj_window.c +190 -26
  54. data/ext/ca_sweep_engine.c +2 -2
  55. data/ext/caframe_csv_reader.c +80 -1
  56. data/ext/carray.h +181 -75
  57. data/ext/carray_access.c +184 -96
  58. data/ext/carray_address_basis.c +343 -87
  59. data/ext/carray_attribute.c +89 -6
  60. data/ext/carray_bincount.c +10 -8
  61. data/ext/carray_broadcast.c +13 -8
  62. data/ext/carray_build_flags.h +3 -0
  63. data/ext/carray_call_cfunc.c +108 -0
  64. data/ext/carray_cast.c +142 -24
  65. data/ext/carray_cast_func.rb +46 -6
  66. data/ext/carray_class.c +4 -3
  67. data/ext/carray_config.h +28 -0
  68. data/ext/carray_conversion.c +16 -7
  69. data/ext/carray_copy.c +51 -14
  70. data/ext/carray_core.c +147 -55
  71. data/ext/carray_count.c +50 -20
  72. data/ext/carray_element.c +76 -33
  73. data/ext/carray_factorize.c +118 -64
  74. data/ext/carray_generate.c +191 -50
  75. data/ext/carray_histogram.c +62 -17
  76. data/ext/carray_hold.c +12 -15
  77. data/ext/carray_index_classifier.c +388 -176
  78. data/ext/carray_internal.h +336 -29
  79. data/ext/carray_kernels_bincmp.c +4453 -0
  80. data/ext/carray_kernels_binop.c +11368 -0
  81. data/ext/carray_kernels_init.c +1734 -0
  82. data/ext/carray_kernels_moncmp.c +2104 -0
  83. data/ext/carray_kernels_monop.c +18369 -0
  84. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  85. data/ext/carray_kernels_reduce_boolean.c +406 -0
  86. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  87. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  88. data/ext/carray_kernels_reduce_variance.c +3849 -0
  89. data/ext/carray_kernels_scan.c +4302 -0
  90. data/ext/carray_kernels_search.c +33829 -0
  91. data/ext/carray_kernels_sort.c +10906 -0
  92. data/ext/carray_kernels_triop.c +1864 -0
  93. data/ext/carray_key_windows.c +216 -0
  94. data/ext/carray_lazy.c +170 -111
  95. data/ext/carray_mask.c +292 -72
  96. data/ext/carray_median_percentile.c +82 -28
  97. data/ext/carray_memory_view.c +669 -561
  98. data/ext/carray_operator.c +124 -47
  99. data/ext/carray_order.c +384 -76
  100. data/ext/carray_partition.c +28 -27
  101. data/ext/carray_random.c +67 -13
  102. data/ext/carray_round.c +273 -0
  103. data/ext/carray_scatter.c +63 -38
  104. data/ext/carray_segment.c +3 -3
  105. data/ext/carray_slab.c +91 -28
  106. data/ext/carray_sort.c +35 -35
  107. data/ext/carray_struct.c +19 -18
  108. data/ext/carray_test.c +26 -5
  109. data/ext/carray_undef.c +4 -0
  110. data/ext/carray_utils.c +246 -45
  111. data/ext/mk_call_cfunc.rb +19 -2
  112. data/ext/mkkernel.rb +198 -240
  113. data/ext/ruby_carray.c +10 -4
  114. data/ext/version.h +4 -4
  115. data/lib/carray/attribute.rb +126 -22
  116. data/lib/carray/autoload_carray.rb +8 -31
  117. data/lib/carray/autoload_method_extension.rb +14 -5
  118. data/lib/carray/axis_group.rb +255 -106
  119. data/lib/carray/basics.rb +78 -10
  120. data/lib/carray/bincount_nd.rb +12 -2
  121. data/lib/carray/block_iterator.rb +126 -23
  122. data/lib/carray/boolean_reduce.rb +17 -17
  123. data/lib/carray/categorical.rb +165 -43
  124. data/lib/carray/categorical_iterator.rb +130 -27
  125. data/lib/carray/complex.rb +5 -5
  126. data/lib/carray/conditional.rb +67 -18
  127. data/lib/carray/const_string.rb +14 -5
  128. data/lib/carray/construct.rb +15 -11
  129. data/lib/carray/data_type_extension.rb +1 -0
  130. data/lib/carray/distance_limit.rb +77 -0
  131. data/lib/carray/fixlen_string.rb +7 -1
  132. data/lib/carray/frame/column_verbs.rb +70 -0
  133. data/lib/carray/frame/convert.rb +69 -0
  134. data/lib/carray/frame/csv_parser.rb +111 -17
  135. data/lib/carray/frame/describe.rb +3 -2
  136. data/lib/carray/frame/frame.rb +246 -21
  137. data/lib/carray/frame/group.rb +510 -95
  138. data/lib/carray/frame/io.rb +115 -28
  139. data/lib/carray/frame/join.rb +312 -51
  140. data/lib/carray/frame/records.rb +83 -7
  141. data/lib/carray/frame/reshape.rb +42 -35
  142. data/lib/carray/frame/sort.rb +7 -19
  143. data/lib/carray/frame/verbs.rb +188 -48
  144. data/lib/carray/frame/windows.rb +117 -0
  145. data/lib/carray/frame.rb +2 -0
  146. data/lib/carray/fuse_source.rb +11 -4
  147. data/lib/carray/fusion.rb +40 -16
  148. data/lib/carray/histogram.rb +6 -1
  149. data/lib/carray/inspect.rb +31 -8
  150. data/lib/carray/iterator.rb +85 -7
  151. data/lib/carray/key_windows.rb +620 -0
  152. data/lib/carray/lazy.rb +182 -183
  153. data/lib/carray/mask_gap_fill.rb +63 -57
  154. data/lib/carray/meld_reduce.rb +12 -14
  155. data/lib/carray/methods/bin.rb +26 -18
  156. data/lib/carray/methods/bincount.rb +16 -0
  157. data/lib/carray/methods/bit_string.rb +1 -1
  158. data/lib/carray/methods/choose.rb +16 -3
  159. data/lib/carray/methods/composition.rb +3 -1
  160. data/lib/carray/methods/gather_nd.rb +14 -5
  161. data/lib/carray/methods/insert_block.rb +2 -2
  162. data/lib/carray/methods/is_between.rb +59 -0
  163. data/lib/carray/methods/is_in.rb +53 -36
  164. data/lib/carray/methods/join.rb +1 -6
  165. data/lib/carray/methods/lag.rb +49 -0
  166. data/lib/carray/methods/locate_addr.rb +47 -51
  167. data/lib/carray/methods/mode.rb +1 -1
  168. data/lib/carray/methods/nunique.rb +11 -6
  169. data/lib/carray/methods/resize.rb +3 -3
  170. data/lib/carray/methods/value_counts.rb +4 -11
  171. data/lib/carray/record_order.rb +3 -3
  172. data/lib/carray/rng.rb +3 -0
  173. data/lib/carray/runtime.rb +53 -8
  174. data/lib/carray/segment_iterator.rb +313 -82
  175. data/lib/carray/serialize.rb +24 -10
  176. data/lib/carray/slab_iterator.rb +50 -0
  177. data/lib/carray/string.rb +105 -5
  178. data/lib/carray/string_operation_extension.rb +8 -1
  179. data/lib/carray/struct.rb +9 -21
  180. data/lib/carray/struct_builder.rb +19 -8
  181. data/lib/carray/time.rb +726 -104
  182. data/lib/carray/window_iterator.rb +122 -16
  183. data/lib/carray.rb +7 -0
  184. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  185. data/yard-stubs/ca_obj_field.rb +4 -0
  186. data/yard-stubs/ca_obj_grid.rb +21 -10
  187. data/yard-stubs/ca_obj_refer.rb +2 -1
  188. data/yard-stubs/ca_obj_roll.rb +2 -2
  189. data/yard-stubs/ca_obj_shift.rb +5 -1
  190. data/yard-stubs/ca_obj_tile.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +8 -3
  192. data/yard-stubs/carray_access.rb +6 -1
  193. data/yard-stubs/carray_attribute.rb +0 -5
  194. data/yard-stubs/carray_cast.rb +10 -0
  195. data/yard-stubs/carray_conversion.rb +2 -1
  196. data/yard-stubs/carray_copy.rb +4 -2
  197. data/yard-stubs/carray_count.rb +4 -4
  198. data/yard-stubs/carray_element.rb +10 -4
  199. data/yard-stubs/carray_generate.rb +21 -1
  200. data/yard-stubs/carray_mask.rb +26 -2
  201. data/yard-stubs/carray_math.rb +27 -1
  202. data/yard-stubs/carray_median_percentile.rb +5 -2
  203. data/yard-stubs/carray_memory_view.rb +40 -23
  204. data/yard-stubs/carray_order.rb +29 -18
  205. data/yard-stubs/carray_random.rb +13 -4
  206. data/yard-stubs/carray_scatter.rb +14 -9
  207. data/yard-stubs/carray_slab.rb +9 -2
  208. data/yard-stubs/carray_sort.rb +6 -5
  209. data/yard-stubs/carray_test.rb +5 -3
  210. metadata +25 -3
  211. data/lib/carray/arrow_tensor.rb +0 -401
  212. data/lib/carray/table.rb +0 -108
data/ext/mkkernel.rb CHANGED
@@ -177,10 +177,8 @@ module MkKernel
177
177
  # expr: { object: "..." }.merge(float_widths("(#2) = sin<f>(#1);"))
178
178
  #
179
179
  # Not every double-taking call wants this. `fabs`, `fmin` / `fmax`,
180
- # `ceil` / `floor` / `trunc` and the `isnan` family are exact on a
181
- # float either way, and `round` (`floor(x + 0.5)`) is exact only in
182
- # double -- narrowing it would round `x + 0.5f` first and step the
183
- # answer at the boundary.
180
+ # `ceil` / `floor` / `trunc` / `round` and the `isnan` family are exact
181
+ # on a float either way.
184
182
  def self.float_widths(body)
185
183
  {
186
184
  [:f32] => at_width(body, "f", "float"),
@@ -706,8 +704,13 @@ module MkKernel
706
704
  # the input is masked). Aligns core cummax / cummin with
707
705
  # the reduction contract (empty max / min = UNDEF, no
708
706
  # identity) and the axis-group scan family.
707
+ # private:
708
+ # false (default) -> bound as the public `<name>_ki`
709
+ # true -> bound only as the private method `<name>`, a part
710
+ # that lib reaches with `send` (not user surface)
709
711
  def self.scan(name, source:, output:, init:, step:, fallback: :raise,
710
- acc_type: nil, axis_default: nil, empty: nil, face_gate: nil)
712
+ acc_type: nil, axis_default: nil, empty: nil, face_gate: nil,
713
+ private: false)
711
714
  raise "duplicate kernel #{name}" if KERNELS.any? { |k| k[:name] == name }
712
715
  source.each do |s|
713
716
  raise "unknown source data_type #{s}" unless DTYPES.key?(s)
@@ -749,6 +752,7 @@ module MkKernel
749
752
  axis_default: axis_default,
750
753
  empty: empty,
751
754
  face_gate: face_gate,
755
+ private: private,
752
756
  }
753
757
  end
754
758
 
@@ -944,7 +948,7 @@ module MkKernel
944
948
  # is up to the body (= value-returning search variants).
945
949
  def self.search(name, source:, output:, body:,
946
950
  no_match: "-1", no_match_check: nil, undef_no_match: false,
947
- mask_self: :raise, mask_query: :ignore, runtime_args: nil,
951
+ mask_self: :raise, runtime_args: nil,
948
952
  fallback: :raise, semantics: :fiber_local)
949
953
  raise "search: unknown semantics #{semantics} (expected :fiber_local or :view_flat)" \
950
954
  unless %i[fiber_local view_flat].include?(semantics)
@@ -956,12 +960,10 @@ module MkKernel
956
960
  raise "search: no_match must be a String" unless no_match.is_a?(String)
957
961
  raise "search: unknown mask_self #{mask_self} (expected :raise | :skip | :ignore)" \
958
962
  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)
963
+ # A masked query cell (or a scalar UNDEF query) has no answer: its output
964
+ # cell is UNDEF, so an undetermined query cannot become a determined
965
+ # result from the value that happens to sit under the mask. There is no
966
+ # option to read under the mask.
965
967
  raise "search: unknown fallback #{fallback} (expected :raise)" \
966
968
  unless fallback == :raise
967
969
 
@@ -1026,7 +1028,6 @@ module MkKernel
1026
1028
  no_match_check: no_match_check,
1027
1029
  undef_no_match: undef_no_match,
1028
1030
  mask_self: mask_self,
1029
- mask_query: mask_query,
1030
1031
  runtime_args: runtime_args || [],
1031
1032
  fallback: fallback,
1032
1033
  semantics: semantics,
@@ -2178,20 +2179,22 @@ module MkKernel
2178
2179
  else
2179
2180
  if k[:array_arg]
2180
2181
  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});"
2182
+ io.puts " ca_size_t __sra_throwaway_mc = 0;"
2183
+ io.puts " CA_SLAB_REDUCE_ARRAY_T_EX(#{si[:c]}, #{wc}, st, p, m, st_w, p_w, #{acc_var}, #{acc_init}, #{reduce_stmt}, __sra_throwaway_mc);"
2184
+ io.puts " (void) __sra_throwaway_mc;"
2182
2185
  else
2183
2186
  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)
2187
+ # Non-mask_policy path: the slab macros always take a masked_cnt,
2188
+ # so hand them a throwaway. The generic form (suffix "", the
2189
+ # :object lane) has a wrapper that hides it.
2190
+ if suffix == ""
2191
+ io.puts " CA_SLAB_REDUCE_T(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt});"
2192
+ else
2188
2193
  io.puts " ca_size_t __sr_throwaway_mc = 0;"
2189
2194
  emit_reduce_slab_call(io, k, src, si, oi, suffix, acc_var, acc_init,
2190
2195
  reduce_stmt, "__sr_throwaway_mc",
2191
2196
  indent: " ")
2192
2197
  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
2198
  end
2196
2199
  end
2197
2200
  finish_emit = view_flat \
@@ -3549,9 +3552,12 @@ module MkKernel
3549
3552
  wio.puts " else {"
3550
3553
  wio.puts " /* CArray val: coerce to the weight storage type (f64 for :promote, so a"
3551
3554
  wio.puts " float weight against an int source is not truncated) */"
3552
- wio.puts " vcw = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3555
+ wio.puts " /* vcv holds the view while cv is read: W-A2 below moves vcw on to"
3556
+ wio.puts " the materialised copy before it is done with cv. */"
3557
+ wio.puts " volatile VALUE vcv = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3553
3558
  wio.puts " CArray *cv;"
3554
- wio.puts " GetCArray(vcw, cv);"
3559
+ wio.puts " GetCArray(vcv, cv);"
3560
+ wio.puts " vcw = vcv;"
3555
3561
  wio.puts ""
3556
3562
  wio.puts " /* rev5 form detection: A3 (same ndim) -> A2 (1-D axis-broadcast) -> raise */"
3557
3563
  wio.puts " if ( cv->ndim == src->ndim ) {"
@@ -3813,14 +3819,16 @@ module MkKernel
3813
3819
 
3814
3820
  # empty: :undef -> allocate the output mask up front when the input
3815
3821
  # 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.
3822
+ # which exist only when the input is masked). The output is a fresh
3823
+ # row-major entity, and the walk cuts its slabs from a buffer laid out
3824
+ # the same way (the output's memory, or the walk's scratch when it
3825
+ # delivers by transfer), so a slab's offset from st_out.alias_ptr is
3826
+ # its offset in the output, and op_mask + that offset / cell size is the
3827
+ # fiber's mask base.
3819
3828
  mask_setup =
3820
3829
  if gated
3821
3830
  "\n" \
3822
3831
  " boolean8_t *op_mask = NULL;\n" \
3823
- " char *co_root = (char *) co->ptr;\n" \
3824
3832
  " if ( ca_has_mask(ca) ) {\n" \
3825
3833
  " ca_create_mask(co);\n" \
3826
3834
  " op_mask = (boolean8_t *) co->mask->ptr;\n" \
@@ -3835,7 +3843,7 @@ module MkKernel
3835
3843
  " st_out, po, #{init_expr}, #{step_stmt});"
3836
3844
  elsif gated
3837
3845
  "boolean8_t *mo_fiber = op_mask\n" \
3838
- " ? op_mask + (ca_size_t)(po - co_root) / (ca_size_t) co->bytes\n" \
3846
+ " ? op_mask + (ca_size_t)(po - st_out.alias_ptr) / (ca_size_t) co->bytes\n" \
3839
3847
  " : NULL;\n" \
3840
3848
  " CA_SLAB_SCAN_T_GATED(#{si[:c]}, #{oi[:c]}, st_in, pi, mi,\n" \
3841
3849
  " st_out, po, mo_fiber, #{init_expr}, #{step_stmt});"
@@ -3968,11 +3976,7 @@ module MkKernel
3968
3976
  io.puts " if ( TYPE(raxis) == T_ARRAY ) {"
3969
3977
  io.puts %Q[ rb_raise(rb_eArgError, "%s: axis must be a single Integer (got Array); a scan over several axes is ambiguous, chain explicitly: a.#{name}(axis: 0).#{name}(axis: 1)", ca_calling_method_name());]
3970
3978
  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 " }"
3979
+ io.puts " int axis = ca_normalize_axis_arg(raxis, src->ndim, NULL);"
3976
3980
  io.puts " switch ( src->data_type ) {"
3977
3981
  k[:source].each do |s|
3978
3982
  si = DTYPES[s]
@@ -4134,35 +4138,15 @@ module MkKernel
4134
4138
  io.puts " const #{name}_pair_#{src} *pa = (const #{name}_pair_#{src} *) a;"
4135
4139
  io.puts " const #{name}_pair_#{src} *pb = (const #{name}_pair_#{src} *) b;"
4136
4140
  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 " }"
4141
+ # The object order: `<=>`, a Float NaN after every other value
4142
+ # (ca_obj_order_cmp); two NaNs fall to the index tie-break.
4143
+ io.puts " int c = ca_obj_order_cmp(pa->v, pb->v);"
4144
+ io.puts " if ( c != 0 ) return c;"
4145
+ elsif is_fp
4146
+ # The float order: NaN after every number (CA_FLOAT_ORDER_CMP).
4147
+ io.puts " int c = CA_FLOAT_ORDER_CMP(pa->v, pb->v);"
4148
+ io.puts " if ( c != 0 ) return c;"
4157
4149
  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
4150
  io.puts " if ( pa->v > pb->v ) return 1;"
4167
4151
  io.puts " if ( pa->v < pb->v ) return -1;"
4168
4152
  end
@@ -4306,17 +4290,21 @@ module MkKernel
4306
4290
  io.puts " #{name}_pair_#{src} *aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4307
4291
  end
4308
4292
  else
4309
- io.puts " #{name}_pair_#{src} *buf ="
4310
- io.puts " xmalloc(sizeof(#{name}_pair_#{src}) * (size_t) fiber_n);"
4293
+ # ALLOCV as well: the walk that fills buf is the only thing that
4294
+ # could raise, and it does not today, but the buffers need not rely
4295
+ # on that.
4296
+ io.puts " volatile VALUE hbuf = 0, haux = 0;"
4297
+ io.puts " #{name}_pair_#{src} *buf = ALLOCV_N(#{name}_pair_#{src}, hbuf, fiber_n);"
4311
4298
  end
4312
4299
  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.
4300
+ # The aux pair buffer for ca_sort_merge_pair_<src> (a ping-pong
4301
+ # bottom-up mergesort), taken once outside the fiber loop.
4316
4302
  io.puts " #{name}_pair_#{src} *aux = NULL;"
4317
4303
  io.puts " if ( do_stable ) {"
4318
- io.puts " aux = xmalloc(sizeof(#{name}_pair_#{src}) * (size_t) fiber_n);"
4304
+ io.puts " aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4319
4305
  io.puts " }"
4306
+ elsif src != :object
4307
+ io.puts " (void) haux;"
4320
4308
  end
4321
4309
 
4322
4310
  if view_flat
@@ -4502,16 +4490,21 @@ module MkKernel
4502
4490
  io.puts " gaps ordinal would have spent on the tie are never used, so a"
4503
4491
  io.puts " later out_r stays <= the ordinal r at that position, preserving"
4504
4492
  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). */"
4493
+ io.puts " descending transform) the descending order. NaN cells tie"
4494
+ io.puts " with each other, as sort_addr and the value-hash family treat"
4495
+ io.puts " every NaN as one value. */"
4507
4496
  io.puts " ca_size_t out_r = 0;"
4508
4497
  io.puts " for ( ca_size_t r = 0; r < #{rank_n}; r++ ) {"
4509
4498
  io.puts " if ( r > 0 ) {"
4510
4499
  io.puts " if ( dense ) {"
4511
4500
  if is_object
4512
- io.puts " if ( ! ca_obj_equal(buf[r].v, buf[r - 1].v) ) out_r++;"
4501
+ io.puts " if ( ! ( ( CA_OBJ_ISNAN(buf[r].v) && CA_OBJ_ISNAN(buf[r - 1].v) )"
4502
+ io.puts " || ca_obj_equal(buf[r].v, buf[r - 1].v) ) ) out_r++;"
4513
4503
  elsif is_fixlen
4514
4504
  io.puts " if ( memcmp(buf[r].vp, buf[r - 1].vp, (size_t) buf[r].nb) != 0 ) out_r++;"
4505
+ elsif FLOAT_DTYPES.include?(src)
4506
+ io.puts " if ( buf[r].v != buf[r - 1].v"
4507
+ io.puts " && ! ( isnan(buf[r].v) && isnan(buf[r - 1].v) ) ) out_r++;"
4515
4508
  else
4516
4509
  io.puts " if ( buf[r].v != buf[r - 1].v ) out_r++;"
4517
4510
  end
@@ -4521,15 +4514,32 @@ module MkKernel
4521
4514
  io.puts " }"
4522
4515
  io.puts " *(#{oi[:c]} *) (po + buf[r].i * ostride) = (#{oi[:c]}) out_r;"
4523
4516
  io.puts " }"
4517
+ # dense == 2 (from CArray#order only): descending dense ranks, the
4518
+ # largest value 0, the NaN cells (sorted to the end) after the numbers.
4519
+ nan_test = if is_object then "CA_OBJ_ISNAN(buf[nan_start - 1].v)"
4520
+ elsif FLOAT_DTYPES.include?(src) then "isnan(buf[nan_start - 1].v)"
4521
+ end
4522
+ io.puts " if ( dense == 2 && #{rank_n} > 0 ) {"
4523
+ io.puts " ca_size_t nan_start = #{rank_n};"
4524
+ io.puts " while ( nan_start > 0 && #{nan_test} ) nan_start--;" if nan_test
4525
+ io.puts " #{oi[:c]} top = nan_start > 0"
4526
+ io.puts " ? *(#{oi[:c]} *) (po + buf[nan_start - 1].i * ostride) : (#{oi[:c]}) -1;"
4527
+ io.puts " for ( ca_size_t r = 0; r < #{rank_n}; r++ ) {"
4528
+ io.puts " #{oi[:c]} *o = (#{oi[:c]} *) (po + buf[r].i * ostride);"
4529
+ io.puts " *o = ( r < nan_start ) ? top - *o : top + 1;"
4530
+ io.puts " }"
4531
+ io.puts " }"
4524
4532
  if mask_skip
4525
4533
  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). */"
4534
+ io.puts " cells). Write via co->mask->ptr directly, because a"
4535
+ io.puts " mask written through a WRITE walk's cursor does not"
4536
+ io.puts " reach the array. co is a fresh row-major entity and"
4537
+ io.puts " the walk cuts its slabs from a buffer laid out the"
4538
+ io.puts " same way, so the slab's offset from st_out.alias_ptr"
4539
+ io.puts " is its offset in co. */"
4530
4540
  io.puts " if ( mi && co->mask ) {"
4531
4541
  io.puts " boolean8_t *omask_base = (boolean8_t *) co->mask->ptr"
4532
- io.puts " + ((po - (char *) co->ptr) / (ca_size_t) co->bytes);"
4542
+ io.puts " + ((po - st_out.alias_ptr) / (ca_size_t) co->bytes);"
4533
4543
  io.puts " ca_size_t mostride = st_out.slab_mask_strides[0];"
4534
4544
  io.puts " for ( ca_size_t k = 0; k < fiber_n; k++ ) {"
4535
4545
  io.puts " if ( mi[k * mstride] ) omask_base[k * mostride] = 1;"
@@ -4555,45 +4565,33 @@ module MkKernel
4555
4565
  end
4556
4566
 
4557
4567
  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
4568
+ io.puts " ALLOCV_END(hbuf);"
4569
+ io.puts " ALLOCV_END(haux);"
4567
4570
  io.puts " ca_iter_state_finish(&st_in);"
4568
4571
  io.puts " ca_iter_state_finish(&st_out);"
4569
4572
  io.puts " return vout;"
4570
4573
  io.puts "}"
4571
4574
  end
4572
4575
 
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.
4576
+ # Shared sort-family Face gate. Every kernel of the family answers
4577
+ # positions, so it runs on the array the Face's order is read from
4578
+ # (ca_order_key): the storage of an ORDERABLE Face, the Face's own dense
4579
+ # rank for a fixlen storage that is not ORDERABLE (CAConstString's offsets,
4580
+ # a record's bytes), and a refusal otherwise. `src`/`self` are rebound to
4581
+ # that array.
4579
4582
  def self.emit_sort_face_gate(io, name)
4580
4583
  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;"
4584
+ io.puts " self = ca_order_key(self, ca_calling_method_name());"
4585
+ io.puts " GetCArray(self, src);"
4591
4586
  io.puts " }"
4592
4587
  end
4593
4588
 
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
4589
+ # Reduce-family Face gate. Descends an ORDERABLE Face to its storage and
4590
+ # refuses any other (ca_face_require_orderable): a fixlen storage is
4591
+ # reduced by memcmp only when the Face declares that its bytes are its
4592
+ # order (CAFixlenString). CAConstString and CARecord, whose bytes are no
4593
+ # order, answer min / max / minmax in Ruby and do not reach this gate.
4594
+ # self/src are rebound to storage. When
4597
4595
  # `relift` is set (value-returning kernels), the original Face is saved to
4598
4596
  # the function-scope `_reduce_face` so the dispatcher can re-lift the output.
4599
4597
  def self.emit_reduce_face_gate(io, name, relift)
@@ -4602,13 +4600,11 @@ module MkKernel
4602
4600
  end
4603
4601
  io.puts " if ( ca_is_face(src) ) {"
4604
4602
  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);"
4603
+ io.puts " VALUE _st;"
4607
4604
  io.puts " CArray *_sc;"
4605
+ io.puts %Q[ ca_face_require_orderable(_face, ca_calling_method_name(), "order");]
4606
+ io.puts " _st = rb_ca_strip_face_value(self);"
4608
4607
  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
4608
  if relift
4613
4609
  io.puts " _reduce_face = _face;"
4614
4610
  end
@@ -4713,10 +4709,10 @@ module MkKernel
4713
4709
  io.puts " CArray *src;"
4714
4710
  io.puts " GetCArray(self, src);"
4715
4711
  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. */"
4712
+ io.puts " /* Face gate: run on the array the Face's order is read from."
4713
+ io.puts " It is never a Face, so the switch below and every index/addr"
4714
+ io.puts " template output build on a plain array, not a face-lifted"
4715
+ io.puts " (NULL-ptr) view. */"
4720
4716
  emit_sort_face_gate(io, name)
4721
4717
  if k[:mask_self] == :raise
4722
4718
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
@@ -4725,11 +4721,7 @@ module MkKernel
4725
4721
  io.puts %Q[ rb_raise(rb_eArgError, "%s: masked input not supported (use ca.value or ca.strip_mask(fill))", ca_calling_method_name());]
4726
4722
  io.puts " }"
4727
4723
  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 " }"
4724
+ io.puts " int axis = ca_normalize_axis_arg(vaxis, src->ndim, NULL);"
4733
4725
  io.puts " switch ( src->data_type ) {"
4734
4726
  k[:source].each do |s|
4735
4727
  si = DTYPES[s]
@@ -4825,8 +4817,7 @@ module MkKernel
4825
4817
  io.puts " CArray *src;"
4826
4818
  io.puts " GetCArray(self, src);"
4827
4819
  emit_lazy_evaluated(io)
4828
- io.puts " /* Face gate (partition): same as the sort dispatcher --"
4829
- io.puts " fixlen -> memcmp default, numeric -> ORDERABLE required. */"
4820
+ io.puts " /* Face gate (partition): same as the sort dispatcher. */"
4830
4821
  emit_sort_face_gate(io, name)
4831
4822
  if k[:mask_self] == :raise
4832
4823
  io.puts " /* sort family rejects masked sources globally (R3 / Q3): "
@@ -4835,11 +4826,7 @@ module MkKernel
4835
4826
  io.puts %Q[ rb_raise(rb_eArgError, "%s: masked input not supported (use ca.value or ca.strip_mask(fill))", ca_calling_method_name());]
4836
4827
  io.puts " }"
4837
4828
  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 " }"
4829
+ io.puts " int axis = ca_normalize_axis_arg(vaxis, src->ndim, NULL);"
4843
4830
  io.puts " ca_size_t kth_raw = (ca_size_t) ca_integer_arg(vkth, \"kth\", NULL);"
4844
4831
  io.puts " ca_size_t kth = kth_raw;"
4845
4832
  io.puts " ca_size_t axis_n = src->dim[axis];"
@@ -5175,56 +5162,41 @@ module MkKernel
5175
5162
  # flat array path. op_mask is lazily allocated on the first no-match.
5176
5163
  undef_nm = k[:undef_no_match]
5177
5164
  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) */"
5165
+ # A masked query cell yields a masked output cell, so an undetermined
5166
+ # query cannot be answered from the value that happens to sit under the
5167
+ # mask. Shares op_mask with undef_no_match.
5168
+ op_mask_decl = "boolean8_t *op_mask = NULL; /* lazily allocated on the first masked output cell */"
5187
5169
 
5188
5170
  # Case A scalar UNDEF query (also reached by the A1 single-element
5189
5171
  # reduction when that element is masked): every output cell is
5190
5172
  # 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
5173
+ undef_query_a = <<~C.chomp
5174
+ if ( rval == CA_UNDEF ) {
5175
+ int8_t undef_axes[1] = { (int8_t) axis };
5176
+ VALUE voutu = rb_ca_new_reduced(self, undef_axes, 1, #{oi[:ca]}, 0);
5177
+ CArray *cou;
5178
+ GetCArray(voutu, cou);
5179
+ MEMZERO((#{oi[:c]} *) cou->ptr, #{oi[:c]}, cou->elements);
5180
+ ca_create_mask(cou);
5181
+ memset(cou->mask->ptr, 1, (size_t) cou->elements);
5182
+ if ( ca->ndim == 1 ) { return Qnil; }
5183
+ return voutu;
5184
+ }
5185
+ C
5208
5186
 
5209
5187
  # 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) */"
5188
+ cv_mask_decl_bc = "boolean8_t *cv_mask_base = ca_has_mask(cv) ? (boolean8_t *) cv->mask->ptr : NULL;"
5213
5189
 
5214
5190
  # Per-cell guard wrapping the body + write in case B/C. A masked query
5215
5191
  # 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) */"
5192
+ undef_query_open_bc = <<~C.chomp
5193
+ if ( cv_mask_base && cv_mask_base[val_offset / cv->bytes] ) {
5194
+ if ( ! op_mask ) { ca_create_mask(co); op_mask = (boolean8_t *) co->mask->ptr; }
5195
+ op_mask[flat] = 1;
5196
+ op[flat] = (#{oi[:c]}) 0; /* undetermined query; the mask bit is what counts */
5197
+ } else {
5198
+ C
5199
+ undef_query_close_bc = "}"
5228
5200
 
5229
5201
  # Per-cell write block for case A (scalar query). `out_i` is the output
5230
5202
  # cursor, `co` the output CArray. When undef_no_match: a no-match masks
@@ -5642,21 +5614,15 @@ module MkKernel
5642
5614
  end
5643
5615
  io.puts "{"
5644
5616
  io.puts " CArray *src;"
5645
- io.puts " VALUE self_ref = self; /* pre-strip Face, for to_comparable */"
5646
5617
  io.puts " int self_was_face = 0;"
5647
5618
  io.puts " GetCArray(self, src);"
5648
5619
  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. */"
5620
+ io.puts " /* Face gate (self): a Face reference is searched on its numeric"
5621
+ io.puts " storage, which ORDERABLE licenses; the query gate below brings"
5622
+ io.puts " the reference and the query into one space and descends. */"
5653
5623
  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 " }"
5624
+ io.puts %Q[ ca_face_require_orderable(self, ca_calling_method_name(), "order");]
5657
5625
  io.puts " self_was_face = 1;"
5658
- io.puts " self = rb_ca_strip_face_value(self);"
5659
- io.puts " GetCArray(self, src);"
5660
5626
  io.puts " }"
5661
5627
 
5662
5628
  if has_eps
@@ -5668,32 +5634,23 @@ module MkKernel
5668
5634
  io.puts " VALUE raxis = argv[1];"
5669
5635
  io.puts " VALUE reps = (argc >= 3) ? argv[2] : Qnil;"
5670
5636
  end
5671
- io.puts " /* Query gate: the reference Face reconciles the query into its own"
5672
- io.puts " space (ca_face_reconcile_operand). */"
5637
+ io.puts " /* Query gate: the reference Face and the query are brought into one"
5638
+ io.puts " space (ca_face_reconcile_pair), then the reference descends to"
5639
+ io.puts " its storage. */"
5673
5640
  io.puts " if ( self_was_face ) {"
5674
- io.puts " rval = ca_face_reconcile_operand(self_ref, rval, ca_calling_method_name());"
5641
+ io.puts " volatile VALUE ref = self, query = rval;"
5642
+ io.puts " ca_face_reconcile_pair(&ref, &query, ca_calling_method_name());"
5643
+ io.puts " rval = query;"
5644
+ io.puts " self = rb_ca_strip_face_value(ref);"
5645
+ io.puts " GetCArray(self, src);"
5675
5646
  io.puts " }"
5676
5647
 
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 " }"
5648
+ # The query and the reference are compared in the type they match in,
5649
+ # as `a.eq(q)` compares (ca_query_match_type): the reference is read
5650
+ # through a cast view of that type, so the query is never cut to the
5651
+ # reference's type.
5652
+ io.puts " if ( ca_query_match_applies(src, rval) ) {"
5653
+ io.puts " int8_t qt = ca_query_match_type(src->data_type, rval);"
5697
5654
  io.puts " if ( qt != src->data_type ) {"
5698
5655
  io.puts " self = rb_ca_wrap_readonly(self, INT2NUM(qt));"
5699
5656
  io.puts " GetCArray(self, src);"
@@ -5710,11 +5667,7 @@ module MkKernel
5710
5667
  # body handles mask_in or ignores it
5711
5668
  end
5712
5669
 
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 " }"
5670
+ io.puts " int axis = ca_normalize_axis_arg(raxis, src->ndim, NULL);"
5718
5671
  io.puts " switch ( src->data_type ) {"
5719
5672
  k[:source].each do |s|
5720
5673
  si = DTYPES[s]
@@ -6539,7 +6492,8 @@ module MkKernel
6539
6492
  # the source's data type. Left out: a boolean target, which checks its
6540
6493
  # input and raises, and a float or complex source to an integer target,
6541
6494
  # where an out-of-range value or NaN has no defined C result for a
6542
- # second compilation to agree on.
6495
+ # second compilation to agree on. An int64 or uint64 source to float32
6496
+ # or cmplx64 goes through double, as ca_cast_block does.
6543
6497
  CAST_SOURCES = %i[bool i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6544
6498
  cmplx64 cmplx128].freeze
6545
6499
  CAST_TARGETS = %i[i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
@@ -6550,8 +6504,9 @@ module MkKernel
6550
6504
  CAST_SOURCES.product(CAST_TARGETS).filter_map { |src, dst|
6551
6505
  next if CAST_INEXACT.include?(src) && !CAST_INEXACT.include?(dst)
6552
6506
  name = DTYPES[dst][:ca].sub(/\ACA_/, "").downcase
6507
+ via = ( %i[i64 u64].include?(src) && %i[f32 cmplx64].include?(dst) ) ? "(double)" : ""
6553
6508
  ["monop", "cast_#{name}", DTYPES[src][:ca].sub(/\ACA_/, "").downcase,
6554
- "(#2) = (#{DTYPES[dst][:c]})(#1);"]
6509
+ "(#2) = (#{DTYPES[dst][:c]})#{via}(#1);"]
6555
6510
  }
6556
6511
  end
6557
6512
 
@@ -6756,12 +6711,16 @@ module MkKernel
6756
6711
  # reduce: default false (E.8 retire), opt-in via bind_ruby: true
6757
6712
  # (used by CF.1 count_equal smoke + future kernel testing)
6758
6713
  # sort: default true, opt-out via bind_ruby: false
6759
- # scan: always bound (no opt-out)
6714
+ # scan: bound as <name>_ki, or only privately as <name> (private:)
6760
6715
  bind = case k[:kind]
6761
6716
  when :reduce then k[:bind_ruby] == true
6762
6717
  when :sort then k[:bind_ruby] != false
6718
+ when :scan then ! k[:private]
6763
6719
  else true
6764
6720
  end
6721
+ if k[:kind] == :scan && k[:private]
6722
+ io.puts %Q[ rb_define_private_method(rb_cCArray, "#{k[:name]}", rb_ca_#{k[:name]}_ki, #{arity});]
6723
+ end
6765
6724
  # Smoke kernels (e.g. count_equal) expose their _ki binding only
6766
6725
  # in dev builds, gated by CARRAY_DEV_BUILD. See
6767
6726
  # devel/PROPOSAL_SMOKE_DEV_BUILD_GATE.md.
@@ -6774,7 +6733,7 @@ module MkKernel
6774
6733
  # directly without a Ruby wrapper. This is paired with
6775
6734
  # axis_default: :flatten because both express the legacy-compat
6776
6735
  # convenience of these specific scan kernels.
6777
- if k[:kind] == :scan && k[:axis_default] == :flatten
6736
+ if k[:kind] == :scan && k[:axis_default] == :flatten && ! k[:private]
6778
6737
  io.puts %Q[ rb_define_method(rb_cCArray, "#{k[:name]}", rb_ca_#{k[:name]}_ki, #{arity});]
6779
6738
  end
6780
6739
  # Phase E rewire: when this kernel replaces a legacy CArray method,
@@ -7398,7 +7357,7 @@ MkKernel.reduce :wsum,
7398
7357
  shape: :rev5_strict, mask: :overlay },
7399
7358
  mask_policy: :min_count,
7400
7359
  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)
7360
+ object_escape: :wsum_object, # CA_OBJECT -> (w*self).sum (runtime.rb)
7402
7361
  bind_ruby: true,
7403
7362
  public_method: true
7404
7363
 
@@ -7415,7 +7374,7 @@ MkKernel.reduce :wsum,
7415
7374
  # - min_count semantic "max masked" -> "min valid required" (Phase E)
7416
7375
  # - complex (CMPLX64/128) dropped (= ALL_NUMERIC + :raise)
7417
7376
  # - per-axis support gained
7418
- # CA_OBJECT is handled via object_escape (runtime.rb __wmean_object__), not
7377
+ # CA_OBJECT is handled via object_escape (runtime.rb wmean_object), not
7419
7378
  # the numeric kernel: weight-coerce vs object arithmetic is cleaner as a
7420
7379
  # (w*self).sum / Σw composition than a dedicated :object kernel branch.
7421
7380
 
@@ -7432,7 +7391,7 @@ MkKernel.reduce :wmean,
7432
7391
  array_arg: { name: :weights, data_type: :promote,
7433
7392
  shape: :rev5_strict, mask: :overlay },
7434
7393
  mask_policy: :min_count,
7435
- object_escape: :__wmean_object__, # CA_OBJECT -> weighted mean via runtime.rb
7394
+ object_escape: :wmean_object, # CA_OBJECT -> weighted mean via runtime.rb
7436
7395
  bind_ruby: true,
7437
7396
  public_method: true
7438
7397
 
@@ -7457,6 +7416,18 @@ MkKernel.scan :cumsum,
7457
7416
  fallback: :raise,
7458
7417
  axis_default: :flatten
7459
7418
 
7419
+ # The running sum in the source's own integer type, added without a range
7420
+ # check (int64 wraps, as accumulate does). CATimedelta#cumsum reads it
7421
+ # for an exact running sum of durations.
7422
+ MkKernel.scan :cumsum_in_own_type,
7423
+ source: MkKernel::INT_DTYPES,
7424
+ output: :preserve,
7425
+ init: "0",
7426
+ step: "acc += v; r = acc",
7427
+ fallback: :raise,
7428
+ axis_default: :flatten,
7429
+ private: true
7430
+
7460
7431
  MkKernel.scan :cumprod,
7461
7432
  source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES + [:bool, :object],
7462
7433
  output: { numeric: :f64, complex: :cmplx128, bool: :u64, object: :object },
@@ -7732,7 +7703,6 @@ MkKernel.search :bsearch,
7732
7703
  while ( lo < hi ) {
7733
7704
  ca_size_t mid = lo + (hi - lo) / 2;
7734
7705
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7735
- if ( isnan((double) v) ) { hi = mid; continue; }
7736
7706
  if ( v < query_val ) lo = mid + 1;
7737
7707
  else if ( v > query_val ) hi = mid;
7738
7708
  else { result = mid; break; }
@@ -7741,35 +7711,30 @@ MkKernel.search :bsearch,
7741
7711
  C
7742
7712
  float: <<~C,
7743
7713
  result = (ca_size_t) -1;
7744
- if ( ! isnan((double) query_val) ) {
7714
+ if ( ! isnan((double) query_val) ) { /* a NaN query matches nothing */
7745
7715
  ca_size_t lo = 0;
7746
7716
  ca_size_t hi = slab_n;
7747
7717
  while ( lo < hi ) {
7748
7718
  ca_size_t mid = lo + (hi - lo) / 2;
7749
7719
  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;
7720
+ int c = CA_FLOAT_ORDER_CMP(v, query_val);
7721
+ if ( c < 0 ) lo = mid + 1;
7722
+ else if ( c > 0 ) hi = mid;
7753
7723
  else { result = mid; break; }
7754
7724
  }
7755
7725
  }
7756
7726
  C
7757
7727
  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. */
7728
+ /* CA_OBJECT bsearch: the object order (ca_obj_order_cmp), which the
7729
+ slab is assumed to be sorted by; a NaN query matches nothing, as in
7730
+ float. */
7761
7731
  result = (ca_size_t) -1;
7762
7732
  ca_size_t lo = 0;
7763
7733
  ca_size_t hi = CA_OBJ_ISNAN(query_val) ? 0 : slab_n; /* NaN matches nothing */
7764
7734
  while ( lo < hi ) {
7765
7735
  ca_size_t mid = lo + (hi - lo) / 2;
7766
7736
  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);
7737
+ int c = ca_obj_order_cmp(v, query_val);
7773
7738
  if ( c < 0 ) lo = mid + 1;
7774
7739
  else if ( c > 0 ) hi = mid;
7775
7740
  else { result = mid; break; }
@@ -7971,7 +7936,6 @@ MkKernel.search :bsearch_addr,
7971
7936
  while ( lo < hi ) {
7972
7937
  ca_size_t mid = lo + (hi - lo) / 2;
7973
7938
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7974
- if ( isnan((double) v) ) { hi = mid; continue; }
7975
7939
  if ( v < query_val ) lo = mid + 1;
7976
7940
  else if ( v > query_val ) hi = mid;
7977
7941
  else { result = mid; break; }
@@ -7980,15 +7944,15 @@ MkKernel.search :bsearch_addr,
7980
7944
  C
7981
7945
  float: <<~C,
7982
7946
  result = (ca_size_t) -1;
7983
- if ( ! isnan((double) query_val) ) {
7947
+ if ( ! isnan((double) query_val) ) { /* a NaN query matches nothing */
7984
7948
  ca_size_t lo = 0;
7985
7949
  ca_size_t hi = slab_n;
7986
7950
  while ( lo < hi ) {
7987
7951
  ca_size_t mid = lo + (hi - lo) / 2;
7988
7952
  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;
7953
+ int c = CA_FLOAT_ORDER_CMP(v, query_val);
7954
+ if ( c < 0 ) lo = mid + 1;
7955
+ else if ( c > 0 ) hi = mid;
7992
7956
  else { result = mid; break; }
7993
7957
  }
7994
7958
  }
@@ -8000,12 +7964,7 @@ MkKernel.search :bsearch_addr,
8000
7964
  while ( lo < hi ) {
8001
7965
  ca_size_t mid = lo + (hi - lo) / 2;
8002
7966
  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);
7967
+ int c = ca_obj_order_cmp(v, query_val);
8009
7968
  if ( c < 0 ) lo = mid + 1;
8010
7969
  else if ( c > 0 ) hi = mid;
8011
7970
  else { result = mid; break; }
@@ -8188,7 +8147,6 @@ MkKernel.search :linear_section_binary,
8188
8147
  no_match: "NAN",
8189
8148
  no_match_check: "isnan(op[0])",
8190
8149
  mask_self: :raise,
8191
- mask_query: :undef,
8192
8150
  body: <<~C,
8193
8151
  result = NAN;
8194
8152
  if ( slab_n <= 1 ) {
@@ -8280,7 +8238,6 @@ MkKernel.search :linear_section_linear,
8280
8238
  no_match: "NAN",
8281
8239
  no_match_check: "isnan(op[0])",
8282
8240
  mask_self: :raise,
8283
- mask_query: :undef,
8284
8241
  body: <<~C,
8285
8242
  result = NAN;
8286
8243
  if ( slab_n <= 1 ) {
@@ -8323,7 +8280,6 @@ MkKernel.search :linear_fetch,
8323
8280
  no_match: "NAN",
8324
8281
  no_match_check: "isnan(op[0])",
8325
8282
  mask_self: :raise,
8326
- mask_query: :undef,
8327
8283
  body: <<~C,
8328
8284
  result = NAN;
8329
8285
  if ( slab_n >= 1 && query_val >= 0.0 && query_val <= (double)(slab_n - 1) ) {
@@ -8588,7 +8544,9 @@ MkKernel.monfunc :round,
8588
8544
  source: MkKernel::ALL_NUMERIC + [:object],
8589
8545
  expr: {
8590
8546
  int: "(#2) = (#1);",
8591
- float: "(#2) = ((#1)>0.0) ? floor((#1)+0.5) : ((#1)<0.0) ? ceil((#1)-0.5) : (#1);",
8547
+ # Half away from zero, as Float#round. floor(x + 0.5) would round in
8548
+ # the addition: 0.49999999999999994 + 0.5 is 1.0.
8549
+ float: "(#2) = round(#1);",
8592
8550
  object: '(#2) = rb_funcall((#1), rb_intern("round"), 0);',
8593
8551
  }
8594
8552
 
@@ -9259,13 +9217,13 @@ MkKernel.triop :fms,
9259
9217
  # UNDEF-fill case can now be expressed as `out = a.clip(lo, hi);
9260
9218
  # out[a.lt(lo) | a.gt(hi)] = UNDEF`). Pinned in test.
9261
9219
  #
9262
- # Bound at the C level as `__clip_ki__` (private kernel entry). The
9220
+ # Bound at the C level as `clamp_between` (made private by lazy.rb). The
9263
9221
  # public `CArray#clip` is a Ruby wrapper in lib/carray/math.rb that
9264
9222
  # restores the rich pre-3.0 API surface (nil-bound one-sided clip +
9265
9223
  # optional fill). `clip!` is retired in 3.0 (view-by-default
9266
9224
  # convention) so the bang form is suppressed via `bang: false`.
9267
9225
  MkKernel.triop :clip,
9268
- op: "__clip_ki__",
9226
+ op: "clamp_between",
9269
9227
  bang: false,
9270
9228
  source: MkKernel::ALL_NUMERIC + [:object],
9271
9229
  expr: {