8
$\begingroup$

In a language with standard let and if expressions, what should be the their precedence such that they can be nested without parentheses?

For example, I want to be able to parse both of these expressions:

if let a = b in c then d else e
let a = if b then c else d in e

I am using lalrpop to generate a parser. My attempt looks like this:

grammar;

pub Expr: () = {
    #[precedence(level="0")]
    Name => { println!("Name: {}", <>); },

    #[precedence(level="1")]
    #[assoc(side="right")]
    "let" <name:Name> "=" <value:Expr> "in" <body:Expr> => { println!("Let: {}", name); },

    #[precedence(level="2")]
    "if" <check:Expr> "then" <then:Expr> "else" <els:Expr> => { println!("If"); },
};

Name: &'input str = r"[a-zA-Z_]+'?";

However, this can only parse the first of the above expressions. If I swap the precedence of the last two rules, then it can only parse the second!

What am I missing?

$\endgroup$
1
  • 3
    $\begingroup$ I don't believe this is a case of precedence conflict. The first keyword (if or let) allows to unambiguosly identify the applicable rule. The same goes for the nested expressions, so having a let in the if's condition is perfectly fine $\endgroup$
    – abel1502
    Jun 9, 2023 at 11:28

3 Answers 3

6
$\begingroup$

As pointed out in the comments, this actually doesn't seem to be a precedence issue. I can get the desired behavior by removing the precedence annotations altogether:

grammar;

pub Expr: () = {
    Name => { println!("Name: {}", <>); },
    "let" <name:Name> "=" <value:Expr> "in" <body:Expr> => { println!("Let: {}", name); },
    "if" <check:Expr> "then" <then:Expr> "else" <els:Expr> => { println!("If"); },
};

Name: &'input str = r"[a-zA-Z_]+'?";

So the answer to the question is they should be at the same level.

$\endgroup$
3
$\begingroup$

Make if let a single construct

This is what Swift does (though it uses , rather than in):

if condition { /* okay */ }
if (condition) { /* okay */ }
if let a = b { /* okay */ }
if let a = b, condition { /* okay */ }
if (let a = b) { /* error */ }
$\endgroup$
3
$\begingroup$

Adding to @Bbrk24's answer, Rust has if let as a separate language construct with its own semantics, as opposed to just combining the semantics of an if expression with a let expression. Particularly, it allows the let binding to be a refutable pattern, unlike let in other contexts. (The same applies for let bindings in other conditional contexts, e.g. while let.)

For example, if some_opt is an option:

if let Some(x) = some_opt {
    // ...
}

The statement let Some(x) = some_opt; by itself would be invalid since the pattern Some(x) is refutable; it doesn't match the value None. But this is allowed in an if let statement, where the variable x is bound only inside the conditional branch, and the conditional branch is executed only when the pattern does match (so that x does have a value).

If your language has pattern matching and/or destructuring assignments already, then it may be worth including this construct in your language. In that case, there is no need to resolve any precedence issue between if and let when parsing if let ..., since this becomes just a single expression.

$\endgroup$
1
  • $\begingroup$ Swift if let x = y is specifically shorthand for if case Optional.some(let x) = y, so it is more semantically similar to Rust than to the let-in statement the OP has. However, Swift's shorthand is more syntactically similar to the OP, and the question is about parsing, not semantics. $\endgroup$
    – Bbrk24
    Jun 9, 2023 at 17:08

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .