#!/usr/local/bin/ruby
#   rbison v0.0.7, generates a ruby parser class from a bison-like spec file.
#   Copyright (C) 2000  Jonathan Aseltine <aseltine@cs.umass.edu>
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

require "getoptlong"

RBISON_VERSION = '0.0.7'

USAGE = <<EOF
rbison #{RBISON_VERSION}, Copyright (C) 2000 Jonathan Aseltine
rbison comes with ABSOLUTELY NO WARRANTY; for details see the source.
						  
Usage: rbison [<Options>] <Infile> <Class>
Options:
    --debug (-d)		Keep temporary files.
    --help (-h)		        This usage message.
EOF


$debug = false
					      
begin
    opts = GetoptLong.new(['--debug', '-d', GetoptLong::NO_ARGUMENT],
			  ['--help', '-h', GetoptLong::NO_ARGUMENT])
    opts.each do |opt, arg|
	case opt
	when '--debug'
	    $debug = true
	when '--help'
	    printf($stderr, USAGE); exit(0)
	end
    end
rescue GetoptLong::InvalidOption
    printf($stderr, USAGE); exit(1)
end

## preprocess so bison won't complain about rubyisms
input = File.open(ARGV[0], 'r')
ry = input.read()
input.close
ry.gsub!(%r!/\*!, "{ruby_fslash_asterisk}")
ry.gsub!(%r!\*/!, "{ruby_bslash_asterisk}")
ry.gsub!(%r!\@!, "{ruby_at}")
ry.gsub!(%r!\#(?=\{)!, "{#ruby_pound}")
ry.gsub!(%r!(\"[^\"\#]*)(\#+)(?=[^\"\n]*\")!, "\\1{\\2ruby_pound}")
ry.gsub!(%r!(\'[^\'\#]*)(\#+)(?=[^\'\n]*\')!, "\\1{\\2ruby_pound}")
ry.gsub!(%r!([^\{\#])\#+.*$!, "\\1")
ry.gsub!(%r!^\#+.*$!, "")
ry.gsub!(%r!\{(\#+)ruby_pound\}!, "\\1")
ry.sub!(/^\%\{/, "%{\n/*")
ry.sub!(/^\%\}/, "*/\n%}")
ry.gsub!(/([^\_\$])\$/, "\\1{ruby_dollar}")
ry.gsub!(/_\$/, "$")
tmp = File.open("rbison.#{$$}.y", 'w')
tmp.write(ry)
tmp.close
%x{bison rbison.#{$$}.y}
File.delete("rbison.#{$$}.y") unless $debug
tabc = File.open("rbison.#{$$}.tab.c", 'r')

printf(<<EOF, ARGV[1])
## Generated by rbison version #{RBISON_VERSION}.

class %s

    class ParseError < StandardError ; end
    class LexError < StandardError ; end
    NULL = nil

    attr :yydebug, true
    attr :yyerror, true

EOF

s = :DEFAULT

tabc.each_line do |li|
    li.gsub!(/\{ruby_dollar\}/, "\$")
    li.gsub!(/\{ruby_fslash_asterisk\}/, "/*")
    li.gsub!(/\{ruby_bslash_asterisk\}/, "*/")
    li.gsub!(/\{ruby_at\}/, "@")
    if li !~ /\S/
	next
    end
    
    if :DEFAULT == s && li =~ /^\#define\s+YYBISON/o
	s = :TOKEN
	next
    end
    
    if :DEFAULT == s && li =~ /^\#define\s+(YYLAST|YYEMPTY|YYEOF|YYFLAG|YYFINAL|YYNTBASE|YYTERROR)\s+(-?\d+)/o
	printf(<<EOF, $1, $2)
    %s = %s
EOF
	next
    end
    
    if :TOKEN == s && li =~ /^\#define\s+(\S+)\s+(-?\d+)/o
	printf(<<EOF, $1, $2)
    %s = %s
EOF
	next
    end
    
    if :TOKEN == s && li =~ /^\#line\s+\d+/o
	print <<EOF
	
    ## Parser declarations begin
EOF
	s = :INCLUDE
	next
    end
    
    if :INCLUDE == s && li =~ /^\#ifndef YYSTYPE/o
	print <<EOF
    ## Parser declarations end

EOF
	s = :DEFAULT
	next
    end
    
    if :INCLUDE == s && li !~ %r!^\s*(?:/\*|\*/)!o
	li.chop!
	printf(<<EOF, li)
    %s
EOF
	next
    end
    
    if :DEFAULT == s && li =~ /^\#define\s+YYTRANSLATE\(x\)\s\D+(\d+)\D+(\d+)/o
	$yytr = sprintf("yychar < 0 || yychar > %d ? %d : YYTRANSLATE[yychar]",
			$1, $2)
	next
    end

## tables
    if :DEFAULT == s && li =~ /^static .*(yytranslate|yyr1|yyr2|yydefact|yydefgoto|yypact|yypgoto|yycheck|yytable|yyprhs|yyrhs|yyrline|yytname)[^\{]+\{\s*((?:(?:-?\d+|\"[^\"]*\"|NULL),?)+)/o
	printf(<<EOF, $1.upcase, $2)
    %s = [ %s
EOF
	s = :TABLE
	next
    end

## table body
    if :TABLE == s && li =~ /^(?:\s*(?:-?\d+|\"[^\"]*\"|NULL),?)+\s*$/o
	print li
	next
    end

    if :TABLE == s && li =~ /^\};/o
    print <<EOF
    ]
EOF
	s = :DEFAULT
	next
    end

## yyparse
    if :DEFAULT == s && li =~ /^yynewstate:/o
	s = :DEFAULT
	print <<EOF
    
    def initialize
	@yyerror = $stderr
	@yydebug = false
    end
    
    def yyparse(lexer)
	yyerrstatus = 0
	yychar = YYEMPTY
	yystate = 0
	yyss = []
	yyvs = ["nil"]
	yyval = 0
	jump = :YYNEWSTATE

	while true
	    
	    case jump
	
	    when :YYNEWSTATE
		if @yydebug
		    printf(@yyerror, "Entering state %d\\n", yystate)
		end
		yyss.push(yystate)
		jump = :YYBACKUP
		next
	
	    when :YYBACKUP
		yyn = YYPACT[yystate]
		if yyn == YYFLAG
		    jump = :YYDEFAULT
		    next
		end
	
		## get a lookahead token if we don't already have one
		if yychar == YYEMPTY
		    if @yydebug
			printf(@yyerror, "Reading a token: ")
		    end
		    begin
			yychar, yylval = lexer.yylex
		    rescue LexError
			raise ParseError, "lex error"
		    end
		end
	
		## if lookahead <= 0, end of input
		if yychar <= 0
		    yychar1 = 0
		    yychar = YYEOF
		    if @yydebug
			printf(@yyerror, "Now at end of input.\\n")
		    end
		else
		    yychar1 = #{$yytr}
		    if @yydebug
			printf(@yyerror, "Next token is %d (%s,%s)\\n", yychar,
			       YYTNAME[yychar1], yylval)
		    end
		end
	
		## see if we know what to do with this token in this state
		yyn += yychar1
		if yyn < 0 || yyn > YYLAST || YYCHECK[yyn] != yychar1
		    jump = :YYDEFAULT
		    next
		end
	
		## yyn is what to do for this token type in this state
		## negative -> reduce, - yyn is the rule number
		## positive -> shift, yyn is the new state
		##    New state is final state, don't bother to shift, just
		##    return success
		## 0, or most negative number -> error
		yyn = YYTABLE[yyn]
		if yyn < 0
		    if yyn == YYFLAG
			jump = :YYERRLAB
			next
		    end
		    yyn = - yyn
		    jump = :YYREDUCE
		    next
		elsif yyn == 0
		    jump = :YYERRLAB
		    next
		end
	    
		if yyn == YYFINAL
		    return ## accept
		end
	   
		## shift the lookahead token
		if @yydebug
		    printf(@yyerror, "Shifting token %d (%s), ", yychar,
			   YYTNAME[yychar1])
		end
		
		## discard the token being shifted unless it is eof
		if yychar != YYEOF
		    yychar = YYEMPTY
		end
		yyvs.push(yylval)
		
		## count tokens shifted since error; after, three turn off
		## error status
		yyerrstatus -= 1 if yyerrstatus > 0

		yystate = yyn
		jump = :YYNEWSTATE
		next
	
	    when :YYDEFAULT
		yyn = YYDEFACT[yystate]
		if yyn == 0
		    jump = :YYERRLAB
		    next
		else
		    jump = :YYREDUCE
		    next
		end
	
		## do a reduction. yyn is the number of the rule to reduce with
	    when :YYREDUCE
		yylen = YYR2[yyn]
		if yylen > 0
		    yyval = yyvs[yyvs.size - yylen]
		end
		
		if @yydebug
		    printf(@yyerror, "Reducing via rule %d (line %d), ", yyn,
			   YYRLINE[yyn])
		    i = YYPRHS[yyn]
		    while YYRHS[i] > 0
			printf(@yyerror, "%s ", YYTNAME[YYRHS[i]])
			i += 1
		    end
		    printf(@yyerror, " -> %s\\n", YYTNAME[YYR1[yyn]])
		end
	
		case yyn
EOF
	next
    end
    
    if :DEFAULT == s && li =~ /^\s*switch \(yyn\)/o
	s = :CASE
	next
    end
    
    if :CASE == s && li =~ /^case\s+(\d+):/o
	printf(<<EOF, $1)
		when %s
EOF
	next
    end

    if :CASE == s && li =~ %r!^#line \d+!o
	s = :CASE_BODY_1
	next
    end
	
    if :CASE_BODY_1 == s && li =~ /^\s*\{/o
	li.sub!(/^\s*\{/o, "")
	while li =~ /yyvsp\[(-?\d+)\]/o
	    li = $` + "yyvs[#{-1 + $1.to_i}]" + $'
	end
#	li.gsub!(/yyvsp\[(-?\d+)\]/o, "yyvs[-1 + \\1]")
	li.sub!(/;\s*$/o, "")
	li.chop!
	printf(<<EOF, li)
		    %s
EOF
	s = :CASE_BODY_2
	next
    end

    if :CASE_BODY_2 == s && li !~ /^\s*break;\}\s*$/o
	while li =~ /yyvsp\[(-?\d+)\]/o
	    li = $` + "yyvs[#{-1 + $1.to_i}]" + $'
	end
#	li.gsub!(/yyvsp\[(-?\d+)\]/o, "yyvs[-1 + \\1]")
	li.sub!(/^\s*/o, "")
	li.sub!(/;\s*$/o, "")
	li.chop!
	printf(<<EOF, li)
		    %s
EOF
	next
    end
	
    if :CASE_BODY_2 == s && li =~ /^\s*break;\}\s*$/o
	s = :CASE
	next
    end

    if :CASE == s && li =~ %r!^\}\s*$!o
	s = :DEFAULT
	print <<EOF
		when -65536 ## never used, placeholder for ruby
		end
		
		if yylen > 0
		    yyss[(yyss.size - yylen) .. (yyss.size - 1)] = []
		    yyvs[(yyvs.size - yylen) .. (yyvs.size - 1)] = []
		end
		
		yyvs.push(yyval)
	
		if @yydebug
		    printf(@yyerror, "State stack now: %s\\n", yyss.join(' '))
		    printf(@yyerror, "Value stack now: %s\\n", yyvs.join(' '))
		end
		
		## "Shift" the result of the reduction.
		yyn = YYR1[yyn]
		yystate = YYPGOTO[yyn - YYNTBASE] + yyss[-1]
		if yystate >=0 && yystate <= YYLAST &&
			YYCHECK[yystate] == yyss[-1]
		    yystate = YYTABLE[yystate]
		else
		    yystate = YYDEFGOTO[yyn - YYNTBASE]
		end
		jump = :YYNEWSTATE
		next
	
	    when :YYERRLAB
		if yyerrstatus == 0 && @yydebug
		    printf(@yyerror, "Parse error!\\n")
		end
		jump = :YYERRLAB1
		next
		
	    when :YYERRLAB1
		if yyerrstatus == 3
		    if yychar == YYEOF
			raise ParseError, "parse error"
		    end
		    if @yydebug
			printf(@yyerror, "Discarding token %d (%s).\\n", yychar,
			       YYTNAME[yychar1])
		    end
		    yychar = YYEMPTY
		end
	    
		yyerrstatus = 3
		jump = :YYERRHANDLE
		next

	    when :YYERRPOP
		if yyvs.empty?
		    raise ParseError, "parse error"
		end
		## don't pop if the state on top of the stack can handle
		## the error token
		yystate = yyss[-1]
		if YYCHECK[YYPACT[yystate] + YYTERROR] != YYTERROR
		    yyvs.pop
		    yyss.pop
		    if @yydebug
			printf(@yyerror, "Error: state stack now: %s\\n",
			       yyss.join(' '))
			printf(@yyerror, "Error: Value stack now: %s\\n",
			       yyvs.join(' '))
		    end
		end
		jump = :YYERRHANDLE
		next

	    when :YYERRHANDLE
		yyn = YYPACT[yystate]
		if yyn == YYFLAG
		    jump = :YYERRPOP
		    next
		end

		yyn += YYTERROR
		if yyn < 0 || yyn > YYLAST || YYCHECK[yyn] != YYTERROR
		    jump = :YYERRPOP
		    next
		end
    
		yyn = YYTABLE[yyn]
		if yyn < 0
		    if yyn == YYFLAG
			jump = :YYERRPOP
			next
		    end
		    yyn = -yyn
		    jump = :YYREDUCE
		    next
		elsif yyn == 0
		    jump = :YYERRPOP
		    next
		end
    
		if yyn == YYFINAL
		    return ## accept
		end
    
		if @yydebug
		    printf(@yyerror, "Shifting error token, ")
		end
    
		yyvs.push(yylval)
		yystate = yyn
		jump = :YYNEWSTATE
		next
		
	    end ## case
	    
	end ## while true
	
    end ## yyparse

end ## class

EOF
	next
    end

    if :DEFAULT == s && li =~ /^yyerrhandle:/o
	s = :ADDITIONAL_CODE_1
	next
    end

    if :ADDITIONAL_CODE_1 == s && li =~ /^\#line\s+\d+/o
	s = :ADDITIONAL_CODE_2
	print "## Additional user code\n"
	next
    end

    if :ADDITIONAL_CODE_2 == s
	printf("%s", li)
	next
    end
	
end

tabc.close
File.delete("rbison.#{$$}.tab.c") unless $debug
