wongi-engine 0.0.1

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.
Files changed (59) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +22 -0
  4. data/README.md +349 -0
  5. data/Rakefile +2 -0
  6. data/examples/ex01.rb +23 -0
  7. data/examples/ex02.rb +36 -0
  8. data/examples/graphviz.rb +15 -0
  9. data/examples/timeline.rb +48 -0
  10. data/lib/wongi-engine.rb +22 -0
  11. data/lib/wongi-engine/alpha_memory.rb +46 -0
  12. data/lib/wongi-engine/beta.rb +10 -0
  13. data/lib/wongi-engine/beta/beta_memory.rb +48 -0
  14. data/lib/wongi-engine/beta/beta_node.rb +164 -0
  15. data/lib/wongi-engine/beta/filter_node.rb +109 -0
  16. data/lib/wongi-engine/beta/join_node.rb +127 -0
  17. data/lib/wongi-engine/beta/ncc_node.rb +46 -0
  18. data/lib/wongi-engine/beta/ncc_partner.rb +43 -0
  19. data/lib/wongi-engine/beta/neg_node.rb +58 -0
  20. data/lib/wongi-engine/beta/optional_node.rb +43 -0
  21. data/lib/wongi-engine/beta/or_node.rb +76 -0
  22. data/lib/wongi-engine/beta/production_node.rb +31 -0
  23. data/lib/wongi-engine/core_ext.rb +57 -0
  24. data/lib/wongi-engine/dsl.rb +112 -0
  25. data/lib/wongi-engine/dsl/action.rb +12 -0
  26. data/lib/wongi-engine/dsl/actions/error_generator.rb +42 -0
  27. data/lib/wongi-engine/dsl/actions/simple_action.rb +23 -0
  28. data/lib/wongi-engine/dsl/actions/simple_collector.rb +51 -0
  29. data/lib/wongi-engine/dsl/actions/statement_generator.rb +52 -0
  30. data/lib/wongi-engine/dsl/actions/trace_action.rb +52 -0
  31. data/lib/wongi-engine/dsl/any_rule.rb +48 -0
  32. data/lib/wongi-engine/dsl/dsl_builder.rb +44 -0
  33. data/lib/wongi-engine/dsl/dsl_extensions.rb +43 -0
  34. data/lib/wongi-engine/dsl/extension_clause.rb +36 -0
  35. data/lib/wongi-engine/dsl/generation_clause.rb +15 -0
  36. data/lib/wongi-engine/dsl/generic_production_rule.rb +78 -0
  37. data/lib/wongi-engine/dsl/ncc_production_rule.rb +21 -0
  38. data/lib/wongi-engine/dsl/production_rule.rb +4 -0
  39. data/lib/wongi-engine/dsl/query.rb +24 -0
  40. data/lib/wongi-engine/graph.rb +71 -0
  41. data/lib/wongi-engine/model_context.rb +13 -0
  42. data/lib/wongi-engine/network.rb +416 -0
  43. data/lib/wongi-engine/network/collectable.rb +42 -0
  44. data/lib/wongi-engine/network/debug.rb +25 -0
  45. data/lib/wongi-engine/ruleset.rb +74 -0
  46. data/lib/wongi-engine/template.rb +111 -0
  47. data/lib/wongi-engine/token.rb +137 -0
  48. data/lib/wongi-engine/version.rb +5 -0
  49. data/lib/wongi-engine/wme.rb +134 -0
  50. data/lib/wongi-engine/wme_match_data.rb +34 -0
  51. data/spec/dataset_spec.rb +26 -0
  52. data/spec/dsl_spec.rb +9 -0
  53. data/spec/high_level_spec.rb +341 -0
  54. data/spec/ruleset_spec.rb +54 -0
  55. data/spec/simple_action_spec.rb +40 -0
  56. data/spec/spec_helper.rb +1 -0
  57. data/spec/wme_spec.rb +83 -0
  58. data/wongi-engine.gemspec +19 -0
  59. metadata +110 -0
@@ -0,0 +1,43 @@
1
+ module Wongi::Engine
2
+ module DSLExtensions
3
+
4
+ def self.create_extension extension
5
+
6
+ section = extension[:section]
7
+ clause = extension[:clause]
8
+ action = extension[:action]
9
+ body = extension[:body]
10
+ acceptor = extension[:accept]
11
+
12
+ define_method clause.first do |*args, &block|
13
+
14
+ raise "#{clause.first} can only be used in section #{section}, currently in #{@current_section}" if section != @current_section
15
+
16
+ if body
17
+
18
+ instance_exec *args, &body
19
+
20
+ elsif acceptor
21
+
22
+ accept acceptor.new( *args, &block )
23
+
24
+ elsif action
25
+
26
+ c = ExtensionClause.new *args, &block
27
+ c.name = clause.first
28
+ c.action = action
29
+ c.rule = self
30
+ accept c
31
+
32
+ end
33
+
34
+ end
35
+
36
+ clause[1..-1].each do |al|
37
+ alias_method al, clause.first
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ module Wongi::Engine
2
+ class ExtensionClause
3
+
4
+ attr_accessor :name, :action, :rule
5
+
6
+ def initialize *args, &block
7
+ @args = args
8
+ @block = block
9
+ end
10
+
11
+ def import_into rete
12
+ if action.respond_to? :call
13
+ self
14
+ else
15
+ a = action.new *@args, &@block
16
+ a.name = name if a.respond_to? :name=
17
+ a.rule = rule if a.respond_to? :rule=
18
+ a.rete = rete if a.respond_to? :rete=
19
+ a
20
+ end
21
+ rescue Exception => e
22
+ e1 = Exception.new "error defining clause #{name} handled by #{action}: #{e}"
23
+ e1.set_backtrace e.backtrace
24
+ raise e1
25
+ end
26
+
27
+ def compile *args
28
+ action.call *args
29
+ end
30
+
31
+ def execute *args
32
+ action.call *args
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ module Wongi::Engine
2
+ class GenerationClause
3
+
4
+ def initialize s, p, o
5
+ @triple = Template.new( s, p, o )
6
+ end
7
+
8
+ def import_into rete
9
+ generator = StatementGenerator.new @triple.import_into( rete )
10
+ generator.rete = rete
11
+ generator
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,78 @@
1
+ module Wongi::Engine
2
+ class GenericProductionRule
3
+
4
+ attr_reader :name
5
+ attr_reader :conditions, :actions
6
+
7
+ include DSLExtensions
8
+
9
+ class << self
10
+
11
+ def section s, *aliases
12
+ unless sections.include?(s)
13
+ sections << s
14
+ define_method s do |&d|
15
+ @current_section = s
16
+ instance_eval &d
17
+ end
18
+ aliases.each { |a| alias_method a, s }
19
+ end
20
+ end
21
+
22
+ def sections
23
+ @sections ||= []
24
+ end
25
+
26
+ end
27
+
28
+ section :forall, :for_all
29
+ section :make, :do!
30
+
31
+ def initialize name
32
+ @name = name
33
+ @conditions = []
34
+ @actions = []
35
+ @current_section = nil
36
+ @acceptors = {}
37
+ GenericProductionRule.sections.each { |section| @acceptors[section] ||= [] }
38
+ end
39
+
40
+ def import_into rete
41
+
42
+ copy = self.class.new @name
43
+
44
+ copy.conditions = @acceptors[:forall].map do |condition|
45
+ if condition.respond_to? :import_into
46
+ condition.import_into(rete)
47
+ else
48
+ condition
49
+ end
50
+ end
51
+
52
+ copy.actions = @acceptors[:make].map do |action|
53
+ if action.respond_to? :import_into
54
+ action.import_into(rete)
55
+ else
56
+ action
57
+ end
58
+ end
59
+
60
+ copy
61
+ rescue Exception => e
62
+ e1 = Exception.new "in rule #{name}: #{e}"
63
+ e1.set_backtrace e.backtrace
64
+ raise e1
65
+ end
66
+
67
+ protected
68
+ attr_writer :conditions, :actions
69
+
70
+ def accept stuff
71
+ @acceptors[@current_section] << stuff
72
+ end
73
+
74
+
75
+ end
76
+
77
+
78
+ end
@@ -0,0 +1,21 @@
1
+ module Wongi::Engine
2
+ class NccProductionRule < GenericProductionRule
3
+
4
+ def initialize name = nil, &block
5
+ super
6
+ if block
7
+ forall &block
8
+ end
9
+ end
10
+
11
+ def import_into rete
12
+ NccSet.new @acceptors[:forall].map { |condition|
13
+ if condition.respond_to? :import_into
14
+ condition.import_into(rete)
15
+ else
16
+ condition
17
+ end
18
+ }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ module Wongi::Engine
2
+ class ProductionRule < GenericProductionRule
3
+ end
4
+ end
@@ -0,0 +1,24 @@
1
+ module Wongi::Engine
2
+
3
+ class Query < GenericProductionRule
4
+
5
+ attr_reader :parameters
6
+
7
+ def initialize name
8
+ super
9
+ @parameters = []
10
+ end
11
+
12
+ def search_on *terms
13
+ terms.each { |term| @parameters << term }
14
+ end
15
+
16
+ def import_into model
17
+ copy = super
18
+ copy.search_on *@parameters
19
+ copy
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,71 @@
1
+ module Wongi::Engine
2
+
3
+ class Graph
4
+
5
+ def initialize rete
6
+ @rete = rete
7
+ end
8
+
9
+ def dot io, opts = { }
10
+
11
+ if String === io
12
+ File.open io, "w" do |actual_io|
13
+ dot actual_io
14
+ end
15
+ return
16
+ end
17
+
18
+ @io = io
19
+
20
+ @io.puts "digraph {"
21
+
22
+ dump_alphas(opts) unless opts[:alpha] == false
23
+ dump_betas(opts)
24
+
25
+ @io.puts "};"
26
+
27
+ ensure
28
+ @io = nil
29
+ end
30
+
31
+ private
32
+
33
+ def print_hash h
34
+ h.to_s.gsub /-/, '_'
35
+ end
36
+
37
+ def dump_alphas opts
38
+ @io.puts "subgraph cluster_alphas {"
39
+ @rete.alphas.select { |alpha| not alpha.betas.empty? }.each do |alpha|
40
+ @io.puts "node#{print_hash alpha.hash} [shape=box label=\"#{alpha.template.to_s.gsub /"/, "\\\""}\"];"
41
+ end
42
+ @io.puts "};"
43
+ end
44
+
45
+ def dump_betas opts
46
+ dump_beta @rete.beta_top, opts
47
+ end
48
+
49
+ def dump_beta beta, opts
50
+ @io.puts "node#{print_hash beta.hash} [label=\"#{beta.class.name.split('::').last}\"];"
51
+ if beta.parent
52
+ @io.puts "node#{print_hash beta.parent.hash} -> node#{print_hash beta.hash};"
53
+ end
54
+ if beta.is_a? NccNode
55
+ @io.puts "node#{print_hash beta.partner.hash} -> node#{print_hash beta.hash};"
56
+ @io.puts "{ rank=same; node#{print_hash beta.partner.hash} node#{print_hash beta.hash} }"
57
+ end
58
+ if beta.respond_to? :alpha and opts[:alpha] != false
59
+ alpha = beta.alpha
60
+ if alpha
61
+ @io.puts "node#{print_hash alpha.hash} -> node#{print_hash beta.hash};"
62
+ end
63
+ end
64
+ beta.children.each do |child|
65
+ dump_beta child, opts
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -0,0 +1,13 @@
1
+ module Wongi
2
+ module Engine
3
+
4
+ class ModelContext
5
+ attr_reader :asserted_wmes, :retracted_wmes, :name
6
+ def initialize name
7
+ @name = name
8
+ @asserted_wmes = []
9
+ @retracted_wmes = []
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,416 @@
1
+ require 'wongi-engine/network/collectable'
2
+ require 'wongi-engine/network/debug'
3
+
4
+ module Wongi::Engine
5
+ class Network
6
+
7
+ attr_reader :alpha_top, :beta_top
8
+ attr_reader :queries, :results
9
+ attr_reader :productions
10
+
11
+ include NetworkParts::Collectable
12
+
13
+ protected
14
+ attr_accessor :alpha_hash
15
+ attr_writer :alpha_top, :beta_top
16
+ attr_writer :queries, :results
17
+
18
+ public
19
+
20
+ def debug!
21
+ extend NetworkParts::Debug
22
+ end
23
+
24
+ def initialize
25
+ @timeline = []
26
+ self.alpha_top = AlphaMemory.new( Template.new( :_, :_, :_ ), self )
27
+ self.alpha_hash = { Template.hash_for( :_, :_, :_ ) => self.alpha_top }
28
+ self.beta_top = BetaMemory.new(nil)
29
+ self.beta_top.rete = self
30
+ self.beta_top.seed
31
+ self.queries = {}
32
+ self.results = {}
33
+ @cache = {}
34
+ @revns = {}
35
+ @contexts = {}
36
+
37
+ @productions = { }
38
+
39
+ @collectors = {}
40
+ @collectors[:error] = []
41
+
42
+ end
43
+
44
+ def dump
45
+ beta_top.dump
46
+ end
47
+
48
+ def alphas
49
+ alpha_hash.values
50
+ end
51
+
52
+ def import thing
53
+ case thing
54
+ when String, Numeric, TrueClass, FalseClass, NilClass
55
+ thing
56
+ when Symbol
57
+ thing
58
+ else
59
+ raise "I don't know how to import a #{thing.class}"
60
+ end
61
+ end
62
+
63
+ def assert wme
64
+
65
+ unless wme.rete == self
66
+ wme = wme.import_into self
67
+ end
68
+
69
+ if @current_context
70
+ @current_context.asserted_wmes << wme
71
+ wme.context = @current_context
72
+ end
73
+
74
+ return if @cache.has_key?(wme)
75
+
76
+ # puts "ASSERTING #{wme}"
77
+ @cache[wme] = wme
78
+
79
+ s = wme.subject
80
+ p = wme.predicate
81
+ o = wme.object
82
+
83
+ alpha_activate(lookup( s, p, o), wme)
84
+ alpha_activate(lookup( s, p, :_), wme)
85
+ alpha_activate(lookup( s, :_, o), wme)
86
+ alpha_activate(lookup(:_, p, o), wme)
87
+ alpha_activate(lookup( s, :_, :_), wme)
88
+ alpha_activate(lookup(:_, p, :_), wme)
89
+ alpha_activate(lookup(:_, :_, o), wme)
90
+ alpha_activate(lookup(:_, :_, :_), wme)
91
+
92
+ wme
93
+ end
94
+
95
+ def wmes
96
+ alpha_top.wmes
97
+ end
98
+ alias_method :statements, :wmes
99
+ alias_method :facts, :wmes
100
+
101
+ def in_snapshot?
102
+ @in_snapshot
103
+ end
104
+
105
+ def snapshot!
106
+ @timeline.each_with_index do |slice, index|
107
+ source = if index == @timeline.size - 1
108
+ alpha_hash
109
+ else
110
+ @timeline[index+1]
111
+ end
112
+ # puts "source = #{source}"
113
+ wmes = {}
114
+ slice.each { |key, alpha| wmes[key] = alpha.wmes }
115
+ slice.each do |key, alpha|
116
+ in_snapshot {
117
+ wmes[key].dup.each { |wme| wme.destroy }
118
+ }
119
+ alpha.snapshot! source[key]
120
+ end
121
+ end
122
+ end
123
+
124
+ def rule name = nil, &block
125
+ r = ProductionRule.new( name || generate_rule_name )
126
+ r.instance_eval &block
127
+ self << r
128
+ end
129
+
130
+ def query name, &block
131
+ q = Query.new name
132
+ q.instance_eval &block
133
+ self << q
134
+ end
135
+
136
+ def << something
137
+ case something
138
+ when Array
139
+ if something.length == 3
140
+ assert WME.new( *something )
141
+ else
142
+ raise "Arrays must have 3 elements"
143
+ end
144
+ when ProductionRule
145
+ derived = something.import_into self
146
+ production = add_production derived.conditions, derived.actions
147
+ if something.name
148
+ productions[ something.name ] = production
149
+ end
150
+ when Query
151
+ derived = something.import_into self
152
+ prepare_query derived.name, derived.conditions, derived.parameters, derived.actions
153
+ when Ruleset
154
+ something.install self
155
+ when WME
156
+ assert something
157
+ #when Wongi::RDF::Document
158
+ # something.statements.each do |st|
159
+ # assert WME.new( st.subject, st.predicate, st.object, self )
160
+ # end
161
+ when Rete
162
+ something.each do |st|
163
+ assert st.import_into( self )
164
+ end
165
+ else
166
+ raise "I don't know how to accept a #{something.class}"
167
+ end
168
+ end
169
+
170
+ def retract wme, is_real = false
171
+
172
+ if ! is_real
173
+ if @current_context
174
+ @current_context.retracted_wmes << wme
175
+ end
176
+ end
177
+
178
+ real = if is_real
179
+ wme
180
+ else
181
+ #find(wme.subject, wme.predicate, wme.object)
182
+ @cache[wme]
183
+ end
184
+
185
+ return false if real.nil?
186
+ @cache.delete(real)
187
+ raise "Cannot retract inferred statements" unless real.manual?
188
+
189
+ real.destroy
190
+
191
+ true
192
+
193
+ end
194
+
195
+ def compile_alpha condition
196
+ template = Template.new :_, :_, :_
197
+ time = condition.time
198
+
199
+ template.subject = condition.subject unless Template.variable?( condition.subject )
200
+ template.predicate = condition.predicate unless Template.variable?( condition.predicate )
201
+ template.object = condition.object unless Template.variable?( condition.object )
202
+
203
+ hash = template.hash
204
+ # puts "COMPILED CONDITION #{condition} WITH KEY #{key}"
205
+ if time == 0
206
+ return self.alpha_hash[ hash ] if self.alpha_hash.has_key?( hash )
207
+ else
208
+ return @timeline[time+1][ hash ] if @timeline[time+1] && @timeline[time+1].has_key?( hash )
209
+ end
210
+
211
+ alpha = AlphaMemory.new( template, self )
212
+
213
+ if time == 0
214
+ self.alpha_hash[ hash ] = alpha
215
+ initial_fill alpha
216
+ else
217
+ if @timeline[time+1].nil?
218
+ # => ensure lineage from 0 to time
219
+ compile_alpha condition.class.new(condition.subject, condition.predicate, condition.object, time + 1)
220
+ @timeline.unshift Hash.new
221
+ end
222
+ @timeline[time+1][ hash ] = alpha
223
+ end
224
+ alpha
225
+ end
226
+
227
+ def cache s, p, o
228
+ compile_alpha Template.new(s, p, o).import_into( self )
229
+ end
230
+
231
+ def initial_fill alpha
232
+ tpl = alpha.template
233
+ source = more_generic_alpha(tpl)
234
+ # puts "more efficient by #{alpha_top.wmes.size - source.wmes.size}" unless source ==
235
+ # alpha_top
236
+ source.wmes.each do |wme|
237
+ alpha.activate wme if wme =~ tpl
238
+ end
239
+ end
240
+
241
+ def add_production conditions, actions = []
242
+ real_add_production self.beta_top, conditions, [], actions, false
243
+ end
244
+
245
+ def remove_production pnode
246
+ delete_node_with_ancestors pnode
247
+ end
248
+
249
+ def prepare_query name, conditions, parameters, actions = []
250
+ query = self.queries[ name ] = BetaMemory.new( nil )
251
+ query.rete = self
252
+ transformed = {}
253
+ parameters.each { |param| transformed[param] = nil }
254
+ query.seed transformed
255
+ self.results[ name ] = real_add_production query, conditions, parameters, actions, true
256
+ end
257
+
258
+ def execute name, valuations
259
+ beta = self.queries[name]
260
+ raise "Undefined query #{name}; known queries are #{queries.keys}" unless beta
261
+ beta.subst valuations
262
+ end
263
+
264
+ def inspect
265
+ "<Rete>"
266
+ end
267
+
268
+ def context= name
269
+ if name && !@contexts.has_key?(name)
270
+ @current_context = (@contexts[name] ||= ModelContext.new name)
271
+ end
272
+ end
273
+
274
+ def retract_context name
275
+ return unless @contexts.has_key?(name)
276
+
277
+ if @current_context && @current_context.name == name
278
+ @current_context = nil
279
+ end
280
+ ctx = @contexts[name]
281
+ ctx.asserted_wmes.select { |wme| wme.generating_tokens.empty? }.each { |wme| retract(wme, true) }
282
+ ctx.retracted_wmes.each { |wme| assert(wme) }
283
+ @contexts.delete name
284
+ end
285
+
286
+ def exists? wme
287
+ @cache[ wme ]
288
+ end
289
+
290
+ def each *args
291
+ return unless block_given?
292
+ unless args.length == 0 || args.length == 3
293
+ raise "Document#each expects a pattern or nothing at all"
294
+ end
295
+ s, p, o = if args.empty?
296
+ [:_, :_, :_]
297
+ else
298
+ args
299
+ end
300
+ no_check = s == :_ && p == :_ && o == :_
301
+ template = Template.new(s, p, o).import_into self
302
+ alpha_top.wmes.each do |wme|
303
+ yield wme if (no_check || wme =~ template)
304
+ end
305
+ end
306
+
307
+ def select s, p, o
308
+ template = Template.new(s, p, o).import_into self
309
+ matching = alpha_top.wmes.select { |wme| wme =~ template }
310
+ if block_given?
311
+ matching.each { |st| yield st.subject, st.predicate, st.object }
312
+ end
313
+ matching
314
+ end
315
+
316
+ def find s, p, o
317
+ template = Template.new(s, p, o).import_into self
318
+ source = best_alpha(template)
319
+ # puts "looking for #{template} among #{source.wmes.size} triples of #{source.template}"
320
+ source.wmes.detect { |wme| wme =~ template }
321
+ end
322
+
323
+ protected
324
+
325
+ def in_snapshot
326
+ @in_snapshot = true
327
+ yield
328
+ ensure
329
+ @in_snapshot = false
330
+ end
331
+
332
+ def generate_rule_name
333
+ "rule_#{productions.length}"
334
+ end
335
+
336
+ def lookup s, p, o
337
+ key = Template.hash_for(s, p, o)
338
+ # puts "Lookup for #{key}"
339
+ self.alpha_hash[ key ]
340
+ end
341
+
342
+ def alpha_activate alpha, wme
343
+ alpha.activate(wme) if alpha
344
+ end
345
+
346
+ def more_generic_alpha template
347
+ return alpha_top # OPTIMISE => temporary; may use later or not use at all
348
+ return alpha_top if template.root?
349
+ more_generic_templates(template).reduce alpha_top do |best, template|
350
+ alpha = alpha_hash[template.hash]
351
+ if alpha && alpha.wmes.size < best.wmes.size
352
+ alpha
353
+ else
354
+ best
355
+ end
356
+ end
357
+ end
358
+
359
+ def more_generic_templates template
360
+ set = []
361
+ set << template.with_subject( :_ ) unless template.subject == :_
362
+ set << template.with_predicate( :_ ) unless template.predicate == :_
363
+ set << template.with_object( :_ ) unless template.object == :_
364
+ set.select { |item| not item.root? }
365
+ end
366
+
367
+ def best_alpha template
368
+ raise
369
+ candidates = alpha_hash.values.select do |alpha|
370
+ template =~ alpha.template
371
+ end
372
+ result = candidates.inject do |best, alpha|
373
+ if best.nil?
374
+ alpha
375
+ elsif alpha.wmes.length < best.wmes.length
376
+ alpha
377
+ else
378
+ best
379
+ end
380
+ end
381
+ puts "Best alpha for #{template} is #{result}"
382
+ result
383
+ end
384
+
385
+ def real_add_production root, conditions, parameters, actions, alpha_deaf
386
+ beta = root.network conditions, [], parameters, alpha_deaf
387
+
388
+ production = ProductionNode.new( beta, actions )
389
+ production.update_above
390
+ production
391
+ end
392
+
393
+ def delete_node_with_ancestors node
394
+
395
+ if node.kind_of?( NccNode )
396
+ delete_node_with_ancestors node.partner
397
+ end
398
+
399
+ if [BetaMemory, NegNode, NccNode, NccPartner].any? { | klass| node.kind_of? klass }
400
+ while node.tokens.first
401
+ node.tokens.first.delete
402
+ end
403
+ end
404
+
405
+ if node.parent
406
+ node.parent.children.delete node
407
+ if node.parent.children.empty?
408
+ delete_node_with_ancestors(node.parent)
409
+ end
410
+ end
411
+
412
+ end
413
+
414
+ end
415
+
416
+ end