Skip to main content

lace_sans_io/logic/
limits.rs

1//! Resource bounds owned by one prepared generic Datalog program.
2
3use std::collections::HashSet;
4
5use super::types::{BodyAtom, Rule, Tuple, is_canonical_unsigned_decimal, validate_tuple_values};
6
7/// Generic Lace-030 preparation and finite-evaluation limits.
8///
9/// Runtime exchange uses the fixed profile's preparation bounds and its separate
10/// lifecycle work budget; these are not interlace operation options.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct DatalogLimits {
13    pub max_base_facts: usize,
14    pub max_runtime_facts: usize,
15    pub max_derived_facts_per_relation: usize,
16    pub max_rules: usize,
17    pub max_stratum_iterations: usize,
18    pub max_tuple_arity: usize,
19}
20
21impl Default for DatalogLimits {
22    fn default() -> Self {
23        Self {
24            max_base_facts: 1 << 20,
25            max_runtime_facts: 1 << 20,
26            max_derived_facts_per_relation: 1 << 18,
27            max_rules: 256,
28            max_stratum_iterations: 1000,
29            max_tuple_arity: 8,
30        }
31    }
32}
33
34/// Typed preparation failure shared by core validation and authoring frontends.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum PreparationError {
37    Policy(String),
38    LimitExceeded(String),
39}
40impl PreparationError {
41    pub fn is_limit_exceeded(&self) -> bool {
42        matches!(self, Self::LimitExceeded(_))
43    }
44}
45impl std::fmt::Display for PreparationError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::Policy(message) | Self::LimitExceeded(message) => message.fmt(f),
49        }
50    }
51}
52impl std::error::Error for PreparationError {}
53impl From<String> for PreparationError {
54    fn from(message: String) -> Self {
55        Self::Policy(message)
56    }
57}
58impl From<&str> for PreparationError {
59    fn from(message: &str) -> Self {
60        Self::Policy(message.into())
61    }
62}
63impl From<PreparationError> for String {
64    fn from(error: PreparationError) -> Self {
65        error.to_string()
66    }
67}
68
69impl DatalogLimits {
70    /// Deterministic exchange lowering is not a source contribution.
71    /// Its input programs have already passed
72    /// source rule accounting; all other preparation/evaluation bounds remain.
73    pub(crate) fn for_generated_rules() -> Self {
74        Self {
75            max_rules: usize::MAX,
76            ..Self::default()
77        }
78    }
79
80    pub(crate) fn validate_arity(&self, arity: usize) -> Result<(), PreparationError> {
81        if arity > self.max_tuple_arity {
82            return Err(PreparationError::LimitExceeded(format!(
83                "predicate arity exceeds {}",
84                self.max_tuple_arity
85            )));
86        }
87        Ok(())
88    }
89
90    pub(crate) fn validate_tuple(&self, tuple: &Tuple) -> Result<(), PreparationError> {
91        validate_tuple_values(tuple)?;
92        self.validate_arity(tuple.0.len() - 1)
93    }
94
95    pub(crate) fn validate_rules<'a>(
96        &self,
97        rules: impl IntoIterator<Item = &'a Rule>,
98    ) -> Result<(), PreparationError> {
99        let mut distinct = HashSet::new();
100        for rule in rules {
101            if self.max_rules != usize::MAX {
102                distinct.insert(rule);
103            }
104            self.check_preparation_rule(rule, distinct.len())?;
105        }
106        Ok(())
107    }
108
109    /// Check one emitted rule and the caller's distinct source-rule count.
110    /// Validation-only placeholders use a count of zero.
111    pub fn check_preparation_rule(
112        &self,
113        rule: &Rule,
114        distinct_count: usize,
115    ) -> Result<(), PreparationError> {
116        if distinct_count > self.max_rules {
117            return Err(PreparationError::LimitExceeded(format!(
118                "rule count exceeds {}",
119                self.max_rules
120            )));
121        }
122        self.validate_arity(rule.head_terms.len())?;
123        for atom in &rule.body {
124            if let Some((_, terms, _)) = atom.relation() {
125                self.validate_arity(terms.len())?;
126            }
127            if let BodyAtom::TopK { limit, .. } = atom {
128                if !is_canonical_unsigned_decimal(limit) {
129                    return Err("TopK limit must be canonical unsigned decimal text".into());
130                }
131                let bound: usize = limit.parse().map_err(|_| {
132                    PreparationError::LimitExceeded(
133                        "TopK limit exceeds the configured derived-fact bound".into(),
134                    )
135                })?;
136                if bound > self.max_derived_facts_per_relation {
137                    return Err(PreparationError::LimitExceeded(
138                        "TopK limit exceeds the configured derived-fact bound".into(),
139                    ));
140                }
141            }
142        }
143        Ok(())
144    }
145}
146
147/// Retain each validated source rule once, checking bounds before retaining it.
148/// Ordered rules preserve source execution order; rendered keys avoid cloning ASTs.
149pub(crate) struct RuleCollector {
150    limits: DatalogLimits,
151    seen: HashSet<String>,
152    rules: Vec<Rule>,
153}
154
155impl RuleCollector {
156    pub(crate) fn new(limits: DatalogLimits) -> Self {
157        Self {
158            limits,
159            seen: HashSet::new(),
160            rules: Vec::new(),
161        }
162    }
163
164    pub(crate) fn push(&mut self, rule: Rule) -> Result<(), PreparationError> {
165        let key = crate::lacegram::render_rule_line(&rule);
166        let fresh = !self.seen.contains(&key);
167        self.limits
168            .check_preparation_rule(&rule, self.rules.len() + usize::from(fresh))?;
169        if fresh {
170            self.seen.insert(key);
171            self.rules.push(rule);
172        }
173        Ok(())
174    }
175
176    pub(crate) fn into_rules(self) -> Vec<Rule> {
177        self.rules
178    }
179}