poe-css 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.
- 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
|