package com.laytonsmith.core.functions; import com.laytonsmith.PureUtilities.Version; import com.laytonsmith.annotations.api; import com.laytonsmith.annotations.breakable; import com.laytonsmith.annotations.core; import com.laytonsmith.annotations.hide; import com.laytonsmith.annotations.noboilerplate; import com.laytonsmith.annotations.nolinking; import com.laytonsmith.annotations.noprofile; import com.laytonsmith.annotations.seealso; import com.laytonsmith.annotations.unbreakable; import com.laytonsmith.core.ArgumentValidation; import com.laytonsmith.core.CHLog; import com.laytonsmith.core.CHVersion; import com.laytonsmith.core.Globals; import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.MethodScriptCompiler; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; import com.laytonsmith.core.Procedure; import com.laytonsmith.core.Script; import com.laytonsmith.core.Static; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.compiler.keywords.InKeyword; import com.laytonsmith.core.constructs.CArray; import com.laytonsmith.core.constructs.CBoolean; import com.laytonsmith.core.constructs.CByteArray; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.CClosure; import com.laytonsmith.core.constructs.CDouble; import com.laytonsmith.core.constructs.CFunction; import com.laytonsmith.core.constructs.CIClosure; import com.laytonsmith.core.constructs.CInt; import com.laytonsmith.core.constructs.CKeyword; import com.laytonsmith.core.constructs.CLabel; import com.laytonsmith.core.constructs.CMutablePrimitive; import com.laytonsmith.core.constructs.CNull; import com.laytonsmith.core.constructs.CSlice; import com.laytonsmith.core.constructs.CString; 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.CommandHelperEnvironment; import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.environments.GlobalEnv; import com.laytonsmith.core.exceptions.CRE.CRECastException; import com.laytonsmith.core.exceptions.CRE.CREFormatException; import com.laytonsmith.core.exceptions.CRE.CREIllegalArgumentException; import com.laytonsmith.core.exceptions.CRE.CREIncludeException; import com.laytonsmith.core.exceptions.CRE.CREIndexOverflowException; import com.laytonsmith.core.exceptions.CRE.CREInsufficientArgumentsException; import com.laytonsmith.core.exceptions.CRE.CREInvalidProcedureException; import com.laytonsmith.core.exceptions.CRE.CRERangeException; import com.laytonsmith.core.exceptions.CRE.CREThrowable; import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigCompileGroupException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; import com.laytonsmith.core.exceptions.FunctionReturnException; import com.laytonsmith.core.exceptions.LoopBreakException; import com.laytonsmith.core.exceptions.LoopContinueException; import com.laytonsmith.core.natives.interfaces.ArrayAccess; import com.laytonsmith.tools.docgen.templates.ArrayIteration; import com.laytonsmith.tools.docgen.templates.Loops; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * */ @core public class DataHandling { private static final String array_get = new ArrayHandling.array_get().getName(); private static final String array_set = new ArrayHandling.array_set().getName(); private static final String array_push = new ArrayHandling.array_push().getName(); public static String docs() { return "This class provides various methods to control script data and program flow."; } @api @seealso({com.laytonsmith.tools.docgen.templates.Arrays.class, ArrayIteration.class}) public static class array extends AbstractFunction implements Optimizable { @Override public String getName() { return "array"; } @Override public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } @Override public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { return new CArray(t, args); } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public String docs() { return "array {[var1, [var2...]]} Creates an array of values."; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_0_1; } @Override public Boolean runAsync() { return null; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "assign(@array, array(1, 2, 3))\nmsg(@array)"), new ExampleScript("Associative array creation", "assign(@array, array(one: 'apple', two: 'banana'))\nmsg(@array)"),}; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC); } FileOptions lastFileOptions = null; @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { //We need to check here to ensure that //we aren't getting a slice in a label, which is used in switch //statements, but doesn't make sense here. //Also check for dynamic labels for (ParseTree child : children) { if (child.getData() instanceof CFunction && new Compiler.centry().getName().equals(child.getData().val())) { if (((CLabel) child.getChildAt(0).getData()).cVal() instanceof CSlice) { throw new ConfigCompileException("Slices cannot be used as array indices", child.getChildAt(0).getTarget()); } if (((CLabel) child.getChildAt(0).getData()).cVal() instanceof IVariable) { String array = "@a"; String valueName = ((IVariable)((CLabel) child.getChildAt(0).getData()).cVal()).getVariableName(); Construct value = child.getChildAt(1).getData(); String v; if(value instanceof IVariable) { v = ((IVariable)value).getVariableName(); } else if(value instanceof CString) { v = ((CString)value).getQuote(); } else { v = "@value"; } if("@a".equals(valueName)) { array = "@myArray"; } throw new ConfigCompileException("Dynamic values cannot be used as indices in array construction." + "\nTo make dynamic indicies, do the following: " + "array " + array + " = array(); " + array + "[" + valueName + "] = " + v + ";", t); } } } return null; } } @api @seealso({com.laytonsmith.tools.docgen.templates.Arrays.class, ArrayIteration.class}) public static class associative_array extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { CArray array = CArray.GetAssociativeArray(t, args); return array; } @Override public String getName() { return "associative_array"; } @Override public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } @Override public String docs() { return "array {[args...]} Works exactly like array(), except the array created will be an associative array, even" + " if the array has been created with no elements. This is the only use case where this is neccessary, vs" + " using the normal array() function, or in the case where you assign sequential keys anyways, and the same" + " array could have been created using array()."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Usage with an empty array", "assign(@array, associative_array())\nmsg(is_associative(@array))"), new ExampleScript("Usage with an array with sequential keys", "assign(@array, array(0: '0', 1: '1'))\nmsg(is_associative(@array))\n" + "assign(@array, associative_array(0: '0', 1: '1'))\nmsg(is_associative(@array))"),}; } } @api @seealso({com.laytonsmith.tools.docgen.templates.Variables.class}) public static class assign extends AbstractFunction implements Optimizable { @Override public String getName() { return "assign"; } @Override public Integer[] numArgs() { return new Integer[]{2, 3}; } @Override public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { IVariableList list = env.getEnv(GlobalEnv.class).GetVarList(); int offset; CClassType type; String name; if (args.length == 3) { offset = 1; if (!(args[offset] instanceof IVariable)) { throw new CRECastException(getName() + " with 3 arguments only accepts an ivariable as the second argument.", t); } name = ((IVariable) args[offset]).getVariableName(); if (list.has(name) && env.getEnv(GlobalEnv.class).GetFlag("no-check-duplicate-assign") == null) { if (env.getEnv(GlobalEnv.class).GetFlag("closure-warn-overwrite") != null) { CHLog.GetLogger().Log(CHLog.Tags.RUNTIME, LogLevel.ERROR, "The variable " + name + " is hiding another value of the" + " same name in the main scope.", t); } else { CHLog.GetLogger().Log(CHLog.Tags.RUNTIME, LogLevel.ERROR, name + " was already defined at " + list.get(name, t, true).getDefinedTarget() + " but is being redefined.", t); } } type = ArgumentValidation.getClassType(args[0], t); } else { offset = 0; if (!(args[offset] instanceof IVariable)) { throw new CRECastException(getName() + " with 2 arguments only accepts an ivariable as the second argument.", t); } name = ((IVariable) args[offset]).getVariableName(); type = list.get(name, t, true).getDefinedType(); } Construct c = args[offset + 1]; while (c instanceof IVariable) { IVariable cur = (IVariable) c; c = list.get(cur.getVariableName(), cur.getTarget()).ival(); } IVariable v = new IVariable(type, name, c, t); list.set(v); return v; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public String docs() { return "ivariable {[type], ivar, mixed} Accepts an ivariable ivar as a parameter, and puts the specified value mixed in it." + " Returns the variable that was assigned. Operator syntax is also supported: <code>@a = 5;</code>." + " Other forms are supported as well, +=, -=, *=, /=, .=, which do multiple operations at once. Array assigns" + " are also supported: @array[5] = 'new value in index 5';"; } @Override public boolean isRestricted() { return false; } @Override public boolean preResolveVariables() { return false; } @Override public CHVersion since() { return CHVersion.V3_0_1; } @Override public Boolean runAsync() { return null; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.OPTIMIZE_CONSTANT, OptimizationOption.OPTIMIZE_DYNAMIC ); } @Override public Construct optimize(Target t, Construct... args) throws ConfigCompileException { //We can't really optimize, but we can check that we are //getting an ivariable. int offset = 0; if (args.length == 3) { offset = 1; if (!(args[0] instanceof CClassType)) { throw new ConfigCompileException("Expecting a ClassType for parameter 1 to assign", t); } } if(args.length > 0 && !(args[offset] instanceof IVariable)) { throw new ConfigCompileException("Expecting an ivar for argument 1 to assign", t); } return null; } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { //Check for too few arguments if (children.size() < 2) { return null; } if (children.get(0).getData() instanceof IVariable && children.get(1).getData() instanceof IVariable) { if (((IVariable) children.get(0).getData()).getVariableName().equals( ((IVariable) children.get(1).getData()).getVariableName())) { CHLog.GetLogger().Log(CHLog.Tags.COMPILER, LogLevel.WARNING, "Assigning a variable to itself", t); } } if (children.get(0).getData() instanceof CFunction && array_get.equals(children.get(0).getData().val())) { if (children.get(0).getChildAt(1).getData() instanceof CSlice) { CSlice cs = (CSlice) children.get(0).getChildAt(1).getData(); if (cs.getStart() == 0 && cs.getFinish() == -1) { //Turn this into an array_push ParseTree tree = new ParseTree(new CFunction(array_push, t), children.get(0).getFileOptions()); tree.addChild(children.get(0).getChildAt(0)); tree.addChild(children.get(1)); return tree; } //else, not really sure what's going on, so we'll just carry on, and probably there //will be an error generated elsewhere } else { //Turn this into an array set instead ParseTree tree = new ParseTree(new CFunction(array_set, t), children.get(0).getFileOptions()); tree.addChild(children.get(0).getChildAt(0)); tree.addChild(children.get(0).getChildAt(1)); tree.addChild(children.get(1)); return tree; } } return null; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "assign(@variable, 5);\nmsg(@variable);"), new ExampleScript("Array assignment", "assign(@variable, associative_array());\nassign(@variable['associative'], 5);\nmsg(@variable);"), new ExampleScript("Operator syntax", "@variable = 5;\nmsg(@variable);"), new ExampleScript("Operator syntax using combined operators", "@variable = 'string';\n@variable .= ' more string';\nmsg(@variable);"), new ExampleScript("Operator syntax using combined operators", "@variable = 5;\n@variable += 10;\nmsg(@variable);"), new ExampleScript("Operator syntax using combined operators", "@variable = 5;\n@variable -= 10;\nmsg(@variable);"), new ExampleScript("Operator syntax using combined operators", "@variable = 5;\n@variable *= 10;\nmsg(@variable);"), new ExampleScript("Operator syntax using combined operators", "@variable = 5;\n@variable /= 10;\nmsg(@variable);"),}; } } @api @noboilerplate @breakable @seealso({com.laytonsmith.tools.docgen.templates.Loops.class, com.laytonsmith.tools.docgen.templates.ArrayIteration.class}) public static class _for extends AbstractFunction implements Optimizable { @Override public String getName() { return "for"; } @Override public Integer[] numArgs() { return new Integer[]{4}; } @Override public Construct exec(Target t, Environment env, Construct... args) { return CVoid.VOID; } @Override public boolean useSpecialExec() { return true; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { return new forelse(true).execs(t, env, parent, nodes); } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public String docs() { return "void {assign, condition, expression1, expression2} Acts as a typical for loop. The assignment is first run. Then, a" + " condition is checked. If that condition is checked and returns true, expression2 is run. After that, expression1 is run. In java" + " syntax, this would be: for(assign; condition; expression1){expression2}. assign must be an ivariable, either a " + "pre defined one, or the results of the assign() function. condition must be a boolean."; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_0_1; } //Doesn't matter, run out of state @Override public Boolean runAsync() { return null; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "for(assign(@i, 0), @i < 5, @i++,\n\tmsg(@i)\n)"), new ExampleScript("With braces", "for(assign(@i, 0), @i < 2, @i++){\n\tmsg(@i)\n}"), new ExampleScript("With continue. (See continue() for more examples)", "for(assign(@i, 0), @i < 2, @i++){\n" + "\tif(@i == 1, continue())\n" + "\tmsg(@i)\n" + "}"),}; } @Override public LogLevel profileAt() { return LogLevel.WARNING; } @Override public String profileMessageS(List<ParseTree> args) { return "Executing function: " + this.getName() + "(" + args.get(0).toStringVerbose() + ", " + args.get(1).toStringVerbose() + ", " + args.get(2).toStringVerbose() + ", <code>)"; } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { //In for(@i = 0, @i < @x, @i++, ...), the @i++ is more optimally written as ++@i, but //it is commonplace to use postfix operations, so if the condition is in fact that simple, //let's reverse it. boolean isInc; try { if (children.get(2).getData() instanceof CFunction && ((isInc = children.get(2).getData().val().equals("postinc")) || children.get(2).getData().val().equals("postdec")) && children.get(2).getChildAt(0).getData() instanceof IVariable) { ParseTree pre = new ParseTree(new CFunction(isInc ? "inc" : "dec", t), children.get(2).getFileOptions()); pre.addChild(children.get(2).getChildAt(0)); children.set(2, pre); } } catch (IndexOutOfBoundsException e) { //Just ignore it. It's a compile error, but we'll let the rest of the //existing system sort that out. } return null; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC); } } @api @noboilerplate @breakable public static class forelse extends AbstractFunction { public forelse() { } boolean runAsFor = false; forelse(boolean runAsFor) { this.runAsFor = runAsFor; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public boolean useSpecialExec() { return true; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return null; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) throws ConfigRuntimeException { ParseTree assign = nodes[0]; ParseTree condition = nodes[1]; ParseTree expression = nodes[2]; ParseTree runnable = nodes[3]; ParseTree elseCode = null; if (!runAsFor) { elseCode = nodes[4]; } boolean hasRunOnce = false; Construct counter = parent.eval(assign, env); if (!(counter instanceof IVariable)) { throw new CRECastException("First parameter of for must be an ivariable", t); } int _continue = 0; while (true) { boolean cond = Static.getBoolean(parent.seval(condition, env)); if (cond == false) { break; } hasRunOnce = true; if (_continue >= 1) { --_continue; parent.eval(expression, env); continue; } try { parent.eval(runnable, env); } catch (LoopBreakException e) { int num = e.getTimes(); if (num > 1) { e.setTimes(--num); throw e; } return CVoid.VOID; } catch (LoopContinueException e) { _continue = e.getTimes() - 1; parent.eval(expression, env); continue; } parent.eval(expression, env); } if (!hasRunOnce && !runAsFor && elseCode != null) { parent.eval(elseCode, env); } return CVoid.VOID; } @Override public String getName() { return "forelse"; } @Override public Integer[] numArgs() { return new Integer[]{5}; } @Override public String docs() { return "void {assign, condition, expression1, expression2, else} Works like a normal for loop, but if upon checking the condition the first time," + " it is determined that it is false (that is, NO code loops are going to be run) the else code is run instead. If the loop runs," + " even once, it will NOT run the else branch. In general, brace syntax and use of for(){ } else { } syntax is preferred, instead" + " of using forelse directly."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } } @api(environments = CommandHelperEnvironment.class) @breakable @seealso({com.laytonsmith.tools.docgen.templates.Loops.class, ArrayIteration.class}) public static class foreach extends AbstractFunction implements Optimizable { @Override public String getName() { return "foreach"; } @Override public Integer[] numArgs() { return new Integer[]{2, 3, 4}; } @Override public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { return CVoid.VOID; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { if (nodes.length < 3) { throw new CREInsufficientArgumentsException("Insufficient arguments passed to " + getName(), t); } ParseTree array = nodes[0]; ParseTree key = null; int offset = 0; if (nodes.length == 4) { //Key and value provided key = nodes[1]; offset = 1; } ParseTree value = nodes[1 + offset]; ParseTree code = nodes[2 + offset]; Construct arr = parent.seval(array, env); Construct ik = null; if (key != null) { ik = parent.eval(key, env); if (!(ik instanceof IVariable)) { throw new CRECastException("Parameter 2 of " + getName() + " must be an ivariable", t); } } Construct iv = parent.eval(value, env); if (arr instanceof CSlice) { long start = ((CSlice) arr).getStart(); long finish = ((CSlice) arr).getFinish(); if (finish < start) { arr = new ArrayHandling.range().exec(t, env, new CInt(start, t), new CInt(finish - 1, t), new CInt(-1, t)); } else { arr = new ArrayHandling.range().exec(t, env, new CInt(start, t), new CInt(finish + 1, t)); } } if (!(arr instanceof ArrayAccess)) { throw new CRECastException("Parameter 1 of " + getName() + " must be an array or array like data structure", t); } if (!(iv instanceof IVariable)) { throw new CRECastException("Parameter " + (2 + offset) + " of " + getName() + " must be an ivariable", t); } ArrayAccess one = (ArrayAccess) arr; IVariable kkey = (IVariable) ik; IVariable two = (IVariable) iv; if (one.isAssociative()) { //Iteration of an associative array is much easier, and we have //special logic here to decrease the complexity. //Clone the set, so changes in the array won't cause changes in //the iteration order. Set<Construct> keySet = new LinkedHashSet<>(one.keySet()); //Continues in an associative array are slightly different, so //we have to track this differently. Basically, we skip the //next element in the array key set. int continues = 0; for (Construct c : keySet) { if (continues > 0) { //If continues is greater than 0, continue in the loop, //however many times necessary to make it 0. continues--; continue; } //If the key isn't null, set that in the variable table. if (kkey != null) { env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(kkey.getDefinedType(), kkey.getVariableName(), c, t)); } //Set the value in the variable table env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(two.getDefinedType(), two.getVariableName(), one.get(c.val(), t), t)); try { //Execute the code parent.eval(code, env); //And handle any break/continues. } catch (LoopBreakException e) { int num = e.getTimes(); if (num > 1) { e.setTimes(--num); throw e; } return CVoid.VOID; } catch (LoopContinueException e) { // In associative arrays, (unlike with normal arrays) we need to decrement it by one, because the nature of // the normal array is such that the counter is handled manually by our code. Because we are letting java // handle our code though, this run actually counts as one run. continues += e.getTimes() - 1; } } return CVoid.VOID; } else { //It's not associative, so we have more complex handling. We will create an ArrayAccessIterator, //and store that in the environment. As the array is iterated, underlying changes in the array //will be reflected in the object, and we will adjust as necessary. The reason we use this mechanism //is to avoid cloning the array, and iterating that. Arrays may be extremely large, and cloning the //entire array is wasteful in that case. We are essentially tracking deltas this way, which prevents //memory usage from getting out of hand. ArrayAccess.ArrayAccessIterator iterator = new ArrayAccess.ArrayAccessIterator(one); List<ArrayAccess.ArrayAccessIterator> arrayAccessList = env.getEnv(GlobalEnv.class).GetArrayAccessIterators(); try { arrayAccessList.add(iterator); int continues = 0; while (true) { int current = iterator.getCurrent(); if (continues > 0) { //We have some continues to handle. Blacklisted //values don't count for the continuing count, so //we have to consider that when counting. iterator.incrementCurrent(); if (iterator.isBlacklisted(current)) { continue; } else { --continues; continue; } } if (current >= one.size()) { //Done with the iterations. break; } //If the item is blacklisted, we skip it. if (!iterator.isBlacklisted(current)) { if (kkey != null) { env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(kkey.getDefinedType(), kkey.getVariableName(), new CInt(current, t), t)); } env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(two.getDefinedType(), two.getVariableName(), one.get(current, t), t)); try { parent.eval(code, env); } catch (LoopBreakException e) { int num = e.getTimes(); if (num > 1) { e.setTimes(--num); throw e; } return CVoid.VOID; } catch (LoopContinueException e) { continues += e.getTimes(); continue; } } iterator.incrementCurrent(); } } finally { arrayAccessList.remove(iterator); } } return CVoid.VOID; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class, CRERangeException.class}; } @Override public String docs() { return "void {array, [key], ivar, code} Walks through array, setting ivar equal to each element in the array, then running code." + " In addition, foreach(1..4, @i, code()) is also valid, setting @i to 1, 2, 3, 4 each time. The same syntax is valid as" + " in an array slice. If key is set (it must be an ivariable) then the index of each iteration will be set to that." + " See the examples for a demonstration. ---- " + " Enhanced syntax may also be used in foreach, using the \"in\", \"as\" and \"else\" keywords. See the examples for" + " examples of each structure. Using these keywords makes the structure of the foreach read much better. For instance," + " with foreach(@value in @array){ } the code very literally reads \"for each value in array\", making ascertaining" + " the behavior of the loop easier. The \"as\" keyword reads less plainly, and so is not recommended for use, but is" + " allowed. Note that the array and value are reversed with the \"as\" keyword. An \"else\" block may be used after" + " the foreach, which will only run if the array provided is empty, that is, the loop code would never run. This provides" + " a good way to provide \"default\" handling. Array modifications while iterating are supported, and are well defined." + " See [[CommandHelper/Staged/Array_iteration|the page documenting array iterations]] for full details."; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_0_1; } //Doesn't matter, runs out of state anyways @Override public Boolean runAsync() { return null; } @Override public boolean useSpecialExec() { return true; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Using \"in\" keyword", "@array = array(1, 2, 3);\n" + "foreach(@value in @array){\n" + "\tmsg(@value);\n" + "}"), new ExampleScript("Using \"in\" keyword, with a key", "@array = array(1, 2, 3);\n" + "foreach(@key: @value in @array){\n" + "\tmsg(@key . ': ' . @value);\n" + "}"), new ExampleScript("Using \"a\" keyword", "@array = array(1, 2, 3);\n" + "foreach(@array as @value){\n" + "\tmsg(@value);\n" + "}"), new ExampleScript("Using \"as\" keyword, with a key", "@array = array(1, 2, 3);\n" + "foreach(@array as @key: @value){\n" + "\tmsg(@key . ': ' . @value);\n" + "}"), new ExampleScript("With else clause", "@array = array() # Note empty array\n" + "foreach(@value in @array){\n" + "\tmsg(@value);\n" + "} else {\n" + "\tmsg('No values were in the array');\n" + "}"), new ExampleScript("Basic functional usage", "assign(@array, array(1, 2, 3))\nforeach(@array, @i,\n\tmsg(@i)\n)"), new ExampleScript("With braces", "assign(@array, array(1, 2, 3))\nforeach(@array, @i){\n\tmsg(@i)\n}"), new ExampleScript("With a slice", "foreach(1..3, @i){\n\tmsg(@i)\n}"), new ExampleScript("With a slice, counting down", "foreach(3..1, @i){\n\tmsg(@i)\n}"), new ExampleScript("With array keys", "@array = array('one': 1, 'two': 2)\nforeach(@array, @key, @value){\n\tmsg(@key.':'.@value)\n}"),}; } @Override public LogLevel profileAt() { return LogLevel.WARNING; } @Override public String profileMessageS(List<ParseTree> args) { return "Executing function: " + this.getName() + "(" + args.get(0).toStringVerbose() + ", " + args.get(1).toStringVerbose() + ", <code>)"; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC); } private static final String CENTRY = new Compiler.centry().getName(); private static final String ASSIGN = new assign().getName(); private static final String SCONCAT = new StringHandling.sconcat().getName(); private static final String IN = new InKeyword().getKeywordName(); private boolean isFunction(ParseTree node, String function) { return node.getData() instanceof CFunction && node.getData().val().equals(function); } private boolean isKeyword(ParseTree node, String keyword) { return node.getData() instanceof CKeyword && node.getData().val().equals(keyword); } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { if (children.size() < 2) { throw new ConfigCompileException("Invalid number of arguments passed to " + getName(), t); } if (isFunction(children.get(0), CENTRY)) { // This is what "@key: @value in @array" looks like initially. We'll refactor this so the next segment can take over properly. ParseTree sconcat = new ParseTree(new CFunction(new StringHandling.sconcat().getName(), t), fileOptions); sconcat.addChild(children.get(0).getChildAt(0)); for (int i = 0; i < children.get(0).getChildAt(1).numberOfChildren(); i++) { sconcat.addChild(children.get(0).getChildAt(1).getChildAt(i)); } children.set(0, sconcat); } if (children.get(0).getData() instanceof CFunction && children.get(0).getData().val().equals(new StringHandling.sconcat().getName())) { // We may be looking at a "@value in @array" or "@array as @value" type // structure, so we need to re-arrange this into the standard format. ParseTree array = null; ParseTree key = null; ParseTree value = null; List<ParseTree> c = children.get(0).getChildren(); if (c.size() == 3) { // No key specified switch (c.get(1).getData().val()) { case "in": // @value in @array value = c.get(0); array = c.get(2); break; case "as": // @array as @value value = c.get(2); array = c.get(0); break; } } else if (c.size() == 4) { if ("in".equals(c.get(2).getData().val())) { // @key: @value in @array key = c.get(0); value = c.get(1); array = c.get(3); } else if ("as".equals(c.get(1).getData().val())) { // @array as @key: @value array = c.get(0); key = c.get(2); value = c.get(3); } } if (key != null && key.getData() instanceof CLabel) { if (!(((CLabel) key.getData()).cVal() instanceof IVariable) && !(((CLabel) key.getData()).cVal() instanceof CFunction && ((CLabel) key.getData()).cVal().val().equals(ASSIGN))) { throw new ConfigCompileException("Expected a variable for key, but \"" + key.getData().val() + "\" was found", t); } key.setData(((CLabel) key.getData()).cVal()); } // Now set up the new tree, and return that. Since foreachelse overrides us, we // need to accept all the arguments after the first, and put those in. List<ParseTree> newChildren = new ArrayList<>(); newChildren.add(array); if (key != null) { newChildren.add(key); } newChildren.add(value); for (int i = 1; i < children.size(); i++) { newChildren.add(children.get(i)); } children.clear(); children.addAll(newChildren); // Change foreach(){ ... } else { ... } to a foreachelse. if (children.get(children.size() - 1).getData() instanceof CFunction && children.get(children.size() - 1).getData().val().equals("else")) { ParseTree foreachelse = new ParseTree(new CFunction(new foreachelse().getName(), t), fileOptions); children.set(children.size() - 1, children.get(children.size() - 1).getChildAt(0)); foreachelse.setChildren(children); return foreachelse; } } return null; } } @api @noboilerplate @breakable @seealso({foreach.class, Loops.class, ArrayIteration.class}) public static class foreachelse extends foreach { @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { ParseTree array = nodes[0]; //The last one ParseTree elseCode = nodes[nodes.length - 1]; Construct data = parent.seval(array, env); if (!(data instanceof CArray) && !(data instanceof CSlice)) { throw new CRECastException(getName() + " expects an array for parameter 1", t); } if (((CArray) data).isEmpty()) { parent.eval(elseCode, env); } else { ParseTree pass[] = new ParseTree[nodes.length - 1]; System.arraycopy(nodes, 0, pass, 0, nodes.length - 1); nodes[0] = new ParseTree(data, null); return super.execs(t, env, parent, pass); } return CVoid.VOID; } @Override public String getName() { return "foreachelse"; } @Override public Integer[] numArgs() { return new Integer[]{4, 5}; } @Override public String docs() { return "void {array, ivar, code, else} Works like a foreach, except if the array is empty, the else code runs instead. That is, if the code" + " would not run at all, the else condition would. In general, brace syntax and use of foreach(){ } else { } syntax is preferred, instead" + " of using foreachelse directly."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage, with the else code not running", "@array = array(1, 2, 3)\n" + "foreachelse(@array, @val,\n" + " msg(@val)\n" + ", #else \n" + " msg('No values in the array')\n" + ")"), new ExampleScript("Empty array, so else block running", "@array = array()\n" + "foreachelse(@array, @val,\n" + " msg(@val)\n" + ", #else \n" + " msg('No values in the array')\n" + ")"),}; } } @api @noboilerplate @breakable @seealso({com.laytonsmith.tools.docgen.templates.Loops.class}) public static class _while extends AbstractFunction { @Override public String getName() { return "while"; } @Override public String docs() { return "void {condition, [code]} While the condition is true, the code is executed. break and continue work" + " inside a dowhile, but continuing more than once is pointless, since the loop isn't inherently" + " keeping track of any counters anyways. Breaking multiple times still works however."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public Integer[] numArgs() { return new Integer[]{1, 2}; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { try { while (Static.getBoolean(parent.seval(nodes[0], env))) { //We allow while(thing()); to be done. This makes certain //types of coding styles possible. if (nodes.length > 1) { try { parent.seval(nodes[1], env); } catch (LoopContinueException e) { //ok. } } } } catch (LoopBreakException e) { if (e.getTimes() > 1) { throw new LoopBreakException(e.getTimes() - 1, t); } } return CVoid.VOID; } @Override public boolean useSpecialExec() { return true; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return CNull.NULL; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "assign(@i, 5)\nwhile(@i > 0,\n" + "\tmsg(@i)\n" + "\t@i--\n" + ")"), new ExampleScript("With a break", "assign(@i, 0)\nwhile(true,\n" + "\tmsg(@i)\n" + "\t@i++\n" + "\tif(@i > 5, break())\n" + ")"),}; } @Override public LogLevel profileAt() { return LogLevel.WARNING; } @Override public String profileMessageS(List<ParseTree> args) { return "Executing function: " + this.getName() + "(" + args.get(0).toStringVerbose() + ", <code>)"; } } @api @noboilerplate @breakable @seealso({com.laytonsmith.tools.docgen.templates.Loops.class}) public static class _dowhile extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return CNull.NULL; } @Override public String getName() { return "dowhile"; } @Override public Integer[] numArgs() { return new Integer[]{2}; } @Override public String docs() { return "void {code, condition} Like while, but always runs the code at least once. The condition is checked" + " after each run of the code, and if it is true, the code is run again. break and continue work" + " inside a dowhile, but continuing more than once is pointless, since the loop isn't inherently" + " keeping track of any counters anyways. Breaking multiple times still works however. In general, using brace" + " syntax is preferred: do { code(); } while(@condition); instead of using dowhile() directly."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public boolean useSpecialExec() { return true; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { try { do { try { parent.seval(nodes[0], env); } catch (LoopContinueException e) { //ok. No matter how many times it tells us to continue, we're only going to continue once. } } while (Static.getBoolean(parent.seval(nodes[1], env))); } catch (LoopBreakException e) { if (e.getTimes() > 1) { throw new LoopBreakException(e.getTimes() - 1, t); } } return CVoid.VOID; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "do {\n" + "\tmsg('This will only run once');\n" + "} while(false);"), new ExampleScript("Pure functional usage", "dowhile(\n" + "\tmsg('This will only run once')\n" + ", #while\n" + "false)") }; } @Override public LogLevel profileAt() { return LogLevel.WARNING; } @Override public String profileMessageS(List<ParseTree> args) { return "Executing function: " + this.getName() + "(<code>, " + args.get(1).toStringVerbose() + ")"; } } @api public static class _break extends AbstractFunction implements Optimizable { @Override public String getName() { return "break"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "nothing {[int]} Stops the current loop. If int is specified, and is greater than 1, the break travels that many loops up. So, if you had" + " a loop embedded in a loop, and you wanted to break in both loops, you would call break(2). If this function is called outside a loop" + " (or the number specified would cause the break to travel up further than any loops are defined), the function will fail. If no" + " argument is specified, it is the same as calling break(1). This function has special compilation rules. The break number" + " must not be dynamic, or a compile error will occur. An integer must be hard coded into the function."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_0; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { int num = 1; if (args.length == 1) { num = Static.getInt32(args[0], t); } throw new LoopBreakException(num, t); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "for(assign(@i, 0), @i < 1000, @i++,\n" + "\tfor(assign(@j, 0), @j < 1000, @j++,\n" + "\t\tmsg('This will only display once')\n" + "\t\tbreak(2)\n" + "\t)" + ")"), new ExampleScript("Invalid number", "for(assign(@i, 0), @i < 1000, @i++,\n" + "\tfor(assign(@j, 0), @j < 1000, @j++,\n" + "\t\tbreak(3) #There are only 2 loops to break out of\n" + "\t)" + ")"),}; } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { if (children.size() == 1) { if (children.get(0).isDynamic()) { //This is absolutely a bad design, if there is a variable here //in the break. Due to optimization, this is a compile error. throw new ConfigCompileException("The parameter sent to break() should" + " be hard coded, and should not be dynamically determinable, since this is always a sign" + " of loose code flow, which should be avoided.", t); } if (!(children.get(0).getData() instanceof CInt)) { throw new ConfigCompileException("break() only accepts integer values.", t); } } return null; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC //, OptimizationOption.TERMINAL This can't be added yet, because of things like switch, where code //branches aren't considered correctly. ); } } @api public static class _continue extends AbstractFunction { @Override public String getName() { return "continue"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "void {[int]} Skips the rest of the code in this loop, and starts the loop over, with it continuing at the next index. If this function" + " is called outside of a loop, the command will fail. If int is set, it will skip 'int' repetitions. If no argument is specified," + " 1 is used."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_0; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { int num = 1; if (args.length == 1) { num = Static.getInt32(args[0], t); } throw new LoopContinueException(num, t); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "for(assign(@i, 0), @i < 5, @i++){\n" + "\tif(@i == 2, continue())\n" + "\tmsg(@i)\n" + "}"), new ExampleScript("Argument specified", "for(assign(@i, 0), @i < 5, @i++){\n" + "\tif(@i == 2, continue(2))\n" + "\tmsg(@i)\n" + "}"),}; } } @api public static class is_stringable extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_stringable"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns whether or not the item is convertable to a string. Everything but arrays can be used as strings."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CBoolean.get(!(args[0] instanceof CArray)); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_stringable('yes')"), new ExampleScript("True condition", "is_stringable(1) #This can be used as a string, yes"), new ExampleScript("False condition", "is_stringable(array(1))"),}; } } @api public static class is_string extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_string"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns whether or not the item is actually a string datatype. If you just care if some data can be used as a string," + " use is_stringable()."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CBoolean.get(args[0] instanceof CString); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_string('yes')"), new ExampleScript("False condition", "is_string(1) #is_stringable() would return true here"),}; } } @api public static class is_bytearray extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_bytearray"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns whether or not the item is actually a ByteArray datatype."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CBoolean.get(args[0] instanceof CByteArray); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_bytearray(string_get_bytes('yay'))"), new ExampleScript("False condition", "is_bytearray('Nay')"), new ExampleScript("False condition", "is_bytearray(123)"),}; } } @api public static class is_array extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_array"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns whether or not the item is an array"; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_2; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CBoolean.get(args[0] instanceof CArray); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_array(array(1))"), new ExampleScript("True condition", "is_array(array(one: 1))"), new ExampleScript("False condition", "is_array('no')"),}; } } @api public static class is_number extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_number"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns whether or not the given item is an integer or a double. Note that numeric strings can usually be used as integers and doubles," + " however this function checks the actual datatype of the item. If you just want to see if an item can be used as a number," + " use is_integral() or is_numeric() instead."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CBoolean.get(args[0] instanceof CInt || args[0] instanceof CDouble); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_number(1)"), new ExampleScript("True condition", "is_number(1.0)"), new ExampleScript("False condition", "is_number('1')"), new ExampleScript("False condition", "is_number('1.0')")}; } } @api public static class is_double extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_double"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns whether or not the given item is a double. Note that numeric strings and integers" + " can usually be used as a double, however this function checks the actual datatype of the item. If" + " you just want to see if an item can be used as a number, use is_numeric() instead."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_2; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CBoolean.get(args[0] instanceof CDouble); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_double(1.0)"), new ExampleScript("False condition", "is_double(1)"),}; } } @api public static class is_integer extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_integer"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns whether or not the given item is an integer. Note that numeric strings can usually be used as integers," + " however this function checks the actual datatype of the item. If you just want to see if an item can be used as a number," + " use is_integral() or is_numeric() instead."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_2; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CBoolean.get(args[0] instanceof CInt); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_integer(1)"), new ExampleScript("False condition", "is_integer(1.0)"),}; } } @api public static class is_boolean extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_boolean"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns whether the given item is of the boolean datatype. Note that all datatypes can be used as booleans, however" + " this function checks the specific datatype of the given item."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_2; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CBoolean.get(args[0] instanceof CBoolean); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_boolean(false)"), new ExampleScript("False condition", "is_boolean(0)"),}; } } @api public static class is_null extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_null"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns whether or not the given item is null."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_2; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CBoolean.get(args[0] instanceof CNull); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_null(null)"), new ExampleScript("False condition", "is_null(0)"),}; } } @api public static class is_numeric extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_numeric"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns false if the item would fail if it were used as a numeric value." + " If it can be parsed or otherwise converted into a numeric value, true is returned."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { boolean b = true; try { Static.getNumber(args[0], t); } catch (ConfigRuntimeException e) { b = false; } return CBoolean.get(b); } @Override public CHVersion since() { return CHVersion.V3_3_0; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_numeric('1.0')"), new ExampleScript("True condition", "is_numeric('1')"), new ExampleScript("True condition", "is_numeric(1)"), new ExampleScript("True condition", "is_numeric(1.5)"), new ExampleScript("False condition", "is_numeric('string')"), new ExampleScript("True condition, because null is coerced to 0.0, which is numeric.", "is_numeric(null)"),}; } } @api public static class is_integral extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_integral"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns true if the numeric value represented by " + " a given double or numeric string could be cast to an integer" + " without losing data (or if it's an integer). For instance," + " is_numeric(4.5) would return true, and integer(4.5) would work," + " however, equals(4.5, integer(4.5)) returns false, because the" + " value was narrowed to 4."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { double d; try { d = Static.getDouble(args[0], t); } catch (ConfigRuntimeException e) { return CBoolean.FALSE; } return CBoolean.get((long) d == d); } @Override public CHVersion since() { return CHVersion.V3_3_0; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_integral(1.0)"), new ExampleScript("True condition", "is_integral(1)"), new ExampleScript("True condition", "is_integral('5.0')"), new ExampleScript("True condition", "is_integral('6')"), new ExampleScript("False condition", "is_integral(1.5)"), new ExampleScript("True condition, because null is coerced to 0, which is integral", "is_integral(null)"),}; } } @api @unbreakable public static class proc extends AbstractFunction { @Override public String getName() { return "proc"; } @Override public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } @Override public String docs() { return "void {[name], [ivar...], procCode} Creates a new user defined procedure (also known as \"function\") that can be called later in code. Please see the more detailed" + " documentation on procedures for more information. In general, brace syntax and keyword usage is preferred:" + " proc _myProc(@a, @b){ procCode(@a, @b); }"; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREFormatException.class}; } @Override public boolean isRestricted() { return false; } @Override public boolean preResolveVariables() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_3; } @Override public Boolean runAsync() { return null; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { Procedure myProc = getProcedure(t, env, parent, nodes); env.getEnv(GlobalEnv.class).GetProcs().put(myProc.getName(), myProc); return CVoid.VOID; } public static Procedure getProcedure(Target t, Environment env, Script parent, ParseTree... nodes) { String name = ""; List<IVariable> vars = new ArrayList<>(); ParseTree tree = null; List<String> varNames = new ArrayList<>(); boolean usesAssign = false; CClassType returnType = CClassType.AUTO; if (nodes[0].getData() instanceof CClassType) { returnType = (CClassType) nodes[0].getData(); ParseTree[] newNodes = new ParseTree[nodes.length - 1]; for (int i = 1; i < nodes.length; i++) { newNodes[i - 1] = nodes[i]; } nodes = newNodes; } // We have to restore the variable list once we're done IVariableList originalList = env.getEnv(GlobalEnv.class).GetVarList().clone(); for (int i = 0; i < nodes.length; i++) { if (i == nodes.length - 1) { tree = nodes[i]; } else { boolean thisNodeIsAssign = false; if (nodes[i].getData() instanceof CFunction) { if (((CFunction) nodes[i].getData()).getValue().equals("assign")) { thisNodeIsAssign = true; if ((nodes[i].getChildren().size() == 3 && nodes[i].getChildAt(0).getData().isDynamic()) || nodes[i].getChildAt(1).getData().isDynamic()) { usesAssign = true; } } } env.getEnv(GlobalEnv.class).SetFlag("no-check-duplicate-assign", true); Construct cons = parent.eval(nodes[i], env); env.getEnv(GlobalEnv.class).ClearFlag("no-check-duplicate-assign"); if (i == 0 && cons instanceof IVariable) { throw new CREInvalidProcedureException("Anonymous Procedures are not allowed", t); } else if (i == 0 && !(cons instanceof IVariable)) { name = cons.val(); } else if (!(cons instanceof IVariable)) { throw new CREInvalidProcedureException("You must use IVariables as the arguments", t); } else { IVariable ivar = null; try { Construct c = cons; if (c instanceof IVariable) { String varName = ((IVariable) c).getVariableName(); if (varNames.contains(varName)) { throw new CREInvalidProcedureException("Same variable name defined twice in " + name, t); } varNames.add(varName); } while (c instanceof IVariable) { c = env.getEnv(GlobalEnv.class).GetVarList().get(((IVariable) c).getVariableName(), t, true).ival(); } if (!thisNodeIsAssign) { //This is required because otherwise a default value that's already in the environment //would end up getting set to the existing value, thereby leaking in the global env //into this proc, if the call to the proc didn't have a value in this slot. c = new CString("", t); } ivar = new IVariable(((IVariable) cons).getDefinedType(), ((IVariable) cons).getVariableName(), c.clone(), t); } catch (CloneNotSupportedException ex) { // } vars.add(ivar); } } } env.getEnv(GlobalEnv.class).SetVarList(originalList); Procedure myProc = new Procedure(name, returnType, vars, tree, t); if (usesAssign) { myProc.definitelyNotConstant(); } return myProc; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CVoid.VOID; } @Override public boolean useSpecialExec() { return true; } /** * Returns either null to indicate that the procedure is not const, or returns a single Construct, which should * replace the call to the procedure. * * @param t * @param myProc * @param children * @return * @throws ConfigRuntimeException */ public static Construct optimizeProcedure(Target t, Procedure myProc, List<ParseTree> children) throws ConfigRuntimeException { if (myProc.isPossiblyConstant()) { //Oooh, it's possibly constant. So, let's run it with our children. try { FileOptions options = new FileOptions(new HashMap<String, String>()); if (!children.isEmpty()) { options = children.get(0).getFileOptions(); } ParseTree root = new ParseTree(new CFunction("__autoconcat__", Target.UNKNOWN), options); Script fakeScript = Script.GenerateScript(root, Static.GLOBAL_PERMISSION); Environment env = Static.GenerateStandaloneEnvironment(); env.getEnv(GlobalEnv.class).SetScript(fakeScript); Construct c = myProc.cexecute(children, env, t); //Yup! It worked. It's a const proc. return c; } catch (ConfigRuntimeException e) { if (e instanceof CREInvalidProcedureException) { //This is the only valid exception that doesn't strictly mean it's a bad //call. return null; } throw e; //Rethrow it. Since the functions are all static, and we actually are //running it with a mostly legit environment, this is a real runtime error, //and we can safely convert it to a compile error upstream } catch (Exception e) { //Nope. Something is preventing us from running it statically. //We don't really care. We just know it can't be optimized. return null; } } else { //Oh. Well, we tried. return null; } } // @Override // public boolean canOptimizeDynamic() { // return true; // } // // @Override // public ParseTree optimizeDynamic(Target t, List<ParseTree> children) throws ConfigCompileException, ConfigRuntimeException { // //We seriously lose out on the ability to optimize this procedure // //if we are assigning a dynamic value as a default, but we have to check // //that here. If we don't, we lose the information // return ; // } } @api public static class _return extends AbstractFunction implements Optimizable { @Override public String getName() { return "return"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "nothing {mixed} Returns the specified value from this procedure. It cannot be called outside a procedure."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_2_0; } @Override public Boolean runAsync() { return null; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.TERMINAL ); } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { Construct ret = (args.length == 1 ? args[0] : CVoid.VOID); throw new FunctionReturnException(ret, t); } } @api public static class include extends AbstractFunction /*implements Optimizable*/ { // Can't currently optimize this, because it depends on knowing whether or not // we are in cmdline mode, which is included with the environment, which doesn't // exist in optimizations yet. @Override public String getName() { return "include"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "void {path} Includes external code at the specified path."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREIncludeException.class}; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_2_0; } @Override public Boolean runAsync() { return true; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { return CVoid.VOID; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { ParseTree tree = nodes[0]; Construct arg = parent.seval(tree, env); String location = arg.val(); File file = Static.GetFileFromArgument(location, env, t, null); ParseTree include = IncludeCache.get(file, t); if (include != null) { // It could be an empty file parent.eval(include.getChildAt(0), env); } return CVoid.VOID; } @Override public boolean useSpecialExec() { return true; } // @Override // public Set<OptimizationOption> optimizationOptions() { // return EnumSet.of( // OptimizationOption.OPTIMIZE_CONSTANT // ); // } // // @Override // public Construct optimize(Target t, Construct... args) throws ConfigCompileException { // //We can't optimize per se, but if the path is constant, and the code is uncompilable, we // //can give a warning, and go ahead and cache the tree. // String path = args[0].val(); // File file = Static.GetFileFromArgument(path, env, t, null); // IncludeCache.get(file, t); // return null; // } } @api public static class call_proc extends AbstractFunction implements Optimizable { @Override public String getName() { return "call_proc"; } @Override public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } @Override public String docs() { return "mixed {proc_name, [var1...]} Dynamically calls a user defined procedure. call_proc(_myProc, 'var1') is the equivalent of" + " _myProc('var1'), except you could dynamically build the procedure name if need be. This is useful for dynamic coding," + " however, closures work best for callbacks. Throws an InvalidProcedureException if the procedure isn't defined. If you are" + " hardcoding the first parameter, a warning will be issued, because it is much more efficient and safe to directly use" + " a procedure if you know what its name is beforehand."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInvalidProcedureException.class}; } @Override public boolean isRestricted() { return true; } @Override public CHVersion since() { return CHVersion.V3_2_0; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { if (args.length < 1) { throw new CREInsufficientArgumentsException("Expecting at least one argument to " + getName(), t); } Procedure proc = env.getEnv(GlobalEnv.class).GetProcs().get(args[0].val()); if (proc != null) { List<Construct> vars = new ArrayList<Construct>(Arrays.asList(args)); vars.remove(0); Environment newEnv = null; try { newEnv = env.clone(); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } return proc.execute(vars, newEnv, t); } throw new CREInvalidProcedureException("Unknown procedure \"" + args[0].val() + "\"", t); } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC); } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { if (children.size() < 1) { throw new CREInsufficientArgumentsException("Expecting at least one argument to " + getName(), t); } if (children.get(0).isConst()) { CHLog.GetLogger().Log(CHLog.Tags.COMPILER, LogLevel.WARNING, "Hardcoding procedure name in " + getName() + ", which is inefficient." + " Consider calling the procedure directly if the procedure name is known at compile time.", t); } return null; } } @api public static class call_proc_array extends call_proc { @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { CArray ca = Static.getArray(args[1], t); if (ca.inAssociativeMode()) { throw new CRECastException("Expected the array passed to " + getName() + " to be non-associative.", t); } Construct[] args2 = new Construct[(int) ca.size() + 1]; args2[0] = args[0]; for (int i = 1; i < args2.length; i++) { args2[i] = ca.get(i - 1, t); } return super.exec(t, environment, args2); } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInvalidProcedureException.class, CRECastException.class}; } @Override public String getName() { return "call_proc_array"; } @Override public Integer[] numArgs() { return new Integer[]{2}; } @Override public String docs() { return "mixed {proc_name, array} Works like call_proc, but allows for variable or unknown number of arguments to be passed to" + " a proc. The array parameter is \"flattened\", and call_proc is essentially called. If the array is associative, an" + " exception is thrown."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { //If they hardcode the name, that's fine, because the variables may just be the only thing that's variable. return null; } } @api(environments = CommandHelperEnvironment.class) public static class is_proc extends AbstractFunction { @Override public String getName() { return "is_proc"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {procName} Returns whether or not the given procName is currently defined, i.e. if calling this proc wouldn't" + " throw an exception."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return true; } @Override public CHVersion since() { return CHVersion.V3_2_0; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) { return CBoolean.get(env.getEnv(GlobalEnv.class).GetProcs().get(args[0].val()) != null); } } @api public static class is_associative extends AbstractFunction implements Optimizable { @Override public String getName() { return "is_associative"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {array} Returns whether or not the array is associative. If the parameter is not an array, throws a CastException."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_3_0; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { if (args[0] instanceof CArray) { return CBoolean.get(((CArray) args[0]).inAssociativeMode()); } else { throw new CRECastException(this.getName() + " expects argument 1 to be an array", t); } } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_associative(array(one: 1, two: 2))"), new ExampleScript("False condition", "is_associative(array(1, 2, 3))"),}; } } @api public static class is_closure extends AbstractFunction { @Override public String getName() { return "is_closure"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {arg} Returns true if the argument is a closure (could be executed)" + " or false otherwise"; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return CBoolean.get(args[0] instanceof CClosure); } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("True condition", "is_closure(closure(msg('code')))"), new ExampleScript("False condition", "is_closure('a string')"),}; } } @api @seealso({_export.class}) public static class _import extends AbstractFunction { @Override public String getName() { return "import"; } @Override public Integer[] numArgs() { return new Integer[]{1, 2}; } @Override public String docs() { return "mixed {key, [default]} This function imports a value from the global value register. It looks for a" + " value stored with the specified key (using the export function), and returns that value." + " If specified key doesn't exist, it will return either null or the default value if specified." + " An array may be used as a key. It is converted into a string with the array values separated by" + " dots. import() is threadsafe."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREIllegalArgumentException.class, CREIndexOverflowException.class}; } @Override public boolean isRestricted() { return true; } @Override public CHVersion since() { return CHVersion.V3_3_0; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { String key; if (args[0] instanceof CString) { key = args[0].val(); } else if (args[0] instanceof CArray) { key = GetNamespace((CArray) args[0], t); } else { throw new CREIllegalArgumentException("Argument 1 in " + this.getName() + " must be a string or array.", t); } Construct c = Globals.GetGlobalConstruct(key); if (args.length == 2 && c instanceof CNull) { c = args[1]; } return c; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new _export().examples(); } } @api @seealso({_import.class}) public static class _export extends AbstractFunction { @Override public String getName() { return "export"; } @Override public Integer[] numArgs() { return new Integer[]{2}; } @Override public String docs() { return "void {key, value} Stores a value in the global storage register." + " An arbitrary value is stored with the given key, and can be retreived using import." + " If the value is already stored, it is overwritten. See {{function|import}}." + " The reference to the value is stored, not a copy of the value, so in the case of" + " arrays, manipulating the contents of the array will manipulate the stored value. An array may" + " be used as a key. It is converted into a string with the array values separated by dots." + " export() is threadsafe."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREIllegalArgumentException.class, CREIndexOverflowException.class}; } @Override public boolean isRestricted() { return true; } @Override public CHVersion since() { return CHVersion.V3_3_0; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { String key; if (args[0] instanceof CString) { key = args[0].val(); } else if (args[0] instanceof CArray) { key = GetNamespace((CArray) args[0], t); } else { throw new CREIllegalArgumentException("Argument 1 in " + this.getName() + " must be a string or array.", t); } Construct c = args[1]; Globals.SetGlobal(key, c); return CVoid.VOID; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "@var = 2;\n" + "export('custom.name', @var);\n" + "@var2 = import('custom.name');\n" + "msg(@var2);"), new ExampleScript("Storage of references", "@array = array(1, 2, 3);\n" + "export('array', @array);\n" + "@array[0] = 4;\n" + "@array2 = import('array');\n" + "msg(@array2);"), new ExampleScript("Array key usage", "@key = array(custom, name);\n" + "export(@key, 'value');\n" + "@value = import(@key);\n" + "msg(@value);"), new ExampleScript("Default value usage", "export('custom.name', null);\n" + "@value = import('custom.name', 'default value');\n" + "msg(@value);") }; } } @api(environments = CommandHelperEnvironment.class) @unbreakable @seealso({com.laytonsmith.tools.docgen.templates.Closures.class}) public static class closure extends AbstractFunction { @Override public String getName() { return "closure"; } @Override public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } @Override public String docs() { return "closure {[varNames...,] code} Returns a closure on the provided code. A closure is" + " a datatype that represents some code as code, not the results of some" + " code after it is run. Code placed in a closure can be used as" + " a string, or executed by other functions using the eval() function." + " If a closure is \"to string'd\" it will not necessarily look like" + " the original code, but will be functionally equivalent. The current environment" + " is \"snapshotted\" and stored with the closure, however, this information is" + " only stored in memory, it isn't retained during a serialization operation." + " Also, the special variable @arguments is automatically created for you, and contains" + " an array of all the arguments passed to the closure, much like procedures." + " See the wiki article on [[CommandHelper/Staged/Closures|closures]] for more details" + " and examples."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return true; } @Override public boolean preResolveVariables() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return CVoid.VOID; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { if (nodes.length == 0) { //Empty closure, do nothing. return new CClosure(null, env, CClassType.AUTO, new String[]{}, new Construct[]{}, new CClassType[]{}, t); } // Handle the closure type first thing CClassType returnType = CClassType.AUTO; if (nodes[0].getData() instanceof CClassType) { returnType = (CClassType) nodes[0].getData(); ParseTree[] newNodes = new ParseTree[nodes.length - 1]; for (int i = 1; i < nodes.length; i++) { newNodes[i - 1] = nodes[i]; } nodes = newNodes; } String[] names = new String[nodes.length - 1]; Construct[] defaults = new Construct[nodes.length - 1]; CClassType[] types = new CClassType[nodes.length - 1]; // We clone the enviornment at this point, because we don't want the values // that are assigned here to overwrite values in the main scope. Environment myEnv; try { myEnv = env.clone(); } catch (CloneNotSupportedException ex) { myEnv = env; } for (int i = 0; i < nodes.length - 1; i++) { ParseTree node = nodes[i]; ParseTree newNode = new ParseTree(new CFunction("g", t), node.getFileOptions()); List<ParseTree> children = new ArrayList<>(); children.add(node); newNode.setChildren(children); Script fakeScript = Script.GenerateScript(newNode, myEnv.getEnv(GlobalEnv.class).GetLabel()); myEnv.getEnv(GlobalEnv.class).SetFlag("closure-warn-overwrite", true); Construct ret = MethodScriptCompiler.execute(newNode, myEnv, null, fakeScript); myEnv.getEnv(GlobalEnv.class).ClearFlag("closure-warn-overwrite"); if (!(ret instanceof IVariable)) { throw new CRECastException("Arguments sent to " + getName() + " barring the last) must be ivariables", t); } names[i] = ((IVariable) ret).getVariableName(); try { defaults[i] = ((IVariable) ret).ival().clone(); types[i] = ((IVariable) ret).getDefinedType(); } catch (CloneNotSupportedException ex) { Logger.getLogger(DataHandling.class.getName()).log(Level.SEVERE, null, ex); } } CClosure closure = new CClosure(nodes[nodes.length - 1], myEnv, returnType, names, defaults, types, t); return closure; } @Override public Version since() { return CHVersion.V3_3_0; } @Override public boolean useSpecialExec() { return true; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Creates a closure", "closure(){\n" + "\tmsg('Hello World!');\n" + "};"), new ExampleScript("Executes a closure", "execute(closure(){\n" + "\tmsg('Hello World!');\n" + "});") }; } } @api @unbreakable @seealso({com.laytonsmith.tools.docgen.templates.Closures.class}) public static class iclosure extends closure { @Override public String getName() { return "iclosure"; } @Override public String docs() { return "iclosure {[varNames...,] code} Returns a scope isolated closure on the provided code. An iclosure is" + " a datatype that represents some code as code, not the results of some" + " code after it is run. Code placed in an iclosure can be used as" + " a string, or executed by other functions using the execute() function." + " If a closure is \"to string'd\" it will not necessarily look like" + " the original code, but will be functionally equivalent. The current environment" + " is \"snapshotted\" and stored with the closure, however, this information is" + " only stored in memory, it isn't retained during a serialization operation. However," + " the variable table of the parent scope is not retained, thus making this closure \"isolated\"" + " from the parent code." + " The special variable @arguments is automatically created for you, and contains" + " an array of all the arguments passed to the closure, much like procedures." + " See the wiki article on [[CommandHelper/Staged/Closures|closures]] for more details" + " and examples."; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { if (nodes.length == 0) { //Empty closure, do nothing. return new CClosure(null, env, CClassType.AUTO, new String[]{}, new Construct[]{}, new CClassType[]{}, t); } // Handle the closure type first thing CClassType returnType = CClassType.AUTO; if (nodes[0].getData() instanceof CClassType) { returnType = (CClassType) nodes[0].getData(); ParseTree[] newNodes = new ParseTree[nodes.length - 1]; for (int i = 1; i < nodes.length; i++) { newNodes[i - 1] = nodes[i]; } nodes = newNodes; } String[] names = new String[nodes.length - 1]; Construct[] defaults = new Construct[nodes.length - 1]; CClassType[] types = new CClassType[nodes.length - 1]; // We clone the enviornment at this point, because we don't want the values // that are assigned here to overwrite values in the main scope. Environment myEnv; try { myEnv = env.clone(); } catch (CloneNotSupportedException ex) { myEnv = env; } for (int i = 0; i < nodes.length - 1; i++) { ParseTree node = nodes[i]; ParseTree newNode = new ParseTree(new CFunction("g", t), node.getFileOptions()); List<ParseTree> children = new ArrayList<>(); children.add(node); newNode.setChildren(children); Script fakeScript = Script.GenerateScript(newNode, myEnv.getEnv(GlobalEnv.class).GetLabel()); myEnv.getEnv(GlobalEnv.class).SetFlag("closure-warn-overwrite", true); Construct ret = MethodScriptCompiler.execute(newNode, myEnv, null, fakeScript); myEnv.getEnv(GlobalEnv.class).ClearFlag("closure-warn-overwrite"); if (!(ret instanceof IVariable)) { throw new CRECastException("Arguments sent to " + getName() + " barring the last) must be ivariables", t); } names[i] = ((IVariable) ret).getVariableName(); try { defaults[i] = ((IVariable) ret).ival().clone(); types[i] = ((IVariable) ret).getDefinedType(); } catch (CloneNotSupportedException ex) { Logger.getLogger(DataHandling.class.getName()).log(Level.SEVERE, null, ex); } } CIClosure closure = new CIClosure(nodes[nodes.length - 1], myEnv, returnType, names, defaults, types, t); return closure; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Creates an iclosure", "iclosure(){\n" + "\tmsg('Hello World!');\n" + "};"), new ExampleScript("Executes an iclosure", "execute(iclosure(){\n" + "\tmsg('Hello World!');\n" + "});"), new ExampleScript("Shows scoping", "@a = \'variable\';\n" + "msg('Outside of iclosure: '.reflect_pull('varlist'));\n" + "// Note that this is an iclosure\n" + "execute('val1', iclosure(@b){\n" + "\tmsg('Inside of iclosure: '.reflect_pull('varlist'));\n" + "});\n" + "// Note that this is a regular closure\n" + "execute('val2', closure(@c){\n" + "\tmsg('Insider of closure: '.reflect_pull('varlist'));\n" + "});") }; } } @api @hide("Until the Federation system is finished, this is hidden") @unbreakable @nolinking public static class rclosure extends closure { @Override public String getName() { return "rclosure"; } @Override public String docs() { return "closure {[varNames...], code} Returns a non-linking closure on the provided code. The same rules apply" + " for closures, except the top level internal code does not check for proper linking at compile time," + " and instead links at runtime. Lexer errors and some other compile time checks ARE done however, but" + " functions are not optimized or linked. This is used for remote code execution, since the remote platform" + " may have some functionality unavailable on this current platform."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } } @api @seealso({com.laytonsmith.tools.docgen.templates.Closures.class}) public static class execute extends AbstractFunction { @Override public String getName() { return "execute"; } @Override public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } @Override public String docs() { return "mixed {[values...,] closure} Executes the given closure. You can also send arguments" + " to the closure, which it may or may not use, depending on the particular closure's" + " definition. If the closure returns a value with return(), then that value will" + " be returned with execute. Otherwise, void is returned."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if (args[args.length - 1] instanceof CClosure) { Construct[] vals = new Construct[args.length - 1]; System.arraycopy(args, 0, vals, 0, args.length - 1); CClosure closure = (CClosure) args[args.length - 1]; try { closure.execute(vals); } catch (FunctionReturnException e) { return e.getReturn(); } } else { throw new CRECastException("Only a closure (created from the closure function) can be sent to execute()", t); } return CVoid.VOID; } @Override public CHVersion since() { return CHVersion.V3_3_1; } } @api public static class _boolean extends AbstractFunction implements Optimizable { @Override public String getName() { return "boolean"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "boolean {item} Returns a new construct that has been cast to a boolean. The item is cast according to" + " the boolean conversion rules. Since all data types can be cast to a" + " a boolean, this function will never throw an exception."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return CBoolean.get(Static.getBoolean(args[0])); } @Override public CHVersion since() { return CHVersion.V3_3_0; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "boolean(1)"), new ExampleScript("Basic usage", "boolean(0)"), new ExampleScript("Basic usage", "boolean(array(1))"), new ExampleScript("Basic usage", "boolean(array())"), new ExampleScript("Basic usage", "boolean(null)"), new ExampleScript("Basic usage", "boolean('string')"), new ExampleScript("Basic usage", "boolean('')"),}; } } @api public static class _integer extends AbstractFunction implements Optimizable { @Override public String getName() { return "integer"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "integer {item} Returns a new construct that has been cast to an integer." + " This function will throw a CastException if is_numeric would return" + " false for this item, but otherwise, it will be cast properly. Data" + " may be lost in this conversion. For instance, 4.5 will be converted" + " to 4, by using integer truncation. You can use is_integral to see" + " if this data loss would occur."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return new CInt((long) Static.getDouble(args[0], t), t); } @Override public CHVersion since() { return CHVersion.V3_3_0; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "integer(1.0)"), new ExampleScript("Basic usage", "integer(1.5)"), new ExampleScript("Failure", "assign(@var, 'string')\ninteger(@var)"),}; } } @api public static class _double extends AbstractFunction implements Optimizable { @Override public String getName() { return "double"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "double {item} Returns a new construct that has been cast to an double." + " This function will throw a CastException if is_numeric would return" + " false for this item, but otherwise, it will be cast properly."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return new CDouble(Static.getDouble(args[0], t), t); } @Override public CHVersion since() { return CHVersion.V3_3_0; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "double(1)"), new ExampleScript("Failure", "@var = 'string';\ndouble(@var);"),}; } } @api public static class _string extends AbstractFunction implements Optimizable { @Override public String getName() { return "string"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "string {item} Creates a new construct that is the \"toString\" of an item." + " For arrays, an human readable version is returned; this should not be" + " used directly, as the format is not guaranteed to remain consistent. Booleans return \"true\"" + " or \"false\" and null returns \"null\"."; } @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return new CString(args[0].val(), t); } @Override public CHVersion since() { return CHVersion.V3_3_0; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.CACHE_RETURN ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "string(1)"), new ExampleScript("Basic usage", "string(true)"), new ExampleScript("Basic usage", "string(false)"), new ExampleScript("Basic usage", "string(null)"), new ExampleScript("Basic usage", "string(array(1, 2))"), new ExampleScript("Basic usage", "string(array(one: 'one', two: 'two'))"),}; } } @api @seealso(parse_int.class) public static class to_radix extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class, CRERangeException.class, CREFormatException.class}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { int radix = Static.getInt32(args[1], t); if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) { throw new CRERangeException("The radix must be between " + Character.MIN_RADIX + " and " + Character.MAX_RADIX + ", inclusive.", t); } return new CString(Long.toString(Static.getInt(args[0], t), radix), t); } @Override public String getName() { return "to_radix"; } @Override public Integer[] numArgs() { return new Integer[]{2}; } @Override public String docs() { return "string {value, radix} Given an int and a radix, returns a string representation of the integer value" + " in the given base. A common use would be to output a hex or binary representation of a number, for" + " instance. ---- It is useful to note that all integers are stored internally by the computer as binary," + " but since we usually represent numbers in text as base 10 numbers, we often times forget that both" + " base 16 'F' and base 10 '15' and base 2 '1111' are actually the same number, just represented differently" + " as strings in different bases. This doesn't change how the program behaves, since the base is just a way to represent" + " the number on paper. The 'radix' is the base. So, given to_radix(10, 10), that would return '10', because" + " in code, we wrote out our value '10' in base 10, and we convert it to base 10, so nothing changes. However," + " if we write to_radix(15, 16) we are saying \"convert the base 10 value 15 to base 16\", so it returns 'F'." + " See {{function|parse_int}} for the opposite operation. The radix must be between " + Character.MIN_RADIX + " and " + Character.MAX_RADIX + ", inclusive, or a range exception is thrown. This is because there are only " + Character.MAX_RADIX + " characters that are normally used to represent different base numbers (that is, 0-9, a-z). The minimum radix is " + Character.MIN_RADIX + ", because it is impossible to represent any numbers with out at least a binary base."; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("To a hex string", "to_radix(15, 16)"), new ExampleScript("To a binary string", "to_radix(15, 2)"), new ExampleScript("Using hex value in source", "to_radix(0xff, 16)"), new ExampleScript("Using binary value in source", "to_radix(0b10101010, 2)") }; } } @api @seealso(to_radix.class) public static class parse_int extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class, CRERangeException.class, CREFormatException.class}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { String value = args[0].val(); int radix = Static.getInt32(args[1], t); if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) { throw new CRERangeException("The radix must be between " + Character.MIN_RADIX + " and " + Character.MAX_RADIX + ", inclusive.", t); } long ret; try { ret = Long.parseLong(value, radix); } catch (NumberFormatException ex) { throw new CREFormatException("The input string: \"" + value + "\" is improperly formatted. (Perhaps you're using a character greater than" + " the radix specified?)", t); } return new CInt(ret, t); } @Override public String getName() { return "parse_int"; } @Override public Integer[] numArgs() { return new Integer[]{2}; } @Override public String docs() { return "int {value, radix} Converts a string representation of an integer to a real integer, given the value's" + " radix (base). See {{function|to_radix}} for a more detailed explanation of number theory. Radix must be" + " between " + Character.MIN_RADIX + " and " + Character.MAX_RADIX + ", inclusive."; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("From hex string", "parse_int('F', 16)"), new ExampleScript("From binary string", "parse_int('1111', 2)") }; } } /** * Generates the namespace for this value, given an array. * * @param array * @return */ private static String GetNamespace(CArray array, Target t) { boolean first = true; StringBuilder b = new StringBuilder(); for (int i = 0; i < array.size(); i++) { if (!first) { b.append("."); } first = false; b.append(array.get(i, t).val()); } return b.toString(); } @api public static class typeof extends AbstractFunction implements Optimizable { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { try { return new CClassType(args[0].typeof(), t); } catch (IllegalArgumentException ex) { throw new Error("Class " + args[0].getClass().getName() + " is not annotated with @typeof. Please report this" + " error to the developers."); } } @Override public String getName() { return "typeof"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "ClassType {arg} Returns a string value of the typeof a value. For instance 'array' is returned" + " for typeof(array()). This is a generic replacement for the is_* series of functions."; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage, typeof string", "typeof('value')"), new ExampleScript("Basic usage, typeof int", "typeof(1)"), new ExampleScript("Basic usage, typeof double", "typeof(1.0)"), new ExampleScript("Basic usage, typeof closure", "typeof(closure(){ msg('test') })"),}; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of(OptimizationOption.CONSTANT_OFFLINE); } } @api public static class eval extends AbstractFunction implements Optimizable { @Override public String getName() { return "eval"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "string {script_string} Executes arbitrary MethodScript. Note that this function is very experimental, and is subject to changing or " + "removal."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public boolean isRestricted() { return true; } @Override public CHVersion since() { return CHVersion.V3_1_0; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { boolean oldDynamicScriptMode = env.getEnv(GlobalEnv.class).GetDynamicScriptingMode(); ParseTree node = nodes[0]; try { env.getEnv(GlobalEnv.class).SetDynamicScriptingMode(true); Construct script = parent.seval(node, env); if (script instanceof CClosure) { throw new CRECastException("Closures cannot be eval'd directly. Use execute() instead.", t); } ParseTree root = MethodScriptCompiler.compile(MethodScriptCompiler.lex(script.val(), t.file(), true)); StringBuilder b = new StringBuilder(); int count = 0; for (ParseTree child : root.getChildren()) { Construct s = parent.seval(child, env); if (!s.val().trim().isEmpty()) { if (count > 0) { b.append(" "); } b.append(s.val()); } count++; } return new CString(b.toString(), t); } catch (ConfigCompileException e) { throw new CREFormatException("Could not compile eval'd code: " + e.getMessage(), t); } catch (ConfigCompileGroupException ex) { StringBuilder b = new StringBuilder(); b.append("Could not compile eval'd code: "); for (ConfigCompileException e : ex.getList()) { b.append(e.getMessage()).append("\n"); } throw new CREFormatException(b.toString(), t); } finally { env.getEnv(GlobalEnv.class).SetDynamicScriptingMode(oldDynamicScriptMode); } } @Override public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { return CVoid.VOID; } //Doesn't matter, run out of state anyways @Override public Boolean runAsync() { return null; } @Override public boolean useSpecialExec() { return true; } @Override public Set<Optimizable.OptimizationOption> optimizationOptions() { return EnumSet.of(Optimizable.OptimizationOption.OPTIMIZE_DYNAMIC); } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { if (children.size() != 1) { throw new ConfigCompileException(getName() + " expects only one argument", t); } if (children.get(0).isConst()) { CHLog.GetLogger().Log(CHLog.Tags.COMPILER, LogLevel.WARNING, "Eval'd code is hardcoded, consider simply using the code directly, as wrapping" + " hardcoded code in " + getName() + " is much less efficient.", t); } return null; } } @api @noprofile @hide("This will eventually be replaced by ; statements.") public static class g extends AbstractFunction { @Override public String getName() { return "g"; } @Override public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } @Override public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { for (int i = 0; i < args.length; i++) { args[i].val(); } return CVoid.VOID; } @Override public String docs() { return "string {func1, [func2...]} Groups any number of functions together, and returns void. "; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_0_1; } @Override public Boolean runAsync() { return null; } } /** * For now, this feature works as is. However, I'm debating on whether or not I should just override assign() * instead. The only issue with this is that if assign is overwritten, then a mutable_primitive will be "stuck" in * the variable. So if later you wanted to make a value not a mutable primitive, there would be no way to do so. * Another method could be introduced to "clear" the value out, but then there would be no way to tell if the value * were actually mutable or not, so a third function would have to be added. The other point of concern is how to * handle typeof() for a CMutablePrimitive. Should it return the underlying type, or mutable_primitive? If * assignments are "sticky", then it would make sense to have it return the underlying type, but there's an issue * with that, because then typeof wouldn't be useable for debug type situations. Given all these potential issues, * it is still hidden, but available for experimental cases. */ @api @hide("This is still experimental") public static class mutable_primitive extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREFormatException.class}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { Construct val = CNull.NULL; if (args.length > 0) { val = args[0]; } return new CMutablePrimitive(val, t); } @Override public String getName() { return "mutable_primitive"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "mutable_primitive {[primitive_value]} Creates a mutable primitive object, initially setting the value of the object to" + " null, or the specified value. The value must be a primitive value, and cannot be an array or object. ----" + " The underlying primitive value is used in all cases where a value can be inferred. In all other cases, you must convert" + " the primitive to the desired type, e.g. double(@mutable_primitive). Mutable primitives work like an array as well," + " in some cases, but not others. In general, setting of the underlying values may be done with array_push(). Assigning" + " a new value to the variable works the same as assigning a new value to any other value, it overwrites the value with" + " the new type. Most array functions will work with the mutable primitive, however, they will return useless data, for" + " instance, array_resize() will simply set the value to the default value shown. array_size() is an exception to this" + " rule, it will not work, and will throw an exception. See the examples for more use cases. In general, this is meant" + " as a convenience feature for values that are passed to closures or procs, but should be passed by reference. Cloning the" + " mutable primitive with the array clone operation creates a distinct copy."; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "@val = mutable_primitive(0);\n" + "msg('typeof: ' . typeof(@val));\n" + "msg('value: ' . @val);\n" + "msg('@val + 5: ' . (@val + 5)); // Works as if it were a primitive with most functions\n" + "(++@val); // As a special exception to how assignments work, increment/decrement works as well\n" + "msg(@val); // 1\n"), new ExampleScript("Basic usage with procs", "proc(_testWithMutable, @a){\n" + "\t@a[] = 5;\n" + "}\n\n" + "" + "proc(_testWithoutMutable, @a){\n" + "\t@a = 10;\n" + "}\n\n" + "" + "@a = mutable_primitive(0);\n" + "msg(@a); // The value starts out as 0\n" + "_testWithMutable(@a); // This will actually change the value\n" + "msg(@a); // Here, the value is 5\n" + "_testWithoutMutable(@a); // This will not change the value\n" + "msg(@a); // Still teh value is 5\n"), new ExampleScript("Basic usage with closure", "@a = mutable_primitive(0);\n" + "execute(closure(){\n" + "\t@a++;\n" + "});\n" + "msg(@a); // 1\n"), new ExampleScript("Cloning the value", "@a = mutable_primitive(0);\n" + "@b = @a[];\n" + "@a[] = 5;\n" + "msg(@a);\n" + "msg(@b);\n") }; } } @api public static class _instanceof extends AbstractFunction implements Optimizable { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if (args[0] instanceof CNull) { return CBoolean.FALSE; } boolean b = InstanceofUtil.isInstanceof(args[0], args[1].val()); return CBoolean.get(b); } @Override public String getName() { return "instanceof"; } @Override public Integer[] numArgs() { return new Integer[]{2}; } @Override public String docs() { return "boolean {value, type} Checks to see if the value is, extends, or implements the given type. Keyword usage is preferred:" + " <code>@value instanceof int</code>. The opposite operation is <code>@value notinstanceof int</code>. ---- Null is a special value, while any type may be assigned null, it does not extend" + " any type, and therefore \"null instanceof AnyType\" will always return false. Likewise, other than null, all" + " values extend \"mixed\", and therefore \"anyNonNullValue instanceof mixed\" will always return true. There is no" + " (single) functional equivalent to the notinstanceof keyword. <code>@value notinstanceof int</code> simply compiles to not(instanceof(@value, int))."; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC); } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { // There are two specific cases here where we will give more precise error messages. // If it's a string, yell at them if (children.get(1).getData() instanceof CString) { throw new ConfigCompileException("Unexpected string type passed to \"instanceof\"", t); } // If it's a variable, also yell at them if (children.get(1).getData() instanceof IVariable) { throw new ConfigCompileException("Variable types are not allowed in \"instanceof\"", t); } // Unknown error, but this is still never valid. if (!(children.get(1).getData() instanceof CClassType)) { throw new ConfigCompileException("Unexpected type for \"instanceof\": " + children.get(1).getData(), t); } // null is technically a type, but instanceof shouldn't work with that if (children.get(1).getData().val().equals("null")) { throw new ConfigCompileException("\"null\" cannot be compared against with instanceof. Use <value> === null.", t); } // It's hardcoded, allow it, but optimize it out. if (children.get(0).isConst()) { return new ParseTree(exec(t, null, children.get(0).getData(), children.get(1).getData()), fileOptions); } return null; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "mixed @a = 5; // Actually an int\n" + "msg(@a instanceof int); // true\n" + "msg(@a instanceof string); // false\n"), new ExampleScript("Functional usage", "instanceof(5, int)"), new ExampleScript("Inverted usage", "mixed @a = 5;\n" + "msg(@a notinstanceof int); // false\n" + "msg(@a notinstanceof string); // true\n"), new ExampleScript("Inverted functional usage", "!instanceof(5, int)") }; } } }