Home | History | Annotate | Download | only in test
      1 #!/usr/bin/ruby
      2 # encoding: utf-8
      3 
      4 require 'antlr3'
      5 require 'antlr3/test/core-extensions'
      6 require 'antlr3/test/call-stack'
      7 
      8 if RUBY_VERSION =~ /^1\.9/
      9   require 'digest/md5'
     10   MD5 = Digest::MD5
     11 else
     12   require 'md5'
     13 end
     14 
     15 module ANTLR3
     16 module Test
     17 module DependantFile
     18   attr_accessor :path, :force
     19   alias force? force
     20   
     21   GLOBAL_DEPENDENCIES = []
     22   
     23   def dependencies
     24     @dependencies ||= GLOBAL_DEPENDENCIES.clone
     25   end
     26   
     27   def depends_on( path )
     28     path = File.expand_path path.to_s
     29     dependencies << path if test( ?f, path )
     30     return path
     31   end
     32   
     33   def stale?
     34     force and return( true )
     35     target_files.any? do |target|
     36       not test( ?f, target ) or
     37         dependencies.any? { |dep| test( ?>, dep, target ) }
     38     end
     39   end
     40 end # module DependantFile
     41 
     42 class Grammar
     43   include DependantFile
     44 
     45   GRAMMAR_TYPES = %w(lexer parser tree combined)
     46   TYPE_TO_CLASS = { 
     47     'lexer'  => 'Lexer',
     48     'parser' => 'Parser',
     49     'tree'   => 'TreeParser'
     50   }
     51   CLASS_TO_TYPE = TYPE_TO_CLASS.invert
     52 
     53   def self.global_dependency( path )
     54     path = File.expand_path path.to_s
     55     GLOBAL_DEPENDENCIES << path if test( ?f, path )
     56     return path
     57   end
     58   
     59   def self.inline( source, *args )
     60     InlineGrammar.new( source, *args )
     61   end
     62   
     63   ##################################################################
     64   ######## CONSTRUCTOR #############################################
     65   ##################################################################
     66   def initialize( path, options = {} )
     67     @path = path.to_s
     68     @source = File.read( @path )
     69     @output_directory = options.fetch( :output_directory, '.' )
     70     @verbose = options.fetch( :verbose, $VERBOSE )
     71     study
     72     build_dependencies
     73     
     74     yield( self ) if block_given?
     75   end
     76   
     77   ##################################################################
     78   ######## ATTRIBUTES AND ATTRIBUTE-ISH METHODS ####################
     79   ##################################################################
     80   attr_reader :type, :name, :source
     81   attr_accessor :output_directory, :verbose
     82   
     83   def lexer_class_name
     84     self.name + "::Lexer"
     85   end
     86   
     87   def lexer_file_name
     88     if lexer? then base = name
     89     elsif combined? then base = name + 'Lexer'
     90     else return( nil )
     91     end
     92     return( base + '.rb' )
     93   end
     94   
     95   def parser_class_name
     96     name + "::Parser"
     97   end
     98   
     99   def parser_file_name
    100     if parser? then base = name
    101     elsif combined? then base = name + 'Parser'
    102     else return( nil )
    103     end
    104     return( base + '.rb' )
    105   end
    106   
    107   def tree_parser_class_name
    108     name + "::TreeParser"
    109   end
    110 
    111   def tree_parser_file_name
    112     tree? and name + '.rb'
    113   end
    114   
    115   def has_lexer?
    116     @type == 'combined' || @type == 'lexer'
    117   end
    118   
    119   def has_parser?
    120     @type == 'combined' || @type == 'parser'
    121   end
    122   
    123   def lexer?
    124     @type == "lexer"
    125   end
    126   
    127   def parser?
    128     @type == "parser"
    129   end
    130   
    131   def tree?
    132     @type == "tree"
    133   end
    134   
    135   alias has_tree? tree?
    136   
    137   def combined?
    138     @type == "combined"
    139   end
    140   
    141   def target_files( include_imports = true )
    142     targets = []
    143     
    144     for target_type in %w(lexer parser tree_parser)
    145       target_name = self.send( :"#{ target_type }_file_name" ) and
    146         targets.push( output_directory / target_name )
    147     end
    148     
    149     targets.concat( imported_target_files ) if include_imports
    150     return targets
    151   end
    152   
    153   def imports
    154     @source.scan( /^\s*import\s+(\w+)\s*;/ ).
    155       tap { |list| list.flatten! }
    156   end
    157   
    158   def imported_target_files
    159     imports.map! do |delegate|
    160       output_directory / "#{ @name }_#{ delegate }.rb"
    161     end
    162   end
    163 
    164   ##################################################################
    165   ##### COMMAND METHODS ############################################
    166   ##################################################################
    167   def compile( options = {} )
    168     if options[ :force ] or stale?
    169       compile!( options )
    170     end
    171   end
    172   
    173   def compile!( options = {} )
    174     command = build_command( options )
    175     
    176     blab( command )
    177     output = IO.popen( command ) do |pipe|
    178       pipe.read
    179     end
    180     
    181     case status = $?.exitstatus
    182     when 0, 130
    183       post_compile( options )
    184     else compilation_failure!( command, status, output )
    185     end
    186     
    187     return target_files
    188   end
    189   
    190   def clean!
    191     deleted = []
    192     for target in target_files
    193       if test( ?f, target )
    194         File.delete( target )
    195         deleted << target
    196       end
    197     end
    198     return deleted
    199   end
    200   
    201   def inspect
    202     sprintf( "grammar %s (%s)", @name, @path )
    203   end
    204   
    205 private
    206   
    207   def post_compile( options )
    208     # do nothing for now
    209   end
    210   
    211   def blab( string, *args )
    212     $stderr.printf( string + "\n", *args ) if @verbose
    213   end
    214   
    215   def default_antlr_jar
    216     ENV[ 'ANTLR_JAR' ] || ANTLR3.antlr_jar
    217   end
    218   
    219   def compilation_failure!( command, status, output )
    220     for f in target_files
    221       test( ?f, f ) and File.delete( f )
    222     end
    223     raise CompilationFailure.new( self, command, status, output )
    224   end
    225 
    226   def build_dependencies
    227     depends_on( @path )
    228     
    229     if @source =~ /tokenVocab\s*=\s*(\S+)\s*;/
    230       foreign_grammar_name = $1
    231       token_file = output_directory / foreign_grammar_name + '.tokens'
    232       grammar_file = File.dirname( path ) / foreign_grammar_name << '.g'
    233       depends_on( token_file )
    234       depends_on( grammar_file )
    235     end    
    236   end
    237   
    238   def shell_escape( token )
    239     token = token.to_s.dup
    240     token.empty? and return "''"
    241     token.gsub!( /([^A-Za-z0-9_\-.,:\/@\n])/n, '\\\1' )
    242     token.gsub!( /\n/, "'\n'" )
    243     return token
    244   end
    245   
    246   def build_command( options )
    247     parts = %w(java)
    248     jar_path = options.fetch( :antlr_jar, default_antlr_jar )
    249     parts.push( '-cp', jar_path )
    250     parts << 'org.antlr.Tool'
    251     parts.push( '-fo', output_directory )
    252     options[ :profile ] and parts << '-profile'
    253     options[ :debug ]   and parts << '-debug'
    254     options[ :trace ]   and parts << '-trace'
    255     options[ :debug_st ] and parts << '-XdbgST'
    256     parts << File.expand_path( @path )
    257     parts.map! { |part| shell_escape( part ) }.join( ' ' ) << ' 2>&1'
    258   end
    259   
    260   def study
    261     @source =~ /^\s*(lexer|parser|tree)?\s*grammar\s*(\S+)\s*;/ or
    262       raise Grammar::FormatError[ source, path ]
    263     @name = $2
    264     @type = $1 || 'combined'
    265   end
    266 end # class Grammar
    267 
    268 class Grammar::InlineGrammar < Grammar
    269   attr_accessor :host_file, :host_line
    270   
    271   def initialize( source, options = {} )
    272     host = call_stack.find { |call| call.file != __FILE__ }
    273     
    274     @host_file = File.expand_path( options[ :file ] || host.file )
    275     @host_line = ( options[ :line ] || host.line )
    276     @output_directory = options.fetch( :output_directory, File.dirname( @host_file ) )
    277     @verbose = options.fetch( :verbose, $VERBOSE )
    278     
    279     @source = source.to_s.fixed_indent( 0 )
    280     @source.strip!
    281     
    282     study
    283     write_to_disk
    284     build_dependencies
    285     
    286     yield( self ) if block_given?
    287   end
    288   
    289   def output_directory
    290     @output_directory and return @output_directory
    291     File.basename( @host_file )
    292   end
    293   
    294   def path=( v )
    295     previous, @path = @path, v.to_s
    296     previous == @path or write_to_disk
    297   end
    298   
    299   def inspect
    300     sprintf( 'inline grammar %s (%s:%s)', name, @host_file, @host_line )
    301   end
    302   
    303 private
    304   
    305   def write_to_disk
    306     @path ||= output_directory / @name + '.g'
    307     test( ?d, output_directory ) or Dir.mkdir( output_directory )
    308     unless test( ?f, @path ) and MD5.digest( @source ) == MD5.digest( File.read( @path ) )
    309       open( @path, 'w' ) { |f| f.write( @source ) }
    310     end
    311   end
    312 end # class Grammar::InlineGrammar
    313 
    314 class Grammar::CompilationFailure < StandardError
    315   JAVA_TRACE = /^(org\.)?antlr\.\S+\(\S+\.java:\d+\)\s*/
    316   attr_reader :grammar, :command, :status, :output
    317   
    318   def initialize( grammar, command, status, output )
    319     @command = command
    320     @status = status
    321     @output = output.gsub( JAVA_TRACE, '' )
    322     
    323     message = <<-END.here_indent! % [ command, status, grammar, @output ]
    324     | command ``%s'' failed with status %s
    325     | %p
    326     | ~ ~ ~ command output ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
    327     | %s
    328     END
    329     
    330     super( message.chomp! || message )
    331   end
    332 end # error Grammar::CompilationFailure
    333 
    334 class Grammar::FormatError < StandardError
    335   attr_reader :file, :source
    336   
    337   def self.[]( *args )
    338     new( *args )
    339   end
    340   
    341   def initialize( source, file = nil )
    342     @file = file
    343     @source = source
    344     message = ''
    345     if file.nil? # inline
    346       message << "bad inline grammar source:\n"
    347       message << ( "-" * 80 ) << "\n"
    348       message << @source
    349       message[ -1 ] == ?\n or message << "\n"
    350       message << ( "-" * 80 ) << "\n"
    351       message << "could not locate a grammar name and type declaration matching\n"
    352       message << "/^\s*(lexer|parser|tree)?\s*grammar\s*(\S+)\s*;/"
    353     else
    354       message << 'bad grammar source in file %p' % @file
    355       message << ( "-" * 80 ) << "\n"
    356       message << @source
    357       message[ -1 ] == ?\n or message << "\n"
    358       message << ( "-" * 80 ) << "\n"
    359       message << "could not locate a grammar name and type declaration matching\n"
    360       message << "/^\s*(lexer|parser|tree)?\s*grammar\s*(\S+)\s*;/"
    361     end
    362     super( message )
    363   end
    364 end # error Grammar::FormatError
    365 
    366 end
    367 end
    368