sequitur 0.0.14 → 0.1.00
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 +8 -8
- data/CHANGELOG.md +4 -0
- data/README.md +18 -9
- data/lib/sequitur/constants.rb +1 -1
- data/lib/sequitur/dynamic_grammar.rb +28 -3
- data/lib/sequitur/formatter/base_text.rb +74 -0
- data/lib/sequitur/formatter/debug.rb +86 -0
- data/lib/sequitur/grammar_visitor.rb +81 -0
- data/lib/sequitur/production.rb +25 -10
- data/lib/sequitur/production_ref.rb +6 -0
- data/lib/sequitur/sequitur_grammar.rb +2 -2
- data/lib/sequitur.rb +2 -0
- data/spec/sequitur/digram_spec.rb +1 -0
- data/spec/sequitur/dynamic_grammar_spec.rb +58 -16
- data/spec/sequitur/formatter/base_text_spec.rb +82 -0
- data/spec/sequitur/formatter/debug_spec.rb +114 -0
- data/spec/sequitur/grammar_visitor_spec.rb +98 -0
- data/spec/sequitur/production_ref_spec.rb +11 -1
- data/spec/sequitur/production_spec.rb +39 -0
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZDAyNmJiYjE4YTQzNWMxZDI0ZWU2NTJhMGZiMTBiMmI0ZDg5MTBkOA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MzI2YzIxMmZkYjExYTk1Yjc4ZTk0MDg0ZjczYjUxNDI4YWNkMWMxZg==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
NzEwYTk3ZGFhOGU5ZDZkMWRlMWVmZWU5MDQxOTQ0YTk5ZDIzMGM4YjY1ODAw
|
10
|
+
NjQyYmU0ODEwZTRhYzM4ZjVmMWQ3ZDgyNjdiNDkzNjMyZmExZjFmN2Q0MTk3
|
11
|
+
ODE4NTQwYjBiZGQ5M2QyY2JlZTczMzIzMWI0YjUwYzVmNzkyNjg=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MTYxOWNiMjg5MzRhY2JlOTdmYjJjZmI0YjVkYzA1MmIxMmY5NjQ4NDY2Zjc1
|
14
|
+
NWJlY2MyY2Q4ZWE2OTFmZGFiZWU1YjZkMjNmMzYzY2ZmYmM2MTE3MWVjODNj
|
15
|
+
ZDBlNzA3M2VjNmI3NTJhNTk5NGY0OGY4ODFlOGJhZDY1MDMyZmU=
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -5,6 +5,7 @@ _Ruby gem implementing the Sequitur algorithm_
|
|
5
5
|
|
6
6
|
[](https://travis-ci.org/famished-tiger/Sequitur)
|
7
7
|
[](http://badge.fury.io/rb/sequitur)
|
8
|
+
[](https://gemnasium.com/famished-tiger/Sequitur)
|
8
9
|
|
9
10
|
|
10
11
|
### What is the Sequitur algorithm? ###
|
@@ -19,22 +20,30 @@ It detects repeated token patterns and can represent them in a compact way.
|
|
19
20
|
|
20
21
|
```ruby
|
21
22
|
|
22
|
-
require 'sequitur'
|
23
|
-
|
24
|
-
input_sequence = '
|
23
|
+
require 'sequitur' # Load the Sequitur library
|
24
|
+
|
25
|
+
input_sequence = 'abcabdabcabd' # Let's analyze this string
|
25
26
|
|
26
27
|
# The SEQUITUR algorithm will detect the repeated 'ab' pattern
|
27
28
|
# and will generate a context-free grammar that represents the input string
|
28
29
|
grammar = Sequitur.build_from(input_sequence)
|
29
30
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
|
31
|
+
# To display the grammar rules on the console output
|
32
|
+
# We use a formatter
|
33
|
+
formatter = Sequitur::Formatter::BaseText.new(STDOUT)
|
34
|
+
|
35
|
+
# Now render the rules. Each rule is displayed with the format:
|
36
|
+
# rule_id : a_sequence_grammar_symbols.
|
33
37
|
# Where:
|
34
|
-
# - rule_id is
|
38
|
+
# - rule_id is either 'start' or a name like 'P_xxxx' (xxxx is a sequential number)
|
35
39
|
# - a grammar symbol is either a terminal symbol
|
36
|
-
# (i.e. a character from the input) or
|
37
|
-
|
40
|
+
# (i.e. a character from the input) or a rule id
|
41
|
+
formatter.render(grammar.visitor)
|
42
|
+
|
43
|
+
# Rendered output is:
|
44
|
+
# start : P_2 P_2.
|
45
|
+
# P_1 : a b.
|
46
|
+
# P_2 : P_1 c P_1 d.
|
38
47
|
```
|
39
48
|
|
40
49
|
### TODO: Add more documentation ###
|
data/lib/sequitur/constants.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'production'
|
2
|
+
require_relative 'grammar_visitor'
|
2
3
|
|
3
4
|
module Sequitur # Module for classes implementing the Sequitur algorithm
|
4
5
|
|
@@ -8,7 +9,7 @@ class DynamicGrammar
|
|
8
9
|
|
9
10
|
# The set of production rules of the grammar
|
10
11
|
attr_reader(:productions)
|
11
|
-
|
12
|
+
|
12
13
|
# nodoc Trace the execution of the algorithm.
|
13
14
|
attr(:trace)
|
14
15
|
|
@@ -31,7 +32,7 @@ class DynamicGrammar
|
|
31
32
|
end
|
32
33
|
|
33
34
|
|
34
|
-
# Add a production to the grammar.
|
35
|
+
# Add a given production to the grammar.
|
35
36
|
def add_production(aProduction)
|
36
37
|
# TODO: remove output
|
37
38
|
puts "Adding #{aProduction.object_id}" if trace
|
@@ -42,7 +43,7 @@ class DynamicGrammar
|
|
42
43
|
|
43
44
|
|
44
45
|
# Remove a production from the grammar
|
45
|
-
def
|
46
|
+
def remove_production(anIndex)
|
46
47
|
puts "Before production removal #{productions[anIndex].object_id}" if trace
|
47
48
|
puts to_string if trace
|
48
49
|
prod = productions.delete_at(anIndex)
|
@@ -59,6 +60,19 @@ class DynamicGrammar
|
|
59
60
|
append_symbol_to(root, aToken)
|
60
61
|
end
|
61
62
|
|
63
|
+
# Part of the 'visitee' role.
|
64
|
+
# [aVisitor] a GrammarVisitor instance
|
65
|
+
def accept(aVisitor)
|
66
|
+
aVisitor.start_visit_grammar(self)
|
67
|
+
productions.each { |prod| prod.accept(aVisitor) }
|
68
|
+
aVisitor.end_visit_grammar(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Factory method. Returns a visitor for this grammar.
|
72
|
+
def visitor()
|
73
|
+
return GrammarVisitor.new(self)
|
74
|
+
end
|
75
|
+
|
62
76
|
protected
|
63
77
|
|
64
78
|
def append_symbol_to(aProduction, aSymbol)
|
@@ -80,6 +94,17 @@ class DynamicGrammar
|
|
80
94
|
end
|
81
95
|
end
|
82
96
|
|
97
|
+
|
98
|
+
# Visitor pattern.
|
99
|
+
# A visitee is expected to accept the visit from a visitor object
|
100
|
+
def accept(aVisitor)
|
101
|
+
aVisitor.start_visit_grammar(self)
|
102
|
+
|
103
|
+
# Let's proceed with the visit of productions
|
104
|
+
productions.each { |a_prod| a_prod.accept(aVisitor) }
|
105
|
+
|
106
|
+
aVisitor.end_visit_grammar(self)
|
107
|
+
end
|
83
108
|
end # class
|
84
109
|
|
85
110
|
end # module
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Sequitur
|
2
|
+
module Formatter
|
3
|
+
|
4
|
+
# A formatter class that can render a dynamic grammar in plain text.
|
5
|
+
# Synopsis:
|
6
|
+
# some_grammar = ... # Points to a DynamicGrammar-like object
|
7
|
+
# Output the result to the standard console output
|
8
|
+
# formatter = Sequitur::Formatter::BaseText.new(STDOUT)
|
9
|
+
# Render the grammar (through a visitor)
|
10
|
+
# formatter.run(some_grammar.visitor)
|
11
|
+
class BaseText
|
12
|
+
attr(:output)
|
13
|
+
|
14
|
+
# Constructor.
|
15
|
+
# [anIO]
|
16
|
+
def initialize(anIO)
|
17
|
+
@output = anIO
|
18
|
+
@prod_lookup = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
public
|
22
|
+
|
23
|
+
def render(aVisitor)
|
24
|
+
aVisitor.subscribe(self)
|
25
|
+
aVisitor.start()
|
26
|
+
aVisitor.unsubscribe(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def before_grammar(aGrammar)
|
30
|
+
aGrammar.productions.each_with_index do |a_prod, index|
|
31
|
+
prod_lookup[a_prod] = index
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def before_production(aProduction)
|
36
|
+
p_name = prod_name(aProduction)
|
37
|
+
output.print p_name
|
38
|
+
end
|
39
|
+
|
40
|
+
def before_rhs(_)
|
41
|
+
output.print ' :'
|
42
|
+
end
|
43
|
+
|
44
|
+
def before_terminal(aSymbol)
|
45
|
+
output.print " #{aSymbol}"
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def before_non_terminal(aProduction)
|
50
|
+
p_name = prod_name(aProduction)
|
51
|
+
output.print " #{p_name}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def after_production(_)
|
55
|
+
output.print ".\n"
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def prod_lookup()
|
61
|
+
return @prod_lookup
|
62
|
+
end
|
63
|
+
|
64
|
+
def prod_name(aProduction)
|
65
|
+
prod_index = prod_lookup[aProduction]
|
66
|
+
name = (prod_index == 0) ? 'start' : "P_#{prod_index}"
|
67
|
+
return name
|
68
|
+
end
|
69
|
+
|
70
|
+
end # class
|
71
|
+
end # module
|
72
|
+
end # module
|
73
|
+
|
74
|
+
# End of file
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Sequitur
|
2
|
+
module Formatter
|
3
|
+
class Debug
|
4
|
+
attr(:indentation)
|
5
|
+
attr(:output)
|
6
|
+
|
7
|
+
# Constructor.
|
8
|
+
# [anIO]
|
9
|
+
def initialize(anIO)
|
10
|
+
@indentation = 0
|
11
|
+
@output = anIO
|
12
|
+
end
|
13
|
+
|
14
|
+
public
|
15
|
+
|
16
|
+
def render(aVisitor)
|
17
|
+
aVisitor.subscribe(self)
|
18
|
+
aVisitor.start()
|
19
|
+
aVisitor.unsubscribe(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def before_grammar(_)
|
23
|
+
output_event(__method__, indentation)
|
24
|
+
indent
|
25
|
+
end
|
26
|
+
|
27
|
+
def before_production(_)
|
28
|
+
output_event(__method__, indentation)
|
29
|
+
indent
|
30
|
+
end
|
31
|
+
|
32
|
+
def before_rhs(_)
|
33
|
+
output_event(__method__, indentation)
|
34
|
+
indent
|
35
|
+
end
|
36
|
+
|
37
|
+
def before_terminal(_)
|
38
|
+
output_event(__method__, indentation)
|
39
|
+
end
|
40
|
+
|
41
|
+
def after_terminal(_)
|
42
|
+
output_event(__method__, indentation)
|
43
|
+
end
|
44
|
+
|
45
|
+
def before_non_terminal(_)
|
46
|
+
output_event(__method__, indentation)
|
47
|
+
end
|
48
|
+
|
49
|
+
def after_non_terminal(_)
|
50
|
+
output_event(__method__, indentation)
|
51
|
+
end
|
52
|
+
|
53
|
+
def after_rhs(_)
|
54
|
+
dedent
|
55
|
+
output_event(__method__, indentation)
|
56
|
+
end
|
57
|
+
|
58
|
+
def after_production(_)
|
59
|
+
dedent
|
60
|
+
output_event(__method__, indentation)
|
61
|
+
end
|
62
|
+
|
63
|
+
def after_grammar(_)
|
64
|
+
dedent
|
65
|
+
output_event(__method__, indentation)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def indent()
|
71
|
+
@indentation += 1
|
72
|
+
end
|
73
|
+
|
74
|
+
def dedent()
|
75
|
+
@indentation -= 1
|
76
|
+
end
|
77
|
+
|
78
|
+
def output_event(anEvent, indentationLevel)
|
79
|
+
output.puts "#{' ' * 2 * indentationLevel}#{anEvent}"
|
80
|
+
end
|
81
|
+
|
82
|
+
end # class
|
83
|
+
end # module
|
84
|
+
end # module
|
85
|
+
|
86
|
+
# End of file
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Sequitur # Module for classes implementing the Sequitur algorithm
|
2
|
+
|
3
|
+
# A visitor class dedicated in the visit of Grammar.
|
4
|
+
|
5
|
+
class GrammarVisitor
|
6
|
+
attr_reader(:grammar)
|
7
|
+
|
8
|
+
attr_reader(:subscribers)
|
9
|
+
|
10
|
+
# Constructor.
|
11
|
+
# [aGrammar] a DynamicGrammar-like instance.
|
12
|
+
def initialize(aGrammar)
|
13
|
+
@grammar = aGrammar
|
14
|
+
@subscribers = []
|
15
|
+
end
|
16
|
+
|
17
|
+
public
|
18
|
+
|
19
|
+
# Add a subscriber to the list.
|
20
|
+
def subscribe(aSubscriber)
|
21
|
+
subscribers << aSubscriber
|
22
|
+
end
|
23
|
+
|
24
|
+
def unsubscribe(aSubscriber)
|
25
|
+
subscribers.delete_if { |entry| entry == aSubscriber}
|
26
|
+
end
|
27
|
+
|
28
|
+
# The signal to start the visit.
|
29
|
+
def start()
|
30
|
+
grammar.send(:accept, self)
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def start_visit_grammar(aGrammar)
|
35
|
+
broadcast(:before_grammar, aGrammar)
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def start_visit_production(aProduction)
|
40
|
+
broadcast(:before_production, aProduction)
|
41
|
+
broadcast(:before_rhs, aProduction.rhs)
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def visit_prod_ref(aProdRef)
|
46
|
+
production = aProdRef.production
|
47
|
+
broadcast(:before_non_terminal, production)
|
48
|
+
broadcast(:after_non_terminal, production)
|
49
|
+
end
|
50
|
+
|
51
|
+
def visit_terminal(aTerminal)
|
52
|
+
broadcast(:before_terminal, aTerminal)
|
53
|
+
broadcast(:after_terminal, aTerminal)
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def end_visit_production(aProduction)
|
58
|
+
broadcast(:after_rhs, aProduction.rhs)
|
59
|
+
broadcast(:after_production, aProduction)
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def end_visit_grammar(aGrammar)
|
65
|
+
broadcast(:after_grammar, aGrammar)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def broadcast(msg, *args)
|
70
|
+
subscribers.each do |a_subscriber|
|
71
|
+
next unless a_subscriber.respond_to?(msg)
|
72
|
+
a_subscriber.send(msg, *args)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
end # class
|
78
|
+
|
79
|
+
end # module
|
80
|
+
|
81
|
+
# End of file
|
data/lib/sequitur/production.rb
CHANGED
@@ -15,7 +15,7 @@ module Sequitur # Module for classes implementing the Sequitur algorithm
|
|
15
15
|
class Production
|
16
16
|
# The right-hand side (rhs) consists of a sequence of grammar symbols
|
17
17
|
attr_reader(:rhs)
|
18
|
-
|
18
|
+
|
19
19
|
# The reference count (= how times other productions reference this one)
|
20
20
|
attr_reader(:refcount)
|
21
21
|
|
@@ -59,7 +59,7 @@ class Production
|
|
59
59
|
end
|
60
60
|
|
61
61
|
|
62
|
-
# Return the set of
|
62
|
+
# Return the set of references to production appearing in the rhs.
|
63
63
|
def references()
|
64
64
|
return rhs.select { |symb| symb.is_a?(ProductionRef) }
|
65
65
|
end
|
@@ -71,8 +71,6 @@ class Production
|
|
71
71
|
end
|
72
72
|
|
73
73
|
|
74
|
-
|
75
|
-
|
76
74
|
# Return the list digrams found in rhs of this production.
|
77
75
|
def recalc_digrams()
|
78
76
|
return [] if rhs.size < 2
|
@@ -137,11 +135,11 @@ class Production
|
|
137
135
|
msg << to_string
|
138
136
|
fail StandardError, msg
|
139
137
|
end
|
140
|
-
new_symb = aSymbol.dup
|
138
|
+
new_symb = aSymbol.dup
|
141
139
|
else
|
142
140
|
new_symb = aSymbol
|
143
141
|
end
|
144
|
-
|
142
|
+
|
145
143
|
rhs << new_symb
|
146
144
|
digrams << Digram.new(rhs[-2], rhs[-1], self) if rhs.size >= 2
|
147
145
|
end
|
@@ -197,7 +195,7 @@ class Production
|
|
197
195
|
rhs[index1].unbind if rhs[index1].is_a?(ProductionRef)
|
198
196
|
rhs.delete_at(index1)
|
199
197
|
end
|
200
|
-
|
198
|
+
|
201
199
|
recalc_digrams
|
202
200
|
end
|
203
201
|
|
@@ -211,19 +209,36 @@ class Production
|
|
211
209
|
def replace_production(another)
|
212
210
|
(0...rhs.size).to_a.reverse.each do |index|
|
213
211
|
next unless rhs[index] == another
|
214
|
-
|
212
|
+
|
215
213
|
# Avoid the aliasing of production reference
|
216
|
-
other_rhs = another.rhs.map do |symb|
|
214
|
+
other_rhs = another.rhs.map do |symb|
|
217
215
|
symb.is_a?(ProductionRef) ? symb.dup : symb
|
218
216
|
end
|
219
217
|
rhs.insert(index + 1, *other_rhs)
|
220
218
|
another.decr_refcount
|
221
219
|
rhs.delete_at(index)
|
222
220
|
end
|
223
|
-
|
221
|
+
|
224
222
|
recalc_digrams
|
225
223
|
end
|
226
224
|
|
225
|
+
|
226
|
+
# Part of the 'visitee' role.
|
227
|
+
# [aVisitor] a GrammarVisitor instance
|
228
|
+
def accept(aVisitor)
|
229
|
+
aVisitor.start_visit_production(self)
|
230
|
+
|
231
|
+
rhs.each do |a_symb|
|
232
|
+
if a_symb.is_a?(ProductionRef)
|
233
|
+
a_symb.accept(aVisitor)
|
234
|
+
else
|
235
|
+
aVisitor.visit_terminal(a_symb)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
aVisitor.end_visit_production(self)
|
240
|
+
end
|
241
|
+
|
227
242
|
end # class
|
228
243
|
|
229
244
|
end # module
|
@@ -41,7 +41,7 @@ class SequiturGrammar < DynamicGrammar
|
|
41
41
|
end
|
42
42
|
|
43
43
|
# Remove a production from the grammar
|
44
|
-
def
|
44
|
+
def remove_production(anIndex)
|
45
45
|
prod = productions[anIndex]
|
46
46
|
|
47
47
|
# Retrieve in the Hash all registered digrams from the removed production
|
@@ -170,7 +170,7 @@ class SequiturGrammar < DynamicGrammar
|
|
170
170
|
!a_prod.references_of(curr_production).empty?
|
171
171
|
end
|
172
172
|
dependent.replace_production(productions[index])
|
173
|
-
|
173
|
+
remove_production(index)
|
174
174
|
update_digrams_from(dependent)
|
175
175
|
end
|
176
176
|
|
data/lib/sequitur.rb
CHANGED
@@ -40,7 +40,7 @@ describe DynamicGrammar do
|
|
40
40
|
subject.add_production(p_a)
|
41
41
|
expect(subject.productions.size).to eq(2)
|
42
42
|
expect(subject.productions.last).to eq(p_a)
|
43
|
-
|
43
|
+
|
44
44
|
# Error: p_b, p_c not in grammar
|
45
45
|
expect { add_production(p_bc) }.to raise_error(StandardError)
|
46
46
|
|
@@ -48,18 +48,18 @@ describe DynamicGrammar do
|
|
48
48
|
expect(subject.productions.size).to eq(3)
|
49
49
|
expect(subject.productions.last).to eq(p_b)
|
50
50
|
|
51
|
-
# Error: p_c not in grammar
|
51
|
+
# Error: p_c not in grammar
|
52
52
|
expect { add_production(p_bc) }.to raise_error(StandardError)
|
53
53
|
|
54
54
|
subject.add_production(p_c)
|
55
55
|
expect(subject.productions.size).to eq(4)
|
56
56
|
expect(subject.productions.last).to eq(p_c)
|
57
|
-
|
57
|
+
|
58
58
|
subject.add_production(p_bc)
|
59
59
|
expect(subject.productions.size).to eq(5)
|
60
|
-
expect(subject.productions.last).to eq(p_bc)
|
60
|
+
expect(subject.productions.last).to eq(p_bc)
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
it 'should complain when rhs refers to unknown production' do
|
64
64
|
subject.add_production(p_a)
|
65
65
|
subject.add_production(p_b)
|
@@ -67,11 +67,11 @@ describe DynamicGrammar do
|
|
67
67
|
msg = "Production #{p_bc.object_id} refers to production #{p_c.object_id}"
|
68
68
|
msg << ' that is not part of the grammar.'
|
69
69
|
expect { subject.add_production(p_bc) }.to raise_error(StandardError, msg)
|
70
|
-
|
70
|
+
|
71
71
|
end
|
72
72
|
end # context
|
73
73
|
|
74
|
-
|
74
|
+
|
75
75
|
context 'Removing a production from the grammar:' do
|
76
76
|
it 'should remove an existing production' do
|
77
77
|
subject.add_production(p_a) # index = 1
|
@@ -79,25 +79,67 @@ describe DynamicGrammar do
|
|
79
79
|
subject.add_production(p_c) # index = 3
|
80
80
|
subject.add_production(p_bc) # index = 4
|
81
81
|
expect(subject.productions.size).to eq(5)
|
82
|
-
|
82
|
+
|
83
83
|
expect(p_a.refcount).to eq(0)
|
84
84
|
expect(p_b.refcount).to eq(1)
|
85
85
|
expect(p_c.refcount).to eq(1)
|
86
|
-
|
87
|
-
subject.
|
86
|
+
|
87
|
+
subject.remove_production(1) # 1 => p_a
|
88
88
|
expect(subject.productions.size).to eq(4)
|
89
89
|
expect(p_b.refcount).to eq(1)
|
90
90
|
expect(p_c.refcount).to eq(1)
|
91
91
|
expect(subject.productions).not_to include(p_a)
|
92
|
-
|
93
|
-
subject.
|
94
|
-
|
92
|
+
|
93
|
+
subject.remove_production(3) # 3 => p_bc
|
94
|
+
|
95
95
|
expect(subject.productions.size).to eq(3)
|
96
96
|
expect(p_b.refcount).to eq(0)
|
97
|
-
expect(p_c.refcount).to eq(0)
|
98
|
-
expect(subject.productions).not_to include(p_bc)
|
97
|
+
expect(p_c.refcount).to eq(0)
|
98
|
+
expect(subject.productions).not_to include(p_bc)
|
99
|
+
end
|
100
|
+
|
101
|
+
end # context
|
102
|
+
|
103
|
+
context 'Visiting:' do
|
104
|
+
it 'should return a visitor' do
|
105
|
+
expect { subject.visitor }.not_to raise_error
|
106
|
+
expect(subject.visitor).to be_kind_of(GrammarVisitor)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should accept a visitor' do
|
110
|
+
subject.add_production(p_a) # index = 1
|
111
|
+
subject.add_production(p_b) # index = 2
|
112
|
+
subject.add_production(p_c) # index = 3
|
113
|
+
subject.add_production(p_bc) # index = 4
|
114
|
+
|
115
|
+
a_visitor = subject.visitor
|
116
|
+
fake_formatter = double('fake-formatter')
|
117
|
+
a_visitor.subscribe(fake_formatter)
|
118
|
+
|
119
|
+
expect(fake_formatter).to receive(:before_grammar).with(subject).ordered
|
120
|
+
expect(fake_formatter).to receive(:before_production).with(subject.root).ordered
|
121
|
+
expect(fake_formatter).to receive(:before_rhs).with([]).ordered
|
122
|
+
expect(fake_formatter).to receive(:after_rhs).with([]).ordered
|
123
|
+
expect(fake_formatter).to receive(:after_production).with(subject.root)
|
124
|
+
expect(fake_formatter).to receive(:before_production).with(p_a)
|
125
|
+
expect(fake_formatter).to receive(:before_rhs).with(p_a.rhs)
|
126
|
+
expect(fake_formatter).to receive(:after_rhs).with(p_a.rhs)
|
127
|
+
expect(fake_formatter).to receive(:after_production).with(p_a)
|
128
|
+
expect(fake_formatter).to receive(:before_production).with(p_b)
|
129
|
+
expect(fake_formatter).to receive(:before_rhs).with(p_b.rhs)
|
130
|
+
expect(fake_formatter).to receive(:after_rhs).with(p_b.rhs)
|
131
|
+
expect(fake_formatter).to receive(:after_production).with(p_b)
|
132
|
+
expect(fake_formatter).to receive(:before_production).with(p_c)
|
133
|
+
expect(fake_formatter).to receive(:before_rhs).with(p_c.rhs)
|
134
|
+
expect(fake_formatter).to receive(:after_rhs).with(p_c.rhs)
|
135
|
+
expect(fake_formatter).to receive(:after_production).with(p_c)
|
136
|
+
expect(fake_formatter).to receive(:before_production).with(p_bc)
|
137
|
+
expect(fake_formatter).to receive(:before_rhs).with(p_bc.rhs)
|
138
|
+
expect(fake_formatter).to receive(:after_rhs).with(p_bc.rhs)
|
139
|
+
expect(fake_formatter).to receive(:after_production).with(p_bc)
|
140
|
+
expect(fake_formatter).to receive(:after_grammar).with(subject)
|
141
|
+
subject.send(:accept, a_visitor)
|
99
142
|
end
|
100
|
-
|
101
143
|
end # context
|
102
144
|
|
103
145
|
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
require_relative '../../../lib/sequitur/dynamic_grammar'
|
5
|
+
|
6
|
+
# Load the class under test
|
7
|
+
require_relative '../../../lib/sequitur/formatter/base_text'
|
8
|
+
|
9
|
+
module Sequitur # Re-open the module to get rid of qualified names
|
10
|
+
module Formatter
|
11
|
+
|
12
|
+
describe BaseText do
|
13
|
+
# Factory method. Build a production with the given sequence
|
14
|
+
# of symbols as its rhs.
|
15
|
+
def build_production(*symbols)
|
16
|
+
prod = Production.new
|
17
|
+
symbols.each { |symb| prod.append_symbol(symb) }
|
18
|
+
return prod
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:p_a) { build_production(:a) }
|
22
|
+
let(:p_b) { build_production(:b) }
|
23
|
+
let(:p_c) { build_production(:c) }
|
24
|
+
let(:p_bc) { build_production(p_b, p_c) }
|
25
|
+
|
26
|
+
let(:empty_grammar) { DynamicGrammar.new }
|
27
|
+
let(:sample_grammar) do
|
28
|
+
grm = DynamicGrammar.new
|
29
|
+
grm.add_production(p_a)
|
30
|
+
grm.add_production(p_b)
|
31
|
+
grm.add_production(p_c)
|
32
|
+
grm.add_production(p_bc)
|
33
|
+
grm
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:destination) { StringIO.new('', 'w') }
|
37
|
+
|
38
|
+
context 'Standard creation & initialization:' do
|
39
|
+
|
40
|
+
it 'should be initialized with an IO argument' do
|
41
|
+
expect { BaseText.new(StringIO.new('', 'w')) }.not_to raise_error
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should know its output destination" do
|
45
|
+
instance = BaseText.new(destination)
|
46
|
+
expect(instance.output).to eq(destination)
|
47
|
+
end
|
48
|
+
end # context
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
context 'Formatting events:' do
|
53
|
+
it 'should support events of an empty grammar' do
|
54
|
+
instance = BaseText.new(destination)
|
55
|
+
a_visitor = empty_grammar.visitor
|
56
|
+
instance.render(a_visitor)
|
57
|
+
expectations =<<-SNIPPET
|
58
|
+
start :.
|
59
|
+
SNIPPET
|
60
|
+
expect(destination.string).to eq(expectations)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should support events of a non-empty grammar' do
|
64
|
+
instance = BaseText.new(destination)
|
65
|
+
a_visitor = sample_grammar.visitor
|
66
|
+
instance.render(a_visitor)
|
67
|
+
expectations =<<-SNIPPET
|
68
|
+
start :.
|
69
|
+
P_1 : a.
|
70
|
+
P_2 : b.
|
71
|
+
P_3 : c.
|
72
|
+
P_4 : P_2 P_3.
|
73
|
+
SNIPPET
|
74
|
+
expect(destination.string).to eq(expectations)
|
75
|
+
end
|
76
|
+
end # context
|
77
|
+
end # describe
|
78
|
+
|
79
|
+
end # module
|
80
|
+
end # module
|
81
|
+
|
82
|
+
# End of file
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
require_relative '../../../lib/sequitur/dynamic_grammar'
|
5
|
+
|
6
|
+
# Load the class under test
|
7
|
+
require_relative '../../../lib/sequitur/formatter/debug'
|
8
|
+
|
9
|
+
module Sequitur # Re-open the module to get rid of qualified names
|
10
|
+
module Formatter
|
11
|
+
|
12
|
+
describe Debug do
|
13
|
+
# Factory method. Build a production with the given sequence
|
14
|
+
# of symbols as its rhs.
|
15
|
+
def build_production(*symbols)
|
16
|
+
prod = Production.new
|
17
|
+
symbols.each { |symb| prod.append_symbol(symb) }
|
18
|
+
return prod
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:p_a) { build_production(:a) }
|
22
|
+
let(:p_b) { build_production(:b) }
|
23
|
+
let(:p_c) { build_production(:c) }
|
24
|
+
let(:p_bc) { build_production(p_b, p_c) }
|
25
|
+
|
26
|
+
let(:empty_grammar) { DynamicGrammar.new }
|
27
|
+
let(:sample_grammar) do
|
28
|
+
grm = DynamicGrammar.new
|
29
|
+
grm.add_production(p_a)
|
30
|
+
grm.add_production(p_b)
|
31
|
+
grm.add_production(p_c)
|
32
|
+
grm.add_production(p_bc)
|
33
|
+
grm
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:destination) { StringIO.new('', 'w') }
|
37
|
+
|
38
|
+
context 'Standard creation & initialization:' do
|
39
|
+
|
40
|
+
it 'should be initialized with an IO argument' do
|
41
|
+
expect { Debug.new(StringIO.new('', 'w')) }.not_to raise_error
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should know its output destination" do
|
45
|
+
instance = Debug.new(destination)
|
46
|
+
expect(instance.output).to eq(destination)
|
47
|
+
end
|
48
|
+
end # context
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
context 'Formatting events:' do
|
53
|
+
it 'should support events of an empty grammar' do
|
54
|
+
instance = Debug.new(destination)
|
55
|
+
a_visitor = empty_grammar.visitor
|
56
|
+
instance.render(a_visitor)
|
57
|
+
expectations =<<-SNIPPET
|
58
|
+
before_grammar
|
59
|
+
before_production
|
60
|
+
before_rhs
|
61
|
+
after_rhs
|
62
|
+
after_production
|
63
|
+
after_grammar
|
64
|
+
SNIPPET
|
65
|
+
expect(destination.string).to eq(expectations)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should support events of a non-empty grammar' do
|
69
|
+
instance = Debug.new(destination)
|
70
|
+
a_visitor = sample_grammar.visitor
|
71
|
+
instance.render(a_visitor)
|
72
|
+
expectations =<<-SNIPPET
|
73
|
+
before_grammar
|
74
|
+
before_production
|
75
|
+
before_rhs
|
76
|
+
after_rhs
|
77
|
+
after_production
|
78
|
+
before_production
|
79
|
+
before_rhs
|
80
|
+
before_terminal
|
81
|
+
after_terminal
|
82
|
+
after_rhs
|
83
|
+
after_production
|
84
|
+
before_production
|
85
|
+
before_rhs
|
86
|
+
before_terminal
|
87
|
+
after_terminal
|
88
|
+
after_rhs
|
89
|
+
after_production
|
90
|
+
before_production
|
91
|
+
before_rhs
|
92
|
+
before_terminal
|
93
|
+
after_terminal
|
94
|
+
after_rhs
|
95
|
+
after_production
|
96
|
+
before_production
|
97
|
+
before_rhs
|
98
|
+
before_non_terminal
|
99
|
+
after_non_terminal
|
100
|
+
before_non_terminal
|
101
|
+
after_non_terminal
|
102
|
+
after_rhs
|
103
|
+
after_production
|
104
|
+
after_grammar
|
105
|
+
SNIPPET
|
106
|
+
expect(destination.string).to eq(expectations)
|
107
|
+
end
|
108
|
+
end # context
|
109
|
+
end # describe
|
110
|
+
|
111
|
+
end # module
|
112
|
+
end # module
|
113
|
+
|
114
|
+
# End of file
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
# Load the class under test
|
4
|
+
require_relative '../../lib/sequitur/grammar_visitor'
|
5
|
+
|
6
|
+
module Sequitur # Re-open the module to get rid of qualified names
|
7
|
+
|
8
|
+
describe GrammarVisitor do
|
9
|
+
# Use a double(mock) as a grammar
|
10
|
+
let(:fake) { double('fake-grammar') }
|
11
|
+
|
12
|
+
context 'Standard creation & initialization:' do
|
13
|
+
|
14
|
+
# Default instantiation rule
|
15
|
+
subject { GrammarVisitor.new(fake) }
|
16
|
+
|
17
|
+
it 'should be initialized with a grammar argument' do
|
18
|
+
expect { GrammarVisitor.new(fake) }.not_to raise_error
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should know the grammar to visit' do
|
22
|
+
expect(subject.grammar).to eq(fake)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "shouldn't have subscribers at start" do
|
26
|
+
expect(subject.subscribers).to be_empty
|
27
|
+
end
|
28
|
+
end # context
|
29
|
+
|
30
|
+
|
31
|
+
context 'Subscribing:' do
|
32
|
+
|
33
|
+
# Default instantiation rule
|
34
|
+
subject { GrammarVisitor.new(fake) }
|
35
|
+
|
36
|
+
let(:listener1) { double('fake-formatter1') }
|
37
|
+
let(:listener2) { double('fake-formatter2') }
|
38
|
+
|
39
|
+
it 'should allow subscriptions' do
|
40
|
+
subject.subscribe(listener1)
|
41
|
+
expect(subject.subscribers.size).to eq(1)
|
42
|
+
expect(subject.subscribers).to eq([listener1])
|
43
|
+
|
44
|
+
subject.subscribe(listener2)
|
45
|
+
expect(subject.subscribers.size).to eq(2)
|
46
|
+
expect(subject.subscribers).to eq([listener1, listener2])
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should allow un-subcriptions' do
|
50
|
+
subject.subscribe(listener1)
|
51
|
+
subject.subscribe(listener2)
|
52
|
+
subject.unsubscribe(listener2)
|
53
|
+
expect(subject.subscribers.size).to eq(1)
|
54
|
+
expect(subject.subscribers).to eq([listener1])
|
55
|
+
subject.unsubscribe(listener1)
|
56
|
+
expect(subject.subscribers).to be_empty
|
57
|
+
end
|
58
|
+
|
59
|
+
end # context
|
60
|
+
|
61
|
+
context 'Notifying visit events:' do
|
62
|
+
|
63
|
+
# Default instantiation rule
|
64
|
+
subject do
|
65
|
+
instance = GrammarVisitor.new(fake)
|
66
|
+
instance.subscribe(listener1)
|
67
|
+
instance
|
68
|
+
end
|
69
|
+
|
70
|
+
# Use doubles/mocks to simulate formatters
|
71
|
+
let(:listener1) { double('fake-formatter1') }
|
72
|
+
let(:listener2) { double('fake-formatter2') }
|
73
|
+
let(:mock_production) { double('fake-production') }
|
74
|
+
|
75
|
+
it 'should react to the start_visit_grammar message' do
|
76
|
+
# Notify subscribers when start the visit of the grammar
|
77
|
+
expect(listener1).to receive(:before_grammar).with(fake)
|
78
|
+
|
79
|
+
subject.start_visit_grammar(fake)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should react to the start_visit_production message' do
|
83
|
+
expect(mock_production).to receive(:rhs).and_return([1, 2, 3])
|
84
|
+
|
85
|
+
# Notify subscribers when start the visit of the production
|
86
|
+
expect(listener1).to receive(:before_production).with(mock_production)
|
87
|
+
expect(listener1).to receive(:before_rhs).with([1, 2, 3])
|
88
|
+
|
89
|
+
subject.start_visit_production(mock_production)
|
90
|
+
end
|
91
|
+
|
92
|
+
end # context
|
93
|
+
|
94
|
+
end # describe
|
95
|
+
|
96
|
+
end # module
|
97
|
+
|
98
|
+
# End of file
|
@@ -92,7 +92,17 @@ describe ProductionRef do
|
|
92
92
|
subject.unbind
|
93
93
|
expect { subject.hash }.to raise_error(StandardError)
|
94
94
|
end
|
95
|
-
|
95
|
+
|
96
|
+
it 'should accept a visitor' do
|
97
|
+
# Use a mock visitor
|
98
|
+
fake = double('fake_visitor')
|
99
|
+
|
100
|
+
# Visitor should receive a visit message
|
101
|
+
expect(fake).to receive(:visit_prod_ref).once
|
102
|
+
expect { subject.accept(fake) }.not_to raise_error
|
103
|
+
end
|
104
|
+
|
105
|
+
end # context
|
96
106
|
|
97
107
|
end # describe
|
98
108
|
|
@@ -322,6 +322,45 @@ describe Production do
|
|
322
322
|
end
|
323
323
|
|
324
324
|
end # context
|
325
|
+
|
326
|
+
context 'Visiting:' do
|
327
|
+
it 'should accept a visitor when its rhs is empty' do
|
328
|
+
# Use a mock visitor
|
329
|
+
fake = double('fake_visitor')
|
330
|
+
|
331
|
+
# Empty production: visitor will receive a start and end visit messages
|
332
|
+
expect(fake).to receive(:start_visit_production).once.ordered
|
333
|
+
expect(fake).to receive(:end_visit_production).once.ordered
|
334
|
+
|
335
|
+
expect { subject.accept(fake) }.not_to raise_error
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'should accept a visitor when rhs consists of terminals only' do
|
339
|
+
# Use a mock visitor
|
340
|
+
fake = double('fake_visitor')
|
341
|
+
expect(fake).to receive(:start_visit_production).once.ordered
|
342
|
+
expect(fake).to receive(:visit_terminal).with('b').ordered
|
343
|
+
expect(fake).to receive(:visit_terminal).with('c').ordered
|
344
|
+
expect(fake).to receive(:end_visit_production).once.ordered
|
345
|
+
|
346
|
+
expect { p_bc.accept(fake) }.not_to raise_error
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'should accept a visitor when rhs consists of non-terminals' do
|
350
|
+
# Add two production references (=non-terminals) to RHS of subject
|
351
|
+
subject.append_symbol(p_a)
|
352
|
+
subject.append_symbol(p_bc)
|
353
|
+
|
354
|
+
fake = double('fake_visitor')
|
355
|
+
expect(fake).to receive(:start_visit_production).once.ordered
|
356
|
+
expect(fake).to receive(:visit_prod_ref).with(p_a).ordered
|
357
|
+
expect(fake).to receive(:visit_prod_ref).with(p_bc).ordered
|
358
|
+
expect(fake).to receive(:end_visit_production).once.ordered
|
359
|
+
|
360
|
+
expect { subject.accept(fake) }.not_to raise_error
|
361
|
+
end
|
362
|
+
|
363
|
+
end # context
|
325
364
|
|
326
365
|
end # describe
|
327
366
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sequitur
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.00
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dimitri Geshef
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-09-
|
11
|
+
date: 2014-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -89,11 +89,17 @@ files:
|
|
89
89
|
- lib/sequitur/constants.rb
|
90
90
|
- lib/sequitur/digram.rb
|
91
91
|
- lib/sequitur/dynamic_grammar.rb
|
92
|
+
- lib/sequitur/formatter/base_text.rb
|
93
|
+
- lib/sequitur/formatter/debug.rb
|
94
|
+
- lib/sequitur/grammar_visitor.rb
|
92
95
|
- lib/sequitur/production.rb
|
93
96
|
- lib/sequitur/production_ref.rb
|
94
97
|
- lib/sequitur/sequitur_grammar.rb
|
95
98
|
- spec/sequitur/digram_spec.rb
|
96
99
|
- spec/sequitur/dynamic_grammar_spec.rb
|
100
|
+
- spec/sequitur/formatter/base_text_spec.rb
|
101
|
+
- spec/sequitur/formatter/debug_spec.rb
|
102
|
+
- spec/sequitur/grammar_visitor_spec.rb
|
97
103
|
- spec/sequitur/production_ref_spec.rb
|
98
104
|
- spec/sequitur/production_spec.rb
|
99
105
|
- spec/sequitur/sequitur_grammar_spec.rb
|
@@ -132,6 +138,9 @@ summary: Ruby implementation of the Sequitur algorithm
|
|
132
138
|
test_files:
|
133
139
|
- spec/sequitur/digram_spec.rb
|
134
140
|
- spec/sequitur/dynamic_grammar_spec.rb
|
141
|
+
- spec/sequitur/formatter/base_text_spec.rb
|
142
|
+
- spec/sequitur/formatter/debug_spec.rb
|
143
|
+
- spec/sequitur/grammar_visitor_spec.rb
|
135
144
|
- spec/sequitur/production_ref_spec.rb
|
136
145
|
- spec/sequitur/production_spec.rb
|
137
146
|
- spec/sequitur/sequitur_grammar_spec.rb
|