diff --git a/core/src/main/java/org/lflang/AttributeUtils.java b/core/src/main/java/org/lflang/AttributeUtils.java
index 095226d51d..9b94267fe0 100644
--- a/core/src/main/java/org/lflang/AttributeUtils.java
+++ b/core/src/main/java/org/lflang/AttributeUtils.java
@@ -187,6 +187,11 @@ public static boolean isSparse(EObject node) {
return findAttributeByName(node, "sparse") != null;
}
+ /** Return true if the node has an {@code @transient} attribute. */
+ public static boolean isTransient(Instantiation node) {
+ return findAttributeByName(node, "transient") != null;
+ }
+
/**
* Return true if the reactor is marked to be a federate.
*
diff --git a/core/src/main/java/org/lflang/federated/extensions/CExtension.java b/core/src/main/java/org/lflang/federated/extensions/CExtension.java
index 124f540b8b..257ba1b323 100644
--- a/core/src/main/java/org/lflang/federated/extensions/CExtension.java
+++ b/core/src/main/java/org/lflang/federated/extensions/CExtension.java
@@ -467,7 +467,7 @@ public String generatePortAbsentReactionBody(
+ receivingPortID
+ ", "
+ connection.getDstFederate().id
- + ", (long long) lf_time_logical_elapsed());",
+ + ", lf_time_logical_elapsed());",
"if (" + sendRef + " == NULL || !" + sendRef + "->is_present) {",
"LF_PRINT_LOG(\"The output port is NULL or it is not present.\");",
" lf_send_port_absent_to_federate("
@@ -556,15 +556,33 @@ protected String makePreamble(
// that handles incoming network messages destined to the specified
// port. This will only be used if there are federates.
int numOfNetworkActions = federate.networkMessageActions.size();
+ int numZDCNetworkActions = federate.zeroDelayCycleNetworkMessageActions.size();
code.pr(
"""
interval_t _lf_action_delay_table[%1$s];
lf_action_base_t* _lf_action_table[%1$s];
size_t _lf_action_table_size = %1$s;
- lf_action_base_t* _lf_zero_delay_cycle_action_table[%2$s];
- size_t _lf_zero_delay_cycle_action_table_size = %2$s;
"""
- .formatted(numOfNetworkActions, federate.zeroDelayCycleNetworkMessageActions.size()));
+ .formatted(numOfNetworkActions));
+ if (numZDCNetworkActions > 0) {
+ code.pr(
+ """
+ lf_action_base_t* _lf_zero_delay_cycle_action_table[%1$s];
+ size_t _lf_zero_delay_cycle_action_table_size = %1$s;
+ uint16_t _lf_zero_delay_cycle_upstream_ids[%1$s];
+ bool _lf_zero_delay_cycle_upstream_disconnected[%1$s] = { false };
+ """
+ .formatted(numZDCNetworkActions));
+ } else {
+ // Make sure these symbols are defined, even though only size will be used.
+ code.pr(
+ """
+ lf_action_base_t** _lf_zero_delay_cycle_action_table = NULL;
+ size_t _lf_zero_delay_cycle_action_table_size = 0;
+ uint16_t* _lf_zero_delay_cycle_upstream_ids = NULL;
+ bool* _lf_zero_delay_cycle_upstream_disconnected = NULL;
+ """);
+ }
int numOfNetworkReactions = federate.networkReceiverReactions.size();
code.pr(
@@ -732,6 +750,8 @@ else if (globalSTP instanceof CodeExprImpl)
}
// Set global variable identifying the federate.
code.pr("_lf_my_fed_id = " + federate.id + ";");
+ // Set indicator variable that specifies whether the federate is transient or not.
+ code.pr("_fed.is_transient = " + federate.isTransient + ";");
// We keep separate record for incoming and outgoing p2p connections to allow incoming traffic
// to be processed in a separate
diff --git a/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java b/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java
index 87f2d59c90..977059ae6d 100644
--- a/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java
+++ b/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java
@@ -60,7 +60,6 @@ public static String initializeTriggersForNetworkActions(
CodeBuilder code = new CodeBuilder();
if (!federate.networkMessageActions.isEmpty()) {
var actionTableCount = 0;
- var zeroDelayActionTableCount = 0;
for (int i = 0; i < federate.networkMessageActions.size(); ++i) {
// Find the corresponding ActionInstance.
Action action = federate.networkMessageActions.get(i);
@@ -83,10 +82,17 @@ public static String initializeTriggersForNetworkActions(
// Set the ID of the source federate.
code.pr(
trigger + ".source_id = " + federate.networkMessageSourceFederate.get(i).id + "; \\");
- if (federate.zeroDelayCycleNetworkMessageActions.contains(action)) {
+ int j = federate.zeroDelayCycleNetworkMessageActions.indexOf(action);
+ if (j >= 0) {
+ var upstream = federate.zeroDelayCycleNetworkUpstreamFeds.get(j);
+ code.pr("_lf_zero_delay_cycle_upstream_ids[" + j + "] = " + upstream.id + "; \\");
+ if (upstream.isTransient) {
+ // Transient federates are assumed to be initially disconnected.
+ code.pr("_lf_zero_delay_cycle_upstream_disconnected[" + j + "] = true; \\");
+ }
code.pr(
"_lf_zero_delay_cycle_action_table["
- + zeroDelayActionTableCount++
+ + j
+ "] = (lf_action_base_t*)&"
+ trigger
+ "; \\");
@@ -152,7 +158,8 @@ public static String stpStructs(FederateInstance federate) {
*/
public static String createPortStatusFieldForInput(Input input) {
StringBuilder builder = new StringBuilder();
- // If it is not a multiport, then we could re-use the port trigger, and nothing needs to be done
+ // If it is not a multiport, then we could re-use the port trigger, and nothing
+ // needs to be done
if (ASTUtils.isMultiport(input)) {
// If it is a multiport, then create an auxiliary list of port
// triggers for each channel of
@@ -241,12 +248,13 @@ static boolean clockSyncIsOn(FederateInstance federate, RtiConfig rtiConfig) {
*
*
Clock synchronization can be enabled using the clock-sync target property.
*
- * @see Documentation
+ * @see Documentation
*/
public static void initializeClockSynchronization(
FederateInstance federate, RtiConfig rtiConfig, MessageReporter messageReporter) {
- // Check if clock synchronization should be enabled for this federate in the first place
+ // Check if clock synchronization should be enabled for this federate in the
+ // first place
if (clockSyncIsOn(federate, rtiConfig)) {
messageReporter
.nowhere()
@@ -272,8 +280,8 @@ public static void initializeClockSynchronization(
*
*
Clock synchronization can be enabled using the clock-sync target property.
*
- * @see Documentation
+ * @see Documentation
*/
public static void addClockSyncCompileDefinitions(FederateInstance federate) {
@@ -316,8 +324,15 @@ public static void generateCMakeInclude(
"add_compile_definitions(LF_SOURCE_DIRECTORY=\"" + fileConfig.srcPath + "\")");
cmakeIncludeCode.pr(
"add_compile_definitions(LF_PACKAGE_DIRECTORY=\"" + fileConfig.srcPkgPath + "\")");
+ // After federates have been divided, their root package directory is different.
cmakeIncludeCode.pr(
- "add_compile_definitions(LF_SOURCE_GEN_DIRECTORY=\"" + fileConfig.getSrcGenPath() + "\")");
+ "add_compile_definitions(LF_FED_PACKAGE_DIRECTORY=\""
+ + fileConfig.srcPkgPath
+ + File.separator
+ + "fed-gen"
+ + File.separator
+ + fileConfig.name
+ + "\")");
cmakeIncludeCode.pr("add_compile_definitions(LF_FILE_SEPARATOR=\"" + File.separator + "\")");
try (var srcWriter = Files.newBufferedWriter(cmakeIncludePath)) {
srcWriter.write(cmakeIncludeCode.getCode());
diff --git a/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java b/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java
index 4eafe71ea0..6fdd6b20dd 100644
--- a/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java
+++ b/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java
@@ -314,9 +314,10 @@ private static void addNetworkReceiverReactor(
connection.dstFederate.networkMessageSourceFederate.add(connection.srcFederate);
connection.dstFederate.networkMessageActionDelays.add(connection.getDefinition().getDelay());
if (connection.srcFederate.isInZeroDelayCycle()
- && connection.getDefinition().getDelay() == null)
+ && connection.getDefinition().getDelay() == null) {
connection.dstFederate.zeroDelayCycleNetworkMessageActions.add(networkAction);
-
+ connection.dstFederate.zeroDelayCycleNetworkUpstreamFeds.add(connection.srcFederate);
+ }
// Get the largest STAA for any reaction triggered by the destination port.
TimeValue maxSTAA = findMaxSTAA(connection, coordination);
diff --git a/core/src/main/java/org/lflang/federated/generator/FederateInstance.java b/core/src/main/java/org/lflang/federated/generator/FederateInstance.java
index 27bd1ef974..8980df35a7 100644
--- a/core/src/main/java/org/lflang/federated/generator/FederateInstance.java
+++ b/core/src/main/java/org/lflang/federated/generator/FederateInstance.java
@@ -13,6 +13,7 @@
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.lflang.AttributeUtils;
import org.lflang.MessageReporter;
import org.lflang.TimeValue;
import org.lflang.ast.ASTUtils;
@@ -73,6 +74,7 @@ public FederateInstance(
this.bankWidth = bankWidth;
this.messageReporter = messageReporter;
this.targetConfig = targetConfig;
+ this.isTransient = AttributeUtils.isTransient(instantiation);
// If the instantiation is in a bank, then we have to append
// the bank index to the name.
@@ -134,6 +136,9 @@ public Instantiation getInstantiation() {
/** The integer ID of this federate. */
public int id;
+ /** Type of the federate: transient if true, and peristent if false . */
+ public boolean isTransient = false;
+
/**
* The name of this federate instance. This will be the instantiation name, possibly appended with
* "__n", where n is the bank position of this instance if the instantiation is of a bank of
@@ -165,6 +170,12 @@ public Instantiation getInstantiation() {
*/
public List zeroDelayCycleNetworkMessageActions = new ArrayList<>();
+ /**
+ * List of upstream federates corresponding to actions in the zeroDelayCycleNetworkMessageActions
+ * list.
+ */
+ public List zeroDelayCycleNetworkUpstreamFeds = new ArrayList<>();
+
/**
* A set of federates with which this federate has an inbound connection There will only be one
* physical connection even if federate A has defined multiple physical connections to federate B.
diff --git a/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java b/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java
index f4d6dbc132..bb7c06df1e 100644
--- a/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java
+++ b/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java
@@ -304,6 +304,11 @@ private String getSetupCode() {
+ " tmux: 2)\"",
" echo \" -h, --help Show this help message\"",
" echo \"\"",
+ " echo \"tmux: This launcher enables mouse mode (pane focus, resize, scroll).\"",
+ " echo \" To copy text to the clipboard, hold Shift or Function while dragging"
+ + " to select\"",
+ " echo \" (most terminals), or press Ctrl+b then [ to enter copy mode.\"",
+ " echo \"\"",
" echo \"All other arguments are forwarded to each federate.\"",
" echo \"For available federate parameters, run a federate binary with --help.\"",
" exit 0",
@@ -370,12 +375,20 @@ private String getRtiCommand(
if (targetConfig.getOrDefault(TracingProperty.INSTANCE).isEnabled()) {
commands.add(" -t \\");
}
+ // Identify the number of transient federates.
+ int transientFederatesNumber = 0;
+ for (FederateInstance federate : federates) {
+ if (federate.isTransient) {
+ transientFederatesNumber++;
+ }
+ }
if (!targetConfig.getOrDefault(DNETProperty.INSTANCE)) {
commands.add(" -d \\");
}
commands.addAll(
List.of(
" -n " + federates.size() + " \\",
+ " -nt " + transientFederatesNumber + " \\",
" -c "
+ targetConfig.getOrDefault(ClockSyncModeProperty.INSTANCE).toString()
+ " \\"));
@@ -790,7 +803,7 @@ private String getTmuxLaunchCode(List federates, RtiConfig rti
// Assign pane titles. The RTI title doubles as an instruction banner.
lines.add(
" tmux select-pane -t \"$RTI_PANE\" -T"
- + " \"RTI — Ctrl+C to stop | Ctrl+B d to detach and kill\"");
+ + " \"RTI — Ctrl+C stop | C-b d detach | Shift or Fn+drag: copy\"");
for (int i = 0; i < numFeds; i++) {
lines.add(
" tmux select-pane -t \"$FED_PANE_" + i + "\" -T \"" + federates.get(i).name + "\"");
@@ -827,6 +840,14 @@ private String getTmuxLaunchCode(List federates, RtiConfig rti
}
lines.add("");
+ // Mouse mode is enabled above so tmux handles the mouse; Shift+drag (or copy mode) is needed
+ // for system clipboard selection in most terminals.
+ lines.add(" echo \"\"");
+ lines.add(
+ " echo \"tmux: Hold Shift or Function while dragging to select text for the clipboard,"
+ + " or Ctrl+b [ for copy mode.\"");
+ lines.add("");
+
// Focus the RTI pane and attach to the session.
lines.add(" tmux select-pane -t \"$RTI_PANE\"");
lines.add(" tmux attach-session -t \"$SESSION_NAME\"");
diff --git a/core/src/main/java/org/lflang/generator/c/CCompiler.java b/core/src/main/java/org/lflang/generator/c/CCompiler.java
index 60ec7f1ad7..225990a2bd 100644
--- a/core/src/main/java/org/lflang/generator/c/CCompiler.java
+++ b/core/src/main/java/org/lflang/generator/c/CCompiler.java
@@ -87,9 +87,9 @@ public boolean runCCompiler(GeneratorBase generator, LFGeneratorContext context)
// avoid any error residue that can occur in CMake from
// a previous build.
// FIXME: This is slow and only needed if an error
- // has previously occurred. Deleting the build directory
- // if no prior errors have occurred can prolong the compilation
- // substantially. See #1416 for discussion.
+ // has previously occurred. Deleting the build directory
+ // if no prior errors have occurred can prolong the compilation
+ // substantially. See #1416 for discussion.
FileUtil.deleteDirectory(buildPath);
// Make sure the build directory exists
Files.createDirectories(buildPath);
@@ -218,14 +218,15 @@ private static List cmakeOptions(TargetConfig targetConfig, FileConfig f
+ FileUtil.toUnixString(fileConfig.getOutPath().relativize(fileConfig.binPath)),
"-DLF_FILE_SEPARATOR='" + quote + separator + quote + "'"));
// Add #define for source file directory.
- // Do not do this for federated programs because for those, the definition is put
- // into the cmake file (and fileConfig.srcPath is the wrong directory anyway).
+ // Do not do this for federated programs because for those, the definition is
+ // put into the cmake file (and fileConfig.srcPath is the wrong directory
+ // anyway).
if (!fileConfig.srcPath.toString().contains("fed-gen")) {
- // Do not convert to Unix path
+ // Do not convert to Unix path. Do not add quotes here; CMake defineString() adds them.
arguments.add("-DLF_SOURCE_DIRECTORY=" + srcPath);
arguments.add("-DLF_PACKAGE_DIRECTORY=" + rootPath);
- arguments.add("-DLF_SOURCE_GEN_DIRECTORY=" + srcGenPath);
}
+ arguments.add("-DLF_SOURCE_GEN_DIRECTORY=" + srcGenPath);
// Append user-provided CMake configure definitions. These come after built-ins so they can
// override defaults (e.g., CMAKE_BUILD_TYPE).
diff --git a/core/src/main/java/org/lflang/validation/AttributeSpec.java b/core/src/main/java/org/lflang/validation/AttributeSpec.java
index 26cca74cf3..b291644856 100644
--- a/core/src/main/java/org/lflang/validation/AttributeSpec.java
+++ b/core/src/main/java/org/lflang/validation/AttributeSpec.java
@@ -231,6 +231,8 @@ enum AttrParamType {
new AttributeSpec(List.of(new AttrParamSpec(VALUE_ATTR, AttrParamType.TIME, false))));
// @sparse
ATTRIBUTE_SPECS_BY_NAME.put("sparse", new AttributeSpec(null));
+ // @transient
+ ATTRIBUTE_SPECS_BY_NAME.put("transient", new AttributeSpec(null));
// @icon("value")
ATTRIBUTE_SPECS_BY_NAME.put(
"icon",
diff --git a/core/src/main/java/org/lflang/validation/LFValidator.java b/core/src/main/java/org/lflang/validation/LFValidator.java
index 16986060e2..6769893377 100644
--- a/core/src/main/java/org/lflang/validation/LFValidator.java
+++ b/core/src/main/java/org/lflang/validation/LFValidator.java
@@ -1353,6 +1353,8 @@ public void checkAttributes(Attribute attr) {
checkMaxWaitAttribute(attr);
} else if (name.equals("absent_after")) {
checkAbsentAfterAttribute(attr);
+ } else if (name.equals("transient")) {
+ checkTransientAttribute(attr);
}
if (GLOBAL_ATTRIBUTE_NAMES.contains(name)) {
checkGlobalAttribute(attr);
@@ -1417,6 +1419,25 @@ private void checkAbsentAfterAttribute(Attribute attr) {
}
}
+ /** @transient marks a federate instantiation as transient (join/leave during execution). */
+ private void checkTransientAttribute(Attribute attr) {
+ EObject container = attr.eContainer();
+ if (!(container instanceof Instantiation)) {
+ error(
+ "The @transient attribute can only be applied to a federate instantiation.",
+ attr,
+ Literals.ATTRIBUTE__ATTR_NAME);
+ return;
+ }
+ EObject parent = container.eContainer();
+ if (!(parent instanceof Reactor reactor) || !reactor.isFederated()) {
+ error(
+ "The @transient attribute can only be applied inside a federated reactor.",
+ attr,
+ Literals.ATTRIBUTE__ATTR_NAME);
+ }
+ }
+
@Check(CheckType.FAST)
public void checkWidthSpec(WidthSpec widthSpec) {
if (!this.target.supportsMultiports()) {
diff --git a/core/src/main/resources/lib/c/reactor-c b/core/src/main/resources/lib/c/reactor-c
index 51034b92f1..a144507760 160000
--- a/core/src/main/resources/lib/c/reactor-c
+++ b/core/src/main/resources/lib/c/reactor-c
@@ -1 +1 @@
-Subproject commit 51034b92f1da8558bd54e9ac8d610de3a8012bfc
+Subproject commit a144507760a67634e4b16a34e8cb1192afbcbd5c
diff --git a/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java b/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java
index 4f1603a532..2fd91c33d4 100644
--- a/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java
+++ b/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java
@@ -1080,6 +1080,57 @@ public void testFederationSupport() throws Exception {
}
}
+ @Test
+ public void testTransientAttributeOnFederateInstantiation() throws Exception {
+ Model model =
+ parseWithoutError(
+ """
+ target C
+ reactor Foo {}
+ federated reactor {
+ @transient
+ foo = new Foo()
+ bar = new Foo()
+ }
+ """);
+ validator.assertNoIssues(model);
+ }
+
+ @Test
+ public void testTransientAttributeRejectedOnMainReactor() throws Exception {
+ Model model =
+ parseWithoutError(
+ """
+ target C
+ @transient
+ main reactor {}
+ """);
+ validator.assertError(
+ model,
+ LfPackage.eINSTANCE.getAttribute(),
+ null,
+ "The @transient attribute can only be applied to a federate instantiation.");
+ }
+
+ @Test
+ public void testTransientAttributeRejectedOnInput() throws Exception {
+ Model model =
+ parseWithoutError(
+ """
+ target C
+ reactor R {
+ @transient
+ input x: int
+ }
+ main reactor {}
+ """);
+ validator.assertError(
+ model,
+ LfPackage.eINSTANCE.getAttribute(),
+ null,
+ "The @transient attribute can only be applied to a federate instantiation.");
+ }
+
/** Tests for state and parameter declarations, including native lists. */
@Test
public void stateAndParameterDeclarationsInC() throws Exception {
diff --git a/test/C/Makefile b/test/C/Makefile
new file mode 100644
index 0000000000..9f48f8bc9b
--- /dev/null
+++ b/test/C/Makefile
@@ -0,0 +1,50 @@
+# This file lets you format the code-base with a single command.
+FILES := $(shell find . -name '*.lf' -not -path "*/fed-gen/*")
+
+.PHONY: help
+help:
+ @echo "Available commands:"
+ @echo " make clean - Clean up build and documentation"
+ @echo " make format - Format all Lingua Franca files using lff"
+ @echo " make format-dev - Format all Lingua Franca files using lff-dev"
+ @echo " make build-all - Build all Lingua Franca files using lfc"
+ @echo " make build-all-dev - Build all Lingua Franca files using lfc-dev"
+
+.PHONY: format
+format:
+ @for file in $(FILES); do \
+ echo "========== Formatting $$file =========="; \
+ lff $$file; \
+ done
+
+.PHONY: format-dev
+format-dev:
+ @for file in $(FILES); do \
+ echo "========== Formatting $$file =========="; \
+ lff-dev $$file; \
+ done
+
+.PHONY: build-all
+build-all:
+ @for file in $(FILES); do \
+ echo "================================================"; \
+ echo "Building $$file"; \
+ echo "================================================"; \
+ lfc $$file; \
+ done
+
+.PHONY: build-all-dev
+build-all-dev:
+ @for file in $(FILES); do \
+ echo "================================================"; \
+ echo "Building $$file"; \
+ echo "================================================"; \
+ lfc-dev $$file; \
+ done
+
+.PHONY: clean
+clean:
+ rm -rf *.lft *.csv *.log src-gen fed-gen include bin
+
+# Set help as the default target
+.DEFAULT_GOAL := help
diff --git a/test/C/src/federated/transient/TransientDownstreamWithTimer.lf b/test/C/src/federated/transient/TransientDownstreamWithTimer.lf
new file mode 100644
index 0000000000..9aeed757ab
--- /dev/null
+++ b/test/C/src/federated/transient/TransientDownstreamWithTimer.lf
@@ -0,0 +1,158 @@
+/**
+ * This LF program tests if a transient federate correctly leaves then joins the federation. It also
+ * tests whether the transient's downstream executes as expected, that is it receives correct TAGs,
+ * regardless of the transient being absent or present. In this test:
+ * - the transient federate spontaneously leaves the federation after 2 reactions to input port
+ * `in`,
+ * - the downstream of the transient federate has only one transient as upstream.
+ */
+target C {
+ timeout: 3 s
+}
+
+preamble {=
+ #include
+ #include
+=}
+
+/** Persistent federate that is responsible for lauching the transient federate */
+reactor TransientExec(launch_time: time = 0, fed_instance_name: char* = "instance") {
+ timer t(launch_time, 0)
+
+ reaction(t) {=
+ // Construct the command to launch the transient federate
+ char mid_launch_cmd[512];
+ sprintf(mid_launch_cmd,
+ "%s/bin/federate__%s -i %s",
+ LF_FED_PACKAGE_DIRECTORY,
+ self->fed_instance_name,
+ lf_get_federation_id()
+ );
+
+ lf_print("Launching federate federate__%s at physical time " PRINTF_TIME ".",
+ self->fed_instance_name, lf_time_physical());
+ lf_print("**** Launch command: %s", mid_launch_cmd);
+
+ int status = system(mid_launch_cmd);
+
+ // Exit if error
+ if (status == 0) {
+ lf_print("Successfully launched federate__%s.", self->fed_instance_name);
+ } else {
+ lf_print_error_and_exit("Unable to launch federate__%s. Abort!", self->fed_instance_name);
+ }
+ =}
+}
+
+/**
+ * Persistent federate, upstream of the transient. It reacts to its timer by sending increments to
+ * output port out.
+ */
+reactor Up(period: time = 500 ms) {
+ output out: int
+ timer t(0, period)
+ state count: int = 0
+
+ reaction(t) -> out {=
+ lf_set(out, self->count);
+ self->count++;
+ =}
+}
+
+/**
+ * Transient federate that forwards whatever it receives from `Up` to `Down`. It reacts twice to
+ * input port `in`, then stops. It will execute twice during the lifetime of the federation. The
+ * second launch is done by `TransientExec` at logical time 1 s. Each time `Middle` joins, it
+ * notifies `Down`.
+ */
+reactor Middle {
+ input in: int
+ output out: int
+ output join: int
+ state count: int = 0
+
+ // Middle notifies its downstream that he joined, but make sure first that the effective start
+ // tag is correct
+ reaction(startup) -> join {=
+ tag_t t = lf_tag_start_effective();
+ if(t.time < lf_time_start()) {
+ lf_print_error_and_exit("Fatal error: the transient's effective start time is less than the federation start time");
+ }
+
+ lf_set(join, 0);
+ =}
+
+ // Pass the input value to the output port and stop spontaneously after two reactions to in
+ reaction(in) -> out {=
+ self->count++;
+ lf_set(out, in->value);
+
+ if (self->count == 2) {
+ lf_stop();
+ }
+ =}
+}
+
+/**
+ * Persistent federate, which is downstream of the transient. It has to keep reacting to its
+ * internal timer and also to inputs from the tansient, if any.
+ */
+reactor Down {
+ timer t(0, 500 ms)
+
+ input in: int
+ input join: int
+
+ state count_timer: int = 0
+ state count_join: int = 0
+ state count_in_mid_reactions: int = 0
+
+ reaction(t) {=
+ self->count_timer++;
+ =}
+
+ reaction(join) {=
+ self->count_join++;
+ =}
+
+ reaction(in) {=
+ self->count_in_mid_reactions++;
+ =}
+
+ reaction(shutdown) {=
+ // Check that the TAG has been successfully issued to Down
+ if (self->count_timer < 5) {
+ lf_print_error_and_exit("Down federate's timer reacted %d times, while it had to react more than %d times.",
+ self->count_timer, 5);
+ }
+
+ // Check that `Middle` have joined 2 times
+ if (self->count_join != 2) {
+ lf_print_error_and_exit("Transient federate did not join twice, but %d times!", self->count_join);
+ }
+
+ // Check that `Middle` have reacted correctly
+ if (self->count_in_mid_reactions < 4) {
+ lf_print_error_and_exit("Transient federate Mid did not execute and pass values from up corretly! Expected >= 4, but had: %d.",
+ self->count_in_mid_reactions);
+ }
+ =}
+}
+
+federated reactor {
+ // Persistent federate that is responsible for lauching the transient once, after 1s
+ midExec = new TransientExec(launch_time = 1 s, fed_instance_name="mid")
+
+ // Persistent downstream and upstream federates of the transient
+ up = new Up()
+ down = new Down()
+
+ // Transient federate
+ @transient
+ mid = new Middle()
+
+ // Connections
+ up.out -> mid.in
+ mid.join -> down.join
+ mid.out -> down.in
+}
diff --git a/test/C/src/federated/transient/TransientDownstreamWithTwoUpstream.lf b/test/C/src/federated/transient/TransientDownstreamWithTwoUpstream.lf
new file mode 100644
index 0000000000..e0692f4c22
--- /dev/null
+++ b/test/C/src/federated/transient/TransientDownstreamWithTwoUpstream.lf
@@ -0,0 +1,128 @@
+/**
+ * This LF program tests whether a transient federate corretly leaves then joins the federation. It
+ * also tests whether the transient's downstream executes as expected, that is it received correct
+ * TAGs, regardless of the transient being absent or present. In this test:
+ * - the transient federate spontaneously leaves the federation after 2 reactions to input port in,
+ * - the downstream of the transient federate has one persistent and one transient upstreams.
+ *
+ * In addition, the program tests if authentication works in case of a federation with transients,
+ * by adding `auth` target property.
+ */
+target C {
+ timeout: 3 s,
+ auth: true
+}
+
+import Up from "TransientDownstreamWithTimer.lf"
+import Middle from "TransientDownstreamWithTimer.lf"
+
+preamble {=
+ #include
+ #include
+=}
+
+/** Persistent federate that is responsible for lauching the transient federate */
+reactor TransientExec(launch_time: time = 0, fed_instance_name: char* = "instance") {
+ timer t(launch_time, 0)
+
+ reaction(t) {=
+ // Construct the command to launch the transient federate
+ char mid_launch_cmd[512];
+ sprintf(mid_launch_cmd,
+ "%s/bin/federate__%s -i %s",
+ LF_FED_PACKAGE_DIRECTORY,
+ self->fed_instance_name,
+ lf_get_federation_id()
+ );
+
+ lf_print("Launching federate federate__%s at physical time " PRINTF_TIME ".",
+ self->fed_instance_name, lf_time_physical());
+
+ int status = system(mid_launch_cmd);
+
+ // Exit if error
+ if (status == 0) {
+ lf_print("Successfully launched federate__%s.", self->fed_instance_name);
+ } else {
+ lf_print_error_and_exit("Unable to launch federate__%s. Abort!", self->fed_instance_name);
+ }
+ =}
+}
+
+/**
+ * Persistent federate, which is downstream of the transient. It has to keep reacting to its
+ * internal timer and also to inputs from the tansient, if any.
+ */
+reactor Down {
+ timer t(0, 500 ms)
+
+ input in_mid: int
+ input in_up: int
+ input join: int
+
+ state count_timer: int = 0
+ state count_join: int = 0
+ state count_in_mid_reactions: int = 0
+ state count_in_up_reactions: int = 0
+
+ reaction(t) {=
+ self->count_timer++;
+ =}
+
+ reaction(join) {=
+ self->count_join++;
+ =}
+
+ reaction(in_mid) {=
+ self->count_in_mid_reactions++;
+ =}
+
+ reaction(in_up) {=
+ self->count_in_up_reactions++;
+ =}
+
+ reaction(shutdown) {=
+ // Check that the TAG have been successfully issued to Down
+ if (self->count_timer < 5) {
+ lf_print_error_and_exit("Federate's timer reacted %d times, while it had to react more than %d times.",
+ self->count_timer,
+ 5);
+ }
+ if (self->count_in_up_reactions < 7) {
+ lf_print_error_and_exit("Federate's timer reacted %d times, while it had to react more than %d times.",
+ self->count_in_up_reactions,
+ 7);
+ }
+
+ // Check that Middle have joined 2 times
+ if (self->count_join != 2) {
+ lf_print_error_and_exit("Transient federate did not join twice, but %d times!", self->count_join);
+ }
+
+ // Check that Middle have reacted correctly
+ if (self->count_in_mid_reactions < 4) {
+ lf_print_error_and_exit("Transient federate Mid did not execute and pass values from up corretly! Expected >= 4, but had: %d.",
+ self->count_in_mid_reactions);
+ }
+ =}
+}
+
+federated reactor {
+ // Persistent federate that is responsible for lauching the transient once, after 1s
+ midExec = new TransientExec(launch_time = 1 s, fed_instance_name="mid")
+
+ // Persistent downstream and upstream federates of the transient
+ up1 = new Up()
+ up2 = new Up(period = 300 msec)
+ down = new Down()
+
+ // Transient federate
+ @transient
+ mid = new Middle()
+
+ // Connections
+ up1.out -> mid.in
+ mid.join -> down.join
+ mid.out -> down.in_mid
+ up2.out -> down.in_up
+}
diff --git a/test/C/src/federated/transient/TransientHotSwap.lf b/test/C/src/federated/transient/TransientHotSwap.lf
new file mode 100644
index 0000000000..683b6debd5
--- /dev/null
+++ b/test/C/src/federated/transient/TransientHotSwap.lf
@@ -0,0 +1,97 @@
+/**
+ * This LF program is a variant of TransientDownstreamWithTimer that tests the Hot Swap mechanism.
+ * For this, it tests whether the transient's downstream executes as expected and if `mid` is
+ * stopped and the second instance joins as expected. In this test:
+ * - the transient federate DOES NOT spontaneously leave the federation.
+ * - the downstream of the transient federate has only one transient as upstream.
+ * - A persistent federate `TransientExec` launches `mid` after 1s to activate the hot mechanism
+ * swap.
+ */
+target C {
+ timeout: 3 s,
+ auth: true
+}
+
+import Up from "TransientDownstreamWithTimer.lf"
+import Down from "TransientDownstreamWithTimer.lf"
+
+preamble {=
+ #include
+ #include
+=}
+
+/** Persistent federate that is responsible for lauching the transient federate */
+reactor TransientExec(launch_time: time = 0, fed_instance_name: char* = "instance") {
+ timer t(launch_time, 0)
+
+ reaction(t) {=
+ // Construct the command to launch the transient federate
+ char mid_launch_cmd[512];
+ sprintf(mid_launch_cmd,
+ "%s/bin/federate__%s -i %s",
+ LF_FED_PACKAGE_DIRECTORY,
+ self->fed_instance_name,
+ lf_get_federation_id()
+ );
+
+ lf_print("Launching federate federate__%s at physical time " PRINTF_TIME ".",
+ self->fed_instance_name, lf_time_physical());
+
+ int status = system(mid_launch_cmd);
+
+ // Exit if error
+ if (status == 0) {
+ lf_print("Successfully launched federate__%s.", self->fed_instance_name);
+ } else {
+ lf_print_error_and_exit("Unable to launch federate__%s. Abort!", self->fed_instance_name);
+ }
+ =}
+}
+
+/**
+ * Transient federate that forwards whatever it receives from `Up` to `Down`. It reacts twice to
+ * input port `in`, then stops. It will execute twice during the lifetime of the federation. The
+ * second launch is done by `TransientExec` at logical time 1 s. Each time `Middle` joins, it
+ * notifies `Down`.
+ */
+reactor Middle {
+ input in: int
+ output out: int
+ output join: int
+ state count: int = 0
+
+ // Middle notifies its downstream that he joined, but make sure first that the effective start
+ // tag is correct
+ reaction(startup) -> join {=
+ tag_t t = lf_tag_start_effective();
+ if(t.time < lf_time_start()) {
+ lf_print_error_and_exit("Fatal error: the transient's effective start time is less than the federation start time");
+ }
+
+ lf_set(join, 0);
+ =}
+
+ // Pass the input value to the output port
+ reaction(in) -> out {=
+ self->count++;
+ lf_set(out, in->value);
+ =}
+}
+
+federated reactor {
+ // Persistent federate that is responsible for lauching the transient once, after 1s
+ midExec = new TransientExec(launch_time = 1 s, fed_instance_name="mid")
+
+ // Persistent downstream and upstream federates of the transient
+ up = new Up()
+ down = new Down()
+
+ // Transient federate
+ @transient
+ mid = new Middle()
+
+ // Connections
+ up.out -> mid.in
+ mid.join -> down.join
+ mid.out -> down.in
+}
diff --git a/test/C/src/federated/transient/TransientStatePersistence.lf b/test/C/src/federated/transient/TransientStatePersistence.lf
new file mode 100644
index 0000000000..bb5c0eff68
--- /dev/null
+++ b/test/C/src/federated/transient/TransientStatePersistence.lf
@@ -0,0 +1,194 @@
+/**
+ * This LF program showcases and tests the persistance of the internal state of a transient federate
+ * across executions. Using the hot swap mechanism, the transient federate `Middle` leaves and then
+ * joins. Whenever the state (of type `federate_state_t`) changes, it notifies `Persistence`.
+ * `Middle` notifies `Persistence` also when it joins. When `Middle` joins the second time or after,
+ * it receives the saved state and sets it. In this, the order of the reactions is important.
+ */
+target C {
+ timeout: 2900 ms
+}
+
+preamble {=
+ #include
+ #include
+ // The internal federate state to be persistent across executions
+ typedef struct federate_state_t {
+ char state_char;
+ int state_count;
+ } federate_state_t;
+=}
+
+/** Persistent federate that is responsible for lauching the transient federate */
+reactor TransientExec(launch_time: time = 0, fed_instance_name: char* = "instance") {
+ timer t(launch_time, 0)
+
+ reaction(t) {=
+ // Construct the command to launch the transient federate
+ char mid_launch_cmd[512];
+ sprintf(mid_launch_cmd,
+ "%s/bin/federate__%s -i %s",
+ LF_FED_PACKAGE_DIRECTORY,
+ self->fed_instance_name,
+ lf_get_federation_id()
+ );
+
+ lf_print("Launching federate %s at physical time " PRINTF_TIME ".",
+ self->fed_instance_name, lf_time_physical());
+
+ int status = system(mid_launch_cmd);
+
+ // Exit if error
+ if (status == 0) {
+ lf_print("Successfully launched federate__%s.", self->fed_instance_name);
+ } else {
+ lf_print_error_and_exit("Unable to launch federate__%s. Abort!", self->fed_instance_name);
+ }
+ =}
+}
+
+reactor Persistence {
+ state middle_state: federate_state_t = {'A', 0}
+ state middle_first_join: bool = true
+
+ input in_from_middle: federate_state_t
+ input in_middle_join: bool
+ output out_to_middle: federate_state_t
+
+ // Only send the previous state if it not the first time Middle joins
+ reaction(in_middle_join) -> out_to_middle {=
+ if (!self->middle_first_join) {
+ lf_set(out_to_middle, self->middle_state);
+ lf_print("Notifying Mid of the latest state: {%c,%d}", self->middle_state.state_char,
+ self->middle_state.state_count);
+ }
+ self->middle_first_join = false;
+ =}
+
+ reaction(in_from_middle) {=
+ self->middle_state.state_char = in_from_middle->value.state_char;
+ self->middle_state.state_count = in_from_middle->value.state_count;
+ lf_print("Latest received state: {%c,%d}", self->middle_state.state_char,
+ self->middle_state.state_count);
+ =}
+}
+
+/**
+ * Persistent federate, upstream of the transient. It reacts to its timer by sending increments to
+ * out output port.
+ */
+reactor Up(period: time = 500 ms) {
+ output out: int
+ timer t(0, period)
+ state count: int = 0
+
+ reaction(t) -> out {=
+ lf_set(out, self->count);
+ self->count++;
+ lf_print("Up timer sent %d", self->count);
+ =}
+}
+
+/**
+ * Transient federate that forwards whatever it receives from Up to down. It reacts twice to in
+ * input ports, then stops. It will execute twice during the lifetime of the federation. The second
+ * launch is done by TransientExec at logical time 1 s. Each time Middle joins, it notifies Down.
+ */
+reactor Middle {
+ input in: int
+ output out: int
+ output join: bool
+ state middle_state: federate_state_t = {'A', 0}
+
+ output out_to_persistence: federate_state_t // State Persistence
+ input in_from_persistence: federate_state_t
+
+ // Middle notifies its downstream that he joined
+ reaction(startup) -> join {=
+ lf_set(join, true);
+ =}
+
+ reaction(in_from_persistence) {=
+ self->middle_state = in_from_persistence->value;
+ lf_print("Received the latest state of: {%c,%d} at " PRINTF_TIME ".",
+ self->middle_state.state_char,
+ self->middle_state.state_count,
+ lf_time_logical_elapsed());
+ =}
+
+ // When an input is received, the internal state is updated, and then sent to
+ // Persistance.
+ reaction(in) -> out, out_to_persistence {=
+ self->middle_state.state_char++;
+ self->middle_state.state_count += 2;
+ lf_set(out, self->middle_state.state_count);
+ lf_set(out_to_persistence, self->middle_state);
+ lf_print("Mid state is: {count='%c', count=%d}",
+ self->middle_state.state_char,
+ self->middle_state.state_count);
+
+ if (self->middle_state.state_count == 4) {
+ lf_stop();
+ }
+ =}
+}
+
+/**
+ * Persistent federate, which is downstream of the transient. It has to keep reacting to its
+ * internal timer and also to inputs from the tansient, if any.
+ */
+reactor Down {
+ timer t(0, 500 ms)
+
+ input in: int
+ input join: bool
+
+ state count_timer: int = 0
+ state count_join: int = 0
+ state count_in_mid_reactions: int = 0
+
+ reaction(t) {=
+ self->count_timer++;
+ lf_print("Down timer count %d", self->count_timer);
+ =}
+
+ reaction(join) {=
+ self->count_join++;
+ lf_print("Down count join %d", self->count_join);
+ =}
+
+ reaction(in) {=
+ self->count_in_mid_reactions++;
+ lf_print("Down in %d", self->count_in_mid_reactions);
+ =}
+
+ reaction(shutdown) {=
+ if(self->count_join == 2 && self->count_in_mid_reactions < 4) {
+ lf_print_error_and_exit("Mid Joined twice, but the state did not persist \
+ across executions! state_count is %d, while is should be > then %d.",
+ self->count_in_mid_reactions,
+ 4);
+ }
+ =}
+}
+
+federated reactor {
+ // Persistent downstream and upstream federates of the transient
+ up = new Up()
+ down = new Down()
+ persistence = new Persistence()
+ // Persistent federate that is responsible for lauching the transient once, after 1s
+ midExec = new TransientExec(launch_time = 1 s, fed_instance_name="mid")
+
+ // Transient federate
+ @transient
+ mid = new Middle()
+
+ // Connections
+ up.out -> mid.in
+ mid.join -> down.join
+ mid.join -> persistence.in_middle_join
+ mid.out -> down.in
+ persistence.out_to_middle -> mid.in_from_persistence
+ mid.out_to_persistence -> persistence.in_from_middle
+}