mirror of
https://github.com/roc-lang/roc.git
synced 2025-10-01 07:41:12 +00:00
Fix parsing edge case re: function calls at eof
This commit is contained in:
parent
4df8064407
commit
c8edddfd48
7 changed files with 1283 additions and 963 deletions
|
@ -57,6 +57,10 @@ pub enum Ident {
|
||||||
pub struct Path(String);
|
pub struct Path(String);
|
||||||
|
|
||||||
impl Path {
|
impl Path {
|
||||||
|
pub fn new(string: String) -> Path {
|
||||||
|
Path(string)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn into_string(self) -> String {
|
pub fn into_string(self) -> String {
|
||||||
let Path(str) = self;
|
let Path(str) = self;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ pub mod parse_state;
|
||||||
pub mod operator;
|
pub mod operator;
|
||||||
pub mod region;
|
pub mod region;
|
||||||
pub mod canonicalize;
|
pub mod canonicalize;
|
||||||
mod collections;
|
pub mod collections;
|
||||||
// mod ena;
|
// mod ena;
|
||||||
|
|
||||||
// #[macro_use]
|
// #[macro_use]
|
||||||
|
|
|
@ -394,10 +394,11 @@ where I: Stream<Item = char, Position = IndentablePosition>,
|
||||||
string("then"),
|
string("then"),
|
||||||
string("else"),
|
string("else"),
|
||||||
string("when"),
|
string("when"),
|
||||||
|
eof().with(value(""))
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -538,7 +539,7 @@ pub fn func_or_var<I>(min_indent: u32) -> impl Parser<Input = I, Output = Expr>
|
||||||
where I: Stream<Item = char, Position = IndentablePosition>,
|
where I: Stream<Item = char, Position = IndentablePosition>,
|
||||||
I::Error: ParseError<I::Item, I::Range, I::Position>
|
I::Error: ParseError<I::Item, I::Range, I::Position>
|
||||||
{
|
{
|
||||||
ident().and(optional(attempt(apply_args(min_indent))))
|
ident().and(optional(apply_args(min_indent)))
|
||||||
.map(|(name, opt_args): (String, Option<Vec<Located<Expr>>>)| {
|
.map(|(name, opt_args): (String, Option<Vec<Located<Expr>>>)| {
|
||||||
// Use optional(sep_by1()) over sep_by() to avoid
|
// Use optional(sep_by1()) over sep_by() to avoid
|
||||||
// allocating a Vec in the common case where this is a var
|
// allocating a Vec in the common case where this is a var
|
||||||
|
@ -559,10 +560,10 @@ where I: Stream<Item = char, Position = IndentablePosition>,
|
||||||
.with(
|
.with(
|
||||||
sep_by1(
|
sep_by1(
|
||||||
located(pattern(min_indent)),
|
located(pattern(min_indent)),
|
||||||
attempt(many1::<Vec<_>, _>(skipped_indented_whitespace_char(min_indent).skip(not_followed_by(string("=>")))))
|
attempt(many1::<Vec<_>, _>(skipped_indented_whitespace_char(min_indent).skip(not_followed_by(string("->")))))
|
||||||
))
|
))
|
||||||
.skip(indented_whitespaces(min_indent))
|
.skip(indented_whitespaces(min_indent))
|
||||||
.skip(string("=>"))
|
.skip(string("->"))
|
||||||
.skip(indented_whitespaces1(min_indent))
|
.skip(indented_whitespaces1(min_indent))
|
||||||
.and(located(expr_body(min_indent)))
|
.and(located(expr_body(min_indent)))
|
||||||
.map(|(patterns, closure_body)| {
|
.map(|(patterns, closure_body)| {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct Region {
|
pub struct Region {
|
||||||
pub start_line: u32,
|
pub start_line: u32,
|
||||||
|
@ -7,7 +9,7 @@ pub struct Region {
|
||||||
pub end_col: u32,
|
pub end_col: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub struct Located<T> {
|
pub struct Located<T> {
|
||||||
pub region: Region,
|
pub region: Region,
|
||||||
pub value: T,
|
pub value: T,
|
||||||
|
@ -30,3 +32,25 @@ impl<T> Located<T> {
|
||||||
Located { region: self.region, value: transform(&self.value) }
|
Located { region: self.region, value: transform(&self.value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for Located<T>
|
||||||
|
where T: fmt::Debug
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let region = self.region;
|
||||||
|
|
||||||
|
if region.start_line == 0 && region.start_col == 0 && region.end_line == 0 && region.end_col == 0 {
|
||||||
|
// In tests, it's super common to set all Located values to 0.
|
||||||
|
// Also in tests, we don't want to bother printing the locations
|
||||||
|
// because it makes failed assertions much harder to read.
|
||||||
|
self.value.fmt(f)
|
||||||
|
} else {
|
||||||
|
write!(f, "|L {}, C {} - L {}, C {}| {:?}",
|
||||||
|
region.start_line, region.start_col,
|
||||||
|
region.end_line, region.end_col,
|
||||||
|
self.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
tests/helpers/mod.rs
Normal file
67
tests/helpers/mod.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use roc::expr::{Expr, Pattern};
|
||||||
|
use roc::region::{Located, Region};
|
||||||
|
|
||||||
|
pub fn loc_box<T>(val: T) -> Box<Located<T>> {
|
||||||
|
Box::new(loc(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loc<T>(val: T) -> Located<T> {
|
||||||
|
Located::new(val, Region {
|
||||||
|
start_line: 0,
|
||||||
|
start_col: 0,
|
||||||
|
|
||||||
|
end_line: 0,
|
||||||
|
end_col: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero_loc<T>(located_val: Located<T>) -> Located<T> {
|
||||||
|
loc(located_val.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zero out the parse locations on everything in this Expr, so we can compare expected/actual without
|
||||||
|
/// having to account for that.
|
||||||
|
pub fn zero_loc_expr(expr: Expr) -> Expr {
|
||||||
|
use roc::expr::Expr::*;
|
||||||
|
|
||||||
|
match expr {
|
||||||
|
Int(_) | Frac(_, _) | Approx(_) | EmptyStr | Str(_) | Char(_) | Var(_) | EmptyRecord => expr,
|
||||||
|
InterpolatedStr(pairs, string) => InterpolatedStr(pairs.into_iter().map(|( prefix, ident )| ( prefix, zero_loc(ident))).collect(), string),
|
||||||
|
Assign(assignments, loc_ret) => {
|
||||||
|
let zeroed_assignments =
|
||||||
|
assignments.into_iter().map(|( pattern, loc_expr )|
|
||||||
|
( zero_loc_pattern(pattern), loc(zero_loc_expr(loc_expr.value)) )
|
||||||
|
).collect();
|
||||||
|
|
||||||
|
Assign(zeroed_assignments, loc_box(zero_loc_expr((*loc_ret).value)))
|
||||||
|
},
|
||||||
|
CallByName(ident, args) => CallByName(ident, args.into_iter().map(|arg| loc(zero_loc_expr(arg.value))).collect()),
|
||||||
|
Apply(fn_expr, args) => Apply(loc_box(zero_loc_expr((*fn_expr).value)), args.into_iter().map(|arg| loc(zero_loc_expr(arg.value))).collect()),
|
||||||
|
Operator(left, op, right) => Operator(loc_box(zero_loc_expr((*left).value)), zero_loc(op), loc_box(zero_loc_expr((*right).value))),
|
||||||
|
Closure(patterns, body) => Closure(patterns.into_iter().map(zero_loc).collect(), loc_box(zero_loc_expr((*body).value))),
|
||||||
|
ApplyVariant(_, None) => expr,
|
||||||
|
ApplyVariant(name, Some(args)) => ApplyVariant(name, Some(args.into_iter().map(|arg| loc(zero_loc_expr(arg.value))).collect())),
|
||||||
|
If(condition, if_true, if_false) => If(loc_box(zero_loc_expr((*condition).value)), loc_box(zero_loc_expr((*if_true).value)), loc_box(zero_loc_expr((*if_false).value))),
|
||||||
|
Case(condition, branches) =>
|
||||||
|
Case(
|
||||||
|
loc_box(zero_loc_expr((*condition).value)),
|
||||||
|
branches.into_iter().map(|( pattern, loc_expr )| ( zero_loc_pattern(pattern), loc(zero_loc_expr(loc_expr.value)) )).collect()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zero out the parse locations on everything in this Pattern, so we can compare expected/actual without
|
||||||
|
/// having to account for that.
|
||||||
|
pub fn zero_loc_pattern(loc_pattern: Located<Pattern>) -> Located<Pattern> {
|
||||||
|
use roc::expr::Pattern::*;
|
||||||
|
|
||||||
|
let pattern = loc_pattern.value;
|
||||||
|
|
||||||
|
match pattern {
|
||||||
|
Identifier(_) | Integer(_) | Fraction(_, _) | ExactString(_) | EmptyRecordLiteral | Underscore => loc(pattern),
|
||||||
|
Variant(loc_name, None) =>
|
||||||
|
loc(Variant(loc(loc_name.value), None)),
|
||||||
|
Variant(loc_name, Some(opt_located_patterns)) =>
|
||||||
|
loc(Variant(loc(loc_name.value), Some(opt_located_patterns.into_iter().map(|loc_pat| zero_loc_pattern(loc_pat)).collect()))),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,222 @@
|
||||||
#[macro_use] extern crate pretty_assertions;
|
#[macro_use] extern crate pretty_assertions;
|
||||||
|
#[macro_use] extern crate indoc;
|
||||||
extern crate combine;
|
extern crate combine;
|
||||||
|
|
||||||
extern crate roc;
|
extern crate roc;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_canonicalize {
|
mod test_canonicalize {
|
||||||
|
use roc::canonicalize;
|
||||||
|
use roc::canonicalize::{Expr, Output, Problem, Resolved, LocalSymbol, Symbol};
|
||||||
|
use roc::canonicalize::Expr::*;
|
||||||
|
use roc::canonicalize::Pattern::*;
|
||||||
|
use roc::expr::{Path, Ident};
|
||||||
|
use roc::operator::Operator::*;
|
||||||
|
use roc::expr;
|
||||||
|
use roc::region::Located;
|
||||||
|
use roc::parse;
|
||||||
|
use roc::collections::{ImMap, ImSet};
|
||||||
|
use roc::parse_state::{IndentablePosition};
|
||||||
|
use combine::{Parser, eof};
|
||||||
|
use combine::stream::state::{State};
|
||||||
|
use helpers::{loc, loc_box, zero_loc_expr};
|
||||||
|
|
||||||
|
fn can_expr(expr_str: &str) -> (Expr, Output, Vec<Problem>) {
|
||||||
|
can_expr_with(expr_str, &ImMap::default(), &ImMap::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_expr_with(
|
||||||
|
expr_str: &str,
|
||||||
|
declared_idents: &ImMap<(Option<Path>, String), Located<expr::Ident>>,
|
||||||
|
declared_variants: &ImMap<(Path, String), Located<expr::VariantName>>,
|
||||||
|
) -> (Expr, Output, Vec<Problem>) {
|
||||||
|
let parse_state: State<&str, IndentablePosition> = State::with_positioner(expr_str, IndentablePosition::default());
|
||||||
|
let expr = match parse::expr().skip(eof()).easy_parse(parse_state) {
|
||||||
|
Ok((expr, state)) => {
|
||||||
|
if !state.input.is_empty() {
|
||||||
|
panic!("There were unconsumed chars left over after parsing \"{}\" - the leftover string was: \"{}\"",
|
||||||
|
expr_str.to_string(), state.input.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
expr
|
||||||
|
},
|
||||||
|
Err(errors) => {
|
||||||
|
panic!("Parse error trying to parse \"{}\" - {}", expr_str.to_string(), errors.to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let home = Path::new("TestModule".to_string());
|
||||||
|
let (loc_expr, output, problems) =
|
||||||
|
canonicalize::canonicalize_declaration(home, loc(zero_loc_expr(expr)), declared_idents, declared_variants);
|
||||||
|
|
||||||
|
(loc_expr.value, output, problems)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recognized_local_sym(string: &str) -> Resolved<Symbol> {
|
||||||
|
Resolved::Recognized(local_sym(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_sym(string: &str) -> Symbol {
|
||||||
|
Symbol::Local(local(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local(string: &str) -> LocalSymbol {
|
||||||
|
LocalSymbol::new(string.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_output(
|
||||||
|
output: Output,
|
||||||
|
applied_variants: Vec<(Path, &str)>,
|
||||||
|
referenced_idents: Vec<(Option<Path>, &str)>,
|
||||||
|
tail_call: Option<Symbol>
|
||||||
|
) {
|
||||||
|
assert_eq!(
|
||||||
|
output.applied_variants,
|
||||||
|
ImSet::from(
|
||||||
|
applied_variants.into_iter().map(|(path, str_ref)|
|
||||||
|
(path, str_ref.to_string())
|
||||||
|
).collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
output.referenced_idents,
|
||||||
|
ImSet::from(
|
||||||
|
referenced_idents.into_iter().map(|(opt_path, str_ref)|
|
||||||
|
(opt_path, str_ref.to_string())
|
||||||
|
).collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(output.tail_call, tail_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_unrecognized_constant() {
|
||||||
|
let (expr, output, problems) = can_expr(indoc!(r#"
|
||||||
|
x
|
||||||
|
"#));
|
||||||
|
|
||||||
|
assert_eq!(problems, vec![
|
||||||
|
Problem::UnrecognizedConstant(loc(Ident::Unqualified("x".to_string())))
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(expr,
|
||||||
|
Var(Resolved::UnrecognizedConstant(loc(Ident::Unqualified("x".to_string()))))
|
||||||
|
);
|
||||||
|
|
||||||
|
check_output(output, vec![], vec![], None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_unrecognized_constant() {
|
||||||
|
let (expr, output, problems) = can_expr(indoc!(r#"
|
||||||
|
a = 5
|
||||||
|
b = 6
|
||||||
|
|
||||||
|
a + b * z
|
||||||
|
"#));
|
||||||
|
|
||||||
|
assert_eq!(problems, vec![
|
||||||
|
Problem::UnrecognizedConstant(loc(Ident::Unqualified("z".to_string())))
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(expr,
|
||||||
|
Assign(
|
||||||
|
vec![
|
||||||
|
(loc(Identifier(local_sym("a"))), loc(Int(5))),
|
||||||
|
(loc(Identifier(local_sym("b"))), loc(Int(6))),
|
||||||
|
],
|
||||||
|
loc_box(Operator(
|
||||||
|
loc_box(Var(recognized_local_sym("a"))),
|
||||||
|
loc(Plus),
|
||||||
|
loc_box(Operator(
|
||||||
|
loc_box(Var(recognized_local_sym("b"))),
|
||||||
|
loc(Star),
|
||||||
|
loc_box(Var(Resolved::UnrecognizedConstant(loc(Ident::Unqualified("z".to_string())))))
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
check_output(output, vec![], vec![(None, "a"), (None, "b")], None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_fibonacci() {
|
||||||
|
return ();
|
||||||
|
let (expr, output, problems) = can_expr(indoc!(r#"
|
||||||
|
fibonacci = \num =>
|
||||||
|
if num < 2 then
|
||||||
|
num
|
||||||
|
else
|
||||||
|
fibonacci (num - 1) + fibonacci (num - 2)
|
||||||
|
|
||||||
|
fibonacci 9
|
||||||
|
"#));
|
||||||
|
|
||||||
|
// assert_eq!(problems, vec![
|
||||||
|
// Problem::UnrecognizedConstant(loc(Ident::Unqualified("z".to_string())))
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// assert_eq!(expr,
|
||||||
|
// Assign(
|
||||||
|
// vec![
|
||||||
|
// (loc(Identifier(local_sym("a"))), loc(Int(5))),
|
||||||
|
// (loc(Identifier(local_sym("b"))), loc(Int(6))),
|
||||||
|
// ],
|
||||||
|
// loc_box(Operator(
|
||||||
|
// loc_box(Var(recognized_local_sym("a"))),
|
||||||
|
// loc(Plus),
|
||||||
|
// loc_box(Operator(
|
||||||
|
// loc_box(Var(recognized_local_sym("b"))),
|
||||||
|
// loc(Star),
|
||||||
|
// loc_box(Var(Resolved::UnrecognizedConstant(loc(Ident::Unqualified("z".to_string())))))
|
||||||
|
// )),
|
||||||
|
// ))
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
|
||||||
|
check_output(output, vec![], vec![(None, "num"), (None, "fibonacci")], None);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn can_fibonacci() {
|
||||||
|
// let (expr, output, problems) = can_expr(indoc!(r#"
|
||||||
|
// fibonacci = \num =>
|
||||||
|
// if num < 2 then
|
||||||
|
// num
|
||||||
|
// else
|
||||||
|
// fibonacci (num - 1) + fibonacci (num - 2)
|
||||||
|
// "#));
|
||||||
|
|
||||||
|
// assert_eq!(problems, vec![
|
||||||
|
// Problem::UnrecognizedConstant(loc(Ident::Unqualified("z".to_string())))
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// assert_eq!(expr,
|
||||||
|
// Assign(
|
||||||
|
// vec![
|
||||||
|
// (loc(Identifier(local_sym("a"))), loc(Int(5))),
|
||||||
|
// (loc(Identifier(local_sym("b"))), loc(Int(6))),
|
||||||
|
// ],
|
||||||
|
// loc_box(Operator(
|
||||||
|
// loc_box(Var(recognized_local_sym("a"))),
|
||||||
|
// loc(Plus),
|
||||||
|
// loc_box(Operator(
|
||||||
|
// loc_box(Var(recognized_local_sym("b"))),
|
||||||
|
// loc(Star),
|
||||||
|
// loc_box(Var(Resolved::UnrecognizedConstant(loc(Ident::Unqualified("z".to_string())))))
|
||||||
|
// )),
|
||||||
|
// ))
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
|
||||||
|
// check_output(output, vec![], vec![(None, "a".to_string()), (None, "b".to_string())], None);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// OPERATOR PRECEDENCE
|
// OPERATOR PRECEDENCE
|
||||||
|
|
||||||
|
|
1916
tests/test_parse.rs
1916
tests/test_parse.rs
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue