Skip to main content

harn_parser/
operators.rs

1//! Operator vocabulary: spelling, precedence, and associativity.
2//!
3//! This module is the single owner of Harn's expression-operator grammar. The
4//! parser climbs [`OperatorGrammar::CURRENT`], the formatter and `harn fix`
5//! decide parenthesization with [`operand_needs_parens`], and the spec's
6//! precedence table and the tree-sitter grammar's precedence constants are
7//! generated from or drift-checked against [`Precedence::ALL`].
8//!
9//! The groups follow Swift's standard precedence groups where Harn has the
10//! same operator: `??` binds looser than range formation and arithmetic and
11//! tighter than comparison, and range formation (`a..b`, `a..=b`) binds looser
12//! than arithmetic and does not associate.
13
14use std::fmt;
15
16/// A binary operator. Spelling, wire name, and precedence live here so no
17/// consumer compares operator text.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub enum BinaryOperator {
20    /// `|>`
21    Pipe,
22    /// `||`
23    Or,
24    /// `&&`
25    And,
26    /// `==`
27    Eq,
28    /// `!=`
29    Neq,
30    /// `<`
31    Lt,
32    /// `>`
33    Gt,
34    /// `<=`
35    Lte,
36    /// `>=`
37    Gte,
38    /// `in`
39    In,
40    /// `not in`
41    NotIn,
42    /// `??`
43    NilCoalesce,
44    /// `+`
45    Add,
46    /// `-`
47    Sub,
48    /// `*`
49    Mul,
50    /// `/`
51    Div,
52    /// `%`
53    Mod,
54    /// `**`
55    Pow,
56}
57
58impl BinaryOperator {
59    pub const ALL: [BinaryOperator; 18] = [
60        BinaryOperator::Pipe,
61        BinaryOperator::Or,
62        BinaryOperator::And,
63        BinaryOperator::Eq,
64        BinaryOperator::Neq,
65        BinaryOperator::Lt,
66        BinaryOperator::Gt,
67        BinaryOperator::Lte,
68        BinaryOperator::Gte,
69        BinaryOperator::In,
70        BinaryOperator::NotIn,
71        BinaryOperator::NilCoalesce,
72        BinaryOperator::Add,
73        BinaryOperator::Sub,
74        BinaryOperator::Mul,
75        BinaryOperator::Div,
76        BinaryOperator::Mod,
77        BinaryOperator::Pow,
78    ];
79
80    /// The operator as written in Harn source.
81    #[must_use]
82    pub const fn symbol(self) -> &'static str {
83        match self {
84            BinaryOperator::Pipe => "|>",
85            BinaryOperator::Or => "||",
86            BinaryOperator::And => "&&",
87            BinaryOperator::Eq => "==",
88            BinaryOperator::Neq => "!=",
89            BinaryOperator::Lt => "<",
90            BinaryOperator::Gt => ">",
91            BinaryOperator::Lte => "<=",
92            BinaryOperator::Gte => ">=",
93            BinaryOperator::In => "in",
94            BinaryOperator::NotIn => "not in",
95            BinaryOperator::NilCoalesce => "??",
96            BinaryOperator::Add => "+",
97            BinaryOperator::Sub => "-",
98            BinaryOperator::Mul => "*",
99            BinaryOperator::Div => "/",
100            BinaryOperator::Mod => "%",
101            BinaryOperator::Pow => "**",
102        }
103    }
104
105    /// The stable identifier used by AST JSON and other wire projections.
106    /// It equals [`Self::symbol`] except that `not in` is spelled `not_in`.
107    #[must_use]
108    pub const fn wire_name(self) -> &'static str {
109        match self {
110            BinaryOperator::NotIn => "not_in",
111            other => other.symbol(),
112        }
113    }
114
115    /// Resolve a wire name produced by [`Self::wire_name`].
116    #[must_use]
117    pub fn from_wire_name(name: &str) -> Option<Self> {
118        Self::ALL.into_iter().find(|op| op.wire_name() == name)
119    }
120
121    #[must_use]
122    pub const fn precedence(self) -> Precedence {
123        match self {
124            BinaryOperator::Pipe => Precedence::Pipe,
125            BinaryOperator::Or => Precedence::LogicalOr,
126            BinaryOperator::And => Precedence::LogicalAnd,
127            BinaryOperator::Eq | BinaryOperator::Neq => Precedence::Equality,
128            BinaryOperator::Lt
129            | BinaryOperator::Gt
130            | BinaryOperator::Lte
131            | BinaryOperator::Gte
132            | BinaryOperator::In
133            | BinaryOperator::NotIn => Precedence::Comparison,
134            BinaryOperator::NilCoalesce => Precedence::NilCoalescing,
135            BinaryOperator::Add | BinaryOperator::Sub => Precedence::Additive,
136            BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => {
137                Precedence::Multiplicative
138            }
139            BinaryOperator::Pow => Precedence::Exponent,
140        }
141    }
142
143    /// Whether a line break may precede the operator inside an expression.
144    ///
145    /// `-` must not, so a line that starts with `-x` remains a new statement;
146    /// `in` and `not in` must not, so a `for x in` header cannot be split.
147    #[must_use]
148    pub const fn continues_across_newline(self) -> bool {
149        !matches!(
150            self,
151            BinaryOperator::Sub | BinaryOperator::In | BinaryOperator::NotIn
152        )
153    }
154
155    /// The operator a compound assignment (`+=`, `-=`, ...) applies.
156    #[must_use]
157    pub const fn is_compound_assignable(self) -> bool {
158        matches!(
159            self,
160            BinaryOperator::Add
161                | BinaryOperator::Sub
162                | BinaryOperator::Mul
163                | BinaryOperator::Div
164                | BinaryOperator::Mod
165        )
166    }
167
168    /// Operators whose result is always a boolean.
169    #[must_use]
170    pub const fn is_comparison(self) -> bool {
171        matches!(
172            self.precedence(),
173            Precedence::Equality | Precedence::Comparison
174        )
175    }
176
177    #[must_use]
178    pub const fn is_logical(self) -> bool {
179        matches!(self, BinaryOperator::And | BinaryOperator::Or)
180    }
181
182    #[must_use]
183    pub const fn is_arithmetic(self) -> bool {
184        matches!(
185            self,
186            BinaryOperator::Add
187                | BinaryOperator::Sub
188                | BinaryOperator::Mul
189                | BinaryOperator::Div
190                | BinaryOperator::Mod
191                | BinaryOperator::Pow
192        )
193    }
194}
195
196impl fmt::Display for BinaryOperator {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        f.write_str(self.symbol())
199    }
200}
201
202impl serde::Serialize for BinaryOperator {
203    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
204        serializer.serialize_str(self.wire_name())
205    }
206}
207
208/// A prefix operator.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
210pub enum UnaryOperator {
211    /// `!`
212    Not,
213    /// `-`
214    Neg,
215}
216
217impl UnaryOperator {
218    pub const ALL: [UnaryOperator; 2] = [UnaryOperator::Not, UnaryOperator::Neg];
219
220    #[must_use]
221    pub const fn symbol(self) -> &'static str {
222        match self {
223            UnaryOperator::Not => "!",
224            UnaryOperator::Neg => "-",
225        }
226    }
227}
228
229impl fmt::Display for UnaryOperator {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        f.write_str(self.symbol())
232    }
233}
234
235impl serde::Serialize for UnaryOperator {
236    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
237        serializer.serialize_str(self.symbol())
238    }
239}
240
241/// How operators of equal precedence group.
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum Associativity {
244    Left,
245    Right,
246    /// Chaining requires explicit parentheses.
247    None,
248}
249
250impl Associativity {
251    #[must_use]
252    pub const fn as_str(self) -> &'static str {
253        match self {
254            Associativity::Left => "left",
255            Associativity::Right => "right",
256            Associativity::None => "none",
257        }
258    }
259}
260
261/// Expression precedence groups, loosest first. The derived ordering is the
262/// binding order of [`OperatorGrammar::CURRENT`].
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
264pub enum Precedence {
265    /// `a |> f`
266    Pipe,
267    /// `c ? a : b`
268    Ternary,
269    LogicalOr,
270    LogicalAnd,
271    Equality,
272    /// `<`, `>`, `<=`, `>=`, `in`, `not in`
273    Comparison,
274    /// `??`
275    NilCoalescing,
276    /// `a..b`, `a..=b`
277    Range,
278    Additive,
279    Multiplicative,
280    /// `!x`, `-x`
281    Prefix,
282    /// `a ** b`
283    Exponent,
284    /// Calls, member access, subscripts, postfix `?`/`!`, and every primary.
285    Postfix,
286}
287
288impl Precedence {
289    pub const ALL: [Precedence; 13] = [
290        Precedence::Pipe,
291        Precedence::Ternary,
292        Precedence::LogicalOr,
293        Precedence::LogicalAnd,
294        Precedence::Equality,
295        Precedence::Comparison,
296        Precedence::NilCoalescing,
297        Precedence::Range,
298        Precedence::Additive,
299        Precedence::Multiplicative,
300        Precedence::Prefix,
301        Precedence::Exponent,
302        Precedence::Postfix,
303    ];
304
305    /// Stable group name used by the spec table and the tree-sitter `PREC`
306    /// constants.
307    #[must_use]
308    pub const fn name(self) -> &'static str {
309        match self {
310            Precedence::Pipe => "pipe",
311            Precedence::Ternary => "ternary",
312            Precedence::LogicalOr => "logical_or",
313            Precedence::LogicalAnd => "logical_and",
314            Precedence::Equality => "equality",
315            Precedence::Comparison => "comparison",
316            Precedence::NilCoalescing => "nil_coalescing",
317            Precedence::Range => "range",
318            Precedence::Additive => "additive",
319            Precedence::Multiplicative => "multiplicative",
320            Precedence::Prefix => "prefix",
321            Precedence::Exponent => "exponent",
322            Precedence::Postfix => "postfix",
323        }
324    }
325
326    #[must_use]
327    pub const fn associativity(self) -> Associativity {
328        match self {
329            Precedence::Ternary | Precedence::Prefix | Precedence::Exponent => Associativity::Right,
330            Precedence::Range => Associativity::None,
331            _ => Associativity::Left,
332        }
333    }
334
335    /// What the group is, for the spec's precedence table.
336    #[must_use]
337    pub const fn description(self) -> &'static str {
338        match self {
339            Precedence::Pipe => "Pipe",
340            Precedence::Ternary => "Ternary conditional",
341            Precedence::LogicalOr => "Logical OR",
342            Precedence::LogicalAnd => "Logical AND",
343            Precedence::Equality => "Equality",
344            Precedence::Comparison => "Comparison / membership",
345            Precedence::NilCoalescing => "Nil coalescing",
346            Precedence::Range => "Range formation",
347            Precedence::Additive => "Additive",
348            Precedence::Multiplicative => "Multiplicative",
349            Precedence::Prefix => "Unary prefix",
350            Precedence::Exponent => "Exponentiation",
351            Precedence::Postfix => "Postfix",
352        }
353    }
354
355    /// Source spellings of the operators in this group, for documentation.
356    #[must_use]
357    pub const fn spellings(self) -> &'static [&'static str] {
358        match self {
359            Precedence::Pipe => &["|>"],
360            Precedence::Ternary => &["? :"],
361            Precedence::LogicalOr => &["||"],
362            Precedence::LogicalAnd => &["&&"],
363            Precedence::Equality => &["==", "!="],
364            Precedence::Comparison => &["<", ">", "<=", ">=", "in", "not in"],
365            Precedence::NilCoalescing => &["??"],
366            Precedence::Range => &[RANGE_EXCLUSIVE, RANGE_INCLUSIVE],
367            Precedence::Additive => &["+", "-"],
368            Precedence::Multiplicative => &["*", "/", "%"],
369            Precedence::Prefix => &["!", "-"],
370            Precedence::Exponent => &["**"],
371            Precedence::Postfix => &[".", "?.", "[]", "?.[]", "[:]", "()", "?", "!"],
372        }
373    }
374
375    /// One-based binding strength in the current grammar (1 = loosest).
376    #[must_use]
377    pub fn level(self) -> usize {
378        Self::ALL
379            .iter()
380            .position(|group| *group == self)
381            .map_or(0, |index| index + 1)
382    }
383
384    /// The precedence an AST expression node occupies in the current grammar.
385    #[must_use]
386    pub fn of_node(node: &crate::Node) -> Precedence {
387        match node {
388            crate::Node::BinaryOp { op, .. } => op.precedence(),
389            crate::Node::UnaryOp { .. } => Precedence::Prefix,
390            crate::Node::RangeExpr { .. } => Precedence::Range,
391            crate::Node::Ternary { .. } => Precedence::Ternary,
392            _ => Precedence::Postfix,
393        }
394    }
395}
396
397/// Exclusive-end range spelling: `0..n` yields `0` through `n - 1`.
398pub const RANGE_EXCLUSIVE: &str = "..";
399/// Inclusive-end range spelling: `1..=n` yields `1` through `n`.
400pub const RANGE_INCLUSIVE: &str = "..=";
401
402/// Which operand of an operator a child expression occupies.
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
404pub enum OperandSide {
405    Left,
406    Right,
407    /// The single operand of a prefix operator.
408    Prefix,
409    /// The receiver of a call, member access, subscript, or postfix operator.
410    Postfix,
411    /// The condition of `c ? a : b`.
412    TernaryCondition,
413    /// Either branch of `c ? a : b`.
414    TernaryBranch,
415}
416
417/// The loosest precedence an expression may have to appear bare as the
418/// `side` operand of an operator in group `parent`.
419#[must_use]
420pub fn loosest_bare_operand(parent: Precedence, side: OperandSide) -> Precedence {
421    match side {
422        OperandSide::Prefix => Precedence::Prefix,
423        OperandSide::Postfix => Precedence::Postfix,
424        OperandSide::TernaryCondition => Precedence::LogicalOr,
425        OperandSide::TernaryBranch => Precedence::Ternary,
426        // The exponent accepts a prefix operand (`2 ** -1`) while the base
427        // binds tighter than a prefix (`-2 ** 2` is `-(2 ** 2)`).
428        OperandSide::Right if parent == Precedence::Exponent => Precedence::Prefix,
429        OperandSide::Left | OperandSide::Right => {
430            let same_group_allowed = matches!(
431                (parent.associativity(), side),
432                (Associativity::Left, OperandSide::Left)
433                    | (Associativity::Right, OperandSide::Right)
434            );
435            if same_group_allowed {
436                parent
437            } else {
438                parent.tighter().unwrap_or(Precedence::Postfix)
439            }
440        }
441    }
442}
443
444/// Whether `child` must be parenthesized to keep its grouping as the `side`
445/// operand of `parent` in the current grammar.
446#[must_use]
447pub fn operand_needs_parens(parent: Precedence, child: Precedence, side: OperandSide) -> bool {
448    child < loosest_bare_operand(parent, side)
449}
450
451impl Precedence {
452    /// The next tighter group, if any.
453    #[must_use]
454    pub fn tighter(self) -> Option<Precedence> {
455        Self::ALL.get(self.level()).copied()
456    }
457}
458
459/// A precedence ladder the parser climbs. [`Self::CURRENT`] is the language;
460/// [`Self::LEGACY`] reproduces the pre-0.11 operator grammar (`??` between
461/// additive and multiplicative, `a to b [exclusive]` ranges just above `|>`)
462/// and exists only so `harn fix --migrate-operator-syntax` can read old source.
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub struct OperatorGrammar {
465    ladder: &'static [Precedence],
466    legacy_ranges: bool,
467}
468
469impl OperatorGrammar {
470    pub const CURRENT: OperatorGrammar = OperatorGrammar {
471        ladder: &Precedence::ALL,
472        legacy_ranges: false,
473    };
474
475    pub const LEGACY: OperatorGrammar = OperatorGrammar {
476        ladder: &[
477            Precedence::Pipe,
478            Precedence::Range,
479            Precedence::Ternary,
480            Precedence::LogicalOr,
481            Precedence::LogicalAnd,
482            Precedence::Equality,
483            Precedence::Comparison,
484            Precedence::Additive,
485            Precedence::NilCoalescing,
486            Precedence::Multiplicative,
487            Precedence::Prefix,
488            Precedence::Exponent,
489            Precedence::Postfix,
490        ],
491        legacy_ranges: true,
492    };
493
494    #[must_use]
495    pub fn loosest(self) -> Precedence {
496        self.ladder[0]
497    }
498
499    /// The group one step tighter than `group` on this ladder.
500    #[must_use]
501    pub fn tighter(self, group: Precedence) -> Precedence {
502        self.ladder
503            .iter()
504            .position(|candidate| *candidate == group)
505            .and_then(|index| self.ladder.get(index + 1))
506            .copied()
507            .unwrap_or(Precedence::Postfix)
508    }
509
510    /// Whether this grammar spells ranges `a to b [exclusive]`.
511    #[must_use]
512    pub fn legacy_ranges(self) -> bool {
513        self.legacy_ranges
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    #[test]
522    fn wire_names_round_trip() {
523        for op in BinaryOperator::ALL {
524            assert_eq!(BinaryOperator::from_wire_name(op.wire_name()), Some(op));
525        }
526        assert_eq!(BinaryOperator::from_wire_name("not in"), None);
527    }
528
529    #[test]
530    fn nil_coalescing_sits_between_comparison_and_range() {
531        assert!(Precedence::Comparison < Precedence::NilCoalescing);
532        assert!(Precedence::NilCoalescing < Precedence::Range);
533        assert!(Precedence::Range < Precedence::Additive);
534    }
535
536    #[test]
537    fn parenthesization_follows_associativity() {
538        let add = Precedence::Additive;
539        assert!(!operand_needs_parens(add, add, OperandSide::Left));
540        assert!(operand_needs_parens(add, add, OperandSide::Right));
541        assert!(operand_needs_parens(
542            Precedence::Range,
543            Precedence::Range,
544            OperandSide::Left
545        ));
546        assert!(!operand_needs_parens(
547            Precedence::Exponent,
548            Precedence::Prefix,
549            OperandSide::Right
550        ));
551        assert!(operand_needs_parens(
552            Precedence::Exponent,
553            Precedence::Prefix,
554            OperandSide::Left
555        ));
556        assert!(operand_needs_parens(
557            Precedence::Additive,
558            Precedence::NilCoalescing,
559            OperandSide::Left
560        ));
561        assert!(!operand_needs_parens(
562            Precedence::NilCoalescing,
563            Precedence::Additive,
564            OperandSide::Right
565        ));
566    }
567
568    #[test]
569    fn legacy_ladder_is_a_permutation_of_the_current_groups() {
570        let mut legacy = OperatorGrammar::LEGACY.ladder.to_vec();
571        legacy.sort();
572        assert_eq!(legacy, Precedence::ALL.to_vec());
573    }
574}