bud 0.9.7 → 0.9.8

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