Skip to content

Commit

Permalink
Implement metmodel transparency of nominalizables (#4058)
Browse files Browse the repository at this point in the history
As a matter of fact, a nominalizable is a wrapper around a nominal or another nominalizable. As such, it makes sense to have their MOP classes transparent, similar to how their typeobjects are transparent down to the final nominal type. I.e., it means that for

    role R[::T] {
        my subset RS of T;
        method foo(RS() $v) {...}
    }

`RS().HOW.archetypes.generic` should be true at the compile time because `RS` wraps a generic. Similarly, `RS` would have it's `coercive` archetype set would it be declared as `my subset RS of T()`. The transparency lets the compiler and runtime to make sure no nominalization is missed no matter how deep they're nested into each other. In particular, it makes the sample above possible in theory. Though in practice implementing it is yet too complicated task impractical in the light of the upcoming RakuAST implementation.

A new metmodel role `Perl6::Metamodel::Nominalizable` is introduced which provides basic common functionality of nominalizables. In particular, it implements `.^wrappee` method which allows to fetch necessary nominalization from the nesting. `RS.^wrappee(:coercion)` would return `T()` for the latter `RS` example. Correspondingly, `RS().^wrappee(:subset)` would result in `RS` itself.

Implemented `instantiate_generic` for `Metamodel::SubsetHOW`.

Implemented partial support of the new transparency in `Actions.nqp`.

Improved the performance of the new binding code by replacing a call to `coercive` method on a parameter object with `nqp::getattr` and `nqp::bitand_i` pair. This brings the overall rakudo performance almost on a par
with v2020.10 with euler script, as mentioned in #4056. In my tests the script is just 3% slower with `time raku euler-10.p6`.

The whitenoise script, mentioned in the same ticket, is 55% faster than PR #4059, and 2.1x times faster than v2020.11. Unfortunately, it's still 4.5 times slower than with v2020.10. But I hope that inlinable `nqp::curcode` would resolve this too.
  • Loading branch information
vrurg committed Nov 28, 2020
1 parent 6852f40 commit d37906d
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 109 deletions.
182 changes: 102 additions & 80 deletions src/Perl6/Actions.nqp
Expand Up @@ -8993,12 +8993,12 @@ class Perl6::Actions is HLL::Actions does STDActions {
my $SIG_ELEM_IS_RW := 256;
my $SIG_ELEM_IS_RAW := 1024;
my $SIG_ELEM_IS_OPTIONAL := 2048;
my $SIG_ELEM_IS_COERCIVE := 67108864;
my @iscont_ops := ['iscont', 'iscont_i', 'iscont_n', 'iscont_s'];
sub lower_signature($block, $sig, @params) {
my @result;
my $clear_topic_bind;
my $saw_slurpy;
my $instantiated_code;
my $Code := $*W.find_single_symbol('Code', :setting-only);
my $Sig := $*W.find_single_symbol('Signature', :setting-only);
my $Param := $*W.find_single_symbol('Parameter', :setting-only);
Expand All @@ -9007,6 +9007,33 @@ class Perl6::Actions is HLL::Actions does STDActions {
my @p_objs := nqp::getattr($sig, $Sig, '@!params');
my int $i := 0;
my int $n := nqp::elems(@params);
# @!params attribute of the code object. Should only be used when handling of generics is needed.
my $ins_params;

sub signature_params($var) {
unless $ins_params {
$ins_params := QAST::Node.unique('__lowered_parameters_');
$var.push(
QAST::Op.new(
:op('bind'),
QAST::Var.new(:name($ins_params), :scope('local'), :decl('var')),
QAST::Op.new( # Get @!params on the signature
:op('getattr'),
QAST::Op.new( # Get signature object
:op('getattr'),
QAST::Op.new( :op('getcodeobj'), QAST::Op.new( :op('curcode') ) ),
QAST::WVal.new(:value($Code)),
QAST::SVal.new(:value('$!signature'))
),
QAST::WVal.new(:value($Sig)),
QAST::SVal.new(:value('@!params'))
)
)
);
}
QAST::Var.new(:name($ins_params), :scope('local'))
}

while $i < $n {
# Some things need the full binder to do.
my %info := @params[$i];
Expand Down Expand Up @@ -9110,11 +9137,11 @@ class Perl6::Actions is HLL::Actions does STDActions {

# Add type checks.
my $param_type := %info<type>;
my $nomtype := $param_type.HOW.archetypes.nominalizable
?? $param_type.HOW.nominalize($param_type)
!! $param_type;
my int $is_generic := %info<type_generic>;
my int $is_coercive := %info<type_coercive>;
my $nomtype := !$is_generic && $param_type.HOW.archetypes.nominalizable
?? $param_type.HOW.nominalize($param_type)
!! $param_type;
my int $is_rw := $flags +& $SIG_ELEM_IS_RW;
my int $spec := nqp::objprimspec($nomtype);
my $decont_name;
Expand All @@ -9135,7 +9162,7 @@ class Perl6::Actions is HLL::Actions does STDActions {
}
return $decont_name;
}
if $spec && !%info<type_generic> {
if !$is_generic && $spec {
if $is_rw {
$var.push(QAST::ParamTypeCheck.new(QAST::Op.new(
:op(@iscont_ops[$spec]),
Expand Down Expand Up @@ -9219,61 +9246,75 @@ class Perl6::Actions is HLL::Actions does STDActions {
}
}

# If there are type captures involved - most commonly $?CLASS and
# ::?CLASS - we emit a piece of code for each target that gets the
# WHAT of the given value and binds it.
#
# In theory, we could bind a local with the result of the WHAT
# operation, but I'm not convinced it's sufficiently expensive.
if %info<type_captures> {
my $iter := nqp::iterator(%info<type_captures>);
while $iter {
$var.push( QAST::Op.new(
:op<bind>,
QAST::Var.new( :name(nqp::shift($iter)), :scope<lexical> ),
get_decont_name()
?? QAST::Op.new( :op<what_nd>, QAST::Var.new( :name(get_decont_name()), :scope<local> ) )
!! QAST::Op.new( :op<what>, QAST::Var.new( :name($name), :scope<local> ) )
)
);
}
}

# Handle coercion.
# For a generic we can't know beforehand if it's going to be a coercive or any other nominalizable. Thus
# we have to fetch the instantiated parameter object and do run-time processing.
my $ptype_archetypes := $param_type.HOW.archetypes;
if $ptype_archetypes.generic {
# For a generic-typed parameter get its instantiated clone and see if its type is a coercion.
$decont_name_invalid := 1;
unless $instantiated_code {
# Produce current code object variable with the first generic-typed parameter encountered. Any
# next generic paramter would re-use the variable sparing a few CPU cycles per call.
$instantiated_code := QAST::Node.unique('__lowered_code_obj_');
$var.push(
QAST::Op.new(
:op('bind'),
QAST::Var.new(:name($instantiated_code), :scope('local'), :decl('var')),
QAST::Op.new(
:op('getcodeobj'),
QAST::Op.new(:op('curcode')))));
}
my $inst_param := QAST::Node.unique('__lowered_param_obj_');
$var.push(
my $low_param_type := QAST::Node.unique('__lowered_param_type');
$var.push( # Fetch instantiated Parameter object
QAST::Op.new(
:op('bind'),
QAST::Var.new( :name($inst_param), :scope('local'), :decl('var') ),
QAST::Op.new(
:op('atpos'),
QAST::Op.new(
:op('getattr'),
QAST::Op.new(
:op('getattr'),
QAST::Var.new(:name($instantiated_code), :scope('local')),
QAST::WVal.new(:value($Code)),
QAST::SVal.new(:value('$!signature'))),
QAST::WVal.new(:value($Sig)),
QAST::SVal.new(:value('@!params'))
),
signature_params($var),
QAST::IVal.new(:value($i)))));
my $low_param_type := QAST::Node.unique('__lowered_param_type_');
$var.push(
QAST::Op.new(
:op('if'),
QAST::Op.new(
:op('callmethod'),
:name('coercive'),
QAST::Var.new(:name($inst_param), :scope('local'))),
QAST::Op.new(
:op('bind'),
QAST::Var.new( :name($name), :scope('local') ),
QAST::Stmts.new(
$var.push( # Get actual parameter type
QAST::Op.new(
:op('bind'),
QAST::Var.new(:name($low_param_type), :scope('local'), :decl('var')),
QAST::Op.new(
:op('getattr'),
QAST::Var.new(:name($inst_param), :scope('local')),
QAST::WVal.new(:value($Param)),
QAST::SVal.new(:value('$!type')))),
QAST::SVal.new(:value('$!type')))));
$var.push(
QAST::Op.new(
:op('if'),
QAST::Op.new(
:op('istype'),
QAST::Var.new(:name($name), :scope('local')),
QAST::Var.new(:name($low_param_type), :scope('local'))
),
QAST::Op.new(
:op('if'),
QAST::Op.new(
:op('bitand_i'),
QAST::Op.new(
:op('getattr'),
QAST::Var.new(:name($inst_param), :scope('local')),
QAST::WVal.new(:value($Param)),
QAST::SVal.new(:value('$!flags'))
),
QAST::IVal.new(:value($SIG_ELEM_IS_COERCIVE))
),
QAST::Op.new(
:op('bind'),
QAST::Var.new(:name($name), :scope('local')),
QAST::Op.new(
:op('callmethod'),
:name('coerce'),
Expand All @@ -9285,26 +9326,27 @@ class Perl6::Actions is HLL::Actions does STDActions {
}
elsif nqp::can($ptype_archetypes, 'coercive') && $ptype_archetypes.coercive {
$decont_name_invalid := 1;
my $target_type := $param_type.HOW.target_type($param_type);
my $coercion_type := $param_type.HOW.wrappee($param_type, :coercion);
my $target_type := $coercion_type.HOW.target_type($coercion_type);
$*W.add_object_if_no_sc($param_type.HOW);
$*W.add_object_if_no_sc($target_type);
$var.push(QAST::Op.new(
:op('unless'),
QAST::Op.new(
:op('istype'),
QAST::Var.new( :name($name), :scope('local') ),
QAST::WVal.new( :value($target_type) )
),
$var.push(
QAST::Op.new(
:op('bind'),
QAST::Var.new( :name($name), :scope('local') ),
:op('unless'),
QAST::Op.new(
:op('callmethod'),
:name('coerce'),
QAST::WVal.new(:value($param_type.HOW)),
QAST::WVal.new(:value($param_type)),
QAST::Var.new( :name($name), :scope('local') ))))
);
:op('istype'),
QAST::Var.new( :name($name), :scope('local') ),
QAST::WVal.new( :value($target_type) )
),
QAST::Op.new(
:op('bind'),
QAST::Var.new( :name($name), :scope('local') ),
QAST::Op.new(
:op('callmethod'),
:name('coerce'),
QAST::WVal.new(:value($param_type.HOW)),
QAST::WVal.new(:value($param_type)),
QAST::Var.new( :name($name), :scope('local') )))));
}

# If it's optional, do any default handling.
Expand All @@ -9320,8 +9362,7 @@ class Perl6::Actions is HLL::Actions does STDActions {
:op('call'),
QAST::Op.new(
:op('p6capturelex'),
QAST::Op.new( :op('callmethod'), :name('clone'), $wval )
)));
QAST::Op.new( :op('callmethod'), :name('clone'), $wval ))));
}
}
else {
Expand Down Expand Up @@ -9361,26 +9402,6 @@ class Perl6::Actions is HLL::Actions does STDActions {
}
}

# If there are type captures involved - most commonly $?CLASS and
# ::?CLASS - we emit a piece of code for each target that gets the
# WHAT of the given value and binds it.
#
# In theory, we could bind a local with the result of the WHAT
# operation, but I'm not convinced it's sufficiently expensive.
if %info<type_captures> {
my $iter := nqp::iterator(%info<type_captures>);
while $iter {
$var.push( QAST::Op.new(
:op<bind>,
QAST::Var.new( :name(nqp::shift($iter)), :scope<lexical> ),
get_decont_name()
?? QAST::Op.new( :op<what_nd>, QAST::Var.new( :name(get_decont_name()), :scope<local> ) )
!! QAST::Op.new( :op<what>, QAST::Var.new( :name($name), :scope<local> ) )
)
);
}
}


# If it's the invocant, needs to go into self also.
if %info<is_invocant> {
Expand Down Expand Up @@ -9475,7 +9496,8 @@ class Perl6::Actions is HLL::Actions does STDActions {
$wrap := nqp::istype($nomtype, $Iterable) || nqp::istype($Iterable, $nomtype);
}
else {
my $coerce_nom := $param_type.HOW.nominal_target($param_type);
my $coercion_type := $param_type.HOW.wrappee($param_type, :coercion);
my $coerce_nom := $coercion_type.HOW.nominal_target($coercion_type);
$wrap := nqp::istype($coerce_nom, $Iterable) || nqp::istype($Iterable, $coerce_nom);
}
}
Expand Down
32 changes: 20 additions & 12 deletions src/Perl6/Metamodel/CoercionHOW.nqp
Expand Up @@ -5,21 +5,22 @@
# avoiding a meta-object instance per coercion type created.
class Perl6::Metamodel::CoercionHOW
does Perl6::Metamodel::LanguageRevision
does Perl6::Metamodel::Nominalizable
{
has $!composed;
has $!target_type;
has $!nominal_target;
has $!constraint_type;
has $!archetypes;

my $archetypes_g := Perl6::Metamodel::Archetypes.new(:coercive, :nominalizable, :generic);
my $archetypes_ng := Perl6::Metamodel::Archetypes.new(:coercive, :nominalizable);

method archetypes() {
unless nqp::isconcrete($!archetypes) {
$!archetypes := $!target_type.HOW.archetypes.generic || $!constraint_type.HOW.archetypes.generic
?? $archetypes_g
!! $archetypes_ng;
my $generic := $!target_type.HOW.archetypes.generic || $!constraint_type.HOW.archetypes.generic;
$!archetypes := Perl6::Metamodel::Archetypes.new(
:coercive, :nominalizable, :$generic,
definite => $!target_type.HOW.archetypes.definite,
);
}
$!archetypes
}
Expand Down Expand Up @@ -217,17 +218,24 @@ class Perl6::Metamodel::CoercionHOW
~ " " ~ $coerced_name;
}
}
unless nqp::isnull(%ex) {
%ex<X::Coerce::Impossible>($target_type_name, $value_type_name, $hint)
}
nqp::die("Impossible coercion from "
~ $value_type_name
~ " into " ~ $target_type_name
~ ": " ~ $hint);
Perl6::Metamodel::Configuration.throw_or_die(
'X::Coerce::Impossible',
"Impossible coercion from "
~ $value_type_name
~ " into " ~ $target_type_name
~ ": " ~ $hint,
:target-type($!target_type),
:from-type($value_type),
:$hint
)
}

$coerced_value
}

# Methods needed by Perl6::Metamodel::Nominalizable
method nominalizable_kind() { 'coercion' }
method !wrappee($obj) { $!target_type }
}
BEGIN {
my $root := nqp::newtype(Perl6::Metamodel::CoercionHOW.new, 'Uninstantiable');
Expand Down
5 changes: 5 additions & 0 deletions src/Perl6/Metamodel/DefiniteHOW.nqp
@@ -1,6 +1,7 @@
class Perl6::Metamodel::DefiniteHOW
#~ does Perl6::Metamodel::Naming
does Perl6::Metamodel::Documenting
does Perl6::Metamodel::Nominalizable

#~ does Perl6::Metamodel::MethodDelegation
#~ does Perl6::Metamodel::TypePretense
Expand Down Expand Up @@ -127,6 +128,10 @@ class Perl6::Metamodel::DefiniteHOW
"Raku"
)
}

# Methods needed by Perl6::Metamodel::Nominalizable
method nominalizable_kind() { 'definite' }
method !wrappee($obj) { self.base_type($obj) }
}

BEGIN {
Expand Down

0 comments on commit d37906d

Please sign in to comment.