carray 3.0.2 → 3.0.3

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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +3 -4
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +225 -0
  6. data/ext/ca_axis_group.c +177 -99
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +66 -71
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +663 -214
  14. data/ext/ca_kernel_iterator.h +231 -167
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +132 -22
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +1 -0
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +96 -25
  25. data/ext/ca_obj_face.c +406 -64
  26. data/ext/ca_obj_face.h +92 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +21 -21
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +189 -107
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +543 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +47 -49
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -118
  46. data/ext/ca_obj_stride.c +195 -34
  47. data/ext/ca_obj_string.c +17 -18
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +149 -27
  54. data/ext/ca_op_ipower.c +24 -11
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_sort_kernels.h +70 -0
  58. data/ext/ca_sweep_engine.c +102 -79
  59. data/ext/ca_sweep_engine.h +7 -20
  60. data/ext/ca_triop_dispatch.c +4 -1
  61. data/ext/ca_triop_dispatch.h +5 -2
  62. data/ext/caframe_csv_reader.c +463 -0
  63. data/ext/carray.h +70 -27
  64. data/ext/carray_access.c +455 -211
  65. data/ext/carray_address_basis.c +177 -36
  66. data/ext/carray_attribute.c +45 -3
  67. data/ext/carray_bincount.c +32 -2
  68. data/ext/carray_broadcast.c +46 -14
  69. data/ext/carray_call_cfunc.c +0 -28
  70. data/ext/carray_cast.c +1444 -209
  71. data/ext/carray_cast_func.rb +34 -21
  72. data/ext/carray_conversion.c +197 -73
  73. data/ext/carray_copy.c +32 -11
  74. data/ext/carray_core.c +514 -122
  75. data/ext/carray_count.c +140 -87
  76. data/ext/carray_element.c +18 -27
  77. data/ext/carray_factorize.c +569 -277
  78. data/ext/carray_generate.c +76 -40
  79. data/ext/carray_histogram.c +152 -94
  80. data/ext/carray_hold.c +2 -1
  81. data/ext/carray_index_classifier.c +63 -96
  82. data/ext/carray_internal.h +385 -3
  83. data/ext/carray_lazy.c +834 -79
  84. data/ext/carray_loop.c +81 -65
  85. data/ext/carray_mask.c +682 -316
  86. data/ext/carray_math_kernel.h +1 -0
  87. data/ext/carray_median_percentile.c +221 -76
  88. data/ext/carray_memory_view.c +254 -248
  89. data/ext/carray_operator.c +1089 -359
  90. data/ext/carray_order.c +133 -69
  91. data/ext/carray_partition.c +102 -56
  92. data/ext/carray_random.c +273 -239
  93. data/ext/carray_scatter.c +308 -65
  94. data/ext/carray_segment.c +257 -0
  95. data/ext/carray_slab.c +170 -70
  96. data/ext/carray_sort.c +215 -218
  97. data/ext/carray_sort_kernel.c +8 -1
  98. data/ext/carray_struct.c +99 -21
  99. data/ext/carray_test.c +167 -64
  100. data/ext/carray_time_text.c +1182 -0
  101. data/ext/carray_utils.c +363 -98
  102. data/ext/extconf.rb +27 -2
  103. data/ext/mk_call_cfunc.rb +0 -2
  104. data/ext/mk_pow5_table.rb +35 -0
  105. data/ext/mkkernel.rb +1076 -846
  106. data/ext/ruby_carray.c +7 -1
  107. data/ext/version.h +4 -4
  108. data/ext/version.rb +4 -2
  109. data/lib/carray/attribute.rb +39 -0
  110. data/lib/carray/autoload_carray.rb +19 -0
  111. data/lib/carray/autoload_method_extension.rb +6 -0
  112. data/lib/carray/axis_group.rb +174 -26
  113. data/lib/carray/basics.rb +91 -25
  114. data/lib/carray/bincount_nd.rb +6 -0
  115. data/lib/carray/block_iterator.rb +118 -48
  116. data/lib/carray/boolean_reduce.rb +21 -29
  117. data/lib/carray/categorical.rb +94 -26
  118. data/lib/carray/categorical_iterator.rb +333 -826
  119. data/lib/carray/complex.rb +36 -29
  120. data/lib/carray/conditional.rb +94 -7
  121. data/lib/carray/const_string.rb +46 -0
  122. data/lib/carray/frame/concat.rb +157 -6
  123. data/lib/carray/frame/convert.rb +13 -7
  124. data/lib/carray/frame/csv_parser.rb +589 -82
  125. data/lib/carray/frame/describe.rb +79 -0
  126. data/lib/carray/frame/frame.rb +124 -25
  127. data/lib/carray/frame/group.rb +189 -63
  128. data/lib/carray/frame/io.rb +412 -72
  129. data/lib/carray/frame/join.rb +134 -46
  130. data/lib/carray/frame/records.rb +37 -16
  131. data/lib/carray/frame/reshape.rb +448 -0
  132. data/lib/carray/frame/sort.rb +26 -2
  133. data/lib/carray/frame/verbs.rb +715 -48
  134. data/lib/carray/frame.rb +2 -0
  135. data/lib/carray/fuse_source.rb +111 -2
  136. data/lib/carray/fusion.rb +229 -38
  137. data/lib/carray/inspect.rb +29 -11
  138. data/lib/carray/iterator.rb +14 -2
  139. data/lib/carray/lazy.rb +54 -30
  140. data/lib/carray/mask_gap_fill.rb +66 -52
  141. data/lib/carray/math.rb +32 -19
  142. data/lib/carray/meld_reduce.rb +40 -7
  143. data/lib/carray/methods/align_addr.rb +8 -4
  144. data/lib/carray/methods/bincount.rb +2 -2
  145. data/lib/carray/methods/bit_string.rb +14 -3
  146. data/lib/carray/methods/composition.rb +16 -10
  147. data/lib/carray/methods/factorize.rb +2 -2
  148. data/lib/carray/methods/gather_nd.rb +13 -2
  149. data/lib/carray/methods/insert_block.rb +9 -14
  150. data/lib/carray/methods/is_in.rb +117 -4
  151. data/lib/carray/methods/join.rb +1 -1
  152. data/lib/carray/methods/locate_addr.rb +177 -36
  153. data/lib/carray/methods/mode.rb +18 -1
  154. data/lib/carray/methods/repeat.rb +1 -20
  155. data/lib/carray/methods/resize.rb +21 -23
  156. data/lib/carray/methods/snap.rb +34 -12
  157. data/lib/carray/methods/unique.rb +11 -2
  158. data/lib/carray/record_order.rb +208 -0
  159. data/lib/carray/runtime.rb +14 -11
  160. data/lib/carray/segment_iterator.rb +990 -0
  161. data/lib/carray/serialize.rb +197 -27
  162. data/lib/carray/slab_iterator.rb +51 -27
  163. data/lib/carray/stack.rb +10 -4
  164. data/lib/carray/string_operation_extension.rb +4 -3
  165. data/lib/carray/struct.rb +142 -51
  166. data/lib/carray/struct_builder.rb +110 -14
  167. data/lib/carray/table.rb +2 -2
  168. data/lib/carray/time.rb +517 -103
  169. data/lib/carray/window_iterator.rb +127 -72
  170. data/lib/carray.rb +2 -2
  171. data/yard-stubs/ca_obj_array.rb +4 -4
  172. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  173. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  174. data/yard-stubs/ca_obj_fake.rb +1 -1
  175. data/yard-stubs/ca_obj_field.rb +1 -1
  176. data/yard-stubs/ca_obj_refer.rb +3 -3
  177. data/yard-stubs/ca_obj_roll.rb +0 -1
  178. data/yard-stubs/ca_obj_stride.rb +10 -7
  179. data/yard-stubs/ca_obj_transpose.rb +1 -1
  180. data/yard-stubs/ca_obj_window.rb +1 -2
  181. data/yard-stubs/carray_access.rb +5 -4
  182. data/yard-stubs/carray_broadcast.rb +1 -1
  183. data/yard-stubs/carray_cast.rb +78 -43
  184. data/yard-stubs/carray_conversion.rb +13 -1
  185. data/yard-stubs/carray_copy.rb +8 -3
  186. data/yard-stubs/carray_core.rb +3 -3
  187. data/yard-stubs/carray_count.rb +9 -0
  188. data/yard-stubs/carray_lazy.rb +5 -1
  189. data/yard-stubs/carray_loop.rb +4 -1
  190. data/yard-stubs/carray_mask.rb +34 -10
  191. data/yard-stubs/carray_math.rb +25 -13
  192. data/yard-stubs/carray_median_percentile.rb +10 -6
  193. data/yard-stubs/carray_order.rb +34 -13
  194. data/yard-stubs/carray_scatter.rb +10 -3
  195. data/yard-stubs/carray_segment.rb +72 -0
  196. data/yard-stubs/carray_slab.rb +2 -1
  197. data/yard-stubs/carray_sort.rb +43 -3
  198. data/yard-stubs/carray_test.rb +10 -0
  199. data/yard-stubs/carray_utils.rb +0 -26
  200. metadata +11 -19
  201. data/ext/ca_for_each_element.h +0 -239
  202. data/ext/carray_build_flags.h +0 -3
  203. data/ext/carray_config.h +0 -28
  204. data/ext/carray_kernels_bincmp.c +0 -4446
  205. data/ext/carray_kernels_binop.c +0 -11001
  206. data/ext/carray_kernels_init.c +0 -1131
  207. data/ext/carray_kernels_map.c +0 -3467
  208. data/ext/carray_kernels_moncmp.c +0 -2097
  209. data/ext/carray_kernels_monop.c +0 -18313
  210. data/ext/carray_kernels_reduce_aggregate.c +0 -26005
  211. data/ext/carray_kernels_reduce_boolean.c +0 -330
  212. data/ext/carray_kernels_reduce_cumulative.c +0 -14862
  213. data/ext/carray_kernels_reduce_extreme.c +0 -17494
  214. data/ext/carray_kernels_reduce_variance.c +0 -3910
  215. data/ext/carray_kernels_scan.c +0 -3693
  216. data/ext/carray_kernels_search.c +0 -32218
  217. data/ext/carray_kernels_sort.c +0 -10626
  218. data/ext/carray_kernels_triop.c +0 -1392
data/lib/carray/frame.rb CHANGED
@@ -14,3 +14,5 @@ require 'carray/frame/group'
14
14
  require 'carray/frame/join'
15
15
  require 'carray/frame/concat'
16
16
  require 'carray/frame/sort'
17
+ require 'carray/frame/reshape'
18
+ 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,110 @@ 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
+ SITES = defined?(ObjectSpace::WeakKeyMap) ? ObjectSpace::WeakKeyMap.new
48
+ : {}.compare_by_identity
49
+
50
+ def self.compiled (block)
51
+ site = RubyVM::InstructionSequence.of(block)
52
+ return compile(block) unless site
53
+ SITES[site] || (SITES[site] = compile(block))
54
+ end
55
+
56
+ # The block's free locals, handed to a lambda that was compiled without
57
+ # them. `self` is the block's own, so instance variables and methods
58
+ # are what they were.
59
+ Compiled = Struct.new(:locals, :lambda) do
60
+ def call (block)
61
+ scope = block.binding
62
+ values = locals.map { |name| scope.local_variable_get(name) }
63
+ scope.receiver.instance_exec(*values, &lambda)
64
+ end
65
+ end
66
+
67
+ # A block that needs its own frame -- it assigns to a local outside it,
68
+ # or yields, or asks for its binding -- is evaluated there every time;
69
+ # only the rewrite is kept.
70
+ InFrame = Struct.new(:text, :file, :line) do
71
+ def call (block)
72
+ eval(text, block.binding, file, line)
73
+ end
74
+ end
75
+
76
+ # Calls that mean something only in the frame they are made from.
77
+ FRAME_BOUND = %i[binding local_variables block_given? __method__ __dir__
78
+ eval].freeze
79
+
80
+ FRAME_BOUND_NODES = [Prism::YieldNode, Prism::SuperNode,
81
+ Prism::ForwardingSuperNode, Prism::ReturnNode,
82
+ Prism::BreakNode, Prism::NextNode,
83
+ Prism::RedoNode].freeze
84
+
85
+ def self.compile (block)
86
+ body = body_source(block)
87
+ text = rewrite(body)
88
+ file, line = block.source_location
89
+ scope = block.binding
90
+ locals, needs_frame = free_locals(body, scope.local_variables)
91
+ return InFrame.new(text, file, line) if needs_frame
92
+ Compiled.new(locals, compile_lambda(text, locals, scope, file, line))
93
+ end
94
+
95
+ # Read on its own, a body names a local from outside as a call with no
96
+ # receiver and no arguments, so every one of them is found this way. A
97
+ # body that assigns to one has to keep its frame.
98
+ def self.free_locals (body, outer)
99
+ reads = []
100
+ needs_frame = false
101
+ Prism.parse(body).value.breadth_first_search do |node|
102
+ case node
103
+ when Prism::CallNode
104
+ reads << node.name if node.variable_call? && outer.include?(node.name)
105
+ needs_frame = true if node.receiver.nil? && FRAME_BOUND.include?(node.name)
106
+ when *FRAME_BOUND_NODES
107
+ needs_frame = true
108
+ else
109
+ if node.class.name.match?(/\APrism::LocalVariable\w*(Write|Target)Node\z/) &&
110
+ outer.include?(node.name)
111
+ needs_frame = true
112
+ end
113
+ end
114
+ false
115
+ end
116
+ [reads.uniq, needs_frame]
117
+ end
118
+
119
+ # Constants resolve as they do where the block was written: each module
120
+ # it sits in is reopened around the lambda, outermost first. The
121
+ # refinements active there are activated again. None of it sees the
122
+ # frame of the call that compiles it.
123
+ def self.compile_lambda (text, locals, scope, file, line)
124
+ nesting = scope.eval("Module.nesting").reverse
125
+ refinements = scope.eval("Module.used_modules")
126
+ Thread.current[:__carray_fuse_scope__] = [nesting, refinements]
127
+ code = refinements.each_index.map { |i|
128
+ "using Thread.current[:__carray_fuse_scope__][1][#{i}];"
129
+ }.join + "lambda { |#{locals.join(", ")}| #{text}\n}"
130
+ (nesting.size - 1).downto(0) do |i|
131
+ code = "Thread.current[:__carray_fuse_scope__][0][#{i}]" \
132
+ ".module_eval(#{code.dump}, #{file.dump}, #{line})"
133
+ end
134
+ EMPTY_SCOPE.call.eval(code, file, line)
135
+ ensure
136
+ Thread.current[:__carray_fuse_scope__] = nil
137
+ end
138
+
36
139
  # -- the block's own text ---------------------------------------------
37
140
 
38
141
  def self.body_source (block)
@@ -121,3 +224,9 @@ class CArray
121
224
  end
122
225
  end
123
226
  end
227
+
228
+ # A scope with no locals and no refinements in it, for compiling a block's
229
+ # expression where it can neither see nor hold on to the frame of the call
230
+ # that happens to compile it. Both are true only of the top of a file.
231
+ CArray::FuseSource::EMPTY_SCOPE = -> { binding }
232
+ 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,12 +19,49 @@ 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
 
@@ -40,14 +79,18 @@ class CArray
40
79
  TRIOP_NAMES = { :__clip_ki__ => :clip }.freeze
41
80
 
42
81
  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
82
+ # A Float or Complex array to an Integer power is its own node, ipow,
83
+ # which `**` makes and no table above names.
84
+ BINOP_BY_ID = CArray::LAZY_BINOP_OP_IDS.invert
85
+ .merge(CABinOp::OP_IPOWER => :ipow).freeze
86
+ # The lazy then_else is its own triop, select, which no method names.
87
+ TRIOP_BY_ID = CArray::LAZY_TRIOP_OP_IDS.invert
88
+ .merge(CATriOp::OP_SELECT => :select).freeze
89
+ MONCMP_BY_ID = CArray::LAZY_MONCMP_OP_IDS.invert.freeze
90
+ # The table holds both spellings of each comparison (`lt` and `<`); the
91
+ # kernels are named by the word.
92
+ BINCMP_BY_ID = CArray::LAZY_BINCMP_OP_IDS
93
+ .select { |name, _| name.to_s.match?(/\A[a-z]/) }.invert.freeze
51
94
 
52
95
  class Refused < StandardError; end
53
96
 
@@ -74,29 +117,82 @@ class CArray
74
117
 
75
118
  # Returns the array, or nil where nothing computed it.
76
119
  def self.evaluate (view)
77
- return nil unless askable?(view)
120
+ plan = worth_asking(view) or return nil
78
121
  out = CArray.__alloc_uninit__(view.data_type, view.dim)
79
- evaluate_into(view, out) ? out : nil
122
+ ask(plan, out) ? out : nil
80
123
  end
81
124
 
82
125
  # Fills an array the caller already has. Called from the store as well,
83
126
  # where making one and copying it over would be most of the work.
84
127
  # Returns true when something computed it.
85
128
  def self.evaluate_into (view, out)
129
+ plan = worth_asking(view) or return false
130
+ ask(plan, out)
131
+ end
132
+
133
+ # The plan to hand over, or nil. A marker over an array, or anything
134
+ # else with nothing to compute, is not worth handing over.
135
+ def self.worth_asking (view)
136
+ return nil unless askable?(view)
137
+ plan = plan(view) or return nil
138
+ plan.nodes.any? { |n| n.is_a?(Op) } ? plan : nil
139
+ rescue StandardError => error
140
+ retire(error)
141
+ nil
142
+ end
143
+
144
+ def self.ask (plan, out)
86
145
  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?
146
+ return false if overlaps?(plan, out)
147
+ # The walk leaves the destination masked where the expression is, so
148
+ # a destination that arrives masked loses its mask to an expression
149
+ # that has none.
150
+ if plan.masked
151
+ out.mask = 0 unless out.has_mask?
152
+ elsif out.has_mask?
153
+ out.mask = 0
154
+ end
93
155
  evaluator.call(plan, out) ? true : false
94
156
  rescue StandardError => error
157
+ retire(error)
158
+ false
159
+ end
160
+
161
+ # Whether the destination shares storage with a leaf it would read. An
162
+ # evaluator computes cell by cell, so writing a cell another leaf reads
163
+ # later -- `z[] = y + y` with z and y overlapping slices of one array --
164
+ # reads what it has just written. The walk reads every operand before
165
+ # it writes. The destination itself as a leaf is fine as long as it is
166
+ # read at the cell being written, not shifted.
167
+ def self.overlaps? (plan, out)
168
+ mine = storages(out)
169
+ shifted = plan.nodes.grep(Shifted).map(&:index)
170
+ plan.leaves.each_with_index.any? do |leaf, index|
171
+ if leaf.equal?(out)
172
+ shifted.include?(index)
173
+ else
174
+ storages(leaf).any? { |r| mine.any? { |m| m.equal?(r) } }
175
+ end
176
+ end
177
+ end
178
+
179
+ # The arrays that hold an array's cells: its root, or each parent's
180
+ # where a view stands over several.
181
+ def self.storages (array, found = [])
182
+ root = array.root_array
183
+ if ! root.entity? && root.respond_to?(:parents)
184
+ root.parents.each { |parent| storages(parent, found) }
185
+ else
186
+ found << root
187
+ end
188
+ found
189
+ end
190
+
191
+ def self.retire (error)
95
192
  CArray.expression_evaluator = nil
96
193
  warn "CArray: the registered expression evaluator raised " \
97
194
  "(#{error.class}: #{error.message}); expressions will be walked " \
98
195
  "from here on"
99
- false
100
196
  end
101
197
 
102
198
  def self.askable? (view)
@@ -115,8 +211,9 @@ class CArray
115
211
  raise Refused, "not a lazy expression" unless lazy?(view)
116
212
  w = Walk.new
117
213
  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)
214
+ masked = w.leaves.any? { |a| a.has_mask? } ||
215
+ w.nodes.any? { |n| n.is_a?(Shifted) && n.bounds.include?(:mask) }
216
+ Plan.new(w.nodes, w.leaves, view.data_type, view.dim, masked, w.signature)
120
217
  end
121
218
 
122
219
  def self.lazy? (x)
@@ -131,6 +228,7 @@ class CArray
131
228
  def initialize
132
229
  @nodes = []
133
230
  @leaves = []
231
+ @leaf_index = {}
134
232
  @seen = {}
135
233
  @signature = +""
136
234
  end
@@ -147,6 +245,9 @@ class CArray
147
245
  when CAMonOp then unary(n)
148
246
  when CABinOp then binary(n)
149
247
  when CATriOp then ternary(n)
248
+ when CAMonCmp then unary_comparison(n)
249
+ when CABinCmp then binary_comparison(n)
250
+ when CAShift then shifted(n)
150
251
  when CScalar then constant(n)
151
252
  when CArray then leaf(n)
152
253
  else raise Refused, "#{n.class} in an expression"
@@ -154,11 +255,24 @@ class CArray
154
255
  end
155
256
 
156
257
  def unary (n)
258
+ return conversion(n) if n.__op_id__ >= CAMonOp::CAST_BASE
157
259
  name = spell(MONOP_BY_ID, n.__op_id__, {})
158
260
  args = [visit(n.parent)]
159
261
  # A view over one array is masked exactly where that array is
160
262
  # (ca_obj_monop.c).
161
- op(:monop, name, n.data_type, args, :pass)
263
+ op(:monop, name, n.data_type, args, :pass, n.__trapping__)
264
+ end
265
+
266
+ # A cast node is named by the type it converts to, cast_<data_type>,
267
+ # and its body is keyed by the type it converts from.
268
+ def conversion (n)
269
+ from = n.parent.data_type
270
+ name = :"cast_#{n.data_type}"
271
+ body = CArray.__kernel_body__(:monop, name, from) or
272
+ raise Refused, "monop #{name} has no body at #{from}"
273
+ args = [visit(n.parent)]
274
+ note("c", name, from, args.join(","))
275
+ push Op.new(:monop, name, n.data_type, args, body, :pass, n.__trapping__)
162
276
  end
163
277
 
164
278
  def binary (n)
@@ -170,32 +284,107 @@ class CArray
170
284
  elsif n.data_type == :boolean && name == :bit_and_i then :kleene_and
171
285
  else :union
172
286
  end
173
- op(:binop, name, n.data_type, args, rule)
287
+ op(:binop, name, n.data_type, args, rule, n.__trapping__)
174
288
  end
175
289
 
176
290
  def ternary (n)
177
291
  name = spell(TRIOP_BY_ID, n.__op_id__, TRIOP_NAMES)
178
292
  args = [visit(n.parent), visit(n.__triop_op2__), visit(n.__triop_op3__)]
179
- op(:triop, name, n.data_type, args, :union)
293
+ # select is masked where its condition is, or where the branch the
294
+ # condition chooses is (ca_obj_triop.c).
295
+ rule = name == :select ? :select : :union
296
+ op(:triop, name, n.data_type, args, rule, n.__trapping__)
297
+ end
298
+
299
+ # A comparison is masked where its operands are (ca_obj_moncmp.c,
300
+ # ca_obj_bincmp.c): there is no three-valued comparison.
301
+ def unary_comparison (n)
302
+ name = spell(MONCMP_BY_ID, n.__op_id__, {})
303
+ operand = n.parent
304
+ args = [visit(operand)]
305
+ comparison(:moncmp, name, operand.data_type, args, :pass, n.__trapping__)
306
+ end
307
+
308
+ def binary_comparison (n)
309
+ name = spell(BINCMP_BY_ID, n.__op_id__, {})
310
+ right = n.__bincmp_right__
311
+ # Both sides are already of the type compared (CABinCmp casts one
312
+ # to the other); a pair that is not has no single body.
313
+ unless n.parent.data_type == right.data_type
314
+ raise Refused, "#{name} between #{n.parent.data_type} and #{right.data_type}"
315
+ end
316
+ args = [visit(n.parent), visit(right)]
317
+ comparison(:bincmp, name, n.parent.data_type, args, :union, n.__trapping__)
180
318
  end
181
319
 
182
- def op (kind, name, type, args, mask)
320
+ # `trapping`: the node skips the cells its operands mask rather than
321
+ # computing them -- one that can raise on a cell or calls Ruby for it.
322
+ # Each lazy view answers it from the rule its own kernel follows.
323
+ def comparison (kind, name, compared, args, mask, trapping)
324
+ body = CArray.__kernel_body__(kind, name, compared) or
325
+ raise Refused, "#{kind} #{name} has no body at #{compared}"
326
+ # A body written against its compared type or a tolerance (`feq`)
327
+ # needs more than its operands substituted.
328
+ if body.match?(/<\w+>|\btol\b/)
329
+ raise Refused, "#{kind} #{name} needs more than its operands"
330
+ end
331
+ note(kind.to_s[0..3], name, compared, args.join(","))
332
+ push Op.new(kind, name, :boolean, args, body, mask, trapping)
333
+ end
334
+
335
+ def op (kind, name, type, args, mask, trapping)
183
336
  body = CArray.__kernel_body__(kind, name, type) or
184
337
  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))
338
+ note(kind.to_s[0], name, type, args.join(","))
339
+ push Op.new(kind, name, type, args, body, mask, trapping)
188
340
  end
189
341
 
190
342
  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?)
343
+ index = leaf_index(n)
344
+ note("a", index, n.data_type, n.has_mask? ? 1 : 0)
345
+ push Leaf.new(index, n.data_type, n.has_mask?)
346
+ end
347
+
348
+ # A shift of an array is that array read elsewhere, so it is the
349
+ # array that becomes a leaf. A shift of anything else -- an
350
+ # expression, say -- has no array to read and is taken as a whole.
351
+ def shifted (n)
352
+ array = n.parent
353
+ bounds = n.__axis_bounds__
354
+ if Fusion.lazy?(array) || array.dim != n.dim ||
355
+ ! bounds.all? { |b| b == :fill || b == :mask }
356
+ return leaf(n)
357
+ end
358
+ offset = n.start
359
+ # The fill as a cell of the array holds it: a boolean array is
360
+ # filled with true or false, not with the Integer it was given.
361
+ fill = bounds.include?(:fill) ? as_cell(n.fill_value, array.data_type) : nil
362
+ index = leaf_index(array)
363
+ note("s", index, array.data_type, array.has_mask? ? 1 : 0,
364
+ offset.join(","), bounds.join(","), fill.inspect)
365
+ push Shifted.new(index, array.data_type, array.has_mask?,
366
+ offset, bounds, fill)
194
367
  end
195
368
 
369
+ def leaf_index (array)
370
+ @leaf_index.fetch(array.object_id) do
371
+ @leaves << array
372
+ @leaf_index[array.object_id] = @leaves.size - 1
373
+ end
374
+ end
375
+
376
+ # A masked scalar has no value to write into the expression.
196
377
  def constant (n)
197
- note("k", n[0], n.data_type)
198
- push Const.new(n[0], n.data_type)
378
+ raise Refused, "a masked scalar" if n.has_mask? && n.is_masked[0]
379
+ value = n[0]
380
+ note("k", value.inspect, n.data_type)
381
+ push Const.new(value, n.data_type)
382
+ end
383
+
384
+ def as_cell (value, data_type)
385
+ cell = CScalar.new(data_type)
386
+ cell[0] = value
387
+ cell[0]
199
388
  end
200
389
 
201
390
  def push (node)
@@ -210,6 +399,8 @@ class CArray
210
399
 
211
400
  # Two expressions of the same shape compute alike, whatever arrays
212
401
  # they are over, so a consumer can keep one compiled kernel for both.
402
+ # The shape includes which node each operation reads and which leaf
403
+ # each read is: `(a - b) - b` and `(a - b) - a` differ only there.
213
404
  def note (*parts)
214
405
  @signature << parts.join(":") << ";"
215
406
  end
@@ -17,13 +17,12 @@ class CArray::Inspector # :nodoc:
17
17
  raise "can't inspect CArray of ndim == 0"
18
18
  end
19
19
  formatter = get_formatter()
20
- class_name = get_class_name()
21
- type_name = get_type_name()
20
+ label = get_label()
22
21
  shape = get_shape()
23
22
  data_spec = get_data_spec(0, Array.new(@carray.ndim){0}, formatter, abbrev)
24
23
  info_list = get_info_list()
25
24
  output = ["<",
26
- format("%s.%s(%s)", class_name, type_name, shape.join(",")),
25
+ format("%s(%s)", label, shape.join(",")),
27
26
  ": ",
28
27
  info_list.join(" "),
29
28
  "\n",
@@ -35,6 +34,16 @@ class CArray::Inspector # :nodoc:
35
34
 
36
35
  private
37
36
 
37
+ # The name before the shape: the class and the storage type, or what a
38
+ # Face says instead through a private inspect_label (CATime gives its
39
+ # tick, which the storage type does not tell).
40
+ def get_label
41
+ if @carray.respond_to?(:inspect_label, true)
42
+ return @carray.send(:inspect_label)
43
+ end
44
+ return format("%s.%s", get_class_name, get_type_name)
45
+ end
46
+
38
47
  def get_class_name
39
48
  return @carray.class.to_s
40
49
  end
@@ -110,15 +119,27 @@ class CArray::Inspector # :nodoc:
110
119
  end
111
120
 
112
121
  def get_formatter
113
- # A Face that defines storage_to_scalar decodes each cell into a surface
122
+ # A Face can write its cells itself through a private
123
+ # inspect_cell_formatter, a lambda taking the decoded cell.
124
+ if @carray.respond_to?(:inspect_cell_formatter, true)
125
+ return @carray.send(:inspect_cell_formatter)
126
+ end
127
+ # A Face that defines storage_to_element decodes each cell into a surface
114
128
  # value (CATime::Element, a String, a category label, ...) that has nothing
115
129
  # to do with the storage data_type, so the formatter must follow the decoded
116
130
  # value, not the storage. Faces without the hook (CAString) hand back the
117
131
  # stored value itself and fall through to the storage formatters below.
118
- if @carray.face? and @carray.respond_to?(:storage_to_scalar)
132
+ if @carray.face? and @carray.respond_to?(:storage_to_element)
119
133
  return lambda { |x| x.inspect }
120
134
  end
121
- case @carray.data_type
135
+ data_type = @carray.data_type
136
+ # The same Faces behind a gated (fixlen) surface hand back the value of
137
+ # their storage, so they take the storage's formatter.
138
+ if @carray.face? and data_type == CA_FIXLEN and not @carray.data_class
139
+ storage = @carray.parent.data_type
140
+ data_type = storage unless storage == CA_FIXLEN
141
+ end
142
+ case data_type
122
143
  when CA_BOOLEAN
123
144
  # Boolean cells fetch as true/false; show compact 1/0 (masked = _).
124
145
  # The type name in the inspect header distinguishes this from an int array.
@@ -134,11 +155,8 @@ class CArray::Inspector # :nodoc:
134
155
  x.real.inspect, (x.imag >= 0) ? "+" : "-", x.imag.abs.inspect) }
135
156
  when CA_FIXLEN
136
157
  if @carray.data_class
137
- if @carray.bytes <= 6
138
- lambda{|x| "%s" % x.encode.dump }
139
- else
140
- lambda{|x| "%s" % (x.encode.chomp[0, 5]+"...").dump }
141
- end
158
+ # A record shows its members, as the element's own inspect does.
159
+ lambda{|x| x.inspect }
142
160
  else
143
161
  if @carray.bytes <= 6
144
162
  lambda{|x| "%s" % x.dump }
@@ -29,6 +29,16 @@ class CAIterator
29
29
  attr_reader :ndim, :shape
30
30
  alias dim shape
31
31
 
32
+ # The keywords a member passes on to a core reduction over its own axes.
33
+ # The member sets `axis:`, so one from the caller is refused; the rest go
34
+ # to the core, which checks them.
35
+ def self.reduce_keywords (op, kw) # :nodoc:
36
+ if kw.key?(:axis)
37
+ raise ArgumentError, "#{op}: unknown keyword: :axis (the iterator sets the axes)"
38
+ end
39
+ kw
40
+ end
41
+
32
42
  # The required reduction surface, modelled on the two reference members
33
43
  # CASlabIterator and CACategoricalIterator (the surface they both provide).
34
44
  # Declared abstract so a later member
@@ -144,7 +154,7 @@ class CAIterator
144
154
  # @yieldparam piece [CArray] next piece.
145
155
  # @yieldreturn [Object] updated accumulator.
146
156
  # @return [Object] the final accumulator.
147
- [
157
+ REQUIRED_SURFACE = [
148
158
  :sum, :accumulate, :prod, :mean, :min, :max, # tier 1
149
159
  :variance, :stddev, :all, :any,
150
160
  :variancep, :stddevp, :minmax, # tier 2
@@ -153,7 +163,9 @@ class CAIterator
153
163
  :median, :percentile, :quantile, # tier 3
154
164
  :count, :count_not_masked, :count_masked, :elements, # count family
155
165
  :each, :reduce, # generic iterate
156
- ].each do |name|
166
+ ].freeze
167
+
168
+ REQUIRED_SURFACE.each do |name|
157
169
  define_method(name) do |*, **, &_blk|
158
170
  raise NotImplementedError, "#{self.class} must implement ##{name}"
159
171
  end