| MooseX-Compile documentation | Contained in the MooseX-Compile distribution. |
MooseX::Compile::Compiler - The Moose metaclass .pmc compiler
my $compiler = MooseX::Compile::Compiler->new();
$compiler->compile_class(
class => "Foo::Bar",
file => $INC{"Foo/Bar.pm"},
pmc_file => "my/pmc/lib/Foo/Bar.pmc",
);
This class does the heavy lifting of emitting a .pmc and a .mopc for a
given class.
This is alpha code. You can tinker, subclass etc but beware that things definitely will change in the near future.
When a final version comes out there will be a documented process for how to extend the compiler to handle your classes, whether by subclassing or using various hooks.
| MooseX-Compile documentation | Contained in the MooseX-Compile distribution. |
#!/usr/bin/perl package MooseX::Compile::Compiler; use base qw(MooseX::Compile::Base); use strict; use warnings; use Data::Dump qw(dump); use Data::Visitor::Callback; use Storable; use B; use B::Deparse; use PadWalker; use Class::Inspector; our %compiled_classes; use constant DEBUG => MooseX::Compile::Base::DEBUG(); # FIXME make this Moose based eventually sub new { my ( $class, %args ) = @_; bless \%args, $class; } sub compile_class { my ( $self, %args ) = @_; my $class = $args{class}; ( my $short_name = "$class.pm" ) =~ s{::}{/}g; $args{short_name} = $short_name; unless ( defined $args{file} ) { $args{file} = $INC{$short_name}; } unless ( defined $args{pmc_file} ) { $args{pmc_file} = "$args{file}c"; } if ( $compiled_classes{$class}++ ) { warn "already compiled class '$class'\n" if DEBUG; return; } my $t = times; $self->cache_meta(%args); $self->write_pmc_file(%args); warn "compilation of .pmc and .mopc for class '$class' took " . ( times - $t ) . "s\n" if DEBUG; } # FIXME these should really be methods, I suppose sub sym ($$;@) { my ( $sym, $type, @args ) = @_; bless { @args, name => $sym }, "MooseX::Compile::mangled::$type"; } sub package_name ($;$) { my ( $code, $cv ) = @_; $cv ||= B::svref_2object($code); local $@; return eval { $cv->GV->STASH->NAME }; } sub code_name ($;$) { my ( $code, $cv ) = @_; $cv ||= B::svref_2object($code); local $@; return eval { join("::", package_name($code, $cv), $cv->GV->NAME) }; } sub verified_code_name ($;$) { my ( $code, $cv ) = @_; if ( my $name = code_name($code, $cv) ) { if ( verify_code_name($code, $name) ) { return $name; } } return; } sub verify_code_name ($$) { my ( $code, $name ) = @_; no strict 'refs'; \&$name == $code; } sub subref ($;$) { my ( $code, $name ) = @_; if ( ref $code ) { my $cv = B::svref_2object($code); $name ||= code_name($code, $cv); if ( $name && verify_code_name($code,$name) ) { my @args; if ( -f ( my $file = $cv->FILE ) ) { my %rev_inc = reverse %INC; push @args, file => $rev_inc{$file} if $rev_inc{$file} !~ /^(?:Moose|metaclass)\.pm$/; } return sym( $name, "subref", @args ); } else { warn "$code has name '$name', but it doesn't point back to the cv" if $name; require Data::Dumper; no strict 'refs'; local $Data::Dumper::Deparse = 1; warn Data::Dumper::Dumper({ name => $name, name_strval => ("". \&$name), name_ref => \&$name, arg_ref => $code, arg_strval => "$code", }); die "Can't make a symbolic ref to $code, it has no name or the name is invalid"; } } else { return sym($code, "subref"); } } sub create_visitor { my ( $self, %args ) = @_; my $class = $args{class}; Data::Visitor::Callback->new( "object" => sub { my ( $self, $obj ) = @_; return $obj if $obj->isa("Moose::Meta::TypeConstraint"); $self->visit_ref($obj); }, object_final => sub { my ( $self, $obj ) = @_; if ( ref($obj) =~ /^Class::MOP::Class::__ANON__::/x ) { die "Instance of anonymous class cannot be thawed: $obj"; } return $obj; }, "Class::MOP::Class" => sub { my ( $self, $meta ) = @_; if ( $meta->is_immutable ) { my $options = $meta->immutable_transformer->options; bless( $meta, $meta->{___original_class} ), # it's a copy, we can rebless return bless { class => $meta, options => $options, }, "MooseX::Compile::mangled::immutable_metaclass"; } if ( $meta->is_anon_class ){ warn "Can't reliably store anonymouse metaclasses yet"; } return $meta; }, "Moose::Meta::TypeConstraint" => sub { my ( $self, $constraint ) = @_; if ( defined ( my $name = $constraint->name ) ) { return sym $name, "constraint"; } else { warn "Anonymous constraint $constraint left in metaclass"; return $constraint; } }, code => sub { my ( $self, $code ) = @_; if ( my $subname = code_name($code) ) { if ( $subname =~ /^Moose::Meta::Method::\w+::(.*)$/ ) { # FIXME should this be verified more closely? # sometimes the coderef $code doesn't match \&{ $class::$1 } return subref "${class}::$1"; } elsif ( $subname =~ /^(?:Moose|metaclass)::([^:]+)$/ ) { my $method = $1; if ( $method eq 'meta' ) { return subref "${class}::meta"; } else { die "subname: $subname"; } } elsif ( $subname !~ /__ANON__$/ ) { return subref $code, $subname; } else { warn "Unable to locate symbol for $code ($subname) found in $class"; use B::Deparse; warn B::Deparse->new->coderef2text($code); return $code; } } return $code; }, ); } sub deflate_meta { my ( $self, %args ) = @_; my $meta = $args{meta}; my $visitor = $self->create_visitor(%args); $visitor->visit($meta); } sub cache_meta { my ( $self, %args ) = @_; my $class = $args{class}; my $meta = $self->deflate_meta( %args, meta => $class->meta ); $self->store_meta( %args, meta => $meta ); } sub store_meta { my ( $self, %args ) = @_; my $meta = $args{meta}; my $mopc_file = $self->cached_meta_file(%args); $mopc_file->dir->mkpath; local $@; eval { Storable::nstore( $meta, $mopc_file ) }; if ( $@ ) { require YAML; no warnings 'once'; $YAML::UseCode = 1; die join("\n", $@, YAML::Dump($meta) ); } if ( DEBUG ) { warn "stored $meta in '$mopc_file'\n"; } return 1; } sub method_category_filters { my ( $self, %args ) = @_; return ( # FIXME recognize aliased methods sub { my ( $self, $entry ) = @_; no warnings 'uninitialized'; return "meta" if $entry->{name} eq 'meta' and package_name($entry->{body}) =~ /^(?: Moose | metaclass )/x, }, sub { my ( $self, $entry ) = @_; return "generated" if $entry->{meta}->isa("Class::MOP::Method::Generated"); }, sub { my ( $self, $entry ) = @_; return "file" if B::svref_2object($entry->{body})->FILE eq $args{file}; }, sub { "unknown_methods" }, ); } sub function_category_filters { my ( $self, %args ) = @_; return ( # FIXME check for Moose exports, too (Scalar::Util stuff, etc) sub { my ( $self, $entry ) = @_; no warnings 'uninitialized'; return "moose_sugar" if package_name($entry->{body}) eq 'Moose'; }, sub { "unknown_functions" }, ); } sub extract_code_symbols { my ( $self, %args ) = @_; my $class = $args{class}; my %seen; my %categorized_symbols; { my @method_filters = $self->method_category_filters(%args); my $method_map = $class->meta->get_method_map; foreach my $name ( sort keys %$method_map ) { $seen{$name}++; my $method = $method_map->{$name}; my $body = $method->body; my $entry = { name => $name, meta => $method, body => $body }; foreach my $filter ( @method_filters ) { if ( my $category = $self->$filter($entry) ) { push @{ $categorized_symbols{$category} ||= [] }, $entry; last; } } } } { my %symbols; @symbols{@{ Class::Inspector->functions($class) || [] }} = @{ Class::Inspector->function_refs($class) || [] }; my @function_filters = $self->function_category_filters(%args); foreach my $name ( sort grep { not $seen{$_}++ } keys %symbols ) { my $body = $symbols{$name}; my $entry = { name => $name, body => $body }; foreach my $filter ( @function_filters ) { if ( my $category = $self->$filter($entry) ) { push @{ $categorized_symbols{$category} ||= [] }, $entry; last; } } } } return %categorized_symbols; } sub compile_code_symbols { my ( $self, %args ) = @_; my $symbols = $args{all_symbols}; my @ret; foreach my $category ( @{ $args{'symbol_categories'} } ) { my $method = "compile_${category}_code_symbols"; push @ret, $self->$method( %args, symbols => delete($symbols->{$category}) ); } @ret; } sub compile_file_code_symbols { # this is already taken care of by the inclusion of the whole .pm after the preamble return; } sub compile_meta_code_symbols { # we fake this one return; } sub compile_moose_exports_code_symbols { # not yet implemented return; } sub compile_moose_sugar_code_symbols { my ( $self, %args ) = @_; return map { my $name = $_->{name}; my $proto = prototype($_->{body}); $proto = $proto ? " ($proto)" : ""; "*$name = Sub::Name::subname('Moose::$name', sub$proto { });"; } @{ $args{symbols} || [] }; } sub compile_generated_code_symbols { my ( $self, %args ) = @_; map { sprintf "*%s = %s;", $_->name => $self->compile_method(%args, method => $_) } map { $_->{meta} } @{ $args{symbols} }; } sub compile_aliased_code_symbols { return; } sub compile_unknown_method_code_symbols { return; } sub compile_unknown_function_code_symbols { return; } sub compile_method { my ( $self, %args ) = @_; my ( $class, $method ) = @args{qw(class method)}; my $d = B::Deparse->new; my $body = $method->body; my $body_str = $d->coderef2text($body); my $closure_vars = PadWalker::closed_over($body); my @env; if ( my $constraints = delete $closure_vars->{'@type_constraints'} ) { my @constraint_code = map { my $name = $_->name; defined $name ? "Moose::Util::TypeConstraints::find_type_constraint(". dump($name) .")" : "die 'missing constraint'" } @$constraints; push @env, "CORE::require Moose::Util::TypeConstraints::OptimizedConstraints", join("\n ", 'my @type_constraints = (', map { "$_," } @constraint_code ) . "\n)", } push @env, map { my $ref = $closure_vars->{$_}; my $scalar = ref($ref) eq 'SCALAR' || ref($ref) eq 'REF'; "my $_ = " . ( $scalar ? $self->_value_to_perl($$ref) : "(" . join(", ", map { $self->_value_to_perl($_) } @$ref ) . ")" ) } keys %$closure_vars; my $name = code_name($body); my $quoted_name = dump($name); if ( @env ) { my $env = join(";\n\n", @env); $env =~ s/^/ /gm; return "Sub::Name::subname( $quoted_name, do {\n$env;\n\n\nsub $body_str\n})"; } else { return "Sub::Name::subname( $quoted_name, sub $body_str )"; } } sub _value_to_perl { my ( $self, $value ) = @_; ( (ref($value)||'') eq 'CODE' ? $self->_subref_to_perl($value) : Data::Dump::dump($value) ) } sub _subref_to_perl { my ( $self, $subref ) = @_; my %rev_inc = reverse %INC; if ( ( my $name = code_name($subref) ) !~ /__ANON__$/ ) { if ( -f ( my $file = B::svref_2object($subref)->FILE ) ) { return "do { require " . dump($rev_inc{$file}) . "; \\&$name }"; } else { return '\&' . $name; } } else { "sub " . B::Deparse->new->coderef2text($subref); } } sub write_pmc_file { my ( $self, %args ) = @_; my ( $class, $short_name, $file, $pmc_file ) = @args{qw(class short_name file pmc_file)}; $pmc_file->dir->mkpath; open my $pm_fh, "<", $file or die "open($file): $!"; open my $pmc_fh, ">", "$pmc_file" or die "Can't write .pmc, open($pmc_file): $!"; local $/; my $pm = <$pm_fh>; close $pm_fh; print $pmc_fh "$1\n\n" if $pm =~ /^(\#\!.*)/; # copy shebang print $pmc_fh $self->pmc_preamble( %args ), "\n"; print $pmc_fh "# verbatim copy of $file follows\n"; print $pmc_fh "# line 1\n"; print $pmc_fh $pm; close $pmc_fh or die "Can't write .pmc, close($pmc_file): $!"; warn "wrote PMC file '$pmc_file'\n" if DEBUG; } sub pmc_preamble_comment { my ( $self, %args ) = @_; return <<COMMENT; # This file is generated by MooseX::Compile, and contains a cached # version of the class '$args{class}'. COMMENT } sub pmc_preamble_header { my( $self, %args ) = @_; my $class = $args{class}; return join("\n\n\n", map { my $method = "pmc_preamble_header_$_"; $self->$method(%args) } $self->pmc_preamble_header_pieces(%args) ); } sub pmc_preamble_header_pieces { return qw(timing modules register_pmc hide_moose); } sub pmc_preamble_header_timing { return <<'TIMING'; # used in debugging output if any my $__mx_compile_t; BEGIN { $__mx_compile_t = times } TIMING } sub pmc_preamble_header_modules { return <<'MODULES' # load a few modules we need use Sub::Name (); use Scalar::Util (); MODULES } sub pmc_preamble_header_register_pmc { my ( $self, %args ) = @_; my ( $quoted_class, $version ) = @args{qw(quoted_class quoted_compiler_version)}; return <<REGISTER; # Register this file as a PMC use MooseX::Compile::Bootstrap ( class => $quoted_class, file => __FILE__, version => $version, ); REGISTER } sub pmc_preamble_header_hide_moose { my ( $self, %args ) = @_; my $hide = <<'#\'HIDE_MOOSE'; #\ # disable requiring and importing of Moose from this compile class my ( $__mx_compile_prev_require, %__mx_compile_overridden_imports ); BEGIN { $__mx_compile_prev_require = defined &CORE::GLOBAL::require ? \&CORE::GLOBAL::require : undef; no warnings 'redefine'; # FIXME move this to Bootstrap? Bootstrap->override_global_require( class => $$quoted_class$$ )? *CORE::GLOBAL::require = sub { my ( $faked_class ) = ( $_[0] =~ m/^ ( Moose | metaclass ) \.pm $/x ); return 1 if caller() eq $$quoted_class$$ and $faked_class; my $hook; if ( $faked_class and not $INC{$_[0]} ) { # load Moose or metaclass in a clean env, and then wrap it's import() no strict 'refs'; my $import = "${faked_class}::import"; my $wrapper = \&$import; undef *$import; # clean out the symbol so it doesn't warn about redefining $hook = bless [sub { $__mx_compile_overridden_imports{$faked_class} = \&$import; # stash the real import *$import = $wrapper; }], "MooseX::Compile::Scope::Guard"; } if ( $__mx_compile_prev_require ) { &$__mx_compile_prev_require; } else { require $_[0]; } }; foreach my $class qw(Moose metaclass) { no strict 'refs'; my $import = "${class}::import"; $__mx_compile_overridden_imports{$class} = defined &$import && \&$import; *$import = sub { if ( caller eq $$quoted_class$$ ) { if ( $class eq 'Moose' ) { strict->import; warnings->import; } return; } if ( my $sub = $__mx_compile_overridden_imports{\$class} ) { goto $sub; } return; }; } } #'HIDE_MOOSE $hide =~ s/\$\$(\w+)\$\$/$args{$1}/ge; return $hide; } sub pmc_preamble_setup_env { my ( $self, %args ) = @_; my $class = $args{class}; my $quoted_class = dump($class); my $decl = $self->pmc_preamble_class_def_for_begin(%args); return <<ENV; # stub the sugar BEGIN { package $class; my \$fake_meta = bless { name => $quoted_class }, "MooseX::Compile::MetaBlackHole"; sub meta { \$fake_meta } $decl our \$__mx_is_compiled = 1; } ENV } sub pmc_preamble_class_def_for_begin { my ( $self, %args ) = @_; join("\n\n", $self->compile_code_symbols( %args, symbol_categories => [qw(moose_sugar moose_exports)] ) ); } sub pmc_preamble_at_end { my ( $self, %args ) = @_; my ( $class, $code ) = @args{qw(class code)}; return <<HOOK # try to approximate the time that Moose generated code enters the class # this presumes you didn't stick the moose sugar in a BEGIN { } block my \$__mx_compile_run_at_end = bless [ sub { $code } ], "MooseX::Compile::Scope::Guard"; HOOK } sub pmc_preamble_unhide_moose { my ( $self, %args ) = @_; return <<'#\'UNHIDE_MOOSE'; #\ # un-hijack CORE::GLOBAL::require so that it no longer hides Moose from this class # and undo the import wrappers that likewise prevent importing if it's already loaded foreach my $class ( keys %__mx_compile_overridden_imports ) { my $import = "${class}::import"; no strict 'refs'; if ( my $prev = delete $__mx_compile_overridden_imports{$class} ) { no warnings 'redefine'; *$import = $prev; } else { delete ${ "${class}::" }{import}; } } if ( $__mx_compile_prev_require ) { no warnings 'redefine'; *CORE::GLOBAL::require = $__mx_compile_prev_require; } else { delete $CORE::GLOBAL::{require}; } #'UNHIDE_MOOSE } sub pmc_preamble_generated_code { my ( $self, %args ) = @_; my $class = $args{class}; return $self->pmc_preamble_at_end( %args, code => join("\n\n", $self->pmc_preamble_unhide_moose(%args), $self->pmc_preamble_generated_code_body(%args), qq{warn "loading of class '$class' finished in " . (times - \$__mx_compile_t) . "s\\n" if MooseX::Compile::Base::DEBUG();}, ), ); } sub pmc_preamble_generated_code_body { my ( $self, %args ) = @_; my $class = $args{class}; my $quoted_class = dump($class); return join("\n", "package $class;", $self->pmc_preamble_class_def_for_end(%args), qq{warn "bootstrap of class '$class' finished in " . (times - \$__mx_compile_t) . "s\\n" if MooseX::Compile::Base::DEBUG();}, ); } sub pmc_preamble_class_def_for_end { my ( $self, %args ) = @_; return ( $self->pmc_preamble_define_isa(%args), $self->pmc_preamble_define_code_symbols(%args), $self->pmc_preamble_call_post_hook(%args), ); } sub pmc_preamble_define_isa { my ( $self, %args ) = @_; my $ISA = dump($args{class}->meta->superclasses); return <<ISA our \@ISA = $ISA; MooseX::Compile::Bootstrap->load_classes(\@ISA); ISA } sub pmc_preamble_define_code_symbols { my ( $self, %args ) = @_; return ( $self->compile_code_symbols(%args, symbol_categories => [qw(generated aliased)]), $self->pmc_preamble_faked_code_symbols(%args), ); } sub pmc_preamble_faked_code_symbols { my ( $self, %args ) = @_; return <<METHODS { no warnings 'redefine'; *meta = Sub::Name::subname("Moose::meta", sub { MooseX::Compile::Bootstrap->load_cached_meta( class => __PACKAGE__, pmc_file => __FILE__ . 'c' ) }); } METHODS } sub pmc_preamble_call_post_hook { my ( $self, %args ) = @_; my $class = $args{class}; return <<HOOK ${class}::__mx_compile_post_hook() if defined \&${class}::__mx_compile_post_hook; HOOK } sub pmc_preamble { my ( $self, %args ) = @_; my ( $class, $file ) = @args{qw(class file)}; ( my $short_name = "$class.pm" ) =~ s{::}{/}g; $args{short_name} = $short_name; $args{quoted_class} = dump($class); $args{compiler_version} = $MooseX::Compile::Base::VERSION; $args{quoted_compiler_version} = dump($MooseX::Compile::Base::VERSION); $args{all_symbols} = { $self->extract_code_symbols(%args) }; my $code = join("\n", $self->pmc_preamble_comment(%args), $self->pmc_preamble_header(%args), $self->pmc_preamble_setup_env(%args), $self->pmc_preamble_generated_code(%args), $self->pmc_preamble_footer(%args), ); delete @{ $args{all_symbols} }{qw(file meta unknown_methods unknown_functions)}; if ( DEBUG && keys %{ $args{all_symbols} } ) { use Data::Dumper; warn "leftover symbols: " . Dumper($args{all_symbols}); } return $code; } sub pmc_preamble_footer { my ( $self, %args ) = @_; return <<FOOTER BEGIN { warn "giving control back to original '$args{short_name}', bootstrap preamble took " . (times - \$__mx_compile_t) . "s\\n" if MooseX::Compile::Base::DEBUG() } FOOTER } __PACKAGE__ __END__