I'm going to limit myself, at least for now, to the question actually asked here: how to deal with the ambiguity between an operator which could be prefix or infix (such as unary negation) and a prefix operator which takes two consecutive arguments. There is a second interesting question, which has to do with prefix operators which bind less tightly than (some) infix operators.
These two language features are not really related, but they are similar in that the naïve grammars are ambiguous, and the ambiguities are difficult to resolve in a strict LR(k) grammar. They are also similar in the fact that many languages suffer from inadequate resolutions of these ambiguities, or resolve them in ways that are poorly understood by language users, in part because the resolutions are hard to document.
Ambiguity with prefix negation
Since prefix negation and infix difference operators generally use the same symbol (-
), an ambiguity is created in any syntactic construct in which two expressions can appear consecutively without an intervening token. Many languages have such constructs. They include:
Prefix operators which take more than one argument, as in your question.
Implicit operators such as implied multiplication (2x
instead of 2*x
) and function application (as in Haskell, where f a
calls f
with argument a
, similar to f(a)
in many other languages). Other examples include Awk's implicit string concatenation syntax, where a b
represents the concatenation of the two variables.
Undelimited sequences, usually lists or tuples, where consecutive elements are simply specified one after another and the elements can be expressions. One early language with this feature is Logo, but there are many others.
Undelimited statements. In languages in which statements can be written consecutively without a separating delimiter (Lua, for example), two expressions can be consecutive if an expression can be a statement, or more generally if there are statement syntaxes which start with an expression (such as assignment statements) and other syntaxes which end with an expression (such as return
statements).
That's not an exhaustive catalogue, but it gives an idea of the range of the issue.
There are three broad strategies to resolve this ambiguity:
Require the first expression (or both expressions) to be either an atomic term or a parenthesised subexpression. If the ambiguity is the result of an implicit operator, this resolution will be natural if the implicit operator's binding precedence is at the top of the precedence list; that's the case with Haskell, for example, where function application takes precedence over any other operator. This strategy is also sometimes used for undelimited lists, allowing a list of three elements to be written [a b c]
if the components are simple, but requiring [a (b + c) d]
for more complicated components.
Prohibit the second expression from starting with an ambiguous prefix operator. Or, in other words, resolve the ambiguity in favour of the infix operator, when there is a choice. This is the preferred solution for implicit multiplication, and it follows the Principle of Least Astonishment, since resolving 2-x
as 2*(-x)
would astonish most users. This does not require the implicit operator to have maximum precedence; indeed, most grammars with implicit multiplication would parse 2x^4
(where ^
is the exponentiation operator) as 2*(x^4)
, again to avoid surprises. (Different parsers resolve 2a/3b
in different ways, so there can still be surprises. Some people feel that implicit multiplication should bind more strongly than explicit multiplication and division; others that implicit and explicit multiplication should bind equally. But that's a different issue.)
Resolve the ambiguity between unary negation and binary difference during lexical analysis. For example, Logo requires that the unary negation operator either be preceded by an open parenthesis or some token which cannot be part of an expression, so that binary difference is not a possible interpretation, or that it be written with at least one space before and no spaces after. The second rule allows a -10 * b
to be interpreted as the two expressions a
and ((-10)*b)
, while a - 10 * b
would necessarily be interpreted as a single expression. A similar, but more complicated, set of whitespace-aware rules was proposed for the Frontier language (never implemented, to my knowledge); it was criticised for being too subtle for code readers.
Solution one is appropriate for Haskell. Since it's simply an operator precedence rule, it's easy to describe. The expression fmap f (Just x)
needs to be written that way because function application is left-associative; fmap f Just x
would be (((fmap f) Just) x)
so the requirement to parenthesise (Just x)
is pretty clear, in the same way that you would have to write a - (b - c)
, if that's what you meant. (It's worth noting that Haskell also has an explicit application operator, $
, with low binding precedence, which can sometimes be used to avoid parentheses.)
Solution three is certainly possible, but whitespace-aware syntax is a common source of confusion for casual users. (And, as noted above, is easy to miss when you're reading code.)
So my preference would be solution two: when both interpretations of the unary/binary operator are possible, always prefer the binary interpretation. That's often simply the expected interpretation, although it can lead to surprises when the consecutive expressions are separated by a newline; even then, the rule is easy to explain.
It's also easy to implement. The basic idea is to define two different expression non-terminals. One is unrestricted; it is used for the first expression in a consecutive sequence. The other one does not accept any expression whose first token is an ambiguous unary operator. (If the language has unambiguous unary operators, they don't need to be restricted.)
If you were using a parser generator which accepted the formalism in the ECMAScript standard, this would be trivial. Unfortunately, few (if any) parser generators allow templated non-terminals, so the implementation requires annoying code duplication in the grammar. In yacc/bison syntax, assuming that operator precedence is established with prior precedence declarations:
expr: expr '+' expr
| expr '-' expr
| expr '*' expr
/* other binary operators omitted */
| '-' expr %prec UNOP
| '+' expr %prec UNOP
| CONSTANT
| IDENTIFIER
| '(' expr ')'
expr_follow
: expr_follow '+' expr
| expr_follow '-' expr
| expr_follow '*' expr
/* ... other binary operators */
/* ambiguous unary operators omitted */
| CONSTANT
| IDENTIFIER
| '(' expr ')'
Note that the expr_follow
restriction cascades only through the first operand of the productions; the operands following an operator or (
do not need to be restricted since they will not affect the first token in the expression.
If you were using cascading-precedence style rather than precedence operators, you'd need to create two non-terminals at each precedence level, something like this:
expr: additive
additive
: additive '+' multiplicative
| additive '-' multiplicative
| multiplicative
multiplicative
: multiplicative '*' negative
| multiplicative '/' negative
| multiplicative '%' negative
| negative
negative
: '-' negative
| exponential
exponential
: unary '^' negative
| unary
unary
: '&' unary /* '&' represents an unambiguous unary operator */
| atom
atom: CONSTANT
| IDENTIFIER
| '(' expr ')'
expr_follow
: additive_follow
additive_follow
: additive_follow '+' multiplicative
| additive_follow '-' multiplicative
| multiplicative_follow
multiplicative_follow
: multiplicative_follow '*' exponential
| multiplicative_follow '/' exponential
| multiplicative_follow '%' exponential
| exponential
In the first stanza, I would usually simplify negative
to:
negative
: '-' negative
| unary '^' negative
| unary
I wrote it the way I did to better illustrate the difference between the two cascades. The second cascade does not have negative
because the unary negative cannot appear at the start of expr_follow
(so there is no negative_follow
). exponential
, unary
and atom
do not need to be duplicated because none of them can start with unary negative.
Read this if you're considering designing a language with consecutive expressions
On the whole, a syntax which allows two consecutive expressions is a snakepit of perpetual syntactic problems. Each ambiguity needs to be identified and then resolved in the parser. Since there is no universal principle which determines a unique resolution, the language designer needs to think through the possible disambiguations and choose the one they feel is best suited to the problem domain.
Each such resolution is, in general, easy to implement once it has been precisely spelled out, but the rules can be very difficult to describe or justify to users. That can lead to mysterious bugs, in which a syntactic construct was resolved in an unexpected way, or mysterious syntax errors, in which a syntactic construct which seems reasonable to the coder was banned for ease of implementation. One frequent consequence in languages which develop to the point of having style guides and linters, is a stylistic ban on precisely these syntactic constructs; either alternative syntaxes or redundant parentheses are required, and these can even give rise to the language implementation itself issuing warnings.
prefix
. So far this hasn't produced any new ambiguities. It seems similar to pattern matching in the left hand side of function definitions in haskell, e.g.flatmap (Just x) f = f x
where you have to parenthesize(Just x)
but notNothing
. Presumably they have the same problem I have, and the same solution for the same reason. $\endgroup$