Skip to content

Commit d9be3b3

Browse files
committed
Merge remote-tracking branch 'origin/master' into genericsTake2
2 parents 09947da + b846e13 commit d9be3b3

57 files changed

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

0 commit comments

Comments
 (0)