sequitur 0.0.14 → 0.1.00
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/famished-tiger/Sequitur.svg?branch=master)](https://travis-ci.org/famished-tiger/Sequitur)
|
7
7
|
[![Gem Version](https://badge.fury.io/rb/sequitur.svg)](http://badge.fury.io/rb/sequitur)
|
8
|
+
[![Dependency Status](https://gemnasium.com/famished-tiger/Sequitur.png)](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
|