This PR introduces node discovery logic in `double_representation/node.rs`.
The parser dependency has been bumped, due to #2154.
Improvements in definition discovery — fixed previous issue where first line in block was ignored.


Original commit: 1370aaf431
This commit is contained in:
Michał Wawrzyniec Urbańczyk 2020-03-10 17:54:28 +01:00 committed by GitHub
parent 619eb4fabd
commit 17f729874e
9 changed files with 353 additions and 72 deletions

View File

@ -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<S:Into<Shape<Ast>>>
(shape:S, id:Option<ID>, len:usize) -> Ast {
let shape = shape.into();
/// Just wraps shape, id and len into Ast node.
pub fn from_ast_id_len(shape:Shape<Ast>, id:Option<ID>, 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<S:Into<Shape<Ast>>>
(shape:S, id:Option<ID>, 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<Item=&Ast> {
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<Str: ToString>(name:Str) -> Ast {
let cons = Cons{ name: name.to_string() };
let cons = Cons {name:name.to_string()};
Ast::from(cons)
}
pub fn var<Str: ToString>(name:Str) -> Ast {
let var = Var{ name: name.to_string() };
let var = Var{name:name.to_string()};
Ast::from(var)
}
pub fn opr<Str: ToString>(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<Str0, Str1, Str2>(larg:Str0, opr:Str1, rarg:Str2) -> Ast
/// Creates an AST node with `Infix` shape, where both its operands are Vars.
pub fn infix_var<Str0, Str1, Str2>(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::<Vec<_>>();

View File

@ -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/

View File

@ -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)))));
}

View File

@ -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);

View File

@ -2,4 +2,6 @@
//! module.
pub mod definition;
pub mod graph;
pub mod node;
pub mod text;

View File

@ -54,7 +54,7 @@ pub fn identifier_name(ast:&Ast) -> Option<String> {
/// 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<Option<Ast>>, kind:ScopeKind) -> Option<DefinitionInfo> {
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<Option<Ast>>, kind:ScopeKind) -> Option<DefinitionInfo> {
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<DefinitionInfo> {
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<ast::BlockLine<Option<Ast>>>,
}
impl<'a> GeneralizedBlock<'a> {
/// Wrap `Module` into `GeneralizedBlock`.
pub fn from_module(module:&'a ast::Module<Ast>) -> GeneralizedBlock<'a> {
GeneralizedBlock { kind:ScopeKind::Root, lines:&module.lines }
}
/// Wrap `Block` into `GeneralizedBlock`.
pub fn from_block(block:&'a ast::Block<Ast>) -> 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<dyn Iterator<Item=&'a Ast> + 'a>;
/// Lists all the definitions in the entity.
fn list_definitions(&self) -> Vec<DefinitionInfo> {
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<DefinitionInfo> {
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<DefinitionInfo> {
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<DefinitionInfo> {
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<dyn Iterator<Item=&'a Ast> + 'a> {
Box::new(self.iter())
}
}
impl DefinitionProvider for known::Block {
fn scope_kind() -> ScopeKind { ScopeKind::NonRoot }
fn line_asts<'a>(&'a self) -> Box<dyn Iterator<Item=&'a Ast> + '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);
}
}

View File

@ -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<Ast>,
/// Describes all known nodes in this graph (does not include special pseudo-nodes like graph
/// inputs and outputs).
pub nodes:Vec<node::NodeInfo>,
}
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<node::NodeInfo> {
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<node::NodeInfo> {
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> {
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");
}
}
}

View File

@ -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<NodeInfo> {
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<NodeInfo> {
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<NodeInfo> {
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);
}
}

View File

@ -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;