Skip to main content

zerodds_lang_rules/
apply.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Forward application. Spec §2.5.
4//!
5//! A rule specification lowers to two directed operationalisations; only
6//! the forward one runs here. The backward direction is what the engine
7//! gives for free and what a later phase will use to read generated code
8//! back — exercising it now would let a rule quietly come to depend on
9//! it.
10
11use crate::adapter::ast::ModelGraph;
12use seesaw_tgg::engine::Engine;
13use seesaw_tgg::ident::GhostId;
14use seesaw_tgg::rules::format::RuleFile;
15use seesaw_tgg::rules::{lower::lower_all, validate::validate};
16use std::collections::BTreeMap;
17use zerodds_ast_core::NodeId;
18
19/// How many engine steps one application may take.
20///
21/// A bound rather than trust: a catalogue whose rules trigger each other
22/// is a defect, and without this it hangs instead of being reported.
23const MAX_STEPS: usize = 100_000;
24
25/// What applying a catalogue produced.
26pub struct Applied {
27    /// The graph after application, holding both sides.
28    pub graph: seesaw_tgg::graph::Graph,
29    /// Which model node each created node corresponds to.
30    ///
31    /// Read off the correspondences the rules established, so a
32    /// materialiser can ask the model for the values a node needs —
33    /// values never enter the overlay, they are resolved from the
34    /// original. Spec §2.3.
35    pub origin: BTreeMap<GhostId, NodeId>,
36    /// How many steps the engine took.
37    pub steps: usize,
38}
39
40/// Why an application could not run.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum ApplyError {
43    /// A rule did not lower, with the engine's reason.
44    Lowering {
45        /// The rule.
46        rule: String,
47        /// What the engine said.
48        detail: String,
49    },
50    /// The engine did not settle within the step bound.
51    DidNotSettle {
52        /// The bound it passed.
53        steps: usize,
54    },
55}
56
57impl core::fmt::Display for ApplyError {
58    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59        match self {
60            Self::Lowering { rule, detail } => write!(f, "rule `{rule}` did not lower: {detail}"),
61            Self::DidNotSettle { steps } => {
62                write!(f, "the catalogue did not settle within {steps} steps")
63            }
64        }
65    }
66}
67
68impl core::error::Error for ApplyError {}
69
70/// Apply `rules` to `model`, forward.
71///
72/// # Errors
73/// [`ApplyError::Lowering`] when a rule does not compile, and
74/// [`ApplyError::DidNotSettle`] when the catalogue keeps firing.
75pub fn apply(model: &ModelGraph, rules: &RuleFile) -> Result<Applied, ApplyError> {
76    let mut graph = model.graph.clone();
77    let values = model.values.clone();
78
79    // Validation first: the loader resolves node names to positions and
80    // reports a rule that names something it never declared, which a
81    // hand-built rule value can otherwise carry all the way to a match
82    // that silently never fires.
83    let resolved = validate(rules).map_err(|error| ApplyError::Lowering {
84        rule: rules.name.clone(),
85        detail: format!("{error:?}"),
86    })?;
87
88    // Both directions come back; only the forward ones run. The backward
89    // direction is what the engine gives for free and what a later phase
90    // will use to read generated code back — exercising it now would let
91    // a rule quietly come to depend on it.
92    let all = lower_all(&resolved, &mut graph).map_err(|error| ApplyError::Lowering {
93        rule: rules.name.clone(),
94        detail: format!("{error:?}"),
95    })?;
96    let directed: Vec<_> = all
97        .into_iter()
98        .filter(|rule| !rule.name.ends_with("__backward"))
99        .collect();
100
101    let mut engine = Engine::new(&directed);
102    engine.seed(&graph, &values);
103    let mut steps = 0usize;
104    while engine.step(&mut graph, &values).is_some() {
105        steps += 1;
106        if steps >= MAX_STEPS {
107            return Err(ApplyError::DidNotSettle { steps });
108        }
109    }
110
111    Ok(Applied {
112        graph,
113        origin: origins(model),
114        steps,
115    })
116}
117
118/// The map from created node to the model node it came from.
119///
120/// Built from the adapter's own map: every node the model contributed
121/// keeps its identity, and a created node's origin is the node its
122/// correspondence anchors on.
123fn origins(model: &ModelGraph) -> BTreeMap<GhostId, NodeId> {
124    let mut out = BTreeMap::new();
125    for (ghost, node) in model.pairs() {
126        let _ = out.insert(ghost, node);
127    }
128    out
129}