carray 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +520 -0
  3. data/README.md +2 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +33 -4
  6. data/ext/ca_axis_group.c +202 -96
  7. data/ext/ca_categorical_iterator.c +108 -54
  8. data/ext/ca_kernel_iterator.c +317 -51
  9. data/ext/ca_kernel_iterator.h +142 -35
  10. data/ext/ca_obj_array.c +62 -20
  11. data/ext/ca_obj_block.c +4 -4
  12. data/ext/ca_obj_const_string.c +85 -26
  13. data/ext/ca_obj_face.c +24 -0
  14. data/ext/ca_obj_face.h +15 -0
  15. data/ext/ca_obj_fixlen_string.c +18 -5
  16. data/ext/ca_obj_meld.c +123 -25
  17. data/ext/ca_obj_object.c +8 -0
  18. data/ext/ca_obj_select.c +49 -34
  19. data/ext/ca_obj_stack.c +3 -8
  20. data/ext/ca_obj_stride.c +72 -1
  21. data/ext/ca_obj_string.c +8 -4
  22. data/ext/ca_obj_window.c +8 -2
  23. data/ext/ca_op_ipower.c +1 -2
  24. data/ext/ca_rng_normal.h +42 -0
  25. data/ext/ca_rng_xoshiro256pp.h +105 -0
  26. data/ext/ca_sweep_engine.c +307 -143
  27. data/ext/ca_sweep_engine.h +26 -5
  28. data/ext/carray.h +21 -2
  29. data/ext/carray_access.c +32 -20
  30. data/ext/carray_address_basis.c +590 -0
  31. data/ext/carray_broadcast.c +3 -3
  32. data/ext/carray_call_cfunc.c +667 -483
  33. data/ext/carray_cast.c +115 -41
  34. data/ext/carray_copy.c +55 -30
  35. data/ext/carray_core.c +83 -3
  36. data/ext/carray_count.c +9 -10
  37. data/ext/carray_factorize.c +46 -25
  38. data/ext/carray_internal.h +17 -0
  39. data/ext/carray_kernels_reduce_aggregate.c +168 -0
  40. data/ext/carray_kernels_reduce_cumulative.c +270 -1
  41. data/ext/carray_kernels_reduce_extreme.c +554 -8
  42. data/ext/carray_kernels_scan.c +4 -4
  43. data/ext/carray_kernels_search.c +94 -14
  44. data/ext/carray_loop.c +7 -1
  45. data/ext/carray_mask.c +23 -8
  46. data/ext/carray_median_percentile.c +55 -0
  47. data/ext/carray_operator.c +4 -4
  48. data/ext/carray_order.c +1 -1
  49. data/ext/carray_random.c +384 -40
  50. data/ext/carray_slab.c +13 -0
  51. data/ext/carray_sort.c +20 -22
  52. data/ext/mk_call_cfunc.rb +103 -116
  53. data/ext/mkkernel.rb +297 -29
  54. data/ext/ruby_carray.c +10 -1
  55. data/ext/version.h +4 -4
  56. data/lib/carray/autoload_carray.rb +5 -3
  57. data/lib/carray/autoload_method_extension.rb +12 -0
  58. data/lib/carray/axis_group.rb +77 -0
  59. data/lib/carray/basics.rb +4 -0
  60. data/lib/carray/block_iterator.rb +92 -16
  61. data/lib/carray/categorical.rb +150 -33
  62. data/lib/carray/categorical_iterator.rb +207 -80
  63. data/lib/carray/const_string.rb +131 -27
  64. data/lib/carray/construct.rb +40 -0
  65. data/lib/carray/data_type_extension.rb +3 -0
  66. data/lib/carray/data_type_limits.rb +91 -0
  67. data/lib/carray/fixlen_string.rb +1 -1
  68. data/lib/carray/frame/csv_parser.rb +11 -4
  69. data/lib/carray/frame/frame.rb +81 -10
  70. data/lib/carray/frame/group.rb +36 -3
  71. data/lib/carray/frame/io.rb +67 -15
  72. data/lib/carray/frame/records.rb +18 -4
  73. data/lib/carray/frame/verbs.rb +14 -11
  74. data/lib/carray/inspect.rb +42 -9
  75. data/lib/carray/iterator.rb +143 -0
  76. data/lib/carray/lazy.rb +0 -37
  77. data/lib/carray/mask_gap_fill.rb +3 -1
  78. data/lib/carray/methods/discovery_along.rb +74 -0
  79. data/lib/carray/methods/factorize.rb +50 -0
  80. data/lib/carray/methods/is_in.rb +13 -2
  81. data/lib/carray/methods/locate_addr.rb +75 -2
  82. data/lib/carray/methods/mask_duplicates.rb +35 -1
  83. data/lib/carray/methods/nunique.rb +22 -1
  84. data/lib/carray/methods/repeat.rb +110 -0
  85. data/lib/carray/methods/unique.rb +41 -1
  86. data/lib/carray/rng.rb +86 -0
  87. data/lib/carray/slab_iterator.rb +58 -13
  88. data/lib/carray/string_operation_extension.rb +5 -1
  89. data/lib/carray/time.rb +18 -2
  90. data/lib/carray/window_iterator.rb +142 -20
  91. data/lib/carray.rb +2 -0
  92. data/yard-stubs/ca_obj_block.rb +2 -7
  93. data/yard-stubs/ca_obj_window.rb +10 -2
  94. data/yard-stubs/carray_access.rb +1 -1
  95. data/yard-stubs/carray_broadcast.rb +1 -1
  96. data/yard-stubs/carray_core.rb +0 -80
  97. data/yard-stubs/carray_count.rb +7 -2
  98. data/yard-stubs/carray_lazy.rb +205 -0
  99. data/yard-stubs/carray_math.rb +1486 -3
  100. data/yard-stubs/carray_median_percentile.rb +16 -2
  101. data/yard-stubs/carray_order.rb +9 -69
  102. data/yard-stubs/carray_slab.rb +9 -7
  103. data/yard-stubs/carray_sort.rb +7 -5
  104. metadata +9 -1
data/ext/mkkernel.rb CHANGED
@@ -360,6 +360,7 @@ module MkKernel
360
360
  face_gate: nil,
361
361
  object_escape: nil,
362
362
  identity_on_empty: false,
363
+ all_nan_result: nil,
363
364
  outputs: 1,
364
365
  # Two-pass centred algorithm (variance / stddev family).
365
366
  # When algorithm: :two_pass_centred is set, `state / init /
@@ -465,6 +466,30 @@ module MkKernel
465
466
  end
466
467
  end
467
468
  end
469
+ # all_nan_result: what an extreme-value reduction answers when every
470
+ # contributing cell was NaN. The `(v < acc) ? v : acc` reduce body is
471
+ # false for NaN, so acc is never updated and the init (+/-INFINITY)
472
+ # leaks out as the answer -- a value that is neither the minimum of
473
+ # anything nor a missing marker, and indistinguishable from data that
474
+ # genuinely held only +INFINITY.
475
+ #
476
+ # :nan -- answer NaN (C99 fmin folded: a lone non-NaN wins, two
477
+ # NaNs give NaN). For kernels whose output can hold it.
478
+ # :undef -- answer UNDEF, by folding into the mask_policy trigger.
479
+ # For kernels whose output is a position (argmin family):
480
+ # an integer output cannot hold NaN, and index 0 would be
481
+ # a lie about where the minimum is.
482
+ #
483
+ # Only the float srcs act on it; integers and boolean have no NaN and
484
+ # their init is a legitimate value.
485
+ unless all_nan_result.nil? || %i[nan undef].include?(all_nan_result)
486
+ raise "#{name}: all_nan_result #{all_nan_result.inspect} invalid " \
487
+ "(expected :nan, :undef, or nil)"
488
+ end
489
+ if all_nan_result == :undef && mask_policy.nil?
490
+ raise "#{name}: all_nan_result: :undef needs a mask_policy " \
491
+ "(UNDEF is written through the mask_policy trigger)"
492
+ end
468
493
  if value_arg
469
494
  raise "#{name}: value_arg must be a Hash" unless value_arg.is_a?(Hash)
470
495
  raise "#{name}: value_arg: target must be :T_IN" unless value_arg[:target] == :T_IN
@@ -478,16 +503,29 @@ module MkKernel
478
503
  # selects direction (:min / :max) and output kind:
479
504
  # :min / :max -> extremum blob, output data_type = CA_FIXLEN
480
505
  # :argmin / :argmax -> position of the extremum, output i64
506
+ # :count_equal -> how many cells memcmp-equal value_arg, output i64
481
507
  # Author must also list :fixlen in source: (parallel to how :object
482
508
  # opts in via source: + an :object body).
509
+ #
510
+ # :count_equal is the one mode that takes a value_arg, and takes it as a
511
+ # byte blob rather than a scalar: the dispatcher packs the query with
512
+ # rb_ca_obj2ptr into a ca->bytes buffer, the same way the search family
513
+ # already does for a fixlen query (which is what makes a short query
514
+ # NUL-pad to the cell width instead of never matching).
483
515
  if fixlen
484
- unless %i[min max argmin argmax].include?(fixlen)
485
- raise "#{name}: fixlen: must be :min / :max / :argmin / :argmax (got #{fixlen.inspect})"
516
+ unless %i[min max argmin argmax count_equal].include?(fixlen)
517
+ raise "#{name}: fixlen: must be :min / :max / :argmin / :argmax / :count_equal (got #{fixlen.inspect})"
486
518
  end
487
519
  raise "#{name}: fixlen: requires :fixlen in source:" unless source.include?(:fixlen)
488
520
  raise "#{name}: fixlen: requires mask_policy: :min_count" unless mask_policy == :min_count
489
521
  raise "#{name}: fixlen: requires outputs: 1" unless outputs == 1
490
- raise "#{name}: fixlen: does not support value_arg / array_arg" if value_arg || array_arg
522
+ raise "#{name}: fixlen: does not support array_arg" if array_arg
523
+ if value_arg && fixlen != :count_equal
524
+ raise "#{name}: fixlen: only :count_equal takes a value_arg"
525
+ end
526
+ if fixlen == :count_equal && !value_arg
527
+ raise "#{name}: fixlen: :count_equal requires value_arg"
528
+ end
491
529
  elsif source.include?(:fixlen)
492
530
  raise "#{name}: source includes :fixlen but no fixlen: spec given"
493
531
  end
@@ -612,6 +650,7 @@ module MkKernel
612
650
  face_gate: face_gate,
613
651
  object_escape: object_escape,
614
652
  identity_on_empty: identity_on_empty,
653
+ all_nan_result: all_nan_result,
615
654
  outputs: outputs,
616
655
  algorithm: algorithm,
617
656
  divisor: divisor,
@@ -1351,6 +1390,62 @@ module MkKernel
1351
1390
  io.puts "#undef #{expr_macro}"
1352
1391
  end
1353
1392
 
1393
+ # State vars that start at the type's limit (T_LIMIT_HI / T_LIMIT_LO)
1394
+ # and so can leak their init as an answer. A position state (argmin's
1395
+ # best_i, init "0") is deliberately not one of them: index 0 is a value
1396
+ # a real minimum can legitimately produce.
1397
+ def self.limit_init_state_vars(k, si, oi, src)
1398
+ return [] unless k[:state]
1399
+ k[:state].keys.filter_map do |var|
1400
+ token = pick_family_string(k[:init][var], src, "init")
1401
+ next unless %w[T_LIMIT_HI T_LIMIT_LO].include?(token)
1402
+ [var, resolve_init_expr(k[:init][var], oi, si, src)]
1403
+ end
1404
+ end
1405
+
1406
+ # Emit the all-NaN fix-up for an extreme-value reduction (see the
1407
+ # all_nan_result: validation in MkKernel.reduce for why it exists).
1408
+ #
1409
+ # The per-cell reduce body is left alone: it stays the SIMD-licensed
1410
+ # ternary, so the 8-way horizontal split (reduce_8way_eligible?) and
1411
+ # the `reduction(min:acc)` clause are untouched, and ordinary data pays
1412
+ # one comparison per slab. Carrying a "saw a number" flag as a second
1413
+ # DSL state var was the alternative and costs more than it looks: it
1414
+ # takes the kernel over reduce_8way_eligible?'s single-state gate and
1415
+ # drops min / max back onto the legacy single-accumulator macro.
1416
+ #
1417
+ # acc still sitting at its init means either nothing updated it or the
1418
+ # data genuinely held only +/-INFINITY. Those are told apart by
1419
+ # walking the slab once more for a non-NaN cell -- O(n), and only for
1420
+ # a slab whose answer came out equal to the init.
1421
+ #
1422
+ # Returns true when it emitted anything.
1423
+ def self.emit_all_nan_fixup(io, k, src, si, pairs, valid_guard,
1424
+ indent: " ", flag_var: nil)
1425
+ mode = k[:all_nan_result]
1426
+ return false unless mode
1427
+ return false unless FLOAT_DTYPES.include?(src)
1428
+ cond = pairs.map { |var, init| "#{var} == (#{init})" }.join(" && ")
1429
+ cond = "(#{cond}) && #{valid_guard}" if valid_guard
1430
+ io.puts "#{indent}/* all-NaN fix-up (all_nan_result: #{mode.inspect}). */"
1431
+ io.puts "#{indent}if ( #{cond} ) {"
1432
+ io.puts "#{indent} int64_t __anf_seen = 0;"
1433
+ io.puts "#{indent} ca_size_t __anf_mc = 0;"
1434
+ io.puts "#{indent} CA_SLAB_REDUCE_T_EX(#{si[:c]}, st, p, m, __anf_seen, 0, " \
1435
+ "__anf_seen |= (v == v), __anf_mc);"
1436
+ io.puts "#{indent} (void) __anf_mc;"
1437
+ io.puts "#{indent} if ( ! __anf_seen ) {"
1438
+ if mode == :nan
1439
+ pairs.each { |var, _| io.puts "#{indent} #{var} = (#{si[:c]}) NAN;" }
1440
+ else
1441
+ raise "#{k[:name]}: all_nan_result: :undef needs flag_var" unless flag_var
1442
+ io.puts "#{indent} #{flag_var} = 1;"
1443
+ end
1444
+ io.puts "#{indent} }"
1445
+ io.puts "#{indent}}"
1446
+ true
1447
+ end
1448
+
1354
1449
  def self.reduce_macro_suffix(k, src = nil)
1355
1450
  # CA_OBJECT cannot ride the SIMD-licensed macros (= _PLUS / _MIN / _MAX
1356
1451
  # / _STAR), which assume C operators (= acc is a VALUE, so
@@ -1774,7 +1869,7 @@ module MkKernel
1774
1869
 
1775
1870
  if streamable
1776
1871
  emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var, acc_init,
1777
- decls, reduce_stmt, finish_expr, extra_args)
1872
+ decls, reduce_stmt, finish_expr, extra_args, src)
1778
1873
  end
1779
1874
 
1780
1875
  # L.1 / L.3 / L.4 (PROPOSAL_REDUCTION_LOOP_INTERCHANGE):
@@ -1934,6 +2029,17 @@ module MkKernel
1934
2029
  emit_reduce_slab_call(io, k, src, si, oi, suffix, acc_var, acc_init,
1935
2030
  reduce_stmt, "masked_cnt", indent: " ")
1936
2031
  end
2032
+ all_nan_flag = nil
2033
+ if k[:all_nan_result]
2034
+ pairs = limit_init_state_vars(k, si, oi, src)
2035
+ if k[:all_nan_result] == :undef && FLOAT_DTYPES.include?(src)
2036
+ all_nan_flag = "__anf_all_nan"
2037
+ io.puts " int #{all_nan_flag} = 0;"
2038
+ end
2039
+ emit_all_nan_fixup(io, k, src, si, pairs,
2040
+ "masked_cnt < st.slab_elements",
2041
+ indent: " ", flag_var: all_nan_flag)
2042
+ end
1937
2043
  trigger = case k[:mask_policy]
1938
2044
  when :strict then "masked_cnt > 0"
1939
2045
  when :all_masked then "masked_cnt == st.slab_elements"
@@ -1955,6 +2061,7 @@ module MkKernel
1955
2061
  ": st.slab_elements - masked_cnt < min_count)"
1956
2062
  end
1957
2063
  end
2064
+ trigger = "(#{trigger}) || #{all_nan_flag}" if all_nan_flag
1958
2065
  finish_emit = view_flat \
1959
2066
  ? "(transform_active ? (outer_off + ((ca_size_t)(#{finish_expr})) * axis_vstride) : ((ca_size_t)(#{finish_expr})))" \
1960
2067
  : "(#{finish_expr})"
@@ -2037,13 +2144,15 @@ module MkKernel
2037
2144
  # the numeric argmin's best_i. mask_policy is always :min_count here.
2038
2145
  def self.emit_reduce_native_fixlen(io, k, src)
2039
2146
  name = k[:name]
2147
+ counting = k[:fixlen] == :count_equal
2040
2148
  want_max = %i[max argmax].include?(k[:fixlen])
2041
- index_out = %i[argmin argmax].include?(k[:fixlen])
2149
+ index_out = %i[argmin argmax].include?(k[:fixlen]) || counting
2042
2150
  cmp = want_max ? ">" : "<"
2151
+ varg = counting ? ", const char *value_arg" : ""
2043
2152
 
2044
2153
  io.puts
2045
2154
  io.puts "static VALUE"
2046
- io.puts "#{name}_ki_native_fixlen (VALUE self, CArray *ca, int8_t *slab_axes, int8_t naxes, int keep_axis, ca_size_t min_count)"
2155
+ io.puts "#{name}_ki_native_fixlen (VALUE self, CArray *ca, int8_t *slab_axes, int8_t naxes, int keep_axis#{varg}, ca_size_t min_count)"
2047
2156
  io.puts "{"
2048
2157
  io.puts " ca_size_t K = ca->bytes; /* uniform fixlen byte width */"
2049
2158
  if index_out
@@ -2064,9 +2173,13 @@ module MkKernel
2064
2173
  io.puts " ca_size_t out_i = 0;"
2065
2174
  io.puts " boolean8_t *op_mask = NULL; /* lazily allocated on first UNDEF */"
2066
2175
  io.puts " while ( ca_iter_state_next_slab_axes(&st, &p, &m) ) {"
2067
- io.puts " const char *best = NULL;"
2068
- io.puts " ca_size_t best_i = 0;"
2069
- io.puts " (void) best_i;" unless index_out # value output ignores the index
2176
+ if counting
2177
+ io.puts " int64_t cnt = 0;"
2178
+ else
2179
+ io.puts " const char *best = NULL;"
2180
+ io.puts " ca_size_t best_i = 0;"
2181
+ io.puts " (void) best_i;" unless index_out # value output ignores the index
2182
+ end
2070
2183
  io.puts " ca_size_t masked_cnt = 0;"
2071
2184
  io.puts " int8_t sndim = st.slab_ndim;"
2072
2185
  io.puts " ca_size_t sidx[CA_RANK_MAX] = { 0 };"
@@ -2082,12 +2195,18 @@ module MkKernel
2082
2195
  io.puts " }"
2083
2196
  io.puts " else {"
2084
2197
  io.puts " const char *q = (const char *) p + doff;"
2085
- io.puts " if ( best == NULL ) {"
2086
- io.puts " best = q; best_i = idx;"
2087
- io.puts " }"
2088
- io.puts " else if ( memcmp(q, best, (size_t) K) #{cmp} 0 ) {"
2089
- io.puts " best = q; best_i = idx;"
2090
- io.puts " }"
2198
+ if counting
2199
+ io.puts " if ( memcmp(q, value_arg, (size_t) K) == 0 ) {"
2200
+ io.puts " cnt++;"
2201
+ io.puts " }"
2202
+ else
2203
+ io.puts " if ( best == NULL ) {"
2204
+ io.puts " best = q; best_i = idx;"
2205
+ io.puts " }"
2206
+ io.puts " else if ( memcmp(q, best, (size_t) K) #{cmp} 0 ) {"
2207
+ io.puts " best = q; best_i = idx;"
2208
+ io.puts " }"
2209
+ end
2091
2210
  io.puts " }"
2092
2211
  io.puts " /* row-major odometer (innermost slab axis fastest) so idx"
2093
2212
  io.puts " matches CA_SLAB_REDUCE_T's flat slab index. */"
@@ -2096,8 +2215,16 @@ module MkKernel
2096
2215
  io.puts " sidx[sk] = 0;"
2097
2216
  io.puts " }"
2098
2217
  io.puts " }"
2099
- io.puts " if ( min_count < 0 ? masked_cnt == st.slab_elements"
2100
- io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2218
+ # ERI.0: a count over nothing is 0, not UNDEF -- so the default
2219
+ # (min_count < 0) never fires the mask for :count_equal. An explicit
2220
+ # min_count: still does.
2221
+ if counting
2222
+ io.puts " if ( min_count < 0 ? 0"
2223
+ io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2224
+ else
2225
+ io.puts " if ( min_count < 0 ? masked_cnt == st.slab_elements"
2226
+ io.puts " : st.slab_elements - masked_cnt < min_count ) {"
2227
+ end
2101
2228
  io.puts " if ( ! op_mask ) {"
2102
2229
  io.puts " ca_create_mask(co);"
2103
2230
  io.puts " op_mask = (boolean8_t *) co->mask->ptr;"
@@ -2111,7 +2238,9 @@ module MkKernel
2111
2238
  io.puts " out_i++;"
2112
2239
  io.puts " }"
2113
2240
  io.puts " else {"
2114
- if index_out
2241
+ if counting
2242
+ io.puts " ((int64_t *) op)[out_i] = cnt;"
2243
+ elsif index_out
2115
2244
  io.puts " ((int64_t *) op)[out_i] = (int64_t) best_i;"
2116
2245
  else
2117
2246
  io.puts " memcpy(op + out_i * K, best, (size_t) K);"
@@ -2439,6 +2568,49 @@ module MkKernel
2439
2568
  buf_decls = plus_info.map do |pi|
2440
2569
  " #{pi[:c_type]} #{pi[:buf]}[512];\n"
2441
2570
  end.join
2571
+
2572
+ # all-NaN fix-up for the tiled core (see emit_all_nan_fixup for the
2573
+ # reduction counterpart). Each output cell of a tile has its own
2574
+ # accumulator, so the check is per column: a column whose accumulator
2575
+ # is still at the init either saw only NaN or only +/-INFINITY.
2576
+ #
2577
+ # The `_Pragma("omp simd")` j-loop is left alone -- carrying a
2578
+ # "saw a number" flag beside the accumulator would double its loads
2579
+ # and stores on every cell of every column. Instead the tile is
2580
+ # tested once after the M loop (tile_len comparisons against M *
2581
+ # tile_len already done), and only a tile that holds a suspicious
2582
+ # column re-reads its rows.
2583
+ li_nan_fixup =
2584
+ if k[:all_nan_result] == :nan && FLOAT_DTYPES.include?(src) &&
2585
+ plus_info.size == 1
2586
+ pi = plus_info.first
2587
+ seen_buf = "__li_buf_seen"
2588
+ lambda do |m_step|
2589
+ <<~C.rstrip
2590
+ {
2591
+ int __li_susp = 0;
2592
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2593
+ if ( #{pi[:buf]}[__j] == (#{pi[:c_type]}) (#{pi[:init]}) ) { __li_susp = 1; break; }
2594
+ }
2595
+ if ( __li_susp ) {
2596
+ int8_t #{seen_buf}[512];
2597
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) #{seen_buf}[__j] = 0;
2598
+ for ( ca_size_t __li_i = 0; __li_i < __li_M; __li_i++ ) {
2599
+ const #{si[:c]} *__li_row = __li_plane + __li_i * #{m_step} + __li_tile;
2600
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2601
+ #{seen_buf}[__j] |= (__li_row[__j] == __li_row[__j]);
2602
+ }
2603
+ }
2604
+ for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2605
+ if ( ! #{seen_buf}[__j] ) #{pi[:buf]}[__j] = (#{pi[:c_type]}) NAN;
2606
+ }
2607
+ }
2608
+ }
2609
+ C
2610
+ end
2611
+ else
2612
+ lambda { |_m_step| "" }
2613
+ end
2442
2614
  init_loops = plus_info.map do |pi|
2443
2615
  " for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {\n" \
2444
2616
  " #{pi[:buf]}[__j] = (#{pi[:c_type]}) (#{pi[:init]});\n" \
@@ -2468,6 +2640,7 @@ module MkKernel
2468
2640
  (void) v;
2469
2641
  }
2470
2642
  }
2643
+ #{li_nan_fixup.call("__li_INNER")}
2471
2644
  for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2472
2645
  #{opv}[__li_o * __li_INNER + __li_tile + __j] = (#{oi[:c]}) (#{finish_li});
2473
2646
  }
@@ -2688,6 +2861,7 @@ module MkKernel
2688
2861
  (void) v;
2689
2862
  }
2690
2863
  }
2864
+ #{li_nan_fixup.call("__li_INNER")}
2691
2865
  for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2692
2866
  __li_op_k[__li_tile + __j] = (#{oi[:c]}) (#{finish_li});
2693
2867
  }
@@ -2846,6 +3020,7 @@ module MkKernel
2846
3020
  (void) v;
2847
3021
  }
2848
3022
  }
3023
+ #{li_nan_fixup.call("__li_M_stride")}
2849
3024
  for ( ca_size_t __j = 0; __j < __li_tile_len; __j++ ) {
2850
3025
  __li_op_k[__li_tile + __j] = (#{oi[:c]}) (#{finish_li});
2851
3026
  }
@@ -2937,6 +3112,10 @@ module MkKernel
2937
3112
  if min_count
2938
3113
  io.puts " ca_size_t masked_cnt = 0;"
2939
3114
  io.puts " CA_SLAB_REDUCE_T_EX(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt}, masked_cnt);"
3115
+ if k[:all_nan_result]
3116
+ emit_all_nan_fixup(io, k, src, si, limit_init_state_vars(k, si, oi, src),
3117
+ "masked_cnt < st.slab_elements", indent: " ")
3118
+ end
2940
3119
  # Same trigger as single-output :min_count: legacy default (all_masked)
2941
3120
  # when min_count < 0, otherwise need at least min_count valid cells.
2942
3121
  trigger = "(min_count < 0 ? masked_cnt == st.slab_elements " \
@@ -3012,7 +3191,7 @@ module MkKernel
3012
3191
  # - mask present: ca_has_mask(ca)
3013
3192
  def self.emit_reduce_streaming(io, k, si, oi, ruby_wrap, acc_var,
3014
3193
  acc_init, decls, reduce_stmt,
3015
- finish_expr, extra_args)
3194
+ finish_expr, extra_args, src)
3016
3195
  name = k[:name]
3017
3196
  min_count = (k[:mask_policy] == :min_count)
3018
3197
  has_mp = !k[:mask_policy].nil?
@@ -3043,6 +3222,14 @@ module MkKernel
3043
3222
  # acc_var requires explicit init (= the macro normally does this).
3044
3223
  # Other state vars in decls already include `= init` per line.
3045
3224
  io.puts " #{acc_var} = (#{acc_init});"
3225
+ # all-NaN fix-up, streaming variant. The other two paths confirm a
3226
+ # suspicious answer by re-reading the data; here re-reading means
3227
+ # evaluating the lazy chain a second time, so the flag rides along in
3228
+ # the chunk loop instead. The loop already pays for producing each
3229
+ # cell, which is what makes one more compare affordable here and not
3230
+ # in the tiled core.
3231
+ all_nan_stream = k[:all_nan_result] == :nan && FLOAT_DTYPES.include?(src)
3232
+ io.puts " int64_t __anf_seen = 0;" if all_nan_stream
3046
3233
  if has_mp
3047
3234
  # Mask-policy reductions need masked_cnt to satisfy the macro/
3048
3235
  # finish_expr signature. On streaming we have no mask, so it's
@@ -3070,11 +3257,15 @@ module MkKernel
3070
3257
  io.puts " for ( __i = 0; __i < __n; __i++ ) {"
3071
3258
  io.puts " #{si[:c]} v = __chunk[__i];"
3072
3259
  io.puts " #{reduce_stmt};"
3260
+ io.puts " __anf_seen |= (v == v);" if all_nan_stream
3073
3261
  io.puts " }"
3074
3262
  io.puts " __outer_off += __r;"
3075
3263
  io.puts " }"
3076
3264
  io.puts " ca_lazy_arena_release(__chunk);"
3077
3265
  io.puts " ca_lazy_arena_exit();"
3266
+ if all_nan_stream
3267
+ io.puts " if ( ! __anf_seen && ca->elements > 0 ) #{acc_var} = (#{si[:c]}) NAN;"
3268
+ end
3078
3269
  if has_mp
3079
3270
  # Streaming path has no mask source, so masked_cnt is 0; min_count
3080
3271
  # / strict / all_masked triggers all evaluate to false except
@@ -3318,7 +3509,17 @@ module MkKernel
3318
3509
  k[:source].each do |s|
3319
3510
  si = DTYPES[s]
3320
3511
  # Per-src value_arg cast: NUM2LL / NUM2ULL / NUM2DBL -> (T_IN).
3321
- varg_decl = has_varg ? " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n" : ""
3512
+ varg_decl = if !has_varg
3513
+ ""
3514
+ elsif s == :fixlen
3515
+ # A fixlen query is a runtime-width byte blob with no
3516
+ # scalar cast; pack it the way the search family does,
3517
+ # which NUL-pads a short String to the cell width.
3518
+ " char *value_arg = ALLOCA_N(char, src->bytes);\n" \
3519
+ " rb_ca_obj2ptr(self, rval, value_arg);\n"
3520
+ else
3521
+ " #{si[:c]} value_arg = (#{si[:c]}) #{si[:num2c]}(rval);\n"
3522
+ end
3322
3523
  if use_result_var || has_varg
3323
3524
  io.puts " case #{si[:ca]}: {"
3324
3525
  io.print varg_decl unless varg_decl.empty?
@@ -5333,7 +5534,7 @@ module MkKernel
5333
5534
  io.puts " }"
5334
5535
  io.puts " if ( self_face_comparable ) {"
5335
5536
  io.puts " if ( rval_is_face ) {"
5336
- io.puts " rval = rb_ca_strip_face_value(rval);"
5537
+ io.puts %Q[ rval = ca_face_operand_descend(rval, "#{name}_ki");]
5337
5538
  io.puts " }"
5338
5539
  io.puts " } else if ( self_was_face ) {"
5339
5540
  io.puts " if ( rb_respond_to(self_ref, rb_intern(\"to_comparable\")) ) {"
@@ -6443,6 +6644,7 @@ MkKernel.reduce :min,
6443
6644
  bool: "acc = ((uint64_t) v < acc) ? (uint64_t) v : acc",
6444
6645
  object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v;' },
6445
6646
  reduction_kind: :min, # SL.1.2
6647
+ all_nan_result: :nan,
6446
6648
  # CA_FIXLEN: memcmp lexicographic min (byte order == the fixlen sort
6447
6649
  # order); the numeric reduce/init above are unused for fixlen (bespoke
6448
6650
  # slab walk, see the fixlen: option in MkKernel.reduce).
@@ -6467,6 +6669,7 @@ MkKernel.reduce :max,
6467
6669
  bool: "acc = ((uint64_t) v > acc) ? (uint64_t) v : acc",
6468
6670
  object: 'if (acc == Qundef) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v;' },
6469
6671
  reduction_kind: :max, # SL.1.2
6672
+ all_nan_result: :nan,
6470
6673
  # CA_FIXLEN: memcmp lexicographic max (byte order == the fixlen sort order).
6471
6674
  fixlen: :max,
6472
6675
  source: MkKernel::ALL_NUMERIC + [:bool, :object, :fixlen],
@@ -6692,6 +6895,7 @@ MkKernel.reduce :minmax,
6692
6895
  # together on init, get set together on first reduce).
6693
6896
  object: 'if (lo == Qundef) { lo = v; hi = v; } else { if (RTEST(rb_funcall(v, rb_intern("<"), 1, lo))) lo = v; if (RTEST(rb_funcall(v, rb_intern(">"), 1, hi))) hi = v; }' },
6694
6897
  outputs: 2,
6898
+ all_nan_result: :nan,
6695
6899
  finish: { min: "lo", max: "hi" },
6696
6900
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
6697
6901
  # bool: u64 (Integer 0/1) so minmax returns [0/1, 0/1], not
@@ -6740,6 +6944,7 @@ MkKernel.reduce :argmin,
6740
6944
  # An ORDERABLE Face descends to its numeric storage (position output needs
6741
6945
  # no re-lift; the axis-local index is identical for Face and storage).
6742
6946
  face_gate: :strip,
6947
+ all_nan_result: :undef,
6743
6948
  public_method: :min_index
6744
6949
 
6745
6950
  MkKernel.reduce :argmax,
@@ -6758,6 +6963,7 @@ MkKernel.reduce :argmax,
6758
6963
  fallback: :raise,
6759
6964
  mask_policy: :min_count,
6760
6965
  face_gate: :strip,
6966
+ all_nan_result: :undef,
6761
6967
  public_method: :max_index
6762
6968
 
6763
6969
  # ---- argmin_addr / argmax_addr (view-flat address variants) ---------
@@ -6801,6 +7007,7 @@ MkKernel.reduce :argmin_addr,
6801
7007
  mask_policy: :min_count,
6802
7008
  semantics: :view_flat,
6803
7009
  face_gate: :strip,
7010
+ all_nan_result: :undef,
6804
7011
  public_method: :min_addr
6805
7012
 
6806
7013
  MkKernel.reduce :argmax_addr,
@@ -6818,6 +7025,7 @@ MkKernel.reduce :argmax_addr,
6818
7025
  mask_policy: :min_count,
6819
7026
  semantics: :view_flat,
6820
7027
  face_gate: :strip,
7028
+ all_nan_result: :undef,
6821
7029
  public_method: :max_addr
6822
7030
 
6823
7031
  # ---- mask_policy demos ------------------------------------------------
@@ -6945,9 +7153,13 @@ MkKernel.reduce :count_false,
6945
7153
 
6946
7154
  MkKernel.reduce :count_equal,
6947
7155
  init: "0",
6948
- reduce: "if (v == value_arg) acc += 1",
7156
+ # The object lane compares with rb_equal (= Ruby ==), not the C ==, which
7157
+ # on two VALUEs would ask whether they are the same object.
7158
+ reduce: { numeric: "if (v == value_arg) acc += 1",
7159
+ object: "if (RTEST(rb_equal(v, value_arg))) acc += 1" },
6949
7160
  reduction_kind: :plus, # SL.1.4 (conditional predication; clang predicates safely under reduction(+:acc))
6950
- source: MkKernel::ALL_NUMERIC,
7161
+ source: MkKernel::ALL_NUMERIC + [:object, :fixlen],
7162
+ fixlen: :count_equal, # bespoke memcmp walk (a blob has no scalar C type)
6951
7163
  output: :i64,
6952
7164
  ruby_scalar: :LL2NUM,
6953
7165
  fallback: :raise,
@@ -7140,22 +7352,38 @@ MkKernel.scan :cumprod,
7140
7352
  # init Qnil is the "no running extremum yet" sentinel, also never leaked
7141
7353
  # (unseen cells are masked). First unmasked cell adopts v as acc;
7142
7354
  # subsequent unmasked cells compare via rb_funcall(:>) / rb_funcall(:<).
7355
+ # The float lane starts at NaN and folds with C99 fmin / fmax, whose
7356
+ # rule is exactly the one wanted: a lone number beats NaN, two NaNs give
7357
+ # NaN. So a prefix that has seen only NaN answers NaN instead of leaking
7358
+ # +/-INFINITY -- the running form of the rule min / max follow
7359
+ # (all_nan_result:). fmin / fmax are exact on a float at either width,
7360
+ # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7361
+ # and keep the limit init and the plain compare.
7143
7362
  MkKernel.scan :cummax,
7144
7363
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7145
7364
  output: { bool: :u64, default: :preserve },
7146
- init: { numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qnil" },
7147
- step: { numeric: "if (v > acc) acc = v; r = acc",
7365
+ init: { float: "NAN", numeric: "T_LIMIT_LO", bool: "T_LIMIT_LO", object: "Qnil" },
7366
+ step: { float: "acc = fmax(acc, v); r = acc",
7367
+ numeric: "if (v > acc) acc = v; r = acc",
7148
7368
  bool: "if ((uint64_t) v > acc) acc = v; r = acc",
7149
7369
  object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern(">"), 1, acc))) acc = v; r = acc' },
7150
7370
  fallback: :raise,
7151
7371
  axis_default: :flatten,
7152
7372
  empty: :undef
7153
7373
 
7374
+ # The float lane starts at NaN and folds with C99 fmin / fmax, whose
7375
+ # rule is exactly the one wanted: a lone number beats NaN, two NaNs give
7376
+ # NaN. So a prefix that has seen only NaN answers NaN instead of leaking
7377
+ # +/-INFINITY -- the running form of the rule min / max follow
7378
+ # (all_nan_result:). fmin / fmax are exact on a float at either width,
7379
+ # so the f32 lane needs no narrowing. Integer and boolean have no NaN
7380
+ # and keep the limit init and the plain compare.
7154
7381
  MkKernel.scan :cummin,
7155
7382
  source: MkKernel::ALL_NUMERIC + [:bool, :object],
7156
7383
  output: { bool: :u64, default: :preserve },
7157
- init: { numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qnil" },
7158
- step: { numeric: "if (v < acc) acc = v; r = acc",
7384
+ init: { float: "NAN", numeric: "T_LIMIT_HI", bool: "T_LIMIT_HI", object: "Qnil" },
7385
+ step: { float: "acc = fmin(acc, v); r = acc",
7386
+ numeric: "if (v < acc) acc = v; r = acc",
7159
7387
  bool: "if ((uint64_t) v < acc) acc = v; r = acc",
7160
7388
  object: 'if (acc == Qnil) acc = v; else if (RTEST(rb_funcall(v, rb_intern("<"), 1, acc))) acc = v; r = acc' },
7161
7389
  fallback: :raise,
@@ -7543,12 +7771,32 @@ MkKernel.search :search_nearest,
7543
7771
  object: <<~C,
7544
7772
  /* CA_OBJECT nearest: minimum of query_val.distance(cell), compared
7545
7773
  with `<` (matches the legacy flat proc_nearest_addr_VALUE). */
7774
+ /* Nearest needs a metric. #distance is the protocol the 2.0 flat
7775
+ kernel used, back when Numeric#distance was a monkey patch; it
7776
+ is an opt-in refinement now, and a refinement does not reach an
7777
+ rb_funcall from C -- so a number reaching here answers no more
7778
+ than a String does. Measure a number the way #distance itself
7779
+ does, keep #distance for anything that defines a real one, and
7780
+ refuse the rest by name rather than let a bare NoMethodError out
7781
+ of the loop below. */
7782
+ ID nearest_id = rb_intern("distance");
7783
+ int nearest_by_distance = rb_respond_to(query_val, nearest_id);
7784
+ if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
7785
+ rb_raise(rb_eCADataTypeError,
7786
+ "search_nearest: nearest needs a distance, and %s is neither a "
7787
+ "number nor answers #distance (define one on the stored "
7788
+ "objects, or use search / bsearch for an exact match)",
7789
+ rb_obj_classname(query_val));
7790
+ }
7546
7791
  result = (ca_size_t) -1;
7547
7792
  VALUE best = Qnil;
7548
7793
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7549
7794
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7550
7795
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7551
- VALUE dist = rb_funcall(query_val, rb_intern("distance"), 1, v);
7796
+ VALUE dist = nearest_by_distance
7797
+ ? rb_funcall(query_val, nearest_id, 1, v)
7798
+ : rb_funcall(rb_funcall(query_val, '-', 1, v),
7799
+ rb_intern("abs"), 0);
7552
7800
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
7553
7801
  best = dist; result = i;
7554
7802
  }
@@ -7724,12 +7972,32 @@ MkKernel.search :search_nearest_addr,
7724
7972
  object: <<~C,
7725
7973
  /* CA_OBJECT nearest (view_flat addr): minimum of
7726
7974
  query_val.distance(cell), compared with `<`. */
7975
+ /* Nearest needs a metric. #distance is the protocol the 2.0 flat
7976
+ kernel used, back when Numeric#distance was a monkey patch; it
7977
+ is an opt-in refinement now, and a refinement does not reach an
7978
+ rb_funcall from C -- so a number reaching here answers no more
7979
+ than a String does. Measure a number the way #distance itself
7980
+ does, keep #distance for anything that defines a real one, and
7981
+ refuse the rest by name rather than let a bare NoMethodError out
7982
+ of the loop below. */
7983
+ ID nearest_id = rb_intern("distance");
7984
+ int nearest_by_distance = rb_respond_to(query_val, nearest_id);
7985
+ if ( ! nearest_by_distance && ! rb_obj_is_kind_of(query_val, rb_cNumeric) ) {
7986
+ rb_raise(rb_eCADataTypeError,
7987
+ "search_nearest_addr: nearest needs a distance, and %s is neither a "
7988
+ "number nor answers #distance (define one on the stored "
7989
+ "objects, or use search / bsearch for an exact match)",
7990
+ rb_obj_classname(query_val));
7991
+ }
7727
7992
  result = (ca_size_t) -1;
7728
7993
  VALUE best = Qnil;
7729
7994
  for ( ca_size_t i = 0; i < slab_n; i++ ) {
7730
7995
  if ( mask_in && mask_in[i * slab_mask_stride] ) continue;
7731
7996
  T_LOAD v = *(T_LOAD *)(slab_ptr + i * slab_stride);
7732
- VALUE dist = rb_funcall(query_val, rb_intern("distance"), 1, v);
7997
+ VALUE dist = nearest_by_distance
7998
+ ? rb_funcall(query_val, nearest_id, 1, v)
7999
+ : rb_funcall(rb_funcall(query_val, '-', 1, v),
8000
+ rb_intern("abs"), 0);
7733
8001
  if ( NIL_P(best) || RTEST(rb_funcall(dist, rb_intern("<"), 1, best)) ) {
7734
8002
  best = dist; result = i;
7735
8003
  }
data/ext/ruby_carray.c CHANGED
@@ -121,6 +121,13 @@ void Init_ca_kernel_iterator ();
121
121
 
122
122
  void Init_carray_slab ();
123
123
 
124
+ void Init_carray_address_basis (); /* CArray::AddressBasis: pointer + byte
125
+ strides lent for the length of a block,
126
+ for code that addresses cells itself.
127
+ A runtime facility at the ca_attach
128
+ layer, not a user surface; see
129
+ guides/devel/21_address_basis.md */
130
+
124
131
  void
125
132
  Init_carray_ext (void)
126
133
  {
@@ -310,7 +317,7 @@ Init_carray_ext (void)
310
317
  Init_ca_obj_refer();
311
318
  Init_ca_obj_stride(); /* CAREFUL: must precede CAStride subclasses
312
319
  (farray / block / transpose / repeat /
313
- unbound_repeat / field). */
320
+ field). */
314
321
  Init_ca_obj_farray();
315
322
  Init_ca_obj_block();
316
323
  Init_ca_obj_select();
@@ -373,5 +380,7 @@ Init_carray_ext (void)
373
380
  Init_ca_kernel_iterator();
374
381
 
375
382
  Init_carray_slab();
383
+
384
+ Init_carray_address_basis();
376
385
  }
377
386
 
data/ext/version.h CHANGED
@@ -1,6 +1,6 @@
1
- #define CA_VERSION "3.0.1"
2
- #define CA_VERSION_CODE 301
1
+ #define CA_VERSION "3.0.2"
2
+ #define CA_VERSION_CODE 302
3
3
  #define CA_VERSION_MAJOR 3
4
4
  #define CA_VERSION_MINOR 0
5
- #define CA_VERSION_TEENY 1
6
- #define CA_VERSION_DATE "2026/09/07"
5
+ #define CA_VERSION_TEENY 2
6
+ #define CA_VERSION_DATE "2026/09/24"
@@ -30,8 +30,9 @@
30
30
 
31
31
  class CArray
32
32
  inspect_rb = "carray/inspect"
33
- autoload_method "inspect", inspect_rb
34
- autoload_method "source_code", inspect_rb
33
+ autoload_method "inspect", inspect_rb
34
+ autoload_method "inspect_full", inspect_rb
35
+ autoload_method "source_code", inspect_rb
35
36
  end
36
37
 
37
38
  # ---- Serializer / Marshal / dump / load ------------------------------------
@@ -42,7 +43,6 @@ class CArray
42
43
  autoload_method "self.save", serialize_rb
43
44
  autoload_method "self.load", serialize_rb
44
45
  autoload_method "self.dump", serialize_rb
45
- autoload_method "self.load_from_file", serialize_rb
46
46
  autoload_method "marshal_dump", serialize_rb
47
47
  autoload_method "marshal_load", serialize_rb
48
48
  end
@@ -201,11 +201,13 @@ class CArray
201
201
  autoload_method "self.meshgrid", "carray/methods/meshgrid"
202
202
 
203
203
  autoload_method "bincount", "carray/methods/bincount"
204
+ autoload_method "repeat", "carray/methods/repeat"
204
205
  autoload_method "self.broadcast", "carray/methods/broadcast"
205
206
  autoload_method "gather_nd", "carray/methods/gather_nd"
206
207
  autoload_method "put_nd", "carray/methods/gather_nd"
207
208
  autoload_method "mask_duplicates", "carray/methods/mask_duplicates"
208
209
  autoload_method "unique", "carray/methods/unique"
210
+ autoload_method "factorize", "carray/methods/factorize"
209
211
  autoload_method "is_in", "carray/methods/is_in"
210
212
  autoload_method "intersection", "carray/methods/is_in"
211
213
  autoload_method "difference", "carray/methods/is_in"