mirror of
https://github.com/roc-lang/roc.git
synced 2025-07-13 09:35:01 +00:00
454 lines
13 KiB
Rust
454 lines
13 KiB
Rust
/// Simple tests for parsing expressions, module headers, module defs, etc.
|
|
/// Note, much more extensive tests are in the `test_syntax` crate (in test_snapshots).
|
|
|
|
#[macro_use]
|
|
extern crate pretty_assertions;
|
|
#[macro_use]
|
|
extern crate indoc;
|
|
extern crate bumpalo;
|
|
extern crate quickcheck;
|
|
|
|
#[macro_use(quickcheck)]
|
|
extern crate quickcheck_macros;
|
|
|
|
extern crate roc_module;
|
|
extern crate roc_parse;
|
|
|
|
#[cfg(test)]
|
|
mod test_parse {
|
|
use bumpalo::collections::vec::Vec;
|
|
use bumpalo::{self, Bump};
|
|
use roc_parse::ast::Expr::{self, *};
|
|
use roc_parse::ast::StrSegment::*;
|
|
use roc_parse::ast::{self, EscapedChar};
|
|
use roc_parse::ast::{CommentOrNewline, StrLiteral::*};
|
|
use roc_parse::header::parse_module_defs;
|
|
use roc_parse::parser::SyntaxError;
|
|
use roc_parse::state::State;
|
|
use roc_parse::test_helpers::parse_expr_with;
|
|
use roc_region::all::{Loc, Region};
|
|
use std::{f64, i64};
|
|
|
|
fn assert_parses_to<'a>(input: &'a str, expected_expr: Expr<'a>) {
|
|
let arena = Bump::new();
|
|
let actual = parse_expr_with(&arena, input.trim());
|
|
assert_eq!(Ok(expected_expr), actual);
|
|
}
|
|
|
|
fn assert_parsing_fails(input: &str, _reason: SyntaxError) {
|
|
let arena = Bump::new();
|
|
let actual = parse_expr_with(&arena, input);
|
|
|
|
assert!(actual.is_err());
|
|
}
|
|
|
|
fn assert_segments<E: Fn(&Bump) -> Vec<'_, ast::StrSegment<'_>>>(input: &str, to_expected: E) {
|
|
let arena = Bump::new();
|
|
let actual = parse_expr_with(&arena, arena.alloc(input));
|
|
let expected_slice = to_expected(&arena);
|
|
let expected_expr = Expr::Str(Line(&expected_slice));
|
|
|
|
assert_eq!(Ok(expected_expr), actual);
|
|
}
|
|
|
|
fn parses_with_escaped_char<
|
|
I: Fn(&str) -> String,
|
|
E: Fn(EscapedChar, &Bump) -> Vec<'_, ast::StrSegment<'_>>,
|
|
>(
|
|
to_input: I,
|
|
to_expected: E,
|
|
) {
|
|
let arena = Bump::new();
|
|
|
|
// Try parsing with each of the escaped chars Roc supports
|
|
for (string, escaped) in &[
|
|
("\\\\", EscapedChar::Backslash),
|
|
("\\n", EscapedChar::Newline),
|
|
("\\r", EscapedChar::CarriageReturn),
|
|
("\\t", EscapedChar::Tab),
|
|
("\\\"", EscapedChar::DoubleQuote),
|
|
("\\\'", EscapedChar::SingleQuote),
|
|
("\\$", EscapedChar::Dollar),
|
|
] {
|
|
let actual = parse_expr_with(&arena, arena.alloc(to_input(string)));
|
|
let expected_slice = to_expected(*escaped, &arena);
|
|
let expected_expr = Expr::Str(Line(&expected_slice));
|
|
|
|
assert_eq!(Ok(expected_expr), actual);
|
|
}
|
|
}
|
|
|
|
// BACKSLASH ESCAPES
|
|
|
|
#[test]
|
|
fn string_with_escaped_char_at_end() {
|
|
parses_with_escaped_char(
|
|
|esc| format!(r#""abcd{esc}""#),
|
|
|esc, arena| bumpalo::vec![in arena; Plaintext("abcd"), EscapedChar(esc)],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_escaped_char_in_front() {
|
|
parses_with_escaped_char(
|
|
|esc| format!(r#""{esc}abcd""#),
|
|
|esc, arena| bumpalo::vec![in arena; EscapedChar(esc), Plaintext("abcd")],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_escaped_char_in_middle() {
|
|
parses_with_escaped_char(
|
|
|esc| format!(r#""ab{esc}cd""#),
|
|
|esc, arena| bumpalo::vec![in arena; Plaintext("ab"), EscapedChar(esc), Plaintext("cd")],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_multiple_escaped_chars() {
|
|
parses_with_escaped_char(
|
|
|esc| format!(r#""{esc}abc{esc}de{esc}fghi{esc}""#),
|
|
|esc, arena| bumpalo::vec![in arena; EscapedChar(esc), Plaintext("abc"), EscapedChar(esc), Plaintext("de"), EscapedChar(esc), Plaintext("fghi"), EscapedChar(esc)],
|
|
);
|
|
}
|
|
|
|
// UNICODE ESCAPES
|
|
|
|
#[test]
|
|
fn unicode_escape_in_middle() {
|
|
assert_segments(r#""Hi, \u(123)!""#, |arena| {
|
|
bumpalo::vec![in arena;
|
|
Plaintext("Hi, "),
|
|
Unicode(Loc::new(8, 11, "123")),
|
|
Plaintext("!")
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_escape_in_front() {
|
|
assert_segments(r#""\u(1234) is a unicode char""#, |arena| {
|
|
bumpalo::vec![in arena;
|
|
Unicode(Loc::new(4, 8, "1234")),
|
|
Plaintext(" is a unicode char")
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_escape_in_back() {
|
|
assert_segments(r#""this is unicode: \u(1)""#, |arena| {
|
|
bumpalo::vec![in arena;
|
|
Plaintext("this is unicode: "),
|
|
Unicode(Loc::new(21, 22, "1"))
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_escape_multiple() {
|
|
assert_segments(r#""\u(a1) this is \u(2Bcd) unicode \u(ef97)""#, |arena| {
|
|
bumpalo::vec![in arena;
|
|
Unicode(Loc::new(4, 6, "a1")),
|
|
Plaintext(" this is "),
|
|
Unicode(Loc::new(19, 23, "2Bcd")),
|
|
Plaintext(" unicode "),
|
|
Unicode(Loc::new(36, 40, "ef97"))
|
|
]
|
|
});
|
|
}
|
|
|
|
// INTERPOLATION
|
|
|
|
#[test]
|
|
fn escaped_interpolation() {
|
|
assert_segments(r#""Hi, \${name}!""#, |arena| {
|
|
bumpalo::vec![in arena;
|
|
Plaintext("Hi, "),
|
|
EscapedChar(EscapedChar::Dollar),
|
|
Plaintext("{name}!"),
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_old_interpolation_still_works_for_now() {
|
|
assert_segments(r#""Hi, $(name)!""#, |arena| {
|
|
let expr = arena.alloc(Var {
|
|
module_name: "",
|
|
ident: "name",
|
|
});
|
|
|
|
bumpalo::vec![in arena;
|
|
Plaintext("Hi, "),
|
|
Interpolated(Loc::new(7, 11, expr)),
|
|
Plaintext("!")
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_mixed_new_and_old_interpolation_braces_fails() {
|
|
assert_parsing_fails(r#""${foo)""#, SyntaxError::Unexpected(Region::zero()));
|
|
assert_parsing_fails(r#""$(foo}""#, SyntaxError::Unexpected(Region::zero()));
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_interpolation_in_middle() {
|
|
assert_segments(r#""Hi, ${name}!""#, |arena| {
|
|
let expr = arena.alloc(Var {
|
|
module_name: "",
|
|
ident: "name",
|
|
});
|
|
|
|
bumpalo::vec![in arena;
|
|
Plaintext("Hi, "),
|
|
Interpolated(Loc::new(7, 11, expr)),
|
|
Plaintext("!")
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_interpolation_in_front() {
|
|
assert_segments(r#""${name}, hi!""#, |arena| {
|
|
let expr = arena.alloc(Var {
|
|
module_name: "",
|
|
ident: "name",
|
|
});
|
|
|
|
bumpalo::vec![in arena;
|
|
Interpolated(Loc::new(3, 7, expr)),
|
|
Plaintext(", hi!")
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn string_of_just_dollar_sign() {
|
|
let arena = Bump::new();
|
|
|
|
assert_eq!(
|
|
Ok(Expr::Str(PlainLine("$"))),
|
|
parse_expr_with(&arena, arena.alloc(r#""$""#))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn string_beginning_with_dollar() {
|
|
let arena = Bump::new();
|
|
|
|
assert_eq!(
|
|
Ok(Expr::Str(PlainLine("$foo"))),
|
|
parse_expr_with(&arena, arena.alloc(r#""$foo""#))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn string_ending_with_dollar() {
|
|
let arena = Bump::new();
|
|
|
|
assert_eq!(
|
|
Ok(Expr::Str(PlainLine("foo$"))),
|
|
parse_expr_with(&arena, arena.alloc(r#""foo$""#))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_interpolation_in_back() {
|
|
assert_segments(r#""Hello ${name}""#, |arena| {
|
|
let expr = arena.alloc(Var {
|
|
module_name: "",
|
|
ident: "name",
|
|
});
|
|
|
|
bumpalo::vec![in arena;
|
|
Plaintext("Hello "),
|
|
Interpolated(Loc::new(9, 13, expr))
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_multiple_interpolations() {
|
|
assert_segments(r#""Hi, ${name}! How is ${project} going?""#, |arena| {
|
|
let expr1 = arena.alloc(Var {
|
|
module_name: "",
|
|
ident: "name",
|
|
});
|
|
|
|
let expr2 = arena.alloc(Var {
|
|
module_name: "",
|
|
ident: "project",
|
|
});
|
|
|
|
bumpalo::vec![in arena;
|
|
Plaintext("Hi, "),
|
|
Interpolated(Loc::new(7, 11, expr1)),
|
|
Plaintext("! How is "),
|
|
Interpolated(Loc::new(23, 30, expr2)),
|
|
Plaintext(" going?")
|
|
]
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn string_with_non_interpolation_dollar_signs() {
|
|
assert_segments(
|
|
r#""$a Hi, ${name}! $b How is ${project} going? $c""#,
|
|
|arena| {
|
|
let expr1 = arena.alloc(Var {
|
|
module_name: "",
|
|
ident: "name",
|
|
});
|
|
|
|
let expr2 = arena.alloc(Var {
|
|
module_name: "",
|
|
ident: "project",
|
|
});
|
|
|
|
bumpalo::vec![in arena;
|
|
Plaintext("$a Hi, "),
|
|
Interpolated(Loc::new(10, 14, expr1)),
|
|
Plaintext("! $b How is "),
|
|
Interpolated(Loc::new(29, 36, expr2)),
|
|
Plaintext(" going? $c")
|
|
]
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_source_file() {
|
|
assert_parsing_fails("", SyntaxError::Eof(Region::zero()));
|
|
}
|
|
|
|
#[quickcheck]
|
|
fn all_i64_values_parse(num: i64) {
|
|
assert_parses_to(num.to_string().as_str(), Num(num.to_string().as_str()));
|
|
}
|
|
|
|
#[quickcheck]
|
|
fn all_f64_values_parse(mut num: f64) {
|
|
// NaN, Infinity, -Infinity (these would all parse as tags in Roc)
|
|
if !num.is_finite() {
|
|
num = 0.0;
|
|
}
|
|
|
|
// These can potentially be whole numbers. `Display` omits the decimal point for those,
|
|
// causing them to no longer be parsed as fractional numbers by Roc.
|
|
// Using `Debug` instead of `Display` ensures they always have a decimal point.
|
|
let float_string = format!("{num:?}");
|
|
|
|
assert_parses_to(float_string.as_str(), Float(float_string.as_str()));
|
|
}
|
|
|
|
// SINGLE QUOTE LITERAL
|
|
#[test]
|
|
fn single_quote() {
|
|
assert_parses_to("'b'", Expr::SingleQuote("b"));
|
|
}
|
|
|
|
#[test]
|
|
fn repro_keyword_bug() {
|
|
// Reproducing this bug requires a bizarre set of things to all be true:
|
|
//
|
|
// * Must be parsing a *module* def (nested expr defs don't repro this)
|
|
// * That top-level module def contains a def inside it
|
|
// * That inner def is defining a function
|
|
// * The name of the inner def begins with a keyword (`if`, `then`, `else`, `when`, `is`)
|
|
//
|
|
// If all of these are true, then lookups on that def get skipped over by the parser.
|
|
// If any one of the above is false, then everything works.
|
|
|
|
let arena = Bump::new();
|
|
let src = indoc!(
|
|
r"
|
|
foo = \list ->
|
|
isTest = \_ -> 5
|
|
List.map list isTest
|
|
"
|
|
);
|
|
let actual = parse_module_defs(&arena, State::new(src.as_bytes()), ast::Defs::default());
|
|
|
|
// It should occur twice in the debug output - once for the pattern,
|
|
// and then again for the lookup.
|
|
let occurrences = format!("{actual:?}").split("isTest").count() - 1;
|
|
|
|
assert_eq!(occurrences, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn outdenting_newline_after_else() {
|
|
let arena = &Bump::new();
|
|
|
|
// highlights a problem with the else branch demanding a newline after its expression
|
|
let src = indoc!(
|
|
r"
|
|
main =
|
|
v = \y -> if x then y else z
|
|
|
|
1
|
|
"
|
|
);
|
|
|
|
let state = State::new(src.as_bytes());
|
|
let parsed = parse_module_defs(arena, state, ast::Defs::default());
|
|
match parsed {
|
|
Ok(_) => {
|
|
// eprintln!("{:?}", _state);
|
|
}
|
|
Err(_) => {
|
|
// eprintln!("{:?}, {:?}", _fail, _state);
|
|
panic!("Failed to parse!");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_expr_size() {
|
|
assert_eq!(std::mem::size_of::<roc_parse::ast::Expr>(), 40);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_two_line_comment_with_crlf() {
|
|
let src = "# foo\r\n# bar\r\n42";
|
|
assert_parses_to(
|
|
src,
|
|
Expr::SpaceBefore(
|
|
&Expr::Num("42"),
|
|
&[
|
|
CommentOrNewline::LineComment(" foo"),
|
|
// We used to have a bug where there was an extra CommentOrNewline::Newline between these.
|
|
CommentOrNewline::LineComment(" bar"),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// PARSE ERROR
|
|
|
|
// TODO this should be parse error, but isn't!
|
|
// #[test]
|
|
// fn trailing_paren() {
|
|
// assert_parses_to(
|
|
// indoc!(
|
|
// r#"
|
|
// r = "foo"
|
|
// s = { left : "foo" }
|
|
|
|
// when 0 is
|
|
// 1 -> { x: s.left, y: s.left }
|
|
// 0 -> { x: s.left, y: r }
|
|
// )
|
|
// "#
|
|
// ),
|
|
// Str(PlainLine("")),
|
|
// );
|
|
// }
|
|
|
|
// TODO test for non-ASCII variables
|
|
//
|
|
// TODO verify that when a string literal contains a newline before the
|
|
// closing " it correctly updates both the line *and* column in the State.
|
|
}
|