Skip to content

Commit 091d8b4

Browse files
committed
Convert to an iterative eval loop instead of a recursive one.
This is a major change in the script evaluation process, which changes how "special execution" functions work. Previously, functions could choose to implement execs instead of exec, which received a ParseTree, instead of Mixed. This allowed the individual function to decide how or even if the ParseTree nodes were further executed. This works in general, however it has several drawbacks. In particular, the core evaluation loop loses control over the script once it decends into individual functions. Therefore features like Ctrl+C in command line scripts relied on each of these "flow" functions to implement that feature correctly, and only some of them did. This also prevents new features from being implemented as easily, like a debugger, since the evaluation loop would need to be modified, and every single flow function would need to make the same changes as well. This also has several performance benefits. Using a recursive approach meant that each frame of MethodScript had about 3 Java frames, which is inefficient. The biggest performance change with this is moving away from exception based control flow. Previously, return, break, and continue were all implemented with Java exceptions. This is way more expensive than it needs to be, especially for very unexecptional cases such as return(). Now, when a proc or closure returns, it triggers a different phase in the state machine, instead of throwing an exception. This also unlocks future features that were not possible today. A debugger could have been implemented before (though it would have been difficult) but now an asynchronous debugger can be implemented. async/await is also possible now. Tail call optimizations can be done, execution time quotas, and the profiler can probably be improved.
1 parent f14c8e5 commit 091d8b4

50 files changed

Lines changed: 3557 additions & 1933 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.laytonsmith.core;
2+
3+
import java.util.ArrayDeque;
4+
import java.util.Iterator;
5+
6+
/**
7+
* A wrapper around an {@link ArrayDeque} of {@link StackFrame}s that provides a debugger-friendly
8+
* {@link #toString()} showing the current execution stack in the style of MethodScript stack traces.
9+
*/
10+
public class EvalStack implements Iterable<StackFrame> {
11+
12+
private final ArrayDeque<StackFrame> stack;
13+
14+
public EvalStack() {
15+
this.stack = new ArrayDeque<>();
16+
}
17+
18+
public void push(StackFrame frame) {
19+
stack.push(frame);
20+
}
21+
22+
public StackFrame pop() {
23+
return stack.pop();
24+
}
25+
26+
public StackFrame peek() {
27+
return stack.peek();
28+
}
29+
30+
public boolean isEmpty() {
31+
return stack.isEmpty();
32+
}
33+
34+
public int size() {
35+
return stack.size();
36+
}
37+
38+
@Override
39+
public Iterator<StackFrame> iterator() {
40+
return stack.iterator();
41+
}
42+
43+
@Override
44+
public String toString() {
45+
if(stack.isEmpty()) {
46+
return "<empty stack>";
47+
}
48+
StringBuilder sb = new StringBuilder();
49+
boolean first = true;
50+
Iterator<StackFrame> it = stack.descendingIterator();
51+
while(it.hasNext()) {
52+
if(!first) {
53+
sb.append("\n");
54+
}
55+
sb.append("at ").append(it.next().toString());
56+
first = false;
57+
}
58+
return sb.toString();
59+
}
60+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.laytonsmith.core;
2+
3+
import com.laytonsmith.core.StepAction.StepResult;
4+
import com.laytonsmith.core.constructs.Target;
5+
import com.laytonsmith.core.natives.interfaces.Mixed;
6+
import com.laytonsmith.core.environments.Environment;
7+
8+
/**
9+
* Interface for functions that need to control how their children are evaluated.
10+
* This replaces the old {@code execs()} mechanism. Instead of a function calling
11+
* {@code parent.eval()} recursively, the interpreter loop calls the FlowFunction methods
12+
* and the function returns {@link StepAction} values to direct evaluation.
13+
*
14+
* <p>Functions that don't need special child evaluation (the majority) don't implement
15+
* this interface — the interpreter loop evaluates all their children left-to-right,
16+
* then calls {@code exec()} with the results.</p>
17+
*
18+
* <p>Functions that DO need it (if, for, and, try, etc.) implement this interface
19+
* and are driven by the interpreter loop via begin/childCompleted/childInterrupted.</p>
20+
*
21+
* <p>The type parameter {@code S} is the per-call state type. Since function instances
22+
* are singletons, per-call mutable state cannot be stored on the function itself.
23+
* Instead, methods receive and return state via {@link StepAction.StepResult}.
24+
* The interpreter stores this state on the {@link StackFrame} as {@code Object} and
25+
* passes it back (with an unchecked cast) on subsequent calls. Functions that need
26+
* no per-call state should use {@code Void} and pass {@code null}.</p>
27+
*/
28+
public interface FlowFunction<S> {
29+
30+
/**
31+
* Called when this function frame is first entered. The function should return
32+
* a {@link StepAction.StepResult} containing the first action (typically
33+
* {@link StepAction.Evaluate}) and the initial per-call state.
34+
*
35+
* @param t The code target of the function call
36+
* @param children The unevaluated child parse trees (same as what execs() received)
37+
* @param env The current environment
38+
* @return The first step action paired with initial state
39+
*/
40+
StepResult<S> begin(Target t, ParseTree[] children, Environment env);
41+
42+
/**
43+
* Called each time a child evaluation (requested via {@link StepAction.Evaluate})
44+
* completes successfully. The function receives the result and its per-call state,
45+
* and returns the next action paired with updated state.
46+
*
47+
* @param t The code target of the function call
48+
* @param state The per-call state from the previous step
49+
* @param result The result of the child evaluation
50+
* @param env The current environment
51+
* @return The next step action paired with updated state
52+
*/
53+
StepResult<S> childCompleted(Target t, S state, Mixed result, Environment env);
54+
55+
/**
56+
* Called when a child evaluation produced a {@link StepAction.FlowControl} action
57+
* that is propagating up the stack. The function can choose to handle it (e.g.,
58+
* a loop handling a break action) or let it propagate by returning {@code null}.
59+
*
60+
* <p>For example, {@code _for}'s implementation handles {@code BreakAction} by completing
61+
* the loop, and handles {@code ContinueAction} by jumping to the increment step.
62+
* {@code _try}'s implementation handles {@code ThrowAction} by switching to the catch branch.</p>
63+
*
64+
* <p>The default implementation returns {@code null}, propagating the action up.</p>
65+
*
66+
* @param t The code target of the function call
67+
* @param state The per-call state from the previous step
68+
* @param action The flow control action propagating through this frame
69+
* @param env The current environment
70+
* @return A {@link StepAction.StepResult} to handle it, or {@code null} to propagate
71+
*/
72+
default StepResult<S> childInterrupted(Target t, S state, StepAction.FlowControl action, Environment env) {
73+
return null;
74+
}
75+
76+
/**
77+
* Called when this function's frame is being removed from the stack, regardless of
78+
* the reason (normal completion, flow control propagation, or exception). This is
79+
* the FlowFunction equivalent of a {@code finally} block — use it to restore
80+
* environment state that was modified in {@code begin()} (e.g., command sender,
81+
* dynamic scripting mode, stack trace elements).
82+
*
83+
* <p>This is called exactly once per frame, after the final action has been determined
84+
* but before the frame is actually popped. The default implementation is a no-op.</p>
85+
*
86+
* @param t The code target of the function call
87+
* @param state The per-call state (may be null if begin() hasn't been called)
88+
* @param env The current environment
89+
*/
90+
default void cleanup(Target t, S state, Environment env) {
91+
}
92+
}

src/main/java/com/laytonsmith/core/LocalPackages.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.laytonsmith.core.exceptions.ConfigCompileException;
1010
import com.laytonsmith.core.exceptions.ConfigCompileGroupException;
1111
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
12-
import com.laytonsmith.core.exceptions.ProgramFlowManipulationException;
1312
import com.laytonsmith.core.functions.IncludeCache;
1413
import com.laytonsmith.core.profiler.ProfilePoint;
1514

@@ -223,9 +222,6 @@ public void executeMS(Environment env) {
223222
if(e.getMessage() != null && !e.getMessage().trim().isEmpty()) {
224223
Static.getLogger().log(Level.INFO, e.getMessage());
225224
}
226-
} catch (ProgramFlowManipulationException e) {
227-
ConfigRuntimeException.HandleUncaughtException(ConfigRuntimeException.CreateUncatchableException(
228-
"Cannot break program flow in main files.", e.getTarget()), env);
229225
}
230226
}
231227
}

src/main/java/com/laytonsmith/core/Method.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.laytonsmith.core.environments.Environment;
1010
import com.laytonsmith.core.exceptions.CancelCommandException;
1111
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
12-
import com.laytonsmith.core.exceptions.ProgramFlowManipulationException;
1312
import com.laytonsmith.core.natives.interfaces.Mixed;
1413
import java.util.Arrays;
1514

@@ -40,7 +39,7 @@ public Method(Target t, Environment env, CClassType returnType, String name, CCl
4039

4140
@Override
4241
public Mixed executeCallable(Environment env, Target t, Mixed... values)
43-
throws ConfigRuntimeException, ProgramFlowManipulationException, CancelCommandException {
42+
throws ConfigRuntimeException, CancelCommandException {
4443
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
4544
}
4645

src/main/java/com/laytonsmith/core/MethodScriptCompiler.java

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
import java.util.EmptyStackException;
7878
import java.util.EnumSet;
7979
import java.util.HashMap;
80+
import java.util.IdentityHashMap;
8081
import java.util.HashSet;
8182
import java.util.Iterator;
8283
import java.util.LinkedList;
@@ -3053,12 +3054,27 @@ public static Mixed execute(String script, File file, boolean inPureMScript, Env
30533054
* @return
30543055
*/
30553056
public static Mixed execute(ParseTree root, Environment env, MethodScriptComplete done, Script script) {
3056-
return execute(root, env, done, script, null);
3057+
Mixed result;
3058+
if(root == null) {
3059+
result = CVoid.VOID;
3060+
} else {
3061+
if(script == null) {
3062+
script = new Script(null, null, env.getEnv(GlobalEnv.class).GetLabel(), env.getEnvClasses(),
3063+
root.getFileOptions(), null);
3064+
}
3065+
result = script.eval(root, env);
3066+
}
3067+
if(done != null) {
3068+
done.done(result.val().trim());
3069+
}
3070+
return result;
30573071
}
30583072

30593073
/**
30603074
* Executes a pre-compiled MethodScript, given the specified Script environment, but also provides a method to set
3061-
* the constants in the script.
3075+
* the constants in the script. The $variable bindings are resolved by walking the tree and mapping each Variable
3076+
* node's identity to its resolved value, then storing the map in the environment. See
3077+
* {@link GlobalEnv#SetDollarVarBindings} for details on why identity-based lookup is used.
30623078
*
30633079
* @param root
30643080
* @param env
@@ -3068,53 +3084,23 @@ public static Mixed execute(ParseTree root, Environment env, MethodScriptComplet
30683084
* @return
30693085
*/
30703086
public static Mixed execute(ParseTree root, Environment env, MethodScriptComplete done, Script script, List<Variable> vars) {
3071-
if(root == null) {
3072-
return CVoid.VOID;
3073-
}
3074-
if(script == null) {
3075-
script = new Script(null, null, env.getEnv(GlobalEnv.class).GetLabel(), env.getEnvClasses(),
3076-
root.getFileOptions(), null);
3077-
}
3078-
if(vars != null) {
3079-
Map<String, Variable> varMap = new HashMap<>();
3087+
if(root != null && vars != null && !vars.isEmpty()) {
3088+
Map<String, String> varValues = new HashMap<>();
30803089
for(Variable v : vars) {
3081-
varMap.put(v.getVariableName(), v);
3090+
varValues.put(v.getVariableName(), v.getDefault());
30823091
}
3092+
IdentityHashMap<Mixed, String> dollarBindings = new IdentityHashMap<>();
30833093
for(Mixed tempNode : root.getAllData()) {
30843094
if(tempNode instanceof Variable variable) {
3085-
Variable vv = varMap.get(variable.getVariableName());
3086-
if(vv != null) {
3087-
variable.setVal(vv.getDefault());
3088-
} else {
3089-
//The variable is unset. I'm not quite sure what cases would cause this
3090-
variable.setVal("");
3095+
String val = varValues.get(variable.getVariableName());
3096+
if(val != null) {
3097+
dollarBindings.put(tempNode, val);
30913098
}
30923099
}
30933100
}
3101+
env.getEnv(GlobalEnv.class).SetDollarVarBindings(dollarBindings);
30943102
}
3095-
StringBuilder b = new StringBuilder();
3096-
Mixed returnable = null;
3097-
for(ParseTree gg : root.getChildren()) {
3098-
Mixed retc = script.eval(gg, env);
3099-
if(root.numberOfChildren() == 1) {
3100-
returnable = retc;
3101-
if(done == null) {
3102-
// string builder is not needed, so return immediately
3103-
return returnable;
3104-
}
3105-
}
3106-
String ret = retc.val();
3107-
if(!ret.trim().isEmpty()) {
3108-
b.append(ret).append(" ");
3109-
}
3110-
}
3111-
if(done != null) {
3112-
done.done(b.toString().trim());
3113-
}
3114-
if(returnable != null) {
3115-
return returnable;
3116-
}
3117-
return Static.resolveConstruct(b.toString().trim(), Target.UNKNOWN);
3103+
return execute(root, env, done, script);
31183104
}
31193105

31203106
private static final List<Character> PDF_STACK = Arrays.asList(

0 commit comments

Comments
 (0)