poe-css 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +186 -0
- data/.ruby-version +1 -0
- data/CONTRIBUTING.md +85 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +105 -0
- data/LICENSE.txt +22 -0
- data/README.md +275 -0
- data/Rakefile +10 -0
- data/bin/poe-css +14 -0
- data/demo/Gemfile +11 -0
- data/demo/Gemfile.lock +45 -0
- data/demo/app.rb +21 -0
- data/demo/views/index.haml +105 -0
- data/lib/dependencies.rb +11 -0
- data/lib/poe-css.rb +253 -0
- data/lib/poe-css/generator.rb +108 -0
- data/lib/poe-css/parser.rb +177 -0
- data/lib/poe-css/preprocessor.rb +186 -0
- data/lib/poe-css/simplifier.rb +201 -0
- data/lib/poe-css/version.rb +5 -0
- data/poe-css.gemspec +32 -0
- data/tests/poecss/all-clauses-1.output +23 -0
- data/tests/poecss/all-clauses-1.poecss +24 -0
- data/tests/poecss/alternation-1.output +8 -0
- data/tests/poecss/alternation-1.poecss +4 -0
- data/tests/poecss/nesting-1.output +8 -0
- data/tests/poecss/nesting-1.poecss +8 -0
- data/tests/poecss/nesting-2.output +30 -0
- data/tests/poecss/nesting-2.poecss +23 -0
- data/tests/poecss/nesting-3.output +15 -0
- data/tests/poecss/nesting-3.poecss +15 -0
- data/tests/poecss/nesting-4.output +14 -0
- data/tests/poecss/nesting-4.poecss +13 -0
- data/tests/poecss/performance-1.output +3 -0
- data/tests/poecss/performance-1.poecss +59 -0
- data/tests/poecss/performance-2.output +60 -0
- data/tests/poecss/performance-2.poecss +75 -0
- data/tests/poecss/rule-ordering-1.output +25 -0
- data/tests/poecss/rule-ordering-1.poecss +30 -0
- data/tests/poecss/rule-ordering-2.output +3 -0
- data/tests/poecss/rule-ordering-2.poecss +29 -0
- data/tests/poecss/rule-ordering-3.output +30 -0
- data/tests/poecss/rule-ordering-3.poecss +29 -0
- data/tests/poecss/rule-ordering-4.output +20 -0
- data/tests/poecss/rule-ordering-4.poecss +30 -0
- data/tests/poecss/rule-ordering-5.output +8 -0
- data/tests/poecss/rule-ordering-5.poecss +9 -0
- data/tests/poecss/rule-ordering-6.output +7 -0
- data/tests/poecss/rule-ordering-6.poecss +14 -0
- data/tests/poecss/simple-1.output +7 -0
- data/tests/poecss/simple-1.poecss +8 -0
- data/tests/poecss/simplification-1.output +6 -0
- data/tests/poecss/simplification-1.poecss +7 -0
- data/tests/poecss/simplification-2.output +6 -0
- data/tests/poecss/simplification-2.poecss +8 -0
- data/tests/poecss/simplification-3.output +2 -0
- data/tests/poecss/simplification-3.poecss +3 -0
- data/tests/poecss/simplification-4.output +3 -0
- data/tests/poecss/simplification-4.poecss +4 -0
- data/tests/poecss/simplification-5.output +3 -0
- data/tests/poecss/simplification-5.poecss +8 -0
- data/tests/poecss/simplification-6.output +2 -0
- data/tests/poecss/simplification-6.poecss +4 -0
- data/tests/preprocessor/arity-mismatch-1.error.preprocessor +4 -0
- data/tests/preprocessor/arity-mismatch-2.error.preprocessor +4 -0
- data/tests/preprocessor/complex.output +20 -0
- data/tests/preprocessor/complex.preprocessor +19 -0
- data/tests/preprocessor/empty.output +0 -0
- data/tests/preprocessor/empty.preprocessor +0 -0
- data/tests/preprocessor/macro-newlines.output +5 -0
- data/tests/preprocessor/macro-newlines.preprocessor +9 -0
- data/tests/preprocessor/macro.output +3 -0
- data/tests/preprocessor/macro.preprocessor +7 -0
- data/tests/preprocessor/missing-identifier-1.error.preprocessor +1 -0
- data/tests/preprocessor/missing-identifier-2.error.preprocessor +1 -0
- data/tests/preprocessor/normal-newlines.output +2 -0
- data/tests/preprocessor/normal-newlines.preprocessor +2 -0
- data/tests/preprocessor/parse-error-1.error.preprocessor +1 -0
- data/tests/preprocessor/parse-error-2.error.preprocessor +1 -0
- data/tests/preprocessor/parse-error-3.error.preprocessor +4 -0
- data/tests/preprocessor/simple.output +1 -0
- data/tests/preprocessor/simple.preprocessor +3 -0
- data/tests/run-all-tests.rb +60 -0
- data/tests/unit/simplify-bounds-test.rb +57 -0
- data/tests/unit/stricter-query-test.rb +24 -0
- metadata +292 -0
data/Rakefile
ADDED
data/bin/poe-css
ADDED
data/demo/Gemfile
ADDED
data/demo/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
ffi (1.9.18)
|
5
|
+
haml (5.0.3)
|
6
|
+
temple (>= 0.8.0)
|
7
|
+
tilt
|
8
|
+
listen (3.1.5)
|
9
|
+
rb-fsevent (~> 0.9, >= 0.9.4)
|
10
|
+
rb-inotify (~> 0.9, >= 0.9.7)
|
11
|
+
ruby_dep (~> 1.2)
|
12
|
+
parslet (1.8.1)
|
13
|
+
rack (1.6.8)
|
14
|
+
rack-protection (1.5.3)
|
15
|
+
rack
|
16
|
+
rb-fsevent (0.10.2)
|
17
|
+
rb-inotify (0.9.10)
|
18
|
+
ffi (>= 0.5.0, < 2)
|
19
|
+
rerun (0.11.0)
|
20
|
+
listen (~> 3.0)
|
21
|
+
ruby_dep (1.5.0)
|
22
|
+
sinatra (1.4.8)
|
23
|
+
rack (~> 1.5)
|
24
|
+
rack-protection (~> 1.4)
|
25
|
+
tilt (>= 1.3, < 3)
|
26
|
+
sinatra-param (1.4.0)
|
27
|
+
sinatra (~> 1.3)
|
28
|
+
temple (0.8.0)
|
29
|
+
tilt (2.0.8)
|
30
|
+
|
31
|
+
PLATFORMS
|
32
|
+
ruby
|
33
|
+
|
34
|
+
DEPENDENCIES
|
35
|
+
haml
|
36
|
+
parslet
|
37
|
+
rerun
|
38
|
+
sinatra (~> 1.3)
|
39
|
+
sinatra-param
|
40
|
+
|
41
|
+
RUBY VERSION
|
42
|
+
ruby 2.4.2p198
|
43
|
+
|
44
|
+
BUNDLED WITH
|
45
|
+
1.15.3
|
data/demo/app.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'haml'
|
4
|
+
require 'sinatra'
|
5
|
+
require 'sinatra/param'
|
6
|
+
|
7
|
+
require_relative '../../lib/dependencies'
|
8
|
+
|
9
|
+
get '/compile' do
|
10
|
+
param :input, String, required: true
|
11
|
+
|
12
|
+
parsed_clauses = POECSS::Parsing.parse_input(params[:input])
|
13
|
+
result = POECSS::Generator.generate_poe_rules(parsed_clauses)
|
14
|
+
|
15
|
+
content_type 'text/plain'
|
16
|
+
body result
|
17
|
+
end
|
18
|
+
|
19
|
+
get '/' do
|
20
|
+
haml :index
|
21
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
:css
|
2
|
+
* {
|
3
|
+
margin: 0;
|
4
|
+
padding: 0;
|
5
|
+
}
|
6
|
+
|
7
|
+
.u-flexCenter {
|
8
|
+
display: flex;
|
9
|
+
width: 100%;
|
10
|
+
align-items: top;
|
11
|
+
justify-content: center;
|
12
|
+
}
|
13
|
+
|
14
|
+
.u-maxWidth {
|
15
|
+
width: 100%;
|
16
|
+
}
|
17
|
+
|
18
|
+
body {
|
19
|
+
padding: 0.5em;
|
20
|
+
}
|
21
|
+
|
22
|
+
.pane {
|
23
|
+
padding: 0.5em;
|
24
|
+
}
|
25
|
+
|
26
|
+
.output {
|
27
|
+
white-space: pre;
|
28
|
+
font-family: monospace;
|
29
|
+
}
|
30
|
+
|
31
|
+
h1 {
|
32
|
+
margin-bottom: 0.25em;
|
33
|
+
}
|
34
|
+
|
35
|
+
.u-flexCenter
|
36
|
+
.pane.u-maxWidth
|
37
|
+
%h1 Input
|
38
|
+
%textarea#input.u-maxWidth(style="height: 50vh")
|
39
|
+
:plain
|
40
|
+
@black: RGB(0, 0, 0)
|
41
|
+
@white: RGB(255, 255, 255)
|
42
|
+
|
43
|
+
@t1-gem-text-color: RGB(30, 200, 200)
|
44
|
+
@t1-gem-border-color: RGB(30, 150, 180)
|
45
|
+
@t1-gem-bg-color: @white
|
46
|
+
|
47
|
+
@volume: 300
|
48
|
+
@t1-drop-sound: 6 @volume
|
49
|
+
@unique-drop-sound: 3 @volume
|
50
|
+
@value-drop-sound: 2 @volume
|
51
|
+
|
52
|
+
@gem-styling() {
|
53
|
+
SetTextColor @t1-gem-text-color
|
54
|
+
SetBorderColor @t1-gem-border-color
|
55
|
+
}
|
56
|
+
|
57
|
+
Class Gems {
|
58
|
+
Hide
|
59
|
+
SetFontSize 36
|
60
|
+
SetBorderColor @black
|
61
|
+
|
62
|
+
Quality >= 1 {
|
63
|
+
Show
|
64
|
+
SetFontSize 40
|
65
|
+
SetBorderColor @t1-gem-border-color
|
66
|
+
}
|
67
|
+
|
68
|
+
BaseType "Detonate Mines" "Added Chaos Damage" "Vaal" "Enhance" | Quality >= 14 {
|
69
|
+
Show
|
70
|
+
SetFontSize 40
|
71
|
+
@gem-styling()
|
72
|
+
PlayAlertSound @value-drop-sound
|
73
|
+
}
|
74
|
+
|
75
|
+
BaseType "Portal" "Empower" "Enlighten" "Vaal Haste" "Vaal Grace" "Item Quantity" "Vaal Breach" {
|
76
|
+
Show
|
77
|
+
SetFontSize 45
|
78
|
+
@gem-styling()
|
79
|
+
PlayAlertSound @unique-drop-sound
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
.pane.u-maxWidth
|
84
|
+
%h1 Output
|
85
|
+
.output#output
|
86
|
+
|
87
|
+
:javascript
|
88
|
+
var inputNode = document.getElementById('input');
|
89
|
+
var outputNode = document.getElementById('output');
|
90
|
+
|
91
|
+
function compile() {
|
92
|
+
var input = inputNode.value;
|
93
|
+
|
94
|
+
fetch('/compile?input=' + encodeURIComponent(input))
|
95
|
+
.then(result => {
|
96
|
+
if (result.ok) {
|
97
|
+
result.text().then(o => { outputNode.innerHTML = o; })
|
98
|
+
} else {
|
99
|
+
outputNode.innerHTML = 'Parse Error';
|
100
|
+
}
|
101
|
+
});
|
102
|
+
};
|
103
|
+
|
104
|
+
inputNode.addEventListener('keyup', compile);
|
105
|
+
compile();
|
data/lib/dependencies.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'parslet'
|
4
|
+
require 'pp'
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
require_relative 'poe-css'
|
8
|
+
require_relative 'poe-css/generator'
|
9
|
+
require_relative 'poe-css/parser'
|
10
|
+
require_relative 'poe-css/preprocessor'
|
11
|
+
require_relative 'poe-css/simplifier'
|
data/lib/poe-css.rb
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module POECSS
|
4
|
+
Clause = Struct.new(:match_alternations, :inner_clauses)
|
5
|
+
MatchClause = Struct.new(:match_key, :match_arguments)
|
6
|
+
CommandClause = Struct.new(:command_key, :command_argument)
|
7
|
+
RGBColorSpec = Struct.new(:r, :g, :b, :a)
|
8
|
+
SimpleClause = Struct.new(:match_clauses, :command_clauses)
|
9
|
+
|
10
|
+
class ParseError < StandardError
|
11
|
+
def initialize(source, parse_trace)
|
12
|
+
program_input = parse_trace.pos.instance_variable_get(:@string)
|
13
|
+
deepest_error = deepest_error(parse_trace)
|
14
|
+
|
15
|
+
line_no, col_no = deepest_error.source.line_and_column(deepest_error.pos)
|
16
|
+
|
17
|
+
program_lines = program_input.split("\n")
|
18
|
+
|
19
|
+
line = program_lines[line_no - 1]
|
20
|
+
|
21
|
+
super([
|
22
|
+
"Parse error in #{source} step:",
|
23
|
+
'',
|
24
|
+
program_lines[line_no - 2],
|
25
|
+
line,
|
26
|
+
(' ' * (col_no - 1)) + '^'
|
27
|
+
].compact.join("\n"))
|
28
|
+
end
|
29
|
+
|
30
|
+
def deepest_error(node)
|
31
|
+
queue = [ [ node, 0 ] ]
|
32
|
+
deepest_error = nil
|
33
|
+
|
34
|
+
while !queue.empty?
|
35
|
+
node, depth = queue.shift
|
36
|
+
|
37
|
+
if node.children.empty?
|
38
|
+
if deepest_error.nil? || deepest_error.last < depth
|
39
|
+
deepest_error = [ node, depth ]
|
40
|
+
end
|
41
|
+
else
|
42
|
+
node.children.each do |c|
|
43
|
+
queue << [ c, depth + 1 ]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
deepest_error.first
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class SimpleClause
|
53
|
+
def inspect
|
54
|
+
matches = 'Match (' + match_clauses.map { |s| [ s.match_key, s.match_arguments.inspect ].compact.join(' ') }.join(' ') + ')'
|
55
|
+
commands = '{ ' + command_clauses.map { |c| [ c.command_key, c.command_argument ].compact.join(' ') }.join(', ') + ' }'
|
56
|
+
'<' + [ matches, commands ].join(' ') + '>'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class RGBColorSpec
|
61
|
+
def inspect
|
62
|
+
"RGBA(#{[ r, g, b, a ].map(&:inspect).join(', ')})"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class << self
|
67
|
+
def compile(input)
|
68
|
+
# Strip comments, which is anything after a # in any line. No
|
69
|
+
# preprocessor or POE CSS syntax uses the #, and it doesn't appear in any
|
70
|
+
# strings (it could, but no item has a # in it), so it's safe to
|
71
|
+
# disregard all syntax when removing comments.
|
72
|
+
input_without_comments = input.split("\n").map { |l| l.gsub(/#.*$/, '') }.join("\n")
|
73
|
+
|
74
|
+
# Use the preprocessor to expand constants and macros.
|
75
|
+
preprocessed_program = Preprocessor.compile(input_without_comments)
|
76
|
+
|
77
|
+
# Hand the contents over to the main parser, which returns a list of Clauses.
|
78
|
+
parsed_clauses = Parser.parse_input(preprocessed_program)
|
79
|
+
|
80
|
+
# These Clauses, each of which can contain alternations (ORs) or nested
|
81
|
+
# clauses, get transformed into SimpleClauses. A SimpleClause is just a
|
82
|
+
# list of ANDed match clauses and a list of commands. The nesting and
|
83
|
+
# alternations have been expanded. We also simplify clauses here to
|
84
|
+
# minimize inputs into the next step as well as to collapse/minimize
|
85
|
+
# duplicated match and command keys for speed/simplicity.
|
86
|
+
expanded_clauses = expand_clauses(parsed_clauses).map { |r| Simplifier.simplify_clause(r) }.compact
|
87
|
+
|
88
|
+
# SimpleClauses are then converted into a list of rules in else-if form,
|
89
|
+
# the logical flow style used by PoE. The datatype is still SimpleClause,
|
90
|
+
# but the clauses have all been transformed and expanded to meet the
|
91
|
+
# else-if form (I call them rules to distinguish them conceptually from
|
92
|
+
# clauses).
|
93
|
+
else_ifed_rules = clauses_to_else_if_rules(expanded_clauses)
|
94
|
+
|
95
|
+
# Finally, rules that will never be hit or contribute nothing to to final
|
96
|
+
# result are deleted. This does not remove every useless clause but is
|
97
|
+
# likely good enough to suffice.
|
98
|
+
rules_without_dead_rules = rules_without_dead_rules(else_ifed_rules)
|
99
|
+
|
100
|
+
# Finally, we hand the results to the generator, which outputs them in
|
101
|
+
# PoE item filter format.
|
102
|
+
Generator.generate_poe_rules(rules_without_dead_rules)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def expand_clauses(clauses)
|
108
|
+
result_clauses = []
|
109
|
+
|
110
|
+
clauses.each do |c|
|
111
|
+
explore_clause(result_clauses, c, SimpleClause.new([], []))
|
112
|
+
end
|
113
|
+
|
114
|
+
result_clauses
|
115
|
+
end
|
116
|
+
|
117
|
+
def explore_clause(result_clauses, clause, clause_context)
|
118
|
+
commands, nested_clauses = clause.inner_clauses.partition { |c| c.is_a?(CommandClause) }
|
119
|
+
|
120
|
+
clause.match_alternations.each do |match_clauses|
|
121
|
+
simple_clause = SimpleClause.new((clause_context.match_clauses + match_clauses).to_set, clause_context.command_clauses + commands)
|
122
|
+
result_clauses << simple_clause
|
123
|
+
|
124
|
+
nested_clauses.each do |n|
|
125
|
+
explore_clause(result_clauses, n, simple_clause)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def rules_without_dead_rules(input_rules)
|
131
|
+
rules = input_rules
|
132
|
+
|
133
|
+
output_rules = []
|
134
|
+
|
135
|
+
loop do
|
136
|
+
output_rules = []
|
137
|
+
|
138
|
+
rules.each do |r|
|
139
|
+
if output_rules.last&.command_clauses == r.command_clauses && implies(output_rules.last, r)
|
140
|
+
output_rules[-1] = r
|
141
|
+
else
|
142
|
+
output_rules << r
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
break if rules.length == output_rules.length
|
147
|
+
|
148
|
+
rules = output_rules
|
149
|
+
end
|
150
|
+
|
151
|
+
output_rules
|
152
|
+
end
|
153
|
+
|
154
|
+
# Return whether or not clause A being true implies that clause B must be true as well.
|
155
|
+
def implies(clause_a, clause_b)
|
156
|
+
simplified_clause_a = Simplifier.simplify_clause(clause_a)
|
157
|
+
simplified_clause_b = Simplifier.simplify_clause(clause_b)
|
158
|
+
|
159
|
+
return false if simplified_clause_a.nil? || simplified_clause_b.nil?
|
160
|
+
|
161
|
+
clause_a_matches = simplified_clause_a.match_clauses.group_by(&:match_key)
|
162
|
+
clause_b_matches = simplified_clause_b.match_clauses.group_by(&:match_key)
|
163
|
+
|
164
|
+
# If B has any match keys that aren't part of A, there's no way A can imply B.
|
165
|
+
return false unless clause_b_matches.keys.to_set.subset?(clause_a_matches.keys.to_set)
|
166
|
+
|
167
|
+
shared_keys = clause_a_matches.keys & clause_b_matches.keys
|
168
|
+
|
169
|
+
shared_keys.all? { |k|
|
170
|
+
a_matches = clause_a_matches[k]
|
171
|
+
b_matches = clause_b_matches[k]
|
172
|
+
|
173
|
+
# If the simplifier simplifies A && B to just A, it means that B was
|
174
|
+
# superfluous to the comparison. Specifically, A && B = A, which means
|
175
|
+
# that when A is true, B must be true. Therefore, A implies B.
|
176
|
+
joined_clause = Simplifier.simplify_clause(SimpleClause.new(a_matches + b_matches, []))
|
177
|
+
joined_clause && joined_clause.match_clauses == a_matches
|
178
|
+
}
|
179
|
+
end
|
180
|
+
|
181
|
+
def clauses_to_else_if_rules(clauses)
|
182
|
+
output_rules = [ SimpleClause.new([], []) ]
|
183
|
+
|
184
|
+
# When encountering a pair of duplicates, this eliminates the earlier one in the array, since later ones have more weight.
|
185
|
+
unique_clauses = clauses.reverse.uniq.reverse
|
186
|
+
|
187
|
+
# This is a modified power set algorithm adapted from the one viewable
|
188
|
+
# here (https://github.com/sagivo/powerset/blob/master/powerset.rb).
|
189
|
+
# This algorithm has the important property that the order of elements in
|
190
|
+
# each sublist is the same as the order of elements in the original list,
|
191
|
+
# which is crucial for maintaining the priority ordering of clauses.
|
192
|
+
# Secondly, each sublist is actually the clause concatenation (ANDed) of
|
193
|
+
# all the clauses that would be in the sublist, and impossible clauses
|
194
|
+
# are ignored. Finally, clauses that are generated are unique, but if
|
195
|
+
# there is a duplicate, the earlier one is deleted because the later one
|
196
|
+
# has higher precedence. We don't want to suppress the later one because
|
197
|
+
# it would keep the weaker precedence, and we don't want to keep it
|
198
|
+
# because we want to minimize the size of the output list. Powerset is a
|
199
|
+
# 2^N operation, so we want to use deduplication and simplification
|
200
|
+
# whenever possible to reduce the search space.
|
201
|
+
#
|
202
|
+
# Why do this at all? Well, the way to convert a list of clauses, any
|
203
|
+
# subset of which may match a given item, to a list of specific else-if
|
204
|
+
# rules is to consider the power set of the clauses, which form every
|
205
|
+
# possible case that an item may fall into. You can then turn each
|
206
|
+
# sublist into a single ANDed clause and sort the result by specificity
|
207
|
+
# from most to least specific (so that the more specific rules are closer
|
208
|
+
# to the top, whereas the reverse would suppress the rule). This is an
|
209
|
+
# easy algorithm, but has the disadvantage of generating all 2^N
|
210
|
+
# combinations before applying the observations above to minimize the
|
211
|
+
# size of the result set. For a set of 24 clauses, that's already 16
|
212
|
+
# million combinations. That's why we mix the filtering in during the
|
213
|
+
# actual powerset algorithm.
|
214
|
+
|
215
|
+
unique_clauses.each do |clause|
|
216
|
+
current_length = output_rules.length
|
217
|
+
|
218
|
+
(0...current_length).each do |i|
|
219
|
+
rule = output_rules[i]
|
220
|
+
|
221
|
+
concatenated_clause = Simplifier.simplify_clause(
|
222
|
+
SimpleClause.new(
|
223
|
+
rule.match_clauses + clause.match_clauses,
|
224
|
+
rule.command_clauses + clause.command_clauses
|
225
|
+
)
|
226
|
+
)
|
227
|
+
|
228
|
+
if concatenated_clause
|
229
|
+
output_rules.delete(concatenated_clause)
|
230
|
+
output_rules << concatenated_clause
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Our list is now sorted in specificity order from least to most
|
236
|
+
# specific. We reverse the list and then filter out rules which would be
|
237
|
+
# subsumed by rules earlier in the output list. These rules are useless
|
238
|
+
# since they would be caught by the existing rule.
|
239
|
+
|
240
|
+
rules = output_rules.drop(1) # Removes the original empty sublist.
|
241
|
+
|
242
|
+
final_output = []
|
243
|
+
|
244
|
+
rules.reverse.each do |r|
|
245
|
+
if final_output.none? { |existing_rule| implies(r, existing_rule) }
|
246
|
+
final_output << r
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
final_output
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|