/** * 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.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.PrintStream; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.TreeMap; import net.java.dev.spellcast.utilities.DataUtilities; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLmafiaCLI; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.StaticEntity; import net.sourceforge.kolmafia.objectpool.IntegerPool; import net.sourceforge.kolmafia.persistence.EffectDatabase; import net.sourceforge.kolmafia.persistence.ItemDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.textui.parsetree.AggregateType; import net.sourceforge.kolmafia.textui.parsetree.AggregateValue; import net.sourceforge.kolmafia.textui.parsetree.ArrayLiteral; import net.sourceforge.kolmafia.textui.parsetree.MapValue; import net.sourceforge.kolmafia.textui.parsetree.Assignment; import net.sourceforge.kolmafia.textui.parsetree.BasicScope; import net.sourceforge.kolmafia.textui.parsetree.BasicScript; import net.sourceforge.kolmafia.textui.parsetree.CompositeReference; import net.sourceforge.kolmafia.textui.parsetree.Concatenate; import net.sourceforge.kolmafia.textui.parsetree.Conditional; import net.sourceforge.kolmafia.textui.parsetree.Else; import net.sourceforge.kolmafia.textui.parsetree.ElseIf; import net.sourceforge.kolmafia.textui.parsetree.ForEachLoop; import net.sourceforge.kolmafia.textui.parsetree.ForLoop; import net.sourceforge.kolmafia.textui.parsetree.Function; import net.sourceforge.kolmafia.textui.parsetree.FunctionCall; import net.sourceforge.kolmafia.textui.parsetree.FunctionInvocation; import net.sourceforge.kolmafia.textui.parsetree.FunctionList; import net.sourceforge.kolmafia.textui.parsetree.FunctionReturn; import net.sourceforge.kolmafia.textui.parsetree.If; import net.sourceforge.kolmafia.textui.parsetree.IncDec; import net.sourceforge.kolmafia.textui.parsetree.JavaForLoop; import net.sourceforge.kolmafia.textui.parsetree.Loop; import net.sourceforge.kolmafia.textui.parsetree.LoopBreak; import net.sourceforge.kolmafia.textui.parsetree.LoopContinue; import net.sourceforge.kolmafia.textui.parsetree.MapLiteral; import net.sourceforge.kolmafia.textui.parsetree.Operation; import net.sourceforge.kolmafia.textui.parsetree.Operator; import net.sourceforge.kolmafia.textui.parsetree.ParseTreeNode; import net.sourceforge.kolmafia.textui.parsetree.PluralValue; import net.sourceforge.kolmafia.textui.parsetree.RecordType; import net.sourceforge.kolmafia.textui.parsetree.RepeatUntilLoop; import net.sourceforge.kolmafia.textui.parsetree.Scope; import net.sourceforge.kolmafia.textui.parsetree.ScriptExit; import net.sourceforge.kolmafia.textui.parsetree.SortBy; import net.sourceforge.kolmafia.textui.parsetree.StaticScope; import net.sourceforge.kolmafia.textui.parsetree.Switch; import net.sourceforge.kolmafia.textui.parsetree.SwitchScope; import net.sourceforge.kolmafia.textui.parsetree.TernaryExpression; import net.sourceforge.kolmafia.textui.parsetree.Try; import net.sourceforge.kolmafia.textui.parsetree.Type; import net.sourceforge.kolmafia.textui.parsetree.TypeDef; import net.sourceforge.kolmafia.textui.parsetree.UserDefinedFunction; import net.sourceforge.kolmafia.textui.parsetree.Value; import net.sourceforge.kolmafia.textui.parsetree.Variable; import net.sourceforge.kolmafia.textui.parsetree.VariableList; import net.sourceforge.kolmafia.textui.parsetree.VariableReference; import net.sourceforge.kolmafia.textui.parsetree.WhileLoop; import net.sourceforge.kolmafia.utilities.ByteArrayStream; import net.sourceforge.kolmafia.utilities.CharacterEntities; import net.sourceforge.kolmafia.utilities.StringUtilities; public class Parser { public static String APPROX = "\u2248"; public static String PRE_INCREMENT = "++X"; public static String PRE_DECREMENT = "--X"; public static String POST_INCREMENT = "X++"; public static String POST_DECREMENT = "X--"; // Variables used during parsing private String fileName; private String shortFileName; private String scriptName; private InputStream istream; private LineNumberReader commandStream; private String currentLine; private String nextLine; private String currentToken; private int lineNumber; private String fullLine; private TreeMap<File, Long> imports; private Function mainMethod = null; private String notifyRecipient = null; public Parser() { this( null, null, null ); } public Parser( final File scriptFile, final InputStream stream, final TreeMap<File, Long> imports ) { this.imports = ( imports != null ) ? imports : new TreeMap<File, Long>(); if ( scriptFile != null ) { this.fileName = scriptFile.getPath(); this.shortFileName = this.fileName.substring( this.fileName.lastIndexOf( File.separator ) + 1 ); this.istream = DataUtilities.getInputStream( scriptFile ); } else if ( stream != null ) { this.fileName = null; this.shortFileName = null; this.istream = stream; } else { this.fileName = null; this.shortFileName = null; this.istream = null; return; } try { this.commandStream = new LineNumberReader( new InputStreamReader( this.istream, "UTF-8" ) ); this.currentLine = this.getNextLine(); this.lineNumber = this.commandStream.getLineNumber(); this.nextLine = this.getNextLine(); } catch ( Exception e ) { // If any part of the initialization fails, // then throw an exception. throw this.parseException( this.fileName + " could not be accessed" ); } } private void disconnect() { try { this.commandStream = null; this.istream.close(); } catch ( IOException e ) { } } public Scope parse() { Scope scope = null; try { scope = this.parseScope( null, null, null, Parser.getExistingFunctionScope(), false, false ); if ( this.currentLine != null ) { throw this.parseException( "Script parsing error" ); } } finally { this.disconnect(); } return scope; } public String getFileName() { return this.fileName; } public String getShortFileName() { return this.shortFileName; } public String getScriptName() { if ( this.scriptName != null ) return this.scriptName; return this.shortFileName; } public int getLineNumber() { return this.lineNumber; } public TreeMap<File, Long> getImports() { return this.imports; } public Function getMainMethod() { return this.mainMethod; } public String getNotifyRecipient() { return this.notifyRecipient; } public static Scope getExistingFunctionScope() { return new Scope( RuntimeLibrary.functions, null, DataTypes.simpleTypes ); } // **************** Parser ***************** private static final HashSet<String> multiCharTokens = new HashSet<String>(); private static final HashSet<String> reservedWords = new HashSet<String>(); static { // Tokens multiCharTokens.add( "==" ); multiCharTokens.add( "!=" ); multiCharTokens.add( "<=" ); multiCharTokens.add( ">=" ); multiCharTokens.add( "||" ); multiCharTokens.add( "&&" ); multiCharTokens.add( "//" ); multiCharTokens.add( "/*" ); multiCharTokens.add( "<<" ); multiCharTokens.add( ">>" ); multiCharTokens.add( ">>>" ); multiCharTokens.add( "++" ); multiCharTokens.add( "--" ); multiCharTokens.add( "**" ); multiCharTokens.add( "+=" ); multiCharTokens.add( "-=" ); multiCharTokens.add( "*=" ); multiCharTokens.add( "/=" ); multiCharTokens.add( "%=" ); multiCharTokens.add( "**=" ); multiCharTokens.add( "&=" ); multiCharTokens.add( "^=" ); multiCharTokens.add( "|=" ); multiCharTokens.add( "<<=" ); multiCharTokens.add( ">>=" ); multiCharTokens.add( ">>>=" ); // Constants reservedWords.add( "true" ); reservedWords.add( "false" ); // Operators reservedWords.add( "contains" ); reservedWords.add( "remove" ); reservedWords.add( "new" ); // Control flow reservedWords.add( "if" ); reservedWords.add( "else" ); reservedWords.add( "foreach" ); reservedWords.add( "in" ); reservedWords.add( "for" ); reservedWords.add( "from" ); reservedWords.add( "upto" ); reservedWords.add( "downto" ); reservedWords.add( "by" ); reservedWords.add( "while" ); reservedWords.add( "repeat" ); reservedWords.add( "until" ); reservedWords.add( "break" ); reservedWords.add( "continue" ); reservedWords.add( "return" ); reservedWords.add( "exit" ); reservedWords.add( "switch" ); reservedWords.add( "case" ); reservedWords.add( "default" ); reservedWords.add( "try" ); reservedWords.add( "finally" ); reservedWords.add( "static" ); // Data types reservedWords.add( "void" ); reservedWords.add( "boolean" ); reservedWords.add( "int" ); reservedWords.add( "float" ); reservedWords.add( "string" ); reservedWords.add( "buffer" ); reservedWords.add( "matcher" ); reservedWords.add( "aggregate" ); reservedWords.add( "item" ); reservedWords.add( "location" ); reservedWords.add( "class" ); reservedWords.add( "stat" ); reservedWords.add( "skill" ); reservedWords.add( "effect" ); reservedWords.add( "familiar" ); reservedWords.add( "slot" ); reservedWords.add( "monster" ); reservedWords.add( "element" ); reservedWords.add( "coinmaster" ); reservedWords.add( "record" ); reservedWords.add( "typedef" ); } private static final boolean isReservedWord( final String name ) { return Parser.reservedWords.contains( name.toLowerCase() ); } public Scope importFile( final String fileName, final Scope scope ) { List<File> matches = KoLmafiaCLI.findScriptFile( fileName ); if ( matches.size() > 1 ) { String s = ""; for ( File f : matches ) { if ( !s.equals( "" ) ) s += "; "; s += f.getPath(); } throw this.parseException( "too many matches for " + fileName + ": " + s ); } if ( matches.size() == 0 ) { throw this.parseException( fileName + " could not be found" ); } File scriptFile = matches.get( 0 ); if ( this.imports.containsKey( scriptFile ) ) { return scope; } Scope result = scope; Parser parser = null; try { parser = new Parser( scriptFile, null, this.imports ); result = parser.parseScope( scope, null, null, scope.getParentScope(), false, false ); if ( parser.currentLine != null ) { throw this.parseException( "Script parsing error" ); } } finally { if ( parser != null ) { parser.disconnect(); } } this.imports.put( scriptFile, new Long( scriptFile.lastModified() ) ); if ( parser.mainMethod != null ) { // Make imported script's main() available under a different name UserDefinedFunction f = new UserDefinedFunction( parser.mainMethod.getName() + "@" + parser.getScriptName().replace( ".ash", "" ) .replaceAll( "[^a-zA-Z0-9]", "_" ), parser.mainMethod.getType(), parser.mainMethod.getVariableReferences() ); f.setScope( ((UserDefinedFunction)parser.mainMethod).getScope() ); result.addFunction( f ); } return result; } private Scope parseCommandOrDeclaration( final Scope result, final Type expectedType ) { Type t = this.parseType( result, true, true ); // If there is no data type, it's a command of some sort if ( t == null ) { ParseTreeNode c = this.parseCommand( expectedType, result, false, false, false ); if ( c == null ) { throw this.parseException( "command or declaration required" ); } result.addCommand( c, this ); return result; } if ( this.parseVariables( t, result ) ) { if ( !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } this.readToken(); //read ; return result; } //Found a type but no function or variable to tie it to throw this.parseException( "Type given but not used to declare anything" ); } private Scope parseScope( final Scope startScope, final Type expectedType, final VariableList variables, final BasicScope parentScope, final boolean allowBreak, final boolean allowContinue ) { Scope result = startScope == null ? new Scope( variables, parentScope ) : startScope; return this.parseScope( result, expectedType, parentScope, allowBreak, allowContinue ); } private Scope parseScope( Scope result, final Type expectedType, final BasicScope parentScope, final boolean allowBreak, final boolean allowContinue ) { String importString; this.parseScriptName(); this.parseNotify(); this.parseSince(); while ( ( importString = this.parseImport() ) != null ) { result = this.importFile( importString, result ); } while ( true ) { if ( this.parseTypedef( result ) ) { if ( !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } this.readToken(); //read ; continue; } Type t = this.parseType( result, true, true ); // If there is no data type, it's a command of some sort if ( t == null ) { // See if it's a regular command ParseTreeNode c = this.parseCommand( expectedType, result, false, allowBreak, allowContinue ); if ( c != null ) { result.addCommand( c, this ); continue; } // No type and no command -> done. break; } // If this is a new record definition, enter it if ( t.getType() == DataTypes.TYPE_RECORD && this.currentToken() != null && this.currentToken().equals( ";" ) ) { this.readToken(); // read ; continue; } Function f = this.parseFunction( t, result ); if ( f != null ) { if ( f.getName().equalsIgnoreCase( "main" ) ) { if ( parentScope.getParentScope() != null ) { throw this.parseException( "main method must appear at top level" ); } this.mainMethod = f; } continue; } if ( this.parseVariables( t, result ) ) { if ( !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } this.readToken(); //read ; continue; } if ( (t.getBaseType() instanceof AggregateType) && "{".equals( this.currentToken() ) ) { this.readToken(); // read { result.addCommand( this.parseAggregateLiteral( result, (AggregateType) t ), this ); } else { //Found a type but no function or variable to tie it to throw this.parseException( "Type given but not used to declare anything" ); } } return result; } private Type parseRecord( final BasicScope parentScope ) { if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( "record" ) ) { return null; } this.readToken(); // read record if ( this.currentToken() == null ) { throw this.parseException( "Record name expected" ); } // Allow anonymous records String recordName = null; if ( !"{".equals( this.currentToken() ) ) { // Named record recordName = this.currentToken(); if ( !this.parseIdentifier( recordName ) ) { throw this.parseException( "Invalid record name '" + recordName + "'" ); } if ( Parser.isReservedWord( recordName ) ) { throw this.parseException( "Reserved word '" + recordName + "' cannot be a record name" ); } if ( parentScope.findType( recordName ) != null ) { throw this.parseException( "Record name '" + recordName + "' is already defined" ); } this.readToken(); // read name } if ( this.currentToken() == null || !this.currentToken().equals( "{" ) ) { throw this.parseException( "{", this.currentToken() ); } this.readToken(); // read { // Loop collecting fields List<Type> fieldTypes = new ArrayList<Type>(); List<String> fieldNames = new ArrayList<String>(); while ( true ) { // Get the field type Type fieldType = this.parseType( parentScope, true, true ); if ( fieldType == null ) { throw this.parseException( "Type name expected" ); } // Get the field name String fieldName = this.currentToken(); if ( fieldName == null ) { throw this.parseException( "Field name expected" ); } if ( !this.parseIdentifier( fieldName ) ) { throw this.parseException( "Invalid field name '" + fieldName + "'" ); } if ( Parser.isReservedWord( fieldName ) ) { throw this.parseException( "Reserved word '" + fieldName + "' cannot be used as a field name" ); } if ( fieldNames.contains( fieldName ) ) { throw this.parseException( "Field name '" + fieldName + "' is already defined" ); } this.readToken(); // read name if ( this.currentToken() == null || !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } this.readToken(); // read ; fieldTypes.add( fieldType ); fieldNames.add( fieldName.toLowerCase() ); if ( this.currentToken() == null ) { throw this.parseException( "}", "EOF" ); } if ( this.currentToken().equals( "}" ) ) { break; } } this.readToken(); // read } String[] fieldNameArray = new String[ fieldNames.size() ]; Type[] fieldTypeArray = new Type[ fieldTypes.size() ]; fieldNames.toArray( fieldNameArray ); fieldTypes.toArray( fieldTypeArray ); RecordType rec = new RecordType( recordName != null ? recordName : ( "(anonymous record " + Integer.toHexString( fieldNameArray.hashCode() ) + ")" ), fieldNameArray, fieldTypeArray ); if ( recordName != null ) { // Enter into type table parentScope.addType( rec ); } return rec; } private Function parseFunction( final Type functionType, final Scope parentScope ) { if ( !this.parseIdentifier( this.currentToken() ) ) { return null; } if ( this.nextToken() == null || !this.nextToken().equals( "(" ) ) { return null; } String functionName = this.currentToken(); if ( Parser.isReservedWord( functionName ) ) { throw this.parseException( "Reserved word '" + functionName + "' cannot be used as a function name" ); } this.readToken(); //read Function name this.readToken(); //read ( VariableList paramList = new VariableList(); List<VariableReference> variableReferences = new ArrayList<VariableReference>(); while ( !this.currentToken().equals( ")" ) ) { Type paramType = this.parseType( parentScope, true, false ); if ( paramType == null ) { throw this.parseException( ")", this.currentToken() ); } Variable param = this.parseVariable( paramType, null ); if ( param == null ) { throw this.parseException( "identifier", this.currentToken() ); } if ( !paramList.add( param ) ) { throw this.parseException( "Variable " + param.getName() + " is already defined" ); } if ( !this.currentToken().equals( ")" ) ) { if ( !this.currentToken().equals( "," ) ) { throw this.parseException( ")", this.currentToken() ); } this.readToken(); //read comma } variableReferences.add( new VariableReference( param ) ); } this.readToken(); //read ) // Add the function to the parent scope before we parse the // function scope to allow recursion. UserDefinedFunction f = new UserDefinedFunction( functionName, functionType, variableReferences ); if ( f.overridesLibraryFunction() ) { throw this.overridesLibraryFunctionException( functionName, variableReferences ); } UserDefinedFunction existing = parentScope.findFunction( f ); if ( existing != null && existing.getScope() != null ) { throw this.multiplyDefinedFunctionException( functionName, variableReferences ); } // Add new function or replace existing forward reference UserDefinedFunction result = parentScope.replaceFunction( existing, f ); if ( this.currentToken() != null && this.currentToken().equals( ";" ) ) { // Return forward reference this.readToken(); // ; return result; } Scope scope = this.parseBlockOrSingleCommand( functionType, paramList, parentScope, false, false, false ); result.setScope( scope ); if ( !result.assertBarrier() && !functionType.equals( DataTypes.TYPE_VOID ) ) { throw this.parseException( "Missing return value" ); } return result; } private boolean parseVariables( final Type t, final BasicScope parentScope ) { while ( true ) { Variable v = this.parseVariable( t, parentScope ); if ( v == null ) { return false; } if ( this.currentToken().equals( "," ) ) { this.readToken(); //read , continue; } return true; } } private Variable parseVariable( final Type t, final BasicScope scope ) { if ( !this.parseIdentifier( this.currentToken() ) ) { return null; } String variableName = this.currentToken(); if ( Parser.isReservedWord( variableName ) ) { throw this.parseException( "Reserved word '" + variableName + "' cannot be a variable name" ); } if ( scope != null && scope.findVariable( variableName ) != null ) { throw this.parseException( "Variable " + variableName + " is already defined" ); } Variable result = new Variable( variableName, t ); this.readToken(); // If parsing of Identifier succeeded, go to next token. // If we are parsing a parameter declaration, we are done if ( scope == null ) { if ( this.currentToken().equals( "=" ) ) { throw this.parseException( "Cannot initialize parameter " + variableName ); } return result; } // Otherwise, we must initialize the variable. Value rhs; Type ltype = t.getBaseType(); if ( this.currentToken().equals( "=" ) ) { this.readToken(); // Eat the equals sign if ( this.currentToken().equals( "{" ) && ltype instanceof AggregateType ) { this.readToken(); // read { rhs = this.parseAggregateLiteral( scope, (AggregateType) ltype ); } else { rhs = this.parseExpression( scope ); } if ( rhs == null ) { throw this.parseException( "Expression expected" ); } if ( !Parser.validCoercion( ltype, rhs.getType(), "assign" ) ) { throw this.parseException( "Cannot store " + rhs.getType() + " in " + variableName + " of type " + ltype ); } } else if ( this.currentToken().equals( "{" ) && ltype instanceof AggregateType ) { this.readToken(); // read { rhs = this.parseAggregateLiteral( scope, (AggregateType) ltype ); } else { rhs = null; } scope.addVariable( result ); VariableReference lhs = new VariableReference( variableName, scope ); scope.addCommand( new Assignment( lhs, rhs ), this ); return result; } private boolean parseTypedef( final Scope parentScope ) { if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( "typedef" ) ) { return false; } this.readToken(); // read typedef Type t = this.parseType( parentScope, true, true ); if ( t == null ) { throw this.parseException( "Missing data type for typedef" ); } String typeName = this.currentToken(); if ( !this.parseIdentifier( typeName ) ) { throw this.parseException( "Invalid type name '" + typeName + "'" ); } if ( Parser.isReservedWord( typeName ) ) { throw this.parseException( "Reserved word '" + typeName + "' cannot be a type name" ); } if ( parentScope.findType( typeName ) != null ) { throw this.parseException( "Type name '" + typeName + "' is already defined" ); } this.readToken(); // read name // Add the type to the type table TypeDef type = new TypeDef( typeName, t ); parentScope.addType( type ); return true; } private ParseTreeNode parseCommand( final Type functionType, final BasicScope scope, final boolean noElse, boolean allowBreak, boolean allowContinue ) { ParseTreeNode result; if ( this.currentToken() == null ) { return null; } if ( this.currentToken().equalsIgnoreCase( "break" ) ) { if ( !allowBreak ) { throw this.parseException( "Encountered 'break' outside of loop" ); } result = new LoopBreak(); this.readToken(); //break } else if ( this.currentToken().equalsIgnoreCase( "continue" ) ) { if ( !allowContinue ) { throw this.parseException( "Encountered 'continue' outside of loop" ); } result = new LoopContinue(); this.readToken(); //continue } else if ( this.currentToken().equalsIgnoreCase( "exit" ) ) { result = new ScriptExit(); this.readToken(); //exit } else if ( ( result = this.parseReturn( functionType, scope ) ) != null ) { ; } else if ( ( result = this.parseBasicScript() ) != null ) { // basic_script doesn't have a ; token return result; } else if ( ( result = this.parseWhile( functionType, scope ) ) != null ) { // while doesn't have a ; token return result; } else if ( ( result = this.parseForeach( functionType, scope ) ) != null ) { // foreach doesn't have a ; token return result; } else if ( ( result = this.parseJavaFor( functionType, scope ) ) != null ) { // for doesn't have a ; token return result; } else if ( ( result = this.parseFor( functionType, scope ) ) != null ) { // for doesn't have a ; token return result; } else if ( ( result = this.parseRepeat( functionType, scope ) ) != null ) { ; } else if ( ( result = this.parseSwitch( functionType, scope, allowContinue ) ) != null ) { // switch doesn't have a ; token return result; } else if ( ( result = this.parseConditional( functionType, scope, noElse, allowBreak, allowContinue ) ) != null ) { // loop doesn't have a ; token return result; } else if ( ( result = this.parseTry( functionType, scope, allowBreak, allowContinue ) ) != null ) { // try doesn't have a ; token return result; } else if ( ( result = this.parseStatic( functionType, scope ) ) != null ) { // try doesn't have a ; token return result; } else if ( ( result = this.parseSort( scope ) ) != null ) { ; } else if ( ( result = this.parseRemove( scope ) ) != null ) { ; } else if ( ( result = this.parseBlock( functionType, null, scope, noElse, allowBreak, allowContinue ) ) != null ) { // {} doesn't have a ; token return result; } else if ( ( result = this.parseValue( scope ) ) != null ) { ; } else { return null; } if ( this.currentToken() == null || !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } this.readToken(); // ; return result; } private Type parseType( final BasicScope scope, final boolean aggregates, final boolean records ) { if ( this.currentToken() == null ) { return null; } Type valType = scope.findType( this.currentToken() ); if ( valType == null ) { if ( records && this.currentToken().equalsIgnoreCase( "record" ) ) { valType = this.parseRecord( scope ); if ( valType == null ) { return null; } if ( aggregates && this.currentToken().equals( "[" ) ) { return this.parseAggregateType( valType, scope ); } return valType; } return null; } this.readToken(); if ( aggregates && this.currentToken().equals( "[" ) ) { return this.parseAggregateType( valType, scope ); } return valType; } private Value parseAggregateLiteral( final BasicScope scope, final AggregateType aggr ) { Type index = aggr.getIndexType(); Type data = aggr.getDataType(); List<Value> keys = new ArrayList<Value>(); List<Value> values = new ArrayList<Value>(); // If index type is an int, it could be an array or a map boolean arrayAllowed = index.equals( DataTypes.INT_TYPE ); // Assume it is a map. boolean isArray = false; while ( this.currentToken() != null && !this.currentToken().equals( "}" ) ) { Value lhs; // If we know we are reading an ArrayLiteral or haven't // yet ensured we are reading a MapLiteral, allow any // type of Value as the "key" Type dataType = data.getBaseType(); if ( ( isArray || arrayAllowed ) && this.currentToken().equals( "{" ) && dataType instanceof AggregateType ) { this.readToken(); // read { lhs = parseAggregateLiteral( scope, (AggregateType) dataType ); } else { lhs = this.parseValue( scope ); } if ( lhs == null || this.currentToken() == null ) { throw this.parseException( "Script parsing error" ); } String delim = this.currentToken(); // If this could be an array and we haven't already // decided it is one, if the delimiter is a comma, // parse as an ArrayLiteral if ( arrayAllowed ) { if ( delim.equals( "," ) || delim.equals( "}" ) ) { isArray = true; } arrayAllowed = false; } // If parsing an ArrayLiteral, accumulate only values if ( isArray ) { // The value must have the correct data type if ( !Parser.validCoercion( dataType, lhs.getType(), "assign" ) ) { throw this.parseException( "Invalid array literal" ); } values.add( lhs ); // If there is not another value, done parsing values if ( !delim.equals( "," ) ) { break; } // Otherwise, move on to the next value this.readToken(); // read ; continue; } // We are parsing a MapLiteral if ( !delim.equals( ":" ) ) { throw this.parseException( ":", this.currentToken() ); } this.readToken(); // read : Value rhs; if ( this.currentToken().equals( "{" ) && dataType instanceof AggregateType ) { this.readToken(); // read { rhs = parseAggregateLiteral( scope, (AggregateType) dataType ); } else { rhs = this.parseValue( scope ); } if ( rhs == null ) { throw this.parseException( "Script parsing error" ); } // Check that each type is valid via validCoercion if ( !Parser.validCoercion( index, lhs.getType(), "assign" ) || !Parser.validCoercion( data, rhs.getType(), "assign" ) ) { throw this.parseException( "Invalid map literal" ); } keys.add( lhs ); values.add( rhs ); if ( !this.currentToken().equals(",") ) { break; } this.readToken(); } if ( !this.currentToken().equals( "}" ) ) { throw this.parseException( "}", this.currentToken() ); } this.readToken(); // "}" if ( isArray ) { int size = aggr.getSize (); if ( size > 0 && size < values.size() ) { throw this.parseException( "Array has " + size + " elements but " + values.size() + " initializers." ); } } return isArray ? new ArrayLiteral( aggr, values ) : new MapLiteral( aggr, keys, values ); } private Type parseAggregateType( final Type dataType, final BasicScope scope ) { this.readToken(); // [ or , if ( this.currentToken() == null ) { throw this.parseException( "Missing index token" ); } if ( this.currentToken().equals( "]" ) ) { this.readToken(); // ] if ( this.currentToken().equals( "[" ) ) { return new AggregateType( this.parseAggregateType( dataType, scope ), 0 ); } return new AggregateType( dataType, 0 ); } if ( this.readIntegerToken( this.currentToken() ) ) { int size = StringUtilities.parseInt( this.currentToken() ); this.readToken(); // integer if ( this.currentToken() == null ) { throw this.parseException( "]", this.currentToken() ); } if ( this.currentToken().equals( "]" ) ) { this.readToken(); // ] if ( this.currentToken().equals( "[" ) ) { return new AggregateType( this.parseAggregateType( dataType, scope ), size ); } return new AggregateType( dataType, size ); } if ( this.currentToken().equals( "," ) ) { return new AggregateType( this.parseAggregateType( dataType, scope ), size ); } throw this.parseException( "]", this.currentToken() ); } Type indexType = scope.findType( this.currentToken() ); if ( indexType == null ) { throw this.parseException( "Invalid type name '" + this.currentToken() + "'" ); } if ( !indexType.isPrimitive() ) { throw this.parseException( "Index type '" + this.currentToken() + "' is not a primitive type" ); } this.readToken(); // type name if ( this.currentToken() == null ) { throw this.parseException( "]", this.currentToken() ); } if ( this.currentToken().equals( "]" ) ) { this.readToken(); // ] if ( this.currentToken().equals( "[" ) ) { return new AggregateType( this.parseAggregateType( dataType, scope ), indexType ); } return new AggregateType( dataType, indexType ); } if ( this.currentToken().equals( "," ) ) { return new AggregateType( this.parseAggregateType( dataType, scope ), indexType ); } throw this.parseException( ", or ]", this.currentToken() ); } private boolean parseIdentifier( final String identifier ) { if ( !Character.isLetter( identifier.charAt( 0 ) ) && identifier.charAt( 0 ) != '_' ) { return false; } for ( int i = 1; i < identifier.length(); ++i ) { if ( !Character.isLetterOrDigit( identifier.charAt( i ) ) && identifier.charAt( i ) != '_' ) { return false; } } return true; } private boolean parseScopedIdentifier( final String identifier ) { if ( !Character.isLetter( identifier.charAt( 0 ) ) && identifier.charAt( 0 ) != '_' ) { return false; } for ( int i = 1; i < identifier.length(); ++i ) { if ( !Character.isLetterOrDigit( identifier.charAt( i ) ) && identifier.charAt( i ) != '_' && identifier.charAt( i ) != '@' ) { return false; } } return true; } private FunctionReturn parseReturn( final Type expectedType, final BasicScope parentScope ) { if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( "return" ) ) { return null; } this.readToken(); //return if ( this.currentToken() != null && this.currentToken().equals( ";" ) ) { if ( expectedType != null && expectedType.equals( DataTypes.TYPE_VOID ) ) { return new FunctionReturn( null, DataTypes.VOID_TYPE ); } throw this.parseException( "Return needs " + expectedType + " value" ); } else { if ( expectedType != null && expectedType.equals( DataTypes.TYPE_VOID ) ) { throw this.parseException( "Cannot return a value from a void function" ); } Value value = this.parseExpression( parentScope ); if ( value == null ) { throw this.parseException( "Expression expected" ); } if ( expectedType != null && !Parser.validCoercion( expectedType, value.getType(), "return" ) ) { throw this.parseException( "Cannot return " + value.getType() + " value from " + expectedType + " function"); } return new FunctionReturn( value, expectedType ); } } private Scope parseSingleCommandScope( final Type functionType, final BasicScope parentScope, final boolean noElse, boolean allowBreak, boolean allowContinue ) { ParseTreeNode command = this.parseCommand( functionType, parentScope, noElse, allowBreak, allowContinue ); if ( command == null ) { if ( this.currentToken() == null || !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } this.readToken(); // ; return new Scope( parentScope ); } return new Scope( command, parentScope ); } private Scope parseBlockOrSingleCommand( final Type functionType, final VariableList variables, final BasicScope parentScope, final boolean noElse, boolean allowBreak, boolean allowContinue ) { Scope scope = this.parseBlock( functionType, variables, parentScope, noElse, allowBreak, allowContinue ); if ( scope != null ) { return scope; } return this.parseSingleCommandScope( functionType, parentScope, noElse, allowBreak, allowContinue ); } private Scope parseBlock( final Type functionType, final VariableList variables, final BasicScope parentScope, final boolean noElse, final boolean allowBreak, final boolean allowContinue ) { if ( this.currentToken() == null || !this.currentToken().equals( "{" ) ) { return null; } this.readToken(); // { Scope scope = this.parseScope( null, functionType, variables, parentScope, allowBreak, allowContinue ); if ( this.currentToken() == null || !this.currentToken().equals( "}" ) ) { throw this.parseException( "}", this.currentToken() ); } this.readToken(); //read } return scope; } private Conditional parseConditional( final Type functionType, final BasicScope parentScope, boolean noElse, final boolean allowBreak, final boolean allowContinue ) { if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( "if" ) ) { return null; } if ( this.nextToken() == null || !this.nextToken().equals( "(" ) ) { throw this.parseException( "(", this.nextToken() ); } this.readToken(); // if this.readToken(); // ( Value condition = this.parseExpression( parentScope ); if ( this.currentToken() == null || !this.currentToken().equals( ")" ) ) { throw this.parseException( ")", this.currentToken() ); } if ( condition == null || condition.getType() != DataTypes.BOOLEAN_TYPE ) { throw this.parseException( "\"if\" requires a boolean conditional expression" ); } this.readToken(); // ) If result = null; boolean elseFound = false; boolean finalElse = false; do { Scope scope = parseBlockOrSingleCommand( functionType, null, parentScope, !elseFound, allowBreak, allowContinue ); if ( result == null ) { result = new If( scope, condition ); } else if ( finalElse ) { result.addElseLoop( new Else( scope, condition ) ); } else { result.addElseLoop( new ElseIf( scope, condition ) ); } if ( !noElse && this.currentToken() != null && this.currentToken().equalsIgnoreCase( "else" ) ) { if ( finalElse ) { throw this.parseException( "Else without if" ); } if ( this.nextToken() != null && this.nextToken().equalsIgnoreCase( "if" ) ) { this.readToken(); //else this.readToken(); //if if ( this.currentToken() == null || !this.currentToken().equals( "(" ) ) { throw this.parseException( "(", this.currentToken() ); } this.readToken(); //( condition = this.parseExpression( parentScope ); if ( this.currentToken() == null || !this.currentToken().equals( ")" ) ) { throw this.parseException( ")", this.currentToken() ); } if ( condition == null || condition.getType() != DataTypes.BOOLEAN_TYPE ) { throw this.parseException( "\"if\" requires a boolean conditional expression" ); } this.readToken(); // ) } else //else without condition { this.readToken(); //else condition = DataTypes.TRUE_VALUE; finalElse = true; } elseFound = true; continue; } elseFound = false; } while ( elseFound ); return result; } private BasicScript parseBasicScript() { if ( this.currentToken() == null ) { return null; } if ( !this.currentToken().equalsIgnoreCase( "cli_execute" ) ) { return null; } if ( this.nextToken() == null || !this.nextToken().equals( "{" ) ) { return null; } this.readToken(); // while this.readToken(); // { ByteArrayStream ostream = new ByteArrayStream(); while ( this.currentToken() != null && !this.currentToken().equals( "}" ) ) { try { ostream.write( this.currentLine.getBytes() ); ostream.write( KoLConstants.LINE_BREAK.getBytes() ); } catch ( Exception e ) { // Byte array output streams do not throw errors, // other than out of memory errors. StaticEntity.printStackTrace( e ); } this.currentLine = ""; this.fixLines(); } if ( this.currentToken() == null ) { throw this.parseException( "}", this.currentToken() ); } this.readToken(); // } return new BasicScript( ostream ); } private Loop parseWhile( final Type functionType, final BasicScope parentScope ) { if ( this.currentToken() == null ) { return null; } if ( !this.currentToken().equalsIgnoreCase( "while" ) ) { return null; } if ( this.nextToken() == null || !this.nextToken().equals( "(" ) ) { throw this.parseException( "(", this.nextToken() ); } this.readToken(); // while this.readToken(); // ( Value condition = this.parseExpression( parentScope ); if ( this.currentToken() == null || !this.currentToken().equals( ")" ) ) { throw this.parseException( ")", this.currentToken() ); } if ( condition == null || condition.getType() != DataTypes.BOOLEAN_TYPE ) { throw this.parseException( "\"while\" requires a boolean conditional expression" ); } this.readToken(); // ) Scope scope = this.parseLoopScope( functionType, null, parentScope ); return new WhileLoop( scope, condition ); } private Loop parseRepeat( final Type functionType, final BasicScope parentScope ) { if ( this.currentToken() == null ) { return null; } if ( !this.currentToken().equalsIgnoreCase( "repeat" ) ) { return null; } this.readToken(); // repeat Scope scope = this.parseLoopScope( functionType, null, parentScope ); if ( this.currentToken() == null || !this.currentToken().equals( "until" ) ) { throw this.parseException( "until", this.currentToken() ); } if ( this.nextToken() == null || !this.nextToken().equals( "(" ) ) { throw this.parseException( "(", this.nextToken() ); } this.readToken(); // until this.readToken(); // ( Value condition = this.parseExpression( parentScope ); if ( this.currentToken() == null || !this.currentToken().equals( ")" ) ) { throw this.parseException( ")", this.currentToken() ); } if ( condition == null || condition.getType() != DataTypes.BOOLEAN_TYPE ) { throw this.parseException( "\"repeat\" requires a boolean conditional expression" ); } this.readToken(); // ) return new RepeatUntilLoop( scope, condition ); } private Switch parseSwitch( final Type functionType, final BasicScope parentScope, final boolean allowContinue ) { if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( "switch" ) ) { return null; } if ( this.nextToken() == null || ( !this.nextToken().equals( "(" ) && !this.nextToken().equals( "{" ) ) ) { throw this.parseException( "( or {", this.nextToken() ); } this.readToken(); // switch Value condition = DataTypes.TRUE_VALUE; if ( this.currentToken().equals( "(" ) ) { this.readToken(); // ( condition = this.parseExpression( parentScope ); if ( this.currentToken() == null || !this.currentToken().equals( ")" ) ) { throw this.parseException( ")", this.currentToken() ); } this.readToken(); // ) if ( condition == null ) { throw this.parseException( "\"switch ()\" requires an expression" ); } } Type type = condition.getType(); if ( this.currentToken() == null || !this.currentToken().equals( "{" ) ) { throw this.parseException( "{", this.currentToken() ); } this.readToken(); // { List<Value> tests = new ArrayList<Value>(); List<Integer> indices = new ArrayList<Integer>(); int defaultIndex = -1; SwitchScope scope = new SwitchScope( parentScope ); int currentIndex = 0; Integer currentInteger = null; TreeMap<Value, Integer> labels = new TreeMap<Value, Integer>(); boolean constantLabels = true; while ( true ) { if ( this.currentToken().equals( "case" ) ) { this.readToken(); // case Value test = this.parseExpression( parentScope ); if ( this.currentToken() == null || !this.currentToken().equals( ":" ) ) { throw this.parseException( ":", this.currentToken() ); } if ( !test.getType().equals( type ) ) { throw this.parseException( "Switch conditional has type " + type + " but label expression has type " + test.getType() ); } this.readToken(); // : if ( currentInteger == null ) { currentInteger = IntegerPool.get( currentIndex ); } if ( test.getClass() == Value.class ) { if ( labels.get( test ) != null ) { throw this.parseException( "Duplicate case label: " + test ); } labels.put( test, currentInteger ); } else { constantLabels = false; } tests.add( test ); indices.add( currentInteger ); scope.resetBarrier(); continue; } if ( this.currentToken().equals( "default" ) ) { this.readToken(); // default if ( this.currentToken() == null || !this.currentToken().equals( ":" ) ) { throw this.parseException( ":", this.currentToken() ); } if ( defaultIndex != -1 ) { throw this.parseException( "Only one default label allowed in a switch statement" ); } this.readToken(); // : defaultIndex = currentIndex; scope.resetBarrier(); continue; } Type t = this.parseType( scope, true, true ); // If there is no data type, it's a command of some sort if ( t == null ) { // See if it's a regular command ParseTreeNode c = this.parseCommand( functionType, scope, false, true, allowContinue ); if ( c != null ) { scope.addCommand( c, this ); currentIndex = scope.commandCount(); currentInteger = null; continue; } // No type and no command -> done. break; } if ( this.parseVariables( t, scope ) ) { if ( !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } this.readToken(); //read ; currentIndex = scope.commandCount(); currentInteger = null; continue; } //Found a type but no function or variable to tie it to throw this.parseException( "Type given but not used to declare anything" ); } if ( this.currentToken() == null || !this.currentToken().equals( "}" ) ) { throw this.parseException( "}", this.currentToken() ); } this.readToken(); // } return new Switch( condition, tests, indices, defaultIndex, scope, constantLabels ? labels : null ); } private Try parseTry( final Type functionType, final BasicScope parentScope, final boolean allowBreak, final boolean allowContinue ) { if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( "try" ) ) { return null; } this.readToken(); // try Scope body = this.parseBlockOrSingleCommand( functionType, null, parentScope, false, allowBreak, allowContinue ); // catch clauses would be parsed here if ( this.currentToken() == null || !this.currentToken().equals( "finally" ) ) { // this would not be an error if at least one catch was present throw this.parseException( "\"try\" without \"finally\" is pointless" ); } this.readToken(); // finally Scope finalClause = this.parseBlockOrSingleCommand( functionType, null, body, false, allowBreak, allowContinue ); return new Try( body, finalClause ); } private Scope parseStatic( final Type functionType, final BasicScope parentScope ) { if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( "static" ) ) { return null; } this.readToken(); // final Scope result = new StaticScope( parentScope ); if ( this.currentToken() == null || !this.currentToken().equals( "{" ) ) // body is a single call { return this.parseCommandOrDeclaration( result, functionType ); } this.readToken(); //read { this.parseScope( result, functionType, parentScope, false, false ); if ( this.currentToken() == null || !this.currentToken().equals( "}" ) ) { throw this.parseException( "}", this.currentToken() ); } this.readToken(); //read } return result; } private SortBy parseSort( final BasicScope parentScope ) { // sort aggregate by expr if ( this.currentToken() == null ) { return null; } if ( !this.currentToken().equalsIgnoreCase( "sort" ) ) { return null; } if ( this.nextToken() == null || this.nextToken().equals( "(" ) || this.nextToken().equals( "=" ) ) { // it's a call to a function named sort(), or an assigment to // a variable named sort, not the sort statement. return null; } this.readToken(); // sort // Get an aggregate reference Value aggregate = this.parseVariableReference( parentScope ); if ( aggregate == null || !( aggregate instanceof VariableReference ) || !( aggregate.getType().getBaseType() instanceof AggregateType ) ) { throw this.parseException( "Aggregate reference expected" ); } if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( "by" ) ) { throw this.parseException( "by", this.currentToken() ); } this.readToken(); // by // Define key variables of appropriate type VariableList varList = new VariableList(); AggregateType type = (AggregateType) aggregate.getType().getBaseType(); Variable valuevar = new Variable( "value", type.getDataType() ); varList.add( valuevar ); Variable indexvar = new Variable( "index", type.getIndexType() ); varList.add( indexvar ); // Parse the key expression in a new scope containing 'index' and 'value' Scope scope = new Scope( varList, parentScope ); Value expr = this.parseExpression( scope ); return new SortBy( (VariableReference) aggregate, indexvar, valuevar, expr, this ); } private Loop parseForeach( final Type functionType, final BasicScope parentScope ) { // foreach key [, key ... ] in aggregate { scope } if ( this.currentToken() == null ) { return null; } if ( !this.currentToken().equalsIgnoreCase( "foreach" ) ) { return null; } this.readToken(); // foreach List<String> names = new ArrayList<String>(); while ( true ) { String name = this.currentToken(); if ( !this.parseIdentifier( name ) ) { throw this.parseException( "Key variable name expected" ); } if ( Parser.isReservedWord( name ) ) { throw this.parseException( "Reserved word '" + name + "' cannot be a key variable name" ); } if ( names.contains( name ) ) { throw this.parseException( "Key variable '" + name + "' is already defined" ); } names.add( name ); this.readToken(); // name if ( this.currentToken() != null ) { if ( this.currentToken().equals( "," ) ) { this.readToken(); // , continue; } if ( this.currentToken().equalsIgnoreCase( "in" ) ) { this.readToken(); // in break; } } throw this.parseException( "in", this.currentToken() ); } // Get an aggregate reference Value aggregate = this.parseValue( parentScope ); if ( aggregate == null || !( aggregate.getType().getBaseType() instanceof AggregateType ) ) { throw this.parseException( "Aggregate reference expected" ); } // Define key variables of appropriate type VariableList varList = new VariableList(); List<VariableReference> variableReferences = new ArrayList<VariableReference>(); Type type = aggregate.getType().getBaseType(); for ( String name : names ) { Type itype; if ( type == null ) { throw this.parseException( "Too many key variables specified" ); } else if ( !( type instanceof AggregateType ) ) { // Variable after all key vars holds the value instead itype = type; type = null; } else { itype = ( (AggregateType) type ).getIndexType(); type = ( (AggregateType) type ).getDataType(); } Variable keyvar = new Variable( name, itype ); varList.add( keyvar ); variableReferences.add( new VariableReference( keyvar ) ); } // Parse the scope with the list of keyVars Scope scope = this.parseLoopScope( functionType, varList, parentScope ); // Add the foreach node with the list of varRefs return new ForEachLoop( scope, variableReferences, aggregate, this ); } private Loop parseFor( final Type functionType, final BasicScope parentScope ) { // for identifier from X [upto|downto|to|] Y [by Z]? {scope } if ( this.currentToken() == null ) { return null; } if ( !this.currentToken().equalsIgnoreCase( "for" ) ) { return null; } String name = this.nextToken(); if ( !this.parseIdentifier( name ) ) { return null; } if ( Parser.isReservedWord( name ) ) { throw this.parseException( "Reserved word '" + name + "' cannot be an index variable name" ); } if ( parentScope.findVariable( name ) != null ) { throw this.parseException( "Index variable '" + name + "' is already defined" ); } this.readToken(); // for this.readToken(); // name if ( !this.currentToken().equalsIgnoreCase( "from" ) ) { throw this.parseException( "from", this.currentToken() ); } this.readToken(); // from Value initial = this.parseExpression( parentScope ); int direction = 0; if ( this.currentToken().equalsIgnoreCase( "upto" ) ) { direction = 1; } else if ( this.currentToken().equalsIgnoreCase( "downto" ) ) { direction = -1; } else if ( this.currentToken().equalsIgnoreCase( "to" ) ) { direction = 0; } else { throw this.parseException( "to, upto, or downto", this.currentToken() ); } this.readToken(); // upto/downto Value last = this.parseExpression( parentScope ); Value increment = DataTypes.ONE_VALUE; if ( this.currentToken().equalsIgnoreCase( "by" ) ) { this.readToken(); // by increment = this.parseExpression( parentScope ); } // Create integer index variable Variable indexvar = new Variable( name, DataTypes.INT_TYPE ); // Put index variable onto a list VariableList varList = new VariableList(); varList.add( indexvar ); Scope scope = this.parseLoopScope( functionType, varList, parentScope ); return new ForLoop( scope, new VariableReference( indexvar ), initial, last, increment, direction, this ); } private Loop parseJavaFor( final Type functionType, final BasicScope parentScope ) { if ( this.currentToken() == null ) { return null; } if ( !this.currentToken().equalsIgnoreCase( "for" ) ) { return null; } if ( this.nextToken() == null || !this.nextToken().equals( "(" ) ) { return null; } this.readToken(); // for this.readToken(); // ( // Parse variables and initializers Scope scope = new Scope( parentScope ); List<Assignment> initializers = new ArrayList<Assignment>(); // Parse each initializer in the context of scope, adding // variable to variable list in the scope, and saving // initialization expressions in initializers. while ( this.currentToken() != null && !this.currentToken.equals( ";" ) ) { Type t = this.parseType( scope, true, true ); String name = this.currentToken(); Variable variable; if ( name == null || !this.parseIdentifier( name ) || Parser.isReservedWord( name ) ) { throw this.parseException( "Identifier required" ); } // If there is no data type, it is using an existing variable if ( t == null ) { variable = parentScope.findVariable( name ); if ( variable == null ) { throw this.parseException( "Unknown variable '" + name + "'" ); } t = variable.getType(); } else { if ( scope.findVariable( name, true ) != null ) { throw this.parseException( "Variable '" + name + "' already defined" ); } // Create variable and add it to the scope variable = new Variable( name, t ); scope.addVariable( variable ); } this.readToken(); // name VariableReference lhs = new VariableReference( name, scope ); Value rhs = null; if ( this.currentToken().equals( "=" ) ) { this.readToken(); // = rhs = this.parseExpression( scope ); if ( rhs == null ) { throw this.parseException( "Expression expected" ); } Type ltype = t.getBaseType(); Type rtype = rhs.getType(); if ( !Parser.validCoercion( ltype, rtype, "assign" ) ) { throw this.parseException( "Cannot store " + rtype + " in " + name + " of type " + ltype ); } } Assignment initializer = new Assignment( lhs, rhs ); initializers.add( initializer); if ( this.currentToken() != null && this.currentToken().equals( "," ) ) { this.readToken(); // , if ( this.currentToken() == null || this.currentToken.equals( ";" ) ) { throw this.parseException( "Identifier expected" ); } } } if ( this.currentToken() == null || !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } this.readToken(); // ; // Parse condition in context of scope Value condition = ( this.currentToken() != null && this.currentToken().equals( ";" ) ) ? DataTypes.TRUE_VALUE : this.parseExpression( scope ); if ( this.currentToken() == null || !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } if ( condition == null || condition.getType() != DataTypes.BOOLEAN_TYPE ) { throw this.parseException( "\"for\" requires a boolean conditional expression" ); } this.readToken(); // ; // Parse incrementers in context of scope List<ParseTreeNode> incrementers = new ArrayList<ParseTreeNode>(); while ( this.currentToken() != null && !this.currentToken.equals( ")" ) ) { Value value = parsePreIncDec( scope ); if ( value != null ) { incrementers.add( value ); } else { value = this.parseVariableReference( scope ); if ( !( value instanceof VariableReference ) ) { throw this.parseException( "Variable reference expected" ); } VariableReference ref = (VariableReference) value; Value lhs = this.parsePostIncDec( ref ); if ( lhs == ref ) { Assignment incrementer = parseAssignment( scope, ref ); if ( incrementer == null ) { throw this.parseException( "Variable '" + ref.getName() + "' not incremented" ); } incrementers.add( incrementer ); } else { incrementers.add( lhs ); } } if ( this.currentToken() != null && this.currentToken().equals( "," ) ) { this.readToken(); // , if ( this.currentToken() == null || this.currentToken.equals( ")" ) ) { throw this.parseException( "Identifier expected" ); } } } if ( this.currentToken() == null || !this.currentToken().equals( ")" ) ) { throw this.parseException( ")", this.currentToken() ); } this.readToken(); // ) // Parse scope body this.parseLoopScope( scope, functionType, parentScope ); return new JavaForLoop( scope, initializers, condition, incrementers ); } private Scope parseLoopScope( final Type functionType, final VariableList varList, final BasicScope parentScope ) { return this.parseLoopScope( new Scope( varList, parentScope ), functionType, parentScope ); } private Scope parseLoopScope( final Scope result, final Type functionType, final BasicScope parentScope ) { if ( this.currentToken() != null && this.currentToken().equals( "{" ) ) { // Scope is a block this.readToken(); // { this.parseScope( result, functionType, parentScope, true, true ); if ( this.currentToken() == null || !this.currentToken().equals( "}" ) ) { throw this.parseException( "}", this.currentToken() ); } this.readToken(); // } } else { // Scope is a single command ParseTreeNode command = this.parseCommand( functionType, result, false, true, true ); if ( command == null ) { if ( this.currentToken() == null || !this.currentToken().equals( ";" ) ) { throw this.parseException( ";", this.currentToken() ); } this.readToken(); // ; } else { result.addCommand( command, this ); } } return result; } private Value parseNewRecord( final BasicScope scope ) { if ( !this.parseIdentifier( this.currentToken() ) ) { return null; } String name = this.currentToken(); Type type = scope.findType( name ); if ( type == null || !( type instanceof RecordType ) ) { throw this.parseException( "'" + name + "' is not a record type" ); } RecordType target = (RecordType) type; this.readToken(); //name List params = new ArrayList<Value>(); String [] names = target.getFieldNames(); Type [] types = target.getFieldTypes(); int param = 0; if ( this.currentToken() != null && this.currentToken().equals( "(" ) ) { this.readToken(); //( while ( this.currentToken() != null && !this.currentToken().equals( ")" ) ) { Type expected = types[param]; Value val; if ( this.currentToken().equals( "," ) ) { val = DataTypes.VOID_VALUE; } else if ( this.currentToken().equals( "{" ) && expected.getBaseType() instanceof AggregateType ) { this.readToken(); // read { val = this.parseAggregateLiteral( scope, (AggregateType) expected.getBaseType() ); } else { val = this.parseExpression( scope ); } if ( val == null ) { throw this.parseException( "Expression expected for field #" + ( param + 1 ) + " (" + names[param] + ")" ); } if ( val != DataTypes.VOID_VALUE ) { Type given = val.getType(); if ( !expected.equals( given ) ) { throw this.parseException( given + " found when " + expected + " expected for field #" + ( param + 1 ) + " (" + names[param] + ")" ); } } params.add( val ); param++; if ( this.currentToken().equals( "," ) ) { if ( param == names.length ) { throw this.parseException( "Too many field initializers for record " + name ); } this.readToken(); // , } } if ( this.currentToken() == null ) { throw this.parseException( ")", this.currentToken() ); } this.readToken(); // ) } return target.initialValueExpression( params ); } private Value parseCall( final BasicScope scope ) { return this.parseCall( scope, null ); } private Value parseCall( final BasicScope scope, final Value firstParam ) { if ( this.nextToken() == null || !this.nextToken().equals( "(" ) ) { return null; } if ( !this.parseScopedIdentifier( this.currentToken() ) ) { return null; } String name = this.currentToken(); this.readToken(); //name List<Value> params = this.parseParameters( scope, firstParam ); Function target = this.findFunction( scope, name, params ); if ( target == null ) { throw this.undefinedFunctionException( name, params ); } FunctionCall call = new FunctionCall( target, params, this ); return parsePostCall( scope, call ); } private List<Value> parseParameters( final BasicScope scope, final Value firstParam ) { if ( !this.currentToken().equals( "(" ) ) { return null; } this.readToken(); //( List<Value> params = new ArrayList<Value>(); if ( firstParam != null ) { params.add( firstParam ); } while ( this.currentToken() != null && !this.currentToken().equals( ")" ) ) { Value val = this.parseExpression( scope ); if ( val != null ) { params.add( val ); } if ( this.currentToken() == null ) { throw this.parseException( ")", "end of file" ); } if ( !this.currentToken().equals( "," ) ) { if ( !this.currentToken().equals( ")" ) ) { throw this.parseException( ")", this.currentToken() ); } continue; } this.readToken(); // , if ( this.currentToken() == null ) { throw this.parseException( ")", "end of file" ); } if ( this.currentToken().equals( ")" ) ) { throw this.parseException( "parameter", this.currentToken() ); } } if ( this.currentToken() == null ) { throw this.parseException( ")", "end of file" ); } if ( !this.currentToken().equals( ")" ) ) { throw this.parseException( ")", this.currentToken() ); } this.readToken(); // ) return params; } private Value parsePostCall( final BasicScope scope, FunctionCall call ) { Value result = call; while ( result != null && this.currentToken() != null && this.currentToken().equals( "." ) ) { Variable current = new Variable( result.getType() ); current.setExpression( result ); result = this.parseVariableReference( scope, current ); } return result; } private Value parseInvoke( final BasicScope scope ) { if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( "call" ) ) { return null; } this.readToken(); // call Type type = this.parseType( scope, true, false ); // You can omit the type, but then this function invocation // cannot be used in an expression if ( type == null ) { type = DataTypes.VOID_TYPE; } String current = this.currentToken(); Value name = null; if ( current.equals( "(" ) ) { name = this.parseExpression( scope ); if ( name == null || !name.getType().equals( DataTypes.STRING_TYPE ) ) { throw this.parseException( "String expression expected for function name" ); } } else { if ( !this.parseIdentifier( current ) ) { throw this.parseException( "Variable reference expected for function name" ); } name = this.parseVariableReference( scope ); if ( name == null || !( name instanceof VariableReference ) ) { throw this.parseException( "Variable reference expected for function name" ); } } List<Value> params = parseParameters( scope, null ); FunctionInvocation call = new FunctionInvocation( scope, type, name, params, this ); return parsePostCall( scope, call ); } private static final String [] COMPAT_FUNCTIONS = { "to_string", "to_boolean", "to_int", "to_float", "to_item", "to_class", "to_stat", "to_skill", "to_effect", "to_location", "to_familiar", "to_monster", "to_slot", "to_element", "to_coinmaster", "to_url", }; private final Function findFunction( final BasicScope scope, final String name, final List<Value> params ) { Function result = this.findFunction( scope, scope.getFunctions(), name, params, true ); if ( result != null ) { return result; } result = this.findFunction( scope, RuntimeLibrary.functions, name, params, true ); if ( result != null ) { return result; } result = this.findFunction( scope, scope.getFunctions(), name, params, false ); if ( result != null ) { return result; } result = this.findFunction( scope, RuntimeLibrary.functions, name, params, false ); if ( result != null ) { return result; } // Just in case some people didn't edit their scripts to use // the new function format, check for the old versions as well. for ( int i = 0; i < COMPAT_FUNCTIONS.length; ++i ) { String fname = COMPAT_FUNCTIONS[i]; if ( !name.equals( fname ) && name.endsWith( fname ) ) { return this.findFunction( scope, fname, params ); } } return null; } private final Function findFunction( BasicScope scope, final FunctionList source, final String name, final List<Value> params, boolean isExactMatch ) { if ( params == null ) { return null; } Function[] functions = source.findFunctions( name ); // First, try to find an exact match on parameter types. // This allows strict matches to take precedence. for ( Function function : functions ) { Iterator<VariableReference> refIterator = function.getVariableReferences().iterator(); Iterator<Value> valIterator = params.iterator(); boolean matched = true; while ( refIterator.hasNext() && valIterator.hasNext() ) { VariableReference currentParam = refIterator.next(); Type paramType = currentParam.getType(); Value currentValue = valIterator.next(); Type valueType = currentValue.getType(); if ( isExactMatch ) { if ( paramType == null || !paramType.equals( valueType ) ) { matched = false; break; } } else if ( !Parser.validCoercion( paramType, valueType, "parameter" ) ) { matched = false; break; } } if ( refIterator.hasNext() || valIterator.hasNext() ) { matched = false; } if ( matched ) { return function; } } if ( isExactMatch || source == RuntimeLibrary.functions ) { return null; } if ( scope.getParentScope() != null ) { return findFunction( scope.getParentScope(), name, params ); } return null; } private Assignment parseAssignment( final BasicScope scope, final VariableReference lhs ) { String operStr = this.currentToken(); if ( !operStr.equals( "=" ) && !operStr.equals( "+=" ) && !operStr.equals( "-=" ) && !operStr.equals( "*=" ) && !operStr.equals( "/=" ) && !operStr.equals( "%=" ) && !operStr.equals( "**=" ) && !operStr.equals( "&=" ) && !operStr.equals( "^=" ) && !operStr.equals( "|=" ) && !operStr.equals( "<<=" ) && !operStr.equals( ">>=" ) && !operStr.equals( ">>>=" ) ) { return null; } Operator oper = new Operator( operStr, this ); this.readToken(); // oper Value rhs = this.parseExpression( scope ); if ( rhs == null ) { throw this.parseException( "Internal error" ); } if ( !Parser.validCoercion( lhs.getType(), rhs.getType(), oper ) ) { String error = oper.isLogical() ? ( oper + " requires an integer or boolean expression and an integer or boolean variable reference" ) : oper.isInteger() ? ( oper + " requires an integer expression and an integer variable reference" ) : ( "Cannot store " + rhs.getType() + " in " + lhs + " of type " + lhs.getType() ); throw this.parseException( error ); } Operator op = operStr.equals( "=" ) ? null : new Operator( operStr.substring( 0, operStr.length() - 1 ), this ); return new Assignment( lhs, rhs, op ); } private Value parseRemove( final BasicScope scope ) { if ( this.currentToken() == null || !this.currentToken().equals( "remove" ) ) { return null; } Value lhs = this.parseExpression( scope ); if ( lhs == null ) { throw this.parseException( "Bad 'remove' statement" ); } return lhs; } private Value parsePreIncDec( final BasicScope scope ) { if ( this.nextToken() == null ) { return null; } Value lhs = null; String operStr = null; // --[VariableReference] // ++[VariableReference] if ( this.currentToken().equals( "++" ) || this.currentToken().equals( "--" ) ) { operStr = this.currentToken().equals( "++" ) ? Parser.PRE_INCREMENT : Parser.PRE_DECREMENT; this.readToken(); // oper lhs = this.parseVariableReference( scope ); if ( lhs == null ) { throw this.parseException( "Variable reference expected" ); } } else { return null; } int ltype = lhs.getType().getType(); if ( ltype != DataTypes.TYPE_INT && ltype != DataTypes.TYPE_FLOAT ) { throw this.parseException( operStr + " requires a numeric variable reference" ); } Operator oper = new Operator( operStr, this ); return new IncDec( (VariableReference) lhs, oper ); } private Value parsePostIncDec( final VariableReference lhs ) { if ( this.currentToken() == null ) { return lhs; } // [VariableReference]++ // [VariableReference]-- if ( !this.currentToken().equals( "++" ) && !this.currentToken().equals( "--" ) ) { return lhs; } String operStr = this.currentToken().equals( "++" ) ? Parser.POST_INCREMENT : Parser.POST_DECREMENT; int ltype = lhs.getType().getType(); if ( ltype != DataTypes.TYPE_INT && ltype != DataTypes.TYPE_FLOAT ) { throw this.parseException( operStr + " requires a numeric variable reference" ); } this.readToken(); // oper Operator oper = new Operator( operStr, this ); return new IncDec( lhs, oper ); } private Value parseExpression( final BasicScope scope ) { return this.parseExpression( scope, null ); } private Value parseExpression( final BasicScope scope, final Operator previousOper ) { if ( this.currentToken() == null ) { return null; } Value lhs = null; Value rhs = null; Operator oper = null; if ( this.currentToken().equals( "!" ) ) { String operator = this.currentToken(); this.readToken(); // ! if ( ( lhs = this.parseValue( scope ) ) == null ) { throw this.parseException( "Value expected" ); } lhs = new Operation( lhs, new Operator( operator, this ) ); if ( lhs.getType() != DataTypes.BOOLEAN_TYPE ) { throw this.parseException( "\"!\" operator requires a boolean value" ); } } else if ( this.currentToken().equals( "~" ) ) { String operator = this.currentToken(); this.readToken(); // ~ if ( ( lhs = this.parseValue( scope ) ) == null ) { throw this.parseException( "Value expected" ); } lhs = new Operation( lhs, new Operator( operator, this ) ); if ( lhs.getType() != DataTypes.INT_TYPE && lhs.getType() != DataTypes.BOOLEAN_TYPE ) { throw this.parseException( "\"~\" operator requires an integer or boolean value" ); } } else if ( this.currentToken().equals( "-" ) ) { // See if it's a negative numeric constant if ( ( lhs = this.parseValue( scope ) ) == null ) { // Nope. Unary minus. String operator = this.currentToken(); this.readToken(); // - if ( ( lhs = this.parseValue( scope ) ) == null ) { throw this.parseException( "Value expected" ); } lhs = new Operation( lhs, new Operator( operator, this ) ); } } else if ( this.currentToken().equals( "remove" ) ) { String operator = this.currentToken(); this.readToken(); // remove lhs = this.parseVariableReference( scope ); if ( lhs == null || !( lhs instanceof CompositeReference ) ) { throw this.parseException( "Aggregate reference expected" ); } lhs = new Operation( lhs, new Operator( operator, this ) ); } else if ( ( lhs = this.parseValue( scope ) ) == null ) { return null; } do { oper = this.parseOperator( this.currentToken() ); if ( oper == null ) { return lhs; } if ( previousOper != null && !oper.precedes( previousOper ) ) { return lhs; } if ( this.currentToken().equals( ":" ) ) { return lhs; } if ( this.currentToken().equals( "?" ) ) { this.readToken(); // ? Value conditional = lhs; if ( conditional.getType() != DataTypes.BOOLEAN_TYPE ) { throw this.parseException( "Non-boolean expression " + conditional + " (" + conditional.getType() + ")" ); } if ( ( lhs = this.parseExpression( scope, null ) ) == null ) { throw this.parseException( "Value expected in left hand side" ); } if ( !this.currentToken().equals( ":" ) ) { throw this.parseException( "\":\" expected" ); } this.readToken(); // : if ( ( rhs = this.parseExpression( scope, null ) ) == null ) { throw this.parseException( "Value expected" ); } if ( !Parser.validCoercion( lhs.getType(), rhs.getType(), oper ) ) { throw this.parseException( "Cannot choose between " + lhs + " (" + lhs.getType() + ") and " + rhs + " (" + rhs.getType() + ")" ); } lhs = new TernaryExpression( conditional, lhs, rhs ); } else { this.readToken(); //operator if ( ( rhs = this.parseExpression( scope, oper ) ) == null ) { throw this.parseException( "Value expected" ); } Type ltype = lhs.getType(); Type rtype = rhs.getType(); if ( oper.equals( "+" ) && ( ltype.equals( DataTypes.TYPE_STRING ) || rtype.equals( DataTypes.TYPE_STRING ) ) ) { // String concatenation if ( lhs instanceof Concatenate ) { Concatenate conc = (Concatenate) lhs; conc.addString( rhs ); } else { lhs = new Concatenate( lhs, rhs ); } } else if ( !Parser.validCoercion( ltype, rtype, oper ) ) { throw this.parseException( "Cannot apply operator " + oper + " to " + lhs + " (" + lhs.getType() + ") and " + rhs + " (" + rhs.getType() + ")" ); } else { lhs = new Operation( lhs, rhs, oper ); } } } while ( true ); } private Value parseValue( final BasicScope scope ) { if ( this.currentToken() == null ) { return null; } Value result = null; // Parse parenthesized expressions if ( this.currentToken().equals( "(" ) ) { this.readToken(); // ( result = this.parseExpression( scope ); if ( this.currentToken() == null || !this.currentToken().equals( ")" ) ) { throw this.parseException( ")", this.currentToken() ); } this.readToken(); // ) } // Parse constant values // true and false are reserved words else if ( this.currentToken().equalsIgnoreCase( "true" ) ) { this.readToken(); result = DataTypes.TRUE_VALUE; } else if ( this.currentToken().equalsIgnoreCase( "false" ) ) { this.readToken(); result = DataTypes.FALSE_VALUE; } else if ( this.currentToken().equals( "__FILE__" ) ) { this.readToken(); result = new Value( String.valueOf( this.shortFileName ) ); } // numbers else if ( ( result = this.parseNumber() ) != null ) { ; } else if ( this.currentToken().equals( "\"" ) || this.currentToken().equals( "\'" ) ) { result = this.parseString( null ); } else if ( this.currentToken().equals( "$" ) ) { result = this.parseTypedConstant( scope ); } else if ( this.currentToken().equalsIgnoreCase( "new" ) ) { this.readToken(); result = this.parseNewRecord( scope ); } else if ( ( result = this.parsePreIncDec( scope ) ) != null ) { return result; } else if ( ( result = this.parseInvoke( scope ) ) != null ) { ; } else if ( ( result = this.parseCall( scope, null ) ) != null ) { ; } else { Type baseType = this.parseType( scope, true, false ); if ( baseType != null && baseType.getBaseType() instanceof AggregateType ) { if ( !"{".equals( this.currentToken() ) ) { throw this.parseException( "{", this.currentToken() ); } this.readToken(); result = this.parseAggregateLiteral( scope, (AggregateType) baseType.getBaseType() ); } else { if ( baseType != null ) { this.replaceToken( baseType.name ); } if ( ( result = this.parseVariableReference( scope ) ) != null ) { ; } } } while ( result != null && this.currentToken() != null && ( this.currentToken().equals( "." ) || this.currentToken().equals( "[" ) ) ) { Variable current = new Variable( result.getType() ); current.setExpression( result ); result = this.parseVariableReference( scope, current ); } if ( result instanceof VariableReference ) { VariableReference ref = (VariableReference) result; Assignment value = this.parseAssignment( scope, ref ); return ( value != null ) ? value : this.parsePostIncDec( ref ); } return result; } private Value parseNumber() { if ( this.currentToken() == null ) { return null; } int sign = 1; if ( this.currentToken().equals( "-" ) ) { String next = this.nextToken(); if ( next == null ) { return null; } if ( !next.equals( "." ) && !this.readIntegerToken( next ) ) { // Unary minus return null; } sign = -1; this.readToken(); // Read - } if ( this.currentToken().equals( "." ) ) { this.readToken(); String fraction = this.currentToken(); if ( !this.readIntegerToken( fraction ) ) { throw this.parseException( "numeric value", fraction ); } this.readToken(); // integer return new Value( sign * StringUtilities.parseDouble( "0." + fraction ) ); } String integer = this.currentToken(); if ( !this.readIntegerToken( integer ) ) { return null; } this.readToken(); // integer if ( this.currentToken().equals( "." ) ) { String fraction = this.nextToken(); if ( !this.readIntegerToken( fraction ) ) { return new Value( sign * StringUtilities.parseLong( integer ) ); } this.readToken(); // . this.readToken(); // fraction return new Value( sign * StringUtilities.parseDouble( integer + "." + fraction ) ); } return new Value( sign * StringUtilities.parseLong( integer ) ); } private boolean readIntegerToken( final String token ) { if ( token == null ) { return false; } for ( int i = 0; i < token.length(); ++i ) { if ( !Character.isDigit( token.charAt( i ) ) ) { return false; } } return true; } private Value parseString( Type type ) { // Directly work with currentLine - ignore any "tokens" you meet until // the string is closed char startCharacter = this.currentLine.charAt( 0 ); char stopCharacter = startCharacter; boolean allowComments = false; List<Value> list = null; if ( type != null ) { // Typed plural constant - handled by same code as plain strings // so that they can share escape character processing stopCharacter = ']'; allowComments = type.getBaseType().getType() != DataTypes.TYPE_STRING; list = new ArrayList<Value>(); } int level = 1; boolean slash = false; StringBuilder resultString = new StringBuilder(); for ( int i = 1; ; ++i ) { if ( i == this.currentLine.length() ) { // Plain strings can't span lines if ( type == null ) { throw this.parseException( "No closing \" found" ); } this.currentLine = ""; this.fixLines(); i = 0; if ( this.currentLine == null ) { throw this.parseException( "No closing ] found" ); } } char ch = this.currentLine.charAt( i ); // Handle escape sequences if ( ch == '\\' ) { if ( i == this.currentLine.length() - 1 ) { i = -1; ch = '\n'; this.currentLine = this.nextLine; this.nextLine = this.getNextLine(); } else { ch = this.currentLine.charAt( ++i ); } switch ( ch ) { case 'n': resultString.append( '\n' ); break; case 'r': resultString.append( '\r' ); break; case 't': resultString.append( '\t' ); break; case ',': resultString.append( ',' ); break; case 'x': try { int hex08 = Integer.parseInt( this.currentLine.substring( i + 1, i + 3 ), 16 ); resultString.append( (char) hex08 ); i += 2; } catch ( NumberFormatException e ) { throw this.parseException( "Hexadecimal character escape requires 2 digits" ); } break; case 'u': try { int hex16 = Integer.parseInt( this.currentLine.substring( i + 1, i + 5 ), 16 ); resultString.append( (char) hex16 ); i += 4; } catch ( NumberFormatException e ) { throw this.parseException( "Unicode character escape requires 4 digits" ); } break; default: if ( Character.isDigit( ch ) ) { try { int octal = Integer.parseInt( this.currentLine.substring( i, i + 3 ), 8 ); resultString.append( (char) octal ); i += 2; break; } catch ( NumberFormatException e ) { throw this.parseException( "Octal character escape requires 3 digits" ); } } resultString.append( ch ); } continue; } // Potentially handle comments if ( allowComments ) { // If we've already seen a slash if ( slash ) { slash = false; if ( ch == '/' ) { // Throw away the rest of the line i = this.currentLine.length() - 1; continue; } resultString.append( '/' ); } else if ( ch == '/' ) { slash = true; continue; } } // Handle plain strings if ( type == null ) { if ( ch == stopCharacter ) { this.currentLine = this.currentLine.substring( i + 1 ); //+ 1 to get rid of '"' token this.currentToken = null; return new Value( resultString.toString() ); } resultString.append( ch ); continue; } // Handle typed constants // Allow start char without escaping if ( ch == startCharacter ) { level++; resultString.append( ch ); continue; } // Match non-initial start char if ( ch == stopCharacter && --level > 0 ) { resultString.append( ch ); continue; } if ( ch != stopCharacter && ch != ',' ) { resultString.append( ch ); continue; } // Add a new element to the list String element = resultString.toString().trim(); resultString.setLength( 0 ); if ( element.length() != 0 ) { list.add( parseLiteral( type, element ) ); } if ( ch == stopCharacter ) { this.currentLine = this.currentLine.substring( i + 1 ); this.currentToken = null; if ( list.size() == 0 ) { // Empty list - caller will interpret this specially return null; } return new PluralValue( type, list ); } } } private final Value parseLiteral( Type type, String element ) { Value value = DataTypes.parseValue( type, element, false ); if ( value == null ) { throw this.parseException( "Bad " + type.toString() + " value: \"" + element + "\"" ); } if ( !StringUtilities.isNumeric( element ) ) { String fullName = value.toString(); if ( !element.equalsIgnoreCase( fullName ) ) { String s1 = CharacterEntities.escape( StringUtilities.globalStringReplace( element, ",", "\\," ).replaceAll("(?<= ) ", "\\\\ " ) ); String s2 = CharacterEntities.escape( StringUtilities.globalStringReplace( fullName, ",", "\\," ).replaceAll("(?<= ) ", "\\\\ " ) ); List<String> names = new ArrayList<String>(); if ( type == DataTypes.ITEM_TYPE ) { int itemId = (int)value.contentLong; String name = ItemDatabase.getItemName( itemId ); int[] ids = ItemDatabase.getItemIds( name, 1, false ); for ( int id : ids ) { String s3 = "$item[[" + String.valueOf( id ) + "]" + name + "]"; names.add( s3 ); } } else if ( type == DataTypes.EFFECT_TYPE ) { int effectId = (int)value.contentLong; String name = EffectDatabase.getEffectName( effectId ); int[] ids = EffectDatabase.getEffectIds( name, false ); for ( int id : ids ) { String s3 = "$effect[[" + String.valueOf( id ) + "]" + name + "]"; names.add( s3 ); } } if ( names.size() > 1 ) { ScriptException ex = this.parseException2( "Multiple matches for \"" + s1 + "\"; using \"" + s2 + "\".", "Clarify by using one of:" ); RequestLogger.printLine( ex.getMessage() ); for ( String name : names ) { RequestLogger.printLine( name ); } } else { ScriptException ex = this.parseException( "Changing \"" + s1 + "\" to \"" + s2 + "\" would get rid of this message." ); RequestLogger.printLine( ex.getMessage() ); } } } return value; } private Value parseTypedConstant( final BasicScope scope ) { this.readToken(); // read $ String name = this.currentToken(); Type type = this.parseType( scope, false, false ); boolean plurals = false; if ( type == null ) { StringBuilder buf = new StringBuilder( this.currentLine ); int length = name.length(); if ( name.endsWith( "ies" ) ) { buf.delete( length - 3, length ); buf.insert( length - 3, "y" ); } else if ( name.endsWith( "es" ) ) { buf.delete( length - 2, length ); } else if ( name.endsWith( "s" ) ) { buf.deleteCharAt( length - 1 ); } else if ( name.endsWith( "a" ) ) { buf.deleteCharAt( length - 1 ); buf.insert( length - 1, "um" ); } else { throw this.parseException( "Unknown type " + name ); } this.currentLine = buf.toString(); this.currentToken = null; type = this.parseType( scope, false, false ); plurals = true; } if ( type == null ) { throw this.parseException( "Unknown type " + name ); } if ( !type.isPrimitive() ) { throw this.parseException( "Non-primitive type " + name ); } if ( !this.currentToken().equals( "[" ) ) { throw this.parseException( "[", this.currentToken() ); } if ( plurals ) { Value value = this.parseString( type ); if ( value != null ) { return value; // explicit list of values } value = type.allValues(); if ( value != null ) { return value; // implicit enumeration } throw this.parseException( "Can't enumerate all " + name ); } StringBuilder resultString = new StringBuilder(); int level = 1; for ( int i = 1;; ++i ) { if ( i == this.currentLine.length() ) { throw this.parseException( "No closing ] found" ); } char c = this.currentLine.charAt( i ); if ( c == '\\' ) { resultString.append( this.currentLine.charAt( ++i ) ); } else if ( c == '[' ) { level++; resultString.append( c ); } else if ( c == ']' ) { if ( --level > 0 ) { resultString.append( c ); continue; } this.currentLine = this.currentLine.substring( i + 1 ); //+1 to get rid of ']' token this.currentToken = null; String input = resultString.toString().trim(); // Make sure that only ASCII characters appear in the string // *** No longer necessary? // if ( !input.matches( "^\\p{ASCII}*$" ) ) // { // throw this.parseException( "Typed constant $" + type.toString() + "[" + input + "] contains non-ASCII characters" ); // } return parseLiteral( type, input ); } else { resultString.append( c ); } } } private Operator parseOperator( final String oper ) { if ( oper == null || !this.isOperator( oper ) ) { return null; } return new Operator( oper, this ); } private boolean isOperator( final String oper ) { return oper.equals( "!" ) || oper.equals( "?" ) || oper.equals( ":" ) || oper.equals( "*" ) || oper.equals( "**" ) || oper.equals( "/" ) || oper.equals( "%" ) || oper.equals( "+" ) || oper.equals( "-" ) || oper.equals( "&" ) || oper.equals( "^" ) || oper.equals( "|" ) || oper.equals( "~" ) || oper.equals( "<<" ) || oper.equals( ">>" ) || oper.equals( ">>>" ) || oper.equals( "<" ) || oper.equals( ">" ) || oper.equals( "<=" ) || oper.equals( ">=" ) || oper.equals( "==" ) || oper.equals( Parser.APPROX ) || oper.equals( "!=" ) || oper.equals( "||" ) || oper.equals( "&&" ) || oper.equals( "contains" ) || oper.equals( "remove" ); } private Value parseVariableReference( final BasicScope scope ) { if ( this.currentToken() == null || !this.parseIdentifier( this.currentToken() ) ) { return null; } String name = this.currentToken(); Variable var = scope.findVariable( name, true ); if ( var == null ) { throw this.parseException( "Unknown variable '" + name + "'" ); } this.readToken(); // read name if ( this.currentToken() == null || !this.currentToken().equals( "[" ) && !this.currentToken().equals( "." ) ) { return new VariableReference( var ); } return this.parseVariableReference( scope, var ); } private Value parseVariableReference( final BasicScope scope, final Variable var ) { Type type = var.getType(); List<Value> indices = new ArrayList<Value>(); boolean parseAggregate = this.currentToken().equals( "[" ); while ( this.currentToken() != null && ( this.currentToken().equals( "[" ) || this.currentToken().equals( "." ) || parseAggregate && this.currentToken().equals( "," ) ) ) { Value index; type = type.getBaseType(); if ( this.currentToken().equals( "[" ) || this.currentToken().equals( "," ) ) { this.readToken(); // read [ or , parseAggregate = true; if ( !( type instanceof AggregateType ) ) { if ( indices.isEmpty() ) { throw this.parseException( "Variable '" + var.getName() + "' cannot be indexed" ); } else { throw this.parseException( "Too many keys for '" + var.getName() + "'" ); } } AggregateType atype = (AggregateType) type; index = this.parseExpression( scope ); if ( index == null ) { throw this.parseException( "Index for '" + var.getName() + "' expected" ); } if ( !index.getType().getBaseType().equals( atype.getIndexType().getBaseType() ) ) { throw this.parseException( "Index for '" + var.getName() + "' has wrong data type " + "(expected " + atype.getIndexType() + ", got " + index.getType() + ")" ); } type = atype.getDataType(); } else { this.readToken(); // read . // Maybe it's a function call with an implied "this" parameter. if ( this.nextToken().equals( "(" ) ) { return this.parseCall( scope, indices.isEmpty() ? new VariableReference( var ) : new CompositeReference( var, indices, this ) ); } type = type.asProxy(); if ( !( type instanceof RecordType ) ) { throw this.parseException( "Record expected" ); } RecordType rtype = (RecordType) type; String field = this.currentToken(); if ( field == null || !this.parseIdentifier( field ) ) { throw this.parseException( "Field name expected" ); } index = rtype.getFieldIndex( field ); if ( index == null ) { throw this.parseException( "Invalid field name '" + field + "'" ); } this.readToken(); // read name type = rtype.getDataType( index ); } indices.add( index ); if ( parseAggregate && this.currentToken() != null ) { if ( this.currentToken().equals( "]" ) ) { this.readToken(); // read ] parseAggregate = false; } } } if ( parseAggregate ) { throw this.parseException( this.currentToken(), "]" ); } return new CompositeReference( var, indices, this ); } private String parseDirective( final String directive ) { if ( this.currentToken() == null || !this.currentToken().equalsIgnoreCase( directive ) ) { return null; } this.readToken(); //directive if ( this.currentToken() == null ) { throw this.parseException( "<", this.currentToken() ); } int directiveEndIndex = this.currentLine.indexOf( ";" ); if ( directiveEndIndex == -1 ) { directiveEndIndex = this.currentLine.length(); } String resultString = this.currentLine.substring( 0, directiveEndIndex ); int startIndex = resultString.indexOf( "<" ); int endIndex = resultString.indexOf( ">" ); if ( startIndex != -1 && endIndex == -1 ) { throw this.parseException( "No closing > found" ); } if ( startIndex == -1 ) { startIndex = resultString.indexOf( "\"" ); endIndex = resultString.indexOf( "\"", startIndex + 1 ); if ( startIndex != -1 && endIndex == -1 ) { throw this.parseException( "No closing \" found" ); } } if ( startIndex == -1 ) { startIndex = resultString.indexOf( "\'" ); endIndex = resultString.indexOf( "\'", startIndex + 1 ); if ( startIndex != -1 && endIndex == -1 ) { throw this.parseException( "No closing \' found" ); } } if ( endIndex == -1 ) { endIndex = resultString.indexOf( ";" ); if ( endIndex == -1 ) { endIndex = resultString.length(); } } resultString = resultString.substring( startIndex + 1, endIndex ); this.currentLine = this.currentLine.substring( endIndex ); this.currentToken = null; if ( this.currentToken().equals( ">" ) || this.currentToken().equals( "\"" ) || this.currentToken().equals( "\'" ) ) { this.readToken(); //get rid of '>' or '"' token } if ( this.currentToken().equals( ";" ) ) { this.readToken(); //read ; } return resultString; } private void parseScriptName() { String resultString = this.parseDirective( "script" ); if ( this.scriptName == null ) { this.scriptName = resultString; } } private void parseNotify() { String resultString = this.parseDirective( "notify" ); if ( this.notifyRecipient == null ) { this.notifyRecipient = resultString; } } private void parseSince() { String revision = this.parseDirective( "since" ); if ( revision != null ) // enforce "since" directives RIGHT NOW at parse time this.enforceSince( revision ); } private String parseImport() { return this.parseDirective( "import" ); } public static final boolean validCoercion( Type lhs, Type rhs, final Operator oper ) { int ltype = lhs.getBaseType().getType(); int rtype = rhs.getBaseType().getType(); if ( oper.isInteger() ) { return ( ltype == DataTypes.TYPE_INT && rtype == DataTypes.TYPE_INT ); } if ( oper.isBoolean() ) { return ltype == rtype && ( ltype == DataTypes.TYPE_BOOLEAN ); } if ( oper.isLogical() ) { return ltype == rtype && ( ltype == DataTypes.TYPE_INT || ltype == DataTypes.TYPE_BOOLEAN ); } return Parser.validCoercion( lhs, rhs, oper.toString() ); } public static final boolean validCoercion( Type lhs, Type rhs, final String oper ) { // Resolve aliases lhs = lhs.getBaseType(); rhs = rhs.getBaseType(); if ( oper == null ) { return lhs.getType() == rhs.getType(); } // "oper" is either a standard operator or is a special name: // // "parameter" - value used as a function parameter // lhs = parameter type, rhs = expression type // // "return" - value returned as function value // lhs = function return type, rhs = expression type // // "assign" - value // lhs = variable type, rhs = expression type // The "contains" operator requires an aggregate on the left // and the correct index type on the right. if ( oper.equals( "contains" ) ) { return lhs.getType() == DataTypes.TYPE_AGGREGATE && ( (AggregateType) lhs ).getIndexType().equals( rhs ); } // If the types are equal, no coercion is necessary if ( lhs.equals( rhs ) ) { return true; } if ( lhs.equals( DataTypes.ANY_TYPE ) ) { return true; } // Noncoercible strings only accept strings if ( lhs.equals( DataTypes.STRICT_STRING_TYPE ) ) { return rhs.equals( DataTypes.TYPE_STRING ) || rhs.equals( DataTypes.TYPE_BUFFER ); } // Anything coerces to a string if ( lhs.equals( DataTypes.TYPE_STRING ) ) { return true; } // Anything coerces to a string for concatenation if ( oper.equals( "+" ) && rhs.equals( DataTypes.TYPE_STRING ) ) { return true; } // Int coerces to float if ( lhs.equals( DataTypes.TYPE_INT ) && rhs.equals( DataTypes.TYPE_FLOAT ) ) { return true; } if ( lhs.equals( DataTypes.TYPE_FLOAT ) && rhs.equals( DataTypes.TYPE_INT ) ) { return true; } return false; } // **************** Tokenizer ***************** private static final char BOM = '\ufeff'; private String getNextLine() { try { do { // Read a line from input, and break out of the // do-while loop when you've read a valid line this.fullLine = this.commandStream.readLine(); // Return null at end of file if ( this.fullLine == null ) { return null; } if ( this.fullLine.length() == 0 ) { continue; } // If the line starts with a Unicode BOM, remove it. if ( this.fullLine.charAt( 0 ) == Parser.BOM ) { this.fullLine = this.fullLine.substring( 1 ); } // Remove whitespace at front and end this.fullLine = this.fullLine.trim(); } while ( this.fullLine.length() == 0 ); // Found valid currentLine - return it return this.fullLine; } catch ( IOException e ) { // This should not happen. Therefore, print // a stack trace for debug purposes. StaticEntity.printStackTrace( e ); return null; } } private String currentToken() { // Repeat until we get a token while ( true ) { // If we've already parsed a token, return it if ( this.currentToken != null ) { return this.currentToken; } // Locate next token this.fixLines(); if ( this.currentLine == null ) { return ";"; } // "#" starts a whole-line comment if ( this.currentLine.startsWith( "#" ) ) { // Skip the comment this.currentLine = ""; continue; } // Get the next token for consideration this.currentToken = this.currentLine.substring( 0, this.tokenLength( this.currentLine ) ); // "//" starts a comment which consumes the rest of the line if ( this.currentToken.equals( "//" ) ) { // Skip the comment this.currentToken = null; this.currentLine = ""; continue; } // "/*" starts a comment which is terminated by "*/" if ( !this.currentToken.equals( "/*" ) ) { return this.currentToken; } while ( this.currentLine != null ) { int end = this.currentLine.indexOf( "*/" ); if ( end == -1 ) { // Skip entire line this.currentLine = ""; this.fixLines(); continue; } this.currentLine = this.currentLine.substring( end + 2 ); this.currentToken = null; break; } } } private String nextToken() { this.fixLines(); if ( this.currentLine == null ) { return null; } if ( this.tokenLength( this.currentLine ) >= this.currentLine.length() ) { if ( this.nextLine == null ) { return null; } return this.nextLine.substring( 0, this.tokenLength( this.nextLine ) ).trim(); } String result = this.currentLine.substring( this.tokenLength( this.currentLine ) ).trim(); if ( result.equals( "" ) ) { if ( this.nextLine == null ) { return null; } return this.nextLine.substring( 0, this.tokenLength( this.nextLine ) ); } return result.substring( 0, this.tokenLength( result ) ); } // Put a token back, so it can be parsed again later. private void replaceToken( String s ) { this.currentLine = s + this.currentLine; this.currentToken = null; } private void readToken() { this.fixLines(); if ( this.currentLine == null ) { return; } this.currentLine = this.currentLine.substring( this.tokenLength( this.currentLine ) ); } private int tokenLength( final String s ) { int result; if ( s == null ) { return 0; } for ( result = 0; result < s.length(); result++ ) { if ( result + 3 < s.length() && this.tokenString( s.substring( result, result + 4 ) ) ) { return result == 0 ? 4 : result; } if ( result + 2 < s.length() && this.tokenString( s.substring( result, result + 3 ) ) ) { return result == 0 ? 3 : result; } if ( result + 1 < s.length() && this.tokenString( s.substring( result, result + 2 ) ) ) { return result == 0 ? 2 : result; } if ( result < s.length() && this.tokenChar( s.charAt( result ) ) ) { return result == 0 ? 1 : result; } } return result; //== s.length() } private void fixLines() { this.currentToken = null; if ( this.currentLine == null ) { return; } while ( this.currentLine.equals( "" ) ) { this.currentLine = this.nextLine; this.lineNumber = this.commandStream.getLineNumber(); this.nextLine = this.getNextLine(); if ( this.currentLine == null ) { return; } } this.currentLine = this.currentLine.trim(); if ( this.nextLine == null ) { return; } while ( this.nextLine.equals( "" ) ) { this.nextLine = this.getNextLine(); if ( this.nextLine == null ) { return; } } this.nextLine = this.nextLine.trim(); } private boolean tokenChar( char ch ) { switch ( ch ) { case ' ': case '\t': case '.': case ',': case '{': case '}': case '(': case ')': case '$': case '!': case '~': case '+': case '-': case '=': case '"': case '\'': case '*': case '/': case '%': case '|': case '^': case '&': case '[': case ']': case ';': case '<': case '>': case '?': case ':': case '\u2248': return true; } return false; } private boolean tokenString( final String s ) { return Parser.multiCharTokens.contains( s ); } // **************** Parse errors ***************** private final ScriptException parseException( final String expected, final String actual ) { return this.parseException( "Expected " + expected + ", found " + actual ); } private final ScriptException parseException( final String message ) { return new ScriptException( message + " " + this.getLineAndFile() ); } private final ScriptException parseException2( final String message1, final String message2 ) { return new ScriptException( message1 + " " + this.getLineAndFile() + " " + message2 ); } private final ScriptException undefinedFunctionException( final String name, final List<Value> params ) { return this.parseException( Parser.undefinedFunctionMessage( name, params ) ); } private final ScriptException multiplyDefinedFunctionException( final String name, final List<VariableReference> params ) { StringBuffer buffer = new StringBuffer(); buffer.append( "Function '" ); Parser.appendFunctionDefinition( buffer, name, params ); buffer.append( "' defined multiple times" ); return this.parseException( buffer.toString() ); } private final ScriptException overridesLibraryFunctionException( final String name, final List<VariableReference> params ) { StringBuffer buffer = new StringBuffer(); buffer.append( "Function '" ); Parser.appendFunctionDefinition( buffer, name, params ); buffer.append( "' overrides a library function" ); return this.parseException( buffer.toString() ); } public final ScriptException sinceException( String current, String target, boolean targetIsRevision ) { String template; if ( targetIsRevision ) { template = "'%s' requires revision r%s of kolmafia or higher (current: r%s). Up-to-date builds can be found at http://builds.kolmafia.us/."; } else { template = "'%s' requires version %s of kolmafia or higher (current: %s). Up-to-date builds can be found at http://builds.kolmafia.us/."; } return new ScriptException( String.format( template, this.shortFileName, target, current ) ); } public static final String undefinedFunctionMessage( final String name, final List<Value> params ) { StringBuffer buffer = new StringBuffer(); buffer.append( "Function '" ); Parser.appendFunctionCall( buffer, name, params ); buffer.append( "' undefined. This script may require a more recent version of KoLmafia and/or its supporting scripts." ); return buffer.toString(); } private void enforceSince( String revision ) { int current = StaticEntity.getRevision(); if ( revision.startsWith( "r" ) ) // revision { revision = revision.substring( 1 ); try { int target = Integer.parseInt( revision ); if ( current < target ) { throw this.sinceException( String.valueOf( current ), revision, true ); } } catch ( NumberFormatException e ) { throw this.parseException( "invalid 'since' format" ); } } else // version (or syntax error) { if ( revision.split( "\\." ).length != 2 ) // why don't java strings have a .count() method, this is ridiculous { throw this.parseException( "invalid 'since' format" ); } String currentVersion = StaticEntity.getVersion(); // strip "KoLMafia v" from the front currentVersion = currentVersion.substring( currentVersion.indexOf( "v" ) + 1 ); if ( currentVersion.compareTo( revision ) < 0 ) { throw this.sinceException( currentVersion, revision, false ); } } } public final void warning( final String msg ) { RequestLogger.printLine( "WARNING: " + msg + " " + this.getLineAndFile() ); } private static final void appendFunctionCall( final StringBuffer buffer, final String name, final List<Value> params ) { buffer.append( name ); buffer.append( "(" ); String sep = " "; for ( Value current : params ) { buffer.append( sep ); sep = ", "; buffer.append( current.getType() ); } buffer.append( " )" ); } private static final void appendFunctionDefinition( final StringBuffer buffer, final String name, final List<VariableReference> params ) { buffer.append( name ); buffer.append( "(" ); String sep = " "; for ( Value current : params ) { buffer.append( sep ); sep = ", "; buffer.append( current.getType() ); } buffer.append( " )" ); } private final String getLineAndFile() { return Parser.getLineAndFile( this.shortFileName, this.lineNumber ); } public static final String getLineAndFile( final String fileName, final int lineNumber ) { if ( fileName == null ) { return "(" + Preferences.getString( "commandLineNamespace" ) + ")"; } return "(" + fileName + ", line " + lineNumber + ")"; } public static void printIndices( final List<Value> indices, final PrintStream stream, final int indent ) { if ( indices == null ) { return; } for ( Value current : indices ) { Interpreter.indentLine( stream, indent ); stream.println( "<KEY>" ); current.print( stream, indent + 1 ); } } }