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