carray 3.0.4 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +205 -0
  3. data/carray.gemspec +1 -1
  4. data/ext/ca_array_pool.c +25 -1
  5. data/ext/ca_axis_dispatch.c +13 -20
  6. data/ext/ca_categorical_iterator.c +27 -20
  7. data/ext/ca_compare.c +4 -4
  8. data/ext/ca_composite_dispatch.c +9 -9
  9. data/ext/ca_kernel_iterator.c +6 -6
  10. data/ext/ca_kernel_iterator.h +112 -83
  11. data/ext/ca_obj_array.c +69 -19
  12. data/ext/ca_obj_bincmp.c +1 -0
  13. data/ext/ca_obj_binop.c +1 -0
  14. data/ext/ca_obj_bitarray.c +5 -1
  15. data/ext/ca_obj_bitfield.c +58 -43
  16. data/ext/ca_obj_byte_swap.c +2 -1
  17. data/ext/ca_obj_const_string.c +15 -3
  18. data/ext/ca_obj_face.c +42 -1
  19. data/ext/ca_obj_field.c +13 -3
  20. data/ext/ca_obj_grid.c +52 -5
  21. data/ext/ca_obj_monop.c +46 -10
  22. data/ext/ca_obj_object.c +64 -12
  23. data/ext/ca_obj_record.c +3 -0
  24. data/ext/ca_obj_reduce.c +4 -0
  25. data/ext/ca_obj_refer.c +18 -9
  26. data/ext/ca_obj_roll.c +26 -6
  27. data/ext/ca_obj_select.c +34 -5
  28. data/ext/ca_obj_shift.c +31 -7
  29. data/ext/ca_obj_stack.c +1 -0
  30. data/ext/ca_obj_stride.c +28 -19
  31. data/ext/ca_obj_string.c +11 -2
  32. data/ext/ca_obj_tile.c +27 -7
  33. data/ext/ca_obj_transpose.c +8 -4
  34. data/ext/ca_obj_triop.c +3 -0
  35. data/ext/ca_obj_window.c +79 -50
  36. data/ext/ca_op_ipower.c +4 -4
  37. data/ext/ca_sort_kernels.h +6 -3
  38. data/ext/carray.h +33 -0
  39. data/ext/carray_access.c +16 -3
  40. data/ext/carray_attribute.c +24 -0
  41. data/ext/carray_cast.c +243 -82
  42. data/ext/carray_cast_func.rb +56 -26
  43. data/ext/carray_conversion.c +2 -2
  44. data/ext/carray_core.c +3 -0
  45. data/ext/carray_count.c +6 -0
  46. data/ext/carray_element.c +9 -4
  47. data/ext/carray_factorize.c +20 -20
  48. data/ext/carray_generate.c +8 -2
  49. data/ext/carray_histogram.c +1 -1
  50. data/ext/carray_index_classifier.c +4 -4
  51. data/ext/carray_internal.h +12 -0
  52. data/ext/carray_kernels_bincmp.c +168 -168
  53. data/ext/carray_kernels_binop.c +900 -900
  54. data/ext/carray_kernels_init.c +4 -4
  55. data/ext/carray_kernels_moncmp.c +96 -96
  56. data/ext/carray_kernels_monop.c +1984 -1984
  57. data/ext/carray_kernels_reduce_aggregate.c +1733 -1733
  58. data/ext/carray_kernels_reduce_cumulative.c +642 -642
  59. data/ext/carray_kernels_reduce_extreme.c +1150 -1150
  60. data/ext/carray_kernels_reduce_variance.c +258 -258
  61. data/ext/carray_kernels_scan.c +128 -128
  62. data/ext/carray_kernels_search.c +340 -340
  63. data/ext/carray_kernels_sort.c +295 -304
  64. data/ext/carray_kernels_triop.c +114 -114
  65. data/ext/carray_key_windows.c +4 -0
  66. data/ext/carray_mask.c +45 -22
  67. data/ext/carray_memory_view.c +2 -2
  68. data/ext/carray_operator.c +5 -5
  69. data/ext/carray_order.c +227 -8
  70. data/ext/carray_random.c +18 -18
  71. data/ext/carray_scatter.c +24 -7
  72. data/ext/carray_slab.c +32 -9
  73. data/ext/carray_sort.c +224 -16
  74. data/ext/carray_sort_kernel.c +5 -7
  75. data/ext/carray_struct.c +35 -43
  76. data/ext/carray_time_text.c +4 -2
  77. data/ext/carray_utils.c +63 -21
  78. data/ext/mkkernel.rb +210 -115
  79. data/ext/version.h +4 -4
  80. data/lib/carray/attribute.rb +17 -4
  81. data/lib/carray/basics.rb +27 -7
  82. data/lib/carray/categorical.rb +30 -16
  83. data/lib/carray/categorical_iterator.rb +6 -6
  84. data/lib/carray/const_string.rb +10 -0
  85. data/lib/carray/data_type_extension.rb +8 -1
  86. data/lib/carray/fixlen_string.rb +41 -2
  87. data/lib/carray/frame/column_verbs.rb +4 -3
  88. data/lib/carray/frame/concat.rb +17 -3
  89. data/lib/carray/frame/frame.rb +6 -7
  90. data/lib/carray/frame/group.rb +100 -13
  91. data/lib/carray/frame/io.rb +3 -1
  92. data/lib/carray/fuse_source.rb +31 -2
  93. data/lib/carray/lazy.rb +35 -0
  94. data/lib/carray/methods/is_in.rb +8 -1
  95. data/lib/carray/segment_iterator.rb +12 -8
  96. data/lib/carray/serialize.rb +19 -2
  97. data/lib/carray/stack.rb +15 -12
  98. data/lib/carray/string.rb +8 -0
  99. data/lib/carray/string_operation_extension.rb +13 -5
  100. data/lib/carray/struct_builder.rb +5 -3
  101. data/lib/carray/time.rb +107 -68
  102. data/yard-stubs/ca_obj_transpose.rb +5 -4
  103. data/yard-stubs/carray_attribute.rb +3 -1
  104. data/yard-stubs/carray_cast.rb +2 -0
  105. data/yard-stubs/carray_conversion.rb +5 -1
  106. data/yard-stubs/carray_element.rb +7 -6
  107. data/yard-stubs/carray_mask.rb +8 -3
  108. data/yard-stubs/carray_math.rb +14 -4
  109. data/yard-stubs/carray_order.rb +100 -4
  110. data/yard-stubs/carray_scatter.rb +7 -5
  111. metadata +1 -1
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
@@ -1775,13 +1778,15 @@ module MkKernel
1775
1778
 
1776
1779
  # Emits the native function of one source data type. The object lane
1777
1780
  # 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
1781
+ # its function is rebuilt around ca_iter_ensure (see guard_native). So is
1782
+ # every lane of a family that passes guard: true, whose walks allocate
1783
+ # (an output mask made on the first empty slab, a scratch buffer), since a
1784
+ # failed allocation raises too. Other lanes are emitted as written.
1785
+ def self.emit_native_for(io, k, src, guard: false)
1786
+ if src == :object || guard
1782
1787
  buf = StringIO.new
1783
1788
  yield buf
1784
- io.print guard_object_native(buf.string, "#{k[:name]}_ki_native_object")
1789
+ io.print guard_native(buf.string, "#{k[:name]}_ki_native_#{src}")
1785
1790
  else
1786
1791
  yield io
1787
1792
  end
@@ -1822,7 +1827,7 @@ module MkKernel
1822
1827
  # frame is gone once the body raises. The body text is kept as written:
1823
1828
  # the parameters are unpacked from the frame under their own names, and
1824
1829
  # each state name is defined to its slot for the length of the body.
1825
- def self.guard_object_native(text, fn)
1830
+ def self.guard_native(text, fn)
1826
1831
  header = /^static VALUE\n#{fn} \(([^)]*)\)\n\{\n/
1827
1832
  m = header.match(text) or raise "mkkernel: #{fn} not found"
1828
1833
  close = matching_brace(text, m.end(0) - 2)
@@ -1845,9 +1850,9 @@ module MkKernel
1845
1850
 
1846
1851
  out = StringIO.new
1847
1852
  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. */"
1853
+ out.puts "/* The walks can raise part way (a Ruby call per cell in the object"
1854
+ out.puts " lane, an allocation in any lane). Their states live in the caller's"
1855
+ out.puts " frame, and ca_iter_ensure finishes them however the body leaves. */"
1851
1856
  out.puts "typedef struct {"
1852
1857
  out.puts " ca_iter_state states[#{states.size}];"
1853
1858
  params.each { |type, name| out.puts " #{type} #{name};".sub(/\* /, "*") }
@@ -1882,7 +1887,7 @@ module MkKernel
1882
1887
  io.puts
1883
1888
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
1884
1889
  k[:source].each do |src|
1885
- emit_native_for(io, k, src) { |o| emit_reduce_native(o, k, src) }
1890
+ emit_native_for(io, k, src, guard: true) { |o| emit_reduce_native(o, k, src) }
1886
1891
  end
1887
1892
  emit_reduce_dispatch(io, k)
1888
1893
  end
@@ -3571,17 +3576,21 @@ module MkKernel
3571
3576
  wio.puts " cw = cv;"
3572
3577
  wio.puts " }"
3573
3578
  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 */"
3579
+ wio.puts " /* W-A2: 1-D axis-broadcast -> materialize to full shape. cw and"
3580
+ wio.puts " its mask are made before cv is attached: making them allocates. */"
3576
3581
  wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3577
3582
  wio.puts " GetCArray(vcw, cw);"
3583
+ wio.puts " if ( ca_has_mask(cv) ) {"
3584
+ wio.puts " ca_create_mask(cw);"
3585
+ wio.puts " }"
3586
+ wio.puts " ca_attach(cv); /* window: nothing raises inside */"
3578
3587
  wio.puts " /* broadcast cv (1-D, length src->dim[slab_axes[0]]) along reduce axis */"
3579
3588
  wio.puts " {"
3580
3589
  wio.puts " int8_t baxis = slab_axes[0];"
3581
3590
  wio.puts " ca_size_t inner_stride = 1;"
3582
3591
  wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3583
3592
  wio.puts " ca_size_t axis_n = src->dim[baxis];"
3584
- wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3593
+ wio.puts " ca_size_t outer = ( axis_n && inner_stride ) ? src->elements / (axis_n * inner_stride) : 0; /* a zero-length axis leaves nothing to fill */"
3585
3594
  wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3586
3595
  wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3587
3596
  wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
@@ -3593,15 +3602,14 @@ module MkKernel
3593
3602
  wio.puts " }"
3594
3603
  wio.puts " /* Propagate cv mask via W-A3 overlay path below (= mask materialize) */"
3595
3604
  wio.puts " if ( ca_has_mask(cv) ) {"
3596
- wio.puts " /* Allocate cw mask + broadcast cv mask along axis */"
3597
- wio.puts " ca_create_mask(cw);"
3605
+ wio.puts " /* broadcast cv mask along axis */"
3598
3606
  wio.puts " boolean8_t *cw_m = (boolean8_t *)cw->mask->ptr;"
3599
3607
  wio.puts " boolean8_t *cv_m = (boolean8_t *)cv->mask->ptr;"
3600
3608
  wio.puts " int8_t baxis = slab_axes[0];"
3601
3609
  wio.puts " ca_size_t inner_stride = 1;"
3602
3610
  wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3603
3611
  wio.puts " ca_size_t axis_n = src->dim[baxis];"
3604
- wio.puts " ca_size_t outer = src->elements / (axis_n * inner_stride);"
3612
+ wio.puts " ca_size_t outer = ( axis_n && inner_stride ) ? src->elements / (axis_n * inner_stride) : 0; /* a zero-length axis leaves nothing to fill */"
3605
3613
  wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3606
3614
  wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3607
3615
  wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
@@ -3827,6 +3835,8 @@ module MkKernel
3827
3835
  # fiber's mask base.
3828
3836
  mask_setup =
3829
3837
  if gated
3838
+ # Before the walks open: creating the mask allocates, and a raise
3839
+ # inside them would jump past their finish.
3830
3840
  "\n" \
3831
3841
  " boolean8_t *op_mask = NULL;\n" \
3832
3842
  " if ( ca_has_mask(ca) ) {\n" \
@@ -3867,6 +3877,7 @@ module MkKernel
3867
3877
  INIT inside the macro. */
3868
3878
  int8_t slab_axes[CA_RANK_MAX];
3869
3879
  slab_axes[0] = (int8_t) axis;
3880
+ #{mask_setup}
3870
3881
  ca_iter_state st_in, st_out;
3871
3882
  int rc;
3872
3883
  rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,
@@ -3882,7 +3893,6 @@ module MkKernel
3882
3893
  rb_raise(rb_eRuntimeError,
3883
3894
  "%s: output init failed rc=%d", ca_calling_method_name(), rc);
3884
3895
  }
3885
- #{mask_setup}
3886
3896
  char *pi, *po;
3887
3897
  boolean8_t *mi, *mo;
3888
3898
  while ( ca_iter_state_next_slab_axes(&st_in, &pi, &mi) &&
@@ -4167,7 +4177,7 @@ module MkKernel
4167
4177
  if is_fixlen
4168
4178
  io.puts " buf[#{dst}].vp = pi + #{kexpr} * istride; buf[#{dst}].nb = (ca_size_t) ca->bytes;"
4169
4179
  else
4170
- io.puts " buf[#{dst}].v = *(#{si[:c]} *) (pi + #{kexpr} * istride);"
4180
+ io.puts " memcpy(&buf[#{dst}].v, pi + #{kexpr} * istride, sizeof(buf[#{dst}].v));"
4171
4181
  end
4172
4182
  end
4173
4183
 
@@ -4263,21 +4273,10 @@ module MkKernel
4263
4273
  io.puts " when input has any mask. */"
4264
4274
  io.puts " if ( ca_has_mask(ca) ) ca_create_mask(co);"
4265
4275
  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;"
4276
+ # The buffers are taken before the walks open: an allocation that
4277
+ # raises inside them would jump past their finish. The slab is the
4278
+ # sort axis alone, so a fiber holds ca->dim[axis] cells.
4279
+ io.puts " ca_size_t fiber_n = ca->dim[axis];"
4281
4280
  if src == :object
4282
4281
  # The object comparator calls <=>, so the sort can raise part way.
4283
4282
  # ALLOCV frees these buffers whether the function returns or unwinds.
@@ -4290,15 +4289,17 @@ module MkKernel
4290
4289
  io.puts " #{name}_pair_#{src} *aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4291
4290
  end
4292
4291
  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
4292
  io.puts " volatile VALUE hbuf = 0, haux = 0;"
4297
4293
  io.puts " #{name}_pair_#{src} *buf = ALLOCV_N(#{name}_pair_#{src}, hbuf, fiber_n);"
4298
4294
  end
4299
- if has_kind && src != :object
4295
+ if has_kind && %i[f32 f64].include?(src)
4296
+ # aux is the mergesort's ping-pong buffer and, before that, the
4297
+ # scratch of the NaN pre-partition, so a float kernel always takes it.
4298
+ io.puts " #{name}_pair_#{src} *aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
4299
+ elsif has_kind && src != :object && src != :fixlen
4300
4300
  # The aux pair buffer for ca_sort_merge_pair_<src> (a ping-pong
4301
- # bottom-up mergesort), taken once outside the fiber loop.
4301
+ # bottom-up mergesort), taken once outside the fiber loop. The fixlen
4302
+ # lane sorts with qsort and needs none.
4302
4303
  io.puts " #{name}_pair_#{src} *aux = NULL;"
4303
4304
  io.puts " if ( do_stable ) {"
4304
4305
  io.puts " aux = ALLOCV_N(#{name}_pair_#{src}, haux, fiber_n);"
@@ -4307,6 +4308,20 @@ module MkKernel
4307
4308
  io.puts " (void) haux;"
4308
4309
  end
4309
4310
 
4311
+ io.puts " ca_iter_state st_in, st_out;"
4312
+ io.puts " int rc;"
4313
+ io.puts " rc = ca_iter_state_init_l2(&st_in, ca, CA_SLAB_AXES,"
4314
+ io.puts " slab_axes, 1, 0);"
4315
+ io.puts " if ( rc != CA_ITER_OK ) {"
4316
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: input init failed rc=%d", ca_calling_method_name(), rc);]
4317
+ io.puts " }"
4318
+ io.puts " rc = ca_iter_state_init_l2_paired(&st_out, &st_in, co, CA_SLAB_AXES,"
4319
+ io.puts " slab_axes, 1, CA_KERNEL_WRITE);"
4320
+ io.puts " if ( rc != CA_ITER_OK ) {"
4321
+ io.puts " ca_iter_state_finish(&st_in);"
4322
+ io.puts %Q[ rb_raise(rb_eRuntimeError, "%s: output init failed rc=%d", ca_calling_method_name(), rc);]
4323
+ io.puts " }"
4324
+
4310
4325
  if view_flat
4311
4326
  io.puts
4312
4327
  io.puts " /* :view_flat semantics: kernel computes view-flat address"
@@ -4358,7 +4373,7 @@ module MkKernel
4358
4373
  if is_fixlen
4359
4374
  io.puts " buf[k].vp = pi + k * istride; buf[k].nb = (ca_size_t) ca->bytes;"
4360
4375
  else
4361
- io.puts " buf[k].v = *(#{si[:c]} *) (pi + k * istride);"
4376
+ io.puts " memcpy(&buf[k].v, pi + k * istride, sizeof(buf[k].v));"
4362
4377
  end
4363
4378
  io.puts " buf[k].i = #{payload.call("k")};"
4364
4379
  io.puts " }"
@@ -4373,7 +4388,7 @@ module MkKernel
4373
4388
  if is_fixlen
4374
4389
  io.puts " buf[k_unmasked].vp = pi + k * istride; buf[k_unmasked].nb = (ca_size_t) ca->bytes;"
4375
4390
  else
4376
- io.puts " buf[k_unmasked].v = *(#{si[:c]} *) (pi + k * istride);"
4391
+ io.puts " memcpy(&buf[k_unmasked].v, pi + k * istride, sizeof(buf[k_unmasked].v));"
4377
4392
  end
4378
4393
  io.puts " buf[k_unmasked].i = k;"
4379
4394
  io.puts " k_unmasked++;"
@@ -4389,7 +4404,7 @@ module MkKernel
4389
4404
  if is_fixlen
4390
4405
  io.puts " buf[k].vp = pi + k * istride; buf[k].nb = (ca_size_t) ca->bytes;"
4391
4406
  else
4392
- io.puts " buf[k].v = *(#{si[:c]} *) (pi + k * istride);"
4407
+ io.puts " memcpy(&buf[k].v, pi + k * istride, sizeof(buf[k].v));"
4393
4408
  end
4394
4409
  io.puts " buf[k].i = k;"
4395
4410
  io.puts " }"
@@ -4462,7 +4477,7 @@ module MkKernel
4462
4477
  io.puts " /* P.3/P.4: NaN pre-partition for float argsort -- NaN pairs are"
4463
4478
  io.puts " pushed to the tail (= NaN-at-end policy, matches sort_copy"
4464
4479
  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});"
4480
+ io.puts " ca_size_t finite_n = ca_partition_nan_pair_#{src}((ca_pair_#{src} *) #{buf_expr}, (ca_size_t) #{sort_n_expr}, (ca_pair_#{src} *) aux);"
4466
4481
  io.puts " if ( do_stable ) {"
4467
4482
  io.puts " ca_sort_merge_pair_#{src}((ca_pair_#{src} *) #{buf_expr}, (ca_pair_#{src} *) aux, finite_n);"
4468
4483
  io.puts " } else {"
@@ -5004,7 +5019,7 @@ module MkKernel
5004
5019
  io.puts
5005
5020
  io.puts "/* ===== #{k[:name]}_ki ============================================ */"
5006
5021
  k[:source].each do |src|
5007
- emit_native_for(io, k, src) { |o| emit_search_native(o, k, src) }
5022
+ emit_native_for(io, k, src, guard: true) { |o| emit_search_native(o, k, src) }
5008
5023
  end
5009
5024
  emit_search_dispatch(io, k)
5010
5025
  end
@@ -5264,7 +5279,7 @@ module MkKernel
5264
5279
  CArray *co;
5265
5280
  GetCArray(vout, co);
5266
5281
  #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
5267
- #{op_mask_decl}
5282
+ #{undef_nm ? op_mask_decl : ""}
5268
5283
 
5269
5284
  ca_iter_state st;
5270
5285
  int rc = ca_iter_state_init_l2(&st, ca, CA_SLAB_AXES,
@@ -5486,6 +5501,12 @@ module MkKernel
5486
5501
  #{op_mask_decl}
5487
5502
 
5488
5503
  #{entity_bc}
5504
+ /* The output mask is made before the window opens, since making it
5505
+ allocates; it comes off again below when no cell needed it. */
5506
+ if ( #{undef_nm ? "1" : "ca_has_mask(cv)"} ) {
5507
+ ca_create_mask(co);
5508
+ op_mask = (boolean8_t *) co->mask->ptr;
5509
+ }
5489
5510
  /* Attach self + val; rev4 uses raw row-major byte addressing. */
5490
5511
  ca_attach_n(2, ca, cv); /* window: numeric bodies do not raise; the object lane reads entities */
5491
5512
 
@@ -5592,6 +5613,11 @@ module MkKernel
5592
5613
 
5593
5614
  ca_detach(ca);
5594
5615
  ca_detach(cv);
5616
+ if ( op_mask && ! memchr(op_mask, 1, (size_t) co->elements) ) {
5617
+ CArray *unused = co->mask;
5618
+ co->mask = NULL;
5619
+ ca_free(unused);
5620
+ }
5595
5621
  return vout;
5596
5622
  }
5597
5623
  C
@@ -5633,6 +5659,13 @@ module MkKernel
5633
5659
  io.puts " VALUE rval = argv[0];"
5634
5660
  io.puts " VALUE raxis = argv[1];"
5635
5661
  io.puts " VALUE reps = (argc >= 3) ? argv[2] : Qnil;"
5662
+ io.puts " /* A tolerance is a distance: a negative one (or NaN) means nothing. */"
5663
+ io.puts " if ( ! NIL_P(reps) ) {"
5664
+ io.puts " double eps_given = NUM2DBL(reps);"
5665
+ io.puts " if ( ! ( eps_given >= 0.0 ) ) {"
5666
+ io.puts %Q[ rb_raise(rb_eArgError, "%s: eps must be zero or positive (got %"PRIsVALUE")", ca_calling_method_name(), rb_inspect(reps));]
5667
+ io.puts " }"
5668
+ io.puts " }"
5636
5669
  end
5637
5670
  io.puts " /* Query gate: the reference Face and the query are brought into one"
5638
5671
  io.puts " space (ca_face_reconcile_pair), then the reference descends to"
@@ -5656,6 +5689,8 @@ module MkKernel
5656
5689
  io.puts " GetCArray(self, src);"
5657
5690
  io.puts " }"
5658
5691
  io.puts " }"
5692
+ io.puts " self = ca_fixlen_widen_for_query(self, rval);"
5693
+ io.puts " GetCArray(self, src);"
5659
5694
 
5660
5695
  case k[:mask_self]
5661
5696
  when :raise
@@ -5687,13 +5722,14 @@ module MkKernel
5687
5722
  # (`ca_monop_<name>_<suffix>`). mkmath uses typedef names
5688
5723
  # `float32_t` / `float64_t` here, not the raw C types `float` / `double`,
5689
5724
  # so that `ca_monop_dispatch.c`'s extern references stay ABI-stable.
5690
- # For all other data_types, the suffix matches `DTYPES[src][:c]`.
5725
+ # For all other data_types, the suffix is `DTYPES[src][:c]` without the
5726
+ # `ca_u_` of its unaligned cell typedef (ca_u_int32_t -> int32_t).
5691
5727
  def self.math_suffix(src)
5692
5728
  case src
5693
5729
  when :f32 then "float32_t"
5694
5730
  when :f64 then "float64_t"
5695
5731
  when :fixlen then "fixlen" # symbol suffix differs from c-type "char"
5696
- else DTYPES[src][:c]
5732
+ else DTYPES[src][:c].sub(/\Aca_u_/, "")
5697
5733
  end
5698
5734
  end
5699
5735
 
@@ -5772,7 +5808,8 @@ module MkKernel
5772
5808
  uses_input = raw.include?("#1")
5773
5809
  # SL.2.x phase 2 (monop emit substrate hardening, MEMO_MONOP_STRIDED_AUDIT.md):
5774
5810
  # 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`.
5811
+ # contig branch (i1==1 && i2==1) uses index form + `omp simd` (`__restrict__`
5812
+ # only when input and output are apart).
5776
5813
  # strided branch uses `p += i` increment form (per-iter multiply removed).
5777
5814
  # mask branch unchanged (per-cell predicate, not SIMD-amenable).
5778
5815
  if uses_input
@@ -5780,11 +5817,11 @@ module MkKernel
5780
5817
 
5781
5818
  static void
5782
5819
  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)
5820
+ char *ptr1, ca_size_t i1,
5821
+ char *ptr2, ca_size_t i2)
5785
5822
  {
5786
- #{c_type_in} * __restrict__ q1 = (#{c_type_in} *) ptr1;
5787
- #{c_type_out} * __restrict__ q2 = (#{c_type_out} *) ptr2;
5823
+ #{c_type_in} *q1 = (#{c_type_in} *) ptr1;
5824
+ #{c_type_out} *q2 = (#{c_type_out} *) ptr2;
5788
5825
  ca_size_t k;
5789
5826
  if ( m ) {
5790
5827
  #{c_type_in} *p1 = q1;
@@ -5800,17 +5837,38 @@ module MkKernel
5800
5837
  }
5801
5838
  } else if ( i1 == 1 && i2 == 1 ) {
5802
5839
  /* 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. */
5840
+ the unit-stride access to the vectorizer. Element-wise monop
5841
+ has no reassoc, so ε-close numeric contract is unaffected.
5842
+ The input and output are either apart, the same cells (a
5843
+ bang, or the lazy scratch applied in place), or overlapping
5844
+ otherwise; `__restrict__` is promised only when apart, since
5845
+ the body may store part of a cell before it has read it. */
5807
5846
  #{c_type_in} *p1;
5808
5847
  #{c_type_out} *p2;
5809
5848
  (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} }
5849
+ uintptr_t a1 = (uintptr_t) q1, a2 = (uintptr_t) q2;
5850
+ if ( a2 + n * sizeof(*q2) <= a1 || a1 + n * sizeof(*q1) <= a2 ) {
5851
+ #{c_type_in} * __restrict__ r1 = q1;
5852
+ #{c_type_out} * __restrict__ r2 = q2;
5853
+ #pragma omp simd
5854
+ for (k=0; k<n; k++) {
5855
+ p1 = r1 + k; p2 = r2 + k;
5856
+ { #{expr_body} }
5857
+ }
5858
+ }
5859
+ else if ( a1 == a2 && sizeof(*q1) == sizeof(*q2) ) {
5860
+ /* in place: an iteration reads and writes only its own cell */
5861
+ #pragma omp simd
5862
+ for (k=0; k<n; k++) {
5863
+ p1 = q1 + k; p2 = q2 + k;
5864
+ { #{expr_body} }
5865
+ }
5866
+ }
5867
+ else {
5868
+ for (k=0; k<n; k++) {
5869
+ p1 = q1 + k; p2 = q2 + k;
5870
+ { #{expr_body} }
5871
+ }
5814
5872
  }
5815
5873
  } else {
5816
5874
  /* strided branch: numo-shape `p += s` increment form. */
@@ -5996,7 +6054,8 @@ module MkKernel
5996
6054
  expr_body = raw.gsub(/#1/, "*p1").gsub(/#2/, "*p2").gsub(/#3/, "*p3").gsub(/<type>/, suffix)
5997
6055
  # Option B+C pilot (binop emit substrate hardening, MEMO_BINOP_STRIDED_AUDIT.md):
5998
6056
  # 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`
6057
+ # `q[k]` index form with `#pragma omp simd` (`__restrict__`-qualified only
6058
+ # when the output is apart from both inputs)
6000
6059
  # (SL.2.x-style insurance, element-wise has no reassoc -> ε-close unaffected),
6001
6060
  # and a strided branch using `p += i` increment form (numo-shape inner loop).
6002
6061
  # `i1`/`i2`/`i3` remain element units (NOT byte) -- byte-flip is Option A scope.
@@ -6006,13 +6065,13 @@ module MkKernel
6006
6065
 
6007
6066
  static void
6008
6067
  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)
6068
+ char *ptr1, ca_size_t i1,
6069
+ char *ptr2, ca_size_t i2,
6070
+ char *ptr3, ca_size_t i3)
6012
6071
  {
6013
- #{c_type} * __restrict__ q1 = (#{c_type} *) ptr1;
6014
- #{c_type} * __restrict__ q2 = (#{c_type} *) ptr2;
6015
- #{c_type} * __restrict__ q3 = (#{c_type} *) ptr3;
6072
+ #{c_type} *q1 = (#{c_type} *) ptr1;
6073
+ #{c_type} *q2 = (#{c_type} *) ptr2;
6074
+ #{c_type} *q3 = (#{c_type} *) ptr3;
6016
6075
  ca_size_t k;
6017
6076
  if ( m ) {
6018
6077
  #{c_type} *p1 = q1, *p2 = q2, *p3 = q3;
@@ -6028,16 +6087,41 @@ module MkKernel
6028
6087
  }
6029
6088
  } else if ( i1 == 1 && i2 == 1 && i3 == 1 ) {
6030
6089
  /* 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. */
6090
+ the unit-stride access to the vectorizer. Element-wise binop
6091
+ has no reassoc, so ε-close numeric contract is unaffected.
6092
+ The output is apart from each input, the same cells as it (a
6093
+ bang), or overlaps it otherwise; `__restrict__` is promised
6094
+ only when apart from both, since the body may store part of a
6095
+ cell before it has read it. */
6035
6096
  #{c_type} *p1, *p2, *p3;
6097
+ uintptr_t a1 = (uintptr_t) q1, a2 = (uintptr_t) q2, a3 = (uintptr_t) q3;
6098
+ uintptr_t nb = (uintptr_t) n * sizeof(*q3);
6099
+ int apart1 = ( a3 + nb <= a1 || a1 + nb <= a3 );
6100
+ int apart2 = ( a3 + nb <= a2 || a2 + nb <= a3 );
6036
6101
  (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} }
6102
+ if ( apart1 && apart2 ) {
6103
+ #{c_type} * __restrict__ r1 = q1;
6104
+ #{c_type} * __restrict__ r2 = q2;
6105
+ #{c_type} * __restrict__ r3 = q3;
6106
+ #pragma omp simd
6107
+ for (k=0; k<n; k++) {
6108
+ p1 = r1 + k; p2 = r2 + k; p3 = r3 + k;
6109
+ { #{expr_body} }
6110
+ }
6111
+ }
6112
+ else if ( ( apart1 || a1 == a3 ) && ( apart2 || a2 == a3 ) ) {
6113
+ /* in place: an iteration reads and writes only its own cell */
6114
+ #pragma omp simd
6115
+ for (k=0; k<n; k++) {
6116
+ p1 = q1 + k; p2 = q2 + k; p3 = q3 + k;
6117
+ { #{expr_body} }
6118
+ }
6119
+ }
6120
+ else {
6121
+ for (k=0; k<n; k++) {
6122
+ p1 = q1 + k; p2 = q2 + k; p3 = q3 + k;
6123
+ { #{expr_body} }
6124
+ }
6041
6125
  }
6042
6126
  } else {
6043
6127
  /* strided branch: numo-shape `p += s` increment form. */
@@ -6500,13 +6584,21 @@ module MkKernel
6500
6584
  cmplx64 cmplx128].freeze
6501
6585
  CAST_INEXACT = %i[f32 f64 cmplx64 cmplx128].freeze
6502
6586
 
6587
+ # The plain C type of a data type, for a body that leaves this file
6588
+ # (CArray.__kernel_body__, a fusion plan): the unaligned typedef in :c is
6589
+ # about how a kernel reaches a cell, not about the value.
6590
+ def self.value_c(src)
6591
+ { "ca_u_float32_t" => "float", "ca_u_float64_t" => "double" }
6592
+ .fetch(DTYPES[src][:c]) { |c| c.sub(/\Aca_u_/, "") }
6593
+ end
6594
+
6503
6595
  def self.cast_body_rows
6504
6596
  CAST_SOURCES.product(CAST_TARGETS).filter_map { |src, dst|
6505
6597
  next if CAST_INEXACT.include?(src) && !CAST_INEXACT.include?(dst)
6506
6598
  name = DTYPES[dst][:ca].sub(/\ACA_/, "").downcase
6507
6599
  via = ( %i[i64 u64].include?(src) && %i[f32 cmplx64].include?(dst) ) ? "(double)" : ""
6508
6600
  ["monop", "cast_#{name}", DTYPES[src][:ca].sub(/\ACA_/, "").downcase,
6509
- "(#2) = (#{DTYPES[dst][:c]})#{via}(#1);"]
6601
+ "(#2) = (#{value_c(dst)})#{via}(#1);"]
6510
6602
  }
6511
6603
  end
6512
6604
 
@@ -7678,8 +7770,8 @@ MkKernel.search :find_value_index,
7678
7770
  mask_self: :raise,
7679
7771
  fallback: :raise
7680
7772
 
7681
- # bsearch_ki: per-slice binary search returning the matching position
7682
- # (assumes a sorted slab). NaN handling: NaN sorts to end; query == NaN
7773
+ # bsearch_ki: per-slice binary search returning the first matching position
7774
+ # (assumes a sorted slab; of equal cells, the first). NaN handling: NaN sorts to end; query == NaN
7683
7775
  # never matches. Hand-rolled strided binary search (= libc bsearch()
7684
7776
  # requires contig memory; slab_stride may be > sizeof(T_LOAD) for a
7685
7777
  # non-innermost search axis). NaN guards are dead code for integer
@@ -7705,7 +7797,7 @@ MkKernel.search :bsearch,
7705
7797
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7706
7798
  if ( v < query_val ) lo = mid + 1;
7707
7799
  else if ( v > query_val ) hi = mid;
7708
- else { result = mid; break; }
7800
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7709
7801
  }
7710
7802
  }
7711
7803
  C
@@ -7720,7 +7812,7 @@ MkKernel.search :bsearch,
7720
7812
  int c = CA_FLOAT_ORDER_CMP(v, query_val);
7721
7813
  if ( c < 0 ) lo = mid + 1;
7722
7814
  else if ( c > 0 ) hi = mid;
7723
- else { result = mid; break; }
7815
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7724
7816
  }
7725
7817
  }
7726
7818
  C
@@ -7737,7 +7829,7 @@ MkKernel.search :bsearch,
7737
7829
  int c = ca_obj_order_cmp(v, query_val);
7738
7830
  if ( c < 0 ) lo = mid + 1;
7739
7831
  else if ( c > 0 ) hi = mid;
7740
- else { result = mid; break; }
7832
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7741
7833
  }
7742
7834
  C
7743
7835
  fixlen: <<~C,
@@ -7753,7 +7845,7 @@ MkKernel.search :bsearch,
7753
7845
  int c = memcmp(v, query_val, (size_t) ca->bytes);
7754
7846
  if ( c < 0 ) lo = mid + 1;
7755
7847
  else if ( c > 0 ) hi = mid;
7756
- else { result = mid; break; }
7848
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7757
7849
  }
7758
7850
  C
7759
7851
  },
@@ -7938,7 +8030,7 @@ MkKernel.search :bsearch_addr,
7938
8030
  T_LOAD v = *(T_LOAD *)(slab_ptr + mid * slab_stride);
7939
8031
  if ( v < query_val ) lo = mid + 1;
7940
8032
  else if ( v > query_val ) hi = mid;
7941
- else { result = mid; break; }
8033
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7942
8034
  }
7943
8035
  }
7944
8036
  C
@@ -7953,7 +8045,7 @@ MkKernel.search :bsearch_addr,
7953
8045
  int c = CA_FLOAT_ORDER_CMP(v, query_val);
7954
8046
  if ( c < 0 ) lo = mid + 1;
7955
8047
  else if ( c > 0 ) hi = mid;
7956
- else { result = mid; break; }
8048
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7957
8049
  }
7958
8050
  }
7959
8051
  C
@@ -7967,7 +8059,7 @@ MkKernel.search :bsearch_addr,
7967
8059
  int c = ca_obj_order_cmp(v, query_val);
7968
8060
  if ( c < 0 ) lo = mid + 1;
7969
8061
  else if ( c > 0 ) hi = mid;
7970
- else { result = mid; break; }
8062
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7971
8063
  }
7972
8064
  C
7973
8065
  fixlen: <<~C,
@@ -7981,7 +8073,7 @@ MkKernel.search :bsearch_addr,
7981
8073
  int c = memcmp(v, query_val, (size_t) ca->bytes);
7982
8074
  if ( c < 0 ) lo = mid + 1;
7983
8075
  else if ( c > 0 ) hi = mid;
7984
- else { result = mid; break; }
8076
+ else { result = mid; hi = mid; } /* keep looking left: the first equal cell */
7985
8077
  }
7986
8078
  C
7987
8079
  },
@@ -8152,13 +8244,13 @@ MkKernel.search :linear_section_binary,
8152
8244
  if ( slab_n <= 1 ) {
8153
8245
  /* A one-point axis has no segment: only the point itself is in
8154
8246
  * range, at 0. An empty one has nothing in range. */
8155
- if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8247
+ if ( slab_n == 1 && query_val == *(ca_u_float64_t *)slab_ptr ) {
8156
8248
  result = 0.0;
8157
8249
  }
8158
8250
  }
8159
8251
  else {
8160
- double y0 = *(double *)(slab_ptr + 0 * slab_stride);
8161
- double yn = *(double *)(slab_ptr + (slab_n - 1) * slab_stride);
8252
+ double y0 = *(ca_u_float64_t *)(slab_ptr + 0 * slab_stride);
8253
+ double yn = *(ca_u_float64_t *)(slab_ptr + (slab_n - 1) * slab_stride);
8162
8254
  int ascending = ( yn >= y0 ); /* grid direction (monotonic assumed) */
8163
8255
  ca_size_t x1 = 0;
8164
8256
  int found = 0;
@@ -8181,8 +8273,8 @@ MkKernel.search :linear_section_binary,
8181
8273
  double gd = (query_val - y0) / (yn - y0) * (double)(slab_n - 1);
8182
8274
  if ( gd >= 0.0 && gd < (double)(slab_n - 1) ) {
8183
8275
  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);
8276
+ double yga = *(ca_u_float64_t *)(slab_ptr + ag * slab_stride);
8277
+ double ygb = *(ca_u_float64_t *)(slab_ptr + (ag + 1) * slab_stride);
8186
8278
  if ( (yga - query_val) * (ygb - query_val) <= 0 ) {
8187
8279
  x1 = ag; found = 1;
8188
8280
  }
@@ -8197,7 +8289,7 @@ MkKernel.search :linear_section_binary,
8197
8289
  double ya = y0;
8198
8290
  while ( (b - a) > 1 ) {
8199
8291
  ca_size_t c = (a + b) / 2;
8200
- double yc = *(double *)(slab_ptr + c * slab_stride);
8292
+ double yc = *(ca_u_float64_t *)(slab_ptr + c * slab_stride);
8201
8293
  if ( (ya - query_val) * (yc - query_val) <= 0 ) { b = c; }
8202
8294
  else { a = c; ya = yc; }
8203
8295
  }
@@ -8206,8 +8298,8 @@ MkKernel.search :linear_section_binary,
8206
8298
  }
8207
8299
 
8208
8300
  if ( found ) {
8209
- double y1 = *(double *)(slab_ptr + x1 * slab_stride);
8210
- double y2 = *(double *)(slab_ptr + (x1 + 1) * slab_stride);
8301
+ double y1 = *(ca_u_float64_t *)(slab_ptr + x1 * slab_stride);
8302
+ double y2 = *(ca_u_float64_t *)(slab_ptr + (x1 + 1) * slab_stride);
8211
8303
  double rest = (query_val - y1) / (y2 - y1);
8212
8304
  if ( y2 != 0.0 && fabs(y2 - query_val) / fabs(y2) < DBL_EPSILON * 100 ) {
8213
8305
  result = (double)(x1 + 1);
@@ -8243,7 +8335,7 @@ MkKernel.search :linear_section_linear,
8243
8335
  if ( slab_n <= 1 ) {
8244
8336
  /* A one-point axis has no segment: only the point itself is in
8245
8337
  * range, at 0. An empty one has nothing in range. */
8246
- if ( slab_n == 1 && query_val == *(double *)slab_ptr ) {
8338
+ if ( slab_n == 1 && query_val == *(ca_u_float64_t *)slab_ptr ) {
8247
8339
  result = 0.0;
8248
8340
  }
8249
8341
  }
@@ -8251,15 +8343,15 @@ MkKernel.search :linear_section_linear,
8251
8343
  ca_size_t x1 = 0;
8252
8344
  int found = 0;
8253
8345
  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);
8346
+ double yk = *(ca_u_float64_t *)(slab_ptr + k * slab_stride);
8347
+ double yk1 = *(ca_u_float64_t *)(slab_ptr + (k + 1) * slab_stride);
8256
8348
  if ( (query_val - yk) * (query_val - yk1) <= 0 ) {
8257
8349
  x1 = k; found = 1; break;
8258
8350
  }
8259
8351
  }
8260
8352
  if ( found ) {
8261
- double y1 = *(double *)(slab_ptr + x1 * slab_stride);
8262
- double y2 = *(double *)(slab_ptr + (x1 + 1) * slab_stride);
8353
+ double y1 = *(ca_u_float64_t *)(slab_ptr + x1 * slab_stride);
8354
+ double y2 = *(ca_u_float64_t *)(slab_ptr + (x1 + 1) * slab_stride);
8263
8355
  result = (query_val - y1) / (y2 - y1) + (double) x1;
8264
8356
  }
8265
8357
 
@@ -8286,8 +8378,8 @@ MkKernel.search :linear_fetch,
8286
8378
  ca_size_t il = (ca_size_t) floor(query_val);
8287
8379
  ca_size_t iu = (ca_size_t) ceil(query_val);
8288
8380
  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);
8381
+ double yl = *(ca_u_float64_t *)(slab_ptr + il * slab_stride);
8382
+ double yu = *(ca_u_float64_t *)(slab_ptr + iu * slab_stride);
8291
8383
  result = yu * w + yl * (1.0 - w);
8292
8384
  }
8293
8385
  C
@@ -8847,7 +8939,7 @@ MkKernel.monfunc :rad_pi,
8847
8939
  # Pair-wise max / min — three flavours:
8848
8940
  #
8849
8941
  # pmax / pmin CArray legacy names (kept for backwards compat),
8850
- # NaN-skip semantics via C99 fmax / fmin.
8942
+ # NaN-skip semantics, as C99 fmax / fmin.
8851
8943
  # fmax / fmin Ruby-level aliases of pmax / pmin (= same kernel,
8852
8944
  # same NaN-skip behaviour). Matches the C99 library
8853
8945
  # function of the same name.
@@ -8857,12 +8949,15 @@ MkKernel.monfunc :rad_pi,
8857
8949
  #
8858
8950
  # Integer branches are identical across all three (no NaN). The float
8859
8951
  # and object branches differ: pmax / pmin let a NaN lose (an object cell's
8860
- # Float NaN included), maximum / minimum let it win.
8952
+ # Float NaN included), maximum / minimum let it win. In both flavours, of
8953
+ # two zeros the max is +0 and the min is -0 in either order. C's fmax /
8954
+ # fmin leave that choice to the library (glibc and macOS answer
8955
+ # differently), so the float branch does not call them.
8861
8956
  MkKernel.binop :pmax,
8862
8957
  source: MkKernel::ALL_NUMERIC + [:object],
8863
8958
  expr: {
8864
8959
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8865
- float: "(#3) = fmax(#1, #2);",
8960
+ float: "(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) > (#2) || ((#1) == (#2) && !signbit(#1))) ? (#1) : (#2);",
8866
8961
  object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern(">")) ? (#2) : (#1);',
8867
8962
  }
8868
8963
 
@@ -8870,7 +8965,7 @@ MkKernel.binop :pmin,
8870
8965
  source: MkKernel::ALL_NUMERIC + [:object],
8871
8966
  expr: {
8872
8967
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8873
- float: "(#3) = fmin(#1, #2);",
8968
+ float: "(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) < (#2) || ((#1) == (#2) && signbit(#1))) ? (#1) : (#2);",
8874
8969
  object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern("<")) ? (#2) : (#1);',
8875
8970
  }
8876
8971