Binding of private attributes: nqp::bindattr vs :=
Asked Answered
E

1

10

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.

Eveevection answered 2/10, 2018 at 17:3 Comment(0)
S
10

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.)

Schmooze answered 2/10, 2018 at 18:14 Comment(6)
I have seen some bits of QAST building in the source but --target povides more clear view. Note also that my knowelge of compiler internals is very basic and I might be asking silly questions. But for now I have discovered that := doesn't work the way I thought it would. It deconts instantly thus binding not to a Proxy instance but to what FETCH returns at the moment when binding happens. Of course, same happens with decont-wrapped Proxy as bindattr parameter.Eveevection
BTW, prior to asking here I was trying to strip off container of FETCH return with both nqp::decont and <>. But neither works, @!foo returns scalar container.Eveevection
This could be useful. Further investigation revealed that direct call to Proxy's FETCH returns validly decontainerised value: @!foo.VAR.^find_method('FETCH')(@!foo).perl prints ["a", "b", "c"] as expected from what I see in BOOTSTRAP.nqp code. It looks more and more like a bug.Eveevection
There's a deliberately retained bug around Proxy handling and return value processing. We didn't properly decont Proxy on return. I fixed it, but the fix then broke a bunch of ecosystem modules because folks hadn't put the required is rw on routines. So the fix was backed out to allow some more time for modules to get fixed up.Schmooze
Sounds somewhat like "it won't be fixed". But at least I now know what to write in the documentation. Thanks Jonathan!Eveevection
We intend to put the fix back; we can't wait for every module to be patched (or even to merge a PR that we send), but we can give a couple of monthly release's grace.Schmooze

© 2022 - 2024 — McMap. All rights reserved.