carray 3.0.4 → 3.0.6

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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +438 -0
  3. data/README.md +1 -1
  4. data/carray.gemspec +3 -2
  5. data/ext/ca_array_pool.c +25 -1
  6. data/ext/ca_axis_dispatch.c +19 -20
  7. data/ext/ca_axis_group.c +1 -1
  8. data/ext/ca_categorical_iterator.c +34 -27
  9. data/ext/ca_compare.c +4 -4
  10. data/ext/ca_composite_dispatch.c +9 -9
  11. data/ext/ca_kernel_iterator.c +122 -237
  12. data/ext/ca_kernel_iterator.h +522 -337
  13. data/ext/ca_obj_array.c +69 -19
  14. data/ext/ca_obj_bincmp.c +10 -1
  15. data/ext/ca_obj_binop.c +11 -1
  16. data/ext/ca_obj_bitarray.c +5 -1
  17. data/ext/ca_obj_bitfield.c +60 -43
  18. data/ext/ca_obj_byte_swap.c +4 -1
  19. data/ext/ca_obj_const_string.c +15 -3
  20. data/ext/ca_obj_face.c +44 -1
  21. data/ext/ca_obj_fake.c +3 -1
  22. data/ext/ca_obj_field.c +13 -3
  23. data/ext/ca_obj_grid.c +219 -17
  24. data/ext/ca_obj_meld.c +69 -113
  25. data/ext/ca_obj_moncmp.c +9 -1
  26. data/ext/ca_obj_monop.c +86 -12
  27. data/ext/ca_obj_object.c +118 -32
  28. data/ext/ca_obj_record.c +3 -0
  29. data/ext/ca_obj_reduce.c +19 -1
  30. data/ext/ca_obj_refer.c +80 -26
  31. data/ext/ca_obj_remap.c +69 -10
  32. data/ext/ca_obj_roll.c +27 -6
  33. data/ext/ca_obj_select.c +38 -6
  34. data/ext/ca_obj_select_axis.c +1 -0
  35. data/ext/ca_obj_shift.c +32 -7
  36. data/ext/ca_obj_stack.c +311 -111
  37. data/ext/ca_obj_stride.c +159 -64
  38. data/ext/ca_obj_string.c +27 -2
  39. data/ext/ca_obj_tile.c +28 -7
  40. data/ext/ca_obj_transpose.c +8 -4
  41. data/ext/ca_obj_triop.c +12 -1
  42. data/ext/ca_obj_window.c +80 -50
  43. data/ext/ca_op_ipower.c +12 -12
  44. data/ext/ca_sort_kernels.h +6 -3
  45. data/ext/caframe_csv_reader.c +1 -1
  46. data/ext/carray.h +35 -1
  47. data/ext/carray_access.c +43 -67
  48. data/ext/carray_address_basis.c +134 -0
  49. data/ext/carray_attribute.c +32 -0
  50. data/ext/carray_broadcast.c +3 -4
  51. data/ext/carray_build_flags.h +1 -1
  52. data/ext/carray_cast.c +251 -93
  53. data/ext/carray_cast_func.rb +56 -26
  54. data/ext/carray_conversion.c +11 -7
  55. data/ext/carray_core.c +814 -72
  56. data/ext/carray_count.c +6 -0
  57. data/ext/carray_element.c +22 -13
  58. data/ext/carray_factorize.c +20 -20
  59. data/ext/carray_generate.c +8 -2
  60. data/ext/carray_histogram.c +26 -4
  61. data/ext/carray_index_classifier.c +4 -4
  62. data/ext/carray_internal.h +159 -12
  63. data/ext/carray_kernels_bincmp.c +2174 -170
  64. data/ext/carray_kernels_binop.c +25653 -5487
  65. data/ext/carray_kernels_init.c +8 -8
  66. data/ext/carray_kernels_moncmp.c +96 -96
  67. data/ext/carray_kernels_monop.c +12253 -5235
  68. data/ext/carray_kernels_reduce_aggregate.c +6150 -2423
  69. data/ext/carray_kernels_reduce_boolean.c +1533 -15
  70. data/ext/carray_kernels_reduce_cumulative.c +2442 -882
  71. data/ext/carray_kernels_reduce_extreme.c +5658 -1661
  72. data/ext/carray_kernels_reduce_variance.c +2534 -402
  73. data/ext/carray_kernels_scan.c +368 -368
  74. data/ext/carray_kernels_search.c +4551 -604
  75. data/ext/carray_kernels_sort.c +659 -751
  76. data/ext/carray_kernels_triop.c +114 -114
  77. data/ext/carray_key_windows.c +4 -0
  78. data/ext/carray_lazy.c +47 -0
  79. data/ext/carray_loop.c +1 -1
  80. data/ext/carray_mask.c +175 -161
  81. data/ext/carray_median_percentile.c +4 -4
  82. data/ext/carray_memory_view.c +15 -9
  83. data/ext/carray_operator.c +152 -49
  84. data/ext/carray_order.c +230 -10
  85. data/ext/carray_random.c +22 -19
  86. data/ext/carray_scatter.c +24 -7
  87. data/ext/carray_slab.c +128 -29
  88. data/ext/carray_sort.c +224 -16
  89. data/ext/carray_sort_kernel.c +5 -7
  90. data/ext/carray_struct.c +35 -43
  91. data/ext/carray_test.c +74 -19
  92. data/ext/carray_time_text.c +6 -4
  93. data/ext/carray_utils.c +63 -21
  94. data/ext/extconf.rb +11 -0
  95. data/ext/mkkernel.rb +546 -199
  96. data/ext/ruby_carray.c +4 -1
  97. data/ext/version.h +4 -4
  98. data/lib/carray/attribute.rb +42 -5
  99. data/lib/carray/autoload_carray.rb +3 -3
  100. data/lib/carray/autoload_method_extension.rb +5 -3
  101. data/lib/carray/axis_group.rb +13 -7
  102. data/lib/carray/basics.rb +28 -8
  103. data/lib/carray/block_iterator.rb +17 -3
  104. data/lib/carray/categorical.rb +35 -23
  105. data/lib/carray/categorical_iterator.rb +8 -8
  106. data/lib/carray/complex.rb +2 -2
  107. data/lib/carray/conditional.rb +3 -2
  108. data/lib/carray/const_string.rb +10 -0
  109. data/lib/carray/data_type_extension.rb +8 -1
  110. data/lib/carray/fixlen_string.rb +41 -2
  111. data/lib/carray/frame/column_verbs.rb +48 -16
  112. data/lib/carray/frame/concat.rb +17 -3
  113. data/lib/carray/frame/convert.rb +19 -0
  114. data/lib/carray/frame/csv_parser.rb +32 -32
  115. data/lib/carray/frame/describe.rb +4 -2
  116. data/lib/carray/frame/frame.rb +405 -85
  117. data/lib/carray/frame/group.rb +777 -64
  118. data/lib/carray/frame/io.rb +212 -72
  119. data/lib/carray/frame/join.rb +146 -37
  120. data/lib/carray/frame/records.rb +3 -3
  121. data/lib/carray/frame/reshape.rb +89 -11
  122. data/lib/carray/frame/sort.rb +57 -9
  123. data/lib/carray/frame/verbs.rb +384 -49
  124. data/lib/carray/frame/windows.rb +44 -6
  125. data/lib/carray/fuse_source.rb +31 -2
  126. data/lib/carray/inspect.rb +7 -2
  127. data/lib/carray/iterator.rb +32 -0
  128. data/lib/carray/lazy.rb +52 -8
  129. data/lib/carray/mask_gap_fill.rb +37 -17
  130. data/lib/carray/meld_reduce.rb +1 -1
  131. data/lib/carray/methods/choose.rb +3 -2
  132. data/lib/carray/methods/is_in.rb +8 -1
  133. data/lib/carray/methods/meshgrid.rb +3 -1
  134. data/lib/carray/runtime.rb +47 -8
  135. data/lib/carray/segment_iterator.rb +35 -16
  136. data/lib/carray/serialize.rb +53 -19
  137. data/lib/carray/stack.rb +33 -18
  138. data/lib/carray/string.rb +8 -0
  139. data/lib/carray/string_operation_extension.rb +13 -5
  140. data/lib/carray/struct_builder.rb +5 -3
  141. data/lib/carray/time.rb +112 -68
  142. data/lib/carray/window_iterator.rb +16 -4
  143. data/yard-stubs/ca_obj_array.rb +26 -1
  144. data/yard-stubs/ca_obj_transpose.rb +5 -4
  145. data/yard-stubs/carray_attribute.rb +3 -1
  146. data/yard-stubs/carray_cast.rb +10 -5
  147. data/yard-stubs/carray_conversion.rb +13 -3
  148. data/yard-stubs/carray_element.rb +7 -6
  149. data/yard-stubs/carray_mask.rb +28 -4
  150. data/yard-stubs/carray_math.rb +45 -20
  151. data/yard-stubs/carray_memory_view.rb +7 -0
  152. data/yard-stubs/carray_order.rb +100 -4
  153. data/yard-stubs/carray_scatter.rb +7 -5
  154. data/yard-stubs/carray_test.rb +7 -4
  155. metadata +18 -4
data/ext/mkkernel.rb CHANGED
@@ -44,26 +44,29 @@ module MkKernel
44
44
  # value_arg: dispatchers (CF.1) to cast a positional Ruby argument to
45
45
  # the kernel's T_IN type. Signed integers use NUM2LL, unsigned
46
46
  # NUM2ULL, floats NUM2DBL, with the dispatcher adding the per-type cast.
47
+ # `c` of a multi-byte type is its unaligned typedef from carray.h
48
+ # (ca_u_float64_t): a view's cells may sit at any byte, and a kernel
49
+ # reads them through `c *`.
47
50
  DTYPES = {
48
51
  i8: { c: "int8_t", ca: "CA_INT8", ruby: "LL2NUM", num2c: "NUM2LL",
49
52
  limit_hi: "INT8_MAX", limit_lo: "INT8_MIN" },
50
53
  u8: { c: "uint8_t", ca: "CA_UINT8", ruby: "ULL2NUM", num2c: "NUM2ULL",
51
54
  limit_hi: "UINT8_MAX", limit_lo: "0" },
52
- i16: { c: "int16_t", ca: "CA_INT16", ruby: "LL2NUM", num2c: "NUM2LL",
55
+ i16: { c: "ca_u_int16_t", ca: "CA_INT16", ruby: "LL2NUM", num2c: "NUM2LL",
53
56
  limit_hi: "INT16_MAX", limit_lo: "INT16_MIN" },
54
- u16: { c: "uint16_t", ca: "CA_UINT16", ruby: "ULL2NUM", num2c: "NUM2ULL",
57
+ u16: { c: "ca_u_uint16_t", ca: "CA_UINT16", ruby: "ULL2NUM", num2c: "NUM2ULL",
55
58
  limit_hi: "UINT16_MAX", limit_lo: "0" },
56
- i32: { c: "int32_t", ca: "CA_INT32", ruby: "LL2NUM", num2c: "NUM2LL",
59
+ i32: { c: "ca_u_int32_t", ca: "CA_INT32", ruby: "LL2NUM", num2c: "NUM2LL",
57
60
  limit_hi: "INT32_MAX", limit_lo: "INT32_MIN" },
58
- u32: { c: "uint32_t", ca: "CA_UINT32", ruby: "ULL2NUM", num2c: "NUM2ULL",
61
+ u32: { c: "ca_u_uint32_t", ca: "CA_UINT32", ruby: "ULL2NUM", num2c: "NUM2ULL",
59
62
  limit_hi: "UINT32_MAX", limit_lo: "0" },
60
- i64: { c: "int64_t", ca: "CA_INT64", ruby: "LL2NUM", num2c: "NUM2LL",
63
+ i64: { c: "ca_u_int64_t", ca: "CA_INT64", ruby: "LL2NUM", num2c: "NUM2LL",
61
64
  limit_hi: "INT64_MAX", limit_lo: "INT64_MIN" },
62
- u64: { c: "uint64_t", ca: "CA_UINT64", ruby: "ULL2NUM", num2c: "NUM2ULL",
65
+ u64: { c: "ca_u_uint64_t", ca: "CA_UINT64", ruby: "ULL2NUM", num2c: "NUM2ULL",
63
66
  limit_hi: "UINT64_MAX", limit_lo: "0" },
64
- f32: { c: "float", ca: "CA_FLOAT32", ruby: "rb_float_new", num2c: "NUM2DBL",
67
+ f32: { c: "ca_u_float32_t", ca: "CA_FLOAT32", ruby: "rb_float_new", num2c: "NUM2DBL",
65
68
  limit_hi: "INFINITY", limit_lo: "-INFINITY" },
66
- f64: { c: "double", ca: "CA_FLOAT64", ruby: "rb_float_new", num2c: "NUM2DBL",
69
+ f64: { c: "ca_u_float64_t", ca: "CA_FLOAT64", ruby: "rb_float_new", num2c: "NUM2DBL",
67
70
  limit_hi: "INFINITY", limit_lo: "-INFINITY" },
68
71
  # E.6a (2026-06-03): boolean as a first-class source DTYPE.
69
72
  # Intentionally NOT included in ALL_NUMERIC -- kernels opt boolean in
@@ -90,11 +93,11 @@ module MkKernel
90
93
  # CA_NTYPE coverage parity with mkmath, mkkernel needs to know the
91
94
  # complex and object data_types too. These entries are NOT included in
92
95
  # ALL_NUMERIC; pick up via MATH_TYPES / MATH_NUMERIC aliases.
93
- cmplx64: { c: "cmplx64_t", ca: "CA_CMPLX64", ruby: "CC2NUM",
96
+ cmplx64: { c: "ca_u_cmplx64_t", ca: "CA_CMPLX64", ruby: "CC2NUM",
94
97
  num2c: "NUM2CC", limit_hi: "0", limit_lo: "0" },
95
- cmplx128: { c: "cmplx128_t", ca: "CA_CMPLX128", ruby: "CC2NUM",
98
+ cmplx128: { c: "ca_u_cmplx128_t", ca: "CA_CMPLX128", ruby: "CC2NUM",
96
99
  num2c: "NUM2CC", limit_hi: "0", limit_lo: "0" },
97
- object: { c: "VALUE", ca: "CA_OBJECT", ruby: "(VALUE)",
100
+ object: { c: "VALUE", ca: "CA_OBJECT", ruby: "(VALUE)",
98
101
  num2c: "(VALUE)", limit_hi: "0", limit_lo: "0" },
99
102
  # P.5b.4: fixlen for bincmp (= match, eq/ne/gt/lt/ge/le against
100
103
  # fixed-byte strings). The c: type is `char` because fixlen kernels
@@ -325,8 +328,8 @@ module MkKernel
325
328
  # # :match_source (legacy) reads the weight at
326
329
  # # the source type -- wrong for fractional
327
330
  # # weights, kept only for DSL forward-compat.
328
- # shape: :rev5_strict, # W-A1 scalar / W-A2 1-D axis-broadcast / W-A3
329
- # # same-shape (:match_source = W-A3 only)
331
+ # shape: :rev5_strict, # W-A1 scalar / W-A4 shape of the reduced axes /
332
+ # # W-A3 self.shape (:match_source = W-A3 only)
330
333
  # mask: :overlay, # weights.mask OR'd into self.mask before
331
334
  # # kernel call (= legacy parity, Q3 (A))
332
335
  # }
@@ -359,6 +362,13 @@ module MkKernel
359
362
  object_escape: nil,
360
363
  identity_on_empty: false,
361
364
  all_nan_result: nil,
365
+ undefined_if: nil,
366
+ # settled: a C expression over `acc` that is true once no
367
+ # further cell can change the result (any: "acc", all:
368
+ # "!acc"). The slab walk then stops at the first block
369
+ # where it holds (CA_SLAB_REDUCE_T_SETTLE). Only for a
370
+ # single-accumulator reduction without mask_policy.
371
+ settled: nil,
362
372
  outputs: 1,
363
373
  # Two-pass centred algorithm (variance / stddev family).
364
374
  # When algorithm: :two_pass_centred is set, `state / init /
@@ -445,6 +455,10 @@ module MkKernel
445
455
  raise "#{name}: identity_on_empty requires a single-accumulator reduction (sum/prod/count/accumulate/wsum); mean/variance/wmean/minmax are excluded" \
446
456
  unless single_acc && outputs == 1
447
457
  end
458
+ if settled
459
+ raise "#{name}: settled requires a single-accumulator reduction without mask_policy, value_arg or array_arg" \
460
+ unless state.nil? && mask_policy.nil? && value_arg.nil? && array_arg.nil? && outputs == 1
461
+ end
448
462
  # SL.1.0: reduction_kind: licenses a SIMD-friendly contig-branch macro
449
463
  # variant (CA_SLAB_REDUCE_T_{PLUS,MIN,MAX,STAR,VAR}_EX). default
450
464
  # :none keeps the existing CA_SLAB_REDUCE_T_EX path (= unchanged
@@ -488,6 +502,14 @@ module MkKernel
488
502
  raise "#{name}: all_nan_result: :undef needs a mask_policy " \
489
503
  "(UNDEF is written through the mask_policy trigger)"
490
504
  end
505
+ # undefined_if: a C expression over the state that, when true for a
506
+ # slab, leaves the answer undefined though cells contributed -- wmean's
507
+ # weights summing to 0, whose quotient has no value. It is folded into
508
+ # the mask_policy trigger, so the cell is UNDEF (or fill_value).
509
+ if undefined_if && mask_policy.nil?
510
+ raise "#{name}: undefined_if needs a mask_policy " \
511
+ "(UNDEF is written through the mask_policy trigger)"
512
+ end
491
513
  if value_arg
492
514
  raise "#{name}: value_arg must be a Hash" unless value_arg.is_a?(Hash)
493
515
  raise "#{name}: value_arg: target must be :T_IN" unless value_arg[:target] == :T_IN
@@ -567,9 +589,9 @@ module MkKernel
567
589
  raise "#{name}: array_arg: data_type must be :promote or :match_source" \
568
590
  unless [:promote, :match_source].include?(array_arg[:data_type])
569
591
  # PROPOSAL_REDUCTION_PER_FIBER_AUX_OPERAND rev1: shape acceptance expanded
570
- # to W-A1 (scalar) / W-A2 (1-D axis-broadcast) / W-A3 (same shape).
592
+ # to W-A1 (scalar) / W-A4 (shape of the reduced axes) / W-A3 (self.shape).
571
593
  # :match_source = legacy W-A3 only (kept for forward-compat in DSL).
572
- # :rev5_strict = W-A1 + W-A2 + W-A3 acceptance (= new default for wsum/wmean).
594
+ # :rev5_strict = W-A1 + W-A4 + W-A3 acceptance (= new default for wsum/wmean).
573
595
  raise "#{name}: array_arg: shape must be :match_source or :rev5_strict" \
574
596
  unless [:match_source, :rev5_strict].include?(array_arg[:shape])
575
597
  raise "#{name}: array_arg: mask must be :overlay (W.1 MVP)" \
@@ -649,6 +671,8 @@ module MkKernel
649
671
  object_escape: object_escape,
650
672
  identity_on_empty: identity_on_empty,
651
673
  all_nan_result: all_nan_result,
674
+ undefined_if: undefined_if,
675
+ settled: settled,
652
676
  outputs: outputs,
653
677
  algorithm: algorithm,
654
678
  divisor: divisor,
@@ -1054,11 +1078,14 @@ module MkKernel
1054
1078
  # => ...` form).
1055
1079
  #
1056
1080
  # `bind:` defaults to true (= register CArray#<name> + CArray#<name>!).
1081
+ # `bang: false` leaves CArray#<name>! unregistered too, for a method whose
1082
+ # Ruby surface is defined by hand on top of the generated rb_ca_<name> and
1083
+ # rb_ca_<name>_bang.
1057
1084
  # `op:` defaults to name (= the public method name; some mkmath ops
1058
1085
  # use a different operator name from the kernel suffix, e.g. operator
1059
1086
  # methods).
1060
- def self.monop(name, source:, expr:, op: nil, bind: true, cmath: true,
1061
- widening: nil, output: :preserve)
1087
+ def self.monop(name, source:, expr:, op: nil, bind: true, bang: true,
1088
+ cmath: true, widening: nil, output: :preserve)
1062
1089
  op ||= name
1063
1090
  # widening = true => emit wrapper that auto-casts integer input to
1064
1091
  # CA_FLOAT64 (= mkmath monfunc behavior). Default: auto-detect based
@@ -1081,6 +1108,7 @@ module MkKernel
1081
1108
  source: source,
1082
1109
  expr: expr,
1083
1110
  bind: bind,
1111
+ bang: bang,
1084
1112
  cmath: cmath,
1085
1113
  widening: widening,
1086
1114
  output: output,
@@ -1092,8 +1120,10 @@ module MkKernel
1092
1120
  # of integer input to f64 is decided by `monop`'s widening auto-detect
1093
1121
  # based on whether `source:` includes integer data_types — matching mkmath's
1094
1122
  # rule "if no INT_TYPES key, auto-cast; otherwise rely on integer kernel".
1095
- def self.monfunc(name, source:, expr:, op: nil, bind: true, cmath: true)
1096
- monop(name, source: source, expr: expr, op: op, bind: bind, cmath: cmath)
1123
+ def self.monfunc(name, source:, expr:, op: nil, bind: true, bang: true,
1124
+ cmath: true)
1125
+ monop(name, source: source, expr: expr, op: op, bind: bind, bang: bang,
1126
+ cmath: cmath)
1097
1127
  end
1098
1128
 
1099
1129
  # Register an alias of an existing monop method under a different
@@ -1355,6 +1385,12 @@ module MkKernel
1355
1385
  when "_STAR" then "CA_SLAB_REDUCE_STAR8_EX"
1356
1386
  end
1357
1387
  acc_c = resolve_state_type(k[:state][acc_var], oi, si, src)
1388
+ # An 8- or 16-bit integer cell converted to double does not vectorize;
1389
+ # the narrow form sums blocks in int32 first.
1390
+ if macro8 == "CA_SLAB_REDUCE_SUM8_EX" && %i[i8 u8 i16 u16].include?(src) &&
1391
+ acc_c == DTYPES[:f64][:c]
1392
+ macro8 = "CA_SLAB_REDUCE_SUM8_NARROW_EX"
1393
+ end
1358
1394
  expr_macro = "__#{k[:name]}_#{src}_EXPR"
1359
1395
  io.puts "#define #{expr_macro}(__x) ((#{acc_c})(__x))"
1360
1396
  io.puts "#{indent}#{macro8}(#{si[:c]}, #{acc_c}, st, p, m, #{acc_var}, #{acc_init}, #{expr_macro}, #{masked_cnt_var});"
@@ -1775,13 +1811,15 @@ module MkKernel
1775
1811
 
1776
1812
  # Emits the native function of one source data type. The object lane
1777
1813
  # calls Ruby for every cell, so any cell can raise in the middle of a walk;
1778
- # its function is rebuilt around ca_iter_ensure (see guard_object_native).
1779
- # Every other lane is emitted as the emitter writes it.
1780
- def self.emit_native_for(io, k, src)
1781
- if src == :object
1814
+ # its function is rebuilt around ca_iter_ensure (see guard_native). So is
1815
+ # every lane of a family that passes guard: true, whose walks allocate
1816
+ # (an output mask made on the first empty slab, a scratch buffer), since a
1817
+ # failed allocation raises too. Other lanes are emitted as written.
1818
+ def self.emit_native_for(io, k, src, guard: false)
1819
+ if src == :object || guard
1782
1820
  buf = StringIO.new
1783
1821
  yield buf
1784
- io.print guard_object_native(buf.string, "#{k[:name]}_ki_native_object")
1822
+ io.print guard_native(buf.string, "#{k[:name]}_ki_native_#{src}")
1785
1823
  else
1786
1824
  yield io
1787
1825
  end
@@ -1822,7 +1860,7 @@ module MkKernel
1822
1860
  # frame is gone once the body raises. The body text is kept as written:
1823
1861
  # the parameters are unpacked from the frame under their own names, and
1824
1862
  # each state name is defined to its slot for the length of the body.
1825
- def self.guard_object_native(text, fn)
1863
+ def self.guard_native(text, fn)
1826
1864
  header = /^static VALUE\n#{fn} \(([^)]*)\)\n\{\n/
1827
1865
  m = header.match(text) or raise "mkkernel: #{fn} not found"
1828
1866
  close = matching_brace(text, m.end(0) - 2)
@@ -1845,9 +1883,9 @@ module MkKernel
1845
1883
 
1846
1884
  out = StringIO.new
1847
1885
  out.print m.pre_match
1848
- out.puts "/* The object lane calls Ruby for every cell, so a walk can raise part"
1849
- out.puts " way. Its states live in the caller's frame, and ca_iter_ensure"
1850
- out.puts " finishes them however the body leaves. */"
1886
+ out.puts "/* The walks can raise part way (a Ruby call per cell in the object"
1887
+ out.puts " lane, an allocation in any lane). Their states live in the caller's"
1888
+ out.puts " frame, and ca_iter_ensure finishes them however the body leaves. */"
1851
1889
  out.puts "typedef struct {"
1852
1890
  out.puts " ca_iter_state states[#{states.size}];"
1853
1891
  params.each { |type, name| out.puts " #{type} #{name};".sub(/\* /, "*") }
@@ -1872,7 +1910,11 @@ module MkKernel
1872
1910
  out.puts "{"
1873
1911
  out.puts " #{frame} fr;"
1874
1912
  params.each { |_, name| out.puts " fr.#{name} = #{name};" }
1875
- out.puts " return ca_iter_ensure(#{states.size}, fr.states, #{fn}_body, (VALUE) &fr);"
1913
+ if params.any? { |type, name| name == "ca" && type.include?("CArray") } && fn.end_with?("_object")
1914
+ out.puts " return ca_iter_ensure_computing(ca, #{states.size}, fr.states, #{fn}_body, (VALUE) &fr);"
1915
+ else
1916
+ out.puts " return ca_iter_ensure(#{states.size}, fr.states, #{fn}_body, (VALUE) &fr);"
1917
+ end
1876
1918
  out.puts "}"
1877
1919
  out.print rest
1878
1920
  out.string
@@ -1882,7 +1924,7 @@ module MkKernel
1882
1924
  io.puts
1883
1925
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
1884
1926
  k[:source].each do |src|
1885
- emit_native_for(io, k, src) { |o| emit_reduce_native(o, k, src) }
1927
+ emit_native_for(io, k, src, guard: true) { |o| emit_reduce_native(o, k, src) }
1886
1928
  end
1887
1929
  emit_reduce_dispatch(io, k)
1888
1930
  end
@@ -2030,6 +2072,7 @@ module MkKernel
2030
2072
  io.puts " if ( naxes != 1 || ! ca_too_few_present(ca->dim[slab_axes[0]], min_count, 0) ) {" if li_gate
2031
2073
  emit_reduce_loop_interchange(io, k, si, oi, ruby_wrap, src,
2032
2074
  acc_var, acc_init, reduce_stmt, finish_expr)
2075
+ emit_reduce_short_fiber(io, k, si, oi, src, reduce_stmt, finish_expr)
2033
2076
  io.puts " }" if li_gate
2034
2077
  end
2035
2078
 
@@ -2163,6 +2206,7 @@ module MkKernel
2163
2206
  "#{k[:identity_on_empty] ? 0 : 1})"
2164
2207
  end
2165
2208
  trigger = "(#{trigger}) || #{all_nan_flag}" if all_nan_flag
2209
+ trigger = "(#{trigger}) || (#{k[:undefined_if]})" if k[:undefined_if]
2166
2210
  finish_emit = view_flat \
2167
2211
  ? "(transform_active ? (outer_off + ((ca_size_t)(#{finish_expr})) * axis_vstride) : ((ca_size_t)(#{finish_expr})))" \
2168
2212
  : "(#{finish_expr})"
@@ -2187,7 +2231,9 @@ module MkKernel
2187
2231
  # Non-mask_policy path: the slab macros always take a masked_cnt,
2188
2232
  # so hand them a throwaway. The generic form (suffix "", the
2189
2233
  # :object lane) has a wrapper that hides it.
2190
- if suffix == ""
2234
+ if k[:settled]
2235
+ io.puts " CA_SLAB_REDUCE_T_SETTLE(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt}, #{k[:settled]});"
2236
+ elsif suffix == ""
2191
2237
  io.puts " CA_SLAB_REDUCE_T(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt});"
2192
2238
  else
2193
2239
  io.puts " ca_size_t __sr_throwaway_mc = 0;"
@@ -2535,8 +2581,16 @@ module MkKernel
2535
2581
  expr_macro_defs = "#define #{p1_macro}(__x) #{p1_expr}\n" \
2536
2582
  "#define #{p2_macro}(__x) #{p2_expr}\n"
2537
2583
  expr_macro_undefs = "#undef #{p1_macro}\n#undef #{p2_macro}\n"
2538
- p1_reduce_call = "CA_SLAB_REDUCE_SUM8_EX(#{si[:c]}, #{sum_c}, st, p, m, sum, #{sum_init}, #{p1_macro}, masked_cnt);"
2539
- p2_reduce_call = "CA_SLAB_REDUCE_SUM8_EX(#{si[:c]}, #{m2_c}, st, p, m, M2, #{m2_init}, #{p2_macro}, __unused_mc);"
2584
+ if %i[i8 u8 i16 u16].include?(src)
2585
+ # Per-cell conversion to double does not vectorize for these widths
2586
+ # (see the CA_SLAB_REDUCE_*_NARROW_EX header comments).
2587
+ q_c = %i[i8 u8].include?(src) ? "int32_t" : "int64_t"
2588
+ p1_reduce_call = "CA_SLAB_REDUCE_SUM8_NARROW_EX(#{si[:c]}, #{sum_c}, st, p, m, sum, #{sum_init}, #{p1_macro}, masked_cnt);"
2589
+ p2_reduce_call = "CA_SLAB_REDUCE_M2_NARROW_EX(#{si[:c]}, #{q_c}, st, p, m, M2, #{m2_init}, mean, #{p2_macro}, __unused_mc);"
2590
+ else
2591
+ p1_reduce_call = "CA_SLAB_REDUCE_SUM8_EX(#{si[:c]}, #{sum_c}, st, p, m, sum, #{sum_init}, #{p1_macro}, masked_cnt);"
2592
+ p2_reduce_call = "CA_SLAB_REDUCE_SUM8_EX(#{si[:c]}, #{m2_c}, st, p, m, M2, #{m2_init}, #{p2_macro}, __unused_mc);"
2593
+ end
2540
2594
  end
2541
2595
 
2542
2596
  io.puts
@@ -2598,6 +2652,101 @@ module MkKernel
2598
2652
  io.print expr_macro_undefs
2599
2653
  end
2600
2654
 
2655
+ # Short-fiber fast path (shortcut short_fiber_reduce): a single-axis
2656
+ # reduction over the innermost axis of a row-major buffer whose fibers are
2657
+ # short. The general walk asks the kernel iterator for each fiber, and for
2658
+ # fibers of a few cells that per-fiber step costs more than the cells
2659
+ # (1M fibers of 4 float64 cells: 12.6 ms against 1.0 ms for a plain loop,
2660
+ # M2 Max). Here every fiber is folded in place from the buffer, in the
2661
+ # order the general walk folds it, into scalar accumulators. Eligibility
2662
+ # is the loop interchange's (same reduction kinds, no mask, an alias
2663
+ # attach); a fiber longer than 32 cells (16 for min / max) goes to the
2664
+ # general walk, which is already faster than this loop there.
2665
+ def self.emit_reduce_short_fiber(io, k, si, oi, src, reduce_stmt, finish_expr)
2666
+ buf_kinds = %i[plus star min max]
2667
+ rk = k[:reduction_kind]
2668
+ if buf_kinds.include?(rk)
2669
+ acc_vars = [k[:state].keys.first]
2670
+ induction_var = nil
2671
+ else
2672
+ acc_vars = k[:state].keys.select { |v| buf_kinds.include?(rk[v]) }
2673
+ ind_pair = rk.find { |_, kind| kind == :induction }
2674
+ induction_var = ind_pair && ind_pair[0]
2675
+ end
2676
+
2677
+ info = acc_vars.map do |v|
2678
+ { var: v,
2679
+ c_type: resolve_state_type(k[:state][v], oi, si, src),
2680
+ init: resolve_init_expr(k[:init][v], oi, si, src) }
2681
+ end
2682
+
2683
+ reduce_sf = reduce_stmt.dup
2684
+ finish_sf = finish_expr.dup
2685
+ if induction_var
2686
+ ind_pat = Regexp.escape(induction_var.to_s)
2687
+ reduce_sf = reduce_sf.gsub(/\b#{ind_pat}\+\+/, "0")
2688
+ .gsub(/\+\+\b#{ind_pat}\b/, "0")
2689
+ finish_sf = finish_sf.gsub(/\b#{ind_pat}\b/, "((int64_t)__sf_M)")
2690
+ end
2691
+
2692
+ decls = info.map { |i| "#{i[:c_type]} #{i[:var]} = (#{i[:c_type]}) (#{i[:init]});" }
2693
+ .join("\n ")
2694
+
2695
+ # A float min / max whose accumulator is still at the init saw only NaN
2696
+ # or only +/-INFINITY; the fiber is re-read to tell which, as the
2697
+ # general walk's all-NaN rule answers NaN for the first.
2698
+ nan_fixup =
2699
+ if k[:all_nan_result] == :nan && FLOAT_DTYPES.include?(src) && info.size == 1
2700
+ i = info.first
2701
+ <<~C.rstrip
2702
+ if ( #{i[:var]} == (#{i[:c_type]}) (#{i[:init]}) ) {
2703
+ int __sf_seen = 0;
2704
+ for ( ca_size_t __j = 0; __j < __sf_M; __j++ ) __sf_seen |= (__sf_row[__j] == __sf_row[__j]);
2705
+ if ( ! __sf_seen ) #{i[:var]} = (#{i[:c_type]}) NAN;
2706
+ }
2707
+ C
2708
+ else
2709
+ ""
2710
+ end
2711
+
2712
+ # A min / max fold is a compare per cell that the general walk splits
2713
+ # eight ways; past 16 cells that split is faster than this loop.
2714
+ max_m = info.any? { |i| %i[min max].include?(buf_kinds.include?(rk) ? rk : rk[i[:var]]) } ? 16 : 32
2715
+
2716
+ io.puts <<~C
2717
+ if ( naxes == 1
2718
+ && ca->ndim >= 2
2719
+ && slab_axes[0] == (int8_t)(ca->ndim - 1)
2720
+ && ca->dim[ca->ndim - 1] >= 1
2721
+ && ca->dim[ca->ndim - 1] <= #{max_m}
2722
+ && ! ca_has_mask(ca)
2723
+ && ca_attach_is_alias(ca)
2724
+ && ca_fast_path(CA_SC_SHORT_FIBER_REDUCE) ) {
2725
+ ca_size_t __sf_M = ca->dim[ca->ndim - 1];
2726
+ ca_size_t __sf_OUTER = ca->elements / __sf_M;
2727
+ VALUE vout = rb_ca_new_reduced(self, slab_axes, naxes, #{oi[:ca]}, keep_axis);
2728
+ CArray *co;
2729
+ GetCArray(vout, co);
2730
+ #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
2731
+ ca_attach(ca); /* window: numeric lanes only, nothing raises inside; O(1) alias attach */
2732
+ const #{si[:c]} *p_root = (const #{si[:c]} *) ca->ptr;
2733
+ for ( ca_size_t __sf_o = 0; __sf_o < __sf_OUTER; __sf_o++ ) {
2734
+ const #{si[:c]} *__sf_row = p_root + __sf_o * __sf_M;
2735
+ #{decls}
2736
+ for ( ca_size_t __j = 0; __j < __sf_M; __j++ ) {
2737
+ #{si[:c]} v = __sf_row[__j];
2738
+ #{reduce_sf};
2739
+ (void) v;
2740
+ }
2741
+ #{nan_fixup}
2742
+ op[__sf_o] = (#{oi[:c]}) (#{finish_sf});
2743
+ }
2744
+ ca_detach(ca);
2745
+ return vout;
2746
+ }
2747
+ C
2748
+ end
2749
+
2601
2750
  def self.emit_reduce_loop_interchange(io, k, si, oi, ruby_wrap, src,
2602
2751
  acc_var, acc_init, reduce_stmt, finish_expr)
2603
2752
  # Classify state vars by reduction_kind:
@@ -2747,7 +2896,7 @@ module MkKernel
2747
2896
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2748
2897
  && ! ca_has_mask(ca)
2749
2898
  && ca_attach_is_alias(ca)
2750
- && ca_fast_paths_enabled() ) {
2899
+ && ca_fast_path(CA_SC_LOOP_INTERCHANGE) ) {
2751
2900
  int8_t __li_ax = slab_axes[0];
2752
2901
  ca_size_t __li_M = ca->dim[__li_ax];
2753
2902
  ca_size_t __li_INNER = 1;
@@ -2798,7 +2947,7 @@ module MkKernel
2798
2947
  && ! ca_has_mask(ca)
2799
2948
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2800
2949
  && ((CAStack *) ca)->k_axis == 0
2801
- && ca_fast_paths_enabled() ) {
2950
+ && ca_fast_path(CA_SC_STACK_LOOP_INTERCHANGE) ) {
2802
2951
  CAStack *__li_st = (CAStack *) ca;
2803
2952
  int8_t __li_ax = slab_axes[0];
2804
2953
  ca_size_t __li_M = ca->dim[__li_ax];
@@ -2826,7 +2975,7 @@ module MkKernel
2826
2975
  ca_size_t __li_parent_out = __li_OUTER * __li_INNER;
2827
2976
  const ca_size_t __li_TILE = 512;
2828
2977
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2829
- ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2978
+ ca_attach(__li_st->parents[__kk]); /* window: a parent may run Ruby to fill itself (a CAObject's copy_data); a raising attach holds nothing and the earlier parents are already detached, and the numeric loop after it does not raise */
2830
2979
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2831
2980
  #{oi[:c]} *__li_op_k = op + (ca_size_t) __kk * __li_parent_out;
2832
2981
  #{core.call("p_root", "__li_op_k", "__li_OUTER")}
@@ -2864,7 +3013,7 @@ module MkKernel
2864
3013
  && ((CAStack *) ca)->k_axis > 0
2865
3014
  && ( slab_axes[0] > ((CAStack *) ca)->k_axis
2866
3015
  || slab_axes[0] + 1 == ((CAStack *) ca)->k_axis )
2867
- && ca_fast_paths_enabled() ) {
3016
+ && ca_fast_path(CA_SC_STACK_LOOP_INTERCHANGE) ) {
2868
3017
  CAStack *__li_st = (CAStack *) ca;
2869
3018
  int8_t __li_ax = slab_axes[0];
2870
3019
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -2921,7 +3070,7 @@ module MkKernel
2921
3070
  ca_size_t __li_plane_elems = __li_M * __li_INNER;
2922
3071
  const ca_size_t __li_TILE = 512;
2923
3072
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2924
- ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
3073
+ ca_attach(__li_st->parents[__kk]); /* window: a parent may run Ruby to fill itself (a CAObject's copy_data); a raising attach holds nothing and the earlier parents are already detached, and the numeric loop after it does not raise */
2925
3074
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2926
3075
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
2927
3076
 
@@ -3007,7 +3156,7 @@ module MkKernel
3007
3156
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
3008
3157
  && ((CAStack *) ca)->k_axis > 0
3009
3158
  && slab_axes[0] + 1 < ((CAStack *) ca)->k_axis
3010
- && ca_fast_paths_enabled() ) {
3159
+ && ca_fast_path(CA_SC_STACK_LOOP_INTERCHANGE) ) {
3011
3160
  CAStack *__li_st = (CAStack *) ca;
3012
3161
  int8_t __li_ax = slab_axes[0];
3013
3162
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -3079,7 +3228,7 @@ module MkKernel
3079
3228
 
3080
3229
  const ca_size_t __li_TILE = 512;
3081
3230
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
3082
- ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
3231
+ ca_attach(__li_st->parents[__kk]); /* window: a parent may run Ruby to fill itself (a CAObject's copy_data); a raising attach holds nothing and the earlier parents are already detached, and the numeric loop after it does not raise */
3083
3232
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
3084
3233
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
3085
3234
 
@@ -3389,7 +3538,7 @@ module MkKernel
3389
3538
  io.puts " ca->elements > 0 &&"
3390
3539
  io.puts " ! ca_too_few_present(ca->elements, min_count, 0) &&" if k[:mask_policy] == :min_count
3391
3540
  io.puts " ca_is_lazy_view(ca) && ! ca_has_mask(ca) &&"
3392
- io.puts " ca_fast_paths_enabled() ) {"
3541
+ io.puts " ca_fast_path(CA_SC_LAZY_STREAMING_REDUCE) ) {"
3393
3542
  io.puts " #{fn}_t __ctx;"
3394
3543
  io.puts " __ctx.chunk = NULL;"
3395
3544
  io.puts " __ctx.ca = ca;"
@@ -3508,12 +3657,12 @@ module MkKernel
3508
3657
  # computation type (so a float weight against an int source is not
3509
3658
  # truncated -- the body reads it as `double`); :match_source keeps the
3510
3659
  # legacy source-type storage. waca is the CA_* constant the three
3511
- # materialization sites (W-A1 / W-A2 / W-A3) allocate/wrap against, and
3660
+ # materialization sites (W-A1 / W-A4 / W-A3) allocate/wrap against, and
3512
3661
  # must agree with the T_W the reduce macro reads (double for :promote).
3513
3662
  waca = (k[:array_arg][:data_type] == :promote) ? "CA_FLOAT64" : "src->data_type"
3514
3663
  # PROPOSAL_REDUCTION_PER_FIBER_AUX_OPERAND rev1: weights acceptance
3515
- # expanded to 3 forms (W-A1 scalar / W-A2 1-D axis-broadcast / W-A3
3516
- # full per-element). W-A1 / W-A2 are materialized to W-A3 form
3664
+ # expanded to 3 forms (W-A1 scalar / W-A4 shape of the reduced axes / W-A3
3665
+ # full per-element). W-A1 / W-A4 are materialized to W-A3 form
3517
3666
  # at dispatch (= simple, uniform kernel path). rev5 strict acceptance
3518
3667
  # mirrors PROPOSAL_LINEAR_INTERP_PER_FIBER_MATCHED rev5 in spirit.
3519
3668
  io.puts " if ( argc < 1 ) {"
@@ -3521,13 +3670,18 @@ module MkKernel
3521
3670
  io.puts " }"
3522
3671
  io.puts " volatile VALUE rweights = argv[0];"
3523
3672
  io.puts " argc--; argv++;"
3524
- # rev5 A1: convert a single-element CArray (CScalar / [1] 1-D / all
3525
- # dim==1) to a Ruby Numeric and route it through the scalar path.
3673
+ # A CScalar is a scalar weight. Any other array, one with a single
3674
+ # element included, is held to the shape rules below.
3526
3675
  io.puts " if ( rb_obj_is_carray(rweights) ) {"
3527
3676
  io.puts " CArray *cv_pre_;"
3528
3677
  io.puts " GetCArray(rweights, cv_pre_);"
3529
- io.puts " if ( cv_pre_->elements == 1 ) {"
3678
+ io.puts " if ( ca_is_scalar(cv_pre_) ) {"
3679
+ io.puts " int8_t wtype_ = cv_pre_->data_type;"
3530
3680
  io.puts " rweights = rb_funcall(rweights, rb_intern(\"[]\"), 1, INT2NUM(0));"
3681
+ # A boolean cell reads as true / false; as a weight it is 1 / 0.
3682
+ io.puts " if ( wtype_ == CA_BOOLEAN && rweights != CA_UNDEF ) {"
3683
+ io.puts " rweights = INT2FIX(RTEST(rweights) ? 1 : 0);"
3684
+ io.puts " }"
3531
3685
  io.puts " }"
3532
3686
  io.puts " }"
3533
3687
  # The weights are attached from here to the end of the call, so they
@@ -3540,7 +3694,16 @@ module MkKernel
3540
3694
  wio.puts " /* W-A1: Ruby Numeric scalar -> full-shape constant weight CArray */"
3541
3695
  wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3542
3696
  wio.puts " GetCArray(vcw, cw); /* a new entity: its ptr needs no attach */"
3543
- wio.puts " rb_ca_obj2ptr(vcw, rweights, cw->ptr); /* fill first cell */"
3697
+ wio.puts " if ( rweights == CA_UNDEF ) {"
3698
+ wio.puts " /* A masked weight (UNDEF, or a one-element array masked): every"
3699
+ wio.puts " cell's weight is masked, so no cell contributes. */"
3700
+ wio.puts " memset(cw->ptr, 0, (size_t) cw->elements * cw->bytes);"
3701
+ wio.puts " ca_create_mask(cw);"
3702
+ wio.puts " memset(cw->mask->ptr, 1, (size_t) cw->elements);"
3703
+ wio.puts " }"
3704
+ wio.puts " else {"
3705
+ wio.puts " rb_ca_obj2ptr(vcw, rweights, cw->ptr); /* fill first cell */"
3706
+ wio.puts " }"
3544
3707
  wio.puts " /* broadcast first cell to all elements (= constant fill) */"
3545
3708
  wio.puts " {"
3546
3709
  wio.puts " char *p0 = cw->ptr;"
@@ -3552,69 +3715,75 @@ module MkKernel
3552
3715
  wio.puts " else {"
3553
3716
  wio.puts " /* CArray val: coerce to the weight storage type (f64 for :promote, so a"
3554
3717
  wio.puts " float weight against an int source is not truncated) */"
3555
- wio.puts " /* vcv holds the view while cv is read: W-A2 below moves vcw on to"
3718
+ wio.puts " /* vcv holds the view while cv is read: W-A4 below moves vcw on to"
3556
3719
  wio.puts " the materialised copy before it is done with cv. */"
3557
3720
  wio.puts " volatile VALUE vcv = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3558
3721
  wio.puts " CArray *cv;"
3559
3722
  wio.puts " GetCArray(vcv, cv);"
3560
3723
  wio.puts " vcw = vcv;"
3561
3724
  wio.puts ""
3562
- wio.puts " /* rev5 form detection: A3 (same ndim) -> A2 (1-D axis-broadcast) -> raise */"
3563
- wio.puts " if ( cv->ndim == src->ndim ) {"
3564
- wio.puts " /* W-A3 commit-or-raise: shape strict match */"
3565
- wio.puts " for ( int8_t i = 0; i < src->ndim; i++ ) {"
3566
- wio.puts " if ( cv->dim[i] != src->dim[i] ) {"
3567
- wio.puts %Q[ rb_raise(rb_eArgError, "%s: weights shape mismatch (dim[%d]=%ld != self.dim[%d]=%ld; expected a scalar, a 1-D array along the reduced axis, or self.shape)", ca_calling_method_name(), (int)i, (long)cv->dim[i], (int)i, (long)src->dim[i]);]
3568
- wio.puts " }"
3569
- wio.puts " }"
3570
- wio.puts " /* W-A3 path: use cv directly (= existing) */"
3725
+ wio.puts " /* form detection: W-A3 (self.shape) -> W-A4 (shape of the reduced axes,"
3726
+ wio.puts " taken in ascending axis order whatever order axis: lists them) -> raise */"
3727
+ wio.puts " int8_t wax[CA_RANK_MAX];"
3728
+ wio.puts " for ( int8_t i = 0; i < naxes; i++ ) {"
3729
+ wio.puts " int8_t a = slab_axes[i], j = i;"
3730
+ wio.puts " while ( j > 0 && wax[j-1] > a ) { wax[j] = wax[j-1]; j--; }"
3731
+ wio.puts " wax[j] = a;"
3732
+ wio.puts " }"
3733
+ wio.puts " int is_full_shape = ( cv->ndim == src->ndim );"
3734
+ wio.puts " for ( int8_t i = 0; is_full_shape && i < src->ndim; i++ ) {"
3735
+ wio.puts " if ( cv->dim[i] != src->dim[i] ) is_full_shape = 0;"
3736
+ wio.puts " }"
3737
+ wio.puts " int is_reduced_shape = ( ! is_full_shape && naxes >= 1 && cv->ndim == naxes );"
3738
+ wio.puts " for ( int8_t i = 0; is_reduced_shape && i < naxes; i++ ) {"
3739
+ wio.puts " if ( cv->dim[i] != src->dim[wax[i]] ) is_reduced_shape = 0;"
3740
+ wio.puts " }"
3741
+ wio.puts " if ( is_full_shape ) {"
3742
+ wio.puts " /* W-A3: per-element weights, used as they are */"
3571
3743
  wio.puts " cw = cv;"
3572
3744
  wio.puts " }"
3573
- wio.puts " else if ( naxes == 1 && cv->ndim == 1 && cv->dim[0] == src->dim[slab_axes[0]] ) {"
3574
- wio.puts " /* W-A2: 1-D axis-broadcast -> materialize to full shape */"
3575
- wio.puts " ca_attach(cv); /* window: nothing raises inside */"
3745
+ wio.puts " else if ( is_reduced_shape ) {"
3746
+ wio.puts " /* W-A4: one weight per cell of the reduced axes, repeated along the"
3747
+ wio.puts " kept axes; materialized to self.shape. cw and its mask are made"
3748
+ wio.puts " before cv is attached: making them allocates. */"
3576
3749
  wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3577
3750
  wio.puts " GetCArray(vcw, cw);"
3578
- wio.puts " /* broadcast cv (1-D, length src->dim[slab_axes[0]]) along reduce axis */"
3579
- wio.puts " {"
3580
- wio.puts " int8_t baxis = slab_axes[0];"
3581
- wio.puts " ca_size_t inner_stride = 1;"
3582
- wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3583
- wio.puts " ca_size_t axis_n = src->dim[baxis];"
3584
- wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3585
- wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3586
- wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3587
- wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3588
- wio.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3589
- wio.puts " memcpy(cw->ptr + flat * cw->bytes, cv->ptr + a * cv->bytes, cw->bytes);"
3590
- wio.puts " }"
3591
- wio.puts " }"
3592
- wio.puts " }"
3593
- wio.puts " }"
3594
- wio.puts " /* Propagate cv mask via W-A3 overlay path below (= mask materialize) */"
3595
3751
  wio.puts " if ( ca_has_mask(cv) ) {"
3596
- wio.puts " /* Allocate cw mask + broadcast cv mask along axis */"
3597
3752
  wio.puts " ca_create_mask(cw);"
3598
- wio.puts " boolean8_t *cw_m = (boolean8_t *)cw->mask->ptr;"
3599
- wio.puts " boolean8_t *cv_m = (boolean8_t *)cv->mask->ptr;"
3600
- wio.puts " int8_t baxis = slab_axes[0];"
3601
- wio.puts " ca_size_t inner_stride = 1;"
3602
- wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3603
- wio.puts " ca_size_t axis_n = src->dim[baxis];"
3604
- wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3605
- wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3606
- wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3607
- wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3608
- wio.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3609
- wio.puts " cw_m[flat] = cv_m[a];"
3610
- wio.puts " }"
3753
+ wio.puts " }"
3754
+ wio.puts " ca_attach(cv); /* window: nothing raises inside */"
3755
+ wio.puts " {"
3756
+ wio.puts " ca_size_t idx[CA_RANK_MAX], wstep[CA_RANK_MAX];"
3757
+ wio.puts " ca_size_t step = 1;"
3758
+ wio.puts " boolean8_t *cw_m = ca_has_mask(cv) ? (boolean8_t *) cw->mask->ptr : NULL;"
3759
+ wio.puts " boolean8_t *cv_m = ca_has_mask(cv) ? (boolean8_t *) cv->mask->ptr : NULL;"
3760
+ wio.puts " for ( int8_t i = 0; i < src->ndim; i++ ) {"
3761
+ wio.puts " idx[i] = 0;"
3762
+ wio.puts " wstep[i] = 0; /* kept axes: the weight repeats */"
3763
+ wio.puts " }"
3764
+ wio.puts " for ( int8_t i = naxes - 1; i >= 0; i-- ) {"
3765
+ wio.puts " wstep[wax[i]] = step;"
3766
+ wio.puts " step *= cv->dim[i];"
3767
+ wio.puts " }"
3768
+ wio.puts " for ( ca_size_t flat = 0; flat < src->elements; flat++ ) {"
3769
+ wio.puts " ca_size_t w = 0;"
3770
+ wio.puts " for ( int8_t i = 0; i < src->ndim; i++ ) w += idx[i] * wstep[i];"
3771
+ wio.puts " memcpy(cw->ptr + flat * cw->bytes, cv->ptr + w * cv->bytes, cw->bytes);"
3772
+ wio.puts " if ( cw_m ) cw_m[flat] = cv_m[w];"
3773
+ wio.puts " for ( int8_t i = src->ndim - 1; i >= 0; i-- ) {"
3774
+ wio.puts " if ( ++idx[i] < src->dim[i] ) break;"
3775
+ wio.puts " idx[i] = 0;"
3611
3776
  wio.puts " }"
3612
3777
  wio.puts " }"
3613
3778
  wio.puts " }"
3614
3779
  wio.puts " ca_detach(cv);"
3615
3780
  wio.puts " }"
3616
3781
  wio.puts " else {"
3617
- wio.puts %Q[ rb_raise(rb_eArgError, "%s: w shape not accepted (val.ndim=%d, val.dim[0]=%ld; expected scalar / [M=%ld] / self.shape)", ca_calling_method_name(), (int)cv->ndim, (long)(cv->ndim >= 1 ? cv->dim[0] : 0), (long)(naxes == 1 ? src->dim[slab_axes[0]] : -1));]
3782
+ wio.puts " volatile VALUE rexpect = rb_ary_new();"
3783
+ wio.puts " for ( int8_t i = 0; i < naxes; i++ ) {"
3784
+ wio.puts " rb_ary_push(rexpect, SIZE2NUM(src->dim[wax[i]]));"
3785
+ wio.puts " }"
3786
+ wio.puts %Q[ rb_raise(rb_eArgError, "%s: weights of shape %"PRIsVALUE" not accepted (expected a scalar, the shape of the reduced axes %"PRIsVALUE", or self.shape %"PRIsVALUE")", ca_calling_method_name(), rb_funcall(vcv, rb_intern("shape"), 0), rexpect, rb_funcall(self, rb_intern("shape"), 0));]
3618
3787
  wio.puts " }"
3619
3788
  wio.puts " }"
3620
3789
  # Mask overlay (= legacy W-A3 path; if the materialized cw carries a mask, overlay it)
@@ -3827,6 +3996,8 @@ module MkKernel
3827
3996
  # fiber's mask base.
3828
3997
  mask_setup =
3829
3998
  if gated
3999
+ # Before the walks open: creating the mask allocates, and a raise
4000
+ # inside them would jump past their finish.
3830
4001
  "\n" \
3831
4002
  " boolean8_t *op_mask = NULL;\n" \
3832
4003
  " if ( ca_has_mask(ca) ) {\n" \
@@ -3867,6 +4038,7 @@ module MkKernel
3867
4038
  INIT inside the macro. */
3868
4039
  int8_t slab_axes[CA_RANK_MAX];
3869
4040
  slab_axes[0] = (int8_t) axis;
4041
+ #{mask_setup}
3870
4042
  ca_iter_state st_in, st_out;
3871
4043
  int rc;
3872
4044
  rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,
@@ -3882,7 +4054,6 @@ module MkKernel
3882
4054
  rb_raise(rb_eRuntimeError,
3883
4055
  "%s: output init failed rc=%d", ca_calling_method_name(), rc);
3884
4056
  }
3885
- #{mask_setup}
3886
4057
  char *pi, *po;
3887
4058
  boolean8_t *mi, *mo;
3888
4059
  while ( ca_iter_state_next_slab_axes(&st_in, &pi, &mi) &&
@@ -4167,7 +4338,7 @@ module MkKernel
4167
4338
  if is_fixlen
4168
4339
  io.puts " buf[#{dst}].vp = pi + #{kexpr} * istride; buf[#{dst}].nb = (ca_size_t) ca->bytes;"
4169
4340
  else
4170
- io.puts " buf[#{dst}].v = *(#{si[:c]} *) (pi + #{kexpr} * istride);"
4341
+ io.puts " memcpy(&buf[#{dst}].v, pi + #{kexpr} * istride, sizeof(buf[#{dst}].v));"
4171
4342
  end
4172
4343
  end
4173
4344
 
@@ -4263,21 +4434,10 @@ module MkKernel
4263
4434
  io.puts " when input has any mask. */"
4264
4435
  io.puts " if ( ca_has_mask(ca) ) ca_create_mask(co);"
4265
4436
  end
4266
- io.puts " ca_iter_state st_in, st_out;"
4267
- io.puts " int rc;"
4268
- io.puts " rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,"
4269
- io.puts " slab_axes, 1, 0);"
4270
- io.puts " if ( rc != CA_ITER_OK ) {"
4271
- io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: input init failed rc=%d", ca_calling_method_name(), rc);]
4272
- io.puts " }"
4273
- io.puts " rc = ca_iter_state_init_l2_paired(&st_out, &st_in, co, CA_SLAB_AXES,"
4274
- io.puts " slab_axes, 1, CA_KERNEL_WRITE);"
4275
- io.puts " if ( rc != CA_ITER_OK ) {"
4276
- io.puts " ca_iter_state_finish(&st_in);"
4277
- io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: output init failed rc=%d", ca_calling_method_name(), rc);]
4278
- io.puts " }"
4279
- io.puts
4280
- io.puts " ca_size_t fiber_n = st_in.slab_elements;"
4437
+ # The buffers are taken before the walks open: an allocation that
4438
+ # raises inside them would jump past their finish. The slab is the
4439
+ # sort axis alone, so a fiber holds ca->dim[axis] cells.
4440
+ io.puts " ca_size_t fiber_n = ca->dim[axis];"
4281
4441
  if src == :object
4282
4442
  # The object comparator calls <=>, so the sort can raise part way.
4283
4443
  # ALLOCV frees these buffers whether the function returns or unwinds.
@@ -4290,15 +4450,17 @@ module MkKernel
4290
4450
  io.puts " #{name}_pair_#{src} *aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4291
4451
  end
4292
4452
  else
4293
- # ALLOCV as well: the walk that fills buf is the only thing that
4294
- # could raise, and it does not today, but the buffers need not rely
4295
- # on that.
4296
4453
  io.puts " volatile VALUE hbuf = 0, haux = 0;"
4297
4454
  io.puts " #{name}_pair_#{src} *buf = ALLOCV_N(#{name}_pair_#{src}, hbuf, fiber_n);"
4298
4455
  end
4299
- if has_kind && src != :object
4456
+ if has_kind && %i[f32 f64].include?(src)
4457
+ # aux is the mergesort's ping-pong buffer and, before that, the
4458
+ # scratch of the NaN pre-partition, so a float kernel always takes it.
4459
+ io.puts " #{name}_pair_#{src} *aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4460
+ elsif has_kind && src != :object && src != :fixlen
4300
4461
  # The aux pair buffer for ca_sort_merge_pair_<src> (a ping-pong
4301
- # bottom-up mergesort), taken once outside the fiber loop.
4462
+ # bottom-up mergesort), taken once outside the fiber loop. The fixlen
4463
+ # lane sorts with qsort and needs none.
4302
4464
  io.puts " #{name}_pair_#{src} *aux = NULL;"
4303
4465
  io.puts " if ( do_stable ) {"
4304
4466
  io.puts " aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
@@ -4307,6 +4469,20 @@ module MkKernel
4307
4469
  io.puts " (void) haux;"
4308
4470
  end
4309
4471
 
4472
+ io.puts " ca_iter_state st_in, st_out;"
4473
+ io.puts " int rc;"
4474
+ io.puts " rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,"
4475
+ io.puts " slab_axes, 1, 0);"
4476
+ io.puts " if ( rc != CA_ITER_OK ) {"
4477
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: input init failed rc=%d", ca_calling_method_name(), rc);]
4478
+ io.puts " }"
4479
+ io.puts " rc = ca_iter_state_init_l2_paired(&st_out, &st_in, co, CA_SLAB_AXES,"
4480
+ io.puts " slab_axes, 1, CA_KERNEL_WRITE);"
4481
+ io.puts " if ( rc != CA_ITER_OK ) {"
4482
+ io.puts " ca_iter_state_finish(&st_in);"
4483
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: output init failed rc=%d", ca_calling_method_name(), rc);]
4484
+ io.puts " }"
4485
+
4310
4486
  if view_flat
4311
4487
  io.puts
4312
4488
  io.puts " /* :view_flat semantics: kernel computes view-flat address"
@@ -4358,7 +4534,7 @@ module MkKernel
4358
4534
  if is_fixlen
4359
4535
  io.puts " buf[k].vp = pi + k * istride; buf[k].nb = (ca_size_t) ca->bytes;"
4360
4536
  else
4361
- io.puts " buf[k].v = *(#{si[:c]} *) (pi + k * istride);"
4537
+ io.puts " memcpy(&buf[k].v, pi + k * istride, sizeof(buf[k].v));"
4362
4538
  end
4363
4539
  io.puts " buf[k].i = #{payload.call("k")};"
4364
4540
  io.puts " }"
@@ -4373,7 +4549,7 @@ module MkKernel
4373
4549
  if is_fixlen
4374
4550
  io.puts " buf[k_unmasked].vp = pi + k * istride; buf[k_unmasked].nb = (ca_size_t) ca->bytes;"
4375
4551
  else
4376
- io.puts " buf[k_unmasked].v = *(#{si[:c]} *) (pi + k * istride);"
4552
+ io.puts " memcpy(&buf[k_unmasked].v, pi + k * istride, sizeof(buf[k_unmasked].v));"
4377
4553
  end
4378
4554
  io.puts " buf[k_unmasked].i = k;"
4379
4555
  io.puts " k_unmasked++;"
@@ -4389,7 +4565,7 @@ module MkKernel
4389
4565
  if is_fixlen
4390
4566
  io.puts " buf[k].vp = pi + k * istride; buf[k].nb = (ca_size_t) ca->bytes;"
4391
4567
  else
4392
- io.puts " buf[k].v = *(#{si[:c]} *) (pi + k * istride);"
4568
+ io.puts " memcpy(&buf[k].v, pi + k * istride, sizeof(buf[k].v));"
4393
4569
  end
4394
4570
  io.puts " buf[k].i = k;"
4395
4571
  io.puts " }"
@@ -4462,7 +4638,7 @@ module MkKernel
4462
4638
  io.puts " /* P.3/P.4: NaN pre-partition for float argsort -- NaN pairs are"
4463
4639
  io.puts " pushed to the tail (= NaN-at-end policy, matches sort_copy"
4464
4640
  io.puts " AC8). Only the finite slice is sorted. */"
4465
- io.puts " ca_size_t finite_n = ca_partition_nan_pair_#{src}((ca_pair_#{src} *) #{buf_expr}, (ca_size_t) #{sort_n_expr});"
4641
+ io.puts " ca_size_t finite_n = ca_partition_nan_pair_#{src}((ca_pair_#{src} *) #{buf_expr}, (ca_size_t) #{sort_n_expr}, (ca_pair_#{src} *) aux);"
4466
4642
  io.puts " if ( do_stable ) {"
4467
4643
  io.puts " ca_sort_merge_pair_#{src}((ca_pair_#{src} *) #{buf_expr}, (ca_pair_#{src} *) aux, finite_n);"
4468
4644
  io.puts " } else {"
@@ -5004,7 +5180,7 @@ module MkKernel
5004
5180
  io.puts
5005
5181
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
5006
5182
  k[:source].each do |src|
5007
- emit_native_for(io, k, src) { |o| emit_search_native(o, k, src) }
5183
+ emit_native_for(io, k, src, guard: true) { |o| emit_search_native(o, k, src) }
5008
5184
  end
5009
5185
  emit_search_dispatch(io, k)
5010
5186
  end
@@ -5101,15 +5277,16 @@ module MkKernel
5101
5277
  # The object lane calls Ruby for every cell of the case B/C walk, which
5102
5278
  # runs between a direct attach and detach of self and val; a raise there
5103
5279
  # would leave both attached. An entity holds no attach, so the lane
5104
- # reads an entity copy of whichever is a view.
5280
+ # reads an entity copy of whichever is a view, or an entity that leaves
5281
+ # ptr NULL at rest (a CASource).
5105
5282
  entity_bc = if is_object
5106
5283
  <<~C.chomp
5107
5284
  volatile VALUE vca_entity = self;
5108
- if ( ! ca_is_entity(ca) ) {
5285
+ if ( ! ca_is_entity(ca) || ! ca->ptr ) {
5109
5286
  vca_entity = rb_ca_copy(self);
5110
5287
  GetCArray(vca_entity, ca);
5111
5288
  }
5112
- if ( ! ca_is_entity(cv) ) {
5289
+ if ( ! ca_is_entity(cv) || ! cv->ptr ) {
5113
5290
  vsrc_val = rb_ca_copy(vsrc_val);
5114
5291
  GetCArray(vsrc_val, cv);
5115
5292
  }
@@ -5264,7 +5441,7 @@ module MkKernel
5264
5441
  CArray *co;
5265
5442
  GetCArray(vout, co);
5266
5443
  #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
5267
- #{op_mask_decl}
5444
+ #{undef_nm ? op_mask_decl : ""}
5268
5445
 
5269
5446
  ca_iter_state st;
5270
5447
  int rc = ca_iter_state_init_l2(&st, ca, CA_SLAB_AXES,
@@ -5486,6 +5663,12 @@ module MkKernel
5486
5663
  #{op_mask_decl}
5487
5664
 
5488
5665
  #{entity_bc}
5666
+ /* The output mask is made before the window opens, since making it
5667
+ allocates; it comes off again below when no cell needed it. */
5668
+ if ( #{undef_nm ? "1" : "ca_has_mask(cv)"} ) {
5669
+ ca_create_mask(co);
5670
+ op_mask = (boolean8_t *) co->mask->ptr;
5671
+ }
5489
5672
  /* Attach self + val; rev4 uses raw row-major byte addressing. */
5490
5673
  ca_attach_n(2, ca, cv); /* window: numeric bodies do not raise; the object lane reads entities */
5491
5674
 
@@ -5592,6 +5775,11 @@ module MkKernel
5592
5775
 
5593
5776
  ca_detach(ca);
5594
5777
  ca_detach(cv);
5778
+ if ( op_mask && ! memchr(op_mask, 1, (size_t) co->elements) ) {
5779
+ CArray *unused = co->mask;
5780
+ co->mask = NULL;
5781
+ ca_free(unused);
5782
+ }
5595
5783
  return vout;
5596
5784
  }
5597
5785
  C
@@ -5633,6 +5821,13 @@ module MkKernel
5633
5821
  io.puts " VALUE rval = argv[0];"
5634
5822
  io.puts " VALUE raxis = argv[1];"
5635
5823
  io.puts " VALUE reps = (argc >= 3) ? argv[2] : Qnil;"
5824
+ io.puts " /* A tolerance is a distance: a negative one (or NaN) means nothing. */"
5825
+ io.puts " if ( ! NIL_P(reps) ) {"
5826
+ io.puts " double eps_given = NUM2DBL(reps);"
5827
+ io.puts " if ( ! ( eps_given >= 0.0 ) ) {"
5828
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: eps must be zero or positive (got %"PRIsVALUE")", ca_calling_method_name(), rb_inspect(reps));]
5829
+ io.puts " }"
5830
+ io.puts " }"
5636
5831
  end
5637
5832
  io.puts " /* Query gate: the reference Face and the query are brought into one"
5638
5833
  io.puts " space (ca_face_reconcile_pair), then the reference descends to"
@@ -5656,6 +5851,8 @@ module MkKernel
5656
5851
  io.puts " GetCArray(self, src);"
5657
5852
  io.puts " }"
5658
5853
  io.puts " }"
5854
+ io.puts " self = ca_fixlen_widen_for_query(self, rval);"
5855
+ io.puts " GetCArray(self, src);"
5659
5856
 
5660
5857
  case k[:mask_self]
5661
5858
  when :raise
@@ -5687,13 +5884,14 @@ module MkKernel
5687
5884
  # (`ca_monop_<name>_<suffix>`). mkmath uses typedef names
5688
5885
  # `float32_t` / `float64_t` here, not the raw C types `float` / `double`,
5689
5886
  # so that `ca_monop_dispatch.c`'s extern references stay ABI-stable.
5690
- # For all other data_types, the suffix matches `DTYPES[src][:c]`.
5887
+ # For all other data_types, the suffix is `DTYPES[src][:c]` without the
5888
+ # `ca_u_` of its unaligned cell typedef (ca_u_int32_t -> int32_t).
5691
5889
  def self.math_suffix(src)
5692
5890
  case src
5693
5891
  when :f32 then "float32_t"
5694
5892
  when :f64 then "float64_t"
5695
5893
  when :fixlen then "fixlen" # symbol suffix differs from c-type "char"
5696
- else DTYPES[src][:c]
5894
+ else DTYPES[src][:c].sub(/\Aca_u_/, "")
5697
5895
  end
5698
5896
  end
5699
5897
 
@@ -5772,7 +5970,8 @@ module MkKernel
5772
5970
  uses_input = raw.include?("#1")
5773
5971
  # SL.2.x phase 2 (monop emit substrate hardening, MEMO_MONOP_STRIDED_AUDIT.md):
5774
5972
  # 3-way split (mask / contig / strided) symmetric with binop SL.2.x phase 1.
5775
- # contig branch (i1==1 && i2==1) uses index form + `__restrict__` + `omp simd`.
5973
+ # contig branch (i1==1 && i2==1) uses index form + `omp simd` (`__restrict__`
5974
+ # only when input and output are apart).
5776
5975
  # strided branch uses `p += i` increment form (per-iter multiply removed).
5777
5976
  # mask branch unchanged (per-cell predicate, not SIMD-amenable).
5778
5977
  if uses_input
@@ -5780,11 +5979,11 @@ module MkKernel
5780
5979
 
5781
5980
  static void
5782
5981
  ca_monop_#{k[:name]}_#{suffix} (ca_size_t n, boolean8_t *m,
5783
- char * __restrict__ ptr1, ca_size_t i1,
5784
- char * __restrict__ ptr2, ca_size_t i2)
5982
+ char *ptr1, ca_size_t i1,
5983
+ char *ptr2, ca_size_t i2)
5785
5984
  {
5786
- #{c_type_in} * __restrict__ q1 = (#{c_type_in} *) ptr1;
5787
- #{c_type_out} * __restrict__ q2 = (#{c_type_out} *) ptr2;
5985
+ #{c_type_in} *q1 = (#{c_type_in} *) ptr1;
5986
+ #{c_type_out} *q2 = (#{c_type_out} *) ptr2;
5788
5987
  ca_size_t k;
5789
5988
  if ( m ) {
5790
5989
  #{c_type_in} *p1 = q1;
@@ -5800,17 +5999,38 @@ module MkKernel
5800
5999
  }
5801
6000
  } else if ( i1 == 1 && i2 == 1 ) {
5802
6001
  /* contig branch: SIMD-friendly form. Index-form `q[k]` exposes
5803
- the unit-stride access to the vectorizer; `__restrict__` on
5804
- q1/q2 promises no aliasing so the omp simd pragma can license
5805
- lane-parallel loads/stores. Element-wise monop has no
5806
- reassoc, so ε-close numeric contract is unaffected. */
6002
+ the unit-stride access to the vectorizer. Element-wise monop
6003
+ has no reassoc, so ε-close numeric contract is unaffected.
6004
+ The input and output are either apart, the same cells (a
6005
+ bang, or the lazy scratch applied in place), or overlapping
6006
+ otherwise; `__restrict__` is promised only when apart, since
6007
+ the body may store part of a cell before it has read it. */
5807
6008
  #{c_type_in} *p1;
5808
6009
  #{c_type_out} *p2;
5809
6010
  (void)p1; (void)p2;
5810
- #pragma omp simd
5811
- for (k=0; k<n; k++) {
5812
- p1 = q1 + k; p2 = q2 + k;
5813
- { #{expr_body} }
6011
+ uintptr_t a1 = (uintptr_t) q1, a2 = (uintptr_t) q2;
6012
+ if ( a2 + n * sizeof(*q2) <= a1 || a1 + n * sizeof(*q1) <= a2 ) {
6013
+ #{c_type_in} * __restrict__ r1 = q1;
6014
+ #{c_type_out} * __restrict__ r2 = q2;
6015
+ #pragma omp simd
6016
+ for (k=0; k<n; k++) {
6017
+ p1 = r1 + k; p2 = r2 + k;
6018
+ { #{expr_body} }
6019
+ }
6020
+ }
6021
+ else if ( a1 == a2 && sizeof(*q1) == sizeof(*q2) ) {
6022
+ /* in place: an iteration reads and writes only its own cell */
6023
+ #pragma omp simd
6024
+ for (k=0; k<n; k++) {
6025
+ p1 = q1 + k; p2 = q2 + k;
6026
+ { #{expr_body} }
6027
+ }
6028
+ }
6029
+ else {
6030
+ for (k=0; k<n; k++) {
6031
+ p1 = q1 + k; p2 = q2 + k;
6032
+ { #{expr_body} }
6033
+ }
5814
6034
  }
5815
6035
  } else {
5816
6036
  /* strided branch: numo-shape `p += s` increment form. */
@@ -5993,10 +6213,14 @@ module MkKernel
5993
6213
  # `op_powi_<type>` resolves to `op_powi_int8_t` for int8 source).
5994
6214
  # Substitution uses math_suffix (= typedef name, matches the suffix
5995
6215
  # used in op_powi macro instantiations in ca_op_powi.h).
6216
+ # An object body calls Ruby on every cell and does not vectorize; the
6217
+ # pragma there only adds a "loop not vectorized" warning.
6218
+ simd = ( src == :object ) ? "" : "#pragma omp simd"
5996
6219
  expr_body = raw.gsub(/#1/, "*p1").gsub(/#2/, "*p2").gsub(/#3/, "*p3").gsub(/<type>/, suffix)
5997
6220
  # Option B+C pilot (binop emit substrate hardening, MEMO_BINOP_STRIDED_AUDIT.md):
5998
6221
  # The no-mask body is split into a contig branch (i1==i2==i3==1) using the
5999
- # `q[k]` index form with `__restrict__`-qualified pointers + `#pragma omp simd`
6222
+ # `q[k]` index form with `#pragma omp simd` (`__restrict__`-qualified only
6223
+ # when the output is apart from both inputs)
6000
6224
  # (SL.2.x-style insurance, element-wise has no reassoc -> ε-close unaffected),
6001
6225
  # and a strided branch using `p += i` increment form (numo-shape inner loop).
6002
6226
  # `i1`/`i2`/`i3` remain element units (NOT byte) -- byte-flip is Option A scope.
@@ -6006,13 +6230,13 @@ module MkKernel
6006
6230
 
6007
6231
  static void
6008
6232
  ca_binop_#{k[:name]}_#{suffix} (ca_size_t n, boolean8_t *m,
6009
- char * __restrict__ ptr1, ca_size_t i1,
6010
- char * __restrict__ ptr2, ca_size_t i2,
6011
- char * __restrict__ ptr3, ca_size_t i3)
6233
+ char *ptr1, ca_size_t i1,
6234
+ char *ptr2, ca_size_t i2,
6235
+ char *ptr3, ca_size_t i3)
6012
6236
  {
6013
- #{c_type} * __restrict__ q1 = (#{c_type} *) ptr1;
6014
- #{c_type} * __restrict__ q2 = (#{c_type} *) ptr2;
6015
- #{c_type} * __restrict__ q3 = (#{c_type} *) ptr3;
6237
+ #{c_type} *q1 = (#{c_type} *) ptr1;
6238
+ #{c_type} *q2 = (#{c_type} *) ptr2;
6239
+ #{c_type} *q3 = (#{c_type} *) ptr3;
6016
6240
  ca_size_t k;
6017
6241
  if ( m ) {
6018
6242
  #{c_type} *p1 = q1, *p2 = q2, *p3 = q3;
@@ -6028,16 +6252,101 @@ module MkKernel
6028
6252
  }
6029
6253
  } else if ( i1 == 1 && i2 == 1 && i3 == 1 ) {
6030
6254
  /* contig branch: SIMD-friendly form. Index-form `q[k]` exposes
6031
- the unit-stride access to the vectorizer; `__restrict__` on
6032
- q1/q2/q3 promises no aliasing so the omp simd pragma can
6033
- license lane-parallel loads/stores. Element-wise binop has
6034
- no reassoc, so ε-close numeric contract is unaffected. */
6255
+ the unit-stride access to the vectorizer. Element-wise binop
6256
+ has no reassoc, so ε-close numeric contract is unaffected.
6257
+ The output is apart from each input, the same cells as it (a
6258
+ bang), or overlaps it otherwise; `__restrict__` is promised
6259
+ only when apart from both, since the body may store part of a
6260
+ cell before it has read it. */
6035
6261
  #{c_type} *p1, *p2, *p3;
6262
+ uintptr_t a1 = (uintptr_t) q1, a2 = (uintptr_t) q2, a3 = (uintptr_t) q3;
6263
+ uintptr_t nb = (uintptr_t) n * sizeof(*q3);
6264
+ int apart1 = ( a3 + nb <= a1 || a1 + nb <= a3 );
6265
+ int apart2 = ( a3 + nb <= a2 || a2 + nb <= a3 );
6036
6266
  (void)p1; (void)p2; (void)p3;
6037
- #pragma omp simd
6038
- for (k=0; k<n; k++) {
6039
- p1 = q1 + k; p2 = q2 + k; p3 = q3 + k;
6040
- { #{expr_body} }
6267
+ if ( apart1 && apart2 ) {
6268
+ #{c_type} * __restrict__ r1 = q1;
6269
+ #{c_type} * __restrict__ r2 = q2;
6270
+ #{c_type} * __restrict__ r3 = q3;
6271
+ #{simd}
6272
+ for (k=0; k<n; k++) {
6273
+ p1 = r1 + k; p2 = r2 + k; p3 = r3 + k;
6274
+ { #{expr_body} }
6275
+ }
6276
+ }
6277
+ else if ( ( apart1 || a1 == a3 ) && ( apart2 || a2 == a3 ) ) {
6278
+ /* in place: an iteration reads and writes only its own cell */
6279
+ #{simd}
6280
+ for (k=0; k<n; k++) {
6281
+ p1 = q1 + k; p2 = q2 + k; p3 = q3 + k;
6282
+ { #{expr_body} }
6283
+ }
6284
+ }
6285
+ else {
6286
+ for (k=0; k<n; k++) {
6287
+ p1 = q1 + k; p2 = q2 + k; p3 = q3 + k;
6288
+ { #{expr_body} }
6289
+ }
6290
+ }
6291
+ } else if ( i3 == 1 && ( ( i1 == 1 && i2 == 0 ) || ( i1 == 0 && i2 == 1 ) ) ) {
6292
+ /* scalar branch: one operand is a scalar (stride 0). It is read
6293
+ into a local before the loop, so a store to the output cannot
6294
+ change it and the loop vectorizes like the contig branch. */
6295
+ #{c_type} sv = ( i1 == 0 ) ? *q1 : *q2;
6296
+ #{c_type} *qa = ( i1 == 0 ) ? q2 : q1;
6297
+ #{c_type} *p1, *p2, *p3;
6298
+ uintptr_t aa = (uintptr_t) qa, a3 = (uintptr_t) q3;
6299
+ uintptr_t nb = (uintptr_t) n * sizeof(*q3);
6300
+ (void)p1; (void)p2; (void)p3;
6301
+ if ( i1 == 0 ) {
6302
+ p1 = &sv;
6303
+ if ( a3 + nb <= aa || aa + nb <= a3 ) {
6304
+ #{c_type} * __restrict__ ra = qa;
6305
+ #{c_type} * __restrict__ r3 = q3;
6306
+ #{simd}
6307
+ for (k=0; k<n; k++) {
6308
+ p2 = ra + k; p3 = r3 + k;
6309
+ { #{expr_body} }
6310
+ }
6311
+ }
6312
+ else if ( aa == a3 ) {
6313
+ #{simd}
6314
+ for (k=0; k<n; k++) {
6315
+ p2 = qa + k; p3 = q3 + k;
6316
+ { #{expr_body} }
6317
+ }
6318
+ }
6319
+ else {
6320
+ for (k=0; k<n; k++) {
6321
+ p2 = qa + k; p3 = q3 + k;
6322
+ { #{expr_body} }
6323
+ }
6324
+ }
6325
+ }
6326
+ else {
6327
+ p2 = &sv;
6328
+ if ( a3 + nb <= aa || aa + nb <= a3 ) {
6329
+ #{c_type} * __restrict__ ra = qa;
6330
+ #{c_type} * __restrict__ r3 = q3;
6331
+ #{simd}
6332
+ for (k=0; k<n; k++) {
6333
+ p1 = ra + k; p3 = r3 + k;
6334
+ { #{expr_body} }
6335
+ }
6336
+ }
6337
+ else if ( aa == a3 ) {
6338
+ #{simd}
6339
+ for (k=0; k<n; k++) {
6340
+ p1 = qa + k; p3 = q3 + k;
6341
+ { #{expr_body} }
6342
+ }
6343
+ }
6344
+ else {
6345
+ for (k=0; k<n; k++) {
6346
+ p1 = qa + k; p3 = q3 + k;
6347
+ { #{expr_body} }
6348
+ }
6349
+ }
6041
6350
  }
6042
6351
  } else {
6043
6352
  /* strided branch: numo-shape `p += s` increment form. */
@@ -6371,6 +6680,24 @@ module MkKernel
6371
6680
  { #{expr_body} }
6372
6681
  }
6373
6682
  }
6683
+ } else if ( i3 == 1 && i1 == 1 && i2 == 0 ) {
6684
+ /* scalar on the right: read it once, so the boolean store
6685
+ cannot be taken to change it and the loop vectorizes */
6686
+ #{c_type} sv = *q2;
6687
+ p2 = &sv;
6688
+ for (k=0; k<n; k++) {
6689
+ p1 = q1 + k;
6690
+ p3 = q3 + k;
6691
+ { #{expr_body} }
6692
+ }
6693
+ } else if ( i3 == 1 && i1 == 0 && i2 == 1 ) {
6694
+ #{c_type} sv = *q1;
6695
+ p1 = &sv;
6696
+ for (k=0; k<n; k++) {
6697
+ p2 = q2 + k;
6698
+ p3 = q3 + k;
6699
+ { #{expr_body} }
6700
+ }
6374
6701
  } else {
6375
6702
  for (k=0; k<n; k++) {
6376
6703
  p1 = q1 + k*i1;
@@ -6500,13 +6827,21 @@ module MkKernel
6500
6827
  cmplx64 cmplx128].freeze
6501
6828
  CAST_INEXACT = %i[f32 f64 cmplx64 cmplx128].freeze
6502
6829
 
6830
+ # The plain C type of a data type, for a body that leaves this file
6831
+ # (CArray.__kernel_body__, a fusion plan): the unaligned typedef in :c is
6832
+ # about how a kernel reaches a cell, not about the value.
6833
+ def self.value_c(src)
6834
+ { "ca_u_float32_t" => "float", "ca_u_float64_t" => "double" }
6835
+ .fetch(DTYPES[src][:c]) { |c| c.sub(/\Aca_u_/, "") }
6836
+ end
6837
+
6503
6838
  def self.cast_body_rows
6504
6839
  CAST_SOURCES.product(CAST_TARGETS).filter_map { |src, dst|
6505
6840
  next if CAST_INEXACT.include?(src) && !CAST_INEXACT.include?(dst)
6506
6841
  name = DTYPES[dst][:ca].sub(/\ACA_/, "").downcase
6507
6842
  via = ( %i[i64 u64].include?(src) && %i[f32 cmplx64].include?(dst) ) ? "(double)" : ""
6508
6843
  ["monop", "cast_#{name}", DTYPES[src][:ca].sub(/\ACA_/, "").downcase,
6509
- "(#2) = (#{DTYPES[dst][:c]})#{via}(#1);"]
6844
+ "(#2) = (#{value_c(dst)})#{via}(#1);"]
6510
6845
  }
6511
6846
  end
6512
6847
 
@@ -6638,7 +6973,7 @@ module MkKernel
6638
6973
  # surface) + optional CAMath module function, NOT as `<name>_ki`.
6639
6974
  if k[:kind] == :monop
6640
6975
  io.puts %Q[ rb_define_method(rb_cCArray, "#{k[:op]}", rb_ca_#{k[:name]}, 0);] if k[:bind]
6641
- io.puts %Q[ rb_define_method(rb_cCArray, "#{k[:name]}!", rb_ca_#{k[:name]}_bang, 0);]
6976
+ io.puts %Q[ rb_define_method(rb_cCArray, "#{k[:name]}!", rb_ca_#{k[:name]}_bang, 0);] if k[:bang]
6642
6977
  io.puts %Q[ rb_define_module_function(rb_mCAMath, "#{k[:name]}", rb_cmath_#{k[:name]}, 1);] if k[:cmath]
6643
6978
  return
6644
6979
  end
@@ -6866,11 +7201,10 @@ MkKernel.reduce :max,
6866
7201
  # its identity value (1 for all, 0 for any) for those cells. An all-
6867
7202
  # masked slab returns the identity (all([]) == true, any([]) == false).
6868
7203
  #
6869
- # Early-break is intentionally NOT implemented; idempotent acc &= v /
6870
- # acc |= v is SIMD-friendly and the branch-free form usually wins on
6871
- # modern CPUs. For dramatically lopsided inputs (= mostly-false `all`
6872
- # or mostly-true `any`) a profile-driven early-break variant can be
6873
- # added later as a separate kernel.
7204
+ # The walk stops once the answer is settled (settled:): any at the first
7205
+ # true, all and none at the first false / true. The test runs between
7206
+ # blocks of branch-free `acc |= v` / `acc &= v`, so a slab that never
7207
+ # settles keeps the speed of the plain fold.
6874
7208
  MkKernel.header_block <<~C
6875
7209
  /* The square root of a variance: an ε-negative from rounding becomes 0,
6876
7210
  a NaN stays NaN (fmax(NaN, 0.0) is 0.0). */
@@ -6894,6 +7228,8 @@ C
6894
7228
  MkKernel.reduce :all,
6895
7229
  init: "1",
6896
7230
  reduce: "acc &= v",
7231
+ settled: "!acc",
7232
+ reduction_kind: :min,
6897
7233
  source: [:bool],
6898
7234
  output: :bool,
6899
7235
  ruby_scalar: :BOOL2VAL,
@@ -6903,6 +7239,8 @@ MkKernel.reduce :all,
6903
7239
  MkKernel.reduce :any,
6904
7240
  init: "0",
6905
7241
  reduce: "acc |= v",
7242
+ settled: "acc",
7243
+ reduction_kind: :max,
6906
7244
  source: [:bool],
6907
7245
  output: :bool,
6908
7246
  ruby_scalar: :BOOL2VAL,
@@ -6917,6 +7255,8 @@ MkKernel.reduce :any,
6917
7255
  MkKernel.reduce :none,
6918
7256
  init: "1",
6919
7257
  reduce: "acc &= !v",
7258
+ settled: "!acc",
7259
+ reduction_kind: :min,
6920
7260
  source: [:bool],
6921
7261
  output: :bool,
6922
7262
  ruby_scalar: :BOOL2VAL,
@@ -7365,9 +7705,9 @@ MkKernel.reduce :wsum,
7365
7705
  # framework as wsum, but uses state+finish form (= sum + den) for the
7366
7706
  # normalisation `mean = Σv*w / Σw` semantic.
7367
7707
  #
7368
- # Edge case: if all valid weights sum to zero (= den == 0 but some cells
7369
- # valid), finish evaluates 0/0 -> NaN. Legacy proc_wmean had the same
7370
- # undefined behavior (no division check); mkkernel preserves it.
7708
+ # Weights summing to zero (den == 0 with cells present, weights of 0 or of
7709
+ # both signs) leave the mean undefined: the cell is UNDEF, as for a slab
7710
+ # with no valid cell, rather than 0/0 = NaN or x/0 = +-Infinity.
7371
7711
  #
7372
7712
  # 3.0 breaking semantic changes vs legacy (= identical to wsum W.2):
7373
7713
  # - calling convention positional -> kwargs (axes positional, opts kw)
@@ -7384,6 +7724,7 @@ MkKernel.reduce :wmean,
7384
7724
  reduce: "(sum += (double) v * (double) w, den += (double) w)",
7385
7725
  reduction_kind: { sum: :plus, den: :plus }, # SL.1.4b
7386
7726
  finish: "sum / den",
7727
+ undefined_if: "den == 0",
7387
7728
  source: MkKernel::ALL_NUMERIC,
7388
7729
  output: :f64,
7389
7730
  ruby_scalar: :rb_float_new,
@@ -7678,8 +8019,8 @@ MkKernel.search :find_value_index,
7678
8019
  mask_self: :raise,
7679
8020
  fallback: :raise
7680
8021
 
7681
- # bsearch_ki: per-slice binary search returning the matching position
7682
- # (assumes a sorted slab). NaN handling: NaN sorts to end; query == NaN
8022
+ # bsearch_ki: per-slice binary search returning the first matching position
8023
+ # (assumes a sorted slab; of equal cells, the first). NaN handling: NaN sorts to end; query == NaN
7683
8024
  # never matches. Hand-rolled strided binary search (= libc bsearch()
7684
8025
  # requires contig memory; slab_stride may be > sizeof(T_LOAD) for a
7685
8026
  # non-innermost search axis). NaN guards are dead code for integer
@@ -7705,7 +8046,7 @@ MkKernel.search :bsearch,
7705
8046
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7706
8047
  if ( v < query_val ) lo = mid + 1;
7707
8048
  else if ( v > query_val ) hi = mid;
7708
- else { result = mid; break; }
8049
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7709
8050
  }
7710
8051
  }
7711
8052
  C
@@ -7720,7 +8061,7 @@ MkKernel.search :bsearch,
7720
8061
  int c = CA_FLOAT_ORDER_CMP(v, query_val);
7721
8062
  if ( c < 0 ) lo = mid + 1;
7722
8063
  else if ( c > 0 ) hi = mid;
7723
- else { result = mid; break; }
8064
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7724
8065
  }
7725
8066
  }
7726
8067
  C
@@ -7737,7 +8078,7 @@ MkKernel.search :bsearch,
7737
8078
  int c = ca_obj_order_cmp(v, query_val);
7738
8079
  if ( c < 0 ) lo = mid + 1;
7739
8080
  else if ( c > 0 ) hi = mid;
7740
- else { result = mid; break; }
8081
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7741
8082
  }
7742
8083
  C
7743
8084
  fixlen: <<~C,
@@ -7753,7 +8094,7 @@ MkKernel.search :bsearch,
7753
8094
  int c = memcmp(v, query_val, (size_t) ca->bytes);
7754
8095
  if ( c < 0 ) lo = mid + 1;
7755
8096
  else if ( c > 0 ) hi = mid;
7756
- else { result = mid; break; }
8097
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7757
8098
  }
7758
8099
  C
7759
8100
  },
@@ -7938,7 +8279,7 @@ MkKernel.search :bsearch_addr,
7938
8279
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7939
8280
  if ( v < query_val ) lo = mid + 1;
7940
8281
  else if ( v > query_val ) hi = mid;
7941
- else { result = mid; break; }
8282
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7942
8283
  }
7943
8284
  }
7944
8285
  C
@@ -7953,7 +8294,7 @@ MkKernel.search :bsearch_addr,
7953
8294
  int c = CA_FLOAT_ORDER_CMP(v, query_val);
7954
8295
  if ( c < 0 ) lo = mid + 1;
7955
8296
  else if ( c > 0 ) hi = mid;
7956
- else { result = mid; break; }
8297
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7957
8298
  }
7958
8299
  }
7959
8300
  C
@@ -7967,7 +8308,7 @@ MkKernel.search :bsearch_addr,
7967
8308
  int c = ca_obj_order_cmp(v, query_val);
7968
8309
  if ( c < 0 ) lo = mid + 1;
7969
8310
  else if ( c > 0 ) hi = mid;
7970
- else { result = mid; break; }
8311
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7971
8312
  }
7972
8313
  C
7973
8314
  fixlen: <<~C,
@@ -7981,7 +8322,7 @@ MkKernel.search :bsearch_addr,
7981
8322
  int c = memcmp(v, query_val, (size_t) ca->bytes);
7982
8323
  if ( c < 0 ) lo = mid + 1;
7983
8324
  else if ( c > 0 ) hi = mid;
7984
- else { result = mid; break; }
8325
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7985
8326
  }
7986
8327
  C
7987
8328
  },
@@ -8152,13 +8493,13 @@ MkKernel.search :linear_section_binary,
8152
8493
  if ( slab_n <= 1 ) {
8153
8494
  /* A one-point axis has no segment: only the point itself is in
8154
8495
  * range, at 0. An empty one has nothing in range. */
8155
- if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8496
+ if ( slab_n == 1 && query_val == *(ca_u_float64_t *)slab_ptr ) {
8156
8497
  result = 0.0;
8157
8498
  }
8158
8499
  }
8159
8500
  else {
8160
- double y0 = *(double *)(slab_ptr + 0 * slab_stride);
8161
- double yn = *(double *)(slab_ptr + (slab_n - 1) * slab_stride);
8501
+ double y0 = *(ca_u_float64_t *)(slab_ptr + 0 * slab_stride);
8502
+ double yn = *(ca_u_float64_t *)(slab_ptr + (slab_n - 1) * slab_stride);
8162
8503
  int ascending = ( yn >= y0 ); /* grid direction (monotonic assumed) */
8163
8504
  ca_size_t x1 = 0;
8164
8505
  int found = 0;
@@ -8181,8 +8522,8 @@ MkKernel.search :linear_section_binary,
8181
8522
  double gd = (query_val - y0) / (yn - y0) * (double)(slab_n - 1);
8182
8523
  if ( gd >= 0.0 && gd < (double)(slab_n - 1) ) {
8183
8524
  ca_size_t ag = (ca_size_t) gd;
8184
- double yga = *(double *)(slab_ptr + ag * slab_stride);
8185
- double ygb = *(double *)(slab_ptr + (ag + 1) * slab_stride);
8525
+ double yga = *(ca_u_float64_t *)(slab_ptr + ag * slab_stride);
8526
+ double ygb = *(ca_u_float64_t *)(slab_ptr + (ag + 1) * slab_stride);
8186
8527
  if ( (yga - query_val) * (ygb - query_val) <= 0 ) {
8187
8528
  x1 = ag; found = 1;
8188
8529
  }
@@ -8197,7 +8538,7 @@ MkKernel.search :linear_section_binary,
8197
8538
  double ya = y0;
8198
8539
  while ( (b - a) > 1 ) {
8199
8540
  ca_size_t c = (a + b) / 2;
8200
- double yc = *(double *)(slab_ptr + c * slab_stride);
8541
+ double yc = *(ca_u_float64_t *)(slab_ptr + c * slab_stride);
8201
8542
  if ( (ya - query_val) * (yc - query_val) <= 0 ) { b = c; }
8202
8543
  else { a = c; ya = yc; }
8203
8544
  }
@@ -8206,8 +8547,8 @@ MkKernel.search :linear_section_binary,
8206
8547
  }
8207
8548
 
8208
8549
  if ( found ) {
8209
- double y1 = *(double *)(slab_ptr + x1 * slab_stride);
8210
- double y2 = *(double *)(slab_ptr + (x1 + 1) * slab_stride);
8550
+ double y1 = *(ca_u_float64_t *)(slab_ptr + x1 * slab_stride);
8551
+ double y2 = *(ca_u_float64_t *)(slab_ptr + (x1 + 1) * slab_stride);
8211
8552
  double rest = (query_val - y1) / (y2 - y1);
8212
8553
  if ( y2 != 0.0 && fabs(y2 - query_val) / fabs(y2) < DBL_EPSILON * 100 ) {
8213
8554
  result = (double)(x1 + 1);
@@ -8243,7 +8584,7 @@ MkKernel.search :linear_section_linear,
8243
8584
  if ( slab_n <= 1 ) {
8244
8585
  /* A one-point axis has no segment: only the point itself is in
8245
8586
  * range, at 0. An empty one has nothing in range. */
8246
- if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8587
+ if ( slab_n == 1 && query_val == *(ca_u_float64_t *)slab_ptr ) {
8247
8588
  result = 0.0;
8248
8589
  }
8249
8590
  }
@@ -8251,15 +8592,15 @@ MkKernel.search :linear_section_linear,
8251
8592
  ca_size_t x1 = 0;
8252
8593
  int found = 0;
8253
8594
  for ( ca_size_t k = 0; k < slab_n - 1; k++ ) {
8254
- double yk = *(double *)(slab_ptr + k * slab_stride);
8255
- double yk1 = *(double *)(slab_ptr + (k + 1) * slab_stride);
8595
+ double yk = *(ca_u_float64_t *)(slab_ptr + k * slab_stride);
8596
+ double yk1 = *(ca_u_float64_t *)(slab_ptr + (k + 1) * slab_stride);
8256
8597
  if ( (query_val - yk) * (query_val - yk1) <= 0 ) {
8257
8598
  x1 = k; found = 1; break;
8258
8599
  }
8259
8600
  }
8260
8601
  if ( found ) {
8261
- double y1 = *(double *)(slab_ptr + x1 * slab_stride);
8262
- double y2 = *(double *)(slab_ptr + (x1 + 1) * slab_stride);
8602
+ double y1 = *(ca_u_float64_t *)(slab_ptr + x1 * slab_stride);
8603
+ double y2 = *(ca_u_float64_t *)(slab_ptr + (x1 + 1) * slab_stride);
8263
8604
  result = (query_val - y1) / (y2 - y1) + (double) x1;
8264
8605
  }
8265
8606
 
@@ -8286,8 +8627,8 @@ MkKernel.search :linear_fetch,
8286
8627
  ca_size_t il = (ca_size_t) floor(query_val);
8287
8628
  ca_size_t iu = (ca_size_t) ceil(query_val);
8288
8629
  double w = query_val - floor(query_val);
8289
- double yl = *(double *)(slab_ptr + il * slab_stride);
8290
- double yu = *(double *)(slab_ptr + iu * slab_stride);
8630
+ double yl = *(ca_u_float64_t *)(slab_ptr + il * slab_stride);
8631
+ double yu = *(ca_u_float64_t *)(slab_ptr + iu * slab_stride);
8291
8632
  result = yu * w + yl * (1.0 - w);
8292
8633
  }
8293
8634
  C
@@ -8540,7 +8881,11 @@ MkKernel.monfunc :floor,
8540
8881
  object: '(#2) = rb_funcall((#1), rb_intern("floor"), 0);',
8541
8882
  }
8542
8883
 
8884
+ # round: the Ruby methods `round(ndigits)` / `round!(ndigits)` are defined in
8885
+ # carray_round.c on top of this kernel, so none is registered here.
8543
8886
  MkKernel.monfunc :round,
8887
+ bind: false,
8888
+ bang: false,
8544
8889
  source: MkKernel::ALL_NUMERIC + [:object],
8545
8890
  expr: {
8546
8891
  int: "(#2) = (#1);",
@@ -8847,7 +9192,7 @@ MkKernel.monfunc :rad_pi,
8847
9192
  # Pair-wise max / min — three flavours:
8848
9193
  #
8849
9194
  # pmax / pmin CArray legacy names (kept for backwards compat),
8850
- # NaN-skip semantics via C99 fmax / fmin.
9195
+ # NaN-skip semantics, as C99 fmax / fmin.
8851
9196
  # fmax / fmin Ruby-level aliases of pmax / pmin (= same kernel,
8852
9197
  # same NaN-skip behaviour). Matches the C99 library
8853
9198
  # function of the same name.
@@ -8857,22 +9202,26 @@ MkKernel.monfunc :rad_pi,
8857
9202
  #
8858
9203
  # Integer branches are identical across all three (no NaN). The float
8859
9204
  # and object branches differ: pmax / pmin let a NaN lose (an object cell's
8860
- # Float NaN included), maximum / minimum let it win.
9205
+ # Float NaN included), maximum / minimum let it win. In both flavours, of
9206
+ # two zeros the max is +0 and the min is -0 in either order. C's fmax /
9207
+ # fmin leave that choice to the library (glibc and macOS answer
9208
+ # differently), so the float branch does not call them. The sign of a
9209
+ # zero is read with copysign(1, x) rather than signbit(x): GCC does not
9210
+ # vectorize signbit on a double, which left the float64 kernels 17x
9211
+ # slower than float32. NaN is handled before, so copysign never sees one.
8861
9212
  MkKernel.binop :pmax,
8862
9213
  source: MkKernel::ALL_NUMERIC + [:object],
8863
9214
  expr: {
8864
9215
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8865
- float: "(#3) = fmax(#1, #2);",
8866
9216
  object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern(">")) ? (#2) : (#1);',
8867
- }
9217
+ }.merge(MkKernel.float_widths("(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) > (#2) || ((#1) == (#2) && copysign<f>(1, (#1)) > 0)) ? (#1) : (#2);"))
8868
9218
 
8869
9219
  MkKernel.binop :pmin,
8870
9220
  source: MkKernel::ALL_NUMERIC + [:object],
8871
9221
  expr: {
8872
9222
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8873
- float: "(#3) = fmin(#1, #2);",
8874
9223
  object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern("<")) ? (#2) : (#1);',
8875
- }
9224
+ }.merge(MkKernel.float_widths("(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) < (#2) || ((#1) == (#2) && copysign<f>(1, (#1)) < 0)) ? (#1) : (#2);"))
8876
9225
 
8877
9226
  # NaN-propagate variants. Float
8878
9227
  # branch tests both operands for NaN and short-circuits to NaN; non-
@@ -8883,17 +9232,15 @@ MkKernel.binop :maximum,
8883
9232
  source: MkKernel::ALL_NUMERIC + [:object],
8884
9233
  expr: {
8885
9234
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8886
- float: "(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) > (#2) || ((#1) == (#2) && !signbit(#1))) ? (#1) : (#2);",
8887
9235
  object: '(#3) = CA_OBJ_ISNAN(#1) ? (#1) : CA_OBJ_ISNAN(#2) ? (#2) : ca_obj_cmp((#2), (#1)) > 0 ? (#2) : (#1);',
8888
- }
9236
+ }.merge(MkKernel.float_widths("(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) > (#2) || ((#1) == (#2) && copysign<f>(1, (#1)) > 0)) ? (#1) : (#2);"))
8889
9237
 
8890
9238
  MkKernel.binop :minimum,
8891
9239
  source: MkKernel::ALL_NUMERIC + [:object],
8892
9240
  expr: {
8893
9241
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8894
- float: "(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) < (#2) || ((#1) == (#2) && signbit(#1))) ? (#1) : (#2);",
8895
9242
  object: '(#3) = CA_OBJ_ISNAN(#1) ? (#1) : CA_OBJ_ISNAN(#2) ? (#2) : ca_obj_cmp((#2), (#1)) < 0 ? (#2) : (#1);',
8896
- }
9243
+ }.merge(MkKernel.float_widths("(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) < (#2) || ((#1) == (#2) && copysign<f>(1, (#1)) < 0)) ? (#1) : (#2);"))
8897
9244
 
8898
9245
  # + and - work on the parts independently, so one generic expression
8899
9246
  # covers every numeric data_type at any width.