#!/usr/bin/ruby
#
# y2racc  --  yacc to racc converter
#
#   Copyright (c) 1999-2001 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 'getopts'
require 'strscan'


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

class Y

  Version = '1.1.0'


  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 )
    ret = []
    until s.empty?
      s.skip /\s+/
      next if s.skip COMMENT

      if t = s.scan(/'((?:[^'\\]+|\\.)*)'/)
        ret.push t
      elsif t = s.scan(/"((?:[^"\\]+|\\.)*)"/)
        ret.push t
      elsif s.skip /\%/
        break
      elsif t = s.scan(/\S+/)
        ret.push t
      else
        raise 'scan error'
      end
    end

    ret
  end

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

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

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

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

  def proc_start( arr )
    @start = arr[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 /%%/
        if $OPT_u
          @footer = s.rest
        end
        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 #{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 not $OPT_u and @footer
      f.puts
      f.puts '---- footer'
      f.puts @footer
    end
  end

end


def usage( stat = 1 )
  $stderr.puts "#{File.basename $0}: wrong option" unless stat == 0
  (stat == 0 ? $stdout : $stderr).print(<<EOS)

y2racc version #{Y::Version}

usage:

    y2racc [-AC] [-c classname] [-o outfile] yaccfile

    -o <file>    name of output file  [r.<inputfile>]
    -c <name>    name of parser class [MyParser]
    -u           output also user code (%%....)
    -H           cut off header (%{....%})
    -A           cut off actions
    -U           cut off user code (%%....) (default)

EOS
  exit stat
end


getopts('uAHU', 'c:', 'o:', 'version', 'copyright', 'help') or usage 1
$OPT_u = false if $OPT_U
if $OPT_version then
  puts "y2racc version #{Y::Version}"
  exit 0
end
if $OPT_copyright then
  puts 'Copyright (c) 2000 Minero Aoki <aamine@loveruby.net>'
  exit 0
end
usage 0 if $OPT_help
usage 1 unless ARGV.size == 1


cname = $OPT_c || 'MyParser'
fname = ARGV[0]
outf = $OPT_o || 'r.' + fname

y = Y.new(cname)
begin
  File.open(fname) {|f|
      y.parse f, fname
  }
rescue Errno::ENOENT
  $stderr.puts "no such file: #{fname}"
  exit 1
end
File.open(outf, 'w') {|f|
    y.output f
}
