mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-29 14:54:47 +00:00
Implement ? binop operator
This commit is contained in:
parent
9d37c906fe
commit
b73e4387ae
12 changed files with 291 additions and 55 deletions
|
@ -4,7 +4,7 @@ use crate::env::Env;
|
|||
use crate::scope::Scope;
|
||||
use bumpalo::collections::Vec;
|
||||
use roc_error_macros::internal_error;
|
||||
use roc_module::called_via::BinOp::{DoubleQuestion, Pizza};
|
||||
use roc_module::called_via::BinOp::{DoubleQuestion, SingleQuestion, Pizza};
|
||||
use roc_module::called_via::{BinOp, CalledVia};
|
||||
use roc_module::ident::ModuleName;
|
||||
use roc_parse::ast::Expr::{self, *};
|
||||
|
@ -213,62 +213,135 @@ fn new_op_call_expr<'a>(
|
|||
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_{}_{}",
|
||||
let ok_var = env.arena.alloc_str(
|
||||
&format!(
|
||||
"double_question_ok_{}_{}",
|
||||
left.region.start().offset,
|
||||
left.region.end().offset
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
branch_1_patts_args.push(Loc::at(
|
||||
|
||||
let ok_branch_pattern_args = env.arena.alloc([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(
|
||||
Pattern::Identifier { ident: ok_var },
|
||||
)]);
|
||||
let ok_branch_patterns = env.arena.alloc([Loc::at(
|
||||
left.region,
|
||||
Pattern::PncApply(
|
||||
branch_1_tag,
|
||||
Collection::with_items(branch_1_patts_args.into_bump_slice()),
|
||||
env.arena.alloc(Loc::at(left.region, Pattern::Tag("Ok"))),
|
||||
Collection::with_items(ok_branch_pattern_args),
|
||||
),
|
||||
));
|
||||
let branch_one: &WhenBranch<'_> = env.arena.alloc(WhenBranch {
|
||||
patterns: branch_1_patts.into_bump_slice(),
|
||||
)]);
|
||||
let ok_branch = &*env.arena.alloc(WhenBranch {
|
||||
patterns: ok_branch_patterns,
|
||||
value: Loc::at(
|
||||
left.region,
|
||||
Expr::Var {
|
||||
module_name: "",
|
||||
ident: success_var,
|
||||
ident: ok_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(
|
||||
|
||||
let err_branch_pattern_args = env.arena.alloc([(Loc::at(right.region, Pattern::Underscore("")))]);
|
||||
let err_branch_patterns = env.arena.alloc([Loc::at(
|
||||
right.region,
|
||||
Pattern::PncApply(
|
||||
branch_2_tag,
|
||||
Collection::with_items(branch_2_patts_args.into_bump_slice()),
|
||||
env.arena.alloc(Loc::at(left.region, Pattern::Tag("Err"))),
|
||||
Collection::with_items(err_branch_pattern_args),
|
||||
),
|
||||
));
|
||||
let branch_two: &WhenBranch<'_> = env.arena.alloc(WhenBranch {
|
||||
patterns: branch_2_patts.into_bump_slice(),
|
||||
)]);
|
||||
let err_branch = &*env.arena.alloc(WhenBranch {
|
||||
patterns: err_branch_patterns,
|
||||
value: *right,
|
||||
guard: None,
|
||||
});
|
||||
branches.push(branch_two);
|
||||
|
||||
When(left, branches.into_bump_slice())
|
||||
When(left, &*env.arena.alloc([ok_branch, err_branch]))
|
||||
}
|
||||
SingleQuestion => {
|
||||
let left = desugar_expr(env, scope, left);
|
||||
let right = desugar_expr(env, scope, right);
|
||||
|
||||
let ok_var = env.arena.alloc_str(
|
||||
&format!(
|
||||
"single_question_ok_{}_{}",
|
||||
left.region.start().offset,
|
||||
left.region.end().offset
|
||||
)
|
||||
);
|
||||
|
||||
let ok_branch_pattern_args = env.arena.alloc([Loc::at(
|
||||
left.region,
|
||||
Pattern::Identifier { ident: ok_var },
|
||||
)]);
|
||||
let ok_branch_patterns = env.arena.alloc([Loc::at(
|
||||
left.region,
|
||||
Pattern::PncApply(
|
||||
env.arena.alloc(Loc::at(left.region, Pattern::Tag("Ok"))),
|
||||
Collection::with_items(ok_branch_pattern_args),
|
||||
),
|
||||
)]);
|
||||
let ok_branch = &*env.arena.alloc(WhenBranch {
|
||||
patterns: ok_branch_patterns,
|
||||
value: Loc::at(
|
||||
left.region,
|
||||
Expr::Var {
|
||||
module_name: "",
|
||||
ident: ok_var,
|
||||
},
|
||||
),
|
||||
guard: None,
|
||||
});
|
||||
|
||||
let err_var = env.arena.alloc_str(
|
||||
&format!(
|
||||
"single_question_err_{}_{}",
|
||||
left.region.start().offset,
|
||||
left.region.end().offset
|
||||
)
|
||||
);
|
||||
|
||||
let err_branch_pattern_args = env.arena.alloc([(Loc::at(
|
||||
right.region,
|
||||
Pattern::Identifier { ident: err_var },
|
||||
))]);
|
||||
let err_branch_patterns = env.arena.alloc([Loc::at(
|
||||
right.region,
|
||||
Pattern::PncApply(
|
||||
env.arena.alloc(Loc::at(left.region, Pattern::Tag("Err"))),
|
||||
Collection::with_items(err_branch_pattern_args),
|
||||
),
|
||||
)]);
|
||||
let map_err_expr = &*env.arena.alloc(Loc::at(right.region, Expr::PncApply(
|
||||
right,
|
||||
Collection::with_items(&*env.arena.alloc([
|
||||
&*env.arena.alloc(Loc::at(
|
||||
left.region,
|
||||
Expr::Var { module_name: "", ident: err_var },
|
||||
))
|
||||
])),
|
||||
)));
|
||||
let err_branch = &*env.arena.alloc(WhenBranch {
|
||||
patterns: err_branch_patterns,
|
||||
value: Loc::at(
|
||||
region,
|
||||
Expr::Return(
|
||||
env.arena.alloc(Loc::at(
|
||||
region,
|
||||
Expr::PncApply(
|
||||
env.arena.alloc(Loc::at(region, Expr::Tag("Err"))),
|
||||
Collection::with_items(&*env.arena.alloc([map_err_expr])),
|
||||
),
|
||||
)),
|
||||
None,
|
||||
),
|
||||
),
|
||||
guard: None,
|
||||
});
|
||||
|
||||
When(left, &*env.arena.alloc([ok_branch, err_branch]))
|
||||
}
|
||||
binop => {
|
||||
let left = desugar_expr(env, scope, left);
|
||||
|
@ -1575,6 +1648,7 @@ fn binop_to_function(binop: BinOp) -> (&'static str, &'static str) {
|
|||
Or => (ModuleName::BOOL, "or"),
|
||||
Pizza => unreachable!("Cannot desugar the |> operator"),
|
||||
DoubleQuestion => unreachable!("Cannot desugar the ?? operator"),
|
||||
SingleQuestion => unreachable!("Cannot desugar the ? operator"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -972,10 +972,10 @@ mod test_can {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn try_desugar_double_question_suffix() {
|
||||
fn try_desugar_double_question_binop() {
|
||||
let src = indoc!(
|
||||
r#"
|
||||
Str.to_u64 "123" ?? Num.max_u64
|
||||
Str.to_u64("123") ?? Num.max_u64
|
||||
"#
|
||||
);
|
||||
let arena = Bump::new();
|
||||
|
@ -985,33 +985,95 @@ mod test_can {
|
|||
|
||||
// Assert that we desugar to:
|
||||
//
|
||||
// when Str.to_u64 "123"
|
||||
// Ok success_BRANCH1_0_9 -> success_BRANCH1_0_9
|
||||
// Err _ -> Num.max_u64
|
||||
// when Str.to_u64("123")
|
||||
// Ok(double_question_ok_0_17) -> Ok(double_question_ok_0_17)
|
||||
// Err(_) -> Num.max_u64
|
||||
|
||||
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
|
||||
let cond_args = assert_func_call(cond_expr, "to_u64", 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_16",
|
||||
"double_question_ok_0_17",
|
||||
&out.interns,
|
||||
);
|
||||
assert_var_usage(
|
||||
&branches[0].value.value,
|
||||
"success_BRANCH1_0_16",
|
||||
"double_question_ok_0_17",
|
||||
&out.interns,
|
||||
);
|
||||
|
||||
assert_pattern_tag_apply_with_underscore(&branches[1].patterns[0].pattern.value, "Err");
|
||||
assert_var_usage(&branches[1].value.value, "max_u64", &out.interns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_desugar_single_question_binop() {
|
||||
let src = indoc!(
|
||||
r#"
|
||||
Str.to_u64("123") ? FailedToConvert
|
||||
"#
|
||||
);
|
||||
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.to_u64("123")
|
||||
// Ok(single_question_ok_0_17) -> single_question_ok_0_17
|
||||
// Err(single_question_err_0_17) -> return Err(FailedToConvert(single_question_err_0_17))
|
||||
|
||||
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
|
||||
let cond_args = assert_func_call(cond_expr, "to_u64", 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",
|
||||
"single_question_ok_0_17",
|
||||
&out.interns,
|
||||
);
|
||||
assert_var_usage(
|
||||
&branches[0].value.value,
|
||||
"single_question_ok_0_17",
|
||||
&out.interns,
|
||||
);
|
||||
|
||||
assert_pattern_tag_apply_with_ident(
|
||||
&branches[1].patterns[0].pattern.value,
|
||||
"Err",
|
||||
"single_question_err_0_17",
|
||||
&out.interns,
|
||||
);
|
||||
|
||||
let err_expr = assert_return_expr(&branches[1].value.value);
|
||||
let mapped_err = assert_tag_application(err_expr, "Err");
|
||||
assert_eq!(mapped_err.len(), 1);
|
||||
let inner_err = assert_tag_application(&mapped_err[0].1.value, "FailedToConvert");
|
||||
assert_eq!(inner_err.len(), 1);
|
||||
assert_var_usage(
|
||||
&inner_err[0].1.value,
|
||||
"single_question_err_0_17",
|
||||
&out.interns,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_desugar_works_elsewhere() {
|
||||
let src = indoc!(
|
||||
|
@ -1192,6 +1254,13 @@ mod test_can {
|
|||
}
|
||||
}
|
||||
|
||||
fn assert_return_expr(expr: &Expr) -> &Expr {
|
||||
match expr {
|
||||
Expr::Return { return_value, .. } => &return_value.value,
|
||||
_ => panic!("Expr was not a Return: {:?}", expr),
|
||||
}
|
||||
}
|
||||
|
||||
// TAIL CALLS
|
||||
fn get_closure(expr: &Expr, i: usize) -> roc_can::expr::Recursive {
|
||||
match expr {
|
||||
|
|
|
@ -963,6 +963,7 @@ fn push_op(buf: &mut Buf, op: BinOp) {
|
|||
called_via::BinOp::Or => buf.push_str("||"),
|
||||
called_via::BinOp::Pizza => buf.push_str("|>"),
|
||||
called_via::BinOp::DoubleQuestion => buf.push_str("??"),
|
||||
called_via::BinOp::SingleQuestion => buf.push_str("?"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2284,7 +2285,8 @@ pub fn sub_expr_requests_parens(expr: &Expr<'_>) -> bool {
|
|||
| BinOp::And
|
||||
| BinOp::Or
|
||||
| BinOp::Pizza
|
||||
| BinOp::DoubleQuestion => true,
|
||||
| BinOp::DoubleQuestion
|
||||
| BinOp::SingleQuestion => true,
|
||||
})
|
||||
}
|
||||
Expr::If { .. } => true,
|
||||
|
|
|
@ -3,7 +3,7 @@ use self::BinOp::*;
|
|||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
|
||||
const PRECEDENCES: [(BinOp, u8); 17] = [
|
||||
const PRECEDENCES: [(BinOp, u8); 18] = [
|
||||
(Caret, 8),
|
||||
(Star, 7),
|
||||
(Slash, 7),
|
||||
|
@ -12,6 +12,7 @@ const PRECEDENCES: [(BinOp, u8); 17] = [
|
|||
(Plus, 5),
|
||||
(Minus, 5),
|
||||
(DoubleQuestion, 5),
|
||||
(SingleQuestion, 5),
|
||||
(Pizza, 4),
|
||||
(Equals, 3),
|
||||
(NotEquals, 3),
|
||||
|
@ -23,7 +24,7 @@ const PRECEDENCES: [(BinOp, u8); 17] = [
|
|||
(Or, 0),
|
||||
];
|
||||
|
||||
const ASSOCIATIVITIES: [(BinOp, Associativity); 17] = [
|
||||
const ASSOCIATIVITIES: [(BinOp, Associativity); 18] = [
|
||||
(Caret, RightAssociative),
|
||||
(Star, LeftAssociative),
|
||||
(Slash, LeftAssociative),
|
||||
|
@ -32,6 +33,7 @@ const ASSOCIATIVITIES: [(BinOp, Associativity); 17] = [
|
|||
(Plus, LeftAssociative),
|
||||
(Minus, LeftAssociative),
|
||||
(DoubleQuestion, LeftAssociative),
|
||||
(SingleQuestion, LeftAssociative),
|
||||
(Pizza, LeftAssociative),
|
||||
(Equals, NonAssociative),
|
||||
(NotEquals, NonAssociative),
|
||||
|
@ -43,7 +45,7 @@ const ASSOCIATIVITIES: [(BinOp, Associativity); 17] = [
|
|||
(Or, RightAssociative),
|
||||
];
|
||||
|
||||
const DISPLAY_STRINGS: [(BinOp, &str); 17] = [
|
||||
const DISPLAY_STRINGS: [(BinOp, &str); 18] = [
|
||||
(Caret, "^"),
|
||||
(Star, "*"),
|
||||
(Slash, "/"),
|
||||
|
@ -52,6 +54,7 @@ const DISPLAY_STRINGS: [(BinOp, &str); 17] = [
|
|||
(Plus, "+"),
|
||||
(Minus, "-"),
|
||||
(DoubleQuestion, "??"),
|
||||
(SingleQuestion, "?"),
|
||||
(Pizza, "|>"),
|
||||
(Equals, "=="),
|
||||
(NotEquals, "!="),
|
||||
|
@ -154,6 +157,7 @@ pub enum BinOp {
|
|||
Plus,
|
||||
Minus,
|
||||
DoubleQuestion,
|
||||
SingleQuestion,
|
||||
Pizza,
|
||||
Equals,
|
||||
NotEquals,
|
||||
|
@ -170,7 +174,7 @@ impl BinOp {
|
|||
/// how wide this operator is when typed out
|
||||
pub fn width(self) -> u16 {
|
||||
match self {
|
||||
Caret | Star | Slash | Percent | Plus | Minus | LessThan | GreaterThan => 1,
|
||||
Caret | Star | Slash | Percent | Plus | Minus | LessThan | GreaterThan | SingleQuestion => 1,
|
||||
DoubleSlash | Equals | NotEquals | LessThanOrEq | GreaterThanOrEq | And | Or
|
||||
| Pizza | DoubleQuestion => 2,
|
||||
}
|
||||
|
@ -206,13 +210,13 @@ pub enum Associativity {
|
|||
|
||||
impl BinOp {
|
||||
pub fn associativity(self) -> Associativity {
|
||||
const ASSOCIATIVITY_TABLE: [Associativity; 17] = generate_associativity_table();
|
||||
const ASSOCIATIVITY_TABLE: [Associativity; 18] = generate_associativity_table();
|
||||
|
||||
ASSOCIATIVITY_TABLE[self as usize]
|
||||
}
|
||||
|
||||
fn precedence(self) -> u8 {
|
||||
const PRECEDENCE_TABLE: [u8; 17] = generate_precedence_table();
|
||||
const PRECEDENCE_TABLE: [u8; 18] = generate_precedence_table();
|
||||
|
||||
PRECEDENCE_TABLE[self as usize]
|
||||
}
|
||||
|
@ -232,14 +236,14 @@ impl Ord for BinOp {
|
|||
|
||||
impl std::fmt::Display for BinOp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
const DISPLAY_TABLE: [&str; 17] = generate_display_table();
|
||||
const DISPLAY_TABLE: [&str; 18] = generate_display_table();
|
||||
|
||||
write!(f, "{}", DISPLAY_TABLE[*self as usize])
|
||||
}
|
||||
}
|
||||
|
||||
const fn generate_precedence_table() -> [u8; 17] {
|
||||
let mut table = [0u8; 17];
|
||||
const fn generate_precedence_table() -> [u8; 18] {
|
||||
let mut table = [0u8; 18];
|
||||
let mut i = 0;
|
||||
|
||||
while i < PRECEDENCES.len() {
|
||||
|
@ -250,8 +254,8 @@ const fn generate_precedence_table() -> [u8; 17] {
|
|||
table
|
||||
}
|
||||
|
||||
const fn generate_associativity_table() -> [Associativity; 17] {
|
||||
let mut table = [NonAssociative; 17];
|
||||
const fn generate_associativity_table() -> [Associativity; 18] {
|
||||
let mut table = [NonAssociative; 18];
|
||||
let mut i = 0;
|
||||
|
||||
while i < ASSOCIATIVITIES.len() {
|
||||
|
@ -262,8 +266,8 @@ const fn generate_associativity_table() -> [Associativity; 17] {
|
|||
table
|
||||
}
|
||||
|
||||
const fn generate_display_table() -> [&'static str; 17] {
|
||||
let mut table = [""; 17];
|
||||
const fn generate_display_table() -> [&'static str; 18] {
|
||||
let mut table = [""; 18];
|
||||
let mut i = 0;
|
||||
|
||||
while i < DISPLAY_STRINGS.len() {
|
||||
|
|
|
@ -4097,6 +4097,7 @@ where
|
|||
|
||||
good!(OperatorOrDef::BinOp(BinOp::Minus), 1)
|
||||
}
|
||||
"?" => good!(OperatorOrDef::BinOp(BinOp::SingleQuestion), 1),
|
||||
"*" => good!(OperatorOrDef::BinOp(BinOp::Star), 1),
|
||||
"/" => good!(OperatorOrDef::BinOp(BinOp::Slash), 1),
|
||||
"%" => good!(OperatorOrDef::BinOp(BinOp::Percent), 1),
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
fallible!(args)
|
||||
? \my_err ->
|
||||
my_err * 2
|
|
@ -0,0 +1,50 @@
|
|||
@0-43 SpaceAfter(
|
||||
BinOps(
|
||||
[
|
||||
(
|
||||
@0-15 PncApply(
|
||||
@0-9 Var {
|
||||
module_name: "",
|
||||
ident: "fallible!",
|
||||
},
|
||||
[
|
||||
@10-14 Var {
|
||||
module_name: "",
|
||||
ident: "args",
|
||||
},
|
||||
],
|
||||
),
|
||||
@18-19 SingleQuestion,
|
||||
),
|
||||
],
|
||||
@20-43 Closure(
|
||||
[
|
||||
@21-27 Identifier {
|
||||
ident: "my_err",
|
||||
},
|
||||
],
|
||||
@33-43 SpaceBefore(
|
||||
BinOps(
|
||||
[
|
||||
(
|
||||
@33-39 Var {
|
||||
module_name: "",
|
||||
ident: "my_err",
|
||||
},
|
||||
@40-41 Star,
|
||||
),
|
||||
],
|
||||
@42-43 Num(
|
||||
"2",
|
||||
),
|
||||
),
|
||||
[
|
||||
Newline,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
[
|
||||
Newline,
|
||||
],
|
||||
)
|
|
@ -0,0 +1,2 @@
|
|||
fallible!(args) ? |my_err|
|
||||
my_err * 2
|
|
@ -0,0 +1 @@
|
|||
fallible!(args) ? WrapOverErr
|
|
@ -0,0 +1,27 @@
|
|||
@0-31 SpaceAfter(
|
||||
BinOps(
|
||||
[
|
||||
(
|
||||
@0-15 PncApply(
|
||||
@0-9 Var {
|
||||
module_name: "",
|
||||
ident: "fallible!",
|
||||
},
|
||||
[
|
||||
@10-14 Var {
|
||||
module_name: "",
|
||||
ident: "args",
|
||||
},
|
||||
],
|
||||
),
|
||||
@18-19 SingleQuestion,
|
||||
),
|
||||
],
|
||||
@20-31 Tag(
|
||||
"WrapOverErr",
|
||||
),
|
||||
),
|
||||
[
|
||||
Newline,
|
||||
],
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
fallible!(args) ? WrapOverErr
|
|
@ -693,6 +693,8 @@ mod test_snapshots {
|
|||
pass/single_arg_closure.expr,
|
||||
pass/single_arg_with_underscore_closure.expr,
|
||||
pass/single_underscore_closure.expr,
|
||||
pass/single_question_binop_closure.expr,
|
||||
pass/single_question_binop_tag.expr,
|
||||
pass/sneaky_implements_in_opaque_fn_type.expr,
|
||||
pass/space_after_opt_field_pat.expr,
|
||||
pass/space_before_colon.full,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue