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
953#[derive(Debug, Clone, Default, PartialEq, Eq)]
954pub(crate) struct ProgramMergeReport {
955    pub domains: Vec<usize>,
956    pub predicates: Vec<usize>,
957    pub functions: Vec<usize>,
958    pub rules: Vec<usize>,
959}
960
961impl Program {
962    /// Create an empty program.
963    pub fn new() -> Self {
964        Self::default()
965    }
966
967    /// Assign or validate stable authored identities before any program transforms.
968    pub fn prepare_authored_constraint_identity(
969        &mut self,
970        authored_source_constraint_count: usize,
971    ) -> Result<()> {
972        if let Some(existing_bound) = self.authored_constraint_source_bound {
973            if existing_bound != authored_source_constraint_count {
974                return Err(XlogError::Compilation(format!(
975                    "authored constraint source bound {existing_bound} does not match requested bound {authored_source_constraint_count}"
976                )));
977            }
978        }
979
980        let assigned = self
981            .constraints
982            .iter()
983            .filter(|constraint| constraint.authored_index.is_some())
984            .count();
985
986        if assigned == 0 {
987            if self.constraints.len() != authored_source_constraint_count {
988                return Err(XlogError::Compilation(format!(
989                    "unassigned constraint count {} does not match authored source bound {}",
990                    self.constraints.len(),
991                    authored_source_constraint_count
992                )));
993            }
994            for (authored_index, constraint) in self.constraints.iter_mut().enumerate() {
995                constraint.authored_index = Some(authored_index);
996            }
997            self.authored_constraint_source_bound = Some(authored_source_constraint_count);
998            return Ok(());
999        }
1000
1001        if assigned != self.constraints.len() {
1002            return Err(XlogError::Compilation(
1003                "mixed assigned and unassigned authored constraint identities".to_string(),
1004            ));
1005        }
1006
1007        let mut seen = std::collections::HashSet::with_capacity(self.constraints.len());
1008        for constraint in &self.constraints {
1009            let authored_index = constraint
1010                .authored_index
1011                .expect("all constraint identities were checked as assigned");
1012            if authored_index >= authored_source_constraint_count {
1013                return Err(XlogError::Compilation(format!(
1014                    "authored constraint index {authored_index} is outside source bound {authored_source_constraint_count}"
1015                )));
1016            }
1017            if !seen.insert(authored_index) {
1018                return Err(XlogError::Compilation(format!(
1019                    "duplicate authored constraint index {authored_index}"
1020                )));
1021            }
1022        }
1023        self.authored_constraint_source_bound = Some(authored_source_constraint_count);
1024        Ok(())
1025    }
1026
1027    /// Assign dense authored identities at the outer full-program boundary.
1028    pub fn prepare_authored_constraint_identity_at_root(&mut self) -> Result<()> {
1029        let authored_source_constraint_count = self.constraints.len();
1030        self.prepare_authored_constraint_identity(authored_source_constraint_count)
1031    }
1032
1033    /// Validate identities on a program already prepared at the outer boundary.
1034    pub fn validate_prepared_authored_constraint_identity(&self) -> Result<()> {
1035        if self.constraints.is_empty() && self.authored_constraint_source_bound.is_none() {
1036            return Ok(());
1037        }
1038        let authored_source_constraint_count =
1039            self.authored_constraint_source_bound.ok_or_else(|| {
1040                XlogError::Compilation(
1041                "prepared constraint compilation requires authored identities and a source bound"
1042                    .to_string(),
1043            )
1044            })?;
1045        if self
1046            .constraints
1047            .iter()
1048            .any(|constraint| constraint.authored_index.is_none())
1049        {
1050            return Err(XlogError::Compilation(
1051                "prepared constraint compilation requires authored identities".to_string(),
1052            ));
1053        }
1054
1055        let mut seen = std::collections::HashSet::with_capacity(self.constraints.len());
1056        for constraint in &self.constraints {
1057            let authored_index = constraint
1058                .authored_index
1059                .expect("all prepared constraint identities were checked as assigned");
1060            if authored_index >= authored_source_constraint_count {
1061                return Err(XlogError::Compilation(format!(
1062                    "authored constraint index {authored_index} is outside source bound {authored_source_constraint_count}"
1063                )));
1064            }
1065            if !seen.insert(authored_index) {
1066                return Err(XlogError::Compilation(format!(
1067                    "duplicate authored constraint index {authored_index}"
1068                )));
1069            }
1070        }
1071        Ok(())
1072    }
1073
1074    /// Iterate over ground facts (rules with empty bodies).
1075    pub fn facts(&self) -> impl Iterator<Item = &Rule> {
1076        self.rules.iter().filter(|r| r.is_fact())
1077    }
1078
1079    /// Iterate over proper rules (non-fact rules with bodies).
1080    pub fn proper_rules(&self) -> impl Iterator<Item = &Rule> {
1081        self.rules.iter().filter(|r| !r.is_fact())
1082    }
1083
1084    /// Collect the set of predicate names defined (appearing as rule heads).
1085    pub fn defined_predicates(&self) -> Vec<&str> {
1086        self.rules
1087            .iter()
1088            .map(|r| r.head.predicate.as_str())
1089            .collect::<std::collections::HashSet<_>>()
1090            .into_iter()
1091            .collect()
1092    }
1093
1094    /// Returns true if this program uses probabilistic features.
1095    pub fn is_probabilistic_profile(&self) -> bool {
1096        !self.prob_facts.is_empty()
1097            || !self.annotated_disjunctions.is_empty()
1098            || !self.evidence.is_empty()
1099            || !self.prob_queries.is_empty()
1100            || self.directives.prob_engine.is_some()
1101            || self.directives.prob_cache.is_some()
1102            || self.directives.prob_samples.is_some()
1103            || self.directives.prob_seed.is_some()
1104            || self.directives.prob_confidence.is_some()
1105            || self.directives.prob_method.is_some()
1106            || self.directives.prob_max_nonmonotone_iterations.is_some()
1107    }
1108
1109    /// Return the probabilistic engine (from directives, or the default).
1110    pub fn prob_engine(&self) -> ProbEngine {
1111        self.directives.prob_engine_or_default()
1112    }
1113
1114    /// Merge another program's exports into this program.
1115    /// Used for importing modules - adds predicates, functions, rules from the imported module.
1116    /// Only merges public items (private items are not exported).
1117    ///
1118    /// # Arguments
1119    /// * `other` - The program to merge from
1120    /// * `imported_items` - Optional set of specific items to import. If None, imports all public items.
1121    pub fn merge_from(
1122        &mut self,
1123        other: &Program,
1124        imported_items: Option<&std::collections::HashSet<String>>,
1125    ) {
1126        self.merge_from_with_report(other, imported_items);
1127    }
1128
1129    pub(crate) fn merge_from_with_report(
1130        &mut self,
1131        other: &Program,
1132        imported_items: Option<&std::collections::HashSet<String>>,
1133    ) -> ProgramMergeReport {
1134        use std::collections::HashSet;
1135
1136        let mut report = ProgramMergeReport::default();
1137
1138        // Track which predicates are private in the source
1139        let private_preds: HashSet<&str> = other
1140            .predicates
1141            .iter()
1142            .filter(|p| p.is_private)
1143            .map(|p| p.name.as_str())
1144            .collect();
1145
1146        let _private_funcs: HashSet<&str> = other
1147            .functions
1148            .iter()
1149            .filter(|f| f.is_private)
1150            .map(|f| f.name.as_str())
1151            .collect();
1152
1153        // Merge predicate declarations (only public ones)
1154        for (source_index, pred) in other.predicates.iter().enumerate() {
1155            if pred.is_private {
1156                continue;
1157            }
1158            // Check if this is in the import list (if specified)
1159            if let Some(items) = imported_items {
1160                if !items.contains(&pred.name) {
1161                    continue;
1162                }
1163            }
1164            // Avoid duplicate declarations
1165            if !self.predicates.iter().any(|p| p.name == pred.name) {
1166                self.predicates.push(pred.clone());
1167                report.predicates.push(source_index);
1168            }
1169        }
1170
1171        // Merge functions (only public ones)
1172        for (source_index, func) in other.functions.iter().enumerate() {
1173            if func.is_private {
1174                continue;
1175            }
1176            if let Some(items) = imported_items {
1177                if !items.contains(&func.name) {
1178                    continue;
1179                }
1180            }
1181            // Avoid duplicate functions
1182            if !self.functions.iter().any(|f| f.name == func.name) {
1183                self.functions.push(func.clone());
1184                report.functions.push(source_index);
1185            }
1186        }
1187
1188        // Merge rules (facts and rules for public predicates)
1189        for (source_index, rule) in other.rules.iter().enumerate() {
1190            // Skip if the head predicate is private
1191            if private_preds.contains(rule.head.predicate.as_str()) {
1192                continue;
1193            }
1194            // Check import list for facts/rules
1195            if let Some(items) = imported_items {
1196                if !items.contains(&rule.head.predicate) {
1197                    continue;
1198                }
1199            }
1200            if !self.rules.iter().any(|existing| existing == rule) {
1201                self.rules.push(rule.clone());
1202                report.rules.push(source_index);
1203            }
1204        }
1205
1206        // Merge domains
1207        for (source_index, domain) in other.domains.iter().enumerate() {
1208            if !self.domains.iter().any(|d| d.name == domain.name) {
1209                self.domains.push(domain.clone());
1210                report.domains.push(source_index);
1211            }
1212        }
1213
1214        report
1215    }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220    use super::*;
1221
1222    #[test]
1223    fn test_directives_set_pragma_names() {
1224        let mut directives = Directives::default();
1225        assert!(directives.set_pragma_names().is_empty());
1226
1227        directives.prob_seed = Some(7);
1228        directives.magic_sets = Some(MagicSetsMode::Auto);
1229        assert_eq!(
1230            directives.set_pragma_names(),
1231            vec!["prob_seed", "magic_sets"]
1232        );
1233    }
1234
1235    #[test]
1236    fn test_directives_set_pragma_names_covers_all_ten_pragmas() {
1237        let directives = Directives {
1238            prob_engine: Some(ProbEngine::Mc),
1239            prob_cache: Some(ProbCache::On),
1240            prob_samples: Some(20000),
1241            prob_seed: Some(7),
1242            prob_confidence: Some(0.9),
1243            prob_method: Some(ProbMethod::Rejection),
1244            prob_max_nonmonotone_iterations: Some(64),
1245            max_recursion_depth: Some(100),
1246            epistemic_mode: Some(EpistemicMode::G91),
1247            magic_sets: Some(MagicSetsMode::Auto),
1248        };
1249        assert_eq!(
1250            directives.set_pragma_names(),
1251            vec![
1252                "prob_engine",
1253                "prob_cache",
1254                "prob_samples",
1255                "prob_seed",
1256                "prob_confidence",
1257                "prob_method",
1258                "prob_max_nonmonotone_iterations",
1259                "max_recursion_depth",
1260                "epistemic_mode",
1261                "magic_sets",
1262            ]
1263        );
1264    }
1265
1266    #[test]
1267    fn test_term_variable() {
1268        let term = Term::Variable("X".to_string());
1269        assert!(term.is_variable());
1270        assert!(!term.is_constant());
1271    }
1272
1273    #[test]
1274    fn test_term_constant() {
1275        let term = Term::Integer(42);
1276        assert!(!term.is_variable());
1277        assert!(term.is_constant());
1278    }
1279
1280    #[test]
1281    fn test_atom_arity() {
1282        let atom = Atom {
1283            predicate: "edge".to_string(),
1284            terms: vec![Term::Integer(1), Term::Integer(2)],
1285        };
1286        assert_eq!(atom.arity(), 2);
1287    }
1288
1289    #[test]
1290    fn test_atom_variables() {
1291        let atom = Atom {
1292            predicate: "edge".to_string(),
1293            terms: vec![Term::Variable("X".to_string()), Term::Integer(2)],
1294        };
1295        let vars = atom.variables();
1296        assert_eq!(vars, vec!["X"]);
1297    }
1298
1299    #[test]
1300    fn predicate_declaration_uses_its_effective_schema_representation() {
1301        let types_only = PredDecl {
1302            name: "types_only".to_string(),
1303            types: vec![TypeRef::Scalar(ScalarType::U64)],
1304            columns: vec![],
1305            is_private: false,
1306        };
1307        assert_eq!(types_only.arity(), 1);
1308        assert_eq!(
1309            types_only.schema_columns(),
1310            vec![PredColumn {
1311                name: None,
1312                typ: TypeRef::Scalar(ScalarType::U64),
1313            }]
1314        );
1315
1316        let columns_only = PredDecl {
1317            name: "columns_only".to_string(),
1318            types: vec![],
1319            columns: vec![PredColumn {
1320                name: Some("value".to_string()),
1321                typ: TypeRef::Scalar(ScalarType::Symbol),
1322            }],
1323            is_private: false,
1324        };
1325        assert_eq!(columns_only.arity(), 1);
1326        assert_eq!(
1327            columns_only.schema_columns(),
1328            vec![PredColumn {
1329                name: Some("value".to_string()),
1330                typ: TypeRef::Scalar(ScalarType::Symbol),
1331            }]
1332        );
1333    }
1334
1335    #[test]
1336    fn test_rule_is_fact() {
1337        let fact = Rule {
1338            head: Atom {
1339                predicate: "edge".to_string(),
1340                terms: vec![Term::Integer(1), Term::Integer(2)],
1341            },
1342            body: vec![],
1343        };
1344        assert!(fact.is_fact());
1345    }
1346
1347    #[test]
1348    fn test_rule_has_negation() {
1349        let rule = Rule {
1350            head: Atom {
1351                predicate: "isolated".to_string(),
1352                terms: vec![Term::Variable("X".to_string())],
1353            },
1354            body: vec![
1355                BodyLiteral::Positive(Atom {
1356                    predicate: "node".to_string(),
1357                    terms: vec![Term::Variable("X".to_string())],
1358                }),
1359                BodyLiteral::Negated(Atom {
1360                    predicate: "edge".to_string(),
1361                    terms: vec![
1362                        Term::Variable("X".to_string()),
1363                        Term::Variable("Y".to_string()),
1364                    ],
1365                }),
1366            ],
1367        };
1368        assert!(rule.has_negation());
1369    }
1370
1371    #[test]
1372    fn test_program_facts() {
1373        let mut program = Program::new();
1374        program.rules.push(Rule {
1375            head: Atom {
1376                predicate: "edge".to_string(),
1377                terms: vec![Term::Integer(1), Term::Integer(2)],
1378            },
1379            body: vec![],
1380        });
1381        program.rules.push(Rule {
1382            head: Atom {
1383                predicate: "reach".to_string(),
1384                terms: vec![
1385                    Term::Variable("X".to_string()),
1386                    Term::Variable("Y".to_string()),
1387                ],
1388            },
1389            body: vec![BodyLiteral::Positive(Atom {
1390                predicate: "edge".to_string(),
1391                terms: vec![
1392                    Term::Variable("X".to_string()),
1393                    Term::Variable("Y".to_string()),
1394                ],
1395            })],
1396        });
1397        assert_eq!(program.facts().count(), 1);
1398        assert_eq!(program.proper_rules().count(), 1);
1399    }
1400
1401    #[test]
1402    fn test_arith_expr_structure() {
1403        let expr = ArithExpr::Add(
1404            Box::new(ArithExpr::Variable("X".to_string())),
1405            Box::new(ArithExpr::Integer(1)),
1406        );
1407        assert!(matches!(expr, ArithExpr::Add(_, _)));
1408    }
1409
1410    #[test]
1411    fn test_is_expr_structure() {
1412        let is_expr = IsExpr {
1413            target: "Z".to_string(),
1414            expr: ArithExpr::Variable("Y".to_string()),
1415        };
1416        assert_eq!(is_expr.target, "Z");
1417    }
1418}