bud 0.9.4 → 0.9.9

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.
@@ -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