Skip to content

Commit ca19232

Browse files
fix operator precedence
1 parent 5ccfa65 commit ca19232

4 files changed

Lines changed: 138 additions & 20 deletions

File tree

hcl2/rule_transformer/hcl2.lark

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ FLOAT_LITERAL: (NEGATIVE_DECIMAL? DECIMAL+ | NEGATIVE_DECIMAL+) "." DECIMAL+ (EX
2424
| (NEGATIVE_DECIMAL? DECIMAL+ | NEGATIVE_DECIMAL+) (EXP_MARK)
2525

2626
// Operators
27-
BINARY_OP : DOUBLE_EQ | NEQ | LT | GT | LEQ | GEQ | MINUS | ASTERISK | SLASH | PERCENT | DOUBLE_AMP | DOUBLE_PIPE | PLUS
2827
DOUBLE_EQ : "=="
2928
NEQ : "!="
3029
LT : "<"
@@ -99,16 +98,61 @@ string_part: STRING_CHARS
9998
| interpolation
10099

101100
// Expressions
102-
?expression : expr_term | operation | conditional
101+
?expression : or_expr QMARK new_line_or_comment? expression new_line_or_comment? COLON new_line_or_comment? expression -> conditional
102+
| or_expr
103103
interpolation: INTERP_START expression RBRACE
104-
conditional : expression QMARK new_line_or_comment? expression new_line_or_comment? COLON new_line_or_comment? expression
105104

106-
// Operations
107-
?operation : unary_op | binary_op
105+
// Operator precedence ladder (lowest to highest)
106+
// Each level uses left recursion for left-associativity.
107+
// Rule aliases (-> binary_op, -> binary_term, -> binary_operator) maintain
108+
// transformer compatibility with BinaryOpRule / BinaryTermRule / BinaryOperatorRule.
109+
110+
// Logical OR
111+
?or_expr : or_expr or_binary_term new_line_or_comment? -> binary_op
112+
| and_expr
113+
or_binary_term : or_binary_operator new_line_or_comment? and_expr -> binary_term
114+
!or_binary_operator : DOUBLE_PIPE -> binary_operator
115+
116+
// Logical AND
117+
?and_expr : and_expr and_binary_term new_line_or_comment? -> binary_op
118+
| eq_expr
119+
and_binary_term : and_binary_operator new_line_or_comment? eq_expr -> binary_term
120+
!and_binary_operator : DOUBLE_AMP -> binary_operator
121+
122+
// Equality
123+
?eq_expr : eq_expr eq_binary_term new_line_or_comment? -> binary_op
124+
| rel_expr
125+
eq_binary_term : eq_binary_operator new_line_or_comment? rel_expr -> binary_term
126+
!eq_binary_operator : DOUBLE_EQ -> binary_operator
127+
| NEQ -> binary_operator
128+
129+
// Relational
130+
?rel_expr : rel_expr rel_binary_term new_line_or_comment? -> binary_op
131+
| add_expr
132+
rel_binary_term : rel_binary_operator new_line_or_comment? add_expr -> binary_term
133+
!rel_binary_operator : LT -> binary_operator
134+
| GT -> binary_operator
135+
| LEQ -> binary_operator
136+
| GEQ -> binary_operator
137+
138+
// Additive
139+
?add_expr : add_expr add_binary_term new_line_or_comment? -> binary_op
140+
| mul_expr
141+
add_binary_term : add_binary_operator new_line_or_comment? mul_expr -> binary_term
142+
!add_binary_operator : PLUS -> binary_operator
143+
| MINUS -> binary_operator
144+
145+
// Multiplicative
146+
?mul_expr : mul_expr mul_binary_term new_line_or_comment? -> binary_op
147+
| unary_expr
148+
mul_binary_term : mul_binary_operator new_line_or_comment? unary_expr -> binary_term
149+
!mul_binary_operator : ASTERISK -> binary_operator
150+
| SLASH -> binary_operator
151+
| PERCENT -> binary_operator
152+
153+
// Unary (highest precedence for operations)
154+
?unary_expr : unary_op | expr_term
108155
!unary_op : (MINUS | NOT) expr_term
109-
binary_op : expression binary_term new_line_or_comment?
110-
binary_term : binary_operator new_line_or_comment? expression
111-
!binary_operator : BINARY_OP
112156

113157
// Expression terms
114158
expr_term : LPAR new_line_or_comment? expression new_line_or_comment? RPAR

hcl2/rule_transformer/reconstructor.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,32 @@
66
from hcl2.rule_transformer.rules.for_expressions import ForIntroRule
77
from hcl2.rule_transformer.rules.literal_rules import IdentifierRule
88
from hcl2.rule_transformer.rules.strings import StringRule
9-
from hcl2.rule_transformer.rules.expressions import ExprTermRule, ConditionalRule
9+
from hcl2.rule_transformer.rules.expressions import (
10+
ExprTermRule,
11+
ConditionalRule,
12+
UnaryOpRule,
13+
)
1014

1115

1216
class HCLReconstructor:
1317
"""This class converts a Lark.Tree AST back into a string representing the underlying HCL code."""
1418

19+
_binary_op_types = {
20+
"DOUBLE_EQ",
21+
"NEQ",
22+
"LT",
23+
"GT",
24+
"LEQ",
25+
"GEQ",
26+
"MINUS",
27+
"ASTERISK",
28+
"SLASH",
29+
"PERCENT",
30+
"DOUBLE_AMP",
31+
"DOUBLE_PIPE",
32+
"PLUS",
33+
}
34+
1535
def __init__(self):
1636
self._reset_state()
1737

@@ -105,8 +125,14 @@ def _should_add_space_before(
105125
if tokens.EQ.lark_name() in [token_type, self._last_token_name]:
106126
return True
107127

108-
# space around binary operators
109-
if tokens.BINARY_OP.lark_name() in [token_type, self._last_token_name]:
128+
# Don't add space around operator tokens inside unary_op
129+
if parent_rule_name == UnaryOpRule.lark_name():
130+
return False
131+
132+
if (
133+
token_type in self._binary_op_types
134+
or self._last_token_name in self._binary_op_types
135+
):
110136
return True
111137

112138
elif isinstance(current_node, Tree):
@@ -130,7 +156,14 @@ def _reconstruct_tree(self, tree: Tree, parent_rule_name: str = None) -> List[st
130156
result = []
131157
rule_name = tree.data
132158

133-
if rule_name == ExprTermRule.lark_name():
159+
if rule_name == UnaryOpRule.lark_name():
160+
for i, child in enumerate(tree.children):
161+
result.extend(self._reconstruct_node(child, rule_name))
162+
if i == 0:
163+
# Suppress space between unary operator and its operand
164+
self._last_was_space = True
165+
166+
elif rule_name == ExprTermRule.lark_name():
134167
# Check if parenthesized
135168
if (
136169
len(tree.children) >= 3

hcl2/rule_transformer/rules/expressions.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,30 @@ class ExpressionRule(InlineCommentMixIn, ABC):
2626
def lark_name() -> str:
2727
return "expression"
2828

29-
def __init__(self, children, meta: Optional[Meta] = None):
29+
def __init__(
30+
self, children, meta: Optional[Meta] = None, parentheses: bool = False
31+
):
3032
super().__init__(children, meta)
33+
self._parentheses = parentheses
34+
35+
def _wrap_into_parentheses(
36+
self, value: str, options=SerializationOptions(), context=SerializationContext()
37+
) -> str:
38+
# do not wrap into parentheses if
39+
# 1. already wrapped or
40+
# 2. is top-level expression (unless explicitly wrapped)
41+
if context.inside_parentheses:
42+
return value
43+
# Look through ExprTermRule wrapper to determine if truly nested
44+
parent = getattr(self, "parent", None)
45+
if parent is None:
46+
return value
47+
if isinstance(parent, ExprTermRule):
48+
if not isinstance(parent.parent, ExpressionRule):
49+
return value
50+
elif not isinstance(parent, ExpressionRule):
51+
return value
52+
return wrap_into_parentheses(value)
3153

3254

3355
class ExprTermRule(ExpressionRule):
@@ -47,18 +69,18 @@ def lark_name() -> str:
4769
return "expr_term"
4870

4971
def __init__(self, children, meta: Optional[Meta] = None):
50-
self._parentheses = False
72+
parentheses = False
5173
if (
5274
isinstance(children[0], LarkToken)
5375
and children[0].lark_name() == "LPAR"
5476
and isinstance(children[-1], LarkToken)
5577
and children[-1].lark_name() == "RPAR"
5678
):
57-
self._parentheses = True
79+
parentheses = True
5880
else:
5981
children = [None, *children, None]
6082
self._insert_optionals(children, [1, 3])
61-
super().__init__(children, meta)
83+
super().__init__(children, meta, parentheses)
6284

6385
@property
6486
def parentheses(self) -> bool:
@@ -71,7 +93,10 @@ def expression(self) -> ExpressionRule:
7193
def serialize(
7294
self, options=SerializationOptions(), context=SerializationContext()
7395
) -> Any:
74-
result = self.expression.serialize(options, context)
96+
with context.modify(
97+
inside_parentheses=self.parentheses or context.inside_parentheses
98+
):
99+
result = self.expression.serialize(options, context)
75100

76101
if self.parentheses:
77102
result = wrap_into_parentheses(result)
@@ -127,6 +152,9 @@ def serialize(
127152
if not context.inside_dollar_string:
128153
result = to_dollar_string(result)
129154

155+
if options.force_operation_parentheses:
156+
result = self._wrap_into_parentheses(result, options, context)
157+
130158
return result
131159

132160

@@ -192,6 +220,9 @@ def serialize(
192220

193221
if not context.inside_dollar_string:
194222
result = to_dollar_string(result)
223+
224+
if options.force_operation_parentheses:
225+
result = self._wrap_into_parentheses(result, options, context)
195226
return result
196227

197228

@@ -214,6 +245,14 @@ def expr_term(self):
214245
def serialize(
215246
self, options=SerializationOptions(), context=SerializationContext()
216247
) -> Any:
217-
return to_dollar_string(
218-
f"{self.operator}{self.expr_term.serialize(options, context)}"
219-
)
248+
249+
with context.modify(inside_dollar_string=True):
250+
result = f"{self.operator}{self.expr_term.serialize(options, context)}"
251+
252+
if not context.inside_dollar_string:
253+
result = to_dollar_string(result)
254+
255+
if options.force_operation_parentheses:
256+
result = self._wrap_into_parentheses(result, options, context)
257+
258+
return result

hcl2/rule_transformer/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ class SerializationOptions:
1515
wrap_tuples: bool = False
1616
explicit_blocks: bool = True
1717
preserve_heredocs: bool = True
18+
force_operation_parentheses: bool = False
1819

1920

2021
@dataclass
2122
class SerializationContext:
2223
inside_dollar_string: bool = False
24+
inside_parentheses: bool = False
2325

2426
def replace(self, **kwargs) -> "SerializationContext":
2527
return replace(self, **kwargs)

0 commit comments

Comments
 (0)