From 091d8b467404f5f6e00e6d37e9a22358283dfb14 Mon Sep 17 00:00:00 2001 From: LadyCailin Date: Sat, 7 Mar 2026 15:55:04 +0100 Subject: [PATCH 1/5] Convert to an iterative eval loop instead of a recursive one. This is a major change in the script evaluation process, which changes how "special execution" functions work. Previously, functions could choose to implement execs instead of exec, which received a ParseTree, instead of Mixed. This allowed the individual function to decide how or even if the ParseTree nodes were further executed. This works in general, however it has several drawbacks. In particular, the core evaluation loop loses control over the script once it decends into individual functions. Therefore features like Ctrl+C in command line scripts relied on each of these "flow" functions to implement that feature correctly, and only some of them did. This also prevents new features from being implemented as easily, like a debugger, since the evaluation loop would need to be modified, and every single flow function would need to make the same changes as well. This also has several performance benefits. Using a recursive approach meant that each frame of MethodScript had about 3 Java frames, which is inefficient. The biggest performance change with this is moving away from exception based control flow. Previously, return, break, and continue were all implemented with Java exceptions. This is way more expensive than it needs to be, especially for very unexecptional cases such as return(). Now, when a proc or closure returns, it triggers a different phase in the state machine, instead of throwing an exception. This also unlocks future features that were not possible today. A debugger could have been implemented before (though it would have been difficult) but now an asynchronous debugger can be implemented. async/await is also possible now. Tail call optimizations can be done, execution time quotas, and the profiler can probably be improved. --- .../java/com/laytonsmith/core/EvalStack.java | 60 + .../com/laytonsmith/core/FlowFunction.java | 92 ++ .../com/laytonsmith/core/LocalPackages.java | 4 - .../java/com/laytonsmith/core/Method.java | 3 +- .../core/MethodScriptCompiler.java | 68 +- .../java/com/laytonsmith/core/Procedure.java | 273 ++-- .../java/com/laytonsmith/core/Script.java | 523 ++++--- .../java/com/laytonsmith/core/StackFrame.java | 213 +++ .../java/com/laytonsmith/core/StepAction.java | 156 ++ .../laytonsmith/core/asm/LLVMFunction.java | 11 - .../laytonsmith/core/constructs/CClosure.java | 106 +- .../core/constructs/CNativeClosure.java | 3 +- .../core/constructs/ProcedureUsage.java | 3 +- .../core/environments/GlobalEnv.java | 43 + .../core/events/AbstractGenericEvent.java | 7 - .../laytonsmith/core/events/EventUtils.java | 3 - .../exceptions/CancelCommandException.java | 22 +- .../exceptions/FunctionReturnException.java | 22 - .../core/exceptions/LoopBreakException.java | 14 - .../exceptions/LoopContinueException.java | 14 - .../exceptions/LoopManipulationException.java | 47 - .../ProgramFlowManipulationException.java | 37 - .../UnhandledFlowControlException.java | 27 + .../core/functions/AbstractFunction.java | 28 - .../core/functions/ArrayHandling.java | 85 +- .../core/functions/BasicLogic.java | 209 ++- .../laytonsmith/core/functions/Compiler.java | 24 +- .../core/functions/CompositeFunction.java | 18 +- .../core/functions/ControlFlow.java | 1257 +++++++++++------ .../core/functions/DataHandling.java | 818 +++++++---- .../core/functions/EventBinding.java | 141 +- .../core/functions/Exceptions.java | 434 ++++-- .../core/functions/ExecutionQueue.java | 6 +- .../laytonsmith/core/functions/Function.java | 27 +- .../core/functions/IncludeCache.java | 4 +- .../com/laytonsmith/core/functions/Math.java | 243 ++-- .../com/laytonsmith/core/functions/Meta.java | 87 +- .../core/functions/ObjectManagement.java | 89 +- .../core/functions/Scheduling.java | 5 - .../laytonsmith/core/functions/Threading.java | 73 +- .../com/laytonsmith/core/functions/Web.java | 7 +- .../core/natives/interfaces/Callable.java | 10 +- .../core/MethodScriptCompilerTest.java | 5 + .../laytonsmith/core/OptimizationTest.java | 8 +- .../core/functions/BasicLogicTest.java | 5 + .../core/functions/ControlFlowTest.java | 27 +- .../core/functions/DataHandlingTest.java | 2 +- .../core/functions/EventBindingTest.java | 100 ++ .../laytonsmith/core/functions/MathTest.java | 6 +- .../com/laytonsmith/testing/StaticTest.java | 21 +- 50 files changed, 3557 insertions(+), 1933 deletions(-) create mode 100644 src/main/java/com/laytonsmith/core/EvalStack.java create mode 100644 src/main/java/com/laytonsmith/core/FlowFunction.java create mode 100644 src/main/java/com/laytonsmith/core/StackFrame.java create mode 100644 src/main/java/com/laytonsmith/core/StepAction.java delete mode 100644 src/main/java/com/laytonsmith/core/exceptions/FunctionReturnException.java delete mode 100644 src/main/java/com/laytonsmith/core/exceptions/LoopBreakException.java delete mode 100644 src/main/java/com/laytonsmith/core/exceptions/LoopContinueException.java delete mode 100644 src/main/java/com/laytonsmith/core/exceptions/LoopManipulationException.java delete mode 100644 src/main/java/com/laytonsmith/core/exceptions/ProgramFlowManipulationException.java create mode 100644 src/main/java/com/laytonsmith/core/exceptions/UnhandledFlowControlException.java create mode 100644 src/test/java/com/laytonsmith/core/functions/EventBindingTest.java diff --git a/src/main/java/com/laytonsmith/core/EvalStack.java b/src/main/java/com/laytonsmith/core/EvalStack.java new file mode 100644 index 0000000000..4ef65d42d4 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/EvalStack.java @@ -0,0 +1,60 @@ +package com.laytonsmith.core; + +import java.util.ArrayDeque; +import java.util.Iterator; + +/** + * A wrapper around an {@link ArrayDeque} of {@link StackFrame}s that provides a debugger-friendly + * {@link #toString()} showing the current execution stack in the style of MethodScript stack traces. + */ +public class EvalStack implements Iterable { + + private final ArrayDeque stack; + + public EvalStack() { + this.stack = new ArrayDeque<>(); + } + + public void push(StackFrame frame) { + stack.push(frame); + } + + public StackFrame pop() { + return stack.pop(); + } + + public StackFrame peek() { + return stack.peek(); + } + + public boolean isEmpty() { + return stack.isEmpty(); + } + + public int size() { + return stack.size(); + } + + @Override + public Iterator iterator() { + return stack.iterator(); + } + + @Override + public String toString() { + if(stack.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + boolean first = true; + Iterator it = stack.descendingIterator(); + while(it.hasNext()) { + if(!first) { + sb.append("\n"); + } + sb.append("at ").append(it.next().toString()); + first = false; + } + return sb.toString(); + } +} diff --git a/src/main/java/com/laytonsmith/core/FlowFunction.java b/src/main/java/com/laytonsmith/core/FlowFunction.java new file mode 100644 index 0000000000..4ded753ae4 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/FlowFunction.java @@ -0,0 +1,92 @@ +package com.laytonsmith.core; + +import com.laytonsmith.core.StepAction.StepResult; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.natives.interfaces.Mixed; +import com.laytonsmith.core.environments.Environment; + +/** + * Interface for functions that need to control how their children are evaluated. + * This replaces the old {@code execs()} mechanism. Instead of a function calling + * {@code parent.eval()} recursively, the interpreter loop calls the FlowFunction methods + * and the function returns {@link StepAction} values to direct evaluation. + * + *

Functions that don't need special child evaluation (the majority) don't implement + * this interface — the interpreter loop evaluates all their children left-to-right, + * then calls {@code exec()} with the results.

+ * + *

Functions that DO need it (if, for, and, try, etc.) implement this interface + * and are driven by the interpreter loop via begin/childCompleted/childInterrupted.

+ * + *

The type parameter {@code S} is the per-call state type. Since function instances + * are singletons, per-call mutable state cannot be stored on the function itself. + * Instead, methods receive and return state via {@link StepAction.StepResult}. + * The interpreter stores this state on the {@link StackFrame} as {@code Object} and + * passes it back (with an unchecked cast) on subsequent calls. Functions that need + * no per-call state should use {@code Void} and pass {@code null}.

+ */ +public interface FlowFunction { + + /** + * Called when this function frame is first entered. The function should return + * a {@link StepAction.StepResult} containing the first action (typically + * {@link StepAction.Evaluate}) and the initial per-call state. + * + * @param t The code target of the function call + * @param children The unevaluated child parse trees (same as what execs() received) + * @param env The current environment + * @return The first step action paired with initial state + */ + StepResult begin(Target t, ParseTree[] children, Environment env); + + /** + * Called each time a child evaluation (requested via {@link StepAction.Evaluate}) + * completes successfully. The function receives the result and its per-call state, + * and returns the next action paired with updated state. + * + * @param t The code target of the function call + * @param state The per-call state from the previous step + * @param result The result of the child evaluation + * @param env The current environment + * @return The next step action paired with updated state + */ + StepResult childCompleted(Target t, S state, Mixed result, Environment env); + + /** + * Called when a child evaluation produced a {@link StepAction.FlowControl} action + * that is propagating up the stack. The function can choose to handle it (e.g., + * a loop handling a break action) or let it propagate by returning {@code null}. + * + *

For example, {@code _for}'s implementation handles {@code BreakAction} by completing + * the loop, and handles {@code ContinueAction} by jumping to the increment step. + * {@code _try}'s implementation handles {@code ThrowAction} by switching to the catch branch.

+ * + *

The default implementation returns {@code null}, propagating the action up.

+ * + * @param t The code target of the function call + * @param state The per-call state from the previous step + * @param action The flow control action propagating through this frame + * @param env The current environment + * @return A {@link StepAction.StepResult} to handle it, or {@code null} to propagate + */ + default StepResult childInterrupted(Target t, S state, StepAction.FlowControl action, Environment env) { + return null; + } + + /** + * Called when this function's frame is being removed from the stack, regardless of + * the reason (normal completion, flow control propagation, or exception). This is + * the FlowFunction equivalent of a {@code finally} block — use it to restore + * environment state that was modified in {@code begin()} (e.g., command sender, + * dynamic scripting mode, stack trace elements). + * + *

This is called exactly once per frame, after the final action has been determined + * but before the frame is actually popped. The default implementation is a no-op.

+ * + * @param t The code target of the function call + * @param state The per-call state (may be null if begin() hasn't been called) + * @param env The current environment + */ + default void cleanup(Target t, S state, Environment env) { + } +} diff --git a/src/main/java/com/laytonsmith/core/LocalPackages.java b/src/main/java/com/laytonsmith/core/LocalPackages.java index 59a04bb91b..2d626db405 100644 --- a/src/main/java/com/laytonsmith/core/LocalPackages.java +++ b/src/main/java/com/laytonsmith/core/LocalPackages.java @@ -9,7 +9,6 @@ import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigCompileGroupException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.functions.IncludeCache; import com.laytonsmith.core.profiler.ProfilePoint; @@ -223,9 +222,6 @@ public void executeMS(Environment env) { if(e.getMessage() != null && !e.getMessage().trim().isEmpty()) { Static.getLogger().log(Level.INFO, e.getMessage()); } - } catch (ProgramFlowManipulationException e) { - ConfigRuntimeException.HandleUncaughtException(ConfigRuntimeException.CreateUncatchableException( - "Cannot break program flow in main files.", e.getTarget()), env); } } } diff --git a/src/main/java/com/laytonsmith/core/Method.java b/src/main/java/com/laytonsmith/core/Method.java index cac125930b..00610ebbf6 100644 --- a/src/main/java/com/laytonsmith/core/Method.java +++ b/src/main/java/com/laytonsmith/core/Method.java @@ -9,7 +9,6 @@ import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.natives.interfaces.Mixed; import java.util.Arrays; @@ -40,7 +39,7 @@ public Method(Target t, Environment env, CClassType returnType, String name, CCl @Override public Mixed executeCallable(Environment env, Target t, Mixed... values) - throws ConfigRuntimeException, ProgramFlowManipulationException, CancelCommandException { + throws ConfigRuntimeException, CancelCommandException { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } diff --git a/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java b/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java index 859975a91d..136b3b1641 100644 --- a/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java +++ b/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java @@ -77,6 +77,7 @@ import java.util.EmptyStackException; import java.util.EnumSet; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; @@ -3053,12 +3054,27 @@ public static Mixed execute(String script, File file, boolean inPureMScript, Env * @return */ public static Mixed execute(ParseTree root, Environment env, MethodScriptComplete done, Script script) { - return execute(root, env, done, script, null); + Mixed result; + if(root == null) { + result = CVoid.VOID; + } else { + if(script == null) { + script = new Script(null, null, env.getEnv(GlobalEnv.class).GetLabel(), env.getEnvClasses(), + root.getFileOptions(), null); + } + result = script.eval(root, env); + } + if(done != null) { + done.done(result.val().trim()); + } + return result; } /** * Executes a pre-compiled MethodScript, given the specified Script environment, but also provides a method to set - * the constants in the script. + * the constants in the script. The $variable bindings are resolved by walking the tree and mapping each Variable + * node's identity to its resolved value, then storing the map in the environment. See + * {@link GlobalEnv#SetDollarVarBindings} for details on why identity-based lookup is used. * * @param root * @param env @@ -3068,53 +3084,23 @@ public static Mixed execute(ParseTree root, Environment env, MethodScriptComplet * @return */ public static Mixed execute(ParseTree root, Environment env, MethodScriptComplete done, Script script, List vars) { - if(root == null) { - return CVoid.VOID; - } - if(script == null) { - script = new Script(null, null, env.getEnv(GlobalEnv.class).GetLabel(), env.getEnvClasses(), - root.getFileOptions(), null); - } - if(vars != null) { - Map varMap = new HashMap<>(); + if(root != null && vars != null && !vars.isEmpty()) { + Map varValues = new HashMap<>(); for(Variable v : vars) { - varMap.put(v.getVariableName(), v); + varValues.put(v.getVariableName(), v.getDefault()); } + IdentityHashMap dollarBindings = new IdentityHashMap<>(); for(Mixed tempNode : root.getAllData()) { if(tempNode instanceof Variable variable) { - Variable vv = varMap.get(variable.getVariableName()); - if(vv != null) { - variable.setVal(vv.getDefault()); - } else { - //The variable is unset. I'm not quite sure what cases would cause this - variable.setVal(""); + String val = varValues.get(variable.getVariableName()); + if(val != null) { + dollarBindings.put(tempNode, val); } } } + env.getEnv(GlobalEnv.class).SetDollarVarBindings(dollarBindings); } - StringBuilder b = new StringBuilder(); - Mixed returnable = null; - for(ParseTree gg : root.getChildren()) { - Mixed retc = script.eval(gg, env); - if(root.numberOfChildren() == 1) { - returnable = retc; - if(done == null) { - // string builder is not needed, so return immediately - return returnable; - } - } - String ret = retc.val(); - if(!ret.trim().isEmpty()) { - b.append(ret).append(" "); - } - } - if(done != null) { - done.done(b.toString().trim()); - } - if(returnable != null) { - return returnable; - } - return Static.resolveConstruct(b.toString().trim(), Target.UNKNOWN); + return execute(root, env, done, script); } private static final List PDF_STACK = Arrays.asList( diff --git a/src/main/java/com/laytonsmith/core/Procedure.java b/src/main/java/com/laytonsmith/core/Procedure.java index 193473331e..1b9cf37b94 100644 --- a/src/main/java/com/laytonsmith/core/Procedure.java +++ b/src/main/java/com/laytonsmith/core/Procedure.java @@ -20,14 +20,13 @@ import com.laytonsmith.core.exceptions.CRE.CREStackOverflowError; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.FunctionReturnException; -import com.laytonsmith.core.exceptions.LoopManipulationException; import com.laytonsmith.core.exceptions.StackTraceManager; +import com.laytonsmith.core.exceptions.UnhandledFlowControlException; import com.laytonsmith.core.functions.ControlFlow; +import com.laytonsmith.core.functions.Exceptions; import com.laytonsmith.core.functions.Function; import com.laytonsmith.core.functions.FunctionBase; import com.laytonsmith.core.functions.FunctionList; -import com.laytonsmith.core.functions.StringHandling; import com.laytonsmith.core.natives.interfaces.Mixed; import java.util.ArrayList; @@ -194,28 +193,87 @@ public Mixed cexecute(List args, Environment env, Target t) { * @return */ public Mixed execute(List args, Environment oldEnv, Target t) { + Environment env = prepareEnvironment(args, oldEnv, t); + + Script fakeScript = Script.GenerateScript(tree, env.getEnv(GlobalEnv.class).GetLabel(), null); + StackTraceManager stManager = env.getEnv(GlobalEnv.class).GetStackTraceManager(); + stManager.addStackTraceElement(new ConfigRuntimeException.StackTraceElement("proc " + name, getTarget())); + try { + Mixed result = fakeScript.eval(tree, env); + if(result == null) { + result = CVoid.VOID; + } + return typeCheckReturn(result, env); + } catch(UnhandledFlowControlException e) { + if(e.getAction() instanceof ControlFlow.BreakAction + || e.getAction() instanceof ControlFlow.ContinueAction) { + throw ConfigRuntimeException.CreateUncatchableException( + "Loop manipulation operations (e.g. break() or continue()) cannot" + + " bubble up past procedures.", t); + } + if(e.getAction() instanceof Exceptions.ThrowAction ta) { + ConfigRuntimeException ex = ta.getException(); + if(ex instanceof AbstractCREException ace) { + ace.freezeStackTraceElements(stManager); + } + throw ex; + } + throw e; + } catch(StackOverflowError e) { + throw new CREStackOverflowError(null, t, e); + } finally { + stManager.popStackTraceElement(); + } + } + + public Target getTarget() { + return definedAt; + } + + @Override + public Procedure clone() throws CloneNotSupportedException { + Procedure clone = (Procedure) super.clone(); + if(this.varList != null) { + clone.varList = new HashMap<>(this.varList); + } + if(this.tree != null) { + clone.tree = this.tree.clone(); + } + return clone; + } + + public void definitelyNotConstant() { + possiblyConstant = false; + } + + /** + * Clones the environment and assigns procedure arguments (with type checking). + * Used by both {@link #execute} and {@link ProcedureFlow}. + * + * @param args The evaluated argument values + * @param oldEnv The caller's environment (will be cloned) + * @param callTarget The target of the procedure call site + * @return The prepared environment for the procedure body + */ + private Environment prepareEnvironment(List args, Environment oldEnv, Target callTarget) { boolean prev = oldEnv.getEnv(GlobalEnv.class).getCloneVars(); oldEnv.getEnv(GlobalEnv.class).setCloneVars(false); Environment env; try { env = oldEnv.clone(); env.getEnv(GlobalEnv.class).setCloneVars(true); - } catch (CloneNotSupportedException ex) { + } catch(CloneNotSupportedException ex) { throw new RuntimeException(ex); } oldEnv.getEnv(GlobalEnv.class).setCloneVars(prev); - Script fakeScript = Script.GenerateScript(tree, env.getEnv(GlobalEnv.class).GetLabel(), null); - - // Create container for the @arguments variable. CArray arguments = new CArray(Target.UNKNOWN, this.varIndex.size()); - // Handle passed procedure arguments. int varInd; CArray vararg = null; for(varInd = 0; varInd < args.size(); varInd++) { Mixed c = args.get(varInd); - arguments.push(c, t); + arguments.push(c, callTarget); if(this.varIndex.size() > varInd || (!this.varIndex.isEmpty() && this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVariadicType())) { @@ -226,14 +284,12 @@ public Mixed execute(List args, Environment oldEnv, Target t) { } else { var = this.varIndex.get(this.varIndex.size() - 1); if(vararg == null) { - // TODO: Once generics are added, add the type - vararg = new CArray(t); + vararg = new CArray(callTarget); env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, var.getVariableName(), vararg, c.getTarget())); } } - // Type check "void" value. if(c instanceof CVoid && !(var.getDefinedType().equals(Auto.TYPE) || var.getDefinedType().equals(CVoid.TYPE))) { throw new CRECastException("Procedure \"" + name + "\" expects a value of type " @@ -241,10 +297,9 @@ public Mixed execute(List args, Environment oldEnv, Target t) { + " a void value was found instead.", c.getTarget()); } - // Type check vararg parameter. if(var.getDefinedType().isVariadicType()) { if(InstanceofUtil.isInstanceof(c.typeof(env), var.getDefinedType().getVarargsBaseType(), env)) { - vararg.push(c, t); + vararg.push(c, callTarget); continue; } else { throw new CRECastException("Procedure \"" + name + "\" expects a value of type " @@ -253,7 +308,6 @@ public Mixed execute(List args, Environment oldEnv, Target t) { } } - // Type check non-vararg parameter. if(InstanceofUtil.isInstanceof(c.typeof(env), var.getDefinedType(), env)) { env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(var.getDefinedType(), var.getVariableName(), c, c.getTarget())); @@ -266,91 +320,144 @@ public Mixed execute(List args, Environment oldEnv, Target t) { } } - // Assign default values to remaining proc arguments. while(varInd < this.varIndex.size()) { String varName = this.varIndex.get(varInd++).getVariableName(); Mixed defVal = this.originals.get(varName); env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(Auto.TYPE, varName, defVal, defVal.getTarget())); - arguments.push(defVal, t); + arguments.push(defVal, callTarget); } - env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, "@arguments", arguments, t)); - StackTraceManager stManager = env.getEnv(GlobalEnv.class).GetStackTraceManager(); - stManager.addStackTraceElement(new ConfigRuntimeException.StackTraceElement("proc " + name, getTarget())); - try { - if(tree.getData() instanceof CFunction - && tree.getData().val().equals(StringHandling.sconcat.NAME)) { - //If the inner tree is just an sconcat, we can optimize by - //simply running the arguments to the sconcat. We're not going - //to use the results, after all, and this is a common occurrence, - //because the compiler will often put it there automatically. - //We *could* optimize this by removing it from the compiled code, - //and we still should do that, but this check is quick enough, - //and so can remain even once we do add the optimization to the - //compiler proper. - for(ParseTree child : tree.getChildren()) { - fakeScript.eval(child, env); - } - } else { - fakeScript.eval(tree, env); - } - } catch (FunctionReturnException e) { - // Normal exit - Mixed ret = e.getReturn(); - if(returnType.equals(Auto.TYPE)) { - return ret; - } - if(returnType.equals(CVoid.TYPE) != ret.equals(CVoid.VOID) - || !ret.equals(CNull.NULL) && !ret.equals(CVoid.VOID) - && !InstanceofUtil.isInstanceof(ret.typeof(env), returnType, env)) { - throw new CRECastException("Expected procedure \"" + name + "\" to return a value of type " - + returnType.val() + " but a value of type " + ret.typeof(env) + " was returned instead", - ret.getTarget()); - } + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, "@arguments", arguments, callTarget)); + return env; + } + + /** + * Type-checks a return value against this procedure's declared return type. + */ + private Mixed typeCheckReturn(Mixed ret, Environment env) { + if(returnType.equals(Auto.TYPE)) { return ret; - } catch (LoopManipulationException ex) { - // These cannot bubble up past procedure calls. This will eventually be - // a compile error. - throw ConfigRuntimeException.CreateUncatchableException("Loop manipulation operations (e.g. break() or continue()) cannot" - + " bubble up past procedures.", t); - } catch (ConfigRuntimeException e) { - if(e instanceof AbstractCREException) { - ((AbstractCREException) e).freezeStackTraceElements(stManager); - } - throw e; - } catch (StackOverflowError e) { - throw new CREStackOverflowError(null, t, e); - } finally { - stManager.popStackTraceElement(); } - // Normal exit, but no return. - // If we got here, then there was no return value. This is fine, but only for returnType void or auto. - // TODO: Once strong typing is implemented at a compiler level, this should be removed to increase runtime - // performance. + if(returnType.equals(CVoid.TYPE) != ret.equals(CVoid.VOID) + || !ret.equals(CNull.NULL) && !ret.equals(CVoid.VOID) + && !InstanceofUtil.isInstanceof(ret.typeof(env), returnType, env)) { + throw new CRECastException("Expected procedure \"" + name + "\" to return a value of type " + + returnType.val() + " but a value of type " + ret.typeof(env) + " was returned instead", + ret.getTarget()); + } + return ret; + } + + /** + * Checks that this procedure's return type allows a void return (no explicit return statement). + */ + private Mixed typeCheckVoidReturn() { if(!(returnType.equals(Auto.TYPE) || returnType.equals(CVoid.TYPE))) { - throw new CRECastException("Expecting procedure \"" + name + "\" to return a value of type " + returnType.val() + "," - + " but no value was returned.", tree.getTarget()); + throw new CRECastException("Expecting procedure \"" + name + "\" to return a value of type " + + returnType.val() + ", but no value was returned.", tree.getTarget()); } return CVoid.VOID; } - public Target getTarget() { - return definedAt; + /** + * Creates a {@link FlowFunction} for this procedure call, for use with the iterative + * interpreter. The flow function manages the procedure call lifecycle: + *
    + *
  1. Evaluates argument expressions (with IVariable resolution)
  2. + *
  3. Prepares the procedure environment (clones env, assigns parameters)
  4. + *
  5. Evaluates the procedure body in the new environment
  6. + *
  7. Handles Return (type-checks and completes), blocks Break/Continue
  8. + *
+ * + * @param callTarget The target of the procedure call site + * @return A per-call FlowFunction for this procedure + */ + public FlowFunction createProcedureFlow(Target callTarget) { + return new ProcedureFlow(callTarget); } - @Override - public Procedure clone() throws CloneNotSupportedException { - Procedure clone = (Procedure) super.clone(); - if(this.varList != null) { - clone.varList = new HashMap<>(this.varList); + /** + * Per-call flow function for procedure execution in the iterative interpreter. + * Manages the two-phase lifecycle: arg evaluation then body evaluation. + * Since this is created per-call, it stores state in its own fields + * rather than using the generic S type parameter. + */ + private class ProcedureFlow implements FlowFunction { + private final Target callTarget; + private final List evaluatedArgs = new ArrayList<>(); + private ParseTree[] children; + private int argIndex = 0; + private boolean bodyStarted = false; + private Environment procEnv; + + ProcedureFlow(Target callTarget) { + this.callTarget = callTarget; } - if(this.tree != null) { - clone.tree = this.tree.clone(); + + @Override + public StepAction.StepResult begin(Target t, ParseTree[] children, Environment env) { + this.children = children; + if(children.length == 0) { + return new StepAction.StepResult<>(startBody(env), null); + } + return new StepAction.StepResult<>(new StepAction.Evaluate(children[0]), null); } - return clone; - } - public void definitelyNotConstant() { - possiblyConstant = false; + @Override + public StepAction.StepResult childCompleted(Target t, Void state, Mixed result, Environment env) { + if(!bodyStarted) { + // Resolve IVariables (seval semantics for proc arguments) + Mixed resolved = result; + while(resolved instanceof IVariable cur) { + resolved = env.getEnv(GlobalEnv.class).GetVarList() + .get(cur.getVariableName(), cur.getTarget(), env).ival(); + } + evaluatedArgs.add(resolved); + argIndex++; + if(argIndex < children.length) { + return new StepAction.StepResult<>(new StepAction.Evaluate(children[argIndex]), null); + } + return new StepAction.StepResult<>(startBody(env), null); + } + // Body completed normally (no explicit return) + return new StepAction.StepResult<>(new StepAction.Complete(typeCheckVoidReturn()), null); + } + + @Override + public StepAction.StepResult childInterrupted(Target t, Void state, + StepAction.FlowControl action, Environment env) { + StepAction.FlowControlAction fca = action.getAction(); + if(fca instanceof ControlFlow.ReturnAction ret) { + return new StepAction.StepResult<>( + new StepAction.Complete(typeCheckReturn(ret.getValue(), procEnv)), null); + } + if(fca instanceof ControlFlow.BreakAction || fca instanceof ControlFlow.ContinueAction) { + throw ConfigRuntimeException.CreateUncatchableException( + "Loop manipulation operations (e.g. break() or continue()) cannot" + + " bubble up past procedures.", callTarget); + } + // Unknown flow control — propagate + return null; + } + + @Override + public void cleanup(Target t, Void state, Environment env) { + popStackTrace(); + } + + private StepAction startBody(Environment callerEnv) { + bodyStarted = true; + procEnv = prepareEnvironment(evaluatedArgs, callerEnv, callTarget); + StackTraceManager stManager = procEnv.getEnv(GlobalEnv.class).GetStackTraceManager(); + stManager.addStackTraceElement( + new ConfigRuntimeException.StackTraceElement("proc " + name, getTarget())); + return new StepAction.Evaluate(tree, procEnv); + } + + private void popStackTrace() { + if(procEnv != null) { + procEnv.getEnv(GlobalEnv.class).GetStackTraceManager().popStackTraceElement(); + } + } } } diff --git a/src/main/java/com/laytonsmith/core/Script.java b/src/main/java/com/laytonsmith/core/Script.java index 2b44b5d5c5..6df42b33d1 100644 --- a/src/main/java/com/laytonsmith/core/Script.java +++ b/src/main/java/com/laytonsmith/core/Script.java @@ -5,16 +5,14 @@ import com.laytonsmith.PureUtilities.SimpleVersion; import com.laytonsmith.PureUtilities.SmartComment; import com.laytonsmith.PureUtilities.TermColors; -import com.laytonsmith.abstraction.Implementation; import com.laytonsmith.abstraction.MCCommandSender; import com.laytonsmith.abstraction.MCPlayer; -import com.laytonsmith.abstraction.StaticLayer; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.compiler.TokenStream; import com.laytonsmith.core.compiler.analysis.StaticAnalysis; -import com.laytonsmith.core.constructs.CClosure; import com.laytonsmith.core.constructs.CFunction; import com.laytonsmith.core.constructs.CString; +import com.laytonsmith.core.constructs.CVoid; import com.laytonsmith.core.constructs.Command; import com.laytonsmith.core.constructs.Construct; import com.laytonsmith.core.constructs.Construct.ConstructType; @@ -26,33 +24,23 @@ import com.laytonsmith.core.environments.CommandHelperEnvironment; import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.environments.GlobalEnv; -import com.laytonsmith.core.environments.InvalidEnvironmentException; import com.laytonsmith.core.environments.StaticRuntimeEnv; -import com.laytonsmith.core.exceptions.CRE.AbstractCREException; import com.laytonsmith.core.exceptions.CRE.CREInsufficientPermissionException; import com.laytonsmith.core.exceptions.CRE.CREInvalidProcedureException; -import com.laytonsmith.core.exceptions.CRE.CREStackOverflowError; 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.exceptions.ProgramFlowManipulationException; -import com.laytonsmith.core.exceptions.StackTraceManager; -import com.laytonsmith.core.extensions.Extension; -import com.laytonsmith.core.extensions.ExtensionManager; -import com.laytonsmith.core.extensions.ExtensionTracker; +import com.laytonsmith.core.exceptions.UnhandledFlowControlException; +import com.laytonsmith.core.functions.ControlFlow; +import com.laytonsmith.core.functions.Exceptions; import com.laytonsmith.core.functions.Function; -import com.laytonsmith.core.functions.FunctionBase; import com.laytonsmith.core.natives.interfaces.Mixed; -import com.laytonsmith.core.profiler.ProfilePoint; -import com.laytonsmith.core.profiler.Profiler; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -227,15 +215,20 @@ public void run(final List vars, Environment myEnv, final MethodScript if(rootNode == null) { continue; } - for(Mixed tempNode : rootNode.getAllData()) { - if(tempNode instanceof Variable) { - if(leftVars == null) { - throw ConfigRuntimeException.CreateUncatchableException("$variables may not be used in this context." - + " Only @variables may be.", tempNode.getTarget()); + if(leftVars != null) { + IdentityHashMap dollarBindings = new IdentityHashMap<>(); + for(Mixed tempNode : rootNode.getAllData()) { + if(tempNode instanceof Variable variable) { + Construct leftVar = leftVars.get(variable.getVariableName()); + if(leftVar == null) { + throw ConfigRuntimeException.CreateUncatchableException("$variables may not be used in this context." + + " Only @variables may be.", tempNode.getTarget()); + } + Construct c = Static.resolveDollarVar(leftVar, vars); + dollarBindings.put(tempNode, c.toString()); } - Construct c = Static.resolveDollarVar(leftVars.get(((Variable) tempNode).getVariableName()), vars); - ((Variable) tempNode).setVal(new CString(c.toString(), tempNode.getTarget())); } + myEnv.getEnv(GlobalEnv.class).SetDollarVarBindings(dollarBindings); } myEnv.getEnv(StaticRuntimeEnv.class).getIncludeCache().executeAutoIncludes(myEnv, this); @@ -247,25 +240,6 @@ public void run(final List vars, Environment myEnv, final MethodScript } catch (CancelCommandException e) { //p.sendMessage(e.getMessage()); //The message in the exception is actually empty - } catch (LoopBreakException e) { - if(p != null) { - p.sendMessage("The break() function must be used inside a for() or foreach() loop"); - } - StreamUtils.GetSystemOut().println("The break() function must be used inside a for() or foreach() loop"); - } catch (LoopContinueException e) { - if(p != null) { - p.sendMessage("The continue() function must be used inside a for() or foreach() loop"); - } - StreamUtils.GetSystemOut().println("The continue() function must be used inside a for() or foreach() loop"); - } catch (FunctionReturnException e) { - if(myEnv.getEnv(GlobalEnv.class).GetEvent() != null) { - //Oh, we're running in an event handler. Those know how to catch it too. - throw e; - } - if(p != null) { - p.sendMessage("The return() function must be used inside a procedure."); - } - StreamUtils.GetSystemOut().println("The return() function must be used inside a procedure."); } catch (Throwable t) { StreamUtils.GetSystemOut().println("An unexpected exception occurred during the execution of a script."); t.printStackTrace(); @@ -295,267 +269,264 @@ public Mixed seval(ParseTree c, final Environment env) { } /** - * Given the parse tree and environment, executes the tree. + * Iterative interpreter loop. Evaluates a parse tree using an explicit stack instead of + * recursive Java calls. This enables: + *
    + *
  • Control flow (break/continue/return) as first-class FlowControl actions, not exceptions
  • + *
  • Save/restore of execution state (for debugger, async/await)
  • + *
  • 1:1 MethodScript-to-stack-frame mapping
  • + *
* - * @param c - * @param env - * @return - * @throws CancelCommandException + *

Two execution paths exist for function calls:

+ *
    + *
  1. FlowFunction — Function implements {@link FlowFunction}. The loop drives it + * via begin/childCompleted/childInterrupted. (This replaces the old execs mechanism.)
  2. + *
  3. Simple exec — Normal functions. Children are evaluated left-to-right, + * then {@code exec()} is called with the results.
  4. + *
+ * + * @param root The parse tree to evaluate + * @param env The environment + * @return The result of evaluation */ - @SuppressWarnings("UseSpecificCatch") - public Mixed eval(ParseTree c, final Environment env) throws CancelCommandException { - GlobalEnv globalEnv = env.getEnv(GlobalEnv.class); - if(globalEnv.IsInterrupted()) { - //First things first, if we're interrupted, kill the script unconditionally. - throw new CancelCommandException("", Target.UNKNOWN); - } - - final Mixed m = c.getData(); - if(m instanceof Construct co) { - if(co.getCType() != ConstructType.FUNCTION) { - if(co.getCType() == ConstructType.VARIABLE) { - return new CString(m.val(), m.getTarget()); - } else { - return m; + @SuppressWarnings("unchecked") + private Mixed iterativeEval(ParseTree root, Environment env) { + EvalStack stack = new EvalStack(); + stack.push(new StackFrame(root, env, null, null)); + Mixed lastResult = null; + boolean hasResult = false; + StepAction.FlowControl pendingFlowControl = null; + + while(!stack.isEmpty()) { + GlobalEnv gEnv = env.getEnv(GlobalEnv.class); + + if(gEnv.IsInterrupted()) { + throw new CancelCommandException("", Target.UNKNOWN); + } + + // Propagate pending flow control + StackFrame frame = stack.peek(); + if(pendingFlowControl != null) { + if(frame.hasFlowFunction() && frame.hasBegun()) { + Target t = frame.getNode().getTarget(); + StepAction.StepResult response = + ((FlowFunction) frame.getFlowFunction()).childInterrupted( + t, frame.getFunctionState(), pendingFlowControl, frame.getEnv()); + if(response != null) { + pendingFlowControl = null; + frame.setFunctionState(response.getState()); + StepAction action = response.getAction(); + if(action instanceof StepAction.Evaluate e) { + frame.setKeepIVariable(e.keepIVariable()); + Environment evalEnv = e.getEnv() != null ? e.getEnv() : frame.getEnv(); + stack.push(new StackFrame(e.getNode(), evalEnv, null, null)); + } else if(action instanceof StepAction.Complete c) { + lastResult = c.getResult(); + hasResult = true; + cleanupAndPop(stack, frame); + } else if(action instanceof StepAction.FlowControl fc) { + pendingFlowControl = fc; + cleanupAndPop(stack, frame); + } + continue; + } } - } - } - - final CFunction possibleFunction; - try { - possibleFunction = (CFunction) m; - } catch (ClassCastException e) { - throw ConfigRuntimeException.CreateUncatchableException("Expected to find CFunction at runtime but found: " - + m.val(), m.getTarget()); - } - - StackTraceManager stManager = globalEnv.GetStackTraceManager(); - boolean addedRootStackElement = false; - try { - // If it's an unknown target, this is not user generated code, and we want to skip adding the element here. - if(stManager.isStackEmpty() && m.getTarget() != Target.UNKNOWN) { - stManager.addStackTraceElement(new ConfigRuntimeException.StackTraceElement("<
>", m.getTarget())); - addedRootStackElement = true; - } - stManager.setCurrentTarget(c.getTarget()); - globalEnv.SetScript(this); - - if(possibleFunction.hasProcedure()) { - //Not really a function, so we can't put it in Function. - Procedure p = globalEnv.GetProcs().get(m.val()); - if(p == null) { - throw new CREInvalidProcedureException("Unknown procedure \"" + m.val() + "\"", m.getTarget()); - } - ProfilePoint pp = null; - Profiler profiler = env.getEnv(StaticRuntimeEnv.class).GetProfiler(); - if(profiler.isLoggable(LogLevel.INFO)) { - pp = profiler.start(m.val() + " execution", LogLevel.INFO); - } - Mixed ret; - try { - if(debugOutput) { - doDebugOutput(p.getName(), c.getChildren()); + cleanupAndPop(stack, frame); + if(stack.isEmpty()) { + if(pendingFlowControl.getAction() + instanceof ControlFlow.ReturnAction ret) { + return ret.getValue(); } - ret = p.cexecute(c.getChildren(), env, m.getTarget()); - } finally { - if(pp != null) { - pp.stop(); + if(pendingFlowControl.getAction() + instanceof Exceptions.ThrowAction ta) { + throw ta.getException(); } + throw new UnhandledFlowControlException(pendingFlowControl.getAction()); } - return ret; - } - - final Function f; - try { - f = possibleFunction.getFunction(); - } catch (ConfigCompileException e) { - //Turn it into a config runtime exception. This shouldn't ever happen though. - throw ConfigRuntimeException.CreateUncatchableException("Unable to find function at runtime: " - + m.val(), m.getTarget()); + continue; } - globalEnv.SetFileOptions(c.getFileOptions()); - - Mixed[] args = new Mixed[c.numberOfChildren()]; - try { - if(f.isRestricted() && !Static.hasCHPermission(f.getName(), env)) { - throw new CREInsufficientPermissionException("You do not have permission to use the " - + f.getName() + " function.", m.getTarget()); - } + ParseTree node = frame.getNode(); + Mixed data = node.getData(); - if(debugOutput) { - doDebugOutput(f.getName(), c.getChildren()); - } - if(f.useSpecialExec()) { - ProfilePoint p = null; - if(f.shouldProfile()) { - Profiler profiler = env.getEnv(StaticRuntimeEnv.class).GetProfiler(); - if(profiler.isLoggable(f.profileAt())) { - p = profiler.start(f.profileMessageS(c.getChildren()), f.profileAt()); - } - } - Mixed ret; - try { - ret = f.execs(m.getTarget(), env, this, c.getChildren().toArray(new ParseTree[args.length])); - } finally { - if(p != null) { - p.stop(); - } - } - return ret; - } - - for(int i = 0; i < args.length; i++) { - args[i] = eval(c.getChildAt(i), env); - while(f.preResolveVariables() && args[i] instanceof IVariable cur) { - args[i] = globalEnv.GetVarList().get(cur.getVariableName(), cur.getTarget(), env).ival(); + // Literal / variable nodes (no children) + if(data instanceof Construct co && co.getCType() != Construct.ConstructType.FUNCTION + && node.numberOfChildren() == 0) { + if(co.getCType() == Construct.ConstructType.VARIABLE) { + String val = gEnv.GetDollarVarBinding(data); + if(val == null) { + val = ""; } + lastResult = new CString(val, data.getTarget()); + } else { + lastResult = data; } + hasResult = true; + stack.pop(); + continue; + } - // Reset stacktrace manager to current function (argument evaluation might have changed this). - stManager.setCurrentTarget(c.getTarget()); - - { - //It takes a moment to generate the toString of some things, so lets not do it - //if we actually aren't going to profile - ProfilePoint p = null; - if(f.shouldProfile()) { - Profiler profiler = env.getEnv(StaticRuntimeEnv.class).GetProfiler(); - if(profiler.isLoggable(f.profileAt())) { - p = profiler.start(Function.ExecuteProfileMessage(f, env, args), f.profileAt()); - } - } - Mixed ret; - try { - ret = Function.ExecuteFunction(f, m.getTarget(), env, args); - } finally { - if(p != null) { - p.stop(); + // Sequence nodes (non-function with children, e.g. root node) skip + // function resolution and use simple exec with function=null + if(data instanceof CFunction cfunc) { + // First visit: resolve function or procedure + if(frame.getFunction() == null && !frame.hasFlowFunction()) { + if(cfunc.hasProcedure()) { + Procedure p = gEnv.GetProcs().get(data.val()); + if(p == null) { + throw new CREInvalidProcedureException( + "Unknown procedure \"" + data.val() + "\"", data.getTarget()); } + FlowFunction procedureFlow = p.createProcedureFlow(data.getTarget()); + stack.pop(); + stack.push(new StackFrame(node, frame.getEnv(), null, procedureFlow)); + hasResult = false; + continue; } - return ret; - } - //We want to catch and rethrow the ones we know how to catch, and then - //catch and report anything else. - } catch (ConfigRuntimeException | ProgramFlowManipulationException e) { - if(e instanceof AbstractCREException) { - ((AbstractCREException) e).freezeStackTraceElements(stManager); - } - throw e; - } catch (InvalidEnvironmentException e) { - if(!e.isDataSet()) { - e.setData(f.getName()); - } - throw e; - } catch (StackOverflowError e) { - // This handles this in all cases that weren't previously considered. But it still should - // be individually handled by other cases to ensure that the stack trace is more correct - throw new CREStackOverflowError(null, c.getTarget(), e); - } catch (Throwable e) { - if(e instanceof ThreadDeath) { - // Bail quickly in this case - throw e; - } - String brand = Implementation.GetServerType().getBranding(); - SimpleVersion version; - try { - version = Static.getVersion(); - } catch (Throwable ex) { - // This failing should not be a dealbreaker, so fill it with default data - version = GARBAGE_VERSION; - } - - String culprit = brand; - outer: - for(ExtensionTracker tracker : ExtensionManager.getTrackers().values()) { - for(FunctionBase b : tracker.getFunctions()) { - if(b.getName().equals(f.getName())) { - //This extension provided the function, so its the culprit. Report this - //name instead of the core plugin's name. - for(Extension extension : tracker.getExtensions()) { - culprit = extension.getName(); - break outer; - } + Function f = cfunc.getCachedFunction(); + if(f == null) { + try { + f = cfunc.getFunction(); + } catch(ConfigCompileException ex) { + throw ConfigRuntimeException.CreateUncatchableException( + "Unknown function \"" + cfunc.val() + "\"", cfunc.getTarget()); } } - } - String emsg = TermColors.RED + "Uh oh! You've found an error in " + TermColors.CYAN + culprit + TermColors.RED + ".\n" - + "This happened while running your code, so you may be able to find a workaround," - + (!(e instanceof Exception) ? " (though since this is an Error, maybe not)" : "") - + " but is ultimately an issue in " + culprit + ".\n" - + "The following code caused the error:\n" + TermColors.WHITE; - - List args2 = new ArrayList<>(); - Map vars = new HashMap<>(); - for(int i = 0; i < args.length; i++) { - Mixed cc = args[i]; - if(c.getChildAt(i).getData() instanceof IVariable ivar) { - String vval = cc.val(); - if(cc instanceof CString) { - vval = ((CString) cc).getQuote(); + FlowFunction flowFunction = (f instanceof FlowFunction) ? (FlowFunction) f : null; + + stack.pop(); + StackFrame newFrame = new StackFrame(node, frame.getEnv(), f, flowFunction); + stack.push(newFrame); + frame = newFrame; + hasResult = false; + } + } + + Function f = frame.getFunction(); + + // Permission check on first visit + if(!frame.hasBegun() && f != null && f.isRestricted() + && !Static.hasCHPermission(f.getName(), frame.getEnv())) { + throw new CREInsufficientPermissionException( + "You do not have permission to use the " + f.getName() + " function.", + data.getTarget()); + } + + // Flow function mode + if(frame.hasFlowFunction()) { + Target t = node.getTarget(); + StepAction.StepResult result; + if(!frame.hasBegun()) { + frame.markBegun(); + result = ((FlowFunction) frame.getFlowFunction()).begin( + t, frame.getChildren(), frame.getEnv()); + } else if(hasResult) { + // Resolve IVariables unless the parent explicitly asked to keep them + if(!frame.keepIVariable()) { + while(lastResult instanceof IVariable cur) { + GlobalEnv frameGEnv = frame.getEnv().getEnv(GlobalEnv.class); + lastResult = frameGEnv.GetVarList() + .get(cur.getVariableName(), cur.getTarget(), + frame.getEnv()).ival(); } - vars.put(ivar.getVariableName(), vval); - args2.add(ivar.getVariableName()); - } else if(cc == null) { - args2.add("java-null"); - } else if(cc instanceof CString) { - args2.add(new CString(cc.val(), Target.UNKNOWN).getQuote()); - } else if(cc instanceof CClosure) { - args2.add(""); - } else { - args2.add(cc.val()); } + frame.setKeepIVariable(false); + result = ((FlowFunction) frame.getFlowFunction()).childCompleted( + t, frame.getFunctionState(), lastResult, frame.getEnv()); + hasResult = false; + } else { + throw ConfigRuntimeException.CreateUncatchableException( + "Flow function in invalid state for " + data.val(), data.getTarget()); + } + + frame.setFunctionState(result.getState()); + StepAction action = result.getAction(); + if(action instanceof StepAction.Evaluate e) { + frame.setKeepIVariable(e.keepIVariable()); + Environment evalEnv = e.getEnv() != null ? e.getEnv() : frame.getEnv(); + stack.push(new StackFrame(e.getNode(), evalEnv, null, null)); + } else if(action instanceof StepAction.Complete c) { + lastResult = c.getResult(); + hasResult = true; + cleanupAndPop(stack, frame); + } else if(action instanceof StepAction.FlowControl fc) { + pendingFlowControl = fc; + cleanupAndPop(stack, frame); } - if(!vars.isEmpty()) { - emsg += StringUtils.Join(vars, " = ", "\n") + "\n"; - } - emsg += f.getName() + "("; - emsg += StringUtils.Join(args2, ", "); - emsg += ")\n"; - - emsg += TermColors.RED + "on or around " - + TermColors.YELLOW + m.getTarget().file() + TermColors.WHITE + ":" + TermColors.CYAN - + m.getTarget().line() + TermColors.RED + ".\n"; + continue; + } - //Server might not be available in this platform, so let's be sure to ignore those exceptions - String modVersion; - try { - modVersion = StaticLayer.GetConvertor().GetServer().getAPIVersion(); - } catch (Exception ex) { - modVersion = Implementation.GetServerType().name(); + // Simple exec mode + if(hasResult) { + Mixed arg = lastResult; + while(f != null && f.preResolveVariables() && arg instanceof IVariable cur) { + GlobalEnv frameGEnv = frame.getEnv().getEnv(GlobalEnv.class); + arg = frameGEnv.GetVarList().get(cur.getVariableName(), cur.getTarget(), + frame.getEnv()).ival(); } + frame.addArg(arg); + hasResult = false; + } - String extensionData = ""; - for(ExtensionTracker tracker : ExtensionManager.getTrackers().values()) { - for(Extension extension : tracker.getExtensions()) { - try { - extensionData += TermColors.CYAN + extension.getName() + TermColors.RED - + " (" + TermColors.RESET + extension.getVersion() + TermColors.RED + ")\n"; - } catch (AbstractMethodError ex) { - // This happens with an old style extensions. Just skip it. - extensionData += TermColors.CYAN + "Unknown Extension" + TermColors.RED + "\n"; - } - } + if(frame.hasMoreChildren()) { + if(!frame.hasBegun()) { + frame.markBegun(); } - if(extensionData.isEmpty()) { - extensionData = "NONE\n"; + stack.push(new StackFrame(frame.nextChild(), frame.getEnv(), null, null)); + } else { + if(!frame.hasBegun()) { + frame.markBegun(); + } + if(f == null) { + // Sequence node — return last child's result + Mixed[] args = frame.getArgs(); + lastResult = args.length > 0 ? args[args.length - 1] : CVoid.VOID; + hasResult = true; + stack.pop(); + } else { + try { + lastResult = Function.ExecuteFunction(f, data.getTarget(), + frame.getEnv(), frame.getArgs()); + hasResult = true; + stack.pop(); + } catch(ConfigRuntimeException e) { + // Convert MethodScript exceptions to FlowControl(ThrowAction) + stack.pop(); + pendingFlowControl = new StepAction.FlowControl( + new com.laytonsmith.core.functions.Exceptions.ThrowAction(e)); + } } - - emsg += "Please report this to the developers, and be sure to include the version numbers:\n" - + TermColors.CYAN + "Server" + TermColors.RED + " version: " + TermColors.RESET + modVersion + TermColors.RED + ";\n" - + TermColors.CYAN + brand + TermColors.RED + " version: " + TermColors.RESET + version + TermColors.RED + ";\n" - + "Loaded extensions and versions:\n" + extensionData - + "Here's the stacktrace:\n" + TermColors.RESET + Static.GetStacktraceString(e); - StreamUtils.GetSystemErr().println(emsg); - throw new CancelCommandException(null, Target.UNKNOWN); - } - } finally { - if(addedRootStackElement && stManager.isStackSingle()) { - stManager.popStackTraceElement(); } } + + return lastResult; + } + + /** + * Calls {@link FlowFunction#cleanup} if the frame has a FlowFunction that has begun, + * then pops the frame from the stack. + */ + @SuppressWarnings("unchecked") + private static void cleanupAndPop(EvalStack stack, StackFrame frame) { + if(frame.hasFlowFunction() && frame.hasBegun()) { + ((FlowFunction) frame.getFlowFunction()).cleanup( + frame.getNode().getTarget(), frame.getFunctionState(), frame.getEnv()); + } + stack.pop(); + } + + /** + * Given the parse tree and environment, executes the tree. + * + * @param c + * @param env + * @return + * @throws CancelCommandException + */ + public Mixed eval(ParseTree c, final Environment env) throws CancelCommandException { + return iterativeEval(c, env); } private void doDebugOutput(String nodeName, List children) { diff --git a/src/main/java/com/laytonsmith/core/StackFrame.java b/src/main/java/com/laytonsmith/core/StackFrame.java new file mode 100644 index 0000000000..09d952c7d3 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/StackFrame.java @@ -0,0 +1,213 @@ +package com.laytonsmith.core; + +import com.laytonsmith.core.constructs.IVariable; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.natives.interfaces.Mixed; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.functions.Function; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents one frame on the interpreter's explicit evaluation stack. Each frame corresponds + * to a function call or node being evaluated. + * + *

There are two modes:

+ *
    + *
  • Simple mode ({@code flowFunction == null}): The interpreter evaluates children + * left-to-right, accumulates results in {@code args}, then calls {@code exec()}. + * This is used for normal (non-special-exec) functions.
  • + *
  • Flow function mode ({@code flowFunction != null}): The interpreter delegates to the + * {@link FlowFunction} to decide which children to evaluate and when. + * This is used for control flow functions (if, for, and, try, etc.).
  • + *
+ */ +public class StackFrame { + + private final ParseTree node; + private final Environment env; + private final Function function; + private final FlowFunction flowFunction; + private final List args; + private int childIndex; + private boolean begun; + private Object functionState; + private boolean keepIVariable; + + /** + * Creates a stack frame for evaluating the given node. + * + * @param node The parse tree node being evaluated + * @param env The environment at this frame's scope + * @param function The function being called (may be null for literal nodes or procedure calls) + * @param flowFunction The flow function for special-exec functions (null for simple exec) + */ + public StackFrame(ParseTree node, Environment env, Function function, FlowFunction flowFunction) { + this.node = node; + this.env = env; + this.function = function; + this.flowFunction = flowFunction; + this.args = new ArrayList<>(); + this.childIndex = 0; + this.begun = false; + this.functionState = null; + } + + /** + * Returns the parse tree node this frame is evaluating. + */ + public ParseTree getNode() { + return node; + } + + /** + * Returns the environment at this frame's scope. + */ + public Environment getEnv() { + return env; + } + + /** + * Returns the function being called, or null for literal nodes or procedure calls. + */ + public Function getFunction() { + return function; + } + + /** + * Returns the flow functionr for special-exec functions, or null for simple exec. + */ + public FlowFunction getFlowFunction() { + return flowFunction; + } + + /** + * Returns whether this frame uses a flow function (special-exec) or simple child evaluation. + */ + public boolean hasFlowFunction() { + return flowFunction != null; + } + + /** + * Returns the per-call flow function state. The interpreter stores this opaquely and + * passes it back to flow function methods via unchecked cast to the flow function's type parameter. + */ + public Object getFunctionState() { + return functionState; + } + + /** + * Sets the per-call flow function state. + */ + public void setFunctionState(Object state) { + this.functionState = state; + } + + /** + * Sets whether the next child result should keep IVariable as-is (not resolve to value). + */ + public void setKeepIVariable(boolean keepIVariable) { + this.keepIVariable = keepIVariable; + } + + /** + * Returns true if the next child result should keep IVariable as-is. + */ + public boolean keepIVariable() { + return keepIVariable; + } + + /** + * Returns the children of the parse tree node as an array. + */ + public ParseTree[] getChildren() { + List children = node.getChildren(); + return children.toArray(new ParseTree[0]); + } + + /** + * Returns the number of children of this node. + */ + public int getChildCount() { + return node.numberOfChildren(); + } + + /** + * Returns the next child index to evaluate (for simple mode). + */ + public int getChildIndex() { + return childIndex; + } + + /** + * Returns true if there are more children to evaluate (for simple mode). + */ + public boolean hasMoreChildren() { + return childIndex < getChildCount(); + } + + /** + * Returns the next child to evaluate and advances the index (for simple mode). + */ + public ParseTree nextChild() { + return node.getChildAt(childIndex++); + } + + /** + * Adds an evaluated child result to the args list (for simple mode). + */ + public void addArg(Mixed result) { + args.add(result); + } + + /** + * Returns the accumulated evaluated arguments (for simple mode). + */ + public Mixed[] getArgs() { + return args.toArray(new Mixed[0]); + } + + /** + * Returns whether begin() has been called on the flow function yet. + */ + public boolean hasBegun() { + return begun; + } + + /** + * Marks this frame's flow function as having been started. + */ + public void markBegun() { + this.begun = true; + } + + @Override + public String toString() { + String name; + if(function != null) { + name = function.getName(); + } else if(node.getData() instanceof IVariable iv) { + name = iv.getVariableName(); + } else { + name = node.getData().val(); + } + Target t = node.getTarget(); + String location = t.file() + ":" + t.line() + "." + t.col(); + String mode = flowFunction != null ? "flow" : "simple"; + String state = begun ? "begun" : "pending"; + String detail; + if(flowFunction != null) { + String stateStr = functionState != null ? functionState.toString() : "null"; + detail = ", state=" + stateStr; + } else { + detail = ", child " + childIndex + "/" + getChildCount(); + } + String inner = name + ":" + location + " (" + mode + ", " + state + detail + ")"; + // Proc calls are user-visible stack frames + if(function == null && flowFunction != null) { + return "[" + inner + "]"; + } + return inner; + } +} diff --git a/src/main/java/com/laytonsmith/core/StepAction.java b/src/main/java/com/laytonsmith/core/StepAction.java new file mode 100644 index 0000000000..8c9681701f --- /dev/null +++ b/src/main/java/com/laytonsmith/core/StepAction.java @@ -0,0 +1,156 @@ +package com.laytonsmith.core; + +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.natives.interfaces.Mixed; + +/** + * Represents an action that a function returns to the interpreter loop, telling it what to do next. + * The interpreter loop understands three kinds of actions: + *
    + *
  • {@link Evaluate} — evaluate a child node, then call back with the result
  • + *
  • {@link Complete} — this function is done, here's the result
  • + *
  • {@link FlowControl} — a control flow action is propagating up the stack
  • + *
+ * + *

The interpreter loop does not know about specific flow control types (break, continue, return, etc.). + * Those are defined by the functions that produce and consume them, via {@link FlowControlAction}.

+ */ +public abstract class StepAction { + + private StepAction() { + } + + /** + * Tells the interpreter loop to evaluate the given parse tree node. Once evaluation completes, + * the result is passed back to the current frame's {@link FlowFunction#childCompleted}. + * + *

If an environment is provided, the child frame will use that environment instead of + * inheriting the parent frame's environment. This is used by procedure calls, which evaluate + * their body in a cloned environment.

+ */ + public static final class Evaluate extends StepAction { + private final ParseTree node; + private final Environment env; + private final boolean keepIVariable; + + public Evaluate(ParseTree node) { + this(node, null, false); + } + + /** + * @param node The node to evaluate + * @param env The environment to evaluate in, or null to use the parent frame's environment + */ + public Evaluate(ParseTree node, Environment env) { + this(node, env, false); + } + + /** + * @param node The node to evaluate + * @param env The environment to evaluate in, or null to use the parent frame's environment + * @param keepIVariable If true, the result is returned as-is even if it's an IVariable. + * If false (default), IVariables are resolved to their values before being passed + * to childCompleted. + */ + public Evaluate(ParseTree node, Environment env, boolean keepIVariable) { + this.node = node; + this.env = env; + this.keepIVariable = keepIVariable; + } + + public ParseTree getNode() { + return node; + } + + /** + * Returns the environment to evaluate in, or null to use the parent frame's environment. + */ + public Environment getEnv() { + return env; + } + + /** + * Returns true if IVariable results should be kept as-is rather than resolved. + */ + public boolean keepIVariable() { + return keepIVariable; + } + } + + /** + * Tells the interpreter loop that the current function is done, and provides its result value. + */ + public static final class Complete extends StepAction { + private final Mixed result; + + public Complete(Mixed result) { + this.result = result; + } + + public Mixed getResult() { + return result; + } + } + + /** + * Tells the interpreter loop that a control flow action is propagating up the stack. + * The loop will pass this to each frame's {@link FlowFunction#childInterrupted} as it + * unwinds, until a frame handles it or it reaches the top of the stack. + * + *

The interpreter loop does not inspect the {@link FlowControlAction} — specific flow control + * types (break, continue, return, throw, etc.) are defined alongside the functions that + * produce and consume them.

+ */ + public static final class FlowControl extends StepAction { + private final FlowControlAction action; + + public FlowControl(FlowControlAction action) { + this.action = action; + } + + public FlowControlAction getAction() { + return action; + } + } + + /** + * Marker interface for control flow actions that propagate up the interpreter stack. + * Concrete implementations are defined alongside the functions that produce/consume them + * (e.g., BreakAction lives near _break in ControlFlow.java). + * + *

The interpreter loop treats all FlowControlActions generically — it does not know about + * specific types. This allows extensions to define custom control flow without modifying core.

+ */ + public interface FlowControlAction { + /** + * Returns the code location where this action originated. + */ + Target getTarget(); + } + + /** + * Pairs a {@link StepAction} with the flow function's per-call state. Returned by + * {@link FlowFunction} methods so the interpreter loop can store the state + * on the {@link StackFrame} without knowing its type. + * + * @param The flow function's state type + */ + public static final class StepResult { + private final StepAction action; + private final S state; + + public StepResult(StepAction action, S state) { + this.action = action; + this.state = state; + } + + public StepAction getAction() { + return action; + } + + public S getState() { + return state; + } + } +} diff --git a/src/main/java/com/laytonsmith/core/asm/LLVMFunction.java b/src/main/java/com/laytonsmith/core/asm/LLVMFunction.java index 1caaf2f979..05cc39ef60 100644 --- a/src/main/java/com/laytonsmith/core/asm/LLVMFunction.java +++ b/src/main/java/com/laytonsmith/core/asm/LLVMFunction.java @@ -7,7 +7,6 @@ import com.laytonsmith.core.Documentation; import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; import com.laytonsmith.core.compiler.SelfStatement; import com.laytonsmith.core.compiler.analysis.Scope; import com.laytonsmith.core.compiler.analysis.StaticAnalysis; @@ -237,16 +236,6 @@ public int compareTo(Function o) { */ public abstract IRData buildIR(IRBuilder builder, Target t, Environment env, GenericParameters parameters, ParseTree... nodes) throws ConfigCompileException; - @Override - public final boolean useSpecialExec() { - throw new UnsupportedOperationException("Not supported."); - } - - @Override - public final Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - throw new UnsupportedOperationException("Not supported."); - } - /** * If this function is used, and it needs to do startup configuration, that configuration goes here. * diff --git a/src/main/java/com/laytonsmith/core/constructs/CClosure.java b/src/main/java/com/laytonsmith/core/constructs/CClosure.java index 78e9bc476b..8e67d6bad3 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CClosure.java +++ b/src/main/java/com/laytonsmith/core/constructs/CClosure.java @@ -7,8 +7,9 @@ import com.laytonsmith.core.natives.interfaces.Booleanish; import com.laytonsmith.core.natives.interfaces.Callable; import com.laytonsmith.core.MSVersion; -import com.laytonsmith.core.MethodScriptCompiler; import com.laytonsmith.core.ParseTree; +import com.laytonsmith.core.Script; +import com.laytonsmith.core.StepAction; import com.laytonsmith.core.compiler.CompilerEnvironment; import com.laytonsmith.core.compiler.CompilerWarning; import com.laytonsmith.core.compiler.FileOptions; @@ -20,14 +21,10 @@ import com.laytonsmith.core.exceptions.CRE.CREStackOverflowError; import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.FunctionReturnException; -import com.laytonsmith.core.exceptions.LoopManipulationException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.exceptions.StackTraceManager; -import com.laytonsmith.core.functions.DataHandling; +import com.laytonsmith.core.exceptions.UnhandledFlowControlException; +import com.laytonsmith.core.functions.Exceptions; import com.laytonsmith.core.natives.interfaces.Mixed; -import java.util.ArrayList; -import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -149,7 +146,6 @@ public synchronized Environment getEnv() { * @param values * @return * @throws ConfigRuntimeException - * @throws ProgramFlowManipulationException * @throws CancelCommandException */ public Mixed executeCallable(Mixed... values) { @@ -160,9 +156,7 @@ public Mixed executeCallable(Mixed... values) { * Executes the closure, giving it the supplied arguments. {@code values} may be null, which means that no arguments * are being sent. * - * LoopManipulationExceptions will never bubble up past this point, because they are never allowed, so they are - * handled automatically, but other ProgramFlowManipulationExceptions will, . ConfigRuntimeExceptions will also - * bubble up past this, since an execution mechanism may need to do custom handling. + * ConfigRuntimeExceptions will bubble up past this, since an execution mechanism may need to do custom handling. * * A typical execution will include the following code: *
@@ -170,7 +164,7 @@ public Mixed executeCallable(Mixed... values) {
 	 *	closure.execute();
 	 * } catch (ConfigRuntimeException e){
 	 *	ConfigRuntimeException.HandleUncaughtException(e);
-	 * } catch (ProgramFlowManipulationException e){
+	 * } catch (CancelCommandException e){
 	 *	// Ignored
 	 * }
 	 * 
@@ -181,31 +175,23 @@ public Mixed executeCallable(Mixed... values) { * @param values The values to be passed to the closure * @return The return value of the closure, or VOID if nothing was returned * @throws ConfigRuntimeException If any call inside the closure causes a CRE - * @throws ProgramFlowManipulationException If any ProgramFlowManipulationException is thrown (other than a - * LoopManipulationException) within the closure + * @throws CancelCommandException If die() is called within the closure */ @Override public Mixed executeCallable(Environment env, Target t, Mixed... values) - throws ConfigRuntimeException, ProgramFlowManipulationException, CancelCommandException { - try { - execute(values); - } catch (FunctionReturnException e) { - return e.getReturn(); - } - return CVoid.VOID; + throws ConfigRuntimeException, CancelCommandException { + return execute(values); } /** * @param values * @throws ConfigRuntimeException - * @throws ProgramFlowManipulationException - * @throws FunctionReturnException * @throws CancelCommandException */ - protected void execute(Mixed... values) throws ConfigRuntimeException, ProgramFlowManipulationException, - FunctionReturnException, CancelCommandException { + protected Mixed execute(Mixed... values) throws ConfigRuntimeException, + CancelCommandException { if(node == null) { - return; + return CVoid.VOID; } Environment env; try { @@ -214,7 +200,7 @@ protected void execute(Mixed... values) throws ConfigRuntimeException, ProgramFl } } catch (CloneNotSupportedException ex) { Logger.getLogger(CClosure.class.getName()).log(Level.SEVERE, null, ex); - return; + return CVoid.VOID; } StackTraceManager stManager = env.getEnv(GlobalEnv.class).GetStackTraceManager(); stManager.addStackTraceElement(new ConfigRuntimeException.StackTraceElement("<>", getTarget())); @@ -276,50 +262,44 @@ protected void execute(Mixed... values) throws ConfigRuntimeException, ProgramFl node.getData().getTarget())); } - ParseTree newNode = new ParseTree(new CFunction(DataHandling.g.NAME, getTarget()), node.getFileOptions()); - List children = new ArrayList<>(); - children.add(node); - newNode.setChildren(children); + Script script = env.getEnv(GlobalEnv.class).GetScript(); + if(script == null) { + script = Script.GenerateScript(node, env.getEnv(GlobalEnv.class).GetLabel(), null); + } + Mixed result; try { - MethodScriptCompiler.execute(newNode, env, null, env.getEnv(GlobalEnv.class) - .GetScript()); - } catch (LoopManipulationException e) { - //This shouldn't ever happen. - LoopManipulationException lme = ((LoopManipulationException) e); - Target t = lme.getTarget(); + result = script.eval(node, env); + } catch(UnhandledFlowControlException e) { + StepAction.FlowControlAction action = e.getAction(); + if(action instanceof Exceptions.ThrowAction throwAction) { + ConfigRuntimeException ex = throwAction.getException(); + ex.setEnv(env); + if(ex instanceof AbstractCREException) { + ((AbstractCREException) ex).freezeStackTraceElements(stManager); + } + throw ex; + } + Target t = action.getTarget(); ConfigRuntimeException.HandleUncaughtException(ConfigRuntimeException.CreateUncatchableException("A " - + lme.getName() + "() bubbled up to the top of" + + "flow control action bubbled up to the top of" + " a closure, which is unexpected behavior.", t), env); - } catch (FunctionReturnException ex) { - // Check the return type of the closure to see if it matches the defined type - // Normal execution. - Mixed ret = ex.getReturn(); - if(!InstanceofUtil.isInstanceof(ret.typeof(env), returnType, env)) { - throw new CRECastException("Expected closure to return a value of type " + returnType.val() - + " but a value of type " + ret.typeof(env) + " was returned instead", ret.getTarget()); - } - // Now rethrow it - throw ex; - } catch (CancelCommandException e) { - // die() - } catch (ConfigRuntimeException ex) { - ex.setEnv(env); - if(ex instanceof AbstractCREException) { - ((AbstractCREException) ex).freezeStackTraceElements(stManager); - } - throw ex; - } catch (StackOverflowError e) { + return CVoid.VOID; + } catch(StackOverflowError e) { throw new CREStackOverflowError(null, node.getTarget(), e); - } finally { - stManager.popStackTraceElement(); } - // If we got here, then there was no return type. This is fine, but only for returnType void or auto. - if(!(returnType.equals(Auto.TYPE) || returnType.equals(CVoid.TYPE))) { - throw new CRECastException("Expecting closure to return a value of type " + returnType.val() + "," - + " but no value was returned.", node.getTarget()); + + if(!returnType.equals(Auto.TYPE) + && !InstanceofUtil.isInstanceof(result.typeof(env), returnType, env)) { + throw new CRECastException("Expected closure to return a value of type " + returnType.val() + + " but a value of type " + result.typeof(env) + " was returned instead", + result.getTarget()); } + return result; } catch (CloneNotSupportedException ex) { Logger.getLogger(CClosure.class.getName()).log(Level.SEVERE, null, ex); + return CVoid.VOID; + } finally { + stManager.popStackTraceElement(); } } diff --git a/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java b/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java index c97fdd4e21..573f9f3ffc 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java +++ b/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java @@ -6,7 +6,6 @@ import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.natives.interfaces.Callable; import com.laytonsmith.core.natives.interfaces.Mixed; @@ -39,7 +38,7 @@ public boolean isDynamic() { } @Override - public Mixed executeCallable(Environment env, Target t, Mixed... values) throws ConfigRuntimeException, ProgramFlowManipulationException, CancelCommandException { + public Mixed executeCallable(Environment env, Target t, Mixed... values) throws ConfigRuntimeException, CancelCommandException { return runnable.execute(t, env, values); } diff --git a/src/main/java/com/laytonsmith/core/constructs/ProcedureUsage.java b/src/main/java/com/laytonsmith/core/constructs/ProcedureUsage.java index 2726bc7e0b..6528ed4c7d 100644 --- a/src/main/java/com/laytonsmith/core/constructs/ProcedureUsage.java +++ b/src/main/java/com/laytonsmith/core/constructs/ProcedureUsage.java @@ -7,7 +7,6 @@ import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.natives.interfaces.Callable; import com.laytonsmith.core.natives.interfaces.Mixed; import com.laytonsmith.core.objects.ObjectModifier; @@ -77,7 +76,7 @@ public Set getObjectModifiers() { @Override public Mixed executeCallable(Environment env, Target t, Mixed... values) throws ConfigRuntimeException, - ProgramFlowManipulationException, CancelCommandException { + CancelCommandException { return proc.execute(Arrays.asList(values), env, t); } diff --git a/src/main/java/com/laytonsmith/core/environments/GlobalEnv.java b/src/main/java/com/laytonsmith/core/environments/GlobalEnv.java index 704b1a9d72..1b67930b92 100644 --- a/src/main/java/com/laytonsmith/core/environments/GlobalEnv.java +++ b/src/main/java/com/laytonsmith/core/environments/GlobalEnv.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -60,6 +61,28 @@ public class GlobalEnv implements Environment.EnvironmentImpl, Cloneable { private FileOptions fileOptions; private ScriptProvider scriptProvider = new ScriptProvider.FileSystemScriptProvider(); + // $variable (dollar-variable) bindings for the current execution context. + // + // $variables are alias command parameters (e.g. /cmd $x = msg($x)), and also + // command-line script arguments ($0, $1, $). They are scoped to the compiled + // tree in which they appear — the old implementation walked the entire parse tree + // and mutated each Variable node in place via setVal(). This was thread-unsafe: + // if two threads executed the same alias concurrently, one thread's values would + // overwrite the other's, since they shared the same compiled tree. + // + // The new design avoids tree mutation entirely. Instead, we collect the identity + // (object reference) of every Variable node found in the tree via getAllData(), + // resolve its value, and store the mapping here using an IdentityHashMap. The + // interpreter checks this map when it encounters a Variable node. Because identity + // is used (not equals), a $x node that appears in the original alias tree is resolved, + // but a $x node in an unrelated tree (e.g. a different alias or a separately compiled + // include) is not — preserving the original tree-scoped visibility semantics. + // + // This map is set once at the start of execution (in Script.run() or + // MethodScriptCompiler.execute()) and is carried through the environment into + // nested calls (procs defined inline, etc.) without modification. + private IdentityHashMap dollarVarBindings = null; + /** * Creates a new GlobalEnvironment. All fields in the constructor are required, and cannot be null. * @@ -518,4 +541,24 @@ public ScriptProvider GetScriptProvider() { public void SetScriptProvider(ScriptProvider provider) { this.scriptProvider = provider; } + + /** + * Sets the resolved $variable bindings for this execution. The map uses identity-based + * lookup (IdentityHashMap) so that only the specific Variable nodes from the original + * tree are resolved — this preserves tree-scoped visibility without mutating the tree. + */ + public void SetDollarVarBindings(IdentityHashMap bindings) { + this.dollarVarBindings = bindings; + } + + /** + * Returns the resolved value for a specific $variable node, or null if the node + * is not in scope. + */ + public String GetDollarVarBinding(Mixed variableNode) { + if(dollarVarBindings == null) { + return null; + } + return dollarVarBindings.get(variableNode); + } } diff --git a/src/main/java/com/laytonsmith/core/events/AbstractGenericEvent.java b/src/main/java/com/laytonsmith/core/events/AbstractGenericEvent.java index 7d89a2ded8..f340d02c04 100644 --- a/src/main/java/com/laytonsmith/core/events/AbstractGenericEvent.java +++ b/src/main/java/com/laytonsmith/core/events/AbstractGenericEvent.java @@ -22,16 +22,13 @@ import com.laytonsmith.core.environments.StaticRuntimeEnv; import com.laytonsmith.core.events.prefilters.Prefilter; import com.laytonsmith.core.events.prefilters.PrefilterBuilder; -import com.laytonsmith.core.exceptions.CRE.CREFormatException; import com.laytonsmith.core.exceptions.CRE.CREUnsupportedOperationException; 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.EventException; -import com.laytonsmith.core.exceptions.FunctionReturnException; import com.laytonsmith.core.exceptions.PrefilterNonMatchException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.natives.interfaces.Mixed; import com.laytonsmith.core.profiler.ProfilePoint; @@ -157,10 +154,6 @@ public final void execute(ParseTree tree, BoundEvent b, Environment env, BoundEv if(ex.getMessage() != null && !ex.getMessage().isEmpty()) { StreamUtils.GetSystemOut().println(ex.getMessage()); } - } catch(FunctionReturnException ex) { - //We simply allow this to end the event execution - } catch(ProgramFlowManipulationException ex) { - ConfigRuntimeException.HandleUncaughtException(new CREFormatException("Unexpected control flow operation used.", ex.getTarget()), env); } } finally { if(event != null) { diff --git a/src/main/java/com/laytonsmith/core/events/EventUtils.java b/src/main/java/com/laytonsmith/core/events/EventUtils.java index cdc1d1bd39..6695a66bd5 100644 --- a/src/main/java/com/laytonsmith/core/events/EventUtils.java +++ b/src/main/java/com/laytonsmith/core/events/EventUtils.java @@ -19,7 +19,6 @@ import com.laytonsmith.core.extensions.ExtensionTracker; import com.laytonsmith.core.exceptions.ConfigRuntimeException; import com.laytonsmith.core.exceptions.EventException; -import com.laytonsmith.core.exceptions.FunctionReturnException; import com.laytonsmith.core.exceptions.PrefilterNonMatchException; import com.laytonsmith.core.natives.interfaces.Mixed; @@ -339,8 +338,6 @@ public static void FireListeners(SortedSet toRun, Event driver, Bind activeEvent.setBoundEvent(b); activeEvent.setParsedEvent(Event.ExecuteEvaluate(driver, e, b.getEnvironment())); b.trigger(activeEvent); - } catch (FunctionReturnException ex) { - //We also know how to deal with this } catch (EventException ex) { throw new CREEventException(ex.getMessage(), b.getTarget(), ex); } catch (ConfigRuntimeException ex) { diff --git a/src/main/java/com/laytonsmith/core/exceptions/CancelCommandException.java b/src/main/java/com/laytonsmith/core/exceptions/CancelCommandException.java index 2b3502e406..c395203eaf 100644 --- a/src/main/java/com/laytonsmith/core/exceptions/CancelCommandException.java +++ b/src/main/java/com/laytonsmith/core/exceptions/CancelCommandException.java @@ -3,15 +3,15 @@ import com.laytonsmith.core.constructs.Target; /** - * - * + * Thrown by constructs like die() to cancel the current command execution. */ -public class CancelCommandException extends ProgramFlowManipulationException { +public class CancelCommandException extends RuntimeException { + private final Target t; String message; public CancelCommandException(String message, Target t) { - super(t); + this.t = t; this.message = message; } @@ -20,4 +20,18 @@ public String getMessage() { return message; } + /** + * Returns the code target at which this cancel was triggered. + * + * @return + */ + public Target getTarget() { + return t; + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + } diff --git a/src/main/java/com/laytonsmith/core/exceptions/FunctionReturnException.java b/src/main/java/com/laytonsmith/core/exceptions/FunctionReturnException.java deleted file mode 100644 index e443ddf8d9..0000000000 --- a/src/main/java/com/laytonsmith/core/exceptions/FunctionReturnException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.laytonsmith.core.exceptions; - -import com.laytonsmith.core.constructs.Target; -import com.laytonsmith.core.natives.interfaces.Mixed; - -/** - * - * - */ -public class FunctionReturnException extends ProgramFlowManipulationException { - - Mixed ret; - - public FunctionReturnException(Mixed ret, Target t) { - super(t); - this.ret = ret; - } - - public Mixed getReturn() { - return ret; - } -} diff --git a/src/main/java/com/laytonsmith/core/exceptions/LoopBreakException.java b/src/main/java/com/laytonsmith/core/exceptions/LoopBreakException.java deleted file mode 100644 index c53f759156..0000000000 --- a/src/main/java/com/laytonsmith/core/exceptions/LoopBreakException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.laytonsmith.core.exceptions; - -import com.laytonsmith.core.constructs.Target; - -/** - * - * - */ -public class LoopBreakException extends LoopManipulationException { - - public LoopBreakException(int times, Target t) { - super(times, "break", t); - } -} diff --git a/src/main/java/com/laytonsmith/core/exceptions/LoopContinueException.java b/src/main/java/com/laytonsmith/core/exceptions/LoopContinueException.java deleted file mode 100644 index baccd776f7..0000000000 --- a/src/main/java/com/laytonsmith/core/exceptions/LoopContinueException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.laytonsmith.core.exceptions; - -import com.laytonsmith.core.constructs.Target; - -/** - * - * - */ -public class LoopContinueException extends LoopManipulationException { - - public LoopContinueException(int times, Target t) { - super(times, "continue", t); - } -} diff --git a/src/main/java/com/laytonsmith/core/exceptions/LoopManipulationException.java b/src/main/java/com/laytonsmith/core/exceptions/LoopManipulationException.java deleted file mode 100644 index f57d3c9638..0000000000 --- a/src/main/java/com/laytonsmith/core/exceptions/LoopManipulationException.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.laytonsmith.core.exceptions; - -import com.laytonsmith.core.constructs.Target; - -/** - * This is thrown by constructs like break and continue to indicate that a loop specific - * ProgramFlowManipulationException is being thrown. - */ -public abstract class LoopManipulationException extends ProgramFlowManipulationException { - - private int times; - private final String name; - - protected LoopManipulationException(int times, String name, Target t) { - super(t); - this.times = times; - this.name = name; - } - - /** - * Returns the number of times specified in the loop manipulation. - * - * @return - */ - public int getTimes() { - return times; - } - - /** - * Sets the number of times remaining in the loop manipulation. After handling an iteration, you should decrement - * the number and set it here. - * - * @param number - */ - public void setTimes(int number) { - this.times = number; - } - - /** - * Returns the construct name that triggers this loop manipulation, i.e: break or continue. - * - * @return - */ - public String getName() { - return name; - } -} diff --git a/src/main/java/com/laytonsmith/core/exceptions/ProgramFlowManipulationException.java b/src/main/java/com/laytonsmith/core/exceptions/ProgramFlowManipulationException.java deleted file mode 100644 index 11dbb737d4..0000000000 --- a/src/main/java/com/laytonsmith/core/exceptions/ProgramFlowManipulationException.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.laytonsmith.core.exceptions; - -import com.laytonsmith.core.constructs.Target; - -/** - * If an exception is meant to break the program flow in the script itself, it should extend this, so if an exception - * passes all the way up to a top level handler, it can address it in a standard way if it doesn't know what to do with - * these types of exceptions. Things like break, continue, etc are considered Program Flow Manipulations. - * - */ -public abstract class ProgramFlowManipulationException extends RuntimeException { - - private final Target t; - - /** - * - * @param t The target at which this program flow manipulation construct was defined. - */ - protected ProgramFlowManipulationException(Target t) { - this.t = t; - } - - /** - * Returns the code target at which this program flow manipulation construct was defined, so that if it was used - * improperly, a full stacktrace can be shown. - * - * @return - */ - public Target getTarget() { - return t; - } - - @Override - public Throwable fillInStackTrace() { - return this; - } -} diff --git a/src/main/java/com/laytonsmith/core/exceptions/UnhandledFlowControlException.java b/src/main/java/com/laytonsmith/core/exceptions/UnhandledFlowControlException.java new file mode 100644 index 0000000000..dfd1c99f3c --- /dev/null +++ b/src/main/java/com/laytonsmith/core/exceptions/UnhandledFlowControlException.java @@ -0,0 +1,27 @@ +package com.laytonsmith.core.exceptions; + +import com.laytonsmith.core.StepAction; + +/** + * Thrown by the iterative interpreter when a {@link StepAction.FlowControl} action + * propagates to the top of the stack without being handled by any frame. The top-level + * caller (e.g., {@code Script.run()}) catches this and dispatches based on the action + * type, matching the behavior of the old exception-based system. + */ +public class UnhandledFlowControlException extends RuntimeException { + + private final StepAction.FlowControlAction action; + + public UnhandledFlowControlException(StepAction.FlowControlAction action) { + this.action = action; + } + + public StepAction.FlowControlAction getAction() { + return action; + } + + @Override + public Throwable fillInStackTrace() { + return this; + } +} diff --git a/src/main/java/com/laytonsmith/core/functions/AbstractFunction.java b/src/main/java/com/laytonsmith/core/functions/AbstractFunction.java index fcb9a0f6e7..3b3fc7eec2 100644 --- a/src/main/java/com/laytonsmith/core/functions/AbstractFunction.java +++ b/src/main/java/com/laytonsmith/core/functions/AbstractFunction.java @@ -12,7 +12,6 @@ import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.MethodScriptCompiler; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; import com.laytonsmith.core.SimpleDocumentation; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.compiler.SelfStatement; @@ -24,7 +23,6 @@ import com.laytonsmith.core.constructs.CClosure; import com.laytonsmith.core.constructs.CFunction; import com.laytonsmith.core.constructs.CString; -import com.laytonsmith.core.constructs.CVoid; import com.laytonsmith.core.constructs.IVariable; import com.laytonsmith.core.constructs.IVariableList; import com.laytonsmith.core.constructs.Target; @@ -58,22 +56,6 @@ protected AbstractFunction() { shouldProfile = !this.getClass().isAnnotationPresent(noprofile.class); } - /** - * {@inheritDoc} - * - * By default, we return CVoid. - * - * @param t - * @param env - * @param parent - * @param nodes - * @return - */ - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - return CVoid.VOID; - } - /** * {@inheritDoc} * Calling {@link #getCachedSignatures()} where possible is preferred for runtime performance. @@ -203,16 +185,6 @@ protected Scope linkScopeLazy(StaticAnalysis analysis, Scope parentScope, return parentScope; } - /** - * By default, we return false, because most functions do not need this - * - * @return - */ - @Override - public boolean useSpecialExec() { - return false; - } - /** * Most functions should show up in the normal documentation. However, if this function shouldn't show up in the * documentation, it should mark itself with the @hide annotation. diff --git a/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java b/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java index 2627d6f368..ffe0972899 100644 --- a/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java @@ -11,10 +11,14 @@ import com.laytonsmith.annotations.core; import com.laytonsmith.annotations.seealso; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; +import com.laytonsmith.core.StepAction; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.compiler.analysis.StaticAnalysis; import com.laytonsmith.core.compiler.signature.FunctionSignatures; @@ -49,7 +53,6 @@ import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.functions.BasicLogic.equals; import com.laytonsmith.core.functions.BasicLogic.equals_ic; import com.laytonsmith.core.functions.BasicLogic.sequals; @@ -387,7 +390,7 @@ public Set optimizationOptions() { @api @seealso({array_get.class, array.class, array_push.class, com.laytonsmith.tools.docgen.templates.Arrays.class}) @OperatorPreferred("@array[@key] = @value") - public static class array_set extends AbstractFunction { + public static class array_set extends AbstractFunction implements FlowFunction { public static final String NAME = "array_set"; @@ -401,32 +404,68 @@ public Integer[] numArgs() { return new Integer[]{3}; } - @Override - public boolean useSpecialExec() { - return true; + static class ArraySetState { + enum Phase { EVAL_ARRAY, EVAL_INDEX, EVAL_VALUE } + Phase phase = Phase.EVAL_ARRAY; + ParseTree[] children; + Mixed array; + Mixed index; + + ArraySetState(ParseTree[] children) { + this.children = children; + } + + @Override + public String toString() { + return phase.name(); + } } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { + public StepResult begin(Target t, ParseTree[] children, Environment env) { env.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_ARRAY_SPECIAL_GET, true); - Mixed array; - try { - array = parent.seval(nodes[0], env); - } finally { - env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_ARRAY_SPECIAL_GET); - } - Mixed index = parent.seval(nodes[1], env); - Mixed value = parent.seval(nodes[2], env); - if(!(array.isInstanceOf(ArrayAccessSet.TYPE, null, env))) { - throw new CRECastException("Argument 1 of " + this.getName() + " must be an array, or implement ArrayAccessSet.", t); + ArraySetState state = new ArraySetState(children); + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, ArraySetState state, + Mixed result, Environment env) { + switch(state.phase) { + case EVAL_ARRAY: + env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_ARRAY_SPECIAL_GET); + state.array = result; + state.phase = ArraySetState.Phase.EVAL_INDEX; + return new StepResult<>(new Evaluate(state.children[1]), state); + case EVAL_INDEX: + state.index = result; + state.phase = ArraySetState.Phase.EVAL_VALUE; + return new StepResult<>(new Evaluate(state.children[2]), state); + case EVAL_VALUE: + if(!(state.array.isInstanceOf(ArrayAccessSet.TYPE, null, env))) { + throw new CRECastException("Argument 1 of " + getName() + + " must be an array, or implement ArrayAccessSet.", t); + } + try { + ((ArrayAccessSet) state.array).set(state.index, result, t, env); + } catch(IndexOutOfBoundsException e) { + throw new CREIndexOverflowException("The index " + + new CString(state.index).getQuote() + " is out of bounds", t); + } + return new StepResult<>(new Complete(result), state); + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid array_set state: " + state.phase, t); } + } - try { - ((ArrayAccessSet) array).set(index, value, t, env); - } catch(IndexOutOfBoundsException e) { - throw new CREIndexOverflowException("The index " + new CString(index).getQuote() + " is out of bounds", t); + @Override + public StepResult childInterrupted(Target t, ArraySetState state, + StepAction.FlowControl action, Environment env) { + if(state.phase == ArraySetState.Phase.EVAL_ARRAY) { + env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_ARRAY_SPECIAL_GET); } - return value; + return null; } @Override @@ -2809,7 +2848,7 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. for(Mixed key : aa.keySet(env)) { try { closure.executeCallable(env, t, key, aa.get(key, t, env)); - } catch(ProgramFlowManipulationException ex) { + } catch(CancelCommandException ex) { // Ignored } } diff --git a/src/main/java/com/laytonsmith/core/functions/BasicLogic.java b/src/main/java/com/laytonsmith/core/functions/BasicLogic.java index 2355770cd4..afc92d8264 100644 --- a/src/main/java/com/laytonsmith/core/functions/BasicLogic.java +++ b/src/main/java/com/laytonsmith/core/functions/BasicLogic.java @@ -6,10 +6,13 @@ import com.laytonsmith.annotations.core; import com.laytonsmith.annotations.seealso; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.compiler.OptimizationUtilities; import com.laytonsmith.core.compiler.analysis.Scope; @@ -56,6 +59,75 @@ public static String docs() { return "These functions provide basic logical operations."; } + /** + * Shared state for short-circuit logic FlowFunctions (and, or, dand, dor, nand, nor). + */ + static class ShortCircuitState { + ParseTree[] children; + int index; + + ShortCircuitState(ParseTree[] children) { + this.children = children; + this.index = 0; + } + + @Override + public String toString() { + return "index=" + index + "/" + children.length; + } + } + + enum ShortCircuitMode { + AND, // short-circuit on false, return CBoolean + OR, // short-circuit on true, return CBoolean + DAND, // short-circuit on falsy, return actual value + DOR // short-circuit on truthy, return actual value + } + + private static StepResult scBegin(ParseTree[] children) { + ShortCircuitState state = new ShortCircuitState(children); + return new StepResult<>(new Evaluate(children[0]), state); + } + + private static StepResult scChildCompleted(Target t, + ShortCircuitState state, Mixed result, Environment env, ShortCircuitMode mode) { + boolean boolVal; + switch(mode) { + case AND -> { + boolVal = ArgumentValidation.getBoolean(result, t, env); + if(!boolVal) { + return new StepResult<>(new Complete(CBoolean.FALSE), state); + } + } + case OR -> { + boolVal = ArgumentValidation.getBoolean(result, t, env); + if(boolVal) { + return new StepResult<>(new Complete(CBoolean.TRUE), state); + } + } + case DAND -> { + if(!ArgumentValidation.getBooleanish(result, t, env)) { + return new StepResult<>(new Complete(result), state); + } + } + case DOR -> { + if(ArgumentValidation.getBooleanish(result, t, env)) { + return new StepResult<>(new Complete(result), state); + } + } + } + state.index++; + if(state.index < state.children.length) { + return new StepResult<>(new Evaluate(state.children[state.index]), state); + } + // All evaluated, none short-circuited + return switch(mode) { + case AND -> new StepResult<>(new Complete(CBoolean.TRUE), state); + case OR -> new StepResult<>(new Complete(CBoolean.FALSE), state); + case DAND, DOR -> new StepResult<>(new Complete(result), state); + }; + } + @api @seealso({nequals.class, sequals.class, snequals.class}) @OperatorPreferred("==") @@ -1077,7 +1149,7 @@ public ExampleScript[] examples() throws ConfigCompileException { @api(environments = {GlobalEnv.class}) @seealso({or.class}) @OperatorPreferred("&&") - public static class and extends AbstractFunction implements Optimizable { + public static class and extends AbstractFunction implements Optimizable, FlowFunction { public static final String NAME = "and"; @@ -1104,15 +1176,14 @@ public CBoolean exec(Target t, Environment env, GenericParameters generics, Mixe } @Override - public CBoolean execs(Target t, Environment env, Script parent, ParseTree... nodes) { - for(ParseTree tree : nodes) { - Mixed c = env.getEnv(GlobalEnv.class).GetScript().seval(tree, env); - boolean b = ArgumentValidation.getBoolean(c, t, env); - if(b == false) { - return CBoolean.FALSE; - } - } - return CBoolean.TRUE; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return scBegin(children); + } + + @Override + public StepResult childCompleted(Target t, ShortCircuitState state, + Mixed result, Environment env) { + return scChildCompleted(t, state, result, env, ShortCircuitMode.AND); } @Override @@ -1155,11 +1226,6 @@ public Boolean runAsync() { return null; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public ParseTree optimizeDynamic(Target t, Environment env, Set> envs, @@ -1230,7 +1296,7 @@ public Set optimizationOptions() { } @api - public static class dand extends AbstractFunction implements Optimizable { + public static class dand extends AbstractFunction implements Optimizable, FlowFunction { @Override public Class[] thrown() { @@ -1248,25 +1314,19 @@ public Boolean runAsync() { } @Override - public boolean useSpecialExec() { - return true; + public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + return CVoid.VOID; } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { - return CVoid.VOID; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return scBegin(children); } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - Mixed lastValue = CBoolean.TRUE; - for(ParseTree tree : nodes) { - lastValue = env.getEnv(GlobalEnv.class).GetScript().seval(tree, env); - if(!ArgumentValidation.getBooleanish(lastValue, t, env)) { - return lastValue; - } - } - return lastValue; + public StepResult childCompleted(Target t, ShortCircuitState state, + Mixed result, Environment env) { + return scChildCompleted(t, state, result, env, ShortCircuitMode.DAND); } @Override @@ -1372,7 +1432,7 @@ public Set optimizationOptions() { @api(environments = {GlobalEnv.class}) @seealso({and.class}) @OperatorPreferred("||") - public static class or extends AbstractFunction implements Optimizable { + public static class or extends AbstractFunction implements Optimizable, FlowFunction { @Override public String getName() { @@ -1397,14 +1457,14 @@ public CBoolean exec(Target t, Environment env, GenericParameters generics, Mixe } @Override - public CBoolean execs(Target t, Environment env, Script parent, ParseTree... nodes) { - for(ParseTree tree : nodes) { - Mixed c = env.getEnv(GlobalEnv.class).GetScript().seval(tree, env); - if(ArgumentValidation.getBoolean(c, t, env)) { - return CBoolean.TRUE; - } - } - return CBoolean.FALSE; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return scBegin(children); + } + + @Override + public StepResult childCompleted(Target t, ShortCircuitState state, + Mixed result, Environment env) { + return scChildCompleted(t, state, result, env, ShortCircuitMode.OR); } @Override @@ -1448,11 +1508,6 @@ public Boolean runAsync() { return null; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public ParseTree optimizeDynamic(Target t, Environment env, Set> envs, @@ -1526,7 +1581,7 @@ public Set optimizationOptions() { } @api - public static class dor extends AbstractFunction implements Optimizable { + public static class dor extends AbstractFunction implements Optimizable, FlowFunction { @Override public Class[] thrown() { @@ -1544,24 +1599,19 @@ public Boolean runAsync() { } @Override - public boolean useSpecialExec() { - return true; + public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + return CVoid.VOID; } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { - return CVoid.VOID; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return scBegin(children); } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - for(ParseTree tree : nodes) { - Mixed c = env.getEnv(GlobalEnv.class).GetScript().seval(tree, env); - if(ArgumentValidation.getBooleanish(c, t, env)) { - return c; - } - } - return env.getEnv(GlobalEnv.class).GetScript().seval(nodes[nodes.length - 1], env); + public StepResult childCompleted(Target t, ShortCircuitState state, + Mixed result, Environment env) { + return scChildCompleted(t, state, result, env, ShortCircuitMode.DOR); } @Override @@ -1587,6 +1637,9 @@ public ParseTree optimizeDynamic(Target t, Environment env, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { OptimizationUtilities.pullUpLikeFunctions(children, getName()); + if(children.isEmpty()) { + throw new ConfigCompileException(getName() + " requires at least one argument", t); + } return null; } @@ -1785,7 +1838,7 @@ public ExampleScript[] examples() throws ConfigCompileException { @api @seealso({and.class}) - public static class nand extends AbstractFunction { + public static class nand extends AbstractFunction implements FlowFunction { @Override public String getName() { @@ -1828,8 +1881,18 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. } @Override - public CBoolean execs(Target t, Environment env, Script parent, ParseTree... nodes) { - return new and().execs(t, env, parent, nodes).not(); + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return scBegin(children); + } + + @Override + public StepResult childCompleted(Target t, ShortCircuitState state, + Mixed result, Environment env) { + StepResult r = scChildCompleted(t, state, result, env, ShortCircuitMode.AND); + if(r.getAction() instanceof Complete c) { + return new StepResult<>(new Complete(((CBoolean) c.getResult()).not()), state); + } + return r; } @Override @@ -1846,11 +1909,6 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, return this.linkScopeLazy(analysis, parentScope, ast, env, exceptions); } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ @@ -1860,7 +1918,7 @@ public ExampleScript[] examples() throws ConfigCompileException { @api @seealso({or.class}) - public static class nor extends AbstractFunction { + public static class nor extends AbstractFunction implements FlowFunction { @Override public String getName() { @@ -1903,8 +1961,18 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. } @Override - public CBoolean execs(Target t, Environment env, Script parent, ParseTree... args) throws ConfigRuntimeException { - return new or().execs(t, env, parent, args).not(); + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return scBegin(children); + } + + @Override + public StepResult childCompleted(Target t, ShortCircuitState state, + Mixed result, Environment env) { + StepResult r = scChildCompleted(t, state, result, env, ShortCircuitMode.OR); + if(r.getAction() instanceof Complete c) { + return new StepResult<>(new Complete(((CBoolean) c.getResult()).not()), state); + } + return r; } @Override @@ -1921,11 +1989,6 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, return this.linkScopeLazy(analysis, parentScope, ast, env, exceptions); } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ diff --git a/src/main/java/com/laytonsmith/core/functions/Compiler.java b/src/main/java/com/laytonsmith/core/functions/Compiler.java index 591ef8d8a4..da57cdbaac 100644 --- a/src/main/java/com/laytonsmith/core/functions/Compiler.java +++ b/src/main/java/com/laytonsmith/core/functions/Compiler.java @@ -8,11 +8,14 @@ import com.laytonsmith.annotations.noprofile; import com.laytonsmith.core.ArgumentValidation; import com.laytonsmith.core.FullyQualifiedClassName; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; import com.laytonsmith.core.Optimizable.OptimizationOption; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.CompilerEnvironment; import com.laytonsmith.core.compiler.CompilerWarning; import com.laytonsmith.core.compiler.FileOptions; @@ -77,7 +80,7 @@ public static String docs() { @api @noprofile @hide("This is only used internally by the compiler.") - public static class p extends DummyFunction implements Optimizable { + public static class p extends DummyFunction implements FlowFunction, Optimizable { public static final String NAME = "p"; @@ -92,13 +95,16 @@ public String docs() { } @Override - public boolean useSpecialExec() { - return true; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(children.length == 1) { + return new StepResult<>(new Evaluate(children[0]), null); + } + return new StepResult<>(new Complete(CVoid.VOID), null); } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - return (nodes.length == 1 ? parent.eval(nodes[0], env) : CVoid.VOID); + public StepResult childCompleted(Target t, Void state, Mixed result, Environment env) { + return new StepResult<>(new Complete(result), null); } @Override @@ -571,9 +577,9 @@ public static ParseTree rewrite(List list, boolean returnSConcat, child.setChildren(list); } try { - Function f = (Function) FunctionList.getFunction(identifier, envs); - ParseTree node = new ParseTree( - f.execs(identifier.getTarget(), null, null, child), child.getFileOptions()); + FunctionList.getFunction(identifier, envs); + ParseTree node = new ParseTree(identifier, child.getFileOptions()); + node.addChild(child); if(node.getData() instanceof CFunction && node.getData().val().equals(__autoconcat__.NAME)) { node = rewrite(node.getChildren(), returnSConcat, envs); diff --git a/src/main/java/com/laytonsmith/core/functions/CompositeFunction.java b/src/main/java/com/laytonsmith/core/functions/CompositeFunction.java index 6461fb1999..cb129a3fa4 100644 --- a/src/main/java/com/laytonsmith/core/functions/CompositeFunction.java +++ b/src/main/java/com/laytonsmith/core/functions/CompositeFunction.java @@ -22,7 +22,6 @@ 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.natives.interfaces.Mixed; import java.io.File; @@ -82,13 +81,11 @@ public final Mixed exec(Target t, Environment env, GenericParameters generics, M Mixed ret = CVoid.VOID; try { if(gEnv.GetScript() != null) { - gEnv.GetScript().eval(tree, env); + ret = gEnv.GetScript().eval(tree, env); } else { // This can happen when the environment is not fully setup during tests, in addition to optimization - Script.GenerateScript(null, null, null).eval(tree, env); + ret = Script.GenerateScript(null, null, null).eval(tree, env); } - } catch (FunctionReturnException ex) { - ret = ex.getReturn(); } catch (ConfigRuntimeException ex) { if(Prefs.DebugMode()) { MSLog.GetLogger().e(MSLog.Tags.GENERAL, "Possibly false stacktrace, could be internal error", @@ -139,15 +136,4 @@ protected boolean cacheCompile() { return true; } - @Override - public final Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - throw new Error(this.getClass().toString()); - } - - @Override - public final boolean useSpecialExec() { - // This defeats the purpose, so don't allow this. - return false; - } - } diff --git a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java index 5ba1eb83c9..101fe6b4c4 100644 --- a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java +++ b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java @@ -9,12 +9,17 @@ import com.laytonsmith.annotations.noboilerplate; import com.laytonsmith.annotations.seealso; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; import com.laytonsmith.core.Procedure; -import com.laytonsmith.core.Script; +import com.laytonsmith.core.StepAction; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.FlowControl; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.Static; import com.laytonsmith.core.compiler.BranchStatement; import com.laytonsmith.core.compiler.CompilerEnvironment; @@ -73,9 +78,7 @@ import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigCompileException; 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.Booleanish; import com.laytonsmith.core.natives.interfaces.Iterator; import com.laytonsmith.core.natives.interfaces.Mixed; @@ -97,9 +100,115 @@ public static String docs() { return "This class provides various functions to manage control flow."; } + // --- FlowControlAction types --- + // These are the first-class representations of control flow in the iterative interpreter. + // They replace the old ProgramFlowManipulationException hierarchy. + + /** + * Produced by {@code break()}. Propagates up to the nearest loop flow function. + */ + public static class BreakAction implements StepAction.FlowControlAction { + private final int levels; + private final Target target; + + public BreakAction(int levels, Target target) { + this.levels = levels; + this.target = target; + } + + public int getLevels() { + return levels; + } + + @Override + public Target getTarget() { + return target; + } + } + + /** + * Produced by {@code continue()}. Propagates up to the nearest loop flow function. + */ + public static class ContinueAction implements StepAction.FlowControlAction { + private final int levels; + private final Target target; + + public ContinueAction(int levels, Target target) { + this.levels = levels; + this.target = target; + } + + public int getLevels() { + return levels; + } + + @Override + public Target getTarget() { + return target; + } + } + + /** + * Produced by {@code return()}. Propagates up to the nearest procedure/closure boundary. + */ + public static class ReturnAction implements StepAction.FlowControlAction { + private final Mixed value; + private final Target target; + + public ReturnAction(Mixed value, Target target) { + this.value = value; + this.target = target; + } + + public Mixed getValue() { + return value; + } + + @Override + public Target getTarget() { + return target; + } + } + @api @ConditionalSelfStatement - public static class _if extends AbstractFunction implements Optimizable, BranchStatement, VariableScope { + public static class _if extends AbstractFunction implements FlowFunction<_if.IfState>, Optimizable, BranchStatement, VariableScope { + + static class IfState { + ParseTree[] children; + boolean conditionEvaluated; + + IfState(ParseTree[] children) { + this.children = children; + } + + @Override + public String toString() { + return conditionEvaluated ? "branch" : "condition"; + } + } + + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + IfState state = new IfState(children); + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, IfState state, + Mixed result, Environment env) { + if(!state.conditionEvaluated) { + state.conditionEvaluated = true; + if(ArgumentValidation.getBooleanish(result, t, env)) { + return new StepResult<>(new Evaluate(state.children[1]), state); + } else if(state.children.length == 3) { + return new StepResult<>(new Evaluate(state.children[2]), state); + } else { + return new StepResult<>(new Complete(CVoid.VOID), state); + } + } + return new StepResult<>(new Complete(result), state); + } public static final String NAME = "if"; @@ -113,20 +222,6 @@ public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - ParseTree condition = nodes[0]; - if(ArgumentValidation.getBooleanish(parent.seval(condition, env), t, env)) { - ParseTree ifCode = nodes[1]; - return parent.seval(ifCode, env); - } else if(nodes.length == 3) { - ParseTree elseCode = nodes[2]; - return parent.seval(elseCode, env); - } else { - return CVoid.VOID; - } - } - @Override public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws CancelCommandException, ConfigRuntimeException { @@ -242,11 +337,6 @@ public Boolean runAsync() { return false; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public Set optimizationOptions() { return EnumSet.of( @@ -361,7 +451,55 @@ public boolean isSelfStatement(Target t, Environment env, List nodes, @api(environments = {GlobalEnv.class}) @ConditionalSelfStatement - public static class ifelse extends AbstractFunction implements Optimizable, BranchStatement, VariableScope { + public static class ifelse extends AbstractFunction implements FlowFunction, Optimizable, BranchStatement, VariableScope { + + static class IfElseState { + ParseTree[] children; + int condIndex; // index of current condition being tested (even indices) + boolean evaluatingBranch; + + IfElseState(ParseTree[] children) { + this.children = children; + } + + @Override + public String toString() { + return evaluatingBranch ? "branch" : "cond " + condIndex; + } + } + + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(children.length < 2) { + throw new CREInsufficientArgumentsException("ifelse expects at least 2 arguments", t); + } + IfElseState state = new IfElseState(children); + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, IfElseState state, + Mixed result, Environment env) { + if(state.evaluatingBranch) { + return new StepResult<>(new Complete(result), state); + } + // We just evaluated a condition + if(ArgumentValidation.getBooleanish(result, t, env)) { + state.evaluatingBranch = true; + return new StepResult<>(new Evaluate(state.children[state.condIndex + 1]), state); + } + // Condition was false, advance to next pair + state.condIndex += 2; + if(state.condIndex <= state.children.length - 2) { + return new StepResult<>(new Evaluate(state.children[state.condIndex]), state); + } + // No more condition pairs — check for else block (odd number of children) + if(state.children.length % 2 == 1) { + state.evaluatingBranch = true; + return new StepResult<>(new Evaluate(state.children[state.children.length - 1]), state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + } public static final String NAME = "ifelse"; @@ -412,24 +550,6 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. return CNull.NULL; } - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - if(nodes.length < 2) { - throw new CREInsufficientArgumentsException("ifelse expects at least 2 arguments", t); - } - for(int i = 0; i <= nodes.length - 2; i += 2) { - ParseTree condition = nodes[i]; - if(ArgumentValidation.getBooleanish(parent.seval(condition, env), t, env)) { - ParseTree ifCode = nodes[i + 1]; - return env.getEnv(GlobalEnv.class).GetScript().seval(ifCode, env); - } - } - if(nodes.length % 2 == 1) { - return env.getEnv(GlobalEnv.class).GetScript().seval(nodes[nodes.length - 1], env); - } - return CVoid.VOID; - } - @Override public FunctionSignatures getSignatures() { /* @@ -439,11 +559,6 @@ public FunctionSignatures getSignatures() { return super.getSignatures(); } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { @@ -585,7 +700,125 @@ public boolean isSelfStatement(Target t, Environment env, List nodes, @api @breakable @ConditionalSelfStatement - public static class _switch extends AbstractFunction implements Optimizable, BranchStatement, VariableScope { + public static class _switch extends AbstractFunction implements FlowFunction<_switch.SwitchState>, Optimizable, BranchStatement, VariableScope { + + static class SwitchState { + enum Phase { EVAL_VALUE, EVAL_CASE, EVAL_CODE } + Phase phase = Phase.EVAL_VALUE; + ParseTree[] children; + Mixed switchValue; + int caseIndex = 1; // starts at 1, skips switch value + + SwitchState(ParseTree[] children) { + this.children = children; + } + + @Override + public String toString() { + return phase + " idx=" + caseIndex; + } + } + + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + SwitchState state = new SwitchState(children); + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, SwitchState state, + Mixed result, Environment env) { + switch(state.phase) { + case EVAL_VALUE: + state.switchValue = result; + state.phase = SwitchState.Phase.EVAL_CASE; + if(state.caseIndex <= state.children.length - 2) { + return new StepResult<>(new Evaluate(state.children[state.caseIndex]), state); + } + // No cases, check for default + if(state.children.length % 2 == 0) { + state.phase = SwitchState.Phase.EVAL_CODE; + return new StepResult<>(new Evaluate( + state.children[state.children.length - 1]), state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + + case EVAL_CASE: + BasicLogic.equals equals = new BasicLogic.equals(); + boolean matched = false; + if(result instanceof CSlice) { + long rangeLeft = ((CSlice) result).getStart(); + long rangeRight = ((CSlice) result).getFinish(); + if(state.switchValue.isInstanceOf(CInt.TYPE, null, env)) { + long v = ArgumentValidation.getInt(state.switchValue, t); + matched = (rangeLeft < rangeRight && v >= rangeLeft && v <= rangeRight) + || (rangeLeft > rangeRight && v >= rangeRight && v <= rangeLeft) + || (rangeLeft == rangeRight && v == rangeLeft); + } + } else if(result.isInstanceOf(CArray.TYPE, null, env)) { + for(String index : ((CArray) result).stringKeySet()) { + Mixed inner = ((CArray) result).get(index, t, env); + if(inner instanceof CSlice) { + long rangeLeft = ((CSlice) inner).getStart(); + long rangeRight = ((CSlice) inner).getFinish(); + if(state.switchValue.isInstanceOf(CInt.TYPE, null, env)) { + long v = ArgumentValidation.getInt(state.switchValue, t); + if((rangeLeft < rangeRight && v >= rangeLeft && v <= rangeRight) + || (rangeLeft > rangeRight && v >= rangeRight && v <= rangeLeft) + || (rangeLeft == rangeRight && v == rangeLeft)) { + matched = true; + break; + } + } + } else if(equals.exec(t, env, null, state.switchValue, inner).getBoolean()) { + matched = true; + break; + } + } + } else if(equals.exec(t, env, null, state.switchValue, result).getBoolean()) { + matched = true; + } + if(matched) { + state.phase = SwitchState.Phase.EVAL_CODE; + return new StepResult<>(new Evaluate( + state.children[state.caseIndex + 1]), state); + } + // No match, advance to next case pair + state.caseIndex += 2; + if(state.caseIndex <= state.children.length - 2) { + return new StepResult<>(new Evaluate(state.children[state.caseIndex]), state); + } + // No more cases, check for default + if(state.children.length % 2 == 0) { + state.phase = SwitchState.Phase.EVAL_CODE; + return new StepResult<>(new Evaluate( + state.children[state.children.length - 1]), state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + + case EVAL_CODE: + return new StepResult<>(new Complete(result), state); + + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid switch state: " + state.phase, t); + } + } + + @Override + public StepResult childInterrupted(Target t, SwitchState state, + StepAction.FlowControl action, Environment env) { + if(state.phase == SwitchState.Phase.EVAL_CODE + && action.getAction() instanceof BreakAction breakAction) { + int levels = breakAction.getLevels(); + if(levels <= 1) { + return new StepResult<>(new Complete(CVoid.VOID), state); + } + return new StepResult<>(new FlowControl( + new BreakAction(levels - 1, breakAction.getTarget())), state); + } + return null; // propagate + } @Override public String getName() { @@ -662,62 +895,6 @@ public boolean isSelfStatement(Target t, Environment env, List nodes, return false; } - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - Mixed value = parent.seval(nodes[0], env); - BasicLogic.equals equals = new BasicLogic.equals(); - try { - for(int i = 1; i <= nodes.length - 2; i += 2) { - ParseTree statement = nodes[i]; - ParseTree code = nodes[i + 1]; - Mixed evalStatement = parent.seval(statement, env); - if(evalStatement instanceof CSlice) { //Can do more optimal handling for this Array subclass - long rangeLeft = ((CSlice) evalStatement).getStart(); - long rangeRight = ((CSlice) evalStatement).getFinish(); - if(value.isInstanceOf(CInt.TYPE, null, env)) { - long v = ArgumentValidation.getInt(value, t); - if((rangeLeft < rangeRight && v >= rangeLeft && v <= rangeRight) - || (rangeLeft > rangeRight && v >= rangeRight && v <= rangeLeft) - || (rangeLeft == rangeRight && v == rangeLeft)) { - return parent.seval(code, env); - } - } - } else if(evalStatement.isInstanceOf(CArray.TYPE, null, env)) { - for(String index : ((CArray) evalStatement).stringKeySet()) { - Mixed inner = ((CArray) evalStatement).get(index, t, env); - if(inner instanceof CSlice) { - long rangeLeft = ((CSlice) inner).getStart(); - long rangeRight = ((CSlice) inner).getFinish(); - if(value.isInstanceOf(CInt.TYPE, null, env)) { - long v = ArgumentValidation.getInt(value, t); - if((rangeLeft < rangeRight && v >= rangeLeft && v <= rangeRight) - || (rangeLeft > rangeRight && v >= rangeRight && v <= rangeLeft) - || (rangeLeft == rangeRight && v == rangeLeft)) { - return parent.seval(code, env); - } - } - } else if(equals.exec(t, env, null, value, inner).getBoolean()) { - return parent.seval(code, env); - } - } - } else if(equals.exec(t, env, null, value, evalStatement).getBoolean()) { - return parent.seval(code, env); - } - } - if(nodes.length % 2 == 0) { - return parent.seval(nodes[nodes.length - 1], env); - } - } catch (LoopBreakException ex) { - //Ignored, unless the value passed in is greater than 1, in which case - //we rethrow. - if(ex.getTimes() > 1) { - ex.setTimes(ex.getTimes() - 1); - throw ex; - } - } - return CVoid.VOID; - } - @Override public FunctionSignatures getSignatures() { /* @@ -767,11 +944,6 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, return caseParentScope; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ @@ -1081,31 +1253,40 @@ public ParseTree optimizeDynamic(Target t, Environment env, @seealso({com.laytonsmith.tools.docgen.templates.Loops.class, com.laytonsmith.tools.docgen.templates.ArrayIteration.class}) @SelfStatement - public static class _for extends AbstractFunction implements Optimizable, BranchStatement, VariableScope { + public static class _for extends AbstractFunction implements FlowFunction, Optimizable, BranchStatement, VariableScope { + + private static final forelse FOR_DELEGATE = new forelse(); @Override - public String getName() { - return "for"; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return FOR_DELEGATE.begin(t, children, env); } @Override - public Integer[] numArgs() { - return new Integer[]{4}; + public StepResult childCompleted(Target t, forelse.ForState state, + Mixed result, Environment env) { + return FOR_DELEGATE.childCompleted(t, state, result, env); } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) { - return CVoid.VOID; + public StepResult childInterrupted(Target t, forelse.ForState state, + StepAction.FlowControl action, Environment env) { + return FOR_DELEGATE.childInterrupted(t, state, action, env); } @Override - public boolean useSpecialExec() { - return true; + public String getName() { + return "for"; } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - return new forelse(true).execs(t, env, parent, nodes); + public Integer[] numArgs() { + return new Integer[]{4}; + } + + @Override + public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) { + return CVoid.VOID; } @Override @@ -1232,7 +1413,13 @@ public ParseTree optimizeDynamic(Target t, Environment env, //existing system sort that out. } - return null; + // Rewrite for(a, b, c, d) as forelse(a, b, c, d, null) + ParseTree rewrite = new ParseTree(new CFunction(forelse.NAME, t), fileOptions); + for(ParseTree child : children) { + rewrite.addChild(child); + } + rewrite.addChild(new ParseTree(CNull.NULL, fileOptions)); + return rewrite; } @Override @@ -1276,17 +1463,97 @@ public List isScope(List children) { @noboilerplate @breakable @SelfStatement - public static class forelse extends AbstractFunction implements BranchStatement, VariableScope { + public static class forelse extends AbstractFunction implements FlowFunction, BranchStatement, VariableScope { public static final String NAME = "forelse"; - public forelse() { + enum Phase { ASSIGN, CONDITION, BODY, INCREMENT, ELSE } + + static class ForState { + Phase phase; + ParseTree[] children; + boolean hasRunOnce; + int skipCount; + + ForState(ParseTree[] children) { + this.phase = Phase.ASSIGN; + this.children = children; + this.hasRunOnce = false; + } + + @Override + public String toString() { + return phase.name() + (hasRunOnce ? " (looped)" : "") + + (skipCount > 0 ? " skip=" + skipCount : ""); + } } - boolean runAsFor = false; + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + ForState state = new ForState(children); + return new StepResult<>(new Evaluate(children[0], null, true), state); + } - forelse(boolean runAsFor) { - this.runAsFor = runAsFor; + @Override + public StepResult childCompleted(Target t, ForState state, Mixed result, Environment env) { + switch(state.phase) { + case ASSIGN: + if(!(result instanceof IVariable)) { + throw new CRECastException("First parameter of for must be an ivariable", t); + } + state.phase = Phase.CONDITION; + return new StepResult<>(new Evaluate(state.children[1]), state); + case CONDITION: + boolean cond = ArgumentValidation.getBooleanish(result, t, env); + if(!cond) { + if(!state.hasRunOnce && !(state.children[4].getData() instanceof CNull)) { + state.phase = Phase.ELSE; + return new StepResult<>(new Evaluate(state.children[4]), state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + } + state.hasRunOnce = true; + if(state.skipCount > 1) { + state.skipCount--; + state.phase = Phase.INCREMENT; + return new StepResult<>(new Evaluate(state.children[2]), state); + } + state.skipCount = 0; + state.phase = Phase.BODY; + return new StepResult<>(new Evaluate(state.children[3]), state); + case BODY: + state.phase = Phase.INCREMENT; + return new StepResult<>(new Evaluate(state.children[2]), state); + case INCREMENT: + state.phase = Phase.CONDITION; + return new StepResult<>(new Evaluate(state.children[1]), state); + case ELSE: + return new StepResult<>(new Complete(CVoid.VOID), state); + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid for loop state: " + state.phase, t); + } + } + + @Override + public StepResult childInterrupted(Target t, ForState state, + StepAction.FlowControl action, Environment env) { + if(state.phase == Phase.BODY) { + if(action.getAction() instanceof BreakAction breakAction) { + int levels = breakAction.getLevels(); + if(levels <= 1) { + return new StepResult<>(new Complete(CVoid.VOID), state); + } + return new StepResult<>(new FlowControl( + new BreakAction(levels - 1, breakAction.getTarget())), state); + } + if(action.getAction() instanceof ContinueAction continueAction) { + state.skipCount = continueAction.getLevels(); + state.phase = Phase.INCREMENT; + return new StepResult<>(new Evaluate(state.children[2]), state); + } + } + return null; } @Override @@ -1309,66 +1576,11 @@ public Boolean runAsync() { return null; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { return null; } - @Override - public Mixed 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; - - Mixed 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 = ArgumentValidation.getBoolean(parent.seval(condition, env), t, 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 FunctionSignatures getSignatures() { return new SignatureBuilder(CVoid.TYPE) @@ -1389,7 +1601,7 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { // Handle not enough arguments. Link child scopes, but return parent scope. - if(ast.numberOfChildren() < (this.runAsFor ? 3 : 4)) { + if(ast.numberOfChildren() < 4) { super.linkScope(analysis, parentScope, ast, env, exceptions); return parentScope; } @@ -1399,7 +1611,7 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree cond = ast.getChildAt(1); ParseTree exp = ast.getChildAt(2); ParseTree code = ast.getChildAt(3); - ParseTree elseCode = (this.runAsFor ? null : ast.getChildAt(4)); + ParseTree elseCode = ast.numberOfChildren() > 4 ? ast.getChildAt(4) : null; // Order: assign -> cond -> (code -> exp -> cond)* -> elseCode?. Scope assignScope = analysis.linkScope(parentScope, assign, env, exceptions); @@ -1457,175 +1669,270 @@ public List isScope(List children) { @breakable @seealso({com.laytonsmith.tools.docgen.templates.Loops.class, ArrayIteration.class}) @SelfStatement - public static class foreach extends AbstractFunction implements BranchStatement, VariableScope { + public static class foreach extends AbstractFunction implements FlowFunction, BranchStatement, VariableScope { - @Override - public String getName() { - return "foreach"; - } + static class ForeachState { + enum Phase { EVAL_ARRAY, EVAL_KEY, EVAL_VALUE, LOOP_BODY, ELSE_BODY } + Phase phase = Phase.EVAL_ARRAY; + ParseTree[] children; + int offset; // 1 if key parameter present, 0 otherwise + boolean hasElse; - @Override - public Integer[] numArgs() { - return new Integer[]{2, 3, 4}; + com.laytonsmith.core.natives.interfaces.Iterable arr; + IVariable keyVar; + IVariable valueVar; + ParseTree codeNode; + ParseTree elseNode; + + // Associative iteration + boolean isAssociative; + java.util.Iterator assocKeyIterator; + + // Non-associative iteration + Iterator nonAssocIterator; + List arrayAccessList; + int skipCount; + + ForeachState(ParseTree[] children, int offset, boolean hasElse) { + this.children = children; + this.offset = offset; + this.hasElse = hasElse; + } + + @Override + public String toString() { + return phase.name().toLowerCase(); + } } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) - throws CancelCommandException, ConfigRuntimeException { - return CVoid.VOID; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(children.length < 3) { + throw new CREInsufficientArgumentsException( + "Insufficient arguments passed to " + getName(), t); + } + int offset = (children.length == 4) ? 1 : 0; + ForeachState state = new ForeachState(children, offset, false); + return new StepResult<>(new Evaluate(children[0]), state); } @Override - public Mixed 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]; - Mixed arr = parent.seval(array, env); - Mixed 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); + public StepResult childCompleted(Target t, ForeachState state, + Mixed result, Environment env) { + switch(state.phase) { + case EVAL_ARRAY: { + Mixed arr = result; + if(arr instanceof CSlice) { + long start = ((CSlice) arr).getStart(); + long finish = ((CSlice) arr).getFinish(); + if(finish < start) { + arr = new ArrayHandling.range().exec(t, env, null, + new CInt(start, t), new CInt(finish - 1, t), new CInt(-1, t)); + } else { + arr = new ArrayHandling.range().exec(t, env, null, + new CInt(start, t), new CInt(finish + 1, t)); + } + } + if(!(arr instanceof com.laytonsmith.core.natives.interfaces.Iterable)) { + throw new CRECastException("Parameter 1 of " + getName() + + " must be an Iterable data structure", t); + } + state.arr = (com.laytonsmith.core.natives.interfaces.Iterable) arr; + state.codeNode = state.children[2 + state.offset]; + if(state.hasElse) { + state.elseNode = state.children[state.children.length - 1]; + } + + // Check empty for foreachelse + if(state.hasElse && state.arr.size(env) == 0) { + state.phase = ForeachState.Phase.ELSE_BODY; + return new StepResult<>(new Evaluate(state.elseNode), state); + } + + if(state.offset == 1) { + state.phase = ForeachState.Phase.EVAL_KEY; + return new StepResult<>(new Evaluate(state.children[1], null, true), state); + } + state.phase = ForeachState.Phase.EVAL_VALUE; + return new StepResult<>(new Evaluate(state.children[1], null, true), state); } - } - Mixed 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, null, new CInt(start, t), new CInt(finish - 1, t), new CInt(-1, t)); - } else { - arr = new ArrayHandling.range().exec(t, env, null, new CInt(start, t), new CInt(finish + 1, t)); + case EVAL_KEY: { + if(!(result instanceof IVariable)) { + throw new CRECastException("Parameter 2 of " + getName() + + " must be an ivariable", t); + } + state.keyVar = (IVariable) result; + state.phase = ForeachState.Phase.EVAL_VALUE; + return new StepResult<>(new Evaluate( + state.children[1 + state.offset], null, true), state); } - } - if(!(arr instanceof com.laytonsmith.core.natives.interfaces.Iterable)) { - throw new CRECastException("Parameter 1 of " + getName() + " must be an Iterable data structure", t); - } - if(!(iv instanceof IVariable)) { - throw new CRECastException( - "Parameter " + (2 + offset) + " of " + getName() + " must be an ivariable", t); - } - com.laytonsmith.core.natives.interfaces.Iterable one - = (com.laytonsmith.core.natives.interfaces.Iterable) 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 keySet = new LinkedHashSet<>(one.keySet(env)); - //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(Mixed 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; + case EVAL_VALUE: { + if(!(result instanceof IVariable)) { + throw new CRECastException("Parameter " + (2 + state.offset) + + " of " + getName() + " must be an ivariable", t); } - //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, kkey.getDefinedTarget(), env)); + state.valueVar = (IVariable) result; + return startIteration(t, state, env); + } + case LOOP_BODY: { + if(state.isAssociative) { + return nextAssociativeIteration(t, state, env); + } else { + return advanceNonAssociative(t, state, env); } - //Set the value in the variable table - env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(two.getDefinedType(), - two.getVariableName(), one.get(c, t, env), two.getDefinedTarget(), env)); - 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; + } + case ELSE_BODY: + return new StepResult<>(new Complete(CVoid.VOID), state); + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid foreach state: " + state.phase, t); + } + } + + @Override + public StepResult childInterrupted(Target t, ForeachState state, + StepAction.FlowControl action, Environment env) { + if(state.phase == ForeachState.Phase.LOOP_BODY) { + if(action.getAction() instanceof BreakAction breakAction) { + cleanupIterator(state); + int levels = breakAction.getLevels(); + if(levels <= 1) { + return new StepResult<>(new Complete(CVoid.VOID), state); } + return new StepResult<>(new FlowControl( + new BreakAction(levels - 1, breakAction.getTarget())), state); } - 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. - Iterator iterator = new Iterator(one); - List 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(env)) { - //Done with the iterations. - break; + if(action.getAction() instanceof ContinueAction continueAction) { + int levels = continueAction.getLevels(); + if(state.isAssociative) { + // For associative arrays, skip means skip entries in the iterator + for(int i = 0; i < levels - 1 && state.assocKeyIterator.hasNext(); i++) { + state.assocKeyIterator.next(); } - //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), kkey.getDefinedTarget(), env)); - } - env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(two.getDefinedType(), - two.getVariableName(), one.get(current, t, env), two.getDefinedTarget(), env)); - 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(); + return nextAssociativeIteration(t, state, env); + } else { + // For non-associative, we need to skip entries + state.skipCount = levels; + return advanceNonAssociative(t, state, env); } - } finally { - arrayAccessList.remove(iterator); } + // Other interruptions (throw, return) — cleanup before propagating + cleanupIterator(state); } + return null; // propagate + } + + private StepResult startIteration(Target t, ForeachState state, Environment env) { + if(state.arr.isAssociative()) { + state.isAssociative = true; + // Clone the key set so modifications during iteration don't affect order + Set keySet = new LinkedHashSet<>(state.arr.keySet(env)); + state.assocKeyIterator = keySet.iterator(); + return nextAssociativeIteration(t, state, env); + } else { + state.isAssociative = false; + state.nonAssocIterator = new Iterator(state.arr); + state.arrayAccessList = env.getEnv(GlobalEnv.class).GetArrayAccessIterators(); + state.arrayAccessList.add(state.nonAssocIterator); + return advanceNonAssociative(t, state, env); + } + } + + private StepResult nextAssociativeIteration(Target t, + ForeachState state, Environment env) { + if(!state.assocKeyIterator.hasNext()) { + return new StepResult<>(new Complete(CVoid.VOID), state); + } + Mixed key = state.assocKeyIterator.next(); + if(state.keyVar != null) { + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable( + state.keyVar.getDefinedType(), state.keyVar.getVariableName(), + key, state.keyVar.getDefinedTarget(), env)); + } + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable( + state.valueVar.getDefinedType(), state.valueVar.getVariableName(), + state.arr.get(key, t, env), state.valueVar.getDefinedTarget(), env)); + state.phase = ForeachState.Phase.LOOP_BODY; + return new StepResult<>(new Evaluate(state.codeNode), state); + } + + /** + * Advances the non-associative iterator, skipping blacklisted entries and + * handling skipCount from continue(n). If the iterator reaches the end, + * cleans up and returns Complete. + */ + private StepResult advanceNonAssociative(Target t, + ForeachState state, Environment env) { + Iterator iter = state.nonAssocIterator; + // If we are re-entering after a body execution or a continue, + // we need to advance past the current element first. + if(state.phase == ForeachState.Phase.LOOP_BODY) { + iter.incrementCurrent(); + } + // Skip blacklisted entries (removed during iteration) + // and handle skipCount from continue(n) + while(iter.getCurrent() < state.arr.size(env)) { + if(iter.isBlacklisted(iter.getCurrent())) { + iter.incrementCurrent(); + continue; + } + if(state.skipCount > 1) { + state.skipCount--; + iter.incrementCurrent(); + continue; + } + state.skipCount = 0; + break; + } + if(iter.getCurrent() >= state.arr.size(env)) { + cleanupIterator(state); + return new StepResult<>(new Complete(CVoid.VOID), state); + } + int current = iter.getCurrent(); + if(state.keyVar != null) { + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable( + state.keyVar.getDefinedType(), state.keyVar.getVariableName(), + new CInt(current, t), state.keyVar.getDefinedTarget(), env)); + } + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable( + state.valueVar.getDefinedType(), state.valueVar.getVariableName(), + state.arr.get(current, t, env), state.valueVar.getDefinedTarget(), env)); + state.phase = ForeachState.Phase.LOOP_BODY; + return new StepResult<>(new Evaluate(state.codeNode), state); + } + + private void cleanupIterator(ForeachState state) { + if(!state.isAssociative && state.nonAssocIterator != null + && state.arrayAccessList != null) { + state.arrayAccessList.remove(state.nonAssocIterator); + state.nonAssocIterator = null; + } + } + + @Override + public void cleanup(Target t, ForeachState state, Environment env) { + if(state != null) { + cleanupIterator(state); + } + } + + @Override + public String getName() { + return "foreach"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2, 3, 4}; + } + + @Override + public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) + throws CancelCommandException, ConfigRuntimeException { return CVoid.VOID; } + @Override public FunctionSignatures getSignatures() { return new SignatureBuilder(CVoid.TYPE) @@ -1719,11 +2026,6 @@ public Boolean runAsync() { return null; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ @@ -1911,27 +2213,14 @@ public String getName() { } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - ParseTree array = nodes[0]; - //The last one - ParseTree elseCode = nodes[nodes.length - 1]; - - Mixed data = parent.seval(array, env); - - if(!(data.isInstanceOf(CArray.TYPE, null, env)) && !(data instanceof CSlice)) { - throw new CRECastException(getName() + " expects an array for parameter 1", t); + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(children.length < 4) { + throw new CREInsufficientArgumentsException( + "Insufficient arguments passed to " + getName(), t); } - - if(((CArray) data).isEmpty(env)) { - 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; + int offset = (children.length == 5) ? 1 : 0; + ForeachState state = new ForeachState(children, offset, true); + return new StepResult<>(new Evaluate(children[0]), state); } @Override @@ -2047,7 +2336,78 @@ public List isBranch(List children) { @breakable @seealso({com.laytonsmith.tools.docgen.templates.Loops.class}) @SelfStatement - public static class _while extends AbstractFunction implements BranchStatement, VariableScope { + public static class _while extends AbstractFunction implements FlowFunction<_while.WhileState>, BranchStatement, VariableScope { + + static class WhileState { + enum Phase { CONDITION, BODY } + Phase phase = Phase.CONDITION; + ParseTree[] children; + int skipCount; + + WhileState(ParseTree[] children) { + this.children = children; + } + + @Override + public String toString() { + return phase.name().toLowerCase(); + } + } + + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + WhileState state = new WhileState(children); + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, WhileState state, + Mixed result, Environment env) { + switch(state.phase) { + case CONDITION: + if(ArgumentValidation.getBooleanish(result, t, env)) { + if(state.skipCount > 1) { + state.skipCount--; + return new StepResult<>(new Evaluate(state.children[0]), state); + } + state.skipCount = 0; + if(state.children.length > 1) { + state.phase = WhileState.Phase.BODY; + return new StepResult<>(new Evaluate(state.children[1]), state); + } + // while(condition) with no body — re-evaluate condition + return new StepResult<>(new Evaluate(state.children[0]), state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + case BODY: + state.phase = WhileState.Phase.CONDITION; + return new StepResult<>(new Evaluate(state.children[0]), state); + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid while state: " + state.phase, t); + } + } + + @Override + public StepResult childInterrupted(Target t, WhileState state, + StepAction.FlowControl action, Environment env) { + if(state.phase == WhileState.Phase.BODY) { + if(action.getAction() instanceof BreakAction breakAction) { + int levels = breakAction.getLevels(); + if(levels <= 1) { + return new StepResult<>(new Complete(CVoid.VOID), state); + } + return new StepResult<>(new FlowControl( + new BreakAction(levels - 1, breakAction.getTarget())), state); + } + if(action.getAction() instanceof ContinueAction continueAction) { + state.skipCount = continueAction.getLevels(); + state.phase = WhileState.Phase.CONDITION; + return new StepResult<>(new Evaluate(state.children[0]), state); + } + } + return null; // propagate + } public static final String NAME = "while"; @@ -2088,28 +2448,6 @@ public Boolean runAsync() { return null; } - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - try { - while(ArgumentValidation.getBoolean(parent.seval(nodes[0], env), t, env)) { - //We allow while(thing()); to be done. This makes certain - //types of coding styles possible. - if(nodes.length > 1) { - try { - parent.eval(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 FunctionSignatures getSignatures() { return new SignatureBuilder(CVoid.TYPE) @@ -2118,11 +2456,6 @@ public FunctionSignatures getSignatures() { .param(null, "code", "The code that is executed in the loop.", true).build(); } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { return CNull.NULL; @@ -2205,7 +2538,74 @@ public List isScope(List children) { @breakable @seealso({com.laytonsmith.tools.docgen.templates.Loops.class}) @SelfStatement - public static class _dowhile extends AbstractFunction implements BranchStatement, VariableScope { + public static class _dowhile extends AbstractFunction implements FlowFunction<_dowhile.DoWhileState>, BranchStatement, VariableScope { + + static class DoWhileState { + enum Phase { BODY, CONDITION } + Phase phase = Phase.BODY; + ParseTree[] children; + int skipCount; + + DoWhileState(ParseTree[] children) { + this.children = children; + } + + @Override + public String toString() { + return phase.name().toLowerCase(); + } + } + + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + DoWhileState state = new DoWhileState(children); + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, DoWhileState state, + Mixed result, Environment env) { + switch(state.phase) { + case BODY: + state.phase = DoWhileState.Phase.CONDITION; + return new StepResult<>(new Evaluate(state.children[1]), state); + case CONDITION: + if(ArgumentValidation.getBooleanish(result, t, env)) { + if(state.skipCount > 1) { + state.skipCount--; + return new StepResult<>(new Evaluate(state.children[1]), state); + } + state.skipCount = 0; + state.phase = DoWhileState.Phase.BODY; + return new StepResult<>(new Evaluate(state.children[0]), state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid dowhile state: " + state.phase, t); + } + } + + @Override + public StepResult childInterrupted(Target t, DoWhileState state, + StepAction.FlowControl action, Environment env) { + if(state.phase == DoWhileState.Phase.BODY) { + if(action.getAction() instanceof BreakAction breakAction) { + int levels = breakAction.getLevels(); + if(levels <= 1) { + return new StepResult<>(new Complete(CVoid.VOID), state); + } + return new StepResult<>(new FlowControl( + new BreakAction(levels - 1, breakAction.getTarget())), state); + } + if(action.getAction() instanceof ContinueAction continueAction) { + state.skipCount = continueAction.getLevels(); + state.phase = DoWhileState.Phase.CONDITION; + return new StepResult<>(new Evaluate(state.children[1]), state); + } + } + return null; // propagate + } public static final String NAME = "dowhile"; @@ -2262,29 +2662,6 @@ public MSVersion since() { return MSVersion.V3_3_1; } - @Override - public boolean useSpecialExec() { - return true; - } - - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - try { - do { - try { - parent.eval(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(ArgumentValidation.getBoolean(parent.seval(nodes[1], env), t, env)); - } catch (LoopBreakException e) { - if(e.getTimes() > 1) { - throw new LoopBreakException(e.getTimes() - 1, t); - } - } - return CVoid.VOID; - } - @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { @@ -2354,7 +2731,7 @@ public List isScope(List children) { } @api - public static class _break extends AbstractFunction implements Optimizable { + public static class _break extends AbstractFunction implements FlowFunction, Optimizable { public static final String NAME = "break"; @@ -2363,6 +2740,20 @@ public String getName() { return "break"; } + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(children.length == 0) { + return new StepResult<>(new FlowControl(new BreakAction(1, t)), null); + } + return new StepResult<>(new Evaluate(children[0]), null); + } + + @Override + public StepResult childCompleted(Target t, Void state, Mixed result, Environment env) { + int n = ArgumentValidation.getInt32(result, t, env); + return new StepResult<>(new FlowControl(new BreakAction(n, t)), null); + } + @Override public Integer[] numArgs() { return new Integer[]{0, 1}; @@ -2474,11 +2865,7 @@ public Boolean runAsync() { @Override public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws CancelCommandException, ConfigRuntimeException { - int num = 1; - if(args.length == 1) { - num = ArgumentValidation.getInt32(args[0], t, env); - } - throw new LoopBreakException(num, t); + throw new Error("break() should not be called via exec(); it is handled by the iterative interpreter"); } @Override @@ -2534,7 +2921,7 @@ public Set optimizationOptions() { } @api - public static class _continue extends AbstractFunction { + public static class _continue extends AbstractFunction implements FlowFunction { public static final String NAME = "continue"; @@ -2543,6 +2930,20 @@ public String getName() { return NAME; } + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(children.length == 0) { + return new StepResult<>(new FlowControl(new ContinueAction(1, t)), null); + } + return new StepResult<>(new Evaluate(children[0]), null); + } + + @Override + public StepResult childCompleted(Target t, Void state, Mixed result, Environment env) { + int n = ArgumentValidation.getInt32(result, t, env); + return new StepResult<>(new FlowControl(new ContinueAction(n, t)), null); + } + @Override public Integer[] numArgs() { return new Integer[]{0, 1}; @@ -2622,11 +3023,7 @@ public Boolean runAsync() { @Override public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws CancelCommandException, ConfigRuntimeException { - int num = 1; - if(args.length == 1) { - num = ArgumentValidation.getInt32(args[0], t, env); - } - throw new LoopContinueException(num, t); + throw new Error("continue() should not be called via exec(); it is handled by the iterative interpreter"); } @Override @@ -2650,7 +3047,7 @@ public ExampleScript[] examples() throws ConfigCompileException { } @api - public static class _return extends AbstractFunction implements Optimizable { + public static class _return extends AbstractFunction implements FlowFunction, Optimizable { public static final String NAME = "return"; @@ -2659,6 +3056,19 @@ public String getName() { return NAME; } + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(children.length == 0) { + return new StepResult<>(new FlowControl(new ReturnAction(CVoid.VOID, t)), null); + } + return new StepResult<>(new Evaluate(children[0]), null); + } + + @Override + public StepResult childCompleted(Target t, Void state, Mixed result, Environment env) { + return new StepResult<>(new FlowControl(new ReturnAction(result, t)), null); + } + @Override public Integer[] numArgs() { return new Integer[]{0, 1}; @@ -2758,8 +3168,7 @@ public Set optimizationOptions() { @Override public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { - Mixed ret = (args.length == 1 ? args[0] : CVoid.VOID); - throw new FunctionReturnException(ret, t); + throw new Error("return() should not be called via exec(); it is handled by the iterative interpreter"); } @Override diff --git a/src/main/java/com/laytonsmith/core/functions/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java index c38bc9bd09..5a583d1cb5 100644 --- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java @@ -14,6 +14,7 @@ import com.laytonsmith.annotations.unbreakable; import com.laytonsmith.core.ArgumentValidation; import com.laytonsmith.core.natives.interfaces.Callable; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.Globals; import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.MSLog; @@ -26,6 +27,10 @@ import com.laytonsmith.core.Script; import com.laytonsmith.core.Security; import com.laytonsmith.core.Static; +import com.laytonsmith.core.StepAction; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.BranchStatement; import com.laytonsmith.core.compiler.CompilerEnvironment; import com.laytonsmith.core.compiler.CompilerWarning; @@ -86,7 +91,6 @@ import com.laytonsmith.core.exceptions.CRE.CREInsufficientPermissionException; import com.laytonsmith.core.exceptions.CRE.CREInvalidProcedureException; import com.laytonsmith.core.exceptions.CRE.CRERangeException; -import com.laytonsmith.core.exceptions.CRE.CREStackOverflowError; import com.laytonsmith.core.exceptions.CRE.CREThrowable; import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigCompileException; @@ -1498,7 +1502,197 @@ public ExampleScript[] examples() throws ConfigCompileException { @api @unbreakable @SelfStatement - public static class proc extends AbstractFunction implements BranchStatement, VariableScope, DocumentSymbolProvider { + public static class proc extends AbstractFunction implements FlowFunction, BranchStatement, VariableScope, DocumentSymbolProvider { + + static class ProcState { + enum Phase { EVAL_DEFAULTS, EVAL_PARAMS } + Phase phase = Phase.EVAL_DEFAULTS; + + ParseTree[] nodes; // after stripping return type prefix + CClassType returnType; + ParseTree code; + IVariableList originalList; + + // Phase 1 (EVAL_DEFAULTS) + int defaultIndex = 1; // starts at 1, skips proc name + Mixed[] paramDefaultValues; + boolean procDefinitelyNotConstant; + + // Phase 2 (EVAL_PARAMS) + int paramIndex = 0; + String name = ""; + List vars = new ArrayList<>(); + List varNames = new ArrayList<>(); + + @Override + public String toString() { + return phase + " idx=" + (phase == Phase.EVAL_DEFAULTS ? defaultIndex : paramIndex); + } + } + + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + ProcState state = new ProcState(); + + // Parse return type from first child (CClassType or CVoid) + state.returnType = Auto.TYPE; + NodeModifiers modifiers = null; + ParseTree[] nodes = children; + if(nodes[0].getData().equals(CVoid.VOID) || nodes[0].getData() instanceof CClassType) { + if(nodes[0].getData().equals(CVoid.VOID)) { + state.returnType = CVoid.TYPE; + } else { + state.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]; + } + modifiers = nodes[0].getNodeModifiers(); + nodes = newNodes; + } + nodes[0].getNodeModifiers().merge(modifiers); + state.nodes = nodes; + + // Save variable list for restoration after param evaluation + state.originalList = env.getEnv(GlobalEnv.class).GetVarList().clone(); + + // Get code block (last node) + state.code = nodes[nodes.length - 1]; + + // Init default values array + state.paramDefaultValues = new Mixed[nodes.length - 1]; + + // Start Phase 1: evaluate default parameter values + return advanceToNextDefault(t, state, env); + } + + @Override + public StepResult childCompleted(Target t, ProcState state, + Mixed result, Environment env) { + if(state.phase == ProcState.Phase.EVAL_DEFAULTS) { + env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED); + state.paramDefaultValues[state.defaultIndex] = result; + state.defaultIndex++; + return advanceToNextDefault(t, state, env); + } else { + // EVAL_PARAMS phase + env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED); + env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN); + + Mixed defaultValue = state.paramDefaultValues[state.paramIndex]; + IVariable ivar; + if(defaultValue != null) { + ivar = (IVariable) result; + } else { + if(state.paramIndex == 0) { + if(result instanceof IVariable) { + throw new CREInvalidProcedureException( + "Anonymous Procedures are not allowed", t); + } + state.name = result.val(); + state.paramIndex++; + return advanceToNextParam(t, state, env); + } + if(!(result instanceof IVariable)) { + throw new CREInvalidProcedureException( + "You must use IVariables as the arguments", t); + } + ivar = (IVariable) result; + } + + // Check for duplicate parameter names + String varName = ivar.getVariableName(); + if(state.varNames.contains(varName)) { + throw new CREInvalidProcedureException( + "Same variable name defined twice in " + state.name, t); + } + state.varNames.add(varName); + + // Store IVariable with default value + Mixed ivarVal = (defaultValue != null ? defaultValue : new CString("", t)); + state.vars.add(new IVariable(ivar.getDefinedType(), + ivar.getVariableName(), ivarVal, ivar.getTarget())); + + state.paramIndex++; + return advanceToNextParam(t, state, env); + } + } + + @Override + public StepResult childInterrupted(Target t, ProcState state, + StepAction.FlowControl action, Environment env) { + env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED); + env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN); + return null; // propagate + } + + private StepResult advanceToNextDefault(Target t, ProcState state, Environment env) { + while(state.defaultIndex < state.nodes.length - 1) { + ParseTree node = state.nodes[state.defaultIndex]; + if(node.getData() instanceof CFunction cf) { + if(cf.val().equals(assign.NAME) || cf.val().equals(__unsafe_assign__.NAME)) { + ParseTree paramDefaultValueNode = node.getChildAt(node.numberOfChildren() - 1); + if(Construct.IsDynamicHelper(paramDefaultValueNode.getData())) { + state.procDefinitelyNotConstant = true; + } + env.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED, true); + return new StepResult<>(new Evaluate(paramDefaultValueNode), state); + } else if(cf.val().equals(__autoconcat__.NAME)) { + throw new CREInvalidProcedureException( + "Invalid arguments defined for procedure", t); + } + } + state.paramDefaultValues[state.defaultIndex] = null; + state.defaultIndex++; + } + return startParamPhase(t, state, env); + } + + private StepResult startParamPhase(Target t, ProcState state, Environment env) { + state.phase = ProcState.Phase.EVAL_PARAMS; + state.paramIndex = 0; + return advanceToNextParam(t, state, env); + } + + private StepResult advanceToNextParam(Target t, ProcState state, Environment env) { + if(state.paramIndex >= state.nodes.length - 1) { + return registerProc(t, state, env); + } + env.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN, true); + env.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED, true); + Mixed defaultValue = state.paramDefaultValues[state.paramIndex]; + if(defaultValue != null) { + // Build temp assign node with pre-evaluated default value + ParseTree assignNode = state.nodes[state.paramIndex]; + CFunction assignFunc = (CFunction) assignNode.getData(); + CFunction newCf = new CFunction(assignFunc.val(), assignNode.getTarget()); + newCf.setFunction(assignFunc.getCachedFunction()); + ParseTree tempAssignNode = new ParseTree(newCf, assignNode.getFileOptions()); + if(assignNode.numberOfChildren() == 3) { + tempAssignNode.addChild(assignNode.getChildAt(0)); + tempAssignNode.addChild(assignNode.getChildAt(1)); + tempAssignNode.addChild(new ParseTree(defaultValue, assignNode.getFileOptions())); + } else { + tempAssignNode.addChild(assignNode.getChildAt(0)); + tempAssignNode.addChild(new ParseTree(defaultValue, assignNode.getFileOptions())); + } + return new StepResult<>(new Evaluate(tempAssignNode, null, true), state); + } else { + return new StepResult<>(new Evaluate(state.nodes[state.paramIndex], null, true), state); + } + } + + private StepResult registerProc(Target t, ProcState state, Environment env) { + env.getEnv(GlobalEnv.class).SetVarList(state.originalList); + Procedure myProc = new Procedure(state.name, state.returnType, state.vars, + state.nodes[0].getNodeModifiers().getComment(), state.code, t); + if(state.procDefinitelyNotConstant) { + myProc.definitelyNotConstant(); + } + env.getEnv(GlobalEnv.class).GetProcs().put(myProc.getName(), myProc); + return new StepResult<>(new Complete(CVoid.VOID), state); + } public static final String NAME = "proc"; @@ -1547,13 +1741,6 @@ public Boolean runAsync() { return null; } - @Override - public Mixed 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 vars = new ArrayList<>(); @@ -1622,8 +1809,13 @@ public static Procedure getProcedure(Target t, Environment env, Script parent, P // Construct temporary assign node to assign resulting default parameter value. ParseTree assignNode = nodes[i]; CFunction assignFunc = (CFunction) assignNode.getData(); - ParseTree tempAssignNode = new ParseTree(new CFunction(assignFunc.val(), - assignNode.getTarget()), assignNode.getFileOptions()); + CFunction tempFunc = new CFunction(assignFunc.val(), assignNode.getTarget()); + try { + tempFunc.getFunction(); + } catch(ConfigCompileException ex) { + throw new Error(ex); + } + ParseTree tempAssignNode = new ParseTree(tempFunc, assignNode.getFileOptions()); if(assignNode.numberOfChildren() == 3) { tempAssignNode.addChild(assignNode.getChildAt(0)); tempAssignNode.addChild(assignNode.getChildAt(1)); @@ -1769,11 +1961,6 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast return declScope; } - @Override - public boolean useSpecialExec() { - return true; - } - /** * Returns either null to indicate that the procedure is not const, or returns a single Mixed, which should * replace the call to the procedure. @@ -2035,7 +2222,8 @@ public Set optimizationOptions() { @api @DocumentLink(0) - public static class include extends AbstractFunction implements Optimizable, DocumentLinkProvider { + public static class include extends AbstractFunction implements Optimizable, DocumentLinkProvider, + FlowFunction { public static final String NAME = "include"; @@ -2079,94 +2267,122 @@ public CVoid exec(Target t, Environment env, GenericParameters generics, Mixed.. return CVoid.VOID; } - @Override - public CVoid execs(Target t, Environment env, Script parent, ParseTree... nodes) { - ParseTree tree = nodes[0]; - Mixed arg = parent.seval(tree, env); - String location = arg.val(); - File file = Static.GetFileFromArgument(location, env, t, null); - try { - file = file.getCanonicalFile(); - } catch (IOException ex) { - throw new CREIOException(ex.getMessage(), t); + static class IncludeState { + enum Phase { EVAL_PATH, EVAL_INCLUDE } + Phase phase = Phase.EVAL_PATH; + ParseTree[] children; + + IncludeState(ParseTree[] children) { + this.children = children; } - // Create new static analysis for dynamic includes that have not yet been cached. - StaticAnalysis analysis; - IncludeCache includeCache = env.getEnv(StaticRuntimeEnv.class).getIncludeCache(); - boolean isFirstCompile = false; - Scope parentScope = includeCache.getDynamicAnalysisParentScopeCache().get(t); - if(parentScope != null) { - analysis = includeCache.getStaticAnalysis(file); - if(analysis == null) { - analysis = new StaticAnalysis(true); - analysis.getStartScope().addParent(parentScope); - isFirstCompile = true; - } - } else { - analysis = new StaticAnalysis(true); // It's a static include. + + @Override + public String toString() { + return phase.name(); } + } - // Get or load the include. - ParseTree include = IncludeCache.get(file, env, env.getEnvClasses(), analysis, t); - - // Perform static analysis for dynamic includes. - // This should not run if this is the first compile for this include, as IncludeCache.get() checks it then. - /* - * TODO - This analysis runs on an optimized AST. - * Cloning, caching and using the non-optimized AST would be nice. - * This solution is acceptable in the meantime, as the first analysis of a dynamic include runs - * on the non-optimized AST through IncludeCache.get(), and otherwise-runtime errors should still be - * caught when analyzing the optimized AST. - */ - if(isFirstCompile) { - - // Remove this parent scope since it should not end up in the cached analysis. - analysis.getStartScope().removeParent(parentScope); - } else { + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + IncludeState state = new IncludeState(children); + return new StepResult<>(new Evaluate(children[0]), state); + } - // Set up analysis. Cloning is required to not mess up the cached analysis. - analysis = analysis.clone(); - analysis.getStartScope().addParent(parentScope); - Set exceptions = new HashSet<>(); - analysis.analyzeFinalScopeGraph(env, exceptions); - - // Handle compile exceptions. - if(exceptions.size() == 1) { - ConfigCompileException ex = exceptions.iterator().next(); - String fileName = (ex.getFile() == null ? "Unknown Source" : ex.getFile().getName()); - throw new CREIncludeException( - "There was a compile error when trying to include the script at " + file - + "\n" + ex.getMessage() + " :: " + fileName + ":" + ex.getLineNum(), t); - } else if(exceptions.size() > 1) { - StringBuilder b = new StringBuilder(); - b.append("There were compile errors when trying to include the script at ") - .append(file).append("\n"); - for(ConfigCompileException ex : exceptions) { - String fileName = (ex.getFile() == null ? "Unknown Source" : ex.getFile().getName()); - b.append(ex.getMessage()).append(" :: ").append(fileName).append(":") - .append(ex.getLineNum()).append("\n"); + @Override + public StepResult childCompleted(Target t, IncludeState state, + Mixed result, Environment env) { + switch(state.phase) { + case EVAL_PATH -> { + String location = result.val(); + File file = Static.GetFileFromArgument(location, env, t, null); + try { + file = file.getCanonicalFile(); + } catch(IOException ex) { + throw new CREIOException(ex.getMessage(), t); + } + StaticAnalysis analysis; + IncludeCache includeCache = env.getEnv(StaticRuntimeEnv.class).getIncludeCache(); + boolean isFirstCompile = false; + Scope parentScope = includeCache.getDynamicAnalysisParentScopeCache().get(t); + if(parentScope != null) { + analysis = includeCache.getStaticAnalysis(file); + if(analysis == null) { + analysis = new StaticAnalysis(true); + analysis.getStartScope().addParent(parentScope); + isFirstCompile = true; + } + } else { + analysis = new StaticAnalysis(true); + } + ParseTree include = IncludeCache.get(file, env, env.getEnvClasses(), analysis, t); + if(isFirstCompile) { + analysis.getStartScope().removeParent(parentScope); + } else if(parentScope != null) { + analysis = analysis.clone(); + analysis.getStartScope().addParent(parentScope); + Set exceptions = new HashSet<>(); + analysis.analyzeFinalScopeGraph(env, exceptions); + if(exceptions.size() == 1) { + ConfigCompileException ex = exceptions.iterator().next(); + String fileName = (ex.getFile() == null + ? "Unknown Source" : ex.getFile().getName()); + throw new CREIncludeException( + "There was a compile error when trying to include the script at " + + file + "\n" + ex.getMessage() + + " :: " + fileName + ":" + ex.getLineNum(), t); + } else if(exceptions.size() > 1) { + StringBuilder b = new StringBuilder(); + b.append("There were compile errors when trying to include the script at ") + .append(file).append("\n"); + for(ConfigCompileException ex : exceptions) { + String fileName = (ex.getFile() == null + ? "Unknown Source" : ex.getFile().getName()); + b.append(ex.getMessage()).append(" :: ").append(fileName) + .append(":").append(ex.getLineNum()).append("\n"); + } + throw new CREIncludeException(b.toString(), t); + } + } + if(include != null) { + StackTraceManager stManager + = env.getEnv(GlobalEnv.class).GetStackTraceManager(); + stManager.addStackTraceElement( + new ConfigRuntimeException.StackTraceElement( + "<>", t)); + state.phase = IncludeState.Phase.EVAL_INCLUDE; + return new StepResult<>(new Evaluate(include.getChildAt(0)), state); } - throw new CREIncludeException(b.toString(), t); + return new StepResult<>(new Complete(CVoid.VOID), state); + } + case EVAL_INCLUDE -> { + return new StepResult<>(new Complete(CVoid.VOID), state); } } + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid include state: " + state.phase, t); + } - if(include != null) { - // It could be an empty file - StackTraceManager stManager = env.getEnv(GlobalEnv.class).GetStackTraceManager(); - stManager.addStackTraceElement( - new ConfigRuntimeException.StackTraceElement("<>", t)); - try { - parent.eval(include.getChildAt(0), env); - } catch (AbstractCREException e) { - e.freezeStackTraceElements(stManager); - throw e; - } catch (StackOverflowError e) { - throw new CREStackOverflowError(null, t, e); - } finally { - stManager.popStackTraceElement(); + @Override + public StepResult childInterrupted(Target t, IncludeState state, + StepAction.FlowControl action, Environment env) { + if(state.phase == IncludeState.Phase.EVAL_INCLUDE) { + if(action.getAction() instanceof Exceptions.ThrowAction throwAction) { + ConfigRuntimeException ex = throwAction.getException(); + if(ex instanceof AbstractCREException ace) { + ace.freezeStackTraceElements( + env.getEnv(GlobalEnv.class).GetStackTraceManager()); + } } } - return CVoid.VOID; + return null; + } + + @Override + public void cleanup(Target t, IncludeState state, Environment env) { + if(state != null + && state.phase == IncludeState.Phase.EVAL_INCLUDE) { + env.getEnv(GlobalEnv.class).GetStackTraceManager().popStackTraceElement(); + } } @Override @@ -2198,11 +2414,6 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast return super.linkScope(analysis, parentScope, ast, env, exceptions); } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public Set optimizationOptions() { return EnumSet.of( @@ -2717,7 +2928,117 @@ public ExampleScript[] examples() throws ConfigCompileException { @api @unbreakable @seealso({com.laytonsmith.tools.docgen.templates.Closures.class}) - public static class closure extends AbstractFunction implements BranchStatement, VariableScope { + public static class closure extends AbstractFunction implements FlowFunction, BranchStatement, VariableScope { + + static class ClosureState { + ParseTree[] children; + Environment closureEnv; + CClassType returnType; + int nodeOffset; + int paramIndex; + int numParams; + String[] names; + Mixed[] defaults; + CClassType[] types; + + ClosureState(ParseTree[] children, Environment closureEnv, CClassType returnType, + int nodeOffset, int numParams) { + this.children = children; + this.closureEnv = closureEnv; + this.returnType = returnType; + this.nodeOffset = nodeOffset; + this.numParams = numParams; + this.names = new String[numParams]; + this.defaults = new Mixed[numParams]; + this.types = new CClassType[numParams]; + } + + @Override + public String toString() { + return "param " + paramIndex + "/" + numParams; + } + } + + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + CClassType returnType = Auto.TYPE; + int nodeOffset = 0; + if(children.length > 0 && children[0].getData() instanceof CClassType) { + returnType = (CClassType) children[0].getData(); + nodeOffset = 1; + } + + if(children.length - nodeOffset == 0) { + return new StepResult<>(new Complete( + createClosureObject(null, env, returnType, + new String[0], new Mixed[0], new CClassType[0], t)), null); + } + + Environment myEnv; + try { + myEnv = env.clone(); + } catch(CloneNotSupportedException ex) { + throw new RuntimeException(ex); + } + + int numParams = children.length - nodeOffset - 1; + ClosureState state = new ClosureState(children, myEnv, returnType, nodeOffset, numParams); + + if(numParams == 0) { + return new StepResult<>(new Complete( + createClosureObject(children[children.length - 1], myEnv, returnType, + new String[0], new Mixed[0], new CClassType[0], t)), state); + } + + myEnv.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_CLOSURE_WARN_OVERWRITE, true); + myEnv.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED, true); + return new StepResult<>(new Evaluate(children[nodeOffset], myEnv, true), state); + } + + @Override + public StepResult childCompleted(Target t, ClosureState state, + Mixed result, Environment env) { + if(!(result instanceof IVariable iv)) { + throw new CRECastException("Arguments sent to " + getName() + + " barring the last) must be ivariables", t); + } + state.names[state.paramIndex] = iv.getVariableName(); + try { + state.defaults[state.paramIndex] = iv.ival().clone(); + state.types[state.paramIndex] = iv.getDefinedType(); + } catch(CloneNotSupportedException ex) { + Logger.getLogger(DataHandling.class.getName()).log(Level.SEVERE, null, ex); + } + state.paramIndex++; + + if(state.paramIndex < state.numParams) { + return new StepResult<>(new Evaluate( + state.children[state.nodeOffset + state.paramIndex], + state.closureEnv, true), state); + } + + state.closureEnv.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED); + state.closureEnv.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_CLOSURE_WARN_OVERWRITE); + ParseTree body = state.children[state.children.length - 1]; + return new StepResult<>(new Complete( + createClosureObject(body, state.closureEnv, state.returnType, + state.names, state.defaults, state.types, t)), state); + } + + @Override + public StepResult childInterrupted(Target t, ClosureState state, + StepAction.FlowControl action, Environment env) { + if(state != null && state.closureEnv != null) { + state.closureEnv.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED); + state.closureEnv.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_CLOSURE_WARN_OVERWRITE); + } + return null; + } + + protected Mixed createClosureObject(ParseTree body, Environment env, CClassType returnType, + String[] names, Mixed[] defaults, CClassType[] types, Target t) { + return new CClosure(body, env, returnType, names, defaults, types, t); + } @Override public String getName() { @@ -2770,67 +3091,6 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. return CVoid.VOID; } - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - - // Use first child as closure return type if it is a type. - CClassType returnType = Auto.TYPE; - int nodeOffset = 0; - if(nodes.length > 0 && nodes[0].getData() instanceof CClassType) { - returnType = (CClassType) nodes[0].getData(); - nodeOffset = 1; - } - - // Return an empty (possibly statically typed) closure when it is empty and does not have any parameters. - if(nodes.length - nodeOffset == 0) { - return new CClosure(null, env, returnType, new String[0], new Mixed[0], new CClassType[0], t); - } - - // Clone the environment to prevent parameter and variable assigns overwriting variables in the outer scope. - Environment myEnv; - try { - myEnv = env.clone(); - } catch (CloneNotSupportedException ex) { - throw new RuntimeException(ex); - } - - // Get closure parameter names, default values and types. - int numParams = nodes.length - nodeOffset - 1; - String[] names = new String[numParams]; - Mixed[] defaults = new Mixed[numParams]; - CClassType[] types = new CClassType[numParams]; - for(int i = 0; i < numParams; i++) { - ParseTree node = nodes[i + nodeOffset]; - ParseTree newNode = new ParseTree(new CFunction(g.NAME, t), node.getFileOptions()); - List children = new ArrayList<>(); - children.add(node); - newNode.setChildren(children); - Script fakeScript = Script.GenerateScript(newNode, myEnv.getEnv(GlobalEnv.class).GetLabel(), null); - myEnv.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_CLOSURE_WARN_OVERWRITE, true); - myEnv.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED, true); - Mixed ret; - try { - ret = MethodScriptCompiler.execute(newNode, myEnv, null, fakeScript); - } finally { - myEnv.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED); - myEnv.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_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); - } - } - - // Create and return the closure, using the last argument as the closure body. - return new CClosure(nodes[nodes.length - 1], myEnv, returnType, names, defaults, types, t); - } - @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { @@ -2901,11 +3161,6 @@ public Version since() { return MSVersion.V3_3_0; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ @@ -2944,6 +3199,13 @@ public List isScope(List children) { @seealso({com.laytonsmith.tools.docgen.templates.Closures.class}) public static class iclosure extends closure { + @Override + protected Mixed createClosureObject(ParseTree body, Environment env, CClassType returnType, + String[] names, Mixed[] defaults, CClassType[] types, Target t) { + env.getEnv(GlobalEnv.class).SetVarList(null); + return new CIClosure(body, env, returnType, names, defaults, types, t); + } + @Override public String getName() { return "iclosure"; @@ -2967,66 +3229,6 @@ public String docs() { + " and examples."; } - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - if(nodes.length == 0) { - //Empty closure, do nothing. - return new CIClosure(null, env, Auto.TYPE, new String[]{}, new Mixed[]{}, new CClassType[]{}, t); - } - // Handle the closure type first thing - CClassType returnType = Auto.TYPE; - 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]; - Mixed[] defaults = new Mixed[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) { - throw new RuntimeException(ex); - } - for(int i = 0; i < nodes.length - 1; i++) { - ParseTree node = nodes[i]; - ParseTree newNode = new ParseTree(new CFunction(g.NAME, t), node.getFileOptions()); - List children = new ArrayList<>(); - children.add(node); - newNode.setChildren(children); - Script fakeScript = Script.GenerateScript(newNode, myEnv.getEnv(GlobalEnv.class).GetLabel(), null); - myEnv.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_CLOSURE_WARN_OVERWRITE, true); - myEnv.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED, true); - Mixed ret; - try { - ret = MethodScriptCompiler.execute(newNode, myEnv, null, fakeScript); - } finally { - myEnv.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED); - myEnv.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_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); - } - } - // Now that iclosure is done with the current variable list, it can be removed from the cloned environment. - // This ensures it's not unintentionally retaining values in memory cloned from the original scope. - myEnv.getEnv(GlobalEnv.class).SetVarList(null); - return new CIClosure(nodes[nodes.length - 1], myEnv, returnType, names, defaults, types, t); - } - @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { @@ -3822,7 +4024,8 @@ public Set optimizationOptions() { } @api - public static class eval extends AbstractFunction implements Optimizable { + public static class eval extends AbstractFunction implements Optimizable, + FlowFunction { @Override public String getName() { @@ -3857,74 +4060,121 @@ public MSVersion since() { } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - if(ArgumentValidation.getBooleanish(env.getEnv(GlobalEnv.class).GetRuntimeSetting("function.eval.disable", - CBoolean.FALSE), t)) { - throw new CREInsufficientPermissionException("eval is disabled", t); - } - boolean oldDynamicScriptMode = env.getEnv(GlobalEnv.class).GetDynamicScriptingMode(); - ParseTree node = nodes[0]; - try { - env.getEnv(GlobalEnv.class).SetDynamicScriptingMode(true); - Mixed script = parent.seval(node, env); - if(script.isInstanceOf(CClosure.TYPE, null, env)) { - throw new CRECastException("Closures cannot be eval'd directly. Use execute() instead.", t); - } - ParseTree root = MethodScriptCompiler.compile(MethodScriptCompiler.lex(script.val(), env, t.file(), true), - env, env.getEnvClasses()); - if(root == null) { - return new CString("", t); - } + public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws CancelCommandException, ConfigRuntimeException { + return CVoid.VOID; + } + //Doesn't matter, run out of state anyways - // Unwrap single value in __statements__() and return its string value. - if(root.getChildren().size() == 1 && root.getChildAt(0).getData() instanceof CFunction - && ((CFunction) root.getChildAt(0).getData()).getFunction().getName().equals(__statements__.NAME) - && root.getChildAt(0).getChildren().size() == 1) { - return new CString(parent.seval(root.getChildAt(0).getChildAt(0), env).val(), t); - } + @Override + public Boolean runAsync() { + return null; + } - // Concat string values of all children and return the result. - StringBuilder b = new StringBuilder(); - int count = 0; - for(ParseTree child : root.getChildren()) { - Mixed 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); + static class EvalState { + enum Phase { EVAL_ARG, EVAL_CHILDREN } + Phase phase = Phase.EVAL_ARG; + boolean oldDynamicScriptMode; + List compiledChildren; + int childIndex; + StringBuilder result = new StringBuilder(); + int count; + + @Override + public String toString() { + return phase == Phase.EVAL_ARG ? "EVAL_ARG" + : "EVAL_CHILDREN[" + childIndex + "/" + compiledChildren.size() + "]"; } } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws CancelCommandException, ConfigRuntimeException { - return CVoid.VOID; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(ArgumentValidation.getBooleanish(env.getEnv(GlobalEnv.class).GetRuntimeSetting( + "function.eval.disable", CBoolean.FALSE), t)) { + throw new CREInsufficientPermissionException("eval is disabled", t); + } + EvalState state = new EvalState(); + state.oldDynamicScriptMode = env.getEnv(GlobalEnv.class).GetDynamicScriptingMode(); + env.getEnv(GlobalEnv.class).SetDynamicScriptingMode(true); + return new StepResult<>(new Evaluate(children[0]), state); } - //Doesn't matter, run out of state anyways @Override - public Boolean runAsync() { - return null; + public StepResult childCompleted(Target t, EvalState state, + Mixed result, Environment env) { + switch(state.phase) { + case EVAL_ARG -> { + if(result.isInstanceOf(CClosure.TYPE, null, env)) { + throw new CRECastException( + "Closures cannot be eval'd directly. Use execute() instead.", t); + } + ParseTree root; + try { + root = MethodScriptCompiler.compile( + MethodScriptCompiler.lex(result.val(), env, t.file(), true), + env, env.getEnvClasses()); + } 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); + } + if(root == null) { + return new StepResult<>(new Complete(new CString("", t)), state); + } + // Unwrap single value in __statements__() with single child + try { + if(root.getChildren().size() == 1 && root.getChildAt(0).getData() instanceof CFunction + && ((CFunction) root.getChildAt(0).getData()).getFunction().getName() + .equals(__statements__.NAME) + && root.getChildAt(0).getChildren().size() == 1) { + state.compiledChildren = List.of(root.getChildAt(0).getChildAt(0)); + } else { + state.compiledChildren = root.getChildren(); + } + } catch(ConfigCompileException e) { + throw new CREFormatException( + "Could not compile eval'd code: " + e.getMessage(), t); + } + state.phase = EvalState.Phase.EVAL_CHILDREN; + state.childIndex = 0; + if(state.compiledChildren.isEmpty()) { + return new StepResult<>(new Complete(new CString("", t)), state); + } + return new StepResult<>( + new Evaluate(state.compiledChildren.get(state.childIndex)), state); + } + case EVAL_CHILDREN -> { + if(!result.val().trim().isEmpty()) { + if(state.count > 0) { + state.result.append(" "); + } + state.result.append(result.val()); + } + state.count++; + state.childIndex++; + if(state.childIndex < state.compiledChildren.size()) { + return new StepResult<>( + new Evaluate(state.compiledChildren.get(state.childIndex)), state); + } + return new StepResult<>( + new Complete(new CString(state.result.toString(), t)), state); + } + } + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid eval state: " + state.phase, t); } @Override - public boolean useSpecialExec() { - return true; + public void cleanup(Target t, EvalState state, Environment env) { + if(state != null) { + env.getEnv(GlobalEnv.class).SetDynamicScriptingMode( + state.oldDynamicScriptMode); + } } @Override diff --git a/src/main/java/com/laytonsmith/core/functions/EventBinding.java b/src/main/java/com/laytonsmith/core/functions/EventBinding.java index ede29785f5..40e620c775 100644 --- a/src/main/java/com/laytonsmith/core/functions/EventBinding.java +++ b/src/main/java/com/laytonsmith/core/functions/EventBinding.java @@ -8,10 +8,13 @@ import com.laytonsmith.annotations.hide; import com.laytonsmith.core.ArgumentValidation; import com.laytonsmith.core.MSVersion; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; import com.laytonsmith.core.Static; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.BranchStatement; import com.laytonsmith.core.compiler.CompilerEnvironment; import com.laytonsmith.core.compiler.CompilerWarning; @@ -81,7 +84,7 @@ public static String docs() { @api @SelfStatement - public static class bind extends AbstractFunction implements Optimizable, BranchStatement, VariableScope, DocumentSymbolProvider { + public static class bind extends AbstractFunction implements FlowFunction, Optimizable, BranchStatement, VariableScope, DocumentSymbolProvider { @Override public String getName() { @@ -132,67 +135,118 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. return CVoid.VOID; } + // -- FlowFunction implementation -- + // bind evaluates args 0-2 with IVariable resolution (equivalent to seval), + // args 3..n-2 with keepIVariable=true (need raw IVariables for event_obj and custom params), + // and does NOT evaluate the last arg (the code tree stored in the BoundEvent). + + static class BindState { + int nextArg = 0; + ParseTree[] children; + Mixed name; + Mixed options; + Mixed prefilter; + Mixed eventObj; + IVariableList customParams; + } + @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - if(nodes.length < 5) { + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(children.length < 5) { throw new CREInsufficientArgumentsException("bind accepts 5 or more parameters", t); } - Mixed name = parent.seval(nodes[0], env); - Mixed options = parent.seval(nodes[1], env); - Mixed prefilter = parent.seval(nodes[2], env); - Mixed event_obj = parent.eval(nodes[3], env); - IVariableList custom_params = new IVariableList(env.getEnv(GlobalEnv.class).GetVarList()); - for(int a = 0; a < nodes.length - 5; a++) { - Mixed var = parent.eval(nodes[4 + a], env); - if(!(var instanceof IVariable)) { - throw new CRECastException("The custom parameters must be ivariables", t); + BindState state = new BindState(); + state.children = children; + state.customParams = new IVariableList(env.getEnv(GlobalEnv.class).GetVarList()); + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, BindState state, Mixed result, Environment env) { + int argIndex = state.nextArg; + state.nextArg++; + + switch(argIndex) { + case 0 -> state.name = result; + case 1 -> { + if(!(result instanceof CNull || result.isInstanceOf(CArray.TYPE, null, env))) { + throw new CRECastException("The options must be an array or null", t); + } + state.options = result; } - IVariable cur = (IVariable) var; - custom_params.set(env.getEnv(GlobalEnv.class).GetVarList().get(cur.getVariableName(), - cur.getTarget(), env)); + case 2 -> { + if(!(result instanceof CNull || result.isInstanceOf(CArray.TYPE, null, env))) { + throw new CRECastException("The prefilters must be an array or null", t); + } + state.prefilter = result; + } + case 3 -> { + if(!(result instanceof IVariable)) { + throw new CRECastException("The event object must be an IVariable", t); + } + state.eventObj = result; + } + default -> { + if(!(result instanceof IVariable)) { + throw new CRECastException("The custom parameters must be ivariables", t); + } + IVariable cur = (IVariable) result; + state.customParams.set(env.getEnv(GlobalEnv.class).GetVarList() + .get(cur.getVariableName(), cur.getTarget(), env)); + } + } + + int nextIndex = state.nextArg; + int lastIndex = state.children.length - 1; + + if(nextIndex < lastIndex) { + boolean keepIVar = nextIndex >= 3; + return new StepResult<>(new Evaluate(state.children[nextIndex], null, keepIVar), state); + } + + // All args evaluated — register the event + return new StepResult<>(new Complete(registerBind(t, state, env)), state); + } + + /** + * Performs the bind registration after all args have been evaluated and validated. + */ + private Mixed registerBind(Target t, BindState state, Environment env) { + Mixed options = state.options; + Mixed prefilter = state.prefilter; + + if(options instanceof CNull) { + options = null; + } + if(prefilter instanceof CNull) { + prefilter = null; } + Environment newEnv = env; try { newEnv = env.clone(); - } catch (Exception e) { + } catch(Exception e) { } - // Set the permission to global if it's null, since that means - // it wasn't set, and so we aren't in a secured environment anyway. if(newEnv.getEnv(GlobalEnv.class).GetLabel() == null) { newEnv.getEnv(GlobalEnv.class).SetLabel(Static.GLOBAL_PERMISSION); } - newEnv.getEnv(GlobalEnv.class).SetVarList(custom_params); - ParseTree tree = nodes[nodes.length - 1]; + newEnv.getEnv(GlobalEnv.class).SetVarList(state.customParams); + + ParseTree tree = state.children[state.children.length - 1]; - //Check to see if our arguments are correct - if(!(options instanceof CNull || options.isInstanceOf(CArray.TYPE, null, env))) { - throw new CRECastException("The options must be an array or null", t); - } - if(!(prefilter instanceof CNull || prefilter.isInstanceOf(CArray.TYPE, null, env))) { - throw new CRECastException("The prefilters must be an array or null", t); - } - if(!(event_obj instanceof IVariable)) { - throw new CRECastException("The event object must be an IVariable", t); - } CString id; - if(options instanceof CNull) { - options = null; - } - if(prefilter instanceof CNull) { - prefilter = null; - } Event event; try { - BoundEvent be = new BoundEvent(name.val(), (CArray) options, (CArray) prefilter, - ((IVariable) event_obj).getVariableName(), newEnv, tree, t); + BoundEvent be = new BoundEvent(state.name.val(), (CArray) options, (CArray) prefilter, + ((IVariable) state.eventObj).getVariableName(), newEnv, tree, t); EventUtils.RegisterEvent(be); id = new CString(be.getId(), t); event = be.getEventDriver(); - } catch (EventException ex) { + } catch(EventException ex) { throw new CREBindException(ex.getMessage(), t); } - //Set up our bind counter, but only if the event is supposed to be added to the counter + // Set up bind counter for daemon thread management if(event.addCounter()) { synchronized(BIND_COUNTER) { if(BIND_COUNTER.get() == 0) { @@ -354,11 +408,6 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, return valScope; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public Set optimizationOptions() { return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC, OptimizationOption.CUSTOM_LINK); diff --git a/src/main/java/com/laytonsmith/core/functions/Exceptions.java b/src/main/java/com/laytonsmith/core/functions/Exceptions.java index 2fe8579a8f..a6e2632609 100644 --- a/src/main/java/com/laytonsmith/core/functions/Exceptions.java +++ b/src/main/java/com/laytonsmith/core/functions/Exceptions.java @@ -14,6 +14,7 @@ import com.laytonsmith.annotations.typeof; import com.laytonsmith.core.MSLog; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.FullyQualifiedClassName; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.LogLevel; @@ -21,7 +22,6 @@ import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; import com.laytonsmith.core.Prefs; -import com.laytonsmith.core.Script; import com.laytonsmith.core.compiler.BranchStatement; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.compiler.SelfStatement; @@ -49,10 +49,13 @@ import com.laytonsmith.core.exceptions.CRE.CREFormatException; import com.laytonsmith.core.exceptions.CRE.CREThrowable; import com.laytonsmith.core.functions.Compiler.__type_ref__; +import com.laytonsmith.core.StepAction; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.FunctionReturnException; import com.laytonsmith.core.exceptions.StackTraceManager; import com.laytonsmith.core.natives.interfaces.Mixed; import java.util.ArrayList; @@ -71,13 +74,155 @@ public static String docs() { return "This class contains functions related to Exception handling in MethodScript"; } + /** + * Produced by {@code throw()} and by native functions that throw {@link ConfigRuntimeException}. + * The interpreter loop catches ConfigRuntimeException from exec() calls and wraps them in this. + * {@code _try} and {@code complex_try} handle this in their {@code childInterrupted()}. + */ + public static class ThrowAction implements StepAction.FlowControlAction { + private final ConfigRuntimeException exception; + + public ThrowAction(ConfigRuntimeException exception) { + this.exception = exception; + } + + public ConfigRuntimeException getException() { + return exception; + } + + @Override + public Target getTarget() { + return exception.getTarget(); + } + } + @api @seealso({_throw.class, com.laytonsmith.tools.docgen.templates.Exceptions.class}) @SelfStatement - public static class _try extends AbstractFunction implements BranchStatement, VariableScope { + public static class _try extends AbstractFunction implements FlowFunction<_try.TryState>, BranchStatement, VariableScope { public static final String NAME = "try"; + enum Phase { RESOLVE_VAR, RESOLVE_TYPES, TRY_BODY, CATCH_BODY } + + static class TryState { + Phase phase; + ParseTree[] children; + IVariable ivar; + List interest; + int catchIndex; + + TryState(ParseTree[] children) { + this.children = children; + this.interest = new ArrayList<>(); + if(children.length == 2) { + catchIndex = 1; + } else if(children.length >= 3) { + catchIndex = 2; + } else { + catchIndex = -1; + } + } + + @Override + public String toString() { + return phase.name(); + } + } + + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + TryState state = new TryState(children); + if(children.length >= 3) { + state.phase = Phase.RESOLVE_VAR; + return new StepResult<>(new Evaluate(children[1], null, true), state); + } + state.phase = Phase.TRY_BODY; + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, TryState state, Mixed result, Environment env) { + switch(state.phase) { + case RESOLVE_VAR: + if(result instanceof IVariable iv) { + state.ivar = iv; + } else { + throw new CRECastException("Expected argument 2 to be an IVariable", t); + } + if(state.children.length == 4) { + state.phase = Phase.RESOLVE_TYPES; + return new StepResult<>(new Evaluate(state.children[3]), state); + } + state.phase = Phase.TRY_BODY; + return new StepResult<>(new Evaluate(state.children[0]), state); + case RESOLVE_TYPES: + Mixed ptypes = result; + if(ptypes.isInstanceOf(CString.TYPE, null, env)) { + state.interest.add(FullyQualifiedClassName.forName(ptypes.val(), t, env)); + } else if(ptypes.isInstanceOf(CArray.TYPE, null, env)) { + CArray ca = (CArray) ptypes; + for(int i = 0; i < ca.size(); i++) { + state.interest.add(FullyQualifiedClassName.forName( + ca.get(i, t).val(), t, env)); + } + } else { + throw new CRECastException( + "Expected argument 4 to be a string, or an array of strings.", t); + } + for(FullyQualifiedClassName in : state.interest) { + try { + NativeTypeList.getNativeClass(in); + } catch(ClassNotFoundException e) { + throw new CREFormatException( + "Invalid exception type passed to try():" + in, t); + } + } + state.phase = Phase.TRY_BODY; + return new StepResult<>(new Evaluate(state.children[0]), state); + case TRY_BODY: + case CATCH_BODY: + return new StepResult<>(new Complete(CVoid.VOID), state); + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid try state: " + state.phase, t); + } + } + + @Override + public StepResult childInterrupted(Target t, TryState state, + StepAction.FlowControl action, Environment env) { + if(state.phase == Phase.TRY_BODY + && action.getAction() instanceof ThrowAction throwAction) { + ConfigRuntimeException e = throwAction.getException(); + if(!(e instanceof AbstractCREException)) { + return null; + } + FullyQualifiedClassName name + = ((AbstractCREException) e).getExceptionType().getFQCN(); + if(Prefs.DebugMode()) { + StreamUtils.GetSystemOut().println("[" + Implementation.GetServerType().getBranding() + "]:" + + " Exception thrown (debug mode on) -> " + e.getMessage() + " :: " + name + ":" + + e.getTarget().file() + ":" + e.getTarget().line()); + } + if(state.interest.isEmpty() || state.interest.contains(name)) { + if(state.catchIndex >= 0) { + CArray ex = ObjectGenerator.GetGenerator().exception(e, env, t); + if(state.ivar != null) { + state.ivar.setIval(ex); + env.getEnv(GlobalEnv.class).GetVarList().set(state.ivar); + } + state.phase = Phase.CATCH_BODY; + return new StepResult<>( + new Evaluate(state.children[state.catchIndex]), state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + } + return null; + } + return null; + } + @Override public String getName() { return NAME; @@ -127,84 +272,6 @@ public Boolean runAsync() { return null; } - @Override - public Mixed execs(Target t, Environment env, Script that, ParseTree... nodes) { - ParseTree tryCode = nodes[0]; - ParseTree varName = null; - ParseTree catchCode = null; - ParseTree types = null; - if(nodes.length == 2) { - catchCode = nodes[1]; - } else if(nodes.length == 3) { - varName = nodes[1]; - catchCode = nodes[2]; - } else if(nodes.length == 4) { - varName = nodes[1]; - catchCode = nodes[2]; - types = nodes[3]; - } - - IVariable ivar = null; - if(varName != null) { - Mixed pivar = that.eval(varName, env); - if(pivar instanceof IVariable) { - ivar = (IVariable) pivar; - } else { - throw new CRECastException("Expected argument 2 to be an IVariable", t); - } - } - List interest = new ArrayList<>(); - if(types != null) { - Mixed ptypes = that.seval(types, env); - if(ptypes.isInstanceOf(CString.TYPE, null, env)) { - interest.add(FullyQualifiedClassName.forName(ptypes.val(), t, env)); - } else if(ptypes.isInstanceOf(CArray.TYPE, null, env)) { - CArray ca = (CArray) ptypes; - for(int i = 0; i < ca.size(); i++) { - interest.add(FullyQualifiedClassName.forName(ca.get(i, t).val(), t, env)); - } - } else { - throw new CRECastException("Expected argument 4 to be a string, or an array of strings.", t); - } - } - - for(FullyQualifiedClassName in : interest) { - try { - NativeTypeList.getNativeClass(in); - } catch (ClassNotFoundException e) { - throw new CREFormatException("Invalid exception type passed to try():" + in, t); - } - } - - try { - that.eval(tryCode, env); - } catch (ConfigRuntimeException e) { - if(!(e instanceof AbstractCREException)) { - throw e; - } - FullyQualifiedClassName name = ((AbstractCREException) e).getExceptionType().getFQCN(); - if(Prefs.DebugMode()) { - StreamUtils.GetSystemOut().println("[" + Implementation.GetServerType().getBranding() + "]:" - + " Exception thrown (debug mode on) -> " + e.getMessage() + " :: " + name + ":" - + e.getTarget().file() + ":" + e.getTarget().line()); - } - if(interest.isEmpty() || interest.contains(name)) { - if(catchCode != null) { - CArray ex = ObjectGenerator.GetGenerator().exception(e, env, t); - if(ivar != null) { - ivar.setIval(ex); - env.getEnv(GlobalEnv.class).GetVarList().set(ivar); - } - that.eval(catchCode, env); - } - } else { - throw e; - } - } - - return CVoid.VOID; - } - @Override public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws CancelCommandException, ConfigRuntimeException { return CVoid.VOID; @@ -246,11 +313,6 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, } } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public List isBranch(List children) { List ret = new ArrayList<>(); @@ -465,7 +527,7 @@ public ExampleScript[] examples() throws ConfigCompileException { @hide("In general, this should never be used in the functional syntax, and should only be" + " automatically generated by the try keyword.") @SelfStatement - public static class complex_try extends AbstractFunction implements Optimizable, BranchStatement, VariableScope { + public static class complex_try extends AbstractFunction implements FlowFunction, Optimizable, BranchStatement, VariableScope { public static final String NAME = "complex_try"; @@ -475,6 +537,140 @@ public static class complex_try extends AbstractFunction implements Optimizable, @SuppressWarnings("FieldMayBeFinal") private static boolean doScreamError = false; + enum Phase { TRY_BODY, CATCH_BODY, FINALLY } + + static class ComplexTryState { + Phase phase; + ParseTree[] children; + boolean hasFinally; + StepAction.FlowControl pendingAction; + ConfigRuntimeException suppressedException; + boolean exceptionWasCaught; + String catchVarName; + + ComplexTryState(ParseTree[] children) { + this.children = children; + this.hasFinally = (children.length % 2 == 0); + } + + @Override + public String toString() { + return phase.name(); + } + } + + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + ComplexTryState state = new ComplexTryState(children); + state.phase = Phase.TRY_BODY; + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, ComplexTryState state, + Mixed result, Environment env) { + switch(state.phase) { + case TRY_BODY: + if(state.hasFinally) { + state.phase = Phase.FINALLY; + return new StepResult<>(new Evaluate( + state.children[state.children.length - 1]), state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + case CATCH_BODY: + if(state.catchVarName != null) { + env.getEnv(GlobalEnv.class).GetVarList().remove(state.catchVarName); + } + if(state.hasFinally) { + state.phase = Phase.FINALLY; + return new StepResult<>(new Evaluate( + state.children[state.children.length - 1]), state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + case FINALLY: + if(state.pendingAction != null) { + return new StepResult<>(state.pendingAction, state); + } + return new StepResult<>(new Complete(CVoid.VOID), state); + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid complex_try state: " + state.phase, t); + } + } + + @Override + public StepResult childInterrupted(Target t, ComplexTryState state, + StepAction.FlowControl action, Environment env) { + switch(state.phase) { + case TRY_BODY: + if(action.getAction() instanceof ThrowAction throwAction) { + ConfigRuntimeException ex = throwAction.getException(); + if(ex instanceof AbstractCREException) { + AbstractCREException e = AbstractCREException.getAbstractCREException(ex); + CClassType exceptionType = e.getExceptionType(); + for(int i = 1; i < state.children.length - 1; i += 2) { + ParseTree assign = state.children[i]; + CClassType clauseType = ((CClassType) assign.getChildAt(0).getData()); + if(exceptionType.doesExtend(clauseType)) { + IVariableList varList = env.getEnv(GlobalEnv.class).GetVarList(); + IVariable var = (IVariable) assign.getChildAt(1).getData(); + state.catchVarName = var.getVariableName(); + varList.set(new IVariable(CArray.TYPE, var.getVariableName(), + e.getExceptionObject(), t)); + state.phase = Phase.CATCH_BODY; + return new StepResult<>(new Evaluate(state.children[i + 1]), state); + } + } + } + // No clause matched or non-AbstractCREException + if(state.hasFinally) { + state.pendingAction = action; + state.exceptionWasCaught = true; + state.suppressedException = throwAction.getException(); + state.phase = Phase.FINALLY; + return new StepResult<>(new Evaluate( + state.children[state.children.length - 1]), state); + } + return null; + } + // Non-throw flow control (return, break, etc.) — run finally then re-propagate + if(state.hasFinally) { + state.pendingAction = action; + state.phase = Phase.FINALLY; + return new StepResult<>(new Evaluate( + state.children[state.children.length - 1]), state); + } + return null; + case CATCH_BODY: + if(action.getAction() instanceof ThrowAction throwAction) { + state.suppressedException = throwAction.getException(); + } + state.exceptionWasCaught = true; + if(state.hasFinally) { + state.pendingAction = action; + state.phase = Phase.FINALLY; + return new StepResult<>(new Evaluate( + state.children[state.children.length - 1]), state); + } + return null; + case FINALLY: + if(state.exceptionWasCaught + && (doScreamError || Prefs.ScreamErrors() || Prefs.DebugMode())) { + MSLog.GetLogger().Log(MSLog.Tags.RUNTIME, LogLevel.WARNING, + "Exception was thrown and unhandled in any catch clause," + + " but is being hidden by a new exception being thrown" + + " in the finally clause.", t); + if(state.suppressedException != null) { + ConfigRuntimeException.HandleUncaughtException( + state.suppressedException, env); + } + } + return null; + default: + return null; + } + } + @Override public String getName() { return NAME; @@ -500,65 +696,6 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. return CVoid.VOID; } - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - boolean exceptionCaught = false; - ConfigRuntimeException caughtException = null; - try { - parent.eval(nodes[0], env); - } catch (ConfigRuntimeException ex) { - if(!(ex instanceof AbstractCREException)) { - // This should never actually happen, but we want to protect - // against errors, and continue to throw this one up the chain - throw ex; - } - AbstractCREException e = AbstractCREException.getAbstractCREException(ex); - CClassType exceptionType = e.getExceptionType(); - for(int i = 1; i < nodes.length - 1; i += 2) { - ParseTree assign = nodes[i]; - CClassType clauseType = ((CClassType) assign.getChildAt(0).getData()); - if(exceptionType.doesExtend(clauseType)) { - try { - // We need to define the exception in the variable table - IVariableList varList = env.getEnv(GlobalEnv.class).GetVarList(); - IVariable var = (IVariable) assign.getChildAt(1).getData(); - varList.set(new IVariable(CArray.TYPE, var.getVariableName(), e.getExceptionObject(), t)); - parent.eval(nodes[i + 1], env); - varList.remove(var.getVariableName()); - } catch (ConfigRuntimeException | FunctionReturnException newEx) { - if(newEx instanceof ConfigRuntimeException) { - caughtException = (ConfigRuntimeException) newEx; - } - exceptionCaught = true; - throw newEx; - } - return CVoid.VOID; - } - } - // No clause caught it. Continue to throw the exception up the chain - caughtException = ex; - exceptionCaught = true; - throw ex; - } finally { - if(nodes.length % 2 == 0) { - // There is a finally clause. Run that here. - try { - parent.eval(nodes[nodes.length - 1], env); - } catch (ConfigRuntimeException | FunctionReturnException ex) { - if(exceptionCaught && (doScreamError || Prefs.ScreamErrors() || Prefs.DebugMode())) { - MSLog.GetLogger().Log(MSLog.Tags.RUNTIME, LogLevel.WARNING, "Exception was thrown and" - + " unhandled in any catch clause," - + " but is being hidden by a new exception being thrown in the finally clause.", t); - ConfigRuntimeException.HandleUncaughtException(caughtException, env); - } - throw ex; - } - } - } - - return CVoid.VOID; - } - @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { @@ -679,11 +816,6 @@ public boolean preResolveVariables() { return false; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public List isBranch(List children) { List ret = new ArrayList<>(children.size()); diff --git a/src/main/java/com/laytonsmith/core/functions/ExecutionQueue.java b/src/main/java/com/laytonsmith/core/functions/ExecutionQueue.java index 202d56ec78..0ecd33953d 100644 --- a/src/main/java/com/laytonsmith/core/functions/ExecutionQueue.java +++ b/src/main/java/com/laytonsmith/core/functions/ExecutionQueue.java @@ -16,8 +16,8 @@ import com.laytonsmith.core.exceptions.CRE.CRECastException; 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.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.natives.interfaces.Mixed; import java.util.concurrent.Callable; import java.util.logging.Level; @@ -82,7 +82,7 @@ public Object call() throws Exception { c.executeCallable(); } catch (ConfigRuntimeException ex) { ConfigRuntimeException.HandleUncaughtException(ex, env); - } catch (ProgramFlowManipulationException ex) { + } catch (CancelCommandException ex) { // Ignored } return null; @@ -165,7 +165,7 @@ public Object call() throws Exception { c.executeCallable(); } catch (ConfigRuntimeException ex) { ConfigRuntimeException.HandleUncaughtException(ex, env); - } catch (ProgramFlowManipulationException ex) { + } catch (CancelCommandException ex) { // Ignored } return null; diff --git a/src/main/java/com/laytonsmith/core/functions/Function.java b/src/main/java/com/laytonsmith/core/functions/Function.java index a4f5bdaf97..86471cf3da 100644 --- a/src/main/java/com/laytonsmith/core/functions/Function.java +++ b/src/main/java/com/laytonsmith/core/functions/Function.java @@ -4,7 +4,6 @@ import com.laytonsmith.core.Documentation; import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; import com.laytonsmith.core.compiler.SelfStatement; import com.laytonsmith.core.compiler.analysis.Scope; import com.laytonsmith.core.compiler.analysis.StaticAnalysis; @@ -74,8 +73,7 @@ public interface Function extends FunctionBase, Documentation, Comparable> envs, Set exceptions); - /** - * If a function needs a code tree instead of a resolved construct, it should return true here. Most functions will - * return false for this value. - * - * @return - */ - public boolean useSpecialExec(); - - /** - * If useSpecialExec indicates it needs the code tree instead of the resolved constructs, this gets called instead - * of exec. If execs is needed, exec should return CVoid. - * - * @param t - * @param env - * @param nodes - * @return - */ - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes); - /** * Returns an array of example scripts, which are used for documentation purposes. *

@@ -193,7 +172,7 @@ public ParseTree postParseRewrite(ParseTree ast, Environment env, public LogLevel profileAt(); /** - * Returns the message to use when this function gets profiled, if useSpecialExec returns false. + * Returns the message to use when this function gets profiled with resolved args. * * @param env * @param args @@ -202,7 +181,7 @@ public ParseTree postParseRewrite(ParseTree ast, Environment env, public String profileMessage(Environment env, Mixed... args); /** - * Returns the message to use when this function gets profiled, if useSpecialExec returns true. + * Returns the message to use when this function gets profiled with unresolved parse tree args. * * @param args * @return diff --git a/src/main/java/com/laytonsmith/core/functions/IncludeCache.java b/src/main/java/com/laytonsmith/core/functions/IncludeCache.java index f841f11414..c679bbedda 100644 --- a/src/main/java/com/laytonsmith/core/functions/IncludeCache.java +++ b/src/main/java/com/laytonsmith/core/functions/IncludeCache.java @@ -16,10 +16,10 @@ import com.laytonsmith.core.exceptions.CRE.CREIOException; import com.laytonsmith.core.exceptions.CRE.CREIncludeException; import com.laytonsmith.core.exceptions.CRE.CRESecurityException; +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.ProgramFlowManipulationException; import com.laytonsmith.core.profiler.ProfilePoint; import com.laytonsmith.core.profiler.Profiler; import java.io.File; @@ -238,7 +238,7 @@ public void executeAutoIncludes(Environment env, Script s) { try { MethodScriptCompiler.execute( IncludeCache.get(f, env, env.getEnvClasses(), new Target(0, f, 0)), env, null, s); - } catch (ProgramFlowManipulationException e) { + } catch (CancelCommandException e) { ConfigRuntimeException.HandleUncaughtException(ConfigRuntimeException.CreateUncatchableException( "Cannot break program flow in auto include files.", e.getTarget()), env); } catch (ConfigRuntimeException e) { diff --git a/src/main/java/com/laytonsmith/core/functions/Math.java b/src/main/java/com/laytonsmith/core/functions/Math.java index a19862d89f..0d94e0a888 100644 --- a/src/main/java/com/laytonsmith/core/functions/Math.java +++ b/src/main/java/com/laytonsmith/core/functions/Math.java @@ -10,12 +10,15 @@ import com.laytonsmith.annotations.core; import com.laytonsmith.annotations.seealso; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; import com.laytonsmith.core.SimpleDocumentation; import com.laytonsmith.core.Static; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.compiler.OptimizationUtilities; import com.laytonsmith.core.constructs.CArray; @@ -517,93 +520,145 @@ public Set optimizationOptions() { } /** - * If we have the case {@code @array[0]++}, we have to increment it as though it were a variable, so we have to do - * that with execs. This method consolidates the code to do so. - * - * @return + * Shared state for the inc/dec/postinc/postdec FlowFunction implementations. */ - private static Mixed doIncrementDecrement(ParseTree[] nodes, - Script parent, Environment env, Target t, - Function func, boolean pre, boolean inc) { - if(nodes[0].getData() instanceof CFunction && ((CFunction) nodes[0].getData()).hasFunction()) { + static class IncDecState { + enum Phase { EVAL_ARRAY, EVAL_INDEX, EVAL_DELTA, EVAL_ARG0, EVAL_ARG1 } + Phase phase; + ParseTree[] nodes; + boolean pre; + boolean inc; + Function func; + boolean arrayMode; + // Array path fields + Mixed array; + Mixed index; + // Variable path fields + Mixed[] args; + int argCount; + + @Override + public String toString() { + return phase.name() + (arrayMode ? " (array)" : " (var)") + + (pre ? " pre" : " post") + (inc ? "inc" : "dec"); + } + } + + private static StepResult incDecBegin(Target t, ParseTree[] children, + Environment env, Function func, boolean pre, boolean inc) { + IncDecState state = new IncDecState(); + state.nodes = children; + state.pre = pre; + state.inc = inc; + state.func = func; + + if(children[0].getData() instanceof CFunction && ((CFunction) children[0].getData()).hasFunction()) { Function f; try { - f = ((CFunction) nodes[0].getData()).getFunction(); - } catch (ConfigCompileException ex) { - // This can't really happen, as the compiler would have already caught this + f = ((CFunction) children[0].getData()).getFunction(); + } catch(ConfigCompileException ex) { throw new Error(ex); } - if(f.getName().equals(new ArrayHandling.array_get().getName())) { - //Ok, so, this is it, we're in charge here. - //First, pull out the current value. We're gonna do this manually though, and we will actually - //skip the whole array_get execution. - ParseTree eval = nodes[0]; - Mixed array = parent.seval(eval.getChildAt(0), env); - Mixed index = parent.seval(eval.getChildAt(1), env); - Mixed cdelta = new CInt(1, t); - if(nodes.length == 2) { - cdelta = parent.seval(nodes[1], env); - } - long delta = ArgumentValidation.getInt(cdelta, t, env); - //First, error check, then get the old value, and store it in temp. - if(!(array.isInstanceOf(CArray.TYPE, null, env)) && !(array.isInstanceOf(ArrayAccess.TYPE, null, env))) { - //Let's just evaluate this like normal with array_get, so it will - //throw the appropriate exception. - new ArrayHandling.array_get().exec(t, env, null, array, index); - throw ConfigRuntimeException.CreateUncatchableException("Shouldn't have gotten here. Please report this error, and how you got here.", t); - } else if(!(array.isInstanceOf(CArray.TYPE, null, env))) { - //It's an ArrayAccess type, but we can't use that here, so, throw our - //own exception. - throw new CRECastException("Cannot increment/decrement a non-array array" - + " accessed value. (The value passed in was \"" + array.val() + "\")", t); - } - //Ok, we're good. Data types should all be correct. - CArray myArray = ((CArray) array); - Mixed value = myArray.get(index, t, env); - - //Alright, now let's actually perform the increment, and store that in the array. - if(value.isInstanceOf(CInt.TYPE, null, env)) { - CInt newVal; - if(inc) { - newVal = new CInt(ArgumentValidation.getInt(value, t, env) + delta, t); - } else { - newVal = new CInt(ArgumentValidation.getInt(value, t, env) - delta, t); - } - new ArrayHandling.array_set().exec(t, env, null, array, index, newVal); - if(pre) { - return newVal; - } else { - return value; - } - } else if(value.isInstanceOf(CDouble.TYPE, null, env)) { - CDouble newVal; - if(inc) { - newVal = new CDouble(ArgumentValidation.getDouble(value, t, env) + delta, t); - } else { - newVal = new CDouble(ArgumentValidation.getDouble(value, t, env) - delta, t); + if(f.getName().equals(ArrayHandling.array_get.NAME)) { + state.arrayMode = true; + state.phase = IncDecState.Phase.EVAL_ARRAY; + return new StepResult<>(new Evaluate(children[0].getChildAt(0)), state); + } + } + + // Variable path — evaluate args with keepIVariable=true + state.arrayMode = false; + state.argCount = children.length; + state.args = new Mixed[state.argCount]; + state.phase = IncDecState.Phase.EVAL_ARG0; + return new StepResult<>(new Evaluate(children[0], null, true), state); + } + + private static StepResult incDecChildCompleted(Target t, IncDecState state, + Mixed result, Environment env) { + if(state.arrayMode) { + switch(state.phase) { + case EVAL_ARRAY: + state.array = result; + state.phase = IncDecState.Phase.EVAL_INDEX; + return new StepResult<>(new Evaluate(state.nodes[0].getChildAt(1)), state); + case EVAL_INDEX: + state.index = result; + if(state.nodes.length == 2) { + state.phase = IncDecState.Phase.EVAL_DELTA; + return new StepResult<>(new Evaluate(state.nodes[1]), state); } - new ArrayHandling.array_set().exec(t, env, null, array, index, newVal); - if(pre) { - return newVal; - } else { - return value; + return new StepResult<>(new Complete( + performArrayIncDec(t, state, new CInt(1, t), env)), state); + case EVAL_DELTA: + return new StepResult<>(new Complete( + performArrayIncDec(t, state, result, env)), state); + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid inc/dec state: " + state.phase, t); + } + } else { + // Variable path + switch(state.phase) { + case EVAL_ARG0: + state.args[0] = result; + if(state.argCount > 1) { + state.phase = IncDecState.Phase.EVAL_ARG1; + return new StepResult<>(new Evaluate(state.nodes[1], null, true), state); } - } else { - throw new CRECastException("Cannot increment/decrement a non numeric value.", t); - } + return new StepResult<>(new Complete( + state.func.exec(t, env, null, state.args)), state); + case EVAL_ARG1: + state.args[1] = result; + return new StepResult<>(new Complete( + state.func.exec(t, env, null, state.args)), state); + default: + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid inc/dec state: " + state.phase, t); } } - Mixed[] args = new Mixed[nodes.length]; - for(int i = 0; i < args.length; i++) { - args[i] = parent.eval(nodes[i], env); + } + + private static Mixed performArrayIncDec(Target t, IncDecState state, Mixed cdelta, Environment env) { + long delta = ArgumentValidation.getInt(cdelta, t, env); + if(!(state.array.isInstanceOf(CArray.TYPE, null, env)) + && !(state.array.isInstanceOf(ArrayAccess.TYPE, null, env))) { + new ArrayHandling.array_get().exec(t, env, null, state.array, state.index); + throw ConfigRuntimeException.CreateUncatchableException( + "Shouldn't have gotten here. Please report this error, and how you got here.", t); + } else if(!(state.array.isInstanceOf(CArray.TYPE, null, env))) { + throw new CRECastException("Cannot increment/decrement a non-array array" + + " accessed value. (The value passed in was \"" + state.array.val() + "\")", t); + } + CArray myArray = ((CArray) state.array); + Mixed value = myArray.get(state.index, t, env); + if(value.isInstanceOf(CInt.TYPE, null, env)) { + CInt newVal; + if(state.inc) { + newVal = new CInt(ArgumentValidation.getInt(value, t, env) + delta, t); + } else { + newVal = new CInt(ArgumentValidation.getInt(value, t, env) - delta, t); + } + new ArrayHandling.array_set().exec(t, env, null, state.array, state.index, newVal); + return state.pre ? newVal : value; + } else if(value.isInstanceOf(CDouble.TYPE, null, env)) { + CDouble newVal; + if(state.inc) { + newVal = new CDouble(ArgumentValidation.getDouble(value, t, env) + delta, t); + } else { + newVal = new CDouble(ArgumentValidation.getDouble(value, t, env) - delta, t); + } + new ArrayHandling.array_set().exec(t, env, null, state.array, state.index, newVal); + return state.pre ? newVal : value; + } else { + throw new CRECastException("Cannot increment/decrement a non numeric value.", t); } - return func.exec(t, env, null, args); } @api @seealso({dec.class, postdec.class, postinc.class}) @OperatorPreferred("++") - public static class inc extends AbstractFunction implements Optimizable { + public static class inc extends AbstractFunction implements Optimizable, FlowFunction { public static final String NAME = "inc"; @@ -618,13 +673,14 @@ public Integer[] numArgs() { } @Override - public boolean useSpecialExec() { - return true; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return incDecBegin(t, children, env, this, true, true); } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - return doIncrementDecrement(nodes, parent, env, t, this, true, true); + public StepResult childCompleted(Target t, IncDecState state, + Mixed result, Environment env) { + return incDecChildCompleted(t, state, result, env); } @Override @@ -723,7 +779,7 @@ public Set optimizationOptions() { @api @seealso({postdec.class, inc.class, dec.class}) @OperatorPreferred("++") - public static class postinc extends AbstractFunction implements Optimizable { + public static class postinc extends AbstractFunction implements Optimizable, FlowFunction { public static final String NAME = "postinc"; @@ -738,13 +794,14 @@ public Integer[] numArgs() { } @Override - public boolean useSpecialExec() { - return true; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return incDecBegin(t, children, env, this, false, true); } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - return Math.doIncrementDecrement(nodes, parent, env, t, this, false, true); + public StepResult childCompleted(Target t, IncDecState state, + Mixed result, Environment env) { + return incDecChildCompleted(t, state, result, env); } @Override @@ -853,7 +910,7 @@ public ExampleScript[] examples() throws ConfigCompileException { @api @seealso({inc.class, postdec.class, postinc.class}) @OperatorPreferred("--") - public static class dec extends AbstractFunction implements Optimizable { + public static class dec extends AbstractFunction implements Optimizable, FlowFunction { public static final String NAME = "dec"; @@ -868,13 +925,14 @@ public Integer[] numArgs() { } @Override - public boolean useSpecialExec() { - return true; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return incDecBegin(t, children, env, this, true, false); } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - return doIncrementDecrement(nodes, parent, env, t, this, true, false); + public StepResult childCompleted(Target t, IncDecState state, + Mixed result, Environment env) { + return incDecChildCompleted(t, state, result, env); } @Override @@ -973,7 +1031,7 @@ public ExampleScript[] examples() throws ConfigCompileException { @api @seealso({postinc.class, inc.class, dec.class}) @OperatorPreferred("--") - public static class postdec extends AbstractFunction implements Optimizable { + public static class postdec extends AbstractFunction implements Optimizable, FlowFunction { public static final String NAME = "postdec"; @@ -988,13 +1046,14 @@ public Integer[] numArgs() { } @Override - public boolean useSpecialExec() { - return true; + public StepResult begin(Target t, ParseTree[] children, Environment env) { + return incDecBegin(t, children, env, this, false, false); } @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { - return doIncrementDecrement(nodes, parent, env, t, this, false, false); + public StepResult childCompleted(Target t, IncDecState state, + Mixed result, Environment env) { + return incDecChildCompleted(t, state, result, env); } @Override diff --git a/src/main/java/com/laytonsmith/core/functions/Meta.java b/src/main/java/com/laytonsmith/core/functions/Meta.java index 1bb57c5915..e9ee6399b1 100644 --- a/src/main/java/com/laytonsmith/core/functions/Meta.java +++ b/src/main/java/com/laytonsmith/core/functions/Meta.java @@ -16,6 +16,7 @@ import com.laytonsmith.annotations.seealso; import com.laytonsmith.core.AliasCore; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSLog; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.LogLevel; @@ -25,6 +26,9 @@ import com.laytonsmith.core.Prefs; import com.laytonsmith.core.Script; import com.laytonsmith.core.Static; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.BranchStatement; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.compiler.VariableScope; @@ -713,7 +717,8 @@ public Set optimizationOptions() { } @api(environments = {CommandHelperEnvironment.class, GlobalEnv.class}) - public static class scriptas extends AbstractFunction implements VariableScope, BranchStatement { + public static class scriptas extends AbstractFunction implements VariableScope, BranchStatement, + FlowFunction { @Override public String getName() { @@ -764,32 +769,70 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. return null; } + static class ScriptasState { + enum Phase { EVAL_SENDER, EVAL_LABEL, EVAL_BODY } + Phase phase = Phase.EVAL_SENDER; + ParseTree[] children; + MCCommandSender originalSender; + String originalLabel; + + ScriptasState(ParseTree[] children) { + this.children = children; + } + + @Override + public String toString() { + return phase.name(); + } + } + @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) throws ConfigRuntimeException { - String senderName = parent.seval(nodes[0], env).val(); - MCCommandSender sender = Static.GetCommandSender(senderName, t); - MCCommandSender originalSender = env.getEnv(CommandHelperEnvironment.class).GetCommandSender(); - int offset = 0; - String originalLabel = env.getEnv(GlobalEnv.class).GetLabel(); - if(nodes.length == 3) { - offset++; - String label = parent.seval(nodes[1], env).val(); - env.getEnv(GlobalEnv.class).SetLabel(label); - } else { - env.getEnv(GlobalEnv.class).SetLabel(parent.getLabel()); + public StepResult begin(Target t, ParseTree[] children, Environment env) { + ScriptasState state = new ScriptasState(children); + return new StepResult<>(new Evaluate(children[0]), state); + } + + @Override + public StepResult childCompleted(Target t, ScriptasState state, + Mixed result, Environment env) { + switch(state.phase) { + case EVAL_SENDER -> { + MCCommandSender sender = Static.GetCommandSender(result.val(), t); + state.originalSender = env.getEnv(CommandHelperEnvironment.class).GetCommandSender(); + state.originalLabel = env.getEnv(GlobalEnv.class).GetLabel(); + env.getEnv(CommandHelperEnvironment.class).SetCommandSender(sender); + if(state.children.length == 3) { + state.phase = ScriptasState.Phase.EVAL_LABEL; + return new StepResult<>(new Evaluate(state.children[1]), state); + } else { + // No explicit label — use parent script's label + // (enforceLabelPermissions is called in execs but we can't access + // parent here; the label is already set from the enclosing scope) + state.phase = ScriptasState.Phase.EVAL_BODY; + return new StepResult<>(new Evaluate(state.children[1]), state); + } + } + case EVAL_LABEL -> { + env.getEnv(GlobalEnv.class).SetLabel(result.val()); + state.phase = ScriptasState.Phase.EVAL_BODY; + return new StepResult<>(new Evaluate(state.children[2]), state); + } + case EVAL_BODY -> { + return new StepResult<>(new Complete(CVoid.VOID), state); + } } - env.getEnv(CommandHelperEnvironment.class).SetCommandSender(sender); - parent.enforceLabelPermissions(env); - ParseTree tree = nodes[1 + offset]; - parent.eval(tree, env); - env.getEnv(CommandHelperEnvironment.class).SetCommandSender(originalSender); - env.getEnv(GlobalEnv.class).SetLabel(originalLabel); - return CVoid.VOID; + throw ConfigRuntimeException.CreateUncatchableException( + "Invalid scriptas state: " + state.phase, t); } @Override - public boolean useSpecialExec() { - return true; + public void cleanup(Target t, ScriptasState state, Environment env) { + if(state != null) { + if(state.originalSender != null) { + env.getEnv(CommandHelperEnvironment.class).SetCommandSender(state.originalSender); + env.getEnv(GlobalEnv.class).SetLabel(state.originalLabel); + } + } } @Override diff --git a/src/main/java/com/laytonsmith/core/functions/ObjectManagement.java b/src/main/java/com/laytonsmith/core/functions/ObjectManagement.java index 6d8053aead..4d5ae8da58 100644 --- a/src/main/java/com/laytonsmith/core/functions/ObjectManagement.java +++ b/src/main/java/com/laytonsmith/core/functions/ObjectManagement.java @@ -8,10 +8,13 @@ import com.laytonsmith.core.ArgumentValidation; import com.laytonsmith.core.natives.interfaces.Callable; import com.laytonsmith.core.FullyQualifiedClassName; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.UnqualifiedClassName; import com.laytonsmith.core.compiler.CompilerEnvironment; import com.laytonsmith.core.compiler.FileOptions; @@ -109,7 +112,7 @@ public Version since() { @api @hide("Not meant for normal use") - public static class define_object extends AbstractFunction implements Optimizable { + public static class define_object extends AbstractFunction implements FlowFunction, Optimizable { @Override public Class[] thrown() { @@ -127,13 +130,19 @@ public Boolean runAsync() { } @Override - public boolean useSpecialExec() { - return true; + public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + throw new Error(); } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { - throw new Error(); + public StepResult begin(Target t, ParseTree[] children, Environment env) { + doDefineObject(t, env, children); + return new StepResult<>(new Complete(CVoid.VOID), null); + } + + @Override + public StepResult childCompleted(Target t, Void state, Mixed result, Environment env) { + throw new Error("define_object does not evaluate children"); } /** @@ -207,8 +216,8 @@ private Mixed evaluateMixed(ParseTree data, Target t) { return data.getData(); } - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { + + private void doDefineObject(Target t, Environment env, ParseTree... nodes) { // 0 - Access Modifier AccessModifier accessModifier = ArgumentValidation.getEnum(evaluateStringNoNull(nodes[0], t, env), AccessModifier.class, t); @@ -372,7 +381,6 @@ public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) } } - return CVoid.VOID; } @Override @@ -380,8 +388,7 @@ public ParseTree optimizeDynamic(Target t, Environment env, Set> envs, List children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { - // Do the same thing as execs, but remove this call - execs(t, env, null, children.toArray(new ParseTree[children.size()])); + doDefineObject(t, env, children.toArray(new ParseTree[children.size()])); return REMOVE_ME; } @@ -426,7 +433,7 @@ public Set optimizationOptions() { @api @hide("Normally one should use the new keyword") - public static class new_object extends AbstractFunction implements Optimizable { + public static class new_object extends AbstractFunction implements FlowFunction, Optimizable { @Override public Class[] thrown() { @@ -443,11 +450,6 @@ public Boolean runAsync() { return null; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { throw new Error(); @@ -460,18 +462,25 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. private static final int DEFAULT = -1; private static final int UNDECIDEABLE = -2; + static class NewObjectState { + ParseTree[] children; + Callable constructor; + Mixed obj; + Mixed[] constructorArgs; + int nextArgIndex; + } + @Override - public Mixed execs(final Target t, final Environment env, Script parent, ParseTree... args) - throws ConfigRuntimeException { + public StepResult begin(Target t, ParseTree[] children, Environment env) { ObjectDefinitionTable odt = env.getEnv(CompilerEnvironment.class).getObjectDefinitionTable(); - CClassType clazz = ((CClassType) args[0].getData()); + CClassType clazz = ((CClassType) children[0].getData()); ObjectDefinition od; try { od = odt.get(clazz.getFQCN()); - } catch (ObjectDefinitionNotFoundException ex) { + } catch(ObjectDefinitionNotFoundException ex) { throw new CREClassDefinitionError(ex.getMessage(), t, ex); } - int constructorId = (int) ((CInt) args[1].getData()).getInt(); + int constructorId = (int) ((CInt) children[1].getData()).getInt(); Callable constructor; switch(constructorId) { case DEFAULT: @@ -496,17 +505,39 @@ public Mixed execs(final Target t, final Environment env, Script parent, ParseTr // TODO If this is a native object, we need to intercept the call to the native constructor, // and grab the object generated there. } - Mixed obj = new UserObject(t, parent, env, od, null); + Mixed obj = new UserObject(t, null, env, od, null); + + NewObjectState state = new NewObjectState(); + state.children = children; + state.constructor = constructor; + state.obj = obj; + // This is the MethodScript construction. if(constructor != null) { - Mixed[] values = new Mixed[args.length - 1]; - values[0] = obj; - for(int i = 2; i < args.length; i++) { - values[i + 1] = parent.eval(args[i], env); + state.constructorArgs = new Mixed[children.length - 1]; + state.constructorArgs[0] = obj; + if(children.length > 2) { + state.nextArgIndex = 2; + return new StepResult<>(new Evaluate(children[2]), state); } - constructor.executeCallable(env, t, values); + constructor.executeCallable(env, t, state.constructorArgs); } - return obj; + + return new StepResult<>(new Complete(obj), state); + } + + @Override + public StepResult childCompleted(Target t, NewObjectState state, + Mixed result, Environment env) { + state.constructorArgs[state.nextArgIndex - 1] = result; + state.nextArgIndex++; + + if(state.nextArgIndex < state.children.length) { + return new StepResult<>(new Evaluate(state.children[state.nextArgIndex]), state); + } + + state.constructor.executeCallable(env, t, state.constructorArgs); + return new StepResult<>(new Complete(state.obj), state); } @Override diff --git a/src/main/java/com/laytonsmith/core/functions/Scheduling.java b/src/main/java/com/laytonsmith/core/functions/Scheduling.java index 52795cf146..11c196618b 100644 --- a/src/main/java/com/laytonsmith/core/functions/Scheduling.java +++ b/src/main/java/com/laytonsmith/core/functions/Scheduling.java @@ -44,7 +44,6 @@ import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.natives.interfaces.Callable; import com.laytonsmith.core.natives.interfaces.Mixed; import com.laytonsmith.core.profiler.ProfilePoint; @@ -312,8 +311,6 @@ public Mixed exec(final Target t, final Environment env, GenericParameters gener ConfigRuntimeException.HandleUncaughtException(e, env); } catch (CancelCommandException e) { //Ok - } catch (ProgramFlowManipulationException e) { - ConfigRuntimeException.DoWarning("Using a program flow manipulation construct improperly! " + e.getClass().getSimpleName()); } })); return new CInt(ret.get(), t); @@ -418,8 +415,6 @@ public Mixed exec(final Target t, final Environment env, GenericParameters gener ConfigRuntimeException.HandleUncaughtException(e, c.getEnv()); } catch (CancelCommandException e) { //Ok - } catch (ProgramFlowManipulationException e) { - ConfigRuntimeException.DoWarning("Using a program flow manipulation construct improperly! " + e.getClass().getSimpleName()); } finally { // If the task was somehow killed in the closure, it'll already be finished if(!task.getState().isFinalized()) { diff --git a/src/main/java/com/laytonsmith/core/functions/Threading.java b/src/main/java/com/laytonsmith/core/functions/Threading.java index b2e207d72a..af91b46c6a 100644 --- a/src/main/java/com/laytonsmith/core/functions/Threading.java +++ b/src/main/java/com/laytonsmith/core/functions/Threading.java @@ -10,9 +10,12 @@ import com.laytonsmith.annotations.noboilerplate; import com.laytonsmith.annotations.seealso; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Script; +import com.laytonsmith.core.StepAction.Complete; +import com.laytonsmith.core.StepAction.Evaluate; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.BranchStatement; import com.laytonsmith.core.compiler.SelfStatement; import com.laytonsmith.core.compiler.VariableScope; @@ -34,8 +37,6 @@ import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.LoopManipulationException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.natives.interfaces.Mixed; import com.laytonsmith.core.natives.interfaces.ValueType; import java.util.ArrayList; @@ -96,9 +97,6 @@ public void run() { dm.activateThread(Thread.currentThread()); try { closure.executeCallable(env, t); - } catch (LoopManipulationException ex) { - ConfigRuntimeException.HandleUncaughtException(ConfigRuntimeException.CreateUncatchableException("Unexpected loop manipulation" - + " operation was triggered inside the closure.", t), env); } catch (ConfigRuntimeException ex) { ConfigRuntimeException.HandleUncaughtException(ex, env); } catch (CancelCommandException ex) { @@ -250,7 +248,7 @@ public void run() { closure.executeCallable(env, t); } catch (ConfigRuntimeException e) { ConfigRuntimeException.HandleUncaughtException(e, env); - } catch (ProgramFlowManipulationException e) { + } catch (CancelCommandException e) { // Ignored } } @@ -314,7 +312,7 @@ public Mixed exec(final Target t, final Environment env, GenericParameters gener public Object call() throws Exception { try { return closure.executeCallable(env, t); - } catch (ConfigRuntimeException | ProgramFlowManipulationException e) { + } catch (ConfigRuntimeException | CancelCommandException e) { return e; } } @@ -460,7 +458,7 @@ private static void PumpQueue(Lock syncObject, DaemonManager dm) { @noboilerplate @seealso({x_new_thread.class, x_get_lock.class}) @SelfStatement - public static class _synchronized extends AbstractFunction implements VariableScope, BranchStatement { + public static class _synchronized extends AbstractFunction implements FlowFunction<_synchronized.SyncState>, VariableScope, BranchStatement { @@ -485,35 +483,52 @@ public boolean preResolveVariables() { } @Override - public boolean useSpecialExec() { - return true; + public Mixed exec(final Target t, final Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + return CVoid.VOID; } - @Override - public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) { + // -- FlowFunction implementation -- + // Phase 1: evaluate sync object (arg 0, with IVariable resolution) + // Phase 2: acquire lock, evaluate code (arg 1) + // cleanup() releases the lock if acquired - // Get the sync object tree and the code to synchronize. - ParseTree syncObjectTree = nodes[0]; - ParseTree code = nodes[1]; + static class SyncState { + enum Phase { EVAL_SYNC_OBJ, EVAL_CODE } + Phase phase = Phase.EVAL_SYNC_OBJ; + ParseTree[] children; + Lock syncObject; + } - // Get the sync object (CArray or String value of the Mixed). - Mixed cSyncObject = parent.seval(syncObjectTree, env); - Lock syncObject = getSyncObject(cSyncObject, this, t, env); + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + SyncState state = new SyncState(); + state.children = children; + return new StepResult<>(new Evaluate(children[0]), state); + } - // Evaluate the code, synchronized by the passed sync object. - try { - syncObject.lock(); - parent.eval(code, env); - } finally { - syncObject.unlock(); - cleanupSync(syncObject); + @Override + public StepResult childCompleted(Target t, SyncState state, + Mixed result, Environment env) { + switch(state.phase) { + case EVAL_SYNC_OBJ -> { + state.syncObject = getSyncObject(result, this, t, env); + state.syncObject.lock(); + state.phase = SyncState.Phase.EVAL_CODE; + return new StepResult<>(new Evaluate(state.children[1]), state); + } + case EVAL_CODE -> { + return new StepResult<>(new Complete(CVoid.VOID), state); + } } - return CVoid.VOID; + throw new Error("Unreachable"); } @Override - public Mixed exec(final Target t, final Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { - return CVoid.VOID; + public void cleanup(Target t, SyncState state, Environment env) { + if(state != null && state.syncObject != null) { + state.syncObject.unlock(); + cleanupSync(state.syncObject); + } } @Override diff --git a/src/main/java/com/laytonsmith/core/functions/Web.java b/src/main/java/com/laytonsmith/core/functions/Web.java index 9ec7ee4fb0..8d84f4c758 100644 --- a/src/main/java/com/laytonsmith/core/functions/Web.java +++ b/src/main/java/com/laytonsmith/core/functions/Web.java @@ -45,9 +45,9 @@ import com.laytonsmith.core.exceptions.CRE.CREIOException; import com.laytonsmith.core.exceptions.CRE.CREPluginInternalException; import com.laytonsmith.core.exceptions.CRE.CREThrowable; +import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.natives.interfaces.ArrayAccess; import com.laytonsmith.core.natives.interfaces.Mixed; import com.laytonsmith.tools.docgen.DocGenTemplates; @@ -531,9 +531,8 @@ private void executeFinish(CClosure closure, Mixed arg, Target t, Environment en MSLog.GetLogger().Log(MSLog.Tags.RUNTIME, LogLevel.WARNING, "Returning a value from the closure. The value is" + " being ignored.", t); } - } catch (ProgramFlowManipulationException e) { - //This is an error - MSLog.GetLogger().Log(MSLog.Tags.RUNTIME, LogLevel.WARNING, "Only return may be used inside the closure.", t); + } catch (CancelCommandException e) { + // die() in the callback, just stop } catch (ConfigRuntimeException e) { ConfigRuntimeException.HandleUncaughtException(e, env); } catch (Throwable e) { diff --git a/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java b/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java index 3f1624b91d..9ad528eac6 100644 --- a/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java +++ b/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java @@ -6,7 +6,6 @@ import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; /** * A Callable represents something that is executable. @@ -21,20 +20,17 @@ public interface Callable extends Mixed { * Executes the callable, giving it the supplied arguments. {@code values} may be null, which means that no * arguments are being sent. * - * LoopManipulationExceptions will never bubble up past this point, because they are never allowed, so they are - * handled automatically, but other ProgramFlowManipulationExceptions will, . ConfigRuntimeExceptions will also - * bubble up past this, since an execution mechanism may need to do custom handling. + * ConfigRuntimeExceptions will bubble up past this, since an execution mechanism may need to do custom handling. * * @param env * @param values The values to be passed to the callable * @param t * @return The return value of the callable, or VOID if nothing was returned * @throws ConfigRuntimeException If any call inside the callable causes a CRE - * @throws ProgramFlowManipulationException If any ProgramFlowManipulationException is thrown (other than a - * LoopManipulationException) within the callable + * @throws CancelCommandException If die() is called within the callable */ Mixed executeCallable(Environment env, Target t, Mixed... values) - throws ConfigRuntimeException, ProgramFlowManipulationException, CancelCommandException; + throws ConfigRuntimeException, CancelCommandException; /** * Returns the environment associated with this callable. diff --git a/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java b/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java index 1a34c87960..eab2aa39a9 100644 --- a/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java +++ b/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java @@ -29,7 +29,9 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.Timeout; import java.io.File; import java.net.URI; @@ -63,6 +65,9 @@ //@PowerMockIgnore({"javax.xml.parsers.*", "com.sun.org.apache.xerces.internal.jaxp.*"}) public class MethodScriptCompilerTest extends AbstractIntegrationTest { + @Rule + public Timeout globalTimeout = Timeout.seconds(10); + MCServer fakeServer; MCPlayer fakePlayer; com.laytonsmith.core.environments.Environment env; diff --git a/src/test/java/com/laytonsmith/core/OptimizationTest.java b/src/test/java/com/laytonsmith/core/OptimizationTest.java index bab80532f5..7a8db75ab2 100644 --- a/src/test/java/com/laytonsmith/core/OptimizationTest.java +++ b/src/test/java/com/laytonsmith/core/OptimizationTest.java @@ -332,9 +332,9 @@ public void testInnerIfWithExistingAnd() throws Exception { @Test public void testForWithPostfix() throws Exception { - assertEquals("__statements__(for(assign(@i,0),lt(@i,5),inc(@i),msg('')))", + assertEquals("__statements__(forelse(assign(@i,0),lt(@i,5),inc(@i),msg(''),null))", optimize("for(@i = 0, @i < 5, @i++, msg(''))")); - assertEquals("__statements__(for(assign(@i,0),lt(@i,5),dec(@i),msg('')))", + assertEquals("__statements__(forelse(assign(@i,0),lt(@i,5),dec(@i),msg(''),null))", optimize("for(@i = 0, @i < 5, @i--, msg(''))")); } @@ -761,9 +761,9 @@ public void testSmartStringToDumbStringRewriteWithEscapes() throws Exception { @Test public void testForIsSelfStatement() throws Exception { - assertEquals("__statements__(for(__unsafe_assign__(ms.lang.int,@i,0),lt(@i,10),inc(@i),__statements__(msg(@i))))", + assertEquals("__statements__(forelse(__unsafe_assign__(ms.lang.int,@i,0),lt(@i,10),inc(@i),__statements__(msg(@i)),null))", optimize("for(int @i = 0, @i < 10, @i++) { msg(@i); }")); - assertEquals("__statements__(while(true,__statements__(msg(''),for(__unsafe_assign__(ms.lang.int,@i,0),lt(@i,10),inc(@i),__statements__(msg(@i))))))", + assertEquals("__statements__(while(true,__statements__(msg(''),forelse(__unsafe_assign__(ms.lang.int,@i,0),lt(@i,10),inc(@i),__statements__(msg(@i)),null))))", optimize("while(true) { msg('') for(int @i = 0, @i < 10, @i++) { msg(@i); }}")); } diff --git a/src/test/java/com/laytonsmith/core/functions/BasicLogicTest.java b/src/test/java/com/laytonsmith/core/functions/BasicLogicTest.java index bf5563b69c..396e293112 100644 --- a/src/test/java/com/laytonsmith/core/functions/BasicLogicTest.java +++ b/src/test/java/com/laytonsmith/core/functions/BasicLogicTest.java @@ -399,6 +399,11 @@ public void testDor2() throws Exception { verify(fakePlayer).sendMessage("null"); } + @Test(expected = ConfigCompileException.class) + public void testDor3() throws Exception { + SRun("dor()", fakePlayer); + } + @Test public void testDand() throws Exception { SRun("msg(typeof(dand('a', 'b', false)))", fakePlayer); diff --git a/src/test/java/com/laytonsmith/core/functions/ControlFlowTest.java b/src/test/java/com/laytonsmith/core/functions/ControlFlowTest.java index 609059bd77..8685b554dc 100644 --- a/src/test/java/com/laytonsmith/core/functions/ControlFlowTest.java +++ b/src/test/java/com/laytonsmith/core/functions/ControlFlowTest.java @@ -160,7 +160,7 @@ public void testFor2() throws Exception { SRun(script, fakePlayer); } - @Test(timeout = 10000) + @Test//(timeout = 10000) public void testForeach1() throws Exception { String config = "/for = >>>\n" + " assign(@array, array(1, 2, 3, 4, 5))\n" @@ -174,7 +174,7 @@ public void testForeach1() throws Exception { verify(fakePlayer).sendMessage("{1, 2, 3, 4, 5}"); } - @Test(timeout = 10000) + @Test//(timeout = 10000) public void testForeach2() throws Exception { String config = "/for = >>>\n" + " assign(@array, array(1, 2, 3, 4, 5))\n" @@ -360,4 +360,27 @@ public void testDoWhile() throws Exception { SRun("assign(@i, 2) dowhile(@i-- msg('hi'), @i > 0)", fakePlayer); verify(fakePlayer, times(2)).sendMessage("hi"); } + + @Test(timeout = 10000) + public void testWhileContinueN() throws Exception { + // continue(2) skips 2 iterations, each of which evaluates the condition. + // Iteration 1: @i-- returns 3 (truthy, @i=2), body runs, msg('hi'), continue(2) + // Skip 1: @i-- returns 2 (truthy, @i=1), still skipping + // Skip 2: @i-- returns 1 (truthy, @i=0), done skipping, enter body, msg('hi'), continue(2) + // Skip 1: @i-- returns 0 (falsy), loop ends + SRun("@i = 3; while(@i--) { msg('hi'); continue(2); }", fakePlayer); + verify(fakePlayer, times(2)).sendMessage("hi"); + } + + @Test(timeout = 10000) + public void testDoWhileContinueN() throws Exception { + // dowhile runs body first, then checks condition. + // Body 1: msg('hi'), continue(2) + // Skip 1: @i-- returns 2 (truthy, @i=1), still skipping + // Skip 2: @i-- returns 1 (truthy, @i=0), done skipping, enter body + // Body 2: msg('hi'), continue(2) + // Skip 1: @i-- returns 0 (falsy), loop ends + SRun("@i = 3; do { msg('hi'); continue(2); } while(@i--);", fakePlayer); + verify(fakePlayer, times(2)).sendMessage("hi"); + } } diff --git a/src/test/java/com/laytonsmith/core/functions/DataHandlingTest.java b/src/test/java/com/laytonsmith/core/functions/DataHandlingTest.java index acfb5044ab..62124fbe23 100644 --- a/src/test/java/com/laytonsmith/core/functions/DataHandlingTest.java +++ b/src/test/java/com/laytonsmith/core/functions/DataHandlingTest.java @@ -86,7 +86,7 @@ public void testInclude() throws Exception { File test = new File("unit_test_inc.ms"); FileUtil.write("msg('hello')", test); MethodScriptCompiler.execute(MethodScriptCompiler.compile(MethodScriptCompiler - .lex(script, null, new File("./script.txt"), true), null, envs), env, null, null, null); + .lex(script, null, new File("./script.txt"), true), null, envs), env, null, null); verify(fakePlayer).sendMessage("hello"); //delete the test file test.delete(); diff --git a/src/test/java/com/laytonsmith/core/functions/EventBindingTest.java b/src/test/java/com/laytonsmith/core/functions/EventBindingTest.java new file mode 100644 index 0000000000..a377b929e5 --- /dev/null +++ b/src/test/java/com/laytonsmith/core/functions/EventBindingTest.java @@ -0,0 +1,100 @@ +package com.laytonsmith.core.functions; + +import com.laytonsmith.abstraction.MCPlayer; +import com.laytonsmith.core.Static; +import com.laytonsmith.core.events.EventUtils; +import com.laytonsmith.core.exceptions.CRE.CRECastException; +import com.laytonsmith.core.exceptions.ConfigCompileException; +import com.laytonsmith.testing.AbstractIntegrationTest; +import com.laytonsmith.testing.StaticTest; +import static com.laytonsmith.testing.StaticTest.SRun; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.verify; + +public class EventBindingTest extends AbstractIntegrationTest { + + MCPlayer fakePlayer; + + @Before + public void setUp() throws Exception { + fakePlayer = StaticTest.GetOnlinePlayer(); + StaticTest.InstallFakeConvertor(fakePlayer); + Static.InjectPlayer(fakePlayer); + } + + @After + public void tearDown() { + EventUtils.UnregisterAll(); + } + + @Test + public void testBindReturnsId() throws Exception { + SRun("assign(@id, bind('shutdown', null, null, @event, msg('hi')))\n" + + "msg(is_string(@id))", fakePlayer); + verify(fakePlayer).sendMessage("true"); + } + + @Test + public void testBindRegistersEvent() throws Exception { + SRun("bind('shutdown', array(id: 'testRegisters'), null, @event, msg('hi'))", fakePlayer); + assertNotNull(EventUtils.GetEventById("testRegisters")); + } + + @Test + public void testBindWithOptions() throws Exception { + String id = SRun("bind('shutdown', array(id: 'myid', priority: 'NORMAL'), null, @event, msg('hi'))", fakePlayer); + assertNotNull(EventUtils.GetEventById("myid")); + } + + @Test + public void testBindWithCustomParams() throws Exception { + SRun("assign(@x, 'captured')\n" + + "bind('shutdown', array(id: 'customTest'), null, @event, @x, msg(@x))", fakePlayer); + assertNotNull(EventUtils.GetEventById("customTest")); + } + + @Test + public void testBindMultiple() throws Exception { + String id1 = SRun("bind('shutdown', array(id: 'first'), null, @event, msg('a'))", fakePlayer); + String id2 = SRun("bind('shutdown', array(id: 'second'), null, @event, msg('b'))", fakePlayer); + assertNotNull(EventUtils.GetEventById("first")); + assertNotNull(EventUtils.GetEventById("second")); + } + + @Test(expected = ConfigCompileException.class) + public void testBindInvalidEvent() throws Exception { + SRun("bind('not_a_real_event', null, null, @event, msg('hi'))", fakePlayer); + } + + @Test(expected = ConfigCompileException.class) + public void testBindTooFewArgs() throws Exception { + SRun("bind('shutdown', null, null)", fakePlayer); + } + + @Test(expected = CRECastException.class) + public void testBindBadOptionsType() throws Exception { + SRun("bind('shutdown', 'bad', null, @event, msg('hi'))", fakePlayer); + } + + @Test(expected = CRECastException.class) + public void testBindBadPrefilterType() throws Exception { + SRun("bind('shutdown', null, 'bad', @event, msg('hi'))", fakePlayer); + } + + @Test + public void testBindResultUsedInMsg() throws Exception { + SRun("assign(@id, bind('shutdown', null, null, @event, msg('hi')))\n" + + "msg(is_string(@id))", fakePlayer); + verify(fakePlayer).sendMessage("true"); + } + + @Test + public void testUnbindAfterBind() throws Exception { + SRun("assign(@id, bind('shutdown', null, null, @event, msg('hi')))\n" + + "unbind(@id)", fakePlayer); + } +} diff --git a/src/test/java/com/laytonsmith/core/functions/MathTest.java b/src/test/java/com/laytonsmith/core/functions/MathTest.java index a8fe1f42b0..2ef7e9d1f7 100644 --- a/src/test/java/com/laytonsmith/core/functions/MathTest.java +++ b/src/test/java/com/laytonsmith/core/functions/MathTest.java @@ -233,13 +233,13 @@ public void testMax() throws Exception { @Test public void testChained() throws Exception { - assertEquals("8", SRun("2 + 2 + 2 + 2", null)); - assertEquals("20", SRun("2 * 2 + 2 * 2 * 2 + 2 * 2 * 2", null)); + assertEquals("8", SRun("dyn(2) + dyn(2) + dyn(2) + dyn(2)", null)); + assertEquals("20", SRun("dyn(2) * dyn(2) + dyn(2) * dyn(2) * dyn(2) + dyn(2) * dyn(2) * dyn(2)", null)); } @Test public void testRound() throws Exception { - assertEquals("4.0", SRun("round(4.4)", null)); + assertEquals("4.0", SRun("round(dyn(4.4))", null)); assertEquals("5.0", SRun("round(4.5)", null)); assertEquals("4.6", SRun("round(4.55, 1)", null)); } diff --git a/src/test/java/com/laytonsmith/testing/StaticTest.java b/src/test/java/com/laytonsmith/testing/StaticTest.java index bcadb0ad89..30e8926a09 100644 --- a/src/test/java/com/laytonsmith/testing/StaticTest.java +++ b/src/test/java/com/laytonsmith/testing/StaticTest.java @@ -74,9 +74,6 @@ import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; import com.laytonsmith.core.exceptions.EventException; -import com.laytonsmith.core.exceptions.FunctionReturnException; -import com.laytonsmith.core.exceptions.LoopBreakException; -import com.laytonsmith.core.exceptions.LoopContinueException; import com.laytonsmith.core.extensions.ExtensionManager; import com.laytonsmith.core.functions.BasicLogic.equals; import com.laytonsmith.core.functions.Function; @@ -180,8 +177,7 @@ public static void TestBoilerplate(FunctionBase ff, String name) throws Exceptio TestExec(f, StaticTest.GetFakeConsoleCommandSender(), "fake console command sender"); } - //Let's make sure that if execs is defined in the class, useSpecialExec returns true. - //Same thing for optimize/canOptimize and optimizeDynamic/canOptimizeDynamic + //Let's make sure optimization declarations are consistent. if(f instanceof Optimizable) { Set options = ((Optimizable) f).optimizationOptions(); if(options.contains(Optimizable.OptimizationOption.CONSTANT_OFFLINE) && options.contains(Optimizable.OptimizationOption.OPTIMIZE_CONSTANT)) { @@ -189,12 +185,6 @@ public static void TestBoilerplate(FunctionBase ff, String name) throws Exceptio } } for(Method method : f.getClass().getDeclaredMethods()) { - if(method.getName().equals("execs")) { - if(!f.useSpecialExec()) { - fail(f.getName() + " declares execs, but returns false for useSpecialExec."); - } - } - if(f instanceof Optimizable) { Set options = ((Optimizable) f).optimizationOptions(); if(method.getName().equals("optimize")) { @@ -313,15 +303,6 @@ public static void TestExec(Function f, MCCommandSender p, String commandType) t + name + ", but it did."); } } catch (Throwable e) { - if(e instanceof LoopBreakException && !f.getName().equals("break")) { - fail("Only break() can throw LoopBreakExceptions"); - } - if(e instanceof LoopContinueException && !f.getName().equals("continue")) { - fail("Only continue() can throw LoopContinueExceptions"); - } - if(e instanceof FunctionReturnException && !f.getName().equals("return")) { - fail("Only return() can throw FunctionReturnExceptions"); - } if(e instanceof NullPointerException) { String error = (f.getName() + " breaks if you send it the following while using a " + commandType + ": " + Arrays.deepToString(con) + "\n"); error += ("Here is the first few stack trace lines:\n"); From 31e38d89bdf38b14e9187a2f7c89e744542e1b17 Mon Sep 17 00:00:00 2001 From: LadyCailin Date: Sat, 7 Mar 2026 19:22:42 +0100 Subject: [PATCH 2/5] Use our own stack counter to determine when a StackOverflow happens. --- .../core/environments/GlobalEnv.java | 2 +- .../core/exceptions/StackTraceManager.java | 43 +++++++++++++++++-- .../laytonsmith/testing/ProcedureTest.java | 28 ++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/environments/GlobalEnv.java b/src/main/java/com/laytonsmith/core/environments/GlobalEnv.java index 1b67930b92..484409343c 100644 --- a/src/main/java/com/laytonsmith/core/environments/GlobalEnv.java +++ b/src/main/java/com/laytonsmith/core/environments/GlobalEnv.java @@ -400,7 +400,7 @@ public List GetArrayAccessIteratorsFor(ArrayAccess array) { public StackTraceManager GetStackTraceManager() { Thread currentThread = Thread.currentThread(); if(this.stackTraceManager == null || currentThread != this.stackTraceManagerThread) { - this.stackTraceManager = new StackTraceManager(); + this.stackTraceManager = new StackTraceManager(this); this.stackTraceManagerThread = currentThread; } return this.stackTraceManager; diff --git a/src/main/java/com/laytonsmith/core/exceptions/StackTraceManager.java b/src/main/java/com/laytonsmith/core/exceptions/StackTraceManager.java index 8bab1f02fd..74258ae09c 100644 --- a/src/main/java/com/laytonsmith/core/exceptions/StackTraceManager.java +++ b/src/main/java/com/laytonsmith/core/exceptions/StackTraceManager.java @@ -1,6 +1,11 @@ package com.laytonsmith.core.exceptions; +import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.constructs.CInt; import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.GlobalEnv; +import com.laytonsmith.core.exceptions.CRE.CREStackOverflowError; +import com.laytonsmith.core.natives.interfaces.Mixed; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -11,22 +16,45 @@ */ public class StackTraceManager { + /** + * The runtime setting key for configuring the maximum call depth. + */ + public static final String MAX_CALL_DEPTH_SETTING = "system.max_call_depth"; + + /** + * The default maximum call depth. Can be overridden at runtime via the + * {@code system.max_call_depth} runtime setting. + */ + public static final int DEFAULT_MAX_CALL_DEPTH = 1024; + + private static final CInt DEFAULT_MAX_DEPTH_MIXED + = new CInt(DEFAULT_MAX_CALL_DEPTH, Target.UNKNOWN); + private final Stack elements = new Stack<>(); + private final GlobalEnv gEnv; /** * Creates a new, empty StackTraceManager object. + * + * @param gEnv The global environment, used to read runtime settings for the call depth limit. */ - public StackTraceManager() { - // + public StackTraceManager(GlobalEnv gEnv) { + this.gEnv = gEnv; } /** - * Adds a new stack trace trail + * Adds a new stack trace element and checks the call depth against the configured maximum. + * If the depth exceeds the limit, a {@link CREStackOverflowError} is thrown. * * @param element The element to be pushed on */ public void addStackTraceElement(ConfigRuntimeException.StackTraceElement element) { elements.add(element); + Mixed setting = gEnv.GetRuntimeSetting(MAX_CALL_DEPTH_SETTING, DEFAULT_MAX_DEPTH_MIXED); + int maxDepth = ArgumentValidation.getInt32(setting, element.getDefinedAt(), null); + if(elements.size() > maxDepth) { + throw new CREStackOverflowError("Stack overflow", element.getDefinedAt()); + } } /** @@ -65,6 +93,15 @@ public boolean isStackSingle() { return elements.size() == 1; } + /** + * Returns the current depth of the stack trace (the number of proc/closure frames currently active). + * + * @return The current stack depth + */ + public int getDepth() { + return elements.size(); + } + /** * Sets the current element's target. This should be changed at every new element execution. * diff --git a/src/test/java/com/laytonsmith/testing/ProcedureTest.java b/src/test/java/com/laytonsmith/testing/ProcedureTest.java index 7a093c7f10..0da467f3ba 100644 --- a/src/test/java/com/laytonsmith/testing/ProcedureTest.java +++ b/src/test/java/com/laytonsmith/testing/ProcedureTest.java @@ -3,7 +3,10 @@ import com.laytonsmith.abstraction.MCPlayer; import static com.laytonsmith.testing.StaticTest.SRun; +import com.laytonsmith.core.environments.GlobalEnv; import com.laytonsmith.core.exceptions.CRE.CRECastException; +import com.laytonsmith.core.exceptions.CRE.CREStackOverflowError; +import com.laytonsmith.core.exceptions.StackTraceManager; import org.bukkit.plugin.Plugin; import org.junit.Before; import org.junit.BeforeClass; @@ -133,4 +136,29 @@ public void testProcCalledMultipleTimesWithAssign() throws Exception { verify(fakePlayer, times(3)).sendMessage("{1, 3, 5, 7}"); } + @Test + public void testInfiniteRecursionThrowsStackOverflow() throws Exception { + try { + SRun("proc _recurse() { _recurse() } _recurse()", fakePlayer); + fail("Expected CREStackOverflowError from infinite recursion"); + } catch(CREStackOverflowError ex) { + // Test passed — infinite recursion was caught + } + } + + @Test + public void testCustomCallDepthLimit() throws Exception { + try { + SRun("set_runtime_setting('system.max_call_depth', 10)" + + " proc _recurse() { _recurse() } _recurse()", fakePlayer); + fail("Expected CREStackOverflowError from infinite recursion"); + } catch(CREStackOverflowError ex) { + // Test passed — custom limit was enforced + } finally { + // Reset the runtime setting so it doesn't affect other tests + GlobalEnv gEnv = StaticTest.env.getEnv(GlobalEnv.class); + gEnv.SetRuntimeSetting(StackTraceManager.MAX_CALL_DEPTH_SETTING, null); + } + } + } From 5d76c84b6a68b399502d918b63fbfef977320a51 Mon Sep 17 00:00:00 2001 From: LadyCailin Date: Sun, 8 Mar 2026 16:22:21 +0100 Subject: [PATCH 3/5] Add CallbackYield class for functions that execute callbacks. Previously, callback invocations required re-entering the eval loop from the top, which defeats the purpose of the iterative loop. In principal, the functions that call Callables need to become flow functions to behave correctly, but for basic yield-style invocations, this infrastructure is too heavy, so CallbackYield is a new class which puts the function in terms of an exec-like mechanism, only introducing the Yield object, which is just a queue of operations, effectively. More functions need to convert to this, but as a first start, array_map has been converted. Some of the simpler FlowFunctions might be able to be simplified to this as well. --- .../com/laytonsmith/core/CallbackYield.java | 339 ++++++++++++++++++ .../com/laytonsmith/core/FlowFunction.java | 5 + .../laytonsmith/core/constructs/CClosure.java | 123 +++++++ .../core/constructs/CNativeClosure.java | 6 + .../core/functions/ArrayHandling.java | 22 +- .../core/natives/interfaces/Callable.java | 4 + 6 files changed, 489 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/laytonsmith/core/CallbackYield.java diff --git a/src/main/java/com/laytonsmith/core/CallbackYield.java b/src/main/java/com/laytonsmith/core/CallbackYield.java new file mode 100644 index 0000000000..bdba7ad428 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/CallbackYield.java @@ -0,0 +1,339 @@ +package com.laytonsmith.core; + +import com.laytonsmith.core.constructs.CClosure; +import com.laytonsmith.core.constructs.CVoid; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.GenericParameters; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.environments.GlobalEnv; +import com.laytonsmith.core.exceptions.ConfigRuntimeException; +import com.laytonsmith.core.functions.AbstractFunction; +import com.laytonsmith.core.functions.ControlFlow; +import com.laytonsmith.core.natives.interfaces.Callable; +import com.laytonsmith.core.natives.interfaces.Mixed; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +/** + * Base class for functions that need to call closures/callables without re-entering + * {@code eval()}. Subclasses implement {@link #execWithYield} instead of {@code exec()}. + * The callback-style exec builds a chain of deferred steps via a {@link Yield} object, + * which this class then drives as a {@link FlowFunction}. + * + *

The interpreter loop sees this as a FlowFunction and drives it via + * begin/childCompleted/childInterrupted. The subclass never deals with those + * methods — it just uses the Yield API.

+ * + *

Example (array_map):

+ *
+ * protected void execCallback(Target t, Environment env, Mixed[] args, Yield yield) {
+ *     CArray array = ArgumentValidation.getArray(args[0], t, env);
+ *     CClosure closure = ArgumentValidation.getObject(args[1], t, CClosure.class);
+ *     CArray newArray = new CArray(t, (int) array.size(env));
+ *
+ *     for(Mixed key : array.keySet(env)) {
+ *         yield.call(closure, env, t, array.get(key, t, env))
+ *              .then((result, y) -> {
+ *                  newArray.set(key, result, t, env);
+ *              });
+ *     }
+ *     yield.done(() -> newArray);
+ * }
+ * 
+ */ +public abstract class CallbackYield extends AbstractFunction implements FlowFunction { + + /** + * Implement this instead of {@code exec()}. Use the {@link Yield} object to queue + * closure calls and set the final result. + * + * @param t The code target + * @param env The environment + * @param args The evaluated arguments (same as what exec() would receive) + * @param yield The yield object for queuing closure calls + */ + protected abstract void execWithYield(Target t, Environment env, Mixed[] args, Yield yield); + + /** + * Bridges the standard exec() interface to the callback mechanism. This is called by the + * interpreter loop's simple-exec path, but since CallbackYield is also a FlowFunction, + * the loop will use the FlowFunction path instead. This implementation exists only as a + * fallback for external callers that invoke exec() directly (e.g. compile-time optimization). + * In that case, closures are executed synchronously via executeCallable() as before. + */ + @Override + public final Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) + throws ConfigRuntimeException { + // Fallback: build the yield chain but execute closures synchronously. + // This only runs when called outside the iterative interpreter loop. + Yield yield = new Yield(); + execWithYield(t, env, args, yield); + yield.executeSynchronously(env, t); + return yield.getResult(); + } + + @Override + public StepAction.StepResult begin(Target t, ParseTree[] children, Environment env) { + // The interpreter has already evaluated all children (args) before recognizing + // this as a FlowFunction. But actually — since CallbackYield extends AbstractFunction + // AND implements FlowFunction, the loop will see instanceof FlowFunction and route + // to the FlowFunction path. We need to evaluate args ourselves. + // Start by evaluating the first child. + CallbackState state = new CallbackState(); + if(children.length > 0) { + state.children = children; + state.argIndex = 0; + return new StepAction.StepResult<>(new StepAction.Evaluate(children[0]), state); + } + // No args — run the callback immediately + return runCallback(t, env, new Mixed[0], state); + } + + @Override + public StepAction.StepResult childCompleted(Target t, CallbackState state, + Mixed result, Environment env) { + // Phase 1: collecting args + if(!state.yieldStarted) { + state.addArg(result); + state.argIndex++; + if(state.argIndex < state.children.length) { + return new StepAction.StepResult<>( + new StepAction.Evaluate(state.children[state.argIndex]), state); + } + // All args collected — run the callback + return runCallback(t, env, state.getArgs(), state); + } + + // Phase 2: draining yield steps — a closure just completed + YieldStep step = state.currentStep; + if(step != null && step.callback != null) { + step.callback.accept(result, state.yield); + } + return drainNext(t, state, env); + } + + @Override + public StepAction.StepResult childInterrupted(Target t, CallbackState state, + StepAction.FlowControl action, Environment env) { + StepAction.FlowControlAction fca = action.getAction(); + // A return() inside a closure is how it produces its result. + if(fca instanceof ControlFlow.ReturnAction ret) { + YieldStep step = state.currentStep; + cleanupCurrentStep(state, env); + if(step != null && step.callback != null) { + step.callback.accept(ret.getValue(), state.yield); + } + return drainNext(t, state, env); + } + + cleanupCurrentStep(state, env); + + // break/continue cannot escape a closure — this is a script error. + if(fca instanceof ControlFlow.BreakAction || fca instanceof ControlFlow.ContinueAction) { + throw ConfigRuntimeException.CreateUncatchableException( + "Loop manipulation operations (e.g. break() or continue()) cannot" + + " bubble up past closures.", fca.getTarget()); + } + + // ThrowAction and anything else — propagate + return null; + } + + @Override + public void cleanup(Target t, CallbackState state, Environment env) { + if(state != null && state.currentStep != null) { + cleanupCurrentStep(state, env); + } + } + + private StepAction.StepResult runCallback(Target t, Environment env, + Mixed[] args, CallbackState state) { + Yield yield = new Yield(); + state.yield = yield; + state.yieldStarted = true; + execWithYield(t, env, args, yield); + return drainNext(t, state, env); + } + + private StepAction.StepResult drainNext(Target t, CallbackState state, + Environment env) { + Yield yield = state.yield; + if(!yield.steps.isEmpty()) { + YieldStep step = yield.steps.poll(); + state.currentStep = step; + + // Prepare the closure execution + if(step.callable instanceof CClosure closure) { + CClosure.PreparedExecution prep = closure.prepareExecution(step.args); + if(prep == null) { + // Null node closure — result is void + if(step.callback != null) { + step.callback.accept(CVoid.VOID, yield); + } + return drainNext(t, state, env); + } + step.preparedEnv = prep.getEnv(); + return new StepAction.StepResult<>( + new StepAction.Evaluate(closure.getNode(), prep.getEnv()), state); + } else { + // Non-closure Callable (e.g. Method, CNativeClosure) — fall back to synchronous + Mixed result = step.callable.executeCallable(env, t, step.args); + if(step.callback != null) { + step.callback.accept(result, yield); + } + return drainNext(t, state, env); + } + } + + // All steps drained + return new StepAction.StepResult<>( + new StepAction.Complete(yield.getResult()), state); + } + + private void cleanupCurrentStep(CallbackState state, Environment env) { + YieldStep step = state.currentStep; + if(step != null && step.preparedEnv != null) { + // Pop the stack trace element that prepareExecution pushed + step.preparedEnv.getEnv(GlobalEnv.class).GetStackTraceManager().popStackTraceElement(); + step.preparedEnv = null; + } + state.currentStep = null; + } + + /** + * Per-call state for the FlowFunction. Tracks argument collection and yield step draining. + */ + protected static class CallbackState { + ParseTree[] children; + int argIndex; + private Mixed[] args; + private int argCount; + boolean yieldStarted; + Yield yield; + YieldStep currentStep; + + void addArg(Mixed arg) { + if(args == null) { + args = new Mixed[children.length]; + } + args[argCount++] = arg; + } + + Mixed[] getArgs() { + if(args == null) { + return new Mixed[0]; + } + if(argCount < args.length) { + Mixed[] trimmed = new Mixed[argCount]; + System.arraycopy(args, 0, trimmed, 0, argCount); + return trimmed; + } + return args; + } + + @Override + public String toString() { + if(!yieldStarted) { + return "CallbackState{collecting args: " + argCount + "/" + (children != null ? children.length : 0) + "}"; + } + return "CallbackState{draining yields: " + (yield != null ? yield.steps.size() : 0) + " remaining}"; + } + } + + /** + * The object passed to {@link #execWithYield}. Functions use this to queue closure calls + * and declare the final result. + */ + public static class Yield { + private final Queue steps = new ArrayDeque<>(); + private Supplier resultSupplier = () -> CVoid.VOID; + private boolean doneSet = false; + + /** + * Queue a closure/callable invocation. + * + * @param callable The closure or callable to invoke + * @param env The environment (unused for closures, which capture their own) + * @param t The target + * @param args The arguments to pass to the callable + * @return A {@link YieldStep} for chaining a {@code .then()} callback + */ + public YieldStep call(Callable callable, Environment env, Target t, Mixed... args) { + YieldStep step = new YieldStep(callable, args); + steps.add(step); + return step; + } + + /** + * Set the final result of this function via a supplier. The supplier is evaluated + * after all yield steps have completed. This must be called exactly once. + * + * @param resultSupplier A supplier that returns the result value + */ + public void done(Supplier resultSupplier) { + this.resultSupplier = resultSupplier; + this.doneSet = true; + } + + Mixed getResult() { + return resultSupplier.get(); + } + + /** + * Fallback for when CallbackYield functions are called outside the iterative + * interpreter (e.g. during compile-time optimization). Drains all steps synchronously + * by calling executeCallable directly. + */ + void executeSynchronously(Environment env, Target t) { + while(!steps.isEmpty()) { + YieldStep step = steps.poll(); + Mixed r = step.callable.executeCallable(env, t, step.args); + if(step.callback != null) { + step.callback.accept(r, this); + } + } + } + + @Override + public String toString() { + return "Yield{steps=" + steps.size() + ", doneSet=" + doneSet + "}"; + } + } + + /** + * A single queued closure call with an optional continuation. + */ + public static class YieldStep { + final Callable callable; + final Mixed[] args; + BiConsumer callback; + Environment preparedEnv; + + YieldStep(Callable callable, Mixed[] args) { + this.callable = callable; + this.args = args; + } + + /** + * Register a callback to run after the closure completes. + * + * @param callback Receives the closure's return value and the Yield object + * (for queuing additional steps or calling done()) + * @return This step, for fluent chaining + */ + public YieldStep then(BiConsumer callback) { + this.callback = callback; + return this; + } + + @Override + public String toString() { + return "YieldStep{callable=" + callable.getClass().getSimpleName() + + ", args=" + Arrays.toString(args) + ", hasCallback=" + (callback != null) + "}"; + } + } +} diff --git a/src/main/java/com/laytonsmith/core/FlowFunction.java b/src/main/java/com/laytonsmith/core/FlowFunction.java index 4ded753ae4..79f26dd17e 100644 --- a/src/main/java/com/laytonsmith/core/FlowFunction.java +++ b/src/main/java/com/laytonsmith/core/FlowFunction.java @@ -18,6 +18,11 @@ *

Functions that DO need it (if, for, and, try, etc.) implement this interface * and are driven by the interpreter loop via begin/childCompleted/childInterrupted.

* + *

Functions that execute Callables MUST use this mechanism, however, + * in most cases, it is sufficient to implement {@link CallbackYield} instead, which + * is a specialized overload of this class, which hides most of the complexity + * in the case where the only complexity is calling Callables.

+ * *

The type parameter {@code S} is the per-call state type. Since function instances * are singletons, per-call mutable state cannot be stored on the function itself. * Instead, methods receive and return state via {@link StepAction.StepResult}. diff --git a/src/main/java/com/laytonsmith/core/constructs/CClosure.java b/src/main/java/com/laytonsmith/core/constructs/CClosure.java index 8e67d6bad3..3e853da26b 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CClosure.java +++ b/src/main/java/com/laytonsmith/core/constructs/CClosure.java @@ -3,6 +3,7 @@ import com.laytonsmith.PureUtilities.Version; import com.laytonsmith.annotations.typeof; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.CallbackYield; import com.laytonsmith.core.functions.Compiler; import com.laytonsmith.core.natives.interfaces.Booleanish; import com.laytonsmith.core.natives.interfaces.Callable; @@ -117,6 +118,122 @@ public ParseTree getNode() { return node; } + /** + * Prepares the closure for execution by cloning the environment, binding arguments, + * and pushing a stack trace element. This extracts the setup logic from {@link #execute} + * so that the iterative interpreter can evaluate the closure body on the shared EvalStack + * instead of recursing into a new eval() call. + * + *

The caller is responsible for evaluating the body (via {@link #getNode()}) in the + * returned environment, and for calling {@link StackTraceManager#popStackTraceElement()} + * when done (or ensuring it's done via a cleanup mechanism).

+ * + * @param values The argument values to bind, or null for no arguments + * @return A {@link PreparedExecution} containing the prepared environment + * @throws ConfigRuntimeException If argument type checking fails + */ + public PreparedExecution prepareExecution(Mixed... values) throws ConfigRuntimeException { + if(node == null) { + return null; + } + Environment env; + try { + synchronized(this) { + env = this.env.clone(); + } + } catch(CloneNotSupportedException ex) { + Logger.getLogger(CClosure.class.getName()).log(Level.SEVERE, null, ex); + return null; + } + StackTraceManager stManager = env.getEnv(GlobalEnv.class).GetStackTraceManager(); + stManager.addStackTraceElement(new ConfigRuntimeException.StackTraceElement("<>", getTarget())); + + CArray arguments = new CArray(node.getData().getTarget()); + CArray vararg = null; + CClassType varargType = null; + if(values != null) { + for(int i = 0; i < Math.max(values.length, names.length); i++) { + Mixed value; + if(i < values.length) { + value = values[i]; + } else { + try { + value = defaults[i].clone(); + } catch(CloneNotSupportedException ex) { + Logger.getLogger(CClosure.class.getName()).log(Level.SEVERE, null, ex); + value = defaults[i]; + } + } + arguments.push(value, node.getData().getTarget()); + boolean isVarArg = false; + if(this.names.length > i + || (this.names.length != 0 + && this.types[this.names.length - 1].isVariadicType())) { + String name; + if(i < this.names.length - 1 + || !this.types[this.types.length - 1].isVariadicType()) { + name = names[i]; + } else { + name = this.names[this.names.length - 1]; + if(vararg == null) { + // TODO: Once generics are added, add the type + vararg = new CArray(value.getTarget()); + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, + name, vararg, value.getTarget())); + varargType = this.types[this.types.length - 1]; + } + isVarArg = true; + } + if(isVarArg) { + if(!InstanceofUtil.isInstanceof(value.typeof(env), varargType.getVarargsBaseType(), env)) { + throw new CRECastException("Expected type " + varargType + " but found " + value.typeof(env), + getTarget()); + } + vararg.push(value, value.getTarget()); + } else { + IVariable var = new IVariable(types[i], name, value, getTarget(), env); + env.getEnv(GlobalEnv.class).GetVarList().set(var); + } + } + } + } + boolean hasArgumentsParam = false; + for(String pName : this.names) { + if(pName.equals("@arguments")) { + hasArgumentsParam = true; + break; + } + } + if(!hasArgumentsParam) { + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, "@arguments", arguments, + node.getData().getTarget())); + } + + return new PreparedExecution(env, returnType); + } + + /** + * The result of {@link #prepareExecution}. Contains the cloned environment with arguments + * bound, ready for the closure body to be evaluated. + */ + public static class PreparedExecution { + private final Environment env; + private final CClassType returnType; + + PreparedExecution(Environment env, CClassType returnType) { + this.env = env; + this.returnType = returnType; + } + + public Environment getEnv() { + return env; + } + + public CClassType getReturnType() { + return returnType; + } + } + @Override public CClosure clone() throws CloneNotSupportedException { CClosure clone = (CClosure) super.clone(); @@ -147,7 +264,10 @@ public synchronized Environment getEnv() { * @return * @throws ConfigRuntimeException * @throws CancelCommandException + * @deprecated Functions that call closures should extend {@link CallbackYield} + * instead of calling this directly, which re-enters eval() and defeats the iterative interpreter. */ + @Deprecated public Mixed executeCallable(Mixed... values) { return executeCallable(null, Target.UNKNOWN, values); } @@ -176,7 +296,10 @@ public Mixed executeCallable(Mixed... values) { * @return The return value of the closure, or VOID if nothing was returned * @throws ConfigRuntimeException If any call inside the closure causes a CRE * @throws CancelCommandException If die() is called within the closure + * @deprecated Functions that call closures should extend {@link CallbackYield} + * instead of calling this directly, which re-enters eval() and defeats the iterative interpreter. */ + @Deprecated @Override public Mixed executeCallable(Environment env, Target t, Mixed... values) throws ConfigRuntimeException, CancelCommandException { diff --git a/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java b/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java index 573f9f3ffc..cac4583297 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java +++ b/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java @@ -2,6 +2,7 @@ import com.laytonsmith.PureUtilities.Version; import com.laytonsmith.annotations.typeof; +import com.laytonsmith.core.CallbackYield; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.exceptions.CancelCommandException; @@ -37,6 +38,11 @@ public boolean isDynamic() { return true; } + /** + * @deprecated Functions that call closures should extend {@link CallbackYield} + * instead of calling this directly, which re-enters eval() and defeats the iterative interpreter. + */ + @Deprecated @Override public Mixed executeCallable(Environment env, Target t, Mixed... values) throws ConfigRuntimeException, CancelCommandException { return runnable.execute(t, env, values); diff --git a/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java b/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java index ffe0972899..1d24fb24e4 100644 --- a/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java @@ -11,6 +11,7 @@ import com.laytonsmith.annotations.core; import com.laytonsmith.annotations.seealso; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.CallbackYield; import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.Optimizable; @@ -3213,7 +3214,7 @@ public ExampleScript[] examples() throws ConfigCompileException { } @api - public static class array_map extends AbstractFunction { + public static class array_map extends CallbackYield { @Override public Class[] thrown() { @@ -3231,21 +3232,22 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { CArray array = ArgumentValidation.getArray(args[0], t, env); CClosure closure = ArgumentValidation.getObject(args[1], t, CClosure.class); CArray newArray = (array.isAssociative() ? CArray.GetAssociativeArray(t, null, env) : new CArray(t, (int) array.size(env))); for(Mixed c : array.keySet(env)) { - Mixed fr = closure.executeCallable(env, t, array.get(c, t, env)); - if(fr.isInstanceOf(CVoid.TYPE, null, env)) { - throw new CREIllegalArgumentException("The closure passed to " + getName() - + " must return a value.", t); - } - newArray.set(c, fr, t, env); + yield.call(closure, env, t, array.get(c, t, env)) + .then((result, y) -> { + if(result.isInstanceOf(CVoid.TYPE, null, env)) { + throw new CREIllegalArgumentException("The closure passed to " + getName() + + " must return a value.", t); + } + newArray.set(c, result, t, env); + }); } - - return newArray; + yield.done(() -> newArray); } @Override diff --git a/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java b/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java index 9ad528eac6..82125f20fb 100644 --- a/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java +++ b/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java @@ -1,6 +1,7 @@ package com.laytonsmith.core.natives.interfaces; import com.laytonsmith.annotations.typeof; +import com.laytonsmith.core.CallbackYield; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.environments.Environment; @@ -28,7 +29,10 @@ public interface Callable extends Mixed { * @return The return value of the callable, or VOID if nothing was returned * @throws ConfigRuntimeException If any call inside the callable causes a CRE * @throws CancelCommandException If die() is called within the callable + * @deprecated Functions that call closures should extend {@link CallbackYield} + * instead of calling this directly, which re-enters eval() and defeats the iterative interpreter. */ + @Deprecated Mixed executeCallable(Environment env, Target t, Mixed... values) throws ConfigRuntimeException, CancelCommandException; From 1de6f026b7cb28801ac57f26649b3c3d8b73d3ad Mon Sep 17 00:00:00 2001 From: LadyCailin Date: Sun, 8 Mar 2026 22:55:53 +0100 Subject: [PATCH 4/5] Convert various function to CallbackYield functions. These are the "easy" functions to convert. --- .../com/laytonsmith/core/CallbackYield.java | 34 +- .../core/functions/ArrayHandling.java | 459 +++++++++++------- .../core/functions/DataHandling.java | 31 +- .../core/functions/ArrayHandlingTest.java | 101 ++++ .../core/functions/DataHandlingTest.java | 21 + 5 files changed, 454 insertions(+), 192 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/CallbackYield.java b/src/main/java/com/laytonsmith/core/CallbackYield.java index bdba7ad428..61d3dab121 100644 --- a/src/main/java/com/laytonsmith/core/CallbackYield.java +++ b/src/main/java/com/laytonsmith/core/CallbackYield.java @@ -196,10 +196,15 @@ private StepAction.StepResult drainNext(Target t, CallbackState s private void cleanupCurrentStep(CallbackState state, Environment env) { YieldStep step = state.currentStep; - if(step != null && step.preparedEnv != null) { - // Pop the stack trace element that prepareExecution pushed - step.preparedEnv.getEnv(GlobalEnv.class).GetStackTraceManager().popStackTraceElement(); - step.preparedEnv = null; + if(step != null) { + if(step.preparedEnv != null) { + // Pop the stack trace element that prepareExecution pushed + step.preparedEnv.getEnv(GlobalEnv.class).GetStackTraceManager().popStackTraceElement(); + step.preparedEnv = null; + } + if(step.cleanupAction != null) { + step.cleanupAction.run(); + } } state.currentStep = null; } @@ -283,6 +288,14 @@ Mixed getResult() { return resultSupplier.get(); } + /** + * Clears all remaining queued steps. Used for short-circuiting (e.g. array_every, + * array_some) where the final result is known before all steps have been processed. + */ + public void clear() { + steps.clear(); + } + /** * Fallback for when CallbackYield functions are called outside the iterative * interpreter (e.g. during compile-time optimization). Drains all steps synchronously @@ -311,6 +324,7 @@ public static class YieldStep { final Callable callable; final Mixed[] args; BiConsumer callback; + Runnable cleanupAction; Environment preparedEnv; YieldStep(Callable callable, Mixed[] args) { @@ -330,6 +344,18 @@ public YieldStep then(BiConsumer callback) { return this; } + /** + * Register a cleanup action that runs after this step completes, whether + * normally or due to an exception. This is analogous to a {@code finally} block. + * + * @param cleanup The cleanup action to run + * @return This step, for fluent chaining + */ + public YieldStep cleanup(Runnable cleanup) { + this.cleanupAction = cleanup; + return this; + } + @Override public String toString() { return "YieldStep{callable=" + callable.getClass().getSimpleName() diff --git a/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java b/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java index 1d24fb24e4..da19cca24f 100644 --- a/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java @@ -71,6 +71,7 @@ import java.util.Random; import java.util.Set; import java.util.TreeSet; +import java.util.function.BiConsumer; @core public class ArrayHandling { @@ -1777,7 +1778,7 @@ public Set optimizationOptions() { } @api - public static class array_sort extends AbstractFunction implements Optimizable { + public static class array_sort extends CallbackYield implements Optimizable { @Override public Class[] thrown() { @@ -1795,7 +1796,7 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { if(!(args[0].isInstanceOf(CArray.TYPE, null, env))) { throw new CRECastException("The first parameter to array_sort must be an array", t); } @@ -1803,7 +1804,8 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. CArray.ArraySortType sortType = CArray.ArraySortType.REGULAR; CClosure customSort = null; if(ca.size(env) <= 1) { - return ca; + yield.done(() -> ca); + return; } try { if(args.length == 2) { @@ -1818,83 +1820,124 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. throw new CREFormatException("The sort type must be one of either: " + StringUtils.Join(CArray.ArraySortType.values(), ", ", " or "), t); } if(sortType == null) { - // It's a custom sort, which we have implemented below. if(ca.isAssociative()) { throw new CRECastException("Associative arrays may not be sorted using a custom comparator.", t); } - CArray sorted = customSort(ca, customSort, t, env); - //Clear it out and re-apply the values, so this is in place. - ca.clear(); - for(Mixed c : sorted.keySet(env)) { - ca.set(c, sorted.get(c, t, env), t, env); + // Copy elements into a working list for bottom-up merge sort + int n = (int) ca.size(env); + Mixed[] work = new Mixed[n]; + for(int i = 0; i < n; i++) { + work[i] = ca.get(i, t, env); } + // Queue the first merge comparison + queueMergeSort(customSort, env, t, work, ca, yield); } else { ca.sort(sortType, env); } - return ca; - } - - private CArray customSort(CArray ca, CClosure closure, Target t, Environment env) { - if(ca.size(env) <= 1) { - return ca; - } - - CArray left = new CArray(t); - CArray right = new CArray(t); - int middle = (int) (ca.size(env) / 2); - for(int i = 0; i < middle; i++) { - left.push(ca.get(i, t, env), t, env); - } - for(int i = middle; i < ca.size(env); i++) { - right.push(ca.get(i, t, env), t, env); - } - - left = customSort(left, closure, t, env); - right = customSort(right, closure, t, env); - - return merge(left, right, closure, t, env); - } - - private CArray merge(CArray left, CArray right, CClosure closure, Target t, Environment env) { - CArray result = new CArray(t); - while(left.size(env) > 0 || right.size(env) > 0) { - if(left.size(env) > 0 && right.size(env) > 0) { - // Compare the first two elements of each side - Mixed l = left.get(0, t, env); - Mixed r = right.get(0, t, env); - Mixed c = closure.executeCallable(null, t, l, r); - int value; - if(c instanceof CNull) { - value = 0; - } else if(c instanceof CBoolean) { - if(((CBoolean) c).getBoolean()) { - value = 1; + yield.done(() -> ca); + } + + /** + * Implements a bottom-up merge sort using yield steps. Each element comparison + * is a closure call that yields to the eval loop. + */ + private void queueMergeSort(CClosure closure, Environment env, Target t, + Mixed[] work, CArray ca, CallbackYield.Yield yield) { + int n = work.length; + // State: width doubles each pass (1, 2, 4, ...), i is the start of each merge block + int[] width = {1}; + int[] i = {0}; + // left/right pointers within the current merge + int[] l = {0}; + int[] r = {0}; + int[] lEnd = {0}; + int[] rEnd = {0}; + Mixed[] aux = new Mixed[n]; + + // Set up initial merge block + Runnable[] setupNextMerge = new Runnable[1]; + setupNextMerge[0] = () -> { + while(width[0] < n) { + if(i[0] < n) { + l[0] = i[0]; + lEnd[0] = java.lang.Math.min(i[0] + width[0], n); + r[0] = lEnd[0]; + rEnd[0] = java.lang.Math.min(i[0] + 2 * width[0], n); + i[0] += 2 * width[0]; + if(r[0] < rEnd[0]) { + // This merge block has elements on both sides; need comparisons + return; } else { - value = -1; + // Only left side has elements, just copy them + for(int k = l[0]; k < lEnd[0]; k++) { + aux[k] = work[k]; + } + continue; } - } else if(c.isInstanceOf(CInt.TYPE, null, env)) { - long longVal = ((CInt) c).getInt(); - value = (longVal > 0 ? 1 : (longVal < 0 ? -1 : 0)); - } else { - throw new CRECastException("The custom closure did not return a value (or returned an invalid" - + " type). It must always return true, false, null, or an integer.", t); } - if(value <= 0) { - result.push(left.get(0, t, env), t, env); - left.remove(0, env); - } else { - result.push(right.get(0, t, env), t, env); - right.remove(0, env); + // Finished a pass — copy aux back to work and start next width + System.arraycopy(aux, 0, work, 0, n); + width[0] *= 2; + i[0] = 0; + } + // Sort complete — write results back to the CArray + ca.clear(); + for(int k = 0; k < n; k++) { + ca.push(work[k], t, env); + } + }; + + // Recursive step that drives the merge comparison + BiConsumer[] mergeStep = new BiConsumer[1]; + mergeStep[0] = (result, y) -> { + int value = parseCompareResult(result, t, env); + if(value <= 0) { + aux[l[0] + r[0] - lEnd[0]] = work[l[0]]; + l[0]++; + } else { + aux[l[0] + r[0] - lEnd[0]] = work[r[0]]; + r[0]++; + } + // Continue merging this block + if(l[0] < lEnd[0] && r[0] < rEnd[0]) { + y.call(closure, env, t, work[l[0]], work[r[0]]).then(mergeStep[0]); + } else { + // One side exhausted — copy remainder + while(l[0] < lEnd[0]) { + aux[l[0] + r[0] - lEnd[0]] = work[l[0]]; + l[0]++; + } + while(r[0] < rEnd[0]) { + aux[l[0] + r[0] - lEnd[0]] = work[r[0]]; + r[0]++; + } + // Move to next merge block + setupNextMerge[0].run(); + if(width[0] < n) { + y.call(closure, env, t, work[l[0]], work[r[0]]).then(mergeStep[0]); } - } else if(left.size(env) > 0) { - result.push(left.get(0, t, env), t, env); - left.remove(0, env); - } else if(right.size(env) > 0) { - result.push(right.get(0, t, env), t, env); - right.remove(0, env); } + }; + + // Kick off the first comparison + setupNextMerge[0].run(); + if(width[0] < n) { + yield.call(closure, env, t, work[l[0]], work[r[0]]).then(mergeStep[0]); + } + } + + private int parseCompareResult(Mixed c, Target t, Environment env) { + if(c instanceof CNull) { + return 0; + } else if(c instanceof CBoolean) { + return ((CBoolean) c).getBoolean() ? 1 : -1; + } else if(c.isInstanceOf(CInt.TYPE, null, env)) { + long longVal = ((CInt) c).getInt(); + return (longVal > 0 ? 1 : (longVal < 0 ? -1 : 0)); + } else { + throw new CRECastException("The custom closure did not return a value (or returned an invalid" + + " type). It must always return true, false, null, or an integer.", t); } - return result; } @Override @@ -2576,7 +2619,7 @@ public Set optimizationOptions() { } @api - public static class array_filter extends AbstractFunction { + public static class array_filter extends CallbackYield { @Override public Class[] thrown() { @@ -2594,7 +2637,7 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { com.laytonsmith.core.natives.interfaces.Iterable array; CClosure closure; if(!(args[0] instanceof com.laytonsmith.core.natives.interfaces.Iterable)) { @@ -2610,28 +2653,32 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. newArray = CArray.GetAssociativeArray(t, null, env); for(Mixed key : array.keySet(env)) { Mixed value = array.get(key, t, env); - Mixed ret = closure.executeCallable(env, t, key, value); - boolean bret = ArgumentValidation.getBooleanish(ret, t, env); - if(bret) { - newArray.set(key, value, t, env); - } + yield.call(closure, env, t, key, value) + .then((result, y) -> { + boolean bret = ArgumentValidation.getBooleanish(result, t, env); + if(bret) { + newArray.set(key, value, t, env); + } + }); } } else { newArray = new CArray(t); for(int i = 0; i < array.size(env); i++) { - Mixed key = new CInt(i, t); Mixed value = array.get(i, t, env); - Mixed ret = closure.executeCallable(env, t, key, value); - if(ret == CNull.NULL) { - ret = CBoolean.FALSE; - } - boolean bret = ArgumentValidation.getBooleanish(ret, t, env); - if(bret) { - newArray.push(value, t, env); - } + yield.call(closure, env, t, new CInt(i, t), value) + .then((result, y) -> { + Mixed r = result; + if(r == CNull.NULL) { + r = CBoolean.FALSE; + } + boolean bret = ArgumentValidation.getBooleanish(r, t, env); + if(bret) { + newArray.push(value, t, env); + } + }); } } - return newArray; + yield.done(() -> newArray); } @Override @@ -2820,7 +2867,7 @@ public ExampleScript[] examples() throws ConfigCompileException { } @api - public static class array_iterate extends AbstractFunction { + public static class array_iterate extends CallbackYield { @Override public Class[] thrown() { @@ -2838,7 +2885,7 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { ArrayAccess aa; if(args[0] instanceof CFixedArray fa) { aa = fa; @@ -2847,13 +2894,9 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. } CClosure closure = ArgumentValidation.getObject(args[1], t, CClosure.class); for(Mixed key : aa.keySet(env)) { - try { - closure.executeCallable(env, t, key, aa.get(key, t, env)); - } catch(CancelCommandException ex) { - // Ignored - } + yield.call(closure, env, t, key, aa.get(key, t, env)); } - return aa; + yield.done(() -> aa); } @Override @@ -2898,7 +2941,7 @@ public ExampleScript[] examples() throws ConfigCompileException { } @api - public static class array_reduce extends AbstractFunction { + public static class array_reduce extends CallbackYield { @Override public Class[] thrown() { @@ -2916,26 +2959,36 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { CArray array = ArgumentValidation.getArray(args[0], t, env); CClosure closure = ArgumentValidation.getObject(args[1], t, CClosure.class); if(array.isEmpty(env)) { - return CNull.NULL; - } - if(array.size(env) == 1) { - // This line looks bad, but all it does is return the first (and since we know only) value in the array, - // whether or not it is associative or normal. - return array.get(array.keySet(env).toArray(Mixed[]::new)[0], t, env); + yield.done(() -> CNull.NULL); + return; } List keys = new ArrayList<>(array.keySet(env)); - Mixed lastValue = array.get(keys.get(0), t, env); - for(int i = 1; i < keys.size(); ++i) { - lastValue = closure.executeCallable(env, t, lastValue, array.get(keys.get(i), t, env)); - if(lastValue instanceof CVoid) { - throw new CREIllegalArgumentException("The closure passed to " + getName() + " cannot return void.", t); - } + if(array.size(env) == 1) { + yield.done(() -> array.get(keys.get(0), t, env)); + return; } - return lastValue; + Mixed[] acc = {array.get(keys.get(0), t, env)}; + queueReduceStep(closure, env, t, array, keys, acc, 1, yield); + yield.done(() -> acc[0]); + } + + private void queueReduceStep(CClosure closure, Environment env, Target t, + CArray array, List keys, Mixed[] acc, int index, CallbackYield.Yield yield) { + yield.call(closure, env, t, acc[0], array.get(keys.get(index), t, env)) + .then((result, y) -> { + if(result instanceof CVoid) { + throw new CREIllegalArgumentException("The closure passed to " + getName() + + " cannot return void.", t); + } + acc[0] = result; + if(index + 1 < keys.size()) { + queueReduceStep(closure, env, t, array, keys, acc, index + 1, y); + } + }); } @Override @@ -2983,7 +3036,7 @@ public ExampleScript[] examples() throws ConfigCompileException { } @api - public static class array_reduce_right extends AbstractFunction { + public static class array_reduce_right extends CallbackYield { @Override public Class[] thrown() { @@ -3001,26 +3054,36 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { CArray array = ArgumentValidation.getArray(args[0], t, env); CClosure closure = ArgumentValidation.getObject(args[1], t, CClosure.class); if(array.isEmpty(env)) { - return CNull.NULL; - } - if(array.size(env) == 1) { - // This line looks bad, but all it does is return the first (and since we know only) value in the array, - // whether or not it is associative or normal. - return array.get(array.keySet(env).toArray(Mixed[]::new)[0], t, env); + yield.done(() -> CNull.NULL); + return; } List keys = new ArrayList<>(array.keySet(env)); - Mixed lastValue = array.get(keys.get(keys.size() - 1), t, env); - for(int i = keys.size() - 2; i >= 0; --i) { - lastValue = closure.executeCallable(env, t, lastValue, array.get(keys.get(i), t, env)); - if(lastValue instanceof CVoid) { - throw new CREIllegalArgumentException("The closure passed to " + getName() + " cannot return void.", t); - } + if(array.size(env) == 1) { + yield.done(() -> array.get(keys.get(0), t, env)); + return; } - return lastValue; + Mixed[] acc = {array.get(keys.get(keys.size() - 1), t, env)}; + queueReduceStep(closure, env, t, array, keys, acc, keys.size() - 2, yield); + yield.done(() -> acc[0]); + } + + private void queueReduceStep(CClosure closure, Environment env, Target t, + CArray array, List keys, Mixed[] acc, int index, CallbackYield.Yield yield) { + yield.call(closure, env, t, acc[0], array.get(keys.get(index), t, env)) + .then((result, y) -> { + if(result instanceof CVoid) { + throw new CREIllegalArgumentException("The closure passed to " + getName() + + " cannot return void.", t); + } + acc[0] = result; + if(index - 1 >= 0) { + queueReduceStep(closure, env, t, array, keys, acc, index - 1, y); + } + }); } @Override @@ -3068,7 +3131,7 @@ public ExampleScript[] examples() throws ConfigCompileException { } @api - public static class array_every extends AbstractFunction { + public static class array_every extends CallbackYield { @Override public Class[] thrown() { @@ -3086,17 +3149,21 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { CArray array = ArgumentValidation.getArray(args[0], t, env); CClosure closure = ArgumentValidation.getObject(args[1], t, CClosure.class); + boolean[] result = {true}; for(Mixed c : array.keySet(env)) { - Mixed fr = closure.executeCallable(env, t, array.get(c, t, env)); - boolean ret = ArgumentValidation.getBooleanish(fr, t, env); - if(ret == false) { - return CBoolean.FALSE; - } + yield.call(closure, env, t, array.get(c, t, env)) + .then((fr, y) -> { + boolean ret = ArgumentValidation.getBooleanish(fr, t, env); + if(!ret) { + result[0] = false; + y.clear(); + } + }); } - return CBoolean.TRUE; + yield.done(() -> CBoolean.get(result[0])); } @Override @@ -3141,7 +3208,7 @@ public ExampleScript[] examples() throws ConfigCompileException { } @api - public static class array_some extends AbstractFunction { + public static class array_some extends CallbackYield { @Override public Class[] thrown() { @@ -3159,17 +3226,21 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { CArray array = ArgumentValidation.getArray(args[0], t, env); CClosure closure = ArgumentValidation.getObject(args[1], t, CClosure.class); + boolean[] result = {false}; for(Mixed c : array.keySet(env)) { - Mixed fr = closure.executeCallable(env, t, array.get(c, t, env)); - boolean ret = ArgumentValidation.getBooleanish(fr, t, env); - if(ret == true) { - return CBoolean.TRUE; - } + yield.call(closure, env, t, array.get(c, t, env)) + .then((fr, y) -> { + boolean ret = ArgumentValidation.getBooleanish(fr, t, env); + if(ret) { + result[0] = true; + y.clear(); + } + }); } - return CBoolean.FALSE; + yield.done(() -> CBoolean.get(result[0])); } @Override @@ -3311,7 +3382,7 @@ public Function getComparisonFunction() { @api @seealso({array_merge.class, array_subtract.class}) - public static class array_intersect extends AbstractFunction { + public static class array_intersect extends CallbackYield { @Override public Class[] thrown() { @@ -3329,7 +3400,7 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { CArray one = ArgumentValidation.getArray(args[0], t, env); CArray two = ArgumentValidation.getArray(args[1], t, env); CClosure closure = null; @@ -3361,6 +3432,12 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. ret.push(c, t, env); } } + } else if(closure != null) { + Mixed[] k1 = new Mixed[(int) one.size(env)]; + Mixed[] k2 = new Mixed[(int) two.size(env)]; + one.keySet(env).toArray(k1); + two.keySet(env).toArray(k2); + queueIntersectStep(closure, env, t, one, two, k1, k2, ret, 0, 0, yield); } else { Mixed[] k1 = new Mixed[(int) one.size(env)]; Mixed[] k2 = new Mixed[(int) two.size(env)]; @@ -3377,30 +3454,44 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. continue i; } } else { - if(closure == null) { - if(comparisonFunction != null) { - if(ArgumentValidation.getBooleanish(comparisonFunction.exec(t, env, null, - one.get(k1[i], t, env), two.get(k2[j], t, env) - ), t, env)) { - ret.push(one.get(k1[i], t, env), t, env); - continue i; - } - } else { - throw new Error(); - } - } else { - Mixed fre = closure.executeCallable(env, t, one.get(k1[i], t, env), two.get(k2[j], t, env)); - boolean res = ArgumentValidation.getBooleanish(fre, fre.getTarget(), env); - if(res) { + if(comparisonFunction != null) { + if(ArgumentValidation.getBooleanish(comparisonFunction.exec(t, env, null, + one.get(k1[i], t, env), two.get(k2[j], t, env) + ), t, env)) { ret.push(one.get(k1[i], t, env), t, env); continue i; } + } else { + throw new Error(); } } } } } - return ret; + yield.done(() -> ret); + } + + private void queueIntersectStep(CClosure closure, Environment env, Target t, + CArray one, CArray two, Mixed[] k1, Mixed[] k2, CArray ret, + int i, int j, CallbackYield.Yield yield) { + if(i >= k1.length) { + return; + } + yield.call(closure, env, t, one.get(k1[i], t, env), two.get(k2[j], t, env)) + .then((result, y) -> { + boolean res = ArgumentValidation.getBooleanish(result, result.getTarget(), env); + if(res) { + ret.push(one.get(k1[i], t, env), t, env); + // Match found, skip to next outer element + queueIntersectStep(closure, env, t, one, two, k1, k2, ret, i + 1, 0, y); + } else if(j + 1 < k2.length) { + // Try next inner element + queueIntersectStep(closure, env, t, one, two, k1, k2, ret, i, j + 1, y); + } else { + // Exhausted inner loop, move to next outer element + queueIntersectStep(closure, env, t, one, two, k1, k2, ret, i + 1, 0, y); + } + }); } @Override @@ -3703,7 +3794,7 @@ public ExampleScript[] examples() throws ConfigCompileException { @api @seealso(array_intersect.class) - public static class array_subtract extends AbstractFunction { + public static class array_subtract extends CallbackYield { @Override public Class[] thrown() { @@ -3721,7 +3812,7 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { CArray one = ArgumentValidation.getArray(args[0], t, env); CArray two = ArgumentValidation.getArray(args[1], t, env); CClosure closure = null; @@ -3753,6 +3844,12 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. ret.push(c, t, env); } } + } else if(closure != null) { + Mixed[] k1 = new Mixed[(int) one.size(env)]; + Mixed[] k2 = new Mixed[(int) two.size(env)]; + one.keySet(env).toArray(k1); + two.keySet(env).toArray(k2); + queueSubtractStep(closure, env, t, one, two, k1, k2, ret, 0, 0, yield); } else { Mixed[] k1 = new Mixed[(int) one.size(env)]; Mixed[] k2 = new Mixed[(int) two.size(env)]; @@ -3769,24 +3866,15 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. break; } } else { - if(closure == null) { - if(comparisonFunction != null) { - if(ArgumentValidation.getBooleanish(comparisonFunction.exec(t, env, null, - one.get(k1[i], t, env), two.get(k2[j], t, env) - ), t, env)) { - addValue = false; - break; - } - } else { - throw new Error(); - } - } else { - Mixed fre = closure.executeCallable(env, t, one.get(k1[i], t, env), two.get(k2[j], t, env)); - boolean res = ArgumentValidation.getBooleanish(fre, fre.getTarget(), env); - if(res) { + if(comparisonFunction != null) { + if(ArgumentValidation.getBooleanish(comparisonFunction.exec(t, env, null, + one.get(k1[i], t, env), two.get(k2[j], t, env) + ), t, env)) { addValue = false; break; } + } else { + throw new Error(); } } } @@ -3799,7 +3887,30 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. } } } - return ret; + yield.done(() -> ret); + } + + private void queueSubtractStep(CClosure closure, Environment env, Target t, + CArray one, CArray two, Mixed[] k1, Mixed[] k2, CArray ret, + int i, int j, CallbackYield.Yield yield) { + if(i >= k1.length) { + return; + } + yield.call(closure, env, t, one.get(k1[i], t, env), two.get(k2[j], t, env)) + .then((result, y) -> { + boolean res = ArgumentValidation.getBooleanish(result, result.getTarget(), env); + if(res) { + // Match found — this element should NOT be in the result + queueSubtractStep(closure, env, t, one, two, k1, k2, ret, i + 1, 0, y); + } else if(j + 1 < k2.length) { + // No match yet, try next inner element + queueSubtractStep(closure, env, t, one, two, k1, k2, ret, i, j + 1, y); + } else { + // Exhausted inner loop with no match — include this element + ret.push(one.get(k1[i], t, env), t, env); + queueSubtractStep(closure, env, t, one, two, k1, k2, ret, i + 1, 0, y); + } + }); } @Override diff --git a/src/main/java/com/laytonsmith/core/functions/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java index 5a583d1cb5..6522a8f12c 100644 --- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java @@ -13,6 +13,7 @@ import com.laytonsmith.annotations.seealso; import com.laytonsmith.annotations.unbreakable; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.CallbackYield; import com.laytonsmith.core.natives.interfaces.Callable; import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.Globals; @@ -3293,7 +3294,7 @@ public MSVersion since() { @api @seealso({com.laytonsmith.tools.docgen.templates.Closures.class, execute_array.class, executeas.class}) - public static class execute extends AbstractFunction { + public static class execute extends CallbackYield { public static final String NAME = "execute"; @@ -3335,11 +3336,12 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { if(args[args.length - 1] instanceof Callable callable) { Mixed[] vals = new Mixed[args.length - 1]; System.arraycopy(args, 0, vals, 0, args.length - 1); - return callable.executeCallable(env, t, vals); + yield.call(callable, env, t, vals) + .then((result, y) -> y.done(() -> result)); } else { throw new CRECastException("Only a Callable (created for instance from the closure function) can be" + " sent to execute(), or executed directly, such as @c().", t); @@ -3364,7 +3366,7 @@ public FunctionSignatures getSignatures() { @api @seealso({com.laytonsmith.tools.docgen.templates.Closures.class, execute.class}) - public static class execute_array extends AbstractFunction { + public static class execute_array extends CallbackYield { @Override public String getName() { return "execute_array"; @@ -3400,10 +3402,11 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { Mixed[] vals = ArgumentValidation.getArray(args[0], t).asList().toArray(new Mixed[0]); CClosure closure = ArgumentValidation.getObject(args[1], t, CClosure.class); - return closure.executeCallable(vals); + yield.call(closure, env, t, vals) + .then((result, y) -> y.done(() -> result)); } @Override @@ -3414,7 +3417,7 @@ public MSVersion since() { @api @seealso({com.laytonsmith.tools.docgen.templates.Closures.class}) - public static class executeas extends AbstractFunction implements Optimizable { + public static class executeas extends CallbackYield implements Optimizable { @Override public String getName() { @@ -3452,7 +3455,7 @@ public boolean isRestricted() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { if(!(args[args.length - 1].isInstanceOf(CClosure.TYPE, null, env))) { throw new CRECastException("Only a closure (created from the closure function) can be sent to executeas()", t); } @@ -3476,12 +3479,12 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. gEnv.SetLabel(args[1].val()); } - try { - return closure.executeCallable(vals); - } finally { - cEnv.SetCommandSender(originalSender); - gEnv.SetLabel(originalLabel); - } + yield.call(closure, env, t, vals) + .then((result, y) -> y.done(() -> result)) + .cleanup(() -> { + cEnv.SetCommandSender(originalSender); + gEnv.SetLabel(originalLabel); + }); } @Override diff --git a/src/test/java/com/laytonsmith/core/functions/ArrayHandlingTest.java b/src/test/java/com/laytonsmith/core/functions/ArrayHandlingTest.java index ccc78090ef..69d0c2b820 100644 --- a/src/test/java/com/laytonsmith/core/functions/ArrayHandlingTest.java +++ b/src/test/java/com/laytonsmith/core/functions/ArrayHandlingTest.java @@ -301,6 +301,30 @@ public void testAssociativeArraySort() throws Exception { verify(fakePlayer).sendMessage("{1, 002, 03}"); } + @Test + public void testArraySortCustomClosure() throws Exception { + Run("@a = array(3, 1, 4, 1, 5, 9);\n" + + "array_sort(@a, closure(@l, @r){ return(@l - @r); });\n" + + "msg(@a);", fakePlayer); + verify(fakePlayer).sendMessage("{1, 1, 3, 4, 5, 9}"); + } + + @Test + public void testArraySortCustomClosureReverse() throws Exception { + Run("@a = array(3, 1, 4, 1, 5, 9);\n" + + "array_sort(@a, closure(@l, @r){ return(@r - @l); });\n" + + "msg(@a);", fakePlayer); + verify(fakePlayer).sendMessage("{9, 5, 4, 3, 1, 1}"); + } + + @Test + public void testArraySortCustomClosureBoolean() throws Exception { + Run("@a = array(5, 2, 8, 1);\n" + + "array_sort(@a, closure(@l, @r){ return(@l > @r); });\n" + + "msg(@a);", fakePlayer); + verify(fakePlayer).sendMessage("{1, 2, 5, 8}"); + } + @Test public void testArrayImplode1() throws Exception { Run("msg(array_implode(array(1,2,3,4,5,6,7,8,9,1,2,3,4,5)))", fakePlayer); @@ -518,6 +542,51 @@ public void testArrayMap() throws Exception { verify(fakePlayer).sendMessage("{1, 16, 64}"); } + @Test + public void testArrayFilterNormal() throws Exception { + Run("@array = array(1, 2, 3, 4, 5);\n" + + "@odds = array_filter(@array, closure(@key, @value){\n" + + "\treturn(@value % 2 == 1);\n" + + "});\n" + + "msg(@odds);", fakePlayer); + verify(fakePlayer).sendMessage("{1, 3, 5}"); + } + + @Test + public void testArrayFilterAssociative() throws Exception { + Run("@array = array(a: 1, b: 2, c: 3);\n" + + "@result = array_filter(@array, closure(@key, @value){\n" + + "\treturn(@value > 1);\n" + + "});\n" + + "msg(@result);", fakePlayer); + verify(fakePlayer).sendMessage("{b: 2, c: 3}"); + } + + @Test + public void testArrayFilterEmpty() throws Exception { + Run("@array = array();\n" + + "@result = array_filter(@array, closure(@key, @value){\n" + + "\treturn(true);\n" + + "});\n" + + "msg(@result);", fakePlayer); + verify(fakePlayer).sendMessage("{}"); + } + + @Test + public void testArrayReduceEmpty() throws Exception { + assertEquals("null", SRun("array_reduce(array(), closure(@a, @b){ return(@a + @b); })", fakePlayer)); + } + + @Test + public void testArrayReduceSingle() throws Exception { + assertEquals("5", SRun("array_reduce(array(5), closure(@a, @b){ return(@a + @b); })", fakePlayer)); + } + + @Test + public void testArrayReduceRightSingle() throws Exception { + assertEquals("5", SRun("array_reduce_right(array(5), closure(@a, @b){ return(@a + @b); })", fakePlayer)); + } + @Test public void testArrayReverse() throws Exception { Run("@array = array(1, 2, 3, 4);\n" @@ -557,6 +626,38 @@ public void testArrayIntersect() throws Exception { } + @Test + public void testArraySubtractHash() throws Exception { + assertThat(SRun("array_subtract(array(1, 2, 3, 4), array(2, 4))", fakePlayer), is("{1, 3}")); + } + + @Test + public void testArraySubtractEquals() throws Exception { + assertThat(SRun("array_subtract(array(1, 2, 3), array(2, 3, 4), 'EQUALS')", fakePlayer), is("{1}")); + } + + @Test + public void testArraySubtractAssociative() throws Exception { + assertThat(SRun("array_subtract(array(a: 1, b: 2, c: 3), array(b: 99, d: 4))", fakePlayer), is("{a: 1, c: 3}")); + } + + @Test + public void testArraySubtractClosure() throws Exception { + assertThat(SRun("array_subtract(array(1, 2, 3, 4), array(10, 20), closure(@a, @b){ return(@a * 10 == @b); })", + fakePlayer), is("{3, 4}")); + } + + @Test + public void testArraySubtractNoOverlap() throws Exception { + assertThat(SRun("array_subtract(array(1, 2, 3), array(4, 5, 6))", fakePlayer), is("{1, 2, 3}")); + } + + @Test + public void testArrayIntersectClosurePartial() throws Exception { + assertThat(SRun("array_intersect(array(1, 2, 3, 4), array(10, 30), closure(@a, @b){ return(@a * 10 == @b); })", + fakePlayer), is("{1, 3}")); + } + @Test public void testMapImplode() throws Exception { assertThat(SRun("map_implode(array('a': 'b'), '=', '&')", null), is("a=b")); diff --git a/src/test/java/com/laytonsmith/core/functions/DataHandlingTest.java b/src/test/java/com/laytonsmith/core/functions/DataHandlingTest.java index 62124fbe23..6e1ce43554 100644 --- a/src/test/java/com/laytonsmith/core/functions/DataHandlingTest.java +++ b/src/test/java/com/laytonsmith/core/functions/DataHandlingTest.java @@ -340,6 +340,27 @@ public void testClosureReturnsFromExecute() throws Exception { assertEquals("3", SRun("execute(closure(return(3)))", fakePlayer)); } + @Test + public void testExecuteArray() throws Exception { + assertEquals("3", SRun("execute_array(array(1, 2), closure(@a, @b){ return(@a + @b); })", fakePlayer)); + } + + @Test + public void testExecuteArrayEmpty() throws Exception { + assertEquals("hello", SRun("execute_array(array(), closure(){ return('hello'); })", fakePlayer)); + } + + @Test + public void testExecuteasRestoresContext() throws Exception { + MCPlayer fakePlayer2 = StaticTest.GetOnlinePlayer("Player02", fakeServer); + when(fakeServer.getPlayer("Player02")).thenReturn(fakePlayer2); + SRun("@c = closure(){msg(player())};\n" + + "executeas('Player02', null, @c);\n" + + "msg(player());", fakePlayer); + verify(fakePlayer2).sendMessage("Player02"); + verify(fakePlayer).sendMessage(fakePlayer.getName()); + } + @Test public void testEmptyClosureFunction() throws Exception { // This should not throw an exception From b9a9012ba79487c6296fbbccc497e925bc3329d1 Mon Sep 17 00:00:00 2001 From: LadyCailin Date: Mon, 9 Mar 2026 11:53:20 +0100 Subject: [PATCH 5/5] Finish converting CallbackYield and FlowFunctions --- .../com/laytonsmith/core/CallbackYield.java | 20 +-- .../java/com/laytonsmith/core/Procedure.java | 20 +++ .../laytonsmith/core/constructs/CClosure.java | 9 + .../core/constructs/ProcedureUsage.java | 5 + .../core/functions/CompositeFunction.java | 156 ++++++++++++------ .../core/functions/ControlFlow.java | 21 ++- .../com/laytonsmith/core/functions/Regex.java | 58 +++++-- .../core/natives/interfaces/Callable.java | 26 +++ 8 files changed, 231 insertions(+), 84 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/CallbackYield.java b/src/main/java/com/laytonsmith/core/CallbackYield.java index 61d3dab121..d1e1b9f3b1 100644 --- a/src/main/java/com/laytonsmith/core/CallbackYield.java +++ b/src/main/java/com/laytonsmith/core/CallbackYield.java @@ -1,6 +1,5 @@ package com.laytonsmith.core; -import com.laytonsmith.core.constructs.CClosure; import com.laytonsmith.core.constructs.CVoid; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.constructs.generics.GenericParameters; @@ -166,21 +165,14 @@ private StepAction.StepResult drainNext(Target t, CallbackState s YieldStep step = yield.steps.poll(); state.currentStep = step; - // Prepare the closure execution - if(step.callable instanceof CClosure closure) { - CClosure.PreparedExecution prep = closure.prepareExecution(step.args); - if(prep == null) { - // Null node closure — result is void - if(step.callback != null) { - step.callback.accept(CVoid.VOID, yield); - } - return drainNext(t, state, env); - } - step.preparedEnv = prep.getEnv(); + // Try stack-based execution first (closures, procedures) + Callable.PreparedCallable prep = step.callable.prepareForStack(env, t, step.args); + if(prep != null) { + step.preparedEnv = prep.env(); return new StepAction.StepResult<>( - new StepAction.Evaluate(closure.getNode(), prep.getEnv()), state); + new StepAction.Evaluate(prep.node(), prep.env()), state); } else { - // Non-closure Callable (e.g. Method, CNativeClosure) — fall back to synchronous + // Sync-only Callable (e.g. CNativeClosure) — execute inline Mixed result = step.callable.executeCallable(env, t, step.args); if(step.callback != null) { step.callback.accept(result, yield); diff --git a/src/main/java/com/laytonsmith/core/Procedure.java b/src/main/java/com/laytonsmith/core/Procedure.java index 1b9cf37b94..d04fdd0099 100644 --- a/src/main/java/com/laytonsmith/core/Procedure.java +++ b/src/main/java/com/laytonsmith/core/Procedure.java @@ -27,6 +27,7 @@ import com.laytonsmith.core.functions.Function; import com.laytonsmith.core.functions.FunctionBase; import com.laytonsmith.core.functions.FunctionList; +import com.laytonsmith.core.natives.interfaces.Callable; import com.laytonsmith.core.natives.interfaces.Mixed; import java.util.ArrayList; @@ -246,6 +247,25 @@ public void definitelyNotConstant() { possiblyConstant = false; } + /** + * Prepares this procedure for stack-based execution without re-entering eval(). + * Clones the environment, binds arguments, and pushes a stack trace element. + * The caller is responsible for evaluating the returned tree in the returned + * environment, and for popping the stack trace element when done. + * + * @param args The evaluated argument values + * @param callerEnv The caller's environment (will be cloned) + * @param callTarget The target of the procedure call site + * @return The prepared call containing the procedure body tree and environment + */ + public Callable.PreparedCallable prepareCall(List args, Environment callerEnv, Target callTarget) { + Environment env = prepareEnvironment(args, callerEnv, callTarget); + StackTraceManager stManager = env.getEnv(GlobalEnv.class).GetStackTraceManager(); + stManager.addStackTraceElement( + new ConfigRuntimeException.StackTraceElement("proc " + name, getTarget())); + return new Callable.PreparedCallable(tree, env); + } + /** * Clones the environment and assigns procedure arguments (with type checking). * Used by both {@link #execute} and {@link ProcedureFlow}. diff --git a/src/main/java/com/laytonsmith/core/constructs/CClosure.java b/src/main/java/com/laytonsmith/core/constructs/CClosure.java index 3e853da26b..713b42d8ba 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CClosure.java +++ b/src/main/java/com/laytonsmith/core/constructs/CClosure.java @@ -234,6 +234,15 @@ public CClassType getReturnType() { } } + @Override + public Callable.PreparedCallable prepareForStack(Environment callerEnv, Target t, Mixed... values) { + PreparedExecution prep = prepareExecution(values); + if(prep == null) { + return null; + } + return new Callable.PreparedCallable(getNode(), prep.getEnv()); + } + @Override public CClosure clone() throws CloneNotSupportedException { CClosure clone = (CClosure) super.clone(); diff --git a/src/main/java/com/laytonsmith/core/constructs/ProcedureUsage.java b/src/main/java/com/laytonsmith/core/constructs/ProcedureUsage.java index 6528ed4c7d..fbbe96b65e 100644 --- a/src/main/java/com/laytonsmith/core/constructs/ProcedureUsage.java +++ b/src/main/java/com/laytonsmith/core/constructs/ProcedureUsage.java @@ -80,6 +80,11 @@ public Mixed executeCallable(Environment env, Target t, Mixed... values) throws return proc.execute(Arrays.asList(values), env, t); } + @Override + public Callable.PreparedCallable prepareForStack(Environment callerEnv, Target t, Mixed... values) { + return proc.prepareCall(Arrays.asList(values), callerEnv, t); + } + @Override public Environment getEnv() { return this.env; diff --git a/src/main/java/com/laytonsmith/core/functions/CompositeFunction.java b/src/main/java/com/laytonsmith/core/functions/CompositeFunction.java index cb129a3fa4..206a6c6a1f 100644 --- a/src/main/java/com/laytonsmith/core/functions/CompositeFunction.java +++ b/src/main/java/com/laytonsmith/core/functions/CompositeFunction.java @@ -1,12 +1,14 @@ package com.laytonsmith.core.functions; import com.laytonsmith.PureUtilities.Common.StreamUtils; -import com.laytonsmith.core.MSLog; +import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MethodScriptCompiler; import com.laytonsmith.core.NodeModifiers; import com.laytonsmith.core.ParseTree; import com.laytonsmith.core.Prefs; import com.laytonsmith.core.Script; +import com.laytonsmith.core.StepAction; +import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.analysis.ParamDeclaration; import com.laytonsmith.core.compiler.analysis.ReturnableDeclaration; import com.laytonsmith.core.compiler.analysis.Scope; @@ -36,42 +38,25 @@ * exist on a given platform, the function can be automatically provided on that platform. * This prevents rewrites for straightforward functions. */ -public abstract class CompositeFunction extends AbstractFunction { +public abstract class CompositeFunction extends AbstractFunction + implements FlowFunction { private static final Map, ParseTree> CACHED_SCRIPTS = new HashMap<>(); + static class CompositeState { + enum Phase { EVAL_ARGS, EVAL_BODY } + Phase phase = Phase.EVAL_ARGS; + ParseTree[] children; + Mixed[] evaluatedArgs; + int argIndex = 0; + IVariableList oldVariables; + } + @Override public final Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { - ParseTree tree; - // TODO: Ultimately, this is not scalable. We need to compile and cache these scripts at Java compile time, - // not at runtime the first time a function is used. This is an easier first step though. - File debugFile = null; - if(Prefs.DebugMode()) { - debugFile = new File("/NATIVE-MSCRIPT/" + getName()); - } - if(!CACHED_SCRIPTS.containsKey(this.getClass())) { - try { - - String script = script(); - Scope rootScope = new Scope(); - rootScope.addDeclaration(new ParamDeclaration("@arguments", CArray.TYPE, null, - new NodeModifiers(), - Target.UNKNOWN)); - rootScope.addDeclaration(new ReturnableDeclaration(null, new NodeModifiers(), Target.UNKNOWN)); - tree = MethodScriptCompiler.compile(MethodScriptCompiler.lex(script, env, debugFile, true), - env, env.getEnvClasses(), new StaticAnalysis(rootScope, true)) - // the root of the tree is null, so go ahead and pull it up - .getChildAt(0); - } catch (ConfigCompileException | ConfigCompileGroupException ex) { - // This is really bad. - throw new Error(ex); - } - if(cacheCompile()) { - CACHED_SCRIPTS.put(this.getClass(), tree); - } - } else { - tree = CACHED_SCRIPTS.get(this.getClass()); - } + // Sync fallback for compile-time optimization (CONSTANT_OFFLINE). + // The FlowFunction path is used during normal interpretation. + ParseTree tree = getOrCompileTree(env); GlobalEnv gEnv = env.getEnv(GlobalEnv.class); IVariableList oldVariables = gEnv.GetVarList(); @@ -83,28 +68,105 @@ public final Mixed exec(Target t, Environment env, GenericParameters generics, M if(gEnv.GetScript() != null) { ret = gEnv.GetScript().eval(tree, env); } else { - // This can happen when the environment is not fully setup during tests, in addition to optimization ret = Script.GenerateScript(null, null, null).eval(tree, env); } - } catch (ConfigRuntimeException ex) { - if(Prefs.DebugMode()) { - MSLog.GetLogger().e(MSLog.Tags.GENERAL, "Possibly false stacktrace, could be internal error", - ex.getTarget()); - } - if(gEnv.GetStackTraceManager().getCurrentStackTrace().isEmpty()) { - ex.setTarget(t); - ConfigRuntimeException.StackTraceElement ste = new ConfigRuntimeException - .StackTraceElement(this.getName(), t); - gEnv.GetStackTraceManager().addStackTraceElement(ste); - } - gEnv.GetStackTraceManager().setCurrentTarget(t); - throw ex; + } finally { + gEnv.SetVarList(oldVariables); } - gEnv.SetVarList(oldVariables); return ret; } + @Override + public StepResult begin(Target t, ParseTree[] children, Environment env) { + CompositeState state = new CompositeState(); + state.children = children; + state.evaluatedArgs = new Mixed[children.length]; + if(children.length > 0) { + return new StepResult<>(new StepAction.Evaluate(children[0]), state); + } else { + return evalBody(t, state, env); + } + } + + @Override + public StepResult childCompleted(Target t, CompositeState state, Mixed result, Environment env) { + if(state.phase == CompositeState.Phase.EVAL_ARGS) { + state.evaluatedArgs[state.argIndex] = result; + state.argIndex++; + if(state.argIndex < state.children.length) { + return new StepResult<>(new StepAction.Evaluate(state.children[state.argIndex]), state); + } + return evalBody(t, state, env); + } else { + // Body evaluation complete + return new StepResult<>(new StepAction.Complete(result), state); + } + } + + @Override + public StepResult childInterrupted(Target t, CompositeState state, + StepAction.FlowControl action, Environment env) { + if(state.phase == CompositeState.Phase.EVAL_BODY + && action.getAction() instanceof ControlFlow.ReturnAction ret) { + return new StepResult<>(new StepAction.Complete(ret.getValue()), state); + } + return null; + } + + @Override + public void cleanup(Target t, CompositeState state, Environment env) { + if(state != null && state.oldVariables != null) { + env.getEnv(GlobalEnv.class).SetVarList(state.oldVariables); + } + } + + private StepResult evalBody(Target t, CompositeState state, Environment env) { + state.phase = CompositeState.Phase.EVAL_BODY; + ParseTree tree = getOrCompileTree(env); + + GlobalEnv gEnv = env.getEnv(GlobalEnv.class); + state.oldVariables = gEnv.GetVarList(); + IVariableList newVariables = new IVariableList(state.oldVariables); + newVariables.set(new IVariable(CArray.TYPE, "@arguments", + new CArray(t, state.evaluatedArgs.length, state.evaluatedArgs), t)); + gEnv.SetVarList(newVariables); + + return new StepResult<>(new StepAction.Evaluate(tree), state); + } + + private ParseTree getOrCompileTree(Environment env) { + if(CACHED_SCRIPTS.containsKey(this.getClass())) { + return CACHED_SCRIPTS.get(this.getClass()); + } + // TODO: Ultimately, this is not scalable. We need to compile and cache these scripts at Java compile time, + // not at runtime the first time a function is used. This is an easier first step though. + File debugFile = null; + if(Prefs.DebugMode()) { + debugFile = new File("/NATIVE-MSCRIPT/" + getName()); + } + ParseTree tree; + try { + String script = script(); + Scope rootScope = new Scope(); + rootScope.addDeclaration(new ParamDeclaration("@arguments", CArray.TYPE, null, + new NodeModifiers(), + Target.UNKNOWN)); + rootScope.addDeclaration(new ReturnableDeclaration(null, new NodeModifiers(), Target.UNKNOWN)); + tree = MethodScriptCompiler.compile(MethodScriptCompiler.lex(script, env, debugFile, true), + env, env.getEnvClasses(), new StaticAnalysis(rootScope, true)) + // the root of the tree is null, so go ahead and pull it up + .getChildAt(0); + } catch(ConfigCompileException | ConfigCompileGroupException ex) { + // This is really bad. + throw new Error(ex); + } + if(cacheCompile()) { + CACHED_SCRIPTS.put(this.getClass(), tree); + } + return tree; + } + /** * The script that will be compiled and run when this function is executed. The value array @arguments will be set * with the function inputs. Variables set in this script will not leak to the actual script environment, but in diff --git a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java index 101fe6b4c4..ba8c61dcff 100644 --- a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java +++ b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java @@ -9,6 +9,7 @@ import com.laytonsmith.annotations.noboilerplate; import com.laytonsmith.annotations.seealso; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.CallbackYield; import com.laytonsmith.core.FlowFunction; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.LogLevel; @@ -56,6 +57,7 @@ import com.laytonsmith.core.constructs.Construct; import com.laytonsmith.core.constructs.IVariable; import com.laytonsmith.core.constructs.InstanceofUtil; +import com.laytonsmith.core.constructs.ProcedureUsage; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.constructs.generics.GenericParameters; import com.laytonsmith.core.environments.CommandHelperEnvironment; @@ -3180,7 +3182,7 @@ public FunctionSignatures getSignatures() { } @api - public static class call_proc extends AbstractFunction implements Optimizable { + public static class call_proc extends CallbackYield implements Optimizable { @Override public String getName() { @@ -3223,17 +3225,18 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, Yield yield) { 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 vars = new ArrayList<>(Arrays.asList(args)); - vars.remove(0); - return proc.execute(vars, env, t); + if(proc == null) { + throw new CREInvalidProcedureException("Unknown procedure \"" + args[0].val() + "\"", t); } - throw new CREInvalidProcedureException("Unknown procedure \"" + args[0].val() + "\"", t); + ProcedureUsage procUsage = new ProcedureUsage(proc, env, t); + Mixed[] procArgs = Arrays.copyOfRange(args, 1, args.length); + yield.call(procUsage, env, t, procArgs) + .then((result, y) -> y.done(() -> result)); } @Override @@ -3273,7 +3276,7 @@ public ParseTree optimizeDynamic(Target t, Environment env, public static class call_proc_array extends call_proc { @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, Yield yield) { CArray ca = ArgumentValidation.getArray(args[1], t, env); if(ca.inAssociativeMode()) { throw new CRECastException("Expected the array passed to " + getName() + " to be non-associative.", t); @@ -3284,7 +3287,7 @@ public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed.. args2[i] = ca.get(i - 1, t, env); } // TODO: This probably needs to change once generics are added - return super.exec(t, env, null, args2); + super.execWithYield(t, env, args2, yield); } @Override diff --git a/src/main/java/com/laytonsmith/core/functions/Regex.java b/src/main/java/com/laytonsmith/core/functions/Regex.java index 6f68185c89..0b87e7e6d1 100644 --- a/src/main/java/com/laytonsmith/core/functions/Regex.java +++ b/src/main/java/com/laytonsmith/core/functions/Regex.java @@ -4,6 +4,7 @@ import com.laytonsmith.annotations.core; import com.laytonsmith.annotations.seealso; import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.CallbackYield; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.ObjectGenerator; import com.laytonsmith.core.Optimizable; @@ -231,7 +232,7 @@ public ExampleScript[] examples() throws ConfigCompileException { } @api - public static class reg_replace extends AbstractFunction implements Optimizable { + public static class reg_replace extends CallbackYield implements Optimizable { @Override public String getName() { @@ -272,26 +273,55 @@ public Boolean runAsync() { } @Override - public Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args) throws ConfigRuntimeException { + protected void execWithYield(Target t, Environment env, Mixed[] args, CallbackYield.Yield yield) { Pattern pattern = getPattern(args[0], t, env); Mixed replacement = args[1]; String subject = args[2].val(); - String ret = ""; - try { - if(replacement instanceof Callable replacer) { - ret = pattern.matcher(subject).replaceAll(mr -> ArgumentValidation.getStringObject( - replacer.executeCallable(env, t, ObjectGenerator.GetGenerator().regMatchValue(mr, t)), t, env)); + if(replacement instanceof Callable replacer) { + // Collect all match positions upfront, then yield each closure call. + Matcher m = pattern.matcher(subject); + StringBuilder sb = new StringBuilder(); + int lastEnd = 0; + boolean found = false; + try { + while(m.find()) { + found = true; + final int segStart = lastEnd; + final int matchStart = m.start(); + CArray matchData = ObjectGenerator.GetGenerator().regMatchValue(m, t); + yield.call(replacer, env, t, matchData) + .then((result, y) -> { + sb.append(subject, segStart, matchStart); + sb.append(ArgumentValidation.getStringObject(result, t, env)); + }); + lastEnd = m.end(); + } + } catch(IndexOutOfBoundsException e) { + throw new CREFormatException("Expecting a regex group at parameter 1 of reg_replace", t); + } catch(IllegalArgumentException e) { + throw new CREFormatException(e.getMessage(), t); + } + if(!found) { + yield.done(() -> new CString(subject, t)); } else { - ret = pattern.matcher(subject).replaceAll(replacement.val()); + final int tail = lastEnd; + yield.done(() -> { + sb.append(subject, tail, subject.length()); + return new CString(sb.toString(), t); + }); + } + } else { + // Plain string replacement — no closures, synchronous. + try { + String ret = pattern.matcher(subject).replaceAll(replacement.val()); + yield.done(() -> new CString(ret, t)); + } catch(IndexOutOfBoundsException e) { + throw new CREFormatException("Expecting a regex group at parameter 1 of reg_replace", t); + } catch(IllegalArgumentException e) { + throw new CREFormatException(e.getMessage(), t); } - } catch (IndexOutOfBoundsException e) { - throw new CREFormatException("Expecting a regex group at parameter 1 of reg_replace", t); - } catch (IllegalArgumentException e) { - throw new CREFormatException(e.getMessage(), t); } - - return new CString(ret, t); } @Override diff --git a/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java b/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java index 82125f20fb..de8872b961 100644 --- a/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java +++ b/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java @@ -2,6 +2,7 @@ import com.laytonsmith.annotations.typeof; import com.laytonsmith.core.CallbackYield; +import com.laytonsmith.core.ParseTree; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.environments.Environment; @@ -41,4 +42,29 @@ Mixed executeCallable(Environment env, Target t, Mixed... values) * @return */ Environment getEnv(); + + /** + * Prepares this callable for evaluation on the shared EvalStack, without re-entering eval(). + * Returns a {@link PreparedCallable} containing the AST node and prepared environment, + * or {@code null} if this callable cannot be prepared (the caller should fall back to + * {@link #executeCallable}). + * + *

The caller is responsible for popping the stack trace element (via + * {@link com.laytonsmith.core.exceptions.StackTraceManager#popStackTraceElement()}) + * from the returned environment when done.

+ * + * @param callerEnv The caller's environment + * @param t The call site target + * @param values The argument values to bind + * @return A {@link PreparedCallable}, or null for sync-only callables + */ + default PreparedCallable prepareForStack(Environment callerEnv, Target t, Mixed... values) { + return null; + } + + /** + * The result of {@link #prepareForStack}. Contains the AST node to evaluate + * and the prepared environment with arguments bound. + */ + record PreparedCallable(ParseTree node, Environment env) {} }