Skip to content

Commit 5d76c84

Browse files
committed
Add CallbackYield class for functions that execute callbacks.
Previously, callback invocations required re-entering the eval loop from the top, which defeats the purpose of the iterative loop. In principal, the functions that call Callables need to become flow functions to behave correctly, but for basic yield-style invocations, this infrastructure is too heavy, so CallbackYield is a new class which puts the function in terms of an exec-like mechanism, only introducing the Yield object, which is just a queue of operations, effectively. More functions need to convert to this, but as a first start, array_map has been converted. Some of the simpler FlowFunctions might be able to be simplified to this as well.
1 parent 31e38d8 commit 5d76c84

6 files changed

Lines changed: 489 additions & 10 deletions

File tree

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
package com.laytonsmith.core;
2+
3+
import com.laytonsmith.core.constructs.CClosure;
4+
import com.laytonsmith.core.constructs.CVoid;
5+
import com.laytonsmith.core.constructs.Target;
6+
import com.laytonsmith.core.constructs.generics.GenericParameters;
7+
import com.laytonsmith.core.environments.Environment;
8+
import com.laytonsmith.core.environments.GlobalEnv;
9+
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
10+
import com.laytonsmith.core.functions.AbstractFunction;
11+
import com.laytonsmith.core.functions.ControlFlow;
12+
import com.laytonsmith.core.natives.interfaces.Callable;
13+
import com.laytonsmith.core.natives.interfaces.Mixed;
14+
15+
import java.util.ArrayDeque;
16+
import java.util.Arrays;
17+
import java.util.Queue;
18+
import java.util.function.BiConsumer;
19+
import java.util.function.Supplier;
20+
21+
/**
22+
* Base class for functions that need to call closures/callables without re-entering
23+
* {@code eval()}. Subclasses implement {@link #execWithYield} instead of {@code exec()}.
24+
* The callback-style exec builds a chain of deferred steps via a {@link Yield} object,
25+
* which this class then drives as a {@link FlowFunction}.
26+
*
27+
* <p>The interpreter loop sees this as a FlowFunction and drives it via
28+
* begin/childCompleted/childInterrupted. The subclass never deals with those
29+
* methods — it just uses the Yield API.</p>
30+
*
31+
* <p>Example (array_map):</p>
32+
* <pre>
33+
* protected void execCallback(Target t, Environment env, Mixed[] args, Yield yield) {
34+
* CArray array = ArgumentValidation.getArray(args[0], t, env);
35+
* CClosure closure = ArgumentValidation.getObject(args[1], t, CClosure.class);
36+
* CArray newArray = new CArray(t, (int) array.size(env));
37+
*
38+
* for(Mixed key : array.keySet(env)) {
39+
* yield.call(closure, env, t, array.get(key, t, env))
40+
* .then((result, y) -&gt; {
41+
* newArray.set(key, result, t, env);
42+
* });
43+
* }
44+
* yield.done(() -&gt; newArray);
45+
* }
46+
* </pre>
47+
*/
48+
public abstract class CallbackYield extends AbstractFunction implements FlowFunction<CallbackYield.CallbackState> {
49+
50+
/**
51+
* Implement this instead of {@code exec()}. Use the {@link Yield} object to queue
52+
* closure calls and set the final result.
53+
*
54+
* @param t The code target
55+
* @param env The environment
56+
* @param args The evaluated arguments (same as what exec() would receive)
57+
* @param yield The yield object for queuing closure calls
58+
*/
59+
protected abstract void execWithYield(Target t, Environment env, Mixed[] args, Yield yield);
60+
61+
/**
62+
* Bridges the standard exec() interface to the callback mechanism. This is called by the
63+
* interpreter loop's simple-exec path, but since CallbackYield is also a FlowFunction,
64+
* the loop will use the FlowFunction path instead. This implementation exists only as a
65+
* fallback for external callers that invoke exec() directly (e.g. compile-time optimization).
66+
* In that case, closures are executed synchronously via executeCallable() as before.
67+
*/
68+
@Override
69+
public final Mixed exec(Target t, Environment env, GenericParameters generics, Mixed... args)
70+
throws ConfigRuntimeException {
71+
// Fallback: build the yield chain but execute closures synchronously.
72+
// This only runs when called outside the iterative interpreter loop.
73+
Yield yield = new Yield();
74+
execWithYield(t, env, args, yield);
75+
yield.executeSynchronously(env, t);
76+
return yield.getResult();
77+
}
78+
79+
@Override
80+
public StepAction.StepResult<CallbackState> begin(Target t, ParseTree[] children, Environment env) {
81+
// The interpreter has already evaluated all children (args) before recognizing
82+
// this as a FlowFunction. But actually — since CallbackYield extends AbstractFunction
83+
// AND implements FlowFunction, the loop will see instanceof FlowFunction and route
84+
// to the FlowFunction path. We need to evaluate args ourselves.
85+
// Start by evaluating the first child.
86+
CallbackState state = new CallbackState();
87+
if(children.length > 0) {
88+
state.children = children;
89+
state.argIndex = 0;
90+
return new StepAction.StepResult<>(new StepAction.Evaluate(children[0]), state);
91+
}
92+
// No args — run the callback immediately
93+
return runCallback(t, env, new Mixed[0], state);
94+
}
95+
96+
@Override
97+
public StepAction.StepResult<CallbackState> childCompleted(Target t, CallbackState state,
98+
Mixed result, Environment env) {
99+
// Phase 1: collecting args
100+
if(!state.yieldStarted) {
101+
state.addArg(result);
102+
state.argIndex++;
103+
if(state.argIndex < state.children.length) {
104+
return new StepAction.StepResult<>(
105+
new StepAction.Evaluate(state.children[state.argIndex]), state);
106+
}
107+
// All args collected — run the callback
108+
return runCallback(t, env, state.getArgs(), state);
109+
}
110+
111+
// Phase 2: draining yield steps — a closure just completed
112+
YieldStep step = state.currentStep;
113+
if(step != null && step.callback != null) {
114+
step.callback.accept(result, state.yield);
115+
}
116+
return drainNext(t, state, env);
117+
}
118+
119+
@Override
120+
public StepAction.StepResult<CallbackState> childInterrupted(Target t, CallbackState state,
121+
StepAction.FlowControl action, Environment env) {
122+
StepAction.FlowControlAction fca = action.getAction();
123+
// A return() inside a closure is how it produces its result.
124+
if(fca instanceof ControlFlow.ReturnAction ret) {
125+
YieldStep step = state.currentStep;
126+
cleanupCurrentStep(state, env);
127+
if(step != null && step.callback != null) {
128+
step.callback.accept(ret.getValue(), state.yield);
129+
}
130+
return drainNext(t, state, env);
131+
}
132+
133+
cleanupCurrentStep(state, env);
134+
135+
// break/continue cannot escape a closure — this is a script error.
136+
if(fca instanceof ControlFlow.BreakAction || fca instanceof ControlFlow.ContinueAction) {
137+
throw ConfigRuntimeException.CreateUncatchableException(
138+
"Loop manipulation operations (e.g. break() or continue()) cannot"
139+
+ " bubble up past closures.", fca.getTarget());
140+
}
141+
142+
// ThrowAction and anything else — propagate
143+
return null;
144+
}
145+
146+
@Override
147+
public void cleanup(Target t, CallbackState state, Environment env) {
148+
if(state != null && state.currentStep != null) {
149+
cleanupCurrentStep(state, env);
150+
}
151+
}
152+
153+
private StepAction.StepResult<CallbackState> runCallback(Target t, Environment env,
154+
Mixed[] args, CallbackState state) {
155+
Yield yield = new Yield();
156+
state.yield = yield;
157+
state.yieldStarted = true;
158+
execWithYield(t, env, args, yield);
159+
return drainNext(t, state, env);
160+
}
161+
162+
private StepAction.StepResult<CallbackState> drainNext(Target t, CallbackState state,
163+
Environment env) {
164+
Yield yield = state.yield;
165+
if(!yield.steps.isEmpty()) {
166+
YieldStep step = yield.steps.poll();
167+
state.currentStep = step;
168+
169+
// Prepare the closure execution
170+
if(step.callable instanceof CClosure closure) {
171+
CClosure.PreparedExecution prep = closure.prepareExecution(step.args);
172+
if(prep == null) {
173+
// Null node closure — result is void
174+
if(step.callback != null) {
175+
step.callback.accept(CVoid.VOID, yield);
176+
}
177+
return drainNext(t, state, env);
178+
}
179+
step.preparedEnv = prep.getEnv();
180+
return new StepAction.StepResult<>(
181+
new StepAction.Evaluate(closure.getNode(), prep.getEnv()), state);
182+
} else {
183+
// Non-closure Callable (e.g. Method, CNativeClosure) — fall back to synchronous
184+
Mixed result = step.callable.executeCallable(env, t, step.args);
185+
if(step.callback != null) {
186+
step.callback.accept(result, yield);
187+
}
188+
return drainNext(t, state, env);
189+
}
190+
}
191+
192+
// All steps drained
193+
return new StepAction.StepResult<>(
194+
new StepAction.Complete(yield.getResult()), state);
195+
}
196+
197+
private void cleanupCurrentStep(CallbackState state, Environment env) {
198+
YieldStep step = state.currentStep;
199+
if(step != null && step.preparedEnv != null) {
200+
// Pop the stack trace element that prepareExecution pushed
201+
step.preparedEnv.getEnv(GlobalEnv.class).GetStackTraceManager().popStackTraceElement();
202+
step.preparedEnv = null;
203+
}
204+
state.currentStep = null;
205+
}
206+
207+
/**
208+
* Per-call state for the FlowFunction. Tracks argument collection and yield step draining.
209+
*/
210+
protected static class CallbackState {
211+
ParseTree[] children;
212+
int argIndex;
213+
private Mixed[] args;
214+
private int argCount;
215+
boolean yieldStarted;
216+
Yield yield;
217+
YieldStep currentStep;
218+
219+
void addArg(Mixed arg) {
220+
if(args == null) {
221+
args = new Mixed[children.length];
222+
}
223+
args[argCount++] = arg;
224+
}
225+
226+
Mixed[] getArgs() {
227+
if(args == null) {
228+
return new Mixed[0];
229+
}
230+
if(argCount < args.length) {
231+
Mixed[] trimmed = new Mixed[argCount];
232+
System.arraycopy(args, 0, trimmed, 0, argCount);
233+
return trimmed;
234+
}
235+
return args;
236+
}
237+
238+
@Override
239+
public String toString() {
240+
if(!yieldStarted) {
241+
return "CallbackState{collecting args: " + argCount + "/" + (children != null ? children.length : 0) + "}";
242+
}
243+
return "CallbackState{draining yields: " + (yield != null ? yield.steps.size() : 0) + " remaining}";
244+
}
245+
}
246+
247+
/**
248+
* The object passed to {@link #execWithYield}. Functions use this to queue closure calls
249+
* and declare the final result.
250+
*/
251+
public static class Yield {
252+
private final Queue<YieldStep> steps = new ArrayDeque<>();
253+
private Supplier<Mixed> resultSupplier = () -> CVoid.VOID;
254+
private boolean doneSet = false;
255+
256+
/**
257+
* Queue a closure/callable invocation.
258+
*
259+
* @param callable The closure or callable to invoke
260+
* @param env The environment (unused for closures, which capture their own)
261+
* @param t The target
262+
* @param args The arguments to pass to the callable
263+
* @return A {@link YieldStep} for chaining a {@code .then()} callback
264+
*/
265+
public YieldStep call(Callable callable, Environment env, Target t, Mixed... args) {
266+
YieldStep step = new YieldStep(callable, args);
267+
steps.add(step);
268+
return step;
269+
}
270+
271+
/**
272+
* Set the final result of this function via a supplier. The supplier is evaluated
273+
* after all yield steps have completed. This must be called exactly once.
274+
*
275+
* @param resultSupplier A supplier that returns the result value
276+
*/
277+
public void done(Supplier<Mixed> resultSupplier) {
278+
this.resultSupplier = resultSupplier;
279+
this.doneSet = true;
280+
}
281+
282+
Mixed getResult() {
283+
return resultSupplier.get();
284+
}
285+
286+
/**
287+
* Fallback for when CallbackYield functions are called outside the iterative
288+
* interpreter (e.g. during compile-time optimization). Drains all steps synchronously
289+
* by calling executeCallable directly.
290+
*/
291+
void executeSynchronously(Environment env, Target t) {
292+
while(!steps.isEmpty()) {
293+
YieldStep step = steps.poll();
294+
Mixed r = step.callable.executeCallable(env, t, step.args);
295+
if(step.callback != null) {
296+
step.callback.accept(r, this);
297+
}
298+
}
299+
}
300+
301+
@Override
302+
public String toString() {
303+
return "Yield{steps=" + steps.size() + ", doneSet=" + doneSet + "}";
304+
}
305+
}
306+
307+
/**
308+
* A single queued closure call with an optional continuation.
309+
*/
310+
public static class YieldStep {
311+
final Callable callable;
312+
final Mixed[] args;
313+
BiConsumer<Mixed, Yield> callback;
314+
Environment preparedEnv;
315+
316+
YieldStep(Callable callable, Mixed[] args) {
317+
this.callable = callable;
318+
this.args = args;
319+
}
320+
321+
/**
322+
* Register a callback to run after the closure completes.
323+
*
324+
* @param callback Receives the closure's return value and the Yield object
325+
* (for queuing additional steps or calling done())
326+
* @return This step, for fluent chaining
327+
*/
328+
public YieldStep then(BiConsumer<Mixed, Yield> callback) {
329+
this.callback = callback;
330+
return this;
331+
}
332+
333+
@Override
334+
public String toString() {
335+
return "YieldStep{callable=" + callable.getClass().getSimpleName()
336+
+ ", args=" + Arrays.toString(args) + ", hasCallback=" + (callback != null) + "}";
337+
}
338+
}
339+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
* <p>Functions that DO need it (if, for, and, try, etc.) implement this interface
1919
* and are driven by the interpreter loop via begin/childCompleted/childInterrupted.</p>
2020
*
21+
* <p>Functions that execute Callables MUST use this mechanism, however,
22+
* in most cases, it is sufficient to implement {@link CallbackYield} instead, which
23+
* is a specialized overload of this class, which hides most of the complexity
24+
* in the case where the only complexity is calling Callables.</p>
25+
*
2126
* <p>The type parameter {@code S} is the per-call state type. Since function instances
2227
* are singletons, per-call mutable state cannot be stored on the function itself.
2328
* Instead, methods receive and return state via {@link StepAction.StepResult}.

0 commit comments

Comments
 (0)