diff --git a/core/src/main/java/org/lflang/AttributeUtils.java b/core/src/main/java/org/lflang/AttributeUtils.java index 79447cb440..37cbcecd38 100644 --- a/core/src/main/java/org/lflang/AttributeUtils.java +++ b/core/src/main/java/org/lflang/AttributeUtils.java @@ -226,6 +226,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. */ public static boolean isFederate(Reactor reactor) { return findAttributeByName(reactor, "_fed_config") != null; 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 edf5bc8d19..2381b8546e 100644 --- a/core/src/main/java/org/lflang/federated/extensions/CExtension.java +++ b/core/src/main/java/org/lflang/federated/extensions/CExtension.java @@ -721,6 +721,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 ad982431a2..aa92d055b3 100644 --- a/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java +++ b/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java @@ -36,17 +36,20 @@ public class CExtensionUtils { // Regular expression pattern for shared_ptr types. - static final Pattern sharedPointerVariable = - Pattern.compile("^(/\\*.*?\\*/)?std::shared_ptr<(?((/\\*.*?\\*/)?(\\S+))+)>$"); + static final Pattern sharedPointerVariable = Pattern + .compile("^(/\\*.*?\\*/)?std::shared_ptr<(?((/\\*.*?\\*/)?(\\S+))+)>$"); /** * Generate C code that initializes network actions. * - *

These network actions will be triggered by federate.c whenever a message is received from + *

+ * These network actions will be triggered by federate.c whenever a message is + * received from * the network. * * @param federate The federate. - * @param main The main reactor that contains the federate (used to lookup references). + * @param main The main reactor that contains the federate (used to lookup + * references). */ public static String initializeTriggersForNetworkActions( FederateInstance federate, ReactorInstance main) { @@ -92,8 +95,11 @@ public static String initializeTriggersForNetworkActions( /** * Generate C code that holds a sorted list of STAA structs by time. * - *

For decentralized execution, on every logical timestep, a thread will iterate through each - * staa struct, wait for the designated offset time, and set the associated port status to absent + *

+ * For decentralized execution, on every logical timestep, a thread will iterate + * through each + * staa struct, wait for the designated offset time, and set the associated port + * status to absent * if it isn't known. * * @param federate The federate. @@ -109,8 +115,7 @@ public static String stpStructs(FederateInstance federate) { // main reactor for each Action. for (int i = 0; i < federate.staaOffsets.size(); ++i) { // Find the corresponding ActionInstance. - List networkActions = - federate.staToNetworkActionMap.get(federate.staaOffsets.get(i)); + List networkActions = federate.staToNetworkActionMap.get(federate.staaOffsets.get(i)); code.pr("staa_lst[" + i + "] = (staa_t*) malloc(sizeof(staa_t));"); code.pr( @@ -143,7 +148,8 @@ public static String stpStructs(FederateInstance federate) { } /** - * Create a port status field variable for a network input port "input" in the self struct of a + * Create a port status field variable for a network input port "input" in the + * self struct of a * reactor. * * @param input The network input port @@ -151,7 +157,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 @@ -163,15 +170,23 @@ public static String createPortStatusFieldForInput(Input input) { } /** - * Given a connection 'delay' expression, return a string that represents the interval_t value of + * Given a connection 'delay' expression, return a string that represents the + * interval_t value of * the additional delay that needs to be applied to the outgoing message. * - *

The returned additional delay in absence of after on network connection (i.e., if delay is - * passed as a null) is NEVER. This has a special meaning in C library functions that send network - * messages that carry timestamps (@see lf_send_tagged_message and lf_send_port_absent_to_federate - * in lib/core/federate.c). In this case, the sender will send its current tag as the timestamp of - * the outgoing message without adding a microstep delay. If the user has assigned an after delay - * to the network connection (that can be zero) either as a time value (e.g., 200 msec) or as a + *

+ * The returned additional delay in absence of after on network connection + * (i.e., if delay is + * passed as a null) is NEVER. This has a special meaning in C library functions + * that send network + * messages that carry timestamps (@see lf_send_tagged_message and + * lf_send_port_absent_to_federate + * in lib/core/federate.c). In this case, the sender will send its current tag + * as the timestamp of + * the outgoing message without adding a microstep delay. If the user has + * assigned an after delay + * to the network connection (that can be zero) either as a time value (e.g., + * 200 msec) or as a * literal (e.g., a parameter), that delay in nsec will be returned. * * @param delay The delay associated with a connection. @@ -215,11 +230,9 @@ public static void handleCompileDefinitions( } private static void handleAdvanceMessageInterval(FederateInstance federate) { - var advanceMessageInterval = - federate.targetConfig.get(CoordinationOptionsProperty.INSTANCE).advanceMessageInterval; + var advanceMessageInterval = federate.targetConfig.get(CoordinationOptionsProperty.INSTANCE).advanceMessageInterval; if (advanceMessageInterval != null) { - federate - .targetConfig + federate.targetConfig .get(CompileDefinitionsProperty.INSTANCE) .put("ADVANCE_MESSAGE_INTERVAL", String.valueOf(advanceMessageInterval.toNanoSeconds())); } @@ -232,16 +245,20 @@ static boolean clockSyncIsOn(FederateInstance federate, RtiConfig rtiConfig) { } /** - * Initialize clock synchronization (if enabled) and its related options for a given federate. + * Initialize clock synchronization (if enabled) and its related options for a + * given federate. * - *

Clock synchronization can be enabled using the clock-sync target property. + *

+ * Clock synchronization can be enabled using the clock-sync target property. * * @see Documentation + * href= + * "https://github.com/icyphy/lingua-franca/wiki/Distributed-Execution#clock-synchronization">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() @@ -263,12 +280,15 @@ public static void initializeClockSynchronization( } /** - * Initialize clock synchronization (if enabled) and its related options for a given federate. + * Initialize clock synchronization (if enabled) and its related options for a + * given federate. * - *

Clock synchronization can be enabled using the clock-sync target property. + *

+ * Clock synchronization can be enabled using the clock-sync target property. * * @see Documentation + * href= + * "https://github.com/icyphy/lingua-franca/wiki/Distributed-Execution#clock-synchronization">Documentation */ public static void addClockSyncCompileDefinitions(FederateInstance federate) { @@ -299,10 +319,9 @@ public static void generateCMakeInclude( FederateInstance federate, FederationFileConfig fileConfig) throws IOException { Files.createDirectories(fileConfig.getSrcPath().resolve("include")); - Path cmakeIncludePath = - fileConfig - .getSrcPath() - .resolve("include" + File.separator + federate.name + "_extension.cmake"); + Path cmakeIncludePath = fileConfig + .getSrcPath() + .resolve("include" + File.separator + federate.name + "_extension.cmake"); CodeBuilder cmakeIncludeCode = new CodeBuilder(); @@ -314,6 +333,8 @@ public static void generateCMakeInclude( cmakeIncludeCode.pr( "add_compile_definitions(LF_SOURCE_GEN_DIRECTORY=\"" + fileConfig.getSrcGenPath() + "\")"); cmakeIncludeCode.pr("add_compile_definitions(LF_FILE_SEPARATOR=\"" + File.separator + "\")"); + cmakeIncludeCode.pr( + "add_compile_definitions(LF_FEDERATES_BIN_DIRECTORY=\"" + fileConfig.getFedBinPath() + "\")"); try (var srcWriter = Files.newBufferedWriter(cmakeIncludePath)) { srcWriter.write(cmakeIncludeCode.getCode()); } @@ -324,7 +345,8 @@ public static void generateCMakeInclude( } /** - * Generate code that sends the neighbor structure message to the RTI. See {@code + * Generate code that sends the neighbor structure message to the RTI. See + * {@code * MSG_TYPE_NEIGHBOR_STRUCTURE} in {@code federated/net_common.h}. * * @param federate The federate that is sending its neighbor structure @@ -397,14 +419,13 @@ public static String generateFederateNeighborStructure(FederateInstance federate // Use NEVER to encode no delay at all. code.pr("candidate_tmp = NEVER;"); } else { - var delayTime = - delay instanceof ParameterReference - // In that case use the default value. - ? CTypes.getInstance() - .getTargetTimeExpr( - ASTUtils.getDefaultAsTimeValue( - ((ParameterReference) delay).getParameter())) - : CTypes.getInstance().getTargetExpr(delay, InferredType.time()); + var delayTime = delay instanceof ParameterReference + // In that case use the default value. + ? CTypes.getInstance() + .getTargetTimeExpr( + ASTUtils.getDefaultAsTimeValue( + ((ParameterReference) delay).getParameter())) + : CTypes.getInstance().getTargetExpr(delay, InferredType.time()); code.pr( String.join( @@ -461,26 +482,27 @@ public static String surroundWithIfElseFederated(String insideIf, String insideE return surroundWithIfFederated(insideIf); } else { return """ - #ifdef FEDERATED - %s - #else - %s - #endif // FEDERATED - """ + #ifdef FEDERATED + %s + #else + %s + #endif // FEDERATED + """ .formatted(insideIf, insideElse); } } /** - * Surround {@code code} with blocks to ensure that code only executes if the program is + * Surround {@code code} with blocks to ensure that code only executes if the + * program is * federated. */ public static String surroundWithIfFederated(String code) { return """ - #ifdef FEDERATED - %s - #endif // FEDERATED - """ + #ifdef FEDERATED + %s + #endif // FEDERATED + """ .formatted(code); } @@ -489,39 +511,41 @@ public static String surroundWithIfElseFederatedCentralized(String insideIf, Str return surroundWithIfFederatedCentralized(insideIf); } else { return """ - #ifdef FEDERATED_CENTRALIZED - %s - #else - %s - #endif // FEDERATED_CENTRALIZED - """ + #ifdef FEDERATED_CENTRALIZED + %s + #else + %s + #endif // FEDERATED_CENTRALIZED + """ .formatted(insideIf, insideElse); } } /** - * Surround {@code code} with blocks to ensure that code only executes if the program is federated + * Surround {@code code} with blocks to ensure that code only executes if the + * program is federated * and has a centralized coordination. */ public static String surroundWithIfFederatedCentralized(String code) { return """ - #ifdef FEDERATED_CENTRALIZED - %s - #endif // FEDERATED_CENTRALIZED - """ + #ifdef FEDERATED_CENTRALIZED + %s + #endif // FEDERATED_CENTRALIZED + """ .formatted(code); } /** - * Surround {@code code} with blocks to ensure that code only executes if the program is federated + * Surround {@code code} with blocks to ensure that code only executes if the + * program is federated * and has a decentralized coordination. */ public static String surroundWithIfFederatedDecentralized(String code) { return """ - #ifdef FEDERATED_DECENTRALIZED - %s - #endif // FEDERATED_DECENTRALIZED - """ + #ifdef FEDERATED_DECENTRALIZED + %s + #endif // FEDERATED_DECENTRALIZED + """ .formatted(code); } @@ -542,7 +566,9 @@ public static String generateSerializationIncludes(FederateInstance federate) { return code.getCode(); } - /** Generate cmake-include code needed for enabled serializers of the federate. */ + /** + * Generate cmake-include code needed for enabled serializers of the federate. + */ public static String generateSerializationCMakeExtension(FederateInstance federate) { CodeBuilder code = new CodeBuilder(); for (SupportedSerializers serializer : federate.enabledSerializers) { 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 4ce02d4793..7d769a731e 100644 --- a/core/src/main/java/org/lflang/federated/generator/FederateInstance.java +++ b/core/src/main/java/org/lflang/federated/generator/FederateInstance.java @@ -36,6 +36,7 @@ import java.util.Set; 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; @@ -96,6 +97,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. @@ -157,6 +159,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 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 10816dc111..171222c966 100644 --- a/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java +++ b/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java @@ -323,9 +323,17 @@ private String getRtiCommand(List federates, boolean isRemote) 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++; + } + } commands.addAll( List.of( " -n " + federates.size() + " \\", + " -nt " + transientFederatesNumber + " \\", " -c " + targetConfig.getOrDefault(ClockSyncModeProperty.INSTANCE).toString() + " \\")); 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 12de4474dc..0883dab91b 100644 --- a/core/src/main/java/org/lflang/generator/c/CCompiler.java +++ b/core/src/main/java/org/lflang/generator/c/CCompiler.java @@ -49,7 +49,8 @@ import org.lflang.util.LFCommand; /** - * Responsible for creating and executing the necessary CMake command to compile code that is + * Responsible for creating and executing the necessary CMake command to compile + * code that is * generated by the CGenerator. This class uses CMake to compile. * * @author Soroush Bateni @@ -66,7 +67,8 @@ public class CCompiler { MessageReporter messageReporter; /** - * Indicate whether the compiler is in C++ mode. In C++ mode, the compiler produces .cpp files + * Indicate whether the compiler is in C++ mode. In C++ mode, the compiler + * produces .cpp files * instead of .c files and uses a C++ compiler to compiler the code. */ private final boolean cppMode; @@ -77,10 +79,11 @@ public class CCompiler { /** * Create an instance of CCompiler. * - * @param targetConfig The current target configuration. - * @param fileConfig The current file configuration. + * @param targetConfig The current target configuration. + * @param fileConfig The current file configuration. * @param messageReporter Used to report errors. - * @param cppMode Whether the generated code should be compiled as if it were C++. + * @param cppMode Whether the generated code should be compiled as if it + * were C++. */ public CCompiler( TargetConfig targetConfig, @@ -97,8 +100,9 @@ public CCompiler( /** * Run the C compiler by invoking cmake and make. * - * @param generator An instance of GeneratorBase, only used to report error line numbers in the - * Eclipse IDE. + * @param generator An instance of GeneratorBase, only used to report error line + * numbers in the + * Eclipse IDE. * @return true if compilation succeeds, false otherwise. */ public boolean runCCompiler(GeneratorBase generator, LFGeneratorContext context) @@ -109,9 +113,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); @@ -193,15 +197,15 @@ public boolean runCCompiler(GeneratorBase generator, LFGeneratorContext context) } /** - * Return a command to compile the specified C file using CMake. This produces a C-specific + * Return a command to compile the specified C file using CMake. This produces a + * C-specific * compile command. */ public LFCommand compileCmakeCommand() { // Set the build directory to be "build" Path buildPath = fileConfig.getSrcGenPath().resolve("build"); - LFCommand command = - commandFactory.createCommand("cmake", cmakeOptions(targetConfig, fileConfig), buildPath); + LFCommand command = commandFactory.createCommand("cmake", cmakeOptions(targetConfig, fileConfig), buildPath); if (command == null) { messageReporter .nowhere() @@ -220,6 +224,7 @@ private static List cmakeOptions(TargetConfig targetConfig, FileConfig f String quote = "\""; String srcPath = fileConfig.srcPath.toString(); String rootPath = fileConfig.srcPkgPath.toString(); + String binPath = fileConfig.binPath.toString(); String srcGenPath = fileConfig.getSrcGenPath().toString(); if (separator.equals("\\")) { // Windows requires escaping the backslashes. @@ -228,6 +233,7 @@ private static List cmakeOptions(TargetConfig targetConfig, FileConfig f srcPath = srcPath.replaceAll("\\\\", "\\\\\\\\"); rootPath = rootPath.replaceAll("\\\\", "\\\\\\\\"); srcGenPath = srcGenPath.replaceAll("\\\\", "\\\\\\\\"); + binPath = binPath.replaceAll("\\\\", "\\\\\\\\"); } arguments.addAll( List.of( @@ -240,13 +246,16 @@ 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 + // 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 arguments.add("-DLF_SOURCE_DIRECTORY='" + quote + srcPath + quote + "'"); arguments.add("-DLF_PACKAGE_DIRECTORY='" + quote + rootPath + quote + "'"); arguments.add("-DLF_SOURCE_GEN_DIRECTORY='" + quote + srcGenPath + quote + "'"); + } else { + arguments.add("-DLF_FEDERATES_BIN_DIRECTORY=\"" + maybeQuote + binPath + maybeQuote + "\""); } arguments.add(FileUtil.toUnixString(fileConfig.getSrcGenPath())); @@ -270,29 +279,31 @@ private String buildTypeToCmakeConfig(BuildType type) { } /** - * Return a command to build the specified C file using CMake. This produces a C-specific build + * Return a command to build the specified C file using CMake. This produces a + * C-specific build * command. * - *

Note: It appears that configuration and build cannot happen in one command. Therefore, this + *

+ * Note: It appears that configuration and build cannot happen in one command. + * Therefore, this * is separated into a compile command and a build command. */ public LFCommand buildCmakeCommand() { // Set the build directory to be "build" Path buildPath = fileConfig.getSrcGenPath().resolve("build"); String cores = String.valueOf(Runtime.getRuntime().availableProcessors()); - LFCommand command = - commandFactory.createCommand( - "cmake", - List.of( - "--build", - ".", - "--target", - "install", - "--parallel", - cores, - "--config", - buildTypeToCmakeConfig(targetConfig.get(BuildTypeProperty.INSTANCE))), - buildPath); + LFCommand command = commandFactory.createCommand( + "cmake", + List.of( + "--build", + ".", + "--target", + "install", + "--parallel", + cores, + "--config", + buildTypeToCmakeConfig(targetConfig.get(BuildTypeProperty.INSTANCE))), + buildPath); if (command == null) { messageReporter .nowhere() @@ -305,8 +316,10 @@ public LFCommand buildCmakeCommand() { } /** - * Return a flash/emulate command using west. If board is null (defaults to qemu_cortex_m3) or - * qemu_* Return a flash command which runs the target as an emulation If ordinary target, return + * Return a flash/emulate command using west. If board is null (defaults to + * qemu_cortex_m3) or + * qemu_* Return a flash command which runs the target as an emulation If + * ordinary target, return * {@code west flash} */ public LFCommand buildWestFlashCommand(PlatformOptions options) { @@ -328,18 +341,23 @@ public LFCommand buildWestFlashCommand(PlatformOptions options) { } /** - * Check if the output produced by CMake has any known and common errors. If a known error is + * Check if the output produced by CMake has any known and common errors. If a + * known error is * detected, a specialized, more informative message is shown. * - *

Errors currently detected: + *

+ * Errors currently detected: * *

* * @param CMakeOutput The captured output from CMake. - * @return true if the provided 'CMakeOutput' contains a known error. false otherwise. + * @return true if the provided 'CMakeOutput' contains a known error. false + * otherwise. */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean outputContainsKnownCMakeErrors(String CMakeOutput) { @@ -369,8 +387,10 @@ private boolean outputContainsKnownCMakeErrors(String CMakeOutput) { * Produces the filename including the target-specific extension * * @param fileName The base name of the file without any extensions - * @param cppMode Indicate whether the compiler is in C++ mode In C++ mode, the compiler produces - * .cpp files instead of .c files and uses a C++ compiler to compiler the code. + * @param cppMode Indicate whether the compiler is in C++ mode In C++ mode, the + * compiler produces + * .cpp files instead of .c files and uses a C++ compiler to + * compiler the code. */ static String getTargetFileName(String fileName, boolean cppMode, TargetConfig targetConfig) { return fileName + getFileExtension(cppMode, targetConfig); @@ -379,8 +399,9 @@ static String getTargetFileName(String fileName, boolean cppMode, TargetConfig t /** * Return the file extension of the output source files. * - * @param cppMode Whether we are building C code using a C++ compiler. - * @param targetConfig The target configuration that parameterizes the build process. + * @param cppMode Whether we are building C code using a C++ compiler. + * @param targetConfig The target configuration that parameterizes the build + * process. */ static String getFileExtension(boolean cppMode, TargetConfig targetConfig) { if (targetConfig.getOrDefault(PlatformProperty.INSTANCE).platform() == Platform.ARDUINO) { diff --git a/core/src/main/java/org/lflang/validation/AttributeSpec.java b/core/src/main/java/org/lflang/validation/AttributeSpec.java index a685e98ca7..cb13cca247 100644 --- a/core/src/main/java/org/lflang/validation/AttributeSpec.java +++ b/core/src/main/java/org/lflang/validation/AttributeSpec.java @@ -205,6 +205,8 @@ enum AttrParamType { new AttributeSpec(List.of(new AttrParamSpec(VALUE_ATTR, AttrParamType.STRING, 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 eb721cf032..10ad9c2c11 100644 --- a/core/src/main/java/org/lflang/validation/LFValidator.java +++ b/core/src/main/java/org/lflang/validation/LFValidator.java @@ -531,6 +531,19 @@ public void checkInstantiation(Instantiation instantiation) { error("Variable-width banks are not supported.", Literals.INSTANTIATION__WIDTH_SPEC); } } + + // If the Instantiation is annotated as '@transient', then: + // - The container has to be a federated reactor, + // - The coordination is centralized, + // - And the target is C. + if (AttributeUtils.isTransient(instantiation)) { + Reactor container = (Reactor) instantiation.eContainer(); + if (!container.isFederated()) { + error( + "Only federates can be transients: " + instantiation.getReactorClass().getName(), + Literals.INSTANTIATION__REACTOR_CLASS); + } + } } @Check(CheckType.FAST) diff --git a/core/src/main/resources/lib/c/reactor-c b/core/src/main/resources/lib/c/reactor-c index 793fc9a623..75db4d6e9d 160000 --- a/core/src/main/resources/lib/c/reactor-c +++ b/core/src/main/resources/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit 793fc9a62378624e39ec52499dbae174146fe1ba +Subproject commit 75db4d6e9d5cb7b31292239f20876fa933e0edf0 diff --git a/test/C/src/federated/transient/TransientDownstreamWithTimer.lf b/test/C/src/federated/transient/TransientDownstreamWithTimer.lf new file mode 100644 index 0000000000..a403057e48 --- /dev/null +++ b/test/C/src/federated/transient/TransientDownstreamWithTimer.lf @@ -0,0 +1,157 @@ +/** + * This LF program tests if a transient federate corretly leaves then joins the federation. It also + * tests if 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/federate__%s -i %s", + lf_get_federates_bin_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, 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(in) {= + self->count_in_mid_reactions++; + =} + + reaction(join) {= + self->count_join++; + =} + + 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..49960b05b4 --- /dev/null +++ b/test/C/src/federated/transient/TransientDownstreamWithTwoUpstream.lf @@ -0,0 +1,128 @@ +/** + * This LF program tests if a transient federate corretly leaves then joins the federation. It also + * tests if 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/federate__%s -i %s", + lf_get_federates_bin_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(in_mid) {= + self->count_in_mid_reactions++; + =} + + reaction(in_up) {= + self->count_in_up_reactions++; + =} + + reaction(join) {= + self->count_join++; + =} + + 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..a4c986d3d4 --- /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 if 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/federate__%s -i %s", + lf_get_federates_bin_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..71f3ed2b2d --- /dev/null +++ b/test/C/src/federated/transient/TransientStatePersistence.lf @@ -0,0 +1,188 @@ +/** + * 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 to save changes (of type `federate_state_t`), it notifies + * `Persistence`. `Middle` notifies `Persistence` also when it joins. When it 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/federate__%s -i %s", + lf_get_federates_bin_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); + } + 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; + =} +} + +/** + * 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; + =} + + // 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(in) {= + self->count_in_mid_reactions++; + lf_print("Down in %d", self->count_in_mid_reactions); + =} + + reaction(join) {= + self->count_join++; + lf_print("Down count join %d", self->count_join); + =} + + reaction(shutdown) in {= + if(self->count_join == 2 && in->value < 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.", + in->value, + 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 +}