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