%---------------------------------------------------------------------------%
% vim: ts=4 sw=4 et ft=mercury
%---------------------------------------------------------------------------%
%
% File: interpreter.m.
% Main author: fjh.
%
% This is an interpreter for definite logic programs
% (i.e. pure Prolog with no negation or if-then-else.)
%
% This is just intended as a demonstration of the use of the
% meta-programming library modules term, varset, and term_io.
%
% There are many extensions/improvements that could be made;
% they are left as an exercise for the reader.
%
% For a more efficient version (using backtrackable destructive update),
% see extras/trailed_update/samples/interpreter.m.
%
% This source file is hereby placed in the public domain. -fjh (the author).
%
%---------------------------------------------------------------------------%
%
% The .exp file is for XXX.
% The .exp2 file is for asm_fast.gc bootchecks.
%
%---------------------------------------------------------------------------%

:- module interpreter.
:- interface.
:- import_module io.

:- pred main(io, io).
:- mode main(di, uo) is det.

%---------------------------------------------------------------------------%

:- implementation.

:- import_module list.
:- import_module require.
:- import_module solutions.
:- import_module string.
:- import_module term.
:- import_module term_io.
:- import_module varset.

main(!IO) :-
    io.write_string("Pure Prolog Interpreter.\n\n", !IO),
    io.command_line_arguments(Args, !IO),
    (
        Args = [],
        io.stderr_stream(StdErr, !IO),
        io.write_string(StdErr, "Usage: interpreter filename ...\n", !IO),
        io.set_exit_status(1, !IO)
    ;
        Args = [_ | _],
        database_init(Database0),
        consult_list(Args, Database0, Database, !IO),
        main_loop(Database, !IO)
    ).

:- pred main_loop(database::in, io::di, io::uo) is det.

main_loop(Database, !IO) :-
    io.write_string("?- ", !IO),
    term_io.read_term(ReadTerm, !IO),
    main_loop_2(ReadTerm, Database, !IO).

:- pred main_loop_2(read_term::in, database::in, io::di, io::uo) is det.

main_loop_2(eof, _Database, !IO).
main_loop_2(error(ErrorMessage, LineNumber), Database, !IO) :-
    io.write_string("Error reading term at line ", !IO),
    io.write_int(LineNumber, !IO),
    io.write_string(" of standard input: ", !IO),
    io.write_string(ErrorMessage, !IO),
    io.write_string("\n", !IO),
    main_loop(Database, !IO).
main_loop_2(term(VarSet0, Goal), Database, !IO) :-
    %%% It would be a good idea to add some special commands
    %%% with side-effects (such as `consult' and `listing');
    %%% these could be identified and processed here.
    solutions(solve(Database, Goal, VarSet0), Solutions),
    write_solutions(Solutions, Goal, !IO),
    main_loop(Database, !IO).

:- pred write_solutions(list(varset)::in, term::in, io::di, io::uo) is det.

write_solutions(Solutions, Goal, !IO) :-
    (
        Solutions = [],
        io.write_string("No.\n", !IO)
    ;
        Solutions = [_ | _],
        write_solutions_2(Solutions, Goal, !IO),
        io.write_string("Yes.\n", !IO)
    ).

:- pred write_solutions_2(list(varset)::in, term::in, io::di, io::uo) is det.

write_solutions_2([], _, !IO).
write_solutions_2([VarSet | VarSets], Goal, !IO) :-
    term_io.write_term_nl(VarSet, Goal, !IO),
    write_solutions_2(VarSets, Goal, !IO).

%---------------------------------------------------------------------------%

:- pred consult_list(list(string)::in, database::in, database::out,
    io::di, io::uo) is det.

consult_list([], !Database, !IO).
consult_list([File | Files], !Database, !IO) :-
    consult(File, !Database, !IO),
    consult_list(Files, !Database, !IO).

:- pred consult(string::in, database::in, database::out,
    io::di, io::uo) is det.

consult(File, Database0, Database, !IO) :-
    io.write_string("Consulting file `", !IO),
    io.write_string(File, !IO),
    io.write_string("'...\n", !IO),
    io.see(File, Result, !IO),
    ( if Result = ok then
        consult_until_eof(Database0, Database, !IO),
        io.seen(!IO)
    else
        io.write_string("Error opening file `", !IO),
        io.write_string(File, !IO),
        io.write_string("' for input.\n", !IO),
        Database = Database0
    ).

:- pred consult_until_eof(database::in, database::out, io::di, io::uo) is det.

consult_until_eof(!Database, !IO) :-
    term_io.read_term(ReadTerm, !IO),
    consult_until_eof_2(ReadTerm, !Database, !IO).

:- pred consult_until_eof_2(read_term::in, database::in, database::out,
    io::di, io::uo) is det.

consult_until_eof_2(eof, !Database, !IO).
consult_until_eof_2(error(ErrorMessage, LineNumber), !Database, !IO) :-
    io.write_string("Error reading term at line ", !IO),
    io.write_int(LineNumber, !IO),
    io.write_string(" of standard input: ", !IO),
    io.write_string(ErrorMessage, !IO),
    io.write_string("\n", !IO),
    consult_until_eof(!Database, !IO).

consult_until_eof_2(term(VarSet, Term), !Database, !IO) :-
    database_assert_clause(VarSet, Term, !Database),
    consult_until_eof(!Database, !IO).

%---------------------------------------------------------------------------%

% Solve takes a database of rules and facts, a goal to be solved,
% and a varset (which includes a supply of fresh vars, a substitution,
% and names for [some subset of] the variables). It updates the varset,
% producing a new substitution and perhaps introducing some new vars,
% and returns the result.

% Goals are stored just as terms.
% (It might be more efficient to parse them before storing them
% in the database. Currently we do this parsing work every time
% we interpret a clause.)

:- pred solve(database::in, term::in, varset::in, varset::out) is nondet.

solve(_Database, term.functor(term.atom("true"), [], _), !VarSet).
solve(Database, term.functor(term.atom(", "), [A, B], _), !VarSet) :-
    solve(Database, A, !VarSet),
    solve(Database, B, !VarSet).
solve(Database, term.functor(term.atom(";"), [A, B], _), !VarSet) :-
    ( solve(Database, A, !VarSet)
    ; solve(Database, B, !VarSet)
    ).
solve(_Database, term.functor(term.atom("="), [A, B], _), !VarSet) :-
    unify(A, B, !VarSet).
solve(Database, Goal, !VarSet) :-
    database_lookup_clause(Database, Goal, ClauseVarSet, Head0, Body0),
    rename_apart(ClauseVarSet, [Head0, Body0], [Head, Body], !VarSet),
    unify(Goal, Head, !VarSet),
    solve(Database, Body, !VarSet).

%---------------------------------------------------------------------------%

:- pred rename_apart(varset, list(term), list(term), varset, varset).
:- mode rename_apart(in, in, out, in, out) is det.

rename_apart(NewVarSet, Terms0, Terms, VarSet0, VarSet) :-
    varset.merge(VarSet0, NewVarSet, Terms0, VarSet, Terms).

%---------------------------------------------------------------------------%

% The standard library module `term' contains routines for
% unifying terms based on separate substitutions, but we are
% using the substitutions that are contained in the `varset',
% so we can't use those versions.

:- pred unify(term::in, term::in, varset::in, varset::out) is semidet.

unify(term.variable(X, _), term.variable(Y, _), VarSet0, VarSet) :-
    ( if varset.search_var(VarSet0, X, BindingOfX) then
        ( if varset.search_var(VarSet0, Y, BindingOfY) then
            % Both X and Y already have bindings -
            % just unify the terms they are bound to.
            unify(BindingOfX, BindingOfY, VarSet0, VarSet)
        else
            % Y is a variable which hasn't been bound yet.
            apply_rec_substitution(BindingOfX, VarSet0, SubstBindingOfX),
            ( if SubstBindingOfX = term.variable(Y, _) then
                VarSet = VarSet0
            else
                not occurs(SubstBindingOfX, Y, VarSet0),
                varset.bind_var(Y, SubstBindingOfX, VarSet0, VarSet)
            )
        )
    else
        ( if varset.search_var(VarSet0, Y, BindingOfY2) then
            % X is a variable which hasn't been bound yet.
            apply_rec_substitution(BindingOfY2, VarSet0, SubstBindingOfY2),
            ( if SubstBindingOfY2 = term.variable(X, _) then
                VarSet = VarSet0
            else
                not occurs(SubstBindingOfY2, X, VarSet0),
                varset.bind_var(X, SubstBindingOfY2, VarSet0, VarSet)
            )
        else
            % Both X and Y are unbound variables - bind one to the other.
            ( if X = Y then
                VarSet = VarSet0
            else
                varset.bind_var(X, term.variable(Y, context_init),
                    VarSet0, VarSet)
            )
        )
    ).
unify(term.variable(X, _), term.functor(F, As, C), VarSet0, VarSet) :-
    ( if varset.search_var(VarSet0, X, BindingOfX) then
        unify(BindingOfX, term.functor(F, As, C), VarSet0,
            VarSet)
    else
        not occurs_list(As, X, VarSet0),
        varset.bind_var(X, term.functor(F, As, C), VarSet0, VarSet)
    ).
unify(term.functor(F, As, C), term.variable(X, _), VarSet0, VarSet) :-
    ( if varset.search_var(VarSet0, X, BindingOfX) then
        unify(term.functor(F, As, C), BindingOfX, VarSet0, VarSet)
    else
        not occurs_list(As, X, VarSet0),
        varset.bind_var(X, term.functor(F, As, C), VarSet0, VarSet)
    ).
unify(term.functor(F, AsX, _), term.functor(F, AsY, _), !VarSet) :-
    unify_list(AsX, AsY, !VarSet).

:- pred unify_list(list(term)::in, list(term)::in, varset::in, varset::out)
    is semidet.

unify_list([], [], !VarSet).
unify_list([X | Xs], [Y | Ys], !VarSet) :-
    unify(X, Y, !VarSet),
    unify_list(Xs, Ys, !VarSet).

%---------------------------------------------------------------------------%

    % occurs(Term, Var, Subst) succeeds if Term contains Var,
    % perhaps indirectly via the substitution. (The variable must
    % not be mapped by the substitution.)

:- pred occurs(term::in, var::in, varset::in) is semidet.

occurs(term.variable(X, _), Y, VarSet) :-
    (
        X = Y
    ;
        varset.search_var(VarSet, X, BindingOfX),
        occurs(BindingOfX, Y, VarSet)
    ).
occurs(term.functor(_F, As, _), Y, VarSet) :-
    occurs_list(As, Y, VarSet).

:- pred occurs_list(list(term)::in, var::in, varset::in) is semidet.

occurs_list([Term | Terms], Y, VarSet) :-
    ( occurs(Term, Y, VarSet)
    ; occurs_list(Terms, Y, VarSet)
    ).

%---------------------------------------------------------------------------%

    % apply_rec_substitution(Term0, VarSet, Term):
    %
    % Recursively apply substitution to Term0 until no more substitions
    % can be applied, and then return the result in Term.
    %
:- pred apply_rec_substitution(term::in, varset::in, term::out) is det.

apply_rec_substitution(term.variable(Var, _), VarSet, Term) :-
    ( if varset.search_var(VarSet, Var, Replacement) then
        % Recursively apply the substitution to the replacement.
        apply_rec_substitution(Replacement, VarSet, Term)
    else
        Term = term.variable(Var, context_init)
    ).
apply_rec_substitution(term.functor(Name, Args0, Context), VarSet,
         term.functor(Name, Args, Context)) :-
    apply_rec_substitution_to_list(Args0, VarSet, Args).

:- pred apply_rec_substitution_to_list(list(term)::in, varset::in,
    list(term)::out) is det.

apply_rec_substitution_to_list([], _VarSet, []).
apply_rec_substitution_to_list([Term0 | Terms0], VarSet, [Term | Terms]) :-
    apply_rec_substitution(Term0, VarSet, Term),
    apply_rec_substitution_to_list(Terms0, VarSet, Terms).

%---------------------------------------------------------------------------%

% We store the database just as a list of clauses.
% (It would be more realistic to index this on the predicate name/arity
% and subindex on the name/arity of the first argument.)

:- type database == list(clause).
:- type clause
    --->    clause(varset, term, term).

:- pred database_init(database::out) is det.

database_init([]).

:- pred database_assert_clause(varset::in, term::in,
    database::in, database::out) is det.

database_assert_clause(VarSet, Term, Database, [Clause | Database]) :-
    ( if Term = term.functor(term.atom(":-"), [H, B], _) then
        Head = H,
        Body = B
    else
        Head = Term,
        term.context_init(Context),
        Body = term.functor(term.atom("true"), [], Context)
    ),
    Clause = clause(VarSet, Head, Body).

:- pred database_lookup_clause(database::in, term::in,
    varset::out, term::out, term::out) is nondet.

database_lookup_clause(Database, _Goal, VarSet, Head, Body) :-
    list.member(Clause, Database),
    Clause = clause(VarSet, Head, Body).

%---------------------------------------------------------------------------%
