I don't think there is a universal definition, but I'll attempt one anyway: the primary expressions in a language are those which have the highest level of precedence, and for which there is no need to specify the relative precedence of different primary expressions because the syntax would never be ambiguous.
So, expressions like 1
(a literal) or x
(a simple name) are primary, but also f(x)
is primary in languages like Java because there are no other primaries you could replace f
or x
with that would need additional parentheses to be unambiguous. Likewise, x.y.z()
is primary because there is no syntactic ambiguity: it must mean (x.y).z()
, because x.(y.z())
is not valid.
Compare this with binary operator expressions, like x + y * z
where either (x + y) * z
or x + (y * z)
could be valid, or unary operator expressions like *x++
where either (*x)++
or *(x++)
could be valid. In these cases, precedence rules are required to disambiguate.
It's also worth thinking about this from the perspective of parsers. In a hand-written parser, the parseExpression()
function could look something like this:
function parseExpression(minPrecedence) {
let expr = parseUnaryExpression(minPrecedence);
while(hasNextBinaryOperator(minPrecedence)) {
let op = pollBinaryOperator();
let rhs = parseExpression(precedenceOf(op));
expr = new BinaryExpr(expr, op, rhs);
}
return expr;
}
This is an oversimplification, but let's keep it simple. You need a parseUnaryExpression()
function because you want to call it unconditionally; if you called parseExpression()
recursively there, it would never terminate. So then the parseUnaryExpression()
function could look like this:
function parseUnaryExpression(minPrecedence) {
if(hasNextUnaryOperator(minPrecedence)) {
let op = pollUnaryOperator();
let rhs = parseUnaryExpression(precedenceOf(op));
return new UnaryExpr(op, rhs);
} else {
return parsePrimaryExpression();
}
}
Again, this is an oversimplification (e.g. it ignores the possibility of postfix unary operators), but it gets the point across: the binary operators are already handled in the first function, and now this function handles all the unary operators, and then for the same reason ─ recursion needing a base case ─ you need a separate parsePrimaryExpression()
function.
However, note that parsePrimaryExpression()
doesn't need a minPrecedence
argument. This is because for all the remaining types of expression, we know when to stop parsing based on knowing what the last token in that expression should be. There might be a loop condition (e.g. to parse a.b.c.d
, keep going as long as there is another dot) but there is no need to consider precedence, because all operators with precedences below the maximum have already been handled elsewhere.