carray 3.0.1 → 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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  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 +90 -41
  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 +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  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 +194 -42
  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 +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -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 +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  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 +551 -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 +88 -75
  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 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  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 +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. 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
@@ -7,18 +7,22 @@ class CArray::Inspector # :nodoc:
7
7
  end
8
8
 
9
9
  # @!visibility private
10
- def inspect_string
10
+ #
11
+ # `abbrev` false renders every element instead of eliding with `...`.
12
+ # It is the one difference between #inspect and #inspect_full: the
13
+ # header and the layout are the same, so there is one renderer rather
14
+ # than two that could drift apart.
15
+ def inspect_string (abbrev: true)
11
16
  if @carray.ndim == 0
12
17
  raise "can't inspect CArray of ndim == 0"
13
18
  end
14
19
  formatter = get_formatter()
15
- class_name = get_class_name()
16
- type_name = get_type_name()
20
+ label = get_label()
17
21
  shape = get_shape()
18
- data_spec = get_data_spec(0, Array.new(@carray.ndim){0}, formatter)
22
+ data_spec = get_data_spec(0, Array.new(@carray.ndim){0}, formatter, abbrev)
19
23
  info_list = get_info_list()
20
24
  output = ["<",
21
- format("%s.%s(%s)", class_name, type_name, shape.join(",")),
25
+ format("%s(%s)", label, shape.join(",")),
22
26
  ": ",
23
27
  info_list.join(" "),
24
28
  "\n",
@@ -30,6 +34,16 @@ class CArray::Inspector # :nodoc:
30
34
 
31
35
  private
32
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
+
33
47
  def get_class_name
34
48
  return @carray.class.to_s
35
49
  end
@@ -105,15 +119,27 @@ class CArray::Inspector # :nodoc:
105
119
  end
106
120
 
107
121
  def get_formatter
108
- # 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
109
128
  # value (CATime::Element, a String, a category label, ...) that has nothing
110
129
  # to do with the storage data_type, so the formatter must follow the decoded
111
130
  # value, not the storage. Faces without the hook (CAString) hand back the
112
131
  # stored value itself and fall through to the storage formatters below.
113
- if @carray.face? and @carray.respond_to?(:storage_to_scalar)
132
+ if @carray.face? and @carray.respond_to?(:storage_to_element)
114
133
  return lambda { |x| x.inspect }
115
134
  end
116
- 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
117
143
  when CA_BOOLEAN
118
144
  # Boolean cells fetch as true/false; show compact 1/0 (masked = _).
119
145
  # The type name in the inspect header distinguishes this from an int array.
@@ -129,11 +155,8 @@ class CArray::Inspector # :nodoc:
129
155
  x.real.inspect, (x.imag >= 0) ? "+" : "-", x.imag.abs.inspect) }
130
156
  when CA_FIXLEN
131
157
  if @carray.data_class
132
- if @carray.bytes <= 6
133
- lambda{|x| "%s" % x.encode.dump }
134
- else
135
- lambda{|x| "%s" % (x.encode.chomp[0, 5]+"...").dump }
136
- end
158
+ # A record shows its members, as the element's own inspect does.
159
+ lambda{|x| x.inspect }
137
160
  else
138
161
  if @carray.bytes <= 6
139
162
  lambda{|x| "%s" % x.dump }
@@ -146,7 +169,7 @@ class CArray::Inspector # :nodoc:
146
169
  end
147
170
  end
148
171
 
149
- def get_data_spec (level, idx, formatter)
172
+ def get_data_spec (level, idx, formatter, abbrev = true)
150
173
  io = +"[ " # mutable buffer; `<<` below appends into it
151
174
  ndim = @carray.ndim
152
175
  dim = @carray.shape
@@ -163,7 +186,7 @@ class CArray::Inspector # :nodoc:
163
186
  if i != dim[level] - 1
164
187
  io << ", "
165
188
  end
166
- if io.length > 48 - 2*level
189
+ if abbrev and io.length > 48 - 2*level
167
190
  if i < dim[level] - 1
168
191
  io << "..."
169
192
  over = true
@@ -182,24 +205,24 @@ class CArray::Inspector # :nodoc:
182
205
  end
183
206
  else
184
207
  over = false
185
- show = [dim[level], 5].min
208
+ show = abbrev ? [dim[level], 5].min : dim[level]
186
209
  show.times do |i|
187
210
  idx[level] = i
188
- io << get_data_spec(level+1, idx, formatter)
211
+ io << get_data_spec(level+1, idx, formatter, abbrev)
189
212
  if i < show - 1
190
213
  io << ",\n" + " " * (level+1)
191
214
  end
192
- if i >= 2 and dim[level] > 5
215
+ if abbrev and i >= 2 and dim[level] > 5
193
216
  break
194
217
  end
195
218
  end
196
- if dim[level] > 5
219
+ if abbrev and dim[level] > 5
197
220
  io << "... ... ..."
198
221
  over = true
199
222
  end
200
223
  if over
201
224
  idx[level] = dim[level] - 1
202
- io << "\n"+ " " * (level+1) + get_data_spec(level+1, idx, formatter)
225
+ io << "\n"+ " " * (level+1) + get_data_spec(level+1, idx, formatter, abbrev)
203
226
  end
204
227
  end
205
228
  io << " ]"
@@ -219,6 +242,34 @@ class CArray
219
242
  return CArray::Inspector.new(self).inspect_string
220
243
  end
221
244
 
245
+ # @overload inspect_full
246
+ # The same description as {#inspect}, with every element rendered
247
+ # instead of the `...` preview.
248
+ #
249
+ # `inspect` abbreviates on purpose -- it is what `p`, `irb` and an
250
+ # error message call, and a million-cell array has to stay readable
251
+ # there. `inspect_full` is for the other moment, when the whole
252
+ # array is the thing you came to look at:
253
+ #
254
+ # puts a.inspect_full
255
+ #
256
+ # The header, the layout and the `_` for a masked cell are
257
+ # `inspect`'s; only the eliding is dropped, so for an array small
258
+ # enough that `inspect` was not abbreviating anything the two give
259
+ # the same string.
260
+ #
261
+ # Note that neither of these is `to_s`, which returns the **raw
262
+ # bytes** of the storage rather than anything printable.
263
+ #
264
+ # The result is one String holding every element, so it is as large
265
+ # as the array is: nothing here is streamed, and a line is as long
266
+ # as the last axis makes it.
267
+ #
268
+ # @return [String]
269
+ def inspect_full
270
+ return CArray::Inspector.new(self).inspect_string(abbrev: false)
271
+ end
272
+
222
273
  private
223
274
 
224
275
  def desc