carray 3.0.0 → 3.0.2

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +845 -3
  4. data/{NEWS.md → CHANGELOG.v1.md} +1 -1
  5. data/README.md +33 -33
  6. data/carray.gemspec +12 -8
  7. data/ext/ca_axis_dispatch.c +33 -4
  8. data/ext/ca_axis_group.c +256 -114
  9. data/ext/ca_binop_dispatch.c +5 -6
  10. data/ext/ca_binop_dispatch.h +0 -7
  11. data/ext/ca_categorical_iterator.c +121 -67
  12. data/ext/ca_for_each_element.h +6 -8
  13. data/ext/ca_group_iter.c +1 -1
  14. data/ext/ca_kernel_iterator.c +386 -74
  15. data/ext/ca_kernel_iterator.h +145 -45
  16. data/ext/ca_obj_array.c +62 -20
  17. data/ext/ca_obj_bincmp.c +57 -27
  18. data/ext/ca_obj_binop.c +86 -30
  19. data/ext/ca_obj_block.c +4 -4
  20. data/ext/ca_obj_const_string.c +88 -27
  21. data/ext/ca_obj_face.c +104 -0
  22. data/ext/ca_obj_face.h +48 -1
  23. data/ext/ca_obj_fake.c +11 -0
  24. data/ext/ca_obj_fixlen_string.c +21 -6
  25. data/ext/ca_obj_grid.c +11 -3
  26. data/ext/ca_obj_meld.c +125 -22
  27. data/ext/ca_obj_moncmp.c +33 -14
  28. data/ext/ca_obj_monop.c +19 -7
  29. data/ext/ca_obj_object.c +107 -4
  30. data/ext/ca_obj_record.c +3 -1
  31. data/ext/ca_obj_refer.c +30 -6
  32. data/ext/ca_obj_roll.c +13 -5
  33. data/ext/ca_obj_select.c +49 -34
  34. data/ext/ca_obj_select_axis.c +16 -6
  35. data/ext/ca_obj_shift.c +3 -3
  36. data/ext/ca_obj_stack.c +3 -8
  37. data/ext/ca_obj_stride.c +178 -24
  38. data/ext/ca_obj_string.c +11 -5
  39. data/ext/ca_obj_tile.c +13 -5
  40. data/ext/ca_obj_time.c +3 -1
  41. data/ext/ca_obj_timedelta.c +3 -1
  42. data/ext/ca_obj_transpose.c +1 -1
  43. data/ext/ca_obj_triop.c +62 -23
  44. data/ext/ca_obj_window.c +94 -19
  45. data/ext/ca_op_cmplx64.h +123 -0
  46. data/ext/ca_op_ipower.c +1 -5
  47. data/ext/ca_rng_normal.h +42 -0
  48. data/ext/ca_rng_xoshiro256pp.h +105 -0
  49. data/ext/ca_sort_kernels.h +5 -5
  50. data/ext/ca_sweep_engine.c +362 -155
  51. data/ext/ca_sweep_engine.h +39 -9
  52. data/ext/ca_transform_common.c +7 -0
  53. data/ext/carray.h +148 -132
  54. data/ext/carray_access.c +83 -49
  55. data/ext/carray_address_basis.c +590 -0
  56. data/ext/carray_bincount.c +8 -8
  57. data/ext/carray_broadcast.c +102 -9
  58. data/ext/carray_build_flags.h +3 -0
  59. data/ext/carray_call_cfunc.c +2010 -335
  60. data/ext/carray_call_cfunc.h +153 -0
  61. data/ext/carray_cast.c +184 -72
  62. data/ext/carray_conversion.c +22 -22
  63. data/ext/carray_copy.c +67 -32
  64. data/ext/carray_core.c +140 -13
  65. data/ext/carray_count.c +10 -11
  66. data/ext/carray_element.c +7 -7
  67. data/ext/carray_factorize.c +72 -51
  68. data/ext/carray_hold.c +1 -1
  69. data/ext/carray_index_classifier.c +7 -21
  70. data/ext/carray_internal.h +36 -3
  71. data/ext/carray_kernels_bincmp.c +1 -0
  72. data/ext/carray_kernels_binop.c +320 -298
  73. data/ext/carray_kernels_init.c +1095 -0
  74. data/ext/carray_kernels_map.c +1 -0
  75. data/ext/carray_kernels_moncmp.c +1 -0
  76. data/ext/carray_kernels_monop.c +412 -411
  77. data/ext/carray_kernels_reduce_aggregate.c +256 -87
  78. data/ext/carray_kernels_reduce_boolean.c +1 -0
  79. data/ext/carray_kernels_reduce_cumulative.c +324 -54
  80. data/ext/carray_kernels_reduce_extreme.c +605 -58
  81. data/ext/carray_kernels_reduce_variance.c +1 -0
  82. data/ext/carray_kernels_scan.c +5 -4
  83. data/ext/carray_kernels_search.c +187 -106
  84. data/ext/carray_kernels_sort.c +1 -0
  85. data/ext/carray_kernels_triop.c +1 -0
  86. data/ext/carray_lazy.c +175 -5
  87. data/ext/carray_loop.c +7 -1
  88. data/ext/carray_mask.c +29 -13
  89. data/ext/carray_median_percentile.c +56 -1
  90. data/ext/carray_memory_view.c +46 -21
  91. data/ext/carray_operator.c +50 -53
  92. data/ext/carray_order.c +1 -1
  93. data/ext/carray_partition.c +4 -4
  94. data/ext/carray_random.c +391 -47
  95. data/ext/carray_scatter.c +1 -1
  96. data/ext/carray_slab.c +13 -0
  97. data/ext/carray_sort.c +22 -24
  98. data/ext/carray_sort_kernel.c +10 -10
  99. data/ext/carray_test.c +1 -1
  100. data/ext/extconf.rb +18 -0
  101. data/ext/mk_call_cfunc.rb +323 -145
  102. data/ext/mkkernel.rb +651 -129
  103. data/ext/ruby_carray.c +28 -17
  104. data/ext/version.h +4 -4
  105. data/lib/carray/autoload_carray.rb +24 -3
  106. data/lib/carray/autoload_method_extension.rb +14 -1
  107. data/lib/carray/axis_group.rb +84 -7
  108. data/lib/carray/basics.rb +63 -59
  109. data/lib/carray/bincount_nd.rb +12 -12
  110. data/lib/carray/block_iterator.rb +111 -25
  111. data/lib/carray/categorical.rb +154 -37
  112. data/lib/carray/categorical_iterator.rb +300 -151
  113. data/lib/carray/conditional.rb +14 -14
  114. data/lib/carray/const_string.rb +131 -27
  115. data/lib/carray/construct.rb +61 -0
  116. data/lib/carray/core_extensions.rb +62 -5
  117. data/lib/carray/data_type_extension.rb +21 -1
  118. data/lib/carray/data_type_limits.rb +91 -0
  119. data/lib/carray/fixlen_string.rb +1 -1
  120. data/lib/carray/frame/concat.rb +3 -3
  121. data/lib/carray/frame/convert.rb +1 -1
  122. data/lib/carray/frame/csv_parser.rb +11 -4
  123. data/lib/carray/frame/frame.rb +85 -21
  124. data/lib/carray/frame/group.rb +36 -3
  125. data/lib/carray/frame/io.rb +224 -15
  126. data/lib/carray/frame/records.rb +18 -4
  127. data/lib/carray/frame/sort.rb +1 -1
  128. data/lib/carray/frame/verbs.rb +32 -12
  129. data/lib/carray/fuse_source.rb +123 -0
  130. data/lib/carray/fusion.rb +218 -0
  131. data/lib/carray/histogram.rb +16 -16
  132. data/lib/carray/inspect.rb +43 -16
  133. data/lib/carray/iterator.rb +147 -3
  134. data/lib/carray/lazy.rb +88 -73
  135. data/lib/carray/mask_gap_fill.rb +3 -1
  136. data/lib/carray/meld_reduce.rb +2 -2
  137. data/lib/carray/methods/align_addr.rb +1 -1
  138. data/lib/carray/methods/composition.rb +1 -1
  139. data/lib/carray/methods/discovery_along.rb +74 -0
  140. data/lib/carray/methods/factorize.rb +50 -0
  141. data/lib/carray/methods/is_in.rb +25 -14
  142. data/lib/carray/methods/locate_addr.rb +81 -3
  143. data/lib/carray/methods/mask_duplicates.rb +36 -2
  144. data/lib/carray/methods/meshgrid.rb +4 -5
  145. data/lib/carray/methods/mode.rb +2 -2
  146. data/lib/carray/methods/nunique.rb +23 -2
  147. data/lib/carray/methods/repeat.rb +110 -0
  148. data/lib/carray/methods/snap.rb +7 -2
  149. data/lib/carray/methods/unique.rb +44 -4
  150. data/lib/carray/methods/value_counts.rb +2 -2
  151. data/lib/carray/rng.rb +86 -0
  152. data/lib/carray/runtime.rb +0 -19
  153. data/lib/carray/slab_iterator.rb +77 -19
  154. data/lib/carray/string_operation_extension.rb +10 -6
  155. data/lib/carray/time.rb +906 -491
  156. data/lib/carray/window_iterator.rb +421 -27
  157. data/lib/carray.rb +7 -5
  158. data/yard-stubs/ca_obj_array.rb +385 -0
  159. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  160. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  161. data/yard-stubs/ca_obj_block.rb +68 -0
  162. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  163. data/yard-stubs/ca_obj_fake.rb +31 -0
  164. data/yard-stubs/ca_obj_farray.rb +32 -0
  165. data/yard-stubs/ca_obj_field.rb +45 -0
  166. data/yard-stubs/ca_obj_grid.rb +35 -0
  167. data/yard-stubs/ca_obj_refer.rb +72 -0
  168. data/yard-stubs/ca_obj_roll.rb +45 -0
  169. data/yard-stubs/ca_obj_shift.rb +43 -0
  170. data/yard-stubs/ca_obj_stride.rb +181 -0
  171. data/yard-stubs/ca_obj_tile.rb +29 -0
  172. data/yard-stubs/ca_obj_transpose.rb +40 -0
  173. data/yard-stubs/ca_obj_window.rb +57 -0
  174. data/yard-stubs/carray_access.rb +131 -0
  175. data/yard-stubs/carray_attribute.rb +246 -0
  176. data/yard-stubs/carray_broadcast.rb +37 -0
  177. data/yard-stubs/carray_cast.rb +489 -0
  178. data/yard-stubs/carray_class.rb +65 -0
  179. data/yard-stubs/carray_conversion.rb +76 -0
  180. data/yard-stubs/carray_copy.rb +79 -0
  181. data/yard-stubs/carray_core.rb +34 -0
  182. data/yard-stubs/carray_count.rb +84 -0
  183. data/yard-stubs/carray_element.rb +108 -0
  184. data/yard-stubs/carray_generate.rb +66 -0
  185. data/yard-stubs/carray_lazy.rb +228 -0
  186. data/yard-stubs/carray_loop.rb +140 -0
  187. data/yard-stubs/carray_mask.rb +259 -0
  188. data/yard-stubs/carray_math.rb +1615 -0
  189. data/yard-stubs/carray_mathfunc.rb +45 -0
  190. data/yard-stubs/carray_median_percentile.rb +103 -0
  191. data/yard-stubs/carray_memory_view.rb +163 -0
  192. data/yard-stubs/carray_order.rb +252 -0
  193. data/yard-stubs/carray_random.rb +89 -0
  194. data/yard-stubs/carray_scatter.rb +106 -0
  195. data/yard-stubs/carray_slab.rb +59 -0
  196. data/yard-stubs/carray_sort.rb +165 -0
  197. data/yard-stubs/carray_test.rb +85 -0
  198. data/yard-stubs/carray_undef.rb +64 -0
  199. data/yard-stubs/carray_utils.rb +97 -0
  200. data/yard-stubs/ruby_carray.rb +193 -0
  201. metadata +67 -10
  202. data/ext/ca_obj_unbound_repeat.c +0 -496
data/ext/mkkernel.rb CHANGED
@@ -144,6 +144,55 @@ module MkKernel
144
144
  SNIPPET
145
145
  end
146
146
 
147
+ # Complex libm comes in two widths: `csqrt` takes a `double _Complex`,
148
+ # `csqrtf` a `float _Complex`. A body written with only the double-
149
+ # taking name still compiles for a cmplx64 cell, because the cell
150
+ # widens on the way in and rounds on the way out -- so the kernel
151
+ # computes at cmplx128 whatever the array said it was, and the `+ - *`
152
+ # kernels beside it stay narrow. This builds the two expr entries that
153
+ # hold each complex data_type at its own width.
154
+ #
155
+ # In `body`, `<f>` marks each spot the `f` suffix belongs and `<t>` a
156
+ # real scalar of the matching width:
157
+ #
158
+ # cmplx_widths("(#2) = csqrt<f>(#1);")
159
+ # cmplx_widths("{ <t> _r = creal<f>(#1); ... }")
160
+ #
161
+ # Returns array-keyed entries, so merge it into an expr Hash that
162
+ # carries the other families:
163
+ #
164
+ # expr: { float: "...", object: "..." }.merge(cmplx_widths("..."))
165
+ def self.cmplx_widths(body)
166
+ {
167
+ [:cmplx64] => at_width(body, "f", "float"),
168
+ [:cmplx128] => at_width(body, "", "double"),
169
+ }
170
+ end
171
+
172
+ # The same split for the real families, and for the same reason: `sin`
173
+ # takes a double, so an f32 cell widens on the way in and rounds on the
174
+ # way out. Use it in place of a `float:` entry whenever the body calls
175
+ # a libm function that has an `f` variant.
176
+ #
177
+ # expr: { object: "..." }.merge(float_widths("(#2) = sin<f>(#1);"))
178
+ #
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.
184
+ def self.float_widths(body)
185
+ {
186
+ [:f32] => at_width(body, "f", "float"),
187
+ [:f64] => at_width(body, "", "double"),
188
+ }
189
+ end
190
+
191
+ # Substitute the width markers in a kernel body.
192
+ def self.at_width(body, suffix, real_type)
193
+ body.gsub("<f>", suffix).gsub("<t>", real_type)
194
+ end
195
+
147
196
  # CA_NTYPE order from ext/carray.h. Drives the per-data_type table layout
148
197
  # for eager-style monop/binop tables `ca_<form>_<name>[CA_NTYPE]`.
149
198
  # `:reserved` slots emit `ca_<form>_not_implement` (= retired holes
@@ -311,6 +360,7 @@ module MkKernel
311
360
  face_gate: nil,
312
361
  object_escape: nil,
313
362
  identity_on_empty: false,
363
+ all_nan_result: nil,
314
364
  outputs: 1,
315
365
  # Two-pass centred algorithm (variance / stddev family).
316
366
  # When algorithm: :two_pass_centred is set, `state / init /
@@ -416,6 +466,30 @@ module MkKernel
416
466
  end
417
467
  end
418
468
  end
469
+ # all_nan_result: what an extreme-value reduction answers when every
470
+ # contributing cell was NaN. The `(v < acc) ? v : acc` reduce body is
471
+ # false for NaN, so acc is never updated and the init (+/-INFINITY)
472
+ # leaks out as the answer -- a value that is neither the minimum of
473
+ # anything nor a missing marker, and indistinguishable from data that
474
+ # genuinely held only +INFINITY.
475
+ #
476
+ # :nan -- answer NaN (C99 fmin folded: a lone non-NaN wins, two
477
+ # NaNs give NaN). For kernels whose output can hold it.
478
+ # :undef -- answer UNDEF, by folding into the mask_policy trigger.
479
+ # For kernels whose output is a position (argmin family):
480
+ # an integer output cannot hold NaN, and index 0 would be
481
+ # a lie about where the minimum is.
482
+ #
483
+ # Only the float srcs act on it; integers and boolean have no NaN and
484
+ # their init is a legitimate value.
485
+ unless all_nan_result.nil? || %i[nan undef].include?(all_nan_result)
486
+ raise "#{name}: all_nan_result #{all_nan_result.inspect} invalid " \
487
+ "(expected :nan, :undef, or nil)"
488
+ end
489
+ if all_nan_result == :undef && mask_policy.nil?
490
+ raise "#{name}: all_nan_result: :undef needs a mask_policy " \
491
+ "(UNDEF is written through the mask_policy trigger)"
492
+ end
419
493
  if value_arg
420
494
  raise "#{name}: value_arg must be a Hash" unless value_arg.is_a?(Hash)
421
495
  raise "#{name}: value_arg: target must be :T_IN" unless value_arg[:target] == :T_IN
@@ -429,16 +503,29 @@ module MkKernel
429
503
  # selects direction (:min / :max) and output kind:
430
504
  # :min / :max -> extremum blob, output data_type = CA_FIXLEN
431
505
  # :argmin / :argmax -> position of the extremum, output i64
506
+ # :count_equal -> how many cells memcmp-equal value_arg, output i64
432
507
  # Author must also list :fixlen in source: (parallel to how :object
433
508
  # opts in via source: + an :object body).
509
+ #
510
+ # :count_equal is the one mode that takes a value_arg, and takes it as a
511
+ # byte blob rather than a scalar: the dispatcher packs the query with
512
+ # rb_ca_obj2ptr into a ca->bytes buffer, the same way the search family
513
+ # already does for a fixlen query (which is what makes a short query
514
+ # NUL-pad to the cell width instead of never matching).
434
515
  if fixlen
435
- unless %i[min max argmin argmax].include?(fixlen)
436
- raise "#{name}: fixlen: must be :min / :max / :argmin / :argmax (got #{fixlen.inspect})"
516
+ unless %i[min max argmin argmax count_equal].include?(fixlen)
517
+ raise "#{name}: fixlen: must be :min / :max / :argmin / :argmax / :count_equal (got #{fixlen.inspect})"
437
518
  end
438
519
  raise "#{name}: fixlen: requires :fixlen in source:" unless source.include?(:fixlen)
439
520
  raise "#{name}: fixlen: requires mask_policy: :min_count" unless mask_policy == :min_count
440
521
  raise "#{name}: fixlen: requires outputs: 1" unless outputs == 1
441
- raise "#{name}: fixlen: does not support value_arg / array_arg" if value_arg || array_arg
522
+ raise "#{name}: fixlen: does not support array_arg" if array_arg
523
+ if value_arg && fixlen != :count_equal
524
+ raise "#{name}: fixlen: only :count_equal takes a value_arg"
525
+ end
526
+ if fixlen == :count_equal && !value_arg
527
+ raise "#{name}: fixlen: :count_equal requires value_arg"
528
+ end
442
529
  elsif source.include?(:fixlen)
443
530
  raise "#{name}: source includes :fixlen but no fixlen: spec given"
444
531
  end
@@ -563,6 +650,7 @@ module MkKernel
563
650
  face_gate: face_gate,
564
651
  object_escape: object_escape,
565
652
  identity_on_empty: identity_on_empty,
653
+ all_nan_result: all_nan_result,
566
654
  outputs: outputs,
567
655
  algorithm: algorithm,
568
656
  divisor: divisor,
@@ -757,7 +845,7 @@ module MkKernel
757
845
  # output cell is UNDEF).
758
846
  # :sentinel -> masked cells are treated as an incomparable sentinel,
759
847
  # the same role NaN plays for :end nan_policy but runtime-
760
- # selectable and dtype-agnostic. Per fiber, unmasked
848
+ # selectable and type-agnostic. Per fiber, unmasked
761
849
  # cells are compacted into a contiguous sub-range and only
762
850
  # that sub-range is sorted/quickselected; masked cells are
763
851
  # compacted into the complementary sub-range at the head
@@ -1005,7 +1093,9 @@ module MkKernel
1005
1093
  # demotes to real f64). Hash form requires the kernel author to
1006
1094
  # write expr that produces output data_type values for each source family
1007
1095
  # (e.g. cabs() for complex returning double).
1008
- raise "#{name}: unknown output #{output}" unless output == :preserve || DTYPES.key?(output) || output.is_a?(Hash)
1096
+ raise "#{name}: unknown output #{output}" \
1097
+ unless output == :preserve || output == :real_of_source ||
1098
+ DTYPES.key?(output) || output.is_a?(Hash)
1009
1099
  KERNELS << {
1010
1100
  kind: :monop,
1011
1101
  name: name,
@@ -1188,6 +1278,13 @@ module MkKernel
1188
1278
  # fallback when no family matches. Uses the same family aliases
1189
1279
  # as monop_expr_family_match? (:numeric / :int / :float / :complex
1190
1280
  # / :bool / :object).
1281
+ #
1282
+ # A Hash value (or a bare output form) may also be :real_of_source,
1283
+ # meaning "the real component width of the source complex data_type"
1284
+ # (cmplx64 -> f32, cmplx128 -> f64). This is what an op like abs
1285
+ # wants for its complex family: the magnitude of a cmplx64 is a
1286
+ # float32, the same width `.real` and `.imag` already return. Naming
1287
+ # :f64 there would hand cmplx64 the real width of cmplx128.
1191
1288
  # SL.1.1: Resolve the reduce macro suffix for a given kernel entry,
1192
1289
  # driven by reduction_kind. Returns "" for :none (= legacy
1193
1290
  # CA_SLAB_REDUCE_T_EX), or "_PLUS" / "_MIN" / "_MAX" / "_STAR" to
@@ -1208,7 +1305,7 @@ module MkKernel
1208
1305
  #
1209
1306
  # Ineligible:
1210
1307
  # - suffix == "" (:object src / no_simd_src override — reduce body
1211
- # is Ruby callback or dtype-specific, single-accumulator required)
1308
+ # is Ruby callback or type-specific, single-accumulator required)
1212
1309
  # - array_arg (weighted reductions use the ARRAY_T_EX macro family,
1213
1310
  # which has its own emit path)
1214
1311
  def self.reduce_8way_eligible?(k, src, suffix)
@@ -1293,6 +1390,62 @@ module MkKernel
1293
1390
  io.puts "#undef #{expr_macro}"
1294
1391
  end
1295
1392
 
1393
+ # State vars that start at the type's limit (T_LIMIT_HI / T_LIMIT_LO)
1394
+ # and so can leak their init as an answer. A position state (argmin's
1395
+ # best_i, init "0") is deliberately not one of them: index 0 is a value
1396
+ # a real minimum can legitimately produce.
1397
+ def self.limit_init_state_vars(k, si, oi, src)
1398
+ return [] unless k[:state]
1399
+ k[:state].keys.filter_map do |var|
1400
+ token = pick_family_string(k[:init][var], src, "init")
1401
+ next unless %w[T_LIMIT_HI T_LIMIT_LO].include?(token)
1402
+ [var, resolve_init_expr(k[:init][var], oi, si, src)]
1403
+ end
1404
+ end
1405
+
1406
+ # Emit the all-NaN fix-up for an extreme-value reduction (see the
1407
+ # all_nan_result: validation in MkKernel.reduce for why it exists).
1408
+ #
1409
+ # The per-cell reduce body is left alone: it stays the SIMD-licensed
1410
+ # ternary, so the 8-way horizontal split (reduce_8way_eligible?) and
1411
+ # the `reduction(min:acc)` clause are untouched, and ordinary data pays
1412
+ # one comparison per slab. Carrying a "saw a number" flag as a second
1413
+ # DSL state var was the alternative and costs more than it looks: it
1414
+ # takes the kernel over reduce_8way_eligible?'s single-state gate and
1415
+ # drops min / max back onto the legacy single-accumulator macro.
1416
+ #
1417
+ # acc still sitting at its init means either nothing updated it or the
1418
+ # data genuinely held only +/-INFINITY. Those are told apart by
1419
+ # walking the slab once more for a non-NaN cell -- O(n), and only for
1420
+ # a slab whose answer came out equal to the init.
1421
+ #
1422
+ # Returns true when it emitted anything.
1423
+ def self.emit_all_nan_fixup(io, k, src, si, pairs, valid_guard,
1424
+ indent: " ", flag_var: nil)
1425
+ mode = k[:all_nan_result]
1426
+ return false unless mode
1427
+ return false unless FLOAT_DTYPES.include?(src)
1428
+ cond = pairs.map { |var, init| "#{var} == (#{init})" }.join(" && ")
1429
+ cond = "(#{cond}) && #{valid_guard}" if valid_guard
1430
+ io.puts "#{indent}/* all-NaN fix-up (all_nan_result: #{mode.inspect}). */"
1431
+ io.puts "#{indent}if ( #{cond} ) {"
1432
+ io.puts "#{indent} int64_t __anf_seen = 0;"
1433
+ io.puts "#{indent} ca_size_t __anf_mc = 0;"
1434
+ io.puts "#{indent} CA_SLAB_REDUCE_T_EX(#{si[:c]}, st, p, m, __anf_seen, 0, " \
1435
+ "__anf_seen |= (v == v), __anf_mc);"
1436
+ io.puts "#{indent} (void) __anf_mc;"
1437
+ io.puts "#{indent} if ( ! __anf_seen ) {"
1438
+ if mode == :nan
1439
+ pairs.each { |var, _| io.puts "#{indent} #{var} = (#{si[:c]}) NAN;" }
1440
+ else
1441
+ raise "#{k[:name]}: all_nan_result: :undef needs flag_var" unless flag_var
1442
+ io.puts "#{indent} #{flag_var} = 1;"
1443
+ end
1444
+ io.puts "#{indent} }"
1445
+ io.puts "#{indent}}"
1446
+ true
1447
+ end
1448
+
1296
1449
  def self.reduce_macro_suffix(k, src = nil)
1297
1450
  # CA_OBJECT cannot ride the SIMD-licensed macros (= _PLUS / _MIN / _MAX
1298
1451
  # / _STAR), which assume C operators (= acc is a VALUE, so
@@ -1325,6 +1478,24 @@ module MkKernel
1325
1478
  end
1326
1479
  end
1327
1480
 
1481
+ # Real component width of a complex data_type: cmplx64 -> f32,
1482
+ # cmplx128 -> f64. Raises for anything else, since :real_of_source
1483
+ # only has a meaning for a complex source.
1484
+ REAL_OF_CMPLX = { cmplx64: :f32, cmplx128: :f64 }.freeze
1485
+
1486
+ def self.resolve_output_dtype(kernel, src, dt)
1487
+ case dt
1488
+ when :preserve then DTYPES[src]
1489
+ when :real_of_source
1490
+ real = REAL_OF_CMPLX[src]
1491
+ raise "#{kernel[:name]}: output :real_of_source needs a complex src (got #{src})" \
1492
+ unless real
1493
+ DTYPES[real]
1494
+ else
1495
+ DTYPES[dt]
1496
+ end
1497
+ end
1498
+
1328
1499
  def self.output_info(kernel, src)
1329
1500
  out = kernel[:output]
1330
1501
  case out
@@ -1334,16 +1505,15 @@ module MkKernel
1334
1505
  next if family == :default
1335
1506
  if monop_expr_family_match?(family, src)
1336
1507
  # Hash value may itself be :preserve (= "same as source for this
1337
- # family") or a data_type symbol like :f64.
1338
- return (dt == :preserve) ? DTYPES[src] : DTYPES[dt]
1508
+ # family"), :real_of_source, or a data_type symbol like :f64.
1509
+ return resolve_output_dtype(kernel, src, dt)
1339
1510
  end
1340
1511
  end
1341
1512
  raise "#{kernel[:name]}: output Hash has no match for src #{src} and no :default" \
1342
1513
  unless out.key?(:default)
1343
- dt = out[:default]
1344
- (dt == :preserve) ? DTYPES[src] : DTYPES[dt]
1514
+ resolve_output_dtype(kernel, src, out[:default])
1345
1515
  else
1346
- DTYPES[out]
1516
+ resolve_output_dtype(kernel, src, out)
1347
1517
  end
1348
1518
  end
1349
1519
 
@@ -1699,7 +1869,7 @@ module MkKernel
1699
1869
 
1700
1870
  if streamable
1701
1871
  emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var, acc_init,
1702
- decls, reduce_stmt, finish_expr, extra_args)
1872
+ decls, reduce_stmt, finish_expr, extra_args, src)
1703
1873
  end
1704
1874
 
1705
1875
  # L.1 / L.3 / L.4 (PROPOSAL_REDUCTION_LOOP_INTERCHANGE):
@@ -1859,6 +2029,17 @@ module MkKernel
1859
2029
  emit_reduce_slab_call(io, k, src, si, oi, suffix, acc_var, acc_init,
1860
2030
  reduce_stmt, "masked_cnt", indent: " ")
1861
2031
  end
2032
+ all_nan_flag = nil
2033
+ if k[:all_nan_result]
2034
+ pairs = limit_init_state_vars(k, si, oi, src)
2035
+ if k[:all_nan_result] == :undef && FLOAT_DTYPES.include?(src)
2036
+ all_nan_flag = "__anf_all_nan"
2037
+ io.puts " int #{all_nan_flag} = 0;"
2038
+ end
2039
+ emit_all_nan_fixup(io, k, src, si, pairs,
2040
+ "masked_cnt < st.slab_elements",
2041
+ indent: " ", flag_var: all_nan_flag)
2042
+ end
1862
2043
  trigger = case k[:mask_policy]
1863
2044
  when :strict then "masked_cnt > 0"
1864
2045
  when :all_masked then "masked_cnt == st.slab_elements"
@@ -1880,6 +2061,7 @@ module MkKernel
1880
2061
  ": st.slab_elements - masked_cnt < min_count)"
1881
2062
  end
1882
2063
  end
2064
+ trigger = "(#{trigger}) || #{all_nan_flag}" if all_nan_flag
1883
2065
  finish_emit = view_flat \
1884
2066
  ? "(transform_active ? (outer_off + ((ca_size_t)(#{finish_expr})) * axis_vstride) : ((ca_size_t)(#{finish_expr})))" \
1885
2067
  : "(#{finish_expr})"
@@ -1962,13 +2144,15 @@ module MkKernel
1962
2144
  # the numeric argmin's best_i. mask_policy is always :min_count here.
1963
2145
  def self.emit_reduce_native_fixlen(io, k, src)
1964
2146
  name = k[:name]
2147
+ counting = k[:fixlen] == :count_equal
1965
2148
  want_max = %i[max argmax].include?(k[:fixlen])
1966
- index_out = %i[argmin argmax].include?(k[:fixlen])
2149
+ index_out = %i[argmin argmax].include?(k[:fixlen]) || counting
1967
2150
  cmp = want_max ? ">" : "<"
2151
+ varg = counting ? ", const char *value_arg" : ""
1968
2152
 
1969
2153
  io.puts
1970
2154
  io.puts "static VALUE"
1971
- io.puts "#{name}_ki_native_fixlen (VALUE self, CArray *ca, int8_t *slab_axes, int8_t naxes, int keep_axis, ca_size_t min_count)"
2155
+ io.puts "#{name}_ki_native_fixlen (VALUE self, CArray *ca, int8_t *slab_axes, int8_t naxes, int keep_axis#{varg}, ca_size_t min_count)"
1972
2156
  io.puts "{"
1973
2157
  io.puts " ca_size_t K = ca->bytes; /* uniform fixlen byte width */"
1974
2158
  if index_out
@@ -1989,9 +2173,13 @@ module MkKernel
1989
2173
  io.puts " ca_size_t out_i = 0;"
1990
2174
  io.puts " boolean8_t *op_mask = NULL; /* lazily allocated on first UNDEF */"
1991
2175
  io.puts " while ( ca_iter_state_next_slab_axes(&st, &p, &m) ) {"
1992
- io.puts " const char *best = NULL;"
1993
- io.puts " ca_size_t best_i = 0;"
1994
- io.puts " (void) best_i;" unless index_out # value output ignores the index
2176
+ if counting
2177
+ io.puts " int64_t cnt = 0;"
2178
+ else
2179
+ io.puts " const char *best = NULL;"
2180
+ io.puts " ca_size_t best_i = 0;"
2181
+ io.puts " (void) best_i;" unless index_out # value output ignores the index
2182
+ end
1995
2183
  io.puts " ca_size_t masked_cnt = 0;"
1996
2184
  io.puts " int8_t sndim = st.slab_ndim;"
1997
2185
  io.puts " ca_size_t sidx[CA_RANK_MAX] = { 0 };"
@@ -2007,12 +2195,18 @@ module MkKernel
2007
2195
  io.puts " }"
2008
2196
  io.puts " else {"
2009
2197
  io.puts " const char *q = (const char *) p + doff;"
2010
- io.puts " if ( best == NULL ) {"
2011
- io.puts " best = q; best_i = idx;"
2012
- io.puts " }"
2013
- io.puts " else if ( memcmp(q, best, (size_t) K) #{cmp} 0 ) {"
2014
- io.puts " best = q; best_i = idx;"
2015
- io.puts " }"
2198
+ if counting
2199
+ io.puts " if ( memcmp(q, value_arg, (size_t) K) == 0 ) {"
2200
+ io.puts " cnt++;"
2201
+ io.puts " }"
2202
+ else
2203
+ io.puts " if ( best == NULL ) {"
2204
+ io.puts " best = q; best_i = idx;"
2205
+ io.puts " }"
2206
+ io.puts " else if ( memcmp(q, best, (size_t) K) #{cmp} 0 ) {"
2207
+ io.puts " best = q; best_i = idx;"
2208
+ io.puts " }"
2209
+ end
2016
2210
  io.puts " }"
2017
2211
  io.puts " /* row-major odometer (innermost slab axis fastest) so idx"
2018
2212
  io.puts " matches CA_SLAB_REDUCE_T's flat slab index. */"
@@ -2021,8 +2215,16 @@ module MkKernel
2021
2215
  io.puts " sidx[sk] = 0;"
2022
2216
  io.puts " }"
2023
2217
  io.puts " }"
2024
- io.puts " if ( min_count < 0 ? masked_cnt == st.slab_elements"
2025
- io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2218
+ # ERI.0: a count over nothing is 0, not UNDEF -- so the default
2219
+ # (min_count < 0) never fires the mask for :count_equal. An explicit
2220
+ # min_count: still does.
2221
+ if counting
2222
+ io.puts " if ( min_count < 0 ? 0"
2223
+ io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2224
+ else
2225
+ io.puts " if ( min_count < 0 ? masked_cnt == st.slab_elements"
2226
+ io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2227
+ end
2026
2228
  io.puts " if ( ! op_mask ) {"
2027
2229
  io.puts " ca_create_mask(co);"
2028
2230
  io.puts " op_mask = (boolean8_t *) co->mask->ptr;"
@@ -2036,7 +2238,9 @@ module MkKernel
2036
2238
  io.puts " out_i++;"
2037
2239
  io.puts " }"
2038
2240
  io.puts " else {"
2039
- if index_out
2241
+ if counting
2242
+ io.puts " ((int64_t *) op)[out_i] = cnt;"
2243
+ elsif index_out
2040
2244
  io.puts " ((int64_t *) op)[out_i] = (int64_t) best_i;"
2041
2245
  else
2042
2246
  io.puts " memcpy(op + out_i * K, best, (size_t) K);"
@@ -2364,6 +2568,49 @@ module MkKernel
2364
2568
  buf_decls = plus_info.map do |pi|
2365
2569
  " #{pi[:c_type]} #{pi[:buf]}[512];\n"
2366
2570
  end.join
2571
+
2572
+ # all-NaN fix-up for the tiled core (see emit_all_nan_fixup for the
2573
+ # reduction counterpart). Each output cell of a tile has its own
2574
+ # accumulator, so the check is per column: a column whose accumulator
2575
+ # is still at the init either saw only NaN or only +/-INFINITY.
2576
+ #
2577
+ # The `_Pragma("omp simd")` j-loop is left alone -- carrying a
2578
+ # "saw a number" flag beside the accumulator would double its loads
2579
+ # and stores on every cell of every column. Instead the tile is
2580
+ # tested once after the M loop (tile_len comparisons against M *
2581
+ # tile_len already done), and only a tile that holds a suspicious
2582
+ # column re-reads its rows.
2583
+ li_nan_fixup =
2584
+ if k[:all_nan_result] == :nan && FLOAT_DTYPES.include?(src) &&
2585
+ plus_info.size == 1
2586
+ pi = plus_info.first
2587
+ seen_buf = "__li_buf_seen"
2588
+ lambda do |m_step|
2589
+ <<~C.rstrip
2590
+ {
2591
+ int __li_susp = 0;
2592
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2593
+ if ( #{pi[:buf]}[__j] == (#{pi[:c_type]}) (#{pi[:init]}) ) { __li_susp = 1; break; }
2594
+ }
2595
+ if ( __li_susp ) {
2596
+ int8_t #{seen_buf}[512];
2597
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) #{seen_buf}[__j] = 0;
2598
+ for ( ca_size_t __li_i = 0; __li_i < __li_M; __li_i++ ) {
2599
+ const #{si[:c]} *__li_row = __li_plane + __li_i * #{m_step} + __li_tile;
2600
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2601
+ #{seen_buf}[__j] |= (__li_row[__j] == __li_row[__j]);
2602
+ }
2603
+ }
2604
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2605
+ if ( ! #{seen_buf}[__j] ) #{pi[:buf]}[__j] = (#{pi[:c_type]}) NAN;
2606
+ }
2607
+ }
2608
+ }
2609
+ C
2610
+ end
2611
+ else
2612
+ lambda { |_m_step| "" }
2613
+ end
2367
2614
  init_loops = plus_info.map do |pi|
2368
2615
  " for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {\n" \
2369
2616
  " #{pi[:buf]}[__j] = (#{pi[:c_type]}) (#{pi[:init]});\n" \
@@ -2393,6 +2640,7 @@ module MkKernel
2393
2640
  (void) v;
2394
2641
  }
2395
2642
  }
2643
+ #{li_nan_fixup.call("__li_INNER")}
2396
2644
  for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2397
2645
  #{opv}[__li_o * __li_INNER + __li_tile + __j] = (#{oi[:c]}) (#{finish_li});
2398
2646
  }
@@ -2613,6 +2861,7 @@ module MkKernel
2613
2861
  (void) v;
2614
2862
  }
2615
2863
  }
2864
+ #{li_nan_fixup.call("__li_INNER")}
2616
2865
  for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2617
2866
  __li_op_k[__li_tile + __j] = (#{oi[:c]}) (#{finish_li});
2618
2867
  }
@@ -2643,8 +2892,8 @@ module MkKernel
2643
2892
  write stride 1 (= contig output tail) both preserved -> SIMD
2644
2893
  tile reduce + write maintained.
2645
2894
 
2646
- Performance characteristic (deliver-via-view, per the CLAUDE.md
2647
- "deliver the materials" principle): bench (b2 pattern, M=200, K=5, eff_INNER
2895
+ Performance characteristic (deliver-via-view -- the surface
2896
+ prioritises delivering the cells over avoiding a copy): bench (b2 pattern, M=200, K=5, eff_INNER
2648
2897
  =360, INNER_pre_K=16) yields 3674 us vs eager-entity 1545 us
2649
2898
  = 2.38x slow. Root cause is the multi-parent data layout
2650
2899
  (= 5 separate 9 MB regions instead of one contig 46 MB),
@@ -2771,6 +3020,7 @@ module MkKernel
2771
3020
  (void) v;
2772
3021
  }
2773
3022
  }
3023
+ #{li_nan_fixup.call("__li_M_stride")}
2774
3024
  for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2775
3025
  __li_op_k[__li_tile + __j] = (#{oi[:c]}) (#{finish_li});
2776
3026
  }
@@ -2800,8 +3050,8 @@ module MkKernel
2800
3050
  # reductions consume the same slab)
2801
3051
  # - no streaming / view_flat / array_arg / value_arg
2802
3052
  # - reduction_kind: :none only
2803
- # The if-form discipline (CLAUDE.md "write multi-reduction fused kernels
2804
- # in if-form") is enforced by author, not by the generator.
3053
+ # The if-form discipline for multi-reduction fused kernels (see
3054
+ # emit_min_max below) is enforced by author, not by the generator.
2805
3055
  def self.emit_reduce_native_multi(io, k, src)
2806
3056
  si = DTYPES[src]
2807
3057
  oi = output_info(k, src)
@@ -2862,6 +3112,10 @@ module MkKernel
2862
3112
  if min_count
2863
3113
  io.puts " ca_size_t masked_cnt = 0;"
2864
3114
  io.puts " CA_SLAB_REDUCE_T_EX(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt}, masked_cnt);"
3115
+ if k[:all_nan_result]
3116
+ emit_all_nan_fixup(io, k, src, si, limit_init_state_vars(k, si, oi, src),
3117
+ "masked_cnt < st.slab_elements", indent: " ")
3118
+ end
2865
3119
  # Same trigger as single-output :min_count: legacy default (all_masked)
2866
3120
  # when min_count < 0, otherwise need at least min_count valid cells.
2867
3121
  trigger = "(min_count < 0 ? masked_cnt == st.slab_elements " \
@@ -2937,7 +3191,7 @@ module MkKernel
2937
3191
  # - mask present: ca_has_mask(ca)
2938
3192
  def self.emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var,
2939
3193
  acc_init, decls, reduce_stmt,
2940
- finish_expr, extra_args)
3194
+ finish_expr, extra_args, src)
2941
3195
  name = k[:name]
2942
3196
  min_count = (k[:mask_policy] == :min_count)
2943
3197
  has_mp = !k[:mask_policy].nil?
@@ -2968,6 +3222,14 @@ module MkKernel
2968
3222
  # acc_var requires explicit init (= the macro normally does this).
2969
3223
  # Other state vars in decls already include `= init` per line.
2970
3224
  io.puts " #{acc_var} = (#{acc_init});"
3225
+ # all-NaN fix-up, streaming variant. The other two paths confirm a
3226
+ # suspicious answer by re-reading the data; here re-reading means
3227
+ # evaluating the lazy chain a second time, so the flag rides along in
3228
+ # the chunk loop instead. The loop already pays for producing each
3229
+ # cell, which is what makes one more compare affordable here and not
3230
+ # in the tiled core.
3231
+ all_nan_stream = k[:all_nan_result] == :nan && FLOAT_DTYPES.include?(src)
3232
+ io.puts " int64_t __anf_seen = 0;" if all_nan_stream
2971
3233
  if has_mp
2972
3234
  # Mask-policy reductions need masked_cnt to satisfy the macro/
2973
3235
  # finish_expr signature. On streaming we have no mask, so it's
@@ -2976,7 +3238,15 @@ module MkKernel
2976
3238
  io.puts " (void) masked_cnt;"
2977
3239
  end
2978
3240
  io.puts " ca_lazy_arena_enter();"
2979
- io.puts " #{si[:c]} *__chunk = (#{si[:c]} *) ca_lazy_arena_acquire(__chunk_elems * sizeof(#{si[:c]}));"
3241
+ if si[:c] == "VALUE"
3242
+ # Object lane: the chunk holds VALUEs pulled from the source, and
3243
+ # the fold below calls rb_funcall per cell. A collection there
3244
+ # would free cells a lazy source produced into the chunk, so the
3245
+ # slot has to stay marked for as long as it is held.
3246
+ io.puts " VALUE *__chunk = (VALUE *) ca_lazy_arena_acquire_object(__chunk_elems);"
3247
+ else
3248
+ io.puts " #{si[:c]} *__chunk = (#{si[:c]} *) ca_lazy_arena_acquire(__chunk_elems * sizeof(#{si[:c]}));"
3249
+ end
2980
3250
  io.puts " while ( __outer_off < __outer ) {"
2981
3251
  io.puts " ca_size_t __r = (__outer - __outer_off < __rows) ? (__outer - __outer_off) : __rows;"
2982
3252
  io.puts " ca_size_t __n = __r * __inner;"
@@ -2987,11 +3257,15 @@ module MkKernel
2987
3257
  io.puts " for ( __i = 0; __i < __n; __i++ ) {"
2988
3258
  io.puts " #{si[:c]} v = __chunk[__i];"
2989
3259
  io.puts " #{reduce_stmt};"
3260
+ io.puts " __anf_seen |= (v == v);" if all_nan_stream
2990
3261
  io.puts " }"
2991
3262
  io.puts " __outer_off += __r;"
2992
3263
  io.puts " }"
2993
3264
  io.puts " ca_lazy_arena_release(__chunk);"
2994
3265
  io.puts " ca_lazy_arena_exit();"
3266
+ if all_nan_stream
3267
+ io.puts " if ( ! __anf_seen && ca->elements > 0 ) #{acc_var} = (#{si[:c]}) NAN;"
3268
+ end
2995
3269
  if has_mp
2996
3270
  # Streaming path has no mask source, so masked_cnt is 0; min_count
2997
3271
  # / strict / all_masked triggers all evaluate to false except
@@ -3235,7 +3509,17 @@ module MkKernel
3235
3509
  k[:source].each do |s|
3236
3510
  si = DTYPES[s]
3237
3511
  # Per-src value_arg cast: NUM2LL / NUM2ULL / NUM2DBL -> (T_IN).
3238
- varg_decl = has_varg ? " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n" : ""
3512
+ varg_decl = if !has_varg
3513
+ ""
3514
+ elsif s == :fixlen
3515
+ # A fixlen query is a runtime-width byte blob with no
3516
+ # scalar cast; pack it the way the search family does,
3517
+ # which NUL-pads a short String to the cell width.
3518
+ " char *value_arg = ALLOCA_N(char, src->bytes);\n" \
3519
+ " rb_ca_obj2ptr(self, rval, value_arg);\n"
3520
+ else
3521
+ " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n"
3522
+ end
3239
3523
  if use_result_var || has_varg
3240
3524
  io.puts " case #{si[:ca]}: {"
3241
3525
  io.print varg_decl unless varg_decl.empty?
@@ -3794,7 +4078,7 @@ module MkKernel
3794
4078
  # `masked_last` parameter. Sets `sort_lo` / `sort_n` to the unmasked
3795
4079
  # sub-range so the downstream sort/quickselect call only ever compares
3796
4080
  # unmasked pairs -- masked cells are an incomparable sentinel, the same
3797
- # role NaN plays for nan_policy: :end, but dtype-agnostic and runtime-
4081
+ # role NaN plays for nan_policy: :end, but type-agnostic and runtime-
3798
4082
  # selectable. `payload_expr(k)` computes the `.i` payload (fiber-local
3799
4083
  # index for :fiber_local semantics, view-flat address for :view_flat)
3800
4084
  # given the Ruby string `k` naming the C loop variable.
@@ -4829,7 +5113,7 @@ module MkKernel
4829
5113
  {
4830
5114
  /* rev4 A1 via single-element CArray: convert CScalar / [1] 1-D /
4831
5115
  all dim==1 etc. to a Ruby Float and route it through the Case A
4832
- scalar path. The dtype matches ca (= rb_ca_obj2ptr coerces it
5116
+ scalar path. The data type matches ca (= rb_ca_obj2ptr coerces it
4833
5117
  downstream). */
4834
5118
  if ( rb_obj_is_carray(rval) ) {
4835
5119
  CArray *cv_pre_;
@@ -5250,7 +5534,7 @@ module MkKernel
5250
5534
  io.puts " }"
5251
5535
  io.puts " if ( self_face_comparable ) {"
5252
5536
  io.puts " if ( rval_is_face ) {"
5253
- io.puts " rval = rb_ca_strip_face_value(rval);"
5537
+ io.puts %Q[ rval = ca_face_operand_descend(rval, "#{name}_ki");]
5254
5538
  io.puts " }"
5255
5539
  io.puts " } else if ( self_was_face ) {"
5256
5540
  io.puts " if ( rb_respond_to(self_ref, rb_intern(\"to_comparable\")) ) {"
@@ -6055,6 +6339,90 @@ module MkKernel
6055
6339
  # The aggregator init.c, where Init_carray_kernels() calls each per-tag
6056
6340
  # Init_<suffix>() in file_tags order. No tag depends on another: an alias
6057
6341
  # already sits after its target within its own file.
6342
+ # ---------------------------------------------------------------------
6343
+ # The kernel bodies, as text, for a caller that has to compute the same
6344
+ # thing somewhere other than in these kernels -- a compiler handed an
6345
+ # expression tree, which must produce what the eager kernel produces.
6346
+ # Emitting the table here rather than reading this generator at runtime
6347
+ # keeps a build-time tool out of the running process.
6348
+ #
6349
+ # The bodies keep their `#1` / `#2` / `#3` and `<type>` placeholders: the
6350
+ # caller substitutes its own operands. Object-lane bodies are left out,
6351
+ # since they call back into the interpreter and cannot be compiled apart
6352
+ # from it.
6353
+ # ---------------------------------------------------------------------
6354
+
6355
+ BODY_TABLE_DTYPES = %i[i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6356
+ bool cmplx64 cmplx128].freeze
6357
+
6358
+ def self.body_table_rows
6359
+ rows = []
6360
+ KERNELS.each do |k|
6361
+ next unless %i[monop binop triop].include?(k[:kind])
6362
+ BODY_TABLE_DTYPES.each do |src|
6363
+ body = monop_expr_for(k, src)
6364
+ next unless body.is_a?(String)
6365
+ next if body.include?("rb_funcall")
6366
+ rows << [k[:kind].to_s, k[:name].to_s,
6367
+ DTYPES[src][:ca].sub(/\ACA_/, "").downcase, body]
6368
+ end
6369
+ end
6370
+ rows
6371
+ end
6372
+
6373
+ def self.c_string_literal(text)
6374
+ '"' + text.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", '\\n') + '"'
6375
+ end
6376
+
6377
+ def self.emit_kernel_bodies(io)
6378
+ rows = body_table_rows
6379
+ io.puts
6380
+ io.puts "/* The text of every kernel body, for a caller that compiles the same"
6381
+ io.puts " operation elsewhere. Placeholders are left in place. */"
6382
+ io.puts
6383
+ io.puts "typedef struct {"
6384
+ io.puts " const char *kind;"
6385
+ io.puts " const char *name;"
6386
+ io.puts " const char *data_type;"
6387
+ io.puts " const char *body;"
6388
+ io.puts "} ca_kernel_body_t;"
6389
+ io.puts
6390
+ io.puts "static const ca_kernel_body_t ca_kernel_bodies[] = {"
6391
+ rows.each do |kind, name, dtype, body|
6392
+ io.puts " { #{c_string_literal(kind)}, #{c_string_literal(name)}, " \
6393
+ "#{c_string_literal(dtype)},"
6394
+ io.puts " #{c_string_literal(body)} },"
6395
+ end
6396
+ io.puts "};"
6397
+ io.puts
6398
+ io.puts "static const int ca_kernel_bodies_count = #{rows.size};"
6399
+ io.puts
6400
+ io.puts <<~C
6401
+ /* CArray.__kernel_body__(kind, name, data_type) -> String, or nil where
6402
+ this operation has no body at that data type. */
6403
+ static VALUE
6404
+ rb_ca_s_kernel_body (VALUE klass, VALUE rkind, VALUE rname, VALUE rtype)
6405
+ {
6406
+ VALUE kind = rb_obj_as_string(rkind);
6407
+ VALUE name = rb_obj_as_string(rname);
6408
+ VALUE type = rb_obj_as_string(rtype);
6409
+ const char *k = StringValueCStr(kind);
6410
+ const char *n = StringValueCStr(name);
6411
+ const char *t = StringValueCStr(type);
6412
+ int i;
6413
+ for ( i = 0; i < ca_kernel_bodies_count; i++ ) {
6414
+ const ca_kernel_body_t *e = &ca_kernel_bodies[i];
6415
+ if ( strcmp(e->kind, k) == 0 &&
6416
+ strcmp(e->name, n) == 0 &&
6417
+ strcmp(e->data_type, t) == 0 ) {
6418
+ return rb_str_new_cstr(e->body);
6419
+ }
6420
+ }
6421
+ return Qnil;
6422
+ }
6423
+ C
6424
+ end
6425
+
6058
6426
  def self.emit_aggregator_init(io, tags)
6059
6427
  io.puts "/* GENERATED aggregator: dispatches to per-tag Init_carray_kernels_<tag>() */"
6060
6428
  io.puts "#include \"carray.h\""
@@ -6063,23 +6431,30 @@ module MkKernel
6063
6431
  io.puts "void Init_carray_kernels_#{file_suffix(kind, sub)} (void);"
6064
6432
  end
6065
6433
  io.puts
6434
+ emit_kernel_bodies(io)
6435
+ io.puts
6066
6436
  io.puts "void"
6067
6437
  io.puts "Init_carray_kernels (void)"
6068
6438
  io.puts "{"
6069
6439
  tags.each do |kind, sub|
6070
6440
  io.puts " Init_carray_kernels_#{file_suffix(kind, sub)}();"
6071
6441
  end
6442
+ io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6443
+ io.puts " rb_ca_s_kernel_body, 3);"
6072
6444
  io.puts "}"
6073
6445
  end
6074
6446
 
6075
6447
  # Single-stream Init_carray_kernels(): every kind emitted in order inside
6076
6448
  # one function.
6077
6449
  def self.emit_init(io)
6450
+ emit_kernel_bodies(io)
6078
6451
  io.puts
6079
6452
  io.puts "void"
6080
6453
  io.puts "Init_carray_kernels (void)"
6081
6454
  io.puts "{"
6082
6455
  KERNELS.each { |k| emit_init_line(io, k) }
6456
+ io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6457
+ io.puts " rb_ca_s_kernel_body, 3);"
6083
6458
  io.puts "}"
6084
6459
  end
6085
6460
 
@@ -6155,7 +6530,7 @@ module MkKernel
6155
6530
  # bench A/B comparison during Phase E (E.1-E.5); after E.7 they
6156
6531
  # are retired. Maps and scans keep their `_ki` names because
6157
6532
  # they have no user-facing equivalent yet (cumsum etc. pending
6158
- # rewire per CLAUDE.md "methods awaiting reimplementation").
6533
+ # rewire).
6159
6534
  #
6160
6535
  # SO.2 rev6 (2026-06-04): sort kernels can opt out of the _ki
6161
6536
  # binding via bind_ruby: false (= internal-only kernels consumed
@@ -6269,6 +6644,7 @@ MkKernel.reduce :min,
6269
6644
  bool: "acc = ((uint64_t) v < acc) ? (uint64_t) v : acc",
6270
6645
  object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v;' },
6271
6646
  reduction_kind: :min, # SL.1.2
6647
+ all_nan_result: :nan,
6272
6648
  # CA_FIXLEN: memcmp lexicographic min (byte order == the fixlen sort
6273
6649
  # order); the numeric reduce/init above are unused for fixlen (bespoke
6274
6650
  # slab walk, see the fixlen: option in MkKernel.reduce).
@@ -6293,6 +6669,7 @@ MkKernel.reduce :max,
6293
6669
  bool: "acc = ((uint64_t) v > acc) ? (uint64_t) v : acc",
6294
6670
  object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v;' },
6295
6671
  reduction_kind: :max, # SL.1.2
6672
+ all_nan_result: :nan,
6296
6673
  # CA_FIXLEN: memcmp lexicographic max (byte order == the fixlen sort order).
6297
6674
  fixlen: :max,
6298
6675
  source: MkKernel::ALL_NUMERIC + [:bool, :object, :fixlen],
@@ -6500,11 +6877,11 @@ MkKernel.reduce :stddev,
6500
6877
  # -fopenmp-simd). The 2x f64 speedup survives SL.1.2's reduction(min/max:)
6501
6878
  # vectorizer reject because fminnm + fmaxnm dual-issue on M2's two FP pipes.
6502
6879
  #
6503
- # if-form discipline (CLAUDE.md "write multi-reduction fused kernels in if-form"):
6880
+ # if-form discipline for multi-reduction fused kernels:
6504
6881
  # the body uses `if (v < lo) lo = v;` etc, NOT ternary `lo = (v < lo) ? v : lo;`.
6505
6882
  # DO NOT change to ternary — pragma-less ILP path depends on if-form.
6506
6883
  #
6507
- # FM.1.0 scope: mask not yet propagated (FM.1.5). Numeric dtypes only.
6884
+ # FM.1.0 scope: mask not yet propagated (FM.1.5). Numeric data types only.
6508
6885
 
6509
6886
  MkKernel.reduce :minmax,
6510
6887
  state: { lo: :load_type, hi: :load_type },
@@ -6518,6 +6895,7 @@ MkKernel.reduce :minmax,
6518
6895
  # together on init, get set together on first reduce).
6519
6896
  object: 'if (lo == Qundef) { lo = v; hi = v; } else { if (RTEST(rb_funcall(v, rb_intern("<"), 1, lo))) lo = v; if (RTEST(rb_funcall(v, rb_intern(">"), 1, hi))) hi = v; }' },
6520
6897
  outputs: 2,
6898
+ all_nan_result: :nan,
6521
6899
  finish: { min: "lo", max: "hi" },
6522
6900
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
6523
6901
  # bool: u64 (Integer 0/1) so minmax returns [0/1, 0/1], not
@@ -6541,9 +6919,9 @@ MkKernel.reduce :minmax,
6541
6919
  # actual name (`best_v` here) through to the macro -- the macro accepts
6542
6920
  # any identifier as its accumulator argument.
6543
6921
  #
6544
- # Exposed as Ruby `min_index` / `max_index` (= naming convention
6545
- # "methods returning a position use *_index", see CLAUDE.md "methods
6546
- # awaiting reimplementation" table). These
6922
+ # Exposed as Ruby `min_index` / `max_index` (= the naming rule that a
6923
+ # method returning a position uses the `_index` suffix; see the rules
6924
+ # table in guides/devel/00_glossary.md). These
6547
6925
  # replace the legacy `min_addr` / `max_addr` retired in E.7 stat_proc
6548
6926
  # retire (commit f5c7ecd). 3.0 breaking: name change from `*_addr` to
6549
6927
  # `*_index` is intentional.
@@ -6566,6 +6944,7 @@ MkKernel.reduce :argmin,
6566
6944
  # An ORDERABLE Face descends to its numeric storage (position output needs
6567
6945
  # no re-lift; the axis-local index is identical for Face and storage).
6568
6946
  face_gate: :strip,
6947
+ all_nan_result: :undef,
6569
6948
  public_method: :min_index
6570
6949
 
6571
6950
  MkKernel.reduce :argmax,
@@ -6584,6 +6963,7 @@ MkKernel.reduce :argmax,
6584
6963
  fallback: :raise,
6585
6964
  mask_policy: :min_count,
6586
6965
  face_gate: :strip,
6966
+ all_nan_result: :undef,
6587
6967
  public_method: :max_index
6588
6968
 
6589
6969
  # ---- argmin_addr / argmax_addr (view-flat address variants) ---------
@@ -6595,10 +6975,10 @@ MkKernel.reduce :argmax,
6595
6975
  # axis-local-to-flat-addr round-trip).
6596
6976
  #
6597
6977
  # Paired with the sort family's sort_addr(axis:) (= already public)
6598
- # and partition family's partition_addr_ki (= internal). Per
6599
- # CLAUDE.md "«`_addr` is OK to expose: a per-axis primitive that returns a real flat address»"
6600
- # (= "dual API: _index for axis-local position, _addr for view-flat
6601
- # address").
6978
+ # and partition family's partition_addr_ki (= internal). `_addr`
6979
+ # is OK to expose for a per-axis primitive that returns a real flat
6980
+ # address: the dual API is `_index` for an axis-local position, `_addr`
6981
+ # for a view-flat one.
6602
6982
  #
6603
6983
  # Runtime constraints:
6604
6984
  # - naxes == 1 (single-axis reduce): view-flat transform applied
@@ -6627,6 +7007,7 @@ MkKernel.reduce :argmin_addr,
6627
7007
  mask_policy: :min_count,
6628
7008
  semantics: :view_flat,
6629
7009
  face_gate: :strip,
7010
+ all_nan_result: :undef,
6630
7011
  public_method: :min_addr
6631
7012
 
6632
7013
  MkKernel.reduce :argmax_addr,
@@ -6644,6 +7025,7 @@ MkKernel.reduce :argmax_addr,
6644
7025
  mask_policy: :min_count,
6645
7026
  semantics: :view_flat,
6646
7027
  face_gate: :strip,
7028
+ all_nan_result: :undef,
6647
7029
  public_method: :max_addr
6648
7030
 
6649
7031
  # ---- mask_policy demos ------------------------------------------------
@@ -6771,9 +7153,13 @@ MkKernel.reduce :count_false,
6771
7153
 
6772
7154
  MkKernel.reduce :count_equal,
6773
7155
  init: "0",
6774
- reduce: "if (v == value_arg) acc += 1",
7156
+ # The object lane compares with rb_equal (= Ruby ==), not the C ==, which
7157
+ # on two VALUEs would ask whether they are the same object.
7158
+ reduce: { numeric: "if (v == value_arg) acc += 1",
7159
+ object: "if (RTEST(rb_equal(v, value_arg))) acc += 1" },
6775
7160
  reduction_kind: :plus, # SL.1.4 (conditional predication; clang predicates safely under reduction(+:acc))
6776
- source: MkKernel::ALL_NUMERIC,
7161
+ source: MkKernel::ALL_NUMERIC + [:object, :fixlen],
7162
+ fixlen: :count_equal, # bespoke memcmp walk (a blob has no scalar C type)
6777
7163
  output: :i64,
6778
7164
  ruby_scalar: :LL2NUM,
6779
7165
  fallback: :raise,
@@ -6806,7 +7192,8 @@ MkKernel.reduce :count_equal,
6806
7192
  # paths removed (= ALL_NUMERIC + :raise fallback). Re-add via demand-
6807
7193
  # driven complex specialization or CA_OBJECT bridge phase.
6808
7194
  # - **per-axis support gained**: `a.wsum(w, 0)`, `a.wsum(w, 0, 1)`, etc.
6809
- # (= original "open per-axis" goal, CLAUDE.md per-axis-for-all principle).
7195
+ # (= the original "open per-axis" goal: every reduction gains an
7196
+ # axis form).
6810
7197
  #
6811
7198
  # public_method: true rebinds `wsum` from legacy rb_ca_wsum to
6812
7199
  # rb_ca_wsum_ki at Init time.
@@ -6965,22 +7352,38 @@ MkKernel.scan :cumprod,
6965
7352
  # init Qnil is the "no running extremum yet" sentinel, also never leaked
6966
7353
  # (unseen cells are masked). First unmasked cell adopts v as acc;
6967
7354
  # subsequent unmasked cells compare via rb_funcall(:>) / rb_funcall(:<).
7355
+ # The float lane starts at NaN and folds with C99 fmin / fmax, whose
7356
+ # rule is exactly the one wanted: a lone number beats NaN, two NaNs give
7357
+ # NaN. So a prefix that has seen only NaN answers NaN instead of leaking
7358
+ # +/-INFINITY -- the running form of the rule min / max follow
7359
+ # (all_nan_result:). fmin / fmax are exact on a float at either width,
7360
+ # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7361
+ # and keep the limit init and the plain compare.
6968
7362
  MkKernel.scan :cummax,
6969
7363
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
6970
7364
  output: { bool: :u64, default: :preserve },
6971
- init: { numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qnil" },
6972
- step: { numeric: "if (v > acc) acc = v; r = acc",
7365
+ init: { float: "NAN", numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qnil" },
7366
+ step: { float: "acc = fmax(acc, v); r = acc",
7367
+ numeric: "if (v > acc) acc = v; r = acc",
6973
7368
  bool: "if ((uint64_t) v > acc) acc = v; r = acc",
6974
7369
  object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v; r = acc' },
6975
7370
  fallback: :raise,
6976
7371
  axis_default: :flatten,
6977
7372
  empty: :undef
6978
7373
 
7374
+ # The float lane starts at NaN and folds with C99 fmin / fmax, whose
7375
+ # rule is exactly the one wanted: a lone number beats NaN, two NaNs give
7376
+ # NaN. So a prefix that has seen only NaN answers NaN instead of leaking
7377
+ # +/-INFINITY -- the running form of the rule min / max follow
7378
+ # (all_nan_result:). fmin / fmax are exact on a float at either width,
7379
+ # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7380
+ # and keep the limit init and the plain compare.
6979
7381
  MkKernel.scan :cummin,
6980
7382
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
6981
7383
  output: { bool: :u64, default: :preserve },
6982
- init: { numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qnil" },
6983
- step: { numeric: "if (v < acc) acc = v; r = acc",
7384
+ init: { float: "NAN", numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qnil" },
7385
+ step: { float: "acc = fmin(acc, v); r = acc",
7386
+ numeric: "if (v < acc) acc = v; r = acc",
6984
7387
  bool: "if ((uint64_t) v < acc) acc = v; r = acc",
6985
7388
  object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v; r = acc' },
6986
7389
  fallback: :raise,
@@ -7015,7 +7418,7 @@ MkKernel.scan :cumcount,
7015
7418
  # acc_type: :load_type -> acc carries T_LOAD (= last seen input value).
7016
7419
  # STEP additionally sees `first` (int) marking the first live cell of
7017
7420
  # the fiber. Output data_type = :bool. No production consumer remains:
7018
- # every dtype's mask_duplicates now uses the O(distinct) seen-set hash
7421
+ # every data type's mask_duplicates now uses the O(distinct) seen-set hash
7019
7422
  # lane (__mask_duplicates__, with boolean riding its uint8 lane), which
7020
7423
  # has no sort buffers. The numeric widths are kept as a standalone scan
7021
7424
  # kernel (a sort-path reference oracle in the mask_duplicates tests).
@@ -7368,12 +7771,32 @@ MkKernel.search :search_nearest,
7368
7771
  object: <<~C,
7369
7772
  /* CA_OBJECT nearest: minimum of query_val.distance(cell), compared
7370
7773
  with `<` (matches the legacy flat proc_nearest_addr_VALUE). */
7774
+ /* Nearest needs a metric. #distance is the protocol the 2.0 flat
7775
+ kernel used, back when Numeric#distance was a monkey patch; it
7776
+ is an opt-in refinement now, and a refinement does not reach an
7777
+ rb_funcall from C -- so a number reaching here answers no more
7778
+ than a String does. Measure a number the way #distance itself
7779
+ does, keep #distance for anything that defines a real one, and
7780
+ refuse the rest by name rather than let a bare NoMethodError out
7781
+ of the loop below. */
7782
+ ID nearest_id = rb_intern("distance");
7783
+ int nearest_by_distance = rb_respond_to(query_val, nearest_id);
7784
+ if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
7785
+ rb_raise(rb_eCADataTypeError,
7786
+ "search_nearest: nearest needs a distance, and %s is neither a "
7787
+ "number nor answers #distance (define one on the stored "
7788
+ "objects, or use search / bsearch for an exact match)",
7789
+ rb_obj_classname(query_val));
7790
+ }
7371
7791
  result = (ca_size_t) -1;
7372
7792
  VALUE best = Qnil;
7373
7793
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7374
7794
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7375
7795
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7376
- VALUE dist = rb_funcall(query_val, rb_intern("distance"), 1, v);
7796
+ VALUE dist = nearest_by_distance
7797
+ ? rb_funcall(query_val, nearest_id, 1, v)
7798
+ : rb_funcall(rb_funcall(query_val, '-', 1, v),
7799
+ rb_intern("abs"), 0);
7377
7800
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
7378
7801
  best = dist; result = i;
7379
7802
  }
@@ -7392,8 +7815,8 @@ MkKernel.search :search_nearest,
7392
7815
  # (= axis-local position) is transformed to a view-flat (row-major) address
7393
7816
  # into self by the dispatcher. Paired with the *_index family
7394
7817
  # (= bsearch / search / search_nearest already returning axis-local
7395
- # positions per the dual `_index` / `_addr` API in CLAUDE.md "`_addr` is
7396
- # OK to expose: a per-axis primitive that returns a real flat address").
7818
+ # positions per the dual `_index` / `_addr` API: `_index` for an
7819
+ # axis-local position, `_addr` for a view-flat one).
7397
7820
  #
7398
7821
  # Scope: scalar val path only (= case A). CArray val + broadcast
7399
7822
  # path (case B/C) raises NotImpError until extended (= the per-element
@@ -7549,12 +7972,32 @@ MkKernel.search :search_nearest_addr,
7549
7972
  object: <<~C,
7550
7973
  /* CA_OBJECT nearest (view_flat addr): minimum of
7551
7974
  query_val.distance(cell), compared with `<`. */
7975
+ /* Nearest needs a metric. #distance is the protocol the 2.0 flat
7976
+ kernel used, back when Numeric#distance was a monkey patch; it
7977
+ is an opt-in refinement now, and a refinement does not reach an
7978
+ rb_funcall from C -- so a number reaching here answers no more
7979
+ than a String does. Measure a number the way #distance itself
7980
+ does, keep #distance for anything that defines a real one, and
7981
+ refuse the rest by name rather than let a bare NoMethodError out
7982
+ of the loop below. */
7983
+ ID nearest_id = rb_intern("distance");
7984
+ int nearest_by_distance = rb_respond_to(query_val, nearest_id);
7985
+ if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
7986
+ rb_raise(rb_eCADataTypeError,
7987
+ "search_nearest_addr: nearest needs a distance, and %s is neither a "
7988
+ "number nor answers #distance (define one on the stored "
7989
+ "objects, or use search / bsearch for an exact match)",
7990
+ rb_obj_classname(query_val));
7991
+ }
7552
7992
  result = (ca_size_t) -1;
7553
7993
  VALUE best = Qnil;
7554
7994
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7555
7995
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7556
7996
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7557
- VALUE dist = rb_funcall(query_val, rb_intern("distance"), 1, v);
7997
+ VALUE dist = nearest_by_distance
7998
+ ? rb_funcall(query_val, nearest_id, 1, v)
7999
+ : rb_funcall(rb_funcall(query_val, '-', 1, v),
8000
+ rb_intern("abs"), 0);
7558
8001
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
7559
8002
  best = dist; result = i;
7560
8003
  }
@@ -7805,13 +8248,16 @@ MkKernel.monop :abs_i,
7805
8248
  MkKernel::SINT64_DTYPES => "(#2) = llabs(#1);",
7806
8249
  MkKernel::UINT_DTYPES => "(#2) = (#1);",
7807
8250
  MkKernel::FLOAT_DTYPES => "(#2) = fabs((float64_t)#1);",
7808
- MkKernel::CMPLX_DTYPES => "(#2) = cabs((cmplx128_t)#1);",
8251
+ [:cmplx64] => "(#2) = cabsf(#1);",
8252
+ [:cmplx128] => "(#2) = cabs(#1);",
7809
8253
  [:object] => '(#2) = rb_funcall((#1), rb_intern("abs"), 0);',
7810
8254
  }
7811
8255
 
7812
8256
  # abs: data_type-changing monop (the framework-piece test customer for monop
7813
8257
  # Hash output form). numeric input -> preserve data_type (= int/float abs),
7814
- # complex input -> f64 output (= magnitude is real). Replaces the hand-
8258
+ # complex input -> the real component width of that complex data_type
8259
+ # (= magnitude is real, and a cmplx64 magnitude is a float32 just as
8260
+ # `.real` and `.imag` are). Replaces the hand-
7815
8261
  # written rb_ca_abs / rb_ca_abs_bang in ext/carray_math.c. Object data_type
7816
8262
  # kept on abs_i (= bind: false on object would need a different output
7817
8263
  # rule; deferred). The abs_i monop above remains the primary kernel for
@@ -7820,13 +8266,15 @@ MkKernel.monop :abs_i,
7820
8266
  # returns the real-valued magnitude entity.
7821
8267
  MkKernel.monop :abs,
7822
8268
  source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7823
- output: { numeric: :preserve, complex: :f64 },
8269
+ output: { numeric: :preserve, complex: :real_of_source },
7824
8270
  expr: {
7825
8271
  MkKernel::SINT_SMALL_DTYPES => "(#2) = abs(#1);",
7826
8272
  MkKernel::SINT64_DTYPES => "(#2) = llabs(#1);",
7827
8273
  MkKernel::UINT_DTYPES => "(#2) = (#1);",
7828
8274
  MkKernel::FLOAT_DTYPES => "(#2) = fabs(#1);",
7829
- MkKernel::CMPLX_DTYPES => "(#2) = cabs(#1);", # complex -> double (real magnitude)
8275
+ # complex -> the real magnitude, at that complex type's own width
8276
+ [:cmplx64] => "(#2) = cabsf(#1);",
8277
+ [:cmplx128] => "(#2) = cabs(#1);",
7830
8278
  }
7831
8279
 
7832
8280
  # abs2: squared magnitude. For real x this is x*x (identical to :square
@@ -7837,22 +8285,22 @@ MkKernel.monop :abs,
7837
8285
  # optics, signal processing) do not pay for a sqrt they immediately
7838
8286
  # square away.
7839
8287
  #
7840
- # Output data_type follows :abs: numeric preserved, complex -> f64.
8288
+ # Output data_type follows :abs: numeric preserved, complex demoted to
8289
+ # its own real component width.
7841
8290
  MkKernel.monop :abs2,
7842
8291
  source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7843
- output: { numeric: :preserve, complex: :f64 },
8292
+ output: { numeric: :preserve, complex: :real_of_source },
7844
8293
  expr: {
7845
8294
  numeric: "(#2) = (#1) * (#1);",
7846
- complex: "{ double _r = creal(#1); double _i = cimag(#1); (#2) = _r * _r + _i * _i; }",
7847
- }
8295
+ }.merge(MkKernel.cmplx_widths(
8296
+ "{ <t> _r = creal<f>(#1); <t> _i = cimag<f>(#1); (#2) = _r * _r + _i * _i; }"))
7848
8297
 
7849
8298
  MkKernel.monop :conj,
7850
8299
  source: MkKernel::MATH_NUMERIC + [:object],
7851
8300
  expr: {
7852
8301
  numeric: "(#2) = (#1);",
7853
- complex: "(#2) = conj(#1);",
7854
8302
  object: '(#2) = rb_funcall((#1), rb_intern("conj"), 0);',
7855
- }
8303
+ }.merge(MkKernel.cmplx_widths("(#2) = conj<f>(#1);"))
7856
8304
 
7857
8305
  # arg: data_type-changing monop — phase angle of the complex plane.
7858
8306
  # Mathematically `arg(z)` for z = re + im*i is `atan2(im, re)` in
@@ -7863,11 +8311,11 @@ MkKernel.monop :conj,
7863
8311
  # Replaces the hand-written rb_ca_arg in ext/carray_numeric.c (which
7864
8312
  # was f64-only, float-or-complex parent, also computed `carg`).
7865
8313
  #
7866
- # Output data_type is always CA_FLOAT64.
7867
- # We do NOT use the abs Hash pattern `{numeric: :preserve}` because
7868
- # pi does not fit any integer slot — preserving int data_type would
7869
- # silently truncate `arg(-1) = pi` to 3. Float32 input also returns
7870
- # f64 since carg itself returns double.
8314
+ # Output data_type keeps the width the input carries its real values in:
8315
+ # a float stays that float, a complex demotes to its own real component
8316
+ # width (cmplx64 -> f32, cmplx128 -> f64). Integers are the exception
8317
+ # and go to f64, because pi does not fit any integer slot — preserving
8318
+ # the int data_type would silently truncate `arg(-1) = pi` to 3.
7871
8319
  #
7872
8320
  # 3.0 breaking (vs hand-written rb_ca_arg):
7873
8321
  # - integer input is now accepted (was a raise). Returns f64
@@ -7897,16 +8345,15 @@ MkKernel.monop :sign,
7897
8345
  MkKernel::UINT_DTYPES => "(#2) = ((#1) > 0) ? 1 : 0;",
7898
8346
  MkKernel::SINT_DTYPES => "(#2) = ((#1) > 0) - ((#1) < 0);",
7899
8347
  MkKernel::FLOAT_DTYPES => "(#2) = isnan(#1) ? (#1) : (((#1) > 0) - ((#1) < 0));",
7900
- MkKernel::CMPLX_DTYPES => "{ double _m = cabs(#1); (#2) = (_m == 0.0) ? 0 : ((#1) / _m); }",
7901
- }
8348
+ }.merge(MkKernel.cmplx_widths(
8349
+ "{ <t> _m = cabs<f>(#1); (#2) = (_m == 0) ? 0 : ((#1) / _m); }"))
7902
8350
 
7903
8351
  MkKernel.monop :arg,
7904
8352
  source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7905
- output: { numeric: :f64, complex: :f64 },
8353
+ output: { int: :f64, float: :preserve, complex: :real_of_source },
7906
8354
  expr: {
7907
8355
  numeric: "(#2) = carg((cmplx128_t)(#1));",
7908
- complex: "(#2) = carg(#1);",
7909
- }
8356
+ }.merge(MkKernel.cmplx_widths("(#2) = carg<f>(#1);"))
7910
8357
 
7911
8358
  # imag_i: data_type-preserving kernel that places the imag part in the
7912
8359
  # slot (= cimag for complex stores into the real component since cmplx
@@ -7920,9 +8367,8 @@ MkKernel.monop :imag_i,
7920
8367
  source: MkKernel::MATH_NUMERIC + [:object],
7921
8368
  expr: {
7922
8369
  numeric: "(#2) = 0;",
7923
- complex: "(#2) = cimag(#1);",
7924
8370
  object: '(#2) = rb_funcall((#1), rb_intern("imaginary"), 0);',
7925
- }
8371
+ }.merge(MkKernel.cmplx_widths("(#2) = cimag<f>(#1);"))
7926
8372
 
7927
8373
  # arg_i: data_type-preserving kernel that writes the complex argument
7928
8374
  # (phase angle) into the slot. For complex input, cassignment from a
@@ -7937,9 +8383,9 @@ MkKernel.monop :imag_i,
7937
8383
  MkKernel.monop :arg_i,
7938
8384
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES,
7939
8385
  expr: {
7940
- [:f32] => "(#2) = (float)carg((cmplx128_t)(#1));",
8386
+ [:f32] => "(#2) = cargf((cmplx64_t)(#1));",
7941
8387
  [:f64] => "(#2) = carg((cmplx128_t)(#1));",
7942
- [:cmplx64] => "(#2) = (float)carg((cmplx128_t)(#1));",
8388
+ [:cmplx64] => "(#2) = cargf(#1);",
7943
8389
  [:cmplx128] => "(#2) = carg(#1);",
7944
8390
  }
7945
8391
 
@@ -7995,7 +8441,8 @@ MkKernel.monfunc :rcp,
7995
8441
  expr: {
7996
8442
  int: "if ((#1)==0) {ca_zerodiv();}; (#2) = 1/(#1);",
7997
8443
  float: "(#2) = 1/(#1);",
7998
- complex: "(#2) = 1/(#1);",
8444
+ [:cmplx64] => "(#2) = op_crcp_cmplx64(#1);",
8445
+ [:cmplx128] => "(#2) = 1/(#1);",
7999
8446
  object: '(#2) = rb_funcall(INT2NUM(1), rb_intern("/"), 1, (#1));',
8000
8447
  }
8001
8448
 
@@ -8004,7 +8451,6 @@ MkKernel.monfunc :rcp,
8004
8451
  {
8005
8452
  sqrt: "sqrt",
8006
8453
  exp: "exp",
8007
- log: "log",
8008
8454
  sin: "sin",
8009
8455
  cos: "cos",
8010
8456
  tan: "tan",
@@ -8015,21 +8461,37 @@ MkKernel.monfunc :rcp,
8015
8461
  MkKernel.monfunc op,
8016
8462
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8017
8463
  expr: {
8018
- float: "(#2) = #{c_fn}(#1);",
8019
- complex: "(#2) = c#{c_fn}(#1);",
8020
8464
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
8021
- }
8465
+ }.merge(MkKernel.float_widths("(#2) = #{c_fn}<f>(#1);"))
8466
+ .merge(MkKernel.cmplx_widths("(#2) = c#{c_fn}<f>(#1);"))
8022
8467
  end
8023
8468
 
8469
+ # log is the one member of the family whose complex form cancels, so it
8470
+ # does not follow the others onto `clogf`. The real part of `clog(z)` is
8471
+ # `log|z|`, which vanishes on the unit circle; computing |z| at the
8472
+ # operand's width rounds away everything the logarithm then needs, and
8473
+ # the answer comes back with a relative error near 400 instead of 1e-7.
8474
+ # The complex branch therefore stays on the double-taking `clog`, the way
8475
+ # `*` and `/` do -- see ca_op_cmplx64.h. Real `log` has no such step:
8476
+ # its operand is the argument itself, and `logf` measures at one float
8477
+ # epsilon right through x = 1.
8478
+ MkKernel.monfunc :log,
8479
+ source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8480
+ expr: {
8481
+ complex: "(#2) = clog(#1);",
8482
+ object: MkKernel.obj_float_math("log(<v>)", "log"),
8483
+ }.merge(MkKernel.float_widths("(#2) = log<f>(#1);"))
8484
+
8024
8485
  # exp2 special case: complex variant uses cpow(2, x), not cexp2 (which
8025
8486
  # isn't standardized in C99/POSIX).
8026
8487
  MkKernel.monfunc :exp2,
8027
8488
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8028
8489
  expr: {
8029
- float: "(#2) = exp2(#1);",
8490
+ # cpow(a, z) is cexp(z * clog(a)), so it inherits clog's cancellation
8491
+ # and stays on the double-taking form for both complex data_types.
8030
8492
  complex: "(#2) = cpow(2, (#1));",
8031
8493
  object: MkKernel.obj_float_math("exp2(<v>)", "exp2"),
8032
- }
8494
+ }.merge(MkKernel.float_widths("(#2) = exp2<f>(#1);"))
8033
8495
 
8034
8496
  # log10, log2, logb: no complex variant in the original mkmath emit
8035
8497
  {
@@ -8040,9 +8502,8 @@ MkKernel.monfunc :exp2,
8040
8502
  MkKernel.monfunc op,
8041
8503
  source: MkKernel::FLOAT_DTYPES + [:object],
8042
8504
  expr: {
8043
- float: "(#2) = #{c_fn}(#1);",
8044
8505
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
8045
- }
8506
+ }.merge(MkKernel.float_widths("(#2) = #{c_fn}<f>(#1);"))
8046
8507
  end
8047
8508
 
8048
8509
  # exp10: special object expr (= bypass OBJ_FLOAT_MATH for the
@@ -8050,8 +8511,6 @@ end
8050
8511
  MkKernel.monfunc :exp10,
8051
8512
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8052
8513
  expr: {
8053
- float: "(#2) = pow(10, (#1));",
8054
- complex: "(#2) = cpow(10, (#1));",
8055
8514
  object: <<~SNIPPET,
8056
8515
  {
8057
8516
  VALUE _obj_arg = (#1);
@@ -8062,10 +8521,13 @@ MkKernel.monfunc :exp10,
8062
8521
  }
8063
8522
  }
8064
8523
  SNIPPET
8065
- }
8524
+ complex: "(#2) = cpow(10, (#1));", # see exp2 on why cpow stays wide
8525
+ }.merge(MkKernel.float_widths("(#2) = pow<f>(10, (#1));"))
8066
8526
 
8067
- # Hyperbolic family: complex variant uses the real-typed C function
8068
- # (matches original mkmath emit, which doesn't prefix `c`).
8527
+ # Hyperbolic family: float uses the real-typed C function, complex uses
8528
+ # the C99 `c`-prefixed one. Passing a `double _Complex` to `sinh(double)`
8529
+ # discards the imaginary part, so the complex branch must not share the
8530
+ # real-typed name.
8069
8531
  {
8070
8532
  sinh: "sinh",
8071
8533
  cosh: "cosh",
@@ -8077,10 +8539,9 @@ MkKernel.monfunc :exp10,
8077
8539
  MkKernel.monfunc op,
8078
8540
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8079
8541
  expr: {
8080
- float: "(#2) = #{c_fn}(#1);",
8081
- complex: "(#2) = #{c_fn}(#1);",
8082
8542
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
8083
- }
8543
+ }.merge(MkKernel.float_widths("(#2) = #{c_fn}<f>(#1);"))
8544
+ .merge(MkKernel.cmplx_widths("(#2) = c#{c_fn}<f>(#1);"))
8084
8545
  end
8085
8546
 
8086
8547
  # ---- M.1 (PyTorch alignment): additional monfunc / monop ------------------
@@ -8094,19 +8555,17 @@ end
8094
8555
  MkKernel.monfunc op,
8095
8556
  source: MkKernel::FLOAT_DTYPES + [:object],
8096
8557
  expr: {
8097
- float: "(#2) = #{c_fn}(#1);",
8098
8558
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
8099
- }
8559
+ }.merge(MkKernel.float_widths("(#2) = #{c_fn}<f>(#1);"))
8100
8560
  end
8101
8561
 
8102
8562
  # rsqrt: 1 / sqrt(x). float + complex + object (complex via 1.0 / csqrt).
8103
8563
  MkKernel.monfunc :rsqrt,
8104
8564
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8105
8565
  expr: {
8106
- float: "(#2) = 1.0 / sqrt(#1);",
8107
- complex: "(#2) = 1.0 / csqrt(#1);",
8108
8566
  object: MkKernel.obj_float_math("1.0 / sqrt(<v>)", "rsqrt"),
8109
- }
8567
+ }.merge(MkKernel.float_widths("(#2) = ((<t>)1.0) / sqrt<f>(#1);"))
8568
+ .merge(MkKernel.cmplx_widths("(#2) = ((<t>)1.0) / csqrt<f>(#1);"))
8110
8569
 
8111
8570
  # trunc: toward-zero rounding. Preserve-data_type form like ceil / floor /
8112
8571
  # round — int branch is identity, float branch uses C99 trunc, object
@@ -8136,7 +8595,7 @@ MkKernel.monop :square,
8136
8595
  # with f64-forced input/output via ca_call_cfunc_1_1; the mkkernel form
8137
8596
  # preserves the same numeric behaviour but rides the lazy substrate +
8138
8597
  # kernel_iterator engine. Widening monfunc: integer input auto-casts
8139
- # to f64, float input preserves dtype.
8598
+ # to f64, float input preserves data type.
8140
8599
 
8141
8600
  # deg_360: fold into [0, 360). Use double-typed local for the fold
8142
8601
  # computation regardless of input precision (matches legacy hand-written
@@ -8315,12 +8774,12 @@ MkKernel.binop :minimum,
8315
8774
  object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("min"), 0);',
8316
8775
  }
8317
8776
 
8318
- # +, -, * use the same generic expression across all numeric data_types.
8777
+ # + and - work on the parts independently, so one generic expression
8778
+ # covers every numeric data_type at any width.
8319
8779
  {
8320
- add: ["+", "+", '"+"'],
8321
- sub: ["-", "-", '"-"'],
8322
- mul: ["*", "*", '"*"'],
8323
- }.each do |name, (op, c_op, _ruby_op)|
8780
+ add: ["+", "+"],
8781
+ sub: ["-", "-"],
8782
+ }.each do |name, (op, c_op)|
8324
8783
  MkKernel.binop name,
8325
8784
  op: op,
8326
8785
  source: MkKernel::MATH_NUMERIC + [:object],
@@ -8331,13 +8790,48 @@ MkKernel.binop :minimum,
8331
8790
  }
8332
8791
  end
8333
8792
 
8793
+ # * does not: a complex product subtracts two products of the parts, and
8794
+ # that subtraction cancels. A cmplx64 gets it in double, where there are
8795
+ # bits left underneath -- see ca_op_cmplx64.h. Unlike the divide this
8796
+ # costs speed rather than saving it, and is paid for the accuracy alone.
8797
+ MkKernel.binop :mul,
8798
+ op: "*",
8799
+ source: MkKernel::MATH_NUMERIC + [:object],
8800
+ expr: {
8801
+ numeric: "(#3) = (#1) * (#2);",
8802
+ [:cmplx64] => "(#3) = op_cmul_cmplx64((#1), (#2));",
8803
+ [:cmplx128] => "(#3) = (#1) * (#2);",
8804
+ object: '(#3) = rb_funcall((#1), rb_intern("*"), 1, (#2));',
8805
+ }
8806
+
8807
+ # div: `/`. Integer division floors toward -inf (= Ruby `Integer#/` and
8808
+ # NumPy `floor_divide`), so that `(a / b) * b + a % b == a` holds for
8809
+ # every sign combination. C's `/` truncates toward zero, so the signed
8810
+ # lanes correct the quotient by one when the division is inexact and the
8811
+ # operands have opposite signs. Unsigned operands can never disagree in
8812
+ # sign, so they keep the bare C form. Float `/` is true division and is
8813
+ # unchanged (matching Ruby `Float#/`); the identity above therefore holds
8814
+ # for integers only, exactly as in Ruby.
8334
8815
  MkKernel.binop :div,
8335
8816
  op: "/",
8336
8817
  source: MkKernel::MATH_NUMERIC + [:object],
8337
8818
  expr: {
8338
- int: "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) / (#2);",
8819
+ MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) / (#2);",
8820
+ int: %{
8821
+ if ((#2)==0) {ca_zerodiv();};
8822
+ {
8823
+ <type> _q = (#1) / (#2);
8824
+ if ( (#1) % (#2) != 0 && (((#1) < 0) != ((#2) < 0)) ) { _q -= 1; }
8825
+ (#3) = _q;
8826
+ }
8827
+ },
8339
8828
  float: "(#3) = (#1) / (#2);",
8340
- complex: "(#3) = (#1) / (#2);",
8829
+ # A cmplx64 divide is computed in double and rounded once: see
8830
+ # ca_op_cmplx64.h for why that is both faster and correctly rounded,
8831
+ # and how Annex G survives it. cmplx128 has no wider type to
8832
+ # borrow, so it stays on the compiler's helper.
8833
+ [:cmplx64] => "(#3) = op_cdiv_cmplx64((#1), (#2));",
8834
+ [:cmplx128] => "(#3) = (#1) / (#2);",
8341
8835
  object: '(#3) = rb_funcall((#1), rb_intern("/"), 1, (#2));',
8342
8836
  }
8343
8837
 
@@ -8357,27 +8851,57 @@ MkKernel.binop :rcp_mul,
8357
8851
  expr: {
8358
8852
  int: "if ((#1)==0) {ca_zerodiv();}; (#3) = (#2) / (#1);",
8359
8853
  float: "(#3) = (#2) / (#1);",
8360
- complex: "(#3) = (#2) / (#1);",
8854
+ [:cmplx64] => "(#3) = op_cdiv_cmplx64((#2), (#1));",
8855
+ [:cmplx128] => "(#3) = (#2) / (#1);",
8361
8856
  object: '(#3) = rb_funcall((#2), rb_intern("/"), 1, (#1));',
8362
8857
  }
8363
8858
 
8859
+ # mod: `%`. Floored modulo -- the result carries the sign of the divisor
8860
+ # (= Ruby `%` and NumPy `np.mod`), and pairs with the floored `/` above.
8861
+ # C's `%` and `fmod` carry the sign of the dividend instead, so the signed
8862
+ # lanes add the divisor back when the remainder is non-zero and disagrees
8863
+ # in sign with it. A zero remainder is given the divisor's sign so the
8864
+ # rule holds without exception (float only; integers have no signed zero).
8865
+ # Unsigned operands already satisfy the rule. The truncating form stays
8866
+ # available as `fmod`.
8364
8867
  MkKernel.binop :mod,
8365
8868
  op: "%",
8366
8869
  source: MkKernel::ALL_NUMERIC + [:object],
8367
8870
  expr: {
8368
- int: "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
8369
- float: "(#3) = fmod(#1, #2);",
8871
+ MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
8872
+ int: %{
8873
+ if ((#2)==0) {ca_zerodiv();};
8874
+ {
8875
+ <type> _r = (#1) % (#2);
8876
+ if ( _r != 0 && ((_r < 0) != ((#2) < 0)) ) { _r += (#2); }
8877
+ (#3) = _r;
8878
+ }
8879
+ },
8880
+ float: %{
8881
+ {
8882
+ <type> _r = fmod((#1), (#2));
8883
+ if ( _r != 0 ) {
8884
+ if ( (_r < 0) != ((#2) < 0) ) { _r += (#2); }
8885
+ }
8886
+ else { _r = copysign((<type>) 0, (#2)); }
8887
+ (#3) = _r;
8888
+ }
8889
+ },
8370
8890
  object: '(#3) = rb_funcall((#1), rb_intern("%"), 1, (#2));',
8371
8891
  }
8372
8892
 
8373
- MkKernel.binop :reminder,
8374
- op: "reminder",
8893
+ # fmod: the truncating counterpart of `%` -- the remainder carries the
8894
+ # sign of the dividend, as C's `fmod` and `%` do. Defined over integers
8895
+ # too (`%` no longer reaches that form since it floors), where it is C's
8896
+ # `%` with the same zero-divisor guard. Ruby's `Numeric#remainder` is
8897
+ # this same operation, so the object lane delegates to it and keeps the
8898
+ # element's class (an Integer stays an Integer).
8899
+ MkKernel.binop :fmod,
8375
8900
  source: MkKernel::ALL_NUMERIC + [:object],
8376
8901
  expr: {
8377
8902
  int: "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
8378
- float: "(#3) = remainder(#1, #2);",
8379
- object: '(#3) = rb_funcall((#1), rb_intern("%"), 1, (#2));',
8380
- }
8903
+ object: '(#3) = rb_funcall((#1), rb_intern("remainder"), 1, (#2));',
8904
+ }.merge(MkKernel.float_widths("(#3) = fmod<f>((#1), (#2));"))
8381
8905
 
8382
8906
  MkKernel.binop :bit_and_i,
8383
8907
  op: "&",
@@ -8482,6 +9006,7 @@ MkKernel.alias_binop :bit_rshift, :">>"
8482
9006
  # pow / cpow. Object uses Ruby's `**`.
8483
9007
  MkKernel.header_block <<~C
8484
9008
  #include "ca_op_powi.h"
9009
+ #include "ca_op_cmplx64.h"
8485
9010
  C
8486
9011
 
8487
9012
  # ---- triop family ---------------------------------------------------------
@@ -8554,10 +9079,9 @@ MkKernel.binop :power,
8554
9079
  source: MkKernel::MATH_NUMERIC + [:object],
8555
9080
  expr: {
8556
9081
  int: "(#3) = op_powi_<type>((#1), (#2));",
8557
- float: "(#3) = pow((#1), (#2));",
8558
- complex: "(#3) = cpow((#1), (#2));",
9082
+ complex: "(#3) = cpow((#1), (#2));", # see exp2 on why cpow stays wide
8559
9083
  object: '(#3) = rb_funcall((#1), rb_intern("**"), 1, (#2));',
8560
- }
9084
+ }.merge(MkKernel.float_widths("(#3) = pow<f>((#1), (#2));"))
8561
9085
 
8562
9086
  # ---- M.2 + M.3 (PyTorch alignment): float-only binop family --------------
8563
9087
  #
@@ -8573,7 +9097,6 @@ MkKernel.binop :power,
8573
9097
  copysign: ["copysign", "copysign"],
8574
9098
  logaddexp: [nil, nil], # custom expr — see below
8575
9099
  nextafter: ["nextafter", "nextafter"],
8576
- fmod: ["fmod", "fmod"],
8577
9100
  atan2: ["atan2", "atan2"],
8578
9101
  hypot: ["hypot", "hypot"],
8579
9102
  }.each do |op_name, (c_fn, ruby_fb)|
@@ -8581,7 +9104,6 @@ MkKernel.binop :power,
8581
9104
  MkKernel.binop op_name,
8582
9105
  source: MkKernel::FLOAT_DTYPES + [:object],
8583
9106
  expr: {
8584
- float: "(#3) = #{c_fn}((#1), (#2));",
8585
9107
  object: <<~SNIPPET,
8586
9108
  {
8587
9109
  VALUE _l = (#1);
@@ -8594,7 +9116,7 @@ MkKernel.binop :power,
8594
9116
  }
8595
9117
  }
8596
9118
  SNIPPET
8597
- }
9119
+ }.merge(MkKernel.float_widths("(#3) = #{c_fn}<f>((#1), (#2));"))
8598
9120
  end
8599
9121
 
8600
9122
  # logaddexp: log(exp(x) + exp(y)). Numerically stable form:
@@ -8603,7 +9125,6 @@ end
8603
9125
  MkKernel.binop :logaddexp,
8604
9126
  source: MkKernel::FLOAT_DTYPES + [:object],
8605
9127
  expr: {
8606
- float: "(#3) = fmax((#1), (#2)) + log1p(exp(-fabs((#1) - (#2))));",
8607
9128
  object: <<~SNIPPET,
8608
9129
  {
8609
9130
  VALUE _l = (#1);
@@ -8619,7 +9140,8 @@ MkKernel.binop :logaddexp,
8619
9140
  }
8620
9141
  }
8621
9142
  SNIPPET
8622
- }
9143
+ }.merge(MkKernel.float_widths(
9144
+ "(#3) = fmax<f>((#1), (#2)) + log1p<f>(exp<f>(-fabs<f>((#1) - (#2))));"))
8623
9145
 
8624
9146
  # ---- P.5b.4: moncmp family (predicates returning bool) ----------------
8625
9147
 
@@ -8669,7 +9191,7 @@ MkKernel.moncmp :is_invalid,
8669
9191
  # -0.0). Integer branch: sint = (#1) < 0, uint = always 0. Float branch
8670
9192
  # uses C99 signbit (handles -0.0 / NaN sign correctly). No complex
8671
9193
  # variant (signbit on a complex is ambiguous; rejected at this layer).
8672
- # `:sint` / `:uint` aren't family aliases — use array-of-dtypes form.
9194
+ # `:sint` / `:uint` aren't family aliases — use the array-of-types form.
8673
9195
  MkKernel.moncmp :signbit,
8674
9196
  source: MkKernel::SINT_DTYPES + MkKernel::UINT_DTYPES +
8675
9197
  MkKernel::FLOAT_DTYPES + [:object],