rakunqp

Binding of private attributes: nqp::bindattr vs :=


I'm trying to find how the binding operation works on attributes and what makes it so different from nqp::bindattr. Consider the following example:

 class Foo {
     has @!foo;

     submethod TWEAK {
         my $fval = [<a b c>];
         use nqp;
         nqp::bindattr( nqp::decont(self), $?CLASS, '@!foo',
         #@!foo :=
             Proxy.new(
                 FETCH => -> $ { $fval },
                 STORE => -> $, $v { $fval = $v }
             )
         );
     }

     method check {
         say @!foo.perl;
     }
 }

 my $inst = Foo.new;
 $inst.check;

It prints:

$["a", "b", "c"]

Replacing nqp::bindattr with the binding operator from the comment gives correct output:

["a", "b", "c"]

Similarly, if foo is a public attribute and accessor is used the output would be correct too due to deconterisation taking place within the accessor.

I use similar code in my AttrX::Mooish module where use of := would overcomplicate the implementation. So far, nqp::bindattr did the good job for me until the above problem arised.

I tried tracing down Rakudo's internals looking for := implementation but without any success so far. I would ask here either for an advise as to how to simulate the operator or where in the source to look for its implementation.


Solution

  • Before I dig into the answer: most things in this post are implementation-defined, and the implementation is free to define them differently in the future.

    To find out what something (naively) compiles into under Rakudo Perl 6, use the --target=ast option (perl6 --target=ast foo.p6). For example, the bind in:

    class C {
        has $!a;
        submethod BUILD() {
            my $x = [1,2,3];
            $!a := $x
        }
    }
    

    Comes out as:

                                  - QAST::Op(bind)  :statement_id<7>
                                    - QAST::Var(attribute $!a) <wanted> $!a
                                      - QAST::Var(lexical self) 
                                      - QAST::WVal(C) 
                                    - QAST::Var(lexical $x)  $x
    

    While switching it for @!a like here:

    class C {
        has @!a;
        submethod BUILD() {
            my $x = [1,2,3];
            @!a := $x
        }
    }
    

    Comes out as:

                                  - QAST::Op(bind)  :statement_id<7>
                                    - QAST::Var(attribute @!a) <wanted> @!a
                                      - QAST::Var(lexical self) 
                                      - QAST::WVal(C) 
                                    - QAST::Op(p6bindassert) 
                                      - QAST::Op(decont) 
                                        - QAST::Var(lexical $x)  $x
                                      - QAST::WVal(Positional) 
    

    The decont instruction is the big difference here, and it will take the contents of the Proxy by calling its FETCH, thus why the containerization is gone. Thus, you can replicate the behavior by inserting nqp::decont around the Proxy, although that rather begs the question of what the Proxy is doing there if the correct answer is obtained without it!

    Both := and = are compiled using case analysis (namely, by looking at what is on the left hand side). := only works for a limited range of simple expressions on the left; it is a decidedly low-level operator. By contrast, = falls back to a sub call if the case analysis doesn't come up with a more efficient form to emit, though in most cases it manages something better.

    The case analysis for := inserts a decont when the target is a lexical or attribute with sigil @ or %, since - at a Perl 6 level - having an item bound to an @ or % makes no sense. Using nqp::bindattr is going a level below Perl 6 semantics, and so it's possible to end up with the Proxy bound directly there using that. However, it also violates expectations elsewhere. Don't expect that to go well (but it seems you don't want to do that anyway.)