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

@ -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 {