1use std::collections::HashMap;
13
14use serde::{Deserialize, Serialize};
15
16use crate::colimit::colimit_by_name;
17use crate::error::GatError;
18use crate::op::Operation;
19use crate::sort::Sort;
20use crate::theory::Theory;
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct CompositionSpec {
25 pub result_name: String,
27 pub steps: Vec<CompositionStep>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub enum CompositionStep {
34 Base(String),
36 Colimit {
38 left: String,
40 right: String,
42 shared_sorts: Vec<String>,
44 #[serde(default)]
47 shared_ops: Vec<String>,
48 },
49}
50
51pub fn recompose<S: ::std::hash::BuildHasher>(
64 spec: &CompositionSpec,
65 registry: &HashMap<String, Theory, S>,
66) -> Result<Theory, GatError> {
67 let mut intermediates: HashMap<String, Theory> = registry
68 .iter()
69 .map(|(k, v)| (k.clone(), v.clone()))
70 .collect();
71 let mut last_name: Option<String> = None;
72
73 for (i, step) in spec.steps.iter().enumerate() {
74 let step_name = format!("step_{i}");
75 match step {
76 CompositionStep::Base(name) => {
77 if !intermediates.contains_key(name) {
78 return Err(GatError::TheoryNotFound(name.clone()));
79 }
80 last_name = Some(name.clone());
81 }
82 CompositionStep::Colimit {
83 left,
84 right,
85 shared_sorts,
86 shared_ops,
87 } => {
88 let left_theory = intermediates
89 .get(left)
90 .ok_or_else(|| GatError::TheoryNotFound(left.clone()))?
91 .clone();
92 let right_theory = intermediates
93 .get(right)
94 .ok_or_else(|| GatError::TheoryNotFound(right.clone()))?
95 .clone();
96
97 let mut shared_op_defs: Vec<Operation> = Vec::with_capacity(shared_ops.len());
100 for op_name in shared_ops {
101 let left_op = left_theory.find_op(op_name).ok_or_else(|| {
102 GatError::FactorizationError(format!(
103 "shared operation '{op_name}' not found in left theory '{left}'"
104 ))
105 })?;
106 if !right_theory.has_op(op_name) {
107 return Err(GatError::FactorizationError(format!(
108 "shared operation '{op_name}' not found in right theory '{right}'"
109 )));
110 }
111 shared_op_defs.push(left_op.clone());
112 }
113
114 let shared = Theory::new(
115 &*step_name,
116 shared_sorts
117 .iter()
118 .map(|s| Sort::simple(s.as_str()))
119 .collect(),
120 shared_op_defs,
121 vec![],
122 );
123
124 let result = colimit_by_name(&left_theory, &right_theory, &shared)?;
125 intermediates.insert(step_name.clone(), result);
126 last_name = Some(step_name);
127 }
128 }
129 }
130
131 let final_name = last_name.ok_or_else(|| {
132 GatError::TheoryNotFound(format!("empty composition spec for '{}'", spec.result_name))
133 })?;
134
135 let mut theory = intermediates
136 .get(&final_name)
137 .ok_or_else(|| GatError::TheoryNotFound(final_name.clone()))?
138 .clone();
139 theory.name = spec.result_name.clone().into();
140 Ok(theory)
141}
142
143#[cfg(test)]
144#[allow(clippy::unwrap_used)]
145mod tests {
146 use super::*;
147 use crate::op::Operation;
148 use crate::sort::{Sort, SortParam};
149
150 fn base_registry() -> HashMap<String, Theory> {
151 let mut reg = HashMap::new();
152 reg.insert(
153 "ThGraph".into(),
154 Theory::new(
155 "ThGraph",
156 vec![Sort::simple("Vertex"), Sort::simple("Edge")],
157 vec![
158 Operation::unary("src", "e", "Edge", "Vertex"),
159 Operation::unary("tgt", "e", "Edge", "Vertex"),
160 ],
161 vec![],
162 ),
163 );
164 reg.insert(
165 "ThConstraint".into(),
166 Theory::new(
167 "ThConstraint",
168 vec![
169 Sort::simple("Vertex"),
170 Sort::dependent("Constraint", vec![SortParam::new("v", "Vertex")]),
171 ],
172 vec![Operation::unary("target", "c", "Constraint", "Vertex")],
173 vec![],
174 ),
175 );
176 reg.insert(
177 "ThMulti".into(),
178 Theory::new(
179 "ThMulti",
180 vec![
181 Sort::simple("Vertex"),
182 Sort::simple("Edge"),
183 Sort::simple("EdgeLabel"),
184 ],
185 vec![Operation::unary("edge_label", "e", "Edge", "EdgeLabel")],
186 vec![],
187 ),
188 );
189 reg.insert(
190 "ThWType".into(),
191 Theory::new(
192 "ThWType",
193 vec![
194 Sort::simple("Node"),
195 Sort::simple("Arc"),
196 Sort::simple("Value"),
197 ],
198 vec![
199 Operation::unary("anchor", "n", "Node", "Vertex"),
200 Operation::unary("arc_src", "a", "Arc", "Node"),
201 Operation::unary("arc_tgt", "a", "Arc", "Node"),
202 Operation::unary("arc_edge", "a", "Arc", "Edge"),
203 Operation::unary("node_value", "n", "Node", "Value"),
204 ],
205 vec![],
206 ),
207 );
208 reg
209 }
210
211 #[test]
212 fn base_step_verifies_existence() {
213 let reg = base_registry();
214 let spec = CompositionSpec {
215 result_name: "Result".into(),
216 steps: vec![CompositionStep::Base("ThWType".into())],
217 };
218 let theory = recompose(&spec, ®).unwrap();
219 assert_eq!(&*theory.name, "Result");
220 assert_eq!(theory.sorts.len(), 3);
221 }
222
223 #[test]
224 fn base_step_missing_theory_errors() {
225 let reg = base_registry();
226 let spec = CompositionSpec {
227 result_name: "Result".into(),
228 steps: vec![CompositionStep::Base("ThNonexistent".into())],
229 };
230 let result = recompose(&spec, ®);
231 assert!(matches!(result, Err(GatError::TheoryNotFound(_))));
232 }
233
234 #[test]
235 fn single_colimit_step() {
236 let reg = base_registry();
237 let spec = CompositionSpec {
238 result_name: "GraphConstraint".into(),
239 steps: vec![CompositionStep::Colimit {
240 left: "ThGraph".into(),
241 right: "ThConstraint".into(),
242 shared_sorts: vec!["Vertex".into()],
243 shared_ops: vec![],
244 }],
245 };
246 let theory = recompose(&spec, ®).unwrap();
247 assert_eq!(&*theory.name, "GraphConstraint");
248 assert_eq!(theory.sorts.len(), 3);
250 assert_eq!(theory.ops.len(), 3);
252 }
253
254 #[test]
255 fn chained_colimit_matches_direct() {
256 let reg = base_registry();
257
258 let spec = CompositionSpec {
260 result_name: "SchemaA".into(),
261 steps: vec![
262 CompositionStep::Colimit {
263 left: "ThGraph".into(),
264 right: "ThConstraint".into(),
265 shared_sorts: vec!["Vertex".into()],
266 shared_ops: vec![],
267 },
268 CompositionStep::Colimit {
269 left: "step_0".into(),
270 right: "ThMulti".into(),
271 shared_sorts: vec!["Vertex".into(), "Edge".into()],
272 shared_ops: vec![],
273 },
274 ],
275 };
276 let from_spec = recompose(&spec, ®).unwrap();
277
278 let g = reg.get("ThGraph").unwrap();
280 let c = reg.get("ThConstraint").unwrap();
281 let m = reg.get("ThMulti").unwrap();
282
283 let shared_v = Theory::new("sv", vec![Sort::simple("Vertex")], vec![], vec![]);
284 let gc = colimit_by_name(g, c, &shared_v).unwrap();
285
286 let shared_ve = Theory::new(
287 "sve",
288 vec![Sort::simple("Vertex"), Sort::simple("Edge")],
289 vec![],
290 vec![],
291 );
292 let mut direct = colimit_by_name(&gc, m, &shared_ve).unwrap();
293 direct.name = "SchemaA".into();
294
295 assert_eq!(from_spec.sorts.len(), direct.sorts.len());
297 assert_eq!(from_spec.ops.len(), direct.ops.len());
298 for sort in &direct.sorts {
299 assert!(
300 from_spec.find_sort(&sort.name).is_some(),
301 "missing sort: {}",
302 sort.name
303 );
304 }
305 for op in &direct.ops {
306 assert!(
307 from_spec.find_op(&op.name).is_some(),
308 "missing op: {}",
309 op.name
310 );
311 }
312 }
313
314 #[test]
315 fn empty_spec_errors() {
316 let reg = base_registry();
317 let spec = CompositionSpec {
318 result_name: "Empty".into(),
319 steps: vec![],
320 };
321 let result = recompose(&spec, ®);
322 assert!(matches!(result, Err(GatError::TheoryNotFound(_))));
323 }
324
325 #[test]
326 fn serde_round_trip() {
327 let spec = CompositionSpec {
328 result_name: "SchemaA".into(),
329 steps: vec![
330 CompositionStep::Colimit {
331 left: "ThGraph".into(),
332 right: "ThConstraint".into(),
333 shared_sorts: vec!["Vertex".into()],
334 shared_ops: vec![],
335 },
336 CompositionStep::Base("ThWType".into()),
337 ],
338 };
339 let json = serde_json::to_string(&spec).unwrap();
340 let roundtripped: CompositionSpec = serde_json::from_str(&json).unwrap();
341 assert_eq!(spec, roundtripped);
342 }
343}