summaryrefslogtreecommitdiff
path: root/lib/coderay/state_based_scanner.rb
blob: b196adc9f2740952c57f670085617e119074d30a (plain)
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
require 'set'

module CodeRay
  module Scanners
    class StateBasedScanner < Scanner
      class State
        attr_reader :names
        attr_reader :rules
        attr_reader :scanner

        def initialize scanner, names, &block
          @scanner = scanner
          @names = names

          @rules = []
          @check = nil

          instance_eval(&block)
        end

        def rules_code
          <<-RUBY
when #{names.map(&:inspect).join(', ')}
#{rules.map.with_index { |rule, index| rule.code(first: index.zero?) }.join}
  else
    puts "no match for \#{state.inspect} => skip character" if $DEBUG
    encoder.text_token getch, :error
  end

          RUBY
        end

        protected

        # structure
        def check *conditions, &block
          return @check unless conditions.any? || block
          raise "Can't nest check yet" if @check

          @check = Conditions.new(conditions)
          instance_eval(&block)
          @check = nil
        end

        # rules
        def on pattern, *actions, &block
          @rules << Rule.new(self, pattern, *actions, check: @check, &block)
        end

        def skip pattern, *actions, &block
          @rules << Rule.new(self, pattern, *actions, check: @check, skip: true, &block)
        end

        def otherwise *actions, &block
          @rules << Rule.new(self, //, *actions, check: @check, skip: true, &block)
        end

        # actions
        def push state
          Push.new(state)
        end

        def pop
          Pop.new
        end

        def kind token_kind = nil, &block
          Kind.new token_kind || scanner.callback(block)
        end

        def groups *token_kinds
          Groups.new(token_kinds)
        end

        def set target, value = nil, &block
          Setter.new target, value || block || true
        end

        def callback block
          scanner.callback(block)
        end

        # magic flag getters
        def method_missing method, *args, &block
          method_name = method.to_s
          if method_name.end_with?('?')
            Getter.new(scanner.variable(method_name.chomp('?')))
          else
            super
          end
        end
      end

      class GroupState < State
      end

      class Rule
        attr_reader :pattern
        attr_reader :actions
        attr_reader :check
        attr_reader :state

        def initialize state, pattern, *actions, check:, skip: false, &block
          @state = state
          @pattern = (skip ? Skip : Scan).new(pattern)
          @actions = *build_actions(actions, block)
          @check = check

          raise [pattern, *actions, check, skip, block].inspect if check == false
        end

        def code first:
          <<-RUBI
  #{'els' unless first}if #{condition_expression}
#{actions_code.gsub(/^/, '  ' * 2)}
          RUBI
        end

        def skip?
          @pattern.is_a?(Skip)
        end

        protected

        def condition_expression
          [check, pattern].compact.map(&:code).join(' && ')
        end

        def actions_code
          actions.map(&:code).join("\n")
        end

        def build_actions actions, block
          actions += [block] if block

          actions.map do |action|
            case action
            when Symbol
              Token.new(action)
            when Proc
              state.instance_eval do
                callback action
              end
            when WordList
              state.instance_eval do
                kind { |match| action[match] }
              end
            when Push, Pop, Groups, Kind, Setter
              action
            else
              raise "Don't know how to build action for %p (%p)" % [action, action.class]
            end
          end
        end
      end

      # conditions
      class Conditions < Struct.new(:conditions)
        def code
          "#{conditions.map(&:code).join(' && ')}"
        end
      end

      class Scan < Struct.new(:pattern)
        def code
          "match = scan(#{pattern.inspect})"
        end
      end

      class Skip < Scan
      end

      class Getter < Struct.new(:name, :negative)
        def code
          "#{negative && '!'}#{name}"
        end

        def !@
          negative
        end

        protected

        def negative
          @negative ||= Getter.new(name, :negative)
        end
      end
      
      # actions
      class Push < Struct.new :state
        def code
          "push"
        end
      end

      class Pop < Class.new
        def code
          "pop"
        end
      end

      class Groups < Struct.new(:token_kinds)
        def code
          "groups"
        end
      end

      class Setter < Struct.new(:name, :value)
        def code
          "set"
        end
      end


      class Kind < Struct.new(:token_kind)
        def code
          case token_kind
          when Callback
            "encoder.text_token match, kind = #{token_kind.code}\n"
          else
            raise "I don't know how to evaluate this kind: %p" % [token_kind]
          end
        end
      end

      class Token < Struct.new(:name)
        def code
          "encoder.text_token match, #{name.inspect}"
        end
      end

      class Callback < Struct.new(:name, :block)
        def code
          if parameter_names.empty?
            name
          else
            "#{name}(#{parameter_names.join(', ')})"
          end
        end

        protected

        def parameter_names
          block.parameters.map(&:last)
        end
      end

      class << self
        def states
          @states ||= {}
        end

        def scan_tokens tokens, options
          self.class.define_scan_tokens!

          scan_tokens tokens, options
        end

        def define_scan_tokens!
          if ENV['PUTS']
            puts CodeRay.scan(scan_tokens_code, :ruby).terminal
            puts "callbacks: #{callbacks.size}"
          end
          
          class_eval scan_tokens_code
        end

        def variable name
          variables << name.to_sym

          name
        end

        def callback block
          return unless block

          callback_name = name_for_callback(block)
          callbacks[callback_name] = define_method(callback_name, &block)
          block.parameters.map(&:last).each { |name| variable name }

          Callback.new(callback_name, block)
        end

        protected

        def state *names, state_class: State, &block
          state_class.new(self, names, &block).tap do |state|
            for name in names
              states[name] = state
            end
          end
        end

        def group_state *names, &block
          state(*names, state_class: GroupState, &block)
        end

        def callbacks
          @callbacks ||= {}
        end

        def variables
          @variables ||= Set.new
        end

        def additional_variables
          variables - %i(encoder options state states match kind)
        end

        def name_for_callback block
          base_name = "__callback_line_#{block.source_location.last}"
          callback_name = base_name
          counter = 'a'

          while callbacks.key?(callback_name)
            callback_name = "#{base_name}_#{counter}"
            counter.succ!
          end
          
          callback_name
        end

        def scan_tokens_code
          <<-"RUBY"
    def scan_tokens encoder, options
      state = options[:state] || @state

#{ restore_local_variables_code.chomp.gsub(/^/, '  ' * 3) }

      states = [state]

      until eos?
        case state
#{ states_code.chomp.gsub(/^/, '  ' * 4) }
        else
          raise_inspect 'Unknown state: %p' % [state], encoder
        end
      end

      if options[:keep_state]
        @state = state
      end

#{ close_groups_code.chomp.gsub(/^/, '  ' * 3) }

      encoder
    end
          RUBY
        end

        def states_code
          states.values.map(&:rules_code).join
        end

        def restore_local_variables_code
          additional_variables.sort.map { |name| "#{name} = @#{name}" }.join("\n")
        end

        def close_groups_code
          "close_groups(encoder, states)"
        end
      end

      def scan_tokens tokens, options
        self.class.define_scan_tokens!

        scan_tokens tokens, options
      end

      protected

      def setup
        @state = :initial
        reset_expectations
      end

      def close_groups encoder, states
        # TODO
      end

      def expect kind
        @expected = kind
      end

      def expected? kind
        @expected == kind
      end

      def reset_expectations
        @expected = nil
      end
    end
  end
end