carray-jit 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +854 -0
  3. data/README.md +7 -6
  4. data/carray-jit.gemspec +1 -3
  5. data/docs/00_Introduction.md +4 -3
  6. data/docs/01_GettingStarted.md +1 -1
  7. data/docs/02_KernelShapes.md +150 -10
  8. data/docs/03_SupportedFeatures.md +582 -26
  9. data/docs/04_Compiling.md +44 -7
  10. data/docs/05_DesignNotes.md +3 -3
  11. data/docs/06_Cheatsheet.md +216 -11
  12. data/docs/07_StepByStep.ja.md +534 -0
  13. data/docs/07_StepByStep.md +535 -0
  14. data/examples/README.md +12 -0
  15. data/examples/applications/alarm.rb +121 -0
  16. data/examples/applications/collatz.rb +105 -0
  17. data/examples/applications/cubic_spline.rb +331 -0
  18. data/examples/applications/dithering.rb +144 -0
  19. data/examples/applications/group_stats.rb +115 -0
  20. data/examples/applications/lookup.rb +126 -0
  21. data/examples/applications/median_filter.rb +153 -0
  22. data/examples/applications/parcel_ascent.rb +220 -0
  23. data/examples/applications/point_cloud.rb +23 -11
  24. data/examples/applications/point_in_polygon.rb +111 -0
  25. data/examples/applications/random_walk.rb +98 -0
  26. data/examples/applications/van_der_pol.rb +186 -0
  27. data/examples/applications/wet_bulb.rb +140 -0
  28. data/examples/features/10_complex.rb +14 -4
  29. data/examples/features/15_loops.rb +7 -1
  30. data/lib/carray/jit/access.rb +14 -0
  31. data/lib/carray/jit/analyzer.rb +2179 -119
  32. data/lib/carray/jit/block_reader.rb +37 -6
  33. data/lib/carray/jit/c_function.rb +634 -74
  34. data/lib/carray/jit/c_generator.rb +1607 -156
  35. data/lib/carray/jit/call.rb +68 -0
  36. data/lib/carray/jit/compiler.rb +94 -12
  37. data/lib/carray/jit/kernel.rb +371 -33
  38. data/lib/carray/jit/node.rb +359 -9
  39. data/lib/carray/jit/sorting_networks.rb +182 -0
  40. data/lib/carray/jit/type_assignment.rb +386 -47
  41. data/lib/carray/jit/version.rb +1 -1
  42. data/lib/carray/jit.rb +936 -63
  43. metadata +22 -8
  44. data/ext/carray_jit_access/carray_jit_access.c +0 -460
  45. data/ext/carray_jit_access/extconf.rb +0 -8
@@ -19,13 +19,19 @@ class CArray
19
19
  # @return [Boolean] whether this call invoked the compiler, rather
20
20
  # than reusing a cached object.
21
21
  attr_reader :source, :c_source, :arrays, :storage_types, :reals,
22
- :integers, :complexes, :rank, :index_names, :written_arrays,
22
+ :integers, :complexes, :unsigned_integers,
23
+ :rank, :index_names, :written_arrays,
24
+ # The arrays handed to a C function whole, by address,
25
+ # rather than walked a cell at a time. A caller that lines
26
+ # operands up has to leave these alone: their shape is the
27
+ # C function's business, not the expression's.
28
+ :address_arrays,
23
29
  :compiled, :masked, :directions, :contracted_names,
24
30
  :index_axes,
25
31
  # How far a stencil's windows reach on each axis, as
26
32
  # [lowest, highest] per axis: what the caller walks the
27
33
  # interior by. Empty for a kernel that has no windows.
28
- :window_reach,
34
+ :window_reach, :window_reaches,
29
35
  # What a cell that stopped can have been raising about --
30
36
  # the block's own `raise`s, and those of the bodies pasted
31
37
  # into this kernel, by the code each reports.
@@ -38,6 +44,7 @@ class CArray
38
44
  @reals = generator.reals
39
45
  @integers = generator.integers
40
46
  @complexes = generator.complexes
47
+ @unsigned_integers = generator.unsigned_integers
41
48
  # Per array, the axes read at an index only the running kernel knows,
42
49
  # and the extents it checks them against.
43
50
  @extent_slots = generator.extent_slots
@@ -48,6 +55,9 @@ class CArray
48
55
  @written_arrays = analyzer.written_arrays
49
56
  @array_ranks = analyzer.array_ranks
50
57
  @inner_ranges = analyzer.inner_ranges
58
+ # An index the block wrote twice has two identifiers here; the
59
+ # messages speak the name it was written with.
60
+ @index_sources = analyzer.index_sources
51
61
  @axis_uses = @arrays.to_h { |array|
52
62
  [array, (0...array_rank(array)).map { |axis| analyzer.axis_use(array, axis) }]
53
63
  }
@@ -61,12 +71,33 @@ class CArray
61
71
  end
62
72
  end
63
73
 
74
+ # Which captured scalars decide where the kernel reaches, rather than
75
+ # what it computes. A pinned subscript, an offset and an inner loop's
76
+ # range are worked out here, in Ruby, before the first cell; every
77
+ # other captured number is packed into a buffer and read by the C. So
78
+ # these are the ones a prepared call has to be keyed on -- and a
79
+ # coefficient or a clock that changes every step is not one of them,
80
+ # which is what keeps a time-stepping caller on the prepared path.
81
+ @shape_scalar_names = shape_scalar_names
82
+ # Calls prepared earlier, most recently used first.
83
+ @plans = []
84
+
64
85
  @masked = generator.masked
65
86
  @window_reach = analyzer.window_reach
87
+ @window_reaches = analyzer.window_reaches
66
88
  # What `raise` in the block said, by the code the cell that raised
67
89
  # writes into the error slot. The message does not travel: C has
68
90
  # nothing to carry it in, and it was known when this was compiled.
69
91
  @raise_messages = generator.raise_messages
92
+ # The arrays this kernel allocates at its entry, as
93
+ # [name, storage, shape] -- so that a shape it could not use, or an
94
+ # allocation that failed, can be named with the lengths it came to on
95
+ # this call. The shapes are worked out here as they are there: from
96
+ # the same expressions over the same captured integers.
97
+ @heap_arrays = analyzer.local_array_declarations.filter_map { |entry|
98
+ _scope, name, storage, shape, heap = entry
99
+ [name, storage, shape] if heap
100
+ }
70
101
  # Kept so the sweep entry point can be built from it if one is ever
71
102
  # asked for. Compiling it eagerly would pay for a road most kernels
72
103
  # never take.
@@ -108,16 +139,15 @@ class CArray
108
139
  def call (array_values, scalar_values, bounds, c_function_values = {},
109
140
  border: false)
110
141
  arrays = @arrays.map { |name| array_values.fetch(name) }
111
- ranges = index_ranges(bounds, scalar_values)
112
- verify(arrays, bounds, ranges, scalar_values, reach: !border)
142
+ plan = prepared_call(arrays, array_values, bounds, scalar_values, border)
113
143
 
114
- writable = @arrays.map { |name| @written_arrays.include?(name) }
144
+ writable = plan[:writable]
115
145
  error = [0].pack("l")
116
- packed_bounds = bounds.flatten.pack("q*")
146
+ packed_bounds = plan[:bounds]
117
147
  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*")
148
+ # The captured integers change from call to call and are packed here;
149
+ # the extents behind them are the arrays' own and travel in the plan.
150
+ integers = packed_scalar_integers(scalar_values) + plan[:extents]
121
151
  functions = @c_function_names.map { |name|
122
152
  c_function_values.fetch(name).pointer.to_i
123
153
  }.pack("Q*")
@@ -135,14 +165,12 @@ class CArray
135
165
  }
136
166
  }.pack("Q*")
137
167
 
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*")
168
+ box = plan[:box]
169
+ # Opened for a kernel rather than for a reader: the four buffers come
170
+ # back packed, instead of a hash and two arrays per operand that this
171
+ # would pack back into bytes and throw away.
172
+ Access.open(arrays, writable, box[0], box[1], true) do
173
+ |pointers, strides, mask_pointers, mask_strides|
146
174
  entry = border ? border_function : @function
147
175
  entry.call(buffer(pointers), buffer(strides), buffer(packed_bounds),
148
176
  buffer(reals), buffer(integers), buffer(functions),
@@ -156,9 +184,17 @@ class CArray
156
184
  array[] = buffer
157
185
  end
158
186
 
159
- report(error)
187
+ report(error, scalar_values)
160
188
  end
161
189
 
190
+ # How many prepared calls one kernel keeps. A stencil asks under its
191
+ # interior's bounds and one pair per axis of frame, so a two-dimensional
192
+ # one needs five before it repeats; a caller alternating between two
193
+ # model states needs two of whatever it was already using. Past this
194
+ # the oldest goes and the call prepares itself again, which is what
195
+ # every call did before.
196
+ PLAN_LIMIT = 8
197
+
162
198
  # @private
163
199
  SLAB_NAME = "carray_jit_slab"
164
200
 
@@ -177,10 +213,7 @@ class CArray
177
213
 
178
214
  error = [0].pack("l")
179
215
  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*")
216
+ integers = packed_integers(scalar_values, array_values)
184
217
  functions = @c_function_names.map { |name|
185
218
  c_function_values.fetch(name).pointer.to_i
186
219
  }.pack("Q*")
@@ -210,7 +243,7 @@ class CArray
210
243
  array[] = item
211
244
  end
212
245
 
213
- report(error)
246
+ report(error, scalar_values)
214
247
  end
215
248
 
216
249
  # The wrapper CArray's sweep calls, for the same reason #c_source is
@@ -229,16 +262,138 @@ class CArray
229
262
 
230
263
  private
231
264
 
265
+ # The parts of a call that the operands' shapes, the bounds and the
266
+ # kernel's own scalars decide, rather than the data or the views.
267
+ #
268
+ # Everything here answers the same for two calls whose key agrees, so it
269
+ # is worked out once and kept: the bounds check, the box the xfer tier
270
+ # transfers, the extents the kernel is handed, and which operands it
271
+ # writes. What is deliberately *not* here is anything read off a view:
272
+ # two arrays of one shape and one type can be laid out differently, and
273
+ # a stride read from the one is not a stride into the other. Those come
274
+ # from Access.open on every call, as they always did.
275
+ #
276
+ # Nor is the refusal of a masked array handed whole to a C function
277
+ # (#address_buffer): a mask is runtime state that leaves the shape and
278
+ # the type alone, so no key here would notice one appearing. It is
279
+ # asked on every call, which costs a predicate and keeps the refusal.
280
+ def prepared_call (arrays, array_values, bounds, scalars, border)
281
+ key = plan_key(arrays, bounds, scalars, border)
282
+ if key
283
+ found = @plans.assoc(key)
284
+ if found
285
+ unless @plans.first.equal?(found)
286
+ @plans.unshift(@plans.delete(found))
287
+ end
288
+ return found[1]
289
+ end
290
+ end
291
+ ranges = index_ranges(bounds, scalars)
292
+ verify(arrays, bounds, ranges, scalars, reach: !border)
293
+ plan = {
294
+ :writable => @arrays.map { |name| @written_arrays.include?(name) },
295
+ :bounds => bounds.flatten.pack("q*"),
296
+ :extents => packed_extents(array_values),
297
+ :box => region_box(ranges, scalars, array_values),
298
+ }
299
+ freeze_throughout(plan)
300
+ if key
301
+ @plans.unshift([key, plan])
302
+ @plans.pop if @plans.size > PLAN_LIMIT
303
+ end
304
+ plan
305
+ end
306
+
307
+ # What two calls have to agree on for one to be prepared like the other.
308
+ #
309
+ # Shapes and types, because that is all `verify` and `region_box` and
310
+ # the extents read of an array; the bounds and the border, because they
311
+ # are what the check is against; and the scalars that decide where the
312
+ # kernel reaches, but not the ones it merely computes with.
313
+ #
314
+ # No identity: two arrays of one shape and type prepare the same way, so
315
+ # keying on which object it was would only make a caller that rebuilds
316
+ # its fields every step miss every time -- and would have this kernel,
317
+ # which lives as long as the process, hold their memory.
318
+ #
319
+ # nil where an operand is not a CArray at all, so that the call falls
320
+ # through to `verify` and is refused by name rather than by whatever
321
+ # the next method call happens to raise.
322
+ def plan_key (arrays, bounds, scalars, border)
323
+ shapes = []
324
+ arrays.each do |array|
325
+ return nil unless array.is_a?(CArray)
326
+ shapes << array.data_type << array.dim
327
+ end
328
+ [shapes, bounds, border,
329
+ @shape_scalar_names.map { |name| scalars[name] }]
330
+ end
331
+
332
+ # A kernel is shared, and Fiddle lets go of the GVL for the call, so two
333
+ # threads can be reading one plan while a third prepares another. That
334
+ # is sound only while nothing in a plan is written after it is published
335
+ # -- and a box is arrays inside an array, so freezing the top of it
336
+ # would leave the parts a later edit could still reach.
337
+ def freeze_throughout (value)
338
+ case value
339
+ when Hash then value.each_value { |item| freeze_throughout(item) }
340
+ when Array then value.each { |item| freeze_throughout(item) }
341
+ end
342
+ value.freeze
343
+ end
344
+
345
+ # The captured scalars a pinned subscript, an offset or an inner loop's
346
+ # range is worked out from.
347
+ def shape_scalar_names
348
+ names = []
349
+ @axis_uses.each_value do |uses|
350
+ uses.each do |walkers, pinned|
351
+ pinned.each { |node| collect_captures(node, names) }
352
+ walkers.each { |walker| walker[1].each { |node|
353
+ collect_captures(node, names)
354
+ } }
355
+ end
356
+ end
357
+ @inner_ranges.each_value do |from, to, _step|
358
+ collect_captures(from, names)
359
+ collect_captures(to, names)
360
+ end
361
+ names.uniq.freeze
362
+ end
363
+
364
+ def collect_captures (node, names)
365
+ return names unless node.is_a?(Node)
366
+ names << node.name if node.is_a?(CaptureRead)
367
+ node.children.each { |child| collect_captures(child, names) }
368
+ names
369
+ end
370
+
232
371
  # What the cell that stopped said, raised now that Ruby has control
233
372
  # back. C cannot raise, so a failure is a code in a slot and a loop
234
373
  # that stops; this is where it becomes the exception the same body run
235
374
  # in Ruby would have raised.
236
- def report (error)
375
+ def report (error, scalars = {})
237
376
  code = error.unpack1("l")
377
+ if (failure = CGenerator::FIXED_FAILURES[code])
378
+ raise failure[0], failure[1]
379
+ end
238
380
  case code
239
381
  when 0 then nil
382
+ when CGenerator::SHAPE_CODE then raise_a_bad_shape(scalars)
383
+ when CGenerator::MEMORY_CODE then raise_out_of_memory(scalars)
240
384
  when 1 then raise ZeroDivisionError, "divided by 0"
241
385
  when 2 then raise IndexError, "index out of range"
386
+ when 3 then raise ArgumentError,
387
+ "min argument must be less than or equal to " \
388
+ "max argument"
389
+ # Ruby's own wording names the value it could not compare, and the
390
+ # slot carries a code rather than a number, so the reason is named
391
+ # instead. The class and the failure are Ruby's.
392
+ when 4 then raise ArgumentError,
393
+ "comparison with a NaN failed, so `clamp` has " \
394
+ "no answer"
395
+ when 5 then raise Math::DomainError,
396
+ "Numerical argument is out of domain - gamma"
242
397
  else
243
398
  message = @raise_messages[code]
244
399
  # A code with no message behind it is this compiler's bug, not the
@@ -250,6 +405,53 @@ class CArray
250
405
  end
251
406
  end
252
407
 
408
+ # A length the kernel worked out that is no length: zero cells, a
409
+ # negative count, or so many that the bytes would not be a `size_t`.
410
+ # The kernel reports which of its arrays it was by reporting at all --
411
+ # it stops before the first cell -- and the lengths are worked out
412
+ # again here, where there is a message to put them in.
413
+ def raise_a_bad_shape (scalars)
414
+ offending = @heap_arrays.filter_map { |name, storage, shape|
415
+ lengths = shape_lengths(shape, scalars)
416
+ next unless lengths&.any? { |length| length < 1 }
417
+ "`#{name}` (#{storage}) asks for " \
418
+ "#{lengths.join(' x ')} #{lengths.size == 1 ? 'cells' : 'cells per axis'}"
419
+ }
420
+ raise ArgumentError,
421
+ "a local array holds at least one cell: " \
422
+ "#{offending.join('; ')}" unless offending.empty?
423
+ raise ArgumentError,
424
+ "a local array of this kernel asks for more cells than its " \
425
+ "bytes could be counted in"
426
+ end
427
+
428
+ def raise_out_of_memory (scalars)
429
+ asked = @heap_arrays.map { |name, storage, shape|
430
+ lengths = shape_lengths(shape, scalars)
431
+ cells = lengths&.inject(1, :*)
432
+ bytes = cells && cells * LOCAL_ARRAY_STORAGE_BYTES.fetch(storage, 1)
433
+ "`#{name}` (#{storage}, #{lengths ? lengths.join(' x ') : 'shape'}" +
434
+ (bytes ? ", #{bytes} bytes" : "") + ")"
435
+ }
436
+ raise NoMemoryError,
437
+ "a kernel could not allocate the local arrays it works in: " \
438
+ "#{asked.join(', ')}"
439
+ end
440
+
441
+ # @private
442
+ LOCAL_ARRAY_STORAGE_BYTES = Analyzer::LOCAL_ARRAY_STORAGE_BYTES
443
+
444
+ # The shape as numbers, or nil where one of its axes cannot be worked
445
+ # out here -- which is not a failure worth a second exception, the one
446
+ # being raised having happened already.
447
+ def shape_lengths (shape, scalars)
448
+ shape.map { |extent|
449
+ extent.is_a?(Integer) ? extent : evaluate(extent.node, scalars)
450
+ }
451
+ rescue StandardError, Unsupported
452
+ nil
453
+ end
454
+
253
455
  def slab_pointer
254
456
  @slab_pointer ||= @handle[SLAB_NAME]
255
457
  end
@@ -279,6 +481,35 @@ class CArray
279
481
  }).pack("d*")
280
482
  end
281
483
 
484
+ # The `integers` slots the kernel reads: the signed captures, then the
485
+ # unsigned ones, then the extents an index is checked against. A uint64
486
+ # is packed as the bits it is -- `pack("q")` would take 2**63 modulo the
487
+ # width and say nothing -- and the kernel casts the slot back, so the
488
+ # two directives are what makes one buffer carry both widths.
489
+ #
490
+ # One method because both loops pack it, and the same buffer packed in
491
+ # two places is what `packed_reals` is one method about.
492
+ def packed_integers (scalar_values, array_values)
493
+ packed_scalar_integers(scalar_values) + packed_extents(array_values)
494
+ end
495
+
496
+ # The captured half, which is different every time a caller steps a
497
+ # counter, and the arrays' half, which is the shape and travels in the
498
+ # plan. Two methods because `#call` packs the first and takes the
499
+ # second from what it prepared earlier; `#sweep` still wants both.
500
+ 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*")
505
+ end
506
+
507
+ def packed_extents (array_values)
508
+ @extent_slots.map { |name, axis|
509
+ array_values.fetch(name).dim[axis]
510
+ }.pack("q*")
511
+ end
512
+
282
513
  def address_buffer (name, array)
283
514
  @address_parameters.fetch(name).each do |parameter|
284
515
  wanted = CDeclaration::DATA_TYPES.fetch(parameter.element.fiddle)
@@ -317,7 +548,7 @@ class CArray
317
548
  packed.empty? ? "\0" * 8 : packed
318
549
  end
319
550
 
320
- # The region tier transfers a box rather than the whole view. The box
551
+ # The xfer tier transfers a box rather than the whole view. The box
321
552
  # is the loop range grown by how far the kernel reaches from the cell it
322
553
  # is on -- per array, because two arrays in one kernel need not be read
323
554
  # at the same offsets, and per axis, because they need not be read at
@@ -375,13 +606,117 @@ class CArray
375
606
  ranges[name] = covered_span(bounds[axis])
376
607
  end
377
608
  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)]
609
+ # In source order, outermost first, so an index a range is written
610
+ # over has its own range settled before it is read.
611
+ @inner_ranges.each do |name, (from, to, step)|
612
+ step ||= 1
613
+ first = span_of(from, scalar_values, flat, ranges, name)
614
+ last = span_of(to, scalar_values, flat, ranges, name)
615
+ # The widest pass the range could describe: counting up, that is the
616
+ # earliest start against the latest end, and counting down it is the
617
+ # other way round.
618
+ start, limit = step.positive? ? [first.first, last.last]
619
+ : [first.last, last.first]
620
+ # A step that skips cells lands on the ones a pass started on, and
621
+ # where the start moves so do they: `(p...8).step(3)` visits 0, 3, 6
622
+ # from one start and 1, 4, 7 from the next. Stepped out from the
623
+ # earliest start alone, the reach stopped at 6 and a pass wrote at
624
+ # 7. So with a start that moves, the span is every cell between
625
+ # the widest start and the widest end, which every pass lies within.
626
+ step = step <=> 0 if first.first != first.last
627
+ ranges[name] = covered_span([start, limit, step])
381
628
  end
382
629
  ranges
383
630
  end
384
631
 
632
+ # A range written over another index is read at its widest (see
633
+ # #span_of), so the number in the message above can be one no pass
634
+ # actually starts or ends at. Saying which index it was written over is
635
+ # what tells a reader whether to narrow the range or the array's use --
636
+ # without it the message names a bound the loop never reaches.
637
+ def widest_reading (index, spelled)
638
+ over = range_indices(index)
639
+ return "" if over.empty?
640
+ ". The range on `#{spelled}` is written over " \
641
+ "#{over.map { |name| "`#{name}`" }.join(' and ')}, and a range over " \
642
+ "another index is read here at its widest -- as far as any value of " \
643
+ "#{over.size == 1 ? 'it' : 'them'} could take it, which may be " \
644
+ "further than it goes on any one pass"
645
+ end
646
+
647
+ # The indices an inner loop's range mentions, by the name the block
648
+ # wrote for each.
649
+ def range_indices (index)
650
+ range = @inner_ranges[index]
651
+ return [] unless range
652
+ from, to, = range
653
+ [from, to].flat_map { |node| mentioned_indices(node) }.uniq
654
+ .map { |name| @index_sources.fetch(name, name) }
655
+ end
656
+
657
+ def mentioned_indices (node)
658
+ return [] unless node.is_a?(Node)
659
+ return [node.name] if node.is_a?(IndexVariable)
660
+ node.children.flat_map { |child| mentioned_indices(child) }
661
+ end
662
+
663
+ # What an index range covers, as [lowest, highest] inclusive.
664
+ #
665
+ # A range written over another index -- `(p+1...3)`, the shape a
666
+ # triangular loop takes -- has no single pair of numbers to be: `k`
667
+ # starts somewhere else for every `p`. What the two callers of this
668
+ # want is where the index can reach, not what it is on a given pass:
669
+ # `verify_bounds` asks whether a subscript can leave its array, and
670
+ # `region_box` asks what a stencil touches. So the answer is an
671
+ # interval, and a range over another index is read at its widest.
672
+ #
673
+ # Widest is the safe direction. It covers every pass the loop can take
674
+ # and then some, so a subscript this says is inside really is; what it
675
+ # costs is that a reach nothing actually makes can still be refused.
676
+ #
677
+ # A range that mentions no index gives an interval of one number, which
678
+ # is what `evaluate` gave before, so those kernels are unchanged.
679
+ def span_of (node, scalars, flat_bounds, ranges, index)
680
+ case node
681
+ when IntegerLiteral then [node.value, node.value]
682
+ when CaptureRead then value = Integer(scalars.fetch(node.name))
683
+ [value, value]
684
+ when BoundsValue then value = flat_bounds.fetch(node.slot)
685
+ [value, value]
686
+ when IndexVariable
687
+ # Held half-open, as the loops are; the last index it visits is one
688
+ # below the end.
689
+ low, high = ranges.fetch(node.name) do
690
+ raise Unsupported,
691
+ "the range on `#{index}` is written over `#{node.name}`, " \
692
+ "which is not an index around it"
693
+ end
694
+ [low, high - 1]
695
+ when UnaryMinus
696
+ low, high = span_of(node.operand, scalars, flat_bounds, ranges, index)
697
+ [-high, -low]
698
+ when BinaryOperation
699
+ left = span_of(node.left, scalars, flat_bounds, ranges, index)
700
+ right = span_of(node.right, scalars, flat_bounds, ranges, index)
701
+ case node.operator
702
+ when :+ then [left.first + right.first, left.last + right.last]
703
+ when :- then [left.first - right.last, left.last - right.first]
704
+ when :*
705
+ # Four corners, because either interval may straddle zero.
706
+ corners = [left.first * right.first, left.first * right.last,
707
+ left.last * right.first, left.last * right.last]
708
+ [corners.min, corners.max]
709
+ else
710
+ raise Unsupported,
711
+ "an inner loop's range is built from `+`, `-` and `*` only"
712
+ end
713
+ else
714
+ raise Unsupported,
715
+ "an inner loop's range is an integer expression over " \
716
+ "literals, captured scalars and the indices around it"
717
+ end
718
+ end
719
+
385
720
  # The half-open span an axis actually touches. A step may skip cells,
386
721
  # but every cell it lands on has to exist, so the span runs from the
387
722
  # first index to the last one visited.
@@ -462,19 +797,22 @@ class CArray
462
797
  walkers.each do |index, offsets|
463
798
  low, high = ranges.fetch(index)
464
799
  next if low >= high
800
+ spelled = @index_sources.fetch(index, index)
465
801
  minimum, maximum = offset_span(offsets, scalars)
466
802
  if low + minimum < 0
467
803
  raise Unsupported,
468
804
  "`#{name}` is indexed at " \
469
- "#{offset_text(name, index, minimum)}, so " \
470
- "the range on `#{index}` cannot start at #{low}"
805
+ "#{offset_text(name, spelled, minimum)}, so " \
806
+ "the range on `#{spelled}` cannot start at #{low}" +
807
+ widest_reading(index, spelled)
471
808
  end
472
809
  if high - 1 + maximum > extent - 1
473
810
  raise Unsupported,
474
811
  "`#{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}"
812
+ "#{offset_text(name, spelled, maximum)}, so " \
813
+ "the range on `#{spelled}` cannot end at #{high} " \
814
+ "for an extent of #{extent}" +
815
+ widest_reading(index, spelled)
478
816
  end
479
817
  end
480
818
  end