Add test_compile crate

This commit is contained in:
Richard Feldman 2024-10-20 11:52:46 -04:00
parent 98535bfbce
commit 67bca80921
No known key found for this signature in database
GPG key ID: 5DE4EE30BB738EDF
9 changed files with 418 additions and 121 deletions

19
Cargo.lock generated
View file

@ -3803,6 +3803,25 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "test_compile"
version = "0.0.1"
dependencies = [
"bumpalo",
"pretty_assertions",
"roc_builtins",
"roc_can",
"roc_derive",
"roc_load",
"roc_parse",
"roc_problem",
"roc_region",
"roc_reporting",
"roc_solve",
"roc_target",
"test_solve_helpers",
]
[[package]] [[package]]
name = "test_derive" name = "test_derive"
version = "0.0.1" version = "0.0.1"

View file

@ -17,6 +17,7 @@ members = [
"crates/repl_wasm", "crates/repl_wasm",
"crates/repl_expect", "crates/repl_expect",
"crates/roc_std", "crates/roc_std",
"crates/test_compile",
"crates/test_utils", "crates/test_utils",
"crates/test_utils_dir", "crates/test_utils_dir",
"crates/valgrind", "crates/valgrind",

View file

@ -35,7 +35,7 @@ use roc_region::all::{Loc, Position, Region};
use crate::parser::Progress::{self, *}; use crate::parser::Progress::{self, *};
fn expr_end<'a>() -> impl Parser<'a, (), EExpr<'a>> { pub fn expr_end<'a>() -> impl Parser<'a, (), EExpr<'a>> {
|_arena, state: State<'a>, _min_indent: u32| { |_arena, state: State<'a>, _min_indent: u32| {
if state.has_reached_end() { if state.has_reached_end() {
Ok((NoProgress, (), state)) Ok((NoProgress, (), state))

View file

@ -7,14 +7,22 @@ edition.workspace = true
license.workspace = true license.workspace = true
version.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] [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 pretty_assertions.workspace = true

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

View 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()
}
}

View file

@ -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 { pub struct ParseExpr {
arena: Box<Bump>, arena: Bump,
ast: Box<Ast>, }
impl Default for ParseExpr {
fn default() -> Self {
Self {
arena: Bump::with_capacity(4096),
}
}
} }
impl ParseExpr { impl ParseExpr {
pub fn parse(&str) -> Self { pub fn parse_expr<'a>(&'a self, input: &'a str) -> Result<ast::Expr<'a>, SyntaxError<'a>> {
let mut arena = Bump::new(); self.parse_loc_expr(input)
let ast = parse(arena, without_indent(str)); .map(|loc_expr| loc_expr.value)
.map_err(|e| e.problem)
}
Self { pub fn parse_loc_expr<'a>(
arena, &'a self,
ast, 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 { pub fn into_arena(self) -> Bump {
self.ast self.arena
} }
} }

View file

@ -1,2 +1,3 @@
mod deindent;
mod help_can;
mod help_parse; mod help_parse;
mod without_indent;

View file

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