Skip to main content

xlog_logic/
parser.rs

1//! Parser for XLOG programs using Pest
2//!
3//! This module parses XLOG source text into AST structures.
4//! It uses the Pest parser generator with a grammar defined in `grammar.pest`.
5#![allow(missing_docs)] // `pest_derive` emits a public `Rule` enum and helpers without doc hooks.
6
7use pest::iterators::Pair;
8use pest::Parser;
9use pest_derive::Parser;
10use xlog_core::{symbol, Result, ScalarType, XlogError};
11
12use crate::ast::{
13    AggExpr, AggOp, AnnotatedDisjunction, ArithExpr, Atom, BodyLiteral, CompOp, Comparison,
14    CondExpr, Constraint, DomainDecl, EpistemicLiteral, EpistemicMode, EpistemicOp, Evidence,
15    FuncBody, FuncDef, FuncParam, IsExpr, LearnableRule, MagicSetsMode, NeuralLabel,
16    NeuralPredDecl, PredColumn, PredDecl, ProbCache, ProbEngine, ProbFact, ProbMethod, ProbQuery,
17    Program, Query, Rule as AstRule, Term, TypeRef, Univ, UseDecl,
18};
19
20/// Pest-based parser for XLOG Datalog syntax.
21#[derive(Parser)]
22#[grammar = "grammar.pest"]
23pub struct XlogParser;
24
25/// Parse result containing the parsed pairs (for low-level access)
26pub type ParseResult<'a> = pest::iterators::Pairs<'a, Rule>;
27
28/// Parse an XLOG program string into an AST Program.
29///
30/// Simple ground facts (`p(1, "a", sym, X, _).`) take a hand-written fast
31/// path (see [`crate::fact_fast_path`]); everything else goes through pest.
32/// The result is identical to [`parse_program_reference`], including errors.
33pub fn parse_program(input: &str) -> Result<Program> {
34    parse_program_with_stats(input).map(|(program, _)| program)
35}
36
37/// Pure-pest reference parser: the whole source through the grammar, no fast
38/// path. `parse_program` is specified to be observationally equal to this.
39pub fn parse_program_reference(input: &str) -> Result<Program> {
40    let pairs =
41        XlogParser::parse(Rule::program, input).map_err(|e| XlogError::Parse(e.to_string()))?;
42
43    build_program(pairs)
44}
45
46/// How much of a parse the fact fast path handled.
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
48pub struct FastParseStats {
49    /// Facts built without pest.
50    pub fast_facts: usize,
51    /// Bytes of source pest still had to parse (everything but the facts).
52    pub residual_bytes: usize,
53    /// The residual failed to parse/build, so the whole source was re-parsed by
54    /// the reference parser (its result is what was returned).
55    pub fell_back: bool,
56}
57
58/// [`parse_program`] plus fast-path statistics (for probes and tests).
59pub fn parse_program_with_stats(input: &str) -> Result<(Program, FastParseStats)> {
60    let mut scan = crate::fact_fast_path::scan(input);
61    if scan.facts.is_empty() {
62        let stats = FastParseStats {
63            fast_facts: 0,
64            residual_bytes: input.len(),
65            fell_back: false,
66        };
67        return parse_program_reference(input).map(|program| (program, stats));
68    }
69    let mut stats = FastParseStats {
70        fast_facts: scan.facts.len(),
71        residual_bytes: input.len() - scan.fact_bytes,
72        fell_back: false,
73    };
74    let facts = std::mem::take(&mut scan.facts);
75    let merged = XlogParser::parse(Rule::program, &scan.residual)
76        .map_err(|e| XlogError::Parse(e.to_string()))
77        .and_then(|pairs| build_program_merged(pairs, facts));
78    match merged {
79        Ok(program) => Ok((program, stats)),
80        Err(_) => {
81            // Any failure on the residual: report exactly what the reference
82            // parser reports for the original source (positions included).
83            stats.fell_back = true;
84            parse_program_reference(input).map(|program| (program, stats))
85        }
86    }
87}
88
89/// Parse a single statement (low-level, returns pest pairs)
90pub fn parse_statement(input: &str) -> Result<ParseResult<'_>> {
91    XlogParser::parse(Rule::statement, input).map_err(|e| XlogError::Parse(e.to_string()))
92}
93
94/// Parse a single atom (low-level, returns pest pairs)
95pub fn parse_atom(input: &str) -> Result<ParseResult<'_>> {
96    XlogParser::parse(Rule::atom, input).map_err(|e| XlogError::Parse(e.to_string()))
97}
98
99// =============================================================================
100// AST Building Functions
101// =============================================================================
102
103/// Build a Program AST from the pest pairs of the residual source plus the
104/// fast-path facts, interleaved in original source order (statement order is
105/// semantically relevant: it fixes rule order and therefore the plan). The
106/// residual has the facts blanked out, so pest's span offsets are original
107/// offsets. Facts intern their symbols here, in merge order, so interning
108/// order is source order exactly as in the reference parser.
109fn build_program_merged(
110    pairs: ParseResult<'_>,
111    facts: Vec<crate::fact_fast_path::FastFact<'_>>,
112) -> Result<Program> {
113    let mut program = Program::new();
114    let mut facts = facts.into_iter().peekable();
115    for pair in pairs {
116        if pair.as_rule() != Rule::program {
117            continue;
118        }
119        for inner in pair.into_inner() {
120            if inner.as_rule() != Rule::statement {
121                continue;
122            }
123            let stmt_offset = inner.as_span().start();
124            while let Some(fact) = facts.next_if(|fact| fact.offset < stmt_offset) {
125                program.rules.push(fact.into_rule());
126            }
127            build_statement(inner, &mut program)?;
128        }
129    }
130    for fact in facts {
131        program.rules.push(fact.into_rule());
132    }
133    Ok(program)
134}
135
136/// Build a Program AST from parsed pest pairs
137fn build_program(pairs: ParseResult<'_>) -> Result<Program> {
138    let mut program = Program::new();
139
140    for pair in pairs {
141        if pair.as_rule() == Rule::program {
142            for inner in pair.into_inner() {
143                match inner.as_rule() {
144                    Rule::statement => {
145                        build_statement(inner, &mut program)?;
146                    }
147                    Rule::EOI => {}
148                    _ => {}
149                }
150            }
151        }
152    }
153
154    Ok(program)
155}
156
157/// Build a statement and add it to the program
158fn build_statement(pair: Pair<'_, Rule>, program: &mut Program) -> Result<()> {
159    for inner in pair.into_inner() {
160        match inner.as_rule() {
161            Rule::use_stmt => {
162                program.imports.push(build_use_stmt(inner));
163            }
164            Rule::domain_decl => {
165                program.domains.push(build_domain_decl(inner)?);
166            }
167            Rule::pred_decl => {
168                program.predicates.push(build_pred_decl(inner)?);
169            }
170            Rule::pragma => {
171                apply_pragma(inner, program)?;
172            }
173            Rule::rule_def => {
174                program.rules.push(build_rule(inner)?);
175            }
176            Rule::fact => {
177                program.rules.push(build_fact(inner)?);
178            }
179            Rule::prob_fact => {
180                program.prob_facts.push(build_prob_fact(inner)?);
181            }
182            Rule::annotated_disjunction => {
183                program
184                    .annotated_disjunctions
185                    .push(build_annotated_disjunction(inner)?);
186            }
187            Rule::evidence_stmt => {
188                program.evidence.push(build_evidence(inner)?);
189            }
190            Rule::prob_query => {
191                program.prob_queries.push(build_prob_query(inner)?);
192            }
193            Rule::constraint => {
194                program.constraints.push(build_constraint(inner)?);
195            }
196            Rule::query => {
197                program.queries.push(build_query(inner)?);
198            }
199            Rule::func_def => {
200                program.functions.push(parse_func_def(inner)?);
201            }
202            Rule::neural_pred_decl => {
203                program
204                    .neural_predicates
205                    .push(build_neural_pred_decl(inner)?);
206            }
207            Rule::learnable_rule => {
208                program.learnable_rules.push(build_learnable_rule(inner)?);
209            }
210            _ => {}
211        }
212    }
213    Ok(())
214}
215
216fn apply_pragma(pair: Pair<'_, Rule>, program: &mut Program) -> Result<()> {
217    let pragma = pair
218        .into_inner()
219        .next()
220        .ok_or_else(|| XlogError::Parse("Empty pragma".to_string()))?;
221
222    match pragma.as_rule() {
223        Rule::pragma_prob_engine => {
224            let value = pragma
225                .into_inner()
226                .next()
227                .ok_or_else(|| XlogError::Parse("Missing prob_engine value".to_string()))?;
228            let engine = match value.as_str() {
229                "exact_ddnnf" => ProbEngine::ExactDdnnf,
230                "mc" => ProbEngine::Mc,
231                other => {
232                    return Err(XlogError::Parse(format!(
233                        "Unknown prob_engine value: {}",
234                        other
235                    )))
236                }
237            };
238            program.directives.prob_engine = Some(engine);
239        }
240        Rule::pragma_prob_cache => {
241            let value = pragma
242                .into_inner()
243                .next()
244                .ok_or_else(|| XlogError::Parse("Missing prob_cache value".to_string()))?;
245            let cache = match value.as_str() {
246                "on" => ProbCache::On,
247                "off" => ProbCache::Off,
248                other => {
249                    return Err(XlogError::Parse(format!(
250                        "Unknown prob_cache value: {}",
251                        other
252                    )))
253                }
254            };
255            program.directives.prob_cache = Some(cache);
256        }
257        Rule::pragma_epistemic_mode => {
258            let value = pragma
259                .into_inner()
260                .next()
261                .ok_or_else(|| XlogError::Parse("Missing epistemic_mode value".to_string()))?;
262            let mode = match value.as_str() {
263                "g91" => EpistemicMode::G91,
264                "faeel" => EpistemicMode::Faeel,
265                other => {
266                    return Err(XlogError::Parse(format!(
267                        "Unknown epistemic_mode value: {}",
268                        other
269                    )))
270                }
271            };
272            program.directives.epistemic_mode = Some(mode);
273        }
274        Rule::pragma_prob_samples => {
275            let value = pragma
276                .into_inner()
277                .next()
278                .ok_or_else(|| XlogError::Parse("Missing prob_samples value".to_string()))?;
279            let samples: usize = value.as_str().parse().map_err(|_| {
280                XlogError::Parse(format!("Invalid prob_samples value: {}", value.as_str()))
281            })?;
282            if samples == 0 {
283                return Err(XlogError::Parse(
284                    "Invalid prob_samples value: expected > 0".to_string(),
285                ));
286            }
287            program.directives.prob_samples = Some(samples);
288        }
289        Rule::pragma_prob_seed => {
290            let value = pragma
291                .into_inner()
292                .next()
293                .ok_or_else(|| XlogError::Parse("Missing prob_seed value".to_string()))?;
294            let seed: u64 = value.as_str().parse().map_err(|_| {
295                XlogError::Parse(format!("Invalid prob_seed value: {}", value.as_str()))
296            })?;
297            program.directives.prob_seed = Some(seed);
298        }
299        Rule::pragma_prob_confidence => {
300            let value = pragma
301                .into_inner()
302                .next()
303                .ok_or_else(|| XlogError::Parse("Missing prob_confidence value".to_string()))?;
304            let confidence: f64 = value.as_str().parse().map_err(|_| {
305                XlogError::Parse(format!("Invalid prob_confidence value: {}", value.as_str()))
306            })?;
307            if !(0.0 < confidence && confidence < 1.0) || confidence.is_nan() {
308                return Err(XlogError::Parse(format!(
309                    "Invalid prob_confidence value: {}; expected 0 < confidence < 1",
310                    value.as_str()
311                )));
312            }
313            program.directives.prob_confidence = Some(confidence);
314        }
315        Rule::pragma_prob_method => {
316            let value = pragma
317                .into_inner()
318                .next()
319                .ok_or_else(|| XlogError::Parse("Missing prob_method value".to_string()))?;
320            let method = match value.as_str() {
321                "rejection" => ProbMethod::Rejection,
322                "evidence_clamping" => ProbMethod::EvidenceClamping,
323                other => {
324                    return Err(XlogError::Parse(format!(
325                        "Unknown prob_method value: {}",
326                        other
327                    )))
328                }
329            };
330            program.directives.prob_method = Some(method);
331        }
332        Rule::pragma_prob_max_nonmonotone_iterations => {
333            let value = pragma.into_inner().next().ok_or_else(|| {
334                XlogError::Parse("Missing prob_max_nonmonotone_iterations value".to_string())
335            })?;
336            let iterations: usize = value.as_str().parse().map_err(|_| {
337                XlogError::Parse(format!(
338                    "Invalid prob_max_nonmonotone_iterations value: {}",
339                    value.as_str()
340                ))
341            })?;
342            if iterations == 0 {
343                return Err(XlogError::Parse(
344                    "Invalid prob_max_nonmonotone_iterations value: expected > 0".to_string(),
345                ));
346            }
347            program.directives.prob_max_nonmonotone_iterations = Some(iterations);
348        }
349        Rule::pragma_max_recursion => {
350            let value = pragma
351                .into_inner()
352                .next()
353                .ok_or_else(|| XlogError::Parse("Missing max_recursion_depth value".to_string()))?;
354            let depth: u32 = value.as_str().parse().map_err(|_| {
355                XlogError::Parse(format!(
356                    "Invalid max_recursion_depth value: {}",
357                    value.as_str()
358                ))
359            })?;
360            program.directives.max_recursion_depth = Some(depth);
361        }
362        Rule::pragma_magic_sets => {
363            let value = pragma
364                .into_inner()
365                .next()
366                .ok_or_else(|| XlogError::Parse("Missing magic_sets value".to_string()))?;
367            let mode = match value.as_str() {
368                "auto" => MagicSetsMode::Auto,
369                "on" => MagicSetsMode::On,
370                "off" => MagicSetsMode::Off,
371                other => {
372                    return Err(XlogError::Parse(format!(
373                        "Unknown magic_sets value: {}",
374                        other
375                    )))
376                }
377            };
378            program.directives.magic_sets = Some(mode);
379        }
380        _ => {}
381    }
382
383    Ok(())
384}
385
386/// Build a domain declaration
387fn build_domain_decl(pair: Pair<'_, Rule>) -> Result<DomainDecl> {
388    let mut inner = pair.into_inner();
389    let name = inner
390        .next()
391        .ok_or_else(|| XlogError::Parse("Missing domain name".to_string()))?
392        .as_str()
393        .to_string();
394    let type_pair = inner
395        .next()
396        .ok_or_else(|| XlogError::Parse("Missing domain type".to_string()))?;
397    let typ = build_scalar_type_spec(type_pair, "domain alias")?;
398
399    Ok(DomainDecl { name, typ })
400}
401
402/// Parse a module path (e.g., "utils/math" -> ["utils", "math"])
403fn parse_module_path(pair: Pair<Rule>) -> Vec<String> {
404    pair.as_str().split('/').map(|s| s.to_string()).collect()
405}
406
407/// Build a use statement
408fn build_use_stmt(pair: Pair<Rule>) -> UseDecl {
409    let mut inner = pair.into_inner();
410
411    // Parse module path
412    let path_pair = inner.next().unwrap();
413    let module_path = parse_module_path(path_pair);
414
415    // Parse optional import list
416    let imports = inner.next().map(|import_list| {
417        import_list
418            .into_inner()
419            .map(|p| p.as_str().to_string())
420            .collect()
421    });
422
423    UseDecl {
424        module_path,
425        imports,
426    }
427}
428
429/// Build a predicate declaration
430fn build_pred_decl(pair: Pair<'_, Rule>) -> Result<PredDecl> {
431    let mut inner = pair.into_inner();
432    let mut is_private = false;
433
434    // Check for private modifier
435    let first = inner
436        .next()
437        .ok_or_else(|| XlogError::Parse("Missing predicate name".to_string()))?;
438
439    let name_pair = if first.as_rule() == Rule::private_mod {
440        is_private = true;
441        inner
442            .next()
443            .ok_or_else(|| XlogError::Parse("Missing predicate name after private".to_string()))?
444    } else {
445        first
446    };
447
448    let name = name_pair.as_str().to_string();
449
450    let mut columns = Vec::new();
451    for type_pair in inner {
452        if type_pair.as_rule() == Rule::type_list {
453            for col in type_pair.into_inner() {
454                columns.push(build_pred_column(col)?);
455            }
456        }
457    }
458    let types = columns.iter().map(|c| c.typ.clone()).collect();
459
460    Ok(PredDecl {
461        name,
462        types,
463        columns,
464        is_private,
465    })
466}
467
468fn build_pred_column(pair: Pair<'_, Rule>) -> Result<PredColumn> {
469    let mut inner = pair.into_inner();
470    let first = inner
471        .next()
472        .ok_or_else(|| XlogError::Parse("Empty predicate column".to_string()))?;
473
474    if first.as_rule() == Rule::ident {
475        let typ_pair = inner
476            .next()
477            .ok_or_else(|| XlogError::Parse("Missing named column type".to_string()))?;
478        Ok(PredColumn {
479            name: Some(first.as_str().to_string()),
480            typ: build_type_ref(typ_pair)?,
481        })
482    } else {
483        Ok(PredColumn {
484            name: None,
485            typ: build_type_ref(first)?,
486        })
487    }
488}
489
490/// Build a source type reference.
491fn build_type_ref(pair: Pair<'_, Rule>) -> Result<TypeRef> {
492    if pair.as_rule() == Rule::type_spec {
493        let raw = pair.as_str().to_string();
494        let mut inner = pair.into_inner();
495        if let Some(child) = inner.next() {
496            return build_type_ref(child);
497        }
498        return match raw.as_str() {
499            "term" => Ok(TypeRef::Term),
500            "compound" => Ok(TypeRef::Compound),
501            "predref" => Ok(TypeRef::PredRef),
502            other => Err(XlogError::Parse(format!("Unknown type: {}", other))),
503        };
504    }
505
506    match pair.as_rule() {
507        Rule::list_type => {
508            let item = pair
509                .into_inner()
510                .next()
511                .ok_or_else(|| XlogError::Parse("Missing list element type".to_string()))?;
512            Ok(TypeRef::List(Box::new(build_type_ref(item)?)))
513        }
514        Rule::scalar_type => build_scalar_type_name(pair.as_str()).map(TypeRef::Scalar),
515        Rule::ident => Ok(TypeRef::Domain(pair.as_str().to_string())),
516        _ => match pair.as_str() {
517            "term" => Ok(TypeRef::Term),
518            "compound" => Ok(TypeRef::Compound),
519            "predref" => Ok(TypeRef::PredRef),
520            other => Err(XlogError::Parse(format!("Unknown type: {}", other))),
521        },
522    }
523}
524
525fn build_scalar_type_spec(pair: Pair<'_, Rule>, context: &str) -> Result<ScalarType> {
526    match build_type_ref(pair)? {
527        TypeRef::Scalar(ty) => Ok(ty),
528        other => Err(XlogError::Parse(format!(
529            "v0.8.5 {} must use a scalar type, got {:?}",
530            context, other
531        ))),
532    }
533}
534
535fn build_scalar_type_name(type_str: &str) -> Result<ScalarType> {
536    match type_str {
537        "u32" => Ok(ScalarType::U32),
538        "u64" => Ok(ScalarType::U64),
539        "i32" => Ok(ScalarType::I32),
540        "i64" => Ok(ScalarType::I64),
541        "f32" => Ok(ScalarType::F32),
542        "f64" => Ok(ScalarType::F64),
543        "bool" => Ok(ScalarType::Bool),
544        "symbol" => Ok(ScalarType::Symbol),
545        _ => Err(XlogError::Parse(format!(
546            "Unknown scalar type: {}",
547            type_str
548        ))),
549    }
550}
551
552/// Parse a function parameter: X or X: f64
553fn parse_func_param(pair: Pair<'_, Rule>) -> Result<FuncParam> {
554    let mut inner = pair.into_inner();
555    let name = inner
556        .next()
557        .ok_or_else(|| XlogError::Parse("Missing parameter name".to_string()))?
558        .as_str()
559        .to_string();
560    let typ = inner
561        .next()
562        .map(|ta| {
563            // type_annotation contains type_spec
564            build_scalar_type_spec(
565                ta.into_inner()
566                    .next()
567                    .expect("type_annotation must contain type_spec"),
568                "function parameter type annotation",
569            )
570        })
571        .transpose()?;
572    Ok(FuncParam { name, typ })
573}
574
575/// Parse a comparison operator
576fn parse_cmp_op(pair: Pair<'_, Rule>) -> CompOp {
577    match pair.as_str() {
578        "==" | "=" => CompOp::Eq,
579        "!=" => CompOp::Ne,
580        "<" => CompOp::Lt,
581        "<=" => CompOp::Le,
582        ">" => CompOp::Gt,
583        ">=" => CompOp::Ge,
584        _ => unreachable!("unexpected comparison operator: {}", pair.as_str()),
585    }
586}
587
588/// Parse a conditional expression: if X < 0 then 0 - X else X
589fn parse_cond_expr(pair: Pair<'_, Rule>) -> Result<CondExpr> {
590    let mut inner = pair.into_inner();
591
592    // Parse condition test
593    let cond_test = inner
594        .next()
595        .ok_or_else(|| XlogError::Parse("Missing condition test".to_string()))?;
596    let mut test_inner = cond_test.into_inner();
597    let cond_left = build_arith_expr(
598        test_inner
599            .next()
600            .ok_or_else(|| XlogError::Parse("Missing left side of condition".to_string()))?,
601    )?;
602    let cond_op = parse_cmp_op(
603        test_inner
604            .next()
605            .ok_or_else(|| XlogError::Parse("Missing condition operator".to_string()))?,
606    );
607    let cond_right = build_arith_expr(
608        test_inner
609            .next()
610            .ok_or_else(|| XlogError::Parse("Missing right side of condition".to_string()))?,
611    )?;
612
613    // Parse then branch
614    let then_branch =
615        Box::new(parse_func_body(inner.next().ok_or_else(|| {
616            XlogError::Parse("Missing then branch".to_string())
617        })?)?);
618
619    // Parse else branch
620    let else_branch =
621        Box::new(parse_func_body(inner.next().ok_or_else(|| {
622            XlogError::Parse("Missing else branch".to_string())
623        })?)?);
624
625    Ok(CondExpr {
626        cond_left,
627        cond_op,
628        cond_right,
629        then_branch,
630        else_branch,
631    })
632}
633
634/// Parse a function body: arithmetic, conditional, or predicate-based
635fn parse_func_body(pair: Pair<'_, Rule>) -> Result<FuncBody> {
636    let inner = pair
637        .into_inner()
638        .next()
639        .ok_or_else(|| XlogError::Parse("Empty function body".to_string()))?;
640    match inner.as_rule() {
641        Rule::func_body_pred => {
642            let mut parts = inner.into_inner();
643            let result = parts
644                .next()
645                .ok_or_else(|| XlogError::Parse("Missing result variable".to_string()))?
646                .as_str()
647                .to_string();
648            let body = build_body(
649                parts
650                    .next()
651                    .ok_or_else(|| XlogError::Parse("Missing predicate body".to_string()))?,
652            )?;
653            Ok(FuncBody::Predicate { result, body })
654        }
655        Rule::func_body_arith => {
656            let arith_inner = inner
657                .into_inner()
658                .next()
659                .ok_or_else(|| XlogError::Parse("Empty arithmetic body".to_string()))?;
660            match arith_inner.as_rule() {
661                Rule::cond_expr => Ok(FuncBody::Conditional(parse_cond_expr(arith_inner)?)),
662                _ => Ok(FuncBody::Arithmetic(build_arith_expr(arith_inner)?)),
663            }
664        }
665        _ => Err(XlogError::Parse(format!(
666            "Unexpected rule in func_body: {:?}",
667            inner.as_rule()
668        ))),
669    }
670}
671
672/// Parse a function definition
673fn parse_func_def(pair: Pair<'_, Rule>) -> Result<FuncDef> {
674    let mut inner = pair.into_inner();
675    let mut is_private = false;
676
677    // Check for private modifier
678    let first = inner
679        .next()
680        .ok_or_else(|| XlogError::Parse("Empty function definition".to_string()))?;
681    let name_pair = if first.as_rule() == Rule::private_mod {
682        is_private = true;
683        inner
684            .next()
685            .ok_or_else(|| XlogError::Parse("Missing function name after private".to_string()))?
686    } else {
687        first
688    };
689
690    let name = name_pair.as_str().to_string();
691
692    let mut params = Vec::new();
693    let mut return_type = None;
694    let mut body = None;
695
696    for p in inner {
697        match p.as_rule() {
698            Rule::func_params => {
699                params = p
700                    .into_inner()
701                    .map(parse_func_param)
702                    .collect::<Result<Vec<_>>>()?;
703            }
704            Rule::return_type => {
705                return_type = Some(build_scalar_type_spec(
706                    p.into_inner()
707                        .next()
708                        .ok_or_else(|| XlogError::Parse("Missing return type".to_string()))?,
709                    "function return type annotation",
710                )?);
711            }
712            Rule::func_body => {
713                body = Some(parse_func_body(p)?);
714            }
715            _ => {}
716        }
717    }
718
719    Ok(FuncDef {
720        name,
721        params,
722        return_type,
723        body: body.ok_or_else(|| XlogError::Parse("Function must have a body".to_string()))?,
724        is_private,
725    })
726}
727
728/// Build a rule (with body)
729fn build_rule(pair: Pair<'_, Rule>) -> Result<AstRule> {
730    let mut inner = pair.into_inner();
731    let head_pair = inner
732        .next()
733        .ok_or_else(|| XlogError::Parse("Missing rule head".to_string()))?;
734    let head = build_head(head_pair)?;
735
736    let body_pair = inner
737        .next()
738        .ok_or_else(|| XlogError::Parse("Missing rule body".to_string()))?;
739    let body = build_body(body_pair)?;
740
741    Ok(AstRule { head, body })
742}
743
744/// Build a fact (rule with empty body)
745fn build_fact(pair: Pair<'_, Rule>) -> Result<AstRule> {
746    let mut inner = pair.into_inner();
747    let atom_pair = inner
748        .next()
749        .ok_or_else(|| XlogError::Parse("Missing fact atom".to_string()))?;
750    let head = build_atom(atom_pair)?;
751
752    Ok(AstRule { head, body: vec![] })
753}
754
755/// Build a constraint
756fn build_constraint(pair: Pair<'_, Rule>) -> Result<Constraint> {
757    let mut inner = pair.into_inner();
758    let body_pair = inner
759        .next()
760        .ok_or_else(|| XlogError::Parse("Missing constraint body".to_string()))?;
761    let body = build_body(body_pair)?;
762
763    Ok(Constraint {
764        authored_index: None,
765        body,
766    })
767}
768
769/// Build a query
770fn build_query(pair: Pair<'_, Rule>) -> Result<Query> {
771    let mut inner = pair.into_inner();
772    let atom_pair = inner
773        .next()
774        .ok_or_else(|| XlogError::Parse("Missing query atom".to_string()))?;
775    let atom = build_atom(atom_pair)?;
776
777    Ok(Query { atom })
778}
779
780fn build_prob_fact(pair: Pair<'_, Rule>) -> Result<ProbFact> {
781    let choice = pair
782        .into_inner()
783        .next()
784        .ok_or_else(|| XlogError::Parse("Missing probabilistic fact".to_string()))?;
785    build_prob_choice(choice)
786}
787
788fn build_annotated_disjunction(pair: Pair<'_, Rule>) -> Result<AnnotatedDisjunction> {
789    let mut choices = Vec::new();
790    for inner in pair.into_inner() {
791        if inner.as_rule() == Rule::prob_choice {
792            choices.push(build_prob_choice(inner)?);
793        }
794    }
795    if choices.is_empty() {
796        return Err(XlogError::Parse(
797            "Annotated disjunction must have at least one choice".to_string(),
798        ));
799    }
800    Ok(AnnotatedDisjunction { choices })
801}
802
803fn build_prob_choice(pair: Pair<'_, Rule>) -> Result<ProbFact> {
804    let mut inner = pair.into_inner();
805    let prob_pair = inner
806        .next()
807        .ok_or_else(|| XlogError::Parse("Missing probability".to_string()))?;
808    let prob: f64 = prob_pair
809        .as_str()
810        .parse()
811        .map_err(|_| XlogError::Parse(format!("Invalid probability: {}", prob_pair.as_str())))?;
812
813    let atom_pair = inner
814        .next()
815        .ok_or_else(|| XlogError::Parse("Missing probabilistic atom".to_string()))?;
816    let atom = build_atom(atom_pair)?;
817
818    Ok(ProbFact { prob, atom })
819}
820
821fn build_evidence(pair: Pair<'_, Rule>) -> Result<Evidence> {
822    let mut inner = pair.into_inner();
823    let atom_pair = inner
824        .next()
825        .ok_or_else(|| XlogError::Parse("Missing evidence atom".to_string()))?;
826    let atom = build_atom(atom_pair)?;
827
828    let value_pair = inner
829        .next()
830        .ok_or_else(|| XlogError::Parse("Missing evidence value".to_string()))?;
831    let value = match value_pair.as_str() {
832        "true" => true,
833        "false" => false,
834        other => {
835            return Err(XlogError::Parse(format!(
836                "Invalid evidence value (expected true/false): {}",
837                other
838            )))
839        }
840    };
841
842    Ok(Evidence { atom, value })
843}
844
845fn build_prob_query(pair: Pair<'_, Rule>) -> Result<ProbQuery> {
846    let atom_pair = pair
847        .into_inner()
848        .next()
849        .ok_or_else(|| XlogError::Parse("Missing query atom".to_string()))?;
850    let atom = build_atom(atom_pair)?;
851    Ok(ProbQuery { atom })
852}
853
854/// Build a neural predicate declaration
855/// Syntax: nn(network, [inputs], output, [labels]) :: pred(args).
856fn build_neural_pred_decl(pair: Pair<'_, Rule>) -> Result<NeuralPredDecl> {
857    let mut inner = pair.into_inner();
858
859    // Parse network name (ident)
860    let network = inner
861        .next()
862        .ok_or_else(|| XlogError::Parse("Missing network name in neural predicate".to_string()))?
863        .as_str()
864        .to_string();
865
866    // Parse input list
867    let input_list = inner
868        .next()
869        .ok_or_else(|| XlogError::Parse("Missing input list in neural predicate".to_string()))?;
870    let inputs: Vec<String> = input_list
871        .into_inner()
872        .map(|p| p.as_str().to_string())
873        .collect();
874
875    // Parse output variable
876    let output = inner
877        .next()
878        .ok_or_else(|| XlogError::Parse("Missing output variable in neural predicate".to_string()))?
879        .as_str()
880        .to_string();
881
882    // Check for optional label list or atom
883    let next = inner
884        .next()
885        .ok_or_else(|| XlogError::Parse("Missing predicate in neural predicate".to_string()))?;
886
887    let (labels, predicate) = if next.as_rule() == Rule::neural_label_list {
888        // Parse labels
889        let label_vec: Vec<NeuralLabel> = next
890            .into_inner()
891            .map(|label_pair| {
892                let label_inner = label_pair
893                    .into_inner()
894                    .next()
895                    .expect("neural_label must have inner");
896                match label_inner.as_rule() {
897                    Rule::integer => {
898                        let val: i64 = label_inner.as_str().parse().expect("valid integer");
899                        NeuralLabel::Integer(val)
900                    }
901                    Rule::ident => NeuralLabel::Symbol(label_inner.as_str().to_string()),
902                    _ => unreachable!("neural_label should be integer or ident"),
903                }
904            })
905            .collect();
906
907        // Parse atom
908        let atom_pair = inner.next().ok_or_else(|| {
909            XlogError::Parse("Missing predicate after labels in neural predicate".to_string())
910        })?;
911        let predicate = build_atom(atom_pair)?;
912
913        (Some(label_vec), predicate)
914    } else {
915        // No labels, next is the atom (embedding mode)
916        let predicate = build_atom(next)?;
917        (None, predicate)
918    };
919
920    Ok(NeuralPredDecl {
921        network,
922        inputs,
923        output,
924        labels,
925        predicate,
926    })
927}
928
929/// Build a learnable rule from a parsed pair.
930/// Grammar: learnable_rule = { "learnable" ~ "(" ~ ident ~ ")" ~ "::" ~ head ~ ":-" ~ body ~ "." }
931/// Uses build_head instead of build_atom because the grammar produces a `head` pair.
932fn build_learnable_rule(pair: Pair<'_, Rule>) -> Result<LearnableRule> {
933    let mut inner = pair.into_inner();
934    let mask_name = inner
935        .next()
936        .ok_or_else(|| XlogError::Parse("Missing learnable mask name".into()))?
937        .as_str()
938        .to_string();
939    let head = build_head(
940        inner
941            .next()
942            .ok_or_else(|| XlogError::Parse("Missing learnable head".into()))?,
943    )?;
944    let body = build_body(
945        inner
946            .next()
947            .ok_or_else(|| XlogError::Parse("Missing learnable body".into()))?,
948    )?;
949    Ok(LearnableRule {
950        mask_name,
951        head,
952        body,
953    })
954}
955
956/// Build a head (atom that may contain aggregates)
957fn build_head(pair: Pair<'_, Rule>) -> Result<Atom> {
958    let mut inner = pair.into_inner();
959    let predicate = inner
960        .next()
961        .ok_or_else(|| XlogError::Parse("Missing head predicate".to_string()))?
962        .as_str()
963        .to_string();
964
965    let mut terms = Vec::new();
966    for term_list in inner {
967        if term_list.as_rule() == Rule::head_term_list {
968            for head_term in term_list.into_inner() {
969                terms.push(build_head_term(head_term)?);
970            }
971        }
972    }
973
974    Ok(Atom { predicate, terms })
975}
976
977/// Build a head term (can be aggregate or regular term)
978fn build_head_term(pair: Pair<'_, Rule>) -> Result<Term> {
979    let inner = pair
980        .into_inner()
981        .next()
982        .ok_or_else(|| XlogError::Parse("Empty head term".to_string()))?;
983
984    match inner.as_rule() {
985        Rule::aggregate => build_aggregate(inner),
986        Rule::agg_term => {
987            // agg_term can contain aggregate or term
988            let agg_inner = inner
989                .into_inner()
990                .next()
991                .ok_or_else(|| XlogError::Parse("Empty agg_term".to_string()))?;
992            match agg_inner.as_rule() {
993                Rule::aggregate => build_aggregate(agg_inner),
994                Rule::term => build_term(agg_inner),
995                _ => build_term(agg_inner),
996            }
997        }
998        Rule::term => build_term(inner),
999        _ => build_term(inner),
1000    }
1001}
1002
1003/// Build an aggregate expression
1004fn build_aggregate(pair: Pair<'_, Rule>) -> Result<Term> {
1005    let mut inner = pair.into_inner();
1006    let op_pair = inner
1007        .next()
1008        .ok_or_else(|| XlogError::Parse("Missing aggregate operator".to_string()))?;
1009    let op = match op_pair.as_str() {
1010        "count" => AggOp::Count,
1011        "sum" => AggOp::Sum,
1012        "min" => AggOp::Min,
1013        "max" => AggOp::Max,
1014        "logsumexp" => AggOp::LogSumExp,
1015        _ => {
1016            return Err(XlogError::Parse(format!(
1017                "Unknown aggregate: {}",
1018                op_pair.as_str()
1019            )))
1020        }
1021    };
1022
1023    let var_pair = inner
1024        .next()
1025        .ok_or_else(|| XlogError::Parse("Missing aggregate variable".to_string()))?;
1026    let variable = var_pair.as_str().to_string();
1027
1028    Ok(Term::Aggregate(AggExpr { op, variable }))
1029}
1030
1031/// Build an atom
1032fn build_atom(pair: Pair<'_, Rule>) -> Result<Atom> {
1033    let mut inner = pair.into_inner();
1034    let predicate = inner
1035        .next()
1036        .ok_or_else(|| XlogError::Parse("Missing atom predicate".to_string()))?
1037        .as_str()
1038        .to_string();
1039
1040    let mut terms = Vec::new();
1041    for term_list in inner {
1042        if term_list.as_rule() == Rule::term_list {
1043            for term in term_list.into_inner() {
1044                terms.push(build_term(term)?);
1045            }
1046        }
1047    }
1048
1049    Ok(Atom { predicate, terms })
1050}
1051
1052/// Build a body (list of literals)
1053fn build_body(pair: Pair<'_, Rule>) -> Result<Vec<BodyLiteral>> {
1054    let mut literals = Vec::new();
1055
1056    for lit in pair.into_inner() {
1057        literals.push(build_body_literal(lit)?);
1058    }
1059
1060    Ok(literals)
1061}
1062
1063/// Build a body literal
1064fn build_body_literal(pair: Pair<'_, Rule>) -> Result<BodyLiteral> {
1065    let inner = pair
1066        .into_inner()
1067        .next()
1068        .ok_or_else(|| XlogError::Parse("Empty body literal".to_string()))?;
1069
1070    match inner.as_rule() {
1071        Rule::nested_modal_chain => build_nested_modal_chain(inner),
1072        Rule::negated_epistemic_atom => build_epistemic_literal(inner, true),
1073        Rule::epistemic_atom => build_epistemic_literal(inner, false),
1074        Rule::negated_atom => {
1075            let atom_pair = inner
1076                .into_inner()
1077                .next()
1078                .ok_or_else(|| XlogError::Parse("Missing negated atom".to_string()))?;
1079            Ok(BodyLiteral::Negated(build_atom(atom_pair)?))
1080        }
1081        Rule::atom => Ok(BodyLiteral::Positive(build_atom(inner)?)),
1082        Rule::comparison => Ok(BodyLiteral::Comparison(build_comparison(inner)?)),
1083        Rule::is_expr => Ok(BodyLiteral::IsExpr(build_is_expr(inner)?)),
1084        Rule::univ => Ok(BodyLiteral::Univ(build_univ(inner)?)),
1085        _ => Err(XlogError::Parse(format!(
1086            "Unknown body literal: {:?}",
1087            inner.as_rule()
1088        ))),
1089    }
1090}
1091
1092fn build_epistemic_literal(pair: Pair<'_, Rule>, negated: bool) -> Result<BodyLiteral> {
1093    let epistemic_pair = if pair.as_rule() == Rule::negated_epistemic_atom {
1094        pair.into_inner()
1095            .next()
1096            .ok_or_else(|| XlogError::Parse("Missing negated epistemic literal".to_string()))?
1097    } else {
1098        pair
1099    };
1100
1101    let mut inner = epistemic_pair.into_inner();
1102    let op_pair = inner
1103        .next()
1104        .ok_or_else(|| XlogError::Parse("Missing epistemic operator".to_string()))?;
1105    let op = match op_pair.as_str() {
1106        "know" => EpistemicOp::Know,
1107        "possible" => EpistemicOp::Possible,
1108        other => {
1109            return Err(XlogError::Parse(format!(
1110                "Unknown epistemic operator: {}",
1111                other
1112            )))
1113        }
1114    };
1115    let atom_pair = inner
1116        .next()
1117        .ok_or_else(|| XlogError::Parse("Missing epistemic atom".to_string()))?;
1118
1119    Ok(BodyLiteral::Epistemic(EpistemicLiteral {
1120        op,
1121        negated,
1122        atom: build_atom(atom_pair)?,
1123    }))
1124}
1125
1126/// Build a single epistemic literal from a NESTED modal chain by applying the
1127/// sound modal-logic (KD45/S5) collapse equivalence.
1128///
1129/// Under the autoepistemic modal axioms XLOG's `know`/`possible` operators assume
1130/// (positive introspection, axiom 4 `Kp → KKp`, and negative introspection,
1131/// axiom 5 `¬Kp → K¬Kp`, evaluated relative to the admissible world-view set),
1132/// a chain of modal operators over an atom COLLAPSES to the operator ADJACENT to
1133/// the atom (the innermost one):
1134///   `know possible p ≡ possible p`     (KM ≡ M)
1135///   `possible know  p ≡ know p`        (MK ≡ K)
1136///   `know know       p ≡ know p`       (KK ≡ K)
1137///   `possible possible p ≡ possible p` (MM ≡ M)
1138/// because each adjacent operator pair reduces by 4/5 and the inner operator wins.
1139/// A single LEADING negation distributes over the whole chain:
1140///   `not know possible p ≡ not possible p`.
1141/// This equivalence holds in BOTH epistemic modes (FAEEL and G91): introspection
1142/// holds WITHIN any single admissible world view in both modes; the modes differ
1143/// only in WHICH world views are admissible (founded least vs all stable), which
1144/// is exactly the single-level per-mode difference the collapsed literal inherits
1145/// by routing through the ordinary single-level epistemic path. The collapse adds
1146/// no new world-of-worlds evaluator.
1147///
1148/// A `not` before any operator negates the modal subformula to its right. A
1149/// `not` before the atom dualizes the atom-adjacent modal (`know not p` becomes
1150/// `not possible p`; `possible not p` becomes `not know p`). Under the same S5
1151/// collapse, all outer modal operators preserve the already-global inner modal
1152/// truth value, so the final single-level literal is determined by the
1153/// atom-adjacent operator, atom negation duality, and parity of `not` placements.
1154fn build_nested_modal_chain(pair: Pair<'_, Rule>) -> Result<BodyLiteral> {
1155    // Walk the chain left-to-right. Each `not_kw` negates whatever named token
1156    // follows it (the next operator, or the trailing atom). We record the
1157    // operators in order plus the negation that immediately precedes each, and
1158    // whether the trailing atom is preceded by a `not`.
1159    let mut ops: Vec<EpistemicOp> = Vec::new();
1160    let mut neg_before_op: Vec<bool> = Vec::new();
1161    let mut atom_pair: Option<Pair<'_, Rule>> = None;
1162    let mut pending_not = false;
1163    let mut neg_before_atom = false;
1164
1165    for token in pair.into_inner() {
1166        match token.as_rule() {
1167            Rule::not_kw => {
1168                pending_not = true;
1169            }
1170            Rule::epistemic_op => {
1171                let op = match token.as_str() {
1172                    "know" => EpistemicOp::Know,
1173                    "possible" => EpistemicOp::Possible,
1174                    other => {
1175                        return Err(XlogError::Parse(format!(
1176                            "Unknown epistemic operator: {}",
1177                            other
1178                        )))
1179                    }
1180                };
1181                ops.push(op);
1182                neg_before_op.push(pending_not);
1183                pending_not = false;
1184            }
1185            Rule::atom => {
1186                neg_before_atom = pending_not;
1187                pending_not = false;
1188                atom_pair = Some(token);
1189            }
1190            other => {
1191                return Err(XlogError::Parse(format!(
1192                    "Unexpected token in nested modal chain: {:?}",
1193                    other
1194                )));
1195            }
1196        }
1197    }
1198
1199    let atom_pair = atom_pair
1200        .ok_or_else(|| XlogError::Parse("Missing atom in nested modal chain".to_string()))?;
1201    if ops.len() < 2 {
1202        return Err(XlogError::Parse(
1203            "Nested modal chain must contain at least two epistemic operators".to_string(),
1204        ));
1205    }
1206
1207    let innermost = *ops
1208        .last()
1209        .ok_or_else(|| XlogError::Parse("Empty modal chain".to_string()))?;
1210    let op = if neg_before_atom {
1211        match innermost {
1212            EpistemicOp::Know => EpistemicOp::Possible,
1213            EpistemicOp::Possible => EpistemicOp::Know,
1214        }
1215    } else {
1216        innermost
1217    };
1218    let negated = neg_before_op
1219        .iter()
1220        .copied()
1221        .fold(neg_before_atom, |acc, neg| acc ^ neg);
1222
1223    Ok(BodyLiteral::Epistemic(EpistemicLiteral {
1224        op,
1225        negated,
1226        atom: build_atom(atom_pair)?,
1227    }))
1228}
1229
1230/// Build a finite univ literal.
1231fn build_univ(pair: Pair<'_, Rule>) -> Result<Univ> {
1232    let mut inner = pair.into_inner();
1233    let term = build_term(
1234        inner
1235            .next()
1236            .ok_or_else(|| XlogError::Parse("Missing univ term".to_string()))?,
1237    )?;
1238    let parts = build_term(
1239        inner
1240            .next()
1241            .ok_or_else(|| XlogError::Parse("Missing univ parts".to_string()))?,
1242    )?;
1243    Ok(Univ { term, parts })
1244}
1245
1246/// Build a comparison
1247fn build_comparison(pair: Pair<'_, Rule>) -> Result<Comparison> {
1248    let mut inner = pair.into_inner();
1249
1250    let left_pair = inner
1251        .next()
1252        .ok_or_else(|| XlogError::Parse("Missing comparison left operand".to_string()))?;
1253    let left = build_term(left_pair)?;
1254
1255    let op_pair = inner
1256        .next()
1257        .ok_or_else(|| XlogError::Parse("Missing comparison operator".to_string()))?;
1258    let op = match op_pair.as_str() {
1259        "==" | "=" => CompOp::Eq,
1260        "!=" => CompOp::Ne,
1261        "<" => CompOp::Lt,
1262        "<=" => CompOp::Le,
1263        ">" => CompOp::Gt,
1264        ">=" => CompOp::Ge,
1265        _ => {
1266            return Err(XlogError::Parse(format!(
1267                "Unknown comparison operator: {}",
1268                op_pair.as_str()
1269            )))
1270        }
1271    };
1272
1273    let right_pair = inner
1274        .next()
1275        .ok_or_else(|| XlogError::Parse("Missing comparison right operand".to_string()))?;
1276    let right = build_term(right_pair)?;
1277
1278    Ok(Comparison { left, op, right })
1279}
1280
1281/// Build a term
1282fn build_term(pair: Pair<'_, Rule>) -> Result<Term> {
1283    // term can directly contain variable, integer, etc. or be wrapped
1284    let inner = if pair.as_rule() == Rule::term {
1285        pair.into_inner()
1286            .next()
1287            .ok_or_else(|| XlogError::Parse("Empty term".to_string()))?
1288    } else {
1289        pair
1290    };
1291
1292    match inner.as_rule() {
1293        Rule::var_or_anon => {
1294            // Unwrap var_or_anon to get either anonymous or variable
1295            let var_inner = inner
1296                .into_inner()
1297                .next()
1298                .ok_or_else(|| XlogError::Parse("Empty var_or_anon".to_string()))?;
1299            match var_inner.as_rule() {
1300                Rule::anonymous => Ok(Term::Anonymous),
1301                Rule::variable => Ok(Term::Variable(var_inner.as_str().to_string())),
1302                _ => Err(XlogError::Parse(format!(
1303                    "Expected variable or anonymous, got: {:?}",
1304                    var_inner.as_rule()
1305                ))),
1306            }
1307        }
1308        Rule::variable => Ok(Term::Variable(inner.as_str().to_string())),
1309        Rule::anonymous => Ok(Term::Anonymous),
1310        Rule::integer => {
1311            let val: i64 = inner
1312                .as_str()
1313                .parse()
1314                .map_err(|_| XlogError::Parse(format!("Invalid integer: {}", inner.as_str())))?;
1315            Ok(Term::Integer(val))
1316        }
1317        Rule::float_num => {
1318            let val: f64 = inner
1319                .as_str()
1320                .parse()
1321                .map_err(|_| XlogError::Parse(format!("Invalid float: {}", inner.as_str())))?;
1322            Ok(Term::Float(val))
1323        }
1324        Rule::string_lit => {
1325            let s = inner.as_str();
1326            // Remove quotes
1327            let unquoted = &s[1..s.len() - 1];
1328            Ok(Term::String(unquoted.to_string()))
1329        }
1330        Rule::list_literal => {
1331            let items = inner
1332                .into_inner()
1333                .map(build_term)
1334                .collect::<Result<Vec<_>>>()?;
1335            Ok(Term::List(items))
1336        }
1337        Rule::cons_pattern => {
1338            let mut parts = inner.into_inner();
1339            let head = parts
1340                .next()
1341                .ok_or_else(|| XlogError::Parse("Missing cons head".to_string()))?;
1342            let tail = parts
1343                .next()
1344                .ok_or_else(|| XlogError::Parse("Missing cons tail".to_string()))?;
1345            Ok(Term::Cons {
1346                head: Box::new(build_term(head)?),
1347                tail: Box::new(build_term(tail)?),
1348            })
1349        }
1350        Rule::compound_term => {
1351            let mut parts = inner.into_inner();
1352            let functor = parts
1353                .next()
1354                .ok_or_else(|| XlogError::Parse("Missing compound functor".to_string()))?
1355                .as_str()
1356                .to_string();
1357            let mut args = Vec::new();
1358            if let Some(term_list) = parts.next() {
1359                for term in term_list.into_inner() {
1360                    args.push(build_term(term)?);
1361                }
1362            }
1363            Ok(Term::Compound { functor, args })
1364        }
1365        Rule::ident => Ok(Term::Symbol(symbol::intern(inner.as_str()))),
1366        _ => Err(XlogError::Parse(format!(
1367            "Unknown term type: {:?}",
1368            inner.as_rule()
1369        ))),
1370    }
1371}
1372
1373/// Build an arithmetic expression (handles additive operations + -)
1374/// Grammar: arith_expr = { arith_term ~ (arith_op_add ~ arith_term)* }
1375fn build_arith_expr(pair: Pair<'_, Rule>) -> Result<ArithExpr> {
1376    let mut inner = pair.into_inner();
1377
1378    // First operand is always an arith_term
1379    let first = inner
1380        .next()
1381        .ok_or_else(|| XlogError::Parse("Empty arithmetic expression".to_string()))?;
1382    let mut result = build_arith_term(first)?;
1383
1384    // Process remaining (operator, operand) pairs
1385    while let Some(op_pair) = inner.next() {
1386        let op_str = op_pair.as_str();
1387        let right_pair = inner.next().ok_or_else(|| {
1388            XlogError::Parse("Missing right operand in arith expression".to_string())
1389        })?;
1390        let right = build_arith_term(right_pair)?;
1391
1392        result = match op_str {
1393            "+" => ArithExpr::Add(Box::new(result), Box::new(right)),
1394            "-" => ArithExpr::Sub(Box::new(result), Box::new(right)),
1395            _ => {
1396                return Err(XlogError::Parse(format!(
1397                    "Unknown additive operator: {}",
1398                    op_str
1399                )))
1400            }
1401        };
1402    }
1403
1404    Ok(result)
1405}
1406
1407/// Build an arithmetic term (handles multiplicative operations * / %)
1408/// Grammar: arith_term = { arith_primary ~ (arith_op_mul ~ arith_primary)* }
1409fn build_arith_term(pair: Pair<'_, Rule>) -> Result<ArithExpr> {
1410    let mut inner = pair.into_inner();
1411
1412    // First operand is always an arith_primary
1413    let first = inner
1414        .next()
1415        .ok_or_else(|| XlogError::Parse("Empty arithmetic term".to_string()))?;
1416    let mut result = build_arith_primary(first)?;
1417
1418    // Process remaining (operator, operand) pairs
1419    while let Some(op_pair) = inner.next() {
1420        let op_str = op_pair.as_str();
1421        let right_pair = inner
1422            .next()
1423            .ok_or_else(|| XlogError::Parse("Missing right operand in arith term".to_string()))?;
1424        let right = build_arith_primary(right_pair)?;
1425
1426        result = match op_str {
1427            "*" => ArithExpr::Mul(Box::new(result), Box::new(right)),
1428            "/" => ArithExpr::Div(Box::new(result), Box::new(right)),
1429            "%" => ArithExpr::Mod(Box::new(result), Box::new(right)),
1430            _ => {
1431                return Err(XlogError::Parse(format!(
1432                    "Unknown multiplicative operator: {}",
1433                    op_str
1434                )))
1435            }
1436        };
1437    }
1438
1439    Ok(result)
1440}
1441
1442/// Build an arithmetic primary (leaf nodes, parentheses, and builtin functions)
1443/// Grammar: arith_primary = {
1444///     builtin_fn ~ "(" ~ arith_expr ~ ("," ~ (arith_expr | type_spec))* ~ ")" |
1445///     "(" ~ arith_expr ~ ")" |
1446///     variable |
1447///     integer |
1448///     float_num
1449/// }
1450fn build_arith_primary(pair: Pair<'_, Rule>) -> Result<ArithExpr> {
1451    let mut inner = pair.into_inner();
1452    let first = inner
1453        .next()
1454        .ok_or_else(|| XlogError::Parse("Empty arithmetic primary".to_string()))?;
1455
1456    match first.as_rule() {
1457        Rule::builtin_fn => {
1458            let fn_name = first.as_str();
1459            // Collect all arguments
1460            let args: Vec<Pair<'_, Rule>> = inner.collect();
1461
1462            match fn_name {
1463                "abs" => {
1464                    if args.len() != 1 {
1465                        return Err(XlogError::Parse(
1466                            "abs() takes exactly 1 argument".to_string(),
1467                        ));
1468                    }
1469                    let arg = build_arith_expr(args.into_iter().next().unwrap())?;
1470                    Ok(ArithExpr::Abs(Box::new(arg)))
1471                }
1472                "min" => {
1473                    if args.len() != 2 {
1474                        return Err(XlogError::Parse(
1475                            "min() takes exactly 2 arguments".to_string(),
1476                        ));
1477                    }
1478                    let mut args_iter = args.into_iter();
1479                    let arg1 = build_arith_expr(args_iter.next().unwrap())?;
1480                    let arg2 = build_arith_expr(args_iter.next().unwrap())?;
1481                    Ok(ArithExpr::Min(Box::new(arg1), Box::new(arg2)))
1482                }
1483                "max" => {
1484                    if args.len() != 2 {
1485                        return Err(XlogError::Parse(
1486                            "max() takes exactly 2 arguments".to_string(),
1487                        ));
1488                    }
1489                    let mut args_iter = args.into_iter();
1490                    let arg1 = build_arith_expr(args_iter.next().unwrap())?;
1491                    let arg2 = build_arith_expr(args_iter.next().unwrap())?;
1492                    Ok(ArithExpr::Max(Box::new(arg1), Box::new(arg2)))
1493                }
1494                "pow" => {
1495                    if args.len() != 2 {
1496                        return Err(XlogError::Parse(
1497                            "pow() takes exactly 2 arguments".to_string(),
1498                        ));
1499                    }
1500                    let mut args_iter = args.into_iter();
1501                    let arg1 = build_arith_expr(args_iter.next().unwrap())?;
1502                    let arg2 = build_arith_expr(args_iter.next().unwrap())?;
1503                    Ok(ArithExpr::Pow(Box::new(arg1), Box::new(arg2)))
1504                }
1505                "cast" => {
1506                    if args.len() != 2 {
1507                        return Err(XlogError::Parse(
1508                            "cast() takes exactly 2 arguments".to_string(),
1509                        ));
1510                    }
1511                    let mut args_iter = args.into_iter();
1512                    let arg1 = build_arith_expr(args_iter.next().unwrap())?;
1513                    let type_pair = args_iter.next().unwrap();
1514                    let target_type = build_scalar_type_spec(type_pair, "cast target")?;
1515                    Ok(ArithExpr::Cast(Box::new(arg1), target_type))
1516                }
1517                _ => Err(XlogError::Parse(format!(
1518                    "Unknown builtin function: {}",
1519                    fn_name
1520                ))),
1521            }
1522        }
1523        Rule::arith_expr => {
1524            // Parenthesized expression
1525            build_arith_expr(first)
1526        }
1527        Rule::variable => Ok(ArithExpr::Variable(first.as_str().to_string())),
1528        Rule::integer => {
1529            let val: i64 = first
1530                .as_str()
1531                .parse()
1532                .map_err(|_| XlogError::Parse(format!("Invalid integer: {}", first.as_str())))?;
1533            Ok(ArithExpr::Integer(val))
1534        }
1535        Rule::float_num => {
1536            let val: f64 = first
1537                .as_str()
1538                .parse()
1539                .map_err(|_| XlogError::Parse(format!("Invalid float: {}", first.as_str())))?;
1540            Ok(ArithExpr::Float(val))
1541        }
1542        Rule::func_call => {
1543            let mut call_inner = first.into_inner();
1544            let name = call_inner
1545                .next()
1546                .ok_or_else(|| XlogError::Parse("Missing function name".to_string()))?
1547                .as_str()
1548                .to_string();
1549            let args: Vec<ArithExpr> = call_inner
1550                .map(build_arith_expr)
1551                .collect::<Result<Vec<_>>>()?;
1552            Ok(ArithExpr::FuncCall { name, args })
1553        }
1554        _ => Err(XlogError::Parse(format!(
1555            "Unexpected token in arith_primary: {:?}",
1556            first.as_rule()
1557        ))),
1558    }
1559}
1560
1561/// Build an is-expression: Z is X + Y
1562fn build_is_expr(pair: Pair<'_, Rule>) -> Result<IsExpr> {
1563    let mut inner = pair.into_inner();
1564    let target = inner
1565        .next()
1566        .ok_or_else(|| XlogError::Parse("Missing target variable in is expression".to_string()))?
1567        .as_str()
1568        .to_string();
1569    let expr = build_arith_expr(
1570        inner
1571            .next()
1572            .ok_or_else(|| XlogError::Parse("Missing expression in is expression".to_string()))?,
1573    )?;
1574    Ok(IsExpr { target, expr })
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579    use super::*;
1580
1581    #[test]
1582    fn test_parse_fact() {
1583        let input = "edge(1, 2).";
1584        let result = parse_program(input);
1585        assert!(result.is_ok(), "Failed to parse fact: {:?}", result.err());
1586
1587        let program = result.unwrap();
1588        assert_eq!(program.rules.len(), 1);
1589        assert!(program.rules[0].is_fact());
1590        assert_eq!(program.rules[0].head.predicate, "edge");
1591        assert_eq!(program.rules[0].head.terms.len(), 2);
1592        assert_eq!(program.rules[0].head.terms[0], Term::Integer(1));
1593        assert_eq!(program.rules[0].head.terms[1], Term::Integer(2));
1594    }
1595
1596    #[test]
1597    fn test_parse_rule() {
1598        let input = "reach(X, Y) :- edge(X, Y).";
1599        let result = parse_program(input);
1600        assert!(result.is_ok(), "Failed to parse rule: {:?}", result.err());
1601
1602        let program = result.unwrap();
1603        assert_eq!(program.rules.len(), 1);
1604        assert!(!program.rules[0].is_fact());
1605        assert_eq!(program.rules[0].head.predicate, "reach");
1606        assert_eq!(program.rules[0].body.len(), 1);
1607    }
1608
1609    #[test]
1610    fn test_parse_recursive_rule() {
1611        let input = "reach(X, Z) :- reach(X, Y), edge(Y, Z).";
1612        let result = parse_program(input);
1613        assert!(
1614            result.is_ok(),
1615            "Failed to parse recursive rule: {:?}",
1616            result.err()
1617        );
1618
1619        let program = result.unwrap();
1620        assert_eq!(program.rules.len(), 1);
1621        assert_eq!(program.rules[0].body.len(), 2);
1622    }
1623
1624    #[test]
1625    fn test_parse_negation() {
1626        let input = "isolated(X) :- node(X), not edge(X, Y).";
1627        let result = parse_program(input);
1628        assert!(
1629            result.is_ok(),
1630            "Failed to parse negation: {:?}",
1631            result.err()
1632        );
1633
1634        let program = result.unwrap();
1635        assert!(program.rules[0].has_negation());
1636        assert!(matches!(&program.rules[0].body[1], BodyLiteral::Negated(_)));
1637    }
1638
1639    #[test]
1640    fn test_parse_epistemic_literal_syntax() {
1641        let input = "believed(X) :- node(X), know edge(X).";
1642        let result = parse_program(input);
1643        assert!(
1644            result.is_ok(),
1645            "Failed to parse epistemic literal: {:?}",
1646            result.err()
1647        );
1648
1649        let program = result.unwrap();
1650        assert_eq!(program.rules.len(), 1);
1651        assert_eq!(program.rules[0].body.len(), 2);
1652    }
1653
1654    #[test]
1655    fn test_parse_predicate_with_epistemic_keyword_prefix_as_atom() {
1656        let input = "friend_of_friend(A, C) :- knows(A, B), knows(B, C), A != C.";
1657        let program = parse_program(input).expect("parse ordinary predicate named knows");
1658
1659        assert_eq!(program.rules.len(), 1);
1660        assert_eq!(program.rules[0].body.len(), 3);
1661        assert!(
1662            matches!(&program.rules[0].body[0], BodyLiteral::Positive(atom) if atom.predicate == "knows")
1663        );
1664        assert!(
1665            matches!(&program.rules[0].body[1], BodyLiteral::Positive(atom) if atom.predicate == "knows")
1666        );
1667    }
1668
1669    #[test]
1670    fn test_parse_aggregate() {
1671        let input = "out_degree(X, count(Y)) :- edge(X, Y).";
1672        let result = parse_program(input);
1673        assert!(
1674            result.is_ok(),
1675            "Failed to parse aggregate: {:?}",
1676            result.err()
1677        );
1678
1679        let program = result.unwrap();
1680        assert!(program.rules[0].has_aggregation());
1681        if let Term::Aggregate(agg) = &program.rules[0].head.terms[1] {
1682            assert_eq!(agg.op, AggOp::Count);
1683            assert_eq!(agg.variable, "Y");
1684        } else {
1685            panic!("Expected aggregate term");
1686        }
1687    }
1688
1689    #[test]
1690    fn test_parse_logsumexp_aggregate() {
1691        let input = "score(X, logsumexp(Y)) :- obs(X, Y).";
1692        let result = parse_program(input);
1693        assert!(
1694            result.is_ok(),
1695            "Failed to parse logsumexp aggregate: {:?}",
1696            result.err()
1697        );
1698
1699        let program = result.unwrap();
1700        assert!(program.rules[0].has_aggregation());
1701        if let Term::Aggregate(agg) = &program.rules[0].head.terms[1] {
1702            assert_eq!(agg.op, AggOp::LogSumExp);
1703            assert_eq!(agg.variable, "Y");
1704        } else {
1705            panic!("Expected aggregate term");
1706        }
1707    }
1708
1709    #[test]
1710    fn test_parse_constraint() {
1711        let input = ":- reach(X, X).";
1712        let result = parse_program(input);
1713        assert!(
1714            result.is_ok(),
1715            "Failed to parse constraint: {:?}",
1716            result.err()
1717        );
1718
1719        let program = result.unwrap();
1720        assert_eq!(program.constraints.len(), 1);
1721        assert_eq!(program.constraints[0].body.len(), 1);
1722    }
1723
1724    #[test]
1725    fn test_parse_query() {
1726        let input = "?- reach(1, N).";
1727        let result = parse_program(input);
1728        assert!(result.is_ok(), "Failed to parse query: {:?}", result.err());
1729
1730        let program = result.unwrap();
1731        assert_eq!(program.queries.len(), 1);
1732        assert_eq!(program.queries[0].atom.predicate, "reach");
1733    }
1734
1735    #[test]
1736    fn test_parse_full_program() {
1737        let input = r#"
1738            edge(1, 2).
1739            edge(2, 3).
1740            edge(3, 4).
1741            reach(X, Y) :- edge(X, Y).
1742            reach(X, Z) :- reach(X, Y), edge(Y, Z).
1743            ?- reach(1, N).
1744        "#;
1745        let result = parse_program(input);
1746        assert!(
1747            result.is_ok(),
1748            "Failed to parse full program: {:?}",
1749            result.err()
1750        );
1751
1752        let program = result.unwrap();
1753        assert_eq!(program.rules.len(), 5); // 3 facts + 2 rules
1754        assert_eq!(program.queries.len(), 1);
1755        assert_eq!(program.facts().count(), 3);
1756        assert_eq!(program.proper_rules().count(), 2);
1757    }
1758
1759    #[test]
1760    fn test_parse_comparison() {
1761        let input = "small(X) :- value(X), X < 10.";
1762        let result = parse_program(input);
1763        assert!(
1764            result.is_ok(),
1765            "Failed to parse comparison: {:?}",
1766            result.err()
1767        );
1768
1769        let program = result.unwrap();
1770        assert_eq!(program.rules[0].body.len(), 2);
1771        if let BodyLiteral::Comparison(cmp) = &program.rules[0].body[1] {
1772            assert_eq!(cmp.op, CompOp::Lt);
1773            assert_eq!(cmp.left, Term::Variable("X".to_string()));
1774            assert_eq!(cmp.right, Term::Integer(10));
1775        } else {
1776            panic!("Expected comparison");
1777        }
1778    }
1779
1780    #[test]
1781    fn test_parse_pred_decl() {
1782        let input = "pred edge(u32, u32).";
1783        let result = parse_program(input);
1784        assert!(
1785            result.is_ok(),
1786            "Failed to parse pred decl: {:?}",
1787            result.err()
1788        );
1789
1790        let program = result.unwrap();
1791        assert_eq!(program.predicates.len(), 1);
1792        assert_eq!(program.predicates[0].name, "edge");
1793        assert_eq!(program.predicates[0].types.len(), 2);
1794        assert_eq!(
1795            program.predicates[0].types[0],
1796            TypeRef::Scalar(ScalarType::U32)
1797        );
1798        assert_eq!(
1799            program.predicates[0].types[1],
1800            TypeRef::Scalar(ScalarType::U32)
1801        );
1802    }
1803
1804    #[test]
1805    fn test_parse_anonymous_wildcard() {
1806        // Test anonymous wildcard in body
1807        let input = "has_child(X) :- parent(X, _).";
1808        let result = parse_program(input);
1809        assert!(
1810            result.is_ok(),
1811            "Failed to parse anonymous wildcard: {:?}",
1812            result.err()
1813        );
1814
1815        let program = result.unwrap();
1816        assert_eq!(program.rules.len(), 1);
1817        let rule = &program.rules[0];
1818        assert_eq!(rule.head.predicate, "has_child");
1819
1820        // Check body atom has anonymous term
1821        if let BodyLiteral::Positive(atom) = &rule.body[0] {
1822            assert_eq!(atom.predicate, "parent");
1823            assert_eq!(atom.terms.len(), 2);
1824            assert_eq!(atom.terms[0], Term::Variable("X".to_string()));
1825            assert_eq!(atom.terms[1], Term::Anonymous);
1826        } else {
1827            panic!("Expected positive atom");
1828        }
1829    }
1830
1831    #[test]
1832    fn test_parse_multiple_wildcards() {
1833        // Multiple wildcards in same rule - each is independent
1834        let input = "exists(X) :- rel(X, _, _).";
1835        let result = parse_program(input);
1836        assert!(
1837            result.is_ok(),
1838            "Failed to parse multiple wildcards: {:?}",
1839            result.err()
1840        );
1841
1842        let program = result.unwrap();
1843        if let BodyLiteral::Positive(atom) = &program.rules[0].body[0] {
1844            assert_eq!(atom.terms.len(), 3);
1845            assert_eq!(atom.terms[0], Term::Variable("X".to_string()));
1846            assert_eq!(atom.terms[1], Term::Anonymous);
1847            assert_eq!(atom.terms[2], Term::Anonymous);
1848        }
1849    }
1850
1851    #[test]
1852    fn test_parse_is_expr() {
1853        // Test that grammar accepts 'is' expressions before AST lowering.
1854        let input = "result(X, Z) :- input(X, Y), Z is Y + 1.";
1855        let result = XlogParser::parse(Rule::program, input);
1856        assert!(
1857            result.is_ok(),
1858            "Failed to parse is expression: {:?}",
1859            result.err()
1860        );
1861    }
1862
1863    #[test]
1864    fn test_parse_arithmetic_precedence() {
1865        // Multiplication before addition
1866        let input = "r(X, Z) :- p(X, A, B), Z is A + B * 2.";
1867        let result = parse_program(input).unwrap();
1868        let rule = &result.rules[0];
1869        assert_eq!(rule.body.len(), 2);
1870        assert!(matches!(&rule.body[1], BodyLiteral::IsExpr(_)));
1871    }
1872
1873    #[test]
1874    fn test_parse_arithmetic_parentheses() {
1875        let input = "r(X, Z) :- p(X, A, B), Z is (A + B) * 2.";
1876        assert!(parse_program(input).is_ok());
1877    }
1878
1879    #[test]
1880    fn test_parse_probabilistic_fact_syntax() {
1881        let input = "0.7::rain().";
1882        let result = parse_program(input);
1883        assert!(
1884            result.is_ok(),
1885            "Failed to parse probabilistic fact: {:?}",
1886            result.err()
1887        );
1888    }
1889
1890    #[test]
1891    fn test_parse_annotated_disjunction_syntax() {
1892        let input = "0.6::coin(heads); 0.4::coin(tails).";
1893        let result = parse_program(input);
1894        assert!(
1895            result.is_ok(),
1896            "Failed to parse annotated disjunction: {:?}",
1897            result.err()
1898        );
1899    }
1900
1901    #[test]
1902    fn test_parse_evidence_is_not_a_fact() {
1903        let input = "evidence(rain(), true).";
1904        let program = parse_program(input).unwrap();
1905        assert_eq!(
1906            program.rules.len(),
1907            0,
1908            "evidence/2 should not be parsed as a regular fact"
1909        );
1910    }
1911
1912    #[test]
1913    fn test_parse_query_directive_is_not_a_fact() {
1914        let input = "query(reach(1,3)).";
1915        let program = parse_program(input).unwrap();
1916        assert_eq!(
1917            program.rules.len(),
1918            0,
1919            "query/1 should not be parsed as a regular fact"
1920        );
1921    }
1922
1923    #[test]
1924    fn test_parse_prob_engine_pragma_syntax() {
1925        let input = "#pragma prob_engine = mc";
1926        let result = parse_program(input);
1927        assert!(
1928            result.is_ok(),
1929            "Failed to parse prob_engine pragma: {:?}",
1930            result.err()
1931        );
1932    }
1933
1934    #[test]
1935    fn test_parse_epistemic_mode_pragma_syntax() {
1936        let input = "#pragma epistemic_mode = faeel";
1937        let result = parse_program(input);
1938        assert!(
1939            result.is_ok(),
1940            "Failed to parse epistemic_mode pragma: {:?}",
1941            result.err()
1942        );
1943    }
1944
1945    #[test]
1946    fn test_parse_probabilistic_fact_ast() {
1947        let program = parse_program("0.7::rain().").unwrap();
1948        assert_eq!(program.prob_facts.len(), 1);
1949        assert!((program.prob_facts[0].prob - 0.7).abs() < 1e-9);
1950        assert_eq!(program.prob_facts[0].atom.predicate, "rain");
1951        assert!(program.prob_facts[0].atom.terms.is_empty());
1952    }
1953
1954    #[test]
1955    fn test_parse_annotated_disjunction_ast() {
1956        let program = parse_program("0.6::coin(heads); 0.4::coin(tails).").unwrap();
1957        assert_eq!(program.annotated_disjunctions.len(), 1);
1958        let ad = &program.annotated_disjunctions[0];
1959        assert_eq!(ad.choices.len(), 2);
1960        assert!((ad.choices[0].prob - 0.6).abs() < 1e-9);
1961        assert_eq!(ad.choices[0].atom.predicate, "coin");
1962        assert_eq!(ad.choices[0].atom.terms.len(), 1);
1963        assert_eq!(
1964            ad.choices[0].atom.terms[0],
1965            Term::Symbol(symbol::intern("heads"))
1966        );
1967        assert!((ad.choices[1].prob - 0.4).abs() < 1e-9);
1968        assert_eq!(
1969            ad.choices[1].atom.terms[0],
1970            Term::Symbol(symbol::intern("tails"))
1971        );
1972    }
1973
1974    #[test]
1975    fn test_parse_evidence_ast() {
1976        let program = parse_program("evidence(rain(), true).").unwrap();
1977        assert_eq!(program.evidence.len(), 1);
1978        assert_eq!(program.evidence[0].atom.predicate, "rain");
1979        assert!(program.evidence[0].value);
1980    }
1981
1982    #[test]
1983    fn test_parse_prob_query_ast() {
1984        let program = parse_program("query(reach(1,3)).").unwrap();
1985        assert_eq!(program.prob_queries.len(), 1);
1986        assert_eq!(program.prob_queries[0].atom.predicate, "reach");
1987        assert_eq!(program.prob_queries[0].atom.terms.len(), 2);
1988        assert_eq!(program.prob_queries[0].atom.terms[0], Term::Integer(1));
1989        assert_eq!(program.prob_queries[0].atom.terms[1], Term::Integer(3));
1990    }
1991
1992    #[test]
1993    fn test_parse_prob_engine_pragma_ast() {
1994        let program = parse_program("#pragma prob_engine = mc").unwrap();
1995        assert_eq!(
1996            program.directives.prob_engine,
1997            Some(crate::ast::ProbEngine::Mc)
1998        );
1999        assert_eq!(program.prob_engine(), crate::ast::ProbEngine::Mc);
2000    }
2001
2002    #[test]
2003    fn test_parse_arithmetic_builtins() {
2004        let inputs = [
2005            "r(X, Z) :- p(X, Y), Z is abs(Y).",
2006            "r(X, Z) :- p(X, A, B), Z is min(A, B).",
2007            "r(X, Z) :- p(X, A, B), Z is max(A, B).",
2008            "r(X, Z) :- p(X, A, B), Z is pow(A, B).",
2009            "r(X, Z) :- p(X, Y), Z is cast(Y, f64).",
2010        ];
2011        for input in inputs {
2012            assert!(parse_program(input).is_ok(), "Failed to parse: {}", input);
2013        }
2014    }
2015
2016    #[test]
2017    fn test_parse_arithmetic_nested() {
2018        let input = "r(X, Z) :- p(X, A, B, C), Z is abs(A - B) + min(B, C) * 2.";
2019        assert!(parse_program(input).is_ok());
2020    }
2021
2022    /// Parse a one-rule program whose single body literal is an epistemic literal
2023    /// and return that literal.
2024    fn first_epistemic_literal(src: &str) -> EpistemicLiteral {
2025        let program = parse_program(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
2026        let rule = program.rules.first().expect("one rule");
2027        match rule.body.first().expect("one body literal") {
2028            BodyLiteral::Epistemic(lit) => lit.clone(),
2029            other => panic!("expected epistemic literal, got {other:?}"),
2030        }
2031    }
2032
2033    #[test]
2034    fn test_nested_modal_chain_collapses_to_innermost_operator() {
2035        // know possible p ≡ possible p  (KM ≡ M, inner operator wins)
2036        let lit = first_epistemic_literal("q() :- know possible p().");
2037        assert_eq!(lit.op, EpistemicOp::Possible);
2038        assert!(!lit.negated);
2039        assert_eq!(lit.atom.predicate, "p");
2040
2041        // possible know p ≡ know p  (MK ≡ K)
2042        let lit = first_epistemic_literal("q() :- possible know p().");
2043        assert_eq!(lit.op, EpistemicOp::Know);
2044        assert!(!lit.negated);
2045
2046        // know know p ≡ know p  (KK ≡ K)
2047        let lit = first_epistemic_literal("q() :- know know p().");
2048        assert_eq!(lit.op, EpistemicOp::Know);
2049
2050        // possible possible p ≡ possible p  (MM ≡ M)
2051        let lit = first_epistemic_literal("q() :- possible possible p().");
2052        assert_eq!(lit.op, EpistemicOp::Possible);
2053
2054        // 3-deep chain: innermost (atom-adjacent) operator still wins.
2055        let lit = first_epistemic_literal("q() :- know possible know p().");
2056        assert_eq!(lit.op, EpistemicOp::Know);
2057    }
2058
2059    #[test]
2060    fn test_nested_modal_chain_leading_negation_distributes() {
2061        // not know possible p ≡ not possible p
2062        let lit = first_epistemic_literal("q() :- not know possible p().");
2063        assert_eq!(lit.op, EpistemicOp::Possible);
2064        assert!(lit.negated);
2065
2066        // not possible know p ≡ not know p
2067        let lit = first_epistemic_literal("q() :- not possible know p().");
2068        assert_eq!(lit.op, EpistemicOp::Know);
2069        assert!(lit.negated);
2070    }
2071
2072    #[test]
2073    fn test_nested_modal_chain_interior_negation_dualizes() {
2074        // know not possible p ≡ not possible p
2075        let lit = first_epistemic_literal("q() :- know not possible p().");
2076        assert_eq!(lit.op, EpistemicOp::Possible);
2077        assert!(lit.negated);
2078
2079        // possible not know p ≡ not know p
2080        let lit = first_epistemic_literal("q() :- possible not know p().");
2081        assert_eq!(lit.op, EpistemicOp::Know);
2082        assert!(lit.negated);
2083
2084        // not know not possible p ≡ possible p
2085        let lit = first_epistemic_literal("q() :- not know not possible p().");
2086        assert_eq!(lit.op, EpistemicOp::Possible);
2087        assert!(!lit.negated);
2088    }
2089
2090    #[test]
2091    fn test_nested_modal_chain_atom_adjacent_negation_dualizes() {
2092        // know possible not p ≡ not know p
2093        let lit = first_epistemic_literal("q() :- know possible not p().");
2094        assert_eq!(lit.op, EpistemicOp::Know);
2095        assert!(lit.negated);
2096
2097        // possible know not p ≡ not possible p
2098        let lit = first_epistemic_literal("q() :- possible know not p().");
2099        assert_eq!(lit.op, EpistemicOp::Possible);
2100        assert!(lit.negated);
2101    }
2102}