// LALRPOP grammar for the Dolfin ontology language
//
// This grammar handles the indentation-based syntax of Dolfin using
// INDENT/DEDENT/NEWLINE tokens provided by the custom lexer.

use crate::ast::*;
use crate::lexer::Token;
use crate::error::{LexerError, Location, Span, SpannedString};

grammar;

// ============================================================================
// EXTERN DEFINITIONS
// ============================================================================

extern {
    type Location = Location;
    type Error = LexerError;

    enum Token {
        // Keywords
        "package" => Token::Package,
        "prefix" => Token::Prefix,
        "@iri_name" => Token::IriName,
        "as" => Token::As,
        "concept" => Token::Concept,
        "property" => Token::Property,
        "one of" => Token::OneOf,
        "rule" => Token::Rule,
        "match" => Token::Match,
        "then" => Token::Then,
        "sub" => Token::Sub,
        "has" => Token::Has,
        "key" => Token::Key,
        "a" => Token::Is,
        "fact" => Token::Fact,
        "is" => Token::IsInverse,

        // Quantifier keywords
        "all" => Token::All,
        "none" => Token::None,
        "at_least" => Token::AtLeast,
        "at_most" => Token::AtMost,
        "exactly" => Token::Exactly,
        "between" => Token::Between,
        "of" => Token::Of,

        // Cardinality keywords
        "one" => Token::One,
        "any" => Token::Any,
        "some" => Token::Some,
        "optional" => Token::Optional,
        
        // Primitive types
        "string" => Token::TString,
        "int" => Token::TInt,
        "float" => Token::TFloat,
        "boolean" => Token::TBoolean,
        
        // Literals
        INT => Token::Int(<i64>),
        FLOAT => Token::Float(<f64>),
        STRING => Token::String(<String>),
        BOOLEAN => Token::Boolean(<bool>),
        IRI => Token::Iri(<String>),
        
        // Identifiers
        NAME => Token::Name(<String>),
        VARIABLE => Token::Variable(<String>),
        
        // Punctuation
        ":" => Token::Colon,
        "," => Token::Comma,
        "." => Token::Dot,
        "->" => Token::Arrow,
        ".." => Token::DoubleDot,
        "*" => Token::Star,
        "|" => Token::Pipe,
        "^^" => Token::DoubleCaret,

        // Brackets
        "[" => Token::LBracket,
        "]" => Token::RBracket,

        // Arithmetic operators
        "+" => Token::Plus,
        "-" => Token::Minus,
        "/" => Token::Slash,
        "(" => Token::LParen,
        ")" => Token::RParen,

        // Comparison operators
        "=" => Token::Equal,
        "!=" => Token::NotEqual,
        "<" => Token::LessThan,
        "<=" => Token::LessEqual,
        ">" => Token::GreaterThan,
        ">=" => Token::GreaterEqual,
        
        // Indentation tokens
        INDENT => Token::Indent,
        DEDENT => Token::Dedent,
        NEWLINE => Token::Newline,
        EOF => Token::Eof,
    }
}

// ============================================================================
// MACROS
// ============================================================================

// Comma-separated list with at least one element
Comma<T>: Vec<T> = {
    <first:T> <rest:("," <T>)*> => {
        let mut v = vec![first];
        v.extend(rest);
        v
    }
};

// Optional element
Optional<T>: Option<T> = {
    <T> => Some(<>),
    => None,
};

// ============================================================================
// TOP-LEVEL
// ============================================================================

/// An ontology file (the main entry point for .dlf files except package.dlf)
pub OntologyFile: OntologyFile = {
    NEWLINE*
    <start:@L>
    <iri_name:IriNameAnnotation?>
    <prefixes:PrefixStmt*>
    // NEWLINE*
    <declarations:Declaration*>
    EOF <end:@R> => OntologyFile::new(
        iri_name, 
        prefixes.into_iter().flatten().collect(), 
        declarations,
        Some(Span { start, end }),
    ),
};


// ============================================================================
// PACKAGE MANIFEST (package.dlf)
// ============================================================================

/// Package definition for package.dlf files
pub PackageFile: PackageFile = {
    <start:@L> "package" <name:QualifiedNameOrIRI> ":" NEWLINE
    INDENT <fields:PackageField+> DEDENT EOF <end:@R>
    => {
        let mut manifest = PackageFile {
            name,
            dolfin_version: String::new(),
            version: String::new(),
            author: None,
            description: None,
            span: Some(Span { start, end }),
        };
        for field in fields {
            match field {
                PackageField::DolfinVersion(v) => manifest.dolfin_version = v,
                PackageField::Version(v) => manifest.version = v,
                PackageField::Author(v) => manifest.author = Some(v),
                PackageField::Description(v) => manifest.description = Some(v),
            }
        }
        manifest
    }
};

PackageField: PackageField = {
    <l:@L> <f:NAME> <r:@R> <v:STRING> NEWLINE =>? {
        match f.as_str() {
            "dolfin_version" => Ok(PackageField::DolfinVersion(v)),
            "version" => Ok(PackageField::Version(v)),
            "author" => Ok(PackageField::Author(v)),
            "description" => Ok(PackageField::Description(v)),
            _ => Err(lalrpop_util::ParseError::UnrecognizedToken {
                token: (l, Token::Name(f), r),
                expected: vec![
                    "dolfin_version".to_string(),
                    "version".to_string(),
                    "author".to_string(),
                    "description".to_string(),
                ],
            }),
        }
    }    
};

// ============================================================================
// PREFIX
// ============================================================================

PrefixStmt: Vec<PrefixDecl> = {
    "prefix" <target:PrefixTarget> => target
};

PrefixTarget: Vec<PrefixDecl> = {
    // Hierarchical prefix: prefix com.example:
    //   Person
    //   Organization as Org
    <prefix:QualifiedName> ":" NEWLINE INDENT <children:PrefixTarget+> DEDENT => {
        children.into_iter()
            .flatten()
            .map(|imp| PrefixDecl {
                path: prefix.join(&imp.path),
                alias: imp.alias,
                span: imp.span,
            })
            .collect()
    },
    // Aliased prefix: prefix com.example.Person as P
    <start:@L> <path:QualifiedNameOrIRI> "as" <alias:NAME> <end:@R> NEWLINE => {
        vec![PrefixDecl { path, alias, span: Some(Span { start, end }) }]
    },
    // Simple prefix: prefix com.example.Person
    <start:@L> <path:QualifiedNameOrIRI> <end:@R> NEWLINE => {
        let alias = path.last().to_string();
        vec![PrefixDecl { path, alias, span: Some(Span { start, end }) }]
    },
};

// ============================================================================
// ANNOTATIONS
// ============================================================================

/// IRI name annotation to override the derived IRI segment
IriNameAnnotation: String = {
    "@iri_name" <name:STRING> NEWLINE+ => name,
};

// ============================================================================
// ONTOLOGY
// ============================================================================

/// Top-level declarations in an ontology file
Declaration: Declaration = {
    <c:ConceptDef> NEWLINE? => Declaration::Concept(c),
    <p:PropertyDef> NEWLINE? => Declaration::Property(p),
    <r:RuleDef> NEWLINE? => Declaration::Rule(r),
    <f:FactDef> NEWLINE? => Declaration::Fact(f),
};


// ============================================================================
// CONCEPT
// ============================================================================

ConceptDef: ConceptDef = {
    <start:@L> "concept" <ns:@L> <name:NAME> <ne:@R> ":" NEWLINE
    INDENT
    <end:@R>
    <members:ConceptMember+>
    DEDENT  => {
        let mut parents = Vec::new();
        let mut has_declarations = Vec::new();
        let mut span = Span { start, end };

        let mut one_of: Option<Vec<OneOfVariant>> = None;

        for (member, mspan) in members {
            match member {
                ConceptMember::Sub(p) => {
                    span = span.merge(&mspan);
                    parents.extend(p)
                },
                ConceptMember::Has(h) => {
                    span = span.merge(&mspan);
                    has_declarations.push(h)
                },
                ConceptMember::OneOf(variants) => {
                    // Check if at most one section "one of" is present
                    span = span.merge(&mspan);
                    one_of = Some(variants);
                },
            }
        }

        ConceptDef::new(name, parents, has_declarations, one_of, Some(span), Some(Span { start: ns, end: ne }))
    },
    // Concept without body (just declaration)
    <start:@L> "concept" <ns:@L> <name:NAME> <ne:@R> <end:@R> NEWLINE => ConceptDef::new(name, vec![], vec![], None, Some(Span { start, end }), Some(Span { start: ns, end: ne })),
};

ConceptMember: (ConceptMember, Span) = {
    <s:SubDeclaration> => (ConceptMember::Sub(s.0), s.1),
    <h:HasDeclaration> => (ConceptMember::Has(h.clone()), h.span.unwrap()),
    <oo:OneOfBlock> => (ConceptMember::OneOf(oo.0), oo.1),
};

OneOfBlock: (Vec<OneOfVariant>, Span) = {
    <start:@L> "one of" ":" NEWLINE 
    INDENT <end:@R>
      <variants:OneOfItem+>
    DEDENT => {
      let mut span = Span { start, end };
      for v in &variants {
        if let Some(vspan) = v.span {
          span = span.merge(&vspan);
        }
      }
      (variants, span)
    }
};

OneOfItem: OneOfVariant = {
    <start:@L> <name:NAME> <pre_end:@R> <constraints:NoCmpConstraintBlock?> <end:@R> NEWLINE => {
        if constraints.is_some() {
          OneOfVariant { name, constraints, span: Some(Span { start, end }) }
        } else {
          OneOfVariant { name, constraints, span: Some(Span { start, end: pre_end }) }
        }
    }
};

SubDeclaration: (Vec<TypeRef>, Span) = {
    <start:@L> "sub" <types:Comma<TypeRef>> <end:@R> NEWLINE => (types, Span { start, end }),
};

HasDeclaration: HasDeclaration = {
    <start:@L> "has" <is_key:("key")?> <name:NAME> ":" <cardinality:Optional<Cardinality>> <type_ref:TypeRef> <end:@R> NEWLINE => {
        HasDeclaration { name, is_key: is_key.is_some(), cardinality, type_ref, span: Some(Span { start, end }) }
    }
};

// ============================================================================
// PROPERTY
// ============================================================================

PropertyDef: PropertyDef = {
    <start:@L> "property" <name_start:@L> <name:NAME> <name_end:@R> ":" 
        <domain_cardinality:Optional<Cardinality>> <domain:TypeRef> 
        "->" 
        <range_cardinality:Optional<Cardinality>> <range:TypeRef> 
    <end:@R> => {
        PropertyDef {
            name: SpannedString::new(name, Some(Span { start: name_start, end: name_end })),
            domain_cardinality,
            domain,
            range_cardinality,
            range,
            span: Some(Span { start, end }),
        }
    }
};

// ============================================================================
// RULE
// ============================================================================

RuleDef: RuleDef = {
    <start:@L> "rule" <name:NAME> ":" NEWLINE
    INDENT <match_block:MatchBlock> <then_block:ThenBlock>
    DEDENT <end:@R> => RuleDef { name, match_block, then_block, span: Some(Span { start, end }) }
};

MatchBlock: MatchBlock = {
    <start:@L> "match" ":" NEWLINE
    INDENT
    <patterns:MatchPattern+>
    DEDENT <end:@R> => MatchBlock { patterns, span: Some(Span { start, end }) }
};

ThenBlock: ThenBlock = {
    <start:@L> "then" ":" NEWLINE
    INDENT
    <items:ThenItem+>
    DEDENT <end:@R> => ThenBlock { items, span: Some(Span { start, end }) }
};

MatchPattern: Pattern = {
    <start:@L> <subject:Subject> <property:QualifiedName> <object:Object> <end:@R> NEWLINE =>
        Pattern::Triple { subject, property, object, span: Some(Span { start, end }) },
    <start:@L> <subject:Subject> <property:QualifiedName> <block:ConstraintBlock> <end:@R> NEWLINE =>
        Pattern::Triple { subject, property, object: Object::Constraint { block }, span: Some(Span { start, end }) },
    <start:@L> <subject:Subject> "a" <type_ref:TypeRef> <end:@R> NEWLINE =>
        Pattern::Type { subject, type_ref, span: Some(Span { start, end }) },
    <start:@L> <quantifier:Quantifier> <variable:VARIABLE> <constraint:Optional<ConstraintBlock>> ":" 
        NEWLINE INDENT <patterns:MatchPattern+> DEDENT <end:@R> => {
        Pattern::Quantified {
            quantifier,
            variable,
            constraint,
            patterns,
            span: Some(Span { start, end }),
        }
    },
};

Quantifier: Quantifier = {
    <start:@L> "all" <end:@R> => Quantifier::All { span: Some(Span { start, end })},
    <start:@L> "none" <end:@R> => Quantifier::None { span: Some(Span { start, end })},
    <start:@L> "at_least" <value:CardinalityValue> <end:@R> => Quantifier::AtLeast { value, span: Some(Span { start, end }) },
    <start:@L> "at_most" <value:CardinalityValue> <end:@R> => Quantifier::AtMost { value, span: Some(Span { start, end }) },
    <start:@L> "exactly" <value:CardinalityValue> <end:@R> => Quantifier::Exactly { value, span: Some(Span { start, end }) },
    <start:@L> "between" <min:CardinalityValue> "," <max:CardinalityValue> <end:@R> => Quantifier::Between { min, max, span: Some(Span { start, end }) },
};

ThenItem: ThenItem = {
    <Assertion> NEWLINE => <>,
    <NestedRule> => ThenItem::NestedRule { rule: <> },
};

Assertion: ThenItem = {
    <start:@L> <subject:Subject> <property:QualifiedName> <object:Object> <end:@R> =>
        ThenItem::AssertionTriple { 
            assertion: Assertion { 
                subject, 
                property, 
                object,
                span: Some(Span { start, end }),
            },
            span: Some(Span { start, end }),
        },
    <start:@L> <subject:Subject> <property:QualifiedName> <block:NoCmpConstraintBlock> <end:@R> =>
        ThenItem::AssertionTriple { 
            assertion: Assertion { 
                subject, 
                property, 
                object: Object::Constraint { block },
                span: Some(Span { start, end }),
            },
            span: Some(Span { start, end }),
        },
    <start:@L> <subject:Subject> "a" <l:@L> <type_ref:TypeRef> <r:@R> <end:@R> =>?
        match type_ref {
            TypeRef::Named { name: typing, .. } => Ok(ThenItem::AssertionTyping { 
                subject, 
                typing,
                span: Some(Span { start, end }),
            }),
            TypeRef::Primitive { kind, .. } => Err(lalrpop_util::ParseError::UnrecognizedToken {
                token: (l, Token::Name(kind.to_string()), r),
                expected: vec![
                    "Qualified name that is not a primitive".to_string(),
                ],
            }),
        },
};

NestedRule: RuleDef = {
    <start:@L> <match_block:MatchBlock> <then_block:ThenBlock> <end:@R> => {
        RuleDef {
            name: "anonymous".to_string(),
            match_block,
            then_block,
            span: Some(Span { start, end }),
        }
    }
};

// ============================================================================
// FACT (INSTANCE DECLARATIONS)
// ============================================================================

/// A top-level fact: `fact <id> a <Type>, ...`
FactDef: FactDef = {
    // fact with body
    <start:@L> "fact" <id:NAME> "a" <types:Comma<QualifiedName>> NEWLINE
    INDENT <assertions:FactAssertion+> DEDENT <end:@R>
    => FactDef { id, types, assertions, span: Some(Span { start, end }) },

    // fact without body (bare declaration)
    <start:@L> "fact" <id:NAME> "a" <types:Comma<QualifiedName>> <end:@R> NEWLINE
    => FactDef { id, types, assertions: vec![], span: Some(Span { start, end }) },
};

/// One assertion line within a fact body
FactAssertion: FactAssertion = {
    // property_name value, value, ...
    <start:@L> <name:NAME> <values:Comma<FactValue>> <end:@R> NEWLINE
    => FactAssertion::Property { name, values, span: Some(Span { start, end }) },

    // is <property> of <value>
    <start:@L> "is" <property:QualifiedName> "of" <value:FactValue> <end:@R> NEWLINE
    => FactAssertion::Inverse { property, value, span: Some(Span { start, end }) },

    // a <ConceptName>  (type hint inside anonymous block)
    <start:@L> "a" <type_ref:QualifiedName> <end:@R> NEWLINE
    => FactAssertion::TypeHint { type_ref, span: Some(Span { start, end }) },
};

/// A value that can appear on the right-hand side of a property assertion
FactValue: FactValue = {
    // Literal scalar
    <start:@L> <l:Literal> <end:@R>
    => FactValue::Literal { value: l, span: Some(Span { start, end }) },

    // :name  (local reference, empty prefix)
    <start:@L> ":" <name:NAME> <end:@R>
    => FactValue::Reference { qualifier: None, name, span: Some(Span { start, end }) },

    // prefix:name  (prefixed reference)
    <start:@L> <prefix:NAME> ":" <name:NAME> <end:@R>
    => FactValue::Reference { qualifier: Some(prefix), name, span: Some(Span { start, end }) },

    // Multi-line anonymous block: prop [ \n INDENT assertions DEDENT ]
    <start:@L> "[" NEWLINE INDENT <assertions:FactAssertion+> DEDENT "]" <end:@R>
    => FactValue::Block { type_hint: None, assertions, span: Some(Span { start, end }) },

    // Qualified name (enum value / concept reference)  — must come last to avoid
    // eating :name or prefix:name prefixes
    <start:@L> <name:QualifiedName> <end:@R>
    => FactValue::Named { name, span: Some(Span { start, end }) },
};

// ============================================================================
// SUBJECTS AND OBJECTS
// ============================================================================

Subject: Subject = {
    <start:@L> <v:VARIABLE> <end:@R> => Subject::Variable { name: v, span: Some(Span { start, end }) },
    <start:@L> <n:QualifiedName> <end:@R> => Subject::Constant { name: n, span: Some(Span { start, end }) },
    <start:@L> <b:ConstraintBlock> <end:@R> => Subject::Constraint { block: b, span: Some(Span { start, end }) },
};

Object: Object = {
    <start:@L> <v:VARIABLE> <end:@R> => Object::Variable { name: v, span: Some(Span { start, end }) },
    <start:@L> <v:Expr> <end:@R> => Object::Literal { value: v, span: Some(Span { start, end }) },
    <start:@L> <n:QualifiedName> <end:@R> => Object::Constant { value: n, span: Some(Span { start, end }) },
};

// ============================================================================
// CONSTRAINTS
// ============================================================================

NoCmpConstraintBlock: ConstraintBlock = {
    <start:@L> "[" <constraints:Comma<NoCmpConstraint>> "]" <end:@R> => ConstraintBlock { constraints, span: Some(Span { start, end }) }
};

NoCmpConstraint: Constraint = {
    <start:@L> "a" <type_ref:TypeRef> <end:@R> => Constraint::TypeIs { type_ref, span: Some(Span { start, end }) },
    <start:@L> <property:QualifiedName> <value:Object> <end:@R> => Constraint::PropertyValue { property, value, span: Some(Span { start, end }) },
    <start:@L> <property:QualifiedName> <block:NoCmpConstraintBlock> <end:@R> => {
        Constraint::PropertyConstraint { property, block, span: Some(Span { start, end }) }
    },
};

ConstraintBlock: ConstraintBlock = {
    <start:@L> "[" <constraints:Comma<Constraint>> "]" <end:@R> => ConstraintBlock { constraints, span: Some(Span { start, end }) }
};

Constraint: Constraint = {
    <start:@L> "a" <type_ref:TypeRef> <end:@R> => Constraint::TypeIs { type_ref, span: Some(Span { start, end }) },
    <start:@L> <op:ComparisonOp> <value:Expr> <end:@R> => Constraint::Comparison { operator: op, value, span: Some(Span { start, end }) },
    <start:@L> <property:QualifiedName> <value:Object> <end:@R> => Constraint::PropertyValue { property, value, span: Some(Span { start, end }) },
    <start:@L> <property:QualifiedName> <block:ConstraintBlock> <end:@R> => {
        Constraint::PropertyConstraint { property, block, span: Some(Span { start, end }) }
    },
};

ComparisonOp: ComparisonOp = {
    "=" => ComparisonOp::Equal,
    "!=" => ComparisonOp::NotEqual,
    "<" => ComparisonOp::LessThan,
    "<=" => ComparisonOp::LessEqual,
    ">" => ComparisonOp::GreaterThan,
    ">=" => ComparisonOp::GreaterEqual,
};

// ============================================================================
// TYPES
// ============================================================================

TypeRef: TypeRef = {
    <start:@L> <n:QualifiedName> <end:@R> => TypeRef::Named { name: n, span: Some(Span { start, end }) },
    <PrimitiveType> => <>,
};

PrimitiveType: TypeRef = {
    <start:@L> "string" <end:@R> => TypeRef::Primitive { kind: PrimitiveKind::String, span: Some(Span { start, end }) },
    <start:@L> "int" <end:@R> => TypeRef::Primitive { kind: PrimitiveKind::Int, span: Some(Span { start, end }) },
    <start:@L> "float" <end:@R> => TypeRef::Primitive { kind: PrimitiveKind::Float, span: Some(Span { start, end }) },
    <start:@L> "boolean" <end:@R> => TypeRef::Primitive { kind: PrimitiveKind::Boolean, span: Some(Span { start, end }) },
};

QualifiedName: QualifiedName = {
    <start:@L> <first:NAME> <rest:("." <NAME>)*> <end:@R> => {
        let mut parts = vec![first];
        parts.extend(rest);
        QualifiedName::new(parts, Some(Span { start, end }))
    }
};

QualifiedNameOrIRI: QualifiedName = {
    <QualifiedName> => <>,
    <start:@L> <iri:IRI> <end:@R> => QualifiedName { 
        parts: vec![iri], 
        span: Some(Span { start, end }),
    }
};


// ============================================================================
// CARDINALITY
// ============================================================================

Cardinality: Cardinality = {
    <start:@L> "one" <end:@R> => Cardinality::One { span: Some(Span { start, end })},
    <start:@L> "any" <end:@R> => Cardinality::Any { span: Some(Span { start, end })},
    <start:@L> "some" <end:@R> => Cardinality::Some { span: Some(Span { start, end })},
    <start:@L> "optional" <end:@R> => Cardinality::Optional { span: Some(Span { start, end })},
    <start:@L> <n:INT> <end:@R> => Cardinality::Exact { value: n as usize, span: Some(Span { start, end }) },
    <start:@L> <min:INT> ".." <max:INT> <end:@R> => Cardinality::Range { 
        min: min as usize, 
        max: Some(max as usize),
        span: Some(Span { start, end }),
    },
    <start:@L> <min:INT> ".." "*" <end:@R> => Cardinality::Range { 
        min: min as usize, 
        max: None,
        span: Some(Span { start, end }),
    },
};

CardinalityValue: CardinalityValue = {
    <start:@L> <n:INT> <end:@R> => CardinalityValue::Int { value: n as usize, span: Some(Span { start, end }) },
    <start:@L> <v:VARIABLE> <end:@R> => CardinalityValue::Variable { name: v, span: Some(Span { start, end }) },
};

// ============================================================================
// LITERALS
// ============================================================================

Literal: Literal = {
    <start:@L> <n:INT> <end:@R> => Literal::Int { value: n, span: Some(Span { start, end })},
    <start:@L> <n:FLOAT> <end:@R> => Literal::Float { value: n, span: Some(Span { start, end })},
    <start:@L> <s:STRING> <end:@R> => Literal::String { value: s, span: Some(Span { start, end })},
    <start:@L> <b:BOOLEAN> <end:@R> => Literal::Boolean { value: b, span: Some(Span { start, end })},
    <start:@L> <i:IRI> <end:@R> => Literal::Iri { value: i, span: Some(Span { start, end })},
};

// ============================================================================
// EXPRESSIONS
// ============================================================================

Expr: Expr = {
    <AddExpr> => <>,
};

AddExpr: Expr = {
    <start:@L> <left:AddExpr> "+" <right:MulExpr> <end:@R> => Expr::BinaryOp {
        op: BinaryOp::Add,
        left: Box::new(left),
        right: Box::new(right),
        span: Some(Span { start, end }),
    },
    <start:@L> <left:AddExpr> "-" <right:MulExpr> <end:@R> => Expr::BinaryOp {
        op: BinaryOp::Sub,
        left: Box::new(left),
        right: Box::new(right),
        span: Some(Span { start, end }),
    },
    <MulExpr> => <>,
};

MulExpr: Expr = {
    <start:@L> <left:MulExpr> "*" <right:UnaryExpr> <end:@R> => Expr::BinaryOp {
        op: BinaryOp::Mul,
        left: Box::new(left),
        right: Box::new(right),
        span: Some(Span { start, end }),
    },
    <start:@L> <left:MulExpr> "/" <right:UnaryExpr> <end:@R> => Expr::BinaryOp {
        op: BinaryOp::Div,
        left: Box::new(left),
        right: Box::new(right),
        span: Some(Span { start, end }),
    },
    <UnaryExpr> => <>,
};

UnaryExpr: Expr = {
    <start:@L> "-" <operand:UnaryExpr> <end:@R> => Expr::UnaryOp {
        op: UnaryOp::Neg,
        operand: Box::new(operand),
        span: Some(Span { start, end }),
    },
    <PrimaryExpr> => <>,
};

PrimaryExpr: Expr = {
    <start:@L> "(" <e:Expr> ")" <end:@R> => e,
    <start:@L> <n:INT> <end:@R> => Expr::Literal {
        value: Literal::Int { value: n, span: Some(Span { start, end }) },
        span: Some(Span { start, end }),
    },
    <start:@L> <n:FLOAT> <end:@R> => Expr::Literal {
        value: Literal::Float { value: n, span: Some(Span { start, end }) },
        span: Some(Span { start, end }),
    },
    <start:@L> <s:STRING> <end:@R> => Expr::Literal {
        value: Literal::String { value: s, span: Some(Span { start, end }) },
        span: Some(Span { start, end }),
    },
    <start:@L> <b:BOOLEAN> <end:@R> => Expr::Literal {
        value: Literal::Boolean { value: b, span: Some(Span { start, end }) },
        span: Some(Span { start, end }),
    },
    <start:@L> <i:IRI> <end:@R> => Expr::Literal {
        value: Literal::Iri { value: i, span: Some(Span { start, end }) },
        span: Some(Span { start, end }),
    },
};
