use strict;
use warnings;
package Clownfish::Class;
use base qw( Clownfish::Symbol );
use Carp;
use Config;
use Clownfish::Function;
use Clownfish::Method;
use Clownfish::Util qw(
verify_args
a_isa_b
);
use Clownfish::Dumpable;
use File::Spec::Functions qw( catfile );
use Scalar::Util qw( reftype );
our %create_PARAMS = (
source_class => undef,
class_name => undef,
cnick => undef,
parent_class_name => undef,
methods => undef,
functions => undef,
member_vars => undef,
inert_vars => undef,
docucomment => undef,
inert => undef,
final => undef,
parcel => undef,
attributes => undef,
exposure => 'parcel',
);
my $dumpable = Clownfish::Dumpable->new;
our %registry;
# Testing only.
sub _zap { delete $registry{ +shift } }
our %fetch_singleton_PARAMS = (
parcel => undef,
class_name => undef,
);
sub fetch_singleton {
my ( undef, %args ) = @_;
verify_args( \%fetch_singleton_PARAMS, %args ) or confess $@;
# Start with the class identifier.
my $class_name = $args{class_name};
confess("Missing required param 'class_name'") unless defined $class_name;
$class_name =~ /(\w+)$/ or confess("Invalid class name: '$class_name'");
my $key = $1;
# Maybe prepend parcel prefix.
my $parcel = $args{parcel};
if ( defined $parcel ) {
if ( !a_isa_b( $parcel, "Clownfish::Parcel" ) ) {
$parcel = Clownfish::Parcel->singleton( name => $parcel );
}
$key = $parcel->get_prefix . $key;
}
return $registry{$key};
}
sub new { confess("The constructor for Clownfish::Class is create()") }
sub create {
my ( $class_class, %args ) = @_;
verify_args( \%create_PARAMS, %args ) or confess $@;
$args{class_cnick} = $args{cnick};
my $self = $class_class->SUPER::new(
%create_PARAMS,
micro_sym => 'class',
struct_sym => undef,
methods => [],
overridden => {},
functions => [],
member_vars => [],
children => [],
parent => undef,
attributes => {},
autocode => '',
tree_grown => 0,
%args,
);
$self->{cnick} ||= $self->{class_cnick};
# Make it possible to look up methods and functions by name.
$self->{meth_by_name}{ $_->micro_sym } = $_ for $self->methods;
$self->{func_by_name}{ $_->micro_sym } = $_ for $self->functions;
# Derive struct name.
confess("Missing required param 'class_name'") unless $self->{class_name};
$self->{class_name} =~ /(\w+)$/;
$self->{struct_sym} = $1;
# Verify that members of supplied arrays meet "is a" requirements.
for ( @{ $self->{functions} } ) {
confess("Not a Clownfish::Function")
unless a_isa_b( $_, 'Clownfish::Function' );
}
for ( @{ $self->{methods} } ) {
confess("Not a Clownfish::Method")
unless a_isa_b( $_, 'Clownfish::Method' );
}
for ( @{ $self->{member_vars} }, @{ $self->{inert_vars} } ) {
confess("Not a Clownfish::Variable")
unless a_isa_b( $_, 'Clownfish::Variable' );
}
# Assume that Foo::Bar should be found in Foo/Bar.h.
$self->{source_class} = $self->{class_name}
unless defined $self->{source_class};
# Validate attributes.
confess("Param 'attributes' not a hashref")
unless reftype( $self->{attributes} ) eq 'HASH';
# Store in registry.
my $key = $self->full_struct_sym;
my $existing = $registry{$key};
if ($existing) {
confess( "New class $self->{class_name} conflicts with previously "
. "compiled class $existing->{class_name}" );
}
$registry{$key} = $self;
# Validate inert param.
confess("Inert classes can't have methods")
if ( $self->{inert} and @{ $self->{methods} } );
return $self;
}
sub file_path {
my ( $self, $base_dir, $ext ) = @_;
my @components = split( '::', $self->{source_class} );
unshift @components, $base_dir
if defined $base_dir;
$components[-1] .= $ext;
return catfile(@components);
}
sub include_h {
my $self = shift;
my @components = split( '::', $self->{source_class} );
$components[-1] .= '.h';
return join( '/', @components );
}
sub has_attribute { exists $_[0]->{attributes}{ $_[1] } }
sub get_cnick { shift->{cnick} }
sub get_struct_sym { shift->{struct_sym} }
sub get_parent_class_name { shift->{parent_class_name} }
sub get_source_class { shift->{source_class} }
sub get_docucomment { shift->{docucomment} }
sub get_parent { shift->{parent} }
sub get_autocode { shift->{autocode} }
sub inert { shift->{inert} }
sub final { shift->{final} }
sub set_parent { $_[0]->{parent} = $_[1] }
sub full_struct_sym { $_[0]->get_prefix . $_[0]->{struct_sym} }
sub short_vtable_var { uc( shift->{struct_sym} ) }
sub full_vtable_var { $_[0]->get_PREFIX . $_[0]->short_vtable_var }
sub full_vtable_type { shift->full_vtable_var . '_VT' }
sub append_autocode { $_[0]->{autocode} .= $_[1] }
sub functions { @{ shift->{functions} } }
sub methods { @{ shift->{methods} } }
sub member_vars { @{ shift->{member_vars} } }
sub inert_vars { @{ shift->{inert_vars} } }
sub children { @{ shift->{children} } }
sub novel_methods {
my $self = shift;
return
grep { $_->get_class_cnick eq $self->{cnick} } @{ $self->{methods} };
}
sub novel_member_vars {
my $self = shift;
return
grep { $_->get_class_cnick eq $self->{cnick} }
@{ $self->{member_vars} };
}
sub function {
my ( $self, $micro_sym ) = @_;
return $self->{func_by_name}{ lc($micro_sym) };
}
sub method {
my ( $self, $micro_sym ) = @_;
return $self->{meth_by_name}{ lc($micro_sym) };
}
sub novel_method {
my ( $self, $micro_sym ) = @_;
my $method = $self->{meth_by_name}{ lc($micro_sym) };
if ( defined $method
and $method->get_class_cnick eq $self->get_class_cnick )
{
return $method;
}
else {
return;
}
}
sub add_child {
my ( $self, $child ) = @_;
confess("Can't call add_child after grow_tree") if $self->{tree_grown};
push @{ $self->{children} }, $child;
}
sub add_method {
my ( $self, $method ) = @_;
confess("Not a Method") unless a_isa_b( $method, "Clownfish::Method" );
confess("Can't call add_method after grow_tree") if $self->{tree_grown};
confess("Can't add_method to an inert class") if $self->{inert};
push @{ $self->{methods} }, $method;
$self->{meth_by_name}{ $method->micro_sym } = $method;
}
# Create dumpable functions unless hand coded versions were supplied.
sub _create_dumpables {
my $self = shift;
$dumpable->add_dumpables($self) if $self->has_attribute('dumpable');
}
sub grow_tree {
my $self = shift;
confess("Can't call grow_tree more than once") if $self->{tree_grown};
$self->_establish_ancestry;
$self->_bequeath_member_vars;
$self->_generate_automethods;
$self->_bequeath_methods;
$self->{tree_grown} = 1;
}
# Let the children know who their parent class is.
sub _establish_ancestry {
my $self = shift;
for my $child ( @{ $self->{children} } ) {
# This is a circular reference and thus a memory leak, but we don't
# care, because we have to have everything in memory at once anyway.
$child->{parent} = $self;
$child->_establish_ancestry;
}
}
# Pass down member vars to from parent to children.
sub _bequeath_member_vars {
my $self = shift;
for my $child ( @{ $self->{children} } ) {
unshift @{ $child->{member_vars} }, @{ $self->{member_vars} };
$child->_bequeath_member_vars;
}
}
# Create auto-generated methods. This must be called after member vars are
# passed down but before methods are passed down.
sub _generate_automethods {
my $self = shift;
$self->_create_dumpables;
for my $child ( @{ $self->{children} } ) {
$child->_generate_automethods;
}
}
sub _bequeath_methods {
my $self = shift;
for my $child ( @{ $self->{children} } ) {
# Pass down methods, with some being overridden.
my @common_methods; # methods which child inherits or overrides
for my $method ( @{ $self->{methods} } ) {
if ( exists $child->{meth_by_name}{ $method->micro_sym } ) {
my $child_method
= $child->{meth_by_name}{ $method->micro_sym };
$child_method->override($method);
push @common_methods, $child_method;
}
else {
$child->{meth_by_name}{ $method->micro_sym } = $method;
push @common_methods, $method;
}
}
# Create array of methods, preserving exact order so vtables match up.
my @new_method_set;
my %seen;
for my $meth ( @common_methods, @{ $child->{methods} } ) {
next if $seen{ $meth->micro_sym };
$seen{ $meth->micro_sym } = 1;
if ( $child->final ) {
$meth = $meth->finalize if $child->final;
$child->{meth_by_name}{ $meth->micro_sym } = $meth;
}
push @new_method_set, $meth;
}
$child->{methods} = \@new_method_set;
# Pass it all down to the next generation.
$child->_bequeath_methods;
$child->{tree_grown} = 1;
}
}
sub tree_to_ladder {
my $self = shift;
my @ladder = ($self);
for my $child ( @{ $self->{children} } ) {
push @ladder, $child->tree_to_ladder;
}
return @ladder;
}
1;
__END__
__POD__
=head1 NAME
Clownfish::Class - An object representing a single class definition.
=head1 CONSTRUCTORS
Clownfish::Class objects are stored as quasi-singletons, one for each
unique parcel/class_name combination.
=head2 fetch_singleton
my $class = Clownfish::Class->fetch_singleton(
parcel => 'Crustacean',
class_name => 'Crustacean::Lobster::LobsterClaw',
);
Retrieve a Class, if one has already been created.
=head2 create
my $class = Clownfish::Class->create(
parcel => 'Crustacean', # default: special
class_name => 'Crustacean::Lobster::LobsterClaw', # required
cnick => 'LobClaw', # default: special
exposure => 'public', # default: 'parcel'
source_class => undef, # default: same as class_name
parent_class_name => 'Crustacean::Claw', # default: undef
inert => undef, # default: undef
methods => \@methods, # default: []
functions => \@funcs, # default: []
member_vars => \@members, # default: []
inert_vars => \@inert_vars, # default: []
docucomment => $documcom, # default: undef,
attributes => \%attributes, # default: {}
);
Create and register a quasi-singleton. May only be called once for each
unique parcel/class_name combination.
=over
=item * B<parcel>, B<class_name>, B<cnick>, B<exposure> - see
L<Clownfish::Symbol>.
=item * B<source_class> - The name of the class that owns the file in which
this class was declared. Should be "Foo" if "Foo::FooJr" is defined in
C<Foo.cfh>.
=item * B<parent_class_name> - The name of this class's parent class. Needed
in order to establish the class hierarchy.
=item * B<inert> - Should be true if the class is inert, i.e. cannot be
instantiated.
=item * B<methods> - An array where each element is a Clownfish::Method.
=item * B<functions> - An array where each element is a Clownfish::Method.
=item * B<member_vars> - An array where each element is a
Clownfish::Variable and should be a member variable in each instantiated
object.
=item * B<inert_vars> - An array where each element is a
Clownfish::Variable and should be a shared (class) variable.
=item * B<docucomment> - A Clownfish::DocuComment describing this Class.
=item * B<attributes> - An arbitrary hash of attributes.
=back
=head1 METHODS
=head2 get_cnick get_struct_sym get_parent_class_name get_source_class
get_docucomment get_parent get_autocode inert final
Accessors.
=head2 set_parent
$class->set_parent($ancestor);
Set the parent class.
=head2 add_child
$class->add_child($child_class);
Add a child class.
=head2 add_method
$class->add_method($method);
Add a Method to the class. Valid only before grow_tree() is called.
=head2 function
my $do_stuff_function = $class->function("do_stuff");
Return the inert Function object for the supplied C<micro_sym>, if any.
=head2 method
my $pinch_method = $class->method("Pinch");
Return the Method object for the supplied C<micro_sym> / C<macro_sym>, if any.
=head2 novel_method
my $pinch_method = $class->novel_method("Pinch");
Return a Method object if the Method corresponding to the supplied string is
novel.
=head2 children
my @child_classes = $class->children;
Return all child classes as a list.
=head2 functions
my @functions = $class->functions;
Return all (inert) functions as a list.
=head2 methods
my @methods = $class->methods;
Return all methods as a list.
=head2 inert_vars
my @inert_vars = $class->inert_vars;
Return all inert (shared, class) variables as a list.
=head2 member_vars
my @members = $class->member_vars;
Return all member variables as a list.
=head2 novel_methods
my @novel_methods = $class->novel_methods;
Return all novel methods as a list.
=head2 novel_member_vars
my @new_members = $class->novel_member_vars;
Return all novel member variables as a list.
=head2 grow_tree
$class->grow_tree;
Bequeath all inherited methods and members to children.
=head2 tree_to_ladder
my @ordered = $class->tree_to_ladder;
Return this class and all its child classes as an array, where all children
appear after their parent nodes.
=head2 file_path
# /path/to/Foo/Bar.c, if source class is Foo::Bar.
my $path = $class->file_path( '/path/to', '.c' );
Provide an OS-specific path for a file relating to this class could be found,
by joining together the components of the C<source_class> name.
=head2 include_h
my $relative_path = $class->include_h;
Return a relative path to a C header file, appropriately formatted for a
pound-include directive.
=head2 append_autocode
$class->append_autocode($code);
Append auxiliary C code.
=head2 short_vtable_var
The short name of the global VTable object for this class.
=head2 full_vtable_var
Fully qualified vtable variable name, including the parcel prefix.
=head2 full_vtable_type
The fully qualified C type specifier for this class's vtable, including the
parcel prefix. Each vtable needs to have its own type because each has a
variable number of methods at the end of the struct, and it's not possible to
initialize a static struct with a flexible array at the end under C89.
=head2 full_struct_sym
Fully qualified struct symbol, including the parcel prefix.
=head1 COPYRIGHT AND LICENSE
Copyright 2008-2011 Marvin Humphrey
This program is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.
=cut