Double Representation Connection operations (https://github.com/enso-org/ide/pull/387)

Original commit: 1d8c282344
This commit is contained in:
Adam Obuchowicz 2020-04-28 17:48:06 +02:00 committed by GitHub
parent de7683c2a9
commit e6a0461674
15 changed files with 809 additions and 67 deletions

View File

@ -1189,6 +1189,7 @@ dependencies = [
"serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
"shapely 0.1.0",
"span-tree 0.1.0",
"utils 0.1.0",
"uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -22,6 +22,7 @@ file-manager-client = { version = "0.1.0" , path = "file-manager"
json-rpc = { version = "0.1.0" , path = "json-rpc" }
parser = { version = "0.1.0" , path = "parser" }
utils = { version = "0.1.0" , path = "utils" }
span-tree = { version = "0.1.0" , path = "span-tree" }
console_error_panic_hook = { version = "0.1.6" }
failure = { version = "0.1.6" }

View File

@ -104,20 +104,28 @@ struct MismatchedCrumbType;
pub trait IntoCrumbs : IntoIterator<Item:Into<Crumb>> + Sized {
/// Convert to the actual Crumbs structure.
fn into_crumbs(self) -> Crumbs {
self.into_iter().map(|cb| cb.into()).collect()
iter_crumbs(self).collect()
}
}
impl<T:IntoIterator<Item:Into<Crumb>> + Sized> IntoCrumbs for T {}
/// Converts `IntoCrumbs` value into a `Crumb`-yielding iterator.
pub fn iter_crumbs(crumbs:impl IntoCrumbs) -> impl Iterator<Item=Crumb> {
crumbs.into_iter().map(|crumb| crumb.into())
}
/// 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 {
( ) => {
Vec::<$crate::crumbs::Crumb>::new()
};
( $( $x:expr ),* ) => {
vec![$(Crumb::from($x)),*]
vec![$($crate::crumbs::Crumb::from($x)),*]
};
}

View File

@ -37,6 +37,7 @@ pub mod prelude {
use crate::prelude::*;
pub use crumbs::Crumb;
pub use crumbs::Crumbs;
use ast_macros::*;
@ -1114,6 +1115,11 @@ impl Ast {
// TODO smart constructors for other cases
// as part of https://github.com/luna/enso/issues/338
/// Creates Blank ast node (underscore).
pub fn blank() -> Ast {
Ast::from(Blank{})
}
/// Creates an Ast node with Number inside.
pub fn number(number:i64) -> Ast {
let number = Number {base:None,int:number.to_string()};

View File

@ -7,6 +7,7 @@ use crate::SectionLeft;
use crate::SectionRight;
use crate::SectionSides;
use crate::Ast;
use crate::Opr;
use crate::Shape;
use crate::assoc::Assoc;
use crate::crumbs::Crumb;
@ -16,6 +17,7 @@ use crate::crumbs::SectionRightCrumb;
use crate::crumbs::SectionSidesCrumb;
use crate::crumbs::Located;
use crate::known;
use utils::vec::VecExt;
@ -67,6 +69,14 @@ pub fn is_assignment(ast:&Ast) -> bool {
infix.map(|infix| is_assignment_opr(&infix.opr)).unwrap_or(false)
}
/// Obtains a new `Opr` with an assignment.
pub fn assignment() -> known::Opr {
// TODO? We could cache and reuse, if we care.
let name = predefined::ASSIGNMENT.into();
let opr = Opr {name};
known::Opr::new(opr,None)
}
// ===========================
@ -499,5 +509,12 @@ mod tests {
expect_at(&chain.args[1].operand,&a);
}
#[test]
fn assignment_opr_test() {
let opr = assignment();
assert_eq!(opr.name, "=");
assert_eq!(opr.repr(), "=");
}
// TODO[ao] add tests for modifying chain.
}

View File

@ -159,8 +159,8 @@ impl<'a> Implementation for node::Ref<'a> {
fn erase_impl(&self) -> Option<EraseOperation> {
match self.node.kind {
node::Kind::Argument{removable:true} |
node::Kind::Target {removable:true} => Some(Box::new(move |root| {
node::Kind::Argument{removable:true,..} |
node::Kind::Target {removable:true} => Some(Box::new(move |root| {
let parent_crumb = &self.ast_crumbs[..self.ast_crumbs.len()-1];
let ast = root.get_traversing(parent_crumb)?;
let new_ast = if let Some(mut infix) = ast::opr::Chain::try_new(ast) {

View File

@ -42,7 +42,7 @@ impl<'a> Iterator for LeafIterator<'a> {
fn next(&mut self) -> Option<Self::Item> {
if self.next_node.is_some() {
let crumbs = self.stack.iter().map(|sf| sf.child_being_visited);
let return_value = self.base_node.clone().traverse_subnode(crumbs);
let return_value = self.base_node.clone().get_descendant(crumbs);
self.make_dfs_step();
self.descend_to_leaf();
return_value

View File

@ -7,6 +7,7 @@
//! operator chain.
#![feature(associated_type_bounds)]
#![feature(option_result_contains)]
#![feature(trait_alias)]
#![warn(missing_docs)]
#![warn(trivial_casts)]
@ -26,7 +27,6 @@ pub mod builder;
pub use node::Node;
/// Module gathering all commonly used traits for massive importing.
pub mod traits {
pub use crate::action::Actions;
@ -38,13 +38,15 @@ pub mod traits {
/// Common types that should be visible across the whole crate.
pub mod prelude {
pub use crate::traits::*;
pub use ast::traits::*;
pub use enso_prelude::*;
pub use utils::fail::FallibleResult;
}
use prelude::*;
// ================
// === SpanTree ===
// ================
@ -53,7 +55,7 @@ use prelude::*;
///
/// This structure is used to have some specific node marked as root node, to avoid confusion
/// regarding SpanTree crumbs and AST crumbs.
#[derive(Debug,Eq,PartialEq)]
#[derive(Clone,Debug,Eq,PartialEq)]
pub struct SpanTree {
/// A root node of the tree.
pub root : Node

View File

@ -9,11 +9,12 @@ use data::text::Index;
use data::text::Size;
// ====================
// === Helper Types ===
// ====================
// === Kind ===
/// An enum describing kind of node.
#[derive(Copy,Clone,Debug,Eq,PartialEq)]
pub enum Kind {
@ -44,11 +45,16 @@ pub enum Kind {
#[derive(Copy,Clone,Debug,Eq,PartialEq)]
pub enum InsertType {BeforeTarget,AfterTarget,Append}
/// A type which identifies some node in SpanTree. This is essentially a iterator over child
/// indices, so `[4]` means _root's fifth child_, `[4, 2]`means _the third child of root's fifth
/// child_ and so on.
pub trait Crumbs = IntoIterator<Item=usize>;
// === Crumbs ===
/// Identifies subtree within a node. It is the index of the child node.
pub type Crumb = usize;
/// Convert crumbs to crumbs pointing to a parent.
pub fn parent_crumbs(crumbs:&[Crumb]) -> Option<&[Crumb]> {
crumbs.len().checked_sub(1).map(|new_len| &crumbs[..new_len])
}
// === Node ===
@ -56,7 +62,7 @@ pub trait Crumbs = IntoIterator<Item=usize>;
///
/// Each node in SpanTree is bound to some span of code, and potentially may have corresponding
/// AST node.
#[derive(Debug,Eq,PartialEq)]
#[derive(Clone,Debug,Eq,PartialEq)]
#[allow(missing_docs)]
pub struct Node {
pub kind : Kind,
@ -85,7 +91,7 @@ impl Node {
/// A structure which contains `Node` being a child of some parent. It contains some additional
/// data regarding this relation
#[derive(Debug,Eq,PartialEq)]
#[derive(Clone,Debug,Eq,PartialEq)]
pub struct Child {
/// A child node.
pub node : Node,
@ -106,11 +112,20 @@ pub struct Ref<'a> {
/// Span begin being an index counted from the root expression.
pub span_begin : Index,
/// Crumbs specifying this node position related to root. See `Crumbs` docs.
pub crumbs : Vec<usize>,
pub crumbs : Vec<Crumb>,
/// Ast crumbs locating associated AST node, related to the root's AST node.
pub ast_crumbs : ast::Crumbs,
}
/// A result of `get_subnode_by_ast_crumbs`
#[derive(Clone,Debug)]
pub struct NodeFoundByAstCrumbs<'a,'b> {
/// A node being a result of the lookup.
pub node : Ref<'a>,
/// AST crumbs locating the searched AST node inside the AST of found SpanTree node.
pub ast_crumbs : &'b [ast::Crumb],
}
impl<'a> Ref<'a> {
/// Get span of current node.
pub fn span(&self) -> data::text::Span {
@ -146,14 +161,39 @@ impl<'a> Ref<'a> {
}
/// Get the sub-node (child, or further descendant) identified by `crumbs`.
pub fn traverse_subnode(self, crumbs:impl Crumbs) -> Option<Ref<'a>> {
pub fn get_descendant(self, crumbs:impl IntoIterator<Item=Crumb>) -> Option<Ref<'a>> {
let mut iter = crumbs.into_iter();
match iter.next() {
Some(index) => self.child(index).and_then(|child| child.traverse_subnode(iter)),
Some(index) => self.child(index).and_then(|child| child.get_descendant(iter)),
None => Some(self)
}
}
/// Get the sub-node by AST crumbs.
///
/// The returned node will be node having corresponding AST node located by given `ast_crumbs`,
/// or a leaf whose AST _contains_ node located by `ast_crumbs` - in that case returned
/// structure will have non-empty `ast_crumbs` field.
pub fn get_descendant_by_ast_crumbs<'b>
(self, ast_crumbs:&'b [ast::Crumb]) -> Option<NodeFoundByAstCrumbs<'a,'b>> {
if self.node.children.is_empty() || ast_crumbs.is_empty() {
let node = self;
let remaining_ast_crumbs = ast_crumbs;
Some(NodeFoundByAstCrumbs{node, ast_crumbs: remaining_ast_crumbs })
} else {
let mut children = self.node.children.iter();
// Please be advised, that the `ch.ast_crumhs` is not a field of Ref, but Child, and
// therefore have different meaning!
let next = children.find_position(|ch| {
!ch.ast_crumbs.is_empty() && ast_crumbs.starts_with(&ch.ast_crumbs)
});
next.and_then(|(id,child)| {
let ast_subcrumbs = &ast_crumbs[child.ast_crumbs.len()..];
self.child(id).unwrap().get_descendant_by_ast_crumbs(ast_subcrumbs)
})
}
}
/// Get the node which exactly matches the given Span. If there many such node's, it pick first
/// found by DFS.
pub fn find_by_span(self, span:&data::text::Span) -> Option<Ref<'a>> {
@ -179,11 +219,13 @@ mod test {
use crate::builder::TreeBuilder;
use crate::node::Kind::*;
use ast::crumbs::InfixCrumb;
use ast::crumbs;
use crate::node::InsertType;
#[test]
fn traversing_tree() {
use InfixCrumb::*;
fn node_lookup() {
use ast::crumbs::InfixCrumb::*;
let removable = false;
let tree = TreeBuilder::new(7)
.add_leaf (0,1,Target{removable},vec![LeftOperand])
@ -196,10 +238,10 @@ mod test {
.build();
let root = tree.root_ref();
let child1 = root.clone().traverse_subnode(vec![0]).unwrap();
let child2 = root.clone().traverse_subnode(vec![2]).unwrap();
let grand_child1 = root.clone().traverse_subnode(vec![2,0]).unwrap();
let grand_child2 = child2.clone().traverse_subnode(vec![1]).unwrap();
let child1 = root.clone().get_descendant(vec![0]).unwrap();
let child2 = root.clone().get_descendant(vec![2]).unwrap();
let grand_child1 = root.clone().get_descendant(vec![2, 0]).unwrap();
let grand_child2 = child2.clone().get_descendant(vec![1]).unwrap();
// Span begin.
assert_eq!(root.span_begin.value , 0);
@ -230,10 +272,44 @@ mod test {
assert_eq!(grand_child2.ast_crumbs, [RightOperand.into(),Operator.into()] );
// Not existing nodes
assert!(root.clone().traverse_subnode(vec![3]).is_none());
assert!(root.clone().traverse_subnode(vec![1,0]).is_none());
assert!(root.clone().traverse_subnode(vec![2,1,0]).is_none());
assert!(root.clone().traverse_subnode(vec![2,5]).is_none());
assert!(root.traverse_subnode(vec![2,5,0]).is_none());
assert!(root.clone().get_descendant(vec![3]).is_none());
assert!(root.clone().get_descendant(vec![1, 0]).is_none());
assert!(root.clone().get_descendant(vec![2, 1, 0]).is_none());
assert!(root.clone().get_descendant(vec![2, 5]).is_none());
assert!(root.get_descendant(vec![2, 5, 0]).is_none());
}
#[test]
fn node_lookup_by_ast_crumbs() {
use ast::crumbs::BlockCrumb::*;
use ast::crumbs::InfixCrumb::*;
use ast::crumbs::PrefixCrumb::*;
let removable = false;
let tree = TreeBuilder::new(7)
.add_leaf (0,1,Target{removable},vec![LeftOperand])
.add_empty_child(1,InsertType::AfterTarget)
.add_leaf (1,1,Operation,vec![Operator])
.add_child(2,5,Argument{removable},vec![RightOperand])
.add_leaf(0,3,Operation,vec![Func])
.add_leaf(3,1,Target{removable},vec![Arg])
.done()
.build();
let root = tree.root_ref();
let cases:&[(ast::Crumbs,&[usize],ast::Crumbs)] = &
[ (crumbs![LeftOperand] ,&[0] ,crumbs![])
, (crumbs![RightOperand] ,&[3] ,crumbs![])
, (crumbs![RightOperand,Func] ,&[3,0],crumbs![])
, (crumbs![RightOperand,Arg] ,&[3,1],crumbs![])
, (crumbs![RightOperand,Arg,HeadLine],&[3,1],crumbs![HeadLine])
];
for case in cases {
let (crumbs,expected_crumbs,expected_remaining_ast_crumbs) = case;
let result = root.clone().get_descendant_by_ast_crumbs(&crumbs).unwrap();
assert_eq!(result.node.crumbs.as_slice(), *expected_crumbs);
assert_eq!(result.ast_crumbs, expected_remaining_ast_crumbs.as_slice());
}
}
}

View File

@ -3,18 +3,24 @@
//! This controller provides access to a specific graph. It lives under a module controller, as
//! each graph belongs to some module.
use crate::prelude::*;
use crate::double_representation::alias_analysis::NormalizedName;
use crate::double_representation::alias_analysis::LocatedName;
use crate::double_representation::definition;
pub use crate::double_representation::graph::Id;
use crate::double_representation::graph::GraphInfo;
pub use crate::double_representation::graph::LocationHint;
use crate::double_representation::definition;
use crate::double_representation::node;
use crate::double_representation::node::NodeInfo;
use crate::model::module::NodeMetadata;
use crate::notification;
use parser::Parser;
use span_tree::action::Actions;
use span_tree::action::Action;
use span_tree::SpanTree;
use ast::crumbs::InfixCrumb;
// ==============
@ -46,7 +52,7 @@ pub struct FailedToCreateNode;
#[derive(Clone,Debug)]
pub struct Node {
/// Information based on AST, from double_representation module.
pub info : double_representation::node::NodeInfo,
pub info : NodeInfo,
/// Information about this node stored in the module's metadata.
pub metadata : Option<NodeMetadata>,
}
@ -84,6 +90,191 @@ impl NewNodeInfo {
// ===================
// === Connections ===
// ===================
/// Identifier for ports.
pub type Port = Vec<span_tree::node::Crumb>;
// === Endpoint
/// Connection endpoint - a port on a node, described using span-tree crumbs.
#[allow(missing_docs)]
#[derive(Clone,Debug)]
pub struct Endpoint {
pub node : double_representation::node::Id,
pub port : Port,
/// Crumbs which locate the Var in the `port` ast node.
///
/// In normal case this is an empty crumb (which means that the whole span of `port` is the
/// mentioned Var. However, span tree does not covers all the possible ast of node expression
/// (e.g. it does not decompose Blocks), but still we want to pass information about connection
/// to such port and be able to remove it.
pub var_crumbs: ast::Crumbs,
}
impl Endpoint {
/// Create endpoint with empty `var_crumbs`.
pub fn new(node:double_representation::node::Id, port: Port) -> Self {
let var_crumbs = default();
Endpoint{node,port,var_crumbs}
}
}
// === Connection ===
/// Connection described using span-tree crumbs.
#[allow(missing_docs)]
#[derive(Clone,Debug)]
pub struct Connection {
pub source : Endpoint,
pub destination : Endpoint
}
// === NodeTrees ===
/// Stores node's span trees: one for inputs (expression) and optionally another one for inputs
/// (pattern).
#[derive(Clone,Debug)]
pub struct NodeTrees {
/// Describes node inputs, i.e. its expression.
pub inputs : SpanTree,
/// Describes node outputs, i.e. its pattern. `None` if a node is not an assignment.
pub outputs : Option<SpanTree>,
}
impl NodeTrees {
#[allow(missing_docs)]
pub fn new(node:&NodeInfo) -> Option<NodeTrees> {
let inputs = SpanTree::new(node.expression()).ok()?;
let outputs = if let Some(pat) = node.pattern() {
Some(SpanTree::new(pat).ok()?)
} else {
None
};
Some(NodeTrees {inputs,outputs})
}
/// Converts AST crumbs (as obtained from double rep's connection endpoint) into the
/// appriopriate span-tree node reference.
pub fn get_span_tree_node<'a,'b>(&'a self, ast_crumbs:&'b [ast::Crumb])
-> Option<span_tree::node::NodeFoundByAstCrumbs<'a,'b>> {
if let Some(outputs) = self.outputs.as_ref() {
// Node in assignment form. First crumb decides which span tree to use.
let tree = match ast_crumbs.get(0) {
Some(ast::crumbs::Crumb::Infix(InfixCrumb::LeftOperand)) => Some(outputs),
Some(ast::crumbs::Crumb::Infix(InfixCrumb::RightOperand)) => Some(&self.inputs),
_ => None,
};
tree.and_then(|tree| tree.root_ref().get_descendant_by_ast_crumbs(&ast_crumbs[1..]))
} else {
// Expression node - there is only inputs span tree.
self.inputs.root_ref().get_descendant_by_ast_crumbs(ast_crumbs)
}
}
}
// === Connections ===
/// Describes connections in the graph. For convenience also includes information about port
/// structure of the involved nodes.
#[derive(Clone,Debug,Default)]
pub struct Connections {
/// Span trees for all nodes that have connections.
pub trees : HashMap<node::Id,NodeTrees>,
/// The connections between nodes in the graph.
pub connections : Vec<Connection>,
}
impl Connections {
/// Describes a connection for given double representation graph.
pub fn new(graph:&GraphInfo) -> Connections {
let trees = graph.nodes().iter().flat_map(|node| {
Some((node.id(), NodeTrees::new(node)?))
}).collect();
let mut ret = Connections {trees, connections:default()};
let connections = graph.connections().into_iter().flat_map(|c|
ret.convert_connection(&c)
).collect();
ret.connections = connections;
ret
}
/// Converts Endpoint from double representation to the span tree crumbs.
pub fn convert_endpoint
(&self, endpoint:&double_representation::connection::Endpoint) -> Option<Endpoint> {
let tree = self.trees.get(&endpoint.node)?;
let span_tree_node = tree.get_span_tree_node(&endpoint.crumbs)?;
Some(Endpoint{
node : endpoint.node,
port : span_tree_node.node.crumbs,
var_crumbs : span_tree_node.ast_crumbs.into(),
})
}
/// Converts Connection from double representation to the span tree crumbs.
pub fn convert_connection
(&self, connection:&double_representation::connection::Connection) -> Option<Connection> {
Some(Connection {
source : self.convert_endpoint(&connection.source)?,
destination : self.convert_endpoint(&connection.destination)?,
})
}
}
// =================
// === Utilities ===
// =================
/// Suggests a variable name for storing results of the given expression.
///
/// Name will try to express result of an infix operation (`sum` for `a+b`), kind of literal
/// (`number` for `5`) and target function name for prefix chain.
///
/// The generated name is not unique and might collide with already present identifiers.
pub fn name_for_ast(ast:&Ast) -> String {
use ast::*;
match ast.shape() {
Shape::Var (ident) => ident.name.clone(),
Shape::Cons (ident) => ident.name.to_lowercase(),
Shape::Number (_) => "number".into(),
Shape::DanglingBase (_) => "number".into(),
Shape::TextLineRaw (_) => "text".into(),
Shape::TextLineFmt (_) => "text".into(),
Shape::TextBlockRaw (_) => "text".into(),
Shape::TextBlockFmt (_) => "text".into(),
Shape::TextUnclosed (_) => "text".into(),
Shape::Opr (opr) => {
match opr.name.as_ref() {
"+" => "sum",
"*" => "product",
"-" => "difference",
"/" => "quotient",
_ => "operator",
}.into()
}
_ => {
if let Some(infix) = ast::opr::GeneralizedInfix::try_new(ast) {
name_for_ast(infix.opr.ast())
} else if let Some(prefix) = ast::prefix::Chain::try_new(ast) {
name_for_ast(&prefix.func)
} else {
"var".into()
}
}
}
}
// ==================
// === Controller ===
// ==================
@ -126,16 +317,14 @@ impl Handle {
}
/// Returns double rep information about all nodes in the graph.
pub fn all_node_infos
(&self) -> FallibleResult<Vec<double_representation::node::NodeInfo>> {
pub fn all_node_infos(&self) -> FallibleResult<Vec<NodeInfo>> {
let definition = self.graph_definition_info()?;
let graph = double_representation::graph::GraphInfo::from_definition(definition);
Ok(graph.nodes())
}
/// Retrieves double rep information about node with given ID.
pub fn node_info
(&self, id:ast::Id) -> FallibleResult<double_representation::node::NodeInfo> {
pub fn node_info(&self, id:ast::Id) -> FallibleResult<NodeInfo> {
let nodes = self.all_node_infos()?;
let node = nodes.into_iter().find(|node_info| node_info.id() == id);
node.ok_or_else(|| NodeNotFound(id).into())
@ -162,6 +351,128 @@ impl Handle {
Ok(nodes)
}
/// Returns information about all the connections between graph's nodes.
pub fn connections(&self) -> FallibleResult<Connections> {
let definition = self.graph_definition_info()?;
let graph = double_representation::graph::GraphInfo::from_definition(definition);
Ok(Connections::new(&graph))
}
/// Suggests a name for a variable that shall store the node value.
///
/// Analyzes the expression, e.g. result for "a+b" shall be named "sum".
/// The caller should make sure that obtained name won't collide with any symbol usage before
/// actually introducing it. See `variable_name_for`.
pub fn variable_name_base_for(node:&NodeInfo) -> String {
name_for_ast(node.expression())
}
/// Identifiers introduced or referred to in the current graph's scope.
///
/// Introducing identifier not included on this list should have no side-effects on the name
/// resolution in the code in this graph.
pub fn used_names(&self) -> FallibleResult<Vec<LocatedName>> {
let def = self.graph_definition_info()?;
if let Ok(block) = ast::known::Block::try_from(def.body()) {
let usage = double_representation::alias_analysis::analyse_block(&block);
let mut idents = usage.introduced;
idents.extend(usage.used.into_iter());
Ok(idents)
} else {
// TODO[mwu] Even if definition is not a block, it still may use name from outside.
Ok(vec![])
}
}
/// Suggests a variable name for storing results of the given node. Name will get a number
/// appended to avoid conflicts with other identifiers used in the graph.
pub fn variable_name_for(&self, node:&NodeInfo) -> FallibleResult<ast::known::Var> {
let base_name = Self::variable_name_base_for(node);
let unavailable = self.used_names()?.into_iter().filter_map(|name| {
let is_relevant = name.item.starts_with(base_name.as_str());
is_relevant.then(name.item)
}).collect::<HashSet<_>>();
let name = (1..).find_map(|i| {
let candidate = NormalizedName::new(iformat!("{base_name}{i}"));
let available = !unavailable.contains(&candidate);
available.and_option_from(|| Some(candidate.deref().clone()))
}).unwrap(); // It always return a value.
Ok(ast::known::Var::new(ast::Var {name}, None))
}
/// Converts node to an assignment, where the whole value is bound to a single identifier.
/// Modifies the node, discarding any previously set pattern.
/// Returns the identifier with the node's expression value.
pub fn introduce_name_on(&self, id:node::Id) -> FallibleResult<ast::known::Var> {
let node = self.node(id)?;
let name = self.variable_name_for(&node.info)?;
self.update_node(id, |mut node| {
node.set_pattern(name.ast().clone());
node
})?;
Ok(name)
}
// TODO[ao] Clean up the code and make it conform the coding guidelines.
/// Create connection in graph.
pub fn connect(&self, connection:&Connection) -> FallibleResult<()> {
let source_node = self.node_info(connection.source.node)?;
let source_ast = if let Some(pat) = source_node.pattern() {
pat
} else {
self.introduce_name_on(connection.source.node)?;
return self.connect(connection);
};
let source_node_outputs = SpanTree::new(source_ast)?;
let source_crumbs = connection.source.port.iter().copied();
let source_port = source_node_outputs.root_ref().get_descendant(source_crumbs).expect("failed locate crumb");
let source_crumbs = &source_port.ast_crumbs;
let source_identifier = source_ast.get_traversing(source_crumbs)?;
let destination_node = self.node_info(connection.destination.node)?;
let destination_ast = destination_node.expression();
let destination_node_inputs = SpanTree::new(destination_ast)?;
let destination_port = destination_node_inputs.root_ref().get_descendant(connection.destination.port.iter().copied()).unwrap();
let replaced_destination = destination_port.set(destination_ast,source_identifier.clone()).unwrap();
let new_expression = replaced_destination;
self.set_expression_ast(destination_node.id(),new_expression)
}
// TODO[ao] Clean up the code and make it conform the coding guidelines.
/// Remove the connections from the graph.
pub fn disconnect(&self, connection:&Connection) -> FallibleResult<()> {
let destination_node = self.node_info(connection.destination.node)?;
let destination_ast = destination_node.expression();
let destination_node_inputs = SpanTree::new(destination_ast)?;
let destination_port = destination_node_inputs.root_ref().get_descendant(connection.destination.port.iter().copied()).unwrap();
// parent chain
let mut parent_port = span_tree::node::parent_crumbs(&connection.destination.port).map(|cr| destination_node_inputs.root_ref().get_descendant(cr.iter().copied()).unwrap());
while parent_port.as_ref().map_or(false, |p| p.node.kind == span_tree::node::Kind::Chained) {
parent_port = parent_port.and_then(|p| span_tree::node::parent_crumbs(&p.crumbs).map(|cr| destination_node_inputs.root_ref().get_descendant(cr.iter().copied()).unwrap()));
}
let ports_after = parent_port.map(|p| p.chain_children_iter().skip_while(|p| p.crumbs != destination_port.crumbs).skip(1));
let only_empty_ports_after = ports_after.map_or(true, |mut ps| ps.all(|p| p.node.is_empty()));
let replaced_destination = if connection.destination.var_crumbs.is_empty() {
if destination_port.is_action_available(Action::Erase) && only_empty_ports_after {
destination_port.erase(destination_ast)
} else {
destination_port.set(destination_ast,Ast::blank())
}
} else {
let crumbs = destination_port.ast_crumbs.iter().chain(connection.destination.var_crumbs.iter()).cloned().collect_vec();
destination_ast.set_traversing(&crumbs,Ast::blank())
}?;
self.set_expression_ast(destination_node.id(),replaced_destination)
}
/// Updates the AST of the definition of this graph.
pub fn update_definition_ast<F>(&self, f:F) -> FallibleResult<()>
where F:FnOnce(definition::DefinitionInfo) -> FallibleResult<definition::DefinitionInfo> {
@ -225,11 +536,34 @@ impl Handle {
/// Sets the given's node expression.
pub fn set_expression(&self, id:ast::Id, expression_text:impl Str) -> FallibleResult<()> {
trace!(self.logger, "Setting node {id} expression to `{expression_text.as_ref()}`");
//trace!(self.logger, "Setting node {id} expression to `{expression_text.as_ref()}`");
let new_expression_ast = self.parse_node_expression(expression_text)?;
self.set_expression_ast(id,new_expression_ast)
}
/// Sets the given's node expression.
pub fn set_expression_ast(&self, id:ast::Id, expression:Ast) -> FallibleResult<()> {
trace!(self.logger, "Setting node {id} expression to `{expression.repr()}`");
self.update_definition_ast(|definition| {
let mut graph = GraphInfo::from_definition(definition);
graph.edit_node(id,new_expression_ast)?;
graph.edit_node(id,expression)?;
Ok(graph.source)
})?;
Ok(())
}
/// Updates the given node in the definition.
///
/// The function `F` is called with the information with the state of the node so far and
pub fn update_node<F>(&self, id:ast::Id, f:F) -> FallibleResult<()>
where F : FnOnce(NodeInfo) -> NodeInfo {
self.update_definition_ast(|definition| {
let mut graph = GraphInfo::from_definition(definition);
graph.update_node(id,|node| {
let new_node = f(node);
trace!(self.logger, "Setting node {id} line to `{new_node.repr()}`");
Some(new_node)
})?;
Ok(graph.source)
})?;
Ok(())
@ -253,10 +587,12 @@ mod tests {
use super::*;
use crate::double_representation::definition::DefinitionName;
use crate::double_representation::node::NodeInfo;
use crate::executor::test_utils::TestWithLocalPoolExecutor;
use crate::notification;
use ast::HasRepr;
use ast::crumbs;
use data::text::Index;
use data::text::TextChange;
use json_rpc::test_util::transport::mock::MockTransport;
@ -486,4 +822,207 @@ main =
module.expect_code(PROGRAM);
})
}
#[wasm_bindgen_test]
fn graph_controller_connections_listing() {
let mut test = GraphControllerFixture::set_up();
const PROGRAM:&str = r"
main =
x,y = get_pos
print x
z = print $ foo y
print z
foo
print z";
test.run_graph_for_main(PROGRAM, "main", |_, graph| async move {
let connections = graph.connections().unwrap();
let (node0,node1,node2,node3,node4) = graph.nodes().unwrap().expect_tuple();
assert_eq!(node0.info.expression().repr(), "get_pos");
assert_eq!(node1.info.expression().repr(), "print x");
assert_eq!(node2.info.expression().repr(), "print $ foo y");
assert_eq!(node3.info.expression().repr(), "print z");
let c = &connections.connections[0];
assert_eq!(c.source.node, node0.info.id());
assert_eq!(c.source.port, vec![1]);
assert_eq!(c.destination.node, node1.info.id());
assert_eq!(c.destination.port, vec![2]);
let c = &connections.connections[1];
assert_eq!(c.source.node , node0.info.id());
assert_eq!(c.source.port , vec![4]);
assert_eq!(c.destination.node, node2.info.id());
assert_eq!(c.destination.port, vec![4,2]);
let c = &connections.connections[2];
assert_eq!(c.source.node , node2.info.id());
assert_eq!(c.source.port , Vec::<usize>::new());
assert_eq!(c.destination.node, node3.info.id());
assert_eq!(c.destination.port, vec![2]);
use ast::crumbs::*;
let c = &connections.connections[3];
assert_eq!(c.source.node , node2.info.id());
assert_eq!(c.source.port , Vec::<usize>::new());
assert_eq!(c.destination.node, node4.info.id());
assert_eq!(c.destination.port, vec![2]);
assert_eq!(c.destination.var_crumbs, crumbs!(BlockCrumb::HeadLine,PrefixCrumb::Arg));
})
}
#[wasm_bindgen_test]
fn graph_controller_create_connection() {
/// A case for creating connection test. The field's names are short to be able to write
/// nice-to-read table of cases without very long lines (see `let cases` below).
#[derive(Clone,Debug)]
struct Case {
/// A pattern (the left side of assignment operator) of source node.
src : &'static str,
/// An expression of destination node.
dst : &'static str,
/// Crumbs of source and destination ports (i.e. SpanTree nodes)
ports : (&'static [usize],&'static [usize]),
/// Expected destination expression after connecting.
expected : &'static str,
}
impl Case {
fn run(&self) {
let mut test = GraphControllerFixture::set_up();
let main_prefix = format!("main = \n {} = foo\n ",self.src);
let main = format!("{}{}",main_prefix,self.dst);
let expected = format!("{}{}",main_prefix,self.expected);
let this = self.clone();
let (src_port,dst_port) = self.ports;
let src_port = src_port.to_vec();
let dst_port = dst_port.to_vec();
test.run_graph_for_main(main, "main", |_, graph| async move {
let (node0,node1) = graph.nodes().unwrap().expect_tuple();
let source = Endpoint::new(node0.info.id(),src_port.to_vec());
let destination = Endpoint::new(node1.info.id(),dst_port.to_vec());
let connection = Connection{source,destination};
graph.connect(&connection).unwrap();
let new_main = graph.graph_definition_info().unwrap().ast.repr();
assert_eq!(new_main,expected,"Case {:?}",this);
})
}
}
let cases = &
[ Case {src:"x" , dst:"foo" , expected:"x" , ports:(&[] ,&[] )}
, Case {src:"x,y" , dst:"foo a" , expected:"foo y" , ports:(&[4] ,&[2] )}
, Case {src:"Vec x y", dst:"1 + 2 + 3", expected:"x + 2 + 3" , ports:(&[0,2],&[0,1])}
];
for case in cases {
case.run()
}
}
#[wasm_bindgen_test]
fn graph_controller_create_connection_introducing_var() {
let mut test = GraphControllerFixture::set_up();
const PROGRAM:&str = r"main =
calculate
print _
calculate1 = calculate2";
const EXPECTED:&str = r"main =
calculate3 = calculate
print calculate3
calculate1 = calculate2";
test.run_graph_for_main(PROGRAM, "main", |_, graph| async move {
assert!(graph.connections().unwrap().connections.is_empty());
let (node0,node1,_) = graph.nodes().unwrap().expect_tuple();
let connection_to_add = Connection {
source : Endpoint {
node : node0.info.id(),
port : vec![],
var_crumbs: vec![]
},
destination : Endpoint {
node : node1.info.id(),
port : vec![2], // `_` in `print _`
var_crumbs: vec![]
}
};
graph.connect(&connection_to_add).unwrap();
let new_main = graph.graph_definition_info().unwrap().ast.repr();
assert_eq!(new_main,EXPECTED);
})
}
#[wasm_bindgen_test]
fn suggested_names() {
let parser = Parser::new_or_panic();
let cases = [
("a+b", "sum"),
("a-b", "difference"),
("a*b", "product"),
("a/b", "quotient"),
("read 'foo.csv'","read"),
("Read 'foo.csv'","read"),
("574", "number"),
("'Hello'", "text"),
("'Hello", "text"),
("\"Hello\"", "text"),
("\"Hello", "text"),
];
for (code,expected_name) in &cases {
let ast = parser.parse_line(*code).unwrap();
let node = NodeInfo::from_line_ast(&ast).unwrap();
let name = Handle::variable_name_base_for(&node);
assert_eq!(&name,expected_name);
}
}
#[wasm_bindgen_test]
fn disconnect() {
#[derive(Clone,Debug)]
struct Case {
dest_node_expr : &'static str,
dest_node_expected : &'static str,
}
impl Case {
fn run(&self) {
let mut test = GraphControllerFixture::set_up();
const MAIN_PREFIX:&str = "main = \n in = foo\n ";
let main = format!("{}{}",MAIN_PREFIX,self.dest_node_expr);
let expected = format!("{}{}",MAIN_PREFIX,self.dest_node_expected);
let this = self.clone();
test.run_graph_for_main(main,"main",|_,graph| async move {
let connections = graph.connections().unwrap();
let connection = connections.connections.first().unwrap();
graph.disconnect(connection).unwrap();
let new_main = graph.graph_definition_info().unwrap().ast.repr();
assert_eq!(new_main,expected,"Case {:?}",this);
})
}
}
let cases = &
[ Case {dest_node_expr:"foo in" , dest_node_expected:"foo _" }
, Case {dest_node_expr:"foo in a" , dest_node_expected:"foo _ a" }
, Case {dest_node_expr:"foo a in" , dest_node_expected:"foo a" }
, Case {dest_node_expr:"in + a" , dest_node_expected:"_ + a" }
, Case {dest_node_expr:"a + in" , dest_node_expected:"a + _" }
, Case {dest_node_expr:"in + b + c" , dest_node_expected:"_ + b + c" }
, Case {dest_node_expr:"a + in + c" , dest_node_expected:"a + _ + c" }
, Case {dest_node_expr:"a + b + in" , dest_node_expected:"a + b" }
, Case {dest_node_expr:"in , a" , dest_node_expected:"_ , a" }
, Case {dest_node_expr:"a , in" , dest_node_expected:"a , _" }
, Case {dest_node_expr:"in , b , c" , dest_node_expected:"_ , b , c" }
, Case {dest_node_expr:"a , in , c" , dest_node_expected:"a , _ , c" }
, Case {dest_node_expr:"a , b , in" , dest_node_expected:"a , b" }
, Case {dest_node_expr:"f\n bar a in", dest_node_expected: "f\n bar a _"}
];
for case in cases {
case.run();
}
}
}

View File

@ -27,6 +27,7 @@ pub type LocatedName = Located<NormalizedName>;
/// The identifier name normalized to a lower-case (as the comparisons are case-insensitive).
/// Implements case-insensitive compare with AST.
#[derive(Clone,Debug,Display,Hash,PartialEq,Eq)]
#[derive(Shrinkwrap)]
pub struct NormalizedName(String);
impl NormalizedName {
@ -40,6 +41,12 @@ impl NormalizedName {
pub fn try_from_ast(ast:&Ast) -> Option<NormalizedName> {
ast::identifier::name(ast).map(NormalizedName::new)
}
/// Is the given string a prefix of this name.
pub fn starts_with(&self, name:impl Str) -> bool {
let prefix = NormalizedName::new(name);
self.0.starts_with(prefix.0.as_str())
}
}
/// Tests if Ast is identifier that might reference the same name (case insensitive match).

View File

@ -137,7 +137,7 @@ impl<'a> IdentifierValidator<'a> {
/// Marks given identifier as checked.
pub fn validate_identifier(&mut self, name:&NormalizedName) {
let err = iformat!("{self.name}: unexpected identifier `{name}` validated");
let used = self.validations.get_mut(&name).expect(&err);
let used = self.validations.get_mut(name).expect(&err);
*used = HasBeenValidated::Yes;
}

View File

@ -77,7 +77,7 @@ pub fn list_block(block:&ast::Block<Ast>) -> Vec<Connection> {
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 source = introduced_names.get(&name.item).cloned()?;
let destination = Endpoint::new_in_block(block,name.crumbs)?;
Some(Connection {source,destination})
}).collect()
@ -113,7 +113,6 @@ mod tests {
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 {

View File

@ -132,33 +132,47 @@ impl GraphInfo {
/// Removes the node from graph.
pub fn remove_node(&mut self, node_id:ast::Id) -> FallibleResult<()> {
self.update_node(node_id, |_| None)
}
/// Sets a new state for the node. The id of the described node must denote already existing
/// node.
pub fn set_node(&mut self, node:&NodeInfo) -> FallibleResult<()> {
self.update_node(node.id(), |_| Some(node.clone()))
}
/// Sets a new state for the node. The id of the described node must denote already existing
/// node.
pub fn update_node(&mut self, id:ast::Id, f:impl FnOnce(NodeInfo) -> Option<NodeInfo>) -> FallibleResult<()> {
let mut lines = self.source.block_lines()?;
lines.drain_filter(|line| {
let node = line.elem.as_ref().and_then(NodeInfo::from_line_ast);
let removed_node = node.filter(|node| node.id() == node_id);
removed_node.is_some()
let node_entry = lines.iter().enumerate().find_map(|(index,line)| {
let node = line.elem.as_ref().and_then(NodeInfo::from_line_ast);
let filtered = node.filter(|node| node.id() == id);
filtered.map(|node| (index,node))
});
if lines.is_empty() {
self.source.set_body_ast(Self::empty_graph_body())
if let Some((index,node_info)) = node_entry {
if let Some(updated_node) = f(node_info) {
lines[index].elem = Some(updated_node.ast().clone_ref());
} else {
lines.remove(index);
}
if lines.is_empty() {
self.source.set_body_ast(Self::empty_graph_body());
Ok(())
} else {
self.source.set_block_lines(lines)
}
} else {
self.source.set_block_lines(lines)?
Err(IdNotFound {id}.into())
}
Ok(())
}
/// Sets expression of the given node.
pub fn edit_node(&mut self, node_id:ast::Id, new_expression:Ast) -> FallibleResult<()> {
let mut lines = self.source.block_lines()?;
let node_entry = lines.iter().enumerate().find_map(|(index,line)| {
let node = line.elem.as_ref().and_then(NodeInfo::from_line_ast);
let filtered = node.filter(|node| node.id() == node_id);
filtered.map(|node| (index,node))
});
if let Some((index,mut node)) = node_entry {
self.update_node(node_id, |mut node| {
node.set_expression(new_expression);
lines[index].elem = Some(node.ast().clone_ref());
}
self.source.set_block_lines(lines)
Some(node)
})
}
#[cfg(test)]
@ -382,7 +396,10 @@ main =
assert_eq!(nodes[0].expression().repr(), "3 + 17");
let expected_code = "main =\n bar = 3 + 17";
graph.expect_code(expected_code)
graph.expect_code(expected_code);
assert!(graph.remove_node(uuid::Uuid::new_v4()).is_err());
graph.expect_code(expected_code);
}
#[wasm_bindgen_test]
@ -431,6 +448,9 @@ main =
let expected_code = r#"main =
foo = print "HELLO"
bar = 3 + 17"#;
graph.expect_code(expected_code)
graph.expect_code(expected_code);
assert!(graph.edit_node(uuid::Uuid::new_v4(), Ast::var("foo")).is_err());
graph.expect_code(expected_code);
}
}

View File

@ -5,7 +5,6 @@ use crate::prelude::*;
use ast::Ast;
use ast::crumbs::Crumbable;
use ast::known;
/// Node Id is the Ast Id attached to the node's expression.
pub type Id = ast::Id;
@ -77,6 +76,14 @@ impl NodeInfo {
}
}
/// AST of the node's pattern (assignment's left-hand side).
pub fn pattern(&self) -> Option<&Ast> {
match self {
NodeInfo::Binding {infix} => Some(&infix.larg),
NodeInfo::Expression{..} => None,
}
}
/// Mutable AST of the node's expression. Maintains ID.
pub fn set_expression(&mut self, expression:Ast) {
let id = self.id();
@ -96,6 +103,35 @@ impl NodeInfo {
NodeInfo::Expression{ast} => ast,
}
}
/// Set the pattern (left side of assignment) for node. If it is an Expression node, the
/// assignment infix will be introduced.
pub fn set_pattern(&mut self, pattern:Ast) {
match self {
NodeInfo::Binding {infix} => {
// Setting infix operand never fails.
infix.update_shape(|infix| infix.larg = pattern)
}
NodeInfo::Expression {ast} => {
let infix = ast::Infix {
larg : pattern,
loff : 1,
opr : Ast::opr("="),
roff : 1,
rarg : ast.clone(),
};
let infix = known::Infix::new(infix, None);
*self = NodeInfo::Binding {infix};
}
}
}
}
impl ast::HasTokens for NodeInfo {
fn feed_to(&self, consumer:&mut impl ast::TokenConsumer) {
self.ast().feed_to(consumer)
}
}
@ -108,6 +144,8 @@ impl NodeInfo {
mod tests {
use super::*;
use ast::opr::predefined::ASSIGNMENT;
fn expect_node(ast:Ast, expression_text:&str, id:Id) {
let node_info = NodeInfo::from_line_ast(&ast).expect("expected a node");
assert_eq!(node_info.expression().repr(),expression_text);
@ -154,11 +192,39 @@ mod tests {
let id = Id::new_v4();
let number = ast::Number { base:None, int: "4".into()};
let larg = Ast::var("foo");
let loff = 1;
let opr = Ast::opr("=");
let roff = 1;
let rarg = Ast::new(number, Some(id));
let ast = Ast::new(ast::Infix {larg,loff,opr,roff,rarg}, None);
let ast = Ast::infix(larg,ASSIGNMENT,rarg);
expect_node(ast,"4",id);
}
#[test]
fn setting_pattern_on_expression_node_test() {
let id = uuid::Uuid::new_v4();
let line_ast = Ast::number(2).with_id(id);
let mut node = NodeInfo::from_line_ast(&line_ast).unwrap();
assert_eq!(node.repr(), "2");
assert_eq!(node.id(),id);
node.set_pattern(Ast::var("foo"));
assert_eq!(node.repr(), "foo = 2");
assert_eq!(node.id(),id);
}
#[test]
fn setting_pattern_on_binding_node_test() {
let id = uuid::Uuid::new_v4();
let larg = Ast::var("foo");
let rarg = Ast::var("bar").with_id(id);
let line_ast = Ast::infix(larg,ASSIGNMENT,rarg);
let mut node = NodeInfo::from_line_ast(&line_ast).unwrap();
assert_eq!(node.repr(), "foo = bar");
assert_eq!(node.id(),id);
node.set_pattern(Ast::var("baz"));
assert_eq!(node.repr(), "baz = bar");
assert_eq!(node.id(),id);
}
}