dendroid 0.0.5 → 0.0.6
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/.rubocop.yml +13 -1
- data/CHANGELOG.md +5 -0
- data/lib/dendroid/syntax/grammar.rb +275 -0
- data/spec/dendroid/syntax/grammar_spec.rb +205 -0
- data/version.txt +1 -1
- metadata +5 -3
- /data/lib/{dendroid/dendroid.rb → dendroid.rb} +0 -0
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: a762fb52e0c8ff6116f41f481b5cb7346b99050d90f77c4e133da37a31dda3b3
         | 
| 4 | 
            +
              data.tar.gz: ace142c5221c038eedaab8ea9882a3fbffa34cb424be08083ee722e9e5d8c6ca
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 2746b3e1cd03e07e0045d7e41b54ed4c8a2f003df89ea45166fa0b66013f37aa9c1cf0044adc71bbe75d5958d99334b7f612d15c116954fe451f8e1e6756bfaa
         | 
| 7 | 
            +
              data.tar.gz: 4917fd27358d80719b722c14a6df8636917ad827dfc29fb8201920b068393dba09dd2b9750d8b5946031a1d821bd6922c84dd27572b4a635e709faa4bfe31ceb
         | 
    
        data/.rubocop.yml
    CHANGED
    
    | @@ -2,9 +2,21 @@ Layout/EndOfLine: | |
| 2 2 | 
             
              Enabled: true
         | 
| 3 3 | 
             
              EnforcedStyle: lf
         | 
| 4 4 |  | 
| 5 | 
            +
            Metrics/AbcSize:
         | 
| 6 | 
            +
              Enabled: true
         | 
| 7 | 
            +
              Max: 25
         | 
| 8 | 
            +
             | 
| 5 9 | 
             
            Metrics/BlockLength:
         | 
| 6 10 | 
             
              Enabled: true
         | 
| 7 | 
            -
              Max:  | 
| 11 | 
            +
              Max: 75
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            Metrics/ClassLength:
         | 
| 14 | 
            +
              Enabled: true
         | 
| 15 | 
            +
              Max: 200
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            Metrics/MethodLength:
         | 
| 18 | 
            +
              Enabled: true
         | 
| 19 | 
            +
              Max: 20
         | 
| 8 20 |  | 
| 9 21 | 
             
            Naming/MethodParameterName:
         | 
| 10 22 | 
             
              Enabled: false
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    
| @@ -0,0 +1,275 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'set'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Dendroid
         | 
| 6 | 
            +
              module Syntax
         | 
| 7 | 
            +
                # A grammar specifies the syntax of a language.
         | 
| 8 | 
            +
                #   Formally, a grammar has:
         | 
| 9 | 
            +
                #   * One start symbol,
         | 
| 10 | 
            +
                #   * One or more other production rules,
         | 
| 11 | 
            +
                #   * Each production has a rhs that is a sequence of grammar symbols.
         | 
| 12 | 
            +
                #   * Grammar symbols are categorized into:
         | 
| 13 | 
            +
                #     -terminal symbols
         | 
| 14 | 
            +
                #     -non-terminal symbols
         | 
| 15 | 
            +
                class Grammar
         | 
| 16 | 
            +
                  # The list of grammar symbols in the language.
         | 
| 17 | 
            +
                  # @return [Array<Dendroid::Syntax::GrmSymbol>] The terminal and non-terminal symbols.
         | 
| 18 | 
            +
                  attr_reader :symbols
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  # The list of production rules for the language.
         | 
| 21 | 
            +
                  # @return [Array<Dendroid::Syntax::Rule>] Array of rules for the grammar.
         | 
| 22 | 
            +
                  attr_reader :rules
         | 
| 23 | 
            +
             | 
| 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 | 
            +
                  # A Hash that maps symbol names to their grammar symbols
         | 
| 30 | 
            +
                  # @return [Hash{Dendroid::Syntax::GrmSymbol => Dendroid::Syntax::Rule}]
         | 
| 31 | 
            +
                  attr_reader :nonterm2productions
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  # Constructor.
         | 
| 34 | 
            +
                  # @param terminals [Array<Dendroid::Syntax::Terminal>]
         | 
| 35 | 
            +
                  def initialize(terminals)
         | 
| 36 | 
            +
                    @symbols = []
         | 
| 37 | 
            +
                    @name2symbol = {}
         | 
| 38 | 
            +
                    add_terminals(terminals)
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  # Add a rule to the grammar
         | 
| 42 | 
            +
                  # @param rule [Dendroid::Syntax::Rule]
         | 
| 43 | 
            +
                  def add_rule(rule)
         | 
| 44 | 
            +
                    if @rules.nil?
         | 
| 45 | 
            +
                      @rules = []
         | 
| 46 | 
            +
                      @nonterm2productions = {}
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                    # TODO: add test for duplicate productions
         | 
| 49 | 
            +
                    if nonterm2productions[rule.head]&.include? rule
         | 
| 50 | 
            +
                      raise StandardError, "Production rule '#{production}' appears more than once in the grammar."
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                    add_symbol(rule.head)
         | 
| 53 | 
            +
                    rule.nonterminals.each { |nonterm| add_symbol(nonterm) }
         | 
| 54 | 
            +
                    rules << rule
         | 
| 55 | 
            +
                    nonterm2productions[rule.head] = [] unless nonterm2productions.include? rule.head
         | 
| 56 | 
            +
                    nonterm2productions[rule.head] << rule
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  # Return the start symbol for the language
         | 
| 60 | 
            +
                  # @return [Dendroid::Syntax::NonTerminal]
         | 
| 61 | 
            +
                  def start_symbol
         | 
| 62 | 
            +
                    rules.first.lhs
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  # A event method to notify the grammar that all grammar rules
         | 
| 66 | 
            +
                  # have been entered. The grammar, in turn, reacts by validating the
         | 
| 67 | 
            +
                  # production rules.
         | 
| 68 | 
            +
                  def complete!
         | 
| 69 | 
            +
                    validate
         | 
| 70 | 
            +
                    analyze
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  private
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  # rubocop: disable Metrics/AbcSize
         | 
| 76 | 
            +
                  # rubocop: disable Metrics/BlockNesting
         | 
| 77 | 
            +
                  # rubocop: disable Metrics/MethodLength
         | 
| 78 | 
            +
                  # rubocop: disable Metrics/PerceivedComplexity
         | 
| 79 | 
            +
                  def add_terminals(terminals)
         | 
| 80 | 
            +
                    terminals.each { |term| add_symbol(term) }
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  def add_symbol(symb)
         | 
| 84 | 
            +
                    return if name2symbol.include? symb.name
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                    symbols.push(symb)
         | 
| 87 | 
            +
                    name2symbol[symb.name] = symb
         | 
| 88 | 
            +
                    name2symbol[symb.name.to_s] = symb
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  def validate
         | 
| 92 | 
            +
                    at_least_one_terminal
         | 
| 93 | 
            +
                    are_terminals_referenced?
         | 
| 94 | 
            +
                    are_nonterminals_rewritten?
         | 
| 95 | 
            +
                    are_symbols_productive?
         | 
| 96 | 
            +
                    are_symbols_reachable?
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  def analyze
         | 
| 100 | 
            +
                    mark_nullable_symbols
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                  # Does the grammar contain at least one terminal symbol?
         | 
| 104 | 
            +
                  def at_least_one_terminal
         | 
| 105 | 
            +
                    found = symbols.any?(&:terminal?)
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    unless found
         | 
| 108 | 
            +
                      err_msg = "Grammar doesn't contain any terminal symbol."
         | 
| 109 | 
            +
                      raise StandardError, err_msg
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                  # Does every terminal symbol appear at least once
         | 
| 114 | 
            +
                  # in a rhs of a production rule?
         | 
| 115 | 
            +
                  def are_terminals_referenced?
         | 
| 116 | 
            +
                    all_terminals = Set.new(symbols.select(&:terminal?))
         | 
| 117 | 
            +
                    terms_in_rhs = rules.reduce(Set.new) do |collected, prd|
         | 
| 118 | 
            +
                      found = prd.terminals
         | 
| 119 | 
            +
                      collected.merge(found)
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
                    check_ok = all_terminals == terms_in_rhs
         | 
| 122 | 
            +
                    unless check_ok
         | 
| 123 | 
            +
                      unused_terms = all_terminals.difference(terms_in_rhs)
         | 
| 124 | 
            +
                      text = unused_terms.map(&:name).join("', '")
         | 
| 125 | 
            +
                      err_msg = "Terminal symbols '#{text}' never appear in production rules."
         | 
| 126 | 
            +
                      raise StandardError, err_msg
         | 
| 127 | 
            +
                    end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                    check_ok
         | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                  def are_nonterminals_rewritten?
         | 
| 133 | 
            +
                    all_nonterminals = Set.new(symbols.reject(&:terminal?))
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                    symbs_in_lhs = rules.reduce(Set.new) do |collected, prd|
         | 
| 136 | 
            +
                      collected.add(prd.head)
         | 
| 137 | 
            +
                    end
         | 
| 138 | 
            +
                    check_ok = all_nonterminals == symbs_in_lhs
         | 
| 139 | 
            +
                    unless check_ok
         | 
| 140 | 
            +
                      undefined_nterms = all_nonterminals.difference(symbs_in_lhs)
         | 
| 141 | 
            +
                      text = undefined_nterms.map(&:name).join("', '")
         | 
| 142 | 
            +
                      err_msg = "Non-terminal symbols '#{text}' never appear in head of any production rule."
         | 
| 143 | 
            +
                      raise StandardError, err_msg
         | 
| 144 | 
            +
                    end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                    check_ok
         | 
| 147 | 
            +
                  end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                  def are_symbols_reachable?
         | 
| 150 | 
            +
                    unreachable = unreachable_symbols
         | 
| 151 | 
            +
                    return true if unreachable.empty?
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                    text = unreachable.to_a.map(&:name).join("', '")
         | 
| 154 | 
            +
                    err_msg = "Symbols '#{text}' are unreachable from start symbol."
         | 
| 155 | 
            +
                    raise StandardError, err_msg
         | 
| 156 | 
            +
                  end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                  def are_symbols_productive?
         | 
| 159 | 
            +
                    non_productive = mark_non_productive_symbols
         | 
| 160 | 
            +
                    return true if non_productive.empty?
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                    text = non_productive.to_a.map(&:name).join("', '")
         | 
| 163 | 
            +
                    err_msg = "Symbols '#{text}' are non-productive."
         | 
| 164 | 
            +
                    raise StandardError, err_msg
         | 
| 165 | 
            +
                  end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  # Are all symbols reachable from start symbol?
         | 
| 168 | 
            +
                  def unreachable_symbols
         | 
| 169 | 
            +
                    backlog = [start_symbol]
         | 
| 170 | 
            +
                    set_reachable = Set.new(backlog.dup)
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                    begin
         | 
| 173 | 
            +
                      reachable_sym = backlog.pop
         | 
| 174 | 
            +
                      prods = nonterm2productions[reachable_sym]
         | 
| 175 | 
            +
                      prods.each do |prd|
         | 
| 176 | 
            +
                        # prd.body.members.each do |member|
         | 
| 177 | 
            +
                        prd.rhs_symbols.each do |member|
         | 
| 178 | 
            +
                          unless member.terminal? || set_reachable.include?(member)
         | 
| 179 | 
            +
                            backlog.push(member)
         | 
| 180 | 
            +
                          end
         | 
| 181 | 
            +
                          set_reachable.add(member)
         | 
| 182 | 
            +
                        end
         | 
| 183 | 
            +
                      end
         | 
| 184 | 
            +
                    end until backlog.empty?
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                    all_symbols = Set.new(symbols)
         | 
| 187 | 
            +
                    unreachable = all_symbols - set_reachable
         | 
| 188 | 
            +
                  end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                  def mark_non_productive_symbols
         | 
| 191 | 
            +
                    prod_count = rules.size
         | 
| 192 | 
            +
                    backlog = Set.new(0...prod_count)
         | 
| 193 | 
            +
                    rules.each_with_index do |prd, i|
         | 
| 194 | 
            +
                      backlog.delete(i) if prd.productive?
         | 
| 195 | 
            +
                    end
         | 
| 196 | 
            +
                    until backlog.empty?
         | 
| 197 | 
            +
                      size_before = backlog.size
         | 
| 198 | 
            +
                      to_remove = []
         | 
| 199 | 
            +
                      backlog.each do |i|
         | 
| 200 | 
            +
                        prd = rules[i]
         | 
| 201 | 
            +
                        to_remove << i if prd.productive?
         | 
| 202 | 
            +
                      end
         | 
| 203 | 
            +
                      break if to_remove.empty?
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                      backlog.subtract(to_remove)
         | 
| 206 | 
            +
                    end
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                    backlog.each { |i| rules[i].non_productive }
         | 
| 209 | 
            +
                    non_productive = symbols.reject(&:productive?)
         | 
| 210 | 
            +
                    non_productive.each { |symb| symb.productive = false }
         | 
| 211 | 
            +
                    non_productive
         | 
| 212 | 
            +
                  end
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                  def mark_nullable_symbols
         | 
| 215 | 
            +
                    nullable_found = false
         | 
| 216 | 
            +
                    sym2seqs = {}
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                    nonterm2productions.each_pair do |sym, prods|
         | 
| 219 | 
            +
                      if prods.any?(&:empty?)
         | 
| 220 | 
            +
                        sym.nullable = nullable_found = true
         | 
| 221 | 
            +
                      else
         | 
| 222 | 
            +
                        sym2seqs[sym] = prods.map(&:rhs).flatten
         | 
| 223 | 
            +
                      end
         | 
| 224 | 
            +
                    end
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                    if nullable_found
         | 
| 227 | 
            +
                      backlog = {} # { SymbolSequence => [Integer, Symbol] }
         | 
| 228 | 
            +
                      sym2seqs.each do |sym, seqs|
         | 
| 229 | 
            +
                        seqs.each { |sq| backlog[sq] = [0, sym] }
         | 
| 230 | 
            +
                      end
         | 
| 231 | 
            +
             | 
| 232 | 
            +
                      begin
         | 
| 233 | 
            +
                        seqs_done = []
         | 
| 234 | 
            +
                        backlog.each_pair do |sq, (elem_index, lhs)|
         | 
| 235 | 
            +
                          member = sq.members[elem_index]
         | 
| 236 | 
            +
                          if member.terminal?
         | 
| 237 | 
            +
                            seqs_done << sq # stop with this sequence: it is non-nullable
         | 
| 238 | 
            +
                            backlog[sq] = [-1, lhs]
         | 
| 239 | 
            +
                          elsif member.nullable?
         | 
| 240 | 
            +
                            if elem_index == sq.size - 1
         | 
| 241 | 
            +
                              seqs_done << sq # end of sequence reached...
         | 
| 242 | 
            +
                              backlog[sq] = [-1, lhs]
         | 
| 243 | 
            +
                              lhs.nullable = true
         | 
| 244 | 
            +
                            else
         | 
| 245 | 
            +
                              backlog[sq] = [elem_index + 1, lhs]
         | 
| 246 | 
            +
                            end
         | 
| 247 | 
            +
                          end
         | 
| 248 | 
            +
                        end
         | 
| 249 | 
            +
                        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
         | 
| 258 | 
            +
                          end
         | 
| 259 | 
            +
                        end
         | 
| 260 | 
            +
                      end until backlog.empty? || seqs_done.empty?
         | 
| 261 | 
            +
                    end
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                    symbols.each do |sym|
         | 
| 264 | 
            +
                      next if sym.terminal?
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                      sym.nullable = false if sym.nullable.nil?
         | 
| 267 | 
            +
                    end
         | 
| 268 | 
            +
                  end
         | 
| 269 | 
            +
                  # rubocop: enable Metrics/AbcSize
         | 
| 270 | 
            +
                  # rubocop: enable Metrics/BlockNesting
         | 
| 271 | 
            +
                  # rubocop: enable Metrics/MethodLength
         | 
| 272 | 
            +
                  # rubocop: enable Metrics/PerceivedComplexity
         | 
| 273 | 
            +
                end # class
         | 
| 274 | 
            +
              end # module
         | 
| 275 | 
            +
            end # module
         | 
| @@ -0,0 +1,205 @@ | |
| 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\syntax\choice'
         | 
| 9 | 
            +
            require_relative '..\..\..\lib\dendroid\syntax\grammar'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            describe Dendroid::Syntax::Grammar do
         | 
| 12 | 
            +
              let(:int_symb) { build_terminal('INTEGER') }
         | 
| 13 | 
            +
              let(:plus_symb) { build_terminal('PLUS') }
         | 
| 14 | 
            +
              let(:star_symb) { build_terminal('STAR') }
         | 
| 15 | 
            +
              let(:p_symb) { build_nonterminal('p') }
         | 
| 16 | 
            +
              let(:s_symb) { build_nonterminal('s') }
         | 
| 17 | 
            +
              let(:m_symb) { build_nonterminal('m') }
         | 
| 18 | 
            +
              let(:t_symb) { build_nonterminal('t') }
         | 
| 19 | 
            +
              let(:all_terminals) { [int_symb, plus_symb, star_symb] }
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              subject { described_class.new(all_terminals) }
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              def build_terminal(name)
         | 
| 24 | 
            +
                Dendroid::Syntax::Terminal.new(name)
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              def build_nonterminal(name)
         | 
| 28 | 
            +
                Dendroid::Syntax::NonTerminal.new(name)
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              def build_symbol_seq(symbols)
         | 
| 32 | 
            +
                Dendroid::Syntax::SymbolSeq.new(symbols)
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              def build_production(lhs, symbols)
         | 
| 36 | 
            +
                Dendroid::Syntax::Production.new(lhs, build_symbol_seq(symbols))
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              def build_choice(lhs, sequences)
         | 
| 40 | 
            +
                Dendroid::Syntax::Choice.new(lhs, sequences.map { |arr| build_symbol_seq(arr) })
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              # rubocop: disable Metrics/AbcSize
         | 
| 44 | 
            +
              def build_all_rules
         | 
| 45 | 
            +
                rule1 = build_production(p_symb, [s_symb]) # p => s
         | 
| 46 | 
            +
                rule2 = build_choice(s_symb, [[s_symb, plus_symb, m_symb], [m_symb]]) # s => s + m | m
         | 
| 47 | 
            +
                rule3 = build_choice(m_symb, [[m_symb, star_symb, t_symb], [t_symb]]) # m => m * t
         | 
| 48 | 
            +
                rule4 = build_production(t_symb, [int_symb]) # t => INTEGER
         | 
| 49 | 
            +
                [rule1, rule2, rule3, rule4]
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
              # rubocop: enable Metrics/AbcSize
         | 
| 52 | 
            +
             | 
| 53 | 
            +
              context 'Initialization:' do
         | 
| 54 | 
            +
                it 'is initialized with an array of terminal symbols' do
         | 
| 55 | 
            +
                  expect { described_class.new(all_terminals) }.not_to raise_error
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                it 'knows its terminal symbols' do
         | 
| 59 | 
            +
                  expect(subject.symbols).to eq(all_terminals)
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                it 'ignores about productions after initialization' do
         | 
| 63 | 
            +
                  expect(subject.rules).to be_nil
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                it 'maps a terminal name to one GrmSymbol object' do
         | 
| 67 | 
            +
                  expect(subject.name2symbol.values.uniq.size).to eq(all_terminals.size)
         | 
| 68 | 
            +
                  expect(subject.name2symbol.values.size).to eq(2 * all_terminals.size)
         | 
| 69 | 
            +
                  expect(subject.name2symbol[:PLUS]).to eq(plus_symb)
         | 
| 70 | 
            +
                  expect(subject.name2symbol['PLUS']).to eq(plus_symb)
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
              end # context
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              context 'Adding productions:' do
         | 
| 75 | 
            +
                it 'allows the addition of one production rule' do
         | 
| 76 | 
            +
                  rule = build_production(p_symb, [s_symb])
         | 
| 77 | 
            +
                  expect { subject.add_rule(rule) }.not_to raise_error
         | 
| 78 | 
            +
                  expect(subject.rules.size).to eq(1)
         | 
| 79 | 
            +
                  expect(subject.rules.first).to eq(rule)
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                it 'allows the addition of multiple production rules' do
         | 
| 83 | 
            +
                  rules = build_all_rules
         | 
| 84 | 
            +
                  rules.each { |rl| subject.add_rule(rl) }
         | 
| 85 | 
            +
                  expect(subject.rules.size).to eq(4)
         | 
| 86 | 
            +
                  expect(subject.rules.first).to eq(rules.first)
         | 
| 87 | 
            +
                  expect(subject.rules.last).to eq(rules.last)
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                it 'updates the set of symbols when adding production rules' do
         | 
| 91 | 
            +
                  rules = build_all_rules
         | 
| 92 | 
            +
                  rules.each { |rl| subject.add_rule(rl) }
         | 
| 93 | 
            +
                  [p_symb, s_symb, m_symb, t_symb].each do |symb|
         | 
| 94 | 
            +
                    expect(subject.symbols.include?(symb)).to be_truthy
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                it 'maps name of every non-terminal to its related GrmSymbol' do
         | 
| 99 | 
            +
                  rules = build_all_rules
         | 
| 100 | 
            +
                  rules.each { |rl| subject.add_rule(rl) }
         | 
| 101 | 
            +
                  [[:p, p_symb],
         | 
| 102 | 
            +
                   ['p', p_symb],
         | 
| 103 | 
            +
                   [:s, s_symb],
         | 
| 104 | 
            +
                   ['s', s_symb],
         | 
| 105 | 
            +
                   [:m, m_symb],
         | 
| 106 | 
            +
                   ['m', m_symb],
         | 
| 107 | 
            +
                   [:t, t_symb],
         | 
| 108 | 
            +
                   [:t, t_symb]].each do |(name, symb)|
         | 
| 109 | 
            +
                    expect(subject.name2symbol[name]).to eq(symb)
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                it 'maps every non-terminal to its defining productions' do
         | 
| 114 | 
            +
                  rules = build_all_rules
         | 
| 115 | 
            +
                  rules.each { |rl| subject.add_rule(rl) }
         | 
| 116 | 
            +
                  %i[p s m t].each do |symb_name|
         | 
| 117 | 
            +
                    symb = subject.name2symbol[symb_name]
         | 
| 118 | 
            +
                    expected_prods = subject.rules.select { |prd| prd.head == symb }
         | 
| 119 | 
            +
                    related_prods = subject.nonterm2productions[symb]
         | 
| 120 | 
            +
                    expect(related_prods).to eq(expected_prods)
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
              end # context
         | 
| 124 | 
            +
             | 
| 125 | 
            +
              context 'Grammar completion:' do
         | 
| 126 | 
            +
                it 'detects and marks nullable symbols (I)' do
         | 
| 127 | 
            +
                  # Case: grammar without nullable symbols
         | 
| 128 | 
            +
                  rules = build_all_rules
         | 
| 129 | 
            +
                  rules.each { |rl| subject.add_rule(rl) }
         | 
| 130 | 
            +
                  subject.complete!
         | 
| 131 | 
            +
                  expect(subject.symbols.none?(&:nullable?)).to be_truthy
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                it 'detects and marks nullable symbols (II)' do
         | 
| 135 | 
            +
                  # Case: grammar with only nullable symbols
         | 
| 136 | 
            +
                  # Grammar inspired for paper "Practical Earley Parser"
         | 
| 137 | 
            +
                  terminal_a = build_terminal('a')
         | 
| 138 | 
            +
                  nterm_s_prime = build_nonterminal("S'")
         | 
| 139 | 
            +
                  nterm_s = build_nonterminal('S')
         | 
| 140 | 
            +
                  nterm_a = build_nonterminal('A')
         | 
| 141 | 
            +
                  nterm_e = build_nonterminal('E')
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                  instance = described_class.new([terminal_a])
         | 
| 144 | 
            +
                  instance.add_rule(build_production(nterm_s_prime, [nterm_s]))
         | 
| 145 | 
            +
                  instance.add_rule(build_production(nterm_s, [nterm_a, nterm_a, nterm_a, nterm_a]))
         | 
| 146 | 
            +
                  instance.add_rule(build_choice(nterm_a, [[terminal_a], [nterm_e]]))
         | 
| 147 | 
            +
                  instance.add_rule(build_production(nterm_e, []))
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                  instance.complete!
         | 
| 150 | 
            +
                  all_nonterminals = subject.symbols.reject(&:terminal?)
         | 
| 151 | 
            +
                  expect(all_nonterminals.all?(&:nullable?)).to be_truthy
         | 
| 152 | 
            +
                end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                it 'detects unreachable symbols' do
         | 
| 155 | 
            +
                  # Case: grammar without unreachable symbols
         | 
| 156 | 
            +
                  rules = build_all_rules
         | 
| 157 | 
            +
                  rules.each { |rl| subject.add_rule(rl) }
         | 
| 158 | 
            +
                  expect(subject.send(:unreachable_symbols)).to be_empty
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                  # Let add's unreachable symbols
         | 
| 161 | 
            +
                  zed_symb = build_nonterminal('Z')
         | 
| 162 | 
            +
                  question_symb = build_nonterminal('?')
         | 
| 163 | 
            +
                  bad_rule = build_production(zed_symb, [zed_symb, question_symb, int_symb]) # Z => Z ? INTEGER
         | 
| 164 | 
            +
                  subject.add_rule(bad_rule)
         | 
| 165 | 
            +
                  unreachable = subject.send(:unreachable_symbols)
         | 
| 166 | 
            +
                  expect(unreachable).not_to be_empty
         | 
| 167 | 
            +
                  expect(unreachable).to eq(Set.new([zed_symb, question_symb]))
         | 
| 168 | 
            +
                end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                it 'detects non-productive symbols' do
         | 
| 171 | 
            +
                  # Case: grammar without non-productive symbols
         | 
| 172 | 
            +
                  rules = build_all_rules
         | 
| 173 | 
            +
                  rules.each { |rl| subject.add_rule(rl) }
         | 
| 174 | 
            +
                  expect(subject.send(:mark_non_productive_symbols)).to be_empty
         | 
| 175 | 
            +
                  expect(t_symb).to be_productive
         | 
| 176 | 
            +
                  expect(p_symb).to be_productive
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                  # Grammar with non-productive symbols
         | 
| 179 | 
            +
                  term_a = build_terminal('a')
         | 
| 180 | 
            +
                  term_b = build_terminal('b')
         | 
| 181 | 
            +
                  term_c = build_terminal('c')
         | 
| 182 | 
            +
                  term_d = build_terminal('d')
         | 
| 183 | 
            +
                  term_e = build_terminal('e')
         | 
| 184 | 
            +
                  term_f = build_terminal('f')
         | 
| 185 | 
            +
                  nterm_A = build_nonterminal('A')
         | 
| 186 | 
            +
                  nterm_B = build_nonterminal('B')
         | 
| 187 | 
            +
                  nterm_C = build_nonterminal('C')
         | 
| 188 | 
            +
                  nterm_D = build_nonterminal('D')
         | 
| 189 | 
            +
                  nterm_E = build_nonterminal('E')
         | 
| 190 | 
            +
                  nterm_F = build_nonterminal('F')
         | 
| 191 | 
            +
                  nterm_S = build_nonterminal('S')
         | 
| 192 | 
            +
                  instance = described_class.new([term_a, term_b, term_c, term_d, term_e, term_f])
         | 
| 193 | 
            +
                  instance.add_rule(build_choice(nterm_S, [[nterm_A, nterm_B], [nterm_D, nterm_E]]))
         | 
| 194 | 
            +
                  instance.add_rule(build_production(nterm_A, [term_a]))
         | 
| 195 | 
            +
                  instance.add_rule(build_production(nterm_B, [term_b, nterm_C]))
         | 
| 196 | 
            +
                  instance.add_rule(build_production(nterm_C, [term_c]))
         | 
| 197 | 
            +
                  instance.add_rule(build_production(nterm_D, [term_d, nterm_F]))
         | 
| 198 | 
            +
                  instance.add_rule(build_production(nterm_E, [term_e]))
         | 
| 199 | 
            +
                  instance.add_rule(build_production(nterm_F, [term_f, nterm_D]))
         | 
| 200 | 
            +
                  nonproductive = instance.send(:mark_non_productive_symbols)
         | 
| 201 | 
            +
                  expect(nonproductive).not_to be_empty
         | 
| 202 | 
            +
                  expect(nonproductive).to eq([nterm_D, nterm_F])
         | 
| 203 | 
            +
                end
         | 
| 204 | 
            +
              end # context
         | 
| 205 | 
            +
            end # describe
         | 
    
        data/version.txt
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            0.0. | 
| 1 | 
            +
            0.0.6
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: dendroid
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 0.0.6
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Dimitri Geshef
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2023-10- | 
| 11 | 
            +
            date: 2023-10-30 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies: []
         | 
| 13 13 | 
             
            description: WIP. A Ruby implementation of a Earley parser
         | 
| 14 14 | 
             
            email: famished.tiger@yahoo.com
         | 
| @@ -23,8 +23,9 @@ files: | |
| 23 23 | 
             
            - Rakefile
         | 
| 24 24 | 
             
            - bin/dendroid
         | 
| 25 25 | 
             
            - dendroid.gemspec
         | 
| 26 | 
            -
            - lib/dendroid | 
| 26 | 
            +
            - lib/dendroid.rb
         | 
| 27 27 | 
             
            - lib/dendroid/syntax/choice.rb
         | 
| 28 | 
            +
            - lib/dendroid/syntax/grammar.rb
         | 
| 28 29 | 
             
            - lib/dendroid/syntax/grm_symbol.rb
         | 
| 29 30 | 
             
            - lib/dendroid/syntax/non_terminal.rb
         | 
| 30 31 | 
             
            - lib/dendroid/syntax/production.rb
         | 
| @@ -32,6 +33,7 @@ files: | |
| 32 33 | 
             
            - lib/dendroid/syntax/symbol_seq.rb
         | 
| 33 34 | 
             
            - lib/dendroid/syntax/terminal.rb
         | 
| 34 35 | 
             
            - spec/dendroid/syntax/choice_spec.rb
         | 
| 36 | 
            +
            - spec/dendroid/syntax/grammar_spec.rb
         | 
| 35 37 | 
             
            - spec/dendroid/syntax/grm_symbol_spec.rb
         | 
| 36 38 | 
             
            - spec/dendroid/syntax/non_terminal_spec.rb
         | 
| 37 39 | 
             
            - spec/dendroid/syntax/production_spec.rb
         | 
| 
            File without changes
         |