Initial working version of proper try keyword

This commit is contained in:
Sam Mohr 2024-12-02 03:20:44 -08:00
parent a7168a4ad6
commit eedade8e81
No known key found for this signature in database
GPG key ID: EA41D161A3C1BC99
33 changed files with 1036 additions and 521 deletions

View file

@ -247,6 +247,8 @@ fn main() -> io::Result<()> {
) {
Ok((problems, total_time)) => {
problems.print_error_warning_count(total_time);
println!(".\n");
exit_code = problems.exit_code();
}

View file

@ -334,7 +334,7 @@ mod cli_tests {
);
let expected_out =
"0 error and 0 warning found in <ignored for test> ms\n0 error and 0 warning found in <ignored for test> ms\n";
"0 errors and 0 warnings found in <ignored for test> ms.\n\n0 errors and 0 warnings found in <ignored for test> ms.\n\n";
cli_build.run().assert_clean_stdout(expected_out);
}

View file

@ -1,7 +1,7 @@
---
source: crates/cli/tests/cli_tests.rs
assertion_line: 429
expression: cli_dev_out.normalize_stdout_and_stderr()
snapshot_kind: text
---
── TOO MANY ARGS in tests/test-projects/module_params/arity_mismatch.roc ───────
@ -36,8 +36,6 @@ make partial application explicit.
────────────────────────────────────────────────────────────────────────────────
3 error and 0 warning found in <ignored for test> ms
.
3 errors and 0 warnings found in <ignored for test> ms.
You can run <ignored for tests>

View file

@ -1,7 +1,7 @@
---
source: crates/cli/tests/cli_tests.rs
assertion_line: 445
expression: cli_dev_out.normalize_stdout_and_stderr()
snapshot_kind: text
---
── TYPE MISMATCH in tests/test-projects/module_params/BadAnn.roc ───────────────
@ -41,8 +41,6 @@ Tip: It looks like it takes too many arguments. I'm seeing 1 extra.
────────────────────────────────────────────────────────────────────────────────
2 error and 1 warning found in <ignored for test> ms
.
2 errors and 1 warning found in <ignored for test> ms.
You can run <ignored for tests>

View file

@ -1,15 +1,16 @@
---
source: crates/cli/tests/cli_tests.rs
assertion_line: 476
expression: cli_dev_out.normalize_stdout_and_stderr()
snapshot_kind: text
---
── TYPE MISMATCH in tests/test-projects/module_params/unexpected_fn.roc ────────
This argument to this string interpolation has an unexpected type:
11│ $(Api.getPost)
^^^^^^^^^^^
10│ """
11│> $(Api.getPost)
12│ """
The argument is an anonymous function of type:
@ -21,8 +22,6 @@ But this string interpolation needs its argument to be:
────────────────────────────────────────────────────────────────────────────────
1 error and 0 warning found in <ignored for test> ms
.
1 error and 0 warnings found in <ignored for test> ms.
You can run <ignored for tests>

View file

@ -31,6 +31,7 @@ pub struct Constraints {
pub cycles: Vec<Cycle>,
pub fx_call_constraints: Vec<FxCallConstraint>,
pub fx_suffix_constraints: Vec<FxSuffixConstraint>,
pub try_target_constraints: Vec<TryTargetConstraint>,
}
impl std::fmt::Debug for Constraints {
@ -87,6 +88,7 @@ impl Constraints {
let cycles = Vec::new();
let fx_call_constraints = Vec::with_capacity(16);
let fx_suffix_constraints = Vec::new();
let result_type_constraints = Vec::new();
categories.extend([
Category::Record,
@ -103,7 +105,6 @@ impl Constraints {
Category::List,
Category::Str,
Category::Character,
Category::Return,
]);
pattern_categories.extend([
@ -138,6 +139,7 @@ impl Constraints {
cycles,
fx_call_constraints,
fx_suffix_constraints,
try_target_constraints: result_type_constraints,
}
}
@ -635,6 +637,25 @@ impl Constraints {
Constraint::FlexToPure(fx_var)
}
pub fn try_target(
&mut self,
result_type_index: TypeOrVar,
ok_payload_var: Variable,
err_payload_var: Variable,
region: Region,
) -> Constraint {
let constraint = TryTargetConstraint {
target_type_index: result_type_index,
ok_payload_var,
err_payload_var,
region,
};
let constraint_index = index_push_new(&mut self.try_target_constraints, constraint);
Constraint::TryTarget(constraint_index)
}
pub fn contains_save_the_environment(&self, constraint: &Constraint) -> bool {
match constraint {
Constraint::SaveTheEnvironment => true,
@ -660,6 +681,7 @@ impl Constraints {
| Constraint::Lookup(..)
| Constraint::Pattern(..)
| Constraint::ExpectEffectful(..)
| Constraint::TryTarget(_)
| Constraint::FxCall(_)
| Constraint::FxSuffix(_)
| Constraint::FlexToPure(_)
@ -843,6 +865,8 @@ pub enum Constraint {
FlexToPure(Variable),
/// Expect statement or ignored def to be effectful
ExpectEffectful(Variable, ExpectEffectfulReason, Region),
/// Expect value to be some kind of Result
TryTarget(Index<TryTargetConstraint>),
/// Used for things that always unify, e.g. blanks and runtime errors
True,
SaveTheEnvironment,
@ -909,6 +933,14 @@ pub struct IncludesTag {
pub region: Region,
}
#[derive(Debug, Clone)]
pub struct TryTargetConstraint {
pub target_type_index: TypeOrVar,
pub ok_payload_var: Variable,
pub err_payload_var: Variable,
pub region: Region,
}
#[derive(Debug, Clone, Copy)]
pub struct Cycle {
pub def_names: Slice<(Symbol, Region)>,
@ -1000,6 +1032,9 @@ impl std::fmt::Debug for Constraint {
Self::FlexToPure(arg0) => {
write!(f, "FlexToPure({arg0:?})")
}
Self::TryTarget(arg0) => {
write!(f, "ExpectResultType({arg0:?})")
}
Self::True => write!(f, "True"),
Self::SaveTheEnvironment => write!(f, "SaveTheEnvironment"),
Self::Let(arg0, arg1) => f.debug_tuple("Let").field(arg0).field(arg1).finish(),

View file

@ -479,7 +479,7 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
fx_type: sub!(*fx_type),
early_returns: early_returns
.iter()
.map(|(var, region)| (sub!(*var), *region))
.map(|(var, region, type_)| (sub!(*var), *region, *type_))
.collect(),
name: *name,
captured_symbols: captured_symbols
@ -718,6 +718,22 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
symbol: *symbol,
},
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
} => Try {
result_expr: Box::new(result_expr.map(|e| go_help!(e))),
result_var: sub!(*result_var),
return_var: sub!(*return_var),
ok_payload_var: sub!(*ok_payload_var),
err_payload_var: sub!(*err_payload_var),
err_ext_var: sub!(*err_ext_var),
},
RuntimeError(err) => RuntimeError(err.clone()),
}
}

View file

@ -452,6 +452,7 @@ fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a,
),
Dbg { .. } => todo!(),
Expect { .. } => todo!(),
Try { .. } => todo!(),
Return { .. } => todo!(),
RuntimeError(_) => todo!(),
}

View file

@ -44,7 +44,7 @@ fn new_op_call_expr<'a>(
match right_without_spaces {
Try => {
let desugared_left = desugar_expr(env, scope, left);
return desugar_try_expr(env, scope, desugared_left);
return Loc::at(region, Expr::LowLevelTry(desugared_left));
}
Apply(&Loc { value: Try, .. }, arguments, _called_via) => {
let try_fn = desugar_expr(env, scope, arguments.first().unwrap());
@ -58,13 +58,12 @@ fn new_op_call_expr<'a>(
.map(|a| desugar_expr(env, scope, a)),
);
return desugar_try_expr(
env,
scope,
env.arena.alloc(Loc::at(
right.region,
return Loc::at(
region,
Expr::LowLevelTry(env.arena.alloc(Loc::at(
region,
Expr::Apply(try_fn, args.into_bump_slice(), CalledVia::Try),
)),
))),
);
}
_ => {}
@ -957,13 +956,17 @@ pub fn desugar_expr<'a>(
desugared_args.push(desugar_expr(env, scope, loc_arg));
}
let args_region =
Region::span_across(&loc_args[0].region, &loc_args[loc_args.len() - 1].region);
env.arena.alloc(Loc::at(
loc_expr.region,
args_region,
Expr::Apply(function, desugared_args.into_bump_slice(), CalledVia::Try),
))
};
env.arena.alloc(desugar_try_expr(env, scope, result_expr))
env.arena
.alloc(Loc::at(loc_expr.region, Expr::LowLevelTry(result_expr)))
}
Apply(loc_fn, loc_args, called_via) => {
let mut desugared_args = Vec::with_capacity_in(loc_args.len(), env.arena);
@ -1134,77 +1137,11 @@ pub fn desugar_expr<'a>(
})
}
// note this only exists after desugaring
LowLevelDbg(_, _, _) => loc_expr,
// note these only exist after desugaring
LowLevelDbg(_, _, _) | LowLevelTry(_) => loc_expr,
}
}
pub fn desugar_try_expr<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
result_expr: &'a Loc<Expr<'a>>,
) -> Loc<Expr<'a>> {
let region = result_expr.region;
let ok_symbol = env.arena.alloc(scope.gen_unique_symbol_name().to_string());
let err_symbol = env.arena.alloc(scope.gen_unique_symbol_name().to_string());
let ok_branch = env.arena.alloc(WhenBranch {
patterns: env.arena.alloc([Loc::at(
region,
Pattern::Apply(
env.arena.alloc(Loc::at(region, Pattern::Tag("Ok"))),
env.arena
.alloc([Loc::at(region, Pattern::Identifier { ident: ok_symbol })]),
),
)]),
value: Loc::at(
region,
Expr::Var {
module_name: "",
ident: ok_symbol,
},
),
guard: None,
});
let err_branch = env.arena.alloc(WhenBranch {
patterns: env.arena.alloc([Loc::at(
region,
Pattern::Apply(
env.arena.alloc(Loc::at(region, Pattern::Tag("Err"))),
env.arena
.alloc([Loc::at(region, Pattern::Identifier { ident: err_symbol })]),
),
)]),
value: Loc::at(
region,
Expr::Return(
env.arena.alloc(Loc::at(
region,
Expr::Apply(
env.arena.alloc(Loc::at(region, Expr::Tag("Err"))),
&*env.arena.alloc([&*env.arena.alloc(Loc::at(
region,
Expr::Var {
module_name: "",
ident: err_symbol,
},
))]),
CalledVia::Try,
),
)),
None,
),
),
guard: None,
});
Loc::at(
region,
Expr::When(result_expr, &*env.arena.alloc([&*ok_branch, &*err_branch])),
)
}
fn desugar_str_segments<'a>(
env: &mut Env<'a>,
scope: &mut Scope,

View file

@ -26,7 +26,9 @@ use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError};
use roc_region::all::{Loc, Region};
use roc_types::num::SingleQuoteBound;
use roc_types::subs::{ExhaustiveMark, IllegalCycleMark, RedundantMark, VarStore, Variable};
use roc_types::types::{Alias, Category, IndexOrField, LambdaSet, OptAbleVar, Type};
use roc_types::types::{
Alias, Category, EarlyReturnKind, IndexOrField, LambdaSet, OptAbleVar, Type,
};
use soa::Index;
use std::fmt::{Debug, Display};
use std::path::PathBuf;
@ -328,6 +330,15 @@ pub enum Expr {
symbol: Symbol,
},
Try {
result_expr: Box<Loc<Expr>>,
result_var: Variable,
return_var: Variable,
ok_payload_var: Variable,
err_payload_var: Variable,
err_ext_var: Variable,
},
Return {
return_value: Box<Loc<Expr>>,
return_var: Variable,
@ -403,9 +414,10 @@ impl Expr {
}
Self::Expect { .. } => Category::Expect,
Self::Crash { .. } => Category::Crash,
Self::Return { .. } => Category::Return,
Self::Return { .. } => Category::Return(EarlyReturnKind::Return),
Self::Dbg { .. } => Category::Expect,
Self::Try { .. } => Category::TrySuccess,
// these nodes place no constraints on the expression's type
Self::RuntimeError(..) => Category::Unknown,
@ -429,7 +441,7 @@ impl Expr {
| Self::ZeroArgumentTag { .. }
| Self::OpaqueWrapFunction(_)
| Self::RuntimeError(..) => false,
Self::Return { .. } => true,
Self::Return { .. } | Self::Try { .. } => true,
Self::List { loc_elems, .. } => loc_elems
.iter()
.any(|elem| elem.value.contains_any_early_returns()),
@ -545,7 +557,7 @@ pub struct ClosureData {
pub closure_type: Variable,
pub return_type: Variable,
pub fx_type: Variable,
pub early_returns: Vec<(Variable, Region)>,
pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>,
pub name: Symbol,
pub captured_symbols: Vec<(Symbol, Variable)>,
pub recursive: Recursive,
@ -1385,6 +1397,28 @@ pub fn canonicalize_expr<'a>(
output,
)
}
ast::Expr::LowLevelTry(loc_expr) => {
let (loc_result_expr, output) =
canonicalize_expr(env, var_store, scope, loc_expr.region, &loc_expr.value);
let return_var = var_store.fresh();
scope
.early_returns
.push((return_var, loc_expr.region, EarlyReturnKind::Try));
(
Try {
result_expr: Box::new(loc_result_expr),
result_var: var_store.fresh(),
return_var,
ok_payload_var: var_store.fresh(),
err_payload_var: var_store.fresh(),
err_ext_var: var_store.fresh(),
},
output,
)
}
ast::Expr::Return(return_expr, after_return) => {
let mut output = Output::default();
@ -1409,7 +1443,9 @@ pub fn canonicalize_expr<'a>(
let return_var = var_store.fresh();
scope.early_returns.push((return_var, return_expr.region));
scope
.early_returns
.push((return_var, return_expr.region, EarlyReturnKind::Return));
(
Return {
@ -2351,6 +2387,29 @@ pub fn inline_calls(var_store: &mut VarStore, expr: Expr) -> Expr {
}
}
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
} => {
let loc_result_expr = Loc {
region: result_expr.region,
value: inline_calls(var_store, result_expr.value),
};
Try {
result_expr: Box::new(loc_result_expr),
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
}
}
LetRec(defs, loc_expr, mark) => {
let mut new_defs = Vec::with_capacity(defs.len());
@ -2645,6 +2704,7 @@ pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool {
| ast::Expr::MalformedIdent(_, _)
| ast::Expr::Tag(_)
| ast::Expr::OpaqueRef(_) => true,
ast::Expr::LowLevelTry(loc_expr) => is_valid_interpolation(&loc_expr.value),
// Newlines are disallowed inside interpolation, and these all require newlines
ast::Expr::DbgStmt { .. }
| ast::Expr::LowLevelDbg(_, _, _)
@ -3401,7 +3461,7 @@ pub struct FunctionDef {
pub closure_type: Variable,
pub return_type: Variable,
pub fx_type: Variable,
pub early_returns: Vec<(Variable, Region)>,
pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>,
pub captured_symbols: Vec<(Symbol, Variable)>,
pub arguments: Vec<(Variable, AnnotatedMark, Loc<Pattern>)>,
}
@ -3543,6 +3603,9 @@ pub(crate) fn get_lookup_symbols(expr: &Expr) -> Vec<ExpectLookup> {
// Intentionally ignore the lookups in the nested `expect` condition itself,
// because they couldn't possibly influence the outcome of this `expect`!
}
Expr::Try { result_expr, .. } => {
stack.push(&result_expr.value);
}
Expr::Return { return_value, .. } => {
stack.push(&return_value.value);
}

View file

@ -371,9 +371,10 @@ pub fn canonicalize_module_defs<'a>(
PatternType::TopLevelDef,
);
for (_early_return_var, early_return_region) in &scope.early_returns {
for (_early_return_var, early_return_region, early_return_kind) in &scope.early_returns {
env.problem(Problem::ReturnOutsideOfFunction {
region: *early_return_region,
return_kind: *early_return_kind,
});
}
@ -964,6 +965,14 @@ fn fix_values_captured_in_closure_expr(
);
}
Try { result_expr, .. } => {
fix_values_captured_in_closure_expr(
&mut result_expr.value,
no_capture_symbols,
closure_captures,
);
}
Return { return_value, .. } => {
fix_values_captured_in_closure_expr(
&mut return_value.value,

View file

@ -5,7 +5,7 @@ use roc_module::symbol::{IdentId, IdentIds, ModuleId, ModuleIds, Symbol};
use roc_problem::can::{RuntimeError, ScopeModuleSource};
use roc_region::all::{Loc, Region};
use roc_types::subs::Variable;
use roc_types::types::{Alias, AliasKind, AliasVar, Type};
use roc_types::types::{Alias, AliasKind, AliasVar, EarlyReturnKind, Type};
use crate::abilities::PendingAbilitiesStore;
@ -49,7 +49,7 @@ pub struct Scope {
/// We won't intern them because they're only used during canonicalization for error reporting.
ignored_locals: VecMap<String, Region>,
pub early_returns: Vec<(Variable, Region)>,
pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>,
}
impl Scope {

View file

@ -400,6 +400,16 @@ pub fn walk_expr<V: Visitor>(visitor: &mut V, expr: &Expr, var: Variable) {
Variable::NULL,
);
}
Expr::Try {
result_expr,
result_var,
return_var: _,
ok_payload_var: _,
err_payload_var: _,
err_ext_var: _,
} => {
visitor.visit_expr(&result_expr.value, result_expr.region, *result_var);
}
Expr::Return {
return_value,
return_var,

View file

@ -816,64 +816,13 @@ mod test_can {
// Assert that we desugar to:
//
// when Str.toU64 "123" is
// Ok `0` -> `0`
// Err `1` -> return Err `1`
// Try(Str.toU64 "123")
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].patterns.len(), 1);
assert!(!branches[0].patterns[0].degenerate);
match &branches[0].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Ok");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "0", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
assert!(&branches[0].guard.is_none());
assert_var_usage(&branches[0].value.value, "0", &out.interns);
assert_eq!(branches[1].patterns.len(), 1);
assert!(!branches[1].patterns[0].degenerate);
match &branches[1].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "1", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
match &branches[1].value.value {
Expr::Return { return_value, .. } => match &return_value.value {
Expr::Tag {
name, arguments, ..
} => {
assert_eq!(name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_var_usage(&arguments[0].1.value, "1", &out.interns);
}
other_inner => panic!("Expr was not a Tag: {:?}", other_inner),
},
other_outer => panic!("Expr was not a Return: {:?}", other_outer),
}
}
#[test]
@ -890,64 +839,13 @@ mod test_can {
// Assert that we desugar to:
//
// when Str.toU64 "123" is
// Ok `0` -> `0`
// Err `1` -> return Err `1`
// Try(Str.toU64 "123")
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].patterns.len(), 1);
assert!(!branches[0].patterns[0].degenerate);
match &branches[0].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Ok");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "0", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
assert!(&branches[0].guard.is_none());
assert_var_usage(&branches[0].value.value, "0", &out.interns);
assert_eq!(branches[1].patterns.len(), 1);
assert!(!branches[1].patterns[0].degenerate);
match &branches[1].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "1", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
match &branches[1].value.value {
Expr::Return { return_value, .. } => match &return_value.value {
Expr::Tag {
name, arguments, ..
} => {
assert_eq!(name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_var_usage(&arguments[0].1.value, "1", &out.interns);
}
other_inner => panic!("Expr was not a Tag: {:?}", other_inner),
},
other_outer => panic!("Expr was not a Return: {:?}", other_outer),
}
}
#[test]
@ -964,64 +862,13 @@ mod test_can {
// Assert that we desugar to:
//
// when Str.toU64 "123" is
// Ok `0` -> `0`
// Err `1` -> return Err `1`
// Try(Str.toU64 "123")
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Space, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].patterns.len(), 1);
assert!(!branches[0].patterns[0].degenerate);
match &branches[0].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Ok");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "0", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
assert!(&branches[0].guard.is_none());
assert_var_usage(&branches[0].value.value, "0", &out.interns);
assert_eq!(branches[1].patterns.len(), 1);
assert!(!branches[1].patterns[0].degenerate);
match &branches[1].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "1", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
match &branches[1].value.value {
Expr::Return { return_value, .. } => match &return_value.value {
Expr::Tag {
name, arguments, ..
} => {
assert_eq!(name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_var_usage(&arguments[0].1.value, "1", &out.interns);
}
other_inner => panic!("Expr was not a Tag: {:?}", other_inner),
},
other_outer => panic!("Expr was not a Return: {:?}", other_outer),
}
}
#[test]
@ -1142,6 +989,13 @@ mod test_can {
}
}
fn assert_try_expr(expr: &Expr) -> &Expr {
match expr {
Expr::Try { result_expr, .. } => &result_expr.value,
_ => panic!("Expr was not a Try: {:?}", expr),
}
}
// TAIL CALLS
fn get_closure(expr: &Expr, i: usize) -> roc_can::expr::Recursive {
match expr {

View file

@ -30,8 +30,8 @@ use roc_region::all::{Loc, Region};
use roc_types::subs::{IllegalCycleMark, Variable};
use roc_types::types::Type::{self, *};
use roc_types::types::{
AliasKind, AnnotationSource, Category, IndexOrField, OptAbleType, PReason, Reason, RecordField,
TypeExtension, TypeTag, Types,
AliasKind, AnnotationSource, Category, EarlyReturnKind, IndexOrField, OptAbleType, PReason,
Reason, RecordField, TypeExtension, TypeTag, Types,
};
use soa::{Index, Slice};
@ -152,7 +152,7 @@ fn constrain_untyped_closure(
closure_var: Variable,
ret_var: Variable,
fx_var: Variable,
early_returns: &[(Variable, Region)],
early_returns: &[(Variable, Region, EarlyReturnKind)],
arguments: &[(Variable, AnnotatedMark, Loc<Pattern>)],
loc_body_expr: &Loc<Expr>,
captured_symbols: &[(Symbol, Variable)],
@ -184,30 +184,16 @@ fn constrain_untyped_closure(
));
let returns_constraint = env.with_fx_expectation(fx_var, None, |env| {
let return_con = constrain_expr(
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
early_returns,
return_type_index,
);
let mut return_constraints = Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type_index,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
constraints.and_constraint(return_constraints)
ret_var,
false,
)
});
// make sure the captured symbols are sorted!
@ -259,6 +245,51 @@ fn constrain_untyped_closure(
constraints.exists_many(vars, cons)
}
pub fn constrain_function_return(
types: &mut Types,
constraints: &mut Constraints,
env: &mut Env,
body_expr: &Loc<Expr>,
early_returns: &[(Variable, Region, EarlyReturnKind)],
return_type_expected: ExpectedTypeIndex,
ret_var: Variable,
should_attach_res_constraints: bool,
) -> Constraint {
let return_con = constrain_expr(
types,
constraints,
env,
body_expr.region,
&body_expr.value,
return_type_expected,
);
let mut return_constraints = Vec::with_capacity(early_returns.len() + 1);
let mut return_type_vars = Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
return_type_vars.push(ret_var);
for (early_return_variable, early_return_region, early_return_kind) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type_expected,
Category::Return(*early_return_kind),
*early_return_region,
);
return_constraints.push(early_return_con);
return_type_vars.push(*early_return_variable);
}
let returns_constraint = constraints.exists_many(return_type_vars, return_constraints);
if should_attach_res_constraints {
attach_resolution_constraints(constraints, env, returns_constraint)
} else {
returns_constraint
}
}
pub fn constrain_expr(
types: &mut Types,
constraints: &mut Constraints,
@ -576,7 +607,6 @@ pub fn constrain_expr(
let mut arg_cons = Vec::with_capacity(loc_args.len());
for (index, (arg_var, loc_arg)) in loc_args.iter().enumerate() {
let region = loc_arg.region;
let arg_type = Variable(*arg_var);
let arg_type_index = constraints.push_variable(*arg_var);
@ -833,6 +863,78 @@ pub fn constrain_expr(
constraints.exists_many([*variable], [message_con, continuation_con])
}
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
} => {
let result_var_index = constraints.push_variable(*result_var);
let result_expected_type = constraints.push_expected_type(ForReason(
Reason::TryResult,
result_var_index,
result_expr.region,
));
let result_constraint = constrain_expr(
types,
constraints,
env,
result_expr.region,
&result_expr.value,
result_expected_type,
);
let try_target_constraint = constraints.try_target(
result_var_index,
*ok_payload_var,
*err_payload_var,
result_expr.region,
);
let return_type_index = constraints.push_variable(*return_var);
let expected_return_value = constraints.push_expected_type(ForReason(
Reason::TryResult,
return_type_index,
result_expr.region,
));
let try_failure_type_index = {
let typ = types.from_old_type(&Type::TagUnion(
vec![("Err".into(), vec![Type::Variable(*err_payload_var)])],
TypeExtension::from_non_annotation_type(Type::Variable(*err_ext_var)),
));
constraints.push_type(types, typ)
};
let try_failure_constraint = constraints.equal_types(
try_failure_type_index,
expected_return_value,
Category::TryFailure,
region,
);
let ok_type_index = constraints.push_variable(*ok_payload_var);
let try_success_constraint =
constraints.equal_types(ok_type_index, expected, Category::TrySuccess, region);
constraints.exists_many(
[
*return_var,
*result_var,
*ok_payload_var,
*err_payload_var,
*err_ext_var,
],
[
result_constraint,
try_target_constraint,
try_failure_constraint,
try_success_constraint,
],
)
}
If {
cond_var,
branch_var,
@ -1018,26 +1120,25 @@ pub fn constrain_expr(
Region::span_across(&loc_cond.region, &branches.last().unwrap().value.region)
};
let branch_expr_reason =
|expected: &Expected<TypeOrVar>, index, branch_region| match expected {
FromAnnotation(name, arity, ann_source, _typ) => {
// NOTE deviation from elm.
//
// in elm, `_typ` is used, but because we have this `expr_var` too
// and need to constrain it, this is what works and gives better error messages
FromAnnotation(
name.clone(),
*arity,
AnnotationSource::TypedWhenBranch {
index,
region: ann_source.region(),
},
body_type_index,
)
}
let branch_expr_reason = |expected: &Expected<TypeOrVar>, index| match expected {
FromAnnotation(name, arity, ann_source, _typ) => {
// NOTE deviation from elm.
//
// in elm, `_typ` is used, but because we have this `expr_var` too
// and need to constrain it, this is what works and gives better error messages
FromAnnotation(
name.clone(),
*arity,
AnnotationSource::TypedWhenBranch {
index,
region: ann_source.region(),
},
body_type_index,
)
}
_ => ForReason(Reason::WhenBranch { index }, body_type_index, branch_region),
};
_ => ForReason(Reason::WhenBranch { index }, body_type_index, region),
};
// Our goal is to constrain and introduce variables in all pattern when branch patterns before
// looking at their bodies.
@ -1091,11 +1192,7 @@ pub fn constrain_expr(
region,
when_branch,
expected_pattern,
branch_expr_reason(
&constraints[expected],
HumanIndex::zero_based(index),
when_branch.value.region,
),
branch_expr_reason(&constraints[expected], HumanIndex::zero_based(index)),
);
pattern_vars.extend(new_pattern_vars);
@ -2074,43 +2171,19 @@ fn constrain_function_def(
constraints.push_type(types, fn_type)
};
let returns_constraint = {
let return_con = constrain_expr(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
return_type_annotation_expected,
);
let mut return_constraints =
Vec::with_capacity(function_def.early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in &function_def.early_returns {
let early_return_type_expected =
constraints.push_expected_type(Expected::ForReason(
Reason::FunctionOutput,
ret_type_index,
*early_return_region,
));
vars.push(*early_return_variable);
let early_return_con = constraints.equal_types_var(
*early_return_variable,
early_return_type_expected,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint = constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
};
let returns_constraint =
env.with_fx_expectation(function_def.fx_type, Some(annotation.region), |env| {
constrain_function_return(
types,
constraints,
env,
loc_body_expr,
&function_def.early_returns,
return_type_annotation_expected,
function_def.return_type,
true,
)
});
vars.push(expr_var);
@ -2459,7 +2532,7 @@ fn constrain_when_branch_help(
types,
constraints,
env,
region,
when_branch.value.region,
&when_branch.value.value,
expr_expected,
);
@ -2966,32 +3039,16 @@ fn constrain_typed_def(
let returns_constraint =
env.with_fx_expectation(fx_var, Some(annotation.region), |env| {
let return_con = constrain_expr(
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
early_returns,
return_type,
);
let mut return_constraints = Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint = constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
ret_var,
true,
)
});
vars.push(*fn_var);
@ -4025,35 +4082,22 @@ fn constraint_recursive_function(
let returns_constraint =
env.with_fx_expectation(fx_var, Some(annotation.region), |env| {
let expected = constraints.push_expected_type(NoExpectation(ret_type_index));
let return_con = constrain_expr(
let expected = constraints.push_expected_type(ForReason(
Reason::FunctionOutput,
ret_type_index,
region,
));
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
&function_def.early_returns,
expected,
);
let mut return_constraints =
Vec::with_capacity(function_def.early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in &function_def.early_returns
{
let early_return_con = constraints.equal_types_var(
*early_return_variable,
expected,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint = constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
ret_var,
true,
)
});
vars.push(expr_var);
@ -4406,6 +4450,7 @@ fn is_generalizable_expr(mut expr: &Expr) -> bool {
| RecordUpdate { .. }
| Expect { .. }
| Dbg { .. }
| Try { .. }
| Return { .. }
| RuntimeError(..)
| ZeroArgumentTag { .. }
@ -4597,37 +4642,20 @@ fn rec_defs_help(
};
let returns_constraint =
env.with_fx_expectation(fx_var, Some(annotation.region), |env| {
let return_type_expected =
constraints.push_expected_type(NoExpectation(ret_type_index));
let return_type_expected = constraints.push_expected_type(
ForReason(Reason::FunctionOutput, ret_type_index, region),
);
let return_con = constrain_expr(
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
early_returns,
return_type_expected,
);
let mut return_constraints =
Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type_expected,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint =
constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
ret_var,
true,
)
});
vars.push(*fn_var);

View file

@ -67,10 +67,6 @@ impl<'a> Formattable for Expr<'a> {
buf.indent(indent);
buf.push_str("crash");
}
Try => {
buf.indent(indent);
buf.push_str("try");
}
Apply(loc_expr, loc_args, _) => {
let apply_needs_parens = parens == Parens::InApply;
@ -201,6 +197,13 @@ impl<'a> Formattable for Expr<'a> {
LowLevelDbg(_, _, _) => unreachable!(
"LowLevelDbg should only exist after desugaring, not during formatting"
),
Try => {
buf.indent(indent);
buf.push_str("try");
}
LowLevelTry(_) => unreachable!(
"LowLevelTry should only exist after desugaring, not during formatting"
),
Return(return_value, after_return) => {
fmt_return(buf, return_value, after_return, parens, newlines, indent);
}
@ -367,6 +370,9 @@ pub fn expr_is_multiline(me: &Expr<'_>, comments_only: bool) -> bool {
| Expr::Crash
| Expr::Dbg
| Expr::Try => false,
Expr::LowLevelTry(_) => {
unreachable!("LowLevelTry should only exist after desugaring, not during formatting")
}
Expr::RecordAccess(inner, _)
| Expr::TupleAccess(inner, _)

View file

@ -1203,7 +1203,8 @@ mod test_reporting {
@r"
TYPE MISMATCH in /code/proj/Main.roc
This expression is used in an unexpected way:
This returns something that's incompatible with the return type of the
enclosing function:
5 f = \x -> g x
^^^
@ -1212,7 +1213,7 @@ mod test_reporting {
List List a
But you are trying to use it as:
But I expected the function to have return type:
List a
@ -1239,7 +1240,8 @@ mod test_reporting {
@r"
TYPE MISMATCH in /code/proj/Main.roc
This expression is used in an unexpected way:
This returns something that's incompatible with the return type of the
enclosing function:
7 g = \x -> f [x]
^^^^^
@ -1248,7 +1250,7 @@ mod test_reporting {
List List b
But you are trying to use it as:
But I expected the function to have return type:
List b
@ -6626,7 +6628,7 @@ All branches in an `if` must have the same type!
@r"
UNFINISHED FUNCTION in tmp/unfinished_closure_pattern_in_parens/Test.roc
I was partway through parsing a function, but I got stuck here:
I was partway through parsing a function, but I got stuck here:
4 x = \( a
5 )
@ -10258,16 +10260,17 @@ All branches in an `if` must have the same type!
@r"
TYPE MISMATCH in /code/proj/Main.roc
This expression is used in an unexpected way:
This returns something that's incompatible with the return type of the
enclosing function:
5 f = \_ -> if Bool.true then {} else f {}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
It is a value of type:
It a value of type:
{}
But you are trying to use it as:
But I expected the function to have return type:
* -> Str
"
@ -10414,11 +10417,12 @@ All branches in an `if` must have the same type!
Something is off with the 2nd branch of this `when` expression:
10 olist : OList
11 olist =
12> when alist is
13> Nil -> @OList Nil
14> Cons _ lst -> lst
10 olist : OList
11 olist =
12 when alist is
13 Nil -> @OList Nil
14 Cons _ lst -> lst
^^^
This `lst` value is a:
@ -10449,6 +10453,7 @@ All branches in an `if` must have the same type!
This 2nd argument to `map` has an unexpected type:
4 A := U8
5 List.map [1u16, 2u16, 3u16] @A
^^
@ -14556,7 +14561,7 @@ All branches in an `if` must have the same type!
@r###"
RETURN OUTSIDE OF FUNCTION in /code/proj/Main.roc
This `return` statement doesn't belong to a function:
This `return` doesn't belong to a function:
7 return x
^^^^^^^^
@ -14634,25 +14639,25 @@ All branches in an `if` must have the same type!
myFunction 3
"#
),
@r###"
TYPE MISMATCH in /code/proj/Main.roc
@r#"
TYPE MISMATCH in /code/proj/Main.roc
This `return` statement doesn't match the return type of its enclosing
function:
This returns something that's incompatible with the return type of the
enclosing function:
5 if x == 5 then
6> return "abc"
7 else
8 x
5 if x == 5 then
6> return "abc"
7 else
8 x
This returns a value of type:
This returns a value of type:
Str
Str
But I expected the function to have return type:
But I expected the function to have return type:
Num *
"###
Num *
"#
);
test_report!(
@ -14701,6 +14706,54 @@ All branches in an `if` must have the same type!
"###
);
test_report!(
return_in_bare_statement,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
# this outputs {}, so it's ignored
if 7 > 5 then
{}
else
return Err TooBig
# this outputs a value, so we are incorrectly
# dropping the parsed value
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
Ok {}
"#
),
@r#"
IGNORED RESULT in /code/proj/Main.roc
The result of this expression is ignored:
16> when List.get [1, 2, 3] 5 is
17> Ok item -> item
18> Err err ->
19> return Err err
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
"#
);
test_report!(
mismatch_only_early_returns,
indoc!(
@ -14714,26 +14767,26 @@ All branches in an `if` must have the same type!
myFunction 3
"#
),
@r###"
TYPE MISMATCH in /code/proj/Main.roc
This `return` statement doesn't match the return type of its enclosing
function:
5 if x == 5 then
6 return "abc"
7 else
8 return 123
^^^^^^^^^^
This returns a value of type:
Num *
But I expected the function to have return type:
Str
"###
@r#"
TYPE MISMATCH in /code/proj/Main.roc
This returns something that's incompatible with the return type of the
enclosing function:
5 if x == 5 then
6 return "abc"
7 else
8 return 123
^^^^^^^^^^
This returns a value of type:
Num *
But I expected the function to have return type:
Str
"#
);
test_report!(
@ -14776,6 +14829,61 @@ All branches in an `if` must have the same type!
"###
);
test_report!(
return_with_ignored_output,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
# not ignored, warning
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
# ignored, OK
_ =
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
# also ignored, also OK
_ignored =
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
Ok {}
"#
),
@r#"
IGNORED RESULT in /code/proj/Main.roc
The result of this expression is ignored:
9> when List.get [1, 2, 3] 5 is
10> Ok item -> item
11> Err err ->
12> return Err err
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
"#
);
test_report!(
no_early_return_in_bare_statement,
indoc!(
@ -14908,6 +15016,141 @@ All branches in an `if` must have the same type!
@"" // no errors
);
test_report!(
try_with_non_result_target,
indoc!(
r#"
invalidTry = \{} ->
x = try 64
Ok (x * 2)
invalidTry {}
"#
),
@r"
INVALID TRY TARGET in /code/proj/Main.roc
This expression cannot be used as a `try` target:
5 x = try 64
^^
I expected a Result, but it actually has type:
Num *
Hint: Did you forget to wrap the value with an `Ok` or an `Err` tag?
"
);
test_report!(
incompatible_try_errs,
indoc!(
r#"
incompatibleTrys = \{} ->
x = try Err 123
y = try Err "abc"
Ok (x + y)
incompatibleTrys {}
"#
),
@r#"
TYPE MISMATCH in /code/proj/Main.roc
This returns something that's incompatible with the return type of the
enclosing function:
5 x = try Err 123
6
7> y = try Err "abc"
8
9 Ok (x + y)
This returns an `Err` of type:
[Err Str, ]
But I expected the function to have return type:
[Err (Num *), ]a
"#
);
test_report!(
try_prefix_in_pipe,
indoc!(
r#"
readFile : Str -> Str
getFileContents : Str -> Result Str _
getFileContents = \filePath ->
contents =
readFile filePath
|> try Result.mapErr ErrWrapper
contents
getFileContents "file.txt"
"#
),
@r"
TYPE MISMATCH in /code/proj/Main.roc
This 1st argument to this function has an unexpected type:
9> readFile filePath
10 |> try Result.mapErr ErrWrapper
This `readFile` call produces:
Str
But this function needs its 1st argument to be:
Result ok a
"
);
test_report!(
try_suffix_in_pipe,
indoc!(
r#"
readFile : Str -> Str
getFileContents : Str -> Result Str _
getFileContents = \filePath ->
contents =
readFile filePath
|> Result.mapErr ErrWrapper
|> try
contents
getFileContents "file.txt"
"#
),
@r"
TYPE MISMATCH in /code/proj/Main.roc
This 1st argument to |> has an unexpected type:
9> readFile filePath
10 |> Result.mapErr ErrWrapper
This `readFile` call produces:
Str
But |> needs its 1st argument to be:
Result ok a
"
);
test_report!(
leftover_statement,
indoc!(

View file

@ -373,6 +373,16 @@ impl<'a> LowerParams<'a> {
expr_stack.push(&mut loc_message.value);
expr_stack.push(&mut loc_continuation.value);
}
Try {
result_expr,
result_var: _,
return_var: _,
ok_payload_var: _,
err_payload_var: _,
err_ext_var: _,
} => {
expr_stack.push(&mut result_expr.value);
}
Return {
return_value,
return_var: _,

View file

@ -104,7 +104,8 @@ pub fn remove_module_param_arguments(
| TypeError::FxInTopLevel(_, _)
| TypeError::ExpectedEffectful(_, _)
| TypeError::UnsuffixedEffectfulFunction(_, _)
| TypeError::SuffixedPureFunction(_, _) => {}
| TypeError::SuffixedPureFunction(_, _)
| TypeError::InvalidTryTarget(_, _) => {}
}
}
}
@ -188,7 +189,8 @@ fn remove_for_reason(
| Reason::CrashArg
| Reason::ImportParams(_)
| Reason::Stmt(_)
| Reason::FunctionOutput => {}
| Reason::FunctionOutput
| Reason::TryResult => {}
}
}

View file

@ -11,7 +11,7 @@ use crate::layout::{
use bumpalo::collections::{CollectIn, Vec};
use bumpalo::Bump;
use roc_can::abilities::SpecializationId;
use roc_can::expr::{AnnotatedMark, ClosureData, ExpectLookup};
use roc_can::expr::{AnnotatedMark, ClosureData, ExpectLookup, WhenBranch, WhenBranchPattern};
use roc_can::module::ExposedByModule;
use roc_collections::all::{default_hasher, BumpMap, BumpMapDefault, MutMap};
use roc_collections::VecMap;
@ -2930,7 +2930,7 @@ fn pattern_to_when_help(
body: Loc<roc_can::expr::Expr>,
symbol: Symbol,
) -> (Symbol, Loc<roc_can::expr::Expr>) {
use roc_can::expr::{Expr, WhenBranch, WhenBranchPattern};
use roc_can::expr::Expr;
let wrapped_body = Expr::When {
cond_var: pattern_var,
@ -5871,6 +5871,85 @@ pub fn with_hole<'a>(
}
}
}
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
} => {
let ok_symbol = env.unique_symbol();
let err_symbol = env.unique_symbol();
let ok_branch = WhenBranch {
patterns: vec![WhenBranchPattern {
pattern: Loc::at_zero(roc_can::pattern::Pattern::AppliedTag {
whole_var: result_var,
ext_var: Variable::EMPTY_TAG_UNION,
tag_name: "Ok".into(),
arguments: vec![(
ok_payload_var,
Loc::at_zero(roc_can::pattern::Pattern::Identifier(ok_symbol)),
)],
}),
degenerate: false,
}],
value: Loc::at_zero(Var(ok_symbol, ok_payload_var)),
guard: None,
redundant: RedundantMark::known_non_redundant(),
};
let err_branch = WhenBranch {
patterns: vec![WhenBranchPattern {
pattern: Loc::at_zero(roc_can::pattern::Pattern::AppliedTag {
whole_var: result_var,
ext_var: err_ext_var,
tag_name: "Err".into(),
arguments: vec![(
err_payload_var,
Loc::at_zero(roc_can::pattern::Pattern::Identifier(err_symbol)),
)],
}),
degenerate: false,
}],
value: Loc::at_zero(Return {
return_var,
return_value: Box::new(Loc::at_zero(Tag {
tag_union_var: return_var,
ext_var: err_ext_var,
name: "Err".into(),
arguments: vec![(
err_payload_var,
Loc::at_zero(Var(err_symbol, err_payload_var)),
)],
})),
}),
guard: None,
redundant: RedundantMark::known_non_redundant(),
};
let result_region = result_expr.region;
let when_expr = When {
loc_cond: result_expr,
cond_var: result_var,
expr_var: ok_payload_var,
region: result_region,
branches: vec![ok_branch, err_branch],
branches_cond_var: result_var,
exhaustive: ExhaustiveMark::known_exhaustive(),
};
with_hole(
env,
when_expr,
variable,
procs,
layout_cache,
assigned,
hole,
)
}
Return {
return_value,
return_var,

View file

@ -511,11 +511,13 @@ pub enum Expr<'a> {
continuation: &'a Loc<Expr<'a>>,
},
// This form of debug is a desugared call to roc_dbg
LowLevelDbg(&'a (&'a str, &'a str), &'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
/// The `try` keyword that performs early return on errors
Try,
// This form of try is a desugared Result unwrapper
LowLevelTry(&'a Loc<Expr<'a>>),
// This form of debug is a desugared call to roc_dbg
LowLevelDbg(&'a (&'a str, &'a str), &'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
// Application
/// To apply by name, do Apply(Var(...), ...)
@ -700,6 +702,7 @@ pub fn is_expr_suffixed(expr: &Expr) -> bool {
}
Expr::LowLevelDbg(_, a, b) => is_expr_suffixed(&a.value) || is_expr_suffixed(&b.value),
Expr::Try => false,
Expr::LowLevelTry(loc_expr) => is_expr_suffixed(&loc_expr.value),
Expr::UnaryOp(a, _) => is_expr_suffixed(&a.value),
Expr::When(cond, branches) => {
is_expr_suffixed(&cond.value) || branches.iter().any(|x| is_when_branch_suffixed(x))
@ -973,6 +976,9 @@ impl<'a, 'b> RecursiveValueDefIter<'a, 'b> {
expr_stack.push(&condition.value);
expr_stack.push(&cont.value);
}
LowLevelTry(loc_expr) => {
expr_stack.push(&loc_expr.value);
}
Return(return_value, after_return) => {
if let Some(after_return) = after_return {
expr_stack.reserve(2);
@ -2513,6 +2519,7 @@ impl<'a> Malformed for Expr<'a> {
DbgStmt { first, extra_args, continuation } => first.is_malformed() || extra_args.iter().any(|a| a.is_malformed()) || continuation.is_malformed(),
LowLevelDbg(_, condition, continuation) => condition.is_malformed() || continuation.is_malformed(),
Try => false,
LowLevelTry(loc_expr) => loc_expr.is_malformed(),
Return(return_value, after_return) => return_value.is_malformed() || after_return.is_some_and(|ar| ar.is_malformed()),
Apply(func, args, _) => func.is_malformed() || args.iter().any(|arg| arg.is_malformed()),
BinOps(firsts, last) => firsts.iter().any(|(expr, _)| expr.is_malformed()) || last.is_malformed(),

View file

@ -2180,6 +2180,7 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result<Pattern<
| Expr::Dbg
| Expr::DbgStmt { .. }
| Expr::LowLevelDbg(_, _, _)
| Expr::LowLevelTry(_)
| Expr::Return(_, _)
| Expr::MalformedSuffixed(..)
| Expr::PrecedenceConflict { .. }

View file

@ -727,6 +727,7 @@ impl<'a> Normalize<'a> for Expr<'a> {
arena.alloc(b.normalize(arena)),
),
Expr::Try => Expr::Try,
Expr::LowLevelTry(a) => Expr::LowLevelTry(arena.alloc(a.normalize(arena))),
Expr::Return(a, b) => Expr::Return(
arena.alloc(a.normalize(arena)),
b.map(|loc_b| &*arena.alloc(loc_b.normalize(arena))),

View file

@ -8,7 +8,7 @@ use roc_module::symbol::{ModuleId, Symbol};
use roc_parse::ast::Base;
use roc_parse::pattern::PatternType;
use roc_region::all::{Loc, Region};
use roc_types::types::AliasKind;
use roc_types::types::{AliasKind, EarlyReturnKind};
use crate::Severity;
@ -244,6 +244,7 @@ pub enum Problem {
},
ReturnOutsideOfFunction {
region: Region,
return_kind: EarlyReturnKind,
},
StatementsAfterReturn {
region: Region,
@ -504,7 +505,7 @@ impl Problem {
| Problem::OverAppliedDbg { region }
| Problem::UnappliedDbg { region }
| Problem::DefsOnlyUsedInRecursion(_, region)
| Problem::ReturnOutsideOfFunction { region }
| Problem::ReturnOutsideOfFunction { region, .. }
| Problem::StatementsAfterReturn { region }
| Problem::ReturnAtEndOfFunction { region }
| Problem::UnsuffixedEffectfulRecordField(region)

View file

@ -16,6 +16,7 @@ use roc_can::abilities::{AbilitiesStore, MemberSpecializationInfo};
use roc_can::constraint::Constraint::{self, *};
use roc_can::constraint::{
Cycle, FxCallConstraint, FxSuffixConstraint, FxSuffixKind, LetConstraint, OpportunisticResolve,
TryTargetConstraint,
};
use roc_can::expected::{Expected, PExpected};
use roc_can::module::ModuleParams;
@ -908,6 +909,95 @@ fn solve(
}
}
}
TryTarget(index) => {
let try_target_constraint = &env.constraints.try_target_constraints[index.index()];
let TryTargetConstraint {
target_type_index,
ok_payload_var,
err_payload_var,
region,
} = try_target_constraint;
let target_actual = either_type_index_to_var(
env,
rank,
problems,
abilities_store,
obligation_cache,
&mut can_types,
aliases,
*target_type_index,
);
let wanted_result_ty = can_types.from_old_type(&Type::TagUnion(
vec![
("Ok".into(), vec![Type::Variable(*ok_payload_var)]),
("Err".into(), vec![Type::Variable(*err_payload_var)]),
],
TypeExtension::Closed,
));
let wanted_result_var = type_to_var(
env,
rank,
problems,
abilities_store,
obligation_cache,
&mut can_types,
aliases,
wanted_result_ty,
);
match unify(
&mut env.uenv(),
target_actual,
wanted_result_var,
UnificationMode::EQ,
Polarity::OF_VALUE,
) {
Success {
vars,
must_implement_ability,
lambda_sets_to_specialize,
extra_metadata: _,
} => {
env.introduce(rank, &vars);
if !must_implement_ability.is_empty() {
let new_problems = obligation_cache.check_obligations(
env.subs,
abilities_store,
must_implement_ability,
AbilityImplError::BadExpr(
*region,
Category::TryTarget,
target_actual,
),
);
problems.extend(new_problems);
}
compact_lambdas_and_check_obligations(
env,
problems,
abilities_store,
obligation_cache,
awaiting_specializations,
lambda_sets_to_specialize,
);
state
}
Failure(vars, actual_type, _expected_type, _bad_impls) => {
env.introduce(rank, &vars);
let problem = TypeError::InvalidTryTarget(*region, actual_type);
problems.push(problem);
state
}
}
}
Let(index, pool_slice) => {
let let_con = &env.constraints.let_constraints[index.index()];

View file

@ -48,6 +48,7 @@ pub enum TypeError {
ExpectedEffectful(Region, ExpectEffectfulReason),
UnsuffixedEffectfulFunction(Region, FxSuffixKind),
SuffixedPureFunction(Region, FxSuffixKind),
InvalidTryTarget(Region, ErrorType),
}
impl TypeError {
@ -77,6 +78,7 @@ impl TypeError {
TypeError::FxInTopLevel(_, _) => Warning,
TypeError::UnsuffixedEffectfulFunction(_, _) => Warning,
TypeError::SuffixedPureFunction(_, _) => Warning,
TypeError::InvalidTryTarget(_, _) => RuntimeError,
}
}
@ -97,7 +99,8 @@ impl TypeError {
| TypeError::FxInTopLevel(region, _)
| TypeError::ExpectedEffectful(region, _)
| TypeError::UnsuffixedEffectfulFunction(region, _)
| TypeError::SuffixedPureFunction(region, _) => Some(*region),
| TypeError::SuffixedPureFunction(region, _)
| TypeError::InvalidTryTarget(region, _) => Some(*region),
TypeError::UnfulfilledAbility(ab, ..) => ab.region(),
TypeError::Exhaustive(e) => Some(e.region()),
TypeError::CircularDef(c) => c.first().map(|ce| ce.symbol_region),

View file

@ -24,25 +24,25 @@ procedure Str.66 (Str.191):
let Str.247 : [C {}, C U64] = TagId(0) Str.248;
ret Str.247;
procedure Test.3 (Test.4):
joinpoint Test.14 Test.5:
let Test.12 : [C {}, C U64] = TagId(1) Test.5;
ret Test.12;
procedure Test.1 (Test.2):
joinpoint Test.11 Test.3:
let Test.7 : [C {}, C U64] = TagId(1) Test.3;
ret Test.7;
in
let Test.13 : [C {}, C U64] = CallByName Str.26 Test.4;
let Test.18 : U8 = 1i64;
let Test.19 : U8 = GetTagId Test.13;
let Test.20 : Int1 = lowlevel Eq Test.18 Test.19;
if Test.20 then
let Test.6 : U64 = UnionAtIndex (Id 1) (Index 0) Test.13;
jump Test.14 Test.6;
let Test.10 : [C {}, C U64] = CallByName Str.26 Test.2;
let Test.15 : U8 = 1i64;
let Test.16 : U8 = GetTagId Test.10;
let Test.17 : Int1 = lowlevel Eq Test.15 Test.16;
if Test.17 then
let Test.8 : U64 = UnionAtIndex (Id 1) (Index 0) Test.10;
jump Test.11 Test.8;
else
let Test.7 : {} = UnionAtIndex (Id 0) (Index 0) Test.13;
let Test.17 : [C {}, C U64] = TagId(0) Test.7;
ret Test.17;
let Test.9 : {} = UnionAtIndex (Id 0) (Index 0) Test.10;
let Test.14 : [C {}, C U64] = TagId(0) Test.9;
ret Test.14;
procedure Test.0 ():
let Test.11 : Str = "123";
let Test.10 : [C {}, C U64] = CallByName Test.3 Test.11;
dec Test.11;
ret Test.10;
let Test.6 : Str = "123";
let Test.5 : [C {}, C U64] = CallByName Test.1 Test.6;
dec Test.6;
ret Test.5;

View file

@ -3483,6 +3483,7 @@ pub enum Reason {
CrashArg,
ImportParams(ModuleId),
FunctionOutput,
TryResult,
}
#[derive(PartialEq, Eq, Debug, Clone)]
@ -3532,10 +3533,21 @@ pub enum Category {
Expect,
Dbg,
Return,
TryTarget,
TrySuccess,
TryFailure,
Return(EarlyReturnKind),
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EarlyReturnKind {
Return,
Try,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatternCategory {
Record,

View file

@ -699,6 +699,7 @@ impl IterTokens for Loc<Expr<'_>> {
.chain(e2.iter_tokens(arena))
.collect_in(arena),
Expr::Try => onetoken(Token::Keyword, region, arena),
Expr::LowLevelTry(e1) => e1.iter_tokens(arena),
Expr::Apply(e1, e2, _called_via) => (e1.iter_tokens(arena).into_iter())
.chain(e2.iter_tokens(arena))
.collect_in(arena),

View file

@ -33,7 +33,7 @@ impl Problems {
const YELLOW: &str = ANSI_STYLE_CODES.yellow;
const RESET: &str = ANSI_STYLE_CODES.reset;
println!(
print!(
"{}{}{} {} and {}{}{} {} found in {} ms",
match self.errors {
0 => GREEN,

View file

@ -9,7 +9,7 @@ use roc_problem::can::{
};
use roc_problem::Severity;
use roc_region::all::{LineColumn, LineColumnRegion, LineInfo, Loc, Region};
use roc_types::types::AliasKind;
use roc_types::types::{AliasKind, EarlyReturnKind};
use std::path::PathBuf;
use crate::error::r#type::suggest;
@ -1350,18 +1350,25 @@ pub fn can_problem<'b>(
title = report.title;
}
Problem::ReturnOutsideOfFunction { region } => {
Problem::ReturnOutsideOfFunction {
region,
return_kind,
} => {
let return_keyword;
(title, return_keyword) = match return_kind {
EarlyReturnKind::Return => ("RETURN OUTSIDE OF FUNCTION".to_string(), "return"),
EarlyReturnKind::Try => ("TRY OUTSIDE OF FUNCTION".to_string(), "try"),
};
doc = alloc.stack([
alloc.concat([
alloc.reflow("This "),
alloc.keyword("return"),
alloc.reflow(" statement doesn't belong to a function:"),
alloc.keyword(return_keyword),
alloc.reflow(" doesn't belong to a function:"),
]),
alloc.region(lines.convert_region(region), severity),
alloc.reflow("I wouldn't know where to return to if I used it!"),
]);
title = "RETURN OUTSIDE OF FUNCTION".to_string();
}
Problem::StatementsAfterReturn { region } => {

View file

@ -1005,7 +1005,7 @@ fn to_unfinished_lambda_report<'a>(
let doc = alloc.stack([
alloc.concat([
alloc.reflow(r"I was partway through parsing a "),
alloc.reflow(r" function, but I got stuck here:"),
alloc.reflow(r"function, but I got stuck here:"),
]),
alloc.region_with_subregion(lines.convert_region(surroundings), region, severity),
message,

View file

@ -21,8 +21,8 @@ use roc_solve_problem::{
use roc_std::RocDec;
use roc_types::pretty_print::{Parens, WILDCARD};
use roc_types::types::{
AbilitySet, AliasKind, Category, ErrorType, IndexOrField, PatternCategory, Polarity, Reason,
RecordField, TypeExt,
AbilitySet, AliasKind, Category, EarlyReturnKind, ErrorType, IndexOrField, PatternCategory,
Polarity, Reason, RecordField, TypeExt,
};
use std::path::PathBuf;
use ven_pretty::{text, DocAllocator};
@ -472,6 +472,37 @@ pub fn type_problem<'b>(
severity,
})
}
InvalidTryTarget(region, actual_type) => {
let stack = [
alloc.concat([
alloc.reflow("This expression cannot be used as a "),
alloc.keyword("try"),
alloc.reflow(" target:"),
]),
alloc.region(lines.convert_region(region), severity),
alloc.concat([
alloc.reflow("I expected a "),
alloc.type_str("Result"),
alloc.reflow(", but it actually has type:"),
]),
alloc.type_block(error_type_to_doc(alloc, actual_type)),
alloc.concat([
alloc.hint(""),
alloc.reflow("Did you forget to wrap the value with an "),
alloc.tag("Ok".into()),
alloc.reflow(" or an "),
alloc.tag("Err".into()),
alloc.reflow(" tag?"),
]),
];
Some(Report {
title: "INVALID TRY TARGET".to_string(),
filename,
doc: alloc.stack(stack),
severity,
})
}
}
}
@ -1237,8 +1268,8 @@ fn to_expr_report<'b>(
&category,
found,
expected_type,
region,
Some(expr_region),
expr_region,
Some(region),
alloc.concat([
alloc.reflow("The "),
alloc.string(index.ordinal()),
@ -1526,8 +1557,8 @@ fn to_expr_report<'b>(
&category,
found,
expected_type,
region,
Some(expr_region),
expr_region,
Some(region),
alloc.concat([
alloc.string(format!("This {argument} to ")),
this_function.clone(),
@ -1806,11 +1837,8 @@ fn to_expr_report<'b>(
Reason::FunctionOutput => {
let problem = alloc.concat([
alloc.text("This "),
alloc.keyword("return"),
alloc.reflow(
" statement doesn't match the return type of its enclosing function:",
),
alloc.reflow("This returns something that's incompatible "),
alloc.reflow("with the return type of the enclosing function:"),
]);
let comparison = type_comparison(
@ -1878,6 +1906,45 @@ fn to_expr_report<'b>(
severity,
}
}
Reason::TryResult => {
let problem = alloc.concat([
alloc.text("This "),
alloc.keyword("try"),
alloc.reflow(
" statement doesn't match the return type of its enclosing function:",
),
]);
let comparison = type_comparison(
alloc,
found,
expected_type,
ExpectationContext::Arbitrary,
add_category(alloc, alloc.text("It is"), &category),
alloc.concat([
alloc.reflow("But I need every "),
alloc.keyword("return"),
alloc.reflow(" statement in that function to return:"),
]),
None,
);
Report {
title: "TYPE MISMATCH".to_string(),
filename,
doc: alloc.stack([
problem,
alloc.region_with_subregion(
lines.convert_region(region),
lines.convert_region(expr_region),
severity,
),
comparison,
]),
severity,
}
}
},
}
}
@ -2216,10 +2283,45 @@ fn format_category<'b>(
alloc.concat([this_is, alloc.text(" a dbg statement")]),
alloc.text(" of type:"),
),
Return => (
Return(EarlyReturnKind::Return) => (
alloc.concat([text!(alloc, "{}his", t), alloc.reflow(" returns a value")]),
alloc.text(" of type:"),
),
Return(EarlyReturnKind::Try) => (
alloc.concat([
text!(alloc, "{}his", t),
alloc.reflow(" returns an "),
alloc.tag_name("Err".into()),
]),
alloc.text(" of type:"),
),
TryTarget => (
alloc.concat([
this_is,
alloc.reflow(" a "),
alloc.keyword("try"),
alloc.reflow(" target"),
]),
alloc.text(" of type:"),
),
TrySuccess => (
alloc.concat([
this_is,
alloc.reflow(" a "),
alloc.keyword("try"),
alloc.reflow(" expression"),
]),
alloc.text(" that succeeds with type:"),
),
TryFailure => (
alloc.concat([
this_is,
alloc.reflow(" a "),
alloc.keyword("try"),
alloc.reflow(" expression"),
]),
alloc.text(" that fails with type:"),
),
}
}