diff --git a/gui/lib/ide/ast/impl/src/lib.rs b/gui/lib/ide/ast/impl/src/lib.rs index c2564ea0f19..b2256348b81 100644 --- a/gui/lib/ide/ast/impl/src/lib.rs +++ b/gui/lib/ide/ast/impl/src/lib.rs @@ -188,15 +188,20 @@ impl Ast { Ast::new_with_length(shape,id,length) } - /// As `new` but sets given declared length for the shape. - pub fn new_with_length>> - (shape:S, id:Option, len:usize) -> Ast { - let shape = shape.into(); + /// Just wraps shape, id and len into Ast node. + pub fn from_ast_id_len(shape:Shape, id:Option, len:usize) -> Ast { let with_length = WithLength { wrapped:shape , len }; let with_id = WithID { wrapped:with_length, id }; Ast { wrapped: Rc::new(with_id) } } + /// As `new` but sets given declared length for the shape. + pub fn new_with_length>> + (shape:S, id:Option, len:usize) -> Ast { + let shape = shape.into(); + Self::from_ast_id_len(shape,id,len) + } + /// Iterates over all transitive child nodes (including self). pub fn iter_recursive(&self) -> impl Iterator { internal::iterate_subtree(self) @@ -895,18 +900,23 @@ impl Ast { // TODO smart constructors for other cases // as part of https://github.com/luna/enso/issues/338 + pub fn number(number:i64) -> Ast { + let number = Number {base:None,int:number.to_string()}; + Ast::from(number) + } + pub fn cons(name:Str) -> Ast { - let cons = Cons{ name: name.to_string() }; + let cons = Cons {name:name.to_string()}; Ast::from(cons) } pub fn var(name:Str) -> Ast { - let var = Var{ name: name.to_string() }; + let var = Var{name:name.to_string()}; Ast::from(var) } pub fn opr(name:Str) -> Ast { - let opr = Opr{ name: name.to_string() }; + let opr = Opr{name:name.to_string() }; Ast::from(opr) } @@ -916,7 +926,8 @@ impl Ast { Ast::from(opr) } - pub fn infix(larg:Str0, opr:Str1, rarg:Str2) -> Ast + /// Creates an AST node with `Infix` shape, where both its operands are Vars. + pub fn infix_var(larg:Str0, opr:Str1, rarg:Str2) -> Ast where Str0: ToString , Str1: ToString , Str2: ToString { @@ -1148,7 +1159,7 @@ mod tests { _ => "«invalid»".to_string(), }; - let infix = Ast::infix("foo", "+", "bar"); + let infix = Ast::infix_var("foo", "+", "bar"); let strings = infix.iter().map(to_string); let strings = strings.collect::>(); diff --git a/gui/lib/ide/parser/build.rs b/gui/lib/ide/parser/build.rs index d8e71b8b882..35c93d8a1f6 100644 --- a/gui/lib/ide/parser/build.rs +++ b/gui/lib/ide/parser/build.rs @@ -24,7 +24,7 @@ use std::path::PathBuf; const PARSER_PATH: &str = "./pkg/scala-parser.js"; /// Commit from `enso` repository that will be used to obtain parser from. -const PARSER_COMMIT: &str = "083fa0e4a50c62cf5d33b06710f5294441729a9e"; +const PARSER_COMMIT: &str = "658bc26800eb03a2995af72df5a79ef21f7eb349"; /// Magic code that needs to be prepended to ScalaJS generated parser due to: /// https://github.com/scala-js/scala-js/issues/3677/ diff --git a/gui/lib/ide/parser/tests/web.rs b/gui/lib/ide/parser/tests/web.rs index 285096581c8..6bff1c85fed 100644 --- a/gui/lib/ide/parser/tests/web.rs +++ b/gui/lib/ide/parser/tests/web.rs @@ -20,8 +20,8 @@ fn web_test() { let mut parser = Parser::new_or_panic(); - let mut parse = |input| { - let span = Span::from((0,5)); + let mut parse = |input:&str| { + let span = Span::from((0,input.len())); let ids = IdMap(vec![(span,uuid)]); let ast = parser.parse(String::from(input), ids).unwrap().wrapped; @@ -37,10 +37,10 @@ fn web_test() { let app_x_y = ast::Prefix {func: Ast::var("x"), off: 3, arg: Ast::var("y")}; - + let var_xy = ast::Var { name: "xy".into() }; assert_eq!(parse(""), line(None)); - assert_eq!(parse("xy"), line(Some(Ast::var("xy")))); + assert_eq!(parse("xy"), line(Some(Ast::new(var_xy, Some(uuid))))); assert_eq!(parse("x y"), line(Some(Ast::new(app_x_y, Some(uuid))))); } diff --git a/gui/lib/ide/src/controller/module.rs b/gui/lib/ide/src/controller/module.rs index 0b1220eceb5..c6275c2437f 100644 --- a/gui/lib/ide/src/controller/module.rs +++ b/gui/lib/ide/src/controller/module.rs @@ -201,10 +201,12 @@ mod test { let uuid1 = Uuid::new_v4(); let uuid2 = Uuid::new_v4(); + let uuid3 = Uuid::new_v4(); let code = "2+2"; let id_map = IdMap(vec! [ (Span::new(Index::new(0), Size::new(1)),uuid1.clone()) , (Span::new(Index::new(2), Size::new(1)),uuid2) + , (Span::new(Index::new(0), Size::new(3)),uuid3) ]); let controller = Handle::new_mock(location,code,id_map,file_manager,parser).unwrap(); @@ -223,7 +225,7 @@ mod test { opr : Ast::new(ast::Opr {name:"+".to_string()}, None), roff : 0, rarg : Ast::new(ast::Number{base:None, int:"2".to_string()}, Some(uuid2)), - }, None)), + }, Some(uuid3))), off: 0 }] }, None); diff --git a/gui/lib/ide/src/double_representation.rs b/gui/lib/ide/src/double_representation.rs index fd85553f4b0..82f7b807baa 100644 --- a/gui/lib/ide/src/double_representation.rs +++ b/gui/lib/ide/src/double_representation.rs @@ -2,4 +2,6 @@ //! module. pub mod definition; +pub mod graph; +pub mod node; pub mod text; diff --git a/gui/lib/ide/src/double_representation/definition.rs b/gui/lib/ide/src/double_representation/definition.rs index 72490bebb50..7f80c9d5c24 100644 --- a/gui/lib/ide/src/double_representation/definition.rs +++ b/gui/lib/ide/src/double_representation/definition.rs @@ -54,7 +54,7 @@ pub fn identifier_name(ast:&Ast) -> Option { /// Structure representing definition name. If this is an extension method, extended type is /// also included. -#[derive(Clone,Debug,PartialEq)] +#[derive(Clone,Debug,Eq,Hash,PartialEq)] pub struct DefinitionName { /// Used when definition is an extension method. Then it stores the segments /// of the extended target type path. @@ -124,34 +124,42 @@ impl DefinitionInfo { pub fn body(&self) -> Ast { self.ast.rarg.clone() } -} -/// Tries to interpret `Line`'s `Ast` as a function definition. -pub fn get_definition_info -(line:&ast::BlockLine>, kind:ScopeKind) -> Option { - let ast = opr::to_assignment(line.elem.as_ref()?)?; + /// Tries to interpret `Line`'s contents as a function definition. + pub fn from_line + (line:&ast::BlockLine>, kind:ScopeKind) -> Option { + let ast = line.elem.as_ref()?; + Self::from_line_ast(ast,kind) + } - // There two cases - function name is either a Var or operator. - // If this is a Var, we have Var, optionally under a Prefix chain with args. - // If this is an operator, we have SectionRight with (if any prefix in arguments). - let lhs = prefix::Chain::new_non_strict(&ast.larg); - let name = DefinitionName::from_ast(&lhs.func)?; - let args = lhs.args; - let ret = DefinitionInfo {ast,name,args}; + /// 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 + /// some binding or other kind of subtree). + pub fn from_line_ast(ast:&Ast, kind:ScopeKind) -> Option { + let infix = opr::to_assignment(ast)?; + // There two cases - function name is either a Var or operator. + // If this is a Var, we have Var, optionally under a Prefix chain with args. + // If this is an operator, we have SectionRight with (if any prefix in arguments). + let lhs = prefix::Chain::new_non_strict(&infix.larg); + let name = DefinitionName::from_ast(&lhs.func)?; + let args = lhs.args; + let ret = DefinitionInfo {ast:infix,name,args}; - // Note [Scope Differences] - if kind == ScopeKind::NonRoot { - // 1. Not an extension method but setter. - let is_setter = !ret.name.extended_target.is_empty(); - // 2. No explicit args -- this is a node, not a definition. - let is_node = ret.args.is_empty(); - if is_setter || is_node { - None + // Note [Scope Differences] + if kind == ScopeKind::NonRoot { + // 1. Not an extension method but setter. + let is_setter = !ret.name.extended_target.is_empty(); + // 2. No explicit args -- this is a node, not a definition. + let is_node = ret.args.is_empty(); + if is_setter || is_node { + None + } else { + Some(ret) + } } else { Some(ret) } - } else { - Some(ret) } } @@ -165,40 +173,55 @@ pub fn get_definition_info // this parameter). In definition, this is just a node (evaluated expression). -/// Either ast::Block or Module's root contents. -#[derive(Clone,Debug)] -pub struct GeneralizedBlock<'a> { - /// If this is a root-scope (module) or nested scope. - pub kind : ScopeKind, - /// Lines placed directly in this scope. - pub lines : &'a Vec>>, -} -impl<'a> GeneralizedBlock<'a> { - /// Wrap `Module` into `GeneralizedBlock`. - pub fn from_module(module:&'a ast::Module) -> GeneralizedBlock<'a> { - GeneralizedBlock { kind:ScopeKind::Root, lines:&module.lines } - } - /// Wrap `Block` into `GeneralizedBlock`. - pub fn from_block(block:&'a ast::Block) -> GeneralizedBlock<'a> { - GeneralizedBlock { kind:ScopeKind::NonRoot, lines:&block.lines } +// ========================== +// === DefinitionProvider === +// ========================== + +/// An entity that contains lines that we want to interpret as definitions. +pub trait DefinitionProvider { + /// What kind of scope this is. + fn scope_kind() -> ScopeKind; + + /// Iterates over non-empty lines' ASTs. + fn line_asts<'a>(&'a self) -> Box + 'a>; + + /// Lists all the definitions in the entity. + fn list_definitions(&self) -> Vec { + self.line_asts().flat_map(|ast| { + DefinitionInfo::from_line_ast(ast,Self::scope_kind()) + }).collect() } - /// Returns information about all definition defined in this block. - pub fn list_definitions(&self) -> Vec { - self.lines.iter().flat_map(|ast| get_definition_info(ast,self.kind)).collect() - } - - /// Goes through definitions introduced in this block and returns one with matching name. - pub fn find_definition(&self, name:&DefinitionName) -> Option { - self.lines.iter().find_map(|ast| { - let definition = get_definition_info(ast, self.kind)?; + /// Tries to find definition by given name in the entity. + fn find_definition(&self, name:&DefinitionName) -> Option { + self.line_asts().find_map(|ast| { + let definition = DefinitionInfo::from_line_ast(ast, Self::scope_kind())?; let matches = &definition.name == name; matches.as_some(definition) - }) + })} +} + +impl DefinitionProvider for known::Module { + fn scope_kind() -> ScopeKind { ScopeKind::Root } + fn line_asts<'a>(&'a self) -> Box + 'a> { + Box::new(self.iter()) } } +impl DefinitionProvider for known::Block { + fn scope_kind() -> ScopeKind { ScopeKind::NonRoot } + fn line_asts<'a>(&'a self) -> Box + 'a> { + Box::new(self.iter()) + } +} + + + +// ============= +// === Tests === +// ============= + #[cfg(test)] mod tests { use super::*; @@ -247,27 +270,26 @@ mod tests { let expected_def_names_in_def = vec!["add", "*"]; // === Program with defnitions in root === - let program = definition_lines.join("\n"); - let module = parser.parse_module(program.into(), default()).unwrap(); - let block = GeneralizedBlock::from_module(&*module); - let definitions = block.list_definitions(); + let program = definition_lines.join("\n"); + let module = parser.parse_module(program.into(), default()).unwrap(); + let definitions = module.list_definitions(); assert_eq_strings(to_names(&definitions),expected_def_names_in_module); // Check that definition can be found and their body is properly described. let add_name = DefinitionName::new_plain("add"); - let add = block.find_definition(&add_name).expect("failed to find `add` function"); + let add = module.find_definition(&add_name).expect("failed to find `add` function"); let body = known::Number::try_new(add.body()).expect("add body should be a Block"); assert_eq!(body.int,"50"); // === Program with definition in `some_func`'s body `Block` === let indented_lines = definition_lines.iter().map(indented).collect_vec(); let program = format!("some_func arg1 arg2 =\n{}", indented_lines.join("\n")); - let root_block = parser.parse_module(program,default()).unwrap(); - let root_defs = GeneralizedBlock::from_module(&*root_block).list_definitions(); + let module = parser.parse_module(program,default()).unwrap(); + let root_defs = module.list_definitions(); let (only_def,) = root_defs.expect_tuple(); assert_eq!(&only_def.name.to_string(),"some_func"); let body_block = known::Block::try_from(only_def.body()).unwrap(); - let nested_defs = GeneralizedBlock::from_block(&body_block).list_definitions(); + let nested_defs = body_block.list_definitions(); assert_eq_strings(to_names(&nested_defs),expected_def_names_in_def); } } diff --git a/gui/lib/ide/src/double_representation/graph.rs b/gui/lib/ide/src/double_representation/graph.rs new file mode 100644 index 00000000000..394b60ef219 --- /dev/null +++ b/gui/lib/ide/src/double_representation/graph.rs @@ -0,0 +1,133 @@ +//! Code for retrieving graph description from AST. + +use crate::prelude::*; + +use crate::double_representation::definition; +use crate::double_representation::node; + +use ast::Ast; +use ast::known; + + + +// ================= +// === GraphInfo === +// ================= + +/// Description of the graph, based on information available in AST. +#[derive(Clone,Debug)] +pub struct GraphInfo { + name : definition::DefinitionName, + args : Vec, + /// Describes all known nodes in this graph (does not include special pseudo-nodes like graph + /// inputs and outputs). + pub nodes:Vec, +} + +impl GraphInfo { + /// Describe graph of the given definition. + pub fn from_definition(info:&definition::DefinitionInfo) -> GraphInfo { + let name = info.name.clone(); + let args = info.args.clone(); + let nodes = Self::from_function_binding(info.ast.clone()); + GraphInfo {name,args,nodes} + } + + /// Lists nodes in the given binding's ast (infix expression). + fn from_function_binding(ast:known::Infix) -> Vec { + let body = ast.rarg.clone(); + if let Ok(body_block) = known::Block::try_new(body.clone()) { + block_nodes(&body_block) + } else { + expression_node(body) + } + } +} + + + +// ===================== +// === Listing nodes === +// ===================== + +/// Collects information about nodes in given code `Block`. +pub fn block_nodes(ast:&known::Block) -> Vec { + ast.iter().flat_map(|line_ast| { + // If this can be a definition, then don't treat it as a node. + match definition::DefinitionInfo::from_line_ast(line_ast, definition::ScopeKind::NonRoot) { + None => node::NodeInfo::from_line_ast(line_ast), + Some(_) => None + } + }).collect() +} + +/// Collects information about nodes in given trivial definition body. +pub fn expression_node(ast:Ast) -> Vec { + node::NodeInfo::new_expression(ast).into_iter().collect() +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + use crate::double_representation::definition::DefinitionName; + use crate::double_representation::definition::DefinitionProvider; + + use parser::api::IsParser; + use wasm_bindgen_test::wasm_bindgen_test; + + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + /// Takes a program with main definition in root and returns main's graph. + fn main_graph(parser:&mut impl IsParser, program:impl Str) -> GraphInfo { + let module = parser.parse_module(program.into(), default()).unwrap(); + let name = DefinitionName::new_plain("main"); + let main = module.find_definition(&name).unwrap(); + println!("{:?}",module); + GraphInfo::from_definition(&main) + } + + #[wasm_bindgen_test] + fn detect_a_node() { + let mut parser = parser::Parser::new_or_panic(); + // Each of these programs should have a `main` definition with a single `2+2` node. + let programs = vec![ + "main = 2+2", + "main = \n 2+2", + "main = \n foo = 2+2", + "main = \n foo = 2+2\n bar b = 2+2", // `bar` is a definition, not a node + ]; + for program in programs { + let graph = main_graph(&mut parser, program); + assert_eq!(graph.nodes.len(), 1); + let node = &graph.nodes[0]; + assert_eq!(node.expression_text(), "2+2"); + let _ = node.id(); // just to make sure it is available + } + } + + #[wasm_bindgen_test] + fn multiple_node_graph() { + let mut parser = parser::Parser::new_or_panic(); + let program = r" +main = + foo = node + foo a = not_node + node +"; + // TODO [mwu] + // Add case like `Int.= a = node` once https://github.com/luna/enso/issues/565 is fixed + + let graph = main_graph(&mut parser, program); + assert_eq!(graph.nodes.len(), 2); + for node in graph.nodes.iter() { + assert_eq!(node.expression_text(), "node"); + } + } +} diff --git a/gui/lib/ide/src/double_representation/node.rs b/gui/lib/ide/src/double_representation/node.rs new file mode 100644 index 00000000000..5419ea4e992 --- /dev/null +++ b/gui/lib/ide/src/double_representation/node.rs @@ -0,0 +1,110 @@ +//! Code for node discovery and other node-related tasks. + +use ast::Ast; +use ast::HasRepr; +use ast::ID; +use ast::known; + + + +// ================ +// === NodeInfo === +// ================ + +/// Description of the node that consists of all information locally available about node. +/// Nodes are required to bear IDs. This enum should never contain an ast of node without id set. +#[derive(Clone,Debug)] +#[allow(missing_docs)] +pub enum NodeInfo { + /// Code with assignment, e.g. `foo = 2 + 2` + Binding { infix: known::Infix }, + /// Code without assignment (no variable binding), e.g. `2 + 2`. + Expression { ast: Ast }, +} + +impl NodeInfo { + /// Tries to interpret the whole binding as a node. Right-hand side will become node's + /// expression. + pub fn new_binding(infix:known::Infix) -> Option { + infix.rarg.id?; + Some(NodeInfo::Binding {infix}) + } + + /// Tries to interpret AST as node, treating whole AST as an expression. + pub fn new_expression(ast:Ast) -> Option { + ast.id?; + Some(NodeInfo::Expression {ast}) + } + + /// Tries to interpret AST as node, treating whole AST as an expression. + pub fn from_line_ast(ast:&Ast) -> Option { + if let Some(infix) = ast::opr::to_assignment(ast) { + Self::new_binding(infix) + } else { + Self::new_expression(ast.clone()) + } + } + + /// Node's unique ID. + pub fn id(&self) -> ID { + // Panic must not happen, as the only available constructor checks that + // there is an ID present. + self.expression_ast().id.expect("Node AST must bear an ID") + } + + /// AST of the node's expression. + pub fn expression_ast(&self) -> &Ast { + match self { + NodeInfo::Binding {infix} => &infix.rarg, + NodeInfo::Expression{ast} => &ast, + } + } + + /// The node's expression textual representation. + pub fn expression_text(&self) -> String { + self.expression_ast().repr() + } +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + use wasm_bindgen_test::wasm_bindgen_test; + + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + 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_text(),expression_text); + assert_eq!(node_info.id(), id); + } + + #[wasm_bindgen_test] + fn expression_node_test() { + // expression: `4` + let id = ID::new_v4(); + let ast = Ast::new(ast::Number { base:None, int: "4".into()}, Some(id)); + expect_node(ast,"4",id); + } + + #[wasm_bindgen_test] + fn binding_node_test() { + // expression: `foo = 4` + 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); + expect_node(ast,"4",id); + } +} diff --git a/gui/lib/ide/src/lib.rs b/gui/lib/ide/src/lib.rs index 09e52a8e6ef..4ff0e540cb5 100644 --- a/gui/lib/ide/src/lib.rs +++ b/gui/lib/ide/src/lib.rs @@ -27,6 +27,7 @@ pub mod prelude { pub use crate::constants; pub use crate::controller; + pub use crate::double_representation; pub use crate::executor; pub use futures::Future;