roc-lang / roc

A fast, friendly, functional language.
https://roc-lang.org
Universal Permissive License v1.0
4.46k stars 313 forks source link

Inconsistent Handling of ! Operator #7103

Closed sajjaduc closed 1 month ago

sajjaduc commented 1 month ago

I've encountered an inconsistency in how the Roc language handles the ! operator when used within functions. Specifically, the behavior differs depending on whether the ! operator is used as a standalone statement or as the last line of a function.

The following code works as expected:

run : Task {} _
run =
    Stdout.line! "Say something:"
    input = Stdin.line!
    Stdout.line! "You said: $(input)"

However, if I remove the last two lines, it fails:

run : Task {} _
run =
    Stdout.line! "Say something:"

I am calling the run function simply as follows:

main =
    run

Note: Removing the annotation solves the problem, and so does removing the ! operator from the last line.

Tested w/ basic-cli (0.15.0).

lukewilliamboswell commented 1 month ago

Some notes investigating this bug...this is about as far as my brain can handle for today. The issue looks to be related to interaction between the top level type annotation and having a suffixed statement in that final position as this gets us to this position in desugar.rs.

In desugar_value_def_suffixed we pass in a TrySuffix as body expression here.

That will immediately unwrap giving us an

Err(EUnwrapped::UnwrappedSubExpr {
    sub_arg,
    sub_pat,
    sub_new,
    target,
})

With the following values...

[crates/compiler/can/src/desugar.rs:270:21] &sub_arg = @26-55 Apply(
    @26-55 Var {
        module_name: "Stdout",
        ident: "line",
    },
    [
        @39-55 Str(
            PlainLine(
                "Say something:",
            ),
        ),
    ],
    Space,
)
[crates/compiler/can/src/desugar.rs:270:21] &sub_pat = @26-55 Identifier {
    ident: "#!0_arg",
}
[crates/compiler/can/src/desugar.rs:270:21] &sub_new = @26-55 Var {
    module_name: "",
    ident: "#!0_arg",
}

We pass these into the apply_try_function

AnnotatedBody {
    ann_pattern,
    ann_type,
    lines_between,
    body_pattern,
    body_expr: apply_try_function(
        arena,
        body_expr.region,
        sub_arg,
        sub_pat,
        sub_new,
        Some((ann_pattern, ann_type)),
        target,
    ),
}

Which now causes issues because the sub_pat is an #!0_arg ident while the ann_pattern is run ... these don't match and so we get a false in this if branch

ident: if loc_ann_pat.value.equivalent(&loc_pat.value) {
    new_ident
} else {
    // create another pattern to preserve inconsistency
    arena.alloc(next_unique_suffixed_ident())
},

And the "create another pattern to preserve inconsistency" then causes us issues. I'm wondering if this should blow up at this point instead of creating another pattern which seems to be failing silently.

sajjaduc commented 1 month ago

Thanks for taking a look @lukewilliamboswell.

I wish I can contribute here but unfortunately I don't know Rust at all. And I could be totally out of line here but logically what makes sense to me is that it's related to the simple fact that Roc allows you to use ! operator on the last line, even though there is no 2nd task added!?! Wouldn't it be more logically correct to not allow that at all?

I guess it's time for me to go learn Rust first because I genuinely want to contribute to Roc in any way shape or form.

Anton-4 commented 1 month ago

Wouldn't it be more logically correct to not allow that at all?

We decided to allow that for consistency but the ! syntax will also undergo significant changes in the future.