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