1#!/usr/bin/ruby 2# encoding: utf-8 3 4require 'antlr3' 5require 'set' 6require 'rake' 7require 'rake/tasklib' 8require 'shellwords' 9 10module ANTLR3 11 12=begin rdoc ANTLR3::CompileTask 13 14A rake task-generating utility concerning ANTLR grammar file 15compilation. This is a general utility -- the grammars do 16not have to be targetted for Ruby output; it handles all 17known ANTLR language targets. 18 19 require 'antlr3/task' 20 21 ANTLR3::CompileTask.define( 22 :name => 'grammars', :output_directory => 'lib/parsers' 23 ) do | t | 24 t.grammar_set( 'antlr/MainParser.g', 'antlr/MainTree.g' ) 25 26 t.grammar_set( 'antlr/Template.g' ) do | gram | 27 gram.output_directory = 'lib/parsers/template' 28 gram.debug = true 29 end 30 end 31 32 33TODO: finish documentation 34 35=end 36 37class CompileTask < Rake::TaskLib 38 attr_reader :grammar_sets, :options 39 attr_accessor :name 40 41 def self.define( *grammar_files ) 42 lib = new( *grammar_files ) 43 block_given? and yield( lib ) 44 lib.define 45 return( lib ) 46 end 47 48 def initialize( *grammar_files ) 49 grammar_files = [ grammar_files ].flatten! 50 options = Hash === grammar_files.last ? grammar_files.pop : {} 51 @grammar_sets = [] 52 @name = options.fetch( :name, 'antlr-grammars' ) 53 @options = options 54 @namespace = Rake.application.current_scope 55 grammar_files.empty? or grammar_set( grammar_files ) 56 end 57 58 def target_files 59 @grammar_sets.inject( [] ) do | list, set | 60 list.concat( set.target_files ) 61 end 62 end 63 64 def grammar_set( *grammar_files ) 65 grammar_files = [ grammar_files ].flatten! 66 options = @options.merge( 67 Hash === grammar_files.last ? grammar_files.pop : {} 68 ) 69 set = GrammarSet.new( grammar_files, options ) 70 block_given? and yield( set ) 71 @grammar_sets << set 72 return( set ) 73 end 74 75 def compile_task 76 full_name = ( @namespace + [ @name, 'compile' ] ).join( ':' ) 77 Rake::Task[ full_name ] 78 end 79 80 def compile! 81 compile_task.invoke 82 end 83 84 def clobber_task 85 full_name = ( @namespace + [ @name, 'clobber' ] ).join( ':' ) 86 Rake::Task[ full_name ] 87 end 88 89 def clobber! 90 clobber_task.invoke 91 end 92 93 def define 94 namespace( @name ) do 95 desc( "trash all ANTLR-generated source code" ) 96 task( 'clobber' ) do 97 for set in @grammar_sets 98 set.clean 99 end 100 end 101 102 for set in @grammar_sets 103 set.define_tasks 104 end 105 106 desc( "compile ANTLR grammars" ) 107 task( 'compile' => target_files ) 108 end 109 end 110 111 112#class CompileTask::GrammarSet 113class GrammarSet 114 attr_accessor :antlr_jar, :debug, 115 :trace, :profile, :compile_options, 116 :java_options 117 attr_reader :load_path, :grammars 118 attr_writer :output_directory 119 120 def initialize( grammar_files, options = {} ) 121 @load_path = grammar_files.map { | f | File.dirname( f ) } 122 @load_path.push( '.', @output_directory ) 123 124 if extra_load = options[ :load_path ] 125 extra_load = [ extra_load ].flatten 126 @load_path.unshift( extra_load ) 127 end 128 @load_path.uniq! 129 130 @grammars = grammar_files.map do | file | 131 GrammarFile.new( self, file ) 132 end 133 @output_directory = '.' 134 dir = options[ :output_directory ] and @output_directory = dir.to_s 135 136 @antlr_jar = options.fetch( :antlr_jar, ANTLR3.antlr_jar ) 137 @debug = options.fetch( :debug, false ) 138 @trace = options.fetch( :trace, false ) 139 @profile = options.fetch( :profile, false ) 140 @compile_options = 141 case opts = options[ :compile_options ] 142 when Array then opts 143 else Shellwords.shellwords( opts.to_s ) 144 end 145 @java_options = 146 case opts = options[ :java_options ] 147 when Array then opts 148 else Shellwords.shellwords( opts.to_s ) 149 end 150 end 151 152 def target_files 153 @grammars.map { | gram | gram.target_files }.flatten 154 end 155 156 def output_directory 157 @output_directory || '.' 158 end 159 160 def define_tasks 161 file( @antlr_jar ) 162 163 for grammar in @grammars 164 deps = [ @antlr_jar ] 165 if vocab = grammar.token_vocab and 166 tfile = find_tokens_file( vocab, grammar ) 167 file( tfile ) 168 deps << tfile 169 end 170 grammar.define_tasks( deps ) 171 end 172 end 173 174 def clean 175 for grammar in @grammars 176 grammar.clean 177 end 178 if test( ?d, output_directory ) and ( Dir.entries( output_directory ) - %w( . .. ) ).empty? 179 rmdir( output_directory ) 180 end 181 end 182 183 def find_tokens_file( vocab, grammar ) 184 gram = @grammars.find { | gram | gram.name == vocab } and 185 return( gram.tokens_file ) 186 file = locate( "#{ vocab }.tokens" ) and return( file ) 187 warn( Util.tidy( <<-END, true ) ) 188 | unable to locate .tokens file `#{ vocab }' referenced in #{ grammar.path } 189 | -- ignoring dependency 190 END 191 return( nil ) 192 end 193 194 def locate( file_name ) 195 dir = @load_path.find do | dir | 196 File.file?( File.join( dir, file_name ) ) 197 end 198 dir and return( File.join( dir, file_name ) ) 199 end 200 201 def compile( grammar ) 202 dir = output_directory 203 test( ?d, dir ) or FileUtils.mkpath( dir ) 204 sh( build_command( grammar ) ) 205 end 206 207 def build_command( grammar ) 208 parts = [ 'java', '-cp', @antlr_jar ] 209 parts.concat( @java_options ) 210 parts << 'org.antlr.Tool' << '-fo' << output_directory 211 parts << '-debug' if @debug 212 parts << '-profile' if @profile 213 parts << '-trace' if @trace 214 parts.concat( @compile_options ) 215 parts << grammar.path 216 return parts.map! { | t | escape( t ) }.join( ' ' ) 217 end 218 219 def escape( token ) 220 token = token.to_s.dup 221 token.empty? and return( %('') ) 222 token.gsub!( /([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1" ) 223 token.gsub!( /\n/, "'\n'" ) 224 return( token ) 225 end 226 227end 228 229class GrammarFile 230 LANGUAGES = { 231 "ActionScript" => [ ".as" ], 232 "CSharp2" => [ ".cs" ], 233 "C" => [ ".c", ".h" ], 234 "ObjC" => [ ".m", ".h" ], 235 "CSharp3" => [ ".cs" ], 236 "Cpp" => [ ".cpp", ".h" ], 237 "Ruby" => [ ".rb" ], 238 "Java" => [ ".java" ], 239 "JavaScript" => [ ".js" ], 240 "Python" => [ ".py" ], 241 "Delphi" => [ ".pas" ], 242 "Perl5" => [ ".pm" ] 243 }.freeze 244 GRAMMAR_TYPES = %w(lexer parser tree combined) 245 246 ################################################################## 247 ######## CONSTRUCTOR ############################################# 248 ################################################################## 249 250 def initialize( group, path, options = {} ) 251 @group = group 252 @path = path.to_s 253 @imports = [] 254 @language = 'Java' 255 @token_vocab = nil 256 @tasks_defined = false 257 @extra_dependencies = [] 258 if extra = options[ :extra_dependencies ] 259 extra = [ extra ].flatten 260 @extra_dependencies.concat( extra ) 261 end 262 263 study 264 yield( self ) if block_given? 265 fetch_imports 266 end 267 268 ################################################################## 269 ######## ATTRIBUTES AND ATTRIBUTE-ISH METHODS #################### 270 ################################################################## 271 attr_reader :type, :name, :language, :source, 272 :token_vocab, :imports, :imported_grammars, 273 :path, :group 274 275 for attr in [ :output_directory, :load_path, :antlr_jar ] 276 class_eval( <<-END ) 277 def #{ attr } 278 @group.#{ attr } 279 end 280 END 281 end 282 283 def lexer_files 284 if lexer? then base = @name 285 elsif combined? then base = @name + 'Lexer' 286 else return( [] ) 287 end 288 return( file_names( base ) ) 289 end 290 291 def parser_files 292 if parser? then base = @name 293 elsif combined? then base = @name + 'Parser' 294 else return( [] ) 295 end 296 return( file_names( base ) ) 297 end 298 299 def tree_parser_files 300 return( tree? ? file_names( @name ) : [] ) 301 end 302 303 def file_names( base ) 304 LANGUAGES.fetch( @language ).map do | ext | 305 File.join( output_directory, base + ext ) 306 end 307 end 308 309 for type in GRAMMAR_TYPES 310 class_eval( <<-END ) 311 def #{ type }? 312 @type == #{ type.inspect } 313 end 314 END 315 end 316 317 def delegate_files( delegate_suffix ) 318 file_names( "#{ name }_#{ delegate_suffix }" ) 319 end 320 321 def tokens_file 322 File.join( output_directory, name + '.tokens' ) 323 end 324 325 def target_files( all = true ) 326 targets = [ tokens_file ] 327 328 for target_type in %w( lexer parser tree_parser ) 329 for file in self.send( :"#{ target_type }_files" ) 330 targets << file 331 end 332 end 333 334 if all 335 for grammar in @imported_grammars 336 targets.concat( grammar.target_files ) 337 end 338 end 339 340 return targets 341 end 342 343 def update 344 touch( @path ) 345 end 346 347 def all_imported_files 348 imported_files = [] 349 for grammar in @imported_grammars 350 imported_files.push( grammar.path, *grammar.all_imported_files ) 351 end 352 return imported_files 353 end 354 355 def clean 356 deleted = [] 357 for target in target_files 358 if test( ?f, target ) 359 rm( target ) 360 deleted << target 361 end 362 end 363 364 for grammar in @imported_grammars 365 deleted.concat( grammar.clean ) 366 end 367 368 return deleted 369 end 370 371 def define_tasks( shared_depends ) 372 unless @tasks_defined 373 depends = [ @path, *all_imported_files ] 374 for f in depends 375 file( f ) 376 end 377 depends = shared_depends + depends 378 379 target_files.each do | target | 380 file( target => ( depends - [ target ] ) ) do # prevents recursive .tokens file dependencies 381 @group.compile( self ) 382 end 383 end 384 385 @tasks_defined = true 386 end 387 end 388 389private 390 391 def fetch_imports 392 @imported_grammars = @imports.map do | imp | 393 file = group.locate( "#{ imp }.g" ) or raise( Util.tidy( <<-END ) ) 394 | #{ @path }: unable to locate imported grammar file #{ imp }.g 395 | search directories ( @load_path ): 396 | - #{ load_path.join( "\n - " ) } 397 END 398 Imported.new( self, file ) 399 end 400 end 401 402 def study 403 @source = File.read( @path ) 404 @source =~ /^\s*(lexer|parser|tree)?\s*grammar\s*(\S+)\s*;/ or 405 raise Grammar::FormatError[ @source, @path ] 406 @name = $2 407 @type = $1 || 'combined' 408 if @source =~ /^\s*options\s*\{(.*?)\}/m 409 option_block = $1 410 if option_block =~ /\s*language\s*=\s*(\S+)\s*;/ 411 @language = $1 412 LANGUAGES.has_key?( @language ) or 413 raise( Grammar::FormatError, "Unknown ANTLR target language: %p" % @language ) 414 end 415 option_block =~ /\s*tokenVocab\s*=\s*(\S+)\s*;/ and 416 @token_vocab = $1 417 end 418 419 @source.scan( /^\s*import\s+(\w+\s*(?:,\s*\w+\s*)*);/ ) do 420 list = $1.strip 421 @imports.concat( list.split( /\s*,\s*/ ) ) 422 end 423 end 424end # class Grammar 425 426class GrammarFile::Imported < GrammarFile 427 def initialize( owner, path ) 428 @owner = owner 429 @path = path.to_s 430 @imports = [] 431 @language = 'Java' 432 @token_vocab = nil 433 study 434 fetch_imports 435 end 436 437 for attr in [ :load_path, :output_directory, :antlr_jar, :verbose, :group ] 438 class_eval( <<-END ) 439 def #{ attr } 440 @owner.#{ attr } 441 end 442 END 443 end 444 445 def delegate_files( suffix ) 446 @owner.delegate_files( "#{ @name }_#{ suffix }" ) 447 end 448 449 def target_files 450 targets = [ tokens_file ] 451 targets.concat( @owner.delegate_files( @name ) ) 452 return( targets ) 453 end 454end 455 456class GrammarFile::FormatError < StandardError 457 attr_reader :file, :source 458 459 def self.[]( *args ) 460 new( *args ) 461 end 462 463 def initialize( source, file = nil ) 464 @file = file 465 @source = source 466 message = '' 467 if file.nil? # inline 468 message << "bad inline grammar source:\n" 469 message << ( "-" * 80 ) << "\n" 470 message << @source 471 message[ -1 ] == ?\n or message << "\n" 472 message << ( "-" * 80 ) << "\n" 473 message << "could not locate a grammar name and type declaration matching\n" 474 message << "/^\s*(lexer|parser|tree)?\s*grammar\s*(\S+)\s*;/" 475 else 476 message << 'bad grammar source in file %p\n' % @file 477 message << ( "-" * 80 ) << "\n" 478 message << @source 479 message[ -1 ] == ?\n or message << "\n" 480 message << ( "-" * 80 ) << "\n" 481 message << "could not locate a grammar name and type declaration matching\n" 482 message << "/^\s*(lexer|parser|tree)?\s*grammar\s*(\S+)\s*;/" 483 end 484 super( message ) 485 end 486end # error Grammar::FormatError 487end # class CompileTask 488end # module ANTLR3 489