diff --git a/crates/compiler/load/tests/test_reporting.rs b/crates/compiler/load/tests/test_reporting.rs index e98eae796e..a349f25252 100644 --- a/crates/compiler/load/tests/test_reporting.rs +++ b/crates/compiler/load/tests/test_reporting.rs @@ -14547,11 +14547,11 @@ All branches in an `if` must have the same type! leftover_statement, indoc!( r#" - app [main] { pf: platform "../../../../../examples/cli/effects-platform/main.roc" } + app [main!] { pf: platform "../../../../../examples/cli/effects-platform/main.roc" } import pf.Effect - main = \{} -> + main! = \{} -> identity {} Effect.putLine! "hello" @@ -14689,4 +14689,66 @@ All branches in an `if` must have the same type! Num * "### ); + + test_report!( + function_def_fx_no_bang, + indoc!( + r#" + app [main!] { pf: platform "../../../../../examples/cli/effects-platform/main.roc" } + + import pf.Effect + + main! = \{} -> + printHello {} + + printHello = \{} -> + Effect.putLine! "hello" + "# + ), + @r###" + ── MISSING EXCLAMATION in /code/proj/Main.roc ────────────────────────────────── + + This function is effectful, but its name does not indicate so: + + 8│ printHello = \{} -> + ^^^^^^^^^^ + + Add an exclamation mark at the end of its name, like: + + printHello! + + This will help readers identify it as a source of effects. + "### + ); + + test_report!( + nested_function_def_fx_no_bang, + indoc!( + r#" + app [main!] { pf: platform "../../../../../examples/cli/effects-platform/main.roc" } + + import pf.Effect + + main! = \{} -> + printHello = \{} -> + Effect.putLine! "hello" + + printHello {} + "# + ), + @r###" + ── MISSING EXCLAMATION in /code/proj/Main.roc ────────────────────────────────── + + This function is effectful, but its name does not indicate so: + + 6│ printHello = \{} -> + ^^^^^^^^^^ + + Add an exclamation mark at the end of its name, like: + + printHello! + + This will help readers identify it as a source of effects. + "### + ); } diff --git a/crates/compiler/lower_params/src/type_error.rs b/crates/compiler/lower_params/src/type_error.rs index e0d2aea405..18ddc62be2 100644 --- a/crates/compiler/lower_params/src/type_error.rs +++ b/crates/compiler/lower_params/src/type_error.rs @@ -100,7 +100,8 @@ pub fn remove_module_param_arguments( | TypeError::UnexpectedModuleParams(_, _) | TypeError::MissingModuleParams(_, _, _) | TypeError::ModuleParamsMismatch(_, _, _, _) - | TypeError::PureStmt(_) => {} + | TypeError::PureStmt(_) + | TypeError::UnsuffixedEffectfulFunction(_, _) => {} } } } diff --git a/crates/compiler/solve/src/solve.rs b/crates/compiler/solve/src/solve.rs index 05152c5cf4..95cdc9fd1a 100644 --- a/crates/compiler/solve/src/solve.rs +++ b/crates/compiler/solve/src/solve.rs @@ -22,6 +22,7 @@ use roc_debug_flags::dbg_do; #[cfg(debug_assertions)] use roc_debug_flags::ROC_VERIFY_RIGID_LET_GENERALIZED; use roc_error_macros::internal_error; +use roc_module::ident::IdentSuffix; use roc_module::symbol::{ModuleId, Symbol}; use roc_problem::can::CycleEntry; use roc_region::all::Loc; @@ -448,6 +449,8 @@ fn solve( ); new_scope.insert_symbol_var_if_vacant(*symbol, loc_var.value); + + check_symbol_suffix(env, problems, *symbol, *loc_var); } // Note that this vars_by_symbol is the one returned by the @@ -1520,6 +1523,31 @@ fn solve( state } +fn check_symbol_suffix( + env: &mut InferenceEnv<'_>, + problems: &mut Vec, + symbol: Symbol, + loc_var: Loc, +) { + match symbol.suffix() { + IdentSuffix::None => { + if let Content::Structure(FlatType::Func(_, _, _, fx)) = + env.subs.get_content_without_compacting(loc_var.value) + { + if let Content::Effectful = env.subs.get_content_without_compacting(*fx) { + problems.push(TypeError::UnsuffixedEffectfulFunction( + loc_var.region, + symbol, + )); + } + } + } + IdentSuffix::Bang => { + // [purity-inference] TODO + } + } +} + fn chase_alias_content(subs: &Subs, mut var: Variable) -> (Variable, &Content) { loop { match subs.get_content_without_compacting(var) { diff --git a/crates/compiler/solve_problem/src/lib.rs b/crates/compiler/solve_problem/src/lib.rs index 4e49041a1d..49c6bcb389 100644 --- a/crates/compiler/solve_problem/src/lib.rs +++ b/crates/compiler/solve_problem/src/lib.rs @@ -40,6 +40,7 @@ pub enum TypeError { MissingModuleParams(Region, ModuleId, ErrorType), ModuleParamsMismatch(Region, ModuleId, ErrorType, ErrorType), PureStmt(Region), + UnsuffixedEffectfulFunction(Region, Symbol), } impl TypeError { @@ -65,6 +66,7 @@ impl TypeError { TypeError::IngestedFileBadUtf8(..) => Fatal, TypeError::IngestedFileUnsupportedType(..) => Fatal, TypeError::PureStmt(..) => Warning, + TypeError::UnsuffixedEffectfulFunction(_, _) => Warning, } } @@ -81,7 +83,8 @@ impl TypeError { | TypeError::UnexpectedModuleParams(region, ..) | TypeError::MissingModuleParams(region, ..) | TypeError::ModuleParamsMismatch(region, ..) - | TypeError::PureStmt(region) => Some(*region), + | TypeError::PureStmt(region) + | TypeError::UnsuffixedEffectfulFunction(region, _) => Some(*region), TypeError::UnfulfilledAbility(ab, ..) => ab.region(), TypeError::Exhaustive(e) => Some(e.region()), TypeError::CircularDef(c) => c.first().map(|ce| ce.symbol_region), diff --git a/crates/reporting/src/error/type.rs b/crates/reporting/src/error/type.rs index 09bf62bc18..e1584e459b 100644 --- a/crates/reporting/src/error/type.rs +++ b/crates/reporting/src/error/type.rs @@ -330,6 +330,23 @@ pub fn type_problem<'b>( severity, }) } + UnsuffixedEffectfulFunction(region, symbol) => { + let stack = [ + alloc.reflow("This function is effectful, but its name does not indicate so:"), + alloc.region(lines.convert_region(region), severity), + alloc.reflow("Add an exclamation mark at the end of its name, like:"), + alloc + .string(format!("{}!", symbol.as_str(alloc.interns))) + .indent(4), + alloc.reflow("This will help readers identify it as a source of effects."), + ]; + Some(Report { + title: "MISSING EXCLAMATION".to_string(), + filename, + doc: alloc.stack(stack), + severity, + }) + } } }