package com.xfestudio.xfeservermanager.core.trigger;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/**
* Resumable, platform-neutral Trigger Program V2 VM. It evaluates pure control flow and yields
* bounded side effects to the owning platform; no Minecraft/JVM object enters persisted state.
*/
public final class TriggerProgramExecutor {
private TriggerProgramExecutor() { }
public enum Status { YIELDED, WAITING, COMPLETED, FAILED }
public record Effect(UUID nodeId, String type, Map<String, Object> parameters,
String idempotencyKey) {
public Effect { parameters = immutable(parameters); }
}
public record CallFrame(int returnPc, Map<String, Object> callerVariables,
Map<String, String> outTargets, String returnTarget,
int handlerDepth) {
public CallFrame {
callerVariables = immutable(callerVariables);
outTargets = Map.copyOf(outTargets == null ? Map.of() : outTargets);
returnTarget = returnTarget == null ? "" : returnTarget;
}
}
public record LoopState(String kind, long index, long count, List<Object> values, String variable) {
public LoopState {
kind = Objects.requireNonNull(kind, "kind");
values = List.copyOf(values == null ? List.of() : values);
variable = variable == null ? "" : variable;
}
}
/** Entire continuation required to restart against the pinned trigger revision. */
public record State(
UUID executionId,
long seed,
String instant,
Map<String, Object> event,
Map<String, Object> variables,
int pc,
int instructions,
long randomCounter,
Map<String, Long> effectCounters,
List<CallFrame> callStack,
Map<String, LoopState> loops,
List<Integer> handlers) {
public State {
executionId = executionId == null ? UUID.randomUUID() : executionId;
instant = instant == null || instant.isBlank() ? Instant.EPOCH.toString() : instant;
event = immutable(event);
variables = immutable(variables);
effectCounters = Map.copyOf(effectCounters == null ? Map.of() : effectCounters);
callStack = List.copyOf(callStack == null ? List.of() : callStack);
loops = Map.copyOf(loops == null ? Map.of() : loops);
handlers = List.copyOf(handlers == null ? List.of() : handlers);
if (pc < 0 || instructions < 0 || randomCounter < 0) {
throw new IllegalArgumentException("VM counters must not be negative");
}
}
}
public record Result(Status status, State state, List<Effect> effects,
String error, UUID errorNodeId) {
public Result {
Objects.requireNonNull(status, "status");
Objects.requireNonNull(state, "state");
effects = List.copyOf(effects == null ? List.of() : effects);
error = error == null ? "" : error;
}
}
public static State start(TriggerProgramV2 program, Map<String, Object> event,
Map<String, Object> suppliedVariables, long seed, Instant instant) {
Objects.requireNonNull(program, "program");
Map<String, Object> variables = new LinkedHashMap<>(
suppliedVariables == null ? Map.of() : suppliedVariables);
TriggerExpressionEvaluator evaluator = new TriggerExpressionEvaluator(
event, variables, seed, instant, null, 0L);
for (TriggerProgramV2.VariableDeclaration declaration : program.declarations()) {
if (!variables.containsKey(declaration.name())) {
variables.put(declaration.name(), declaration.initialValue() == null
? null : evaluator.evaluate(declaration.initialValue()));
}
}
return new State(UUID.randomUUID(), seed, (instant == null ? Instant.EPOCH : instant).toString(),
event, variables, 0, 0, evaluator.randomCounter(), Map.of(), List.of(), Map.of(), List.of());
}
/** Runs until completion, a wait action, or the side-effect allowance is consumed. */
public static Result resume(TriggerProgramV2 program, State persisted, int maximumEffects) {
Objects.requireNonNull(program, "program");
Objects.requireNonNull(persisted, "state");
if (maximumEffects < 1 || maximumEffects > 64) {
throw new IllegalArgumentException("maximumEffects must be between 1 and 64");
}
Program bytecode = Compiler.compile(program);
Machine machine = new Machine(bytecode, persisted);
return machine.run(maximumEffects);
}
private enum Op {
ACTION, SET, JUMP, JUMP_FALSE, SWITCH, REPEAT_INIT, REPEAT_NEXT,
WHILE_GUARD, FOREACH_INIT, FOREACH_NEXT, CALL, RETURN,
PUSH_HANDLER, POP_HANDLER, END
}
private static final class Instruction {
private final Op op;
private final TriggerProgramV2.Statement statement;
private int target;
private int alternate;
private Map<UUID, Integer> cases = Map.of();
private Instruction(Op op, TriggerProgramV2.Statement statement) {
this.op = op;
this.statement = statement;
}
}
private record Program(List<Instruction> instructions,
Map<String, Integer> functions,
Map<String, TriggerProgramV2.FunctionDeclaration> declarations) { }
private record LoopTargets(int continuePc, List<Instruction> breaks) { }
private static final class Compiler {
private final TriggerProgramV2 source;
private final List<Instruction> instructions = new ArrayList<>();
private final Map<String, Integer> functions = new LinkedHashMap<>();
private final Map<String, TriggerProgramV2.FunctionDeclaration> declarations = new LinkedHashMap<>();
private Compiler(TriggerProgramV2 source) { this.source = source; }
private static Program compile(TriggerProgramV2 source) {
Compiler compiler = new Compiler(source);
source.functions().forEach(value -> compiler.declarations.put(value.name(), value));
compiler.statements(source.statements(), null);
compiler.add(Op.END, null);
for (TriggerProgramV2.FunctionDeclaration function : source.functions()) {
compiler.functions.put(function.name(), compiler.instructions.size());
compiler.statements(function.statements(), null);
compiler.add(Op.RETURN, null);
}
return new Program(List.copyOf(compiler.instructions), Map.copyOf(compiler.functions),
Map.copyOf(compiler.declarations));
}
private void statements(List<TriggerProgramV2.Statement> values, LoopTargets loop) {
for (TriggerProgramV2.Statement statement : values) statement(statement, loop);
}
private void statement(TriggerProgramV2.Statement statement, LoopTargets loop) {
switch (statement.kind()) {
case ACTION -> add(Op.ACTION, statement);
case SET -> add(Op.SET, statement);
case IF -> branch(statement, loop);
case SWITCH -> switchStatement(statement, loop);
case REPEAT -> repeat(statement);
case WHILE -> whileStatement(statement);
case FOREACH -> foreach(statement);
case BREAK -> {
if (loop == null) throw new IllegalArgumentException("break used outside a loop");
Instruction jump = add(Op.JUMP, statement);
loop.breaks().add(jump);
}
case CONTINUE -> {
if (loop == null) throw new IllegalArgumentException("continue used outside a loop");
Instruction jump = add(Op.JUMP, statement);
jump.target = loop.continuePc();
}
case CALL -> add(Op.CALL, statement);
case RETURN -> add(Op.RETURN, statement);
case TRY -> tryStatement(statement, loop);
}
}
private void branch(TriggerProgramV2.Statement statement, LoopTargets loop) {
Instruction condition = add(Op.JUMP_FALSE, statement);
statements(statement.statements(), loop);
Instruction end = add(Op.JUMP, statement);
condition.target = instructions.size();
statements(statement.elseStatements(), loop);
end.target = instructions.size();
}
private void switchStatement(TriggerProgramV2.Statement statement, LoopTargets loop) {
Instruction selector = add(Op.SWITCH, statement);
Map<UUID, Integer> targets = new LinkedHashMap<>();
List<Instruction> ends = new ArrayList<>();
for (TriggerProgramV2.SwitchCase branch : statement.cases()) {
targets.put(branch.nodeId(), instructions.size());
statements(branch.statements(), loop);
ends.add(add(Op.JUMP, statement));
}
selector.alternate = instructions.size();
statements(statement.elseStatements(), loop);
int end = instructions.size();
ends.forEach(value -> value.target = end);
selector.cases = Map.copyOf(targets);
}
private void repeat(TriggerProgramV2.Statement statement) {
Instruction initial = add(Op.REPEAT_INIT, statement);
int body = instructions.size();
Instruction nextMarker = new Instruction(Op.REPEAT_NEXT, statement);
LoopTargets loop = new LoopTargets(-1, new ArrayList<>());
statements(statement.statements(), loop);
int next = instructions.size();
nextMarker.target = body;
instructions.add(nextMarker);
int end = instructions.size();
initial.target = end;
loop.breaks().forEach(value -> value.target = end);
patchContinues(body, next, end);
}
private void whileStatement(TriggerProgramV2.Statement statement) {
int guardPc = instructions.size();
Instruction guard = add(Op.WHILE_GUARD, statement);
LoopTargets loop = new LoopTargets(guardPc, new ArrayList<>());
statements(statement.statements(), loop);
Instruction back = add(Op.JUMP, statement);
back.target = guardPc;
int end = instructions.size();
guard.target = end;
loop.breaks().forEach(value -> value.target = end);
}
private void foreach(TriggerProgramV2.Statement statement) {
Instruction initial = add(Op.FOREACH_INIT, statement);
int body = instructions.size();
LoopTargets loop = new LoopTargets(-1, new ArrayList<>());
statements(statement.statements(), loop);
int next = instructions.size();
Instruction advance = add(Op.FOREACH_NEXT, statement);
advance.target = body;
int end = instructions.size();
initial.target = end;
loop.breaks().forEach(value -> value.target = end);
patchContinues(body, next, end);
}
private void patchContinues(int from, int next, int end) {
for (int index = from; index < next; index++) {
Instruction value = instructions.get(index);
if (value.op == Op.JUMP && value.statement != null
&& value.statement.kind() == TriggerProgramV2.StatementKind.CONTINUE
&& value.target < 0) value.target = next;
}
}
private void tryStatement(TriggerProgramV2.Statement statement, LoopTargets loop) {
Instruction push = add(Op.PUSH_HANDLER, statement);
statements(statement.statements(), loop);
add(Op.POP_HANDLER, statement);
Instruction end = add(Op.JUMP, statement);
push.target = instructions.size();
statements(statement.elseStatements(), loop);
end.target = instructions.size();
}
private Instruction add(Op op, TriggerProgramV2.Statement statement) {
Instruction value = new Instruction(op, statement);
value.target = -1;
value.alternate = -1;
instructions.add(value);
return value;
}
}
private static final class Machine {
private final Program program;
private final UUID executionId;
private final long seed;
private final String instant;
private final Map<String, Object> event;
private final Map<String, Object> variables;
private final Map<String, Long> effectCounters;
private final List<CallFrame> callStack;
private final Map<String, LoopState> loops;
private final List<Integer> handlers;
private final List<Effect> effects = new ArrayList<>();
private int pc;
private int instructions;
private TriggerExpressionEvaluator evaluator;
private Machine(Program program, State state) {
this.program = program;
executionId = state.executionId();
seed = state.seed();
instant = state.instant();
event = new LinkedHashMap<>(state.event());
variables = new LinkedHashMap<>(state.variables());
effectCounters = new LinkedHashMap<>(state.effectCounters());
callStack = new ArrayList<>(state.callStack());
loops = new LinkedHashMap<>(state.loops());
handlers = new ArrayList<>(state.handlers());
pc = state.pc();
instructions = state.instructions();
rebuildEvaluator(state.randomCounter());
}
private void rebuildEvaluator(long counter) {
evaluator = new TriggerExpressionEvaluator(event, variables, seed, Instant.parse(instant), null, counter);
}
private Result run(int maximumEffects) {
UUID nodeId = null;
try {
while (pc < program.instructions().size()) {
Instruction instruction = program.instructions().get(pc);
nodeId = instruction.statement == null ? null : instruction.statement.nodeId();
if (++instructions > TriggerProgramValidator.MAX_INSTRUCTIONS) {
throw new IllegalStateException("instruction budget exceeded");
}
try {
Status status = execute(instruction);
if (status != null) return result(status, "", null);
if (effects.size() >= maximumEffects) return result(Status.YIELDED, "", null);
} catch (RuntimeException failure) {
if (handlers.isEmpty()) throw failure;
pc = handlers.remove(handlers.size() - 1);
variables.put("error.message", message(failure));
rebuildEvaluator(evaluator.randomCounter());
}
}
return result(Status.COMPLETED, "", null);
} catch (RuntimeException failure) {
return result(Status.FAILED, message(failure), nodeId);
}
}
private Status execute(Instruction instruction) {
TriggerProgramV2.Statement statement = instruction.statement;
switch (instruction.op) {
case ACTION -> {
Map<String, Object> parameters = evaluateInputs(statement.inputs());
long invocation = effectCounters.merge(statement.nodeId().toString(), 1L, Long::sum) - 1L;
effects.add(new Effect(statement.nodeId(), statement.name(), parameters,
executionId + ":" + statement.nodeId() + ":" + invocation));
pc++;
if (statement.name().equals("wait")) return Status.WAITING;
}
case SET -> {
variables.put(referenceName(statement.name()), evaluator.evaluate(statement.expression()));
pc++;
}
case JUMP -> pc = instruction.target;
case JUMP_FALSE -> pc = evaluator.evaluateBoolean(statement.expression())
? pc + 1 : instruction.target;
case SWITCH -> selectSwitch(instruction);
case REPEAT_INIT -> repeatInitial(instruction);
case REPEAT_NEXT -> repeatNext(instruction);
case WHILE_GUARD -> whileGuard(instruction);
case FOREACH_INIT -> foreachInitial(instruction);
case FOREACH_NEXT -> foreachNext(instruction);
case CALL -> call(instruction);
case RETURN -> returned(instruction);
case PUSH_HANDLER -> { handlers.add(instruction.target); pc++; }
case POP_HANDLER -> { if (!handlers.isEmpty()) handlers.remove(handlers.size() - 1); pc++; }
case END -> { return Status.COMPLETED; }
}
return null;
}
private void selectSwitch(Instruction instruction) {
Object selector = evaluator.evaluate(instruction.statement.expression());
for (TriggerProgramV2.SwitchCase branch : instruction.statement.cases()) {
if (equal(selector, evaluator.evaluate(branch.match()))) {
pc = instruction.cases.get(branch.nodeId());
return;
}
}
pc = instruction.alternate;
}
private void repeatInitial(Instruction instruction) {
long count = integral(evaluator.evaluate(instruction.statement.expression()));
if (count < 0 || count > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new IllegalArgumentException("repeat count exceeds loop budget");
}
String key = instruction.statement.nodeId().toString();
if (count == 0) { loops.remove(key); pc = instruction.target; return; }
loops.put(key, new LoopState("repeat", 0, count, List.of(), "loop.index"));
variables.put("loop.index", 0L);
pc++;
}
private void repeatNext(Instruction instruction) {
String key = instruction.statement.nodeId().toString();
LoopState state = requiredLoop(key);
long next = state.index() + 1;
if (next >= state.count()) { loops.remove(key); pc++; return; }
loops.put(key, new LoopState(state.kind(), next, state.count(), state.values(), state.variable()));
variables.put("loop.index", next);
pc = instruction.target;
}
private void whileGuard(Instruction instruction) {
String key = instruction.statement.nodeId().toString();
if (!evaluator.evaluateBoolean(instruction.statement.expression())) {
loops.remove(key); pc = instruction.target; return;
}
long count = loops.containsKey(key) ? loops.get(key).count() + 1 : 1;
if (count > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new IllegalStateException("while loop exceeds loop budget");
}
loops.put(key, new LoopState("while", 0, count, List.of(), ""));
pc++;
}
private void foreachInitial(Instruction instruction) {
Object source = evaluator.evaluate(instruction.statement.expression());
List<Object> values = collection(source);
if (values.size() > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new IllegalArgumentException("foreach exceeds loop budget");
}
String key = instruction.statement.nodeId().toString();
if (values.isEmpty()) { loops.remove(key); pc = instruction.target; return; }
loops.put(key, new LoopState("foreach", 0, values.size(), values, instruction.statement.name()));
variables.put(referenceName(instruction.statement.name()), values.get(0));
variables.put("loop.index", 0L);
pc++;
}
private void foreachNext(Instruction instruction) {
String key = instruction.statement.nodeId().toString();
LoopState state = requiredLoop(key);
long next = state.index() + 1;
if (next >= state.values().size()) { loops.remove(key); pc++; return; }
loops.put(key, new LoopState(state.kind(), next, state.count(), state.values(), state.variable()));
variables.put(referenceName(state.variable()), state.values().get((int) next));
variables.put("loop.index", next);
pc = instruction.target;
}
private void call(Instruction instruction) {
TriggerProgramV2.FunctionDeclaration function = program.declarations().get(instruction.statement.name());
Integer entry = program.functions().get(instruction.statement.name());
if (function == null || entry == null) throw new IllegalArgumentException(
"unknown function: " + instruction.statement.name());
if (callStack.size() >= TriggerProgramValidator.MAX_CALL_DEPTH) {
throw new IllegalStateException("function call depth exceeded");
}
Map<String, Object> caller = new LinkedHashMap<>(variables);
Map<String, Object> arguments = evaluateInputs(instruction.statement.inputs());
Map<String, String> out = new LinkedHashMap<>();
for (TriggerProgramV2.Parameter parameter : function.parameters()) {
TriggerExpression input = instruction.statement.inputs().get(parameter.name());
if (parameter.mode() != TriggerProgramV2.ParameterMode.IN) {
out.put(parameter.name(), referenceName(input.name()));
}
}
String returnTarget = instruction.statement.expression() == null ? ""
: referenceName(instruction.statement.expression().name());
callStack.add(new CallFrame(pc + 1, caller, out, returnTarget, handlers.size()));
variables.clear();
variables.putAll(caller);
for (TriggerProgramV2.Parameter parameter : function.parameters()) {
variables.put(parameter.name(), parameter.mode() == TriggerProgramV2.ParameterMode.OUT
? null : arguments.get(parameter.name()));
}
for (TriggerProgramV2.VariableDeclaration local : function.locals()) {
variables.put(local.name(), local.initialValue() == null ? null
: evaluator.evaluate(local.initialValue()));
}
rebuildEvaluator(evaluator.randomCounter());
pc = entry;
}
private void returned(Instruction instruction) {
Object value = instruction.statement == null || instruction.statement.expression() == null
? null : evaluator.evaluate(instruction.statement.expression());
if (callStack.isEmpty()) { pc = program.instructions().size(); return; }
CallFrame frame = callStack.remove(callStack.size() - 1);
Map<String, Object> callee = new LinkedHashMap<>(variables);
variables.clear();
variables.putAll(frame.callerVariables());
frame.outTargets().forEach((parameter, target) -> variables.put(target, callee.get(parameter)));
if (!frame.returnTarget().isBlank()) variables.put(frame.returnTarget(), value);
while (handlers.size() > frame.handlerDepth()) handlers.remove(handlers.size() - 1);
rebuildEvaluator(evaluator.randomCounter());
pc = frame.returnPc();
}
private Map<String, Object> evaluateInputs(Map<String, TriggerExpression> inputs) {
Map<String, Object> values = new LinkedHashMap<>();
inputs.forEach((name, value) -> values.put(name, evaluator.evaluate(value)));
return values;
}
private LoopState requiredLoop(String key) {
LoopState state = loops.get(key);
if (state == null) throw new IllegalStateException("loop continuation is missing");
return state;
}
private Result result(Status status, String error, UUID errorNode) {
State state = new State(executionId, seed, instant, event, variables, pc, instructions,
evaluator.randomCounter(), effectCounters, callStack, loops, handlers);
return new Result(status, state, effects, error, errorNode);
}
}
private static String referenceName(String value) {
if (value == null) return "";
return value.startsWith("var.") || value.startsWith("global.") || value.startsWith("local.")
? value.substring(value.indexOf('.') + 1) : value;
}
private static long integral(Object value) {
if (value instanceof Number number) {
double exact = number.doubleValue();
if (!Double.isFinite(exact) || exact != Math.rint(exact)) {
throw new IllegalArgumentException("loop count must be an integer");
}
return number.longValue();
}
try { return Long.parseLong(String.valueOf(value)); }
catch (NumberFormatException invalid) {
throw new IllegalArgumentException("loop count must be an integer", invalid);
}
}
private static boolean equal(Object left, Object right) {
if (left instanceof Number a && right instanceof Number b) {
return Double.compare(a.doubleValue(), b.doubleValue()) == 0;
}
return Objects.equals(left, right);
}
private static List<Object> collection(Object source) {
if (source instanceof Collection<?> collection) return new ArrayList<>(collection);
if (source instanceof Map<?, ?> map) {
List<Object> result = new ArrayList<>();
map.forEach((key, value) -> {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("key", key);
entry.put("value", value);
result.add(java.util.Collections.unmodifiableMap(entry));
});
return result;
}
throw new IllegalArgumentException("foreach requires a collection");
}
private static String message(RuntimeException failure) {
return failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage();
}
private static Map<String, Object> immutable(Map<String, ?> values) {
if (values == null || values.isEmpty()) return Map.of();
Map<String, Object> copy = new LinkedHashMap<>();
values.forEach(copy::put);
return java.util.Collections.unmodifiableMap(copy);
}
}
package com.xfestudio.xfeservermanager.core.trigger;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/**
* Resumable, platform-neutral Trigger Program V2 VM. It evaluates pure control flow and yields
* bounded side effects to the owning platform; no Minecraft/JVM object enters persisted state.
*/
public final class TriggerProgramExecutor {
private TriggerProgramExecutor() { }
public enum Status { YIELDED, WAITING, COMPLETED, FAILED }
public record Effect(UUID nodeId, String type, Map<String, Object> parameters,
String idempotencyKey) {
public Effect { parameters = immutable(parameters); }
}
public record CallFrame(int returnPc, Map<String, Object> callerVariables,
Map<String, String> outTargets, String returnTarget,
int handlerDepth) {
public CallFrame {
callerVariables = immutable(callerVariables);
outTargets = Map.copyOf(outTargets == null ? Map.of() : outTargets);
returnTarget = returnTarget == null ? "" : returnTarget;
}
}
public record LoopState(String kind, long index, long count, List<Object> values, String variable) {
public LoopState {
kind = Objects.requireNonNull(kind, "kind");
values = List.copyOf(values == null ? List.of() : values);
variable = variable == null ? "" : variable;
}
}
/** Entire continuation required to restart against the pinned trigger revision. */
public record State(
UUID executionId,
long seed,
String instant,
Map<String, Object> event,
Map<String, Object> variables,
int pc,
int instructions,
long randomCounter,
Map<String, Long> effectCounters,
List<CallFrame> callStack,
Map<String, LoopState> loops,
List<Integer> handlers) {
public State {
executionId = executionId == null ? UUID.randomUUID() : executionId;
instant = instant == null || instant.isBlank() ? Instant.EPOCH.toString() : instant;
event = immutable(event);
variables = immutable(variables);
effectCounters = Map.copyOf(effectCounters == null ? Map.of() : effectCounters);
callStack = List.copyOf(callStack == null ? List.of() : callStack);
loops = Map.copyOf(loops == null ? Map.of() : loops);
handlers = List.copyOf(handlers == null ? List.of() : handlers);
if (pc < 0 || instructions < 0 || randomCounter < 0) {
throw new IllegalArgumentException("VM counters must not be negative");
}
}
}
public record Result(Status status, State state, List<Effect> effects,
String error, UUID errorNodeId) {
public Result {
Objects.requireNonNull(status, "status");
Objects.requireNonNull(state, "state");
effects = List.copyOf(effects == null ? List.of() : effects);
error = error == null ? "" : error;
}
}
public static State start(TriggerProgramV2 program, Map<String, Object> event,
Map<String, Object> suppliedVariables, long seed, Instant instant) {
Objects.requireNonNull(program, "program");
Map<String, Object> variables = new LinkedHashMap<>(
suppliedVariables == null ? Map.of() : suppliedVariables);
TriggerExpressionEvaluator evaluator = new TriggerExpressionEvaluator(
event, variables, seed, instant, null, 0L);
for (TriggerProgramV2.VariableDeclaration declaration : program.declarations()) {
if (!variables.containsKey(declaration.name())) {
variables.put(declaration.name(), declaration.initialValue() == null
? null : evaluator.evaluate(declaration.initialValue()));
}
}
return new State(UUID.randomUUID(), seed, (instant == null ? Instant.EPOCH : instant).toString(),
event, variables, 0, 0, evaluator.randomCounter(), Map.of(), List.of(), Map.of(), List.of());
}
/** Runs until completion, a wait action, or the side-effect allowance is consumed. */
public static Result resume(TriggerProgramV2 program, State persisted, int maximumEffects) {
Objects.requireNonNull(program, "program");
Objects.requireNonNull(persisted, "state");
if (maximumEffects < 1 || maximumEffects > 64) {
throw new IllegalArgumentException("maximumEffects must be between 1 and 64");
}
Program bytecode = Compiler.compile(program);
Machine machine = new Machine(bytecode, persisted);
return machine.run(maximumEffects);
}
private enum Op {
ACTION, SET, JUMP, JUMP_FALSE, SWITCH, REPEAT_INIT, REPEAT_NEXT,
WHILE_GUARD, FOREACH_INIT, FOREACH_NEXT, CALL, RETURN,
PUSH_HANDLER, POP_HANDLER, END
}
private static final class Instruction {
private final Op op;
private final TriggerProgramV2.Statement statement;
private int target;
private int alternate;
private Map<UUID, Integer> cases = Map.of();
private Instruction(Op op, TriggerProgramV2.Statement statement) {
this.op = op;
this.statement = statement;
}
}
private record Program(List<Instruction> instructions,
Map<String, Integer> functions,
Map<String, TriggerProgramV2.FunctionDeclaration> declarations) { }
private record LoopTargets(int continuePc, List<Instruction> breaks) { }
private static final class Compiler {
private final TriggerProgramV2 source;
private final List<Instruction> instructions = new ArrayList<>();
private final Map<String, Integer> functions = new LinkedHashMap<>();
private final Map<String, TriggerProgramV2.FunctionDeclaration> declarations = new LinkedHashMap<>();
private Compiler(TriggerProgramV2 source) { this.source = source; }
private static Program compile(TriggerProgramV2 source) {
Compiler compiler = new Compiler(source);
source.functions().forEach(value -> compiler.declarations.put(value.name(), value));
compiler.statements(source.statements(), null);
compiler.add(Op.END, null);
for (TriggerProgramV2.FunctionDeclaration function : source.functions()) {
compiler.functions.put(function.name(), compiler.instructions.size());
compiler.statements(function.statements(), null);
compiler.add(Op.RETURN, null);
}
return new Program(List.copyOf(compiler.instructions), Map.copyOf(compiler.functions),
Map.copyOf(compiler.declarations));
}
private void statements(List<TriggerProgramV2.Statement> values, LoopTargets loop) {
for (TriggerProgramV2.Statement statement : values) statement(statement, loop);
}
private void statement(TriggerProgramV2.Statement statement, LoopTargets loop) {
switch (statement.kind()) {
case ACTION -> add(Op.ACTION, statement);
case SET -> add(Op.SET, statement);
case IF -> branch(statement, loop);
case SWITCH -> switchStatement(statement, loop);
case REPEAT -> repeat(statement);
case WHILE -> whileStatement(statement);
case FOREACH -> foreach(statement);
case BREAK -> {
if (loop == null) throw new IllegalArgumentException("break used outside a loop");
Instruction jump = add(Op.JUMP, statement);
loop.breaks().add(jump);
}
case CONTINUE -> {
if (loop == null) throw new IllegalArgumentException("continue used outside a loop");
Instruction jump = add(Op.JUMP, statement);
jump.target = loop.continuePc();
}
case CALL -> add(Op.CALL, statement);
case RETURN -> add(Op.RETURN, statement);
case TRY -> tryStatement(statement, loop);
}
}
private void branch(TriggerProgramV2.Statement statement, LoopTargets loop) {
Instruction condition = add(Op.JUMP_FALSE, statement);
statements(statement.statements(), loop);
Instruction end = add(Op.JUMP, statement);
condition.target = instructions.size();
statements(statement.elseStatements(), loop);
end.target = instructions.size();
}
private void switchStatement(TriggerProgramV2.Statement statement, LoopTargets loop) {
Instruction selector = add(Op.SWITCH, statement);
Map<UUID, Integer> targets = new LinkedHashMap<>();
List<Instruction> ends = new ArrayList<>();
for (TriggerProgramV2.SwitchCase branch : statement.cases()) {
targets.put(branch.nodeId(), instructions.size());
statements(branch.statements(), loop);
ends.add(add(Op.JUMP, statement));
}
selector.alternate = instructions.size();
statements(statement.elseStatements(), loop);
int end = instructions.size();
ends.forEach(value -> value.target = end);
selector.cases = Map.copyOf(targets);
}
private void repeat(TriggerProgramV2.Statement statement) {
Instruction initial = add(Op.REPEAT_INIT, statement);
int body = instructions.size();
Instruction nextMarker = new Instruction(Op.REPEAT_NEXT, statement);
LoopTargets loop = new LoopTargets(-1, new ArrayList<>());
statements(statement.statements(), loop);
int next = instructions.size();
nextMarker.target = body;
instructions.add(nextMarker);
int end = instructions.size();
initial.target = end;
loop.breaks().forEach(value -> value.target = end);
patchContinues(body, next, end);
}
private void whileStatement(TriggerProgramV2.Statement statement) {
int guardPc = instructions.size();
Instruction guard = add(Op.WHILE_GUARD, statement);
LoopTargets loop = new LoopTargets(guardPc, new ArrayList<>());
statements(statement.statements(), loop);
Instruction back = add(Op.JUMP, statement);
back.target = guardPc;
int end = instructions.size();
guard.target = end;
loop.breaks().forEach(value -> value.target = end);
}
private void foreach(TriggerProgramV2.Statement statement) {
Instruction initial = add(Op.FOREACH_INIT, statement);
int body = instructions.size();
LoopTargets loop = new LoopTargets(-1, new ArrayList<>());
statements(statement.statements(), loop);
int next = instructions.size();
Instruction advance = add(Op.FOREACH_NEXT, statement);
advance.target = body;
int end = instructions.size();
initial.target = end;
loop.breaks().forEach(value -> value.target = end);
patchContinues(body, next, end);
}
private void patchContinues(int from, int next, int end) {
for (int index = from; index < next; index++) {
Instruction value = instructions.get(index);
if (value.op == Op.JUMP && value.statement != null
&& value.statement.kind() == TriggerProgramV2.StatementKind.CONTINUE
&& value.target < 0) value.target = next;
}
}
private void tryStatement(TriggerProgramV2.Statement statement, LoopTargets loop) {
Instruction push = add(Op.PUSH_HANDLER, statement);
statements(statement.statements(), loop);
add(Op.POP_HANDLER, statement);
Instruction end = add(Op.JUMP, statement);
push.target = instructions.size();
statements(statement.elseStatements(), loop);
end.target = instructions.size();
}
private Instruction add(Op op, TriggerProgramV2.Statement statement) {
Instruction value = new Instruction(op, statement);
value.target = -1;
value.alternate = -1;
instructions.add(value);
return value;
}
}
private static final class Machine {
private final Program program;
private final UUID executionId;
private final long seed;
private final String instant;
private final Map<String, Object> event;
private final Map<String, Object> variables;
private final Map<String, Long> effectCounters;
private final List<CallFrame> callStack;
private final Map<String, LoopState> loops;
private final List<Integer> handlers;
private final List<Effect> effects = new ArrayList<>();
private int pc;
private int instructions;
private TriggerExpressionEvaluator evaluator;
private Machine(Program program, State state) {
this.program = program;
executionId = state.executionId();
seed = state.seed();
instant = state.instant();
event = new LinkedHashMap<>(state.event());
variables = new LinkedHashMap<>(state.variables());
effectCounters = new LinkedHashMap<>(state.effectCounters());
callStack = new ArrayList<>(state.callStack());
loops = new LinkedHashMap<>(state.loops());
handlers = new ArrayList<>(state.handlers());
pc = state.pc();
instructions = state.instructions();
rebuildEvaluator(state.randomCounter());
}
private void rebuildEvaluator(long counter) {
evaluator = new TriggerExpressionEvaluator(event, variables, seed, Instant.parse(instant), null, counter);
}
private Result run(int maximumEffects) {
UUID nodeId = null;
try {
while (pc < program.instructions().size()) {
Instruction instruction = program.instructions().get(pc);
nodeId = instruction.statement == null ? null : instruction.statement.nodeId();
if (++instructions > TriggerProgramValidator.MAX_INSTRUCTIONS) {
throw new IllegalStateException("instruction budget exceeded");
}
try {
Status status = execute(instruction);
if (status != null) return result(status, "", null);
if (effects.size() >= maximumEffects) return result(Status.YIELDED, "", null);
} catch (RuntimeException failure) {
if (handlers.isEmpty()) throw failure;
pc = handlers.remove(handlers.size() - 1);
variables.put("error.message", message(failure));
rebuildEvaluator(evaluator.randomCounter());
}
}
return result(Status.COMPLETED, "", null);
} catch (RuntimeException failure) {
return result(Status.FAILED, message(failure), nodeId);
}
}
private Status execute(Instruction instruction) {
TriggerProgramV2.Statement statement = instruction.statement;
switch (instruction.op) {
case ACTION -> {
Map<String, Object> parameters = evaluateInputs(statement.inputs());
long invocation = effectCounters.merge(statement.nodeId().toString(), 1L, Long::sum) - 1L;
effects.add(new Effect(statement.nodeId(), statement.name(), parameters,
executionId + ":" + statement.nodeId() + ":" + invocation));
pc++;
if (statement.name().equals("wait")) return Status.WAITING;
}
case SET -> {
variables.put(referenceName(statement.name()), evaluator.evaluate(statement.expression()));
pc++;
}
case JUMP -> pc = instruction.target;
case JUMP_FALSE -> pc = evaluator.evaluateBoolean(statement.expression())
? pc + 1 : instruction.target;
case SWITCH -> selectSwitch(instruction);
case REPEAT_INIT -> repeatInitial(instruction);
case REPEAT_NEXT -> repeatNext(instruction);
case WHILE_GUARD -> whileGuard(instruction);
case FOREACH_INIT -> foreachInitial(instruction);
case FOREACH_NEXT -> foreachNext(instruction);
case CALL -> call(instruction);
case RETURN -> returned(instruction);
case PUSH_HANDLER -> { handlers.add(instruction.target); pc++; }
case POP_HANDLER -> { if (!handlers.isEmpty()) handlers.remove(handlers.size() - 1); pc++; }
case END -> { return Status.COMPLETED; }
}
return null;
}
private void selectSwitch(Instruction instruction) {
Object selector = evaluator.evaluate(instruction.statement.expression());
for (TriggerProgramV2.SwitchCase branch : instruction.statement.cases()) {
if (equal(selector, evaluator.evaluate(branch.match()))) {
pc = instruction.cases.get(branch.nodeId());
return;
}
}
pc = instruction.alternate;
}
private void repeatInitial(Instruction instruction) {
long count = integral(evaluator.evaluate(instruction.statement.expression()));
if (count < 0 || count > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new IllegalArgumentException("repeat count exceeds loop budget");
}
String key = instruction.statement.nodeId().toString();
if (count == 0) { loops.remove(key); pc = instruction.target; return; }
loops.put(key, new LoopState("repeat", 0, count, List.of(), "loop.index"));
variables.put("loop.index", 0L);
pc++;
}
private void repeatNext(Instruction instruction) {
String key = instruction.statement.nodeId().toString();
LoopState state = requiredLoop(key);
long next = state.index() + 1;
if (next >= state.count()) { loops.remove(key); pc++; return; }
loops.put(key, new LoopState(state.kind(), next, state.count(), state.values(), state.variable()));
variables.put("loop.index", next);
pc = instruction.target;
}
private void whileGuard(Instruction instruction) {
String key = instruction.statement.nodeId().toString();
if (!evaluator.evaluateBoolean(instruction.statement.expression())) {
loops.remove(key); pc = instruction.target; return;
}
long count = loops.containsKey(key) ? loops.get(key).count() + 1 : 1;
if (count > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new IllegalStateException("while loop exceeds loop budget");
}
loops.put(key, new LoopState("while", 0, count, List.of(), ""));
pc++;
}
private void foreachInitial(Instruction instruction) {
Object source = evaluator.evaluate(instruction.statement.expression());
List<Object> values = collection(source);
if (values.size() > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new IllegalArgumentException("foreach exceeds loop budget");
}
String key = instruction.statement.nodeId().toString();
if (values.isEmpty()) { loops.remove(key); pc = instruction.target; return; }
loops.put(key, new LoopState("foreach", 0, values.size(), values, instruction.statement.name()));
variables.put(referenceName(instruction.statement.name()), values.get(0));
variables.put("loop.index", 0L);
pc++;
}
private void foreachNext(Instruction instruction) {
String key = instruction.statement.nodeId().toString();
LoopState state = requiredLoop(key);
long next = state.index() + 1;
if (next >= state.values().size()) { loops.remove(key); pc++; return; }
loops.put(key, new LoopState(state.kind(), next, state.count(), state.values(), state.variable()));
variables.put(referenceName(state.variable()), state.values().get((int) next));
variables.put("loop.index", next);
pc = instruction.target;
}
private void call(Instruction instruction) {
TriggerProgramV2.FunctionDeclaration function = program.declarations().get(instruction.statement.name());
Integer entry = program.functions().get(instruction.statement.name());
if (function == null || entry == null) throw new IllegalArgumentException(
"unknown function: " + instruction.statement.name());
if (callStack.size() >= TriggerProgramValidator.MAX_CALL_DEPTH) {
throw new IllegalStateException("function call depth exceeded");
}
Map<String, Object> caller = new LinkedHashMap<>(variables);
Map<String, Object> arguments = evaluateInputs(instruction.statement.inputs());
Map<String, String> out = new LinkedHashMap<>();
for (TriggerProgramV2.Parameter parameter : function.parameters()) {
TriggerExpression input = instruction.statement.inputs().get(parameter.name());
if (parameter.mode() != TriggerProgramV2.ParameterMode.IN) {
out.put(parameter.name(), referenceName(input.name()));
}
}
String returnTarget = instruction.statement.expression() == null ? ""
: referenceName(instruction.statement.expression().name());
callStack.add(new CallFrame(pc + 1, caller, out, returnTarget, handlers.size()));
variables.clear();
variables.putAll(caller);
for (TriggerProgramV2.Parameter parameter : function.parameters()) {
variables.put(parameter.name(), parameter.mode() == TriggerProgramV2.ParameterMode.OUT
? null : arguments.get(parameter.name()));
}
for (TriggerProgramV2.VariableDeclaration local : function.locals()) {
variables.put(local.name(), local.initialValue() == null ? null
: evaluator.evaluate(local.initialValue()));
}
rebuildEvaluator(evaluator.randomCounter());
pc = entry;
}
private void returned(Instruction instruction) {
Object value = instruction.statement == null || instruction.statement.expression() == null
? null : evaluator.evaluate(instruction.statement.expression());
if (callStack.isEmpty()) { pc = program.instructions().size(); return; }
CallFrame frame = callStack.remove(callStack.size() - 1);
Map<String, Object> callee = new LinkedHashMap<>(variables);
variables.clear();
variables.putAll(frame.callerVariables());
frame.outTargets().forEach((parameter, target) -> variables.put(target, callee.get(parameter)));
if (!frame.returnTarget().isBlank()) variables.put(frame.returnTarget(), value);
while (handlers.size() > frame.handlerDepth()) handlers.remove(handlers.size() - 1);
rebuildEvaluator(evaluator.randomCounter());
pc = frame.returnPc();
}
private Map<String, Object> evaluateInputs(Map<String, TriggerExpression> inputs) {
Map<String, Object> values = new LinkedHashMap<>();
inputs.forEach((name, value) -> values.put(name, evaluator.evaluate(value)));
return values;
}
private LoopState requiredLoop(String key) {
LoopState state = loops.get(key);
if (state == null) throw new IllegalStateException("loop continuation is missing");
return state;
}
private Result result(Status status, String error, UUID errorNode) {
State state = new State(executionId, seed, instant, event, variables, pc, instructions,
evaluator.randomCounter(), effectCounters, callStack, loops, handlers);
return new Result(status, state, effects, error, errorNode);
}
}
private static String referenceName(String value) {
if (value == null) return "";
return value.startsWith("var.") || value.startsWith("global.") || value.startsWith("local.")
? value.substring(value.indexOf('.') + 1) : value;
}
private static long integral(Object value) {
if (value instanceof Number number) {
double exact = number.doubleValue();
if (!Double.isFinite(exact) || exact != Math.rint(exact)) {
throw new IllegalArgumentException("loop count must be an integer");
}
return number.longValue();
}
try { return Long.parseLong(String.valueOf(value)); }
catch (NumberFormatException invalid) {
throw new IllegalArgumentException("loop count must be an integer", invalid);
}
}
private static boolean equal(Object left, Object right) {
if (left instanceof Number a && right instanceof Number b) {
return Double.compare(a.doubleValue(), b.doubleValue()) == 0;
}
return Objects.equals(left, right);
}
private static List<Object> collection(Object source) {
if (source instanceof Collection<?> collection) return new ArrayList<>(collection);
if (source instanceof Map<?, ?> map) {
List<Object> result = new ArrayList<>();
map.forEach((key, value) -> {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("key", key);
entry.put("value", value);
result.add(java.util.Collections.unmodifiableMap(entry));
});
return result;
}
throw new IllegalArgumentException("foreach requires a collection");
}
private static String message(RuntimeException failure) {
return failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage();
}
private static Map<String, Object> immutable(Map<String, ?> values) {
if (values == null || values.isEmpty()) return Map.of();
Map<String, Object> copy = new LinkedHashMap<>();
values.forEach(copy::put);
return java.util.Collections.unmodifiableMap(copy);
}
}