--- /dev/null
+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;
--- /dev/null
+package DX::Op::Backtrack;
+
+use Moo;
+
+with 'DX::Role::Op';
+
+sub run { $_[1]->backtrack };
+
+1;
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;
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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;
return;
}
return +{
- map +($_ => $state->scope_var($_)->bound_value), keys %{$state->scope}
+ map +($_ => $state->scope_var($_)->copy), keys %{$state->scope}
};
}
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 { {} });
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);
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,
);
}
-sub _expand_constrain {
+sub _expand_op_constrain {
my ($self, $vars, $constraint) = @_;
DX::Op::ApplyConstraint->new(
vars => $vars,
);
}
-sub _expand_observe {
+sub _expand_op_observe {
my ($self, $vars, $builder) = @_;
DX::Op::Observe->new(
vars => $vars,
);
}
+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;
return $self;
}
+sub remove_value {
+ my ($self, $value) = @_;
+ delete $self->values->{$value->${\$self->over}};
+ return $self;
+}
+
1;
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(
package DX::State;
use Return::MultiLevel qw(with_return);
-use DX::Op::FromCode;
+use DX::Op::Backtrack;
use Scalar::Util qw(blessed);
use Moo;
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 {
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;
$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;
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');
)
);
+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' },
);
is_deeply(
- [ $callstream->results ],
+ [ bound_values $callstream->results ],
[
{ P => 'csh', S => 'jim.example.com' },
{ P => 'csh', S => 'joe.example.com' },
);
is_deeply(
- [ $orstream->results ],
+ [ bound_values $orstream->results ],
[
{
S => "kitty.scsys.co.uk"
);
is_deeply(
- [ $orstream_2->results ],
+ [ bound_values $orstream_2->results ],
[
{
S => "jim.example.com"
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' ] ])
);
is_deeply(
- [ sort map $_->{Srv}{name}, $s->results ],
+ [ sort map $_->{Srv}->bound_value->{name}, $s->results ],
[ qw(joe.example.com kitty.scsys.co.uk) ]
);
use DX::Solver;
use DX::SetOver;
use DX::Observer::FromCode;
+use DX::Action::FromCode;
use Test::Exception;
{
use Moo;
has path => (is => 'ro', required => 1);
- has info => (is => 'ro', required => 1);
+ has info => (is => 'ro');
package My::PathStatusInfo;
),
);
+my %empty = (
+ '.ssh' => My::PathStatus->new(
+ path => '.ssh'
+ )
+);
+
my %path_status;
my $solver = DX::Solver->new(
[ 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 } ] ],
%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;
}
)->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'};
[ 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(
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;
use Test::More;
use aliased 'DX::Op::FromCode';
use aliased 'DX::ArrayStream';
-use DX::ResultStream;
use DX::Var;
use DX::State;
use DX::ObservationRequired;
use strictures 1;
use Test::More;
-use Unknown::Values;
use List::Util qw(reduce);
use aliased 'DX::Op::FromCode';
use DX::Var;