/** * Copyright (c) 2005-2017, KoLmafia development team * http://kolmafia.sourceforge.net/ * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * [1] Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * [2] Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * [3] Neither the name "KoLmafia" nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package net.sourceforge.kolmafia.textui; import java.io.File; import java.io.InputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Stack; import java.util.TreeMap; import net.sourceforge.kolmafia.KoLConstants.MafiaState; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.StaticEntity; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.request.RelayRequest; import net.sourceforge.kolmafia.request.SendMailRequest; import net.sourceforge.kolmafia.textui.parsetree.Function; import net.sourceforge.kolmafia.textui.parsetree.FunctionList; import net.sourceforge.kolmafia.textui.parsetree.Scope; import net.sourceforge.kolmafia.textui.parsetree.Type; import net.sourceforge.kolmafia.textui.parsetree.Value; import net.sourceforge.kolmafia.textui.parsetree.VariableList; import net.sourceforge.kolmafia.textui.parsetree.VariableReference; import net.sourceforge.kolmafia.utilities.CharacterEntities; import net.sourceforge.kolmafia.utilities.NullStream; public class Interpreter { protected Parser parser; protected Scope scope; // Variables used during execution public static final String STATE_NORMAL = "NORMAL"; public static final String STATE_RETURN = "RETURN"; public static final String STATE_BREAK = "BREAK"; public static final String STATE_CONTINUE = "CONTINUE"; public static final String STATE_EXIT = "EXIT"; private static Stack interpreterStack = new Stack(); private String currentState = Interpreter.STATE_NORMAL; private int traceIndentation = 0; public Profiler profiler; // key, then aggregate, then iterator for every active foreach loop public ArrayList iterators = new ArrayList(); // For use in runtime error messages private String fileName; private int lineNumber; // For use in LibraryFunction return values private boolean hadPendingState; // For use by RuntimeLibrary's CLI command batching feature LinkedHashMap<String, LinkedHashMap<String, StringBuilder>> batched; // For use in ASH relay scripts private RelayRequest relayRequest = null; private StringBuffer serverReplyBuffer = null; // GLOBAL control of tracing private static PrintStream traceStream = NullStream.INSTANCE; public static boolean isTracing() { return Interpreter.traceStream != NullStream.INSTANCE; } public static final void openTraceStream() { Interpreter.traceStream = RequestLogger.openStream( "ASH_" + KoLConstants.DAILY_FORMAT.format( new Date() ) + ".txt", Interpreter.traceStream, true ); } public static final void println( final String string ) { Interpreter.traceStream.println( string ); } public static final void closeTraceStream() { RequestLogger.closeStream( Interpreter.traceStream ); Interpreter.traceStream = NullStream.INSTANCE; } public Interpreter() { this.parser = new Parser(); this.scope = new Scope( new VariableList(), Parser.getExistingFunctionScope() ); this.hadPendingState = false; } private Interpreter( final Interpreter source, final File scriptFile ) { this.parser = new Parser( scriptFile, null, source.getImports() ); this.scope = source.scope; this.hadPendingState = false; } public void initializeRelayScript( final RelayRequest request ) { this.relayRequest = request; if ( this.serverReplyBuffer == null ) { this.serverReplyBuffer = new StringBuffer(); } else { this.serverReplyBuffer.setLength( 0 ); } // Allow a relay script to execute regardless of error state KoLmafia.forceContinue(); } public RelayRequest getRelayRequest() { return this.relayRequest; } public StringBuffer getServerReplyBuffer() { return this.serverReplyBuffer; } public void finishRelayScript() { this.relayRequest = null; this.serverReplyBuffer = null; } public void cloneRelayScript( final Interpreter caller ) { this.finishRelayScript(); if ( caller != null ) { this.relayRequest = caller.getRelayRequest(); this.serverReplyBuffer = caller.getServerReplyBuffer(); } } public Parser getParser() { return this.parser; } public String getFileName() { return this.parser.getFileName(); } public TreeMap getImports() { return this.parser.getImports(); } public FunctionList getFunctions() { return this.scope.getFunctions(); } public String getState() { return this.currentState; } public void setState( final String state ) { this.currentState = state; } public static void rememberPendingState() { if ( Interpreter.interpreterStack.isEmpty() ) { return; } Interpreter current = (Interpreter) Interpreter.interpreterStack.peek(); current.hadPendingState = true; } public static void forgetPendingState() { if ( Interpreter.interpreterStack.isEmpty() ) { return; } Interpreter current = (Interpreter) Interpreter.interpreterStack.peek(); current.hadPendingState = false; } public static boolean getContinueValue() { if ( !KoLmafia.permitsContinue() ) { return false; } if ( Interpreter.interpreterStack.isEmpty() ) { return true; } Interpreter current = (Interpreter) Interpreter.interpreterStack.peek(); return !current.hadPendingState; } public void setLineAndFile( final String fileName, final int lineNumber ) { this.fileName = fileName; this.lineNumber = lineNumber; } private static final String indentation = " " + " " + " "; public static final void indentLine( final PrintStream stream, final int indent ) { if ( stream != null && stream != NullStream.INSTANCE ) { for ( int i = 0; i < indent; ++i ) { stream.print( indentation ); } } } // **************** Parsing and execution ***************** public boolean validate( final File scriptFile, final InputStream stream ) { try { this.parser = new Parser( scriptFile, stream, null ); this.scope = parser.parse(); this.resetTracing(); if ( Interpreter.isTracing() ) { this.printScope( this.scope ); } return true; } catch ( ScriptException e ) { String message = CharacterEntities.escape( e.getMessage() ); KoLmafia.updateDisplay( MafiaState.ERROR, message ); return false; } catch ( Exception e ) { StaticEntity.printStackTrace( e ); return false; } } public Value execute( final String functionName, final Object[] parameters ) { String currentScript = this.getFileName() == null ? "<>" : "<" + this.getFileName() + ">"; String notifyList = Preferences.getString( "previousNotifyList" ); String notifyRecipient = this.parser.getNotifyRecipient(); if ( notifyRecipient != null && notifyList.indexOf( currentScript ) == -1 ) { Preferences.setString( "previousNotifyList", notifyList + currentScript ); SendMailRequest notifier = new SendMailRequest( notifyRecipient, this ); RequestThread.postRequest( notifier ); } return this.execute( functionName, parameters, true ); } public Value execute( final String functionName, final Object[] parameters, final boolean executeTopLevel ) { try { return this.executeScope( this.scope, functionName, parameters, executeTopLevel ); } catch ( ScriptException e ) { KoLmafia.updateDisplay( MafiaState.ERROR, e.getMessage() ); } catch ( StackOverflowError e ) { KoLmafia.updateDisplay( MafiaState.ERROR, "Stack overflow during ASH script: " + Parser.getLineAndFile( this.fileName, this.lineNumber ) ); } catch ( Exception e ) { StaticEntity.printStackTrace( e, "", true ); KoLmafia.updateDisplay( MafiaState.ERROR, "Script execution aborted (" + e.getMessage() + "): " + Parser.getLineAndFile( this.fileName, this.lineNumber ) ); } return DataTypes.VOID_VALUE; } private Value executeScope( final Scope topScope, final String functionName, final Object[] parameters, final boolean executeTopLevel ) { Function main; Value result = null; Interpreter.interpreterStack.push( this ); this.currentState = Interpreter.STATE_NORMAL; this.resetTracing(); if ( functionName.equals( "main" ) ) { main = this.parser.getMainMethod(); } else { main = topScope.findFunction( functionName, parameters != null ); if ( main == null && topScope.getCommandList().isEmpty() ) { KoLmafia.updateDisplay( MafiaState.ERROR, "Unable to invoke " + functionName ); return DataTypes.VOID_VALUE; } } // First execute top-level commands; if ( executeTopLevel ) { if ( Interpreter.isTracing() ) { this.trace( "Executing top-level commands" ); } result = topScope.execute( this ); } if ( this.currentState == Interpreter.STATE_EXIT ) { return result; } // Now execute main function, if any if ( main != null ) { if ( Interpreter.isTracing() ) { this.trace( "Executing main function" ); } Object[] values = new Object[ main.getVariableReferences().size() + 1]; values[ 0 ] = this; if ( !this.requestUserParams( main, parameters, values ) ) { return null; } result = main.execute( this, values ); } Interpreter.interpreterStack.pop(); return result; } private boolean requestUserParams( final Function targetFunction, final Object[] parameters, Object[] values ) { int args = parameters == null ? 0 : parameters.length; Type type = null; int index = 0; for ( VariableReference param : targetFunction.getVariableReferences() ) { type = param.getType(); String name = param.getName(); Value value = null; while ( value == null ) { if ( type == DataTypes.VOID_TYPE ) { value = DataTypes.VOID_VALUE; break; } Object input = ( index >= args ) ? DataTypes.promptForValue( type, name ) : parameters[ index ]; // User declined to supply a parameter if ( input == null ) { return false; } try { value = DataTypes.coerceValue( type, input, false ); } catch ( Exception e ) { value = null; } if ( value == null ) { RequestLogger.printLine( "Bad " + type.toString() + " value: \"" + input + "\"" ); // Punt if parameter came from the CLI if ( index < args ) { return false; } } } values[ ++index ] = value; } if ( index < args && type != null ) { StringBuilder inputs = new StringBuilder(); for ( int i = index - 1; i < args; ++i ) { inputs.append( parameters[ i ] ); inputs.append( " " ); } Value value = DataTypes.parseValue( type, inputs.toString().trim(), true ); values[ index ] = value; } return true; } // **************** Debug printing ***************** private void printScope( final Scope scope ) { if ( scope == null ) { return; } PrintStream stream = this.traceStream; scope.print( stream, 0 ); Function mainMethod = this.parser.getMainMethod(); if ( mainMethod != null ) { this.indentLine( 1 ); stream.println( "<MAIN>" ); mainMethod.print( stream, 2 ); } } // **************** Tracing ***************** public final void resetTracing() { this.traceIndentation = 0; this.traceStream = Interpreter.traceStream; } private final void indentLine( final int indent ) { if ( this.isTracing() ) { Interpreter.indentLine( this.traceStream, indent ); } } public final void traceIndent() { this.traceIndentation++ ; } public final void traceUnindent() { this.traceIndentation-- ; } public final void trace( final String string ) { if ( Interpreter.isTracing() ) { this.indentLine( this.traceIndentation ); this.traceStream.println( string ); } } public final void captureValue( final Value value ) { // We've just executed a command in a context that captures the // return value. if ( KoLmafia.refusesContinue() || value == null ) { // User aborted this.setState( STATE_EXIT ); return; } // Even if an error occurred, since we captured the result, // permit further execution. this.setState( STATE_NORMAL ); KoLmafia.forceContinue(); } public final ScriptException runtimeException( final String message ) { return Interpreter.runtimeException( message, this.fileName, this.lineNumber ); } public static final ScriptException runtimeException( final String message, final String fileName, final int lineNumber ) { return new ScriptException( message + " " + Parser.getLineAndFile( fileName, lineNumber ) ); } public final ScriptException runtimeException2( final String message1, final String message2 ) { return Interpreter.runtimeException2( message1, message2, this.fileName, this.lineNumber ); } public static final ScriptException runtimeException2( final String message1, final String message2, final String fileName, final int lineNumber ) { return new ScriptException( message1 + " " + Parser.getLineAndFile( fileName, lineNumber ) + " " + message2); } public final ScriptException undefinedFunctionException( final String name, final List<Value> params ) { return this.runtimeException( Parser.undefinedFunctionMessage( name, params ) ); } }