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}