Search code examples
rakurakudo

Assignment destructuring and operator precedence


The documentation says that the comma operator has higher precedence than the assignment = operator, and this is specifically different than in Perl, so that we are allowed to remove parentheses in some contexts.

This allows us to do things like this:

my @array = 1, 2, 3;

What I don't understand is why when do something like this:

sub test() { return 1, 2 }
my ($a, $b);
$a, $b = test();

$b get assigned [1 2] while $a gets no value.

While I would assume that the following would be equivalent, because the comma operator is tighter than the assignment.

($a, $b) = test();

The semantics of Raku have a lot of subtlety and I guess I am thinking too much in terms of Perl.

Like raiph said in the comments, my original assumption that the comma operator has higher precedence than the assignment operator was false. And it was due to a problem in the rendering of the operator precedence table, which didn't presented operators in their precedence order. This explains the actual behavior of Raku for my examples.


Solution

  • The = operator itself is always item assignment level, which is tighter than comma. However, it may apply a "sub-precedence", which is how it is compared to any further infixes in the expression that follow it. The term that was parsed prior to the = is considered, and:

    • In the case that the assignment is to a Scalar variable, then the = operator works just like any other item assignment precedence operator, which is tighter than the precedence of ,
    • In any other case, its precedence relative to following infixes is list prefix, which is looser than the precedence of ,

    To consider some cases (first, where it doesn't impact anything):

    $foo = 42;     # $ sigil, so item assignment precedence after
    @foo = 1;      # @ sigil, so list assignment precedence after
    @foo[0] = 1;   # postcircumfix (indexing operator) means list assignment after...
    $foo[0] = 1;   # ...always, even in this case
    

    If we have a single variable on the left and a list on the right, then:

    @foo = 1, 2;      # List assignment into @foo two values
    $foo = 1, 2;      # Assignment of 1 into $foo, 2 is dead code
    

    These apply with the = initializer (following a my $var declaration). This means that:

    loop (my $i = 0, my $j = $end; $i < $end; $i++, $j--) {
        ...
    }
    

    Will result in $i being assigned 0 and $j being assigned $end.

    Effectively, the rule means we get to have parentheses-free initialization of array and hash variables, while still having lists of scalar initializations work out as in the loop case.

    Turning to the examples in the question. First, this:

    ($a, $b) = test();
    

    Parses a single term, then encounters the =. The precedence when comparing any following infixes would be list prefix (looser than ,). However, there are no more infixes here, so it doesn't really matter.

    In this case:

    sub test() { return 1, 2 }
    my ($a, $b);
    $a, $b = test();
    

    The precedence parser sees the , infix, followed by the = infix. The = infix in itself is tighter than comma (item assignment precedence); the sub-precedence is only visible to infixes parsed after the = (and there are none here).

    Note that were it not this way, and the precedence shift applied to the expression as a whole, then:

    loop (my $i = 0, my @lagged = Nil, |@values; $i < @values; $i++) {
        ...
    }
    

    Would end up grouped not as (my $i = 0), (my @lagged = Nil, |@values), but rather (my $i = 0, my @lagged) = Nil, |@values, which is rather less useful.