diff --git a/src/main/kotlin/at/ac/uibk/dps/cirrina/Runtime.kt b/src/main/kotlin/at/ac/uibk/dps/cirrina/Runtime.kt index 5c1c6bd0..378e38b1 100644 --- a/src/main/kotlin/at/ac/uibk/dps/cirrina/Runtime.kt +++ b/src/main/kotlin/at/ac/uibk/dps/cirrina/Runtime.kt @@ -84,7 +84,7 @@ constructor( eventHandler.bind( graph = graph, instanceNames = run, - subscribedTo = instances.values.flatMap { it.subscriptions }, + subscribedTo = instances.values.flatMap { it.subscriptions } /*.toSet()*/, handlers = instances.values.map { it::pushEvent }, ) } diff --git a/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/Action.kt b/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/Action.kt index 1ec34322..7a97df39 100644 --- a/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/Action.kt +++ b/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/Action.kt @@ -13,7 +13,8 @@ sealed interface Action { description.type, description.mode, buildVariables(description.input), - buildEvents(description.emits), + description.output, + buildConditionalEvents(description.emits), ) is MatchDescription -> @@ -57,7 +58,8 @@ sealed interface Action { ContextVariable.lazy(k, expression) } - private fun buildEvents(events: List) = events.map { Event.from(it) } + private fun buildConditionalEvents(events: List) = + events.map { ConditionalEvent.from(it) } } } @@ -74,12 +76,13 @@ internal constructor( val type: String, val mode: InvocationMode, val input: List, - val emits: List, + val output: List, + val emits: List, ) : EventRaisingAction { - override fun raises(): List = emits + override fun raises(): List = emits.map { it.event } override fun toString() = - "InvokeAction(type='$type', mode='$mode', input='$input', emits='$emits')" + "InvokeAction(type='$type', mode='$mode', input='$input', output='$output', emits='$emits')" } class MatchAction diff --git a/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/ActionCommand.kt b/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/ActionCommand.kt index 0a115cff..acfbf249 100644 --- a/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/ActionCommand.kt +++ b/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/ActionCommand.kt @@ -50,12 +50,30 @@ class ActionExecutor( val delta = measureTime { runCatching { service.invoke(input) } .onSuccess { output -> - action.emits.forEach { eventTemplate -> - val emittedEvent = eventTemplate.copy(data = output) - if (emittedEvent.channel == EventChannel.INTERNAL) { - eventHandler.propagateToParent(emittedEvent) - } else { - eventHandler.emit(emittedEvent) + action.output.forEach { reference -> + output + .firstOrNull { it.name == reference } + ?.let { + runCatching { scope.extent.set(reference, it.value) } + .onFailure { e -> + logger.warn(e) { + "failed to assign service output to variable '${reference}'" + } + } + } + ?: logger.warn { + "service output does not contain expected variable '${reference}'" + } + } + + action.emits.forEach { conditionalEvent -> + if (conditionalEvent.provided?.evaluate(scope.extent) != false) { + val emittedEvent = conditionalEvent.event.copy(data = output) + if (emittedEvent.channel == EventChannel.INTERNAL) { + eventHandler.propagateToParent(emittedEvent) + } else { + eventHandler.emit(emittedEvent) + } } } } diff --git a/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/Event.kt b/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/Event.kt index 60ce8fa8..0facfa43 100644 --- a/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/Event.kt +++ b/src/main/kotlin/at/ac/uibk/dps/cirrina/execution/object/Event.kt @@ -1,5 +1,6 @@ package at.ac.uibk.dps.cirrina.execution.`object` +import at.ac.uibk.dps.cirrina.csm.Csml.ConditionalEventDescription import at.ac.uibk.dps.cirrina.csm.Csml.EventChannel import at.ac.uibk.dps.cirrina.csm.Csml.EventDescription import at.ac.uibk.dps.cirrina.util.getInsecureUuid @@ -29,3 +30,17 @@ data class Event( } } } + +data class ConditionalEvent(val provided: Guard?, val event: Event) { + override fun toString(): String = + "${this::class.simpleName}(provided='$provided', event='$event')" + + companion object { + fun from(description: ConditionalEventDescription): ConditionalEvent { + return ConditionalEvent( + description.provided?.let { Guard.from(it) }, + Event.from(description.event), + ) + } + } +} diff --git a/src/main/resources/pkl/csm/csml.pkl b/src/main/resources/pkl/csm/csml.pkl index ec2c3b67..668f6970 100644 --- a/src/main/resources/pkl/csm/csml.pkl +++ b/src/main/resources/pkl/csm/csml.pkl @@ -96,7 +96,13 @@ class InvokeDescription extends ActionDescription { type: InvocationType mode: InvocationMode = "remote" input: Context - emits: Listing + output: Listing + emits: Listing +} + +class ConditionalEventDescription { + provided: Expression? + event: EventDescription } class EvalDescription extends ActionDescription { @@ -187,6 +193,7 @@ typealias Transition = TransitionDescription typealias Action = ActionDescription typealias Eval = EvalDescription typealias Invoke = InvokeDescription +typealias ConditionalEvent = ConditionalEventDescription typealias Case = CaseDescription typealias Match = MatchDescription typealias Emit = EmitDescription diff --git a/src/test/kotlin/at/ac/uibk/dps/cirrina/InvokeTest.kt b/src/test/kotlin/at/ac/uibk/dps/cirrina/InvokeTest.kt index af3d1741..0a8fbe9c 100644 --- a/src/test/kotlin/at/ac/uibk/dps/cirrina/InvokeTest.kt +++ b/src/test/kotlin/at/ac/uibk/dps/cirrina/InvokeTest.kt @@ -36,7 +36,7 @@ class InvokeTest { val duration = measureTime { runtime.run() } println("invoke execution: $duration") - assertEquals(10, context.get("v")) + assertEquals(100, context.get("v")) } finally { server.stop(1) } diff --git a/src/test/kotlin/at/ac/uibk/dps/cirrina/LoopTest.kt b/src/test/kotlin/at/ac/uibk/dps/cirrina/LoopTest.kt new file mode 100644 index 00000000..2f5cfce8 --- /dev/null +++ b/src/test/kotlin/at/ac/uibk/dps/cirrina/LoopTest.kt @@ -0,0 +1,34 @@ +package at.ac.uibk.dps.cirrina + +import at.ac.uibk.dps.cirrina.data.DefaultDescriptions +import at.ac.uibk.dps.cirrina.di.DaggerTestComponent +import at.ac.uibk.dps.cirrina.di.TestModule +import at.ac.uibk.dps.cirrina.execution.provider.ContextInMemory +import java.time.Duration +import kotlin.time.measureTime +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertTimeout + +class LoopTest { + @Test + fun testLoopExecute() { + assertTimeout(Duration.ofSeconds(10)) { + assertDoesNotThrow { + val context = ContextInMemory() + + val runtime = + DaggerTestComponent.builder() + .testModule(TestModule(context, DefaultDescriptions.loop, listOf("one", "two"))) + .build() + .runtime() + + val duration = measureTime { runtime.run() } + println("loop execution: $duration") + + assertEquals(1, context.get("v")) + } + } + } +} diff --git a/src/test/kotlin/at/ac/uibk/dps/cirrina/data/DefaultDescriptions.kt b/src/test/kotlin/at/ac/uibk/dps/cirrina/data/DefaultDescriptions.kt index 0b1cdf95..6b198571 100644 --- a/src/test/kotlin/at/ac/uibk/dps/cirrina/data/DefaultDescriptions.kt +++ b/src/test/kotlin/at/ac/uibk/dps/cirrina/data/DefaultDescriptions.kt @@ -11,4 +11,5 @@ object DefaultDescriptions { val noop: URI by lazy { resourceUri("pkl/noop/main.pkl") } val empty: URI by lazy { resourceUri("pkl/empty/main.pkl") } val events: URI by lazy { resourceUri("pkl/events/main.pkl") } + val loop: URI by lazy { resourceUri("pkl/loop/main.pkl") } } diff --git a/src/test/resources/pkl/complete/main.pkl b/src/test/resources/pkl/complete/main.pkl index 7e007f31..4f59bba7 100644 --- a/src/test/resources/pkl/complete/main.pkl +++ b/src/test/resources/pkl/complete/main.pkl @@ -28,7 +28,7 @@ collaborativeStateMachine { type = "increment" mode = "local" input { ["v"] = "x" } - emits { new Internal { topic = "e2" } } + emits { new ConditionalEvent { event = new Internal { topic = "e2" } } } } } exit { new Eval { expression = "e = true" } } diff --git a/src/test/resources/pkl/invoke/main.pkl b/src/test/resources/pkl/invoke/main.pkl index d7785ddb..b6075472 100644 --- a/src/test/resources/pkl/invoke/main.pkl +++ b/src/test/resources/pkl/invoke/main.pkl @@ -7,7 +7,7 @@ collaborativeStateMachine { ["a"] = new Initial { after { ["timeout"] = new Timeout { - delay = "100" + delay = "1" triggers = new Emit { event = new Internal { topic = "update" } } } } @@ -19,24 +19,15 @@ collaborativeStateMachine { type = "increment" mode = "local" input { ["v"] = "v" } - emits { new Internal { topic = "tob" } } + output { "v" } + emits { new ConditionalEvent { provided = "v >= 100"; event = new Internal { topic = "tob" } } } } - new Eval { expression = "e = e + 1" } } } ["tob"] = new Transition { to = "b" } } } - ["b"] = new State { - entry { new Eval { expression = "v = $v" } } - always { - new Transition { - to = "a" - provided = "v < 10"; or = "c" - } - } - } - ["c"] = new Terminal {} + ["b"] = new Terminal {} } } } diff --git a/src/test/resources/pkl/loop/main.pkl b/src/test/resources/pkl/loop/main.pkl new file mode 100644 index 00000000..3e78e656 --- /dev/null +++ b/src/test/resources/pkl/loop/main.pkl @@ -0,0 +1,49 @@ +amends "modulepath:/pkl/csm/csml.pkl" + +collaborativeStateMachine { + stateMachines { + ["one"] { + states { + ["a"] = new Initial { + entry { + new Emit { event { topic = "e1" }; target = "'two'" } + } + always { + new Transition { to = "b" } + } + } + ["b"] = new Terminal {} + } + } + ["two"] { + states { + ["a"] = new Initial { + after { + ["t"] = new Timeout { + delay = "1000" + triggers = new Emit { event = new Internal { topic = "toc" } } + } + } + on { + ["e1"] { to = "b" } + ["toc"] { to = "c" } + } + } + ["b"] { + entry { + new Eval { expression = "v =+ 1" } + } + always { + new Transition { to = "a"; provided = "v < 1000" } + } + } + ["c"] = new Terminal {} + } + } + } + persistent { ["v"] = "0" } +} +instances { + ["one"] { stateMachineName = "one" } + ["two"] { stateMachineName = "two" } +} \ No newline at end of file