bud 0.9.7 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -58,7 +58,7 @@ Have a look at the following classic "transitive closure" example, which compute
58
58
 
59
59
  state do
60
60
  table :link, [:from, :to, :cost]
61
- table :path, [:from, :to, :cost]
61
+ table :path, [:from, :to, :cost]
62
62
  end
63
63
 
64
64
  bloom :make_paths do
@@ -67,7 +67,7 @@ Have a look at the following classic "transitive closure" example, which compute
67
67
 
68
68
  # recurse: path of length n+1 made by a link to a path of length n
69
69
  path <= (link*path).pairs(:to => :from) do |l,p|
70
- [l.from, p.to, l.cost+p.cost]
70
+ [l.from, p.to, l.cost + p.cost]
71
71
  end
72
72
  end
73
73
 
@@ -97,4 +97,4 @@ Note that it is possible to write a program in Bloom that is *unstratifiable*: t
97
97
 
98
98
  glass <= one_item {|t| ['full'] if glass.empty? }
99
99
 
100
- Consider the case where we start out with glass being empty. Then we know the fact `glass.empty?`, and the bloom statement says that `(glass.empty? => not glass.empty?)` which is equivalent to `(glass.empty? and not glass.empty?)` which is a contradiction. The Bud runtime detects cycles through non-monotonicity for you automatically when you instantiate your class.
100
+ Consider the case where we start out with glass being empty. Then we know the fact `glass.empty?`, and the bloom statement says that `(glass.empty? => not glass.empty?)` which is equivalent to `(glass.empty? and not glass.empty?)` which is a contradiction. The Bud runtime detects cycles through non-monotonicity for you automatically when you instantiate your class.
@@ -36,7 +36,7 @@ program = ShortestPaths.new
36
36
 
37
37
  # populate our little example. we put two links between 'a' and 'b'
38
38
  # to see whether our shortest-paths code does the right thing.
39
- program.link <= [['a', 'b', 1],
39
+ program.link <+ [['a', 'b', 1],
40
40
  ['a', 'b', 4],
41
41
  ['b', 'c', 1],
42
42
  ['c', 'd', 1],
@@ -48,6 +48,6 @@ program.shortest.to_a.sort.each {|t| puts t.inspect}
48
48
  puts "----"
49
49
 
50
50
  # now lets add an extra link and recompute
51
- program.link << ['e', 'f', 1]
51
+ program.link <+ [['e', 'f', 1]]
52
52
  program.tick
53
53
  program.shortest.to_a.sort.each {|t| puts t.inspect}
@@ -4,5 +4,5 @@ module ChatProtocol
4
4
  channel :mcast
5
5
  end
6
6
 
7
- DEFAULT_ADDR = "localhost:12345"
7
+ DEFAULT_ADDR = "127.0.0.1:12345"
8
8
  end
data/lib/bud.rb CHANGED
@@ -70,11 +70,11 @@ module Bud
70
70
  attr_reader :budtime, :inbound, :options, :meta_parser, :viz, :rtracer, :dsock
71
71
  attr_reader :tables, :builtin_tables, :channels, :zk_tables, :dbm_tables, :app_tables, :lattices
72
72
  attr_reader :push_sources, :push_elems, :push_joins, :scanners, :merge_targets
73
- attr_reader :this_stratum, :this_rule, :rule_orig_src, :done_bootstrap
73
+ attr_reader :this_stratum, :this_rule_context, :done_bootstrap
74
74
  attr_reader :inside_tick
75
75
  attr_accessor :stratified_rules
76
76
  attr_accessor :metrics, :periodics
77
- attr_accessor :this_rule_context, :qualified_name
77
+ attr_accessor :qualified_name
78
78
  attr_reader :running_async
79
79
 
80
80
  # options to the Bud runtime are passed in a hash, with the following keys
@@ -109,9 +109,6 @@ module Bud
109
109
  # * <tt>:dbm_dir</tt> filesystem directory to hold DBM-backed collections
110
110
  # * <tt>:dbm_truncate</tt> if true, DBM-backed collections are opened with +OTRUNC+
111
111
  def initialize(options={})
112
- # capture the binding for a subsequent 'eval'. This ensures that local
113
- # variable names introduced later in this method don't interfere with
114
- # table names used in the eval block.
115
112
  options[:dump_rewrite] ||= ENV["BUD_DUMP_REWRITE"].to_i > 0
116
113
  options[:dump_ast] ||= ENV["BUD_DUMP_AST"].to_i > 0
117
114
  options[:print_wiring] ||= ENV["BUD_PRINT_WIRING"].to_i > 0
@@ -139,7 +136,8 @@ module Bud
139
136
  @instance_id = ILLEGAL_INSTANCE_ID # Assigned when we start running
140
137
  @metrics = {}
141
138
  @endtime = nil
142
- @this_stratum = 0
139
+ @this_stratum = -1
140
+ @this_rule_id = -1
143
141
  @push_sorted_elems = nil
144
142
  @running_async = false
145
143
  @bud_started = false
@@ -158,8 +156,6 @@ module Bud
158
156
  resolve_imports
159
157
  call_state_methods
160
158
 
161
- @declarations = self.class.instance_methods.select {|m| m =~ /^__bloom__.+$/}.map {|m| m.to_s}
162
-
163
159
  @viz = VizOnline.new(self) if @options[:trace]
164
160
  @rtracer = RTrace.new(self) if @options[:rtrace]
165
161
 
@@ -500,8 +496,6 @@ module Bud
500
496
  rescan_invalidate_tc(stratum, rescan, invalidate)
501
497
  end
502
498
 
503
- prune_rescan_invalidate(rescan, invalidate)
504
- # transitive closure
505
499
  @default_rescan = rescan.to_a
506
500
  @default_invalidate = invalidate.to_a
507
501
 
@@ -526,7 +520,7 @@ module Bud
526
520
  rescan = dflt_rescan.clone
527
521
  invalidate = dflt_invalidate + [scanner.collection]
528
522
  rescan_invalidate_tc(stratum, rescan, invalidate)
529
- prune_rescan_invalidate(rescan, invalidate)
523
+ prune_rescan_set(rescan)
530
524
 
531
525
  # Make sure we reset the rescan/invalidate flag for this scanner at
532
526
  # end-of-tick, but we can remove the scanner from its own
@@ -599,7 +593,7 @@ module Bud
599
593
  end
600
594
  end
601
595
 
602
- def prune_rescan_invalidate(rescan, invalidate)
596
+ def prune_rescan_set(rescan)
603
597
  rescan.delete_if {|e| e.rescan_at_tick}
604
598
  end
605
599
 
@@ -616,7 +610,7 @@ module Bud
616
610
  end
617
611
 
618
612
  def do_rewrite
619
- @meta_parser = BudMeta.new(self, @declarations)
613
+ @meta_parser = BudMeta.new(self)
620
614
  @stratified_rules = @meta_parser.meta_rewrite
621
615
  end
622
616
 
@@ -737,16 +731,30 @@ module Bud
737
731
  # method blocks until Bud has been shutdown. If +stop_em+ is true, the
738
732
  # EventMachine event loop is also shutdown; this will interfere with the
739
733
  # execution of any other Bud instances in the same process (as well as
740
- # anything else that happens to use EventMachine).
734
+ # anything else that happens to use EventMachine). We always shutdown the EM
735
+ # loop if there are no more running Bud instances (this does interfere with
736
+ # other EM-using apps, but it is necessary).
741
737
  def stop(stop_em=false, do_shutdown_cb=true)
742
738
  schedule_and_wait do
743
739
  do_shutdown(do_shutdown_cb)
744
740
  end
745
741
 
742
+ # If we're shutting down the last active Bud instance, shutdown the EM event
743
+ # loop as well. This is probably good practice in general, but it also
744
+ # prevents weird EM behavior -- it seems as though EM::ConnectionNotBound
745
+ # exceptions can be raised if the EM event loop is left running and
746
+ # subsequent events arrive.
747
+ $signal_lock.synchronize {
748
+ stop_em = true if $bud_instances.empty? and EventMachine::reactor_running?
749
+ }
750
+
746
751
  if stop_em
747
752
  Bud.stop_em_loop
748
- EventMachine::reactor_thread.join
753
+ unless Thread.current == EventMachine::reactor_thread
754
+ EventMachine::reactor_thread.join
755
+ end
749
756
  end
757
+
750
758
  report_metrics if options[:metrics]
751
759
  end
752
760
  alias :stop_bg :stop
@@ -1065,6 +1073,35 @@ module Bud
1065
1073
  @done_bootstrap = true
1066
1074
  end
1067
1075
 
1076
+ def do_invalidate_rescan
1077
+ @default_rescan.each {|elem| elem.rescan = true}
1078
+ @default_invalidate.each {|elem|
1079
+ elem.invalidated = true
1080
+ # Call tick on tables here itself. The rest below
1081
+ elem.invalidate_cache unless elem.class <= PushElement
1082
+ }
1083
+
1084
+ # The following loop invalidates additional (non-default) elements and
1085
+ # tables that depend on the run-time invalidation state of a table. Loop
1086
+ # once to set the flags.
1087
+ each_scanner do |scanner, stratum|
1088
+ if scanner.rescan
1089
+ scanner.rescan_set.each {|e| e.rescan = true}
1090
+ scanner.invalidate_set.each {|e|
1091
+ e.invalidated = true
1092
+ e.invalidate_cache unless e.class <= PushElement
1093
+ }
1094
+ end
1095
+ end
1096
+
1097
+ # Loop a second time to actually call invalidate_cache. We can't merge this
1098
+ # with the loops above because some versions of invalidate_cache (e.g.,
1099
+ # join) depend on the rescan state of other elements.
1100
+ @num_strata.times do |stratum|
1101
+ @push_sorted_elems[stratum].each {|e| e.invalidate_cache if e.invalidated}
1102
+ end
1103
+ end
1104
+
1068
1105
  # One timestep of Bloom execution. This MUST be invoked from the EventMachine
1069
1106
  # thread; it is not intended to be called directly by client code.
1070
1107
  def tick_internal
@@ -1084,32 +1121,7 @@ module Bud
1084
1121
  else
1085
1122
  # inform tables and elements about beginning of tick.
1086
1123
  @app_tables.each {|t| t.tick}
1087
- @default_rescan.each {|elem| elem.rescan = true}
1088
- @default_invalidate.each {|elem|
1089
- elem.invalidated = true
1090
- # Call tick on tables here itself. The rest below
1091
- elem.invalidate_cache unless elem.class <= PushElement
1092
- }
1093
-
1094
- # The following loop invalidates additional (non-default) elements and
1095
- # tables that depend on the run-time invalidation state of a table.
1096
- # Loop once to set the flags.
1097
- each_scanner do |scanner, stratum|
1098
- if scanner.rescan
1099
- scanner.rescan_set.each {|e| e.rescan = true}
1100
- scanner.invalidate_set.each {|e|
1101
- e.invalidated = true
1102
- e.invalidate_cache unless e.class <= PushElement
1103
- }
1104
- end
1105
- end
1106
-
1107
- # Loop a second time to actually call invalidate_cache. We can't merge
1108
- # this with the loops above because some versions of invalidate_cache
1109
- # (e.g., join) depend on the rescan state of other elements.
1110
- @num_strata.times do |stratum|
1111
- @push_sorted_elems[stratum].each {|e| e.invalidate_cache if e.invalidated}
1112
- end
1124
+ do_invalidate_rescan
1113
1125
  end
1114
1126
 
1115
1127
  receive_inbound
@@ -1201,6 +1213,7 @@ module Bud
1201
1213
  table :t_cycle, [:predicate, :via, :neg, :temporal]
1202
1214
  table :t_depends, [:bud_obj, :rule_id, :lhs, :op, :body] => [:nm, :in_body]
1203
1215
  table :t_provides, [:interface] => [:input]
1216
+ table :t_rule_stratum, [:bud_obj, :rule_id] => [:stratum]
1204
1217
  table :t_rules, [:bud_obj, :rule_id] => [:lhs, :op, :src, :orig_src, :unsafe_funcs_called]
1205
1218
  table :t_stratum, [:predicate] => [:stratum]
1206
1219
  table :t_table_info, [:tab_name, :tab_type]
@@ -1211,12 +1224,12 @@ module Bud
1211
1224
  @builtin_tables = @tables.clone if toplevel
1212
1225
  end
1213
1226
 
1214
- # Handle any inbound tuples off the wire. Received messages are placed
1215
- # directly into the storage of the appropriate local channel. The inbound
1216
- # queue is cleared at the end of the tick.
1227
+ # Handle external inputs: channels, terminals, and periodics. Received
1228
+ # messages are placed directly into the storage of the appropriate local
1229
+ # collection. The inbound queue is cleared at the end of the tick.
1217
1230
  def receive_inbound
1218
1231
  @inbound.each do |tbl_name, msg_buf|
1219
- puts "channel #{tbl_name} rcv: #{msg_buf}" if $BUD_DEBUG
1232
+ puts "recv via #{tbl_name}: #{msg_buf}" if $BUD_DEBUG
1220
1233
  msg_buf.each do |b|
1221
1234
  tables[tbl_name] << b
1222
1235
  end
@@ -1241,7 +1254,8 @@ module Bud
1241
1254
  # of PushElements
1242
1255
  @this_stratum = strat_num
1243
1256
  rules.each_with_index do |rule, i|
1244
- @this_rule_context = rule.bud_obj # user-supplied code blocks will be evaluated in this context at run-time
1257
+ # user-supplied code blocks will be evaluated in this context at run-time
1258
+ @this_rule_context = rule.bud_obj
1245
1259
  begin
1246
1260
  eval_rule(rule.bud_obj, rule.src)
1247
1261
  rescue Exception => e
@@ -1252,6 +1266,8 @@ module Bud
1252
1266
  raise new_e
1253
1267
  end
1254
1268
  end
1269
+ @this_rule_context = nil
1270
+ @this_stratum = -1
1255
1271
  end
1256
1272
 
1257
1273
  ######## ids and timers
@@ -1282,10 +1298,10 @@ module Bud
1282
1298
  EventMachine::release_machine
1283
1299
  EventMachine::instance_variable_set('@reactor_running', false)
1284
1300
  end
1301
+
1285
1302
  # Shutdown all the Bud instances inherited from the parent process, but
1286
1303
  # don't invoke their shutdown callbacks
1287
1304
  Bud.shutdown_all_instances(false)
1288
-
1289
1305
  $got_shutdown_signal = false
1290
1306
  $signal_handler_setup = false
1291
1307
 
@@ -1305,16 +1321,16 @@ module Bud
1305
1321
  end
1306
1322
 
1307
1323
  # Signal handling. If multiple Bud instances are running inside a single
1308
- # process, we want a SIGINT or SIGTERM signal to cleanly shutdown all of them.
1324
+ # process, we want a SIGINT or SIGTERM signal to cleanly shutdown all of
1325
+ # them. Note that we don't try to do any significant work in the signal
1326
+ # handlers themselves: we just set a flag that is checked by a periodic timer.
1309
1327
  def self.init_signal_handlers(b)
1310
1328
  $signal_lock.synchronize {
1311
- # If we setup signal handlers and then fork a new process, we want to
1312
- # reinitialize the signal handler in the child process.
1329
+ # Initialize or re-initialize signal handlers if necessary.
1313
1330
  unless b.options[:signal_handling] == :none || $signal_handler_setup
1314
1331
  EventMachine::PeriodicTimer.new(SIGNAL_CHECK_PERIOD) do
1315
1332
  if $got_shutdown_signal
1316
1333
  Bud.shutdown_all_instances
1317
- Bud.stop_em_loop
1318
1334
  $got_shutdown_signal = false
1319
1335
  end
1320
1336
  end
@@ -1,10 +1,14 @@
1
1
  require 'bud/rewrite'
2
2
 
3
3
  class BudMeta #:nodoc: all
4
- def initialize(bud_instance, declarations)
5
- @bud_instance = bud_instance
6
- @declarations = declarations
7
- @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
8
12
  end
9
13
 
10
14
  def meta_rewrite
@@ -12,20 +16,30 @@ class BudMeta #:nodoc: all
12
16
 
13
17
  stratified_rules = []
14
18
  if @bud_instance.toplevel == @bud_instance
15
- 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
16
25
 
17
26
  # stratum_map = {fully qualified pred => stratum}. Copy stratum_map data
18
27
  # into t_stratum format.
19
28
  raise unless @bud_instance.t_stratum.to_a.empty?
20
29
  @bud_instance.t_stratum.merge(stratum_map.to_a)
21
30
 
22
- # slot each rule into the stratum corresponding to its lhs pred (from stratum_map)
23
31
  stratified_rules = Array.new(top_stratum + 2) { [] } # stratum -> [ rules ]
24
32
  @bud_instance.t_rules.each do |rule|
25
33
  if rule.op == '<='
26
34
  # Deductive rules are assigned to strata based on the basic Datalog
27
- # stratification algorithm
28
- 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
+
29
43
  # If the rule body doesn't reference any collections, it won't be
30
44
  # assigned a stratum, so just place it in stratum zero
31
45
  belongs_in ||= 0
@@ -35,28 +49,39 @@ class BudMeta #:nodoc: all
35
49
  stratified_rules[top_stratum + 1] << rule
36
50
  end
37
51
  end
38
- # stratified_rules[0] may be empty if none of the nodes at stratum 0 are on the lhs
39
- # stratified_rules[top_stratum+1] will be empty if there are no temporal rules.
40
- # Cleanup
41
- 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
+
42
64
  dump_rewrite(stratified_rules) if @bud_instance.options[:dump_rewrite]
43
65
  end
44
66
  return stratified_rules
45
67
  end
46
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
+
47
75
  def shred_rules
48
- # to completely characterize the rules of a bud class we must extract
49
- # from all parent classes/modules
50
- # after making this pass, we no longer care about the names of methods.
51
- # we are shredding down to the granularity of rule heads.
52
- 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.
53
78
  rulebag = {}
54
79
  @bud_instance.class.ancestors.reverse.each do |anc|
55
80
  @declarations.each do |meth_name|
56
- rw = rewrite_rule_block(anc, meth_name, seed)
81
+ rw = rewrite_rule_block(anc, meth_name)
57
82
  if rw
58
- seed = rw.rule_indx
59
83
  rulebag[meth_name] = rw
84
+ @rule_idx = rw.rule_idx
60
85
  end
61
86
  end
62
87
  end
@@ -67,7 +92,7 @@ class BudMeta #:nodoc: all
67
92
  end
68
93
  end
69
94
 
70
- def rewrite_rule_block(klass, block_name, seed)
95
+ def rewrite_rule_block(klass, block_name)
71
96
  return unless klass.respond_to? :__bloom_asts__
72
97
 
73
98
  pt = klass.__bloom_asts__[block_name]
@@ -103,7 +128,7 @@ class BudMeta #:nodoc: all
103
128
  end
104
129
  raise Bud::CompileError, "#{error_msg} in rule block \"#{block_name}\"#{src_msg}"
105
130
  end
106
- rewriter = RuleRewriter.new(seed, @bud_instance)
131
+ rewriter = RuleRewriter.new(@bud_instance, @rule_idx)
107
132
  rewriter.process(pt)
108
133
  return rewriter
109
134
  end
@@ -176,64 +201,61 @@ class BudMeta #:nodoc: all
176
201
  end
177
202
 
178
203
 
179
- Node = Struct.new :name, :status, :stratum, :edges, :in_lhs, :in_body, :in_cycle, :is_neg_head
180
204
  # Node.status is one of :init, :in_progress, :done
205
+ Node = Struct.new :name, :status, :stratum, :edges, :in_lhs, :in_body, :already_neg
181
206
  Edge = Struct.new :to, :op, :neg, :temporal
182
207
 
183
- def stratify_preds
184
- bud = @bud_instance.toplevel
208
+ def compute_node_graph
185
209
  nodes = {}
186
- bud.t_depends.each do |d|
187
- #t_depends [:bud_instance, :rule_id, :lhs, :op, :body] => [:nm, :in_body]
188
- 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))
189
213
  lhs.in_lhs = true
190
- body = (nodes[d.body] ||= Node.new(d.body, :init, 0, [], false, true, false, false))
191
- temporal = d.op != "<="
192
- lhs.edges << Edge.new(body, d.op, d.nm, temporal)
214
+ body = (nodes[d.body] ||= Node.new(d.body, :init, 0, [], false, true))
193
215
  body.in_body = true
216
+ temporal = (d.op != "<=")
217
+ lhs.edges << Edge.new(body, d.op, d.nm, temporal)
194
218
  end
195
219
 
220
+ return nodes
221
+ end
222
+
223
+ def stratify_preds(nodes)
196
224
  nodes.each_value {|n| calc_stratum(n, false, false, [n.name])}
225
+
197
226
  # Normalize stratum numbers because they may not be 0-based or consecutive
198
227
  remap = {}
199
228
  # if the nodes stratum numbers are [2, 3, 2, 4], remap = {2 => 0, 3 => 1, 4 => 2}
200
229
  nodes.values.map {|n| n.stratum}.uniq.sort.each_with_index do |num, i|
201
230
  remap[num] = i
202
231
  end
232
+
203
233
  stratum_map = {}
204
- top_stratum = -1
205
234
  nodes.each_pair do |name, n|
206
235
  n.stratum = remap[n.stratum]
207
236
  stratum_map[n.name] = n.stratum
208
- top_stratum = max(top_stratum, n.stratum)
209
237
  end
210
- analyze_dependencies(nodes)
211
- return nodes, stratum_map, top_stratum
238
+ return stratum_map
212
239
  end
213
240
 
214
- def max(a, b) ; a > b ? a : b ; end
215
-
216
241
  def calc_stratum(node, neg, temporal, path)
217
242
  if node.status == :in_process
218
- node.in_cycle = true
219
- if neg and !temporal and node.is_neg_head
243
+ if neg and not temporal and not node.already_neg
220
244
  raise Bud::CompileError, "unstratifiable program: #{path.uniq.join(',')}"
221
245
  end
222
246
  elsif node.status == :init
223
247
  node.status = :in_process
224
248
  node.edges.each do |edge|
225
- node.is_neg_head = edge.neg
226
249
  next unless edge.op == "<="
250
+ node.already_neg = neg
227
251
  body_stratum = calc_stratum(edge.to, (neg or edge.neg), (edge.temporal or temporal), path + [edge.to.name])
228
- node.is_neg_head = false # reset for next edge
229
- node.stratum = max(node.stratum, body_stratum + (edge.neg ? 1 : 0))
252
+ node.stratum = [node.stratum, body_stratum + (edge.neg ? 1 : 0)].max
230
253
  end
231
254
  node.status = :done
232
255
  end
233
256
  node.stratum
234
257
  end
235
258
 
236
-
237
259
  def analyze_dependencies(nodes) # nodes = {node name => node}
238
260
  preds_in_lhs = nodes.select {|_, node| node.in_lhs}.map {|name, _| name}.to_set
239
261
  preds_in_body = nodes.select {|_, node| node.in_body}.map {|name, _| name}.to_set