carray-jit 0.1.3 → 0.1.5

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
@@ -233,9 +235,10 @@ class CArray
233
235
 
234
236
  held = [buffer(reals), buffer(integers), buffer(functions),
235
237
  buffer(data), masks, masks, error]
236
- context = held.map { |item| Fiddle::Pointer[item].to_i }.pack("Q*")
237
-
238
- Sweep.call(slab_pointer, fsync, arrays, Fiddle::Pointer[context])
238
+ error = in_one_block(held) { |context, slots|
239
+ Sweep.call(slab_pointer, fsync, arrays, context)
240
+ slots.last[0, error.bytesize]
241
+ }
239
242
 
240
243
  packed.each_with_index do |(array, item), index|
241
244
  next if array.equal?(item)
@@ -497,11 +500,19 @@ class CArray
497
500
  # counter, and the arrays' half, which is the shape and travels in the
498
501
  # plan. Two methods because `#call` packs the first and takes the
499
502
  # second from what it prepared earlier; `#sweep` still wants both.
503
+ # A captured Integer that meets a uint64 takes that width, and a
504
+ # negative one has none to take: it would compute as 2**64-1. CArray
505
+ # refuses the same operand on a uint64 array, and so does this.
500
506
  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*")
507
+ @integers.map { |name|
508
+ value = Integer(scalar_values.fetch(name))
509
+ if value.negative? && @unsigned_captures.include?(name)
510
+ raise RangeError, "#{value} is out of range for uint64 " \
511
+ "(`#{name}` meets a uint64)"
512
+ end
513
+ value
514
+ }.pack("q*") +
515
+ @unsigned_integers.map { |name| Integer(scalar_values.fetch(name)) }.pack("Q*")
505
516
  end
506
517
 
507
518
  def packed_extents (array_values)
@@ -548,6 +559,31 @@ class CArray
548
559
  packed.empty? ? "\0" * 8 : packed
549
560
  end
550
561
 
562
+ # Copies `parts` into one block of C memory, each part 8-byte aligned,
563
+ # after a table of their addresses, and yields a pointer to the table
564
+ # and a pointer to each part; the block is freed when this returns.
565
+ #
566
+ # For a call that is handed addresses inside other memory rather than
567
+ # arguments: a Ruby String's bytes may be moved by GC compaction, and
568
+ # an address taken from one and packed elsewhere is not one Ruby knows
569
+ # to keep in place. Memory from malloc is never moved.
570
+ def in_one_block (parts)
571
+ table = parts.size * Fiddle::SIZEOF_VOIDP
572
+ offsets = parts.each_with_object([]) { |part, list|
573
+ last = list.empty? ? table : list.last + parts[list.size - 1].bytesize
574
+ list << ((last + 7) & ~7)
575
+ }
576
+ size = offsets.last + parts.last.bytesize
577
+ Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE) do |block|
578
+ slots = parts.zip(offsets).map { |part, offset|
579
+ block[offset, part.bytesize] = part
580
+ block + offset
581
+ }
582
+ block[0, table] = slots.map(&:to_i).pack("Q*")
583
+ yield block, slots
584
+ end
585
+ end
586
+
551
587
  # The xfer tier transfers a box rather than the whole view. The box
552
588
  # is the loop range grown by how far the kernel reaches from the cell it
553
589
  # is on -- per array, because two arrays in one kernel need not be read
@@ -609,6 +645,9 @@ class CArray
609
645
  # In source order, outermost first, so an index a range is written
610
646
  # over has its own range settled before it is read.
611
647
  @inner_ranges.each do |name, (from, to, step)|
648
+ # A range read from a local or a cell has no span before the loop
649
+ # is reached; the subscripts it walks are checked at each access.
650
+ next if @unbounded_indices.include?(name)
612
651
  step ||= 1
613
652
  first = span_of(from, scalar_values, flat, ranges, name)
614
653
  last = span_of(to, scalar_values, flat, ranges, name)
@@ -765,9 +804,9 @@ class CArray
765
804
  unless array.is_a?(CArray)
766
805
  raise Unsupported, "`#{name}` is not a CArray"
767
806
  end
768
- unless array.rank == array_rank(name)
807
+ unless array.ndim == array_rank(name)
769
808
  raise Unsupported,
770
- "`#{name}` has rank #{array.rank}, but is indexed with " \
809
+ "`#{name}` has rank #{array.ndim}, but is indexed with " \
771
810
  "#{array_rank(name)} #{array_rank(name) == 1 ? 'index' : 'indices'}"
772
811
  end
773
812
  unless array.data_type_name == @storage_types.fetch(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