Extend test_syntax to check that canonicalization doesn't panic on the input

Importantly, this would have caught the issue discovered in #7279, prior to that landing on main.
This commit is contained in:
Joshua Warner 2024-12-04 17:55:43 -08:00
parent e65460b0bf
commit b4fa7294c7
No known key found for this signature in database
GPG key ID: 89AD497003F93FDD
387 changed files with 559 additions and 393 deletions

View file

@ -1,14 +1,31 @@
use std::path::Path;
use bumpalo::Bump;
use roc_can::desugar;
use roc_can::env::Env;
use roc_can::expr::canonicalize_expr;
use roc_can::scope::Scope;
use roc_error_macros::set_panic_not_exit;
use roc_fmt::{annotation::Formattable, header::fmt_header, MigrationFlags};
use roc_module::symbol::{IdentIds, Interns, ModuleIds, PackageModuleIds, Symbol};
use roc_parse::header::parse_module_defs;
use roc_parse::parser::Parser;
use roc_parse::parser::SyntaxError;
use roc_parse::state::State;
use roc_parse::test_helpers::parse_loc_with;
use roc_parse::{ast::Malformed, normalize::Normalize};
use roc_parse::{
ast::{Defs, Expr, FullAst, Header, Malformed, SpacesBefore},
header::parse_module_defs,
normalize::Normalize,
parser::{Parser, SyntaxError},
state::State,
test_helpers::{parse_defs_with, parse_expr_with, parse_header_with},
ast::{Defs, Expr, FullAst, Header, SpacesBefore},
test_helpers::{parse_defs_with, parse_header_with},
};
// use roc_problem::can::Problem;
use roc_region::all::Loc;
use roc_region::all::Region;
use roc_test_utils::assert_multiline_str_eq;
use roc_types::{
subs::{VarStore, Variable},
types::{AliasVar, Type},
};
use roc_fmt::Buf;
@ -74,7 +91,7 @@ pub enum Output<'a> {
ModuleDefs(Defs<'a>),
Expr(Expr<'a>),
Expr(Loc<Expr<'a>>),
Full(FullAst<'a>),
}
@ -116,6 +133,81 @@ impl<'a> Output<'a> {
Output::Full { .. } => format!("{self:#?}\n"),
}
}
pub fn canonicalize(&self, arena: &Bump, src: &str) {
set_panic_not_exit(true); // can has a bunch of internal_error! calls
match self {
Output::Header(_) => {}
Output::ModuleDefs(_) => {
// TODO: canonicalize module defs
}
Output::Full(_) => {
// TODO: canonicalize full ast
}
Output::Expr(loc_expr) => {
let mut var_store = VarStore::default();
let qualified_module_ids = PackageModuleIds::default();
let home = ModuleIds::default().get_or_insert(&"Test".into());
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,
src,
home,
Path::new("Test.roc"),
&dep_idents,
&qualified_module_ids,
None,
roc_can::env::FxMode::PurityInference,
);
// 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,
};
}
}
}
}
impl<'a> Malformed for Output<'a> {
@ -163,7 +255,7 @@ impl<'a> Input<'a> {
}
Input::Expr(input) => {
let expr = parse_expr_with(arena, input)?;
let expr = parse_loc_with(arena, input).map_err(|e| e.problem)?;
Ok(Output::Expr(expr))
}
@ -197,6 +289,7 @@ impl<'a> Input<'a> {
&self,
handle_formatted_output: impl Fn(Input),
check_idempotency: bool,
canonicalize_mode: Option<bool>,
) {
let arena = Bump::new();
@ -259,5 +352,27 @@ impl<'a> Input<'a> {
assert_multiline_str_eq!(output.as_ref().as_str(), reformatted.as_ref().as_str());
}
}
if let Some(expect_panic) = canonicalize_mode {
if expect_panic {
let text = self.as_str();
let res = std::panic::catch_unwind(|| {
let new_arena = Bump::new();
actual.canonicalize(&new_arena, text);
});
assert!(
res.is_err(),
"Canonicalize was expected to panic, but it did not. \
If you're running test_snapshots, you may need to remove this test from \
the list of tests that are expected to panic (great!), \
in `fn expect_canonicalize_panics`"
);
} else {
// TODO grab the output here and assert things about it.
// For now we just make sure that it doesn't crash on this input
actual.canonicalize(&arena, self.as_str());
}
}
}
}