carray 3.0.0 → 3.0.1

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +325 -3
  4. data/{NEWS.md → CHANGELOG.v1.md} +1 -1
  5. data/README.md +32 -32
  6. data/carray.gemspec +12 -8
  7. data/ext/ca_axis_group.c +55 -19
  8. data/ext/ca_binop_dispatch.c +5 -6
  9. data/ext/ca_binop_dispatch.h +0 -7
  10. data/ext/ca_categorical_iterator.c +13 -13
  11. data/ext/ca_for_each_element.h +6 -8
  12. data/ext/ca_group_iter.c +1 -1
  13. data/ext/ca_kernel_iterator.c +69 -23
  14. data/ext/ca_kernel_iterator.h +3 -10
  15. data/ext/ca_obj_bincmp.c +57 -27
  16. data/ext/ca_obj_binop.c +86 -30
  17. data/ext/ca_obj_const_string.c +3 -1
  18. data/ext/ca_obj_face.c +80 -0
  19. data/ext/ca_obj_face.h +33 -1
  20. data/ext/ca_obj_fake.c +11 -0
  21. data/ext/ca_obj_fixlen_string.c +3 -1
  22. data/ext/ca_obj_grid.c +11 -3
  23. data/ext/ca_obj_meld.c +13 -8
  24. data/ext/ca_obj_moncmp.c +33 -14
  25. data/ext/ca_obj_monop.c +19 -7
  26. data/ext/ca_obj_object.c +99 -4
  27. data/ext/ca_obj_record.c +3 -1
  28. data/ext/ca_obj_refer.c +30 -6
  29. data/ext/ca_obj_roll.c +13 -5
  30. data/ext/ca_obj_select_axis.c +16 -6
  31. data/ext/ca_obj_shift.c +3 -3
  32. data/ext/ca_obj_stride.c +106 -23
  33. data/ext/ca_obj_string.c +3 -1
  34. data/ext/ca_obj_tile.c +13 -5
  35. data/ext/ca_obj_time.c +3 -1
  36. data/ext/ca_obj_timedelta.c +3 -1
  37. data/ext/ca_obj_transpose.c +1 -1
  38. data/ext/ca_obj_triop.c +62 -23
  39. data/ext/ca_obj_window.c +86 -17
  40. data/ext/ca_op_cmplx64.h +123 -0
  41. data/ext/ca_op_ipower.c +0 -3
  42. data/ext/ca_sort_kernels.h +5 -5
  43. data/ext/ca_sweep_engine.c +78 -35
  44. data/ext/ca_sweep_engine.h +13 -4
  45. data/ext/ca_transform_common.c +7 -0
  46. data/ext/carray.h +127 -130
  47. data/ext/carray_access.c +56 -34
  48. data/ext/carray_bincount.c +8 -8
  49. data/ext/carray_broadcast.c +99 -6
  50. data/ext/carray_build_flags.h +3 -0
  51. data/ext/carray_call_cfunc.c +1491 -0
  52. data/ext/carray_call_cfunc.h +153 -0
  53. data/ext/carray_cast.c +69 -31
  54. data/ext/carray_conversion.c +22 -22
  55. data/ext/carray_copy.c +12 -2
  56. data/ext/carray_core.c +57 -10
  57. data/ext/carray_count.c +1 -1
  58. data/ext/carray_element.c +7 -7
  59. data/ext/carray_factorize.c +28 -28
  60. data/ext/carray_hold.c +1 -1
  61. data/ext/carray_index_classifier.c +7 -21
  62. data/ext/carray_internal.h +19 -3
  63. data/ext/carray_kernels_bincmp.c +1 -0
  64. data/ext/carray_kernels_binop.c +320 -298
  65. data/ext/carray_kernels_init.c +1095 -0
  66. data/ext/carray_kernels_map.c +1 -0
  67. data/ext/carray_kernels_moncmp.c +1 -0
  68. data/ext/carray_kernels_monop.c +412 -411
  69. data/ext/carray_kernels_reduce_aggregate.c +88 -87
  70. data/ext/carray_kernels_reduce_boolean.c +1 -0
  71. data/ext/carray_kernels_reduce_cumulative.c +54 -53
  72. data/ext/carray_kernels_reduce_extreme.c +51 -50
  73. data/ext/carray_kernels_reduce_variance.c +1 -0
  74. data/ext/carray_kernels_scan.c +1 -0
  75. data/ext/carray_kernels_search.c +93 -92
  76. data/ext/carray_kernels_sort.c +1 -0
  77. data/ext/carray_kernels_triop.c +1 -0
  78. data/ext/carray_lazy.c +175 -5
  79. data/ext/carray_mask.c +6 -5
  80. data/ext/carray_median_percentile.c +1 -1
  81. data/ext/carray_memory_view.c +46 -21
  82. data/ext/carray_operator.c +46 -49
  83. data/ext/carray_partition.c +4 -4
  84. data/ext/carray_random.c +7 -7
  85. data/ext/carray_scatter.c +1 -1
  86. data/ext/carray_sort.c +3 -3
  87. data/ext/carray_sort_kernel.c +10 -10
  88. data/ext/carray_test.c +1 -1
  89. data/ext/extconf.rb +18 -0
  90. data/ext/mk_call_cfunc.rb +243 -52
  91. data/ext/mkkernel.rb +354 -100
  92. data/ext/ruby_carray.c +18 -16
  93. data/ext/version.h +4 -4
  94. data/lib/carray/autoload_carray.rb +19 -0
  95. data/lib/carray/autoload_method_extension.rb +2 -1
  96. data/lib/carray/axis_group.rb +7 -7
  97. data/lib/carray/basics.rb +59 -59
  98. data/lib/carray/bincount_nd.rb +12 -12
  99. data/lib/carray/block_iterator.rb +24 -14
  100. data/lib/carray/categorical.rb +4 -4
  101. data/lib/carray/categorical_iterator.rb +98 -76
  102. data/lib/carray/conditional.rb +14 -14
  103. data/lib/carray/construct.rb +21 -0
  104. data/lib/carray/core_extensions.rb +62 -5
  105. data/lib/carray/data_type_extension.rb +18 -1
  106. data/lib/carray/frame/concat.rb +3 -3
  107. data/lib/carray/frame/convert.rb +1 -1
  108. data/lib/carray/frame/frame.rb +4 -11
  109. data/lib/carray/frame/io.rb +159 -2
  110. data/lib/carray/frame/sort.rb +1 -1
  111. data/lib/carray/frame/verbs.rb +18 -1
  112. data/lib/carray/fuse_source.rb +123 -0
  113. data/lib/carray/fusion.rb +218 -0
  114. data/lib/carray/histogram.rb +16 -16
  115. data/lib/carray/inspect.rb +1 -7
  116. data/lib/carray/iterator.rb +4 -3
  117. data/lib/carray/lazy.rb +125 -73
  118. data/lib/carray/meld_reduce.rb +2 -2
  119. data/lib/carray/methods/align_addr.rb +1 -1
  120. data/lib/carray/methods/composition.rb +1 -1
  121. data/lib/carray/methods/is_in.rb +12 -12
  122. data/lib/carray/methods/locate_addr.rb +6 -1
  123. data/lib/carray/methods/mask_duplicates.rb +1 -1
  124. data/lib/carray/methods/meshgrid.rb +4 -5
  125. data/lib/carray/methods/mode.rb +2 -2
  126. data/lib/carray/methods/nunique.rb +1 -1
  127. data/lib/carray/methods/snap.rb +7 -2
  128. data/lib/carray/methods/unique.rb +3 -3
  129. data/lib/carray/methods/value_counts.rb +2 -2
  130. data/lib/carray/runtime.rb +0 -19
  131. data/lib/carray/slab_iterator.rb +20 -7
  132. data/lib/carray/string_operation_extension.rb +5 -5
  133. data/lib/carray/time.rb +890 -491
  134. data/lib/carray/window_iterator.rb +285 -13
  135. data/lib/carray.rb +5 -5
  136. data/yard-stubs/ca_obj_array.rb +385 -0
  137. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  138. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  139. data/yard-stubs/ca_obj_block.rb +73 -0
  140. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  141. data/yard-stubs/ca_obj_fake.rb +31 -0
  142. data/yard-stubs/ca_obj_farray.rb +32 -0
  143. data/yard-stubs/ca_obj_field.rb +45 -0
  144. data/yard-stubs/ca_obj_grid.rb +35 -0
  145. data/yard-stubs/ca_obj_refer.rb +72 -0
  146. data/yard-stubs/ca_obj_roll.rb +45 -0
  147. data/yard-stubs/ca_obj_shift.rb +43 -0
  148. data/yard-stubs/ca_obj_stride.rb +181 -0
  149. data/yard-stubs/ca_obj_tile.rb +29 -0
  150. data/yard-stubs/ca_obj_transpose.rb +40 -0
  151. data/yard-stubs/ca_obj_window.rb +49 -0
  152. data/yard-stubs/carray_access.rb +131 -0
  153. data/yard-stubs/carray_attribute.rb +246 -0
  154. data/yard-stubs/carray_broadcast.rb +37 -0
  155. data/yard-stubs/carray_cast.rb +489 -0
  156. data/yard-stubs/carray_class.rb +65 -0
  157. data/yard-stubs/carray_conversion.rb +76 -0
  158. data/yard-stubs/carray_copy.rb +79 -0
  159. data/yard-stubs/carray_core.rb +114 -0
  160. data/yard-stubs/carray_count.rb +79 -0
  161. data/yard-stubs/carray_element.rb +108 -0
  162. data/yard-stubs/carray_generate.rb +66 -0
  163. data/yard-stubs/carray_lazy.rb +23 -0
  164. data/yard-stubs/carray_loop.rb +140 -0
  165. data/yard-stubs/carray_mask.rb +259 -0
  166. data/yard-stubs/carray_math.rb +132 -0
  167. data/yard-stubs/carray_mathfunc.rb +45 -0
  168. data/yard-stubs/carray_median_percentile.rb +89 -0
  169. data/yard-stubs/carray_memory_view.rb +163 -0
  170. data/yard-stubs/carray_order.rb +312 -0
  171. data/yard-stubs/carray_random.rb +89 -0
  172. data/yard-stubs/carray_scatter.rb +106 -0
  173. data/yard-stubs/carray_slab.rb +57 -0
  174. data/yard-stubs/carray_sort.rb +163 -0
  175. data/yard-stubs/carray_test.rb +85 -0
  176. data/yard-stubs/carray_undef.rb +64 -0
  177. data/yard-stubs/carray_utils.rb +97 -0
  178. data/yard-stubs/ruby_carray.rb +193 -0
  179. metadata +59 -10
  180. 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
@@ -757,7 +806,7 @@ module MkKernel
757
806
  # output cell is UNDEF).
758
807
  # :sentinel -> masked cells are treated as an incomparable sentinel,
759
808
  # the same role NaN plays for :end nan_policy but runtime-
760
- # selectable and dtype-agnostic. Per fiber, unmasked
809
+ # selectable and type-agnostic. Per fiber, unmasked
761
810
  # cells are compacted into a contiguous sub-range and only
762
811
  # that sub-range is sorted/quickselected; masked cells are
763
812
  # compacted into the complementary sub-range at the head
@@ -1005,7 +1054,9 @@ module MkKernel
1005
1054
  # demotes to real f64). Hash form requires the kernel author to
1006
1055
  # write expr that produces output data_type values for each source family
1007
1056
  # (e.g. cabs() for complex returning double).
1008
- raise "#{name}: unknown output #{output}" unless output == :preserve || DTYPES.key?(output) || output.is_a?(Hash)
1057
+ raise "#{name}: unknown output #{output}" \
1058
+ unless output == :preserve || output == :real_of_source ||
1059
+ DTYPES.key?(output) || output.is_a?(Hash)
1009
1060
  KERNELS << {
1010
1061
  kind: :monop,
1011
1062
  name: name,
@@ -1188,6 +1239,13 @@ module MkKernel
1188
1239
  # fallback when no family matches. Uses the same family aliases
1189
1240
  # as monop_expr_family_match? (:numeric / :int / :float / :complex
1190
1241
  # / :bool / :object).
1242
+ #
1243
+ # A Hash value (or a bare output form) may also be :real_of_source,
1244
+ # meaning "the real component width of the source complex data_type"
1245
+ # (cmplx64 -> f32, cmplx128 -> f64). This is what an op like abs
1246
+ # wants for its complex family: the magnitude of a cmplx64 is a
1247
+ # float32, the same width `.real` and `.imag` already return. Naming
1248
+ # :f64 there would hand cmplx64 the real width of cmplx128.
1191
1249
  # SL.1.1: Resolve the reduce macro suffix for a given kernel entry,
1192
1250
  # driven by reduction_kind. Returns "" for :none (= legacy
1193
1251
  # CA_SLAB_REDUCE_T_EX), or "_PLUS" / "_MIN" / "_MAX" / "_STAR" to
@@ -1208,7 +1266,7 @@ module MkKernel
1208
1266
  #
1209
1267
  # Ineligible:
1210
1268
  # - suffix == "" (:object src / no_simd_src override — reduce body
1211
- # is Ruby callback or dtype-specific, single-accumulator required)
1269
+ # is Ruby callback or type-specific, single-accumulator required)
1212
1270
  # - array_arg (weighted reductions use the ARRAY_T_EX macro family,
1213
1271
  # which has its own emit path)
1214
1272
  def self.reduce_8way_eligible?(k, src, suffix)
@@ -1325,6 +1383,24 @@ module MkKernel
1325
1383
  end
1326
1384
  end
1327
1385
 
1386
+ # Real component width of a complex data_type: cmplx64 -> f32,
1387
+ # cmplx128 -> f64. Raises for anything else, since :real_of_source
1388
+ # only has a meaning for a complex source.
1389
+ REAL_OF_CMPLX = { cmplx64: :f32, cmplx128: :f64 }.freeze
1390
+
1391
+ def self.resolve_output_dtype(kernel, src, dt)
1392
+ case dt
1393
+ when :preserve then DTYPES[src]
1394
+ when :real_of_source
1395
+ real = REAL_OF_CMPLX[src]
1396
+ raise "#{kernel[:name]}: output :real_of_source needs a complex src (got #{src})" \
1397
+ unless real
1398
+ DTYPES[real]
1399
+ else
1400
+ DTYPES[dt]
1401
+ end
1402
+ end
1403
+
1328
1404
  def self.output_info(kernel, src)
1329
1405
  out = kernel[:output]
1330
1406
  case out
@@ -1334,16 +1410,15 @@ module MkKernel
1334
1410
  next if family == :default
1335
1411
  if monop_expr_family_match?(family, src)
1336
1412
  # 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]
1413
+ # family"), :real_of_source, or a data_type symbol like :f64.
1414
+ return resolve_output_dtype(kernel, src, dt)
1339
1415
  end
1340
1416
  end
1341
1417
  raise "#{kernel[:name]}: output Hash has no match for src #{src} and no :default" \
1342
1418
  unless out.key?(:default)
1343
- dt = out[:default]
1344
- (dt == :preserve) ? DTYPES[src] : DTYPES[dt]
1419
+ resolve_output_dtype(kernel, src, out[:default])
1345
1420
  else
1346
- DTYPES[out]
1421
+ resolve_output_dtype(kernel, src, out)
1347
1422
  end
1348
1423
  end
1349
1424
 
@@ -2643,8 +2718,8 @@ module MkKernel
2643
2718
  write stride 1 (= contig output tail) both preserved -> SIMD
2644
2719
  tile reduce + write maintained.
2645
2720
 
2646
- Performance characteristic (deliver-via-view, per the CLAUDE.md
2647
- "deliver the materials" principle): bench (b2 pattern, M=200, K=5, eff_INNER
2721
+ Performance characteristic (deliver-via-view -- the surface
2722
+ prioritises delivering the cells over avoiding a copy): bench (b2 pattern, M=200, K=5, eff_INNER
2648
2723
  =360, INNER_pre_K=16) yields 3674 us vs eager-entity 1545 us
2649
2724
  = 2.38x slow. Root cause is the multi-parent data layout
2650
2725
  (= 5 separate 9 MB regions instead of one contig 46 MB),
@@ -2800,8 +2875,8 @@ module MkKernel
2800
2875
  # reductions consume the same slab)
2801
2876
  # - no streaming / view_flat / array_arg / value_arg
2802
2877
  # - 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.
2878
+ # The if-form discipline for multi-reduction fused kernels (see
2879
+ # emit_min_max below) is enforced by author, not by the generator.
2805
2880
  def self.emit_reduce_native_multi(io, k, src)
2806
2881
  si = DTYPES[src]
2807
2882
  oi = output_info(k, src)
@@ -2976,7 +3051,15 @@ module MkKernel
2976
3051
  io.puts " (void) masked_cnt;"
2977
3052
  end
2978
3053
  io.puts " ca_lazy_arena_enter();"
2979
- io.puts " #{si[:c]} *__chunk = (#{si[:c]} *) ca_lazy_arena_acquire(__chunk_elems * sizeof(#{si[:c]}));"
3054
+ if si[:c] == "VALUE"
3055
+ # Object lane: the chunk holds VALUEs pulled from the source, and
3056
+ # the fold below calls rb_funcall per cell. A collection there
3057
+ # would free cells a lazy source produced into the chunk, so the
3058
+ # slot has to stay marked for as long as it is held.
3059
+ io.puts " VALUE *__chunk = (VALUE *) ca_lazy_arena_acquire_object(__chunk_elems);"
3060
+ else
3061
+ io.puts " #{si[:c]} *__chunk = (#{si[:c]} *) ca_lazy_arena_acquire(__chunk_elems * sizeof(#{si[:c]}));"
3062
+ end
2980
3063
  io.puts " while ( __outer_off < __outer ) {"
2981
3064
  io.puts " ca_size_t __r = (__outer - __outer_off < __rows) ? (__outer - __outer_off) : __rows;"
2982
3065
  io.puts " ca_size_t __n = __r * __inner;"
@@ -3794,7 +3877,7 @@ module MkKernel
3794
3877
  # `masked_last` parameter. Sets `sort_lo` / `sort_n` to the unmasked
3795
3878
  # sub-range so the downstream sort/quickselect call only ever compares
3796
3879
  # unmasked pairs -- masked cells are an incomparable sentinel, the same
3797
- # role NaN plays for nan_policy: :end, but dtype-agnostic and runtime-
3880
+ # role NaN plays for nan_policy: :end, but type-agnostic and runtime-
3798
3881
  # selectable. `payload_expr(k)` computes the `.i` payload (fiber-local
3799
3882
  # index for :fiber_local semantics, view-flat address for :view_flat)
3800
3883
  # given the Ruby string `k` naming the C loop variable.
@@ -4829,7 +4912,7 @@ module MkKernel
4829
4912
  {
4830
4913
  /* rev4 A1 via single-element CArray: convert CScalar / [1] 1-D /
4831
4914
  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
4915
+ scalar path. The data type matches ca (= rb_ca_obj2ptr coerces it
4833
4916
  downstream). */
4834
4917
  if ( rb_obj_is_carray(rval) ) {
4835
4918
  CArray *cv_pre_;
@@ -6055,6 +6138,90 @@ module MkKernel
6055
6138
  # The aggregator init.c, where Init_carray_kernels() calls each per-tag
6056
6139
  # Init_<suffix>() in file_tags order. No tag depends on another: an alias
6057
6140
  # already sits after its target within its own file.
6141
+ # ---------------------------------------------------------------------
6142
+ # The kernel bodies, as text, for a caller that has to compute the same
6143
+ # thing somewhere other than in these kernels -- a compiler handed an
6144
+ # expression tree, which must produce what the eager kernel produces.
6145
+ # Emitting the table here rather than reading this generator at runtime
6146
+ # keeps a build-time tool out of the running process.
6147
+ #
6148
+ # The bodies keep their `#1` / `#2` / `#3` and `<type>` placeholders: the
6149
+ # caller substitutes its own operands. Object-lane bodies are left out,
6150
+ # since they call back into the interpreter and cannot be compiled apart
6151
+ # from it.
6152
+ # ---------------------------------------------------------------------
6153
+
6154
+ BODY_TABLE_DTYPES = %i[i8 u8 i16 u16 i32 u32 i64 u64 f32 f64
6155
+ bool cmplx64 cmplx128].freeze
6156
+
6157
+ def self.body_table_rows
6158
+ rows = []
6159
+ KERNELS.each do |k|
6160
+ next unless %i[monop binop triop].include?(k[:kind])
6161
+ BODY_TABLE_DTYPES.each do |src|
6162
+ body = monop_expr_for(k, src)
6163
+ next unless body.is_a?(String)
6164
+ next if body.include?("rb_funcall")
6165
+ rows << [k[:kind].to_s, k[:name].to_s,
6166
+ DTYPES[src][:ca].sub(/\ACA_/, "").downcase, body]
6167
+ end
6168
+ end
6169
+ rows
6170
+ end
6171
+
6172
+ def self.c_string_literal(text)
6173
+ '"' + text.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", '\\n') + '"'
6174
+ end
6175
+
6176
+ def self.emit_kernel_bodies(io)
6177
+ rows = body_table_rows
6178
+ io.puts
6179
+ io.puts "/* The text of every kernel body, for a caller that compiles the same"
6180
+ io.puts " operation elsewhere. Placeholders are left in place. */"
6181
+ io.puts
6182
+ io.puts "typedef struct {"
6183
+ io.puts " const char *kind;"
6184
+ io.puts " const char *name;"
6185
+ io.puts " const char *data_type;"
6186
+ io.puts " const char *body;"
6187
+ io.puts "} ca_kernel_body_t;"
6188
+ io.puts
6189
+ io.puts "static const ca_kernel_body_t ca_kernel_bodies[] = {"
6190
+ rows.each do |kind, name, dtype, body|
6191
+ io.puts " { #{c_string_literal(kind)}, #{c_string_literal(name)}, " \
6192
+ "#{c_string_literal(dtype)},"
6193
+ io.puts " #{c_string_literal(body)} },"
6194
+ end
6195
+ io.puts "};"
6196
+ io.puts
6197
+ io.puts "static const int ca_kernel_bodies_count = #{rows.size};"
6198
+ io.puts
6199
+ io.puts <<~C
6200
+ /* CArray.__kernel_body__(kind, name, data_type) -> String, or nil where
6201
+ this operation has no body at that data type. */
6202
+ static VALUE
6203
+ rb_ca_s_kernel_body (VALUE klass, VALUE rkind, VALUE rname, VALUE rtype)
6204
+ {
6205
+ VALUE kind = rb_obj_as_string(rkind);
6206
+ VALUE name = rb_obj_as_string(rname);
6207
+ VALUE type = rb_obj_as_string(rtype);
6208
+ const char *k = StringValueCStr(kind);
6209
+ const char *n = StringValueCStr(name);
6210
+ const char *t = StringValueCStr(type);
6211
+ int i;
6212
+ for ( i = 0; i < ca_kernel_bodies_count; i++ ) {
6213
+ const ca_kernel_body_t *e = &ca_kernel_bodies[i];
6214
+ if ( strcmp(e->kind, k) == 0 &&
6215
+ strcmp(e->name, n) == 0 &&
6216
+ strcmp(e->data_type, t) == 0 ) {
6217
+ return rb_str_new_cstr(e->body);
6218
+ }
6219
+ }
6220
+ return Qnil;
6221
+ }
6222
+ C
6223
+ end
6224
+
6058
6225
  def self.emit_aggregator_init(io, tags)
6059
6226
  io.puts "/* GENERATED aggregator: dispatches to per-tag Init_carray_kernels_<tag>() */"
6060
6227
  io.puts "#include \"carray.h\""
@@ -6063,23 +6230,30 @@ module MkKernel
6063
6230
  io.puts "void Init_carray_kernels_#{file_suffix(kind, sub)} (void);"
6064
6231
  end
6065
6232
  io.puts
6233
+ emit_kernel_bodies(io)
6234
+ io.puts
6066
6235
  io.puts "void"
6067
6236
  io.puts "Init_carray_kernels (void)"
6068
6237
  io.puts "{"
6069
6238
  tags.each do |kind, sub|
6070
6239
  io.puts " Init_carray_kernels_#{file_suffix(kind, sub)}();"
6071
6240
  end
6241
+ io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6242
+ io.puts " rb_ca_s_kernel_body, 3);"
6072
6243
  io.puts "}"
6073
6244
  end
6074
6245
 
6075
6246
  # Single-stream Init_carray_kernels(): every kind emitted in order inside
6076
6247
  # one function.
6077
6248
  def self.emit_init(io)
6249
+ emit_kernel_bodies(io)
6078
6250
  io.puts
6079
6251
  io.puts "void"
6080
6252
  io.puts "Init_carray_kernels (void)"
6081
6253
  io.puts "{"
6082
6254
  KERNELS.each { |k| emit_init_line(io, k) }
6255
+ io.puts " rb_define_singleton_method(rb_cCArray, \"__kernel_body__\","
6256
+ io.puts " rb_ca_s_kernel_body, 3);"
6083
6257
  io.puts "}"
6084
6258
  end
6085
6259
 
@@ -6155,7 +6329,7 @@ module MkKernel
6155
6329
  # bench A/B comparison during Phase E (E.1-E.5); after E.7 they
6156
6330
  # are retired. Maps and scans keep their `_ki` names because
6157
6331
  # they have no user-facing equivalent yet (cumsum etc. pending
6158
- # rewire per CLAUDE.md "methods awaiting reimplementation").
6332
+ # rewire).
6159
6333
  #
6160
6334
  # SO.2 rev6 (2026-06-04): sort kernels can opt out of the _ki
6161
6335
  # binding via bind_ruby: false (= internal-only kernels consumed
@@ -6500,11 +6674,11 @@ MkKernel.reduce :stddev,
6500
6674
  # -fopenmp-simd). The 2x f64 speedup survives SL.1.2's reduction(min/max:)
6501
6675
  # vectorizer reject because fminnm + fmaxnm dual-issue on M2's two FP pipes.
6502
6676
  #
6503
- # if-form discipline (CLAUDE.md "write multi-reduction fused kernels in if-form"):
6677
+ # if-form discipline for multi-reduction fused kernels:
6504
6678
  # the body uses `if (v < lo) lo = v;` etc, NOT ternary `lo = (v < lo) ? v : lo;`.
6505
6679
  # DO NOT change to ternary — pragma-less ILP path depends on if-form.
6506
6680
  #
6507
- # FM.1.0 scope: mask not yet propagated (FM.1.5). Numeric dtypes only.
6681
+ # FM.1.0 scope: mask not yet propagated (FM.1.5). Numeric data types only.
6508
6682
 
6509
6683
  MkKernel.reduce :minmax,
6510
6684
  state: { lo: :load_type, hi: :load_type },
@@ -6541,9 +6715,9 @@ MkKernel.reduce :minmax,
6541
6715
  # actual name (`best_v` here) through to the macro -- the macro accepts
6542
6716
  # any identifier as its accumulator argument.
6543
6717
  #
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
6718
+ # Exposed as Ruby `min_index` / `max_index` (= the naming rule that a
6719
+ # method returning a position uses the `_index` suffix; see the rules
6720
+ # table in guides/devel/00_glossary.md). These
6547
6721
  # replace the legacy `min_addr` / `max_addr` retired in E.7 stat_proc
6548
6722
  # retire (commit f5c7ecd). 3.0 breaking: name change from `*_addr` to
6549
6723
  # `*_index` is intentional.
@@ -6595,10 +6769,10 @@ MkKernel.reduce :argmax,
6595
6769
  # axis-local-to-flat-addr round-trip).
6596
6770
  #
6597
6771
  # 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").
6772
+ # and partition family's partition_addr_ki (= internal). `_addr`
6773
+ # is OK to expose for a per-axis primitive that returns a real flat
6774
+ # address: the dual API is `_index` for an axis-local position, `_addr`
6775
+ # for a view-flat one.
6602
6776
  #
6603
6777
  # Runtime constraints:
6604
6778
  # - naxes == 1 (single-axis reduce): view-flat transform applied
@@ -6806,7 +6980,8 @@ MkKernel.reduce :count_equal,
6806
6980
  # paths removed (= ALL_NUMERIC + :raise fallback). Re-add via demand-
6807
6981
  # driven complex specialization or CA_OBJECT bridge phase.
6808
6982
  # - **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).
6983
+ # (= the original "open per-axis" goal: every reduction gains an
6984
+ # axis form).
6810
6985
  #
6811
6986
  # public_method: true rebinds `wsum` from legacy rb_ca_wsum to
6812
6987
  # rb_ca_wsum_ki at Init time.
@@ -7015,7 +7190,7 @@ MkKernel.scan :cumcount,
7015
7190
  # acc_type: :load_type -> acc carries T_LOAD (= last seen input value).
7016
7191
  # STEP additionally sees `first` (int) marking the first live cell of
7017
7192
  # the fiber. Output data_type = :bool. No production consumer remains:
7018
- # every dtype's mask_duplicates now uses the O(distinct) seen-set hash
7193
+ # every data type's mask_duplicates now uses the O(distinct) seen-set hash
7019
7194
  # lane (__mask_duplicates__, with boolean riding its uint8 lane), which
7020
7195
  # has no sort buffers. The numeric widths are kept as a standalone scan
7021
7196
  # kernel (a sort-path reference oracle in the mask_duplicates tests).
@@ -7392,8 +7567,8 @@ MkKernel.search :search_nearest,
7392
7567
  # (= axis-local position) is transformed to a view-flat (row-major) address
7393
7568
  # into self by the dispatcher. Paired with the *_index family
7394
7569
  # (= 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").
7570
+ # positions per the dual `_index` / `_addr` API: `_index` for an
7571
+ # axis-local position, `_addr` for a view-flat one).
7397
7572
  #
7398
7573
  # Scope: scalar val path only (= case A). CArray val + broadcast
7399
7574
  # path (case B/C) raises NotImpError until extended (= the per-element
@@ -7805,13 +7980,16 @@ MkKernel.monop :abs_i,
7805
7980
  MkKernel::SINT64_DTYPES => "(#2) = llabs(#1);",
7806
7981
  MkKernel::UINT_DTYPES => "(#2) = (#1);",
7807
7982
  MkKernel::FLOAT_DTYPES => "(#2) = fabs((float64_t)#1);",
7808
- MkKernel::CMPLX_DTYPES => "(#2) = cabs((cmplx128_t)#1);",
7983
+ [:cmplx64] => "(#2) = cabsf(#1);",
7984
+ [:cmplx128] => "(#2) = cabs(#1);",
7809
7985
  [:object] => '(#2) = rb_funcall((#1), rb_intern("abs"), 0);',
7810
7986
  }
7811
7987
 
7812
7988
  # abs: data_type-changing monop (the framework-piece test customer for monop
7813
7989
  # Hash output form). numeric input -> preserve data_type (= int/float abs),
7814
- # complex input -> f64 output (= magnitude is real). Replaces the hand-
7990
+ # complex input -> the real component width of that complex data_type
7991
+ # (= magnitude is real, and a cmplx64 magnitude is a float32 just as
7992
+ # `.real` and `.imag` are). Replaces the hand-
7815
7993
  # written rb_ca_abs / rb_ca_abs_bang in ext/carray_math.c. Object data_type
7816
7994
  # kept on abs_i (= bind: false on object would need a different output
7817
7995
  # rule; deferred). The abs_i monop above remains the primary kernel for
@@ -7820,13 +7998,15 @@ MkKernel.monop :abs_i,
7820
7998
  # returns the real-valued magnitude entity.
7821
7999
  MkKernel.monop :abs,
7822
8000
  source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7823
- output: { numeric: :preserve, complex: :f64 },
8001
+ output: { numeric: :preserve, complex: :real_of_source },
7824
8002
  expr: {
7825
8003
  MkKernel::SINT_SMALL_DTYPES => "(#2) = abs(#1);",
7826
8004
  MkKernel::SINT64_DTYPES => "(#2) = llabs(#1);",
7827
8005
  MkKernel::UINT_DTYPES => "(#2) = (#1);",
7828
8006
  MkKernel::FLOAT_DTYPES => "(#2) = fabs(#1);",
7829
- MkKernel::CMPLX_DTYPES => "(#2) = cabs(#1);", # complex -> double (real magnitude)
8007
+ # complex -> the real magnitude, at that complex type's own width
8008
+ [:cmplx64] => "(#2) = cabsf(#1);",
8009
+ [:cmplx128] => "(#2) = cabs(#1);",
7830
8010
  }
7831
8011
 
7832
8012
  # abs2: squared magnitude. For real x this is x*x (identical to :square
@@ -7837,22 +8017,22 @@ MkKernel.monop :abs,
7837
8017
  # optics, signal processing) do not pay for a sqrt they immediately
7838
8018
  # square away.
7839
8019
  #
7840
- # Output data_type follows :abs: numeric preserved, complex -> f64.
8020
+ # Output data_type follows :abs: numeric preserved, complex demoted to
8021
+ # its own real component width.
7841
8022
  MkKernel.monop :abs2,
7842
8023
  source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7843
- output: { numeric: :preserve, complex: :f64 },
8024
+ output: { numeric: :preserve, complex: :real_of_source },
7844
8025
  expr: {
7845
8026
  numeric: "(#2) = (#1) * (#1);",
7846
- complex: "{ double _r = creal(#1); double _i = cimag(#1); (#2) = _r * _r + _i * _i; }",
7847
- }
8027
+ }.merge(MkKernel.cmplx_widths(
8028
+ "{ <t> _r = creal<f>(#1); <t> _i = cimag<f>(#1); (#2) = _r * _r + _i * _i; }"))
7848
8029
 
7849
8030
  MkKernel.monop :conj,
7850
8031
  source: MkKernel::MATH_NUMERIC + [:object],
7851
8032
  expr: {
7852
8033
  numeric: "(#2) = (#1);",
7853
- complex: "(#2) = conj(#1);",
7854
8034
  object: '(#2) = rb_funcall((#1), rb_intern("conj"), 0);',
7855
- }
8035
+ }.merge(MkKernel.cmplx_widths("(#2) = conj<f>(#1);"))
7856
8036
 
7857
8037
  # arg: data_type-changing monop — phase angle of the complex plane.
7858
8038
  # Mathematically `arg(z)` for z = re + im*i is `atan2(im, re)` in
@@ -7863,11 +8043,11 @@ MkKernel.monop :conj,
7863
8043
  # Replaces the hand-written rb_ca_arg in ext/carray_numeric.c (which
7864
8044
  # was f64-only, float-or-complex parent, also computed `carg`).
7865
8045
  #
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.
8046
+ # Output data_type keeps the width the input carries its real values in:
8047
+ # a float stays that float, a complex demotes to its own real component
8048
+ # width (cmplx64 -> f32, cmplx128 -> f64). Integers are the exception
8049
+ # and go to f64, because pi does not fit any integer slot — preserving
8050
+ # the int data_type would silently truncate `arg(-1) = pi` to 3.
7871
8051
  #
7872
8052
  # 3.0 breaking (vs hand-written rb_ca_arg):
7873
8053
  # - integer input is now accepted (was a raise). Returns f64
@@ -7897,16 +8077,15 @@ MkKernel.monop :sign,
7897
8077
  MkKernel::UINT_DTYPES => "(#2) = ((#1) > 0) ? 1 : 0;",
7898
8078
  MkKernel::SINT_DTYPES => "(#2) = ((#1) > 0) - ((#1) < 0);",
7899
8079
  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
- }
8080
+ }.merge(MkKernel.cmplx_widths(
8081
+ "{ <t> _m = cabs<f>(#1); (#2) = (_m == 0) ? 0 : ((#1) / _m); }"))
7902
8082
 
7903
8083
  MkKernel.monop :arg,
7904
8084
  source: MkKernel::ALL_NUMERIC + MkKernel::CMPLX_DTYPES,
7905
- output: { numeric: :f64, complex: :f64 },
8085
+ output: { int: :f64, float: :preserve, complex: :real_of_source },
7906
8086
  expr: {
7907
8087
  numeric: "(#2) = carg((cmplx128_t)(#1));",
7908
- complex: "(#2) = carg(#1);",
7909
- }
8088
+ }.merge(MkKernel.cmplx_widths("(#2) = carg<f>(#1);"))
7910
8089
 
7911
8090
  # imag_i: data_type-preserving kernel that places the imag part in the
7912
8091
  # slot (= cimag for complex stores into the real component since cmplx
@@ -7920,9 +8099,8 @@ MkKernel.monop :imag_i,
7920
8099
  source: MkKernel::MATH_NUMERIC + [:object],
7921
8100
  expr: {
7922
8101
  numeric: "(#2) = 0;",
7923
- complex: "(#2) = cimag(#1);",
7924
8102
  object: '(#2) = rb_funcall((#1), rb_intern("imaginary"), 0);',
7925
- }
8103
+ }.merge(MkKernel.cmplx_widths("(#2) = cimag<f>(#1);"))
7926
8104
 
7927
8105
  # arg_i: data_type-preserving kernel that writes the complex argument
7928
8106
  # (phase angle) into the slot. For complex input, cassignment from a
@@ -7937,9 +8115,9 @@ MkKernel.monop :imag_i,
7937
8115
  MkKernel.monop :arg_i,
7938
8116
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES,
7939
8117
  expr: {
7940
- [:f32] => "(#2) = (float)carg((cmplx128_t)(#1));",
8118
+ [:f32] => "(#2) = cargf((cmplx64_t)(#1));",
7941
8119
  [:f64] => "(#2) = carg((cmplx128_t)(#1));",
7942
- [:cmplx64] => "(#2) = (float)carg((cmplx128_t)(#1));",
8120
+ [:cmplx64] => "(#2) = cargf(#1);",
7943
8121
  [:cmplx128] => "(#2) = carg(#1);",
7944
8122
  }
7945
8123
 
@@ -7995,7 +8173,8 @@ MkKernel.monfunc :rcp,
7995
8173
  expr: {
7996
8174
  int: "if ((#1)==0) {ca_zerodiv();}; (#2) = 1/(#1);",
7997
8175
  float: "(#2) = 1/(#1);",
7998
- complex: "(#2) = 1/(#1);",
8176
+ [:cmplx64] => "(#2) = op_crcp_cmplx64(#1);",
8177
+ [:cmplx128] => "(#2) = 1/(#1);",
7999
8178
  object: '(#2) = rb_funcall(INT2NUM(1), rb_intern("/"), 1, (#1));',
8000
8179
  }
8001
8180
 
@@ -8004,7 +8183,6 @@ MkKernel.monfunc :rcp,
8004
8183
  {
8005
8184
  sqrt: "sqrt",
8006
8185
  exp: "exp",
8007
- log: "log",
8008
8186
  sin: "sin",
8009
8187
  cos: "cos",
8010
8188
  tan: "tan",
@@ -8015,21 +8193,37 @@ MkKernel.monfunc :rcp,
8015
8193
  MkKernel.monfunc op,
8016
8194
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8017
8195
  expr: {
8018
- float: "(#2) = #{c_fn}(#1);",
8019
- complex: "(#2) = c#{c_fn}(#1);",
8020
8196
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
8021
- }
8197
+ }.merge(MkKernel.float_widths("(#2) = #{c_fn}<f>(#1);"))
8198
+ .merge(MkKernel.cmplx_widths("(#2) = c#{c_fn}<f>(#1);"))
8022
8199
  end
8023
8200
 
8201
+ # log is the one member of the family whose complex form cancels, so it
8202
+ # does not follow the others onto `clogf`. The real part of `clog(z)` is
8203
+ # `log|z|`, which vanishes on the unit circle; computing |z| at the
8204
+ # operand's width rounds away everything the logarithm then needs, and
8205
+ # the answer comes back with a relative error near 400 instead of 1e-7.
8206
+ # The complex branch therefore stays on the double-taking `clog`, the way
8207
+ # `*` and `/` do -- see ca_op_cmplx64.h. Real `log` has no such step:
8208
+ # its operand is the argument itself, and `logf` measures at one float
8209
+ # epsilon right through x = 1.
8210
+ MkKernel.monfunc :log,
8211
+ source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8212
+ expr: {
8213
+ complex: "(#2) = clog(#1);",
8214
+ object: MkKernel.obj_float_math("log(<v>)", "log"),
8215
+ }.merge(MkKernel.float_widths("(#2) = log<f>(#1);"))
8216
+
8024
8217
  # exp2 special case: complex variant uses cpow(2, x), not cexp2 (which
8025
8218
  # isn't standardized in C99/POSIX).
8026
8219
  MkKernel.monfunc :exp2,
8027
8220
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8028
8221
  expr: {
8029
- float: "(#2) = exp2(#1);",
8222
+ # cpow(a, z) is cexp(z * clog(a)), so it inherits clog's cancellation
8223
+ # and stays on the double-taking form for both complex data_types.
8030
8224
  complex: "(#2) = cpow(2, (#1));",
8031
8225
  object: MkKernel.obj_float_math("exp2(<v>)", "exp2"),
8032
- }
8226
+ }.merge(MkKernel.float_widths("(#2) = exp2<f>(#1);"))
8033
8227
 
8034
8228
  # log10, log2, logb: no complex variant in the original mkmath emit
8035
8229
  {
@@ -8040,9 +8234,8 @@ MkKernel.monfunc :exp2,
8040
8234
  MkKernel.monfunc op,
8041
8235
  source: MkKernel::FLOAT_DTYPES + [:object],
8042
8236
  expr: {
8043
- float: "(#2) = #{c_fn}(#1);",
8044
8237
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
8045
- }
8238
+ }.merge(MkKernel.float_widths("(#2) = #{c_fn}<f>(#1);"))
8046
8239
  end
8047
8240
 
8048
8241
  # exp10: special object expr (= bypass OBJ_FLOAT_MATH for the
@@ -8050,8 +8243,6 @@ end
8050
8243
  MkKernel.monfunc :exp10,
8051
8244
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8052
8245
  expr: {
8053
- float: "(#2) = pow(10, (#1));",
8054
- complex: "(#2) = cpow(10, (#1));",
8055
8246
  object: <<~SNIPPET,
8056
8247
  {
8057
8248
  VALUE _obj_arg = (#1);
@@ -8062,10 +8253,13 @@ MkKernel.monfunc :exp10,
8062
8253
  }
8063
8254
  }
8064
8255
  SNIPPET
8065
- }
8256
+ complex: "(#2) = cpow(10, (#1));", # see exp2 on why cpow stays wide
8257
+ }.merge(MkKernel.float_widths("(#2) = pow<f>(10, (#1));"))
8066
8258
 
8067
- # Hyperbolic family: complex variant uses the real-typed C function
8068
- # (matches original mkmath emit, which doesn't prefix `c`).
8259
+ # Hyperbolic family: float uses the real-typed C function, complex uses
8260
+ # the C99 `c`-prefixed one. Passing a `double _Complex` to `sinh(double)`
8261
+ # discards the imaginary part, so the complex branch must not share the
8262
+ # real-typed name.
8069
8263
  {
8070
8264
  sinh: "sinh",
8071
8265
  cosh: "cosh",
@@ -8077,10 +8271,9 @@ MkKernel.monfunc :exp10,
8077
8271
  MkKernel.monfunc op,
8078
8272
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8079
8273
  expr: {
8080
- float: "(#2) = #{c_fn}(#1);",
8081
- complex: "(#2) = #{c_fn}(#1);",
8082
8274
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
8083
- }
8275
+ }.merge(MkKernel.float_widths("(#2) = #{c_fn}<f>(#1);"))
8276
+ .merge(MkKernel.cmplx_widths("(#2) = c#{c_fn}<f>(#1);"))
8084
8277
  end
8085
8278
 
8086
8279
  # ---- M.1 (PyTorch alignment): additional monfunc / monop ------------------
@@ -8094,19 +8287,17 @@ end
8094
8287
  MkKernel.monfunc op,
8095
8288
  source: MkKernel::FLOAT_DTYPES + [:object],
8096
8289
  expr: {
8097
- float: "(#2) = #{c_fn}(#1);",
8098
8290
  object: MkKernel.obj_float_math("#{c_fn}(<v>)", c_fn),
8099
- }
8291
+ }.merge(MkKernel.float_widths("(#2) = #{c_fn}<f>(#1);"))
8100
8292
  end
8101
8293
 
8102
8294
  # rsqrt: 1 / sqrt(x). float + complex + object (complex via 1.0 / csqrt).
8103
8295
  MkKernel.monfunc :rsqrt,
8104
8296
  source: MkKernel::FLOAT_DTYPES + MkKernel::CMPLX_DTYPES + [:object],
8105
8297
  expr: {
8106
- float: "(#2) = 1.0 / sqrt(#1);",
8107
- complex: "(#2) = 1.0 / csqrt(#1);",
8108
8298
  object: MkKernel.obj_float_math("1.0 / sqrt(<v>)", "rsqrt"),
8109
- }
8299
+ }.merge(MkKernel.float_widths("(#2) = ((<t>)1.0) / sqrt<f>(#1);"))
8300
+ .merge(MkKernel.cmplx_widths("(#2) = ((<t>)1.0) / csqrt<f>(#1);"))
8110
8301
 
8111
8302
  # trunc: toward-zero rounding. Preserve-data_type form like ceil / floor /
8112
8303
  # round — int branch is identity, float branch uses C99 trunc, object
@@ -8136,7 +8327,7 @@ MkKernel.monop :square,
8136
8327
  # with f64-forced input/output via ca_call_cfunc_1_1; the mkkernel form
8137
8328
  # preserves the same numeric behaviour but rides the lazy substrate +
8138
8329
  # kernel_iterator engine. Widening monfunc: integer input auto-casts
8139
- # to f64, float input preserves dtype.
8330
+ # to f64, float input preserves data type.
8140
8331
 
8141
8332
  # deg_360: fold into [0, 360). Use double-typed local for the fold
8142
8333
  # computation regardless of input precision (matches legacy hand-written
@@ -8315,12 +8506,12 @@ MkKernel.binop :minimum,
8315
8506
  object: '(#3) = rb_funcall(rb_assoc_new((#1),(#2)), rb_intern("min"), 0);',
8316
8507
  }
8317
8508
 
8318
- # +, -, * use the same generic expression across all numeric data_types.
8509
+ # + and - work on the parts independently, so one generic expression
8510
+ # covers every numeric data_type at any width.
8319
8511
  {
8320
- add: ["+", "+", '"+"'],
8321
- sub: ["-", "-", '"-"'],
8322
- mul: ["*", "*", '"*"'],
8323
- }.each do |name, (op, c_op, _ruby_op)|
8512
+ add: ["+", "+"],
8513
+ sub: ["-", "-"],
8514
+ }.each do |name, (op, c_op)|
8324
8515
  MkKernel.binop name,
8325
8516
  op: op,
8326
8517
  source: MkKernel::MATH_NUMERIC + [:object],
@@ -8331,13 +8522,48 @@ MkKernel.binop :minimum,
8331
8522
  }
8332
8523
  end
8333
8524
 
8525
+ # * does not: a complex product subtracts two products of the parts, and
8526
+ # that subtraction cancels. A cmplx64 gets it in double, where there are
8527
+ # bits left underneath -- see ca_op_cmplx64.h. Unlike the divide this
8528
+ # costs speed rather than saving it, and is paid for the accuracy alone.
8529
+ MkKernel.binop :mul,
8530
+ op: "*",
8531
+ source: MkKernel::MATH_NUMERIC + [:object],
8532
+ expr: {
8533
+ numeric: "(#3) = (#1) * (#2);",
8534
+ [:cmplx64] => "(#3) = op_cmul_cmplx64((#1), (#2));",
8535
+ [:cmplx128] => "(#3) = (#1) * (#2);",
8536
+ object: '(#3) = rb_funcall((#1), rb_intern("*"), 1, (#2));',
8537
+ }
8538
+
8539
+ # div: `/`. Integer division floors toward -inf (= Ruby `Integer#/` and
8540
+ # NumPy `floor_divide`), so that `(a / b) * b + a % b == a` holds for
8541
+ # every sign combination. C's `/` truncates toward zero, so the signed
8542
+ # lanes correct the quotient by one when the division is inexact and the
8543
+ # operands have opposite signs. Unsigned operands can never disagree in
8544
+ # sign, so they keep the bare C form. Float `/` is true division and is
8545
+ # unchanged (matching Ruby `Float#/`); the identity above therefore holds
8546
+ # for integers only, exactly as in Ruby.
8334
8547
  MkKernel.binop :div,
8335
8548
  op: "/",
8336
8549
  source: MkKernel::MATH_NUMERIC + [:object],
8337
8550
  expr: {
8338
- int: "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) / (#2);",
8551
+ MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) / (#2);",
8552
+ int: %{
8553
+ if ((#2)==0) {ca_zerodiv();};
8554
+ {
8555
+ <type> _q = (#1) / (#2);
8556
+ if ( (#1) % (#2) != 0 && (((#1) < 0) != ((#2) < 0)) ) { _q -= 1; }
8557
+ (#3) = _q;
8558
+ }
8559
+ },
8339
8560
  float: "(#3) = (#1) / (#2);",
8340
- complex: "(#3) = (#1) / (#2);",
8561
+ # A cmplx64 divide is computed in double and rounded once: see
8562
+ # ca_op_cmplx64.h for why that is both faster and correctly rounded,
8563
+ # and how Annex G survives it. cmplx128 has no wider type to
8564
+ # borrow, so it stays on the compiler's helper.
8565
+ [:cmplx64] => "(#3) = op_cdiv_cmplx64((#1), (#2));",
8566
+ [:cmplx128] => "(#3) = (#1) / (#2);",
8341
8567
  object: '(#3) = rb_funcall((#1), rb_intern("/"), 1, (#2));',
8342
8568
  }
8343
8569
 
@@ -8357,27 +8583,57 @@ MkKernel.binop :rcp_mul,
8357
8583
  expr: {
8358
8584
  int: "if ((#1)==0) {ca_zerodiv();}; (#3) = (#2) / (#1);",
8359
8585
  float: "(#3) = (#2) / (#1);",
8360
- complex: "(#3) = (#2) / (#1);",
8586
+ [:cmplx64] => "(#3) = op_cdiv_cmplx64((#2), (#1));",
8587
+ [:cmplx128] => "(#3) = (#2) / (#1);",
8361
8588
  object: '(#3) = rb_funcall((#2), rb_intern("/"), 1, (#1));',
8362
8589
  }
8363
8590
 
8591
+ # mod: `%`. Floored modulo -- the result carries the sign of the divisor
8592
+ # (= Ruby `%` and NumPy `np.mod`), and pairs with the floored `/` above.
8593
+ # C's `%` and `fmod` carry the sign of the dividend instead, so the signed
8594
+ # lanes add the divisor back when the remainder is non-zero and disagrees
8595
+ # in sign with it. A zero remainder is given the divisor's sign so the
8596
+ # rule holds without exception (float only; integers have no signed zero).
8597
+ # Unsigned operands already satisfy the rule. The truncating form stays
8598
+ # available as `fmod`.
8364
8599
  MkKernel.binop :mod,
8365
8600
  op: "%",
8366
8601
  source: MkKernel::ALL_NUMERIC + [:object],
8367
8602
  expr: {
8368
- int: "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
8369
- float: "(#3) = fmod(#1, #2);",
8603
+ MkKernel::UINT_DTYPES => "if ((#2)==0) {ca_zerodiv();}; (#3) = (#1) % (#2);",
8604
+ int: %{
8605
+ if ((#2)==0) {ca_zerodiv();};
8606
+ {
8607
+ <type> _r = (#1) % (#2);
8608
+ if ( _r != 0 && ((_r < 0) != ((#2) < 0)) ) { _r += (#2); }
8609
+ (#3) = _r;
8610
+ }
8611
+ },
8612
+ float: %{
8613
+ {
8614
+ <type> _r = fmod((#1), (#2));
8615
+ if ( _r != 0 ) {
8616
+ if ( (_r < 0) != ((#2) < 0) ) { _r += (#2); }
8617
+ }
8618
+ else { _r = copysign((<type>) 0, (#2)); }
8619
+ (#3) = _r;
8620
+ }
8621
+ },
8370
8622
  object: '(#3) = rb_funcall((#1), rb_intern("%"), 1, (#2));',
8371
8623
  }
8372
8624
 
8373
- MkKernel.binop :reminder,
8374
- op: "reminder",
8625
+ # fmod: the truncating counterpart of `%` -- the remainder carries the
8626
+ # sign of the dividend, as C's `fmod` and `%` do. Defined over integers
8627
+ # too (`%` no longer reaches that form since it floors), where it is C's
8628
+ # `%` with the same zero-divisor guard. Ruby's `Numeric#remainder` is
8629
+ # this same operation, so the object lane delegates to it and keeps the
8630
+ # element's class (an Integer stays an Integer).
8631
+ MkKernel.binop :fmod,
8375
8632
  source: MkKernel::ALL_NUMERIC + [:object],
8376
8633
  expr: {
8377
8634
  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
- }
8635
+ object: '(#3) = rb_funcall((#1), rb_intern("remainder"), 1, (#2));',
8636
+ }.merge(MkKernel.float_widths("(#3) = fmod<f>((#1), (#2));"))
8381
8637
 
8382
8638
  MkKernel.binop :bit_and_i,
8383
8639
  op: "&",
@@ -8482,6 +8738,7 @@ MkKernel.alias_binop :bit_rshift, :">>"
8482
8738
  # pow / cpow. Object uses Ruby's `**`.
8483
8739
  MkKernel.header_block <<~C
8484
8740
  #include "ca_op_powi.h"
8741
+ #include "ca_op_cmplx64.h"
8485
8742
  C
8486
8743
 
8487
8744
  # ---- triop family ---------------------------------------------------------
@@ -8554,10 +8811,9 @@ MkKernel.binop :power,
8554
8811
  source: MkKernel::MATH_NUMERIC + [:object],
8555
8812
  expr: {
8556
8813
  int: "(#3) = op_powi_<type>((#1), (#2));",
8557
- float: "(#3) = pow((#1), (#2));",
8558
- complex: "(#3) = cpow((#1), (#2));",
8814
+ complex: "(#3) = cpow((#1), (#2));", # see exp2 on why cpow stays wide
8559
8815
  object: '(#3) = rb_funcall((#1), rb_intern("**"), 1, (#2));',
8560
- }
8816
+ }.merge(MkKernel.float_widths("(#3) = pow<f>((#1), (#2));"))
8561
8817
 
8562
8818
  # ---- M.2 + M.3 (PyTorch alignment): float-only binop family --------------
8563
8819
  #
@@ -8573,7 +8829,6 @@ MkKernel.binop :power,
8573
8829
  copysign: ["copysign", "copysign"],
8574
8830
  logaddexp: [nil, nil], # custom expr — see below
8575
8831
  nextafter: ["nextafter", "nextafter"],
8576
- fmod: ["fmod", "fmod"],
8577
8832
  atan2: ["atan2", "atan2"],
8578
8833
  hypot: ["hypot", "hypot"],
8579
8834
  }.each do |op_name, (c_fn, ruby_fb)|
@@ -8581,7 +8836,6 @@ MkKernel.binop :power,
8581
8836
  MkKernel.binop op_name,
8582
8837
  source: MkKernel::FLOAT_DTYPES + [:object],
8583
8838
  expr: {
8584
- float: "(#3) = #{c_fn}((#1), (#2));",
8585
8839
  object: <<~SNIPPET,
8586
8840
  {
8587
8841
  VALUE _l = (#1);
@@ -8594,7 +8848,7 @@ MkKernel.binop :power,
8594
8848
  }
8595
8849
  }
8596
8850
  SNIPPET
8597
- }
8851
+ }.merge(MkKernel.float_widths("(#3) = #{c_fn}<f>((#1), (#2));"))
8598
8852
  end
8599
8853
 
8600
8854
  # logaddexp: log(exp(x) + exp(y)). Numerically stable form:
@@ -8603,7 +8857,6 @@ end
8603
8857
  MkKernel.binop :logaddexp,
8604
8858
  source: MkKernel::FLOAT_DTYPES + [:object],
8605
8859
  expr: {
8606
- float: "(#3) = fmax((#1), (#2)) + log1p(exp(-fabs((#1) - (#2))));",
8607
8860
  object: <<~SNIPPET,
8608
8861
  {
8609
8862
  VALUE _l = (#1);
@@ -8619,7 +8872,8 @@ MkKernel.binop :logaddexp,
8619
8872
  }
8620
8873
  }
8621
8874
  SNIPPET
8622
- }
8875
+ }.merge(MkKernel.float_widths(
8876
+ "(#3) = fmax<f>((#1), (#2)) + log1p<f>(exp<f>(-fabs<f>((#1) - (#2))));"))
8623
8877
 
8624
8878
  # ---- P.5b.4: moncmp family (predicates returning bool) ----------------
8625
8879
 
@@ -8669,7 +8923,7 @@ MkKernel.moncmp :is_invalid,
8669
8923
  # -0.0). Integer branch: sint = (#1) < 0, uint = always 0. Float branch
8670
8924
  # uses C99 signbit (handles -0.0 / NaN sign correctly). No complex
8671
8925
  # variant (signbit on a complex is ambiguous; rejected at this layer).
8672
- # `:sint` / `:uint` aren't family aliases — use array-of-dtypes form.
8926
+ # `:sint` / `:uint` aren't family aliases — use the array-of-types form.
8673
8927
  MkKernel.moncmp :signbit,
8674
8928
  source: MkKernel::SINT_DTYPES + MkKernel::UINT_DTYPES +
8675
8929
  MkKernel::FLOAT_DTYPES + [:object],