use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use crate::stack::resolve::{minimal_partial_paths, PartialPath};
use crate::stack::StackGraph;
use crate::NodeId;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[non_exhaustive]
pub struct PartialPathId<'a> {
pub file: &'a str,
pub index: usize,
}
#[derive(Clone, Debug, Default)]
pub struct PartialPathDatabase<'a> {
files: BTreeMap<&'a str, FilePaths<'a>>,
}
#[derive(Clone, Debug)]
struct FilePaths<'a> {
graph: &'a StackGraph,
paths: Vec<PartialPath<'a>>,
exhausted: Vec<NodeId>,
}
impl<'a> PartialPathDatabase<'a> {
pub fn add_file(&mut self, file: &'a str, graph: &'a StackGraph, max_work: usize) {
let found = minimal_partial_paths(graph, max_work);
let mut paths = found.paths;
paths.sort_by_key(|path| path.start);
let exhausted = found.exhausted;
self.files.insert(
file,
FilePaths {
graph,
paths,
exhausted,
},
);
}
pub fn drop_file(&mut self, file: &str) {
self.files.remove(file);
}
#[must_use]
pub fn graph(&self, file: &str) -> Option<&'a StackGraph> {
self.files.get(file).map(|stored| stored.graph)
}
#[must_use]
pub fn path(&self, id: PartialPathId<'_>) -> Option<&PartialPath<'a>> {
self.files.get(id.file)?.paths.get(id.index)
}
pub fn starting_at(
&self,
file: &str,
node: NodeId,
) -> impl Iterator<Item = (PartialPathId<'_>, &PartialPath<'a>)> + '_ {
self.files
.get_key_value(file)
.into_iter()
.flat_map(move |(&file, stored)| {
let first = stored.paths.partition_point(|path| path.start < node);
stored
.paths
.iter()
.enumerate()
.skip(first)
.take_while(move |(_, path)| path.start == node)
.map(move |(index, path)| (PartialPathId { file, index }, path))
})
}
#[must_use]
pub fn exhausted(&self, file: &str) -> &[NodeId] {
self.files
.get(file)
.map_or(&[], |stored| stored.exhausted.as_slice())
}
}
#[cfg(test)]
mod tests {
use super::{PartialPathDatabase, PartialPathId};
use crate::stack::resolve::DEFAULT_MAX_WORK;
use crate::stack::{Definition, StackGraph, StackNode, StackScope, SymbolPop, SymbolPush};
use crate::NodeId;
use alloc::borrow::ToOwned;
use alloc::vec::Vec;
const ROOT: NodeId = NodeId(1);
const REFERENCE: NodeId = NodeId(2);
const SCOPE: NodeId = NodeId(3);
const DEFINITION: NodeId = NodeId(4);
const RELAY: NodeId = NodeId(5);
fn push(symbol: &str, is_reference: bool) -> StackNode {
StackNode::PushSymbol(SymbolPush {
symbol: symbol.to_owned(),
precedence: 0,
is_reference,
})
}
fn node(id: NodeId) -> StackNode {
match id {
ROOT => StackNode::Root,
REFERENCE => push("x", true),
RELAY => push("m", false),
DEFINITION => StackNode::PopSymbol(SymbolPop {
symbol: "x".to_owned(),
precedence: 0,
definition: Some(Definition::default()),
import: None,
}),
_ => StackNode::Scope(StackScope::default()),
}
}
fn graph(edges: &[(NodeId, NodeId)]) -> StackGraph {
let mut graph = StackGraph::new();
for (source, sink) in edges {
graph.add_node(*source, node(*source));
graph.add_node(*sink, node(*sink));
graph.add_edge(*source, *sink, 0);
}
graph
}
fn ends(database: &PartialPathDatabase<'_>, file: &str, start: NodeId) -> Vec<NodeId> {
database
.starting_at(file, start)
.map(|(_, path)| path.end)
.collect()
}
#[test]
fn the_paths_starting_at_a_node_leave_out_those_starting_elsewhere() {
let graph = graph(&[(ROOT, DEFINITION), (REFERENCE, SCOPE), (SCOPE, ROOT)]);
let mut database = PartialPathDatabase::default();
database.add_file("a.py", &graph, DEFAULT_MAX_WORK);
assert_eq!(ends(&database, "a.py", REFERENCE), [ROOT]);
}
#[test]
fn a_node_id_of_one_file_does_not_reach_the_paths_of_another() {
let local = graph(&[(REFERENCE, SCOPE), (SCOPE, DEFINITION)]);
let other = graph(&[(REFERENCE, SCOPE), (SCOPE, ROOT)]);
let mut database = PartialPathDatabase::default();
database.add_file("a.py", &local, DEFAULT_MAX_WORK);
database.add_file("b.py", &other, DEFAULT_MAX_WORK);
assert_eq!(ends(&database, "a.py", REFERENCE), [DEFINITION]);
}
#[test]
fn a_file_never_stored_has_no_paths() {
let database = PartialPathDatabase::default();
assert_eq!(ends(&database, "a.py", REFERENCE), []);
}
#[test]
fn the_id_of_a_path_gives_back_that_path() {
let graph = graph(&[(ROOT, DEFINITION), (REFERENCE, SCOPE), (SCOPE, ROOT)]);
let mut database = PartialPathDatabase::default();
database.add_file("a.py", &graph, DEFAULT_MAX_WORK);
let found = database.starting_at("a.py", REFERENCE);
assert_eq!(
found
.map(|(id, path)| database.path(id) == Some(path))
.collect::<Vec<_>>(),
[true]
);
}
#[test]
fn storing_a_file_again_replaces_its_paths() {
let first = graph(&[(REFERENCE, SCOPE), (SCOPE, DEFINITION)]);
let second = graph(&[(REFERENCE, SCOPE), (SCOPE, ROOT)]);
let mut database = PartialPathDatabase::default();
database.add_file("a.py", &first, DEFAULT_MAX_WORK);
database.add_file("a.py", &second, DEFAULT_MAX_WORK);
assert_eq!(ends(&database, "a.py", REFERENCE), [ROOT]);
}
#[test]
fn the_id_of_a_dropped_file_gives_no_path() {
let graph = graph(&[(REFERENCE, SCOPE), (SCOPE, DEFINITION)]);
let mut database = PartialPathDatabase::default();
database.add_file("a.py", &graph, DEFAULT_MAX_WORK);
let id = PartialPathId {
file: "a.py",
index: 0,
};
database.drop_file("a.py");
assert_eq!(database.path(id), None);
}
#[test]
fn a_file_keeps_the_graph_its_paths_walk() {
let graph = graph(&[(REFERENCE, SCOPE), (SCOPE, DEFINITION)]);
let mut database = PartialPathDatabase::default();
database.add_file("a.py", &graph, DEFAULT_MAX_WORK);
assert_eq!(database.graph("a.py"), Some(&graph));
}
#[test]
fn a_file_keeps_the_endpoints_whose_search_ran_out_of_work() {
let graph = graph(&[(REFERENCE, SCOPE), (SCOPE, RELAY), (RELAY, SCOPE)]);
let mut database = PartialPathDatabase::default();
database.add_file("a.py", &graph, 100);
assert_eq!(database.exhausted("a.py"), [REFERENCE]);
}
}