ref #2203

Original commit: c1c68bf6a0
This commit is contained in:
Michał Wawrzyniec Urbańczyk 2020-04-20 21:42:05 +02:00 committed by GitHub
parent 5a8d1a9c05
commit 6f3a328c23
10 changed files with 322 additions and 54 deletions

View File

@ -80,6 +80,14 @@ struct MismatchedCrumbType;
/// Sequence of `Crumb`s describing traversal path through AST.
pub type Crumbs = Vec<Crumb>;
/// Helper macro. Behaves like `vec!` but converts each element into `Crumb`.
#[macro_export]
macro_rules! crumbs {
( $( $x:expr ),* ) => {
vec![$(Crumb::from($x)),*]
};
}
/// Crumb identifies location of child AST in an AST node. Allows for a single step AST traversal.
/// The enum variants are paired with Shape variants. For example, `ModuleCrumb` allows obtaining
/// (or setting) `Ast` stored within a `Module` shape.
@ -94,21 +102,21 @@ pub type Crumbs = Vec<Crumb>;
// === InvalidSuffix ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub struct InvalidSuffixCrumb;
// === TextLineFmt ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub struct TextLineFmtCrumb {pub segment_index:usize}
// === TextBlockFmt ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub struct TextBlockFmtCrumb {
pub text_line_index : usize,
pub segment_index : usize
@ -118,7 +126,7 @@ pub struct TextBlockFmtCrumb {
// === TextUnclosed ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub struct TextUnclosedCrumb {
pub text_line_crumb : TextLineFmtCrumb
}
@ -127,7 +135,7 @@ pub struct TextUnclosedCrumb {
// === Prefix ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub enum PrefixCrumb {
Func,
Arg
@ -137,7 +145,7 @@ pub enum PrefixCrumb {
// === Infix ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub enum InfixCrumb {
LeftOperand,
Operator,
@ -148,7 +156,7 @@ pub enum InfixCrumb {
// === SectionLeft ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub enum SectionLeftCrumb {
Arg,
Opr
@ -158,7 +166,7 @@ pub enum SectionLeftCrumb {
// === SectionRight ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub enum SectionRightCrumb {
Opr,
Arg
@ -168,21 +176,21 @@ pub enum SectionRightCrumb {
// === SectionSides ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub struct SectionSidesCrumb;
// === Module ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub struct ModuleCrumb {pub line_index:usize}
// === Block ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub enum BlockCrumb {
/// The first non-empty line in block.
HeadLine,
@ -194,14 +202,14 @@ pub enum BlockCrumb {
// === Import ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub struct ImportCrumb {pub index:usize}
// === Mixfix ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub enum MixfixCrumb {
Name {index:usize},
Args {index:usize}
@ -211,14 +219,14 @@ pub enum MixfixCrumb {
// === Group ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub struct GroupCrumb;
// === Def ===
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub enum DefCrumb {
Name,
Args {index:usize},
@ -281,7 +289,7 @@ macro_rules! impl_crumbs {
}
/// Crumb identifies location of child AST in an AST node. Allows for a single step AST traversal.
#[derive(Clone,Copy,Debug,PartialEq,Eq,Hash)]
#[derive(Clone,Copy,Debug,PartialEq,Eq,PartialOrd,Ord,Hash)]
#[allow(missing_docs)]
pub enum Crumb {
$($id($crumb_id),)*
@ -879,7 +887,7 @@ pub fn non_empty_line_indices<'a, T:'a>
// ===============
/// Item which location is identified by `Crumbs`.
#[derive(Clone,Debug,Shrinkwrap,PartialEq,Eq,Hash)]
#[derive(Clone,Debug,Shrinkwrap,PartialEq,Eq,PartialOrd,Ord,Hash)]
pub struct Located<T> {
/// Crumbs from containing parent.
pub crumbs : Crumbs,

View File

@ -32,7 +32,7 @@ pub struct Handle {
impl Handle {
/// Create a new project controller.
///
/// The remote connections should be already established.
/// The remote connection should be already established.
pub fn new(file_manager_transport:impl Transport + 'static) -> Self {
Handle {
file_manager : fmc::Handle::new(file_manager_transport),

View File

@ -2,11 +2,14 @@
//! module.
pub mod alias_analysis;
pub mod connection;
pub mod definition;
pub mod graph;
pub mod node;
pub mod text;
#[cfg(test)]
pub mod test_utils;
// ==============

View File

@ -119,7 +119,7 @@ impl Scope {
#[derive(Clone,Debug,Default)]
pub struct AliasAnalyzer {
/// Root scope for this analyzer.
root_scope : Scope,
pub root_scope : Scope,
/// Stack of scopes that shadow the root one.
shadowing_scopes : Vec<Scope>,
/// Stack of context. Lack of any context information is considered non-pattern context.
@ -229,7 +229,7 @@ impl AliasAnalyzer {
}
/// Processes all subtrees of the given AST in their respective locations.
fn process_subtrees(&mut self, ast:&Ast) {
pub fn process_subtrees(&mut self, ast:&impl Crumbable) {
for (crumb,ast) in ast.enumerate() {
self.process_subtree_at(crumb, ast)
}
@ -241,7 +241,7 @@ impl AliasAnalyzer {
pub fn process_ast(&mut self, ast:&Ast) {
if let Some(definition) = DefinitionInfo::from_line_ast(&ast,ScopeKind::NonRoot,default()) {
// If AST looks like definition, we disregard its arguments and body, as they cannot
// form connections in the analyzed graph. However, we need to record the name, because
// form connection in the analyzed graph. However, we need to record the name, because
// it may shadow identifier from parent scope.
let name = NormalizedName::new(definition.name.name);
self.record_identifier(OccurrenceKind::Introduced,name);
@ -320,12 +320,19 @@ impl AliasAnalyzer {
/// Describes identifiers that nodes introduces into the graph and identifiers from graph's scope
/// that node uses. This logic serves as a base for connection discovery.
pub fn analyse_identifier_usage(node:&NodeInfo) -> IdentifierUsage {
pub fn analyse_node(node:&NodeInfo) -> IdentifierUsage {
let mut analyzer = AliasAnalyzer::new();
analyzer.process_ast(node.ast());
analyzer.root_scope.symbols
}
/// Describes variable usage within a given code block.
pub fn analyse_block(block:&ast::Block<Ast>) -> IdentifierUsage {
let mut analyzer = AliasAnalyzer::default();
analyzer.process_subtrees(block);
analyzer.root_scope.symbols
}
// =============
@ -353,7 +360,7 @@ mod tests {
println!("Case: {}",&case.code);
let ast = parser.parse_line(&case.code).unwrap();
let node = NodeInfo::from_line_ast(&ast).unwrap();
let result = analyse_identifier_usage(&node);
let result = analyse_node(&node);
println!("Analysis results: {:?}", result);
validate_identifiers("introduced",&node, case.expected_introduced, &result.introduced);
validate_identifiers("used", &node, case.expected_used, &result.used);

View File

@ -5,9 +5,9 @@ use crate::prelude::*;
use crate::double_representation::alias_analysis::NormalizedName;
use crate::double_representation::alias_analysis::LocatedName;
use crate::double_representation::node::NodeInfo;
use crate::double_representation::test_utils::MarkdownProcessor;
use regex::Captures;
use regex::Match;
use regex::Regex;
use regex::Replacer;
@ -75,30 +75,11 @@ const USED:&str="used";
/// * accumulates spans of introduced and used identifiers.
#[derive(Debug,Default)]
struct MarkdownReplacer {
markdown_bytes_consumed : usize,
processor:MarkdownProcessor,
/// Spans in the unmarked code.
introduced : Vec<Range<usize>>,
introduced:Vec<Range<usize>>,
/// Spans in the unmarked code.
used : Vec<Range<usize>>,
}
impl MarkdownReplacer {
fn marked_to_unmarked_index(&self, i:usize) -> usize {
assert!(self.markdown_bytes_consumed <= i);
i - self.markdown_bytes_consumed
}
/// Increments the consumed marker bytes count by size of a single marker character.
fn consume_marker(&mut self) {
self.markdown_bytes_consumed += '«'.len_utf8();
}
/// Consumes opening and closing marker. Returns span of marked item in unmarked text indices.
fn consume_marked(&mut self, capture:&Match) -> Range<usize> {
self.consume_marker();
let start = self.marked_to_unmarked_index(capture.start());
let end = self.marked_to_unmarked_index(capture.end());
self.consume_marker();
start .. end
}
used:Vec<Range<usize>>,
}
// Processes every single match for a marked entity.
@ -112,13 +93,11 @@ impl Replacer for MarkdownReplacer {
panic!("Unexpected capture: expected named capture `{}` or `{}`.",INTRODUCED,USED)
};
let span = self.consume_marked(&matched);
let out_vec = match kind {
Kind::Introduced => &mut self.introduced,
Kind::Used => &mut self.used,
let span = self.processor.process_match(captures,&matched,dst);
match kind {
Kind::Introduced => self.introduced.push(span),
Kind::Used => self.used.push(span),
};
out_vec.push(span);
dst.push_str(matched.as_str());
}
}

View File

@ -0,0 +1,209 @@
//! Code related to connection discovery and operations.
use crate::prelude::*;
use crate::double_representation::alias_analysis::analyse_block;
use crate::double_representation::alias_analysis::NormalizedName;
use crate::double_representation::node::Id;
use crate::double_representation::node::NodeInfo;
use ast::crumbs::Crumb;
use ast::crumbs::Crumbs;
use crate::double_representation::definition::{DefinitionInfo, ScopeKind};
// ================
// === Endpoint ===
// ================
/// A connection endpoint.
#[derive(Clone,Debug,PartialEq)]
pub struct Endpoint {
/// Id of the node where the endpoint is located.
pub node : Id,
/// Crumbs to the AST creating this endpoint. These crumbs are relative to the node's AST,
/// not just expression, if the node is binding, there'll crumb for left/right operand first.
pub crumbs : Crumbs,
}
impl Endpoint {
/// First crumb identifies line in a given block, i.e. the node. Remaining crumbs identify
/// AST within the node's AST.
///
/// Returns None if first crumb is not present or does not denote a valid node.
fn new_in_block(block:&ast::Block<Ast>, mut crumbs:Crumbs) -> Option<Endpoint> {
let line_crumb = crumbs.pop_front()?;
let line_crumb = match line_crumb {
Crumb::Block(block_crumb) => Some(block_crumb),
_ => None,
}?;
let line_ast = block.get(&line_crumb).ok()?;
let definition = DefinitionInfo::from_line_ast(&line_ast,ScopeKind::NonRoot,block.indent);
let is_non_def = definition.is_none();
let node = is_non_def.and_option_from(|| NodeInfo::from_line_ast(&line_ast))?.id();
Some(Endpoint { node, crumbs })
}
}
/// Connection source, i.e. the port generating the data / identifier introducer.
pub type Source = Endpoint;
/// Connection destination, i.e. the port receiving data / identifier user.
pub type Destination = Endpoint;
// ==================
// === Connection ===
// ==================
/// Describes a connection between two endpoints: from `source` to `destination`.
#[allow(missing_docs)]
#[derive(Clone,Debug,PartialEq)]
pub struct Connection {
pub source:Source,
pub destination:Destination,
}
/// Lists all the connection in the graph for the given code block.
pub fn list_block(block:&ast::Block<Ast>) -> Vec<Connection> {
let identifiers = analyse_block(block);
let introduced_iter = identifiers.introduced.into_iter();
type NameMap = HashMap<NormalizedName,Endpoint>;
let introduced_names = introduced_iter.flat_map(|name| {
let endpoint = Endpoint::new_in_block(block,name.crumbs)?;
Some((name.item,endpoint))
}).collect::<NameMap>();
identifiers.used.into_iter().flat_map(|name| {
// If name is both introduced and used in the graph's scope; and both of these occurrences
// can be represented as endpoints, then we have a connection.
let source = introduced_names.get(&name).cloned()?;
let destination = Endpoint::new_in_block(block,name.crumbs)?;
Some(Connection {source,destination})
}).collect()
}
/// Lists all the connection in the single-expression definition body.
pub fn list_expression(_ast:&Ast) -> Vec<Connection> {
// At this points single-expression graphs do not have any connection.
// This will change when there will be input/output pseudo-nodes.
vec![]
}
/// Lists connections in the given definition body. For now it only makes sense for block shape.
pub fn list(body:&Ast) -> Vec<Connection> {
match body.shape() {
ast::Shape::Block(block) => list_block(block),
_ => list_expression(body),
}
}
// =============
// === Tests ===
// =============
#[cfg(test)]
mod tests {
use super::*;
use parser::Parser;
use crate::double_representation::definition::DefinitionInfo;
use crate::double_representation::graph::GraphInfo;
use ast::crumbs;
use ast::crumbs::Crumb;
use ast::crumbs::InfixCrumb;
struct TestRun {
graph : GraphInfo,
connections : Vec<Connection>
}
impl TestRun {
fn from_definition(definition:DefinitionInfo) -> TestRun {
let graph = GraphInfo::from_definition(definition.clone());
let repr_of = |connection:&Connection| {
let endpoint = &connection.source;
let node = graph.find_node(endpoint.node).unwrap();
let ast = node.ast().get_traversing(&endpoint.crumbs).unwrap();
ast.repr()
};
let mut connections = graph.connections();
connections.sort_by(|lhs,rhs| {
repr_of(&lhs).cmp(&repr_of(&rhs))
});
TestRun {graph,connections}
}
fn from_main_def(code:impl Str) -> TestRun {
let parser = Parser::new_or_panic();
let module = parser.parse_module(code,default()).unwrap();
let definition = DefinitionInfo::from_root_line(&module.lines[0]).unwrap();
Self::from_definition(definition)
}
fn from_block(code:impl Str) -> TestRun {
let body = code.as_ref().lines().map(|line| format!(" {}", line.trim())).join("\n");
let definition_code = format!("main =\n{}",body);
Self::from_main_def(definition_code)
}
fn endpoint_node_repr(&self, endpoint:&Endpoint) -> String {
self.graph.find_node(endpoint.node).unwrap().ast().clone().repr()
}
}
#[wasm_bindgen_test]
pub fn connection_listing_test_plain() {
use InfixCrumb::LeftOperand;
use InfixCrumb::RightOperand;
let code_block = r"
d,e = p
a = d
b = e
c = a + b
fun a = a b
f = fun 2";
let run = TestRun::from_block(code_block);
let c = &run.connections[0];
assert_eq!(run.endpoint_node_repr(&c.source), "a = d");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "c = a + b");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand,LeftOperand]);
let c = &run.connections[1];
assert_eq!(run.endpoint_node_repr(&c.source), "b = e");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "c = a + b");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand,RightOperand]);
let c = &run.connections[2];
assert_eq!(run.endpoint_node_repr(&c.source), "d,e = p");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand,LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "a = d");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand]);
let c = &run.connections[3];
assert_eq!(run.endpoint_node_repr(&c.source), "d,e = p");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand,RightOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "b = e");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand]);
// Note that line `fun a = a b` des not introduce any connections, as it is a definition.
assert_eq!(run.connections.len(),4);
}
#[wasm_bindgen_test]
pub fn inline_definition() {
let run = TestRun::from_main_def("main = a");
assert!(run.connections.is_empty());
}
}

View File

@ -268,6 +268,13 @@ impl DefinitionInfo {
})
}
/// Tries to interpret a root line (i.e. the AST being placed in a line directly in the module
/// scope) as a definition.
pub fn from_root_line(line:&ast::BlockLine<Option<Ast>>) -> Option<DefinitionInfo> {
let indent = 0;
Self::from_line_ast(line.elem.as_ref()?,ScopeKind::Root,indent)
}
/// Tries to interpret `Line`'s `Ast` as a function definition.
///
/// Assumes that the AST represents the contents of line (and not e.g. right-hand side of

View File

@ -10,6 +10,7 @@ use ast::Ast;
use ast::BlockLine;
use ast::known;
use utils::fail::FallibleResult;
use crate::double_representation::connection::Connection;
/// Graph uses the same `Id` as the definition which introduces the graph.
pub type Id = double_representation::definition::Id;
@ -83,6 +84,11 @@ impl GraphInfo {
Self::from_function_binding(self.source.ast.clone())
}
/// Gets the list of connections between the nodes in this graph.
pub fn connections(&self) -> Vec<Connection> {
double_representation::connection::list(&self.source.ast.rarg)
}
fn is_node_by_id(line:&BlockLine<Option<Ast>>, id:ast::Id) -> bool {
let node_info = line.elem.as_ref().and_then(NodeInfo::from_line_ast);
let id_matches = node_info.map(|node| node.id() == id);
@ -113,6 +119,11 @@ impl GraphInfo {
self.source.set_block_lines(lines)
}
/// Locates a node with the given id.
pub fn find_node(&self,id:ast::Id) -> Option<NodeInfo> {
self.nodes().iter().find(|node| node.id() == id).cloned()
}
/// After removing last node, we want to insert a placeholder value for definition value.
/// This defines its AST. Currently it is just `Nothing`.
pub fn empty_graph_body() -> Ast {

View File

@ -3,11 +3,11 @@
use crate::prelude::*;
use ast::Ast;
use ast::Id;
use ast::crumbs::Crumbable;
use ast::known;
/// Node Id is the Ast Id attached to the node's expression.
pub type Id = ast::Id;
// ================
// === NodeInfo ===

View File

@ -0,0 +1,44 @@
//! General-purpose utilities for creating tests for double representation.
use crate::prelude::*;
use regex::Captures;
use regex::Match;
/// Helper type for markdown-defined test cases with `regex` library.
/// When implementing a `Replacer`, on each match the `process_match` should be called.
#[derive(Clone,Copy,Debug,Default)]
pub struct MarkdownProcessor {
markdown_bytes_consumed : usize,
}
impl MarkdownProcessor {
/// Convert index from marked to unmarked code.
fn marked_to_unmarked_index(&self, i:usize) -> usize {
assert!(self.markdown_bytes_consumed <= i);
i - self.markdown_bytes_consumed
}
/// Convert indices range from marked to unmarked code.
fn marked_to_unmarked_range(&self, range:Range<usize>) -> Range<usize> {
Range {
start : self.marked_to_unmarked_index(range.start),
end : self.marked_to_unmarked_index(range.end),
}
}
/// Assumes that given match is the part of capture that should be passed to the dst string.
/// Appends the `body` match contents to the `dst` and returns its span in unmarked text.
/// All characters in the capture that do not belong to `body` are considered markdown.
pub fn process_match
(&mut self, captures:&Captures, body:&Match, dst:&mut String) -> Range<usize> {
let whole_match = captures.get(0).expect("Capture 0 should always be present.");
let bytes_to_body = body.start() - whole_match.start();
let bytes_after_body = whole_match.end() - body.end();
self.markdown_bytes_consumed += bytes_to_body;
let ret = self.marked_to_unmarked_range(body.range());
self.markdown_bytes_consumed += bytes_after_body;
dst.push_str(body.as_str());
ret
}
}