1#!/usr/bin/ruby
2# encoding: utf-8
3
4=begin LICENSE
5[The "BSD licence"]
6Copyright (c) 2009-2010 Kyle Yetter
7All rights reserved.
8
9Redistribution and use in source and binary forms, with or without
10modification, are permitted provided that the following conditions
11are met:
12
13 1. Redistributions of source code must retain the above copyright
14    notice, this list of conditions and the following disclaimer.
15 2. Redistributions in binary form must reproduce the above copyright
16    notice, this list of conditions and the following disclaimer in the
17    documentation and/or other materials provided with the distribution.
18 3. The name of the author may not be used to endorse or promote products
19    derived from this software without specific prior written permission.
20
21THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
22IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32=end
33
34require 'optparse'
35require 'antlr3'
36
37module ANTLR3
38
39=begin rdoc ANTLR3::Main
40
41Namespace module for the quick script Main classes.
42
43=end
44
45module Main
46
47
48=begin rdoc ANTLR3::Main::Options
49
50Defines command-line options and attribute mappings shared by all types of
51Main classes.
52
53=end
54
55module Options
56  # the input encoding type; defaults to +nil+ (currently, not used)
57  attr_accessor :encoding
58  # the input stream used by the Main script; defaults to <tt>$stdin</tt>
59  attr_accessor :input
60  # a boolean flag indicating whether or not to run the Main
61  # script in interactive mode; defaults to +false+
62  attr_accessor :interactive
63  attr_accessor :no_output
64  attr_accessor :profile
65  attr_accessor :debug_socket
66  attr_accessor :ruby_prof
67
68  def initialize( options = {} )
69    @no_output    = options.fetch( :no_output, false )
70    @profile      = options.fetch( :profile, false )
71    @debug_socket = options.fetch( :debug_socket, false )
72    @ruby_prof    = options.fetch( :ruby_prof, false )
73    @encoding     = options.fetch( :encoding, nil )
74    @interactive  = options.fetch( :interactive, false )
75    @input        = options.fetch( :input, $stdin )
76  end
77
78  # constructs an OptionParser and parses the argument list provided by +argv+
79  def parse_options( argv = ARGV )
80    oparser = OptionParser.new do | o |
81      o.separator 'Input Options:'
82
83      o.on( '-i', '--input "text to process"', doc( <<-END ) ) { |val| @input = val }
84      | a string to use as direct input to the recognizer
85      END
86
87      o.on( '-I', '--interactive', doc( <<-END ) ) { @interactive = true }
88      | run an interactive session with the recognizer
89      END
90    end
91
92    setup_options( oparser )
93    return oparser.parse( argv )
94  end
95
96private
97
98  def setup_options( oparser )
99    # overridable hook to modify / append options
100  end
101
102  def doc( description_string )
103    description_string.chomp!
104    description_string.gsub!( /^ *\| ?/, '' )
105    description_string.gsub!( /\s+/, ' ' )
106    return description_string
107  end
108
109end
110
111=begin rdoc ANTLR3::Main::Main
112
113The base-class for the three primary Main script-runner classes.
114It defines the skeletal structure shared by all main
115scripts, but isn't particularly useful on its own.
116
117=end
118
119class Main
120  include Options
121  include Util
122  attr_accessor :output, :error
123
124  def initialize( options = {} )
125    @input  = options.fetch( :input, $stdin )
126    @output = options.fetch( :output, $stdout )
127    @error  = options.fetch( :error, $stderr )
128    @name   = options.fetch( :name, File.basename( $0, '.rb' ) )
129    super
130    block_given? and yield( self )
131  end
132
133
134  # runs the script
135  def execute( argv = ARGV )
136    args = parse_options( argv )
137    setup
138
139    @interactive and return execute_interactive
140
141    in_stream =
142      case
143      when @input.is_a?( ::String ) then StringStream.new( @input )
144      when args.length == 1 && args.first != '-'
145        ANTLR3::FileStream.new( args[ 0 ] )
146      else ANTLR3::FileStream.new( @input )
147      end
148    case
149    when @ruby_prof
150      load_ruby_prof
151      profile = RubyProf.profile do
152        recognize( in_stream )
153      end
154      printer = RubyProf::FlatPrinter.new( profile )
155      printer.print( @output )
156    when @profile
157      require 'profiler'
158      Profiler__.start_profile
159      recognize( in_stream )
160      Profiler__.print_profile
161    else
162      recognize( in_stream )
163    end
164  end
165
166private
167
168  def recognize( *args )
169    # overriden by subclasses
170  end
171
172  def execute_interactive
173    @output.puts( tidy( <<-END ) )
174    | ===================================================================
175    | Ruby ANTLR Console for #{ $0 }
176    | ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
177    | * Enter source code lines
178    | * Enter EOF to finish up and exit
179    |   (control+D on Mac/Linux/Unix or control+Z on Windows)
180    | ===================================================================
181    |
182    END
183
184    read_method =
185      begin
186        require 'readline'
187        line_number = 0
188        lambda do
189          begin
190            if line = Readline.readline( "#@name:#{ line_number += 1 }> ", true )
191              line << $/
192            else
193              @output.print( "\n" ) # ensures result output is on a new line after EOF is entered
194              nil
195            end
196          rescue Interrupt, EOFError
197            retry
198          end
199          line << "\n" if line
200        end
201
202      rescue LoadError
203        lambda do
204          begin
205            printf( "%s:%i> ", @name, @input.lineno )
206            flush
207            line = @input.gets or
208              @output.print( "\n" ) # ensures result output is on a new line after EOF is entered
209            line
210          rescue Interrupt, EOFError
211            retry
212          end
213          line
214        end
215      end
216
217    stream = InteractiveStringStream.new( :name => @name, &read_method )
218    recognize( stream )
219  end
220
221  def screen_width
222    ( ENV[ 'COLUMNS' ] || 80 ).to_i
223  end
224
225  def attempt( lib, message = nil, exit_status = nil )
226    yield
227  rescue LoadError => error
228    message or raise
229    @error.puts( message )
230    report_error( error )
231    report_load_path
232    exit( exit_status ) if exit_status
233  rescue => error
234    @error.puts( "received an error while attempting to load %p" % lib )
235    report_error( error )
236    exit( exit_status ) if exit_status
237  end
238
239  def report_error( error )
240    puts!( "~ error details:" )
241    puts!( '  [ %s ]' % error.class.name )
242    message = error.to_s.gsub( /\n/, "\n     " )
243    puts!( '  -> ' << message )
244    for call in error.backtrace
245      puts!( '     ' << call )
246    end
247  end
248
249  def report_load_path
250    puts!( "~ content of $LOAD_PATH: " )
251    for dir in $LOAD_PATH
252      puts!( "  - #{ dir }" )
253    end
254  end
255
256  def setup
257    # hook
258  end
259
260  def fetch_class( name )
261    name.nil? || name.empty? and return( nil )
262    unless constant_exists?( name )
263      try_to_load( name )
264      constant_exists?( name ) or return( nil )
265    end
266
267    name.split( /::/ ).inject( Object ) do |mod, name|
268      # ::SomeModule splits to ['', 'SomeModule'] - so ignore empty strings
269      name.empty? and next( mod )
270      mod.const_get( name )
271    end
272  end
273
274  def constant_exists?( name )
275    eval( "defined?(#{ name })" ) == 'constant'
276  end
277
278  def try_to_load( name )
279    if name =~ /(\w+)::(Lexer|Parser|TreeParser)$/
280      retry_ok = true
281      module_name, recognizer_type = $1, $2
282      script = name.gsub( /::/, '' )
283      begin
284        return( require( script ) )
285      rescue LoadError
286        if retry_ok
287          script, retry_ok = module_name, false
288          retry
289        else
290          return( nil )
291        end
292      end
293    end
294  end
295
296  %w(puts print printf flush).each do |method|
297    class_eval( <<-END, __FILE__, __LINE__ )
298      private
299
300      def #{ method }(*args)
301        @output.#{ method }(*args) unless @no_output
302      end
303
304      def #{ method }!( *args )
305        @error.#{ method }(*args) unless @no_output
306      end
307    END
308  end
309end
310
311
312=begin rdoc ANTLR3::Main::LexerMain
313
314A class which implements a handy test script which is executed whenever an ANTLR
315generated lexer file is run directly from the command line.
316
317=end
318class LexerMain < Main
319  def initialize( lexer_class, options = {} )
320    super( options )
321    @lexer_class = lexer_class
322  end
323
324  def recognize( in_stream )
325    lexer = @lexer_class.new( in_stream )
326
327    loop do
328      begin
329        token = lexer.next_token
330        if token.nil? || token.type == ANTLR3::EOF then break
331        else display_token( token )
332        end
333      rescue ANTLR3::RecognitionError => error
334        report_error( error )
335        break
336      end
337    end
338  end
339
340  def display_token( token )
341    case token.channel
342    when ANTLR3::DEFAULT_CHANNEL
343      prefix = '-->'
344      suffix = ''
345    when ANTLR3::HIDDEN_CHANNEL
346      prefix = '#  '
347      suffix = ' (hidden)'
348    else
349      prefix = '~~>'
350      suffix = ' (channel %p)' % token.channel
351    end
352
353    printf( "%s %-15s %-15p @ line %-3i col %-3i%s\n",
354           prefix, token.name, token.text,
355           token.line, token.column, suffix )
356  end
357
358end
359
360=begin rdoc ANTLR3::Main::ParserMain
361
362A class which implements a handy test script which is executed whenever an ANTLR
363generated parser file is run directly from the command line.
364
365=end
366class ParserMain < Main
367  attr_accessor :lexer_class_name,
368                :lexer_class,
369                :parser_class,
370                :parser_rule,
371                :port,
372                :log
373
374  def initialize( parser_class, options = {} )
375    super( options )
376    @lexer_class_name = options[ :lexer_class_name ]
377    @lexer_class      = options[ :lexer_class ]
378    @parser_class     = parser_class
379    @parser_rule = options[ :parser_rule ]
380    if @debug = ( @parser_class.debug? rescue false )
381      @trace = options.fetch( :trace, nil )
382      @port = options.fetch( :port, ANTLR3::Debug::DEFAULT_PORT )
383      @log  = options.fetch( :log, @error )
384    end
385    @profile = ( @parser_class.profile? rescue false )
386  end
387
388  def setup_options( opt )
389    super
390
391    opt.separator ""
392    opt.separator( "Parser Configuration:" )
393
394    opt.on( '--lexer-name CLASS_NAME', "name of the lexer class to use" ) { |val|
395      @lexer_class_name = val
396      @lexer_class = nil
397    }
398
399    opt.on( '--lexer-file PATH_TO_LIBRARY', "path to library defining the lexer class" ) { |val|
400      begin
401        test( ?f, val ) ? load( val ) : require( val )
402      rescue LoadError
403        warn( "unable to load the library specified by --lexer-file: #{ $! }" )
404      end
405    }
406
407    opt.on( '--rule NAME', "name of the parser rule to execute" ) { |val| @parser_rule = val }
408
409    if @debug
410      opt.separator ''
411      opt.separator "Debug Mode Options:"
412
413      opt.on( '--trace', '-t', "print rule trace instead of opening a debug socket" ) do
414        @trace = true
415      end
416
417      opt.on( '--port NUMBER', Integer, "port number to use for the debug socket" ) do |number|
418        @port = number
419      end
420
421      opt.on( '--log PATH', "path of file to use to record socket activity",
422             "(stderr by default)" ) do |path|
423        @log = open( path, 'w' )
424      end
425    end
426  end
427
428  def setup
429    unless @lexer_class ||= fetch_class( @lexer_class_name )
430      if @lexer_class_name
431        fail( "unable to locate the lexer class ``#@lexer_class_name''" )
432      else
433        unless @lexer_class = @parser_class.associated_lexer
434          fail( doc( <<-END ) )
435          | no lexer class has been specified with the --lexer-name option
436          | and #@parser_class does not appear to have an associated
437          | lexer class
438          END
439        end
440      end
441    end
442    @parser_rule ||= @parser_class.default_rule or
443      fail( "a parser rule name must be specified via --rule NAME" )
444  end
445
446  def recognize( in_stream )
447    parser_options = {}
448    if @debug
449      if @trace
450        parser_options[ :debug_listener ] = ANTLR3::Debug::RuleTracer.new
451      else
452        parser_options[ :port ] = @port
453        parser_options[ :log ]  = @log
454      end
455    end
456    lexer = @lexer_class.new( in_stream )
457    # token_stream = CommonTokenStream.new( lexer )
458    parser = @parser_class.new( lexer, parser_options )
459    result = parser.send( @parser_rule ) and present( result )
460    @profile and puts( parser.generate_report )
461  end
462
463  def present( return_value )
464    ASTBuilder > @parser_class and return_value = return_value.tree
465    if return_value
466      text =
467        begin
468          require 'pp'
469          return_value.pretty_inspect
470        rescue LoadError, NoMethodError
471          return_value.inspect
472        end
473      puts( text )
474    end
475  end
476
477end
478
479=begin rdoc ANTLR3::Main::WalkerMain
480
481A class which implements a handy test script which is executed whenever an ANTLR
482generated tree walker (tree parser) file is run directly from the command line.
483
484=end
485
486class WalkerMain < Main
487  attr_accessor :walker_class, :lexer_class, :parser_class
488
489  def initialize( walker_class, options = {} )
490    super( options )
491    @walker_class = walker_class
492    @lexer_class_name = options[ :lexer_class_name ]
493    @lexer_class  = options[ :lexer_class ]
494    @parser_class_name = options[ :parser_class_name ]
495    @parser_class = options[ :parser_class ]
496    if @debug = ( @parser_class.debug? rescue false )
497      @port = options.fetch( :port, ANTLR3::Debug::DEFAULT_PORT )
498      @log  = options.fetch( :log, @error )
499    end
500  end
501
502  def setup_options( opt )
503    super
504
505    opt.separator ''
506    opt.separator "Tree Parser Configuration:"
507
508    opt.on( '--lexer-name CLASS_NAME', 'full name of the lexer class to use' ) { |val| @lexer_class_name = val }
509    opt.on(
510      '--lexer-file PATH_TO_LIBRARY',
511      'path to load to make the lexer class available'
512    ) { |val|
513      begin
514        test( ?f, val ) ? load( val ) : require( val )
515      rescue LoadError
516        warn( "unable to load the library `#{ val }' specified by --lexer-file: #{ $! }" )
517      end
518    }
519
520    opt.on(
521      '--parser-name CLASS_NAME',
522      'full name of the parser class to use'
523    ) { |val| @parser_class_name = val }
524    opt.on(
525      '--parser-file PATH_TO_LIBRARY',
526      'path to load to make the parser class available'
527    ) { |val|
528      begin
529        test( ?f, val ) ? load( val ) : require( val )
530      rescue LoadError
531        warn( "unable to load the library specified by --parser-file: #{ $! }" )
532      end
533    }
534
535    opt.on( '--parser-rule NAME', "name of the parser rule to use on the input" ) { |val| @parser_rule = val }
536    opt.on( '--rule NAME', "name of the rule to invoke in the tree parser" ) { |val| @walker_rule = val }
537
538    if @debug
539      opt.separator ''
540      opt.separator "Debug Mode Options:"
541
542      opt.on( '--port NUMBER', Integer, "port number to use for the debug socket" ) do |number|
543        @port = number
544      end
545      opt.on( '--log PATH', "path of file to use to record socket activity",
546             "(stderr by default)" ) do |path|
547        @log = open( path, 'w' )
548      end
549    end
550  end
551
552  # TODO: finish the Main modules
553  def setup
554    unless @lexer_class ||= fetch_class( @lexer_class_name )
555      fail( "unable to locate the lexer class #@lexer_class_name" )
556    end
557    unless @parser_class ||= fetch_class( @parser_class_name )
558      fail( "unable to locate the parser class #@parser_class_name" )
559    end
560  end
561
562  def recognize( in_stream )
563    walker_options = {}
564    if @debug
565      walker_options[ :port ] = @port
566      walker_options[ :log ] = @log
567    end
568    @lexer = @lexer_class.new( in_stream )
569    @token_stream = ANTLR3::CommonTokenStream.new( @lexer )
570    @parser = @parser_class.new( @token_stream )
571    if result = @parser.send( @parser_rule )
572      result.respond_to?( :tree ) or fail( "Parser did not return an AST for rule #@parser_rule" )
573      @node_stream = ANTLR3::CommonTreeNodeStream.new( result.tree )
574      @node_stream.token_stream = @token_stream
575      @walker = @walker_class.new( @node_stream, walker_options )
576      if result = @walker.send( @walker_rule )
577        out = result.tree.inspect rescue result.inspect
578        puts( out )
579      else puts!( "walker.#@walker_rule returned nil" )
580      end
581    else puts!( "parser.#@parser_rule returned nil" )
582    end
583  end
584end
585end
586end
587