dendroid 0.0.6 → 0.0.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a762fb52e0c8ff6116f41f481b5cb7346b99050d90f77c4e133da37a31dda3b3
4
- data.tar.gz: ace142c5221c038eedaab8ea9882a3fbffa34cb424be08083ee722e9e5d8c6ca
3
+ metadata.gz: 989c88ea00734b7145dfc64ded1318c296a3678039213904c15ead0a306942a2
4
+ data.tar.gz: 73aa3efff885fcf1f71f23c7907b7c7b953fd3b174aefb9324a1326663652a9c
5
5
  SHA512:
6
- metadata.gz: 2746b3e1cd03e07e0045d7e41b54ed4c8a2f003df89ea45166fa0b66013f37aa9c1cf0044adc71bbe75d5958d99334b7f612d15c116954fe451f8e1e6756bfaa
7
- data.tar.gz: 4917fd27358d80719b722c14a6df8636917ad827dfc29fb8201920b068393dba09dd2b9750d8b5946031a1d821bd6922c84dd27572b4a635e709faa4bfe31ceb
6
+ metadata.gz: 906746623d20819ce0f1923c8156a27c7ce58b2d926174e0a1dff611407bd873378272c2a2125bb665c8aec20c3beef787942c9fc7f0376aff22e7db4fb6a235
7
+ data.tar.gz: 34f3aabf74b7598a506e3acd38c9df03a7040203e8a9bda2fa02804b96ea0054d17c3044c7ca1384380bdb55af2cd80f1b9e88baa1b5c985642797cc68d80783
data/.rubocop.yml CHANGED
@@ -4,7 +4,7 @@ Layout/EndOfLine:
4
4
 
5
5
  Metrics/AbcSize:
6
6
  Enabled: true
7
- Max: 25
7
+ Max: 29
8
8
 
9
9
  Metrics/BlockLength:
10
10
  Enabled: true
@@ -13,10 +13,18 @@ Metrics/BlockLength:
13
13
  Metrics/ClassLength:
14
14
  Enabled: true
15
15
  Max: 200
16
+
17
+ Metrics/CyclomaticComplexity:
18
+ Enabled: true
19
+ Max: 10
16
20
 
17
21
  Metrics/MethodLength:
18
22
  Enabled: true
19
- Max: 20
23
+ Max: 30
24
+
25
+ Metrics/PerceivedComplexity:
26
+ Enabled: true
27
+ Max: 10
20
28
 
21
29
  Naming/MethodParameterName:
22
30
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.0.8] - 2023-10-30
6
+ ### Added
7
+ - Class `DottedItem` and its spec file
8
+
9
+ ## [0.0.7] - 2023-10-30
10
+ ### Added
11
+ - Class `BaseGrmBuilder` and its spec file
5
12
 
6
13
  ## [0.0.6] - 2023-10-30
7
14
  ### Added
data/README.md CHANGED
@@ -3,10 +3,12 @@ dendroid
3
3
 
4
4
  A Ruby implementation of the Earley parsing algorithm. WIP
5
5
 
6
+ dendroid - Resembling a tree in form and branching structure.
7
+
6
8
  Features
7
9
  --------
8
10
 
9
- * WIP
11
+ * WIP
10
12
 
11
13
  Examples
12
14
  --------
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dendroid
4
+ module GrmAnalysis
5
+ # For a given production rule, a dotted item represents a recognition state.
6
+ # The dot partitions the rhs of the rule in two parts:
7
+ # a) the left part consists of the symbols in the rhs that are matched
8
+ # by the input tokens.
9
+ # b) The right part consists of symbols that are predicted to match the
10
+ # input tokens.
11
+ # The terminology stems from the traditional way to visualize the partition
12
+ # by using a fat dot character as a separator between the left and right
13
+ # parts.
14
+ # An item with the dot at the beginning (i.e. before any rhs symbol)
15
+ # is called a predicted item.
16
+ # An item with the dot at the end (i.e. after all rhs symbols)
17
+ # is called a reduce item.
18
+ # An item with a dot in front of a terminal is called a shift item.
19
+ # An item with the dot not at the beginning is sometimes referred to as a kernel item
20
+ class DottedItem
21
+ # Reference to the production rule
22
+ # @return [Dendroid::Syntax::Production]
23
+ attr_reader :rule
24
+
25
+ # @return [Integer] the dot position
26
+ attr_reader :position
27
+
28
+ # Constructor.
29
+ # @param aRule [Dendroid::Syntax::Rule]
30
+ # @param aPosition [Integer] Position of the dot in rhs of production.
31
+ def initialize(aRule, aPosition)
32
+ @rule = aRule
33
+ @position = valid_position(aPosition)
34
+ end
35
+
36
+ # Return a String representation of the dotted item.
37
+ # @return [String]
38
+ def to_s
39
+ rhs_names = rule.body.map(&:to_s)
40
+ dotted_rhs = rhs_names.insert(position, '.')
41
+ "#{rule.head} => #{dotted_rhs.join(' ')}"
42
+ end
43
+
44
+ # Indicate whether the rhs of the rule is empty
45
+ # @return [Boolean]
46
+ def empty?
47
+ rule.empty?
48
+ end
49
+
50
+ # Terminology inspired from Luger's book
51
+ # @return [Symbol] one of: :initial, :initial_and_completed, :partial, :completed
52
+ def state
53
+ return :initial_and_completed if empty?
54
+ return :initial if position.zero?
55
+
56
+ position == rule.body.size ? :completed : :partial
57
+ end
58
+
59
+ # Indicate whether the dot is at the start of rhs
60
+ # @return [Boolean]
61
+ def initial_pos?
62
+ position.zero? || empty?
63
+ end
64
+
65
+ # Indicate whether the dot is at the end of rhs
66
+ # @return [Boolean]
67
+ def final_pos?
68
+ empty? || position == rule.body.size
69
+ end
70
+
71
+ alias completed? final_pos?
72
+
73
+ # Indicate the dot isn't at start nor at end position
74
+ # @return [Boolean]
75
+ def intermediate_pos?
76
+ return false if empty? || position.zero?
77
+
78
+ position < rule.body.size
79
+ end
80
+
81
+ # Return the symbol right after the dot (if any)
82
+ # @return [Dendroid::Syntax::GrmSymbol, NilClass]
83
+ def next_symbol
84
+ return nil if empty? || completed?
85
+
86
+ rule.body[position]
87
+ end
88
+
89
+ # Check whether the given symbol is the same as after the dot.
90
+ # @param [Dendroid::Syntax::GrmSymbol]
91
+ # @return [Boolean]
92
+ def expecting?(aSymbol)
93
+ actual = next_symbol
94
+ return false if actual.nil?
95
+
96
+ actual == aSymbol
97
+ end
98
+
99
+ # Check whether the dotted item is a shift item.
100
+ # In other words, it expects a terminal to be next symbol
101
+ # @return [Boolean]
102
+ def pre_scan?
103
+ next_symbol&.terminal?
104
+ end
105
+
106
+ # Test for equality with another dotted item.
107
+ # Two dotted items are equal if they refer to the same rule and
108
+ # have both the same rhs and dot positions.
109
+ # @return [Boolean]
110
+ def ==(other)
111
+ return true if eql?(other)
112
+
113
+ (position == other.position) && rule.eql?(other.rule)
114
+ end
115
+
116
+ private
117
+
118
+ def valid_position(aPosition)
119
+ raise StandardError if aPosition.negative? || aPosition > rule.body.size
120
+
121
+ aPosition
122
+ end
123
+ end # class
124
+ end # module
125
+ end # module
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '..\syntax\terminal'
4
+ require_relative '..\syntax\non_terminal'
5
+ require_relative '..\syntax\symbol_seq'
6
+ require_relative '..\syntax\production'
7
+ require_relative '..\syntax\choice'
8
+ require_relative '..\syntax\grammar'
9
+
10
+ module Dendroid
11
+ # This module contains classes that define Domain-Specific Language specialized
12
+ # in grammar definition.
13
+ module GrmDSL
14
+ # Builder GoF pattern: Builder builds a complex object.
15
+ # here the builder creates a grammar from simpler objects
16
+ # (symbols and production rules)
17
+ # and using a step by step approach.
18
+ class BaseGrmBuilder
19
+ # @return [Symbol] one of: :declaring, :building, :complete
20
+ attr_reader :state
21
+
22
+ # @return [Hash{String, Dendroid::Syntax::GrmSymbol}] The mapping of grammar symbol names
23
+ # to the matching grammar symbol object.
24
+ attr_reader :symbols
25
+
26
+ # @return [Array<Dendroid::Syntax::Rule>] The list of rules of the grammar
27
+ attr_reader :rules
28
+
29
+ # Creates a new grammar builder object.
30
+ # @param aBlock [Proc] code block used to build the grammar.
31
+ # @example Building a tiny English grammar
32
+ # builder = Rley::Syntax::GrammarBuilder.new do
33
+ # declare_terminals('n', 'v', 'adj', 'det')
34
+ # rule 'S' => 'NP VP'
35
+ # rule 'VP' => 'v NP'
36
+ # rule 'NP' => ['det n', 'adj NP']
37
+ # end
38
+ # # Now with `builder`, let's create the grammar
39
+ # tiny_eng = builder.grammar
40
+ def initialize(&aBlock)
41
+ @symbols = {}
42
+ @rules = []
43
+ @state = :declaring
44
+
45
+ return unless block_given?
46
+
47
+ instance_exec(&aBlock)
48
+ grammar_complete!
49
+ end
50
+
51
+ # Add the given terminal symbols to the grammar of the language
52
+ # @param terminalSymbols [String, Terminal] 1..* terminal symbols.
53
+ # @return [void]
54
+ def declare_terminals(*terminalSymbols)
55
+ err_msg = "Terminal symbols may only be declared in state :declaring, current state is: #{state}"
56
+ raise StandardError, err_msg unless state == :declaring
57
+
58
+ new_symbs = build_symbols(Dendroid::Syntax::Terminal, terminalSymbols)
59
+ symbols.merge!(new_symbs)
60
+ end
61
+
62
+ # Add a production rule in the grammar given one
63
+ # key-value pair of the form: String => String.
64
+ # Where the key is the name of the non-terminal appearing in the
65
+ # left side of the rule.
66
+ # When the value is a String, it is a sequence of grammar symbol names separated by space.
67
+ # When the value is an array of String, the elements represent an alternative rhs
68
+ # The rule is created and inserted in the grammar.
69
+ # @example
70
+ # builder.rule('sentence' => 'noun_phrase verb_phrase')
71
+ # builder.rule('noun_phrase' => ['noun', 'adj noun'])
72
+ # @param productionRuleRepr [Hash{String, String|Array<String>}]
73
+ # A Hash-based representation of a production.
74
+ # @return [Dendroid::Syntax::Rule] The created Production or Choice instance
75
+ def rule(productionRuleRepr)
76
+ raise StandardError, 'Cannot add a production rule in state :complete' if state == :complete
77
+
78
+ @state = :building
79
+
80
+ return nil unless productionRuleRepr.is_a?(Hash)
81
+
82
+ head_name = productionRuleRepr.keys.first
83
+ if symbols.include? head_name
84
+ err_msg = "Terminal symbol '#{head_name}' may not be on left-side of a rule."
85
+ raise StandardError, err_msg if symbols[head_name].is_a?(Dendroid::Syntax::Terminal)
86
+ else
87
+ symbols.merge!(build_symbols(Dendroid::Syntax::NonTerminal, [head_name]))
88
+ end
89
+ lhs = symbols[head_name]
90
+ raw_rhs = productionRuleRepr.values.first
91
+
92
+ if raw_rhs.is_a? String
93
+ new_prod = Dendroid::Syntax::Production.new(lhs, build_symbol_seq(raw_rhs))
94
+ else
95
+ rhs = raw_rhs.map { |raw| build_symbol_seq(raw) }
96
+ new_prod = Dendroid::Syntax::Choice.new(lhs, rhs)
97
+ end
98
+ rules << new_prod
99
+ new_prod
100
+ end
101
+
102
+ # A method used to notify the builder that the grammar is complete
103
+ # (i.e. all rules were entered).
104
+ def grammar_complete!
105
+ @state = :complete
106
+ end
107
+
108
+ # Generate the grammar according to the specifications.
109
+ # @return [Dendroid::Syntax::Grammar]
110
+ def grammar
111
+ terminals = symbols.values.select(&:terminal?)
112
+ grm = Dendroid::Syntax::Grammar.new(terminals)
113
+ rules.each { |prod| grm.add_rule(prod) }
114
+ grm.complete!
115
+ grm
116
+ end
117
+
118
+ private
119
+
120
+ def build_symbol_seq(raw_symbols)
121
+ symb_array = []
122
+ raw_stripped = raw_symbols.strip
123
+ return Dendroid::Syntax::SymbolSeq.new([]) if raw_stripped.empty?
124
+
125
+ symbol_names = raw_stripped.split(/(?: |\t)+/)
126
+ symbol_names.each do |symb_name|
127
+ unless symbols.include?(symb_name)
128
+ symbols.merge!(build_symbols(Dendroid::Syntax::NonTerminal, [symb_name]))
129
+ end
130
+ symb_array << symbols[symb_name]
131
+ end
132
+
133
+ Dendroid::Syntax::SymbolSeq.new(symb_array)
134
+ end
135
+
136
+ # Add the given grammar symbols.
137
+ # @param aClass [Class] The class of grammar symbols to instantiate.
138
+ # @param theSymbols [Array] array of elements are treated as follows:
139
+ # if the element is already a grammar symbol, then it added as is,
140
+ # otherwise it is considered as the name of a grammar symbol
141
+ # of the specified class to build.
142
+ def build_symbols(aClass, theSymbols)
143
+ symbs = {}
144
+ theSymbols.each do |s|
145
+ new_symbol = build_symbol(aClass, s)
146
+ symbs[new_symbol.name] = new_symbol
147
+ symbs[s] = new_symbol
148
+ end
149
+
150
+ symbs
151
+ end
152
+
153
+ # If the argument is already a grammar symbol object then it is
154
+ # returned as is. Otherwise, the argument is treated as a name
155
+ # for a new instance of the given class.
156
+ # @param aClass [Class] The class of grammar symbols to instantiate
157
+ # @param aSymbolArg [GrmSymbol-like or String]
158
+ # @return [Array] list of grammar symbols
159
+ def build_symbol(aClass, aSymbolArg)
160
+ if aSymbolArg.is_a?(Dendroid::Syntax::GrmSymbol)
161
+ aSymbolArg
162
+ else
163
+ aClass.new(aSymbolArg)
164
+ end
165
+ end
166
+ end # class
167
+ end # module
168
+ end # module
@@ -66,15 +66,26 @@ module Dendroid
66
66
  private
67
67
 
68
68
  def valid_alternatives(alt)
69
+ raise StandardError, "Expecting an Array, found a #{rhs.class} instead." unless alt.is_a?(Array)
70
+
69
71
  if alt.size < 2
70
72
  # A choice must have at least two alternatives
71
- raise StandardError.new, "The choice for #{lhs} must have at least two alternatives."
73
+ raise StandardError, "The choice for `#{head}` must have at least two alternatives."
72
74
  end
73
75
 
74
- cyclic = alt.find { |a| a.size == 1 && lhs == a.first }
75
- if cyclic
76
- # Forbid cyclic rules (e.g. A => A)
77
- raise StandardError.new, "Cyclic rule of the kind #{lhs} => #{lhs} is not allowed."
76
+ # Verify that each array element is a valid symbol sequence
77
+ alt.each { |elem| valid_sequence(elem) }
78
+
79
+ # Fail when duplicate rhs found
80
+ alt_texts = alt.map(&:to_s)
81
+ no_duplicate = alt_texts.uniq
82
+ if alt_texts.size > no_duplicate.size
83
+ alt_texts.each_with_index do |str, i|
84
+ next if str == no_duplicate[i]
85
+
86
+ err_msg = "Duplicate alternatives: #{head} => #{alt_texts[i]}"
87
+ raise StandardError, err_msg
88
+ end
78
89
  end
79
90
 
80
91
  alt
@@ -49,6 +49,7 @@ module Dendroid
49
49
  if nonterm2productions[rule.head]&.include? rule
50
50
  raise StandardError, "Production rule '#{production}' appears more than once in the grammar."
51
51
  end
52
+
52
53
  add_symbol(rule.head)
53
54
  rule.nonterminals.each { |nonterm| add_symbol(nonterm) }
54
55
  rules << rule
@@ -104,10 +105,10 @@ module Dendroid
104
105
  def at_least_one_terminal
105
106
  found = symbols.any?(&:terminal?)
106
107
 
107
- unless found
108
- err_msg = "Grammar doesn't contain any terminal symbol."
109
- raise StandardError, err_msg
110
- end
108
+ return true if found
109
+
110
+ err_msg = "Grammar doesn't contain any terminal symbol."
111
+ raise StandardError, err_msg
111
112
  end
112
113
 
113
114
  # Does every terminal symbol appear at least once
@@ -173,7 +174,6 @@ module Dendroid
173
174
  reachable_sym = backlog.pop
174
175
  prods = nonterm2productions[reachable_sym]
175
176
  prods.each do |prd|
176
- # prd.body.members.each do |member|
177
177
  prd.rhs_symbols.each do |member|
178
178
  unless member.terminal? || set_reachable.include?(member)
179
179
  backlog.push(member)
@@ -184,7 +184,7 @@ module Dendroid
184
184
  end until backlog.empty?
185
185
 
186
186
  all_symbols = Set.new(symbols)
187
- unreachable = all_symbols - set_reachable
187
+ all_symbols - set_reachable
188
188
  end
189
189
 
190
190
  def mark_non_productive_symbols
@@ -194,7 +194,6 @@ module Dendroid
194
194
  backlog.delete(i) if prd.productive?
195
195
  end
196
196
  until backlog.empty?
197
- size_before = backlog.size
198
197
  to_remove = []
199
198
  backlog.each do |i|
200
199
  prd = rules[i]
@@ -232,7 +231,7 @@ module Dendroid
232
231
  begin
233
232
  seqs_done = []
234
233
  backlog.each_pair do |sq, (elem_index, lhs)|
235
- member = sq.members[elem_index]
234
+ member = sq[elem_index]
236
235
  if member.terminal?
237
236
  seqs_done << sq # stop with this sequence: it is non-nullable
238
237
  backlog[sq] = [-1, lhs]
@@ -247,14 +246,14 @@ module Dendroid
247
246
  end
248
247
  end
249
248
  seqs_done.each do |sq|
250
- if backlog.include? sq
251
- (_, lhs) = backlog[sq]
252
- if lhs.nullable?
253
- to_drop = sym2seqs[lhs]
254
- to_drop.each { |seq| backlog.delete(seq) }
255
- else
256
- backlog.delete(sq)
257
- end
249
+ next unless backlog.include? sq
250
+
251
+ (_, lhs) = backlog[sq]
252
+ if lhs.nullable?
253
+ to_drop = sym2seqs[lhs]
254
+ to_drop.each { |seq| backlog.delete(seq) }
255
+ else
256
+ backlog.delete(sq)
258
257
  end
259
258
  end
260
259
  end until backlog.empty? || seqs_done.empty?
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dendroid
4
+ # The namespace for all classes used to build a grammar.
4
5
  module Syntax
5
6
  # Abstract class for grammar symbols.
6
7
  # A grammar symbol is an element that appears in grammar rules.
@@ -20,7 +21,7 @@ module Dendroid
20
21
  name.to_s
21
22
  end
22
23
 
23
- # Equality testing (based on symbol name)
24
+ # Equality testing (based on symbol names)
24
25
  # @return [Boolean]
25
26
  def ==(other)
26
27
  name == other.name
@@ -15,7 +15,7 @@ module Dendroid
15
15
  # @param rhs [Dendroid::Syntax::SymbolSeq] the sequence of symbols on rhs.
16
16
  def initialize(lhs, rhs)
17
17
  super(lhs)
18
- @body = valid_body(rhs)
18
+ @body = valid_sequence(rhs)
19
19
  end
20
20
 
21
21
  # Predicate method to check whether the rule body (its rhs) is empty.
@@ -73,17 +73,6 @@ module Dendroid
73
73
 
74
74
  private
75
75
 
76
- def valid_body(rhs)
77
- raise StandardError unless rhs.is_a?(SymbolSeq)
78
-
79
- if rhs.size == 1 && lhs == rhs.first
80
- # Forbid cyclic rules (e.g. A => A)
81
- raise StandardError.new, "Cyclic rule of the kind #{lhs} => #{lhs} is not allowed."
82
- end
83
-
84
- rhs
85
- end
86
-
87
76
  def productive=(val)
88
77
  @productive = val
89
78
  lhs.productive = val
@@ -47,6 +47,19 @@ module Dendroid
47
47
  rhs_symbols.select(&:terminal?)
48
48
  end
49
49
 
50
+ protected
51
+
52
+ def valid_sequence(rhs)
53
+ raise StandardError, "Expecting a SymbolSeq, found a #{rhs.class} instead." unless rhs.is_a?(SymbolSeq)
54
+
55
+ if rhs.size == 1 && lhs == rhs.first
56
+ # Forbid cyclic rules (e.g. A => A)
57
+ raise StandardError.new, "Cyclic rules of the kind #{lhs} => #{lhs} are not allowed."
58
+ end
59
+
60
+ rhs
61
+ end
62
+
50
63
  private
51
64
 
52
65
  def valid_head(lhs)
@@ -13,7 +13,7 @@ module Dendroid
13
13
  # @return [Array<Dendroid::Syntax::GrmSymbol>] The sequence of symbols
14
14
  attr_reader :members
15
15
 
16
- def_delegators(:@members, :empty?, :first, :map, :size)
16
+ def_delegators(:@members, :[], :empty?, :first, :map, :size)
17
17
 
18
18
  # Create a sequence of grammar symbols (as in right-hand side of
19
19
  # a production rule).
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '..\..\spec_helper'
4
+ require_relative '..\..\..\lib\dendroid\syntax\terminal'
5
+ require_relative '..\..\..\lib\dendroid\syntax\non_terminal'
6
+ require_relative '..\..\..\lib\dendroid\syntax\symbol_seq'
7
+ require_relative '..\..\..\lib\dendroid\syntax\production'
8
+ require_relative '..\..\..\lib\dendroid\grm_analysis\dotted_item'
9
+
10
+ describe Dendroid::GrmAnalysis::DottedItem do
11
+ let(:num_symb) { Dendroid::Syntax::Terminal.new('NUMBER') }
12
+ let(:plus_symb) { Dendroid::Syntax::Terminal.new('PLUS') }
13
+ let(:expr_symb) { Dendroid::Syntax::NonTerminal.new('expression') }
14
+ let(:rhs) { Dendroid::Syntax::SymbolSeq.new([num_symb, plus_symb, num_symb]) }
15
+ let(:empty_body) { Dendroid::Syntax::SymbolSeq.new([]) }
16
+ let(:prod) { Dendroid::Syntax::Production.new(expr_symb, rhs) }
17
+ let(:empty_prod) { Dendroid::Syntax::Production.new(expr_symb, empty_body) }
18
+
19
+ # Implements a dotted item: expression => NUMBER . PLUS NUMBER
20
+ subject { described_class.new(prod, 1) }
21
+
22
+ context 'Initialization:' do
23
+ it 'is initialized with a production and a dot position' do
24
+ expect { described_class.new(prod, 1) }.not_to raise_error
25
+ end
26
+
27
+ it 'knows its related production' do
28
+ expect(subject.rule).to eq(prod)
29
+ end
30
+
31
+ it 'knows its position' do
32
+ expect(subject.position).to eq(1)
33
+ end
34
+ end # context
35
+
36
+ context 'Provided services:' do
37
+ it 'renders a String representation of itself' do
38
+ expect(subject.to_s).to eq('expression => NUMBER . PLUS NUMBER')
39
+ end
40
+
41
+ it 'knows its state' do
42
+ expect(described_class.new(prod, 0).state).to eq(:initial)
43
+ expect(described_class.new(prod, 1).state).to eq(:partial)
44
+ expect(described_class.new(prod, 3).state).to eq(:completed)
45
+
46
+ # Case of an empty production
47
+ expect(described_class.new(empty_prod, 0).state).to eq(:initial_and_completed)
48
+ end
49
+
50
+ it 'knows whether it is in the initial position' do
51
+ expect(described_class.new(prod, 0)).to be_initial_pos
52
+ expect(described_class.new(prod, 2)).not_to be_initial_pos
53
+ expect(described_class.new(prod, 3)).not_to be_initial_pos
54
+
55
+ # Case of an empty production
56
+ expect(described_class.new(empty_prod, 0)).to be_initial_pos
57
+ end
58
+
59
+ it 'knows whether it is in the final position' do
60
+ expect(described_class.new(prod, 0)).not_to be_final_pos
61
+ expect(described_class.new(prod, 2)).not_to be_final_pos
62
+ expect(described_class.new(prod, 3)).to be_final_pos
63
+ expect(described_class.new(prod, 3)).to be_completed
64
+
65
+ # Case of an empty production
66
+ expect(described_class.new(empty_prod, 0)).to be_final_pos
67
+ end
68
+
69
+ it 'knows whether it is in an intermediate position' do
70
+ expect(described_class.new(prod, 0)).not_to be_intermediate_pos
71
+ expect(described_class.new(prod, 2)).to be_intermediate_pos
72
+ expect(described_class.new(prod, 3)).not_to be_intermediate_pos
73
+
74
+ # Case of an empty production
75
+ expect(described_class.new(empty_prod, 0)).not_to be_intermediate_pos
76
+ end
77
+
78
+ it 'knows the symbol after the dot (if any)' do
79
+ expect(described_class.new(prod, 0).next_symbol.name).to eq(:NUMBER)
80
+ expect(described_class.new(prod, 1).next_symbol.name).to eq(:PLUS)
81
+ expect(described_class.new(prod, 2).next_symbol.name).to eq(:NUMBER)
82
+ expect(described_class.new(prod, 3).next_symbol).to be_nil
83
+
84
+ # Case of an empty production
85
+ expect(described_class.new(empty_prod, 0).next_symbol).to be_nil
86
+ end
87
+
88
+ it 'can compare a given symbol to the expected one' do
89
+ expect(described_class.new(prod, 0)).to be_expecting(num_symb)
90
+ expect(described_class.new(prod, 0)).not_to be_expecting(plus_symb)
91
+ expect(described_class.new(prod, 1)).to be_expecting(plus_symb)
92
+ expect(described_class.new(prod, 2)).to be_expecting(num_symb)
93
+ expect(described_class.new(prod, 3)).not_to be_expecting(num_symb)
94
+ expect(described_class.new(prod, 3)).not_to be_expecting(plus_symb)
95
+
96
+ # Case of an empty production
97
+ expect(described_class.new(empty_prod, 0)).not_to be_expecting(num_symb)
98
+ expect(described_class.new(empty_prod, 0)).not_to be_expecting(plus_symb)
99
+ end
100
+ end # context
101
+ end # describe
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '..\..\spec_helper'
4
+ require_relative '..\..\..\lib\dendroid\grm_dsl\base_grm_builder'
5
+
6
+ describe Dendroid::GrmDSL::BaseGrmBuilder do
7
+ # Builds ingredients for a grammar inspired from https://en.wikipedia.org/wiki/Earley_parser
8
+ subject do
9
+ instance = described_class.new
10
+ instance.declare_terminals('PLUS', 'STAR', 'INTEGER')
11
+ instance
12
+ end
13
+
14
+ context 'Initialization:' do
15
+ it 'is initialized with an optional code block' do
16
+ expect { described_class.new }.not_to raise_error
17
+ end
18
+
19
+ it 'is in "declaring" state by default' do
20
+ expect(described_class.new.state).to eq(:declaring)
21
+ end
22
+
23
+ it 'has no grammar symbol by default' do
24
+ expect(described_class.new.symbols).to be_empty
25
+ end
26
+
27
+ it 'has no production rule by default' do
28
+ expect(described_class.new.rules).to be_empty
29
+ end
30
+ end # context
31
+
32
+ context 'Provided services:' do
33
+ it 'builds declared terminal symbols' do
34
+ instance = described_class.new
35
+ terminals = %w[PLUS STAR INTEGER]
36
+ instance.declare_terminals(*terminals)
37
+ expect(instance.symbols.size).to eq(2 * terminals.size)
38
+ expect(instance.symbols[:PLUS]).to be_kind_of(Dendroid::Syntax::Terminal)
39
+ expect(instance.symbols['PLUS']).to eq(instance.symbols[:PLUS])
40
+ expect(instance.symbols[:PLUS].name).to eq(:PLUS)
41
+ expect(instance.symbols[:STAR]).to be_kind_of(Dendroid::Syntax::Terminal)
42
+ expect(instance.symbols['STAR']).to eq(instance.symbols[:STAR])
43
+ expect(instance.symbols[:STAR].name).to eq(:STAR)
44
+ expect(instance.symbols[:INTEGER]).to be_kind_of(Dendroid::Syntax::Terminal)
45
+ expect(instance.symbols['INTEGER']).to eq(instance.symbols[:INTEGER])
46
+ expect(instance.symbols[:INTEGER].name).to eq(:INTEGER)
47
+ expect(instance.state).to eq(:declaring)
48
+ end
49
+
50
+ it 'builds production rules' do
51
+ subject.rule('p' => 's')
52
+ expect(subject.state).to eq(:building)
53
+
54
+ # Undeclared symbols in production represent non-terminals
55
+ expect(subject.symbols['p']).to be_kind_of(Dendroid::Syntax::NonTerminal)
56
+ expect(subject.symbols['s']).to be_kind_of(Dendroid::Syntax::NonTerminal)
57
+
58
+ expect(subject.rules.size).to eq(1)
59
+ expect(subject.rules.first.to_s).to eq('p => s')
60
+ end
61
+
62
+ it 'builds a grammar' do
63
+ subject.rule('p' => 's')
64
+ subject.rule('s' => ['s PLUS m', 'm'])
65
+ subject.rule('m' => ['m STAR t', 't'])
66
+ subject.rule('t' => 'INTEGER')
67
+ subject.grammar_complete!
68
+
69
+ grm = subject.grammar
70
+ expect(grm).to be_kind_of(Dendroid::Syntax::Grammar)
71
+ (terms, nonterms) = grm.symbols.partition(&:terminal?)
72
+ expect(terms.map(&:name)).to eq(%i[PLUS STAR INTEGER])
73
+ expect(nonterms.map(&:name)).to eq(%i[p s m t])
74
+ grammar_rules = [
75
+ 'p => s',
76
+ 's => s PLUS m | m',
77
+ 'm => m STAR t | t',
78
+ 't => INTEGER'
79
+ ]
80
+ expect(subject.rules.map(&:to_s)).to eq(grammar_rules)
81
+ end
82
+
83
+ it 'provides a simple DSL' do
84
+ instance = described_class.new do
85
+ declare_terminals('PLUS', 'STAR', 'INTEGER')
86
+ rule('p' => 's')
87
+ rule('s' => ['s PLUS m', 'm'])
88
+ rule('m' => ['m STAR t', 't'])
89
+ rule('t' => 'INTEGER')
90
+ end
91
+
92
+ grm = instance.grammar
93
+ expect(grm).to be_kind_of(Dendroid::Syntax::Grammar)
94
+ (terms, nonterms) = grm.symbols.partition(&:terminal?)
95
+ expect(terms.map(&:name)).to eq(%i[PLUS STAR INTEGER])
96
+ expect(nonterms.map(&:name)).to eq(%i[p s m t])
97
+ grammar_rules = [
98
+ 'p => s',
99
+ 's => s PLUS m | m',
100
+ 'm => m STAR t | t',
101
+ 't => INTEGER'
102
+ ]
103
+ expect(instance.rules.map(&:to_s)).to eq(grammar_rules)
104
+ end
105
+ end # context
106
+ end # describe
@@ -51,4 +51,18 @@ describe Dendroid::Syntax::Choice do
51
51
  expect(instance.nonterminals).to eq([expr_symb, foo_symb])
52
52
  end
53
53
  end # context
54
+
55
+ context 'Errors:' do
56
+ it 'fails when initialized with one alternative only' do
57
+ err = StandardError
58
+ err_msg = 'The choice for `expression` must have at least two alternatives.'
59
+ expect { described_class.new(expr_symb, [alt1]) }.to raise_error(err, err_msg)
60
+ end
61
+
62
+ it 'fails in presence of duplicate rhs' do
63
+ err = StandardError
64
+ err_msg = 'Duplicate alternatives: expression => NUMBER PLUS NUMBER'
65
+ expect { described_class.new(expr_symb, [alt1, alt2, alt1]) }.to raise_error(err, err_msg)
66
+ end
67
+ end # context
54
68
  end # describe
@@ -40,7 +40,6 @@ describe Dendroid::Syntax::Grammar do
40
40
  Dendroid::Syntax::Choice.new(lhs, sequences.map { |arr| build_symbol_seq(arr) })
41
41
  end
42
42
 
43
- # rubocop: disable Metrics/AbcSize
44
43
  def build_all_rules
45
44
  rule1 = build_production(p_symb, [s_symb]) # p => s
46
45
  rule2 = build_choice(s_symb, [[s_symb, plus_symb, m_symb], [m_symb]]) # s => s + m | m
@@ -48,7 +47,6 @@ describe Dendroid::Syntax::Grammar do
48
47
  rule4 = build_production(t_symb, [int_symb]) # t => INTEGER
49
48
  [rule1, rule2, rule3, rule4]
50
49
  end
51
- # rubocop: enable Metrics/AbcSize
52
50
 
53
51
  context 'Initialization:' do
54
52
  it 'is initialized with an array of terminal symbols' do
@@ -12,6 +12,7 @@ describe Dendroid::Syntax::Production do
12
12
  let(:expr_symb) { Dendroid::Syntax::NonTerminal.new('expression') }
13
13
  let(:foo_symb) { Dendroid::Syntax::NonTerminal.new('foo') }
14
14
  let(:rhs) { Dendroid::Syntax::SymbolSeq.new([num_symb, plus_symb, num_symb]) }
15
+ let(:cyclic_rhs) { Dendroid::Syntax::SymbolSeq.new([foo_symb]) }
15
16
  let(:empty_body) { Dendroid::Syntax::SymbolSeq.new([]) }
16
17
 
17
18
  # Implements a production rule: expression => NUMBER PLUS NUMBER
@@ -74,4 +75,18 @@ describe Dendroid::Syntax::Production do
74
75
  end
75
76
  # rubocop: enable Lint/BinaryOperatorWithIdenticalOperands
76
77
  end # context
78
+
79
+ context 'Errors:' do
80
+ it "fails when rhs isn't initialized with a SymbolSeq" do
81
+ err = StandardError
82
+ err_msg = 'Expecting a SymbolSeq, found a String instead.'
83
+ expect { described_class.new(foo_symb, 'bad') }.to raise_error err, err_msg
84
+ end
85
+
86
+ it 'fails when the production is cyclic' do
87
+ err = StandardError
88
+ err_msg = 'Cyclic rules of the kind foo => foo are not allowed.'
89
+ expect { described_class.new(foo_symb, cyclic_rhs) }.to raise_error err, err_msg
90
+ end
91
+ end # context
77
92
  end # describe
@@ -20,4 +20,11 @@ describe Dendroid::Syntax::Rule do
20
20
  expect(subject.head).to eq(expr_symb)
21
21
  end
22
22
  end # context
23
+
24
+ context 'Errors:' do
25
+ it 'fails when initialized with a terminal' do
26
+ msg = "Terminal symbol 'NUMBER' may not be on left-side of a rule."
27
+ expect { described_class.new(num_symb) }.to raise_error(StandardError, msg)
28
+ end
29
+ end
23
30
  end # describe
data/version.txt CHANGED
@@ -1 +1 @@
1
- 0.0.6
1
+ 0.0.8
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dendroid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dimitri Geshef
@@ -24,6 +24,8 @@ files:
24
24
  - bin/dendroid
25
25
  - dendroid.gemspec
26
26
  - lib/dendroid.rb
27
+ - lib/dendroid/grm_analysis/dotted_item.rb
28
+ - lib/dendroid/grm_dsl/base_grm_builder.rb
27
29
  - lib/dendroid/syntax/choice.rb
28
30
  - lib/dendroid/syntax/grammar.rb
29
31
  - lib/dendroid/syntax/grm_symbol.rb
@@ -32,6 +34,8 @@ files:
32
34
  - lib/dendroid/syntax/rule.rb
33
35
  - lib/dendroid/syntax/symbol_seq.rb
34
36
  - lib/dendroid/syntax/terminal.rb
37
+ - spec/dendroid/grm_analysis/dotted_item_spec.rb
38
+ - spec/dendroid/grm_dsl/base_grm_builder_spec.rb
35
39
  - spec/dendroid/syntax/choice_spec.rb
36
40
  - spec/dendroid/syntax/grammar_spec.rb
37
41
  - spec/dendroid/syntax/grm_symbol_spec.rb