%---------------------------------------------------------------------------% % 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, term_io, and varset. % % 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). % %---------------------------------------------------------------------------% % % This module is also used as a test case in tests/debugger. % %---------------------------------------------------------------------------% :- module interpreter. :- interface. :- import_module io. :- pred main(io::di, io::uo) is det. %---------------------------------------------------------------------------% :- implementation. :- import_module list. :- import_module mercury_term_parser. :- import_module require. :- import_module solutions. :- import_module string. :- import_module term. :- import_module term_context. :- 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, "No files consulted.\n", !IO), io.set_exit_status(1, !IO) ; Args = [_ | _], database_init(Database0), consult_files(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), read_term(ReadTerm, !IO), ( ReadTerm = eof ; ReadTerm = error(ErrorMessage, LineNumber), io.format("Error reading term at line %d of standard input: %s\n", [i(LineNumber), s(ErrorMessage)], !IO), main_loop(Database, !IO) ; ReadTerm = term(VarSet0, Goal), % Any special commands with side-effects (such as `consult' % and `listing') could be identified and processed here. solutions(solve(Database, Goal, VarSet0), Solutions), ( Solutions = [], io.write_string("No.\n", !IO) ; Solutions = [_ | _], write_solutions(Solutions, Goal, !IO), io.write_string("Yes.\n", !IO) ), main_loop(Database, !IO) ). :- pred write_solutions(list(varset)::in, term::in, io::di, io::uo) is det. write_solutions([], _, !IO). write_solutions([VarSet | VarSets], Goal, !IO) :- term_io.write_term_nl(VarSet, Goal, !IO), write_solutions(VarSets, Goal, !IO). %---------------------------------------------------------------------------% :- pred consult_files(list(string)::in, database::in, database::out, io::di, io::uo) is det. consult_files([], !Database, !IO). consult_files([File | Files], !Database, !IO) :- consult_file(File, !Database, !IO), consult_files(Files, !Database, !IO). :- pred consult_file(string::in, database::in, database::out, io::di, io::uo) is det. consult_file(File, !Database, !IO) :- io.format("Consulting file `%s'...\n", [s(File)], !IO), io.open_input(File, OpenResult, !IO), ( OpenResult = ok(InStream), consult_until_eof(InStream, !Database, !IO), io.close_input(InStream, !IO) ; OpenResult = error(_), io.format("Error opening file `%s' for input.\n", [s(File)], !IO) ). :- pred consult_until_eof(io.text_input_stream::in, database::in, database::out, io::di, io::uo) is det. consult_until_eof(InStream, !Database, !IO) :- read_term(InStream, ReadTerm, !IO), ( ReadTerm = eof ; ReadTerm = error(ErrorMessage, LineNumber), io.format("Error reading term at line %d of standard input: %s\n", [i(LineNumber), s(ErrorMessage)], !IO), consult_until_eof(InStream, !Database, !IO) ; ReadTerm = term(VarSet, Term), database_assert_clause(VarSet, Term, !Database), consult_until_eof(InStream, !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 varset thus updated as the result. % % We represent goals simply as terms. We parse (i.e. we discover % the structure of) the body of each clause every time we interpret % that clause. Definite logic programs do not allow disjunctions % in the bodies of clauses, but we do, so for us, each clause is % a boolean expression built up (using the conjunction operator "," % and/or the disjunction operator ";") from three kinds of primitives: % `true', unifications, and calls to user-defined predicates. % % Parsing each clause just once, before we put it into the database, % would be more efficient. % % Not looking up the database of user-defined predicates on goals % whose top-level functor is ,/2, ;/2, true/0 or =/2 would also be % more efficient, as well as semantically cleaner. % :- pred solve(database::in, term::in, varset::in, varset::out) is nondet. solve(Database, Goal, !VarSet) :- ( Goal = term.functor(term.atom(", "), [SubGoalA, SubGoalB], _), solve(Database, SubGoalA, !VarSet), solve(Database, SubGoalB, !VarSet) ; Goal = term.functor(term.atom(";"), [SubGoalA, SubGoalB], _), ( solve(Database, SubGoalA, !VarSet) ; solve(Database, SubGoalB, !VarSet) ) ; Goal = term.functor(term.atom("true"), [], _) ; Goal = term.functor(term.atom("="), [TermA, TermB], _), unify_term_pair(TermA, TermB, !VarSet) ; database_lookup_clause(Database, Goal, ClauseVarSet, Head0, Body0), rename_apart(ClauseVarSet, [Head0, Body0], [Head, Body], !VarSet), unify_term_pair(Goal, Head, !VarSet), solve(Database, Body, !VarSet) ). %---------------------------------------------------------------------------% :- pred rename_apart(varset::in, list(term)::in, list(term)::out, varset::in, varset::out) is det. rename_apart(NewVarSet, Terms0, Terms, VarSet0, VarSet) :- varset.merge(VarSet0, NewVarSet, Terms0, VarSet, Terms). %---------------------------------------------------------------------------% % unify_term_pair(TermX, TermY, !VarSet): % % Unify TermX with TermY, updating the varset if the unification succeeds. % % The standard library module `term' contains routines for unifying terms % based on separate substitutions (maps from variables to terms), % but here we are using substitutions that are contained in the varset % itself, so we cannot use those versions. % :- pred unify_term_pair(term::in, term::in, varset::in, varset::out) is semidet. unify_term_pair(TermX, TermY, !VarSet) :- ( TermX = term.variable(VarX, _ContextX), TermY = term.variable(VarY, _ContextY), ( if varset.search_var(!.VarSet, VarX, BindingOfX) then ( if varset.search_var(!.VarSet, VarY, BindingOfY) then % Both X and Y already have bindings; % unify the terms they are bound to. unify_term_pair(BindingOfX, BindingOfY, !VarSet) else % X is bound, Y is not. Symmetrical with the opposite case. apply_rec_substitution(!.VarSet, BindingOfX, SubstBindingOfX), ( if SubstBindingOfX = term.variable(VarY, _) then true else not var_occurs_in_term(VarY, SubstBindingOfX, !.VarSet), varset.bind_var(VarY, SubstBindingOfX, !VarSet) ) ) else ( if varset.search_var(!.VarSet, VarY, BindingOfY) then % Y is bound, X is not. Symmetrical with the opposite case. apply_rec_substitution(!.VarSet, BindingOfY, SubstBindingOfY), ( if SubstBindingOfY = term.variable(VarX, _) then true else not var_occurs_in_term(VarX, SubstBindingOfY, !.VarSet), varset.bind_var(VarX, SubstBindingOfY, !VarSet) ) else % Both X and Y are unbound variables; bind one to the other. % It does not matter whether we bind X to Y, or Y to X. ( if VarX = VarY then true else TermY = term.variable(VarY, term_context.dummy_context), varset.bind_var(VarX, TermY, !VarSet) ) ) ) ; TermX = term.variable(VarX, _ContextX), TermY = term.functor(_FunctorY, _ArgTermsY, _ContextY), unify_var_functor(VarX, TermY, !VarSet) ; TermX = term.functor(_FunctorX, _ArgTermsX, _ContextX), TermY = term.variable(VarY, _ContextY), unify_var_functor(VarY, TermX, !VarSet) ; TermX = term.functor(FunctorX, ArgTermsX, _ContextX), TermY = term.functor(FunctorY, ArgTermsY, _ContextY), FunctorX = FunctorY, unify_term_pairs(ArgTermsX, ArgTermsY, !VarSet) ). :- inst term_functor for term/1 ---> functor(ground, ground, ground). % Unify a variable with a term that is known to be a functor % applied to some argument terms. % :- pred unify_var_functor(term.var::in, term::in(term_functor), varset::in, varset::out) is semidet. unify_var_functor(VarX, TermY, !VarSet) :- ( if varset.search_var(!.VarSet, VarX, BindingOfX) then unify_term_pair(BindingOfX, TermY, !VarSet) else TermY = term.functor(_FunctorY, ArgTermsY, _ContextY), not var_occurs_in_terms(VarX, ArgTermsY, !.VarSet), varset.bind_var(VarX, TermY, !VarSet) ). :- pred unify_term_pairs(list(term)::in, list(term)::in, varset::in, varset::out) is semidet. unify_term_pairs([], [], !VarSet). unify_term_pairs([TermX | TermXs], [TermY | TermYs], !VarSet) :- unify_term_pair(TermX, TermY, !VarSet), unify_term_pairs(TermXs, TermYs, !VarSet). %---------------------------------------------------------------------------% % var_occurs_in_term(VarX, TermY, VarSet): % % Succeed iff VarX occurs in TermY, either as is, % or after the substitution in VarSet is applied to TermY. % % VarX must not be mapped by the substitution in VarSet. % :- pred var_occurs_in_term(var::in, term::in, varset::in) is semidet. var_occurs_in_term(VarX, TermY, VarSet) :- ( TermY = term.variable(VarY, _), ( VarX = VarY ; varset.search_var(VarSet, VarY, BindingOfY), var_occurs_in_term(VarX, BindingOfY, VarSet) ) ; TermY = term.functor(_FunctorY, ArgTermsY, _ContextY), var_occurs_in_terms(VarX, ArgTermsY, VarSet) ). % var_occurs_in_terms(VarX, TermsY, VarSet): % % Succeed iff VarX occurs in any of the TermsY, either as is, % or after the substitution in VarSet is applied to TermsY. % % VarX must not be mapped by the substitution in VarSet. % :- pred var_occurs_in_terms(var::in, list(term)::in, varset::in) is semidet. var_occurs_in_terms(VarX, [TermY | TermsY], VarSet) :- ( var_occurs_in_term(VarX, TermY, VarSet) ; var_occurs_in_terms(VarX, TermsY, VarSet) ). %---------------------------------------------------------------------------% % apply_rec_substitution(VarSet, Term0, Term): % % Recursively apply substitution to Term0 until no more substitutions % can be applied, and then return the result in Term. % :- pred apply_rec_substitution(varset::in, term::in, term::out) is det. apply_rec_substitution(VarSet, Term0, Term) :- ( Term0 = term.variable(Var, _), ( if varset.search_var(VarSet, Var, Replacement) then % Recursively apply the substitution to the replacement. apply_rec_substitution(VarSet, Replacement, Term) else Term = term.variable(Var, term_context.dummy_context) ) ; Term0 = term.functor(Name, ArgTerms0, Context), apply_rec_substitution_to_list(VarSet, ArgTerms0, ArgTerms), Term = term.functor(Name, ArgTerms, Context) ). :- pred apply_rec_substitution_to_list(varset::in, list(term)::in, list(term)::out) is det. apply_rec_substitution_to_list(_VarSet, [], []). apply_rec_substitution_to_list(VarSet, [Term0 | Terms0], [Term | Terms]) :- apply_rec_substitution(VarSet, Term0, Term), apply_rec_substitution_to_list(VarSet, Terms0, Terms). %---------------------------------------------------------------------------% % We store the database just as a list of clauses. % % This makes the code simple and readable, but it severely limits its % performance on anything bigger than toy programs. % % It would be more realistic to index the database on the predicate name/arity, % and subindex on the name/arity of the first argument. :- type database == list(clause). :- type clause ---> clause( clause_vars :: varset, clause_head :: term, clause_body :: 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, Body = term.functor(term.atom("true"), [], term_context.dummy_context) ), Clause = clause(VarSet, Head, Body). % database_lookup_clause(Database, Goal, VarSet, Head, Body): % % For each clause in Database whose head may unify with Goal, % return the representation of that clause: its varset, head and body. % % Since our database has no indexing, we ignore the goal, and return % *all* the clauses. % :- 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). %---------------------------------------------------------------------------%