File: parser.rb

package info (click to toggle)
ruby-kpeg 1.3.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 608 kB
  • sloc: ruby: 11,839; makefile: 10
file content (193 lines) | stat: -rw-r--r-- 3,705 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
require 'kpeg/position'

module KPeg
  class Parser < StringScanner
    def initialize(str, grammar, log=false)
      super str

      @grammar = grammar
      # A 2 level hash.
      @memoizations = Hash.new { |h,k| h[k] = {} }

      @failing_offset = nil
      @failing_op = nil
      @log = log
    end

    attr_reader :grammar, :memoizations, :failing_offset
    attr_accessor :failing_op

    include Position

    def switch_grammar(gram)
      begin
        old = @grammar
        @grammar = gram
        yield
      ensure
        @grammar = old
      end
    end

    def fail(op)
      @failing_offset = pos
      @failing_op = op
      return nil
    end

    def expected_string
      case @failing_op
      when Choice
        return Range.new(@failing_op.start, @failing_op.fin)
      when Dot
        return nil
      else
        @failing_op.string
      end
    end

    class LeftRecursive
      def initialize(detected=false)
        @detected = detected
      end

      attr_accessor :detected
    end

    class MemoEntry
      def initialize(ans, pos)
        @ans = ans
        @pos = pos
        @uses = 1
      end

      attr_reader :ans, :pos, :uses

      def inc!
        @uses += 1
      end

      def move!(ans, pos)
        @ans = ans
        @pos = pos
      end
    end

    # Call a rule without memoization
    def invoke(rule)
      rule.op.match(self)
    end

    def apply(rule)
      ans = nil
      if m = @memoizations[rule][pos]
        m.inc!

        self.pos = m.pos
        if m.ans.kind_of? LeftRecursive
          m.ans.detected = true
          if @log
            puts "LR #{rule.name} @ #{self.inspect}"
          end
          return nil
        end

        ans = m.ans
      else
        lr = LeftRecursive.new(false)
        m = MemoEntry.new(lr, pos)
        @memoizations[rule][pos] = m
        start_pos = pos

        if @log
          puts "START #{rule.name} @ #{self.inspect}"
        end

        ans = rule.op.match(self)

        m.move! ans, pos

        # Don't bother trying to grow the left recursion
        # if it's failing straight away (thus there is no seed)
        if ans and lr.detected
          ans = grow_lr(rule, start_pos, m)
        end
      end

      if @log
        if ans
          puts "   OK #{rule.name} @ #{self.inspect}"
        else
          puts " FAIL #{rule.name} @ #{self.inspect}"
        end
      end
      return ans
    end

    def grow_lr(rule, start_pos, m)
      while true
        self.pos = start_pos
        ans = rule.op.match(self)
        return nil unless ans

        break if pos <= m.pos

        m.move! ans, pos
      end

      self.pos = m.pos
      return m.ans
    end

    def failed?
      !!@failing_op
    end

    def parse(name=nil)
      if name
        rule = @grammar.find(name)
        unless rule
          raise "Unknown rule - #{name}"
        end
      else
        rule = @grammar.root
      end

      match = apply rule

      if pos == string.size
        @failing_op = nil
      end

      return match
    end

    def expectation
      error_pos = @failing_offset
      line_no = current_line(error_pos)
      col_no = current_column(error_pos)

      expected = expected_string()

      prefix = nil

      case expected
      when String
        prefix = expected.inspect
      when Range
        prefix = "to be between #{expected.begin} and #{expected.end}"
      when Array
        prefix = "to be one of #{expected.inspect}"
      when nil
        prefix = "anything (no more input)"
      else
        prefix = "unknown"
      end

      return "Expected #{prefix} at line #{line_no}, column #{col_no} (offset #{error_pos})"
    end

  end


end