I'm continuing my quest into the deep waters of Perl6 subtle implementation details. This time I have a problem with installing own methods into a role. Fasten your seatbelts please as we start the journey into the code.
The idea is an attribute trait which installs methods on type objects it's being composed into. The problem was initially discovered on private methods which I would expect to be installed in the role the attribute is being declared in. At that point I have discovered that under certain conditions generated methods referring a scalar from their closure cannot be called! Most likely due to the closure being lost at run-time. But the most confusing point is that it happens only to the roles and only if one role is consuming another!
So, here is the trait source:
unit module trait-foo;
role FooClassHOW {...}
role FooAttr {
has $.base-name = self.name.substr(2);
method compose (Mu \type) {
callsame;
if (type.HOW ~~ Metamodel::ClassHOW) && (type.HOW !~~ FooClassHOW) {
type.HOW does FooClassHOW;
}
}
method install-method ( Mu \type ) {
my $attr = self;
type.^add_private_method(
"attr-{$attr.base-name}",
method { "by attr {$attr.name}" }
);
type.^add_method(
"pubattr-{$attr.base-name}",
method { "by attr {$attr.name} - public" }
);
type.^add_private_method(
"check-{$attr.base-name}",
method { "not using closure" }
);
}
}
role FooClassHOW {
method compose ( Mu \type ) {
for type.^attributes.grep( FooAttr ) -> $attr {
$attr.install-method( type );
type.^add_private_method(
"class-{$attr.base-name}",
method { "by class: attr {$attr.name}" }
);
}
nextsame;
}
}
role FooRoleHOW {
method compose ( Mu \type ) {
for type.^attributes.grep( FooAttr ) -> $attr {
$attr.install-method( type );
type.^add_private_method(
"role-{$attr.base-name}",
method { "by role: attr {$attr.name}" }
);
}
nextsame;
}
}
multi trait_mod:<is> (Attribute:D $attr, :$foo!) is export {
$attr does FooAttr;
given $*PACKAGE.HOW {
when Metamodel::ParametricRoleHOW {
$_ does FooRoleHOW unless $_ ~~ FooRoleHOW;
}
default {
$_ does FooClassHOW unless $_ ~~ FooClassHOW;
}
}
}
The key point here is the install-method
which installs a public method pubattr-<attr>
, and private methods attr-<attr>
, check-<attr>
. The difference between pubattr-
, attr-
and check-
is that the first two are referring their closure while the latter doesn't. Here is what happens if two roles and a class are defined in their individual files:
compose_method_inject.p6
#!/usr/bin/env perl6
use lib '.';
use trait-foo;
use compose-foorole;
class Foo does FooRole {
has $.fubar is foo;
method class-test {
say self!check-fubar;
say self!class-fubar;
say self!attr-fubar;
}
}
my $inst = Foo.new;
note "> Class";
$inst.class-test;
note "> BarRole";
$inst.bar-role-test;
note "> FooRole";
$inst.foo-role-test;
compose-foorole.pm6
unit package compose;
use trait-foo;
use compose-barrole;
role FooRole does BarRole is export {
has $.foo is foo;
method foo-role-test {
note FooRole.^candidates[0].^private_method_table;
say self!check-foo;
say self!role-foo;
say self!attr-foo;
}
}
compose-barrole.pm6
unit package compose;
use trait-foo;
role BarRole is export {
has $.bar is foo;
method bar-role-test {
note BarRole.^candidates[0].^private_method_table;
say self!check-bar;
say self!role-bar;
say self!inattr-bar;
}
}
Executing compose_method_inject.p6 produces the following output:
> Class not using closure by class: attr $!fubar by attr $!fubar by attr $!fubar - public > BarRole {attr-bar => <anon>, check-bar => <anon>, role-bar => <anon>} not using closure by role: attr $!bar Cannot invoke this object (REPR: Null; VMNull)
Note that the class works ok while similar code in BarRole
fails. Same result would be observed if foo-role-test
from FooRole
is executed first:
> Class not using closure by class: attr $!fubar by attr $!fubar by attr $!fubar - public > FooRole {attr-foo => <anon>, check-foo => <anon>, role-foo => <anon>} not using closure by role: attr $!foo Cannot invoke this object (REPR: Null; VMNull)
It is also noteworthy that method installed from FooRoleHOW
doesn't lose its closure and is been successfully executed.
Now, to another trick. I remove does BarRole
from FooRole
and make it applied directly on Foo:
class Foo does FooRole does BarRole {
The output changes drastically and situation becomes even more confusing:
> Class not using closure by class: attr $!fubar by attr $!fubar by attr $!fubar - public > FooRole {attr-foo => <anon>, check-foo => <anon>, role-foo => <anon>} not using closure by role: attr $!foo by attr $!foo > BarRole {attr-bar => <anon>, check-bar => <anon>, role-bar => <anon>} not using closure by role: attr $!bar Cannot invoke this object (REPR: Null; VMNull)
UPD Another important thing to note is that both roles and the class a intentionally split by files because placing them all in common file makes things work as intended.
BTW, I don't want get deeper into it, but in the original code where the above samples were extracted from I was also setting methods names with .set_name
. Names were strings including the $attr
scalar from the closure. Dumping method tables in the compose()
was producing hashes with the set names as values; dumping same tables in user code shows output similar to the above – with <anon>
as values. Seemingly, method names were GC'ed together with the closure.
Now, I would like to hear from someone that I'm stupid and methods has to be installed differently. Or that the information about attribute has to be preserved in some other way but not through relying on closures. Or any other idea which would let me create private attribute-related methods.
This is not exactly answer but rather a note and a workaround for the bug. Ok, the note has been just made: this is a bug. Though it is not present in Linux version of rakudo, I only observe it on macOS/darwin. Of course, it doesn't mean that other platforms are not vulnerable.
The bug has a workaround. Since methods installed from class/role composers do not lost their closures, then installation of methods has to be moved into them. In my case, because similar functionality is desirable for both of them, use of a role implementing method installator works as a charm.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With