#!/usr/bin/ruby1.8
#
# y2racc  --  yacc to racc converter
#
# Copyright (c) 1999-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public Lisence version 2 or later.
#

require 'optparse'
require 'strscan'
unless defined? StringScanner_C or $".include?('strscan.so')
  $stderr.puts "#{$0}: fatal: NEVER USE ruby-level strscan with y2racc"
  exit 3
end


Y2RACC_VERSION = '1.1.1'

def main
  op = OptionParser.new do |op|
    op.banner = 'Usage: y2racc [-AC] [-c classname] [-o outfile] yaccfile'

    op.on('-o <file>', 'name of output file  [r.<inputfile>]') do |file|
      $OPT_o = file
    end
    op.on('-c <name>', 'name of parser class [MyParser]') do |name|
      $OPT_c = name
    end
    op.on('-u', 'output also user code (%%....)') do
      $OPT_u = true
    end
    op.on('-H', 'cut off header (%{....%})') do
      $OPT_H = true
    end
    op.on('-A', 'cut off actions') do
      $OPT_A = true
    end
    op.on('-U', 'cut off user code (%%....) (default)') do
      $OPT_U = true
    end

    op.on('--help', 'print this message and quit') do
      print op
      exit 0
    end
    op.on('--version', 'print version and quit') do
      puts "racc2y version #{Y2RACC_VERSION}"
      exit 0
    end
    op.on('--copyright', 'print copyright and quit') do
      puts 'Copyright (c) 1999-2003 Minero Aoki'
      exit 0
    end

    begin
      op.parse!
    rescue
      $stderr.puts 'wrong option'
      print op
      exit 1
    end
  end

  $OPT_u = false if $OPT_U
  unless ARGV.size == 1
    $stderr.puts 'wrong option'
    print op
    exit 1
  end

  cname = $OPT_c || 'MyParser'
  fname = ARGV[0]
  outf = $OPT_o || 'r.' + File.basename(fname)
  conv = Converter.new(cname)
  begin
    File.open(fname) {|f|
      conv.parse f, fname
    }
  rescue Errno::ENOENT
    $stderr.puts "no such file: #{fname}"
    exit 1
  end
  File.open(outf, 'w') {|f|
    conv.output f
  }
end


class Converter

  def initialize( cname )
    @cname = cname
    @prectab = []
    @start = nil
    @tokens = []
    @grammer = []

    @header = nil
    @footer = nil
  end

  COMMENT = %r</\*[^*]*\*+(?:[^/*][^*]*\*+)*/>

  #
  # parse
  #

  def parse( f, fname )
    @fname = fname
    str = f.read
    s = StringScanner.new(str)

    procdef s
    procrule s
  end


  def procdef( s )
    skip_until_percent s
    until s.empty?
      if t = s.scan(/(left|right|nonassoc|token|start)\b/)
        __send__ 'proc_' + t, get_tokens(s)

      elsif s.skip %r<(?:
            type | union | expect | thong | binary |
            semantic_parser | pure_parser | no_lines |
            raw | token_table                          )\b>x
        skip_until_percent s

      elsif s.skip /\{/
        @header = s.scan_until(/\%\}/)
        @header.chop!; @header.chop!   # %}
        skip_until_percent s

      elsif s.skip /\%/   # %%
        return

      else
        raise 'scan error'
      end
    end
  end

  def skip_until_percent( s )
    until s.empty?
      s.skip /[^\%\/]+/
      if t = s.scan(COMMENT)
        ;
      elsif s.getch == '/'
        ;
      else
        return
      end
    end
  end

  def get_tokens( s )
    list = []
    until s.empty?
      s.skip /\s+/
      next if s.skip(COMMENT)
      if t = s.scan(/'((?:[^'\\]+|\\.)*)'/)
        list.push t
      elsif t = s.scan(/"((?:[^"\\]+|\\.)*)"/)
        list.push t
      elsif s.skip(/\%/)
        break
      elsif t = s.scan(/\S+/)
        list.push t
      else
        raise 'scan error'
      end
    end
    list
  end

  def proc_left( list )
    @prectab.push ['left', list]
  end

  def proc_right( list )
    @prectab.push ['right', list]
  end

  def proc_nonassoc( list )
    @prectab.push ['nonassoc', list]
  end

  def proc_token( list )
    list.shift if /\A<(.*)>\z/ === list[0]
    @tokens.concat list
  end

  def proc_start( list )
    @start = list[0]
  end

  ###

  STRINGq = /'(?:[^'\\]+|\\.)*'/
  STRINGQ = /"(?:[^"\\]+|\\.)*"/

  def procrule( s )
    @text = []
    until s.empty?
      if t = s.scan(/[^%'"{\/]+/)
        @text.push t
        break if s.empty?
      end
      if s.skip /\{/
        if $OPT_A
          skip_action s
        else
          scan_action s
        end
      elsif t = s.scan(STRINGq) then @text.push t
      elsif t = s.scan(STRINGQ) then @text.push t
      elsif t = s.scan(COMMENT) then @text.push t
      elsif s.skip(/%prec\b/)   then @text.push '='
      elsif s.skip(/%%/)
        @footer = s.rest if $OPT_u
        break
      else
        @text.push s.getch
      end
    end
  end

  def skip_action( s )
    @text, save = [], @text
    scan_action s
    @text = save
    #@text.push "{\n                # action\n            }"
    @text.push "{  }"
  end

  def scan_action( s )
    @text.push '{'
    nest = 1
    until s.empty?
      if t = s.scan(/[^{'"}\/]+/)
        @text.push t
        break if s.empty?
      end
      if t = s.scan(COMMENT)
        @text.push t
        next
      end
      c = s.getch
      @text.push c
      case c
      when '{'
        nest += 1
      when '}'
        nest -= 1
        if nest == 0
          return
        end
      else
        ;
      end
    end
    $stderr.puts "warning: unterminated action in #{@fname}"
  end

  #
  # output
  #

  def output( f )
    f.print(<<SRC)
#
# converted from "#{@fname}" by y2racc version #{Y2RACC_VERSION}
#

class #{@cname}

SRC
    f.print 'token'
    total = 0
    @tokens.each do |t|
      if total > 60
        f.print "\n     "
        total = 0
      end
      total += f.write(" #{t}")
    end
    f.puts
    f.puts

    unless @prectab.empty?
      f.puts 'preclow'
      @prectab.each do |type, toks|
        f.printf "  %-8s %s\n", type, toks.join(' ') unless toks.empty?
      end
      f.puts 'prechigh'
      f.puts
    end

    if @start
      f.puts "start #{@start}"
      f.puts
    end

    f.puts 'rule'
    @text.each {|t| f.print t }
    f.puts
    f.puts 'end'

    if not $OPT_H and @header
      f.puts
      f.puts '---- header'
      f.puts @header
    end
    if $OPT_u and @footer
      f.puts
      f.puts '---- footer'
      f.puts @footer
    end
  end

end


main
