carray 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +845 -3
  4. data/{NEWS.md → CHANGELOG.v1.md} +1 -1
  5. data/README.md +33 -33
  6. data/carray.gemspec +12 -8
  7. data/ext/ca_axis_dispatch.c +33 -4
  8. data/ext/ca_axis_group.c +256 -114
  9. data/ext/ca_binop_dispatch.c +5 -6
  10. data/ext/ca_binop_dispatch.h +0 -7
  11. data/ext/ca_categorical_iterator.c +121 -67
  12. data/ext/ca_for_each_element.h +6 -8
  13. data/ext/ca_group_iter.c +1 -1
  14. data/ext/ca_kernel_iterator.c +386 -74
  15. data/ext/ca_kernel_iterator.h +145 -45
  16. data/ext/ca_obj_array.c +62 -20
  17. data/ext/ca_obj_bincmp.c +57 -27
  18. data/ext/ca_obj_binop.c +86 -30
  19. data/ext/ca_obj_block.c +4 -4
  20. data/ext/ca_obj_const_string.c +88 -27
  21. data/ext/ca_obj_face.c +104 -0
  22. data/ext/ca_obj_face.h +48 -1
  23. data/ext/ca_obj_fake.c +11 -0
  24. data/ext/ca_obj_fixlen_string.c +21 -6
  25. data/ext/ca_obj_grid.c +11 -3
  26. data/ext/ca_obj_meld.c +125 -22
  27. data/ext/ca_obj_moncmp.c +33 -14
  28. data/ext/ca_obj_monop.c +19 -7
  29. data/ext/ca_obj_object.c +107 -4
  30. data/ext/ca_obj_record.c +3 -1
  31. data/ext/ca_obj_refer.c +30 -6
  32. data/ext/ca_obj_roll.c +13 -5
  33. data/ext/ca_obj_select.c +49 -34
  34. data/ext/ca_obj_select_axis.c +16 -6
  35. data/ext/ca_obj_shift.c +3 -3
  36. data/ext/ca_obj_stack.c +3 -8
  37. data/ext/ca_obj_stride.c +178 -24
  38. data/ext/ca_obj_string.c +11 -5
  39. data/ext/ca_obj_tile.c +13 -5
  40. data/ext/ca_obj_time.c +3 -1
  41. data/ext/ca_obj_timedelta.c +3 -1
  42. data/ext/ca_obj_transpose.c +1 -1
  43. data/ext/ca_obj_triop.c +62 -23
  44. data/ext/ca_obj_window.c +94 -19
  45. data/ext/ca_op_cmplx64.h +123 -0
  46. data/ext/ca_op_ipower.c +1 -5
  47. data/ext/ca_rng_normal.h +42 -0
  48. data/ext/ca_rng_xoshiro256pp.h +105 -0
  49. data/ext/ca_sort_kernels.h +5 -5
  50. data/ext/ca_sweep_engine.c +362 -155
  51. data/ext/ca_sweep_engine.h +39 -9
  52. data/ext/ca_transform_common.c +7 -0
  53. data/ext/carray.h +148 -132
  54. data/ext/carray_access.c +83 -49
  55. data/ext/carray_address_basis.c +590 -0
  56. data/ext/carray_bincount.c +8 -8
  57. data/ext/carray_broadcast.c +102 -9
  58. data/ext/carray_build_flags.h +3 -0
  59. data/ext/carray_call_cfunc.c +2010 -335
  60. data/ext/carray_call_cfunc.h +153 -0
  61. data/ext/carray_cast.c +184 -72
  62. data/ext/carray_conversion.c +22 -22
  63. data/ext/carray_copy.c +67 -32
  64. data/ext/carray_core.c +140 -13
  65. data/ext/carray_count.c +10 -11
  66. data/ext/carray_element.c +7 -7
  67. data/ext/carray_factorize.c +72 -51
  68. data/ext/carray_hold.c +1 -1
  69. data/ext/carray_index_classifier.c +7 -21
  70. data/ext/carray_internal.h +36 -3
  71. data/ext/carray_kernels_bincmp.c +1 -0
  72. data/ext/carray_kernels_binop.c +320 -298
  73. data/ext/carray_kernels_init.c +1095 -0
  74. data/ext/carray_kernels_map.c +1 -0
  75. data/ext/carray_kernels_moncmp.c +1 -0
  76. data/ext/carray_kernels_monop.c +412 -411
  77. data/ext/carray_kernels_reduce_aggregate.c +256 -87
  78. data/ext/carray_kernels_reduce_boolean.c +1 -0
  79. data/ext/carray_kernels_reduce_cumulative.c +324 -54
  80. data/ext/carray_kernels_reduce_extreme.c +605 -58
  81. data/ext/carray_kernels_reduce_variance.c +1 -0
  82. data/ext/carray_kernels_scan.c +5 -4
  83. data/ext/carray_kernels_search.c +187 -106
  84. data/ext/carray_kernels_sort.c +1 -0
  85. data/ext/carray_kernels_triop.c +1 -0
  86. data/ext/carray_lazy.c +175 -5
  87. data/ext/carray_loop.c +7 -1
  88. data/ext/carray_mask.c +29 -13
  89. data/ext/carray_median_percentile.c +56 -1
  90. data/ext/carray_memory_view.c +46 -21
  91. data/ext/carray_operator.c +50 -53
  92. data/ext/carray_order.c +1 -1
  93. data/ext/carray_partition.c +4 -4
  94. data/ext/carray_random.c +391 -47
  95. data/ext/carray_scatter.c +1 -1
  96. data/ext/carray_slab.c +13 -0
  97. data/ext/carray_sort.c +22 -24
  98. data/ext/carray_sort_kernel.c +10 -10
  99. data/ext/carray_test.c +1 -1
  100. data/ext/extconf.rb +18 -0
  101. data/ext/mk_call_cfunc.rb +323 -145
  102. data/ext/mkkernel.rb +651 -129
  103. data/ext/ruby_carray.c +28 -17
  104. data/ext/version.h +4 -4
  105. data/lib/carray/autoload_carray.rb +24 -3
  106. data/lib/carray/autoload_method_extension.rb +14 -1
  107. data/lib/carray/axis_group.rb +84 -7
  108. data/lib/carray/basics.rb +63 -59
  109. data/lib/carray/bincount_nd.rb +12 -12
  110. data/lib/carray/block_iterator.rb +111 -25
  111. data/lib/carray/categorical.rb +154 -37
  112. data/lib/carray/categorical_iterator.rb +300 -151
  113. data/lib/carray/conditional.rb +14 -14
  114. data/lib/carray/const_string.rb +131 -27
  115. data/lib/carray/construct.rb +61 -0
  116. data/lib/carray/core_extensions.rb +62 -5
  117. data/lib/carray/data_type_extension.rb +21 -1
  118. data/lib/carray/data_type_limits.rb +91 -0
  119. data/lib/carray/fixlen_string.rb +1 -1
  120. data/lib/carray/frame/concat.rb +3 -3
  121. data/lib/carray/frame/convert.rb +1 -1
  122. data/lib/carray/frame/csv_parser.rb +11 -4
  123. data/lib/carray/frame/frame.rb +85 -21
  124. data/lib/carray/frame/group.rb +36 -3
  125. data/lib/carray/frame/io.rb +224 -15
  126. data/lib/carray/frame/records.rb +18 -4
  127. data/lib/carray/frame/sort.rb +1 -1
  128. data/lib/carray/frame/verbs.rb +32 -12
  129. data/lib/carray/fuse_source.rb +123 -0
  130. data/lib/carray/fusion.rb +218 -0
  131. data/lib/carray/histogram.rb +16 -16
  132. data/lib/carray/inspect.rb +43 -16
  133. data/lib/carray/iterator.rb +147 -3
  134. data/lib/carray/lazy.rb +88 -73
  135. data/lib/carray/mask_gap_fill.rb +3 -1
  136. data/lib/carray/meld_reduce.rb +2 -2
  137. data/lib/carray/methods/align_addr.rb +1 -1
  138. data/lib/carray/methods/composition.rb +1 -1
  139. data/lib/carray/methods/discovery_along.rb +74 -0
  140. data/lib/carray/methods/factorize.rb +50 -0
  141. data/lib/carray/methods/is_in.rb +25 -14
  142. data/lib/carray/methods/locate_addr.rb +81 -3
  143. data/lib/carray/methods/mask_duplicates.rb +36 -2
  144. data/lib/carray/methods/meshgrid.rb +4 -5
  145. data/lib/carray/methods/mode.rb +2 -2
  146. data/lib/carray/methods/nunique.rb +23 -2
  147. data/lib/carray/methods/repeat.rb +110 -0
  148. data/lib/carray/methods/snap.rb +7 -2
  149. data/lib/carray/methods/unique.rb +44 -4
  150. data/lib/carray/methods/value_counts.rb +2 -2
  151. data/lib/carray/rng.rb +86 -0
  152. data/lib/carray/runtime.rb +0 -19
  153. data/lib/carray/slab_iterator.rb +77 -19
  154. data/lib/carray/string_operation_extension.rb +10 -6
  155. data/lib/carray/time.rb +906 -491
  156. data/lib/carray/window_iterator.rb +421 -27
  157. data/lib/carray.rb +7 -5
  158. data/yard-stubs/ca_obj_array.rb +385 -0
  159. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  160. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  161. data/yard-stubs/ca_obj_block.rb +68 -0
  162. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  163. data/yard-stubs/ca_obj_fake.rb +31 -0
  164. data/yard-stubs/ca_obj_farray.rb +32 -0
  165. data/yard-stubs/ca_obj_field.rb +45 -0
  166. data/yard-stubs/ca_obj_grid.rb +35 -0
  167. data/yard-stubs/ca_obj_refer.rb +72 -0
  168. data/yard-stubs/ca_obj_roll.rb +45 -0
  169. data/yard-stubs/ca_obj_shift.rb +43 -0
  170. data/yard-stubs/ca_obj_stride.rb +181 -0
  171. data/yard-stubs/ca_obj_tile.rb +29 -0
  172. data/yard-stubs/ca_obj_transpose.rb +40 -0
  173. data/yard-stubs/ca_obj_window.rb +57 -0
  174. data/yard-stubs/carray_access.rb +131 -0
  175. data/yard-stubs/carray_attribute.rb +246 -0
  176. data/yard-stubs/carray_broadcast.rb +37 -0
  177. data/yard-stubs/carray_cast.rb +489 -0
  178. data/yard-stubs/carray_class.rb +65 -0
  179. data/yard-stubs/carray_conversion.rb +76 -0
  180. data/yard-stubs/carray_copy.rb +79 -0
  181. data/yard-stubs/carray_core.rb +34 -0
  182. data/yard-stubs/carray_count.rb +84 -0
  183. data/yard-stubs/carray_element.rb +108 -0
  184. data/yard-stubs/carray_generate.rb +66 -0
  185. data/yard-stubs/carray_lazy.rb +228 -0
  186. data/yard-stubs/carray_loop.rb +140 -0
  187. data/yard-stubs/carray_mask.rb +259 -0
  188. data/yard-stubs/carray_math.rb +1615 -0
  189. data/yard-stubs/carray_mathfunc.rb +45 -0
  190. data/yard-stubs/carray_median_percentile.rb +103 -0
  191. data/yard-stubs/carray_memory_view.rb +163 -0
  192. data/yard-stubs/carray_order.rb +252 -0
  193. data/yard-stubs/carray_random.rb +89 -0
  194. data/yard-stubs/carray_scatter.rb +106 -0
  195. data/yard-stubs/carray_slab.rb +59 -0
  196. data/yard-stubs/carray_sort.rb +165 -0
  197. data/yard-stubs/carray_test.rb +85 -0
  198. data/yard-stubs/carray_undef.rb +64 -0
  199. data/yard-stubs/carray_utils.rb +97 -0
  200. data/yard-stubs/ruby_carray.rb +193 -0
  201. metadata +67 -10
  202. data/ext/ca_obj_unbound_repeat.c +0 -496
@@ -34,6 +34,7 @@ static inline VALUE BOOL2VAL (boolean8_t x) { return x ? Qtrue : Qfalse; }
34
34
  #endif
35
35
 
36
36
  #include "ca_op_powi.h"
37
+ #include "ca_op_cmplx64.h"
37
38
 
38
39
  /* ===== sort_index_ki ============================================ */
39
40
 
@@ -34,6 +34,7 @@ static inline VALUE BOOL2VAL (boolean8_t x) { return x ? Qtrue : Qfalse; }
34
34
  #endif
35
35
 
36
36
  #include "ca_op_powi.h"
37
+ #include "ca_op_cmplx64.h"
37
38
 
38
39
  /* ===== triop fma ============================================ */
39
40
 
data/ext/carray_lazy.c CHANGED
@@ -93,6 +93,10 @@ ca_lazy_marker_setup (CALazyMarker *ca, CArray *parent)
93
93
  the parent and violate the shadow semantics. */
94
94
  ca_set_flag(ca, CA_FLAG_READ_ONLY);
95
95
 
96
+ /* Storage-identical wrapper: the kernel_iterator entry strip and the
97
+ view-creation lift both ask for this. */
98
+ ca_set_flag(ca, CA_FLAG_IS_LAZY_MARKER);
99
+
96
100
  return 0;
97
101
  }
98
102
 
@@ -253,6 +257,105 @@ rb_ca_lazy (VALUE self)
253
257
  return rb_ca_lazy_marker_new(self);
254
258
  }
255
259
 
260
+ /* ------------------------------------------------------------------- */
261
+ /* CA_OBJECT GC guard */
262
+ /* ------------------------------------------------------------------- */
263
+
264
+ /* A CA_OBJECT cell is a VALUE. While a view materialises into a raw
265
+ buffer -- the destination `copy` will hand out, a view's own attach
266
+ buffer, or an arena scratch -- those VALUEs live somewhere the GC
267
+ cannot see: an xmalloc'd block owned by no Ruby object. For every
268
+ other data_type that is harmless, but an object-lane kernel calls
269
+ rb_funcall per cell, and any of those can collect. The results
270
+ written so far are then freed under the buffer, and the array comes
271
+ back holding recycled slots: wrong values, and a crash once a slot is
272
+ reused as something else.
273
+
274
+ So such buffers are registered here for the duration and marked from
275
+ a hidden guard object that lives as long as the process. Two kinds
276
+ are registered:
277
+
278
+ holds a contiguous run of cells, pushed and popped around a
279
+ whole-view transfer or the handoff to a Ruby owner
280
+ slots an arena scratch, tagged at acquire and held until release
281
+
282
+ Only contiguous runs are held. A strided window would cover the same
283
+ cells but its extent has to be taken on trust from the caller's
284
+ strides, and a window wider than the buffer behind it would be filled
285
+ and marked out of bounds. The whole-view entry knows the extent
286
+ exactly, and every partial transfer writes inside it. */
287
+
288
+ #define CA_GC_HOLD_MAX 64
289
+
290
+ typedef struct ca_gc_hold {
291
+ VALUE *ptr;
292
+ ca_size_t elements;
293
+ } ca_gc_hold_t;
294
+
295
+ static ca_gc_hold_t ca_gc_holds[CA_GC_HOLD_MAX];
296
+ static int ca_gc_hold_depth = 0;
297
+
298
+ static void ca_lazy_arena_mark_object_slots (void);
299
+
300
+ /* Returns the depth to hand back to ca_gc_hold_pop_to, so nested holds
301
+ unwind in order. Pushing is best-effort at the ceiling: a chain
302
+ deeper than CA_GC_HOLD_MAX loses the protection for its innermost
303
+ buffers rather than raising in the middle of a materialise. The cells
304
+ must already be valid VALUEs -- the caller fills a fresh buffer with
305
+ Qnil first. */
306
+ int
307
+ ca_gc_hold_push (void *ptr, ca_size_t n_elements)
308
+ {
309
+ int depth = ca_gc_hold_depth;
310
+
311
+ if ( depth >= CA_GC_HOLD_MAX || n_elements <= 0 || ptr == NULL ) {
312
+ return -1;
313
+ }
314
+ ca_gc_holds[depth].ptr = (VALUE *) ptr;
315
+ ca_gc_holds[depth].elements = n_elements;
316
+ ca_gc_hold_depth = depth + 1;
317
+ return depth;
318
+ }
319
+
320
+ void
321
+ ca_gc_hold_pop_to (int depth)
322
+ {
323
+ if ( depth >= 0 && depth < ca_gc_hold_depth ) {
324
+ ca_gc_hold_depth = depth;
325
+ }
326
+ }
327
+
328
+ static VALUE ca_gc_guard = Qnil;
329
+
330
+ /* The wrapped pointer must be non-NULL: Ruby's GC skips the mark
331
+ function of a TypedData whose data pointer is NULL. */
332
+ static int ca_gc_guard_body = 0;
333
+
334
+ static void
335
+ ca_gc_guard_mark (void *ptr)
336
+ {
337
+ int i;
338
+ (void) ptr;
339
+ for ( i = 0; i < ca_gc_hold_depth; i++ ) {
340
+ VALUE *p = ca_gc_holds[i].ptr;
341
+ ca_size_t n = ca_gc_holds[i].elements;
342
+ while ( n-- ) rb_gc_mark(*p++);
343
+ }
344
+ ca_lazy_arena_mark_object_slots();
345
+ }
346
+
347
+ static void
348
+ ca_gc_guard_free (void *ptr)
349
+ {
350
+ (void) ptr;
351
+ }
352
+
353
+ static const rb_data_type_t ca_gc_guard_data_type = {
354
+ "carray_object_gc_guard",
355
+ { ca_gc_guard_mark, ca_gc_guard_free, NULL, },
356
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
357
+ };
358
+
256
359
  /* ------------------------------------------------------------------- */
257
360
  /* ca_lazy_arena */
258
361
  /* ------------------------------------------------------------------- */
@@ -277,8 +380,9 @@ rb_ca_lazy (VALUE self)
277
380
  programming error or a pathological chain rather than a graceful
278
381
  fallback.
279
382
 
280
- CAREFUL: single-thread only (see CLAUDE.md "thread-safety is not a
281
- goal"). The arena is process-global static state. */
383
+ CAREFUL: single-thread only -- thread-safety across concurrent access
384
+ is not a goal (see guides/devel/04_attach_lifecycle.md). The arena is
385
+ process-global static state. */
282
386
 
283
387
  #define CA_LAZY_ARENA_SLOTS 32
284
388
 
@@ -286,6 +390,7 @@ typedef struct ca_lazy_arena_slot {
286
390
  void *ptr; /* xmalloc'd buffer, NULL = unused-and-unallocated */
287
391
  ca_size_t bytes; /* allocated size of ptr (= capacity of this slot) */
288
392
  int in_use; /* 1 = currently acquired, 0 = available */
393
+ ca_size_t object_elements; /* >0 = holds VALUEs; marked while in_use */
289
394
  } ca_lazy_arena_slot_t;
290
395
 
291
396
  typedef struct ca_lazy_arena {
@@ -308,6 +413,7 @@ ca_lazy_arena_enter (void)
308
413
  int i;
309
414
  for ( i = 0; i < CA_LAZY_ARENA_SLOTS; i++ ) {
310
415
  ca_lazy_arena.slots[i].in_use = 0;
416
+ ca_lazy_arena.slots[i].object_elements = 0;
311
417
  }
312
418
  }
313
419
  ca_lazy_arena.depth++;
@@ -319,7 +425,16 @@ ca_lazy_arena_exit (void)
319
425
  if ( ca_lazy_arena.depth > 0 ) {
320
426
  ca_lazy_arena.depth--;
321
427
  }
322
- /* Keep all slot buffers; they amortise into subsequent calls. */
428
+ /* Keep all slot buffers, and leave in_use and the object tags alone.
429
+ A scratch can be acquired outside any enter/exit bracket -- a lazy
430
+ view's attach path does exactly that -- so an inner materialise
431
+ dropping to depth 0 is not evidence that nothing is held. Recovery
432
+ from an abandoned slot happens on the next depth-0 entry instead.
433
+
434
+ An abandoned object slot stays marked until then, which is a leak
435
+ but not a hazard: acquire only ever hands out a slot that is not in
436
+ use, so nothing overwrites it, and marking is what keeps its cells
437
+ from being collected in the first place. */
323
438
  }
324
439
 
325
440
  void *
@@ -350,6 +465,7 @@ ca_lazy_arena_acquire (ca_size_t bytes)
350
465
  }
351
466
  if ( best >= 0 ) {
352
467
  ca_lazy_arena.slots[best].in_use = 1;
468
+ ca_lazy_arena.slots[best].object_elements = 0;
353
469
  ca_lazy_arena.debug_reuse_count++;
354
470
  return ca_lazy_arena.slots[best].ptr;
355
471
  }
@@ -367,6 +483,7 @@ ca_lazy_arena_acquire (ca_size_t bytes)
367
483
  ca_lazy_arena.slots[empty].ptr = xmalloc(bytes);
368
484
  ca_lazy_arena.slots[empty].bytes = bytes;
369
485
  ca_lazy_arena.slots[empty].in_use = 1;
486
+ ca_lazy_arena.slots[empty].object_elements = 0;
370
487
  ca_lazy_arena.debug_xmalloc_count++;
371
488
  return ca_lazy_arena.slots[empty].ptr;
372
489
  }
@@ -380,6 +497,7 @@ ca_lazy_arena_acquire (ca_size_t bytes)
380
497
  ca_lazy_arena.slots[i].ptr = xmalloc(bytes);
381
498
  ca_lazy_arena.slots[i].bytes = bytes;
382
499
  ca_lazy_arena.slots[i].in_use = 1;
500
+ ca_lazy_arena.slots[i].object_elements = 0;
383
501
  ca_lazy_arena.debug_xmalloc_count++;
384
502
  return ca_lazy_arena.slots[i].ptr;
385
503
  }
@@ -402,6 +520,7 @@ ca_lazy_arena_release (void *ptr)
402
520
  for ( i = 0; i < CA_LAZY_ARENA_SLOTS; i++ ) {
403
521
  if ( ca_lazy_arena.slots[i].ptr == ptr ) {
404
522
  ca_lazy_arena.slots[i].in_use = 0;
523
+ ca_lazy_arena.slots[i].object_elements = 0;
405
524
  return;
406
525
  }
407
526
  }
@@ -410,6 +529,52 @@ ca_lazy_arena_release (void *ptr)
410
529
  genuine bugs at unrelated callsites. */
411
530
  }
412
531
 
532
+ /* Acquire a scratch that will hold `n_elements` VALUEs.
533
+
534
+ Unlike the byte-sized acquire this one Qnil-fills the buffer and tags
535
+ the slot, so the cells stay markable for as long as the caller holds
536
+ it. Object-lane kernels run rb_funcall over their operands, so a
537
+ scratch pulled from a lazy operand holds VALUEs that exist nowhere
538
+ else until the kernel has consumed them. */
539
+ void *
540
+ ca_lazy_arena_acquire_object (ca_size_t n_elements)
541
+ {
542
+ void *ptr;
543
+ int i;
544
+
545
+ if ( n_elements < 0 ) n_elements = 0;
546
+ ptr = ca_lazy_arena_acquire(n_elements * (ca_size_t) sizeof(VALUE));
547
+
548
+ {
549
+ VALUE *p = (VALUE *) ptr;
550
+ ca_size_t n = n_elements;
551
+ while ( n-- ) *p++ = Qnil;
552
+ }
553
+
554
+ for ( i = 0; i < CA_LAZY_ARENA_SLOTS; i++ ) {
555
+ if ( ca_lazy_arena.slots[i].ptr == ptr ) {
556
+ ca_lazy_arena.slots[i].object_elements = n_elements;
557
+ break;
558
+ }
559
+ }
560
+ return ptr;
561
+ }
562
+
563
+ static void
564
+ ca_lazy_arena_mark_object_slots (void)
565
+ {
566
+ int i;
567
+ for ( i = 0; i < CA_LAZY_ARENA_SLOTS; i++ ) {
568
+ ca_lazy_arena_slot_t *s = &ca_lazy_arena.slots[i];
569
+ ca_size_t n;
570
+ VALUE *p;
571
+ if ( ! s->in_use || s->object_elements <= 0 ) continue;
572
+ p = (VALUE *) s->ptr;
573
+ n = s->object_elements;
574
+ while ( n-- ) rb_gc_mark(*p++);
575
+ }
576
+ }
577
+
413
578
  /* Test instrumentation — exposed to Ruby for the arena smoke and
414
579
  regression assertions. Not user-facing API. */
415
580
  static VALUE
@@ -490,8 +655,7 @@ rb_ca_lazy_arena_s_slot_capacities (VALUE klass)
490
655
  * just let the scalar choose)
491
656
  * numeric self -> self_ca->data_type
492
657
  *
493
- * Callers broadcast against the array via stride 0 in xfer_stride
494
- * (or wrap the scalar in CAUnboundRepeat when the chain needs it). */
658
+ * Callers broadcast against the array via stride 0 in xfer_stride. */
495
659
  VALUE
496
660
  ca_lazy_wrap_scalar (VALUE other, CArray *self_ca)
497
661
  {
@@ -543,6 +707,12 @@ Init_carray_lazy (void)
543
707
 
544
708
  rb_define_method(rb_cCArray, "lazy", rb_ca_lazy, 0);
545
709
 
710
+ /* Hidden, never collected: its mark function is what keeps the
711
+ in-flight CA_OBJECT buffers reachable. */
712
+ ca_gc_guard = TypedData_Wrap_Struct(rb_cObject, &ca_gc_guard_data_type,
713
+ &ca_gc_guard_body);
714
+ rb_gc_register_mark_object(ca_gc_guard);
715
+
546
716
  rb_define_alloc_func(rb_cCALazyMarker, rb_ca_lazy_marker_s_allocate);
547
717
  rb_define_method(rb_cCALazyMarker, "initialize_copy",
548
718
  rb_ca_lazy_marker_initialize_copy, 1);
data/ext/carray_loop.c CHANGED
@@ -120,6 +120,13 @@ rb_ca_index_walk (VALUE self, CArray *ca, int8_t level,
120
120
  volatile VALUE ret = Qnil;
121
121
  ca_size_t i;
122
122
  if ( level == ca->ndim - 1 ) {
123
+ /* One value-yield buffer for the whole innermost run. It was once an
124
+ ALLOCA_N inside the loop, which never releases per iteration: the C
125
+ stack grew by (ndim + 1) VALUEs per cell and a long enough axis
126
+ raised SystemStackError (a 1-D each_with_index / map_with_index!
127
+ died around a million cells on the main stack, and far earlier on a
128
+ Thread's smaller one). */
129
+ VALUE argv[CA_RANK_MAX + 1];
123
130
  for (i=0; i<ca->dim[level]; i++) {
124
131
  volatile VALUE obj;
125
132
  idx[level] = i;
@@ -132,7 +139,6 @@ rb_ca_index_walk (VALUE self, CArray *ca, int8_t level,
132
139
  that want the whole subscript should write |*idx|. */
133
140
  if ( mode & CA_LOOP_WITH_VALUE ) {
134
141
  int argc = (int)ca->ndim + 1;
135
- VALUE *argv = ALLOCA_N(VALUE, argc);
136
142
  argv[0] = rb_ca_fetch_index(self, idx);
137
143
  MEMCPY(argv + 1, RARRAY_CONST_PTR(ridx), VALUE, ca->ndim);
138
144
  obj = rb_yield_values2(argc, argv);
data/ext/carray_mask.c CHANGED
@@ -607,7 +607,7 @@ ca_count_not_masked (void *ap)
607
607
  /* Scalar-fill every masked cell with fill_value, then clear its mask bit.
608
608
  This depends only on the cell BYTE WIDTH, not the data type (the work is
609
609
  a width-sized copy), so a single width switch covers every numeric /
610
- complex / object dtype; the default arm handles fixlen and any other
610
+ complex / object data type; the default arm handles fixlen and any other
611
611
  width via memcpy. The typed arms let the compiler emit a plain store. */
612
612
  #define proc_fill_bang_width(T) \
613
613
  { \
@@ -660,32 +660,47 @@ ca_unmask (void *ap, char *fill_value)
660
660
  }
661
661
  }
662
662
 
663
- CArray *
664
- ca_unmask_copy (void *ap, char *fill_value)
665
- {
666
- CArray *ca = (CArray *) ap;
663
+ typedef struct {
664
+ CArray *ca;
667
665
  CArray *co;
666
+ char *fill_value;
667
+ } ca_unmask_copy_args_t;
668
+
669
+ static VALUE
670
+ ca_unmask_copy_fill (VALUE arg)
671
+ {
672
+ ca_unmask_copy_args_t *a = (ca_unmask_copy_args_t *) arg;
673
+ CArray *ca = a->ca, *co = a->co;
668
674
  char *q;
669
675
  boolean8_t *m;
670
676
  ca_size_t i;
671
677
 
672
- co = ca_template(ca);
673
678
  ca_copy_data(ca, co->ptr);
674
679
 
675
- if ( fill_value && ca_has_mask(ca) ) {
680
+ if ( a->fill_value && ca_has_mask(ca) ) {
676
681
  ca_attach(ca);
677
682
  q = co->ptr;
678
683
  m = (boolean8_t *) ca->mask->ptr;
679
684
  for (i=0; i<ca->elements; i++) {
680
685
  if ( *m ) {
681
- memcpy(q, fill_value, ca->bytes);
686
+ memcpy(q, a->fill_value, ca->bytes);
682
687
  }
683
688
  m++; q+=co->bytes;
684
689
  }
685
690
  ca_detach(ca);
686
691
  }
692
+ return Qnil;
693
+ }
687
694
 
688
- return co;
695
+ CArray *
696
+ ca_unmask_copy (void *ap, char *fill_value)
697
+ {
698
+ ca_unmask_copy_args_t a;
699
+ a.ca = (CArray *) ap;
700
+ a.co = ca_template(a.ca);
701
+ a.fill_value = fill_value;
702
+ ca_fill_or_free(a.co, ca_unmask_copy_fill, (VALUE) &a);
703
+ return a.co;
689
704
  }
690
705
 
691
706
  void
@@ -894,6 +909,7 @@ rb_ca_value_array (VALUE self)
894
909
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca);
895
910
 
896
911
  obj = rb_ca_refer_new(self, ca->data_type, ca->ndim, ca->dim, ca->bytes, 0);
912
+
897
913
  TypedData_Get_Struct(obj, CArray, &carray_data_type, co);
898
914
 
899
915
  /* Value arrays ignore the mask by definition. ca_stride_setup
@@ -1112,7 +1128,7 @@ The returned array doesn't have the mask array.
1112
1128
  static VALUE
1113
1129
  rb_ca_unmask_method (int argc, VALUE *argv, VALUE self)
1114
1130
  {
1115
- volatile VALUE rfval = CA_NIL, rcs;
1131
+ volatile VALUE rfval = CA_UNSPECIFIED, rcs;
1116
1132
  CArray *ca;
1117
1133
  CScalar *cv;
1118
1134
  char *fval = NULL;
@@ -1125,7 +1141,7 @@ rb_ca_unmask_method (int argc, VALUE *argv, VALUE self)
1125
1141
 
1126
1142
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca);
1127
1143
 
1128
- if ( rfval != CA_NIL ) {
1144
+ if ( rfval != CA_UNSPECIFIED ) {
1129
1145
  /* Face has surface != storage; the fill value is cast in the storage data_type. */
1130
1146
  int8_t conv_type = ca->data_type;
1131
1147
  if ( ca_is_face(ca) ) {
@@ -1171,7 +1187,7 @@ The returned array doesn't have the mask array.
1171
1187
  static VALUE
1172
1188
  rb_ca_unmask_copy_method (int argc, VALUE *argv, VALUE self)
1173
1189
  {
1174
- volatile VALUE obj, rfval = CA_NIL, rcs;
1190
+ volatile VALUE obj, rfval = CA_UNSPECIFIED, rcs;
1175
1191
  CArray *ca, *co;
1176
1192
  CScalar *cv;
1177
1193
  char *fval = NULL;
@@ -1182,7 +1198,7 @@ rb_ca_unmask_copy_method (int argc, VALUE *argv, VALUE self)
1182
1198
 
1183
1199
  TypedData_Get_Struct(self, CArray, &carray_data_type, ca);
1184
1200
 
1185
- if ( rfval != CA_NIL ) {
1201
+ if ( rfval != CA_UNSPECIFIED ) {
1186
1202
  /* Face has surface != storage; the fill value is cast in the storage data_type. */
1187
1203
  int8_t conv_type = ca->data_type;
1188
1204
  if ( ca_is_face(ca) ) {
@@ -152,7 +152,7 @@ typedef enum { PCT_LOWER_ONLY, PCT_UPPER_ONLY, PCT_BOTH } pct_need_t;
152
152
 
153
153
  /* [shared] which of lower (sorted[k]) / upper (sorted[k+1]) the method
154
154
  needs. Lets the caller skip the fetch it won't use. Used by both
155
- lanes (the need classification is dtype-independent). */
155
+ lanes (the need classification is data type-independent). */
156
156
  static pct_need_t
157
157
  pct_need (VALUE method, long k, double r, long n)
158
158
  {
@@ -336,6 +336,48 @@ median_flat (VALUE self, long min_count, VALUE fill_value, int keep_axis)
336
336
  Every function in this block (down to the median entry) is [object].
337
337
  ===================================================================== */
338
338
 
339
+ /* [object] Is this operand's arithmetic the arithmetic interpolation means?
340
+ `lo` / `hi` are a stored object on the flat path and an object CArray of
341
+ them on the per-axis one, so look through a CArray to an element. An
342
+ empty one has nothing to interpolate and passes. */
343
+ static int
344
+ mp_object_interpolates (VALUE v)
345
+ {
346
+ if ( rb_obj_is_kind_of(v, rb_cNumeric) ) {
347
+ return 1;
348
+ }
349
+ if ( rb_obj_is_kind_of(v, rb_cCArray) ) {
350
+ CArray *ca;
351
+ GetCArray(v, ca);
352
+ if ( ca->elements == 0 ) {
353
+ return 1;
354
+ }
355
+ return rb_obj_is_kind_of(rb_ca_fetch_addr(v, 0), rb_cNumeric) ? 1 : 0;
356
+ }
357
+ return 0;
358
+ }
359
+
360
+ /* [object] Refuse to interpolate between two elements that have no
361
+ arithmetic meaning it. A String has both a `*` and a `+`, and
362
+ String#* truncates the Float weight to zero -- so :linear quietly
363
+ returned "" instead of failing, which is the reason this guard is
364
+ here rather than a rescue. The three picking methods need no
365
+ arithmetic and are the way through. */
366
+ static void
367
+ mp_require_interpolable (const char *who, VALUE method, VALUE lo)
368
+ {
369
+ if ( mp_object_interpolates(lo) ) {
370
+ return;
371
+ }
372
+ rb_raise(rb_eCADataTypeError,
373
+ "%s: method: :%s interpolates between two elements, which %s "
374
+ "does not support; use method: :lower, :higher or :nearest to "
375
+ "pick an element instead",
376
+ who, rb_id2name(SYM2ID(method)),
377
+ rb_obj_classname(rb_obj_is_kind_of(lo, rb_cCArray)
378
+ ? rb_ca_fetch_addr(lo, 0) : lo));
379
+ }
380
+
339
381
  /* [object] recv.mid(axis: raxis) -- e.g. min/max/sort along an axis. */
340
382
  static VALUE
341
383
  obj_call_axis (VALUE recv, ID mid, VALUE raxis)
@@ -419,6 +461,17 @@ median_object_axis (VALUE self, long axis, long n, int keep_axis)
419
461
  long k = n / 2 - 1;
420
462
  VALUE lo, hi;
421
463
  obj_kth_pair(self, axis, k, n, &lo, &hi);
464
+ /* An even count has no middle element, only the average of two -- so
465
+ a column whose objects have no arithmetic has a median only at odd
466
+ length. percentile(50, method: :lower) names one either way. */
467
+ if ( ! mp_object_interpolates(lo) ) {
468
+ rb_raise(rb_eCADataTypeError,
469
+ "median: an even number of elements has no middle one, and "
470
+ "%s cannot be averaged; use percentile(50, method: :lower) "
471
+ "(or :higher / :nearest) to pick an element instead",
472
+ rb_obj_classname(rb_obj_is_kind_of(lo, rb_cCArray)
473
+ ? rb_ca_fetch_addr(lo, 0) : lo));
474
+ }
422
475
  result = rb_funcall(rb_funcall(lo, id_plus, 1, hi), id_div, 1, DBL2NUM(2.0));
423
476
  } else {
424
477
  VALUE kv = obj_kth_one(self, axis, (n - 1) / 2);
@@ -480,12 +533,14 @@ pct_compute_object (VALUE method, long k, double r, long n, VALUE lo, VALUE hi)
480
533
  }
481
534
  if ( method == sym_linear ) {
482
535
  if ( r == 0.0 || k + 1 >= n ) return rb_funcall(lo, id_mul, 1, DBL2NUM(1.0));
536
+ mp_require_interpolable("percentile", method, lo);
483
537
  VALUE a = rb_funcall(lo, id_mul, 1, DBL2NUM(1.0 - r));
484
538
  VALUE b = rb_funcall(hi, id_mul, 1, DBL2NUM(r));
485
539
  return rb_funcall(a, id_plus, 1, b);
486
540
  }
487
541
  if ( method == sym_midpoint ) {
488
542
  if ( k + 1 >= n ) return rb_funcall(lo, id_mul, 1, DBL2NUM(1.0));
543
+ mp_require_interpolable("percentile", method, lo);
489
544
  return rb_funcall(rb_funcall(lo, id_plus, 1, hi), id_div, 1, DBL2NUM(2.0));
490
545
  }
491
546
  rb_raise(rb_eArgError, "percentile: invalid method (BUG)");
@@ -99,46 +99,70 @@ static ca_mv_runtime_type_t ca_mv_runtime_types[CA_MV_NUM_RUNTIME_OBJ_TYPES] = {
99
99
 
100
100
  /* ---------------- format / data_type mapping ----------------
101
101
  Bidirectional table. Outbound table (ca_mv_format_for) picks the
102
- canonical PEP 3118 specifier for each CArray data_type. Inbound
103
- table (ca_mv_data_type_from_format) is Postel: it accepts a wider
104
- set of synonyms that other producers emit (they describe int32
105
- differently). */
102
+ specifier each CArray data_type publishes at the top level of a
103
+ MemoryView. Inbound table (ca_mv_data_type_from_format) is Postel:
104
+ it accepts a wider set of synonyms that other producers emit (they
105
+ describe int32 differently).
106
+
107
+ The outbound spellings follow ruby/memory_view.h, which documents
108
+ `format` as a sequence of pack-derived specifiers and expects
109
+ item_size to equal rb_memory_view_item_size_from_format(format).
110
+ PEP 3118 reuses several of those letters for other types, so the two
111
+ vocabularies agree on `i` / `I` / `q` / `Q` / `f` / `d` and disagree
112
+ below 32 bits: what PEP calls `b` / `B` / `h` / `H`, Ruby calls
113
+ `c` / `C` / `s` / `S`. Emitting Ruby's spelling is what lets a
114
+ generic Ruby consumer -- Fiddle::MemoryView, say -- read elements out
115
+ of a CArray at all; PEP-speaking consumers are reached across the
116
+ language boundary, where pycall-memoryview translates.
117
+
118
+ Three types have no Ruby spelling at all: boolean and the two
119
+ complex widths. They keep their PEP form, which Ruby's parser
120
+ rejects, because the alternative ("C" for a boolean, "dd" for a
121
+ complex) would misdescribe the data rather than merely fail to
122
+ describe it. */
106
123
 
107
124
  static const char *
108
125
  ca_mv_format_for (int8_t data_type)
109
126
  {
110
127
  switch (data_type) {
111
- case CA_BOOLEAN: return "?";
112
- case CA_INT8: return "b";
113
- case CA_UINT8: return "B";
114
- case CA_INT16: return "h";
115
- case CA_UINT16: return "H";
128
+ case CA_BOOLEAN: return "?"; /* no Ruby spelling */
129
+ case CA_INT8: return "c";
130
+ case CA_UINT8: return "C";
131
+ case CA_INT16: return "s";
132
+ case CA_UINT16: return "S";
116
133
  case CA_INT32: return "i";
117
134
  case CA_UINT32: return "I";
118
135
  case CA_INT64: return "q";
119
136
  case CA_UINT64: return "Q";
120
137
  case CA_FLOAT32: return "f";
121
138
  case CA_FLOAT64: return "d";
122
- case CA_CMPLX64: return "Zf";
123
- case CA_CMPLX128: return "Zd";
139
+ case CA_CMPLX64: return "Zf"; /* no Ruby spelling */
140
+ case CA_CMPLX128: return "Zd"; /* no Ruby spelling */
124
141
  default:
125
142
  return NULL;
126
143
  }
127
144
  }
128
145
 
129
146
  /* PEP 3118 single-character specifier for a primitive data_type, for
130
- use inside a `T{...}` struct format body. Identical to the top-level
131
- producer specifier: since the 2026-06-29 PEP 3118 strict flip both the
132
- top-level and struct-body contexts emit the same chars, so this
133
- delegates to ca_mv_format_for. Kept as a distinct entry point so the
134
- struct-body context can diverge again without touching call sites.
147
+ use inside a `T{...}` struct format body. This is the context the
148
+ distinct entry point was kept for: `T{...}` is a PEP 3118 construct
149
+ with no Ruby counterpart, so a record body stays in PEP's vocabulary
150
+ even though the top level now publishes Ruby's. A consumer reading
151
+ the body reads it as PEP, and a bridge that forwards the record
152
+ whole -- as pycall-memoryview does -- keeps it intact.
135
153
 
136
154
  Returns NULL for non-primitive (CA_FIXLEN / CA_OBJECT) or for the
137
155
  retired float128 / cmplx256 enum slots. */
138
156
  static const char *
139
157
  ca_mv_pep3118_char (int8_t data_type)
140
158
  {
141
- return ca_mv_format_for(data_type);
159
+ switch (data_type) {
160
+ case CA_INT8: return "b";
161
+ case CA_UINT8: return "B";
162
+ case CA_INT16: return "h";
163
+ case CA_UINT16: return "H";
164
+ default: return ca_mv_format_for(data_type);
165
+ }
142
166
  }
143
167
 
144
168
  /* Build (or fetch from cache) the PEP 3118 struct format string for
@@ -362,7 +386,7 @@ ca_mv_data_type_from_format (const char *format, ssize_t item_size)
362
386
  }
363
387
 
364
388
  /* Derive the data_type a MemoryView producer would import as, by parsing its
365
- format — without importing the data. This is the canonical "what dtype is
389
+ format — without importing the data. This is the canonical "what data type is
366
390
  this MV" question, decoupled from the import strategy (copy via
367
391
  from_memory_view vs zero-copy via wrap_memory_view). Used by
368
392
  ca_arg_to_data_type so CArray.result_type / promote_list can treat an MV
@@ -414,7 +438,6 @@ ca_mv_strategy_for (int16_t obj_type)
414
438
  if (obj_type == CA_OBJ_SELECT) return CA_MV_ATTACH;
415
439
  if (obj_type == CA_OBJ_REPEAT) return CA_MV_REPEAT;
416
440
  if (obj_type == CA_OBJ_OBJECT) return CA_MV_REJECT;
417
- if (obj_type == CA_OBJ_UNBOUND_REPEAT) return CA_MV_REJECT;
418
441
  /* Plain CAStride (created via #as_strided, or wrap_memory_view from
419
442
  a strided producer). Reuse the CA_MV_TRANS handler since it
420
443
  already reads strides[] / base_offset directly from the CAStride
@@ -510,7 +533,6 @@ ca_mv_reject_reason_for (CArray *ca)
510
533
  /* Compile-time obj_types we know about */
511
534
  if (ca->obj_type == CA_OBJ_SELECT) return "boolean-mask selection (positions not expressible as strides)";
512
535
  if (ca->obj_type == CA_OBJ_OBJECT) return "stores Ruby VALUEs, not raw bytes";
513
- if (ca->obj_type == CA_OBJ_UNBOUND_REPEAT) return "shape is not bound";
514
536
  /* Runtime-assigned obj_types: look up by name in the strategy table */
515
537
  for (int i = 0; i < CA_MV_NUM_RUNTIME_OBJ_TYPES; i++) {
516
538
  if (ca_mv_runtime_types[i].id == ca->obj_type) {
@@ -1110,7 +1132,10 @@ ca_mv_acquire_mask_view (VALUE mask_src, int8_t data_ndim,
1110
1132
  if (c == '<' || c == '>' || c == '=' || c == '!' || c == '@') {
1111
1133
  c = mv->format[1];
1112
1134
  }
1113
- if (c != '?' && c != 'B' && c != 'b') {
1135
+ /* One byte per element, under either vocabulary: PEP 3118 spells the
1136
+ integer widths 'b' / 'B', Ruby's MemoryView spells them 'c' / 'C'.
1137
+ '?' is PEP's boolean, which Ruby has no spelling for. */
1138
+ if (c != '?' && c != 'B' && c != 'b' && c != 'C' && c != 'c') {
1114
1139
  char fmt_buf[64];
1115
1140
  snprintf(fmt_buf, sizeof(fmt_buf), "%s", mv->format);
1116
1141
  MV_MASK_CLEANUP_AND_RAISE("mask: format must be bool/uint8/int8 (got %s)",