serde_generate/analyzer.rs
1// Copyright (c) Facebook, Inc. and its affiliates
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use serde_reflection::{ContainerFormat, Format, FormatHolder, Registry, Result};
5use std::collections::{BTreeMap, BTreeSet, HashSet};
6
7/// Compute dependencies while ignoring external names.
8fn get_dependencies<'a>(
9 format: &'a ContainerFormat,
10 external: &BTreeSet<String>,
11) -> Result<BTreeSet<&'a str>> {
12 let mut result = BTreeSet::new();
13 format.visit(&mut |format| {
14 if let Format::TypeName(x) = format {
15 if !external.contains(x) {
16 result.insert(x.as_str());
17 }
18 }
19 Ok(())
20 })?;
21 Ok(result)
22}
23
24/// Build a map of dependencies between the entries of a `Registry`.
25/// * By definition, an entry named `x` depends on `y` iff the container format of `x` in the registry
26/// syntactically contains a reference to `y` (i.e. an expression `Format::TypeName(y)`).
27/// * Dependencies can play a role in code generation in some languages (e.g. Rust or C++) where inductive
28/// definitions may require explicit "boxing" (i.e. adding pointer indirections) to ensure finite object sizes.
29pub fn get_dependency_map(registry: &Registry) -> Result<BTreeMap<&str, BTreeSet<&str>>> {
30 get_dependency_map_with_external_dependencies(registry, &BTreeSet::new())
31}
32
33/// Same as get_dependency_map but allow to specify a set of externally-provided names to ignore.
34pub fn get_dependency_map_with_external_dependencies<'a>(
35 registry: &'a Registry,
36 external: &BTreeSet<String>,
37) -> Result<BTreeMap<&'a str, BTreeSet<&'a str>>> {
38 let mut children = BTreeMap::new();
39 for (name, format) in registry {
40 children.insert(name.as_str(), get_dependencies(format, external)?);
41 }
42 Ok(children)
43}
44
45/// Classic topological sorting algorithm except that it doesn't abort in case of cycles.
46pub fn best_effort_topological_sort<T>(children: &BTreeMap<T, BTreeSet<T>>) -> Vec<T>
47where
48 T: Clone + std::cmp::Ord + std::cmp::Eq + std::hash::Hash,
49{
50 // Build the initial queue so that we pick up nodes with less children first (and otherwise
51 // those with smaller key first).
52 // This is a heuristic to break cycles preferably at large nodes (see comment below).
53 let mut queue: Vec<_> = children.keys().rev().cloned().collect();
54 queue.sort_by(|node1, node2| children[node1].len().cmp(&children[node2].len()));
55
56 let mut result = Vec::new();
57 // Nodes already inserted in result.
58 let mut sorted = HashSet::new();
59 // Nodes for which children have been enqueued.
60 let mut seen = HashSet::new();
61
62 while let Some(node) = queue.pop() {
63 if sorted.contains(&node) {
64 continue;
65 }
66 if seen.contains(&node) {
67 // Second time we see this node.
68 // * If `node` does not belong to a cycle in the graph, then by now, all its children
69 // have been sorted.
70 // * If `node` has children that depend back on it. We may be visiting `node` again
71 // before some of those children. Just insert `node` here. By ignoring edges going back
72 // to `node` now, we are effectively deciding to "break the cycle" there in future
73 // applications (e.g. `node` may be forward-declared in C++ and `Box`-ed in Rust).
74 sorted.insert(node.clone());
75 result.push(node);
76 continue;
77 }
78 // First time we see this node:
79 // 1. Mark it so that it is no longer enqueued.
80 seen.insert(node.clone());
81 // 2. Schedule all the (yet unseen) children then this node for a second visit.
82 // (If possible, visit children by increasing key.)
83 queue.push(node.clone());
84 for child in children[&node].iter().rev() {
85 if !seen.contains(child) {
86 queue.push(child.clone());
87 }
88 }
89 }
90 result
91}