1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
use itertools::PeekingNext;
use skullbrain_diagnostics::{EmitDiagnostics, ParseError};
use skullbrain_lexer::{OperatorKind, TokenKind};
use crate::{state::ConcreteSyntaxTreeBuilder, Diagnostics, ParserState, SyntaxKind};
impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
pub(crate) fn parse_with_binding_power(&mut self, min_bp: u8) {
let checkpoint = self.checkpoint();
// FIRST: Handle prefix/unary operators and primary expressions
self.parse_optional_unary_prefix();
while let Some(item) = self.peek() {
let Some(op_kind) = item.kind().operator_ref() else {
break;
};
let Some((left_bp, right_bp)) = binding_power(op_kind) else {
break;
};
if left_bp < min_bp {
break;
}
let op_syntax = operator_to_syntax(op_kind);
self.start_node_at(checkpoint.clone(), SyntaxKind::Operator);
self.parse_token(op_syntax);
self.skip_trivia();
self.start_node(SyntaxKind::Rhs);
self.parse_with_binding_power(right_bp);
self.finish_node();
self.finish_node();
}
}
fn parse_optional_unary_prefix(&mut self) {
let item = self.peeking_next(
|i| matches!(i.kind(),TokenKind::Operator(op_kind) if is_unary_prefix(op_kind) ),
);
let start_checkpoint = self.checkpoint();
match item {
Some(item) => {
let Some(op_kind) = item.kind().operator_ref() else {
unreachable!()
};
self.start_node(SyntaxKind::Operator);
// For unary prefix, use the prefix binding power, not min_bp
let prefix_bp = prefix_binding_power(op_kind).unwrap_or(15);
self.token(operator_to_syntax(op_kind), item);
self.skip_trivia();
let before_expr_checkpoint = self.checkpoint();
self.parse_with_binding_power(prefix_bp);
self.skip_trivia();
// This ensures that for standalone unary expressions like "!true", the
// actual parses expression is under a node so that disambiguating unary-minus vs binary-minus is easier
if self.peek().is_none() {
self.start_node_at(before_expr_checkpoint, SyntaxKind::Rhs);
self.finish_node();
}
self.finish_node();
// Wrap the unary operator in an LHS node if there are more tokens to parse.
// This ensures that for standalone unary expressions like "!true", the root
// node is an Operator, but when part of a larger expression like "x + !true",
// the unary operator gets properly wrapped as the LHS of the binary operation.
if self.peek().is_some() {
self.start_node_at(start_checkpoint, SyntaxKind::Lhs);
self.finish_node();
}
}
None => {
// Check if we're about to encounter a binary operator without a left operand
// (e.g., "&& literal" or "+ 5" at start of expression)
// This catches malformed expressions
match self.peek() {
Some(item) if matches!(item.kind(),TokenKind::Operator(op) if op.is_not_assignment()) =>
{
let diagnostic_details = self.diagnostic_details();
let error = ParseError::expected_expression_before();
self.emit_diagnostic(error, diagnostic_details);
}
_ => self.__parse_actual_expression(),
}
self.skip_trivia();
// Only wrap the expression in an LHS node if it will be the left operand
// of a binary operation. For simple expressions like "ident", no LHS wrapper
// is needed, but for "ident + something", the "ident" needs LHS wrapping.
if let Some(_) = self.peek().filter(
|item| matches!(item.kind(),TokenKind::Operator(op) if op.is_not_assignment()),
) {
self.start_node_at(start_checkpoint, SyntaxKind::Lhs);
self.finish_node();
}
}
}
}
}
/// Returns the binding power for binary operators as (left_bp, right_bp).
/// Higher numbers = higher precedence (binds tighter).
///
/// Right-associative: left_bp > right_bp
/// Left-associative: left_bp < right_bp
const fn binding_power(op_kind: &OperatorKind) -> Option<(u8, u8)> {
match op_kind {
// Assignment is right-associative (a = b = c means a = (b = c))
// Lowest precedence of all operators
OperatorKind::Assignment => Some((2, 1)),
// Logical OR - lowest precedence, left-associative
OperatorKind::Or => Some((3, 4)),
// Logical AND - higher than OR, left-associative
OperatorKind::And => Some((5, 6)),
// Equality operators - same precedence, left-associative
OperatorKind::Eq | OperatorKind::NotEq => Some((7, 8)),
// Comparison operators - higher than equality, left-associative
OperatorKind::Lt | OperatorKind::Gt | OperatorKind::LtEq | OperatorKind::GtEq => {
Some((9, 10))
}
// Addition and Subtraction - left-associative
OperatorKind::Plus | OperatorKind::Minus => Some((11, 12)),
// Multiplication, Division, Modulo - highest precedence, left-associative
OperatorKind::Mul | OperatorKind::Divide | OperatorKind::Mod => Some((13, 14)),
// Field access/method chaining - highest precedence, left-associative
// This should bind tighter than unary operators
OperatorKind::MethodChaining => Some((17, 18)),
// Unary operators don't have binary binding power
OperatorKind::Not => None,
}
}
/// Returns the binding power for unary prefix operators.
/// Higher numbers = higher precedence.
const fn prefix_binding_power(op_kind: &OperatorKind) -> Option<u8> {
match op_kind {
// Unary minus: -x
OperatorKind::Minus => Some(15),
// Logical not: !x
OperatorKind::Not => Some(15),
// These are only binary operators
OperatorKind::Plus
| OperatorKind::Mul
| OperatorKind::Divide
| OperatorKind::Mod
| OperatorKind::Eq
| OperatorKind::NotEq
| OperatorKind::Lt
| OperatorKind::Gt
| OperatorKind::LtEq
| OperatorKind::GtEq
| OperatorKind::And
| OperatorKind::Or
| OperatorKind::Assignment
| OperatorKind::MethodChaining => None,
}
}
/// Check if this operator can be used as a unary prefix operator
const fn is_unary_prefix(op_kind: &OperatorKind) -> bool {
prefix_binding_power(op_kind).is_some()
}
const fn operator_to_syntax(op_kind: &OperatorKind) -> SyntaxKind {
match op_kind {
OperatorKind::Eq => SyntaxKind::OpEq,
OperatorKind::NotEq => SyntaxKind::OpNotEq,
OperatorKind::Lt => SyntaxKind::OpLt,
OperatorKind::Gt => SyntaxKind::OpGt,
OperatorKind::LtEq => SyntaxKind::OpLtEq,
OperatorKind::GtEq => SyntaxKind::OpGtEq,
OperatorKind::Minus => SyntaxKind::OpMinus,
OperatorKind::And => SyntaxKind::OpAnd,
OperatorKind::Or => SyntaxKind::OpOr,
OperatorKind::Not => SyntaxKind::OpNot,
OperatorKind::Plus => SyntaxKind::OpPlus,
OperatorKind::Mul => SyntaxKind::OpMul,
OperatorKind::Divide => SyntaxKind::OpDivide,
OperatorKind::Mod => SyntaxKind::OpMod,
OperatorKind::Assignment => SyntaxKind::Assignment,
OperatorKind::MethodChaining => SyntaxKind::OpMethodChaining,
}
}
#[cfg(test)]
mod test {
#[test]
fn test_arithmetic_or_boolean() {
crate::test::create_parsing_test("arithmetic_or_boolean/*.skull", |state| {
state.parse_expression();
});
}
}