#!/usr/bin/perl
# $PostgreSQL: pgsql/src/interfaces/ecpg/preproc/parse.pl,v 1.9 2010/06/04 10:09:58 meskes Exp $
# parser generater for ecpg
# call with backend parser as stdin
#
# Copyright (c) 2007-2010, PostgreSQL Global Development Group
#
# Written by Mike Aubury <mike.aubury@aubit.com>
#	     Michael Meskes <meskes@postgresql.org>
#
# Placed under the same license as PostgreSQL.
#

if (@ARGV) {
	$path = $ARGV[0];
	shift @ARGV;
}

if ($path eq '') { $path = "."; }

$[ = 1;			# set array base to 1
$, = ' ';		# set output field separator
$\ = "\n";		# set output record separator

$copymode = 'off';
$brace_indent = 0;
$yaccmode = 0;
$header_included = 0;
$feature_not_supported = 0;
$tokenmode = 0;

# some token have to be replaced by other symbols
# either in the rule
$replace_token{'BCONST'} = 'ecpg_bconst';
$replace_token{'FCONST'} = 'ecpg_fconst';
$replace_token{'Sconst'} = 'ecpg_sconst';
$replace_token{'IDENT'} = 'ecpg_ident';
$replace_token{'PARAM'} = 'ecpg_param';
# or in the block
$replace_string{'WITH_TIME'} = 'with time';
$replace_string{'NULLS_FIRST'} = 'nulls first';
$replace_string{'NULLS_LAST'} = 'nulls last';
$replace_string{'TYPECAST'} = '::';
$replace_string{'DOT_DOT'} = '..';
$replace_string{'COLON_EQUALS'} = ':=';

# specific replace_types for specific non-terminals - never include the ':'
# ECPG-only replace_types are defined in ecpg-replace_types
$replace_types{'PrepareStmt'} = '<prep>';
$replace_types{'opt_array_bounds'} = '<index>';
# "ignore" means: do not create type and rules for this non-term-id
$replace_types{'stmtblock'} = 'ignore';
$replace_types{'stmtmulti'} = 'ignore';
$replace_types{'CreateAsStmt'} = 'ignore';
$replace_types{'DeallocateStmt'} = 'ignore';
$replace_types{'ColId'} = 'ignore';
$replace_types{'type_function_name'} = 'ignore';
$replace_types{'ColLabel'} = 'ignore';
$replace_types{'Sconst'} = 'ignore';

# these replace_line commands excise certain keywords from the core keyword
# lists.  Be sure to account for these in ColLabel and related productions.
$replace_line{'unreserved_keywordCONNECTION'} = 'ignore';
$replace_line{'unreserved_keywordCURRENT_P'} = 'ignore';
$replace_line{'unreserved_keywordDAY_P'} = 'ignore';
$replace_line{'unreserved_keywordHOUR_P'} = 'ignore';
$replace_line{'unreserved_keywordINPUT_P'} = 'ignore';
$replace_line{'unreserved_keywordMINUTE_P'} = 'ignore';
$replace_line{'unreserved_keywordMONTH_P'} = 'ignore';
$replace_line{'unreserved_keywordSECOND_P'} = 'ignore';
$replace_line{'unreserved_keywordYEAR_P'} = 'ignore';
$replace_line{'col_name_keywordCHAR_P'} = 'ignore';
$replace_line{'col_name_keywordINT_P'} = 'ignore';
$replace_line{'col_name_keywordVALUES'} = 'ignore';
$replace_line{'reserved_keywordTO'} = 'ignore';
$replace_line{'reserved_keywordUNION'} = 'ignore';

# some other production rules have to be ignored or replaced
$replace_line{'fetch_argsFORWARDopt_from_incursor_name'} = 'ignore';
$replace_line{'fetch_argsBACKWARDopt_from_incursor_name'} = 'ignore';
$replace_line{"opt_array_boundsopt_array_bounds'['Iconst']'"} = 'ignore';
$replace_line{'VariableShowStmtSHOWvar_name'} = 'SHOW var_name ecpg_into';
$replace_line{'VariableShowStmtSHOWTIMEZONE'} = 'SHOW TIME ZONE ecpg_into';
$replace_line{'VariableShowStmtSHOWTRANSACTIONISOLATIONLEVEL'} = 'SHOW TRANSACTION ISOLATION LEVEL ecpg_into';
$replace_line{'VariableShowStmtSHOWSESSIONAUTHORIZATION'} = 'SHOW SESSION AUTHORIZATION ecpg_into';
$replace_line{'returning_clauseRETURNINGtarget_list'} = 'RETURNING target_list ecpg_into';
$replace_line{'ExecuteStmtEXECUTEnameexecute_param_clause'} = 'EXECUTE prepared_name execute_param_clause execute_rest';
$replace_line{'ExecuteStmtCREATEOptTempTABLEcreate_as_targetASEXECUTEnameexecute_param_clause'} = 'CREATE OptTemp TABLE create_as_target AS EXECUTE prepared_name execute_param_clause';
$replace_line{'PrepareStmtPREPAREnameprep_type_clauseASPreparableStmt'} = 'PREPARE prepared_name prep_type_clause AS PreparableStmt';
$replace_line{'var_nameColId'} = 'ECPGColId';

line: while (<>) {
    chomp;	# strip record separator
    @Fld = split(' ', $_, -1);

    # Dump the action for a rule - 
    # mode indicates if we are processing the 'stmt:' rule (mode==0 means normal,  mode==1 means stmt:)
    # flds are the fields to use. These may start with a '$' - in which case they are the result of a previous non-terminal
    #                             if they dont start with a '$' then they are token name
    #
    # len is the number of fields in flds...
    # leadin is the padding to apply at the beginning (just use for formatting)

    if (/ERRCODE_FEATURE_NOT_SUPPORTED/) {
	$feature_not_supported = 1;
	next line;
    }

    if (/^%%/) {
	$tokenmode = 2;
	$copymode = 'on';
	$yaccmode++;
	$infield = 0;
	$fieldcount = 0;
    }

    $S = $_;
    $prec = 0;
    # Make sure any braces are split
    $s = '{', $S =~ s/$s/ { /g;
    $s = '}', $S =~ s/$s/ } /g;
    # Any comments are split
    $s = '[/][*]', $S =~ s#$s# /* #g;
    $s = '[*][/]', $S =~ s#$s# */ #g;

    # Now split the line into individual fields
    $n = (@arr = split(' ', $S));

    if ($arr[1] eq '%token' && $tokenmode == 0) {
	$tokenmode = 1;
	&include_stuff('tokens', 'ecpg.tokens', '', 1, 0);
	$type = 1;
    }
    elsif ($arr[1] eq '%type' && $header_included == 0) {
	&include_stuff('header', 'ecpg.header', '', 1, 0);
	&include_stuff('ecpgtype', 'ecpg.type', '', 1, 0);
	$header_included = 1;
    }

    if ($tokenmode == 1) {
	$str = '';
	for ($a = 1; $a <= $n; $a++) {
	    if ($arr[$a] eq '/*') {
		$comment++;
		next;
	    }
	    if ($arr[$a] eq '*/') {
		$comment--;
		next;
	    }
	    if ($comment) {
		next;
	    }
	    if (substr($arr[$a], 1, 1) eq '<') {
		next;
		# its a type
	    }
	    $tokens{$arr[$a]} = 1;

	    $str = $str . ' ' . $arr[$a];
	    if ($arr[$a] eq 'IDENT' && $arr[$a - 1] eq '%nonassoc') {
	    # add two more tokens to the list
		$str = $str . "\n%nonassoc CSTRING\n%nonassoc UIDENT";
	    }
	}
	&add_to_buffer('orig_tokens', $str);
	next line;
    }

    # Dont worry about anything if we're not in the right section of gram.y
    if ($yaccmode != 1) {
	next line;
    }

    # Go through each field in turn
    for ($fieldIndexer = 1; $fieldIndexer <= $n; $fieldIndexer++) {
	if ($arr[$fieldIndexer] eq '*/' && $comment) {
	    $comment = 0;
	    next;
	}
	elsif ($comment) {
	    next;
	}
	elsif ($arr[$fieldIndexer] eq '/*') {
	    # start of a multiline comment
	    $comment = 1;
	    next;
	}
	elsif ($arr[$fieldIndexer] eq '//') {
	    next line;
	}
	elsif ($arr[$fieldIndexer] eq '}') {
	    $brace_indent--;
	    next;
	}
	elsif ($arr[$fieldIndexer] eq '{') {
	    $brace_indent++;
	    next;
	}

	if ($brace_indent > 0) {
	    next;
	}
	if ($arr[$fieldIndexer] eq ';') {
	    if ($copymode eq 'on') {
		if ($infield && $includetype eq '') {
		    &dump_line($stmt_mode, $fields, $field_count);
		}
		&add_to_buffer('rules', ";\n\n");
	    }
	    else {
		$copymode = 'on';
	    }
	    $field_count = 0;
	    $infield = 0;
	    $line = '';
	    $includetype = '';
	    next;
	}

	if ($arr[$fieldIndexer] eq '|') {
	    if ($copymode eq 'on') {
		if ($infield && $includetype eq '') {
		    $infield = $infield + &dump_line($stmt_mode, $fields, $field_count);
		}
		if ($infield > 1) {
		    $line = '| ';
		}
	    }
	    $field_count = 0;
	    $includetype = '';
	    next;
	}

	if ($replace_token{$arr[$fieldIndexer]}) {
	    $arr[$fieldIndexer] = $replace_token{$arr[$fieldIndexer]};
	}
	
	# Are we looking at a declaration of a non-terminal ? 
	if (($arr[$fieldIndexer] =~ '[A-Za-z0-9]+:') || $arr[$fieldIndexer + 1] eq ':') {
	    $non_term_id = $arr[$fieldIndexer];
	    $s = ':', $non_term_id =~ s/$s//g;

	    if ($replace_types{$non_term_id} eq '') {
		$replace_types{$non_term_id} = '<str>';
	    }
	    if ($replace_types{$non_term_id} eq 'ignore') {
		$copymode = ';';
		$line = '';
		next line;
	    }
	    else {
		$copymode = 'on';
	    }
	    $line = $line . ' ' . $arr[$fieldIndexer];
	    # Do we have the : attached already ? 
	    # If yes, we'll have already printed the ':'
	    if (!($arr[$fieldIndexer] =~ '[A-Za-z0-9]+:')) {
		# Consume the ':' which is next...
		$line = $line . ':';
		$fieldIndexer++;
	    }

	    # Special mode? 
	    if ($non_term_id eq 'stmt') {
		$stmt_mode = 1;
	    }
	    else {
		$stmt_mode = 0;
	    }
	    $tstr = '%type ' . $replace_types{$non_term_id} . ' ' .  $non_term_id;
	    &add_to_buffer('types', $tstr);

	    if ($copymode eq 'on') {
		&add_to_buffer('rules', $line);
	    }
	    $line = '';
	    $field_count = 0;
	    $infield = 1;
	    next;
	}
	elsif ($copymode eq 'on') {
	    $line = $line . ' ' . $arr[$fieldIndexer];
	}
	if ($arr[$fieldIndexer] eq '%prec') {
	    $prec = 1;
	    next;
	}

	if ($copymode eq 'on' && !$prec && !$comment && $arr[$fieldIndexer] ne '/*EMPTY*/' && length($arr[$fieldIndexer]) && $infield) {
	    $nfield = $field_count + 1;
	    if ($arr[$fieldIndexer] ne 'Op' && ($tokens{$arr[$fieldIndexer]} > 0 || $arr[$fieldIndexer] =~ "'.+'") || $stmt_mode == 1) {
		if ($replace_string{$arr[$fieldIndexer]}) {
		    $S = $replace_string{$arr[$fieldIndexer]};
		}
		else {
		    $S = $arr[$fieldIndexer];
		}
		$s = '_P', $S =~ s/$s//g;
		$s = "'", $S =~ s/$s//g;
		if ($stmt_mode == 1) {
		    $fields{$field_count++} = $S;
		}
		else {
		    $fields{$field_count++} = lc($S);
		}
	    }
	    else {
		$fields{$field_count++} = "\$" . $nfield;
	    }
	}
    }
}

&dump('header');
&dump('tokens');
&dump('types');
&dump('ecpgtype');
&dump('orig_tokens');
print '%%';
print 'prog: statements;';
&dump('rules');
&include_stuff('trailer', 'ecpg.trailer', '', 1, 0);
&dump('trailer');

sub include_stuff {
    local($includestream, $includefilename, $includeblock, $copy, $field_count) = @_;
    $copied = 0;
    $inblock = 0;
    $filename = $path . "/" . $includefilename;
    while (($_ = &Getline2($filename),$getline_ok)) {
	if ($includeblock ne '' && $Fld[1] eq 'ECPG:' && $inblock == 0) {
	    if ($Fld[2] eq $includeblock) {
		$copy = 1;
		$inblock = 1;
		$includetype = $Fld[3];
		if ($includetype eq 'rule') {
		    &dump_fields($stmt_mode, *fields, $field_count, ' { ');
		}
		elsif ($includetype eq 'addon') {
		    &add_to_buffer('rules', ' { ');
		}
	    }
	    else {
		$copy = 0;
	    }
	}
	else {
	    if ($copy == 1 && $Fld[1] ne 'ECPG:') {
		&add_to_buffer($includestream, $_);
		$copied = 1;
		$inblock = 0;
	    }
	}
    }
    delete $opened{$filename} && close($filename);
    if ($includetype eq 'addon') {
	&dump_fields($stmt_mode, *fields, $field_count, '');
    }
    if ($copied == 1) {
	$field_count = 0;
	$line = '';
    }
    $copied;
}

sub add_to_buffer {
    local($buffer, $str) = @_;
    $buff{$buffer, $buffcnt{$buffer}++} = $str;
}

sub dump {
    local($buffer) = @_;
    print '/* ' . $buffer . ' */';
    for ($a = 0; $a < $buffcnt{$buffer}; $a++) {
	print $buff{$buffer, $a};
    }
}

sub dump_fields {
    local($mode, *flds, $len, $ln) = @_;
    if ($mode == 0) {
	#Normal 
	&add_to_buffer('rules', $ln);
	if ($feature_not_supported == 1) {
	    # we found an unsupported feature, but we have to
	    # filter out ExecuteStmt: CREATE OptTemp TABLE ...
	    # because the warning there is only valid in some situations
	    if ($flds{0} ne 'create' || $flds{2} ne 'table') {
		&add_to_buffer('rules', "mmerror(PARSE_ERROR, ET_WARNING, \"unsupported feature will be passed to server\");");
	    }
	    $feature_not_supported = 0;
	}

	if ($len == 0) {
	    # We have no fields ? 
	    &add_to_buffer('rules', " \$\$=EMPTY; }");
	}
	else {
	    # Go through each field and try to 'aggregate' the tokens into a single 'make_str' where possible
	    $cnt = 0;
	    for ($z = 0; $z < $len; $z++) {
		if (substr($flds{$z}, 1, 1) eq "\$") {
		    $flds_new{$cnt++} = $flds{$z};
		    next;
		}

		$str = $flds{$z};

		while (1) {
		    if ($z >= $len - 1 || substr($flds{$z + 1}, 1, 1) eq "\$") {
			# We're at the end...
			$flds_new{$cnt++} = "make_str(\"" . $str . "\")";
			last;
		    }
		    $z++;
		    $str = $str . ' ' . $flds{$z};
		}
	    }

	    # So - how many fields did we end up with ? 
	    if ($cnt == 1) {
		# Straight assignement
		$str = " \$\$ = " . $flds_new{0} . ';';
		&add_to_buffer('rules', $str);
	    }
	    else {
		# Need to concatenate the results to form
		# our final string
		$str = " \$\$ = cat_str(" . $cnt;

		for ($z = 0; $z < $cnt; $z++) {
		    $str = $str . ',' . $flds_new{$z};
		}
		$str = $str . ');';
		&add_to_buffer('rules', $str);
	    }
	    if ($literal_mode == 0) {
		&add_to_buffer('rules', '}');
	    }
	}
    }
    else {
	# we're in the stmt: rule
	if ($len) {
	    # or just the statement ...
	    &add_to_buffer('rules', " { output_statement(\$1, 0, ECPGst_normal); }");
	}
	else {
	    &add_to_buffer('rules', " { \$\$ = NULL; }");
	}
    }
}

sub generate_block {
    local($line) = @_;
    $block = $non_term_id . $line;
    $s = ' ', $block =~ s/$s//g;
    $s = "\\|", $block =~ s/$s//g;
    return $block;
}

sub dump_line {
    local($stmt_mode, $fields, $field_count) = @_;
    $block = &generate_block($line);
    if ($replace_line{$block} eq 'ignore') {
	return 0;
    }
    elsif ($replace_line{$block}) {
	if (index($line, '|') != 0) {
	    $line = '| ' . $replace_line{$block};
	}
	else {
	    $line = $replace_line{$block};
	}
	$block = &generate_block($line);
    }
    &add_to_buffer('rules', $line);
    $i = &include_stuff('rules', 'ecpg.addons', $block, 0, $field_count);
    if ($i == 0) {
	&dump_fields($stmt_mode, *fields, $field_count, ' { ');
    }
    return 1;
}

sub Getline2 {
    &Pick('',@_);
    if ($getline_ok = (($_ = <$fh>) ne '')) {
	chomp;	# strip record separator
	@Fld = split(' ', $_, -1);
    }
    $_;
}

sub Pick {
    local($mode,$name,$pipe) = @_;
    $fh = $name;
    open($name,$mode.$name.$pipe) unless $opened{$name}++;
}
