Skip to main content

xlog_logic/
ast.rs

1//! Abstract Syntax Tree for XLOG programs
2
3use xlog_core::{Result, ScalarType, XlogError};
4
5/// A term in an atom
6#[derive(Debug, Clone, PartialEq)]
7pub enum Term {
8    /// Named logic variable (e.g. `X`).
9    Variable(String),
10    /// Anonymous wildcard `_` -- each occurrence is a fresh unnamed variable.
11    Anonymous,
12    /// Integer literal.
13    Integer(i64),
14    /// Floating-point literal.
15    Float(f64),
16    /// Quoted string literal.
17    String(String),
18    /// Interned symbol ID -- use `xlog_core::symbol::resolve(id)` to get the string.
19    Symbol(u32),
20    /// Finite list literal.
21    List(Vec<Term>),
22    /// Finite cons pattern `[Head | Tail]`.
23    Cons {
24        /// Head term.
25        head: Box<Term>,
26        /// Tail term.
27        tail: Box<Term>,
28    },
29    /// Finite compound term.
30    Compound {
31        /// Functor name.
32        functor: String,
33        /// Compound arguments.
34        args: Vec<Term>,
35    },
36    /// Static predicate reference.
37    PredRef(String),
38    /// Aggregate expression (e.g. `count(X)`).
39    Aggregate(AggExpr),
40}
41
42impl Term {
43    /// Returns true if this is a named variable.
44    pub fn is_variable(&self) -> bool {
45        matches!(self, Term::Variable(_))
46    }
47
48    /// Returns true if this is an anonymous wildcard `_`
49    pub fn is_anonymous(&self) -> bool {
50        matches!(self, Term::Anonymous)
51    }
52
53    /// Returns true if this is any kind of variable (named or anonymous)
54    pub fn is_any_variable(&self) -> bool {
55        matches!(self, Term::Variable(_) | Term::Anonymous)
56    }
57
58    /// Returns true if this is a ground (non-variable, non-aggregate) term.
59    pub fn is_constant(&self) -> bool {
60        !self.is_any_variable()
61            && !matches!(
62                self,
63                Term::Aggregate(_)
64                    | Term::List(_)
65                    | Term::Cons { .. }
66                    | Term::Compound { .. }
67                    | Term::PredRef(_)
68            )
69    }
70
71    /// Returns the variable name, or None for anonymous/constants
72    pub fn variable_name(&self) -> Option<&str> {
73        match self {
74            Term::Variable(name) => Some(name),
75            _ => None,
76        }
77    }
78
79    /// Infer the scalar storage type used when no predicate declaration
80    /// supplies a schema for this term.
81    pub fn inferred_scalar_type(&self) -> ScalarType {
82        match self {
83            Term::Variable(_) | Term::Anonymous => ScalarType::U64,
84            Term::Integer(value) => {
85                if *value >= 0 && *value <= u32::MAX as i64 {
86                    ScalarType::U32
87                } else {
88                    ScalarType::I64
89                }
90            }
91            Term::Float(_) => ScalarType::F64,
92            Term::String(_) | Term::Symbol(_) => ScalarType::Symbol,
93            Term::List(_) | Term::Cons { .. } | Term::Compound { .. } | Term::PredRef(_) => {
94                ScalarType::U64
95            }
96            Term::Aggregate(aggregate) => aggregate.default_result_type(),
97        }
98    }
99
100    /// Return all named variables referenced by this term.
101    pub fn variables(&self) -> Vec<&str> {
102        match self {
103            Term::Variable(name) => vec![name.as_str()],
104            Term::List(items) => items.iter().flat_map(Term::variables).collect(),
105            Term::Cons { head, tail } => {
106                let mut vars = head.variables();
107                vars.extend(tail.variables());
108                vars
109            }
110            Term::Compound { args, .. } => args.iter().flat_map(Term::variables).collect(),
111            Term::Anonymous
112            | Term::Integer(_)
113            | Term::Float(_)
114            | Term::String(_)
115            | Term::Symbol(_)
116            | Term::PredRef(_)
117            | Term::Aggregate(_) => vec![],
118        }
119    }
120}
121
122/// Aggregate expression
123#[derive(Debug, Clone, PartialEq)]
124pub struct AggExpr {
125    /// The aggregation operator.
126    pub op: AggOp,
127    /// The variable being aggregated.
128    pub variable: String,
129}
130
131impl AggExpr {
132    /// Return the runtime result type for a known aggregate input type.
133    ///
134    /// `None` means the execution provider does not support that operator for the
135    /// supplied input type.
136    pub(crate) fn result_type_for_input(&self, input: ScalarType) -> Option<ScalarType> {
137        match self.op {
138            AggOp::Count => Some(ScalarType::U64),
139            AggOp::Sum if matches!(input, ScalarType::U32 | ScalarType::U64) => {
140                Some(ScalarType::U64)
141            }
142            AggOp::Min | AggOp::Max if matches!(input, ScalarType::U32 | ScalarType::U64) => {
143                Some(input)
144            }
145            AggOp::LogSumExp if input == ScalarType::F64 => Some(ScalarType::F64),
146            AggOp::Sum | AggOp::Min | AggOp::Max | AggOp::LogSumExp => None,
147        }
148    }
149
150    /// Return a result type that is independent of a not-yet-known input.
151    pub(crate) fn input_independent_result_type(&self) -> Option<ScalarType> {
152        match self.op {
153            AggOp::Count | AggOp::Sum => Some(ScalarType::U64),
154            AggOp::LogSumExp => Some(ScalarType::F64),
155            AggOp::Min | AggOp::Max => None,
156        }
157    }
158
159    fn default_result_type(&self) -> ScalarType {
160        self.input_independent_result_type()
161            .unwrap_or(ScalarType::U64)
162    }
163}
164
165/// Aggregation operator
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
167pub enum AggOp {
168    /// Count aggregation.
169    Count,
170    /// Sum aggregation.
171    Sum,
172    /// Minimum aggregation.
173    Min,
174    /// Maximum aggregation.
175    Max,
176    /// Log-sum-exp aggregation.
177    LogSumExp,
178}
179
180/// Arithmetic expression tree
181#[derive(Debug, Clone, PartialEq)]
182pub enum ArithExpr {
183    /// Variable reference.
184    Variable(String),
185    /// Integer literal.
186    Integer(i64),
187    /// Float literal.
188    Float(f64),
189
190    /// Addition.
191    Add(Box<ArithExpr>, Box<ArithExpr>),
192    /// Subtraction.
193    Sub(Box<ArithExpr>, Box<ArithExpr>),
194    /// Multiplication.
195    Mul(Box<ArithExpr>, Box<ArithExpr>),
196    /// Division.
197    Div(Box<ArithExpr>, Box<ArithExpr>),
198    /// Modulo.
199    Mod(Box<ArithExpr>, Box<ArithExpr>),
200
201    /// Absolute value.
202    Abs(Box<ArithExpr>),
203    /// Minimum of two values.
204    Min(Box<ArithExpr>, Box<ArithExpr>),
205    /// Maximum of two values.
206    Max(Box<ArithExpr>, Box<ArithExpr>),
207    /// Power (base, exponent).
208    Pow(Box<ArithExpr>, Box<ArithExpr>),
209
210    /// Type cast to the given scalar type.
211    Cast(Box<ArithExpr>, ScalarType),
212
213    /// User-defined function call
214    FuncCall {
215        /// Function name being invoked.
216        name: String,
217        /// Positional arguments supplied to the function.
218        args: Vec<ArithExpr>,
219    },
220
221    /// Conditional expression (for expanded function bodies)
222    Conditional {
223        /// Left operand of the condition.
224        cond_left: Box<ArithExpr>,
225        /// Comparison operator used in the condition.
226        cond_op: CompOp,
227        /// Right operand of the condition.
228        cond_right: Box<ArithExpr>,
229        /// Expression evaluated when the condition is true.
230        then_expr: Box<ArithExpr>,
231        /// Expression evaluated when the condition is false.
232        else_expr: Box<ArithExpr>,
233    },
234}
235
236impl ArithExpr {
237    /// Get all variable names used in this expression
238    pub fn variables(&self) -> Vec<&str> {
239        match self {
240            ArithExpr::Variable(name) => vec![name.as_str()],
241            ArithExpr::Integer(_) | ArithExpr::Float(_) => vec![],
242            ArithExpr::Add(l, r)
243            | ArithExpr::Sub(l, r)
244            | ArithExpr::Mul(l, r)
245            | ArithExpr::Div(l, r)
246            | ArithExpr::Mod(l, r)
247            | ArithExpr::Min(l, r)
248            | ArithExpr::Max(l, r)
249            | ArithExpr::Pow(l, r) => {
250                let mut vars = l.variables();
251                vars.extend(r.variables());
252                vars
253            }
254            ArithExpr::Abs(e) | ArithExpr::Cast(e, _) => e.variables(),
255            ArithExpr::FuncCall { args, .. } => args.iter().flat_map(|a| a.variables()).collect(),
256            ArithExpr::Conditional {
257                cond_left,
258                cond_right,
259                then_expr,
260                else_expr,
261                ..
262            } => {
263                let mut vars = cond_left.variables();
264                vars.extend(cond_right.variables());
265                vars.extend(then_expr.variables());
266                vars.extend(else_expr.variables());
267                vars
268            }
269        }
270    }
271}
272
273/// Is-expression for variable binding: Z is X + Y
274#[derive(Debug, Clone, PartialEq)]
275pub struct IsExpr {
276    /// Target variable (must be a fresh, unbound variable).
277    pub target: String,
278    /// Arithmetic expression to evaluate.
279    pub expr: ArithExpr,
280}
281
282/// An atom (predicate applied to terms)
283#[derive(Debug, Clone, PartialEq)]
284pub struct Atom {
285    /// Predicate name.
286    pub predicate: String,
287    /// Argument terms.
288    pub terms: Vec<Term>,
289}
290
291impl Atom {
292    /// Number of arguments.
293    pub fn arity(&self) -> usize {
294        self.terms.len()
295    }
296
297    /// Collect all named variables in this atom.
298    pub fn variables(&self) -> Vec<&str> {
299        self.terms.iter().flat_map(Term::variables).collect()
300    }
301}
302
303/// Epistemic operator on an atom.
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
305pub enum EpistemicOp {
306    /// Known/believed true in the selected epistemic mode.
307    Know,
308    /// Possible/consistent in the selected epistemic mode.
309    Possible,
310}
311
312/// Epistemic atom literal in a rule body.
313#[derive(Debug, Clone, PartialEq)]
314pub struct EpistemicLiteral {
315    /// Epistemic operator.
316    pub op: EpistemicOp,
317    /// Whether this epistemic literal is explicitly negated.
318    pub negated: bool,
319    /// Atom under the epistemic operator.
320    pub atom: Atom,
321}
322
323/// Comparison operator
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum CompOp {
326    /// Equal.
327    Eq,
328    /// Not equal.
329    Ne,
330    /// Less than.
331    Lt,
332    /// Less than or equal.
333    Le,
334    /// Greater than.
335    Gt,
336    /// Greater than or equal.
337    Ge,
338}
339
340/// A comparison expression
341#[derive(Debug, Clone, PartialEq)]
342pub struct Comparison {
343    /// Left operand.
344    pub left: Term,
345    /// Comparison operator.
346    pub op: CompOp,
347    /// Right operand.
348    pub right: Term,
349}
350
351/// A finite univ expression (`Term =.. Parts`) in a rule body.
352#[derive(Debug, Clone, PartialEq)]
353pub struct Univ {
354    /// Term side of the univ relation.
355    pub term: Term,
356    /// Parts-list side of the univ relation.
357    pub parts: Term,
358}
359
360/// A literal in the body of a rule
361#[derive(Debug, Clone, PartialEq)]
362pub enum BodyLiteral {
363    /// Positive atom.
364    Positive(Atom),
365    /// Negated atom (`not p(...)`).
366    Negated(Atom),
367    /// Epistemic atom (`know p(...)`, `possible p(...)`, or negated form).
368    Epistemic(EpistemicLiteral),
369    /// Arithmetic comparison (e.g. `X < Y`).
370    Comparison(Comparison),
371    /// Is-expression binding (e.g. `Z is X + Y`).
372    IsExpr(IsExpr),
373    /// Finite univ relation (`Term =.. Parts`).
374    Univ(Univ),
375}
376
377impl BodyLiteral {
378    /// Returns true if this is a positive literal.
379    pub fn is_positive(&self) -> bool {
380        matches!(self, BodyLiteral::Positive(_))
381    }
382
383    /// Returns true if this is a negated literal.
384    pub fn is_negated(&self) -> bool {
385        matches!(self, BodyLiteral::Negated(_))
386    }
387
388    /// Returns the atom if this is a positive or negated literal.
389    pub fn atom(&self) -> Option<&Atom> {
390        match self {
391            BodyLiteral::Positive(a) | BodyLiteral::Negated(a) => Some(a),
392            BodyLiteral::Epistemic(lit) => Some(&lit.atom),
393            BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => None,
394        }
395    }
396
397    /// Collect all named variables referenced by this literal.
398    pub fn variables(&self) -> Vec<&str> {
399        match self {
400            BodyLiteral::Positive(a) | BodyLiteral::Negated(a) => a.variables(),
401            BodyLiteral::Epistemic(lit) => lit.atom.variables(),
402            BodyLiteral::Comparison(c) => {
403                let mut vars = vec![];
404                vars.extend(c.left.variables());
405                vars.extend(c.right.variables());
406                vars
407            }
408            BodyLiteral::IsExpr(is_expr) => {
409                let mut vars = is_expr.expr.variables();
410                vars.push(is_expr.target.as_str());
411                vars
412            }
413            BodyLiteral::Univ(univ) => {
414                let mut vars = univ.term.variables();
415                vars.extend(univ.parts.variables());
416                vars
417            }
418        }
419    }
420}
421
422/// A rule (head :- body)
423#[derive(Debug, Clone, PartialEq)]
424pub struct Rule {
425    /// Head atom of the rule.
426    pub head: Atom,
427    /// Body literals (empty for facts).
428    pub body: Vec<BodyLiteral>,
429}
430
431impl Rule {
432    /// Returns true if this rule is a ground fact (empty body).
433    pub fn is_fact(&self) -> bool {
434        self.body.is_empty()
435    }
436
437    /// Returns true if any body literal is negated.
438    pub fn has_negation(&self) -> bool {
439        self.body.iter().any(|l| l.is_negated())
440    }
441
442    /// Returns true if the head contains an aggregate term.
443    pub fn has_aggregation(&self) -> bool {
444        self.head
445            .terms
446            .iter()
447            .any(|t| matches!(t, Term::Aggregate(_)))
448    }
449
450    /// Collect predicate names from the body.
451    pub fn body_predicates(&self) -> Vec<&str> {
452        self.body
453            .iter()
454            .filter_map(|l| l.atom().map(|a| a.predicate.as_str()))
455            .collect()
456    }
457
458    /// Collect named variables from the head.
459    pub fn head_variables(&self) -> Vec<&str> {
460        self.head.variables()
461    }
462
463    /// Collect all named variables from the body.
464    pub fn body_variables(&self) -> Vec<&str> {
465        self.body.iter().flat_map(|l| l.variables()).collect()
466    }
467
468    /// Infer a head variable's type from ordinary body atoms in source order.
469    ///
470    /// The caller supplies a column lookup so each compilation context can use
471    /// its own predicate identity and decide whether negated atoms provide type
472    /// evidence. The first known type from an accepted body occurrence is
473    /// returned; other literal kinds are not considered here.
474    pub(crate) fn inferred_head_variable_type<F>(
475        &self,
476        variable: &str,
477        mut column_type: F,
478    ) -> Option<ScalarType>
479    where
480        F: FnMut(&Atom, usize, bool) -> Option<ScalarType>,
481    {
482        for literal in &self.body {
483            let (atom, negated) = match literal {
484                BodyLiteral::Positive(atom) => (atom, false),
485                BodyLiteral::Negated(atom) => (atom, true),
486                BodyLiteral::Epistemic(_)
487                | BodyLiteral::Comparison(_)
488                | BodyLiteral::IsExpr(_)
489                | BodyLiteral::Univ(_) => continue,
490            };
491            for (index, term) in atom.terms.iter().enumerate() {
492                if matches!(term, Term::Variable(name) if name == variable) {
493                    if let Some(typ) = column_type(atom, index, negated) {
494                        return Some(typ);
495                    }
496                }
497            }
498        }
499        None
500    }
501}
502
503/// A constraint (:- body)
504#[derive(Debug, Clone, PartialEq)]
505pub struct Constraint {
506    /// Stable index of this constraint in the complete authored program.
507    pub authored_index: Option<usize>,
508    /// Body literals whose conjunction must never be satisfiable.
509    pub body: Vec<BodyLiteral>,
510}
511
512impl Constraint {
513    /// Return the authored identity required by prepared compilation paths.
514    pub fn require_authored_index(&self) -> Result<usize> {
515        self.authored_index.ok_or_else(|| {
516            XlogError::Compilation(
517                "prepared constraint compilation requires authored identities".to_string(),
518            )
519        })
520    }
521}
522
523/// A query (`?- atom.`)
524#[derive(Debug, Clone, PartialEq)]
525pub struct Query {
526    /// Query atom.
527    pub atom: Atom,
528}
529
530/// Probabilistic engine selection.
531#[derive(Debug, Clone, Copy, PartialEq, Eq)]
532pub enum ProbEngine {
533    /// Exact inference via d-DNNF compilation.
534    ExactDdnnf,
535    /// Approximate inference via Monte Carlo sampling.
536    Mc,
537}
538
539/// Probabilistic compilation caching.
540#[derive(Debug, Clone, Copy, PartialEq, Eq)]
541pub enum ProbCache {
542    /// Enable circuit caching.
543    On,
544    /// Disable circuit caching.
545    Off,
546}
547
548/// Epistemic semantics mode.
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub enum EpistemicMode {
551    /// Gelfond-1991-style compatibility semantics, selected by `g91`.
552    G91,
553    /// Founded Autoepistemic Equilibrium Logic.
554    Faeel,
555}
556
557/// Monte Carlo sampling method selection.
558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum ProbMethod {
560    /// Rejection sampling.
561    Rejection,
562    /// Forceable evidence clamping.
563    EvidenceClamping,
564}
565
566/// Magic-set rewrite mode for bound recursive deterministic queries.
567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568pub enum MagicSetsMode {
569    /// Apply the rewrite when the compiler can prove the supported safe subset.
570    Auto,
571    /// Require the rewrite and fail with a typed diagnostic if it is unsafe.
572    On,
573    /// Disable magic-set rewriting.
574    Off,
575}
576
577/// Compilation/evaluation directives (e.g., `#pragma ...`).
578#[derive(Debug, Clone, Default, PartialEq)]
579pub struct Directives {
580    /// Override for the probabilistic inference engine.
581    pub prob_engine: Option<ProbEngine>,
582    /// Override for circuit caching.
583    pub prob_cache: Option<ProbCache>,
584    /// Monte Carlo sample count.
585    pub prob_samples: Option<usize>,
586    /// Monte Carlo deterministic RNG seed.
587    pub prob_seed: Option<u64>,
588    /// Monte Carlo confidence level.
589    pub prob_confidence: Option<f64>,
590    /// Monte Carlo sampling method.
591    pub prob_method: Option<ProbMethod>,
592    /// Maximum nonmonotone MC iterations.
593    pub prob_max_nonmonotone_iterations: Option<usize>,
594    /// Maximum UDF recursion depth.
595    pub max_recursion_depth: Option<u32>,
596    /// Override for epistemic semantics.
597    pub epistemic_mode: Option<EpistemicMode>,
598    /// Magic-set rewrite mode.
599    pub magic_sets: Option<MagicSetsMode>,
600}
601
602impl Directives {
603    /// Names of the pragmas explicitly set on this program, using the
604    /// source spelling (`#pragma <name> = ...`), in declaration-struct
605    /// order.
606    pub fn set_pragma_names(&self) -> Vec<&'static str> {
607        // Destructure so adding a `Directives` field without extending this
608        // list is a compile error, not a pragma that silently vanishes from
609        // the ignored-import warnings.
610        let Directives {
611            prob_engine,
612            prob_cache,
613            prob_samples,
614            prob_seed,
615            prob_confidence,
616            prob_method,
617            prob_max_nonmonotone_iterations,
618            max_recursion_depth,
619            epistemic_mode,
620            magic_sets,
621        } = self;
622        let mut names = Vec::new();
623        if prob_engine.is_some() {
624            names.push("prob_engine");
625        }
626        if prob_cache.is_some() {
627            names.push("prob_cache");
628        }
629        if prob_samples.is_some() {
630            names.push("prob_samples");
631        }
632        if prob_seed.is_some() {
633            names.push("prob_seed");
634        }
635        if prob_confidence.is_some() {
636            names.push("prob_confidence");
637        }
638        if prob_method.is_some() {
639            names.push("prob_method");
640        }
641        if prob_max_nonmonotone_iterations.is_some() {
642            names.push("prob_max_nonmonotone_iterations");
643        }
644        if max_recursion_depth.is_some() {
645            names.push("max_recursion_depth");
646        }
647        if epistemic_mode.is_some() {
648            names.push("epistemic_mode");
649        }
650        if magic_sets.is_some() {
651            names.push("magic_sets");
652        }
653        names
654    }
655
656    /// Return the configured prob engine, defaulting to ExactDdnnf.
657    pub fn prob_engine_or_default(&self) -> ProbEngine {
658        self.prob_engine.unwrap_or(ProbEngine::ExactDdnnf)
659    }
660
661    /// Return the configured max recursion depth, defaulting to 1000.
662    pub fn max_recursion_depth_or_default(&self) -> u32 {
663        self.max_recursion_depth.unwrap_or(1000)
664    }
665
666    /// Return the configured epistemic mode, defaulting to FAEEL.
667    pub fn epistemic_mode_or_default(&self) -> EpistemicMode {
668        self.epistemic_mode.unwrap_or(EpistemicMode::Faeel)
669    }
670
671    /// Return the configured MC sample count, defaulting to 10000.
672    pub fn prob_samples_or_default(&self) -> usize {
673        self.prob_samples.unwrap_or(10000)
674    }
675
676    /// Return the configured MC seed, defaulting to 0.
677    pub fn prob_seed_or_default(&self) -> u64 {
678        self.prob_seed.unwrap_or(0)
679    }
680
681    /// Return the configured MC confidence, defaulting to 0.95.
682    pub fn prob_confidence_or_default(&self) -> f64 {
683        self.prob_confidence.unwrap_or(0.95)
684    }
685
686    /// Return the configured nonmonotone MC iteration cap, defaulting to 1024.
687    pub fn prob_max_nonmonotone_iterations_or_default(&self) -> usize {
688        self.prob_max_nonmonotone_iterations.unwrap_or(1024)
689    }
690}
691
692/// A probabilistic fact (`p::atom.`)
693#[derive(Debug, Clone, PartialEq)]
694pub struct ProbFact {
695    /// Probability weight.
696    pub prob: f64,
697    /// Ground atom.
698    pub atom: Atom,
699}
700
701/// Neural predicate declaration
702///
703/// Neural predicates connect neural networks to probabilistic logic.
704/// Syntax: `nn(network, [inputs], output, [labels]) :: pred(args).`
705///
706/// The neural network produces probability distributions over labels,
707/// which become probabilistic facts in the logic program.
708///
709/// # Examples
710/// ```text
711/// nn(mnist_net, [X], Y, [0,1,2,3,4,5,6,7,8,9]) :: digit(X, Y).
712/// nn(encoder, [Text], Embedding) :: encode(Text, Embedding).
713/// ```
714#[derive(Debug, Clone, PartialEq)]
715pub struct NeuralPredDecl {
716    /// Name of the registered neural network
717    pub network: String,
718    /// Input variable names (bind to tensor sources)
719    pub inputs: Vec<String>,
720    /// Output variable name
721    pub output: String,
722    /// Optional classification labels (for classification networks)
723    /// If None, the network produces embeddings
724    pub labels: Option<Vec<NeuralLabel>>,
725    /// The predicate this neural network defines
726    pub predicate: Atom,
727}
728
729/// A label in a neural predicate classification
730///
731/// Labels can be integers or symbols (identifiers).
732#[derive(Debug, Clone, PartialEq)]
733pub enum NeuralLabel {
734    /// Integer label value.
735    Integer(i64),
736    /// Symbolic (string) label value.
737    Symbol(String),
738}
739
740/// A learnable rule template parameterized by a named tensor mask.
741/// Used for differentiable ILP — the mask selects which (body1, body2, head)
742/// combinations are active during execution.
743#[derive(Debug, Clone)]
744pub struct LearnableRule {
745    /// Name of the tensor mask controlling rule activation.
746    pub mask_name: String,
747    /// Head atom of the rule template.
748    pub head: Atom,
749    /// Body literals of the rule template.
750    pub body: Vec<BodyLiteral>,
751}
752
753/// Annotated disjunction (`p1::a1; p2::a2.`)
754#[derive(Debug, Clone, PartialEq)]
755pub struct AnnotatedDisjunction {
756    /// Disjunctive choices with their probability weights.
757    pub choices: Vec<ProbFact>,
758}
759
760/// Evidence statement (`evidence(atom, true|false).`)
761#[derive(Debug, Clone, PartialEq)]
762pub struct Evidence {
763    /// The observed atom.
764    pub atom: Atom,
765    /// Whether the atom is observed true or false.
766    pub value: bool,
767}
768
769/// Probabilistic query statement (`query(atom).`)
770#[derive(Debug, Clone, PartialEq)]
771pub struct ProbQuery {
772    /// The atom whose probability is being queried.
773    pub atom: Atom,
774}
775
776/// Import statement: use module. or use module::{pred1, pred2}.
777#[derive(Debug, Clone, PartialEq)]
778pub struct UseDecl {
779    /// Module path segments, e.g., ["utils", "math"]
780    pub module_path: Vec<String>,
781    /// Specific imports (None = import all public)
782    pub imports: Option<Vec<String>>,
783}
784
785/// Domain declaration
786#[derive(Debug, Clone, PartialEq)]
787pub struct DomainDecl {
788    /// Domain name.
789    pub name: String,
790    /// Scalar type for the domain.
791    pub typ: ScalarType,
792}
793
794/// A type reference in source declarations.
795#[derive(Debug, Clone, PartialEq, Eq)]
796pub enum TypeRef {
797    /// Built-in scalar type.
798    Scalar(ScalarType),
799    /// Domain alias resolved during semantic analysis.
800    Domain(String),
801    /// Finite homogeneous list type.
802    List(Box<TypeRef>),
803    /// Finite term type.
804    Term,
805    /// Finite compound term type.
806    Compound,
807    /// Static predicate reference type.
808    PredRef,
809}
810
811/// Predicate declaration column.
812#[derive(Debug, Clone, PartialEq, Eq)]
813pub struct PredColumn {
814    /// Optional source-level column name.
815    pub name: Option<String>,
816    /// Column type reference.
817    pub typ: TypeRef,
818}
819
820/// Predicate declaration
821#[derive(Debug, Clone, PartialEq)]
822pub struct PredDecl {
823    /// Predicate name.
824    pub name: String,
825    /// Column types.
826    pub types: Vec<TypeRef>,
827    /// Declared columns, including optional names.
828    pub columns: Vec<PredColumn>,
829    /// Whether this predicate is module-private.
830    pub is_private: bool,
831}
832
833impl PredDecl {
834    /// Return the effective declared columns, including optional source names.
835    ///
836    /// Parsed declarations populate `columns`; callers constructing the AST
837    /// directly may supply the equivalent unnamed schema through `types`.
838    pub fn schema_columns(&self) -> Vec<PredColumn> {
839        if self.columns.is_empty() {
840            self.types
841                .iter()
842                .cloned()
843                .map(|typ| PredColumn { name: None, typ })
844                .collect()
845        } else {
846            self.columns.clone()
847        }
848    }
849
850    /// Return the arity of the effective declared schema.
851    pub fn arity(&self) -> usize {
852        if self.columns.is_empty() {
853            self.types.len()
854        } else {
855            self.columns.len()
856        }
857    }
858}
859
860/// Function parameter with optional type annotation
861#[derive(Debug, Clone, PartialEq)]
862pub struct FuncParam {
863    /// Parameter name.
864    pub name: String,
865    /// Optional type annotation.
866    pub typ: Option<ScalarType>,
867}
868
869/// Conditional expression: if X < 0 then A else B
870#[derive(Debug, Clone, PartialEq)]
871pub struct CondExpr {
872    /// Left side of condition
873    pub cond_left: ArithExpr,
874    /// Comparison operator
875    pub cond_op: CompOp,
876    /// Right side of condition
877    pub cond_right: ArithExpr,
878    /// Value if condition is true
879    pub then_branch: Box<FuncBody>,
880    /// Value if condition is false
881    pub else_branch: Box<FuncBody>,
882}
883
884/// Function body - arithmetic, conditional, or predicate-based
885#[derive(Debug, Clone, PartialEq)]
886pub enum FuncBody {
887    /// Pure arithmetic expression: X * X
888    Arithmetic(ArithExpr),
889    /// Conditional expression: if X < 0 then ...
890    Conditional(CondExpr),
891    /// Predicate-based: P :- parent(X, P)
892    Predicate {
893        /// Result variable
894        result: String,
895        /// Body literals
896        body: Vec<BodyLiteral>,
897    },
898}
899
900/// User-defined function
901#[derive(Debug, Clone, PartialEq)]
902pub struct FuncDef {
903    /// Function name
904    pub name: String,
905    /// Parameters
906    pub params: Vec<FuncParam>,
907    /// Optional return type annotation
908    pub return_type: Option<ScalarType>,
909    /// Function body
910    pub body: FuncBody,
911    /// Is this function private?
912    pub is_private: bool,
913}
914
915/// A complete XLOG program
916#[derive(Debug, Clone, Default)]
917pub struct Program {
918    /// Import declarations (`use ...`).
919    pub imports: Vec<UseDecl>,
920    /// User-defined function definitions.
921    pub functions: Vec<FuncDef>,
922    /// Domain declarations.
923    pub domains: Vec<DomainDecl>,
924    /// Predicate type declarations.
925    pub predicates: Vec<PredDecl>,
926    /// Rules and facts.
927    pub rules: Vec<Rule>,
928    /// Integrity constraints (`:- ...`).
929    pub constraints: Vec<Constraint>,
930    /// Number of integrity constraints in the authored source program.
931    ///
932    /// This is carried through transforms so sparse constraint subsets can
933    /// validate their authored identities without being locally re-enumerated.
934    pub authored_constraint_source_bound: Option<usize>,
935    /// Queries (`?- ...`).
936    pub queries: Vec<Query>,
937    /// Probabilistic facts (`p::atom.`).
938    pub prob_facts: Vec<ProbFact>,
939    /// Annotated disjunctions.
940    pub annotated_disjunctions: Vec<AnnotatedDisjunction>,
941    /// Evidence statements.
942    pub evidence: Vec<Evidence>,
943    /// Probabilistic queries (`query(atom).`).
944    pub prob_queries: Vec<ProbQuery>,
945    /// Neural predicate declarations.
946    pub neural_predicates: Vec<NeuralPredDecl>,
947    /// Learnable rule templates (ILP).
948    pub learnable_rules: Vec<LearnableRule>,
949    /// Compilation directives.
950    pub directives: Directives,
951}
952
953impl Program {
954    /// Create an empty program.
955    pub fn new() -> Self {
956        Self::default()
957    }
958
959    /// Assign or validate stable authored identities before any program transforms.
960    pub fn prepare_authored_constraint_identity(
961        &mut self,
962        authored_source_constraint_count: usize,
963    ) -> Result<()> {
964        if let Some(existing_bound) = self.authored_constraint_source_bound {
965            if existing_bound != authored_source_constraint_count {
966                return Err(XlogError::Compilation(format!(
967                    "authored constraint source bound {existing_bound} does not match requested bound {authored_source_constraint_count}"
968                )));
969            }
970        }
971
972        let assigned = self
973            .constraints
974            .iter()
975            .filter(|constraint| constraint.authored_index.is_some())
976            .count();
977
978        if assigned == 0 {
979            if self.constraints.len() != authored_source_constraint_count {
980                return Err(XlogError::Compilation(format!(
981                    "unassigned constraint count {} does not match authored source bound {}",
982                    self.constraints.len(),
983                    authored_source_constraint_count
984                )));
985            }
986            for (authored_index, constraint) in self.constraints.iter_mut().enumerate() {
987                constraint.authored_index = Some(authored_index);
988            }
989            self.authored_constraint_source_bound = Some(authored_source_constraint_count);
990            return Ok(());
991        }
992
993        if assigned != self.constraints.len() {
994            return Err(XlogError::Compilation(
995                "mixed assigned and unassigned authored constraint identities".to_string(),
996            ));
997        }
998
999        let mut seen = std::collections::HashSet::with_capacity(self.constraints.len());
1000        for constraint in &self.constraints {
1001            let authored_index = constraint
1002                .authored_index
1003                .expect("all constraint identities were checked as assigned");
1004            if authored_index >= authored_source_constraint_count {
1005                return Err(XlogError::Compilation(format!(
1006                    "authored constraint index {authored_index} is outside source bound {authored_source_constraint_count}"
1007                )));
1008            }
1009            if !seen.insert(authored_index) {
1010                return Err(XlogError::Compilation(format!(
1011                    "duplicate authored constraint index {authored_index}"
1012                )));
1013            }
1014        }
1015        self.authored_constraint_source_bound = Some(authored_source_constraint_count);
1016        Ok(())
1017    }
1018
1019    /// Assign dense authored identities at the outer full-program boundary.
1020    pub fn prepare_authored_constraint_identity_at_root(&mut self) -> Result<()> {
1021        let authored_source_constraint_count = self.constraints.len();
1022        self.prepare_authored_constraint_identity(authored_source_constraint_count)
1023    }
1024
1025    /// Validate identities on a program already prepared at the outer boundary.
1026    pub fn validate_prepared_authored_constraint_identity(&self) -> Result<()> {
1027        if self.constraints.is_empty() && self.authored_constraint_source_bound.is_none() {
1028            return Ok(());
1029        }
1030        let authored_source_constraint_count =
1031            self.authored_constraint_source_bound.ok_or_else(|| {
1032                XlogError::Compilation(
1033                "prepared constraint compilation requires authored identities and a source bound"
1034                    .to_string(),
1035            )
1036            })?;
1037        if self
1038            .constraints
1039            .iter()
1040            .any(|constraint| constraint.authored_index.is_none())
1041        {
1042            return Err(XlogError::Compilation(
1043                "prepared constraint compilation requires authored identities".to_string(),
1044            ));
1045        }
1046
1047        let mut seen = std::collections::HashSet::with_capacity(self.constraints.len());
1048        for constraint in &self.constraints {
1049            let authored_index = constraint
1050                .authored_index
1051                .expect("all prepared constraint identities were checked as assigned");
1052            if authored_index >= authored_source_constraint_count {
1053                return Err(XlogError::Compilation(format!(
1054                    "authored constraint index {authored_index} is outside source bound {authored_source_constraint_count}"
1055                )));
1056            }
1057            if !seen.insert(authored_index) {
1058                return Err(XlogError::Compilation(format!(
1059                    "duplicate authored constraint index {authored_index}"
1060                )));
1061            }
1062        }
1063        Ok(())
1064    }
1065
1066    /// Iterate over ground facts (rules with empty bodies).
1067    pub fn facts(&self) -> impl Iterator<Item = &Rule> {
1068        self.rules.iter().filter(|r| r.is_fact())
1069    }
1070
1071    /// Iterate over proper rules (non-fact rules with bodies).
1072    pub fn proper_rules(&self) -> impl Iterator<Item = &Rule> {
1073        self.rules.iter().filter(|r| !r.is_fact())
1074    }
1075
1076    /// Collect the set of predicate names defined (appearing as rule heads).
1077    pub fn defined_predicates(&self) -> Vec<&str> {
1078        self.rules
1079            .iter()
1080            .map(|r| r.head.predicate.as_str())
1081            .collect::<std::collections::HashSet<_>>()
1082            .into_iter()
1083            .collect()
1084    }
1085
1086    /// Returns true if this program uses probabilistic features.
1087    pub fn is_probabilistic_profile(&self) -> bool {
1088        !self.prob_facts.is_empty()
1089            || !self.annotated_disjunctions.is_empty()
1090            || !self.evidence.is_empty()
1091            || !self.prob_queries.is_empty()
1092            || self.directives.prob_engine.is_some()
1093            || self.directives.prob_cache.is_some()
1094            || self.directives.prob_samples.is_some()
1095            || self.directives.prob_seed.is_some()
1096            || self.directives.prob_confidence.is_some()
1097            || self.directives.prob_method.is_some()
1098            || self.directives.prob_max_nonmonotone_iterations.is_some()
1099    }
1100
1101    /// Return the probabilistic engine (from directives, or the default).
1102    pub fn prob_engine(&self) -> ProbEngine {
1103        self.directives.prob_engine_or_default()
1104    }
1105
1106    /// Merge another program's exports into this program.
1107    /// Used for importing modules - adds predicates, functions, rules from the imported module.
1108    /// Only merges public items (private items are not exported).
1109    ///
1110    /// # Arguments
1111    /// * `other` - The program to merge from
1112    /// * `imported_items` - Optional set of specific items to import. If None, imports all public items.
1113    pub fn merge_from(
1114        &mut self,
1115        other: &Program,
1116        imported_items: Option<&std::collections::HashSet<String>>,
1117    ) {
1118        use std::collections::HashSet;
1119
1120        // Track which predicates are private in the source
1121        let private_preds: HashSet<&str> = other
1122            .predicates
1123            .iter()
1124            .filter(|p| p.is_private)
1125            .map(|p| p.name.as_str())
1126            .collect();
1127
1128        let _private_funcs: HashSet<&str> = other
1129            .functions
1130            .iter()
1131            .filter(|f| f.is_private)
1132            .map(|f| f.name.as_str())
1133            .collect();
1134
1135        // Merge predicate declarations (only public ones)
1136        for pred in &other.predicates {
1137            if pred.is_private {
1138                continue;
1139            }
1140            // Check if this is in the import list (if specified)
1141            if let Some(items) = imported_items {
1142                if !items.contains(&pred.name) {
1143                    continue;
1144                }
1145            }
1146            // Avoid duplicate declarations
1147            if !self.predicates.iter().any(|p| p.name == pred.name) {
1148                self.predicates.push(pred.clone());
1149            }
1150        }
1151
1152        // Merge functions (only public ones)
1153        for func in &other.functions {
1154            if func.is_private {
1155                continue;
1156            }
1157            if let Some(items) = imported_items {
1158                if !items.contains(&func.name) {
1159                    continue;
1160                }
1161            }
1162            // Avoid duplicate functions
1163            if !self.functions.iter().any(|f| f.name == func.name) {
1164                self.functions.push(func.clone());
1165            }
1166        }
1167
1168        // Merge rules (facts and rules for public predicates)
1169        for rule in &other.rules {
1170            // Skip if the head predicate is private
1171            if private_preds.contains(rule.head.predicate.as_str()) {
1172                continue;
1173            }
1174            // Check import list for facts/rules
1175            if let Some(items) = imported_items {
1176                if !items.contains(&rule.head.predicate) {
1177                    continue;
1178                }
1179            }
1180            if !self.rules.iter().any(|existing| existing == rule) {
1181                self.rules.push(rule.clone());
1182            }
1183        }
1184
1185        // Merge domains
1186        for domain in &other.domains {
1187            if !self.domains.iter().any(|d| d.name == domain.name) {
1188                self.domains.push(domain.clone());
1189            }
1190        }
1191    }
1192}
1193
1194#[cfg(test)]
1195mod tests {
1196    use super::*;
1197
1198    #[test]
1199    fn test_directives_set_pragma_names() {
1200        let mut directives = Directives::default();
1201        assert!(directives.set_pragma_names().is_empty());
1202
1203        directives.prob_seed = Some(7);
1204        directives.magic_sets = Some(MagicSetsMode::Auto);
1205        assert_eq!(
1206            directives.set_pragma_names(),
1207            vec!["prob_seed", "magic_sets"]
1208        );
1209    }
1210
1211    #[test]
1212    fn test_directives_set_pragma_names_covers_all_ten_pragmas() {
1213        let directives = Directives {
1214            prob_engine: Some(ProbEngine::Mc),
1215            prob_cache: Some(ProbCache::On),
1216            prob_samples: Some(20000),
1217            prob_seed: Some(7),
1218            prob_confidence: Some(0.9),
1219            prob_method: Some(ProbMethod::Rejection),
1220            prob_max_nonmonotone_iterations: Some(64),
1221            max_recursion_depth: Some(100),
1222            epistemic_mode: Some(EpistemicMode::G91),
1223            magic_sets: Some(MagicSetsMode::Auto),
1224        };
1225        assert_eq!(
1226            directives.set_pragma_names(),
1227            vec![
1228                "prob_engine",
1229                "prob_cache",
1230                "prob_samples",
1231                "prob_seed",
1232                "prob_confidence",
1233                "prob_method",
1234                "prob_max_nonmonotone_iterations",
1235                "max_recursion_depth",
1236                "epistemic_mode",
1237                "magic_sets",
1238            ]
1239        );
1240    }
1241
1242    #[test]
1243    fn test_term_variable() {
1244        let term = Term::Variable("X".to_string());
1245        assert!(term.is_variable());
1246        assert!(!term.is_constant());
1247    }
1248
1249    #[test]
1250    fn test_term_constant() {
1251        let term = Term::Integer(42);
1252        assert!(!term.is_variable());
1253        assert!(term.is_constant());
1254    }
1255
1256    #[test]
1257    fn test_atom_arity() {
1258        let atom = Atom {
1259            predicate: "edge".to_string(),
1260            terms: vec![Term::Integer(1), Term::Integer(2)],
1261        };
1262        assert_eq!(atom.arity(), 2);
1263    }
1264
1265    #[test]
1266    fn test_atom_variables() {
1267        let atom = Atom {
1268            predicate: "edge".to_string(),
1269            terms: vec![Term::Variable("X".to_string()), Term::Integer(2)],
1270        };
1271        let vars = atom.variables();
1272        assert_eq!(vars, vec!["X"]);
1273    }
1274
1275    #[test]
1276    fn predicate_declaration_uses_its_effective_schema_representation() {
1277        let types_only = PredDecl {
1278            name: "types_only".to_string(),
1279            types: vec![TypeRef::Scalar(ScalarType::U64)],
1280            columns: vec![],
1281            is_private: false,
1282        };
1283        assert_eq!(types_only.arity(), 1);
1284        assert_eq!(
1285            types_only.schema_columns(),
1286            vec![PredColumn {
1287                name: None,
1288                typ: TypeRef::Scalar(ScalarType::U64),
1289            }]
1290        );
1291
1292        let columns_only = PredDecl {
1293            name: "columns_only".to_string(),
1294            types: vec![],
1295            columns: vec![PredColumn {
1296                name: Some("value".to_string()),
1297                typ: TypeRef::Scalar(ScalarType::Symbol),
1298            }],
1299            is_private: false,
1300        };
1301        assert_eq!(columns_only.arity(), 1);
1302        assert_eq!(
1303            columns_only.schema_columns(),
1304            vec![PredColumn {
1305                name: Some("value".to_string()),
1306                typ: TypeRef::Scalar(ScalarType::Symbol),
1307            }]
1308        );
1309    }
1310
1311    #[test]
1312    fn test_rule_is_fact() {
1313        let fact = Rule {
1314            head: Atom {
1315                predicate: "edge".to_string(),
1316                terms: vec![Term::Integer(1), Term::Integer(2)],
1317            },
1318            body: vec![],
1319        };
1320        assert!(fact.is_fact());
1321    }
1322
1323    #[test]
1324    fn test_rule_has_negation() {
1325        let rule = Rule {
1326            head: Atom {
1327                predicate: "isolated".to_string(),
1328                terms: vec![Term::Variable("X".to_string())],
1329            },
1330            body: vec![
1331                BodyLiteral::Positive(Atom {
1332                    predicate: "node".to_string(),
1333                    terms: vec![Term::Variable("X".to_string())],
1334                }),
1335                BodyLiteral::Negated(Atom {
1336                    predicate: "edge".to_string(),
1337                    terms: vec![
1338                        Term::Variable("X".to_string()),
1339                        Term::Variable("Y".to_string()),
1340                    ],
1341                }),
1342            ],
1343        };
1344        assert!(rule.has_negation());
1345    }
1346
1347    #[test]
1348    fn test_program_facts() {
1349        let mut program = Program::new();
1350        program.rules.push(Rule {
1351            head: Atom {
1352                predicate: "edge".to_string(),
1353                terms: vec![Term::Integer(1), Term::Integer(2)],
1354            },
1355            body: vec![],
1356        });
1357        program.rules.push(Rule {
1358            head: Atom {
1359                predicate: "reach".to_string(),
1360                terms: vec![
1361                    Term::Variable("X".to_string()),
1362                    Term::Variable("Y".to_string()),
1363                ],
1364            },
1365            body: vec![BodyLiteral::Positive(Atom {
1366                predicate: "edge".to_string(),
1367                terms: vec![
1368                    Term::Variable("X".to_string()),
1369                    Term::Variable("Y".to_string()),
1370                ],
1371            })],
1372        });
1373        assert_eq!(program.facts().count(), 1);
1374        assert_eq!(program.proper_rules().count(), 1);
1375    }
1376
1377    #[test]
1378    fn test_arith_expr_structure() {
1379        let expr = ArithExpr::Add(
1380            Box::new(ArithExpr::Variable("X".to_string())),
1381            Box::new(ArithExpr::Integer(1)),
1382        );
1383        assert!(matches!(expr, ArithExpr::Add(_, _)));
1384    }
1385
1386    #[test]
1387    fn test_is_expr_structure() {
1388        let is_expr = IsExpr {
1389            target: "Z".to_string(),
1390            expr: ArithExpr::Variable("Y".to_string()),
1391        };
1392        assert_eq!(is_expr.target, "Z");
1393    }
1394}