mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-26 13:29:12 +00:00
Add test_compile crate
This commit is contained in:
parent
98535bfbce
commit
67bca80921
9 changed files with 418 additions and 121 deletions
|
@ -7,14 +7,22 @@ edition.workspace = true
|
|||
license.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
roc_builtins = { path = "../compiler/builtins" }
|
||||
roc_derive = { path = "../compiler/derive", features = [
|
||||
"debug-derived-symbols",
|
||||
] }
|
||||
roc_region = { path = "../compiler/region" }
|
||||
roc_load = { path = "../compiler/load" }
|
||||
roc_parse = { path = "../compiler/parse" }
|
||||
roc_can = { path = "../compiler/can" }
|
||||
roc_problem = { path = "../compiler/problem" }
|
||||
roc_reporting = { path = "../reporting" }
|
||||
roc_target = { path = "../compiler/roc_target" }
|
||||
roc_solve = { path = "../compiler/solve" }
|
||||
test_solve_helpers = { path = "../compiler/test_solve_helpers" }
|
||||
|
||||
bumpalo.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
roc_builtins = { path = "../builtins" }
|
||||
roc_derive = { path = "../derive", features = ["debug-derived-symbols"] }
|
||||
roc_load = { path = "../load" }
|
||||
roc_parse = { path = "../parse" }
|
||||
roc_problem = { path = "../problem" }
|
||||
roc_reporting = { path = "../../reporting" }
|
||||
roc_target = { path = "../roc_target" }
|
||||
roc_solve = { path = "../solve" }
|
||||
test_solve_helpers = { path = "../test_solve_helpers" }
|
||||
pretty_assertions.workspace = true
|
||||
|
|
222
crates/test_compile/src/deindent.rs
Normal file
222
crates/test_compile/src/deindent.rs
Normal file
|
@ -0,0 +1,222 @@
|
|||
use bumpalo::Bump;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// The purpose of this function is to let us run tests like this:
|
||||
///
|
||||
/// run_some_test(r#"
|
||||
/// x = 1
|
||||
///
|
||||
/// x
|
||||
/// ")
|
||||
///
|
||||
/// ...without needing to call a macro like `indoc!` to deal with the fact that
|
||||
/// multiline Rust string literals preserve all the indented spaces.
|
||||
///
|
||||
/// This function removes the indentation by removing leading newlines (e.g. after
|
||||
/// the `(r#"` opening) and then counting how many spaces precede the first line
|
||||
/// (e.g. `" x = 1"` here) and trimming that many spaces from the beginning
|
||||
/// of each subsequent line. The end of the string is then trimmed normally, and
|
||||
/// any remaining empty lines are left empty.
|
||||
///
|
||||
/// This function is a no-op on single-line strings.
|
||||
pub fn trim_and_deindent<'a>(arena: &'a Bump, input: &'a str) -> &'a str {
|
||||
let newline_count = input.chars().filter(|&ch| ch == '\n').count();
|
||||
|
||||
// If it's a single-line string, return it without allocating anything.
|
||||
if newline_count == 0 {
|
||||
return input.trim(); // Trim to remove spaces
|
||||
}
|
||||
|
||||
// Trim leading blank lines - we expect at least one, because the opening line will be `(r#"`
|
||||
// (Also, there may be stray blank lines at the start, which this will trim off too.)
|
||||
let mut lines = bumpalo::collections::Vec::with_capacity_in(newline_count + 1, arena);
|
||||
|
||||
for line in input
|
||||
.lines()
|
||||
// Keep skipping until we hit a line that is neither empty nor all spaces.
|
||||
.skip_while(|line| line.chars().all(|ch| ch == ' '))
|
||||
{
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
// Drop trailing blank lines
|
||||
while lines
|
||||
.last()
|
||||
.map_or(false, |line| line.chars().all(|ch| ch == ' '))
|
||||
{
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
// Now that we've trimmed leading and trailing blank lines,
|
||||
// Find the smallest indent of the remaining lines. That's our indentation amount.
|
||||
let smallest_indent = lines
|
||||
.iter()
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(|line| line.chars().take_while(|&ch| ch == ' ').count())
|
||||
.min()
|
||||
.unwrap_or(0);
|
||||
|
||||
// Remove this amount of indentation from each line.
|
||||
let mut final_str_len = 0;
|
||||
|
||||
lines.iter_mut().for_each(|line| {
|
||||
if line.starts_with(" ") {
|
||||
*line = line.get(smallest_indent..).unwrap_or("");
|
||||
}
|
||||
final_str_len += line.len() + 1; // +1 for the newline that will be added to the end of this line.
|
||||
});
|
||||
|
||||
// Convert lines into a bumpalo::String
|
||||
let mut answer = bumpalo::collections::String::with_capacity_in(final_str_len, arena);
|
||||
|
||||
// Unconditionally push a newline after each line we add. We'll trim off the last one before we return.
|
||||
for line in lines {
|
||||
answer.push_str(line);
|
||||
answer.push('\n');
|
||||
}
|
||||
|
||||
// Trim off the extra newline we added at the end. (Saturate to 0 if we ended up with no lines.)
|
||||
&answer.into_bump_str()[..final_str_len.saturating_sub(1)]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_single_line_input() {
|
||||
let input = "single line";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), "single line");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline_with_indentation() {
|
||||
let input = r#"
|
||||
x = 1
|
||||
|
||||
x
|
||||
"#;
|
||||
let expected = "x = 1\n\nx";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline_with_varying_indentation() {
|
||||
let input = r#"
|
||||
x = 1
|
||||
y = 2
|
||||
z = 3
|
||||
"#;
|
||||
let expected = "x = 1\n y = 2\nz = 3";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline_with_empty_lines() {
|
||||
let input = r#"
|
||||
x = 1
|
||||
|
||||
y = 2
|
||||
|
||||
z = 3
|
||||
"#;
|
||||
let expected = "x = 1\n\ny = 2\n\nz = 3";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_without_leading_newline() {
|
||||
let input = " x = 1\n y = 2";
|
||||
let expected = "x = 1\ny = 2";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_multiple_leading_newlines() {
|
||||
let input = "\n\n\n x = 1\n y = 2";
|
||||
let expected = "x = 1\ny = 2";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_mixed_indentation() {
|
||||
let input = r#"
|
||||
x = 1
|
||||
y = 2
|
||||
z = 3
|
||||
"#;
|
||||
let expected = " x = 1\ny = 2\n z = 3";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_only_spaces() {
|
||||
let input = " ";
|
||||
let expected = "";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_only_newlines() {
|
||||
let input = "\n\n\n";
|
||||
let expected = "";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_tabs() {
|
||||
let input = "\t\tx = 1\n\t\ty = 2";
|
||||
let expected = "\t\tx = 1\n\t\ty = 2";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_mixed_spaces_and_tabs() {
|
||||
let input = " \tx = 1\n \ty = 2";
|
||||
let expected = "\tx = 1\n\ty = 2";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_trailing_spaces() {
|
||||
let input = " x = 1 \n y = 2 ";
|
||||
let expected = "x = 1 \ny = 2 ";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_empty_lines_and_spaces() {
|
||||
let input = " x = 1\n \n y = 2";
|
||||
let expected = "x = 1\n\ny = 2";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_different_indentation_levels() {
|
||||
let input = " x = 1\n y = 2\n z = 3";
|
||||
let expected = " x = 1\n y = 2\nz = 3";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_non_space_characters_at_start() {
|
||||
let input = "x = 1\n y = 2\n z = 3";
|
||||
let expected = "x = 1\n y = 2\n z = 3";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_input() {
|
||||
let input = "";
|
||||
let expected = "";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_only_one_indented_line() {
|
||||
let input = " x = 1";
|
||||
let expected = "x = 1";
|
||||
assert_eq!(trim_and_deindent(&Bump::new(), input), expected);
|
||||
}
|
||||
}
|
106
crates/test_compile/src/help_can.rs
Normal file
106
crates/test_compile/src/help_can.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use crate::help_parse::ParseExpr;
|
||||
use bumpalo::Bump;
|
||||
use roc_can::expr::Expr;
|
||||
|
||||
pub struct CanExpr {
|
||||
parse_expr: ParseExpr,
|
||||
}
|
||||
|
||||
impl Default for CanExpr {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
parse_expr: ParseExpr::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanExpr {
|
||||
pub fn can_expr<'a>(&'a self, input: &'a str) -> Result<Expr, CanExprProblem> {
|
||||
match self.parse_expr.parse_expr(input) {
|
||||
Ok(ast) => {
|
||||
// todo canonicalize AST and return that result.
|
||||
let loc_expr = roc_parse::test_helpers::parse_loc_with(arena, expr_str).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"can_expr_with() got a parse error when attempting to canonicalize:\n\n{expr_str:?} {e:?}"
|
||||
)
|
||||
});
|
||||
|
||||
let mut var_store = VarStore::default();
|
||||
let var = var_store.fresh();
|
||||
let qualified_module_ids = PackageModuleIds::default();
|
||||
|
||||
let mut scope = Scope::new(
|
||||
home,
|
||||
"TestPath".into(),
|
||||
IdentIds::default(),
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
let dep_idents = IdentIds::exposed_builtins(0);
|
||||
let mut env = Env::new(
|
||||
arena,
|
||||
expr_str,
|
||||
home,
|
||||
Path::new("Test.roc"),
|
||||
&dep_idents,
|
||||
&qualified_module_ids,
|
||||
None,
|
||||
);
|
||||
|
||||
// Desugar operators (convert them to Apply calls, taking into account
|
||||
// operator precedence and associativity rules), before doing other canonicalization.
|
||||
//
|
||||
// If we did this *during* canonicalization, then each time we
|
||||
// visited a BinOp node we'd recursively try to apply this to each of its nested
|
||||
// operators, and then again on *their* nested operators, ultimately applying the
|
||||
// rules multiple times unnecessarily.
|
||||
let loc_expr = desugar::desugar_expr(&mut env, &mut scope, &loc_expr);
|
||||
|
||||
scope.add_alias(
|
||||
Symbol::NUM_INT,
|
||||
Region::zero(),
|
||||
vec![Loc::at_zero(AliasVar::unbound(
|
||||
"a".into(),
|
||||
Variable::EMPTY_RECORD,
|
||||
))],
|
||||
vec![],
|
||||
Type::EmptyRec,
|
||||
roc_types::types::AliasKind::Structural,
|
||||
);
|
||||
|
||||
let (loc_expr, output) = canonicalize_expr(
|
||||
&mut env,
|
||||
&mut var_store,
|
||||
&mut scope,
|
||||
Region::zero(),
|
||||
&loc_expr.value,
|
||||
);
|
||||
|
||||
let mut all_ident_ids = IdentIds::exposed_builtins(1);
|
||||
all_ident_ids.insert(home, scope.locals.ident_ids);
|
||||
|
||||
let interns = Interns {
|
||||
module_ids: env.qualified_module_ids.clone().into_module_ids(),
|
||||
all_ident_ids,
|
||||
};
|
||||
|
||||
CanExprOut {
|
||||
loc_expr,
|
||||
output,
|
||||
problems: env.problems,
|
||||
home: env.home,
|
||||
var_store,
|
||||
interns,
|
||||
var,
|
||||
}
|
||||
}
|
||||
Err(syntax_error) => {
|
||||
// todo panic due to unexpected syntax error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_arena(self) -> Bump {
|
||||
self.parse_expr.into_arena()
|
||||
}
|
||||
}
|
|
@ -1,20 +1,60 @@
|
|||
use bumpalo::Bump;
|
||||
use roc_parse::{
|
||||
ast,
|
||||
blankspace::space0_before_optional_after,
|
||||
expr::{expr_end, loc_expr_block},
|
||||
parser::{skip_second, EExpr, Parser, SourceError, SyntaxError},
|
||||
state::State,
|
||||
};
|
||||
use roc_region::all::{Loc, Position};
|
||||
|
||||
use crate::deindent::trim_and_deindent;
|
||||
|
||||
pub struct ParseExpr {
|
||||
arena: Box<Bump>,
|
||||
ast: Box<Ast>,
|
||||
arena: Bump,
|
||||
}
|
||||
|
||||
impl Default for ParseExpr {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
arena: Bump::with_capacity(4096),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseExpr {
|
||||
pub fn parse(&str) -> Self {
|
||||
let mut arena = Bump::new();
|
||||
let ast = parse(arena, without_indent(str));
|
||||
pub fn parse_expr<'a>(&'a self, input: &'a str) -> Result<ast::Expr<'a>, SyntaxError<'a>> {
|
||||
self.parse_loc_expr(input)
|
||||
.map(|loc_expr| loc_expr.value)
|
||||
.map_err(|e| e.problem)
|
||||
}
|
||||
|
||||
Self {
|
||||
arena,
|
||||
ast,
|
||||
pub fn parse_loc_expr<'a>(
|
||||
&'a self,
|
||||
input: &'a str,
|
||||
) -> Result<Loc<ast::Expr<'a>>, SourceError<'a, SyntaxError<'a>>> {
|
||||
let original_bytes = trim_and_deindent(&self.arena, input).as_bytes();
|
||||
let state = State::new(original_bytes);
|
||||
|
||||
let parser = skip_second(
|
||||
space0_before_optional_after(
|
||||
loc_expr_block(true),
|
||||
EExpr::IndentStart,
|
||||
EExpr::IndentEnd,
|
||||
),
|
||||
expr_end(),
|
||||
);
|
||||
|
||||
match parser.parse(&self.arena, state, 0) {
|
||||
Ok((_, loc_expr, _)) => Ok(loc_expr),
|
||||
Err((_, fail)) => Err(SourceError {
|
||||
problem: SyntaxError::Expr(fail, Position::default()),
|
||||
bytes: original_bytes,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ast(&self) -> &Ast {
|
||||
self.ast
|
||||
pub fn into_arena(self) -> Bump {
|
||||
self.arena
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
mod deindent;
|
||||
mod help_can;
|
||||
mod help_parse;
|
||||
mod without_indent;
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
/// The purpose of this function is to let us run tests like this:
|
||||
///
|
||||
/// run_some_test(r#"
|
||||
/// x = 1
|
||||
///
|
||||
/// x
|
||||
/// ")
|
||||
///
|
||||
/// ...without needing to call a macro like `indoc!` to deal with the fact that
|
||||
/// multiline Rust string literals preserve all the indented spaces. This takes out
|
||||
/// the indentation as well as the leading newline in examples like the above, and it's
|
||||
/// a no-op on single-line strings.
|
||||
pub fn without_indent(input: &str) -> &str {
|
||||
// Ignore any leading newlines, which we expect because the opening line will be `(r#"`
|
||||
let input = input.trim_start_matches('\n');
|
||||
let leading_spaces = input.chars().take_while(|&ch| ch == ' ').count();
|
||||
|
||||
input
|
||||
.lines()
|
||||
.map(|line| {
|
||||
if line.starts_with(" ") {
|
||||
line.get(leading_spaces..).unwrap_or("")
|
||||
} else {
|
||||
line
|
||||
}
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_single_line_input() {
|
||||
let input = "single line";
|
||||
assert_eq!(without_indent(input), "single line");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline_with_indentation() {
|
||||
let input = r#"
|
||||
x = 1
|
||||
|
||||
x
|
||||
"#;
|
||||
let expected = "x = 1\n\nx";
|
||||
assert_eq!(without_indent(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline_with_varying_indentation() {
|
||||
let input = r#"
|
||||
x = 1
|
||||
y = 2
|
||||
z = 3
|
||||
"#;
|
||||
let expected = "x = 1\n y = 2\nz = 3";
|
||||
assert_eq!(without_indent(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline_with_empty_lines() {
|
||||
let input = r#"
|
||||
x = 1
|
||||
|
||||
y = 2
|
||||
|
||||
z = 3
|
||||
"#;
|
||||
let expected = "x = 1\n\ny = 2\n\nz = 3";
|
||||
assert_eq!(without_indent(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_without_leading_newline() {
|
||||
let input = " x = 1\n y = 2";
|
||||
let expected = "x = 1\ny = 2";
|
||||
assert_eq!(without_indent(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_multiple_leading_newlines() {
|
||||
let input = "\n\n\n x = 1\n y = 2";
|
||||
let expected = "x = 1\ny = 2";
|
||||
assert_eq!(without_indent(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_with_mixed_indentation() {
|
||||
let input = r#"
|
||||
x = 1
|
||||
y = 2
|
||||
z = 3
|
||||
"#;
|
||||
let expected = "x = 1\ny = 2\n z = 3";
|
||||
assert_eq!(without_indent(input), expected);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue