carray-jit 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,11 +56,32 @@ class CArray
56
56
  # only where it means something -- and Apple's linker rejects it
57
57
  # outright, which would trade a Linux crash for a macOS build that never
58
58
  # compiles at all.
59
+ #
60
+ # A compiled `jit_function` is the exception and exports no fixed name:
61
+ # its body, its helpers and the four words a window reads are static,
62
+ # and what leaves the object is the declared symbol, the shim beside it
63
+ # and the accessor -- all three carrying the body's own digest. That is
64
+ # what lets carray-jit-aot link several of them into one library.
59
65
  SYMBOLIC =
60
66
  (RbConfig::CONFIG["host_os"] =~ /darwin|mswin|mingw|cygwin/ ?
61
67
  [] : ["-Wl,-Bsymbolic"]).freeze
62
68
 
63
- FLAGS = ["-O3", "-fPIC", "-shared", "-ffp-contract=off", *SYMBOLIC].freeze
69
+ # -fwrapv makes signed overflow wrap, as CArray's own operators do and
70
+ # as the documentation promises. Without it the compiler is free to
71
+ # assume a sum cannot overflow, so one kernel contradicts itself:
72
+ # `y = -a; y > 0 ? y : 0` stored a negative value for a = INT64_MIN.
73
+ FLAGS = ["-O3", "-fPIC", "-shared", "-ffp-contract=off", "-fwrapv", *SYMBOLIC].freeze
74
+
75
+ # A compiled body calls a function borrowed with `jit_extern` by name,
76
+ # and the library it came from is one the process opened -- which is
77
+ # where the name is resolved, when the object is loaded. ELF leaves a
78
+ # shared object's undefined names to the loader as it is; Apple's
79
+ # linker refuses them unless told to, so a borrowed function from
80
+ # anywhere but libm (which `-lm` names) failed to link on macOS.
81
+ # `jit_extern` has opened the library before any body that calls it
82
+ # is compiled, so by the time the object loads the name is there.
83
+ LINK = (RbConfig::CONFIG["host_os"] =~ /darwin/ ?
84
+ ["-undefined", "dynamic_lookup"] : []).freeze
64
85
 
65
86
  # Kernels retained on disk. Each costs about 17 KB, so the default is
66
87
  # roughly 9 MB -- far more than any real program compiles, but a bound
@@ -142,7 +163,9 @@ class CArray
142
163
  if defined?(Gem::Specification)
143
164
  begin
144
165
  Gem::Specification.find_by_name("carray").version.to_s
145
- rescue StandardError
166
+ rescue StandardError, LoadError
167
+ # A gem that is not installed raises Gem::MissingSpecError,
168
+ # which is a LoadError rather than a StandardError.
146
169
  "none"
147
170
  end
148
171
  else
@@ -537,7 +560,8 @@ class CArray
537
560
  "Set CARRAY_JIT_CC to a C compiler, or leave it unset for " \
538
561
  "the one Ruby was built with."
539
562
  end
540
- command = [compiler_command, *flags, source_path, "-o", object_path, "-lm"]
563
+ command = [compiler_command, *flags, source_path, "-o", object_path,
564
+ "-lm", *LINK]
541
565
  output = IO.popen(command, err: [:child, :out]) { |io| io.read }
542
566
  unless $?.success?
543
567
  raise CompilationError, "#{command.join(' ')}\n#{output}"
@@ -11,8 +11,15 @@ class CArray
11
11
  # nothing here restates any of that: it substitutes operands into bodies
12
12
  # it was given and wraps the result in a loop.
13
13
  #
14
- # Declining is ordinary. An expression this cannot address -- an operand
15
- # that is not laid out end to end, a data type with no C to write it in --
14
+ # An operand need not be laid out end to end. A slice, a transpose or a
15
+ # broadcast column is read where it lies, by the byte strides CArray
16
+ # lends for it -- a broadcast axis is a stride of 0 -- and a view with no
17
+ # strides of its own is lent as a packed copy of it. The strides are
18
+ # handed over at the call, so one kernel serves every view of the same
19
+ # kind; which operands are read that way is part of what it is cached by.
20
+ #
21
+ # Declining is ordinary. An expression this cannot address -- a data
22
+ # type with no C to write it in, a destination whose cells overlap --
16
23
  # goes back to CArray, which walks it and arrives at the same answer.
17
24
  class Expression
18
25
 
@@ -54,14 +61,22 @@ class CArray
54
61
  def call (plan, out)
55
62
  return false unless C_TYPES.key?(plan.data_type)
56
63
  aliased = plan.leaves.any? { |array| array.equal?(out) }
57
- kernel = kernel_for(plan, aliased) or return false
64
+ # A shifted read reaches cells other than the one being written, so
65
+ # over its own output it would read what it has just written.
66
+ return false if aliased && shifted_leaves(plan).any? { |i| plan.leaves[i].equal?(out) }
67
+ return false unless out.dim == plan.dim
58
68
  arrays = [out, *plan.leaves]
59
69
  writable = [true, *Array.new(plan.leaves.size, false)]
60
70
  Access.open(arrays, writable) do |bases|
61
- return false unless bases.each_with_index.all? { |basis, i|
62
- basis[:strides] == end_to_end(arrays[i])
63
- }
64
- kernel.call(out.elements, *pointers(plan, bases))
71
+ strided = strided_slots(arrays, bases) or return false
72
+ kernel = kernel_for(plan, aliased, strided) or return false
73
+ # Held here so that the buffers outlive the call.
74
+ dim = out.dim.pack("q*")
75
+ steps = strided.flat_map { |slot| slot_strides(bases, slot) }.pack("q*")
76
+ args = [out.elements]
77
+ args << Fiddle::Pointer[dim] if walks_axes?(plan, strided)
78
+ args << Fiddle::Pointer[steps] unless strided.empty?
79
+ kernel.call(*args, *pointers(plan, bases))
65
80
  end
66
81
  true
67
82
  rescue ZeroDivisionError
@@ -88,27 +103,46 @@ class CArray
88
103
  private
89
104
 
90
105
  # An expression of the same shape compiles to the same kernel whatever
91
- # arrays it is over, which is what the plan's signature says.
92
- def kernel_for (plan, aliased)
93
- @kernels.fetch([plan.signature, aliased]) do
94
- @kernels[[plan.signature, aliased]] = compile(plan, aliased)
106
+ # arrays it is over, which is what the plan's signature says -- given
107
+ # the same operands read by strides. The strides themselves are
108
+ # arguments, not part of the kernel.
109
+ def kernel_for (plan, aliased, strided)
110
+ key = [plan.signature, aliased, strided]
111
+ @kernels.fetch(key) do
112
+ @kernels[key] = compile(plan, aliased, strided)
95
113
  end
96
114
  end
97
115
 
98
- def compile (plan, aliased)
116
+ def compile (plan, aliased, strided)
117
+ @strided = strided
99
118
  source = source_for(plan, aliased) or return nil
100
119
  handle, = Compiler.build(source, "carray_jit_expression", flags: FLAGS)
101
120
  Fiddle::Function.new(handle["carray_jit_expression"],
102
121
  [Fiddle::TYPE_LONG_LONG] +
103
- [Fiddle::TYPE_VOIDP] * (1 + arity(plan)),
122
+ [Fiddle::TYPE_VOIDP] * (1 + arity(plan, strided)),
104
123
  Fiddle::TYPE_VOID)
105
124
  rescue CompilationError
106
125
  nil
107
126
  end
108
127
 
109
- def arity (plan)
128
+ def arity (plan, strided)
110
129
  plan.leaves.size + plan.leaves.count { |array| array.has_mask? } +
111
- (plan.masked ? 1 : 0)
130
+ (plan.masked ? 1 : 0) + (walks_axes?(plan, strided) ? 1 : 0) +
131
+ (strided.empty? ? 0 : 1)
132
+ end
133
+
134
+ # A plan that reads an operand shifted, or by strides, needs to know
135
+ # where along each axis the cell is; any other is one flat loop.
136
+ def walks_axes? (plan, strided)
137
+ shifted?(plan) || ! strided.empty?
138
+ end
139
+
140
+ def shifted? (plan)
141
+ plan.nodes.any? { |node| node.is_a?(CArray::Fusion::Shifted) }
142
+ end
143
+
144
+ def shifted_leaves (plan)
145
+ plan.nodes.grep(CArray::Fusion::Shifted).map(&:index)
112
146
  end
113
147
 
114
148
  def pointers (plan, bases)
@@ -121,23 +155,80 @@ class CArray
121
155
  args
122
156
  end
123
157
 
124
- def end_to_end (array)
125
- steps = Array.new(array.ndim)
126
- step = array.bytes
127
- (array.ndim - 1).downto(0) do |axis|
158
+ def end_to_end (dim, bytes)
159
+ steps = Array.new(dim.size)
160
+ step = bytes
161
+ (dim.size - 1).downto(0) do |axis|
128
162
  steps[axis] = step
129
- step *= array.dim[axis]
163
+ step *= dim[axis]
130
164
  end
131
165
  steps
132
166
  end
133
167
 
168
+ # The C names of the pointers that are not laid out end to end, in the
169
+ # order their strides are handed over: the destination, its mask, then
170
+ # each operand and its mask. Nil where the destination has two cells
171
+ # at one address -- writing them one at a time is not what the walk
172
+ # does -- or where an operand is not the shape of the destination.
173
+ def strided_slots (arrays, bases)
174
+ slots = []
175
+ bases.each_with_index do |basis, i|
176
+ return nil unless basis[:dim] == arrays[0].dim
177
+ data, mask = i == 0 ? ["out", "out_mask"] : ["a#{i - 1}", "k#{i - 1}"]
178
+ if i == 0
179
+ return nil unless one_to_one?(basis[:strides], basis[:dim], basis[:bytes])
180
+ return nil if basis[:mask_strides] && ! one_to_one?(basis[:mask_strides], basis[:dim], 1)
181
+ end
182
+ slots << data unless basis[:strides] == end_to_end(basis[:dim], basis[:bytes])
183
+ if basis[:mask_strides] && basis[:mask_strides] != end_to_end(basis[:dim], 1)
184
+ slots << mask
185
+ end
186
+ end
187
+ slots.freeze
188
+ end
189
+
190
+ def slot_strides (bases, slot)
191
+ index = slot.start_with?("out") ? 0 : slot[1..].to_i + 1
192
+ slot.start_with?("k", "out_mask") ? bases[index][:mask_strides] : bases[index][:strides]
193
+ end
194
+
195
+ # Whether every cell has an address of its own. Taken axis by axis
196
+ # from the shortest stride up, each has to step past everything the
197
+ # ones below it reach. A layout that interleaves axes without sharing
198
+ # an address fails this too, and is declined though it need not be.
199
+ def one_to_one? (strides, dim, bytes)
200
+ reach = bytes
201
+ strides.zip(dim).reject { |_, d| d == 1 }.sort_by { |s, _| s.abs }.all? do |s, d|
202
+ fits = s.abs >= reach
203
+ reach += s.abs * (d - 1)
204
+ fits
205
+ end
206
+ end
207
+
208
+ # A cell of the array `slot` names: at the flat index `flat` where it
209
+ # is laid out end to end, and at the sum of `axes` times its strides
210
+ # where it is not.
211
+ def cell (slot, type, flat, axes)
212
+ return "#{slot}[#{flat}]" unless @strided.include?(slot)
213
+ offset = axes.each_with_index.map { |a, k| "(#{a}) * s_#{slot}_#{k}" }.join(" + ")
214
+ qualifier = slot.start_with?("out") ? "" : "const "
215
+ "*(#{qualifier}#{type} *) ((#{qualifier}char *) #{slot} + #{offset})"
216
+ end
217
+
218
+ def axes (plan)
219
+ (0...plan.dim.size).map { |k| "i#{k}" }
220
+ end
221
+
134
222
  # -- the C ------------------------------------------------------------
135
223
 
136
224
  def source_for (plan, aliased)
137
225
  out_type = C_TYPES.fetch(plan.data_type)
138
226
  restrict = aliased ? "" : "restrict "
139
- body = plan.nodes.each_with_index.map { |node, i| line(plan, node, i) }
140
- return nil if body.any?(&:nil?)
227
+ body = statements(plan, :edge) or return nil
228
+ last = plan.nodes.size - 1
229
+ result = ["#{cell("out", out_type, "n", axes(plan))} = v#{last};",
230
+ *(plan.masked ? ["#{cell("out_mask", "uint8_t", "n", axes(plan))} = m#{last};"] : [])]
231
+ walking = walks_axes?(plan, @strided)
141
232
  <<~C
142
233
  #include <math.h>
143
234
  #include <stdlib.h>
@@ -153,17 +244,92 @@ class CArray
153
244
  typedef float float32_t;
154
245
  typedef double float64_t;
155
246
 
247
+ #{helpers}
156
248
  void
157
- carray_jit_expression (int64_t elements, #{out_type} *#{restrict}out#{mask_parameter(plan, restrict)}#{parameters(plan, restrict)})
249
+ carray_jit_expression (int64_t elements#{walking ? ", const int64_t *dim" : ""}#{@strided.empty? ? "" : ", const int64_t *steps"}, #{out_type} *#{restrict}out#{mask_parameter(plan, restrict)}#{parameters(plan, restrict)})
158
250
  {
159
- for ( int64_t n = 0; n < elements; n++ ) {
160
- #{body.flatten.map { |l| " " + l }.join("\n")}
161
- out[n] = v#{plan.nodes.size - 1};#{plan.masked ? "\n out_mask[n] = m#{plan.nodes.size - 1};" : ""}
162
- }
251
+ #{stride_lines(plan).join("\n")}
252
+ #{loop(plan, body + result, walking && (statements(plan, :inside) + result)).join("\n")}
163
253
  }
164
254
  C
165
255
  end
166
256
 
257
+ # The C for every node, in order. `where` matters only to a shifted
258
+ # read: at the :edge it may fall outside the array, :inside it cannot.
259
+ def statements (plan, where)
260
+ @where = where
261
+ body = plan.nodes.each_with_index.map { |node, i| line(plan, node, i) }
262
+ body.any?(&:nil?) ? nil : body.flatten
263
+ end
264
+
265
+ # The strides handed over, one per axis for each slot, named.
266
+ def stride_lines (plan)
267
+ ndim = plan.dim.size
268
+ @strided.each_with_index.flat_map { |slot, j|
269
+ (0...ndim).map { |k| " const int64_t s_#{slot}_#{k} = steps[#{j * ndim + k}];" }
270
+ }
271
+ end
272
+
273
+ # Cell by cell in storage order. A plan that reads an array shifted
274
+ # or by strides needs to know where along each axis the cell is, so it
275
+ # walks the outer axes and then the last. A shifted read splits the
276
+ # last one in three: the stretch in the middle, where no shifted read
277
+ # can leave the array, is a plain loop the compiler can vectorise, and
278
+ # only the ends check.
279
+ def loop (plan, edge, inside = nil)
280
+ unless inside
281
+ return [" for ( int64_t n = 0; n < elements; n++ ) {",
282
+ *edge.map { |l| " " + l }, " }"]
283
+ end
284
+ ndim = plan.dim.size
285
+ last = ndim - 1
286
+ offsets = plan.nodes.grep(CArray::Fusion::Shifted).map(&:offset)
287
+ lines = (0...ndim).map { |k| " const int64_t d#{k} = dim[#{k}];" }
288
+ plan.nodes.each_with_index do |node, i|
289
+ next unless node.is_a?(CArray::Fusion::Shifted)
290
+ flat = (1...ndim).reduce("(int64_t)(#{node.offset[0]})") { |acc, k|
291
+ "(#{acc}) * d#{k} + (#{node.offset[k]})"
292
+ }
293
+ lines << " const int64_t o#{i} = #{flat};"
294
+ end
295
+ (0...ndim).each do |k|
296
+ break if offsets.empty?
297
+ below = [0, *offsets.map { |o| -o[k] }].max
298
+ above = [0, *offsets.map { |o| o[k] }].max
299
+ lines << " const int64_t lo#{k} = d#{k} < #{below} ? d#{k} : #{below};"
300
+ lines << " const int64_t hi#{k} = d#{k} - #{above} > lo#{k} ? d#{k} - #{above} : lo#{k};"
301
+ end
302
+ indent = " "
303
+ (0...last).each do |k|
304
+ lines << "#{indent}for ( int64_t i#{k} = 0; i#{k} < d#{k}; i#{k}++ ) {"
305
+ indent += " "
306
+ end
307
+ row = (0...last).reduce("(int64_t) 0") { |acc, k| "(#{acc}) * d#{k} + i#{k}" }
308
+ row = "(#{row}) * d#{last}"
309
+ outer = (0...last).map { |k| "i#{k} >= lo#{k} && i#{k} < hi#{k}" }
310
+ lines << "#{indent}const int64_t row = #{row};"
311
+ unless offsets.empty?
312
+ lines << "#{indent}const int64_t a = #{outer.empty? ? "lo#{last}" : "(#{outer.join(" && ")}) ? lo#{last} : d#{last}"};"
313
+ lines << "#{indent}const int64_t b = a < hi#{last} ? hi#{last} : a;"
314
+ end
315
+ stretches = if offsets.empty?
316
+ [["0", "d#{last}", edge]]
317
+ else
318
+ [["0", "a", edge], ["a", "b", inside], ["b", "d#{last}", edge]]
319
+ end
320
+ stretches.each do |from, to, body|
321
+ lines << "#{indent}for ( int64_t i#{last} = #{from}; i#{last} < #{to}; i#{last}++ ) {"
322
+ lines << "#{indent} const int64_t n = row + i#{last};"
323
+ lines.concat(body.map { |l| "#{indent} " + l })
324
+ lines << "#{indent}}"
325
+ end
326
+ (last - 1).downto(0) do |k|
327
+ indent = indent[2..]
328
+ lines << "#{indent}}"
329
+ end
330
+ lines
331
+ end
332
+
167
333
  def mask_parameter (plan, restrict)
168
334
  plan.masked ? ", uint8_t *#{restrict}out_mask" : ""
169
335
  end
@@ -180,31 +346,100 @@ class CArray
180
346
  type = C_TYPES[node.data_type] or return nil
181
347
  case node
182
348
  when CArray::Fusion::Leaf
183
- ["#{type} v#{i} = a#{node.index}[n];",
184
- *(plan.masked ? ["uint8_t m#{i} = #{node.masked ? "k#{node.index}[n]" : "0"};"] : [])]
349
+ ["#{type} v#{i} = #{cell("a#{node.index}", type, "n", axes(plan))};",
350
+ *(plan.masked ? ["uint8_t m#{i} = #{node.masked ? cell("k#{node.index}", "uint8_t", "n", axes(plan)) : "0"};"] : [])]
351
+ when CArray::Fusion::Shifted
352
+ shifted_line(plan, node, i, type)
185
353
  when CArray::Fusion::Const
186
354
  ["#{type} v#{i} = #{literal(node)};",
187
355
  *(plan.masked ? ["uint8_t m#{i} = 0;"] : [])]
188
356
  when CArray::Fusion::Op
189
357
  statement = substitute(node, i, type) or return nil
190
- [*(plan.masked ? [mask_line(node, i)] : []),
358
+ # A mask rule this gem does not know is a plan it declines.
359
+ mask = plan.masked ? (mask_line(node, i) or return nil) : nil
360
+ [*mask,
191
361
  "#{type} v#{i};",
192
362
  *guarded(node, i, statement, plan.masked)]
193
363
  end
194
364
  end
195
365
 
366
+ # The cell `offset` away, read from an index clamped into the array so
367
+ # that the load is always in bounds, then replaced where it fell out.
368
+ def shifted_line (plan, node, i, type)
369
+ bounds = node.bounds.uniq
370
+ return nil unless bounds.size == 1
371
+ ndim = node.offset.size
372
+ if @where == :inside
373
+ moved = (0...ndim).map { |k| "i#{k} + (#{node.offset[k]})" }
374
+ return ["#{type} v#{i} = #{cell("a#{node.index}", type, "n + o#{i}", moved)};",
375
+ *(plan.masked ? ["uint8_t m#{i} = #{node.masked ? cell("k#{node.index}", "uint8_t", "n + o#{i}", moved) : "0"};"] : [])]
376
+ end
377
+ index = (0...ndim).map { |k| "s#{i}_#{k}" }
378
+ lines = node.offset.each_with_index.map { |o, k|
379
+ "int64_t #{index[k]} = i#{k} + (#{o});"
380
+ }
381
+ inside = (0...ndim).map { |k| "#{index[k]} >= 0 && #{index[k]} < d#{k}" }.join(" && ")
382
+ lines << "int s#{i}_in = #{inside};"
383
+ (0...ndim).each { |k|
384
+ lines << "int64_t c#{i}_#{k} = #{index[k]} < 0 ? 0 : (#{index[k]} >= d#{k} ? d#{k} - 1 : #{index[k]});"
385
+ }
386
+ flat = (1...ndim).reduce("c#{i}_0") { |acc, k| "(#{acc}) * d#{k} + c#{i}_#{k}" }
387
+ clamped = (0...ndim).map { |k| "c#{i}_#{k}" }
388
+ lines << "#{type} v#{i} = #{cell("a#{node.index}", type, flat, clamped)};"
389
+ if bounds.first == :fill
390
+ fill = CArray::Fusion::Const.new(node.fill, node.data_type)
391
+ lines << "if ( ! s#{i}_in ) { v#{i} = #{literal(fill)}; }"
392
+ end
393
+ if plan.masked
394
+ inner = node.masked ? cell("k#{node.index}", "uint8_t", flat, clamped) : "0"
395
+ outer = bounds.first == :mask ? "1" : "0"
396
+ lines << "uint8_t m#{i} = s#{i}_in ? #{inner} : #{outer};"
397
+ end
398
+ lines
399
+ end
400
+
196
401
  def literal (node)
197
402
  case node.data_type
198
- when :float64, :float32 then "%.17g" % node.value
199
- when :boolean then node.value ? 1 : 0
403
+ when :float64, :float32 then float_literal(node.value.to_f)
404
+ when :boolean then truth(node.value) ? 1 : 0
200
405
  else node.value.to_s
201
406
  end
202
407
  end
203
408
 
409
+ # `%.17g` writes -0.0 as "-0", which C reads as the integer 0 negated,
410
+ # and writes NaN and the infinities as words C does not know.
411
+ def float_literal (value)
412
+ return "NAN" if value.nan?
413
+ return (value > 0 ? "INFINITY" : "(-INFINITY)") if value.infinite?
414
+ text = "%.17g" % value
415
+ text.match?(/[.e]/) ? text : "#{text}.0"
416
+ end
417
+
418
+ # A boolean cell as CArray stores it: 0 is false, as is false.
419
+ def truth (value)
420
+ value.is_a?(Numeric) ? value != 0 : !!value
421
+ end
422
+
423
+ # `<type>` names a helper by CArray's spelling of the type -- the
424
+ # float64_t of op_powi_float64_t -- not by its C spelling.
204
425
  def substitute (node, i, type)
205
426
  text = node.body.dup
206
427
  node.args.each_with_index { |arg, k| text = text.gsub("##{k + 1}", "v#{arg}") }
207
- text.gsub("##{node.args.size + 1}", "v#{i}").gsub("<type>", type).lines.map(&:strip)
428
+ text.gsub("##{node.args.size + 1}", "v#{i}")
429
+ .gsub("<type>", carray_type(node.data_type, type)).lines.map(&:strip)
430
+ end
431
+
432
+ def carray_type (data_type, c_type)
433
+ case data_type
434
+ when :float32, :float64 then "#{data_type}_t"
435
+ else c_type
436
+ end
437
+ end
438
+
439
+ # The C a body may call beyond the standard library, as CArray hands it
440
+ # over.
441
+ def helpers
442
+ CArray.__kernel_helpers__
208
443
  end
209
444
 
210
445
  # A masked cell is not computed where computing it would raise: the
@@ -220,6 +455,9 @@ class CArray
220
455
  case node.mask
221
456
  when :pass then "uint8_t m#{i} = m#{args[0]};"
222
457
  when :union then "uint8_t m#{i} = #{args.map { |a| "m#{a}" }.join(" | ")};"
458
+ # The condition's mask, else the mask of the operand it chooses.
459
+ when :select
460
+ "uint8_t m#{i} = m#{args[0]} | (v#{args[0]} ? m#{args[1]} : m#{args[2]});"
223
461
  when :kleene_or, :kleene_and
224
462
  known = if node.mask == :kleene_or
225
463
  "((!m#{args[0]} && v#{args[0]}) || (!m#{args[1]} && v#{args[1]}))"
@@ -45,6 +45,7 @@ class CArray
45
45
  @integers = generator.integers
46
46
  @complexes = generator.complexes
47
47
  @unsigned_integers = generator.unsigned_integers
48
+ @unsigned_captures = generator.unsigned_captures
48
49
  # Per array, the axes read at an index only the running kernel knows,
49
50
  # and the extents it checks them against.
50
51
  @extent_slots = generator.extent_slots
@@ -55,6 +56,7 @@ class CArray
55
56
  @written_arrays = analyzer.written_arrays
56
57
  @array_ranks = analyzer.array_ranks
57
58
  @inner_ranges = analyzer.inner_ranges
59
+ @unbounded_indices = analyzer.unbounded_indices
58
60
  # An index the block wrote twice has two identifiers here; the
59
61
  # messages speak the name it was written with.
60
62
  @index_sources = analyzer.index_sources
@@ -497,11 +499,19 @@ class CArray
497
499
  # counter, and the arrays' half, which is the shape and travels in the
498
500
  # plan. Two methods because `#call` packs the first and takes the
499
501
  # second from what it prepared earlier; `#sweep` still wants both.
502
+ # A captured Integer that meets a uint64 takes that width, and a
503
+ # negative one has none to take: it would compute as 2**64-1. CArray
504
+ # refuses the same operand on a uint64 array, and so does this.
500
505
  def packed_scalar_integers (scalar_values)
501
- @integers.map { |name| Integer(scalar_values.fetch(name)) }.pack("q*") +
502
- @unsigned_integers.map { |name|
503
- Integer(scalar_values.fetch(name))
504
- }.pack("Q*")
506
+ @integers.map { |name|
507
+ value = Integer(scalar_values.fetch(name))
508
+ if value.negative? && @unsigned_captures.include?(name)
509
+ raise RangeError, "#{value} is out of range for uint64 " \
510
+ "(`#{name}` meets a uint64)"
511
+ end
512
+ value
513
+ }.pack("q*") +
514
+ @unsigned_integers.map { |name| Integer(scalar_values.fetch(name)) }.pack("Q*")
505
515
  end
506
516
 
507
517
  def packed_extents (array_values)
@@ -609,6 +619,9 @@ class CArray
609
619
  # In source order, outermost first, so an index a range is written
610
620
  # over has its own range settled before it is read.
611
621
  @inner_ranges.each do |name, (from, to, step)|
622
+ # A range read from a local or a cell has no span before the loop
623
+ # is reached; the subscripts it walks are checked at each access.
624
+ next if @unbounded_indices.include?(name)
612
625
  step ||= 1
613
626
  first = span_of(from, scalar_values, flat, ranges, name)
614
627
  last = span_of(to, scalar_values, flat, ranges, name)
@@ -153,6 +153,21 @@ class CArray
153
153
  end
154
154
  end
155
155
 
156
+ # Whether a pointer parameter was handed an address at all: `grad.nil?`,
157
+ # `grad == nil`, and `if grad` where a condition is read. A callback's
158
+ # caller decides that per call -- NLopt hands a derivative-free method's
159
+ # objective no gradient to fill -- so it is a question the body asks,
160
+ # not one the declaration can settle. `null` is true for the spelling
161
+ # that asks whether it is missing.
162
+ class PointerTest < Node
163
+ attr_reader :name, :null
164
+ def initialize (name, null, location = nil)
165
+ super(location)
166
+ @name = name
167
+ @null = null
168
+ end
169
+ end
170
+
156
171
  # Zero of whatever type another expression has.
157
172
  #
158
173
  # A sum has to start from a zero of the summand's type: start it from an
@@ -361,6 +361,8 @@ class CArray
361
361
  when MaskTest
362
362
  walk_subscripts(node.subscripts)
363
363
  node.type = :boolean
364
+ when PointerTest
365
+ node.type = :boolean
364
366
  when NumericPredicate
365
367
  walk(node.operand)
366
368
  node.type = :boolean
@@ -430,7 +432,9 @@ class CArray
430
432
  node.alternative.each { |statement| walk(statement) }
431
433
  @bindings = merge_bindings(from_consequent, @bindings)
432
434
  when IntegerLiteral
433
- node.type = :int64
435
+ # Above 2**63-1 a literal is a uint64, as a captured Integer is;
436
+ # where it then meets an int64 it is refused (#refuse_wide_literal).
437
+ node.type = TypeAssignment.integer_type(node.value)
434
438
  when FloatLiteral
435
439
  node.type = :double
436
440
  when ImaginaryLiteral
@@ -565,6 +569,14 @@ class CArray
565
569
  walk(node.right)
566
570
  node.type =
567
571
  if Analyzer::COMPARISON_OPERATORS.include?(node.operator)
572
+ # Compared in the wider of the two, which for a uint64 against a
573
+ # negative literal is a uint64 the literal does not fit.
574
+ if [node.left.type, node.right.type].include?(:uint64)
575
+ refuse_negative_literal(node.left, node.right)
576
+ refuse_negative_literal(node.right, node.left)
577
+ refuse_wide_literal(node.left, node.right)
578
+ refuse_wide_literal(node.right, node.left)
579
+ end
568
580
  :boolean
569
581
  elsif [:<<, :>>].include?(node.operator)
570
582
  # A shift takes its count as a number and keeps the type of the
@@ -813,6 +825,11 @@ class CArray
813
825
  "too. Store `.real`, `.imag` or `.abs`",
814
826
  location)
815
827
  end
828
+ if wanted == :int64 && expression.is_a?(IntegerLiteral) &&
829
+ expression.value > INT64_MAX
830
+ raise Unsupported.new(wide_literal_message(expression.value),
831
+ location)
832
+ end
816
833
  return if !boolean?(wanted) && numeric?(given)
817
834
  if boolean?(wanted) && expression.is_a?(IntegerLiteral) &&
818
835
  [0, 1].include?(expression.value)
@@ -873,6 +890,8 @@ class CArray
873
890
  return scalar.type if KIND_RANK[scalar_kind] > KIND_RANK[typed_kind]
874
891
  if scalar_kind == typed_kind
875
892
  refuse_wide_capture(scalar, typed)
893
+ refuse_negative_literal(scalar, typed)
894
+ refuse_wide_literal(scalar, typed)
876
895
  # The scalar becomes that type rather than being widened to meet it,
877
896
  # and says so: a literal emitted as a double would take the C
878
897
  # expression back to double however this node is typed.
@@ -881,6 +900,37 @@ class CArray
881
900
  typed.type
882
901
  end
883
902
 
903
+ # A negative literal meeting a uint64 would take that width and compute
904
+ # as 2**64 minus its magnitude. CArray refuses the same operand on a
905
+ # uint64 array (`RangeError: -1 is out of range for uint64`); a captured
906
+ # one is refused when the kernel is called, since its value is not part
907
+ # of the kernel.
908
+ def refuse_negative_literal (scalar, typed)
909
+ return unless scalar.is_a?(IntegerLiteral) && scalar.value.negative? &&
910
+ typed.type == :uint64
911
+ raise Unsupported.new(
912
+ "#{scalar.value} is out of range for uint64: a negative Integer " \
913
+ "meeting a uint64 would compute as 2**64#{scalar.value}. CArray " \
914
+ "refuses the same operand. Convert the uint64 side first, " \
915
+ "`x.to_f` or a signed capture", scalar.location)
916
+ end
917
+
918
+ # An integer literal above 2**63-1 computed as an int64, where it would
919
+ # wrap to a negative number. In Ruby it is an Integer, which has no
920
+ # width; meeting a uint64 or a Float it keeps its value and is accepted.
921
+ def refuse_wide_literal (scalar, typed)
922
+ return unless scalar.is_a?(IntegerLiteral) &&
923
+ scalar.value > INT64_MAX && typed.type == :int64
924
+ raise Unsupported.new(wide_literal_message(scalar.value),
925
+ scalar.location)
926
+ end
927
+
928
+ def wide_literal_message (value)
929
+ "#{value} does not fit an int64, which is what it is computed in " \
930
+ "here, and would wrap to a negative number. Meet it with a uint64 " \
931
+ "or a Float, or store it into a uint64 array"
932
+ end
933
+
884
934
  # A captured Integer above int64 meeting an integer, which absorption
885
935
  # has no answer for: the scalar is supposed to take the other side's
886
936
  # width, and this value did not come from a width -- it came from a Ruby
@@ -932,6 +982,8 @@ class CArray
932
982
  # Nothing to check: no value is computed.
933
983
  when MaskTest
934
984
  # Nothing to check: it reads a mask byte.
985
+ when PointerTest
986
+ # Nothing to check: it compares an address with NULL.
935
987
  when Clamp
936
988
  node.children.each { |child| verify(child) }
937
989
  types = node.children.map(&:type)
@@ -1,5 +1,5 @@
1
1
  class CArray
2
2
  module JIT
3
- VERSION = "0.1.3"
3
+ VERSION = "0.1.4"
4
4
  end
5
5
  end