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