Prolog.java

/*
 * #%L
 * prolobjectlink-jpi-jtrolog
 * %%
 * Copyright (C) 2012 - 2018 WorkLogic Project
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 2.1 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
 * #L%
 */
/*
 * tuProlog - Copyright (C) 2001-2002  aliCE team at deis.unibo.it
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package jTrolog.engine;

import jTrolog.errors.*;
import jTrolog.lib.BasicLibrary;
import jTrolog.lib.Library;
import jTrolog.lib.BuiltIn;
import jTrolog.parser.Parser;
import jTrolog.terms.*;
import jTrolog.terms.Number;

import java.io.Serializable;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.IOException;
import java.util.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * The class representing a jTrolog Prolog machine.
 */
@SuppressWarnings({ "rawtypes", "unchecked","serial", "unused" })
public class Prolog implements Serializable {

	public static final String VERSION = "2.1";// jTrolog version
	public static final int OP_LOW = 1;
	public static final int OP_HIGH = 1200;

	private LibraryAndTheoryManager libraryAndTheoryManager; // Rule and Fact
																// Database
																// Manager
	private OperatorTable opTable; // Table mapping operators

	private PrintStream currentPS = System.out; // the current printstream
	private Struct currentQuery; // the last query
	private Engine currentEngine; // the last initialized engine
	public static final Prolog defaultMachine = new DefaultProlog();
	public static final int FX = 0;
	public static final int FY = 1;
	public static final int XFX = 2;
	public static final int XFY = 3;
	public static final int YFX = 4;
	public static final int XF = 5;
	public static final int YF = 6;

	/**
	 * Builds a prolog engine with default libraries: BasicLibrary, ISOLibrary,
	 * IOLibrary
	 */
	public Prolog() {
		this(new String[] { "jTrolog.lib.BasicLibrary", "jTrolog.lib.ISOLibrary", "jTrolog.lib.IOLibrary" });
	}

	/**
	 * Builds a prolog engine with default libraries: BasicLibrary, ISOLibrary,
	 * IOLibrary + the extraLib
	 * 
	 * @param extraLib
	 *            additional library to be loaded
	 */
	public Prolog(String extraLib) {
		this();
		try {
			loadLibrary(extraLib);
		} catch (InvalidLibraryException e) {
			warn(e.toString());
		}
	}

	/**
	 * Builds a prolog engine using _only_ the specified libraries as parameters
	 * 
	 * @param libNames
	 *            the (class) names of the libraries to be loaded
	 */
	public Prolog(String[] libNames) {
		opTable = new OperatorTable();
		libraryAndTheoryManager = new LibraryAndTheoryManager(this);
		try {
			loadLibrary(new BuiltIn(this));
		} catch (InvalidLibraryException e) {
			warn(e.toString());
		}

		if (libNames == null)
			return;
		for (int i = 0; i < libNames.length; i++) {
			try {
				loadLibrary(libNames[i]);
			} catch (InvalidLibraryException e) {
				warn(e.toString());
			}
		}
	}

	/**
	 * Starts a command line interface with jTrolog Prolog engine. Builtin-,
	 * Basic-, ISO- and IO-Libraries are loaded. In the future, args could be
	 * made to look for URLs with theories?
	 */
	public static void main(String[] args) throws IOException {
		Prolog vm = new Prolog();
		System.out.println("jTrolog - Java Trondheim Prolog - v.2.1");
		System.out.print("?- ");
		BufferedReader consoleIn = new BufferedReader(new InputStreamReader(System.in));
		String input = "";
		String tmp;
		while ((tmp = consoleIn.readLine()) != null) {
			input += tmp;
			try {
				if (input.trim().endsWith(".")) {
					Solution x = vm.solve(input);
					System.out.println("\nresult: " + x);
					System.out.println(x.bindingsToString());
				} else if (input.trim().length() == 0) {
					Solution x = vm.solveNext();
					System.out.println("\nresult: " + x);
					System.out.println(x.bindingsToString());
				}
			} catch (Throwable throwable) {
				System.out.println("Prolog error (but don't be alarmed):\n" + throwable.getMessage());
			} finally {
				input = "";
				System.out.print("?- ");
			}
		}
	}

	/*******************************************************************************************************************
	 * The Rule and Fact Database System The Library System
	 */
	public void assertZ(Clause toBeAsserted) throws PrologException {
		String index = toBeAsserted.head.predicateIndicator;
		if (staticDBContainsPredicate(index))
			throw new PrologException("permission_error(modify, static_procedure, " + index + ")");
		libraryAndTheoryManager.assertZ(toBeAsserted, index);
	}

	public void assertA(Clause toBeAsserted) throws PrologException {
		String index = toBeAsserted.head.predicateIndicator;
		if (staticDBContainsPredicate(index))
			throw new PrologException("permission_error(modify, static_procedure, " + index + ")");
		libraryAndTheoryManager.assertA(toBeAsserted, index);
	}

	public Struct retract(Struct sarg0) throws PrologException {
		String index = ((Struct) sarg0.getArg(0)).predicateIndicator;
		if (staticDBContainsPredicate(index))
			throw new PrologException("permission_error(modify, static_procedure, " + index + ")");
		Struct struct = libraryAndTheoryManager.retract(sarg0, index);
		if (struct != null)
			warn("DELETE: " + struct + "\n");
		return struct;
	}

	private boolean staticDBContainsPredicate(String key) {
		return libraryAndTheoryManager.isLibraryRule(key) || hasPrimitive(key);
	}

	public void abolish(String predicateIndicator) throws PrologException {
		if (staticDBContainsPredicate(predicateIndicator))
			throw new PrologException("permission_error(modify, static_procedure, " + predicateIndicator + ")");
		List l = libraryAndTheoryManager.abolish(predicateIndicator);
	}

	public void setDynamicPredicateIndicator(String predicateIndicator) throws PrologException {
		if (staticDBContainsPredicate(predicateIndicator))
			throw new PrologException("permission_error(modify, static_procedure, " + predicateIndicator + ")");
		libraryAndTheoryManager.setDynamic(predicateIndicator);
	}

	public List find(String predIndicator) throws PrologException {
		List rulesFromDatabase = libraryAndTheoryManager.find(predIndicator);
		if (rulesFromDatabase == null)
			throw new PrologException("The predicate " + predIndicator + " is unknown.");
		return rulesFromDatabase;
	}

	public Iterator dynamicPredicateIndicators() {
		return libraryAndTheoryManager.dynamicPredicateIndicators();
	}

	/**
	 * clears all dynamic predicates and sets the new newTheory
	 * 
	 * @deprecated use clearTheory() + addTheory(newTheory) instead
	 * @param newTheory
	 *            to be set
	 * @throws PrologException
	 *             if newTheory is not valid
	 */
	public void setTheory(String newTheory) throws PrologException {
		libraryAndTheoryManager.clear();
		addTheory(newTheory);
	}

	/**
	 * removes all dynamic predicates
	 */
	public void clearTheory() {
		libraryAndTheoryManager.clear();
	}

	/**
	 * @param theory
	 *            to be added to the existing set of theories in the database.
	 * @throws PrologException
	 *             if the new theory is not valid
	 */
	public void addTheory(String theory) throws PrologException {
		libraryAndTheoryManager.consult(theory);
	}

	/**
	 * @return the current theory in the Prolog machine (only dynamic)
	 */
	public String getTheory() {
		return libraryAndTheoryManager.getTheory(true);
	}

	/**
	 * @return the last theory to be consulted or attempted consulted as text
	 */
	public String getLastConsultedTheory() {
		return libraryAndTheoryManager.getLastConsultedTheory();
	}

	/**
	 * Loads a library. If a library with the same name is already present, a
	 * warning event is notified. Then, the current instance of that library is
	 * discarded, and the new instance gets loaded.
	 * 
	 * @param className
	 *            of the Java class containing the Library to be loaded
	 * @return the reference to the Library just loaded
	 * @throws InvalidLibraryException
	 *             if name is not a valid library
	 */
	public synchronized Library loadLibrary(String className) throws InvalidLibraryException {
		try {
			return loadLibrary((Library) Class.forName(className).newInstance());
		} catch (Exception e) {
			throw new InvalidLibraryException(className);
		}
	}

	/**
	 * Loads a specific instance of a library If a library of the same class is
	 * already present, a warning event is notified. Then, the current instance
	 * of that library is discarded, and the new instance gets loaded.
	 * 
	 * @param lib
	 *            the (Java class) name of the library to be loaded
	 * @throws InvalidLibraryException
	 *             if name is not a valid library
	 */
	public Library loadLibrary(Library lib) throws InvalidLibraryException {
		String name = lib.getName();
		if (getLibrary(name) != null)
			throw new InvalidLibraryException("library " + name + " already loaded.");

		lib.setEngine(this);

		addPrimitives(lib);
		try {
			return libraryAndTheoryManager.consultLib(lib);
		} catch (PrologException e) {
			throw new InvalidLibraryException(lib.getName(), e);
		}
	}

	/**
	 * @param name
	 *            of the library to be unloaded
	 * @throws InvalidLibraryException
	 *             if no loaded Library has the given name
	 */
	public void unloadLibrary(String name) throws InvalidLibraryException {
		Library library = getLibrary(name);
		if (library == null)
			throw new InvalidLibraryException(name);
		Library unloaded = libraryAndTheoryManager.unconsultLib(library);
		removePrimitives(unloaded);
	}

	public Library getLibrary(String name) {
		return libraryAndTheoryManager.getLibrary(name);
	}

	/**
	 * @return the names of the libraries currently loaded
	 */
	public Iterator getCurrentLibraries() {
		return libraryAndTheoryManager.getCurrentLibraries();
	}

	/**
	 * @param ps
	 *            the engine printstream.
	 */
	public void setPrintStream(PrintStream ps) {
		currentPS = ps;
	}

	/**
	 * @return the engine printstream.
	 */
	public PrintStream getPrintStream() {
		return currentPS;
	}

	/*******************************************************************************************************************
	 * The Operator System
	 */

	/**
	 * Gets the list of the operators currently defined
	 * 
	 * @return the list of the operators
	 */
	public synchronized Iterator getCurrentOperators() {
		return opTable.getAllOperators();
	}

	public int getOperatorPriority(String name, int type) {
		return opTable.getOperatorPriority(name, type);
	}

	public void opNew(String name, int type, int i) {
		opTable.addOperator(name, type, i);
	}

	public void opNew(String name, String type, int i) {
		if (type.equalsIgnoreCase("fx"))
			opTable.addOperator(name, FX, i);
		if (type.equalsIgnoreCase("fy"))
			opTable.addOperator(name, FY, i);
		if (type.equalsIgnoreCase("xfx"))
			opTable.addOperator(name, XFX, i);
		if (type.equalsIgnoreCase("xfy"))
			opTable.addOperator(name, XFY, i);
		if (type.equalsIgnoreCase("yfx"))
			opTable.addOperator(name, YFX, i);
		if (type.equalsIgnoreCase("yf"))
			opTable.addOperator(name, YF, i);
		if (type.equalsIgnoreCase("xf"))
			opTable.addOperator(name, XF, i);

	}

	/*******************************************************************************************************************
	 * To solve, that is the problem
	 */

	/**
	 * Solves a query
	 * 
	 * @param st
	 *            the string representing the goal to be demonstrated
	 * @return the result of the demonstration
	 * @see SolutionManager
	 */
	public synchronized Solution solve(String st) throws Throwable {
		Parser p = new Parser(st, this);
		Term t = p.nextTerm(true);
		if (!(t instanceof Struct)) // Var or Number is considered true, since
									// they are not false?
			return new Solution(t);

		try {
			Struct g = (Struct) t;
			if (getPrimitiveExp(g) != null)
				return new Solution(new BindingsTable().evalExpression(this, g));
			return solve(g);
		} catch (InvocationTargetException e) {
			Throwable cause = e;
			while (cause instanceof InvocationTargetException)
				cause = cause.getCause();
			throw cause;
		}
	}

	/**
	 * Solves a query
	 * 
	 * @param g
	 *            the term representing the goal to be demonstrated
	 * @return the result of the demonstration
	 * @see SolutionManager
	 */
	public synchronized Solution solve(Struct g) throws Throwable {
		onSolveBegin(g);
		currentQuery = (Struct) BuiltIn.convertTermToClauseBody(g);
		currentEngine = new Engine(this, BuiltIn.convertTermToClauseBody2(currentQuery));
		BindingsTable result = currentEngine.runFirst();
		onSolveEnd();
		return SolutionManager.prepareSolution(currentQuery, result);
	}

	/**
	 * Gets next solution
	 * 
	 * @return the result of the demonstration
	 * @throws NoMorePrologSolutions
	 *             if no more solutions are present
	 * @see SolutionManager
	 */
	public synchronized Solution solveNext() throws Throwable {
		if (currentEngine == null || !currentEngine.hasAlternatives())
			throw new NoMorePrologSolutions();
		BindingsTable result = currentEngine.run(Engine.BACK);
		onSolveEnd();
		return SolutionManager.prepareSolution(currentQuery, result);
	}

	public synchronized void onSolveBegin(Term g) {
		for (Iterator it = getCurrentLibraries(); it.hasNext();)
			((Library) it.next()).onSolveBegin(g);
	}

	public synchronized void onSolveEnd() {
		for (Iterator it = getCurrentLibraries(); it.hasNext();)
			((Library) it.next()).onSolveEnd();
	}

	/**
	 * Asks for the presence of open alternatives to be explored in current
	 * demostration process.
	 * 
	 * @return true if open alternatives are present
	 */
	public synchronized boolean hasOpenAlternatives() throws Throwable {
		return currentEngine.hasAlternatives();
	}

	/**
	 * Matches the structure of the two original terms. OBS: Variables in the
	 * original terms are not resolved. If this is desired, then the terms
	 * passed in should be clonedAndResolved first.
	 * 
	 * OBS2: no unification of variables is made.
	 * 
	 * @param t0
	 *            first term to be matched
	 * @param t1
	 *            second term to be matched
	 * @return true if the structure of the two terms match
	 */
	public static synchronized boolean match(Term t0, Term t1) {
		if (t0 instanceof Var || t1 instanceof Var)
			return true;

		if (t0 instanceof Number && t1 instanceof jTrolog.terms.Number)
			return BasicLibrary.number_equality_2((Number) t0, (Number) t1);

		if (t0 instanceof StructAtom && t1 instanceof StructAtom)
			return t0.equals(t1);

		if (t0 instanceof Struct && t1 instanceof Struct) {
			Struct s0 = (Struct) t0;
			Struct s1 = (Struct) t1;
			if (s0.arity != s1.arity)
				return false;
			for (int i = 0; i < s1.arity; i++) {
				if (!match(s0.getArg(i), s1.getArg(i)))
					return false;
			}
			return true;
		}
		return false;
	}

	/*******************************************************************************************************************
	 * The Primitives system
	 */
	private HashMap directives = new HashMap();
	private HashMap expressions = new HashMap();

	public boolean hasPrimitive(String predicateIndiciator) {
		return directives.containsKey(predicateIndiciator) || expressions.containsKey(predicateIndiciator);
	}

	public boolean hasPrimitiveExp(String predicateIndiciator) {
		return expressions.containsKey(predicateIndiciator);
	}

	/**
	 * @return the primitive matching the predicate indicator signature of the
	 *         struct passed as argument.
	 */
	final PrimitiveInfo getPrimitive(Struct struct) {
		return (PrimitiveInfo) directives.get(struct.predicateIndicator);
	}

	final PrimitiveInfo getPrimitiveExp(Struct struct) {
		return (PrimitiveInfo) expressions.get(struct.predicateIndicator);
	}

	private void addPrimitives(Library src) {
		List prims = getPrimitives(src);
		for (int i = 0; i < prims.size(); i++) {
			PrimitiveInfo p = (PrimitiveInfo) prims.get(i);
			if (p.method.getReturnType() == Term.class)
				expressions.put(p.key, p);
			else
				directives.put(p.key, p);
		}
	}

	private void removePrimitives(Library src) {
		List prims = getPrimitives(src);
		for (int i = 0; i < prims.size(); i++) {
			PrimitiveInfo p = (PrimitiveInfo) prims.get(i);
			directives.remove(p.key);
			expressions.remove(p.key);
		}
	}

	/**
	 * 1. Every method in the library that matches the following criteria: -
	 * return type either boolean, void or Term, - the first parameter is
	 * BindingsTable, and - the method name ends with "_N" where N = number of
	 * parameters - 1. are added as Primitives to the Prolog machine that loads
	 * the Library.
	 * 
	 * 2. For each primitive added, the getSynomym(name) is also queried. For
	 * each synonyms, a copy of the primitive is added to the machine using the
	 * synonym name.
	 * 
	 * @param library
	 *            that is scanned for primitives
	 * @return a list of PrimitiveInfo
	 */
	private static List getPrimitives(Library library) {
		ArrayList result = new ArrayList();

		Method[] mlist = library.getClass().getMethods();
		methodLoop: for (int i = 0; i < mlist.length; i++) {
			Method m = mlist[i];
			String mName = m.getName();
			Class[] params = m.getParameterTypes();

			Class retType = m.getReturnType();
			if (!(retType == boolean.class || retType == Term.class || retType == void.class))
				continue;

			int index = mName.lastIndexOf('_');
			if (index == -1)
				continue;

			// retrieve and check arg number
			int arity = Integer.parseInt(mName.substring(index + 1, mName.length()));
			if (params.length - 1 != arity)
				continue;

			for (int j = 1; j < arity; j++) {
				if (!Term.class.isAssignableFrom(params[j]))
					continue methodLoop;
			}

			String rawName = mName.substring(0, index);
			result.add(new PrimitiveInfo(library, m, rawName, arity));

			// adding synonyms
			String[] synonyms = library.getSynonym(rawName);
			if (synonyms != null) {
				for (int j = 0; j < synonyms.length; j++)
					result.add(new PrimitiveInfo(library, m, synonyms[j], arity));
			}
		}
		return result;
	}

	/*******************************************************************************************************************
	 * The Warning system
	 */
	List warnings = new LinkedList();

	public synchronized void resetWarningList() {
		warnings.clear();
	}

	public List getAndResetWarnings() {
		List tmp = warnings;
		warnings = new LinkedList();
		return tmp;
	}

	/**
	 * @param m
	 *            adds the warning message to the warnings list
	 */
	public void warn(String m) {
		warnings.add(m);
	}

	/*******************************************************************************************************************
	 * The Flag system
	 */
	private HashMap flags = new HashMap();

	public void defineFlag(String name, Struct valueList, Term defValue, boolean modifiable) {
		flags.put(name, new Flag(name, valueList, defValue, modifiable));
	}

	public Flag getFlag(String name) {
		return (Flag) flags.get(name);
	}

	public Term getFlagValue(String name) {
		Flag flag = (Flag) flags.get(name);
		return flag == null ? null : flag.getValue();
	}

	public Term getPrologFlagList() {
		Struct flist = Term.emptyList;
		for (Iterator it = flags.values().iterator(); it.hasNext();) {
			Flag fl = (Flag) it.next();
			Term at0 = new Struct("flag", new Term[] { new StructAtom(fl.getName()), fl.getValue() });
			flist = new Struct(".", new Term[] { at0, flist });
		}
		return flist;
	}

	public static boolean evalPrimitive(PrimitiveInfo prim, Object[] primitive_args) throws Throwable {
		Method method = prim.method;
		try {
			if (method.getReturnType() == void.class) {
				method.invoke(prim.source, primitive_args);
				return true;
			}
			return ((Boolean) method.invoke(prim.source, primitive_args)).booleanValue();
		} catch (IllegalArgumentException e) {
			Class[] expectedArgs = method.getParameterTypes();
			for (int i = 1; i < primitive_args.length; i++) {
				Term actual = (Term) primitive_args[i];
				Class expectedClass = expectedArgs[i];
				if (expectedClass.isAssignableFrom(actual.getClass()))
					continue; // nothing wrong with this one, check next param
				if (actual instanceof Var)
					throw new PrologException("instantiation_error"); // expected
																		// anything
																		// but a
																		// Var
				String expected = expectedClass.getName();
				expected = expected.substring(expected.lastIndexOf('.') + 1);
				throw new PrologException("type_error(" + expected + ", " + actual + ")");
			}
			throw new PrologException("WTF: Bug in system.");
		}
	}
}