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..d1e1b9f3b1 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/CallbackYield.java @@ -0,0 +1,357 @@ +package com.laytonsmith.core; + +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; + + // 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(prep.node(), prep.env()), state); + } else { + // 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); + } + 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) { + 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; + } + + /** + * 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(); + } + + /** + * 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 + * 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; + Runnable cleanupAction; + 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; + } + + /** + * 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() + + ", args=" + Arrays.toString(args) + ", hasCallback=" + (callback != null) + "}"; + } + } +} 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..79f26dd17e --- /dev/null +++ b/src/main/java/com/laytonsmith/core/FlowFunction.java @@ -0,0 +1,97 @@ +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.

+ * + *

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}. + * 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..d04fdd0099 100644 --- a/src/main/java/com/laytonsmith/core/Procedure.java +++ b/src/main/java/com/laytonsmith/core/Procedure.java @@ -20,14 +20,14 @@ 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.Callable; import com.laytonsmith.core.natives.interfaces.Mixed; import java.util.ArrayList; @@ -194,28 +194,106 @@ 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; + } + + /** + * 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}. + * + * @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 +304,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 +317,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 +328,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 +340,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..713b42d8ba 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CClosure.java +++ b/src/main/java/com/laytonsmith/core/constructs/CClosure.java @@ -3,12 +3,14 @@ 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; 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 +22,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; @@ -120,6 +118,131 @@ 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 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(); @@ -149,9 +272,11 @@ public synchronized Environment getEnv() { * @param values * @return * @throws ConfigRuntimeException - * @throws ProgramFlowManipulationException * @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); } @@ -160,9 +285,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 +293,7 @@ public Mixed executeCallable(Mixed... values) {
 	 *	closure.execute();
 	 * } catch (ConfigRuntimeException e){
 	 *	ConfigRuntimeException.HandleUncaughtException(e);
-	 * } catch (ProgramFlowManipulationException e){
+	 * } catch (CancelCommandException e){
 	 *	// Ignored
 	 * }
 	 * 
@@ -181,31 +304,26 @@ 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 + * @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, 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 +332,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 +394,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..cac4583297 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java +++ b/src/main/java/com/laytonsmith/core/constructs/CNativeClosure.java @@ -2,11 +2,11 @@ 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; 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; @@ -38,8 +38,13 @@ 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, 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..fbbe96b65e 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,10 +76,15 @@ 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); } + @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/environments/GlobalEnv.java b/src/main/java/com/laytonsmith/core/environments/GlobalEnv.java index 704b1a9d72..484409343c 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. * @@ -377,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; @@ -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/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/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..da19cca24f 100644 --- a/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/ArrayHandling.java @@ -11,10 +11,15 @@ 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; 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 +54,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; @@ -67,6 +71,7 @@ import java.util.Random; import java.util.Set; import java.util.TreeSet; +import java.util.function.BiConsumer; @core public class ArrayHandling { @@ -387,7 +392,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 +406,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 @@ -1737,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() { @@ -1755,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); } @@ -1763,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) { @@ -1778,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 @@ -2536,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() { @@ -2554,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)) { @@ -2570,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 @@ -2780,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() { @@ -2798,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; @@ -2807,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(ProgramFlowManipulationException ex) { - // Ignored - } + yield.call(closure, env, t, key, aa.get(key, t, env)); } - return aa; + yield.done(() -> aa); } @Override @@ -2858,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() { @@ -2876,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 @@ -2943,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() { @@ -2961,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 @@ -3028,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() { @@ -3046,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 @@ -3101,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() { @@ -3119,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 @@ -3174,7 +3285,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() { @@ -3192,21 +3303,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 @@ -3270,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() { @@ -3288,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; @@ -3320,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)]; @@ -3336,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 @@ -3662,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() { @@ -3680,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; @@ -3712,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)]; @@ -3728,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(); } } } @@ -3758,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/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..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; @@ -22,7 +24,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; @@ -37,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(); @@ -82,32 +66,107 @@ 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); - } - } 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", - ex.getTarget()); - } - if(gEnv.GetStackTraceManager().getCurrentStackTrace().isEmpty()) { - ex.setTarget(t); - ConfigRuntimeException.StackTraceElement ste = new ConfigRuntimeException - .StackTraceElement(this.getName(), t); - gEnv.GetStackTraceManager().addStackTraceElement(ste); + ret = Script.GenerateScript(null, null, null).eval(tree, env); } - 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 @@ -139,15 +198,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..ba8c61dcff 100644 --- a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java +++ b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java @@ -9,12 +9,18 @@ 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; 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; @@ -51,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; @@ -73,9 +80,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 +102,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 +224,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 +339,6 @@ public Boolean runAsync() { return false; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public Set optimizationOptions() { return EnumSet.of( @@ -361,7 +453,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 +552,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 +561,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 +702,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 +897,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 +946,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 +1255,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 +1415,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 +1465,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 +1578,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 +1603,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 +1613,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 +1671,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 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; - } + 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(); } - 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 +2028,6 @@ public Boolean runAsync() { return null; } - @Override - public boolean useSpecialExec() { - return true; - } - @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ @@ -1911,27 +2215,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); - } - - 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); + public StepResult begin(Target t, ParseTree[] children, Environment env) { + if(children.length < 4) { + throw new CREInsufficientArgumentsException( + "Insufficient arguments passed to " + getName(), t); } - - 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 +2338,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 +2450,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 +2458,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 +2540,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 +2664,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 +2733,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 +2742,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 +2867,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 +2923,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 +2932,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 +3025,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 +3049,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 +3058,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 +3170,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 @@ -2771,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() { @@ -2814,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 @@ -2864,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); @@ -2875,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/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java index c38bc9bd09..6522a8f12c 100644 --- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java @@ -13,7 +13,9 @@ 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; import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.MSLog; @@ -26,6 +28,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 +92,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 +1503,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 +1742,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 +1810,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 +1962,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 +2223,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 +2268,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); } - throw new CREIncludeException(b.toString(), 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); + } + 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 +2415,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 +2929,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 +3092,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 +3162,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 +3200,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 +3230,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) { @@ -3091,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"; @@ -3133,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); @@ -3162,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"; @@ -3198,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 @@ -3212,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() { @@ -3250,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); } @@ -3274,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 @@ -3822,7 +4027,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 +4063,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/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/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..de8872b961 100644 --- a/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java +++ b/src/main/java/com/laytonsmith/core/natives/interfaces/Callable.java @@ -1,12 +1,13 @@ package com.laytonsmith.core.natives.interfaces; 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; 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,24 +22,49 @@ 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 + * @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, ProgramFlowManipulationException, CancelCommandException; + throws ConfigRuntimeException, CancelCommandException; /** * Returns the environment associated with this callable. * @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) {} } 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/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/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..6e1ce43554 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(); @@ -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 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/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); + } + } + } 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");