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