slightly less useless error on lack of variable
[scpubgit/DKit.git] / lib / DX / State.pm
1 package DX::State;
2
3 use Return::MultiLevel qw(with_return);
4 use DX::Op::Backtrack;
5 use Scalar::Util qw(blessed);
6 use List::MoreUtils qw(uniq);
7 use Safe::Isa;
8 use Moo;
9
10 has next_op => (is => 'ro', required => 1);
11
12 has return_stack => (is => 'ro', required => 1);
13
14 has by_id => (is => 'ro', required => 1);
15
16 has scope => (is => 'ro', required => 1);
17
18 has last_choice => (is => 'ro', required => 1);
19
20 has id_gen => (is => 'ro', default => sub { {} });
21
22 has rule_set => (is => 'ro');
23
24 has facts => (is => 'ro');
25
26 has dependencies => (is => 'ro', default => sub { {} });
27
28 has actions => (is => 'ro', default => sub { {} });
29
30 sub scope_var {
31   my ($self, $name) = @_;
32   my $id = $self->scope->{$name}
33     or die "no such variable in scope: $name";
34   $self->by_id->{$id};
35 }
36
37 sub resolve_value {
38   my ($self, $var) = @_;
39 die("FUCK") unless $var;
40   die "Can't resolve unbound ${\$var->id}" unless $var->is_bound;
41   my $val = $var->bound_value;
42   if ($val->$_does('DX::Role::Ref')) {
43     return $val->resolve($self);
44   }
45   return $val;
46 }
47
48 sub allocate_var {
49   my ($self, $name, $var, $id_gen) = @_;
50   my $id = join('_', $name, ++($id_gen->{$name}||='000'));
51   DX::Var->new(id => $id, %$var);
52 }
53
54 sub expand_vars {
55   my ($self, %vars) = @_;
56   my %by_id = %{$self->by_id};
57   my %id_gen = %{$self->id_gen};
58   foreach my $name (keys %vars) {
59     unless (blessed($vars{$name})) {
60       my $var = $vars{$name} = $self->allocate_var(
61         $name, $vars{$name}, \%id_gen
62       );
63       $by_id{$var->id} = $var;
64     }
65   }
66   $self->but(
67     by_id => \%by_id, id_gen => \%id_gen,
68   ), %vars;
69 }
70
71 sub record_action {
72   my ($self, $action) = @_;
73   my %id_gen = %{$self->id_gen};
74   my ($type) = (ref($action) =~ /([^:]+)$/);
75   my $id = join('_', $type, ++($id_gen{$type}||='000'));
76   my $recorded = $action->but(id => $id);
77   $self->but(
78     id_gen => \%id_gen,
79     actions => { %{$self->actions}, $id => $recorded }
80   ), $id;
81 }
82
83 sub assign_vars {
84   my ($self, %vars) = @_;
85   my ($state, %expanded) = $self->expand_vars(%vars);
86   $state->but(
87     scope => { %{$self->scope}, map +($_ => $expanded{$_}->id), keys %expanded }
88   );
89 }
90
91 sub bind_value {
92   my ($self, $var_id, $value) = @_;
93   my $bound = $self->by_id->{$var_id}->with_value($value);
94   $self->but(
95     by_id => { %{$self->by_id}, $var_id => $bound },
96   );
97 }
98
99 sub bind_stream {
100   my ($self, $var, $stream) = @_;
101   my $bound = $var->with_stream($stream);
102   $self->but(
103     by_id => { %{$self->by_id}, $var->id => $bound },
104   )->mark_choice($bound);
105 }
106
107 sub bind_root_set {
108   my ($self, $var_id, $set) = @_;
109   my $bound = $self->by_id->{$var_id}->with_root_set($set);
110   $self->but(
111     by_id => { %{$self->by_id}, $var_id => $bound },
112   )->mark_choice($bound);
113 }
114
115 sub mark_choice {
116   my ($self, $var) = @_;
117   $self->but(last_choice => [ $self, $var ]);
118 }
119
120 sub backtrack {
121   my ($self) = @_;
122   my ($state, $var) = ($self);
123   while (($state, $var) = @{$state->last_choice}) {
124     $var->bound_value; $var->clear_bound_value;
125     return $state->mark_choice($var) unless $var->bound_stream->is_exhausted;
126   }
127   $self->return_from_run(undef);
128 }
129
130 sub then {
131   my ($self, $then) = @_;
132   $self->but(next_op => $then);
133 }
134
135 sub return_from_run {
136   my (undef, $return) = @_;
137   (our $Run_Return)->($return);
138 }
139
140 sub return_from_op {
141   my (undef, $return) = @_;
142   if (our $Op_Return) {
143     $Op_Return->($return);
144   }
145 }
146
147 sub run {
148   my ($state) = @_;
149   with_return {
150     my ($return) = @_;
151     local our $Run_Return = $return;
152     while (my $op = $state->next_op) {
153       my $backtrack = with_return {
154         my ($return) = @_;
155         local our $Op_Return = $return;
156         $state = $op->run($state);
157         return;
158       };
159       if ($backtrack) { $state = $state->backtrack }
160     }
161     return $state;
162   }
163 }
164
165 sub push_backtrack {
166   $_[0]->then(DX::Op::Backtrack->new);
167 }
168
169 sub but {
170   my ($self, @but) = @_;
171   $self->new(%$self, @but);
172 }
173
174 sub pop_return_stack {
175   my ($self) = @_;
176   my @stack = @{$self->return_stack};
177   my $top = pop @stack;
178   $self->but(return_stack => \@stack, next_op => $top->[0]);
179 }
180
181 sub push_return_then {
182   my ($self, $return, $then) = @_;
183   $self->but(
184     return_stack => [ @{$self->return_stack}, [ $return, $self ] ],
185     next_op => $then
186   );
187 }
188
189 sub add_dependencies {
190   my ($self, @pairs) = @_;
191   my %deps = %{$self->dependencies};
192   while (my ($from, $to) = splice(@pairs, 0, 2)) {
193     unless ($deps{$from}{$to}) {
194       $deps{$from} = { %{$deps{$from}||{}}, $to => 1 };
195     }
196   }
197   $self->but(dependencies => \%deps);
198 }
199
200 sub action_dependencies {
201   my ($self, @ids) = @_;
202   my @found;
203   my $deps = $self->dependencies;
204   my $by_id = $self->by_id;
205   my %seen;
206   my @queue = @ids;
207   while (my $id = shift @queue) {
208     $seen{$id}++;
209     my $value = $self->resolve_value($by_id->{$id});
210     push @found, $value if $value->$_does('DX::Role::Fact')
211                            and $value->has_required_action;
212     push @queue, grep !$seen{$_}, keys %{$deps->{$id}};
213   }
214   return uniq map $_->required_action, @found;
215 }
216
217 sub copy_vars {
218   my ($self) = @_;
219   my $by_id = $self->by_id;
220   $self->but(by_id => {
221     map +($_ => $by_id->{$_}->copy), keys %$by_id
222   });
223 }
224
225 1;