#!/usr/local/bin/ruby
#
# $Id: racc,v 1.7 2005/11/20 17:31:32 aamine Exp $
#
# Copyright (c) 1999-2005 Minero Aoki
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU LGPL, Lesser General Public License version 2.1.
# For details of LGPL, see the file "COPYING".
#

require 'racc/compiler'
require 'racc/parser'
require 'racc/parser-text'
require 'racc/info'
require 'racc/exception'
require 'racc/compat'
require 'rbconfig'
require 'getoptlong'

##### main -----------------------------------------------------

def racc_main
  opt = get_options()
  srcfn = ARGV[0]
  racc = Racc::Compiler.new
  begin
    racc_main0 racc, srcfn, opt
  rescue Racc::Error, Errno::ENOENT => err
    raise if racc.debug
    lineno = err.message.slice(/\A\d+:/).to_s
    $stderr.puts "#{File.basename $0}: #{srcfn}:#{lineno} #{err.message.strip}"
    exit 1
  end
end

def racc_main0(racc, srcfn, opt)
  srcstr = File.read(srcfn)
  set_flags__before_parse racc, opt
  if opt['-P']
    prof = pcompile(racc, srcstr, srcfn) {
      set_flags__after_parse racc, opt
    }
  else
    racc.parse srcstr, srcfn
    if opt['--check-only']
      $stderr.puts 'syntax ok'
      exit 0
    end
    set_flags__after_parse racc, opt
    racc.compile
  end
  write_table_file racc, srcfn, opt
  report_profile prof if prof
  if opt['--verbose']
    output_fn = opt['--log-file'] ||
        make_filename(opt['--output'] || srcfn, '.output')
    File.open(output_fn, 'w') {|f|
      racc.output f
    }
  end
  report_conflict racc, opt
  report_useless racc, opt
end

##### parse arg ------------------------------------------------

Racc_Options = <<EOS
  o -g --debug            - output parser for user level debugging
  o -o --output-file <outfile>  file name of output [<fname>.tab.rb]
  o -e --executable <rubypath>  insert #! line in output ('ruby' to default)
  o -E --embedded         - output file which don't need runtime
  o -l --no-line-convert  - never convert line numbers (for ruby<=1.4.3)
  o -c --line-convert-all - convert line numbers also header and footer
  x -s --super <super>      use <super> instead of Racc::Parser
  x -r --runtime <file>     use <file> instead of 'racc/parser'
  o -a --no-omit-actions  - never omit actions
  x -  --omit-actions     - always omit actions

  o -v --verbose          - create <filename>.output file
  o -O --log-file <fname>   file name of verbose output [<fname>.output]

  o -C --check-only       - syntax check only
  o -S --output-status    - output status time to time
  x -  --no-extentions    - run without any ruby extentions
  o -  --version          - print version and quit
  o -  --runtime-version  - print runtime version and quit
  o -  --copyright        - print copyright and quit
  x -h --help             - print this message and quit
  x -P -                  - report profile
  x -D - <flags>            set (racc's) debug flags
EOS

module Racc
end

def get_options
  gl_args = Racc_Options.map {|line|
              next if line.strip.empty?
              disp, sopt, lopt, takearg, doc = line.strip.split(/\s+/, 5)
              a = []
              a.push lopt unless lopt == '-'
              a.push sopt unless sopt == '-'
              a.push takearg == '-' ?
                     GetoptLong::NO_ARGUMENT : GetoptLong::REQUIRED_ARGUMENT
              a
            }.compact
  getopt = GetoptLong.new(*gl_args)
  getopt.quiet = true

  opt = {}
  begin
    getopt.each do |name, arg|
      raise GetoptLong::InvalidOption, "#{name} given twice" if opt.key?(name)
      opt[name] = arg.empty?() ? true : arg
    end
  rescue GetoptLong::AmbigousOption, GetoptLong::InvalidOption,
         GetoptLong::MissingArgument, GetoptLong::NeedlessArgument
    usage 1, $!.message
  end

  usage 0 if opt['--help']

  if opt['--version']
    puts "racc version #{Racc::Version}"
    exit 0
  end
  # Racc.const_set :Racc_No_Extentions, opt['--no-extensions']
  if opt['--runtime-version']
    printf "racc runtime version %s (rev. %s); %s\n",
           Racc::Parser::Racc_Runtime_Version,
           Racc::Parser::Racc_Runtime_Revision,
           if Racc::Parser.racc_runtime_type == 'ruby'
             sprintf('ruby core version %s (rev. %s)',
                     Racc::Parser::Racc_Runtime_Core_Version_R,
                     Racc::Parser::Racc_Runtime_Core_Revision_R)
           else
             sprintf('c core version %s (rev. %s)',
                     Racc::Parser::Racc_Runtime_Core_Version_C,
                     Racc::Parser::Racc_Runtime_Core_Revision_C)
           end
    exit 0
  end
  if opt['--copyright']
    puts "racc version #{Racc::Version}"
    puts "#{Racc::Copyright} <aamine@loveruby.net>"
    exit 0
  end

  usage(1, 'no grammar file given') if ARGV.empty?
  usage(1, 'too many grammar files given') if ARGV.size > 1

  opt['--line-convert'] = true
  if opt['--no-line-convert']
    opt['--line-convert'] = false
    opt['--line-convert-all'] = false
  end

  opt['--omit-actions'] = false if opt['--no-omit-actions']

  opt
end

def usage(status, msg = nil)
  f = (status == 0 ? $stdout : $stderr)
  f.puts "#{File.basename $0}: #{msg}" if msg
  f.puts 'Usage: racc [options] <grammar file>'
  f.puts 'Options:'
  Racc_Options.each do |line|
    next if line.strip.empty?
    show_p, sopt, lopt, takearg, description = line.strip.split(/\s+/, 5)
    next if show_p == 'x'
    optdesc = [
      [ (sopt == '-') ? nil : sopt,
        (lopt == '-') ? nil : lopt ].compact.join(','),
      (takearg == '-') ? nil : takearg
    ].compact.join(' ')
    f.printf "  %-27s %s\n", optdesc, description
  end
  exit status
end

##### compile ----------------------------------------------

def set_flags__before_parse(racc, opt)
  racc.verbose = opt['--output-status']
  racc.d_parse = true if opt['-D'] and /p/ =~ opt['-D']
end

def set_flags__after_parse(racc, opt)
  # overwrite source's
  racc.debug_parser = opt['--debug']
  racc.convert_line = opt['--line-convert']
  racc.omit_action  = opt['--omit-actions'] unless opt['--omit-actions'] == nil
  if opt['-D'] or $DEBUG
    optd = (opt['-D'] || '')
    $stdout.sync = true
    racc.debug   = true
    racc.d_rule  = true if /r/ =~ optd
    racc.d_token = true if /t/ =~ optd
    racc.d_state = true if /s/ =~ optd
    racc.d_la    = true if /l/ =~ optd
    racc.d_prec  = true if /c/ =~ optd
  end
end

##### profile

def pcompile(racc, srcstr, srcfn)
  times = []
  times.push [ 'parse',   get_lap{ racc.parse(srcstr, srcfn) } ]
  yield
  times.push [ 'state',   get_lap{ racc.nfa } ]
  times.push [ 'resolve', get_lap{ racc.dfa } ]
  times
end

def get_lap
  begt = Time.times.utime
  yield
  endt = Time.times.utime
  endt - begt
end

def current_utime
  if RUBY_VERSION >= '1.7.0'
  then Process.times.utime
  else Time.times.utime
  end
end

def report_profile(prof)
  f = $stderr
  whole = 0
  prof.each do |arr|
    whole += arr[1]
  end
  whole = 0.01 if whole == 0
  f.puts '--task-----------+--sec------+---%-'
  prof.each do |arr|
    name, time = arr
    f.printf "%-19s %s %3d%%\n", name, pjust(time,4,4), (time/whole * 100).to_i
  end
  f.puts '-----------------+-----------+-----'
  f.printf "%-20s%s\n", 'total', pjust(whole,4,4)
end

def pjust(num, i, j)
  m = /(\d+)(\.\d+)?/.match(num.to_s)
  str = m[1].rjust(i)
  str.concat m[2].ljust(j+1)[0,j+1] if m[2]
  str
end

##### output -----------------------------------------------

def write_table_file(racc, srcfn, opt)
  tabfn = opt['--output-file'] || make_filename(srcfn, '.tab.rb')
  RaccTableFile.new(tabfn, srcfn, opt['--line-convert']) {|f|
    f.shebang opt['--executable'] if opt['--executable']
    f.notice
    f.require opt['--embedded'], opt['--runtime'] || 'racc/parser'
    f.header opt['--line-convert-all']
    f.parser_class(racc.parser.class_name,
                   opt['--super'] || racc.parser.super_class || 'Racc::Parser') {
      f.inner
      racc.source f.file
    }
    f.footer opt['--line-convert-all']
  }
  File.chmod 0755, tabfn if opt['--executable']
end

def make_filename(fname, suffix)
  fname.sub(/(?:\..*?)?\z/, suffix)
end

class RaccTableFile

  def initialize(tabfn, srcfn, c)
    @tabfn = tabfn
    @srcfn = srcfn
    @convline = c
    @header_labels = %w( header prepare )
    @inner_labels  = %w( inner )
    @footer_labels = %w( footer driver )
    @is_top = false
    @uniq = {}
    init_user_code
    File.open(tabfn, 'w') {|f|
      @f = f
      yield self
    }
  end

  def file
    @f
  end

  c = ::Config::CONFIG
  RUBY_PATH = "#{c['bindir']}/#{c['ruby_install_name']}#{c['EXEEXT']}"

  def shebang(path)
    @f.print "#!#{path == 'ruby' ? RUBY_PATH : path}\n"
  end

  def notice
    @f.puts %q[#]
    @f.puts %q[# DO NOT MODIFY!!!!]
    @f.puts %Q[# This file is automatically generated by racc #{Racc::Version}]
    @f.puts %Q[# from racc grammer file "#{@srcfn}".]
    @f.puts %q[#]
  end

  def require(embed_p, parser_rb)
    if embed_p
      @f.puts '#'
      @f.puts "# #{@tabfn}: generated by racc (runtime embedded)"
      @f.puts '#'
      is_top {
        @f.puts %<###### racc/parser.rb begin>
        @f.puts %<unless $".index 'racc/parser.rb'>
        @f.puts %<$".push 'racc/parser.rb'>
        add ::Racc::PARSER_TEXT, 'racc/parser.rb', 1
        @f.puts %<end>
        @f.puts %<###### racc/parser.rb end>
      }
      @f.puts
    else
      @f.puts
      @f.puts  "require '#{parser_rb}'"
      @f.puts
    end
  end

  def parser_class(classname, superclass)
    @f.puts
    mods = classname.split('::')
    real = mods.pop
    mods.each_with_index do |m,i|
      @f.puts  "#{'  ' * i}module #{m}"
      @f.puts
    end
    @f.puts "#{'  ' * mods.size}class #{real} < #{superclass}"
    yield
    @f.puts "#{'  ' * mods.size}end   \# class #{real}"
    mods.reverse.each_with_index do |m,i|
      @f.puts
      @f.puts  "#{'  ' * (mods.size - i - 1)}end   \# module #{m}"
    end
  end

  def header(conv)
    is_top {
      add_part conv, @header_labels
    }
  end

  def inner
    add_part @convline, @inner_labels
  end

  def footer(conv)
    is_top {
      add_part conv, @footer_labels
    }
  end

  private

  ###
  ### init
  ###

  def init_user_code
    @usercode = Racc::GrammarFileParser.get_usercode(@srcfn)
    check_duplicated_user_code
  end

  def check_duplicated_user_code
    [ @header_labels, @inner_labels, @footer_labels ].each do |names|
      codes = names.select {|n| @usercode[n] }
      if codes.length > 1
        raise Racc::CompileError, "#{codes.join(' and ')} are exclusive"
      end
    end
  end

  ###
  ### part
  ###

  def add_part(convp, names)
    str, lineno, fnames = getent(names)
    convert_line(convp) {
      add str, @srcfn, lineno if str and not str.strip.empty?
    }
    fnames.each do |n|
      add_file n
    end if fnames
  end

  def getent(names)
    names.map {|n| @usercode[n] }.compact[0]
  end

  def convert_line(b)
    save, @convline = @convline, b
    yield
    @convline = save
  end

  ###
  ### low level code
  ###

  def add_file(fname)
    File.open(fname) {|f|
      add f.read, fname, 1
    }
  end

  def add(str, fn, lineno)
    @f.puts
    if @convline
      surrounding_by_eval(fn, str, lineno) {
        @f.puts str
      }
    else
      @f.puts str
    end
  end

  def surrounding_by_eval(nm, str, lineno)
    sep = makesep(nm, str)
    @f.print 'self.class.' if toplevel?
    @f.puts "module_eval <<'#{sep}', '#{nm}', #{lineno}"
    yield
    @f.puts sep
  end

  def is_top
    @is_top = true
    yield
    @is_top = false
  end

  def toplevel?
    @is_top
  end

  def makesep(nm, str)
    sep = uniqlabel(nm)
    sep *= 2 while str.index(sep)
    sep
  end

  def uniqlabel(nm)
    ret = "..end #{nm} modeval..id"
    srand
    5.times do
      ret << sprintf('%02x', rand(255))
    end
    ret << sprintf('%02x', rand(255)) while @uniq[ret]
    @uniq[ret] = true
    ret
  end

end   # class RaccTableFile

##### report -----------------------------------------------

def report_conflict(racc, opt)
  sr = 0
  rr = 0
  racc.statetable.each do |st|
    sr += st.srconf.size if st.srconf
    rr += st.rrconf.size if st.rrconf
  end
  if opt['-D'] and /o/ =~ opt['-D']
    debugout {|f|
      f.print "ex#{racc.ruletable.expect}\n"
      f.print "sr#{sr}\n" if sr > 0
      f.print "rr#{rr}\n" if rr > 0
    }
    return
  end
  ex = racc.ruletable.expect
  $stderr.puts "#{sr} shift/reduce conflicts" if (sr > 0 or ex) and sr != ex
  $stderr.puts "#{rr} reduce/reduce conflicts" if rr > 0
end

def report_useless(racc, opt)
  nt = 0
  rl = 0
  racc.symboltable.each_nonterm do |t|
    nt += 1 if t.useless?
  end
  racc.ruletable.each do |r|
    rl += 1 if r.useless?
  end
  if opt['-D'] and /o/ =~ opt['-D']
    debugout('a') {|f|
      f.print "un#{nt}\n" if nt > 0
      f.print "ur#{rl}\n" if rl > 0
    }
    return
  end
  if nt > 0 or rl > 0
    $stderr.printf "%s%s%s\n",
                   nt > 0                 ? "#{nt} useless nonterminals" : '',
                   ((nt > 0) && (rl > 0)) ? ' and '                     : '',
                   rl > 0                 ? "#{rl} useless rules"        : ''
  end
  if racc.ruletable.start.useless?
    $stderr.puts 'fatal: start symbol does not derive any sentence'
  end
end

def debugout(flag = 'w', &block)
  File.open("log/#{File.basename(ARGV[0])}", flag, &block)
end

##### entry point ------------------------------------------

racc_main
