[PHP4BETA] zend-parser.y shift/reduce conflicts and suggestions

From: Date: Tue, 17 Aug 1999 09:49:36 +0000
Subject: [PHP4BETA] zend-parser.y shift/reduce conflicts and suggestions
Groups: php.version4 
Request: Send a blank email to php-version4+get-3473@lists.php.net to get a copy of this message
I have seen that the Zend scanner does not solve shift/reduce conflicts for IF/ELSE statements very cleanly. In fact I suspect that the current implementation also has reduce/reduce conflicts after parsing "if(condition)" where a reduce action is required, but where there is no clean solution do decide which reduction will match (this is because the "else" and "elseif" branchs have been made optional using null reductions. I think that this causes poor analysis for IF/ELSE and for ELSE IF constructs. But there's an alternative in yacc/Bison to solve this, which does not cause problems with ELSE IF combined-statements. Here is what I have written for a running Sybase Transac SQL parser, which does not exhibit any shift/reduce and reduce/reduce conflict when introducing the ELSE IF construction (by two consecutive IF/ELSE statements) when I want to detect exclusive branchs. This code was necessary in a compiler to avoid much code redondancy and to enhance the readability of the generated code, avoiding many embedded instruction blocks: ------------ cut here (begin 1) ------------- %union { /* token type names definitions (snipped)... */ } /* tokens with precedence */ %nonassoc LOWER_THAN_ELSE /* pseudo-token which never gets generated by flex */ /* but is only used for precedence resolution. */ /* this token must have immediate lower priority */ /* than the ELSE token. */ %nonassoc ELSE %left OR %left AND %left NOT %left TIMES_EQUAL EQUAL_TIMES '=' '<' '>' %nonassoc '!' %left '+' '-' '|' '^' '&' DOUBLE_PIPES %left '*' '/' '%' %nonassoc UMINUS '~' /* UMINUS never gets generated by flex but is */ /* convenient when fixing precedence of an unary '-' */ /* operator */ /* other tokens... */ %token IF WHILE BREAK RETURN GOTO /* etc... */ %% statement_list: statement { GiConstTokenState = L_TOP; } | statement_list statement { GiConstTokenState = L_TOP; } ; statement: if_condition_statement %prec LOWER_THAN_ELSE { vEndIf(); } | if_condition_statement ELSE { vElseBranch(); } statement_but_if %prec ELSE { vEndIf(); } | statement_but_if ; if_condition_statement: IF { GiLanguageState = S_SPL; } search_condition { vIfStatement($3); GiLanguageState = S_INIT; } statement | if_condition_statement ELSE IF { GiLanguageState = S_SPL; } search_condition { vElifBranch($5); GiLanguageState = S_INIT; } statement ; statement_but_if: declare_variable_list | begin_end_block | break_statement /* | other statements (snipped)... */ ; ------------ cut here (end 1) ------------- The main idea there is that we do not need to markup the end of IF/ELSEIF/ELSE constructs by special marks like an ENDIF keyword, and we are still able to detect "ELSE IF" constructs, only by its combined syntax while preserving the syntax and semantic of the simple IF statement with a single optionnal ELSE branch. More, you do not need rules with empty alternates which cause parser to produce much more reductions than necessary, and sometimes produces bad code in the actions launched from reductions. Note that my grammar does not require a specific ELSEIF token to be detected by the scanner, so they are detected as two separate tokens with any number of blanks or comments between them (this suppresses complex regular expressions tricks used to detect an ELSEIF single token and this allows the preservation of comments in the code generation phase if they are desired...) Note also that in the code above, the GiLanguageState and GiConstTokenState are used only by the code generator, and it does not alter the syntaxic parser. They are there only to demonstrate that you can really insert code reduction rules at the places where they are shown without introducing any reduce/reduce conflicts. The role of my two variables are to manage some special tokens (e.g. CLIENT, FILE or SIZE) which may be recognized as such by the lexer but which in most cases are simple regular identifiers that can be used as column names or table names when they are not used in some specific Sybase statements. In zend-parser.y, the same technic could be applied: you only need to define statement like I did (it only defines the IF/ELSE statements constructs), and then report all other statements that already are present in your existing "statement" rule into another rule (here named "statement_but_if"). Doing that you'll see that the generated parser is greatly reduced in size because you solve many shift/reduce conflicts otherwise only solved in too many rules by an extensive use of operator precedence... For better performance and for code size reduction, statement lists should also have their empty branch removed, by adding rules that match alternate forms where an existing rule are empty (today), and could simply not exist (if you make the change). There are many reasons to do it: - The parser complexity does not depend on the number of rules - A parser with nullable non terminal tokens generates much more states because the next look-ahead lexical token can cross several non-terminal nullable tokens, so this creates many reduce/reduce conflicts that forbids the extension of the syntax without many stormbraining kludges in the syntax. - This really reduces the total number of reductions and greatly enhance the syntax recognition and selectivity of the parser (and it allows a better recovery of syntax errors which otherwise can only be done at the top syntax level, which means that any syntax error cannot recover and aborts the scanner, and implies that very few information are available to help the language scripter to correct this error !). - By eliminating nullable non terminal tokens, you may divide the overall size of the generated parser often by 4 or dozens, because you will reduce the global number of parser states !! - A parser in which the forms of a syntax explicitly show when an optional member really exists can produce more complex code only when these optionnal members are there, and often can produce much simpler actions when these members are not specified in the source being parsed, because there are more rules and the generation decision is made at the parsing stage instead of the generation stages, eliminating many states variables, eliminating many sources of bugs due to the value of these "global" states variables, and improving the global speed of the generator, while not altering the speed of the parser. - While eliminating empty rules for a nullable token you'll see that some forms of expressions are simply illegal (because despite they are syntaxically correct with the intuitive simple syntax their semantics are always incorrect) and can be simply eliminated from the accepted syntax, instead of detecting them later by semantic rules, because the intuitive simple syntax is too laxist. Avoiding these unnecessary branches, you can eliminate many reduce/reduce conflicts, and divide the number of states in your parser (so you also reduce the parser code size!) - Eliminating nullable tokens in a grammar is not always easy because of the presence of generating actions which must be copied with all token variables ($1, $2, ...) being renumbered, and many assignments to $$ removed. It can sometimes be quite brainstorming. But the final value you get from this chalenge is really cost-effective: your language gains in abstraction, and gains extensitivity in areas which could have been impossible without generating many, many syntax conflicts. For example doing it allows you to create new operators, new forms of declarations, new contextual grammar forms such as when adding OO functionalities to an existing language... Note in the code below that for the "top_statement_list" (which may be empty), the simplest way to make it not nullable is to add a T_EOF token generated by the scanner. Also note that this grammar does not only use T_ELSEIF but simply uses T_ELSE T_IF as two separate tokens (this eliminates the Lexer kludge about comments and blanks between them, if you want to accept the form "else if" as equivalent to the PHP "elseif" keyword). ------------ cut here (begin 2) ------------- %nonassoc T_LOWER_THAN_ELSE /* make these 2 tokens first in that order */ %nonassoc T_ELSE /* ... other tokens for statements... */ %% /* Rules */ language: statement_list T_EOF | T_EOF ; statement_list: statement { ELS_FETCH(); HANDLE_INTERACTIVE(); } | statement_list { do_extended_info(CLS_C); } statement { ELS_FETCH(); HANDLE_INTERACTIVE(); } ; statement: if_condition_statement %prec T_LOWER_THAN_ELSE { do_if_end(CLS_C); } | if_condition_statement T_ELSE statement_but_if %prec T_ELSE { do_if_end(CLS_C); } | newif_condition_statement T_ENDIF ';' { do_if_end(CLS_C); } | newif_condition_statement T_ELSE ':' statement T_ENDIF ';' { do_if_end(CLS_C); } | statement_but_if ; if_condition_statement: T_IF '(' expr ')' { do_if_cond(&$3, &$4 CLS_CC); } statement { do_if_after_statement(&$4, 1 CLS_CC); } | if_condition_statement T_ELSEIF '(' expr ')' { do_if_cond(&$4, &$5 CLS_CC); } statement { do_if_after_statement(&$5, 0 CLS_CC); } /* accept also the "else if" form with separate tokens: */ | if_condition_statement T_ELSE T_IF '(' expr ')' { do_if_cond(&$5, &$6 CLS_CC); } statement { do_if_after_statement(&$6, 0 CLS_CC); } ; newif_condition_statement: /* here the ':' is required to force the detection of the "endif;" tokens */ T_IF '(' expr ')' ':' { do_if_cond(&$3, &$4 CLS_CC); } statement { do_if_after_statement(&$4, 1 CLS_CC); } /* elseif constructs normally require a ':' but this is not necessary */ | newif_condition_statement T_ELSEIF '(' expr ')' ':' { do_if_cond(&$4, &$5 CLS_CC); } statement { do_if_after_statement(&$5, 0 CLS_CC); } | newif_condition_statement T_ELSE T_IF '(' expr ')' ':' { do_if_cond(&$5, &$6 CLS_CC); } statement { do_if_after_statement(&$6, 0 CLS_CC); } /* possible (optionnal) acceptable syntaxes without the unnecessary ':' */ | newif_condition_statement T_ELSEIF '(' expr ')' { do_if_cond(&$4, &$5 CLS_CC); } statement { do_if_after_statement(&$5, 0 CLS_CC); } | newif_condition_statement T_ELSE T_IF '(' expr ')' { do_if_cond(&$5, &$6 CLS_CC); } statement { do_if_after_statement(&$6, 0 CLS_CC); } ; statement_but_if: '{' statement_list '}' | '{' '}' /* because now 'statement_list' rule can't be empty */ | T_WHILE '(' { $1.u.opline_num = get_next_op_number(CG(active_op_array)); } expr ')' { do_while_cond(&$4, &$5 CLS_CC); } while_statement { do_while_end(&$1, &$5 CLS_CC); } | T_DO { $1.u.opline_num = get_next_op_number(CG(active_op_array)); do_do_while_begin(CLS_C); } statement T_WHILE '(' expr ')' ';' { do_do_while_end(&$1, &$6 CLS_CC); } | T_FOR '(' for_expr ';' { do_free(&$3 CLS_CC); $4.u.opline_num = get_next_op_number(CG(active_op_array)); } for_expr ';' { do_for_cond(&$6, &$7 CLS_CC); } for_expr ')' { do_free(&$9 CLS_CC); do_for_before_statement(&$4, &$7 CLS_CC); } for_statement { do_for_end(&$7 CLS_CC); } | T_SWITCH '(' expr ')' { do_switch_cond(&$3 CLS_CC); } switch_case_list { do_switch_end(&$6 CLS_CC); } | T_BREAK ';' { do_brk_cont(ZEND_BRK, NULL CLS_CC); } /* | other statements ... */ ; /* and then replace all further reference to 'inner_statement_list' by simply 'statement_list', and add a rule without it because the new 'statement_list' can't be empty... for example, the rule: */ for_statement: statement | ':' inner_statement_list T_ENDFOR ';' ; /* will now become: */ for_statement: statement | ':' statement_list T_ENDFOR ';' | ':' T_ENDFOR ';' ; /* Note that the new forms for IF, WHILE, FOR (with a ':' which introduces a corresponding ENDIF or ENDWHILE or ENDFOR) could be also modified to accept unambiguous forms without the ':', where ENDIF, ENDWHILE and ENDFOR would still be valid. Many will see the ':' as superfluous, while they find the ENDing keyword useful. Additionally a new form of the DO statement may accept a 'statement_list' instead of a single 'statement' before the ending WHILE condition, e.g: "DO: statement1; statement2; WHILE(condition);" With a configurable option, enforced or relaxed by a test in the semantic parser actions, the ':' may also be made superfluous, and the form without it made acceptable. And there could be other styles of loop controls (which are possible with PHP and not in C because Zend language does not have a preprocessor): - "REPEAT" as an synonym for "DO" (a-la Pascal) - "UNTIL(condition) instead of "WHILE(!condition)" - "DO...LOOP;" instead of "DO...WHILE(TRUE);" or "WHILE(TRUE)...ENDWHILE;" - "BREAK IF(condition);" instead of "IF(condition): BREAK; ENDIF;" with the new style for IF, or "IF(condition) BREAK;" with the old style but which is plagged by a possible "ELSE" clause after it. - "CONTINUE IF(condition);" and "GOTO label IF(condition);", using the same idea about the new style for IF... */ ------------ cut here (end 2) -------------

« previous php.version4 (#3473) next »