368 lines
8.9 KiB
Ruby
Executable File
368 lines
8.9 KiB
Ruby
Executable File
#!/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
|