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