I'm writing a compiler, and I've built a recursive-descent parser to handle the syntax analysis. I'd like to enhance the type system to support functions as a valid variable type, but I'm building a statically typed language, and my desired syntax for a function type renders the grammar temporarily* ambiguous until resolved. I'd rather not use a parser generator, though I know that Elkhound would be an option. I know I can alter the grammar to make it parse within a fixed number of steps, but I'm more interested in how to implement this by hand.
I've made a number of attempts at figuring out the high-level control flow, but every time I do this I end up forgetting a dimension of complexity, and my parser becomes impossible to use and maintain.
There are two layers of ambiguity: a statement can be an expression, a variable definition, or a function declaration, and the function declaration can have a complex return type.
Grammar subset, demonstrating the ambiguity:
basetype
: TYPE
| IDENTIFIER
;
type
: basetype
| basetype parameter_list
;
arguments
: arraytype IDENTIFIER
| arraytype IDENTIFIER COMMA arguments
;
argument_list
: OPEN_PAREN CLOSE_PAREN
| OPEN_PAREN arguments CLOSE_PAREN
;
parameters
: arraytype
| arraytype COMMA parameters
;
parameter_list
: OPEN_PAREN CLOSE_PAREN
| OPEN_PAREN parameters CLOSE_PAREN
;
expressions
: expression
| expression COMMA expressions
;
expression_list
: OPEN_PAREN CLOSE_PAREN
| OPEN_PAREN expressions CLOSE_PAREN
;
// just a type that can be an array (this language does not support
// multidimensional arrays)
arraytype:
: type
| type OPEN_BRACKET CLOSE_BRACKET
;
block
: OPEN_BRACE CLOSE_BRACE
| OPEN_BRACE statements CLOSE_BRACE
;
function_expression
: arraytype argument_list block
| arraytype IDENTIFIER argument_list block
;
call_expression
: expression expressions
;
index_expression
: expression OPEN_BRACKET expression CLOSE_BRACKET
;
expression
: function_expression
| call_expression
| index_expression
| OPEN_PAREN expression CLOSE_PAREN
;
function_statement
: arraytype IDENTIFIER argument_list block
;
define_clause
: IDENTIFIER
| IDENTIFIER ASSIGN expression
;
define_chain
: define_clause
| define_clause COMMA define_chain
;
define_statement
: arraytype define_chain
;
statement
: function_statement
| define_statement SEMICOLON
| expression SEMICOLON
;
statements
:
| statement statements
;
Example parses:
// function 'fn' returns a reference to a void, parameterless function
void() fn() {}
// the parser doesn't know which of these are types and which are variables,
// so it doesn't know until the end that this is a call_expression
Object(var, var, var, var)
// the parser only finds out at the end that this is a function declaration
Object(var, var, var, var) fn2() {}
(Object(var, var, var, var) ())
(Object(var, var, var, var) () {})
// the parser could possibly detect the "string" type and figure out that
// this has to be a define statement or a function declaration, but it's
// still ambiguous to the end
Object(Object(string, Object), string[]) fn3() {}
Object(Object(string, Object), string[]) fn4 = fn3;
My basic approach has been to write functions that could parse the unambiguous components of this subset of the grammar, and then flatten the more complex control flow into individual functional blocks to capture state in function calls. This has proved unsuccessful, what techniques can one use to solve this kind of problem?
*There is likely a better word for this