bud 0.9.6 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
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