carray-jit 0.1.0

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +10 -0
  3. data/CHANGELOG.md +84 -0
  4. data/LICENSE +21 -0
  5. data/README.md +88 -0
  6. data/bin/carray-jit +194 -0
  7. data/carray-jit.gemspec +41 -0
  8. data/docs/00_Introduction.md +40 -0
  9. data/docs/01_GettingStarted.md +80 -0
  10. data/docs/02_KernelShapes.md +397 -0
  11. data/docs/03_SupportedFeatures.md +595 -0
  12. data/docs/04_Compiling.md +234 -0
  13. data/docs/05_DesignNotes.md +136 -0
  14. data/docs/06_Cheatsheet.md +177 -0
  15. data/examples/README.md +56 -0
  16. data/examples/applications/game_of_life.rb +161 -0
  17. data/examples/applications/heat_equation.rb +117 -0
  18. data/examples/applications/kepler.rb +178 -0
  19. data/examples/applications/mandelbrot.rb +151 -0
  20. data/examples/applications/moving_average.rb +124 -0
  21. data/examples/applications/partial_sums.rb +141 -0
  22. data/examples/applications/point_cloud.rb +110 -0
  23. data/examples/applications/quicksort.rb +118 -0
  24. data/examples/applications/recursion.rb +121 -0
  25. data/examples/applications/relaxation.rb +115 -0
  26. data/examples/applications/sensor_gaps.rb +118 -0
  27. data/examples/applications/sieve.rb +95 -0
  28. data/examples/applications/sobel_edges.rb +80 -0
  29. data/examples/features/01_element_wise.rb +69 -0
  30. data/examples/features/02_stencil.rb +40 -0
  31. data/examples/features/03_recurrence.rb +50 -0
  32. data/examples/features/04_thomas.rb +81 -0
  33. data/examples/features/05_reduction.rb +90 -0
  34. data/examples/features/06_jit_contract.rb +58 -0
  35. data/examples/features/07_masks.rb +55 -0
  36. data/examples/features/08_views.rb +46 -0
  37. data/examples/features/09_inspecting.rb +55 -0
  38. data/examples/features/10_complex.rb +107 -0
  39. data/examples/features/11_c_functions.rb +260 -0
  40. data/examples/features/12_sweep.rb +139 -0
  41. data/examples/features/13_cscalar.rb +80 -0
  42. data/examples/features/14_stencil_window.rb +106 -0
  43. data/examples/features/15_loops.rb +148 -0
  44. data/examples/features/16_raising.rb +69 -0
  45. data/ext/carray_jit_access/carray_jit_access.c +460 -0
  46. data/ext/carray_jit_access/extconf.rb +8 -0
  47. data/lib/carray/jit/analyzer.rb +1847 -0
  48. data/lib/carray/jit/block_reader.rb +139 -0
  49. data/lib/carray/jit/c_function.rb +777 -0
  50. data/lib/carray/jit/c_generator.rb +2305 -0
  51. data/lib/carray/jit/compiler.rb +468 -0
  52. data/lib/carray/jit/errors.rb +37 -0
  53. data/lib/carray/jit/expression.rb +202 -0
  54. data/lib/carray/jit/kernel.rb +509 -0
  55. data/lib/carray/jit/node.rb +573 -0
  56. data/lib/carray/jit/sweep.rb +97 -0
  57. data/lib/carray/jit/type_assignment.rb +811 -0
  58. data/lib/carray/jit/version.rb +5 -0
  59. data/lib/carray/jit.rb +1210 -0
  60. metadata +139 -0
@@ -0,0 +1,509 @@
1
+ require "fiddle"
2
+ require "carray/jit/sweep"
3
+
4
+ class CArray
5
+ module JIT
6
+
7
+ # A compiled kernel, bound to one set of array data types and one set of
8
+ # scalar types.
9
+ #
10
+ # Every kernel has the same C signature, so the Fiddle::Function shape is
11
+ # fixed and the per-kernel detail travels in the six buffers.
12
+ class CompiledKernel
13
+
14
+ # @!attribute [r] source
15
+ # @return [String] the block's Ruby source, as it was read.
16
+ # @!attribute [r] c_source
17
+ # @return [String] the C this kernel was compiled from.
18
+ # @!attribute [r] compiled
19
+ # @return [Boolean] whether this call invoked the compiler, rather
20
+ # than reusing a cached object.
21
+ attr_reader :source, :c_source, :arrays, :storage_types, :reals,
22
+ :integers, :complexes, :rank, :index_names, :written_arrays,
23
+ :compiled, :masked, :directions, :contracted_names,
24
+ :index_axes,
25
+ # How far a stencil's windows reach on each axis, as
26
+ # [lowest, highest] per axis: what the caller walks the
27
+ # interior by. Empty for a kernel that has no windows.
28
+ :window_reach,
29
+ # What a cell that stopped can have been raising about --
30
+ # the block's own `raise`s, and those of the bodies pasted
31
+ # into this kernel, by the code each reports.
32
+ :raise_messages
33
+
34
+ def initialize (source:, generator:, analyzer:, storage_types:)
35
+ @source = source
36
+ @c_source = generator.provenance + generator.generate
37
+ @arrays = generator.arrays
38
+ @reals = generator.reals
39
+ @integers = generator.integers
40
+ @complexes = generator.complexes
41
+ # Per array, the axes read at an index only the running kernel knows,
42
+ # and the extents it checks them against.
43
+ @extent_slots = generator.extent_slots
44
+ @dynamic_arrays = @extent_slots.map(&:first).uniq
45
+ @storage_types = storage_types
46
+ @rank = analyzer.rank
47
+ @index_names = analyzer.index_names
48
+ @written_arrays = analyzer.written_arrays
49
+ @array_ranks = analyzer.array_ranks
50
+ @inner_ranges = analyzer.inner_ranges
51
+ @axis_uses = @arrays.to_h { |array|
52
+ [array, (0...array_rank(array)).map { |axis| analyzer.axis_use(array, axis) }]
53
+ }
54
+ @contracted_names = analyzer.contracted_names
55
+ # Where each index appears, so that a contraction can take its extents
56
+ # from the arrays and say so when they disagree.
57
+ @index_axes = Hash.new { |hash, key| hash[key] = [] }
58
+ @axis_uses.each do |array, uses|
59
+ uses.each_with_index do |(walkers, _), axis|
60
+ walkers.each { |index, _, _| @index_axes[index] << [array, axis] }
61
+ end
62
+ end
63
+
64
+ @masked = generator.masked
65
+ @window_reach = analyzer.window_reach
66
+ # What `raise` in the block said, by the code the cell that raised
67
+ # writes into the error slot. The message does not travel: C has
68
+ # nothing to carry it in, and it was known when this was compiled.
69
+ @raise_messages = generator.raise_messages
70
+ # Kept so the sweep entry point can be built from it if one is ever
71
+ # asked for. Compiling it eagerly would pay for a road most kernels
72
+ # never take.
73
+ @generator = generator
74
+ # The names of the C functions the block called, in the order their
75
+ # addresses are packed into the `functions` buffer. Names, not the
76
+ # functions themselves: a borrowed function is keyed on its signature
77
+ # alone, so this kernel is shared by every function of that shape and
78
+ # the address has to come from the call rather than from the build.
79
+ @c_function_names = generator.address_functions.keys
80
+ # Arrays handed to a C function whole, in the order their addresses
81
+ # are packed into `data`, and what each was promised to be.
82
+ @address_arrays = generator.address_arrays
83
+ @address_parameters = generator.address_parameters
84
+ # One translation unit holds both entry points -- the kernel and the
85
+ # wrapper CArray's sweep calls -- and one build produces both. They
86
+ # were two builds of the same body until the second was noticed: the
87
+ # wrapper's source already contains the kernel verbatim, so compiling
88
+ # it separately meant compiling everything twice and throwing the
89
+ # first object away whenever the pass swept, which is the usual case
90
+ # for jit_each. About 230 ms per call site, and an entry in the
91
+ # cache that was never opened.
92
+ @source_text = generator.provenance + generator.generate_slab(SLAB_NAME)
93
+ @handle, @compiled = Compiler.build(generator.generate_slab(SLAB_NAME),
94
+ CGenerator::FUNCTION_NAME,
95
+ header: generator.provenance)
96
+ @function = Fiddle::Function.new(@handle[CGenerator::FUNCTION_NAME],
97
+ [Fiddle::TYPE_VOIDP] * 10,
98
+ Fiddle::TYPE_VOID)
99
+ end
100
+
101
+ # `bounds` is one [start, limit, step] per axis.
102
+ #
103
+ # `border:` runs the frame's entry point instead of the interior's: the
104
+ # same statements, with a window that falls off the array answered by
105
+ # the rule the kernel was compiled for rather than by reading. Which is
106
+ # why the reach is not checked for one -- reaching outside is what it is
107
+ # for, and the C answers for it.
108
+ def call (array_values, scalar_values, bounds, c_function_values = {},
109
+ border: false)
110
+ arrays = @arrays.map { |name| array_values.fetch(name) }
111
+ ranges = index_ranges(bounds, scalar_values)
112
+ verify(arrays, bounds, ranges, scalar_values, reach: !border)
113
+
114
+ writable = @arrays.map { |name| @written_arrays.include?(name) }
115
+ error = [0].pack("l")
116
+ packed_bounds = bounds.flatten.pack("q*")
117
+ reals = packed_reals(scalar_values)
118
+ integers = (@integers.map { |name| Integer(scalar_values.fetch(name)) } +
119
+ @extent_slots.map { |name, axis| array_values.fetch(name).dim[axis] })
120
+ .pack("q*")
121
+ functions = @c_function_names.map { |name|
122
+ c_function_values.fetch(name).pointer.to_i
123
+ }.pack("Q*")
124
+ # An array handed over whole is walked contiguously by the C, so it is
125
+ # packed into an entity first and copied back after if the C may have
126
+ # written to it. This is the same lifecycle CFunction#call keeps, and for
127
+ # the same reason.
128
+ addressed = @address_arrays.map { |name|
129
+ [name, array_values.fetch(name)]
130
+ }
131
+ packed = addressed.map { |name, array| [array, address_buffer(name, array)] }
132
+ data = packed.map { |_, buffer|
133
+ Access.open([buffer], [false], [nil], [nil]) { |bases|
134
+ bases.first[:pointer]
135
+ }
136
+ }.pack("Q*")
137
+
138
+ box = region_box(ranges, scalar_values, array_values)
139
+ Access.open(arrays, writable, box[0], box[1]) do |bases|
140
+ pointers = bases.map { |basis| basis[:pointer] }.pack("Q*")
141
+ strides = bases.flat_map { |basis| basis[:strides] }.pack("q*")
142
+ mask_pointers = bases.map { |basis| basis[:mask_pointer] || 0 }.pack("Q*")
143
+ mask_strides = bases.flat_map { |basis|
144
+ basis[:mask_strides] || Array.new(@rank, 0)
145
+ }.pack("q*")
146
+ entry = border ? border_function : @function
147
+ entry.call(buffer(pointers), buffer(strides), buffer(packed_bounds),
148
+ buffer(reals), buffer(integers), buffer(functions),
149
+ buffer(data),
150
+ buffer(mask_pointers), buffer(mask_strides), error)
151
+ end
152
+
153
+ packed.each_with_index do |(array, buffer), index|
154
+ next if array.equal?(buffer)
155
+ next if @address_parameters.fetch(@address_arrays[index]).all?(&:const)
156
+ array[] = buffer
157
+ end
158
+
159
+ report(error)
160
+ end
161
+
162
+ # @private
163
+ SLAB_NAME = "carray_jit_slab"
164
+
165
+ # Runs the kernel as CArray's chunked sweep rather than through the
166
+ # addressing here: CArray acquires the operands, broadcasts them, ORs
167
+ # and propagates the masks, and calls back with a chunk at a time.
168
+ #
169
+ # `array_values` are the operands in the order the kernel addresses
170
+ # them, which is the order it packs `pointers` in -- so the same three
171
+ # arguments the kernel's own loop takes are the ones a chunk arrives as.
172
+ def sweep (array_values, scalar_values, c_function_values = {})
173
+ arrays = @arrays.map { |name| array_values.fetch(name) }
174
+ fsync = @arrays.map { |name|
175
+ @written_arrays.include?(name) ? "1" : "0"
176
+ }.join
177
+
178
+ error = [0].pack("l")
179
+ reals = packed_reals(scalar_values)
180
+ integers = (@integers.map { |name| Integer(scalar_values.fetch(name)) } +
181
+ @extent_slots.map { |name, axis|
182
+ array_values.fetch(name).dim[axis]
183
+ }).pack("q*")
184
+ functions = @c_function_names.map { |name|
185
+ c_function_values.fetch(name).pointer.to_i
186
+ }.pack("Q*")
187
+ addressed = @address_arrays.map { |name|
188
+ [name, array_values.fetch(name)]
189
+ }
190
+ packed = addressed.map { |name, array| [array, address_buffer(name, array)] }
191
+ data = packed.map { |_, item|
192
+ Access.open([item], [false], [nil], [nil]) { |bases|
193
+ bases.first[:pointer]
194
+ }
195
+ }.pack("Q*")
196
+ # The kernel declares these only when it reads a mask, and a body that
197
+ # reads one does not come this way; they are here because the
198
+ # signature has the slots.
199
+ masks = ("\0" * 8)
200
+
201
+ held = [buffer(reals), buffer(integers), buffer(functions),
202
+ buffer(data), masks, masks, error]
203
+ context = held.map { |item| Fiddle::Pointer[item].to_i }.pack("Q*")
204
+
205
+ Sweep.call(slab_pointer, fsync, arrays, Fiddle::Pointer[context])
206
+
207
+ packed.each_with_index do |(array, item), index|
208
+ next if array.equal?(item)
209
+ next if @address_parameters.fetch(@address_arrays[index]).all?(&:const)
210
+ array[] = item
211
+ end
212
+
213
+ report(error)
214
+ end
215
+
216
+ # The wrapper CArray's sweep calls, for the same reason #c_source is
217
+ # here: the generated C is the debugging surface. Built on demand,
218
+ # because a kernel that never sweeps never needs it.
219
+ def slab_source
220
+ @source_text
221
+ end
222
+
223
+ # True when this kernel could be run as a sweep at all: the body must
224
+ # not ask about a mask, since a chunk carries no per-cell mask the
225
+ # generated code can test.
226
+ def sweepable?
227
+ !@masked
228
+ end
229
+
230
+ private
231
+
232
+ # What the cell that stopped said, raised now that Ruby has control
233
+ # back. C cannot raise, so a failure is a code in a slot and a loop
234
+ # that stops; this is where it becomes the exception the same body run
235
+ # in Ruby would have raised.
236
+ def report (error)
237
+ code = error.unpack1("l")
238
+ case code
239
+ when 0 then nil
240
+ when 1 then raise ZeroDivisionError, "divided by 0"
241
+ when 2 then raise IndexError, "index out of range"
242
+ else
243
+ message = @raise_messages[code]
244
+ # A code with no message behind it is this compiler's bug, not the
245
+ # block's, and says so rather than raising something the block
246
+ # looks responsible for.
247
+ raise Error, "the kernel reported #{code}, which is no failure it " \
248
+ "was compiled to report" unless message
249
+ raise RuntimeError, message
250
+ end
251
+ end
252
+
253
+ def slab_pointer
254
+ @slab_pointer ||= @handle[SLAB_NAME]
255
+ end
256
+
257
+ # Resolved when the frame is first walked rather than at build time: a
258
+ # kernel that has one is a stencil with a border rule, and even that one
259
+ # runs its interior first.
260
+ def border_function
261
+ @border_function ||=
262
+ Fiddle::Function.new(@handle[CGenerator::BORDER_NAME],
263
+ [Fiddle::TYPE_VOIDP] * 10, Fiddle::TYPE_VOID)
264
+ end
265
+
266
+ # What a C function may be handed as a pointer, and what has to be true
267
+ # of it. The checks are the declaration's own: the type it points at,
268
+ # the length it said it was, and whether it may be written through.
269
+ # The `reals` slots the kernel reads. A captured Complex travels as its
270
+ # two parts, after the reals, and the kernel puts it back together with
271
+ # CMPLX -- so both loops pack this the same way, which is why it is one
272
+ # method: #sweep once packed the reals alone, and a kernel that captured
273
+ # a Complex then read slots nothing had written.
274
+ def packed_reals (scalar_values)
275
+ (@reals.map { |name| Float(scalar_values.fetch(name)) } +
276
+ @complexes.flat_map { |name|
277
+ value = scalar_values.fetch(name)
278
+ [Float(value.real), Float(value.imaginary)]
279
+ }).pack("d*")
280
+ end
281
+
282
+ def address_buffer (name, array)
283
+ @address_parameters.fetch(name).each do |parameter|
284
+ wanted = CDeclaration::DATA_TYPES.fetch(parameter.element.fiddle)
285
+ unless array.data_type_name == wanted.to_s
286
+ raise Unsupported,
287
+ "`#{name}` is handed to a C function as `#{parameter.text}`, " \
288
+ "which takes a #{wanted} array, and `#{name}` is " \
289
+ "#{array.data_type_name}"
290
+ end
291
+ if parameter.sized? && array.elements < parameter.array
292
+ raise Unsupported,
293
+ "`#{name}` is handed to a C function as `#{parameter.text}`, " \
294
+ "which reads #{parameter.array} of them, and `#{name}` has " \
295
+ "#{array.elements}"
296
+ end
297
+ end
298
+ if array.has_mask?
299
+ # A masked cell's bytes are out of contract, and a C function has no
300
+ # mask to consult -- it would read whatever is underneath. What is
301
+ # refused is carrying a mask at all rather than having a cell under
302
+ # it, because the way through is the same either way: `#strip_mask`
303
+ # is where the caller says what the C should see there, and an array
304
+ # that masks nothing loses the mask and no values.
305
+ raise Unsupported,
306
+ "`#{name}` carries a mask and is handed to a C function, " \
307
+ "which has no mask to read; the values under a mask are not " \
308
+ "values, so `#{name}.strip_mask(fill)` is what says what the " \
309
+ "C should see there"
310
+ end
311
+ Access.classify(array)[:entity] ? array : array.copy
312
+ end
313
+
314
+ # A pointer into an empty string is not something Fiddle can hand over,
315
+ # and a kernel with no scalars has an empty buffer.
316
+ def buffer (packed)
317
+ packed.empty? ? "\0" * 8 : packed
318
+ end
319
+
320
+ # The region tier transfers a box rather than the whole view. The box
321
+ # is the loop range grown by how far the kernel reaches from the cell it
322
+ # is on -- per array, because two arrays in one kernel need not be read
323
+ # at the same offsets, and per axis, because they need not be read at
324
+ # the same offsets on each of them.
325
+ def region_box (ranges, scalars, array_values)
326
+ starts = []
327
+ counts = []
328
+ @arrays.each do |name|
329
+ per_axis_start = []
330
+ per_axis_count = []
331
+ # An index the kernel works out could reach any cell, so the box is
332
+ # the whole thing. For a view that has to be transferred that costs
333
+ # what a copy of it would have cost -- which is what the caller
334
+ # would otherwise have written by hand.
335
+ if @dynamic_arrays.include?(name)
336
+ starts << Array.new(array_rank(name), 0)
337
+ counts << array_values.fetch(name).dim.dup
338
+ next
339
+ end
340
+ @axis_uses.fetch(name).each do |walkers, pinned|
341
+ positions = pinned_positions(pinned, scalars)
342
+ # One box per axis, so an axis walked by two indices takes the box
343
+ # that covers both.
344
+ walkers.each do |index, offsets|
345
+ low, high = ranges.fetch(index)
346
+ next unless low < high
347
+ minimum, maximum = offset_span(offsets, scalars)
348
+ positions << low + minimum
349
+ positions << high - 1 + maximum
350
+ end
351
+ if positions.empty?
352
+ per_axis_start << 0
353
+ per_axis_count << 0
354
+ next
355
+ end
356
+ per_axis_start << positions.min
357
+ per_axis_count << positions.max - positions.min + 1
358
+ end
359
+ starts << per_axis_start
360
+ counts << per_axis_count
361
+ end
362
+ [starts, counts]
363
+ end
364
+
365
+ def array_rank (array)
366
+ @array_ranks.fetch(array, @rank)
367
+ end
368
+
369
+ # Every index's range: the outer ones from the extents, the inner ones
370
+ # from the range each `each` was written with, which is an integer
371
+ # expression over literals and captured scalars.
372
+ def index_ranges (bounds, scalar_values)
373
+ ranges = {}
374
+ @index_names.each_with_index do |name, axis|
375
+ ranges[name] = covered_span(bounds[axis])
376
+ end
377
+ flat = bounds.flatten
378
+ @inner_ranges.each do |name, (from, to)|
379
+ ranges[name] = [evaluate(from, scalar_values, flat),
380
+ evaluate(to, scalar_values, flat)]
381
+ end
382
+ ranges
383
+ end
384
+
385
+ # The half-open span an axis actually touches. A step may skip cells,
386
+ # but every cell it lands on has to exist, so the span runs from the
387
+ # first index to the last one visited.
388
+ def covered_span (triple)
389
+ start, limit, step = triple
390
+ return [0, 0] if step.positive? ? start >= limit : start <= limit
391
+ count = ((limit - start).abs + step.abs - 1) / step.abs
392
+ last = start + step * (count - 1)
393
+ step.positive? ? [start, last + 1] : [last, start + 1]
394
+ end
395
+
396
+ def evaluate (node, scalars, flat_bounds = nil)
397
+ case node
398
+ when IntegerLiteral then node.value
399
+ when CaptureRead then Integer(scalars.fetch(node.name))
400
+ when BoundsValue then flat_bounds.fetch(node.slot)
401
+ when UnaryMinus then -evaluate(node.operand, scalars, flat_bounds)
402
+ when BinaryOperation
403
+ left = evaluate(node.left, scalars, flat_bounds)
404
+ right = evaluate(node.right, scalars, flat_bounds)
405
+ case node.operator
406
+ when :+ then left + right
407
+ when :- then left - right
408
+ when :* then left * right
409
+ else
410
+ raise Unsupported,
411
+ "an inner loop's range is built from `+`, `-` and `*` only"
412
+ end
413
+ else
414
+ raise Unsupported,
415
+ "an inner loop's range is an integer expression over literals " \
416
+ "and captured scalars"
417
+ end
418
+ end
419
+
420
+ def verify (arrays, bounds, ranges, scalars, reach: true)
421
+ # A contraction carries the summed indices' extents after the free
422
+ # ones, because the loops over them are generated rather than written.
423
+ expected = @rank + @contracted_names.size
424
+ unless bounds.size == expected
425
+ raise Unsupported,
426
+ "the kernel has #{expected} index/indices, got #{bounds.size} extent(s)"
427
+ end
428
+ @arrays.each_with_index do |name, position|
429
+ array = arrays[position]
430
+ unless array.is_a?(CArray)
431
+ raise Unsupported, "`#{name}` is not a CArray"
432
+ end
433
+ unless array.rank == array_rank(name)
434
+ raise Unsupported,
435
+ "`#{name}` has rank #{array.rank}, but is indexed with " \
436
+ "#{array_rank(name)} #{array_rank(name) == 1 ? 'index' : 'indices'}"
437
+ end
438
+ unless array.data_type_name == @storage_types.fetch(name)
439
+ raise Unsupported,
440
+ "the kernel was compiled for `#{name}` as " \
441
+ "#{@storage_types.fetch(name)}, got #{array.data_type_name}"
442
+ end
443
+ verify_bounds(name, array, ranges, scalars) if reach
444
+ end
445
+ end
446
+
447
+ # Every cell the kernel would touch has to exist. Range and offsets and
448
+ # extents are all known here, so reaching outside an array is caught
449
+ # before anything runs rather than read -- or written -- past its end.
450
+ def verify_bounds (name, array, ranges, scalars)
451
+ @axis_uses.fetch(name).each_with_index do |(walkers, pinned), axis|
452
+ extent = array.dim[axis]
453
+
454
+ pinned_positions(pinned, scalars).each do |position|
455
+ next if position >= 0 && position < extent
456
+ raise Unsupported,
457
+ "`#{name}` is indexed at position #{position} on axis " \
458
+ "#{axis}, " \
459
+ "which has an extent of #{extent}"
460
+ end
461
+
462
+ walkers.each do |index, offsets|
463
+ low, high = ranges.fetch(index)
464
+ next if low >= high
465
+ minimum, maximum = offset_span(offsets, scalars)
466
+ if low + minimum < 0
467
+ raise Unsupported,
468
+ "`#{name}` is indexed at " \
469
+ "#{offset_text(name, index, minimum)}, so " \
470
+ "the range on `#{index}` cannot start at #{low}"
471
+ end
472
+ if high - 1 + maximum > extent - 1
473
+ raise Unsupported,
474
+ "`#{name}` is indexed at " \
475
+ "#{offset_text(name, index, maximum)}, so " \
476
+ "the range on `#{index}` cannot end at #{high} " \
477
+ "for an extent of #{extent}"
478
+ end
479
+ end
480
+ end
481
+ end
482
+
483
+ # A pinned subscript's position is an integer expression over literals
484
+ # and captured scalars, so its value is known here even though it is not
485
+ # known when the kernel is compiled.
486
+ def pinned_positions (pinned, scalars)
487
+ pinned.map { |node| node.is_a?(Node) ? evaluate(node, scalars) : node }
488
+ end
489
+
490
+ # An offset may be an expression over captured integers, so how far the
491
+ # kernel reaches along an axis is known here rather than when it was
492
+ # compiled.
493
+ def offset_span (offsets, scalars)
494
+ values = offsets.map { |offset|
495
+ offset.is_a?(Node) ? evaluate(offset, scalars) : offset
496
+ }
497
+ [values.min, values.max]
498
+ end
499
+
500
+ def offset_text (name, index, offset)
501
+ return "`#{name}[#{index}]`" if offset.zero?
502
+ sign = offset.negative? ? "-" : "+"
503
+ "`#{name}[#{index} #{sign} #{offset.abs}]`"
504
+ end
505
+
506
+ end
507
+
508
+ end
509
+ end