Treat untyped unsuffixed functions as pure

This commit is contained in:
Agus Zubiaga 2024-10-23 21:27:04 -03:00
parent 70fa4d036c
commit af6fc6306f
No known key found for this signature in database
5 changed files with 186 additions and 24 deletions

View file

@ -635,6 +635,10 @@ impl Constraints {
Constraint::FxSuffix(constraint_index) 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 { pub fn contains_save_the_environment(&self, constraint: &Constraint) -> bool {
match constraint { match constraint {
Constraint::SaveTheEnvironment => true, Constraint::SaveTheEnvironment => true,

View file

@ -234,6 +234,7 @@ fn constrain_untyped_closure(
ret_constraint, ret_constraint,
Generalizable(true), Generalizable(true),
), ),
constraints.and_constraint(pattern_state.delayed_fx_suffix_constraints),
constraints.equal_types_with_storage( constraints.equal_types_with_storage(
function_type, function_type,
expected, expected,
@ -243,7 +244,7 @@ fn constrain_untyped_closure(
), ),
early_returns_constraint, early_returns_constraint,
closure_constraint, closure_constraint,
Constraint::FlexToPure(fx_var), constraints.flex_to_pure(fx_var),
]; ];
constraints.exists_many(vars, cons) constraints.exists_many(vars, cons)
@ -2029,6 +2030,7 @@ fn constrain_function_def(
vars: Vec::with_capacity(function_def.arguments.len()), vars: Vec::with_capacity(function_def.arguments.len()),
constraints: Vec::with_capacity(1), constraints: Vec::with_capacity(1),
delayed_is_open_constraints: vec![], 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 mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1);
let closure_var = function_def.closure_type; let closure_var = function_def.closure_type;
@ -2171,9 +2173,12 @@ fn constrain_function_def(
Category::Lambda, Category::Lambda,
region, 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. // Finally put the solved closure type into the dedicated def expr variable.
constraints.store(signature_index, expr_var, std::file!(), std::line!()), constraints.store(signature_index, expr_var, std::file!(), std::line!()),
closure_constraint, closure_constraint,
constraints.flex_to_pure(function_def.fx_type),
]; ];
let expr_con = constraints.exists_many(vars, cons); let expr_con = constraints.exists_many(vars, cons);
@ -2488,6 +2493,7 @@ fn constrain_when_branch_help(
vars: Vec::with_capacity(2), vars: Vec::with_capacity(2),
constraints: Vec::with_capacity(2), constraints: Vec::with_capacity(2),
delayed_is_open_constraints: Vec::new(), delayed_is_open_constraints: Vec::new(),
delayed_fx_suffix_constraints: Vec::new(),
}; };
for (i, loc_pattern) in when_branch.patterns.iter().enumerate() { for (i, loc_pattern) in when_branch.patterns.iter().enumerate() {
@ -2512,6 +2518,9 @@ fn constrain_when_branch_help(
state state
.delayed_is_open_constraints .delayed_is_open_constraints
.extend(partial_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 { if i == 0 {
state.headers.extend(partial_state.headers); state.headers.extend(partial_state.headers);
@ -2840,6 +2849,7 @@ pub(crate) fn constrain_def_pattern(
vars: Vec::with_capacity(1), vars: Vec::with_capacity(1),
constraints: Vec::with_capacity(1), constraints: Vec::with_capacity(1),
delayed_is_open_constraints: vec![], delayed_is_open_constraints: vec![],
delayed_fx_suffix_constraints: vec![],
}; };
constrain_pattern( constrain_pattern(
@ -2949,6 +2959,7 @@ fn constrain_typed_def(
vars: Vec::with_capacity(arguments.len()), vars: Vec::with_capacity(arguments.len()),
constraints: Vec::with_capacity(1), constraints: Vec::with_capacity(1),
delayed_is_open_constraints: vec![], 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 mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1);
let ret_var = *ret_var; let ret_var = *ret_var;
@ -3032,6 +3043,8 @@ fn constrain_typed_def(
// This is a syntactic function, it can be generalized // This is a syntactic function, it can be generalized
Generalizable(true), 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 // 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 // when we check that the solved function type matches the annotation, we can
// display the fully inferred return variable. // 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, *fn_var, std::file!(), std::line!()),
constraints.store(signature_index, expr_var, std::file!(), std::line!()), constraints.store(signature_index, expr_var, std::file!(), std::line!()),
closure_constraint, closure_constraint,
Constraint::FlexToPure(fx_var), constraints.flex_to_pure(fx_var),
]; ];
let expr_con = constraints.exists_many(vars, cons); let expr_con = constraints.exists_many(vars, cons);
@ -3933,6 +3946,7 @@ fn constraint_recursive_function(
vars: Vec::with_capacity(function_def.arguments.len()), vars: Vec::with_capacity(function_def.arguments.len()),
constraints: Vec::with_capacity(1), constraints: Vec::with_capacity(1),
delayed_is_open_constraints: vec![], 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 mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1);
let ret_var = function_def.return_type; let ret_var = function_def.return_type;
@ -4022,13 +4036,15 @@ fn constraint_recursive_function(
// Syntactic function can be generalized // Syntactic function can be generalized
Generalizable(true), 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), 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 // "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 // 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(signature_index, expr_var, std::file!(), std::line!()),
constraints.store(ret_type_index, ret_var, std::file!(), std::line!()), constraints.store(ret_type_index, ret_var, std::file!(), std::line!()),
closure_constraint, closure_constraint,
Constraint::FlexToPure(fx_var), constraints.flex_to_pure(fx_var),
]; ];
let and_constraint = constraints.and_constraint(cons); let and_constraint = constraints.and_constraint(cons);
@ -4502,6 +4518,7 @@ fn rec_defs_help(
vars: Vec::with_capacity(arguments.len()), vars: Vec::with_capacity(arguments.len()),
constraints: Vec::with_capacity(1), constraints: Vec::with_capacity(1),
delayed_is_open_constraints: vec![], delayed_is_open_constraints: vec![],
delayed_fx_suffix_constraints: Vec::with_capacity(arguments.len()),
}; };
let mut vars = let mut vars =
Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); Vec::with_capacity(argument_pattern_state.vars.capacity() + 1);
@ -4578,6 +4595,10 @@ fn rec_defs_help(
expr_con, expr_con,
generalizable, generalizable,
), ),
// Check argument suffixes against usage
constraints.and_constraint(
argument_pattern_state.delayed_fx_suffix_constraints,
),
constraints.equal_types( constraints.equal_types(
fn_type_index, fn_type_index,
expected_index, expected_index,
@ -4595,6 +4616,7 @@ fn rec_defs_help(
), ),
constraints.store(ret_type_index, ret_var, std::file!(), std::line!()), constraints.store(ret_type_index, ret_var, std::file!(), std::line!()),
closure_constraint, closure_constraint,
constraints.flex_to_pure(fx_var),
]; ];
let and_constraint = constraints.and_constraint(cons); let and_constraint = constraints.and_constraint(cons);

View file

@ -22,6 +22,7 @@ pub struct PatternState {
pub vars: Vec<Variable>, pub vars: Vec<Variable>,
pub constraints: Vec<Constraint>, pub constraints: Vec<Constraint>,
pub delayed_is_open_constraints: Vec<Constraint>, pub delayed_is_open_constraints: Vec<Constraint>,
pub delayed_fx_suffix_constraints: Vec<Constraint>,
} }
/// If there is a type annotation, the pattern state headers can be optimized by putting the /// 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, region: Region,
expected: PExpectedTypeIndex, expected: PExpectedTypeIndex,
state: &mut PatternState, 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 { match pattern {
Underscore => { Underscore => {
@ -276,9 +299,13 @@ pub fn constrain_pattern(
.push(constraints.is_open_type(type_index)); .push(constraints.is_open_type(type_index));
} }
state if is_shallow {
.constraints // Identifiers introduced in nested patterns get let constraints
.push(constraints.fx_pattern_suffix(*symbol, type_index, region)); // 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( state.headers.insert(
*symbol, *symbol,
@ -301,7 +328,7 @@ pub fn constrain_pattern(
}, },
); );
constrain_pattern( constrain_pattern_help(
types, types,
constraints, constraints,
env, env,
@ -309,6 +336,7 @@ pub fn constrain_pattern(
subpattern.region, subpattern.region,
expected, expected,
state, state,
false,
) )
} }
@ -534,7 +562,7 @@ pub fn constrain_pattern(
)); ));
state.vars.push(*guard_var); state.vars.push(*guard_var);
constrain_pattern( constrain_pattern_help(
types, types,
constraints, constraints,
env, env,
@ -542,6 +570,7 @@ pub fn constrain_pattern(
loc_pattern.region, loc_pattern.region,
expected, expected,
state, state,
false,
); );
pat_type pat_type
@ -632,7 +661,7 @@ pub fn constrain_pattern(
)); ));
state.vars.push(*guard_var); state.vars.push(*guard_var);
constrain_pattern( constrain_pattern_help(
types, types,
constraints, constraints,
env, env,
@ -640,6 +669,7 @@ pub fn constrain_pattern(
loc_guard.region, loc_guard.region,
expected, expected,
state, state,
false,
); );
RecordField::Demanded(pat_type) RecordField::Demanded(pat_type)
@ -755,7 +785,7 @@ pub fn constrain_pattern(
loc_pat.region, loc_pat.region,
)); ));
constrain_pattern( constrain_pattern_help(
types, types,
constraints, constraints,
env, env,
@ -763,6 +793,7 @@ pub fn constrain_pattern(
loc_pat.region, loc_pat.region,
expected, expected,
state, state,
false,
); );
} }
@ -811,7 +842,7 @@ pub fn constrain_pattern(
pattern_type, pattern_type,
region, region,
)); ));
constrain_pattern( constrain_pattern_help(
types, types,
constraints, constraints,
env, env,
@ -819,6 +850,7 @@ pub fn constrain_pattern(
loc_pattern.region, loc_pattern.region,
expected, expected,
state, state,
false,
); );
} }
@ -876,7 +908,7 @@ pub fn constrain_pattern(
// First, add a constraint for the argument "who" // First, add a constraint for the argument "who"
let arg_pattern_expected = constraints let arg_pattern_expected = constraints
.push_pat_expected_type(PExpected::NoExpectation(arg_pattern_type_index)); .push_pat_expected_type(PExpected::NoExpectation(arg_pattern_type_index));
constrain_pattern( constrain_pattern_help(
types, types,
constraints, constraints,
env, env,
@ -884,6 +916,7 @@ pub fn constrain_pattern(
loc_arg_pattern.region, loc_arg_pattern.region,
arg_pattern_expected, arg_pattern_expected,
state, state,
false,
); );
// Next, link `whole_var` to the opaque type of "@Id who" // Next, link `whole_var` to the opaque type of "@Id who"

View file

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

View file

@ -452,7 +452,7 @@ fn solve(
new_scope.insert_symbol_var_if_vacant(*symbol, loc_var.value); new_scope.insert_symbol_var_if_vacant(*symbol, loc_var.value);
check_ident_suffix( solve_suffix_fx(
env, env,
problems, problems,
FxSuffixKind::Let(*symbol), FxSuffixKind::Let(*symbol),
@ -853,7 +853,7 @@ fn solve(
*type_index, *type_index,
); );
check_ident_suffix(env, problems, *kind, actual, region); solve_suffix_fx(env, problems, *kind, actual, region);
state state
} }
EffectfulStmt(variable, region) => { EffectfulStmt(variable, region) => {
@ -1622,7 +1622,7 @@ fn solve(
state state
} }
fn check_ident_suffix( fn solve_suffix_fx(
env: &mut InferenceEnv<'_>, env: &mut InferenceEnv<'_>,
problems: &mut Vec<TypeError>, problems: &mut Vec<TypeError>,
kind: FxSuffixKind, kind: FxSuffixKind,
@ -1634,20 +1634,36 @@ fn check_ident_suffix(
if let Content::Structure(FlatType::Func(_, _, _, fx)) = if let Content::Structure(FlatType::Func(_, _, _, fx)) =
env.subs.get_content_without_compacting(variable) env.subs.get_content_without_compacting(variable)
{ {
if let Content::Effectful = env.subs.get_content_without_compacting(*fx) { let fx = *fx;
problems.push(TypeError::UnsuffixedEffectfulFunction(*region, kind)); 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 => { IdentSuffix::Bang => match env.subs.get_content_without_compacting(variable) {
if let Content::Structure(FlatType::Func(_, _, _, fx)) = Content::Structure(FlatType::Func(_, _, _, fx)) => {
env.subs.get_content_without_compacting(variable) let fx = *fx;
{ match env.subs.get_content_without_compacting(fx) {
if let Content::Pure = env.subs.get_content_without_compacting(*fx) { Content::Pure => {
problems.push(TypeError::SuffixedPureFunction(*region, kind)); problems.push(TypeError::SuffixedPureFunction(*region, kind));
}
Content::FlexVar(_) => {
env.subs.set_content(fx, Content::Effectful);
}
_ => {}
} }
} }
} Content::FlexVar(_) => {
// [purity-inference] TODO: Require effectful fn
}
_ => {}
},
} }
} }