not and action infrastructure
Matt S Trout [Mon, 3 Feb 2014 09:34:58 +0000 (09:34 +0000)]
17 files changed:
lib/DX/Action/FromCode.pm [new file with mode: 0644]
lib/DX/Op/Backtrack.pm [new file with mode: 0644]
lib/DX/Op/CallRule.pm
lib/DX/Op/Materialize.pm [new file with mode: 0644]
lib/DX/Op/Not.pm [new file with mode: 0644]
lib/DX/Op/ProposeAction.pm [new file with mode: 0644]
lib/DX/ResultStream.pm
lib/DX/RuleSet.pm
lib/DX/SetOver.pm
lib/DX/Solver.pm
lib/DX/State.pm
lib/DX/Var.pm
t/basic.t
t/basic_rule.t
t/dot_ssh.t
t/observe.t
t/ssh_key.t

diff --git a/lib/DX/Action/FromCode.pm b/lib/DX/Action/FromCode.pm
new file mode 100644 (file)
index 0000000..88e8b27
--- /dev/null
@@ -0,0 +1,12 @@
+package DX::Action::FromCode;
+
+use Moo;
+
+has expect => (is => 'ro', required => 1);
+
+has perform => (is => 'ro', required => 1);
+
+sub expected_effect { $_[0]->expect->() }
+sub run { $_[0]->perform->() }
+
+1;
diff --git a/lib/DX/Op/Backtrack.pm b/lib/DX/Op/Backtrack.pm
new file mode 100644 (file)
index 0000000..098959f
--- /dev/null
@@ -0,0 +1,9 @@
+package DX::Op::Backtrack;
+
+use Moo;
+
+with 'DX::Role::Op';
+
+sub run { $_[1]->backtrack };
+
+1;
index e5dbb48..f3d1b4f 100644 (file)
@@ -17,6 +17,7 @@ has full_name => (is => 'lazy', builder => sub {
 
 sub run {
   my ($self, $state) = @_;
+#warn "Call: ".$self->full_name;
   my @args = map $self->_expand_argspec($state, $_), @{$self->rule_args};
   my @rules = @{$state->rule_set->rules->{$self->full_name}||[]};
   die "No rules for ${\$self->full_name}" unless @rules;
diff --git a/lib/DX/Op/Materialize.pm b/lib/DX/Op/Materialize.pm
new file mode 100644 (file)
index 0000000..beb15f8
--- /dev/null
@@ -0,0 +1,15 @@
+package DX::Op::Materialize;
+
+use Moo;
+
+with 'DX::Role::Op';
+
+has var_name => (is => 'ro', required => 1);
+
+sub run {
+  my ($self, $state) = @_;
+  $state->scope_var($self->var_name)->bound_value;
+  $state->then($self->next);
+}
+
+1;
diff --git a/lib/DX/Op/Not.pm b/lib/DX/Op/Not.pm
new file mode 100644 (file)
index 0000000..a70183a
--- /dev/null
@@ -0,0 +1,32 @@
+package DX::Op::Not;
+
+use DX::Op::FromCode;
+use DX::Var;
+use DX::ArrayStream;
+use DX::Op::Return;
+use Moo;
+
+with 'DX::Role::Op';
+
+has body => (is => 'ro', required => 1);
+
+sub run {
+  my ($self, $state) = @_;
+  my $var = DX::Var->new(id => "rule:not")
+                   ->with_stream(DX::ArrayStream->from_array(
+                       $self->body, DX::Op::Return->new
+                     ));
+  my $invoke = DX::Op::FromCode->new(
+    code => sub {
+      my ($self, $state) = @_;
+      my $op = $var->bound_value;
+      $state->then($op);
+    }
+  );
+  my $ret_op = DX::Op::SetScope->new(
+    scope => $state->scope, next => $self->next
+  );
+  $state->push_return_then($self->next, $invoke)->mark_choice($var);
+}
+
+1;
diff --git a/lib/DX/Op/ProposeAction.pm b/lib/DX/Op/ProposeAction.pm
new file mode 100644 (file)
index 0000000..ca9c38d
--- /dev/null
@@ -0,0 +1,28 @@
+package DX::Op::ProposeAction;
+
+use DX::ObservationRequired;
+use Moo;
+
+with 'DX::Role::Op';
+
+has vars => (is => 'ro', required => 1);
+has builder => (is => 'ro', required => 1);
+
+has _arg_map => (is => 'lazy', builder => sub {
+  my ($self) = @_;
+  my $name = 'arg0';
+  +{ map +($name++, $_), @{$self->vars} };
+});
+
+sub run {
+  my ($self, $state) = @_;
+  ($state, my %args) = $self->_expand_args($state, %{$self->_arg_map});
+  my @vars = @args{sort keys %args};
+  my $action = $self->builder->(@vars);
+  my ($id, $value) = $action->expected_effect;
+  my $var = $state->by_id->{$id}->with_value($value)->with_action($action);
+  $state->but(by_id => { %{$state->by_id}, $id => $var })
+        ->then($self->next);
+}
+
+1;
index 17f0bd7..e439ce9 100644 (file)
@@ -39,7 +39,7 @@ sub next {
     return;
   }
   return +{
-    map +($_ => $state->scope_var($_)->bound_value), keys %{$state->scope}
+    map +($_ => $state->scope_var($_)->copy), keys %{$state->scope}
   };
 }
 
index 13b6696..8840fb0 100644 (file)
@@ -6,7 +6,11 @@ use DX::Op::MemberOf;
 use DX::Op::ApplyConstraint;
 use DX::Op::Return;
 use DX::Op::Cut;
+use DX::Op::Backtrack;
 use DX::Op::Observe;
+use DX::Op::Not;
+use DX::Op::ProposeAction;
+use DX::Op::Materialize;
 use List::Util qw(reduce);
 
 has rules => (is => 'ro', default => sub { {} });
@@ -20,15 +24,20 @@ sub add_rule {
 
 sub _make_rule {
   my ($self, $vars, @body) = @_;
-  my $head = reduce { $b->but(next => $a) }
-               DX::Op::Return->new,
-               reverse map $self->expand(@$_), @body;
+  my $head = $self->_expand_and_link(DX::Op::Return->new, @body);
   [ $vars, $head ];
 }
 
+sub _expand_and_link {
+  my ($self, $last, @body) = @_;
+  return reduce { $b->but(next => $a) }
+           $last,
+           reverse map $self->expand(@$_), @body;
+}
+
 sub expand {
   my ($self, $type, @rest) = @_;
-  if ($self->can(my $expand_meth = "_expand_${type}")) {
+  if ($self->can(my $expand_meth = "_expand_op_${type}")) {
     return $self->$expand_meth(@rest);
   }
   return $self->_expand_call($type, @rest);
@@ -39,9 +48,19 @@ sub _expand_call {
   DX::Op::CallRule->new(rule_name => $name, rule_args => \@args);
 }
 
-sub _expand_cut { return DX::Op::Cut->new }
+sub _expand_op_cut { return DX::Op::Cut->new }
 
-sub _expand_member_of {
+sub _expand_op_fail { return DX::Op::Backtrack->new }
+
+sub _expand_op_not {
+  my ($self, @contents) = @_;
+  my $cut = DX::Op::Cut->new(next => DX::Op::Backtrack->new);
+  DX::Op::Not->new(
+    body => $self->_expand_and_link($cut, @contents)
+  );
+}
+
+sub _expand_op_member_of {
   my ($self, $member_var, $coll_var) = @_;
   DX::Op::MemberOf->new(
     member_var => $member_var,
@@ -49,7 +68,7 @@ sub _expand_member_of {
   );
 }
 
-sub _expand_constrain {
+sub _expand_op_constrain {
   my ($self, $vars, $constraint) = @_;
   DX::Op::ApplyConstraint->new(
     vars => $vars,
@@ -57,7 +76,7 @@ sub _expand_constrain {
   );
 }
 
-sub _expand_observe {
+sub _expand_op_observe {
   my ($self, $vars, $builder) = @_;
   DX::Op::Observe->new(
     vars => $vars,
@@ -65,4 +84,17 @@ sub _expand_observe {
   );
 }
 
+sub _expand_op_act {
+  my ($self, $vars, $builder) = @_;
+  DX::Op::ProposeAction->new(
+    vars => $vars,
+    builder => $builder,
+  );
+}
+
+sub _expand_op_materialize {
+  my ($self, $var_name) = @_;
+  DX::Op::Materialize->new(var_name => $var_name);
+}
+
 1;
index 74090bd..45ca5ba 100644 (file)
@@ -25,4 +25,10 @@ sub set_value {
   return $self;
 }
 
+sub remove_value {
+  my ($self, $value) = @_;
+  delete $self->values->{$value->${\$self->over}};
+  return $self;
+}
+
 1;
index c425b27..51fcdbe 100644 (file)
@@ -17,6 +17,7 @@ has observation_policy => (is => 'ro');
 sub query {
   my ($self, $vars, @terms) = @_;
   my $rule_set = $self->rule_set;
+  push @terms, map +[ materialize => $_ ], @$vars;
   my $head = reduce { $b->but(next => $a) }
                reverse map $rule_set->expand(@$_), @terms;
   my $state = DX::State->new(
index 83e8cc3..7ef08c7 100644 (file)
@@ -1,7 +1,7 @@
 package DX::State;
 
 use Return::MultiLevel qw(with_return);
-use DX::Op::FromCode;
+use DX::Op::Backtrack;
 use Scalar::Util qw(blessed);
 use Moo;
 
@@ -97,23 +97,36 @@ sub then {
 
 sub return_from_run {
   my (undef, $return) = @_;
-  (our $Nonlocal_Return)->($return);
+  (our $Run_Return)->($return);
+}
+
+sub return_from_op {
+  my (undef, $return) = @_;
+  if (our $Op_Return) {
+    $Op_Return->($return);
+  }
 }
 
 sub run {
   my ($state) = @_;
   with_return {
     my ($return) = @_;
-    local our $Nonlocal_Return = $return;
+    local our $Run_Return = $return;
     while (my $op = $state->next_op) {
-      $state = $op->run($state);
+      my $backtrack = with_return {
+        my ($return) = @_;
+        local our $Op_Return = $return;
+        $state = $op->run($state);
+        return;
+      };
+      if ($backtrack) { $state = $state->backtrack }
     }
     return $state;
   }
 }
 
 sub push_backtrack {
-  $_[0]->then(DX::Op::FromCode->new(code => sub { $_[1]->backtrack }));
+  $_[0]->then(DX::Op::Backtrack->new);
 }
 
 sub but {
index eafa895..d17f375 100644 (file)
@@ -7,9 +7,15 @@ has id => (is => 'ro', required => 1);
 has bound_stream => (is => 'ro');
 
 has bound_value => (is => 'lazy', predicate => 1, clearer => 1, builder => sub {
-  $_[0]->bound_stream->next;
+  if (defined(my $next = $_[0]->bound_stream->next)) {
+    return $next;
+  }
+  DX::State->return_from_op('backtrack');
+  return;
 });
 
+has action => (is => 'ro');
+
 sub is_bound {
   my ($self) = @_;
   $self->has_bound_value || $self->bound_stream;
@@ -25,4 +31,14 @@ sub with_value {
   $self->new(%$self, bound_value => $stream);
 }
 
+sub with_action {
+  my ($self, $action) = @_;
+  $self->new(%$self, action => $action);
+}
+
+sub copy {
+  my ($self) = @_;
+  ref($self)->new(%$self);
+}
+
 1;
index be3e32d..10e5d62 100644 (file)
--- a/t/basic.t
+++ b/t/basic.t
@@ -78,7 +78,7 @@ sub make_state {
 
 my $stream = DX::ResultStream->new(for_state => make_state([ 'S' ], $op));
 
-is($stream->next->{'S'}, $_)
+is($stream->next->{'S'}->bound_value, $_)
   for qw(jim.example.com joe.example.com bob.example.com);
 
 is($stream->next, undef, 'No more');
@@ -96,12 +96,21 @@ my $complex_op = FromCode->new(
   )
 );
 
+sub bound_values {
+  map {
+    my $v = $_;
+    +{
+       map +($_ => $v->{$_}->bound_value), keys %$v
+    }
+  } @_
+}
+
 my $cstream = DX::ResultStream->new(
   for_state => make_state([ qw(S P) ], $complex_op)
 );
 
 is_deeply(
-  [ $cstream->results ],
+  [ bound_values $cstream->results ],
   [
     { P => 'csh', S => 'jim.example.com' },
     { P => 'csh', S => 'joe.example.com' },
@@ -141,7 +150,7 @@ my $callstream = DX::ResultStream->new(
 );
 
 is_deeply(
-  [ $callstream->results ],
+  [ bound_values $callstream->results ],
   [
     { P => 'csh', S => 'jim.example.com' },
     { P => 'csh', S => 'joe.example.com' },
@@ -182,7 +191,7 @@ my $orstream = DX::ResultStream->new(
 );
 
 is_deeply(
-  [ $orstream->results ],
+  [ bound_values $orstream->results ],
   [
     {
       S => "kitty.scsys.co.uk"
@@ -218,7 +227,7 @@ my $orstream_2 = DX::ResultStream->new(
 );
 
 is_deeply(
-  [ $orstream_2->results ],
+  [ bound_values $orstream_2->results ],
   [
     {
       S => "jim.example.com"
index 173b47b..72b9621 100644 (file)
@@ -45,9 +45,9 @@ $solver->add_rule(
   server => [ 'S' ] => [ member_of => S => [ value => 'servers' ] ]
 );
 
-my $s = $solver->query([ 'S' ], [ call => server => 'S' ]);
+my $s = $solver->query([ 'S' ], [ server => 'S' ]);
 
-is_deeply([ map $_->{S}{name}, $s->results ], [ sort @servers ]);
+is_deeply([ map $_->{S}->bound_value->{name}, $s->results ], [ sort @servers ]);
 
 $solver->add_rule(
   shell => [ 'S' ] => [ member_of => S => [ value => 'shells' ] ])
@@ -69,7 +69,7 @@ $s = $solver->query(
      );
 
 is_deeply(
-  [ sort map $_->{Srv}{name}, $s->results ],
+  [ sort map $_->{Srv}->bound_value->{name}, $s->results ],
   [ qw(joe.example.com kitty.scsys.co.uk) ]
 );
 
index 7a3491d..6e4b017 100644 (file)
@@ -3,6 +3,7 @@ use Test::More;
 use DX::Solver;
 use DX::SetOver;
 use DX::Observer::FromCode;
+use DX::Action::FromCode;
 use Test::Exception;
 
 {
@@ -11,7 +12,7 @@ use Test::Exception;
   use Moo;
 
   has path => (is => 'ro', required => 1);
-  has info => (is => 'ro', required => 1);
+  has info => (is => 'ro');
 
   package My::PathStatusInfo;
 
@@ -35,6 +36,12 @@ my %protos = (
   ),
 );
 
+my %empty = (
+  '.ssh' => My::PathStatus->new(
+    path => '.ssh'
+  )
+);
+
 my %path_status;
 
 my $solver = DX::Solver->new(
@@ -51,7 +58,12 @@ $solver->add_rule(@$_) for (
     [ constrain => [ qw(PS P) ], sub { $_[0]->path eq $_[1] } ] ],
   [ mode => [ qw(PS M) ],
     [ constrain => [ qw(PS M) ],
-       sub { $_[0]->info and $_[0]->info->mode eq $_[1] } ] ],
+        sub { $_[0]->info and $_[0]->info->mode eq $_[1] } ] ],
+  [ exists_path => [ qw(PS) ],
+    [ constrain => [ qw(PS) ],
+        sub {
+          $_[0]->info and ($_[0]->info->is_directory or $_[0]->info->is_file)
+        } ] ],
   [ is_directory => [ qw(PS) ],
     [ constrain => [ qw(PS) ],
         sub { $_[0]->info and $_[0]->info->is_directory } ] ],
@@ -63,7 +75,7 @@ $solver->add_rule(@$_) for (
 %path_status = %protos;
 
 sub paths_for_simple {
-  join ' ', map $_->{PS}->path, $solver->query(
+  join ' ', map $_->{PS}->bound_value->path, $solver->query(
     [ qw(PS) ], [ path_status => 'PS' ], @_
   )->results;
 }
@@ -115,7 +127,7 @@ lives_ok {
   )->results
 };
 
-is(join(' ', map $_->{PS}->path, @res), '.ssh');
+is(join(' ', map $_->{PS}->bound_value->path, @res), '.ssh');
 
 delete $solver->rule_set->rules->{'path_status_at/2'};
 
@@ -142,12 +154,12 @@ $solver->add_rule(
     [ path => qw(PS P) ],
 );
 
-%path_status = ('.ssh/authorized_keys' => $protos{'.ssh/authorized_keys'});
+%path_status = ();
 
 $ob_res{'.ssh'} = $protos{'.ssh'};
 
 sub paths_for {
-  join ' ', map $_->{PS}->path, $solver->query([ 'PS' ], @_)->results;
+  join ' ', map $_->{PS}->bound_value->path, $solver->query([ 'PS' ], @_)->results;
 }
 
 is(
@@ -174,4 +186,85 @@ delete $solver->{observation_policy};
 lives_ok { paths_for([ path_status_at => 'PS', [ value => '.ssh' ] ]) }
   'No observation required anymore';
 
+$path_status{'.ssh/authorized_keys'} = $protos{'.ssh/authorized_keys'};
+
+is(
+  paths_for([ path_status => 'PS' ], [ not => [ is_directory => 'PS' ] ]),
+  '.ssh/authorized_keys',
+  'Negation'
+);
+
+$solver->add_rule(@$_) for (
+  [ directory_at => [ qw(PS P) ],
+    [ path_status_at => qw(PS P) ],
+    [ is_directory => 'PS' ] ],
+);
+
+%path_status = ();
+
+$ob_res{'.ssh'} = $empty{'.ssh'};
+
+#%path_status = %protos;
+
+$solver->{observation_policy} = sub { 1 };
+
+sub dot_ssh_query {
+  $solver->query([ 'PS' ], [ directory_at => 'PS' => [ value => '.ssh' ] ]);
+}
+
+is_deeply(
+  [ dot_ssh_query()->results ],
+  []
+);
+
+#::Dwarn(paths_for([ directory_at => 'PS', [ value => '.ssh' ] ]));
+
+$solver->add_rule(@$_) for (
+  [ is_directory => [ qw(PS) ],
+    [ not => [ exists_path => 'PS' ] ],
+    [ act => [ 'PS' ],
+        sub {
+          my ($ps_var) = @_;
+          my ($id, $value) = ($ps_var->id, $ps_var->bound_value);
+          DX::Action::FromCode->new(
+            expect => sub {
+              ($id => My::PathStatus->new(
+                path => $value->path,
+                info => My::PathStatusInfo->new(
+                  is_directory => 1, mode => ''
+                )
+              ))
+            },
+            perform => sub {
+              $ob_res{$value->path} = $protos{$value->path};
+              (path_status => $value);
+            }
+          )
+        } ] ]
+);
+
+%path_status = ();
+
+@res = dot_ssh_query()->results;
+
+is(scalar(@res),1,'Single result');
+
+is($path_status{'.ssh'}, $empty{'.ssh'}, 'Empty observed');
+
+ok(my $action = $res[0]->{PS}->action);
+
+my ($type, $value) = $action->run;
+
+$solver->facts->{$type}->remove_value($value);
+
+ok(!$path_status{'.ssh'}, 'Empty retracted');
+
+@res = dot_ssh_query()->results;
+
+is(scalar(@res),1,'Single result');
+
+is($path_status{'.ssh'}, $protos{'.ssh'}, 'Created observed');
+
+ok(!$res[0]->{PS}->action, 'No action');
+
 done_testing;
index 7896be3..70b70c1 100644 (file)
@@ -2,7 +2,6 @@ use strictures 1;
 use Test::More;
 use aliased 'DX::Op::FromCode';
 use aliased 'DX::ArrayStream';
-use DX::ResultStream;
 use DX::Var;
 use DX::State;
 use DX::ObservationRequired;
index b7ed6ed..ef61682 100644 (file)
@@ -1,6 +1,5 @@
 use strictures 1;
 use Test::More;
-use Unknown::Values;
 use List::Util qw(reduce);
 use aliased 'DX::Op::FromCode';
 use DX::Var;