1#![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#[derive(Parser)]
25#[grammar = "grammar.pest"]
26pub struct XlogParser;
27
28pub type ParseResult<'a> = pest::iterators::Pairs<'a, Rule>;
30
31pub fn parse_program(input: &str) -> Result<Program> {
37 parse_program_with_stats(input).map(|(program, _)| program)
38}
39
40pub 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
51pub struct FastParseStats {
52 pub fast_facts: usize,
54 pub residual_bytes: usize,
56 pub fell_back: bool,
59}
60
61pub 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 stats.fell_back = true;
87 parse_program_reference(input).map(|program| (program, stats))
88 }
89 }
90}
91
92pub fn parse_statement(input: &str) -> Result<ParseResult<'_>> {
94 XlogParser::parse(Rule::statement, input).map_err(|e| XlogError::Parse(e.to_string()))
95}
96
97pub fn parse_atom(input: &str) -> Result<ParseResult<'_>> {
99 XlogParser::parse(Rule::atom, input).map_err(|e| XlogError::Parse(e.to_string()))
100}
101
102fn 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
139fn 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
160fn 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
389fn 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
405fn parse_module_path(pair: Pair<Rule>) -> Vec<String> {
407 pair.as_str().split('/').map(|s| s.to_string()).collect()
408}
409
410fn build_use_stmt(pair: Pair<Rule>) -> UseDecl {
412 let mut inner = pair.into_inner();
413
414 let path_pair = inner.next().unwrap();
416 let module_path = parse_module_path(path_pair);
417
418 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
432fn build_pred_decl(pair: Pair<'_, Rule>) -> Result<PredDecl> {
434 let mut inner = pair.into_inner();
435 let mut is_private = false;
436
437 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
493fn 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
555fn 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 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
578fn 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
591fn parse_cond_expr(pair: Pair<'_, Rule>) -> Result<CondExpr> {
593 let mut inner = pair.into_inner();
594
595 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 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 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
637fn 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
675fn parse_func_def(pair: Pair<'_, Rule>) -> Result<FuncDef> {
677 let mut inner = pair.into_inner();
678 let mut is_private = false;
679
680 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
731fn 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
747fn 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
758fn 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
772fn 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
857fn build_neural_pred_decl(pair: Pair<'_, Rule>) -> Result<NeuralPredDecl> {
860 let mut inner = pair.into_inner();
861
862 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 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 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 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 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 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 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
932fn 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
959fn 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
980fn 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 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
1006fn 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
1034fn 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
1055fn 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
1066fn 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
1129fn build_nested_modal_chain(pair: Pair<'_, Rule>) -> Result<BodyLiteral> {
1158 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
1233fn 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
1249fn 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
1284fn build_term(pair: Pair<'_, Rule>) -> Result<Term> {
1286 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 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 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
1376fn build_arith_expr(pair: Pair<'_, Rule>) -> Result<ArithExpr> {
1379 let mut inner = pair.into_inner();
1380
1381 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 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
1410fn build_arith_term(pair: Pair<'_, Rule>) -> Result<ArithExpr> {
1413 let mut inner = pair.into_inner();
1414
1415 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 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
1445fn 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 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 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
1564fn 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); 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 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 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 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 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 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 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 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 let lit = first_epistemic_literal("q() :- possible know p().");
2046 assert_eq!(lit.op, EpistemicOp::Know);
2047 assert!(!lit.negated);
2048
2049 let lit = first_epistemic_literal("q() :- know know p().");
2051 assert_eq!(lit.op, EpistemicOp::Know);
2052
2053 let lit = first_epistemic_literal("q() :- possible possible p().");
2055 assert_eq!(lit.op, EpistemicOp::Possible);
2056
2057 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 let lit = first_epistemic_literal("q() :- not know possible p().");
2066 assert_eq!(lit.op, EpistemicOp::Possible);
2067 assert!(lit.negated);
2068
2069 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 let lit = first_epistemic_literal("q() :- know not possible p().");
2079 assert_eq!(lit.op, EpistemicOp::Possible);
2080 assert!(lit.negated);
2081
2082 let lit = first_epistemic_literal("q() :- possible not know p().");
2084 assert_eq!(lit.op, EpistemicOp::Know);
2085 assert!(lit.negated);
2086
2087 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 let lit = first_epistemic_literal("q() :- know possible not p().");
2097 assert_eq!(lit.op, EpistemicOp::Know);
2098 assert!(lit.negated);
2099
2100 let lit = first_epistemic_literal("q() :- possible know not p().");
2102 assert_eq!(lit.op, EpistemicOp::Possible);
2103 assert!(lit.negated);
2104 }
2105}