bud 0.9.6 → 0.9.7

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.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MTY4MjlkMTY1NjZlYTZiOTVjNmYyMmIzYTliNDVlZDIwZjA0YjY4Yw==
5
- data.tar.gz: !binary |-
6
- MTE0ZGY3MDg5NzBmMTJkYTExNDNjYTcyNTFhOThmMzk0ZTM4YzgxOA==
7
- !binary "U0hBNTEy":
8
- metadata.gz: !binary |-
9
- OGJmYmMwZjU4Y2RlZWUyNDVkMGFkYTczZDhlNjk5MGQ5NmVlZDIyYzQyMjAz
10
- NzEyZWI0YTI1MjVjM2VhZGRjMWE4ZDM4MGVlYzA4YzliYmJjNGUwZjEyY2I2
11
- YjZhOTVjNzk5MGVjNWJkMTA0YWRmODk3MDAyYmVlOTM4MmVmYzA=
12
- data.tar.gz: !binary |-
13
- YjZkOTlkYzQwODJkZTI4YjU0YmFhMmY4MDY5Mjg3Y2ZjMzUyN2ZmZTAyOGI5
14
- M2M3NWQ5OTgyM2U2ZjA3NGIzMjkwZjhjMTBhNWFmYzc3OTNiYmZjOWMzNmU3
15
- MTkzZmI1MzJlODNkMmE2Y2E1NzU5ODYzNDlhM2Q3N2Q3MDk2Yzg=
2
+ SHA1:
3
+ metadata.gz: 3d9ec3dd7f92dded5c77e541617853e0954fccaf
4
+ data.tar.gz: 37c31e760cbfe5305ebb73d3ff6e2417e499022d
5
+ SHA512:
6
+ metadata.gz: a9fe2678a4aa143ebfa6ae677426bc60593550bda6209edc056855a6bca1b4f7ee5b4fc99e773d74679ca57fc615da7128e6db4622c855d45fda60e138bb78de
7
+ data.tar.gz: 240d532afbe76c4a7740f65276e7d0bb405b52661e8dde63fc42e54a5d40deb6d23c0bea1ef7d4a9e0e0e060c837533a0e3202c3ab41f33779861df6db673509
data/History.txt CHANGED
@@ -1,3 +1,44 @@
1
+ == 0.9.7 / 2013-04-22
2
+
3
+ * Avoid raising an exception when Bud is used with Ruby 2.0. There isn't
4
+ anything special that Bud itself needs to do to support Ruby 2.0, but
5
+ RubyParser doesn't properly support 2.0 yet. So using 2.0-only syntax in Bud
6
+ rules is unlikely to work (until RubyParser is updated), but no other problems
7
+ have been observed.
8
+ * Reject <= on collections from outside Bloom rules (#289, Aaron Davidson). The
9
+ previous behavior was error-prone: inserting into a scratch collection via <=
10
+ was not rejected but the inserted data would immediately be discarded at the
11
+ beginning of the next tick. Hence, it is simpler to require <+ or <~ for all
12
+ insertions from outside Bloom rules.
13
+ * Fix bug in join operators whose qualifiers reference collections defined
14
+ inside imported modules (#301)
15
+ * Fix bug in join operators when the same table and column name appears on the
16
+ LHS of multiple join predicates (#313)
17
+ * Fix bug in outer joins with multiple predicates (#315)
18
+ * Fix several bugs in rescan/invalidation logic for tables in the presence of
19
+ upstream deletions (#303)
20
+ * Fix regression in file_reader collections (#304)
21
+ * Improve chaining of notin operators
22
+ * Optimize join evaluation with multiple predicates
23
+ * Optimize notin self joins
24
+ * Improve error reporting for syntax errors on temp collections (#310)
25
+ * Add Travis CI integration hooks (#300, Josh Rosen)
26
+
27
+ Lattices:
28
+
29
+ * Rename lmap#apply_monotone to lmap#apply. Also, allow #apply to take either
30
+ monotone functions or morphisms, and improve error reporting.
31
+ * Add lmap#filter. This takes an lmap whose values are elements of the lbool
32
+ lattice and returns an lmap containing only those k/v pairs where the value is
33
+ true.
34
+ * More intuitive lattice equi-join syntax (lset#eqjoin). Rather than specifying
35
+ the join predicate(s) using array indexes, instead allow them to be specified
36
+ using a hash of field names, as in traditional Bloom joins. This only works
37
+ when the lset contains Structs.
38
+ * Remove lset#project, and allow lset#eqjoin to take zero predicates; in the
39
+ latter case, eqjoin computes the Cartesian product.
40
+ * Support deletion rules (<-) with lattice values on the RHS
41
+
1
42
  == 0.9.6 / 2013-02-25
2
43
 
3
44
  * Support syntax sugar for initializing lattices (#294). For example, rather
data/README.md CHANGED
@@ -11,11 +11,12 @@ documentation.
11
11
  Main deficiencies at this point are:
12
12
 
13
13
  - No Ruby constraints: Within Bloom programs the full power of Ruby is also
14
- available, including mutable state. This allows programmers to get outside
15
- the Bloom framework and lose cleanliness.
14
+ available, including mutable state. This allows programmers to get outside the
15
+ Bloom framework and lose cleanliness.
16
16
 
17
- - Compatibility: Bud only works with Ruby (MRI) 1.8.7 and 1.9. JRuby and other
18
- Ruby implementations are currently not supported.
17
+ - Compatibility: Bud only works with Ruby (MRI) 1.8.7 and 1.9. Bud also has
18
+ experimental support for Ruby 2.0. JRuby and other Ruby implementations are
19
+ currently not supported.
19
20
 
20
21
  ## Installation
21
22
 
data/docs/cheat.md CHANGED
@@ -21,15 +21,23 @@ As in Ruby, backslash is used to escape a newline.<br>
21
21
  end
22
22
 
23
23
  ## State Declarations ##
24
- A `state` block contains Bud collection definitions. A Bud collection is a *set*
25
- of *facts*; each fact is an array of Ruby values. Note that collections do not
26
- contain duplicates (inserting a duplicate fact into a collection is ignored).
24
+
25
+ A `state` block contains definitions of two kinds of program state:
26
+ *collections* and *lattices*. A Bud collection is a *set* of *facts*; each fact
27
+ is an array of Ruby values. Note that collections do not contain duplicates
28
+ (inserting a duplicate fact into a collection is ignored).
27
29
 
28
30
  Like a table in a relational database, a subset of the columns in a collection
29
31
  makeup the collection's _key_. Attempting to insert two facts into a collection
30
32
  that agree on the key columns (but are not duplicates) results in a runtime
31
33
  exception.
32
34
 
35
+ A lattice represents a value that *grows* over time, where the definition of
36
+ "growth" depends on the kind of lattice in question. For example, an `lset`
37
+ lattice contains a set of facts that grows over time (similar to a traditional
38
+ Bud collection), whereas an `lmax` lattice holds an increasing integer
39
+ value. For more information on lattices, see the section below.
40
+
33
41
  ### Default Declaration Syntax ###
34
42
  *BudCollection :name, [keys] => [values]*
35
43
 
@@ -245,12 +253,6 @@ Finally, we output every tuple of `bc` that does *not* appear in `t`.
245
253
  * `bc.argagg(:exemplary_agg_name, [:attr1], :attr2))`. *generalizes argmin/max: returns the bc items per attr1 that are chosen by the exemplary
246
254
  aggregate named*
247
255
 
248
- ### Built-in Aggregates: ###
249
-
250
- * Exemplary aggs: `min`, `max`, `choose`
251
- * Summary aggs: `count`, `sum`, `avg`
252
- * Structural aggs: `accum`
253
-
254
256
  Note that custom aggregation can be written using `reduce`.
255
257
 
256
258
  ## Collection Combination (Join) ###
@@ -313,6 +315,103 @@ The schema of a temp collection in inherited from the rhs; if the rhs has no
313
315
  schema, a simple one is manufactured to suit the data found in the rhs at
314
316
  runtime: `[c0, c1, ...]`.
315
317
 
318
+ ## Lattices ##
319
+
320
+ In addition to traditional Bud collections and relational-style statements that
321
+ operate over collections, Bud also supports lattices and rules that operate over
322
+ lattices. Lattices provide a way to represent values that *grow over time*,
323
+ where the definition of "growth" depends on the kind of lattice. The following
324
+ built-in lattice types are currently supported:
325
+
326
+ <table>
327
+ <tr>
328
+ <td><b>Name</b></td>
329
+ <td><b>Description</b></td>
330
+ <td><b>Initial Value</b></td>
331
+ <td><b>Monotone Functions</b></td>
332
+ </tr>
333
+
334
+ <tr>
335
+ <td><code>lbool</code></td>
336
+ <td>Threshold test (<code>false</code> => <code>true</code> conditional)</td>
337
+ <td>false</td>
338
+ <td>when_true</td>
339
+ </tr>
340
+
341
+ <tr>
342
+ <td><code>lmax</code></td>
343
+ <td>Increasing numeric value</td>
344
+ <td>-&infin;</td>
345
+ <td>gt(n), gt_eq(n), +(n), -(n)</td>
346
+ </tr>
347
+
348
+ <tr>
349
+ <td><code>lmin</code></td>
350
+ <td>Decreasing numeric value</td>
351
+ <td>+&infin;</td>
352
+ <td>lt(n), lt_eq(n), +(n), -(n)</td>
353
+ </tr>
354
+
355
+ <tr>
356
+ <td><code>lset</code></td>
357
+ <td>Growing set of values</td>
358
+ <td>empty set</td>
359
+ <td>contains?, eqjoin, filter, intersect, product, project, size</td>
360
+ </tr>
361
+
362
+ <tr>
363
+ <td><code>lpset</code></td>
364
+ <td>Growing set of non-negative numeric values</td>
365
+ <td>empty set</td>
366
+ <td>contains?, eqjoin, filter, intersect, product, project, size, sum</td>
367
+ </tr>
368
+
369
+ <tr>
370
+ <td><code>lbag</code></td>
371
+ <td>Growing multiset of values</td>
372
+ <td>empty multiset</td>
373
+ <td>contains?, multiplicity, intersect, product, project, size</td>
374
+ </tr>
375
+
376
+ <tr>
377
+ <td><code>lmap</code></td>
378
+ <td>Map from keys to lattice values</td>
379
+ <td>empty map</td>
380
+ <td>at, intersect, key?, key_set, project, size</td>
381
+ </tr>
382
+ </table>
383
+
384
+ Lattices can be declared in `state` blocks in a similar manner to traditional
385
+ Bud collections. Similarly, Bloom rules can invoke functions on lattice
386
+ values. A simple Bloom program that uses lattices to compute a quorum vote is as
387
+ follows:
388
+
389
+ ```ruby
390
+ QUORUM_SIZE = 5
391
+ RESULT_ADDR = "example.org"
392
+
393
+ class QuorumVote
394
+ include Bud
395
+
396
+ state do
397
+ channel :vote_chn, [:@addr, :voter_id]
398
+ channel :result_chn, [:@addr]
399
+ lset :votes
400
+ lmax :vote_cnt
401
+ lbool :vote_done
402
+ end
403
+
404
+ bloom do
405
+ votes <= vote_chn {|v| v.voter_id}
406
+ vote_cnt <= votes.size
407
+ got_quorum <= vote_cnt.gt_eq(QUORUM_SIZE)
408
+ result_chn <~ got_quorum.when_true { [RESULT_ADDR] }
409
+ end
410
+ end
411
+ ```
412
+
413
+ For more information on lattice support in Bloom, see this [recent paper](http://db.cs.berkeley.edu/papers/socc12-blooml.pdf).
414
+
316
415
  ## Bud Modules ##
317
416
  A Bud module combines state (collections) and logic (Bloom rules). Using modules allows your program to be decomposed into a collection of smaller units.
318
417
 
data/lib/bud/aggs.rb CHANGED
@@ -36,7 +36,7 @@ module Bud
36
36
 
37
37
  class Min < ArgExemplary #:nodoc: all
38
38
  def trans(the_state, val)
39
- if the_state < val
39
+ if (the_state <=> val) < 0
40
40
  return the_state, :ignore
41
41
  elsif the_state == val
42
42
  return the_state, :keep
@@ -53,8 +53,10 @@ module Bud
53
53
 
54
54
  class Max < ArgExemplary #:nodoc: all
55
55
  def trans(the_state, val)
56
- if the_state > val
56
+ if (the_state <=> val) > 0
57
57
  return the_state, :ignore
58
+ elsif the_state == val
59
+ return the_state, :keep
58
60
  else
59
61
  return val, :replace
60
62
  end
data/lib/bud/bud_meta.rb CHANGED
@@ -17,7 +17,7 @@ class BudMeta #:nodoc: all
17
17
  # stratum_map = {fully qualified pred => stratum}. Copy stratum_map data
18
18
  # into t_stratum format.
19
19
  raise unless @bud_instance.t_stratum.to_a.empty?
20
- @bud_instance.t_stratum <= stratum_map.to_a
20
+ @bud_instance.t_stratum.merge(stratum_map.to_a)
21
21
 
22
22
  # slot each rule into the stratum corresponding to its lhs pred (from stratum_map)
23
23
  stratified_rules = Array.new(top_stratum + 2) { [] } # stratum -> [ rules ]
@@ -265,9 +265,6 @@ class BudMeta #:nodoc: all
265
265
  da = ::DepAnalysis.new
266
266
  da.providing <+ @bud_instance.tables[:t_provides].to_a
267
267
  da.depends <+ @bud_instance.t_depends.map{|d| [d.lhs, d.op, d.body, d.nm]}
268
-
269
- #@bud_instance.tables[:t_provides].each {|t| da.providing <+ t}
270
- #@bud_instance.tables[:t_depends].each {|t| da.depends_tc <+ t}
271
268
  da.tick_internal
272
269
  @dependency_analysis = da
273
270
  end
@@ -14,11 +14,11 @@ module Bud
14
14
  class BudCollection
15
15
  include Enumerable
16
16
 
17
- attr_accessor :bud_instance, :tabname # :nodoc: all
18
- attr_reader :cols, :key_cols # :nodoc: all
17
+ attr_accessor :bud_instance # :nodoc: all
18
+ attr_reader :tabname, :cols, :key_cols # :nodoc: all
19
19
  attr_reader :struct
20
20
  attr_reader :storage, :delta, :new_delta, :pending, :tick_delta # :nodoc: all
21
- attr_reader :wired_by, :to_delete
21
+ attr_reader :wired_by, :scanner_cnt
22
22
  attr_accessor :invalidated, :rescan
23
23
  attr_accessor :is_source
24
24
  attr_accessor :accumulate_tick_deltas # updated in bud.do_wiring
@@ -28,6 +28,7 @@ module Bud
28
28
  @bud_instance = bud_instance
29
29
  @invalidated = true
30
30
  @is_source = true # unless it shows up on the lhs of some rule
31
+ @scanner_cnt = 0
31
32
  @wired_by = []
32
33
  @accumulate_tick_deltas = false
33
34
  init_schema(given_schema) unless given_schema.nil? and defer_schema
@@ -62,7 +63,7 @@ module Bud
62
63
  if @cols.empty?
63
64
  @cols = nil
64
65
  else
65
- @struct = ($struct_classes[@cols] ||= Struct.new(*@cols))
66
+ @struct = ($struct_classes[@cols] ||= Bud::TupleStruct.new(*@cols))
66
67
  @structlen = @struct.members.length
67
68
  end
68
69
  setup_accessors
@@ -131,11 +132,19 @@ module Bud
131
132
  end
132
133
  end
133
134
 
134
- # set up schema accessors, which are class methods
135
+ # Setup schema accessors, which are class methods. Note that the same
136
+ # table/column name might appear multiple times on the LHS of a single
137
+ # join (e.g., (foo * bar).combos(foo.x => bar.y, foo.x => bar.z)). Because
138
+ # the join predicates are represented as a hash, we need the two instances
139
+ # of foo.x to be distinct values (otherwise the resulting hash will only
140
+ # have a single key). Hence, we add a unique ID to the value returned by
141
+ # schema accessors.
135
142
  @cols_access = Module.new do
136
143
  sc.each_with_index do |c, i|
137
144
  define_method c do
138
- [@tabname, i, c]
145
+ @counter ||= 0
146
+ @counter += 1
147
+ [qualified_tabname, i, c, @counter]
139
148
  end
140
149
  end
141
150
  end
@@ -188,11 +197,8 @@ module Bud
188
197
  if @bud_instance.wiring?
189
198
  pusher = to_push_elem(the_name, the_schema)
190
199
  # If there is no code block evaluate, use the scanner directly
191
- return pusher if blk.nil?
192
- pusher_pro = pusher.pro(&blk)
193
- pusher_pro.elem_name = the_name
194
- pusher_pro.tabname = the_name
195
- pusher_pro
200
+ pusher = pusher.pro(&blk) unless blk.nil?
201
+ pusher
196
202
  else
197
203
  rv = []
198
204
  self.each do |t|
@@ -209,7 +215,7 @@ module Bud
209
215
  def each_with_index(the_name=tabname, the_schema=schema, &blk)
210
216
  if @bud_instance.wiring?
211
217
  pusher = to_push_elem(the_name, the_schema)
212
- pusher.each_with_index(the_name, the_schema, &blk)
218
+ pusher.each_with_index(&blk)
213
219
  else
214
220
  super(&blk)
215
221
  end
@@ -251,7 +257,7 @@ module Bud
251
257
  end
252
258
 
253
259
  def rename(the_name, the_schema=nil, &blk)
254
- raise unless @bud_instance.wiring?
260
+ raise Bud::Error unless @bud_instance.wiring?
255
261
  # a scratch with this name should have been defined during rewriting
256
262
  unless @bud_instance.respond_to? the_name
257
263
  raise Bud::Error, "rename failed to define a scratch named #{the_name}"
@@ -397,7 +403,7 @@ module Bud
397
403
  private
398
404
  def raise_pk_error(new, old)
399
405
  key = get_key_vals(old)
400
- raise Bud::KeyConstraintError, "key conflict inserting #{new.inspect} into \"#{tabname}\": existing tuple #{old.inspect}, key = #{key.inspect}"
406
+ raise Bud::KeyConstraintError, "key conflict inserting #{new.inspect} into \"#{qualified_tabname}\": existing tuple #{old.inspect}, key = #{key.inspect}"
401
407
  end
402
408
 
403
409
  private
@@ -408,7 +414,7 @@ module Bud
408
414
  private
409
415
  def prep_tuple(o)
410
416
  return o if o.class == @struct
411
- if o.class == Array
417
+ if o.kind_of? Array
412
418
  if @struct.nil?
413
419
  sch = (1 .. o.length).map{|i| "c#{i}".to_sym}
414
420
  init_schema(sch)
@@ -446,7 +452,7 @@ module Bud
446
452
  when @delta.object_id; "delta"
447
453
  when @new_delta.object_id; "new_delta"
448
454
  end
449
- puts "#{qualified_tabname}.#{storetype} ==> #{t}"
455
+ puts "#{qualified_tabname}.#{storetype} ==> #{t.inspect}"
450
456
  end
451
457
  return if t.nil? # silently ignore nils resulting from map predicates failing
452
458
  t = prep_tuple(t)
@@ -600,6 +606,10 @@ module Bud
600
606
  public
601
607
  # instantaneously merge items from collection +o+ into +buf+
602
608
  def <=(collection)
609
+ unless bud_instance.toplevel.inside_tick
610
+ raise Bud::CompileError, "illegal use of <= outside of bloom block, use <+ instead"
611
+ end
612
+
603
613
  merge(collection)
604
614
  end
605
615
 
@@ -712,6 +722,7 @@ module Bud
712
722
  self, the_schema)
713
723
  toplevel.scanners[this_stratum][[oid, the_name]] = scanner
714
724
  toplevel.push_sources[this_stratum][[oid, the_name]] = scanner
725
+ @scanner_cnt += 1
715
726
  end
716
727
  return toplevel.scanners[this_stratum][[oid, the_name]]
717
728
  end
@@ -839,6 +850,11 @@ module Bud
839
850
  public
840
851
  def add_rescan_invalidate(rescan, invalidate)
841
852
  srcs = non_temporal_predecessors
853
+
854
+ # XXX: this seems wrong. We might rescan a node for many reasons (e.g.,
855
+ # because another one of the node's outputs needs to be refilled). We only
856
+ # need to invalidate + rescan this scratch if one of the inputs to this
857
+ # collection is *invalidated*.
842
858
  if srcs.any? {|e| rescan.member? e}
843
859
  invalidate << self
844
860
  rescan.merge(srcs)
@@ -1012,7 +1028,7 @@ module Bud
1012
1028
  if @payload_struct.nil?
1013
1029
  payload_cols = cols.dup
1014
1030
  payload_cols.delete_at(@locspec_idx)
1015
- @payload_struct = Struct.new(*payload_cols)
1031
+ @payload_struct = Bud::TupleStruct.new(*payload_cols)
1016
1032
  @payload_colnums = payload_cols.map {|k| cols.index(k)}
1017
1033
  end
1018
1034
 
@@ -1078,11 +1094,9 @@ module Bud
1078
1094
  # add the terminal file descriptor to the EM event loop.
1079
1095
  private
1080
1096
  def read_line
1081
- toplevel = @bud_instance.toplevel
1082
- if @prompt
1083
- get_out_io.print("#{tabname} > ")
1084
- end
1097
+ get_out_io.print("#{tabname} > ") if @prompt
1085
1098
 
1099
+ toplevel = @bud_instance.toplevel
1086
1100
  in_io = toplevel.options[:stdin]
1087
1101
  input_str = in_io.gets
1088
1102
  return false if input_str.nil? # Hit EOF
@@ -1113,7 +1127,7 @@ module Bud
1113
1127
  public
1114
1128
  def tick #:nodoc: all
1115
1129
  unless @pending.empty?
1116
- @delta = @pending # pending used for input tuples in this case.
1130
+ @delta = @pending # pending used for input tuples in this case
1117
1131
  @tick_delta = @pending.values
1118
1132
  @pending.clear
1119
1133
  else
@@ -1121,7 +1135,7 @@ module Bud
1121
1135
  @delta.clear
1122
1136
  @tick_delta.clear
1123
1137
  end
1124
- @invalidated = true # channels and terminals are always invalidated.
1138
+ @invalidated = true # channels and terminals are always invalidated
1125
1139
  end
1126
1140
 
1127
1141
  public
@@ -1213,8 +1227,8 @@ module Bud
1213
1227
  public
1214
1228
  def tick #:nodoc: all
1215
1229
  if $BUD_DEBUG
1216
- puts "#{tabname}. storage -= pending deletes" unless @to_delete.empty? and @to_delete_by_key.empty?
1217
- puts "#{tabname}. delta += pending" unless @pending.empty?
1230
+ puts "#{tabname}.storage -= pending deletes" unless @to_delete.empty? and @to_delete_by_key.empty?
1231
+ puts "#{tabname}.delta += pending" unless @pending.empty?
1218
1232
  end
1219
1233
  @tick_delta.clear
1220
1234
  deleted = nil
@@ -1242,7 +1256,9 @@ module Bud
1242
1256
  end
1243
1257
 
1244
1258
  def invalidated=(val)
1245
- raise Bud::Error, "internal error: must not set invalidate on tables"
1259
+ # Might be reset to false at end-of-tick, but shouldn't be set to true
1260
+ raise Bud::Error, "cannot not set invalidate on table '#{@tabname}'" if val
1261
+ super
1246
1262
  end
1247
1263
 
1248
1264
  def pending_delete(o)
@@ -1256,6 +1272,12 @@ module Bud
1256
1272
  add_merge_target
1257
1273
  tbl = register_coll_expr(o)
1258
1274
  tbl.pro.wire_to(self, :delete)
1275
+ elsif o.class <= Bud::LatticePushElement
1276
+ add_merge_target
1277
+ o.wire_to(self, :delete)
1278
+ elsif o.class <= Bud::LatticeWrapper
1279
+ add_merge_target
1280
+ o.to_push_elem.wire_to(self, :delete)
1259
1281
  else
1260
1282
  unless o.nil?
1261
1283
  o = o.uniq.compact if o.respond_to?(:uniq)
@@ -1293,7 +1315,7 @@ module Bud
1293
1315
  # No cache to invalidate. Also, tables do not invalidate dependents,
1294
1316
  # because their own state is not considered invalidated; that happens only
1295
1317
  # if there were pending deletes at the beginning of a tick (see tick())
1296
- puts "******** invalidate_cache called on BudTable" if $BUD_DEBUG
1318
+ puts "******** invalidate_cache called on table '#{@tabname}'" if $BUD_DEBUG
1297
1319
  end
1298
1320
 
1299
1321
  public
@@ -1339,6 +1361,7 @@ module Bud
1339
1361
  public
1340
1362
  def each(&block)
1341
1363
  v = @expr.call
1364
+ return if v.nil? or v == [nil]
1342
1365
 
1343
1366
  # XXX: Gross hack. We want to support RHS expressions that do not
1344
1367
  # necessarily return BudCollections (they might instead return lattice
@@ -1367,6 +1390,11 @@ module Bud
1367
1390
  # NEEDS A TRY/RESCUE BLOCK
1368
1391
  @fd = File.open(@filename, "r")
1369
1392
  @linenum = 0
1393
+ @invalidated = true
1394
+ end
1395
+
1396
+ def tick
1397
+ @invalidated = true
1370
1398
  end
1371
1399
 
1372
1400
  public
@@ -3,7 +3,8 @@ Notes on Invalidate and Rescan in Bud
3
3
 
4
4
  (I'll use 'downstream' to mean rhs to lhs (like in budplot). In every stratum,
5
5
  data originates at scanned sources at the "top", winds its way through various
6
- PushElements and ends up in a collection at the "bottom". I'll also the term
6
+ PushElements and ends up in a collection at the "bottom". That is, data flows
7
+ from "upstream" producers to "downstream" consumers. I'll also the term
7
8
  "elements" to mean both dataflow nodes (PushElements) and collections).
8
9
 
9
10
  Invalidation strategy works through two flags/signals, rescan and