Commit | Line | Data |
91d1f239 |
1 | package DX::Tcl; |
2 | |
3 | use strictures 1; |
4 | use curry; |
5 | use DX::Solver; |
6 | use DX::Lib::FS; |
7 | use Term::ReadLine; |
8 | use Devel::Dwarn; |
9 | use Tcl; |
10 | use Safe::Isa; |
11 | use Moo; |
12 | |
13 | has solver => (is => 'lazy', builder => sub { |
14 | my $solver = DX::Solver->new(observation_policy => sub { 1 }); |
15 | DX::Lib::FS->new->load_into($solver); |
16 | $solver; |
17 | }); |
18 | |
19 | has tcl => (is => 'lazy', builder => sub { |
20 | my ($self) = @_; |
21 | my $tcl = Tcl->new; |
22 | $tcl->CreateCommand('DX::_call' => $self->curry::weak::handle_tcl_call); |
23 | our $Tcl_Data ||= do { local $/; my $data = <DATA>; close(DATA); $data }; |
24 | $tcl->Eval($Tcl_Data); |
25 | $tcl->call(namespace => import => 'DX::*'); |
26 | return $tcl; |
27 | }); |
28 | |
29 | sub handle_tcl_call { |
30 | my ($self, undef, undef, undef, $call, @args) = @_; |
31 | return $self->${\"_tcl_${call}"}(@args); |
32 | #Dwarn(\@stuff); |
33 | return 1; |
34 | } |
35 | |
36 | sub _tcl_rule_names { |
37 | my ($self) = @_; |
38 | return [ map /(.*)\/(\d+)/, keys %{$self->solver->rule_set->rules} ]; |
39 | } |
40 | |
41 | sub _tcl_add_rule { |
42 | my ($self, $name, $args, $raw_body) = @_; |
43 | my @args = $self->tcl->SplitList($args); |
44 | #warn $raw_body; |
45 | my $body = $self->_expand_tcl_data($raw_body); |
46 | #Dwarn([ $name, \@args, @$body ]); |
47 | $self->solver->add_rule($name => \@args => @$body); |
48 | return; |
49 | } |
50 | |
51 | sub _tcl_first_result { |
52 | my ($self, $type, $raw_query) = @_; |
53 | my $query = $self->_expand_tcl_data($raw_query); |
54 | my $rs = $self->solver->$type(@$query); |
55 | my $result = $rs->next; |
56 | return $result ? $self->_implode_result($result) : 'false'; |
57 | } |
58 | |
59 | sub _tcl_each_result { |
60 | my ($self, $type, $raw_query) = @_; |
61 | my $query = $self->_expand_tcl_data($raw_query); |
62 | my $rs = $self->solver->$type(@$query); |
63 | return [ map $self->_implode_result($_), $rs->results ]; |
64 | } |
65 | |
66 | sub _tcl_ensure_result { |
67 | my ($self, $raw_query) = @_; |
68 | my $query = $self->_expand_tcl_data($raw_query); |
69 | my $result = $self->solver->ensure(@$query); |
70 | return $result ? $self->_implode_result($result) : 'false'; |
71 | } |
72 | |
73 | sub _implode_result { |
74 | my ($self, $result) = @_; |
75 | my $imploded = $self->_implode_hashref($result->all_values); |
76 | if (my @act = $result->actions) { |
77 | my @flat_act = map +($_->[0], $self->_implode_hashref($_->[1])), |
78 | map [ %{$_->as_structure} ], @act; |
79 | unshift @$imploded, +(_actions => \@flat_act); |
80 | } |
81 | return $imploded; |
82 | } |
83 | |
84 | sub _implode_hashref { |
85 | my ($self, $values) = @_; |
86 | my @flat; |
87 | my %meta; |
88 | foreach my $key (sort keys %$values) { |
89 | my $v = $values->{$key}; |
90 | if ($v->$_does('DX::Role::Set')) { |
91 | $meta{$key}{is_list} = 1; |
92 | push @flat, ($key => [ map "$_", $v->all ]); |
93 | } elsif (ref($v) and ref($v) eq 'ARRAY') { |
94 | $meta{$key}{is_list} = 1; |
95 | push @flat, ($key => [ map "$_", @$v ]); |
96 | } else { |
97 | push @flat, ($key => "$v"); |
98 | } |
99 | } |
100 | my @flat_meta = map +($_ => [ %{$meta{$_}} ]), sort keys %meta; |
101 | return [ (@flat_meta ? (_meta => \@flat_meta) : ()), @flat ]; |
102 | } |
103 | |
104 | sub _expand_tcl_data { |
105 | my ($self, $data) = @_; |
106 | my ($type, @rest) = $self->tcl->SplitList($data); |
107 | die "No type" unless $type; |
108 | if ($type eq 'seq') { |
109 | return [ map $self->_expand_tcl_data($_), @rest ]; |
110 | } elsif ($type eq 'call') { |
111 | my ($name, @args) = @rest; |
112 | return [ $name => map $self->_expand_tcl_data($_), @args ]; |
113 | } elsif ($type eq 'var') { |
114 | return $rest[0]; |
115 | } elsif ($type eq 'value') { |
116 | return \$rest[0] |
117 | } |
118 | die "Unknown type ${type}"; |
119 | } |
120 | |
121 | sub run { shift->repl } |
122 | |
123 | sub repl { |
124 | my ($self) = @_; |
125 | my $rl = Term::ReadLine->new; |
126 | my $tcl = $self->tcl; |
127 | my $cmd; |
128 | |
129 | while (defined(my $line = $rl->readline(length($cmd)?'> ':'$ '))) { |
130 | $cmd .= "${line}\n"; |
131 | my $out; |
132 | if ($tcl->call(info => complete => $cmd)) { |
133 | if (eval { $out = $tcl->Eval($cmd); 1 }) { |
134 | print $out; |
135 | if (length($out) and $out !~ /\n$/) { print "\n" } |
136 | } else { |
137 | $tcl->Eval(q{puts $::errorInfo}); |
138 | } |
139 | $cmd = ''; |
140 | } |
141 | } |
142 | } |
143 | |
144 | 1; |
145 | |
146 | __DATA__ |
147 | namespace eval DX { |
148 | |
149 | variable current_body {} |
150 | |
151 | proc rule {name args body} { |
152 | setup_eval_scope |
153 | _call add_rule $name $args [expand_body $body] |
154 | } |
155 | |
156 | proc query {body {out ""}} { |
157 | setup_eval_scope |
158 | set res [_call first_result query [expand_body $body]] |
159 | return [format_response $res $out] |
160 | } |
161 | |
162 | proc query* {body {out ""}} { |
163 | setup_eval_scope |
164 | set each_res [_call each_result query [expand_body $body]] |
165 | if [string length $out] { |
166 | set join_by "" |
167 | } else { |
168 | set join_by "--\n" |
169 | } |
170 | return [join [lmap res $each_res {format_response $res $out}] $join_by] |
171 | } |
172 | |
173 | proc solve {body {out ""}} { |
174 | setup_eval_scope |
175 | set res [_call first_result solve [expand_body $body]] |
176 | return [format_response $res $out] |
177 | } |
178 | |
179 | proc ensure {body {out ""}} { |
180 | setup_eval_scope |
181 | set res [_call ensure_result [expand_body $body]] |
182 | return [format_response $res $out] |
183 | } |
184 | |
185 | proc format_response {res out {i ""}} { |
186 | if {[llength $res] < 2} { |
187 | return "${i}$res\n"; |
188 | } |
189 | if [string length $out] { |
190 | return [dict with res $out] |
191 | } |
192 | set flat "" |
193 | if [dict exists $res _actions] { |
194 | foreach {n v} [dict get $res _actions] { |
195 | append flat "$n -> \n"; |
196 | append flat [format_response $v "" " "] |
197 | } |
198 | append flat "->\n"; |
199 | } |
200 | dict for {k v} $res { |
201 | if ![string match _* $k] { |
202 | if {[dict exists $res _meta $k is_list] |
203 | && [dict get $res _meta $k is_list]} { |
204 | append flat "$i$k:\n" |
205 | foreach l $v { append flat "$i $l\n" } |
206 | } else { |
207 | append flat "$i$k: $v\n" |
208 | } |
209 | } |
210 | } |
211 | return $flat |
212 | } |
213 | |
214 | proc expand_body body { |
215 | variable current_body |
216 | set save_body $current_body |
217 | set current_body {seq} |
218 | namespace eval ::DX::Eval $body |
219 | set final_body $current_body |
220 | set current_body $save_body |
221 | return $final_body |
222 | } |
223 | |
224 | proc body_add {name raw_args} { |
225 | variable current_body |
226 | lappend current_body [concat call $name [::DX::mangle_args $raw_args]] |
227 | } |
228 | |
229 | proc setup_eval_scope {} { |
230 | set rules [ _call rule_names ]; |
231 | foreach {name arity} $rules { |
232 | if ![llength [info procs ::DX::Eval::$name]] { |
233 | namespace inscope ::DX::Eval proc $name args [ |
234 | concat DX::body_add $name {$args} |
235 | ] |
236 | } |
237 | } |
238 | } |
239 | |
240 | proc mangle_arg {arg} { |
241 | if [regexp {^'(.*)'$} $arg -> value] { |
242 | return [list value $value] |
243 | } |
244 | if {[llength $arg] > 1} { |
245 | return [expand_body $arg] |
246 | } |
247 | return [list var $arg] |
248 | } |
249 | |
250 | proc mangle_args {raw} { |
251 | return [lmap x $raw { mangle_arg $x }] |
252 | } |
253 | |
254 | namespace eval ::DX::Eval { |
255 | proc findall args { DX::body_add findall $args } |
256 | proc foreach args { DX::body_add foreach $args } |
257 | proc not args { DX::body_add not $args } |
258 | } |
259 | |
260 | namespace export rule query query* solve ensure |
261 | } |