carray-jit 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -179,6 +179,9 @@ class CArray
179
179
  # radius, kept per side because a window need not be symmetric. It is
180
180
  # what the caller walks the interior by.
181
181
  attr_reader :window_reach
182
+ # The same, per window: what one array is reached into, rather than the
183
+ # widest reach over all of them.
184
+ attr_reader :window_reaches
182
185
  # The names the block gave its windows, which are the arrays a border
183
186
  # rule applies to: the ones the block reaches away from the cell in.
184
187
  attr_reader :windows
@@ -233,10 +236,18 @@ class CArray
233
236
  # returned, and it reaches no array. It is the smallest of the three --
234
237
  # with no cell to address there is no extent, no direction and no mask,
235
238
  # so most of what follows never runs.
239
+ # `free_indices` are the result's axes, named at the call site rather
240
+ # than taken from the block's parameters. Naming them puts the
241
+ # contraction in its explicit form: these are free however often they
242
+ # appear, and a parameter is summed by repeating as it is under the
243
+ # convention. Nil is "nothing was named" and an empty list is "named,
244
+ # and none of them are free" -- which is a contraction to a single
245
+ # number, and is not the same statement.
236
246
  def initialize (source, node: nil, array_names: [], c_functions: {}, rank: nil,
237
247
  steps: nil, contract: false, result: nil, function: false,
238
248
  pointers: {}, map: false, cell_names: [],
239
- recursion: nil, windows: [], returns: true)
249
+ recursion: nil, windows: [], returns: true,
250
+ free_indices: nil)
240
251
  @source = source
241
252
  @node = node
242
253
  @array_names = array_names
@@ -277,6 +288,12 @@ class CArray
277
288
  @whole_array = false
278
289
  # How far the windows reach on each axis, filled in as they are read.
279
290
  @window_reach = Array.new(rank.to_i) { [0, 0] }
291
+ # And how far each window reaches on its own. The kernel walks the
292
+ # interior by the widest of them, but whether one array may be written
293
+ # in place is a question about that array's window alone.
294
+ @window_reaches = Hash.new { |reaches, name|
295
+ reaches[name] = Array.new(rank.to_i) { [0, 0] }
296
+ }
280
297
  @uses_undef = false
281
298
  @calls_for_effect = false
282
299
  @outer_names = []
@@ -293,6 +310,7 @@ class CArray
293
310
  @given_rank = rank
294
311
  @steps = steps
295
312
  @contract = contract
313
+ @free_indices = free_indices
296
314
  @result = result
297
315
  @contracted_names = []
298
316
  @free_names = []
@@ -544,10 +562,9 @@ class CArray
544
562
 
545
563
  # Turns `c[i,j] = a[i,k] * b[k,j]` into the loops it stands for.
546
564
  #
547
- # Which indices are summed is not the assignment's business: an index
548
- # that appears twice in the term is summed, and that repetition is the
549
- # notation -- it is what stands in for the sigma. An index appearing
550
- # once is free. The left-hand side says where the result goes and in
565
+ # Which indices are summed is not the assignment's business: a repeated
566
+ # index is summed, and that repetition is the notation -- it is what
567
+ # stands in for the sigma. An index appearing once is free. The left-hand side says where the result goes and in
551
568
  # what order its axes lie; it cannot make an index disappear.
552
569
  # The block's value is what every cell of the result gets, so the last
553
570
  # expression becomes a write into the result at the cell the loop is on.
@@ -641,8 +658,9 @@ class CArray
641
658
  end
642
659
 
643
660
  # Counts where each index sits on a tensor, on the right-hand side only.
644
- # Once is free, twice is summed, and more than twice is not the
645
- # convention -- it says nothing about which pair to sum.
661
+ # Once is free and repeated is summed, however often it repeats: three
662
+ # positions are not a pair to choose between but one index read at three
663
+ # of them, and `a[i,i,i]` is the sum along the cube's long diagonal.
646
664
  def classify_indices (statements, write)
647
665
  counts = Hash.new(0)
648
666
  subscripts_of(statements, write).each { |index, _| counts[index] += 1 }
@@ -653,16 +671,23 @@ class CArray
653
671
  "#{missing.map { |name| "`#{name}`" }.join(', ')} " \
654
672
  "#{missing.size == 1 ? 'names no axis' : 'name no axis'} here")
655
673
  end
656
- crowded = counts.select { |_, count| count > 2 }.keys
657
- unless crowded.empty?
658
- raise Unsupported.new(
659
- "#{crowded.map { |name| "`#{name}`" }.join(', ')} appears more " \
660
- "than twice; a contraction sums a pair, and there is no pair to " \
661
- "choose")
674
+ # Naming the result's axes says which indices are free; it does not
675
+ # say what a repetition means, and a repetition still means a sum. So
676
+ # a named index is free however often it appears -- twice is what a
677
+ # point number does, and `square[a,a]` is the diagonal rather than a
678
+ # trace -- while a parameter is summed by repeating, here as under the
679
+ # convention. One that appears once is free and was not named, which
680
+ # is a sum along an axis and is not a contraction.
681
+ if @free_indices
682
+ parameters = @index_names - @free_indices
683
+ alone = parameters.select { |name| counts[name] == 1 }
684
+ unless alone.empty?
685
+ raise Unsupported.new(describe_a_lone_parameter(alone))
686
+ end
687
+ return [@free_indices, parameters]
662
688
  end
663
-
664
689
  [@index_names.select { |name| counts[name] == 1 },
665
- @index_names.select { |name| counts[name] == 2 }]
690
+ @index_names.select { |name| counts[name] > 1 }]
666
691
  end
667
692
 
668
693
  # Every subscript on the right-hand side: the summand, and whatever the
@@ -681,20 +706,44 @@ class CArray
681
706
  collected.reject { |index, _| index.nil? }
682
707
  end
683
708
 
709
+ # A parameter at one position only. Nothing there stands in for a
710
+ # sigma, so summing it would be the argument list quietly meaning more
711
+ # than it says -- the same reason the convention refuses it.
712
+ def describe_a_lone_parameter (alone)
713
+ listed = alone.map { |name| "`#{name}`" }.join(", ")
714
+ named = (@free_indices + alone).map { |name| ":#{name}" }.join(", ")
715
+ "#{listed} #{alone.size == 1 ? 'appears' : 'appear'} once, so " \
716
+ "#{alone.size == 1 ? 'it is' : 'they are'} free rather than summed. " \
717
+ "A contraction sums the indices that repeat; name " \
718
+ "#{alone.size == 1 ? 'it' : 'them'} as " \
719
+ "#{alone.size == 1 ? 'an axis' : 'axes'} of the result " \
720
+ "(`CArray.jit_contract(#{named})`) to keep " \
721
+ "#{alone.size == 1 ? 'it' : 'them'}, or use sum(axis:) to sum along " \
722
+ "the axis"
723
+ end
724
+
684
725
  def describe_index_mismatch (written, free, summed)
685
726
  summed_on_left = written & summed
686
727
  unless summed_on_left.empty?
728
+ # What to name is the whole left-hand side, in its order: those are
729
+ # the result's axes, and the one that repeats is only the reason
730
+ # the convention could not see it.
731
+ named = written.map { |name| ":#{name}" }.join(", ")
687
732
  return "#{summed_on_left.map { |name| "`#{name}`" }.join(', ')} " \
688
- "#{summed_on_left.size == 1 ? 'appears' : 'appear'} twice on " \
689
- "the right, so #{summed_on_left.size == 1 ? 'it is' : 'they are'} " \
690
- "summed over and cannot also be free"
733
+ "#{summed_on_left.size == 1 ? 'is repeated' : 'are repeated'} " \
734
+ "on the right, so #{summed_on_left.size == 1 ? 'it is' : 'they are'} " \
735
+ "summed over and cannot also be free. That a repeated index " \
736
+ "is summed is the convention for dimensions; an index " \
737
+ "that numbers things -- a point, a sample -- is not one, and " \
738
+ "naming the result's axes says so: " \
739
+ "`CArray.jit_contract(#{named})`"
691
740
  end
692
741
  dropped = free - written
693
742
  "#{dropped.map { |name| "`#{name}`" }.join(', ')} " \
694
743
  "#{dropped.size == 1 ? 'appears' : 'appear'} once, so " \
695
744
  "#{dropped.size == 1 ? 'it is' : 'they are'} free and must be on the " \
696
- "left. A contraction sums the indices that appear twice; to sum one " \
697
- "that does not, write the loop with jit_for, or use sum(axis:)"
745
+ "left. A contraction sums the indices that repeat; to sum one that " \
746
+ "does not, write the loop with jit_for, or use sum(axis:)"
698
747
  end
699
748
 
700
749
  # The sum starts from zero of whatever the summand is; which zero that
@@ -714,9 +763,16 @@ class CArray
714
763
 
715
764
  private
716
765
 
766
+ # The accumulator is a local this writes rather than one the block
767
+ # named, so it has to avoid every name that is already something: the
768
+ # block's locals, and the indices -- an index called `contraction` would
769
+ # have shared the identifier with the accumulator and the sum would have
770
+ # come out zero, with nothing said.
717
771
  def free_local_name
772
+ taken = @local_names + @index_names + @outer_names + @inner_names +
773
+ @contracted_names
718
774
  name = :contraction
719
- name = :"#{name}_" while @local_names.include?(name)
775
+ name = :"#{name}_" while taken.include?(name)
720
776
  name
721
777
  end
722
778
 
@@ -777,6 +833,27 @@ class CArray
777
833
  return
778
834
  end
779
835
 
836
+ if @contract && @free_indices
837
+ # The explicit form: the result's axes were named at the call site,
838
+ # so what the block names are the indices that are summed -- and a
839
+ # contraction may now take no parameters at all, which is how the
840
+ # diagonal of one array is written.
841
+ summed = requireds.map(&:name)
842
+ both = @free_indices & summed
843
+ unless both.empty?
844
+ raise Unsupported.new(
845
+ "#{both.map { |name| "`#{name}`" }.join(', ')} " \
846
+ "#{both.size == 1 ? 'is' : 'are'} named as " \
847
+ "#{both.size == 1 ? 'an axis' : 'axes'} of the result and again " \
848
+ "as a block parameter; the parameters are the indices that are " \
849
+ "summed")
850
+ end
851
+ @index_names = @free_indices + summed
852
+ refuse_reserved_indices
853
+ @outer_names = @index_names.dup
854
+ return
855
+ end
856
+
780
857
  if @windows.any?
781
858
  # The parameters were read before this analyzer was built -- the
782
859
  # caller had to, to know which array each window is onto -- so what
@@ -798,9 +875,23 @@ class CArray
798
875
  else
799
876
  @index_names = requireds.map(&:name)
800
877
  end
878
+ refuse_reserved_indices
801
879
  @outer_names = @index_names.dup
802
880
  end
803
881
 
882
+ # An index becomes a variable in the C this writes, alongside the
883
+ # kernel's own parameters, so a name C has taken already is refused
884
+ # here rather than by the compiler -- which would complain about a
885
+ # source the block's author never saw.
886
+ def refuse_reserved_indices
887
+ taken = @index_names & CGenerator::RESERVED_NAMES
888
+ return if taken.empty?
889
+ raise Unsupported.new(
890
+ "#{taken.map { |name| "`#{name}`" }.join(', ')} " \
891
+ "#{taken.size == 1 ? 'is a name' : 'are names'} the kernel's own C " \
892
+ "uses, so #{taken.size == 1 ? 'it cannot be an index' : 'they cannot be indices'}")
893
+ end
894
+
804
895
  # `printf` writes to the terminal from inside the loop, which is what
805
896
  # it is for: the kernel is otherwise silent until it finishes. The
806
897
  # format has to be a literal, since C reads it at compile time.
@@ -873,6 +964,19 @@ class CArray
873
964
  if @whole_array && @array_names.include?(node.name)
874
965
  return whole_array_write(node.name, expression, node.location)
875
966
  end
967
+ # An index is the loop's, not the block's. Ruby reads `i = 2` as
968
+ # rebinding the parameter and leaves the loop alone; the C would
969
+ # assign to the counter, so the loop would walk somewhere else --
970
+ # to cells outside the array, given a value outside its extent.
971
+ # The two do not mean the same thing, so this is not compiled.
972
+ if index_in_scope?(node.name)
973
+ raise Unsupported.new(
974
+ "`#{node.name}` is a loop index, and assigning to it here would " \
975
+ "move the loop rather than the value: in Ruby the same line " \
976
+ "rebinds the parameter and the loop runs on. Use a local of " \
977
+ "another name",
978
+ node.location)
979
+ end
876
980
  @local_names << node.name unless @local_names.include?(node.name)
877
981
  Assignment.new(node.name, expression, node.location)
878
982
  when Prism::CallNode
@@ -1642,6 +1746,9 @@ class CArray
1642
1746
  end
1643
1747
  @window_reach[axis] = [[@window_reach[axis][0], offset].min,
1644
1748
  [@window_reach[axis][1], offset].max]
1749
+ own = @window_reaches[array][axis]
1750
+ @window_reaches[array][axis] = [[own[0], offset].min,
1751
+ [own[1], offset].max]
1645
1752
  [@outer_names[axis], offset]
1646
1753
  }
1647
1754
  end
@@ -1661,12 +1768,10 @@ class CArray
1661
1768
  # An index expression is `j`, `j + c` or `j - c`, where `j` is any index
1662
1769
  # in scope, so that the offset is a compile-time constant.
1663
1770
  def read_subscript (node, location)
1664
- if node.is_a?(Prism::LocalVariableReadNode) && index_in_scope?(node.name)
1665
- return [node.name, 0]
1666
- end
1771
+ name = index_name(node)
1772
+ return [name, 0] if name && index_in_scope?(name)
1667
1773
  if node.is_a?(Prism::CallNode) && [:+, :-].include?(node.name) &&
1668
- node.receiver.is_a?(Prism::LocalVariableReadNode) &&
1669
- index_in_scope?(node.receiver.name)
1774
+ (receiver = index_name(node.receiver)) && index_in_scope?(receiver)
1670
1775
  return walked_subscript(node)
1671
1776
  end
1672
1777
  # Anything else pins the axis at a position the loop does not walk:
@@ -1741,6 +1846,20 @@ class CArray
1741
1846
  [receiver.name, node.name == :+ ? offset : UnaryMinus.new(offset)]
1742
1847
  end
1743
1848
 
1849
+ # An index the block names as a parameter reads as a local variable.
1850
+ # One named at the call site is a parameter of nothing, so Ruby reads it
1851
+ # as a method call -- or as a local variable, where the scope the block
1852
+ # was written in happens to have one by that name. Both spell the same
1853
+ # index, and which it is says nothing about what it means.
1854
+ def index_name (node)
1855
+ case node
1856
+ when Prism::LocalVariableReadNode
1857
+ node.name
1858
+ when Prism::CallNode
1859
+ node.name if node.receiver.nil? && node.arguments.nil? && node.block.nil?
1860
+ end
1861
+ end
1862
+
1744
1863
  def index_in_scope? (name)
1745
1864
  @outer_names.include?(name) || @inner_names.include?(name)
1746
1865
  end
@@ -368,25 +368,32 @@ class CArray
368
368
  Fiddle::TYPE_USHORT => :uint16,
369
369
  Fiddle::TYPE_INT => :int32,
370
370
  Fiddle::TYPE_UINT => :uint32,
371
- Fiddle::TYPE_LONG => :int64,
372
- Fiddle::TYPE_LONG_LONG => :int64,
371
+ Fiddle::TYPE_LONG => :int64,
372
+ Fiddle::TYPE_LONG_LONG => :int64,
373
+ # `unsigned long` and `size_t` arrive as the same code, and
374
+ # `uint64_t` as the other one.
375
+ Fiddle::TYPE_ULONG => :uint64,
376
+ Fiddle::TYPE_ULONG_LONG => :uint64,
373
377
  }.freeze
374
378
 
375
379
  # Codes Fiddle may return, mapped to what a kernel computes them in.
376
380
  # Absent means the type may be written down but holds no value a body
377
- # can compute with. `uint64_t` is absent for the reason CArray has no
378
- # uint64 array: no computation type holds it without losing a bit.
381
+ # can compute with -- `void`, and a pointer to it.
379
382
  COMPUTATION = {
380
- Fiddle::TYPE_DOUBLE => :double,
381
- Fiddle::TYPE_FLOAT => :double,
382
- Fiddle::TYPE_CHAR => :int64,
383
- Fiddle::TYPE_UCHAR => :int64,
384
- Fiddle::TYPE_SHORT => :int64,
385
- Fiddle::TYPE_USHORT => :int64,
386
- Fiddle::TYPE_INT => :int64,
387
- Fiddle::TYPE_UINT => :int64,
388
- Fiddle::TYPE_LONG => :int64,
389
- Fiddle::TYPE_LONG_LONG => :int64,
383
+ Fiddle::TYPE_DOUBLE => :double,
384
+ Fiddle::TYPE_FLOAT => :double,
385
+ Fiddle::TYPE_CHAR => :int64,
386
+ Fiddle::TYPE_UCHAR => :int64,
387
+ Fiddle::TYPE_SHORT => :int64,
388
+ Fiddle::TYPE_USHORT => :int64,
389
+ Fiddle::TYPE_INT => :int64,
390
+ Fiddle::TYPE_UINT => :int64,
391
+ Fiddle::TYPE_LONG => :int64,
392
+ Fiddle::TYPE_LONG_LONG => :int64,
393
+ # uint64 is a computation type of its own, precisely because int64
394
+ # cannot carry what it holds; CArray has the array to match.
395
+ Fiddle::TYPE_ULONG => :uint64,
396
+ Fiddle::TYPE_ULONG_LONG => :uint64,
390
397
  }.freeze
391
398
 
392
399
  module_function
@@ -681,6 +688,22 @@ class CArray
681
688
  "the signature rather than a value; the body cannot read it"
682
689
  end)
683
690
  end
691
+ # A body is handed its values through the kernel's three scalar buses,
692
+ # which carry doubles, int64s and complexes. A uint64 argument is the
693
+ # one numeric type none of them can carry whole -- that is what having
694
+ # its own computation type means -- so it is refused here, where the
695
+ # declaration is, rather than deeper down as a kernel that cannot be
696
+ # built. A `uint64_t *` reaches an array of them, and a uint64_t
697
+ # value comes back out of a body unharmed.
698
+ names.zip(parameters).each do |parameter, type|
699
+ next if type.pointer || type.computation != :uint64
700
+ raise Unsupported,
701
+ "`#{parameter}` is declared `#{type.text}`, and a value is " \
702
+ "handed to a body as a double, an int64 or a complex -- a " \
703
+ "uint64 fits none of them without losing a bit. Take " \
704
+ "`#{type.text} *` and index it, or take an int64 where the " \
705
+ "values are small enough to be one"
706
+ end
684
707
  types = names.zip(parameters).reject { |_, type| type.pointer }
685
708
  .to_h { |parameter, type| [parameter, type.computation] }
686
709
 
@@ -68,6 +68,18 @@ class CArray
68
68
  "double *reals, int64_t *integers, void **functions, void **data, " \
69
69
  "char **mask_pointers, int64_t *mask_strides, int32_t *error)".freeze
70
70
 
71
+ # Names an index may not take, because the C it becomes is written with
72
+ # them: the kernel's own parameters above, and the words C keeps for
73
+ # itself. A name from either list compiles to something else or to
74
+ # nothing, and the compiler's complaint is about a source the block's
75
+ # author did not write.
76
+ RESERVED_NAMES =
77
+ (SIGNATURE.scan(/\*?(\w+)[,)]/).flatten +
78
+ %w[auto break case char const continue default do double else enum
79
+ extern float for goto if inline int long register restrict return
80
+ short signed sizeof static struct switch typedef union unsigned
81
+ void volatile while]).map(&:to_sym).freeze
82
+
71
83
  ARGUMENTS =
72
84
  "pointers, strides, bounds, reals, integers, functions, data, " \
73
85
  "mask_pointers, mask_strides, error".freeze
@@ -40,7 +40,26 @@ class CArray
40
40
  # being lower was not a decision -- the flag list is the one the first
41
41
  # milestone was scaffolded with, and only the contraction flag beside
42
42
  # it was ever argued for.
43
- FLAGS = ["-O3", "-fPIC", "-shared", "-ffp-contract=off"].freeze
43
+
44
+ # Every generated object exports the same fixed names -- carray_jit_kernel,
45
+ # carray_jit_slab, the border and error helpers -- because each one is a
46
+ # module of its own. Fiddle.dlopen passes RTLD_GLOBAL, so on ELF those
47
+ # names all land in one flat namespace and an intra-module call resolves
48
+ # to the first definition loaded: a later kernel's carray_jit_slab calls
49
+ # an earlier kernel's carray_jit_kernel, which takes a different number
50
+ # of operands, reads past the pointers it was given, and the process
51
+ # segfaults. -Wl,-Bsymbolic binds each object's own definitions before
52
+ # the global scope, which is what one object per kernel assumed all
53
+ # along. Nothing else needs saying so: Mach-O binds within each dylib's
54
+ # own two-level namespace and PE within each DLL, so the flag is named
55
+ # only where it means something -- and Apple's linker rejects it
56
+ # outright, which would trade a Linux crash for a macOS build that never
57
+ # compiles at all.
58
+ SYMBOLIC =
59
+ (RbConfig::CONFIG["host_os"] =~ /darwin|mswin|mingw|cygwin/ ?
60
+ [] : ["-Wl,-Bsymbolic"]).freeze
61
+
62
+ FLAGS = ["-O3", "-fPIC", "-shared", "-ffp-contract=off", *SYMBOLIC].freeze
44
63
 
45
64
  # Kernels retained on disk. Each costs about 17 KB, so the default is
46
65
  # roughly 9 MB -- far more than any real program compiles, but a bound
@@ -63,11 +82,29 @@ class CArray
63
82
  # directory for this one.
64
83
  def cache_root
65
84
  return ephemeral_directory if ephemeral?
66
- ENV["CARRAY_JIT_CACHE"] ||
85
+ ENV["CARRAY_JIT_CACHE"] || @cache_root ||
67
86
  File.join(ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache"),
68
87
  "carray-jit")
69
88
  end
70
89
 
90
+ # An application saying where its own kernels live, rather than
91
+ # sharing the one cache under the home directory. Set it before the
92
+ # first kernel is compiled; kernels already loaded keep working, and
93
+ # what is already on disk stays where it is. `nil` restores the
94
+ # default.
95
+ #
96
+ # The environment still comes first: `CARRAY_JIT_CACHE` redirects an
97
+ # application that names a directory here, and `CARRAY_JIT_NO_CACHE`
98
+ # takes the cache away, so whoever runs a program can still put it
99
+ # somewhere writable, or do without.
100
+ #
101
+ # The path is expanded when it is given, not when it is read: a
102
+ # relative one would otherwise name a different directory after the
103
+ # program changes its working directory.
104
+ def cache_root= (path)
105
+ @cache_root = path && File.expand_path(path)
106
+ end
107
+
71
108
  def cache_directory
72
109
  File.join(cache_root, environment_tag)
73
110
  end
@@ -16,13 +16,24 @@ class CArray
16
16
  # goes back to CArray, which walks it and arrives at the same answer.
17
17
  class Expression
18
18
 
19
+ # A kernel that can divide by zero calls ca_zerodiv, which the CArray
20
+ # extension defines and this object does not. ELF is content to leave
21
+ # the name undefined and resolve it when the object is loaded, which is
22
+ # what the declaration in #source_for assumes; Mach-O refuses to link at
23
+ # all, so on macOS every expression holding an integer `/` or `%` failed
24
+ # to compile and was silently handed back to CArray to walk. The flag
25
+ # says to look the name up at load time, which is what Ruby builds its
26
+ # own extensions with and the only thing this object leaves undefined.
27
+ DYNAMIC_LOOKUP =
28
+ (RbConfig::CONFIG["host_os"] =~ /darwin/ ? ["-Wl,-undefined,dynamic_lookup"] : []).freeze
29
+
19
30
  # Built the way CArray's own kernels were, since that is what the answer
20
31
  # is being compared against. The Prism front end wants the opposite of
21
32
  # this on one point -- it answers to a Ruby loop, which does not fuse a
22
33
  # multiply and an add into one rounding, so it compiles with
23
34
  # -ffp-contract=off. Here the reference is the eager kernel, which was
24
35
  # built with whatever CArray settled on.
25
- FLAGS = ["-fPIC", "-shared", *CArray::BUILD_FLAGS.split].freeze
36
+ FLAGS = ["-fPIC", "-shared", *CArray::BUILD_FLAGS.split, *DYNAMIC_LOOKUP].freeze
26
37
 
27
38
  C_TYPES = {
28
39
  float64: "double", float32: "float",
@@ -37,7 +48,9 @@ class CArray
37
48
  @kernels = {}
38
49
  end
39
50
 
40
- # Fills `out` and returns true, or writes nothing and returns false.
51
+ # Fills `out` and returns true, or returns false and leaves it to
52
+ # CArray. A decline may have written part of `out` first, which is
53
+ # what walking it over again then settles.
41
54
  def call (plan, out)
42
55
  return false unless C_TYPES.key?(plan.data_type)
43
56
  aliased = plan.leaves.any? { |array| array.equal?(out) }
@@ -51,6 +64,25 @@ class CArray
51
64
  kernel.call(out.elements, *pointers(plan, bases))
52
65
  end
53
66
  true
67
+ rescue ZeroDivisionError
68
+ # A zero divisor is an answer, not a fault: it is what ca_zerodiv --
69
+ # the only thing a kernel here calls out to -- reports, and the same
70
+ # expression walked reaches the same place and raises the same error.
71
+ #
72
+ # CArray cannot tell the two apart, though. It catches whatever an
73
+ # evaluator raises, retires it for the rest of the process and says so
74
+ # on stderr, which is right for an evaluator that is broken and wrong
75
+ # for one that has just met a zero. So this declines instead, and
76
+ # CArray walks the expression and raises it there. Anything else
77
+ # still reaches CArray and still retires this, which is what that net
78
+ # is for.
79
+ #
80
+ # What it costs: a kernel that raised where the walk does not -- over
81
+ # a masked zero, say -- now reads as slow rather than as wrong, since
82
+ # the walk answers and nobody sees the difference. The answer is
83
+ # right either way, and the masked-divisor test asks the evaluator
84
+ # directly for that reason.
85
+ false
54
86
  end
55
87
 
56
88
  private
@@ -25,7 +25,7 @@ class CArray
25
25
  # How far a stencil's windows reach on each axis, as
26
26
  # [lowest, highest] per axis: what the caller walks the
27
27
  # interior by. Empty for a kernel that has no windows.
28
- :window_reach,
28
+ :window_reach, :window_reaches,
29
29
  # What a cell that stopped can have been raising about --
30
30
  # the block's own `raise`s, and those of the bodies pasted
31
31
  # into this kernel, by the code each reports.
@@ -63,6 +63,7 @@ class CArray
63
63
 
64
64
  @masked = generator.masked
65
65
  @window_reach = analyzer.window_reach
66
+ @window_reaches = analyzer.window_reaches
66
67
  # What `raise` in the block said, by the code the cell that raised
67
68
  # writes into the error slot. The message does not travel: C has
68
69
  # nothing to carry it in, and it was known when this was compiled.
@@ -8,15 +8,14 @@ class CArray
8
8
  # propagates bottom-up.
9
9
  #
10
10
  # The distinction that matters is between *storage* type and *computation*
11
- # type. A Ruby block reading a float32 array gets a Ruby Float -- a
12
- # double -- computes in double, and rounds back to float32 only when the
13
- # value is stored. Computing in float instead would diverge from the Ruby
14
- # evaluator, so reads from any float array are typed :double here and the
15
- # generator casts once, at the store.
11
+ # type: what an array holds, and what a kernel works on a cell of it in.
12
+ # They are not the same -- int8 is computed in int64 -- and the table
13
+ # below is what says which.
16
14
  #
17
- # cmplx64 stands in the same relation to cmplx128: a cell of either is a
18
- # Ruby Complex whose parts are Floats, so both are typed :complex and the
19
- # narrowing happens at the store.
15
+ # A narrow float is computed narrow: float32 in `float` and cmplx64 in
16
+ # `float _Complex`, read narrow and stored narrow. That is what CArray's
17
+ # own kernels do, so the two agree; the Ruby evaluator, which would widen
18
+ # to a double, is what they both differ from. See docs/03 and docs/05.
20
19
  class TypeAssignment
21
20
 
22
21
  STORAGE_COMPUTATION_TYPES = {
@@ -60,11 +59,14 @@ class CArray
60
59
  # the reason KINDS gives: a table of names kept somewhere else keeps
61
60
  # answering after a computation type is added, and answers wrongly.
62
61
  RESULT_STORAGE_TYPES = {
63
- :int64 => :int64,
64
- :uint64 => :uint64,
65
- :float => :float32,
66
- :double => :float64,
67
- :complex => :cmplx128,
62
+ :int64 => :int64,
63
+ :uint64 => :uint64,
64
+ :float => :float32,
65
+ :double => :float64,
66
+ # cmplx64 is to cmplx128 what float32 is to float64: the value was
67
+ # computed narrow, and cmplx64 holds it without widening or losing it.
68
+ :float_complex => :cmplx64,
69
+ :complex => :cmplx128,
68
70
  }.freeze
69
71
 
70
72
  # Raises rather than falling back, because there is no type to fall back
@@ -1,5 +1,5 @@
1
1
  class CArray
2
2
  module JIT
3
- VERSION = "0.1.0"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  end