[SPOILER] Re: Perl 'Expert' Quiz-of-the-Week #24 (Module dependency evaluation)
Marc Prewitt <mprewitt-DgHDs1fpoj1Wk0Htik3J/[email protected]> Tue, 28 Sep 2004 12:48:11 -0400
| Newsgroups | gmane.comp.lang.perl.qotw.discuss |
|---|---|
| Message-ID | <[email protected]> |
Just read MJD's posting about Randy's dissapointment on solutions. So,
I thought I'd send mine even though it's not complete. However, I've
enjoyed working on this and if Randy likes the way this solution is
going, I'd also be willing to continue to complete it.
I've done a few projects which needed small parsers and just ended up
doing them by hand. I thought I could do this the same way but had been
itching to use a 'real' parser. I thought this would be a good
opportunity to learn one and choose to try out Parse::RecDescent.
The nice thing about using RecDescent is that you don't actually need
Parse::RecDescent after you've generated the parser code from the
grammar. In other words, this solution wouldn't require
Parse::RecDescent to run when distributed.
I only had limited time to work on this so my first few were spent
figuring out how to use RecDescent and whether I wanted RecDescent to do
all the work or just generate a tree which I could later walkt and
evaluate. I choose the tree.
The tree which is created is comprised of terminal nodes which are
package names or version numbers. The non-terminal nodes are operators,
either boolean operators, module comparison operators or a special
'exists' operator.
When walking the tree, the 'exists' and 'comparison' operators call the
Versions->from_module() to determine if a module dependency is met.
They return a special 'pkg' object whose boolean value indicates whether
or not the dependency is met and whose string value describes the
dependency (pkg==ver, pkg>ver, pkg, etc...) The boolean operators
simply do their boolean thing on the 'pkg' objects and return the
result. In the end, you have a 'pkg' object which is the met or unmet
dependency. If the pkg object's boolean value is false, it's string
value is the name of the unmet dependency.
The one bug with this right now is short circuiting. It currently
shorts circuits if the first module dependency in an AND/XOR expression
fails. I think this can be easily changed by evaluating the truth of
all operands for AND/XOR operators and returning a list of the untrue
ones instead of a single value.
I still need to complete the grammar to include xor, not and parenthesis
and macros.
A packaged version with Makefile.PL and sample tests is available at:
http://www.chelsea.net/~mprewitt/Prereq/PrereqExpr-2004928.tar.gz
Here's grammar:
startrule: expr 'EOF'
expr: disjunction
| conjunction
| classdef
disjunction: <leftop: conjunction '||' conjunction>
{
my $node;
$node->{op} = 'or';
$node->{operands} = $item[1];
$return = $node;
}
conjunction: <leftop: classdef '&&' classdef>
{
my $node;
$node->{op} = 'and';
$node->{operands} = $item[1];
$return = $node;
}
classdef:
pkg compare version
{
my $node;
$node->{op} = $item{compare};
$node->{operands} = [ $item{pkg}, $item{version} ];
$return = $node;
}
| pkg
{
my $node;
$node->{op} = "exists";
$node->{operands} = $item{pkg};
$return = $node;
}
version: /\S+/
compare: />=/ | /<=/ | /==/ | />/ | /</
pkg: /\w+(?:\:\:\w+)*/
Here's a Prereq::Expr module with the 'eval' method which takes a
requires string like:
q[
( DBD::Pg > 1.1 && DateTime::Format::Pg )
||
( DBD::mysql <= 1.2 && DateTime::Format::mysql )
]
#
# $Id: Expr.pm,v 1.2 2004/09/25 23:31:50 mprewitt Exp mprewitt $
#
package Prereq::Expr;
use strict;
use vars qw( $VERSION );
use Parse::RecDescent;
use Carp;
use Versions;
$VERSION = qw( $Revision: 1.2 $)[1];
my $parser;
# Returns a string which says which modules/versions are missing
sub eval {
my $requires = shift;
my $parsed = $parser->startrule("$requires EOF")
|| return carp "Syntax error in requires";
my $result = eval_tree($parsed);
return $result ? '' : "$result";
}
sub eval_tree {
my $tree = shift;
my $op = $tree->{op};
my $operands = $tree->{operands};
if ($op eq 'and') {
my $return = eval_tree(shift @{$tree->{operands}});
foreach (@{$tree->{operands}}) {
$return &&= eval_tree($_);
}
return $return;
} elsif ($op eq 'or') {
my $return = eval_tree(shift @{$tree->{operands}});
foreach (@{$tree->{operands}}) {
$return ||= eval_tree($_);
}
return $return;
} elsif ($op eq 'exists') {
return Prereq::Expr::pkg->new(Versions->from_module(
$tree->{operands} ), $tree->{operands});
} elsif ($op eq '==' || $op eq '>=' || $op eq '<=' || $op eq '>' ||
$op eq '<') {
my $pkg = $tree->{operands}->[0];
my $ver = $tree->{operands}->[1];
return Prereq::Expr::pkg->new(
eval "Versions->from_module( '$pkg' ) $op $ver",
"$pkg$op$ver");
} else {
warn "Unknown operator: $op";
return;
}
}
{
open GRAMMAR_FILE, "Grammar.pm" or die;
local $/;
my $grammar = <GRAMMAR_FILE>;
Parse::RecDescent->Precompile( $grammar, 'PrereqParser' )
|| die "Unable to create parser from grammar";
$parser = new Parse::RecDescent( $grammar )
|| die "Unable to create parser from grammar";;
}
package Prereq::Expr::pkg;
use overload (
'""' => sub { $_[0]->{name} },
'bool' => sub { $_[0]->{truth} }
);
sub new {
my $type = shift;
my $self = bless {}, $type;
$self->{truth} = shift;
$self->{name} = shift;
return $self;
}
1;