Skip to main content

conjure_cp_essence_parser/parser/
expression.rs

1use crate::diagnostics::diagnostics_api::SymbolKind;
2use crate::errors::{FatalParseError, RecoverableParseError};
3use crate::parser::ParseContext;
4use crate::parser::atom::parse_atom;
5use crate::parser::comprehension::parse_quantifier_or_aggregate_expr;
6use crate::util::TypecheckingContext;
7use crate::util::named_children;
8use crate::{child, field, named_child};
9use conjure_cp_core::ast::{Atom, DeclarationKind, ReturnType, Typeable};
10use conjure_cp_core::ast::{Expression, Metadata, Moo};
11use conjure_cp_core::into_matrix_expr;
12use conjure_cp_core::{domain_int, matrix_expr, range};
13use tree_sitter::Node;
14use uniplate::Uniplate;
15
16pub fn parse_expression(
17    ctx: &mut ParseContext,
18    node: Node,
19) -> Result<Option<Expression>, FatalParseError> {
20    match node.kind() {
21        "atom" => parse_atom(ctx, &node),
22        "bool_expr" => parse_boolean_expression(ctx, &node),
23        "arithmetic_expr" => parse_arithmetic_expression(ctx, &node),
24        "comparison_expr" => parse_comparison_expression(ctx, &node),
25        "dominance_relation" => parse_dominance_relation(ctx, &node),
26        "all_diff_comparison" => parse_all_diff_comparison(ctx, &node),
27        _ => {
28            ctx.record_error(RecoverableParseError::new(
29                format!("Unexpected expression type: '{}'", node.kind()),
30                Some(node.range()),
31            ));
32            Ok(None)
33        }
34    }
35}
36
37fn parse_dominance_relation(
38    ctx: &mut ParseContext,
39    node: &Node,
40) -> Result<Option<Expression>, FatalParseError> {
41    if ctx.root.kind() == "dominance_relation" {
42        ctx.record_error(RecoverableParseError::new(
43            "Nested dominance relations are not allowed".to_string(),
44            Some(node.range()),
45        ));
46        return Ok(None);
47    }
48
49    let Some(inner_node) = field!(recover, ctx, node, "expression") else {
50        return Ok(None);
51    };
52
53    // NB: In all other cases, we keep the root the same;
54    // However, here we create a new context with the new root so downstream functions
55    // know we are inside a dominance relation
56    let mut inner_ctx = ParseContext {
57        source_code: ctx.source_code,
58        root: node,
59        symbols: ctx.symbols.clone(),
60        errors: ctx.errors,
61        source_map: &mut *ctx.source_map,
62        decl_spans: ctx.decl_spans,
63        typechecking_context: ctx.typechecking_context,
64    };
65
66    let Some(inner) = parse_expression(&mut inner_ctx, inner_node)? else {
67        return Ok(None);
68    };
69
70    Ok(Some(Expression::DominanceRelation(
71        Metadata::new(),
72        Moo::new(inner),
73    )))
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77enum ParetoDirection {
78    Minimising,
79    Maximising,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83enum ReferenceRewriteAction {
84    LeaveAsIs,
85    ExpandValueLetting,
86    WrapInFromSolution,
87}
88
89pub fn parse_pareto_expression(
90    ctx: &mut ParseContext,
91    node: &Node,
92) -> Result<Option<Expression>, FatalParseError> {
93    if ctx.root.kind() != "dominance_relation" {
94        ctx.record_error(RecoverableParseError::new(
95            "pareto(...) only allowed inside dominance relations".to_string(),
96            Some(node.range()),
97        ));
98        return Ok(None);
99    }
100
101    let mut non_worsening = Vec::new();
102    let mut strict_improvements = Vec::new();
103    let components = field!(node, "components");
104
105    if components.kind() != "pareto_items" {
106        return Err(FatalParseError::internal_error(
107            format!("Unexpected pareto component list: '{}'", components.kind()),
108            Some(components.range()),
109        ));
110    }
111
112    for item_node in named_children(&components) {
113        let direction_node = field!(item_node, "direction");
114        let direction_str =
115            &ctx.source_code[direction_node.start_byte()..direction_node.end_byte()];
116        let direction = match direction_str {
117            "minimising" => ParetoDirection::Minimising,
118            "maximising" => ParetoDirection::Maximising,
119            _ => {
120                return Err(FatalParseError::internal_error(
121                    format!("Unexpected pareto direction: '{direction_str}'"),
122                    Some(direction_node.range()),
123                ));
124            }
125        };
126
127        let component_node = field!(item_node, "expression");
128        let Some(component_expr) = parse_pareto_component(ctx, &component_node)? else {
129            return Ok(None);
130        };
131        let Some((non_worse, strict)) =
132            build_pareto_constraints(ctx, &component_node, component_expr, direction)
133        else {
134            return Ok(None);
135        };
136        non_worsening.push(non_worse);
137        strict_improvements.push(strict);
138    }
139
140    let mut conjuncts = non_worsening;
141    conjuncts.push(combine_with_and_or(strict_improvements, true));
142
143    Ok(Some(combine_with_and_or(conjuncts, false)))
144}
145
146fn parse_pareto_component(
147    ctx: &mut ParseContext,
148    node: &Node,
149) -> Result<Option<Expression>, FatalParseError> {
150    let saved_context = ctx.typechecking_context;
151    ctx.typechecking_context = TypecheckingContext::Unknown;
152    let parsed = parse_expression(ctx, *node)?;
153    ctx.typechecking_context = saved_context;
154    Ok(parsed)
155}
156
157fn build_pareto_constraints(
158    ctx: &mut ParseContext,
159    node: &Node,
160    component: Expression,
161    direction: ParetoDirection,
162) -> Option<(Expression, Expression)> {
163    if component
164        .universe()
165        .iter()
166        .any(|expr| matches!(expr, Expression::FromSolution(_, _)))
167    {
168        ctx.record_error(RecoverableParseError::new(
169            "pareto(...) components cannot contain fromSolution(...) explicitly".to_string(),
170            Some(node.range()),
171        ));
172        return None;
173    }
174
175    let current = expand_value_lettings(&component);
176    let previous = lift_to_previous_solution(&current);
177
178    match current.return_type() {
179        ReturnType::Int => Some(match direction {
180            ParetoDirection::Minimising => (
181                Expression::Leq(
182                    Metadata::new(),
183                    Moo::new(current.clone()),
184                    Moo::new(previous.clone()),
185                ),
186                Expression::Lt(Metadata::new(), Moo::new(current), Moo::new(previous)),
187            ),
188            ParetoDirection::Maximising => (
189                Expression::Geq(
190                    Metadata::new(),
191                    Moo::new(current.clone()),
192                    Moo::new(previous.clone()),
193                ),
194                Expression::Gt(Metadata::new(), Moo::new(current), Moo::new(previous)),
195            ),
196        }),
197        ReturnType::Bool => Some(match direction {
198            ParetoDirection::Minimising => (
199                Expression::Imply(
200                    Metadata::new(),
201                    Moo::new(current.clone()),
202                    Moo::new(previous.clone()),
203                ),
204                combine_with_and_or(
205                    vec![
206                        Expression::Not(Metadata::new(), Moo::new(current)),
207                        previous,
208                    ],
209                    false,
210                ),
211            ),
212            ParetoDirection::Maximising => (
213                Expression::Imply(
214                    Metadata::new(),
215                    Moo::new(previous.clone()),
216                    Moo::new(current.clone()),
217                ),
218                combine_with_and_or(
219                    vec![
220                        current,
221                        Expression::Not(Metadata::new(), Moo::new(previous)),
222                    ],
223                    false,
224                ),
225            ),
226        }),
227        found => {
228            ctx.record_error(RecoverableParseError::new(
229                format!(
230                    "pareto(...) only supports int or bool components, found '{}'",
231                    found
232                ),
233                Some(node.range()),
234            ));
235            None
236        }
237    }
238}
239
240fn expand_value_lettings(expr: &Expression) -> Expression {
241    rewrite_references(expr, false)
242}
243
244fn lift_to_previous_solution(expr: &Expression) -> Expression {
245    rewrite_references(expr, true)
246}
247
248fn rewrite_references(expr: &Expression, to_previous_solution: bool) -> Expression {
249    let mut lifted = expr.clone();
250
251    loop {
252        let next = lifted.rewrite(&|subexpr| match subexpr {
253            Expression::Atomic(_, Atom::Reference(ref reference)) => {
254                let action = {
255                    let kind = reference.ptr.kind();
256                    match &*kind {
257                        DeclarationKind::Find(_) if to_previous_solution => {
258                            ReferenceRewriteAction::WrapInFromSolution
259                        }
260                        DeclarationKind::Find(_) => ReferenceRewriteAction::LeaveAsIs,
261                        DeclarationKind::ValueLetting(_, _)
262                        | DeclarationKind::TemporaryValueLetting(_) => {
263                            ReferenceRewriteAction::ExpandValueLetting
264                        }
265                        DeclarationKind::Given(_)
266                        | DeclarationKind::Quantified(_)
267                        | DeclarationKind::QuantifiedExpr(_)
268                        | DeclarationKind::DomainLetting(_)
269                        | DeclarationKind::RecordField(_)
270                        | _ => ReferenceRewriteAction::LeaveAsIs,
271                    }
272                };
273
274                match action {
275                    ReferenceRewriteAction::LeaveAsIs => Some(subexpr),
276                    ReferenceRewriteAction::ExpandValueLetting => reference.resolve_expression(),
277                    ReferenceRewriteAction::WrapInFromSolution => Some(Expression::FromSolution(
278                        Metadata::new(),
279                        Moo::new(Atom::Reference(reference.clone())),
280                    )),
281                }
282            }
283            _ => Some(subexpr),
284        });
285
286        if next == lifted {
287            return lifted;
288        }
289
290        lifted = next;
291    }
292}
293
294fn combine_with_and_or(exprs: Vec<Expression>, is_or: bool) -> Expression {
295    match exprs.len() {
296        0 => {
297            if is_or {
298                Expression::Or(Metadata::new(), Moo::new(into_matrix_expr![exprs]))
299            } else {
300                Expression::And(Metadata::new(), Moo::new(into_matrix_expr![exprs]))
301            }
302        }
303        1 => match exprs.into_iter().next() {
304            Some(expr) => expr,
305            None => unreachable!("vector length already checked"),
306        },
307        _ => {
308            if is_or {
309                Expression::Or(Metadata::new(), Moo::new(into_matrix_expr![exprs]))
310            } else {
311                Expression::And(Metadata::new(), Moo::new(into_matrix_expr![exprs]))
312            }
313        }
314    }
315}
316
317fn parse_arithmetic_expression(
318    ctx: &mut ParseContext,
319    node: &Node,
320) -> Result<Option<Expression>, FatalParseError> {
321    ctx.typechecking_context = TypecheckingContext::Arithmetic;
322    let Some(inner) = named_child!(recover, ctx, node) else {
323        return Ok(None);
324    };
325    match inner.kind() {
326        "atom" => parse_atom(ctx, &inner),
327        "negative_expr" | "abs_value" | "sub_arith_expr" | "factorial_expr" => {
328            parse_unary_expression(ctx, &inner)
329        }
330        "toInt_expr" => {
331            // add special handling for toInt, as it is arithmetic but takes a non-arithmetic operand
332            ctx.typechecking_context = TypecheckingContext::Unknown;
333            parse_unary_expression(ctx, &inner)
334        }
335        "exponent" | "product_expr" | "sum_expr" => parse_binary_expression(ctx, &inner),
336        "list_combining_expr_arith" => parse_list_combining_expression(ctx, &inner),
337        "aggregate_expr" => parse_quantifier_or_aggregate_expr(ctx, &inner),
338        _ => {
339            ctx.record_error(RecoverableParseError::new(
340                format!("Expected arithmetic expression, found: {}", inner.kind()),
341                Some(inner.range()),
342            ));
343            Ok(None)
344        }
345    }
346}
347
348fn parse_comparison_expression(
349    ctx: &mut ParseContext,
350    node: &Node,
351) -> Result<Option<Expression>, FatalParseError> {
352    let Some(inner) = named_child!(recover, ctx, node) else {
353        return Ok(None);
354    };
355    match inner.kind() {
356        "arithmetic_comparison" => {
357            // Arithmetic comparisons require arithmetic operands
358            ctx.typechecking_context = TypecheckingContext::Arithmetic;
359            parse_binary_expression(ctx, &inner)
360        }
361        "lex_comparison" => {
362            // TODO: check that both operands are comparable collections.
363            ctx.typechecking_context = TypecheckingContext::Unknown;
364            parse_binary_expression(ctx, &inner)
365        }
366        "equality_comparison" => {
367            // Equality works on any type
368            // TODO: add type checking to ensure both sides have the same type
369            ctx.typechecking_context = TypecheckingContext::Unknown;
370            parse_binary_expression(ctx, &inner)
371        }
372        "set_comparison" => {
373            // Set comparisons require set operands (except 'in', which is hadled later)
374            ctx.typechecking_context = TypecheckingContext::Set;
375            parse_binary_expression(ctx, &inner)
376        }
377        "all_diff_comparison" => {
378            // TODO: check that operand is a collection with compatible element type.
379            ctx.typechecking_context = TypecheckingContext::Unknown;
380            parse_all_diff_comparison(ctx, &inner)
381        }
382        _ => {
383            ctx.record_error(RecoverableParseError::new(
384                format!("Expected comparison expression, found '{}'", inner.kind()),
385                Some(inner.range()),
386            ));
387            Ok(None)
388        }
389    }
390}
391
392fn parse_boolean_expression(
393    ctx: &mut ParseContext,
394    node: &Node,
395) -> Result<Option<Expression>, FatalParseError> {
396    ctx.typechecking_context = TypecheckingContext::Boolean;
397    let Some(inner) = named_child!(recover, ctx, node) else {
398        return Ok(None);
399    };
400    match inner.kind() {
401        "atom" => parse_atom(ctx, &inner),
402        "not_expr" | "sub_bool_expr" => parse_unary_expression(ctx, &inner),
403        "and_expr" | "or_expr" | "implication" | "iff_expr" => parse_binary_expression(ctx, &inner),
404        "list_combining_expr_bool" => parse_list_combining_expression(ctx, &inner),
405        "quantifier_expr" => parse_quantifier_or_aggregate_expr(ctx, &inner),
406        _ => {
407            ctx.record_error(RecoverableParseError::new(
408                format!("Expected boolean expression, found '{}'", inner.kind()),
409                Some(inner.range()),
410            ));
411            Ok(None)
412        }
413    }
414}
415
416fn parse_list_combining_expression(
417    ctx: &mut ParseContext,
418    node: &Node,
419) -> Result<Option<Expression>, FatalParseError> {
420    let Some(operator_node) = field!(recover, ctx, node, "operator") else {
421        return Ok(None);
422    };
423    let operator_str = &ctx.source_code[operator_node.start_byte()..operator_node.end_byte()];
424
425    let Some(arg_node) = field!(recover, ctx, node, "arg") else {
426        return Ok(None);
427    };
428    let Some(inner) = parse_atom(ctx, &arg_node)? else {
429        return Ok(None);
430    };
431
432    let expr = match operator_str {
433        "and" => Ok(Some(Expression::And(Metadata::new(), Moo::new(inner)))),
434        "or" => Ok(Some(Expression::Or(Metadata::new(), Moo::new(inner)))),
435        "sum" => Ok(Some(Expression::Sum(Metadata::new(), Moo::new(inner)))),
436        "product" => Ok(Some(Expression::Product(Metadata::new(), Moo::new(inner)))),
437        "min" => Ok(Some(Expression::Min(Metadata::new(), Moo::new(inner)))),
438        "max" => Ok(Some(Expression::Max(Metadata::new(), Moo::new(inner)))),
439        _ => {
440            ctx.record_error(RecoverableParseError::new(
441                format!("Invalid operator: '{operator_str}'"),
442                Some(operator_node.range()),
443            ));
444            Ok(None)
445        }
446    };
447
448    if expr.is_ok() {
449        ctx.add_span_and_doc_hover(
450            &operator_node,
451            operator_str,
452            SymbolKind::Function,
453            None,
454            None,
455        );
456    }
457
458    expr
459}
460
461fn parse_all_diff_comparison(
462    ctx: &mut ParseContext,
463    node: &Node,
464) -> Result<Option<Expression>, FatalParseError> {
465    let Some(arg_node) = field!(recover, ctx, node, "arg") else {
466        return Ok(None);
467    };
468    let Some(inner) = parse_expression(ctx, arg_node)? else {
469        return Ok(None);
470    };
471
472    let all_diff_keyword_node = child!(node, 0, "allDiff");
473    ctx.add_span_and_doc_hover(
474        &all_diff_keyword_node,
475        "allDiff",
476        SymbolKind::Function,
477        None,
478        None,
479    );
480    Ok(Some(Expression::AllDiff(Metadata::new(), Moo::new(inner))))
481}
482
483fn parse_unary_expression(
484    ctx: &mut ParseContext,
485    node: &Node,
486) -> Result<Option<Expression>, FatalParseError> {
487    let Some(expr_node) = field!(recover, ctx, node, "expression") else {
488        return Ok(None);
489    };
490    let Some(inner) = parse_expression(ctx, expr_node)? else {
491        return Ok(None);
492    };
493
494    match node.kind() {
495        "negative_expr" => Ok(Some(Expression::Neg(Metadata::new(), Moo::new(inner)))),
496        "abs_value" => Ok(Some(Expression::Abs(Metadata::new(), Moo::new(inner)))),
497        "not_expr" => Ok(Some(Expression::Not(Metadata::new(), Moo::new(inner)))),
498        "toInt_expr" => {
499            let to_int_keyword_node = child!(node, 0, "toInt");
500            ctx.add_span_and_doc_hover(
501                &to_int_keyword_node,
502                "toInt",
503                SymbolKind::Function,
504                None,
505                None,
506            );
507            Ok(Some(Expression::ToInt(Metadata::new(), Moo::new(inner))))
508        }
509        "factorial_expr" => {
510            // looking for the operator node (either '!' at the end or 'factorial' at the start) to add hover info
511            if let Some(op_node) = (0..node.child_count())
512                .filter_map(|i| node.child(i.try_into().unwrap()))
513                .find(|c| matches!(c.kind(), "!" | "factorial"))
514            {
515                ctx.add_span_and_doc_hover(
516                    &op_node,
517                    "post_factorial",
518                    SymbolKind::Function,
519                    None,
520                    None,
521                );
522            }
523
524            Ok(Some(Expression::Factorial(
525                Metadata::new(),
526                Moo::new(inner),
527            )))
528        }
529        "sub_bool_expr" | "sub_arith_expr" => Ok(Some(inner)),
530        _ => {
531            ctx.record_error(RecoverableParseError::new(
532                format!("Unrecognised unary operation: '{}'", node.kind()),
533                Some(node.range()),
534            ));
535            Ok(None)
536        }
537    }
538}
539
540pub fn parse_binary_expression(
541    ctx: &mut ParseContext,
542    node: &Node,
543) -> Result<Option<Expression>, FatalParseError> {
544    let Some(op_node) = field!(recover, ctx, node, "operator") else {
545        return Ok(None);
546    };
547    let op_str = &ctx.source_code[op_node.start_byte()..op_node.end_byte()];
548
549    let saved_ctx = ctx.typechecking_context;
550
551    // Special handling for 'in' operator, as the left operand doesn't have to be a set
552    if op_str == "in" {
553        ctx.typechecking_context = TypecheckingContext::Unknown
554    }
555
556    // parse left operand
557    let Some(left_node) = field!(recover, ctx, node, "left") else {
558        return Ok(None);
559    };
560    let Some(left) = parse_expression(ctx, left_node)? else {
561        return Ok(None);
562    };
563
564    // reset context, if needed
565    ctx.typechecking_context = saved_ctx;
566
567    // parse right operand
568    let Some(right_node) = field!(recover, ctx, node, "right") else {
569        return Ok(None);
570    };
571    let Some(right) = parse_expression(ctx, right_node)? else {
572        return Ok(None);
573    };
574
575    let Some(op_node) = field!(recover, ctx, node, "operator") else {
576        return Ok(None);
577    };
578    let op_str = &ctx.source_code[op_node.start_byte()..op_node.end_byte()];
579
580    let mut doc_name = "";
581    let expr = match op_str {
582        // NB: We are deliberately setting the index domain to 1.., not 1..2.
583        // Semantically, this means "a list that can grow/shrink arbitrarily".
584        // This is expected by rules which will modify the terms of the sum expression
585        // (e.g. by partially evaluating them).
586        "+" => {
587            doc_name = "L_Plus";
588            Ok(Some(Expression::Sum(
589                Metadata::new(),
590                Moo::new(matrix_expr![left, right; domain_int!(1..)]),
591            )))
592        }
593        "-" => {
594            doc_name = "L_Minus";
595            Ok(Some(Expression::Minus(
596                Metadata::new(),
597                Moo::new(left),
598                Moo::new(right),
599            )))
600        }
601        "*" => {
602            doc_name = "L_Times";
603            Ok(Some(Expression::Product(
604                Metadata::new(),
605                Moo::new(matrix_expr![left, right; domain_int!(1..)]),
606            )))
607        }
608        "/\\" => {
609            doc_name = "and";
610            Ok(Some(Expression::And(
611                Metadata::new(),
612                Moo::new(matrix_expr![left, right; domain_int!(1..)]),
613            )))
614        }
615        "\\/" => {
616            // No documentation for or in Bits yet
617            doc_name = "or";
618            Ok(Some(Expression::Or(
619                Metadata::new(),
620                Moo::new(matrix_expr![left, right; domain_int!(1..)]),
621            )))
622        }
623        "**" => {
624            doc_name = "L_Pow";
625            Ok(Some(Expression::UnsafePow(
626                Metadata::new(),
627                Moo::new(left),
628                Moo::new(right),
629            )))
630        }
631        "/" => {
632            //TODO: add checks for if division is safe or not
633            doc_name = "L_Div";
634            Ok(Some(Expression::UnsafeDiv(
635                Metadata::new(),
636                Moo::new(left),
637                Moo::new(right),
638            )))
639        }
640        "%" => {
641            //TODO: add checks for if mod is safe or not
642            doc_name = "L_Mod";
643            Ok(Some(Expression::UnsafeMod(
644                Metadata::new(),
645                Moo::new(left),
646                Moo::new(right),
647            )))
648        }
649
650        "=" => {
651            doc_name = "L_Eq"; //no docs yet
652            Ok(Some(Expression::Eq(
653                Metadata::new(),
654                Moo::new(left),
655                Moo::new(right),
656            )))
657        }
658        "!=" => {
659            doc_name = "L_Neq"; //no docs yet
660            Ok(Some(Expression::Neq(
661                Metadata::new(),
662                Moo::new(left),
663                Moo::new(right),
664            )))
665        }
666        "<=" => {
667            doc_name = "L_Leq"; //no docs yet
668            Ok(Some(Expression::Leq(
669                Metadata::new(),
670                Moo::new(left),
671                Moo::new(right),
672            )))
673        }
674        ">=" => {
675            doc_name = "L_Geq"; //no docs yet
676            Ok(Some(Expression::Geq(
677                Metadata::new(),
678                Moo::new(left),
679                Moo::new(right),
680            )))
681        }
682        "<" => {
683            doc_name = "L_Lt"; //no docs yet
684            Ok(Some(Expression::Lt(
685                Metadata::new(),
686                Moo::new(left),
687                Moo::new(right),
688            )))
689        }
690        ">" => {
691            doc_name = "L_Gt"; //no docs yet
692            Ok(Some(Expression::Gt(
693                Metadata::new(),
694                Moo::new(left),
695                Moo::new(right),
696            )))
697        }
698
699        "->" => {
700            doc_name = "L_Imply"; //no docs yet
701            Ok(Some(Expression::Imply(
702                Metadata::new(),
703                Moo::new(left),
704                Moo::new(right),
705            )))
706        }
707        "<->" => {
708            doc_name = "L_Iff"; //no docs yet
709            Ok(Some(Expression::Iff(
710                Metadata::new(),
711                Moo::new(left),
712                Moo::new(right),
713            )))
714        }
715        "<lex" => {
716            doc_name = "L_LexLt"; //no docs yet
717            Ok(Some(Expression::LexLt(
718                Metadata::new(),
719                Moo::new(left),
720                Moo::new(right),
721            )))
722        }
723        ">lex" => {
724            doc_name = "L_LexGt"; //no docs yet
725            Ok(Some(Expression::LexGt(
726                Metadata::new(),
727                Moo::new(left),
728                Moo::new(right),
729            )))
730        }
731        "<=lex" => {
732            doc_name = "L_LexLeq"; //no docs yet
733            Ok(Some(Expression::LexLeq(
734                Metadata::new(),
735                Moo::new(left),
736                Moo::new(right),
737            )))
738        }
739        ">=lex" => {
740            doc_name = "L_LexGeq"; //no docs yet
741            Ok(Some(Expression::LexGeq(
742                Metadata::new(),
743                Moo::new(left),
744                Moo::new(right),
745            )))
746        }
747        "in" => {
748            doc_name = "L_in";
749            Ok(Some(Expression::In(
750                Metadata::new(),
751                Moo::new(left),
752                Moo::new(right),
753            )))
754        }
755        "subset" => {
756            doc_name = "L_subset";
757            Ok(Some(Expression::Subset(
758                Metadata::new(),
759                Moo::new(left),
760                Moo::new(right),
761            )))
762        }
763        "subsetEq" => {
764            doc_name = "L_subsetEq";
765            Ok(Some(Expression::SubsetEq(
766                Metadata::new(),
767                Moo::new(left),
768                Moo::new(right),
769            )))
770        }
771        "supset" => {
772            doc_name = "L_supset";
773            Ok(Some(Expression::Supset(
774                Metadata::new(),
775                Moo::new(left),
776                Moo::new(right),
777            )))
778        }
779        "supsetEq" => {
780            doc_name = "L_supsetEq";
781            Ok(Some(Expression::SupsetEq(
782                Metadata::new(),
783                Moo::new(left),
784                Moo::new(right),
785            )))
786        }
787        "union" => {
788            doc_name = "L_union";
789            Ok(Some(Expression::Union(
790                Metadata::new(),
791                Moo::new(left),
792                Moo::new(right),
793            )))
794        }
795        "intersect" => {
796            doc_name = "L_intersect";
797            Ok(Some(Expression::Intersect(
798                Metadata::new(),
799                Moo::new(left),
800                Moo::new(right),
801            )))
802        }
803        _ => {
804            ctx.record_error(RecoverableParseError::new(
805                format!("Invalid operator: '{op_str}'"),
806                Some(op_node.range()),
807            ));
808            Ok(None)
809        }
810    };
811
812    if expr.is_ok() {
813        ctx.add_span_and_doc_hover(&op_node, doc_name, SymbolKind::Function, None, None);
814    }
815
816    expr
817}