Issue 7089: ?? operator

This commit is contained in:
Anthony Bullard 2024-12-23 07:00:33 -06:00
parent 57cab7f69a
commit c70ceb4f98
No known key found for this signature in database
7 changed files with 367 additions and 16 deletions

View file

@ -6,7 +6,7 @@ use crate::suffixed::{apply_try_function, unwrap_suffixed_expression, EUnwrapped
use bumpalo::collections::Vec; use bumpalo::collections::Vec;
use bumpalo::Bump; use bumpalo::Bump;
use roc_error_macros::internal_error; use roc_error_macros::internal_error;
use roc_module::called_via::BinOp::Pizza; use roc_module::called_via::BinOp::{DoubleQuestion, Pizza};
use roc_module::called_via::{BinOp, CalledVia}; use roc_module::called_via::{BinOp, CalledVia};
use roc_module::ident::ModuleName; use roc_module::ident::ModuleName;
use roc_parse::ast::Expr::{self, *}; use roc_parse::ast::Expr::{self, *};
@ -154,6 +154,61 @@ fn new_op_call_expr<'a>(
} }
} }
} }
DoubleQuestion => {
let left = desugar_expr(env, scope, left);
let right = desugar_expr(env, scope, right);
let mut branches = Vec::with_capacity_in(2, env.arena);
let mut branch_1_patts = Vec::with_capacity_in(1, env.arena);
let mut branch_1_patts_args = Vec::with_capacity_in(1, env.arena);
let success_var = env.arena.alloc_str(
format!(
"success_BRANCH1_{}_{}",
left.region.start().offset,
left.region.end().offset
)
.as_str(),
);
branch_1_patts_args.push(Loc::at(
left.region,
Pattern::Identifier { ident: success_var },
));
let branch_1_tag: &Loc<Pattern<'a>> =
env.arena.alloc(Loc::at(left.region, Pattern::Tag("Ok")));
branch_1_patts.push(Loc::at(
left.region,
Pattern::Apply(branch_1_tag, branch_1_patts_args.into_bump_slice()),
));
let branch_one: &WhenBranch<'_> = env.arena.alloc(WhenBranch {
patterns: branch_1_patts.into_bump_slice(),
value: Loc::at(
left.region,
Expr::Var {
module_name: "",
ident: success_var,
},
),
guard: None,
});
branches.push(branch_one);
let mut branch_2_patts = Vec::with_capacity_in(1, env.arena);
let mut branch_2_patts_args = Vec::with_capacity_in(1, env.arena);
branch_2_patts_args.push(Loc::at(right.region, Pattern::Underscore("")));
let branch_2_tag: &Loc<Pattern<'a>> =
env.arena.alloc(Loc::at(left.region, Pattern::Tag("Err")));
branch_2_patts.push(Loc::at(
right.region,
Pattern::Apply(branch_2_tag, branch_2_patts_args.into_bump_slice()),
));
let branch_two: &WhenBranch<'_> = env.arena.alloc(WhenBranch {
patterns: branch_2_patts.into_bump_slice(),
value: *right,
guard: None,
});
branches.push(branch_two);
When(left, branches.into_bump_slice())
}
binop => { binop => {
let left = desugar_expr(env, scope, left); let left = desugar_expr(env, scope, left);
let right = desugar_expr(env, scope, right); let right = desugar_expr(env, scope, right);
@ -1601,6 +1656,7 @@ fn binop_to_function(binop: BinOp) -> (&'static str, &'static str) {
And => (ModuleName::BOOL, "and"), And => (ModuleName::BOOL, "and"),
Or => (ModuleName::BOOL, "or"), Or => (ModuleName::BOOL, "or"),
Pizza => unreachable!("Cannot desugar the |> operator"), Pizza => unreachable!("Cannot desugar the |> operator"),
DoubleQuestion => unreachable!("Cannot desugar the ?? operator"),
} }
} }

View file

@ -0,0 +1,180 @@
---
source: crates/compiler/can/tests/test_suffixed.rs
expression: snapshot
---
Defs {
tags: [
EitherIndex(2147483648),
],
regions: [
@0-85,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 1 },
],
spaces: [
Newline,
],
type_defs: [],
value_defs: [
Body(
@0-4 Identifier {
ident: "main",
},
@11-69 Apply(
@11-69 Var {
module_name: "Task",
ident: "await",
},
[
@11-69 Defs(
Defs {
tags: [
EitherIndex(2147483648),
],
regions: [
@11-69,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [],
value_defs: [
AnnotatedBody {
ann_pattern: @11-69 Identifier {
ident: "#!0_stmt",
},
ann_type: @11-69 Apply(
"",
"Task",
[
@11-69 Record {
fields: [],
ext: None,
},
@11-69 Inferred,
],
),
lines_between: [],
body_pattern: @11-69 Identifier {
ident: "#!0_stmt",
},
body_expr: @11-69 Apply(
@11-69 Var {
module_name: "",
ident: "line",
},
[
@11-56 Apply(
@47-56 Var {
module_name: "Num",
ident: "toStr",
},
[
@11-39 Apply(
@24-39 When(
@24-32 Var {
module_name: "Str",
ident: "toU8",
},
[
WhenBranch {
patterns: [
@24-32 Apply(
@24-32 Tag(
"Ok",
),
[
@24-32 Identifier {
ident: "success_BRANCH1_24_32",
},
],
),
],
value: @24-32 Var {
module_name: "",
ident: "success_BRANCH1_24_32",
},
guard: None,
},
WhenBranch {
patterns: [
@36-39 Apply(
@24-32 Tag(
"Err",
),
[
@36-39 Underscore(
"",
),
],
),
],
value: @36-39 Num(
"255",
),
guard: None,
},
],
),
[
@11-16 Str(
PlainLine(
"123",
),
),
],
BinOp(
Pizza,
),
),
],
BinOp(
Pizza,
),
),
],
BinOp(
Pizza,
),
),
},
],
},
@11-69 Var {
module_name: "",
ident: "#!0_stmt",
},
),
@11-69 Closure(
[
@11-69 Underscore(
"#!stmt",
),
],
@75-85 Apply(
@75-82 Var {
module_name: "Task",
ident: "ok",
},
[
@83-85 Record(
[],
),
],
Space,
),
),
],
BangSuffix,
),
),
],
}

View file

@ -22,7 +22,7 @@ mod test_can {
use roc_problem::can::{CycleEntry, FloatErrorKind, IntErrorKind, Problem, RuntimeError}; use roc_problem::can::{CycleEntry, FloatErrorKind, IntErrorKind, Problem, RuntimeError};
use roc_region::all::{Loc, Position, Region}; use roc_region::all::{Loc, Position, Region};
use roc_types::subs::Variable; use roc_types::subs::Variable;
use std::{f64, i64}; use std::{assert_eq, f64, i64};
fn assert_can_runtime_error(input: &str, expected: RuntimeError) { fn assert_can_runtime_error(input: &str, expected: RuntimeError) {
let arena = Bump::new(); let arena = Bump::new();
@ -971,6 +971,47 @@ mod test_can {
assert_str_value(&cond_args[0].1.value, "123"); assert_str_value(&cond_args[0].1.value, "123");
} }
#[test]
fn try_desugar_double_question_suffix() {
let src = indoc!(
r#"
Str.toU64 "123" ?? Num.maxU64
"#
);
let arena = Bump::new();
let out = can_expr_with(&arena, test_home(), src);
assert_eq!(out.problems, Vec::new());
// Assert that we desugar to:
//
// when Str.toU64 "123"
// Ok success_BRANCH1_0_9 -> success_BRANCH1_0_9
// Err _ -> Num.maxU64
let (cond_expr, branches) = assert_when(&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_eq!(branches[1].patterns.len(), 1);
assert_pattern_tag_apply_with_ident(
&branches[0].patterns[0].pattern.value,
"Ok",
"success_BRANCH1_0_15",
&out.interns,
);
assert_var_usage(
&branches[0].value.value,
"success_BRANCH1_0_15",
&out.interns,
);
assert_pattern_tag_apply_with_underscore(&branches[1].patterns[0].pattern.value, "Err");
assert_var_usage(&branches[1].value.value, "maxU64", &out.interns);
}
#[test] #[test]
fn try_desugar_works_elsewhere() { fn try_desugar_works_elsewhere() {
let src = indoc!( let src = indoc!(
@ -1090,6 +1131,51 @@ mod test_can {
} }
} }
fn assert_pattern_tag_apply_with_ident(
pattern: &Pattern,
name: &str,
ident: &str,
interns: &roc_module::symbol::Interns,
) {
match pattern {
Pattern::AppliedTag {
tag_name,
arguments,
..
} if arguments.len() == 1 => {
assert_eq!(tag_name.as_ident_str().as_str(), name);
match arguments[0].1.value {
Pattern::Identifier(sym) => assert_eq!(sym.as_str(interns), ident),
_ => panic!(
"The tag was expected to be applied with {:?} but we instead found {:?}",
ident, arguments[0].1.value
),
}
}
_ => panic!("Pattern was not an applied tag: {:?}", pattern),
}
}
fn assert_pattern_tag_apply_with_underscore(pattern: &Pattern, name: &str) {
match pattern {
Pattern::AppliedTag {
tag_name,
arguments,
..
} if arguments.len() == 1 => {
assert_eq!(tag_name.as_ident_str().as_str(), name);
match arguments[0].1.value {
Pattern::Underscore => {},
_ => panic!(
"The tag was expected to be applied with an underscore but we instead found {:?}",
arguments[0].1.value
),
}
}
_ => panic!("Pattern was not an applied tag: {:?}", pattern),
}
}
fn assert_when(expr: &Expr) -> (&Expr, &Vec<WhenBranch>) { fn assert_when(expr: &Expr) -> (&Expr, &Vec<WhenBranch>) {
match expr { match expr {
Expr::When { Expr::When {

View file

@ -150,6 +150,28 @@ mod suffixed_tests {
); );
} }
/**
* Example of unwrapping a Result with ?? operator
*
* Note that ?? is desugared into a when expression,
* however this also tests the parser.
*
*/
#[test]
fn simple_double_question() {
run_test!(
r#"
main =
"123"
|> Str.toU8 ?? 255
|> Num.toStr
|> line!
Task.ok {}
"#
);
}
/** /**
* Example with a parens suffixed sub-expression * Example with a parens suffixed sub-expression
* in the function part of an Apply. * in the function part of an Apply.

View file

@ -941,6 +941,7 @@ fn push_op(buf: &mut Buf, op: BinOp) {
called_via::BinOp::And => buf.push_str("&&"), called_via::BinOp::And => buf.push_str("&&"),
called_via::BinOp::Or => buf.push_str("||"), called_via::BinOp::Or => buf.push_str("||"),
called_via::BinOp::Pizza => buf.push_str("|>"), called_via::BinOp::Pizza => buf.push_str("|>"),
called_via::BinOp::DoubleQuestion => buf.push_str("??"),
} }
} }
@ -2223,7 +2224,8 @@ pub fn sub_expr_requests_parens(expr: &Expr<'_>) -> bool {
| BinOp::GreaterThanOrEq | BinOp::GreaterThanOrEq
| BinOp::And | BinOp::And
| BinOp::Or | BinOp::Or
| BinOp::Pizza => true, | BinOp::Pizza
| BinOp::DoubleQuestion => true,
}) })
} }
Expr::If { .. } => true, Expr::If { .. } => true,

View file

@ -3,7 +3,7 @@ use self::BinOp::*;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt; use std::fmt;
const PRECEDENCES: [(BinOp, u8); 16] = [ const PRECEDENCES: [(BinOp, u8); 17] = [
(Caret, 8), (Caret, 8),
(Star, 7), (Star, 7),
(Slash, 7), (Slash, 7),
@ -11,6 +11,7 @@ const PRECEDENCES: [(BinOp, u8); 16] = [
(Percent, 6), (Percent, 6),
(Plus, 5), (Plus, 5),
(Minus, 5), (Minus, 5),
(DoubleQuestion, 5),
(Pizza, 4), (Pizza, 4),
(Equals, 3), (Equals, 3),
(NotEquals, 3), (NotEquals, 3),
@ -22,7 +23,7 @@ const PRECEDENCES: [(BinOp, u8); 16] = [
(Or, 0), (Or, 0),
]; ];
const ASSOCIATIVITIES: [(BinOp, Associativity); 16] = [ const ASSOCIATIVITIES: [(BinOp, Associativity); 17] = [
(Caret, RightAssociative), (Caret, RightAssociative),
(Star, LeftAssociative), (Star, LeftAssociative),
(Slash, LeftAssociative), (Slash, LeftAssociative),
@ -30,6 +31,7 @@ const ASSOCIATIVITIES: [(BinOp, Associativity); 16] = [
(Percent, LeftAssociative), (Percent, LeftAssociative),
(Plus, LeftAssociative), (Plus, LeftAssociative),
(Minus, LeftAssociative), (Minus, LeftAssociative),
(DoubleQuestion, LeftAssociative),
(Pizza, LeftAssociative), (Pizza, LeftAssociative),
(Equals, NonAssociative), (Equals, NonAssociative),
(NotEquals, NonAssociative), (NotEquals, NonAssociative),
@ -41,7 +43,7 @@ const ASSOCIATIVITIES: [(BinOp, Associativity); 16] = [
(Or, RightAssociative), (Or, RightAssociative),
]; ];
const DISPLAY_STRINGS: [(BinOp, &str); 16] = [ const DISPLAY_STRINGS: [(BinOp, &str); 17] = [
(Caret, "^"), (Caret, "^"),
(Star, "*"), (Star, "*"),
(Slash, "/"), (Slash, "/"),
@ -49,6 +51,7 @@ const DISPLAY_STRINGS: [(BinOp, &str); 16] = [
(Percent, "%"), (Percent, "%"),
(Plus, "+"), (Plus, "+"),
(Minus, "-"), (Minus, "-"),
(DoubleQuestion, "??"),
(Pizza, "|>"), (Pizza, "|>"),
(Equals, "=="), (Equals, "=="),
(NotEquals, "!="), (NotEquals, "!="),
@ -152,6 +155,7 @@ pub enum BinOp {
Slash, Slash,
DoubleSlash, DoubleSlash,
Percent, Percent,
DoubleQuestion,
Plus, Plus,
Minus, Minus,
Pizza, Pizza,
@ -172,7 +176,7 @@ impl BinOp {
match self { match self {
Caret | Star | Slash | Percent | Plus | Minus | LessThan | GreaterThan => 1, Caret | Star | Slash | Percent | Plus | Minus | LessThan | GreaterThan => 1,
DoubleSlash | Equals | NotEquals | LessThanOrEq | GreaterThanOrEq | And | Or DoubleSlash | Equals | NotEquals | LessThanOrEq | GreaterThanOrEq | And | Or
| Pizza => 2, | Pizza | DoubleQuestion => 2,
} }
} }
} }
@ -206,13 +210,13 @@ pub enum Associativity {
impl BinOp { impl BinOp {
pub fn associativity(self) -> Associativity { pub fn associativity(self) -> Associativity {
const ASSOCIATIVITY_TABLE: [Associativity; 16] = generate_associativity_table(); const ASSOCIATIVITY_TABLE: [Associativity; 17] = generate_associativity_table();
ASSOCIATIVITY_TABLE[self as usize] ASSOCIATIVITY_TABLE[self as usize]
} }
fn precedence(self) -> u8 { fn precedence(self) -> u8 {
const PRECEDENCE_TABLE: [u8; 16] = generate_precedence_table(); const PRECEDENCE_TABLE: [u8; 17] = generate_precedence_table();
PRECEDENCE_TABLE[self as usize] PRECEDENCE_TABLE[self as usize]
} }
@ -232,14 +236,14 @@ impl Ord for BinOp {
impl std::fmt::Display for BinOp { impl std::fmt::Display for BinOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const DISPLAY_TABLE: [&str; 16] = generate_display_table(); const DISPLAY_TABLE: [&str; 17] = generate_display_table();
write!(f, "{}", DISPLAY_TABLE[*self as usize]) write!(f, "{}", DISPLAY_TABLE[*self as usize])
} }
} }
const fn generate_precedence_table() -> [u8; 16] { const fn generate_precedence_table() -> [u8; 17] {
let mut table = [0u8; 16]; let mut table = [0u8; 17];
let mut i = 0; let mut i = 0;
while i < PRECEDENCES.len() { while i < PRECEDENCES.len() {
@ -250,8 +254,8 @@ const fn generate_precedence_table() -> [u8; 16] {
table table
} }
const fn generate_associativity_table() -> [Associativity; 16] { const fn generate_associativity_table() -> [Associativity; 17] {
let mut table = [NonAssociative; 16]; let mut table = [NonAssociative; 17];
let mut i = 0; let mut i = 0;
while i < ASSOCIATIVITIES.len() { while i < ASSOCIATIVITIES.len() {
@ -262,8 +266,8 @@ const fn generate_associativity_table() -> [Associativity; 16] {
table table
} }
const fn generate_display_table() -> [&'static str; 16] { const fn generate_display_table() -> [&'static str; 17] {
let mut table = [""; 16]; let mut table = [""; 17];
let mut i = 0; let mut i = 0;
while i < DISPLAY_STRINGS.len() { while i < DISPLAY_STRINGS.len() {

View file

@ -4046,6 +4046,7 @@ where
"=" => good!(OperatorOrDef::Assignment, 1), "=" => good!(OperatorOrDef::Assignment, 1),
":=" => good!(OperatorOrDef::AliasOrOpaque(AliasOrOpaque::Opaque), 2), ":=" => good!(OperatorOrDef::AliasOrOpaque(AliasOrOpaque::Opaque), 2),
":" => good!(OperatorOrDef::AliasOrOpaque(AliasOrOpaque::Alias), 1), ":" => good!(OperatorOrDef::AliasOrOpaque(AliasOrOpaque::Alias), 1),
"??" => good!(OperatorOrDef::BinOp(BinOp::DoubleQuestion), 2),
"|>" => good!(OperatorOrDef::BinOp(BinOp::Pizza), 2), "|>" => good!(OperatorOrDef::BinOp(BinOp::Pizza), 2),
"==" => good!(OperatorOrDef::BinOp(BinOp::Equals), 2), "==" => good!(OperatorOrDef::BinOp(BinOp::Equals), 2),
"!=" => good!(OperatorOrDef::BinOp(BinOp::NotEquals), 2), "!=" => good!(OperatorOrDef::BinOp(BinOp::NotEquals), 2),