bud 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
data/bin/budplot CHANGED
@@ -4,7 +4,9 @@ require 'bud'
4
4
  require 'bud/bud_meta'
5
5
  require 'bud/depanalysis'
6
6
  require 'bud/graphs'
7
+ require 'bud/meta_algebra'
7
8
  require 'bud/viz_util'
9
+ require 'getopt/std'
8
10
 
9
11
  include VizUtil
10
12
 
@@ -42,12 +44,34 @@ def make_instance(mods)
42
44
 
43
45
  def_lines = ["class FooBar",
44
46
  "include Bud",
47
+ "include MetaAlgebra",
48
+ "include MetaReports",
45
49
  mods.map {|m| "include #{m}"},
46
50
  "end"
47
51
  ]
48
52
  class_def = def_lines.flatten.join("\n")
49
53
  eval(class_def)
50
- FooBar.new
54
+ f =FooBar.new
55
+ 3.times{ f.tick }
56
+ f
57
+ end
58
+
59
+ def trace_counts(begins)
60
+ complexity = {:data => {}, :coord => {}}
61
+ if !begins[:start].nil?
62
+ begins[:start].each_pair do |k, v|
63
+ if @data and @data[k]
64
+ complexity[:data][k] = @data[k].length
65
+ end
66
+ end
67
+
68
+ begins[:finish].each_pair do |k, v|
69
+ if @data and @data[k]
70
+ complexity[:coord][k] = @data[k].length
71
+ end
72
+ end
73
+ end
74
+ complexity
51
75
  end
52
76
 
53
77
  def process(mods)
@@ -79,12 +103,44 @@ def process(mods)
79
103
  end
80
104
 
81
105
  viz_name = "bud_doc/" + mods.join("_") + "_viz"
82
- write_index(inp, outp, priv, viz_name)
83
- graph_from_instance(d, "#{viz_name}_collapsed", "bud_doc", true)
84
- graph_from_instance(d, "#{viz_name}_expanded", "bud_doc", false)
106
+ graph_from_instance(d, "#{viz_name}_collapsed", "bud_doc", true, nil, @data)
107
+ graph_from_instance(d, "#{viz_name}_expanded", "bud_doc", false, nil, @data)
108
+ begins = graph_from_instance(d, "#{viz_name}_expanded_dot", "bud_doc", false, "dot", @data)
109
+
110
+
111
+ complexity = trace_counts(begins)
112
+ # try to figure out the degree of the async edges
113
+ deg = find_degrees(d, @data)
114
+ unless deg.nil?
115
+ deg.each_pair do |k, v|
116
+ puts "DEGREE: #{k} = #{v.keys.length}"
117
+ end
118
+ end
119
+
120
+ write_index(inp, outp, priv, viz_name, complexity)
85
121
  end
86
122
 
87
- def write_index(inp, outp, priv, viz_name)
123
+ def find_degrees(inst, data)
124
+ degree = {}
125
+ return if data.nil?
126
+ data.each_pair do |k, v|
127
+ tab = inst.tables[k.gsub("_snd", "").to_sym]
128
+ if !tab.nil?
129
+ if tab.class == Bud::BudChannel
130
+ v.each_pair do |k2, v2|
131
+ v2.each do |row|
132
+ loc = row[tab.locspec_idx]
133
+ degree[k] ||= {}
134
+ degree[k][loc] = true
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ return degree
141
+ end
142
+
143
+ def write_index(inp, outp, priv, viz_name, cx)
88
144
  f = File.open("bud_doc/index.html", "w")
89
145
  f.puts "<html>"
90
146
  f.puts "<embed src=\"#{ENV['PWD']}/#{viz_name}_collapsed.svg\" width=\"100%\" height=\"60%\" type=\"image/svg+xml\" pluginspage=\"http://www.adobe.com/svg/viewer/install/\" />"
@@ -97,14 +153,24 @@ def write_index(inp, outp, priv, viz_name)
97
153
  f.puts "<h2>Output Interfaces</h2>"
98
154
  do_table(f, outp)
99
155
  f.puts "</td><td>"
100
- f.puts "<h2>Private State</h2>"
101
- do_table(f, priv)
102
- f.puts "</td>"
156
+ f.puts "<h2>Trace Analysis Results</h2>"
157
+ f.puts "<h3>Data Complexity</h3>"
158
+ do_cx(f, cx[:data])
159
+ f.puts "<h3>Coordination Complexity</h3>"
160
+ do_cx(f, cx[:coord])
103
161
  f.puts "</tr></table>"
104
162
  f.puts "</html>"
105
163
  f.close
106
164
  end
107
165
 
166
+ def do_cx(f, cx)
167
+ f.puts "<table border='1'>"
168
+ cx.each_pair do |k, v|
169
+ f.puts "<tr><td>#{k}</td><td>#{v.inspect}</td></tr>"
170
+ end
171
+ f.puts "</table>"
172
+ end
173
+
108
174
  def do_table(f, info)
109
175
  f.puts "<table border='1'>"
110
176
  info.sort{|a, b| a[0].to_s <=> b[0].to_s}.each do |tbl_name, tbl_impl|
@@ -117,11 +183,34 @@ def do_table(f, info)
117
183
  f.puts "</table>"
118
184
  end
119
185
 
186
+ def get_trace_data
187
+ data = nil
188
+
189
+ if @opts["t"]
190
+ data = {}
191
+ traces = @opts['t'].class == String ? [@opts['t']] : @opts['t']
192
+ traces.each do |t|
193
+ meta, da = get_meta2(t)
194
+ da.each do |d|
195
+ data[d[1]] ||= {}
196
+ data[d[1]][d[0]] ||= []
197
+ data[d[1]][d[0]] << d[2]
198
+ end
199
+ end
200
+ end
201
+ data
202
+ end
203
+
120
204
  if ARGV.length < 2
121
205
  puts "Usage: budplot LIST_OF_FILES LIST_OF_MODULES_OR_CLASSES"
122
206
  exit
123
207
  end
124
208
 
209
+ @opts = Getopt::Std.getopts("t:")
210
+
211
+
212
+
213
+ @data = get_trace_data
125
214
  `mkdir bud_doc`
126
215
 
127
216
  modules = []
data/bin/budvis CHANGED
@@ -28,10 +28,9 @@ usage unless ARGV[0]
28
28
  usage if ARGV[0] == '--help'
29
29
 
30
30
  meta, data = get_meta2(BUD_DBM_DIR)
31
- vh = VizHelper.new(meta[:tabinf], meta[:cycle], meta[:depends], meta[:rules], ARGV[0])
31
+ vh = VizHelper.new(meta[:tabinf], meta[:cycle], meta[:depends], meta[:rules], ARGV[0], meta[:provides])
32
32
  data.each do |d|
33
33
  vh.full_info << d
34
34
  end
35
-
36
35
  vh.tick
37
36
  vh.summarize(ARGV[0], meta[:schminf])
data/docs/cheat.md CHANGED
@@ -196,7 +196,7 @@ implicit map:
196
196
  end
197
197
 
198
198
  ## BudCollection-Specific Methods ##
199
- `bc.schema`: returns the schema of `bc` (Hash of key column names => non-key column names)<br>
199
+ `bc.schema`: returns the schema of `bc` (Hash of key column names => non-key column names). Note that for channels, this omits the location specifier (<tt>@</tt>).<br>
200
200
 
201
201
  `bc.cols`: returns the column names in `bc` as an Array<br>
202
202
 
data/lib/bud/aggs.rb CHANGED
@@ -181,7 +181,8 @@ module Bud
181
181
  end
182
182
 
183
183
  # aggregate method to be used in Bud::BudCollection.group.
184
- # accumulates all x inputs into an array
184
+ # accumulates all x inputs into an array. note that the order of the elements
185
+ # in the resulting array is undefined.
185
186
  def accum(x)
186
187
  [Accum.new, x]
187
188
  end
data/lib/bud/bud_meta.rb CHANGED
@@ -49,7 +49,7 @@ class BudMeta #:nodoc: all
49
49
  @bud_instance.sinks[s.first] = true
50
50
  end
51
51
 
52
- dump_rewrite(rewritten_strata) if @bud_instance.options[:dump_rewrite]
52
+ dump_rewrite(no_attr_rewrite_strata) if @bud_instance.options[:dump_rewrite]
53
53
 
54
54
  return rewritten_strata, no_attr_rewrite_strata
55
55
  end
@@ -97,13 +97,13 @@ class BudMeta #:nodoc: all
97
97
  unless rv.nil?
98
98
  if rv.class <= Sexp
99
99
  error_pt = rv
100
- error_msg = "Parse error"
100
+ error_msg = "parse error"
101
101
  else
102
102
  error_pt, error_msg = rv
103
103
  end
104
104
 
105
- # try to "generate" the source code associated with the problematic block,
106
- # so as to generate a more meaningful error message.
105
+ # try to dump the source code associated with the problematic block, so as
106
+ # to produce a more meaningful error message.
107
107
  begin
108
108
  code = Ruby2Ruby.new.process(Marshal.load(Marshal.dump(error_pt)))
109
109
  src_msg = "\nCode: #{code}"
@@ -139,7 +139,7 @@ class BudMeta #:nodoc: all
139
139
 
140
140
  # Check for a common case
141
141
  if n.sexp_type == :lasgn
142
- return [n, "Illegal operator: '='"]
142
+ return [n, "illegal operator: '='"]
143
143
  end
144
144
  return pt unless n.sexp_type == :call and n.length == 4
145
145
 
@@ -150,10 +150,10 @@ class BudMeta #:nodoc: all
150
150
  return n if lhs.nil? or lhs.sexp_type != :call
151
151
  lhs_name = lhs[2].to_sym
152
152
  unless @bud_instance.tables.has_key? lhs_name
153
- return [n, "Table does not exist: '#{lhs_name}'"]
153
+ return [n, "collection does not exist: '#{lhs_name}'"]
154
154
  end
155
155
 
156
- return [n, "Illegal operator: '#{op}'"] unless [:<, :<=].include? op
156
+ return [n, "illegal operator: '#{op}'"] unless [:<, :<=].include? op
157
157
 
158
158
  # Check superator invocation. A superator that begins with "<" is parsed
159
159
  # as a call to the binary :< operator. The right operand to :< is a :call
@@ -195,6 +195,7 @@ class BudMeta #:nodoc: all
195
195
  else
196
196
  top = 1
197
197
  end
198
+
198
199
  return top
199
200
  end
200
201
 
@@ -149,7 +149,7 @@ module Bud
149
149
  # project the collection to its key attributes
150
150
  public
151
151
  def keys
152
- self.map{|t| @key_colnums.map {|i| t[i]}}
152
+ self.map{|t| get_key_vals(t)}
153
153
  end
154
154
 
155
155
  # project the collection to its non-key attributes
@@ -255,6 +255,7 @@ module Bud
255
255
  def [](k)
256
256
  # assumes that key is in storage or delta, but not both
257
257
  # is this enforced in do_insert?
258
+ check_enumerable(k)
258
259
  t = @storage[k]
259
260
  return t.nil? ? @delta[k] : t
260
261
  end
@@ -264,7 +265,7 @@ module Bud
264
265
  def include?(item)
265
266
  return true if key_cols.nil? or (key_cols.empty? and length > 0)
266
267
  return false if item.nil? or item.empty?
267
- key = @key_colnums.map{|i| item[i]}
268
+ key = get_key_vals(item)
268
269
  return (item == self[key])
269
270
  end
270
271
 
@@ -282,8 +283,8 @@ module Bud
282
283
 
283
284
  private
284
285
  def raise_pk_error(new_guy, old)
285
- keycols = @key_colnums.map{|i| old[i]}
286
- raise KeyConstraintError, "key conflict inserting #{new_guy.inspect} into \"#{tabname}\": existing tuple #{old.inspect}, key_cols = #{keycols.inspect}"
286
+ key = get_key_vals(old)
287
+ raise KeyConstraintError, "key conflict inserting #{new_guy.inspect} into \"#{tabname}\": existing tuple #{old.inspect}, key = #{key.inspect}"
287
288
  end
288
289
 
289
290
  private
@@ -308,15 +309,22 @@ module Bud
308
309
  return o
309
310
  end
310
311
 
312
+ private
313
+ def get_key_vals(t)
314
+ @key_colnums.map do |i|
315
+ t[i]
316
+ end
317
+ end
318
+
311
319
  private
312
320
  def do_insert(o, store)
313
321
  return if o.nil? # silently ignore nils resulting from map predicates failing
314
322
  o = prep_tuple(o)
315
- keycols = @key_colnums.map{|i| o[i]}
323
+ key = get_key_vals(o)
316
324
 
317
- old = store[keycols]
325
+ old = store[key]
318
326
  if old.nil?
319
- store[keycols] = tuple_accessors(o)
327
+ store[key] = tuple_accessors(o)
320
328
  else
321
329
  raise_pk_error(o, old) unless old == o
322
330
  end
@@ -374,10 +382,10 @@ module Bud
374
382
  end
375
383
 
376
384
  private
377
- def include_any_buf?(t, key_vals)
385
+ def include_any_buf?(t, key)
378
386
  bufs = [self, @delta, @new_delta]
379
387
  bufs.each do |b|
380
- old = b[key_vals]
388
+ old = b[key]
381
389
  next if old.nil?
382
390
  if old != t
383
391
  raise_pk_error(t, old)
@@ -391,16 +399,15 @@ module Bud
391
399
  public
392
400
  def merge(o, buf=@new_delta) # :nodoc: all
393
401
  unless o.nil?
394
- o = o.uniq if o.respond_to?(:uniq)
395
402
  check_enumerable(o)
396
403
  establish_schema(o) if @cols.nil?
397
404
 
398
- # it's a pity that we are massaging the tuples that already exist in the head
405
+ # it's a pity that we are massaging tuples that may be dups
399
406
  o.each do |t|
400
407
  next if t.nil? or t == []
401
408
  t = prep_tuple(t)
402
- key_vals = @key_colnums.map{|k| t[k]}
403
- buf[key_vals] = tuple_accessors(t) unless include_any_buf?(t, key_vals)
409
+ key = get_key_vals(t)
410
+ buf[key] = tuple_accessors(t) unless include_any_buf?(t, key)
404
411
  end
405
412
  end
406
413
  return self
@@ -432,7 +439,7 @@ module Bud
432
439
  self <+ o
433
440
  self <- o.map do |t|
434
441
  unless t.nil?
435
- self[@key_colnums.map{|k| t[k]}]
442
+ self[get_key_vals(t)]
436
443
  end
437
444
  end
438
445
  end
@@ -461,9 +468,14 @@ module Bud
461
468
  @new_delta = {}
462
469
  end
463
470
 
464
- private
465
- def method_missing(sym, *args, &block)
466
- @storage.send sym, *args, &block
471
+ public
472
+ def length
473
+ @storage.length
474
+ end
475
+
476
+ public
477
+ def empty?
478
+ @storage.empty?
467
479
  end
468
480
 
469
481
  ######## aggs
@@ -480,7 +492,6 @@ module Bud
480
492
  end
481
493
  end
482
494
 
483
-
484
495
  # a generalization of argmin/argmax to arbitrary exemplary aggregates.
485
496
  # for each distinct value of the grouping key columns, return the items in that group
486
497
  # that have the value of the exemplary aggregate +aggname+
@@ -592,7 +603,7 @@ module Bud
592
603
  # Attributes can be referenced as symbols, or as +collection_name.attribute_name+
593
604
  public
594
605
  def group(key_cols, *aggpairs)
595
- key_cols = [] if key_cols.nil?
606
+ key_cols ||= []
596
607
  keynames = key_cols.map do |k|
597
608
  if k.class == Symbol
598
609
  k
@@ -892,15 +903,15 @@ module Bud
892
903
  public
893
904
  def tick #:nodoc: all
894
905
  @to_delete.each do |tuple|
895
- keycols = @key_colnums.map{|k| tuple[k]}
896
- if @storage[keycols] == tuple
897
- @storage.delete keycols
906
+ key = get_key_vals(tuple)
907
+ if @storage[key] == tuple
908
+ @storage.delete key
898
909
  end
899
910
  end
900
- @pending.each do |keycols, tuple|
901
- old = @storage[keycols]
911
+ @pending.each do |key, tuple|
912
+ old = @storage[key]
902
913
  if old.nil?
903
- @storage[keycols] = tuple
914
+ @storage[key] = tuple
904
915
  else
905
916
  raise_pk_error(tuple, old) unless tuple == old
906
917
  end
@@ -957,26 +968,3 @@ module Bud
957
968
  end
958
969
  end
959
970
  end
960
-
961
- module Enumerable
962
- public
963
- # monkeypatch to Enumerable to rename collections and their schemas
964
- def rename(new_tabname, new_schema=nil)
965
- budi = (respond_to?(:bud_instance)) ? bud_instance : nil
966
- if new_schema.nil? and respond_to?(:schema)
967
- new_schema = schema
968
- end
969
- scr = Bud::BudScratch.new(new_tabname.to_s, budi, new_schema)
970
- scr.uniquify_tabname
971
- scr.merge(self, scr.storage)
972
- scr
973
- end
974
-
975
- public
976
- # We rewrite "map" calls in Bloom blocks to invoke the "pro" method
977
- # instead. This is fine when applied to a BudCollection; when applied to a
978
- # normal Enumerable, just treat pro as an alias for map.
979
- def pro(&blk)
980
- map(&blk)
981
- end
982
- end
data/lib/bud/graphs.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  require 'rubygems'
2
- require 'digest/md5'
3
2
  require 'graphviz'
4
3
 
5
4
  class GraphGen #:nodoc: all
6
5
  attr_reader :nodes
7
6
 
8
- def initialize(tableinfo, builtin_tables, cycle, name, budtime, collapse=false, cardinalities={})
7
+ def initialize(tableinfo, builtin_tables, cycle, name, budtime, collapse=false, cardinalities={}, pathsto={}, begins={})
9
8
  @graph = GraphViz.new(:G, :type => :digraph, :label => "")
9
+ #@graph.dim = 2
10
10
  @graph.node[:fontname] = "Times-Roman"
11
11
  @graph.node[:fontsize] = 18
12
12
  @graph.edge[:fontname] = "Times-Roman"
@@ -16,6 +16,8 @@ class GraphGen #:nodoc: all
16
16
  @collapse = collapse
17
17
  @budtime = budtime
18
18
  @builtin_tables = builtin_tables
19
+ @pathsto = pathsto
20
+ @begins = begins
19
21
 
20
22
  # map: table name -> type
21
23
  @tabinf = {}
@@ -87,20 +89,46 @@ class GraphGen #:nodoc: all
87
89
  end
88
90
  end
89
91
 
92
+ def color_node(paths)
93
+ return "" if paths.nil?
94
+
95
+ case paths[0][:val]
96
+ when :A, :N
97
+ "yellow"
98
+ when :D, :G
99
+ "red"
100
+ else
101
+ puts "UNKNOWN tag #{paths[0][:val]} class #{paths[0][:val].class}"
102
+ "black"
103
+ end
104
+ end
105
+
90
106
  def addonce(node, negcluster, inhead=false)
91
107
  if !@nodes[node]
92
- @nodes[node] = @graph.add_node("n_#{node}")
108
+ @nodes[node] = @graph.add_nodes(node)
109
+ node_p = @nodes[node]
110
+ node_p.label = node
111
+ if @begins[:finish] and @begins[:finish][node]
112
+ # point of divergence.
113
+ node_p.penwidth = 4
114
+ end
115
+
93
116
  if @cards and @cards[node]
94
- @nodes[node].label = "#{node}\n (#{@cards[node].to_s})"
117
+ node_p.label = "#{node}\n (#{@cards[node].to_s})"
118
+ node_p.color = "green"
95
119
  else
96
- @nodes[node].label = node
120
+ p = @pathsto[node].nil? ? "" : "\n(#{@pathsto[node][0][:val]})"
121
+ node_p.label = node + p
122
+ node_p.color = color_node(@pathsto[node])
97
123
  end
124
+ else
125
+ node_p = @nodes[node]
98
126
  end
99
127
 
100
128
  if @budtime == -1
101
- @nodes[node].URL = "#{node}.html" if inhead
129
+ node_p.URL = "#{node}.html" if inhead
102
130
  else
103
- @nodes[node].URL = "javascript:openWin(\"#{node}\", #{@budtime})"
131
+ node_p.URL = "javascript:openWin(\"#{node}\", #{@budtime})"
104
132
  end
105
133
 
106
134
  if negcluster
@@ -116,13 +144,13 @@ class GraphGen #:nodoc: all
116
144
  res = res + ", " + p
117
145
  end
118
146
  end
119
- @nodes[node].label = res
120
- @nodes[node].color = "red"
121
- @nodes[node].shape = "octagon"
122
- @nodes[node].penwidth = 3
123
- @nodes[node].URL = "#{File.basename(@name).gsub(".staging", "").gsub("collapsed", "expanded")}.svg"
147
+ node_p.label = res
148
+ node_p.color = "red"
149
+ node_p.shape = "octagon"
150
+ node_p.penwidth = 3
151
+ node_p.URL = "#{File.basename(@name).gsub(".staging", "").gsub("collapsed", "expanded")}.svg"
124
152
  elsif @tabinf[node] and (@tabinf[node] == "Bud::BudTable")
125
- @nodes[node].shape = "rect"
153
+ node_p.shape = "rect"
126
154
  end
127
155
  end
128
156
 
@@ -134,9 +162,10 @@ class GraphGen #:nodoc: all
134
162
 
135
163
  ekey = body + head
136
164
  if !@edges[ekey]
137
- @edges[ekey] = @graph.add_edge(@nodes[body], @nodes[head], :penwidth => 5)
165
+ @edges[ekey] = @graph.add_edges(@nodes[body], @nodes[head], :penwidth => 5)
138
166
  @edges[ekey].arrowsize = 2
139
167
 
168
+ @edges[ekey].color = (@nodes[body]["color"].source || "")
140
169
  @edges[ekey].URL = "#{rule_id}.html" unless rule_id.nil?
141
170
  if head =~ /_msg\z/
142
171
  @edges[ekey].minlen = 2
@@ -212,7 +241,7 @@ class GraphGen #:nodoc: all
212
241
  if output.nil?
213
242
  @graph.output(:svg => @name)
214
243
  else
215
- @graph.output(output => @name)
244
+ @graph.output(output.to_sym => @name)
216
245
  end
217
246
  end
218
247
  end
@@ -234,9 +263,8 @@ class SpaceTime
234
263
  @head = {}
235
264
  last = nil
236
265
  processes.each_with_index do |p, i|
237
- #@head[p] = @hdr.add_node("process #{p}(#{i})")#, :color => "white", :label => "")
238
266
  @subs[p] = @g.subgraph("buster_#{i+1}")
239
- @head[p] = @hdr.add_node("process #{p}(#{i})", :group => p)#, :color => "white", :label => "")
267
+ @head[p] = @hdr.add_nodes("process #{p}(#{i})", :group => p)#, :color => "white", :label => "")
240
268
  end
241
269
  end
242
270
 
@@ -272,10 +300,9 @@ class SpaceTime
272
300
  if @links
273
301
  url = "DBM_#{k}/tm_#{item}.svg"
274
302
  end
275
- snd = @subs[k].add_node(label, {:label => item.to_s, :width => 0.1, :height => 0.1, :fontsize => 6, :pos => [1, i], :group => k, :URL => url})
276
-
303
+ snd = @subs[k].add_nodes(label, {:label => item.to_s, :width => 0.1, :height => 0.1, :fontsize => 6, :group => k, :URL => url})
277
304
  unless @head[k].id == snd.id
278
- @subs[k].add_edge(@head[k], snd, :weight => 2)
305
+ @subs[k].add_edges(@head[k], snd, :weight => 2)
279
306
  @head[k] = snd
280
307
  end
281
308
  end
@@ -295,7 +322,7 @@ class SpaceTime
295
322
  def finish(file, fmt=nil)
296
323
  @edges.each_pair do |k, v|
297
324
  lbl = v[3] > 1 ? "#{v[2]}(#{v[3]})" : v[2]
298
- @g.add_edge(v[0], v[1], :label => lbl, :color => "red", :weight => 1)
325
+ @g.add_edges(v[0], v[1], :label => lbl, :color => "red", :weight => 1)
299
326
  end
300
327
  if fmt.nil?
301
328
  @g.output(:svg => "#{file}.svg")
data/lib/bud/joins.rb CHANGED
@@ -291,7 +291,7 @@ module Bud
291
291
  otherpreds = allpreds - @localpreds
292
292
  unless otherpreds.empty?
293
293
  unless @rels[1].class <= Bud::BudJoin
294
- raise Bud::CompileError, "join predicates don't match tables being joined: #{otherpreds.inspect}"
294
+ raise Bud::CompileError, "join predicates don't match collections being joined: #{otherpreds.inspect}"
295
295
  end
296
296
  @rels[1].setup_preds(otherpreds)
297
297
  end