Port graph editor to new AST (#4113)

Use the Rust parser rather than the Scala parser to parse Enso code in the IDE.

Implements:
- https://www.pivotaltracker.com/story/show/182975925
- https://www.pivotaltracker.com/story/show/182988419
- https://www.pivotaltracker.com/story/show/182970096
- https://www.pivotaltracker.com/story/show/182973659
- https://www.pivotaltracker.com/story/show/182974161
- https://www.pivotaltracker.com/story/show/182974205

There is additional functionality needed before the transition is fully-completed, however I think it's time for this to see review and testing, so I've opened separate issues. In rough order of urgency (these issues are also linked from the corresponding disabled tests):
- #5573
- #5571
- #5572
- #5574

# Important Notes
The implementation is based partly on translation, and partly on new analysis. Method- and operator-related shapes are translated to the old `Ast` variants, so that all the analysis applied to them doesn't need to be ported at this time. Everything else (mostly "macros" in the old AST) is implemented with new analysis.
This commit is contained in:
Kaz Wesley 2023-02-10 10:05:40 -08:00 committed by GitHub
parent b56d6d74b9
commit d1af25793a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 2813 additions and 5454 deletions

1293
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ resolver = "2"
# where plausible. # where plausible.
members = [ members = [
"app/gui", "app/gui",
"app/gui/language/parser",
"app/gui/enso-profiler-enso-data", "app/gui/enso-profiler-enso-data",
"build/cli", "build/cli",
"build/macros", "build/macros",

View File

@ -32,10 +32,11 @@ ensogl-hardcoded-theme = { path = "../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-drop-manager = { path = "../../lib/rust/ensogl/component/drop-manager" } ensogl-drop-manager = { path = "../../lib/rust/ensogl/component/drop-manager" }
fuzzly = { path = "../../lib/rust/fuzzly" } fuzzly = { path = "../../lib/rust/fuzzly" }
ast = { path = "language/ast/impl" } ast = { path = "language/ast/impl" }
parser = { path = "language/parser" }
parser-scala = { path = "language/parser-scala" }
ide-view = { path = "view" } ide-view = { path = "view" }
engine-protocol = { path = "controller/engine-protocol" } engine-protocol = { path = "controller/engine-protocol" }
json-rpc = { path = "../../lib/rust/json-rpc" } json-rpc = { path = "../../lib/rust/json-rpc" }
parser-scala = { path = "language/parser" }
span-tree = { path = "language/span-tree" } span-tree = { path = "language/span-tree" }
bimap = { version = "0.4.0" } bimap = { version = "0.4.0" }
console_error_panic_hook = { workspace = true } console_error_panic_hook = { workspace = true }

View File

@ -9,8 +9,8 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
ast = { path = "../../language/ast/impl" } ast = { path = "../../language/ast/impl" }
parser = { path = "../../language/parser" }
engine-protocol = { path = "../engine-protocol" } engine-protocol = { path = "../engine-protocol" }
parser-scala = { path = "../../language/parser" }
enso-data-structures = { path = "../../../../lib/rust/data-structures" } enso-data-structures = { path = "../../../../lib/rust/data-structures" }
enso-prelude = { path = "../../../../lib/rust/prelude" } enso-prelude = { path = "../../../../lib/rust/prelude" }
enso-profiler = { path = "../../../../lib/rust/profiler" } enso-profiler = { path = "../../../../lib/rust/profiler" }

View File

@ -240,13 +240,6 @@ impl AliasAnalyzer {
self.process_assignment(&assignment); self.process_assignment(&assignment);
} else if let Some(lambda) = ast::macros::as_lambda(ast) { } else if let Some(lambda) = ast::macros::as_lambda(ast) {
self.process_lambda(&lambda); self.process_lambda(&lambda);
} else if let Ok(macro_match) = ast::known::Match::try_from(ast) {
// Macros (except for lambdas which were covered in the previous check) never introduce
// new scopes or different context. We skip the keywords ("if" in "if-then-else" is not
// an identifier) and process the matched subtrees as usual.
self.process_given_subtrees(macro_match.shape(), macro_match.iter_pat_match_subcrumbs())
} else if let Ok(ambiguous) = ast::known::Ambiguous::try_from(ast) {
self.process_given_subtrees(ambiguous.shape(), ambiguous.iter_pat_match_subcrumbs())
} else if self.is_in_pattern() { } else if self.is_in_pattern() {
// We are in the pattern (be it a lambda's or assignment's left side). Three options: // We are in the pattern (be it a lambda's or assignment's left side). Three options:
// 1) This is a destructuring pattern match using infix syntax, like `head,tail`. // 1) This is a destructuring pattern match using infix syntax, like `head,tail`.
@ -371,8 +364,6 @@ mod tests {
use super::test_utils::*; use super::test_utils::*;
use super::*; use super::*;
wasm_bindgen_test_configure!(run_in_browser);
/// Checks if actual observed sequence of located identifiers matches the expected one. /// Checks if actual observed sequence of located identifiers matches the expected one.
/// Expected identifiers are described as code spans in the node's text representation. /// Expected identifiers are described as code spans in the node's text representation.
fn validate_identifiers( fn validate_identifiers(
@ -386,7 +377,7 @@ mod tests {
} }
/// Runs the test for the given test case description. /// Runs the test for the given test case description.
fn run_case(parser: &parser_scala::Parser, case: Case) { fn run_case(parser: &parser::Parser, case: Case) {
debug!("\n===========================================================================\n"); debug!("\n===========================================================================\n");
debug!("Case: {}", case.code); debug!("Case: {}", case.code);
let ast = parser.parse_line_ast(&case.code).unwrap(); let ast = parser.parse_line_ast(&case.code).unwrap();
@ -397,15 +388,15 @@ mod tests {
} }
/// Runs the test for the test case expressed using markdown notation. See `Case` for details. /// Runs the test for the test case expressed using markdown notation. See `Case` for details.
fn run_markdown_case(parser: &parser_scala::Parser, marked_code: impl AsRef<str>) { fn run_markdown_case(parser: &parser::Parser, marked_code: impl AsRef<str>) {
debug!("Running test case for {}", marked_code.as_ref()); debug!("Running test case for {}", marked_code.as_ref());
let case = Case::from_markdown(marked_code.as_ref()); let case = Case::from_markdown(marked_code.as_ref());
run_case(parser, case) run_case(parser, case)
} }
#[wasm_bindgen_test] #[test]
fn test_alias_analysis() { fn test_alias_analysis() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let test_cases = [ let test_cases = [
"»foo«", "»foo«",
"«five» = 5", "«five» = 5",
@ -433,21 +424,11 @@ mod tests {
"»A« -> »b«", "»A« -> »b«",
"a -> »A« -> a", "a -> »A« -> a",
"a -> a -> »A«", "a -> a -> »A«",
"x»,«y -> »B«",
"x»,«y -> y",
"x »,« »Y« -> _",
"(»foo«)", "(»foo«)",
"(«foo») = (»bar«)", "(«foo») = (»bar«)",
"if »A« then »B«", "if »A« then »B«",
"if »a« then »b« else »c«", "if »a« then »b« else »c«",
"case »foo« of\n »Number« a -> a\n »Wildcard« -> »bar«\n a»,«b -> a",
// === Macros Ambiguous ===
"(»foo«", "(»foo«",
"if »a«",
"case »a«",
// "->»a«", // TODO [mwu] restore (and implement) when parser is able to parse this
// "a ->", // TODO [mwu] restore (and implement) when parser is able to parse this
// === Definition === // === Definition ===
"«foo» a b c = »foo« a »d«", "«foo» a b c = »foo« a »d«",
"«foo» a b c = d -> a d", "«foo» a b c = d -> a d",

View File

@ -158,7 +158,7 @@ mod tests {
use ast::crumbs; use ast::crumbs;
use ast::crumbs::InfixCrumb; use ast::crumbs::InfixCrumb;
use parser_scala::Parser; use parser::Parser;
struct TestRun { struct TestRun {
graph: GraphInfo, graph: GraphInfo,
@ -182,7 +182,7 @@ mod tests {
} }
fn from_main_def(code: impl Str) -> TestRun { fn from_main_def(code: impl Str) -> TestRun {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let module = parser.parse_module(code, default()).unwrap(); let module = parser.parse_module(code, default()).unwrap();
let definition = DefinitionInfo::from_root_line(&module.lines[0]).unwrap(); let definition = DefinitionInfo::from_root_line(&module.lines[0]).unwrap();
Self::from_definition(definition) Self::from_definition(definition)
@ -199,15 +199,15 @@ mod tests {
} }
} }
#[wasm_bindgen_test] #[test]
pub fn connection_listing_test_plain() { pub fn connection_listing_test_plain() {
use InfixCrumb::LeftOperand; use InfixCrumb::LeftOperand;
use InfixCrumb::RightOperand; use InfixCrumb::RightOperand;
let code_block = r" let code_block = r"
d,e = p d = p
a = d a = d
b = e b = d
c = a + b c = a + b
fun a = a b fun a = a b
f = fun 2"; f = fun 2";
@ -221,21 +221,21 @@ f = fun 2";
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand, LeftOperand]); assert_eq!(&c.destination.crumbs, &crumbs![RightOperand, LeftOperand]);
let c = &run.connections[1]; let c = &run.connections[1];
assert_eq!(run.endpoint_node_repr(&c.source), "b = e"); assert_eq!(run.endpoint_node_repr(&c.source), "b = d");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]); assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "c = a + b"); assert_eq!(run.endpoint_node_repr(&c.destination), "c = a + b");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand, RightOperand]); assert_eq!(&c.destination.crumbs, &crumbs![RightOperand, RightOperand]);
let c = &run.connections[2]; let c = &run.connections[2];
assert_eq!(run.endpoint_node_repr(&c.source), "d,e = p"); assert_eq!(run.endpoint_node_repr(&c.source), "d = p");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand, LeftOperand]); assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "a = d"); assert_eq!(run.endpoint_node_repr(&c.destination), "a = d");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand]); assert_eq!(&c.destination.crumbs, &crumbs![RightOperand]);
let c = &run.connections[3]; let c = &run.connections[3];
assert_eq!(run.endpoint_node_repr(&c.source), "d,e = p"); assert_eq!(run.endpoint_node_repr(&c.source), "d = p");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand, RightOperand]); assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "b = e"); assert_eq!(run.endpoint_node_repr(&c.destination), "b = d");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand]); assert_eq!(&c.destination.crumbs, &crumbs![RightOperand]);
// Note that line `fun a = a b` des not introduce any connections, as it is a definition. // Note that line `fun a = a b` des not introduce any connections, as it is a definition.
@ -243,13 +243,13 @@ f = fun 2";
assert_eq!(run.connections.len(), 4); assert_eq!(run.connections.len(), 4);
} }
#[wasm_bindgen_test] #[test]
pub fn inline_definition() { pub fn inline_definition() {
let run = TestRun::from_main_def("main = a"); let run = TestRun::from_main_def("main = a");
assert!(run.connections.is_empty()); assert!(run.connections.is_empty());
} }
#[wasm_bindgen_test] #[test]
pub fn listing_dependent_nodes() { pub fn listing_dependent_nodes() {
let code_block = "\ let code_block = "\
f,g = p f,g = p
@ -259,7 +259,6 @@ f = fun 2";
d = a + b d = a + b
e = b"; e = b";
let mut expected_dependent_nodes = HashMap::<&'static str, Vec<&'static str>>::new(); let mut expected_dependent_nodes = HashMap::<&'static str, Vec<&'static str>>::new();
expected_dependent_nodes.insert("f,g = p", vec!["a = f", "b = g", "d = a + b", "e = b"]);
expected_dependent_nodes.insert("a = f", vec!["d = a + b"]); expected_dependent_nodes.insert("a = f", vec!["d = a + b"]);
expected_dependent_nodes.insert("b = g", vec!["d = a + b", "e = b"]); expected_dependent_nodes.insert("b = g", vec!["d = a + b", "e = b"]);
expected_dependent_nodes.insert("c = 2", vec![]); expected_dependent_nodes.insert("c = 2", vec![]);

View File

@ -11,7 +11,7 @@ use ast::crumbs::InfixCrumb;
use ast::crumbs::Located; use ast::crumbs::Located;
use ast::known; use ast::known;
use ast::opr; use ast::opr;
use parser_scala::Parser; use parser::Parser;
use std::iter::FusedIterator; use std::iter::FusedIterator;
@ -284,9 +284,7 @@ impl DefinitionInfo {
let elem = line.elem.ok_or(MissingLineWithAst)?; let elem = line.elem.ok_or(MissingLineWithAst)?;
let off = line.off; let off = line.off;
let first_line = ast::BlockLine { elem, off }; let first_line = ast::BlockLine { elem, off };
let is_orphan = false; let block = ast::Block { indent, empty_lines, first_line, lines };
let ty = ast::BlockType::Discontinuous {};
let block = ast::Block { ty, indent, empty_lines, first_line, lines, is_orphan };
let body_ast = Ast::new(block, None); let body_ast = Ast::new(block, None);
self.set_body_ast(body_ast); self.set_body_ast(body_ast);
Ok(()) Ok(())
@ -603,10 +601,6 @@ mod tests {
use crate::module; use crate::module;
use crate::INDENT; use crate::INDENT;
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
fn assert_eq_strings(lhs: Vec<impl Str>, rhs: Vec<impl Str>) { fn assert_eq_strings(lhs: Vec<impl Str>, rhs: Vec<impl Str>) {
let lhs = lhs.iter().map(|s| s.as_ref()).collect_vec(); let lhs = lhs.iter().map(|s| s.as_ref()).collect_vec();
let rhs = rhs.iter().map(|s| s.as_ref()).collect_vec(); let rhs = rhs.iter().map(|s| s.as_ref()).collect_vec();
@ -621,9 +615,9 @@ mod tests {
format!(" {line}") format!(" {line}")
} }
#[wasm_bindgen_test] #[test]
fn generating_definition_to_add() { fn generating_definition_to_add() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let mut to_add = ToAdd { let mut to_add = ToAdd {
name: DefinitionName::new_method("Main", "add"), name: DefinitionName::new_method("Main", "add"),
explicit_parameter_names: vec!["arg1".into(), "arg2".into()], explicit_parameter_names: vec!["arg1".into(), "arg2".into()],
@ -649,9 +643,9 @@ mod tests {
assert_eq!(ast.repr(), "Main.add arg1 arg2 =\n arg1 + arg2\n arg1 - arg2"); assert_eq!(ast.repr(), "Main.add arg1 arg2 =\n arg1 + arg2\n arg1 - arg2");
} }
#[wasm_bindgen_test] #[test]
fn definition_name_tests() { fn definition_name_tests() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let ast = parser.parse_line_ast("Foo.Bar.baz").unwrap(); let ast = parser.parse_line_ast("Foo.Bar.baz").unwrap();
let name = DefinitionName::from_ast(&ast).unwrap(); let name = DefinitionName::from_ast(&ast).unwrap();
@ -664,16 +658,16 @@ mod tests {
assert_eq!(ast.get_traversing(&name.extended_target[1].crumbs).unwrap().repr(), "Bar"); assert_eq!(ast.get_traversing(&name.extended_target[1].crumbs).unwrap().repr(), "Bar");
} }
#[wasm_bindgen_test] #[test]
fn definition_name_rejecting_incomplete_names() { fn definition_name_rejecting_incomplete_names() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let ast = parser.parse_line_ast("Foo. .baz").unwrap(); let ast = parser.parse_line_ast("Foo. .baz").unwrap();
assert!(DefinitionName::from_ast(&ast).is_none()); assert!(DefinitionName::from_ast(&ast).is_none());
} }
#[wasm_bindgen_test] #[test]
fn definition_info_name() { fn definition_info_name() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let ast = parser.parse_line_ast("Foo.bar a b c = baz").unwrap(); let ast = parser.parse_line_ast("Foo.bar a b c = baz").unwrap();
let definition = DefinitionInfo::from_root_line_ast(&ast).unwrap(); let definition = DefinitionInfo::from_root_line_ast(&ast).unwrap();
@ -681,9 +675,9 @@ mod tests {
assert_eq!(ast.get_traversing(&definition.name.crumbs).unwrap().repr(), "Foo.bar"); assert_eq!(ast.get_traversing(&definition.name.crumbs).unwrap().repr(), "Foo.bar");
} }
#[wasm_bindgen_test] #[test]
fn located_definition_args() { fn located_definition_args() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let ast = parser.parse_line_ast("foo bar baz = a + b + c").unwrap(); let ast = parser.parse_line_ast("foo bar baz = a + b + c").unwrap();
let definition = DefinitionInfo::from_root_line_ast(&ast).unwrap(); let definition = DefinitionInfo::from_root_line_ast(&ast).unwrap();
let (arg0, arg1) = definition.args.expect_tuple(); let (arg0, arg1) = definition.args.expect_tuple();
@ -700,7 +694,7 @@ mod tests {
assert_eq!(ast.get_traversing(&arg1.crumbs).unwrap(), &arg1.item); assert_eq!(ast.get_traversing(&arg1.crumbs).unwrap(), &arg1.item);
} }
#[wasm_bindgen_test] #[test]
fn match_is_not_definition() { fn match_is_not_definition() {
let cons = Ast::cons("Foo"); let cons = Ast::cons("Foo");
let arg = Ast::number(5); let arg = Ast::number(5);
@ -723,28 +717,24 @@ mod tests {
assert!(def_opt.is_some()); assert!(def_opt.is_some());
} }
#[wasm_bindgen_test] #[test]
fn list_definition_test() { fn list_definition_test() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
// TODO [mwu]
// Due to a parser bug, extension methods defining operators cannot be currently
// correctly recognized. When it is fixed, the following should be also supported
// and covered in test: `Int.+ a = _` and `Int.+ = _`.
// Issue link: https://github.com/enso-org/enso/issues/565
let definition_lines = vec![ let definition_lines = vec![
"main = _", "main = _",
"Foo.Bar.foo = _", "Foo.Bar.foo = _",
"Foo.Bar.baz a b = _", "Foo.Bar.baz a b = _",
"+ = _", "+ a = _",
"Int.+ a = _",
"bar = _", "bar = _",
"add a b = 50", "add a b = 50",
"* a b = _", "* a b = _",
]; ];
let expected_def_names_in_module = let expected_def_names_in_module =
vec!["main", "Foo.Bar.foo", "Foo.Bar.baz", "+", "bar", "add", "*"]; vec!["main", "Foo.Bar.foo", "Foo.Bar.baz", "+", "Int.+", "bar", "add", "*"];
// In definition there are no extension methods nor arg-less definitions. // In definition there are no extension methods nor arg-less definitions.
let expected_def_names_in_def = vec!["add", "*"]; let expected_def_names_in_def = vec!["+", "add", "*"];
// === Program with definitions in root === // === Program with definitions in root ===
let program = definition_lines.join("\n"); let program = definition_lines.join("\n");
@ -770,7 +760,7 @@ mod tests {
assert_eq_strings(to_names(&nested_defs), expected_def_names_in_def); assert_eq_strings(to_names(&nested_defs), expected_def_names_in_def);
} }
#[wasm_bindgen_test] #[test]
fn finding_root_definition() { fn finding_root_definition() {
let program_to_expected_main_pos = vec![ let program_to_expected_main_pos = vec![
("main = bar", 0), ("main = bar", 0),
@ -780,7 +770,7 @@ mod tests {
("foo = bar\n\nmain = bar", 2), ("foo = bar\n\nmain = bar", 2),
]; ];
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let main_id = Id::new_plain_name("main"); let main_id = Id::new_plain_name("main");
for (program, expected_line_index) in program_to_expected_main_pos { for (program, expected_line_index) in program_to_expected_main_pos {
let module = parser.parse_module(program, default()).unwrap(); let module = parser.parse_module(program, default()).unwrap();
@ -793,7 +783,7 @@ mod tests {
} }
} }
#[wasm_bindgen_test] #[test]
fn getting_nested_definition() { fn getting_nested_definition() {
let program = r" let program = r"
main = main =
@ -806,7 +796,7 @@ main =
add foo bar"; add foo bar";
let module = parser_scala::Parser::new_or_panic().parse_module(program, default()).unwrap(); let module = parser::Parser::new().parse_module(program, default()).unwrap();
let check_def = |id, expected_body| { let check_def = |id, expected_body| {
let definition = module::get_definition(&module, &id).unwrap(); let definition = module::get_definition(&module, &id).unwrap();
assert_eq!(definition.body().repr(), expected_body); assert_eq!(definition.body().repr(), expected_body);

View File

@ -225,29 +225,26 @@ mod tests {
use ast::macros::DocumentationCommentInfo; use ast::macros::DocumentationCommentInfo;
use ast::test_utils::expect_single_line; use ast::test_utils::expect_single_line;
use ast::HasRepr; use ast::HasRepr;
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. /// Takes a program with main definition in root and returns main's graph.
fn main_graph(parser: &parser_scala::Parser, program: impl Str) -> GraphInfo { fn main_graph(parser: &parser::Parser, program: impl Str) -> GraphInfo {
let module = parser.parse_module(program.into(), default()).unwrap(); let module = parser.parse_module(program.as_ref(), default()).unwrap();
let name = DefinitionName::new_plain("main"); let name = DefinitionName::new_plain("main");
let main = module.def_iter().find_by_name(&name).unwrap(); let main = module.def_iter().find_by_name(&name).unwrap();
GraphInfo::from_definition(main.item) GraphInfo::from_definition(main.item)
} }
fn find_graph(parser: &parser_scala::Parser, program: impl Str, name: impl Str) -> GraphInfo { fn find_graph(parser: &parser::Parser, program: impl Str, name: impl Str) -> GraphInfo {
let module = parser.parse_module(program.into(), default()).unwrap(); let module = parser.parse_module(program.as_ref(), default()).unwrap();
let crumbs = name.into().split('.').map(DefinitionName::new_plain).collect(); let crumbs = name.into().split('.').map(DefinitionName::new_plain).collect();
let id = Id { crumbs }; let id = Id { crumbs };
let definition = get_definition(&module, &id).unwrap(); let definition = get_definition(&module, &id).unwrap();
GraphInfo::from_definition(definition) GraphInfo::from_definition(definition)
} }
#[wasm_bindgen_test] #[test]
fn detect_a_node() { fn detect_a_node() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
// Each of these programs should have a `main` definition with a single `2+2` node. // Each of these programs should have a `main` definition with a single `2+2` node.
let programs = vec![ let programs = vec![
"main = 2+2", "main = 2+2",
@ -265,8 +262,8 @@ mod tests {
} }
} }
fn new_expression_node(parser: &parser_scala::Parser, expression: &str) -> NodeInfo { fn new_expression_node(parser: &parser::Parser, expression: &str) -> NodeInfo {
let node_ast = parser.parse(expression.to_string(), default()).unwrap(); let node_ast = parser.parse(expression, default());
let line_ast = expect_single_line(&node_ast).clone(); let line_ast = expect_single_line(&node_ast).clone();
NodeInfo::from_main_line_ast(&line_ast).unwrap() NodeInfo::from_main_line_ast(&line_ast).unwrap()
} }
@ -281,16 +278,16 @@ mod tests {
fn assert_same(left: &NodeInfo, right: &NodeInfo) { fn assert_same(left: &NodeInfo, right: &NodeInfo) {
assert_eq!(left.id(), right.id()); assert_eq!(left.id(), right.id());
assert_eq!( assert_eq!(
left.documentation.as_ref().map(DocumentationCommentInfo::to_string), left.documentation.as_ref().map(DocumentationCommentInfo::pretty_text),
right.documentation.as_ref().map(DocumentationCommentInfo::to_string) right.documentation.as_ref().map(DocumentationCommentInfo::pretty_text)
); );
assert_eq!(left.main_line.repr(), right.main_line.repr()); assert_eq!(left.main_line.repr(), right.main_line.repr());
} }
#[wasm_bindgen_test] #[test]
fn add_node_to_graph_with_single_line() { fn add_node_to_graph_with_single_line() {
let program = "main = print \"hello\""; let program = "main = print \"hello\"";
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let mut graph = main_graph(&parser, program); let mut graph = main_graph(&parser, program);
let nodes = graph.nodes(); let nodes = graph.nodes();
assert_eq!(nodes.len(), 1); assert_eq!(nodes.len(), 1);
@ -310,14 +307,14 @@ mod tests {
assert_all(nodes.as_slice(), &[node_to_add1, node_to_add0, initial_node]); assert_all(nodes.as_slice(), &[node_to_add1, node_to_add0, initial_node]);
} }
#[wasm_bindgen_test] #[test]
fn add_node_to_graph_with_multiple_lines() { fn add_node_to_graph_with_multiple_lines() {
// TODO [dg] Also add test for binding node when it's possible to update its id. // TODO [dg] Also add test for binding node when it's possible to update its id.
let program = r#"main = let program = r#"main =
foo = node foo = node
foo a = not_node foo a = not_node
print "hello""#; print "hello""#;
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let mut graph = main_graph(&parser, program); let mut graph = main_graph(&parser, program);
let node_to_add0 = new_expression_node(&parser, "4 + 4"); let node_to_add0 = new_expression_node(&parser, "4 + 4");
@ -365,7 +362,7 @@ mod tests {
assert_eq!(graph.nodes()[1].expression().repr(), "not_node"); assert_eq!(graph.nodes()[1].expression().repr(), "not_node");
} }
#[wasm_bindgen_test] #[test]
fn add_node_to_graph_with_blank_line() { fn add_node_to_graph_with_blank_line() {
// The trailing `foo` definition is necessary for the blank line after "node2" to be // The trailing `foo` definition is necessary for the blank line after "node2" to be
// included in the `main` block. Otherwise, the block would end on "node2" and the blank // included in the `main` block. Otherwise, the block would end on "node2" and the blank
@ -376,7 +373,7 @@ mod tests {
node2 node2
foo = 5"; foo = 5";
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let mut graph = main_graph(&parser, program); let mut graph = main_graph(&parser, program);
let id2 = graph.nodes()[0].id(); let id2 = graph.nodes()[0].id();
@ -396,15 +393,14 @@ foo = 5";
node1 node1
node2 node2
node3 node3
node4 node4";
";
// `foo` is not part of expected code, as it belongs to module, not `main` graph. // `foo` is not part of expected code, as it belongs to module, not `main` graph.
graph.expect_code(expected_code); graph.expect_code(expected_code);
} }
#[wasm_bindgen_test] #[test]
fn multiple_node_graph() { fn multiple_node_graph() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let program = r" let program = r"
main = main =
## Faux docstring ## Faux docstring
@ -433,9 +429,9 @@ main =
assert_eq!(nodes.len(), 4); assert_eq!(nodes.len(), 4);
} }
#[wasm_bindgen_test] #[test]
fn removing_node_from_graph() { fn removing_node_from_graph() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let program = r" let program = r"
main = main =
foo = 2 + 2 foo = 2 + 2
@ -459,9 +455,9 @@ main =
graph.expect_code(expected_code); graph.expect_code(expected_code);
} }
#[wasm_bindgen_test] #[test]
fn removing_last_node_from_graph() { fn removing_last_node_from_graph() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let program = r" let program = r"
main = main =
foo = 2 + 2"; foo = 2 + 2";
@ -477,9 +473,9 @@ main =
graph.expect_code("main = Nothing"); graph.expect_code("main = Nothing");
} }
#[wasm_bindgen_test] #[test]
fn add_first_node_to_empty_graph() { fn add_first_node_to_empty_graph() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let program = r"main = Nothing"; let program = r"main = Nothing";
let mut graph = main_graph(&parser, program); let mut graph = main_graph(&parser, program);
assert!(graph.nodes().is_empty()); assert!(graph.nodes().is_empty());
@ -489,15 +485,14 @@ main =
assert_eq!(graph.nodes()[0].expression().repr(), "node0"); assert_eq!(graph.nodes()[0].expression().repr(), "node0");
} }
#[test]
#[wasm_bindgen_test]
fn editing_nodes_expression_in_graph() { fn editing_nodes_expression_in_graph() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let program = r" let program = r"
main = main =
foo = 2 + 2 foo = 2 + 2
bar = 3 + 17"; bar = 3 + 17";
let new_expression = parser.parse("print \"HELLO\"".to_string(), default()).unwrap(); let new_expression = parser.parse("print \"HELLO\"", default());
let new_expression = expect_single_line(&new_expression).clone(); let new_expression = expect_single_line(&new_expression).clone();
let mut graph = main_graph(&parser, program); let mut graph = main_graph(&parser, program);

View File

@ -5,11 +5,7 @@ use crate::prelude::*;
use crate::name::NamePath; use crate::name::NamePath;
use crate::name::QualifiedName; use crate::name::QualifiedName;
use ast::known;
use ast::Ast; use ast::Ast;
use ast::HasRepr;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeSet; use std::collections::BTreeSet;
@ -18,7 +14,6 @@ use std::collections::BTreeSet;
// === Constants === // === Constants ===
// ================= // =================
const LIST_SEPARATOR: char = ',';
const ALIAS_KEYWORD: &str = "as"; const ALIAS_KEYWORD: &str = "as";
const ALL_KEYWORD: &str = "all"; const ALL_KEYWORD: &str = "all";
const HIDING_KEYWORD: &str = "hiding"; const HIDING_KEYWORD: &str = "hiding";
@ -40,7 +35,7 @@ pub type Id = u64;
/// A structure describing what names are imported from the module in a specific import declaration. /// A structure describing what names are imported from the module in a specific import declaration.
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone, Debug, Eq, Deserialize, Hash, Ord, PartialEq, PartialOrd, Serialize)] #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ImportedNames { pub enum ImportedNames {
/// The import is `import <module> [as <alias>]` and only module name is imported. /// The import is `import <module> [as <alias>]` and only module name is imported.
Module { alias: Option<String> }, Module { alias: Option<String> },
@ -53,35 +48,8 @@ pub enum ImportedNames {
List { names: BTreeSet<String> }, List { names: BTreeSet<String> },
} }
impl ImportedNames {
/// Create [`ImportedNames`] structure from the second `Match` segment body.
///
/// The unqualified imports are always parsed as [`Match`](crate::Shape::Match) AST node, where
/// the second segment starts from `import` and ends with end of the import declaration. Thus,
/// the second segment body may be `all`, `all hiding <comma-separated-name-list>`, or just
/// comma separated name list.
fn from_unqualified_import_match_second_segment(segment: impl AsRef<str>) -> Self {
let is_token_sep = |c: char| c.is_ascii_whitespace() || c == LIST_SEPARATOR;
let scope_split = segment.as_ref().split(is_token_sep);
let mut scope_tokens = scope_split.filter(|tok| !tok.is_empty());
let first_token = scope_tokens.next();
let second_token = scope_tokens.next();
let third_and_further_tokens = scope_tokens;
match (first_token, second_token) {
(Some("all"), Some("hiding")) =>
Self::AllExcept { not_imported: third_and_further_tokens.map(Into::into).collect() },
(Some("all"), _) => Self::All,
(first_name, second_name) => {
let all_names =
first_name.into_iter().chain(second_name).chain(third_and_further_tokens);
Self::List { names: all_names.map(Into::into).collect() }
}
}
}
}
/// Representation of a single import declaration. /// Representation of a single import declaration.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize, Hash)] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
pub struct Info { pub struct Info {
/// The path of the qualified name of the imported module. /// The path of the qualified name of the imported module.
pub module: NamePath, pub module: NamePath,
@ -110,55 +78,27 @@ impl Info {
QualifiedName::from_all_segments(&self.module) QualifiedName::from_all_segments(&self.module)
} }
/// Construct from an AST. Fails if the Ast is not an import declaration. /// Construct from an AST, if the Ast is an import declaration.
pub fn from_ast(ast: &Ast) -> Option<Self> { pub fn from_ast(ast: &Ast) -> Option<Self> {
let macro_match = known::Match::try_from(ast).ok()?; if let ast::Shape::Tree(ast::Tree {
Self::from_match(macro_match) type_info: ast::TreeType::Import { module, imported },
} ..
}) = ast.shape()
/// Construct from a macro match AST. Fails if the Ast is not an import declaration. {
pub fn from_match(ast: known::Match) -> Option<Self> { let module = module.clone();
if ast::macros::is_match_qualified_import(&ast) { let imported = match imported.clone() {
Some(Self { ast::ImportedNames::All { except } if except.is_empty() => ImportedNames::All,
module: Self::module_name_from_str(ast.segs.head.body.repr()), ast::ImportedNames::All { except } =>
// TODO[ao] the current parser does not recognize aliases for imports. Should be ImportedNames::AllExcept { not_imported: except },
// fixed with the new parser. Once new parser will be integrated, the alias ast::ImportedNames::List { names } => ImportedNames::List { names },
// support will be implemented as task ast::ImportedNames::Module { alias } => ImportedNames::Module { alias },
// https://www.pivotaltracker.com/story/show/183590537 };
imported: ImportedNames::Module { alias: None }, Some(Info { module, imported })
})
} else if ast::macros::is_match_unqualified_import(&ast) {
let module = ast.segs.head.body.repr();
let imported = ast.segs.tail.first().map_or_default(|s| s.body.repr());
Some(Self::from_module_and_scope_str(module, imported))
} else { } else {
None None
} }
} }
/// Create [`Info`] from unqualified import segment's body representations.
///
/// The unqualified imports are always parsed as [`Match`](crate::Shape::Match) AST node, where
/// the first segment contains keyword `from` and module name, and second segment the rest of
/// the import.
fn from_module_and_scope_str(module: impl AsRef<str>, imported: impl AsRef<str>) -> Self {
Self {
module: Self::module_name_from_str(module),
imported: ImportedNames::from_unqualified_import_match_second_segment(imported),
}
}
fn module_name_from_str(module: impl AsRef<str>) -> NamePath {
let name = module.as_ref().trim();
if name.is_empty() {
default()
} else {
let segments = name.split(ast::opr::predefined::ACCESS);
let trimmed = segments.map(str::trim);
trimmed.map(Into::into).collect()
}
}
/// Return the ID of the import. /// Return the ID of the import.
/// ///
/// The ID is based on a hash of the qualified name of the imported target. This ID is GUI /// The ID is based on a hash of the qualified name of the imported target. This ID is GUI
@ -211,7 +151,7 @@ impl Display for Info {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use parser_scala::Parser; use parser::Parser;
struct Fixture { struct Fixture {
parser: Parser, parser: Parser,
@ -219,7 +159,7 @@ mod tests {
impl Fixture { impl Fixture {
fn new() -> Self { fn new() -> Self {
Self { parser: Parser::new_or_panic() } Self { parser: Parser::new() }
} }
fn run_case(&self, code: &str, expected: Info) { fn run_case(&self, code: &str, expected: Info) {
@ -229,7 +169,7 @@ mod tests {
} }
} }
#[wasm_bindgen_test] #[test]
fn qualified_import_info_from_ast() { fn qualified_import_info_from_ast() {
let test = Fixture::new(); let test = Fixture::new();
let make_info = |module: &[&str]| Info { let make_info = |module: &[&str]| Info {
@ -241,16 +181,12 @@ mod tests {
let normal_case_expected = make_info(&["Standard", "Base", "Data"]); let normal_case_expected = make_info(&["Standard", "Base", "Data"]);
test.run_case(normal_case, normal_case_expected); test.run_case(normal_case, normal_case_expected);
let weird_spaces = "import Standard .Base . Data ";
let weird_spaces_expected = make_info(&["Standard", "Base", "Data"]);
test.run_case(weird_spaces, weird_spaces_expected);
let single_segment = "import local"; let single_segment = "import local";
let single_segment_expected = make_info(&["local"]); let single_segment_expected = make_info(&["local"]);
test.run_case(single_segment, single_segment_expected); test.run_case(single_segment, single_segment_expected);
} }
#[wasm_bindgen_test] #[test]
fn unrestricted_import_info_from_ast() { fn unrestricted_import_info_from_ast() {
let test = Fixture::new(); let test = Fixture::new();
let make_info = |module: &[&str]| Info { let make_info = |module: &[&str]| Info {
@ -261,13 +197,9 @@ mod tests {
let normal_case = "from Standard.Base import all"; let normal_case = "from Standard.Base import all";
let normal_case_expected = make_info(&["Standard", "Base"]); let normal_case_expected = make_info(&["Standard", "Base"]);
test.run_case(normal_case, normal_case_expected); test.run_case(normal_case, normal_case_expected);
let weird_spaces = "from Standard . Base import all ";
let weird_spaces_expected = make_info(&["Standard", "Base"]);
test.run_case(weird_spaces, weird_spaces_expected);
} }
#[wasm_bindgen_test] #[test]
fn restricted_import_info_from_ast() { fn restricted_import_info_from_ast() {
let test = Fixture::new(); let test = Fixture::new();
let make_info = |module: &[&str], names: &[&str]| Info { let make_info = |module: &[&str], names: &[&str]| Info {
@ -288,7 +220,7 @@ mod tests {
test.run_case(single_name, single_name_expected); test.run_case(single_name, single_name_expected);
} }
#[wasm_bindgen_test] #[test]
fn hiding_import_info_from_ast() { fn hiding_import_info_from_ast() {
let test = Fixture::new(); let test = Fixture::new();
let make_info = |module: &[&str], hidden_names: &[&str]| Info { let make_info = |module: &[&str], hidden_names: &[&str]| Info {

View File

@ -65,11 +65,6 @@ pub mod prelude {
pub use enso_prelude::*; pub use enso_prelude::*;
pub use enso_profiler as profiler; pub use enso_profiler as profiler;
pub use enso_profiler::prelude::*; pub use enso_profiler::prelude::*;
#[cfg(test)]
pub use wasm_bindgen_test::wasm_bindgen_test;
#[cfg(test)]
pub use wasm_bindgen_test::wasm_bindgen_test_configure;
} }
@ -206,7 +201,7 @@ mod tests {
use crate::definition::DefinitionProvider; use crate::definition::DefinitionProvider;
use ast::macros::DocumentationCommentInfo; use ast::macros::DocumentationCommentInfo;
use parser_scala::Parser; use parser::Parser;
/// Expect `main` method, where first line is a documentation comment. /// Expect `main` method, where first line is a documentation comment.
@ -229,9 +224,9 @@ mod tests {
assert_eq!(doc.line().repr(), doc2.line().repr()) assert_eq!(doc.line().repr(), doc2.line().repr())
} }
#[wasm_bindgen_test] #[test]
fn parse_single_line_comment() { fn parse_single_line_comment() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
// Typical single line case. // Typical single line case.
let code = r#" let code = r#"
@ -266,15 +261,15 @@ main =
run_case(&parser, code, expected); run_case(&parser, code, expected);
} }
#[wasm_bindgen_test] #[test]
fn parse_multi_line_comment() { fn parse_multi_line_comment() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let code = r#" let code = r#"
main = main =
## First line ## First line
Second line Second line
node"#; node"#;
let expected = " First line\n Second line"; let expected = " First line\nSecond line";
run_case(&parser, code, expected); run_case(&parser, code, expected);
} }
} }

View File

@ -208,7 +208,7 @@ impl Info {
// TODO [mwu] // TODO [mwu]
// Ideally we should not require parser but should use some sane way of generating AST from // Ideally we should not require parser but should use some sane way of generating AST from
// the `ImportInfo` value. // the `ImportInfo` value.
pub fn add_import(&mut self, parser: &parser_scala::Parser, to_add: import::Info) -> usize { pub fn add_import(&mut self, parser: &parser::Parser, to_add: import::Info) -> usize {
// Find last import that is not "after" the added one lexicographically. // Find last import that is not "after" the added one lexicographically.
let previous_import = let previous_import =
self.enumerate_imports().take_while(|(_, import)| &to_add > import).last(); self.enumerate_imports().take_while(|(_, import)| &to_add > import).last();
@ -224,7 +224,7 @@ impl Info {
/// For more details the mechanics see [`add_import`] documentation. /// For more details the mechanics see [`add_import`] documentation.
pub fn add_import_if_missing( pub fn add_import_if_missing(
&mut self, &mut self,
parser: &parser_scala::Parser, parser: &parser::Parser,
to_add: import::Info, to_add: import::Info,
) -> Option<usize> { ) -> Option<usize> {
(!self.contains_import(to_add.id())).then(|| self.add_import(parser, to_add)) (!self.contains_import(to_add.id())).then(|| self.add_import(parser, to_add))
@ -279,7 +279,7 @@ impl Info {
&mut self, &mut self,
method: definition::ToAdd, method: definition::ToAdd,
location: Placement, location: Placement,
parser: &parser_scala::Parser, parser: &parser::Parser,
) -> FallibleResult { ) -> FallibleResult {
let no_indent = 0; let no_indent = 0;
let definition_ast = method.ast(no_indent, parser)?; let definition_ast = method.ast(no_indent, parser)?;
@ -509,13 +509,10 @@ mod tests {
use crate::definition::DefinitionName; use crate::definition::DefinitionName;
use engine_protocol::language_server::MethodPointer; use engine_protocol::language_server::MethodPointer;
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[test]
#[wasm_bindgen_test]
fn import_listing() { fn import_listing() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let expect_imports = |code: &str, expected: &[&[&str]]| { let expect_imports = |code: &str, expected: &[&[&str]]| {
let ast = parser.parse_module(code, default()).unwrap(); let ast = parser.parse_module(code, default()).unwrap();
let info = Info { ast }; let info = Info { ast };
@ -536,9 +533,9 @@ mod tests {
]]); ]]);
} }
#[wasm_bindgen_test] #[test]
fn import_adding_and_removing() { fn import_adding_and_removing() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let code = "import Foo.Bar.Baz"; let code = "import Foo.Bar.Baz";
let ast = parser.parse_module(code, default()).unwrap(); let ast = parser.parse_module(code, default()).unwrap();
let mut info = Info { ast }; let mut info = Info { ast };
@ -565,9 +562,9 @@ mod tests {
info.expect_code("import Bar.Gar"); info.expect_code("import Bar.Gar");
} }
#[wasm_bindgen_test] #[test]
fn implicit_method_resolution() { fn implicit_method_resolution() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let module_name = let module_name =
QualifiedName::from_all_segments(["local", "ProjectName", "Main"]).unwrap(); QualifiedName::from_all_segments(["local", "ProjectName", "Main"]).unwrap();
let expect_find = |method: &MethodPointer, code, expected: &definition::Id| { let expect_find = |method: &MethodPointer, code, expected: &definition::Id| {
@ -623,7 +620,7 @@ mod tests {
expect_not_found(&ptr, "bar a b = a + b"); expect_not_found(&ptr, "bar a b = a + b");
} }
#[wasm_bindgen_test] #[test]
fn test_definition_location() { fn test_definition_location() {
let code = r" let code = r"
some def = some def =
@ -639,13 +636,13 @@ other def =
last def = inline expression"; last def = inline expression";
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let module = parser.parse_module(code, default()).unwrap(); let module = parser.parse_module(code, default()).unwrap();
let module = Info { ast: module }; let module = Info { ast: module };
let id = definition::Id::new_plain_name("other"); let id = definition::Id::new_plain_name("other");
let span = definition_span(&module.ast, &id).unwrap(); let span = definition_span(&module.ast, &id).unwrap();
assert!(code[span].ends_with("last line of other def\n")); assert!(code[span].ends_with("last line of other def"));
let id = definition::Id::new_plain_name("last"); let id = definition::Id::new_plain_name("last");
let span = definition_span(&module.ast, &id).unwrap(); let span = definition_span(&module.ast, &id).unwrap();
@ -656,9 +653,9 @@ last def = inline expression";
assert!(code[span].ends_with("nested body")); assert!(code[span].ends_with("nested body"));
} }
#[wasm_bindgen_test] #[test]
fn add_method() { fn add_method() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let module = r#"Main.method1 arg = body let module = r#"Main.method1 arg = body
main = Main.method1 10"#; main = Main.method1 10"#;

View File

@ -237,7 +237,7 @@ impl NodeInfo {
} }
/// Obtain documentation text. /// Obtain documentation text.
pub fn documentation_text(&self) -> Option<String> { pub fn documentation_text(&self) -> Option<ImString> {
self.documentation.as_ref().map(|doc| doc.pretty_text()) self.documentation.as_ref().map(|doc| doc.pretty_text())
} }
} }

View File

@ -16,7 +16,7 @@ use crate::node::NodeInfo;
use ast::crumbs::Located; use ast::crumbs::Located;
use ast::BlockLine; use ast::BlockLine;
use parser_scala::Parser; use parser::Parser;
use std::collections::BTreeSet; use std::collections::BTreeSet;
@ -441,9 +441,9 @@ mod tests {
} }
#[allow(unused_parens)] // False warning. #[allow(unused_parens)] // False warning.
#[wasm_bindgen_test] #[test]
fn test_collapse() { fn test_collapse() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let module_name = "Main".to_owned(); let module_name = "Main".to_owned();
let introduced_name = Identifier::try_from("custom_new").unwrap(); let introduced_name = Identifier::try_from("custom_new").unwrap();
let refactored_name = DefinitionName::new_plain("custom_old"); let refactored_name = DefinitionName::new_plain("custom_old");

View File

@ -185,7 +185,7 @@ mod test {
use ast::HasIdMap; use ast::HasIdMap;
use enso_prelude::default; use enso_prelude::default;
use parser_scala::Parser; use parser::Parser;
use uuid::Uuid; use uuid::Uuid;
/// A sample text edit used to test "text api" properties. /// A sample text edit used to test "text api" properties.
@ -244,10 +244,8 @@ mod test {
fn assert_edit_keeps_main_node_ids(&self, parser: &Parser) { fn assert_edit_keeps_main_node_ids(&self, parser: &Parser) {
let ast1 = parser.parse_module(&self.code, default()).unwrap(); let ast1 = parser.parse_module(&self.code, default()).unwrap();
let mut id_map = ast1.id_map(); let mut id_map = ast1.id_map();
apply_code_change_to_id_map(&mut id_map, &self.change, &self.code); apply_code_change_to_id_map(&mut id_map, &self.change, &self.code);
let code2 = self.resulting_code(); let code2 = self.resulting_code();
let ast2 = parser.parse_module(code2, id_map.clone()).unwrap(); let ast2 = parser.parse_module(code2, id_map.clone()).unwrap();
self.assert_same_node_ids(&ast1, &ast2); self.assert_same_node_ids(&ast1, &ast2);
} }
@ -257,8 +255,6 @@ mod test {
fn assert_same_node_ids(&self, ast1: &ast::known::Module, ast2: &ast::known::Module) { fn assert_same_node_ids(&self, ast1: &ast::known::Module, ast2: &ast::known::Module) {
let ids1 = main_nodes(ast1); let ids1 = main_nodes(ast1);
let ids2 = main_nodes(ast2); let ids2 = main_nodes(ast2);
debug!("IDs1: {ids1:?}");
debug!("IDs2: {ids2:?}");
assert_eq!(ids1, ids2, "Node ids mismatch in {self:?}"); assert_eq!(ids1, ids2, "Node ids mismatch in {self:?}");
} }
} }
@ -300,9 +296,9 @@ mod test {
assert_eq!(case.resulting_code(), "fooc"); assert_eq!(case.resulting_code(), "fooc");
} }
#[wasm_bindgen_test] #[test]
fn applying_code_changes_to_id_map() { fn applying_code_changes_to_id_map() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
// All the cases describe edit to a middle line in three line main definition. // All the cases describe edit to a middle line in three line main definition.
let cases = [ let cases = [

View File

@ -13,7 +13,6 @@ failure = { workspace = true }
lazy_static = { workspace = true } lazy_static = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
serde = { version = "1.0", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { workspace = true }
uuid = { version = "0.8.1", features = ["serde", "v4", "wasm-bindgen"] } uuid = { version = "0.8.1", features = ["serde", "v4", "wasm-bindgen"] }
ast-macros = { path = "../macros" } ast-macros = { path = "../macros" }
enso-data-structures = { path = "../../../../../lib/rust/data-structures" } enso-data-structures = { path = "../../../../../lib/rust/data-structures" }

File diff suppressed because it is too large Load Diff

View File

@ -45,9 +45,9 @@ impl IdMap {
// ====================== // =================
// === IdMapForParser === // === JsonIdMap ===
// ====================== // =================
/// Strongly typed index of char. /// Strongly typed index of char.
/// ///

View File

@ -9,11 +9,6 @@ use crate::HasTokens;
use crate::Shape; use crate::Shape;
use crate::TokenConsumer; use crate::TokenConsumer;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
// ================= // =================
@ -165,25 +160,6 @@ impl<'a, T> From<&'a KnownAst<T>> for &'a Ast {
} }
} }
impl<T> Serialize for KnownAst<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
self.ast.serialize(serializer)
}
}
impl<'de, T, E> Deserialize<'de> for KnownAst<T>
where
for<'t> &'t Shape<Ast>: TryInto<&'t T, Error = E>,
E: fmt::Display,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de> {
let ast = Ast::deserialize(deserializer)?;
Self::try_new(ast).map_err(serde::de::Error::custom)
}
}
impl<T> HasTokens for KnownAst<T> { impl<T> HasTokens for KnownAst<T> {
fn feed_to(&self, consumer: &mut impl TokenConsumer) { fn feed_to(&self, consumer: &mut impl TokenConsumer) {
self.ast.feed_to(consumer) self.ast.feed_to(consumer)

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,9 @@
use crate::prelude::*; use crate::prelude::*;
use crate::crumbs::AmbiguousCrumb;
use crate::crumbs::Located; use crate::crumbs::Located;
use crate::crumbs::MatchCrumb;
use crate::known; use crate::known;
use crate::BlockLine; use crate::BlockLine;
use crate::Shifted;
// ============== // ==============
@ -23,12 +20,6 @@ pub mod skip_and_freeze;
// === Recognized Macros Keywords === // === Recognized Macros Keywords ===
// ================================== // ==================================
/// The keyword introducing a disabled code line.
pub const DISABLING_COMMENT_INTRODUCER: &str = "#";
/// The keyword introducing a documentation block.
pub const DOCUMENTATION_COMMENT_INTRODUCER: &str = "##";
/// The keyword introducing an qualified import declaration. See: /// The keyword introducing an qualified import declaration. See:
/// https://dev.enso.org/docs/enso/syntax/imports.html#import-syntax /// https://dev.enso.org/docs/enso/syntax/imports.html#import-syntax
pub const QUALIFIED_IMPORT_KEYWORD: &str = "import"; pub const QUALIFIED_IMPORT_KEYWORD: &str = "import";
@ -36,29 +27,21 @@ pub const QUALIFIED_IMPORT_KEYWORD: &str = "import";
/// The keyword introducing an unqualified import declaration. /// The keyword introducing an unqualified import declaration.
pub const UNQUALIFIED_IMPORT_KEYWORD: &str = "from"; pub const UNQUALIFIED_IMPORT_KEYWORD: &str = "from";
/// The keyword introducing an unqualified export declaration.
pub const QUALIFIED_EXPORT_KEYWORD: &str = "export";
// ======================== // ========================
// === Disable Comments === // === Disable Comments ===
// ======================== // ========================
/// Try Interpreting the line as disabling comment. Return the text after `#`.
pub fn as_disable_comment(ast: &Ast) -> Option<String> {
let r#match = crate::known::Match::try_from(ast).ok()?;
let first_segment = &r#match.segs.head;
if crate::identifier::name(&first_segment.head) == Some(DISABLING_COMMENT_INTRODUCER) {
Some(first_segment.body.repr())
} else {
None
}
}
/// Check if this AST is a disabling comment. /// Check if this AST is a disabling comment.
pub fn is_disable_comment(ast: &Ast) -> bool { pub fn is_disable_comment(ast: &Ast) -> bool {
as_disable_comment(ast).is_some() if let crate::Shape::Tree(tree) = ast.shape()
&& tree.type_info == crate::TreeType::ExpressionWithComment
&& !tree.span_info.iter().any(|e| matches!(e, crate::SpanSeed::Child(_))) {
true
} else {
false
}
} }
@ -72,40 +55,32 @@ pub fn is_disable_comment(ast: &Ast) -> bool {
/// Describes the AST of a documentation comment. /// Describes the AST of a documentation comment.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DocumentationCommentAst { pub struct DocumentationCommentAst {
ast: known::Match, ast: known::Tree,
body: crate::MacroPatternMatch<Shifted<Ast>>, rendered: ImString,
} }
impl DocumentationCommentAst { impl DocumentationCommentAst {
/// Interpret given Ast as a documentation comment. Return `None` if it is not recognized. /// Interpret given Ast as a documentation comment. Return `None` if it is not recognized.
pub fn new(ast: &Ast) -> Option<Self> { pub fn new(ast: &Ast) -> Option<Self> {
let ast = crate::known::Match::try_from(ast).ok()?; let ast = crate::known::Tree::try_from(ast).ok()?;
let first_segment = &ast.segs.head; if let crate::TreeType::Documentation { rendered } = &ast.type_info {
let introducer = crate::identifier::name(&first_segment.head)?; let rendered = rendered.clone();
if introducer == DOCUMENTATION_COMMENT_INTRODUCER { Some(DocumentationCommentAst { ast, rendered })
let body = first_segment.body.clone_ref();
Some(DocumentationCommentAst { ast, body })
} else { } else {
None None
} }
} }
/// Get the documentation comment's AST.
pub fn ast(&self) -> known::Match {
self.ast.clone_ref()
}
} }
// === Line Description === // === Line Description ===
/// Describes the line with a documentation comment. /// Describes the line with a documentation comment.
#[derive(Clone, Debug, Deref)] #[derive(Clone, Debug)]
pub struct DocumentationCommentLine { pub struct DocumentationCommentLine {
/// Stores the documentation AST and the trailing whitespace length. /// Stores the documentation AST and the trailing whitespace length.
#[deref] line: BlockLine<known::Tree>,
line: BlockLine<known::Match>, rendered: ImString,
body: crate::MacroPatternMatch<Shifted<Ast>>,
} }
impl DocumentationCommentLine { impl DocumentationCommentLine {
@ -117,22 +92,17 @@ impl DocumentationCommentLine {
/// Treat given documentation AST as the line with a given trailing whitespace. /// Treat given documentation AST as the line with a given trailing whitespace.
pub fn from_doc_ast(ast_doc: DocumentationCommentAst, off: usize) -> Self { pub fn from_doc_ast(ast_doc: DocumentationCommentAst, off: usize) -> Self {
Self { line: BlockLine { elem: ast_doc.ast, off }, body: ast_doc.body } Self { line: BlockLine { elem: ast_doc.ast, off }, rendered: ast_doc.rendered }
}
/// Get the documentation comment's AST.
pub fn ast(&self) -> known::Match {
self.line.elem.clone_ref()
} }
/// Get the line with this comment. /// Get the line with this comment.
pub fn line(&self) -> &BlockLine<known::Match> { fn line(&self) -> &BlockLine<known::Tree> {
&self.line &self.line
} }
/// Convenience function that throws away some information to return the line description that /// Convenience function that throws away some information to return the line description that
/// is used in AST blocks. /// is used in AST blocks.
pub fn block_line(&self) -> BlockLine<Option<Ast>> { fn block_line(&self) -> BlockLine<Option<Ast>> {
self.line.as_ref().map(|known_ast| Some(known_ast.ast().clone_ref())) self.line.as_ref().map(|known_ast| Some(known_ast.ast().clone_ref()))
} }
} }
@ -142,10 +112,9 @@ impl DocumentationCommentLine {
/// Structure holding the documentation comment AST and related information necessary to deal with /// Structure holding the documentation comment AST and related information necessary to deal with
/// them. /// them.
#[derive(Clone, Debug, Deref)] #[derive(Clone, Debug)]
pub struct DocumentationCommentInfo { pub struct DocumentationCommentInfo {
/// Description of the line with the documentation comment. /// Description of the line with the documentation comment.
#[deref]
pub line: DocumentationCommentLine, pub line: DocumentationCommentLine,
/// The absolute indent of the block that contains the line with documentation comment. /// The absolute indent of the block that contains the line with documentation comment.
pub block_indent: usize, pub block_indent: usize,
@ -157,18 +126,28 @@ impl DocumentationCommentInfo {
Some(Self { line: DocumentationCommentLine::new(line)?, block_indent }) Some(Self { line: DocumentationCommentLine::new(line)?, block_indent })
} }
/// Get the line with this comment.
pub fn line(&self) -> &BlockLine<known::Tree> {
self.line.line()
}
/// Get the documentation comment's AST.
pub fn ast(&self) -> known::Tree {
self.line.line.elem.clone_ref()
}
/// Convenience function that throws away some information to return the line description that
/// is used in AST blocks.
pub fn block_line(&self) -> BlockLine<Option<Ast>> {
self.line.block_line()
}
/// Get the documentation text. /// Get the documentation text.
/// ///
/// The text is pretty printed as per UI perspective -- all lines leading whitespace is stripped /// The text is pretty printed as per UI perspective--leading whitespace is stripped from all
/// up to the column following comment introducer (`##`). /// lines up to the column following comment introducer (`##`).
pub fn pretty_text(&self) -> String { pub fn pretty_text(&self) -> ImString {
let mut repr = self.body.repr(); self.line.rendered.clone()
// Trailing whitespace must be maintained.
repr.extend(std::iter::repeat(' ').take(self.line.off));
let indent = self.block_indent + DOCUMENTATION_COMMENT_INTRODUCER.len();
let old = format!("\n{}", " ".repeat(indent));
let new = "\n";
repr.replace(&old, new)
} }
/// Generates the source code text of the comment line from a pretty text. /// Generates the source code text of the comment line from a pretty text.
@ -177,25 +156,12 @@ impl DocumentationCommentInfo {
let mut lines = text.lines(); let mut lines = text.lines();
// First line must always exist, even for an empty comment. // First line must always exist, even for an empty comment.
let first_line = format!("##{}", lines.next().unwrap_or_default()); let first_line = format!("##{}", lines.next().unwrap_or_default());
let other_lines = lines.map(|line| format!("{indent} {line}")); let other_lines = lines.map(|line| format!("{indent} {line}"));
let mut out_lines = std::iter::once(first_line).chain(other_lines); let mut out_lines = std::iter::once(first_line).chain(other_lines);
out_lines.join("\n") out_lines.join("\n")
} }
} }
impl AsRef<Ast> for DocumentationCommentInfo {
fn as_ref(&self) -> &Ast {
self.line.elem.ast()
}
}
impl Display for DocumentationCommentInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.pretty_text())
}
}
/// Check if given Ast stores a documentation comment. /// Check if given Ast stores a documentation comment.
pub fn is_documentation_comment(ast: &Ast) -> bool { pub fn is_documentation_comment(ast: &Ast) -> bool {
DocumentationCommentAst::new(ast).is_some() DocumentationCommentAst::new(ast).is_some()
@ -203,112 +169,27 @@ pub fn is_documentation_comment(ast: &Ast) -> bool {
// ===============
// === Imports ===
// ===============
/// If the given AST node is an import declaration, returns it as a Match (which is the only shape
/// capable of storing import declarations). Returns `None` otherwise.
pub fn ast_as_import_match(ast: &Ast) -> Option<known::Match> {
let macro_match = known::Match::try_from(ast).ok()?;
is_match_import(&macro_match).then_some(macro_match)
}
/// If the given AST node is a qualified import declaration (`import <module name>`), returns it as
/// a Match (which is the only shape capable of storing import declarations). Returns `None`
/// otherwise.
pub fn is_match_qualified_import(ast: &known::Match) -> bool {
let segment = &ast.segs.head;
let keyword = crate::identifier::name(&segment.head);
keyword.contains_if(|str| *str == QUALIFIED_IMPORT_KEYWORD)
}
/// If the given AST node is an unqualified import declaration (`from <module name> import <...>`),
/// returns it as a Match (which is the only shape capable of storing import declarations). Returns
/// `None` otherwise.
pub fn is_match_unqualified_import(ast: &known::Match) -> bool {
let first_segment = &ast.segs.head;
let first_keyword = crate::identifier::name(&first_segment.head);
let second_segment = &ast.segs.tail.first();
let second_keyword = second_segment.and_then(|s| crate::identifier::name(&s.head));
first_keyword == Some(UNQUALIFIED_IMPORT_KEYWORD)
&& second_keyword == Some(QUALIFIED_IMPORT_KEYWORD)
}
/// Check if the given macro match node is an import declaration.
pub fn is_match_import(ast: &known::Match) -> bool {
is_match_qualified_import(ast) || is_match_unqualified_import(ast)
}
/// Check if the given ast node is an import declaration.
pub fn is_ast_import(ast: &Ast) -> bool {
ast_as_import_match(ast).is_some()
}
// =============== // ===============
// === Lambdas === // === Lambdas ===
// =============== // ===============
/// Describes the lambda-expression's three pieces: the argument, the arrow operator and the body. /// Describes the lambda-expression's pieces: the argument and the body.
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct LambdaInfo<'a> { pub struct LambdaInfo<'a> {
pub arg: Located<&'a Ast>, pub arg: Located<&'a Ast>,
pub opr: Located<&'a Ast>,
pub body: Located<&'a Ast>, pub body: Located<&'a Ast>,
} }
/// If this is the builtin macro for `->` (lambda expression), returns it as known `Match`.
pub fn as_lambda_match(ast: &Ast) -> Option<known::Match> {
let macro_match = known::Match::try_from(ast).ok()?;
let segment = &macro_match.segs.head;
crate::opr::is_arrow_opr(&segment.head).then_some(macro_match)
}
/// Describes the given Ast as lambda, if this is a matched `->` builtin macro. /// Describes the given Ast as lambda, if this is a matched `->` builtin macro.
pub fn as_lambda(ast: &Ast) -> Option<LambdaInfo> { pub fn as_lambda(ast: &Ast) -> Option<LambdaInfo> {
let _ = as_lambda_match(ast)?; if let crate::Shape::Tree(crate::Tree { type_info: crate::TreeType::Lambda, .. }) = ast.shape()
let mut child_iter = ast.iter_subcrumbs(); {
let arg = ast.get_located(child_iter.next()?).ok()?; let mut iter = ast.iter_subcrumbs().map(|crumb| ast.get_located(crumb).unwrap());
let opr = ast.get_located(child_iter.next()?).ok()?; let arg = iter.next().unwrap();
let body = ast.get_located(child_iter.next()?).ok()?; let body = iter.next().unwrap();
let is_arrow = crate::opr::is_arrow_opr(opr.item); Some(LambdaInfo { arg, body })
is_arrow.then_some(LambdaInfo { arg, opr, body }) } else {
} None
// ===================
// === Match Utils ===
// ===================
impl crate::Match<Ast> {
/// Iterates matched ASTs. Skips segment heads ("keywords").
/// For example, for `(a)` it iterates only over `a`, skkipping segment heads `(` and `)`.
pub fn iter_pat_match_subcrumbs(&self) -> impl Iterator<Item = MatchCrumb> + '_ {
self.iter_subcrumbs().filter(|crumb| {
use crate::crumbs::SegmentMatchCrumb;
match crumb {
MatchCrumb::Segs { val, .. } => val != &SegmentMatchCrumb::Head,
_ => true,
}
})
}
}
// =======================
// === Ambiguous Utils ===
// =======================
impl crate::Ambiguous<Ast> {
/// Iterates matched ASTs. Skips segment heads ("keywords").
/// For example, for `(a)` it iterates only over `a`, skkipping segment heads `(` and `)`.
pub fn iter_pat_match_subcrumbs(&self) -> impl Iterator<Item = AmbiguousCrumb> + '_ {
self.iter_subcrumbs()
.filter(|crumb| crumb.field != crate::crumbs::AmbiguousSegmentCrumb::Head)
} }
} }

View File

@ -9,7 +9,6 @@ use enso_prelude::*;
use crate::known; use crate::known;
use crate::Ast; use crate::Ast;
use crate::Crumbable;
use crate::HasRepr; use crate::HasRepr;
@ -132,26 +131,12 @@ fn preserving_macro(
} }
/// Check if AST contains a prefix-like macro call with a given name. /// Check if AST contains a prefix-like macro call with a given name.
///
/// We check for both [`known::Prefix`] and [`known::Match`], because first one is used when we
/// modify AST using this module, and the second one can be provided by the Engine.
///
/// Using [`known::Match`] everywhere would be perfect, but it is extremely annoying to construct
/// without using the parser. To construct [`known::Match`] we need to know exactly how the specific
/// macro is represented in the parser and create a sequence of `MacroPatternMatch` segments. The
/// parser generates a huge AST for a simple `skip foo` expression, and it is not wise to exactly
/// repeat this AST generation here. If our generated AST is different from the one generated by the
/// parser anyway, we would rather generate a much more simple [`known::Prefix`]. It is easier to
/// both construct and deconstruct later.
pub fn is_macro_call(ast: &Ast, identifier: &str) -> bool { pub fn is_macro_call(ast: &Ast, identifier: &str) -> bool {
if let Ok(prefix) = known::Prefix::try_from(ast) { if let Ok(prefix) = known::Prefix::try_from(ast) {
let name = crate::identifier::name(&prefix.func); let name = crate::identifier::name(&prefix.func);
name == Some(identifier) name == Some(identifier)
} else if let Ok(macro_match) = known::Match::try_from(ast) {
let first_segment = &macro_match.segs.head;
let name = crate::identifier::name(&first_segment.head);
name == Some(identifier)
} else { } else {
// TODO: Check for a [`Tree`] macro (https://github.com/enso-org/enso/issues/5572).
false false
} }
} }
@ -160,11 +145,8 @@ pub fn is_macro_call(ast: &Ast, identifier: &str) -> bool {
pub fn maybe_prefix_macro_body(ast: &Ast) -> Option<Ast> { pub fn maybe_prefix_macro_body(ast: &Ast) -> Option<Ast> {
if let Ok(prefix) = known::Prefix::try_from(ast) { if let Ok(prefix) = known::Prefix::try_from(ast) {
Some(prefix.arg.clone()) Some(prefix.arg.clone())
} else if let Ok(macro_match) = known::Match::try_from(ast) {
let body_crumb = macro_match.iter_subcrumbs().nth(1)?;
let body_ast = macro_match.get(&body_crumb).ok()?;
Some(body_ast.clone())
} else { } else {
// TODO: Check for a [`Tree`] macro (https://github.com/enso-org/enso/issues/5572).
None None
} }
} }

View File

@ -107,7 +107,7 @@ pub fn is_assignment(ast: &Ast) -> bool {
pub fn assignment() -> known::Opr { pub fn assignment() -> known::Opr {
// TODO? We could cache and reuse, if we care. // TODO? We could cache and reuse, if we care.
let name = predefined::ASSIGNMENT.into(); let name = predefined::ASSIGNMENT.into();
let opr = Opr { name }; let opr = Opr { name, right_assoc: false };
known::Opr::new(opr, None) known::Opr::new(opr, None)
} }
@ -150,7 +150,10 @@ pub fn make_operator(opr: &Ast) -> Option<Operator> {
/// Describes associativity of the given operator AST. /// Describes associativity of the given operator AST.
pub fn assoc(ast: &known::Opr) -> Assoc { pub fn assoc(ast: &known::Opr) -> Assoc {
Assoc::of(&ast.name) match ast.right_assoc {
true => Assoc::Right,
false => Assoc::Left,
}
} }
@ -327,7 +330,7 @@ pub struct Chain {
/// Subsequent operands applied to the `target`. /// Subsequent operands applied to the `target`.
pub args: Vec<ChainElement>, pub args: Vec<ChainElement>,
/// Operator AST. Generally all operators in the chain should be the same (except for id). /// Operator AST. Generally all operators in the chain should be the same (except for id).
/// It is not specified which exactly operator's in the chain this AST belongs to. /// It is not specified exactly which operators in the chain this AST belongs to.
pub operator: known::Opr, pub operator: known::Opr,
} }

View File

@ -53,66 +53,6 @@ pub const FMT_BLOCK_QUOTES: &str = "'''";
// ===============
// === Builder ===
// ===============
has_tokens!(Empty);
has_tokens!(Letter, self.char);
has_tokens!(Space, self);
has_tokens!(Text, self.str);
has_tokens!(Seq, self.first, self.second);
// =====================
// === TextBlockLine ===
// =====================
/// Not an instance of `Tokenizer`, as it needs to know parent block's offset.
impl<T: HasTokens> TextBlockLine<T> {
fn feed_to(&self, consumer: &mut impl TokenConsumer, offset: usize) {
for empty_line_spaces in &self.empty_lines {
(NEWLINE, empty_line_spaces).feed_to(consumer);
}
(NEWLINE, offset, &self.text).feed_to(consumer);
}
}
// =====================
// === Text Segments ===
// =====================
has_tokens!(SegmentPlain, self.value);
has_tokens!(SegmentRawEscape, BACKSLASH, self.code);
has_tokens!(SegmentExpr<T>, EXPR_QUOTE, self.value, EXPR_QUOTE);
has_tokens!(SegmentEscape, BACKSLASH, self.code);
// =================
// === RawEscape ===
// =================
has_tokens!(Unfinished);
has_tokens!(Invalid, self.str);
has_tokens!(Slash, BACKSLASH);
has_tokens!(Quote, FMT_QUOTE);
has_tokens!(RawQuote, RAW_QUOTE);
// ==============
// === Escape ===
// ==============
has_tokens!(EscapeCharacter, self.c);
has_tokens!(EscapeControl, self.name);
has_tokens!(EscapeNumber, self.digits);
has_tokens!(EscapeUnicode16, UNICODE16_INTRODUCER, self.digits);
has_tokens!(EscapeUnicode21, UNICODE21_OPENER.deref(), self.digits, UNICODE21_CLOSER.deref());
has_tokens!(EscapeUnicode32, UNICODE32_INTRODUCER, self.digits);
// ============= // =============
// === Block === // === Block ===
// ============= // =============
@ -120,68 +60,20 @@ has_tokens!(EscapeUnicode32, UNICODE32_INTRODUCER, self.digits);
has_tokens!(BlockLine<T>, self.elem, self.off); has_tokens!(BlockLine<T>, self.elem, self.off);
// =============
// === Macro ===
// =============
// === Macro Segments ==
has_tokens!(MacroMatchSegment<T>, self.head, self.body);
has_tokens!(MacroAmbiguousSegment<T>, self.head, self.body);
// === MacroPatternMatch subtypes ===
has_tokens!(MacroPatternMatchRawBegin);
has_tokens!(MacroPatternMatchRawEnd);
has_tokens!(MacroPatternMatchRawNothing);
has_tokens!(MacroPatternMatchRawSeq<T>, self.elem);
has_tokens!(MacroPatternMatchRawOr<T>, self.elem);
has_tokens!(MacroPatternMatchRawMany<T>, self.elem);
has_tokens!(MacroPatternMatchRawExcept<T>, self.elem);
has_tokens!(MacroPatternMatchRawBuild<T>, self.elem);
has_tokens!(MacroPatternMatchRawErr<T>, self.elem);
has_tokens!(MacroPatternMatchRawTag<T>, self.elem);
has_tokens!(MacroPatternMatchRawCls<T>, self.elem);
has_tokens!(MacroPatternMatchRawTok<T>, self.elem);
has_tokens!(MacroPatternMatchRawBlank<T>, self.elem);
has_tokens!(MacroPatternMatchRawVar<T>, self.elem);
has_tokens!(MacroPatternMatchRawCons<T>, self.elem);
has_tokens!(MacroPatternMatchRawOpr<T>, self.elem);
has_tokens!(MacroPatternMatchRawAnnotation<T>, self.elem);
has_tokens!(MacroPatternMatchRawMod<T>, self.elem);
has_tokens!(MacroPatternMatchRawNum<T>, self.elem);
has_tokens!(MacroPatternMatchRawText<T>, self.elem);
has_tokens!(MacroPatternMatchRawBlock<T>, self.elem);
has_tokens!(MacroPatternMatchRawMacro<T>, self.elem);
has_tokens!(MacroPatternMatchRawInvalid<T>, self.elem);
has_tokens!(MacroPatternMatchRawFailedMatch);
// === Switch ===
has_tokens!(Switch<T>, self.deref());
// ===============
// === Shifted === // === Shifted ===
// ===============
has_tokens!(Shifted<T>, self.off, self.wrapped); has_tokens!(Shifted<T>, self.off, self.wrapped);
has_tokens!(ShiftedVec1<T>, self.head, self.tail); has_tokens!(ShiftedVec1<T>, self.head, self.tail);
// ============================================================================= // =============================================================================
// === Shape =================================================================== // === Shape ===================================================================
// ============================================================================= // =============================================================================
// ===============
// === Invalid ===
// ===============
has_tokens!(Unrecognized, self.str);
has_tokens!(Unexpected<T>, self.stream);
has_tokens!(InvalidQuote, self.quote);
has_tokens!(InlineBlock, self.quote);
// =================== // ===================
// === Identifiers === // === Identifiers ===
@ -193,7 +85,6 @@ has_tokens!(Cons, self.name);
has_tokens!(Opr, self.name); has_tokens!(Opr, self.name);
has_tokens!(Annotation, self.name); has_tokens!(Annotation, self.name);
has_tokens!(Mod, self.name, MOD_SUFFIX); has_tokens!(Mod, self.name, MOD_SUFFIX);
has_tokens!(InvalidSuffix<T>, self.elem, self.suffix);
// ============== // ==============
@ -205,55 +96,6 @@ struct NumberBase<T>(T);
has_tokens!(NumberBase<T>, self.0, NUMBER_BASE_SEPARATOR); has_tokens!(NumberBase<T>, self.0, NUMBER_BASE_SEPARATOR);
has_tokens!(Number, self.base.as_ref().map(NumberBase), self.int); has_tokens!(Number, self.base.as_ref().map(NumberBase), self.int);
has_tokens!(DanglingBase, self.base, NUMBER_BASE_SEPARATOR);
// ============
// === Text ===
// ============
// === Lines ===
has_tokens!(TextLineRaw, RAW_QUOTE, self.text, RAW_QUOTE);
has_tokens!(TextLineFmt<T>, FMT_QUOTE, self.text, FMT_QUOTE);
// === TextBlockRaw ==
impl HasTokens for TextBlockRaw {
fn feed_to(&self, consumer: &mut impl TokenConsumer) {
(RAW_BLOCK_QUOTES, self.spaces).feed_to(consumer);
for line in self.text.iter() {
line.feed_to(consumer, self.offset);
}
}
}
// === TextBlockFmt ==
impl<T: HasTokens> HasTokens for TextBlockFmt<T> {
fn feed_to(&self, consumer: &mut impl TokenConsumer) {
(FMT_BLOCK_QUOTES, self.spaces).feed_to(consumer);
for line in self.text.iter() {
line.feed_to(consumer, self.offset);
}
}
}
// === TextUnclosed ==
impl<T: HasTokens> HasTokens for TextUnclosed<T> {
fn feed_to(&self, consumer: &mut impl TokenConsumer) {
match &self.line {
TextLine::TextLineRaw(line) => (RAW_QUOTE, &line.text).feed_to(consumer),
TextLine::TextLineFmt(line) => (FMT_QUOTE, &line.text).feed_to(consumer),
}
}
}
@ -291,7 +133,7 @@ impl<T: HasTokens> HasTokens for Module<T> {
impl<T: HasTokens> HasTokens for Block<T> { impl<T: HasTokens> HasTokens for Block<T> {
fn feed_to(&self, consumer: &mut impl TokenConsumer) { fn feed_to(&self, consumer: &mut impl TokenConsumer) {
(!self.is_orphan).as_some(NEWLINE).feed_to(consumer); NEWLINE.feed_to(consumer);
for empty_line_space in &self.empty_lines { for empty_line_space in &self.empty_lines {
(empty_line_space, NEWLINE).feed_to(consumer); (empty_line_space, NEWLINE).feed_to(consumer);
} }
@ -304,171 +146,26 @@ impl<T: HasTokens> HasTokens for Block<T> {
// ============== // ============
// === Macros === // === Tree ===
// ============== // ============
// === Match == impl<T: HasTokens> HasTokens for Tree<T> {
impl<T: HasTokens> HasTokens for Match<T> {
fn feed_to(&self, consumer: &mut impl TokenConsumer) { fn feed_to(&self, consumer: &mut impl TokenConsumer) {
for pat_match in &self.pfx { if let Some(str) = &self.leaf_info {
for sast in pat_match.iter() { Token::Str(str).feed_to(consumer)
// reverse the order for prefix: ast before spacing } else {
(&sast.wrapped, &sast.off).feed_to(consumer); for element in &self.span_info {
match element {
SpanSeed::Space(SpanSeedSpace { space }) =>
Token::Off(*space).feed_to(consumer),
SpanSeed::Token(SpanSeedToken { token }) => Token::Str(token).feed_to(consumer),
SpanSeed::Child(SpanSeedChild { node }) => node.feed_to(consumer),
}
} }
} }
self.segs.feed_to(consumer); if let Some(str) = &self.trailing_token {
} Token::Str(str).feed_to(consumer)
} }
// === Ambiguous ===
has_tokens!(Ambiguous<T>, self.segs);
// =====================
// === Spaceless AST ===
// =====================
spaceless_ast!(Comment);
spaceless_ast!(Documented<T>);
spaceless_ast!(Import<T>);
spaceless_ast!(Export<T>);
spaceless_ast!(JavaImport<T>);
spaceless_ast!(Mixfix<T>);
spaceless_ast!(Group<T>);
spaceless_ast!(SequenceLiteral<T>);
spaceless_ast!(TypesetLiteral<T>);
spaceless_ast!(Def<T>);
spaceless_ast!(Foreign);
spaceless_ast!(Modified<T>);
// =============
// === Tests ===
// =============
/// Tests for spacelesss AST. Other AST is covered by parsing tests that verify
/// that correct lengths and text representation are generated. Only spaceless AST
/// is not returned by the parser and can't be covered in this way.
#[cfg(test)]
mod tests {
use super::*;
// === Comment ===
fn make_comment() -> Shape<Ast> {
Comment { lines: vec![] }.into()
}
#[test]
#[should_panic]
fn comment_panics_on_repr() {
make_comment().repr();
}
#[test]
#[should_panic]
fn comment_panics_on_length() {
make_comment().len();
}
// === Import ===
fn make_import() -> Shape<Ast> {
let path = vec![Ast::var("Target")];
Import { path, rename: None, isAll: false, onlyNames: None, hidingNames: None }.into()
}
#[test]
#[should_panic]
fn import_panics_on_repr() {
make_import().repr();
}
#[test]
#[should_panic]
fn import_panics_on_length() {
make_import().len();
}
// === Mixfix ===
fn make_mixfix() -> Shape<Ast> {
Mixfix { name: vec![], args: vec![] }.into()
}
#[test]
#[should_panic]
fn mixfix_panics_on_repr() {
make_mixfix().repr();
}
#[test]
#[should_panic]
fn mixfix_panics_on_length() {
make_mixfix().len();
}
// === Group ===
fn make_group() -> Shape<Ast> {
Group { body: None }.into()
}
#[test]
#[should_panic]
fn group_panics_on_repr() {
make_group().repr();
}
#[test]
#[should_panic]
fn group_panics_on_length() {
make_group().len();
}
// === Def ===
fn make_def() -> Shape<Ast> {
Def { name: Ast::cons("Foo"), args: vec![], body: None }.into()
}
#[test]
#[should_panic]
fn def_panics_on_repr() {
make_def().repr();
}
#[test]
#[should_panic]
fn def_panics_on_length() {
make_def().len();
}
// === Foreign ===
fn make_foreign() -> Shape<Ast> {
Foreign { indent: 0, lang: "Python".into(), code: vec![] }.into()
}
#[test]
#[should_panic]
fn foreign_panics_on_repr() {
make_foreign().repr();
}
#[test]
#[should_panic]
fn foreign_panics_on_length() {
make_foreign().len();
} }
} }

View File

@ -45,7 +45,6 @@ pub fn ast_node(
let output = quote! { let output = quote! {
#[derive(Clone,Eq,PartialEq,Debug)] #[derive(Clone,Eq,PartialEq,Debug)]
#[derive(Iterator)] #[derive(Iterator)]
#[derive(Serialize,Deserialize)]
#input #input
}; };
output.into() output.into()
@ -283,32 +282,3 @@ pub fn has_tokens(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let maker = syn::parse::<TokenDescription>(input).unwrap(); let maker = syn::parse::<TokenDescription>(input).unwrap();
maker.has_tokens().into() maker.has_tokens().into()
} }
/// Generates `HasTokens` instances that are just sum of their parts.
///
/// Takes 1+ parameters:
/// * first goes the typename for which implementations are generated (can take type parameters, as
/// long as they implement `HasTokens`)
/// * then arbitrary number (0 or more) of expressions, that shall yield values implementing
/// `HasTokens`. The `self` can be used in th expressions.
///
/// For example, for invocation:
/// ```text
/// has_tokens!(SegmentExpr<T>, EXPR_QUOTE, self.value, EXPR_QUOTE);
/// ```
/// the following output is produced:
/// ```text
/// impl<T: HasTokens> HasTokens for SegmentExpr<T> {
/// fn feed_to(&self, consumer:&mut impl TokenConsumer) {
/// EXPR_QUOTE.feed(consumer);
/// self.value.feed(consumer);
/// EXPR_QUOTE.feed(consumer);
/// }
/// }
/// ```
/// Generates `HasTokens` implementations for spaceless AST that panics when used.
#[proc_macro]
pub fn spaceless_ast(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
crate::token::spaceless_ast(input)
}

View File

@ -9,20 +9,6 @@ use syn::Token;
/// Generates `HasTokens` implementations for spaceless AST that panics when used.
pub fn spaceless_ast(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let target = syn::parse::<PathSegment>(input).unwrap();
let ty_args = path_segment_generic_args(&target);
let ret = quote! {
impl<#(#ty_args),*> HasTokens for #target {
fn feed_to(&self, consumer:&mut impl TokenConsumer) {
panic!("HasTokens not supported for Spaceless AST!")
}
}
};
ret.into()
}
/// Inner logic for `derive_has_tokens`. /// Inner logic for `derive_has_tokens`.
pub fn derive_for_enum(decl: &syn::DeriveInput, data: &syn::DataEnum) -> TokenStream { pub fn derive_for_enum(decl: &syn::DeriveInput, data: &syn::DataEnum) -> TokenStream {
let ident = &decl.ident; let ident = &decl.ident;

View File

@ -0,0 +1,34 @@
[package]
name = "parser-scala"
version = "0.1.0"
authors = ["Enso Team <contact@enso.org>"]
edition = "2021"
build = "build.rs"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
ast = { path = "../ast/impl" }
enso-prelude = { path = "../../../../lib/rust/prelude" }
enso-profiler = { path = "../../../../lib/rust/profiler" }
console_error_panic_hook = { workspace = true }
failure = { workspace = true }
js-sys = { workspace = true }
matches = { workspace = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["unbounded_depth"] }
wasm-bindgen = { workspace = true }
[dev-dependencies]
wasm-bindgen-test = { workspace = true }
[build-dependencies]
ide-ci = { path = "../../../../build/ci_utils" }
bytes = { workspace = true }
futures = { workspace = true }
reqwest = { workspace = true }
tokio = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
websocket = "0.26.5"

View File

@ -0,0 +1,29 @@
//! A module containing structures and traits used in parser API.
use crate::prelude::*;
// ===========
// == Error ==
// ===========
/// A result of parsing code.
pub type Result<T> = std::result::Result<T, Error>;
/// An error which may be result of parsing code.
#[derive(Debug, Fail)]
pub enum Error {
/// Error due to inner workings of the parser.
#[fail(display = "Internal parser error: {:?}.", _0)]
ParsingError(String),
/// Error related to wrapping = communication with the parser service.
#[fail(display = "Interop error: {}.", _0)]
InteropError(#[cause] Box<dyn Fail>),
}
/// Wraps an arbitrary `std::error::Error` as an `InteropError.`
pub fn interop_error<T>(error: T) -> Error
where T: Fail {
Error::InteropError(Box::new(error))
}

View File

@ -4,11 +4,6 @@ use crate::prelude::*;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use crate::api; use crate::api;
use crate::api::Ast;
use crate::from_json_str_without_recursion_limit;
use ast::id_map::JsonIdMap;
use ast::IdMap;
@ -43,10 +38,6 @@ impl From<JsValue> for Error {
#[wasm_bindgen(module = "/pkg/scala-parser.js")] #[wasm_bindgen(module = "/pkg/scala-parser.js")]
extern "C" { extern "C" {
#[wasm_bindgen(catch)]
fn parse(input: String, ids: String) -> std::result::Result<String, JsValue>;
#[wasm_bindgen(catch)]
fn parse_with_metadata(content: String) -> std::result::Result<String, JsValue>;
#[wasm_bindgen(catch)] #[wasm_bindgen(catch)]
fn doc_parser_generate_html_source(content: String) -> std::result::Result<String, JsValue>; fn doc_parser_generate_html_source(content: String) -> std::result::Result<String, JsValue>;
#[wasm_bindgen(catch)] #[wasm_bindgen(catch)]
@ -65,31 +56,6 @@ impl Client {
Ok(Client {}) Ok(Client {})
} }
/// Parses Enso code with JS-based parser.
pub fn parse(&self, program: String, ids: IdMap) -> api::Result<Ast> {
let ast = || {
let ids = JsonIdMap::from_id_map(&ids, &program.clone().into());
let json_ids = serde_json::to_string(&ids)?;
let json_ast = parse(program, json_ids)?;
let ast = from_json_str_without_recursion_limit(&json_ast)?;
Result::Ok(ast)
};
Ok(ast()?)
}
/// Parses Enso code with metadata.
pub fn parse_with_metadata<M: api::Metadata>(
&self,
program: String,
) -> api::Result<api::ParsedSourceFile<M>> {
let result = || {
let json = &parse_with_metadata(program)?;
let module = from_json_str_without_recursion_limit(json)?;
Result::Ok(module)
};
Ok(result()?)
}
/// Calls JS doc parser to generate HTML from documented Enso code. /// Calls JS doc parser to generate HTML from documented Enso code.
pub fn generate_html_docs(&self, program: String) -> api::Result<String> { pub fn generate_html_docs(&self, program: String) -> api::Result<String> {
let html_code = || { let html_code = || {

View File

@ -0,0 +1,108 @@
//! Crate wrapping parser API in nice-to-use Rust code.
//!
//! The Parser is a library written in scala. There are two implementations of Rust wrappers to
//! this parser: one for local parser which binds scala parser compiled to WebAssembly to the Rust
//! crate. The second is calling a Parser running remotely using WebSockets.
// === Features ===
#![feature(trait_alias)]
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
#![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::let_and_return)]
// === Non-Standard Linter Configuration ===
#![warn(missing_docs)]
#![warn(trivial_casts)]
#![warn(trivial_numeric_casts)]
#![warn(unused_import_braces)]
#![warn(unused_qualifications)]
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
// ==============
// === Export ===
// ==============
pub mod api;
mod jsclient;
mod wsclient;
use crate::prelude::*;
use std::panic;
#[allow(missing_docs)]
pub mod prelude {
pub use ast::traits::*;
pub use enso_prelude::*;
pub use enso_profiler as profiler;
pub use enso_profiler::prelude::*;
}
// ==========================================
// === Documentation Parser and Generator ===
// ==========================================
/// Handle to a doc parser implementation.
///
/// Currently this component is implemented as a wrapper over documentation
/// parser written in Scala. Depending on compilation target (native or wasm)
/// it uses either implementation provided by `wsclient` or `jsclient`.
#[derive(Clone, CloneRef, Debug, Deref, DerefMut)]
pub struct DocParser(pub Rc<RefCell<Client>>);
impl DocParser {
/// Obtains a default doc parser implementation.
#[cfg(not(target_arch = "wasm32"))]
pub fn new() -> api::Result<DocParser> {
let client = wsclient::Client::new()?;
let doc_parser = Rc::new(RefCell::new(client));
Ok(DocParser(doc_parser))
}
/// Obtains a default doc parser implementation.
#[cfg(target_arch = "wasm32")]
pub fn new() -> api::Result<DocParser> {
let client = jsclient::Client::new()?;
let doc_parser = Rc::new(RefCell::new(client));
Ok(DocParser(doc_parser))
}
/// Obtains a default doc parser implementation, panicking in case of failure.
pub fn new_or_panic() -> DocParser {
DocParser::new().unwrap_or_else(|e| panic!("Failed to create doc parser: {e:?}"))
}
/// Parses program with documentation and generates HTML code.
/// If the program does not have any documentation will return empty string.
pub fn generate_html_docs(&self, program: String) -> api::Result<String> {
self.borrow_mut().generate_html_docs(program)
}
/// Parses pure documentation code and generates HTML code.
/// Will return empty string for empty entry.
pub fn generate_html_doc_pure(&self, code: String) -> api::Result<String> {
self.borrow_mut().generate_html_doc_pure(code)
}
}
// === Support ===
/// Websocket parser client.
/// Used as an interface for our (scala) parser.
#[cfg(not(target_arch = "wasm32"))]
type Client = wsclient::Client;
/// Javascript parser client.
/// Used as an interface for our (scala) parser.
#[cfg(target_arch = "wasm32")]
type Client = jsclient::Client;

View File

@ -4,12 +4,7 @@ use crate::api::Error::*;
use crate::prelude::*; use crate::prelude::*;
use crate::api; use crate::api;
use crate::api::Ast;
use crate::api::Metadata;
use crate::api::ParsedSourceFile;
use ast::id_map::JsonIdMap;
use ast::IdMap;
use std::fmt::Formatter; use std::fmt::Formatter;
use websocket::stream::sync::TcpStream; use websocket::stream::sync::TcpStream;
use websocket::ClientBuilder; use websocket::ClientBuilder;
@ -93,24 +88,10 @@ impl From<serde_json::error::Error> for Error {
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum Request { pub enum Request {
ParseRequest { program: String, ids: JsonIdMap },
ParseRequestWithMetadata { content: String },
DocParserGenerateHtmlSource { program: String }, DocParserGenerateHtmlSource { program: String },
DocParserGenerateHtmlFromDoc { code: String }, DocParserGenerateHtmlFromDoc { code: String },
} }
/// All responses that Parser Service might reply with.
#[derive(Debug, serde::Deserialize)]
pub enum Response<M> {
#[serde(bound(deserialize = "M: Metadata"))]
Success {
module: ParsedSourceFile<M>,
},
Error {
message: String,
},
}
/// All responses that Doc Parser Service might reply with. /// All responses that Doc Parser Service might reply with.
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
pub enum ResponseDoc { pub enum ResponseDoc {
@ -171,19 +152,6 @@ mod internal {
Ok(()) Ok(())
} }
/// Obtains a text message from peer and deserializes it using JSON
/// into a `Response`.
///
/// Should be called exactly once after each `send_request` invocation.
pub fn recv_response<M: Metadata>(&mut self) -> Result<Response<M>> {
let response = self.connection.recv_message()?;
match response {
websocket::OwnedMessage::Text(text) =>
crate::from_json_str_without_recursion_limit(&text).map_err(Into::into),
_ => Err(Error::NonTextResponse(response)),
}
}
/// Obtains a text message from peer and deserializes it using JSON /// Obtains a text message from peer and deserializes it using JSON
/// into a `ResponseDoc`. /// into a `ResponseDoc`.
/// ///
@ -196,15 +164,6 @@ mod internal {
} }
} }
/// Sends given `Request` to peer and receives a `Response`.
///
/// Both request and response are exchanged in JSON using text messages
/// over WebSocket.
pub fn rpc_call<M: Metadata>(&mut self, request: Request) -> Result<Response<M>> {
self.send_request(request)?;
self.recv_response()
}
/// Sends given `Request` to peer and receives a `ResponseDoc`. /// Sends given `Request` to peer and receives a `ResponseDoc`.
/// ///
/// Both request and response are exchanged in JSON using text messages /// Both request and response are exchanged in JSON using text messages
@ -237,30 +196,6 @@ impl Client {
Ok(client) Ok(client)
} }
/// Sends a request to parser service to parse Enso code.
pub fn parse(&mut self, program: String, ids: IdMap) -> api::Result<Ast> {
let ids = JsonIdMap::from_id_map(&ids, &program.as_str().into());
let request = Request::ParseRequest { program, ids };
let response = self.rpc_call::<serde_json::Value>(request)?;
match response {
Response::Success { module } => Ok(module.ast.into()),
Response::Error { message } => Err(ParsingError(message)),
}
}
/// Sends a request to parser service to parse code with metadata.
pub fn parse_with_metadata<M: Metadata>(
&mut self,
program: String,
) -> api::Result<ParsedSourceFile<M>> {
let request = Request::ParseRequestWithMetadata { content: program };
let response = self.rpc_call(request)?;
match response {
Response::Success { module } => Ok(module),
Response::Error { message } => Err(ParsingError(message)),
}
}
/// Sends a request to parser service to generate HTML code from documented Enso code. /// Sends a request to parser service to generate HTML code from documented Enso code.
pub fn generate_html_docs(&mut self, program: String) -> api::Result<String> { pub fn generate_html_docs(&mut self, program: String) -> api::Result<String> {
let request = Request::DocParserGenerateHtmlSource { program }; let request = Request::DocParserGenerateHtmlSource { program };

View File

@ -1,37 +1,19 @@
[package] [package]
name = "parser-scala" name = "parser"
version = "0.1.0" version = "0.1.0"
authors = ["Enso Team <contact@enso.org>"] authors = ["Enso Team <contact@enso.org>"]
edition = "2021" edition = "2021"
build = "build.rs"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
ast = { path = "../ast/impl" } ast = { path = "../ast/impl" }
enso-data-structures = { path = "../../../../lib/rust/data-structures" } enso-parser = { path = "../../../../lib/rust/parser" }
enso-prelude = { path = "../../../../lib/rust/prelude" } enso-prelude = { path = "../../../../lib/rust/prelude" }
enso-profiler = { path = "../../../../lib/rust/profiler" } enso-profiler = { path = "../../../../lib/rust/profiler" }
enso-text = { path = "../../../../lib/rust/text" }
console_error_panic_hook = { workspace = true }
failure = { workspace = true }
js-sys = { workspace = true }
matches = { workspace = true }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["unbounded_depth"] } serde_json = { version = "1.0", features = ["unbounded_depth"] }
uuid = { version = "0.8", features = ["serde", "v5", "wasm-bindgen"] } enso-text = { path = "../../../../lib/rust/text" }
wasm-bindgen = { workspace = true } failure = { version = "0.1" }
uuid = { version = "0.8" }
[dev-dependencies]
wasm-bindgen-test = { workspace = true }
[build-dependencies]
ide-ci = { path = "../../../../build/ci_utils" }
bytes = { workspace = true }
futures = { workspace = true }
reqwest = { workspace = true }
tokio = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
websocket = "0.26.5"

View File

@ -1,8 +1,7 @@
//! A module containing structures and traits used in parser API. //! A module containing structures and traits used in parser API.
use crate::prelude::*; use enso_prelude::*;
use enso_text::index::*; use enso_text::index::*;
use enso_text::traits::*;
use enso_text::unit::*; use enso_text::unit::*;
use ast::id_map::JsonIdMap; use ast::id_map::JsonIdMap;
@ -10,16 +9,66 @@ use ast::HasIdMap;
use ast::HasRepr; use ast::HasRepr;
use ast::IdMap; use ast::IdMap;
use enso_text::Range; use enso_text::Range;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde::Serialize;
// ==============
// === Export ===
// ==============
pub use ast::Ast; /// A parsed file containing source code and attached metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParsedSourceFile<M> {
/// Ast representation.
pub ast: ast::known::Module,
/// Raw metadata in json.
pub metadata: M,
}
const NEWLINES_BEFORE_TAG: usize = 3;
const METADATA_TAG: &str = "#### METADATA ####";
impl<M: Metadata> ParsedSourceFile<M> {
/// Serialize to the SourceFile structure,
pub fn serialize(&self) -> std::result::Result<SourceFile, serde_json::Error> {
fn to_json_single_line(
val: &impl serde::Serialize,
) -> std::result::Result<String, serde_json::Error> {
let json = serde_json::to_string(val)?;
let line = json.chars().filter(|c| *c != '\n' && *c != '\r').collect();
Ok(line)
}
let code = self.ast.repr().into();
let before_tag = "\n".repeat(NEWLINES_BEFORE_TAG);
let before_idmap = "\n";
let json_id_map = JsonIdMap::from_id_map(&self.ast.id_map(), &code);
let id_map = to_json_single_line(&json_id_map)?;
let before_metadata = "\n";
let metadata = to_json_single_line(&self.metadata)?;
let id_map_start =
code.len().value + before_tag.len() + METADATA_TAG.len() + before_idmap.len();
let id_map_start_bytes = Byte::from(id_map_start);
let metadata_start = id_map_start + id_map.len() + before_metadata.len();
let metadata_start_bytes = Byte::from(metadata_start);
let content = format!(
"{code}{before_tag}{METADATA_TAG}{before_idmap}{id_map}{before_metadata}{metadata}"
);
Ok(SourceFile {
content,
code: (0.byte()..code.len().to_byte()).into(),
id_map: (id_map_start_bytes..id_map_start_bytes + ByteDiff::from(id_map.len())).into(),
metadata: (metadata_start_bytes..metadata_start_bytes + ByteDiff::from(metadata.len()))
.into(),
})
}
}
impl<M: Metadata> Display for ParsedSourceFile<M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.serialize() {
Ok(serialized) => write!(f, "{serialized}"),
Err(_) => write!(f, "[UNREPRESENTABLE SOURCE FILE]"),
}
}
}
@ -48,7 +97,9 @@ pub trait PruneUnusedIds {
} }
/// Things that are metadata. /// Things that are metadata.
pub trait Metadata: Default + Serialize + DeserializeOwned + PruneUnusedIds {} pub trait Metadata:
Default + serde::de::DeserializeOwned + serde::Serialize + PruneUnusedIds {
}
/// Raw metadata. /// Raw metadata.
impl PruneUnusedIds for serde_json::Value {} impl PruneUnusedIds for serde_json::Value {}
@ -164,98 +215,6 @@ impl SourceFile {
} }
// === Parsed Source File ===
/// Parsed file / module with metadata.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(bound = "M: Metadata")]
#[serde(from = "ParsedSourceFileWithUnusedIds<M>")]
pub struct ParsedSourceFile<M> {
/// Ast representation.
pub ast: ast::known::Module,
/// Raw metadata in json.
pub metadata: M,
}
/// Helper for deserialization. `metadata` is filled with `default()` value if not present or is
/// invalid.
///
/// [`PruneUnusedIds::prune_unused_ids`] is called on deserialization.
#[derive(Deserialize)]
struct ParsedSourceFileWithUnusedIds<Metadata> {
ast: ast::known::Module,
#[serde(bound(deserialize = "Metadata:Default+DeserializeOwned"))]
#[serde(deserialize_with = "enso_prelude::deserialize_or_default")]
metadata: Metadata,
}
impl<M: Metadata> From<ParsedSourceFileWithUnusedIds<M>> for ParsedSourceFile<M> {
fn from(file: ParsedSourceFileWithUnusedIds<M>) -> Self {
let ast = file.ast;
let mut metadata = file.metadata;
metadata.prune_unused_ids(&ast.id_map());
Self { ast, metadata }
}
}
impl<M: Metadata> TryFrom<&ParsedSourceFile<M>> for String {
type Error = serde_json::Error;
fn try_from(val: &ParsedSourceFile<M>) -> std::result::Result<String, Self::Error> {
Ok(val.serialize()?.content)
}
}
impl<M: Metadata> Display for ParsedSourceFile<M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.serialize() {
Ok(serialized) => write!(f, "{serialized}"),
Err(_) => write!(f, "[NOT REPRESENTABLE SOURCE FILE]"),
}
}
}
// === Parsed Source File Serialization ===
const NEWLINES_BEFORE_TAG: usize = 3;
const METADATA_TAG: &str = "#### METADATA ####";
fn to_json_single_line(val: &impl Serialize) -> std::result::Result<String, serde_json::Error> {
let json = serde_json::to_string(val)?;
let line = json.chars().filter(|c| *c != '\n' && *c != '\r').collect();
Ok(line)
}
impl<M: Metadata> ParsedSourceFile<M> {
/// Serialize to the SourceFile structure,
pub fn serialize(&self) -> std::result::Result<SourceFile, serde_json::Error> {
let code = self.ast.repr().into();
let before_tag = "\n".repeat(NEWLINES_BEFORE_TAG);
let before_idmap = "\n";
let json_id_map = JsonIdMap::from_id_map(&self.ast.id_map(), &code);
let id_map = to_json_single_line(&json_id_map)?;
let before_metadata = "\n";
let metadata = to_json_single_line(&self.metadata)?;
let id_map_start =
code.len().value + before_tag.len() + METADATA_TAG.len() + before_idmap.len();
let id_map_start_bytes = Byte::from(id_map_start);
let metadata_start = id_map_start + id_map.len() + before_metadata.len();
let metadata_start_bytes = Byte::from(metadata_start);
Ok(SourceFile {
content: format!(
"{code}{before_tag}{METADATA_TAG}{before_idmap}{id_map}{before_metadata}{metadata}"
),
code: (0.byte()..code.len().to_byte()).into(),
id_map: (id_map_start_bytes..id_map_start_bytes + ByteDiff::from(id_map.len()))
.into(),
metadata: (metadata_start_bytes..metadata_start_bytes + ByteDiff::from(metadata.len()))
.into(),
})
}
}
// =========== // ===========
// == Error == // == Error ==
@ -293,55 +252,3 @@ pub fn interop_error<T>(error: T) -> Error
where T: Fail { where T: Fail {
Error::InteropError(Box::new(error)) Error::InteropError(Box::new(error))
} }
// =============
// === Tests ===
// =============
#[cfg(test)]
mod test {
use super::*;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
struct Metadata {
foo: usize,
}
impl PruneUnusedIds for Metadata {}
impl crate::api::Metadata for Metadata {}
#[test]
fn serializing_parsed_source_file() {
let main = ast::Ast::var("main");
let node = ast::Ast::infix_var("2", "+", "2");
let infix = ast::Ast::infix(main, "=", node);
let ast: ast::known::Module = ast::Ast::one_line_module(infix).try_into().unwrap();
let repr = ast.repr().into();
let metadata = Metadata { foo: 321 };
let source = ParsedSourceFile { ast, metadata };
let serialized = source.serialize().unwrap();
let expected_json_id_map = JsonIdMap::from_id_map(&source.ast.id_map(), &repr);
let expected_id_map = to_json_single_line(&expected_json_id_map).unwrap();
let expected_metadata = to_json_single_line(&source.metadata).unwrap();
let expected_content = format!(
r#"main = 2 + 2
#### METADATA ####
{expected_id_map}
{expected_metadata}"#
);
assert_eq!(serialized.content, expected_content);
assert_eq!(serialized.code_slice(), "main = 2 + 2");
assert_eq!(serialized.id_map_slice(), expected_id_map.as_str());
assert_eq!(serialized.metadata_slice(), expected_metadata.as_str());
// Check that SourceFile round-trips.
let source_file = SourceFile::new(serialized.content.clone());
assert_eq!(source_file, serialized);
}
}

View File

@ -1,49 +0,0 @@
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
#![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::let_and_return)]
use enso_prelude::*;
/// Simple interactive tester - calls parser with its argument (or a
/// hardcoded default) and prints the result, then calls doc parser
/// and prints the HTML code or an error message.
fn main() {
let default_input = String::from("import Foo.Bar\nfoo = a + 2");
let program = std::env::args().nth(1).unwrap_or(default_input);
debug!("Will parse: {}", program);
let parser = parser_scala::Parser::new_or_panic();
let output = parser.parse(program, default());
match output {
Ok(result) => debug!("Parser responded with: {result:?}"),
Err(e) => debug!("Failed to obtain a response: {e:?}"),
}
let default_input = String::from("##\n DEPRECATED\n Foo bar baz\ntype Foo\n type Bar");
let program = std::env::args().nth(1).unwrap_or(default_input);
debug!("Will parse: {}", program);
let parser = parser_scala::DocParser::new_or_panic();
let output = parser.generate_html_docs(program);
match output {
Ok(result) => debug!("Doc parser responded with: {result:?}"),
Err(e) => debug!("Failed to obtain a response: {e:?}"),
}
let default_input = String::from("Computes the _logical_ conjunction of *two* booleans");
let program = std::env::args().nth(1).unwrap_or(default_input);
debug!("Will parse: {}", program);
let parser = parser_scala::DocParser::new_or_panic();
let output = parser.generate_html_doc_pure(program);
match output {
Ok(result) => debug!("Doc parser responded with: {result:?}"),
Err(e) => debug!("Failed to obtain a response: {e:?}"),
}
}

View File

@ -1,11 +1,10 @@
//! Crate wrapping parser API in nice-to-use Rust code. //! [`Parser`] adapts a [`enso_syntax::Parser`] to produce the [`ast::AST`]/[`span_tree`]
//! //! representation used by the Graph Editor.
//! The Parser is a library written in scala. There are two implementations of Rust wrappers to
//! this parser: one for local parser which binds scala parser compiled to WebAssembly to the Rust
//! crate. The second is calling a Parser running remotely using WebSockets.
// === Features === // === Features ===
#![feature(trait_alias)] #![feature(extend_one)]
#![feature(let_chains)]
#![feature(if_let_guard)]
// === Standard Linter Configuration === // === Standard Linter Configuration ===
#![deny(non_ascii_idents)] #![deny(non_ascii_idents)]
#![warn(unsafe_code)] #![warn(unsafe_code)]
@ -13,12 +12,19 @@
#![allow(clippy::let_and_return)] #![allow(clippy::let_and_return)]
// === Non-Standard Linter Configuration === // === Non-Standard Linter Configuration ===
#![warn(missing_docs)] #![warn(missing_docs)]
#![warn(trivial_casts)]
#![warn(trivial_numeric_casts)] use enso_prelude::*;
#![warn(unused_import_braces)] use enso_profiler::prelude::*;
#![warn(unused_qualifications)]
#![warn(missing_copy_implementations)] use ast::prelude::FallibleResult;
#![warn(missing_debug_implementations)] use ast::HasIdMap;
use ast::IdMap;
use enso_profiler as profiler;
mod translation;
// ============== // ==============
@ -29,126 +35,113 @@ pub mod api;
mod jsclient;
pub mod test_utils;
mod wsclient;
use crate::prelude::*;
use ast::Ast;
use ast::BlockLine;
use ast::IdMap;
use std::panic;
#[allow(missing_docs)]
pub mod prelude {
pub use ast::traits::*;
pub use enso_prelude::*;
pub use enso_profiler as profiler;
pub use enso_profiler::prelude::*;
}
// ============== // ==============
// === Parser === // === Parser ===
// ============== // ==============
/// Websocket parser client. /// Parses Enso syntax.
/// Used as an interface for our (scala) parser. #[derive(Debug, Default, Clone, CloneRef)]
#[cfg(not(target_arch = "wasm32"))] pub struct Parser {
type Client = wsclient::Client; parser: Rc<enso_parser::Parser>,
/// Javascript parser client. }
/// Used as an interface for our (scala) parser.
#[cfg(target_arch = "wasm32")]
type Client = jsclient::Client;
/// Handle to a parser implementation.
/// // === Core methods provided by the underlying parser ===
/// Currently this component is implemented as a wrapper over parser written
/// in Scala. Depending on compilation target (native or wasm) it uses either
/// implementation provided by `wsclient` or `jsclient`.
#[derive(Clone, CloneRef, Debug, Deref, DerefMut)]
pub struct Parser(pub Rc<RefCell<Client>>);
impl Parser { impl Parser {
/// Obtains a default parser implementation. /// Create a new parser.
#[cfg(not(target_arch = "wasm32"))] pub fn new() -> Self {
pub fn new() -> api::Result<Parser> { let parser = Rc::new(enso_parser::Parser::new());
let client = wsclient::Client::new()?; Self { parser }
let parser = Rc::new(RefCell::new(client));
Ok(Parser(parser))
} }
/// Obtains a default parser implementation. /// Parse the given source code with the specified ID map.
#[cfg(target_arch = "wasm32")] #[profile(Task)]
pub fn new() -> api::Result<Parser> { pub fn parse(&self, program: impl Str, ids: IdMap) -> ast::Ast {
let client = jsclient::Client::new()?; let tree = self.parser.run(program.as_ref());
let parser = Rc::new(RefCell::new(client)); let ids = ids
Ok(Parser(parser)) .vec
.into_iter()
.map(|(range, id)| ((range.start.value, range.end.value), id))
.collect();
translation::tree_to_ast(&tree, ids)
} }
/// Obtains a default parser implementation, panicking in case of failure. /// Parse the given source code, using metadata (including ID map) found in the input string.
pub fn new_or_panic() -> Parser { #[profile(Task)]
Parser::new().unwrap_or_else(|e| panic!("Failed to create a parser: {e:?}"))
}
/// Parse program.
pub fn parse(&self, program: String, ids: IdMap) -> api::Result<Ast> {
self.borrow_mut().parse(program, ids)
}
/// Parse contents of the program source file, where program code may be followed by idmap and
/// metadata.
///
/// If metadata deserialization fails, error is ignored and default value for metadata is used.
/// Other errors are returned through `Result`.
#[profile(Detail)]
pub fn parse_with_metadata<M: api::Metadata>( pub fn parse_with_metadata<M: api::Metadata>(
&self, &self,
program: String, program: impl Str,
) -> api::Result<api::ParsedSourceFile<M>> { ) -> api::ParsedSourceFile<M> {
self.borrow_mut().parse_with_metadata(program) let (code, meta) = enso_parser::metadata::extract(program.as_ref());
if meta.is_none() {
info!("parse_with_metadata: No metadata found.");
}
let meta_lines = meta.and_then(|meta| meta.split_once('\n'));
if meta.is_some() && meta_lines.is_none() {
warn!("parse_with_metadata: Expected two lines of metadata.");
}
let ids = meta_lines.map(|lines| lines.0);
let application_metadata = meta_lines.map(|lines| lines.1);
let ids = enso_parser::metadata::parse_metadata(ids.unwrap_or_default());
if ids.is_none() {
warn!("parse_with_metadata: Failed to parse ID map.");
}
let ids = ids
.unwrap_or_default()
.iter()
.map(|((start, len), id)| ((*start, start + len), uuid::Uuid::from_u128(id.as_u128())))
.collect();
let tree = self.parser.run(code);
let metadata = application_metadata.and_then(|meta| serde_json::from_str(meta).ok());
if application_metadata.is_some() && metadata.is_none() {
warn!("parse_with_metadata: Failed to deserialize metadata.");
}
let ast = translation::tree_to_ast(&tree, ids);
let id_map = ast.id_map();
let ast = ast::known::Module::try_from(ast).unwrap();
let mut metadata: M = metadata.unwrap_or_default();
metadata.prune_unused_ids(&id_map);
api::ParsedSourceFile { ast, metadata }
} }
}
/// Parse program into module.
// === Convenience methods ===
impl Parser {
/// Parse the given source code as a module, and return a [`ast::known::Module`].
pub fn parse_module(&self, program: impl Str, ids: IdMap) -> api::Result<ast::known::Module> { pub fn parse_module(&self, program: impl Str, ids: IdMap) -> api::Result<ast::known::Module> {
let ast = self.parse(program.into(), ids)?; let ast = self.parse(program.as_ref(), ids);
ast::known::Module::try_from(ast).map_err(|_| api::Error::NonModuleRoot) ast::known::Module::try_from(ast).map_err(|_| api::Error::NonModuleRoot)
} }
/// Program is expected to be single non-empty line module. The line's AST is /// Parse the given line of source code, and return just the [`ast::Ast`].
/// returned. The program is parsed with empty IdMap. pub fn parse_line_ast(&self, program: impl Str) -> FallibleResult<ast::Ast> {
#[profile(Debug)] self.parse_line(program).map(|line| line.elem)
pub fn parse_line_ast(&self, program: impl Str) -> FallibleResult<Ast> {
self.parse_line_with_id_map(program, default()).map(|line| line.elem)
} }
/// Program is expected to be single non-empty line module. The line's AST is /// Parse the given line of source code.
/// returned. The program is parsed with empty IdMap. pub fn parse_line(&self, program: impl Str) -> FallibleResult<ast::BlockLine<ast::Ast>> {
pub fn parse_line(&self, program: impl Str) -> FallibleResult<BlockLine<Ast>> {
self.parse_line_with_id_map(program, default()) self.parse_line_with_id_map(program, default())
} }
/// Program is expected to be single non-empty line module. The line's AST is returned. /// Parse the given line of source code, attaching the given IDs.
pub fn parse_line_ast_with_id_map( pub fn parse_line_ast_with_id_map(
&self, &self,
program: impl Str, program: impl Str,
id_map: IdMap, id_map: IdMap,
) -> FallibleResult<Ast> { ) -> FallibleResult<ast::Ast> {
self.parse_line_with_id_map(program, id_map).map(|line| line.elem) self.parse_line_with_id_map(program, id_map).map(|line| line.elem)
} }
/// Program is expected to be single non-empty line module. Return the parsed line. /// Program is expected to be single non-empty line module. Return the parsed line.
pub fn parse_line_with_id_map( fn parse_line_with_id_map(
&self, &self,
program: impl Str, program: impl Str,
id_map: IdMap, id_map: IdMap,
) -> FallibleResult<BlockLine<Ast>> { ) -> FallibleResult<ast::BlockLine<ast::Ast>> {
let module = self.parse_module(program, id_map)?; let module = self.parse_module(program, id_map)?;
let mut lines = let mut lines =
module.lines.clone().into_iter().filter_map(|line| line.map(|elem| elem).transpose()); module.lines.clone().into_iter().filter_map(|line| line.map(|elem| elem).transpose());
if let Some(first_non_empty_line) = lines.next() { if let Some(first_non_empty_line) = lines.next() {
@ -163,63 +156,68 @@ impl Parser {
} }
} }
/// Deserialize value from json text. #[cfg(test)]
/// mod tests {
/// Unlike `serde_json::from_str` it runs with recursion limit disabled, allowing deserialization of use super::*;
/// deeply nested ASTs. use ast::HasRepr;
pub fn from_json_str_without_recursion_limit<'de, Value: serde::Deserialize<'de>>(
json_text: &'de str,
) -> Result<Value, serde_json::Error> {
let mut de = serde_json::Deserializer::from_str(json_text);
de.disable_recursion_limit();
Value::deserialize(&mut de)
}
#[test]
fn test_group_repr() {
// ========================================== let code = "bar (Foo (a b))";
// === Documentation Parser and Generator === let ast = Parser::new().parse_line_ast(code).unwrap();
// ========================================== assert_eq!(ast.repr(), code);
/// Handle to a doc parser implementation.
///
/// Currently this component is implemented as a wrapper over documentation
/// parser written in Scala. Depending on compilation target (native or wasm)
/// it uses either implementation provided by `wsclient` or `jsclient`.
#[derive(Clone, CloneRef, Debug, Deref, DerefMut)]
pub struct DocParser(pub Rc<RefCell<Client>>);
impl DocParser {
/// Obtains a default doc parser implementation.
#[cfg(not(target_arch = "wasm32"))]
pub fn new() -> api::Result<DocParser> {
let client = wsclient::Client::new()?;
let doc_parser = Rc::new(RefCell::new(client));
Ok(DocParser(doc_parser))
} }
/// Obtains a default doc parser implementation. #[test]
#[cfg(target_arch = "wasm32")] fn test_text_repr() {
pub fn new() -> api::Result<DocParser> { let code = "operator17 = operator16.order_by (Sort_Column.Name 'Orders Value' Sort_Direction.Descending)";
let client = jsclient::Client::new()?; let ast = Parser::new().parse_line_ast(code).unwrap();
let doc_parser = Rc::new(RefCell::new(client)); assert_eq!(ast.repr(), code);
Ok(DocParser(doc_parser))
} }
/// Obtains a default doc parser implementation, panicking in case of failure. #[test]
pub fn new_or_panic() -> DocParser { fn test_orders_repr() {
DocParser::new().unwrap_or_else(|e| panic!("Failed to create doc parser: {e:?}")) let code = r#"
from Standard.Base import all
from Standard.Table import all
import Standard.Visualization
import Standard.Examples
main =
## The file contains three different sheets relating to operations of an
online store.
operator2 = enso_project.data / 'store_data.xlsx'
## Read the customers table.
operator3 = operator2.read (Excel (Worksheet 'Customers'))
## Read the products table.
operator4 = operator2.read (Excel (Worksheet 'Items'))
## Read the orders history.
operator5 = operator2.read (Excel (Worksheet 'Orders'))
## Join the item data to the order history, to get information on item
prices in the orders table.
operator8 = operator5.join operator4 Join_Kind.Inner ['Item ID']
operator1 = operator8.at 'Unit Price'
operator9 = operator8.at 'Quantity'
## Multiply item prices and counts to get total order value.
product1 = operator1 * operator9
operator10 = operator8.set 'Order Value' product1
## Group all orders by the Customer ID, to compute the total value of orders
placed by each client.
operator11 = operator10.aggregate [Aggregate_Column.Group_By 'Customer ID', Aggregate_Column.Sum 'Order Value' 'Orders Value']
## Join the customer data into orders table, to include names in the final
ranking.
operator16 = operator3.join operator11 Join_Kind.Inner ["Customer ID"]
## Sort the customers by their lifetime value, with the most valuable
customers at the start of the table.
operator17 = operator16.order_by (Sort_Column.Name 'Orders Value' Sort_Direction.Descending)
"#;
let ast = Parser::new().parse_module(code, default()).unwrap();
assert_eq!(ast.repr(), code);
} }
/// Parses program with documentation and generates HTML code. #[test]
/// If the program does not have any documentation will return empty string. fn test_as_lambda() {
pub fn generate_html_docs(&self, program: String) -> api::Result<String> { let ast = Parser::new().parse_line_ast("a->4").unwrap();
self.borrow_mut().generate_html_docs(program) assert!(ast::macros::as_lambda(&ast).is_some(), "{ast:?}");
}
/// Parses pure documentation code and generates HTML code.
/// Will return empty string for empty entry.
pub fn generate_html_doc_pure(&self, code: String) -> api::Result<String> {
self.borrow_mut().generate_html_doc_pure(code)
} }
} }

View File

@ -0,0 +1,998 @@
use enso_prelude::*;
use enso_profiler::prelude::*;
use ast::Ast;
use enso_parser::syntax;
use enso_parser::syntax::tree;
use enso_parser::syntax::Tree;
use enso_profiler as profiler;
use std::collections::BTreeMap;
/// Enable extra log messages and assertions.
const DEBUG: bool = false;
// =======================
// === Translation API ===
// =======================
/// Translates an [`AST`] from the [`Tree`] representation produced by [`enso_parser`] to the
/// [`Ast`] representation used by the GUI (see [`crate`] documentation for high-level overview).
/// The returned tree will contain IDs from the given map.
#[profile(Detail)]
pub fn tree_to_ast(mut tree: &Tree, ids: BTreeMap<(usize, usize), uuid::Uuid>) -> Ast {
use ast::HasRepr;
let mut context = Translate { ids, ..Default::default() };
let ast = loop {
match &*tree.variant {
tree::Variant::BodyBlock(block) => break context.translate_module(block),
tree::Variant::Invalid(tree::Invalid { ast, error }) => {
warn!("Parser reports invalid module: {}", error.message);
tree = ast
}
_ => unreachable!("enso_parser always returns a tree with a BodyBlock as root."),
}
};
if DEBUG {
debug_assert_eq!(ast.repr(), tree.code(), "Ast should represent same code as Tree.");
if !context.ids.is_empty() || !context.ids_missed.is_empty() {
warn!(
"ids not matched: {:?}\nids missed: {:?}\nids assigned: {:?}",
&context.ids, &context.ids_missed, &context.ids_assigned
);
}
}
ast
}
// === Implementation ===
#[derive(Debug, Default)]
struct Translate {
/// The offset, in bytes, from the beginning of the input source code. This must be tracked
/// during [`Tree`] traversal because [`Tree`] doesn't have absolute source references, only
/// token lengths.
offset: usize,
/// IDs to associate with byte ranges of the source code.
ids: BTreeMap<(usize, usize), uuid::Uuid>,
/// The [`AstBuilder`] interface supports associating IDs with byte ranges; however, it's
/// important to ensure that the byte ranges don't include any leading whitespace. This would
/// be a difficult invariant to maintain through careful usage of [`AstBuilder`]; instead,
/// we record where whitespace occurs in [`space_after`], and use that to adjust the byte
/// ranges.
space_after: BTreeMap<usize, usize>,
// === Diagnostic information used when [`DEBUG`] is enabled ===
/// IDs that were in [`ids`], and now have been successfully attached to [`Ast`] nodes.
ids_assigned: Vec<((usize, usize), uuid::Uuid)>,
/// Byte ranges of [`Ast`] nodes that we didn't find any IDs for.
ids_missed: Vec<(usize, usize)>,
}
impl Translate {
/// This must be called at the beginning of each [`Tree`], as they are processed in depth-first
/// order. It updates the internal counter to include the leading whitespace bytes, and returns
/// the visible width (indent) of the leading space.
fn visit_space(&mut self, span: &enso_parser::source::Span) -> usize {
let space = span.left_offset.code.repr.len();
self.space_after.insert(self.offset, space);
self.offset += space;
span.left_offset.visible.width_in_spaces
}
/// This must be called at the beginning of each [`Token`], as they are processed in depth-first
/// order. It updates the internal counter for the token's bytes, and returns its contents.
fn visit_token<T: Copy>(&mut self, token: &syntax::Token<T>) -> WithInitialSpace<String> {
self.visit_token_ref(syntax::token::Ref::<T>::from(token))
}
/// This must be called at the beginning of each [`Token`], as they are processed in depth-first
/// order. It updates the internal counter for the token's bytes, and returns its contents.
fn visit_token_ref<T>(&mut self, token: syntax::token::Ref<T>) -> WithInitialSpace<String> {
let space = token.left_offset.visible.width_in_spaces;
let body = token.code.to_string();
self.space_after.insert(self.offset, space);
self.offset += token.left_offset.code.repr.len();
self.offset += token.code.repr.len();
WithInitialSpace { space, body }
}
}
impl Translate {
/// Translate a [`Tree`].
/// The returned [`Ast`] can be [`None`] if an empty block is encountered.
fn translate(&mut self, tree: &Tree) -> WithInitialSpace<Option<Ast>> {
let space = self.visit_space(&tree.span);
let body = self.translate_expression_body(tree);
WithInitialSpace { space, body }
}
/// Translate a [`Tree`], except for the leading space.
/// This can return [`None`] if an empty block is encountered.
fn translate_expression_body(&mut self, tree: &Tree) -> Option<Ast> {
let builder = self.start_ast();
Some(match &*tree.variant {
tree::Variant::BodyBlock(block) => {
let block = self.translate_block(&block.statements)?.expect_unspaced();
self.finish_ast(block, builder)
}
tree::Variant::Ident(tree::Ident { token }) => {
let name = self.visit_token(token).expect_unspaced();
match token.is_type {
true => self.finish_ast(ast::Cons { name }, builder),
false => self.finish_ast(ast::Var { name }, builder),
}
}
tree::Variant::Number(tree::Number { base, integer, fractional_digits }) => {
let base = base.as_ref().map(|base| self.visit_token(base).expect_unspaced());
let mut int = integer
.as_ref()
.map(|integer| self.visit_token(integer).expect_unspaced())
.unwrap_or_default();
if let Some(tree::FractionalDigits { dot, digits }) = fractional_digits {
let dot = self.visit_token(dot).expect_unspaced();
let digits = self.visit_token(digits).expect_unspaced();
int = format!("{int}{dot}{digits}");
}
self.finish_ast(ast::Number { base, int }, builder)
}
tree::Variant::App(tree::App { func, arg }) => {
let func = self.translate(func);
let arg = self.translate(arg);
let app = maybe_prefix(func, arg).expect_unspaced()?;
self.finish_ast(app, builder)
}
tree::Variant::OprApp(tree::OprApp { lhs: Some(_), opr: Ok(opr), rhs: Some(_) })
if opr.properties.is_arrow() =>
{
let ast = ast::Tree::lambda(self.translate_items(tree));
self.finish_ast(ast, builder)
}
tree::Variant::OprApp(tree::OprApp { lhs, opr, rhs }) => {
let larg = lhs.as_ref().map(|a| self.translate(a)).unwrap_or_default();
let opr = self.translate_operators(opr);
let rarg = rhs.as_ref().map(|a| self.translate(a)).unwrap_or_default();
let opr_app = infix(larg, opr, rarg).expect_unspaced();
self.finish_ast(opr_app, builder)
}
tree::Variant::OprSectionBoundary(tree::OprSectionBoundary { ast, .. }) =>
self.translate(ast).expect_unspaced()?,
tree::Variant::Function(func) => {
let func = self.translate_function(func);
self.finish_ast(func, builder)
}
tree::Variant::ForeignFunction(func) => {
let func = self.translate_foreign_function(func);
self.finish_ast(func, builder)
}
tree::Variant::UnaryOprApp(tree::UnaryOprApp { opr, rhs }) => {
let opr = self.translate_operator(opr);
if let Some(arg) = rhs {
let non_block_operand = "Unary operator cannot be applied to an (empty) block.";
let arg = self.translate(arg).expect(non_block_operand);
let section = section_right(opr, arg).expect_unspaced();
self.finish_ast(section, builder)
} else {
let opr = opr.expect_unspaced();
self.finish_ast(ast::SectionSides { opr }, builder)
}
}
tree::Variant::Assignment(tree::Assignment { pattern, equals, expr }) =>
self.opr_app(pattern, equals, expr).expect_unspaced(),
tree::Variant::OperatorBlockApplication(app) => {
let tree::OperatorBlockApplication { lhs, expressions, excess } = app;
let func = lhs.as_ref().map(|lhs| self.translate(lhs)).unwrap_or_default();
let block = self.translate_operator_block(expressions, excess);
let app = maybe_prefix(func, block).expect_unspaced()?;
self.finish_ast(app, builder)
}
tree::Variant::TemplateFunction(tree::TemplateFunction { ast, .. }) =>
self.translate(ast).expect_unspaced()?,
tree::Variant::Wildcard(tree::Wildcard { token, .. }) => {
self.visit_token(token).expect_unspaced();
self.finish_ast(ast::Blank {}, builder)
}
tree::Variant::ArgumentBlockApplication(app) => {
let tree::ArgumentBlockApplication { lhs, arguments } = app;
let func = lhs.as_ref().map(|lhs| self.translate(lhs)).unwrap_or_default();
let arg = self
.translate_block(arguments)
.map(|arg| arg.map(|arg| Some(Ast::from(arg))))
.unwrap_or_default();
let app = maybe_prefix(func, arg).expect_unspaced()?;
self.finish_ast(app, builder)
}
tree::Variant::DefaultApp(tree::DefaultApp { func, default }) => {
let func = self.translate(func);
let arg_builder = self.start_ast();
let default = self.visit_token(default);
let arg = default.map(|name| self.finish_ast(ast::Var { name }, arg_builder));
let app = maybe_prefix(func, arg).expect_unspaced()?;
self.finish_ast(app, builder)
}
tree::Variant::NamedApp(tree::NamedApp { func, open, name, equals, arg, close }) => {
let func = self.translate(func);
let open = open.as_ref().map(|token| self.visit_token(token));
let name = self.visit_token(name);
let larg = name.map(|name| Ast::from(ast::Var { name }));
let opr = self.translate_operator(equals);
let non_block_operand = "Named-application operand cannot be an (empty) block.";
let rarg = self.translate(arg).expect(non_block_operand);
let mut arg = infix(larg, opr, rarg).map(Ast::from);
let close = close.as_ref().map(|token| self.visit_token(token));
if let Some(open) = open && let Some(close) = close {
arg = open.map(|open| group(open, arg, close));
}
let app = maybe_prefix(func, arg).expect_unspaced()?;
self.finish_ast(app, builder)
}
tree::Variant::TypeSignature(tree::TypeSignature { variable, operator, type_ }) =>
self.opr_app(variable, operator, type_).expect_unspaced(),
tree::Variant::TypeAnnotated(tree::TypeAnnotated { expression, operator, type_ }) =>
self.opr_app(expression, operator, type_).expect_unspaced(),
tree::Variant::AnnotatedBuiltin(tree::AnnotatedBuiltin {
token,
annotation,
newlines,
expression,
}) => {
let at = self.visit_token(token).expect_unspaced();
let func = self.visit_token(annotation);
let func =
func.map(|func| Ast::from(ast::Annotation { name: format!("{at}{func}") }));
let arg = expression.as_ref().map(|e| self.translate(e)).unwrap_or_default();
if !newlines.is_empty() {
error!("Multiline expression must be handled in translate_lines.");
}
let app = maybe_prefix(func, arg).expect_unspaced()?;
self.finish_ast(app, builder)
}
tree::Variant::Documented(tree::Documented { documentation: _, expression }) => {
warn!("Multiline expression should have been handled in translate_lines.");
self.translate(expression.as_ref()?).without_space()?
}
tree::Variant::Import(import) => {
let span_info = self.translate_items(tree);
let type_info = analyze_import(import).unwrap_or_default();
let ast = ast::Tree::expression(span_info).with_type_info(type_info);
self.finish_ast(ast, builder)
}
tree::Variant::TextLiteral(_) => {
self.translate_items(tree);
let ast = ast::Tree::text(tree.trimmed_code());
self.finish_ast(ast, builder)
}
tree::Variant::Group(_) => {
let span_info = self.translate_items(tree);
let ast = ast::Tree::group(span_info);
self.finish_ast(ast, builder)
}
_ => {
let ast = ast::Tree::expression(self.translate_items(tree));
self.finish_ast(ast, builder)
}
})
}
/// Translate [`Tree`]s to [`Ast`]s in a multi-line context (i.e. when building a block or
/// module).
fn translate_lines(&mut self, tree: &Tree, out: &mut Vec<WithInitialSpace<Option<Ast>>>) {
match &*tree.variant {
tree::Variant::AnnotatedBuiltin(tree::AnnotatedBuiltin {
token,
annotation,
newlines,
expression,
}) if !newlines.is_empty() => {
let space = self.visit_space(&tree.span);
let at = self.visit_token(token).expect_unspaced();
let annotation = self.visit_token(annotation).expect_unspaced();
let annotation = Ast::from(ast::Annotation { name: format!("{at}{annotation}") });
out.extend_one(WithInitialSpace { space, body: Some(annotation) });
self.translate_linebreaks(out, newlines);
if let Some(expression) = expression {
self.translate_lines(expression, out);
}
}
tree::Variant::Annotated(tree::Annotated {
token,
annotation,
argument,
newlines,
expression,
}) => {
let space = self.visit_space(&tree.span);
let at = self.visit_token(token).expect_unspaced();
let annotation = self.visit_token(annotation).expect_unspaced();
let body = Ast::from(ast::Annotation { name: format!("{at}{annotation}") });
let mut annotation = WithInitialSpace { space, body };
let argument =
argument.as_ref().and_then(|arg| ignore_space_if_empty(self.translate(arg)));
if let Some(argument) = argument {
annotation = prefix(annotation, argument).map(Ast::from);
}
out.extend_one(annotation.map(Some));
self.translate_linebreaks(out, newlines);
if let Some(expression) = expression {
self.translate_lines(expression, out);
}
}
tree::Variant::Documented(tree::Documented { documentation, expression }) => {
let space = self.visit_space(&tree.span);
self.translate_doc(space, documentation, out);
if let Some(expression) = expression {
self.translate_lines(expression, out);
}
}
_ => out.extend_one(self.translate(tree)),
}
}
/// Translate a sequence of line breaks and trailing comments to [`Ast`] representation.
fn translate_linebreaks(
&mut self,
out: &mut Vec<WithInitialSpace<Option<Ast>>>,
newlines: &[syntax::token::Newline],
) {
// In the [`Ast`] representation, each block line has one implicit newline.
let out_newlines = newlines.len().saturating_sub(1);
out.reserve(out_newlines);
let mut prev = None;
for token in newlines {
let next = self.visit_token(token).split();
if let Some((space, token)) = prev.replace(next) {
if let Some(text) = into_comment(&token) {
append_comment(out, space, text);
} else {
out.push(WithInitialSpace { space, body: None });
}
}
}
}
/// Translate a documentation comment to [`Ast`] representation.
fn translate_doc(
&mut self,
space: usize,
documentation: &tree::DocComment,
out: &mut Vec<WithInitialSpace<Option<Ast>>>,
) {
let open = self.visit_token(&documentation.open);
let mut span_info = SpanSeedBuilder::new();
span_info.token(open);
for element in &documentation.elements {
span_info.token(match element {
tree::TextElement::Section { text } => self.visit_token(text),
tree::TextElement::Escape { token } => self.visit_token(token),
tree::TextElement::Newline { newline } => self.visit_token(newline),
tree::TextElement::Splice { .. } => {
let error = "Lexer must not emit splices in documentation comments.";
debug_assert!(false, "{error}");
error!("{error}");
continue;
}
})
}
let rendered = documentation.content().into();
let type_info = ast::TreeType::Documentation { rendered };
let span_info = span_info.build().expect_unspaced();
let body = Some(Ast::from(ast::Tree::expression(span_info).with_type_info(type_info)));
out.extend_one(WithInitialSpace { space, body });
self.translate_linebreaks(out, &documentation.newlines);
}
/// Lower a function definition to [`Ast`] representation.
fn translate_function(&mut self, function: &tree::Function) -> ast::Shape<Ast> {
let tree::Function { name, args, equals, body } = function;
let non_empty_name = "A function name cannot be an (empty) block.";
let name = self.translate(name).expect(non_empty_name);
let mut lhs_terms = vec![name];
lhs_terms.extend(args.iter().map(|a| self.translate_argument_definition(a)));
let larg = lhs_terms
.into_iter()
.reduce(|func, arg| prefix(func, arg).map(Ast::from))
.expect("`lhs_terms` has at least one value, because it was initialized with a value.");
let opr = self.translate_operator(equals);
let body = body.as_ref().map(|body| self.translate(body)).unwrap_or_default();
infix(larg, opr, body).expect_unspaced()
}
/// Lower a foreign-function definition to [`Ast`] representation.
fn translate_foreign_function(&mut self, func: &tree::ForeignFunction) -> ast::Shape<Ast> {
let tree::ForeignFunction { foreign, language, name, args, equals, body } = func;
let mut lhs_terms: Vec<_> = [foreign, language, name]
.into_iter()
.map(|ident| self.visit_token(ident).map(|name| Ast::from(ast::Var { name })))
.collect();
lhs_terms.extend(args.iter().map(|a| self.translate_argument_definition(a)));
let lhs = lhs_terms
.into_iter()
.reduce(|func, arg| prefix(func, arg).map(Ast::from))
.expect("`lhs_terms` has at least one value, because it was initialized with values.");
let equals = self.translate_operator(equals);
let body = self.translate(body);
infix(lhs, equals, body).expect_unspaced()
}
/// Construct an operator application from [`Tree`] operands and a specific operator.
fn opr_app(
&mut self,
lhs: &Tree,
opr: &syntax::token::Operator,
rhs: &Tree,
) -> WithInitialSpace<Ast> {
let builder = self.start_ast();
let lhs = self.translate(lhs);
let opr = self.translate_operator(opr);
let rhs = self.translate(rhs);
infix(lhs, opr, rhs).map(|opr_app| self.finish_ast(opr_app, builder))
}
/// Translate an operator or multiple-operator erorr into the [`Ast`] representation.
fn translate_operators(&mut self, opr: &tree::OperatorOrError) -> WithInitialSpace<Ast> {
match opr {
Ok(name) => match name.code.repr.strip_suffix('=') {
Some(mod_name) if mod_name.contains(|c| c != '=') => {
let opr_builder = self.start_ast();
let token = self.visit_token(name);
token.map(|_| {
let name = mod_name.to_string();
let opr = ast::Mod { name };
self.finish_ast(opr, opr_builder)
})
}
_ => self.translate_operator(name),
},
Err(names) => {
let opr_builder = self.start_ast();
let mut span_info = SpanSeedBuilder::new();
for token in &names.operators {
span_info.token(self.visit_token(token));
}
let opr = span_info
.build()
.map(|span_info| ast::Shape::from(ast::Tree::expression(span_info)));
opr.map(|opr| self.finish_ast(opr, opr_builder))
}
}
}
/// Translate an operator token into its [`Ast`] representation.
fn translate_operator(&mut self, token: &syntax::token::Operator) -> WithInitialSpace<Ast> {
let opr_builder = self.start_ast();
let right_assoc = token.properties.associativity() == syntax::token::Associativity::Right;
self.visit_token(token)
.map(|name| self.finish_ast(ast::Opr { name, right_assoc }, opr_builder))
}
/// Translate a [`tree::BodyBlock`] into an [`Ast`] module.
fn translate_module(&mut self, block: &tree::BodyBlock) -> Ast {
let (lines, _) =
self.translate_block_lines(&block.statements).unwrap_or_default().expect_unspaced();
Ast::new_no_id(ast::Module { lines })
}
/// Translate the lines of [`Tree`] block into the [`Ast`] block representation.
fn translate_block<'a, 's: 'a>(
&mut self,
tree_lines: impl IntoIterator<Item = &'a tree::block::Line<'s>>,
) -> Option<WithInitialSpace<ast::Block<Ast>>> {
let (space, (ast_lines, indent)) = self.translate_block_lines(tree_lines)?.split();
let mut empty_lines = vec![];
let mut first_line = None;
let mut lines = vec![];
for line in ast_lines {
if first_line.is_none() {
if let Some(elem) = line.elem {
first_line = Some(ast::BlockLine { elem, off: line.off });
} else {
empty_lines.push(line.off);
}
} else {
lines.push(line);
}
}
let first_line = first_line?;
let body = ast::Block { indent, empty_lines, first_line, lines };
Some(WithInitialSpace { space, body })
}
/// Translate the lines of [`Tree`] block into [`Ast`] block lines.
fn translate_block_lines<'a, 's: 'a>(
&mut self,
tree_lines: impl IntoIterator<Item = &'a tree::block::Line<'s>>,
) -> Option<WithInitialSpace<(Vec<ast::BlockLine<Option<Ast>>>, usize)>> {
let tree_lines = tree_lines.into_iter();
let mut ast_lines: Vec<ast::BlockLine<Option<Ast>>> =
Vec::with_capacity(tree_lines.size_hint().0);
let mut statement_lines = vec![];
let mut initial_indent = None;
let mut space = 0;
for tree::block::Line { newline, expression } in tree_lines {
// Mapping from [`Tree`]'s leading offsets to [`Ast`]'s trailing offsets:
// Initially, we create each line with no trailing offset.
let off = 0;
// We write each line's leading offset into the trailing offset of the previous line
// (or, for the first line, the initial offset).
let (trailing_space, newline) = self.visit_token(newline).split();
if let Some(prev_line_comment) = into_comment(&newline) {
append_comment_ast(&mut ast_lines, trailing_space, prev_line_comment);
continue;
}
*ast_lines.last_mut().map(|line| &mut line.off).unwrap_or(&mut space) = trailing_space;
match &expression {
Some(statement) => {
self.translate_lines(statement, &mut statement_lines);
if initial_indent.is_none() && let Some(first) = statement_lines.first() {
initial_indent = Some(first.space);
}
let new_lines = statement_lines
.drain(..)
.map(|elem| ast::BlockLine { elem: elem.without_space(), off });
ast_lines.extend(new_lines);
}
None => ast_lines.push(ast::BlockLine { elem: None, off }),
}
}
let body = (ast_lines, initial_indent?);
Some(WithInitialSpace { space, body })
}
/// Lower an operator block into the [`Ast`] block representation.
fn translate_operator_block<'a, 's: 'a>(
&mut self,
operator_lines: impl IntoIterator<Item = &'a tree::block::OperatorLine<'s>>,
excess: impl IntoIterator<Item = &'a tree::block::Line<'s>>,
) -> WithInitialSpace<Ast> {
let mut ast_lines: Vec<ast::BlockLine<_>> = vec![];
let mut indent = None;
let mut space = 0;
let off = 0;
for tree::block::OperatorLine { newline, expression } in operator_lines {
let (trailing_space, newline) = self.visit_token(newline).split();
if let Some(prev_line_comment) = into_comment(&newline) {
append_comment_ast(&mut ast_lines, trailing_space, prev_line_comment);
continue;
}
*ast_lines.last_mut().map(|line| &mut line.off).unwrap_or(&mut space) = trailing_space;
let elem = expression.as_ref().map(|expression| {
let opr = self.translate_operators(&expression.operator);
let non_block_operand = "Expression in operand-line cannot be an (empty) block.";
let arg = self.translate(&expression.expression).expect(non_block_operand);
let (space, elem) = section_right(opr, arg).split();
if indent.is_none() {
indent = Some(space);
}
Ast::from(elem)
});
ast_lines.push(ast::BlockLine { elem, off });
}
let non_empty_block = "An operator block must have at least one operator line.";
let indent = indent.expect(non_empty_block);
let mut statement_lines = vec![];
for tree::block::Line { newline, expression } in excess {
let (trailing_space, newline) = self.visit_token(newline).split();
if let Some(prev_line_comment) = into_comment(&newline) {
append_comment_ast(&mut ast_lines, trailing_space, prev_line_comment);
continue;
}
*ast_lines.last_mut().map(|line| &mut line.off).unwrap_or(&mut space) = trailing_space;
match &expression {
Some(statement) => {
self.translate_lines(statement, &mut statement_lines);
let new_lines = statement_lines
.drain(..)
.map(|elem| ast::BlockLine { elem: elem.without_space(), off });
ast_lines.extend(new_lines);
}
None => ast_lines.push(ast::BlockLine { elem: None, off }),
}
}
let mut first_line = None;
let mut empty_lines = vec![];
let mut lines = vec![];
for ast::BlockLine { elem, off } in ast_lines {
match (elem, &mut first_line) {
(None, None) => empty_lines.push(off),
(Some(elem), None) => first_line = Some(ast::BlockLine { elem, off }),
(elem, Some(_)) => lines.push(ast::BlockLine { elem, off }),
}
}
let first_line = first_line.expect(non_empty_block);
let body = Ast::from(ast::Block { indent, empty_lines, first_line, lines });
WithInitialSpace { space, body }
}
/// Lower an argument definition into the [`Ast`] representation.
fn translate_argument_definition(
&mut self,
arg: &tree::ArgumentDefinition,
) -> WithInitialSpace<Ast> {
let tree::ArgumentDefinition {
open,
open2,
suspension,
pattern,
type_,
close2,
default,
close,
} = arg;
let open = open.as_ref().map(|token| self.visit_token(token));
let open2 = open2.as_ref().map(|token| self.visit_token(token));
let suspension = suspension.as_ref().map(|token| self.translate_operator(token));
let non_block_operand = "Pattern in argument definition cannot be an (empty) block.";
let mut term = self.translate(pattern).expect(non_block_operand);
if let Some(opr) = suspension {
term = section_right(opr, term).map(Ast::from);
}
if let Some(tree::ArgumentType { operator, type_ }) = type_ {
let opr = self.translate_operator(operator);
let rarg = self.translate(type_);
term = infix(term, opr, rarg).map(Ast::from);
}
let close2 = close2.as_ref().map(|token| self.visit_token(token));
if let Some(open) = open2 && let Some(close) = close2 {
term = open.map(|open| group(open, term, close));
}
if let Some(tree::ArgumentDefault { equals, expression }) = default {
let opr = self.translate_operator(equals);
let rarg = self.translate(expression);
term = infix(term, opr, rarg).map(Ast::from);
}
let close = close.as_ref().map(|token| self.visit_token(token));
if let Some(open) = open && let Some(close) = close {
term = open.map(|open| group(open, term, close));
}
term
}
/// Analyze a [`Tree`] and produce a representation used by the graph editor.
fn translate_items(&mut self, tree: &syntax::tree::Tree<'_>) -> Vec<ast::SpanSeed<Ast>> {
let mut span_info = SpanSeedBuilder::new();
tree.visit_items(|item| match item {
syntax::item::Ref::Token(token) => span_info.token(self.visit_token_ref(token)),
syntax::item::Ref::Tree(tree) => {
if let Some(ast) = ignore_space_if_empty(self.translate(tree)) {
span_info.child(ast);
}
}
});
span_info.build().expect_unspaced()
}
}
// === Span-tracking ===
/// Tracks what input bytes are visited during the construction of a particular [`Ast`], and uses
/// that information to assign an ID from the ID map.
struct AstBuilder {
start: usize,
}
impl Translate {
/// Marks the beginning of the input byte range that will be included in a particular [`Ast`]
/// node.
fn start_ast(&mut self) -> AstBuilder {
AstBuilder { start: self.offset }
}
/// Marks the end of the input byte range that will be included in a particular [`Ast`] node,
/// and constructs the node from an [`ast::Shape`], using the calculated byte range to assign
/// an ID.
fn finish_ast<S: Into<ast::Shape<Ast>>>(&mut self, shape: S, builder: AstBuilder) -> Ast {
let AstBuilder { start } = builder;
let start = start + self.space_after.get(&start).copied().unwrap_or_default();
let end = self.offset;
let id = self.ids.remove(&(start, end));
if DEBUG {
match id {
Some(id) => self.ids_assigned.push(((start, end), id)),
None => self.ids_missed.push((start, end)),
}
}
Ast::new(shape, id)
}
}
// === Semantic Analysis ===
/// Analyze an import statement to identify the module referenced and the names imported.
// TODO: In place of this analysis (and a similar analysis in Java [`TreeToIr`]),
// refactor [`tree::Import`] to a higher-level representation resembling
// [`ast::ImportedNames`] (but with concrete tokens).
fn analyze_import(import: &tree::Import) -> Option<ast::TreeType> {
let tree::Import { polyglot, from, import, all, as_, hiding } = import;
fn parse_module(tree: &Tree) -> Option<Vec<ImString>> {
let mut segments = vec![];
for tree in tree.left_assoc_rev(".") {
match &*tree.variant {
tree::Variant::Ident(tree::Ident { token }) =>
segments.push(token.code.to_string().into()),
_ => return None,
}
}
segments.reverse();
Some(segments)
}
fn parse_ident(tree: &Tree) -> Option<String> {
match &*tree.variant {
tree::Variant::Ident(tree::Ident { token }) => Some(token.code.to_string()),
_ => None,
}
}
fn parse_idents(tree: &Tree) -> Option<std::collections::BTreeSet<String>> {
let mut names = std::collections::BTreeSet::new();
for tree in tree.left_assoc_rev(",") {
match &*tree.variant {
tree::Variant::Ident(tree::Ident { token }) => {
names.insert(token.code.to_string());
}
_ => return None,
}
}
Some(names)
}
let module;
let imported;
match (polyglot, from, all, as_, hiding) {
(None, None, None, None, None) => {
module = import.body.as_ref().and_then(parse_module)?;
imported = ast::ImportedNames::Module { alias: None };
}
(None, None, None, Some(as_), None) => {
module = import.body.as_ref().and_then(parse_module)?;
let alias = as_.body.as_ref().and_then(parse_ident);
imported = ast::ImportedNames::Module { alias };
}
(None, Some(from), None, None, None) => {
module = from.body.as_ref().and_then(parse_module)?;
let names = import.body.as_ref().and_then(parse_idents)?;
imported = ast::ImportedNames::List { names };
}
(None, Some(from), Some(_), None, None) => {
module = from.body.as_ref().and_then(parse_module)?;
imported = ast::ImportedNames::All { except: Default::default() };
}
(None, Some(from), Some(_), None, Some(hiding)) => {
module = from.body.as_ref().and_then(parse_module)?;
let except = hiding.body.as_ref().and_then(parse_idents)?;
imported = ast::ImportedNames::All { except };
}
_ => return None,
}
Some(ast::TreeType::Import { module, imported })
}
/// Distinguish a plain newline from a trailing comment.
fn into_comment(mut newline: &str) -> Option<String> {
if let Some(text) = newline.strip_suffix('\n') {
newline = text;
}
if let Some(text) = newline.strip_suffix('\r') {
newline = text;
}
(!newline.is_empty()).then(|| newline.to_string())
}
// === WithInitialSpace ===
/// Helper for propagating spacing from children (Tree-style left offsets) to parents (Ast-style
/// top-down spacing).
#[derive(Debug, Default)]
struct WithInitialSpace<T> {
space: usize,
body: T,
}
impl<T: Debug> WithInitialSpace<T> {
/// If any initial space is present, emit a warning; forget the space and return the value.
fn expect_unspaced(self) -> T {
if DEBUG {
debug_assert_eq!(self.space, 0, "Expected no space before term: {:?}", &self.body);
} else if self.space != 0 {
warn!("Expected no space before term: {:?}", &self.body);
}
self.body
}
}
impl<T> WithInitialSpace<T> {
/// Return the value, ignoring any initial space.
fn without_space(self) -> T {
self.body
}
/// Return the initial space and the value.
fn split(self) -> (usize, T) {
(self.space, self.body)
}
fn map<U>(self, f: impl FnOnce(T) -> U) -> WithInitialSpace<U> {
let WithInitialSpace { space, body } = self;
let body = f(body);
WithInitialSpace { space, body }
}
}
impl<T> WithInitialSpace<Option<T>> {
/// Convenience function that applies [`Option::expect`] to the inner value.
fn expect(self, message: &str) -> WithInitialSpace<T> {
let WithInitialSpace { space, body } = self;
WithInitialSpace { space, body: body.expect(message) }
}
}
/// Convenience function that transposes an optional value that always has initial space count, to
/// an optional value that, if present, has an initial space count. Note that this is a lossy
/// operation.
fn ignore_space_if_empty<T>(spaced: WithInitialSpace<Option<T>>) -> Option<WithInitialSpace<T>> {
spaced.body.map(|body| WithInitialSpace { space: spaced.space, body })
}
// === Shape-building helpers ===
/// Construct an [`ast::Prefix`], representing the initial spaces of the inputs appropriately.
fn prefix(
func: WithInitialSpace<Ast>,
arg: WithInitialSpace<Ast>,
) -> WithInitialSpace<ast::Shape<Ast>> {
func.map(|func| {
let (off, arg) = arg.split();
(ast::Prefix { func, off, arg }).into()
})
}
/// Construct an [`ast::Prefix`] if both operands are present; otherwise, returns a single operand
/// if present.
fn maybe_prefix<T: Into<Option<Ast>>, U: Into<Option<Ast>>>(
func: WithInitialSpace<T>,
arg: WithInitialSpace<U>,
) -> WithInitialSpace<Option<ast::Shape<Ast>>> {
func.map(|func| {
let arg = ignore_space_if_empty(arg.map(|arg| arg.into()));
match (func.into(), arg) {
(Some(func), Some(arg)) => {
let (off, arg) = arg.split();
Some((ast::Prefix { func, off, arg }).into())
}
(Some(func), None) => Some(func.shape().clone()),
(None, Some(arg)) => Some(arg.expect_unspaced().shape().clone()),
(None, None) => None,
}
})
}
/// Constructs an operator section for an operator with only a right operand.
fn section_right(
opr: WithInitialSpace<Ast>,
arg: WithInitialSpace<Ast>,
) -> WithInitialSpace<ast::Shape<Ast>> {
opr.map(|opr| {
let (off, arg) = arg.split();
(ast::SectionRight { opr, off, arg }).into()
})
}
/// Constructs an infix-application node. If any of the inputs are [`Option`] types, then an
/// operator section will be produced if appropriate.
fn infix<T: Into<Option<Ast>>, U: Into<Option<Ast>>>(
larg: WithInitialSpace<T>,
opr: WithInitialSpace<Ast>,
rarg: WithInitialSpace<U>,
) -> WithInitialSpace<ast::Shape<Ast>> {
larg.map(|larg| {
let (opr_off, opr) = opr.split();
let rarg = ignore_space_if_empty(rarg.map(|arg| arg.into()));
match (larg.into(), rarg) {
(Some(larg), Some(rarg)) => {
let (roff, rarg) = rarg.split();
(ast::Infix { larg, loff: opr_off, opr, roff, rarg }).into()
}
(Some(arg), None) => (ast::SectionLeft { arg, off: opr_off, opr }).into(),
(None, Some(arg)) => {
let (off, arg) = arg.split();
(ast::SectionRight { opr, off, arg }).into()
}
(None, None) => (ast::SectionSides { opr }).into(),
}
})
}
/// Wrap an input in a parenthesized-group node.
fn group(open: String, body: WithInitialSpace<Ast>, close: WithInitialSpace<String>) -> Ast {
let (body_space, body) = body.split();
let (close_space, close) = close.split();
let min_elements = 3; // There are always at least 3 elements: open, close, and body
let mut span_info = Vec::with_capacity(min_elements);
span_info.push(ast::SpanSeed::Token(ast::SpanSeedToken { token: open }));
span_info.extend(ast::SpanSeed::space(body_space));
span_info.push(ast::SpanSeed::Child(ast::SpanSeedChild { node: body }));
span_info.extend(ast::SpanSeed::space(close_space));
span_info.push(ast::SpanSeed::Token(ast::SpanSeedToken { token: close }));
Ast::from(ast::Tree::expression(span_info))
}
/// Append a trailing-comment to the last line of the given block, creating a line to hold it if the
/// block is empty.
fn append_comment(lines: &mut Vec<WithInitialSpace<Option<Ast>>>, space: usize, comment: String) {
let prev = lines.pop();
let space_after_expression = match prev.as_ref().and_then(|spaced| spaced.body.as_ref()) {
Some(_) => space,
None => 0,
};
let prev = prev.unwrap_or(WithInitialSpace { space, body: None });
let line = prev.map(|prev| {
Some(Ast::from(ast::Tree::expression_with_comment(prev, space_after_expression, comment)))
});
lines.push(line);
}
/// Append a trailing-comment to the last line of the given block, creating a line to hold it if the
/// block is empty.
fn append_comment_ast(lines: &mut Vec<ast::BlockLine<Option<Ast>>>, space: usize, comment: String) {
let prev = lines.pop();
let off = prev.as_ref().map(|line| line.off).unwrap_or_default();
let prev = prev.and_then(|line| line.elem);
// If there's no expression before the comment, the space to the left of the comment
// is already represented as the indent.
let trailing_space = match prev {
Some(_) => space,
None => 0,
};
let elem = ast::Tree::expression_with_comment(prev, trailing_space, comment);
lines.push(ast::BlockLine { elem: Some(Ast::from(elem)), off });
}
// === SpanSeedBuilder ===
/// Constructs a sequence of [`ast::SpanSeed`] values.
#[derive(Debug, Default)]
pub struct SpanSeedBuilder {
space: Option<usize>,
spans: Vec<ast::SpanSeed<Ast>>,
}
impl SpanSeedBuilder {
fn new() -> Self {
Self::default()
}
/// Append a token.
fn token(&mut self, value: WithInitialSpace<String>) {
let (space, value) = value.split();
if self.space.is_none() {
self.space = Some(space);
} else {
self.spans.extend(ast::SpanSeed::space(space));
}
self.spans.push(ast::SpanSeed::Token(ast::SpanSeedToken { token: value }));
}
/// Append a node.
fn child(&mut self, node: WithInitialSpace<Ast>) {
let (space, node) = node.split();
if self.space.is_none() {
self.space = Some(space);
} else {
self.spans.extend(ast::SpanSeed::space(space));
}
self.spans.push(ast::SpanSeed::Child(ast::SpanSeedChild { node }));
}
/// Construct the sequence.
fn build(self) -> WithInitialSpace<Vec<ast::SpanSeed<Ast>>> {
let space = self.space.unwrap_or_default();
let body = self.spans;
WithInitialSpace { space, body }
}
}

View File

@ -1,124 +0,0 @@
//! Tests specific to Ast rather than parser itself but placed here because they depend on parser
//! to easily generate test input.
// TODO: [mwu]
// That means that likely either `parser` should be merged with `ast` or that we should have a
// separate `ast_ops` crate that depends on both. Now it is better to tests here than none but
// a decision should be made as to which way we want to go.
// === Non-Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use parser_scala::prelude::*;
use ast::opr;
use ast::opr::GeneralizedInfix;
use ast::prefix;
use ast::test_utils::expect_single_line;
use ast::HasRepr;
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
pub fn to_assignment_test() {
let parser = parser_scala::Parser::new_or_panic();
let is_assignment = |code: &str| {
let ast = parser.parse(code.to_string(), default()).unwrap();
let line = expect_single_line(&ast);
ast::opr::to_assignment(line).is_some()
};
let expected_assignments = vec!["a = 5", "a=5", "foo bar = a b c", "(x,y) = pos"];
let expected_not_assignments = vec!["= 5", "a=", "=", "foo", "a->b", "a+b"];
for code in expected_assignments {
assert!(is_assignment(code), "{code} expected to be recognized as assignment");
}
for code in expected_not_assignments {
assert!(!is_assignment(code), "{code} expected to not be recognized as assignment");
}
}
#[wasm_bindgen_test]
pub fn generalized_infix_test() {
let parser = parser_scala::Parser::new_or_panic();
let make_gen_infix = |code: &str| {
let ast = parser.parse(code.to_string(), default()).unwrap();
let line = expect_single_line(&ast);
GeneralizedInfix::try_new(line)
};
let infix = make_gen_infix("a+b").unwrap();
assert_eq!(infix.name(), "+");
assert_eq!(infix.left.map(|op| op.arg).repr(), "a");
assert_eq!(infix.right.map(|op| op.arg).repr(), "b");
let right = make_gen_infix("+b").unwrap();
assert_eq!(right.name(), "+");
assert_eq!(right.right.map(|op| op.arg).repr(), "b");
let left = make_gen_infix("a+").unwrap();
assert_eq!(left.name(), "+");
assert_eq!(left.left.map(|op| op.arg).repr(), "a");
let sides = make_gen_infix("+").unwrap();
assert_eq!(sides.name(), "+");
let var_as_infix = make_gen_infix("a");
assert!(var_as_infix.is_none());
}
#[wasm_bindgen_test]
pub fn flatten_prefix_test() {
fn expect_pieces(flattened: &prefix::Chain, pieces: Vec<&str>) {
let mut piece_itr = pieces.iter();
assert_eq!(flattened.args.len() + 1, pieces.len()); // +1 because `func` piece is separate field
assert_eq!(&flattened.func.repr(), piece_itr.next().unwrap());
flattened.args.iter().zip(piece_itr).for_each(|(lhs, rhs)| {
assert_eq!(&lhs.repr(), rhs);
})
}
let parser = parser_scala::Parser::new_or_panic();
let case = |code: &str, expected_pieces: Vec<&str>| {
let ast = parser.parse(code.into(), default()).unwrap();
let ast = ast::test_utils::expect_single_line(&ast);
let flattened = prefix::Chain::from_ast_non_strict(ast);
expect_pieces(&flattened, expected_pieces);
assert_eq!(flattened.repr(), code);
};
case("a", vec!["a"]);
case("a b c d", vec!["a", " b", " c", " d"]);
case("+ a b c", vec!["+", " a", " b", " c"]);
case("a b + c d", vec!["a b + c d"]); // nothing to flatten, this is infix, not prefix
}
#[wasm_bindgen_test]
pub fn flatten_infix_test() {
fn expect_pieces(flattened: &opr::Chain, target: &str, pieces: Vec<&str>) {
assert_eq!(flattened.target.as_ref().map(|a| &a.arg).repr(), target);
let piece_itr = pieces.iter();
assert_eq!(flattened.args.len(), pieces.len());
flattened.args.iter().zip(piece_itr).for_each(|(lhs, rhs)| {
assert_eq!(lhs.operand.as_ref().map(|a| &a.arg).repr(), *rhs);
})
}
let parser = parser_scala::Parser::new_or_panic();
let case = |code: &str, target: &str, expected_pieces: Vec<&str>| {
let ast = parser.parse(code.into(), default()).unwrap();
let ast = ast::test_utils::expect_single_line(&ast);
let flattened = opr::Chain::try_new(ast).unwrap();
expect_pieces(&flattened, target, expected_pieces);
};
case("a+b+c", "a", vec!["b", "c"]);
case("a,b,c", "c", vec!["b", "a"]);
case("a+b*c+d", "a", vec!["b*c", "d"]);
}

View File

@ -1,41 +0,0 @@
//! Tests for cases where parser currently fails. They are ignored, should be removed and placed
//! elsewhere, as the parser gets fixed.
// === Non-Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn no_doc_found() {
let input = String::from("type Foo\n type Bar");
let program = std::env::args().nth(1).unwrap_or(input);
let parser = parser_scala::DocParser::new_or_panic();
let gen_code = parser.generate_html_docs(program).unwrap();
// gen_code should be empty.
assert_eq!(gen_code.len(), 22, "Generated length differs from the expected\"{gen_code}\"");
}
#[wasm_bindgen_test]
fn extension_operator_methods() {
let ast = parser_scala::Parser::new_or_panic().parse_line_ast("Int.+").unwrap();
use ast::*;
if let Shape::Infix(Infix { larg: _larg, loff: _loff, opr, roff: _roff, rarg }, ..) =
ast.shape()
{
if let Shape::Opr(Opr { .. }) = opr.shape() {
// TODO: should be Opr(+). https://github.com/enso-org/enso/issues/565
if let Shape::Var(Var { .. }) = rarg.shape() {
return;
}
}
}
panic!("Should have matched into return.");
}

View File

@ -1,33 +0,0 @@
// === Non-Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use enso_prelude::*;
use ast::crumbs::Crumbable;
use ast::HasRepr;
use parser_scala::Parser;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn macro_crumb_test() {
let ast = Parser::new_or_panic().parse_line_ast("foo -> bar").unwrap();
let crumbs = ast.iter_subcrumbs().collect_vec();
assert_eq!(ast.get(&crumbs[0]).unwrap().repr(), "foo");
assert_eq!(ast.get(&crumbs[1]).unwrap().repr(), "->");
assert_eq!(ast.get(&crumbs[2]).unwrap().repr(), "bar");
let ast = Parser::new_or_panic().parse_line_ast("( foo bar )").unwrap();
let crumbs = ast.iter_subcrumbs().collect_vec();
assert_eq!(ast.get(&crumbs[0]).unwrap().repr(), "(");
assert_eq!(ast.get(&crumbs[1]).unwrap().repr(), "foo bar");
assert_eq!(ast.get(&crumbs[2]).unwrap().repr(), ")");
}

View File

@ -1,33 +0,0 @@
// === Non-Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use parser_scala::DocParser;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn doc_gen_test() {
// Case of pure documentation code.
let input = String::from("Foo *Bar* Baz");
let program = std::env::args().nth(1).unwrap_or(input);
let parser = DocParser::new_or_panic();
let gen_code = parser.generate_html_doc_pure(program).unwrap();
assert_ne!(gen_code.len(), 0);
let input = String::from("##\n foo\ntype Foo\n");
let program = std::env::args().nth(1).unwrap_or(input);
let parser = DocParser::new_or_panic();
let gen_code = parser.generate_html_docs(program).unwrap();
assert_ne!(gen_code.len(), 0);
let input = String::from("##\n DEPRECATED\n Foo bar baz\ntype Foo\n type Bar");
let program = std::env::args().nth(1).unwrap_or(input);
let parser = DocParser::new_or_panic();
let gen_code = parser.generate_html_docs(program).unwrap();
assert_ne!(gen_code.len(), 0);
}

View File

@ -1,35 +0,0 @@
// === Non-Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use parser_scala::prelude::*;
use ast::HasIdMap;
use parser_scala::Parser;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn id_map_round_tripping() {
let cases = [
"main =\n 2 + 2",
"main = \n \n 2 + 2\n foo = bar \n baz",
"main = \n foo\n\n bar",
"main = \n foo\n \n bar",
"main = \n foo\n \n bar",
"main = \n foo\n baz \n bar",
];
let parser = Parser::new().unwrap();
for case in cases.iter().copied() {
let id_map = default();
let ast1 = parser.parse_module(case, id_map).unwrap();
let id_map = ast1.id_map();
let ast2 = parser.parse_module(case, id_map).unwrap();
assert_eq!(ast1, ast2)
}
}

View File

@ -1,82 +0,0 @@
// === Non-Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use parser_scala::prelude::*;
use parser_scala::Parser;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn import_utilities() {
use ast::macros::ast_as_import_match;
use ast::macros::is_ast_import;
use ast::macros::is_match_import;
let parser = Parser::new_or_panic();
let expect_import = |code: &str| {
let ast = parser.parse_line_ast(code).unwrap();
assert!(is_ast_import(&ast), "Not Ast import: {ast:?}");
let ast_match = ast_as_import_match(&ast).unwrap();
assert_eq!(&ast, ast_match.ast());
assert!(is_match_import(&ast_match));
};
let expect_not_import = |code: &str| {
let ast = parser.parse_line_ast(code).unwrap();
assert!(!is_ast_import(&ast));
assert!(ast_as_import_match(&ast).is_none());
};
expect_import("import");
expect_import("import Foo");
expect_import("import foo.Foo.Bar");
expect_import("import foo.Foo.Bar");
expect_import("import Foo.Bar");
expect_import("import Foo.Bar.Baz");
expect_import("from Foo import Bar");
expect_import("from foo.Foo import all hiding Bar");
expect_import("from Base.Data.List import all hiding Cons, Nil");
expect_not_import("type Foo");
expect_not_import("type Foo as Bar");
expect_not_import("if Foo then Bar else Baz");
expect_not_import("Foo.Bar.Baz");
expect_not_import("->");
expect_not_import("export");
expect_not_import("export Foo");
expect_not_import("from Foo export all hiding Bar");
}
#[wasm_bindgen_test]
fn recognizing_lambdas() {
let parser = Parser::new_or_panic();
let expect_lambda = |code: &str, arg: &str, body: &str| {
let ast = parser.parse_line_ast(code).unwrap();
let lambda = ast::macros::as_lambda(&ast).expect("failed to recognize lambda");
assert_eq!(lambda.arg.repr(), arg);
assert_eq!(lambda.body.repr(), body);
assert_eq!(*lambda.arg, ast.get_traversing(&lambda.arg.crumbs).unwrap());
assert_eq!(*lambda.body, ast.get_traversing(&lambda.body.crumbs).unwrap());
};
let expect_not_lambda = |code: &str| {
let ast = parser.parse_line_ast(code).unwrap();
assert!(ast::macros::as_lambda_match(&ast).is_none(), "wrongly recognized a lambda");
};
expect_lambda("a->b", "a", "b");
expect_lambda("foo->4+(4)", "foo", "4+(4)");
expect_lambda("a->b->c", "a", "b->c");
expect_lambda("(a->b)->c", "(a->b)", "c");
expect_not_lambda("(a->b)");
expect_not_lambda("a+b");
expect_not_lambda("'a+b'");
expect_not_lambda("497");
}

View File

@ -1,550 +0,0 @@
// === Features ===
#![feature(generators, generator_trait)]
// === Non-Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use ast::*;
use parser_scala::prelude::*;
use ast::test_utils::expect_shape;
use parser_scala::api::Metadata;
use parser_scala::api::ParsedSourceFile;
use parser_scala::api::PruneUnusedIds;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde::Serialize;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
// ===============
// === Helpers ===
// ===============
/// Asserts that given AST is a Var with given name.
fn assert_var<StringLike: Into<String>>(ast: &Ast, name: StringLike) {
let actual: &Var = expect_shape(ast);
let expected = Var { name: name.into() };
assert_eq!(*actual, expected);
}
/// Asserts that given AST is an Opr with given name.
fn assert_opr<StringLike: Into<String>>(ast: &Ast, name: StringLike) {
let actual: &Opr = expect_shape(ast);
let expected = Opr { name: name.into() };
assert_eq!(*actual, expected);
}
fn roundtrip_program_with(parser: &parser_scala::Parser, program: &str) {
let ast = parser.parse(program.to_string(), Default::default()).unwrap();
assert_eq!(ast.repr(), program, "{ast:#?}");
}
fn roundtrip_program(program: &str) {
let parser = parser_scala::Parser::new_or_panic();
roundtrip_program_with(&parser, program);
}
// ================
// === Metadata ===
// ================
/// Wrapper for using any serializable type as metadata.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
struct FauxMetadata<T>(T);
impl<T> PruneUnusedIds for FauxMetadata<T> {}
impl<T: Default + Serialize + DeserializeOwned> Metadata for FauxMetadata<T> {}
// ===============
// === Fixture ===
// ===============
/// Persists parser (which is expensive to construct, so we want to reuse it
/// between tests. Additionally, hosts a number of helper methods.
struct Fixture {
parser: parser_scala::Parser,
}
impl Fixture {
// === Helper methods ===
/// Create a new fixture, obtaining a default parser.
fn new() -> Fixture {
Fixture { parser: parser_scala::Parser::new_or_panic() }
}
/// Program is expected to be single line module. The line's Shape subtype
/// is obtained and passed to `tester`.
fn test_shape<T, F>(&mut self, program: &str, tester: F)
where
for<'t> &'t Shape<Ast>: TryInto<&'t T>,
F: FnOnce(&T), {
let ast = self.parser.parse_line_ast(program).unwrap();
let shape = expect_shape(&ast);
tester(shape);
}
// === Test Methods ===
fn blank_line_round_trip(&mut self) {
let program = "main = \n foo\n \n bar";
let ast = self.parser.parse_module(program, default()).unwrap();
assert_eq!(ast.repr(), program);
}
fn deserialize_metadata(&mut self) {
let term = ast::Module { lines: vec![ast::BlockLine { elem: None, off: 0 }] };
let ast = known::KnownAst::new_no_id(term);
let file = ParsedSourceFile { ast, metadata: serde_json::json!({}) };
let code = String::try_from(&file).unwrap();
assert_eq!(self.parser.parse_with_metadata(code).unwrap(), file);
}
fn deserialize_unrecognized(&mut self) {
let unfinished = "`";
self.test_shape(unfinished, |shape: &Unrecognized| {
assert_eq!(shape.str, "`");
});
}
#[allow(dead_code)] // TODO [mwu] https://github.com/enso-org/enso/issues/1016
fn deserialize_unexpected(&mut self) {
let unexpected = "import";
let ast = self.parser.parse_line_ast(unexpected).unwrap();
// This does not deserialize to "Unexpected" but to a very complex macro match tree that has
// Unexpected somewhere within. We just make sure that it is somewhere, and that confirms
// that we are able to deserialize such node.
let has_unexpected =
ast.iter_recursive().find(|ast| matches!(ast.shape(), Shape::Unexpected(_)));
assert!(has_unexpected.is_some());
}
fn deserialize_invalid_quote(&mut self) {
let unfinished = "'a''";
self.test_shape(unfinished, |shape: &Prefix<Ast>| {
// ignore shape.func, being TextUnclosed tested elsewhere
let arg: &InvalidQuote = expect_shape(&shape.arg);
let expected_quote = Text { str: "''".into() };
assert_eq!(arg.quote, expected_quote.into());
});
}
fn deserialize_inline_block(&mut self) {
let unfinished = "'''a";
self.test_shape(unfinished, |shape: &Prefix<Ast>| {
let func: &InlineBlock = expect_shape(&shape.func);
let expected_quote = Text { str: "'''".into() };
assert_eq!(func.quote, expected_quote.into());
assert_var(&shape.arg, "a");
});
}
fn deserialize_blank(&mut self) {
let expect_blank = |_: &Blank| {};
self.test_shape("_", expect_blank);
}
fn deserialize_var(&mut self) {
self.test_shape("foo", |var: &Var| {
let expected_var = Var { name: "foo".into() };
assert_eq!(var, &expected_var);
});
}
fn deserialize_cons(&mut self) {
let name = "FooBar";
self.test_shape(name, |shape: &Cons| {
assert_eq!(shape.name, name);
});
}
fn deserialize_mod(&mut self) {
self.test_shape("+=", |shape: &Mod| {
assert_eq!(shape.name, "+");
});
}
fn deserialize_invalid_suffix(&mut self) {
self.test_shape("foo'bar", |shape: &InvalidSuffix<Ast>| {
assert_var(&shape.elem, "foo'");
assert_eq!(shape.suffix, "bar");
});
}
fn deserialize_number(&mut self) {
self.test_shape("127", |shape: &Number| {
assert_eq!(shape.base, None);
assert_eq!(shape.int, "127");
});
self.test_shape("16_ff", |shape: &Number| {
assert_eq!(shape.base.as_ref().unwrap(), "16");
assert_eq!(shape.int, "ff");
});
}
fn deserialize_text_line_raw(&mut self) {
self.test_shape("\"foo\"", |shape: &TextLineRaw| {
let (segment,) = (&shape.text).expect_tuple();
let expected = SegmentPlain { value: "foo".to_string() };
assert_eq!(*segment, expected.into());
});
let tricky_raw = r#""\\\'\n""#;
self.test_shape(tricky_raw, |shape: &TextLineRaw| {
let segments: (_,) = (&shape.text).expect_tuple();
assert_eq!(*segments.0, SegmentPlain { value: r"\\\'\n".to_string() }.into());
});
}
fn test_text_fmt_segment<F>(&mut self, program: &str, tester: F)
where F: FnOnce(&SegmentFmt<Ast>) {
self.test_shape(program, |shape: &TextLineFmt<Ast>| {
let (segment,) = (&shape.text).expect_tuple();
tester(segment)
});
}
fn deserialize_text_line_fmt(&mut self) {
use SegmentFmt::SegmentExpr;
// plain segment
self.test_shape("'foo'", |shape: &TextLineFmt<Ast>| {
let (segment,) = (&shape.text).expect_tuple();
let expected = SegmentPlain { value: "foo".into() };
assert_eq!(*segment, expected.into());
});
// escapes
let tricky_fmt = r#"'\\\'\"'"#;
self.test_shape(tricky_fmt, |shape: &TextLineFmt<Ast>| {
let segments: (_, _, _) = (&shape.text).expect_tuple();
assert_eq!(*segments.0, Slash {}.into());
assert_eq!(*segments.1, Quote {}.into());
assert_eq!(*segments.2, Invalid { str: '"' }.into());
});
// expression empty
let expr_fmt = r#"'``'"#;
self.test_text_fmt_segment(expr_fmt, |segment| match segment {
SegmentExpr(expr) => assert_eq!(expr.value, None),
_ => panic!("wrong segment type received"),
});
// expression non-empty
let expr_fmt = r#"'`foo`'"#;
self.test_text_fmt_segment(expr_fmt, |segment| match segment {
SegmentExpr(expr) => assert_var(expr.value.as_ref().unwrap(), "foo"),
_ => panic!("wrong segment type received"),
});
self.test_text_fmt_segment(r#"'\n'"#, |segment| {
let expected = EscapeCharacter { c: 'n' };
assert_eq!(*segment, expected.into());
});
self.test_text_fmt_segment(r#"'\u0394'"#, |segment| {
let expected = EscapeUnicode16 { digits: "0394".into() };
assert_eq!(*segment, expected.into());
});
// TODO [MWU] We don't test Unicode21 as it is not yet supported by the
// parser.
self.test_text_fmt_segment(r#"'\U0001f34c'"#, |segment| {
let expected = EscapeUnicode32 { digits: "0001f34c".into() };
assert_eq!(*segment, expected.into());
});
}
fn deserialize_text_block_raw(&mut self) {
let program = "\"\"\" \n \n X";
self.test_shape(program, |shape: &TextBlockRaw| {
assert_eq!(shape.spaces, 1);
assert_eq!(shape.offset, 0);
let (line,) = (&shape.text).expect_tuple();
let (empty_line,) = (&line.empty_lines).expect_tuple();
assert_eq!(*empty_line, 2);
let (segment,) = (&line.text).expect_tuple();
let expected_segment = SegmentPlain { value: " X".into() };
assert_eq!(*segment, expected_segment.into());
});
}
fn deserialize_text_block_fmt(&mut self) {
let program = "''' \n\n X\n Y";
self.test_shape(program, |shape: &TextBlockFmt<Ast>| {
assert_eq!(shape.spaces, 2);
assert_eq!(shape.offset, 0);
assert_eq!(shape.text.len(), 2);
let (line1, line2) = (&shape.text).expect_tuple();
let (empty_line,) = (&line1.empty_lines).expect_tuple();
assert_eq!(*empty_line, 0);
let (segment,) = (&line1.text).expect_tuple();
let expected_segment = SegmentPlain { value: " X".into() };
assert_eq!(*segment, expected_segment.into());
assert!(line2.empty_lines.is_empty());
let (segment,) = (&line2.text).expect_tuple();
let expected_segment = SegmentPlain { value: " Y".into() };
assert_eq!(*segment, expected_segment.into());
});
}
fn deserialize_unfinished_text(&mut self) {
let unfinished = r#""\"#;
self.test_shape(unfinished, |shape: &TextUnclosed<Ast>| {
let line = &shape.line;
let line: &TextLineRaw = line.try_into().unwrap();
let (segment,) = (&line.text).expect_tuple();
let expected = SegmentPlain { value: r"\".into() };
assert_eq!(*segment, expected.into());
});
}
fn deserialize_dangling_base(&mut self) {
self.test_shape("16_", |shape: &DanglingBase| {
assert_eq!(shape.base, "16");
});
}
fn deserialize_prefix(&mut self) {
self.test_shape("foo bar", |shape: &Prefix<Ast>| {
assert_var(&shape.func, "foo");
assert_eq!(shape.off, 3);
assert_var(&shape.arg, "bar");
});
}
fn deserialize_infix(&mut self) {
self.test_shape("foo + bar", |shape: &Infix<Ast>| {
assert_var(&shape.larg, "foo");
assert_eq!(shape.loff, 1);
assert_opr(&shape.opr, "+");
assert_eq!(shape.roff, 2);
assert_var(&shape.rarg, "bar");
});
}
fn deserialize_left(&mut self) {
self.test_shape("foo +", |shape: &SectionLeft<Ast>| {
assert_var(&shape.arg, "foo");
assert_eq!(shape.off, 1);
assert_opr(&shape.opr, "+");
});
}
fn deserialize_right(&mut self) {
self.test_shape("+ bar", |shape: &SectionRight<Ast>| {
assert_opr(&shape.opr, "+");
assert_eq!(shape.off, 1);
assert_var(&shape.arg, "bar");
});
}
fn deserialize_sides(&mut self) {
self.test_shape("+", |shape: &SectionSides<Ast>| {
assert_opr(&shape.opr, "+");
});
}
fn deserialize_block(&mut self) {
self.test_shape(" foo\n bar", |block: &Block<Ast>| {
assert_eq!(block.ty, BlockType::Continuous {});
assert_eq!(block.indent, 1);
assert_eq!(block.empty_lines.len(), 0);
assert!(block.is_orphan);
let first_line = &block.first_line;
assert_eq!(first_line.off, 0);
assert_var(&first_line.elem, "foo");
let (second_line,) = (&block.lines).expect_tuple();
assert_eq!(second_line.off, 0);
assert_var(second_line.elem.as_ref().unwrap(), "bar");
});
}
fn deserialize_annotation(&mut self) {
self.test_shape("@Tail_call", |annotation: &Annotation| {
let expected_annotation = Annotation { name: "@Tail_call".into() };
assert_eq!(annotation, &expected_annotation);
});
}
/// Tests parsing a number of sample macro usages.
///
/// As macros generate usually really huge ASTs, this test only checks
/// that we are able to deserialize the response and that it is a macro
/// match node. Node contents is not covered.
fn deserialize_macro_matches(&mut self) {
let macro_usages = vec![
"[]",
"[1,2,3]",
"{x}",
"polyglot java import com.example.MyClass",
"foo -> bar",
"()",
"(foo -> bar)",
"a b c -> bar",
"type Maybe a\n Just val:a",
"if foo > 8 then 10 else 9",
"skip bar",
"freeze bar",
"case foo of\n bar",
"import foo",
"import",
"export bar",
"from bar import all",
"from bar export bo",
"a ->",
"-> a",
"(a -> b) -> c",
];
for macro_usage in macro_usages.iter() {
println!(">>>>>>>>>> {macro_usage}");
let ast = self.parser.parse_line_ast(*macro_usage).unwrap();
println!("{ast:?}");
expect_shape::<Match<Ast>>(&ast);
}
}
fn deserialize_macro_ambiguous(&mut self) {
self.test_shape("if foo", |shape: &Ambiguous<Ast>| {
let segment = &shape.segs.head;
assert_var(&segment.head, "if");
let segment_body = segment.body.as_ref().unwrap();
assert_eq!(segment_body.off, 2);
assert_var(&segment_body.wrapped, "foo");
});
}
fn run(&mut self) {
// Shapes not covered by separate test:
// * Opr (doesn't parse on its own, covered by Infix and other)
// * Module (covered by every single test, as parser wraps everything into module)
self.blank_line_round_trip();
self.deserialize_metadata();
self.deserialize_unrecognized();
//self.deserialize_unexpected(); // TODO [mwu] https://github.com/enso-org/enso/issues/1016
self.deserialize_invalid_quote();
self.deserialize_inline_block();
self.deserialize_blank();
self.deserialize_var();
self.deserialize_cons();
self.deserialize_mod();
self.deserialize_invalid_suffix();
self.deserialize_number();
self.deserialize_text_line_raw();
self.deserialize_text_line_fmt();
self.deserialize_text_block_raw();
self.deserialize_text_block_fmt();
self.deserialize_unfinished_text();
self.deserialize_dangling_base();
self.deserialize_prefix();
self.deserialize_infix();
self.deserialize_left();
self.deserialize_right();
self.deserialize_sides();
self.deserialize_block();
self.deserialize_annotation();
self.deserialize_macro_matches();
self.deserialize_macro_ambiguous();
}
}
/// A single entry point for all the tests here using external parser.
///
/// Setting up the parser is costly, so we run all tests as a single batch.
/// Until proper CI solution for calling external parser is devised, this
/// test is marked with `#[ignore]`.
#[wasm_bindgen_test]
fn parser_tests() {
Fixture::new().run()
}
/// Test case for https://github.com/enso-org/ide/issues/296
#[wasm_bindgen_test]
fn block_roundtrip() {
let programs = vec![
"main = 10 + 10",
"main =\n a = 10\n b = 20\n a * b",
"main =\n foo a =\n a * 10\n foo 10\n print \"hello\"",
"main =\n foo\n \n bar",
"main =\n \n foo\n \n bar",
];
for program in programs {
roundtrip_program(program);
}
}
/// Test case for https://github.com/enso-org/ide/issues/296
#[wasm_bindgen_test]
fn nested_macros() {
let parser = parser_scala::Parser::new_or_panic();
// Generate nested brackets. Stop at 8 because it gets slower and slower.
// At 12 the deserialization fails on WASM.
// At 14 the parsing fails in parser-service.
for i in 0..8 {
let program = format!("{}{}{}", "[".repeat(i), "foo", "]".repeat(i));
roundtrip_program_with(&parser, &program);
}
// Cases from https://github.com/enso-org/ide/issues/1351
let program = r#"from Standard.Base import all
main =
operator13 = Json.from_pairs [["a", 42], ["foo", [1,2,3]]]
var1 = [operator13, operator13]"#;
roundtrip_program_with(&parser, program);
let program = r#"triplets n = 1.up_to n . to_vector . flat_map a->
a+1 . up_to n . to_vector . flat_map b->
b+1 . up_to n . to_vector . flat_map c->
if a+b+c == n then [[a,b,c]] else []
n = 10
here.triplets n
IO.println(here.triplets n)"#;
roundtrip_program_with(&parser, program);
}
#[wasm_bindgen_test]
fn dealing_with_invalid_metadata() {
let f = Fixture::new();
let id = ast::Id::from_str("52233542-5c73-430b-a2b7-a68aaf81341b").unwrap();
let var = ast::Ast::new(ast::Var { name: "variable1".into() }, Some(id));
let module = ast::Module::from_line(var);
let ast = known::Module::new_no_id(module);
let metadata = FauxMetadata("certainly_not_a_number".to_string());
// Make sure that out metadata cannot be deserialized as `FauxMetadata<i32>`.
let serialized_text_metadata = serde_json::to_string(&metadata).unwrap();
assert!(serde_json::from_str::<FauxMetadata<i32>>(&serialized_text_metadata).is_err());
let parsed_file = parser_scala::api::ParsedSourceFile { ast, metadata };
let generated = parsed_file.serialize().unwrap();
let expected_generated = r#"variable1
#### METADATA ####
[[{"index":{"value":0},"size":{"value":9}},"52233542-5c73-430b-a2b7-a68aaf81341b"]]
"certainly_not_a_number""#;
assert_eq!(generated.content, expected_generated);
let r = f.parser.parse_with_metadata::<FauxMetadata<i32>>(generated.content).unwrap();
assert_eq!(r.metadata, default());
}

View File

@ -1,44 +0,0 @@
// === Non-Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use enso_prelude::*;
use ast::Ast;
use parser_scala::api::ParsedSourceFile;
use parser_scala::Parser;
use uuid::Uuid;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn web_test() {
let uuid = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap();
let parser = Parser::new_or_panic();
let parse = |input| parser.parse_with_metadata(input).unwrap();
let file = |term| ParsedSourceFile {
metadata: serde_json::json!({}),
ast: ast::known::KnownAst::new_no_id(term),
};
let line = |term| ast::Module { lines: vec![ast::BlockLine { elem: term, off: 0 }] };
let app = ast::Prefix { func: Ast::var("x"), off: 3, arg: Ast::var("y") };
let var = ast::Var { name: "x".into() };
let ast = file(line(None));
assert_eq!(parse(String::try_from(&ast).unwrap()), ast);
let ast = file(line(Some(Ast::new(var, Some(uuid)))));
assert_eq!(parse(String::try_from(&ast).unwrap()), ast);
let ast = file(line(Some(Ast::new(app, Some(uuid)))));
assert_eq!(parse(String::try_from(&ast).unwrap()), ast);
}

View File

@ -13,5 +13,5 @@ enso-profiler = { path = "../../../../lib/rust/profiler" }
failure = { workspace = true } failure = { workspace = true }
[dev-dependencies] [dev-dependencies]
parser-scala = { path = "../parser" } parser = { path = "../parser" }
wasm-bindgen-test = { workspace = true } wasm-bindgen-test = { workspace = true }

View File

@ -4,7 +4,6 @@
#![allow(clippy::bool_to_int_with_if)] #![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::let_and_return)] #![allow(clippy::let_and_return)]
use ast::crumbs::PatternMatchCrumb::*;
use ast::crumbs::*; use ast::crumbs::*;
use enso_prelude::*; use enso_prelude::*;
use enso_text::traits::*; use enso_text::traits::*;

View File

@ -239,10 +239,9 @@ mod test {
use crate::SpanTree; use crate::SpanTree;
use ast::HasRepr; use ast::HasRepr;
use parser_scala::Parser; use parser::Parser;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test] #[test]
fn actions_in_span_tree() { fn actions_in_span_tree() {
#[derive(Debug)] #[derive(Debug)]
struct Case { struct Case {
@ -262,11 +261,12 @@ mod test {
panic!("Invalid case {:?}: no node with span {:?}", self, self.span) panic!("Invalid case {:?}: no node with span {:?}", self, self.span)
}); });
let arg = Ast::new(ast::Var { name: "foo".to_string() }, None); let arg = Ast::new(ast::Var { name: "foo".to_string() }, None);
let case = format!("{self:?}");
let result = match &self.action { let result = match &self.action {
Set => node.set(&ast, arg), Set => node.set(&ast, arg),
Erase => node.erase(&ast), Erase => node.erase(&ast),
} }
.unwrap(); .expect(&case);
let result_repr = result.repr(); let result_repr = result.repr();
assert_eq!(result_repr, self.expected, "Wrong answer for case {self:?}"); assert_eq!(result_repr, self.expected, "Wrong answer for case {self:?}");
assert_eq!(ast_id, result.id, "Changed AST ID in case {self:?}"); assert_eq!(ast_id, result.id, "Changed AST ID in case {self:?}");
@ -280,9 +280,6 @@ mod test {
, Case{expr:"a + b" , span:4..5 , action:Set , expected:"a + foo" } , Case{expr:"a + b" , span:4..5 , action:Set , expected:"a + foo" }
, Case{expr:"a + b + c" , span:0..1 , action:Set , expected:"foo + b + c" } , Case{expr:"a + b + c" , span:0..1 , action:Set , expected:"foo + b + c" }
, Case{expr:"a + b + c" , span:4..5 , action:Set , expected:"a + foo + c" } , Case{expr:"a + b + c" , span:4..5 , action:Set , expected:"a + foo + c" }
, Case{expr:"a , b , c" , span:0..1 , action:Set , expected:"foo , b , c" }
, Case{expr:"a , b , c" , span:4..5 , action:Set , expected:"a , foo , c" }
, Case{expr:"a , b , c" , span:8..9 , action:Set , expected:"a , b , foo" }
, Case{expr:"f a b" , span:0..1 , action:Set , expected:"foo a b" } , Case{expr:"f a b" , span:0..1 , action:Set , expected:"foo a b" }
, Case{expr:"f a b" , span:2..3 , action:Set , expected:"f foo b" } , Case{expr:"f a b" , span:2..3 , action:Set , expected:"f foo b" }
, Case{expr:"f a b" , span:4..5 , action:Set , expected:"f a foo" } , Case{expr:"f a b" , span:4..5 , action:Set , expected:"f a foo" }
@ -298,10 +295,6 @@ mod test {
, Case{expr:"+ b" , span:3..3 , action:Set , expected:"+ b + foo" } , Case{expr:"+ b" , span:3..3 , action:Set , expected:"+ b + foo" }
, Case{expr:"a + b + c" , span:0..0 , action:Set , expected:"foo + a + b + c"} , Case{expr:"a + b + c" , span:0..0 , action:Set , expected:"foo + a + b + c"}
, Case{expr:"a + b + c" , span:5..5 , action:Set , expected:"a + b + foo + c"} , Case{expr:"a + b + c" , span:5..5 , action:Set , expected:"a + b + foo + c"}
, Case{expr:"a , b , c" , span:0..0 , action:Set , expected:"foo , a , b , c"}
, Case{expr:"a , b , c" , span:4..4 , action:Set , expected:"a , foo , b , c"}
, Case{expr:"a , b , c" , span:8..8 , action:Set , expected:"a , b , foo , c"}
, Case{expr:"a , b , c" , span:9..9 , action:Set , expected:"a , b , c , foo"}
, Case{expr:", b" , span:3..3 , action:Set , expected:", b , foo" } , Case{expr:", b" , span:3..3 , action:Set , expected:", b , foo" }
, Case{expr:"f a b" , span:2..2 , action:Set , expected:"f foo a b" } , Case{expr:"f a b" , span:2..2 , action:Set , expected:"f foo a b" }
, Case{expr:"f a b" , span:3..3 , action:Set , expected:"f a foo b" } , Case{expr:"f a b" , span:3..3 , action:Set , expected:"f a foo b" }
@ -314,21 +307,18 @@ mod test {
, Case{expr:"a + b + c" , span:0..1 , action:Erase, expected:"b + c" } , Case{expr:"a + b + c" , span:0..1 , action:Erase, expected:"b + c" }
, Case{expr:"a + b + c" , span:4..5 , action:Erase, expected:"a + c" } , Case{expr:"a + b + c" , span:4..5 , action:Erase, expected:"a + c" }
, Case{expr:"a + b + c" , span:8..9 , action:Erase, expected:"a + b" } , Case{expr:"a + b + c" , span:8..9 , action:Erase, expected:"a + b" }
, Case{expr:"a , b , c" , span:0..1 , action:Erase, expected:"b , c" }
, Case{expr:"a , b , c" , span:4..5 , action:Erase, expected:"a , c" }
, Case{expr:"a , b , c" , span:8..9 , action:Erase, expected:"a , b" }
, Case{expr:"f a b" , span:2..3 , action:Erase, expected:"f b" } , Case{expr:"f a b" , span:2..3 , action:Erase, expected:"f b" }
, Case{expr:"f a b" , span:4..5 , action:Erase, expected:"f a" } , Case{expr:"f a b" , span:4..5 , action:Erase, expected:"f a" }
, Case{expr:"(a + b + c)", span:5..6 , action:Erase, expected: "(a + c)" } , Case{expr:"(a + b + c)", span:5..6 , action:Erase, expected: "(a + c)" }
, Case{expr:"(a + b + c" , span:5..6 , action:Erase, expected: "(a + c" } , Case{expr:"(a + b + c" , span:5..6 , action:Erase, expected: "(a + c" }
]; ];
let parser = Parser::new_or_panic(); let parser = Parser::new();
for case in cases { for case in cases {
case.run(&parser); case.run(&parser);
} }
} }
#[wasm_bindgen_test] #[test]
fn possible_actions_in_span_tree() { fn possible_actions_in_span_tree() {
#[derive(Debug)] #[derive(Debug)]
struct Case { struct Case {
@ -385,10 +375,9 @@ mod test {
Case { expr: "[a,b]", span: 4..5, expected: &[] }, Case { expr: "[a,b]", span: 4..5, expected: &[] },
Case { expr: "(a + b + c)", span: 5..6, expected: &[Set, Erase] }, Case { expr: "(a + b + c)", span: 5..6, expected: &[Set, Erase] },
Case { expr: "(a", span: 1..2, expected: &[Set] }, Case { expr: "(a", span: 1..2, expected: &[Set] },
Case { expr: "(a", span: 0..1, expected: &[] },
Case { expr: "(a + b + c", span: 5..6, expected: &[Set, Erase] }, Case { expr: "(a + b + c", span: 5..6, expected: &[Set, Erase] },
]; ];
let parser = Parser::new_or_panic(); let parser = Parser::new();
for case in cases { for case in cases {
case.run(&parser); case.run(&parser);
} }

View File

@ -11,13 +11,11 @@ use crate::ArgumentInfo;
use crate::Node; use crate::Node;
use crate::SpanTree; use crate::SpanTree;
use ast::assoc::Assoc;
use ast::crumbs::Located; use ast::crumbs::Located;
use ast::opr::GeneralizedInfix; use ast::opr::GeneralizedInfix;
use ast::Ast; use ast::Ast;
use ast::HasRepr; use ast::HasRepr;
use ast::MacroAmbiguousSegment; use ast::SpanSeed;
use ast::MacroMatchSegment;
use std::collections::VecDeque; use std::collections::VecDeque;
@ -26,7 +24,6 @@ use std::collections::VecDeque;
// ============== // ==============
pub mod context; pub mod context;
pub mod macros;
pub use context::Context; pub use context::Context;
@ -39,7 +36,7 @@ pub use context::Context;
/// A trait for all types from which we can generate referred SpanTree. Meant to be implemented for /// A trait for all types from which we can generate referred SpanTree. Meant to be implemented for
/// all AST-like structures. /// all AST-like structures.
pub trait SpanTreeGenerator<T> { pub trait SpanTreeGenerator<T> {
/// Generate node with it's whole subtree. /// Generate node with its whole subtree.
fn generate_node( fn generate_node(
&self, &self,
kind: impl Into<node::Kind>, kind: impl Into<node::Kind>,
@ -272,13 +269,8 @@ fn generate_node_for_ast<T: Payload>(
match ast.shape() { match ast.shape() {
ast::Shape::Prefix(_) => ast::Shape::Prefix(_) =>
ast::prefix::Chain::from_ast(ast).unwrap().generate_node(kind, context), ast::prefix::Chain::from_ast(ast).unwrap().generate_node(kind, context),
// Lambdas should fall in _ case, because we don't want to create subports for ast::Shape::Tree(tree) if tree.type_info != ast::TreeType::Lambda =>
// them tree_generate_node(tree, kind, context, ast.id),
ast::Shape::Match(_) if ast::macros::as_lambda_match(ast).is_none() =>
ast::known::Match::try_new(ast.clone_ref()).unwrap().generate_node(kind, context),
ast::Shape::Ambiguous(_) => ast::known::Ambiguous::try_new(ast.clone_ref())
.unwrap()
.generate_node(kind, context),
_ => { _ => {
let size = (ast.len().value as i32).byte_diff(); let size = (ast.len().value as i32).byte_diff();
let ast_id = ast.id; let ast_id = ast.id;
@ -374,7 +366,7 @@ fn generate_node_for_opr_chain<T: Payload>(
} }
gen.generate_empty_node(InsertionPointType::Append); gen.generate_empty_node(InsertionPointType::Append);
if ast::opr::assoc(&this.operator) == Assoc::Right { if this.operator.right_assoc {
gen.reverse_children(); gen.reverse_children();
} }
@ -461,135 +453,6 @@ fn generate_node_for_prefix_chain<T: Payload>(
} }
// === Match ===
impl<T: Payload> SpanTreeGenerator<T> for ast::known::Match {
fn generate_node(
&self,
kind: impl Into<node::Kind>,
context: &impl Context,
) -> FallibleResult<Node<T>> {
generate_node_for_known_match(self, kind.into(), context)
}
}
fn generate_node_for_known_match<T: Payload>(
this: &ast::known::Match,
kind: node::Kind,
context: &impl Context,
) -> FallibleResult<Node<T>> {
let removable = false;
let children_kind = node::Kind::argument().with_removable(removable);
let mut gen = ChildGenerator::default();
if let Some(pat) = &this.pfx {
for macros::AstInPattern { ast, crumbs } in macros::all_ast_nodes_in_pattern(pat) {
let ast_crumb = ast::crumbs::MatchCrumb::Pfx { val: crumbs };
let located_ast = Located::new(ast_crumb, ast.wrapped);
gen.generate_ast_node(located_ast, children_kind.clone(), context)?;
gen.spacing(ast.off);
}
}
let first_segment_index = 0;
generate_children_from_segment(&mut gen, first_segment_index, &this.segs.head, context)?;
for (index, segment) in this.segs.tail.iter().enumerate() {
gen.spacing(segment.off);
generate_children_from_segment(&mut gen, index + 1, &segment.wrapped, context)?;
}
Ok(Node {
kind,
size: gen.current_offset,
children: gen.children,
ast_id: this.id(),
payload: default(),
})
}
fn generate_children_from_segment<T: Payload>(
gen: &mut ChildGenerator<T>,
index: usize,
segment: &MacroMatchSegment<Ast>,
context: &impl Context,
) -> FallibleResult {
// generate child for head
let ast = segment.head.clone_ref();
let segment_crumb = ast::crumbs::SegmentMatchCrumb::Head;
let ast_crumb = ast::crumbs::MatchCrumb::Segs { val: segment_crumb, index };
let located_ast = Located::new(ast_crumb, ast);
gen.generate_ast_node(located_ast, node::Kind::Token, context)?;
for macros::AstInPattern { ast, crumbs } in macros::all_ast_nodes_in_pattern(&segment.body) {
let child_kind = match crumbs.last() {
Some(ast::crumbs::PatternMatchCrumb::Tok) => node::Kind::Token,
_ => node::Kind::argument().into(),
};
gen.spacing(ast.off);
let segment_crumb = ast::crumbs::SegmentMatchCrumb::Body { val: crumbs };
let ast_crumb = ast::crumbs::MatchCrumb::Segs { val: segment_crumb, index };
let located_ast = Located::new(ast_crumb, ast.wrapped);
gen.generate_ast_node(located_ast, child_kind, context)?;
}
Ok(())
}
// === Ambiguous ==
impl<T: Payload> SpanTreeGenerator<T> for ast::known::Ambiguous {
fn generate_node(
&self,
kind: impl Into<node::Kind>,
context: &impl Context,
) -> FallibleResult<Node<T>> {
generate_node_for_known_ambiguous(self, kind.into(), context)
}
}
fn generate_node_for_known_ambiguous<T: Payload>(
this: &ast::known::Ambiguous,
kind: node::Kind,
context: &impl Context,
) -> FallibleResult<Node<T>> {
let mut gen = ChildGenerator::default();
let first_segment_index = 0;
generate_children_from_ambiguous(&mut gen, first_segment_index, &this.segs.head, context)?;
for (index, segment) in this.segs.tail.iter().enumerate() {
gen.spacing(segment.off);
generate_children_from_ambiguous(&mut gen, index + 1, &segment.wrapped, context)?;
}
Ok(Node {
kind,
size: gen.current_offset,
children: gen.children,
ast_id: this.id(),
payload: default(),
})
}
fn generate_children_from_ambiguous<T: Payload>(
gen: &mut ChildGenerator<T>,
index: usize,
segment: &MacroAmbiguousSegment<Ast>,
context: &impl Context,
) -> FallibleResult {
let children_kind = node::Kind::argument();
// generate child for head
let ast = segment.head.clone_ref();
let segment_crumb = ast::crumbs::AmbiguousSegmentCrumb::Head;
let ast_crumb = ast::crumbs::AmbiguousCrumb { field: segment_crumb, index };
let located_ast = Located::new(ast_crumb, ast);
gen.generate_ast_node(located_ast, node::Kind::Token, context)?;
if let Some(sast) = &segment.body {
gen.spacing(sast.off);
let field = ast::crumbs::AmbiguousSegmentCrumb::Body;
let located_ast =
Located::new(ast::crumbs::AmbiguousCrumb { index, field }, sast.clone_ref());
gen.generate_ast_node(located_ast, children_kind, context)?;
}
Ok(())
}
// === Common Utility == // === Common Utility ==
/// Build a prefix application-like span tree structure where the prefix argument has not been /// Build a prefix application-like span tree structure where the prefix argument has not been
@ -632,6 +495,61 @@ fn generate_expected_arguments<T: Payload>(
// =========================
// === SpanTree for Tree ===
// =========================
fn tree_generate_node<T: Payload>(
tree: &ast::Tree<Ast>,
kind: impl Into<node::Kind>,
context: &impl Context,
ast_id: Option<Id>,
) -> FallibleResult<Node<T>> {
let kind = match &tree.type_info {
ast::TreeType::Group => node::Kind::Group,
_ => kind.into(),
};
let mut children = vec![];
let size;
if let Some(leaf_info) = &tree.leaf_info {
size = ByteDiff::from(leaf_info.len());
} else {
let mut offset = ByteDiff::from(0);
for (index, raw_span_info) in tree.span_info.iter().enumerate() {
match raw_span_info {
SpanSeed::Space(ast::SpanSeedSpace { space }) => offset += ByteDiff::from(space),
SpanSeed::Token(ast::SpanSeedToken { token }) => {
let kind = node::Kind::Token;
let size = ByteDiff::from(token.len());
let ast_crumbs = vec![ast::crumbs::TreeCrumb { index }.into()];
let node = Node { kind, size, ..default() };
children.push(node::Child { node, offset, ast_crumbs });
offset += size;
}
SpanSeed::Child(ast::SpanSeedChild { node }) => {
let kind = node::Kind::Argument(node::Argument {
removable: false,
name: None,
tp: None,
call_id: None,
tag_values: vec![],
});
let node = node.generate_node(kind, context)?;
let child_size = node.size;
let ast_crumbs = vec![ast::crumbs::TreeCrumb { index }.into()];
children.push(node::Child { node, offset, ast_crumbs });
offset += child_size;
}
}
}
size = offset;
}
let payload = default();
Ok(Node { kind, size, children, ast_id, payload })
}
// =================== // ===================
// === MockContext === // === MockContext ===
// =================== // ===================
@ -676,21 +594,13 @@ mod test {
use crate::node::Payload; use crate::node::Payload;
use crate::ArgumentInfo; use crate::ArgumentInfo;
use ast::crumbs::AmbiguousCrumb;
use ast::crumbs::AmbiguousSegmentCrumb;
use ast::crumbs::InfixCrumb; use ast::crumbs::InfixCrumb;
use ast::crumbs::PatternMatchCrumb;
use ast::crumbs::PrefixCrumb; use ast::crumbs::PrefixCrumb;
use ast::crumbs::SectionLeftCrumb; use ast::crumbs::SectionLeftCrumb;
use ast::crumbs::SectionRightCrumb; use ast::crumbs::SectionRightCrumb;
use ast::crumbs::SectionSidesCrumb;
use ast::Crumbs; use ast::Crumbs;
use ast::IdMap; use ast::IdMap;
use parser_scala::Parser; use parser::Parser;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
/// A helper function which removes information about expression id from thw tree rooted at /// A helper function which removes information about expression id from thw tree rooted at
@ -716,9 +626,9 @@ mod test {
} }
} }
#[wasm_bindgen_test] #[test]
fn generating_span_tree() { fn generating_span_tree() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let mut id_map = IdMap::default(); let mut id_map = IdMap::default();
id_map.generate(0..15); id_map.generate(0..15);
id_map.generate(0..11); id_map.generate(0..11);
@ -733,7 +643,7 @@ mod test {
let (span, id) = id_map_entry; let (span, id) = id_map_entry;
let node = tree.root_ref().find_by_span(&span); let node = tree.root_ref().find_by_span(&span);
assert!(node.is_some(), "Node with span {span} not found"); assert!(node.is_some(), "Node with span {span} not found");
assert_eq!(node.unwrap().node.ast_id, Some(id)); assert_eq!(node.unwrap().node.ast_id, Some(id), "Span: {span}");
} }
// Check the other fields: // Check the other fields:
@ -763,9 +673,9 @@ mod test {
assert_eq!(expected, tree) assert_eq!(expected, tree)
} }
#[wasm_bindgen_test] #[test]
fn generate_span_tree_with_chains() { fn generate_span_tree_with_chains() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let ast = parser.parse_line_ast("2 + 3 + foo bar baz 13 + 5").unwrap(); let ast = parser.parse_line_ast("2 + 3 + foo bar baz 13 + 5").unwrap();
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap(); let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
clear_expression_ids(&mut tree.root); clear_expression_ids(&mut tree.root);
@ -806,198 +716,54 @@ mod test {
assert_eq!(expected, tree); assert_eq!(expected, tree);
} }
#[wasm_bindgen_test] #[test]
#[ignore]
fn generating_span_tree_from_right_assoc_operator() { fn generating_span_tree_from_right_assoc_operator() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let ast = parser.parse_line_ast("1,2,3").unwrap(); let ast = parser.parse_line_ast("1<|2<|3").unwrap();
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap(); let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
clear_expression_ids(&mut tree.root); clear_expression_ids(&mut tree.root);
clear_parameter_infos(&mut tree.root); clear_parameter_infos(&mut tree.root);
let expected = TreeBuilder::new(7)
let expected = TreeBuilder::new(5)
.add_empty_child(0, Append) .add_empty_child(0, Append)
.add_leaf(0, 1, node::Kind::argument().removable(), InfixCrumb::LeftOperand) .add_leaf(0, 1, node::Kind::argument().removable(), InfixCrumb::LeftOperand)
.add_leaf(1, 1, node::Kind::operation(), InfixCrumb::Operator) .add_leaf(1, 2, node::Kind::operation(), InfixCrumb::Operator)
.add_child(2, 3, node::Kind::Chained, InfixCrumb::RightOperand) .add_child(3, 3, node::Kind::Chained, InfixCrumb::RightOperand)
.add_empty_child(0, Append) .add_empty_child(0, Append)
.add_leaf(0, 1, node::Kind::argument().removable(), InfixCrumb::LeftOperand) .add_leaf(0, 1, node::Kind::argument().removable(), InfixCrumb::LeftOperand)
.add_leaf(1, 1, node::Kind::operation(), InfixCrumb::Operator) .add_leaf(1, 2, node::Kind::operation(), InfixCrumb::Operator)
.add_empty_child(2, AfterTarget) .add_empty_child(3, AfterTarget)
.add_leaf(2, 1, node::Kind::this().removable(), InfixCrumb::RightOperand) .add_leaf(3, 1, node::Kind::this().removable(), InfixCrumb::RightOperand)
.add_empty_child(3, BeforeTarget) .add_empty_child(4, BeforeTarget)
.done() .done()
.build(); .build();
assert_eq!(expected, tree) assert_eq!(expected, tree)
} }
#[wasm_bindgen_test] #[test]
fn generating_span_tree_from_section() { #[ignore]
let parser = Parser::new_or_panic();
// The star makes `SectionSides` ast being one of the parameters of + chain. First + makes
// SectionRight, and last + makes SectionLeft.
let ast = parser.parse_line_ast("+ * + + 2 +").unwrap();
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
clear_expression_ids(&mut tree.root);
clear_parameter_infos(&mut tree.root);
let expected = TreeBuilder::new(11)
.add_child(0, 9, node::Kind::Chained, SectionLeftCrumb::Arg)
.add_child(0, 5, node::Kind::Chained, InfixCrumb::LeftOperand)
.add_child(0, 3, node::Kind::Chained, SectionLeftCrumb::Arg)
.add_empty_child(0, BeforeTarget)
.add_leaf(0, 1, node::Kind::operation(), SectionRightCrumb::Opr)
.add_child(2, 1, node::Kind::argument().removable(), SectionRightCrumb::Arg)
.add_empty_child(0, BeforeTarget)
.add_leaf(0, 1, node::Kind::operation(), SectionSidesCrumb)
.add_empty_child(1, Append)
.done()
.add_empty_child(3, Append)
.done()
.add_leaf(4, 1, node::Kind::operation(), SectionLeftCrumb::Opr)
.add_empty_child(5, Append)
.done()
.add_leaf(6, 1, node::Kind::operation(), InfixCrumb::Operator)
.add_leaf(8, 1, node::Kind::argument().removable(), InfixCrumb::RightOperand)
.add_empty_child(9, Append)
.done()
.add_leaf(10, 1, node::Kind::operation(), SectionLeftCrumb::Opr)
.add_empty_child(11, Append)
.build();
assert_eq!(expected, tree);
}
#[wasm_bindgen_test]
fn generating_span_tree_from_right_assoc_section() { fn generating_span_tree_from_right_assoc_section() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let ast = parser.parse_line_ast(",2,").unwrap(); let ast = parser.parse_line_ast("<|2<|").unwrap();
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap(); let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
clear_expression_ids(&mut tree.root); clear_expression_ids(&mut tree.root);
clear_parameter_infos(&mut tree.root); clear_parameter_infos(&mut tree.root);
let expected = TreeBuilder::new(5)
let expected = TreeBuilder::new(3)
.add_empty_child(0, Append) .add_empty_child(0, Append)
.add_leaf(0, 1, node::Kind::operation(), SectionRightCrumb::Opr) .add_leaf(0, 2, node::Kind::operation(), SectionRightCrumb::Opr)
.add_child(1, 2, node::Kind::Chained, SectionRightCrumb::Arg) .add_child(2, 2, node::Kind::Chained, SectionRightCrumb::Arg)
.add_empty_child(0, Append) .add_empty_child(0, Append)
.add_leaf(0, 1, node::Kind::argument().removable(), SectionLeftCrumb::Arg) .add_leaf(0, 1, node::Kind::argument().removable(), SectionLeftCrumb::Arg)
.add_leaf(1, 1, node::Kind::operation(), SectionLeftCrumb::Opr) .add_leaf(1, 1, node::Kind::operation(), SectionLeftCrumb::Opr)
.add_empty_child(2, BeforeTarget) .add_empty_child(2, BeforeTarget)
.done() .done()
.build(); .build();
assert_eq!(expected, tree); assert_eq!(expected, tree);
} }
#[wasm_bindgen_test] #[test]
fn generating_span_tree_from_matched_macros() {
use PatternMatchCrumb::*;
let parser = Parser::new_or_panic();
let mut id_map = IdMap::default();
let expected_id = id_map.generate(0..29);
let expression = "if foo then (a + b) x else ()";
let ast = parser.parse_line_ast_with_id_map(expression, id_map).unwrap();
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
// Check if expression id is set
assert_eq!(tree.root_ref().ast_id, Some(expected_id));
// Check the other fields
clear_expression_ids(&mut tree.root);
clear_parameter_infos(&mut tree.root);
let seq = Seq { right: false };
let if_then_else_cr = vec![seq, Or, Build];
let parens_cr = vec![seq, Or, Or, Build];
let expected = TreeBuilder::new(29)
.add_leaf(0, 2, node::Kind::Token, segment_head_crumbs(0))
.add_leaf(3, 3, node::Kind::argument(), segment_body_crumbs(0, &if_then_else_cr))
.add_leaf(7, 4, node::Kind::Token, segment_head_crumbs(1))
.add_child(12, 9, node::Kind::argument(), segment_body_crumbs(1, &if_then_else_cr))
.add_child(0, 7, node::Kind::operation(), PrefixCrumb::Func)
.add_leaf(0, 1, node::Kind::Token, segment_head_crumbs(0))
.add_child(1, 5, node::Kind::argument(), segment_body_crumbs(0, &parens_cr))
.add_empty_child(0, BeforeTarget)
.add_leaf(0, 1, node::Kind::this(), InfixCrumb::LeftOperand)
.add_empty_child(1, AfterTarget)
.add_leaf(2, 1, node::Kind::operation(), InfixCrumb::Operator)
.add_leaf(4, 1, node::Kind::argument(), InfixCrumb::RightOperand)
.add_empty_child(5, Append)
.done()
.add_leaf(6, 1, node::Kind::Token, segment_head_crumbs(1))
.done()
.add_empty_child(8, BeforeTarget)
.add_leaf(8, 1, node::Kind::this(), PrefixCrumb::Arg)
.add_empty_child(9, Append)
.done()
.add_leaf(22, 4, node::Kind::Token, segment_head_crumbs(2))
.add_child(27, 2, node::Kind::argument(), segment_body_crumbs(2, &if_then_else_cr))
.add_leaf(0, 1, node::Kind::Token, segment_head_crumbs(0))
.add_leaf(1, 1, node::Kind::Token, segment_head_crumbs(1))
.done()
.build();
assert_eq!(expected, tree);
}
#[wasm_bindgen_test]
fn generating_span_tree_from_matched_list_macro() {
use PatternMatchCrumb::*;
let parser = Parser::new_or_panic();
let expression = "[a,b]";
let ast = parser.parse_line_ast(expression).unwrap();
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
// Check the other fields
clear_expression_ids(&mut tree.root);
let left_seq = Seq { right: false };
let right_seq = Seq { right: true };
let many = Many { index: 0 };
let first_element_cr = vec![left_seq, Or, Or, left_seq, Build];
let second_element_cr = vec![left_seq, Or, Or, right_seq, many, right_seq, Build];
let comma_cr = vec![left_seq, Or, Or, right_seq, many, left_seq, Tok];
let expected = TreeBuilder::new(5)
.add_leaf(0, 1, node::Kind::Token, segment_head_crumbs(0))
.add_leaf(1, 1, node::Kind::argument(), segment_body_crumbs(0, &first_element_cr))
.add_leaf(2, 1, node::Kind::Token, segment_body_crumbs(0, &comma_cr))
.add_leaf(3, 1, node::Kind::argument(), segment_body_crumbs(0, &second_element_cr))
.add_leaf(4, 1, node::Kind::Token, segment_head_crumbs(1))
.build();
assert_eq!(expected, tree);
}
#[wasm_bindgen_test]
fn generating_span_tree_from_ambiguous_macros() {
let parser = Parser::new_or_panic();
let mut id_map = IdMap::default();
id_map.generate(0..2);
let ast = parser.parse_line_ast_with_id_map("(4", id_map.clone()).unwrap();
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
// Check the expression id:
let (_, expected_id) = id_map.vec.first().unwrap();
assert_eq!(tree.root_ref().ast_id, Some(*expected_id));
// Check the other fields:
clear_expression_ids(&mut tree.root);
let head_crumb = AmbiguousCrumb { index: 0, field: AmbiguousSegmentCrumb::Head };
let body_crumb = AmbiguousCrumb { index: 0, field: AmbiguousSegmentCrumb::Body };
let expected = TreeBuilder::new(2)
.add_leaf(0, 1, node::Kind::Token, head_crumb)
.add_leaf(1, 1, node::Kind::argument(), body_crumb)
.build();
assert_eq!(expected, tree);
}
#[wasm_bindgen_test]
fn generating_span_tree_for_lambda() { fn generating_span_tree_for_lambda() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let ast = parser.parse_line_ast("foo a-> b + c").unwrap(); let ast = parser.parse_line_ast("foo a-> b + c").unwrap();
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap(); let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
clear_expression_ids(&mut tree.root); clear_expression_ids(&mut tree.root);
@ -1013,9 +779,9 @@ mod test {
assert_eq!(expected, tree); assert_eq!(expected, tree);
} }
#[wasm_bindgen_test] #[test]
fn generating_span_tree_for_unfinished_call() { fn generating_span_tree_for_unfinished_call() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let this_param = |call_id| ArgumentInfo { let this_param = |call_id| ArgumentInfo {
name: Some("self".to_owned()), name: Some("self".to_owned()),
tp: Some("Any".to_owned()), tp: Some("Any".to_owned()),
@ -1143,17 +909,4 @@ mod test {
clear_parameter_infos(&mut tree.root); clear_parameter_infos(&mut tree.root);
assert_eq!(tree, expected); assert_eq!(tree, expected);
} }
fn segment_body_crumbs(
index: usize,
pattern_crumb: &[PatternMatchCrumb],
) -> ast::crumbs::MatchCrumb {
let val = ast::crumbs::SegmentMatchCrumb::Body { val: pattern_crumb.to_vec() };
ast::crumbs::MatchCrumb::Segs { val, index }
}
fn segment_head_crumbs(index: usize) -> ast::crumbs::MatchCrumb {
let val = ast::crumbs::SegmentMatchCrumb::Head;
ast::crumbs::MatchCrumb::Segs { val, index }
}
} }

View File

@ -1,139 +0,0 @@
//! A module with utilities for generating SpanTree from macros (Match and Ambiguous).
// TODO[ao] Duplicated with `pattern_subcrumbs` function in `ast::crumbs`, but adds information
// about spacing. All the 'crumblike' utilities should be merged to one solution
use crate::prelude::*;
use ast::crumbs::PatternMatchCrumb;
use ast::Ast;
use ast::MacroPatternMatch;
use ast::Shifted;
// ======================
// === LocatedPattern ===
// ======================
/// A fragment of MacroPatternMatch localized by PatternMatchCrumbs.
#[allow(missing_docs)]
#[derive(Debug)]
pub struct LocatedPattern<'a> {
pub pattern: &'a MacroPatternMatch<Shifted<Ast>>,
pub crumbs: Vec<PatternMatchCrumb>,
}
// ==================
// === PatternDfs ===
// ==================
/// A iterator over all nodes in MacroPatternMatch tree, traversing it with DFS algorithm.
struct PatternDfs<'a> {
/// The FILO queue of nodes to visit.
to_visit: Vec<LocatedPattern<'a>>,
}
impl<'a> Iterator for PatternDfs<'a> {
type Item = LocatedPattern<'a>;
fn next(&mut self) -> Option<Self::Item> {
let to_return = self.to_visit.pop();
if let Some(pattern) = &to_return {
self.push_children_to_visit(pattern);
}
to_return
}
}
impl<'a> PatternDfs<'a> {
/// Create iterator which start from `root` node.
pub fn new(root: &'a MacroPatternMatch<Shifted<Ast>>) -> Self {
let first_to_visit = LocatedPattern { pattern: root, crumbs: vec![] };
PatternDfs { to_visit: vec![first_to_visit] }
}
/// Obtain all children of `pattern` and push them to `to_visit` queue.
fn push_children_to_visit(&mut self, pattern: &LocatedPattern<'a>) {
use ast::MacroPatternMatchRaw::*;
match pattern.pattern.deref() {
Except(pat) => self.push_child_to_visit(pattern, &pat.elem, PatternMatchCrumb::Except),
Tag(pat) => self.push_child_to_visit(pattern, &pat.elem, PatternMatchCrumb::Tag),
Cls(pat) => self.push_child_to_visit(pattern, &pat.elem, PatternMatchCrumb::Cls),
Or(pat) => self.push_child_to_visit(pattern, &pat.elem, PatternMatchCrumb::Or),
Seq(pat) => {
let (left_elem, right_elem) = &pat.elem;
self.push_child_to_visit(pattern, right_elem, PatternMatchCrumb::Seq {
right: true,
});
self.push_child_to_visit(pattern, left_elem, PatternMatchCrumb::Seq {
right: false,
});
}
Many(pat) =>
for (index, elem) in pat.elem.iter().enumerate().rev() {
self.push_child_to_visit(pattern, elem, PatternMatchCrumb::Many { index });
},
// Other patterns does not have children.
_ => {}
}
}
fn push_child_to_visit(
&mut self,
pattern: &LocatedPattern<'a>,
child: &'a MacroPatternMatch<Shifted<Ast>>,
crumb: PatternMatchCrumb,
) {
let loc_pattern = LocatedPattern {
pattern: child,
crumbs: pattern.crumbs.iter().cloned().chain(std::iter::once(crumb)).collect(),
};
self.to_visit.push(loc_pattern);
}
}
// ==========================================
// === Retrieving AST Nodes From Patterns ===
// ==========================================
/// An AST node being inside a Match node
#[allow(missing_docs)]
#[derive(Debug)]
pub struct AstInPattern {
pub ast: Shifted<Ast>,
pub crumbs: Vec<PatternMatchCrumb>,
}
/// Helper function that returns all AST nodes being on leaves of MacroPatternMatch.
pub fn all_ast_nodes_in_pattern(
pattern: &MacroPatternMatch<Shifted<Ast>>,
) -> impl Iterator<Item = AstInPattern> + '_ {
use ast::MacroPatternMatchRaw::*;
PatternDfs::new(pattern).filter_map(|pattern| {
let opt_ast_and_crumb = match pattern.pattern.deref() {
Build(pat) => Some((&pat.elem, PatternMatchCrumb::Build)),
Err(pat) => Some((&pat.elem, PatternMatchCrumb::Err)),
Tok(pat) => Some((&pat.elem, PatternMatchCrumb::Tok)),
Blank(pat) => Some((&pat.elem, PatternMatchCrumb::Blank)),
Var(pat) => Some((&pat.elem, PatternMatchCrumb::Var)),
Cons(pat) => Some((&pat.elem, PatternMatchCrumb::Cons)),
Opr(pat) => Some((&pat.elem, PatternMatchCrumb::Opr)),
Mod(pat) => Some((&pat.elem, PatternMatchCrumb::Mod)),
Num(pat) => Some((&pat.elem, PatternMatchCrumb::Num)),
Text(pat) => Some((&pat.elem, PatternMatchCrumb::Text)),
Block(pat) => Some((&pat.elem, PatternMatchCrumb::Block)),
Macro(pat) => Some((&pat.elem, PatternMatchCrumb::Macro)),
Invalid(pat) => Some((&pat.elem, PatternMatchCrumb::Invalid)),
_ => None,
};
opt_ast_and_crumb.map(|(ast, crumb)| AstInPattern {
ast: ast.clone(),
crumbs: pattern.crumbs.into_iter().chain(std::iter::once(crumb)).collect(),
})
})
}

View File

@ -8,7 +8,6 @@ use crate::iter::LeafIterator;
use crate::iter::TreeFragment; use crate::iter::TreeFragment;
use crate::ArgumentInfo; use crate::ArgumentInfo;
use ast::crumbs::IntoCrumbs;
use enso_text as text; use enso_text as text;
@ -65,12 +64,6 @@ impl<T: Payload> Node<T> {
default() default()
} }
/// Define a new child by using the `ChildBuilder` pattern. Consumes self.
pub fn new_child(mut self, f: impl FnOnce(ChildBuilder<T>) -> ChildBuilder<T>) -> Self {
ChildBuilder::apply_to_node(&mut self, f);
self
}
/// Payload mapping utility. /// Payload mapping utility.
pub fn map<S>(self, f: impl Copy + Fn(T) -> S) -> Node<S> { pub fn map<S>(self, f: impl Copy + Fn(T) -> S) -> Node<S> {
let kind = self.kind; let kind = self.kind;
@ -86,15 +79,9 @@ impl<T: Payload> Node<T> {
#[allow(missing_docs)] #[allow(missing_docs)]
impl<T: Payload> Node<T> { impl<T: Payload> Node<T> {
// FIXME[WD]: This is a hack, which just checks token placement, not a real solution.
/// Check whether the node is a parensed expression.
pub fn is_parensed(&self) -> bool { pub fn is_parensed(&self) -> bool {
let check = |t: Option<&Child<T>>| { self.kind == Kind::Group
t.map(|t| t.kind == Kind::Token && t.size.value == 1) == Some(true)
};
check(self.children.first()) && check(self.children.last()) && self.children.len() == 3
} }
pub fn is_root(&self) -> bool { pub fn is_root(&self) -> bool {
self.kind.is_root() self.kind.is_root()
} }
@ -216,116 +203,6 @@ impl<T> DerefMut for Child<T> {
// ====================
// === ChildBuilder ===
// ====================
/// A builder pattern for `SpanTree`. A think wrapper for `Child` which adds useful methods for
/// building properties of the current node.
///
/// This builder exposes two main functions - `new_child`, and `add_child`. The former provides a
/// nice, user-friendly interface for building a `SpanTree`, while the later provides a very
/// explicit argument setting interface meant for building `SpanTree` for shape testing purposes.
#[derive(Debug)]
#[allow(missing_docs)]
pub struct ChildBuilder<T = ()> {
pub child: Child<T>,
}
impl<T> Deref for ChildBuilder<T> {
type Target = Child<T>;
fn deref(&self) -> &Self::Target {
&self.child
}
}
impl<T> DerefMut for ChildBuilder<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.child
}
}
impl<T: Payload> ChildBuilder<T> {
/// Constructor.
pub fn new(child: Child<T>) -> Self {
Self { child }
}
/// Add new child and use the `ChildBuilder` pattern to define its properties. This is a smart
/// child constructor. This function will automatically compute all not provided properties,
/// such as span or offset. Moreover, it will default all other not provided fields.
pub fn new_child(mut self, f: impl FnOnce(Self) -> Self) -> Self {
Self::apply_to_node(&mut self.node, f);
self
}
/// Define a new child by using the `ChildBuilder` pattern.
fn apply_to_node(node: &mut Node<T>, f: impl FnOnce(ChildBuilder<T>) -> ChildBuilder<T>) {
let mut new_child = Child::default();
let offset = node.size;
new_child.offset = offset;
let builder = ChildBuilder::new(new_child);
let child = f(builder).child;
let offset_diff = child.offset - offset;
node.size = node.size + child.size + offset_diff;
node.children.push(child);
}
/// Add new child and use the `ChildBuilder` pattern to define its properties. This function
/// accepts explicit list of arguments and disables all automatic computation of spans and
/// offsets. It is useful for testing purposes.
pub fn add_child(
mut self,
offset: usize,
size: usize,
kind: Kind,
crumbs: impl IntoCrumbs,
f: impl FnOnce(Self) -> Self,
) -> Self {
let child: ChildBuilder<T> = ChildBuilder::new(default());
let child = f(child.offset(offset.into()).size(size.into()).kind(kind).crumbs(crumbs));
self.node.children.push(child.child);
self
}
/// Offset setter.
pub fn offset(mut self, offset: ByteDiff) -> Self {
self.offset = offset;
self
}
/// Crumbs setter.
pub fn crumbs(mut self, crumbs: impl IntoCrumbs) -> Self {
self.ast_crumbs = crumbs.into_crumbs();
self
}
/// Kind setter.
pub fn kind(mut self, kind: impl Into<Kind>) -> Self {
self.node.kind = kind.into();
self
}
/// Size setter.
pub fn size(mut self, size: ByteDiff) -> Self {
self.node.size = size;
self
}
/// Expression ID setter.
pub fn ast_id(mut self, id: ast::Id) -> Self {
self.node.ast_id = Some(id);
self
}
/// Expression ID generator.
pub fn new_ast_id(self) -> Self {
self.ast_id(ast::Id::new_v4())
}
}
// ============== // ==============
// === Crumbs === // === Crumbs ===
// ============== // ==============
@ -545,7 +422,7 @@ impl<'a, T: Payload> Ref<'a, T> {
!ch.ast_crumbs.is_empty() && ast_crumbs.starts_with(&ch.ast_crumbs) !ch.ast_crumbs.is_empty() && ast_crumbs.starts_with(&ch.ast_crumbs)
}) })
.or_else(|| { .or_else(|| {
// We try to find appriopriate node second time, this time expecting case of // We try to find appropriate node second time, this time expecting case of
// "prefix-like" nodes with `InsertionPoint(ExpectedArgument(_))`. See also docs // "prefix-like" nodes with `InsertionPoint(ExpectedArgument(_))`. See also docs
// for `generate::generate_expected_argument`. // for `generate::generate_expected_argument`.
// TODO[ao]: As implementation of SpanTree will extend there may be some day // TODO[ao]: As implementation of SpanTree will extend there may be some day
@ -561,7 +438,7 @@ impl<'a, T: Payload> Ref<'a, T> {
} }
} }
/// Get the node which exactly matches the given Span. If there many such node's, it pick first /// Get the node which exactly matches the given Span. If there are many such nodes, pick first
/// found by DFS. /// found by DFS.
pub fn find_by_span(self, span: &text::Range<Byte>) -> Option<Ref<'a, T>> { pub fn find_by_span(self, span: &text::Range<Byte>) -> Option<Ref<'a, T>> {
if self.span() == *span { if self.span() == *span {

View File

@ -31,6 +31,8 @@ pub enum Kind {
/// between AST tokens. For example, given expression `foo bar`, the span assigned to the /// between AST tokens. For example, given expression `foo bar`, the span assigned to the
/// `InsertionPoint` between `foo` and `bar` should be set to 3. /// `InsertionPoint` between `foo` and `bar` should be set to 3.
InsertionPoint(InsertionPoint), InsertionPoint(InsertionPoint),
/// A parenthesized expression.
Group,
} }
@ -197,6 +199,7 @@ impl Kind {
Self::Argument(_) => "Argument", Self::Argument(_) => "Argument",
Self::Token => "Token", Self::Token => "Token",
Self::InsertionPoint(_) => "InsertionPoint", Self::InsertionPoint(_) => "InsertionPoint",
Self::Group => "Group",
} }
} }
} }

View File

@ -23,7 +23,7 @@ use double_representation::node::MainLine;
use double_representation::node::NodeInfo; use double_representation::node::NodeInfo;
use double_representation::node::NodeLocation; use double_representation::node::NodeLocation;
use engine_protocol::language_server; use engine_protocol::language_server;
use parser_scala::Parser; use parser::Parser;
use span_tree::action::Action; use span_tree::action::Action;
use span_tree::action::Actions; use span_tree::action::Actions;
use span_tree::generate::context::CalledMethodInfo; use span_tree::generate::context::CalledMethodInfo;
@ -334,15 +334,10 @@ impl Connections {
pub fn name_for_ast(ast: &Ast) -> String { pub fn name_for_ast(ast: &Ast) -> String {
use ast::*; use ast::*;
match ast.shape() { match ast.shape() {
Shape::Tree(tree) if let Some(name) = &tree.descriptive_name => name.to_string(),
Shape::Var(ident) => ident.name.clone(), Shape::Var(ident) => ident.name.clone(),
Shape::Cons(ident) => ident.name.to_lowercase(), Shape::Cons(ident) => ident.name.to_lowercase(),
Shape::Number(_) => "number".into(), 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() { Shape::Opr(opr) => match opr.name.as_ref() {
"+" => "sum", "+" => "sum",
"*" => "product", "*" => "product",
@ -1041,8 +1036,7 @@ pub mod tests {
use double_representation::name::project; use double_representation::name::project;
use engine_protocol::language_server::MethodPointer; use engine_protocol::language_server::MethodPointer;
use enso_text::index::*; use enso_text::index::*;
use parser_scala::Parser; use parser::Parser;
use wasm_bindgen_test::wasm_bindgen_test;
@ -1097,7 +1091,7 @@ pub mod tests {
/// Create a graph controller from the current mock data. /// Create a graph controller from the current mock data.
pub fn graph(&self) -> Handle { pub fn graph(&self) -> Handle {
let parser = Parser::new().unwrap(); let parser = Parser::new();
let urm = Rc::new(model::undo_redo::Repository::new()); let urm = Rc::new(model::undo_redo::Repository::new());
let module = self.module_data().plain(&parser, urm); let module = self.module_data().plain(&parser, urm);
let id = self.graph_id.clone(); let id = self.graph_id.clone();
@ -1109,7 +1103,6 @@ pub mod tests {
self.module_path.method_pointer(self.project_name.clone(), self.graph_id.to_string()) self.module_path.method_pointer(self.project_name.clone(), self.graph_id.to_string())
} }
#[profile(Debug)]
pub fn suggestion_db(&self) -> Rc<model::SuggestionDatabase> { pub fn suggestion_db(&self) -> Rc<model::SuggestionDatabase> {
use model::suggestion_database::SuggestionDatabase; use model::suggestion_database::SuggestionDatabase;
let entries = self.suggestions.iter(); let entries = self.suggestions.iter();
@ -1147,7 +1140,7 @@ pub mod tests {
} }
} }
#[wasm_bindgen_test] #[test]
fn node_operations() { fn node_operations() {
Fixture::set_up().run(|graph| async move { Fixture::set_up().run(|graph| async move {
let uid = graph.all_node_infos().unwrap()[0].id(); let uid = graph.all_node_infos().unwrap()[0].id();
@ -1158,7 +1151,7 @@ pub mod tests {
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_notification_relay() { fn graph_controller_notification_relay() {
Fixture::set_up().run(|graph| async move { Fixture::set_up().run(|graph| async move {
let mut sub = graph.subscribe(); let mut sub = graph.subscribe();
@ -1168,7 +1161,7 @@ pub mod tests {
}); });
} }
#[wasm_bindgen_test] #[test]
fn suggestion_db_updates_graph_values() { fn suggestion_db_updates_graph_values() {
Fixture::set_up().run(|graph| async move { Fixture::set_up().run(|graph| async move {
let mut sub = graph.subscribe(); let mut sub = graph.subscribe();
@ -1181,7 +1174,7 @@ pub mod tests {
}); });
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_inline_definition() { fn graph_controller_inline_definition() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
const EXPRESSION: &str = "2+2"; const EXPRESSION: &str = "2+2";
@ -1196,7 +1189,7 @@ pub mod tests {
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_block_definition() { fn graph_controller_block_definition() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
test.data.code = r" test.data.code = r"
@ -1212,7 +1205,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_parse_expression() { fn graph_controller_parse_expression() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
test.run(|graph| async move { test.run(|graph| async move {
@ -1227,7 +1220,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn span_tree_context_handling_metadata_and_name() { fn span_tree_context_handling_metadata_and_name() {
let entry = crate::test::mock::data::suggestion_entry_foo(); let entry = crate::test::mock::data::suggestion_entry_foo();
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
@ -1266,7 +1259,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_used_names_in_inline_def() { fn graph_controller_used_names_in_inline_def() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
test.data.code = "main = foo".into(); test.data.code = "main = foo".into();
@ -1277,7 +1270,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_nested_definition() { fn graph_controller_nested_definition() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
test.data.code = r"main = test.data.code = r"main =
@ -1298,7 +1291,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn collapsing_nodes_avoids_name_conflicts() { fn collapsing_nodes_avoids_name_conflicts() {
// Checks that generated name avoid collision with other methods defined in the module // Checks that generated name avoid collision with other methods defined in the module
// and with symbols used that could be shadowed by the extracted method's name. // and with symbols used that could be shadowed by the extracted method's name.
@ -1335,7 +1328,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn collapsing_nodes() { fn collapsing_nodes() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
let code = r" let code = r"
@ -1385,7 +1378,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_doubly_nested_definition() { fn graph_controller_doubly_nested_definition() {
// Tests editing nested definition that requires transforming inline expression into // Tests editing nested definition that requires transforming inline expression into
// into a new block. // into a new block.
@ -1404,7 +1397,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_node_operations_node() { fn graph_controller_node_operations_node() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
const PROGRAM: &str = r" const PROGRAM: &str = r"
@ -1483,7 +1476,8 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
#[ignore] // FIXME (https://github.com/enso-org/enso/issues/5574)
fn graph_controller_connections_listing() { fn graph_controller_connections_listing() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
const PROGRAM: &str = r" const PROGRAM: &str = r"
@ -1532,7 +1526,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_create_connection() { fn graph_controller_create_connection() {
/// A case for creating connection test. The field's names are short to be able to write /// 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). /// nice-to-read table of cases without very long lines (see `let cases` below).
@ -1573,22 +1567,18 @@ main =
} }
} }
let cases = &[ let cases = &[Case { src: "x", dst: "foo", expected: "x", ports: (&[], &[]) }, Case {
Case { src: "x", dst: "foo", expected: "x", ports: (&[], &[]) }, src: "Vec x y",
Case { src: "x,y", dst: "foo a", expected: "foo y", ports: (&[4], &[2]) }, dst: "1 + 2 + 3",
Case { expected: "x + 2 + 3",
src: "Vec x y", ports: (&[0, 2], &[0, 1]),
dst: "1 + 2 + 3", }];
expected: "x + 2 + 3",
ports: (&[0, 2], &[0, 1]),
},
];
for case in cases { for case in cases {
case.run() case.run()
} }
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_create_connection_reordering() { fn graph_controller_create_connection_reordering() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
const PROGRAM: &str = r"main = const PROGRAM: &str = r"main =
@ -1621,7 +1611,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_create_connection_reordering_with_dependency() { fn graph_controller_create_connection_reordering_with_dependency() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
const PROGRAM: &str = r"main = const PROGRAM: &str = r"main =
@ -1660,7 +1650,7 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn graph_controller_create_connection_introducing_var() { fn graph_controller_create_connection_introducing_var() {
let mut test = Fixture::set_up(); let mut test = Fixture::set_up();
const PROGRAM: &str = r"main = const PROGRAM: &str = r"main =
@ -1697,9 +1687,9 @@ main =
}) })
} }
#[wasm_bindgen_test] #[test]
fn suggested_names() { fn suggested_names() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let cases = [ let cases = [
("a+b", "sum"), ("a+b", "sum"),
("a-b", "difference"), ("a-b", "difference"),
@ -1722,7 +1712,7 @@ main =
} }
} }
#[wasm_bindgen_test] #[test]
fn disconnect() { fn disconnect() {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct Case { struct Case {
@ -1756,11 +1746,6 @@ main =
Case { dest_node_expr: "var + b + c", dest_node_expected: "_ + b + c" }, Case { dest_node_expr: "var + b + c", dest_node_expected: "_ + b + c" },
Case { dest_node_expr: "a + var + c", dest_node_expected: "a + _ + c" }, Case { dest_node_expr: "a + var + c", dest_node_expected: "a + _ + c" },
Case { dest_node_expr: "a + b + var", dest_node_expected: "a + b" }, Case { dest_node_expr: "a + b + var", dest_node_expected: "a + b" },
Case { dest_node_expr: "var , a", dest_node_expected: "_ , a" },
Case { dest_node_expr: "a , var", dest_node_expected: "a , _" },
Case { dest_node_expr: "var , b , c", dest_node_expected: "_ , b , c" },
Case { dest_node_expr: "a , var , c", dest_node_expected: "a , _ , c" },
Case { dest_node_expr: "a , b , var", dest_node_expected: "a , b" },
Case { Case {
dest_node_expr: "f\n bar a var", dest_node_expr: "f\n bar a var",
dest_node_expected: "f\n bar a _", dest_node_expected: "f\n bar a _",

View File

@ -385,7 +385,7 @@ pub mod tests {
impl MockData { impl MockData {
pub fn controller(&self) -> Handle { pub fn controller(&self) -> Handle {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let repository = Rc::new(model::undo_redo::Repository::new()); let repository = Rc::new(model::undo_redo::Repository::new());
let module = self.module.plain(&parser, repository); let module = self.module.plain(&parser, repository);
let method = self.graph.method(); let method = self.graph.method();

View File

@ -400,9 +400,7 @@ impl QueryData {
/// Escape a string to be used as a visualization argument. Transforms the string into an enso /// Escape a string to be used as a visualization argument. Transforms the string into an enso
/// expression with string literal. /// expression with string literal.
fn escape_visualization_argument(arg: &str) -> String { fn escape_visualization_argument(arg: &str) -> String {
let segment = ast::SegmentPlain { value: arg.into() }; Ast::raw_text_literal(arg).repr()
let text = ast::TextLineRaw { text: vec![segment.into()] };
text.repr()
} }
/// Escape a list of strings to be used as a visualization argument. Transforms the strings into /// Escape a list of strings to be used as a visualization argument. Transforms the strings into

View File

@ -7,7 +7,7 @@ use crate::prelude::*;
use double_representation::name::project; use double_representation::name::project;
use mockall::automock; use mockall::automock;
use parser_scala::Parser; use parser::Parser;
// ============== // ==============

View File

@ -15,7 +15,7 @@ use engine_protocol::project_manager;
use engine_protocol::project_manager::MissingComponentAction; use engine_protocol::project_manager::MissingComponentAction;
use engine_protocol::project_manager::ProjectMetadata; use engine_protocol::project_manager::ProjectMetadata;
use engine_protocol::project_manager::ProjectName; use engine_protocol::project_manager::ProjectName;
use parser_scala::Parser; use parser::Parser;
@ -69,7 +69,7 @@ impl Handle {
) -> Self { ) -> Self {
let current_project = Rc::new(CloneCell::new(project)); let current_project = Rc::new(CloneCell::new(project));
let status_notifications = default(); let status_notifications = default();
let parser = Parser::new_or_panic(); let parser = Parser::new();
let notifications = default(); let notifications = default();
Self { current_project, project_manager, status_notifications, parser, notifications } Self { current_project, project_manager, status_notifications, parser, notifications }
} }

View File

@ -11,7 +11,7 @@ use crate::model::project::synchronized::Properties;
use double_representation::name::project; use double_representation::name::project;
use engine_protocol::project_manager::ProjectName; use engine_protocol::project_manager::ProjectName;
use parser_scala::Parser; use parser::Parser;
@ -46,7 +46,7 @@ impl Handle {
/// Create IDE Controller for a given opened project. /// Create IDE Controller for a given opened project.
pub fn new(project: model::Project) -> Self { pub fn new(project: model::Project) -> Self {
let status_notifications = default(); let status_notifications = default();
let parser = Parser::new_or_panic(); let parser = Parser::new();
Self { status_notifications, parser, project } Self { status_notifications, parser, project }
} }
@ -73,7 +73,7 @@ impl Handle {
) )
.await?; .await?;
let status_notifications = default(); let status_notifications = default();
let parser = Parser::new_or_panic(); let parser = Parser::new();
Ok(Self { status_notifications, parser, project }) Ok(Self { status_notifications, parser, project })
} }
} }

View File

@ -14,7 +14,7 @@ use double_representation::name::QualifiedName;
use double_representation::text::apply_code_change_to_id_map; use double_representation::text::apply_code_change_to_id_map;
use engine_protocol::language_server; use engine_protocol::language_server;
use engine_protocol::types::Sha3_224; use engine_protocol::types::Sha3_224;
use parser_scala::Parser; use parser::Parser;
@ -89,7 +89,7 @@ impl Handle {
"The module controller ast was not synchronized with text editor \ "The module controller ast was not synchronized with text editor \
content!\n >>> Module: {my_code}\n >>> Editor: {code}" content!\n >>> Module: {my_code}\n >>> Editor: {code}"
); );
let actual_ast = self.parser.parse(code, default())?.try_into()?; let actual_ast = self.parser.parse(code, default()).try_into()?;
self.model.update_ast(actual_ast)?; self.model.update_ast(actual_ast)?;
} }
Ok(()) Ok(())
@ -171,7 +171,7 @@ impl Handle {
parser: Parser, parser: Parser,
repository: Rc<model::undo_redo::Repository>, repository: Rc<model::undo_redo::Repository>,
) -> FallibleResult<Self> { ) -> FallibleResult<Self> {
let ast = parser.parse(code.to_string(), id_map)?.try_into()?; let ast = parser.parse(code.to_string(), id_map).try_into()?;
let metadata = default(); let metadata = default();
let model = Rc::new(model::module::Plain::new(path, ast, metadata, repository)); let model = Rc::new(model::module::Plain::new(path, ast, metadata, repository));
Ok(Handle { model, language_server, parser }) Ok(Handle { model, language_server, parser })
@ -200,15 +200,14 @@ mod test {
use ast::Ast; use ast::Ast;
use ast::BlockLine; use ast::BlockLine;
use enso_text::index::*; use enso_text::index::*;
use parser_scala::Parser; use parser::Parser;
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test] #[test]
fn update_ast_after_text_change() { fn update_ast_after_text_change() {
TestWithLocalPoolExecutor::set_up().run_task(async { TestWithLocalPoolExecutor::set_up().run_task(async {
let ls = language_server::Connection::new_mock_rc(default()); let ls = language_server::Connection::new_mock_rc(default());
let parser = Parser::new().unwrap(); let parser = Parser::new();
let location = Path::from_mock_module_name("Test"); let location = Path::from_mock_module_name("Test");
let code = "2+2"; let code = "2+2";
let uuid1 = Uuid::new_v4(); let uuid1 = Uuid::new_v4();
@ -236,7 +235,10 @@ mod test {
Some(uuid1), Some(uuid1),
), ),
loff: 0, loff: 0,
opr: Ast::new(ast::Opr { name: "+".to_string() }, Some(uuid2)), opr: Ast::new(
ast::Opr { name: "+".to_string(), right_assoc: false },
Some(uuid2),
),
roff: 0, roff: 0,
rarg: Ast::new( rarg: Ast::new(
ast::Number { base: None, int: "2".to_string() }, ast::Number { base: None, int: "2".to_string() },

View File

@ -12,7 +12,7 @@ use engine_protocol::language_server::MethodPointer;
use engine_protocol::language_server::Path; use engine_protocol::language_server::Path;
use enso_frp::web::platform; use enso_frp::web::platform;
use enso_frp::web::platform::Platform; use enso_frp::web::platform::Platform;
use parser_scala::Parser; use parser::Parser;
@ -276,7 +276,7 @@ mod tests {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn adding_missing_main() { fn adding_missing_main() {
let _ctx = TestWithLocalPoolExecutor::set_up(); let _ctx = TestWithLocalPoolExecutor::set_up();
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
let mut data = crate::test::mock::Unified::new(); let mut data = crate::test::mock::Unified::new();
let module_name = data.module_path.module_name().to_owned(); let module_name = data.module_path.module_name().to_owned();
let main_ptr = main_method_ptr(data.project_name.clone(), &data.module_path); let main_ptr = main_method_ptr(data.project_name.clone(), &data.module_path);

View File

@ -26,7 +26,7 @@ use enso_text::Byte;
use enso_text::Location; use enso_text::Location;
use enso_text::Rope; use enso_text::Rope;
use flo_stream::Subscriber; use flo_stream::Subscriber;
use parser_scala::Parser; use parser::Parser;
// ============== // ==============
@ -1778,7 +1778,7 @@ pub mod test {
project.expect_qualified_name().returning_st(move || project_qname.clone()); project.expect_qualified_name().returning_st(move || project_qname.clone());
project.expect_name().returning_st(move || project_name.clone()); project.expect_name().returning_st(move || project_name.clone());
let project = Rc::new(project); let project = Rc::new(project);
ide.expect_parser().return_const(Parser::new_or_panic()); ide.expect_parser().return_const(Parser::new());
let current_project = project.clone_ref(); let current_project = project.clone_ref();
ide.expect_current_project().returning_st(move || Some(current_project.clone_ref())); ide.expect_current_project().returning_st(move || Some(current_project.clone_ref()));
ide.expect_manage_projects() ide.expect_manage_projects()
@ -2123,7 +2123,7 @@ pub mod test {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn parsed_input() { fn parsed_input() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
fn args_reprs(prefix: &ast::prefix::Chain) -> Vec<String> { fn args_reprs(prefix: &ast::prefix::Chain) -> Vec<String> {
prefix.args.iter().map(|arg| arg.repr()).collect() prefix.args.iter().map(|arg| arg.repr()).collect()
@ -2186,9 +2186,7 @@ pub mod test {
let expression = parsed.expression.unwrap(); let expression = parsed.expression.unwrap();
assert_eq!(expression.off, 0); assert_eq!(expression.off, 0);
assert_eq!(expression.func.repr(), "foo"); assert_eq!(expression.func.repr(), "foo");
assert_eq!(args_reprs(&expression), vec![" bar".to_string()]); assert_eq!(args_reprs(&expression), vec![" bar".to_string(), " (baz".to_string()]);
assert_eq!(parsed.pattern_offset, 1);
assert_eq!(parsed.pattern.as_str(), "(baz ");
} }
fn are_same( fn are_same(
@ -2272,7 +2270,7 @@ pub mod test {
} }
fn run(&self) { fn run(&self) {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let ast = parser.parse_line_ast(self.before).unwrap(); let ast = parser.parse_line_ast(self.before).unwrap();
let new_ast = apply_this_argument("foo", &ast); let new_ast = apply_this_argument("foo", &ast);
assert_eq!(new_ast.repr(), self.after, "Case {self:?} failed: {ast:?}"); assert_eq!(new_ast.repr(), self.after, "Case {self:?} failed: {ast:?}");
@ -2424,7 +2422,7 @@ pub mod test {
let module = searcher.graph.graph().module.clone_ref(); let module = searcher.graph.graph().module.clone_ref();
// Setup searcher. // Setup searcher.
let parser = Parser::new_or_panic(); let parser = Parser::new();
let picked_method = FragmentAddedByPickingSuggestion { let picked_method = FragmentAddedByPickingSuggestion {
id: CompletedFragmentId::Function, id: CompletedFragmentId::Function,
picked_suggestion: action::Suggestion::FromDatabase(entry4), picked_suggestion: action::Suggestion::FromDatabase(entry4),
@ -2507,7 +2505,7 @@ pub mod test {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn simple_function_call_parsing() { fn simple_function_call_parsing() {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let ast = parser.parse_line_ast("foo").unwrap(); let ast = parser.parse_line_ast("foo").unwrap();
let call = SimpleFunctionCall::try_new(&ast).expect("Returned None for \"foo\""); let call = SimpleFunctionCall::try_new(&ast).expect("Returned None for \"foo\"");

View File

@ -152,7 +152,7 @@ mod test {
use crate::executor::test_utils::TestWithLocalPoolExecutor; use crate::executor::test_utils::TestWithLocalPoolExecutor;
use enso_text::index::*; use enso_text::index::*;
use parser_scala::Parser; use parser::Parser;
use wasm_bindgen_test::wasm_bindgen_test; use wasm_bindgen_test::wasm_bindgen_test;
fn setup_mock_project(setup: impl FnOnce(&mut model::project::MockAPI)) -> model::Project { fn setup_mock_project(setup: impl FnOnce(&mut model::project::MockAPI)) -> model::Project {
@ -171,7 +171,7 @@ mod test {
test.run_task(async move { test.run_task(async move {
let ls = language_server::Connection::new_mock_rc(default()); let ls = language_server::Connection::new_mock_rc(default());
let path = model::module::Path::from_mock_module_name("Test"); let path = model::module::Path::from_mock_module_name("Test");
let parser = Parser::new().unwrap(); let parser = Parser::new();
let module_res = let module_res =
controller::Module::new_mock(path, "main = 2+2", default(), ls, parser, default()); controller::Module::new_mock(path, "main = 2+2", default(), ls, parser, default());
let module = module_res.unwrap(); let module = module_res.unwrap();
@ -204,7 +204,7 @@ mod test {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn obtain_text_controller_for_module() { fn obtain_text_controller_for_module() {
let parser = parser_scala::Parser::new_or_panic(); let parser = parser::Parser::new();
TestWithLocalPoolExecutor::set_up().run_task(async move { TestWithLocalPoolExecutor::set_up().run_task(async move {
let code = "2 + 2".to_string(); let code = "2 + 2".to_string();
let undo = default(); let undo = default();

View File

@ -43,6 +43,7 @@
#![feature(assert_matches)] #![feature(assert_matches)]
#![feature(hash_drain_filter)] #![feature(hash_drain_filter)]
#![feature(unwrap_infallible)] #![feature(unwrap_infallible)]
#![feature(if_let_guard)]
// === Standard Linter Configuration === // === Standard Linter Configuration ===
#![deny(non_ascii_idents)] #![deny(non_ascii_idents)]
#![warn(unsafe_code)] #![warn(unsafe_code)]

View File

@ -13,10 +13,10 @@ use double_representation::name::project;
use double_representation::name::QualifiedName; use double_representation::name::QualifiedName;
use engine_protocol::language_server::MethodPointer; use engine_protocol::language_server::MethodPointer;
use flo_stream::Subscriber; use flo_stream::Subscriber;
use parser_scala::api::ParsedSourceFile; use parser::api::ParsedSourceFile;
use parser_scala::api::PruneUnusedIds; use parser::api::PruneUnusedIds;
use parser_scala::api::SourceFile; use parser::api::SourceFile;
use parser_scala::Parser; use parser::Parser;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
@ -340,7 +340,7 @@ impl PruneUnusedIds for Metadata {
} }
} }
impl parser_scala::api::Metadata for Metadata {} impl parser::api::Metadata for Metadata {}
impl Default for Metadata { impl Default for Metadata {
fn default() -> Self { fn default() -> Self {
@ -738,7 +738,7 @@ pub mod test {
parser: &Parser, parser: &Parser,
repository: Rc<model::undo_redo::Repository>, repository: Rc<model::undo_redo::Repository>,
) -> Module { ) -> Module {
let ast = parser.parse_module(self.code.clone(), self.id_map.clone()).unwrap(); let ast = parser.parse_module(&self.code, self.id_map.clone()).unwrap();
let module = Plain::new(self.path.clone(), ast, self.metadata.clone(), repository); let module = Plain::new(self.path.clone(), ast, self.metadata.clone(), repository);
Rc::new(module) Rc::new(module)
} }
@ -746,8 +746,7 @@ pub mod test {
pub fn plain_from_code(code: impl Into<String>) -> Module { pub fn plain_from_code(code: impl Into<String>) -> Module {
let urm = default(); let urm = default();
MockData { code: code.into(), ..default() } MockData { code: code.into(), ..default() }.plain(&parser::Parser::new(), urm)
.plain(&parser_scala::Parser::new_or_panic(), urm)
} }
#[test] #[test]
@ -783,7 +782,7 @@ pub mod test {
assert_eq!(qualified.to_string(), "n.P.Foo.Bar"); assert_eq!(qualified.to_string(), "n.P.Foo.Bar");
} }
#[wasm_bindgen_test] #[test]
fn outdated_metadata_parses() { fn outdated_metadata_parses() {
// Metadata here will fail to serialize because `File` is not a valid qualified name. // Metadata here will fail to serialize because `File` is not a valid qualified name.
// Expected behavior is that invalid metadata parts will be filled with defaults. // Expected behavior is that invalid metadata parts will be filled with defaults.
@ -794,8 +793,7 @@ main = 5
#### METADATA #### #### METADATA ####
[[{"index":{"value":7},"size":{"value":8}},"04f2bbe4-6291-4bad-961c-146228f3aee4"],[{"index":{"value":15},"size":{"value":1}},"20f4e5e3-3ab4-4c68-ae7a-d261d3f23af0"],[{"index":{"value":16},"size":{"value":13}},"746b453a-3fed-4128-86ce-a3853ef684b0"],[{"index":{"value":0},"size":{"value":29}},"aead2cca-c429-47f2-85ef-fe090433990b"],[{"index":{"value":30},"size":{"value":4}},"063ab796-e79b-4037-bf94-1f24c9545b9a"],[{"index":{"value":35},"size":{"value":1}},"4b4992bd-7d8e-401b-aebf-42b30a4a5cae"],[{"index":{"value":37},"size":{"value":1}},"1d6660c6-a70b-4eeb-b5f7-82f05a51df25"],[{"index":{"value":30},"size":{"value":8}},"ad5b88bf-0cdb-4eba-90fe-07afc37e3953"],[{"index":{"value":0},"size":{"value":39}},"602dfcea-2321-48fa-95b1-1f58fb028099"]] [[{"index":{"value":7},"size":{"value":8}},"04f2bbe4-6291-4bad-961c-146228f3aee4"],[{"index":{"value":15},"size":{"value":1}},"20f4e5e3-3ab4-4c68-ae7a-d261d3f23af0"],[{"index":{"value":16},"size":{"value":13}},"746b453a-3fed-4128-86ce-a3853ef684b0"],[{"index":{"value":0},"size":{"value":29}},"aead2cca-c429-47f2-85ef-fe090433990b"],[{"index":{"value":30},"size":{"value":4}},"063ab796-e79b-4037-bf94-1f24c9545b9a"],[{"index":{"value":35},"size":{"value":1}},"4b4992bd-7d8e-401b-aebf-42b30a4a5cae"],[{"index":{"value":37},"size":{"value":1}},"1d6660c6-a70b-4eeb-b5f7-82f05a51df25"],[{"index":{"value":30},"size":{"value":8}},"ad5b88bf-0cdb-4eba-90fe-07afc37e3953"],[{"index":{"value":0},"size":{"value":39}},"602dfcea-2321-48fa-95b1-1f58fb028099"]]
{"ide":{"node":{"1d6660c6-a70b-4eeb-b5f7-82f05a51df25":{"position":{"vector":[-75.5,52]},"intended_method":{"module":"Base.System.File","defined_on_type":"File","name":"read"}}}}}"#; {"ide":{"node":{"1d6660c6-a70b-4eeb-b5f7-82f05a51df25":{"position":{"vector":[-75.5,52]},"intended_method":{"module":"Base.System.File","defined_on_type":"File","name":"read"}}}}}"#;
let result = Parser::new_or_panic().parse_with_metadata::<Metadata>(code.into()); let file = Parser::new().parse_with_metadata::<Metadata>(code);
let file = result.unwrap();
assert_eq!(file.ast.repr(), "import Standard.Visualization\nmain = 5"); assert_eq!(file.ast.repr(), "import Standard.Visualization\nmain = 5");
assert_eq!(file.metadata.ide.node.len(), 1); assert_eq!(file.metadata.ide.node.len(), 1);
let id = ast::Id::from_str("1d6660c6-a70b-4eeb-b5f7-82f05a51df25").unwrap(); let id = ast::Id::from_str("1d6660c6-a70b-4eeb-b5f7-82f05a51df25").unwrap();

View File

@ -19,9 +19,9 @@ use double_representation::definition::DefinitionInfo;
use double_representation::definition::DefinitionProvider; use double_representation::definition::DefinitionProvider;
use double_representation::import; use double_representation::import;
use flo_stream::Subscriber; use flo_stream::Subscriber;
use parser_scala::api::ParsedSourceFile; use parser::api::ParsedSourceFile;
use parser_scala::api::SourceFile; use parser::api::SourceFile;
use parser_scala::Parser; use parser::Parser;
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
@ -178,7 +178,7 @@ impl model::module::API for Module {
let replaced_end = code.offset_to_location_snapped(change.range.end); let replaced_end = code.offset_to_location_snapped(change.range.end);
let replaced_location = enso_text::Range::new(replaced_start, replaced_end); let replaced_location = enso_text::Range::new(replaced_start, replaced_end);
code.apply_change(change.as_ref()); code.apply_change(change.as_ref());
let new_ast = parser.parse(code.into(), new_id_map)?.try_into()?; let new_ast = parser.parse(code.to_string(), new_id_map).try_into()?;
let notification = NotificationKind::CodeChanged { change, replaced_location }; let notification = NotificationKind::CodeChanged { change, replaced_location };
self.update_content(notification, |content| content.ast = new_ast) self.update_content(notification, |content| content.ast = new_ast)
} }
@ -318,7 +318,7 @@ fn restore_edited_node_in_graph(
"Restoring edited node {node_id} to original expression \ "Restoring edited node {node_id} to original expression \
\"{previous_expression}\"." \"{previous_expression}\"."
); );
graph.edit_node(node_id, Parser::new()?.parse_line_ast(previous_expression)?)?; graph.edit_node(node_id, Parser::new().parse_line_ast(previous_expression)?)?;
md_entry.get_mut().intended_method = previous_intended_method; md_entry.get_mut().intended_method = previous_intended_method;
} }
None => {} None => {}
@ -360,7 +360,7 @@ mod test {
range: enso_text::Range::new(2.byte(), 5.byte()), range: enso_text::Range::new(2.byte(), 5.byte()),
text: "- abc".to_string(), text: "- abc".to_string(),
}; };
module.apply_code_change(change, &Parser::new_or_panic(), default()).unwrap(); module.apply_code_change(change, &Parser::new(), default()).unwrap();
assert_eq!("2 - abc", module.ast().repr()); assert_eq!("2 - abc", module.ast().repr());
} }
@ -391,7 +391,7 @@ mod test {
range: enso_text::Range::new(0.byte(), 1.byte()), range: enso_text::Range::new(0.byte(), 1.byte()),
text: "foo".to_string(), text: "foo".to_string(),
}; };
module.apply_code_change(change.clone(), &Parser::new_or_panic(), default()).unwrap(); module.apply_code_change(change.clone(), &Parser::new(), default()).unwrap();
let replaced_location = enso_text::Range { let replaced_location = enso_text::Range {
start: enso_text::Location { line: 0.into(), offset: 0.byte() }, start: enso_text::Location { line: 0.into(), offset: 0.byte() },
end: enso_text::Location { line: 0.into(), offset: 1.byte() }, end: enso_text::Location { line: 0.into(), offset: 1.byte() },

View File

@ -24,8 +24,8 @@ use enso_text::text;
use enso_text::Location; use enso_text::Location;
use enso_text::Range; use enso_text::Range;
use flo_stream::Subscriber; use flo_stream::Subscriber;
use parser_scala::api::SourceFile; use parser::api::SourceFile;
use parser_scala::Parser; use parser::Parser;
@ -172,9 +172,7 @@ impl Module {
info!("Read content of the module {path}, digest is {:?}", opened.current_version); info!("Read content of the module {path}, digest is {:?}", opened.current_version);
let end_of_file_byte = content.last_line_end_location(); let end_of_file_byte = content.last_line_end_location();
let end_of_file = content.utf16_code_unit_location_of_location(end_of_file_byte); let end_of_file = content.utf16_code_unit_location_of_location(end_of_file_byte);
// TODO[ao] We should not fail here when metadata are malformed, but discard them and set let source = parser.parse_with_metadata(opened.content);
// default instead.
let source = parser.parse_with_metadata(opened.content)?;
let digest = opened.current_version; let digest = opened.current_version;
let summary = ContentSummary { digest, end_of_file }; let summary = ContentSummary { digest, end_of_file };
let model = model::module::Plain::new(path, source.ast, source.metadata, repository); let model = model::module::Plain::new(path, source.ast, source.metadata, repository);
@ -726,12 +724,12 @@ pub mod test {
let parser = data.parser.clone(); let parser = data.parser.clone();
let module = fixture.synchronized_module(); let module = fixture.synchronized_module();
let new_content = "main =\n println \"Test\"".to_string(); let new_content = "main =\n println \"Test\"";
let new_ast = parser.parse_module(new_content, default()).unwrap(); let new_ast = parser.parse_module(new_content, default()).unwrap();
module.update_ast(new_ast).unwrap(); module.update_ast(new_ast).unwrap();
runner.perhaps_run_until_stalled(&mut fixture); runner.perhaps_run_until_stalled(&mut fixture);
let change = TextChange { range: (20..24).into(), text: "Test 2".to_string() }; let change = TextChange { range: (20..24).into(), text: "Test 2".to_string() };
module.apply_code_change(change, &Parser::new_or_panic(), default()).unwrap(); module.apply_code_change(change, &Parser::new(), default()).unwrap();
runner.perhaps_run_until_stalled(&mut fixture); runner.perhaps_run_until_stalled(&mut fixture);
}; };

View File

@ -14,7 +14,7 @@ use engine_protocol::language_server;
use engine_protocol::language_server::ContentRoot; use engine_protocol::language_server::ContentRoot;
use flo_stream::Subscriber; use flo_stream::Subscriber;
use mockall::automock; use mockall::automock;
use parser_scala::Parser; use parser::Parser;
use uuid::Uuid; use uuid::Uuid;

View File

@ -25,7 +25,7 @@ use engine_protocol::project_manager::MissingComponentAction;
use engine_protocol::project_manager::ProjectName; use engine_protocol::project_manager::ProjectName;
use flo_stream::Subscriber; use flo_stream::Subscriber;
use json_rpc::error::RpcError; use json_rpc::error::RpcError;
use parser_scala::Parser; use parser::Parser;
@ -287,9 +287,9 @@ impl Project {
let language_server = language_server_rpc.clone(); let language_server = language_server_rpc.clone();
let module_registry = default(); let module_registry = default();
let execution_contexts = default(); let execution_contexts = default();
let parser = Parser::new();
let visualization = let visualization =
controller::Visualization::new(language_server, embedded_visualizations); controller::Visualization::new(language_server, embedded_visualizations);
let parser = Parser::new_or_panic();
let language_server = &*language_server_rpc; let language_server = &*language_server_rpc;
let suggestion_db = SuggestionDatabase::create_synchronized(language_server); let suggestion_db = SuggestionDatabase::create_synchronized(language_server);
let suggestion_db = Rc::new(suggestion_db.await.map_err(&wrap)?); let suggestion_db = Rc::new(suggestion_db.await.map_err(&wrap)?);

View File

@ -152,7 +152,7 @@ impl Nodes {
removed_views removed_views
} }
/// Remove node represented by given view (if any) and return it's AST ID. /// Remove node represented by given view (if any) and return its AST ID.
pub fn remove_node(&mut self, node: ViewNodeId) -> Option<AstNodeId> { pub fn remove_node(&mut self, node: ViewNodeId) -> Option<AstNodeId> {
let ast_id = self.ast_node_by_view_id.remove(&node)?; let ast_id = self.ast_node_by_view_id.remove(&node)?;
self.nodes.remove(&ast_id); self.nodes.remove(&ast_id);
@ -827,10 +827,10 @@ impl<'a> ViewChange<'a> {
mod tests { mod tests {
use super::*; use super::*;
use engine_protocol::language_server::MethodPointer; use engine_protocol::language_server::MethodPointer;
use parser_scala::Parser; use parser::Parser;
fn create_test_node(expression: &str) -> controller::graph::Node { fn create_test_node(expression: &str) -> controller::graph::Node {
let parser = Parser::new_or_panic(); let parser = Parser::new();
let ast = parser.parse_line_ast(expression).unwrap(); let ast = parser.parse_line_ast(expression).unwrap();
controller::graph::Node { controller::graph::Node {
info: double_representation::node::NodeInfo { info: double_representation::node::NodeInfo {
@ -969,7 +969,7 @@ mod tests {
fn refreshing_node_expression() { fn refreshing_node_expression() {
let Fixture { state, nodes } = Fixture::setup_nodes(&["foo bar"]); let Fixture { state, nodes } = Fixture::setup_nodes(&["foo bar"]);
let node_id = nodes[0].node.id(); let node_id = nodes[0].node.id();
let new_ast = Parser::new_or_panic().parse_line_ast("foo baz").unwrap().with_id(node_id); let new_ast = Parser::new().parse_line_ast("foo baz").unwrap().with_id(node_id);
let new_node = controller::graph::Node { let new_node = controller::graph::Node {
info: double_representation::node::NodeInfo { info: double_representation::node::NodeInfo {
documentation: None, documentation: None,

View File

@ -134,7 +134,7 @@ pub mod mock {
pub module_path: model::module::Path, pub module_path: model::module::Path,
pub suggestions: HashMap<suggestion_database::entry::Id, suggestion_database::Entry>, pub suggestions: HashMap<suggestion_database::entry::Id, suggestion_database::Entry>,
pub context_id: model::execution_context::Id, pub context_id: model::execution_context::Id,
pub parser: parser_scala::Parser, pub parser: parser::Parser,
code: String, code: String,
id_map: ast::IdMap, id_map: ast::IdMap,
metadata: crate::model::module::Metadata, metadata: crate::model::module::Metadata,
@ -171,7 +171,7 @@ pub mod mock {
metadata: default(), metadata: default(),
context_id: CONTEXT_ID, context_id: CONTEXT_ID,
root_definition: definition_name(), root_definition: definition_name(),
parser: parser_scala::Parser::new_or_panic(), parser: parser::Parser::new(),
} }
} }
@ -180,7 +180,7 @@ pub mod mock {
} }
pub fn module(&self, urm: Rc<undo_redo::Manager>) -> crate::model::Module { pub fn module(&self, urm: Rc<undo_redo::Manager>) -> crate::model::Module {
let ast = self.parser.parse_module(self.code.clone(), self.id_map.clone()).unwrap(); let ast = self.parser.parse_module(&self.code, self.id_map.clone()).unwrap();
let path = self.module_path.clone(); let path = self.module_path.clone();
let metadata = self.metadata.clone(); let metadata = self.metadata.clone();
let repository = urm.repository.clone_ref(); let repository = urm.repository.clone_ref();

View File

@ -12,7 +12,8 @@ enso-prelude = { path = "../../../lib/rust/prelude" }
convert_case = { workspace = true } convert_case = { workspace = true }
span-tree = { path = "../language/span-tree" } span-tree = { path = "../language/span-tree" }
ast = { path = "../language/ast/impl" } ast = { path = "../language/ast/impl" }
parser-scala = { path = "../language/parser" } parser = { path = "../language/parser" }
parser-scala = { path = "../language/parser-scala" }
enso-text = { path = "../../../lib/rust/text" } enso-text = { path = "../../../lib/rust/text" }
double-representation = { path = "../controller/double-representation" } double-representation = { path = "../controller/double-representation" }
engine-protocol = { path = "../controller/engine-protocol" } engine-protocol = { path = "../controller/engine-protocol" }

View File

@ -5,7 +5,7 @@ use crate::prelude::*;
use double_representation::definition; use double_representation::definition;
use double_representation::definition::DefinitionName; use double_representation::definition::DefinitionName;
use double_representation::module; use double_representation::module;
use parser_scala::Parser; use parser::Parser;
@ -74,7 +74,7 @@ impl Example {
) -> FallibleResult<definition::ToAdd> { ) -> FallibleResult<definition::ToAdd> {
let base_name = self.function_name(); let base_name = self.function_name();
let name = DefinitionName::new_plain(module.generate_name(&base_name)?); let name = DefinitionName::new_plain(module.generate_name(&base_name)?);
let code_ast = parser.parse_module(self.code.clone(), default())?; let code_ast = parser.parse_module(&self.code, default())?;
let body_block = code_ast.shape().as_block(0).ok_or(InvalidExample)?; let body_block = code_ast.shape().as_block(0).ok_or(InvalidExample)?;
let body_ast = Ast::new(body_block, None); let body_ast = Ast::new(body_block, None);
Ok(definition::ToAdd::new_with_body(name, default(), body_ast)) Ok(definition::ToAdd::new_with_body(name, default(), body_ast))

View File

@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
ast = { path = "../language/ast/impl" } ast = { path = "../language/ast/impl" }
parser = { path = "../language/parser" }
enso-config = { path = "../config" } enso-config = { path = "../config" }
enso-frp = { path = "../../../lib/rust/frp" } enso-frp = { path = "../../../lib/rust/frp" }
enso-prelude = { path = "../../../lib/rust/prelude" } enso-prelude = { path = "../../../lib/rust/prelude" }
@ -23,7 +24,6 @@ ensogl-hardcoded-theme = { path = "../../../lib/rust/ensogl/app/theme/hardcoded"
ide-view-component-browser = { path = "component-browser" } ide-view-component-browser = { path = "component-browser" }
ide-view-documentation = { path = "documentation" } ide-view-documentation = { path = "documentation" }
ide-view-graph-editor = { path = "graph-editor" } ide-view-graph-editor = { path = "graph-editor" }
parser-scala = { path = "../language/parser" }
span-tree = { path = "../language/span-tree" } span-tree = { path = "../language/span-tree" }
js-sys = { workspace = true } js-sys = { workspace = true }
multi-map = { workspace = true } multi-map = { workspace = true }

View File

@ -9,12 +9,12 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
ast = { path = "../../../language/ast/impl" } ast = { path = "../../../language/ast/impl" }
parser = { path = "../../../language/parser" }
enso-frp = { path = "../../../../../lib/rust/frp" } enso-frp = { path = "../../../../../lib/rust/frp" }
ensogl = { path = "../../../../../lib/rust/ensogl" } ensogl = { path = "../../../../../lib/rust/ensogl" }
ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" } ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-text-msdf = { path = "../../../../../lib/rust/ensogl/component/text/src/font/msdf" } ensogl-text-msdf = { path = "../../../../../lib/rust/ensogl/component/text/src/font/msdf" }
ide-view = { path = "../.." } ide-view = { path = "../.." }
parser-scala = { path = "../../../language/parser" }
span-tree = { path = "../../../language/span-tree" } span-tree = { path = "../../../language/span-tree" }
uuid = { version = "0.8", features = ["v4", "wasm-bindgen"] } uuid = { version = "0.8", features = ["v4", "wasm-bindgen"] }
wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true }

View File

@ -15,10 +15,7 @@
#![warn(unused_import_braces)] #![warn(unused_import_braces)]
#![warn(unused_qualifications)] #![warn(unused_qualifications)]
use ast::crumbs::PatternMatchCrumb::*;
use ast::crumbs::*;
use ensogl::prelude::*; use ensogl::prelude::*;
use span_tree::traits::*;
use enso_frp as frp; use enso_frp as frp;
use ensogl::application::Application; use ensogl::application::Application;
@ -37,8 +34,7 @@ use ide_view::graph_editor::Type;
use ide_view::project; use ide_view::project;
use ide_view::root; use ide_view::root;
use ide_view::status_bar; use ide_view::status_bar;
use parser_scala::Parser; use parser::Parser;
use uuid::Uuid;
@ -323,7 +319,7 @@ fn init(app: &Application) {
pub fn expression_mock_string(label: &str) -> Expression { pub fn expression_mock_string(label: &str) -> Expression {
let pattern = Some(label.to_string()); let pattern = Some(label.to_string());
let code = format!("\"{label}\""); let code = format!("\"{label}\"");
let parser = Parser::new_or_panic(); let parser = Parser::new();
let parameters = vec![]; let parameters = vec![];
let ast = parser.parse_line_ast(&code).unwrap(); let ast = parser.parse_line_ast(&code).unwrap();
let invocation_info = span_tree::generate::context::CalledMethodInfo { parameters }; let invocation_info = span_tree::generate::context::CalledMethodInfo { parameters };
@ -338,7 +334,7 @@ pub fn expression_mock_string(label: &str) -> Expression {
pub fn expression_mock() -> Expression { pub fn expression_mock() -> Expression {
let pattern = Some("var1".to_string()); let pattern = Some("var1".to_string());
let code = "[1,2,3]".to_string(); let code = "[1,2,3]".to_string();
let parser = Parser::new_or_panic(); let parser = Parser::new();
let this_param = span_tree::ArgumentInfo { let this_param = span_tree::ArgumentInfo {
name: Some("self".to_owned()), name: Some("self".to_owned()),
tp: Some("Text".to_owned()), tp: Some("Text".to_owned()),
@ -355,52 +351,11 @@ pub fn expression_mock() -> Expression {
Expression { pattern, code, whole_expression_id, input_span_tree, output_span_tree } Expression { pattern, code, whole_expression_id, input_span_tree, output_span_tree }
} }
// TODO[ao] This expression mocks results in panic. If you want to use it, please fix it first.
pub fn expression_mock2() -> Expression {
let pattern = Some("var1".to_string());
let pattern_cr = vec![Seq { right: false }, Or, Or, Build];
let val = ast::crumbs::SegmentMatchCrumb::Body { val: pattern_cr };
let parens_cr = ast::crumbs::MatchCrumb::Segs { val, index: 0 };
let code = "make_maps size (distribution normal)".to_string();
let output_span_tree = span_tree::SpanTree::default();
let input_span_tree = span_tree::builder::TreeBuilder::new(36)
.add_child(0, 14, span_tree::node::Kind::Chained, PrefixCrumb::Func)
.add_child(0, 9, span_tree::node::Kind::operation(), PrefixCrumb::Func)
.set_ast_id(Uuid::new_v4())
.done()
.add_empty_child(10, span_tree::node::InsertionPointType::BeforeTarget)
.add_child(10, 4, span_tree::node::Kind::this().removable(), PrefixCrumb::Arg)
.set_ast_id(Uuid::new_v4())
.done()
.add_empty_child(14, span_tree::node::InsertionPointType::Append)
.set_ast_id(Uuid::new_v4())
.done()
.add_child(15, 21, span_tree::node::Kind::argument().removable(), PrefixCrumb::Arg)
.set_ast_id(Uuid::new_v4())
.add_child(1, 19, span_tree::node::Kind::argument(), parens_cr)
.set_ast_id(Uuid::new_v4())
.add_child(0, 12, span_tree::node::Kind::operation(), PrefixCrumb::Func)
.set_ast_id(Uuid::new_v4())
.done()
.add_empty_child(13, span_tree::node::InsertionPointType::BeforeTarget)
.add_child(13, 6, span_tree::node::Kind::this(), PrefixCrumb::Arg)
.set_ast_id(Uuid::new_v4())
.done()
.add_empty_child(19, span_tree::node::InsertionPointType::Append)
.done()
.done()
.add_empty_child(36, span_tree::node::InsertionPointType::Append)
.build();
let whole_expression_id = default();
let code = code.into();
Expression { pattern, code, whole_expression_id, input_span_tree, output_span_tree }
}
pub fn expression_mock3() -> Expression { pub fn expression_mock3() -> Expression {
let pattern = Some("Vector x y z".to_string()); let pattern = Some("Vector x y z".to_string());
// let code = "image.blur ((foo bar) baz)".to_string(); // let code = "image.blur ((foo bar) baz)".to_string();
let code = "Vector x y z".to_string(); let code = "Vector x y z".to_string();
let parser = Parser::new_or_panic(); let parser = Parser::new();
let this_param = span_tree::ArgumentInfo { let this_param = span_tree::ArgumentInfo {
name: Some("self".to_owned()), name: Some("self".to_owned()),
tp: Some("Image".to_owned()), tp: Some("Image".to_owned()),
@ -440,7 +395,7 @@ pub fn expression_mock3() -> Expression {
pub fn expression_mock_trim() -> Expression { pub fn expression_mock_trim() -> Expression {
let pattern = Some("trim_node".to_string()); let pattern = Some("trim_node".to_string());
let code = "\" hello \".trim".to_string(); let code = "\" hello \".trim".to_string();
let parser = Parser::new_or_panic(); let parser = Parser::new();
let this_param = span_tree::ArgumentInfo { let this_param = span_tree::ArgumentInfo {
name: Some("self".to_owned()), name: Some("self".to_owned()),
tp: Some("Text".to_owned()), tp: Some("Text".to_owned()),

View File

@ -104,6 +104,7 @@ impl Debug for Expression {
} }
} }
// === Pretty printing debug adapter === // === Pretty printing debug adapter ===
/// Debug adapter used for pretty-printing the `Expression` span tree. Can be used to print the /// Debug adapter used for pretty-printing the `Expression` span tree. Can be used to print the

View File

@ -171,13 +171,13 @@ public class ErrorCompilerTest extends CompilerTest {
@Test @Test
public void malformedImport7() throws Exception { public void malformedImport7() throws Exception {
var ir = parse("import Foo hiding"); var ir = parse("import Foo hiding");
assertSingleSyntaxError(ir, IR$Error$Syntax$InvalidImport$.MODULE$, "Imports must have a valid module path.", 17, 17); assertSingleSyntaxError(ir, IR$Error$Syntax$InvalidImport$.MODULE$, "Imports must have a valid module path.", 7, 17);
} }
@Test @Test
public void malformedImport8() throws Exception { public void malformedImport8() throws Exception {
var ir = parse("import Foo hiding X,"); var ir = parse("import Foo hiding X,");
assertSingleSyntaxError(ir, IR$Error$Syntax$InvalidImport$.MODULE$, "Imports must have a valid module path.", 18, 20); assertSingleSyntaxError(ir, IR$Error$Syntax$InvalidImport$.MODULE$, "Imports must have a valid module path.", 7, 20);
} }
@Test @Test

View File

@ -840,6 +840,9 @@ fn export() {
fn metadata_raw() { fn metadata_raw() {
let code = [ let code = [
"x", "x",
"",
"",
"",
"#### METADATA ####", "#### METADATA ####",
r#"[[{"index":{"value":7},"size":{"value":8}},"5bad897e-099b-4b00-9348-64092636746d"]]"#, r#"[[{"index":{"value":7},"size":{"value":8}},"5bad897e-099b-4b00-9348-64092636746d"]]"#,
]; ];

View File

@ -47,15 +47,10 @@ fn register_import_macros(macros: &mut resolver::SegmentMap<'_>) {
let defs = [ let defs = [
macro_definition! {("import", everything()) import_body}, macro_definition! {("import", everything()) import_body},
macro_definition! {("import", everything(), "as", everything()) import_body}, macro_definition! {("import", everything(), "as", everything()) import_body},
macro_definition! {("import", everything(), "hiding", everything()) import_body},
macro_definition! {("polyglot", everything(), "import", everything()) import_body}, macro_definition! {("polyglot", everything(), "import", everything()) import_body},
macro_definition! { macro_definition! {
("polyglot", everything(), "import", everything(), "as", everything()) import_body}, ("polyglot", everything(), "import", everything(), "as", everything()) import_body},
macro_definition! { macro_definition! {
("polyglot", everything(), "import", everything(), "hiding", everything()) import_body},
macro_definition! {
("from", everything(), "import", everything(), "hiding", everything()) import_body},
macro_definition! {
("from", everything(), "import", nothing(), "all", nothing()) import_body}, ("from", everything(), "import", nothing(), "all", nothing()) import_body},
macro_definition! { macro_definition! {
("from", everything(), "import", nothing(), "all", nothing(), "hiding", everything()) ("from", everything(), "import", nothing(), "all", nothing(), "hiding", everything())

View File

@ -9,7 +9,7 @@ use uuid::Uuid;
const MARKER: &str = "#### METADATA ####\n"; const MARKER: &str = "\n\n\n#### METADATA ####\n";
@ -47,6 +47,14 @@ impl From<MetadataFormat> for Metadata {
} }
} }
/// Split input source file into the code and the metadata section, if any was found.
pub fn extract(input: &str) -> (&str, Option<&str>) {
match input.rsplit_once(MARKER) {
Some((code, metadata)) => (code, Some(metadata)),
None => (input, None),
}
}
/// Given source code, if a metadata section is found: Attempt to parse it; return the result, and /// Given source code, if a metadata section is found: Attempt to parse it; return the result, and
/// the non-metadata portion of the input. /// the non-metadata portion of the input.
pub fn parse(input: &str) -> Option<(Result, &str)> { pub fn parse(input: &str) -> Option<(Result, &str)> {
@ -54,6 +62,18 @@ pub fn parse(input: &str) -> Option<(Result, &str)> {
Some((metadata.parse().map(|data: MetadataFormat| data.into()), code)) Some((metadata.parse().map(|data: MetadataFormat| data.into()), code))
} }
/// Parse just the metadata section.
pub fn parse_metadata(input: &str) -> Option<Vec<((usize, usize), Uuid)>> {
Some(
MetadataFormat::from_str(input)
.ok()?
.id_map
.into_iter()
.map(|(location, id)| ((location.index.value, location.size.value), id))
.collect(),
)
}
/// Result of parsing metadata. /// Result of parsing metadata.
pub type Result<T = Metadata> = std::result::Result<T, String>; pub type Result<T = Metadata> = std::result::Result<T, String>;

View File

@ -498,6 +498,33 @@ pub struct DocComment<'s> {
pub newlines: Vec<token::Newline<'s>>, pub newlines: Vec<token::Newline<'s>>,
} }
impl<'s> DocComment<'s> {
/// Return the contents of the comment, with leading whitespace, the `##` token, and following
/// empty lines removed; newlines will be normalized.
pub fn content(&self) -> String {
let mut buf = String::new();
macro_rules! emit_token {
($buf:expr, $token:expr) => {{
$buf.push_str(&$token.left_offset.code.repr);
$buf.push_str(&$token.code.repr);
}};
}
for element in &self.elements {
match element {
TextElement::Section { text } => buf.push_str(&text.code.repr),
TextElement::Escape { token } => emit_token!(buf, token),
TextElement::Newline { newline } => {
buf.push_str(&newline.left_offset.code.repr);
buf.push('\n');
}
// Unreachable.
TextElement::Splice { .. } => continue,
}
}
buf
}
}
impl<'s> span::Builder<'s> for DocComment<'s> { impl<'s> span::Builder<'s> for DocComment<'s> {
fn add_to_span(&mut self, span: Span<'s>) -> Span<'s> { fn add_to_span(&mut self, span: Span<'s>) -> Span<'s> {
span.add(&mut self.open).add(&mut self.elements).add(&mut self.newlines) span.add(&mut self.open).add(&mut self.elements).add(&mut self.newlines)
@ -1320,6 +1347,13 @@ impl<'s> Tree<'s> {
self.visit_item(&mut visitor); self.visit_item(&mut visitor);
visitor.code visitor.code
} }
/// Return source code of this AST, excluding initial whitespace.
pub fn trimmed_code(&self) -> String {
let mut visitor = CodePrinterVisitor::default();
self.variant.visit_item(&mut visitor);
visitor.code
}
} }
@ -1391,3 +1425,61 @@ impl<'s> Tree<'s> {
self.visit_mut(&mut visitor); self.visit_mut(&mut visitor);
} }
} }
// === ItemFnVisitor ===
impl<'s> Tree<'s> {
/// Apply the provided function to each [`Token`] or [`Tree`] that is a child of the node.
pub fn visit_items<F>(&self, f: F)
where F: for<'a> FnMut(item::Ref<'s, 'a>) {
struct ItemFnVisitor<F> {
f: F,
}
impl<F> Visitor for ItemFnVisitor<F> {}
impl<'a, 's: 'a, F> ItemVisitor<'s, 'a> for ItemFnVisitor<F>
where F: FnMut(item::Ref<'s, 'a>)
{
fn visit_item(&mut self, item: item::Ref<'s, 'a>) -> bool {
(self.f)(item);
false
}
}
self.variant.visit_item(&mut ItemFnVisitor { f });
}
}
// =================
// === Traversal ===
// =================
impl<'s> Tree<'s> {
/// Return an iterator over the operands of the given left-associative operator, in reverse
/// order.
pub fn left_assoc_rev<'t, 'o>(&'t self, operator: &'o str) -> LeftAssocRev<'o, 't, 's> {
let tree = Some(self);
LeftAssocRev { operator, tree }
}
}
/// Iterator over the operands of a particular left-associative operator, in reverse order.
#[derive(Debug)]
pub struct LeftAssocRev<'o, 't, 's> {
operator: &'o str,
tree: Option<&'t Tree<'s>>,
}
impl<'o, 't, 's> Iterator for LeftAssocRev<'o, 't, 's> {
type Item = &'t Tree<'s>;
fn next(&mut self) -> Option<Self::Item> {
if let box Variant::OprApp(OprApp { lhs, opr: Ok(opr), rhs }) = &self.tree?.variant
&& opr.code == self.operator {
self.tree = lhs.into();
rhs.into()
} else {
self.tree.take()
}
}
}

View File

@ -284,7 +284,7 @@ where I: vec_indexed_by::Index
} }
/// Get the tail reference. /// Get the tail reference.
pub fn tail(&mut self) -> &[T] pub fn tail(&self) -> &[T]
where I: From<u8> { where I: From<u8> {
&self.elems[I::from(1_u8)..] &self.elems[I::from(1_u8)..]
} }