bud 0.9.4 → 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,3 @@
1
- require 'set'
2
-
3
1
  module Bud
4
2
  ######## Agg definitions
5
3
  class Agg #:nodoc: all
@@ -38,7 +36,7 @@ module Bud
38
36
 
39
37
  class Min < ArgExemplary #:nodoc: all
40
38
  def trans(the_state, val)
41
- if the_state < val
39
+ if (the_state <=> val) < 0
42
40
  return the_state, :ignore
43
41
  elsif the_state == val
44
42
  return the_state, :keep
@@ -55,8 +53,10 @@ module Bud
55
53
 
56
54
  class Max < ArgExemplary #:nodoc: all
57
55
  def trans(the_state, val)
58
- if the_state > val
56
+ if (the_state <=> val) > 0
59
57
  return the_state, :ignore
58
+ elsif the_state == val
59
+ return the_state, :keep
60
60
  else
61
61
  return val, :replace
62
62
  end
@@ -207,4 +207,20 @@ module Bud
207
207
  def accum(x)
208
208
  [Accum.new, x]
209
209
  end
210
+
211
+ class AccumPair < Agg #:nodoc: all
212
+ def init(fst, snd)
213
+ [[fst, snd]].to_set
214
+ end
215
+ def trans(the_state, fst, snd)
216
+ the_state << [fst, snd]
217
+ return the_state, nil
218
+ end
219
+ end
220
+
221
+ # aggregate method to be used in Bud::BudCollection.group.
222
+ # accumulates x, y inputs into a set of pairs (two element arrays).
223
+ def accum_pair(x, y)
224
+ [AccumPair.new, x, y]
225
+ end
210
226
  end
@@ -1,11 +1,14 @@
1
1
  require 'bud/rewrite'
2
2
 
3
-
4
3
  class BudMeta #:nodoc: all
5
- def initialize(bud_instance, declarations)
6
- @bud_instance = bud_instance
7
- @declarations = declarations
8
- @dependency_analysis = nil # the results of bud_meta are analyzed further using a helper bloom instance. See depanalysis())
4
+ def initialize(bud_i)
5
+ @bud_instance = bud_i
6
+ @declarations = bud_i.methods.select {|m| m =~ /^__bloom__.+$/}.map {|m| m.to_s}
7
+ @rule_idx = 0
8
+
9
+ # The results of bud_meta are analyzed further using a helper Bloom
10
+ # instance. See depanalysis().
11
+ @dependency_analysis = nil
9
12
  end
10
13
 
11
14
  def meta_rewrite
@@ -13,20 +16,30 @@ class BudMeta #:nodoc: all
13
16
 
14
17
  stratified_rules = []
15
18
  if @bud_instance.toplevel == @bud_instance
16
- nodes, stratum_map, top_stratum = stratify_preds
19
+ nodes = compute_node_graph
20
+ analyze_dependencies(nodes)
21
+
22
+ stratum_map = stratify_preds(nodes)
23
+ top_stratum = stratum_map.values.max
24
+ top_stratum ||= -1
17
25
 
18
26
  # stratum_map = {fully qualified pred => stratum}. Copy stratum_map data
19
27
  # into t_stratum format.
20
28
  raise unless @bud_instance.t_stratum.to_a.empty?
21
- @bud_instance.t_stratum <= stratum_map.to_a
29
+ @bud_instance.t_stratum.merge(stratum_map.to_a)
22
30
 
23
- # slot each rule into the stratum corresponding to its lhs pred (from stratum_map)
24
31
  stratified_rules = Array.new(top_stratum + 2) { [] } # stratum -> [ rules ]
25
32
  @bud_instance.t_rules.each do |rule|
26
33
  if rule.op == '<='
27
34
  # Deductive rules are assigned to strata based on the basic Datalog
28
- # stratification algorithm
29
- belongs_in = stratum_map[rule.lhs]
35
+ # stratification algorithm. Note that we don't place all the rules
36
+ # with a given lhs relation in the same strata; rather, we place a
37
+ # rule in the lowest strata we can, as determined by the rule's rhs
38
+ # relations (and whether the rel is used in a non-monotonic context).
39
+ body_rels = find_body_rels(rule)
40
+ body_strata = body_rels.map {|r,is_nm| stratum_map[r] + (is_nm ? 1 : 0) || 0}
41
+ belongs_in = body_strata.max
42
+
30
43
  # If the rule body doesn't reference any collections, it won't be
31
44
  # assigned a stratum, so just place it in stratum zero
32
45
  belongs_in ||= 0
@@ -36,28 +49,39 @@ class BudMeta #:nodoc: all
36
49
  stratified_rules[top_stratum + 1] << rule
37
50
  end
38
51
  end
39
- # stratified_rules[0] may be empty if none of the nodes at stratum 0 are on the lhs
40
- # stratified_rules[top_stratum+1] will be empty if there are no temporal rules.
41
- # Cleanup
42
- stratified_rules = stratified_rules.reject{|r| r.empty?}
52
+
53
+ # stratified_rules[0] may be empty if none of the nodes at stratum 0 are
54
+ # on the lhs stratified_rules[top_stratum+1] will be empty if there are no
55
+ # temporal rules.
56
+ stratified_rules.reject! {|r| r.empty?}
57
+
58
+ stratified_rules.each_with_index do |strat,strat_num|
59
+ strat.each do |rule|
60
+ @bud_instance.t_rule_stratum << [rule.bud_obj, rule.rule_id, strat_num]
61
+ end
62
+ end
63
+
43
64
  dump_rewrite(stratified_rules) if @bud_instance.options[:dump_rewrite]
44
65
  end
45
66
  return stratified_rules
46
67
  end
47
68
 
69
+ def find_body_rels(rule)
70
+ @bud_instance.t_depends.map do |d|
71
+ [d.body, d.nm] if d.rule_id == rule.rule_id and d.bud_obj == rule.bud_obj
72
+ end.compact
73
+ end
74
+
48
75
  def shred_rules
49
- # to completely characterize the rules of a bud class we must extract
50
- # from all parent classes/modules
51
- # after making this pass, we no longer care about the names of methods.
52
- # we are shredding down to the granularity of rule heads.
53
- seed = 0
76
+ # After making this pass, we no longer care about the names of methods. We
77
+ # are shredding down to the granularity of rule heads.
54
78
  rulebag = {}
55
79
  @bud_instance.class.ancestors.reverse.each do |anc|
56
80
  @declarations.each do |meth_name|
57
- rw = rewrite_rule_block(anc, meth_name, seed)
81
+ rw = rewrite_rule_block(anc, meth_name)
58
82
  if rw
59
- seed = rw.rule_indx
60
83
  rulebag[meth_name] = rw
84
+ @rule_idx = rw.rule_idx
61
85
  end
62
86
  end
63
87
  end
@@ -68,7 +92,7 @@ class BudMeta #:nodoc: all
68
92
  end
69
93
  end
70
94
 
71
- def rewrite_rule_block(klass, block_name, seed)
95
+ def rewrite_rule_block(klass, block_name)
72
96
  return unless klass.respond_to? :__bloom_asts__
73
97
 
74
98
  pt = klass.__bloom_asts__[block_name]
@@ -104,18 +128,18 @@ class BudMeta #:nodoc: all
104
128
  end
105
129
  raise Bud::CompileError, "#{error_msg} in rule block \"#{block_name}\"#{src_msg}"
106
130
  end
107
- rewriter = RuleRewriter.new(seed, @bud_instance)
131
+ rewriter = RuleRewriter.new(@bud_instance, @rule_idx)
108
132
  rewriter.process(pt)
109
133
  return rewriter
110
134
  end
111
135
 
112
136
  def get_qual_name(pt)
113
137
  # expect to see a parse tree corresponding to a dotted name
114
- # a.b.c == s(:call, s1, :c, (:args))
115
- # where s1 == s(:call, s2, :b, (:args))
116
- # where s2 == s(:call, nil, :a, (:args))
117
- tag, recv, name, args = pt
118
- return nil unless tag == :call and args.length == 1
138
+ # a.b.c == s(:call, s1, :c)
139
+ # where s1 == s(:call, s2, :b)
140
+ # where s2 == s(:call, nil, :a)
141
+ tag, recv, name, *args = pt
142
+ return nil unless tag == :call and args.empty?
119
143
 
120
144
  if recv
121
145
  qn = get_qual_name(recv)
@@ -127,23 +151,16 @@ class BudMeta #:nodoc: all
127
151
  end
128
152
 
129
153
  # Perform some basic sanity checks on the AST of a rule block. We expect a
130
- # rule block to consist of a :defn, a nested :scope, and then a sequence of
154
+ # rule block to consist of a :defn whose body consists of a sequence of
131
155
  # statements. Each statement is a :call node. Returns nil (no error found), a
132
156
  # Sexp (containing an error), or a pair of [Sexp, error message].
133
157
  def check_rule_ast(pt)
134
- # :defn format: node tag, block name, args, nested scope
135
- return pt if pt.sexp_type != :defn
136
- scope = pt[3]
137
- return pt if scope.sexp_type != :scope
138
- block = scope[1]
139
-
140
- block.each_with_index do |n,i|
141
- if i == 0
142
- return pt if n != :block
143
- next
144
- end
158
+ # :defn format: node tag, block name, args, body_0, ..., body_n
159
+ tag, name, args, *body = pt
160
+ return pt if tag != :defn
145
161
 
146
- next if i == 1 and n.sexp_type == :nil # a block got rewritten to an empty block
162
+ body.each_with_index do |n,i|
163
+ next if i == 0 and n == s(:nil) # a block got rewritten to an empty block
147
164
 
148
165
  # Check for a common case
149
166
  if n.sexp_type == :lasgn
@@ -157,8 +174,9 @@ class BudMeta #:nodoc: all
157
174
  # Check that LHS references a named collection
158
175
  lhs_name = get_qual_name(lhs)
159
176
  return [n, "unexpected lhs format: #{lhs}"] if lhs_name.nil?
160
- unless @bud_instance.tables.has_key? lhs_name.to_sym
161
- return [n, "collection does not exist: '#{lhs_name}'"]
177
+ unless @bud_instance.tables.has_key? lhs_name.to_sym or
178
+ @bud_instance.lattices.has_key? lhs_name.to_sym
179
+ return [n, "Collection does not exist: '#{lhs_name}'"]
162
180
  end
163
181
 
164
182
  return [n, "illegal operator: '#{op}'"] unless [:<, :<=].include? op
@@ -171,13 +189,11 @@ class BudMeta #:nodoc: all
171
189
  # XXX: We don't check for illegal superators (e.g., "<--"). That would be
172
190
  # tricky, because they are encoded as a nested unary op in the rule body.
173
191
  if op == :<
174
- return n unless rhs.sexp_type == :arglist
175
- body = rhs[1]
176
- return n unless body.sexp_type == :call
177
- op_tail = body[2]
192
+ return n unless rhs.sexp_type == :call
193
+ op_tail = rhs[2]
178
194
  return n unless [:~, :-@, :+@].include? op_tail
179
- rhs_args = body[3]
180
- return n if rhs_args.sexp_type != :arglist or rhs_args.length != 1
195
+ rhs_args = rhs[3..-1]
196
+ return n unless rhs_args.empty?
181
197
  end
182
198
  end
183
199
 
@@ -185,64 +201,61 @@ class BudMeta #:nodoc: all
185
201
  end
186
202
 
187
203
 
188
- Node = Struct.new :name, :status, :stratum, :edges, :in_lhs, :in_body, :in_cycle, :is_neg_head
189
204
  # Node.status is one of :init, :in_progress, :done
205
+ Node = Struct.new :name, :status, :stratum, :edges, :in_lhs, :in_body, :already_neg
190
206
  Edge = Struct.new :to, :op, :neg, :temporal
191
207
 
192
- def stratify_preds
193
- bud = @bud_instance.toplevel
208
+ def compute_node_graph
194
209
  nodes = {}
195
- bud.t_depends.each do |d|
196
- #t_depends [:bud_instance, :rule_id, :lhs, :op, :body] => [:nm]
197
- lhs = (nodes[d.lhs] ||= Node.new(d.lhs, :init, 0, [], true, false, false, false))
210
+ @bud_instance.toplevel.t_depends.each do |d|
211
+ # t_depends [:bud_instance, :rule_id, :lhs, :op, :body] => [:nm, :in_body]
212
+ lhs = (nodes[d.lhs] ||= Node.new(d.lhs, :init, 0, [], true, false))
198
213
  lhs.in_lhs = true
199
- body = (nodes[d.body] ||= Node.new(d.body, :init, 0, [], false, true, false, false))
200
- temporal = d.op != "<="
201
- lhs.edges << Edge.new(body, d.op, d.nm, temporal)
214
+ body = (nodes[d.body] ||= Node.new(d.body, :init, 0, [], false, true))
202
215
  body.in_body = true
216
+ temporal = (d.op != "<=")
217
+ lhs.edges << Edge.new(body, d.op, d.nm, temporal)
203
218
  end
204
219
 
220
+ return nodes
221
+ end
222
+
223
+ def stratify_preds(nodes)
205
224
  nodes.each_value {|n| calc_stratum(n, false, false, [n.name])}
225
+
206
226
  # Normalize stratum numbers because they may not be 0-based or consecutive
207
227
  remap = {}
208
228
  # if the nodes stratum numbers are [2, 3, 2, 4], remap = {2 => 0, 3 => 1, 4 => 2}
209
229
  nodes.values.map {|n| n.stratum}.uniq.sort.each_with_index do |num, i|
210
230
  remap[num] = i
211
231
  end
232
+
212
233
  stratum_map = {}
213
- top_stratum = -1
214
234
  nodes.each_pair do |name, n|
215
235
  n.stratum = remap[n.stratum]
216
236
  stratum_map[n.name] = n.stratum
217
- top_stratum = max(top_stratum, n.stratum)
218
237
  end
219
- analyze_dependencies(nodes)
220
- return nodes, stratum_map, top_stratum
238
+ return stratum_map
221
239
  end
222
240
 
223
- def max(a, b) ; a > b ? a : b ; end
224
-
225
241
  def calc_stratum(node, neg, temporal, path)
226
242
  if node.status == :in_process
227
- node.in_cycle = true
228
- if neg and !temporal and node.is_neg_head
243
+ if neg and not temporal and not node.already_neg
229
244
  raise Bud::CompileError, "unstratifiable program: #{path.uniq.join(',')}"
230
245
  end
231
246
  elsif node.status == :init
232
247
  node.status = :in_process
233
248
  node.edges.each do |edge|
234
- node.is_neg_head = edge.neg
235
249
  next unless edge.op == "<="
250
+ node.already_neg = neg
236
251
  body_stratum = calc_stratum(edge.to, (neg or edge.neg), (edge.temporal or temporal), path + [edge.to.name])
237
- node.is_neg_head = false # reset for next edge
238
- node.stratum = max(node.stratum, body_stratum + (edge.neg ? 1 : 0))
252
+ node.stratum = [node.stratum, body_stratum + (edge.neg ? 1 : 0)].max
239
253
  end
240
254
  node.status = :done
241
255
  end
242
256
  node.stratum
243
257
  end
244
258
 
245
-
246
259
  def analyze_dependencies(nodes) # nodes = {node name => node}
247
260
  preds_in_lhs = nodes.select {|_, node| node.in_lhs}.map {|name, _| name}.to_set
248
261
  preds_in_body = nodes.select {|_, node| node.in_body}.map {|name, _| name}.to_set
@@ -274,9 +287,6 @@ class BudMeta #:nodoc: all
274
287
  da = ::DepAnalysis.new
275
288
  da.providing <+ @bud_instance.tables[:t_provides].to_a
276
289
  da.depends <+ @bud_instance.t_depends.map{|d| [d.lhs, d.op, d.body, d.nm]}
277
-
278
- #@bud_instance.tables[:t_provides].each {|t| da.providing <+ t}
279
- #@bud_instance.tables[:t_depends].each {|t| da.depends_tc <+ t}
280
290
  da.tick_internal
281
291
  @dependency_analysis = da
282
292
  end
@@ -1,6 +1,3 @@
1
- require 'msgpack'
2
-
3
- $struct_classes = {}
4
1
  module Bud
5
2
  ########
6
3
  #--
@@ -16,11 +13,11 @@ module Bud
16
13
  class BudCollection
17
14
  include Enumerable
18
15
 
19
- attr_accessor :bud_instance, :tabname # :nodoc: all
20
- attr_reader :cols, :key_cols # :nodoc: all
16
+ attr_accessor :bud_instance # :nodoc: all
17
+ attr_reader :tabname, :cols, :key_cols # :nodoc: all
21
18
  attr_reader :struct
22
- attr_reader :storage, :delta, :new_delta, :pending, :tick_delta # :nodoc: all
23
- attr_reader :wired_by, :to_delete
19
+ attr_reader :new_delta, :pending # :nodoc: all
20
+ attr_reader :wired_by, :scanner_cnt
24
21
  attr_accessor :invalidated, :rescan
25
22
  attr_accessor :is_source
26
23
  attr_accessor :accumulate_tick_deltas # updated in bud.do_wiring
@@ -30,6 +27,7 @@ module Bud
30
27
  @bud_instance = bud_instance
31
28
  @invalidated = true
32
29
  @is_source = true # unless it shows up on the lhs of some rule
30
+ @scanner_cnt = 0
33
31
  @wired_by = []
34
32
  @accumulate_tick_deltas = false
35
33
  init_schema(given_schema) unless given_schema.nil? and defer_schema
@@ -54,16 +52,17 @@ module Bud
54
52
  # user-specified schema.
55
53
  @cols.each do |s|
56
54
  if s.to_s.start_with? "@"
57
- raise Bud::Error, "illegal use of location specifier (@) in column #{s} of non-channel collection #{tabname}"
55
+ raise Bud::CompileError, "illegal use of location specifier (@) in column #{s} of non-channel collection #{tabname}"
58
56
  end
59
57
  end
60
58
 
61
59
  @key_colnums = @key_cols.map {|k| @cols.index(k)}
60
+ @val_colnums = val_cols.map {|k| @cols.index(k)}
62
61
 
63
62
  if @cols.empty?
64
63
  @cols = nil
65
64
  else
66
- @struct = ($struct_classes[@cols] ||= Struct.new(*@cols))
65
+ @struct = Bud::TupleStruct.new_struct(@cols)
67
66
  @structlen = @struct.members.length
68
67
  end
69
68
  setup_accessors
@@ -132,11 +131,19 @@ module Bud
132
131
  end
133
132
  end
134
133
 
135
- # set up schema accessors, which are class methods
134
+ # Setup schema accessors, which are class methods. Note that the same
135
+ # table/column name might appear multiple times on the LHS of a single
136
+ # join (e.g., (foo * bar).combos(foo.x => bar.y, foo.x => bar.z)). Because
137
+ # the join predicates are represented as a hash, we need the two instances
138
+ # of foo.x to be distinct values (otherwise the resulting hash will only
139
+ # have a single key). Hence, we add a unique ID to the value returned by
140
+ # schema accessors.
136
141
  @cols_access = Module.new do
137
142
  sc.each_with_index do |c, i|
138
143
  define_method c do
139
- [@tabname, i, c]
144
+ @counter ||= 0
145
+ @counter += 1
146
+ [qualified_tabname, i, c, @counter]
140
147
  end
141
148
  end
142
149
  end
@@ -189,13 +196,27 @@ module Bud
189
196
  if @bud_instance.wiring?
190
197
  pusher = to_push_elem(the_name, the_schema)
191
198
  # If there is no code block evaluate, use the scanner directly
192
- return pusher if blk.nil?
193
- pusher_pro = pusher.pro(&blk)
194
- pusher_pro.elem_name = the_name
195
- pusher_pro.tabname = the_name
196
- pusher_pro
199
+ pusher = pusher.pro(&blk) unless blk.nil?
200
+ pusher
197
201
  else
198
- @storage.map(&blk)
202
+ rv = []
203
+ self.each do |t|
204
+ t = blk.call(t)
205
+ rv << t unless t.nil?
206
+ end
207
+ rv
208
+ end
209
+ end
210
+
211
+ # XXX: Although we support each_with_index over Bud collections, using it is
212
+ # probably not a great idea: the index assigned to a given collection member
213
+ # is not defined by the language semantics.
214
+ def each_with_index(the_name=tabname, the_schema=schema, &blk)
215
+ if @bud_instance.wiring?
216
+ pusher = to_push_elem(the_name, the_schema)
217
+ pusher.each_with_index(&blk)
218
+ else
219
+ super(&blk)
199
220
  end
200
221
  end
201
222
 
@@ -218,7 +239,7 @@ module Bud
218
239
  end
219
240
  elem.set_block(&f)
220
241
  toplevel.push_elems[[self.object_id, :flatten]] = elem
221
- return elem
242
+ elem
222
243
  else
223
244
  @storage.flat_map(&blk)
224
245
  end
@@ -228,16 +249,18 @@ module Bud
228
249
  def sort(&blk)
229
250
  if @bud_instance.wiring?
230
251
  pusher = self.pro
231
- pusher.sort("sort#{object_id}", @bud_instance, @cols, &blk)
252
+ pusher.sort("sort#{object_id}".to_sym, @bud_instance, @cols, &blk)
232
253
  else
233
- @storage.sort
254
+ @storage.values.sort(&blk)
234
255
  end
235
256
  end
236
257
 
237
258
  def rename(the_name, the_schema=nil, &blk)
238
- raise unless @bud_instance.wiring?
259
+ raise Bud::Error unless @bud_instance.wiring?
239
260
  # a scratch with this name should have been defined during rewriting
240
- raise Bud::Error, "rename failed to define a scratch named #{the_name}" unless @bud_instance.respond_to? the_name
261
+ unless @bud_instance.respond_to? the_name
262
+ raise Bud::Error, "rename failed to define a scratch named #{the_name}"
263
+ end
241
264
  pro(the_name, the_schema, &blk)
242
265
  end
243
266
 
@@ -251,7 +274,12 @@ module Bud
251
274
 
252
275
  public
253
276
  def each_raw(&block)
254
- @storage.each_value(&block)
277
+ each_from([@storage], &block)
278
+ end
279
+
280
+ public
281
+ def each_delta(&block)
282
+ each_from([@delta], &block)
255
283
  end
256
284
 
257
285
  public
@@ -266,43 +294,37 @@ module Bud
266
294
 
267
295
  public
268
296
  def non_temporal_predecessors
269
- @wired_by.select {|elem| elem.outputs.include? self}
297
+ @wired_by.select {|e| e.outputs.include? self}
298
+ end
299
+
300
+ public
301
+ def positive_predecessors
302
+ @wired_by.select {|e| e.outputs.include?(self) || e.pendings.include?(self)}
270
303
  end
271
304
 
272
305
  public
273
306
  def tick_metrics
274
307
  strat_num = bud_instance.this_stratum
275
- rule_num = bud_instance.this_rule
276
- addr = nil
277
308
  addr = bud_instance.ip_port unless bud_instance.port.nil?
309
+ key = { :addr=>addr, :tabname=>qualified_tabname,
310
+ :strat_num=>strat_num}
311
+
278
312
  bud_instance.metrics[:collections] ||= {}
279
- bud_instance.metrics[:collections][{:addr=>addr, :tabname=>qualified_tabname, :strat_num=>strat_num, :rule_num=>rule_num}] ||= 0
280
- bud_instance.metrics[:collections][{:addr=>addr, :tabname=>qualified_tabname, :strat_num=>strat_num, :rule_num=>rule_num}] += 1
313
+ bud_instance.metrics[:collections][key] ||= 0
314
+ bud_instance.metrics[:collections][key] += 1
281
315
  end
282
316
 
283
317
  private
284
318
  def each_from(bufs, &block) # :nodoc: all
319
+ do_metrics = bud_instance.options[:metrics]
285
320
  bufs.each do |b|
286
321
  b.each_value do |v|
287
- tick_metrics if bud_instance and bud_instance.options[:metrics]
322
+ tick_metrics if do_metrics
288
323
  yield v
289
324
  end
290
325
  end
291
326
  end
292
327
 
293
- public
294
- def each_from_sym(buf_syms, &block) # :nodoc: all
295
- bufs = buf_syms.map do |s|
296
- case s
297
- when :storage then @storage
298
- when :delta then @delta
299
- when :new_delta then @new_delta
300
- else raise Bud::Error, "bad symbol passed into each_from_sym"
301
- end
302
- end
303
- each_from(bufs, &block)
304
- end
305
-
306
328
  private
307
329
  def init_storage
308
330
  @storage = {}
@@ -345,7 +367,7 @@ module Bud
345
367
  # checks for +item+ in the collection
346
368
  public
347
369
  def include?(item)
348
- return true if key_cols.nil? or (key_cols.empty? and length > 0)
370
+ return true if key_cols.nil?
349
371
  return false if item.nil?
350
372
  key = get_key_vals(item)
351
373
  return (item == self[key])
@@ -374,13 +396,18 @@ module Bud
374
396
  private
375
397
  def raise_pk_error(new, old)
376
398
  key = get_key_vals(old)
377
- raise Bud::KeyConstraintError, "key conflict inserting #{new.inspect} into \"#{tabname}\": existing tuple #{old.inspect}, key = #{key.inspect}"
399
+ raise Bud::KeyConstraintError, "key conflict inserting #{new.inspect} into \"#{qualified_tabname}\": existing tuple #{old.inspect}, key = #{key.inspect}"
400
+ end
401
+
402
+ private
403
+ def is_lattice_val(v)
404
+ v.kind_of? Bud::Lattice
378
405
  end
379
406
 
380
407
  private
381
408
  def prep_tuple(o)
382
409
  return o if o.class == @struct
383
- if o.class == Array
410
+ if o.kind_of? Array
384
411
  if @struct.nil?
385
412
  sch = (1 .. o.length).map{|i| "c#{i}".to_sym}
386
413
  init_schema(sch)
@@ -391,9 +418,16 @@ module Bud
391
418
  raise Bud::TypeError, "array or struct type expected in \"#{qualified_tabname}\": #{o.inspect}"
392
419
  end
393
420
 
421
+ @key_colnums.each do |i|
422
+ next if i >= o.length
423
+ if is_lattice_val(o[i])
424
+ raise Bud::TypeError, "lattice value cannot be a key for #{qualified_tabname}: #{o[i].inspect}"
425
+ end
426
+ end
394
427
  if o.length > @structlen
395
428
  raise Bud::TypeError, "too many columns for \"#{qualified_tabname}\": #{o.inspect}"
396
429
  end
430
+
397
431
  return @struct.new(*o)
398
432
  end
399
433
 
@@ -411,7 +445,7 @@ module Bud
411
445
  when @delta.object_id; "delta"
412
446
  when @new_delta.object_id; "new_delta"
413
447
  end
414
- puts "#{qualified_tabname}.#{storetype} ==> #{t}"
448
+ puts "#{qualified_tabname}.#{storetype} ==> #{t.inspect}"
415
449
  end
416
450
  return if t.nil? # silently ignore nils resulting from map predicates failing
417
451
  t = prep_tuple(t)
@@ -420,13 +454,44 @@ module Bud
420
454
  end
421
455
 
422
456
  # Merge "tup" with key values "key" into "buf". "old" is an existing tuple
423
- # with the same key columns as "tup" (if any such tuple exists).
457
+ # with the same key columns as "tup" (if any such tuple exists). If "old"
458
+ # exists and "tup" is not a duplicate, check whether the two tuples disagree
459
+ # on a non-key, non-lattice value; if so, raise a PK error. Otherwise,
460
+ # construct and return a merged tuple by using lattice merge functions.
424
461
  private
425
462
  def merge_to_buf(buf, key, tup, old)
426
- if old.nil? # no matching tuple found
463
+ if old.nil? # no matching tuple found
427
464
  buf[key] = tup
428
- elsif old != tup # ignore duplicates
429
- raise_pk_error(tup, old)
465
+ return
466
+ end
467
+ return if tup == old # ignore duplicates
468
+
469
+ # Check for PK violation
470
+ @val_colnums.each do |i|
471
+ old_v = old[i]
472
+ new_v = tup[i]
473
+
474
+ unless old_v == new_v || (is_lattice_val(old_v) && is_lattice_val(new_v))
475
+ raise_pk_error(tup, old)
476
+ end
477
+ end
478
+
479
+ # Construct new tuple version. We discard the newly-constructed tuple if
480
+ # merging every lattice field doesn't yield a new value.
481
+ new_t = null_tuple
482
+ saw_change = false
483
+ @val_colnums.each do |i|
484
+ if old[i] == tup[i]
485
+ new_t[i] = old[i]
486
+ else
487
+ new_t[i] = old[i].merge(tup[i])
488
+ saw_change = true if new_t[i].reveal != old[i].reveal
489
+ end
490
+ end
491
+
492
+ if saw_change
493
+ @key_colnums.each {|k| new_t[k] = old[k]}
494
+ buf[key] = new_t
430
495
  end
431
496
  end
432
497
 
@@ -508,6 +573,12 @@ module Bud
508
573
  add_merge_target
509
574
  tbl = register_coll_expr(o)
510
575
  tbl.pro.wire_to self
576
+ elsif o.class <= Bud::LatticePushElement
577
+ add_merge_target
578
+ o.wire_to self
579
+ elsif o.class <= Bud::LatticeWrapper
580
+ add_merge_target
581
+ o.to_push_elem.wire_to self
511
582
  else
512
583
  unless o.nil?
513
584
  o = o.uniq.compact if o.respond_to?(:uniq)
@@ -528,6 +599,10 @@ module Bud
528
599
  public
529
600
  # instantaneously merge items from collection +o+ into +buf+
530
601
  def <=(collection)
602
+ unless bud_instance.toplevel.inside_tick
603
+ raise Bud::CompileError, "illegal use of <= outside of bloom block, use <+ instead"
604
+ end
605
+
531
606
  merge(collection)
532
607
  end
533
608
 
@@ -557,11 +632,22 @@ module Bud
557
632
  add_merge_target
558
633
  tbl = register_coll_expr(o)
559
634
  tbl.pro.wire_to(self, :pending)
635
+ elsif o.class <= Bud::LatticePushElement
636
+ add_merge_target
637
+ o.wire_to(self, :pending)
638
+ elsif o.class <= Bud::LatticeWrapper
639
+ add_merge_target
640
+ o.to_push_elem.wire_to(self, :pending)
560
641
  else
561
642
  pending_merge(o)
562
643
  end
563
644
  end
564
645
 
646
+ superator "<~" do |o|
647
+ # Overridden when <~ is defined (i.e., channels and terminals)
648
+ raise Bud::CompileError, "#{tabname} cannot appear on the lhs of a <~ operator"
649
+ end
650
+
565
651
  def tick
566
652
  raise Bud::Error, "tick must be overriden in #{self.class}"
567
653
  end
@@ -634,6 +720,7 @@ module Bud
634
720
  self, the_schema)
635
721
  toplevel.scanners[this_stratum][[oid, the_name]] = scanner
636
722
  toplevel.push_sources[this_stratum][[oid, the_name]] = scanner
723
+ @scanner_cnt += 1
637
724
  end
638
725
  return toplevel.scanners[this_stratum][[oid, the_name]]
639
726
  end
@@ -656,10 +743,10 @@ module Bud
656
743
  # for each distinct value of the grouping key columns, return the items in that group
657
744
  # that have the value of the exemplary aggregate +aggname+
658
745
  public
659
- def argagg(aggname, gbkey_cols, collection)
746
+ def argagg(aggname, gbkey_cols, collection, &blk)
660
747
  elem = to_push_elem
661
748
  gbkey_cols = gbkey_cols.map{|k| canonicalize_col(k)} unless gbkey_cols.nil?
662
- retval = elem.argagg(aggname, gbkey_cols, canonicalize_col(collection))
749
+ retval = elem.argagg(aggname, gbkey_cols, canonicalize_col(collection), &blk)
663
750
  # PushElement inherits the schema accessors from this Collection
664
751
  retval.extend @cols_access
665
752
  retval
@@ -669,37 +756,46 @@ module Bud
669
756
  # that group that have the minimum value of the attribute +col+. Note that
670
757
  # multiple tuples might be returned.
671
758
  public
672
- def argmin(gbkey_cols, col)
673
- argagg(:min, gbkey_cols, col)
759
+ def argmin(gbkey_cols, col, &blk)
760
+ argagg(:min, gbkey_cols, col, &blk)
674
761
  end
675
762
 
676
763
  # for each distinct value of the grouping key columns, return the items in
677
764
  # that group that have the maximum value of the attribute +col+. Note that
678
765
  # multiple tuples might be returned.
679
766
  public
680
- def argmax(gbkey_cols, col)
681
- argagg(:max, gbkey_cols, col)
767
+ def argmax(gbkey_cols, col, &blk)
768
+ argagg(:max, gbkey_cols, col, &blk)
682
769
  end
683
770
 
684
771
  # form a collection containing all pairs of items in +self+ and items in
685
772
  # +collection+
686
773
  public
687
774
  def *(collection)
688
- elem1 = to_push_elem
689
- return elem1.join(collection)
775
+ return to_push_elem.join(collection)
776
+ end
777
+
778
+ def prep_aggpairs(aggpairs)
779
+ aggpairs.map do |ap|
780
+ agg, *rest = ap
781
+ if rest.empty?
782
+ [agg]
783
+ else
784
+ [agg] + rest.map {|c| canonicalize_col(c)}
785
+ end
786
+ end
690
787
  end
691
788
 
692
789
  def group(key_cols, *aggpairs, &blk)
693
- elem = to_push_elem
694
790
  key_cols = key_cols.map{|k| canonicalize_col(k)} unless key_cols.nil?
695
- aggpairs = aggpairs.map{|ap| [ap[0], canonicalize_col(ap[1])].compact} unless aggpairs.nil?
696
- return elem.group(key_cols, *aggpairs, &blk)
791
+ aggpairs = prep_aggpairs(aggpairs)
792
+ return to_push_elem.group(key_cols, *aggpairs, &blk)
697
793
  end
698
794
 
699
795
  def notin(collection, *preds, &blk)
700
796
  elem1 = to_push_elem
701
797
  elem2 = collection.to_push_elem
702
- return elem1.notin(elem2, preds, &blk)
798
+ return elem1.notin(elem2, *preds, &blk)
703
799
  end
704
800
 
705
801
  def canonicalize_col(col)
@@ -725,6 +821,7 @@ module Bud
725
821
  false
726
822
  end
727
823
 
824
+ # tick_delta for scratches is @storage, so iterate over that instead
728
825
  public
729
826
  def each_tick_delta(&block)
730
827
  @storage.each_value(&block)
@@ -751,6 +848,11 @@ module Bud
751
848
  public
752
849
  def add_rescan_invalidate(rescan, invalidate)
753
850
  srcs = non_temporal_predecessors
851
+
852
+ # XXX: this seems wrong. We might rescan a node for many reasons (e.g.,
853
+ # because another one of the node's outputs needs to be refilled). We only
854
+ # need to invalidate + rescan this scratch if one of the inputs to this
855
+ # collection is *invalidated*.
754
856
  if srcs.any? {|e| rescan.member? e}
755
857
  invalidate << self
756
858
  rescan.merge(srcs)
@@ -786,6 +888,8 @@ module Bud
786
888
  given_schema ||= [:@address, :val]
787
889
  @is_loopback = loopback
788
890
  @locspec_idx = nil
891
+ @wire_buf = StringIO.new
892
+ @packer = MessagePack::Packer.new(@wire_buf)
789
893
 
790
894
  # We're going to mutate the caller's given_schema (to remove the location
791
895
  # specifier), so make a deep copy first. We also save a ref to the
@@ -870,28 +974,66 @@ module Bud
870
974
  raise Bud::Error, "'#{t[@locspec_idx]}', channel '#{@tabname}'" if the_locspec[0].nil? or the_locspec[1].nil? or the_locspec[0] == '' or the_locspec[1] == ''
871
975
  end
872
976
  puts "channel #{qualified_tabname}.send: #{t}" if $BUD_DEBUG
873
- toplevel.dsock.send_datagram([qualified_tabname.to_s, t].to_msgpack,
977
+
978
+ # Convert the tuple into a suitable wire format. Because MsgPack cannot
979
+ # marshal arbitrary Ruby objects that we need to send via channels (in
980
+ # particular, lattice values and Class instances), we first encode such
981
+ # values using Marshal, and then encode the entire tuple with
982
+ # MsgPack. Obviously, this is gross. The wire format also includes an
983
+ # array of indices, indicating which fields hold Marshall'd values.
984
+ @packer.write_array_header(3)
985
+ @packer.write(qualified_tabname.to_s)
986
+ # The second element, wire_tuple, is an array. We will write it one
987
+ # element at a time:
988
+ @packer.write_array_header(t.length)
989
+ @packer.flush
990
+ marshall_indexes = []
991
+ t.each_with_index do |f,i|
992
+ # Performance optimization for cases where we know that we can't
993
+ # marshal the field using MsgPack:
994
+ if [Bud::Lattice, Class].any?{|t| f.class <= t}
995
+ marshall_indexes << i
996
+ @wire_buf << Marshal.dump(f).to_msgpack
997
+ else
998
+ begin
999
+ @wire_buf << f.to_msgpack
1000
+ rescue NoMethodError
1001
+ # If MsgPack can't marshal the field, fall back to Marshal.
1002
+ # This handles fields that contain nested non-MsgPack-able
1003
+ # objects (in these cases, the entire field is Marshal'd.)
1004
+ marshall_indexes << i
1005
+ @wire_buf << Marshal.dump(f).to_msgpack
1006
+ end
1007
+ end
1008
+ end
1009
+ @packer.write(marshall_indexes)
1010
+ @packer.flush
1011
+ toplevel.dsock.send_datagram(@wire_buf.string,
874
1012
  the_locspec[0], the_locspec[1])
1013
+
1014
+ # Reset output buffer
1015
+ @wire_buf.rewind
1016
+ @wire_buf.truncate(0)
875
1017
  end
876
1018
  @pending.clear
877
1019
  end
878
1020
 
879
1021
  public
880
1022
  # project to the non-address fields
881
- def payloads
882
- return self.pro if @is_loopback
883
-
884
- if cols.size > 2
885
- # bundle up each tuple's non-locspec fields into an array
886
- retval = case @locspec_idx
887
- when 0 then self.pro{|t| t.values_at(1..(t.size-1))}
888
- when (schema.size - 1) then self.pro{|t| t.values_at(0..(t.size-2))}
889
- else self.pro{|t| t.values_at(0..(@locspec_idx-1), @locspec_idx+1..(t.size-1))}
890
- end
891
- else
892
- # just return each tuple's non-locspec field value
893
- retval = self.pro{|t| t[(@locspec_idx == 0) ? 1 : 0]}
1023
+ def payloads(&blk)
1024
+ return self.pro(&blk) if @is_loopback
1025
+
1026
+ if @payload_struct.nil?
1027
+ payload_cols = cols.dup
1028
+ payload_cols.delete_at(@locspec_idx)
1029
+ @payload_struct = Bud::TupleStruct.new(*payload_cols)
1030
+ @payload_colnums = payload_cols.map {|k| cols.index(k)}
1031
+ end
1032
+
1033
+ retval = self.pro do |t|
1034
+ @payload_struct.new(*t.values_at(*@payload_colnums))
894
1035
  end
1036
+ retval = retval.pro(&blk) unless blk.nil?
895
1037
  return retval
896
1038
  end
897
1039
 
@@ -903,51 +1045,40 @@ module Bud
903
1045
  elsif o.class <= Proc
904
1046
  tbl = register_coll_expr(o)
905
1047
  tbl.pro.wire_to(self, :pending)
1048
+ elsif o.class <= Bud::LatticePushElement
1049
+ add_merge_target
1050
+ o.wire_to(self, :pending)
1051
+ elsif o.class <= Bud::LatticeWrapper
1052
+ add_merge_target
1053
+ o.to_push_elem.wire_to(self, :pending)
906
1054
  else
907
1055
  pending_merge(o)
908
1056
  end
909
1057
  end
910
1058
 
911
1059
  superator "<+" do |o|
912
- raise Bud::Error, "illegal use of <+ with channel '#{@tabname}' on left"
1060
+ raise Bud::CompileError, "illegal use of <+ with channel '#{@tabname}' on left"
913
1061
  end
914
1062
 
915
1063
  undef merge
916
1064
 
917
1065
  def <=(o)
918
- raise Bud::Error, "illegal use of <= with channel '#{@tabname}' on left"
1066
+ raise Bud::CompileError, "illegal use of <= with channel '#{@tabname}' on left"
919
1067
  end
920
1068
  end
921
1069
 
922
1070
  class BudTerminal < BudScratch # :nodoc: all
923
- def initialize(name, given_schema, bud_instance, prompt=false) # :nodoc: all
924
- super(name, bud_instance, given_schema)
1071
+ def initialize(name, bud_instance, prompt=false) # :nodoc: all
1072
+ super(name, bud_instance, [:line])
925
1073
  @prompt = prompt
926
1074
  end
927
1075
 
928
1076
  public
929
1077
  def start_stdin_reader # :nodoc: all
930
- # XXX: Ugly hack. Rather than sending terminal data to EM via UDP,
931
- # we should add the terminal file descriptor to the EM event loop.
932
- @reader = Thread.new do
1078
+ Thread.new do
933
1079
  begin
934
- toplevel = @bud_instance.toplevel
935
1080
  while true
936
- out_io = get_out_io
937
- out_io.print("#{tabname} > ") if @prompt
938
-
939
- in_io = toplevel.options[:stdin]
940
- s = in_io.gets
941
- break if s.nil? # Hit EOF
942
- s = s.chomp if s
943
- tup = [s]
944
-
945
- ip = toplevel.ip
946
- port = toplevel.port
947
- EventMachine::schedule do
948
- socket = EventMachine::open_datagram_socket("127.0.0.1", 0)
949
- socket.send_datagram([tabname, tup].to_msgpack, ip, port)
950
- end
1081
+ break unless read_line
951
1082
  end
952
1083
  rescue Exception
953
1084
  puts "terminal reader thread failed: #{$!}"
@@ -957,13 +1088,37 @@ module Bud
957
1088
  end
958
1089
  end
959
1090
 
1091
+ # XXX: Ugly hack. Rather than sending terminal data to EM via UDP, we should
1092
+ # add the terminal file descriptor to the EM event loop.
1093
+ private
1094
+ def read_line
1095
+ get_out_io.print("#{tabname} > ") if @prompt
1096
+
1097
+ toplevel = @bud_instance.toplevel
1098
+ in_io = toplevel.options[:stdin]
1099
+ input_str = in_io.gets
1100
+ return false if input_str.nil? # Hit EOF
1101
+ input_str.chomp!
1102
+
1103
+ EventMachine::schedule do
1104
+ socket = EventMachine::open_datagram_socket("127.0.0.1", 0)
1105
+ socket.send_datagram([tabname, [input_str], []].to_msgpack,
1106
+ toplevel.ip, toplevel.port)
1107
+ end
1108
+
1109
+ return true
1110
+ end
1111
+
1112
+ public
1113
+ def bootstrap
1114
+ # override BudCollection; pending should not be moved into delta.
1115
+ end
1116
+
960
1117
  public
961
1118
  def flush #:nodoc: all
962
1119
  out_io = get_out_io
963
- @pending.each_value do |p|
964
- out_io.puts p[0]
965
- out_io.flush
966
- end
1120
+ @pending.each_value {|p| out_io.puts p[0]}
1121
+ out_io.flush
967
1122
  @pending.clear
968
1123
  end
969
1124
 
@@ -975,7 +1130,7 @@ module Bud
975
1130
  public
976
1131
  def tick #:nodoc: all
977
1132
  unless @pending.empty?
978
- @delta = @pending # pending used for input tuples in this case.
1133
+ @delta = @pending # pending used for input tuples in this case
979
1134
  @tick_delta = @pending.values
980
1135
  @pending.clear
981
1136
  else
@@ -983,8 +1138,7 @@ module Bud
983
1138
  @delta.clear
984
1139
  @tick_delta.clear
985
1140
  end
986
- @invalidated = true # channels and terminals are always invalidated.
987
- raise Bud::Error, "orphaned pending tuples in terminal" unless @pending.empty?
1141
+ @invalidated = true # channels and terminals are always invalidated
988
1142
  end
989
1143
 
990
1144
  public
@@ -995,7 +1149,11 @@ module Bud
995
1149
 
996
1150
  public
997
1151
  def <=(o) #:nodoc: all
998
- raise Bud::Error, "illegal use of <= with terminal '#{@tabname}' on left"
1152
+ raise Bud::CompileError, "illegal use of <= with terminal '#{@tabname}' on left"
1153
+ end
1154
+
1155
+ superator "<+" do |o|
1156
+ raise Bud::CompileError, "illegal use of <+ with terminal '#{@tabname}' on left"
999
1157
  end
1000
1158
 
1001
1159
  superator "<~" do |o|
@@ -1015,26 +1173,28 @@ module Bud
1015
1173
  def get_out_io
1016
1174
  rv = @bud_instance.toplevel.options[:stdout]
1017
1175
  rv ||= $stdout
1018
- raise Bud::Error, "attempting to write to terminal #{tabname} that was already closed" if rv.closed?
1176
+ if rv.closed?
1177
+ raise Bud::Error, "attempt to write to closed terminal '#{tabname}'"
1178
+ end
1019
1179
  rv
1020
1180
  end
1021
1181
  end
1022
1182
 
1023
1183
  class BudPeriodic < BudScratch # :nodoc: all
1024
1184
  def <=(o)
1025
- raise Bud::Error, "illegal use of <= with periodic '#{tabname}' on left"
1185
+ raise Bud::CompileError, "illegal use of <= with periodic '#{tabname}' on left"
1026
1186
  end
1027
1187
 
1028
1188
  superator "<~" do |o|
1029
- raise Bud::Error, "illegal use of <~ with periodic '#{tabname}' on left"
1189
+ raise Bud::CompileError, "illegal use of <~ with periodic '#{tabname}' on left"
1030
1190
  end
1031
1191
 
1032
1192
  superator "<-" do |o|
1033
- raise Bud::Error, "illegal use of <- with periodic '#{tabname}' on left"
1193
+ raise Bud::CompileError, "illegal use of <- with periodic '#{tabname}' on left"
1034
1194
  end
1035
1195
 
1036
1196
  superator "<+" do |o|
1037
- raise Bud::Error, "illegal use of <+ with periodic '#{tabname}' on left"
1197
+ raise Bud::CompileError, "illegal use of <+ with periodic '#{tabname}' on left"
1038
1198
  end
1039
1199
 
1040
1200
  def tick
@@ -1070,8 +1230,8 @@ module Bud
1070
1230
  public
1071
1231
  def tick #:nodoc: all
1072
1232
  if $BUD_DEBUG
1073
- puts "#{tabname}. storage -= pending deletes" unless @to_delete.empty? and @to_delete_by_key.empty?
1074
- puts "#{tabname}. delta += pending" unless @pending.empty?
1233
+ puts "#{tabname}.storage -= pending deletes" unless @to_delete.empty? and @to_delete_by_key.empty?
1234
+ puts "#{tabname}.delta += pending" unless @pending.empty?
1075
1235
  end
1076
1236
  @tick_delta.clear
1077
1237
  deleted = nil
@@ -1099,7 +1259,9 @@ module Bud
1099
1259
  end
1100
1260
 
1101
1261
  def invalidated=(val)
1102
- raise Bud::Error, "internal error: must not set invalidate on tables"
1262
+ # Might be reset to false at end-of-tick, but shouldn't be set to true
1263
+ raise Bud::Error, "cannot set invalidate on table '#{@tabname}'" if val
1264
+ super
1103
1265
  end
1104
1266
 
1105
1267
  def pending_delete(o)
@@ -1113,6 +1275,12 @@ module Bud
1113
1275
  add_merge_target
1114
1276
  tbl = register_coll_expr(o)
1115
1277
  tbl.pro.wire_to(self, :delete)
1278
+ elsif o.class <= Bud::LatticePushElement
1279
+ add_merge_target
1280
+ o.wire_to(self, :delete)
1281
+ elsif o.class <= Bud::LatticeWrapper
1282
+ add_merge_target
1283
+ o.to_push_elem.wire_to(self, :delete)
1116
1284
  else
1117
1285
  unless o.nil?
1118
1286
  o = o.uniq.compact if o.respond_to?(:uniq)
@@ -1150,7 +1318,7 @@ module Bud
1150
1318
  # No cache to invalidate. Also, tables do not invalidate dependents,
1151
1319
  # because their own state is not considered invalidated; that happens only
1152
1320
  # if there were pending deletes at the beginning of a tick (see tick())
1153
- puts "******** invalidate_cache called on BudTable" if $BUD_DEBUG
1321
+ puts "******** invalidate_cache called on table '#{@tabname}'" if $BUD_DEBUG
1154
1322
  end
1155
1323
 
1156
1324
  public
@@ -1166,11 +1334,11 @@ module Bud
1166
1334
 
1167
1335
  class BudReadOnly < BudCollection # :nodoc: all
1168
1336
  superator "<+" do |o|
1169
- raise CompileError, "illegal use of <+ with read-only collection '#{@tabname}' on left"
1337
+ raise Bud::CompileError, "illegal use of <+ with read-only collection '#{@tabname}' on left"
1170
1338
  end
1171
1339
  public
1172
1340
  def merge(o) #:nodoc: all
1173
- raise CompileError, "illegal use of <= with read-only collection '#{@tabname}' on left"
1341
+ raise Bud::CompileError, "illegal use of <= with read-only collection '#{@tabname}' on left"
1174
1342
  end
1175
1343
  public
1176
1344
  def invalidate_cache
@@ -1195,7 +1363,20 @@ module Bud
1195
1363
 
1196
1364
  public
1197
1365
  def each(&block)
1198
- @expr.call.each(&block)
1366
+ v = @expr.call
1367
+ return if v.nil? or v == [nil]
1368
+
1369
+ # XXX: Gross hack. We want to support RHS expressions that do not
1370
+ # necessarily return BudCollections (they might instead return lattice
1371
+ # values or hashes). Since it isn't easy to distinguish between these two
1372
+ # cases statically, instead we just always use CollExpr; at runtime, if
1373
+ # the value doesn't look like a traditional Bloom collection, we don't try
1374
+ # to break it up into tuples.
1375
+ if v.class <= Array || v.class <= BudCollection
1376
+ v.each(&block)
1377
+ else
1378
+ yield v
1379
+ end
1199
1380
  end
1200
1381
 
1201
1382
  public
@@ -1205,13 +1386,18 @@ module Bud
1205
1386
  end
1206
1387
 
1207
1388
  class BudFileReader < BudReadOnly # :nodoc: all
1208
- def initialize(name, filename, delimiter, bud_instance) # :nodoc: all
1389
+ def initialize(name, filename, bud_instance) # :nodoc: all
1209
1390
  super(name, bud_instance, {[:lineno] => [:text]})
1210
1391
  @filename = filename
1211
1392
  @storage = {}
1212
1393
  # NEEDS A TRY/RESCUE BLOCK
1213
1394
  @fd = File.open(@filename, "r")
1214
1395
  @linenum = 0
1396
+ @invalidated = true
1397
+ end
1398
+
1399
+ def tick
1400
+ @invalidated = true
1215
1401
  end
1216
1402
 
1217
1403
  public