wheeeeeeeeee
Matt S Trout [Thu, 30 Jan 2014 22:35:58 +0000 (22:35 +0000)]
lib/DX/Op/ApplyConstraint.pm [new file with mode: 0644]
lib/DX/Op/CallRule.pm [new file with mode: 0644]
lib/DX/Op/MemberLookup.pm [new file with mode: 0644]
lib/DX/Op/MemberOf.pm [new file with mode: 0644]
lib/DX/Op/Return.pm [new file with mode: 0644]
lib/DX/Op/SetScope.pm [new file with mode: 0644]
lib/DX/RuleSet.pm [new file with mode: 0644]
lib/DX/Solver.pm [new file with mode: 0644]
lib/DX/State.pm
t/basic_rule.t [new file with mode: 0644]

diff --git a/lib/DX/Op/ApplyConstraint.pm b/lib/DX/Op/ApplyConstraint.pm
new file mode 100644 (file)
index 0000000..1dfee28
--- /dev/null
@@ -0,0 +1,19 @@
+package DX::Op::ApplyConstraint;
+
+use Moo;
+
+with 'DX::Role::Op';
+
+has vars => (is => 'ro', required => 1);
+has constraint => (is => 'ro', required => 1);
+
+sub run {
+  my ($self, $state) = @_;
+  my @vars = map $state->scope_var($_)->bound_value, @{$self->vars};
+  if ($self->constraint->(@vars)) {
+    return $state->then($self->next);
+  }
+  return $state->backtrack;
+}
+
+1;
diff --git a/lib/DX/Op/CallRule.pm b/lib/DX/Op/CallRule.pm
new file mode 100644 (file)
index 0000000..f97d187
--- /dev/null
@@ -0,0 +1,51 @@
+package DX::Op::CallRule;
+
+use DX::Op::SetScope;
+use DX::Op::FromCode;
+use DX::Var;
+use DX::ArrayStream;
+use Moo;
+
+with 'DX::Role::Op';
+
+has rule_name => (is => 'ro', required => 1);
+has rule_args => (is => 'ro', required => 1);
+has full_name => (is => 'lazy', builder => sub {
+  my ($self) = @_;
+  join('/', $self->rule_name, scalar @{$self->rule_args});
+});
+
+sub run {
+  my ($self, $state) = @_;
+  my @args = map {
+    if (!ref($_)) {
+      $state->scope_var($_)
+    } elsif (ref($_) eq 'ARRAY') {
+      if ($_->[0] eq 'value') {
+        +{ bound_value => $_->[1] };
+      } else {
+        die "Arrayref in argspec is not value";
+      }
+    } else {
+      die "Argspec incomprehensible";
+    }
+  } @{$self->rule_args};
+  my @rules = @{$state->rule_set->rules->{$self->full_name}||[]};
+  die "No rules for ${\$self->full_name}" unless @rules;
+  my $var = DX::Var->new(id => 'OR')
+                   ->with_stream(DX::ArrayStream->from_array(@rules));
+  my $invoke = DX::Op::FromCode->new(
+    code => sub {
+      my ($self, $state) = @_;
+      my ($names, $body) = @{$var->bound_value};
+      my %new; @new{@$names} = @args;
+      $state->but(scope => {})->assign_vars(%new)->then($body);
+    }
+  );
+  my $ret_op = DX::Op::SetScope->new(
+    scope => $state->scope, next => $self->next
+  );
+  $state->push_return_then($ret_op, $invoke)->mark_choice($var);
+}
+
+1;
diff --git a/lib/DX/Op/MemberLookup.pm b/lib/DX/Op/MemberLookup.pm
new file mode 100644 (file)
index 0000000..51827f2
--- /dev/null
@@ -0,0 +1,24 @@
+package DX::Op::MemberLookup;
+
+use Moo;
+
+with 'DX::Role::Op';
+
+has member_var => (is => 'ro', required => 1);
+has coll_name => (is => 'ro', required => 1);
+has key_name => (is => 'ro', required => 1);
+has key_var => (is => 'ro', required => 1);
+
+sub run {
+  my ($self, $state) = @_;
+  my ($member, $key) = map $state->scope_var($self->$_), qw(member_var key_var);
+  die "key unbound" unless $key->is_bound;
+  die "member bound" if $member->is_bound;
+  my $set = $state->facts->{$self->coll_name};
+  if (my $value = $set->{$key->bound_value}) {
+    return $state->bind_var_then($member, $value, $self->next);
+  }
+  return $state->backtrack;
+}
+
+1;
diff --git a/lib/DX/Op/MemberOf.pm b/lib/DX/Op/MemberOf.pm
new file mode 100644 (file)
index 0000000..94eceb2
--- /dev/null
@@ -0,0 +1,20 @@
+package DX::Op::MemberOf;
+
+use DX::ArrayStream;
+use Moo;
+
+with 'DX::Role::Op';
+
+has member_var => (is => 'ro', required => 1);
+has coll_name => (is => 'ro', required => 1);
+
+sub run {
+  my ($self, $state) = @_;
+  my $member = $state->scope_var($self->member_var);
+  die "member bound" if $member->is_bound;
+  my $set = $state->facts->{$self->coll_name};
+  my $stream = DX::ArrayStream->from_array(@{$set}{sort keys %$set});
+  return $state->bind_stream_then($member, $stream, $self->next);
+}
+
+1;
diff --git a/lib/DX/Op/Return.pm b/lib/DX/Op/Return.pm
new file mode 100644 (file)
index 0000000..da187ab
--- /dev/null
@@ -0,0 +1,9 @@
+package DX::Op::Return;
+
+use Moo;
+
+with 'DX::Role::Op';
+
+sub run { $_[1]->pop_return_stack };
+
+1;
diff --git a/lib/DX/Op/SetScope.pm b/lib/DX/Op/SetScope.pm
new file mode 100644 (file)
index 0000000..132a044
--- /dev/null
@@ -0,0 +1,14 @@
+package DX::Op::SetScope;
+
+use Moo;
+
+with 'DX::Role::Op';
+
+has scope => (is => 'ro', required => 1);
+
+sub run {
+  my ($self, $state) = @_;
+  $state->but(scope => $self->scope, next_op => $self->next);
+}
+
+1;
diff --git a/lib/DX/RuleSet.pm b/lib/DX/RuleSet.pm
new file mode 100644 (file)
index 0000000..692ac8e
--- /dev/null
@@ -0,0 +1,64 @@
+package DX::RuleSet;
+
+use Moo;
+use DX::Op::CallRule;
+use DX::Op::MemberOf;
+use DX::Op::MemberLookup;
+use DX::Op::ApplyConstraint;
+use DX::Op::Return;
+use List::Util qw(reduce);
+
+has rules => (is => 'ro', default => sub { {} });
+
+sub add_rule {
+  my ($self, $name, $vars, @body) = @_;
+  my $full_name = join('/', $name, scalar @$vars);
+  push @{$self->rules->{$full_name}}, $self->_make_rule($vars, @body);
+  return $self;
+}
+
+sub _make_rule {
+  my ($self, $vars, @body) = @_;
+  my $head = reduce { $b->but(next => $a) }
+               DX::Op::Return->new,
+               reverse map $self->expand(@$_), @body;
+  [ $vars, $head ];
+}
+
+sub expand {
+  my ($self, $type, @rest) = @_;
+  $self->${\"_expand_${type}"}(@rest);
+}
+
+sub _expand_call {
+  my ($self, $name, @args) = @_;
+  DX::Op::CallRule->new(rule_name => $name, rule_args => \@args);
+}
+
+sub _expand_member_of {
+  my ($self, $member_var, $coll_name) = @_;
+  DX::Op::MemberOf->new(
+    member_var => $member_var,
+    coll_name => $coll_name,
+  );
+}
+
+sub _expand_member_lookup {
+  my ($self, $member_var, $coll_name, $key_name, $key_var) = @_;
+  DX::Op::MemberLookup->new(
+    member_var => $member_var,
+    coll_name => $coll_name,
+    key_name => $key_name,
+    key_var => $key_var
+  );
+}
+
+sub _expand_constrain {
+  my ($self, $vars, $constraint) = @_;
+  DX::Op::ApplyConstraint->new(
+    vars => $vars,
+    constraint => $constraint
+  );
+}
+
+1;
diff --git a/lib/DX/Solver.pm b/lib/DX/Solver.pm
new file mode 100644 (file)
index 0000000..05523ec
--- /dev/null
@@ -0,0 +1,34 @@
+package DX::Solver;
+
+use DX::RuleSet;
+use DX::State;
+use DX::ResultStream;
+use List::Util qw(reduce);
+use Moo;
+
+has rule_set => (is => 'lazy', handles => [ 'add_rule' ], builder => sub {
+  DX::RuleSet->new
+});
+
+has facts => (is => 'ro', required => 1);
+
+sub query {
+  my ($self, $vars, @terms) = @_;
+  my $rule_set = $self->rule_set;
+  my $head = reduce { $b->but(next => $a) }
+               reverse map $rule_set->expand(@$_), @terms;
+  my $state = DX::State->new(
+    next_op => $head,
+    return_stack => [],
+    by_id => {},
+    scope => {},
+    last_choice => [],
+    facts => $self->facts,
+    rule_set => $rule_set,
+  )->assign_vars(map +($_ => {}), @$vars);
+  return DX::ResultStream->new(
+    for_state => $state
+  );
+}
+
+1;
index b4ad233..379b41f 100644 (file)
@@ -17,6 +17,10 @@ has last_choice => (is => 'ro', required => 1);
 
 has id_gen => (is => 'ro', default => sub { {} });
 
+has rule_set => (is => 'ro');
+
+has facts => (is => 'ro');
+
 sub scope_var {
   my ($self, $name) = @_;
   $self->by_id->{$self->scope->{$name}};
@@ -31,14 +35,15 @@ sub allocate_var {
 sub assign_vars {
   my ($self, %vars) = @_;
   my %by_id = %{$self->by_id};
+  my $state = $self->but(id_gen => { %{$self->id_gen} });
   foreach my $name (keys %vars) {
 warn "assign: ${name}";
     unless (blessed($vars{$name})) {
-      my $var = $vars{$name} = $self->allocate_var($name, $vars{$name});
+      my $var = $vars{$name} = $state->allocate_var($name, $vars{$name});
       $by_id{$var->id} = $var;
     }
   }
-  $self->but(
+  $state->but(
     by_id => \%by_id,
     scope => { %{$self->scope}, map +($_ => $vars{$_}->id), keys %vars }
   );
diff --git a/t/basic_rule.t b/t/basic_rule.t
new file mode 100644 (file)
index 0000000..01a5e99
--- /dev/null
@@ -0,0 +1,62 @@
+use strictures 1;
+use Test::More;
+use DX::Solver;
+
+my @servers = qw(
+  kitty.scsys.co.uk
+  jim.example.com
+  joe.example.com
+  pryde.scsys.co.uk
+  bob.example.com
+);
+
+my %servers = map +($_ => { name => $_ }), @servers;
+
+my @shells = qw(csh bash);
+
+my %shells = (
+  bash => { name => 'bash',
+            installed_on => {
+              map +($_ => 1),
+                qw(joe.example.com kitty.scsys.co.uk)
+            },
+          },
+  csh => { name => 'csh',
+           installed_on => {
+             map +($_ => 1),
+               qw(jim.example.com joe.example.com bob.example.com)
+            },
+          },
+);
+
+my $solver = DX::Solver->new(
+  facts => { servers => \%servers, shells => \%shells }
+);
+
+$solver->add_rule(server => [ 'S' ] => [ member_of => qw(S servers) ]);
+
+my $s = $solver->query([ 'S' ], [ call => server => 'S' ]);
+
+is_deeply([ map $_->{S}{name}, $s->results ], [ sort @servers ]);
+
+$solver->add_rule(shell => [ 'S' ] => [ member_of => qw(S shells) ])
+       ->add_rule(name => [ qw(T N) ],
+                    [ constrain => [ qw(T N) ],
+                      sub { ::Dwarn(\@_); $_[0]->{name} eq $_[1] } ]
+                 )
+       ->add_rule(shell_installed_on => [ qw(Shell Srv) ],
+                    [ constrain => [ qw(Shell Srv) ],
+                      sub { $_[0]->{installed_on}{$_[1]->{name}} } ]
+                 );
+
+$s = $solver->query(
+       [ qw(Shell Srv) ],
+       [ call => shell => 'Shell' ],
+       [ call => name => 'Shell', [ value => 'bash' ] ],
+       [ call => server => 'Srv' ],
+       [ call => shell_installed_on => qw(Shell Srv) ],
+     );
+
+::Dwarn($s->results);
+
+done_testing;