Interpreter.java

package coneforest.psylla.runtime;

import coneforest.psylla.core.*;
import coneforest.psylla.runtime.parser.Parser;
import coneforest.psylla.runtime.parser.ParserConstants;
import coneforest.psylla.runtime.parser.Token;
import coneforest.psylla.runtime.parser.TokenMgrError;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringJoiner;
import jline.ConsoleReader;

/**
*	The Psylla language interpreter.
*/
public class Interpreter
	//extends Thread
	implements PsyContext
{
	protected DictStack dstack;
	private final OperandStack ostack;
	private final ExecutionStack estack;
	private final ProcStack procstack;
	private final HashMap<String, String> resourceRegistry=new HashMap<>();
	private final NamespacePool nspool=new NamespacePool();
	//private final Thread thread=Thread.ofVirtual().unstarted(this);
	private final Thread thread=Thread.ofPlatform().unstarted(this);
	/*private final Thread thread=new Thread()
		{
			@Override
			public void run()
			{
				Interpreter.this.run();
			}
		};*/

	private boolean stopped=false;
	private boolean running=true;

	private final ClassLoader classLoader=new DynamicClassLoader()
		{
			@Override
			protected Iterable<String> getClassPath()
				throws PsyUndefinedException
			{
				@SuppressWarnings("unchecked")
				final var parentIterator
					=((PsyFormalArray<PsyTextual>)systemDict().get("classpath")).iterator();
				return new Iterable<String>()
					{
						@Override
						public Iterator<String> iterator()
						{
							return new Iterator<String>()
								{
									@Override
									public boolean hasNext()
									{
										return parentIterator.hasNext();
									}

									@Override
									public String next()
									{
										return parentIterator.next().stringValue();
									}
								};
						}
					};
			}
		};

	/**
	*	Creates a new Psylla language interpreter.
	*/
	public Interpreter()
	{
		try
		{
			ostack=new OperandStack();
			estack=new ExecutionStack();
			procstack=new ProcStack();
			dstack=new DictStack();
		}
		catch(final PsyErrorException e)
		{
			// TODO more appropriate exception
			throw new AssertionError(e);
		}
	}

	@Override
	public void fork()
		throws PsyStackUnderflowException, PsyUnmatchedMarkException
	{
		final var ostack=operandStackBacked(1);
		final var o=ostack.getBacked(0);
		final var forkedDstack=dstack.clone();
		final var oForkedContext=new Interpreter()
			{
				{
					dstack=forkedDstack;
				}

				@Override
				public void run()
				{
					o.invoke(this);
					handleExecutionStack(0);
					if(getStopped())
						return;
				}
			};
		final int i=ostack.findMarkPosition();
		final int ostackSize=ostack.size();
		final var forkedOstack=oForkedContext.operandStack();
		for(int j=i+1; j<ostackSize; j++)
			forkedOstack.push(ostack.get(j));
		ostack.setSize(i);
		ostack.push(oForkedContext);
		oForkedContext.start();
	}

	/*public void importType(final String typeName)
		throws PsyErrorException
	{
		PsyNamespace.namespace("system").psyImport(PsyNamespace.namespace(TypeResolver.resolve(typeName)));
	}*/

	@Override
	public OperandStack operandStack()
	{
		return ostack;
	}

	@Override
	public OperandStack operandStackBacked(final int count)
		throws PsyStackUnderflowException
	{
		ostack.popOperands(count);
		return ostack;
	}

	@Override
	public DictStack dictStack()
	{
		return dstack;
	}

	@Override
	public ExecutionStack executionStack()
	{
		return estack;
	}

	/**
	*	{@return the interpreter’s class loader}
	*/
	public ClassLoader classLoader()
	{
		return classLoader;
	}

	@SuppressWarnings("unchecked")
	protected <T extends PsyObject> T load(final String name)
		throws PsyUndefinedException
	{
		final var prefixOffset=name.indexOf('@');
		if(prefixOffset==-1)
			return dstack.<T>load(name);
		return (T)nspool.get(name.substring(0, prefixOffset))
				.get(name.substring(prefixOffset+1));
	}

	@Override
	public <T extends PsyObject> T psyLoad(final PsyTextual oKey)
		throws PsyUndefinedException
	{
		return this.<T>load(oKey.stringValue());
	}

	@Override
	public void handleExecutionStack(final int level)
	{
		while(estack.size()>level)
			estack.pop().execute(this);
	}

	@Override
	public PsyFormalDict<PsyObject> /*<? extends PsyObject>*/ currentDict()
	{
		return dstack.peek();
	}

	@Override
	public PsyFormalDict<PsyObject> /*<? extends PsyObject>*/ systemDict()
	{
		return dstack.get(0);
	}

	@Override
	public PsyFormalDict<PsyObject> /*<? extends PsyObject>*/ userDict()
	{
		return dstack.get(1);
	}

	@Override
	public NamespacePool namespacePool()
	{
		return nspool;
	}

	/**
	*	{@return the current namespace}
	*/
	public PsyNamespace currentNamespace()
	{
		return dstack.currentNamespace();
	}

	/**
	*	Sets the interpreter’s standard reader.
	*
	*	@param reader the reader.
	*/
	public void setReader(final Reader reader)
	{
		systemDict().put("stdin", new PsyReader(reader));
	}

	/**
	*	Sets the interpreter’s standard writer.
	*
	*	@param writer the writer.
	*/
	public void setWriter(final Writer writer)
	{
		systemDict().put("stdout", new PsyWriter(writer));
	}

	/**
	*	Sets the interpreter’s standard error writer.
	*
	*	@param writer the error writer.
	*/
	public void setErrorWriter(final Writer writer)
	{
		systemDict().put("stderr", new PsyWriter(writer));
	}

	/**
	*	Reseeds the interpreter’s standard pseudorandom generator, using the given seed.
	*
	*	@param randomSeed the seed.
	*	@throws PsyUndefinedException when TODO
	*/
	public void setRandomSeed(final Long randomSeed)
		throws PsyUndefinedException
	{
		if(randomSeed!=null)
			((PsyRandom)systemDict().get("stdrandom"))
					.psySetSeed(PsyInteger.of(randomSeed));
	}

	public void setClassPath(final String[] classPath)
		throws
			PsyLimitCheckException,
			PsyRangeCheckException,
			PsyUndefinedException
	{
		@SuppressWarnings("unchecked")
		final var oClassPath
			=(PsyFormalArray<PsyTextual>)systemDict().get("classpath");
		final var envClassPath=System.getenv("PSYLLA_CLASSPATH");
		if(envClassPath!=null)
			for(final var pathItem: envClassPath.split(File.pathSeparator))
				oClassPath.psyAppend(new PsyString(pathItem));
		if(classPath!=null)
			for(final var pathItem: classPath)
				oClassPath.psyAppend(new PsyString(pathItem));
	}

	public void setLibraryPath(final String[] libraryPath)
		throws
			PsyLimitCheckException,
			PsyRangeCheckException,
			PsyUndefinedException
	{
		@SuppressWarnings("unchecked")
		final var oLibraryPath
			=(PsyFormalArray<PsyTextual>)systemDict().get("librarypath");
		final var envLibraryPath=System.getenv("PSYLLA_LIBRARYPATH");
		if(envLibraryPath!=null)
			for(final var pathItem: envLibraryPath.split(File.pathSeparator))
				oLibraryPath.psyAppend(new PsyString(pathItem));
		if(libraryPath!=null)
			for(final var pathItem: libraryPath)
				oLibraryPath.psyAppend(new PsyString(pathItem));
	}

	public void interpret(final Reader reader)
	{
		interpret(new PsyReader(reader));
	}

	public void interpret(final String string)
	{
		interpret(new PsyStringReader(string));
	}

	@Override
	public void interpret(final PsyReader oReader)
	{
		final var initProcLevel=procstack.size();
		final var parser=new Parser(oReader);
		try
		{
			while(running)
			{
				final var token=parser.getNextToken();
				if(token.kind==ParserConstants.EOF)
					break;
				processToken(token);

				// If "stop" is invoked outside an explicit stopping context
				if(getStopped())
					return;
			}
			// Incomplete procedure read
			if(procstack.size()>initProcLevel)
			{
				final var e=new PsySyntaxErrorException();
				e.setEmitter(oReader);
				throw e;
			}

			dstack.<PsyFlushable>load("stdout").psyFlush();
			dstack.<PsyFlushable>load("stderr").psyFlush();
		}
		catch(final PsyErrorException e)
		{
			e.setEmitter(oReader); // IMPORTANT
			e.setStacks(ostack, estack, dstack);
			e.invoke(this);
		}
		catch(final TokenMgrError ex)
		{
			final var e=new PsySyntaxErrorException();
			e.setEmitter(oReader);
			e.setStacks(ostack, estack, dstack);
			e.invoke(this);
		}
	}

	@Override
	public void interpretBraced(final PsyReader oReader)
		throws PsyLimitCheckException
	{
		procstack.push(new PsyProc());
		interpret(oReader);
		if(procstack.size()==0)
		{
			final var e=new PsySyntaxErrorException();
			e.setEmitter(oReader);
			e.setStacks(ostack, estack, dstack);
			e.invoke(this);
		}
		final var proc=procstack.pop();
		if(procstack.size()>0)
			procstack.peek().psyAppend(proc);
		else
			ostack.push(proc);
	}

	private void processToken(final Token token)
		throws
			PsyInvalidRegExpException,
			PsyLimitCheckException,
			PsySyntaxErrorException,
			PsyUndefinedException,
			PsyUndefinedResultException
	{
		if(procstack.size()==0)
		{
			switch(token.kind)
			{
				case ParserConstants.NAME->
					{
						parseToken(token).execute(this);
						handleExecutionStack(0);
					}
				case /*ParserConstants.INTEGRAL*/ ParserConstants.RATIONAL,
						ParserConstants.REAL,
						ParserConstants.STRING,
						ParserConstants.STRINGBUFFER,
						ParserConstants.IMMEDIATE,
						ParserConstants.REGEXP,
						ParserConstants.LITERAL->
					ostack.push(parseToken(token));
				case ParserConstants.OPEN_BRACE->
					procstack.push(new PsyProc());
				case ParserConstants.CLOSE_BRACE->
					throw new PsySyntaxErrorException();
				case ParserConstants.EOF->{}
			}
		}
		else
		{
			switch(token.kind)
			{
				case ParserConstants.OPEN_BRACE->
					procstack.push(new PsyProc());
				case ParserConstants.CLOSE_BRACE->
					{
						final var proc=procstack.pop();
						if(procstack.size()>0)
							procstack.peek().psyAppend(proc);
						else
							ostack.push(proc);
					}
				case /*ParserConstants.INTEGRAL*/ ParserConstants.RATIONAL,
						ParserConstants.REAL,
						ParserConstants.NAME,
						ParserConstants.STRING,
						ParserConstants.STRINGBUFFER,
						ParserConstants.IMMEDIATE,
						ParserConstants.REGEXP,
						ParserConstants.LITERAL
					->procstack.peek().psyAppend(parseToken(token));
				case ParserConstants.EOF->
					throw new PsySyntaxErrorException();
			}
		}
	}

	private PsyObject parseToken(final Token token)
		throws
			PsyInvalidRegExpException,
			PsySyntaxErrorException,
			PsyUndefinedException,
			PsyUndefinedResultException
	{
		final var image=token.image;
		return switch(token.kind)
			{
				case ParserConstants.IMMEDIATE->dstack.load(image.substring(2));
				case ParserConstants.STRING->PsyString.parseLiteral(image);
				//case ParserConstants.INTEGRAL->PsyIntegral.parseLiteral(image);
				case ParserConstants.RATIONAL->PsyRational.parseLiteral(image);
				case ParserConstants.REAL->PsyReal.parseLiteral(image);
				case ParserConstants.STRINGBUFFER->PsyStringBuffer.parseLiteral(image);
				case ParserConstants.REGEXP->PsyRegExp.parseLiteral(image);
				case ParserConstants.NAME->new PsyName(image);
				case ParserConstants.LITERAL->parseLiteralImage(image);
				default->throw new AssertionError();	// TODO more appropriate exception
			};
	}

	private PsyObject parseLiteralImage(final String image)
		throws
			PsyUndefinedException, // TODO
			PsySyntaxErrorException
	{
		final int i=image.indexOf('=');
		final var typeName=image.substring(0, i);
		final var typeClass=TypeResolver.resolve(typeName);
		try
		{
			final var mh=MethodHandles.lookup().findStatic(
					typeClass,
					"parseLiteral",
					MethodType.methodType(typeClass, String.class));
			return typeClass.cast(mh.invoke(image.substring(i+2, image.length()-1)));
		}
		catch(final NoSuchMethodException|IllegalAccessException ex)
		{
			throw new PsyUndefinedException();	// TODO more appropriate exception
		}
		catch(final Throwable ex)
		{
			throw new PsySyntaxErrorException();
		}
	}

	@SuppressWarnings("unchecked")
	public PsyFormalDict<PsyObject> errorDict()
		throws PsyUndefinedException
	{
		return (PsyFormalDict<PsyObject>)systemDict().get("errordict");
		//return PsyNamespace.namespace("errordict");
	}

	/*
	@Override
	public void handleError(final PsyErrorException oException)
	{
		final var errorName=oException.getName();
		final var errorObj=new PsyDict();
		errorObj.put("newerror", PsyBoolean.TRUE);
		errorObj.put("errorname", new PsyString(errorName));
		errorObj.put("emitter", oException.getEmitter());
		errorObj.put("ostack", new PsyArray((ArrayList<PsyObject>)ostack.clone()));
		errorObj.put("estack", new PsyArray((ArrayList<PsyObject>)estack.clone()));
		errorObj.put("dstack", new PsyArray((ArrayList<PsyObject>)dstack.clone()));
		systemDict().put("$error", errorObj);

		try
		{
			final var errorDict=errorDict();
			if(errorDict.known(errorName))
				errorDict.get(errorName).invoke(this);
			else
				stop();
		}
		catch(final PsyErrorException e)
		{
			throw new AssertionError(e);
		}
	}
	*/

	@Override
	public void showStacks()
	{
		System.err.print(Messages.getString("handleErrorMessageOStack"));
		{
			final var sj=new StringJoiner(" ", "\n\t", "");
			sj.setEmptyValue(" "+Messages.getString("handleErrorMessageEmpty"));
			ostack.forEach(o->sj.add(o.toSyntaxString()));
			System.err.println(sj.toString());
		}

		System.err.print(Messages.getString("handleErrorMessageEStack"));
		{
			final var sj=new StringJoiner(" ", "\n\t", "");
			sj.setEmptyValue(" "+Messages.getString("handleErrorMessageEmpty"));
			estack.forEach(o->sj.add(o.toSyntaxString()));
			System.err.println(sj.toString());
		}
	}

	@Override
	public int execLevel()
	{
		return estack.size();
	}

	@Override
	public boolean getStopped()
	{
		return stopped;
	}

	@Override
	public void setStopped(final boolean stopped)
	{
		this.stopped=stopped;
	}

	public void setScriptName(final String scriptName)
	{
		systemDict().put("script", new PsyString(scriptName));
	}

	public void setShellArguments(final String[] args)
		throws PsyLimitCheckException, PsyUndefinedException
	{
		final var oArguments=(PsyArray)systemDict().get("arguments");
		for(final var arg: args)
			oArguments.psyAppend(new PsyString(arg));
	}

	public void setEnvironment(final Map<String, String> env)
	{
		systemDict().put("environment", new PsyEnvironment(env));
	}

	@Override
	public void quit()
	{
		running=stopped=false;
		estack.clear();
	}

	@Override
	public void repl()
		throws PsyIOErrorException
	{
		try
		{
			final var cr=new ConsoleReader();
			cr.printString(banner());
			while(running)
			{
				cr.setDefaultPrompt(prompt());
				final var line=cr.readLine();
				if(line==null)
				{
					cr.printNewline();
					cr.flushConsole();
					return;
				}
				final var oReader=new PsyReader(new StringReader(line));
				final var parser=new Parser(oReader);
				try
				{
					while(running)
					{
						final var token=parser.getNextToken();
						if(token.kind==ParserConstants.EOF)
							break;
						processToken(token);
						// If "stop" invoked outside an explicit stopping context
						if(getStopped())
						{
							setStopped(false);
							break;
						}
					}
				}
				catch(final PsyErrorException e)
				{
					e.setEmitter(oReader);
					e.setStacks(ostack, estack, dstack);
					e.invoke(this);
				}
				catch(final TokenMgrError ex)
				{
					final var e=new PsySyntaxErrorException();
					e.setEmitter(oReader);
					e.setStacks(ostack, estack, dstack);
					e.invoke(this);
				}
			}
		}
		catch(final IOException ex)
		{
			throw new PsyIOErrorException();
		}
	}

	/**
	*	{@return the Psylla banner}
	*/
	protected String banner()
	{
		return String.format(Messages.getString("banner"), Version.getVersion());
	}

	/**
	*	{@return the REPL prompt}
	*/
	public String prompt()
	{
		final var sb=new StringBuilder("PSYLLA");
		sb.append("{".repeat(procstack.size()));
		if(ostack.size()>0)
			sb.append("<"+ostack.size());
		sb.append("> ");
		return sb.toString();
	}

	public void start()
	{
		thread.start();
		//thread=Thread.startVirtualThread(this::run);
	}

	@Override
	public void run()
	{
	}

	@Override
	public void join()
		throws InterruptedException
	{
		thread.join();
	}

	//@Override
	public void join(final long millis)
		throws InterruptedException
	{
		thread.join(millis);
	}

	@Override
	public long getId()
	{
		return thread.threadId();
	}

	@Override
	public void stop()
	{
		setStopped(true);
		estack.exitStop();	// TODO quit()
	}

	public boolean loadLibraryResource(final String resourceName)
		throws
			PsyFileAccessDeniedException,
			PsyFileNotFoundException,
			PsyIOErrorException,
			PsySecurityErrorException,
			PsyUndefinedException,
			PsyErrorException	// TODO
	{
		final var oLibraryPath=dstack.<PsyFormalArray<PsyTextual>>load("librarypath");
		final var filePath=resourceName.replace('.', '/');
		for(final var oPathItem: oLibraryPath)
		{
			final var oFullResourceName
				=new PsyString(oPathItem.stringValue()+'/'+filePath+".psy");
			if(PsyFileSystem.psyFileExists(oFullResourceName).booleanValue()
					&& PsyFileSystem.psyIsFile(oFullResourceName).booleanValue())
			{
				final var resourceID
					="file:"+PsyFileSystem.psyFileAbsolutePath(oFullResourceName).stringValue();
				if(resourceRegistry.containsKey(resourceName))
					// TODO
					System.out.println("Already loaded: "+resourceID);
				else
				{
					resourceRegistry.put(resourceName, resourceID);
					new PsyFileReader(oFullResourceName).psyEval(this);
				}
				return true;
			}
		}
		return false;
	}

	/*
	public void loadAnnotatedOperators(final Class<PsyObject> clazz)
	{
		for(final var method: clazz.getDeclaredMethods())
			if(method.isAnnotationPresent(Operator.class))
			{
				final var oOperator=new PsyOperator.Method(method);
				nspool.get(oOperator.getPrefix()).put(oOperator.getSimpleName(), oOperator);
				//oNamespace.put(method.getDeclaredAnnotation(Operator.class).value(),
				//		PsyOperator.of(method));
			}
		//
		for(final var constructor: clazz.getDeclaredConstructors())
			if(constructor.isAnnotationPresent(Operator.class))
				oNamespace.put(constructor.getDeclaredAnnotation(Operator.class).value(),
						PsyOperator.of(constructor));
		//
	}
	*/

	public boolean loadType(final String typeName)
	{
		// TODO check NPE at readLine
		try
		{
			final var resourceStream=classLoader.getResourceAsStream(
					"META-INF/psylla/type/"+typeName);
			if(resourceStream==null)
				return false;
			final var className=(new BufferedReader(new InputStreamReader(
					resourceStream))).readLine();
			if(className==null)
				return false;
			final var clazz=Class.forName(
				className, true, classLoader).asSubclass(PsyObject.class);
			final var resourceID="class:"+clazz.getName();
			if(resourceRegistry.containsKey(typeName))
			{
				System.out.println("Already loaded: "+resourceID);
				return true;
			}
			else
				resourceRegistry.put(typeName, resourceID);
			//final var oNamespace=PsyNamespace.namespace(clazz.getAnnotation(Type.class).value());
			/*
			for(final var method: clazz.getDeclaredMethods())
			{
				if(method.isAnnotationPresent(Operator.class))
				{
					final var operatorName=method.getDeclaredAnnotation(Operator.class).value();
					oNamespace.put(operatorName, PsyOperator.of(method));
				}
			}
			for(final var constructor: clazz.getDeclaredConstructors())
			{
				if(constructor.isAnnotationPresent(Operator.class))
				{
					final var operatorName=constructor.getDeclaredAnnotation(Operator.class).value();
					oNamespace.put(operatorName, PsyOperator.of(constructor));
				}
			}
			for(final var field: clazz.getDeclaredFields())
			{
				if(field.isAnnotationPresent(Operator.class))
				{
					System.out.println("FIELD");
					final var operatorName=field.getDeclaredAnnotation(Operator.class).value();
					oNamespace.put(operatorName, (PsyOperator)field.get(null));
				}
			}
			*/
			/*
			for(final var field: clazz.getDeclaredFields())
			{
				if(field.isAnnotationPresent(Export.class))
				{
					//System.out.println("FIELD");
					//final var operatorName=field.getDeclaredAnnotation(Operator.class).value();
					//oNamespace.put(operatorName, (PsyOperator)field.get(null));
					final var operators=(PsyOperator[])field.get(null);
					for(final var oOperator: operators)
					{
						//System.out.println(oOperator.getName());
						systemDict().put(oOperator.getName(), oOperator);
					}

				}
			}
			*/
			return true;
		}
		//catch(IOException|ClassNotFoundException|NullPointerException|IllegalAccessException e)
		catch(final IOException|ClassNotFoundException ex)
		{
			return false;
		}
	}

	@Override
	public void psyRequire(final PsyTextual oResourceName)
		throws
			PsyFileAccessDeniedException,
			PsyFileNotFoundException,
			PsyIOErrorException,
			PsySecurityErrorException,
			PsyUndefinedException,
			PsyErrorException	// TODO
	{
		final var resourceName=oResourceName.stringValue();

		if(loadType(resourceName))
			return;
		if(loadLibraryResource(resourceName))
			return;
		throw new PsyUndefinedException(); // TODO: more appropriate exception
	}

}