1#!/usr/bin/ruby
2# encoding: utf-8
3
4require 'antlr3'
5require 'antlr3/test/core-extensions'
6require 'antlr3/test/call-stack'
7
8if RUBY_VERSION =~ /^1\.9/
9  require 'digest/md5'
10  MD5 = Digest::MD5
11else
12  require 'md5'
13end
14
15module ANTLR3
16module Test
17module 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
40end # module DependantFile
41
42class 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
205private
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
266end # class Grammar
267
268class 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
303private
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
312end # class Grammar::InlineGrammar
313
314class 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
332end # error Grammar::CompilationFailure
333
334class 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
364end # error Grammar::FormatError
365
366end
367end
368