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;
/**
* Read-only Trigger Program V2 virtual machine. Side effects are emitted as planned actions and
* are never dispatched to a platform adapter.
*/
public final class TriggerSimulator {
private TriggerSimulator() { }
public static Result simulate(TriggerProgramV2 program, Request request) {
Objects.requireNonNull(program, "program");
request = request == null ? new Request(Map.of(), Map.of(), 0L, Instant.EPOCH) : request;
Machine machine = new Machine(program, request);
try {
machine.initialize(program.declarations());
machine.run(program.statements(), 0, false);
return machine.result(Status.COMPLETED, "");
} catch (SimulationFailure failure) {
machine.trace(failure.nodeId, "error", failure.getMessage(), Map.of());
return machine.result(Status.FAILED, failure.getMessage());
} catch (RuntimeException failure) {
machine.trace(null, "error", failure.getMessage(), Map.of());
return machine.result(Status.FAILED, failure.getMessage());
}
}
public record Request(Map<String, Object> event, Map<String, Object> variables, long seed, Instant instant) {
public Request {
event = immutableMap(event);
variables = immutableMap(variables);
instant = instant == null ? Instant.EPOCH : instant;
}
}
public enum Status { COMPLETED, FAILED }
public record PlannedAction(UUID nodeId, String type, Map<String, Object> parameters, String idempotencyKey) {
public PlannedAction {
parameters = immutableMap(parameters);
}
}
public record TraceStep(
long sequence,
UUID nodeId,
String kind,
Object result,
Map<String, Object> writes,
int instructions) {
public TraceStep {
kind = kind == null ? "" : kind;
writes = immutableMap(writes);
}
}
public record Result(
UUID executionId,
Status status,
String error,
Map<String, Object> variables,
List<PlannedAction> actions,
List<TraceStep> trace,
int instructions,
long seed) {
public Result {
variables = immutableMap(variables);
actions = List.copyOf(actions);
trace = List.copyOf(trace);
}
}
private enum Signal { NONE, BREAK, CONTINUE, RETURN }
private static final class Flow {
private static final Flow NONE = new Flow(Signal.NONE, null);
private final Signal signal;
private final Object value;
private Flow(Signal signal, Object value) {
this.signal = signal;
this.value = value;
}
}
private static final class Machine {
private final UUID executionId = UUID.randomUUID();
private final TriggerProgramV2 program;
private final Request request;
private final Map<String, Object> variables = new LinkedHashMap<>();
private final Map<String, TriggerProgramV2.FunctionDeclaration> functions = new LinkedHashMap<>();
private final List<PlannedAction> actions = new ArrayList<>();
private final List<TraceStep> trace = new ArrayList<>();
private TriggerExpressionEvaluator evaluator;
private int instructions;
private long sequence;
private Machine(TriggerProgramV2 program, Request request) {
this.program = program;
this.request = request;
this.variables.putAll(request.variables());
program.functions().forEach(function -> functions.put(function.name(), function));
rebuildEvaluator();
}
private void rebuildEvaluator() {
evaluator = new TriggerExpressionEvaluator(
request.event(), variables, request.seed(), request.instant(),
(expression, value) -> trace(expression.nodeId(), "expression", value, Map.of()));
}
private void initialize(List<TriggerProgramV2.VariableDeclaration> declarations) {
for (TriggerProgramV2.VariableDeclaration declaration : declarations) {
if (!variables.containsKey(declaration.name())) {
variables.put(declaration.name(), evaluator.evaluate(declaration.initialValue()));
}
}
}
private Flow run(List<TriggerProgramV2.Statement> statements, int callDepth, boolean loop) {
if (callDepth > TriggerProgramValidator.MAX_CALL_DEPTH) {
throw new SimulationFailure(null, "function call depth exceeds " + TriggerProgramValidator.MAX_CALL_DEPTH);
}
for (TriggerProgramV2.Statement statement : statements) {
instruction(statement.nodeId());
Flow flow;
try {
flow = execute(statement, callDepth, loop);
} catch (SimulationFailure failure) {
if (statement.kind() != TriggerProgramV2.StatementKind.TRY) throw failure;
variables.put("error.message", failure.getMessage());
trace(statement.nodeId(), "error_branch", failure.getMessage(), Map.of("error.message", failure.getMessage()));
flow = run(statement.elseStatements(), callDepth, loop);
} catch (RuntimeException failure) {
if (statement.kind() != TriggerProgramV2.StatementKind.TRY) {
throw new SimulationFailure(statement.nodeId(), safeMessage(failure));
}
variables.put("error.message", safeMessage(failure));
trace(statement.nodeId(), "error_branch", safeMessage(failure),
Map.of("error.message", safeMessage(failure)));
flow = run(statement.elseStatements(), callDepth, loop);
}
if (flow.signal != Signal.NONE) return flow;
}
return Flow.NONE;
}
private Flow execute(TriggerProgramV2.Statement statement, int callDepth, boolean loop) {
return switch (statement.kind()) {
case ACTION -> action(statement);
case SET -> set(statement);
case IF -> branch(statement, callDepth, loop);
case SWITCH -> switchBranch(statement, callDepth, loop);
case REPEAT -> repeat(statement, callDepth);
case WHILE -> whileLoop(statement, callDepth);
case FOREACH -> foreach(statement, callDepth);
case BREAK -> {
if (!loop) throw new SimulationFailure(statement.nodeId(), "break used outside a loop");
yield new Flow(Signal.BREAK, null);
}
case CONTINUE -> {
if (!loop) throw new SimulationFailure(statement.nodeId(), "continue used outside a loop");
yield new Flow(Signal.CONTINUE, null);
}
case CALL -> call(statement, callDepth, loop);
case RETURN -> new Flow(Signal.RETURN,
statement.expression() == null ? null : evaluator.evaluate(statement.expression()));
case TRY -> run(statement.statements(), callDepth, loop);
};
}
private Flow action(TriggerProgramV2.Statement statement) {
Map<String, Object> parameters = evaluateInputs(statement.inputs());
String key = executionId + ":" + statement.nodeId();
actions.add(new PlannedAction(statement.nodeId(), statement.name(), parameters, key));
trace(statement.nodeId(), "planned_action", statement.name(), parameters);
return Flow.NONE;
}
private Flow set(TriggerProgramV2.Statement statement) {
Object value = evaluator.evaluate(statement.expression());
variables.put(statement.name(), value);
Map<String, Object> writes = new LinkedHashMap<>();
writes.put(statement.name(), value);
trace(statement.nodeId(), "variable_write", value, writes);
return Flow.NONE;
}
private Flow branch(TriggerProgramV2.Statement statement, int callDepth, boolean loop) {
boolean matched = evaluator.evaluateBoolean(statement.expression());
trace(statement.nodeId(), "branch", matched, Map.of());
return run(matched ? statement.statements() : statement.elseStatements(), callDepth, loop);
}
private Flow switchBranch(TriggerProgramV2.Statement statement, int callDepth, boolean loop) {
Object selector = evaluator.evaluate(statement.expression());
for (TriggerProgramV2.SwitchCase branch : statement.cases()) {
Object match = evaluator.evaluate(branch.match());
if (Objects.equals(selector, match)
|| selector instanceof Number && match instanceof Number
&& Double.compare(((Number) selector).doubleValue(), ((Number) match).doubleValue()) == 0) {
trace(branch.nodeId(), "switch_case", match, Map.of());
return run(branch.statements(), callDepth, loop);
}
}
trace(statement.nodeId(), "switch_default", selector, Map.of());
return run(statement.elseStatements(), callDepth, loop);
}
private Flow repeat(TriggerProgramV2.Statement statement, int callDepth) {
long count = integral(evaluator.evaluate(statement.expression()), statement.nodeId());
if (count < 0 || count > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new SimulationFailure(statement.nodeId(), "repeat count exceeds loop budget");
}
for (long index = 0; index < count; index++) {
variables.put("loop.index", index);
Flow flow = run(statement.statements(), callDepth, true);
if (flow.signal == Signal.BREAK) break;
if (flow.signal == Signal.RETURN) return flow;
}
return Flow.NONE;
}
private Flow whileLoop(TriggerProgramV2.Statement statement, int callDepth) {
for (int count = 0; evaluator.evaluateBoolean(statement.expression()); count++) {
if (count >= TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new SimulationFailure(statement.nodeId(), "while loop exceeds loop budget");
}
Flow flow = run(statement.statements(), callDepth, true);
if (flow.signal == Signal.BREAK) break;
if (flow.signal == Signal.RETURN) return flow;
}
return Flow.NONE;
}
private Flow foreach(TriggerProgramV2.Statement statement, int callDepth) {
Object source = evaluator.evaluate(statement.expression());
Collection<?> values;
if (source instanceof Collection<?> collection) values = collection;
else if (source instanceof Map<?, ?> map) values = map.entrySet();
else throw new SimulationFailure(statement.nodeId(), "foreach requires a collection");
if (values.size() > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new SimulationFailure(statement.nodeId(), "foreach exceeds loop budget");
}
int index = 0;
for (Object value : values) {
variables.put(statement.name(), value);
variables.put("loop.index", (long) index++);
Flow flow = run(statement.statements(), callDepth, true);
if (flow.signal == Signal.BREAK) break;
if (flow.signal == Signal.RETURN) return flow;
}
return Flow.NONE;
}
private Flow call(TriggerProgramV2.Statement statement, int callDepth, boolean loop) {
TriggerProgramV2.FunctionDeclaration function = functions.get(statement.name());
if (function == null) throw new SimulationFailure(statement.nodeId(), "unknown function: " + statement.name());
Map<String, Object> caller = new LinkedHashMap<>(variables);
Map<String, Object> arguments = new LinkedHashMap<>();
Map<String, TriggerExpression> references = new LinkedHashMap<>();
// Evaluate all IN values in the caller scope before parameter names can shadow them.
for (TriggerProgramV2.Parameter parameter : function.parameters()) {
TriggerExpression input = statement.inputs().get(parameter.name());
if (parameter.mode() != TriggerProgramV2.ParameterMode.OUT) {
if (input == null) throw new SimulationFailure(statement.nodeId(), "missing function input: " + parameter.name());
arguments.put(parameter.name(), evaluator.evaluate(input));
}
if (parameter.mode() != TriggerProgramV2.ParameterMode.IN) {
if (input == null || input.kind() != TriggerExpression.Kind.REFERENCE) {
throw new SimulationFailure(statement.nodeId(), "OUT/INOUT input must be a variable reference: " + parameter.name());
}
references.put(parameter.name(), input);
}
}
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(), evaluator.evaluate(local.initialValue()));
}
Flow functionFlow = run(function.statements(), callDepth + 1, false);
Map<String, Object> callee = new LinkedHashMap<>(variables);
variables.clear();
variables.putAll(caller);
references.forEach((parameter, reference) -> variables.put(
referenceName(reference.name()), callee.get(parameter)));
if (statement.expression() != null && statement.expression().kind() == TriggerExpression.Kind.REFERENCE) {
variables.put(referenceName(statement.expression().name()), functionFlow.value);
}
trace(statement.nodeId(), "function_call", functionFlow.value, Map.of());
return Flow.NONE;
}
private static String referenceName(String value) {
return value.startsWith("var.") || value.startsWith("global.") || value.startsWith("local.")
? value.substring(value.indexOf('.') + 1) : value;
}
private Map<String, Object> evaluateInputs(Map<String, TriggerExpression> inputs) {
Map<String, Object> result = new LinkedHashMap<>();
inputs.forEach((name, expression) -> result.put(name, evaluator.evaluate(expression)));
return result;
}
private void instruction(UUID nodeId) {
if (++instructions > TriggerProgramValidator.MAX_INSTRUCTIONS) {
throw new SimulationFailure(nodeId, "instruction budget exceeded");
}
}
private void trace(UUID nodeId, String kind, Object value, Map<String, Object> writes) {
trace.add(new TraceStep(++sequence, nodeId, kind, value, writes, instructions));
}
private Result result(Status status, String error) {
return new Result(executionId, status, error == null ? "" : error,
variables, actions, trace, instructions, request.seed());
}
private static long integral(Object value, UUID nodeId) {
if (value instanceof Number number) return number.longValue();
try {
return Long.parseLong(String.valueOf(value));
} catch (NumberFormatException invalid) {
throw new SimulationFailure(nodeId, "loop count must be an integer");
}
}
private static String safeMessage(RuntimeException failure) {
return failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage();
}
}
private static final class SimulationFailure extends RuntimeException {
private final UUID nodeId;
private SimulationFailure(UUID nodeId, String message) {
super(message);
this.nodeId = nodeId;
}
}
private static Map<String, Object> immutableMap(Map<String, ?> source) {
if (source == null || source.isEmpty()) return Map.of();
Map<String, Object> result = new LinkedHashMap<>();
source.forEach(result::put);
return java.util.Collections.unmodifiableMap(result);
}
}
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;
/**
* Read-only Trigger Program V2 virtual machine. Side effects are emitted as planned actions and
* are never dispatched to a platform adapter.
*/
public final class TriggerSimulator {
private TriggerSimulator() { }
public static Result simulate(TriggerProgramV2 program, Request request) {
Objects.requireNonNull(program, "program");
request = request == null ? new Request(Map.of(), Map.of(), 0L, Instant.EPOCH) : request;
Machine machine = new Machine(program, request);
try {
machine.initialize(program.declarations());
machine.run(program.statements(), 0, false);
return machine.result(Status.COMPLETED, "");
} catch (SimulationFailure failure) {
machine.trace(failure.nodeId, "error", failure.getMessage(), Map.of());
return machine.result(Status.FAILED, failure.getMessage());
} catch (RuntimeException failure) {
machine.trace(null, "error", failure.getMessage(), Map.of());
return machine.result(Status.FAILED, failure.getMessage());
}
}
public record Request(Map<String, Object> event, Map<String, Object> variables, long seed, Instant instant) {
public Request {
event = immutableMap(event);
variables = immutableMap(variables);
instant = instant == null ? Instant.EPOCH : instant;
}
}
public enum Status { COMPLETED, FAILED }
public record PlannedAction(UUID nodeId, String type, Map<String, Object> parameters, String idempotencyKey) {
public PlannedAction {
parameters = immutableMap(parameters);
}
}
public record TraceStep(
long sequence,
UUID nodeId,
String kind,
Object result,
Map<String, Object> writes,
int instructions) {
public TraceStep {
kind = kind == null ? "" : kind;
writes = immutableMap(writes);
}
}
public record Result(
UUID executionId,
Status status,
String error,
Map<String, Object> variables,
List<PlannedAction> actions,
List<TraceStep> trace,
int instructions,
long seed) {
public Result {
variables = immutableMap(variables);
actions = List.copyOf(actions);
trace = List.copyOf(trace);
}
}
private enum Signal { NONE, BREAK, CONTINUE, RETURN }
private static final class Flow {
private static final Flow NONE = new Flow(Signal.NONE, null);
private final Signal signal;
private final Object value;
private Flow(Signal signal, Object value) {
this.signal = signal;
this.value = value;
}
}
private static final class Machine {
private final UUID executionId = UUID.randomUUID();
private final TriggerProgramV2 program;
private final Request request;
private final Map<String, Object> variables = new LinkedHashMap<>();
private final Map<String, TriggerProgramV2.FunctionDeclaration> functions = new LinkedHashMap<>();
private final List<PlannedAction> actions = new ArrayList<>();
private final List<TraceStep> trace = new ArrayList<>();
private TriggerExpressionEvaluator evaluator;
private int instructions;
private long sequence;
private Machine(TriggerProgramV2 program, Request request) {
this.program = program;
this.request = request;
this.variables.putAll(request.variables());
program.functions().forEach(function -> functions.put(function.name(), function));
rebuildEvaluator();
}
private void rebuildEvaluator() {
evaluator = new TriggerExpressionEvaluator(
request.event(), variables, request.seed(), request.instant(),
(expression, value) -> trace(expression.nodeId(), "expression", value, Map.of()));
}
private void initialize(List<TriggerProgramV2.VariableDeclaration> declarations) {
for (TriggerProgramV2.VariableDeclaration declaration : declarations) {
if (!variables.containsKey(declaration.name())) {
variables.put(declaration.name(), evaluator.evaluate(declaration.initialValue()));
}
}
}
private Flow run(List<TriggerProgramV2.Statement> statements, int callDepth, boolean loop) {
if (callDepth > TriggerProgramValidator.MAX_CALL_DEPTH) {
throw new SimulationFailure(null, "function call depth exceeds " + TriggerProgramValidator.MAX_CALL_DEPTH);
}
for (TriggerProgramV2.Statement statement : statements) {
instruction(statement.nodeId());
Flow flow;
try {
flow = execute(statement, callDepth, loop);
} catch (SimulationFailure failure) {
if (statement.kind() != TriggerProgramV2.StatementKind.TRY) throw failure;
variables.put("error.message", failure.getMessage());
trace(statement.nodeId(), "error_branch", failure.getMessage(), Map.of("error.message", failure.getMessage()));
flow = run(statement.elseStatements(), callDepth, loop);
} catch (RuntimeException failure) {
if (statement.kind() != TriggerProgramV2.StatementKind.TRY) {
throw new SimulationFailure(statement.nodeId(), safeMessage(failure));
}
variables.put("error.message", safeMessage(failure));
trace(statement.nodeId(), "error_branch", safeMessage(failure),
Map.of("error.message", safeMessage(failure)));
flow = run(statement.elseStatements(), callDepth, loop);
}
if (flow.signal != Signal.NONE) return flow;
}
return Flow.NONE;
}
private Flow execute(TriggerProgramV2.Statement statement, int callDepth, boolean loop) {
return switch (statement.kind()) {
case ACTION -> action(statement);
case SET -> set(statement);
case IF -> branch(statement, callDepth, loop);
case SWITCH -> switchBranch(statement, callDepth, loop);
case REPEAT -> repeat(statement, callDepth);
case WHILE -> whileLoop(statement, callDepth);
case FOREACH -> foreach(statement, callDepth);
case BREAK -> {
if (!loop) throw new SimulationFailure(statement.nodeId(), "break used outside a loop");
yield new Flow(Signal.BREAK, null);
}
case CONTINUE -> {
if (!loop) throw new SimulationFailure(statement.nodeId(), "continue used outside a loop");
yield new Flow(Signal.CONTINUE, null);
}
case CALL -> call(statement, callDepth, loop);
case RETURN -> new Flow(Signal.RETURN,
statement.expression() == null ? null : evaluator.evaluate(statement.expression()));
case TRY -> run(statement.statements(), callDepth, loop);
};
}
private Flow action(TriggerProgramV2.Statement statement) {
Map<String, Object> parameters = evaluateInputs(statement.inputs());
String key = executionId + ":" + statement.nodeId();
actions.add(new PlannedAction(statement.nodeId(), statement.name(), parameters, key));
trace(statement.nodeId(), "planned_action", statement.name(), parameters);
return Flow.NONE;
}
private Flow set(TriggerProgramV2.Statement statement) {
Object value = evaluator.evaluate(statement.expression());
variables.put(statement.name(), value);
Map<String, Object> writes = new LinkedHashMap<>();
writes.put(statement.name(), value);
trace(statement.nodeId(), "variable_write", value, writes);
return Flow.NONE;
}
private Flow branch(TriggerProgramV2.Statement statement, int callDepth, boolean loop) {
boolean matched = evaluator.evaluateBoolean(statement.expression());
trace(statement.nodeId(), "branch", matched, Map.of());
return run(matched ? statement.statements() : statement.elseStatements(), callDepth, loop);
}
private Flow switchBranch(TriggerProgramV2.Statement statement, int callDepth, boolean loop) {
Object selector = evaluator.evaluate(statement.expression());
for (TriggerProgramV2.SwitchCase branch : statement.cases()) {
Object match = evaluator.evaluate(branch.match());
if (Objects.equals(selector, match)
|| selector instanceof Number && match instanceof Number
&& Double.compare(((Number) selector).doubleValue(), ((Number) match).doubleValue()) == 0) {
trace(branch.nodeId(), "switch_case", match, Map.of());
return run(branch.statements(), callDepth, loop);
}
}
trace(statement.nodeId(), "switch_default", selector, Map.of());
return run(statement.elseStatements(), callDepth, loop);
}
private Flow repeat(TriggerProgramV2.Statement statement, int callDepth) {
long count = integral(evaluator.evaluate(statement.expression()), statement.nodeId());
if (count < 0 || count > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new SimulationFailure(statement.nodeId(), "repeat count exceeds loop budget");
}
for (long index = 0; index < count; index++) {
variables.put("loop.index", index);
Flow flow = run(statement.statements(), callDepth, true);
if (flow.signal == Signal.BREAK) break;
if (flow.signal == Signal.RETURN) return flow;
}
return Flow.NONE;
}
private Flow whileLoop(TriggerProgramV2.Statement statement, int callDepth) {
for (int count = 0; evaluator.evaluateBoolean(statement.expression()); count++) {
if (count >= TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new SimulationFailure(statement.nodeId(), "while loop exceeds loop budget");
}
Flow flow = run(statement.statements(), callDepth, true);
if (flow.signal == Signal.BREAK) break;
if (flow.signal == Signal.RETURN) return flow;
}
return Flow.NONE;
}
private Flow foreach(TriggerProgramV2.Statement statement, int callDepth) {
Object source = evaluator.evaluate(statement.expression());
Collection<?> values;
if (source instanceof Collection<?> collection) values = collection;
else if (source instanceof Map<?, ?> map) values = map.entrySet();
else throw new SimulationFailure(statement.nodeId(), "foreach requires a collection");
if (values.size() > TriggerProgramValidator.MAX_LOOP_ITERATIONS) {
throw new SimulationFailure(statement.nodeId(), "foreach exceeds loop budget");
}
int index = 0;
for (Object value : values) {
variables.put(statement.name(), value);
variables.put("loop.index", (long) index++);
Flow flow = run(statement.statements(), callDepth, true);
if (flow.signal == Signal.BREAK) break;
if (flow.signal == Signal.RETURN) return flow;
}
return Flow.NONE;
}
private Flow call(TriggerProgramV2.Statement statement, int callDepth, boolean loop) {
TriggerProgramV2.FunctionDeclaration function = functions.get(statement.name());
if (function == null) throw new SimulationFailure(statement.nodeId(), "unknown function: " + statement.name());
Map<String, Object> caller = new LinkedHashMap<>(variables);
Map<String, Object> arguments = new LinkedHashMap<>();
Map<String, TriggerExpression> references = new LinkedHashMap<>();
// Evaluate all IN values in the caller scope before parameter names can shadow them.
for (TriggerProgramV2.Parameter parameter : function.parameters()) {
TriggerExpression input = statement.inputs().get(parameter.name());
if (parameter.mode() != TriggerProgramV2.ParameterMode.OUT) {
if (input == null) throw new SimulationFailure(statement.nodeId(), "missing function input: " + parameter.name());
arguments.put(parameter.name(), evaluator.evaluate(input));
}
if (parameter.mode() != TriggerProgramV2.ParameterMode.IN) {
if (input == null || input.kind() != TriggerExpression.Kind.REFERENCE) {
throw new SimulationFailure(statement.nodeId(), "OUT/INOUT input must be a variable reference: " + parameter.name());
}
references.put(parameter.name(), input);
}
}
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(), evaluator.evaluate(local.initialValue()));
}
Flow functionFlow = run(function.statements(), callDepth + 1, false);
Map<String, Object> callee = new LinkedHashMap<>(variables);
variables.clear();
variables.putAll(caller);
references.forEach((parameter, reference) -> variables.put(
referenceName(reference.name()), callee.get(parameter)));
if (statement.expression() != null && statement.expression().kind() == TriggerExpression.Kind.REFERENCE) {
variables.put(referenceName(statement.expression().name()), functionFlow.value);
}
trace(statement.nodeId(), "function_call", functionFlow.value, Map.of());
return Flow.NONE;
}
private static String referenceName(String value) {
return value.startsWith("var.") || value.startsWith("global.") || value.startsWith("local.")
? value.substring(value.indexOf('.') + 1) : value;
}
private Map<String, Object> evaluateInputs(Map<String, TriggerExpression> inputs) {
Map<String, Object> result = new LinkedHashMap<>();
inputs.forEach((name, expression) -> result.put(name, evaluator.evaluate(expression)));
return result;
}
private void instruction(UUID nodeId) {
if (++instructions > TriggerProgramValidator.MAX_INSTRUCTIONS) {
throw new SimulationFailure(nodeId, "instruction budget exceeded");
}
}
private void trace(UUID nodeId, String kind, Object value, Map<String, Object> writes) {
trace.add(new TraceStep(++sequence, nodeId, kind, value, writes, instructions));
}
private Result result(Status status, String error) {
return new Result(executionId, status, error == null ? "" : error,
variables, actions, trace, instructions, request.seed());
}
private static long integral(Object value, UUID nodeId) {
if (value instanceof Number number) return number.longValue();
try {
return Long.parseLong(String.valueOf(value));
} catch (NumberFormatException invalid) {
throw new SimulationFailure(nodeId, "loop count must be an integer");
}
}
private static String safeMessage(RuntimeException failure) {
return failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage();
}
}
private static final class SimulationFailure extends RuntimeException {
private final UUID nodeId;
private SimulationFailure(UUID nodeId, String message) {
super(message);
this.nodeId = nodeId;
}
}
private static Map<String, Object> immutableMap(Map<String, ?> source) {
if (source == null || source.isEmpty()) return Map.of();
Map<String, Object> result = new LinkedHashMap<>();
source.forEach(result::put);
return java.util.Collections.unmodifiableMap(result);
}
}