From eedade8e81ff5ebcb89b54244e11bfbab41cb7fd Mon Sep 17 00:00:00 2001 From: Sam Mohr Date: Mon, 2 Dec 2024 03:20:44 -0800 Subject: [PATCH] Initial working version of proper `try` keyword --- crates/cli/src/main.rs | 2 + crates/cli/tests/cli_tests.rs | 2 +- ...ple_zig__module_params_arity_mismatch.snap | 6 +- ...orm_simple_zig__module_params_bad_ann.snap | 6 +- ...mple_zig__module_params_unexpected_fn.snap | 11 +- crates/compiler/can/src/constraint.rs | 37 +- crates/compiler/can/src/copy.rs | 18 +- crates/compiler/can/src/debug/pretty_print.rs | 1 + crates/compiler/can/src/desugar.rs | 91 +---- crates/compiler/can/src/expr.rs | 75 +++- crates/compiler/can/src/module.rs | 11 +- crates/compiler/can/src/scope.rs | 4 +- crates/compiler/can/src/traverse.rs | 10 + crates/compiler/can/tests/test_can.rs | 172 +-------- crates/compiler/constrain/src/expr.rs | 346 ++++++++++-------- crates/compiler/fmt/src/expr.rs | 14 +- crates/compiler/load/tests/test_reporting.rs | 337 ++++++++++++++--- crates/compiler/lower_params/src/lower.rs | 10 + .../compiler/lower_params/src/type_error.rs | 6 +- crates/compiler/mono/src/ir.rs | 83 ++++- crates/compiler/parse/src/ast.rs | 13 +- crates/compiler/parse/src/expr.rs | 1 + crates/compiler/parse/src/normalize.rs | 1 + crates/compiler/problem/src/can.rs | 5 +- crates/compiler/solve/src/solve.rs | 90 +++++ crates/compiler/solve_problem/src/lib.rs | 5 +- .../test_mono/generated/return_annotated.txt | 36 +- crates/compiler/types/src/types.rs | 14 +- crates/language_server/src/analysis/tokens.rs | 1 + crates/reporting/src/cli.rs | 2 +- crates/reporting/src/error/canonicalize.rs | 19 +- crates/reporting/src/error/parse.rs | 2 +- crates/reporting/src/error/type.rs | 126 ++++++- 33 files changed, 1036 insertions(+), 521 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 17a9b5b43a1..499e55fa2e2 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -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(); } diff --git a/crates/cli/tests/cli_tests.rs b/crates/cli/tests/cli_tests.rs index faf159ff616..e55ea65cdf6 100644 --- a/crates/cli/tests/cli_tests.rs +++ b/crates/cli/tests/cli_tests.rs @@ -334,7 +334,7 @@ mod cli_tests { ); let expected_out = - "0 error and 0 warning found in ms\n0 error and 0 warning found in ms\n"; + "0 errors and 0 warnings found in ms.\n\n0 errors and 0 warnings found in ms.\n\n"; cli_build.run().assert_clean_stdout(expected_out); } diff --git a/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_arity_mismatch.snap b/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_arity_mismatch.snap index 91f75346796..b43bfdcbe5e 100644 --- a/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_arity_mismatch.snap +++ b/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_arity_mismatch.snap @@ -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 ms -. +3 errors and 0 warnings found in ms. You can run - diff --git a/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_bad_ann.snap b/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_bad_ann.snap index 175f22fe7a8..6abc4bcf2bd 100644 --- a/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_bad_ann.snap +++ b/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_bad_ann.snap @@ -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 ms -. +2 errors and 1 warning found in ms. You can run - diff --git a/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_unexpected_fn.snap b/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_unexpected_fn.snap index 0c38fbfe954..55ed00e6a1a 100644 --- a/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_unexpected_fn.snap +++ b/crates/cli/tests/snapshots/cli_tests__cli_tests__test_platform_simple_zig__module_params_unexpected_fn.snap @@ -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 ms -. +1 error and 0 warnings found in ms. You can run - diff --git a/crates/compiler/can/src/constraint.rs b/crates/compiler/can/src/constraint.rs index d71d56f0c03..1c1bdbd0489 100644 --- a/crates/compiler/can/src/constraint.rs +++ b/crates/compiler/can/src/constraint.rs @@ -31,6 +31,7 @@ pub struct Constraints { pub cycles: Vec, pub fx_call_constraints: Vec, pub fx_suffix_constraints: Vec, + pub try_target_constraints: Vec, } 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), /// 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(), diff --git a/crates/compiler/can/src/copy.rs b/crates/compiler/can/src/copy.rs index 2d9a75770c6..3a64709a2b2 100644 --- a/crates/compiler/can/src/copy.rs +++ b/crates/compiler/can/src/copy.rs @@ -479,7 +479,7 @@ fn deep_copy_expr_help(env: &mut C, copied: &mut Vec, 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(env: &mut C, copied: &mut Vec, 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()), } } diff --git a/crates/compiler/can/src/debug/pretty_print.rs b/crates/compiler/can/src/debug/pretty_print.rs index 80f66c07684..9c91d48a8bc 100644 --- a/crates/compiler/can/src/debug/pretty_print.rs +++ b/crates/compiler/can/src/debug/pretty_print.rs @@ -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!(), } diff --git a/crates/compiler/can/src/desugar.rs b/crates/compiler/can/src/desugar.rs index 2c4bf6e7d84..1717191de73 100644 --- a/crates/compiler/can/src/desugar.rs +++ b/crates/compiler/can/src/desugar.rs @@ -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>, -) -> Loc> { - 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, diff --git a/crates/compiler/can/src/expr.rs b/crates/compiler/can/src/expr.rs index 1c50a2234bc..2e66697d794 100644 --- a/crates/compiler/can/src/expr.rs +++ b/crates/compiler/can/src/expr.rs @@ -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>, + result_var: Variable, + return_var: Variable, + ok_payload_var: Variable, + err_payload_var: Variable, + err_ext_var: Variable, + }, + Return { return_value: Box>, 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)>, } @@ -3543,6 +3603,9 @@ pub(crate) fn get_lookup_symbols(expr: &Expr) -> Vec { // 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); } diff --git a/crates/compiler/can/src/module.rs b/crates/compiler/can/src/module.rs index 12411c088b9..56610dcee8b 100644 --- a/crates/compiler/can/src/module.rs +++ b/crates/compiler/can/src/module.rs @@ -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, diff --git a/crates/compiler/can/src/scope.rs b/crates/compiler/can/src/scope.rs index fe6608f4ffe..c6310f26819 100644 --- a/crates/compiler/can/src/scope.rs +++ b/crates/compiler/can/src/scope.rs @@ -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, - pub early_returns: Vec<(Variable, Region)>, + pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>, } impl Scope { diff --git a/crates/compiler/can/src/traverse.rs b/crates/compiler/can/src/traverse.rs index c27e6fb8f42..0bdd2ffa969 100644 --- a/crates/compiler/can/src/traverse.rs +++ b/crates/compiler/can/src/traverse.rs @@ -400,6 +400,16 @@ pub fn walk_expr(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, diff --git a/crates/compiler/can/tests/test_can.rs b/crates/compiler/can/tests/test_can.rs index 14f893ddef4..5b2264c7e9f 100644 --- a/crates/compiler/can/tests/test_can.rs +++ b/crates/compiler/can/tests/test_can.rs @@ -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 { diff --git a/crates/compiler/constrain/src/expr.rs b/crates/compiler/constrain/src/expr.rs index a271afffece..5b269bf9ded 100644 --- a/crates/compiler/constrain/src/expr.rs +++ b/crates/compiler/constrain/src/expr.rs @@ -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)], loc_body_expr: &Loc, 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, + 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, 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, 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); diff --git a/crates/compiler/fmt/src/expr.rs b/crates/compiler/fmt/src/expr.rs index 6809f263866..694b48eae45 100644 --- a/crates/compiler/fmt/src/expr.rs +++ b/crates/compiler/fmt/src/expr.rs @@ -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, _) diff --git a/crates/compiler/load/tests/test_reporting.rs b/crates/compiler/load/tests/test_reporting.rs index 82bb13101ca..f985f6e678d 100644 --- a/crates/compiler/load/tests/test_reporting.rs +++ b/crates/compiler/load/tests/test_reporting.rs @@ -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!( diff --git a/crates/compiler/lower_params/src/lower.rs b/crates/compiler/lower_params/src/lower.rs index cdd03db47fd..7e62349a57a 100644 --- a/crates/compiler/lower_params/src/lower.rs +++ b/crates/compiler/lower_params/src/lower.rs @@ -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: _, diff --git a/crates/compiler/lower_params/src/type_error.rs b/crates/compiler/lower_params/src/type_error.rs index 70e0a4d2844..56607b05d64 100644 --- a/crates/compiler/lower_params/src/type_error.rs +++ b/crates/compiler/lower_params/src/type_error.rs @@ -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 => {} } } diff --git a/crates/compiler/mono/src/ir.rs b/crates/compiler/mono/src/ir.rs index 125b08870a8..547f7e56f50 100644 --- a/crates/compiler/mono/src/ir.rs +++ b/crates/compiler/mono/src/ir.rs @@ -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, symbol: Symbol, ) -> (Symbol, Loc) { - 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, diff --git a/crates/compiler/parse/src/ast.rs b/crates/compiler/parse/src/ast.rs index f67a95c5736..3f49f82da38 100644 --- a/crates/compiler/parse/src/ast.rs +++ b/crates/compiler/parse/src/ast.rs @@ -511,11 +511,13 @@ pub enum Expr<'a> { continuation: &'a Loc>, }, - // This form of debug is a desugared call to roc_dbg - LowLevelDbg(&'a (&'a str, &'a str), &'a Loc>, &'a Loc>), - /// The `try` keyword that performs early return on errors Try, + // This form of try is a desugared Result unwrapper + LowLevelTry(&'a Loc>), + + // This form of debug is a desugared call to roc_dbg + LowLevelDbg(&'a (&'a str, &'a str), &'a Loc>, &'a Loc>), // 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(), diff --git a/crates/compiler/parse/src/expr.rs b/crates/compiler/parse/src/expr.rs index 2d66fc9baef..7cc8845b171 100644 --- a/crates/compiler/parse/src/expr.rs +++ b/crates/compiler/parse/src/expr.rs @@ -2180,6 +2180,7 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result 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))), diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs index 73496278f76..fdf6d90bf8b 100644 --- a/crates/compiler/problem/src/can.rs +++ b/crates/compiler/problem/src/can.rs @@ -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) diff --git a/crates/compiler/solve/src/solve.rs b/crates/compiler/solve/src/solve.rs index e45907f8ca5..ffd621172ce 100644 --- a/crates/compiler/solve/src/solve.rs +++ b/crates/compiler/solve/src/solve.rs @@ -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()]; diff --git a/crates/compiler/solve_problem/src/lib.rs b/crates/compiler/solve_problem/src/lib.rs index 2bffc881663..64b02d20bed 100644 --- a/crates/compiler/solve_problem/src/lib.rs +++ b/crates/compiler/solve_problem/src/lib.rs @@ -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), diff --git a/crates/compiler/test_mono/generated/return_annotated.txt b/crates/compiler/test_mono/generated/return_annotated.txt index c87a64563dd..b384d1aafd6 100644 --- a/crates/compiler/test_mono/generated/return_annotated.txt +++ b/crates/compiler/test_mono/generated/return_annotated.txt @@ -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; diff --git a/crates/compiler/types/src/types.rs b/crates/compiler/types/src/types.rs index 9af9f5aa28e..ce413149623 100644 --- a/crates/compiler/types/src/types.rs +++ b/crates/compiler/types/src/types.rs @@ -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, diff --git a/crates/language_server/src/analysis/tokens.rs b/crates/language_server/src/analysis/tokens.rs index 1d9bc2fff41..807728e8fef 100644 --- a/crates/language_server/src/analysis/tokens.rs +++ b/crates/language_server/src/analysis/tokens.rs @@ -699,6 +699,7 @@ impl IterTokens for Loc> { .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), diff --git a/crates/reporting/src/cli.rs b/crates/reporting/src/cli.rs index 911635dceac..4ee6b89908a 100644 --- a/crates/reporting/src/cli.rs +++ b/crates/reporting/src/cli.rs @@ -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, diff --git a/crates/reporting/src/error/canonicalize.rs b/crates/reporting/src/error/canonicalize.rs index eed1ee9b030..1c56e3b8f73 100644 --- a/crates/reporting/src/error/canonicalize.rs +++ b/crates/reporting/src/error/canonicalize.rs @@ -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 } => { diff --git a/crates/reporting/src/error/parse.rs b/crates/reporting/src/error/parse.rs index 62a36dda738..5820df4ee1a 100644 --- a/crates/reporting/src/error/parse.rs +++ b/crates/reporting/src/error/parse.rs @@ -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, diff --git a/crates/reporting/src/error/type.rs b/crates/reporting/src/error/type.rs index 2fe6c652a08..e04cc3662a2 100644 --- a/crates/reporting/src/error/type.rs +++ b/crates/reporting/src/error/type.rs @@ -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:"), + ), } }