1use indexmap::IndexMap;
44use serde_json::{Map, Value};
45use std::collections::HashSet;
46use thiserror::Error;
47
48#[deprecated(
55 since = "0.2.0",
56 note = "renamed to `ReferenceResolutionError` (API_SPEC.md §8 item 10)"
57)]
58pub type ReferenceError = ReferenceResolutionError;
59
60#[derive(Error, Debug, Clone, PartialEq, Eq)]
66pub enum ReferenceResolutionError {
67 #[error(
69 "undeclared index set '{name}' referenced by range '{index}' of node {node} (model '{model}', at {path})"
70 )]
71 UndeclaredIndexSet {
72 name: String,
73 index: String,
74 node: String,
75 model: String,
76 path: String,
77 },
78
79 #[error(
84 "derived index set '{index_set}' references from_faq '{from_faq}', which is not the id of any expression node in the document (model '{model}')"
85 )]
86 UnknownFaqNode {
87 index_set: String,
88 from_faq: String,
89 model: String,
90 },
91
92 #[error("duplicate expression-node id '{id}' in model '{model}' (at {path} and {first})")]
98 DuplicateNodeId {
99 id: String,
100 model: String,
101 path: String,
102 first: String,
103 },
104
105 #[error(
107 "join factor '{factor}' of node {node} names no range, output index, factor arg, declared variable, or index set in scope (model '{model}', at {path})"
108 )]
109 UnresolvedJoinFactor {
110 factor: String,
111 node: String,
112 model: String,
113 path: String,
114 },
115
116 #[error("reference cycle detected: {}", .path.join(" -> "))]
118 ReferenceCycle { path: Vec<String> },
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum VertexKind {
124 Node,
126 IndexSet,
128 Factor,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum EdgeKind {
135 RangeFrom,
137 FromFaq,
139 JoinFactor,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct ReferenceVertex {
150 pub key: String,
151 pub kind: VertexKind,
152 pub name: String,
153 pub op: Option<String>,
154 pub node_id: Option<String>,
155 pub path: Option<String>,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct ReferenceEdge {
161 pub source: String,
162 pub target: String,
163 pub kind: EdgeKind,
164}
165
166#[derive(Debug, Clone, Default)]
173pub struct ReferenceGraph {
174 pub model: String,
175 pub vertices: IndexMap<String, ReferenceVertex>,
176 pub edges: Vec<ReferenceEdge>,
177 out: IndexMap<String, Vec<String>>,
178 incoming: IndexMap<String, Vec<String>>,
179}
180
181impl ReferenceGraph {
182 fn ensure_vertex(&mut self, vertex: ReferenceVertex) {
183 if !self.vertices.contains_key(&vertex.key) {
184 self.out.entry(vertex.key.clone()).or_default();
185 self.incoming.entry(vertex.key.clone()).or_default();
186 self.vertices.insert(vertex.key.clone(), vertex);
187 }
188 }
189
190 fn add_edge(&mut self, source: &str, target: &str, kind: EdgeKind) {
191 self.edges.push(ReferenceEdge {
192 source: source.to_string(),
193 target: target.to_string(),
194 kind,
195 });
196 self.out
197 .entry(source.to_string())
198 .or_default()
199 .push(target.to_string());
200 self.incoming
201 .entry(target.to_string())
202 .or_default()
203 .push(source.to_string());
204 }
205
206 pub fn dependencies(&self, key: &str) -> Vec<String> {
208 self.out.get(key).cloned().unwrap_or_default()
209 }
210
211 pub fn dependents(&self, key: &str) -> Vec<String> {
213 self.incoming.get(key).cloned().unwrap_or_default()
214 }
215
216 pub fn edges_of_kind(&self, kind: EdgeKind) -> Vec<&ReferenceEdge> {
218 self.edges.iter().filter(|e| e.kind == kind).collect()
219 }
220
221 pub fn detect_cycle(&self) -> Option<Vec<String>> {
226 #[derive(Clone, Copy, PartialEq)]
227 enum Colour {
228 White,
229 Grey,
230 Black,
231 }
232 let mut colour: IndexMap<String, Colour> = self
233 .vertices
234 .keys()
235 .map(|k| (k.clone(), Colour::White))
236 .collect();
237 let mut starts: Vec<String> = self.vertices.keys().cloned().collect();
238 starts.sort();
239
240 for start in starts {
241 if colour.get(&start).copied().unwrap_or(Colour::White) != Colour::White {
242 continue;
243 }
244 let mut stack: Vec<(String, usize)> = vec![(start.clone(), 0)];
245 let mut path: Vec<String> = vec![start.clone()];
246 colour.insert(start.clone(), Colour::Grey);
247 while let Some((node, i)) = stack.last().cloned() {
248 let mut neigh = self.out.get(&node).cloned().unwrap_or_default();
249 neigh.sort();
250 if i < neigh.len() {
251 stack.last_mut().unwrap().1 = i + 1;
252 let nxt = neigh[i].clone();
253 match colour.get(&nxt).copied().unwrap_or(Colour::White) {
254 Colour::Grey => {
255 let idx = path.iter().position(|p| *p == nxt).unwrap_or(0);
256 let mut cyc: Vec<String> = path[idx..].to_vec();
257 cyc.push(nxt);
258 return Some(cyc);
259 }
260 Colour::White => {
261 colour.insert(nxt.clone(), Colour::Grey);
262 stack.push((nxt.clone(), 0));
263 path.push(nxt);
264 }
265 Colour::Black => {}
266 }
267 } else {
268 colour.insert(node.clone(), Colour::Black);
269 stack.pop();
270 path.pop();
271 }
272 }
273 }
274 None
275 }
276
277 pub fn topological_order(&self) -> Result<Vec<String>, ReferenceResolutionError> {
283 if let Some(cyc) = self.detect_cycle() {
291 return Err(ReferenceResolutionError::ReferenceCycle { path: cyc });
292 }
293 let mut emitted: Vec<String> = Vec::new();
294 let mut done: HashSet<String> = HashSet::new();
295 let mut keys: Vec<String> = self.vertices.keys().cloned().collect();
296 keys.sort();
297 while emitted.len() < self.vertices.len() {
298 let mut progressed = false;
299 for k in &keys {
300 if done.contains(k) {
301 continue;
302 }
303 let deps = self.out.get(k).cloned().unwrap_or_default();
304 if deps.iter().all(|d| done.contains(d)) {
305 emitted.push(k.clone());
306 done.insert(k.clone());
307 progressed = true;
308 }
309 }
310 if !progressed {
311 break;
312 }
313 }
314 Ok(emitted)
315 }
316}
317
318fn node_key(addr: &str) -> String {
319 format!("node:{addr}")
320}
321fn index_set_key(name: &str) -> String {
322 format!("index_set:{name}")
323}
324fn factor_key(name: &str) -> String {
325 format!("factor:{name}")
326}
327
328fn nonempty_str(value: Option<&Value>) -> Option<&str> {
329 value.and_then(|v| v.as_str()).filter(|s| !s.is_empty())
330}
331
332fn join_binder_class(
355 map: &Map<String, Value>,
356 name: &str,
357 variables: Option<&Map<String, Value>>,
358 index_sets: Option<&Map<String, Value>>,
359) -> Option<JoinBinder> {
360 if name.is_empty() {
361 return None;
362 }
363 if let Some(ranges) = map.get("ranges").and_then(|v| v.as_object())
365 && ranges.contains_key(name)
366 {
367 return Some(JoinBinder::Range);
368 }
369 if let Some(oi) = map.get("output_idx").and_then(|v| v.as_array())
370 && oi.iter().any(|o| o.as_str() == Some(name))
371 {
372 return Some(JoinBinder::OutputIdx);
373 }
374 if let Some(args) = map.get("args").and_then(|v| v.as_array())
376 && args.iter().any(|a| a.as_str() == Some(name))
377 {
378 return Some(JoinBinder::Arg);
379 }
380 if variables.map(|v| v.contains_key(name)).unwrap_or(false) {
382 return Some(JoinBinder::Variable);
383 }
384 if index_sets.map(|v| v.contains_key(name)).unwrap_or(false) {
386 return Some(JoinBinder::IndexSet);
387 }
388 None
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393enum JoinBinder {
394 Range,
395 OutputIdx,
396 Arg,
397 Variable,
398 IndexSet,
399}
400
401fn check_join_name(
403 map: &Map<String, Value>,
404 name: Option<&str>,
405 variables: Option<&Map<String, Value>>,
406 index_sets: Option<&Map<String, Value>>,
407 node_key: &str,
408 model_name: &str,
409 path: &str,
410) -> Result<(), ReferenceResolutionError> {
411 let resolved = name
412 .and_then(|n| join_binder_class(map, n, variables, index_sets))
413 .is_some();
414 if resolved {
415 Ok(())
416 } else {
417 Err(ReferenceResolutionError::UnresolvedJoinFactor {
418 factor: name.unwrap_or("").to_string(),
419 node: node_key.to_string(),
420 model: model_name.to_string(),
421 path: path.to_string(),
422 })
423 }
424}
425
426#[derive(Clone, Debug)]
428struct DocNode {
429 addr: String,
430 path: String,
431 op: Option<String>,
432}
433
434#[allow(clippy::too_many_arguments)]
435fn register_and_process(
436 map: &Map<String, Value>,
437 path: &str,
438 model_name: &str,
439 index_sets: Option<&Map<String, Value>>,
440 variables: Option<&Map<String, Value>>,
441 graph: &mut ReferenceGraph,
442 id_to_addr: &mut IndexMap<String, DocNode>,
443) -> Result<(), ReferenceResolutionError> {
444 let op = map.get("op").and_then(|v| v.as_str());
445 let nid = nonempty_str(map.get("id"));
446 let is_agg = op.map(crate::faq::is_faq_op).unwrap_or(false);
447 if !is_agg && nid.is_none() {
450 return Ok(());
451 }
452 let addr = nid
453 .map(|s| s.to_string())
454 .unwrap_or_else(|| path.to_string());
455 let key = node_key(&addr);
456
457 if let Some(id) = nid {
458 if let Some(first) = id_to_addr.get(id) {
459 return Err(ReferenceResolutionError::DuplicateNodeId {
460 id: id.to_string(),
461 model: model_name.to_string(),
462 path: path.to_string(),
463 first: first.path.clone(),
464 });
465 }
466 id_to_addr.insert(
467 id.to_string(),
468 DocNode {
469 addr: addr.clone(),
470 path: path.to_string(),
471 op: op.map(|s| s.to_string()),
472 },
473 );
474 }
475
476 graph.ensure_vertex(ReferenceVertex {
477 key: key.clone(),
478 kind: VertexKind::Node,
479 name: addr.clone(),
480 op: op.map(|s| s.to_string()),
481 node_id: nid.map(|s| s.to_string()),
482 path: Some(path.to_string()),
483 });
484
485 if let Some(ranges) = map.get("ranges").and_then(|v| v.as_object()) {
487 for (idx_name, spec) in ranges {
488 if let Some(spec_obj) = spec.as_object()
489 && let Some(from) = spec_obj.get("from")
490 {
491 let target = from.as_str().unwrap_or("");
492 let declared = index_sets.map(|m| m.contains_key(target)).unwrap_or(false);
493 if target.is_empty() || !declared {
494 return Err(ReferenceResolutionError::UndeclaredIndexSet {
495 name: target.to_string(),
496 index: idx_name.clone(),
497 node: key.clone(),
498 model: model_name.to_string(),
499 path: path.to_string(),
500 });
501 }
502 graph.add_edge(&key, &index_set_key(target), EdgeKind::RangeFrom);
503 }
504 }
505 }
506
507 if let Some(join) = map.get("join").and_then(|v| v.as_array()) {
509 for clause in join {
510 let on = match clause.get("on").and_then(|v| v.as_array()) {
511 Some(on) => on,
512 None => continue,
513 };
514 for pair in on {
515 let reference = pair
516 .as_array()
517 .and_then(|p| p.first())
518 .and_then(|v| v.as_str());
519 check_join_name(
520 map, reference, variables, index_sets, &key, model_name, path,
521 )?;
522 if let Some(right) = pair.as_array().and_then(|p| p.get(1))
536 && let Some(r) = right.as_str()
537 {
538 check_join_name(map, Some(r), variables, index_sets, &key, model_name, path)?;
539 }
540 match reference {
541 Some(r) => {
542 graph.ensure_vertex(ReferenceVertex {
543 key: factor_key(r),
544 kind: VertexKind::Factor,
545 name: r.to_string(),
546 op: None,
547 node_id: None,
548 path: None,
549 });
550 graph.add_edge(&key, &factor_key(r), EdgeKind::JoinFactor);
551 }
552 None => unreachable!("check_join_name rejects a non-string left column"),
553 }
554 }
555 }
556 }
557
558 Ok(())
559}
560
561#[allow(clippy::too_many_arguments)]
562fn walk(
563 value: &Value,
564 path: &str,
565 model_name: &str,
566 index_sets: Option<&Map<String, Value>>,
567 variables: Option<&Map<String, Value>>,
568 graph: &mut ReferenceGraph,
569 id_to_addr: &mut IndexMap<String, DocNode>,
570) -> Result<(), ReferenceResolutionError> {
571 match value {
572 Value::Object(map) => {
573 if map.contains_key("op") {
574 register_and_process(
575 map, path, model_name, index_sets, variables, graph, id_to_addr,
576 )?;
577 }
578 for (k, v) in map {
579 walk(
580 v,
581 &format!("{path}/{k}"),
582 model_name,
583 index_sets,
584 variables,
585 graph,
586 id_to_addr,
587 )?;
588 }
589 }
590 Value::Array(arr) => {
591 for (i, v) in arr.iter().enumerate() {
592 walk(
593 v,
594 &format!("{path}/{i}"),
595 model_name,
596 index_sets,
597 variables,
598 graph,
599 id_to_addr,
600 )?;
601 }
602 }
603 _ => {}
604 }
605 Ok(())
606}
607
608const WALK_ROOTS: [&str; 2] = ["equations", "initialization_equations"];
610
611fn collect_ids(
613 value: &Value,
614 path: &str,
615 model_name: &str,
616 nodes: &mut IndexMap<String, DocNode>,
617) -> Result<(), ReferenceResolutionError> {
618 match value {
619 Value::Object(map) => {
620 if map.contains_key("op")
621 && let Some(id) = nonempty_str(map.get("id"))
622 {
623 let qualified = format!("models/{model_name}/{path}");
624 if let Some(first) = nodes.get(id) {
625 return Err(ReferenceResolutionError::DuplicateNodeId {
626 id: id.to_string(),
627 model: model_name.to_string(),
628 path: qualified,
629 first: first.path.clone(),
630 });
631 }
632 nodes.insert(
633 id.to_string(),
634 DocNode {
635 addr: id.to_string(),
636 path: qualified,
637 op: map
638 .get("op")
639 .and_then(|v| v.as_str())
640 .map(|s| s.to_string()),
641 },
642 );
643 }
644 for (k, v) in map {
645 collect_ids(v, &format!("{path}/{k}"), model_name, nodes)?;
646 }
647 }
648 Value::Array(arr) => {
649 for (i, v) in arr.iter().enumerate() {
650 collect_ids(v, &format!("{path}/{i}"), model_name, nodes)?;
651 }
652 }
653 _ => {}
654 }
655 Ok(())
656}
657
658fn collect_document_node_ids(
669 document: &Value,
670) -> Result<IndexMap<String, DocNode>, ReferenceResolutionError> {
671 let mut nodes = IndexMap::new();
672 if let Some(models) = document.get("models").and_then(|v| v.as_object()) {
673 for (model_name, model) in models {
674 for root in WALK_ROOTS {
675 if let Some(v) = model.get(root) {
676 collect_ids(v, root, model_name, &mut nodes)?;
677 }
678 }
679 }
680 }
681 Ok(nodes)
682}
683
684#[deprecated(
691 since = "0.2.0",
692 note = "the registry is now the trailing argument of `build_reference_graph` (API_SPEC.md §8 item 17)"
693)]
694pub fn build_reference_graph_with_index_sets(
695 model: &Value,
696 model_name: &str,
697 doc_index_sets: Option<&Map<String, Value>>,
698) -> Result<ReferenceGraph, ReferenceResolutionError> {
699 build_reference_graph(model, model_name, doc_index_sets)
700}
701
702pub fn build_reference_graph(
721 model: &Value,
722 model_name: &str,
723 doc_index_sets: Option<&Map<String, Value>>,
724) -> Result<ReferenceGraph, ReferenceResolutionError> {
725 build_reference_graph_impl(model, model_name, doc_index_sets, None)
726}
727
728fn build_reference_graph_impl(
733 model: &Value,
734 model_name: &str,
735 doc_index_sets: Option<&Map<String, Value>>,
736 document_nodes: Option<&IndexMap<String, DocNode>>,
737) -> Result<ReferenceGraph, ReferenceResolutionError> {
738 let mut graph = ReferenceGraph {
739 model: model_name.to_string(),
740 ..Default::default()
741 };
742
743 let model_index_sets = model.get("index_sets").and_then(|v| v.as_object());
746 let merged_index_sets: Option<Map<String, Value>> = match (doc_index_sets, model_index_sets) {
747 (None, None) => None,
748 (Some(d), None) => Some(d.clone()),
749 (None, Some(m)) => Some(m.clone()),
750 (Some(d), Some(m)) => {
751 let mut merged = d.clone();
752 for (k, v) in m {
753 merged.insert(k.clone(), v.clone());
754 }
755 Some(merged)
756 }
757 };
758 let index_sets = merged_index_sets.as_ref();
759
760 if let Some(is) = index_sets {
762 for name in is.keys() {
763 graph.ensure_vertex(ReferenceVertex {
764 key: index_set_key(name),
765 kind: VertexKind::IndexSet,
766 name: name.clone(),
767 op: None,
768 node_id: None,
769 path: None,
770 });
771 }
772 }
773
774 let variables = model.get("variables").and_then(|v| v.as_object());
779
780 let mut id_to_addr: IndexMap<String, DocNode> = IndexMap::new();
784 for root in WALK_ROOTS {
785 if let Some(v) = model.get(root) {
786 walk(
787 v,
788 root,
789 model_name,
790 index_sets,
791 variables,
792 &mut graph,
793 &mut id_to_addr,
794 )?;
795 }
796 }
797
798 let faq_scope = document_nodes.unwrap_or(&id_to_addr);
802 if let Some(is) = index_sets {
803 for (name, entry) in is {
804 if entry.get("kind").and_then(|v| v.as_str()) == Some("derived") {
805 let faq = entry.get("from_faq").and_then(|v| v.as_str());
806 match faq.and_then(|f| faq_scope.get(f)) {
807 Some(target) => {
808 graph.ensure_vertex(ReferenceVertex {
812 key: node_key(&target.addr),
813 kind: VertexKind::Node,
814 name: target.addr.clone(),
815 op: target.op.clone(),
816 node_id: faq.map(|s| s.to_string()),
817 path: Some(target.path.clone()),
818 });
819 graph.add_edge(
820 &index_set_key(name),
821 &node_key(&target.addr),
822 EdgeKind::FromFaq,
823 );
824 }
825 None => {
826 return Err(ReferenceResolutionError::UnknownFaqNode {
827 index_set: name.clone(),
828 from_faq: faq.unwrap_or("").to_string(),
829 model: model_name.to_string(),
830 });
831 }
832 }
833 }
834 }
835 }
836
837 Ok(graph)
838}
839
840pub fn resolve_references(
849 document: &Value,
850) -> Result<IndexMap<String, ReferenceGraph>, ReferenceResolutionError> {
851 let mut out = IndexMap::new();
852 let models = match document.get("models").and_then(|v| v.as_object()) {
853 Some(m) => m,
854 None => return Ok(out),
855 };
856 let doc_index_sets = document.get("index_sets").and_then(|v| v.as_object());
859 let document_nodes = collect_document_node_ids(document)?;
860 for (model_name, model) in models {
861 let graph =
862 build_reference_graph_impl(model, model_name, doc_index_sets, Some(&document_nodes))?;
863 if let Some(cyc) = graph.detect_cycle() {
864 return Err(ReferenceResolutionError::ReferenceCycle { path: cyc });
865 }
866 out.insert(model_name.clone(), graph);
867 }
868 Ok(out)
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874 use serde_json::json;
875
876 fn agg(extra: Value) -> Value {
877 let mut base = json!({"op": "faq", "args": []});
878 if let (Some(b), Some(e)) = (base.as_object_mut(), extra.as_object()) {
879 for (k, v) in e {
880 b.insert(k.clone(), v.clone());
881 }
882 }
883 base
884 }
885
886 #[test]
889 fn from_faq_resolves_to_node_by_id() {
890 let producer = agg(json!({"id": "edge_faq", "output_idx": ["edge"],
891 "ranges": {"f": {"from": "faces"}}}));
892 let model = json!({
893 "index_sets": {
894 "faces": {"kind": "interval", "size": 8},
895 "edges": {"kind": "derived", "from_faq": "edge_faq"}
896 },
897 "equations": [{"lhs": producer, "rhs": 0}]
898 });
899 let g = build_reference_graph(&model, "M", None).unwrap();
900 let from_faq = g.edges_of_kind(EdgeKind::FromFaq);
901 assert_eq!(from_faq.len(), 1);
902 assert_eq!(from_faq[0].source, "index_set:edges");
903 assert_eq!(from_faq[0].target, "node:edge_faq");
904 assert_eq!(
905 g.vertices["node:edge_faq"].node_id.as_deref(),
906 Some("edge_faq")
907 );
908 assert!(
909 g.dependencies("index_set:edges")
910 .contains(&"node:edge_faq".to_string())
911 );
912 }
913
914 #[test]
915 fn from_faq_unknown_node_id_errors() {
916 let model = json!({
917 "index_sets": {"edges": {"kind": "derived", "from_faq": "missing"}},
918 "equations": [{"lhs": agg(json!({"id": "present"})), "rhs": 0}]
919 });
920 let err = build_reference_graph(&model, "M", None).unwrap_err();
921 assert!(matches!(
922 err,
923 ReferenceResolutionError::UnknownFaqNode { .. }
924 ));
925 }
926
927 #[test]
928 fn duplicate_node_id_errors() {
929 let model = json!({
930 "equations": [
931 {"lhs": agg(json!({"id": "dup"})), "rhs": 0},
932 {"lhs": agg(json!({"id": "dup"})), "rhs": 0}
933 ]
934 });
935 let err = build_reference_graph(&model, "M", None).unwrap_err();
936 assert!(matches!(
937 err,
938 ReferenceResolutionError::DuplicateNodeId { .. }
939 ));
940 }
941
942 #[test]
945 fn range_from_resolves_to_index_set() {
946 let node = agg(json!({"output_idx": ["i"], "ranges": {"i": {"from": "cells"}}}));
947 let model = json!({
948 "index_sets": {"cells": {"kind": "interval", "size": 4}},
949 "equations": [{"lhs": node, "rhs": 0}]
950 });
951 let g = build_reference_graph(&model, "M", None).unwrap();
952 let rf = g.edges_of_kind(EdgeKind::RangeFrom);
953 assert_eq!(rf.len(), 1);
954 assert_eq!(rf[0].target, "index_set:cells");
955 assert!(
956 g.dependencies(&rf[0].source)
957 .contains(&"index_set:cells".to_string())
958 );
959 }
960
961 #[test]
962 fn range_from_undeclared_index_set_errors() {
963 let node = agg(json!({"output_idx": ["i"], "ranges": {"i": {"from": "nope"}}}));
964 let model = json!({
965 "index_sets": {"cells": {"kind": "interval", "size": 4}},
966 "equations": [{"lhs": node, "rhs": 0}]
967 });
968 let err = build_reference_graph(&model, "M", None).unwrap_err();
969 assert!(matches!(
970 err,
971 ReferenceResolutionError::UndeclaredIndexSet { .. }
972 ));
973 }
974
975 #[test]
976 fn dense_tuple_ranges_make_no_edge() {
977 let node = agg(json!({"output_idx": ["i"], "ranges": {"i": [1, 64]}}));
978 let model = json!({"equations": [{"lhs": node, "rhs": 0}]});
979 let g = build_reference_graph(&model, "M", None).unwrap();
980 assert!(g.edges.is_empty());
981 }
982
983 #[test]
986 fn join_factor_resolves_to_arg_factor() {
987 let node = agg(json!({
988 "output_idx": ["county"],
989 "ranges": {"county": {"from": "county"}, "src": {"from": "sourceType"}},
990 "join": [{"on": [["activity", "sourceType"]]}],
991 "args": ["activity", "base_rate"]
992 }));
993 let model = json!({
994 "index_sets": {
995 "county": {"kind": "categorical", "members": ["A", "B"]},
996 "sourceType": {"kind": "categorical", "members": ["x"]}
997 },
998 "equations": [{"lhs": node, "rhs": 0}]
999 });
1000 let g = build_reference_graph(&model, "M", None).unwrap();
1001 let jf = g.edges_of_kind(EdgeKind::JoinFactor);
1002 assert_eq!(jf.len(), 1);
1003 assert_eq!(jf[0].target, "factor:activity");
1004 assert_eq!(g.vertices["factor:activity"].kind, VertexKind::Factor);
1005 assert!(
1006 g.dependencies(&jf[0].source)
1007 .contains(&"factor:activity".to_string())
1008 );
1009 }
1010
1011 #[test]
1012 fn join_factor_resolves_to_range_key() {
1013 let node = agg(json!({
1014 "output_idx": ["county"],
1015 "ranges": {"county": {"from": "county"}, "src": {"from": "sourceType"}},
1016 "join": [{"on": [["src", "sourceType"]]}],
1017 "args": ["activity"]
1018 }));
1019 let model = json!({
1020 "index_sets": {
1021 "county": {"kind": "categorical", "members": ["A"]},
1022 "sourceType": {"kind": "categorical", "members": ["x"]}
1023 },
1024 "equations": [{"lhs": node, "rhs": 0}]
1025 });
1026 let g = build_reference_graph(&model, "M", None).unwrap();
1027 let jf = g.edges_of_kind(EdgeKind::JoinFactor);
1028 assert_eq!(jf.len(), 1);
1029 assert_eq!(jf[0].target, "factor:src");
1030 }
1031
1032 #[test]
1033 fn join_factor_unresolved_errors() {
1034 let node = agg(json!({
1035 "output_idx": ["i"],
1036 "ranges": {"i": {"from": "cells"}},
1037 "join": [{"on": [["ghost", "col"]]}],
1038 "args": ["activity"]
1039 }));
1040 let model = json!({
1041 "index_sets": {"cells": {"kind": "interval", "size": 2}},
1042 "equations": [{"lhs": node, "rhs": 0}]
1043 });
1044 let err = build_reference_graph(&model, "M", None).unwrap_err();
1045 assert!(matches!(
1046 err,
1047 ReferenceResolutionError::UnresolvedJoinFactor { .. }
1048 ));
1049 }
1050
1051 fn four_class_model(on: serde_json::Value) -> Value {
1054 json!({
1055 "index_sets": {"cells": {"kind": "interval", "size": 4}},
1056 "variables": {"bin": {"type": "parameter"}, "w": {"type": "parameter"}},
1057 "equations": [{"lhs": agg(json!({
1058 "id": "j",
1059 "args": ["w"],
1060 "output_idx": [],
1061 "ranges": {"i": {"from": "cells"}},
1062 "join": [{"on": [on]}]
1063 })), "rhs": 0}]
1064 })
1065 }
1066
1067 #[test]
1071 fn join_binder_classes_all_resolve_on_both_columns() {
1072 for on in [
1073 json!(["i", "i"]), json!(["w", "w"]), json!(["bin", "bin"]), json!(["cells", "cells"]), ] {
1078 let model = four_class_model(on.clone());
1079 build_reference_graph(&model, "M", None)
1080 .unwrap_or_else(|e| panic!("join on {on} should resolve: {e}"));
1081 }
1082 }
1083
1084 #[test]
1088 fn join_binder_classes_still_reject_an_undefined_name() {
1089 for on in [
1090 json!(["no_such_name", "bin"]),
1091 json!(["bin", "no_such_name"]),
1092 ] {
1093 let err = build_reference_graph(&four_class_model(on.clone()), "M", None).unwrap_err();
1094 match &err {
1095 ReferenceResolutionError::UnresolvedJoinFactor { factor, .. } => {
1096 assert_eq!(factor, "no_such_name", "for {on}");
1097 }
1098 other => panic!("want UnresolvedJoinFactor for {on}, got {other:?}"),
1099 }
1100 }
1101 }
1102
1103 #[test]
1106 fn graph_is_queryable_topologically() {
1107 let producer = agg(json!({"id": "edge_faq", "output_idx": ["edge"],
1108 "ranges": {"f": {"from": "faces"}}}));
1109 let consumer = agg(json!({"output_idx": ["e"], "ranges": {"e": {"from": "edges"}}}));
1110 let model = json!({
1111 "index_sets": {
1112 "faces": {"kind": "interval", "size": 8},
1113 "edges": {"kind": "derived", "from_faq": "edge_faq"}
1114 },
1115 "equations": [{"lhs": producer, "rhs": 0}, {"lhs": consumer, "rhs": 0}]
1116 });
1117 let g = build_reference_graph(&model, "M", None).unwrap();
1118 let order = g.topological_order().unwrap();
1119 assert_eq!(order.len(), g.vertices.len());
1120 let pos = |k: &str| order.iter().position(|x| x == k).unwrap();
1121 assert!(pos("node:edge_faq") < pos("index_set:edges"));
1122 assert!(g.dependencies("index_set:faces").is_empty());
1123 }
1124
1125 #[test]
1128 fn reference_cycle_is_detected() {
1129 let producer = agg(json!({"id": "edge_faq", "output_idx": ["edge"],
1130 "ranges": {"e": {"from": "edges"}}}));
1131 let model = json!({
1132 "index_sets": {"edges": {"kind": "derived", "from_faq": "edge_faq"}},
1133 "equations": [{"lhs": producer, "rhs": 0}]
1134 });
1135 let g = build_reference_graph(&model, "M", None).unwrap();
1136 let cyc = g.detect_cycle().expect("cycle");
1137 assert_eq!(cyc.first(), cyc.last());
1138 assert!(cyc.contains(&"node:edge_faq".to_string()));
1139 assert!(cyc.contains(&"index_set:edges".to_string()));
1140
1141 let doc = json!({"models": {"M": model}});
1142 let err = resolve_references(&doc).unwrap_err();
1143 assert!(matches!(
1144 err,
1145 ReferenceResolutionError::ReferenceCycle { .. }
1146 ));
1147 }
1148
1149 #[test]
1152 fn no_references_empty_graph() {
1153 let model = json!({
1154 "variables": {"u": {"type": "unknown"}},
1155 "equations": [{"lhs": {"op": "D", "args": ["u"], "wrt": "t"}, "rhs": -1}]
1156 });
1157 let g = build_reference_graph(&model, "M", None).unwrap();
1158 assert!(g.edges.is_empty());
1159 assert!(g.detect_cycle().is_none());
1160 }
1161
1162 #[test]
1163 fn resolve_references_multi_model() {
1164 let m1 = json!({
1165 "index_sets": {"cells": {"kind": "interval", "size": 4}},
1166 "equations": [{"lhs": agg(json!({"output_idx": ["i"], "ranges": {"i": {"from": "cells"}}})), "rhs": 0}]
1167 });
1168 let m2 = json!({"equations": [{"lhs": {"op": "D", "args": ["u"], "wrt": "t"}, "rhs": 0}]});
1169 let doc = json!({"models": {"A": m1, "B": m2}});
1170 let graphs = resolve_references(&doc).unwrap();
1171 assert_eq!(graphs.len(), 2);
1172 assert_eq!(graphs["A"].edges_of_kind(EdgeKind::RangeFrom).len(), 1);
1173 assert!(graphs["B"].edges.is_empty());
1174 }
1175
1176 #[test]
1181 fn document_scoped_index_sets_resolve() {
1182 let node = agg(json!({"output_idx": ["i"], "ranges": {"i": {"from": "cells"}}}));
1183 let doc = json!({
1184 "index_sets": {"cells": {"kind": "interval", "size": 4}},
1185 "models": {"M": {"equations": [{"lhs": node, "rhs": 0}]}}
1186 });
1187 let graphs = resolve_references(&doc).unwrap();
1188 let g = &graphs["M"];
1189 let rf = g.edges_of_kind(EdgeKind::RangeFrom);
1190 assert_eq!(rf.len(), 1);
1191 assert_eq!(rf[0].target, "index_set:cells");
1192 assert!(g.vertices.contains_key("index_set:cells"));
1194 }
1195
1196 #[test]
1197 fn document_and_model_scoped_index_sets_merge() {
1198 let node = agg(json!({
1201 "output_idx": ["i"],
1202 "ranges": {"i": {"from": "cells"}, "f": {"from": "faces"}}
1203 }));
1204 let doc = json!({
1205 "index_sets": {"faces": {"kind": "interval", "size": 8}},
1206 "models": {"M": {
1207 "index_sets": {"cells": {"kind": "interval", "size": 4}},
1208 "equations": [{"lhs": node, "rhs": 0}]
1209 }}
1210 });
1211 let graphs = resolve_references(&doc).unwrap();
1212 let g = &graphs["M"];
1213 assert_eq!(g.edges_of_kind(EdgeKind::RangeFrom).len(), 2);
1214 assert!(g.vertices.contains_key("index_set:faces"));
1215 assert!(g.vertices.contains_key("index_set:cells"));
1216 }
1217
1218 #[test]
1221 fn document_registry_is_a_trailing_argument() {
1222 let node = agg(json!({"output_idx": ["i"], "ranges": {"i": {"from": "cells"}}}));
1223 let model = json!({"equations": [{"lhs": node, "rhs": 0}]});
1224 let registry: Map<String, Value> = json!({"cells": {"kind": "interval", "size": 4}})
1225 .as_object()
1226 .expect("object")
1227 .clone();
1228
1229 let e = build_reference_graph(&model, "M", None).unwrap_err();
1231 assert!(matches!(
1232 e,
1233 ReferenceResolutionError::UndeclaredIndexSet { .. }
1234 ));
1235
1236 let g = build_reference_graph(&model, "M", Some(®istry)).expect("resolves");
1238 assert!(g.vertices.contains_key("index_set:cells"));
1239 assert_eq!(g.edges_of_kind(EdgeKind::RangeFrom).len(), 1);
1240 }
1241}