A lot depends on how you intend on using this datastructure. One way is "Hey, mister! Give me all the valid moves for this piece at this location." Another way is "Hey, mister! Is this move legal for this piece at this location?"
Now, with hashes, you can do both. Build it with $isLegal{ $Piece }{ $Start }{ $End } = !!1;. Now, you can either query it with exists $isLegal{ $Piece }{ $Start }{ $End } to determine if $Start->$End is legal for $Piece or you can do a keys %{ $isLegal{ $Piece }{ $Start } }; to get a list of the legal moves for $Piece at $Start.
It would be best to encapsulate this data structure in an object so that you can simply do things like:
foreach my $move ( $LegalMoves->get_legal_moves( $Piece, $Start ) ) {
# Do stuff here
}
Now, the generation part. This is very simple, assuming you have an algorithm that will determine if a given move is legal for a given piece. This is going to require computation, but you're only doing this once. You could even do this offline and store it with DBM::Deep. I strongly recommend doing this.
foreach my $Piece (@list_of_pieces) {
foreach my $Start (@list_of_squares) {
foreach my $End (@list_of_squares) {
# You supply the is_legal_move() function!!
next unless is_legal_move( $Piece, $Start, $End );
$isLegal{ $Piece }{ $Start }{ $End } = !!1;
}
}
}