| #!/usr/bin/ruby |
| # encoding: utf-8 |
| |
| require 'antlr3' |
| require 'antlr3/test/core-extensions' |
| require 'antlr3/test/call-stack' |
| |
| if RUBY_VERSION =~ /^1\.9/ |
| require 'digest/md5' |
| MD5 = Digest::MD5 |
| else |
| require 'md5' |
| end |
| |
| module ANTLR3 |
| module Test |
| module DependantFile |
| attr_accessor :path, :force |
| alias force? force |
| |
| GLOBAL_DEPENDENCIES = [] |
| |
| def dependencies |
| @dependencies ||= GLOBAL_DEPENDENCIES.clone |
| end |
| |
| def depends_on( path ) |
| path = File.expand_path path.to_s |
| dependencies << path if test( ?f, path ) |
| return path |
| end |
| |
| def stale? |
| force and return( true ) |
| target_files.any? do |target| |
| not test( ?f, target ) or |
| dependencies.any? { |dep| test( ?>, dep, target ) } |
| end |
| end |
| end # module DependantFile |
| |
| class Grammar |
| include DependantFile |
| |
| GRAMMAR_TYPES = %w(lexer parser tree combined) |
| TYPE_TO_CLASS = { |
| 'lexer' => 'Lexer', |
| 'parser' => 'Parser', |
| 'tree' => 'TreeParser' |
| } |
| CLASS_TO_TYPE = TYPE_TO_CLASS.invert |
| |
| def self.global_dependency( path ) |
| path = File.expand_path path.to_s |
| GLOBAL_DEPENDENCIES << path if test( ?f, path ) |
| return path |
| end |
| |
| def self.inline( source, *args ) |
| InlineGrammar.new( source, *args ) |
| end |
| |
| ################################################################## |
| ######## CONSTRUCTOR ############################################# |
| ################################################################## |
| def initialize( path, options = {} ) |
| @path = path.to_s |
| @source = File.read( @path ) |
| @output_directory = options.fetch( :output_directory, '.' ) |
| @verbose = options.fetch( :verbose, $VERBOSE ) |
| study |
| build_dependencies |
| |
| yield( self ) if block_given? |
| end |
| |
| ################################################################## |
| ######## ATTRIBUTES AND ATTRIBUTE-ISH METHODS #################### |
| ################################################################## |
| attr_reader :type, :name, :source |
| attr_accessor :output_directory, :verbose |
| |
| def lexer_class_name |
| self.name + "::Lexer" |
| end |
| |
| def lexer_file_name |
| if lexer? then base = name |
| elsif combined? then base = name + 'Lexer' |
| else return( nil ) |
| end |
| return( base + '.rb' ) |
| end |
| |
| def parser_class_name |
| name + "::Parser" |
| end |
| |
| def parser_file_name |
| if parser? then base = name |
| elsif combined? then base = name + 'Parser' |
| else return( nil ) |
| end |
| return( base + '.rb' ) |
| end |
| |
| def tree_parser_class_name |
| name + "::TreeParser" |
| end |
| |
| def tree_parser_file_name |
| tree? and name + '.rb' |
| end |
| |
| def has_lexer? |
| @type == 'combined' || @type == 'lexer' |
| end |
| |
| def has_parser? |
| @type == 'combined' || @type == 'parser' |
| end |
| |
| def lexer? |
| @type == "lexer" |
| end |
| |
| def parser? |
| @type == "parser" |
| end |
| |
| def tree? |
| @type == "tree" |
| end |
| |
| alias has_tree? tree? |
| |
| def combined? |
| @type == "combined" |
| end |
| |
| def target_files( include_imports = true ) |
| targets = [] |
| |
| for target_type in %w(lexer parser tree_parser) |
| target_name = self.send( :"#{ target_type }_file_name" ) and |
| targets.push( output_directory / target_name ) |
| end |
| |
| targets.concat( imported_target_files ) if include_imports |
| return targets |
| end |
| |
| def imports |
| @source.scan( /^\s*import\s+(\w+)\s*;/ ). |
| tap { |list| list.flatten! } |
| end |
| |
| def imported_target_files |
| imports.map! do |delegate| |
| output_directory / "#{ @name }_#{ delegate }.rb" |
| end |
| end |
| |
| ################################################################## |
| ##### COMMAND METHODS ############################################ |
| ################################################################## |
| def compile( options = {} ) |
| if options[ :force ] or stale? |
| compile!( options ) |
| end |
| end |
| |
| def compile!( options = {} ) |
| command = build_command( options ) |
| |
| blab( command ) |
| output = IO.popen( command ) do |pipe| |
| pipe.read |
| end |
| |
| case status = $?.exitstatus |
| when 0, 130 |
| post_compile( options ) |
| else compilation_failure!( command, status, output ) |
| end |
| |
| return target_files |
| end |
| |
| def clean! |
| deleted = [] |
| for target in target_files |
| if test( ?f, target ) |
| File.delete( target ) |
| deleted << target |
| end |
| end |
| return deleted |
| end |
| |
| def inspect |
| sprintf( "grammar %s (%s)", @name, @path ) |
| end |
| |
| private |
| |
| def post_compile( options ) |
| # do nothing for now |
| end |
| |
| def blab( string, *args ) |
| $stderr.printf( string + "\n", *args ) if @verbose |
| end |
| |
| def default_antlr_jar |
| ENV[ 'ANTLR_JAR' ] || ANTLR3.antlr_jar |
| end |
| |
| def compilation_failure!( command, status, output ) |
| for f in target_files |
| test( ?f, f ) and File.delete( f ) |
| end |
| raise CompilationFailure.new( self, command, status, output ) |
| end |
| |
| def build_dependencies |
| depends_on( @path ) |
| |
| if @source =~ /tokenVocab\s*=\s*(\S+)\s*;/ |
| foreign_grammar_name = $1 |
| token_file = output_directory / foreign_grammar_name + '.tokens' |
| grammar_file = File.dirname( path ) / foreign_grammar_name << '.g' |
| depends_on( token_file ) |
| depends_on( grammar_file ) |
| end |
| end |
| |
| def shell_escape( token ) |
| token = token.to_s.dup |
| token.empty? and return "''" |
| token.gsub!( /([^A-Za-z0-9_\-.,:\/@\n])/n, '\\\1' ) |
| token.gsub!( /\n/, "'\n'" ) |
| return token |
| end |
| |
| def build_command( options ) |
| parts = %w(java) |
| jar_path = options.fetch( :antlr_jar, default_antlr_jar ) |
| parts.push( '-cp', jar_path ) |
| parts << 'org.antlr.Tool' |
| parts.push( '-fo', output_directory ) |
| options[ :profile ] and parts << '-profile' |
| options[ :debug ] and parts << '-debug' |
| options[ :trace ] and parts << '-trace' |
| options[ :debug_st ] and parts << '-XdbgST' |
| parts << File.expand_path( @path ) |
| parts.map! { |part| shell_escape( part ) }.join( ' ' ) << ' 2>&1' |
| end |
| |
| def study |
| @source =~ /^\s*(lexer|parser|tree)?\s*grammar\s*(\S+)\s*;/ or |
| raise Grammar::FormatError[ source, path ] |
| @name = $2 |
| @type = $1 || 'combined' |
| end |
| end # class Grammar |
| |
| class Grammar::InlineGrammar < Grammar |
| attr_accessor :host_file, :host_line |
| |
| def initialize( source, options = {} ) |
| host = call_stack.find { |call| call.file != __FILE__ } |
| |
| @host_file = File.expand_path( options[ :file ] || host.file ) |
| @host_line = ( options[ :line ] || host.line ) |
| @output_directory = options.fetch( :output_directory, File.dirname( @host_file ) ) |
| @verbose = options.fetch( :verbose, $VERBOSE ) |
| |
| @source = source.to_s.fixed_indent( 0 ) |
| @source.strip! |
| |
| study |
| write_to_disk |
| build_dependencies |
| |
| yield( self ) if block_given? |
| end |
| |
| def output_directory |
| @output_directory and return @output_directory |
| File.basename( @host_file ) |
| end |
| |
| def path=( v ) |
| previous, @path = @path, v.to_s |
| previous == @path or write_to_disk |
| end |
| |
| def inspect |
| sprintf( 'inline grammar %s (%s:%s)', name, @host_file, @host_line ) |
| end |
| |
| private |
| |
| def write_to_disk |
| @path ||= output_directory / @name + '.g' |
| test( ?d, output_directory ) or Dir.mkdir( output_directory ) |
| unless test( ?f, @path ) and MD5.digest( @source ) == MD5.digest( File.read( @path ) ) |
| open( @path, 'w' ) { |f| f.write( @source ) } |
| end |
| end |
| end # class Grammar::InlineGrammar |
| |
| class Grammar::CompilationFailure < StandardError |
| JAVA_TRACE = /^(org\.)?antlr\.\S+\(\S+\.java:\d+\)\s*/ |
| attr_reader :grammar, :command, :status, :output |
| |
| def initialize( grammar, command, status, output ) |
| @command = command |
| @status = status |
| @output = output.gsub( JAVA_TRACE, '' ) |
| |
| message = <<-END.here_indent! % [ command, status, grammar, @output ] |
| | command ``%s'' failed with status %s |
| | %p |
| | ~ ~ ~ command output ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ |
| | %s |
| END |
| |
| super( message.chomp! || message ) |
| end |
| end # error Grammar::CompilationFailure |
| |
| class Grammar::FormatError < StandardError |
| attr_reader :file, :source |
| |
| def self.[]( *args ) |
| new( *args ) |
| end |
| |
| def initialize( source, file = nil ) |
| @file = file |
| @source = source |
| message = '' |
| if file.nil? # inline |
| message << "bad inline grammar source:\n" |
| message << ( "-" * 80 ) << "\n" |
| message << @source |
| message[ -1 ] == ?\n or message << "\n" |
| message << ( "-" * 80 ) << "\n" |
| message << "could not locate a grammar name and type declaration matching\n" |
| message << "/^\s*(lexer|parser|tree)?\s*grammar\s*(\S+)\s*;/" |
| else |
| message << 'bad grammar source in file %p' % @file |
| message << ( "-" * 80 ) << "\n" |
| message << @source |
| message[ -1 ] == ?\n or message << "\n" |
| message << ( "-" * 80 ) << "\n" |
| message << "could not locate a grammar name and type declaration matching\n" |
| message << "/^\s*(lexer|parser|tree)?\s*grammar\s*(\S+)\s*;/" |
| end |
| super( message ) |
| end |
| end # error Grammar::FormatError |
| |
| end |
| end |