bud 0.9.4 → 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,7 +6,7 @@ class DepAnalysis #:nodoc: all
6
6
 
7
7
  state do
8
8
  # Data inserted by client, usually from t_depends and t_provides
9
- scratch :depends, [:lhs, :op, :body, :neg]
9
+ scratch :depends, [:lhs, :op, :body, :neg, :in_body]
10
10
  scratch :providing, [:pred, :input]
11
11
 
12
12
  # Intermediate state
@@ -36,7 +36,7 @@ class DepAnalysis #:nodoc: all
36
36
 
37
37
  cycle <= depends_tc do |d|
38
38
  if d.lhs == d.body
39
- unless d.neg and !d.temporal
39
+ unless d.neg and !d.temporal
40
40
  [d.lhs, d.via, d.neg, d.temporal]
41
41
  end
42
42
  end
@@ -60,11 +60,10 @@ class DepAnalysis #:nodoc: all
60
60
  [p.pred, true]
61
61
  end
62
62
  else
63
- unless depends_tc.map{|d| d.lhs if d.lhs != d.body}.include? p.pred
63
+ unless depends_tc.map{|dt| dt.lhs if dt.lhs != dt.body}.include? p.pred
64
64
  [p.pred, false]
65
65
  end
66
66
  end
67
67
  end
68
68
  end
69
69
  end
70
-
@@ -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
@@ -1,4 +1,3 @@
1
- require 'set'
2
1
  require 'bud/collections'
3
2
 
4
3
  module Bud
@@ -13,19 +12,18 @@ module Bud
13
12
  class PushElement < BudCollection
14
13
  attr_accessor :rescan, :invalidated
15
14
  attr_accessor :elem_name
16
- attr_reader :found_delta, :refcount, :wired_by, :outputs
15
+ attr_reader :found_delta, :wired_by, :outputs, :pendings
17
16
 
18
17
  def initialize(name_in, bud_instance, collection_name=nil, given_schema=nil, defer_schema=false, &blk)
19
18
  super(name_in, bud_instance, given_schema, defer_schema)
20
19
  @blk = blk
21
- @outputs = []
22
- @pendings = []
23
- @deletes = []
24
- @delete_keys = []
20
+ @outputs = Set.new
21
+ @pendings = Set.new
22
+ @deletes = Set.new
23
+ @delete_keys = Set.new
25
24
  @wired_by = []
26
25
  @elem_name = name_in
27
26
  @found_delta = false
28
- @refcount = 1
29
27
  @collection_name = collection_name
30
28
  @invalidated = true
31
29
  @rescan = true
@@ -58,6 +56,8 @@ module Bud
58
56
  print "#{next_accum} "
59
57
  if o.class <= Bud::BudCollection
60
58
  puts "#{(o.object_id*2).to_s(16)}: #{o.qualified_tabname} (#{o.class})"
59
+ elsif o.class <= Bud::LatticeWrapper
60
+ puts "#{o.inspect}"
61
61
  else
62
62
  puts "#{(o.object_id*2).to_s(16)}: (#{o.class.name})"
63
63
  end
@@ -83,17 +83,15 @@ module Bud
83
83
 
84
84
  case kind
85
85
  when :output
86
- raise Bud::Error unless element.respond_to? :insert
87
86
  @outputs << element
88
87
  when :pending
89
- raise Bud::Error unless element.respond_to? :pending_merge
90
88
  @pendings << element
91
89
  when :delete
92
- raise Bud::Error unless element.respond_to? :pending_delete
93
90
  @deletes << element
94
91
  when :delete_by_key
95
- raise Bud::Error unless element.respond_to? :pending_delete_keys
96
92
  @delete_keys << element
93
+ else
94
+ raise Bud::Error, "unrecognized wiring kind: #{kind}"
97
95
  end
98
96
 
99
97
  element.wired_by << self if element.respond_to? :wired_by
@@ -116,27 +114,34 @@ module Bud
116
114
  end
117
115
 
118
116
  def push_out(item, do_block=true)
119
- if item
120
- if do_block && @blk
121
- item = item.to_a if @blk.arity > 1
122
- item = @blk.call item
117
+ if do_block && @blk
118
+ item = item.to_a if @blk.arity > 1
119
+ item = @blk.call item
120
+ return if item.nil?
121
+ end
122
+
123
+ @outputs.each do |ou|
124
+ if ou.class <= Bud::PushElement
125
+ ou.insert(item, self)
126
+ elsif ou.class <= Bud::BudCollection
127
+ ou.do_insert(item, ou.new_delta)
128
+ elsif ou.class <= Bud::LatticeWrapper
129
+ ou.insert(item, self)
130
+ else
131
+ raise Bud::Error, "unexpected output target: #{ou.class}"
123
132
  end
133
+ end
124
134
 
125
- unless item.nil?
126
- @outputs.each do |ou|
127
- if ou.class <= Bud::PushElement
128
- ou.insert(item, self)
129
- elsif ou.class <= Bud::BudCollection
130
- ou.do_insert(item, ou.new_delta)
131
- else
132
- raise Bud::Error, "expected either a PushElement or a BudCollection"
133
- end
134
- end
135
+ # for the following, o is a BudCollection
136
+ @deletes.each{|o| o.pending_delete([item])}
137
+ @delete_keys.each{|o| o.pending_delete_keys([item])}
135
138
 
136
- # for all the following, o is a BudCollection
137
- @deletes.each{|o| o.pending_delete([item])}
138
- @delete_keys.each{|o| o.pending_delete_keys([item])}
139
- @pendings.each{|o| o.pending_merge([item])}
139
+ # o is a LatticeWrapper or a BudCollection
140
+ @pendings.each do |o|
141
+ if o.class <= Bud::LatticeWrapper
142
+ o <+ item
143
+ else
144
+ o.pending_merge([item])
140
145
  end
141
146
  end
142
147
  end
@@ -144,18 +149,17 @@ module Bud
144
149
  # default for stateless elements
145
150
  public
146
151
  def add_rescan_invalidate(rescan, invalidate)
147
- # if any of the source elements are in rescan mode, then put this node in
148
- # rescan.
152
+ # If any sources are in rescan mode, then put this node in rescan
149
153
  srcs = non_temporal_predecessors
150
154
  if srcs.any?{|p| rescan.member? p}
151
155
  rescan << self
152
156
  end
153
157
 
154
- # pass the current state to the non-element outputs, and see if they end
155
- # up marking this node for rescan
158
+ # Pass the current state to each output collection and see if they end up
159
+ # marking this node for rescan
156
160
  invalidate_tables(rescan, invalidate)
157
161
 
158
- # finally, if this node is in rescan, pass the request on to all source
162
+ # Finally, if this node is in rescan, pass the request on to all source
159
163
  # elements
160
164
  if rescan.member? self
161
165
  rescan.merge(srcs)
@@ -163,14 +167,16 @@ module Bud
163
167
  end
164
168
 
165
169
  def invalidate_tables(rescan, invalidate)
166
- # exchange rescan and invalidate information with tables. If this node is
167
- # in rescan, it may invalidate a target table (if it is a scratch). And if
168
- # the target node is invalidated, this node marks itself for rescan to
169
- # enable a refill of that table at run-time
170
- @outputs.each do |o|
171
- unless o.class <= PushElement
172
- o.add_rescan_invalidate(rescan, invalidate)
173
- rescan << self if invalidate.member? o
170
+ # Exchange rescan and invalidate information with tables. If this node is
171
+ # in rescan, it may invalidate an output table (if it is a scratch). And
172
+ # if the output table is going to be invalidated, this node marks itself
173
+ # for rescan to enable a refill of that table at run-time.
174
+ [@outputs, @pendings].each do |v|
175
+ v.each do |o|
176
+ unless o.class <= PushElement
177
+ o.add_rescan_invalidate(rescan, invalidate)
178
+ rescan << self if invalidate.member? o
179
+ end
174
180
  end
175
181
  end
176
182
  end
@@ -193,7 +199,7 @@ module Bud
193
199
  public
194
200
  def pro(the_name=elem_name, the_schema=schema, &blk)
195
201
  toplevel = @bud_instance.toplevel
196
- elem = Bud::PushElement.new("project#{object_id}",
202
+ elem = Bud::PushElement.new("project#{object_id}".to_sym,
197
203
  toplevel.this_rule_context,
198
204
  @collection_name, the_schema)
199
205
  self.wire_to(elem)
@@ -204,43 +210,38 @@ module Bud
204
210
 
205
211
  alias each pro
206
212
 
207
- # XXX: "the_name" & "the_schema" parameters are unused
208
213
  public
209
- def each_with_index(the_name=elem_name, the_schema=schema, &blk)
214
+ def each_with_index(&blk)
210
215
  toplevel = @bud_instance.toplevel
211
- elem = Bud::PushEachWithIndex.new("each_with_index#{object_id}",
216
+ elem = Bud::PushEachWithIndex.new("each_with_index#{object_id}".to_sym,
212
217
  toplevel.this_rule_context,
213
218
  @collection_name)
214
219
  elem.set_block(&blk)
215
220
  self.wire_to(elem)
216
- toplevel.push_elems[[self.object_id, :each, blk]] = elem
221
+ toplevel.push_elems[[self.object_id, :each_with_index, blk]] = elem
217
222
  end
218
223
 
219
224
  def join(elem2, &blk)
220
- # cached = @bud_instance.push_elems[[self.object_id,:join,[self,elem2], @bud_instance, blk]]
221
- # if cached.nil?
222
- elem2 = elem2.to_push_elem unless elem2.class <= PushElement
223
- toplevel = @bud_instance.toplevel
224
- join = Bud::PushSHJoin.new([self, elem2], toplevel.this_rule_context, [])
225
- self.wire_to(join)
226
- elem2.wire_to(join)
227
- toplevel.push_elems[[self.object_id, :join, [self, elem2], toplevel, blk]] = join
228
- toplevel.push_joins[toplevel.this_stratum] << join
229
- # else
230
- # cached.refcount += 1
231
- # end
232
- return toplevel.push_elems[[self.object_id, :join, [self, elem2], toplevel, blk]]
225
+ elem2 = elem2.to_push_elem unless elem2.kind_of? PushElement
226
+ toplevel = @bud_instance.toplevel
227
+ join = Bud::PushSHJoin.new([self, elem2], toplevel.this_rule_context, [])
228
+ self.wire_to(join)
229
+ elem2.wire_to(join)
230
+ toplevel.push_elems[[self.object_id, :join, [self, elem2], toplevel, blk]] = join
231
+ toplevel.push_joins[toplevel.this_stratum] << join
232
+ return join
233
233
  end
234
234
  def *(elem2, &blk)
235
235
  join(elem2, &blk)
236
236
  end
237
237
 
238
- def notin(elem2, preds=nil, &blk)
238
+ def notin(elem2, *preds, &blk)
239
+ elem2 = elem2.to_push_elem unless elem2.kind_of? PushElement
239
240
  toplevel = @bud_instance.toplevel
240
241
  notin_elem = Bud::PushNotIn.new([self, elem2], toplevel.this_rule_context, preds, &blk)
241
242
  self.wire_to(notin_elem)
242
243
  elem2.wire_to(notin_elem)
243
- toplevel.push_elems[[self.object_id, :notin, collection, toplevel, blk]] = notin_elem
244
+ toplevel.push_elems[[self.object_id, :notin, [self, elem2], toplevel, blk]] = notin_elem
244
245
  return notin_elem
245
246
  end
246
247
 
@@ -252,6 +253,7 @@ module Bud
252
253
  end
253
254
  end
254
255
  alias <= merge
256
+
255
257
  superator "<~" do |o|
256
258
  raise Bud::Error, "illegal use of <~ with pusher '#{tabname}' on left"
257
259
  end
@@ -266,7 +268,7 @@ module Bud
266
268
 
267
269
  def group(keycols, *aggpairs, &blk)
268
270
  # establish schema
269
- keycols = [] if keycols.nil?
271
+ keycols ||= []
270
272
  keycols = keycols.map{|c| canonicalize_col(c)}
271
273
  keynames = keycols.map{|k| k[2]}
272
274
  aggcolsdups = aggpairs.map{|ap| ap[0].class.name.split("::").last}
@@ -280,44 +282,37 @@ module Bud
280
282
  the_schema = { keynames => aggcols }
281
283
  end
282
284
 
283
- aggpairs = aggpairs.map{|ap| ap[1].nil? ? [ap[0]] : [ap[0], canonicalize_col(ap[1])]}
285
+ aggpairs = prep_aggpairs(aggpairs)
284
286
  toplevel = @bud_instance.toplevel
285
- # if @bud_instance.push_elems[[self.object_id, :group, keycols, aggpairs, blk]].nil?
286
- g = Bud::PushGroup.new('grp'+Time.new.tv_usec.to_s, toplevel.this_rule_context, @collection_name, keycols, aggpairs, the_schema, &blk)
287
- self.wire_to(g)
288
- toplevel.push_elems[[self.object_id, :group, keycols, aggpairs, blk]] = g
289
- # end
290
- # toplevel.push_elems[[self.object_id, :group, keycols, aggpairs, blk]]
287
+ g = Bud::PushGroup.new("grp#{Time.new.tv_usec}".to_sym, toplevel.this_rule_context,
288
+ @collection_name, keycols, aggpairs, the_schema, &blk)
289
+ self.wire_to(g)
290
+ toplevel.push_elems[[self.object_id, :group, keycols, aggpairs, blk]] = g
291
291
  return g
292
292
  end
293
293
 
294
294
  def argagg(aggname, gbkey_cols, collection, &blk)
295
+ gbkey_cols ||= []
295
296
  gbkey_cols = gbkey_cols.map{|c| canonicalize_col(c)}
296
297
  collection = canonicalize_col(collection)
297
298
  toplevel = @bud_instance.toplevel
298
299
  agg = toplevel.send(aggname, collection)[0]
299
- raise Bud::Error, "#{aggname} not declared exemplary" unless agg.class <= Bud::ArgExemplary
300
- keynames = gbkey_cols.map do |k|
301
- if k.class == Symbol
302
- k.to_s
303
- else
304
- k[2]
305
- end
300
+ unless agg.class <= Bud::ArgExemplary
301
+ raise Bud::Error, "#{aggname} not declared exemplary"
306
302
  end
303
+
307
304
  aggpairs = [[agg, collection]]
308
- # if toplevel.push_elems[[self.object_id,:argagg, gbkey_cols, aggpairs, blk]].nil?
309
- aa = Bud::PushArgAgg.new('argagg'+Time.new.tv_usec.to_s, toplevel.this_rule_context, @collection_name, gbkey_cols, aggpairs, schema, &blk)
310
- self.wire_to(aa)
311
- toplevel.push_elems[[self.object_id,:argagg, gbkey_cols, aggpairs, blk]] = aa
312
- # end
313
- # return toplevel.push_elems[[self.object_id,:argagg, gbkey_cols, aggpairs, blk]]
305
+ aa = Bud::PushArgAgg.new("argagg#{Time.new.tv_usec}".to_sym, toplevel.this_rule_context,
306
+ @collection_name, gbkey_cols, aggpairs, schema, &blk)
307
+ self.wire_to(aa)
308
+ toplevel.push_elems[[self.object_id, :argagg, gbkey_cols, aggpairs, blk]] = aa
314
309
  return aa
315
310
  end
316
311
  def argmax(gbcols, col, &blk)
317
- argagg(gbcols, Bud::max(col), blk)
312
+ argagg(:max, gbcols, col, &blk)
318
313
  end
319
314
  def argmin(gbcols, col, &blk)
320
- argagg(gbcols, Bud::min(col), blk)
315
+ argagg(:min, gbcols, col, &blk)
321
316
  end
322
317
  def sort(name=nil, bud_instance=nil, the_schema=nil, &blk)
323
318
  elem = Bud::PushSort.new(name, bud_instance, the_schema, &blk)
@@ -351,7 +346,7 @@ module Bud
351
346
  end
352
347
 
353
348
  def reduce(initial, &blk)
354
- retval = Bud::PushReduce.new("reduce#{Time.new.tv_usec}",
349
+ retval = Bud::PushReduce.new("reduce#{Time.new.tv_usec}".to_sym,
355
350
  @bud_instance, @collection_name,
356
351
  schema, initial, &blk)
357
352
  self.wire_to(retval)
@@ -394,7 +389,8 @@ module Bud
394
389
  end
395
390
 
396
391
  class PushPredicate < PushStatefulElement
397
- def initialize(pred_symbol, elem_name=nil, collection_name=nil, bud_instance=nil, schema_in=nil, &blk)
392
+ def initialize(pred_symbol, elem_name=nil, collection_name=nil,
393
+ bud_instance=nil, schema_in=nil, &blk)
398
394
  @pred_symbol = pred_symbol
399
395
  @in_buf = []
400
396
  super(elem_name, bud_instance, collection_name, schema_in, &blk)
@@ -447,6 +443,7 @@ module Bud
447
443
  class ScannerElement < PushElement
448
444
  attr_reader :collection
449
445
  attr_reader :rescan_set, :invalidate_set
446
+ attr_accessor :force_rescan
450
447
 
451
448
  def initialize(elem_name, bud_instance, collection_in,
452
449
  the_schema=collection_in.schema, &blk)
@@ -454,6 +451,7 @@ module Bud
454
451
  @collection = collection_in
455
452
  @rescan_set = []
456
453
  @invalidate_set = []
454
+ @force_rescan = false
457
455
  end
458
456
 
459
457
  def rescan
@@ -464,20 +462,21 @@ module Bud
464
462
  @collection.invalidate_at_tick # need to scan afresh if collection invalidated.
465
463
  end
466
464
 
467
- # collection of others to rescan/invalidate if this scanner's collection
468
- # were to be invalidated.
465
+ # What should be rescanned/invalidated if this scanner's collection were to
466
+ # be invalidated.
469
467
  def invalidate_at_tick(rescan, invalidate)
470
468
  @rescan_set = rescan
471
469
  @invalidate_set = invalidate
472
470
  end
473
471
 
474
472
  def add_rescan_invalidate(rescan, invalidate)
475
- # if the collection is to be invalidated, the scanner needs to be in
473
+ # If the collection is to be invalidated, the scanner needs to be in
476
474
  # rescan mode
477
475
  rescan << self if invalidate.member? @collection
478
476
 
479
- # in addition, default PushElement rescan/invalidate logic applies
480
- super
477
+ # Pass the current state to each output collection and see if they end up
478
+ # marking this node for rescan
479
+ invalidate_tables(rescan, invalidate)
481
480
 
482
481
  # Note also that this node can be nominated for rescan by a target node;
483
482
  # in other words, a scanner element can be set to rescan even if the
@@ -485,9 +484,11 @@ module Bud
485
484
  end
486
485
 
487
486
  def scan(first_iter)
488
- if first_iter
487
+ if @force_rescan
488
+ @collection.each_raw {|item| push_out(item)}
489
+ @force_rescan = false
490
+ elsif first_iter
489
491
  if rescan
490
- # Scan entire storage
491
492
  @collection.each_raw {|item| push_out(item)}
492
493
  else
493
494
  # In the first iteration, tick_delta would be non-null IFF the
@@ -497,7 +498,7 @@ module Bud
497
498
  end
498
499
 
499
500
  # send deltas out in all cases
500
- @collection.delta.each_value {|item| push_out(item)}
501
+ @collection.each_delta {|item| push_out(item)}
501
502
  end
502
503
  end
503
504
 
@@ -1,5 +1,4 @@
1
1
  require 'bud/executor/elements'
2
- require 'set'
3
2
 
4
3
  module Bud
5
4
  class PushGroup < PushStatefulElement
@@ -10,21 +9,25 @@ module Bud
10
9
  else
11
10
  @keys = keys_in.map{|k| k[1]}
12
11
  end
13
- # An aggpair is an array: [agg class instance, index of input field].
14
- # ap[1] is nil for Count.
15
- @aggpairs = aggpairs_in.map{|ap| [ap[0], ap[1].nil? ? nil : ap[1][1]]}
12
+ # An aggpair is an array: [agg class instance, array of indexes of input
13
+ # agg input columns]. The second field is nil for Count.
14
+ @aggpairs = aggpairs_in.map do |ap|
15
+ agg, *rest = ap
16
+ if rest.empty?
17
+ [agg, nil]
18
+ else
19
+ [agg, rest.map {|r| r[1]}]
20
+ end
21
+ end
16
22
  @groups = {}
17
23
 
18
24
  # Check whether we need to eliminate duplicates from our input (we might
19
25
  # see duplicates because of the rescan/invalidation logic, as well as
20
26
  # because we don't do duplicate elimination on the output of a projection
21
27
  # operator). We don't need to dupelim if all the args are exemplary.
22
- @elim_dups = @aggpairs.any? {|a| not a[0].kind_of? ArgExemplary}
23
- if @elim_dups
24
- @input_cache = Set.new
25
- end
28
+ @elim_dups = @aggpairs.any? {|ap| not ap[0].kind_of? ArgExemplary}
29
+ @input_cache = Set.new if @elim_dups
26
30
 
27
- @seen_new_data = false
28
31
  super(elem_name, bud_instance, collection_name, schema_in, &blk)
29
32
  end
30
33
 
@@ -34,19 +37,25 @@ module Bud
34
37
  @input_cache << item
35
38
  end
36
39
 
37
- @seen_new_data = true
38
40
  key = item.values_at(*@keys)
39
41
  group_state = @groups[key]
40
42
  if group_state.nil?
41
43
  @groups[key] = @aggpairs.map do |ap|
42
- input_val = ap[1].nil? ? item : item[ap[1]]
43
- ap[0].init(input_val)
44
+ if ap[1].nil?
45
+ ap[0].init(item)
46
+ else
47
+ ap[0].init(*item.values_at(*ap[1]))
48
+ end
44
49
  end
45
50
  else
46
51
  @aggpairs.each_with_index do |ap, agg_ix|
47
- input_val = ap[1].nil? ? item : item[ap[1]]
48
- state_val = ap[0].trans(group_state[agg_ix], input_val)[0]
49
- group_state[agg_ix] = state_val
52
+ state_val = group_state[agg_ix]
53
+ if ap[1].nil?
54
+ trans_rv = ap[0].trans(state_val, item)
55
+ else
56
+ trans_rv = ap[0].trans(state_val, *item.values_at(*ap[1]))
57
+ end
58
+ group_state[agg_ix] = trans_rv[0]
50
59
  end
51
60
  end
52
61
  end
@@ -62,14 +71,12 @@ module Bud
62
71
  puts "#{self.class}/#{self.tabname} invalidated" if $BUD_DEBUG
63
72
  @groups.clear
64
73
  @input_cache.clear if @elim_dups
65
- @seen_new_data = false
66
74
  end
67
75
 
68
76
  def flush
69
- # If we haven't seen any input since the last call to flush(), we're done:
70
- # our output would be the same as before.
71
- return unless @seen_new_data
72
- @seen_new_data = false
77
+ # Don't emit fresh output unless a rescan is needed
78
+ return unless @rescan
79
+ @rescan = false
73
80
 
74
81
  @groups.each do |key, group_state|
75
82
  rv = key.clone
@@ -87,7 +94,6 @@ module Bud
87
94
  raise Bud::Error, "multiple aggpairs #{aggpairs_in.map{|a| a.class.name}} in ArgAgg; only one allowed"
88
95
  end
89
96
  super(elem_name, bud_instance, collection_name, keys_in, aggpairs_in, schema_in, &blk)
90
- @agg, @aggcol = @aggpairs[0]
91
97
  @winners = {}
92
98
  end
93
99
 
@@ -101,18 +107,16 @@ module Bud
101
107
  key = @keys.map{|k| item[k]}
102
108
  group_state = @groups[key]
103
109
  if group_state.nil?
104
- @seen_new_data = true
105
110
  @groups[key] = @aggpairs.map do |ap|
106
111
  @winners[key] = [item]
107
- input_val = item[ap[1]]
108
- ap[0].init(input_val)
112
+ input_vals = item.values_at(*ap[1])
113
+ ap[0].init(*input_vals)
109
114
  end
110
115
  else
111
116
  @aggpairs.each_with_index do |ap, agg_ix|
112
- input_val = item[ap[1]]
113
- state_val, flag, *rest = ap[0].trans(group_state[agg_ix], input_val)
117
+ input_vals = item.values_at(*ap[1])
118
+ state_val, flag, *rest = ap[0].trans(group_state[agg_ix], *input_vals)
114
119
  group_state[agg_ix] = state_val
115
- @seen_new_data = true unless flag == :ignore
116
120
 
117
121
  case flag
118
122
  when :ignore
@@ -133,14 +137,13 @@ module Bud
133
137
  end
134
138
 
135
139
  def flush
136
- # If we haven't seen any input since the last call to flush(), we're done:
137
- # our output would be the same as before.
138
- return unless @seen_new_data
139
- @seen_new_data = false
140
+ # Don't emit fresh output unless a rescan is needed
141
+ return unless @rescan
142
+ @rescan = false
140
143
 
141
144
  @groups.each_key do |g|
142
145
  @winners[g].each do |t|
143
- push_out(t, false)
146
+ push_out(t)
144
147
  end
145
148
  end
146
149
  end