33
34:- use_module(library(scasp)). 35:- use_module(library(scasp/html)). 36:- use_module(library(scasp/output)). 37:- use_module(library(scasp/json)). 38
39:- use_module(library(http/http_server)). 40:- use_module(library(http/html_write)). 41:- use_module(library(http/js_write)). 42:- use_module(library(http/html_head)). 43:- use_module(library(http/http_path)). 44:- use_module(library(http/http_error)). 45:- use_module(library(http/jquery)). 46:- use_module(library(http/http_dispatch)). 47:- use_module(library(dcg/high_order)). 48:- use_module(library(http/term_html)). 49:- use_module(library(http/http_json)). 50:- use_module(library(http/http_client)). 51:- use_module(library(http/http_host)). 52
55
56:- if((current_prolog_flag(unix,true),
57 \+ stream_property(current_input, tty(true)))). 58
59:- format(user_error, '% Loading as Unix daemon~n', []). 60:- use_module(library(http/http_unix_daemon)). 61
62:- else. 63
64:- initialization(main,main). 65
66main(Argv) :-
67 argv_options(Argv, _, Options),
68 ( select_option(examples(Ex), Options, Options1)
69 -> attach_examples(Ex)
70 ; Options1 = Options
71 ),
72 server(Options1),
73 ( option(interactive(true), Options1)
74 -> cli_enable_development_system
75 ; thread_get_message(quit) 76 ).
77
78opt_type(port, port, natural).
79opt_type(p, port, natural).
80opt_type(examples, examples, atom).
81opt_type(i, interactive, boolean).
82
83opt_help(port, "Port used by the server").
84opt_help(interactive, "Start interactive toplevel").
85opt_help(examples, "':' separated list of files and directories from \c
86 which to load examples").
87
88opt_meta(port, 'PORT').
89opt_meta(examples, 'DIRECTORY').
90
94
95server(Options) :-
96 ( option(port(_), Options)
97 -> http_server(Options)
98 ; http_server([port(8080)|Options])
99 ).
100:- endif. 101
102:- multifile http:location/3. 103:- dynamic http:location/3. 104
105http:location(scasp, root(scasp), []).
106http:location(js, scasp(js), []).
107http:location(css, scasp(css), []).
108
109:- http_handler(root(.), http_redirect(see_other, scasp(.)), []). 110:- http_handler(scasp(.), home, []). 111:- http_handler(scasp(solve), solve, [id(solve)]). 112:- http_handler(scasp(help), scasp_help, [id(help)]). 113:- http_handler(scasp(example), example_data, [id(example_data)]). 114
115 118
123
124home(_Request) :-
125 http_link_to_id(help, [], Help),
126 reply_html_page([ title('s(CASP) web server')
127 ],
128 [ h1(['s(CASP) web server ', a(href(Help), 'Help')]),
129 \query_page
130 ]).
131
132query_page -->
133 html_requires(jquery),
134 html_requires(scasp),
135 styles,
136 html([ div(class(section),
137 [ div(class(select), \example_data),
138 textarea([id(data), rows(10), cols(80),
139 placeholder('s(CASP) program')], '')
140 ]),
141 h4('Query'),
142 '?- ', input([id(query), size(50),
143 placeholder('Query')]),
144 ' Limit ', input([type(number), min(1), id(limit), value(1),
145 size(3),
146 placeholder('Empty means all answer sets')]),
147 \html_json_radio,
148 h4(''),
149 button(id(solve), 'Solve'),
150 button(id(clear), 'Clear'),
151 div(id('results-html'), []),
152 pre(id('results-json'), [])
153 ]),
154 button_actions.
155
159
160example_data -->
161 { \+ example(_,_,_) },
162 !.
163example_data -->
164 { findall(Id-Comment, example(Id,_File,Comment), Pairs),
165 http_link_to_id(example_data, [], ExDataURL)
166 },
167 html([ select([name(example), id(example)],
168 \sequence(example_option, Pairs))
169 ]),
170 js_script({|javascript(ExDataURL)||
171$(function() {
172$("#example").change(function() {
173 var ex = $(this).val();
174
175 $.get(ExDataURL,
176 {"example": ex},
177 function(reply) {
178 var rows = reply.data.split("\n").length + 1;
179 if ( rows > 20 ) rows = 20;
180 $("#data").val(reply.data)
181 .attr("rows", rows);
182 $("#query").val(reply.query || "");
183 });
184});
185
186});
187 |}).
188
189example_option(Id-Comment) -->
190 html(option(value(Id), Comment)).
191
192example_data(Request) :-
193 http_parameters(Request, [example(Id, [atom])]),
194 example(Id, File, Comment),
195 read_file_to_string(File, Data, []),
196 Reply0 = _{comment:Comment, data:Data},
197 ( re_matchsub('^\\?-\\s*(.*?)\\.($|\\s)'/m, Data, Dict, [])
198 -> Query = Dict.1,
199 Reply = Reply0.put(query, Query)
200 ; Reply = Reply0
201 ),
202 reply_json(Reply).
203
204styles -->
205 html({|html||
206<style>
207div.html-json-switch { margin-top: 10px; margin-left: 2ex; }
208</style>
209 |}).
210
211
212html_json_radio -->
213 html(div(class('html-json-switch'),
214 [ 'Display results as: ',
215 input([type(radio), id(rhtml), name(format), value(html),
216 checked(checked)]),
217 label(for(rhtml), 'HTML'),
218 input([type(radio), id(rjson), name(format), value(json)]),
219 label(for(rjson), 'JSON')
220 ])).
221
222
223button_actions -->
224 { http_link_to_id(solve, [], SolveURL) },
225 js_script({|javascript(SolveURL)||
226$(function() {
227
228$("#solve").on("click", function() {
229 var data = $("#data").val();
230 var query = $("#query").val();
231 var limit = $("#limit").val();
232 var format = $('input[type="radio"][name="format"]:checked').val();
233 $("#results-html").empty();
234 $("#results-json").empty();
235 $.get(SolveURL,
236 { data: data,
237 query: query,
238 limit: limit,
239 format: format
240 },
241 function(reply) {
242
243 if ( typeof(reply) == "string" ) {
244 var results = $("#results-html");
245 results.html(reply);
246 results.sCASP('swish_answer');
247 } else if ( typeof(reply) == "object" ) {
248 var results = $("#results-json");
249 results.text(JSON.stringify(reply, undefined, 2));
250 }
251 })
252 .fail(function(error) {
253 if ( error.responseJSON ) {
254 $("#results").text(error.responseJSON.message);
255 } else {
256 console.log(error);
257 }
258 });
259});
260
261$("#clear").on("click", function() {
262 $("#results").empty();
263});
264
265});
266 |}).
267
268
273
274solve(Request) :-
275 option(method(post), Request),
276 option(content_type(Type), Request),
277 sub_atom(Type, 0, _, _, 'text/x-scasp'),
278 !,
279 http_read_data(Request, Data, [to(string)]),
280 solve(Request, #{ data:Data, format:json }).
281solve(Request) :-
282 option(method(post), Request),
283 option(content_type(Type), Request),
284 sub_atom(Type, 0, _, _, 'application/json'),
285 !,
286 http_read_json(Request, Dict0, [json_object(dict)]),
287 foldl(to_atom, [format], Dict0, Dict1),
288 foldl(dict_default, [format(json)], Dict1, Dict),
289 solve(Request, Dict).
290solve(Request) :-
291 http_parameters(Request,
292 [ data(Data, []),
293 query(QueryS, [optional(true)]),
294 limit(Limit, [optional(true), integer]),
295 format(Format, [default(html)]),
296 tree(Tree, [boolean,optional(true)])
297 ]),
298 solve(Request,
299 #{ data:Data,
300 query:QueryS,
301 limit:Limit,
302 format:Format,
303 tree:Tree}).
304
305to_atom(Key, Dict0, Dict) :-
306 ( Value = Dict0.get(Key)
307 -> atom_string(Atom, Value),
308 Dict = Dict0.put(Key, Atom)
309 ; Dict = Dict0
310 ).
311
312dict_default(Term, Dict0, Dict) :-
313 Term =.. [Name,Value],
314 ( _ = Dict0.get(Name)
315 -> Dict = Dict0
316 ; Dict = Dict0.put(Name, Value)
317 ).
318
322
323solve(_Request, Dict) :-
324 _{ data:Data,
325 query:QueryS,
326 limit:Limit,
327 format:Format,
328 tree:Tree } >:< Dict,
329
330 ignore(Limit = infinite),
331 solve_options(Format, Tree, Options),
332
333 334 Error = error(Formal,_),
335 catch(( setup_call_cleanup(
336 open_string(Data, In),
337 read_terms(In, Terms0),
338 close(In)),
339 query(QueryS, Query, Bindings, Terms0, Terms)
340 ),
341 Error,
342 true),
343
344 345 ( nonvar(Formal)
346 -> reply_html_page([],
347 \error(Error))
348 ; in_temporary_module(
349 M,
350 ( use_module(library(scasp)),
351 maplist(add_to_program(M), Terms)
352 ),
353 ( call_time(findall(Result,
354 scasp_result(M:Query, Bindings, Result, Options),
355 Results),
356 TotalTime),
357 ovar_set_bindings(Bindings),
358 ovar_analyze_term(Query, []),
359 inline_constraints(Query, []),
360 reply(Format, M:Query, TotalTime, Results)
361 ))
362 ).
363
364error(Error) -->
365 { message_to_string(Error, Message) },
366 html(div(class(error), Message)).
367
368solve_options(html, _, Options) =>
369 Options = [tree(true), unknown(fail)].
370solve_options(json, Tree, Options) =>
371 ignore(Tree=true),
372 Options = [tree(Tree), unknown(fail), inline_constraints(false)].
373
377
378read_terms(In, Terms) :-
379 read_one_term(In, Term0, Pos0),
380 read_terms(Term0, Pos0, In, Terms).
381
382read_terms(end_of_file, _, _, []) :-
383 !.
384read_terms(Term0, Pos0, In, [Term0-Pos0|T]) :-
385 read_one_term(In, Term, Pos),
386 read_terms(Term, Pos, In, T).
387
388read_one_term(In, Term, Pos) :-
389 read_term(In, Term,
390 [ module(scasp_dyncall),
391 variable_names(Bindings),
392 term_position(Pos)
393 ]),
394 fixup_pred(Term, Bindings).
395
400
401fixup_pred((#pred Atom :: _Template), Bindings) =>
402 fixup_atom(Atom, Bindings).
403fixup_pred((:-pred Atom :: _Template), Bindings) =>
404 fixup_atom(Atom, Bindings).
405fixup_pred((?- Query), Bindings) =>
406 fixup_atom(Query, Bindings).
407fixup_pred(_, _) =>
408 true.
409
410fixup_atom(Var, Bindings), var(Var) =>
411 ( member(Name = V, Bindings),
412 V == Var
413 -> Var = '$VAR'(Name)
414 ; true
415 ).
416fixup_atom(Term, Bindings), compound(Term) =>
417 compound_name_arguments(Term, _Name, Args),
418 maplist(fixup_atom_r(Bindings), Args).
419fixup_atom(_, _) =>
420 true.
421
422fixup_atom_r(Bindings, Atom) :-
423 fixup_atom(Atom, Bindings).
424
428
429add_to_program(M, (# Directive)-Pos) =>
430 #(M:Directive, Pos).
431add_to_program(M, (:- show Spec)-_) =>
432 show(M:Spec).
433add_to_program(M, (:- pred Spec)-_) =>
434 pred(M:Spec).
435add_to_program(M, (:- abducible Spec)-Pos) =>
436 abducible(M:Spec, Pos).
437add_to_program(M, Term-Pos) =>
438 scasp_assert(M:Term, Pos).
439
441
442query(QueryS, Query, Bindings, Terms, Terms) :-
443 atomic(QueryS), QueryS \== '',
444 !,
445 term_string(Query, QueryS, [variable_names(Bindings)]).
446query(_, Query, Bindings, TermsIn, TermsOut) :-
447 partition(is_query, TermsIn, Queries, TermsOut),
448 last(Queries, (?- Query0)-_),
449 !,
450 varnumbers_names(Query0, Query, Bindings).
451query(_, _, _, _, _) :-
452 existence_error(scasp_query, program).
453
454is_query((?-_)-_) => true.
455is_query(_) => false.
456
461
462scasp_result(Query,
463 Bindings,
464 scasp{ query:Query,
465 answer:Counter,
466 bindings:Bindings,
467 model:Model,
468 tree:Tree,
469 time:Time
470 },
471 Options) :-
472 option(tree(true), Options),
473 !,
474 Query = M:_,
475 call_nth(call_time(scasp(Query,
476 [ model(M:Model),
477 tree(M:Tree)
478 | Options
479 ]),
480 Time), Counter),
481 analyze_variables(t(Bindings, Model, Tree), Bindings, Options).
482scasp_result(Query,
483 Bindings,
484 scasp{ query:Query,
485 answer:Counter,
486 bindings:Bindings,
487 model:Model,
488 time:Time
489 },
490 Options) :-
491 Query = M:_,
492 call_nth(call_time(scasp(Query,
493 [ model(M:Model)
494 | Options
495 ]), Time),
496 Counter),
497 analyze_variables(t(Bindings, Model), Bindings, Options).
498
499analyze_variables(Term, Bindings, Options) :-
500 ovar_set_bindings(Bindings),
501 ( option(inline_constraints(false), Options)
502 -> ovar_analyze_term(Term, [name_constraints(true)])
503 ; ovar_analyze_term(Term, []),
504 inline_constraints(Term, [])
505 ).
506
510
511reply(html, M:_Query, TotalTime, Results) =>
512 reply_html_page([],
513 \results(Results, M, TotalTime)).
514reply(json, Query, TotalTime, Results) =>
515 scasp_results_json(#{query:Query,
516 answers:Results,
517 cpu:TotalTime},
518 JSON),
519 reply_json_dict(JSON, []).
520
521
522 525
526results([], _, Time) -->
527 !,
528 html(h3('No models (~3f sec)'-[Time.cpu])).
529results(Results, M, _Time) -->
530 sequence(result(M), Results).
531
532result(M, Result) -->
533 html(div(class(result),
534 [ h3('Result #~D (~3f sec)'-[Result.answer, Result.time.cpu]),
535 \binding_section(Result.bindings),
536 \html_model(M:Result.model, [class('collapsable-content')]),
537 \justification_section(M:Result)
538 ])).
539
541
542binding_section([]) -->
543 !.
544binding_section(Bindings) -->
545 { copy_term(Bindings, Bindings1, Constraints0),
546 var_names(Constraints0, Constraints1),
547 exclude(no_op_binding, Bindings1, Bindings2)
548 },
549 html(div(class(bindings),
550 [ h4('Bindings'),
551 \bindings(Bindings2, Constraints1)
552 ])).
553
554var_names([], []).
555var_names([H|T0], T) :-
556 var_name(H),
557 !,
558 var_names(T0, T).
559var_names([H|T0], [H|T]) :-
560 var_names(T0, T).
561
562var_name(put_attr(Var, scasp_output, name(Name))) :-
563 Var = '$VAR'(Name).
564var_name(put_attr(Var, scasp_output, singleton)) :-
565 Var = '$VAR'('_').
566
567no_op_binding(Name = '$VAR'(Name)) => true.
568no_op_binding(_) => false.
569
570
574
575bindings([], []) -->
576 !.
577bindings([], Constraints) -->
578 !,
579 constraints(Constraints).
580bindings([H|T], Constraints) -->
581 ( {T == [], Constraints == []}
582 -> binding(H, '')
583 ; binding(H, ',')
584 ),
585 bindings(T, Constraints).
586
587binding(Name=Value, End) -->
588 html(div(class(binding),
589 [ var(Name),
590 ' = ',
591 \term(Value),
592 End
593 ])).
594
595constraints([]) -->
596 [].
597constraints([H|T]) -->
598 ( {T==[]}
599 -> constraint(H, '')
600 ; constraint(H, ','),
601 constraints(T)
602 ).
603
604constraint(H, End) -->
605 html(div(class(constraint),
606 [ \constraint(H),
607 End
608 ])).
609
610constraint('\u2209'(V,S)) -->
611 !,
612 html([ \term(V), ' \u2209 ', \term(S) ]).
613constraint({ClpQ}) -->
614 !,
615 { comma_list(ClpQ, List),
616 maplist(to_clpq, List, Constraints)
617 },
618 constraints(Constraints).
619constraint(C) -->
620 term(C).
621
622to_clpq(A > B, C) => C = (A #> B).
623to_clpq(A < B, C) => C = (A #< B).
624to_clpq(A >= B, C) => C = (A #>= B).
625to_clpq(A =< B, C) => C = (A #=< B).
626to_clpq(A = B, C) => C = (A #= B).
627to_clpq(A =\= B, C) => C = (A #<> B).
628to_clpq(X, Y) => Y = X.
629
630term(T) -->
631 term(T,
632 [ quoted(true),
633 numbervars(true)
634 ]).
635
636justification_section(M:Results) -->
637 { Tree = Results.get(tree) },
638 !,
639 html_justification_tree(M:Tree, []).
640justification_section(_) -->
641 [].
642
643
644 647
648scasp_help(Request) :-
649 reply_html_page([ title('s(CASP) web server -- help')
650 ],
651 [ h1(['The s(CASP) web server']),
652 \help_page(Request)
653 ]).
654
655help_page(Request) -->
656 { http_public_host_url(Request, Home),
657 http_link_to_id(solve, [], URL),
658 atom_concat(Home, URL, Solve)
659 },
660 html({|html(Solve)||
661<h2>Diclaimer</h2>
662
663<blockquote>
664This is a <b>demonstrator</b>. This service is likely to change functionality and location.
665This service runs on <a href="https://www.swi-prolog.org">SWI-Prolog</a> using the
666<a href="https://github.com/JanWielemaker/sCASP">[GitHub] SWI-Prolog port of s(CASP)</a>. Bugs
667should be reported at the issue tracker of the the GIT repository. Discussion can use the
668<a href="https://swi-prolog.discourse.group/">SWI-Prolog forum</a>
669
670</blockquote>
671
672<h2>About</h2>
673
674<p>The <code>s(CASP)</code> system is a top-down interpreter for ASP programs with
675constraints.</p>
676
677<p>This work was presented at ICLP'18 (<a href="https://www.cambridge.org/core/journals/theory-and-practice-of-logic-programming/article/constraint-answer-set-programming-without-grounding/55A678C618EF54487777F021D89B3FE7">Arias et al. 2018</a>), also available <a href="https://arxiv.org/abs/1804.11162">here</a>.</p>
678
679<p>And extended description of the justification trees was presented at ICLP'20 (<a href="http://www.cliplab.org/papers/sCASP-ICLP2020/TC-explainCASP.pdf">Arias et al. 2020</a>).</p>
680
681<p><code>s(CASP)</code> by <a href="mailto:joaquin.arias@urjc.es">Joaquin Arias</a>, is based on
682<a href="https://sourceforge.net/projects/sasp-system/"><code>s(ASP)</code></a> by
683Kyle Marple.</p>
684
685<p><code>s(CASP)</code> is an implementation of the stable model semantics of
686constraint logic programming. Unlike similar systems, it does not
687employ any form of grounding. This allows <code>s(CASP)</code> to execute programs
688that are not finitely groundable, including those which make use of
689lists and terms.</p>
690
691<h2>Using as a service</h2>
692
693<p>
694The server accepts data requests on <span>Solve</span>. It deals with several
695formats:
696</p>
697
698 <ul>
699 <li>POST using <code>Content-type: text/x-scasp</code><br>
700 In this case the document is plain text containing both the
701 program and the query as <code>?- Query</code>. Using <code>curl</code>
702 we can use this script:
703
704 <pre>
705 curl --data-binary @file.pl -H "Content-Type: text/x-scasp" -X POST <code>Solve</code>
706 </pre>
707 </li><li>POST using <code>Content-type: application/json</code><br>
708 In this case the request is a JSON document containing these keys:
709 <ul>
710 <li> <code>data</code><br>
711 This key is required and contains the program.
712 </li><li><code>query</code><br>
713 This key is required if the program does not contain a query. If
714 the program contains a query, the specified query in the JSON
715 request is used, <b>not</b> the one in the program.
716 </li><li><code>limit</code><br>
717 Max number of answers to return. Default is all.
718 </li><li><code>format</code><br>
719 One of <code>html</code> or <code>json</code>. Default is <code>json</code>.
720 </li><li><code>tree</code><br>
721 Boolean that indicates whether the justification is produced. Default
722 <code>true</code>.
723 </li>
724 </ul>
725 </li><li>POST using <code>Content-type: application/x-www-form-urlencoded</code><br>
726 As above. The default reply format is <code>html</code>.
727 </li><li>GET<br>
728 Interpret the query parameters as above. The default reply format is <code>html</code>.
729 </li>
730 </ul>
731 |}).
732
733
734 737
738:- dynamic
739 example/3. 740
741attach_examples(Spec) :-
742 atomic_list_concat(Ex, ':', Spec),
743 maplist(attach_example, Ex).
744
745attach_example(Dir) :-
746 exists_directory(Dir),
747 !,
748 forall(directory_member(Dir, File,
749 [ recursive(true),
750 extensions([pl]),
751 access(read)
752 ]),
753 attach_ex(File)).
754attach_example(File) :-
755 exists_file(File),
756 !,
757 attach_ex(File).
758attach_example(File) :-
759 print_message(warning, error(existence_error(file, File),_)).
760
761attach_ex(File) :-
762 variant_sha1(File, Id),
763 ex_comment(File, Comment),
764 assertz(example(Id, File, Comment)).
765
766
(File, Comment) :-
768 setup_call_cleanup(
769 open(File, read, In),
770 ( read_line_to_string(In, Line),
771 sub_atom(Line, 0, _, _, '%'),
772 sub_atom(Line, 1, _, 0, Comment0),
773 split_string(Comment0, "", " \t%", [Title])
774 ),
775 close(In)),
776 !,
777 file_base_name(File, Base),
778 format(string(Comment), '~w -- ~w', [Base, Title]).
779ex_comment(File, Comment) :-
780 file_base_name(File, Comment)