carray 3.0.2 → 3.0.4

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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +568 -0
  3. data/README.md +6 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +220 -0
  7. data/ext/ca_axis_group.c +201 -123
  8. data/ext/ca_binop_dispatch.c +46 -52
  9. data/ext/ca_binop_dispatch.h +1 -1
  10. data/ext/ca_categorical_iterator.c +249 -183
  11. data/ext/ca_compare.c +7 -12
  12. data/ext/ca_for_buffer.h +21 -68
  13. data/ext/ca_group_iter.c +38 -23
  14. data/ext/ca_kernel_iterator.c +1765 -2076
  15. data/ext/ca_kernel_iterator.h +584 -566
  16. data/ext/ca_monop_dispatch.c +86 -63
  17. data/ext/ca_monop_dispatch.h +5 -0
  18. data/ext/ca_obj_array.c +223 -56
  19. data/ext/ca_obj_bincmp.c +172 -138
  20. data/ext/ca_obj_binop.c +116 -230
  21. data/ext/ca_obj_bitarray.c +40 -26
  22. data/ext/ca_obj_bitfield.c +100 -68
  23. data/ext/ca_obj_block.c +19 -9
  24. data/ext/ca_obj_byte_swap.c +45 -46
  25. data/ext/ca_obj_const_string.c +269 -44
  26. data/ext/ca_obj_face.c +612 -89
  27. data/ext/ca_obj_face.h +111 -47
  28. data/ext/ca_obj_fake.c +36 -79
  29. data/ext/ca_obj_farray.c +7 -3
  30. data/ext/ca_obj_field.c +33 -8
  31. data/ext/ca_obj_fixlen_string.c +23 -24
  32. data/ext/ca_obj_grid.c +541 -210
  33. data/ext/ca_obj_meld.c +331 -111
  34. data/ext/ca_obj_moncmp.c +80 -65
  35. data/ext/ca_obj_monop.c +177 -149
  36. data/ext/ca_obj_object.c +664 -248
  37. data/ext/ca_obj_record.c +63 -23
  38. data/ext/ca_obj_reduce.c +18 -16
  39. data/ext/ca_obj_refer.c +140 -58
  40. data/ext/ca_obj_remap.c +85 -24
  41. data/ext/ca_obj_repeat.c +24 -9
  42. data/ext/ca_obj_roll.c +121 -16
  43. data/ext/ca_obj_select.c +61 -58
  44. data/ext/ca_obj_select_axis.c +73 -107
  45. data/ext/ca_obj_shift.c +32 -102
  46. data/ext/ca_obj_stack.c +372 -122
  47. data/ext/ca_obj_stride.c +348 -140
  48. data/ext/ca_obj_string.c +88 -22
  49. data/ext/ca_obj_tile.c +38 -21
  50. data/ext/ca_obj_time.c +45 -31
  51. data/ext/ca_obj_timedelta.c +43 -29
  52. data/ext/ca_obj_transpose.c +17 -6
  53. data/ext/ca_obj_triop.c +125 -141
  54. data/ext/ca_obj_window.c +330 -44
  55. data/ext/ca_op_ipower.c +24 -11
  56. data/ext/ca_op_powi.h +57 -30
  57. data/ext/ca_pow5_table.h +656 -0
  58. data/ext/ca_sort_kernels.h +70 -0
  59. data/ext/ca_sweep_engine.c +104 -81
  60. data/ext/ca_sweep_engine.h +7 -20
  61. data/ext/ca_triop_dispatch.c +4 -1
  62. data/ext/ca_triop_dispatch.h +5 -2
  63. data/ext/caframe_csv_reader.c +542 -0
  64. data/ext/carray.h +241 -92
  65. data/ext/carray_access.c +608 -276
  66. data/ext/carray_address_basis.c +494 -97
  67. data/ext/carray_attribute.c +129 -4
  68. data/ext/carray_bincount.c +39 -7
  69. data/ext/carray_broadcast.c +56 -19
  70. data/ext/carray_build_flags.h +1 -1
  71. data/ext/carray_call_cfunc.c +108 -28
  72. data/ext/carray_cast.c +1576 -223
  73. data/ext/carray_cast_func.rb +77 -24
  74. data/ext/carray_class.c +4 -3
  75. data/ext/carray_conversion.c +208 -75
  76. data/ext/carray_copy.c +82 -24
  77. data/ext/carray_core.c +646 -162
  78. data/ext/carray_count.c +171 -88
  79. data/ext/carray_element.c +94 -60
  80. data/ext/carray_factorize.c +670 -324
  81. data/ext/carray_generate.c +260 -83
  82. data/ext/carray_histogram.c +209 -106
  83. data/ext/carray_hold.c +14 -16
  84. data/ext/carray_index_classifier.c +422 -243
  85. data/ext/carray_internal.h +692 -3
  86. data/ext/carray_kernels_bincmp.c +7 -0
  87. data/ext/carray_kernels_binop.c +487 -120
  88. data/ext/carray_kernels_init.c +653 -50
  89. data/ext/carray_kernels_moncmp.c +15 -8
  90. data/ext/carray_kernels_monop.c +99 -43
  91. data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
  92. data/ext/carray_kernels_reduce_boolean.c +112 -36
  93. data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
  94. data/ext/carray_kernels_reduce_extreme.c +3032 -2892
  95. data/ext/carray_kernels_reduce_variance.c +439 -500
  96. data/ext/carray_kernels_scan.c +1097 -488
  97. data/ext/carray_kernels_search.c +3787 -2176
  98. data/ext/carray_kernels_sort.c +1200 -920
  99. data/ext/carray_kernels_triop.c +479 -7
  100. data/ext/carray_key_windows.c +216 -0
  101. data/ext/carray_lazy.c +967 -153
  102. data/ext/carray_loop.c +81 -65
  103. data/ext/carray_mask.c +910 -324
  104. data/ext/carray_math_kernel.h +1 -0
  105. data/ext/carray_median_percentile.c +299 -100
  106. data/ext/carray_memory_view.c +872 -758
  107. data/ext/carray_operator.c +1171 -364
  108. data/ext/carray_order.c +505 -133
  109. data/ext/carray_partition.c +112 -65
  110. data/ext/carray_random.c +327 -239
  111. data/ext/carray_round.c +273 -0
  112. data/ext/carray_scatter.c +346 -78
  113. data/ext/carray_segment.c +257 -0
  114. data/ext/carray_slab.c +249 -86
  115. data/ext/carray_sort.c +224 -227
  116. data/ext/carray_sort_kernel.c +8 -1
  117. data/ext/carray_struct.c +118 -39
  118. data/ext/carray_test.c +193 -69
  119. data/ext/carray_time_text.c +1182 -0
  120. data/ext/carray_undef.c +4 -0
  121. data/ext/carray_utils.c +579 -113
  122. data/ext/extconf.rb +27 -2
  123. data/ext/mk_call_cfunc.rb +19 -4
  124. data/ext/mk_pow5_table.rb +35 -0
  125. data/ext/mkkernel.rb +1202 -1014
  126. data/ext/ruby_carray.c +17 -5
  127. data/ext/version.h +4 -4
  128. data/ext/version.rb +4 -2
  129. data/lib/carray/attribute.rb +160 -17
  130. data/lib/carray/autoload_carray.rb +23 -27
  131. data/lib/carray/autoload_method_extension.rb +20 -5
  132. data/lib/carray/axis_group.rb +360 -63
  133. data/lib/carray/basics.rb +162 -28
  134. data/lib/carray/bincount_nd.rb +18 -2
  135. data/lib/carray/block_iterator.rb +231 -58
  136. data/lib/carray/boolean_reduce.rb +34 -42
  137. data/lib/carray/categorical.rb +242 -52
  138. data/lib/carray/categorical_iterator.rb +437 -827
  139. data/lib/carray/complex.rb +41 -34
  140. data/lib/carray/conditional.rb +150 -14
  141. data/lib/carray/const_string.rb +59 -4
  142. data/lib/carray/construct.rb +15 -11
  143. data/lib/carray/data_type_extension.rb +1 -0
  144. data/lib/carray/distance_limit.rb +77 -0
  145. data/lib/carray/fixlen_string.rb +7 -1
  146. data/lib/carray/frame/column_verbs.rb +70 -0
  147. data/lib/carray/frame/concat.rb +157 -6
  148. data/lib/carray/frame/convert.rb +82 -7
  149. data/lib/carray/frame/csv_parser.rb +688 -87
  150. data/lib/carray/frame/describe.rb +80 -0
  151. data/lib/carray/frame/frame.rb +369 -45
  152. data/lib/carray/frame/group.rb +631 -90
  153. data/lib/carray/frame/io.rb +506 -79
  154. data/lib/carray/frame/join.rb +411 -62
  155. data/lib/carray/frame/records.rb +116 -19
  156. data/lib/carray/frame/reshape.rb +455 -0
  157. data/lib/carray/frame/sort.rb +30 -18
  158. data/lib/carray/frame/verbs.rb +868 -61
  159. data/lib/carray/frame/windows.rb +117 -0
  160. data/lib/carray/frame.rb +4 -0
  161. data/lib/carray/fuse_source.rb +118 -2
  162. data/lib/carray/fusion.rb +257 -42
  163. data/lib/carray/histogram.rb +6 -1
  164. data/lib/carray/inspect.rb +54 -13
  165. data/lib/carray/iterator.rb +99 -9
  166. data/lib/carray/key_windows.rb +620 -0
  167. data/lib/carray/lazy.rb +216 -193
  168. data/lib/carray/mask_gap_fill.rb +112 -92
  169. data/lib/carray/math.rb +32 -19
  170. data/lib/carray/meld_reduce.rb +52 -21
  171. data/lib/carray/methods/align_addr.rb +8 -4
  172. data/lib/carray/methods/bin.rb +26 -18
  173. data/lib/carray/methods/bincount.rb +18 -2
  174. data/lib/carray/methods/bit_string.rb +15 -4
  175. data/lib/carray/methods/choose.rb +16 -3
  176. data/lib/carray/methods/composition.rb +18 -10
  177. data/lib/carray/methods/factorize.rb +2 -2
  178. data/lib/carray/methods/gather_nd.rb +25 -5
  179. data/lib/carray/methods/insert_block.rb +9 -14
  180. data/lib/carray/methods/is_between.rb +59 -0
  181. data/lib/carray/methods/is_in.rb +140 -10
  182. data/lib/carray/methods/join.rb +2 -7
  183. data/lib/carray/methods/lag.rb +49 -0
  184. data/lib/carray/methods/locate_addr.rb +182 -45
  185. data/lib/carray/methods/mode.rb +18 -1
  186. data/lib/carray/methods/nunique.rb +11 -6
  187. data/lib/carray/methods/repeat.rb +1 -20
  188. data/lib/carray/methods/resize.rb +21 -23
  189. data/lib/carray/methods/snap.rb +34 -12
  190. data/lib/carray/methods/unique.rb +11 -2
  191. data/lib/carray/methods/value_counts.rb +4 -11
  192. data/lib/carray/record_order.rb +208 -0
  193. data/lib/carray/rng.rb +3 -0
  194. data/lib/carray/runtime.rb +65 -17
  195. data/lib/carray/segment_iterator.rb +1221 -0
  196. data/lib/carray/serialize.rb +214 -30
  197. data/lib/carray/slab_iterator.rb +101 -27
  198. data/lib/carray/stack.rb +10 -4
  199. data/lib/carray/string.rb +105 -5
  200. data/lib/carray/string_operation_extension.rb +12 -4
  201. data/lib/carray/struct.rb +137 -58
  202. data/lib/carray/struct_builder.rb +124 -17
  203. data/lib/carray/time.rb +1222 -186
  204. data/lib/carray/window_iterator.rb +242 -81
  205. data/lib/carray.rb +9 -2
  206. data/yard-stubs/ca_obj_array.rb +4 -4
  207. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  208. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  209. data/yard-stubs/ca_obj_fake.rb +1 -1
  210. data/yard-stubs/ca_obj_field.rb +5 -1
  211. data/yard-stubs/ca_obj_grid.rb +21 -10
  212. data/yard-stubs/ca_obj_refer.rb +5 -4
  213. data/yard-stubs/ca_obj_roll.rb +2 -3
  214. data/yard-stubs/ca_obj_shift.rb +5 -1
  215. data/yard-stubs/ca_obj_stride.rb +10 -7
  216. data/yard-stubs/ca_obj_tile.rb +1 -1
  217. data/yard-stubs/ca_obj_transpose.rb +1 -1
  218. data/yard-stubs/ca_obj_window.rb +9 -5
  219. data/yard-stubs/carray_access.rb +11 -5
  220. data/yard-stubs/carray_attribute.rb +0 -5
  221. data/yard-stubs/carray_broadcast.rb +1 -1
  222. data/yard-stubs/carray_cast.rb +88 -43
  223. data/yard-stubs/carray_conversion.rb +15 -2
  224. data/yard-stubs/carray_copy.rb +12 -5
  225. data/yard-stubs/carray_core.rb +3 -3
  226. data/yard-stubs/carray_count.rb +9 -0
  227. data/yard-stubs/carray_element.rb +10 -4
  228. data/yard-stubs/carray_generate.rb +21 -1
  229. data/yard-stubs/carray_lazy.rb +5 -1
  230. data/yard-stubs/carray_loop.rb +4 -1
  231. data/yard-stubs/carray_mask.rb +60 -12
  232. data/yard-stubs/carray_math.rb +52 -14
  233. data/yard-stubs/carray_median_percentile.rb +15 -8
  234. data/yard-stubs/carray_memory_view.rb +40 -23
  235. data/yard-stubs/carray_order.rb +61 -29
  236. data/yard-stubs/carray_random.rb +13 -4
  237. data/yard-stubs/carray_scatter.rb +21 -9
  238. data/yard-stubs/carray_segment.rb +72 -0
  239. data/yard-stubs/carray_slab.rb +10 -2
  240. data/yard-stubs/carray_sort.rb +49 -8
  241. data/yard-stubs/carray_test.rb +15 -3
  242. data/yard-stubs/carray_utils.rb +0 -26
  243. metadata +19 -5
  244. data/ext/ca_for_each_element.h +0 -239
  245. data/ext/carray_kernels_map.c +0 -3467
  246. data/lib/carray/arrow_tensor.rb +0 -401
  247. data/lib/carray/table.rb +0 -108
@@ -0,0 +1,117 @@
1
+ # CAFrame windows bounded by a width of a key column.
2
+
3
+ class CAFrame
4
+ # Moving windows bounded by a width of the key column +on+ (a time, a
5
+ # duration or a number) instead of a count of rows: the window of a row
6
+ # whose key is t holds the rows whose key lies between t - before and
7
+ # t + after. +exclude:+ leaves out the lower end (default), the upper end,
8
+ # both or neither, as CArray#is_between does. +by:+ names columns that
9
+ # must match exactly, so each window stays within one station; +interval:+
10
+ # gives the key's grid step for WindowedFrame#is_complete. The rows need
11
+ # not be sorted; the answers come in row order. A row whose key is
12
+ # masked, NaN or infinite is in no window and has no window.
13
+ #
14
+ # w = df.windows(on: "time", before: "1 hour", by: "station")
15
+ # w.mean("temp") # => CArray, one value per row
16
+ # w.mean # => frame of the windowed mean of every column
17
+ # w.elements # => the number of rows in each window
18
+ #
19
+ # Given +at:+, the windows sit at those points of the key instead of at
20
+ # the rows, and every answer is one per point: a frame of the reductions
21
+ # has a row per point, the points as its index and the on: column's name
22
+ # as its row axis name. The points need not be sorted or on the rows'
23
+ # keys. A grid of points gives windows on a grid; a window with no row
24
+ # answers as an empty reduction does (+mean+ UNDEF, +count+ 0). +at:+
25
+ # cannot be given with +by:+.
26
+ #
27
+ # grid = CArray.time_range(t0, t1, unit: "1 hour")
28
+ # df.windows(on: "time", at: grid, after: "3 hours", exclude: :upper).mean
29
+ # # => a frame with a row per hour, the mean over [t, t + 3 hours)
30
+ #
31
+ # @return [WindowedFrame]
32
+ def windows (on:, before: nil, after: nil, exclude: :lower, by: nil, interval: nil, at: nil)
33
+ on_name = column_key(on, "windows")
34
+ key = key_column(on_name, "windows")
35
+ by_names = by.nil? ? [] : column_keys(by.is_a?(Array) ? by : [by], "windows")
36
+ by_cols = by_names.map { |b| key_column(b, "windows") }
37
+ # The points are taken as they are now: a later write to the array
38
+ # given changes neither the windows nor a result's index.
39
+ at = at.copy if at.is_a?(CArray)
40
+ plan = CAKeyWindowIterator.new(CArray.int64(nrow).seq, on: key,
41
+ before: before, after: after, exclude: exclude,
42
+ by: by_cols.empty? ? nil : by_cols,
43
+ interval: interval, at: at)
44
+ WindowedFrame.new(self, plan, [on_name, *by_names], at: at)
45
+ end
46
+ end
47
+
48
+ # The windows of a CAFrame (CAFrame#windows): the reductions over each row's
49
+ # window, one answer per row, and what each window covered.
50
+ class WindowedFrame
51
+ include CAFrame::ColumnVerbs
52
+
53
+ def initialize (frame, plan, key_names, at: nil)
54
+ @frame = frame
55
+ @plan = plan
56
+ @key_names = key_names
57
+ @at = at
58
+ end
59
+
60
+ # @overload [](name)
61
+ # Returns the CAKeyWindowIterator of one column.
62
+ # @return [CAKeyWindowIterator]
63
+ def [] (name)
64
+ @plan.for_value(@frame[@frame.__send__(:column_key, name, "windows")])
65
+ end
66
+
67
+ # What each window covered, one value per row: see CAKeyWindowIterator.
68
+ [:first_addr, :last_addr, :first_key, :last_key, :elements, :is_complete].each do |m|
69
+ define_method(m, &Ractor.make_shareable(proc { @plan.public_send(m) }))
70
+ end
71
+
72
+ # @overload bounds
73
+ # Returns the range each window was asked to cover, as two arrays of the
74
+ # key's kind.
75
+ # @return [Array(CArray, CArray)]
76
+ def bounds
77
+ @plan.bounds
78
+ end
79
+
80
+ # The windowed reductions. Without names, a frame of every column whose
81
+ # type has the reduction (the on: and by: columns left out), on the
82
+ # frame's rows and index; given names, the CArray (one) or the Array of
83
+ # them (several), as +df[...]+ returns columns.
84
+ CArray.send(:define_named_methods, self,
85
+ [:sum, :mean, :min, :max, :prod, :accumulate, :median,
86
+ :variance, :stddev, :variancep, :stddevp], :window_reduction)
87
+
88
+ CArray.send(:define_named_methods, self,
89
+ [:first, :last, :nunique, :count, :all, :any], :window_value)
90
+
91
+ private
92
+
93
+ def window_reduction (reduction, *names, min_count: nil, fill_value: nil)
94
+ window_reduce(reduction, names, min_count: min_count, fill_value: fill_value)
95
+ end
96
+
97
+ def window_value (reduction, *names)
98
+ window_reduce(reduction, names)
99
+ end
100
+
101
+ # An N-D column (each row a profile) is reduced along the rows, each
102
+ # window keeping the trailing shape, as a group reduces it.
103
+ def window_reduce (reduction, names, **keywords)
104
+ keywords = keywords.compact
105
+ out = over_named_columns(reduction, names, @key_names) do |col|
106
+ @plan.for_value(col).public_send(reduction, **keywords)
107
+ end
108
+ return out unless names.empty?
109
+ if @at
110
+ # Each result has an index of its own, as a group's result has.
111
+ key = @frame.__send__(:key_column, @key_names.first, "windows")
112
+ CAFrame.new(out, axis_name: @key_names.first, index: CAFrame.__send__(:key_labels, key, @at.copy))
113
+ else
114
+ CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
115
+ end
116
+ end
117
+ end
data/lib/carray/frame.rb CHANGED
@@ -6,11 +6,15 @@
6
6
  # devel/MEMO_DATAFRAME_ON_CARRAY.md for the design.
7
7
 
8
8
  require 'carray/frame/frame'
9
+ require 'carray/frame/column_verbs'
9
10
  require 'carray/frame/verbs'
10
11
  require 'carray/frame/convert'
11
12
  require 'carray/frame/io'
12
13
  require 'carray/frame/records'
13
14
  require 'carray/frame/group'
14
15
  require 'carray/frame/join'
16
+ require 'carray/frame/windows'
15
17
  require 'carray/frame/concat'
16
18
  require 'carray/frame/sort'
19
+ require 'carray/frame/reshape'
20
+ require 'carray/frame/describe'
@@ -11,6 +11,12 @@
11
11
  # shadows back -- `fuse(a, b) { |x, y| ... }` -- which names each of them
12
12
  # twice. Julia writes `@.` for the same reason and does the same thing to
13
13
  # the expression underneath.
14
+ #
15
+ # Reading and rewriting a block gives the same text every time it is
16
+ # called, so it is done once per block -- per place in the source -- and
17
+ # kept. What is kept is a lambda that takes the block's free locals as
18
+ # arguments and is run with the block's `self`; a call only reads the
19
+ # locals.
14
20
  # ---------------------------------------------------------------------------
15
21
 
16
22
  require "prism"
@@ -26,13 +32,117 @@ class CArray
26
32
  end
27
33
 
28
34
  def self.evaluate (block)
29
- result = eval(rewrite(body_source(block)), block.binding,
30
- *block.source_location)
35
+ result = compiled(block).call(block)
31
36
  # An expression that is just an array is that array; the shadow put
32
37
  # around it has nothing to fuse.
33
38
  result.is_a?(CALazyMarker) ? result.parent : result
34
39
  end
35
40
 
41
+ # -- one compiled form per block --------------------------------------
42
+
43
+ # Keyed by the block's instruction sequence, which is the same object
44
+ # every time the same block is called and a different one for each
45
+ # block, even two on one line. Where Ruby has weak keys, a block whose
46
+ # code is unloaded takes its entry with it.
47
+ def self.new_sites
48
+ defined?(ObjectSpace::WeakKeyMap) ? ObjectSpace::WeakKeyMap.new
49
+ : {}.compare_by_identity
50
+ end
51
+ SITES = new_sites
52
+
53
+ # A Ractor other than the main one keeps its own table, and evaluates
54
+ # every block in its frame: the empty scope a lambda is compiled in is
55
+ # the top of a file, which only the main Ractor has.
56
+ def self.compiled (block)
57
+ site = RubyVM::InstructionSequence.of(block)
58
+ return compile(block) unless site
59
+ sites = Ractor.current == Ractor.main ? SITES : (Ractor.current[:carray_fuse_sites] ||= new_sites)
60
+ sites[site] || (sites[site] = compile(block))
61
+ end
62
+
63
+ # The block's free locals, handed to a lambda that was compiled without
64
+ # them. `self` is the block's own, so instance variables and methods
65
+ # are what they were.
66
+ Compiled = Struct.new(:locals, :lambda) do
67
+ def call (block)
68
+ scope = block.binding
69
+ values = locals.map { |name| scope.local_variable_get(name) }
70
+ scope.receiver.instance_exec(*values, &lambda)
71
+ end
72
+ end
73
+
74
+ # A block that needs its own frame -- it assigns to a local outside it,
75
+ # or yields, or asks for its binding -- is evaluated there every time;
76
+ # only the rewrite is kept.
77
+ InFrame = Struct.new(:text, :file, :line) do
78
+ def call (block)
79
+ eval(text, block.binding, file, line)
80
+ end
81
+ end
82
+
83
+ # Calls that mean something only in the frame they are made from.
84
+ FRAME_BOUND = %i[binding local_variables block_given? __method__ __dir__
85
+ eval].freeze
86
+
87
+ FRAME_BOUND_NODES = [Prism::YieldNode, Prism::SuperNode,
88
+ Prism::ForwardingSuperNode, Prism::ReturnNode,
89
+ Prism::BreakNode, Prism::NextNode,
90
+ Prism::RedoNode].freeze
91
+
92
+ def self.compile (block)
93
+ body = body_source(block)
94
+ text = rewrite(body)
95
+ file, line = block.source_location
96
+ scope = block.binding
97
+ locals, needs_frame = free_locals(body, scope.local_variables)
98
+ return InFrame.new(text, file, line) if needs_frame || Ractor.current != Ractor.main
99
+ Compiled.new(locals, compile_lambda(text, locals, scope, file, line))
100
+ end
101
+
102
+ # Read on its own, a body names a local from outside as a call with no
103
+ # receiver and no arguments, so every one of them is found this way. A
104
+ # body that assigns to one has to keep its frame.
105
+ def self.free_locals (body, outer)
106
+ reads = []
107
+ needs_frame = false
108
+ Prism.parse(body).value.breadth_first_search do |node|
109
+ case node
110
+ when Prism::CallNode
111
+ reads << node.name if node.variable_call? && outer.include?(node.name)
112
+ needs_frame = true if node.receiver.nil? && FRAME_BOUND.include?(node.name)
113
+ when *FRAME_BOUND_NODES
114
+ needs_frame = true
115
+ else
116
+ if node.class.name.match?(/\APrism::LocalVariable\w*(Write|Target)Node\z/) &&
117
+ outer.include?(node.name)
118
+ needs_frame = true
119
+ end
120
+ end
121
+ false
122
+ end
123
+ [reads.uniq, needs_frame]
124
+ end
125
+
126
+ # Constants resolve as they do where the block was written: each module
127
+ # it sits in is reopened around the lambda, outermost first. The
128
+ # refinements active there are activated again. None of it sees the
129
+ # frame of the call that compiles it.
130
+ def self.compile_lambda (text, locals, scope, file, line)
131
+ nesting = scope.eval("Module.nesting").reverse
132
+ refinements = scope.eval("Module.used_modules")
133
+ Thread.current[:__carray_fuse_scope__] = [nesting, refinements]
134
+ code = refinements.each_index.map { |i|
135
+ "using Thread.current[:__carray_fuse_scope__][1][#{i}];"
136
+ }.join + "lambda { |#{locals.join(", ")}| #{text}\n}"
137
+ (nesting.size - 1).downto(0) do |i|
138
+ code = "Thread.current[:__carray_fuse_scope__][0][#{i}]" \
139
+ ".module_eval(#{code.dump}, #{file.dump}, #{line})"
140
+ end
141
+ EMPTY_SCOPE.call.eval(code, file, line)
142
+ ensure
143
+ Thread.current[:__carray_fuse_scope__] = nil
144
+ end
145
+
36
146
  # -- the block's own text ---------------------------------------------
37
147
 
38
148
  def self.body_source (block)
@@ -121,3 +231,9 @@ class CArray
121
231
  end
122
232
  end
123
233
  end
234
+
235
+ # A scope with no locals and no refinements in it, for compiling a block's
236
+ # expression where it can neither see nor hold on to the frame of the call
237
+ # that happens to compile it. Both are true only of the top of a file.
238
+ CArray::FuseSource::EMPTY_SCOPE = -> { binding }
239
+ CArray::FuseSource.private_constant :EMPTY_SCOPE
data/lib/carray/fusion.rb CHANGED
@@ -2,7 +2,9 @@
2
2
  # Turning a lazy expression into a plan a compiler can read.
3
3
  #
4
4
  # A lazy view is already a typed, closed expression graph, and the kernels
5
- # already carry the C that computes each operation (CArray.__kernel_body__).
5
+ # already carry the C that computes each operation (CArray.__kernel_body__,
6
+ # with CArray.__kernel_helpers__ for the functions a body calls beyond the
7
+ # C standard library).
6
8
  # What is missing between them is the reading: which operation each node is,
7
9
  # what its mask does, and where the leaves are. That is what a plan holds.
8
10
  #
@@ -17,16 +19,54 @@ class CArray
17
19
 
18
20
  # One value per node, in evaluation order; the last is the result.
19
21
  #
20
- # Op an operation, reading the nodes named in `args`
21
- # Leaf an array, the `index`-th of plan.leaves
22
- # Const a scalar written into the expression
23
- Op = Struct.new(:kind, :name, :data_type, :args, :body, :mask, :trapping)
24
- Leaf = Struct.new(:index, :data_type, :masked)
25
- Const = Struct.new(:value, :data_type)
22
+ # Op an operation, reading the nodes named in `args`
23
+ # Leaf an array, the `index`-th of plan.leaves, read at the cell
24
+ # being computed
25
+ # Shifted the same, read `offset` cells away along each axis -- what
26
+ # CArray#shift makes. A cell that falls outside the array is
27
+ # `fill` where `bounds` says :fill and masked where it says
28
+ # :mask. `masked` says whether the array itself has a mask.
29
+ # Const a scalar written into the expression
30
+ #
31
+ # A Shifted node reads cells other than the one being computed, so an
32
+ # evaluator that writes its output over one of the plan's leaves must
33
+ # not compute one that is read shifted: a cell it reads may already
34
+ # have been written.
35
+ #
36
+ # A comparison's body is the one for the data type it compares; what it
37
+ # answers is boolean, which is the node's data_type.
38
+ # An Op's fields:
39
+ #
40
+ # kind :monop, :binop, :triop, :moncmp or :bincmp
41
+ # name the operation as the kernels name it (:add, :sqrt, :ipow)
42
+ # args indices of the nodes it reads, in operand order
43
+ # body its C, from CArray.__kernel_body__: `#1`, `#2`, ... are
44
+ # the operands and the next number is the result; `<type>`
45
+ # is CArray's name for the type (float64_t)
46
+ # mask how the result is masked: :pass (as its one operand),
47
+ # :union (where any operand is), :kleene_or / :kleene_and
48
+ # (boolean | and &, unmasked where the known side settles it),
49
+ # :select (where the first operand is, else where the
50
+ # operand it chooses is: the second where it is true, the
51
+ # third where it is false)
52
+ # trapping true where a masked cell must not be computed at all
53
+ #
54
+ # What a reader may rely on: the node classes, the kinds, the names, the
55
+ # mask rules and the fields above keep the meaning they have here. New
56
+ # ones are added -- a node class, a kind, an operation, a mask rule --
57
+ # and an evaluator that meets one it does not know declines the plan
58
+ # (returns false). It does not raise: raising takes it out of service
59
+ # for the rest of the process. Declining is what lets CArray add to
60
+ # the plan without a new evaluator being released at the same time.
61
+ Op = Struct.new(:kind, :name, :data_type, :args, :body, :mask, :trapping)
62
+ Leaf = Struct.new(:index, :data_type, :masked)
63
+ Shifted = Struct.new(:index, :data_type, :masked, :offset, :bounds, :fill)
64
+ Const = Struct.new(:value, :data_type)
26
65
 
27
66
  Plan = Struct.new(:nodes, :leaves, :data_type, :dim, :masked, :signature)
28
67
 
29
68
  LAZY_CLASSES = [CAMonOp, CABinOp, CATriOp, CAMonCmp, CABinCmp, CALazyMarker]
69
+ Ractor.make_shareable(LAZY_CLASSES)
30
70
 
31
71
  # A lazy node names its operation by an id and the kernels name it by a
32
72
  # symbol. These are the same operations, spelled the way each side
@@ -37,17 +77,21 @@ class CArray
37
77
  :& => :bit_and_i, :| => :bit_or_i, :^ => :bit_xor_i,
38
78
  :<< => :bit_lshift, :>> => :bit_rshift,
39
79
  }.freeze
40
- TRIOP_NAMES = { :__clip_ki__ => :clip }.freeze
80
+ TRIOP_NAMES = { :clamp_between => :clip }.freeze
41
81
 
42
82
  MONOP_BY_ID = CArray::LAZY_MONOP_OP_IDS.invert.freeze
43
- BINOP_BY_ID = CArray::LAZY_BINOP_OP_IDS.invert.freeze
44
- TRIOP_BY_ID = CArray::LAZY_TRIOP_OP_IDS.invert.freeze
45
-
46
- # Integer division and its relatives raise on a zero divisor, so a cell
47
- # the mask excludes must not be computed at all -- the divisor there is
48
- # nobody's business (ca_binop_dispatch.c).
49
- TRAPPING = %i[div mod quo_i fmod].freeze
50
- INTEGERS = %i[int8 int16 int32 int64 uint8 uint16 uint32 uint64].freeze
83
+ # A Float or Complex array to an Integer power is its own node, ipow,
84
+ # which `**` makes and no table above names.
85
+ BINOP_BY_ID = CArray::LAZY_BINOP_OP_IDS.invert
86
+ .merge(CABinOp::OP_IPOWER => :ipow).freeze
87
+ # The lazy then_else is its own triop, select, which no method names.
88
+ TRIOP_BY_ID = CArray::LAZY_TRIOP_OP_IDS.invert
89
+ .merge(CATriOp::OP_SELECT => :select).freeze
90
+ MONCMP_BY_ID = CArray::LAZY_MONCMP_OP_IDS.invert.freeze
91
+ # The table holds both spellings of each comparison (`lt` and `<`); the
92
+ # kernels are named by the word.
93
+ BINCMP_BY_ID = CArray::LAZY_BINCMP_OP_IDS
94
+ .select { |name, _| name.to_s.match?(/\A[a-z]/) }.invert.freeze
51
95
 
52
96
  class Refused < StandardError; end
53
97
 
@@ -74,29 +118,100 @@ class CArray
74
118
 
75
119
  # Returns the array, or nil where nothing computed it.
76
120
  def self.evaluate (view)
77
- return nil unless askable?(view)
78
- out = CArray.__alloc_uninit__(view.data_type, view.dim)
79
- evaluate_into(view, out) ? out : nil
121
+ plan = worth_asking(view) or return nil
122
+ out = CArray.__alloc_uninit__(view.data_type, view.shape)
123
+ ask(plan, out) ? out : nil
80
124
  end
81
125
 
82
126
  # Fills an array the caller already has. Called from the store as well,
83
127
  # where making one and copying it over would be most of the work.
84
128
  # Returns true when something computed it.
85
129
  def self.evaluate_into (view, out)
130
+ plan = worth_asking(view) or return false
131
+ ask(plan, out)
132
+ end
133
+
134
+ # The plan to hand over, or nil. A marker over an array, or anything
135
+ # else with nothing to compute, is not worth handing over. Nothing here
136
+ # is the evaluator's doing, so nothing here retires it: an array that
137
+ # raises when it is looked at (a CAObject whose backing is gone) raises
138
+ # again in the walk, where the caller sees it.
139
+ def self.worth_asking (view)
140
+ return nil unless askable?(view)
141
+ plan = plan(view) or return nil
142
+ plan.nodes.any? { |n| n.is_a?(Op) } ? plan : nil
143
+ rescue StandardError
144
+ nil
145
+ end
146
+
147
+ def self.ask (plan, out)
86
148
  evaluator = CArray.expression_evaluator or return false
87
- return false unless askable?(view)
88
- plan = plan(view) or return false
89
- # A marker over an array, or anything else with nothing to compute,
90
- # is not worth handing over.
91
- return false unless plan.nodes.any? { |n| n.is_a?(Op) }
92
- out.mask = 0 if plan.masked && ! out.has_mask?
93
- evaluator.call(plan, out) ? true : false
94
- rescue StandardError => error
149
+ return false if overlaps?(plan, out)
150
+ return false unless ready_mask(plan, out)
151
+ begin
152
+ evaluator.call(plan, out) ? true : false
153
+ rescue AddressBasis::Failure
154
+ # Raised by CArray lending the arrays or taking them back: a refusal,
155
+ # or an array that cannot be read. The walk meets the same array.
156
+ false
157
+ rescue StandardError => error
158
+ retire(error)
159
+ false
160
+ end
161
+ end
162
+
163
+ # The walk leaves the destination masked where the expression is, so a
164
+ # destination that arrives masked loses its mask to an expression that
165
+ # has none. A destination that cannot hold a mask (a value array) is
166
+ # left to the walk, which says so.
167
+ def self.ready_mask (plan, out)
168
+ if plan.masked
169
+ out.mask = 0 unless out.has_mask?
170
+ elsif out.has_mask?
171
+ out.mask = 0
172
+ end
173
+ true
174
+ rescue StandardError
175
+ false
176
+ end
177
+
178
+ # Whether the destination shares storage with a leaf it would read. An
179
+ # evaluator computes cell by cell, so writing a cell another leaf reads
180
+ # later -- `z[] = y + y` with z and y overlapping slices of one array --
181
+ # reads what it has just written. The walk reads every operand before
182
+ # it writes. The destination itself as a leaf is fine as long as it is
183
+ # read at the cell being written, not shifted.
184
+ def self.overlaps? (plan, out)
185
+ mine = storages(out)
186
+ shifted = plan.nodes.grep(Shifted).map(&:index)
187
+ plan.leaves.each_with_index.any? do |leaf, index|
188
+ if leaf.equal?(out)
189
+ shifted.include?(index)
190
+ else
191
+ storages(leaf).any? { |r| mine.any? { |m| m.send(:storage_overlaps?, r) } }
192
+ end
193
+ end
194
+ end
195
+
196
+ # The arrays that hold an array's cells: its root, or each parent's
197
+ # where a view stands over several. Two of them share storage when
198
+ # they are one array, or entities whose memory overlaps (an array and a
199
+ # wrap_memory_view of it).
200
+ def self.storages (array, found = [])
201
+ root = array.root_array
202
+ if ! root.entity? && root.respond_to?(:parents)
203
+ root.parents.each { |parent| storages(parent, found) }
204
+ else
205
+ found << root
206
+ end
207
+ found
208
+ end
209
+
210
+ def self.retire (error)
95
211
  CArray.expression_evaluator = nil
96
212
  warn "CArray: the registered expression evaluator raised " \
97
213
  "(#{error.class}: #{error.message}); expressions will be walked " \
98
214
  "from here on"
99
- false
100
215
  end
101
216
 
102
217
  def self.askable? (view)
@@ -115,8 +230,9 @@ class CArray
115
230
  raise Refused, "not a lazy expression" unless lazy?(view)
116
231
  w = Walk.new
117
232
  w.visit(view)
118
- Plan.new(w.nodes, w.leaves, view.data_type, view.dim,
119
- w.leaves.any? { |a| a.has_mask? }, w.signature)
233
+ masked = w.leaves.any? { |a| a.has_mask? } ||
234
+ w.nodes.any? { |n| n.is_a?(Shifted) && n.bounds.include?(:mask) }
235
+ Plan.new(w.nodes, w.leaves, view.data_type, view.shape, masked, w.signature)
120
236
  end
121
237
 
122
238
  def self.lazy? (x)
@@ -131,6 +247,7 @@ class CArray
131
247
  def initialize
132
248
  @nodes = []
133
249
  @leaves = []
250
+ @leaf_index = {}
134
251
  @seen = {}
135
252
  @signature = +""
136
253
  end
@@ -147,6 +264,9 @@ class CArray
147
264
  when CAMonOp then unary(n)
148
265
  when CABinOp then binary(n)
149
266
  when CATriOp then ternary(n)
267
+ when CAMonCmp then unary_comparison(n)
268
+ when CABinCmp then binary_comparison(n)
269
+ when CAShift then shifted(n)
150
270
  when CScalar then constant(n)
151
271
  when CArray then leaf(n)
152
272
  else raise Refused, "#{n.class} in an expression"
@@ -154,11 +274,24 @@ class CArray
154
274
  end
155
275
 
156
276
  def unary (n)
277
+ return conversion(n) if n.__op_id__ >= CAMonOp::CAST_BASE
157
278
  name = spell(MONOP_BY_ID, n.__op_id__, {})
158
279
  args = [visit(n.parent)]
159
280
  # A view over one array is masked exactly where that array is
160
281
  # (ca_obj_monop.c).
161
- op(:monop, name, n.data_type, args, :pass)
282
+ op(:monop, name, n.data_type, args, :pass, n.__trapping__)
283
+ end
284
+
285
+ # A cast node is named by the type it converts to, cast_<data_type>,
286
+ # and its body is keyed by the type it converts from.
287
+ def conversion (n)
288
+ from = n.parent.data_type
289
+ name = :"cast_#{n.data_type}"
290
+ body = CArray.__kernel_body__(:monop, name, from) or
291
+ raise Refused, "monop #{name} has no body at #{from}"
292
+ args = [visit(n.parent)]
293
+ note("c", name, from, args.join(","))
294
+ push Op.new(:monop, name, n.data_type, args, body, :pass, n.__trapping__)
162
295
  end
163
296
 
164
297
  def binary (n)
@@ -170,32 +303,112 @@ class CArray
170
303
  elsif n.data_type == :boolean && name == :bit_and_i then :kleene_and
171
304
  else :union
172
305
  end
173
- op(:binop, name, n.data_type, args, rule)
306
+ op(:binop, name, n.data_type, args, rule, n.__trapping__)
174
307
  end
175
308
 
176
309
  def ternary (n)
177
310
  name = spell(TRIOP_BY_ID, n.__op_id__, TRIOP_NAMES)
178
311
  args = [visit(n.parent), visit(n.__triop_op2__), visit(n.__triop_op3__)]
179
- op(:triop, name, n.data_type, args, :union)
312
+ # select is masked where its condition is, or where the branch the
313
+ # condition chooses is (ca_obj_triop.c).
314
+ rule = name == :select ? :select : :union
315
+ op(:triop, name, n.data_type, args, rule, n.__trapping__)
316
+ end
317
+
318
+ # A comparison is masked where its operands are (ca_obj_moncmp.c,
319
+ # ca_obj_bincmp.c): there is no three-valued comparison.
320
+ def unary_comparison (n)
321
+ name = spell(MONCMP_BY_ID, n.__op_id__, {})
322
+ operand = n.parent
323
+ args = [visit(operand)]
324
+ comparison(:moncmp, name, operand.data_type, args, :pass, n.__trapping__)
325
+ end
326
+
327
+ def binary_comparison (n)
328
+ name = spell(BINCMP_BY_ID, n.__op_id__, {})
329
+ right = n.__bincmp_right__
330
+ # Both sides are already of the type compared (CABinCmp casts one
331
+ # to the other); a pair that is not has no single body.
332
+ unless n.parent.data_type == right.data_type
333
+ raise Refused, "#{name} between #{n.parent.data_type} and #{right.data_type}"
334
+ end
335
+ args = [visit(n.parent), visit(right)]
336
+ comparison(:bincmp, name, n.parent.data_type, args, :union, n.__trapping__)
180
337
  end
181
338
 
182
- def op (kind, name, type, args, mask)
339
+ # `trapping`: the node skips the cells its operands mask rather than
340
+ # computing them -- one that can raise on a cell or calls Ruby for it.
341
+ # Each lazy view answers it from the rule its own kernel follows.
342
+ def comparison (kind, name, compared, args, mask, trapping)
343
+ body = CArray.__kernel_body__(kind, name, compared) or
344
+ raise Refused, "#{kind} #{name} has no body at #{compared}"
345
+ # A body written against its compared type or a tolerance (`feq`)
346
+ # needs more than its operands substituted.
347
+ if body.match?(/<\w+>|\btol\b/)
348
+ raise Refused, "#{kind} #{name} needs more than its operands"
349
+ end
350
+ note(kind.to_s[0..3], name, compared, args.join(","))
351
+ push Op.new(kind, name, :boolean, args, body, mask, trapping)
352
+ end
353
+
354
+ def op (kind, name, type, args, mask, trapping)
183
355
  body = CArray.__kernel_body__(kind, name, type) or
184
356
  raise Refused, "#{kind} #{name} has no body at #{type}"
185
- note(kind.to_s[0], name, type)
186
- push Op.new(kind, name, type, args, body, mask,
187
- TRAPPING.include?(name) && INTEGERS.include?(type))
357
+ note(kind.to_s[0], name, type, args.join(","))
358
+ push Op.new(kind, name, type, args, body, mask, trapping)
188
359
  end
189
360
 
190
361
  def leaf (n)
191
- note("a", n.data_type, n.has_mask? ? 1 : 0)
192
- @leaves << n
193
- push Leaf.new(@leaves.size - 1, n.data_type, n.has_mask?)
362
+ index = leaf_index(n)
363
+ note("a", index, n.data_type, n.has_mask? ? 1 : 0)
364
+ push Leaf.new(index, n.data_type, n.has_mask?)
365
+ end
366
+
367
+ # A shift of an array is that array read elsewhere, so it is the
368
+ # array that becomes a leaf. A shift of anything else -- an
369
+ # expression, say -- has no array to read and is taken as a whole.
370
+ def shifted (n)
371
+ array = n.parent
372
+ bounds = n.__boundary_rule_of_each_axis__
373
+ if Fusion.lazy?(array) || array.shape != n.shape ||
374
+ ! bounds.all? { |b| b == :fill || b == :mask }
375
+ return leaf(n)
376
+ end
377
+ offset = n.start
378
+ # The fill as a cell of the array holds it: a boolean array is
379
+ # filled with true or false, not with the Integer it was given.
380
+ fill = bounds.include?(:fill) ? as_cell(n.fill_value, array.data_type) : nil
381
+ index = leaf_index(array)
382
+ note("s", index, array.data_type, array.has_mask? ? 1 : 0,
383
+ offset.join(","), bounds.join(","), fill.inspect)
384
+ push Shifted.new(index, array.data_type, array.has_mask?,
385
+ offset, bounds, fill)
194
386
  end
195
387
 
388
+ # A leaf the address bases would refuse to lend cannot be read by an
389
+ # evaluator, so the expression is not described.
390
+ def leaf_index (array)
391
+ @leaf_index.fetch(array.object_id) do
392
+ if (reason = AddressBasis.classify(array)[:refusal])
393
+ raise Refused, "a leaf: #{reason}"
394
+ end
395
+ @leaves << array
396
+ @leaf_index[array.object_id] = @leaves.size - 1
397
+ end
398
+ end
399
+
400
+ # A masked scalar has no value to write into the expression.
196
401
  def constant (n)
197
- note("k", n[0], n.data_type)
198
- push Const.new(n[0], n.data_type)
402
+ raise Refused, "a masked scalar" if n.has_mask? && n.is_masked[0]
403
+ value = n[0]
404
+ note("k", value.inspect, n.data_type)
405
+ push Const.new(value, n.data_type)
406
+ end
407
+
408
+ def as_cell (value, data_type)
409
+ cell = CScalar.new(data_type)
410
+ cell[0] = value
411
+ cell[0]
199
412
  end
200
413
 
201
414
  def push (node)
@@ -210,6 +423,8 @@ class CArray
210
423
 
211
424
  # Two expressions of the same shape compute alike, whatever arrays
212
425
  # they are over, so a consumer can keep one compiled kernel for both.
426
+ # The shape includes which node each operation reads and which leaf
427
+ # each read is: `(a - b) - b` and `(a - b) - a` differ only there.
213
428
  def note (*parts)
214
429
  @signature << parts.join(":") << ";"
215
430
  end