diff --git a/crates/compiler/can/src/constraint.rs b/crates/compiler/can/src/constraint.rs index 8ac14f35fd..e2787bb4b8 100644 --- a/crates/compiler/can/src/constraint.rs +++ b/crates/compiler/can/src/constraint.rs @@ -635,6 +635,10 @@ impl Constraints { Constraint::FxSuffix(constraint_index) } + pub fn flex_to_pure(&mut self, fx_var: Variable) -> Constraint { + Constraint::FlexToPure(fx_var) + } + pub fn contains_save_the_environment(&self, constraint: &Constraint) -> bool { match constraint { Constraint::SaveTheEnvironment => true, diff --git a/crates/compiler/constrain/src/expr.rs b/crates/compiler/constrain/src/expr.rs index 8c41c9c779..df0295bcd9 100644 --- a/crates/compiler/constrain/src/expr.rs +++ b/crates/compiler/constrain/src/expr.rs @@ -234,6 +234,7 @@ fn constrain_untyped_closure( ret_constraint, Generalizable(true), ), + constraints.and_constraint(pattern_state.delayed_fx_suffix_constraints), constraints.equal_types_with_storage( function_type, expected, @@ -243,7 +244,7 @@ fn constrain_untyped_closure( ), early_returns_constraint, closure_constraint, - Constraint::FlexToPure(fx_var), + constraints.flex_to_pure(fx_var), ]; constraints.exists_many(vars, cons) @@ -2029,6 +2030,7 @@ fn constrain_function_def( vars: Vec::with_capacity(function_def.arguments.len()), constraints: Vec::with_capacity(1), delayed_is_open_constraints: vec![], + delayed_fx_suffix_constraints: Vec::with_capacity(function_def.arguments.len()), }; let mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); let closure_var = function_def.closure_type; @@ -2171,9 +2173,12 @@ fn constrain_function_def( Category::Lambda, region, ), + // Check argument suffixes against usage + constraints.and_constraint(argument_pattern_state.delayed_fx_suffix_constraints), // Finally put the solved closure type into the dedicated def expr variable. constraints.store(signature_index, expr_var, std::file!(), std::line!()), closure_constraint, + constraints.flex_to_pure(function_def.fx_type), ]; let expr_con = constraints.exists_many(vars, cons); @@ -2488,6 +2493,7 @@ fn constrain_when_branch_help( vars: Vec::with_capacity(2), constraints: Vec::with_capacity(2), delayed_is_open_constraints: Vec::new(), + delayed_fx_suffix_constraints: Vec::new(), }; for (i, loc_pattern) in when_branch.patterns.iter().enumerate() { @@ -2512,6 +2518,9 @@ fn constrain_when_branch_help( state .delayed_is_open_constraints .extend(partial_state.delayed_is_open_constraints); + state + .delayed_fx_suffix_constraints + .extend(partial_state.delayed_fx_suffix_constraints); if i == 0 { state.headers.extend(partial_state.headers); @@ -2840,6 +2849,7 @@ pub(crate) fn constrain_def_pattern( vars: Vec::with_capacity(1), constraints: Vec::with_capacity(1), delayed_is_open_constraints: vec![], + delayed_fx_suffix_constraints: vec![], }; constrain_pattern( @@ -2949,6 +2959,7 @@ fn constrain_typed_def( vars: Vec::with_capacity(arguments.len()), constraints: Vec::with_capacity(1), delayed_is_open_constraints: vec![], + delayed_fx_suffix_constraints: Vec::with_capacity(arguments.len()), }; let mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); let ret_var = *ret_var; @@ -3032,6 +3043,8 @@ fn constrain_typed_def( // This is a syntactic function, it can be generalized Generalizable(true), ), + // Check argument suffixes against usage + constraints.and_constraint(argument_pattern_state.delayed_fx_suffix_constraints), // Store the inferred ret var into the function type now, so that // when we check that the solved function type matches the annotation, we can // display the fully inferred return variable. @@ -3047,7 +3060,7 @@ fn constrain_typed_def( constraints.store(signature_index, *fn_var, std::file!(), std::line!()), constraints.store(signature_index, expr_var, std::file!(), std::line!()), closure_constraint, - Constraint::FlexToPure(fx_var), + constraints.flex_to_pure(fx_var), ]; let expr_con = constraints.exists_many(vars, cons); @@ -3933,6 +3946,7 @@ fn constraint_recursive_function( vars: Vec::with_capacity(function_def.arguments.len()), constraints: Vec::with_capacity(1), delayed_is_open_constraints: vec![], + delayed_fx_suffix_constraints: Vec::with_capacity(function_def.arguments.len()), }; let mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); let ret_var = function_def.return_type; @@ -4022,13 +4036,15 @@ fn constraint_recursive_function( // Syntactic function can be generalized Generalizable(true), ), + // Check argument suffixes against usage + constraints.and_constraint(argument_pattern_state.delayed_fx_suffix_constraints), constraints.equal_types(fn_type, annotation_expected, Category::Lambda, region), // "fn_var is equal to the closure's type" - fn_var is used in code gen // Store type into AST vars. We use Store so errors aren't reported twice constraints.store(signature_index, expr_var, std::file!(), std::line!()), constraints.store(ret_type_index, ret_var, std::file!(), std::line!()), closure_constraint, - Constraint::FlexToPure(fx_var), + constraints.flex_to_pure(fx_var), ]; let and_constraint = constraints.and_constraint(cons); @@ -4502,6 +4518,7 @@ fn rec_defs_help( vars: Vec::with_capacity(arguments.len()), constraints: Vec::with_capacity(1), delayed_is_open_constraints: vec![], + delayed_fx_suffix_constraints: Vec::with_capacity(arguments.len()), }; let mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); @@ -4578,6 +4595,10 @@ fn rec_defs_help( expr_con, generalizable, ), + // Check argument suffixes against usage + constraints.and_constraint( + argument_pattern_state.delayed_fx_suffix_constraints, + ), constraints.equal_types( fn_type_index, expected_index, @@ -4595,6 +4616,7 @@ fn rec_defs_help( ), constraints.store(ret_type_index, ret_var, std::file!(), std::line!()), closure_constraint, + constraints.flex_to_pure(fx_var), ]; let and_constraint = constraints.and_constraint(cons); diff --git a/crates/compiler/constrain/src/pattern.rs b/crates/compiler/constrain/src/pattern.rs index bc02e188b1..6a5a0e7b31 100644 --- a/crates/compiler/constrain/src/pattern.rs +++ b/crates/compiler/constrain/src/pattern.rs @@ -22,6 +22,7 @@ pub struct PatternState { pub vars: Vec, pub constraints: Vec, pub delayed_is_open_constraints: Vec, + pub delayed_fx_suffix_constraints: Vec, } /// If there is a type annotation, the pattern state headers can be optimized by putting the @@ -247,6 +248,28 @@ pub fn constrain_pattern( region: Region, expected: PExpectedTypeIndex, state: &mut PatternState, +) { + constrain_pattern_help( + types, + constraints, + env, + pattern, + region, + expected, + state, + true, + ); +} + +pub fn constrain_pattern_help( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + pattern: &Pattern, + region: Region, + expected: PExpectedTypeIndex, + state: &mut PatternState, + is_shallow: bool, ) { match pattern { Underscore => { @@ -276,9 +299,13 @@ pub fn constrain_pattern( .push(constraints.is_open_type(type_index)); } - state - .constraints - .push(constraints.fx_pattern_suffix(*symbol, type_index, region)); + if is_shallow { + // Identifiers introduced in nested patterns get let constraints + // and therefore don't need fx_pattern_suffix constraints. + state + .delayed_fx_suffix_constraints + .push(constraints.fx_pattern_suffix(*symbol, type_index, region)); + } state.headers.insert( *symbol, @@ -301,7 +328,7 @@ pub fn constrain_pattern( }, ); - constrain_pattern( + constrain_pattern_help( types, constraints, env, @@ -309,6 +336,7 @@ pub fn constrain_pattern( subpattern.region, expected, state, + false, ) } @@ -534,7 +562,7 @@ pub fn constrain_pattern( )); state.vars.push(*guard_var); - constrain_pattern( + constrain_pattern_help( types, constraints, env, @@ -542,6 +570,7 @@ pub fn constrain_pattern( loc_pattern.region, expected, state, + false, ); pat_type @@ -632,7 +661,7 @@ pub fn constrain_pattern( )); state.vars.push(*guard_var); - constrain_pattern( + constrain_pattern_help( types, constraints, env, @@ -640,6 +669,7 @@ pub fn constrain_pattern( loc_guard.region, expected, state, + false, ); RecordField::Demanded(pat_type) @@ -755,7 +785,7 @@ pub fn constrain_pattern( loc_pat.region, )); - constrain_pattern( + constrain_pattern_help( types, constraints, env, @@ -763,6 +793,7 @@ pub fn constrain_pattern( loc_pat.region, expected, state, + false, ); } @@ -811,7 +842,7 @@ pub fn constrain_pattern( pattern_type, region, )); - constrain_pattern( + constrain_pattern_help( types, constraints, env, @@ -819,6 +850,7 @@ pub fn constrain_pattern( loc_pattern.region, expected, state, + false, ); } @@ -876,7 +908,7 @@ pub fn constrain_pattern( // First, add a constraint for the argument "who" let arg_pattern_expected = constraints .push_pat_expected_type(PExpected::NoExpectation(arg_pattern_type_index)); - constrain_pattern( + constrain_pattern_help( types, constraints, env, @@ -884,6 +916,7 @@ pub fn constrain_pattern( loc_arg_pattern.region, arg_pattern_expected, state, + false, ); // Next, link `whole_var` to the opaque type of "@Id who" diff --git a/crates/compiler/load/tests/test_reporting.rs b/crates/compiler/load/tests/test_reporting.rs index b6918ba0f5..2df423a9cc 100644 --- a/crates/compiler/load/tests/test_reporting.rs +++ b/crates/compiler/load/tests/test_reporting.rs @@ -15275,4 +15275,91 @@ All branches in an `if` must have the same type! Hint: Did you forget to run an effect? Is the type annotation wrong? "### ); + + test_report!( + fx_passed_to_untyped_pure_hof, + indoc!( + r#" + app [main!] { pf: platform "../../../../../examples/cli/effects-platform/main.roc" } + + import pf.Effect + + main! = \{} -> + pureHigherOrder Effect.putLine! "hi" + + pureHigherOrder = \f, x -> f x + "# + ), + @r###" + ── TYPE MISMATCH in /code/proj/Main.roc ──────────────────────────────────────── + + This 1st argument to `pureHigherOrder` has an unexpected type: + + 6│ pureHigherOrder Effect.putLine! "hi" + ^^^^^^^^^^^^^^^ + + This `Effect.putLine!` value is a: + + Str => {} + + But `pureHigherOrder` needs its 1st argument to be: + + Str -> {} + + ── UNNECESSARY EXCLAMATION in /code/proj/Main.roc ────────────────────────────── + + This function is pure, but its name suggests otherwise: + + 5│ main! = \{} -> + ^^^^^ + + The exclamation mark at the end is reserved for effectful functions. + + Hint: Did you forget to run an effect? Is the type annotation wrong? + "### + ); + + test_report!( + fx_passed_to_partially_inferred_pure_hof, + indoc!( + r#" + app [main!] { pf: platform "../../../../../examples/cli/effects-platform/main.roc" } + + import pf.Effect + + main! = \{} -> + pureHigherOrder Effect.putLine! "hi" + + pureHigherOrder : _, _ -> _ + pureHigherOrder = \f, x -> f x + "# + ), + @r###" + ── TYPE MISMATCH in /code/proj/Main.roc ──────────────────────────────────────── + + This 1st argument to `pureHigherOrder` has an unexpected type: + + 6│ pureHigherOrder Effect.putLine! "hi" + ^^^^^^^^^^^^^^^ + + This `Effect.putLine!` value is a: + + Str => {} + + But `pureHigherOrder` needs its 1st argument to be: + + Str -> {} + + ── UNNECESSARY EXCLAMATION in /code/proj/Main.roc ────────────────────────────── + + This function is pure, but its name suggests otherwise: + + 5│ main! = \{} -> + ^^^^^ + + The exclamation mark at the end is reserved for effectful functions. + + Hint: Did you forget to run an effect? Is the type annotation wrong? + "### + ); } diff --git a/crates/compiler/solve/src/solve.rs b/crates/compiler/solve/src/solve.rs index d42bcb9208..b84f6219c0 100644 --- a/crates/compiler/solve/src/solve.rs +++ b/crates/compiler/solve/src/solve.rs @@ -452,7 +452,7 @@ fn solve( new_scope.insert_symbol_var_if_vacant(*symbol, loc_var.value); - check_ident_suffix( + solve_suffix_fx( env, problems, FxSuffixKind::Let(*symbol), @@ -853,7 +853,7 @@ fn solve( *type_index, ); - check_ident_suffix(env, problems, *kind, actual, region); + solve_suffix_fx(env, problems, *kind, actual, region); state } EffectfulStmt(variable, region) => { @@ -1622,7 +1622,7 @@ fn solve( state } -fn check_ident_suffix( +fn solve_suffix_fx( env: &mut InferenceEnv<'_>, problems: &mut Vec, kind: FxSuffixKind, @@ -1634,20 +1634,36 @@ fn check_ident_suffix( if let Content::Structure(FlatType::Func(_, _, _, fx)) = env.subs.get_content_without_compacting(variable) { - if let Content::Effectful = env.subs.get_content_without_compacting(*fx) { - problems.push(TypeError::UnsuffixedEffectfulFunction(*region, kind)); + let fx = *fx; + match env.subs.get_content_without_compacting(fx) { + Content::Effectful => { + problems.push(TypeError::UnsuffixedEffectfulFunction(*region, kind)); + } + Content::FlexVar(_) => { + env.subs.set_content(fx, Content::Pure); + } + _ => {} } } } - IdentSuffix::Bang => { - if let Content::Structure(FlatType::Func(_, _, _, fx)) = - env.subs.get_content_without_compacting(variable) - { - if let Content::Pure = env.subs.get_content_without_compacting(*fx) { - problems.push(TypeError::SuffixedPureFunction(*region, kind)); + IdentSuffix::Bang => match env.subs.get_content_without_compacting(variable) { + Content::Structure(FlatType::Func(_, _, _, fx)) => { + let fx = *fx; + match env.subs.get_content_without_compacting(fx) { + Content::Pure => { + problems.push(TypeError::SuffixedPureFunction(*region, kind)); + } + Content::FlexVar(_) => { + env.subs.set_content(fx, Content::Effectful); + } + _ => {} } } - } + Content::FlexVar(_) => { + // [purity-inference] TODO: Require effectful fn + } + _ => {} + }, } }