package com.laytonsmith.core; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.core.constructs.CArray; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.CFunction; import com.laytonsmith.core.constructs.CNull; import com.laytonsmith.core.constructs.CVoid; import com.laytonsmith.core.constructs.Construct; import com.laytonsmith.core.constructs.IVariable; import com.laytonsmith.core.constructs.IVariableList; import com.laytonsmith.core.constructs.InstanceofUtil; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.environments.GlobalEnv; import com.laytonsmith.core.exceptions.CRE.AbstractCREException; import com.laytonsmith.core.exceptions.CRE.CRECastException; import com.laytonsmith.core.exceptions.CRE.CREFormatException; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; import com.laytonsmith.core.exceptions.FunctionReturnException; import com.laytonsmith.core.exceptions.LoopManipulationException; import com.laytonsmith.core.exceptions.StackTraceManager; import com.laytonsmith.core.functions.DataHandling; import com.laytonsmith.core.functions.Function; import com.laytonsmith.core.functions.FunctionBase; import com.laytonsmith.core.functions.FunctionList; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; /** * A procedure is a user defined function, essentially. Unlike a closure, however, it does not * clone a reference to the environment when it is defined. It takes on the environment characteristics * of the executing environment, not the defining environment. */ public class Procedure implements Cloneable { private final String name; private Map<String, IVariable> varList; private final Map<String, Construct> originals = new HashMap<>(); private final List<IVariable> varIndex = new ArrayList<>(); private ParseTree tree; private CClassType returnType; private boolean possiblyConstant = false; private static final Pattern PROCEDURE_NAME_REGEX = Pattern.compile("^_[\\p{L}0-9]+[\\p{L}_0-9]*"); /** * The line the procedure is defined at (for stacktraces) */ private final Target definedAt; public Procedure(String name, CClassType returnType, List<IVariable> varList, ParseTree tree, Target t) { this.name = name; this.definedAt = t; this.varList = new HashMap<>(); for (IVariable var : varList) { try { this.varList.put(var.getVariableName(), var.clone()); } catch (CloneNotSupportedException e) { this.varList.put(var.getVariableName(), var); } this.varIndex.add(var); this.originals.put(var.getVariableName(), var.ival()); } this.tree = tree; if (!PROCEDURE_NAME_REGEX.matcher(name).matches()) { throw new CREFormatException("Procedure names must start with an underscore, and may only contain letters, underscores, and digits. (Found " + this.name + ")", t); } //Let's look through the tree now, and see if this is possibly constant or not. //If it is, it may or may not help us during compilation, but if it's not, //we can be sure that we cannot inline this in any way. this.possiblyConstant = checkPossiblyConstant(tree); this.returnType = returnType; } private boolean checkPossiblyConstant(ParseTree tree) { //TODO: This whole thing is a mess. Instead of doing it this way, //individual procs need to be inlined as deemed appropriate. if(true){ return false; } if (!tree.getData().isDynamic()) { //If it isn't dynamic, it certainly could be constant return true; } else if (tree.getData() instanceof IVariable) { //Variables will return true for isDynamic, but they are technically constant, because //they are being declared in this scope, or passed in. An import() would break this //contract, but import() itself is dynamic, so this is not an issue. return true; } else if (tree.getData() instanceof CFunction) { //If the function itself is not optimizable, we needn't recurse. try { FunctionBase fb = FunctionList.getFunction(tree.getData()); if (fb instanceof Function) { Function f = (Function) fb; if (f instanceof DataHandling._return) { //This is a special exception. Return itself is not optimizable, //but if the contents are optimizable, it is still considered constant. if (!tree.hasChildren()) { return true; } else { return checkPossiblyConstant(tree.getChildAt(0)); } } //If it's optimizable, it's possible. If it's restricted, it doesn't matter, because //we can't optimize it out anyways, because we need to do the permission check Set<Optimizable.OptimizationOption> o = EnumSet.noneOf(Optimizable.OptimizationOption.class); if(f instanceof Optimizable){ o = ((Optimizable)f).optimizationOptions(); } if (!( ( o != null && (o.contains(Optimizable.OptimizationOption.OPTIMIZE_DYNAMIC) || o.contains(Optimizable.OptimizationOption.OPTIMIZE_CONSTANT))) && !f.isRestricted() )) { return false; //Nope. Doesn't matter if the children are or not } } else { return false; } } catch (ConfigCompileException e) { //It's a proc. We will treat this just like any other function call, } //Ok, well, we have to check the children first. for (ParseTree child : tree.getChildren()) { if (!checkPossiblyConstant(child)) { return false; //Nope, since our child can't be constant, neither can we } } //They all check out, so, yep, we could possibly be constant return true; } else { //Uh. Ok, well, nope. return false; } } public boolean isPossiblyConstant() { return this.possiblyConstant; } public String getName() { return name; } @Override public String toString() { return name + "(" + StringUtils.Join(varList.keySet(), ", ") + ")"; } /** * Convenience wrapper around executing a procedure if the parameters are in * tree mode still. * * @param args * @param env * @param t * @return */ public Construct cexecute(List<ParseTree> args, Environment env, Target t) { List<Construct> list = new ArrayList<>(); for (ParseTree arg : args) { list.add(env.getEnv(GlobalEnv.class).GetScript().seval(arg, env)); } return execute(list, env, t); } /** * Executes this procedure, with the arguments that were passed in * * @param args * @param env * @param t * @return */ public Construct execute(List<Construct> args, Environment env, Target t) { env.getEnv(GlobalEnv.class).SetVarList(new IVariableList()); //This is what will become our @arguments var CArray arguments = new CArray(Target.UNKNOWN); for (String key : originals.keySet()) { Construct c = originals.get(key); env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CClassType.AUTO, key, c, Target.UNKNOWN)); arguments.push(c, t); } Script fakeScript = Script.GenerateScript(tree, env.getEnv(GlobalEnv.class).GetLabel());//new Script(null, null); for (int i = 0; i < args.size(); i++) { Construct c = args.get(i); arguments.set(i, c, t); if (varIndex.size() > i) { String varname = varIndex.get(i).getVariableName(); if(c instanceof CNull || InstanceofUtil.isInstanceof(c, varIndex.get(i).getDefinedType()) || varIndex.get(i).getDefinedType().equals(CClassType.AUTO)){ env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(varIndex.get(i).getDefinedType(), varname, c, c.getTarget())); } else { throw new CRECastException("Procedure \"" + name + "\" expects a value of type " + varIndex.get(i).getDefinedType().val() + " in argument " + (i + 1) + ", but" + " a value of type " + c.typeof() + " was found instead.", c.getTarget()); } } } env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(new CClassType("array", Target.UNKNOWN), "@arguments", arguments, Target.UNKNOWN)); StackTraceManager stManager = env.getEnv(GlobalEnv.class).GetStackTraceManager(); stManager.addStackTraceElement(new ConfigRuntimeException.StackTraceElement("proc " + name, getTarget())); try { if(tree.getData() instanceof CFunction && "sconcat".equals(tree.getData().val())){ //If the inner tree is just an sconcat, we can optimize by //simply running the arguments to the sconcat. We're not going //to use the results, after all, and this is a common occurance, //because the compiler will often put it there automatically. //We *could* optimize this by removing it from the compiled code, //and we still should do that, but this check is quick enough, //and so can remain even once we do add the optimization to the //compiler proper. for(ParseTree child : tree.getChildren()){ fakeScript.eval(child, env); } } else { fakeScript.eval(tree, env); } } catch (FunctionReturnException e) { // Normal exit stManager.popStackTraceElement(); Construct ret = e.getReturn(); if(!InstanceofUtil.isInstanceof(ret, returnType)){ throw new CRECastException("Expected procedure \"" + name + "\" to return a value of type " + returnType.val() + " but a value of type " + ret.typeof() + " was returned instead", ret.getTarget()); } return ret; } catch(LoopManipulationException ex){ // Not exactly normal, but pop anyways stManager.popStackTraceElement(); // These cannot bubble up past procedure calls. This will eventually be // a compile error. throw ConfigRuntimeException.CreateUncatchableException("Loop manipulation operations (e.g. break() or continue()) cannot" + " bubble up past procedures.", t); } catch(ConfigRuntimeException e){ if(e instanceof AbstractCREException){ ((AbstractCREException)e).freezeStackTraceElements(stManager); } stManager.popStackTraceElement(); throw e; } catch(Throwable th){ // Not sure. Pop, but rethrow stManager.popStackTraceElement(); throw th; } // Normal exit, but no return. stManager.popStackTraceElement(); // If we got here, then there was no return value. This is fine, but only for returnType void or auto. if(!(returnType.equals(CClassType.AUTO) || returnType.equals(CClassType.VOID))){ throw new CRECastException("Expecting procedure \"" + name + "\" to return a value of type " + returnType.val() + "," + " but no value was returned.", tree.getTarget()); } return CVoid.VOID; } public Target getTarget(){ return definedAt; } @Override public Procedure clone() throws CloneNotSupportedException { Procedure clone = (Procedure) super.clone(); if (this.varList != null) { clone.varList = new HashMap<>(this.varList); } if (this.tree != null) { clone.tree = this.tree.clone(); } return clone; } public void definitelyNotConstant() { possiblyConstant = false; } }