Skip to main content

lace_sans_io/logic/
lacegram.rs

1//! Datalog lacegram parsing, validation, and canonicalization.
2//!
3//! Parses rule lines per docs/spec/030-DATALOG.md, checks safety and stratification,
4//! and assigns strata.
5
6use super::dependency_graph::DependencyGraph;
7use super::limits::{DatalogLimits, PreparationError, RuleCollector};
8use crate::types::{Lacegram, Rule};
9
10mod parser;
11mod syntax;
12mod validation;
13
14use parser::head_name;
15pub use parser::{parse_rule, render_rule_line};
16use validation::check_safety;
17pub use validation::predicate_variable_kinds;
18
19/// Canonical text and its already parsed, validated Datalog program.
20///
21/// Compiler stages carry this pair together so canonical text is never reparsed
22/// merely to recover the program that produced it.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct CanonicalLacegram {
25    pub text: String,
26    pub program: Lacegram,
27}
28
29/// Canonicalize and compile Lace Datalog source without accepting semantic sugar.
30///
31/// This helper trims blank/comment lines, parses only valid core body atoms,
32/// renders canonical rule text, sorts/deduplicates rules, and validates the
33/// resulting program. External DSLs must emit valid Lace Datalog before calling
34/// into the core library.
35pub fn canonicalize_lacegram_source(
36    text: &str,
37    limits: DatalogLimits,
38) -> Result<CanonicalLacegram, PreparationError> {
39    if text.contains('\r') {
40        return Err("lacegram contains CR".into());
41    }
42    let mut rules = RuleCollector::new(limits);
43    for line in text.lines() {
44        let line = line.trim();
45        if line.is_empty() || line.starts_with('#') {
46            continue;
47        }
48        rules.push(parse_rule(line)?)?;
49    }
50    prepare_lacegram_rules(rules.into_rules(), limits)
51}
52
53pub fn canonicalize_lacegram_rules(
54    rules: Vec<Rule>,
55    limits: DatalogLimits,
56) -> Result<CanonicalLacegram, String> {
57    prepare_lacegram_rules(rules, limits).map_err(Into::into)
58}
59
60/// Canonical preparation retaining typed resource-limit failures.
61pub fn prepare_lacegram_rules(
62    rules: Vec<Rule>,
63    limits: DatalogLimits,
64) -> Result<CanonicalLacegram, PreparationError> {
65    if rules.is_empty() {
66        return Err("lacegram has no rules".into());
67    }
68    limits.validate_rules(&rules)?;
69    let mut parsed = rules
70        .into_iter()
71        .map(|rule| (render_rule_line(&rule), rule))
72        .collect::<Vec<_>>();
73    parsed.sort_by(|(left, _), (right, _)| {
74        head_name(left).cmp(head_name(right)).then(left.cmp(right))
75    });
76    parsed.dedup_by(|(left, _), (right, _)| left == right);
77    let text = parsed
78        .iter()
79        .map(|(line, _)| line.as_str())
80        .collect::<Vec<_>>()
81        .join("\n");
82    let rules = parsed.into_iter().map(|(_, rule)| rule).collect();
83    let program = compile_rules(rules, limits)?;
84    Ok(CanonicalLacegram { text, program })
85}
86
87/// Parse a canonical lacegram text into a compiled `Lacegram`.
88///
89/// Rejects text that differs from the shared preparer's canonical rendering,
90/// including empty programs, comments, noncanonical spacing, rule order,
91/// duplicates, and trailing LF. Authoring syntax is not accepted.
92pub fn parse_lacegram(text: &str, limits: DatalogLimits) -> Result<Lacegram, String> {
93    let canonical = canonicalize_lacegram_source(text, limits)?;
94    if canonical.text != text {
95        return Err("lacegram text is not canonical".into());
96    }
97    Ok(canonical.program)
98}
99
100fn compile_rules(rules: Vec<Rule>, limits: DatalogLimits) -> Result<Lacegram, String> {
101    validate_rules(&rules)?;
102    let strata = DependencyGraph::new(&rules).strata(rules)?;
103    Ok(Lacegram { strata, limits })
104}
105
106/// Validate current parsed rules, including publicly mutable Lacegram inputs.
107pub(crate) fn validate_rules(rules: &[Rule]) -> Result<(), String> {
108    for rule in rules {
109        syntax::validate_rule_syntax(rule)?;
110    }
111    check_safety(rules)
112}
113
114#[cfg(test)]
115mod limit_tests;
116#[cfg(test)]
117mod tests;