Output-precedence ranks, derived directly from the parser’s Precedence
ladder (higher binds tighter) so it stays the single source of truth:
reordering or inserting a parser level reranks these automatically, and only
the variant each rank maps to is maintained by hand. They classify the top
operator an expr prints with, so the binary-operator printer can parenthesize
an operand that would otherwise re-associate on reparse. ATOM, the one rank
with no parser counterpart, is layered one above the tightest parser level to
mark the self-delimiting primaries. They never need parens.
The loosest precedence exposed on expr’s left spine, the mirror of
right_edge. For a right operand (an operator on its left), this is what
decides parenthesization: a left-associative operator printed to its left
reaches into the left spine and re-associates if that spine exposes a
precedence at or below the operator’s. The top operator alone is not enough,
because a left-nested chain can bury a looser operator down its left edge:
387 = ANY (...) LIKE a IN (...) has a top IN (Like) but exposes the
= ANY (Cmp) on its left, so a tighter <> to its left
(48 <> 387 = ANY (...) ...) would steal the <> into the = ANY’s left
rather than leave it as the <>’s right operand. Forms that open with their
own token on the left (a prefix operator, a keyword, (…), a literal) are
ATOM.
Whether the operand of a prefix operator (-/+/~) must be parenthesized
to round-trip. A prefix op binds tighter than COLLATE/AT TIME ZONE and
the binary/comparison operators, but looser than the postfix ::/[…]
forms — and - <number> additionally lexes as a negative literal. So peel
the tight postfixes (::/[…]); if the chain bottoms out at a numeric
literal the sign would fold into it, and if it bottoms out at anything other
than a self-delimiting non-COLLATE primary (a COLLATE, a binary op, …) the
prefix op would re-associate — both need parens. (a + b COLLATE c reparses
as a + (b COLLATE c); - x COLLATE c as (- x) COLLATE c.)
Whether expr prints in a self-delimiting form — atomic, or wrapped in its
own brackets/parens (name(...), (…), ARRAY[…], CASE … END, …) — so it
is safe to print immediately to the left of a tight postfix operator (::,
COLLATE, or the IN delimiter of the position(<needle> IN …) special
form) without the operator re-associating into the expression’s spine.
The loosest precedence exposed on expr’s right spine, the precedence at
which an operator printed immediately to its right would bind into it
rather than wrap it. For a left operand / subject of a construct that prints
to its right, this is what decides parenthesization (its mirror, left_edge,
decides right operands), because a prefix operator and the right operand of a
binary/BETWEEN/LIKE/IS DISTINCT FROM are right-transparent:
- NOT a IN (b) exposes the NOT’s IN on the right even though its top node
is unary -. Forms that close with a bracket on the right ((…), […],
::type, IS NULL) are ATOM.
The precedence at which a prefix operator (Op with no second operand)
parses its operand, mirroring Parser::parse_prefix: -/+ at
PrefixPlusMinus, but ~ (and namespaced prefixes) at Other, so ~ a + b
parses as ~ (a + b). ~ binds looser than +/-/*.
Write bound as a BETWEEN … AND … bound. The parser parses both bounds with
parse_subexpr(Precedence::Like) (see Parser::parse_between), starting fresh
with nothing to the bound’s left, so it walks the bound’s left spine and
stops at the first operator binding at or below Like, leaving that operator
outside the bound (x BETWEEN 1 IS NULL AND y parses 1 as the bound, then
expects AND but finds IS). A bound is therefore safe bare only when its
left edge binds strictly above Like. Use left_edge (not right_edge,
which closes at ATOM for the right-closing IS NULL/= ANY (…)/IN (…)
forms whose looseness is on the left). The parser wraps these bounds in
Expr::Nested (which is ATOM, so it prints bare). This re-adds the parens
for ASTs where that wrapper is absent.
Write expr as the receiver of a . operator (used by FieldAccess and
WildcardAccess), parenthesizing when the receiver could re-bind the
trailing dot on reparse. The . token has very high precedence and both
the lexer and parser greedily extend adjacent tokens: 1.x tokenizes the
number 1. and leaves x as an alias, and 'a'::T.x consumes T.x as a
qualified type name. The whitelist below covers receivers that print as
self-terminating syntax (parenthesized exprs, function calls, bracketed
collections, etc.). Anything else gets explicit parens.
Write left as the LHS of <left> <op> ANY/ALL (...). The printed <op> is an
ordinary binary infix. It can be any operator the parser accepts here, from
=/< (Cmp) all the way down to *///% (MultiplyDivide), and on
reparse it binds into any operator exposed on left’s right spine that is
strictly looser than <op> itself, stealing that suffix into the quantified
expression’s left rather than wrapping the whole left. So parenthesize
exactly when left’s right_edge binds looser than <op>’s own precedence
(binary_op_precedence), mirroring the binary-Op arm. Using the operator’s
real precedence (not a fixed Like threshold) both parenthesizes a
tighter-binding <op> over a looser left and leaves an equal-or-tighter left
bare (a = b = ANY (…), a LIKE b = ANY (…)), which the old fixed threshold
over-parenthesized. The tighter-binding case, (a + b) * ANY (…), would
otherwise print a + b * ANY (…) and reparse as the different
a + (b * ANY (…)). right_edge also sees a looser spine hidden under
right-transparent prefixes, e.g. the NOT’s IN in - NOT a IN (b) = ANY (…).
Write expr as the receiver of a […] subscript. An unparenthesized
Identifier(["map"]) reparses as Token::Keyword(MAP) followed by [,
which dispatches to parse_map (the map-literal grammar) instead of a
regular subscript. Parenthesize identifiers whose last component is a
context-sensitive keyword so the round trip stays an identifier subscript.