mirror of
https://github.com/enso-org/enso.git
synced 2024-12-21 06:11:35 +03:00
Double Representation Connection operations (https://github.com/enso-org/ide/pull/387)
Original commit: 1d8c282344
This commit is contained in:
parent
de7683c2a9
commit
e6a0461674
1
gui/src/rust/Cargo.lock
generated
1
gui/src/rust/Cargo.lock
generated
@ -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)",
|
||||
|
@ -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" }
|
||||
|
@ -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)),*]
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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()};
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user