dendroid 0.0.12 → 0.2.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/dendroid/formatters/ascii_tree.rb +142 -0
- data/lib/dendroid/formatters/base_formatter.rb +25 -0
- data/lib/dendroid/formatters/bracket_notation.rb +50 -0
- data/lib/dendroid/grm_analysis/dotted_item.rb +46 -30
- data/lib/dendroid/grm_analysis/grm_analyzer.rb +24 -61
- data/lib/dendroid/grm_analysis/{choice_items.rb → rule_items.rb} +10 -10
- data/lib/dendroid/grm_dsl/base_grm_builder.rb +3 -4
- data/lib/dendroid/parsing/and_node.rb +56 -0
- data/lib/dendroid/parsing/chart_walker.rb +293 -0
- data/lib/dendroid/parsing/composite_parse_node.rb +21 -0
- data/lib/dendroid/parsing/empty_rule_node.rb +28 -0
- data/lib/dendroid/parsing/or_node.rb +51 -0
- data/lib/dendroid/parsing/parse_node.rb +26 -0
- data/lib/dendroid/parsing/parse_tree_visitor.rb +127 -0
- data/lib/dendroid/parsing/parser.rb +185 -0
- data/lib/dendroid/parsing/terminal_node.rb +32 -0
- data/lib/dendroid/parsing/walk_progress.rb +117 -0
- data/lib/dendroid/recognizer/chart.rb +18 -2
- data/lib/dendroid/recognizer/e_item.rb +21 -2
- data/lib/dendroid/recognizer/item_set.rb +7 -2
- data/lib/dendroid/recognizer/recognizer.rb +69 -69
- data/lib/dendroid/syntax/grammar.rb +72 -60
- data/lib/dendroid/syntax/rule.rb +71 -13
- data/spec/dendroid/grm_analysis/dotted_item_spec.rb +59 -47
- data/spec/dendroid/grm_analysis/{choice_items_spec.rb → rule_items_spec.rb} +5 -6
- data/spec/dendroid/parsing/chart_walker_spec.rb +223 -0
- data/spec/dendroid/parsing/terminal_node_spec.rb +36 -0
- data/spec/dendroid/recognizer/e_item_spec.rb +5 -5
- data/spec/dendroid/recognizer/item_set_spec.rb +16 -8
- data/spec/dendroid/recognizer/recognizer_spec.rb +57 -5
- data/spec/dendroid/support/sample_grammars.rb +2 -0
- data/spec/dendroid/syntax/grammar_spec.rb +44 -34
- data/spec/dendroid/syntax/rule_spec.rb +56 -7
- data/version.txt +1 -1
- metadata +20 -13
- data/lib/dendroid/grm_analysis/alternative_item.rb +0 -70
- data/lib/dendroid/grm_analysis/production_items.rb +0 -55
- data/lib/dendroid/syntax/choice.rb +0 -95
- data/lib/dendroid/syntax/production.rb +0 -82
- data/spec/dendroid/grm_analysis/alternative_item_spec.rb +0 -12
- data/spec/dendroid/grm_analysis/production_items_spec.rb +0 -68
- data/spec/dendroid/syntax/choice_spec.rb +0 -68
- data/spec/dendroid/syntax/production_spec.rb +0 -92
| @@ -17,47 +17,48 @@ module Dendroid | |
| 17 17 | 
             
                  # @return [Array<Dendroid::Syntax::GrmSymbol>] The terminal and non-terminal symbols.
         | 
| 18 18 | 
             
                  attr_reader :symbols
         | 
| 19 19 |  | 
| 20 | 
            +
                  # A Hash that maps symbol names to their grammar symbols
         | 
| 21 | 
            +
                  # @return [Hash{String|Symbol => Dendroid::Syntax::GrmSymbol}]
         | 
| 22 | 
            +
                  attr_reader :name2symbol
         | 
| 23 | 
            +
             | 
| 20 24 | 
             
                  # The list of production rules for the language.
         | 
| 21 25 | 
             
                  # @return [Array<Dendroid::Syntax::Rule>] Array of rules for the grammar.
         | 
| 22 26 | 
             
                  attr_reader :rules
         | 
| 23 27 |  | 
| 24 | 
            -
                  # A Hash that maps symbol names to their grammar symbols
         | 
| 25 | 
            -
                  # @return [Hash{String => Dendroid::Syntax::GrmSymbol}]
         | 
| 26 | 
            -
                  attr_reader :name2symbol
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                  # TODO: make nonterminal - rules one-to-one
         | 
| 29 28 | 
             
                  # A Hash that maps symbol names to their grammar symbols
         | 
| 30 29 | 
             
                  # @return [Hash{Dendroid::Syntax::GrmSymbol => Dendroid::Syntax::Rule}]
         | 
| 31 | 
            -
                  attr_reader : | 
| 30 | 
            +
                  attr_reader :nonterm2production
         | 
| 32 31 |  | 
| 33 32 | 
             
                  # Constructor.
         | 
| 34 33 | 
             
                  # @param terminals [Array<Dendroid::Syntax::Terminal>]
         | 
| 35 34 | 
             
                  def initialize(terminals)
         | 
| 36 35 | 
             
                    @symbols = []
         | 
| 37 36 | 
             
                    @name2symbol = {}
         | 
| 37 | 
            +
                    @rules = []
         | 
| 38 | 
            +
                    @nonterm2production = {}
         | 
| 38 39 | 
             
                    add_terminals(terminals)
         | 
| 39 40 | 
             
                  end
         | 
| 40 41 |  | 
| 41 | 
            -
                  # Add a rule to the grammar
         | 
| 42 | 
            +
                  # Add a rule to the grammar.
         | 
| 42 43 | 
             
                  # @param rule [Dendroid::Syntax::Rule]
         | 
| 43 44 | 
             
                  def add_rule(rule)
         | 
| 44 | 
            -
                    if  | 
| 45 | 
            -
                       | 
| 46 | 
            -
                       | 
| 45 | 
            +
                    if lhs_already_defined?(rule)
         | 
| 46 | 
            +
                      msg = "Non-terminal '#{rule.head}' is on left-hand side of more than one rule."
         | 
| 47 | 
            +
                      raise StandardError, msg
         | 
| 47 48 | 
             
                    end
         | 
| 48 | 
            -
                     | 
| 49 | 
            -
             | 
| 50 | 
            -
                      raise StandardError, "Production rule '#{rule}' appears more than once in the grammar."
         | 
| 49 | 
            +
                    if duplicate_rule?(rule)
         | 
| 50 | 
            +
                      raise StandardError, "Duplicate production rule '#{rule}'."
         | 
| 51 51 | 
             
                    end
         | 
| 52 52 |  | 
| 53 53 | 
             
                    add_symbol(rule.head)
         | 
| 54 54 | 
             
                    rule.nonterminals.each { |nonterm| add_symbol(nonterm) }
         | 
| 55 55 | 
             
                    rules << rule
         | 
| 56 | 
            -
                     | 
| 57 | 
            -
                    nonterm2productions[rule.head] << rule
         | 
| 56 | 
            +
                    nonterm2production[rule.head] = rule
         | 
| 58 57 | 
             
                  end
         | 
| 59 58 |  | 
| 60 | 
            -
                  # Return the start symbol for the language
         | 
| 59 | 
            +
                  # Return the start symbol for the language, that is,
         | 
| 60 | 
            +
                  # the non-terminal symbol used to denote the top-level
         | 
| 61 | 
            +
                  # construct of the language being defined.
         | 
| 61 62 | 
             
                  # @return [Dendroid::Syntax::NonTerminal]
         | 
| 62 63 | 
             
                  def start_symbol
         | 
| 63 64 | 
             
                    rules.first.lhs
         | 
| @@ -73,10 +74,14 @@ module Dendroid | |
| 73 74 |  | 
| 74 75 | 
             
                  private
         | 
| 75 76 |  | 
| 76 | 
            -
                   | 
| 77 | 
            -
             | 
| 78 | 
            -
                   | 
| 79 | 
            -
             | 
| 77 | 
            +
                  def lhs_already_defined?(rule)
         | 
| 78 | 
            +
                    nonterm2production.include? rule.head
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def duplicate_rule?(rule)
         | 
| 82 | 
            +
                    nonterm2production[rule.head]&.include? rule
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 80 85 | 
             
                  def add_terminals(terminals)
         | 
| 81 86 | 
             
                    terminals.each { |term| add_symbol(term) }
         | 
| 82 87 | 
             
                  end
         | 
| @@ -89,6 +94,15 @@ module Dendroid | |
| 89 94 | 
             
                    name2symbol[symb.name.to_s] = symb
         | 
| 90 95 | 
             
                  end
         | 
| 91 96 |  | 
| 97 | 
            +
                  def all_terminals
         | 
| 98 | 
            +
                    Set.new(symbols.select(&:terminal?))
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  def all_nonterminals
         | 
| 102 | 
            +
                    Set.new(symbols.reject(&:terminal?))
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  # Perform correctness checks of the grammar.
         | 
| 92 106 | 
             
                  def validate
         | 
| 93 107 | 
             
                    at_least_one_terminal
         | 
| 94 108 | 
             
                    are_terminals_referenced?
         | 
| @@ -104,7 +118,6 @@ module Dendroid | |
| 104 118 | 
             
                  # Does the grammar contain at least one terminal symbol?
         | 
| 105 119 | 
             
                  def at_least_one_terminal
         | 
| 106 120 | 
             
                    found = symbols.any?(&:terminal?)
         | 
| 107 | 
            -
             | 
| 108 121 | 
             
                    return true if found
         | 
| 109 122 |  | 
| 110 123 | 
             
                    err_msg = "Grammar doesn't contain any terminal symbol."
         | 
| @@ -114,37 +127,28 @@ module Dendroid | |
| 114 127 | 
             
                  # Does every terminal symbol appear at least once
         | 
| 115 128 | 
             
                  # in a rhs of a production rule?
         | 
| 116 129 | 
             
                  def are_terminals_referenced?
         | 
| 117 | 
            -
                    all_terminals = Set.new(symbols.select(&:terminal?))
         | 
| 118 130 | 
             
                    terms_in_rhs = rules.reduce(Set.new) do |collected, prd|
         | 
| 119 131 | 
             
                      found = prd.terminals
         | 
| 120 132 | 
             
                      collected.merge(found)
         | 
| 121 133 | 
             
                    end
         | 
| 122 | 
            -
                     | 
| 123 | 
            -
                    unless check_ok
         | 
| 124 | 
            -
                      unused_terms = all_terminals.difference(terms_in_rhs)
         | 
| 125 | 
            -
                      text = unused_terms.map(&:name).join("', '")
         | 
| 126 | 
            -
                      err_msg = "Terminal symbols '#{text}' never appear in production rules."
         | 
| 127 | 
            -
                      raise StandardError, err_msg
         | 
| 128 | 
            -
                    end
         | 
| 134 | 
            +
                    return true if all_terminals == terms_in_rhs
         | 
| 129 135 |  | 
| 130 | 
            -
                     | 
| 136 | 
            +
                    unused_terms = all_terminals.difference(terms_in_rhs)
         | 
| 137 | 
            +
                    text = unused_terms.map(&:name).join("', '")
         | 
| 138 | 
            +
                    err_msg = "Terminal symbols '#{text}' never appear in production rules."
         | 
| 139 | 
            +
                    raise StandardError, err_msg
         | 
| 131 140 | 
             
                  end
         | 
| 132 141 |  | 
| 133 142 | 
             
                  def are_nonterminals_rewritten?
         | 
| 134 | 
            -
                    all_nonterminals = Set.new(symbols.reject(&:terminal?))
         | 
| 135 | 
            -
             | 
| 136 143 | 
             
                    symbs_in_lhs = rules.reduce(Set.new) do |collected, prd|
         | 
| 137 144 | 
             
                      collected.add(prd.head)
         | 
| 138 145 | 
             
                    end
         | 
| 139 | 
            -
                     | 
| 140 | 
            -
                    unless check_ok
         | 
| 141 | 
            -
                      undefined_nterms = all_nonterminals.difference(symbs_in_lhs)
         | 
| 142 | 
            -
                      text = undefined_nterms.map(&:name).join("', '")
         | 
| 143 | 
            -
                      err_msg = "Non-terminal symbols '#{text}' never appear in head of any production rule."
         | 
| 144 | 
            -
                      raise StandardError, err_msg
         | 
| 145 | 
            -
                    end
         | 
| 146 | 
            +
                    return true if all_nonterminals == symbs_in_lhs
         | 
| 146 147 |  | 
| 147 | 
            -
                     | 
| 148 | 
            +
                    undefined_nterms = all_nonterminals.difference(symbs_in_lhs)
         | 
| 149 | 
            +
                    text = undefined_nterms.map(&:name).join("', '")
         | 
| 150 | 
            +
                    err_msg = "Non-terminal symbols '#{text}' never appear in head of any production rule."
         | 
| 151 | 
            +
                    raise StandardError, err_msg
         | 
| 148 152 | 
             
                  end
         | 
| 149 153 |  | 
| 150 154 | 
             
                  def are_symbols_reachable?
         | 
| @@ -165,28 +169,31 @@ module Dendroid | |
| 165 169 | 
             
                    raise StandardError, err_msg
         | 
| 166 170 | 
             
                  end
         | 
| 167 171 |  | 
| 172 | 
            +
                  # rubocop: disable Metrics/AbcSize
         | 
| 173 | 
            +
                  # rubocop: disable Metrics/CyclomaticComplexity
         | 
| 174 | 
            +
                  # rubocop: disable Metrics/PerceivedComplexity
         | 
| 175 | 
            +
             | 
| 168 176 | 
             
                  # Are all symbols reachable from start symbol?
         | 
| 177 | 
            +
                  # @return [Set<NonTerminal>] Set of unreachable symbols
         | 
| 169 178 | 
             
                  def unreachable_symbols
         | 
| 170 179 | 
             
                    backlog = [start_symbol]
         | 
| 171 180 | 
             
                    set_reachable = Set.new(backlog.dup)
         | 
| 172 181 |  | 
| 173 | 
            -
                     | 
| 182 | 
            +
                    loop do
         | 
| 174 183 | 
             
                      reachable_sym = backlog.pop
         | 
| 175 | 
            -
                       | 
| 176 | 
            -
                       | 
| 177 | 
            -
                         | 
| 178 | 
            -
             | 
| 179 | 
            -
                            backlog.push(member)
         | 
| 180 | 
            -
                          end
         | 
| 181 | 
            -
                          set_reachable.add(member)
         | 
| 182 | 
            -
                        end
         | 
| 184 | 
            +
                      prd = nonterm2production[reachable_sym]
         | 
| 185 | 
            +
                      prd.rhs_symbols.each do |member|
         | 
| 186 | 
            +
                        backlog.push(member) unless member.terminal? || set_reachable.include?(member)
         | 
| 187 | 
            +
                        set_reachable.add(member)
         | 
| 183 188 | 
             
                      end
         | 
| 184 | 
            -
             | 
| 189 | 
            +
                      break if backlog.empty?
         | 
| 190 | 
            +
                    end
         | 
| 185 191 |  | 
| 186 192 | 
             
                    all_symbols = Set.new(symbols)
         | 
| 187 193 | 
             
                    all_symbols - set_reachable
         | 
| 188 194 | 
             
                  end
         | 
| 189 195 |  | 
| 196 | 
            +
                  # @return [Array<Dendroid::Syntax::NonTerminal>]
         | 
| 190 197 | 
             
                  def mark_non_productive_symbols
         | 
| 191 198 | 
             
                    prod_count = rules.size
         | 
| 192 199 | 
             
                    backlog = Set.new(0...prod_count)
         | 
| @@ -204,7 +211,7 @@ module Dendroid | |
| 204 211 | 
             
                      backlog.subtract(to_remove)
         | 
| 205 212 | 
             
                    end
         | 
| 206 213 |  | 
| 207 | 
            -
                    backlog.each { |i| rules[i].non_productive }
         | 
| 214 | 
            +
                    # backlog.each { |i| rules[i].non_productive }
         | 
| 208 215 | 
             
                    non_productive = symbols.reject(&:productive?)
         | 
| 209 216 | 
             
                    non_productive.each { |symb| symb.productive = false }
         | 
| 210 217 | 
             
                    non_productive
         | 
| @@ -214,11 +221,11 @@ module Dendroid | |
| 214 221 | 
             
                    nullable_found = false
         | 
| 215 222 | 
             
                    sym2seqs = {}
         | 
| 216 223 |  | 
| 217 | 
            -
                     | 
| 218 | 
            -
                      if  | 
| 224 | 
            +
                    nonterm2production.each_pair do |sym, prod|
         | 
| 225 | 
            +
                      if prod.empty?
         | 
| 219 226 | 
             
                        sym.nullable = nullable_found = true
         | 
| 220 227 | 
             
                      else
         | 
| 221 | 
            -
                        sym2seqs[sym] =  | 
| 228 | 
            +
                        sym2seqs[sym] = prod.rhs
         | 
| 222 229 | 
             
                      end
         | 
| 223 230 | 
             
                    end
         | 
| 224 231 |  | 
| @@ -228,7 +235,7 @@ module Dendroid | |
| 228 235 | 
             
                        seqs.each { |sq| backlog[sq] = [0, sym] }
         | 
| 229 236 | 
             
                      end
         | 
| 230 237 |  | 
| 231 | 
            -
                       | 
| 238 | 
            +
                      loop do
         | 
| 232 239 | 
             
                        seqs_done = []
         | 
| 233 240 | 
             
                        backlog.each_pair do |sq, (elem_index, lhs)|
         | 
| 234 241 | 
             
                          member = sq[elem_index]
         | 
| @@ -256,18 +263,23 @@ module Dendroid | |
| 256 263 | 
             
                            backlog.delete(sq)
         | 
| 257 264 | 
             
                          end
         | 
| 258 265 | 
             
                        end
         | 
| 259 | 
            -
             | 
| 266 | 
            +
                        break if backlog.empty? || seqs_done.empty?
         | 
| 267 | 
            +
                      end
         | 
| 260 268 | 
             
                    end
         | 
| 261 269 |  | 
| 270 | 
            +
                    # symbols.each do |sym|
         | 
| 271 | 
            +
                    #   next if sym.terminal?
         | 
| 272 | 
            +
                    #
         | 
| 273 | 
            +
                    #   sym.nullable = false if sym.nullable.nil?
         | 
| 274 | 
            +
                    # end
         | 
| 262 275 | 
             
                    symbols.each do |sym|
         | 
| 263 | 
            -
                      next if sym.terminal?
         | 
| 276 | 
            +
                      next if sym.terminal? || sym.nullable?
         | 
| 264 277 |  | 
| 265 | 
            -
                      sym.nullable = false | 
| 278 | 
            +
                      sym.nullable = false
         | 
| 266 279 | 
             
                    end
         | 
| 267 280 | 
             
                  end
         | 
| 268 281 | 
             
                  # rubocop: enable Metrics/AbcSize
         | 
| 269 | 
            -
                  # rubocop: enable Metrics/ | 
| 270 | 
            -
                  # rubocop: enable Metrics/MethodLength
         | 
| 282 | 
            +
                  # rubocop: enable Metrics/CyclomaticComplexity
         | 
| 271 283 | 
             
                  # rubocop: enable Metrics/PerceivedComplexity
         | 
| 272 284 | 
             
                end # class
         | 
| 273 285 | 
             
              end # module
         | 
    
        data/lib/dendroid/syntax/rule.rb
    CHANGED
    
    | @@ -2,28 +2,60 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Dendroid
         | 
| 4 4 | 
             
              module Syntax
         | 
| 5 | 
            -
                #  | 
| 6 | 
            -
                #  | 
| 7 | 
            -
                # and the right-hand side (RHS) consists of one or more sequence of symbols.
         | 
| 8 | 
            -
                # The symbols in RHS can be either terminal or non-terminal symbols.
         | 
| 9 | 
            -
                # The rule stipulates that the LHS is equivalent to the RHS,
         | 
| 10 | 
            -
                # in other words every occurrence of the LHS can be substituted to
         | 
| 11 | 
            -
                # corresponding RHS.
         | 
| 5 | 
            +
                # A specialization of the Rule class.
         | 
| 6 | 
            +
                # A choice is a rule with multiple rhs
         | 
| 12 7 | 
             
                class Rule
         | 
| 13 8 | 
             
                  # @return [Dendroid::Syntax::NonTerminal] The left-hand side of the rule.
         | 
| 14 9 | 
             
                  attr_reader :head
         | 
| 15 10 | 
             
                  alias lhs head
         | 
| 16 11 |  | 
| 17 | 
            -
                  #  | 
| 18 | 
            -
                   | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 12 | 
            +
                  # @return [Array<Dendroid::Syntax::SymbolSeq>]
         | 
| 13 | 
            +
                  attr_reader :alternatives
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  # Create a Choice instance.
         | 
| 16 | 
            +
                  # @param theLhs [Dendroid::Syntax::NonTerminal] The left-hand side of the rule.
         | 
| 17 | 
            +
                  # @param alt [Array<Dendroid::Syntax::SymbolSeq>] the alternatives (each as a sequence of symbols).
         | 
| 18 | 
            +
                  def initialize(theLhs, alt)
         | 
| 19 | 
            +
                    @head = valid_head(theLhs)
         | 
| 20 | 
            +
                    @alternatives = valid_alternatives(alt)
         | 
| 21 21 | 
             
                  end
         | 
| 22 22 |  | 
| 23 | 
            -
                  # Return the text representation of the  | 
| 23 | 
            +
                  # Return the text representation of the choice
         | 
| 24 24 | 
             
                  # @return [String]
         | 
| 25 25 | 
             
                  def to_s
         | 
| 26 | 
            -
                    head. | 
| 26 | 
            +
                    "#{head} => #{alternatives.join(' | ')}"
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  # Predicate method to check whether the choice rule body is productive.
         | 
| 30 | 
            +
                  # It is productive when at least one of its alternative is productive.
         | 
| 31 | 
            +
                  # @return [Boolean]
         | 
| 32 | 
            +
                  def productive?
         | 
| 33 | 
            +
                    productive_alts = alternatives.select(&:productive?)
         | 
| 34 | 
            +
                    return false if productive_alts.empty?
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    @productive = Set.new(productive_alts)
         | 
| 37 | 
            +
                    head.productive = true
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  # Predicate method to check whether the rule has at least one empty alternative.
         | 
| 41 | 
            +
                  # @return [Boolean]
         | 
| 42 | 
            +
                  def empty?
         | 
| 43 | 
            +
                    alternatives.any?(&:empty?)
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  # Returns an array with the symbol sequence of its alternatives
         | 
| 47 | 
            +
                  # @return [Array<Dendroid::Syntax::SymbolSeq>]
         | 
| 48 | 
            +
                  def rhs
         | 
| 49 | 
            +
                    alternatives
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  # Equality operator
         | 
| 53 | 
            +
                  # Two production rules are equal when their head and alternatives are equal.
         | 
| 54 | 
            +
                  # @return [Boolean]
         | 
| 55 | 
            +
                  def ==(other)
         | 
| 56 | 
            +
                    return true if equal?(other)
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    (head == other.head) && (alternatives == other.alternatives)
         | 
| 27 59 | 
             
                  end
         | 
| 28 60 |  | 
| 29 61 | 
             
                  # The set of all grammar symbols that occur in the rhs.
         | 
| @@ -70,6 +102,32 @@ module Dendroid | |
| 70 102 |  | 
| 71 103 | 
             
                    lhs
         | 
| 72 104 | 
             
                  end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  def valid_alternatives(alt)
         | 
| 107 | 
            +
                    raise StandardError, "Expecting an Array, found a #{rhs.class} instead." unless alt.is_a?(Array)
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    if alt.size.zero?
         | 
| 110 | 
            +
                      # A choice must have at least two alternatives
         | 
| 111 | 
            +
                      raise StandardError, "The choice for `#{head}` must have at least one alternative."
         | 
| 112 | 
            +
                    end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                    # Verify that each array element is a valid symbol sequence
         | 
| 115 | 
            +
                    alt.each { |elem| valid_sequence(elem) }
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    # Fail when duplicate rhs found
         | 
| 118 | 
            +
                    alt_texts = alt.map(&:to_s)
         | 
| 119 | 
            +
                    no_duplicate = alt_texts.uniq
         | 
| 120 | 
            +
                    if alt_texts.size > no_duplicate.size
         | 
| 121 | 
            +
                      alt_texts.each_with_index do |str, i|
         | 
| 122 | 
            +
                        next if str == no_duplicate[i]
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                        err_msg = "Duplicate alternatives: #{head} => #{alt_texts[i]}"
         | 
| 125 | 
            +
                        raise StandardError, err_msg
         | 
| 126 | 
            +
                      end
         | 
| 127 | 
            +
                    end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                    alt
         | 
| 130 | 
            +
                  end
         | 
| 73 131 | 
             
                end # class
         | 
| 74 132 | 
             
              end # module
         | 
| 75 133 | 
             
            end # module
         | 
| @@ -4,28 +4,29 @@ require_relative '..\..\spec_helper' | |
| 4 4 | 
             
            require_relative '..\..\..\lib\dendroid\syntax\terminal'
         | 
| 5 5 | 
             
            require_relative '..\..\..\lib\dendroid\syntax\non_terminal'
         | 
| 6 6 | 
             
            require_relative '..\..\..\lib\dendroid\syntax\symbol_seq'
         | 
| 7 | 
            -
            require_relative '..\..\..\lib\dendroid\syntax\ | 
| 7 | 
            +
            require_relative '..\..\..\lib\dendroid\syntax\rule'
         | 
| 8 8 | 
             
            require_relative '..\..\..\lib\dendroid\grm_analysis\dotted_item'
         | 
| 9 9 |  | 
| 10 10 | 
             
            describe Dendroid::GrmAnalysis::DottedItem do
         | 
| 11 11 | 
             
              let(:num_symb) { Dendroid::Syntax::Terminal.new('NUMBER') }
         | 
| 12 12 | 
             
              let(:plus_symb) { Dendroid::Syntax::Terminal.new('PLUS') }
         | 
| 13 | 
            +
              let(:minus_symb) { Dendroid::Syntax::Terminal.new('MINUS') }
         | 
| 13 14 | 
             
              let(:expr_symb) { Dendroid::Syntax::NonTerminal.new('expression') }
         | 
| 14 | 
            -
              let(: | 
| 15 | 
            +
              let(:rhs1) { Dendroid::Syntax::SymbolSeq.new([num_symb, plus_symb, num_symb]) }
         | 
| 16 | 
            +
              let(:rhs2) { Dendroid::Syntax::SymbolSeq.new([num_symb, minus_symb, num_symb]) }
         | 
| 15 17 | 
             
              let(:empty_body) { Dendroid::Syntax::SymbolSeq.new([]) }
         | 
| 16 | 
            -
              let(: | 
| 17 | 
            -
              let(:empty_prod) { Dendroid::Syntax::Production.new(expr_symb, empty_body) }
         | 
| 18 | 
            +
              let(:choice) { Dendroid::Syntax::Rule.new(expr_symb, [rhs1, rhs2, empty_body]) }
         | 
| 18 19 |  | 
| 19 | 
            -
              # Implements a dotted item: expression => NUMBER .  | 
| 20 | 
            -
              subject { described_class.new( | 
| 20 | 
            +
              # Implements a dotted item: expression => NUMBER . MINUS NUMBER
         | 
| 21 | 
            +
              subject { described_class.new(choice, 1, 1) }
         | 
| 21 22 |  | 
| 22 23 | 
             
              context 'Initialization:' do
         | 
| 23 24 | 
             
                it 'is initialized with a production and a dot position' do
         | 
| 24 | 
            -
                  expect { described_class.new( | 
| 25 | 
            +
                  expect { described_class.new(choice, 1, 1) }.not_to raise_error
         | 
| 25 26 | 
             
                end
         | 
| 26 27 |  | 
| 27 28 | 
             
                it 'knows its related production' do
         | 
| 28 | 
            -
                  expect(subject.rule).to eq( | 
| 29 | 
            +
                  expect(subject.rule).to eq(choice)
         | 
| 29 30 | 
             
                end
         | 
| 30 31 |  | 
| 31 32 | 
             
                it 'knows its position' do
         | 
| @@ -35,67 +36,78 @@ describe Dendroid::GrmAnalysis::DottedItem do | |
| 35 36 |  | 
| 36 37 | 
             
              context 'Provided services:' do
         | 
| 37 38 | 
             
                it 'renders a String representation of itself' do
         | 
| 38 | 
            -
                  expect(subject.to_s).to eq('expression => NUMBER .  | 
| 39 | 
            +
                  expect(subject.to_s).to eq('expression => NUMBER . MINUS NUMBER')
         | 
| 39 40 | 
             
                end
         | 
| 40 41 |  | 
| 41 42 | 
             
                it 'knows its state' do
         | 
| 42 | 
            -
                  expect(described_class.new( | 
| 43 | 
            -
                  expect(described_class.new( | 
| 44 | 
            -
                  expect(described_class.new( | 
| 43 | 
            +
                  expect(described_class.new(choice, 0, 1).state).to eq(:initial)
         | 
| 44 | 
            +
                  expect(described_class.new(choice, 1, 1).state).to eq(:partial)
         | 
| 45 | 
            +
                  expect(described_class.new(choice, 3, 1).state).to eq(:completed)
         | 
| 45 46 |  | 
| 46 | 
            -
                  # Case of an empty  | 
| 47 | 
            -
                  expect(described_class.new( | 
| 47 | 
            +
                  # Case of an empty alternative
         | 
| 48 | 
            +
                  expect(described_class.new(choice, 0, 2).state).to eq(:initial_and_completed)
         | 
| 48 49 | 
             
                end
         | 
| 49 50 |  | 
| 50 51 | 
             
                it 'knows whether it is in the initial position' do
         | 
| 51 | 
            -
                  expect(described_class.new( | 
| 52 | 
            -
                  expect(described_class.new( | 
| 53 | 
            -
                  expect(described_class.new( | 
| 52 | 
            +
                  expect(described_class.new(choice, 0, 0)).to be_initial_pos
         | 
| 53 | 
            +
                  expect(described_class.new(choice, 2, 0)).not_to be_initial_pos
         | 
| 54 | 
            +
                  expect(described_class.new(choice, 3, 0)).not_to be_initial_pos
         | 
| 54 55 |  | 
| 55 | 
            -
                  # Case of an empty  | 
| 56 | 
            -
                  expect(described_class.new( | 
| 56 | 
            +
                  # Case of an empty alternative
         | 
| 57 | 
            +
                  expect(described_class.new(choice, 0, 2)).to be_initial_pos
         | 
| 57 58 | 
             
                end
         | 
| 58 59 |  | 
| 59 60 | 
             
                it 'knows whether it is in the final position' do
         | 
| 60 | 
            -
                  expect(described_class.new( | 
| 61 | 
            -
                  expect(described_class.new( | 
| 62 | 
            -
                  expect(described_class.new( | 
| 63 | 
            -
                  expect(described_class.new( | 
| 61 | 
            +
                  expect(described_class.new(choice, 0, 1)).not_to be_final_pos
         | 
| 62 | 
            +
                  expect(described_class.new(choice, 2, 1)).not_to be_final_pos
         | 
| 63 | 
            +
                  expect(described_class.new(choice, 3, 1)).to be_final_pos
         | 
| 64 | 
            +
                  expect(described_class.new(choice, 3,1)).to be_completed
         | 
| 64 65 |  | 
| 65 | 
            -
                  # Case of an empty  | 
| 66 | 
            -
                  expect(described_class.new( | 
| 66 | 
            +
                  # Case of an empty alternative
         | 
| 67 | 
            +
                  expect(described_class.new(choice, 0, 2)).to be_final_pos
         | 
| 67 68 | 
             
                end
         | 
| 68 69 |  | 
| 69 70 | 
             
                it 'knows whether it is in an intermediate position' do
         | 
| 70 | 
            -
                  expect(described_class.new( | 
| 71 | 
            -
                  expect(described_class.new( | 
| 72 | 
            -
                  expect(described_class.new( | 
| 71 | 
            +
                  expect(described_class.new(choice, 0, 0)).not_to be_intermediate_pos
         | 
| 72 | 
            +
                  expect(described_class.new(choice, 2, 0)).to be_intermediate_pos
         | 
| 73 | 
            +
                  expect(described_class.new(choice, 3, 0)).not_to be_intermediate_pos
         | 
| 73 74 |  | 
| 74 | 
            -
                  # Case of an empty  | 
| 75 | 
            -
                  expect(described_class.new( | 
| 75 | 
            +
                  # Case of an empty alternative
         | 
| 76 | 
            +
                  expect(described_class.new(choice, 0, 2)).not_to be_intermediate_pos
         | 
| 76 77 | 
             
                end
         | 
| 77 78 |  | 
| 78 79 | 
             
                it 'knows the symbol after the dot (if any)' do
         | 
| 79 | 
            -
                  expect(described_class.new( | 
| 80 | 
            -
                  expect(described_class.new( | 
| 81 | 
            -
                  expect(described_class.new( | 
| 82 | 
            -
                  expect(described_class.new( | 
| 80 | 
            +
                  expect(described_class.new(choice, 0, 1).next_symbol.name).to eq(:NUMBER)
         | 
| 81 | 
            +
                  expect(described_class.new(choice, 1, 1).next_symbol.name).to eq(:MINUS)
         | 
| 82 | 
            +
                  expect(described_class.new(choice, 2, 1).next_symbol.name).to eq(:NUMBER)
         | 
| 83 | 
            +
                  expect(described_class.new(choice, 3, 1).next_symbol).to be_nil
         | 
| 83 84 |  | 
| 84 | 
            -
                  # Case of an empty  | 
| 85 | 
            -
                  expect(described_class.new( | 
| 85 | 
            +
                  # Case of an empty alternative
         | 
| 86 | 
            +
                  expect(described_class.new(choice, 0, 2).next_symbol).to be_nil
         | 
| 86 87 | 
             
                end
         | 
| 87 88 |  | 
| 88 | 
            -
                it ' | 
| 89 | 
            -
                  expect(described_class.new( | 
| 90 | 
            -
                  expect(described_class.new( | 
| 91 | 
            -
                  expect(described_class.new( | 
| 92 | 
            -
                  expect(described_class.new( | 
| 93 | 
            -
             | 
| 94 | 
            -
                   | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 89 | 
            +
                it 'knows the symbol before the dot (if any)' do
         | 
| 90 | 
            +
                  expect(described_class.new(choice, 0, 1).prev_symbol).to be_nil
         | 
| 91 | 
            +
                  expect(described_class.new(choice, 1, 1).prev_symbol.name).to eq(:NUMBER)
         | 
| 92 | 
            +
                  expect(described_class.new(choice, 2, 1).prev_symbol.name).to eq(:MINUS)
         | 
| 93 | 
            +
                  expect(described_class.new(choice, 3, 1).prev_symbol.name).to eq(:NUMBER)
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  # Case of an empty alternative
         | 
| 96 | 
            +
                  expect(described_class.new(choice, 0, 1).prev_symbol).to be_nil
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                it 'can compare a given symbol to the one expected' do
         | 
| 100 | 
            +
                  expect(described_class.new(choice, 0, 1)).to be_expecting(num_symb)
         | 
| 101 | 
            +
                  expect(described_class.new(choice, 0, 1)).not_to be_expecting(plus_symb)
         | 
| 102 | 
            +
                  expect(described_class.new(choice, 1, 0)).to be_expecting(plus_symb)
         | 
| 103 | 
            +
                  expect(described_class.new(choice, 1, 1)).to be_expecting(minus_symb)
         | 
| 104 | 
            +
                  expect(described_class.new(choice, 2, 0)).to be_expecting(num_symb)
         | 
| 105 | 
            +
                  expect(described_class.new(choice, 3, 1)).not_to be_expecting(num_symb)
         | 
| 106 | 
            +
                  expect(described_class.new(choice, 3, 1)).not_to be_expecting(plus_symb)
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  # Case of an empty alternative
         | 
| 109 | 
            +
                  expect(described_class.new(choice, 0, 2)).not_to be_expecting(num_symb)
         | 
| 110 | 
            +
                  expect(described_class.new(choice, 0, 2)).not_to be_expecting(plus_symb)
         | 
| 99 111 | 
             
                end
         | 
| 100 112 | 
             
              end # context
         | 
| 101 113 | 
             
            end # describe
         | 
| @@ -4,11 +4,10 @@ require_relative '..\..\spec_helper' | |
| 4 4 | 
             
            require_relative '..\..\..\lib\dendroid\syntax\terminal'
         | 
| 5 5 | 
             
            require_relative '..\..\..\lib\dendroid\syntax\non_terminal'
         | 
| 6 6 | 
             
            require_relative '..\..\..\lib\dendroid\syntax\symbol_seq'
         | 
| 7 | 
            -
            require_relative '..\..\..\lib\dendroid\syntax\ | 
| 8 | 
            -
             | 
| 9 | 
            -
            require_relative '..\..\..\lib\dendroid\grm_analysis\choice_items'
         | 
| 7 | 
            +
            require_relative '..\..\..\lib\dendroid\syntax\rule'
         | 
| 8 | 
            +
            require_relative '..\..\..\lib\dendroid\grm_analysis\rule_items'
         | 
| 10 9 |  | 
| 11 | 
            -
            describe Dendroid::GrmAnalysis:: | 
| 10 | 
            +
            describe Dendroid::GrmAnalysis::RuleItems do
         | 
| 12 11 | 
             
              let(:num_symb) { Dendroid::Syntax::Terminal.new('NUMBER') }
         | 
| 13 12 | 
             
              let(:plus_symb) { Dendroid::Syntax::Terminal.new('PLUS') }
         | 
| 14 13 | 
             
              let(:star_symb) { Dendroid::Syntax::Terminal.new('STAR') }
         | 
| @@ -17,8 +16,8 @@ describe Dendroid::GrmAnalysis::ChoiceItems do | |
| 17 16 | 
             
              let(:alt2) { Dendroid::Syntax::SymbolSeq.new([num_symb, star_symb, num_symb]) }
         | 
| 18 17 | 
             
              let(:alt3) { Dendroid::Syntax::SymbolSeq.new([]) }
         | 
| 19 18 | 
             
              subject do
         | 
| 20 | 
            -
                choice = Dendroid::Syntax:: | 
| 21 | 
            -
                choice.extend(Dendroid::GrmAnalysis:: | 
| 19 | 
            +
                choice = Dendroid::Syntax::Rule.new(expr_symb, [alt1, alt2, alt3])
         | 
| 20 | 
            +
                choice.extend(Dendroid::GrmAnalysis::RuleItems)
         | 
| 22 21 | 
             
                choice.build_items
         | 
| 23 22 | 
             
                choice
         | 
| 24 23 | 
             
              end
         |