# How can ambiguity with unary and binary operators be resolved?

Most languages use +/- for addition and subtraction, in addition to using +/- as unary prefix positive/negative operators (the former typically being a no-op). However, this can easily lead to ambiguity.

For example, many languages include a ++ or -- operator. Sometimes these are prefix/postfix increment or decrement operators, typically in C-like languages, and sometimes they're binary operators, such as concatenation in Haskell. When ++ is an operator, 1++2 becomes ambiguous; is it 1 ++ 2, or 1 + (+2)?

Another example is languages where function application doesn't require parentheses (for example, add 1 2). In this case, even 2 - 1 could be ambiguous; is it the subtraction 2 - 1, or is it a 2 followed by a -1? This case could maybe be solved by tracking the number of arguments to any functions before those tokens, but this falls apart with cases like a three-argument function followed by 1 - 2 3 - 4.

How can this ambiguity be resolved, whether by changing the syntax (such as APL's separate "high minus" for negative number literals), the parsing (such as signficant whitespace), or some other aspect of the language?

• Oh, this also comes up with * for multiplication, pointer dereferencing, and/or exponentiation (**). Commented May 16, 2023 at 19:05
• Precedence, parenthesis, or spaces. It can just be established that one form has precedence over another. Commented May 16, 2023 at 21:32

# Who says you have to?

Never mind that APL has a separate "high minus" for negative literal numbers, the APL unary/binary operators (called monadic/dyadic functions in APL lingo) scheme is unambiguous, because:

• No multi-glyphs are used; each symbol stands on its own.
• All single-argument calls are on the same side.
• All operators have the same precedence.

APL specifically has unary operators on the left, and operators having long right scope, but this is rather arbitrary, and any scheme adhering to the above rules will be unambiguous.

It is even possible to use mult-glyphs, but then the involved glyphs must not have unary meanings to the side away from their companion glyphs. In fact, GNU APL uniquely does have a few bi-glyph operators that work like this.

This is called maximal munch, and is usually handled during lexing. It means that the lexer will make the tokens as large as possible. This is obvious with identifiers, abc won't be lexed as [a, b, c] but will instead be lexed a single token abc. The same applies to operators. Take "---x" and this simple lexer:

def lex(input_string):
tokens = []
i = 0

while i < len(input_string):
while input_string[i].strip() == "":
i += 1
current_char = input_string[i]

if current_char == "-":
# Check if it's '--' or '-'
if input_string[i + 1] == "-":
tokens.append("--")
i += 2
else:
# It's '-' token
tokens.append("-")
i += 1

else:
# Match other characters as individual tokens
tokens.append(current_char)
i += 1



first the lexer will see that it starts with -, it will check if the next character is also - and because it is lex it as --. The lexer will then loop to see the next token (which is the third -), the lexer will check if the next token is -, it is but the character after isn't - so it will be lexed as -. Finally the lexer will see the x and lex it as x. All together it produces --, -, x for ---x. But for - --x it produces -, --, x.

For what you said, with 1++2 being 1 ++ 2 or 1 + (+2). The answer is 1 ++ 2 again due to maximal munch. Of course maximal munch isn't used in all languages and so it can vary from language to language.

• There could be some issues with using this specifically (e.g., - --x makes sense, but -- -x doesn't), but I guess any guaranteed order for how ambiguous situations are handled would be a solution Commented May 16, 2023 at 19:59
• @RadvylfPrograms There definitely are issues with this! In C/C++ for example ---x is also parsed as -- - x and (with GCC) trying to compile gets the error lvalue required as decrement operand. It's definitely possible to add special handling, but that makes the language more complicated for a small issue (writing -(--x) or - --x isn't that hard), so it's probably unneeded. Commented May 16, 2023 at 20:04

# Require parentheses whenever different operators are mixed

Whenever different operators appear in the same expression, whether infix, prefix, or postfix, they must be fully parenthesised. No exceptions. Chains of the same operator, like 1+2+3 can be ok, but anything else has to be resolved by the programmer.

In this case, 1+-2 can only be a single hypothetical +- operator and 1 + - 2 is a syntax error because the infix + and prefix - operators are mingled. The longest-token rule allows identifying ++ or -- as a single operator, and that will be consistent even if one of the signs is swapped, so flipping 1++2 into 1+-2 doesn't suddenly change the parse. In this way, errors happen early â€” either a syntax error, or no-such-operator â€” and ultimately programmers won't write the difficult expressions into their program in the first place.

This also deals to precedence and associativity issues. Especially when there are user-defined operators, there is no universal set of rules for interpreting how they combine, and even with only basic arithmetic operations mixing them together is a common source of bugs. A reader will never be faced with guessing how 1 # 2 @ 3 or 1#@2 is parsed, or have a subtle bug where refactoring xâ‡’x+1 introduced a bug changing 2 * x to 2 * x + 1, if any ambiguity must always be resolved immediately.

For cases like add 1 - 2, this same rule applies because juxtaposition is also an operator there â€” it's just a hidden one, written as a space or a token boundary between the add and 1. This is a syntax error and always has to be disambiguated as add (1 - 2), add 1 (-2), or (add 1) - 2 - no special cases.

## Drawbacks

This rule can be annoying for standard arithmetic expressions, but it's helpful whenever others are involved, and it can be a plus to be consistent. It also prohibits code like x||!y&&z, which is probably a plus for reducing logic errors too.

I wouldn't use this in every language, but it's a good option if the expected audience is novice users especially, or when unrestricted user operators are desirable and expected to be on the same level as built-in operators.

In Haskell and ML, which don't use parenthesis for function calls, their solution to the problem is that they require parenthesis around unary operators:

f x (-1)


# Spacing

Swift allows a * b and a*b for infix operators, but a *b is interpreted as a prefix operator on b, and a* b is interpreted as a postfix operator on a.

The I programming language uses whitespace for precedence too, rather than parentheses. I don't think this is very user-friendly, but hey, it exists.

• Oh, so that's why Swift doesn't allow uneven spacing in expressions like 2 + 2 (2+ 2 is not allowed). I had learned the rule, but didn't know there was a reason behind it other than cosmetic (because the lopsided version is ugly :D) Commented Jul 4, 2023 at 19:05
• C# also has this in some cases. The expression a++ + b has unary ++ "from the right" (postfix) on a and the right-most + is binary (i.e. addition), whereas a + ++b has unary ++ "from the left" (prefix) on b instead, and here the left-most + is the binary operator. There is also a + + +b in which the two right-most plusses are unary (behaves the same as a + (+(+b))). Of course, it is allowed to add parentheses if you prefer. In the absence of any spaces in between the plusses, like a+++b, there is some rule that says this reads as (a++) + b. Commented Jan 29 at 15:35
• ... (continued) And with spaces, you can have more plusses still, for example a++ + + ++b which has (in order from left to right) postfix ++, binary +, unary +, and prefix ++. Commented Jan 29 at 15:37

If you do come across any conflicts that may arise from a clash between unary and binary operators, I'd recommend checking out a language with a similar set of operators to see how they handle them. You could also write a few snippets in your language to see what mix-ups could occur and implement edge cases for them (e.g. x - -y can be written as x--y which could potentially get interpreted as x-- y or x --y).

Don't worry about missing a few, most of them will be caught in testing and the early versions of the language.