diff --git a/.github/workflows/cpp-ros2-tests.yml b/.github/workflows/cpp-ros2-tests.yml deleted file mode 100644 index e405caf404..0000000000 --- a/.github/workflows/cpp-ros2-tests.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: ROS2 C++ tests - -on: - workflow_call: - inputs: - compiler-ref: - required: false - type: string - runtime-ref: - required: false - type: string - -jobs: - cpp-ros2-tests: - runs-on: ubuntu-24.04 - steps: - - name: Check out lingua-franca repository - uses: actions/checkout@v3 - with: - repository: lf-lang/lingua-franca - submodules: true - ref: ${{ inputs.compiler-ref }} - fetch-depth: 0 - - name: Prepare build environment - uses: ./.github/actions/prepare-build-env - - name: Check out specific ref of reactor-cpp - uses: actions/checkout@v3 - with: - repository: lf-lang/reactor-cpp - path: core/src/main/resources/lib/cpp/reactor-cpp - ref: ${{ inputs.runtime-ref }} - if: ${{ inputs.runtime-ref }} - - name: Setup ROS2 - uses: ./.github/actions/setup-ros2 - - name: Run C++ tests; - run: | - source /opt/ros/*/setup.bash - ./gradlew targetTest -Ptarget=CppRos2 - - name: Report to CodeCov - uses: ./.github/actions/report-code-coverage - with: - files: core/build/reports/jacoco/integrationTestCodeCoverageReport/integrationTestCodeCoverageReport.xml - if: ${{ github.repository == 'lf-lang/lingua-franca' }} diff --git a/.github/workflows/only-cpp.yml b/.github/workflows/only-cpp.yml index 46dc2f8bf6..7139f88ab4 100644 --- a/.github/workflows/only-cpp.yml +++ b/.github/workflows/only-cpp.yml @@ -24,7 +24,3 @@ jobs: uses: ./.github/workflows/cpp-tests.yml with: all-platforms: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} - - # Run the C++ integration tests on ROS2. - cpp-ros2-tests: - uses: ./.github/workflows/cpp-ros2-tests.yml diff --git a/.github/workflows/trace-plugin-tests.yml b/.github/workflows/trace-plugin-tests.yml new file mode 100644 index 0000000000..d05c0edb19 --- /dev/null +++ b/.github/workflows/trace-plugin-tests.yml @@ -0,0 +1,19 @@ +name: Trace plugin tests + +on: + push: + branches: + - master + pull_request: + types: [synchronize, opened, reopened, ready_for_review, converted_to_draft] + workflow_dispatch: + +concurrency: + group: trace-plugin-${{ github.ref }}-${{ github.run_id }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +jobs: + trace-plugin-tests: + uses: lf-lang/lf-trace-xronos/.github/workflows/ci.yml@main + with: + lf-ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/core/src/integrationTest/java/org/lflang/tests/runtime/CCliTest.java b/core/src/integrationTest/java/org/lflang/tests/runtime/CCliTest.java new file mode 100644 index 0000000000..b289efaea5 --- /dev/null +++ b/core/src/integrationTest/java/org/lflang/tests/runtime/CCliTest.java @@ -0,0 +1,137 @@ +package org.lflang.tests.runtime; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.lflang.target.Target; +import org.lflang.tests.LFTest; +import org.lflang.tests.LFTest.Result; +import org.lflang.tests.TestBase; +import org.lflang.tests.TestError; +import org.lflang.util.LFCommand; + +/** + * Integration tests for command-line parameter override functionality. + * + *

Verifies that main reactor parameters can be overridden via command-line arguments and that + * the overridden values propagate correctly to child reactors, including timers and deadlines. + * + *

Uses a ThreadLocal so that the CLI arguments set in each test method are visible to the + * runner instance created reflectively by {@link TestBase#runSingleTestAndPrintResults}, which + * executes on the same thread. This is safe for concurrent test execution. + * + * @author Edward A. Lee + */ +public class CCliTest extends TestBase { + + private static final ThreadLocal> cliArgs = ThreadLocal.withInitial(List::of); + + @AfterEach + void resetCliArgs() { + cliArgs.remove(); + } + + public CCliTest() { + super(Target.C); + } + + @Override + protected ProcessBuilder getExecCommand(LFTest test) throws TestError { + LFCommand command = test.getFileConfig().getCommand(); + if (command == null) { + throw new TestError("File: " + test.getFileConfig().getExecutable(), Result.NO_EXEC_FAIL); + } + List cmdList = new ArrayList<>(command.command()); + cmdList.addAll(cliArgs.get()); + return new ProcessBuilder(cmdList).directory(command.directory()); + } + + /** + * Test that --period, --expected, and --value override timer behavior and double parameter. + * With defaults (period=1s, expected=6, value=3.14159), overriding to period=500ms changes + * the firing count to 11, and overriding value=2.71828 changes the double sent on each output. + */ + @Test + public void testCommandLineParameterOverride() { + cliArgs.set(List.of("--period", "500", "msec", "--expected", "11", "--value", "2.71828")); + Path testFile = Path.of("test/C/src/CommandLineParam.lf").toAbsolutePath(); + LFTest test = new LFTest(testFile); + runSingleTestAndPrintResults(test, CCliTest.class, TestLevel.EXECUTION); + } + + /** + * Test that --min_delay and --min_spacing override action timing. With defaults (min_delay=1ns, + * min_spacing=10ns), overriding to min_delay=10us and min_spacing=100us changes when the action + * fires. The test verifies that elapsed times match the overridden values. + */ + @Test + public void testCommandLineActionOverride() { + cliArgs.set(List.of("--min_delay", "10", "us", "--min_spacing", "100", "us")); + Path testFile = Path.of("test/C/src/CommandLineAction.lf").toAbsolutePath(); + LFTest test = new LFTest(testFile); + runSingleTestAndPrintResults(test, CCliTest.class, TestLevel.EXECUTION); + } + + /** + * Test that --deadline_time and --execution_time override deadline behavior. With defaults + * (execution_time=100ms, deadline_time=50ms) a deadline violation occurs. The CLI override + * sets execution_time=10ms and deadline_time=200ms, so NO violation occurs, proving both + * overrides took effect. + */ + @Test + public void testCommandLineDeadlineOverride() { + cliArgs.set( + List.of( + "--execution_time", + "10", + "msec", + "--deadline_time", + "500", + "msec", + "--expect_violation", + "0")); + Path testFile = Path.of("test/C/src/CommandLineDeadline.lf").toAbsolutePath(); + LFTest test = new LFTest(testFile); + runSingleTestAndPrintResults(test, CCliTest.class, TestLevel.EXECUTION); + } + + /** + * Test that --value and --use_default override string and bool parameters. + * With default use_default=false and value="Hello, world!", overriding value to "Goodbye!" + * should cause the Print reactor to expect "Goodbye!". + * Running with --use_default true should ignore the value parameter and use the hardcoded default. + */ + @Test + public void testCommandLineStringBoolOverride() { + cliArgs.set(List.of("--value", "Goodbye!", "--use_default", "false")); + Path testFile = Path.of("test/C/src/CommandLineStringBool.lf").toAbsolutePath(); + LFTest test = new LFTest(testFile); + runSingleTestAndPrintResults(test, CCliTest.class, TestLevel.EXECUTION); + } + + /** + * Test that CLI overrides propagate through the launch script in federated execution. With + * defaults (execution_time=100ms, deadline_time=50ms) a deadline violation occurs. The CLI + * override sets execution_time=10ms and deadline_time=500ms with expect_violation=0, so + * NO violation occurs, proving the launch script forwards arguments to federates. + */ + @Test + public void testCommandLineDeadlineFederatedOverride() { + cliArgs.set( + List.of( + "--execution_time", + "10", + "msec", + "--deadline_time", + "500", + "msec", + "--expect_violation", + "0")); + Path testFile = + Path.of("test/C/src/federated/CommandLineDeadlineFederated.lf").toAbsolutePath(); + LFTest test = new LFTest(testFile); + runSingleTestAndPrintResults(test, CCliTest.class, TestLevel.EXECUTION); + } +} diff --git a/core/src/main/java/org/lflang/AttributeUtils.java b/core/src/main/java/org/lflang/AttributeUtils.java index 27ced35ae0..c107bdc5e0 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..b2081f5a3b 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,25 @@ 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 + ";"); + + // Emit initialization code for _fed.port_to_transient_feds_mapping: for each output port, + // record the IDs of transient downstream federates so the runtime can look them up. + int keyCount = 0; + for (String portName: federate.portNameTransientFedIdsMapping.keySet()) { + code.pr(String.format("_fed.port_to_transient_feds_mapping[%d].port_name = \"%s\" ;", keyCount, portName)); + int numTransients = federate.portNameTransientFedIdsMapping.get(portName).size(); + code.pr(String.format("_fed.port_to_transient_feds_mapping[%d].transient_fed_id = (uint16_t*)malloc(sizeof(uint16_t) * %d);", keyCount, numTransients)); + int idCount = 0; + for(int fedId: federate.portNameTransientFedIdsMapping.get(portName)) { + code.pr(String.format("_fed.port_to_transient_feds_mapping[%d].transient_fed_id[%d] = %d;", keyCount, idCount, fedId)); + idCount += 1; + } + code.pr(String.format("_fed.port_to_transient_feds_mapping[%d].num_of_transients = %d;", keyCount, idCount)); + keyCount += 1; + } + code.pr(String.format("_fed.port_map_size = %d;",keyCount)); // 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 7217d18a64..ebf20d5830 100644 --- a/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java +++ b/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java @@ -306,9 +306,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/FedGenerator.java b/core/src/main/java/org/lflang/federated/generator/FedGenerator.java index 314364f61b..b1b9c9046e 100644 --- a/core/src/main/java/org/lflang/federated/generator/FedGenerator.java +++ b/core/src/main/java/org/lflang/federated/generator/FedGenerator.java @@ -162,6 +162,9 @@ public boolean doGenerate(Resource resource, LFGeneratorContext context) throws instantiation.setWidthSpec(null); } + // Create a mapping of port names to the IDs of transient downstream federates connected to them. + createPortToDstTransientFederateMapping(); + // Find all the connections between federates. // For each connection between federates, replace it in the // AST with an action (which inherits the delay) and three reactions. @@ -230,6 +233,9 @@ public boolean doGenerate(Resource resource, LFGeneratorContext context) throws TLSGenerator.setupTLS(fileConfig, federates, messageReporter, context); } + // Generate the transient config file that lists all transient federates in this federation. + SSTGenerator.generateTransientFederateConfig(fileConfig, federates); + context.finish(Status.COMPILED, codeMapMap); return context.getErrorReporter().getErrorsOccurred(); } @@ -759,6 +765,24 @@ private List getFederateInstances( } return federateInstances; } + /** + * Populate each federate's {@code portNameTransientFedIdsMapping}: a map from output port name to + * the list of IDs of transient downstream federates connected to that port. This mapping is used + * during code generation to emit the per-federate transient-destination tables. + */ + private void createPortToDstTransientFederateMapping() { + for (var federates : federatesByInstantiation.values()) { + for (var federate : federates) { + for ( var connection: federate.connections) { + if (federate == connection.getSrcFederate() && connection.getDstFederate().isTransient) { + var portName = connection.getSourcePortInstance().getName(); + var dstFedId = connection.dstFederate.id; + federate.portNameTransientFedIdsMapping.computeIfAbsent(portName, k-> new ArrayList<>()).add(dstFedId); + } + } + } + } + } /** * Replace connections between federates in the AST with proxies that handle sending and receiving 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..7e5271ef0f 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. @@ -255,6 +266,9 @@ public Instantiation getInstantiation() { /** Keep a map of network actions to their associated instantiations */ public HashMap networkActionToInstantiation = new HashMap<>(); + /** Map from each output port name to the IDs of transient downstream federates connected to it. */ + public HashMap> portNameTransientFedIdsMapping = new HashMap<>(); + /** An message reporter */ private final MessageReporter messageReporter; diff --git a/core/src/main/java/org/lflang/federated/generator/FederationFileConfig.java b/core/src/main/java/org/lflang/federated/generator/FederationFileConfig.java index 16ffa00d0d..0a2eab1b68 100644 --- a/core/src/main/java/org/lflang/federated/generator/FederationFileConfig.java +++ b/core/src/main/java/org/lflang/federated/generator/FederationFileConfig.java @@ -11,6 +11,7 @@ import org.lflang.target.property.CmakeInitIncludeProperty; import org.lflang.target.property.FilesProperty; import org.lflang.target.property.ProtobufsProperty; +import org.lflang.target.property.TracePluginProperty; import org.lflang.util.FileUtil; /** @@ -124,6 +125,29 @@ public void relativizePaths(FederateTargetConfig targetConfig) { p.override(targetConfig, relativizePathList(targetConfig.get(p))); } }); + + // Handle TracePluginProperty separately since it uses TracePluginSpec, not List. + if (targetConfig.isSet(TracePluginProperty.INSTANCE)) { + var spec = targetConfig.get(TracePluginProperty.INSTANCE); + if (spec != null && spec.paths != null && !spec.paths.isBlank()) { + spec.paths = relativizeSemicolonSeparatedPaths(spec.paths); + TracePluginProperty.INSTANCE.override(targetConfig, spec); + } + } + } + + /** + * Relativize each segment of a semicolon-separated path list (CMake list syntax). + * + * @param cmakePathList Semicolon-separated paths to relativize. + */ + private String relativizeSemicolonSeparatedPaths(String cmakePathList) { + String[] segments = cmakePathList.split(";"); + List result = new ArrayList<>(); + for (String segment : segments) { + result.add(relativizePath(Paths.get(segment.trim()))); + } + return String.join(";", result); } /** diff --git a/core/src/main/java/org/lflang/federated/generator/SSTGenerator.java b/core/src/main/java/org/lflang/federated/generator/SSTGenerator.java index 385d4e754a..a17a87194e 100644 --- a/core/src/main/java/org/lflang/federated/generator/SSTGenerator.java +++ b/core/src/main/java/org/lflang/federated/generator/SSTGenerator.java @@ -216,6 +216,72 @@ public static Path getSSTConfig(FederationFileConfig fileConfig, String name) { return fileConfig.getSSTConfigPath().resolve(name + ".config"); } + /** + * Generate a {@code transient_federates.config} file in the RTI's src-gen directory. The file + * lists every transient federate in the federation so the RTI can launch them on demand. Each + * entry records the federate's ID, name, IP address, SSH user, binary launch path, and SST + * config path. + */ + public static void generateTransientFederateConfig( + FederationFileConfig fileConfig, List federates) { + + StringBuilder transientConfig = new StringBuilder(); + for (FederateInstance federate : federates) { + if (federate.isTransient) { + int id = federate.id; + String fedName = federate.name; + String fedIp = federate.host; + String fedUser = federate.user; + String launchFilePath; + String sstConfigPath; + if (fedIp == null || fedIp.equals("localhost")) { + fedIp = "localhost"; + launchFilePath = fileConfig.getFedBinPath().resolve(fedName).toString(); + sstConfigPath = fileConfig.getSSTConfigPath().resolve(fedName + ".config").toString(); + } else { + launchFilePath = getRemoteBasePath(fileConfig, "") + "/bin/" + fedName; + sstConfigPath = getSSTRemoteBasePath(fileConfig, fedName) + fedName + ".config"; + } + + transientConfig + .append("federateId=") + .append(id) + .append("\n") + .append("federateName=") + .append(fedName) + .append("\n") + .append("federateUser=") + .append(fedUser) + .append("\n") + .append("federateIp=") + .append(fedIp) + .append("\n") + .append("federateLaunchPath=") + .append(launchFilePath) + .append("\n") + .append("federateSSTPath=") + .append(sstConfigPath) + .append("\n"); + } + } + + // No need to create the file if no transient federates are present + if (transientConfig.length() <= 0) { + return; + } + + // Write the transient federates config to the RTI's src-gen directory. + try { + Path newFilePath; + newFilePath = fileConfig.getRtiSrcGenPath().resolve("transient_federates.config"); + BufferedWriter writer = new BufferedWriter(new FileWriter(newFilePath.toFile())); + writer.write(transientConfig.toString()); + writer.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private static void generateSSTConfig(FederationFileConfig fileConfig, String name, String authAndRtiIP) { // Values to fill in String entityName = "net1." + name; 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 678533fa11..3c6615bb91 100644 --- a/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java +++ b/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java @@ -290,6 +290,27 @@ private String getSetupCode() { "set -m", "shopt -s huponexit", "", + "# Parse launcher arguments.", + "LOG_TO_FILE=false", + "REMAINING_ARGS=()", + "for arg in \"$@\"; do", + " if [ \"$arg\" = \"--help\" ] || [ \"$arg\" = \"-h\" ]; then", + " echo \"Usage: $0 [-l] [-h|--help] [FEDERATE_ARGS...]\"", + " echo \"\"", + " echo \"Launcher options:\"", + " echo \" -l Log federate output to files instead of stdout\"", + " echo \" -h, --help Show this help message\"", + " echo \"\"", + " echo \"All other arguments are forwarded to each federate.\"", + " echo \"For available federate parameters, run a federate binary with --help.\"", + " exit 0", + " elif [ \"$arg\" = \"-l\" ]; then", + " LOG_TO_FILE=true", + " else", + " REMAINING_ARGS+=(\"$arg\")", + " fi", + "done", + "", "# Set a trap to kill all background jobs on error or control-C", "# Use two distinct traps so we can see which signal causes this.", "cleanup() {", @@ -355,6 +376,13 @@ private String getDistHeader() { private String getRtiCommand( String rtiBinPath, List federates, boolean isRemote) { List commands = new ArrayList<>(); + // Identify the number of transient federates. + int transientFederatesNumber = 0; + for (FederateInstance federate : federates) { + if (federate.isTransient) { + transientFederatesNumber++; + } + } if (isRemote) { commands.add(rtiBinPath + " -i '${FEDERATION_ID}' \\"); } else { @@ -366,6 +394,7 @@ private String getRtiCommand( if (targetConfig.getOrDefault(TracingProperty.INSTANCE).isEnabled()) { commands.add(" -t \\"); } + if (targetConfig.get(CommunicationModeProperty.INSTANCE) == CommunicationMode.SST) { String sstConfigPath; if (isRemote) { @@ -389,12 +418,29 @@ private String getRtiCommand( commands.add(" -tls " + certPath + " " + keyPath + " \\"); } + + // Adds the -tf argument to the RTI command followed by the transient_federates.config file that lists + // all the transient federates in the federation + if (transientFederatesNumber > 0) { + String transientConfigPath; + if (isRemote) { + transientConfigPath = + SSTGenerator.getRemoteBasePath(fileConfig, "RTI") + "/transient_federates.config"; + } else { + transientConfigPath = + fileConfig.getRtiSrcGenPath().resolve("transient_federates.config").toString(); + } + + commands.add(" -tf " + transientConfigPath + " \\"); + } + if (!targetConfig.getOrDefault(DNETProperty.INSTANCE)) { commands.add(" -d \\"); } commands.addAll( List.of( " -n " + federates.size() + " \\", + " -nt " + transientFederatesNumber + " \\", " -c " + targetConfig.getOrDefault(ClockSyncModeProperty.INSTANCE).toString() + " \\")); @@ -425,7 +471,7 @@ private String getLaunchCode(String rtiLaunchCode) { "# The RTI will be brought back to foreground", "# to be responsive to user inputs after all federates", "# are launched.", - "if [ \"$1\" = \"-l\" ]; then", + "if [ \"$LOG_TO_FILE\" = true ]; then", launchCodeWithLogging, "else", launchCodeWithoutLogging, @@ -681,10 +727,10 @@ private String getFedLocalLaunchCode( return String.join( "\n", "echo \"#### Launching the federate " + federate.name + ".\"", - "if [ \"$1\" = \"-l\" ]; then", - " " + executeCommand + " >& " + federate.name + ".log &", + "if [ \"$LOG_TO_FILE\" = true ]; then", + " " + executeCommand + " \"${REMAINING_ARGS[@]}\" >& " + federate.name + ".log &", "else", - " " + executeCommand + " &", + " " + executeCommand + " \"${REMAINING_ARGS[@]}\" &", "fi", "pids[" + federateIndex + "]=$!"); } diff --git a/core/src/main/java/org/lflang/generator/GeneratorUtils.java b/core/src/main/java/org/lflang/generator/GeneratorUtils.java index 6f016928ae..9748913a9f 100644 --- a/core/src/main/java/org/lflang/generator/GeneratorUtils.java +++ b/core/src/main/java/org/lflang/generator/GeneratorUtils.java @@ -1,5 +1,6 @@ package org.lflang.generator; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import org.eclipse.core.resources.IResource; @@ -87,25 +88,34 @@ public static Iterable findAll(Resource resource, Class nodeType) { * @return the resources that provide the given reactors. */ public static Set getResources(Iterable reactors) { - Set visited = new LinkedHashSet<>(); + Set resources = new LinkedHashSet<>(); + Set visitedReactors = new HashSet<>(); for (Reactor r : reactors) { - if (!visited.contains(r.eResource())) { - addInheritedResources(r, visited); - } + addInheritedResources(r, resources, visitedReactors); } - return visited; + return resources; } /** Collect all resources associated with reactor through class inheritance. */ - private static void addInheritedResources(Reactor reactor, Set resources) { + private static void addInheritedResources( + Reactor reactor, Set resources, Set visitedReactors) { + // Skip if we've already processed this reactor + if (reactor == null || visitedReactors.contains(reactor)) { + return; + } + visitedReactors.add(reactor); resources.add(reactor.eResource()); + + // Process superclasses - each reactor may have different superclasses even if in the same file for (var s : reactor.getSuperClasses()) { - if (!resources.contains(s)) { - if (s instanceof ImportedReactor i) { - addInheritedResources(i.getReactorClass(), resources); - } else if (s instanceof Reactor r) { - addInheritedResources(r, resources); - } + Reactor superReactor = null; + if (s instanceof ImportedReactor i) { + superReactor = i.getReactorClass(); + } else if (s instanceof Reactor r) { + superReactor = r; + } + if (superReactor != null) { + addInheritedResources(superReactor, resources, visitedReactors); } } } diff --git a/core/src/main/java/org/lflang/generator/c/CActionGenerator.java b/core/src/main/java/org/lflang/generator/c/CActionGenerator.java index 5865260775..fe5b2b3437 100644 --- a/core/src/main/java/org/lflang/generator/c/CActionGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CActionGenerator.java @@ -4,11 +4,14 @@ import java.util.ArrayList; import java.util.List; +import org.lflang.TimeValue; import org.lflang.ast.ASTUtils; import org.lflang.generator.ActionInstance; import org.lflang.generator.CodeBuilder; import org.lflang.generator.ReactorInstance; import org.lflang.lf.Action; +import org.lflang.lf.Expression; +import org.lflang.lf.ParameterReference; import org.lflang.target.Target; /** @@ -30,26 +33,25 @@ public class CActionGenerator { * and period fields. * * @param instance The reactor. + * @param useParamRefs If true, use self-struct parameter references for min_delay/min_spacing + * when the AST expression is a ParameterReference, so runtime CLI overrides take effect. */ - public static String generateInitializers(ReactorInstance instance) { + public static String generateInitializers(ReactorInstance instance, boolean useParamRefs) { List code = new ArrayList<>(); for (ActionInstance action : instance.actions) { if (!action.isShutdown()) { var triggerStructName = CUtil.reactorRef(action.getParent()) + "->_lf__" + action.getName(); - var minDelay = action.getMinDelay(); - var minSpacing = action.getMinSpacing(); - var offsetInitializer = - triggerStructName - + ".offset = " - + CTypes.getInstance().getTargetTimeExpr(minDelay) - + ";"; - var periodInitializer = - triggerStructName - + ".period = " - + (minSpacing != null - ? CTypes.getInstance().getTargetTimeExpr(minSpacing) - : CGenerator.UNDEFINED_MIN_SPACING) - + ";"; + var selfRef = CUtil.reactorRef(action.getParent()); + var def = action.getDefinition(); + var offsetExpr = + getActionTimeExpr(def.getMinDelay(), selfRef, action.getMinDelay(), useParamRefs); + var spacingExpr = + action.getMinSpacing() != null + ? getActionTimeExpr( + def.getMinSpacing(), selfRef, action.getMinSpacing(), useParamRefs) + : CGenerator.UNDEFINED_MIN_SPACING; + var offsetInitializer = triggerStructName + ".offset = " + offsetExpr + ";"; + var periodInitializer = triggerStructName + ".period = " + spacingExpr + ";"; var lastTimeInitializer = triggerStructName + ".last_tag = NEVER_TAG;"; code.addAll( List.of( @@ -76,6 +78,19 @@ public static String generateInitializers(ReactorInstance instance) { return String.join("\n", code); } + /** + * Return a C expression for an action's min_delay or min_spacing. If {@code useParamRefs} is true + * and the AST expression is a {@link ParameterReference}, return a self-struct field reference so + * that CLI overrides propagate to the action timing. + */ + private static String getActionTimeExpr( + Expression astExpr, String selfRef, TimeValue resolved, boolean useParamRefs) { + if (useParamRefs && astExpr instanceof ParameterReference paramRef) { + return selfRef + "->" + paramRef.getParameter().getName(); + } + return CTypes.getInstance().getTargetTimeExpr(resolved); + } + /** * Create a template token initialized to the payload size. This token is marked to not be freed * so that the trigger_t struct always has a template token. At the start of each time step, we diff --git a/core/src/main/java/org/lflang/generator/c/CCmakeGenerator.java b/core/src/main/java/org/lflang/generator/c/CCmakeGenerator.java index 9f64b18d75..d812abc4fe 100644 --- a/core/src/main/java/org/lflang/generator/c/CCmakeGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CCmakeGenerator.java @@ -308,10 +308,17 @@ CodeBuilder generateCMakeCode( cMakeCode.pr("set(" + key + " " + v + " CACHE STRING \"\")\n"); }); // Add trace-plugin data - var tracePlugin = targetConfig.getOrDefault(TracePluginProperty.INSTANCE); - System.out.println(tracePlugin); - if (tracePlugin != null) { - cMakeCode.pr("set(LF_TRACE_PLUGIN " + tracePlugin + " CACHE STRING \"\")\n"); + if (targetConfig.isSet(TracePluginProperty.INSTANCE)) { + var tracePlugin = targetConfig.get(TracePluginProperty.INSTANCE); + if (tracePlugin != null) { + cMakeCode.pr("set(LF_TRACE_PLUGIN " + tracePlugin.pkg + " CACHE STRING \"\")\n"); + cMakeCode.pr( + "set(LF_TRACE_PLUGIN_LIBRARY " + tracePlugin.library + " CACHE STRING \"\")\n"); + if (tracePlugin.paths != null && !tracePlugin.paths.isBlank()) { + var absPaths = absolutizeCmakePathList(tracePlugin.paths); + cMakeCode.pr("set(LF_TRACE_PLUGIN_PATHS \"" + absPaths + "\" CACHE STRING \"\")\n"); + } + } } // Setup main target for different platforms @@ -529,6 +536,40 @@ CodeBuilder generateCMakeCode( return cMakeCode; } + /** + * Convert a CMake list of paths (semicolon-separated) into an absolute-path list. + * + *

Relative paths are resolved against the directory of the top-level LF file. + */ + private String absolutizeCmakePathList(String cmakePathList) { + var parts = cmakePathList.split(";"); + var out = new ArrayList(parts.length); + + for (var part : parts) { + var p = part.trim(); + if (p.isEmpty()) continue; + + // Leave CMake-style variables and "~" untouched. + if (p.contains("$") || p.startsWith("~")) { + out.add(p); + continue; + } + + try { + var path = Paths.get(p); + if (!path.isAbsolute()) { + path = fileConfig.srcPath.resolve(path).normalize().toAbsolutePath(); + } + out.add(FileUtil.toUnixString(path)); + } catch (Exception e) { + // If it's not a valid OS path, don't rewrite it. + out.add(p); + } + } + + return String.join(";", out); + } + /** Provide a strategy for configuring the main target of the CMake build. */ public interface SetUpMainTarget { // Implementation note: This indirection is necessary because the Python 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 7620ede66f..225990a2bd 100644 --- a/core/src/main/java/org/lflang/generator/c/CCompiler.java +++ b/core/src/main/java/org/lflang/generator/c/CCompiler.java @@ -14,6 +14,7 @@ import org.lflang.generator.LFGeneratorContext; import org.lflang.target.TargetConfig; import org.lflang.target.property.BuildTypeProperty; +import org.lflang.target.property.CmakeArgsProperty; import org.lflang.target.property.CompilerProperty; import org.lflang.target.property.PlatformProperty; import org.lflang.target.property.PlatformProperty.Option; @@ -86,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); @@ -217,14 +218,22 @@ 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). + targetConfig + .getOrDefault(CmakeArgsProperty.INSTANCE) + .forEach((key, value) -> arguments.add("-D" + key + "=" + (value == null ? "" : value))); + arguments.add(FileUtil.toUnixString(fileConfig.getSrcGenPath())); if (GeneratorUtils.isHostWindows()) { diff --git a/core/src/main/java/org/lflang/generator/c/CGenerator.java b/core/src/main/java/org/lflang/generator/c/CGenerator.java index 4ff4105999..762d9c376d 100644 --- a/core/src/main/java/org/lflang/generator/c/CGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CGenerator.java @@ -54,6 +54,8 @@ import org.lflang.lf.Instantiation; import org.lflang.lf.Mode; import org.lflang.lf.Model; +import org.lflang.lf.Parameter; +import org.lflang.lf.ParameterReference; import org.lflang.lf.Port; import org.lflang.lf.Preamble; import org.lflang.lf.Reaction; @@ -296,6 +298,9 @@ public class CGenerator extends GeneratorBase { /** A code-generator for enclave-specific code, */ private CEnclaveGenerator enclaveGenerator; + /** Main reactor parameters that can be overridden from the command line. */ + private List cliParameters = new ArrayList<>(); + /** The enclave AST transformation is stored here and later passed to the enclave-generator. */ private final CEnclavedReactorTransformation enclaveAST; @@ -563,7 +568,11 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { private void generateCodeFor(String lfModuleName) throws IOException { code.pr(generateDirectives()); - code.pr(new CMainFunctionGenerator(targetConfig).generateCode()); + Reactor mainReactorClass = + mainDef != null ? ASTUtils.toDefinition(mainDef.getReactorClass()) : null; + var mainFunctionGenerator = new CMainFunctionGenerator(targetConfig, mainReactorClass); + code.pr(mainFunctionGenerator.generateCode()); + this.cliParameters = mainFunctionGenerator.getCliParameters(); // Generate code for each reactor. generateReactorDefinitions(); copyUserFiles(targetConfig, fileConfig); @@ -1650,12 +1659,22 @@ private void generateTimerInitializations(ReactorInstance instance) { for (TimerInstance timer : instance.timers) { if (!timer.isStartup()) { initializeTriggerObjects.pr( - CTimerGenerator.generateInitializer(timer, instance.containingEnclave)); + CTimerGenerator.generateInitializer( + timer, instance.containingEnclave, supportsNativeParameterReferences())); instance.containingEnclave.numTimerTriggers += timer.getParent().getTotalWidth(); } } } + /** + * Return true if the generated C self struct stores parameters as native C types so that + * timer/deadline init code can reference them directly. The Python target stores parameters + * as PyObject* and must override this to return false. + */ + protected boolean supportsNativeParameterReferences() { + return true; + } + /** * Process a given .proto file. * @@ -1807,7 +1826,8 @@ public void generateReactorInstance(ReactorInstance instance) { * @param instance The reactor. */ private void generateActionInitializations(ReactorInstance instance) { - initializeTriggerObjects.pr(CActionGenerator.generateInitializers(instance)); + initializeTriggerObjects.pr( + CActionGenerator.generateInitializers(instance, supportsNativeParameterReferences())); } /** @@ -1825,7 +1845,7 @@ private void generateInitializeActionToken(ReactorInstance reactor) { var payloadSize = "0"; if (!type.isUndefined()) { var typeStr = types.getTargetType(type); - if (CUtil.isTokenType(type)) { + if (CUtil.isTokenType(type) || CUtil.isFixedSizeArrayType(type)) { typeStr = CUtil.rootType(typeStr); } if (typeStr != null && !typeStr.equals("") && !typeStr.equals("void")) { @@ -1885,9 +1905,22 @@ private void generateSetDeadline(ReactorInstance instance) { for (ReactionInstance reaction : instance.reactions) { var selfRef = CUtil.reactorRef(reaction.getParent()) + "->_lf__reaction_" + reaction.index; if (reaction.declaredDeadline != null) { - var deadline = reaction.declaredDeadline.maxDelay; - initializeTriggerObjects.pr( - selfRef + ".deadline = " + types.getTargetTimeExpr(deadline) + ";"); + var delayExpr = reaction.getDefinition().getDeadline().getDelay(); + if (supportsNativeParameterReferences() + && delayExpr instanceof ParameterReference paramRef) { + var reactorRef = CUtil.reactorRef(reaction.getParent()); + initializeTriggerObjects.pr( + selfRef + + ".deadline = " + + reactorRef + + "->" + + paramRef.getParameter().getName() + + ";"); + } else { + var deadline = reaction.declaredDeadline.maxDelay; + initializeTriggerObjects.pr( + selfRef + ".deadline = " + types.getTargetTimeExpr(deadline) + ";"); + } } else { // No deadline. initializeTriggerObjects.pr(selfRef + ".deadline = NEVER;"); } @@ -1968,6 +2001,22 @@ protected void generateParameterInitialization(ReactorInstance instance) { selfRef + "->" + parameter.getName() + " = " + initializer + ";"); } } + // For the main reactor, override parameters with command-line values if given. + if (instance.isMainOrFederated() && !cliParameters.isEmpty()) { + for (Parameter param : cliParameters) { + var name = param.getName(); + var baseType = param.getType() != null ? ASTUtils.baseType(param.getType()) : ""; + initializeTriggerObjects.pr("if (_lf_cli_" + name + "_given) {"); + if ("string".equals(baseType)) { + // CLI variable is const char*; self struct field is char* (typedef string). + initializeTriggerObjects.pr( + " " + selfRef + "->" + name + " = (char*)_lf_cli_" + name + ";"); + } else { + initializeTriggerObjects.pr(" " + selfRef + "->" + name + " = _lf_cli_" + name + ";"); + } + initializeTriggerObjects.pr("}"); + } + } } /** @@ -2118,67 +2167,77 @@ protected String generateTopLevelPreambles(Reactor reactor) { } private String generateTopLevelPreambles(Reactor reactor, Set visited) { - if (visited.contains(reactor.eContainer())) { - // If we have already visited the container of this reactor, then we do not need to - // generate the preamble again. - return ""; - } - CodeBuilder builder = new CodeBuilder(); // First, generate the preambles for the base classes of the specified reactor. - var superClasses = reactor.getSuperClasses(); - if (superClasses != null) { - for (var superClass : superClasses) { - builder.pr(generateTopLevelPreambles(toDefinition(superClass), visited)); + // Use ASTUtils.superClasses to get the full resolved superclass chain, which handles + // imported reactors correctly. This returns classes in deepest-first order. + var allSuperClasses = ASTUtils.superClasses(reactor); + if (allSuperClasses != null) { + for (var superClass : allSuperClasses) { + generatePreambleForFile(superClass.eContainer(), visited, builder); } } - // These could have been in the same file, in which case we avoid generating again. - if (!visited.contains(reactor.eContainer())) { - visited.add(reactor.eContainer()); - // Generate the preambles for the specified reactor. - // We need to guard it with a #ifndef. - // The eContainer() of the reactor is the file in which it is defined, which - // is where top-level preambles reside. Hence, guard the preamble with an - // identifier unique to the file. - var preambles = ((Model) reactor.eContainer()).getPreambles(); - var hasPreamble = - !preambles.isEmpty() || targetConfig.get(ProtobufsProperty.INSTANCE).size() > 0; - if (hasPreamble) { - var guard = "TOP_LEVEL_PREAMBLE_" + reactor.eContainer().hashCode() + "_H"; - builder.pr("#ifndef " + guard); - builder.pr("#define " + guard); - - for (var preamble : preambles) { - var code = preamble.getCode(); - if (code != null) { - var text = toText(code); - builder.pr(text); - } - } - // Also generate the preambles for all the .proto files that are used. - for (String file : targetConfig.get(ProtobufsProperty.INSTANCE)) { - var dotIndex = file.lastIndexOf("."); - var rootFilename = file; - if (dotIndex > 0) { - rootFilename = file.substring(0, dotIndex); - } - code.pr("#include " + addDoubleQuotes(rootFilename + ".pb-c.h")); - builder.pr("#include " + addDoubleQuotes(rootFilename + ".pb-c.h")); - } + // Generate preamble for the current reactor's file (may have been visited via superclasses). + generatePreambleForFile(reactor.eContainer(), visited, builder); - builder.pr("#endif // " + guard); - } - } // Finally, generate the preambles for all the reactors that are instantiated. + // This recursively collects preambles from instantiated reactors and their superclasses. for (var instantiation : ASTUtils.allInstantiations(reactor)) { var instantiated = toDefinition(instantiation.getReactorClass()); - builder.pr(generateTopLevelPreambles(toDefinition(instantiated), visited)); + if (instantiated != null) { + builder.pr(generateTopLevelPreambles(instantiated, visited)); + } } return builder.toString(); } + /** + * Generate guarded preamble code for the given file (Model container) if not already visited. + * + * @param fileContainer The eContainer of a reactor (the Model/file containing it). + * @param visited Set of already-visited containers to avoid duplicates. + * @param builder The CodeBuilder to append the preamble to. + */ + private void generatePreambleForFile( + EObject fileContainer, Set visited, CodeBuilder builder) { + if (visited.contains(fileContainer)) { + return; + } + visited.add(fileContainer); + + var preambles = ((Model) fileContainer).getPreambles(); + var hasPreamble = + !preambles.isEmpty() || targetConfig.get(ProtobufsProperty.INSTANCE).size() > 0; + if (!hasPreamble) { + return; + } + + var guard = "TOP_LEVEL_PREAMBLE_" + fileContainer.hashCode() + "_H"; + builder.pr("#ifndef " + guard); + builder.pr("#define " + guard); + + for (var preamble : preambles) { + var code = preamble.getCode(); + if (code != null) { + builder.pr(toText(code)); + } + } + + // Also generate includes for all the .proto files that are used. + for (String file : targetConfig.get(ProtobufsProperty.INSTANCE)) { + var dotIndex = file.lastIndexOf("."); + var rootFilename = file; + if (dotIndex > 0) { + rootFilename = file.substring(0, dotIndex); + } + builder.pr("#include " + addDoubleQuotes(rootFilename + ".pb-c.h")); + } + + builder.pr("#endif // " + guard); + } + protected boolean targetLanguageIsCpp() { return cppMode; } diff --git a/core/src/main/java/org/lflang/generator/c/CMainFunctionGenerator.java b/core/src/main/java/org/lflang/generator/c/CMainFunctionGenerator.java index 1aa61d1b9b..771e94cb65 100644 --- a/core/src/main/java/org/lflang/generator/c/CMainFunctionGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CMainFunctionGenerator.java @@ -1,8 +1,23 @@ package org.lflang.generator.c; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import org.lflang.TimeValue; +import org.lflang.ast.ASTUtils; import org.lflang.generator.CodeBuilder; +import org.lflang.lf.Assignment; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Literal; +import org.lflang.lf.Parameter; +import org.lflang.lf.ParameterReference; +import org.lflang.lf.Port; +import org.lflang.lf.Reactor; +import org.lflang.lf.WidthSpec; +import org.lflang.lf.WidthTerm; import org.lflang.target.TargetConfig; import org.lflang.target.property.FastProperty; import org.lflang.target.property.KeepaliveProperty; @@ -19,13 +34,27 @@ public class CMainFunctionGenerator { private TargetConfig targetConfig; + /** The main reactor definition, or null if there is no main reactor. */ + private Reactor mainReactor; + /** The command to run the generated code if specified in the target directive. */ private List runCommand; - public CMainFunctionGenerator(TargetConfig targetConfig) { + /** Parameters of the main reactor that can be overridden from the command line. */ + private List cliParams; + + /** Names of top-level parameters that are used for multiport or bank widths. */ + private Set widthParams; + + public CMainFunctionGenerator(TargetConfig targetConfig, Reactor mainReactor) { this.targetConfig = targetConfig; + this.mainReactor = mainReactor; runCommand = new ArrayList<>(); + cliParams = new ArrayList<>(); + widthParams = new HashSet<>(); parseTargetParameters(); + collectCliParameters(); + collectWidthParameters(); } /** @@ -36,6 +65,7 @@ public CMainFunctionGenerator(TargetConfig targetConfig) { */ public String generateCode() { CodeBuilder code = new CodeBuilder(); + code.pr(generateCliGlobals()); code.pr(generateMainFunction()); code.pr(generateSetDefaultCliOption()); return code.toString(); @@ -86,15 +116,174 @@ private String generateMainFunction() { return String.join("\n", "int main(void) {", " return lf_reactor_c_main(0, NULL);", "}"); } default -> { - return String.join( - "\n", - "int main(int argc, const char* argv[]) {", - " return lf_reactor_c_main(argc, argv);", - "}"); + if (cliParams.isEmpty()) { + return String.join( + "\n", + "int main(int argc, const char* argv[]) {", + " return lf_reactor_c_main(argc, argv);", + "}"); + } + return generateMainWithCliParsing(); } } } + /** + * Generate a main() function that uses the table-driven process_user_args() + * to extract user-defined main reactor parameters before forwarding the + * remaining arguments to lf_reactor_c_main(). + */ + private String generateMainWithCliParsing() { + var code = new CodeBuilder(); + code.pr("int main(int argc, const char* argv[]) {"); + code.indent(); + code.pr("_lf_cli_params = _lf_cli_params_table;"); + code.pr("_lf_cli_params_count = " + cliParams.size() + ";"); + code.pr("// Use a fixed-size array because MSVC does not support variable-length arrays."); + code.pr("#define MAX_ARGV 64"); + code.pr("if (argc > MAX_ARGV) {"); + code.indent(); + code.pr("lf_print_error(\"Too many command-line arguments (max %d).\", MAX_ARGV);"); + code.pr("return 1;"); + code.unindent(); + code.pr("}"); + code.pr("const char* newargv[MAX_ARGV];"); + code.pr("int newargc = 0;"); + code.pr("int result = process_user_args(argc, argv, &newargc, newargv);"); + code.pr("if (result != 0) return (result == 1) ? 0 : 1;"); + code.pr("return lf_reactor_c_main(newargc, newargv);"); + code.unindent(); + code.pr("}"); + return code.toString(); + } + + /** + * Generate global variable declarations for CLI parameter overrides + * and the parameter descriptor table used by process_user_args(). + */ + private String generateCliGlobals() { + if (cliParams.isEmpty()) { + return ""; + } + var code = new CodeBuilder(); + + // Declare storage variables for each parameter. + for (Parameter param : cliParams) { + var name = param.getName(); + code.pr(cTypeFor(param) + " _lf_cli_" + name + ";"); + code.pr("bool _lf_cli_" + name + "_given = false;"); + } + + // Generate the parameter descriptor table. + code.pr("lf_cli_param_t _lf_cli_params_table[] = {"); + code.indent(); + for (Parameter param : cliParams) { + var name = param.getName(); + var isWidth = widthParams.contains(name); + String description = descriptionFor(param); + code.pr( + "{\"" + + name + + "\", " + + cliTypeEnumFor(param) + + ", &_lf_cli_" + + name + + ", &_lf_cli_" + + name + + "_given, \"" + + description + + "\", " + + isWidth + + "},"); + } + code.unindent(); + code.pr("};"); + return code.toString(); + } + + /** Supported scalar types for command-line overrides. */ + private static final Set SUPPORTED_CLI_TYPES = + Set.of("int", "double", "float", "bool", "string"); + + /** + * Collect main reactor parameters that can be overridden from the command line. + * Supports time and stringparameters and scalar parameters of type int, double, float, and bool. + */ + private void collectCliParameters() { + if (mainReactor == null) { + return; + } + // Embedded platforms have no command-line interface. + if (targetConfig.isSet(PlatformProperty.INSTANCE)) { + var platform = targetConfig.get(PlatformProperty.INSTANCE).platform(); + if (platform == Platform.ARDUINO + || platform == Platform.ZEPHYR + || platform == Platform.RP2040 + || platform == Platform.FLEXPRET) { + return; + } + } + for (Parameter param : ASTUtils.allParameters(mainReactor)) { + if (ASTUtils.isOfTimeType(param)) { + cliParams.add(param); + } else if (param.getType() != null + && !param.getType().isTime() + && param.getType().getCStyleArraySpec() == null) { + var baseType = ASTUtils.baseType(param.getType()); + if (SUPPORTED_CLI_TYPES.contains(baseType)) { + cliParams.add(param); + } + } + } + } + + /** Return the C type string for a CLI parameter. */ + private String cTypeFor(Parameter param) { + if (ASTUtils.isOfTimeType(param)) return "interval_t"; + var baseType = ASTUtils.baseType(param.getType()); + return switch (baseType) { + case "double" -> "double"; + case "float" -> "float"; + case "bool" -> "bool"; + case "string" -> "const char*"; + default -> "int"; + }; + } + + /** Return the lf_cli_type_t enum constant name for a CLI parameter. */ + private String cliTypeEnumFor(Parameter param) { + if (ASTUtils.isOfTimeType(param)) return "CLI_TIME"; + var baseType = ASTUtils.baseType(param.getType()); + return switch (baseType) { + case "double" -> "CLI_DOUBLE"; + case "float" -> "CLI_FLOAT"; + case "bool" -> "CLI_BOOL"; + case "string" -> "CLI_STRING"; + default -> "CLI_INT"; + }; + } + + /** Return a human-readable description string for a CLI parameter's help message. */ + private String descriptionFor(Parameter param) { + if (ASTUtils.isOfTimeType(param)) { + TimeValue defaultVal = ASTUtils.getDefaultAsTimeValue(param); + String defaultStr = (defaultVal != null) ? defaultVal.toString() : "0"; + return "time value (default: " + defaultStr + ")"; + } + var baseType = ASTUtils.baseType(param.getType()); + String defaultStr = "unspecified"; + var init = param.getInit(); + if (init != null) { + var expr = init.getExpr(); + if (expr instanceof Literal) { + defaultStr = ((Literal) expr).getLiteral(); + } + // Escape double quotes in the default string (e.g., string literals). + defaultStr = defaultStr.replace("\"", "\\\""); + } + return baseType + " value (default: " + defaultStr + ")"; + } + /** * Generate code that is used to override the command line options to the `main` function */ @@ -116,6 +305,11 @@ private String generateSetDefaultCliOption() { : "void lf_set_default_command_line_options() {}"; } + /** Return the list of main reactor parameters that can be overridden from the command line. */ + public List getCliParameters() { + return cliParams; + } + /** Parse the target parameters and set flags to the runCommand accordingly. */ private void parseTargetParameters() { if (targetConfig.get(FastProperty.INSTANCE)) { @@ -136,4 +330,86 @@ private void parseTargetParameters() { runCommand.add(0, "dummy"); } } + + /** + * Collect names of top-level parameters that are transitively used as multiport + * widths or bank widths. Overriding these from the command line is not supported + * because the connection topology is determined at compile time. + */ + private void collectWidthParameters() { + if (mainReactor == null) return; + Set mainParamNames = new HashSet<>(); + for (Parameter p : cliParams) mainParamNames.add(p.getName()); + if (mainParamNames.isEmpty()) return; + + Map identity = new HashMap<>(); + for (String n : mainParamNames) identity.put(n, n); + findWidthParams(mainReactor, mainParamNames, identity, widthParams, new HashSet<>()); + } + + /** + * Recursively search for parameters used in port or bank width specs, tracing + * parameter assignments through instantiations back to the main reactor. + * + * @param reactor The reactor to inspect. + * @param trackedParams Names of parameters in this reactor that originate from the main reactor. + * @param toMainParam Maps a tracked parameter name in this reactor to the originating + * main-reactor parameter name. + * @param result Accumulates main-reactor parameter names that influence widths. + * @param stack Recursion stack for cycle detection. Unlike a global visited set, entries + * are removed after returning so that the same reactor definition can be + * analyzed multiple times with different parameter mappings. + */ + private void findWidthParams( + Reactor reactor, + Set trackedParams, + Map toMainParam, + Set result, + Set stack) { + if (!stack.add(reactor)) return; + + for (Port port : ASTUtils.allPorts(reactor)) { + WidthSpec ws = port.getWidthSpec(); + if (ws == null) continue; + for (WidthTerm term : ws.getTerms()) { + Parameter p = term.getParameter(); + if (p != null && trackedParams.contains(p.getName())) { + result.add(toMainParam.get(p.getName())); + } + } + } + + for (Instantiation inst : ASTUtils.allInstantiations(reactor)) { + WidthSpec bws = inst.getWidthSpec(); + if (bws != null) { + for (WidthTerm term : bws.getTerms()) { + Parameter p = term.getParameter(); + if (p != null && trackedParams.contains(p.getName())) { + result.add(toMainParam.get(p.getName())); + } + } + } + + Reactor childReactor = ASTUtils.toDefinition(inst.getReactorClass()); + if (childReactor == null) continue; + + Set childTracked = new HashSet<>(); + Map childToMain = new HashMap<>(); + for (Assignment assign : inst.getParameters()) { + var rhs = assign.getRhs().getExpr(); + if (rhs instanceof ParameterReference pr) { + String srcName = pr.getParameter().getName(); + if (trackedParams.contains(srcName)) { + String childName = assign.getLhs().getName(); + childTracked.add(childName); + childToMain.put(childName, toMainParam.get(srcName)); + } + } + } + if (!childTracked.isEmpty()) { + findWidthParams(childReactor, childTracked, childToMain, result, stack); + } + } + stack.remove(reactor); + } } diff --git a/core/src/main/java/org/lflang/generator/c/CTimerGenerator.java b/core/src/main/java/org/lflang/generator/c/CTimerGenerator.java index 0d5689f7d0..f32af02958 100644 --- a/core/src/main/java/org/lflang/generator/c/CTimerGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CTimerGenerator.java @@ -1,7 +1,10 @@ package org.lflang.generator.c; import java.util.List; +import org.lflang.TimeValue; import org.lflang.generator.TimerInstance; +import org.lflang.lf.Expression; +import org.lflang.lf.ParameterReference; /** * Generates C code to declare and initialize timers. @@ -16,11 +19,18 @@ public class CTimerGenerator { * * @param timer The timer to initialize for. * @param enc The enclave instance. + * @param useParamRefs If true and the timer offset/period is a parameter reference, generate + * a reference to the self struct field so runtime overrides take effect. Set to false for + * targets (e.g., Python) whose self struct stores parameters as non-native types. */ - public static String generateInitializer(TimerInstance timer, CEnclaveInstance enc) { + public static String generateInitializer( + TimerInstance timer, CEnclaveInstance enc, boolean useParamRefs) { var triggerStructName = CUtil.reactorRef(timer.getParent()) + "->_lf__" + timer.getName(); - var offset = CTypes.getInstance().getTargetTimeExpr(timer.getOffset()); - var period = CTypes.getInstance().getTargetTimeExpr(timer.getPeriod()); + var selfRef = CUtil.reactorRef(timer.getParent()); + var offset = + getTimerExpr(timer.getDefinition().getOffset(), selfRef, timer.getOffset(), useParamRefs); + var period = + getTimerExpr(timer.getDefinition().getPeriod(), selfRef, timer.getPeriod(), useParamRefs); var mode = timer.getMode(false); var envId = enc.getReactorInstance().uniqueID(); var modeRef = @@ -49,4 +59,18 @@ public static String generateInitializer(TimerInstance timer, CEnclaveInstance e + ";", triggerStructName + ".mode = " + modeRef + ";")); } + + /** + * Return a C expression for a timer offset or period. If {@code useParamRefs} is true and + * the AST expression is a parameter reference, return a reference to the parameter field on + * the self struct so that runtime overrides take effect. Otherwise, return the resolved + * literal time expression. + */ + private static String getTimerExpr( + Expression expr, String selfRef, TimeValue resolved, boolean useParamRefs) { + if (useParamRefs && expr instanceof ParameterReference paramRef) { + return selfRef + "->" + paramRef.getParameter().getName(); + } + return CTypes.getInstance().getTargetTimeExpr(resolved); + } } diff --git a/core/src/main/java/org/lflang/generator/python/PythonGenerator.java b/core/src/main/java/org/lflang/generator/python/PythonGenerator.java index 7b597c5f57..b60ad4c318 100644 --- a/core/src/main/java/org/lflang/generator/python/PythonGenerator.java +++ b/core/src/main/java/org/lflang/generator/python/PythonGenerator.java @@ -448,14 +448,25 @@ protected void generateStateVariableInitializations(ReactorInstance instance) { } /** - * Generate runtime initialization code in C for parameters of a given reactor instance + * Generate runtime initialization code in C for parameters of a given reactor instance. + * In Python, parameters are initialized in Python and stored as PyObject* on the C self + * struct, so no C-side initialization is needed. * * @param instance The reactor instance. */ @Override protected void generateParameterInitialization(ReactorInstance instance) { - // Do nothing - // Parameters are initialized in Python + // Do nothing. Parameters are initialized in Python. + } + + /** + * Return false because the Python C self struct stores parameters as PyObject*, not native + * C types. Timer and deadline init code must use resolved literal values instead of + * parameter references. + */ + @Override + protected boolean supportsNativeParameterReferences() { + return false; } /** diff --git a/core/src/main/java/org/lflang/target/Target.java b/core/src/main/java/org/lflang/target/Target.java index cdf52f6d57..a9930996d9 100644 --- a/core/src/main/java/org/lflang/target/Target.java +++ b/core/src/main/java/org/lflang/target/Target.java @@ -548,6 +548,7 @@ public void initialize(TargetConfig config) { BuildTypeProperty.INSTANCE, ClockSyncModeProperty.INSTANCE, ClockSyncOptionsProperty.INSTANCE, + CmakeArgsProperty.INSTANCE, CmakeIncludeProperty.INSTANCE, CommunicationModeProperty.INSTANCE, SSTProperty.INSTANCE, diff --git a/core/src/main/java/org/lflang/target/property/CmakeArgsProperty.java b/core/src/main/java/org/lflang/target/property/CmakeArgsProperty.java new file mode 100644 index 0000000000..173e35ba61 --- /dev/null +++ b/core/src/main/java/org/lflang/target/property/CmakeArgsProperty.java @@ -0,0 +1,67 @@ +package org.lflang.target.property; + +import java.util.HashMap; +import java.util.Map; +import org.lflang.MessageReporter; +import org.lflang.ast.ASTUtils; +import org.lflang.lf.Element; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.type.StringDictionaryType; + +/** + * Directive to specify additional CMake configure arguments. + * + *

Each key-value pair is converted to a CMake configure definition of the form {@code -Dkey=value}. + * This is applied when invoking CMake (not when generating CMakeLists.txt). + */ +public final class CmakeArgsProperty + extends TargetProperty, StringDictionaryType> { + + /** Singleton target property instance. */ + public static final CmakeArgsProperty INSTANCE = new CmakeArgsProperty(); + + private CmakeArgsProperty() { + // Reuse the existing "string keys -> string values" dictionary type. + super(StringDictionaryType.COMPILE_DEFINITION); + } + + @Override + public void update(TargetConfig config, Map value) { + var pairs = new HashMap<>(value); + var existing = config.get(this); + if (config.isSet(this)) { + existing.forEach( + (k, v) -> { + if (!pairs.containsKey(k)) { + pairs.put(k, v); + } + }); + } + config.set(this, pairs); + } + + @Override + public Map initialValue() { + return Map.of(); + } + + @Override + protected Map fromAst(Element node, MessageReporter reporter) { + return ASTUtils.elementToStringMaps(node); + } + + @Override + protected Map fromString(String string, MessageReporter reporter) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Element toAstElement(Map value) { + return ASTUtils.toElement(value); + } + + @Override + public String name() { + return "cmake-args"; + } +} diff --git a/core/src/main/java/org/lflang/target/property/TracePluginProperty.java b/core/src/main/java/org/lflang/target/property/TracePluginProperty.java index 49bc833892..10d06fb6d7 100644 --- a/core/src/main/java/org/lflang/target/property/TracePluginProperty.java +++ b/core/src/main/java/org/lflang/target/property/TracePluginProperty.java @@ -1,18 +1,149 @@ package org.lflang.target.property; -/** Property that provides an alternative tracing implementation. */ -/** The compiler to invoke, unless a build command has been specified. */ -public final class TracePluginProperty extends StringProperty { +import java.util.Objects; +import org.lflang.MessageReporter; +import org.lflang.ast.ASTUtils; +import org.lflang.lf.Element; +import org.lflang.lf.KeyValuePair; +import org.lflang.lf.KeyValuePairs; +import org.lflang.lf.LfFactory; +import org.lflang.lf.LfPackage.Literals; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.type.DictionaryType; +import org.lflang.target.property.type.DictionaryType.DictionaryElement; +import org.lflang.target.property.type.PrimitiveType; +import org.lflang.target.property.type.TargetPropertyType; + +/** Property that provides an alternative tracing implementation via a CMake package + target. */ +public final class TracePluginProperty + extends TargetProperty { /** Singleton target property instance. */ public static final TracePluginProperty INSTANCE = new TracePluginProperty(); private TracePluginProperty() { - super(); + super(DictionaryType.TRACE_PLUGIN_DICT); + } + + /** Parsed value of the trace-plugin target property. */ + public static final class TracePluginSpec { + /** CMake package name used for find_package(); becomes LF_TRACE_PLUGIN. */ + public String pkg; + + /** CMake target to link; becomes LF_TRACE_PLUGIN_LIBRARY. */ + public String library; + + /** + * Optional CMake search paths passed as LF_TRACE_PLUGIN_PATHS. + * + *

This should be a semicolon-separated list of paths (CMake list syntax), e.g. + * {@code /opt/myplugin;/home/me/myplugin/install}. + */ + public String paths; + } + + @Override + public TracePluginSpec initialValue() { + return null; // property is unset by default + } + + @Override + protected TracePluginSpec fromAst(Element node, MessageReporter reporter) { + var spec = new TracePluginSpec(); + for (KeyValuePair entry : node.getKeyvalue().getPairs()) { + TracePluginOption opt = + (TracePluginOption) DictionaryType.TRACE_PLUGIN_DICT.forName(entry.getName()); + switch (Objects.requireNonNull(opt)) { + case PACKAGE -> spec.pkg = ASTUtils.elementToSingleString(entry.getValue()); + case LIBRARY -> spec.library = ASTUtils.elementToSingleString(entry.getValue()); + case PATH -> spec.paths = ASTUtils.elementToSingleString(entry.getValue()); + } + } + return spec; + } + + @Override + protected TracePluginSpec fromString(String string, MessageReporter reporter) { + throw new UnsupportedOperationException( + "trace-plugin no longer accepts a string; use a dictionary with keys 'package' and" + + " 'library'."); + } + + @Override + public void validate(TargetConfig config, MessageReporter reporter) { + if (!config.isSet(this)) return; + var pair = config.lookup(this); + var spec = config.get(this); + if (spec == null + || spec.pkg == null + || spec.pkg.isBlank() + || spec.library == null + || spec.library.isBlank()) { + reporter + .at(pair, Literals.KEY_VALUE_PAIR__VALUE) + .error("trace-plugin must be a dictionary with keys 'package' and 'library'."); + } + } + + @Override + public Element toAstElement(TracePluginSpec value) { + if (value == null) return null; + if (value.pkg == null && value.library == null) return null; + + Element e = LfFactory.eINSTANCE.createElement(); + KeyValuePairs kvp = LfFactory.eINSTANCE.createKeyValuePairs(); + + if (value.pkg != null) { + KeyValuePair p = LfFactory.eINSTANCE.createKeyValuePair(); + p.setName(TracePluginOption.PACKAGE.toString()); + p.setValue(ASTUtils.toElement(value.pkg)); + kvp.getPairs().add(p); + } + + if (value.library != null) { + KeyValuePair p = LfFactory.eINSTANCE.createKeyValuePair(); + p.setName(TracePluginOption.LIBRARY.toString()); + p.setValue(ASTUtils.toElement(value.library)); + kvp.getPairs().add(p); + } + + if (value.paths != null) { + KeyValuePair p = LfFactory.eINSTANCE.createKeyValuePair(); + p.setName(TracePluginOption.PATH.toString()); + p.setValue(ASTUtils.toElement(value.paths)); + kvp.getPairs().add(p); + } + + e.setKeyvalue(kvp); + return kvp.getPairs().isEmpty() ? null : e; } @Override public String name() { return "trace-plugin"; } + + public enum TracePluginOption implements DictionaryElement { + PACKAGE("package", PrimitiveType.STRING), + LIBRARY("library", PrimitiveType.STRING), + PATH("path", PrimitiveType.STRING); + + private final String description; + private final PrimitiveType type; + + TracePluginOption(String description, PrimitiveType type) { + this.description = description; + this.type = type; + } + + @Override + public String toString() { + return description; + } + + @Override + public TargetPropertyType getType() { + return type; + } + } } diff --git a/core/src/main/java/org/lflang/target/property/type/DictionaryType.java b/core/src/main/java/org/lflang/target/property/type/DictionaryType.java index 95b2d5096a..baf6661347 100644 --- a/core/src/main/java/org/lflang/target/property/type/DictionaryType.java +++ b/core/src/main/java/org/lflang/target/property/type/DictionaryType.java @@ -14,6 +14,7 @@ import org.lflang.target.property.CoordinationOptionsProperty.CoordinationOption; import org.lflang.target.property.DockerProperty.DockerOption; import org.lflang.target.property.PlatformProperty.PlatformOption; +import org.lflang.target.property.TracePluginProperty.TracePluginOption; import org.lflang.target.property.SSTProperty.SSTOption; import org.lflang.target.property.TracingProperty.TracingOption; @@ -27,8 +28,9 @@ public enum DictionaryType implements TargetPropertyType { DOCKER_DICT(Arrays.asList(DockerOption.values())), PLATFORM_DICT(Arrays.asList(PlatformOption.values())), COORDINATION_OPTION_DICT(Arrays.asList(CoordinationOption.values())), - SST_DICT(Arrays.asList(SSTOption.values())), - TRACING_DICT(Arrays.asList(TracingOption.values())); + TRACING_DICT(Arrays.asList(TracingOption.values())), + TRACE_PLUGIN_DICT(Arrays.asList(TracePluginOption.values())), + SST_DICT(Arrays.asList(SSTOption.values())); /** The keys and assignable types that are allowed in this dictionary. */ public List options; diff --git a/core/src/main/java/org/lflang/validation/AttributeSpec.java b/core/src/main/java/org/lflang/validation/AttributeSpec.java index 9f4855543a..0eb214c565 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 d9989b1e51..f26292e85f 100644 --- a/core/src/main/java/org/lflang/validation/LFValidator.java +++ b/core/src/main/java/org/lflang/validation/LFValidator.java @@ -458,7 +458,9 @@ public void checkCEnclaves(Instantiation inst) { if (isCBasedTarget() && isEnclave(inst)) { // 1. Disallow banks of enclaves if (inst.getWidthSpec() != null) { - error("Banks of enclaves are not supported in the C target", Literals.WIDTH_SPEC__TERMS); + error( + "Banks of enclaves are not supported in the C target", + Literals.INSTANTIATION__WIDTH_SPEC); } // 2. Disallow multiports and array ports on enclaves @@ -466,23 +468,25 @@ public void checkCEnclaves(Instantiation inst) { for (Input input : encDef.getInputs()) { if (input.getWidthSpec() != null) { error( - "Enclaves with multiports not supported in the C target", Literals.WIDTH_SPEC__TERMS); + "Enclaves with multiports not supported in the C target", + Literals.INSTANTIATION__REACTOR_CLASS); } if (input.getType().getCStyleArraySpec() != null) { error( "Enclaves do not currently support ports with array types in the C target", - Literals.WIDTH_SPEC__TERMS); + Literals.INSTANTIATION__REACTOR_CLASS); } } for (Output output : encDef.getOutputs()) { if (output.getWidthSpec() != null) { error( - "Enclaves with multiports not supported in the C target", Literals.WIDTH_SPEC__TERMS); + "Enclaves with multiports not supported in the C target", + Literals.INSTANTIATION__REACTOR_CLASS); } if (output.getType().getCStyleArraySpec() != null) { error( "Enclaves do not currently support ports with array types in the C target", - Literals.WIDTH_SPEC__TERMS); + Literals.INSTANTIATION__REACTOR_CLASS); } } @@ -491,14 +495,16 @@ public void checkCEnclaves(Instantiation inst) { for (Reaction r : parent.getReactions()) { for (VarRef effect : r.getEffects()) { if (effect.getContainer().equals(inst)) { - error("Enclave input ports can not be driven by reactions", Literals.REACTION__EFFECTS); + error( + "Enclave input ports can not be driven by reactions", + Literals.INSTANTIATION__REACTOR_CLASS); } } for (VarRef source : r.getSources()) { if (source.getContainer().equals(inst)) { error( "Enclave output ports can not be sources for reactions", - Literals.REACTION__EFFECTS); + Literals.INSTANTIATION__REACTOR_CLASS); } } for (TriggerRef trigger : r.getTriggers()) { @@ -506,7 +512,7 @@ public void checkCEnclaves(Instantiation inst) { if (((VarRef) trigger).getContainer().equals(inst)) { error( "Enclave output ports can not be triggers for reactions", - Literals.REACTION__EFFECTS); + Literals.INSTANTIATION__REACTOR_CLASS); } } } @@ -693,6 +699,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) @@ -762,16 +781,36 @@ public void checkParameter(Parameter param) { if (this.target == Target.CPP) { EObject container = param.eContainer(); - Reactor reactor = (Reactor) container; - if (reactor.isMain()) { - // we need to check for the cli parameters that are always taken - List cliParams = List.of("t", "threads", "o", "timeout", "f", "fast", "help"); - if (cliParams.contains(param.getName())) { - error( - "Parameter '" - + param.getName() - + "' is already in use as command line argument by Lingua Franca,", - Literals.PARAMETER__NAME); + if (container instanceof Reactor) { + Reactor reactor = (Reactor) container; + if (reactor.isMain()) { + // we need to check for the cli parameters that are always taken + List cliParams = List.of("t", "threads", "o", "timeout", "f", "fast", "help"); + if (cliParams.contains(param.getName())) { + error( + "Parameter '" + + param.getName() + + "' is already in use as command line argument by Lingua Franca,", + Literals.PARAMETER__NAME); + } + } + } + } + + if (isCBasedTarget()) { + EObject container = param.eContainer(); + if (container instanceof Reactor) { + Reactor reactor = (Reactor) container; + if (reactor.isMain() || reactor.isFederated()) { + List reservedNames = + List.of("fast", "timeout", "keepalive", "workers", "id", "rti", "help"); + if (reservedNames.contains(param.getName())) { + error( + "Parameter '" + + param.getName() + + "' name conflicts with a built-in command-line option.", + Literals.PARAMETER__NAME); + } } } } diff --git a/core/src/main/resources/lib/c/reactor-c b/core/src/main/resources/lib/c/reactor-c index b8f189b391..01709df9b6 160000 --- a/core/src/main/resources/lib/c/reactor-c +++ b/core/src/main/resources/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit b8f189b3914fefbb2929ce19aff8b06e81ec1b12 +Subproject commit 01709df9b6fa7c6ef48779a17a8e2e8b62d7c9d4 diff --git a/core/src/main/resources/lib/cpp/reactor-cpp b/core/src/main/resources/lib/cpp/reactor-cpp index d568ddd6fe..836f22e821 160000 --- a/core/src/main/resources/lib/cpp/reactor-cpp +++ b/core/src/main/resources/lib/cpp/reactor-cpp @@ -1 +1 @@ -Subproject commit d568ddd6fef775108c76f42cebc64560da4ff518 +Subproject commit 836f22e82193ef66101ffbf756c24395e58b083c 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 9d2c65d7b4..4f1603a532 100644 --- a/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java +++ b/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java @@ -29,6 +29,7 @@ import org.lflang.target.property.CargoDependenciesProperty; import org.lflang.target.property.PlatformProperty; import org.lflang.target.property.TargetProperty; +import org.lflang.target.property.TracePluginProperty; import org.lflang.target.property.type.ArrayType; import org.lflang.target.property.type.DictionaryType; import org.lflang.target.property.type.DictionaryType.DictionaryElement; @@ -1586,6 +1587,11 @@ public Collection checkTargetProperties() throws Exception { // we test that separately as it has better error messages continue; } + if (property instanceof TracePluginProperty) { + // trace-plugin has semantic requirements beyond its structural dictionary type (it requires + // both 'package' and 'library'), so it doesn't fit the generic type-synthesis tests here. + continue; + } var type = property.type; List knownCorrect = synthesizeExamples(type, true); @@ -2371,4 +2377,32 @@ public void testMutuallyExclusiveThreadingParams() throws Exception { null, "Cannot specify workers in single-threaded mode."); } + + @Test + public void tracePluginRequiresPackageAndLibraryMissingLibrary() throws Exception { + String testCase = + """ + target C { trace-plugin: {package: "foo"} }; + main reactor {} + """; + validator.assertError( + parseWithoutError(testCase), + LfPackage.eINSTANCE.getKeyValuePair(), + null, + "trace-plugin must be a dictionary with keys 'package' and 'library'."); + } + + @Test + public void tracePluginRequiresPackageAndLibraryMissingPackage() throws Exception { + String testCase = + """ + target C { trace-plugin: {library: "bar"} }; + main reactor {} + """; + validator.assertError( + parseWithoutError(testCase), + LfPackage.eINSTANCE.getKeyValuePair(), + null, + "trace-plugin must be a dictionary with keys 'package' and 'library'."); + } } diff --git a/test/C/src/AfterArray.lf b/test/C/src/AfterArray.lf new file mode 100644 index 0000000000..8099d1b568 --- /dev/null +++ b/test/C/src/AfterArray.lf @@ -0,0 +1,52 @@ +target C { + fast: true, + timeout: 5 sec +} + +reactor Source(size: int = 10) { + output out: uint8_t[16] + + state inputs: uint8_t[16] = {0} + + timer t(0, 1 s) + + reaction(startup) {= + for (int i = 0; i < self->size; i++) { + self->inputs[i] = i+1; + } + =} + + reaction(t) -> out {= + memcpy(out->value, (uint8_t *)self->inputs , self->size); + lf_set_present(out); + =} +} + +reactor Sink { + input inp: uint8_t[16] + state count: int = 0 + + reaction(inp) {= + uint64_t ts = 0; + lf_print("======================="); + for (int i = 0; i < 16; i++) { + lf_print("received %d-th value: %d", i, inp->value[i]); + if (i < 4 && inp->value[i] != i+1) { + lf_print_error_and_exit("Expected value %d, but got %d", i+1, inp->value[i]); + } + } + self->count++; + =} + + reaction(shutdown) {= + if (self->count != 5) { + lf_print_error_and_exit("Expected 5 inputs, but got %d", self->count); + } + =} +} + +main reactor { + p = new Source() + m = new Sink() + p.out -> m.inp after 10 msec +} diff --git a/test/C/src/CommandLineAction.lf b/test/C/src/CommandLineAction.lf new file mode 100644 index 0000000000..a7eb6b81c6 --- /dev/null +++ b/test/C/src/CommandLineAction.lf @@ -0,0 +1,32 @@ +target C { + fast: true +} + +main reactor(min_delay: time = 1 ns, min_spacing: time = 10 ns) { + logical action a(min_delay, min_spacing) + timer t(0, 1 ns) + state count: int = 0 + + reaction(t) -> a {= + lf_schedule(a, 0); + =} + + reaction(a) {= + interval_t elapsed = lf_time_logical_elapsed(); + lf_print("Elapsed: " PRINTF_TIME ".\n", elapsed); + interval_t expected = self->count++ * self->min_spacing + self->min_delay; + if (elapsed != expected) { + lf_print_error_and_exit("Expected " PRINTF_TIME " but got " PRINTF_TIME, expected, elapsed); + } + if (self->count == 10) { + lf_request_stop(); + } + =} + + reaction(shutdown) {= + if (self->count != 10) { + lf_print_error_and_exit("Expected 10 events but got %d", self->count); + } + lf_print("Test passed.\n"); + =} +} diff --git a/test/C/src/CommandLineDeadline.lf b/test/C/src/CommandLineDeadline.lf new file mode 100644 index 0000000000..22dc69ee7c --- /dev/null +++ b/test/C/src/CommandLineDeadline.lf @@ -0,0 +1,45 @@ +// Test that main reactor parameters propagate to child reactor deadlines. +// Also tests command-line override of parameters. +// Default: execution_time=100ms, deadline_time=50ms -> violation occurs -> expect_violation=1. +// CLI test: --execution_time 10 msec --deadline_time 200 msec --expect_violation 0 +// -> no violation -> expect_violation=0. +target C + +reactor Source(execution_time: time = 100 ms) { + output out: int + + reaction(startup) -> out {= + lf_sleep(self->execution_time); + lf_set(out, 42); + =} +} + +reactor Print(deadline_time: time = 50 ms, expect_violation: int = 1) { + input in: int + state violation_detected: int = 0 + + reaction(in) {= + lf_print("No deadline violation."); + =} deadline(deadline_time) {= + self->violation_detected = 1; + lf_print("Deadline violation detected."); + =} + + reaction(shutdown) {= + if (self->violation_detected != self->expect_violation) { + lf_print_error_and_exit("Expected expect_violation=%d but got %d.", + self->expect_violation, self->violation_detected); + } + printf("SUCCESS: expect_violation=%d, violation_detected=%d.\n", + self->expect_violation, self->violation_detected); + =} +} + +main reactor CommandLineDeadline( + execution_time: time = 100 ms, + deadline_time: time = 50 ms, + expect_violation: int = 1) { + s = new Source(execution_time=execution_time) + p = new Print(deadline_time=deadline_time, expect_violation=expect_violation) + s.out -> p.in +} diff --git a/test/C/src/CommandLineParam.lf b/test/C/src/CommandLineParam.lf new file mode 100644 index 0000000000..1df34528c1 --- /dev/null +++ b/test/C/src/CommandLineParam.lf @@ -0,0 +1,50 @@ +// Test that main reactor parameters propagate to child reactor timers. +// Also tests command-line override of parameters. +// With a timeout of 5 sec and a period of 1 sec, expect 6 firings (at t=0,1,2,3,4,5). +// A JUnit integration test verifies CLI override with --period 500 msec --expected 11. +target C { + timeout: 5 sec, + fast: true +} + +reactor Source(period: time = 1 sec, value: double = 3.14159) { + timer t(0, period) + output out: double + state count: int = 0 + + reaction(t) -> out {= + self->count++; + lf_set(out, self->value); + =} + + reaction(shutdown) {= + printf("Total firings: %d\n", self->count); + =} +} + +reactor Print(expected: int = 6, value: double = 3.14159) { + input in: double + state count: int = 0 + + reaction(in) {= + self->count++; + printf("Received: %f at time %lld\n", in->value, + (long long)(lf_time_logical_elapsed() / 1000000)); + if (in->value != self->value) { + lf_print_error_and_exit("Expected %f but got %f.", self->value, in->value); + } + =} + + reaction(shutdown) {= + if (self->count != self->expected) { + lf_print_error_and_exit("Expected %d firings but got %d.", self->expected, self->count); + } + printf("SUCCESS: %d firings.\n", self->count); + =} +} + +main reactor CommandLineParam(period: time = 1 sec, expected: int = 6, value: double = 3.14159) { + s = new Source(period=period, value=value) + p = new Print(expected=expected, value=value) + s.out -> p.in +} diff --git a/test/C/src/TopLevelPreambles.lf b/test/C/src/TopLevelPreambles.lf new file mode 100644 index 0000000000..8180773c79 --- /dev/null +++ b/test/C/src/TopLevelPreambles.lf @@ -0,0 +1,18 @@ +target C + +import CustomTypeEE from "lib/CustomTypeEE.lf" + +reactor CustomTypeEEE extends CustomTypeEE { + reaction(startup) {= + lf_print("CustomTypeEEE startup"); + =} +} + +main reactor TopLevelPreambles { + custom_type = new CustomTypeEEE() + + reaction(custom_type.out) {= + custom_type_t value = custom_type.out->value; + printf("Received: %d, %d\n", value.value1, value.value2); + =} +} diff --git a/test/C/src/federated/CommandLineDeadlineFederated.lf b/test/C/src/federated/CommandLineDeadlineFederated.lf new file mode 100644 index 0000000000..7b6cf65cc9 --- /dev/null +++ b/test/C/src/federated/CommandLineDeadlineFederated.lf @@ -0,0 +1,47 @@ +// Test that main reactor parameters propagate to child reactor deadlines for federated execution. +// Also tests command-line override of parameters. +// Default: execution_time=100ms, deadline_time=50ms -> violation occurs -> expect_violation=1. +// CLI test: --execution_time 10 msec --deadline_time 200 msec --expect_violation 0 +// -> no violation -> expect_violation=0. +target C { + timeout: 0 s +} + +reactor Source(execution_time: time = 100 ms) { + output out: int + + reaction(startup) -> out {= + lf_sleep(self->execution_time); + lf_set(out, 42); + =} +} + +reactor Print(deadline_time: time = 50 ms, expect_violation: int = 1) { + input in: int + state violation_detected: int = 0 + + reaction(in) {= + lf_print("No deadline violation."); + =} deadline(deadline_time) {= + self->violation_detected = 1; + lf_print("Deadline violation detected."); + =} + + reaction(shutdown) {= + if (self->violation_detected != self->expect_violation) { + lf_print_error_and_exit("Expected expect_violation=%d but got %d.", + self->expect_violation, self->violation_detected); + } + printf("SUCCESS: expect_violation=%d, violation_detected=%d.\n", + self->expect_violation, self->violation_detected); + =} +} + +federated reactor( + execution_time: time = 100 ms, + deadline_time: time = 50 ms, + expect_violation: int = 1) { + s = new Source(execution_time=execution_time) + p = new Print(deadline_time=deadline_time, expect_violation=expect_violation) + s.out -> p.in +} diff --git a/test/C/src/federated/transient/TransientDownstreamWithTimer.lf b/test/C/src/federated/transient/TransientDownstreamWithTimer.lf new file mode 100644 index 0000000000..0b9626a362 --- /dev/null +++ b/test/C/src/federated/transient/TransientDownstreamWithTimer.lf @@ -0,0 +1,158 @@ +/** + * 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/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..745ee2727d --- /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/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..b8270af8d7 --- /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/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 +} diff --git a/test/C/src/lib/CustomType.lf b/test/C/src/lib/CustomType.lf new file mode 100644 index 0000000000..625014e43b --- /dev/null +++ b/test/C/src/lib/CustomType.lf @@ -0,0 +1,17 @@ +target C + +preamble {= + typedef struct { + int value1; + int value2; + } custom_type_t; +=} + +reactor CustomType { + output out: custom_type_t + + reaction(startup) -> out {= + custom_type_t value = {42, 43}; + lf_set(out, value); + =} +} diff --git a/test/C/src/lib/CustomTypeE.lf b/test/C/src/lib/CustomTypeE.lf new file mode 100644 index 0000000000..78328fce2a --- /dev/null +++ b/test/C/src/lib/CustomTypeE.lf @@ -0,0 +1,9 @@ +target C + +import CustomType from "CustomType.lf" + +reactor CustomTypeE extends CustomType { + reaction(startup) -> out {= + lf_print("CustomTypeE startup"); + =} +} diff --git a/test/C/src/lib/CustomTypeEE.lf b/test/C/src/lib/CustomTypeEE.lf new file mode 100644 index 0000000000..75ed58eb7d --- /dev/null +++ b/test/C/src/lib/CustomTypeEE.lf @@ -0,0 +1,9 @@ +target C + +import CustomTypeE from "CustomTypeE.lf" + +reactor CustomTypeEE extends CustomTypeE { + reaction(startup) {= + lf_print("CustomTypeEE startup"); + =} +}