diff --git a/.github/workflows/ts-tests.yml b/.github/workflows/ts-tests.yml index de2b2be406..c4f10db875 100644 --- a/.github/workflows/ts-tests.yml +++ b/.github/workflows/ts-tests.yml @@ -28,7 +28,7 @@ jobs: if: ${{ runner.os == 'macOS' }} - name: Perform TypeScript tests run: | - ./gradlew test --tests org.lflang.tests.runtime.TypeScriptTest.* + ./gradlew test --tests org.lflang.tests.runtime.TypeScriptTest.* -Druntime="git://github.com/lf-lang/reactor-ts.git#fed-gen" - name: Report to CodeCov uses: codecov/codecov-action@v2.1.0 with: diff --git a/.gitignore b/.gitignore index 5d10393cdb..bb35405752 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ **/build/ **/test-bin/ **/src-gen/ +**/fed-gen/ **/xtend-gen/ # Created by https://www.toptal.com/developers/gitignore/api/intellij,gradle,eclipse,maven,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=intellij,gradle,eclipse,maven,visualstudiocode @@ -73,6 +74,8 @@ local.properties .cache-main .scala_dependencies .worksheet +.metals/ +.bloop/ # Uncomment this line if you wish to ignore the project description file. # Typically, this file would be tracked if it contains build/dependency configurations: diff --git a/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServerExtension.java b/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServerExtension.java index f237bf6fc0..7d354fd4b6 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServerExtension.java +++ b/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServerExtension.java @@ -15,6 +15,7 @@ import org.eclipse.xtext.ide.server.ILanguageServerAccess; import org.eclipse.xtext.util.CancelIndicator; +import org.lflang.generator.GeneratorResult.Status; import org.lflang.generator.IntegratedBuilder; import org.lflang.generator.GeneratorResult; import org.lflang.LFStandaloneSetup; @@ -90,11 +91,12 @@ public void partialBuild(String uri) { @JsonNotification("generator/buildAndRun") public CompletableFuture buildAndRun(String uri) { return new CompletableFuture().completeAsync(() -> { - LFCommand result = buildWithProgress(client, uri, true).getCommand(); - if (result == null) return null; + var result = buildWithProgress(client, uri, true); + if (!result.equals(Status.COMPILED)) return null; + LFCommand cmd = result.getContext().getFileConfig().getCommand(); ArrayList ret = new ArrayList<>(); - ret.add(result.directory().toString()); - ret.addAll(result.command()); + ret.add(cmd.directory().toString()); + ret.addAll(cmd.command()); return ret.toArray(new String[0]); }); } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java index 018f11c623..cd013d6c57 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java @@ -278,7 +278,7 @@ public KNode transform(final Model model) { List reactorNodes = new ArrayList<>(); for (Reactor reactor : model.getReactors()) { if (reactor == main) continue; - ReactorInstance reactorInstance = new ReactorInstance(reactor, new SynthesisErrorReporter(), new HashSet<>()); + ReactorInstance reactorInstance = new ReactorInstance(reactor, new SynthesisErrorReporter()); reactorNodes.addAll(createReactorNode(reactorInstance, main == null, HashBasedTable.create(), HashBasedTable.create(), diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.java index 4c76c9ad8f..e7143b44fc 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.java @@ -46,12 +46,12 @@ public static IPropertyHolder linkInstance(KGraphElement elem, NamedInstance } /** - * Returns the linked NamedInstance for ther given KGraphElement. + * Returns the linked NamedInstance for the given KGraphElement. */ - public static > T getLinkedInstance(KGraphElement elem) { - NamedInstance instance = elem.getProperty(LINKED_INSTANCE); + public static NamedInstance getLinkedInstance(KGraphElement elem) { + var instance = elem.getProperty(LINKED_INSTANCE); if (instance != null) { - return (T) instance; + return instance; } return null; } diff --git a/org.lflang.tests/src/org/lflang/tests/LFTest.java b/org.lflang.tests/src/org/lflang/tests/LFTest.java index d3ef39c892..cc69dea77a 100644 --- a/org.lflang.tests/src/org/lflang/tests/LFTest.java +++ b/org.lflang.tests/src/org/lflang/tests/LFTest.java @@ -32,9 +32,6 @@ public class LFTest implements Comparable { /** The result of the test. */ private Result result = Result.UNKNOWN; - /** Object used to determine where the code generator puts files. */ - private FileConfig fileConfig; - /** Context provided to the code generators */ private LFGeneratorContext context; @@ -76,7 +73,7 @@ public OutputStream getOutputStream() { return compilationLog; } - public FileConfig getFileConfig() { return fileConfig; } + public FileConfig getFileConfig() { return context.getFileConfig(); } public LFGeneratorContext getContext() { return context; } @@ -155,7 +152,7 @@ public void handleTestError(TestError e) { } if (e.getException() != null) { issues.append(System.lineSeparator()); - issues.append(TestBase.stackTraceToString(e)); + issues.append(TestBase.stackTraceToString(e.getException())); } } @@ -165,9 +162,8 @@ public void markPassed() { execLog.clear(); } - void configure(LFGeneratorContext context, FileConfig fileConfig) { + void configure(LFGeneratorContext context) { this.context = context; - this.fileConfig = fileConfig; } /** diff --git a/org.lflang.tests/src/org/lflang/tests/RunSingleTestMain.java b/org.lflang.tests/src/org/lflang/tests/RunSingleTestMain.java index 329c9028a8..0f7e919174 100644 --- a/org.lflang.tests/src/org/lflang/tests/RunSingleTestMain.java +++ b/org.lflang.tests/src/org/lflang/tests/RunSingleTestMain.java @@ -31,7 +31,6 @@ import java.util.regex.Pattern; import org.lflang.Target; -import org.lflang.tests.TestBase.TestLevel; import org.lflang.tests.runtime.CCppTest; import org.lflang.tests.runtime.CTest; import org.lflang.tests.runtime.CppTest; diff --git a/org.lflang.tests/src/org/lflang/tests/TestBase.java b/org.lflang.tests/src/org/lflang/tests/TestBase.java index 80e3e02ac8..d0092c54ef 100644 --- a/org.lflang.tests/src/org/lflang/tests/TestBase.java +++ b/org.lflang.tests/src/org/lflang/tests/TestBase.java @@ -9,9 +9,9 @@ import java.io.StringWriter; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; import java.nio.file.Path; import java.io.File; -import java.io.FileWriter; import java.io.BufferedWriter; import java.util.Arrays; import java.util.Collections; @@ -28,7 +28,6 @@ import org.eclipse.emf.ecore.resource.Resource.Diagnostic; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.xtext.diagnostics.Severity; -import org.eclipse.xtext.generator.IGeneratorContext; import org.eclipse.xtext.generator.JavaIoFileSystemAccess; import org.eclipse.xtext.testing.InjectWith; import org.eclipse.xtext.testing.extensions.InjectionExtension; @@ -44,7 +43,6 @@ import org.lflang.LFStandaloneSetup; import org.lflang.Target; import org.lflang.generator.GeneratorResult; -import org.lflang.generator.DockerGeneratorBase; import org.lflang.generator.LFGenerator; import org.lflang.generator.LFGeneratorContext; import org.lflang.generator.LFGeneratorContext.BuildParm; @@ -85,7 +83,7 @@ public abstract class TestBase { private static final PrintStream err = System.err; /** Execution timeout enforced for all tests. */ - private static final long MAX_EXECUTION_TIME_SECONDS = 60; + private static final long MAX_EXECUTION_TIME_SECONDS = 180; /** Content separator used in test output, 78 characters wide. */ public static final String THIN_LINE = @@ -159,9 +157,6 @@ public static class Message { public static final String DESC_SCHED_SWAPPING = "Running with non-default runtime scheduler "; public static final String DESC_ROS2 = "Running tests using ROS2."; public static final String DESC_MODAL = "Run modal reactor tests."; - - /* Missing dependency messages */ - public static final String MISSING_DOCKER = "Executable 'docker' not found or 'docker' daemon thread not running"; } /** Constructor for test classes that test a single target. */ @@ -183,7 +178,6 @@ protected TestBase(List targets) { * @param selected A predicate that given a test category returns whether * it should be included in this test run or not. * @param configurator A procedure for configuring the tests. - * @param level The level of testing to be performed during this run. * @param copy Whether or not to work on copies of tests in the test. * registry. */ @@ -191,8 +185,7 @@ protected final void runTestsAndPrintResults(Target target, Predicate selected, Configurator configurator, boolean copy) { - var categories = Arrays.stream(TestCategory.values()).filter(selected) - .collect(Collectors.toList()); + var categories = Arrays.stream(TestCategory.values()).filter(selected).toList(); for (var category : categories) { System.out.println(category.getHeader()); var tests = TestRegistry.getRegisteredTests(target, category, copy); @@ -213,7 +206,6 @@ protected final void runTestsAndPrintResults(Target target, * @param selected A predicate that given a test category returns whether * it should be included in this test run or not. * @param configurator A procedure for configuring the tests. - * @param level The level of testing to be performed during this run. * @param copy Whether or not to work on copies of tests in the test. * registry. */ @@ -235,7 +227,6 @@ protected void runTestsForTargets(String description, * @param selected A predicate that given a test category returns whether * it should be included in this test run or not. * @param configurator A procedure for configuring the tests. - * @param level The level of testing to be performed during this run. * @param copy Whether to work on copies of tests in the test. * registry. */ @@ -251,7 +242,7 @@ protected void runTestsFor(List subset, } /** - * Whether to enable {@link #runWithThreadingOff()}. + * Whether to enable threading. */ protected boolean supportsSingleThreadedExecution() { return false; @@ -359,11 +350,14 @@ protected static void printTestHeader(Target target, String description) { * @param tests The tests to inspect the results of. */ private static void checkAndReportFailures(Set tests) { - var passed = tests.stream().filter(it -> !it.hasFailed()).count(); - - System.out.print(THIN_LINE); - System.out.println("Passing: " + passed + "/" + tests.size()); - System.out.print(THIN_LINE); + var passed = tests.stream().filter(it -> it.hasPassed()).collect(Collectors.toList()); + var s = new StringBuffer(); + s.append(THIN_LINE); + s.append("Passing: " + passed.size() + "/" + tests.size() + "\n"); + s.append(THIN_LINE); + passed.forEach(test -> s.append("Passed: ").append(test).append("\n")); + s.append(THIN_LINE); + System.out.print(s.toString()); for (var test : tests) { test.reportErrors(); @@ -383,12 +377,12 @@ private static void checkAndReportFailures(Set tests) { * @param configurator The configurator to apply to the test. * @param level The level of testing in which the generator context will be * used. - * @return a generator context with a fresh resource, unaffected by any AST - * transformation that may have occured in other tests. - * @throws IOException if there is any file access problem */ - private LFGeneratorContext configure(LFTest test, Configurator configurator, TestLevel level) throws IOException, TestError { + private void configure(LFTest test, Configurator configurator, TestLevel level) throws IOException, TestError { var props = new Properties(); + props.setProperty("hierarchical-bin", "true"); + addExtraLfcArgs(props); + var sysProps = System.getProperties(); // Set the external-runtime-path property if it was specified. if (sysProps.containsKey("runtime")) { @@ -401,11 +395,6 @@ private LFGeneratorContext configure(LFTest test, Configurator configurator, Tes System.out.println("Using default runtime."); } - var context = new MainContext( - LFGeneratorContext.Mode.STANDALONE, CancelIndicator.NullImpl, (m, p) -> {}, props, true, - fileConfig -> new DefaultErrorReporter() - ); - var r = resourceSetProvider.get().getResource( URI.createFileURI(test.getSrcPath().toFile().getAbsolutePath()), true); @@ -416,30 +405,35 @@ private LFGeneratorContext configure(LFTest test, Configurator configurator, Tes } fileAccess.setOutputPath(FileConfig.findPackageRoot(test.getSrcPath(), s -> {}).resolve(FileConfig.DEFAULT_SRC_GEN_DIR).toString()); - test.configure(context, new FileConfig(r, FileConfig.getSrcGenRoot(fileAccess), context.useHierarchicalBin())); + var context = new MainContext( + LFGeneratorContext.Mode.STANDALONE, CancelIndicator.NullImpl, (m, p) -> {}, props, r, fileAccess, + fileConfig -> new DefaultErrorReporter() + ); + + test.configure(context); // Set the no-compile flag the test is not supposed to reach the build stage. if (level.compareTo(TestLevel.BUILD) < 0) { context.getArgs().setProperty("no-compile", ""); } - addExtraLfcArgs(context.getArgs()); - // Update the test by applying the configuration. E.g., to carry out an AST transformation. - if (configurator != null && !configurator.configure(test)) { - throw new TestError("Test configuration unsuccessful.", Result.CONFIG_FAIL); + if (configurator != null) { + if (!configurator.configure(test)) { + throw new TestError("Test configuration unsuccessful.", Result.CONFIG_FAIL); + } + context.loadTargetConfig(); // Reload in case target properties have changed. } - - return context; } /** * Validate the given test. Throw an TestError if validation failed. */ - private void validate(LFTest test, IGeneratorContext context) throws TestError { + private void validate(LFTest test) throws TestError { // Validate the resource and store issues in the test object. try { - var issues = validator.validate(test.getFileConfig().resource, + var context = test.getContext(); + var issues = validator.validate(context.getFileConfig().resource, CheckMode.ALL, context.getCancelIndicator()); if (issues != null && !issues.isEmpty()) { if (issues.stream().anyMatch(it -> it.getSeverity() == Severity.ERROR)) { @@ -463,7 +457,6 @@ protected void addExtraLfcArgs(Properties args) { args.setProperty("logging", "Debug"); } - /** * Invoke the code generator for the given test. * @@ -492,59 +485,55 @@ private GeneratorResult generateCode(LFTest test) throws TestError { * did not execute, took too long to execute, or executed but exited with * an error code. */ - private void execute(LFTest test, GeneratorResult generatorResult) throws TestError { - final List pbList = getExecCommand(test, generatorResult); - if (pbList.isEmpty()) { - return; - } + private void execute(LFTest test) throws TestError { + final var pb = getExecCommand(test); try { - for (ProcessBuilder pb : pbList) { - var p = pb.start(); - var stdout = test.recordStdOut(p); - var stderr = test.recordStdErr(p); - - var stdoutException = new AtomicReference(null); - var stderrException = new AtomicReference(null); - - stdout.setUncaughtExceptionHandler((thread, throwable) -> stdoutException.set(throwable)); - stderr.setUncaughtExceptionHandler((thread, throwable) -> stderrException.set(throwable)); - - stderr.start(); - stdout.start(); - - if (!p.waitFor(MAX_EXECUTION_TIME_SECONDS, TimeUnit.SECONDS)) { - stdout.interrupt(); - stderr.interrupt(); - p.destroyForcibly(); - throw new TestError(Result.TEST_TIMEOUT); - } else { - if (stdoutException.get() != null || stderrException.get() != null) { - StringBuffer sb = new StringBuffer(); - if (stdoutException.get() != null) { - sb.append("Error during stdout handling:" + System.lineSeparator()); - sb.append(stackTraceToString(stdoutException.get())); - } - if (stderrException.get() != null) { - sb.append("Error during stderr handling:" + System.lineSeparator()); - sb.append(stackTraceToString(stderrException.get())); - } - throw new TestError(sb.toString(), Result.TEST_EXCEPTION); + var p = pb.start(); + var stdout = test.recordStdOut(p); + var stderr = test.recordStdErr(p); + + var stdoutException = new AtomicReference(null); + var stderrException = new AtomicReference(null); + + stdout.setUncaughtExceptionHandler((thread, throwable) -> stdoutException.set(throwable)); + stderr.setUncaughtExceptionHandler((thread, throwable) -> stderrException.set(throwable)); + + stderr.start(); + stdout.start(); + + if (!p.waitFor(MAX_EXECUTION_TIME_SECONDS, TimeUnit.SECONDS)) { + stdout.interrupt(); + stderr.interrupt(); + p.destroyForcibly(); + throw new TestError(Result.TEST_TIMEOUT); + } else { + if (stdoutException.get() != null || stderrException.get() != null) { + StringBuffer sb = new StringBuffer(); + if (stdoutException.get() != null) { + sb.append("Error during stdout handling:" + System.lineSeparator()); + sb.append(stackTraceToString(stdoutException.get())); + } + if (stderrException.get() != null) { + sb.append("Error during stderr handling:" + System.lineSeparator()); + sb.append(stackTraceToString(stderrException.get())); } - if (p.exitValue() != 0) { - String message = "Exit code: " + p.exitValue(); - if (p.exitValue() == 139) { - // The java ProcessBuiler and Process interface does not allow us to reliably retrieve stderr and stdout - // from a process that segfaults. We can only print a message indicating that the putput is incomplete. - message += System.lineSeparator() + + throw new TestError(sb.toString(), Result.TEST_EXCEPTION); + } + if (p.exitValue() != 0) { + String message = "Exit code: " + p.exitValue(); + if (p.exitValue() == 139) { + // The java ProcessBuilder and Process interface does not allow us to reliably retrieve stderr and stdout + // from a process that segfaults. We can only print a message indicating that the putput is incomplete. + message += System.lineSeparator() + "This exit code typically indicates a segfault. In this case, the execution output is likely missing or incomplete."; - } - throw new TestError(message, Result.TEST_FAIL); } + throw new TestError(message, Result.TEST_FAIL); } } } catch (TestError e) { - throw e; + throw e; } catch (Throwable e) { + e.printStackTrace(); throw new TestError("Exception during test execution.", Result.TEST_EXCEPTION, e); } } @@ -558,117 +547,104 @@ static public String stackTraceToString(Throwable t) { return sw.toString(); } + /** Bash script that is used to execute docker tests. */ + static private String DOCKER_RUN_SCRIPT = """ + #!/bin/bash + + # exit when any command fails + set -e + + docker compose -f "$1" rm -f + docker compose -f "$1" up --build | tee docker_log.txt + docker compose -f "$1" down --rmi local + + errors=`grep -E "exited with code [1-9]" docker_log.txt | cat` + rm docker_log.txt + + if [[ $errors ]]; then + echo "====================================================================" + echo "ERROR: One or multiple containers exited with a non-zero exit code." + echo " See the log above for details. The following containers failed:" + echo $errors + exit 1 + fi + + exit 0 + """; + /** - * Return the content of the bash script used for testing docker option in federated execution. - * @param dockerFiles A list of paths to docker files. - * @param dockerComposeFilePath The path to the docker compose file. + * Path to a bash script containing DOCKER_RUN_SCRIPT. */ - private String getDockerRunScript(List dockerFiles, Path dockerComposeFilePath) { - var dockerComposeCommand = DockerGeneratorBase.getDockerComposeCommand(); - StringBuilder shCode = new StringBuilder(); - shCode.append("#!/bin/bash\n"); - shCode.append("pids=\"\"\n"); - shCode.append(String.format("%s run -f %s --rm -T rti &\n", - dockerComposeCommand, dockerComposeFilePath)); - shCode.append("pids+=\"$!\"\nsleep 3\n"); - for (Path dockerFile : dockerFiles) { - var composeServiceName = dockerFile.getFileName().toString().replace(".Dockerfile", ""); - shCode.append(String.format("%s run -f %s --rm -T %s &\n", - dockerComposeCommand, - dockerComposeFilePath, - composeServiceName)); - shCode.append("pids+=\" $!\"\n"); - } - shCode.append("for p in $pids; do\n"); - shCode.append(" if wait $p; then\n"); - shCode.append(" :\n"); - shCode.append(" else\n"); - shCode.append(" exit 1\n"); - shCode.append(" fi\n"); - shCode.append("done\n"); - return shCode.toString(); - } + private static Path dockerRunScript = null; /** - * Returns true if docker exists, false otherwise. + * Return the path to a bash script containing DOCKER_RUN_SCRIPT. + * + * If the script does not yet exist, it is created. */ - private boolean checkDockerExists() { - LFCommand checkCommand = LFCommand.get("docker", List.of("info")); - return checkCommand.run() == 0; + private Path getDockerRunScript() throws TestError { + if (dockerRunScript != null) { + return dockerRunScript; + } + + try { + var file = File.createTempFile("run_docker_test", "sh"); + file.deleteOnExit(); + file.setExecutable(true); + var path = file.toPath(); + try (BufferedWriter writer = Files.newBufferedWriter(path)) { + writer.write(DOCKER_RUN_SCRIPT); + } + dockerRunScript = path; + } catch (IOException e) { + throw new TestError("IO Error during test preparation.", Result.TEST_EXCEPTION, e); + } + + return dockerRunScript; } /** - * Return a list of ProcessBuilders used to test the docker option under non-federated execution. - * See the following for references on the instructions called: - * docker build: https://docs.docker.com/engine/reference/commandline/build/ - * docker run: https://docs.docker.com/engine/reference/run/ - * docker image: https://docs.docker.com/engine/reference/commandline/image/ - * - * @param test The test to get the execution command for. + * Throws TestError if docker does not exist. Does nothing otherwise. */ - private List getNonfederatedDockerExecCommand(LFTest test) { - if (!checkDockerExists()) { - System.out.println(Message.MISSING_DOCKER); - return List.of(new ProcessBuilder("exit", "1")); + private void checkDockerExists() throws TestError { + if (LFCommand.get("docker", List.of()) == null) { + throw new TestError("Executable 'docker' not found" , Result.NO_EXEC_FAIL); + } + if (LFCommand.get("docker-compose", List.of()) == null) { + throw new TestError("Executable 'docker-compose' not found" , Result.NO_EXEC_FAIL); } - var srcGenPath = test.getFileConfig().getSrcGenPath(); - var dockerComposeFile = FileUtil.globFilesEndsWith(srcGenPath, "docker-compose.yml").get(0); - var dockerComposeCommand = DockerGeneratorBase.getDockerComposeCommand(); - return List.of(new ProcessBuilder(dockerComposeCommand, "-f", dockerComposeFile.toString(), "rm", "-f"), - new ProcessBuilder(dockerComposeCommand, "-f", dockerComposeFile.toString(), "up", "--build"), - new ProcessBuilder(dockerComposeCommand, "-f", dockerComposeFile.toString(), "down", "--rmi", "local")); } /** - * Return a list of ProcessBuilders used to test the docker option under federated execution. + * Return a ProcessBuilder used to test the docker execution. * @param test The test to get the execution command for. */ - private List getFederatedDockerExecCommand(LFTest test) { - if (!checkDockerExists()) { - System.out.println(Message.MISSING_DOCKER); - return List.of(new ProcessBuilder("exit", "1")); - } + private ProcessBuilder getDockerExecCommand(LFTest test) throws TestError { + checkDockerExists(); var srcGenPath = test.getFileConfig().getSrcGenPath(); - List dockerFiles = FileUtil.globFilesEndsWith(srcGenPath, ".Dockerfile"); - try { - File testScript = File.createTempFile("dockertest", null); - testScript.deleteOnExit(); - if (!testScript.setExecutable(true)) { - throw new IOException("Failed to make test script executable"); - } - FileWriter fileWriter = new FileWriter(testScript.getAbsoluteFile(), true); - BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); - var dockerComposeFile = FileUtil.globFilesEndsWith(srcGenPath, "docker-compose.yml").get(0); - bufferedWriter.write(getDockerRunScript(dockerFiles, dockerComposeFile)); - bufferedWriter.close(); - return List.of(new ProcessBuilder(testScript.getAbsolutePath())); - } catch (IOException e) { - return List.of(new ProcessBuilder("exit", "1")); - } + var dockerComposeFile = FileUtil.globFilesEndsWith(srcGenPath, "docker-compose.yml").get(0); + return new ProcessBuilder(getDockerRunScript().toString(), dockerComposeFile.toString()); } /** - * Return a list of preconfigured ProcessBuilder(s) for the command(s) - * that should be used to execute the test program. + * Return a preconfigured ProcessBuilder for executing the test program. * @param test The test to get the execution command for. */ - private List getExecCommand(LFTest test, GeneratorResult generatorResult) throws TestError { + private ProcessBuilder getExecCommand(LFTest test) throws TestError { + var srcBasePath = test.getFileConfig().srcPkgPath.resolve("src"); var relativePathName = srcBasePath.relativize(test.getFileConfig().srcPath).toString(); // special case to test docker file generation - if (relativePathName.equalsIgnoreCase(TestCategory.DOCKER.getPath())) { - return getNonfederatedDockerExecCommand(test); - } else if (relativePathName.equalsIgnoreCase(TestCategory.DOCKER_FEDERATED.getPath())) { - return getFederatedDockerExecCommand(test); + if (relativePathName.equalsIgnoreCase(TestCategory.DOCKER.getPath()) || + relativePathName.equalsIgnoreCase(TestCategory.DOCKER_FEDERATED.getPath())) { + return getDockerExecCommand(test); } else { - LFCommand command = generatorResult.getCommand(); + LFCommand command = test.getFileConfig().getCommand(); if (command == null) { - throw new TestError("File: " + generatorResult.getExecutable(), Result.NO_EXEC_FAIL); + throw new TestError("File: " + test.getFileConfig().getExecutable(), Result.NO_EXEC_FAIL); } - return command == null ? List.of() : List.of( - new ProcessBuilder(command.command()).directory(command.directory()) - ); + return new ProcessBuilder(command.command()).directory(command.directory()); } } @@ -691,14 +667,13 @@ private void validateAndRun(Set tests, Configurator configurator, TestLe for (var test : tests) { try { redirectOutputs(test); - var context = configure(test, configurator, level); - validate(test, context); - GeneratorResult result = GeneratorResult.NOTHING; + configure(test, configurator, level); + validate(test); if (level.compareTo(TestLevel.CODE_GEN) >= 0) { - result = generateCode(test); + generateCode(test); } if (level == TestLevel.EXECUTION) { - execute(test, result); + execute(test); } test.markPassed(); } catch (TestError e) { diff --git a/org.lflang.tests/src/org/lflang/tests/TestRegistry.java b/org.lflang.tests/src/org/lflang/tests/TestRegistry.java index cf40ccc9f8..534f3150cc 100644 --- a/org.lflang.tests/src/org/lflang/tests/TestRegistry.java +++ b/org.lflang.tests/src/org/lflang/tests/TestRegistry.java @@ -78,7 +78,7 @@ public Set getTests(Target t, TestCategory c) { * test file that has a directory in its path that matches an entry in this * array will not be discovered. */ - public static final String[] IGNORED_DIRECTORIES = {"failing", "knownfailed", "failed"}; + public static final String[] IGNORED_DIRECTORIES = {"failing", "knownfailed", "failed", "fed-gen"}; /** * Path to the root of the repository. @@ -293,17 +293,16 @@ public static String getCoverageReport(Target target, TestCategory category) { int missing = all.size() - own.size(); if (missing > 0) { all.stream().filter(test -> !own.contains(test)) - .forEach(test -> s.append("Missing: ").append(test.toString()).append("\n")); + .forEach(test -> s.append("Missing: ").append(test).append("\n")); } } else { s.append("\n").append(TestBase.THIN_LINE); s.append("Covered: ").append(own.size()).append("/").append(own.size()).append("\n"); s.append(TestBase.THIN_LINE); } - return s.toString(); - } - + } + /** * FileVisitor implementation that maintains a stack to map found tests to * the appropriate category and excludes directories that are listed as diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LetInferenceTests.java b/org.lflang.tests/src/org/lflang/tests/compiler/LetInferenceTests.java index 7d86357ac3..cae338ca60 100644 --- a/org.lflang.tests/src/org/lflang/tests/compiler/LetInferenceTests.java +++ b/org.lflang.tests/src/org/lflang/tests/compiler/LetInferenceTests.java @@ -25,7 +25,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ***************/ -import java.nio.file.Path; +import static org.lflang.ASTUtils.toDefinition; + import javax.inject.Inject; import org.eclipse.emf.common.util.TreeIterator; @@ -39,21 +40,19 @@ import org.lflang.ASTUtils; import org.lflang.DefaultErrorReporter; -import org.lflang.FileConfig; import org.lflang.TimeUnit; import org.lflang.TimeValue; import org.lflang.ast.AfterDelayTransformation; import org.lflang.generator.ReactionInstance; import org.lflang.generator.ReactorInstance; import org.lflang.generator.c.CDelayBodyGenerator; -import org.lflang.generator.c.CGenerator; import org.lflang.generator.c.CTypes; import org.lflang.lf.Instantiation; import org.lflang.lf.LfFactory; + import org.lflang.lf.Model; import org.lflang.lf.Reactor; import org.lflang.tests.LFInjectorProvider; -import static org.lflang.ASTUtils.*; @ExtendWith(InjectionExtension.class) @InjectWith(LFInjectorProvider.class) diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaParsingTest.java b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaParsingTest.java index 083220c528..fc66c7ed57 100644 --- a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaParsingTest.java +++ b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaParsingTest.java @@ -24,18 +24,18 @@ ***************/ package org.lflang.tests.compiler; -import com.google.inject.Inject; import org.eclipse.xtext.testing.InjectWith; import org.eclipse.xtext.testing.extensions.InjectionExtension; import org.eclipse.xtext.testing.util.ParseHelper; - -import org.lflang.lf.LfPackage; -import org.lflang.lf.Model; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + +import org.lflang.lf.Model; import org.lflang.tests.LFInjectorProvider; +import com.google.inject.Inject; + /** * Test harness for ensuring that grammar captures * all corner cases. diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java index ca330cf766..4c0d5dee08 100644 --- a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java +++ b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java @@ -45,6 +45,7 @@ import org.lflang.TargetProperty.DictionaryElement; import org.lflang.TargetProperty.DictionaryType; import org.lflang.TargetProperty.PrimitiveType; +import org.lflang.TargetProperty.StringDictionaryType; import org.lflang.TargetProperty.TargetPropertyType; import org.lflang.TargetProperty.UnionType; import org.lflang.TimeValue; @@ -1629,6 +1630,25 @@ private List synthesizeExamples(DictionaryType type, boolean correct) { } return examples; } + + private List synthesizeExamples(StringDictionaryType type, boolean correct) { + List examples = new LinkedList<>(); + // Produce a set of singleton dictionaries. + // If incorrect examples are wanted, use non-strings for values. + List goodStrs = synthesizeExamples(PrimitiveType.STRING, true); + List badStrs = synthesizeExamples(PrimitiveType.STRING, false); + List goodIDs = List.of("foo", "Bar", "__ab0_9fC", "f1o_O2B_a3r"); + if (correct) { + for (String gs : goodStrs) { + goodIDs.forEach(it -> examples.add("{" + it + ": " + gs + "}")); + } + } else { + for (String bs : badStrs) { + goodIDs.forEach(it -> examples.add("{" + it + ": " + bs + "}")); + } + } + return examples; + } /** * Synthesize a list of values that either conform to the given type or @@ -1656,6 +1676,8 @@ private List synthesizeExamples(TargetPropertyType type, boolean correct return synthesizeExamples((ArrayType) type, correct); } else if (type instanceof DictionaryType) { return synthesizeExamples((DictionaryType) type, correct); + } else if (type instanceof StringDictionaryType) { + return synthesizeExamples((StringDictionaryType) type, correct); } else { Assertions.fail("Encountered an unknown type: " + type); } @@ -1729,9 +1751,15 @@ public void checkTargetProperties() throws Exception { List knownIncorrect = synthesizeExamples(prop.type, false); if (!(knownIncorrect == null || knownIncorrect.isEmpty())) { for (String it : knownIncorrect) { - validator.assertError(createModel(prop, it), - LfPackage.eINSTANCE.getKeyValuePair(), null, - String.format("Target property '%s' is required to be %s.", prop.toString(), prop.type)); + if (prop.type instanceof StringDictionaryType) { + validator.assertError(createModel(prop, it), + LfPackage.eINSTANCE.getKeyValuePair(), null, + String.format("Target property '%s.", prop), "' is required to be a string."); + } else { + validator.assertError(createModel(prop, it), + LfPackage.eINSTANCE.getKeyValuePair(), null, + String.format("Target property '%s' is required to be %s.", prop.toString(), prop.type)); + } } } else { // No type was synthesized. It must be a composite type. diff --git a/org.lflang.tests/src/org/lflang/tests/lsp/ErrorInserter.java b/org.lflang.tests/src/org/lflang/tests/lsp/ErrorInserter.java index 82d22d28ce..a7b1c9d8ec 100644 --- a/org.lflang.tests/src/org/lflang/tests/lsp/ErrorInserter.java +++ b/org.lflang.tests/src/org/lflang/tests/lsp/ErrorInserter.java @@ -68,7 +68,7 @@ private boolean get() { /** The zero-based indices of the touched lines. */ private final List badLines; /** The original test on which this is based. */ - private final Path path; + private final Path srcFile; /** The content of this test. */ private final LinkedList lines; /** Whether the error inserter is permitted to insert a line before the current line. */ @@ -82,9 +82,9 @@ private boolean get() { */ private AlteredTest(Path originalTest, BiPredicate insertCondition) throws IOException { this.badLines = new ArrayList<>(); - this.path = originalTest; + this.srcFile = originalTest; this.lines = new LinkedList<>(); // Constant-time insertion during iteration is desired. - this.lines.addAll(Files.readAllLines(originalTest)); + this.lines.addAll(Files.readAllLines(srcFile)); this.insertCondition = it -> { it.previous(); String s0 = it.previous(); @@ -95,8 +95,8 @@ private AlteredTest(Path originalTest, BiPredicate insertConditi } /** Return the location where the content of {@code this} lives. */ - public Path getPath() { - return path; + public Path getSrcFile() { + return srcFile; } /** @@ -104,10 +104,11 @@ public Path getPath() { * @throws IOException If an I/O error occurred. */ public void write() throws IOException { - if (!path.toFile().renameTo(swapFile(path).toFile())) { + Path src = srcFile; + if (!src.toFile().renameTo(swapFile(src).toFile())) { throw new IOException("Failed to create a swap file."); } - try (PrintWriter writer = new PrintWriter(path.toFile())) { + try (PrintWriter writer = new PrintWriter(src.toFile())) { lines.forEach(writer::println); } } @@ -117,11 +118,12 @@ public void write() throws IOException { */ @Override public void close() throws IOException { - if (!swapFile(path).toFile().exists()) throw new IllegalStateException("Swap file does not exist."); - if (!path.toFile().delete()) { + Path src = srcFile; + if (!swapFile(src).toFile().exists()) throw new IllegalStateException("Swap file does not exist."); + if (!src.toFile().delete()) { throw new IOException("Failed to delete the file associated with the original test."); } - if (!swapFile(path).toFile().renameTo(path.toFile())) { + if (!swapFile(src).toFile().renameTo(src.toFile())) { throw new IOException("Failed to restore the altered LF file to its original state."); } } @@ -334,7 +336,7 @@ private ErrorInserter( /** * Alter the given test and return the altered version. - * @param test An LF file that can be used as a test. + * @param test The path to the test. * @return An {@code AlteredTest} that is based on {@code test}. */ public AlteredTest alterTest(Path test) throws IOException { diff --git a/org.lflang.tests/src/org/lflang/tests/lsp/LspTests.java b/org.lflang.tests/src/org/lflang/tests/lsp/LspTests.java index a14e16ce3d..99c1260d44 100644 --- a/org.lflang.tests/src/org/lflang/tests/lsp/LspTests.java +++ b/org.lflang.tests/src/org/lflang/tests/lsp/LspTests.java @@ -101,8 +101,8 @@ private void targetLanguageValidationTest(Target target, ErrorInserter.Builder b System.out.println(" but the expected error could not be found."); System.out.printf( "%s failed. Content of altered version of %s:%n%s%n", - alteredTest.getPath().getFileName(), - alteredTest.getPath().getFileName(), + alteredTest.getSrcFile(), + alteredTest.getSrcFile(), TestBase.THIN_LINE ); System.out.println(alteredTest + "\n" + TestBase.THIN_LINE); @@ -138,7 +138,7 @@ private void checkDiagnostics( client.clearDiagnostics(); if (alterer != null) { try (AlteredTest altered = alterer.alterTest(test.getSrcPath())) { - runTest(altered.getPath()); + runTest(altered.getSrcFile()); Assertions.assertTrue(requirementGetter.apply(altered).test(client.getReceivedDiagnostics())); } } else { @@ -193,7 +193,7 @@ private static Predicate> diagnosticsHaveKeyword(String keyword * @param requiredText A keyword that a list of diagnostics should be searched for. * @return The predicate, "X includes {@code requiredText}." */ - private static Predicate> diagnosticsIncludeText(String requiredText) { + private static Predicate> diagnosticsIncludeText(@SuppressWarnings("SameParameterValue") String requiredText) { return diagnostics -> diagnostics.stream().anyMatch( d -> d.getMessage().toLowerCase().contains(requiredText) ); @@ -201,15 +201,20 @@ private static Predicate> diagnosticsIncludeText(String require /** * Run the given test. - * @param test An integration test. + * @param test The test b */ private void runTest(Path test) { MockReportProgress reportProgress = new MockReportProgress(); - builder.run( - URI.createFileURI(test.toString()), - false, reportProgress, - () -> false - ); + try { + builder.run( + URI.createFileURI(test.toString()), + false, reportProgress, + () -> false + ); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } Assertions.assertFalse(reportProgress.failed()); } } diff --git a/org.lflang.tests/src/org/lflang/tests/lsp/MockReportProgress.java b/org.lflang.tests/src/org/lflang/tests/lsp/MockReportProgress.java index c90c4ee0fe..901f0e6154 100644 --- a/org.lflang.tests/src/org/lflang/tests/lsp/MockReportProgress.java +++ b/org.lflang.tests/src/org/lflang/tests/lsp/MockReportProgress.java @@ -17,7 +17,7 @@ public MockReportProgress() { @Override public void apply(String message, Integer percentage) { - System.out.println(message); + System.out.printf("%s [%d -> %d]%n", message, previousPercentProgress, percentage); if (percentage == null) return; if (percentage < previousPercentProgress || percentage < 0 || percentage > 100) failed = true; previousPercentProgress = percentage; diff --git a/org.lflang.tests/src/org/lflang/tests/runtime/PythonTest.java b/org.lflang.tests/src/org/lflang/tests/runtime/PythonTest.java index 36915f87f0..e028e6ac4d 100644 --- a/org.lflang.tests/src/org/lflang/tests/runtime/PythonTest.java +++ b/org.lflang.tests/src/org/lflang/tests/runtime/PythonTest.java @@ -70,7 +70,7 @@ protected boolean supportsSingleThreadedExecution() { @Override protected boolean supportsDockerOption() { - return true; + return false; // FIXME: https://issues.lf-lang.org/1564 } @Test diff --git a/org.lflang/src/lib/c/reactor-c b/org.lflang/src/lib/c/reactor-c index d42631aa69..060c5e1e18 160000 --- a/org.lflang/src/lib/c/reactor-c +++ b/org.lflang/src/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit d42631aa6902e98f0808a55bf6cef72b50bb7619 +Subproject commit 060c5e1e1868199f8657bdf869c7f4fcd31fc7eb diff --git a/org.lflang/src/lib/ts/.eslintrc.json b/org.lflang/src/lib/ts/.eslintrc.json index 5cf28e0201..8ee61417cd 100644 --- a/org.lflang/src/lib/ts/.eslintrc.json +++ b/org.lflang/src/lib/ts/.eslintrc.json @@ -10,6 +10,7 @@ ], "rules": { "prefer-const": "warn", + "ban-types": "warn", "@typescript-eslint/no-inferrable-types": "warn" } } diff --git a/org.lflang/src/lib/ts/package.json b/org.lflang/src/lib/ts/package.json index 8f7cb491ca..e230c56017 100644 --- a/org.lflang/src/lib/ts/package.json +++ b/org.lflang/src/lib/ts/package.json @@ -2,7 +2,7 @@ "name": "LinguaFrancaDefault", "type": "commonjs", "dependencies": { - "@lf-lang/reactor-ts": "^0.1.0", + "@lf-lang/reactor-ts": "git://github.com/lf-lang/reactor-ts.git#fed-gen", "@babel/cli": "^7.8.4", "@babel/core": "^7.8.7", "@babel/node": "^7.8.7", diff --git a/org.lflang/src/org/lflang/ASTUtils.java b/org.lflang/src/org/lflang/ASTUtils.java index 959738b85a..fe6fdfeb2b 100644 --- a/org.lflang/src/org/lflang/ASTUtils.java +++ b/org.lflang/src/org/lflang/ASTUtils.java @@ -11,15 +11,15 @@ this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -59,6 +60,8 @@ import org.lflang.generator.InvalidSourceException; import org.lflang.lf.Action; import org.lflang.lf.Assignment; +import org.lflang.lf.AttrParm; +import org.lflang.lf.Attribute; import org.lflang.lf.Code; import org.lflang.lf.Connection; import org.lflang.lf.Element; @@ -102,12 +105,12 @@ * @author Christian Menard */ public class ASTUtils { - + /** * The Lingua Franca factory for creating new AST nodes. */ public static final LfFactory factory = LfFactory.eINSTANCE; - + /** * The Lingua Franca feature package. */ @@ -115,7 +118,7 @@ public class ASTUtils { /* Match an abbreviated form of a float literal. */ private static final Pattern ABBREVIATED_FLOAT = Pattern.compile("[+\\-]?\\.\\d+[\\deE+\\-]*"); - + /** * A mapping from Reactor features to corresponding Mode features for collecting contained elements. */ @@ -144,7 +147,7 @@ public static List getAllReactors(Resource resource) { /** * Find connections in the given resource that would be conflicting writes if they were not located in mutually * exclusive modes. - * + * * @param resource The AST. * @return a list of connections being able to be transformed */ @@ -154,7 +157,7 @@ public static Collection findConflictingConnectionsInModalReactors(R for (Reactor reactor : getAllReactors(resource)) { if (!reactor.getModes().isEmpty()) { // Only for modal reactors var allWriters = HashMultimap., EObject>create(); - + // Collect destinations for (var rea : allReactions(reactor)) { for (var eff : rea.getEffects()) { @@ -168,7 +171,7 @@ public static Collection findConflictingConnectionsInModalReactors(R allWriters.put(Tuples.pair(port.getContainer(), port.getVariable()), con); } } - + // Handle conflicting writers for (var key : allWriters.keySet()) { var writers = allWriters.get(key); @@ -195,12 +198,12 @@ public static Collection findConflictingConnectionsInModalReactors(R } } } - + return transform; } - + /** - * Return the enclosing reactor of an LF EObject in a reactor or mode. + * Return the enclosing reactor of an LF EObject in a reactor or mode. * @param obj the LF model element * @return the reactor or null */ @@ -212,7 +215,17 @@ public static Reactor getEnclosingReactor(EObject obj) { } return null; } - + + /** + * Return the main reactor in the given resource if there is one, null otherwise. + */ + public static Reactor findMainReactor(Resource resource) { + return IteratorExtensions.findFirst( + Iterators.filter(resource.getAllContents(), Reactor.class), + Reactor::isMain + ); + } + /** * Find the main reactor and change it to a federated reactor. * Return true if the transformation was successful (or the given resource @@ -220,10 +233,7 @@ public static Reactor getEnclosingReactor(EObject obj) { */ public static boolean makeFederated(Resource resource) { // Find the main reactor - Reactor r = IteratorExtensions.findFirst( - Iterators.filter(resource.getAllContents(), Reactor.class), - Reactor::isMain - ); + Reactor r = findMainReactor(resource); if (r == null) { return false; } @@ -231,7 +241,7 @@ public static boolean makeFederated(Resource resource) { r.setFederated(true); return true; } - + /** * Change the target name to 'newTargetName'. * For example, change C to CCpp. @@ -262,7 +272,7 @@ public static boolean addTargetProperty(final Resource resource, final String na config.getPairs().add(newProperty); return true; } - + /** * Return true if the connection involves multiple ports on the left or right side of the connection, or * if the port on the left or right of the connection involves a bank of reactors or a multiport. @@ -303,7 +313,7 @@ public static String getUniqueIdentifier(Reactor reactor, String name) { int index = 0; String suffix = ""; - boolean exists = true; + boolean exists = true; while (exists) { String id = name + suffix; if (IterableExtensions.exists(vars, it -> it.equals(id))) { @@ -315,10 +325,10 @@ public static String getUniqueIdentifier(Reactor reactor, String name) { } return name + suffix; } - + //////////////////////////////// //// Utility functions for supporting inheritance and modes - + /** * Given a reactor class, return a list of all its actions, * which includes actions of base classes that it extends. @@ -329,7 +339,7 @@ public static String getUniqueIdentifier(Reactor reactor, String name) { public static List allActions(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_Actions()); } - + /** * Given a reactor class, return a list of all its connections, * which includes connections of base classes that it extends. @@ -340,7 +350,7 @@ public static List allActions(Reactor definition) { public static List allConnections(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_Connections()); } - + /** * Given a reactor class, return a list of all its inputs, * which includes inputs of base classes that it extends. @@ -352,7 +362,7 @@ public static List allConnections(Reactor definition) { public static List allInputs(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_Inputs()); } - + /** * Given a reactor class, return a list of all its instantiations, * which includes instantiations of base classes that it extends. @@ -363,7 +373,7 @@ public static List allInputs(Reactor definition) { public static List allInstantiations(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_Instantiations()); } - + /** * Given a reactor class, return a list of all its methods, * which includes methods of base classes that it extends. @@ -390,7 +400,7 @@ public static List allOutputs(Reactor definition) { public static List allParameters(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_Parameters()); } - + /** * Given a reactor class, return a list of all its reactions, * which includes reactions of base classes that it extends. @@ -401,7 +411,7 @@ public static List allParameters(Reactor definition) { public static List allReactions(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_Reactions()); } - + /** * Given a reactor class, return a list of all its state variables, * which includes state variables of base classes that it extends. @@ -412,7 +422,7 @@ public static List allReactions(Reactor definition) { public static List allStateVars(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_StateVars()); } - + /** * Given a reactor class, return a list of all its timers, * which includes timers of base classes that it extends. @@ -432,7 +442,7 @@ public static List allTimers(Reactor definition) { public static List allModes(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_Modes()); } - + /** * Return all the superclasses of the specified reactor * in deepest-first order. For example, if A extends B and C, and @@ -457,7 +467,7 @@ public static LinkedHashSet superClasses(Reactor reactor) { public static List collectElements(Reactor definition, EStructuralFeature feature) { return ASTUtils.collectElements(definition, feature, true, true); } - + /** * Collect elements of type T contained in given reactor definition, including * modes and the class hierarchy defined depending on configuration. @@ -471,7 +481,7 @@ public static List collectElements(Reactor definition, ES @SuppressWarnings("unchecked") public static List collectElements(Reactor definition, EStructuralFeature feature, boolean includeSuperClasses, boolean includeModes) { List result = new ArrayList<>(); - + if (includeSuperClasses) { // Add elements of elements defined in superclasses. LinkedHashSet s = superClasses(definition); @@ -481,10 +491,10 @@ public static List collectElements(Reactor definition, ES } } } - + // Add elements of the current reactor. result.addAll((EList) definition.eGet(feature)); - + if (includeModes && reactorModeFeatureMap.containsKey(feature)) { var modeFeature = reactorModeFeatureMap.get(feature); // Add elements of elements defined in modes. @@ -492,16 +502,16 @@ public static List collectElements(Reactor definition, ES insertModeElementsAtTextualPosition(result, (EList) mode.eGet(modeFeature), mode); } } - + return result; } - + /** * Adds the elements into the given list at a location matching to their textual position. - * + * * When creating a flat view onto reactor elements including modes, the final list must be ordered according * to the textual positions. - * + * * Example: * reactor R { * reaction // -> is R.reactions[0] @@ -511,9 +521,9 @@ public static List collectElements(Reactor definition, ES * } * reaction // -> is R.reactions[1] * } - * In this example, it is important that the reactions in the mode are inserted between the top-level + * In this example, it is important that the reactions in the mode are inserted between the top-level * reactions to retain the correct global reaction ordering, which will be derived from this flattened view. - * + * * @param list The list to add the elements into. * @param elements The elements to add. * @param mode The mode containing the elements. @@ -523,7 +533,7 @@ private static void insertModeElementsAtTextualPosition(List if (elements.isEmpty()) { return; // Nothing to add } - + var idx = list.size(); if (idx > 0) { // If there are elements in the list, first check if the last element has the same container as the mode. @@ -585,28 +595,28 @@ public static String toOriginalText(EObject node) { if (node == null) return ""; return ToText.instance.doSwitch(node); } - + /** * Return an integer representation of the given element. - * + * * Internally, this method uses Integer.decode, so it will * also understand hexadecimal, binary, etc. - * + * * @param e The element to be rendered as an integer. */ public static Integer toInteger(Element e) { return Integer.decode(e.getLiteral()); } - + /** * Return a time value based on the given element. - * + * * @param e The element to be rendered as a time value. */ public static TimeValue toTimeValue(Element e) { return new TimeValue(e.getTime(), TimeUnit.fromName(e.getUnit())); } - + /** * Returns the time value represented by the given AST node. */ @@ -620,7 +630,7 @@ public static TimeValue toTimeValue(Time e) { /** * Return a boolean based on the given element. - * + * * @param e The element to be rendered as a boolean. */ public static boolean toBoolean(Element e) { @@ -648,7 +658,7 @@ public static String elementToSingleString(Element e) { /** * Given the right-hand side of a target property, return a list with all * the strings that the property lists. - * + * * Arrays are traversed, so strings are collected recursively. Empty strings * are ignored; they are not added to the list. * @param value The right-hand side of a target property. @@ -668,7 +678,107 @@ public static List elementToListOfStrings(Element value) { } return elements; } - + + /** + * Convert key-value pairs in an Element to a map, assuming that both the key + * and the value are strings. + */ + public static Map elementToStringMaps(Element value) { + Map elements = new HashMap<>(); + for (var element: value.getKeyvalue().getPairs()) { + elements.put( + element.getName().trim(), + StringUtil.removeQuotes(elementToSingleString(element.getValue())) + ); + } + return elements; + } + + // Various utility methods to convert various data types to Elements + + /** + * Convert a map to key-value pairs in an Element. + */ + public static Element toElement(Map map) { + Element e = LfFactory.eINSTANCE.createElement(); + if (map.size() == 0) return null; + else { + var kv = LfFactory.eINSTANCE.createKeyValuePairs(); + for (var entry : map.entrySet()) { + var pair = LfFactory.eINSTANCE.createKeyValuePair(); + pair.setName(entry.getKey()); + var element = LfFactory.eINSTANCE.createElement(); + element.setLiteral(StringUtil.addDoubleQuotes(entry.getValue())); + pair.setValue(element); + kv.getPairs().add(pair); + } + e.setKeyvalue(kv); + } + + return e; + } + + /** + * Given a single string, convert it into its AST representation. + * {@code addQuotes} controls if the generated representation should be + * accompanied by double quotes ("") or not. + */ + private static Element toElement(String str, boolean addQuotes) { + if (str == null) return null; + var strToReturn = addQuotes? StringUtil.addDoubleQuotes(str):str; + Element e = LfFactory.eINSTANCE.createElement(); + e.setLiteral(strToReturn); + return e; + + } + + /** + * Given a single string, convert it into its AST representation. + */ + public static Element toElement(String str) { + return toElement(str, true); + } + + /** + * Given a list of strings, convert it into its AST representation. + * Stores the list in the Array field of the element, unless the list only has one string, + * in which case it is stored in the Literal field. Returns null if the provided list is empty. + */ + public static Element toElement(List list) { + Element e = LfFactory.eINSTANCE.createElement(); + if (list.size() == 0) return null; + else if (list.size() == 1) { + return toElement(list.get(0)); + } else { + var arr = LfFactory.eINSTANCE.createArray(); + for (String s : list) { + arr.getElements().add(ASTUtils.toElement(s)); + } + e.setArray(arr); + } + return e; + } + + /** + * Convert a TimeValue to its AST representation. The value is type-cast to int in order to fit inside an Element. + */ + public static Element toElement(TimeValue tv) { + Element e = LfFactory.eINSTANCE.createElement(); + e.setTime((int)tv.time); + if (tv.unit != null) { + e.setUnit(tv.unit.toString()); + } + return e; + } + + public static Element toElement(boolean val) { + return toElement(Boolean.toString(val), false); + } + + public static Element toElement(int val) { + return toElement(Integer.toString(val), false); + } + /** * Translate the given type into its textual representation, but * do not append any array specifications. @@ -700,7 +810,7 @@ public static String baseType(Type type) { } return ""; } - + /** * Report whether the given literal is zero or not. * @param literal AST node to inspect. @@ -780,7 +890,7 @@ public static boolean isFloat(String literal) { public static boolean isInteger(Code code) { return isInteger(toText(code)); } - + /** * Report whether the given expression is an integer number or not. * @param expr AST node to inspect. @@ -794,7 +904,7 @@ public static boolean isInteger(Expression expr) { } return false; } - + /** * Report whether the given expression denotes a valid time or not. * @param expr AST node to inspect. @@ -823,7 +933,6 @@ public static boolean isValidTime(Time t) { TimeUnit.isValidUnit(unit); } - /** * If the initializer contains exactly one expression, * return it. Otherwise, return null. @@ -950,7 +1059,7 @@ public static InferredType getInferredType(Port p) { return getInferredType(p.getType(), null); } - + /** * If the given string can be recognized as a floating-point number that has a leading decimal point, @@ -1043,13 +1152,13 @@ public static boolean isOfTimeType(Parameter param) { /** * Given a parameter, return its initial value. * The initial value is a list of instances of Expressions. - * + * * If the instantiations argument is null or an empty list, then the * value returned is simply the default value given when the parameter * is defined. - * + * * If a list of instantiations is given, then the first instantiation - * is required to be an instantiation of the reactor class that is + * is required to be an instantiation of the reactor class that is * parameterized by the parameter. I.e., * ``` * parameter.eContainer == instantiations.get(0).reactorClass @@ -1065,11 +1174,11 @@ public static boolean isOfTimeType(Parameter param) { * ``` * If any of these conditions is not satisfied, then an IllegalArgumentException * will be thrown. - * + * * Note that this chain of reactions cannot be inferred from the parameter because * in each of the predicates above, there may be more than one instantiation that * can appear on the right hand side of the predicate. - * + * * For example, consider the following program: * ``` * reactor A(x:int(1)) {} @@ -1103,12 +1212,12 @@ public static boolean isOfTimeType(Parameter param) { * initialValue(y, [b1]) returns 3 * initialValue(y, [b2]) returns -2 * ``` - * + * * @param parameter The parameter. * @param instantiations The (optional) list of instantiations. - * + * * @return The value of the parameter. - * + * * @throws IllegalArgumentException If an instantiation provided is not an * instantiation of the reactor class that is parameterized by the * respective parameter or if the chain of instantiations is not nested. @@ -1166,7 +1275,7 @@ public static List initialValue(Parameter parameter, List i } return result; } - + /** * Given the width specification of port or instantiation * and an (optional) list of nested instantiations, return @@ -1239,8 +1348,8 @@ public static Integer initialValueInt(Parameter parameter, List i * or the list of instantiations is incomplete or missing. * If there are parameter references in the width, they are * evaluated to the extent possible given the instantiations list. - * - * The instantiations list is as in + * + * The instantiations list is as in * {@link #initialValue(Parameter, List)}. * If the spec belongs to an instantiation (for a bank of reactors), * then the first element on this list should be the instantiation @@ -1250,7 +1359,7 @@ public static Integer initialValueInt(Parameter parameter, List i * * @param spec The width specification or null (to return 1). * @param instantiations The (optional) list of instantiations. - * + * * @return The width, or -1 if the width could not be determined. * * @throws IllegalArgumentException If an instantiation provided is not as @@ -1296,12 +1405,12 @@ public static int width(WidthSpec spec, List instantiations) { * which is an Instantiation that may refer to a bank of reactors. * The width will be the product of the bank width and the port width. * The returned value will be 1 if the port is not in a bank and is not a multiport. - * + * * If the width cannot be determined, this will return -1. * The width cannot be determined if the list of instantiations is * missing or incomplete. - * - * The instantiations list is as in + * + * The instantiations list is as in * {@link #initialValue(Parameter, List)}. * The first element on this list should be the instantiation * that contains the specified connection. @@ -1309,7 +1418,7 @@ public static int width(WidthSpec spec, List instantiations) { * @param reference A port reference. * @param connection A connection, or null if not in the context of a connection. * @param instantiations The (optional) list of instantiations. - * + * * @return The width or -1 if it could not be determined. * * @throws IllegalArgumentException If an instantiation provided is not as @@ -1331,11 +1440,11 @@ public static int inferPortWidth( } int portWidth = width(((Port) reference.getVariable()).getWidthSpec(), extended); - if (portWidth < 0) { + if (portWidth < 0) { // Could not determine port width. - return -1; + return -1; } - + // Next determine the bank width. This may be unspecified, in which // case it has to be inferred using the connection. int bankWidth = 1; @@ -1392,12 +1501,12 @@ public static int inferPortWidth( // Check that portWidth divides the discrepancy. if (discrepancy % portWidth != 0) { // This is an error. - return -1; + return -1; } bankWidth = discrepancy / portWidth; } else { // Could not determine the bank width. - return -1; + return -1; } } } @@ -1406,7 +1515,7 @@ public static int inferPortWidth( // Argument is not a port. return -1; } - + /** * Given an instantiation of a reactor or bank of reactors, return * the width. This will be 1 if this is not a reactor bank. Otherwise, @@ -1422,7 +1531,7 @@ public static int inferPortWidth( * @see #width(WidthSpec, List) * * @param instantiation A reactor instantiation. - * + * * @return The width, if it can be determined. * @deprecated */ @@ -1446,20 +1555,20 @@ public static boolean isInitialized(StateVar v) { } /** - * Report whether the given time state variable is initialized using a + * Report whether the given time state variable is initialized using a * parameter or not. * @param s A state variable. - * @return True if the argument is initialized using a parameter, false + * @return True if the argument is initialized using a parameter, false * otherwise. */ public static boolean isParameterized(StateVar s) { - return s.getInit() != null && + return s.getInit() != null && IterableExtensions.exists(s.getInit().getExprs(), it -> it instanceof ParameterReference); } /** * Check if the reactor class uses generics - * @param r the reactor to check + * @param r the reactor to check * @return true if the reactor uses generics */ public static boolean isGeneric(Reactor r) { @@ -1468,7 +1577,7 @@ public static boolean isGeneric(Reactor r) { } return r.getTypeParms().size() != 0; } - + /** * If the specified reactor declaration is an import, then * return the imported reactor class definition. Otherwise, @@ -1547,14 +1656,14 @@ public static Predicate sameLine(ICompositeNode compNode) { * @param resource The resource to find the main reactor in. */ public static void setMainName(Resource resource, String name) { - Reactor main = IteratorExtensions.findFirst(Iterators.filter(resource.getAllContents(), Reactor.class), + Reactor main = IteratorExtensions.findFirst(Iterators.filter(resource.getAllContents(), Reactor.class), it -> it.isMain() || it.isFederated() ); if (main != null && StringExtensions.isNullOrEmpty(main.getName())) { main.setName(name); } } - + /** * Create a new instantiation node with the given reactor as its defining class. * @param reactor The reactor class to create an instantiation of. @@ -1573,7 +1682,7 @@ public static Instantiation createInstantiation(Reactor reactor) { } else { inst.setName(""); } - + } else { inst.setName(reactor.getName()); } @@ -1595,10 +1704,10 @@ public static TargetDecl targetDecl(Model model) { public static TargetDecl targetDecl(Resource model) { return IteratorExtensions.head(Iterators.filter(model.getAllContents(), TargetDecl.class)); } - + ///////////////////////////////////////////////////////// //// Private methods - + /** * Returns the list if it is not null. Otherwise, return an empty list. */ @@ -1682,4 +1791,10 @@ private static int inferWidthFromConnections(WidthSpec spec, List // A connection was not found with the instantiation. return -1; } + + public static void addReactionAttribute(Reaction reaction, String name) { + var fedAttr = factory.createAttribute(); + fedAttr.setAttrName(name); + reaction.getAttributes().add(fedAttr); + } } diff --git a/org.lflang/src/org/lflang/AttributeUtils.java b/org.lflang/src/org/lflang/AttributeUtils.java index 62847bf36b..ec5edc4b9d 100644 --- a/org.lflang/src/org/lflang/AttributeUtils.java +++ b/org.lflang/src/org/lflang/AttributeUtils.java @@ -11,15 +11,15 @@ this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ @@ -34,6 +34,7 @@ import org.eclipse.xtext.resource.XtextResource; import org.lflang.lf.Action; +import org.lflang.lf.AttrParm; import org.lflang.lf.Attribute; import org.lflang.lf.Input; import org.lflang.lf.Instantiation; @@ -47,7 +48,7 @@ /** * A helper class for processing attributes in the AST. - * + * * @author Shaokai Lin * @author Clément Fournier * @author Alexander Schulz-Rosengarten @@ -157,9 +158,10 @@ public static String findAnnotationInComments(EObject object, String key) { * Returns null if no such parameter is found. */ public static String getAttributeParameter(Attribute attribute, String parameterName) { - return attribute.getAttrParms().stream() + return (attribute == null) ? null : attribute.getAttrParms().stream() .filter(param -> Objects.equals(param.getName(), parameterName)) - .map(param -> param.getValue()) + .map(AttrParm::getValue) + .map(StringUtil::removeQuotes) .findFirst() .orElse(null); } @@ -189,6 +191,33 @@ public static boolean isSparse(EObject node) { return findAttributeByName(node, "sparse") != null; } + /** + * Return true if the reaction is unordered. + * + * Currently, this is only used for synthesized reactions in the context of + * federated execution. + */ + public static boolean isUnordered(Reaction reaction) { + return findAttributeByName(reaction, "_unordered") != null; + } + + /** + * Return true if the reactor is marked to be a federate. + */ + public static boolean isFederate(Reactor reactor) { + return findAttributeByName(reactor, "_fed_config") != null; + } + + /** + * Return true if the reaction is marked to have a C code body. + * + * Currently, this is only used for synthesized reactions in the context of + * federated execution in Python. + */ + public static boolean hasCBody(Reaction reaction) { + return findAttributeByName(reaction, "_c_body") != null; + } + /** * Return the declared label of the node, as given by the @label annotation. */ diff --git a/org.lflang/src/org/lflang/FileConfig.java b/org.lflang/src/org/lflang/FileConfig.java index ec70156b28..2b0b541a5a 100644 --- a/org.lflang/src/org/lflang/FileConfig.java +++ b/org.lflang/src/org/lflang/FileConfig.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import java.util.function.Consumer; import org.eclipse.core.resources.IProject; @@ -13,7 +14,11 @@ import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.generator.IFileSystemAccess2; + +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.generator.GeneratorUtils; import org.lflang.util.FileUtil; +import org.lflang.util.LFCommand; /** * Base class that governs the interactions between code generators and the file system. @@ -21,7 +26,7 @@ * @author Marten Lohstroh * */ -public class FileConfig { +public abstract class FileConfig { // Public static fields. @@ -72,7 +77,7 @@ public class FileConfig { * IFile representing the Lingua Franca program. * This is the XText view of the file, which is distinct * from the Eclipse eCore view of the file and the OS view of the file. - * + *

* This is null if running outside an Eclipse IDE. */ public final IResource iResource; @@ -86,7 +91,7 @@ public class FileConfig { /** * The directory in which the source .lf file was found. */ - public final Path srcPath; + public final Path srcPath; // FIXME: rename this to srcDir? /** * Indicate whether the bin directory should be hierarchical. @@ -110,14 +115,13 @@ public class FileConfig { */ protected Path srcGenPath; - // private fields /** * The parent of the directory designated for placing generated sources into (`./src-gen` by default). Additional * directories (such as `bin` or `build`) should be created as siblings of the directory for generated sources, * which means that such directories should be created relative to the path assigned to this class variable. - * + *

* The generated source directory is specified in the IDE (Project Properties->LF->Compiler->Output Folder). When * invoking the standalone compiler, the output path is specified directly using the `-o` or `--output-path` option. */ @@ -164,7 +168,7 @@ public Path getDirectory(Resource r) throws IOException { * The parent of the directory designated for placing generated sources into (`./src-gen` by default). Additional * directories (such as `bin` or `build`) should be created as siblings of the directory for generated sources, * which means that such directories should be created relative to the path assigned to this class variable. - * + *

* The generated source directory is specified in the IDE (Project Properties->LF->Compiler->Output Folder). When * invoking the standalone compiler, the output path is specified directly using the `-o` or `--output-path` option. */ @@ -240,7 +244,7 @@ protected Path getSubPkgPath(Path srcPath) { /** * Clean any artifacts produced by the code generator and target compilers. - * + *

* The base implementation deletes the bin and src-gen directories. If the * target code generator creates additional files or directories, the * corresponding generator should override this method. @@ -290,4 +294,27 @@ public static Path findPackageRoot(final Path input, final Consumer prin return p.getParent(); } + /** + * Return an LFCommand instance that can be used to execute the program under compilation. + */ + public LFCommand getCommand() { + String cmd = GeneratorUtils.isHostWindows() ? + getExecutable().toString() : + srcPkgPath.relativize(getExecutable()).toString(); + return LFCommand.get(cmd, List.of(), true, srcPkgPath); + } + + /** + * Return the extension used for binaries on the platform on which compilation takes place. + */ + protected String getExecutableExtension() { + return (GeneratorUtils.isHostWindows() ? ".exe" : ""); + } + + /** + * Return a path to an executable version of the program under compilation. + */ + public Path getExecutable() { + return binPath.resolve(name + getExecutableExtension()); + } } diff --git a/org.lflang/src/org/lflang/Target.java b/org.lflang/src/org/lflang/Target.java index e529956ea5..4842bc1672 100644 --- a/org.lflang/src/org/lflang/Target.java +++ b/org.lflang/src/org/lflang/Target.java @@ -402,10 +402,10 @@ public enum Target { /** - * Private constructor for targets without pakcageName and classNamePrefix. + * Private constructor for targets without packageName and classNamePrefix. */ Target(String displayName, boolean requiresTypes, Collection keywords) { - this(displayName, requiresTypes, "N/A", "N/A", keywords); + this(displayName, requiresTypes, "N/A", "N/A", keywords); // FIXME: prefix } @@ -486,16 +486,27 @@ public boolean supportsMultiports() { * on constants). */ public boolean supportsParameterizedWidths() { - switch (this) { - case C: - case CCPP: - case CPP: - case Python: - case Rust: - case TS: - return true; - } - return false; + return switch (this) { + case C, CCPP, CPP, Python, Rust, TS -> true; + }; + } + + /** + * Return true if this code for this target should be built using Docker if Docker is used. + * @return + */ + public boolean buildsUsingDocker() { + return switch (this) { + case TS -> false; + case C, CCPP, CPP, Python, Rust -> true; + }; + } + + /** + * Return a string that demarcates the beginning of a single-line comment. + */ + public String getSingleLineCommentPrefix() { + return this.equals(Target.Python) ? "#" : "//"; } /** diff --git a/org.lflang/src/org/lflang/TargetConfig.java b/org.lflang/src/org/lflang/TargetConfig.java index d3566eb5b3..04697ed6e7 100644 --- a/org.lflang/src/org/lflang/TargetConfig.java +++ b/org.lflang/src/org/lflang/TargetConfig.java @@ -29,6 +29,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.lflang.TargetProperty.BuildType; @@ -38,6 +39,7 @@ import org.lflang.TargetProperty.Platform; import org.lflang.TargetProperty.SchedulerOption; import org.lflang.generator.rust.RustTargetConfig; +import org.lflang.lf.TargetDecl; /** * A class for keeping the current target configuration. @@ -48,6 +50,12 @@ */ public class TargetConfig { + public final Target target; + + public TargetConfig(TargetDecl target) { + this.target = Target.fromDecl(target); + } + /** * Keep track of every target property that is explicitly set by the user. */ @@ -268,7 +276,10 @@ public class TargetConfig { public boolean exportToYaml = false; /** Rust-specific configuration. */ - public final RustTargetConfig rust = new RustTargetConfig(); + public final RustTargetConfig rust = new RustTargetConfig(); // FIXME: https://issue.lf-lang.org/1558 + + /** Path to a C file used by the Python target to setup federated execution. */ + public String fedSetupPreamble = null; // FIXME: https://issue.lf-lang.org/1558 /** * Settings related to clock synchronization. @@ -341,6 +352,18 @@ public static class DockerOptions { * The base image and tag from which to build the Docker image. The default is "alpine:latest". */ public String from = "alpine:latest"; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DockerOptions that = (DockerOptions) o; + return from.equals(that.from); + } } /** @@ -420,14 +443,19 @@ public static class PlatformOptions { */ public String board = null; + /** + * The string value used to determine the port on which to flash the compiled program (i.e. /dev/cu.usbmodem21301) + */ + public String port = ""; + /** * The baud rate used as a parameter to certain embedded platforms. 9600 is a standard rate amongst systems like Arduino, so it's the default value. */ public int baudRate = 9600; -/** - * Should LFC invoke external tools to flash the resulting binary onto the target board - */ + /** + * Should LFC invoke external tools to flash the resulting binary onto the target board + */ public boolean flash = false; } @@ -440,5 +468,17 @@ public static class TracingOptions { * This defaults to the name of the .lf file. */ public String traceFileName = null; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TracingOptions that = (TracingOptions) o; + return Objects.equals(traceFileName, that.traceFileName); // traceFileName may be null + } } } diff --git a/org.lflang/src/org/lflang/TargetProperty.java b/org.lflang/src/org/lflang/TargetProperty.java index 3099aed2cc..03536a97de 100644 --- a/org.lflang/src/org/lflang/TargetProperty.java +++ b/org.lflang/src/org/lflang/TargetProperty.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.function.Predicate; @@ -45,6 +46,8 @@ import org.lflang.lf.Element; import org.lflang.lf.KeyValuePair; import org.lflang.lf.KeyValuePairs; +import org.lflang.lf.LfFactory; +import org.lflang.lf.TargetDecl; import org.lflang.util.FileUtil; import org.lflang.util.StringUtil; import org.lflang.validation.LFValidator; @@ -62,15 +65,18 @@ public enum TargetProperty { * Directive to allow including OpenSSL libraries and process HMAC authentication. */ AUTH("auth", PrimitiveType.BOOLEAN, - Arrays.asList(Target.C, Target.CCPP), (config, value, err) -> { + Arrays.asList(Target.C, Target.CCPP), + (config) -> ASTUtils.toElement(config.auth), + (config, value, err) -> { config.auth = ASTUtils.toBoolean(value); }), - /** * Directive to let the generator use the custom build command. */ BUILD("build", UnionType.STRING_OR_STRING_ARRAY, - Arrays.asList(Target.C, Target.CCPP), (config, value, err) -> { + Arrays.asList(Target.C, Target.CCPP), + (config) -> ASTUtils.toElement(config.buildCommands), + (config, value, err) -> { config.buildCommands = ASTUtils.elementToListOfStrings(value); }), @@ -79,7 +85,9 @@ public enum TargetProperty { * This is also used in the Rust target to select a Cargo profile. */ BUILD_TYPE("build-type", UnionType.BUILD_TYPE_UNION, - Arrays.asList(Target.C, Target.CCPP, Target.CPP, Target.Rust), (config, value, err) -> { + Arrays.asList(Target.C, Target.CCPP, Target.CPP, Target.Rust), + (config) -> ASTUtils.toElement(config.cmakeBuildType.toString()), + (config, value, err) -> { config.cmakeBuildType = (BuildType) UnionType.BUILD_TYPE_UNION .forName(ASTUtils.elementToSingleString(value)); // set it there too, because the default is different. @@ -90,16 +98,51 @@ public enum TargetProperty { * Directive to let the federate execution handle clock synchronization in software. */ CLOCK_SYNC("clock-sync", UnionType.CLOCK_SYNC_UNION, - Arrays.asList(Target.C, Target.CCPP, Target.Python), (config, value, err) -> { - config.clockSync = (ClockSyncMode) UnionType.CLOCK_SYNC_UNION - .forName(ASTUtils.elementToSingleString(value)); - }), - + Arrays.asList(Target.C, Target.CCPP, Target.Python), + (config) -> ASTUtils.toElement(config.clockSync.toString()), + (config, value, err) -> { + config.clockSync = (ClockSyncMode) UnionType.CLOCK_SYNC_UNION + .forName(ASTUtils.elementToSingleString(value)); + }), /** * Key-value pairs giving options for clock synchronization. */ CLOCK_SYNC_OPTIONS("clock-sync-options", DictionaryType.CLOCK_SYNC_OPTION_DICT, Arrays.asList(Target.C, Target.CCPP, Target.Python), + (config) -> { + Element e = LfFactory.eINSTANCE.createElement(); + KeyValuePairs kvp = LfFactory.eINSTANCE.createKeyValuePairs(); + for (ClockSyncOption opt : ClockSyncOption.values()) { + KeyValuePair pair = LfFactory.eINSTANCE.createKeyValuePair(); + pair.setName(opt.toString()); + switch (opt) { + case ATTENUATION: + pair.setValue(ASTUtils.toElement(config.clockSyncOptions.attenuation)); + break; + case COLLECT_STATS: + pair.setValue(ASTUtils.toElement(config.clockSyncOptions.collectStats)); + break; + case LOCAL_FEDERATES_ON: + pair.setValue(ASTUtils.toElement(config.clockSyncOptions.localFederatesOn)); + break; + case PERIOD: + if (config.clockSyncOptions.period == null) continue; // don't set if null + pair.setValue(ASTUtils.toElement(config.clockSyncOptions.period)); + break; + case TEST_OFFSET: + if (config.clockSyncOptions.testOffset == null) continue; // don't set if null + pair.setValue(ASTUtils.toElement(config.clockSyncOptions.testOffset)); + break; + case TRIALS: + pair.setValue(ASTUtils.toElement(config.clockSyncOptions.trials)); + break; + } + kvp.getPairs().add(pair); + } + e.setKeyvalue(kvp); + // kvp will never be empty + return e; + }, (config, value, err) -> { for (KeyValuePair entry : value.getKeyvalue().getPairs()) { ClockSyncOption option = (ClockSyncOption) DictionaryType.CLOCK_SYNC_OPTION_DICT @@ -143,7 +186,9 @@ public enum TargetProperty { * can be adjusted in the included file. */ CMAKE_INCLUDE("cmake-include", UnionType.FILE_OR_FILE_ARRAY, - Arrays.asList(Target.CPP, Target.C, Target.CCPP), (config, value, err) -> { + Arrays.asList(Target.CPP, Target.C, Target.CCPP), + (config) -> ASTUtils.toElement(config.cmakeIncludes), + (config, value, err) -> { config.cmakeIncludes = ASTUtils.elementToListOfStrings(value); }, // FIXME: This merging of lists is potentially dangerous since @@ -153,49 +198,67 @@ public enum TargetProperty { (config, value, err) -> { config.cmakeIncludes.addAll(ASTUtils.elementToListOfStrings(value)); }), - /** * Directive to specify the target compiler. */ COMPILER("compiler", PrimitiveType.STRING, Target.ALL, + (config) -> ASTUtils.toElement(config.compiler), (config, value, err) -> { config.compiler = ASTUtils.elementToSingleString(value); }), + /** + * Directive to specify compile-time definitions. + */ + COMPILE_DEFINITIONS("compile-definitions", StringDictionaryType.COMPILE_DEFINITION, + Arrays.asList(Target.C, Target.CCPP, Target.Python), + (config) -> ASTUtils.toElement(config.compileDefinitions), + (config, value, err) -> { + config.compileDefinitions = ASTUtils.elementToStringMaps(value); + }), + /** * Directive to generate a Dockerfile. This is either a boolean, * true or false, or a dictionary of options. */ DOCKER("docker", UnionType.DOCKER_UNION, - Arrays.asList(Target.C, Target.CCPP, Target.Python, Target.TS), (config, value, err) -> { - if (value.getLiteral() != null) { - if (ASTUtils.toBoolean(value)) { - config.dockerOptions = new DockerOptions(); - } else { - config.dockerOptions = null; - } + Arrays.asList(Target.C, Target.CCPP, Target.Python, Target.TS), + (config) -> { + if (config.dockerOptions == null) { + return null; + } else if(config.dockerOptions.equals(new DockerOptions())) { + // default configuration + return ASTUtils.toElement(true); } else { - config.dockerOptions = new DockerOptions(); - for (KeyValuePair entry : value.getKeyvalue().getPairs()) { - DockerOption option = (DockerOption) DictionaryType.DOCKER_DICT - .forName(entry.getName()); - switch (option) { + Element e = LfFactory.eINSTANCE.createElement(); + KeyValuePairs kvp = LfFactory.eINSTANCE.createKeyValuePairs(); + for (DockerOption opt : DockerOption.values()) { + KeyValuePair pair = LfFactory.eINSTANCE.createKeyValuePair(); + pair.setName(opt.toString()); + switch (opt) { case FROM: - config.dockerOptions.from = ASTUtils.elementToSingleString(entry.getValue()); - break; - default: + if (config.dockerOptions.from == null) continue; + pair.setValue(ASTUtils.toElement(config.dockerOptions.from)); break; } + kvp.getPairs().add(pair); } + e.setKeyvalue(kvp); + if (kvp.getPairs().isEmpty()) return null; + return e; } - }), + }, + (config, value, err) -> setDockerProperty(config, value), + (config, value, err) -> setDockerProperty(config, value)), /** * Directive for specifying a path to an external runtime to be used for the * compiled binary. */ EXTERNAL_RUNTIME_PATH("external-runtime-path", PrimitiveType.STRING, - List.of(Target.CPP), (config, value, err) -> { + List.of(Target.CPP), + (config) -> ASTUtils.toElement(config.externalRuntimePath), + (config, value, err) -> { config.externalRuntimePath = ASTUtils.elementToSingleString(value); }), @@ -204,6 +267,7 @@ public enum TargetProperty { * faster than physical time. */ FAST("fast", PrimitiveType.BOOLEAN, Target.ALL, + (config) -> ASTUtils.toElement(config.fastMode), (config, value, err) -> { config.fastMode = ASTUtils.toBoolean(value); }), @@ -213,6 +277,7 @@ public enum TargetProperty { * processed by the code generator. */ FILES("files", UnionType.FILE_OR_FILE_ARRAY, List.of(Target.C, Target.CCPP, Target.Python), + (config) -> ASTUtils.toElement(config.fileNames), (config, value, err) -> { config.fileNames = ASTUtils.elementToListOfStrings(value); }, @@ -228,15 +293,22 @@ public enum TargetProperty { * Flags to be passed on to the target compiler. */ FLAGS("flags", UnionType.STRING_OR_STRING_ARRAY, - Arrays.asList(Target.C, Target.CCPP), (config, value, err) -> { - config.compilerFlags = ASTUtils.elementToListOfStrings(value); - }), + Arrays.asList(Target.C, Target.CCPP), + (config) -> ASTUtils.toElement(config.compilerFlags), + (config, value, err) -> { + config.compilerFlags = ASTUtils.elementToListOfStrings(value); + }), /** * Directive to specify the coordination mode */ COORDINATION("coordination", UnionType.COORDINATION_UNION, Arrays.asList(Target.C, Target.CCPP, Target.Python), + (config) -> ASTUtils.toElement(config.coordination.toString()), + (config, value, err) -> { + config.coordination = (CoordinationType) UnionType.COORDINATION_UNION + .forName(ASTUtils.elementToSingleString(value)); + }, (config, value, err) -> { config.coordination = (CoordinationType) UnionType.COORDINATION_UNION .forName(ASTUtils.elementToSingleString(value)); @@ -247,6 +319,24 @@ public enum TargetProperty { */ COORDINATION_OPTIONS("coordination-options", DictionaryType.COORDINATION_OPTION_DICT, Arrays.asList(Target.C, Target.CCPP, Target.Python, Target.TS), + (config) -> { + Element e = LfFactory.eINSTANCE.createElement(); + KeyValuePairs kvp = LfFactory.eINSTANCE.createKeyValuePairs(); + for (CoordinationOption opt : CoordinationOption.values()) { + KeyValuePair pair = LfFactory.eINSTANCE.createKeyValuePair(); + pair.setName(opt.toString()); + switch (opt) { + case ADVANCE_MESSAGE_INTERVAL: + if (config.coordinationOptions.advance_message_interval == null) continue; + pair.setValue(ASTUtils.toElement(config.coordinationOptions.advance_message_interval)); + break; + } + kvp.getPairs().add(pair); + } + e.setKeyvalue(kvp); + if (kvp.getPairs().isEmpty()) return null; + return e; + }, (config, value, err) -> { for (KeyValuePair entry : value.getKeyvalue().getPairs()) { CoordinationOption option = (CoordinationOption) DictionaryType.COORDINATION_OPTION_DICT @@ -260,6 +350,20 @@ public enum TargetProperty { break; } } + }, + (config, value, err) -> { + for (KeyValuePair entry : value.getKeyvalue().getPairs()) { + CoordinationOption option = (CoordinationOption) DictionaryType.COORDINATION_OPTION_DICT + .forName(entry.getName()); + switch (option) { + case ADVANCE_MESSAGE_INTERVAL: + config.coordinationOptions.advance_message_interval = ASTUtils + .toTimeValue(entry.getValue()); + break; + default: + break; + } + } }), /** @@ -267,6 +371,7 @@ public enum TargetProperty { * are no more events in the event queue. */ KEEPALIVE("keepalive", PrimitiveType.BOOLEAN, Target.ALL, + (config) -> ASTUtils.toElement(config.keepalive), (config, value, err) -> { config.keepalive = ASTUtils.toBoolean(value); }), @@ -275,9 +380,14 @@ public enum TargetProperty { * Directive to specify the grain at which to report log messages during execution. */ LOGGING("logging", UnionType.LOGGING_UNION, Target.ALL, + (config) -> ASTUtils.toElement(config.logLevel.toString()), (config, value, err) -> { config.logLevel = (LogLevel) UnionType.LOGGING_UNION .forName(ASTUtils.elementToSingleString(value)); + }, + (config, value, err) -> { + config.logLevel = (LogLevel) UnionType.LOGGING_UNION + .forName(ASTUtils.elementToSingleString(value)); }), /** @@ -285,6 +395,7 @@ public enum TargetProperty { */ NO_COMPILE("no-compile", PrimitiveType.BOOLEAN, Arrays.asList(Target.C, Target.CPP, Target.CCPP, Target.Python), + (config) -> ASTUtils.toElement(config.noCompile), (config, value, err) -> { config.noCompile = ASTUtils.toBoolean(value); }), @@ -293,15 +404,46 @@ public enum TargetProperty { * Directive to disable validation of reactor rules at runtime. */ NO_RUNTIME_VALIDATION("no-runtime-validation", PrimitiveType.BOOLEAN, - Arrays.asList(Target.CPP), (config, value, err) -> { + Arrays.asList(Target.CPP), + (config) -> ASTUtils.toElement(config.noRuntimeValidation), + (config, value, err) -> { config.noRuntimeValidation = ASTUtils.toBoolean(value); }), /** * Directive to specify the platform for cross code generation. This is either a string of the platform - * or a dictionary of options that includes the string name. - */ - PLATFORM("platform", UnionType.PLATFORM_STRING_OR_DICTIONARY, Target.ALL, + * or a dictionary of options that includes the string name. + */ + PLATFORM("platform", UnionType.PLATFORM_STRING_OR_DICTIONARY, Target.ALL, + (config) -> { + Element e = LfFactory.eINSTANCE.createElement(); + KeyValuePairs kvp = LfFactory.eINSTANCE.createKeyValuePairs(); + for (PlatformOption opt : PlatformOption.values()) { + KeyValuePair pair = LfFactory.eINSTANCE.createKeyValuePair(); + pair.setName(opt.toString()); + switch (opt) { + case NAME: + pair.setValue(ASTUtils.toElement(config.platformOptions.platform.toString())); + break; + case BAUDRATE: + pair.setValue(ASTUtils.toElement(config.platformOptions.baudRate)); + break; + case BOARD: + pair.setValue(ASTUtils.toElement(config.platformOptions.board)); + break; + case FLASH: + pair.setValue(ASTUtils.toElement(config.platformOptions.flash)); + break; + case PORT: + pair.setValue(ASTUtils.toElement(config.platformOptions.port)); + break; + } + kvp.getPairs().add(pair); + } + e.setKeyvalue(kvp); + if (kvp.getPairs().isEmpty()) return null; + return e; + }, (config, value, err) -> { if (value.getLiteral() != null) { config.platformOptions = new PlatformOptions(); @@ -316,7 +458,7 @@ public enum TargetProperty { case NAME: Platform p = (Platform) UnionType.PLATFORM_UNION .forName(ASTUtils.elementToSingleString(entry.getValue())); - if(p == null){ + if(p == null) { String s = "Unidentified Platform Type, LF supports the following platform types: " + Arrays.asList(Platform.values()).toString(); err.reportError(s); throw new AssertionError(s); @@ -332,6 +474,9 @@ public enum TargetProperty { case FLASH: config.platformOptions.flash = ASTUtils.toBoolean(entry.getValue()); break; + case PORT: + config.platformOptions.port = ASTUtils.elementToSingleString(entry.getValue()); + break; default: break; } @@ -345,6 +490,7 @@ public enum TargetProperty { */ PROTOBUFS("protobufs", UnionType.FILE_OR_FILE_ARRAY, Arrays.asList(Target.C, Target.CCPP, Target.TS, Target.Python), + (config) -> ASTUtils.toElement(config.protoFiles), (config, value, err) -> { config.protoFiles = ASTUtils.elementToListOfStrings(value); }), @@ -354,7 +500,9 @@ public enum TargetProperty { * Directive to specify that ROS2 specific code is generated, */ ROS2("ros2", PrimitiveType.BOOLEAN, - List.of(Target.CPP), (config, value, err) -> { + List.of(Target.CPP), + (config) -> ASTUtils.toElement(config.ros2), + (config, value, err) -> { config.ros2 = ASTUtils.toBoolean(value); }), @@ -362,7 +510,9 @@ public enum TargetProperty { * Directive to specify additional ROS2 packages that this LF program depends on. */ ROS2_DEPENDENCIES("ros2-dependencies", ArrayType.STRING_ARRAY, - List.of(Target.CPP), (config, value, err) -> { + List.of(Target.CPP), + (config) -> ASTUtils.toElement(config.ros2Dependencies), + (config, value, err) -> { config.ros2Dependencies = ASTUtils.elementToListOfStrings(value); }), @@ -370,7 +520,9 @@ public enum TargetProperty { * Directive for specifying a specific version of the reactor runtime library. */ RUNTIME_VERSION("runtime-version", PrimitiveType.STRING, - Arrays.asList(Target.CPP), (config, value, err) -> { + Arrays.asList(Target.CPP), + (config) -> ASTUtils.toElement(config.runtimeVersion), + (config, value, err) -> { config.runtimeVersion = ASTUtils.elementToSingleString(value); }), @@ -379,7 +531,9 @@ public enum TargetProperty { * Directive for specifying a specific runtime scheduler, if supported. */ SCHEDULER("scheduler", UnionType.SCHEDULER_UNION, - Arrays.asList(Target.C, Target.CCPP, Target.Python), (config, value, err) -> { + Arrays.asList(Target.C, Target.CCPP, Target.Python), + (config) -> ASTUtils.toElement(config.schedulerType.toString()), + (config, value, err) -> { config.schedulerType = (SchedulerOption) UnionType.SCHEDULER_UNION .forName(ASTUtils.elementToSingleString(value)); }), @@ -388,7 +542,9 @@ public enum TargetProperty { * Directive to specify that all code is generated in a single file. */ SINGLE_FILE_PROJECT("single-file-project", PrimitiveType.BOOLEAN, - List.of(Target.Rust), (config, value, err) -> { + List.of(Target.Rust), + (config) -> ASTUtils.toElement(config.singleFileProject), + (config, value, err) -> { config.singleFileProject = ASTUtils.toBoolean(value); }), @@ -397,6 +553,7 @@ public enum TargetProperty { */ THREADING("threading", PrimitiveType.BOOLEAN, List.of(Target.C, Target.CCPP, Target.Python), + (config) -> ASTUtils.toElement(config.threading), (config, value, err) -> { config.threading = ASTUtils.toBoolean(value); }), @@ -406,6 +563,7 @@ public enum TargetProperty { */ WORKERS("workers", PrimitiveType.NON_NEGATIVE_INTEGER, List.of(Target.C, Target.CCPP, Target.Python, Target.CPP, Target.Rust), + (config) -> ASTUtils.toElement(config.workers), (config, value, err) -> { config.workers = ASTUtils.toInteger(value); }), @@ -414,6 +572,10 @@ public enum TargetProperty { * Directive to specify the execution timeout. */ TIMEOUT("timeout", PrimitiveType.TIME_VALUE, Target.ALL, + (config) -> ASTUtils.toElement(config.timeout), + (config, value, err) -> { + config.timeout = ASTUtils.toTimeValue(value); + }, (config, value, err) -> { config.timeout = ASTUtils.toTimeValue(value); }), @@ -423,7 +585,32 @@ public enum TargetProperty { * true or false, or a dictionary of options. */ TRACING("tracing", UnionType.TRACING_UNION, - Arrays.asList(Target.C, Target.CCPP, Target.CPP, Target.Python), (config, value, err) -> { + Arrays.asList(Target.C, Target.CCPP, Target.CPP, Target.Python), + (config) -> { + if (config.tracing == null) { + return null; + } else if (config.tracing.equals(new TracingOptions())) { + // default values + return ASTUtils.toElement(true); + } else { + Element e = LfFactory.eINSTANCE.createElement(); + KeyValuePairs kvp = LfFactory.eINSTANCE.createKeyValuePairs(); + for (TracingOption opt : TracingOption.values()) { + KeyValuePair pair = LfFactory.eINSTANCE.createKeyValuePair(); + pair.setName(opt.toString()); + switch (opt) { + case TRACE_FILE_NAME: + if (config.tracing.traceFileName == null) continue; + pair.setValue(ASTUtils.toElement(config.tracing.traceFileName)); + } + kvp.getPairs().add(pair); + } + e.setKeyvalue(kvp); + if (kvp.getPairs().isEmpty()) return null; + return e; + } + }, + (config, value, err) -> { if (value.getLiteral() != null) { if (ASTUtils.toBoolean(value)) { config.tracing = new TracingOptions(); @@ -452,8 +639,9 @@ public enum TargetProperty { * * This is a debugging feature and currently only used for C++ and Rust programs. */ - EXPORT_DEPENDENCY_GAPH("export-dependency-graph", PrimitiveType.BOOLEAN, + EXPORT_DEPENDENCY_GRAPH("export-dependency-graph", PrimitiveType.BOOLEAN, List.of(Target.CPP, Target.Rust), + (config) -> ASTUtils.toElement(config.exportDependencyGraph), (config, value, err) -> { config.exportDependencyGraph = ASTUtils.toBoolean(value); }), @@ -465,6 +653,7 @@ public enum TargetProperty { */ EXPORT_TO_YAML("export-to-yaml", PrimitiveType.BOOLEAN, List.of(Target.CPP), + (config) -> ASTUtils.toElement(config.exportToYaml), (config, value, err) -> { config.exportToYaml = ASTUtils.toBoolean(value); }), @@ -479,37 +668,56 @@ public enum TargetProperty { */ RUST_INCLUDE("rust-include", UnionType.FILE_OR_FILE_ARRAY, - List.of(Target.Rust), (config, value, err) -> { - Path referencePath; - try { - referencePath = FileUtil.toPath(value.eResource().getURI()).toAbsolutePath(); - } catch (IOException e) { - err.reportError(value, "Invalid path? " + e.getMessage()); - throw new RuntimeIOException(e); - } + List.of(Target.Rust), + (config) -> { + // do not check paths here, and simply copy the absolute path over + List paths = config.rust.getRustTopLevelModules(); + if (paths.isEmpty()) return null; + else if (paths.size() == 1) { + return ASTUtils.toElement(paths.get(0).toString()); + } else { + Element e = LfFactory.eINSTANCE.createElement(); + Array arr = LfFactory.eINSTANCE.createArray(); + for (Path p : paths) { + arr.getElements().add(ASTUtils.toElement(p.toString())); + } + e.setArray(arr); + return e; + } + }, + (config, value, err) -> { + Path referencePath; + try { + referencePath = FileUtil.toPath(value.eResource().getURI()).toAbsolutePath(); + } catch (IOException e) { + err.reportError(value, "Invalid path? " + e.getMessage()); + throw new RuntimeIOException(e); + } - // we'll resolve relative paths to check that the files - // are as expected. + // we'll resolve relative paths to check that the files + // are as expected. - if (value.getLiteral() != null) { - Path resolved = referencePath.resolveSibling(StringUtil.removeQuotes(value.getLiteral())); - - config.rust.addAndCheckTopLevelModule(resolved, value, err); - } else if (value.getArray() != null) { - for (Element element : value.getArray().getElements()) { - String literal = StringUtil.removeQuotes(element.getLiteral()); - Path resolved = referencePath.resolveSibling(literal); - config.rust.addAndCheckTopLevelModule(resolved, element, err); - } - } - }), + if (value.getLiteral() != null) { + Path resolved = referencePath.resolveSibling(StringUtil.removeQuotes(value.getLiteral())); + + config.rust.addAndCheckTopLevelModule(resolved, value, err); + } else if (value.getArray() != null) { + for (Element element : value.getArray().getElements()) { + String literal = StringUtil.removeQuotes(element.getLiteral()); + Path resolved = referencePath.resolveSibling(literal); + config.rust.addAndCheckTopLevelModule(resolved, element, err); + } + } + }), /** * Directive for specifying Cargo features of the generated * program to enable. */ CARGO_FEATURES("cargo-features", ArrayType.STRING_ARRAY, - List.of(Target.Rust), (config, value, err) -> { + List.of(Target.Rust), + (config) -> ASTUtils.toElement(config.rust.getCargoFeatures()), + (config, value, err) -> { config.rust.setCargoFeatures(ASTUtils.elementToListOfStrings(value)); }), @@ -539,12 +747,65 @@ public enum TargetProperty { */ CARGO_DEPENDENCIES("cargo-dependencies", CargoDependenciesPropertyType.INSTANCE, - List.of(Target.Rust), (config, value, err) -> { - config.rust.setCargoDependencies(CargoDependencySpec.parseAll(value)); + List.of(Target.Rust), + (config) -> { + var deps = config.rust.getCargoDependencies(); + if (deps.size() == 0) return null; + else { + Element e = LfFactory.eINSTANCE.createElement(); + KeyValuePairs kvp = LfFactory.eINSTANCE.createKeyValuePairs(); + for (var ent : deps.entrySet()) { + KeyValuePair pair = LfFactory.eINSTANCE.createKeyValuePair(); + pair.setName(ent.getKey()); + pair.setValue(CargoDependencySpec.extractSpec(ent.getValue())); + kvp.getPairs().add(pair); + } + e.setKeyvalue(kvp); + return e; + } + }, + (config, value, err) -> { + config.rust.setCargoDependencies(CargoDependencySpec.parseAll(value)); }), + /** + * Directs the C or Python target to include the associated C file used for + * setting up federated execution before processing the first tag. + */ + FED_SETUP("_fed_setup", PrimitiveType.FILE, + Arrays.asList(Target.C, Target.CCPP, Target.Python), + (config) -> ASTUtils.toElement(config.fedSetupPreamble), + (config, value, err) -> + config.fedSetupPreamble = StringUtil.removeQuotes(ASTUtils.elementToSingleString(value)) + ) ; + /** + * Update {@code config}.dockerOptions based on value. + */ + private static void setDockerProperty(TargetConfig config, Element value) { + if (value.getLiteral() != null) { + if (ASTUtils.toBoolean(value)) { + config.dockerOptions = new DockerOptions(); + } else { + config.dockerOptions = null; + } + } else { + config.dockerOptions = new DockerOptions(); + for (KeyValuePair entry : value.getKeyvalue().getPairs()) { + DockerOption option = (DockerOption) DictionaryType.DOCKER_DICT + .forName(entry.getName()); + switch (option) { + case FROM: + config.dockerOptions.from = ASTUtils.elementToSingleString(entry.getValue()); + break; + default: + break; + } + } + } + } + /** * String representation of this target property. */ @@ -586,6 +847,18 @@ private interface PropertyParser { void parseIntoTargetConfig(TargetConfig config, Element element, ErrorReporter err); } + public final PropertyGetter getter; + + @FunctionalInterface + private interface PropertyGetter { + + /** + * Read this property from the target config and build an element which represents it for the AST. + * May return null if the given value of this property is the same as the default. + */ + Element getPropertyElement(TargetConfig config); + } + /** * Private constructor for target properties. * @@ -597,10 +870,12 @@ private interface PropertyParser { */ TargetProperty(String description, TargetPropertyType type, List supportedBy, + PropertyGetter getter, PropertyParser setter) { this.description = description; this.type = type; this.supportedBy = supportedBy; + this.getter = getter; this.setter = setter; this.updater = (config, value, err) -> { /* Ignore the update by default */ }; } @@ -618,11 +893,13 @@ private interface PropertyParser { */ TargetProperty(String description, TargetPropertyType type, List supportedBy, + PropertyGetter getter, PropertyParser setter, PropertyParser updater) { this.description = description; this.type = type; this.supportedBy = supportedBy; + this.getter = getter; this.setter = setter; this.updater = updater; } @@ -659,6 +936,40 @@ public static void set(TargetConfig config, List properties, Error }); } + /** + * Extracts all properties as a list of key-value pairs from a TargetConfig. Only extracts properties explicitly set by user. + * @param config The TargetConfig to extract from. + * @return The extracted properties. + */ + public static List extractProperties(TargetConfig config) { + var res = new LinkedList(); + for (TargetProperty p : config.setByUser) { + KeyValuePair kv = LfFactory.eINSTANCE.createKeyValuePair(); + kv.setName(p.toString()); + kv.setValue(p.getter.getPropertyElement(config)); + if (kv.getValue() != null) + res.add(kv); + } + return res; + } + + /** + * Constructs a TargetDecl by extracting the fields of the given TargetConfig. + * @param target The target to generate for. + * @param config The TargetConfig to extract from. + * @return A generated TargetDecl. + */ + public static TargetDecl extractTargetDecl(Target target, TargetConfig config) { + TargetDecl decl = LfFactory.eINSTANCE.createTargetDecl(); + KeyValuePairs kvp = LfFactory.eINSTANCE.createKeyValuePairs(); + for (KeyValuePair p : extractProperties(config)) { + kvp.getPairs().add(p); + } + decl.setName(target.toString()); + decl.setConfig(kvp); + return decl; + } + /** * Update the given configuration using the given target properties. * @@ -669,6 +980,8 @@ public static void update(TargetConfig config, List properties,Err properties.forEach(property -> { TargetProperty p = forName(property.getName()); if (p != null) { + // Mark the specified target property as set by the user + config.setByUser.add(p); p.updater.parseIntoTargetConfig(config, property.getValue(), err); } }); @@ -725,7 +1038,37 @@ public String toString() { // Inner classes for the various supported types. + /** + * Dictionary type that allows for keys that will be interpreted as strings + * and string values. + */ + public enum StringDictionaryType implements TargetPropertyType { + COMPILE_DEFINITION(); + @Override + public boolean validate(Element e) { + if (e.getKeyvalue() != null) { + return true; + } + return false; + } + + @Override + public void check(Element e, String name, LFValidator v) { + KeyValuePairs kv = e.getKeyvalue(); + if (kv == null) { + TargetPropertyType.produceError(name, this.toString(), v); + } else { + for (KeyValuePair pair : kv.getPairs()) { + String key = pair.getName(); + Element val = pair.getValue(); + // Make sure the type is string + PrimitiveType.STRING.check(val, name + "." + key, v); + } + } + + } + } /** * Interface for dictionary elements. It associates an entry with a type. @@ -1206,7 +1549,7 @@ public boolean validate(Element e) { * * @param e The element to type check. * @param name The name of the target property. - * @param v The validator to which any errors should be reported. + * @param v The LFValidator to append errors to. */ public void check(Element e, String name, LFValidator v) { if (!this.validate(e)) { @@ -1323,18 +1666,19 @@ public enum PlatformOption implements DictionaryElement { NAME("name", PrimitiveType.STRING), BAUDRATE("baud-rate", PrimitiveType.NON_NEGATIVE_INTEGER), BOARD("board", PrimitiveType.STRING), - FLASH("flash", PrimitiveType.BOOLEAN); + FLASH("flash", PrimitiveType.BOOLEAN), + PORT("port", PrimitiveType.STRING); public final PrimitiveType type; - + private final String description; - + private PlatformOption(String alias, PrimitiveType type) { this.description = alias; this.type = type; } - - + + /** * Return the description of this dictionary element. */ @@ -1342,7 +1686,7 @@ private PlatformOption(String alias, PrimitiveType type) { public String toString() { return this.description; } - + /** * Return the type associated with this dictionary element. */ @@ -1405,7 +1749,7 @@ public String toString() { */ public enum Platform { AUTO, - ARDUINO("Arduino"), + ARDUINO, NRF52("Nrf52"), LINUX("Linux"), MAC("Darwin"), diff --git a/org.lflang/src/org/lflang/TimeUnit.java b/org.lflang/src/org/lflang/TimeUnit.java index dbd4ccb0d6..00c6314e51 100644 --- a/org.lflang/src/org/lflang/TimeUnit.java +++ b/org.lflang/src/org/lflang/TimeUnit.java @@ -112,4 +112,8 @@ public static List list() { return Arrays.stream(values()).flatMap(it -> it.allNames.stream()).collect(Collectors.toList()); } + @Override + public String toString() { + return this.canonicalName; + } } diff --git a/org.lflang/src/org/lflang/ast/AfterDelayTransformation.java b/org.lflang/src/org/lflang/ast/AfterDelayTransformation.java index 266c4177be..ababf3787c 100644 --- a/org.lflang/src/org/lflang/ast/AfterDelayTransformation.java +++ b/org.lflang/src/org/lflang/ast/AfterDelayTransformation.java @@ -333,6 +333,8 @@ private Reactor getDelayClass(Type type) { r2.setCode(factory.createCode()); r2.getCode().setBody(generator.generateForwardBody(action, outRef)); + generator.finalizeReactions(r1, r2); + // Add the reactions to the newly created reactor class. // These need to go in the opposite order in case // a new input arrives at the same time the delayed diff --git a/org.lflang/src/org/lflang/ast/FormattingUtils.java b/org.lflang/src/org/lflang/ast/FormattingUtils.java index bcdd75ea94..bd9fbaf3d8 100644 --- a/org.lflang/src/org/lflang/ast/FormattingUtils.java +++ b/org.lflang/src/org/lflang/ast/FormattingUtils.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.Function; import java.util.function.ToLongFunction; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -11,7 +12,9 @@ import org.eclipse.emf.ecore.EObject; import org.lflang.ASTUtils; +import org.lflang.Target; import org.lflang.lf.Model; +import org.lflang.lf.TargetDecl; /** * Utility functions that determine the specific behavior of the LF formatter. @@ -48,10 +51,24 @@ public class FormattingUtils { * {@code lineLength}. */ public static String render(EObject object, int lineLength) { + return render(object, lineLength, inferTarget(object), false); + } + + /** Return a function that renders AST nodes for the given target. */ + public static Function renderer(TargetDecl targetDecl) { + return object -> render(object, DEFAULT_LINE_LENGTH, Target.fromDecl(targetDecl), true); + } + + /** + * Return a String representation of {@code object}, with lines wrapped at + * {@code lineLength}, with the assumption that the target language is + * {@code target}. + */ + public static String render(EObject object, int lineLength, Target target, boolean codeMapTags) { MalleableString ms = ToLf.instance.doSwitch(object); - String singleLineCommentPrefix = getSingleLineCommentPrefix(object); + String singleLineCommentPrefix = target.getSingleLineCommentPrefix(); ms.findBestRepresentation( - () -> ms.render(INDENTATION, singleLineCommentPrefix), + () -> ms.render(INDENTATION, singleLineCommentPrefix, codeMapTags, null), r -> r.levelsOfCommentDisplacement() * BADNESS_PER_LEVEL_OF_COMMENT_DISPLACEMENT + countCharactersViolatingLineLength(lineLength).applyAsLong(r.rendering()) * BADNESS_PER_CHARACTER_VIOLATING_LINE_LENGTH @@ -60,7 +77,7 @@ public static String render(EObject object, int lineLength) { INDENTATION, singleLineCommentPrefix ); - var optimizedRendering = ms.render(INDENTATION, singleLineCommentPrefix); + var optimizedRendering = ms.render(INDENTATION, singleLineCommentPrefix, codeMapTags, null); List comments = optimizedRendering.unplacedComments().toList(); return comments.stream().allMatch(String::isBlank) ? optimizedRendering.rendering() : lineWrapComments(comments, lineLength, singleLineCommentPrefix) @@ -68,17 +85,16 @@ public static String render(EObject object, int lineLength) { } /** - * Return the prefix that the formatter should use to mark the start of a - * single-line comment. + * Infer the target language of the object. */ - private static String getSingleLineCommentPrefix(EObject object) { + private static Target inferTarget(EObject object) { if (object instanceof Model model) { var targetDecl = ASTUtils.targetDecl(model); - if (targetDecl != null && targetDecl.getName().toUpperCase().contains("PYTHON")) { - return "#"; + if (targetDecl != null) { + return Target.fromDecl(targetDecl); } } - return "//"; + throw new IllegalArgumentException("Unable to determine target based on given EObject."); } /** @@ -227,7 +243,7 @@ static boolean placeComment( if (comment.stream().allMatch(String::isBlank)) return true; String wrapped = FormattingUtils.lineWrapComments(comment, width, singleLineCommentPrefix); if (keepCommentsOnSameLine && wrapped.lines().count() == 1 && !wrapped.startsWith("/**")) { - int cumsum = 0; + int sum = 0; for (int j = 0; j < components.size(); j++) { String current = components.get(j); if (j >= i && current.contains("\n")) { @@ -235,14 +251,14 @@ static boolean placeComment( "\n", " ".repeat(Math.max( 2, - startColumn - cumsum - components.get(j).indexOf("\n") + startColumn - sum - components.get(j).indexOf("\n") )) + wrapped + "\n" )); return true; } else if (current.contains("\n")) { - cumsum = current.length() - current.lastIndexOf("\n") - 1; + sum = current.length() - current.lastIndexOf("\n") - 1; } else { - cumsum += current.length(); + sum += current.length(); } } } diff --git a/org.lflang/src/org/lflang/ast/MalleableString.java b/org.lflang/src/org/lflang/ast/MalleableString.java index 8475aab301..3daff23e07 100644 --- a/org.lflang/src/org/lflang/ast/MalleableString.java +++ b/org.lflang/src/org/lflang/ast/MalleableString.java @@ -14,6 +14,9 @@ import java.util.function.ToLongFunction; import java.util.stream.Collector; import java.util.stream.Stream; +import org.eclipse.emf.ecore.EObject; +import org.lflang.generator.CodeMap; +import org.lflang.lf.Code; import org.lflang.util.StringUtil; /** @@ -23,6 +26,7 @@ public abstract class MalleableString { protected List comments = new ArrayList<>(); + protected EObject sourceEObject = null; /** Return this, indented by one more level. */ public MalleableString indent() { @@ -55,11 +59,21 @@ public MalleableString addComments(Stream comments) { return this; } + /** Specify the EObject from which this originated, if applicable. */ + public MalleableString setSourceEObject(EObject sourceEObject) { + this.sourceEObject = sourceEObject; + return this; + } + /** * Render this using {@code indentation} spaces per indentation level and {@code * singleLineCommentMarker} to mark the beginnings of single-line comments. */ - public abstract RenderResult render(int indentation, String singleLineCommentMarker); + public abstract RenderResult render( + int indentation, + String singleLineCommentMarker, + boolean codeMapTag, + EObject enclosingEObject); /** Return an object that can be represented as any one of the given alternatives. */ public static MalleableString anyOf(MalleableString... possibilities) { @@ -88,7 +102,7 @@ private static String[] objectArrayToString(Object[] objects) { public String toString() { List temp = comments; comments = List.of(); - String ret = render(0, "").rendering; + String ret = render(0, "", false, null).rendering; comments = temp; return ret; } @@ -232,10 +246,23 @@ private Sequence(ImmutableList components) { private int width = 0; @Override - public RenderResult render(int indentation, String singleLineCommentPrefix) { + public RenderResult render( + int indentation, + String singleLineCommentPrefix, + boolean codeMapTag, + EObject enclosingEObject) { List componentRenderings = components.stream() - .map(malleableString -> malleableString.render(indentation, singleLineCommentPrefix)) + .map( + malleableString -> + // The code map tags *should* not affect the correctness of the formatting + // since most + // formatting has already happened in findBestRepresentation. + malleableString.render( + indentation, + singleLineCommentPrefix, + codeMapTag, + sourceEObject != null ? sourceEObject : enclosingEObject)) .toList(); List> commentsFromChildren = componentRenderings.stream().map(it -> it.unplacedComments).map(Stream::toList).toList(); @@ -333,7 +360,7 @@ public void findBestRepresentation( if (components.stream() .noneMatch( it -> - it.render(indentation, singleLineCommentPrefix) + it.render(indentation, singleLineCommentPrefix, false, null) .unplacedComments .findAny() .isPresent())) return; @@ -399,8 +426,17 @@ public boolean isEmpty() { } @Override - public RenderResult render(int indentation, String singleLineCommentPrefix) { - var result = nested.render(indentation, singleLineCommentPrefix); + public RenderResult render( + int indentation, + String singleLineCommentPrefix, + boolean codeMapTag, + EObject enclosingEObject) { + var result = + nested.render( + indentation, + singleLineCommentPrefix, + codeMapTag, + sourceEObject != null ? sourceEObject : enclosingEObject); String renderedComments = FormattingUtils.lineWrapComments( result.unplacedComments.toList(), width - indentation, singleLineCommentPrefix); @@ -478,9 +514,17 @@ public boolean isEmpty() { } @Override - public RenderResult render(int indentation, String singleLineCommentPrefix) { + public RenderResult render( + int indentation, + String singleLineCommentPrefix, + boolean codeMapTag, + EObject enclosingEObject) { return getChosenPossibility() - .render(indentation, singleLineCommentPrefix) + .render( + indentation, + singleLineCommentPrefix, + codeMapTag, + sourceEObject != null ? sourceEObject : enclosingEObject) .with(comments.stream()); } } @@ -508,8 +552,17 @@ public boolean isEmpty() { } @Override - public RenderResult render(int indentation, String singleLineCommentPrefix) { - return new RenderResult(comments.stream(), getChosenPossibility(), 0); + public RenderResult render( + int indentation, + String singleLineCommentPrefix, + boolean codeMapTag, + EObject enclosingEObject) { + return new RenderResult( + comments.stream(), + enclosingEObject instanceof Code && codeMapTag + ? CodeMap.Correspondence.tag(enclosingEObject, getChosenPossibility(), true) + : getChosenPossibility(), + 0); } } } diff --git a/org.lflang/src/org/lflang/ast/ToLf.java b/org.lflang/src/org/lflang/ast/ToLf.java index 6633c8a6af..7582ee3a59 100644 --- a/org.lflang/src/org/lflang/ast/ToLf.java +++ b/org.lflang/src/org/lflang/ast/ToLf.java @@ -104,6 +104,7 @@ public MalleableString caseArraySpec(ArraySpec spec) { @Override public MalleableString doSwitch(EObject eObject) { ICompositeNode node = NodeModelUtils.findActualNodeFor(eObject); + if (node == null) return super.doSwitch(eObject); var ancestorComments = getAncestorComments(node); Predicate doesNotBelongToAncestor = n -> !ancestorComments.contains(n); List followingComments = @@ -123,9 +124,10 @@ public MalleableString doSwitch(EObject eObject) { allComments.addAll(followingComments); if (allComments.stream().anyMatch(s -> KEEP_FORMAT_COMMENT.matcher(s).matches())) { return MalleableString.anyOf(StringUtil.trimCodeBlock(node.getText(), 0)) - .addComments(followingComments.stream()); + .addComments(followingComments.stream()) + .setSourceEObject(eObject); } - return super.doSwitch(eObject).addComments(allComments.stream()); + return super.doSwitch(eObject).addComments(allComments.stream()).setSourceEObject(eObject); } /** Return all comments contained by ancestors of {@code node} that belong to said ancestors. */ @@ -1021,29 +1023,40 @@ private MalleableString indentedStatements( var sorted = statementListList.stream() .flatMap(List::stream) - .sorted(Comparator.comparing(object -> NodeModelUtils.getNode(object).getStartLine())) + .sequential() + .sorted( + Comparator.comparing( + object -> { + INode node = NodeModelUtils.getNode(object); + return node == null ? 0 : node.getStartLine(); + })) .toList(); if (sorted.isEmpty()) return MalleableString.anyOf(""); var ret = new Builder(); var first = true; for (var object : sorted) { if (!first) { - INode node = NodeModelUtils.getNode(object); - StringBuilder leadingText = new StringBuilder(); - if (!forceWhitespace) { - for (INode n : node.getAsTreeIterable()) { - if (n instanceof ICompositeNode) continue; - if (!ASTUtils.isComment(n) && !n.getText().isBlank()) break; - leadingText.append(n.getText()); - } - } - boolean hasLeadingBlankLines = - leadingText.toString().lines().skip(1).filter(String::isBlank).count() > 1; - ret.append("\n".repeat(forceWhitespace || hasLeadingBlankLines ? 2 : 1)); + ret.append("\n".repeat(shouldAddWhitespaceBefore(object, forceWhitespace) ? 2 : 1)); } ret.append(doSwitch(object)); first = false; } return ret.append("\n").get().indent(); } + + private static boolean shouldAddWhitespaceBefore(EObject object, boolean forceWhitespace) { + INode node = NodeModelUtils.getNode(object); + if (node == null) return true; + StringBuilder leadingText = new StringBuilder(); + if (!forceWhitespace) { + for (INode n : node.getAsTreeIterable()) { + if (n instanceof ICompositeNode) continue; + if (!ASTUtils.isComment(n) && !n.getText().isBlank()) break; + leadingText.append(n.getText()); + } + } + boolean hasLeadingBlankLines = + leadingText.toString().lines().skip(1).filter(String::isBlank).count() > 1; + return forceWhitespace || hasLeadingBlankLines; + } } diff --git a/org.lflang/src/org/lflang/cli/Lfc.java b/org.lflang/src/org/lflang/cli/Lfc.java index 2751085b26..45b64f29c9 100644 --- a/org.lflang/src/org/lflang/cli/Lfc.java +++ b/org.lflang/src/org/lflang/cli/Lfc.java @@ -4,6 +4,7 @@ package org.lflang.cli; + import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -21,6 +22,7 @@ import org.lflang.ASTUtils; import org.lflang.FileConfig; + import org.lflang.generator.LFGeneratorContext; import org.lflang.generator.LFGeneratorContext.BuildParm; import org.lflang.generator.MainContext; @@ -199,8 +201,8 @@ else if (cmd.hasOption(CLIOption.FEDERATED.option.getOpt())) { exitIfCollectedErrors(); LFGeneratorContext context = new MainContext( - LFGeneratorContext.Mode.STANDALONE, CancelIndicator.NullImpl, (m, p) -> {}, properties, false, - fileConfig -> errorReporter + LFGeneratorContext.Mode.STANDALONE, CancelIndicator.NullImpl, + (m, p) -> {}, properties, resource, this.fileAccess, fileConfig -> errorReporter ); try { diff --git a/org.lflang/src/org/lflang/cli/ReportingUtil.kt b/org.lflang/src/org/lflang/cli/ReportingUtil.kt index 998b1174f9..fc7ea934ee 100644 --- a/org.lflang/src/org/lflang/cli/ReportingUtil.kt +++ b/org.lflang/src/org/lflang/cli/ReportingUtil.kt @@ -255,7 +255,7 @@ class ReportingBackend constructor( val severity = issue.severity val filePath = issue.file?.normalize() - val header = severity.name.toLowerCase(Locale.ROOT) + val header = severity.name.lowercase(Locale.ROOT) var fullMessage: String = this.header + colors.severityColors(header, severity) + colors.bold(": " + issue.message) + System.lineSeparator() val snippet: String? = filePath?.let { formatIssue(issue, filePath) } diff --git a/org.lflang/src/org/lflang/federated/CGeneratorExtension.java b/org.lflang/src/org/lflang/federated/CGeneratorExtension.java deleted file mode 100644 index 21c3cd5ebd..0000000000 --- a/org.lflang/src/org/lflang/federated/CGeneratorExtension.java +++ /dev/null @@ -1,250 +0,0 @@ -/************* - * Copyright (c) 2021, The University of California at Berkeley. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - ***************/ - -package org.lflang.federated; - -import org.lflang.ASTUtils; -import org.lflang.TargetConfig; -import org.lflang.ASTUtils; -import org.lflang.TimeValue; -import org.lflang.generator.ReactorInstance; -import org.lflang.generator.c.CGenerator; -import org.lflang.generator.c.CUtil; -import org.lflang.lf.Action; -import org.lflang.lf.Expression; -import org.lflang.lf.Input; -import org.lflang.lf.Parameter; -import org.lflang.lf.ParameterReference; -import org.lflang.lf.Reactor; -import org.lflang.lf.ReactorDecl; -import org.lflang.lf.VarRef; - -/** - * An extension class to the CGenerator that enables certain federated - * functionalities. Currently, this class offers the following features: - * - * - Allocating and initializing C structures for federated communication - - * Creating status field for network input ports that help the receiver logic in - * federate.c communicate the status of a network input port with network input - * control reactions. - * - * @author Soroush Bateni - * - */ -public class CGeneratorExtension { - - /** - * Generate C code that allocates sufficient memory for the following two - * critical data structures that support network control reactions: - * - triggers_for_network_input_control_reactions: These are triggers that are - * used at runtime to insert network input control reactions into the - * reaction queue. - * - trigger_for_network_output_control_reactions: Triggers for - * network output control reactions, which are unique per each output port. - * There could be multiple network output control reactions for each network - * output port if it is connected to multiple downstream federates. - * - * @param federate The top-level federate instance - * @param generator The instance of the CGenerator passed to keep this - * extension function static. - * @return A string that allocates memory for the aforementioned three - * structures. - */ - public static String allocateTriggersForFederate( - FederateInstance federate, - int startTimeStepIsPresentCount, - boolean isFederated, - boolean isFederatedAndDecentralized - ) { - - StringBuilder builder = new StringBuilder(); - - // Create the table to initialize intended tag fields to 0 between time - // steps. - if (isFederatedAndDecentralized && - startTimeStepIsPresentCount > 0) { - // Allocate the initial (before mutations) array of pointers to - // intended_tag fields. - // There is a 1-1 map between structs containing is_present and - // intended_tag fields, - // thus, we reuse startTimeStepIsPresentCount as the counter. - builder.append( - "// Create the array that will contain pointers to intended_tag fields to reset on each step.\n" - + "_lf_intended_tag_fields_size = " - + startTimeStepIsPresentCount + ";\n" - + "_lf_intended_tag_fields = (tag_t**)malloc(" - + "_lf_intended_tag_fields_size * sizeof(tag_t*));\n"); - } - - if (isFederated) { - if (federate.networkInputControlReactionsTriggers.size() > 0) { - // Proliferate the network input control reaction trigger array - builder.append( - "// Initialize the array of pointers to network input port triggers\n" - + "_fed.triggers_for_network_input_control_reactions_size = " - + federate.networkInputControlReactionsTriggers.size() - + ";\n" - + "_fed.triggers_for_network_input_control_reactions = (trigger_t**)malloc(" - + "_fed.triggers_for_network_input_control_reactions_size * sizeof(trigger_t*)" - + ");\n"); - - } - } - - return builder.toString(); - } - - /** - * Generate C code that initializes three critical structures that support - * network control reactions: - * - triggers_for_network_input_control_reactions: These are triggers that are - * used at runtime to insert network input control reactions into the - * reaction queue. There could be multiple network input control reactions - * for one network input at multiple levels in the hierarchy. - * - trigger_for_network_output_control_reactions: Triggers for - * network output control reactions, which are unique per each output port. - * There could be multiple network output control reactions for each network - * output port if it is connected to multiple downstream federates. - * - * @param instance The reactor instance that is at any level of the - * hierarchy within the federate. - * @param federate The top-level federate - * @param generator The instance of the CGenerator passed to keep this - * extension function static. - * @return A string that initializes the aforementioned three structures. - */ - public static String initializeTriggerForControlReactions( - ReactorInstance instance, - ReactorInstance main, - FederateInstance federate - ) { - StringBuilder builder = new StringBuilder(); - // The network control reactions are always in the main federated - // reactor - if (instance != main) { - return ""; - } - - ReactorDecl reactorClass = instance.getDefinition().getReactorClass(); - Reactor reactor = ASTUtils.toDefinition(reactorClass); - String nameOfSelfStruct = CUtil.reactorRef(instance); - - // Initialize triggers for network input control reactions - for (Action trigger : federate.networkInputControlReactionsTriggers) { - // Check if the trigger belongs to this reactor instance - if (ASTUtils.allReactions(reactor).stream().anyMatch(r -> { - return r.getTriggers().stream().anyMatch(t -> { - if (t instanceof VarRef) { - return ((VarRef) t).getVariable().equals(trigger); - } else { - return false; - } - }); - })) { - // Initialize the triggers_for_network_input_control_reactions for the input - builder.append("// Add trigger " + nameOfSelfStruct + "->_lf__" - + trigger.getName() - + " to the global list of network input ports.\n" - + "_fed.triggers_for_network_input_control_reactions[" - + federate.networkInputControlReactionsTriggers.indexOf(trigger) - + "]= &" + nameOfSelfStruct + "" + "->_lf__" - + trigger.getName() + ";\n"); - } - } - - nameOfSelfStruct = CUtil.reactorRef(instance); - - // Initialize the trigger for network output control reactions if it doesn't exist. - if (federate.networkOutputControlReactionsTrigger != null) { - builder.append("_fed.trigger_for_network_output_control_reactions=&" - + nameOfSelfStruct - + "->_lf__outputControlReactionTrigger;\n"); - } - - return builder.toString(); - } - - /** - * Create a port status field variable for a network input port "input" in - * the self struct of a reactor. - * - * @param input The network input port - * @return A string containing the appropriate variable - */ - public static String createPortStatusFieldForInput(Input input) { - StringBuilder builder = new StringBuilder(); - // Check if the port is a multiport - if (ASTUtils.isMultiport(input)) { - // If it is a multiport, then create an auxiliary list of port - // triggers for each channel of - // the multiport to keep track of the status of each channel - // individually - builder.append("trigger_t* _lf__" + input.getName() - + "_network_port_status;\n"); - } else { - // If it is not a multiport, then we could re-use the port trigger, - // and nothing needs to be - // done - } - return builder.toString(); - } - - /** - * Given a connection 'delay' predicate, return a string that represents the - * interval_t value of the additional delay that needs to be applied to the - * outgoing message. - * - * The returned additional delay in absence of after on network connection - * (i.e., if delay is passed as a null) is NEVER. This has a special - * meaning in C library functions that send network messages that carry - * timestamps (@see send_timed_message and send_port_absent_to_federate - * in lib/core/federate.c). In this case, the sender will send its current - * tag as the timestamp of the outgoing message without adding a microstep delay. - * If the user has assigned an after delay to the network connection (that - * can be zero) either as a time value (e.g., 200 msec) or as a literal - * (e.g., a parameter), that delay in nsec will be returned. - * - * @param delay - * @param generator - * @return - */ - public static String getNetworkDelayLiteral(Expression delay) { - String additionalDelayString = "NEVER"; - if (delay != null) { - TimeValue tv; - if (delay instanceof ParameterReference) { - // The parameter has to be parameter of the main reactor. - // And that value has to be a Time. - tv = ASTUtils.getDefaultAsTimeValue(((ParameterReference)delay).getParameter()); - } else { - tv = ASTUtils.getLiteralTimeValue(delay); - } - additionalDelayString = Long.toString(tv.toNanoSeconds()); - } - return additionalDelayString; - } - -} diff --git a/org.lflang/src/org/lflang/federated/FedASTUtils.java b/org.lflang/src/org/lflang/federated/FedASTUtils.java deleted file mode 100644 index 44a471d884..0000000000 --- a/org.lflang/src/org/lflang/federated/FedASTUtils.java +++ /dev/null @@ -1,778 +0,0 @@ -/************* - * Copyright (c) 2021, The University of California at Berkeley. - * Copyright (c) 2021, The University of Texas at Dallas. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - ***************/ - -package org.lflang.federated; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.eclipse.emf.ecore.util.EcoreUtil; -import org.lflang.ASTUtils; -import org.lflang.InferredType; -import org.lflang.TargetProperty.CoordinationType; -import org.lflang.TimeValue; -import org.lflang.federated.serialization.SupportedSerializers; -import org.lflang.generator.GeneratorBase; -import org.lflang.generator.PortInstance; -import org.lflang.lf.Action; -import org.lflang.lf.ActionOrigin; -import org.lflang.lf.Connection; -import org.lflang.lf.Expression; -import org.lflang.lf.Instantiation; -import org.lflang.lf.LfFactory; -import org.lflang.lf.ParameterReference; -import org.lflang.lf.Reaction; -import org.lflang.lf.Reactor; -import org.lflang.lf.Type; -import org.lflang.lf.VarRef; -import org.lflang.lf.Variable; - -/** - * A helper class for AST transformations needed for federated - * execution. - * - * @author Soroush Bateni - * @author Edward A. Lee - * - */ -public class FedASTUtils { - - /** - * Return a null-safe List - * - * @param The type of the list - * @param list The potentially null List - * @return Empty list or the original list - */ - public static List safe(List list) { - return list == null ? Collections.emptyList() : list; - } - - /** - * Create a "network action" in the reactor that contains the given - * connection and return it. - * - * The purpose of this action is to serve as a trigger for a "network - * input reaction" that is responsible for relaying messages to the - * port that is on the receiving side of the given connection. The - * connection is assumed to be between two reactors that reside in - * distinct federates. Hence, the container of the connection is - * assumed to be top-level. - * - * @param connection A connection between to federates. - * @param serializer The serializer used on the connection. - * @param type The type of the source port (indicating the type of - * data to be received). - * @param networkBufferType The type of the buffer used for network - * communication in the target (e.g., uint8_t* in C). - * @return The newly created action. - */ - private static Action createNetworkAction( - Connection connection, - SupportedSerializers serializer, - Type type, - String networkBufferType - ) { - Reactor top = (Reactor) connection.eContainer(); - LfFactory factory = LfFactory.eINSTANCE; - - Action action = factory.createAction(); - // Name the newly created action; set its delay and type. - action.setName(ASTUtils.getUniqueIdentifier(top, "networkMessage")); - if (serializer == SupportedSerializers.NATIVE) { - action.setType(type); - } else { - Type action_type = factory.createType(); - action_type.setId(networkBufferType); - action.setType(action_type); - } - - // The connection is 'physical' if it uses the ~> notation. - if (connection.isPhysical()) { - action.setOrigin(ActionOrigin.PHYSICAL); - // Messages sent on physical connections do not - // carry a timestamp, or a delay. The delay - // provided using after is enforced by setting - // the minDelay. - if (connection.getDelay() != null) { - action.setMinDelay(connection.getDelay()); - } - } else { - action.setOrigin(ActionOrigin.LOGICAL); - } - - return action; - } - - /** - * Add a network receiver reaction for a given input port 'destination' to - * destination's parent reactor. This reaction will react to a generated - * 'networkAction' (triggered asynchronously, e.g., by federate.c). This - * 'networkAction' will contain the actual message that is sent by the sender - * in 'action->value'. This value is forwarded to 'destination' in the network - * receiver reaction. - * - * @note: Used in federated execution - * - * @param networkAction The network action (also, @see createNetworkAction) - * @param source The source port instance. - * @param destination The destination port instance. - * @param connection The network connection. - * @param sourceFederate The source federate. - * @param destinationFederate The destination federate. - * @param rightBankIndex The right bank index or -1 if the right reactor is not in a bank. - * @param rightChannelIndex The right channel index or -1 if the right port is not a multiport. - * @param generator The GeneratorBase instance used to perform some target-specific actions - * @param coordination One of CoordinationType.DECENTRALIZED or CoordinationType.CENTRALIZED. - * @param serializer The serializer used on the connection - */ - private static void addNetworkReceiverReaction( - Action networkAction, - PortInstance source, - PortInstance destination, - Connection connection, - FederateInstance sourceFederate, - FederateInstance destinationFederate, - int rightBankIndex, - int rightChannelIndex, - GeneratorBase generator, - CoordinationType coordination, - SupportedSerializers serializer - ) { - LfFactory factory = LfFactory.eINSTANCE; - VarRef sourceRef = factory.createVarRef(); - VarRef destRef = factory.createVarRef(); - Reactor parent = (Reactor)connection.eContainer(); - Reaction networkReceiverReaction = factory.createReaction(); - - // These reactions do not require any dependency relationship - // to other reactions in the container. - generator.makeUnordered(networkReceiverReaction); - - // If the sender or receiver is in a bank of reactors, then we want - // these reactions to appear only in the federate whose bank ID matches. - generator.setReactionBankIndex(networkReceiverReaction, rightBankIndex); - - // The connection is 'physical' if it uses the ~> notation. - if (connection.isPhysical()) { - destinationFederate.inboundP2PConnections.add(sourceFederate); - } else { - // If the connection is logical but coordination - // is decentralized, we would need - // to make P2P connections - if (coordination == CoordinationType.DECENTRALIZED) { - destinationFederate.inboundP2PConnections.add(sourceFederate); - } - } - - // Record this action in the right federate. - // The ID of the receiving port (rightPort) is the position - // of the action in this list. - int receivingPortID = destinationFederate.networkMessageActions.size(); - - // Establish references to the involved ports. - sourceRef.setContainer(source.getParent().getDefinition()); - sourceRef.setVariable(source.getDefinition()); - destRef.setContainer(destination.getParent().getDefinition()); - destRef.setVariable(destination.getDefinition()); - - if (!connection.isPhysical() && connection.getDelay() == null) { - // If the connection is not physical and there is no delay, - // add the original output port of the source federate - // as a trigger to keep the overall dependency structure. - // This is useful when assigning levels. - VarRef senderOutputPort = factory.createVarRef(); - senderOutputPort.setContainer(source.getParent().getDefinition()); - senderOutputPort.setVariable(source.getDefinition()); - networkReceiverReaction.getTriggers().add(senderOutputPort); - - // Add this trigger to the list of disconnected network reaction triggers - destinationFederate.remoteNetworkReactionTriggers.add(senderOutputPort); - } - - // Add the input port at the receiver federate reactor as an effect - networkReceiverReaction.getEffects().add(destRef); - - VarRef triggerRef = factory.createVarRef(); - // Establish references to the action. - triggerRef.setVariable(networkAction); - // Add the action as a trigger to the receiver reaction - networkReceiverReaction.getTriggers().add(triggerRef); - - // Generate code for the network receiver reaction - networkReceiverReaction.setCode(factory.createCode()); - networkReceiverReaction.getCode().setBody(generator.generateNetworkReceiverBody( - networkAction, - sourceRef, - destRef, - receivingPortID, - sourceFederate, - destinationFederate, - rightBankIndex, - rightChannelIndex, - ASTUtils.getInferredType(networkAction), - connection.isPhysical(), - serializer - )); - - // Add the receiver reaction to the parent - parent.getReactions().add(networkReceiverReaction); - - // Add the network receiver reaction to the federate instance's list - // of network reactions - destinationFederate.networkReactions.add(networkReceiverReaction); - } - - /** - * Add a network control reaction for a given input port 'destination' to - * destination's parent reactor. This reaction will block for - * any valid logical time until it is known whether the trigger for the - * action corresponding to the given port is present or absent. - * - * @note Used in federated execution - * - * @param source The output port of the source federate reactor. - * Added as a trigger to the network control reaction to preserve the - * overall dependency structure of the program across federates. - * @param destination The input port of the destination federate reactor. - * @param connection The network connection. - * @param recevingPortID The ID of the receiving port - * @param bankIndex The bank index of the receiving federate, or -1 if not in a bank. - * @param instance The federate instance is used to keep track of all - * network input ports globally - * @param generator The GeneratorBase instance used to perform some target-specific actions - */ - private static void addNetworkInputControlReaction( - PortInstance source, - PortInstance destination, - Connection connection, - int recevingPortID, - int bankIndex, - FederateInstance instance, - GeneratorBase generator - ) { - - LfFactory factory = LfFactory.eINSTANCE; - Reaction reaction = factory.createReaction(); - VarRef destRef = factory.createVarRef(); - - // If the sender or receiver is in a bank of reactors, then we want - // these reactions to appear only in the federate whose bank ID matches. - generator.setReactionBankIndex(reaction, bankIndex); - - // Create a new action that will be used to trigger the - // input control reactions. - Action newTriggerForControlReactionInput = factory.createAction(); - newTriggerForControlReactionInput.setOrigin(ActionOrigin.LOGICAL); - - // Set the container and variable according to the network port - destRef.setContainer(destination.getParent().getDefinition()); - destRef.setVariable(destination.getDefinition()); - - Reactor top = destination.getParent().getParent().reactorDefinition; - - newTriggerForControlReactionInput.setName( - ASTUtils.getUniqueIdentifier(top, "inputControlReactionTrigger")); - - // Add the newly created Action to the action list of the federated reactor. - top.getActions().add(newTriggerForControlReactionInput); - - // Create the trigger for the reaction - VarRef newTriggerForControlReaction = factory.createVarRef(); - newTriggerForControlReaction.setVariable(newTriggerForControlReactionInput); - - // Add the appropriate triggers to the list of triggers of the reaction - reaction.getTriggers().add(newTriggerForControlReaction); - - - // Add the original output port of the source federate - // as a trigger to keep the overall dependency structure. - // This is useful when assigning levels. - VarRef sourceRef = factory.createVarRef(); - - sourceRef.setContainer(source.getParent().getDefinition()); - sourceRef.setVariable(source.getDefinition()); - reaction.getTriggers().add(sourceRef); - // Add this trigger to the list of disconnected network reaction triggers - instance.remoteNetworkReactionTriggers.add(sourceRef); - - // Add the destination port as an effect of the reaction - reaction.getEffects().add(destRef); - - // Generate code for the network input control reaction - reaction.setCode(factory.createCode()); - - TimeValue maxSTP = findMaxSTP( - destination.getDefinition(), - instance, - generator, - destination.getParent().reactorDefinition - ); - - reaction.getCode() - .setBody(generator.generateNetworkInputControlReactionBody( - recevingPortID, maxSTP)); - - generator.makeUnordered(reaction); - - // Insert the reaction - top.getReactions().add(reaction); - - // Add the trigger for this reaction to the list of triggers, used to actually - // trigger the reaction at the beginning of each logical time. - instance.networkInputControlReactionsTriggers.add(newTriggerForControlReactionInput); - - // Add the network input control reaction to the federate instance's list - // of network reactions - instance.networkReactions.add(reaction); - } - - /** - * Find the maximum STP offset for the given 'port'. - * - * An STP offset predicate can be nested in contained reactors in - * the federate. - * @param port The port to generate the STP list for. - * @param generator The GeneratorBase instance used to perform some target-specific actions - * @param reactor The top-level reactor (not the federate reactor) - * @return The maximum STP as a TimeValue - */ - private static TimeValue findMaxSTP(Variable port, - FederateInstance instance, - GeneratorBase generator, Reactor reactor) { - // Find a list of STP offsets (if any exists) - List STPList = new LinkedList<>(); - - // First, check if there are any connections to contained reactors that - // need to be handled - List connectionsWithPort = ASTUtils - .allConnections(reactor).stream().filter(c -> c.getLeftPorts() - .stream() - .anyMatch((VarRef v) -> v - .getVariable().equals(port))) - .collect(Collectors.toList()); - - - // Find the list of reactions that have the port as trigger or source - // (could be a variable name) - List reactionsWithPort = ASTUtils - .allReactions(reactor).stream().filter(r -> { - // Check the triggers of reaction r first - return r.getTriggers().stream().anyMatch(t -> { - if (t instanceof VarRef) { - // Check if the variables match - return ((VarRef) t).getVariable() == port; - } else { - // Not a network port (startup or shutdown) - return false; - } - }) || // Then check the sources of reaction r - r.getSources().stream().anyMatch(s -> s.getVariable() == port); - }).collect(Collectors.toList()); - - // Find a list of STP offsets (if any exists) - if (generator.isFederatedAndDecentralized()) { - for (Reaction r : safe(reactionsWithPort)) { - if (!instance.contains(r)) { - continue; - } - // If STP offset is determined, add it - // If not, assume it is zero - if (r.getStp() != null) { - if (r.getStp().getValue() instanceof ParameterReference) { - List instantList = new ArrayList<>(); - instantList.add(instance.instantiation); - final var param = ((ParameterReference)r.getStp().getValue()).getParameter(); - STPList.addAll(ASTUtils.initialValue(param, instantList)); - } else { - STPList.add(r.getStp().getValue()); - } - } - } - // Check the children for STPs as well - for (Connection c : safe(connectionsWithPort)) { - VarRef childPort = c.getRightPorts().get(0); - Reactor childReactor = (Reactor) childPort.getVariable() - .eContainer(); - // Find the list of reactions that have the port as trigger or - // source (could be a variable name) - List childReactionsWithPort = ASTUtils - .allReactions(childReactor).stream().filter(r -> r.getTriggers().stream().anyMatch(t -> { - if (t instanceof VarRef) { - // Check if the variables match - return ((VarRef) t) - .getVariable() == childPort - .getVariable(); - } else { - // Not a network port (startup or shutdown) - return false; - } - }) || r.getSources().stream().anyMatch(s -> s.getVariable() == childPort - .getVariable())).collect(Collectors.toList()); - - for (Reaction r : safe(childReactionsWithPort)) { - if (!instance.contains(r)) { - continue; - } - // If STP offset is determined, add it - // If not, assume it is zero - if (r.getStp() != null) { - if (r.getStp().getValue() instanceof ParameterReference) { - List instantList = new ArrayList<>(); - instantList.add(childPort.getContainer()); - final var param = ((ParameterReference)r.getStp().getValue()).getParameter(); - STPList.addAll(ASTUtils.initialValue(param, instantList)); - } else { - STPList.add(r.getStp().getValue()); - } - } - } - } - } - - return STPList.stream() - .map(ASTUtils::getLiteralTimeValue) - .filter(Objects::nonNull) - .reduce(TimeValue.ZERO, TimeValue::max); - } - - /** - * Add a network sender reaction for a given input port 'source' to - * source's parent reactor. This reaction will react to the 'source' - * and then send a message on the network destined for the destinationFederate. - * - * @note Used in federated execution - * - * @param source The source port instance. - * @param destination The destination port instance. - * @param connection The network connection. - * @param sourceFederate The source federate. - * @param leftBankIndex The left bank index or -1 if the left reactor is not in a bank. - * @param leftChannelIndex The left channel index or -1 if the left port is not a multiport. - * @param destinationFederate The destination federate. - * @param generator The GeneratorBase instance used to perform some target-specific actions - * @param coordination One of CoordinationType.DECENTRALIZED or CoordinationType.CENTRALIZED. - * @param serializer The serializer used on the connection - */ - private static void addNetworkSenderReaction( - PortInstance source, - PortInstance destination, - Connection connection, - FederateInstance sourceFederate, - int leftBankIndex, - int leftChannelIndex, - FederateInstance destinationFederate, - GeneratorBase generator, - CoordinationType coordination, - SupportedSerializers serializer - ) { - LfFactory factory = LfFactory.eINSTANCE; - // Assume all the types are the same, so just use the first on the right. - Type type = EcoreUtil.copy(source.getDefinition().getType()); - VarRef sourceRef = factory.createVarRef(); - VarRef destRef = factory.createVarRef(); - Reactor parent = (Reactor)connection.eContainer(); - Reaction networkSenderReaction = factory.createReaction(); - - // These reactions do not require any dependency relationship - // to other reactions in the container. - generator.makeUnordered(networkSenderReaction); - - // If the sender or receiver is in a bank of reactors, then we want - // these reactions to appear only in the federate whose bank ID matches. - generator.setReactionBankIndex(networkSenderReaction, leftBankIndex); - - // The connection is 'physical' if it uses the ~> notation. - if (connection.isPhysical()) { - sourceFederate.outboundP2PConnections.add(destinationFederate); - } else { - // If the connection is logical but coordination - // is decentralized, we would need - // to make P2P connections - if (coordination == CoordinationType.DECENTRALIZED) { - sourceFederate.outboundP2PConnections.add(destinationFederate); - } - } - - // Record this action in the right federate. - // The ID of the receiving port (rightPort) is the position - // of the action in this list. - int receivingPortID = destinationFederate.networkMessageActions.size(); - - - // Establish references to the involved ports. - sourceRef.setContainer(source.getParent().getDefinition()); - sourceRef.setVariable(source.getDefinition()); - destRef.setContainer(destination.getParent().getDefinition()); - destRef.setVariable(destination.getDefinition()); - - // Configure the sending reaction. - networkSenderReaction.getTriggers().add(sourceRef); - networkSenderReaction.setCode(factory.createCode()); - networkSenderReaction.getCode().setBody(generator.generateNetworkSenderBody( - sourceRef, - destRef, - receivingPortID, - sourceFederate, - leftBankIndex, - leftChannelIndex, - destinationFederate, - InferredType.fromAST(type), - connection.isPhysical(), - connection.getDelay(), - serializer - )); - - // Add the sending reaction to the parent. - parent.getReactions().add(networkSenderReaction); - - // Add the network sender reaction to the federate instance's list - // of network reactions - sourceFederate.networkReactions.add(networkSenderReaction); - } - - /** - * Add a network control reaction for a given output port 'source' to - * source's parent reactor. This reaction will send a port absent - * message if the status of the output port is absent. - * - * @note Used in federated execution - * - * @param source The output port of the source federate - * @param instance The federate instance is used to keep track of all - * network reactions and some relevant triggers - * @param receivingPortID The ID of the receiving port - * @param channelIndex The channel index of the sending port, if it is a multiport. - * @param bankIndex The bank index of the sending federate, if it is a bank. - * @param receivingFedID The ID of destination federate. - * @param generator The GeneratorBase instance used to perform some target-specific actions - * @param delay The delay value imposed on the connection using after - */ - private static void addNetworkOutputControlReaction( - PortInstance source, - FederateInstance instance, - int receivingPortID, - int bankIndex, - int channelIndex, - int receivingFedID, - GeneratorBase generator, - Expression delay - ) { - LfFactory factory = LfFactory.eINSTANCE; - Reaction reaction = factory.createReaction(); - Reactor top = source.getParent().getParent().reactorDefinition; // Top-level reactor. - - // If the sender or receiver is in a bank of reactors, then we want - // these reactions to appear only in the federate whose bank ID matches. - generator.setReactionBankIndex(reaction, bankIndex); - - // Add the output from the contained reactor as a source to - // the reaction to preserve precedence order. - VarRef newPortRef = factory.createVarRef(); - newPortRef.setContainer(source.getParent().getDefinition()); - newPortRef.setVariable(source.getDefinition()); - reaction.getSources().add(newPortRef); - - // We use an action at the top-level to manually - // trigger output control reactions. That action is created once - // and recorded in the federate instance. - // Check whether the action already has been created. - if (instance.networkOutputControlReactionsTrigger == null) { - // The port has not been created. - String triggerName = "outputControlReactionTrigger"; - - // Find the trigger definition in the reactor definition, which could have been - // generated for another federate instance if there are multiple instances - // of the same reactor that are each distinct federates. - Optional optTriggerInput - = top.getActions().stream().filter( - I -> I.getName().equals(triggerName)).findFirst(); - - if (optTriggerInput.isEmpty()) { - // If no trigger with the name "outputControlReactionTrigger" is - // already added to the reactor definition, we need to create it - // for the first time. The trigger is a logical action. - Action newTriggerForControlReactionVariable = factory.createAction(); - newTriggerForControlReactionVariable.setName(triggerName); - newTriggerForControlReactionVariable.setOrigin(ActionOrigin.LOGICAL); - top.getActions().add(newTriggerForControlReactionVariable); - - // Now that the variable is created, store it in the federate instance - instance.networkOutputControlReactionsTrigger - = newTriggerForControlReactionVariable; - } else { - // If the "outputControlReactionTrigger" trigger is already - // there, we can re-use it for this new reaction since a single trigger - // will trigger - // all network output control reactions. - instance.networkOutputControlReactionsTrigger = optTriggerInput.get(); - } - } - - // Add the trigger for all output control reactions to the list of triggers - VarRef triggerRef = factory.createVarRef(); - triggerRef.setVariable(instance.networkOutputControlReactionsTrigger); - reaction.getTriggers().add(triggerRef); - - // Generate the code - reaction.setCode(factory.createCode()); - - reaction.getCode().setBody( - generator.generateNetworkOutputControlReactionBody(newPortRef, - receivingPortID, receivingFedID, bankIndex, channelIndex, delay)); - - // Make the reaction unordered w.r.t. other reactions in the top level. - generator.makeUnordered(reaction); - - // Insert the newly generated reaction after the generated sender and - // receiver top-level reactions. - top.getReactions().add(reaction); - - // Add the network output control reaction to the federate instance's list - // of network reactions - instance.networkReactions.add(reaction); - } - - /** - * Replace the specified connection with communication between federates. - * @param source The source port instance. - * @param destination The destination port instance. - * @param connection The connection. - * @param sourceFederate The source federate. - * @param leftBankIndex The left bank index or -1 if the left reactor is not in a bank. - * @param leftChannelIndex The left channel index or -1 if the left port is not a multiport. - * @param destinationFederate The destination federate. - * @param rightBankIndex The right bank index or -1 if the right reactor is not in a bank. - * @param rightChannelIndex The right channel index or -1 if the right port is not a multiport. - * @param generator The GeneratorBase instance used to perform some target-specific actions - * @param coordination One of CoordinationType.DECENTRALIZED or CoordinationType.CENTRALIZED. - */ - public static void makeCommunication( - PortInstance source, - PortInstance destination, - Connection connection, - FederateInstance sourceFederate, - int leftBankIndex, - int leftChannelIndex, - FederateInstance destinationFederate, - int rightBankIndex, - int rightChannelIndex, - GeneratorBase generator, - CoordinationType coordination - ) { - // Get the serializer - var serializer = SupportedSerializers.NATIVE; - if (connection.getSerializer() != null) { - serializer = SupportedSerializers.valueOf( - connection.getSerializer().getType().toUpperCase() - ); - } - // Add it to the list of enabled serializers - generator.enabledSerializers.add(serializer); - - // Add the sender reaction. - addNetworkSenderReaction( - source, - destination, - connection, - sourceFederate, - leftBankIndex, - leftChannelIndex, - destinationFederate, - generator, - coordination, - serializer - ); - - // Next, generate control reactions - if ( - !connection.isPhysical() && // Connections that are physical don't need control reactions - connection.getDelay() == null // Connections that have delays don't need control reactions - ) { - - // The ID of the receiving port (rightPort) is the position - // of the networkAction (see below) in this list. - int receivingPortID = destinationFederate.networkMessageActions.size(); - - // Add the network output control reaction to the parent - FedASTUtils.addNetworkOutputControlReaction( - source, - sourceFederate, - receivingPortID, - leftBankIndex, - leftChannelIndex, - destinationFederate.id, - generator, - connection.getDelay() - ); - - // Add the network input control reaction to the parent - FedASTUtils.addNetworkInputControlReaction( - source, - destination, - connection, - receivingPortID, - rightBankIndex, - destinationFederate, - generator - ); - } - - // Create the network action (@see createNetworkAction) - Action networkAction = createNetworkAction( - connection, - serializer, - EcoreUtil.copy(source.getDefinition().getType()), - generator.getNetworkBufferType()); - - // Keep track of this action in the destination federate. - destinationFederate.networkMessageActions.add(networkAction); - - // Add the action definition to the parent reactor. - ((Reactor)connection.eContainer()).getActions().add(networkAction); - - // Add the network receiver reaction in the destinationFederate - addNetworkReceiverReaction( - networkAction, - source, - destination, - connection, - sourceFederate, - destinationFederate, - rightBankIndex, - rightChannelIndex, - generator, - coordination, - serializer - ); - } -} diff --git a/org.lflang/src/org/lflang/federated/FedFileConfig.java b/org.lflang/src/org/lflang/federated/FedFileConfig.java deleted file mode 100644 index b6bd0c61b6..0000000000 --- a/org.lflang/src/org/lflang/federated/FedFileConfig.java +++ /dev/null @@ -1,61 +0,0 @@ -/************* - * Copyright (c) 2019-2021, The University of California at Berkeley. - - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ***************/ - -package org.lflang.federated; - -import java.io.IOException; - -import org.lflang.FileConfig; - -/** - * A child class of @see FileConfig that extends the base functionality to add support - * for federated execution. The code generator should create one instance of this class - * for each federate. - * - * @author Soroush Bateni - * - */ -public class FedFileConfig extends FileConfig { - - /** Name of the federate for this FedFileConfig */ - protected final String federateName; - - /** - * Create an instance of FedFileConfig for federate 'federateName' from an existing - * 'fileConfig' instance (an instance of 'FileConfig'). - * - * @param fileConfig The existing instance of the 'FileConfig' class. - * @param federateName The name of the federate. - * @throws IOException - */ - public FedFileConfig(final FileConfig fileConfig, final String federateName) throws IOException { - super(fileConfig.resource, fileConfig.getSrcGenBasePath(), fileConfig.useHierarchicalBin); - - this.federateName = federateName; - // The generated code for each federate should be located at fileConfig.srcGenPath + "/federateName/" - this.srcGenPath = fileConfig.getSrcGenPath().resolve(federateName); - } - -} diff --git a/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java b/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java deleted file mode 100644 index 278b48e1ec..0000000000 --- a/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java +++ /dev/null @@ -1,209 +0,0 @@ -/************* - * Copyright (c) 2021, The University of Texas at Dallas. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - ***************/ - -package org.lflang.federated; - -import org.lflang.InferredType; -import org.lflang.ASTUtils; -import org.lflang.TargetProperty.CoordinationType; -import org.lflang.federated.serialization.FedNativePythonSerialization; -import org.lflang.federated.serialization.FedSerialization; -import org.lflang.federated.serialization.SupportedSerializers; -import org.lflang.generator.c.CUtil; -import org.lflang.lf.Action; -import org.lflang.lf.Expression; -import org.lflang.lf.VarRef; - -/** - * An extension class to the PythonGenerator that enables certain federated - * functionalities. - * - * @author Soroush Bateni - * - */ -public class PythonGeneratorExtension { - /** - * Generate code for the body of a reaction that handles an output - * that is to be sent over the network. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The variable reference to the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. - * @param receivingFed The destination federate. - * @param type The type. - * @param isPhysical Indicates whether the connection is physical or not - * @param delay The delay value imposed on the connection using after - * @param serializer The serializer used on the connection. - * @param generator The instance of the PythonGenerator. - */ - public static String generateNetworkSenderBody( - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - int sendingBankIndex, - int sendingChannelIndex, - FederateInstance receivingFed, - InferredType type, - boolean isPhysical, - Expression delay, - SupportedSerializers serializer, - CoordinationType coordinationType - ) { - String sendRef = CUtil.portRefInReaction(sendingPort, sendingBankIndex, sendingChannelIndex); - String receiveRef = ASTUtils.generateVarRef(receivingPort); // Used for comments only, so no need for bank/multiport index. - StringBuilder result = new StringBuilder(); - result.append("// Sending from " + sendRef + - " in federate " + sendingFed.name + " to " + receiveRef + " in federate " + receivingFed.name + "\n"); - // If the connection is physical and the receiving federate is remote, send it directly on a socket. - // If the connection is logical and the coordination mode is centralized, send via RTI. - // If the connection is logical and the coordination mode is decentralized, send directly - String messageType; - // Name of the next immediate destination of this message - String next_destination_name = "\"federate " + receivingFed.id + "\""; - - // Get the delay literal - String additionalDelayString = CGeneratorExtension.getNetworkDelayLiteral(delay); - - if (isPhysical) { - messageType = "MSG_TYPE_P2P_MESSAGE"; - } else if (coordinationType == CoordinationType.DECENTRALIZED) { - messageType = "MSG_TYPE_P2P_TAGGED_MESSAGE"; - } else { - // Logical connection - // Send the message via rti - messageType = "MSG_TYPE_TAGGED_MESSAGE"; - next_destination_name = "\"federate " + receivingFed.id + " via the RTI\""; - } - - - String sendingFunction = "send_timed_message"; - String commonArgs = additionalDelayString + ", " - + messageType + ", " - + receivingPortID + ", " - + receivingFed.id + ", " - + next_destination_name + ", " - + "message_length"; - if (isPhysical) { - // Messages going on a physical connection do not - // carry a timestamp or require the delay; - sendingFunction = "send_message"; - commonArgs = messageType + ", " + receivingPortID + ", " + receivingFed.id + ", " - + next_destination_name + ", message_length"; - } - - String lengthExpression = ""; - String pointerExpression = ""; - switch (serializer) { - case NATIVE: { - var variableToSerialize = sendRef+"->value"; - FedNativePythonSerialization pickler = new FedNativePythonSerialization(); - lengthExpression = pickler.serializedBufferLength(); - pointerExpression = pickler.seializedBufferVar(); - result.append( - pickler.generateNetworkSerializerCode(variableToSerialize, null) - ); - result.append("size_t message_length = "+lengthExpression+";\n"); - result.append(sendingFunction+"("+commonArgs+", "+pointerExpression+");\n"); - break; - } - case PROTO: { - throw new UnsupportedOperationException("Protbuf serialization is not supported yet."); - } - case ROS2: { - throw new UnsupportedOperationException("ROS2 serialization is not supported yet."); - } - - } - return result.toString(); - } - - /** - * Generate code for the body of a reaction that handles the - * action that is triggered by receiving a message from a remote - * federate. - * @param action The action. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param receivingFed The destination federate. - * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. - * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. - * @param type The type. - * @param isPhysical Indicates whether or not the connection is physical - * @param serializer The serializer used on the connection. - * @param coordinationType The coordination type - */ - public static String generateNetworkReceiverBody( - Action action, - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - FederateInstance receivingFed, - int receivingBankIndex, - int receivingChannelIndex, - InferredType type, - boolean isPhysical, - SupportedSerializers serializer, - CoordinationType coordinationType - ) { - - String receiveRef = CUtil.portRefInReaction(receivingPort, receivingBankIndex, receivingChannelIndex); - StringBuilder result = new StringBuilder(); - - // Transfer the physical time of arrival from the action to the port - result.append(receiveRef+"->physical_time_of_arrival = self->_lf__"+action.getName()+".physical_time_of_arrival;\n"); - - if (coordinationType == CoordinationType.DECENTRALIZED && !isPhysical) { - // Transfer the intended tag. - result.append(receiveRef+"->intended_tag = self->_lf__"+action.getName()+".intended_tag;\n"); - } - - String value = ""; - switch (serializer) { - case NATIVE: { - value = action.getName(); - FedNativePythonSerialization pickler = new FedNativePythonSerialization(); - result.append(pickler.generateNetworkDeserializerCode(value, null)); - result.append("lf_set("+receiveRef+", "+FedSerialization.deserializedVarName+");\n"); - break; - } - case PROTO: { - throw new UnsupportedOperationException("Protbuf serialization is not supported yet."); - } - case ROS2: { - throw new UnsupportedOperationException("ROS2 serialization is not supported yet."); - } - - } - - return result.toString(); - } -} diff --git a/org.lflang/src/org/lflang/federated/extensions/CExtension.java b/org.lflang/src/org/lflang/federated/extensions/CExtension.java new file mode 100644 index 0000000000..8e4cb668aa --- /dev/null +++ b/org.lflang/src/org/lflang/federated/extensions/CExtension.java @@ -0,0 +1,763 @@ +/************* + * Copyright (c) 2021, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.federated.extensions; + +import static org.lflang.util.StringUtil.addDoubleQuotes; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; + +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.InferredType; +import org.lflang.TargetProperty; +import org.lflang.TargetProperty.CoordinationType; +import org.lflang.TimeValue; +import org.lflang.federated.generator.FedASTUtils; +import org.lflang.federated.generator.FedConnectionInstance; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; +import org.lflang.federated.serialization.FedROS2CPPSerialization; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.GeneratorBase; +import org.lflang.generator.GeneratorUtils; +import org.lflang.generator.LFGeneratorContext; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.c.CGenerator; +import org.lflang.generator.c.CTypes; +import org.lflang.generator.c.CUtil; +import org.lflang.lf.Action; +import org.lflang.lf.Output; +import org.lflang.lf.Port; +import org.lflang.lf.VarRef; +import org.lflang.util.FileUtil; + +/** + * An extension class to the CGenerator that enables certain federated + * functionalities. Currently, this class offers the following features: + * + * - Allocating and initializing C structures for federated communication - + * Creating status field for network input ports that help the receiver logic in + * federate.c communicate the status of a network input port with network input + * control reactions. + * + * @author {Soroush Bateni } + * @author {Hou Seng Wong } + * @author {Billy Bao } + * + */ +public class CExtension implements FedTargetExtension { + + @Override + public void initializeTargetConfig( + LFGeneratorContext context, + int numOfFederates, FederateInstance federate, + FedFileConfig fileConfig, + ErrorReporter errorReporter, + LinkedHashMap federationRTIProperties + ) throws IOException { + if(GeneratorUtils.isHostWindows()) { + errorReporter.reportError( + "Federated LF programs with a C target are currently not supported on Windows. " + + "Exiting code generation." + ); + // Return to avoid compiler errors + return; + } + + CExtensionUtils.handleCompileDefinitions(federate, numOfFederates, federationRTIProperties); + + generateCMakeInclude(federate, fileConfig); + + federate.targetConfig.fileNames.add("include/federated"); + federate.targetConfig.setByUser.add(TargetProperty.FILES); + FileUtil.copyDirectoryFromClassPath( + "/lib/c/reactor-c/core/federated", + fileConfig.getSrcPath().resolve("include" + File.separator + "federated"), + true + ); + + federate.targetConfig.keepalive = true; + federate.targetConfig.setByUser.add(TargetProperty.KEEPALIVE); + + // If there are federates, copy the required files for that. + // Also, create the RTI C file and the launcher script. + // Handle target parameters. + // If the program is federated, then ensure that threading is enabled. + federate.targetConfig.threading = true; + federate.targetConfig.setByUser.add(TargetProperty.THREADING); + + // Include the fed setup file for this federate in the target property + String relPath = "include" + File.separator + "_" + federate.name + "_preamble.c"; + federate.targetConfig.fedSetupPreamble = relPath; + federate.targetConfig.setByUser.add(TargetProperty.FED_SETUP); + } + + /** + * Generate a cmake-include file for {@code federate} if needed. + */ + protected void generateCMakeInclude(FederateInstance federate, FedFileConfig fileConfig) throws IOException { + CExtensionUtils.generateCMakeInclude(federate, fileConfig); + } + + /** + * Generate code for the body of a reaction that handles the + * action that is triggered by receiving a message from a remote + * federate. + * @param action The action. + * @param sendingPort The output port providing the data to send. + * @param receivingPort The ID of the destination port. + * @param connection FIXME + * @param type FIXME + * @param coordinationType The coordination type + * @param errorReporter + */ + public String generateNetworkReceiverBody( + Action action, + VarRef sendingPort, + VarRef receivingPort, + FedConnectionInstance connection, + InferredType type, + CoordinationType coordinationType, + ErrorReporter errorReporter + ) { + var receiveRef = CUtil.portRefInReaction(receivingPort, connection.getDstBank(), connection.getDstChannel()); + var result = new CodeBuilder(); + // We currently have no way to mark a reaction "unordered" + // in the AST, so we use a magic string at the start of the body. + result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER); + // Transfer the physical time of arrival from the action to the port + result.pr(receiveRef+"->physical_time_of_arrival = self->_lf__"+action.getName()+".physical_time_of_arrival;"); + if (coordinationType == CoordinationType.DECENTRALIZED && !connection.getDefinition().isPhysical()) { + // Transfer the intended tag. + result.pr(receiveRef+"->intended_tag = self->_lf__"+action.getName()+".intended_tag;\n"); + } + + deserialize(action, receivingPort, connection, type, receiveRef, result, errorReporter); + return result.toString(); + } + + /** + * Generate code to deserialize a message received over the network. + * @param action The network action that is mapped to the {@code receivingPort} + * @param receivingPort The receiving port + * @param connection The connection used to receive the message + * @param type Type for the port + * @param receiveRef A target language reference to the receiving port + * @param result Used to put generated code in + * @param errorReporter Used to report errors, if any + */ + protected void deserialize( + Action action, + VarRef receivingPort, + FedConnectionInstance connection, + InferredType type, + String receiveRef, + CodeBuilder result, + ErrorReporter errorReporter + ) { + CTypes types = new CTypes(errorReporter); + // Adjust the type of the action and the receivingPort. + // If it is "string", then change it to "char*". + // This string is dynamically allocated, and type 'string' is to be + // used only for statically allocated strings. This would force the + // CGenerator to treat the port and action as token types. + if (types.getTargetType(action).equals("string")) { + action.getType().setCode(null); + action.getType().setId("char*"); + } + if (types.getTargetType((Port) receivingPort.getVariable()).equals("string")) { + ((Port) receivingPort.getVariable()).getType().setCode(null); + ((Port) receivingPort.getVariable()).getType().setId("char*"); + } + var value = ""; + switch (connection.getSerializer()) { + case NATIVE: { + // NOTE: Docs say that malloc'd char* is freed on conclusion of the time step. + // So passing it downstream should be OK. + value = action.getName()+"->value"; + if (CUtil.isTokenType(type, types)) { + result.pr("lf_set_token("+ receiveRef +", "+ action.getName()+"->token);"); + } else { + result.pr("lf_set("+ receiveRef +", "+value+");"); + } + break; + } + case PROTO: { + throw new UnsupportedOperationException("Protobuf serialization is not supported yet."); + } + case ROS2: { + var portType = ASTUtils.getInferredType(((Port) receivingPort.getVariable())); + var portTypeStr = types.getTargetType(portType); + if (CUtil.isTokenType(portType, types)) { + throw new UnsupportedOperationException("Cannot handle ROS serialization when ports are pointers."); + } else if (CExtensionUtils.isSharedPtrType(portType, types)) { + var matcher = CExtensionUtils.sharedPointerVariable.matcher(portTypeStr); + if (matcher.find()) { + portTypeStr = matcher.group("type"); + } + } + var ROSDeserializer = new FedROS2CPPSerialization(); + value = FedROS2CPPSerialization.deserializedVarName; + result.pr( + ROSDeserializer.generateNetworkDeserializerCode( + "self->_lf__"+ action.getName(), + portTypeStr + ) + ); + if (CExtensionUtils.isSharedPtrType(portType, types)) { + result.pr("auto msg_shared_ptr = std::make_shared<"+portTypeStr+">("+value+");"); + result.pr("lf_set("+ receiveRef +", msg_shared_ptr);"); + } else { + result.pr("lf_set("+ receiveRef +", std::move("+value+"));"); + } + break; + } + } + } + + /** + * Generate code for the body of a reaction that handles an output + * that is to be sent over the network. + * @param sendingPort The output port providing the data to send. + * @param receivingPort The variable reference to the destination port. + * @param connection + * @param type + * @param coordinationType + * @param errorReporter FIXME + */ + public String generateNetworkSenderBody( + VarRef sendingPort, + VarRef receivingPort, + FedConnectionInstance connection, + InferredType type, + CoordinationType coordinationType, + ErrorReporter errorReporter + ) { + var sendRef = CUtil.portRefInReaction(sendingPort, connection.getSrcBank(), connection.getSrcChannel()); + var receiveRef = ASTUtils.generateVarRef(receivingPort); // Used for comments only, so no need for bank/multiport index. + var result = new CodeBuilder(); + // The ID of the receiving port (rightPort) is the position + // of the action in this list. + int receivingPortID = connection.getDstFederate().networkMessageActions.size(); + + // We currently have no way to mark a reaction "unordered" + // in the AST, so we use a magic string at the start of the body. + result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); + + result.pr("// Sending from " + sendRef + " in federate " + + connection.getSrcFederate().name + " to " + receiveRef + + " in federate " + connection.getDstFederate().name); + + // In case sendRef is a multiport or is in a bank, this reaction will be triggered when any channel or bank index of sendRef is present + // ex. if a.out[i] is present, the entire output a.out is triggered. + if (connection.getSrcBank() != -1 || connection.getSrcChannel() != -1) { + result.pr("if (!"+sendRef+"->is_present) return;"); + } + + // If the connection is physical and the receiving federate is remote, send it directly on a socket. + // If the connection is logical and the coordination mode is centralized, send via RTI. + // If the connection is logical and the coordination mode is decentralized, send directly + String messageType; + // Name of the next immediate destination of this message + var next_destination_name = "\"federate "+connection.getDstFederate().id+"\""; + + // Get the delay literal + String additionalDelayString = CExtensionUtils.getNetworkDelayLiteral(connection.getDefinition().getDelay()); + + if (connection.getDefinition().isPhysical()) { + messageType = "MSG_TYPE_P2P_MESSAGE"; + } else if (coordinationType == CoordinationType.DECENTRALIZED) { + messageType = "MSG_TYPE_P2P_TAGGED_MESSAGE"; + } else { + // Logical connection + // Send the message via rti + messageType = "MSG_TYPE_TAGGED_MESSAGE"; + next_destination_name = "\"federate "+connection.getDstFederate().id+" via the RTI\""; + } + + + String sendingFunction = "send_timed_message"; + String commonArgs = String.join(", ", + additionalDelayString, + messageType, + receivingPortID + "", + connection.getDstFederate().id + "", + next_destination_name, + "message_length" + ); + if (connection.getDefinition().isPhysical()) { + // Messages going on a physical connection do not + // carry a timestamp or require the delay; + sendingFunction = "send_message"; + commonArgs = messageType+", "+receivingPortID+", "+connection.getDstFederate().id+", "+next_destination_name+", message_length"; + } + + serializeAndSend( + connection, + type, + sendRef, + result, + sendingFunction, + commonArgs, + errorReporter + ); + return result.toString(); + } + + /** + * FIXME + * @param connection + * @param type + * @param sendRef + * @param result + * @param sendingFunction + * @param commonArgs + * @param errorReporter + */ + protected void serializeAndSend( + FedConnectionInstance connection, + InferredType type, + String sendRef, + CodeBuilder result, + String sendingFunction, + String commonArgs, + ErrorReporter errorReporter + ) { + CTypes types = new CTypes(errorReporter); + var lengthExpression = ""; + var pointerExpression = ""; + switch (connection.getSerializer()) { + case NATIVE: { + // Handle native types. + if (CUtil.isTokenType(type, types)) { + // NOTE: Transporting token types this way is likely to only work if the sender and receiver + // both have the same endianness. Otherwise, you have to use protobufs or some other serialization scheme. + result.pr("size_t message_length = "+ sendRef +"->token->length * "+ sendRef + +"->token->element_size;"); + result.pr(sendingFunction +"("+ commonArgs +", (unsigned char*) "+ sendRef + +"->value);"); + } else { + // string types need to be dealt with specially because they are hidden pointers. + // void type is odd, but it avoids generating non-standard expression sizeof(void), + // which some compilers reject. + lengthExpression = "sizeof("+ types.getTargetType(type)+")"; + pointerExpression = "(unsigned char*)&"+ sendRef +"->value"; + var targetType = types.getTargetType(type); + if (targetType.equals("string")) { + lengthExpression = "strlen("+ sendRef +"->value) + 1"; + pointerExpression = "(unsigned char*) "+ sendRef +"->value"; + } else if (targetType.equals("void")) { + lengthExpression = "0"; + } + result.pr("size_t message_length = "+lengthExpression+";"); + result.pr( + sendingFunction +"("+ commonArgs +", "+pointerExpression+");"); + } + break; + } + case PROTO: { + throw new UnsupportedOperationException("Protobuf serialization is not supported yet."); + } + case ROS2: { + var variableToSerialize = sendRef; + var typeStr = types.getTargetType(type); + if (CUtil.isTokenType(type, types)) { + throw new UnsupportedOperationException("Cannot handle ROS serialization when ports are pointers."); + } else if (CExtensionUtils.isSharedPtrType(type, types)) { + var matcher = CExtensionUtils.sharedPointerVariable.matcher(typeStr); + if (matcher.find()) { + typeStr = matcher.group("type"); + } + } + var ROSSerializer = new FedROS2CPPSerialization(); + lengthExpression = ROSSerializer.serializedBufferLength(); + pointerExpression = ROSSerializer.seializedBufferVar(); + result.pr( + ROSSerializer.generateNetworkSerializerCode(variableToSerialize, typeStr, CExtensionUtils.isSharedPtrType(type, types)) + ); + result.pr("size_t message_length = "+lengthExpression+";"); + result.pr(sendingFunction +"("+ commonArgs +", "+pointerExpression+");"); + break; + } + + } + } + + /** + * Generate code for the body of a reaction that decides whether the trigger for the given + * port is going to be present or absent for the current logical time. + * This reaction is put just before the first reaction that is triggered by the network + * input port "port" or has it in its sources. If there are only connections to contained + * reactors, in the top-level reactor. + * + * @param receivingPortID The port to generate the control reaction for + * @param maxSTP The maximum value of STP is assigned to reactions (if any) + * that have port as their trigger or source + */ + public String generateNetworkInputControlReactionBody( + int receivingPortID, + TimeValue maxSTP, + CoordinationType coordination + ) { + // Store the code + var result = new CodeBuilder(); + + // We currently have no way to mark a reaction "unordered" + // in the AST, so we use a magic string at the start of the body. + result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); + result.pr("interval_t max_STP = 0LL;"); + + // Find the maximum STP for decentralized coordination + if(coordination == CoordinationType.DECENTRALIZED) { + result.pr("max_STP = "+ GeneratorBase.timeInTargetLanguage(maxSTP)+";"); + } + result.pr("// Wait until the port status is known"); + result.pr("wait_until_port_status_known("+receivingPortID+", max_STP);"); + return result.toString(); + } + + /** + * Generate code for the body of a reaction that sends a port status message for the given + * port if it is absent. + * + * @oaram srcOutputPort FIXME + * @param connection FIXME + */ + public String generateNetworkOutputControlReactionBody( + VarRef srcOutputPort, + FedConnectionInstance connection + ) { + // Store the code + var result = new CodeBuilder(); + // The ID of the receiving port (rightPort) is the position + // of the networkAction (see below) in this list. + int receivingPortID = connection.getDstFederate().networkMessageActions.size(); + + // We currently have no way to mark a reaction "unordered" + // in the AST, so we use a magic string at the start of the body. + result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); + var sendRef = CUtil.portRefInReaction(srcOutputPort, connection.getSrcBank(), connection.getSrcChannel()); + // Get the delay literal + var additionalDelayString = CExtensionUtils.getNetworkDelayLiteral(connection.getDefinition().getDelay()); + result.pr(String.join("\n", + "// If the output port has not been lf_set for the current logical time,", + "// send an ABSENT message to the receiving federate ", + "LF_PRINT_LOG(\"Contemplating whether to send port \"", + " \"absent for port %d to federate %d.\", ", + " "+receivingPortID+", "+connection.getDstFederate().id+");", + "if ("+sendRef+" == NULL || !"+sendRef+"->is_present) {", + " // The output port is NULL or it is not present.", + " send_port_absent_to_federate("+additionalDelayString+", "+receivingPortID+", "+connection.getDstFederate().id+");", + "}" + )); + return result.toString(); + } + + + public String getNetworkBufferType() { + return "uint8_t*"; + } + + /** + * Add preamble to a separate file `include/_federateName_preamble.c` to set up federated execution. + * Return an empty string since no code generated needs to go in the source. + */ + @Override + public String generatePreamble( + FederateInstance federate, + FedFileConfig fileConfig, + LinkedHashMap federationRTIProperties, + ErrorReporter errorReporter + ) throws IOException { + // Put the C preamble in a `include/_federate.name + _preamble.c` file + String cPreamble = makePreamble(federate, fileConfig, federationRTIProperties, errorReporter); + String relPath = "include" + File.separator + "_" + federate.name + "_preamble.c"; + Path fedPreamblePath = fileConfig.getSrcPath().resolve(relPath); + Files.createDirectories(fedPreamblePath.getParent()); + try (var writer = Files.newBufferedWriter(fedPreamblePath)) { + writer.write(cPreamble); + } + + return ""; + } + + /** + * Generate the preamble to setup federated execution in C. + */ + protected String makePreamble( + FederateInstance federate, + FedFileConfig fileConfig, + LinkedHashMap federationRTIProperties, + ErrorReporter errorReporter) { + + var code = new CodeBuilder(); + + code.pr("#include \"../federated/federate.c\""); + + // Generate function to return a pointer to the action trigger_t + // that handles incoming network messages destined to the specified + // port. This will only be used if there are federates. + int numOfNetworkActions = federate.networkMessageActions.size(); + code.pr(""" + trigger_t* _lf_action_table[%1$s]; + trigger_t* _lf_action_for_port(int port_id) { + if ((port_id < %1$s) && (port_id >= 0)) return _lf_action_table[port_id]; + else return NULL; + } + """.formatted(numOfNetworkActions)); + + code.pr(generateSerializationPreamble(federate, fileConfig)); + + code.pr(generateExecutablePreamble(federate, federationRTIProperties, errorReporter)); + + code.pr(generateInitializeTriggers(federate, errorReporter)); + + code.pr(CExtensionUtils.generateFederateNeighborStructure(federate)); + + return code.getCode(); + } + + /** + * Generate preamble code needed for enabled serializers of the federate. + */ + protected String generateSerializationPreamble(FederateInstance federate, FedFileConfig fileConfig) { + return CExtensionUtils.generateSerializationPreamble(federate, fileConfig); + } + + /** + * Create a macro that initializes necessary triggers for federated execution, + * which are the triggers for control reactions and references to all network + * actions (which are triggered upon receiving network messages). + * + * @param federate The federate to initialize triggers for. + * @param errorReporter Used to report errors. + * @return The generated code for the macro. + */ + private String generateInitializeTriggers(FederateInstance federate, ErrorReporter errorReporter) { + CodeBuilder code = new CodeBuilder(); + // Temporarily change the original federate reactor's name in the AST to + // the federate's name so that trigger references are accurate. + var federatedReactor = FedASTUtils.findFederatedReactor(federate.instantiation.eResource()); + var oldFederatedReactorName = federatedReactor.getName(); + federatedReactor.setName(federate.name); + var main = new ReactorInstance(federatedReactor, errorReporter, 1); + code.pr(CExtensionUtils.initializeTriggersForNetworkActions(federate, main)); + code.pr(CExtensionUtils.initializeTriggerForControlReactions(main, main, federate)); + federatedReactor.setName(oldFederatedReactorName); + return """ + #define initialize_triggers_for_federate() \\ + do { \\ + %s + } \\ + while (0) + """.formatted((code.getCode().isBlank() ? "\\" : code.getCode()).indent(4).stripTrailing()); + } + + /** + * Generate code for an executed preamble. + * + */ + private String generateExecutablePreamble(FederateInstance federate, LinkedHashMap federationRTIProperties, ErrorReporter errorReporter) { + CodeBuilder code = new CodeBuilder(); + + code.pr(generateCodeForPhysicalActions(federate, errorReporter)); + + code.pr(generateCodeToInitializeFederate(federate, federationRTIProperties)); + + code.pr(CExtensionUtils.allocateTriggersForFederate(federate)); + + return """ + void _lf_executable_preamble() { + %s + } + """.formatted(code.toString().indent(4).stripTrailing()); + } + + /** + * Generate code to initialize the {@code federate}. + * @param federationRTIProperties Properties related to the RTI. + * @return The generated code + */ + private String generateCodeToInitializeFederate(FederateInstance federate, LinkedHashMap federationRTIProperties) { + CodeBuilder code = new CodeBuilder(); + code.pr("// ***** Start initializing the federated execution. */"); + code.pr(String.join("\n", + "// Initialize the socket mutex", + "lf_mutex_init(&outbound_socket_mutex);", + "lf_cond_init(&port_status_changed);" + )); + + // Find the STA (A.K.A. the global STP offset) for this federate. + if (federate.targetConfig.coordination == CoordinationType.DECENTRALIZED) { + var reactor = ASTUtils.toDefinition(federate.instantiation.getReactorClass()); + var stpParam = reactor.getParameters().stream().filter( + param -> + param.getName().equalsIgnoreCase("STP_offset") + && (param.getType() == null || param.getType().isTime()) + ).findFirst(); + + if (stpParam.isPresent()) { + var globalSTP = ASTUtils.initialValue(stpParam.get(), List.of(federate.instantiation)).get(0); + var globalSTPTV = ASTUtils.getLiteralTimeValue(globalSTP); + code.pr("lf_set_stp_offset("+ CGenerator.timeInTargetLanguage(globalSTPTV)+");"); + } + } + + // Set indicator variables that specify whether the federate has + // upstream logical connections. + if (federate.dependsOn.size() > 0) { + code.pr("_fed.has_upstream = true;"); + } + if (federate.sendsTo.size() > 0) { + code.pr("_fed.has_downstream = true;"); + } + // Set global variable identifying the federate. + code.pr("_lf_my_fed_id = "+ federate.id+";"); + + // We keep separate record for incoming and outgoing p2p connections to allow incoming traffic to be processed in a separate + // thread without requiring a mutex lock. + var numberOfInboundConnections = federate.inboundP2PConnections.size(); + var numberOfOutboundConnections = federate.outboundP2PConnections.size(); + + code.pr(String.join("\n", + "_fed.number_of_inbound_p2p_connections = "+numberOfInboundConnections+";", + "_fed.number_of_outbound_p2p_connections = "+numberOfOutboundConnections+";" + )); + if (numberOfInboundConnections > 0) { + code.pr(String.join("\n", + "// Initialize the array of socket for incoming connections to -1.", + "for (int i = 0; i < NUMBER_OF_FEDERATES; i++) {", + " _fed.sockets_for_inbound_p2p_connections[i] = -1;", + "}" + )); + } + if (numberOfOutboundConnections > 0) { + code.pr(String.join("\n", + "// Initialize the array of socket for outgoing connections to -1.", + "for (int i = 0; i < NUMBER_OF_FEDERATES; i++) {", + " _fed.sockets_for_outbound_p2p_connections[i] = -1;", + "}" + )); + } + + // If a test clock offset has been specified, insert code to set it here. + if (federate.targetConfig.clockSyncOptions.testOffset != null) { + code.pr("lf_set_physical_clock_offset((1 + "+ federate.id+") * "+ federate.targetConfig.clockSyncOptions.testOffset.toNanoSeconds()+"LL);"); + } + + code.pr(String.join("\n", + "// Connect to the RTI. This sets _fed.socket_TCP_RTI and _lf_rti_socket_UDP.", + "connect_to_rti("+addDoubleQuotes(federationRTIProperties.get("host").toString())+", "+ federationRTIProperties.get("port")+");" + )); + + // Disable clock synchronization for the federate if it resides on the same host as the RTI, + // unless that is overridden with the clock-sync-options target property. + if (CExtensionUtils.clockSyncIsOn(federate, federationRTIProperties)) { + code.pr("synchronize_initial_physical_clock_with_rti(_fed.socket_TCP_RTI);"); + } + + if (numberOfInboundConnections > 0) { + code.pr(String.join("\n", + "// Create a socket server to listen to other federates.", + "// If a port is specified by the user, that will be used", + "// as the only possibility for the server. If not, the port", + "// will start from STARTING_PORT. The function will", + "// keep incrementing the port until the number of tries reaches PORT_RANGE_LIMIT.", + "create_server("+ federate.port+");", + "// Connect to remote federates for each physical connection.", + "// This is done in a separate thread because this thread will call", + "// connect_to_federate for each outbound physical connection at the same", + "// time that the new thread is listening for such connections for inbound", + "// physical connections. The thread will live until all connections", + "// have been established.", + "lf_thread_create(&_fed.inbound_p2p_handling_thread_id, handle_p2p_connections_from_federates, NULL);" + )); + } + + for (FederateInstance remoteFederate : federate.outboundP2PConnections) { + code.pr("connect_to_federate("+remoteFederate.id+");"); + } + return code.getCode(); + } + + /** + * Generate code to handle physical actions in the {@code federate}. + * @param errorReporter Used to report errors. + * @return Generated code. + */ + private String generateCodeForPhysicalActions(FederateInstance federate, ErrorReporter errorReporter) { + CodeBuilder code = new CodeBuilder(); + if (federate.targetConfig.coordination.equals(CoordinationType.CENTRALIZED)) { + // If this program uses centralized coordination then check + // for outputs that depend on physical actions so that null messages can be + // sent to the RTI. + var federateClass = ASTUtils.toDefinition(federate.instantiation.getReactorClass()); + var main = new ReactorInstance(FedASTUtils.findFederatedReactor(federate.instantiation.eResource()), errorReporter, 1); + var instance = new ReactorInstance(federateClass, main, errorReporter); + var outputDelayMap = federate + .findOutputsConnectedToPhysicalActions(instance); + var minDelay = TimeValue.MAX_VALUE; + Output outputFound = null; + for (Output output : outputDelayMap.keySet()) { + var outputDelay = outputDelayMap.get(output); + if (outputDelay.isEarlierThan(minDelay)) { + minDelay = outputDelay; + outputFound = output; + } + } + if (minDelay != TimeValue.MAX_VALUE) { + // Unless silenced, issue a warning. + if (federate.targetConfig.coordinationOptions.advance_message_interval + == null) { + errorReporter.reportWarning(outputFound, String.join("\n", + "Found a path from a physical action to output for reactor " + + addDoubleQuotes(instance.getName()) + + ". ", + "The amount of delay is " + + minDelay + + ".", + "With centralized coordination, this can result in a large number of messages to the RTI.", + "Consider refactoring the code so that the output does not depend on the physical action,", + "or consider using decentralized coordination. To silence this warning, set the target", + "parameter coordination-options with a value like {advance-message-interval: 10 msec}" + )); + } + code.pr( + "_fed.min_delay_from_physical_action_to_federate_output = " + + GeneratorBase.timeInTargetLanguage(minDelay) + ";"); + } + } + return code.getCode(); + } + +} diff --git a/org.lflang/src/org/lflang/federated/extensions/CExtensionUtils.java b/org.lflang/src/org/lflang/federated/extensions/CExtensionUtils.java new file mode 100644 index 0000000000..9f21684068 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/extensions/CExtensionUtils.java @@ -0,0 +1,582 @@ +package org.lflang.federated.extensions; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; + +import org.lflang.ASTUtils; +import org.lflang.InferredType; +import org.lflang.TargetConfig.ClockSyncOptions; +import org.lflang.TargetProperty; +import org.lflang.TargetProperty.ClockSyncMode; +import org.lflang.TimeValue; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; +import org.lflang.federated.serialization.FedROS2CPPSerialization; +import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.GeneratorBase; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.c.CTypes; +import org.lflang.generator.c.CUtil; +import org.lflang.lf.Action; +import org.lflang.lf.Expression; +import org.lflang.lf.Input; +import org.lflang.lf.ParameterReference; +import org.lflang.lf.Reactor; +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.VarRef; + +public class CExtensionUtils { + + // Regular expression pattern for shared_ptr types. + static final Pattern sharedPointerVariable = Pattern.compile("^(/\\*.*?\\*/)?std::shared_ptr<(?((/\\*.*?\\*/)?(\\S+))+)>$"); + + /** + * Generate C code that allocates sufficient memory for the following two + * critical data structures that support network control reactions: + * - triggers_for_network_input_control_reactions: These are triggers that + * are + * used at runtime to insert network input control reactions into the + * reaction queue. + * - trigger_for_network_output_control_reactions: Triggers for + * network output control reactions, which are unique per each output port. + * There could be multiple network output control reactions for each + * network + * output port if it is connected to multiple downstream federates. + * + * @param federate The top-level federate instance + * @return A string that allocates memory for the aforementioned three + * structures. + */ + public static String allocateTriggersForFederate( + FederateInstance federate + ) { + + CodeBuilder builder = new CodeBuilder(); + if (federate.networkInputControlReactionsTriggers.size() > 0) { + // Proliferate the network input control reaction trigger array + builder.pr( + """ + // Initialize the array of pointers to network input port triggers + _fed.triggers_for_network_input_control_reactions_size = %s; + _fed.triggers_for_network_input_control_reactions = (trigger_t**)malloc( + _fed.triggers_for_network_input_control_reactions_size * sizeof(trigger_t*)); + """.formatted(federate.networkInputControlReactionsTriggers.size())); + + } + return builder.getCode(); + } + + /** + * Generate C code that initializes network actions. + * + * These network actions will be triggered by federate.c whenever a message + * is received from the network. + * @param federate The federate. + * @param main The main reactor that contains the federate (used to lookup references). + * @return + */ + public static String initializeTriggersForNetworkActions(FederateInstance federate, ReactorInstance main) { + CodeBuilder code = new CodeBuilder(); + if (federate.networkMessageActions.size() > 0) { + // Create a static array of trigger_t pointers. + // networkMessageActions is a list of Actions, but we + // need a list of trigger struct names for ActionInstances. + // There should be exactly one ActionInstance in the + // main reactor for each Action. + var triggers = new LinkedList(); + for (Action action : federate.networkMessageActions) { + // Find the corresponding ActionInstance. + var actionInstance = main.lookupActionInstance(action); + triggers.add(CUtil.triggerRef(actionInstance)); + } + var actionTableCount = 0; + for (String trigger : triggers) { + code.pr("_lf_action_table[" + (actionTableCount++) + "] = &" + + trigger + "; \\"); + } + } + return code.getCode(); + } + + /** + * Generate C code that initializes three critical structures that support + * network control reactions: + * - triggers_for_network_input_control_reactions: These are triggers that are + * used at runtime to insert network input control reactions into the + * reaction queue. There could be multiple network input control reactions + * for one network input at multiple levels in the hierarchy. + * - trigger_for_network_output_control_reactions: Triggers for + * network output control reactions, which are unique per each output port. + * There could be multiple network output control reactions for each network + * output port if it is connected to multiple downstream federates. + * + * @param instance The reactor instance that is at any level of the + * hierarchy within the federate. + * @param federate The top-level federate + * @return A string that initializes the aforementioned three structures. + */ + public static String initializeTriggerForControlReactions( + ReactorInstance instance, + ReactorInstance main, + FederateInstance federate + ) { + CodeBuilder builder = new CodeBuilder(); + // The network control reactions are always in the main federated + // reactor + if (instance != main) { + return ""; + } + + ReactorDecl reactorClass = instance.getDefinition().getReactorClass(); + Reactor reactor = ASTUtils.toDefinition(reactorClass); + String nameOfSelfStruct = CUtil.reactorRef(instance); + + // Initialize triggers for network input control reactions + for (Action trigger : federate.networkInputControlReactionsTriggers) { + // Check if the trigger belongs to this reactor instance + if (ASTUtils.allReactions(reactor).stream().anyMatch(r -> { + return r.getTriggers().stream().anyMatch(t -> { + if (t instanceof VarRef) { + return ((VarRef) t).getVariable().equals(trigger); + } else { + return false; + } + }); + })) { + // Initialize the triggers_for_network_input_control_reactions for the input + builder.pr( + String.join("\n", + "/* Add trigger " + nameOfSelfStruct + "->_lf__"+trigger.getName()+" to the global list of network input ports. */ \\", + "_fed.triggers_for_network_input_control_reactions["+federate.networkInputControlReactionsTriggers.indexOf(trigger)+"]= \\", + " &"+nameOfSelfStruct+"->_lf__"+trigger.getName()+"; \\" + ) + ); + } + } + + nameOfSelfStruct = CUtil.reactorRef(instance); + + // Initialize the trigger for network output control reactions if it doesn't exist. + if (federate.networkOutputControlReactionsTrigger != null) { + builder.pr("_fed.trigger_for_network_output_control_reactions=&" + + nameOfSelfStruct + + "->_lf__outputControlReactionTrigger; \\"); + } + + return builder.getCode(); + } + + /** + * Create a port status field variable for a network input port "input" in + * the self struct of a reactor. + * + * @param input The network input port + * @return A string containing the appropriate variable + */ + public static String createPortStatusFieldForInput(Input input) { + StringBuilder builder = new StringBuilder(); + // Check if the port is a multiport + if (ASTUtils.isMultiport(input)) { + // If it is a multiport, then create an auxiliary list of port + // triggers for each channel of + // the multiport to keep track of the status of each channel + // individually + builder.append("trigger_t* _lf__" + input.getName() + + "_network_port_status;\n"); + } else { + // If it is not a multiport, then we could re-use the port trigger, + // and nothing needs to be + // done + } + return builder.toString(); + } + + /** + * Given a connection 'delay' predicate, return a string that represents the + * interval_t value of the additional delay that needs to be applied to the + * outgoing message. + * + * The returned additional delay in absence of after on network connection + * (i.e., if delay is passed as a null) is NEVER. This has a special + * meaning in C library functions that send network messages that carry + * timestamps (@see send_timed_message and send_port_absent_to_federate + * in lib/core/federate.c). In this case, the sender will send its current + * tag as the timestamp of the outgoing message without adding a microstep delay. + * If the user has assigned an after delay to the network connection (that + * can be zero) either as a time value (e.g., 200 msec) or as a literal + * (e.g., a parameter), that delay in nsec will be returned. + * + * @param delay + * @return + */ + public static String getNetworkDelayLiteral(Expression delay) { + String additionalDelayString = "NEVER"; + if (delay != null) { + TimeValue tv; + if (delay instanceof ParameterReference) { + // The parameter has to be parameter of the main reactor. + // And that value has to be a Time. + tv = ASTUtils.getDefaultAsTimeValue(((ParameterReference)delay).getParameter()); + } else { + tv = ASTUtils.getLiteralTimeValue(delay); + } + additionalDelayString = Long.toString(tv.toNanoSeconds()); + } + return additionalDelayString; + } + + static boolean isSharedPtrType(InferredType type, CTypes types) { + return !type.isUndefined() && sharedPointerVariable.matcher(types.getTargetType(type)).find(); + } + + public static void handleCompileDefinitions( + FederateInstance federate, + int numOfFederates, + LinkedHashMap federationRTIProperties + ) { + federate.targetConfig.setByUser.add(TargetProperty.COMPILE_DEFINITIONS); + federate.targetConfig.compileDefinitions.put("FEDERATED", ""); + federate.targetConfig.compileDefinitions.put("FEDERATED_"+federate.targetConfig.coordination.toString().toUpperCase(), ""); + federate.targetConfig.compileDefinitions.put("NUMBER_OF_FEDERATES", String.valueOf(numOfFederates)); + federate.targetConfig.compileDefinitions.put("EXECUTABLE_PREAMBLE", ""); + federate.targetConfig.compileDefinitions.put("WORKERS_NEEDED_FOR_FEDERATE", String.valueOf(minThreadsToHandleInputPorts(federate))); + + handleAdvanceMessageInterval(federate); + + initializeClockSynchronization(federate, federationRTIProperties); + } + + /** + * The number of threads needs to be at least one larger than the input ports + * to allow the federate to wait on all input ports while allowing an additional + * worker thread to process incoming messages. + * @return The minimum number of threads needed. + */ + public static int minThreadsToHandleInputPorts(FederateInstance federate) { + int nthreads = 1; + nthreads = Math.max(nthreads, federate.networkMessageActions.size() + 1); + return nthreads; + } + + private static void handleAdvanceMessageInterval(FederateInstance federate) { + var advanceMessageInterval = federate.targetConfig.coordinationOptions.advance_message_interval; + federate.targetConfig.setByUser.remove(TargetProperty.COORDINATION_OPTIONS); + if (advanceMessageInterval != null) { + federate.targetConfig.compileDefinitions.put( + "ADVANCE_MESSAGE_INTERVAL", + String.valueOf(advanceMessageInterval.toNanoSeconds()) + ); + } + } + + static boolean clockSyncIsOn(FederateInstance federate, LinkedHashMap federationRTIProperties) { + return federate.targetConfig.clockSync != ClockSyncMode.OFF + && (!federationRTIProperties.get("host").toString().equals(federate.host) + || federate.targetConfig.clockSyncOptions.localFederatesOn); + } + + /** + * Initialize clock synchronization (if enabled) and its related options for a given federate. + * + * Clock synchronization can be enabled using the clock-sync target property. + * @see Documentation + */ + public static void initializeClockSynchronization( + FederateInstance federate, + LinkedHashMap federationRTIProperties + ) { + // Check if clock synchronization should be enabled for this federate in the first place + if (clockSyncIsOn(federate, federationRTIProperties)) { + System.out.println("Initial clock synchronization is enabled for federate " + + federate.id + ); + if (federate.targetConfig.clockSync == ClockSyncMode.ON) { + if (federate.targetConfig.clockSyncOptions.collectStats) { + System.out.println("Will collect clock sync statistics for federate " + federate.id); + // Add libm to the compiler flags + // FIXME: This is a linker flag not compile flag but we don't have a way to add linker flags + // FIXME: This is probably going to fail on MacOS (especially using clang) + // because libm functions are builtin + federate.targetConfig.compilerFlags.add("-lm"); + federate.targetConfig.setByUser.add(TargetProperty.FLAGS); + } + System.out.println("Runtime clock synchronization is enabled for federate " + + federate.id + ); + } + + addClockSyncCompileDefinitions(federate); + } + } + + + /** + * Initialize clock synchronization (if enabled) and its related options for a given federate. + * + * Clock synchronization can be enabled using the clock-sync target property. + * @see Documentation + */ + public static void addClockSyncCompileDefinitions(FederateInstance federate) { + + ClockSyncMode mode = federate.targetConfig.clockSync; + ClockSyncOptions options = federate.targetConfig.clockSyncOptions; + + + federate.targetConfig.compileDefinitions.put("_LF_CLOCK_SYNC_INITIAL", ""); + federate.targetConfig.compileDefinitions.put("_LF_CLOCK_SYNC_PERIOD_NS", String.valueOf(options.period.toNanoSeconds())); + federate.targetConfig.compileDefinitions.put("_LF_CLOCK_SYNC_EXCHANGES_PER_INTERVAL", String.valueOf(options.trials)); + federate.targetConfig.compileDefinitions.put("_LF_CLOCK_SYNC_ATTENUATION", String.valueOf(options.attenuation)); + + if (mode == ClockSyncMode.ON) { + federate.targetConfig.compileDefinitions.put("_LF_CLOCK_SYNC_ON", ""); + if (options.collectStats) { + federate.targetConfig.compileDefinitions.put("_LF_CLOCK_SYNC_COLLECT_STATS", ""); + } + } + } + + + /** + * Generate a file to be included by CMake. + */ + public static void generateCMakeInclude( + FederateInstance federate, + FedFileConfig fileConfig + ) throws IOException { + Files.createDirectories(fileConfig.getSrcPath().resolve("include")); + + Path cmakeIncludePath = fileConfig.getSrcPath() + .resolve("include" + File.separator + federate.name + "_extension.cmake"); + + CodeBuilder cmakeIncludeCode = new CodeBuilder(); + + cmakeIncludeCode.pr(generateSerializationCMakeExtension(federate)); + + try (var srcWriter = Files.newBufferedWriter(cmakeIncludePath)) { + srcWriter.write(cmakeIncludeCode.getCode()); + } + + federate.targetConfig.cmakeIncludes.add(fileConfig.getSrcPath().relativize(cmakeIncludePath).toString()); + federate.targetConfig.setByUser.add(TargetProperty.CMAKE_INCLUDE); + } + + + /** + * Generate code that sends the neighbor structure message to the RTI. + * See {@code MSG_TYPE_NEIGHBOR_STRUCTURE} in {@code federated/net_common.h}. + * + * @param federate The federate that is sending its neighbor structure + */ + public static String generateFederateNeighborStructure(FederateInstance federate) { + var code = new CodeBuilder(); + code.pr(String.join("\n", + "/**", + "* Generated function that sends information about connections between this federate and", + "* other federates where messages are routed through the RTI. Currently, this", + "* only includes logical connections when the coordination is centralized. This", + "* information is needed for the RTI to perform the centralized coordination.", + "* @see MSG_TYPE_NEIGHBOR_STRUCTURE in net_common.h", + "*/", + "void send_neighbor_structure_to_RTI(int rti_socket) {" + )); + code.indent(); + // Initialize the array of information about the federate's immediate upstream + // and downstream relayed (through the RTI) logical connections, to send to the + // RTI. + code.pr(String.join("\n", + "interval_t candidate_tmp;", + "size_t buffer_size = 1 + 8 + ", + " "+federate.dependsOn.keySet().size()+" * ( sizeof(uint16_t) + sizeof(int64_t) ) +", + " "+federate.sendsTo.keySet().size()+" * sizeof(uint16_t);", + "unsigned char buffer_to_send[buffer_size];", + "", + "size_t message_head = 0;", + "buffer_to_send[message_head] = MSG_TYPE_NEIGHBOR_STRUCTURE;", + "message_head++;", + "encode_int32((int32_t)"+federate.dependsOn.keySet().size()+", &(buffer_to_send[message_head]));", + "message_head+=sizeof(int32_t);", + "encode_int32((int32_t)"+federate.sendsTo.keySet().size()+", &(buffer_to_send[message_head]));", + "message_head+=sizeof(int32_t);" + )); + + if (!federate.dependsOn.keySet().isEmpty()) { + // Next, populate these arrays. + // Find the minimum delay in the process. + // FIXME: Zero delay is not really the same as a microstep delay. + for (FederateInstance upstreamFederate : federate.dependsOn.keySet()) { + code.pr(String.join("\n", + "encode_uint16((uint16_t)"+upstreamFederate.id+", &(buffer_to_send[message_head]));", + "message_head += sizeof(uint16_t);" + )); + // The minimum delay calculation needs to be made in the C code because it + // may depend on parameter values. + // FIXME: These would have to be top-level parameters, which don't really + // have any support yet. Ideally, they could be overridden on the command line. + // When that is done, they will need to be in scope here. + var delays = federate.dependsOn.get(upstreamFederate); + if (delays != null) { + // There is at least one delay, so find the minimum. + // If there is no delay at all, this is encoded as NEVER. + code.pr("candidate_tmp = FOREVER;"); + for (Expression delay : delays) { + if (delay == null) { + // Use NEVER to encode no delay at all. + code.pr("candidate_tmp = NEVER;"); + } else { + var delayTime = GeneratorBase.getTargetTime(delay); + if (delay instanceof ParameterReference) { + // The delay is given as a parameter reference. Find its value. + final var param = ((ParameterReference)delay).getParameter(); + delayTime = GeneratorBase.timeInTargetLanguage(ASTUtils.getDefaultAsTimeValue(param)); + } + code.pr(String.join("\n", + "if ("+delayTime+" < candidate_tmp) {", + " candidate_tmp = "+delayTime+";", + "}" + )); + } + } + code.pr(String.join("\n", + "encode_int64((int64_t)candidate_tmp, &(buffer_to_send[message_head]));", + "message_head += sizeof(int64_t);" + )); + } else { + // Use NEVER to encode no delay at all. + code.pr(String.join("\n", + "encode_int64(NEVER, &(buffer_to_send[message_head]));", + "message_head += sizeof(int64_t);" + )); + } + } + } + + // Next, set up the downstream array. + if (!federate.sendsTo.keySet().isEmpty()) { + // Next, populate the array. + // Find the minimum delay in the process. + // FIXME: Zero delay is not really the same as a microstep delay. + for (FederateInstance downstreamFederate : federate.sendsTo.keySet()) { + code.pr(String.join("\n", + "encode_uint16("+downstreamFederate.id+", &(buffer_to_send[message_head]));", + "message_head += sizeof(uint16_t);" + )); + } + } + code.pr(String.join("\n", + "write_to_socket_errexit(", + " rti_socket, ", + " buffer_size,", + " buffer_to_send,", + " \"Failed to send the neighbor structure message to the RTI.\"", + ");" + )); + code.unindent(); + code.pr("}"); + return code.toString(); + } + + + public static List getFederatedFiles() { + return List.of( + "federated/net_util.c", + "federated/net_util.h", + "federated/net_common.h", + "federated/federate.c", + "federated/federate.h", + "federated/clock-sync.h", + "federated/clock-sync.c" + ); + } + + /** + * Surround {@code code} with blocks to ensure that code only executes + * if the program is federated. + */ + public static String surroundWithIfFederated(String code) { + return """ + #ifdef FEDERATED + %s + #endif // FEDERATED + """.formatted(code); + } + + /** + * Surround {@code code} with blocks to ensure that code only executes + * if the program is federated and has a centralized coordination. + */ + public static String surroundWithIfFederatedCentralized(String code) { + return """ + #ifdef FEDERATED_CENTRALIZED + %s + #endif // FEDERATED_CENTRALIZED + """.formatted(code); + } + + /** + * Surround {@code code} with blocks to ensure that code only executes + * if the program is federated and has a decentralized coordination. + */ + public static String surroundWithIfFederatedDecentralized(String code) { + return """ + #ifdef FEDERATED_DECENTRALIZED + %s + #endif // FEDERATED_DECENTRALIZED + """.formatted(code); + } + + /** + * Generate preamble code needed for enabled serializers of the federate. + */ + public static String generateSerializationPreamble( + FederateInstance federate, + FedFileConfig fileConfig + ) { + CodeBuilder code = new CodeBuilder(); + for (SupportedSerializers serializer : federate.enabledSerializers) { + switch (serializer) { + case NATIVE: + case PROTO: { + // No need to do anything at this point. + break; + } + case ROS2: { + var ROSSerializer = new FedROS2CPPSerialization(); + code.pr(ROSSerializer.generatePreambleForSupport().toString()); + break; + } + } + } + return code.getCode(); + } + + /** + * Generate cmake-include code needed for enabled serializers of the federate. + */ + public static String generateSerializationCMakeExtension( + FederateInstance federate + ) { + CodeBuilder code = new CodeBuilder(); + for (SupportedSerializers serializer : federate.enabledSerializers) { + switch (serializer) { + case NATIVE: + case PROTO: { + // No CMake code is needed for now + break; + } + case ROS2: { + var ROSSerializer = new FedROS2CPPSerialization(); + code.pr(ROSSerializer.generateCompilerExtensionForSupport()); + break; + } + } + } + return code.getCode(); + } +} diff --git a/org.lflang/src/org/lflang/federated/extensions/FedTargetExtension.java b/org.lflang/src/org/lflang/federated/extensions/FedTargetExtension.java new file mode 100644 index 0000000000..7fb42bf007 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/extensions/FedTargetExtension.java @@ -0,0 +1,132 @@ +package org.lflang.federated.extensions; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import org.lflang.ErrorReporter; +import org.lflang.FileConfig; +import org.lflang.InferredType; +import org.lflang.Target; +import org.lflang.TargetConfig; +import org.lflang.TargetProperty.CoordinationType; +import org.lflang.TimeValue; +import org.lflang.federated.generator.FedConnectionInstance; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; +import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.LFGeneratorContext; +import org.lflang.lf.Action; +import org.lflang.lf.Reaction; +import org.lflang.lf.VarRef; + +public interface FedTargetExtension { + + /** + * Perform necessary actions to initialize the target config. + * + * @param context + * @param numOfFederates + * @param federate The federate instance. + * @param fileConfig An instance of {@code FedFileConfig}. + * @param errorReporter Used to report errors. + */ + void initializeTargetConfig(LFGeneratorContext context, int numOfFederates, FederateInstance federate, FedFileConfig fileConfig, + ErrorReporter errorReporter, LinkedHashMap federationRTIProperties) throws IOException; + + /** + * Generate code for the body of a reaction that handles the + * action that is triggered by receiving a message from a remote + * federate. + * @param action The action. + * @param sendingPort The output port providing the data to send. + * @param receivingPort The ID of the destination port. + * @param connection FIXME + * @param type FIXME + * @param coordinationType The coordination type + * @param errorReporter + */ + String generateNetworkReceiverBody( + Action action, + VarRef sendingPort, + VarRef receivingPort, + FedConnectionInstance connection, + InferredType type, + CoordinationType coordinationType, + ErrorReporter errorReporter + ); + + /** + * Generate code for the body of a reaction that handles an output + * that is to be sent over the network. + * @param sendingPort The output port providing the data to send. + * @param receivingPort The variable reference to the destination port. + * @param connection + * @param type + * @param coordinationType + * @param errorReporter FIXME + */ + String generateNetworkSenderBody( + VarRef sendingPort, + VarRef receivingPort, + FedConnectionInstance connection, + InferredType type, + CoordinationType coordinationType, + ErrorReporter errorReporter + ); + + /** + * Generate code for the body of a reaction that decides whether the trigger for the given + * port is going to be present or absent for the current logical time. + * This reaction is put just before the first reaction that is triggered by the network + * input port "port" or has it in its sources. If there are only connections to contained + * reactors, in the top-level reactor. + * + * @param receivingPortID The port to generate the control reaction for + * @param maxSTP The maximum value of STP is assigned to reactions (if any) + * that have port as their trigger or source + * @param coordination FIXME + */ + String generateNetworkInputControlReactionBody( + int receivingPortID, + TimeValue maxSTP, + CoordinationType coordination + ); + + /** + * Generate code for the body of a reaction that sends a port status message for the given + * port if it is absent. + * + * @oaram srcOutputPort FIXME + * @param connection FIXME + */ + String generateNetworkOutputControlReactionBody( + VarRef srcOutputPort, + FedConnectionInstance connection + ); + + /** Optionally apply additional annotations to the reaction. */ + default void annotateReaction(Reaction reaction) {} + + /** + * Return the type for the raw network buffer in the target language (e.g., `uint_8` in C). This would be the type of the + * network messages after serialization and before deserialization. It is primarily used to determine the type for the + * network action at the receiver. + */ + String getNetworkBufferType(); + + /** + * Add necessary preamble to the source to set up federated execution. + * + * @param federate + * @param federationRTIProperties + * @param errorReporter + * @return + */ + String generatePreamble(FederateInstance federate, + FedFileConfig fileConfig, + LinkedHashMap federationRTIProperties, + ErrorReporter errorReporter) + throws IOException; +} diff --git a/org.lflang/src/org/lflang/federated/extensions/FedTargetExtensionFactory.java b/org.lflang/src/org/lflang/federated/extensions/FedTargetExtensionFactory.java new file mode 100644 index 0000000000..f3ab17514d --- /dev/null +++ b/org.lflang/src/org/lflang/federated/extensions/FedTargetExtensionFactory.java @@ -0,0 +1,24 @@ +package org.lflang.federated.extensions; + +import org.lflang.Target; +import org.lflang.lf.TargetDecl; + +/** + * Class for instantiating target extensions. + * @author Soroush Bateni + */ +public class FedTargetExtensionFactory { + + /** + * Given a target, return the appropriate extension. + */ + public static FedTargetExtension getExtension(TargetDecl target) { + switch (Target.fromDecl(target)) { + case CCPP: + case C: return new CExtension(); + case Python: return new PythonExtension(); + case TS: return new TSExtension(); + default: throw new RuntimeException("Target not supported"); + } + } +} diff --git a/org.lflang/src/org/lflang/federated/extensions/PythonExtension.java b/org.lflang/src/org/lflang/federated/extensions/PythonExtension.java new file mode 100644 index 0000000000..73bed06f7b --- /dev/null +++ b/org.lflang/src/org/lflang/federated/extensions/PythonExtension.java @@ -0,0 +1,210 @@ +/************* + * Copyright (c) 2021, The University of Texas at Dallas. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.federated.extensions; + +import java.io.IOException; + +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.InferredType; +import org.lflang.TargetProperty.CoordinationType; +import org.lflang.federated.generator.FedConnectionInstance; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; +import org.lflang.federated.serialization.FedNativePythonSerialization; +import org.lflang.federated.serialization.FedSerialization; +import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.python.PyUtil; +import org.lflang.lf.Action; +import org.lflang.lf.Reaction; +import org.lflang.lf.VarRef; + +/** + * An extension class to the PythonGenerator that enables certain federated + * functionalities. + * + * @author Soroush Bateni + */ +public class PythonExtension extends CExtension { + + @Override + protected void generateCMakeInclude(FederateInstance federate, FedFileConfig fileConfig) throws IOException {} + + @Override + protected String generateSerializationPreamble(FederateInstance federate, FedFileConfig fileConfig) { + CodeBuilder code = new CodeBuilder(); + for (SupportedSerializers serialization : federate.enabledSerializers) { + switch (serialization) { + case NATIVE: { + FedNativePythonSerialization pickler = new FedNativePythonSerialization(); + code.pr(pickler.generatePreambleForSupport().toString()); + } + case PROTO: { + // Nothing needs to be done + } + case ROS2: { + // FIXME: Not supported yet + } + } + } + return code.getCode(); + } + + @Override + public String generateNetworkSenderBody( + VarRef sendingPort, + VarRef receivingPort, + FedConnectionInstance connection, + InferredType type, + CoordinationType coordinationType, + ErrorReporter errorReporter + ) { + var result = new CodeBuilder(); + + // We currently have no way to mark a reaction "unordered" + // in the AST, so we use a magic string at the start of the body. + result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); + result.pr(PyUtil.generateGILAcquireCode() + "\n"); + result.pr( + super.generateNetworkSenderBody( + sendingPort, + receivingPort, + connection, + type, + coordinationType, + errorReporter + ) + ); + result.pr(PyUtil.generateGILReleaseCode() + "\n"); + return result.getCode(); + } + + @Override + public String generateNetworkReceiverBody( + Action action, + VarRef sendingPort, + VarRef receivingPort, + FedConnectionInstance connection, + InferredType type, + CoordinationType coordinationType, + ErrorReporter errorReporter + ) { + var result = new CodeBuilder(); + + // We currently have no way to mark a reaction "unordered" + // in the AST, so we use a magic string at the start of the body. + result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); + result.pr(PyUtil.generateGILAcquireCode() + "\n"); + result.pr( + super.generateNetworkReceiverBody( + action, + sendingPort, + receivingPort, + connection, + type, + coordinationType, + errorReporter + ) + ); + result.pr(PyUtil.generateGILReleaseCode() + "\n"); + return result.getCode(); + + } + + @Override + protected void deserialize( + Action action, + VarRef receivingPort, + FedConnectionInstance connection, + InferredType type, + String receiveRef, + CodeBuilder result, + ErrorReporter errorReporter + ) { + String value = ""; + switch (connection.getSerializer()) { + case NATIVE: { + value = action.getName(); + FedNativePythonSerialization pickler = new FedNativePythonSerialization(); + result.pr(pickler.generateNetworkDeserializerCode(value, null)); + result.pr("lf_set(" + receiveRef + ", " + + FedSerialization.deserializedVarName + ");\n"); + break; + } + case PROTO: { + throw new UnsupportedOperationException("Protobuf serialization is not supported yet."); + } + case ROS2: { + throw new UnsupportedOperationException("ROS2 serialization is not supported yet."); + } + + } + } + + @Override + protected void serializeAndSend( + FedConnectionInstance connection, + InferredType type, + String sendRef, + CodeBuilder result, + String sendingFunction, + String commonArgs, + ErrorReporter errorReporter + ) { + String lengthExpression = ""; + String pointerExpression = ""; + switch (connection.getSerializer()) { + case NATIVE: { + var variableToSerialize = sendRef + "->value"; + FedNativePythonSerialization pickler = new FedNativePythonSerialization(); + lengthExpression = pickler.serializedBufferLength(); + pointerExpression = pickler.seializedBufferVar(); + result.pr(pickler.generateNetworkSerializerCode(variableToSerialize, null)); + result.pr( + "size_t message_length = " + lengthExpression + ";"); + result.pr( + sendingFunction + "(" + commonArgs + ", " + pointerExpression + + ");\n"); + break; + } + case PROTO: { + throw new UnsupportedOperationException("Protbuf serialization is not supported yet."); + } + case ROS2: { + throw new UnsupportedOperationException("ROS2 serialization is not supported yet."); + } + + } + } + + @Override + public void annotateReaction(Reaction reaction) { + ASTUtils.addReactionAttribute(reaction, "_c_body"); + } +} diff --git a/org.lflang/src/org/lflang/federated/extensions/TSExtension.java b/org.lflang/src/org/lflang/federated/extensions/TSExtension.java new file mode 100644 index 0000000000..21b469228e --- /dev/null +++ b/org.lflang/src/org/lflang/federated/extensions/TSExtension.java @@ -0,0 +1,170 @@ +package org.lflang.federated.extensions; + +import static org.lflang.util.StringUtil.addDoubleQuotes; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.stream.Collectors; + +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.InferredType; +import org.lflang.Target; +import org.lflang.TargetProperty.CoordinationType; +import org.lflang.TimeValue; +import org.lflang.federated.generator.FedASTUtils; +import org.lflang.federated.generator.FedConnectionInstance; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; +import org.lflang.generator.GeneratorBase; +import org.lflang.generator.LFGeneratorContext; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.ts.TSExtensionsKt; +import org.lflang.lf.Action; +import org.lflang.lf.Expression; +import org.lflang.lf.Output; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; + +public class TSExtension implements FedTargetExtension { + @Override + public void initializeTargetConfig(LFGeneratorContext context, int numOfFederates, FederateInstance federate, FedFileConfig fileConfig, ErrorReporter errorReporter, LinkedHashMap federationRTIProperties) throws IOException { + + } + + @Override + public String generateNetworkReceiverBody(Action action, VarRef sendingPort, VarRef receivingPort, FedConnectionInstance connection, InferredType type, CoordinationType coordinationType, ErrorReporter errorReporter) { + return """ + // generateNetworkReceiverBody + if (%1$s !== undefined) { + %2$s.%3$s = %1$s; + } + """.formatted( + action.getName(), + receivingPort.getContainer().getName(), + receivingPort.getVariable().getName() + ); + } + + @Override + public String generateNetworkSenderBody(VarRef sendingPort, VarRef receivingPort, FedConnectionInstance connection, InferredType type, CoordinationType coordinationType, ErrorReporter errorReporter) { + return""" + if (%1$s.%2$s !== undefined) { + this.util.sendRTITimedMessage(%1$s.%2$s, %3$s, %4$s, %5$s); + } + """.formatted( + sendingPort.getContainer().getName(), + sendingPort.getVariable().getName(), + connection.getDstFederate().id, + connection.getDstFederate().networkMessageActions.size(), + getNetworkDelayLiteral(connection.getDefinition().getDelay()) + ); + } + + private String getNetworkDelayLiteral(Expression e) { + var cLiteral = CExtensionUtils.getNetworkDelayLiteral(e); + return cLiteral.equals("NEVER") ? "0" : cLiteral; + } + + @Override + public String generateNetworkInputControlReactionBody(int receivingPortID, TimeValue maxSTP, CoordinationType coordination) { + return "// TODO(hokeun): Figure out what to do for generateNetworkInputControlReactionBody"; + } + + @Override + public String generateNetworkOutputControlReactionBody(VarRef srcOutputPort, FedConnectionInstance connection) { + return "// TODO(hokeun): Figure out what to do for generateNetworkOutputControlReactionBody"; + } + + @Override + public String getNetworkBufferType() { + return ""; + } + + /** + * Add necessary preamble to the source to set up federated execution. + * + * @return + */ + @Override + public String generatePreamble(FederateInstance federate, FedFileConfig fileConfig, + LinkedHashMap federationRTIProperties, + ErrorReporter errorReporter) { + var minOutputDelay = getMinOutputDelay(federate, fileConfig, errorReporter); + return + """ + preamble {= + const defaultFederateConfig: __FederateConfig = { + dependsOn: [%s], + executionTimeout: undefined, + fast: false, + federateID: %d, + federationID: "Unidentified Federation", + keepAlive: false, + minOutputDelay: %s, + networkMessageActions: [%s], + rtiHost: "%s", + rtiPort: %d, + sendsTo: [%s] + } + =}""".formatted( + federate.dependsOn.keySet().stream() + .map(e->String.valueOf(e.id)) + .collect(Collectors.joining(",")), + federate.id, + minOutputDelay == null ? "undefined" + : "%s".formatted(TSExtensionsKt.toTsTime(minOutputDelay)), + federate.networkMessageActions + .stream() + .map(Variable::getName) + .collect(Collectors.joining(",", "\"", "\"")), + federationRTIProperties.get("host"), + federationRTIProperties.get("port"), + federate.sendsTo.keySet().stream() + .map(e->String.valueOf(e.id)) + .collect(Collectors.joining(",")) + ); + } + + private TimeValue getMinOutputDelay(FederateInstance federate, FedFileConfig fileConfig, ErrorReporter errorReporter) { + if (federate.targetConfig.coordination.equals(CoordinationType.CENTRALIZED)) { + // If this program uses centralized coordination then check + // for outputs that depend on physical actions so that null messages can be + // sent to the RTI. + var federateClass = ASTUtils.toDefinition(federate.instantiation.getReactorClass()); + var main = new ReactorInstance(FedASTUtils.findFederatedReactor(federate.instantiation.eResource()), errorReporter, 1); + var instance = new ReactorInstance(federateClass, main, errorReporter); + var outputDelayMap = federate + .findOutputsConnectedToPhysicalActions(instance); + var minOutputDelay = TimeValue.MAX_VALUE; + Output outputFound = null; + for (Output output : outputDelayMap.keySet()) { + var outputDelay = outputDelayMap.get(output); + if (outputDelay.isEarlierThan(minOutputDelay)) { + minOutputDelay = outputDelay; + outputFound = output; + } + } + if (minOutputDelay != TimeValue.MAX_VALUE) { + // Unless silenced, issue a warning. + if (federate.targetConfig.coordinationOptions.advance_message_interval + == null) { + errorReporter.reportWarning(outputFound, String.join("\n", + "Found a path from a physical action to output for reactor " + + addDoubleQuotes(instance.getName()) + + ". ", + "The amount of delay is " + + minOutputDelay + + ".", + "With centralized coordination, this can result in a large number of messages to the RTI.", + "Consider refactoring the code so that the output does not depend on the physical action,", + "or consider using decentralized coordination. To silence this warning, set the target", + "parameter coordination-options with a value like {advance-message-interval: 10 msec}" + )); + } + return minOutputDelay; + } + } + return null; + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedASTUtils.java b/org.lflang/src/org/lflang/federated/generator/FedASTUtils.java new file mode 100644 index 0000000000..21fb3a0729 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedASTUtils.java @@ -0,0 +1,862 @@ +/************* + * Copyright (c) 2021, The University of California at Berkeley. + * Copyright (c) 2021, The University of Texas at Dallas. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.federated.generator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.xbase.lib.IteratorExtensions; + +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.InferredType; +import org.lflang.ModelInfo; +import org.lflang.TargetProperty.CoordinationType; +import org.lflang.TimeValue; +import org.lflang.federated.extensions.FedTargetExtensionFactory; +import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.lf.Action; +import org.lflang.lf.ActionOrigin; +import org.lflang.lf.BuiltinTriggerRef; +import org.lflang.lf.Connection; +import org.lflang.lf.Expression; +import org.lflang.lf.Instantiation; +import org.lflang.lf.LfFactory; +import org.lflang.lf.Model; +import org.lflang.lf.ParameterReference; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; +import org.lflang.lf.Type; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; + +import com.google.common.collect.Iterators; + +/** + * A helper class for AST transformations needed for federated + * execution. + * + * @author Soroush Bateni + * @author Edward A. Lee + */ +public class FedASTUtils { + + /** + * Map from reactions to bank indices + */ + private static Map reactionBankIndices = null; + + /** + * Mark the specified reaction to belong to only the specified + * bank index. This is needed because reactions cannot declare + * a specific bank index as an effect or trigger. Reactions that + * send messages between federates, including absent messages, + * need to be specific to a bank member. + * @param reaction The reaction. + * @param bankIndex The bank index, or -1 if there is no bank. + */ + public static void setReactionBankIndex(Reaction reaction, int bankIndex) { + if (bankIndex < 0) { + return; + } + if (reactionBankIndices == null) { + reactionBankIndices = new LinkedHashMap<>(); + } + reactionBankIndices.put(reaction, bankIndex); + } + + /** + * Return the reaction bank index. + * @see #setReactionBankIndex(Reaction reaction, int bankIndex) + * @param reaction The reaction. + * @return The reaction bank index, if one has been set, and -1 otherwise. + */ + public static int getReactionBankIndex(Reaction reaction) { + if (reactionBankIndices == null) return -1; + if (reactionBankIndices.get(reaction) == null) return -1; + return reactionBankIndices.get(reaction); + } + + /** + * Find the federated reactor in a .lf file. + * + * @param resource Resource representing a .lf file. + * @return The federated reactor if found. + */ + public static Reactor findFederatedReactor(Resource resource) { + return IteratorExtensions.findFirst( + Iterators.filter(resource.getAllContents(), Reactor.class), + Reactor::isFederated + ); + } + + /** + * Replace the specified connection with communication between federates. + * + * @param connection Network connection between two federates. + * @param coordination One of CoordinationType.DECENTRALIZED or + * CoordinationType.CENTRALIZED. + * @param errorReporter Used to report errors encountered. + */ + public static void makeCommunication( + FedConnectionInstance connection, + CoordinationType coordination, + ErrorReporter errorReporter + ) { + + // Add the sender reaction. + addNetworkSenderReaction( + connection, + coordination, + errorReporter + ); + + // Next, generate control reactions + if ( + !connection.getDefinition().isPhysical() && + // Connections that are physical don't need control reactions + connection.getDefinition().getDelay() + == null // Connections that have delays don't need control reactions + ) { + // Add the network output control reaction to the parent + FedASTUtils.addNetworkOutputControlReaction(connection); + + // Add the network input control reaction to the parent + FedASTUtils.addNetworkInputControlReaction(connection, coordination, errorReporter); + } + + // Create the network action (@see createNetworkAction) + Action networkAction = createNetworkAction(connection); + + // Keep track of this action in the destination federate. + connection.dstFederate.networkMessageActions.add(networkAction); + + // Add the action definition to the parent reactor. + ((Reactor) connection.getDefinition().eContainer()).getActions().add(networkAction); + + // Add the network receiver reaction in the destinationFederate + addNetworkReceiverReaction( + networkAction, + connection, + coordination, + errorReporter + ); + } + + /** + * Create a "network action" in the reactor that contains the given + * connection and return it. + * + * The purpose of this action is to serve as a trigger for a "network + * input reaction" that is responsible for relaying messages to the + * port that is on the receiving side of the given connection. The + * connection is assumed to be between two reactors that reside in + * distinct federates. Hence, the container of the connection is + * assumed to be top-level. + * + * @param connection A connection between two federates + * @return The newly created action. + */ + private static Action createNetworkAction(FedConnectionInstance connection) { + Reactor top = (Reactor) connection.getDefinition().eContainer(); + LfFactory factory = LfFactory.eINSTANCE; + + Action action = factory.createAction(); + // Name the newly created action; set its delay and type. + action.setName(ASTUtils.getUniqueIdentifier(top, "networkMessage")); + if (connection.serializer == SupportedSerializers.NATIVE) { + action.setType(EcoreUtil.copy(connection.getSourcePortInstance().getDefinition().getType())); + } else { + Type action_type = factory.createType(); + action_type.setId( + FedTargetExtensionFactory.getExtension( + connection.srcFederate.target + ).getNetworkBufferType() + ); + action.setType(action_type); + } + + // The connection is 'physical' if it uses the ~> notation. + if (connection.getDefinition().isPhysical()) { + action.setOrigin(ActionOrigin.PHYSICAL); + // Messages sent on physical connections do not + // carry a timestamp, or a delay. The delay + // provided using after is enforced by setting + // the minDelay. + if (connection.getDefinition().getDelay() != null) { + action.setMinDelay(connection.getDefinition().getDelay()); + } + } else { + action.setOrigin(ActionOrigin.LOGICAL); + } + + return action; + } + + /** + * Add a network receiver reaction for a given input port 'destination' to + * destination's parent reactor. This reaction will react to a generated + * 'networkAction' (triggered asynchronously, e.g., by federate.c). This + * 'networkAction' will contain the actual message that is sent by the + * sender + * in 'action->value'. This value is forwarded to 'destination' in the + * network + * receiver reaction. + * + * @param networkAction The network action (also, @see createNetworkAction) + * @param connection FIXME + * @param coordination One of CoordinationType.DECENTRALIZED or + * CoordinationType.CENTRALIZED. + * @note: Used in federated execution + */ + private static void addNetworkReceiverReaction( + Action networkAction, + FedConnectionInstance connection, + CoordinationType coordination, + ErrorReporter errorReporter + ) { + LfFactory factory = LfFactory.eINSTANCE; + VarRef sourceRef = factory.createVarRef(); + VarRef destRef = factory.createVarRef(); + Reactor parent = (Reactor) connection.getDefinition().eContainer(); + Reaction networkReceiverReaction = factory.createReaction(); + + // If the sender or receiver is in a bank of reactors, then we want + // these reactions to appear only in the federate whose bank ID matches. + setReactionBankIndex(networkReceiverReaction, connection.getDstBank()); + + // FIXME: do not create a new extension every time it is used + FedTargetExtensionFactory.getExtension(connection.srcFederate.target) + .annotateReaction(networkReceiverReaction); + + // The connection is 'physical' if it uses the ~> notation. + if (connection.getDefinition().isPhysical()) { + connection.dstFederate.inboundP2PConnections.add(connection.srcFederate); + } else { + // If the connection is logical but coordination + // is decentralized, we would need + // to make P2P connections + if (coordination == CoordinationType.DECENTRALIZED) { + connection.dstFederate.inboundP2PConnections.add(connection.srcFederate); + } + } + + // Establish references to the involved ports. + sourceRef.setContainer(connection.getSourcePortInstance().getParent().getDefinition()); + sourceRef.setVariable(connection.getSourcePortInstance().getDefinition()); + destRef.setContainer(connection.getDestinationPortInstance().getParent().getDefinition()); + destRef.setVariable(connection.getDestinationPortInstance().getDefinition()); + + // Add the input port at the receiver federate reactor as an effect + networkReceiverReaction.getEffects().add(destRef); + + VarRef triggerRef = factory.createVarRef(); + // Establish references to the action. + triggerRef.setVariable(networkAction); + // Add the action as a trigger to the receiver reaction + networkReceiverReaction.getTriggers().add(triggerRef); + + // Generate code for the network receiver reaction + networkReceiverReaction.setCode(factory.createCode()); + networkReceiverReaction.getCode().setBody( + FedTargetExtensionFactory.getExtension( + connection.dstFederate.target).generateNetworkReceiverBody( + networkAction, + sourceRef, + destRef, + connection, + ASTUtils.getInferredType(networkAction), + coordination, + errorReporter + )); + + + ASTUtils.addReactionAttribute(networkReceiverReaction, "_unordered"); + + // Add the receiver reaction to the parent + parent.getReactions().add(networkReceiverReaction); + + // Add the network receiver reaction to the federate instance's list + // of network reactions + connection.dstFederate.networkReactions.add(networkReceiverReaction); + + + if ( + !connection.getDefinition().isPhysical() && + // Connections that are physical don't need control reactions + connection.getDefinition().getDelay() + == null // Connections that have delays don't need control reactions + ) { + // Add necessary dependencies to reaction to ensure that it executes correctly + // relative to other network input control reactions in the federate. + addRelativeDependency(connection, networkReceiverReaction, errorReporter); + } + } + + /** + * Add a network control reaction for a given input port 'destination' to + * destination's parent reactor. This reaction will block for + * any valid logical time until it is known whether the trigger for the + * action corresponding to the given port is present or absent. + * + * @param connection FIXME + * @param coordination FIXME + * @param errorReporter + * @note Used in federated execution + */ + private static void addNetworkInputControlReaction( + FedConnectionInstance connection, + CoordinationType coordination, + ErrorReporter errorReporter) { + + LfFactory factory = LfFactory.eINSTANCE; + Reaction reaction = factory.createReaction(); + VarRef destRef = factory.createVarRef(); + int receivingPortID = connection.dstFederate.networkMessageActions.size(); + + // If the sender or receiver is in a bank of reactors, then we want + // these reactions to appear only in the federate whose bank ID matches. + setReactionBankIndex(reaction, connection.getDstBank()); + + // FIXME: do not create a new extension every time it is used + FedTargetExtensionFactory.getExtension(connection.srcFederate.target) + .annotateReaction(reaction); + + // Create a new action that will be used to trigger the + // input control reactions. + Action newTriggerForControlReactionInput = factory.createAction(); + newTriggerForControlReactionInput.setOrigin(ActionOrigin.LOGICAL); + + // Set the container and variable according to the network port + destRef.setContainer(connection.getDestinationPortInstance().getParent().getDefinition()); + destRef.setVariable(connection.getDestinationPortInstance().getDefinition()); + + Reactor top = connection.getDestinationPortInstance() + .getParent() + .getParent().reactorDefinition; + + newTriggerForControlReactionInput.setName( + ASTUtils.getUniqueIdentifier(top, "inputControlReactionTrigger")); + + // Add the newly created Action to the action list of the federated reactor. + top.getActions().add(newTriggerForControlReactionInput); + + // Create the trigger for the reaction + VarRef newTriggerForControlReaction = factory.createVarRef(); + newTriggerForControlReaction.setVariable(newTriggerForControlReactionInput); + + // Add the appropriate triggers to the list of triggers of the reaction + reaction.getTriggers().add(newTriggerForControlReaction); + + // Add the destination port as an effect of the reaction + reaction.getEffects().add(destRef); + + // Generate code for the network input control reaction + reaction.setCode(factory.createCode()); + + TimeValue maxSTP = findMaxSTP(connection, coordination); + + reaction.getCode() + .setBody( + FedTargetExtensionFactory + .getExtension(connection.dstFederate.target) + .generateNetworkInputControlReactionBody( + receivingPortID, + maxSTP, + coordination + ) + ); + + ASTUtils.addReactionAttribute(reaction, "_unordered"); + + // Insert the reaction + top.getReactions().add(reaction); + + // Add the trigger for this reaction to the list of triggers, used to actually + // trigger the reaction at the beginning of each logical time. + connection.dstFederate.networkInputControlReactionsTriggers.add(newTriggerForControlReactionInput); + + // Add the network input control reaction to the federate instance's list + // of network reactions + connection.dstFederate.networkReactions.add(reaction); + + // Add necessary dependencies to reaction to ensure that it executes correctly + // relative to other network input control reactions in the federate. + addRelativeDependency(connection, reaction, errorReporter); + } + + /** + * Add necessary dependency information to the signature of {@code networkInputReaction} so that + * it can execute in the correct order relative to other network reactions in the federate. + * + * In particular, we want to avoid a deadlock if multiple network input control reactions in federate + * are in a zero-delay cycle through other federates in the federation. To avoid the deadlock, we encode the + * zero-delay cycle inside the federate by adding an artificial dependency from the output port of this federate + * that is involved in the cycle to the signature of {@code networkInputReaction} as a source. + */ + private static void addRelativeDependency( + FedConnectionInstance connection, + Reaction networkInputReaction, + ErrorReporter errorReporter) { + var upstreamOutputPortsInFederate = + findUpstreamPortsInFederate( + connection.dstFederate, + connection.getSourcePortInstance(), + new HashSet<>(), + new HashSet<>() + ); + + ModelInfo info = new ModelInfo(); + for (var port: upstreamOutputPortsInFederate) { + VarRef sourceRef = ASTUtils.factory.createVarRef(); + + sourceRef.setContainer(port.getParent().getDefinition()); + sourceRef.setVariable(port.getDefinition()); + networkInputReaction.getSources().add(sourceRef); + + // Remove the port if it introduces cycles + info.update( + (Model)networkInputReaction.eContainer().eContainer(), + errorReporter + ); + if (!info.topologyCycles().isEmpty()) { + networkInputReaction.getSources().remove(sourceRef); + } + } + + } + + /** + * Go upstream from input port {@code port} until we reach one or more output + * ports that belong to the same federate. + * + * Along the path, we follow direct connections, as well as reactions, as long + * as there is no logical delay. When following reactions, we also follow + * dependant reactions (because we are traversing a potential cycle backwards). + * + * @return A set of {@link PortInstance}. If no port exist that match the + * criteria, return an empty set. + */ + private static Set findUpstreamPortsInFederate( + FederateInstance federate, + PortInstance port, + Set visitedPorts, + Set reactionVisited + ) { + Set toReturn = new HashSet<>(); + if (port == null) return toReturn; + else if (federate.contains(port.getParent())) { + // Reached the requested federate + toReturn.add(port); + visitedPorts.add(port); + } else if (visitedPorts.contains(port)) { + return toReturn; + } else { + visitedPorts.add(port); + // Follow depends on reactions + port.getDependsOnReactions().forEach( + reaction -> { + followReactionUpstream(federate, visitedPorts, toReturn, reaction, reactionVisited); + } + ); + // Follow depends on ports + port.getDependsOnPorts().forEach( + it -> toReturn.addAll( + findUpstreamPortsInFederate(federate, it.instance, visitedPorts, reactionVisited) + ) + ); + } + return toReturn; + } + + /** + * Follow reactions upstream. This is part of the algorithm of + * {@link #findUpstreamPortsInFederate(FederateInstance, PortInstance, Set)}. + */ + private static void followReactionUpstream( + FederateInstance federate, + Set visitedPorts, + Set toReturn, + ReactionInstance reaction, + Set reactionVisited + ) { + if (reactionVisited.contains(reaction)) return; + reactionVisited.add(reaction); + // Add triggers + Set varRefsToFollow = new HashSet<>(); + varRefsToFollow.addAll( + reaction.getDefinition() + .getTriggers() + .stream() + .filter( + trigger -> !(trigger instanceof BuiltinTriggerRef) + ) + .map(VarRef.class::cast).toList()); + // Add sources + varRefsToFollow.addAll(reaction.getDefinition().getSources()); + + // Follow everything upstream + varRefsToFollow.forEach( + varRef -> + toReturn.addAll( + findUpstreamPortsInFederate( + federate, + reaction.getParent() + .lookupPortInstance(varRef), + visitedPorts, + reactionVisited + ) + ) + ); + + reaction.dependsOnReactions() + .stream() + .filter( + // Stay within the reactor + it -> it.getParent().equals(reaction.getParent()) + ) + .forEach( + it -> followReactionUpstream(federate, visitedPorts, toReturn, it, reactionVisited) + ); + + // FIXME: This is most certainly wrong. Please fix it. + reaction.dependentReactions() + .stream() + .filter( + // Stay within the reactor + it -> it.getParent().equals(reaction.getParent()) + ) + .forEach( + it -> followReactionUpstream(federate, visitedPorts, toReturn, it, reactionVisited) + ); + } + + /** + * Find the maximum STP offset for the given 'port'. + * + * An STP offset predicate can be nested in contained reactors in + * the federate. + * + * @param connection The connection to find the max STP offset for. + * @param coordination The coordination scheme. + * @return The maximum STP as a TimeValue + */ + private static TimeValue findMaxSTP(FedConnectionInstance connection, + CoordinationType coordination) { + Variable port = connection.getDestinationPortInstance().getDefinition(); + FederateInstance instance = connection.dstFederate; + Reactor reactor = connection.getDestinationPortInstance().getParent().reactorDefinition; + + // Find a list of STP offsets (if any exists) + List STPList = new LinkedList<>(); + + // First, check if there are any connections to contained reactors that + // need to be handled + List connectionsWithPort = ASTUtils + .allConnections(reactor).stream().filter(c -> c.getLeftPorts() + .stream() + .anyMatch((VarRef v) -> v + .getVariable().equals(port))) + .collect(Collectors.toList()); + + + // Find the list of reactions that have the port as trigger or source + // (could be a variable name) + List reactionsWithPort = ASTUtils + .allReactions(reactor).stream().filter(r -> { + // Check the triggers of reaction r first + return r.getTriggers().stream().anyMatch(t -> { + if (t instanceof VarRef) { + // Check if the variables match + return ((VarRef) t).getVariable() == port; + } else { + // Not a network port (startup or shutdown) + return false; + } + }) || // Then check the sources of reaction r + r.getSources().stream().anyMatch(s -> s.getVariable() + == port); + }).collect(Collectors.toList()); + + // Find a list of STP offsets (if any exists) + if (coordination == CoordinationType.DECENTRALIZED) { + for (Reaction r : safe(reactionsWithPort)) { + // If STP offset is determined, add it + // If not, assume it is zero + if (r.getStp() != null) { + if (r.getStp().getValue() instanceof ParameterReference) { + List instantList = new ArrayList<>(); + instantList.add(instance.instantiation); + final var param = ((ParameterReference) r.getStp().getValue()).getParameter(); + STPList.addAll(ASTUtils.initialValue(param, instantList)); + } else { + STPList.add(r.getStp().getValue()); + } + } + } + // Check the children for STPs as well + for (Connection c : safe(connectionsWithPort)) { + VarRef childPort = c.getRightPorts().get(0); + Reactor childReactor = (Reactor) childPort.getVariable() + .eContainer(); + // Find the list of reactions that have the port as trigger or + // source (could be a variable name) + List childReactionsWithPort = + ASTUtils.allReactions(childReactor) + .stream() + .filter(r -> + r.getTriggers().stream().anyMatch(t -> { + if (t instanceof VarRef) { + // Check if the variables match + return + ((VarRef) t).getVariable() + == childPort.getVariable(); + } else { + // Not a network port (startup or shutdown) + return false; + } + }) || + r.getSources() + .stream() + .anyMatch(s -> + s.getVariable() + == childPort.getVariable()) + ).collect(Collectors.toList()); + + for (Reaction r : safe(childReactionsWithPort)) { + // If STP offset is determined, add it + // If not, assume it is zero + if (r.getStp() != null) { + if (r.getStp().getValue() instanceof ParameterReference) { + List instantList = new ArrayList<>(); + instantList.add(childPort.getContainer()); + final var param = ((ParameterReference) r.getStp().getValue()).getParameter(); + STPList.addAll(ASTUtils.initialValue(param, instantList)); + } else { + STPList.add(r.getStp().getValue()); + } + } + } + } + } + + return STPList.stream() + .map(ASTUtils::getLiteralTimeValue) + .filter(Objects::nonNull) + .reduce(TimeValue.ZERO, TimeValue::max); + } + + /** + * Return a null-safe List + * + * @param The type of the list + * @param list The potentially null List + * @return Empty list or the original list + */ + public static List safe(List list) { + return list == null ? Collections.emptyList() : list; + } + + /** + * Add a network sender reaction for a given input port 'source' to + * source's parent reactor. This reaction will react to the 'source' + * and then send a message on the network destined for the + * destinationFederate. + * + * @param connection Network connection between two federates. + * @param coordination One of CoordinationType.DECENTRALIZED or + * CoordinationType.CENTRALIZED. + * @param errorReporter FIXME + * @note Used in federated execution + */ + private static void addNetworkSenderReaction( + FedConnectionInstance connection, + CoordinationType coordination, + ErrorReporter errorReporter + ) { + LfFactory factory = LfFactory.eINSTANCE; + // Assume all the types are the same, so just use the first on the right. + Type type = EcoreUtil.copy(connection.getSourcePortInstance().getDefinition().getType()); + VarRef sourceRef = factory.createVarRef(); + VarRef destRef = factory.createVarRef(); + Reactor parent = (Reactor) connection.getDefinition().eContainer(); + Reaction networkSenderReaction = factory.createReaction(); + + // FIXME: do not create a new extension every time it is used + FedTargetExtensionFactory.getExtension(connection.srcFederate.target) + .annotateReaction(networkSenderReaction); + + // If the sender or receiver is in a bank of reactors, then we want + // these reactions to appear only in the federate whose bank ID matches. + setReactionBankIndex(networkSenderReaction, connection.getSrcBank()); + + // The connection is 'physical' if it uses the ~> notation. + if (connection.getDefinition().isPhysical()) { + connection.srcFederate.outboundP2PConnections.add(connection.dstFederate); + } else { + // If the connection is logical but coordination + // is decentralized, we would need + // to make P2P connections + if (coordination == CoordinationType.DECENTRALIZED) { + connection.srcFederate.outboundP2PConnections.add(connection.dstFederate); + } + } + + // Establish references to the involved ports. + sourceRef.setContainer(connection.getSourcePortInstance().getParent().getDefinition()); + sourceRef.setVariable(connection.getSourcePortInstance().getDefinition()); + destRef.setContainer(connection.getDestinationPortInstance().getParent().getDefinition()); + destRef.setVariable(connection.getDestinationPortInstance().getDefinition()); + + // Configure the sending reaction. + networkSenderReaction.getTriggers().add(sourceRef); + networkSenderReaction.setCode(factory.createCode()); + networkSenderReaction.getCode().setBody( + FedTargetExtensionFactory.getExtension(connection.srcFederate.target) + .generateNetworkSenderBody( + sourceRef, + destRef, + connection, + InferredType.fromAST(type), + coordination, + errorReporter + )); + + ASTUtils.addReactionAttribute(networkSenderReaction, "_unordered"); + + // Add the sending reaction to the parent. + parent.getReactions().add(networkSenderReaction); + + // Add the network sender reaction to the federate instance's list + // of network reactions + connection.srcFederate.networkReactions.add(networkSenderReaction); + } + + /** + * Add a network control reaction for a given output port 'source' to + * source's parent reactor. This reaction will send a port absent + * message if the status of the output port is absent. + * + * @param connection FIXME + * @note Used in federated execution + */ + private static void addNetworkOutputControlReaction(FedConnectionInstance connection) { + LfFactory factory = LfFactory.eINSTANCE; + Reaction reaction = factory.createReaction(); + Reactor top = connection.getSourcePortInstance() + .getParent() + .getParent().reactorDefinition; // Top-level reactor. + + // Add the output from the contained reactor as a source to + // the reaction to preserve precedence order. + VarRef newPortRef = factory.createVarRef(); + newPortRef.setContainer(connection.getSourcePortInstance().getParent().getDefinition()); + newPortRef.setVariable(connection.getSourcePortInstance().getDefinition()); + reaction.getSources().add(newPortRef); + + // If the sender or receiver is in a bank of reactors, then we want + // these reactions to appear only in the federate whose bank ID matches. + setReactionBankIndex(reaction, connection.getSrcBank()); + + // FIXME: do not create a new extension every time it is used + FedTargetExtensionFactory.getExtension(connection.srcFederate.target) + .annotateReaction(reaction); + + // We use an action at the top-level to manually + // trigger output control reactions. That action is created once + // and recorded in the federate instance. + // Check whether the action already has been created. + if (connection.srcFederate.networkOutputControlReactionsTrigger == null) { + // The port has not been created. + String triggerName = "outputControlReactionTrigger"; + + // Find the trigger definition in the reactor definition, which could have been + // generated for another federate instance if there are multiple instances + // of the same reactor that are each distinct federates. + Optional optTriggerInput + = top.getActions().stream().filter( + I -> I.getName().equals(triggerName)).findFirst(); + + if (optTriggerInput.isEmpty()) { + // If no trigger with the name "outputControlReactionTrigger" is + // already added to the reactor definition, we need to create it + // for the first time. The trigger is a logical action. + Action newTriggerForControlReactionVariable = factory.createAction(); + newTriggerForControlReactionVariable.setName(triggerName); + newTriggerForControlReactionVariable.setOrigin(ActionOrigin.LOGICAL); + top.getActions().add(newTriggerForControlReactionVariable); + + // Now that the variable is created, store it in the federate instance + connection.srcFederate.networkOutputControlReactionsTrigger + = newTriggerForControlReactionVariable; + } else { + // If the "outputControlReactionTrigger" trigger is already + // there, we can re-use it for this new reaction since a single trigger + // will trigger + // all network output control reactions. + connection.srcFederate.networkOutputControlReactionsTrigger = optTriggerInput.get(); + } + } + + // Add the trigger for all output control reactions to the list of triggers + VarRef triggerRef = factory.createVarRef(); + triggerRef.setVariable(connection.srcFederate.networkOutputControlReactionsTrigger); + reaction.getTriggers().add(triggerRef); + + // Generate the code + reaction.setCode(factory.createCode()); + + reaction.getCode().setBody( + FedTargetExtensionFactory.getExtension(connection.srcFederate.target) + .generateNetworkOutputControlReactionBody(newPortRef, connection)); + + ASTUtils.addReactionAttribute(reaction, "_unordered"); + + + // Insert the newly generated reaction after the generated sender and + // receiver top-level reactions. + top.getReactions().add(reaction); + + // Add the network output control reaction to the federate instance's list + // of network reactions + connection.srcFederate.networkReactions.add(reaction); + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedConnectionInstance.java b/org.lflang/src/org/lflang/federated/generator/FedConnectionInstance.java new file mode 100644 index 0000000000..a82e1d2dfa --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedConnectionInstance.java @@ -0,0 +1,110 @@ +package org.lflang.federated.generator; + +import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.PortInstance; +import org.lflang.generator.RuntimeRange; +import org.lflang.generator.SendRange; +import org.lflang.lf.Connection; + +/** + * Class representing a federated connection. + * + * This is an undocumented class written by a previous contributor who is no longer active. + * It merely serves as a record, presumably to make it easier to pass around information + * around. + * + * @author Soroush Bateni + */ +public class FedConnectionInstance { + + SendRange srcRange; + + RuntimeRange dstRange; + + int srcChannel; + + int srcBank; + + int dstChannel; + + int dstBank; + + FederateInstance srcFederate; + + FederateInstance dstFederate; + + SupportedSerializers serializer; + + public FedConnectionInstance( + SendRange srcRange, + RuntimeRange dstRange, + int srcChannel, + int srcBank, + int dstChannel, + int dstBank, + FederateInstance srcFederate, + FederateInstance dstFederate, + SupportedSerializers serializer + ) { + this.srcRange = srcRange; + this.srcChannel = srcChannel; + this.srcBank = srcBank; + this.dstChannel = dstChannel; + this.dstBank = dstBank; + this.srcFederate = srcFederate; + this.dstFederate = dstFederate; + this.dstRange = dstRange; + this.serializer = serializer; + + this.srcFederate.connections.add(this); + this.dstFederate.connections.add(this); + } + + public SendRange getSrcRange() { + return srcRange; + } + + public RuntimeRange getDstRange() { + return dstRange; + } + + public int getSrcChannel() { + return srcChannel; + } + + public int getSrcBank() { + return srcBank; + } + + public int getDstChannel() { + return dstChannel; + } + + public int getDstBank() { + return dstBank; + } + + public FederateInstance getSrcFederate() { + return srcFederate; + } + + public FederateInstance getDstFederate() { + return dstFederate; + } + + public SupportedSerializers getSerializer() { + return serializer; + } + + public Connection getDefinition() { + return srcRange.connection; + } + + public PortInstance getSourcePortInstance() { + return srcRange.instance; + } + + public PortInstance getDestinationPortInstance() { + return dstRange.instance; + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedEmitter.java b/org.lflang/src/org/lflang/federated/generator/FedEmitter.java new file mode 100644 index 0000000000..e86af3ab77 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedEmitter.java @@ -0,0 +1,76 @@ +package org.lflang.federated.generator; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.lflang.ErrorReporter; +import org.lflang.generator.CodeMap; +import org.lflang.generator.LFGeneratorContext; +import org.lflang.lf.Reactor; + +/** + * Helper class to generate code for federates. + */ +public class FedEmitter { + + private final FedFileConfig fileConfig; + private final Reactor originalMainReactor; + private final ErrorReporter errorReporter; + private final LinkedHashMap federationRTIProperties; + + public FedEmitter( + FedFileConfig fileConfig, + Reactor originalMainReactor, + ErrorReporter errorReporter, + LinkedHashMap federationRTIProperties + ) { + this.fileConfig = fileConfig; + this.originalMainReactor = originalMainReactor; + this.errorReporter = errorReporter; + this.federationRTIProperties = federationRTIProperties; + } + + /** + * Generate a .lf file for federate {@code federate}. + * + * @throws IOException + */ + Map generateFederate( + LFGeneratorContext context, + FederateInstance federate, + int numOfFederates + ) throws IOException { + String fedName = federate.name; + Files.createDirectories(fileConfig.getSrcPath()); + System.out.println("##### Generating code for federate " + fedName + + " in directory " + + fileConfig.getSrcPath()); + + Path lfFilePath = fileConfig.getSrcPath().resolve( + fedName + ".lf"); + + String federateCode = String.join( + "\n", + new FedTargetEmitter().generateTarget(context, numOfFederates, federate, fileConfig, errorReporter, federationRTIProperties), + new FedImportEmitter().generateImports(federate, fileConfig), + new FedPreambleEmitter().generatePreamble(federate, fileConfig, federationRTIProperties, errorReporter), + new FedReactorEmitter().generateReactorDefinitions(federate), + new FedMainEmitter().generateMainReactor( + federate, + originalMainReactor, + errorReporter + ) + ); + Map codeMapMap = new HashMap<>(); + try (var srcWriter = Files.newBufferedWriter(lfFilePath)) { + var codeMap = CodeMap.fromGeneratedCode(federateCode); + codeMapMap.put(lfFilePath, codeMap); + srcWriter.write(codeMap.getGeneratedCode()); + } + return codeMapMap; + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedFileConfig.java b/org.lflang/src/org/lflang/federated/generator/FedFileConfig.java new file mode 100644 index 0000000000..8e9df19d6e --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedFileConfig.java @@ -0,0 +1,97 @@ +/************* + * Copyright (c) 2019-2021, The University of California at Berkeley. + + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.federated.generator; + +import java.io.IOException; +import java.nio.file.Path; + +import org.eclipse.emf.ecore.resource.Resource; + +import org.lflang.FileConfig; +import org.lflang.util.FileUtil; + +/** + * A subclass of @see FileConfig that extends the base functionality to add support + * for compiling federated LF programs. The code generator should create one instance + * of this class for each federate. + * + * @author Soroush Bateni + * + */ +public class FedFileConfig extends FileConfig { + + public FedFileConfig(Resource resource, Path srcGenBasePath, boolean useHierarchicalBin) throws IOException { + // FIMXE: It is unclear to me that we need this class. + super(resource, srcGenBasePath, useHierarchicalBin); + + } + + public FedFileConfig(FileConfig fileConfig) throws IOException { + super(fileConfig.resource, fileConfig.getSrcGenBasePath(), fileConfig.useHierarchicalBin); + } + + /** + * Return the path to the root of a LF project generated on the basis of a + * federated LF program currently under compilation. + */ + public Path getGenPath() { + return srcPkgPath.resolve("fed-gen").resolve(name); + } + + /** + * Return the path for storing generated LF sources that jointly constitute a + * federation. + */ + public Path getSrcPath() { + return getGenPath().resolve("src"); + } + + /** + * The directory in which to put the generated sources. + * This takes into account the location of the source file relative to the + * package root. Specifically, if the source file is x/y/Z.lf relative + * to the package root, then the generated sources will be put in x/y/Z + * relative to srcGenBasePath. + */ + @Override + public Path getSrcGenPath() { + return getGenPath().resolve("src-gen"); + } + + /** + * Return the path to the root of a LF project generated on the basis of a + * federated LF program currently under compilation. + */ + public Path getFedGenPath() { + return srcPkgPath.resolve("fed-gen").resolve(this.name); + } + + @Override + public void doClean() throws IOException { + super.doClean(); + FileUtil.deleteDirectory(this.getFedGenPath()); + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedGenerator.java b/org.lflang/src/org/lflang/federated/generator/FedGenerator.java new file mode 100644 index 0000000000..093ea2b760 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedGenerator.java @@ -0,0 +1,631 @@ +package org.lflang.federated.generator; + +import static org.lflang.generator.DockerGenerator.dockerGeneratorFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.generator.JavaIoFileSystemAccess; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.resource.XtextResourceSet; +import org.eclipse.xtext.xbase.lib.CollectionLiterals; +import org.eclipse.xtext.xbase.lib.Exceptions; +import org.eclipse.xtext.xbase.lib.Pair; +import org.eclipse.xtext.xbase.lib.Procedures.Procedure1; + +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.FileConfig; +import org.lflang.LFStandaloneSetup; +import org.lflang.Target; +import org.lflang.TargetConfig; +import org.lflang.TargetProperty.CoordinationType; +import org.lflang.federated.launcher.FedLauncher; +import org.lflang.federated.launcher.FedLauncherFactory; +import org.lflang.generator.CodeMap; +import org.lflang.generator.DockerData; +import org.lflang.generator.FedDockerComposeGenerator; +import org.lflang.generator.GeneratorResult.Status; +import org.lflang.generator.GeneratorUtils; +import org.lflang.generator.IntegratedBuilder; +import org.lflang.generator.LFGenerator; +import org.lflang.generator.LFGeneratorContext; +import org.lflang.generator.LFGeneratorContext.BuildParm; +import org.lflang.generator.MixedRadixInt; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.RuntimeRange; +import org.lflang.generator.SendRange; +import org.lflang.generator.SubContext; +import org.lflang.lf.Expression; +import org.lflang.lf.Instantiation; +import org.lflang.lf.LfFactory; +import org.lflang.lf.Reactor; +import org.lflang.lf.TargetDecl; + +import com.google.inject.Injector; + +public class FedGenerator { + + /** Average asynchronously reported numbers and do something with them. */ + private static class Averager { + private final int n; + private final int[] reports; + + /** Create an averager of reports from {@code n} processes. */ + public Averager(int n) { + this.n = n; + reports = new int[n]; + } + + /** + * Receive {@code x} from process {@code id} and invoke {@code callback} + * on the mean of the numbers most recently reported by the processes. + */ + public synchronized void report(int id, int x, Procedure1 callback) { + assert 0 <= id && id < n; + reports[id] = x; + callback.apply(Arrays.stream(reports).sum() / n); + } + } + + private final FedFileConfig fileConfig; + private final ErrorReporter errorReporter; + /** + * The current target configuration. + */ + private final TargetConfig targetConfig; + /** + * A list of federate instances. + */ + private final List federates = new ArrayList<>(); + /** + * A map from federate IDs to federate instances. + */ + private final Map federateByID = new LinkedHashMap<>(); + /** + * The federation RTI properties, which defaults to 'localhost: 15045'. + */ + final LinkedHashMap federationRTIProperties = CollectionLiterals.newLinkedHashMap( + Pair.of("host", "localhost"), + Pair.of("port", 0) // Indicator to use the default port, typically 15045. + ); + /** + * A map from instantiations to the federate instances for that + * instantiation. + * If the instantiation has a width, there may be more than one federate + * instance. + */ + private Map> federatesByInstantiation; + + /** + * Definition of the main (top-level) reactor. + * This is an automatically generated AST node for the top-level + * reactor. + */ + private Instantiation mainDef; + + public FedGenerator(LFGeneratorContext context) { + this.fileConfig = (FedFileConfig) context.getFileConfig(); + this.targetConfig = context.getTargetConfig(); + this.errorReporter = context.getErrorReporter(); + } + + public boolean doGenerate(Resource resource, LFGeneratorContext context) throws IOException { + if (!federatedExecutionIsSupported(resource)) return true; + cleanIfNeeded(context); + + // In a federated execution, we need keepalive to be true, + // otherwise a federate could exit simply because it hasn't received + // any messages. + targetConfig.keepalive = true; + + // Process command-line arguments + processCLIArguments(context); + + // Find the federated reactor + Reactor fedReactor = FedASTUtils.findFederatedReactor(resource); + + // Extract some useful information about the federation + analyzeFederates(fedReactor); + + // Find all the connections between federates. + // For each connection between federates, replace it in the + // AST with an action (which inherits the delay) and four reactions. + // The action will be physical for physical connections and logical + // for logical connections. + replaceFederateConnectionsWithProxies(fedReactor); + + createLauncher(fileConfig, errorReporter, federationRTIProperties); + + FedEmitter fedEmitter = new FedEmitter( + fileConfig, + ASTUtils.toDefinition(mainDef.getReactorClass()), + errorReporter, + federationRTIProperties + ); + // Generate code for each federate + Map lf2lfCodeMapMap = new HashMap<>(); + for (FederateInstance federate : federates) { + lf2lfCodeMapMap.putAll(fedEmitter.generateFederate( + context, federate, federates.size() + )); + } + + Map codeMapMap = compileFederates(context, lf2lfCodeMapMap, (subContexts) -> { + if (context.getTargetConfig().dockerOptions == null) return; + final List services = new ArrayList(); + // 1. create a Dockerfile for each federate + subContexts.forEach((subContext) -> { + // Inherit Docker properties from main context + subContext.getTargetConfig().dockerOptions = context.getTargetConfig().dockerOptions; + var dockerGenerator = dockerGeneratorFactory(subContext); + var dockerData = dockerGenerator.generateDockerData(); + try { + dockerData.writeDockerFile(); + } catch (IOException e) { + Exceptions.sneakyThrow(e); + } + services.add(dockerData); + }); + // 2. create a docker-compose.yml for the federation + try { + // FIXME: https://issue.lf-lang.org/1559 + // It appears that the rtiHost information should come from federationRTIproperties, + // which is a kludge and not in scope here. + (new FedDockerComposeGenerator(context, "localhost")).writeDockerComposeFile(services); + } catch (IOException e) { + Exceptions.sneakyThrow(e); + } + }); + + context.finish(Status.COMPILED, codeMapMap); + return false; // FIXME why false? + } + + /** + * Check if a clean was requested from the standalone compiler and perform + * the clean step. + * @param context + */ + private void cleanIfNeeded(LFGeneratorContext context) { + if (context.getArgs().containsKey(BuildParm.CLEAN.getKey())) { + try { + fileConfig.doClean(); + } catch (IOException e) { + System.err.println("WARNING: IO Error during clean"); + } + } + } + + /** + * Create a launcher for the federation. + * @param fileConfig + * @param errorReporter + * @param federationRTIProperties + */ + public void createLauncher( + FedFileConfig fileConfig, + ErrorReporter errorReporter, + LinkedHashMap federationRTIProperties + ) { + FedLauncher launcher; + if (federates.size() == 0) { + // no federates, use target properties of main file + TargetDecl targetDecl = GeneratorUtils.findTarget(fileConfig.resource); + launcher = FedLauncherFactory.getLauncher(Target.fromDecl(targetDecl), + targetConfig, + fileConfig, + errorReporter); + } else { + launcher = FedLauncherFactory.getLauncher( + federates.get(0), // FIXME: This would not work for mixed-target programs. + fileConfig, + errorReporter + ); + } + try { + launcher.createLauncher( + federates, + federationRTIProperties + ); + } catch (IOException e) { + errorReporter.reportError(e.getMessage()); + } + + // System.out.println(PythonInfoGenerator.generateFedRunInfo(fileConfig)); + } + + /** Return whether federated execution is supported for {@code resource}. */ + private boolean federatedExecutionIsSupported(Resource resource) { + var target = Target.fromDecl(GeneratorUtils.findTarget(resource)); + var ret = List.of(Target.C, Target.Python, Target.TS, Target.CPP, Target.CCPP).contains(target); + if (!ret) { + errorReporter.reportError( + "Federated execution is not supported with target " + target + "." + ); + } + return ret; + } + + private Map compileFederates( + LFGeneratorContext context, + Map lf2lfCodeMapMap, + Consumer> finalizer) { + + // FIXME: Use the appropriate resource set instead of always using standalone + Injector inj = new LFStandaloneSetup() + .createInjectorAndDoEMFRegistration(); + XtextResourceSet rs = inj.getInstance(XtextResourceSet.class); + rs.addLoadOption(XtextResource.OPTION_RESOLVE_ALL, Boolean.TRUE); + // define output path here + JavaIoFileSystemAccess fsa = inj.getInstance(JavaIoFileSystemAccess.class); + fsa.setOutputPath("DEFAULT_OUTPUT", fileConfig.getSrcGenPath().toString()); + + var numOfCompileThreads = Math.min(6, + Math.min( + Math.max(federates.size(), 1), + Runtime.getRuntime().availableProcessors() + ) + ); + var compileThreadPool = Executors.newFixedThreadPool(numOfCompileThreads); + System.out.println("******** Using "+numOfCompileThreads+" threads to compile the program."); + Map codeMapMap = new ConcurrentHashMap<>(); + List subContexts = Collections.synchronizedList(new ArrayList()); + Averager averager = new Averager(federates.size()); + final var threadSafeErrorReporter = new SynchronizedErrorReporter(errorReporter); + for (int i = 0; i < federates.size(); i++) { + FederateInstance fed = federates.get(i); + final int id = i; + compileThreadPool.execute(() -> { + Resource res = rs.getResource(URI.createFileURI( + fileConfig.getSrcPath().resolve(fed.name + ".lf").toAbsolutePath().toString() + ), true); + FileConfig subFileConfig = LFGenerator.createFileConfig(res, fileConfig.getSrcGenPath(), false); + ErrorReporter subContextErrorReporter = new LineAdjustingErrorReporter(threadSafeErrorReporter, lf2lfCodeMapMap); + + var props = new Properties(); + if (targetConfig.dockerOptions != null && targetConfig.target.buildsUsingDocker()) { + props.put("no-compile", "true"); + } + props.put("docker", "false"); + + TargetConfig subConfig = GeneratorUtils.getTargetConfig( + props, GeneratorUtils.findTarget(subFileConfig.resource), subContextErrorReporter + ); + SubContext subContext = new SubContext(context, IntegratedBuilder.VALIDATED_PERCENT_PROGRESS, 100) { + @Override + public ErrorReporter getErrorReporter() { + return subContextErrorReporter; + } + + @Override + public void reportProgress(String message, int percentage) { + averager.report(id, percentage, meanPercentage -> super.reportProgress(message, meanPercentage)); + } + + @Override + public FileConfig getFileConfig() { + return subFileConfig; + } + + @Override + public TargetConfig getTargetConfig() { + return subConfig; + } + }; + + inj.getInstance(LFGenerator.class).doGenerate(res, fsa, subContext); + codeMapMap.putAll(subContext.getResult().getCodeMaps()); + subContexts.add(subContext); + }); + } + // Initiate an orderly shutdown in which previously submitted tasks are + // executed, but no new tasks will be accepted. + compileThreadPool.shutdown(); + + // Wait for all compile threads to finish (NOTE: Can block forever) + try { + compileThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } catch (Exception e) { + Exceptions.sneakyThrow(e); + } finally { + finalizer.accept(subContexts); + } + return codeMapMap; + } + + /** + * Process command-line arguments passed on to the generator. + * + * @param context Context of the build process. + */ + private void processCLIArguments(LFGeneratorContext context) { + if (context.getArgs().containsKey("rti")) { + setFederationRTIProperties(context); + } + } + + /** + * Set the RTI hostname, port and username if given as compiler arguments + * + * @param context Context of the build process. + */ + private void setFederationRTIProperties(LFGeneratorContext context) { + String rtiAddr = context.getArgs().getProperty("rti"); + Pattern pattern = Pattern.compile("([a-zA-Z0-9]+@)?([a-zA-Z0-9]+\\.?[a-z]{2,}|[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+):?([0-9]+)?"); + Matcher matcher = pattern.matcher(rtiAddr); + + if (!matcher.find()) { + return; + } + + // the user match group contains a trailing "@" which needs to be removed. + String userWithAt = matcher.group(1); + String user = (userWithAt == null) ? null : userWithAt.substring(0, + userWithAt.length() + - 1); + String host = matcher.group(2); + String port = matcher.group(3); + + if (host != null) { + federationRTIProperties.put("host", host); + } + if (port != null) { + federationRTIProperties.put("port", port); + } + if (user != null) { + federationRTIProperties.put("user", user); + } + } + + /** + * Analyze the federation and record various properties of it. + * + * @param fedReactor The federated reactor that contains all federates' instances. + */ + private void analyzeFederates(Reactor fedReactor) { + // Create an instantiation for the fed reactor because there isn't one. + // Creating a definition for the main reactor because there isn't one. + mainDef = LfFactory.eINSTANCE.createInstantiation(); + mainDef.setName(fedReactor.getName()); + mainDef.setReactorClass(fedReactor); + + // Make sure that if no federation RTI properties were given in the + // cmdline, then those specified in the lf file are not lost + if (federationRTIProperties.get("host").equals("localhost") && + fedReactor.getHost() != null && + !fedReactor.getHost().getAddr().equals("localhost")) { + federationRTIProperties.put("host", fedReactor.getHost().getAddr()); + } + + // Since federates are always within the main (federated) reactor, + // create a list containing just that one containing instantiation. + // This will be used to look up parameter values. + List mainReactorContext = new ArrayList<>(); + mainReactorContext.add(mainDef); + + // Create a FederateInstance for each top-level reactor. + for (Instantiation instantiation : ASTUtils.allInstantiations(fedReactor)) { + int bankWidth = ASTUtils.width(instantiation.getWidthSpec(), mainReactorContext); + if (bankWidth < 0) { + errorReporter.reportError(instantiation, "Cannot determine bank width! Assuming width of 1."); + // Continue with a bank width of 1. + bankWidth = 1; + } + List federateInstances = getFederateInstances(instantiation, bankWidth); + if (federatesByInstantiation == null) { + federatesByInstantiation = new LinkedHashMap<>(); + } + federatesByInstantiation.put(instantiation, federateInstances); + } + } + + /** + * Get federate instances for a given {@code instantiation}. A bank will + * result in the creation of multiple federate instances (one for each + * member of the bank). + * + * @param instantiation An instantiation that corresponds to a federate. + * @param bankWidth The width specified for the instantiation. + * @return A list of federate instance (of type @see FederateInstance). + */ + private List getFederateInstances(Instantiation instantiation, int bankWidth) { + // Create one federate instance for each instance in a bank of reactors. + List federateInstances = new ArrayList<>(bankWidth); + for (int i = 0; i < bankWidth; i++) { + // Assign an integer ID to the federate. + int federateID = federates.size(); + FederateInstance federateInstance = new FederateInstance(instantiation, federateID, i, errorReporter); + federateInstance.bankIndex = i; + federates.add(federateInstance); + federateInstances.add(federateInstance); + federateByID.put(federateID, federateInstance); + + if (instantiation.getHost() != null) { + federateInstance.host = instantiation.getHost().getAddr(); + // The following could be 0. + federateInstance.port = instantiation.getHost().getPort(); + // The following could be null. + federateInstance.user = instantiation.getHost().getUser(); + /* FIXME: The at keyword should support a directory component. + * federateInstance.dir = instantiation.getHost().dir + */ + if (federateInstance.host != null + && !federateInstance.host.equals("localhost") + && !federateInstance.host.equals("0.0.0.0")) { + federateInstance.isRemote = true; + } + } + } + return federateInstances; + } + + /** + * Replace connections between federates in the AST with proxies that + * handle sending and receiving data. + * + * @param fedReactor + */ + private void replaceFederateConnectionsWithProxies(Reactor fedReactor) { + // Each connection in the AST may represent more than one connection between + // federate instances because of banks and multiports. We need to generate communication + // for each of these. To do this, we create a ReactorInstance so that we don't have + // to duplicate the rather complicated logic in that class. We specify a depth of 1, + // so it only creates the reactors immediately within the top level, not reactors + // that those contain. + ReactorInstance mainInstance = new ReactorInstance(fedReactor, errorReporter); + + for (ReactorInstance child : mainInstance.children) { + for (PortInstance output : child.outputs) { + replaceConnectionFromOutputPort(output); + } + } + + // Remove the connections at the top level + fedReactor.getConnections().clear(); + + // There will be AST transformations that invalidate some info + // cached in ReactorInstance. FIXME: most likely not needed anymore + mainInstance.clearCaches(false); + } + + /** + * Replace the connections from the specified output port. + * + * @param output The output port instance. + */ + private void replaceConnectionFromOutputPort(PortInstance output) { + // Iterate through ranges of the output port + for (SendRange srcRange : output.getDependentPorts()) { + if (srcRange.connection == null) { + // This should not happen. + errorReporter.reportError( + output.getDefinition(), + "Unexpected error. Cannot find output connection for port" + ); + continue; + } + // Iterate through destinations + for (RuntimeRange dstRange : srcRange.destinations) { + replaceOneToManyConnection( + srcRange, + dstRange + ); + } + } + } + + /** + * Replace (potentially multiple) connection(s) that originate from an + * output port to multiple destinations. + * + * @param srcRange A range of an output port that sources data for this + * connection. + * @param dstRange A range of input ports that receive the data. + */ + private void replaceOneToManyConnection( + SendRange srcRange, + RuntimeRange dstRange + ) { + MixedRadixInt srcID = srcRange.startMR(); + MixedRadixInt dstID = dstRange.startMR(); + int dstCount = 0; + int srcCount = 0; + + while (dstCount++ < dstRange.width) { + int srcChannel = srcID.getDigits().get(0); + int srcBank = srcID.get(1); + int dstChannel = dstID.getDigits().get(0); + int dstBank = dstID.get(1); + + FederateInstance srcFederate = federatesByInstantiation.get( + srcRange.instance.getParent().getDefinition() + ).get(srcBank); + FederateInstance dstFederate = federatesByInstantiation.get( + dstRange.instance.getParent().getDefinition() + ).get(dstBank); + + // Clear banks + srcFederate.instantiation.setWidthSpec(null); + dstFederate.instantiation.setWidthSpec(null); + + FedConnectionInstance fedConnection = new FedConnectionInstance( + srcRange, + dstRange, + srcChannel, + srcBank, + dstChannel, + dstBank, + srcFederate, + dstFederate, + FedUtils.getSerializer(srcRange.connection, srcFederate, dstFederate) + ); + + replaceFedConnection(fedConnection); + + dstID.increment(); + srcID.increment(); + srcCount++; + if (srcCount == srcRange.width) { + srcID = srcRange.startMR(); // Multicast. Start over. + } + } + } + + /** + * Replace a one-to-one federated connection with proxies. + * + * @param connection A connection between two federates. + */ + private void replaceFedConnection(FedConnectionInstance connection) { + if (!connection.getDefinition().isPhysical() + && targetConfig.coordination != CoordinationType.DECENTRALIZED) { + // Map the delays on connections between federates. + Set dependsOnDelays = + connection.dstFederate.dependsOn.computeIfAbsent( + connection.srcFederate, + k -> new LinkedHashSet<>() + ); + // Put the delay on the cache. + if (connection.getDefinition().getDelay() != null) { + dependsOnDelays.add(connection.getDefinition().getDelay()); + } else { + // To indicate that at least one connection has no delay, add a null entry. + dependsOnDelays.add(null); + } + // Map the connections between federates. + Set sendsToDelays = + connection.srcFederate.sendsTo.computeIfAbsent( + connection.dstFederate, + k -> new LinkedHashSet<>() + ); + if (connection.getDefinition().getDelay() != null) { + sendsToDelays.add(connection.getDefinition().getDelay()); + } else { + // To indicate that at least one connection has no delay, add a null entry. + sendsToDelays.add(null); + } + } + + FedASTUtils.makeCommunication(connection, targetConfig.coordination, errorReporter); + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedImportEmitter.java b/org.lflang/src/org/lflang/federated/generator/FedImportEmitter.java new file mode 100644 index 0000000000..86923c8914 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedImportEmitter.java @@ -0,0 +1,62 @@ +package org.lflang.federated.generator; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.emf.ecore.util.EcoreUtil; + +import org.lflang.ast.FormattingUtils; +import org.lflang.generator.CodeBuilder; +import org.lflang.lf.Import; +import org.lflang.lf.Model; + +/** + * Helper class to generate import statements for a federate. + * + * @author Soroush Bateni + */ +public class FedImportEmitter { + + private static Set visitedImports = new HashSet<>(); + + /** + * Generate import statements for {@code federate}. + */ + String generateImports(FederateInstance federate, FedFileConfig fileConfig) { + var imports = ((Model) federate.instantiation.eContainer().eContainer()) + .getImports() + .stream() + .filter(federate::contains).toList(); + + // Transform the URIs + imports.stream() + .filter(i -> !visitedImports.contains(i)) + .forEach(i -> { + visitedImports.add(i); + Path importPath = + fileConfig.srcPath + .resolve(i.getImportURI()).toAbsolutePath(); + i.setImportURI(fileConfig.getSrcPath().relativize(importPath) + .toString().replace('\\', '/') + ); + }); + + var importStatements = new CodeBuilder(); + + // Add import statements needed for the ordinary functionality of the federate + importStatements.pr(imports.stream() + .map(i -> { + var new_import = EcoreUtil.copy(i); + new_import.getReactorClasses().removeIf( + importedReactor -> !federate.contains(importedReactor) + ); + return new_import; + }) + .map(FormattingUtils.renderer(federate.target)) + .collect(Collectors.joining("\n"))); + + return importStatements.getCode(); + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedMainEmitter.java b/org.lflang/src/org/lflang/federated/generator/FedMainEmitter.java new file mode 100644 index 0000000000..ee3b64ab4a --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedMainEmitter.java @@ -0,0 +1,93 @@ +package org.lflang.federated.generator; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.eclipse.emf.ecore.EObject; + +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.ast.FormattingUtils; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.ReactorInstance; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Reactor; +import org.lflang.lf.Variable; + +/** + * Helper class to generate a main reactor + */ +public class FedMainEmitter { + + /** + * Generate a main reactor for {@code federate}. + * + * @param federate + * @param originalMainReactor The original main reactor. + * @param errorReporter Used to report errors. + * @return The main reactor. + */ + String generateMainReactor(FederateInstance federate, Reactor originalMainReactor, ErrorReporter errorReporter) { + // FIXME: Handle modes at the top-level + if (!ASTUtils.allModes(originalMainReactor).isEmpty()) { + errorReporter.reportError( + ASTUtils.allModes(originalMainReactor).stream().findFirst().get(), + "Modes at the top level are not supported under federated execution." + ); + } + var renderer = FormattingUtils.renderer(federate.target); + + return String + .join( + "\n", + generateMainSignature(federate, originalMainReactor, renderer), + String.join( + "\n", + renderer.apply(federate.instantiation), + ASTUtils.allStateVars(originalMainReactor).stream().filter(federate::contains).map(renderer).collect(Collectors.joining("\n")), + ASTUtils.allActions(originalMainReactor).stream().filter(federate::contains).map(renderer).collect(Collectors.joining("\n")), + ASTUtils.allTimers(originalMainReactor).stream().filter(federate::contains).map(renderer).collect(Collectors.joining("\n")), + ASTUtils.allMethods(originalMainReactor).stream().filter(federate::contains).map(renderer).collect(Collectors.joining("\n")), + ASTUtils.allReactions(originalMainReactor).stream().filter(federate::contains).map(renderer).collect(Collectors.joining("\n")) + ).indent(4).stripTrailing(), + "}" + ); + } + + /** + * Generate the signature of the main reactor. + * @param federate The federate. + * @param originalMainReactor The original main reactor of the original .lf file. + * @param renderer Used to render EObjects (in String representation). + */ + private CharSequence generateMainSignature(FederateInstance federate, Reactor originalMainReactor, Function renderer) { + var paramList = ASTUtils.allParameters(originalMainReactor) + .stream() + .filter(federate::contains) + .map(renderer) + .collect( + Collectors.joining( + ",", "(", ")" + ) + ); + // Empty "()" is currently not allowed by the syntax + + var networkMessageActionsListString = federate.networkMessageActions + .stream() + .map(Variable::getName) + .collect(Collectors.joining(",")); + + return + """ + @_fed_config(network_message_actions="%s") + main reactor %s { + """.formatted(networkMessageActionsListString, + paramList.equals("()") ? "" : paramList); + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedPreambleEmitter.java b/org.lflang/src/org/lflang/federated/generator/FedPreambleEmitter.java new file mode 100644 index 0000000000..827a31fe33 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedPreambleEmitter.java @@ -0,0 +1,46 @@ +package org.lflang.federated.generator; + +import static org.lflang.ASTUtils.toText; + +import java.io.IOException; +import java.util.LinkedHashMap; + +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.federated.extensions.FedTargetExtensionFactory; +import org.lflang.generator.CodeBuilder; +import org.lflang.lf.Model; +import org.lflang.lf.Preamble; + +public class FedPreambleEmitter { + + public FedPreambleEmitter() {} + + /** + * Add necessary code to the source and necessary build support to + * enable the requested serializations in 'enabledSerializations' + */ + String generatePreamble(FederateInstance federate, FedFileConfig fileConfig, LinkedHashMap federationRTIProperties, ErrorReporter errorReporter) + throws IOException { + CodeBuilder preambleCode = new CodeBuilder(); + + // Transfer top-level preambles + var mainModel = (Model) ASTUtils.toDefinition(federate.instantiation.getReactorClass()).eContainer(); + for (Preamble p : mainModel.getPreambles()) { + preambleCode.pr( + """ + %spreamble {= + %s + =} + """.formatted( + p.getVisibility() == null ? "" : p.getVisibility() + " ", + toText(p.getCode()) + )); + } + + preambleCode.pr(FedTargetExtensionFactory.getExtension(federate.target).generatePreamble( + federate, fileConfig, federationRTIProperties, errorReporter)); + + return preambleCode.getCode(); + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedReactorEmitter.java b/org.lflang/src/org/lflang/federated/generator/FedReactorEmitter.java new file mode 100644 index 0000000000..066b344417 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedReactorEmitter.java @@ -0,0 +1,24 @@ +package org.lflang.federated.generator; + +import java.util.stream.Collectors; + +import org.lflang.ast.FormattingUtils; +import org.lflang.lf.Model; + +public class FedReactorEmitter { + + public FedReactorEmitter() {} + + /** + * @param federate + * @return + */ + String generateReactorDefinitions(FederateInstance federate) { + return ((Model) federate.instantiation.eContainer().eContainer()) + .getReactors() + .stream() + .filter(federate::contains) + .map(FormattingUtils.renderer(federate.target)) + .collect(Collectors.joining("\n")); + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedTargetEmitter.java b/org.lflang/src/org/lflang/federated/generator/FedTargetEmitter.java new file mode 100644 index 0000000000..7143eb18c5 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedTargetEmitter.java @@ -0,0 +1,109 @@ +package org.lflang.federated.generator; + +import static org.lflang.ASTUtils.convertToEmptyListIfNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import org.lflang.ErrorReporter; +import org.lflang.Target; +import org.lflang.TargetProperty; +import org.lflang.ast.FormattingUtils; +import org.lflang.federated.extensions.FedTargetExtensionFactory; +import org.lflang.generator.GeneratorUtils; +import org.lflang.generator.LFGeneratorContext; + +public class FedTargetEmitter { + + String generateTarget( + LFGeneratorContext context, + int numOfFederates, + FederateInstance federate, + FedFileConfig fileConfig, + ErrorReporter errorReporter, + LinkedHashMap federationRTIProperties + ) throws IOException { + federate.targetConfig = + GeneratorUtils.getTargetConfig( + context.getArgs(), + federate.target, + errorReporter + ); + // FIXME: Should we merge some properties with the main .lf file if the federate is imported? + // https://issue.lf-lang.org/1560 + var fedReactorClass = federate.instantiation.getReactorClass(); + if (!fedReactorClass.eResource().equals(fileConfig.resource)) { + // Merge some target properties of the main .lf file. + var target = GeneratorUtils.findTarget(fileConfig.resource); + if (target.getConfig() != null) { + // Merge properties + TargetProperty.update( + federate.targetConfig, + convertToEmptyListIfNull(target.getConfig().getPairs()), + errorReporter + ); + } + } + + relativizeTargetPaths(federate, fileConfig); + + clearFederatedTargetPropertiesI(federate); + + FedTargetExtensionFactory.getExtension(federate.target) + .initializeTargetConfig( + context, + numOfFederates, + federate, + fileConfig, + errorReporter, + federationRTIProperties + ); + + return FormattingUtils.renderer(federate.target).apply( + TargetProperty.extractTargetDecl( + Target.fromDecl(federate.target), + federate.targetConfig + ) + ); + } + + /** + * Clear target properties that should not end up in the generated .lf file + * for {@code federate}. + */ + private void clearFederatedTargetPropertiesI(FederateInstance federate) { + federate.targetConfig.setByUser.remove(TargetProperty.CLOCK_SYNC); + federate.targetConfig.setByUser.remove(TargetProperty.CLOCK_SYNC_OPTIONS); + } + + + /** + * Relativize target properties that involve paths like files and cmake-include to be + * relative to the generated .lf file for the federate. + */ + private void relativizeTargetPaths(FederateInstance federate, FedFileConfig fileConfig) { + // FIXME: Should we relativize here or calculate absolute paths? + relativizePathList(federate.targetConfig.protoFiles, fileConfig); + + relativizePathList(federate.targetConfig.fileNames, fileConfig); + + relativizePathList(federate.targetConfig.cmakeIncludes, fileConfig); + } + + private void relativizePathList(List paths, FedFileConfig fileConfig) { + List tempList = new ArrayList<>(); + paths.forEach( f -> { + tempList.add(relativizePath(f, fileConfig)); + }); + paths.clear(); + paths.addAll(tempList); + } + + private String relativizePath(String path, FedFileConfig fileConfig) { + Path resolvedPath = fileConfig.srcPath.resolve(path).toAbsolutePath(); + return fileConfig.getSrcPath().relativize(resolvedPath).toString(); + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/FedUtils.java b/org.lflang/src/org/lflang/federated/generator/FedUtils.java new file mode 100644 index 0000000000..dd5ee6a9b3 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/FedUtils.java @@ -0,0 +1,38 @@ +package org.lflang.federated.generator; + +import java.util.List; + +import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.lf.Connection; +import org.lflang.lf.Reaction; +import org.lflang.lf.VarRef; + +/** + * A collection of utility methods for the federated generator. + */ +public class FedUtils { + /** + * Get the serializer for the {@code connection} between {@code srcFederate} and {@code dstFederate}. + */ + public static SupportedSerializers getSerializer( + Connection connection, + FederateInstance srcFederate, + FederateInstance dstFederate + ) { + // Get the serializer + SupportedSerializers serializer = SupportedSerializers.NATIVE; + if (connection.getSerializer() != null) { + serializer = SupportedSerializers.valueOf( + connection.getSerializer().getType().toUpperCase() + ); + } + // Add it to the list of enabled serializers for the source and destination federates + srcFederate.enabledSerializers.add(serializer); + dstFederate.enabledSerializers.add(serializer); + return serializer; + } + +} diff --git a/org.lflang/src/org/lflang/federated/FederateInstance.java b/org.lflang/src/org/lflang/federated/generator/FederateInstance.java similarity index 73% rename from org.lflang/src/org/lflang/federated/FederateInstance.java rename to org.lflang/src/org/lflang/federated/generator/FederateInstance.java index 6995fabd35..fd309dc4a1 100644 --- a/org.lflang/src/org/lflang/federated/FederateInstance.java +++ b/org.lflang/src/org/lflang/federated/generator/FederateInstance.java @@ -1,6 +1,5 @@ -/** Instance of a federate specification. */ - -/************* +/** Instance of a federate specification. + * Copyright (c) 2020, The University of California at Berkeley. Redistribution and use in source and binary forms, with or without modification, @@ -24,9 +23,10 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ***************/ -package org.lflang.federated; +package org.lflang.federated.generator; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -35,12 +35,15 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import java.util.stream.Collectors; import java.util.stream.Stream; -import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.eclipse.emf.ecore.EObject; + import org.lflang.ASTUtils; import org.lflang.ErrorReporter; +import org.lflang.TargetConfig; import org.lflang.TimeValue; +import org.lflang.federated.serialization.SupportedSerializers; import org.lflang.generator.ActionInstance; -import org.lflang.generator.GeneratorBase; +import org.lflang.generator.GeneratorUtils; import org.lflang.generator.PortInstance; import org.lflang.generator.ReactionInstance; import org.lflang.generator.ReactorInstance; @@ -48,11 +51,18 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; import org.lflang.lf.Expression; +import org.lflang.lf.Import; +import org.lflang.lf.ImportedReactor; import org.lflang.lf.Input; import org.lflang.lf.Instantiation; import org.lflang.lf.Output; +import org.lflang.lf.Parameter; +import org.lflang.lf.ParameterReference; import org.lflang.lf.Reaction; import org.lflang.lf.Reactor; +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.StateVar; +import org.lflang.lf.TargetDecl; import org.lflang.lf.Timer; import org.lflang.lf.TriggerRef; import org.lflang.lf.VarRef; @@ -63,11 +73,13 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY /** * Instance of a federate, or marker that no federation has been defined - * (if isSingleton() returns true). Every top-level reactor (contained + * (if isSingleton() returns true) FIXME: this comment makes no sense. + * Every top-level reactor (contained * directly by the main reactor) is a federate, so there will be one * instance of this class for each top-level reactor. * * @author Edward A. Lee + * @author Soroush Bateni */ public class FederateInstance { @@ -79,24 +91,21 @@ public class FederateInstance { * or null if no federation has been defined. * @param id The federate ID. * @param bankIndex If instantiation.widthSpec !== null, this gives the bank position. - * @param generator The generator * @param errorReporter The error reporter - * - * FIXME: Do we really need to pass the complete generator here? It is only used - * to determine the number of federates. */ public FederateInstance( Instantiation instantiation, int id, - int bankIndex, - GeneratorBase generator, + int bankIndex, ErrorReporter errorReporter) { this.instantiation = instantiation; this.id = id; - this.generator = generator; this.bankIndex = bankIndex; this.errorReporter = errorReporter; - + this.target = GeneratorUtils.findTarget( + ASTUtils.toDefinition(instantiation.getReactorClass()).eResource() + ); + this.targetConfig = new TargetConfig(target); // FIXME: this is actually set in FedTargetEmitter. Why? if (instantiation != null) { this.name = instantiation.getName(); // If the instantiation is in a bank, then we have to append @@ -125,16 +134,25 @@ public FederateInstance( * The host, if specified using the 'at' keyword. */ public String host = "localhost"; - + + /** * The instantiation of the top-level reactor, or null if there is no federation. */ public Instantiation instantiation; + public Instantiation getInstantiation() { + return instantiation; + } + + /** + * A list of individual connections between federates + */ + public Set connections = new HashSet<>(); /** * Map from the federates that this federate receives messages from * to the delays on connections from that federate. The delay set - * may may include null, meaning that there is a connection + * may include null, meaning that there is a connection * from the federate instance that has no delay. */ public Map> dependsOn = new LinkedHashMap<>(); @@ -166,10 +184,11 @@ public FederateInstance( * The integer ID of this federate. */ public int id = 0; - + + /** * The name of this federate instance. This will be the instantiation - * name, poassibly appended with "__n", where n is the bank position of + * name, possibly appended with "__n", where n is the bank position of * this instance if the instantiation is of a bank of reactors. */ public String name = "Unnamed"; @@ -228,57 +247,146 @@ public FederateInstance( * reactions) that belong to this federate instance. */ public List networkReactions = new ArrayList<>(); - + /** - * List of triggers of network reactions that belong to remote federates. - * These might need to be removed before code generation to avoid unnecessary compile - * errors, since they might reference structures that are not present in - * the current federate. Even though it is impossible for a trigger that is on a remote - * federate to trigger a reaction on this federate, these triggers need to be here - * to ensure that dependency analysis between reactions is done correctly. - * Without these triggers, the reaction precedence graph is broken and - * dependencies not properly represented. + * Target of the federate. */ - public List remoteNetworkReactionTriggers = new ArrayList<>(); + public TargetDecl target; - ///////////////////////////////////////////// - //// Public Methods + /** + * Parsed target config of the federate. + */ + public TargetConfig targetConfig; + + /** + * Keep a unique list of enabled serializers + */ + public HashSet enabledSerializers = new HashSet<>(); + + /** + * Return true if the specified EObject should be included in the code + * generated for this federate. + * + * @param object An {@code EObject} + * @return True if this federate contains the EObject. + */ + public boolean contains(EObject object) { + if (object instanceof Action) { + return contains((Action)object); + } else if (object instanceof Reaction) { + return contains((Reaction)object); + } else if (object instanceof Timer) { + return contains((Timer)object); + } else if (object instanceof ReactorDecl) { + return contains(this.instantiation, (ReactorDecl)object); + } else if (object instanceof Import) { + return contains((Import)object); + } else if (object instanceof Parameter) { + return contains((Parameter)object); + } else if (object instanceof StateVar) { + return true; // FIXME: Should we disallow state vars at the top level? + } + throw new UnsupportedOperationException("EObject class "+object.eClass().getName()+" not supported."); + } + + /** + * Return true if the specified reactor belongs to this federate. + * @param instantiation The instantiation to look inside + * @param reactor The reactor declaration to find + */ + private boolean contains( + Instantiation instantiation, + ReactorDecl reactor + ) { + if (instantiation.getReactorClass().equals(ASTUtils.toDefinition(reactor))) { + return true; + } + + boolean instantiationsCheck = false; + // For a federate, we don't need to look inside imported reactors. + if (instantiation.getReactorClass() instanceof Reactor reactorDef) { + for (Instantiation child : reactorDef.getInstantiations()) { + instantiationsCheck |= contains(child, reactor); + } + } + + return instantiationsCheck; + } + + /** + * Return true if the specified import should be included in the code generated for this federate. + * @param imp The import + */ + private boolean contains(Import imp) { + for (ImportedReactor reactor : imp.getReactorClasses()) { + if (contains(reactor)) { + return true; + } + } + return false; + } + + /** + * Return true if the specified parameter should be included in the code generated for this federate. + * @param param The parameter + */ + private boolean contains(Parameter param) { + boolean returnValue = false; + // Check if param is referenced in this federate's instantiation + returnValue = instantiation.getParameters().stream().anyMatch( + assignment -> assignment.getRhs() + .getExprs() + .stream() + .filter( + it -> it instanceof ParameterReference + ) + .map(it -> ((ParameterReference) it).getParameter()) + .toList() + .contains(param) + ); + // If there are any user-defined top-level reactions, they could access + // the parameters, so we need to include the parameter. + var topLevelUserDefinedReactions = ((Reactor) instantiation.eContainer()) + .getReactions().stream().filter( + r -> !networkReactions.contains(r) && contains(r) + ).collect(Collectors.toCollection(ArrayList::new)); + returnValue |= !topLevelUserDefinedReactions.isEmpty(); + return returnValue; + } /** * Return true if the specified action should be included in the code generated - * for the federate. This means that either the action is used as a trigger, + * for this federate. This means that either the action is used as a trigger, * a source, or an effect in a top-level reaction that belongs to this federate. * This returns true if the program is not federated. - * + * * @param action The action - * @return True if this federate contains the action in the specified reactor + * @return True if this federate contains the action. */ - public boolean contains(Action action) { + private boolean contains(Action action) { Reactor reactor = ASTUtils.getEnclosingReactor(action); - if (!reactor.isFederated() || isSingleton()) return true; - + // If the action is used as a trigger, a source, or an effect for a top-level reaction // that belongs to this federate, then generate it. for (Reaction react : ASTUtils.allReactions(reactor)) { if (contains(react)) { // Look in triggers for (TriggerRef trigger : convertToEmptyListIfNull(react.getTriggers())) { - if (trigger instanceof VarRef) { - VarRef triggerAsVarRef = (VarRef) trigger; - if (Objects.equal(triggerAsVarRef.getVariable(), (Variable) action)) { + if (trigger instanceof VarRef triggerAsVarRef) { + if (Objects.equal(triggerAsVarRef.getVariable(), action)) { return true; } } } // Look in sources for (VarRef source : convertToEmptyListIfNull(react.getSources())) { - if (Objects.equal(source.getVariable(), (Variable) action)) { + if (Objects.equal(source.getVariable(), action)) { return true; } } // Look in effects for (VarRef effect : convertToEmptyListIfNull(react.getEffects())) { - if (Objects.equal(effect.getVariable(), (Variable) action)) { + if (Objects.equal(effect.getVariable(), action)) { return true; } } @@ -301,18 +409,18 @@ public boolean contains(Action action) { * * @param reaction The reaction. */ - public boolean contains(Reaction reaction) { + private boolean contains(Reaction reaction) { Reactor reactor = ASTUtils.getEnclosingReactor(reaction); - if (!reactor.isFederated() || this.isSingleton()) return true; - + + assert reactor != null; if (!reactor.getReactions().contains(reaction)) return false; if (networkReactions.contains(reaction)) { // Reaction is a network reaction that belongs to this federate return true; } - - int reactionBankIndex = generator.getReactionBankIndex(reaction); + + int reactionBankIndex = FedASTUtils.getReactionBankIndex(reaction); if (reactionBankIndex >= 0 && this.bankIndex >= 0 && reactionBankIndex != this.bankIndex) { return false; } @@ -327,7 +435,36 @@ public boolean contains(Reaction reaction) { return !excludeReactions.contains(reaction); } - + + /** + * Return true if the specified timer should be included in the code generated + * for the federate. This means that the timer is used as a trigger + * in a top-level reaction that belongs to this federate. + * This also returns true if the program is not federated. + * + * @return True if this federate contains the action in the specified reactor + */ + private boolean contains(Timer timer) { + Reactor reactor = ASTUtils.getEnclosingReactor(timer); + + // If the action is used as a trigger, a source, or an effect for a top-level reaction + // that belongs to this federate, then generate it. + for (Reaction r : ASTUtils.allReactions(reactor)) { + if (contains(r)) { + // Look in triggers + for (TriggerRef trigger : convertToEmptyListIfNull(r.getTriggers())) { + if (trigger instanceof VarRef triggerAsVarRef) { + if (Objects.equal(triggerAsVarRef.getVariable(), timer)) { + return true; + } + } + } + } + } + + return false; + } + /** * Return true if the specified reactor instance or any parent * reactor instance is contained by this federate. @@ -343,9 +480,6 @@ public boolean contains(Reaction reaction) { * @return True if this federate contains the reactor instance */ public boolean contains(ReactorInstance instance) { - if (isSingleton()) { - return instance != null; - } if (instance.getParent() == null) { return true; // Top-level reactor } @@ -359,52 +493,6 @@ public boolean contains(ReactorInstance instance) { } return false; } - - /** - * Return true if the specified timer should be included in the code generated - * for the federate. This means that the timer is used as a trigger - * in a top-level reaction that belongs to this federate. - * This also returns true if the program is not federated. - * - * @param timer The timer - * @return True if this federate contains the action in the specified reactor - */ - public boolean contains(Timer timer) { - Reactor reactor = ASTUtils.getEnclosingReactor(timer); - if (!reactor.isFederated() || this.isSingleton()) return true; - - // If the action is used as a trigger, a source, or an effect for a top-level reaction - // that belongs to this federate, then generate it. - for (Reaction r : ASTUtils.allReactions(reactor)) { - if (contains(r)) { - // Look in triggers - for (TriggerRef trigger : convertToEmptyListIfNull(r.getTriggers())) { - if (trigger instanceof VarRef) { - VarRef triggerAsVarRef = (VarRef) trigger; - if (Objects.equal(triggerAsVarRef.getVariable(), (Variable) timer)) { - return true; - } - } - } - } - } - - return false; - } - - /** - * Return the total number of runtime instances of the specified reactor - * instance in this federate. This is zero if the reactor is not in the - * federate at all, and otherwise is the product of the bank widths of - * all the parent containers of the instance, except that if the depth - * one parent is bank, its width is ignored (only one bank member can be - * in any federate). - */ - public int numRuntimeInstances(ReactorInstance reactor) { - if (!contains(reactor)) return 0; - int depth = this.isSingleton() ? 0 : 1; - return reactor.getTotalWidth(depth); - } /** * Build an index of reactions at the top-level (in the @@ -420,31 +508,31 @@ private void indexExcludedTopLevelReactions(Reactor federatedReactor) { throw new IllegalStateException("The index for excluded reactions at the top level is already built."); } - excludeReactions = new LinkedHashSet(); + excludeReactions = new LinkedHashSet<>(); // Construct the set of excluded reactions for this federate. // If a reaction is a network reaction that belongs to this federate, we // don't need to perform this analysis. - Iterable reactions = IterableExtensions.filter(ASTUtils.allReactions(federatedReactor), it -> { return !networkReactions.contains(it); }); + Iterable reactions = ASTUtils.allReactions(federatedReactor).stream().filter(it -> !networkReactions.contains(it)).collect(Collectors.toList()); for (Reaction react : reactions) { - // Create a collection of all the VarRefs (i.e., triggers, sources, and effects) in the react + // Create a collection of all the VarRefs (i.e., triggers, sources, and effects) in the react // signature that are ports that reference federates. // We then later check that all these VarRefs reference this federate. If not, we will add this // react to the list of reactions that have to be excluded (note that mixing VarRefs from // different federates is not allowed). - List allVarRefsReferencingFederates = new ArrayList(); + List allVarRefsReferencingFederates = new ArrayList<>(); // Add all the triggers that are outputs Stream triggersAsVarRef = react.getTriggers().stream().filter(it -> it instanceof VarRef).map(it -> (VarRef) it); allVarRefsReferencingFederates.addAll( - triggersAsVarRef.filter(it -> it.getVariable() instanceof Output).collect(Collectors.toList()) + triggersAsVarRef.filter(it -> it.getVariable() instanceof Output).toList() ); // Add all the sources that are outputs allVarRefsReferencingFederates.addAll( - react.getSources().stream().filter(it -> it.getVariable() instanceof Output).collect(Collectors.toList()) + react.getSources().stream().filter(it -> it.getVariable() instanceof Output).toList() ); // Add all the effects that are inputs allVarRefsReferencingFederates.addAll( - react.getEffects().stream().filter(it -> it.getVariable() instanceof Input).collect(Collectors.toList()) + react.getEffects().stream().filter(it -> it.getVariable() instanceof Input).toList() ); inFederate = containsAllVarRefs(allVarRefsReferencingFederates); if (!inFederate) { @@ -455,10 +543,10 @@ private void indexExcludedTopLevelReactions(Reactor federatedReactor) { /** * Return true if all members of 'varRefs' belong to this federate. - * - * As a convenience measure, if some members of 'varRefs' are from + * + * As a convenience measure, if some members of 'varRefs' are from * different federates, also report an error. - * + * * @param varRefs A collection of VarRefs */ private boolean containsAllVarRefs(Iterable varRefs) { @@ -469,23 +557,16 @@ private boolean containsAllVarRefs(Iterable varRefs) { referencesFederate = true; } else { if (referencesFederate) { - errorReporter.reportError(varRef, "Mixed triggers and effects from" + - " different federates. This is not permitted"); + errorReporter.reportError(varRef, + "Mixed triggers and effects from" + + + " different federates. This is not permitted"); } inFederate = false; } } return inFederate; } - - /** - * Return true if this is singleton, meaning either that no federation - * has been defined or that there is only one federate. - * @return True if no federation has been defined or there is only one federate. - */ - public boolean isSingleton() { - return ((instantiation == null) || (generator.federates.size() <= 1)); - } /** * Find output ports that are connected to a physical action trigger upstream @@ -507,6 +588,17 @@ public LinkedHashMap findOutputsConnectedToPhysicalActions(Re } return physicalActionToOutputMinDelay; } + + /** + * Return a list of federates that are upstream of this federate and have a + * zero-delay (direct) connection to this federate. + */ + public List getZeroDelayImmediateUpstreamFederates() { + return this.dependsOn.entrySet() + .stream() + .filter(e -> e.getValue().contains(null)) + .map(Map.Entry::getKey).toList(); + } @Override public String toString() { @@ -522,18 +614,6 @@ public String toString() { */ private Set excludeReactions = null; - /** - * The generator using this. - */ - private GeneratorBase generator = null; - - /** - * Returns the generator that is using this federate instance - */ - public GeneratorBase getGenerator() { - return this.generator; - } - /** * An error reporter */ @@ -550,8 +630,7 @@ public GeneratorBase getGenerator() { public TimeValue findNearestPhysicalActionTrigger(ReactionInstance reaction) { TimeValue minDelay = TimeValue.MAX_VALUE; for (TriggerInstance trigger : reaction.triggers) { - if (trigger.getDefinition() instanceof Action) { - Action action = (Action) trigger.getDefinition(); + if (trigger.getDefinition() instanceof Action action) { ActionInstance actionInstance = (ActionInstance) trigger; if (action.getOrigin() == ActionOrigin.PHYSICAL) { if (actionInstance.getMinDelay().isEarlierThan(minDelay)) { @@ -585,17 +664,8 @@ public TimeValue findNearestPhysicalActionTrigger(ReactionInstance reaction) { return minDelay; } - /** - * Remove triggers in this federate's network reactions that are defined in remote federates. - */ - public void removeRemoteFederateConnectionPorts() { - for (Reaction reaction : this.networkReactions) { - reaction.getTriggers().removeAll(this.remoteNetworkReactionTriggers); - } - } - // TODO: Put this function into a utils file instead - private List convertToEmptyListIfNull(List list) { + private List convertToEmptyListIfNull(List list) { return list == null ? new ArrayList<>() : list; } } diff --git a/org.lflang/src/org/lflang/federated/generator/LineAdjustingErrorReporter.java b/org.lflang/src/org/lflang/federated/generator/LineAdjustingErrorReporter.java new file mode 100644 index 0000000000..f483c228db --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/LineAdjustingErrorReporter.java @@ -0,0 +1,128 @@ +package org.lflang.federated.generator; + +import java.nio.file.Path; +import java.util.Map; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.lsp4j.DiagnosticSeverity; + +import org.lflang.ErrorReporter; +import org.lflang.generator.CodeMap; +import org.lflang.generator.Position; +import org.lflang.generator.Range; + +public class LineAdjustingErrorReporter implements ErrorReporter { + + private final ErrorReporter parent; + private final Map codeMapMap; + + public LineAdjustingErrorReporter(ErrorReporter parent, Map codeMapMap) { + this.parent = parent; + this.codeMapMap = codeMapMap; + } + + @Override + public String reportError(String message) { + return parent.reportError(message); + } + + @Override + public String reportWarning(String message) { + return parent.reportWarning(message); + } + + @Override + public String reportInfo(String message) { + return parent.reportInfo(message); + } + + @Override + public String reportError(EObject object, String message) { + return parent.reportError(object, message); + } + + @Override + public String reportWarning(EObject object, String message) { + return parent.reportWarning(object, message); + } + + @Override + public String reportInfo(EObject object, String message) { + return parent.reportInfo(object, message); + } + + @Override + public String reportError(Path file, Integer line, String message) { + return report(file, line, message, DiagnosticSeverity.Error); + } + + @Override + public String reportWarning(Path file, Integer line, String message) { + return report(file, line, message, DiagnosticSeverity.Warning); + } + + @Override + public String reportInfo(Path file, Integer line, String message) { + return report(file, line, message, DiagnosticSeverity.Information); + } + + private String report(Path file, Integer line, String message, DiagnosticSeverity severity) { + if (line == null) return report(file, severity, message); + var position = Position.fromOneBased(line, Integer.MAX_VALUE); + return report( + file, + severity, + message, + Position.fromZeroBased(position.getZeroBasedLine(), 0), + position + ); + } + + @Override + public String report(Path file, DiagnosticSeverity severity, String message) { + return ErrorReporter.super.report(file, severity, message); + } + + @Override + public String report(Path file, DiagnosticSeverity severity, String message, int line) { + return ErrorReporter.super.report(file, severity, message, line); + } + + @Override + public String report( + Path file, + DiagnosticSeverity severity, + String message, + Position startPos, + Position endPos + ) { + String ret = null; + if (codeMapMap.containsKey(file)) { + var relevantMap = codeMapMap.get(file); + for (Path lfSource : relevantMap.lfSourcePaths()) { + var adjustedRange = relevantMap.adjusted( + lfSource, + new Range(startPos, endPos) + ); + ret = parent.report( + lfSource, + severity, + message, + adjustedRange.getStartInclusive().equals(Position.ORIGIN) ? + Position.fromZeroBased( + adjustedRange.getEndExclusive().getZeroBasedLine(), + 0 + ) : adjustedRange.getStartInclusive(), + adjustedRange.getEndExclusive() + ); + } + } + if (ret == null) return severity == DiagnosticSeverity.Error ? reportError(message) : reportWarning(message); + return ret; + } + + @Override + public boolean getErrorsOccurred() { + return parent.getErrorsOccurred(); + } +} diff --git a/org.lflang/src/org/lflang/federated/generator/SynchronizedErrorReporter.java b/org.lflang/src/org/lflang/federated/generator/SynchronizedErrorReporter.java new file mode 100644 index 0000000000..571d2de5c7 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/generator/SynchronizedErrorReporter.java @@ -0,0 +1,83 @@ +package org.lflang.federated.generator; + +import java.nio.file.Path; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.lsp4j.DiagnosticSeverity; + +import org.lflang.ErrorReporter; +import org.lflang.generator.Position; + +public class SynchronizedErrorReporter implements ErrorReporter { + + private final ErrorReporter parent; + + public SynchronizedErrorReporter(ErrorReporter parent) { + this.parent = parent; + } + + @Override + public synchronized String reportError(String message) { + return parent.reportError(message); + } + + @Override + public synchronized String reportWarning(String message) { + return parent.reportWarning(message); + } + + @Override + public synchronized String reportInfo(String message) { + return parent.reportInfo(message); + } + + @Override + public synchronized String reportError(EObject object, String message) { + return parent.reportError(object, message); + } + + @Override + public synchronized String reportWarning(EObject object, String message) { + return parent.reportWarning(object, message); + } + + @Override + public synchronized String reportInfo(EObject object, String message) { + return parent.reportInfo(object, message); + } + + @Override + public synchronized String reportError(Path file, Integer line, String message) { + return parent.reportError(file, line, message); + } + + @Override + public synchronized String reportWarning(Path file, Integer line, String message) { + return parent.reportWarning(file, line, message); + } + + @Override + public synchronized String reportInfo(Path file, Integer line, String message) { + return parent.reportInfo(file, line, message); + } + + @Override + public synchronized String report(Path file, DiagnosticSeverity severity, String message) { + return parent.report(file, severity, message); + } + + @Override + public synchronized String report(Path file, DiagnosticSeverity severity, String message, int line) { + return parent.report(file, severity, message, line); + } + + @Override + public synchronized String report(Path file, DiagnosticSeverity severity, String message, Position startPos, Position endPos) { + return parent.report(file, severity, message, startPos, endPos); + } + + @Override + public synchronized boolean getErrorsOccurred() { + return parent.getErrorsOccurred(); + } +} diff --git a/org.lflang/src/org/lflang/federated/launcher/FedCLauncher.java b/org.lflang/src/org/lflang/federated/launcher/FedCLauncher.java index e9ec32dab3..0b45444be9 100644 --- a/org.lflang/src/org/lflang/federated/launcher/FedCLauncher.java +++ b/org.lflang/src/org/lflang/federated/launcher/FedCLauncher.java @@ -29,10 +29,9 @@ import java.io.IOException; import org.lflang.ErrorReporter; -import org.lflang.FileConfig; import org.lflang.TargetConfig; -import org.lflang.federated.FedFileConfig; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; import org.lflang.generator.c.CCompiler; /** @@ -51,9 +50,9 @@ public class FedCLauncher extends FedLauncher { * @param errorReporter A error reporter for reporting any errors or warnings during the code generation */ public FedCLauncher( - TargetConfig targetConfig, - FileConfig fileConfig, - ErrorReporter errorReporter + TargetConfig targetConfig, + FedFileConfig fileConfig, + ErrorReporter errorReporter ) { super(targetConfig, fileConfig, errorReporter); } @@ -67,14 +66,7 @@ public FedCLauncher( @Override protected String compileCommandForFederate(FederateInstance federate) { - FedFileConfig fedFileConfig = null; TargetConfig localTargetConfig = targetConfig; - try { - fedFileConfig = new FedFileConfig(fileConfig, federate.name); - } catch (IOException e) { - errorReporter.reportError("Failed to create file config for federate "+federate.name); - return ""; - } String commandToReturn = ""; // FIXME: Hack to add platform support only for linux systems. @@ -83,7 +75,7 @@ String compileCommandForFederate(FederateInstance federate) { if (!localTargetConfig.compileAdditionalSources.contains(linuxPlatformSupport)) { localTargetConfig.compileAdditionalSources.add(linuxPlatformSupport); } - CCompiler cCompiler= new CCompiler(localTargetConfig, fedFileConfig, errorReporter, false); + CCompiler cCompiler= new CCompiler(localTargetConfig, fileConfig, errorReporter, false); commandToReturn = String.join(" ", cCompiler.compileCCommand( fileConfig.name+"_"+federate.name, @@ -106,15 +98,14 @@ String executeCommandForRemoteFederate(FederateInstance federate) { } /** - * Return the command that will execute a local federate, assuming that the current - * directory is the top-level project folder. This is used to create a launcher script - * for federates. + * Return the command that will execute a local federate. + * This is used to create a launcher script for federates. * * @param federate The federate to execute. */ @Override protected - String executeCommandForLocalFederate(FileConfig fileConfig, FederateInstance federate) { - return fileConfig.binPath.resolve(fileConfig.name)+"_"+federate.name+" -i $FEDERATION_ID"; + String executeCommandForLocalFederate(FedFileConfig fileConfig, FederateInstance federate) { + return fileConfig.getGenPath().resolve("bin/"+federate.name)+" -i $FEDERATION_ID"; } } diff --git a/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java b/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java index 288056f76c..48a7149729 100644 --- a/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java +++ b/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java @@ -28,17 +28,17 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import org.lflang.ErrorReporter; -import org.lflang.FileConfig; import org.lflang.TargetConfig; import org.lflang.TargetProperty.ClockSyncMode; -import org.lflang.federated.FedFileConfig; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; /** * Utility class that can be used to create a launcher for federated LF programs. @@ -46,10 +46,10 @@ * @author Edward A. Lee * @author Soroush Bateni */ -class FedLauncher { +public class FedLauncher { protected TargetConfig targetConfig; - protected FileConfig fileConfig; + protected FedFileConfig fileConfig; protected ErrorReporter errorReporter; /** @@ -57,7 +57,7 @@ class FedLauncher { * @param fileConfig The current file configuration. * @param errorReporter A error reporter for reporting any errors or warnings during the code generation */ - public FedLauncher(TargetConfig targetConfig, FileConfig fileConfig, ErrorReporter errorReporter) { + public FedLauncher(TargetConfig targetConfig, FedFileConfig fileConfig, ErrorReporter errorReporter) { this.targetConfig = targetConfig; this.fileConfig = fileConfig; this.errorReporter = errorReporter; @@ -68,7 +68,7 @@ public FedLauncher(TargetConfig targetConfig, FileConfig fileConfig, ErrorReport * * @param federate The federate to compile. */ - protected String compileCommandForFederate(org.lflang.federated.FederateInstance federate) { + protected String compileCommandForFederate(FederateInstance federate) { throw new UnsupportedOperationException("Don't know how to compile the federates."); } @@ -79,7 +79,7 @@ protected String compileCommandForFederate(org.lflang.federated.FederateInstance * * @param federate The federate to execute. */ - protected String executeCommandForRemoteFederate(org.lflang.federated.FederateInstance federate) { + protected String executeCommandForRemoteFederate(FederateInstance federate) { throw new UnsupportedOperationException("Don't know how to execute the federates."); } @@ -90,8 +90,8 @@ protected String executeCommandForRemoteFederate(org.lflang.federated.FederateIn * * @param federate The federate to execute. */ - protected String executeCommandForLocalFederate(FileConfig fileConfig, - org.lflang.federated.FederateInstance federate) { + protected String executeCommandForLocalFederate(FedFileConfig fileConfig, + FederateInstance federate) { throw new UnsupportedOperationException("Don't know how to execute the federates."); } @@ -157,8 +157,7 @@ public void createLauncher( Object host = federationRTIProperties.get("host"); Object target = host; - Object path = federationRTIProperties.get("dir"); - if (path == null) path = "LinguaFrancaRemote"; + Path path = Path.of(federationRTIProperties.get("dir") == null ? "LinguaFrancaRemote" : federationRTIProperties.get("dir").toString()); Object user = federationRTIProperties.get("user"); if (user != null) { @@ -201,13 +200,12 @@ public void createLauncher( int federateIndex = 0; for (FederateInstance federate : federates) { if (federate.isRemote) { - FedFileConfig fedFileConfig = new FedFileConfig(fileConfig, federate.name); - Path fedRelSrcGenPath = fedFileConfig.getSrcGenBasePath().relativize(fedFileConfig.getSrcGenPath()); + Path fedRelSrcGenPath = fileConfig.getOutPath().relativize(fileConfig.getSrcGenPath()).resolve(federate.name); if(distCode.length() == 0) distCode.append(distHeader + "\n"); - String logFileName = String.format("log/%s_%s.log", fedFileConfig.name, federate.name); + String logFileName = String.format("log/%s_%s.log", fileConfig.name, federate.name); String compileCommand = compileCommandForFederate(federate); // FIXME: Should $FEDERATION_ID be used to ensure unique directories, executables, on the remote host? - distCode.append(getDistCode(path, federate, fedRelSrcGenPath, logFileName, fedFileConfig, compileCommand) + "\n"); + distCode.append(getDistCode(path, federate, fedRelSrcGenPath, logFileName, fileConfig.getSrcGenPath(), compileCommand) + "\n"); String executeCommand = executeCommandForRemoteFederate(federate); shCode.append(getFedRemoteLaunchCode(federate, path, logFileName, executeCommand, federateIndex++) + "\n"); } else { @@ -232,6 +230,15 @@ public void createLauncher( "echo \"All done.\"" ) + "\n"); + // Create bin directory for the script. + if (!Files.exists(fileConfig.binPath)) { + Files.createDirectories(fileConfig.binPath); + } + + System.out.println("##### Generating launcher for federation " + + " in directory " + + fileConfig.binPath); + // Write the launcher file. // Delete file previously produced, if any. File file = fileConfig.binPath.resolve(fileConfig.name).toFile(); @@ -391,29 +398,47 @@ private String getRemoteLaunchCode(Object host, Object target, String logFileNam } private String getDistCode( - Object path, + Path remoteBase, FederateInstance federate, - Path fedRelSrcGenPath, + Path remoteRelSrcGenPath, String logFileName, - FedFileConfig fedFileConfig, + Path localAbsSrcGenPath, String compileCommand) { - return String.join("\n", - "echo \"Making directory "+path+" and subdirectories src-gen, bin, and log on host "+getUserHost(federate.user, federate.host)+"\"", - "# The >> syntax appends stdout to a file. The 2>&1 appends stderr to the same file.", - "ssh "+getUserHost(federate.user, federate.host)+" '\\", - " mkdir -p "+path+"/src-gen/"+fedRelSrcGenPath+"/core "+path+"/bin "+path+"/log; \\", - " echo \"--------------\" >> "+path+"/"+logFileName+"; \\", - " date >> "+path+"/"+logFileName+";", - "'", - "pushd "+fedFileConfig.getSrcGenPath()+" > /dev/null", - "echo \"Copying source files to host "+getUserHost(federate.user, federate.host)+"\"", - "scp -r * "+getUserHost(federate.user, federate.host)+":"+path+"/src-gen/"+fedRelSrcGenPath, - "popd > /dev/null", - "echo \"Compiling on host "+getUserHost(federate.user, federate.host)+" using: "+compileCommand+"\"", - "ssh "+getUserHost(federate.user, federate.host)+" 'cd "+path+"; \\", - " echo \"In "+path+" compiling with: "+compileCommand+"\" >> "+logFileName+" 2>&1; \\", - " # Capture the output in the log file and stdout. \\", - " "+compileCommand+" 2>&1 | tee -a "+logFileName+";' " + return String.join("\n", + "echo \"Making directory " + remoteBase + + " and subdirectories src-gen, bin, and log on host " + + getUserHost(federate.user, federate.host) + + "\"", + "# The >> syntax appends stdout to a file. The 2>&1 appends stderr to the same file.", + "ssh " + getUserHost(federate.user, federate.host) + + " '\\", + " mkdir -p " + remoteBase.resolve(remoteRelSrcGenPath).resolve("core") + + " " + remoteBase.resolve("bin") + " " + + remoteBase + "/log; \\", + " echo \"--------------\" >> " + remoteBase + "/" + + logFileName + "; \\", + " date >> " + remoteBase + "/" + logFileName + ";", + "'", + "pushd " + localAbsSrcGenPath + + " > /dev/null", + "echo \"Copying source files to host " + + getUserHost(federate.user, federate.host) + + "\"", + "scp -r * " + + getUserHost(federate.user, federate.host) + ":" + + remoteBase.resolve(remoteRelSrcGenPath), + "popd > /dev/null", + "echo \"Compiling on host " + + getUserHost(federate.user, federate.host) + + " using: " + compileCommand + "\"", + "ssh " + getUserHost(federate.user, federate.host) + + " 'cd " + remoteBase + "; \\", + " echo \"In " + remoteBase + " compiling with: " + + compileCommand + "\" >> " + logFileName + + " 2>&1; \\", + " # Capture the output in the log file and stdout. \\", + " " + compileCommand + " 2>&1 | tee -a " + + logFileName + ";' " ); } diff --git a/org.lflang/src/org/lflang/federated/launcher/FedLauncherFactory.java b/org.lflang/src/org/lflang/federated/launcher/FedLauncherFactory.java new file mode 100644 index 0000000000..c92cea1807 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/launcher/FedLauncherFactory.java @@ -0,0 +1,64 @@ +package org.lflang.federated.launcher; + +import org.lflang.ErrorReporter; +import org.lflang.Target; +import org.lflang.TargetConfig; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; + +/** + * Helper class to get the appropriate launcher generator. + * + * FIXME: This architecture needs to be redesigned for multi-target federations. + */ +public class FedLauncherFactory { + + public static FedLauncher getLauncher ( + FederateInstance federate, + FedFileConfig fileConfig, + ErrorReporter errorReporter + ) { + return getLauncher(Target.fromDecl(federate.target), federate.targetConfig, fileConfig, errorReporter); + } + + /** + * Return a launcher generator. + * @param target The target to generate for. + * @param targetConfig The target config of the federate. + * @param fileConfig The file config for the federate. + * @param errorReporter The error reporter to use. + * @return null if not supported, an instance of {@code #FedLauncher} otherwise. + */ + public static FedLauncher getLauncher( + Target target, + TargetConfig targetConfig, + FedFileConfig fileConfig, + ErrorReporter errorReporter + ) { + switch (target) { + case C: + case CCPP: + return new FedCLauncher( + targetConfig, + fileConfig, + errorReporter + ); + case CPP: + case Rust: + return null; + case TS: + return new FedTSLauncher( + targetConfig, + fileConfig, + errorReporter + ); + case Python: + return new FedPyLauncher( + targetConfig, + fileConfig, + errorReporter + ); + } + return null; + } +} diff --git a/org.lflang/src/org/lflang/federated/launcher/FedPyLauncher.java b/org.lflang/src/org/lflang/federated/launcher/FedPyLauncher.java index 552000d2f1..414caf2ee4 100644 --- a/org.lflang/src/org/lflang/federated/launcher/FedPyLauncher.java +++ b/org.lflang/src/org/lflang/federated/launcher/FedPyLauncher.java @@ -28,9 +28,9 @@ package org.lflang.federated.launcher; import org.lflang.ErrorReporter; -import org.lflang.FileConfig; import org.lflang.TargetConfig; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; /** * Utility class that can be used to create a launcher for federated LF programs @@ -48,9 +48,9 @@ public class FedPyLauncher extends FedLauncher { * @param errorReporter A error reporter for reporting any errors or warnings during the code generation */ public FedPyLauncher( - TargetConfig targetConfig, - FileConfig fileConfig, - ErrorReporter errorReporter + TargetConfig targetConfig, + FedFileConfig fileConfig, + ErrorReporter errorReporter ) { super(targetConfig, fileConfig, errorReporter); } @@ -77,7 +77,7 @@ String executeCommandForRemoteFederate(FederateInstance federate) { */ @Override protected - String executeCommandForLocalFederate(FileConfig fileConfig, FederateInstance federate) { - return "python3 " + fileConfig.getSrcGenPath() + "/" + federate.name + "/" + fileConfig.name+"_"+federate.name+".py -i $FEDERATION_ID"; + String executeCommandForLocalFederate(FedFileConfig fileConfig, FederateInstance federate) { + return "python3 " + fileConfig.getSrcGenPath() + "/" + federate.name + "/" + federate.name+".py -i $FEDERATION_ID"; } } diff --git a/org.lflang/src/org/lflang/federated/launcher/FedTSLauncher.java b/org.lflang/src/org/lflang/federated/launcher/FedTSLauncher.java index c7544f73ee..67dbd134fa 100644 --- a/org.lflang/src/org/lflang/federated/launcher/FedTSLauncher.java +++ b/org.lflang/src/org/lflang/federated/launcher/FedTSLauncher.java @@ -26,9 +26,9 @@ package org.lflang.federated.launcher; import org.lflang.ErrorReporter; -import org.lflang.FileConfig; import org.lflang.TargetConfig; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FederateInstance; /** * Utility class that can be used to create a launcher for federated LF programs @@ -47,9 +47,9 @@ public class FedTSLauncher extends FedLauncher { * @param errorReporter A error reporter for reporting any errors or warnings during the code generation */ public FedTSLauncher( - TargetConfig targetConfig, - FileConfig fileConfig, - ErrorReporter errorReporter + TargetConfig targetConfig, + FedFileConfig fileConfig, + ErrorReporter errorReporter ) { super(targetConfig, fileConfig, errorReporter); } @@ -63,9 +63,8 @@ public FedTSLauncher( */ @Override protected - String executeCommandForLocalFederate(FileConfig fileConfig, FederateInstance federate) { - String jsFilename = fileConfig.name + "_" + federate.name + ".js"; - return "node "+fileConfig.getSrcGenPath().resolve("dist").resolve(jsFilename)+" -i $FEDERATION_ID"; - //return fileConfig.binPath.resolve(fileConfig.name)+"_"+federate.name+" -i $FEDERATION_ID"; + String executeCommandForLocalFederate(FedFileConfig fileConfig, FederateInstance federate) { + String jsFilename = federate.name + ".js"; + return "node "+fileConfig.getSrcGenPath().resolve(federate.name).resolve("dist").resolve(jsFilename)+" -i $FEDERATION_ID"; } } diff --git a/org.lflang/src/org/lflang/federated/serialization/FedROS2CPPSerialization.java b/org.lflang/src/org/lflang/federated/serialization/FedROS2CPPSerialization.java index ba11972e4e..5e6e6557a6 100644 --- a/org.lflang/src/org/lflang/federated/serialization/FedROS2CPPSerialization.java +++ b/org.lflang/src/org/lflang/federated/serialization/FedROS2CPPSerialization.java @@ -97,8 +97,8 @@ public StringBuilder generateNetworkSerializerCode(String varName, String origin // Use the port type verbatim here, which can result // in compile error if it is not a valid ROS type serializerCode.append("using MessageT = "+originalType+";\n"); - serializerCode.append("static rclcpp::Serialization serializer;\n"); - serializerCode.append("serializer.serialize_message(&"+varName+"->value , &"+serializedVarName+");\n"); + serializerCode.append("static rclcpp::Serialization _lf_serializer;\n"); + serializerCode.append("_lf_serializer.serialize_message(&"+varName+"->value , &"+serializedVarName+");\n"); return serializerCode; } @@ -116,11 +116,11 @@ public StringBuilder generateNetworkSerializerCode(String varName, String origin // Use the port type verbatim here, which can result // in compile error if it is not a valid ROS type serializerCode.append("using MessageT = "+originalType+";\n"); - serializerCode.append("static rclcpp::Serialization serializer;\n"); + serializerCode.append("static rclcpp::Serialization _lf_serializer;\n"); if (isSharedPtrType) { - serializerCode.append("serializer.serialize_message("+varName+"->value.get() , &"+serializedVarName+");\n"); + serializerCode.append("_lf_serializer.serialize_message("+varName+"->value.get() , &"+serializedVarName+");\n"); } else { - serializerCode.append("serializer.serialize_message(&"+varName+"->value , &"+serializedVarName+");\n"); + serializerCode.append("_lf_serializer.serialize_message(&"+varName+"->value , &"+serializedVarName+");\n"); } return serializerCode; @@ -158,8 +158,8 @@ public StringBuilder generateNetworkDeserializerCode(String varName, String targ deserializerCode.append("using MessageT = "+targetType+";\n"); deserializerCode.append( "MessageT "+deserializedVarName+" = MessageT();\n" - + "auto serializer = rclcpp::Serialization();\n" - + "serializer.deserialize_message(msg.get(), &"+deserializedVarName+");\n" + + "auto _lf_serializer = rclcpp::Serialization();\n" + + "_lf_serializer.deserialize_message(msg.get(), &"+deserializedVarName+");\n" ); return deserializerCode; diff --git a/org.lflang/src/org/lflang/federated/validation/FedValidator.java b/org.lflang/src/org/lflang/federated/validation/FedValidator.java new file mode 100644 index 0000000000..ce03b18964 --- /dev/null +++ b/org.lflang/src/org/lflang/federated/validation/FedValidator.java @@ -0,0 +1,69 @@ +package org.lflang.federated.validation; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.lf.Input; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Output; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; +import org.lflang.lf.VarRef; + +/** + * Helper class that is used to validate a federated reactor. + */ +public class FedValidator { + + public static void validateFederatedReactor(Reactor reactor, ErrorReporter errorReporter) { + if (!reactor.isFederated()) return; + + // Construct the set of excluded reactions for this federate. + // If a reaction is a network reaction that belongs to this federate, we + // don't need to perform this analysis. + Iterable reactions = ASTUtils.allReactions(reactor); + for (Reaction react : reactions) { + // Create a collection of all the VarRefs (i.e., triggers, sources, and effects) in the react + // signature that are ports that reference federates. + // We then later check that all these VarRefs reference this federate. If not, we will add this + // react to the list of reactions that have to be excluded (note that mixing VarRefs from + // different federates is not allowed). + List allVarRefsReferencingFederates = new ArrayList<>(); + // Add all the triggers that are outputs + Stream triggersAsVarRef = react.getTriggers().stream().filter(it -> it instanceof VarRef).map(it -> (VarRef) it); + allVarRefsReferencingFederates.addAll( + triggersAsVarRef.filter(it -> it.getVariable() instanceof Output).toList() + ); + // Add all the sources that are outputs + allVarRefsReferencingFederates.addAll( + react.getSources().stream().filter(it -> it.getVariable() instanceof Output).toList() + ); + // Add all the effects that are inputs + allVarRefsReferencingFederates.addAll( + react.getEffects().stream().filter(it -> it.getVariable() instanceof Input).toList() + ); + containsAllVarRefs(allVarRefsReferencingFederates, errorReporter); + } + } + + /** + * Check if this federate contains all the {@code varRefs}. If not, report an error using {@code errorReporter}. + */ + private static void containsAllVarRefs(List varRefs, ErrorReporter errorReporter) { + var referencesFederate = false; + Instantiation instantiation = null; + for (VarRef varRef : varRefs) { + if (instantiation == null) { + instantiation = varRef.getContainer(); + referencesFederate = true; + } else if (!varRef.getContainer().equals(instantiation)) { + errorReporter.reportError(varRef, "Mixed triggers and effects from" + + " different federates. This is not permitted"); + } + } + + } +} diff --git a/org.lflang/src/org/lflang/generator/CodeBuilder.java b/org.lflang/src/org/lflang/generator/CodeBuilder.java index e67274cb64..a921a47239 100644 --- a/org.lflang/src/org/lflang/generator/CodeBuilder.java +++ b/org.lflang/src/org/lflang/generator/CodeBuilder.java @@ -6,7 +6,7 @@ import org.eclipse.emf.common.CommonPlugin; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FederateInstance; import org.lflang.generator.c.CUtil; import org.lflang.lf.Code; import org.lflang.util.FileUtil; @@ -192,26 +192,15 @@ public void startScopedBlock() { * This must be followed by an {@link #endScopedBlock()}. * * @param reactor The reactor instance. - * @param restrict For federated execution only, if this is true, then - * skip iterations where the topmost bank member is not in the federate. */ public void startScopedBlock( - ReactorInstance reactor, - FederateInstance federate, - boolean isFederated, - boolean restrict + ReactorInstance reactor ) { if (reactor != null && reactor.isBank()) { var index = CUtil.bankIndexName(reactor); - if (reactor.depth == 1 && isFederated && restrict) { - // Special case: A bank of federates. Instantiate only the current federate. - startScopedBlock(); - pr("int "+index+" = "+federate.bankIndex+";"); - } else { - pr("// Reactor is a bank. Iterate over bank members."); - pr("for (int "+index+" = 0; "+index+" < "+reactor.width+"; "+index+"++) {"); - indent(); - } + pr("// Reactor is a bank. Iterate over bank members."); + pr("for (int "+index+" = 0; "+index+" < "+reactor.width+"; "+index+"++) {"); + indent(); } else { startScopedBlock(); } @@ -250,15 +239,13 @@ public void startChannelIteration(PortInstance port) { * @param count The variable name to use for the counter, or * null to not provide a counter. */ - public void startScopedBankChannelIteration( - PortInstance port, FederateInstance currentFederate, - String count, boolean isFederated - ) { + public void startScopedBankChannelIteration(PortInstance port, + String count) { if (count != null) { startScopedBlock(); pr("int "+count+" = 0;"); } - startScopedBlock(port.parent, currentFederate, isFederated, true); + startScopedBlock(port.parent); startChannelIteration(port); } @@ -266,7 +253,7 @@ public void startScopedBankChannelIteration( * Start a scoped block that iterates over the specified range of port channels. * * This must be followed by a call to - * {@link #endScopedRangeBlock(RuntimeRange, boolean)}. + * {@link #endScopedRangeBlock(RuntimeRange)}. * * This block should NOT be nested, where each block is * put within a similar block for the reactor's parent. @@ -286,19 +273,13 @@ public void startScopedBankChannelIteration( * @param nested If true, then the runtimeIndex variable will be set * to the bank index of the port's parent's parent rather than the * port's parent. - * @param restrict For federated execution (only), if this argument - * is true, then the iteration will skip over bank members that - * are not in the current federate. */ public void startScopedRangeBlock( - FederateInstance currentFederate, RuntimeRange range, String runtimeIndex, String bankIndex, String channelIndex, - boolean nested, - boolean isFederated, - boolean restrict + boolean nested ) { pr("// Iterate over range "+range.toString()+"."); @@ -332,32 +313,11 @@ public void startScopedRangeBlock( "int "+bi+" = "+(sizeMR <= 1 ? "0" : "range_mr.digits[1]")+"; // Bank index.", "SUPPRESS_UNUSED_WARNING("+bi+");" )); - if (isFederated) { - if (restrict) { - // In case we have a bank of federates. Need that iteration - // only cover the one federate. The last digit of the mixed-radix - // number is the bank index (or 0 if this is not a bank of federates). - pr("if (range_mr.digits[range_mr.size - 1] == "+currentFederate.bankIndex+") {"); - indent(); - } else { - startScopedBlock(); - } - } + } else { var ciValue = rangeMR.getDigits().get(0); var riValue = rangeMR.get(nestedLevel); var biValue = (sizeMR > 1)? rangeMR.getDigits().get(1) : 0; - if (isFederated) { - if (restrict) { - // Special case. Have a bank of federates. Need that iteration - // only cover the one federate. The last digit of the mixed-radix - // number identifies the bank member (or is 0 if not within a bank). - pr("if ("+rangeMR.get(sizeMR - 1)+" == "+currentFederate.bankIndex+") {"); - indent(); - } else { - startScopedBlock(); - } - } pr(String.join("\n", "int "+ri+" = "+riValue+"; SUPPRESS_UNUSED_WARNING("+ri+"); // Runtime index.", "int "+ci+" = "+ciValue+"; SUPPRESS_UNUSED_WARNING("+ci+"); // Channel index.", @@ -393,16 +353,14 @@ public void startScopedRangeBlock( * and related functions must provide the above variable names. * * This must be followed by a call to - * {@link #endScopedRangeBlock(SendRange, RuntimeRange, boolean)}. + * {@link #endScopedRangeBlock(SendRange, RuntimeRange)}. * * @param srcRange The send range. * @param dstRange The destination range. */ public void startScopedRangeBlock( - FederateInstance currentFederate, SendRange srcRange, - RuntimeRange dstRange, - boolean isFederated + RuntimeRange dstRange ) { var srcRangeMR = srcRange.startMR(); var srcSizeMR = srcRangeMR.getRadixes().size(); @@ -410,15 +368,7 @@ public void startScopedRangeBlock( var dstNested = dstRange.instance.isOutput(); pr("// Iterate over ranges "+srcRange+" and "+dstRange+"."); - - if (isFederated && srcRange.width == 1) { - // Skip this whole block if the src is not in the federate. - pr("if ("+srcRangeMR.get(srcRangeMR.numDigits() - 1)+" == "+currentFederate.bankIndex+") {"); - indent(); - } else { - startScopedBlock(); - } - + startScopedBlock(); if (srcRange.width > 1) { pr(String.join("\n", "int src_start[] = { "+joinObjects(srcRangeMR.getDigits(), ", ")+" };", @@ -446,7 +396,7 @@ public void startScopedRangeBlock( )); } - startScopedRangeBlock(currentFederate, dstRange, dr, db, dc, dstNested, isFederated, true); + startScopedRangeBlock(dstRange, dr, db, dc, dstNested); if (srcRange.width > 1) { pr(String.join("\n", @@ -458,16 +408,6 @@ public void startScopedRangeBlock( "SUPPRESS_UNUSED_WARNING("+sb+");" )); } - - // The above startScopedRangeBlock() call will skip any iteration where the destination - // is a bank member is not in the federation. Here, we skip any iteration where the - // source is a bank member not in the federation. - if (isFederated && srcRange.width > 1) { - // The last digit of the mixed radix - // number identifies the bank (or is 0 if no bank). - pr("if (src_range_mr.digits[src_range_mr.size - 1] == "+currentFederate.bankIndex+") {"); - indent(); - } } public void endScopedBlock() { @@ -515,13 +455,8 @@ public void endScopedBankChannelIteration( * @param range The send range. */ public void endScopedRangeBlock( - RuntimeRange range, - boolean isFederated + RuntimeRange range ) { - if (isFederated) { - // Terminate the if statement or block (if not restrict). - endScopedBlock(); - } if (range.width > 1) { pr("mixed_radix_incr(&range_mr);"); endScopedBlock(); // Terminate for loop. @@ -537,18 +472,8 @@ public void endScopedRangeBlock( */ public void endScopedRangeBlock( SendRange srcRange, - RuntimeRange dstRange, - boolean isFederated + RuntimeRange dstRange ) { - // Do not use endScopedRangeBlock because we need things nested. - if (isFederated) { - if (srcRange.width > 1) { - // Terminate the if statement. - endScopedBlock(); - } - // Terminate the if statement or block (if not restrict). - endScopedBlock(); - } if (srcRange.width > 1) { pr(String.join("\n", "mixed_radix_incr(&src_range_mr);", diff --git a/org.lflang/src/org/lflang/generator/CodeMap.java b/org.lflang/src/org/lflang/generator/CodeMap.java index 09a0533eb0..12d5ea8f65 100644 --- a/org.lflang/src/org/lflang/generator/CodeMap.java +++ b/org.lflang/src/org/lflang/generator/CodeMap.java @@ -245,9 +245,6 @@ public String getGeneratedCode() { * Returns the set of all paths to Lingua Franca files * that are known to contain code that corresponds to * code in the generated file represented by this. - * @return the set of all paths to Lingua Franca files - * that are known to contain code that corresponds to - * code in the generated file represented by this */ public Set lfSourcePaths() { return map.keySet(); diff --git a/org.lflang/src/org/lflang/generator/DelayBodyGenerator.java b/org.lflang/src/org/lflang/generator/DelayBodyGenerator.java index a26d002ae5..39742a3488 100644 --- a/org.lflang/src/org/lflang/generator/DelayBodyGenerator.java +++ b/org.lflang/src/org/lflang/generator/DelayBodyGenerator.java @@ -1,6 +1,7 @@ package org.lflang.generator; import org.lflang.lf.Action; +import org.lflang.lf.Reaction; import org.lflang.lf.VarRef; public interface DelayBodyGenerator { @@ -53,4 +54,6 @@ public interface DelayBodyGenerator { */ boolean generateAfterDelaysWithVariableWidth(); + /** Used to optionally apply additional transformations to the generated reactions */ + default void finalizeReactions(Reaction delayReaction, Reaction forwardReaction) { } } diff --git a/org.lflang/src/org/lflang/generator/DockerComposeGenerator.java b/org.lflang/src/org/lflang/generator/DockerComposeGenerator.java new file mode 100644 index 0000000000..040d925614 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/DockerComposeGenerator.java @@ -0,0 +1,123 @@ +package org.lflang.generator; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +import org.lflang.util.FileUtil; + +/** + * Code generator for docker-compose configurations. + * + * @author Marten Lohstroh + * @author Steven Wong + */ +public class DockerComposeGenerator { + + /** + * Path to the docker-compose.yml file. + */ + protected final Path path; + + public DockerComposeGenerator(LFGeneratorContext context) { + this.path = context.getFileConfig().getSrcGenPath().resolve("docker-compose.yml"); + } + + /** + * Return a string that represents the network portion of the docker-compose configuration. + * @param networkName Name of the default network + */ + protected String generateDockerNetwork(String networkName) { + return """ + networks: + default: + name: "%s" + """.formatted(networkName); + } + + /** + * Return a string that represents the services portion of the docker-compose configuration. + * @param services A list of docker data representing the services to render + */ + protected String generateDockerServices(List services) { + return """ + version: "3.9" + services: + %s + """.formatted(services.stream().map( + data -> getServiceDescription(data) + ).collect(Collectors.joining("\n"))); + } + + /** + * Return the command to build and run using the docker-compose configuration. + */ + public String getUsageInstructions() { + return """ + ##################################### + To build and run: + pushd %s && docker compose up --build + To return to the current working directory afterwards: + popd + ##################################### + """.formatted(path.getParent()); + } + + /** + * Turn given docker data into a string. + */ + protected String getServiceDescription(DockerData data) { + return """ + %s: + build: + context: "%s" + container_name: "%s" + """.formatted(getServiceName(data), getBuildContext(data), getContainerName(data)); + } + + /** + * Return the name of the service represented by the given data. + */ + protected String getServiceName(DockerData data) { + return "main"; + } + + /** + * Return the name of the service represented by the given data. + */ + protected String getBuildContext(DockerData data) { + return "."; + } + + /** + * Return the name of the container for the given data. + */ + protected String getContainerName(DockerData data) { + return data.serviceName; + } + + /** + * Write the docker-compose.yml file with a default network called "lf". + * @param services A list of all the services. + */ + public void writeDockerComposeFile(List services) throws IOException { + writeDockerComposeFile(services, "lf"); + } + + /** + * Write the docker-compose.yml file. + * @param services A list of all the services to include. + * @param networkName The name of the network to which docker will connect the services. + */ + public void writeDockerComposeFile( + List services, + String networkName + ) throws IOException { + var contents = String.join("\n", + this.generateDockerServices(services), + this.generateDockerNetwork(networkName)); + FileUtil.writeToFile(contents, path); + System.out.println(getUsageInstructions()); + } +} diff --git a/org.lflang/src/org/lflang/generator/DockerData.java b/org.lflang/src/org/lflang/generator/DockerData.java new file mode 100644 index 0000000000..a2b1927125 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/DockerData.java @@ -0,0 +1,53 @@ +package org.lflang.generator; + +import java.io.IOException; +import java.nio.file.Path; + +import org.lflang.util.FileUtil; + +/** + * Build configuration of a docker service. + * + * @author Marten Lohstroh + */ +public class DockerData { + /** + * The absolute path to the docker file. + */ + private final Path dockerFilePath; + + /** + * The content of the docker file to be generated. + */ + private final String dockerFileContent; + + /** + * The name of the service. + */ + public final String serviceName; + + public DockerData( + String serviceName, + Path dockerFilePath, + String dockerFileContent + ) { + + if (!dockerFilePath.toFile().isAbsolute()) { + throw new RuntimeException("Cannot use relative docker file path in DockerData instance"); + } + this.serviceName = serviceName; + this.dockerFilePath = dockerFilePath; + this.dockerFileContent = dockerFileContent; + } + + /** + * Write a docker file based on this data. + */ + public void writeDockerFile() throws IOException { + if (dockerFilePath.toFile().exists()) { + dockerFilePath.toFile().delete(); + } + FileUtil.writeToFile(dockerFileContent, dockerFilePath); + System.out.println("Dockerfile written to " + dockerFilePath); + } +} diff --git a/org.lflang/src/org/lflang/generator/DockerGenerator.java b/org.lflang/src/org/lflang/generator/DockerGenerator.java new file mode 100644 index 0000000000..902d664631 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/DockerGenerator.java @@ -0,0 +1,58 @@ +package org.lflang.generator; + +import org.lflang.generator.c.CDockerGenerator; +import org.lflang.generator.python.PythonDockerGenerator; +import org.lflang.generator.ts.TSDockerGenerator; + + +/** + * A class for generating docker files. + * + * @author Marten Lohstroh + * @author Hou Seng Wong + */ +public abstract class DockerGenerator { + + /** + * Configuration for interactions with the filesystem. + */ + protected final LFGeneratorContext context; + + /** + * The constructor for the base docker file generation class. + * @param context The context of the code generator. + */ + public DockerGenerator(LFGeneratorContext context) { + this.context = context; + + } + + /** + * Generate the contents of a Dockerfile. + */ + protected abstract String generateDockerFileContent(); + + /** + * Produce a DockerData object. + * If the returned object is to be used in a federated context, + * pass in the file configuration of the federated generator, null otherwise. + * @return docker data created based on the context in this instance + */ + public DockerData generateDockerData() { + var name = context.getFileConfig().name; + var dockerFilePath = context.getFileConfig().getSrcGenPath().resolve("Dockerfile"); + var dockerFileContent = generateDockerFileContent(); + + return new DockerData(name, dockerFilePath, dockerFileContent); + } + + public static DockerGenerator dockerGeneratorFactory(LFGeneratorContext context) { + var target = context.getTargetConfig().target; + return switch (target) { + case C, CCPP -> new CDockerGenerator(context); + case TS -> new TSDockerGenerator(context); + case Python -> new PythonDockerGenerator(context); + case CPP, Rust -> throw new IllegalArgumentException("No Docker support for " + target + " yet."); + }; + } +} diff --git a/org.lflang/src/org/lflang/generator/DockerGeneratorBase.java b/org.lflang/src/org/lflang/generator/DockerGeneratorBase.java deleted file mode 100644 index be750306f6..0000000000 --- a/org.lflang/src/org/lflang/generator/DockerGeneratorBase.java +++ /dev/null @@ -1,302 +0,0 @@ -package org.lflang.generator; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import org.lflang.util.FileUtil; - -/** - * The base class for docker file related code generation. - * - * The design of abstractions is as follows: - * - * Docker-facing API - * - This ("DockerGeneratorBase") class defines a "DockerData" class - * that specifies the information it needs to generate docker files and - * docker compose files for any target. This is the docker-facing - * API. - * - * Target Code Generator-facing API - * - Each target-specific docker generator implements this class and - * defines in themselves a class that implements the "GeneratorData" - * interface of this class. This is the target code generator-facing API. - * - * The purpose of this abstraction design is to contain all - * docker-specific information inside docker generator classes and - * prevent docker-related information from polluting target code generators. - * - * @author Hou Seng Wong - */ -abstract public class DockerGeneratorBase { - /** - * The docker compose services representing each federate. - * Ideally, this would be a list of Strings instead of a StringBuilder. - */ - protected StringBuilder composeServices; - - /** - * A docker file will be generated for each lingua franca module. - * This maps the name of the LF module to the data related to the docker - * file for that module. - */ - protected List dockerDataList; - - /** - * Indicates whether or not the program is federated. - */ - protected final boolean isFederated; - - /** - * In federated execution, the host of the rti. - */ - protected String host = null; - - /** - * Generates the docker file related code for the Python target. - * The type specified in the following javadoc refers to the - * type of the object stored in `moduleNameToData.get(lfModuleName)` - */ - protected class DockerData { - /** - * The absolute path to the docker file. - */ - private Path filePath; - /** - * The content of the docker file to be generated. - */ - private String fileContent; - /** - * The name of the docker compose service for the LF module. - */ - private String composeServiceName; - /** - * The build context of the docker container. - */ - private String buildContext; - - public DockerData( - Path dockerFilePath, - String dockerFileContent, - String dockerBuildContext - ) { - if (dockerFilePath == null || dockerFileContent == null || - dockerBuildContext == null) { - throw new RuntimeException("Missing fields in DockerData instance"); - } - if (!dockerFilePath.toFile().isAbsolute()) { - throw new RuntimeException("Non-absolute docker file path in DockerData instance"); - } - if (!dockerFilePath.toString().endsWith(".Dockerfile")) { - throw new RuntimeException( - "Docker file path does not end with \".Dockerfile\" in DockerData instance"); - } - filePath = dockerFilePath; - fileContent = dockerFileContent; - composeServiceName = filePath.getFileName().toString().replace(".Dockerfile", "").toLowerCase(); - buildContext = dockerBuildContext; - } - - public Path getFilePath() { return filePath; } - public String getFileContent() { return fileContent; } - public String getComposeServiceName() { return composeServiceName; } - public String getBuildContext() { return buildContext; } - } - - /** - * The interface for data from the code generator. - * - * Target-specific docker generators can have a class - * that implements this interface to specify - * what kinds of generator-related data is needed - * during docker file generation. - */ - public interface GeneratorData {} - - /** - * The constructor for the base docker file generation class. - * @param isFederated True if federated execution. False otherwise. - */ - public DockerGeneratorBase(boolean isFederated) { - dockerDataList = new ArrayList<>(); - composeServices = new StringBuilder(); - this.isFederated = isFederated; - } - - /** - * Translate data from the code generator to docker data as - * specified in the DockerData class. - * - * @param generatorData Data from the code generator. - * @return docker data as specified in the DockerData class - */ - abstract protected DockerData generateDockerData(GeneratorData generatorData); - - /** - * Add a file to the list of docker files to generate. - * - * @param generatorData Data from the code generator. - */ - public void addFile(GeneratorData generatorData) { - DockerData dockerData = generateDockerData(generatorData); - dockerDataList.add(dockerData); - appendFederateToDockerComposeServices(dockerData); - } - - /** - * Write the docker files generated for the federates added using `addFederate` so far. - * - * @param dockerComposeFilePath The path where the docker compose file will be written. - */ - public void writeDockerFiles(Path dockerComposeFilePath) throws IOException { - if (!dockerComposeFilePath.getFileName().toString().equals("docker-compose.yml")) { - throw new RuntimeException( - "Docker compose file must have the name \"docker-compose.yml\""); - } - for (DockerData dockerData : dockerDataList) { - writeDockerFile(dockerData); - System.out.println( - getDockerBuildCommandMsg( - dockerComposeFilePath, dockerData)); - } - - System.out.println(getDockerComposeUpMsg(dockerComposeFilePath)); - if (isFederated && host != null) { - appendRtiToDockerComposeServices( - "lflang/rti:rti", - host - ); - } - writeFederatesDockerComposeFile(dockerComposeFilePath, "lf"); - } - - /** - * Writes the docker file given the docker data. - * - * @param dockerData The docker data as specified in the DockerData class. - */ - private void writeDockerFile(DockerData dockerData) throws IOException { - var dockerFilePath = dockerData.getFilePath(); - if (dockerFilePath.toFile().exists()) { - dockerFilePath.toFile().delete(); - } - FileUtil.writeToFile(dockerData.getFileContent(), dockerFilePath); - } - - /** - * Get the command for docker compose depending on the OS. - */ - public static String getDockerComposeCommand() { - String OS = System.getProperty("os.name").toLowerCase(); - return (OS.indexOf("nux") >= 0) ? "docker-compose" : "docker compose"; - } - - /** - * Get the command to build the docker images using the compose file. - * @param dockerComposeFilePath The directory where the docker compose file is generated. - * @param dockerData The docker data as specified in the DockerData class. - * @return The build command printed to the user as to how to build a docker image - * using the generated docker file. - */ - private String getDockerBuildCommandMsg( - Path dockerComposeFilePath, - DockerData dockerData - ) { - return String.join("\n", - "Dockerfile for "+dockerData.getComposeServiceName()+" written to "+dockerData.getFilePath(), - "#####################################", - "To build the docker image, go to "+dockerComposeFilePath.getParent()+" and run:", - "", - " "+getDockerComposeCommand()+" build "+dockerData.getComposeServiceName(), - "", - "#####################################" - ); - } - - /** - * Get the command to launch all containers using the compose file - * @param dockerComposeFilePath The directory where the docker compose file is generated. - */ - private String getDockerComposeUpMsg(Path dockerComposeFilePath) { - return String.join("\n", - "#####################################", - "To launch the docker container(s), go to "+dockerComposeFilePath.getParent()+" and run:", - "", - " "+getDockerComposeCommand()+" up", - "", - "#####################################" - ); - } - - /** - * Write the docker-compose.yml for orchestrating the federates. - * @param dockerComposeFilePath The directory where the docker compose file is generated. - * @param networkName The name of the network to which docker will connect the containers. - */ - private void writeFederatesDockerComposeFile( - Path dockerComposeFilePath, - String networkName - ) throws IOException { - var contents = new CodeBuilder(); - contents.pr(String.join("\n", - "version: \"3.9\"", - "services:", - composeServices.toString(), - "networks:", - " lingua-franca:", - " name: "+networkName - )); - FileUtil.writeToFile(contents.toString(), dockerComposeFilePath); - } - - /** - * Append a service to the "services" section of the docker-compose.yml file. - * @param dockerData The docker data as specified in the DockerData class. - */ - private void appendFederateToDockerComposeServices( - DockerData dockerData - ) { - var tab = " ".repeat(4); - composeServices.append(tab+dockerData.getComposeServiceName()+":\n"); - composeServices.append(tab+tab+"build:\n"); - composeServices.append(tab+tab+tab+"context: "+dockerData.getBuildContext()+"\n"); - composeServices.append(tab+tab+tab+"dockerfile: "+dockerData.getFilePath()+"\n"); - if (isFederated) { - composeServices.append(tab+tab+"command: -i 1\n"); - } - } - - /** - * Append the RTI to the "services" section of the docker-compose.yml file. - * - * @param rtiImageName The name of the docker image of the RTI. - * @param hostName The name of the host to put the RTI docker container. - */ - private void appendRtiToDockerComposeServices(String rtiImageName, String hostName) { - var tab = " ".repeat(4); - composeServices.append(tab+"rti:\n"); - composeServices.append(tab+tab+"image: "+rtiImageName+"\n"); - composeServices.append(tab+tab+"hostname: "+hostName+"\n"); - composeServices.append(tab+tab+"command: -i 1 -n "+dockerDataList.size()+"\n"); - } - - /** - * Set the `host` of the container - * that launches the RTI. - * @param host The host to set. - */ - public void setHost(Object host) { - if (host != null) { - setHost(host.toString()); - } - } - - /** - * Set the `host` of the container - * that launches the RTI. - * @param host The host to set. - */ - public void setHost(String host) { - this.host = host; - } -} diff --git a/org.lflang/src/org/lflang/generator/FedDockerComposeGenerator.java b/org.lflang/src/org/lflang/generator/FedDockerComposeGenerator.java new file mode 100644 index 0000000000..d666abba2d --- /dev/null +++ b/org.lflang/src/org/lflang/generator/FedDockerComposeGenerator.java @@ -0,0 +1,66 @@ +package org.lflang.generator; + + +import java.util.List; + +/** + * A docker-compose configuration generator for a federated program. + * + * @author Marten Lohstroh + */ +public class FedDockerComposeGenerator extends DockerComposeGenerator { + + /** + * The host on which to run the rti. + */ + private String rtiHost; + + /** + * The name of this federation. + */ + private String containerName; + + public FedDockerComposeGenerator(LFGeneratorContext context, String rtiHost) { + super(context); + this.rtiHost = rtiHost; + this.containerName = context.getFileConfig().name; + } + + @Override + protected String generateDockerServices(List services) { + return """ + %s\ + rti: + image: "lflang/rti:rti" + hostname: "%s" + command: "-i 1 -n %s" + container_name: "%s-rti" + """.formatted(super.generateDockerServices(services), + this.rtiHost, services.size(), containerName); + } + + @Override + protected String getServiceDescription(DockerData data) { + return """ + %s\ + command: "-i 1" + depends_on: + - rti + """.formatted(super.getServiceDescription(data)); + } + + @Override + protected String getServiceName(DockerData data) { + return data.serviceName; + } + + @Override + protected String getBuildContext(DockerData data) { + return data.serviceName; + } + + @Override + protected String getContainerName(DockerData data) { + return this.containerName + "-" + data.serviceName; + } +} diff --git a/org.lflang/src/org/lflang/generator/GeneratorBase.java b/org.lflang/src/org/lflang/generator/GeneratorBase.java index a3b6365de7..c55a9cc29f 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorBase.java +++ b/org.lflang/src/org/lflang/generator/GeneratorBase.java @@ -28,50 +28,38 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; -import java.util.LinkedList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + import org.eclipse.core.resources.IMarker; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.util.EcoreUtil; -import org.eclipse.xtext.util.CancelIndicator; -import org.eclipse.xtext.xbase.lib.CollectionLiterals; import org.eclipse.xtext.xbase.lib.IterableExtensions; import org.eclipse.xtext.xbase.lib.IteratorExtensions; -import org.eclipse.xtext.xbase.lib.Pair; + import org.lflang.ASTUtils; import org.lflang.ErrorReporter; import org.lflang.FileConfig; -import org.lflang.InferredType; import org.lflang.MainConflictChecker; import org.lflang.Target; import org.lflang.TargetConfig; -import org.lflang.TargetProperty.CoordinationType; import org.lflang.TimeUnit; import org.lflang.TimeValue; import org.lflang.ast.AstTransformation; -import org.lflang.federated.FedASTUtils; -import org.lflang.federated.FederateInstance; -import org.lflang.federated.serialization.SupportedSerializers; import org.lflang.graph.InstantiationGraph; -import org.lflang.lf.Action; import org.lflang.lf.Connection; import org.lflang.lf.Expression; import org.lflang.lf.Instantiation; import org.lflang.lf.LfFactory; import org.lflang.lf.Mode; -import org.lflang.lf.Parameter; + import org.lflang.lf.Reaction; import org.lflang.lf.Reactor; import org.lflang.lf.Time; -import org.lflang.lf.VarRef; import org.lflang.validation.AbstractLFValidator; import com.google.common.base.Objects; @@ -106,14 +94,11 @@ public abstract class GeneratorBase extends AbstractLFValidator { /** * The current target configuration. */ - protected TargetConfig targetConfig = new TargetConfig(); + protected final TargetConfig targetConfig; public TargetConfig getTargetConfig() { return this.targetConfig;} - /** - * The current file configuration. - */ - protected FileConfig fileConfig; + public final LFGeneratorContext context; /** * A factory for compiler commands. @@ -141,7 +126,7 @@ public abstract class GeneratorBase extends AbstractLFValidator { /** * The set of resources referenced reactor classes reside in. */ - protected Set resources = new LinkedHashSet<>(); + protected Set resources = new LinkedHashSet<>(); // FIXME: Why do we need this? /** * Graph that tracks dependencies between instantiations. @@ -173,18 +158,7 @@ public abstract class GeneratorBase extends AbstractLFValidator { protected Map reactionBankIndices = null; /** - * Keep a unique list of enabled serializers - */ - public HashSet enabledSerializers = new HashSet<>(); - - /** - * Indicates whether or not the current Lingua Franca program - * contains a federation. - */ - public boolean isFederated = false; - - /** - * Indicates whether or not the current Lingua Franca program + * Indicates whether the current Lingua Franca program * contains model reactors. */ public boolean hasModalReactors = false; @@ -195,38 +169,6 @@ public abstract class GeneratorBase extends AbstractLFValidator { */ public boolean hasDeadlines = false; - // ////////////////////////////////////////// - // // Target properties, if they are included. - /** - * A list of federate instances or a list with a single empty string - * if there are no federates specified. FIXME: Why put a single empty string there? It should be just empty... - */ - public List federates = new ArrayList<>(); - - /** - * A map from federate IDs to federate instances. - */ - protected Map federateByID = new LinkedHashMap<>(); - - /** - * A map from instantiations to the federate instances for that instantiation. - * If the instantiation has a width, there may be more than one federate instance. - */ - protected Map> federatesByInstantiation; - - /** - * The federation RTI properties, which defaults to 'localhost: 15045'. - */ - protected LinkedHashMap federationRTIProperties = CollectionLiterals.newLinkedHashMap( - Pair.of("host", "localhost"), - Pair.of("port", 0) // Indicator to use the default port, typically 15045. - ); - - /** - * Contents of $LF_CLASSPATH, if it was set. - */ - protected String classpathLF; - // ////////////////////////////////////////// // // Private fields. @@ -238,10 +180,11 @@ public abstract class GeneratorBase extends AbstractLFValidator { /** * Create a new GeneratorBase object. */ - public GeneratorBase(FileConfig fileConfig, ErrorReporter errorReporter) { - this.fileConfig = fileConfig; - this.errorReporter = errorReporter; - this.commandFactory = new GeneratorCommandFactory(errorReporter, fileConfig); + public GeneratorBase(LFGeneratorContext context) { + this.context = context; + this.targetConfig = context.getTargetConfig(); + this.errorReporter = context.getErrorReporter(); + this.commandFactory = new GeneratorCommandFactory(errorReporter, context.getFileConfig()); } /** @@ -262,9 +205,9 @@ protected void registerTransformation(AstTransformation transformation) { */ private void createMainInstantiation() { // Find the main reactor and create an AST node for its instantiation. - Iterable nodes = IteratorExtensions.toIterable(fileConfig.resource.getAllContents()); + Iterable nodes = IteratorExtensions.toIterable(context.getFileConfig().resource.getAllContents()); for (Reactor reactor : Iterables.filter(nodes, Reactor.class)) { - if (reactor.isMain() || reactor.isFederated()) { + if (reactor.isMain()) { // Creating a definition for the main reactor because there isn't one. this.mainDef = LfFactory.eINSTANCE.createInstantiation(); this.mainDef.setName(reactor.getName()); @@ -287,10 +230,8 @@ private void createMainInstantiation() { */ public void doGenerate(Resource resource, LFGeneratorContext context) { - GeneratorUtils.setTargetConfig( - context, GeneratorUtils.findTarget(fileConfig.resource), targetConfig, errorReporter - ); - + // FIXME: the signature can be reduced to only take context. + // The constructor also need not take a file config because this is tied to the context as well. cleanIfNeeded(context); printInfo(context.getMode()); @@ -301,13 +242,13 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { ((EclipseErrorReporter) errorReporter).clearMarkers(); } - ASTUtils.setMainName(fileConfig.resource, fileConfig.name); + ASTUtils.setMainName(context.getFileConfig().resource, context.getFileConfig().name); createMainInstantiation(); // Check if there are any conflicting main reactors elsewhere in the package. if (Objects.equal(context.getMode(), LFGeneratorContext.Mode.STANDALONE) && mainDef != null) { - for (String conflict : new MainConflictChecker(fileConfig).conflicts) { + for (String conflict : new MainConflictChecker(context.getFileConfig()).conflicts) { errorReporter.reportError(this.mainDef.getReactorClass(), "Conflicting main reactor in " + conflict); } } @@ -318,24 +259,21 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { commandFactory.setQuiet(); } - // This must be done before desugaring delays below. - analyzeFederates(context); - // Process target files. Copy each of them into the src-gen dir. // FIXME: Should we do this here? This doesn't make sense for federates the way it is // done here. - copyUserFiles(this.targetConfig, this.fileConfig); + copyUserFiles(this.targetConfig, context.getFileConfig()); // Collect reactors and create an instantiation graph. // These are needed to figure out which resources we need // to validate, which happens in setResources(). setReactorsAndInstantiationGraph(context.getMode()); - GeneratorUtils.validate(context, fileConfig, instantiationGraph, errorReporter); + GeneratorUtils.validate(context, context.getFileConfig(), instantiationGraph, errorReporter); List allResources = GeneratorUtils.getResources(reactors); resources.addAll(allResources.stream() // FIXME: This filter reproduces the behavior of the method it replaces. But why must it be so complicated? Why are we worried about weird corner cases like this? - .filter(it -> !Objects.equal(it, fileConfig.resource) || mainDef != null && it == mainDef.getReactorClass().eResource()) - .map(it -> GeneratorUtils.getLFResource(it, fileConfig.getSrcGenBasePath(), context, errorReporter)) + .filter(it -> !Objects.equal(it, context.getFileConfig().resource) || mainDef != null && it == mainDef.getReactorClass().eResource()) + .map(it -> GeneratorUtils.getLFResource(it, context.getFileConfig().getSrcGenBasePath(), context, errorReporter)) .toList() ); GeneratorUtils.accommodatePhysicalActionsIfPresent( @@ -363,8 +301,6 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { hasModalReactors = IterableExtensions.exists(reactors, it -> !it.getModes().isEmpty()); checkModalReactorSupport(false); additionalPostProcessingForModes(); - - enableSupportForSerializationIfApplicable(context.getCancelIndicator()); } /** @@ -374,7 +310,7 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { protected void cleanIfNeeded(LFGeneratorContext context) { if (context.getArgs().containsKey("clean")) { try { - fileConfig.doClean(); + context.getFileConfig().doClean(); } catch (IOException e) { System.err.println("WARNING: IO Error during clean"); } @@ -391,7 +327,7 @@ protected void cleanIfNeeded(LFGeneratorContext context) { */ protected void setReactorsAndInstantiationGraph(LFGeneratorContext.Mode mode) { // Build the instantiation graph . - instantiationGraph = new InstantiationGraph(fileConfig.resource, false); + instantiationGraph = new InstantiationGraph(context.getFileConfig().resource, false); // Topologically sort the reactors such that all of a reactor's instantiation dependencies occur earlier in // the sorted list of reactors. This helps the code generator output code in the correct order. @@ -402,7 +338,7 @@ protected void setReactorsAndInstantiationGraph(LFGeneratorContext.Mode mode) { // If there is no main reactor or if all reactors in the file need to be validated, then make sure the reactors // list includes even reactors that are not instantiated anywhere. if (mainDef == null || Objects.equal(mode, LFGeneratorContext.Mode.LSP_MEDIUM)) { - Iterable nodes = IteratorExtensions.toIterable(fileConfig.resource.getAllContents()); + Iterable nodes = IteratorExtensions.toIterable(context.getFileConfig().resource.getAllContents()); for (Reactor r : IterableExtensions.filter(nodes, Reactor.class)) { if (!reactors.contains(r)) { reactors.add(r); @@ -586,136 +522,6 @@ protected void additionalPostProcessingForModes() { // Do nothing } - /** - * Generate code for the body of a reaction that handles the - * action that is triggered by receiving a message from a remote - * federate. - * @param action The action. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param receivingFed The destination federate. - * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. - * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. - * @param type The type. - * @param isPhysical Indicates whether or not the connection is physical - * @param serializer The serializer used on the connection. - */ - public String generateNetworkReceiverBody( - Action action, - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - FederateInstance receivingFed, - int receivingBankIndex, - int receivingChannelIndex, - InferredType type, - boolean isPhysical, - SupportedSerializers serializer - ) { - throw new UnsupportedOperationException("This target does not support network connections between federates."); - } - - /** - * Generate code for the body of a reaction that handles an output - * that is to be sent over the network. This base class throws an exception. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. - * @param receivingFed The destination federate. - * @param type The type. - * @param isPhysical Indicates whether the connection is physical or not - * @param delay The delay value imposed on the connection using after - * @throws UnsupportedOperationException If the target does not support this operation. - * @param serializer The serializer used on the connection. - */ - public String generateNetworkSenderBody( - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - int sendingBankIndex, - int sendingChannelIndex, - FederateInstance receivingFed, - InferredType type, - boolean isPhysical, - Expression delay, - SupportedSerializers serializer - ) { - throw new UnsupportedOperationException("This target does not support network connections between federates."); - } - - /** - * Generate code for the body of a reaction that waits long enough so that the status - * of the trigger for the given port becomes known for the current logical time. - * - * @param receivingPortID port The port to generate the control reaction for - * @param maxSTP The maximum value of STP is assigned to reactions (if any) - * that have port as their trigger or source - */ - public String generateNetworkInputControlReactionBody( - int receivingPortID, - TimeValue maxSTP - ) { - throw new UnsupportedOperationException("This target does not support network connections between federates."); - } - - /** - * Generate code for the body of a reaction that sends a port status message for the given - * port if it is absent. - * - * @param port The port to generate the control reaction for - * @param portID The ID assigned to the port in the AST transformation - * @param receivingFederateID The ID of the receiving federate - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel if a multiport - * @param delay The delay value imposed on the connection using after - */ - public String generateNetworkOutputControlReactionBody( - VarRef port, - int portID, - int receivingFederateID, - int sendingBankIndex, - int sendingChannelIndex, - Expression delay - ) { - throw new UnsupportedOperationException("This target does not support network connections between federates."); - } - - /** - * Add necessary code to the source and necessary build support to - * enable the requested serializations in 'enabledSerializations' - */ - public void enableSupportForSerializationIfApplicable(CancelIndicator cancelIndicator) { - if (!IterableExtensions.isNullOrEmpty(enabledSerializers)) { - throw new UnsupportedOperationException( - "Serialization is target-specific "+ - " and is not implemented for the "+getTarget().toString()+" target." - ); - } - } - - /** - * Returns true if the program is federated and uses the decentralized - * coordination mechanism. - */ - public boolean isFederatedAndDecentralized() { - return isFederated && targetConfig.coordination == CoordinationType.DECENTRALIZED; - } - - /** - * Returns true if the program is federated and uses the centralized - * coordination mechanism. - */ - public boolean isFederatedAndCentralized() { - return isFederated && targetConfig.coordination == CoordinationType.CENTRALIZED; - } - /** * Parsed error message from a compiler is returned here. */ @@ -766,7 +572,7 @@ public void reportCommandErrors(String stderr) { String[] lines = stderr.split("\\r?\\n"); StringBuilder message = new StringBuilder(); Integer lineNumber = null; - Path path = fileConfig.srcFile; + Path path = context.getFileConfig().srcFile; // In case errors occur within an imported file, record the original path. Path originalPath = path; @@ -848,330 +654,17 @@ public void reportCommandErrors(String stderr) { } } - /** - * Generate target code for a parameter reference. - * - * @param param The parameter to generate code for - * @return Parameter reference in target code - */ - protected String getTargetReference(Parameter param) { - return param.getName(); - } - // ////////////////////////////////////////////////// // // Private functions - /** - * Remove triggers in each federates' network reactions that are defined - * in remote federates. - * - * This must be done in code generators after the dependency graphs - * are built and levels are assigned. Otherwise, these disconnected ports - * might reference data structures in remote federates and cause - * compile/runtime errors. - * - * @param instance The reactor instance to remove these ports from if any. - * Can be null. - */ - protected void removeRemoteFederateConnectionPorts(ReactorInstance instance) { - if (!isFederated) { - return; - } - for (FederateInstance federate : federates) { - // Remove disconnected network triggers from the AST - federate.removeRemoteFederateConnectionPorts(); - if (instance == null) { - continue; - } - // If passed a reactor instance, also purge the disconnected network triggers - // from the reactor instance graph - for (Reaction reaction : federate.networkReactions) { - ReactionInstance networkReaction = instance.lookupReactionInstance(reaction); - if (networkReaction == null) { - continue; - } - for (VarRef port : federate.remoteNetworkReactionTriggers) { - PortInstance disconnectedPortInstance = instance.lookupPortInstance(port); - if (disconnectedPortInstance != null) { - networkReaction.removePortInstance(disconnectedPortInstance); - } - } - } - } - } - - /** - * Set the RTI hostname, port and username if given as compiler arguments - */ - private void setFederationRTIProperties(LFGeneratorContext context) { - String rtiAddr = context.getArgs().getProperty("rti"); - Pattern pattern = Pattern.compile("([a-zA-Z0-9]+@)?([a-zA-Z0-9]+\\.?[a-z]{2,}|[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+):?([0-9]+)?"); - Matcher matcher = pattern.matcher(rtiAddr); - - if (!matcher.find()) { - return; - } - - // the user match group contains a trailing "@" which needs to be removed. - String userWithAt = matcher.group(1); - String user = userWithAt == null ? null : userWithAt.substring(0, userWithAt.length() - 1); - String host = matcher.group(2); - String port = matcher.group(3); - - if (host != null) { - federationRTIProperties.put("host", host); - } - if (port != null) { - federationRTIProperties.put("port", port); - } - if (user != null) { - federationRTIProperties.put("user", user); - } - } - - /** - * Analyze the AST to determine whether code is being mapped to - * single or to multiple target machines. If it is being mapped - * to multiple machines, then set the {@link #isFederated} field to true, - * create a FederateInstance for each federate, and record various - * properties of the federation - * - * In addition, for each top-level connection, add top-level reactions to the AST - * that send and receive messages over the network. - * - * This class is target independent, so the target code - * generator still has quite a bit of work to do. - * It needs to provide the body of the sending and - * receiving reactions. It also needs to provide the - * runtime infrastructure that uses the dependency - * information between federates. See the C target - * for a reference implementation. - */ - private void analyzeFederates(LFGeneratorContext context) { - // Next, if there actually are federates, analyze the topology - // interconnecting them and replace the connections between them - // with an action and two reactions. - Reactor mainReactor = mainDef != null ? ASTUtils.toDefinition(mainDef.getReactorClass()) : null; - - if (mainDef == null || !mainReactor.isFederated()) { - // The program is not federated. - // Ensure federates is never empty. - FederateInstance federateInstance = new FederateInstance(null, 0, 0, this, errorReporter); - federates.add(federateInstance); - federateByID.put(0, federateInstance); - } else { - // The Lingua Franca program is federated - isFederated = true; - - // If the "--rti" flag is given to the compiler, use the argument from the flag. - if (context.getArgs().containsKey("rti")) { - setFederationRTIProperties(context); - } else if (mainReactor.getHost() != null) { - // Get the host information, if specified. - // If not specified, this defaults to 'localhost' - if (mainReactor.getHost().getAddr() != null) { - federationRTIProperties.put("host", mainReactor.getHost().getAddr()); - } - // Get the port information, if specified. - // If not specified, this defaults to 14045 - if (mainReactor.getHost().getPort() != 0) { - federationRTIProperties.put("port", mainReactor.getHost().getPort()); - } - // Get the user information, if specified. - if (mainReactor.getHost().getUser() != null) { - federationRTIProperties.put("user", mainReactor.getHost().getUser()); - } - } - - // Since federates are always within the main (federated) reactor, - // create a list containing just that one containing instantiation. - // This will be used to look up parameter values. - List mainReactorContext = new ArrayList<>(); - mainReactorContext.add(mainDef); - - // Create a FederateInstance for each top-level reactor. - for (Instantiation instantiation : ASTUtils.allInstantiations(mainReactor)) { - int bankWidth = ASTUtils.width(instantiation.getWidthSpec(), mainReactorContext); - if (bankWidth < 0) { - errorReporter.reportError(instantiation, "Cannot determine bank width! Assuming width of 1."); - // Continue with a bank width of 1. - bankWidth = 1; - } - // Create one federate instance for each instance in a bank of reactors. - List federateInstances = new ArrayList<>(bankWidth); - for (int i = 0; i < bankWidth; i++) { - // Assign an integer ID to the federate. - int federateID = federates.size(); - FederateInstance federateInstance = new FederateInstance(instantiation, federateID, i, this, errorReporter); - federateInstance.bankIndex = i; - federates.add(federateInstance); - federateInstances.add(federateInstance); - federateByID.put(federateID, federateInstance); - - if (instantiation.getHost() != null) { - federateInstance.host = instantiation.getHost().getAddr(); - // The following could be 0. - federateInstance.port = instantiation.getHost().getPort(); - // The following could be null. - federateInstance.user = instantiation.getHost().getUser(); - /* FIXME: The at keyword should support a directory component. - * federateInstance.dir = instantiation.getHost().dir - */ - if (federateInstance.host != null && - !federateInstance.host.equals("localhost") && - !federateInstance.host.equals("0.0.0.0") - ) { - federateInstance.isRemote = true; - } - } - } - if (federatesByInstantiation == null) { - federatesByInstantiation = new LinkedHashMap<>(); - } - federatesByInstantiation.put(instantiation, federateInstances); - } - - // In a federated execution, we need keepalive to be true, - // otherwise a federate could exit simply because it hasn't received - // any messages. - targetConfig.keepalive = true; - - // Analyze the connection topology of federates. - // First, find all the connections between federates. - // For each connection between federates, replace it in the - // AST with an action (which inherits the delay) and two reactions. - // The action will be physical for physical connections and logical - // for logical connections. - replaceFederateConnectionsWithActions(); - - // Remove the connections at the top level - mainReactor.getConnections().clear(); - } - } - - /** - * Replace connections between federates in the AST with actions that - * handle sending and receiving data. - */ - private void replaceFederateConnectionsWithActions() { - Reactor mainReactor = mainDef != null ? ASTUtils.toDefinition(mainDef.getReactorClass()) : null; - - // Each connection in the AST may represent more than one connection between - // federate instances because of banks and multiports. We need to generate communication - // for each of these. To do this, we create a ReactorInstance so that we don't have - // to duplicate the rather complicated logic in that class. We specify a depth of 1, - // so it only creates the reactors immediately within the top level, not reactors - // that those contain. - ReactorInstance mainInstance = new ReactorInstance(mainReactor, errorReporter, 1); - - for (ReactorInstance child : mainInstance.children) { - for (PortInstance output : child.outputs) { - replaceConnectionFromFederate(output, child, mainInstance); - } - } - } - - /** - * Replace the connections from the specified output port for the specified federate reactor. - * @param output The output port instance. - * @param federateReactor The reactor instance for that federate. - * @param mainInstance The main reactor instance. - */ - private void replaceConnectionFromFederate( - PortInstance output, - ReactorInstance federateReactor, - ReactorInstance mainInstance - ) { - for (SendRange srcRange : output.dependentPorts) { - for (RuntimeRange dstRange : srcRange.destinations) { - - MixedRadixInt srcID = srcRange.startMR(); - MixedRadixInt dstID = dstRange.startMR(); - int dstCount = 0; - int srcCount = 0; - - while (dstCount++ < dstRange.width) { - int srcChannel = srcID.getDigits().get(0); - int srcBank = srcID.get(1); - int dstChannel = dstID.getDigits().get(0); - int dstBank = dstID.get(1); - - FederateInstance srcFederate = federatesByInstantiation.get( - srcRange.instance.parent.definition - ).get(srcBank); - FederateInstance dstFederate = federatesByInstantiation.get( - dstRange.instance.parent.definition - ).get(dstBank); - - Connection connection = srcRange.connection; - - if (connection == null) { - // This should not happen. - errorReporter.reportError(output.definition, - "Unexpected error. Cannot find output connection for port"); - } else { - if ( - !connection.isPhysical() - && targetConfig.coordination != CoordinationType.DECENTRALIZED - ) { - // Map the delays on connections between federates. - Set dependsOnDelays = dstFederate.dependsOn.computeIfAbsent( - srcFederate, - k -> new LinkedHashSet<>() - ); - // Put the delay on the cache. - if (connection.getDelay() != null) { - dependsOnDelays.add(connection.getDelay()); - } else { - // To indicate that at least one connection has no delay, add a null entry. - dependsOnDelays.add(null); - } - // Map the connections between federates. - Set sendsToDelays = srcFederate.sendsTo.computeIfAbsent( - dstFederate, - k -> new LinkedHashSet<>() - ); - if (connection.getDelay() != null) { - sendsToDelays.add(connection.getDelay()); - } else { - // To indicate that at least one connection has no delay, add a null entry. - sendsToDelays.add(null); - } - } - - FedASTUtils.makeCommunication( - srcRange.instance, - dstRange.instance, - connection, - srcFederate, - srcBank, - srcChannel, - dstFederate, - dstBank, - dstChannel, - this, - targetConfig.coordination - ); - } - dstID.increment(); - srcID.increment(); - srcCount++; - if (srcCount == srcRange.width) { - srcID = srcRange.startMR(); // Multicast. Start over. - } - } - } - } - } - /** * Print to stdout information about what source file is being generated, * what mode the generator is in, and where the generated sources are to be put. */ public void printInfo(LFGeneratorContext.Mode mode) { - System.out.println("Generating code for: " + fileConfig.resource.getURI().toString()); + System.out.println("Generating code for: " + context.getFileConfig().resource.getURI().toString()); System.out.println("******** mode: " + mode); - System.out.println("******** generated sources: " + fileConfig.getSrcGenPath()); + System.out.println("******** generated sources: " + context.getFileConfig().getSrcGenPath()); } /** diff --git a/org.lflang/src/org/lflang/generator/GeneratorResult.java b/org.lflang/src/org/lflang/generator/GeneratorResult.java index 475aea144f..2b42e17f34 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorResult.java +++ b/org.lflang/src/org/lflang/generator/GeneratorResult.java @@ -1,11 +1,18 @@ package org.lflang.generator; +import java.io.File; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; import java.util.function.Function; +import org.eclipse.xtext.generator.GeneratorContext; + +import org.lflang.FileConfig; +import org.lflang.Target; +import org.lflang.TargetConfig; import org.lflang.util.LFCommand; /** @@ -17,8 +24,8 @@ public class GeneratorResult { public static GeneratorResult NOTHING = incompleteGeneratorResult(Status.NOTHING); public static GeneratorResult CANCELLED = incompleteGeneratorResult(Status.CANCELLED); public static GeneratorResult FAILED = incompleteGeneratorResult(Status.FAILED); - public static Function, GeneratorResult> GENERATED_NO_EXECUTABLE - = codeMaps -> new GeneratorResult(Status.GENERATED, null, null, codeMaps); + public static BiFunction, GeneratorResult> GENERATED_NO_EXECUTABLE + = (context, codeMaps) -> new GeneratorResult(Status.GENERATED, context, codeMaps); /** * A {@code Status} is a level of completion of a code generation task. @@ -37,12 +44,10 @@ public enum Status { */ public interface GetUserMessage { GetUserMessage COMPLETED = result -> { - if (result.executable != null) { - return String.format( - "Code generation complete. The executable is at \"%s\".", result.executable - ); - } - return "Code generation completed."; + return String.format( + "Code generation complete. The executable is at \"%s\".", + result.getContext().getFileConfig().getExecutable() + ); }; String apply(GeneratorResult result); } @@ -57,23 +62,20 @@ public interface GetUserMessage { } private final Status status; - private final Path executable; - private final LFCommand command; + + private final LFGeneratorContext context; + private final Map codeMaps; /** * Initialize a GeneratorResult. * @param status The level of completion of a code generation task. - * @param executable The file that stores the final output of the code - * generation task. Examples include a fully linked binary or a Python - * file that can be passed to the Python interpreter. - * @param command A command that runs the executable. + * @param context The context within which the result was produced. * @param codeMaps A mapping from generated files to their CodeMaps. */ - public GeneratorResult(Status status, Path executable, LFCommand command, Map codeMaps) { + public GeneratorResult(Status status, LFGeneratorContext context, Map codeMaps) { this.status = status != null ? status : Status.NOTHING; - this.executable = executable; - this.command = command; + this.context = context; this.codeMaps = codeMaps != null ? codeMaps : Collections.emptyMap(); } @@ -84,7 +86,7 @@ public GeneratorResult(Status status, Path executable, LFCommand command, Map getCodeMaps() { return Collections.unmodifiableMap(codeMaps); } + + public LFGeneratorContext getContext() { + return context; + } + } diff --git a/org.lflang/src/org/lflang/generator/GeneratorUtils.java b/org.lflang/src/org/lflang/generator/GeneratorUtils.java index be4e122c5e..f51439c390 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorUtils.java +++ b/org.lflang/src/org/lflang/generator/GeneratorUtils.java @@ -7,6 +7,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Properties; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; @@ -25,6 +26,7 @@ import org.lflang.FileConfig; import org.lflang.Target; import org.lflang.TargetConfig; +import org.lflang.TargetConfig.DockerOptions; import org.lflang.TargetProperty.BuildType; import org.lflang.TargetProperty.LogLevel; import org.lflang.TargetProperty.UnionType; @@ -68,63 +70,72 @@ public static TargetDecl findTarget(Resource resource) { /** * Set the appropriate target properties based on the target properties of * the main .lf file and the given command-line arguments, if applicable. - * @param context The generator invocation context. - * @param target The target configuration that appears in an LF source file. - * @param targetConfig The target config to be updated. + * @param args The commandline arguments to process. + * @param target The target properties AST node. * @param errorReporter The error reporter to which errors should be sent. */ - public static void setTargetConfig( - LFGeneratorContext context, + public static TargetConfig getTargetConfig( + Properties args, TargetDecl target, - TargetConfig targetConfig, ErrorReporter errorReporter ) { + final TargetConfig targetConfig = new TargetConfig(target); // FIXME: why not just do all of this in the constructor? if (target.getConfig() != null) { List pairs = target.getConfig().getPairs(); TargetProperty.set(targetConfig, pairs != null ? pairs : List.of(), errorReporter); } - if (context.getArgs().containsKey("no-compile")) { + if (args.containsKey("no-compile")) { targetConfig.noCompile = true; } - if (context.getArgs().containsKey("build-type")) { - targetConfig.cmakeBuildType = (BuildType) UnionType.BUILD_TYPE_UNION.forName(context.getArgs().getProperty("build-type")); + if (args.containsKey("docker")) { + var arg = args.getProperty("docker"); + if (Boolean.parseBoolean(arg)) { + targetConfig.dockerOptions = new DockerOptions(); + } else { + targetConfig.dockerOptions = null; + } + // FIXME: this is pretty ad-hoc and does not account for more complex overrides yet. + } + if (args.containsKey("build-type")) { + targetConfig.cmakeBuildType = (BuildType) UnionType.BUILD_TYPE_UNION.forName(args.getProperty("build-type")); } - if (context.getArgs().containsKey("logging")) { - targetConfig.logLevel = LogLevel.valueOf(context.getArgs().getProperty("logging").toUpperCase()); + if (args.containsKey("logging")) { + targetConfig.logLevel = LogLevel.valueOf(args.getProperty("logging").toUpperCase()); } - if (context.getArgs().containsKey("workers")) { - targetConfig.workers = Integer.parseInt(context.getArgs().getProperty("workers")); + if (args.containsKey("workers")) { + targetConfig.workers = Integer.parseInt(args.getProperty("workers")); } - if (context.getArgs().containsKey("threading")) { - targetConfig.threading = Boolean.parseBoolean(context.getArgs().getProperty("threading")); + if (args.containsKey("threading")) { + targetConfig.threading = Boolean.parseBoolean(args.getProperty("threading")); } - if (context.getArgs().containsKey("target-compiler")) { - targetConfig.compiler = context.getArgs().getProperty("target-compiler"); + if (args.containsKey("target-compiler")) { + targetConfig.compiler = args.getProperty("target-compiler"); } - if (context.getArgs().containsKey("scheduler")) { + if (args.containsKey("scheduler")) { targetConfig.schedulerType = SchedulerOption.valueOf( - context.getArgs().getProperty("scheduler") + args.getProperty("scheduler") ); targetConfig.setByUser.add(TargetProperty.SCHEDULER); } - if (context.getArgs().containsKey("target-flags")) { + if (args.containsKey("target-flags")) { targetConfig.compilerFlags.clear(); - if (!context.getArgs().getProperty("target-flags").isEmpty()) { + if (!args.getProperty("target-flags").isEmpty()) { targetConfig.compilerFlags.addAll(List.of( - context.getArgs().getProperty("target-flags").split(" ") + args.getProperty("target-flags").split(" ") )); } } - if (context.getArgs().containsKey("runtime-version")) { - targetConfig.runtimeVersion = context.getArgs().getProperty("runtime-version"); + if (args.containsKey("runtime-version")) { + targetConfig.runtimeVersion = args.getProperty("runtime-version"); } - if (context.getArgs().containsKey("external-runtime-path")) { - targetConfig.externalRuntimePath = context.getArgs().getProperty("external-runtime-path"); + if (args.containsKey("external-runtime-path")) { + targetConfig.externalRuntimePath = args.getProperty("external-runtime-path"); } - if (context.getArgs().containsKey(TargetProperty.KEEPALIVE.description)) { + if (args.containsKey(TargetProperty.KEEPALIVE.description)) { targetConfig.keepalive = Boolean.parseBoolean( - context.getArgs().getProperty(TargetProperty.KEEPALIVE.description)); + args.getProperty(TargetProperty.KEEPALIVE.description)); } + return targetConfig; } /** @@ -306,20 +317,15 @@ public static LFResource getLFResource( LFGeneratorContext context, ErrorReporter errorReporter ) { - TargetDecl target = GeneratorUtils.findTarget(resource); + var target = ASTUtils.targetDecl(resource); KeyValuePairs config = target.getConfig(); - var targetConfig = new TargetConfig(); + var targetConfig = new TargetConfig(target); if (config != null) { List pairs = config.getPairs(); TargetProperty.set(targetConfig, pairs != null ? pairs : List.of(), errorReporter); } - try { - FileConfig fc = new FileConfig(resource, srcGenBasePath, context.useHierarchicalBin()); - return new LFResource(resource, fc, targetConfig); - } catch (IOException e) { - throw new RuntimeException("Failed to instantiate an imported resource because an I/O error " - + "occurred."); - } + FileConfig fc = LFGenerator.createFileConfig(resource, srcGenBasePath, context.getFileConfig().useHierarchicalBin); + return new LFResource(resource, fc, targetConfig); } /** diff --git a/org.lflang/src/org/lflang/generator/IntegratedBuilder.java b/org.lflang/src/org/lflang/generator/IntegratedBuilder.java index e957b8cd3f..d6a2d3d681 100644 --- a/org.lflang/src/org/lflang/generator/IntegratedBuilder.java +++ b/org.lflang/src/org/lflang/generator/IntegratedBuilder.java @@ -78,8 +78,6 @@ public GeneratorResult run( ReportProgress reportProgress, CancelIndicator cancelIndicator ) { - // FIXME: A refactoring of the following line is needed. This refactor will affect FileConfig and - // org.lflang.cli.Lfc. The issue is that there is duplicated code. fileAccess.setOutputPath( FileConfig.findPackageRoot(Path.of(uri.path()), s -> {}).resolve(FileConfig.DEFAULT_SRC_GEN_DIR).toString() ); @@ -124,9 +122,12 @@ private GeneratorResult doGenerate( ReportProgress reportProgress, CancelIndicator cancelIndicator ) { + var resource = getResource(uri); LFGeneratorContext context = new MainContext( - mustComplete ? Mode.LSP_SLOW : LFGeneratorContext.Mode.LSP_MEDIUM, cancelIndicator, reportProgress, new Properties(), - false, fileConfig -> new LanguageServerErrorReporter(fileConfig.resource.getContents().get(0)) + mustComplete ? Mode.LSP_SLOW : LFGeneratorContext.Mode.LSP_MEDIUM, + cancelIndicator, reportProgress, new Properties(), + resource, fileAccess, + fileConfig -> new LanguageServerErrorReporter(resource.getContents().get(0)) ); generator.generate(getResource(uri), fileAccess, context); return context.getResult(); diff --git a/org.lflang/src/org/lflang/generator/LFGenerator.java b/org.lflang/src/org/lflang/generator/LFGenerator.java index ac0c462c84..0c9a45dd7c 100644 --- a/org.lflang/src/org/lflang/generator/LFGenerator.java +++ b/org.lflang/src/org/lflang/generator/LFGenerator.java @@ -2,9 +2,7 @@ import java.io.IOException; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.nio.file.Path; -import java.util.Objects; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.generator.AbstractGenerator; @@ -16,7 +14,12 @@ import org.lflang.ErrorReporter; import org.lflang.FileConfig; import org.lflang.Target; +import org.lflang.federated.generator.FedASTUtils; +import org.lflang.federated.generator.FedFileConfig; +import org.lflang.federated.generator.FedGenerator; +import org.lflang.generator.c.CFileConfig; import org.lflang.generator.c.CGenerator; +import org.lflang.generator.python.PyFileConfig; import org.lflang.generator.python.PythonGenerator; import org.lflang.scoping.LFGlobalScopeProvider; @@ -51,31 +54,41 @@ public class LFGenerator extends AbstractGenerator { * @return A FileConfig object in Kotlin if the class can be found. * @throws IOException If the file config could not be created properly */ - private FileConfig createFileConfig(final Target target, - Resource resource, - IFileSystemAccess2 fsa, - LFGeneratorContext context) throws IOException { - Path srcGenBasePath = FileConfig.getSrcGenRoot(fsa); + public static FileConfig createFileConfig(Resource resource, Path srcGenBasePath, + boolean useHierarchicalBin) { + + final Target target = Target.fromDecl(ASTUtils.targetDecl(resource)); + assert target != null; + // Since our Eclipse Plugin uses code injection via guice, we need to // play a few tricks here so that FileConfig does not appear as an - // import. Instead we look the class up at runtime and instantiate it if + // import. Instead, we look the class up at runtime and instantiate it if // found. - switch (target) { - case CPP: - case Rust: - case TS: - String className = "org.lflang.generator." + target.packageName + "." + target.classNamePrefix + "FileConfig"; - try { - return (FileConfig) Class.forName(className) - .getDeclaredConstructor(Resource.class, Path.class, boolean.class) - .newInstance(resource, srcGenBasePath, context.useHierarchicalBin()); - } catch (InvocationTargetException e) { - throw new RuntimeException("Exception instantiating " + className, e.getTargetException()); - } catch (ReflectiveOperationException e) { - return new FileConfig(resource, srcGenBasePath, context.useHierarchicalBin()); + try { + if (FedASTUtils.findFederatedReactor(resource) != null) { + return new FedFileConfig(resource, srcGenBasePath, useHierarchicalBin); } - default: - return new FileConfig(resource, srcGenBasePath, context.useHierarchicalBin()); + switch (target) { + case CCPP: + case C: return new CFileConfig(resource, srcGenBasePath, useHierarchicalBin); + case Python: return new PyFileConfig(resource, srcGenBasePath, useHierarchicalBin); + case CPP: + case Rust: + case TS: + String className = "org.lflang.generator." + target.packageName + "." + target.classNamePrefix + "FileConfig"; + try { + return (FileConfig) Class.forName(className) + .getDeclaredConstructor(Resource.class, Path.class, boolean.class) + .newInstance(resource, srcGenBasePath, useHierarchicalBin); + } catch (ReflectiveOperationException e) { + throw new RuntimeException( + "Exception instantiating " + className, e.getCause()); + } + default: + throw new RuntimeException("Could not find FileConfig implementation for target " + target); + } + } catch (IOException e) { + throw new RuntimeException("Unable to create FileConfig object for target " + target + ": " + e.getStackTrace()); } } @@ -83,18 +96,18 @@ private FileConfig createFileConfig(final Target target, * Create a generator object for the given target. * Returns null if the generator could not be created. */ - private GeneratorBase createGenerator(Target target, FileConfig fileConfig, ErrorReporter errorReporter) { - switch (target) { - case C: return new CGenerator(fileConfig, errorReporter, false); - case CCPP: return new CGenerator(fileConfig, errorReporter, true); - case Python: return new PythonGenerator(fileConfig, errorReporter); - case CPP: - case TS: - case Rust: - return createKotlinBaseGenerator(target, fileConfig, errorReporter); - } - // If no case matched, then throw a runtime exception. - throw new RuntimeException("Unexpected target!"); + private GeneratorBase createGenerator(LFGeneratorContext context) { + final Target target = Target.fromDecl(ASTUtils.targetDecl(context.getFileConfig().resource)); + assert target != null; + return switch (target) { + case C -> new CGenerator(context, false); + case CCPP -> new CGenerator(context, true); + case Python -> new PythonGenerator(context); + case CPP, TS, Rust -> + createKotlinBaseGenerator(target, context); + // If no case matched, then throw a runtime exception. + default -> throw new RuntimeException("Unexpected target!"); + }; } @@ -110,27 +123,21 @@ private GeneratorBase createGenerator(Target target, FileConfig fileConfig, Erro * * @return A Kotlin Generator object if the class can be found */ - private GeneratorBase createKotlinBaseGenerator(Target target, FileConfig fileConfig, - ErrorReporter errorReporter) { + private GeneratorBase createKotlinBaseGenerator(Target target, LFGeneratorContext context) { // Since our Eclipse Plugin uses code injection via guice, we need to // play a few tricks here so that Kotlin FileConfig and - // Kotlin Generator do not appear as an import. Instead we look the + // Kotlin Generator do not appear as an import. Instead, we look the // class up at runtime and instantiate it if found. String classPrefix = "org.lflang.generator." + target.packageName + "." + target.classNamePrefix; try { Class generatorClass = Class.forName(classPrefix + "Generator"); - Class fileConfigClass = Class.forName(classPrefix + "FileConfig"); Constructor ctor = generatorClass - .getDeclaredConstructor(fileConfigClass, ErrorReporter.class, LFGlobalScopeProvider.class); - - return (GeneratorBase) ctor.newInstance(fileConfig, errorReporter, scopeProvider); + .getDeclaredConstructor(LFGeneratorContext.class, LFGlobalScopeProvider.class); - } catch (InvocationTargetException e) { - throw new RuntimeException("Exception instantiating " + classPrefix + "FileConfig", - e.getTargetException()); + return (GeneratorBase) ctor.newInstance(context, scopeProvider); } catch (ReflectiveOperationException e) { generatorErrorsOccurred = true; - errorReporter.reportError( + context.getErrorReporter().reportError( "The code generator for the " + target + " target could not be found. " + "This is likely because you built Epoch using " + "Eclipse. The " + target + " code generator is written in Kotlin " @@ -146,24 +153,33 @@ private GeneratorBase createKotlinBaseGenerator(Target target, FileConfig fileCo @Override public void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) { - final LFGeneratorContext lfContext = LFGeneratorContext.lfGeneratorContextOf(context, resource); - if (lfContext.getMode() == LFGeneratorContext.Mode.LSP_FAST) return; // The fastest way to generate code is to not generate any code. - final Target target = Target.fromDecl(ASTUtils.targetDecl(resource)); - assert target != null; - - FileConfig fileConfig; - try { - fileConfig = Objects.requireNonNull(createFileConfig(target, resource, fsa, lfContext)); - } catch (IOException e) { - throw new RuntimeIOException("Error during FileConfig instantiation", e); + final LFGeneratorContext lfContext; + if (context instanceof LFGeneratorContext) { + lfContext = (LFGeneratorContext)context; + } else { + lfContext = LFGeneratorContext.lfGeneratorContextOf(resource, fsa, context); } - final ErrorReporter errorReporter = lfContext.constructErrorReporter(fileConfig); - final GeneratorBase generator = createGenerator(target, fileConfig, errorReporter); - if (generator != null) { - generator.doGenerate(resource, lfContext); - generatorErrorsOccurred = generator.errorsOccurred(); + // The fastest way to generate code is to not generate any code. + if (lfContext.getMode() == LFGeneratorContext.Mode.LSP_FAST) return; + + if (FedASTUtils.findFederatedReactor(resource) != null) { + try { + generatorErrorsOccurred = (new FedGenerator(lfContext)).doGenerate(resource, lfContext); + } catch (IOException e) { + throw new RuntimeIOException("Error during federated code generation", e); + } + + } else { + + final GeneratorBase generator = createGenerator(lfContext); + + if (generator != null) { + generator.doGenerate(resource, lfContext); + generatorErrorsOccurred = generator.errorsOccurred(); + } } + final ErrorReporter errorReporter = lfContext.getErrorReporter(); if (errorReporter instanceof LanguageServerErrorReporter) { ((LanguageServerErrorReporter) errorReporter).publishDiagnostics(); } diff --git a/org.lflang/src/org/lflang/generator/LFGeneratorContext.java b/org.lflang/src/org/lflang/generator/LFGeneratorContext.java index 5a6e096a2f..c1cfd06b28 100644 --- a/org.lflang/src/org/lflang/generator/LFGeneratorContext.java +++ b/org.lflang/src/org/lflang/generator/LFGeneratorContext.java @@ -1,16 +1,19 @@ package org.lflang.generator; +import java.io.IOException; import java.nio.file.Path; -import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Properties; import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.generator.IFileSystemAccess2; import org.eclipse.xtext.generator.IGeneratorContext; +import org.eclipse.xtext.util.RuntimeIOException; import org.lflang.ErrorReporter; import org.lflang.FileConfig; -import org.lflang.util.LFCommand; +import org.lflang.TargetConfig; /** * An {@code LFGeneratorContext} is the context of a Lingua Franca build process. @@ -24,7 +27,7 @@ public interface LFGeneratorContext extends IGeneratorContext { /** * Enumeration of keys used to parameterize the build process. */ - public enum BuildParm { + enum BuildParm { BUILD_TYPE("The build type to use"), CLEAN("Clean before building."), EXTERNAL_RUNTIME_PATH("Specify an external runtime library to be used by the compiled binary."), @@ -87,17 +90,10 @@ enum Mode { Properties getArgs(); /** - * Return whether the bin directory should be hierarchical. - * @return whether the bin directory should be hierarchical + * Get the error reporter for this context; construct one if it hasn't been + * constructed yet. */ - boolean useHierarchicalBin(); - - /** - * Construct the appropriate error reporter for {@code fileConfig}. - * @param fileConfig The {@code FileConfig} used by a build process. - * @return the appropriate error reporter for {@code fileConfig} - */ - ErrorReporter constructErrorReporter(FileConfig fileConfig); + ErrorReporter getErrorReporter(); /** * Mark the code generation process performed in this @@ -116,6 +112,10 @@ enum Mode { */ GeneratorResult getResult(); + FileConfig getFileConfig(); + + TargetConfig getTargetConfig(); + /** * Report the progress of a build. * @param message A message for the LF programmer to read. @@ -126,50 +126,13 @@ enum Mode { /** * Conclude this build and record the result if necessary. * @param status The status of the result. - * @param execName The name of the executable produced by this code - * generation process, or {@code null} if no executable was produced. - * @param binPath The directory containing the executable (if applicable). - * @param fileConfig The {@code FileConfig} instance used by the build. - * @param codeMaps The generated files and their corresponding code maps. - * @param interpreter The interpreter needed to run the executable, if - * applicable. - */ - default void finish( - GeneratorResult.Status status, - String execName, - Path binPath, - FileConfig fileConfig, - Map codeMaps, - String interpreter - ) { - final boolean isWindows = GeneratorUtils.isHostWindows(); - if (execName != null && binPath != null) { - Path executable = binPath.resolve(execName + (isWindows && interpreter == null ? ".exe" : "")); - String relativeExecutable = fileConfig.srcPkgPath.relativize(executable).toString(); - LFCommand command = interpreter != null ? - LFCommand.get(interpreter, List.of(relativeExecutable), true, fileConfig.srcPkgPath) : - LFCommand.get(isWindows ? executable.toString() : relativeExecutable, List.of(), true, fileConfig.srcPkgPath); - finish(new GeneratorResult(status, executable, command, codeMaps)); - } else { - finish(new GeneratorResult(status, null, null, codeMaps)); - } - } - - /** - * Conclude this build and record the result if necessary. - * @param status The status of the result. - * @param execName The name of the executable produced by this code - * generation process, or {@code null} if no executable was produced. - * @param fileConfig The directory containing the executable (if applicable) * @param codeMaps The generated files and their corresponding code maps. */ default void finish( GeneratorResult.Status status, - String execName, - FileConfig fileConfig, Map codeMaps ) { - finish(status, execName, fileConfig.binPath, fileConfig, codeMaps, null); + finish(new GeneratorResult(status, this, codeMaps)); } /** @@ -185,17 +148,20 @@ default void unsuccessfulFinish() { /** * Return the {@code LFGeneratorContext} that best describes the given {@code context} when * building {@code Resource}. + * @param resource + * @param fsa * @param context The context of a Lingua Franca build process. - * @param resource The resource being built. * @return The {@code LFGeneratorContext} that best describes the given {@code context} when * building {@code Resource}. */ - static LFGeneratorContext lfGeneratorContextOf(IGeneratorContext context, Resource resource) { + static LFGeneratorContext lfGeneratorContextOf(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) { if (context instanceof LFGeneratorContext) return (LFGeneratorContext) context; + if (resource.getURI().isPlatform()) return new MainContext( - Mode.EPOCH, context.getCancelIndicator(), (m, p) -> {}, new Properties(), false, - EclipseErrorReporter::new + Mode.EPOCH, context.getCancelIndicator(), (m, p) -> {}, new Properties(), + resource, fsa, EclipseErrorReporter::new ); - return new MainContext(Mode.LSP_FAST, context.getCancelIndicator()); + + return new MainContext(Mode.LSP_FAST, resource, fsa, context.getCancelIndicator()); } } diff --git a/org.lflang/src/org/lflang/generator/LFResource.java b/org.lflang/src/org/lflang/generator/LFResource.java index 788130e612..bc59c418ee 100644 --- a/org.lflang/src/org/lflang/generator/LFResource.java +++ b/org.lflang/src/org/lflang/generator/LFResource.java @@ -13,7 +13,7 @@ */ public class LFResource { LFResource(Resource resource, FileConfig fileConfig, TargetConfig targetConfig) { - this.eResource = resource; + this.eResource = resource; // FIXME: this is redundant because fileConfig already has the resource. this.fileConfig = fileConfig; this.targetConfig = targetConfig; } diff --git a/org.lflang/src/org/lflang/generator/MainContext.java b/org.lflang/src/org/lflang/generator/MainContext.java index e55e144354..4f024d1d22 100644 --- a/org.lflang/src/org/lflang/generator/MainContext.java +++ b/org.lflang/src/org/lflang/generator/MainContext.java @@ -1,13 +1,19 @@ package org.lflang.generator; +import java.io.IOException; +import java.util.Objects; import java.util.Properties; import java.util.function.Function; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.generator.IFileSystemAccess2; import org.eclipse.xtext.util.CancelIndicator; +import org.eclipse.xtext.util.RuntimeIOException; import org.lflang.DefaultErrorReporter; import org.lflang.ErrorReporter; import org.lflang.FileConfig; +import org.lflang.TargetConfig; import org.lflang.generator.IntegratedBuilder.ReportProgress; /** @@ -25,14 +31,18 @@ public class MainContext implements LFGeneratorContext { private final CancelIndicator cancelIndicator; /** The {@code ReportProgress} function of {@code this}. */ private final ReportProgress reportProgress; + + private final FileConfig fileConfig; + /** Whether the requested build is required to be complete. */ private final Mode mode; + + private TargetConfig targetConfig; + /** The result of the code generation process. */ private GeneratorResult result = null; private final Properties args; - private final boolean hierarchicalBin; // FIXME: The interface would be simpler if this were part of {@code args}, and in addition, a potentially useful feature would be exposed to the user. - private final Function constructErrorReporter; - private boolean hasConstructedErrorReporter; + private final ErrorReporter errorReporter; /** * Initialize the context of a build process whose cancellation is @@ -41,10 +51,10 @@ public class MainContext implements LFGeneratorContext { * @param cancelIndicator The cancel indicator of the code generation * process to which this corresponds. */ - public MainContext(Mode mode, CancelIndicator cancelIndicator) { + public MainContext(Mode mode, Resource resource, IFileSystemAccess2 fsa, CancelIndicator cancelIndicator) { this( - mode, cancelIndicator, (message, completion) -> {}, new Properties(), false, - fileConfig -> new DefaultErrorReporter() + mode, cancelIndicator, (message, completion) -> {}, new Properties(), resource, fsa, + fg -> new DefaultErrorReporter() ); } @@ -58,8 +68,8 @@ public MainContext(Mode mode, CancelIndicator cancelIndicator) { * {@code this}. * @param args Any arguments that may be used to affect the product of the * build. - * @param hierarchicalBin Whether the bin directory should be structured - * hierarchically. + * @param resource ... + * @param fsa ... * @param constructErrorReporter A function that constructs the appropriate * error reporter for the given FileConfig. */ @@ -68,16 +78,25 @@ public MainContext( CancelIndicator cancelIndicator, ReportProgress reportProgress, Properties args, - boolean hierarchicalBin, + Resource resource, + IFileSystemAccess2 fsa, Function constructErrorReporter ) { this.mode = mode; this.cancelIndicator = cancelIndicator == null ? () -> false : cancelIndicator; this.reportProgress = reportProgress; this.args = args; - this.hierarchicalBin = hierarchicalBin; - this.constructErrorReporter = constructErrorReporter; - this.hasConstructedErrorReporter = false; + + try { + var useHierarchicalBin = args.containsKey("hierarchical-bin") && Boolean.parseBoolean(args.getProperty("hierarchical-bin")); + fileConfig = Objects.requireNonNull(LFGenerator.createFileConfig(resource, FileConfig.getSrcGenRoot(fsa), useHierarchicalBin)); + } catch (IOException e) { + throw new RuntimeIOException("Error during FileConfig instantiation", e); + } + + this.errorReporter = constructErrorReporter.apply(this.fileConfig); + + loadTargetConfig(); } @Override @@ -95,18 +114,10 @@ public Properties getArgs() { return args; } - @Override - public boolean useHierarchicalBin() { - return hierarchicalBin; - } @Override - public ErrorReporter constructErrorReporter(FileConfig fileConfig) { - if (hasConstructedErrorReporter) { - throw new IllegalStateException("Only one error reporter should be constructed for a given context."); - } - hasConstructedErrorReporter = true; - return constructErrorReporter.apply(fileConfig); + public ErrorReporter getErrorReporter() { + return errorReporter; } @Override @@ -121,8 +132,31 @@ public GeneratorResult getResult() { return result != null ? result : GeneratorResult.NOTHING; } + @Override + public FileConfig getFileConfig() { + return this.fileConfig; + } + + @Override + public TargetConfig getTargetConfig() { + return this.targetConfig; + } + @Override public void reportProgress(String message, int percentage) { reportProgress.apply(message, percentage); } + + /** + * Load the target configuration based on the contents of the resource. + * This is done automatically upon instantiation of the context, but + * in case the resource changes (e.g., due to an AST transformation), + * this method can be called to reload to ensure that the changes are + * reflected in the target configuration. + */ + public void loadTargetConfig() { + this.targetConfig = GeneratorUtils.getTargetConfig( + args, GeneratorUtils.findTarget(fileConfig.resource), errorReporter + ); + } } diff --git a/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java b/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java index 1738322ef7..116cbf3046 100644 --- a/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java +++ b/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java @@ -30,8 +30,10 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import org.lflang.generator.ReactionInstance.Runtime; +import org.lflang.generator.c.CUtil; import org.lflang.graph.PrecedenceGraph; import org.lflang.lf.Variable; @@ -83,6 +85,10 @@ public ReactionInstanceGraph(ReactorInstance main) { public void rebuild() { this.clear(); addNodesAndEdges(main); + + // FIXME: Use {@link TargetProperty#EXPORT_DEPENDENCY_GRAPH}. + // System.out.println(toDOT()); + // Assign a level to each reaction. // If there are cycles present in the graph, it will be detected here. assignLevels(); @@ -367,4 +373,68 @@ private void adjustNumReactionsPerLevel(int level, int valueToAdd) { numReactionsPerLevel.add(valueToAdd); } } + + /** + * Return the DOT (GraphViz) representation of the graph. + */ + @Override + public String toDOT() { + var dotRepresentation = new CodeBuilder(); + var edges = new StringBuilder(); + + // Start the digraph with a left-write rank + dotRepresentation.pr( + """ + digraph { + rankdir=LF; + graph [compound=True, rank=LR, rankdir=LR]; + node [fontname=Times, shape=rectangle]; + edge [fontname=Times]; + """); + + var nodes = nodes(); + // Group nodes by levels + var groupedNodes = + nodes.stream() + .collect( + Collectors.groupingBy(it -> it.level) + ); + + dotRepresentation.indent(); + // For each level + for (var level : groupedNodes.keySet()) { + // Create a subgraph + dotRepresentation.pr("subgraph cluster_level_" + level + " {"); + dotRepresentation.pr(" graph [compound=True, label = \"level " + level + "\"];"); + + // Get the nodes at the current level + var currentLevelNodes = groupedNodes.get(level); + for (var node: currentLevelNodes) { + // Draw the node + var label = CUtil.getName(node.getReaction().getParent().reactorDeclaration) + "." + node.getReaction().getName(); + // Need a positive number to name the nodes in GraphViz + var labelHashCode = label.hashCode() & 0xfffffff; + dotRepresentation.pr(" node_" + labelHashCode + " [label=\""+ label +"\"];"); + + // Draw the edges + var downstreamNodes = getDownstreamAdjacentNodes(node); + for (var downstreamNode: downstreamNodes) { + var downstreamLabel = CUtil.getName(downstreamNode.getReaction().getParent().reactorDeclaration) + "." + downstreamNode.getReaction().getName(); + edges.append(" node_" + labelHashCode + " -> node_" + + (downstreamLabel.hashCode() & 0xfffffff) + ";\n" + ); + } + } + // Close the subgraph + dotRepresentation.pr("}"); + } + dotRepresentation.unindent(); + // Add the edges to the definition of the graph at the bottom + dotRepresentation.pr(edges); + // Close the digraph + dotRepresentation.pr("}"); + + // Return the DOT representation + return dotRepresentation.toString(); + } } diff --git a/org.lflang/src/org/lflang/generator/ReactorInstance.java b/org.lflang/src/org/lflang/generator/ReactorInstance.java index 154a672202..305ef420c4 100644 --- a/org.lflang/src/org/lflang/generator/ReactorInstance.java +++ b/org.lflang/src/org/lflang/generator/ReactorInstance.java @@ -35,7 +35,10 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import java.util.Map; import java.util.Set; +import org.eclipse.emf.ecore.util.EcoreUtil; + import org.lflang.ASTUtils; +import org.lflang.AttributeUtils; import org.lflang.ErrorReporter; import org.lflang.TimeValue; import org.lflang.generator.TriggerInstance.BuiltinTriggerVariable; @@ -46,6 +49,7 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.lf.Expression; import org.lflang.lf.Input; import org.lflang.lf.Instantiation; +import org.lflang.lf.LfFactory; import org.lflang.lf.Mode; import org.lflang.lf.Output; import org.lflang.lf.Parameter; @@ -88,7 +92,7 @@ public class ReactorInstance extends NamedInstance { * @param reporter The error reporter. */ public ReactorInstance(Reactor reactor, ErrorReporter reporter) { - this(ASTUtils.createInstantiation(reactor), null, reporter, -1, null); + this(ASTUtils.createInstantiation(reactor), null, reporter, -1); } /** @@ -99,17 +103,7 @@ public ReactorInstance(Reactor reactor, ErrorReporter reporter) { * @param desiredDepth The depth to which to go, or -1 to construct the full hierarchy. */ public ReactorInstance(Reactor reactor, ErrorReporter reporter, int desiredDepth) { - this(ASTUtils.createInstantiation(reactor), null, reporter, desiredDepth, null); - } - - /** - * Create a new instantiation hierarchy that starts with the given reactor. - * @param reactor The top-level reactor. - * @param reporter The error reporter. - * @param unorderedReactions A list of reactions that should be treated as unordered. - */ - public ReactorInstance(Reactor reactor, ErrorReporter reporter, Set unorderedReactions) { - this(ASTUtils.createInstantiation(reactor), null, reporter, -1, unorderedReactions); + this(ASTUtils.createInstantiation(reactor), null, reporter, desiredDepth); } /** @@ -121,7 +115,7 @@ public ReactorInstance(Reactor reactor, ErrorReporter reporter, Set un * @param reporter The error reporter. */ public ReactorInstance(Reactor reactor, ReactorInstance parent, ErrorReporter reporter) { - this(ASTUtils.createInstantiation(reactor), parent, reporter, -1, null); + this(ASTUtils.createInstantiation(reactor), parent, reporter, -1); } ////////////////////////////////////////////////////// @@ -314,6 +308,20 @@ public String getName() { return this.definition.getName(); } + /** + * @see NamedInstance#uniqueID() + * + * Append `_main` to the name of the main reactor to allow instantiations + * within that reactor to have the same name. + */ + @Override + public String uniqueID() { + if (this.isMainOrFederated()) { + return super.uniqueID() + "_main"; + } + return super.uniqueID(); + } + /** * Return the specified output by name or null if there is no such output. * @param name The output name. @@ -721,6 +729,9 @@ protected void createReactionInstances() { // Check for startup and shutdown triggers. for (Reaction reaction : reactions) { + if (AttributeUtils.isUnordered(reaction)) { + unorderedReactions.add(reaction); + } // Create the reaction instance. var reactionInstance = new ReactionInstance(reaction, this, unorderedReactions.contains(reaction), count++); @@ -753,24 +764,17 @@ protected TriggerInstance getOrCreateBuiltinTrigger(BuiltinT * @param parent The parent, or null for the main rector. * @param reporter An error reporter. * @param desiredDepth The depth to which to expand the hierarchy. - * @param unorderedReactions A list of reactions that should be treated as unordered. - * It can be passed as null. */ private ReactorInstance( Instantiation definition, ReactorInstance parent, ErrorReporter reporter, - int desiredDepth, - Set unorderedReactions) { + int desiredDepth) { super(definition, parent); this.reporter = reporter; this.reactorDeclaration = definition.getReactorClass(); this.reactorDefinition = ASTUtils.toDefinition(reactorDeclaration); - if (unorderedReactions != null) { - this.unorderedReactions = unorderedReactions; - } - // check for recursive instantiation var currentParent = parent; var foundSelfAsParent = false; @@ -823,8 +827,7 @@ private ReactorInstance( child, this, reporter, - desiredDepth, - this.unorderedReactions + desiredDepth ); this.children.add(childInstance); } diff --git a/org.lflang/src/org/lflang/generator/SubContext.java b/org.lflang/src/org/lflang/generator/SubContext.java index 9c09b918fe..5e7a981ab8 100644 --- a/org.lflang/src/org/lflang/generator/SubContext.java +++ b/org.lflang/src/org/lflang/generator/SubContext.java @@ -1,11 +1,13 @@ package org.lflang.generator; +import java.io.File; import java.util.Properties; import org.eclipse.xtext.util.CancelIndicator; import org.lflang.ErrorReporter; import org.lflang.FileConfig; +import org.lflang.TargetConfig; /** * A {@code SubContext} is the context of a process within a build process. For example, @@ -19,6 +21,9 @@ public class SubContext implements LFGeneratorContext { private final LFGeneratorContext containingContext; private final int startPercentProgress; private final int endPercentProgress; + private GeneratorResult result = null; + + protected ErrorReporter errorReporter; /** * Initializes the context within {@code containingContext} of the process that extends from @@ -51,25 +56,28 @@ public Properties getArgs() { } @Override - public boolean useHierarchicalBin() { - return containingContext.useHierarchicalBin(); + public ErrorReporter getErrorReporter() { + return containingContext.getErrorReporter(); } @Override - public ErrorReporter constructErrorReporter(FileConfig fileConfig) { - throw new UnsupportedOperationException( - "Nested contexts should use the error reporters constructed by their containing contexts." - ); + public void finish(GeneratorResult result) { + this.result = result; } @Override - public void finish(GeneratorResult result) { - // Do nothing. A build process is not finished until the outermost containing context is finished. + public GeneratorResult getResult() { + return result; } @Override - public GeneratorResult getResult() { - throw new UnsupportedOperationException("Only the outermost context can have a final result."); + public FileConfig getFileConfig() { + return containingContext.getFileConfig(); + } + + @Override + public TargetConfig getTargetConfig() { + return containingContext.getTargetConfig(); } @Override diff --git a/org.lflang/src/org/lflang/generator/c/CActionGenerator.java b/org.lflang/src/org/lflang/generator/c/CActionGenerator.java index d24af804ae..a5c7e2f5d1 100644 --- a/org.lflang/src/org/lflang/generator/c/CActionGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CActionGenerator.java @@ -4,7 +4,7 @@ import java.util.ArrayList; import org.lflang.ASTUtils; import org.lflang.Target; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FederateInstance; import org.lflang.generator.ActionInstance; import org.lflang.generator.CodeBuilder; import org.lflang.generator.GeneratorBase; @@ -30,17 +30,13 @@ public class CActionGenerator { * For each action of the specified reactor instance, generate initialization code * for the offset and period fields. * @param instance The reactor. - * @param currentFederate The federate we are */ public static String generateInitializers( - ReactorInstance instance, - FederateInstance currentFederate + ReactorInstance instance ) { List code = new ArrayList<>(); for (ActionInstance action : instance.actions) { - if (currentFederate.contains(action.getDefinition()) && - !action.isShutdown() - ) { + if (!action.isShutdown()) { var triggerStructName = CUtil.reactorRef(action.getParent()) + "->_lf__" + action.getName(); var minDelay = action.getMinDelay(); var minSpacing = action.getMinSpacing(); @@ -98,24 +94,20 @@ public static String generateTokenInitializer( * * @param reactor The reactor to generate declarations for * @param decl The reactor's declaration - * @param currentFederate The federate that is being generated * @param body The content of the self struct * @param constructorCode The constructor code of the reactor */ public static void generateDeclarations( Reactor reactor, ReactorDecl decl, - FederateInstance currentFederate, CodeBuilder body, CodeBuilder constructorCode ) { for (Action action : ASTUtils.allActions(reactor)) { - if (currentFederate.contains(action)) { - var actionName = action.getName(); - body.pr(action, CGenerator.variableStructType(action, decl)+" _lf_"+actionName+";"); - // Initialize the trigger pointer in the action. - constructorCode.pr(action, "self->_lf_"+actionName+".trigger = &self->_lf__"+actionName+";"); - } + var actionName = action.getName(); + body.pr(action, CGenerator.variableStructType(action, decl)+" _lf_"+actionName+";"); + // Initialize the trigger pointer in the action. + constructorCode.pr(action, "self->_lf_"+actionName+".trigger = &self->_lf__"+actionName+";"); } } diff --git a/org.lflang/src/org/lflang/generator/c/CConstructorGenerator.java b/org.lflang/src/org/lflang/generator/c/CConstructorGenerator.java index b769b7dd29..0cc2199cae 100644 --- a/org.lflang/src/org/lflang/generator/c/CConstructorGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CConstructorGenerator.java @@ -1,6 +1,6 @@ package org.lflang.generator.c; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FederateInstance; import org.lflang.generator.CodeBuilder; import org.lflang.lf.ReactorDecl; @@ -12,13 +12,11 @@ public class CConstructorGenerator { /** * Generate a constructor for the specified reactor in the specified federate. * @param reactor The parsed reactor data structure. - * @param federate A federate name, or null to unconditionally generate. * @param constructorCode Lines of code previously generated that need to * go into the constructor. */ public static String generateConstructor( ReactorDecl reactor, - FederateInstance federate, String constructorCode ) { var structType = CUtil.selfType(reactor); diff --git a/org.lflang/src/org/lflang/generator/c/CDockerGenerator.java b/org.lflang/src/org/lflang/generator/c/CDockerGenerator.java index 6c220f4be8..ae9058dcbb 100644 --- a/org.lflang/src/org/lflang/generator/c/CDockerGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CDockerGenerator.java @@ -3,9 +3,10 @@ import java.util.stream.Collectors; import org.eclipse.xtext.xbase.lib.IterableExtensions; -import org.lflang.FileConfig; -import org.lflang.TargetConfig; -import org.lflang.generator.DockerGeneratorBase; + +import org.lflang.Target; +import org.lflang.generator.DockerGenerator; +import org.lflang.generator.LFGeneratorContext; import org.lflang.util.StringUtil; /** @@ -13,75 +14,34 @@ * * @author Hou Seng Wong */ -public class CDockerGenerator extends DockerGeneratorBase { +public class CDockerGenerator extends DockerGenerator { private static final String DEFAULT_BASE_IMAGE = "alpine:latest"; - private final boolean CCppMode; - private final TargetConfig targetConfig; - - /** - * The interface for data from the C code generator. - * - * @param lfModuleName The name of the LF module in CGenerator. - * Typically, this is "fileConfig.name + _ + federate.name" - * in federated execution and "fileConfig.name" in non-federated - * execution. - * @param federateName The value of "currentFederate.name" in CGenerator. - * @param fileConfig The value of "fileConfig" in CGenerator. - */ - public record CGeneratorData( - String lfModuleName, String federateName, FileConfig fileConfig - ) implements GeneratorData {} - - public CDockerGenerator(boolean isFederated, boolean CCppMode, TargetConfig targetConfig) { - super(isFederated); - this.CCppMode = CCppMode; - this.targetConfig = targetConfig; - } /** - * Translate data from the code generator to a map. + * The constructor for the base docker file generation class. * - * @return data from the code generator put in a Map. + * @param context The context of the code generator. */ - public CGeneratorData fromData( - String lfModuleName, - String federateName, - FileConfig fileConfig - ) { - return new CGeneratorData(lfModuleName, federateName, fileConfig); + public CDockerGenerator(LFGeneratorContext context) { + super(context); } - /** - * Translate data from the code generator to docker data as - * specified in the DockerData class. - * - * @param generatorData Data from the code generator. - * @return docker data as specified in the DockerData class - */ - @Override - protected DockerData generateDockerData(GeneratorData generatorData) { - CGeneratorData cGeneratorData = (CGeneratorData) generatorData; - var lfModuleName = cGeneratorData.lfModuleName(); - var federateName = cGeneratorData.federateName(); - var fileConfig = cGeneratorData.fileConfig(); - var dockerFilePath = fileConfig.getSrcGenPath().resolve(lfModuleName + ".Dockerfile"); - var dockerFileContent = generateDockerFileContent(cGeneratorData); - var dockerBuildContext = isFederated ? federateName : "."; - return new DockerData(dockerFilePath, dockerFileContent,dockerBuildContext); - } /** * Generate the contents of the docker file. - * - * @param generatorData Data from the code generator. */ - protected String generateDockerFileContent(CGeneratorData generatorData) { - var lfModuleName = generatorData.lfModuleName(); - var compileCommand = IterableExtensions.isNullOrEmpty(targetConfig.buildCommands) ? + @Override + protected String generateDockerFileContent() { + var lfModuleName = context.getFileConfig().name; + var config = context.getTargetConfig(); + var compileCommand = IterableExtensions.isNullOrEmpty(config.buildCommands) ? generateDefaultCompileCommand() : - StringUtil.joinObjects(targetConfig.buildCommands, " "); - var compiler = CCppMode ? "g++" : "gcc"; - var baseImage = targetConfig.dockerOptions.from == null ? DEFAULT_BASE_IMAGE : targetConfig.dockerOptions.from; + StringUtil.joinObjects(config.buildCommands, " "); + var compiler = config.target == Target.CCPP ? "g++" : "gcc"; + var baseImage = DEFAULT_BASE_IMAGE; + if (config.dockerOptions != null && config.dockerOptions.from != null) { + baseImage = config.dockerOptions.from; + } return String.join("\n", "# For instructions, see: https://www.lf-lang.org/docs/handbook/containerized-execution", "FROM "+baseImage+" AS builder", @@ -106,7 +66,7 @@ protected String generateDefaultCompileCommand() { return String.join("\n", "RUN set -ex && \\", "mkdir bin && \\", - "cmake " + CCompiler.cmakeCompileDefinitions(targetConfig) + "cmake " + CCompiler.cmakeCompileDefinitions(context.getTargetConfig()) .collect(Collectors.joining(" ")) + " -S src-gen -B bin && \\", "cd bin && \\", diff --git a/org.lflang/src/org/lflang/generator/c/CFederateGenerator.java b/org.lflang/src/org/lflang/generator/c/CFederateGenerator.java deleted file mode 100644 index 68e81e28ee..0000000000 --- a/org.lflang/src/org/lflang/generator/c/CFederateGenerator.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.lflang.generator.c; - -import org.lflang.ASTUtils; -import org.lflang.federated.FederateInstance; -import org.lflang.generator.CodeBuilder; -import org.lflang.generator.GeneratorBase; -import org.lflang.lf.Expression; -import org.lflang.lf.ParameterReference; - -/** - * Generate code for federate related functionality - * - * @author Soroush Bateni - * @author Hou Seng Wong - */ -public class CFederateGenerator { - /** - * Generate code that sends the neighbor structure message to the RTI. - * @see MSG_TYPE_NEIGHBOR_STRUCTURE in net_common.h - * - * @param federate The federate that is sending its neighbor structure - */ - public static String generateFederateNeighborStructure(FederateInstance federate) { - var code = new CodeBuilder(); - code.pr(String.join("\n", - "/**", - "* Generated function that sends information about connections between this federate and", - "* other federates where messages are routed through the RTI. Currently, this", - "* only includes logical connections when the coordination is centralized. This", - "* information is needed for the RTI to perform the centralized coordination.", - "* @see MSG_TYPE_NEIGHBOR_STRUCTURE in net_common.h", - "*/", - "void send_neighbor_structure_to_RTI(int rti_socket) {" - )); - code.indent(); - // Initialize the array of information about the federate's immediate upstream - // and downstream relayed (through the RTI) logical connections, to send to the - // RTI. - code.pr(String.join("\n", - "interval_t candidate_tmp;", - "size_t buffer_size = 1 + 8 + ", - " "+federate.dependsOn.keySet().size()+" * ( sizeof(uint16_t) + sizeof(int64_t) ) +", - " "+federate.sendsTo.keySet().size()+" * sizeof(uint16_t);", - "unsigned char buffer_to_send[buffer_size];", - "", - "size_t message_head = 0;", - "buffer_to_send[message_head] = MSG_TYPE_NEIGHBOR_STRUCTURE;", - "message_head++;", - "encode_int32((int32_t)"+federate.dependsOn.keySet().size()+", &(buffer_to_send[message_head]));", - "message_head+=sizeof(int32_t);", - "encode_int32((int32_t)"+federate.sendsTo.keySet().size()+", &(buffer_to_send[message_head]));", - "message_head+=sizeof(int32_t);" - )); - - if (!federate.dependsOn.keySet().isEmpty()) { - // Next, populate these arrays. - // Find the minimum delay in the process. - // FIXME: Zero delay is not really the same as a microstep delay. - for (FederateInstance upstreamFederate : federate.dependsOn.keySet()) { - code.pr(String.join("\n", - "encode_uint16((uint16_t)"+upstreamFederate.id+", &(buffer_to_send[message_head]));", - "message_head += sizeof(uint16_t);" - )); - // The minimum delay calculation needs to be made in the C code because it - // may depend on parameter values. - // FIXME: These would have to be top-level parameters, which don't really - // have any support yet. Ideally, they could be overridden on the command line. - // When that is done, they will need to be in scope here. - var delays = federate.dependsOn.get(upstreamFederate); - if (delays != null) { - // There is at least one delay, so find the minimum. - // If there is no delay at all, this is encoded as NEVER. - code.pr("candidate_tmp = FOREVER;"); - for (Expression delay : delays) { - if (delay == null) { - // Use NEVER to encode no delay at all. - code.pr("candidate_tmp = NEVER;"); - } else { - var delayTime = GeneratorBase.getTargetTime(delay); - if (delay instanceof ParameterReference) { - // The delay is given as a parameter reference. Find its value. - final var param = ((ParameterReference)delay).getParameter(); - delayTime = GeneratorBase.timeInTargetLanguage(ASTUtils.getDefaultAsTimeValue(param)); - } - code.pr(String.join("\n", - "if ("+delayTime+" < candidate_tmp) {", - " candidate_tmp = "+delayTime+";", - "}" - )); - } - } - code.pr(String.join("\n", - "encode_int64((int64_t)candidate_tmp, &(buffer_to_send[message_head]));", - "message_head += sizeof(int64_t);" - )); - } else { - // Use NEVER to encode no delay at all. - code.pr(String.join("\n", - "encode_int64(NEVER, &(buffer_to_send[message_head]));", - "message_head += sizeof(int64_t);" - )); - } - } - } - - // Next, set up the downstream array. - if (!federate.sendsTo.keySet().isEmpty()) { - // Next, populate the array. - // Find the minimum delay in the process. - // FIXME: Zero delay is not really the same as a microstep delay. - for (FederateInstance downstreamFederate : federate.sendsTo.keySet()) { - code.pr(String.join("\n", - "encode_uint16("+downstreamFederate.id+", &(buffer_to_send[message_head]));", - "message_head += sizeof(uint16_t);" - )); - } - } - code.pr(String.join("\n", - "write_to_socket_errexit(", - " rti_socket, ", - " buffer_size,", - " buffer_to_send,", - " \"Failed to send the neighbor structure message to the RTI.\"", - ");" - )); - code.unindent(); - code.pr("}"); - return code.toString(); - } -} diff --git a/org.lflang/src/org/lflang/generator/c/CFileConfig.java b/org.lflang/src/org/lflang/generator/c/CFileConfig.java new file mode 100644 index 0000000000..8bd70b74c3 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/c/CFileConfig.java @@ -0,0 +1,19 @@ +package org.lflang.generator.c; + +import java.io.IOException; +import java.nio.file.Path; + +import org.eclipse.emf.ecore.resource.Resource; + +import org.lflang.FileConfig; + +public class CFileConfig extends FileConfig { + public CFileConfig(Resource resource, Path srcGenBasePath, boolean useHierarchicalBin) throws IOException { + super(resource, srcGenBasePath, useHierarchicalBin); + } +} + + + + + diff --git a/org.lflang/src/org/lflang/generator/c/CGenerator.java b/org.lflang/src/org/lflang/generator/c/CGenerator.java index ebf5789b83..c763e9969a 100644 --- a/org.lflang/src/org/lflang/generator/c/CGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CGenerator.java @@ -41,56 +41,48 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.xtext.util.CancelIndicator; import org.eclipse.xtext.xbase.lib.Exceptions; import org.eclipse.xtext.xbase.lib.IterableExtensions; import org.eclipse.xtext.xbase.lib.StringExtensions; + import org.lflang.ASTUtils; -import org.lflang.ErrorReporter; +import org.lflang.generator.DockerComposeGenerator; import org.lflang.FileConfig; -import org.lflang.InferredType; import org.lflang.Target; import org.lflang.TargetConfig; import org.lflang.TargetProperty; -import org.lflang.TargetProperty.ClockSyncMode; -import org.lflang.TargetProperty.CoordinationType; import org.lflang.TargetProperty.Platform; -import org.lflang.TimeValue; + +import org.lflang.federated.extensions.CExtensionUtils; + import org.lflang.ast.AfterDelayTransformation; -import org.lflang.federated.FedFileConfig; -import org.lflang.federated.FederateInstance; -import org.lflang.federated.launcher.FedCLauncher; -import org.lflang.federated.serialization.FedROS2CPPSerialization; -import org.lflang.federated.serialization.SupportedSerializers; + import org.lflang.generator.ActionInstance; import org.lflang.generator.CodeBuilder; +import org.lflang.generator.DockerGenerator; import org.lflang.generator.GeneratorBase; import org.lflang.generator.GeneratorResult; import org.lflang.generator.GeneratorUtils; + import org.lflang.generator.DelayBodyGenerator; -import org.lflang.generator.IntegratedBuilder; + import org.lflang.generator.LFGeneratorContext; import org.lflang.generator.LFResource; import org.lflang.generator.ParameterInstance; import org.lflang.generator.PortInstance; import org.lflang.generator.ReactionInstance; import org.lflang.generator.ReactorInstance; -import org.lflang.generator.SubContext; import org.lflang.generator.TargetTypes; import org.lflang.generator.TimerInstance; import org.lflang.generator.TriggerInstance; import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; -import org.lflang.lf.Expression; import org.lflang.lf.Input; import org.lflang.lf.Instantiation; import org.lflang.lf.Mode; @@ -102,7 +94,6 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.lf.Reactor; import org.lflang.lf.ReactorDecl; import org.lflang.lf.StateVar; -import org.lflang.lf.VarRef; import org.lflang.lf.Variable; import org.lflang.util.FileUtil; @@ -348,12 +339,11 @@ public class CGenerator extends GeneratorBase { /** The main place to put generated code. */ protected CodeBuilder code = new CodeBuilder(); - /** The current federate for which we are generating code. */ - protected FederateInstance currentFederate = null; - /** Place to collect code to initialize the trigger objects for all reactor instances. */ protected CodeBuilder initializeTriggerObjects = new CodeBuilder(); + protected final CFileConfig fileConfig; + /** * Count of the number of is_present fields of the self struct that * need to be reinitialized in _lf_start_time_step(). @@ -389,14 +379,14 @@ public class CGenerator extends GeneratorBase { private final CCmakeGenerator cmakeGenerator; protected CGenerator( - FileConfig fileConfig, - ErrorReporter errorReporter, + LFGeneratorContext context, boolean CCppMode, CTypes types, CCmakeGenerator cmakeGenerator, DelayBodyGenerator delayBodyGenerator ) { - super(fileConfig, errorReporter); + super(context); + this.fileConfig = (CFileConfig) context.getFileConfig(); this.CCppMode = CCppMode; this.types = types; this.cmakeGenerator = cmakeGenerator; @@ -405,44 +395,16 @@ protected CGenerator( registerTransformation(new AfterDelayTransformation(delayBodyGenerator, types, fileConfig.resource)); } - public CGenerator(FileConfig fileConfig, ErrorReporter errorReporter, boolean CCppMode, CTypes types) { + public CGenerator(LFGeneratorContext context, boolean ccppMode) { this( - fileConfig, - errorReporter, - CCppMode, - types, - new CCmakeGenerator(fileConfig, List.of()), - new CDelayBodyGenerator(types) + context, + ccppMode, + new CTypes(context.getErrorReporter()), + new CCmakeGenerator(context.getFileConfig(), List.of()), + new CDelayBodyGenerator(new CTypes(context.getErrorReporter())) ); } - public CGenerator(FileConfig fileConfig, ErrorReporter errorReporter, boolean CCppMode) { - this(fileConfig, errorReporter, CCppMode, new CTypes(errorReporter)); - } - - //////////////////////////////////////////// - //// Public methods - /** - * Set C-specific default target configurations if needed. - */ - public void setCSpecificDefaults() { - if (isFederated) { - // Add compile definitions for federated execution - targetConfig.compileDefinitions.put("FEDERATED", ""); - if(targetConfig.auth) { - // The federates are authenticated before joining federation. - targetConfig.compileDefinitions.put("FEDERATED_AUTHENTICATED", ""); - } - if (targetConfig.coordination == CoordinationType.CENTRALIZED) { - // The coordination is centralized. - targetConfig.compileDefinitions.put("FEDERATED_CENTRALIZED", ""); - } else if (targetConfig.coordination == CoordinationType.DECENTRALIZED) { - // The coordination is decentralized - targetConfig.compileDefinitions.put("FEDERATED_DECENTRALIZED", ""); - } - } - } - /** * Look for physical actions in all resources. * If found, set threads to be at least one to allow asynchronous schedule calls. @@ -475,14 +437,6 @@ public void accommodatePhysicalActionsIfPresent() { */ protected boolean isOSCompatible() { if (GeneratorUtils.isHostWindows()) { - if (isFederated) { - errorReporter.reportError( - "Federated LF programs with a C target are currently not supported on Windows. " + - "Exiting code generation." - ); - // Return to avoid compiler errors - return false; - } if (CCppMode) { errorReporter.reportError( "LF programs with a CCpp target are currently not supported on Windows. " + @@ -514,52 +468,18 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { // Perform set up that does not generate code setUpGeneralParameters(); - var commonCode = new CodeBuilder(code); - FileUtil.createDirectoryIfDoesNotExist(fileConfig.getSrcGenPath().toFile()); FileUtil.createDirectoryIfDoesNotExist(fileConfig.binPath.toFile()); + handleProtoFiles(); - // Docker related paths - CDockerGenerator dockerGenerator = getDockerGenerator(); + var lfModuleName = fileConfig.name; + generateCodeFor(lfModuleName); - // Keep a separate file config for each federate - var oldFileConfig = fileConfig; - var numOfCompileThreads = Math.min(6, - Math.min( - Math.max(federates.size(), 1), - Runtime.getRuntime().availableProcessors() - ) - ); - var compileThreadPool = Executors.newFixedThreadPool(numOfCompileThreads); - System.out.println("******** Using "+numOfCompileThreads+" threads to compile the program."); - LFGeneratorContext generatingContext = new SubContext( - context, IntegratedBuilder.VALIDATED_PERCENT_PROGRESS, IntegratedBuilder.GENERATED_PERCENT_PROGRESS - ); - var federateCount = 0; - for (FederateInstance federate : federates) { - var lfModuleName = isFederated ? fileConfig.name + "_" + federate.name : fileConfig.name; - setUpFederateSpecificParameters(federate, commonCode); - if (isFederated) { - // If federated, append the federate name to the file name. - // Only generate one output if there is no federation. - try { - fileConfig = new FedFileConfig(fileConfig, federate.name); - } catch (IOException e) { - //noinspection ThrowableNotThrown,ResultOfMethodCallIgnored - Exceptions.sneakyThrow(e); - } - } - generateCodeForCurrentFederate(lfModuleName); + // Derive target filename from the .lf filename. + var cFilename = CCompiler.getTargetFileName(lfModuleName, this.CCppMode); + var targetFile = fileConfig.getSrcGenPath() + File.separator + cFilename; - // Derive target filename from the .lf filename. - var cFilename = CCompiler.getTargetFileName(lfModuleName, this.CCppMode); - var targetFile = fileConfig.getSrcGenPath() + File.separator + cFilename; - try { - if (isFederated) { - // Need to copy user files again since the source structure changes - // for federated programs. - copyUserFiles(this.targetConfig, this.fileConfig); - } + try { // Copy the core lib FileUtil.copyDirectoryFromClassPath( @@ -570,30 +490,29 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { // Copy the C target files copyTargetFiles(); - // If we are running an Arduino Target, need to copy over the Arduino-CMake files. - if (targetConfig.platformOptions.platform == Platform.ARDUINO) { - FileUtil.copyDirectoryFromClassPath( - "/lib/platform/arduino/Arduino-CMake-Toolchain/Arduino", - fileConfig.getSrcGenPath().resolve("toolchain/Arduino"), - false - ); - FileUtil.copyDirectoryFromClassPath( - "/lib/platform/arduino/Arduino-CMake-Toolchain/Platform", - fileConfig.getSrcGenPath().resolve("toolchain/Platform"), - false - ); - FileUtil.copyFileFromClassPath( - "/lib/platform/arduino/Arduino-CMake-Toolchain/Arduino-toolchain.cmake", - fileConfig.getSrcGenPath().resolve("toolchain/Arduino-toolchain.cmake"), - true - ); + // If we are running an Arduino Target, need to copy over the Arduino-CMake files. + if (targetConfig.platformOptions.platform == Platform.ARDUINO) { + FileUtil.copyDirectoryFromClassPath( + "/lib/platform/arduino/Arduino-CMake-Toolchain/Arduino", + fileConfig.getSrcGenPath().resolve("toolchain/Arduino"), + false + ); + FileUtil.copyDirectoryFromClassPath( + "/lib/platform/arduino/Arduino-CMake-Toolchain/Platform", + fileConfig.getSrcGenPath().resolve("toolchain/Platform"), + false + ); + FileUtil.copyFileFromClassPath( + "/lib/platform/arduino/Arduino-CMake-Toolchain/Arduino-toolchain.cmake", + fileConfig.getSrcGenPath().resolve("toolchain/Arduino-toolchain.cmake"), + true + ); + StringBuilder s = new StringBuilder(); + s.append("set(ARDUINO_BOARD \""); + s.append("\")"); + FileUtil.writeToFile(s.toString(), fileConfig.getSrcGenPath().resolve("toolchain/BoardOptions.cmake")); + } - StringBuilder s = new StringBuilder(); - s.append("set(ARDUINO_BOARD \""); - s.append("\")"); - FileUtil.writeToFile(s.toString(), - fileConfig.getSrcGenPath().resolve("toolchain/BoardOptions.cmake")); - } // If Zephyr target then copy default config and board files // for Zephyr support @@ -625,10 +544,15 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { // Create docker file. if (targetConfig.dockerOptions != null && mainDef != null) { - dockerGenerator.addFile( - dockerGenerator.fromData(lfModuleName, federate.name, fileConfig)); + try { + var dockerData = getDockerGenerator(context).generateDockerData(); + dockerData.writeDockerFile(); + (new DockerComposeGenerator(context)).writeDockerComposeFile(List.of(dockerData)); + } catch (IOException e) { + throw new RuntimeException("Error while writing Docker files", e); + } } - // If cmake is requested, generate the CMakeLists.txt + var cmakeFile = fileConfig.getSrcGenPath() + File.separator + "CMakeLists.txt"; var cmakeCode = cmakeGenerator.generateCMakeCode( List.of(cFilename), @@ -646,101 +570,65 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { Exceptions.sneakyThrow(e); } - // Dump the additional compile definitions to a file to keep the generated project - // self contained. In this way, third-party build tools like PlatformIO, west, arduino-cli can - // take over and do the rest of compilation. - - try { - String compileDefs = targetConfig.compileDefinitions.keySet().stream() - .map(key -> key + "=" + targetConfig.compileDefinitions.get(key)) - .collect(Collectors.joining("\n")); - FileUtil.writeToFile( - compileDefs, - Path.of(fileConfig.getSrcGenPath() + File.separator + "CompileDefinitions.txt") - ); - } catch (IOException e) { - Exceptions.sneakyThrow(e); - } - - // If this code generator is directly compiling the code, compile it now so that we - // clean it up after, removing the #line directives after errors have been reported. - if ( - !targetConfig.noCompile - && IterableExtensions.isNullOrEmpty(targetConfig.buildCommands) - && !federate.isRemote - // This code is unreachable in LSP_FAST mode, so that check is omitted. - && context.getMode() != LFGeneratorContext.Mode.LSP_MEDIUM - ) { - // FIXME: Currently, a lack of main is treated as a request to not produce - // a binary and produce a .o file instead. There should be a way to control - // this. - // Create an anonymous Runnable class and add it to the compileThreadPool - // so that compilation can happen in parallel. - var cleanCode = code.removeLines("#line"); - - var threadFileConfig = fileConfig; - var generator = this; // FIXME: currently only passed to report errors with line numbers in the Eclipse IDE - var CppMode = CCppMode; - federateCount++; - generatingContext.reportProgress( - String.format("Generated code for %d/%d executables. Compiling...", federateCount, federates.size()), - 100 * federateCount / federates.size() - ); - compileThreadPool.execute(() -> { - var cCompiler = new CCompiler(targetConfig, threadFileConfig, errorReporter, CppMode); - try { - if (!cCompiler.runCCompiler(generator, context)) { - // If compilation failed, remove any bin files that may have been created. - CUtil.deleteBinFiles(threadFileConfig); - // If finish has already been called, it is illegal and makes no sense. However, - // if finish has already been called, then this must be a federated execution. - if (!isFederated) context.unsuccessfulFinish(); - } else if (!isFederated) context.finish( - GeneratorResult.Status.COMPILED, lfModuleName, fileConfig, null - ); - cleanCode.writeToFile(targetFile); - } catch (IOException e) { - //noinspection ThrowableNotThrown,ResultOfMethodCallIgnored - Exceptions.sneakyThrow(e); - } - }); - } - fileConfig = oldFileConfig; - } - - // Initiate an orderly shutdown in which previously submitted tasks are - // executed, but no new tasks will be accepted. - compileThreadPool.shutdown(); + // Dump the additional compile definitions to a file to keep the generated project + // self-contained. In this way, third-party build tools like PlatformIO, west, arduino-cli can + // take over and do the rest of compilation. - // Wait for all compile threads to finish (NOTE: Can block forever) try { - if (!compileThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) { - throw new InterruptedException("Compilation timed out."); - } - } catch (InterruptedException e) { - //noinspection ThrowableNotThrown,ResultOfMethodCallIgnored + String compileDefs = targetConfig.compileDefinitions.keySet().stream() + .map(key -> key + "=" + targetConfig.compileDefinitions.get(key)) + .collect(Collectors.joining("\n")); + FileUtil.writeToFile( + compileDefs, + Path.of(fileConfig.getSrcGenPath() + File.separator + "CompileDefinitions.txt") + ); + } catch (IOException e) { Exceptions.sneakyThrow(e); } - if (isFederated) { + // If this code generator is directly compiling the code, compile it now so that we + // clean it up after, removing the #line directives after errors have been reported. + if ( + !targetConfig.noCompile && targetConfig.dockerOptions == null + && IterableExtensions.isNullOrEmpty(targetConfig.buildCommands) + // This code is unreachable in LSP_FAST mode, so that check is omitted. + && context.getMode() != LFGeneratorContext.Mode.LSP_MEDIUM + ) { + // FIXME: Currently, a lack of main is treated as a request to not produce + // a binary and produce a .o file instead. There should be a way to control + // this. + // Create an anonymous Runnable class and add it to the compileThreadPool + // so that compilation can happen in parallel. + var cleanCode = code.removeLines("#line"); + + var execName = lfModuleName; + var threadFileConfig = fileConfig; + var generator = this; // FIXME: currently only passed to report errors with line numbers in the Eclipse IDE + var CppMode = CCppMode; + // generatingContext.reportProgress( + // String.format("Generated code for %d/%d executables. Compiling...", federateCount, federates.size()), + // 100 * federateCount / federates.size() + // ); // FIXME: Move to FedGenerator + // Create the compiler to be used later + var cCompiler = new CCompiler(targetConfig, threadFileConfig, errorReporter, CppMode); try { - createFederatedLauncher(); + if (!cCompiler.runCCompiler(generator, context)) { + // If compilation failed, remove any bin files that may have been created. + CUtil.deleteBinFiles(threadFileConfig); + // If finish has already been called, it is illegal and makes no sense. However, + // if finish has already been called, then this must be a federated execution. + context.unsuccessfulFinish(); + } else { + context.finish( + GeneratorResult.Status.COMPILED, null + ); + } + cleanCode.writeToFile(targetFile); } catch (IOException e) { - //noinspection ThrowableNotThrown,ResultOfMethodCallIgnored Exceptions.sneakyThrow(e); } } - if (targetConfig.dockerOptions != null && mainDef != null) { - dockerGenerator.setHost(federationRTIProperties.get("host")); - try { - dockerGenerator.writeDockerFiles( - fileConfig.getSrcGenPath().resolve("docker-compose.yml")); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - // If a build directive has been given, invoke it now. // Note that the code does not get cleaned in this case. if (!targetConfig.noCompile) { @@ -754,23 +642,19 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { context.getMode() ); context.finish( - GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig, null - ); - } else if (isFederated) { - context.finish( - GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig, null + GeneratorResult.Status.COMPILED, null ); } System.out.println("Compiled binary is in " + fileConfig.binPath); } else { - context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(null)); + context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(context, null)); } // In case we are in Eclipse, make sure the generated code is visible. GeneratorUtils.refreshProject(resource, context.getMode()); } - private void generateCodeForCurrentFederate( + private void generateCodeFor( String lfModuleName ) { startTimeStepIsPresentCount = 0; @@ -829,58 +713,17 @@ private void generateCodeForCurrentFederate( modalStateResetCount )); - // Generate function to return a pointer to the action trigger_t - // that handles incoming network messages destined to the specified - // port. This will only be used if there are federates. - if (currentFederate.networkMessageActions.size() > 0) { - // Create a static array of trigger_t pointers. - // networkMessageActions is a list of Actions, but we - // need a list of trigger struct names for ActionInstances. - // There should be exactly one ActionInstance in the - // main reactor for each Action. - var triggers = new LinkedList(); - for (Action action : currentFederate.networkMessageActions) { - // Find the corresponding ActionInstance. - var actionInstance = main.lookupActionInstance(action); - triggers.add(CUtil.triggerRef(actionInstance, null)); - } - var actionTableCount = 0; - for (String trigger : triggers) { - initializeTriggerObjects.pr("_lf_action_table["+ actionTableCount++ +"] = &"+trigger+";"); - } - code.pr(String.join("\n", - "trigger_t* _lf_action_table["+currentFederate.networkMessageActions.size()+"];", - "trigger_t* _lf_action_for_port(int port_id) {", - " if (port_id < "+currentFederate.networkMessageActions.size()+") {", - " return _lf_action_table[port_id];", - " } else {", - " return NULL;", - " }", - "}" - )); - } else { - code.pr(String.join("\n", - "trigger_t* _lf_action_for_port(int port_id) {", - " return NULL;", - "}" - )); - } - // Generate function to initialize the trigger objects for all reactors. code.pr(CTriggerObjectsGenerator.generateInitializeTriggerObjects( - currentFederate, main, targetConfig, initializeTriggerObjects, startTimeStep, types, lfModuleName, - federationRTIProperties, startTimeStepTokens, startTimeStepIsPresentCount, - isFederated, - isFederatedAndDecentralized(), - clockSyncIsOn() + startupReactionCount )); // Generate function to trigger startup reactions for all reactors. @@ -897,15 +740,12 @@ private void generateCodeForCurrentFederate( if (CCppMode) code.pr("extern \"C\""); code.pr(String.join("\n", "void logical_tag_complete(tag_t tag_to_send) {", - isFederatedAndCentralized() ? - " _lf_logical_tag_complete(tag_to_send);" : "", + CExtensionUtils.surroundWithIfFederatedCentralized( + " _lf_logical_tag_complete(tag_to_send);" + ), "}" )); - if (isFederated) { - code.pr(CFederateGenerator.generateFederateNeighborStructure(currentFederate)); - } - // Generate function to schedule shutdown reactions if any // reactors have reactions to shutdown. code.pr(CReactionGenerator.generateLfTriggerShutdownReactions(shutdownReactionCount, hasModalReactors)); @@ -914,9 +754,12 @@ private void generateCodeForCurrentFederate( // execution. For federated execution, an implementation is // provided in federate.c. That implementation will resign // from the federation and close any open sockets. - if (!isFederated) { - code.pr("void terminate_execution() {}"); - } + code.pr(""" + #ifndef FEDERATED + void terminate_execution() {} + #endif""" + ); + // Generate functions for modes code.pr(CModesGenerator.generateLfInitializeModes( @@ -934,14 +777,10 @@ private void generateCodeForCurrentFederate( } } - protected CDockerGenerator getDockerGenerator() { - return new CDockerGenerator(isFederated, CCppMode, targetConfig); - } - @Override public void checkModalReactorSupport(boolean __) { // Modal reactors are currently only supported for non federated applications - super.checkModalReactorSupport(!isFederated); + super.checkModalReactorSupport(true); } @Override @@ -1012,10 +851,10 @@ private void inspectReactorEResource(ReactorDecl reactor) { } /** - * Copy all files or directories listed in the target property `files` and `cmake-include` - * into the src-gen folder of the main .lf file + * Copy all files or directories listed in the target property `files`, `cmake-include`, + * and `_fed_setup` into the src-gen folder of the main .lf file * - * @param targetConfig The targetConfig to read the `files` and `cmake-include` from. + * @param targetConfig The targetConfig to read the target properties from. * @param fileConfig The fileConfig used to make the copy and resolve paths. */ @Override @@ -1064,6 +903,15 @@ public void copyUserFiles(TargetConfig targetConfig, FileConfig fileConfig) { ); } } + + if (!StringExtensions.isNullOrEmpty(targetConfig.fedSetupPreamble)) { + try { + FileUtil.copyFile(fileConfig.srcFile.getParent().resolve(targetConfig.fedSetupPreamble), + targetDir.resolve(targetConfig.fedSetupPreamble)); + } catch (IOException e) { + errorReporter.reportError("Failed to find _fed_setup file " + targetConfig.fedSetupPreamble); + } + } } /** @@ -1120,8 +968,7 @@ private void generateReactorChildren( LinkedHashSet generatedReactorDecls ) { for (ReactorInstance r : reactor.children) { - if (currentFederate.contains(r) && - r.reactorDeclaration != null && + if (r.reactorDeclaration != null && !generatedReactorDecls.contains(r.reactorDeclaration)) { generatedReactorDecls.add(r.reactorDeclaration); generateReactorChildren(r, generatedReactorDecls); @@ -1147,52 +994,6 @@ private void pickCompilePlatform() { } } - /** Create a launcher script that executes all the federates and the RTI. */ - public void createFederatedLauncher() throws IOException{ - var launcher = new FedCLauncher( - targetConfig, - fileConfig, - errorReporter - ); - launcher.createLauncher( - federates, - federationRTIProperties - ); - } - - protected boolean clockSyncIsOn() { - return targetConfig.clockSync != ClockSyncMode.OFF - && (!federationRTIProperties.get("host").toString().equals(currentFederate.host) - || targetConfig.clockSyncOptions.localFederatesOn); - } - - /** - * Initialize clock synchronization (if enabled) and its related options for a given federate. - * - * Clock synchronization can be enabled using the clock-sync target property. - * @see Documentation - */ - protected void initializeClockSynchronization() { - // Check if clock synchronization should be enabled for this federate in the first place - if (clockSyncIsOn()) { - System.out.println("Initial clock synchronization is enabled for federate " - + currentFederate.id - ); - if (targetConfig.clockSync == ClockSyncMode.ON) { - if (targetConfig.clockSyncOptions.collectStats) { - System.out.println("Will collect clock sync statistics for federate " + currentFederate.id); - // Add libm to the compiler flags - // FIXME: This is a linker flag not compile flag but we don't have a way to add linker flags - // FIXME: This is probably going to fail on MacOS (especially using clang) - // because libm functions are builtin - targetConfig.compilerFlags.add("-lm"); - } - System.out.println("Runtime clock synchronization is enabled for federate " - + currentFederate.id - ); - } - } - } /** * Copy target-specific header file to the src-gen directory. @@ -1249,8 +1050,8 @@ private void generateReactorClass(ReactorDecl reactor) { generateAuxiliaryStructs(reactor); generateSelfStruct(reactor, constructorCode); generateMethods(reactor); - generateReactions(reactor, currentFederate); - generateConstructor(reactor, currentFederate, constructorCode); + generateReactions(reactor); + generateConstructor(reactor, constructorCode); code.pr("// =============== END reactor class " + reactor.getName()); code.pr(""); @@ -1279,16 +1080,14 @@ protected void generateUserPreamblesForReactor(Reactor reactor) { /** * Generate a constructor for the specified reactor in the specified federate. * @param reactor The parsed reactor data structure. - * @param federate A federate name, or null to unconditionally generate. * @param constructorCode Lines of code previously generated that need to * go into the constructor. */ protected void generateConstructor( - ReactorDecl reactor, FederateInstance federate, CodeBuilder constructorCode + ReactorDecl reactor, CodeBuilder constructorCode ) { code.pr(CConstructorGenerator.generateConstructor( reactor, - federate, constructorCode.toString() )); } @@ -1308,12 +1107,15 @@ protected void generateAuxiliaryStructs(ReactorDecl decl) { // port or action is late due to network // latency, etc.. var federatedExtension = new CodeBuilder(); - if (isFederatedAndDecentralized()) { - federatedExtension.pr(types.getTargetTagType()+" intended_tag;"); - } - if (isFederated) { - federatedExtension.pr(types.getTargetTimeType()+" physical_time_of_arrival;"); - } + federatedExtension.pr(""" + #ifdef FEDERATED + #ifdef FEDERATED_DECENTRALIZED + %s intended_tag; + #endif + %s physical_time_of_arrival; + #endif + """.formatted(types.getTargetTagType(), types.getTargetTimeType()) + ); // First, handle inputs. for (Input input : allInputs(reactor)) { code.pr(CPortGenerator.generateAuxiliaryStruct( @@ -1341,15 +1143,13 @@ protected void generateAuxiliaryStructs(ReactorDecl decl) { // a trigger_t* because the struct will be cast to (trigger_t*) // by the lf_schedule() functions to get to the trigger. for (Action action : allActions(reactor)) { - if (currentFederate.contains(action)) { - code.pr(CActionGenerator.generateAuxiliaryStruct( - decl, - action, - getTarget(), - types, - federatedExtension - )); - } + code.pr(CActionGenerator.generateAuxiliaryStruct( + decl, + action, + getTarget(), + types, + federatedExtension + )); } } @@ -1378,7 +1178,7 @@ private void generateSelfStruct(ReactorDecl decl, CodeBuilder constructorCode) { body.pr(CStateGenerator.generateDeclarations(reactor, types)); // Next handle actions. - CActionGenerator.generateDeclarations(reactor, decl, currentFederate, body, constructorCode); + CActionGenerator.generateDeclarations(reactor, decl, body, constructorCode); // Next handle inputs and outputs. CPortGenerator.generateDeclarations(reactor, decl, body, constructorCode); @@ -1394,13 +1194,10 @@ private void generateSelfStruct(ReactorDecl decl, CodeBuilder constructorCode) { // Next, generate the fields needed for each reaction. CReactionGenerator.generateReactionAndTriggerStructs( - currentFederate, body, decl, constructorCode, - types, - isFederated, - isFederatedAndDecentralized() + types ); // Next, generate fields for modes @@ -1440,7 +1237,7 @@ private void generateInteractingContainedReactors( ) { // The contents of the struct will be collected first so that // we avoid duplicate entries and then the struct will be constructed. - var contained = new InteractingContainedReactors(reactor, currentFederate); + var contained = new InteractingContainedReactors(reactor); // Next generate the relevant code. for (Instantiation containedReactor : contained.containedReactors()) { // First define an _width variable in case it is a bank. @@ -1504,9 +1301,13 @@ private void generateInteractingContainedReactors( } var portOnSelf = "self->_lf_"+containedReactor.getName()+reactorIndex+"."+port.getName(); - if (isFederatedAndDecentralized()) { - constructorCode.pr(port, portOnSelf+"_trigger.intended_tag = (tag_t) { .time = NEVER, .microstep = 0u};"); - } + constructorCode.pr( + port, + CExtensionUtils.surroundWithIfFederatedDecentralized( + portOnSelf+"_trigger.intended_tag = (tag_t) { .time = NEVER, .microstep = 0u};" + ) + ); + var triggered = contained.reactionsTriggered(containedReactor, port); //noinspection StatementWithEmptyBody if (triggered.size() > 0) { @@ -1533,10 +1334,15 @@ private void generateInteractingContainedReactors( portOnSelf+"_trigger.number_of_reactions = "+triggered.size()+";" )); - if (isFederated) { - // Set the physical_time_of_arrival - constructorCode.pr(port, portOnSelf+"_trigger.physical_time_of_arrival = NEVER;"); - } + + // Set the physical_time_of_arrival + constructorCode.pr( + port, + CExtensionUtils.surroundWithIfFederated( + portOnSelf+"_trigger.physical_time_of_arrival = NEVER;" + ) + ); + if (containedReactor.getWidthSpec() != null) { constructorCode.unindent(); constructorCode.pr("}"); @@ -1570,17 +1376,12 @@ protected void generateSelfStructExtension( * a struct that contains parameters, state variables, inputs (triggering or not), * actions (triggering or produced), and outputs. * @param decl The reactor. - * @param federate The federate, or null if this is not - * federated or not the main reactor and reactions should be - * unconditionally generated. */ - public void generateReactions(ReactorDecl decl, FederateInstance federate) { + public void generateReactions(ReactorDecl decl) { var reactionIndex = 0; var reactor = ASTUtils.toDefinition(decl); for (Reaction reaction : allReactions(reactor)) { - if (federate == null || federate.contains(reaction)) { - generateReaction(reaction, decl, reactionIndex); - } + generateReaction(reaction, decl, reactionIndex); // Increment reaction index even if the reaction is not in the federate // so that across federates, the reaction indices are consistent. reactionIndex++; @@ -1603,7 +1404,6 @@ protected void generateReaction(Reaction reaction, ReactorDecl decl, int reactio mainDef, errorReporter, types, - isFederatedAndDecentralized(), getTarget().requiresTypes )); } @@ -1616,41 +1416,39 @@ private void recordBuiltinTriggers(ReactorInstance instance) { // For each reaction instance, allocate the arrays that will be used to // trigger downstream reactions. for (ReactionInstance reaction : instance.reactions) { - if (currentFederate.contains(reaction.getDefinition())) { - var reactor = reaction.getParent(); - var temp = new CodeBuilder(); - var foundOne = false; - - var reactionRef = CUtil.reactionRef(reaction); - - // Next handle triggers of the reaction that come from a multiport output - // of a contained reactor. Also, handle startup and shutdown triggers. - for (TriggerInstance trigger : reaction.triggers) { - if (trigger.isStartup()) { - temp.pr("_lf_startup_reactions[_lf_startup_reactions_count++] = &"+reactionRef+";"); - startupReactionCount += currentFederate.numRuntimeInstances(reactor); - foundOne = true; - } else if (trigger.isShutdown()) { - temp.pr("_lf_shutdown_reactions[_lf_shutdown_reactions_count++] = &"+reactionRef+";"); - foundOne = true; - shutdownReactionCount += currentFederate.numRuntimeInstances(reactor); - - if (targetConfig.tracing != null) { - var description = CUtil.getShortenedName(reactor); - var reactorRef = CUtil.reactorRef(reactor); - temp.pr(String.join("\n", - "_lf_register_trace_event("+reactorRef+", &("+reactorRef+"->_lf__shutdown),", - "trace_trigger, "+addDoubleQuotes(description+".shutdown")+");" - )); - } - } else if (trigger.isReset()) { - temp.pr("_lf_reset_reactions[_lf_reset_reactions_count++] = &"+reactionRef+";"); - resetReactionCount += currentFederate.numRuntimeInstances(reactor); - foundOne = true; + var reactor = reaction.getParent(); + var temp = new CodeBuilder(); + var foundOne = false; + + var reactionRef = CUtil.reactionRef(reaction); + + // Next handle triggers of the reaction that come from a multiport output + // of a contained reactor. Also, handle startup and shutdown triggers. + for (TriggerInstance trigger : reaction.triggers) { + if (trigger.isStartup()) { + temp.pr("_lf_startup_reactions[_lf_startup_reactions_count++] = &"+reactionRef+";"); + startupReactionCount += reactor.getTotalWidth(); + foundOne = true; + } else if (trigger.isShutdown()) { + temp.pr("_lf_shutdown_reactions[_lf_shutdown_reactions_count++] = &"+reactionRef+";"); + foundOne = true; + shutdownReactionCount += reactor.getTotalWidth(); + + if (targetConfig.tracing != null) { + var description = CUtil.getShortenedName(reactor); + var reactorRef = CUtil.reactorRef(reactor); + temp.pr(String.join("\n", + "_lf_register_trace_event("+reactorRef+", &("+reactorRef+"->_lf__shutdown),", + "trace_trigger, "+addDoubleQuotes(description+".shutdown")+");" + )); } + } else if (trigger.isReset()) { + temp.pr("_lf_reset_reactions[_lf_reset_reactions_count++] = &"+reactionRef+";"); + resetReactionCount += reactor.getTotalWidth(); + foundOne = true; } - if (foundOne) initializeTriggerObjects.pr(temp.toString()); } + if (foundOne) initializeTriggerObjects.pr(temp.toString()); } } @@ -1665,19 +1463,19 @@ private void generateStartTimeStep(ReactorInstance instance) { // First, set up to decrement reference counts for each token type // input of a contained reactor that is present. for (ReactorInstance child : instance.children) { - if (currentFederate.contains(child) && child.inputs.size() > 0) { + if (child.inputs.size() > 0) { // Avoid generating code if not needed. var foundOne = false; var temp = new CodeBuilder(); - temp.startScopedBlock(child, currentFederate, isFederated, true); + temp.startScopedBlock(child); for (PortInstance input : child.inputs) { if (CUtil.isTokenType(getInferredType(input.getDefinition()), types)) { foundOne = true; temp.pr(CPortGenerator.initializeStartTimeStepTableForInput(input)); - startTimeStepTokens += currentFederate.numRuntimeInstances(input.getParent()) * input.getWidth(); + startTimeStepTokens += input.getParent().getTotalWidth() * input.getWidth(); } } temp.endScopedBlock(); @@ -1699,71 +1497,71 @@ private void generateStartTimeStep(ReactorInstance instance) { // port so we have to avoid listing the port more than once. var portsSeen = new LinkedHashSet(); for (ReactionInstance reaction : instance.reactions) { - if (currentFederate.contains(reaction.getDefinition())) { - for (PortInstance port : Iterables.filter(reaction.effects, PortInstance.class)) { - if (port.getDefinition() instanceof Input && !portsSeen.contains(port)) { - portsSeen.add(port); - // This reaction is sending to an input. Must be - // the input of a contained reactor in the federate. - // NOTE: If instance == main and the federate is within a bank, - // this assumes that the reaction writes only to the bank member in the federate. - if (currentFederate.contains(port.getParent())) { - foundOne = true; - - temp.pr("// Add port "+port.getFullName()+" to array of is_present fields."); - - if (!Objects.equal(port.getParent(), instance)) { - // The port belongs to contained reactor, so we also have - // iterate over the instance bank members. - temp.startScopedBlock(); - temp.pr("int count = 0; SUPPRESS_UNUSED_WARNING(count);"); - temp.startScopedBlock(instance, currentFederate, isFederated, true); - temp.startScopedBankChannelIteration(port, currentFederate, null, isFederated); - } else { - temp.startScopedBankChannelIteration(port, currentFederate, "count", isFederated); - } - var portRef = CUtil.portRefNested(port); - var con = (port.isMultiport()) ? "->" : "."; - - temp.pr("_lf_is_present_fields["+startTimeStepIsPresentCount+" + count] = &"+portRef+con+"is_present;"); - if (isFederatedAndDecentralized()) { - // Intended_tag is only applicable to ports in federated execution. - temp.pr("_lf_intended_tag_fields["+startTimeStepIsPresentCount+" + count] = &"+portRef+con+"intended_tag;"); - } - - startTimeStepIsPresentCount += port.getWidth() * currentFederate.numRuntimeInstances(port.getParent()); - - if (!Objects.equal(port.getParent(), instance)) { - temp.pr("count++;"); - temp.endScopedBlock(); - temp.endScopedBlock(); - temp.endScopedBankChannelIteration(port, null); - } else { - temp.endScopedBankChannelIteration(port, "count"); - } - } + for (PortInstance port : Iterables.filter(reaction.effects, PortInstance.class)) { + if (port.getDefinition() instanceof Input && !portsSeen.contains(port)) { + portsSeen.add(port); + // This reaction is sending to an input. Must be + // the input of a contained reactor in the federate. + // NOTE: If instance == main and the federate is within a bank, + // this assumes that the reaction writes only to the bank member in the federate. + foundOne = true; + + temp.pr("// Add port "+port.getFullName()+" to array of is_present fields."); + + if (!Objects.equal(port.getParent(), instance)) { + // The port belongs to contained reactor, so we also have + // iterate over the instance bank members. + temp.startScopedBlock(); + temp.pr("int count = 0; SUPPRESS_UNUSED_WARNING(count);"); + temp.startScopedBlock(instance); + temp.startScopedBankChannelIteration(port, null); + } else { + temp.startScopedBankChannelIteration(port, "count"); + } + var portRef = CUtil.portRefNested(port); + var con = (port.isMultiport()) ? "->" : "."; + + temp.pr("_lf_is_present_fields["+startTimeStepIsPresentCount+" + count] = &"+portRef+con+"is_present;"); + // Intended_tag is only applicable to ports in federated execution. + temp.pr( + CExtensionUtils.surroundWithIfFederatedDecentralized( + "_lf_intended_tag_fields["+startTimeStepIsPresentCount+" + count] = &"+portRef+con+"intended_tag;" + ) + ); + + startTimeStepIsPresentCount += port.getWidth() * port.getParent().getTotalWidth(); + + if (!Objects.equal(port.getParent(), instance)) { + temp.pr("count++;"); + temp.endScopedBlock(); + temp.endScopedBlock(); + temp.endScopedBankChannelIteration(port, null); + } else { + temp.endScopedBankChannelIteration(port, "count"); } } - // Find outputs of contained reactors that have token types and therefore - // need to have their reference counts decremented. - for (PortInstance port : Iterables.filter(reaction.sources, PortInstance.class)) { - if (port.isOutput() && !portsSeen.contains(port)) { - portsSeen.add(port); - // This reaction is receiving data from the port. - if (CUtil.isTokenType(ASTUtils.getInferredType(port.getDefinition()), types)) { - foundOne = true; - temp.pr("// Add port "+port.getFullName()+" to array _lf_tokens_with_ref_count."); - // Potentially have to iterate over bank members of the instance - // (parent of the reaction), bank members of the contained reactor (if a bank), - // and channels of the multiport (if multiport). - temp.startScopedBlock(instance, currentFederate, isFederated, true); - temp.startScopedBankChannelIteration(port, currentFederate, "count", isFederated); - var portRef = CUtil.portRef(port, true, true, null, null, null); - temp.pr(CPortGenerator.initializeStartTimeStepTableForPort(portRef)); - startTimeStepTokens += port.getWidth() * currentFederate.numRuntimeInstances(port.getParent()); - temp.endScopedBankChannelIteration(port, "count"); - temp.endScopedBlock(); - } + } + // Find outputs of contained reactors that have token types and therefore + // need to have their reference counts decremented. + for (PortInstance port : Iterables.filter(reaction.sources, PortInstance.class)) { + if (port.isOutput() && !portsSeen.contains(port)) { + portsSeen.add(port); + // This reaction is receiving data from the port. + if (CUtil.isTokenType(ASTUtils.getInferredType(((Output) port.getDefinition())), types)) { + foundOne = true; + temp.pr("// Add port " + port.getFullName() + + " to array _lf_tokens_with_ref_count."); + // Potentially have to iterate over bank members of the instance + // (parent of the reaction), bank members of the contained reactor (if a bank), + // and channels of the multiport (if multiport). + temp.startScopedBlock(instance); + temp.startScopedBankChannelIteration(port, "count"); + var portRef = CUtil.portRef(port, true, true, null, null, null); + temp.pr(CPortGenerator.initializeStartTimeStepTableForPort(portRef)); + startTimeStepTokens += port.getWidth() * + port.getParent().getTotalWidth(); + temp.endScopedBankChannelIteration(port, "count"); + temp.endScopedBlock(); } } } @@ -1773,26 +1571,30 @@ private void generateStartTimeStep(ReactorInstance instance) { foundOne = false; for (ActionInstance action : instance.actions) { - if (currentFederate == null || currentFederate.contains(action.getDefinition())) { - foundOne = true; - temp.startScopedBlock(instance, currentFederate, isFederated, true); - - temp.pr(String.join("\n", - "// Add action "+action.getFullName()+" to array of is_present fields.", - "_lf_is_present_fields["+startTimeStepIsPresentCount+"] ", - " = &"+containerSelfStructName+"->_lf_"+action.getName()+".is_present;" - )); - if (isFederatedAndDecentralized()) { - // Intended_tag is only applicable to actions in federated execution with decentralized coordination. - temp.pr(String.join("\n", - "// Add action "+action.getFullName()+" to array of intended_tag fields.", - "_lf_intended_tag_fields["+startTimeStepIsPresentCount+"] ", - " = &"+containerSelfStructName+"->_lf_"+action.getName()+".intended_tag;" - )); - } - startTimeStepIsPresentCount += currentFederate.numRuntimeInstances(action.getParent()); - temp.endScopedBlock(); - } + foundOne = true; + temp.startScopedBlock(instance); + + temp.pr(String.join("\n", + "// Add action "+action.getFullName()+" to array of is_present fields.", + "_lf_is_present_fields["+startTimeStepIsPresentCount+"] ", + " = &"+containerSelfStructName+"->_lf_"+action.getName()+".is_present;" + )); + + // Intended_tag is only applicable to actions in federated execution with decentralized coordination. + temp.pr( + CExtensionUtils.surroundWithIfFederatedDecentralized( + String.join("\n", + "// Add action " + action.getFullName() + + " to array of intended_tag fields.", + "_lf_intended_tag_fields[" + + startTimeStepIsPresentCount + "] ", + " = &" + containerSelfStructName + + "->_lf_" + action.getName() + + ".intended_tag;" + ))); + + startTimeStepIsPresentCount += action.getParent().getTotalWidth(); + temp.endScopedBlock(); } if (foundOne) startTimeStep.pr(temp.toString()); temp = new CodeBuilder(); @@ -1800,11 +1602,11 @@ private void generateStartTimeStep(ReactorInstance instance) { // Next, set up the table to mark each output of each contained reactor absent. for (ReactorInstance child : instance.children) { - if (currentFederate.contains(child) && child.outputs.size() > 0) { + if (child.outputs.size() > 0) { temp.startScopedBlock(); temp.pr("int count = 0; SUPPRESS_UNUSED_WARNING(count);"); - temp.startScopedBlock(child, currentFederate, isFederated, true); + temp.startScopedBlock(child); var channelCount = 0; for (PortInstance output : child.outputs) { @@ -1814,20 +1616,20 @@ private void generateStartTimeStep(ReactorInstance instance) { temp.startChannelIteration(output); temp.pr("_lf_is_present_fields["+startTimeStepIsPresentCount+" + count] = &"+CUtil.portRef(output)+".is_present;"); - if (isFederatedAndDecentralized()) { - // Intended_tag is only applicable to ports in federated execution with decentralized coordination. - temp.pr(String.join("\n", - "// Add port "+output.getFullName()+" to array of intended_tag fields.", - "_lf_intended_tag_fields["+startTimeStepIsPresentCount+" + count] = &"+CUtil.portRef(output)+".intended_tag;" - )); - } + // Intended_tag is only applicable to ports in federated execution with decentralized coordination. + temp.pr( + CExtensionUtils.surroundWithIfFederatedDecentralized( + String.join("\n", + "// Add port "+output.getFullName()+" to array of intended_tag fields.", + "_lf_intended_tag_fields["+startTimeStepIsPresentCount+" + count] = &"+CUtil.portRef(output)+".intended_tag;" + ))); temp.pr("count++;"); channelCount += output.getWidth(); temp.endChannelIteration(output); } } - startTimeStepIsPresentCount += channelCount * currentFederate.numRuntimeInstances(child); + startTimeStepIsPresentCount += channelCount * child.getTotalWidth(); temp.endScopedBlock(); temp.endScopedBlock(); } @@ -1846,11 +1648,9 @@ private void generateStartTimeStep(ReactorInstance instance) { */ private void generateTimerInitializations(ReactorInstance instance) { for (TimerInstance timer : instance.timers) { - if (currentFederate.contains(timer.getDefinition())) { - if (!timer.isStartup()) { - initializeTriggerObjects.pr(CTimerGenerator.generateInitializer(timer)); - timerCount += currentFederate.numRuntimeInstances(timer.getParent()); - } + if (!timer.isStartup()) { + initializeTriggerObjects.pr(CTimerGenerator.generateInitializer(timer)); + timerCount += timer.getParent().getTotalWidth(); } } } @@ -1862,7 +1662,7 @@ private void generateTimerInitializations(ReactorInstance instance) { * the required .h and .c files. * @param filename Name of the file to process. */ - public void processProtoFile(String filename, CancelIndicator cancelIndicator) { + public void processProtoFile(String filename) { var protoc = commandFactory.createCommand( "protoc-c", List.of("--c_out="+this.fileConfig.getSrcGenPath(), filename), @@ -1871,7 +1671,7 @@ public void processProtoFile(String filename, CancelIndicator cancelIndicator) { errorReporter.reportError("Processing .proto files requires protoc-c >= 1.3.3."); return; } - var returnCode = protoc.run(cancelIndicator); + var returnCode = protoc.run(); if (returnCode == 0) { var nameSansProto = filename.substring(0, filename.length() - 6); targetConfig.compileAdditionalSources.add( @@ -1920,7 +1720,7 @@ public static String variableStructType(TriggerInstance portOrAction) { private void generateTraceTableEntries(ReactorInstance instance) { if (targetConfig.tracing != null) { initializeTriggerObjects.pr( - CTracingGenerator.generateTraceTableEntries(instance, currentFederate) + CTracingGenerator.generateTraceTableEntries(instance) ); } } @@ -1960,47 +1760,14 @@ public void generateReactorInstance(ReactorInstance instance) { // Recursively generate code for the children. for (ReactorInstance child : instance.children) { - if (currentFederate.contains(child)) { - // If this reactor is a placeholder for a bank of reactors, then generate - // an array of instances of reactors and create an enclosing for loop. - // Need to do this for each of the builders into which the code writes. - startTimeStep.startScopedBlock(child, currentFederate, isFederated, true); - initializeTriggerObjects.startScopedBlock(child, currentFederate, isFederated, true); - generateReactorInstance(child); - initializeTriggerObjects.endScopedBlock(); - startTimeStep.endScopedBlock(); - } - } - - // If this program is federated with centralized coordination and this reactor - // instance is a federate, then check - // for outputs that depend on physical actions so that null messages can be - // sent to the RTI. - if (isFederatedAndCentralized() && instance.getParent() == main) { - var outputDelayMap = currentFederate.findOutputsConnectedToPhysicalActions(instance); - var minDelay = TimeValue.MAX_VALUE; - Output outputFound = null; - for (Output output : outputDelayMap.keySet()) { - var outputDelay = outputDelayMap.get(output); - if (outputDelay.isEarlierThan(minDelay)) { - minDelay = outputDelay; - outputFound = output; - } - } - if (minDelay != TimeValue.MAX_VALUE) { - // Unless silenced, issue a warning. - if (targetConfig.coordinationOptions.advance_message_interval == null) { - errorReporter.reportWarning(outputFound, String.join("\n", - "Found a path from a physical action to output for reactor "+addDoubleQuotes(instance.getName())+". ", - "The amount of delay is "+minDelay+".", - "With centralized coordination, this can result in a large number of messages to the RTI.", - "Consider refactoring the code so that the output does not depend on the physical action,", - "or consider using decentralized coordination. To silence this warning, set the target", - "parameter coordination-options with a value like {advance-message-interval: 10 msec}" - )); - } - initializeTriggerObjects.pr("_fed.min_delay_from_physical_action_to_federate_output = "+GeneratorBase.timeInTargetLanguage(minDelay)+";"); - } + // If this reactor is a placeholder for a bank of reactors, then generate + // an array of instances of reactors and create an enclosing for loop. + // Need to do this for each of the builders into which the code writes. + startTimeStep.startScopedBlock(child); + initializeTriggerObjects.startScopedBlock(child); + generateReactorInstance(child); + initializeTriggerObjects.endScopedBlock(); + startTimeStep.endScopedBlock(); } // For this instance, define what must be done at the start of @@ -2018,7 +1785,7 @@ public void generateReactorInstance(ReactorInstance instance) { * @param instance The reactor. */ private void generateActionInitializations(ReactorInstance instance) { - initializeTriggerObjects.pr(CActionGenerator.generateInitializers(instance, currentFederate)); + initializeTriggerObjects.pr(CActionGenerator.generateInitializers(instance)); } /** @@ -2031,7 +1798,6 @@ private void generateInitializeActionToken(ReactorInstance reactor) { for (ActionInstance action : reactor.actions) { // Skip this step if the action is not in use. if (action.getParent().getTriggers().contains(action) - && currentFederate.contains(action.getDefinition()) ) { var type = getInferredType(action.getDefinition()); var payloadSize = "0"; @@ -2051,7 +1817,7 @@ private void generateInitializeActionToken(ReactorInstance reactor) { selfStruct, action.getName(), payloadSize ) ); - startTimeStepTokens += currentFederate.numRuntimeInstances(action.getParent()); + startTimeStepTokens += action.getParent().getTotalWidth(); } } } @@ -2090,7 +1856,7 @@ protected void generateStateVariableInitializations(ReactorInstance instance) { types )); if (mode != null && stateVar.isReset()) { - modalStateResetCount += currentFederate.numRuntimeInstances(instance); + modalStateResetCount += instance.getTotalWidth(); } } } @@ -2103,14 +1869,12 @@ protected void generateStateVariableInitializations(ReactorInstance instance) { */ private void generateSetDeadline(ReactorInstance instance) { for (ReactionInstance reaction : instance.reactions) { - if (currentFederate.contains(reaction.getDefinition())) { - var selfRef = CUtil.reactorRef(reaction.getParent())+"->_lf__reaction_"+reaction.index; - if (reaction.declaredDeadline != null) { - var deadline = reaction.declaredDeadline.maxDelay; - initializeTriggerObjects.pr(selfRef+".deadline = "+GeneratorBase.timeInTargetLanguage(deadline)+";"); - } else { // No deadline. - initializeTriggerObjects.pr(selfRef+".deadline = NEVER;"); - } + var selfRef = CUtil.reactorRef(reaction.getParent())+"->_lf__reaction_"+reaction.index; + if (reaction.declaredDeadline != null) { + var deadline = reaction.declaredDeadline.maxDelay; + initializeTriggerObjects.pr(selfRef+".deadline = "+GeneratorBase.timeInTargetLanguage(deadline)+";"); + } else { // No deadline. + initializeTriggerObjects.pr(selfRef+".deadline = NEVER;"); } } } @@ -2122,7 +1886,7 @@ private void generateSetDeadline(ReactorInstance instance) { private void generateModeStructure(ReactorInstance instance) { CModesGenerator.generateModeStructure(instance, initializeTriggerObjects); if (!instance.modes.isEmpty()) { - modalReactorCount += currentFederate.numRuntimeInstances(instance); + modalReactorCount += instance.getTotalWidth(); } } @@ -2190,6 +1954,15 @@ public TargetTypes getTargetTypes() { return types; } + /** + * + * @param context + * @return + */ + protected DockerGenerator getDockerGenerator(LFGeneratorContext context) { + return new CDockerGenerator(context); + } + // ////////////////////////////////////////// // // Protected methods. @@ -2198,22 +1971,8 @@ protected void setUpGeneralParameters() { accommodatePhysicalActionsIfPresent(); targetConfig.compileDefinitions.put("LOG_LEVEL", targetConfig.logLevel.ordinal() + ""); targetConfig.compileAdditionalSources.addAll(CCoreFilesUtils.getCTargetSrc()); - setCSpecificDefaults(); // Create the main reactor instance if there is a main reactor. createMainReactorInstance(); - // If there are federates, copy the required files for that. - // Also, create the RTI C file and the launcher script. - if (isFederated) { - // Handle target parameters. - // If the program is federated, then ensure that threading is enabled. - targetConfig.threading = true; - // Convey to the C runtime the required number of worker threads to - // handle network input control reactions. - targetConfig.compileDefinitions.put( - "WORKERS_NEEDED_FOR_FEDERATE", - CUtil.minThreadsToHandleInputPorts(federates) + "" - ); - } if (hasModalReactors) { // So that each separate compile knows about modal reactors, do this: targetConfig.compileDefinitions.put("MODAL_REACTORS", "TRUE"); @@ -2241,236 +2000,58 @@ protected void setUpGeneralParameters() { pickCompilePlatform(); } - // Perform set up that does not generate code - protected void setUpFederateSpecificParameters(FederateInstance federate, CodeBuilder commonCode) { - currentFederate = federate; - if (isFederated) { - // Reset the cmake-includes and files, to be repopulated for each federate individually. - // This is done to enable support for separately - // adding cmake-includes/files for different federates to prevent linking and mixing - // all federates' supporting libraries/files together. - targetConfig.cmakeIncludes.clear(); - targetConfig.cmakeIncludesWithoutPath.clear(); - targetConfig.fileNames.clear(); - targetConfig.filesNamesWithoutPath.clear(); - - // Re-apply the cmake-include target property of the main .lf file. - var target = GeneratorUtils.findTarget(mainDef.getReactorClass().eResource()); - if (target.getConfig() != null) { - // Update the cmake-include - TargetProperty.updateOne( - this.targetConfig, - TargetProperty.CMAKE_INCLUDE, - convertToEmptyListIfNull(target.getConfig().getPairs()), - errorReporter - ); - // Update the files - TargetProperty.updateOne( - this.targetConfig, - TargetProperty.FILES, - convertToEmptyListIfNull(target.getConfig().getPairs()), - errorReporter - ); - } - // Clear out previously generated code. - code = new CodeBuilder(commonCode); - initializeTriggerObjects = new CodeBuilder(); - // Enable clock synchronization if the federate - // is not local and clock-sync is enabled - initializeClockSynchronization(); - startTimeStep = new CodeBuilder(); - } - } - - - - /** - * Generate code for the body of a reaction that handles the - * action that is triggered by receiving a message from a remote - * federate. - * @param action The action. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param receivingFed The destination federate. - * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. - * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. - * @param type The type. - * @param isPhysical Indicates whether or not the connection is physical - * @param serializer The serializer used on the connection. - */ - @Override - public String generateNetworkReceiverBody( - Action action, - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - FederateInstance receivingFed, - int receivingBankIndex, - int receivingChannelIndex, - InferredType type, - boolean isPhysical, - SupportedSerializers serializer - ) { - return CNetworkGenerator.generateNetworkReceiverBody( - action, - sendingPort, - receivingPort, - receivingPortID, - sendingFed, - receivingFed, - receivingBankIndex, - receivingChannelIndex, - type, - isPhysical, - serializer, - types, - targetConfig.coordination - ); - } - - /** - * Generate code for the body of a reaction that handles an output - * that is to be sent over the network. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The variable reference to the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. - * @param receivingFed The destination federate. - * @param type The type. - * @param isPhysical Indicates whether the connection is physical or not - * @param delay The delay value imposed on the connection using after - * @param serializer The serializer used on the connection. - */ - @Override - public String generateNetworkSenderBody( - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - int sendingBankIndex, - int sendingChannelIndex, - FederateInstance receivingFed, - InferredType type, - boolean isPhysical, - Expression delay, - SupportedSerializers serializer - ) { - return CNetworkGenerator.generateNetworkSenderBody( - sendingPort, - receivingPort, - receivingPortID, - sendingFed, - sendingBankIndex, - sendingChannelIndex, - receivingFed, - type, - isPhysical, - delay, - serializer, - types, - targetConfig.coordination - ); - } - /** - * Generate code for the body of a reaction that decides whether the trigger for the given - * port is going to be present or absent for the current logical time. - * This reaction is put just before the first reaction that is triggered by the network - * input port "port" or has it in its sources. If there are only connections to contained - * reactors, in the top-level reactor. - * - * @param receivingPortID The ID of the port to generate the control reaction for - * @param maxSTP The maximum value of STP is assigned to reactions (if any) - * that have port as their trigger or source - */ - @Override - public String generateNetworkInputControlReactionBody( - int receivingPortID, - TimeValue maxSTP - ) { - return CNetworkGenerator.generateNetworkInputControlReactionBody( - receivingPortID, - maxSTP, - isFederatedAndDecentralized() - ); - } - - /** - * Generate code for the body of a reaction that sends a port status message for the given - * port if it is absent. - * - * @param port The port to generate the control reaction for - * @param portID The ID assigned to the port in the AST transformation - * @param receivingFederateID The ID of the receiving federate - * @param sendingBankIndex The bank index of the sending federate, if it is in a bank. - * @param sendingChannelIndex The channel if a multiport - * @param delay The delay value imposed on the connection using after - */ - @Override - public String generateNetworkOutputControlReactionBody( - VarRef port, - int portID, - int receivingFederateID, - int sendingBankIndex, - int sendingChannelIndex, - Expression delay - ) { - return CNetworkGenerator.generateNetworkOutputControlReactionBody( - port, - portID, - receivingFederateID, - sendingBankIndex, - sendingChannelIndex, - delay - ); - } - - /** - * Add necessary code to the source and necessary build supports to - * enable the requested serializer in 'enabledSerializers' - */ - @Override - public void enableSupportForSerializationIfApplicable(CancelIndicator cancelIndicator) { - if (!IterableExtensions.isNullOrEmpty(targetConfig.protoFiles)) { - // Enable support for proto serialization - enabledSerializers.add(SupportedSerializers.PROTO); - } - for (SupportedSerializers serializer : enabledSerializers) { - switch (serializer) { - case NATIVE -> { - // No need to do anything at this point. - } - case PROTO -> { - // Handle .proto files. - for (String file : targetConfig.protoFiles) { - this.processProtoFile(file, cancelIndicator); - var dotIndex = file.lastIndexOf("."); - var rootFilename = file; - if (dotIndex > 0) { - rootFilename = file.substring(0, dotIndex); - } - code.pr("#include " + addDoubleQuotes(rootFilename + ".pb-c.h")); - } - } - case ROS2 -> { - if (!CCppMode) { - throw new UnsupportedOperationException( - "To use the ROS 2 serializer, please use the CCpp target." - ); - } - var ROSSerializer = new FedROS2CPPSerialization(); - code.pr(ROSSerializer.generatePreambleForSupport().toString()); - cMakeExtras = String.join("\n", - cMakeExtras, - ROSSerializer.generateCompilerExtensionForSupport() - ); - } +// // Perform set up that does not generate code +// protected void setUpFederateSpecificParameters(FederateInstance federate, CodeBuilder commonCode) { +// currentFederate = federate; +// if (isFederated) { +// // Reset the cmake-includes and files, to be repopulated for each federate individually. +// // This is done to enable support for separately +// // adding cmake-includes/files for different federates to prevent linking and mixing +// // all federates' supporting libraries/files together. +// targetConfig.cmakeIncludes.clear(); +// targetConfig.cmakeIncludesWithoutPath.clear(); +// targetConfig.fileNames.clear(); +// targetConfig.filesNamesWithoutPath.clear(); +// +// // Re-apply the cmake-include target property of the main .lf file. +// var target = GeneratorUtils.findTarget(mainDef.getReactorClass().eResource()); +// if (target.getConfig() != null) { +// // Update the cmake-include +// TargetProperty.updateOne( +// this.targetConfig, +// TargetProperty.CMAKE_INCLUDE, +// convertToEmptyListIfNull(target.getConfig().getPairs()), +// errorReporter +// ); +// // Update the files +// TargetProperty.updateOne( +// this.targetConfig, +// TargetProperty.FILES, +// convertToEmptyListIfNull(target.getConfig().getPairs()), +// errorReporter +// ); +// } +// // Clear out previously generated code. +// code = new CodeBuilder(commonCode); +// initializeTriggerObjects = new CodeBuilder(); +// // Enable clock synchronization if the federate +// // is not local and clock-sync is enabled +// initializeClockSynchronization(); +// startTimeStep = new CodeBuilder(); +// } +// } + + protected void handleProtoFiles() { + // Handle .proto files. + for (String file : targetConfig.protoFiles) { + this.processProtoFile(file); + var dotIndex = file.lastIndexOf("."); + var rootFilename = file; + if (dotIndex > 0) { + rootFilename = file.substring(0, dotIndex); } + code.pr("#include " + addDoubleQuotes(rootFilename + ".pb-c.h")); } } @@ -2484,16 +2065,12 @@ public String generateDirectives() { code.prComment("file:/" + FileUtil.toUnixString(fileConfig.srcFile)); code.pr(CPreambleGenerator.generateDefineDirectives( targetConfig, - federates.size(), - isFederated, fileConfig.getSrcGenPath(), - clockSyncIsOn(), hasModalReactors )); code.pr(CPreambleGenerator.generateIncludeStatements( targetConfig, - CCppMode, - isFederated + CCppMode )); return code.toString(); } @@ -2503,6 +2080,15 @@ public String generateDirectives() { */ protected String generateTopLevelPreambles() { CodeBuilder code = new CodeBuilder(); + + // preamble for federated execution setup + if (targetConfig.fedSetupPreamble != null) { + if (targetLanguageIsCpp()) code.pr("extern \"C\" {"); + code.pr("#include \"" + targetConfig.fedSetupPreamble + "\""); + if (targetLanguageIsCpp()) code.pr("}"); + } + + // user preambles if (this.mainDef != null) { var mainModel = (Model) toDefinition(mainDef.getReactorClass()).eContainer(); for (Preamble p : mainModel.getPreambles()) { @@ -2554,11 +2140,6 @@ public Target getTarget() { return Target.C; } - @Override - public String getNetworkBufferType() { - return "uint8_t*"; - } - //////////////////////////////////////////////////////////// //// Private methods @@ -2571,10 +2152,8 @@ public String getNetworkBufferType() { private void createMainReactorInstance() { if (this.mainDef != null) { if (this.main == null) { - // Recursively build instances. This is done once because - // it is the same for all federates. - this.main = new ReactorInstance(toDefinition(mainDef.getReactorClass()), errorReporter, - this.unorderedReactions); + // Recursively build instances. + this.main = new ReactorInstance(toDefinition(mainDef.getReactorClass()), errorReporter); var reactionInstanceGraph = this.main.assignLevels(); if (reactionInstanceGraph.nodeCount() > 0) { errorReporter.reportError("Main reactor has causality cycles. Skipping code generation."); @@ -2594,17 +2173,8 @@ private void createMainReactorInstance() { ); } } - - // Force reconstruction of dependence information. - if (isFederated) { - // Avoid compile errors by removing disconnected network ports. - // This must be done after assigning levels. - removeRemoteFederateConnectionPorts(main); - // There will be AST transformations that invalidate some info - // cached in ReactorInstance. - this.main.clearCaches(false); - } } + } /** @@ -2613,13 +2183,6 @@ private void createMainReactorInstance() { * @param r The reactor instance. */ private void generateSelfStructs(ReactorInstance r) { - if (!currentFederate.contains(r)) return; - // FIXME: For federated execution, if the reactor is a bank, then - // it may be that only one of the bank members is in the federate, - // but this creates an array big enough to hold all bank members. - // Fixing this will require making the functions in CUtil that - // create references to the runtime instances aware of this exception. - // For now, we just create a larger array than needed. initializeTriggerObjects.pr(CUtil.selfType(r)+"* "+CUtil.reactorRefName(r)+"["+r.getTotalWidth()+"];"); initializeTriggerObjects.pr("SUPPRESS_UNUSED_WARNING("+CUtil.reactorRefName(r)+");"); for (ReactorInstance child : r.children) { diff --git a/org.lflang/src/org/lflang/generator/c/CNetworkGenerator.java b/org.lflang/src/org/lflang/generator/c/CNetworkGenerator.java index 6d35cd6b7d..ea4252e038 100644 --- a/org.lflang/src/org/lflang/generator/c/CNetworkGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CNetworkGenerator.java @@ -1,24 +1,5 @@ package org.lflang.generator.c; -import org.lflang.federated.CGeneratorExtension; -import org.lflang.federated.FederateInstance; -import org.lflang.lf.Expression; -import org.lflang.lf.VarRef; -import org.lflang.lf.Action; -import org.lflang.lf.Port; - -import java.util.regex.Pattern; - -import org.lflang.ASTUtils; -import org.lflang.InferredType; -import org.lflang.TimeValue; -import org.lflang.TargetProperty.CoordinationType; -import org.lflang.federated.serialization.FedROS2CPPSerialization; -import org.lflang.federated.serialization.SupportedSerializers; -import org.lflang.generator.CodeBuilder; -import org.lflang.generator.GeneratorBase; -import org.lflang.generator.ReactionInstance; - /** * Generates C code to support messaging-related functionalities * in federated execution. @@ -28,333 +9,4 @@ * @author Hou Seng Wong */ public class CNetworkGenerator { - private static boolean isSharedPtrType(InferredType type, CTypes types) { - return !type.isUndefined() && sharedPointerVariable.matcher(types.getTargetType(type)).find(); - } - - // Regular expression pattern for shared_ptr types. - static final Pattern sharedPointerVariable = Pattern.compile("^(/\\*.*?\\*/)?std::shared_ptr<(?((/\\*.*?\\*/)?(\\S+))+)>$"); - - /** - * Generate code for the body of a reaction that handles the - * action that is triggered by receiving a message from a remote - * federate. - * @param action The action. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param receivingFed The destination federate. - * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. - * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. - * @param type The type. - * @param isPhysical Indicates whether or not the connection is physical - * @param serializer The serializer used on the connection. - * @param coordinationType The coordination type - */ - public static String generateNetworkReceiverBody( - Action action, - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - FederateInstance receivingFed, - int receivingBankIndex, - int receivingChannelIndex, - InferredType type, - boolean isPhysical, - SupportedSerializers serializer, - CTypes types, - CoordinationType coordinationType - ) { - // Adjust the type of the action and the receivingPort. - // If it is "string", then change it to "char*". - // This string is dynamically allocated, and type 'string' is to be - // used only for statically allocated strings. - // FIXME: Is the getTargetType method not responsible for generating the desired C code - // (e.g., char* rather than string)? If not, what exactly is that method - // responsible for? If generateNetworkReceiverBody has different requirements - // than those that the method was designed to satisfy, should we use a different - // method? The best course of action is not obvious, but we have a pattern of adding - // downstream patches to generated strings rather than fixing them at their source. - if (types.getTargetType(action).equals("string")) { - action.getType().setCode(null); - action.getType().setId("char*"); - } - if (types.getTargetType((Port) receivingPort.getVariable()).equals("string")) { - ((Port) receivingPort.getVariable()).getType().setCode(null); - ((Port) receivingPort.getVariable()).getType().setId("char*"); - } - var receiveRef = CUtil.portRefInReaction(receivingPort, receivingBankIndex, receivingChannelIndex); - var result = new CodeBuilder(); - // We currently have no way to mark a reaction "unordered" - // in the AST, so we use a magic string at the start of the body. - result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER); - // Transfer the physical time of arrival from the action to the port - result.pr(receiveRef+"->physical_time_of_arrival = self->_lf__"+action.getName()+".physical_time_of_arrival;"); - if (coordinationType == CoordinationType.DECENTRALIZED && !isPhysical) { - // Transfer the intended tag. - result.pr(receiveRef+"->intended_tag = self->_lf__"+action.getName()+".intended_tag;\n"); - } - var value = ""; - switch (serializer) { - case NATIVE: { - // NOTE: Docs say that malloc'd char* is freed on conclusion of the time step. - // So passing it downstream should be OK. - value = action.getName()+"->value"; - if (CUtil.isTokenType(type, types)) { - result.pr("lf_set_token("+receiveRef+", "+action.getName()+"->token);"); - } else { - result.pr("lf_set("+receiveRef+", "+value+");"); - } - break; - } - case PROTO: { - throw new UnsupportedOperationException("Protobuf serialization is not supported yet."); - } - case ROS2: { - var portType = ASTUtils.getInferredType(((Port) receivingPort.getVariable())); - var portTypeStr = types.getTargetType(portType); - if (CUtil.isTokenType(portType, types)) { - throw new UnsupportedOperationException("Cannot handle ROS serialization when ports are pointers."); - } else if (isSharedPtrType(portType, types)) { - var matcher = sharedPointerVariable.matcher(portTypeStr); - if (matcher.find()) { - portTypeStr = matcher.group("type"); - } - } - var ROSDeserializer = new FedROS2CPPSerialization(); - value = FedROS2CPPSerialization.deserializedVarName; - result.pr( - ROSDeserializer.generateNetworkDeserializerCode( - "self->_lf__"+action.getName(), - portTypeStr - ) - ); - if (isSharedPtrType(portType, types)) { - result.pr("auto msg_shared_ptr = std::make_shared<"+portTypeStr+">("+value+");"); - result.pr("lf_set("+receiveRef+", msg_shared_ptr);"); - } else { - result.pr("lf_set("+receiveRef+", std::move("+value+"));"); - } - break; - } - } - return result.toString(); - } - - /** - * Generate code for the body of a reaction that handles an output - * that is to be sent over the network. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The variable reference to the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. - * @param receivingFed The destination federate. - * @param type The type. - * @param isPhysical Indicates whether the connection is physical or not - * @param delay The delay value imposed on the connection using after - * @param serializer The serializer used on the connection. - */ - public static String generateNetworkSenderBody( - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - int sendingBankIndex, - int sendingChannelIndex, - FederateInstance receivingFed, - InferredType type, - boolean isPhysical, - Expression delay, - SupportedSerializers serializer, - CTypes types, - CoordinationType coordinationType - ) { - var sendRef = CUtil.portRefInReaction(sendingPort, sendingBankIndex, sendingChannelIndex); - var receiveRef = ASTUtils.generateVarRef(receivingPort); // Used for comments only, so no need for bank/multiport index. - var result = new CodeBuilder(); - - // We currently have no way to mark a reaction "unordered" - // in the AST, so we use a magic string at the start of the body. - result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); - - result.pr("// Sending from "+sendRef+" in federate "+sendingFed.name+" to "+receiveRef+" in federate "+receivingFed.name); - - // In case sendRef is a multiport or is in a bank, this reaction will be triggered when any channel or bank index of sendRef is present - // ex. if a.out[i] is present, the entire output a.out is triggered. - if (sendingBankIndex != -1 || sendingChannelIndex != -1) { - result.pr("if (!"+sendRef+"->is_present) return;"); - } - - // If the connection is physical and the receiving federate is remote, send it directly on a socket. - // If the connection is logical and the coordination mode is centralized, send via RTI. - // If the connection is logical and the coordination mode is decentralized, send directly - String messageType; - // Name of the next immediate destination of this message - var next_destination_name = "\"federate "+receivingFed.id+"\""; - - // Get the delay literal - String additionalDelayString = CGeneratorExtension.getNetworkDelayLiteral(delay); - - if (isPhysical) { - messageType = "MSG_TYPE_P2P_MESSAGE"; - } else if (coordinationType == CoordinationType.DECENTRALIZED) { - messageType = "MSG_TYPE_P2P_TAGGED_MESSAGE"; - } else { - // Logical connection - // Send the message via rti - messageType = "MSG_TYPE_TAGGED_MESSAGE"; - next_destination_name = "\"federate "+receivingFed.id+" via the RTI\""; - } - - - String sendingFunction = "send_timed_message"; - String commonArgs = String.join(", ", - additionalDelayString, - messageType, - receivingPortID + "", - receivingFed.id + "", - next_destination_name, - "message_length" - ); - if (isPhysical) { - // Messages going on a physical connection do not - // carry a timestamp or require the delay; - sendingFunction = "send_message"; - commonArgs = messageType+", "+receivingPortID+", "+receivingFed.id+", "+next_destination_name+", message_length"; - } - - var lengthExpression = ""; - var pointerExpression = ""; - switch (serializer) { - case NATIVE: { - // Handle native types. - if (CUtil.isTokenType(type, types)) { - // NOTE: Transporting token types this way is likely to only work if the sender and receiver - // both have the same endianness. Otherwise, you have to use protobufs or some other serialization scheme. - result.pr("size_t message_length = "+sendRef+"->token->length * "+sendRef+"->token->element_size;"); - result.pr(sendingFunction+"("+commonArgs+", (unsigned char*) "+sendRef+"->value);"); - } else { - // string types need to be dealt with specially because they are hidden pointers. - // void type is odd, but it avoids generating non-standard expression sizeof(void), - // which some compilers reject. - lengthExpression = "sizeof("+types.getTargetType(type)+")"; - pointerExpression = "(unsigned char*)&"+sendRef+"->value"; - var targetType = types.getTargetType(type); - if (targetType.equals("string")) { - lengthExpression = "strlen("+sendRef+"->value) + 1"; - pointerExpression = "(unsigned char*) "+sendRef+"->value"; - } else if (targetType.equals("void")) { - lengthExpression = "0"; - } - result.pr("size_t message_length = "+lengthExpression+";"); - result.pr(sendingFunction+"("+commonArgs+", "+pointerExpression+");"); - } - break; - } - case PROTO: { - throw new UnsupportedOperationException("Protobuf serialization is not supported yet."); - } - case ROS2: { - var variableToSerialize = sendRef; - var typeStr = types.getTargetType(type); - if (CUtil.isTokenType(type, types)) { - throw new UnsupportedOperationException("Cannot handle ROS serialization when ports are pointers."); - } else if (isSharedPtrType(type, types)) { - var matcher = sharedPointerVariable.matcher(typeStr); - if (matcher.find()) { - typeStr = matcher.group("type"); - } - } - var ROSSerializer = new FedROS2CPPSerialization(); - lengthExpression = ROSSerializer.serializedBufferLength(); - pointerExpression = ROSSerializer.seializedBufferVar(); - result.pr( - ROSSerializer.generateNetworkSerializerCode(variableToSerialize, typeStr, isSharedPtrType(type, types)) - ); - result.pr("size_t message_length = "+lengthExpression+";"); - result.pr(sendingFunction+"("+commonArgs+", "+pointerExpression+");"); - break; - } - - } - return result.toString(); - } - - /** - * Generate code for the body of a reaction that decides whether the trigger for the given - * port is going to be present or absent for the current logical time. - * This reaction is put just before the first reaction that is triggered by the network - * input port "port" or has it in its sources. If there are only connections to contained - * reactors, in the top-level reactor. - * - * @param receivingPortID The port to generate the control reaction for - * @param maxSTP The maximum value of STP is assigned to reactions (if any) - * that have port as their trigger or source - */ - public static String generateNetworkInputControlReactionBody( - int receivingPortID, - TimeValue maxSTP, - boolean isFederatedAndDecentralized - ) { - // Store the code - var result = new CodeBuilder(); - - // We currently have no way to mark a reaction "unordered" - // in the AST, so we use a magic string at the start of the body. - result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); - result.pr("interval_t max_STP = 0LL;"); - - // Find the maximum STP for decentralized coordination - if(isFederatedAndDecentralized) { - result.pr("max_STP = "+GeneratorBase.timeInTargetLanguage(maxSTP)+";"); - } - result.pr("// Wait until the port status is known"); - result.pr("wait_until_port_status_known("+receivingPortID+", max_STP);"); - return result.toString(); - } - - /** - * Generate code for the body of a reaction that sends a port status message for the given - * port if it is absent. - * - * @param port The port to generate the control reaction for - * @param portID The ID assigned to the port in the AST transformation - * @param receivingFederateID The ID of the receiving federate - * @param sendingBankIndex The bank index of the sending federate, if it is in a bank. - * @param sendingChannelIndex The channel if a multiport - * @param delay The delay value imposed on the connection using after - */ - public static String generateNetworkOutputControlReactionBody( - VarRef port, - int portID, - int receivingFederateID, - int sendingBankIndex, - int sendingChannelIndex, - Expression delay - ) { - // Store the code - var result = new CodeBuilder(); - // We currently have no way to mark a reaction "unordered" - // in the AST, so we use a magic string at the start of the body. - result.pr("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); - var sendRef = CUtil.portRefInReaction(port, sendingBankIndex, sendingChannelIndex); - // Get the delay literal - var additionalDelayString = CGeneratorExtension.getNetworkDelayLiteral(delay); - result.pr(String.join("\n", - "// If the output port has not been lf_set for the current logical time,", - "// send an ABSENT message to the receiving federate ", - "LF_PRINT_LOG(\"Contemplating whether to send port \"", - " \"absent for port %d to federate %d.\", ", - " "+portID+", "+receivingFederateID+");", - "if ("+sendRef+" == NULL || !"+sendRef+"->is_present) {", - " // The output port is NULL or it is not present.", - " send_port_absent_to_federate("+additionalDelayString+", "+portID+", "+receivingFederateID+");", - "}" - )); - return result.toString(); - } } diff --git a/org.lflang/src/org/lflang/generator/c/CPreambleGenerator.java b/org.lflang/src/org/lflang/generator/c/CPreambleGenerator.java index 39c0a800af..b2059ace61 100644 --- a/org.lflang/src/org/lflang/generator/c/CPreambleGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CPreambleGenerator.java @@ -34,8 +34,7 @@ public class CPreambleGenerator { /** Add necessary source files specific to the target language. */ public static String generateIncludeStatements( TargetConfig targetConfig, - boolean cppMode, - boolean isFederated + boolean cppMode ) { var tracing = targetConfig.tracing; CodeBuilder code = new CodeBuilder(); @@ -55,9 +54,6 @@ public static String generateIncludeStatements( if (targetConfig.threading) { code.pr("#include \"core/threaded/scheduler.h\""); } - if (isFederated) { - code.pr("#include \"core/federated/federate.c\""); - } if (tracing != null) { code.pr("#include \"core/trace.h\""); } @@ -72,37 +68,36 @@ public static String generateIncludeStatements( public static String generateDefineDirectives( TargetConfig targetConfig, - int numFederates, - boolean isFederated, Path srcGenPath, - boolean clockSyncIsOn, boolean hasModalReactors ) { int logLevel = targetConfig.logLevel.ordinal(); var coordinationType = targetConfig.coordination; - var advanceMessageInterval = targetConfig.coordinationOptions.advance_message_interval; var tracing = targetConfig.tracing; CodeBuilder code = new CodeBuilder(); // TODO: Get rid of all of these code.pr("#define LOG_LEVEL " + logLevel); code.pr("#define TARGET_FILES_DIRECTORY " + addDoubleQuotes(srcGenPath.toString())); - if (isFederated) { - code.pr("#define NUMBER_OF_FEDERATES " + numFederates); - code.pr(generateFederatedDefineDirective(coordinationType)); - if (advanceMessageInterval != null) { - code.pr("#define ADVANCE_MESSAGE_INTERVAL " + - GeneratorBase.timeInTargetLanguage(advanceMessageInterval)); - } + + if (targetConfig.platformOptions.platform == Platform.ARDUINO) { + code.pr("#define MICROSECOND_TIME"); + code.pr("#define BIT_32"); } + if (tracing != null) { targetConfig.compileDefinitions.put("LF_TRACE", tracing.traceFileName); } - if (clockSyncIsOn) { - code.pr(generateClockSyncDefineDirective( - targetConfig.clockSync, - targetConfig.clockSyncOptions - )); + // if (clockSyncIsOn) { + // code.pr(generateClockSyncDefineDirective( + // targetConfig.clockSync, + // targetConfig.clockSyncOptions + // )); + // } + if (targetConfig.threading) { + targetConfig.compileDefinitions.put("LF_THREADED", "1"); + } else { + targetConfig.compileDefinitions.put("LF_UNTHREADED", "1"); } if (targetConfig.threading) { targetConfig.compileDefinitions.put("LF_THREADED", "1"); @@ -112,47 +107,4 @@ public static String generateDefineDirectives( code.newLine(); return code.toString(); } - - /** - * Returns the #define directive for the given coordination type. - * - * NOTE: Instead of checking #ifdef FEDERATED, we could - * use #if (NUMBER_OF_FEDERATES > 1). - * To Soroush Bateni, the former is more accurate. - */ - private static String generateFederatedDefineDirective(CoordinationType coordinationType) { - List directives = new ArrayList<>(); - directives.add("#define FEDERATED"); - if (coordinationType == CoordinationType.CENTRALIZED) { - directives.add("#define FEDERATED_CENTRALIZED"); - } else if (coordinationType == CoordinationType.DECENTRALIZED) { - directives.add("#define FEDERATED_DECENTRALIZED"); - } - return String.join("\n", directives); - } - - /** - * Initialize clock synchronization (if enabled) and its related options for a given federate. - * - * Clock synchronization can be enabled using the clock-sync target property. - * @see Documentation - */ - private static String generateClockSyncDefineDirective( - ClockSyncMode mode, - ClockSyncOptions options - ) { - List code = new ArrayList<>(List.of( - "#define _LF_CLOCK_SYNC_INITIAL", - "#define _LF_CLOCK_SYNC_PERIOD_NS " + GeneratorBase.timeInTargetLanguage(options.period), - "#define _LF_CLOCK_SYNC_EXCHANGES_PER_INTERVAL " + options.trials, - "#define _LF_CLOCK_SYNC_ATTENUATION " + options.attenuation - )); - if (mode == ClockSyncMode.ON) { - code.add("#define _LF_CLOCK_SYNC_ON"); - if (options.collectStats) { - code.add("#define _LF_CLOCK_SYNC_COLLECT_STATS"); - } - } - return String.join("\n", code); - } } diff --git a/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java b/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java index cac6594edc..3771109e4b 100644 --- a/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java @@ -13,8 +13,8 @@ import org.lflang.ASTUtils; import org.lflang.ErrorReporter; import org.lflang.InferredType; -import org.lflang.federated.CGeneratorExtension; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.extensions.CExtensionUtils; +import org.lflang.federated.generator.FederateInstance; import org.lflang.generator.CodeBuilder; import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; @@ -53,7 +53,6 @@ public static String generateInitializationForReaction(String body, CTypes types, ErrorReporter errorReporter, Instantiation mainDef, - boolean isFederatedAndDecentralized, boolean requiresTypes) { Reactor reactor = ASTUtils.toDefinition(decl); @@ -689,13 +688,10 @@ public static String generateOutputVariablesInReaction( * @param constructorCode The place to put the constructor code. */ public static void generateReactionAndTriggerStructs( - FederateInstance currentFederate, CodeBuilder body, ReactorDecl decl, CodeBuilder constructorCode, - CTypes types, - boolean isFederated, - boolean isFederatedAndDecentralized + CTypes types ) { var reactionCount = 0; var reactor = ASTUtils.toDefinition(decl); @@ -712,82 +708,79 @@ public static void generateReactionAndTriggerStructs( var shutdownReactions = new LinkedHashSet(); var resetReactions = new LinkedHashSet(); for (Reaction reaction : ASTUtils.allReactions(reactor)) { - if (currentFederate.contains(reaction)) { - // Create the reaction_t struct. - body.pr(reaction, "reaction_t _lf__reaction_"+reactionCount+";"); + // Create the reaction_t struct. + body.pr(reaction, "reaction_t _lf__reaction_"+reactionCount+";"); - // Create the map of triggers to reactions. - for (TriggerRef trigger : reaction.getTriggers()) { - // trigger may not be a VarRef (it could be "startup" or "shutdown"). - if (trigger instanceof VarRef) { - var triggerAsVarRef = (VarRef) trigger; - var reactionList = triggerMap.get(triggerAsVarRef.getVariable()); - if (reactionList == null) { - reactionList = new LinkedList<>(); - triggerMap.put(triggerAsVarRef.getVariable(), reactionList); - } - reactionList.add(reactionCount); - if (triggerAsVarRef.getContainer() != null) { - outputsOfContainedReactors.put(triggerAsVarRef.getVariable(), triggerAsVarRef.getContainer()); - } - } else if (trigger instanceof BuiltinTriggerRef) { - switch(((BuiltinTriggerRef) trigger).getType()) { - case STARTUP: - startupReactions.add(reactionCount); - break; - case SHUTDOWN: - shutdownReactions.add(reactionCount); - break; - case RESET: - resetReactions.add(reactionCount); - break; - } + // Create the map of triggers to reactions. + for (TriggerRef trigger : reaction.getTriggers()) { + // trigger may not be a VarRef (it could be "startup" or "shutdown"). + if (trigger instanceof VarRef) { + var triggerAsVarRef = (VarRef) trigger; + var reactionList = triggerMap.get(triggerAsVarRef.getVariable()); + if (reactionList == null) { + reactionList = new LinkedList<>(); + triggerMap.put(triggerAsVarRef.getVariable(), reactionList); } - } - // Create the set of sources read but not triggering. - for (VarRef source : reaction.getSources()) { - sourceSet.add(source.getVariable()); - if (source.getContainer() != null) { - outputsOfContainedReactors.put(source.getVariable(), source.getContainer()); + reactionList.add(reactionCount); + if (triggerAsVarRef.getContainer() != null) { + outputsOfContainedReactors.put(triggerAsVarRef.getVariable(), triggerAsVarRef.getContainer()); + } + } else if (trigger instanceof BuiltinTriggerRef) { + switch(((BuiltinTriggerRef) trigger).getType()) { + case STARTUP: + startupReactions.add(reactionCount); + break; + case SHUTDOWN: + shutdownReactions.add(reactionCount); + break; + case RESET: + resetReactions.add(reactionCount); + break; } } - - var deadlineFunctionPointer = "NULL"; - if (reaction.getDeadline() != null) { - // The following has to match the name chosen in generateReactions - var deadlineFunctionName = generateDeadlineFunctionName(decl, reactionCount); - deadlineFunctionPointer = "&" + deadlineFunctionName; - } - - // Assign the STP handler - var STPFunctionPointer = "NULL"; - if (reaction.getStp() != null) { - // The following has to match the name chosen in generateReactions - var STPFunctionName = generateStpFunctionName(decl, reactionCount); - STPFunctionPointer = "&" + STPFunctionName; + } + // Create the set of sources read but not triggering. + for (VarRef source : reaction.getSources()) { + sourceSet.add(source.getVariable()); + if (source.getContainer() != null) { + outputsOfContainedReactors.put(source.getVariable(), source.getContainer()); } + } - // Set the defaults of the reaction_t struct in the constructor. - // Since the self struct is allocated using calloc, there is no need to set: - // self->_lf__reaction_"+reactionCount+".index = 0; - // self->_lf__reaction_"+reactionCount+".chain_id = 0; - // self->_lf__reaction_"+reactionCount+".pos = 0; - // self->_lf__reaction_"+reactionCount+".status = inactive; - // self->_lf__reaction_"+reactionCount+".deadline = 0LL; - // self->_lf__reaction_"+reactionCount+".is_STP_violated = false; - constructorCode.pr(reaction, String.join("\n", - "self->_lf__reaction_"+reactionCount+".number = "+reactionCount+";", - "self->_lf__reaction_"+reactionCount+".function = "+ CReactionGenerator.generateReactionFunctionName(decl, reactionCount)+";", - "self->_lf__reaction_"+reactionCount+".self = self;", - "self->_lf__reaction_"+reactionCount+".deadline_violation_handler = "+deadlineFunctionPointer+";", - "self->_lf__reaction_"+reactionCount+".STP_handler = "+STPFunctionPointer+";", - "self->_lf__reaction_"+reactionCount+".name = "+addDoubleQuotes("?")+";", - (reaction.eContainer() instanceof Mode ? - "self->_lf__reaction_"+reactionCount+".mode = &self->_lf__modes["+reactor.getModes().indexOf((Mode) reaction.eContainer())+"];" : - "self->_lf__reaction_"+reactionCount+".mode = NULL;") - )); + var deadlineFunctionPointer = "NULL"; + if (reaction.getDeadline() != null) { + // The following has to match the name chosen in generateReactions + var deadlineFunctionName = generateDeadlineFunctionName(decl, reactionCount); + deadlineFunctionPointer = "&" + deadlineFunctionName; + } + // Assign the STP handler + var STPFunctionPointer = "NULL"; + if (reaction.getStp() != null) { + // The following has to match the name chosen in generateReactions + var STPFunctionName = generateStpFunctionName(decl, reactionCount); + STPFunctionPointer = "&" + STPFunctionName; } + + // Set the defaults of the reaction_t struct in the constructor. + // Since the self struct is allocated using calloc, there is no need to set: + // self->_lf__reaction_"+reactionCount+".index = 0; + // self->_lf__reaction_"+reactionCount+".chain_id = 0; + // self->_lf__reaction_"+reactionCount+".pos = 0; + // self->_lf__reaction_"+reactionCount+".status = inactive; + // self->_lf__reaction_"+reactionCount+".deadline = 0LL; + // self->_lf__reaction_"+reactionCount+".is_STP_violated = false; + constructorCode.pr(reaction, String.join("\n", + "self->_lf__reaction_"+reactionCount+".number = "+reactionCount+";", + "self->_lf__reaction_"+reactionCount+".function = "+CReactionGenerator.generateReactionFunctionName(decl, reactionCount)+";", + "self->_lf__reaction_"+reactionCount+".self = self;", + "self->_lf__reaction_"+reactionCount+".deadline_violation_handler = "+deadlineFunctionPointer+";", + "self->_lf__reaction_"+reactionCount+".STP_handler = "+STPFunctionPointer+";", + "self->_lf__reaction_"+reactionCount+".name = "+addDoubleQuotes("?")+";", + (reaction.eContainer() instanceof Mode ? + "self->_lf__reaction_"+reactionCount+".mode = &self->_lf__modes["+reactor.getModes().indexOf((Mode) reaction.eContainer())+"];" : + "self->_lf__reaction_"+reactionCount+".mode = NULL;") + )); // Increment the reactionCount even if the reaction is not in the federate // so that reaction indices are consistent across federates. reactionCount++; @@ -796,60 +789,57 @@ public static void generateReactionAndTriggerStructs( // Next, create and initialize the trigger_t objects. // Start with the timers. for (Timer timer : ASTUtils.allTimers(reactor)) { - createTriggerT(body, timer, triggerMap, constructorCode, types, isFederated, isFederatedAndDecentralized); + createTriggerT(body, timer, triggerMap, constructorCode, types); // Since the self struct is allocated using calloc, there is no need to set: // self->_lf__"+timer.name+".is_physical = false; // self->_lf__"+timer.name+".drop = false; // self->_lf__"+timer.name+".element_size = 0; constructorCode.pr("self->_lf__"+timer.getName()+".is_timer = true;"); - if (isFederatedAndDecentralized) { - constructorCode.pr("self->_lf__"+timer.getName()+".intended_tag = (tag_t) { .time = NEVER, .microstep = 0u};"); - } + constructorCode.pr(CExtensionUtils.surroundWithIfFederatedDecentralized( + "self->_lf__"+timer.getName()+".intended_tag = (tag_t) { .time = NEVER, .microstep = 0u};")); } // Handle builtin triggers. if (startupReactions.size() > 0) { - generateBuiltinTriggerdReactionsArray(startupReactions, "startup", body, constructorCode, isFederatedAndDecentralized); + generateBuiltinTriggeredReactionsArray(startupReactions, "startup", body, constructorCode); } // Handle shutdown triggers. if (shutdownReactions.size() > 0) { - generateBuiltinTriggerdReactionsArray(shutdownReactions, "shutdown", body, constructorCode, isFederatedAndDecentralized); + generateBuiltinTriggeredReactionsArray(shutdownReactions, "shutdown", body, constructorCode); } if (resetReactions.size() > 0) { - generateBuiltinTriggerdReactionsArray(resetReactions, "reset", body, constructorCode, isFederatedAndDecentralized); + generateBuiltinTriggeredReactionsArray(resetReactions, "reset", body, constructorCode); } // Next handle actions. for (Action action : ASTUtils.allActions(reactor)) { - if (currentFederate.contains(action)) { - createTriggerT(body, action, triggerMap, constructorCode, types, isFederated, isFederatedAndDecentralized); - var isPhysical = "true"; - if (action.getOrigin().equals(ActionOrigin.LOGICAL)) { - isPhysical = "false"; - } - var elementSize = "0"; - // If the action type is 'void', we need to avoid generating the code - // 'sizeof(void)', which some compilers reject. - var rootType = action.getType() != null ? CUtil.rootType(types.getTargetType(action)) : null; - if (rootType != null && !rootType.equals("void")) { - elementSize = "sizeof("+rootType+")"; - } - - // Since the self struct is allocated using calloc, there is no need to set: - // self->_lf__"+action.getName()+".is_timer = false; - constructorCode.pr(String.join("\n", - "self->_lf__"+action.getName()+".is_physical = "+isPhysical+";", - (!(action.getPolicy() == null || action.getPolicy().isEmpty()) ? - "self->_lf__"+action.getName()+".policy = "+action.getPolicy()+";" : - ""), - "self->_lf__"+action.getName()+".element_size = "+elementSize+";" - )); + createTriggerT(body, action, triggerMap, constructorCode, types); + var isPhysical = "true"; + if (action.getOrigin().equals(ActionOrigin.LOGICAL)) { + isPhysical = "false"; + } + var elementSize = "0"; + // If the action type is 'void', we need to avoid generating the code + // 'sizeof(void)', which some compilers reject. + var rootType = action.getType() != null ? CUtil.rootType(types.getTargetType(action)) : null; + if (rootType != null && !rootType.equals("void")) { + elementSize = "sizeof("+rootType+")"; } + + // Since the self struct is allocated using calloc, there is no need to set: + // self->_lf__"+action.getName()+".is_timer = false; + constructorCode.pr(String.join("\n", + "self->_lf__"+action.getName()+".is_physical = "+isPhysical+";", + !(action.getPolicy() == null || action.getPolicy().isEmpty()) ? + "self->_lf__"+action.getName()+".policy = "+action.getPolicy()+";" : + "", + "self->_lf__"+action.getName()+".element_size = "+elementSize+";" + )); } // Next handle inputs. for (Input input : ASTUtils.allInputs(reactor)) { - createTriggerT(body, input, triggerMap, constructorCode, types, isFederated, isFederatedAndDecentralized); + createTriggerT(body, input, triggerMap, constructorCode, types); } } @@ -868,17 +858,14 @@ private static void createTriggerT( Variable variable, LinkedHashMap> triggerMap, CodeBuilder constructorCode, - CTypes types, - boolean isFederated, - boolean isFederatedAndDecentralized + CTypes types ) { var varName = variable.getName(); // variable is a port, a timer, or an action. body.pr(variable, "trigger_t _lf__"+varName+";"); constructorCode.pr(variable, "self->_lf__"+varName+".last = NULL;"); - if (isFederatedAndDecentralized) { - constructorCode.pr(variable, "self->_lf__"+varName+".intended_tag = (tag_t) { .time = NEVER, .microstep = 0u};"); - } + constructorCode.pr(variable, CExtensionUtils.surroundWithIfFederatedDecentralized( + "self->_lf__"+varName+".intended_tag = (tag_t) { .time = NEVER, .microstep = 0u};")); // Generate the reactions triggered table. var reactionsTriggered = triggerMap.get(variable); @@ -896,10 +883,9 @@ private static void createTriggerT( "self->_lf__"+varName+".number_of_reactions = "+count+";" )); - if (isFederated) { - // Set the physical_time_of_arrival - constructorCode.pr(variable, "self->_lf__"+varName+".physical_time_of_arrival = NEVER;"); - } + // If federated, set the physical_time_of_arrival + constructorCode.pr(variable, CExtensionUtils.surroundWithIfFederated( + "self->_lf__"+varName+".physical_time_of_arrival = NEVER;")); } if (variable instanceof Input) { var rootType = CUtil.rootType(types.getTargetType((Input) variable)); @@ -913,28 +899,26 @@ private static void createTriggerT( // 'sizeof(void)', which some compilers reject. var size = (rootType.equals("void")) ? "0" : "sizeof("+rootType+")"; constructorCode.pr("self->_lf__"+varName+".element_size = "+size+";"); - if (isFederated) { - body.pr( - CGeneratorExtension.createPortStatusFieldForInput((Input) variable) - ); - } + body.pr( + CExtensionUtils.surroundWithIfFederated( + CExtensionUtils.createPortStatusFieldForInput((Input) variable) + ) + ); } } - public static void generateBuiltinTriggerdReactionsArray( + public static void generateBuiltinTriggeredReactionsArray( Set reactions, String name, CodeBuilder body, - CodeBuilder constructorCode, - boolean isFederatedAndDecentralized + CodeBuilder constructorCode ) { body.pr(String.join("\n", "trigger_t _lf__"+name+";", "reaction_t* _lf__"+name+"_reactions["+reactions.size()+"];" )); - if (isFederatedAndDecentralized) { - constructorCode.pr("self->_lf__"+name+".intended_tag = (tag_t) { .time = NEVER, .microstep = 0u};"); - } + constructorCode.pr(CExtensionUtils.surroundWithIfFederatedDecentralized( + "self->_lf__"+name+".intended_tag = (tag_t) { .time = NEVER, .microstep = 0u};")); var i = 0; for (Integer reactionIndex : reactions) { constructorCode.pr("self->_lf__"+name+"_reactions["+i+++"] = &self->_lf__reaction_"+reactionIndex+";"); @@ -1079,7 +1063,6 @@ public static String generateReaction( Instantiation mainDef, ErrorReporter errorReporter, CTypes types, - boolean isFederatedAndDecentralized, boolean requiresType ) { var code = new CodeBuilder(); @@ -1087,7 +1070,6 @@ public static String generateReaction( String init = generateInitializationForReaction( body, reaction, decl, reactionIndex, types, errorReporter, mainDef, - isFederatedAndDecentralized, requiresType); code.pr( "#include " + StringUtil.addDoubleQuotes( @@ -1138,7 +1120,7 @@ public static String generateFunction(String header, String init, Code code) { * @param reactionIndex The number assigned to this reaction deadline */ public static String generateDeadlineFunctionName(ReactorDecl decl, int reactionIndex) { - return decl.getName().toLowerCase() + "_deadline_function" + reactionIndex; + return CUtil.getName(decl).toLowerCase() + "_deadline_function" + reactionIndex; } /** @@ -1149,7 +1131,7 @@ public static String generateDeadlineFunctionName(ReactorDecl decl, int reaction * @return The function name for the reaction. */ public static String generateReactionFunctionName(ReactorDecl reactor, int reactionIndex) { - return reactor.getName().toLowerCase() + "reaction_function_" + reactionIndex; + return CUtil.getName(reactor).toLowerCase() + "reaction_function_" + reactionIndex; } /** @@ -1158,7 +1140,7 @@ public static String generateReactionFunctionName(ReactorDecl reactor, int react * @param reactionIndex The number assigned to this reaction deadline */ public static String generateStpFunctionName(ReactorDecl decl, int reactionIndex) { - return decl.getName().toLowerCase() + "_STP_function" + reactionIndex; + return CUtil.getName(decl).toLowerCase() + "_STP_function" + reactionIndex; } /** Return the top level C function header for the deadline function numbered "reactionIndex" in "decl" diff --git a/org.lflang/src/org/lflang/generator/c/CTimerGenerator.java b/org.lflang/src/org/lflang/generator/c/CTimerGenerator.java index a196823473..e0c2000e87 100644 --- a/org.lflang/src/org/lflang/generator/c/CTimerGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CTimerGenerator.java @@ -63,7 +63,7 @@ public static String generateLfInitializeTimer(int timerCount) { if (_lf_timer_triggers[i] != NULL) { _lf_initialize_timer(_lf_timer_triggers[i]); } - }""".indent(4) : + }""".indent(4).stripTrailing() : "", "}" ); diff --git a/org.lflang/src/org/lflang/generator/c/CTracingGenerator.java b/org.lflang/src/org/lflang/generator/c/CTracingGenerator.java index 765a1e6ebb..f999d14b3e 100644 --- a/org.lflang/src/org/lflang/generator/c/CTracingGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CTracingGenerator.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FederateInstance; import org.lflang.generator.ActionInstance; import org.lflang.generator.ReactorInstance; import org.lflang.generator.TimerInstance; @@ -27,11 +27,9 @@ public class CTracingGenerator { * the header information in the trace file. * * @param instance The reactor instance. - * @param currentFederate The federate instance we are generating code for. */ public static String generateTraceTableEntries( - ReactorInstance instance, - FederateInstance currentFederate + ReactorInstance instance ) { List code = new ArrayList<>(); var description = CUtil.getShortenedName(instance); @@ -41,20 +39,16 @@ public static String generateTraceTableEntries( "trace_reactor", description) ); for (ActionInstance action : instance.actions) { - if (currentFederate.contains(action.getDefinition())) { - code.add(registerTraceEvent( - selfStruct, getTrigger(selfStruct, action.getName()), - "trace_trigger", description + "." + action.getName()) - ); - } + code.add(registerTraceEvent( + selfStruct, getTrigger(selfStruct, action.getName()), + "trace_trigger", description + "." + action.getName()) + ); } for (TimerInstance timer : instance.timers) { - if (currentFederate.contains(timer.getDefinition())) { - code.add(registerTraceEvent( - selfStruct, getTrigger(selfStruct, timer.getName()), - "trace_trigger", description + "." + timer.getName()) - ); - } + code.add(registerTraceEvent( + selfStruct, getTrigger(selfStruct, timer.getName()), + "trace_trigger", description + "." + timer.getName()) + ); } return String.join("\n", code); } diff --git a/org.lflang/src/org/lflang/generator/c/CTriggerObjectsGenerator.java b/org.lflang/src/org/lflang/generator/c/CTriggerObjectsGenerator.java index 74a4f33ed4..cf8f6930ce 100644 --- a/org.lflang/src/org/lflang/generator/c/CTriggerObjectsGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CTriggerObjectsGenerator.java @@ -20,8 +20,8 @@ import org.lflang.TimeValue; import org.lflang.TargetProperty.CoordinationType; import org.lflang.TargetProperty.LogLevel; -import org.lflang.federated.CGeneratorExtension; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.extensions.CExtensionUtils; +import org.lflang.federated.generator.FederateInstance; import org.lflang.generator.CodeBuilder; import org.lflang.generator.GeneratorBase; import org.lflang.generator.ParameterInstance; @@ -45,19 +45,15 @@ public class CTriggerObjectsGenerator { * Generate the _lf_initialize_trigger_objects function for 'federate'. */ public static String generateInitializeTriggerObjects( - FederateInstance federate, ReactorInstance main, TargetConfig targetConfig, CodeBuilder initializeTriggerObjects, CodeBuilder startTimeStep, CTypes types, String lfModuleName, - LinkedHashMap federationRTIProperties, int startTimeStepTokens, int startTimeStepIsPresentCount, - boolean isFederated, - boolean isFederatedAndDecentralized, - boolean clockSyncIsOn + int startupReactionCount ) { var code = new CodeBuilder(); code.pr("void _lf_initialize_trigger_objects() {"); @@ -73,10 +69,6 @@ public static String generateInitializeTriggerObjects( var traceFileName = lfModuleName; if (targetConfig.tracing.traceFileName != null) { traceFileName = targetConfig.tracing.traceFileName; - // Since all federates would have the same name, we need to append the federate name. - if (isFederated) { - traceFileName += "_" + federate.name; - } } code.pr(String.join("\n", "// Initialize tracing", @@ -107,40 +99,42 @@ public static String generateInitializeTriggerObjects( )); } - // Allocate the memory for triggers used in federated execution - code.pr(CGeneratorExtension.allocateTriggersForFederate(federate, startTimeStepIsPresentCount, isFederated, isFederatedAndDecentralized)); + // Create the table to initialize intended tag fields to 0 between time + // steps. + if (startTimeStepIsPresentCount > 0) { + // Allocate the initial (before mutations) array of pointers to + // intended_tag fields. + // There is a 1-1 map between structs containing is_present and + // intended_tag fields, + // thus, we reuse startTimeStepIsPresentCount as the counter. + code.pr(String.join("\n", + CExtensionUtils.surroundWithIfFederatedDecentralized(""" + // Create the array that will contain pointers to intended_tag fields to reset on each step. + _lf_intended_tag_fields_size = %s; + _lf_intended_tag_fields = (tag_t**)malloc(_lf_intended_tag_fields_size * sizeof(tag_t*)); + """.formatted(startTimeStepIsPresentCount) + ) + )); + } - code.pr(initializeTriggerObjects.toString()); - // Assign appropriate pointers to the triggers - // FIXME: For python target, almost surely in the wrong place. - code.pr(CGeneratorExtension.initializeTriggerForControlReactions(main, main, federate)); - var reactionsInFederate = Iterables.filter( - main.reactions, - r -> federate.contains(r.getDefinition()) - ); + code.pr(initializeTriggerObjects.toString()); code.pr(deferredInitialize( - federate, main, - reactionsInFederate, + main.reactions, targetConfig, - types, - isFederated + types )); code.pr(deferredInitializeNonNested( - federate, main, main, - reactionsInFederate, - isFederated + main.reactions )); // Next, for every input port, populate its "self" struct // fields with pointers to the output port that sends it data. code.pr(deferredConnectInputsToOutputs( - federate, - main, - isFederated + main )); // Put the code here to set up the tables that drive resetting is_present and // decrementing reference counts between time steps. This code has to appear @@ -148,20 +142,22 @@ public static String generateInitializeTriggerObjects( // between inputs and outputs. code.pr(startTimeStep.toString()); code.pr(setReactionPriorities( - federate, - main, - isFederated - )); - code.pr(initializeFederate( - federate, main, targetConfig, - federationRTIProperties, - isFederated, - clockSyncIsOn + main )); code.pr(generateSchedulerInitializer( main, targetConfig )); + + code.pr(""" + #ifdef EXECUTABLE_PREAMBLE + _lf_executable_preamble(); + #endif + """); + + // Initialize triggers for federated execution. + code.pr(CExtensionUtils.surroundWithIfFederated("initialize_triggers_for_federate();")); + code.unindent(); code.pr("}\n"); return code.toString(); @@ -197,149 +193,28 @@ public static String generateSchedulerInitializer( return code.toString(); } - /** - * If the number of federates is greater than one, then generate the code - * that initializes global variables that describe the federate. - * @param federate The federate instance. - */ - private static String initializeFederate( - FederateInstance federate, - ReactorInstance main, - TargetConfig targetConfig, - Map federationRTIProperties, - boolean isFederated, - boolean clockSyncIsOn - ) { - var code = new CodeBuilder(); - if (!isFederated) { - return ""; - } - code.pr("// ***** Start initializing the federated execution. */"); - code.pr(String.join("\n", - "// Initialize the socket mutex", - "lf_mutex_init(&outbound_socket_mutex);", - "lf_cond_init(&port_status_changed);" - )); - - if (isFederated && targetConfig.coordination == CoordinationType.DECENTRALIZED) { - var reactorInstance = main.getChildReactorInstance(federate.instantiation); - for (ParameterInstance param : reactorInstance.parameters) { - if (param.getName().equalsIgnoreCase("STP_offset") && param.type.isTime) { - var stp = ASTUtils.getLiteralTimeValue(param.getInitialValue().get(0)); - if (stp != null) { - code.pr("lf_set_stp_offset("+GeneratorBase.timeInTargetLanguage(stp)+");"); - } - } - } - } - - // Set indicator variables that specify whether the federate has - // upstream logical connections. - if (federate.dependsOn.size() > 0) { - code.pr("_fed.has_upstream = true;"); - } - if (federate.sendsTo.size() > 0) { - code.pr("_fed.has_downstream = true;"); - } - // Set global variable identifying the federate. - code.pr("_lf_my_fed_id = "+federate.id+";"); - - // We keep separate record for incoming and outgoing p2p connections to allow incoming traffic to be processed in a separate - // thread without requiring a mutex lock. - var numberOfInboundConnections = federate.inboundP2PConnections.size(); - var numberOfOutboundConnections = federate.outboundP2PConnections.size(); - - code.pr(String.join("\n", - "_fed.number_of_inbound_p2p_connections = "+numberOfInboundConnections+";", - "_fed.number_of_outbound_p2p_connections = "+numberOfOutboundConnections+";" - )); - if (numberOfInboundConnections > 0) { - code.pr(String.join("\n", - "// Initialize the array of socket for incoming connections to -1.", - "for (int i = 0; i < NUMBER_OF_FEDERATES; i++) {", - " _fed.sockets_for_inbound_p2p_connections[i] = -1;", - "}" - )); - } - if (numberOfOutboundConnections > 0) { - code.pr(String.join("\n", - "// Initialize the array of socket for outgoing connections to -1.", - "for (int i = 0; i < NUMBER_OF_FEDERATES; i++) {", - " _fed.sockets_for_outbound_p2p_connections[i] = -1;", - "}" - )); - } - - // If a test clock offset has been specified, insert code to set it here. - if (targetConfig.clockSyncOptions.testOffset != null) { - code.pr("lf_set_physical_clock_offset((1 + "+federate.id+") * "+targetConfig.clockSyncOptions.testOffset.toNanoSeconds()+"LL);"); - } - - code.pr(String.join("\n", - "// Connect to the RTI. This sets _fed.socket_TCP_RTI and _lf_rti_socket_UDP.", - "connect_to_rti("+addDoubleQuotes(federationRTIProperties.get("host").toString())+", "+federationRTIProperties.get("port")+");" - )); - - // Disable clock synchronization for the federate if it resides on the same host as the RTI, - // unless that is overridden with the clock-sync-options target property. - if (clockSyncIsOn) { - code.pr("synchronize_initial_physical_clock_with_rti(_fed.socket_TCP_RTI);"); - } - - if (numberOfInboundConnections > 0) { - code.pr(String.join("\n", - "// Create a socket server to listen to other federates.", - "// If a port is specified by the user, that will be used", - "// as the only possibility for the server. If not, the port", - "// will start from STARTING_PORT. The function will", - "// keep incrementing the port until the number of tries reaches PORT_RANGE_LIMIT.", - "create_server("+federate.port+");", - "// Connect to remote federates for each physical connection.", - "// This is done in a separate thread because this thread will call", - "// connect_to_federate for each outbound physical connection at the same", - "// time that the new thread is listening for such connections for inbound", - "// physical connections. The thread will live until all connections", - "// have been established.", - "lf_thread_create(&_fed.inbound_p2p_handling_thread_id, handle_p2p_connections_from_federates, NULL);" - )); - } - - for (FederateInstance remoteFederate : federate.outboundP2PConnections) { - code.pr("connect_to_federate("+remoteFederate.id+");"); - } - return code.toString(); - } - /** * * Set the reaction priorities based on dependency analysis. * - * @param currentFederate The federate to generate code for. * @param reactor The reactor on which to do this. - * @param isFederated True if program is federated, false otherwise. */ private static String setReactionPriorities( - FederateInstance currentFederate, - ReactorInstance reactor, - boolean isFederated + ReactorInstance reactor ) { var code = new CodeBuilder(); - setReactionPriorities(currentFederate, reactor, code, isFederated); + setReactionPriorities(reactor, code); return code.toString(); } /** * * Set the reaction priorities based on dependency analysis. * - * @param currentFederate The federate to generate code for. * @param reactor The reactor on which to do this. * @param builder Where to write the code. - * @param isFederated True if program is federated, false otherwise. */ private static boolean setReactionPriorities( - FederateInstance currentFederate, ReactorInstance reactor, - CodeBuilder builder, - boolean isFederated + CodeBuilder builder ) { var foundOne = false; // Force calculation of levels if it has not been done. @@ -355,100 +230,97 @@ private static boolean setReactionPriorities( var prolog = new CodeBuilder(); var epilog = new CodeBuilder(); + for (ReactionInstance r : reactor.reactions) { - if (currentFederate.contains(r.getDefinition())) { - var levelSet = r.getLevels(); - var deadlineSet = r.getInferredDeadlines(); - - if (levelSet.size() > 1 || deadlineSet.size() > 1) { - // Scenario 2-4 - if (prolog.length() == 0) { - prolog.startScopedBlock(); - epilog.endScopedBlock(); - } + var levelSet = r.getLevels(); + var deadlineSet = r.getInferredDeadlines(); + + if (levelSet.size() > 1 || deadlineSet.size() > 1) { + // Scenario 2-4 + if (prolog.length() == 0) { + prolog.startScopedBlock(); + epilog.endScopedBlock(); } - if (deadlineSet.size() > 1) { - // Scenario (2) or (4) - var deadlines = r.getInferredDeadlinesList().stream() - .map(elt -> ("0x" + Long.toString(elt.toNanoSeconds(), 16) + "LL")) - .collect(Collectors.toList()); + } + if (deadlineSet.size() > 1) { + // Scenario (2) or (4) + var deadlines = r.getInferredDeadlinesList().stream() + .map(elt -> ("0x" + Long.toString(elt.toNanoSeconds(), 16) + "LL")) + .collect(Collectors.toList()); - prolog.pr("interval_t "+r.uniqueID()+"_inferred_deadlines[] = { "+joinObjects(deadlines, ", ")+" };"); - } + prolog.pr("interval_t "+r.uniqueID()+"_inferred_deadlines[] = { "+joinObjects(deadlines, ", ")+" };"); + } - if (levelSet.size() > 1) { - // Scenario (3) or (4) - // Cannot use the above set of levels because it is a set, not a list. - prolog.pr("int "+r.uniqueID()+"_levels[] = { "+joinObjects(r.getLevelsList(), ", ")+" };"); - } + if (levelSet.size() > 1) { + // Scenario (3) or (4) + // Cannot use the above set of levels because it is a set, not a list. + prolog.pr("int "+r.uniqueID()+"_levels[] = { "+joinObjects(r.getLevelsList(), ", ")+" };"); } } + var temp = new CodeBuilder(); temp.pr("// Set reaction priorities for " + reactor); - temp.startScopedBlock(reactor, currentFederate, isFederated, true); + temp.startScopedBlock(reactor); for (ReactionInstance r : reactor.reactions) { - if (currentFederate.contains(r.getDefinition())) { - foundOne = true; - var levelSet = r.getLevels(); - var deadlineSet = r.getInferredDeadlines(); - - // Get the head of the associated lists. To avoid duplication in - // several of the following cases - var level = r.getLevelsList().get(0); - var inferredDeadline = r.getInferredDeadlinesList().get(0); - var runtimeIdx =CUtil.runtimeIndex(r.getParent()); - - if (levelSet.size() == 1 && deadlineSet.size() == 1) { - // Scenario (1) - - // xtend doesn't support bitwise operators... - var indexValue = inferredDeadline.toNanoSeconds() << 16 | level; - - var reactionIndex = "0x" + Long.toUnsignedString(indexValue, 16) + "LL"; - - temp.pr(String.join("\n", - CUtil.reactionRef(r)+".chain_id = "+r.chainID+";", - "// index is the OR of level "+level+" and ", - "// deadline "+ inferredDeadline.toNanoSeconds()+" shifted left 16 bits.", - CUtil.reactionRef(r)+".index = "+reactionIndex+";" - )); - } else if (levelSet.size() == 1 && deadlineSet.size() > 1) { - // Scenario 2 - temp.pr(String.join("\n", - CUtil.reactionRef(r)+".chain_id = "+r.chainID+";", - "// index is the OR of levels["+runtimeIdx+"] and ", - "// deadlines["+runtimeIdx+"] shifted left 16 bits.", - CUtil.reactionRef(r)+".index = ("+r.uniqueID()+"_inferred_deadlines["+runtimeIdx+"] << 16) | " + - level+";" - )); + //if (currentFederate.contains(r.getDefinition())) { + foundOne = true; + var levelSet = r.getLevels(); + var deadlineSet = r.getInferredDeadlines(); - } else if (levelSet.size() > 1 && deadlineSet.size() == 1) { - // Scenarion (3) - temp.pr(String.join("\n", - CUtil.reactionRef(r)+".chain_id = "+r.chainID+";", - "// index is the OR of levels["+runtimeIdx+"] and ", - "// deadlines["+runtimeIdx+"] shifted left 16 bits.", - CUtil.reactionRef(r)+".index = ("+inferredDeadline.toNanoSeconds()+" << 16) | " + - r.uniqueID()+"_levels["+runtimeIdx+"];" - )); + // Get the head of the associated lists. To avoid duplication in + // several of the following cases + var level = r.getLevelsList().get(0); + var inferredDeadline = r.getInferredDeadlinesList().get(0); + var runtimeIdx =CUtil.runtimeIndex(r.getParent()); - } else if (levelSet.size() > 1 && deadlineSet.size() > 1) { - // Scenario (4) - temp.pr(String.join("\n", - CUtil.reactionRef(r)+".chain_id = "+r.chainID+";", - "// index is the OR of levels["+runtimeIdx+"] and ", - "// deadlines["+runtimeIdx+"] shifted left 16 bits.", - CUtil.reactionRef(r)+".index = ("+r.uniqueID()+"_inferred_deadlines["+runtimeIdx+"] << 16) | " + - r.uniqueID()+"_levels["+runtimeIdx+"];" - )); - } + if (levelSet.size() == 1 && deadlineSet.size() == 1) { + // Scenario (1) + + var indexValue = inferredDeadline.toNanoSeconds() << 16 | level; + + var reactionIndex = "0x" + Long.toUnsignedString(indexValue, 16) + "LL"; + + temp.pr(String.join("\n", + CUtil.reactionRef(r)+".chain_id = "+r.chainID+";", + "// index is the OR of level "+level+" and ", + "// deadline "+ inferredDeadline.toNanoSeconds()+" shifted left 16 bits.", + CUtil.reactionRef(r)+".index = "+reactionIndex+";" + )); + } else if (levelSet.size() == 1 && deadlineSet.size() > 1) { + // Scenario 2 + temp.pr(String.join("\n", + CUtil.reactionRef(r)+".chain_id = "+r.chainID+";", + "// index is the OR of levels["+runtimeIdx+"] and ", + "// deadlines["+runtimeIdx+"] shifted left 16 bits.", + CUtil.reactionRef(r)+".index = ("+r.uniqueID()+"_inferred_deadlines["+runtimeIdx+"] << 16) | " + + level+";" + )); + + } else if (levelSet.size() > 1 && deadlineSet.size() == 1) { + // Scenarion (3) + temp.pr(String.join("\n", + CUtil.reactionRef(r)+".chain_id = "+r.chainID+";", + "// index is the OR of levels["+runtimeIdx+"] and ", + "// deadlines["+runtimeIdx+"] shifted left 16 bits.", + CUtil.reactionRef(r)+".index = ("+inferredDeadline.toNanoSeconds()+" << 16) | " + + r.uniqueID()+"_levels["+runtimeIdx+"];" + )); + + } else if (levelSet.size() > 1 && deadlineSet.size() > 1) { + // Scenario (4) + temp.pr(String.join("\n", + CUtil.reactionRef(r)+".chain_id = "+r.chainID+";", + "// index is the OR of levels["+runtimeIdx+"] and ", + "// deadlines["+runtimeIdx+"] shifted left 16 bits.", + CUtil.reactionRef(r)+".index = ("+r.uniqueID()+"_inferred_deadlines["+runtimeIdx+"] << 16) | " + + r.uniqueID()+"_levels["+runtimeIdx+"];" + )); } + } for (ReactorInstance child : reactor.children) { - if (currentFederate.contains(child)) { - foundOne = setReactionPriorities(currentFederate, child, temp, isFederated) || foundOne; - } + foundOne = setReactionPriorities(child, temp) || foundOne; } temp.endScopedBlock(); @@ -468,9 +340,7 @@ private static boolean setReactionPriorities( * @param instance The reactor instance. */ private static String deferredConnectInputsToOutputs( - FederateInstance currentFederate, - ReactorInstance instance, - boolean isFederated + ReactorInstance instance ) { var code = new CodeBuilder(); code.pr("// Connect inputs and outputs for reactor "+instance.getFullName()+"."); @@ -478,17 +348,17 @@ private static String deferredConnectInputsToOutputs( for (PortInstance input : instance.inputs) { if (!input.getDependsOnReactions().isEmpty()) { // Input is written to by reactions in the parent of the port's parent. - code.pr(connectPortToEventualDestinations(currentFederate, input, isFederated)); + code.pr(connectPortToEventualDestinations(input)); } } for (PortInstance output : instance.outputs) { if (!output.getDependsOnReactions().isEmpty()) { // Output is written to by reactions in the port's parent. - code.pr(connectPortToEventualDestinations(currentFederate, output, isFederated)); + code.pr(connectPortToEventualDestinations(output)); } } for (ReactorInstance child: instance.children) { - code.pr(deferredConnectInputsToOutputs(currentFederate, child, isFederated)); + code.pr(deferredConnectInputsToOutputs(child)); } return code.toString(); } @@ -503,11 +373,8 @@ private static String deferredConnectInputsToOutputs( * @param src A port that is written to by reactions. */ private static String connectPortToEventualDestinations( - FederateInstance currentFederate, - PortInstance src, - boolean isFederated + PortInstance src ) { - if (!currentFederate.contains(src.getParent())) return ""; var code = new CodeBuilder(); for (SendRange srcRange: src.eventualDestinations()) { for (RuntimeRange dstRange : srcRange.destinations) { @@ -518,27 +385,25 @@ private static String connectPortToEventualDestinations( // by the currentFederate because an AST transformation removes connections // between ports of distinct federates. So the following check is not // really necessary. - if (currentFederate.contains(dst.getParent())) { - var mod = (dst.isMultiport() || (src.isInput() && src.isMultiport()))? "" : "&"; - code.pr("// Connect "+srcRange+" to port "+dstRange); - code.startScopedRangeBlock(currentFederate, srcRange, dstRange, isFederated); - if (src.isInput()) { - // Source port is written to by reaction in port's parent's parent - // and ultimate destination is further downstream. - code.pr(CUtil.portRef(dst, dr, db, dc)+" = ("+destStructType+"*)"+mod+CUtil.portRefNested(src, sr, sb, sc)+";"); - } else if (dst.isOutput()) { - // An output port of a contained reactor is triggering a reaction. - code.pr(CUtil.portRefNested(dst, dr, db, dc)+" = ("+destStructType+"*)&"+CUtil.portRef(src, sr, sb, sc)+";"); - } else { - // An output port is triggering an input port. - code.pr(CUtil.portRef(dst, dr, db, dc)+" = ("+destStructType+"*)&"+CUtil.portRef(src, sr, sb, sc)+";"); - if (AttributeUtils.isSparse(dst.getDefinition())) { - code.pr(CUtil.portRef(dst, dr, db, dc)+"->sparse_record = "+CUtil.portRefName(dst, dr, db, dc)+"__sparse;"); - code.pr(CUtil.portRef(dst, dr, db, dc)+"->destination_channel = "+dc+";"); - } + var mod = (dst.isMultiport() || (src.isInput() && src.isMultiport()))? "" : "&"; + code.pr("// Connect "+srcRange+" to port "+dstRange); + code.startScopedRangeBlock(srcRange, dstRange); + if (src.isInput()) { + // Source port is written to by reaction in port's parent's parent + // and ultimate destination is further downstream. + code.pr(CUtil.portRef(dst, dr, db, dc)+" = ("+destStructType+"*)"+mod+CUtil.portRefNested(src, sr, sb, sc)+";"); + } else if (dst.isOutput()) { + // An output port of a contained reactor is triggering a reaction. + code.pr(CUtil.portRefNested(dst, dr, db, dc)+" = ("+destStructType+"*)&"+CUtil.portRef(src, sr, sb, sc)+";"); + } else { + // An output port is triggering an input port. + code.pr(CUtil.portRef(dst, dr, db, dc)+" = ("+destStructType+"*)&"+CUtil.portRef(src, sr, sb, sc)+";"); + if (AttributeUtils.isSparse(dst.getDefinition())) { + code.pr(CUtil.portRef(dst, dr, db, dc)+"->sparse_record = "+CUtil.portRefName(dst, dr, db, dc)+"__sparse;"); + code.pr(CUtil.portRef(dst, dr, db, dc)+"->destination_channel = "+dc+";"); } - code.endScopedRangeBlock(srcRange, dstRange, isFederated); } + code.endScopedRangeBlock(srcRange, dstRange); } } return code.toString(); @@ -553,28 +418,10 @@ private static String connectPortToEventualDestinations( * @param r The reactor. */ private static String deferredOptimizeForSingleDominatingReaction( - FederateInstance currentFederate, - ReactorInstance r, - boolean isFederated + ReactorInstance r ) { var code = new CodeBuilder(); for (ReactionInstance reaction : r.reactions) { - if (currentFederate.contains(reaction.getDefinition()) - && currentFederate.contains(reaction.getParent()) - ) { - - // For federated systems, the above test may not be enough if there is a bank - // of federates. Calculate the divisor needed to compute the federate bank - // index from the instance index of the reaction. - var divisor = 1; - if (isFederated) { - var parent = reaction.getParent(); - while (parent.getDepth() > 1) { - divisor *= parent.getWidth(); - parent = parent.getParent(); - } - } - // The following code attempts to gather into a loop assignments of successive // bank members relations between reactions to avoid large chunks of inline code // when a large bank sends to a large bank or when a large bank receives from @@ -591,7 +438,7 @@ private static String deferredOptimizeForSingleDominatingReaction( if (runtime.dominating != previousRuntime.dominating) { // End of streak of same dominating reaction runtime instance. code.pr(printOptimizeForSingleDominatingReaction( - currentFederate, previousRuntime, start, end, domStart, same, divisor, isFederated + previousRuntime, start, end, domStart, same )); same = false; start = runtime.id; @@ -607,7 +454,7 @@ private static String deferredOptimizeForSingleDominatingReaction( if (runtime.dominating.id != previousRuntime.dominating.id + 1) { // End of a streak of contiguous runtimes. printOptimizeForSingleDominatingReaction( - currentFederate, previousRuntime, start, end, domStart, same, divisor, isFederated + previousRuntime, start, end, domStart, same ); same = false; start = runtime.id; @@ -616,7 +463,7 @@ private static String deferredOptimizeForSingleDominatingReaction( } else { // Different dominating reaction. printOptimizeForSingleDominatingReaction( - currentFederate, previousRuntime, start, end, domStart, same, divisor, isFederated + previousRuntime, start, end, domStart, same ); same = false; start = runtime.id; @@ -629,10 +476,9 @@ private static String deferredOptimizeForSingleDominatingReaction( } if (end > start) { printOptimizeForSingleDominatingReaction( - currentFederate, previousRuntime, start, end, domStart, same, divisor, isFederated + previousRuntime, start, end, domStart, same ); } - } } return code.toString(); } @@ -641,35 +487,13 @@ private static String deferredOptimizeForSingleDominatingReaction( * Print statement that sets the last_enabling_reaction field of a reaction. */ private static String printOptimizeForSingleDominatingReaction( - FederateInstance currentFederate, ReactionInstance.Runtime runtime, int start, int end, int domStart, - boolean same, - int divisor, - boolean isFederated + boolean same ) { var code = new CodeBuilder(); - var domDivisor = 1; - if (isFederated && runtime.dominating != null) { - var domReaction = runtime.dominating.getReaction(); - // No need to do anything if the dominating reaction is not in the federate. - // Note that this test is imperfect because the current federate may be a - // bank member. - if (!currentFederate.contains(domReaction.getDefinition()) - || !currentFederate.contains(domReaction.getParent())) { - return ""; - } - // To really know whether the dominating reaction is in the federate, - // we need to calculate a divisor for its runtime index. - var parent = runtime.dominating.getReaction().getParent(); - while (parent.getDepth() > 1) { - domDivisor *= parent.getWidth(); - parent = parent.getParent(); - } - } - var dominatingRef = "NULL"; if (end > start + 1) { @@ -686,32 +510,19 @@ private static String printOptimizeForSingleDominatingReaction( "// "+runtime.getReaction().getFullName()+" dominating upstream reaction.", "int j = "+domStart+";", "for (int i = "+start+"; i < "+end+"; i++) {", - (isFederated ? - " if (i / "+divisor+" != "+currentFederate.bankIndex+") continue; // Reaction is not in the federate." : - ""), - (runtime.dominating != null ? - " if (j / "+domDivisor+" != "+currentFederate.bankIndex+") continue; // Dominating reaction is not in the federate." : - ""), " "+reactionRef+".last_enabling_reaction = "+dominatingRef+";", "}" )); code.endScopedBlock(); } else if (end == start + 1) { var reactionRef = CUtil.reactionRef(runtime.getReaction(), "" + start); - if (runtime.dominating != null - && (domDivisor == 1 || domStart/domDivisor == currentFederate.bankIndex) - ) { + if (runtime.dominating != null) { dominatingRef = "&(" + CUtil.reactionRef(runtime.dominating.getReaction(), "" + domStart) + ")"; } - if (!isFederated - || (start/divisor == currentFederate.bankIndex) - && (runtime.dominating == null || domStart/domDivisor == currentFederate.bankIndex) - ) { - code.pr(String.join("\n", - "// "+runtime.getReaction().getFullName()+" dominating upstream reaction.", - reactionRef+".last_enabling_reaction = "+dominatingRef+";" - )); - } + code.pr(String.join("\n", + "// "+runtime.getReaction().getFullName()+" dominating upstream reaction.", + reactionRef+".last_enabling_reaction = "+dominatingRef+";" + )); } return code.toString(); } @@ -723,9 +534,7 @@ private static String printOptimizeForSingleDominatingReaction( * @param reactions The reactions. */ private static String deferredFillTriggerTable( - FederateInstance currentFederate, - Iterable reactions, - boolean isFederated + Iterable reactions ) { var code = new CodeBuilder(); for (ReactionInstance reaction : reactions) { @@ -748,31 +557,25 @@ private static String deferredFillTriggerTable( // We generate the code to fill the triggers array first in a temporary code buffer, // so that we can simultaneously calculate the size of the total array. for (SendRange srcRange : port.eventualDestinations()) { - var srcNested = (port.isInput())? true : false; - code.startScopedRangeBlock(currentFederate, srcRange, sr, sb, sc, srcNested, isFederated, true); + var srcNested = port.isInput(); + code.startScopedRangeBlock(srcRange, sr, sb, sc, srcNested); var triggerArray = CUtil.reactionRef(reaction, sr)+".triggers[triggers_index["+sr+"]++]"; // Skip ports whose parent is not in the federation. // This can happen with reactions in the top-level that have // as an effect a port in a bank. - if (currentFederate.contains(port.getParent())) { - code.pr(String.join("\n", - "// Reaction "+reaction.index+" of "+name+" triggers "+srcRange.destinations.size()+" downstream reactions", - "// through port "+port.getFullName()+".", - CUtil.reactionRef(reaction, sr)+".triggered_sizes[triggers_index["+sr+"]] = "+srcRange.destinations.size()+";", - "// For reaction "+reaction.index+" of "+name+", allocate an", - "// array of trigger pointers for downstream reactions through port "+port.getFullName(), - "trigger_t** trigger_array = (trigger_t**)_lf_allocate(", - " "+srcRange.destinations.size()+", sizeof(trigger_t*),", - " &"+reactorSelfStruct+"->base.allocations); ", - triggerArray+" = trigger_array;" - )); - } else { - // Port is not in the federate or has no destinations. - // Set the triggered_width fields to 0. - code.pr(CUtil.reactionRef(reaction, sr)+".triggered_sizes["+sc+"] = 0;"); - } - code.endScopedRangeBlock(srcRange, isFederated); + code.pr(String.join("\n", + "// Reaction "+reaction.index+" of "+name+" triggers "+srcRange.destinations.size()+" downstream reactions", + "// through port "+port.getFullName()+".", + CUtil.reactionRef(reaction, sr)+".triggered_sizes[triggers_index["+sr+"]] = "+srcRange.destinations.size()+";", + "// For reaction "+reaction.index+" of "+name+", allocate an", + "// array of trigger pointers for downstream reactions through port "+port.getFullName(), + "trigger_t** trigger_array = (trigger_t**)_lf_allocate(", + " "+srcRange.destinations.size()+", sizeof(trigger_t*),", + " &"+reactorSelfStruct+"->base.allocations); ", + triggerArray+" = trigger_array;" + )); + code.endScopedRangeBlock(srcRange); } } var cumulativePortWidth = 0; @@ -782,56 +585,52 @@ private static String deferredFillTriggerTable( code.pr("for (int i = 0; i < "+reaction.getParent().getTotalWidth()+"; i++) triggers_index[i] = "+cumulativePortWidth+";"); for (SendRange srcRange : port.eventualDestinations()) { - if (currentFederate.contains(port.getParent())) { - var srcNested = srcRange.instance.isInput(); - var multicastCount = 0; - for (RuntimeRange dstRange : srcRange.destinations) { - var dst = dstRange.instance; - - code.startScopedRangeBlock(currentFederate, srcRange, dstRange, isFederated); - - // If the source is nested, need to take into account the parent's bank index - // when indexing into the triggers array. - var triggerArray = ""; - if (srcNested && port.getParent().getWidth() > 1 && !(isFederated && port.getParent().getDepth() == 1)) { - triggerArray = CUtil.reactionRef(reaction, sr)+".triggers[triggers_index["+sr+"] + "+sc+" + src_range_mr.digits[1] * src_range_mr.radixes[0]]"; - } else { - triggerArray = CUtil.reactionRef(reaction, sr)+".triggers[triggers_index["+sr+"] + "+sc+"]"; - } + var srcNested = srcRange.instance.isInput(); + var multicastCount = 0; + for (RuntimeRange dstRange : srcRange.destinations) { + var dst = dstRange.instance; + + code.startScopedRangeBlock(srcRange, dstRange); + + // If the source is nested, need to take into account the parent's bank index + // when indexing into the triggers array. + var triggerArray = ""; + if (srcNested && port.getParent().getWidth() > 1) { + triggerArray = CUtil.reactionRef(reaction, sr)+".triggers[triggers_index["+sr+"] + "+sc+" + src_range_mr.digits[1] * src_range_mr.radixes[0]]"; + } else { + triggerArray = CUtil.reactionRef(reaction, sr)+".triggers[triggers_index["+sr+"] + "+sc+"]"; + } - if (dst.isOutput()) { - // Include this destination port only if it has at least one - // reaction in the federation. - var belongs = false; - for (ReactionInstance destinationReaction : dst.getDependentReactions()) { - if (currentFederate.contains(destinationReaction.getParent())) { - belongs = true; - } - } - if (belongs) { - code.pr(String.join("\n", - "// Port "+port.getFullName()+" has reactions in its parent's parent.", - "// Point to the trigger struct for those reactions.", - triggerArray+"["+multicastCount+"] = &"+CUtil.triggerRefNested(dst, dr, db)+";" - )); - } else { - // Put in a NULL pointer. - code.pr(String.join("\n", - "// Port "+port.getFullName()+" has reactions in its parent's parent.", - "// But those are not in the federation.", - triggerArray+"["+multicastCount+"] = NULL;" - )); - } + if (dst.isOutput()) { + // Include this destination port only if it has at least one + // reaction in the federation. + var belongs = false; + for (ReactionInstance destinationReaction : dst.getDependentReactions()) { + belongs = true; + } + if (belongs) { + code.pr(String.join("\n", + "// Port "+port.getFullName()+" has reactions in its parent's parent.", + "// Point to the trigger struct for those reactions.", + triggerArray+"["+multicastCount+"] = &"+CUtil.triggerRefNested(dst, dr, db)+";" + )); } else { - // Destination is an input port. + // Put in a NULL pointer. code.pr(String.join("\n", - "// Point to destination port "+dst.getFullName()+"'s trigger struct.", - triggerArray+"["+multicastCount+"] = &"+CUtil.triggerRef(dst, dr)+";" + "// Port "+port.getFullName()+" has reactions in its parent's parent.", + "// But those are not in the federation.", + triggerArray+"["+multicastCount+"] = NULL;" )); } - code.endScopedRangeBlock(srcRange, dstRange, isFederated); - multicastCount++; + } else { + // Destination is an input port. + code.pr(String.join("\n", + "// Point to destination port "+dst.getFullName()+"'s trigger struct.", + triggerArray+"["+multicastCount+"] = &"+CUtil.triggerRef(dst, dr)+";" + )); } + code.endScopedRangeBlock(srcRange, dstRange); + multicastCount++; } } // If the port is an input of a contained reactor, then we have to take @@ -857,9 +656,7 @@ private static String deferredFillTriggerTable( * @param reactions The reactions. */ private static String deferredInputNumDestinations( - FederateInstance currentFederate, - Iterable reactions, - boolean isFederated + Iterable reactions ) { // Reference counts are decremented by each destination reactor // at the conclusion of a time step. Hence, the initial reference @@ -879,11 +676,11 @@ private static String deferredInputNumDestinations( code.pr("// For reference counting, set num_destinations for port "+port.getParent().getName()+"."+port.getName()+"."); // The input port may itself have multiple destinations. for (SendRange sendingRange : port.eventualDestinations()) { - code.startScopedRangeBlock(currentFederate, sendingRange, sr, sb, sc, sendingRange.instance.isInput(), isFederated, true); + code.startScopedRangeBlock(sendingRange, sr, sb, sc, sendingRange.instance.isInput()); // Syntax is slightly different for a multiport output vs. single port. var connector = (port.isMultiport())? "->" : "."; code.pr(CUtil.portRefNested(port, sr, sb, sc)+connector+"num_destinations = "+sendingRange.getNumberOfDestinationReactors()+";"); - code.endScopedRangeBlock(sendingRange, isFederated); + code.endScopedRangeBlock(sendingRange); } } } @@ -900,9 +697,7 @@ private static String deferredInputNumDestinations( * @param reactor The reactor instance. */ private static String deferredOutputNumDestinations( - FederateInstance currentFederate, - ReactorInstance reactor, - boolean isFederated + ReactorInstance reactor ) { // Reference counts are decremented by each destination reactor // at the conclusion of a time step. Hence, the initial reference @@ -914,9 +709,9 @@ private static String deferredOutputNumDestinations( for (PortInstance output : reactor.outputs) { for (SendRange sendingRange : output.eventualDestinations()) { code.pr("// For reference counting, set num_destinations for port " + output.getFullName() + "."); - code.startScopedRangeBlock(currentFederate, sendingRange, sr, sb, sc, sendingRange.instance.isInput(), isFederated, true); + code.startScopedRangeBlock(sendingRange, sr, sb, sc, sendingRange.instance.isInput()); code.pr(CUtil.portRef(output, sr, sb, sc)+".num_destinations = "+sendingRange.getNumberOfDestinationReactors()+";"); - code.endScopedRangeBlock(sendingRange, isFederated); + code.endScopedRangeBlock(sendingRange); } } return code.toString(); @@ -929,15 +724,11 @@ private static String deferredOutputNumDestinations( * so each function it calls must handle its own iteration * over all runtime instance. * @param reactor The container. - * @param currentFederate The federate (used to determine whether a - * reaction belongs to the federate). */ private static String deferredInitializeNonNested( - FederateInstance currentFederate, ReactorInstance reactor, ReactorInstance main, - Iterable reactions, - boolean isFederated + Iterable reactions ) { var code = new CodeBuilder(); code.pr("// **** Start non-nested deferred initialize for "+reactor.getFullName()); @@ -945,9 +736,7 @@ private static String deferredInitializeNonNested( // This needs to be outside the above scoped block because it performs // its own iteration over ranges. code.pr(deferredInputNumDestinations( - currentFederate, - reactions, - isFederated + reactions )); // Second batch of initializes cannot be within a for loop @@ -955,31 +744,21 @@ private static String deferredInitializeNonNested( // ranges which may span bank members. if (reactor != main) { code.pr(deferredOutputNumDestinations( - currentFederate, - reactor, - isFederated + reactor )); } code.pr(deferredFillTriggerTable( - currentFederate, - reactions, - isFederated + reactions )); code.pr(deferredOptimizeForSingleDominatingReaction( - currentFederate, - reactor, - isFederated + reactor )); for (ReactorInstance child: reactor.children) { - if (currentFederate.contains(child)) { - code.pr(deferredInitializeNonNested( - currentFederate, - child, - main, - child.reactions, - isFederated - )); - } + code.pr(deferredInitializeNonNested( + child, + main, + child.reactions + )); } code.pr("// **** End of non-nested deferred initialize for "+reactor.getFullName()); return code.toString(); @@ -1023,10 +802,8 @@ private static String deferredCreateDefaultTokens( * @param reaction The reaction instance. */ private static String deferredReactionOutputs( - FederateInstance currentFederate, ReactionInstance reaction, - TargetConfig targetConfig, - boolean isFederated + TargetConfig targetConfig ) { var code = new CodeBuilder(); // val selfRef = CUtil.reactorRef(reaction.getParent()); @@ -1062,7 +839,7 @@ private static String deferredReactionOutputs( if (effect.isInput()) { init.pr("// Reaction writes to an input of a contained reactor."); bankWidth = effect.getParent().getWidth(); - init.startScopedBlock(effect.getParent(), currentFederate, isFederated, true); + init.startScopedBlock(effect.getParent()); portRef = CUtil.portRefNestedName(effect); } else { init.startScopedBlock(); @@ -1125,20 +902,16 @@ private static String deferredReactionOutputs( * @param reactions A list of reactions. */ private static String deferredReactionMemory( - FederateInstance currentFederate, Iterable reactions, - TargetConfig targetConfig, - boolean isFederated + TargetConfig targetConfig ) { var code = new CodeBuilder(); // For each reaction instance, allocate the arrays that will be used to // trigger downstream reactions. for (ReactionInstance reaction : reactions) { code.pr(deferredReactionOutputs( - currentFederate, reaction, - targetConfig, - isFederated + targetConfig )); var reactorSelfStruct = CUtil.reactorRef(reaction.getParent()); @@ -1153,7 +926,7 @@ private static String deferredReactionMemory( "// Allocate memory to store pointers to the multiport output "+trigger.getName()+" ", "// of a contained reactor "+trigger.getParent().getFullName() )); - code.startScopedBlock(trigger.getParent(), currentFederate, isFederated, true); + code.startScopedBlock(trigger.getParent()); var width = trigger.getWidth(); var portStructType = CGenerator.variableStructType(trigger); @@ -1182,9 +955,7 @@ private static String deferredReactionMemory( * @param reactor The reactor. */ private static String deferredAllocationForEffectsOnInputs( - FederateInstance currentFederate, - ReactorInstance reactor, - boolean isFederated + ReactorInstance reactor ) { var code = new CodeBuilder(); // Keep track of ports already handled. There may be more than one reaction @@ -1197,11 +968,10 @@ private static String deferredAllocationForEffectsOnInputs( if (effect.getParent().getDepth() > reactor.getDepth() // port of a contained reactor. && effect.isMultiport() && !portsHandled.contains(effect) - && currentFederate.contains(effect.getParent()) ) { code.pr("// A reaction writes to a multiport of a child. Allocate memory."); portsHandled.add(effect); - code.startScopedBlock(effect.getParent(), currentFederate, isFederated, true); + code.startScopedBlock(effect.getParent()); var portStructType = CGenerator.variableStructType(effect); var effectRef = CUtil.portRefNestedName(effect); code.pr(String.join("\n", @@ -1229,25 +999,18 @@ private static String deferredAllocationForEffectsOnInputs( * all reactor runtime instances have been created. * This function creates nested loops over nested banks. * @param reactor The container. - * @param currentFederate The federate (used to determine whether a - * reaction belongs to the federate). */ private static String deferredInitialize( - FederateInstance currentFederate, ReactorInstance reactor, Iterable reactions, TargetConfig targetConfig, - CTypes types, - boolean isFederated + CTypes types ) { - if (!currentFederate.contains(reactor)) { - return ""; - } var code = new CodeBuilder(); code.pr("// **** Start deferred initialize for "+reactor.getFullName()); // First batch of initializations is within a for loop iterating // over bank members for the reactor's parent. - code.startScopedBlock(reactor, currentFederate, isFederated, true); + code.startScopedBlock(reactor); // If the child has a multiport that is an effect of some reaction in its container, // then we have to generate code to allocate memory for arrays pointing to @@ -1255,15 +1018,11 @@ private static String deferredInitialize( // bank width because a reaction cannot specify which bank members it writes // to so we have to assume it can write to any. code.pr(deferredAllocationForEffectsOnInputs( - currentFederate, - reactor, - isFederated + reactor )); code.pr(deferredReactionMemory( - currentFederate, reactions, - targetConfig, - isFederated + targetConfig )); // For outputs that are not primitive types (of form type* or type[]), @@ -1273,16 +1032,12 @@ private static String deferredInitialize( types )); for (ReactorInstance child: reactor.children) { - if (currentFederate.contains(child)) { - code.pr(deferredInitialize( - currentFederate, - child, - child.reactions, - targetConfig, - types, - isFederated) - ); - } + code.pr(deferredInitialize( + child, + child.reactions, + targetConfig, + types) + ); } code.endScopedBlock(); code.pr("// **** End of deferred initialize for "+reactor.getFullName()); diff --git a/org.lflang/src/org/lflang/generator/c/CUtil.java b/org.lflang/src/org/lflang/generator/c/CUtil.java index 0e0d09456f..21499f958c 100644 --- a/org.lflang/src/org/lflang/generator/c/CUtil.java +++ b/org.lflang/src/org/lflang/generator/c/CUtil.java @@ -42,7 +42,7 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.FileConfig; import org.lflang.InferredType; import org.lflang.TargetConfig; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FederateInstance; import org.lflang.generator.GeneratorCommandFactory; import org.lflang.generator.LFGeneratorContext; import org.lflang.generator.PortInstance; @@ -128,6 +128,18 @@ public static String channelIndexName(PortInstance port) { return port.uniqueID() + "_c"; } + /** + * Return the name of the reactor. A '_main` is appended to the name if the + * reactor is main (to allow for instantiations that have the same name as + * the main reactor or the .lf file). + */ + public static String getName(ReactorDecl reactor) { + if (reactor instanceof Reactor r && r.isMain()) { + return reactor.getName() + "_main"; + } + return reactor.getName(); + } + /** * Return a reference to the specified port. * @@ -490,6 +502,9 @@ public static String runtimeIndex(ReactorInstance reactor) { * @return The type of a self struct for the specified reactor class. */ public static String selfType(ReactorDecl reactor) { + if (reactor instanceof Reactor r && r.isMain()) { + return reactor.getName().toLowerCase() + "_main_self_t"; + } return reactor.getName().toLowerCase() + "_self_t"; } @@ -845,22 +860,6 @@ public static boolean isTokenType(InferredType type, CTypes types) { return type.isVariableSizeList || targetType.trim().endsWith("*"); } - /** - * The number of threads needs to be at least one larger than the input ports - * to allow the federate to wait on all input ports while allowing an additional - * worker thread to process incoming messages. - * - * @param federates - * @return The minimum number of threads needed. - */ - public static int minThreadsToHandleInputPorts(List federates) { - int nthreads = 1; - for (FederateInstance federate : federates) { - nthreads = Math.max(nthreads, federate.networkMessageActions.size() + 1); - } - return nthreads; - } - public static String generateWidthVariable(String var) { return var + "_width"; } diff --git a/org.lflang/src/org/lflang/generator/c/InteractingContainedReactors.java b/org.lflang/src/org/lflang/generator/c/InteractingContainedReactors.java index 71b9781fff..6fd6175a6f 100644 --- a/org.lflang/src/org/lflang/generator/c/InteractingContainedReactors.java +++ b/org.lflang/src/org/lflang/generator/c/InteractingContainedReactors.java @@ -7,7 +7,7 @@ import java.util.Set; import org.lflang.ASTUtils; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FederateInstance; import org.lflang.lf.Input; import org.lflang.lf.Instantiation; import org.lflang.lf.Output; @@ -51,47 +51,45 @@ public class InteractingContainedReactors { * Scan the reactions of the specified reactor and record which ports are * referenced by reactions and which reactions are triggered by such ports. */ - public InteractingContainedReactors(Reactor reactor, FederateInstance federate) { + public InteractingContainedReactors(Reactor reactor) { var reactionCount = 0; for (Reaction reaction : ASTUtils.allReactions(reactor)) { - if (federate == null || federate.contains(reaction)) { - // First, handle reactions that produce data sent to inputs - // of contained reactors. - for (VarRef effect : ASTUtils.convertToEmptyListIfNull(reaction.getEffects())) { - // If an effect is an input, then it must be an input - // of a contained reactor. - if (effect.getVariable() instanceof Input) { - // This reaction is not triggered by the port, so - // we do not add it to the list returned by the following. - addPort(effect.getContainer(), (Input) effect.getVariable()); - } + // First, handle reactions that produce data sent to inputs + // of contained reactors. + for (VarRef effect : ASTUtils.convertToEmptyListIfNull(reaction.getEffects())) { + // If an effect is an input, then it must be an input + // of a contained reactor. + if (effect.getVariable() instanceof Input) { + // This reaction is not triggered by the port, so + // we do not add it to the list returned by the following. + addPort(effect.getContainer(), (Input) effect.getVariable()); } - // Second, handle reactions that are triggered by outputs - // of contained reactors. - for (TriggerRef trigger : ASTUtils.convertToEmptyListIfNull(reaction.getTriggers())) { - if (trigger instanceof VarRef triggerAsVarRef) { - // If an trigger is an output, then it must be an output - // of a contained reactor. - if (triggerAsVarRef.getVariable() instanceof Output) { - var list = addPort(triggerAsVarRef.getContainer(), (Output) triggerAsVarRef.getVariable()); - list.add(reactionCount); - } + } + // Second, handle reactions that are triggered by outputs + // of contained reactors. + for (TriggerRef trigger : ASTUtils.convertToEmptyListIfNull(reaction.getTriggers())) { + if (trigger instanceof VarRef triggerAsVarRef) { + // If an trigger is an output, then it must be an output + // of a contained reactor. + if (triggerAsVarRef.getVariable() instanceof Output) { + var list = addPort(triggerAsVarRef.getContainer(), (Output) triggerAsVarRef.getVariable()); + list.add(reactionCount); } } - // Third, handle reading (but not triggered by) - // outputs of contained reactors. - for (VarRef source : ASTUtils.convertToEmptyListIfNull(reaction.getSources())) { - if (source.getVariable() instanceof Output) { - // If an source is an output, then it must be an output - // of a contained reactor. - // This reaction is not triggered by the port, so - // we do not add it to the list returned by the following. - addPort(source.getContainer(), (Output) source.getVariable()); - } + } + // Third, handle reading (but not triggered by) + // outputs of contained reactors. + for (VarRef source : ASTUtils.convertToEmptyListIfNull(reaction.getSources())) { + if (source.getVariable() instanceof Output) { + // If an source is an output, then it must be an output + // of a contained reactor. + // This reaction is not triggered by the port, so + // we do not add it to the list returned by the following. + addPort(source.getContainer(), (Output) source.getVariable()); } } - // Increment the reaction count even if not in the federate for consistency. - reactionCount++; + // Increment the reaction count even if not in the federate for consistency. + reactionCount++; } } diff --git a/org.lflang/src/org/lflang/generator/cpp/CppDelayBodyGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppDelayBodyGenerator.kt index 3a03f889c7..45be5cb14d 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppDelayBodyGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppDelayBodyGenerator.kt @@ -42,5 +42,4 @@ object CppDelayBodyGenerator : DelayBodyGenerator { override fun generateDelayGeneric() = "T" override fun generateAfterDelaysWithVariableWidth() = false - } diff --git a/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt b/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt index 8ca1b41fd0..ec4ad15713 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt @@ -42,18 +42,18 @@ class CppFileConfig(resource: Resource, srcGenBasePath: Path, useHierarchicalBin @Throws(IOException::class) override fun doClean() { super.doClean() - cppBuildDirectories.forEach { FileUtil.deleteDirectory(it) } + this.cppBuildDirectories.forEach { FileUtil.deleteDirectory(it) } } val cppBuildDirectories = listOf( - outPath.resolve("build"), - outPath.resolve("lib"), - outPath.resolve("include"), - outPath.resolve("share") + this.outPath.resolve("build"), + this.outPath.resolve("lib"), + this.outPath.resolve("include"), + this.outPath.resolve("share") ) /** Relative path to the directory where all source files for this resource should be generated in. */ - private fun getGenDir(r: Resource): Path = getDirectory(r).resolve(r.name) + private fun getGenDir(r: Resource): Path = this.getDirectory(r).resolve(r.name) /** Path to the preamble header file corresponding to this resource */ fun getPreambleHeaderPath(r: Resource): Path = getGenDir(r).resolve("_lf_preamble.hh") @@ -71,5 +71,5 @@ class CppFileConfig(resource: Resource, srcGenBasePath: Path, useHierarchicalBin fun getReactorSourcePath(r: Reactor): Path = getGenDir(r.eResource()).resolve("${r.name}.cc") /** Path to the build directory containing CMake-generated files */ - val buildPath: Path get() = outPath.resolve("build") + val buildPath: Path get() = this.outPath.resolve("build") } \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt index c4608bbcec..f287164a42 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt @@ -46,16 +46,17 @@ import java.nio.file.Path @Suppress("unused") class CppGenerator( - val cppFileConfig: CppFileConfig, - errorReporter: ErrorReporter, + val context: LFGeneratorContext, private val scopeProvider: LFGlobalScopeProvider ) : - GeneratorBase(cppFileConfig, errorReporter) { + GeneratorBase(context) { // keep a list of all source files we generate val cppSources = mutableListOf() val codeMaps = mutableMapOf() + val fileConfig: CppFileConfig = context.fileConfig as CppFileConfig + companion object { /** Path to the Cpp lib directory (relative to class path) */ const val libDir = "/lib/cpp" @@ -72,7 +73,7 @@ class CppGenerator( if (!canGenerate(errorsOccurred(), mainDef, errorReporter, context)) return - // create a platform specifi generator + // create a platform-specific generator val platformGenerator: CppPlatformGenerator = if (targetConfig.ros2) CppRos2Generator(this) else CppStandaloneGenerator(this) @@ -84,15 +85,15 @@ class CppGenerator( if (targetConfig.noCompile || errorsOccurred()) { println("Exiting before invoking target compiler.") - context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(codeMaps)) + context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(context, codeMaps)) } else if (context.mode == Mode.LSP_MEDIUM) { context.reportProgress( "Code generation complete. Validating generated code...", IntegratedBuilder.GENERATED_PERCENT_PROGRESS ) if (platformGenerator.doCompile(context)) { - CppValidator(cppFileConfig, errorReporter, codeMaps).doValidate(context) - context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(codeMaps)) + CppValidator(fileConfig, errorReporter, codeMaps).doValidate(context) + context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(context, codeMaps)) } else { context.unsuccessfulFinish() } @@ -101,7 +102,7 @@ class CppGenerator( "Code generation complete. Compiling...", IntegratedBuilder.GENERATED_PERCENT_PROGRESS ) if (platformGenerator.doCompile(context)) { - context.finish(GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig, codeMaps) + context.finish(GeneratorResult.Status.COMPILED, codeMaps) } else { context.unsuccessfulFinish() } @@ -152,9 +153,9 @@ class CppGenerator( // generate header and source files for all reactors for (r in reactors) { - val generator = CppReactorGenerator(r, cppFileConfig, errorReporter) - val headerFile = cppFileConfig.getReactorHeaderPath(r) - val sourceFile = if (r.isGeneric) cppFileConfig.getReactorHeaderImplPath(r) else cppFileConfig.getReactorSourcePath(r) + val generator = CppReactorGenerator(r, fileConfig, errorReporter) + val headerFile = fileConfig.getReactorHeaderPath(r) + val sourceFile = if (r.isGeneric) fileConfig.getReactorHeaderImplPath(r) else fileConfig.getReactorSourcePath(r) val reactorCodeMap = CodeMap.fromGeneratedCode(generator.generateSource()) if (!r.isGeneric) cppSources.add(sourceFile) @@ -168,9 +169,9 @@ class CppGenerator( // generate file level preambles for all resources for (r in resources) { - val generator = CppPreambleGenerator(r.eResource, cppFileConfig, scopeProvider) - val sourceFile = cppFileConfig.getPreambleSourcePath(r.eResource) - val headerFile = cppFileConfig.getPreambleHeaderPath(r.eResource) + val generator = CppPreambleGenerator(r.eResource, fileConfig, scopeProvider) + val sourceFile = fileConfig.getPreambleSourcePath(r.eResource) + val headerFile = fileConfig.getPreambleHeaderPath(r.eResource) val preambleCodeMap = CodeMap.fromGeneratedCode(generator.generateSource()) cppSources.add(sourceFile) codeMaps[srcGenPath.resolve(sourceFile)] = preambleCodeMap diff --git a/org.lflang/src/org/lflang/generator/cpp/CppPlatformGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppPlatformGenerator.kt index e659a6b565..102657522b 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppPlatformGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppPlatformGenerator.kt @@ -12,12 +12,12 @@ abstract class CppPlatformGenerator(protected val generator: CppGenerator) { protected val codeMaps = generator.codeMaps protected val cppSources = generator.cppSources protected val errorReporter: ErrorReporter = generator.errorReporter - protected val fileConfig = generator.cppFileConfig + protected val fileConfig: CppFileConfig = generator.fileConfig protected val targetConfig: TargetConfig = generator.targetConfig protected val commandFactory: GeneratorCommandFactory = generator.commandFactory protected val mainReactor = generator.mainDef.reactorClass.toDefinition() - open val srcGenPath: Path = generator.cppFileConfig.srcGenPath + open val srcGenPath: Path = generator.fileConfig.srcGenPath abstract fun generatePlatformFiles() diff --git a/org.lflang/src/org/lflang/generator/cpp/CppRos2Generator.kt b/org.lflang/src/org/lflang/generator/cpp/CppRos2Generator.kt index a13036ccaf..a689a53c88 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppRos2Generator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppRos2Generator.kt @@ -7,8 +7,8 @@ import java.nio.file.Path /** C++ platform generator for the ROS2 platform.*/ class CppRos2Generator(generator: CppGenerator) : CppPlatformGenerator(generator) { - override val srcGenPath: Path = generator.cppFileConfig.srcGenPath.resolve("src") - private val packagePath: Path = generator.cppFileConfig.srcGenPath + override val srcGenPath: Path = generator.fileConfig.srcGenPath.resolve("src") + private val packagePath: Path = generator.fileConfig.srcGenPath private val nodeGenerator = CppRos2NodeGenerator(mainReactor, targetConfig, fileConfig); private val packageGenerator = CppRos2PackageGenerator(generator, nodeGenerator.nodeName) @@ -66,4 +66,4 @@ class CppRos2Generator(generator: CppGenerator) : CppPlatformGenerator(generator return !errorReporter.errorsOccurred } -} \ No newline at end of file +} diff --git a/org.lflang/src/org/lflang/generator/cpp/CppRos2PackageGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppRos2PackageGenerator.kt index 3709d15e81..b4e00262aa 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppRos2PackageGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppRos2PackageGenerator.kt @@ -7,7 +7,7 @@ import java.nio.file.Path /** A C++ code generator for creating the required files for defining a ROS2 package. */ class CppRos2PackageGenerator(generator: CppGenerator, private val nodeName: String) { - private val fileConfig = generator.cppFileConfig + private val fileConfig = generator.fileConfig private val targetConfig = generator.targetConfig val reactorCppSuffix = targetConfig.runtimeVersion ?: "default" val reactorCppName = "reactor-cpp-$reactorCppSuffix" diff --git a/org.lflang/src/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt index 29acf94d65..1464d6514f 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt @@ -24,6 +24,7 @@ package org.lflang.generator.cpp +import org.lflang.FileConfig import org.lflang.TargetConfig import org.lflang.generator.PrependOperator import org.lflang.joinWithLn @@ -31,7 +32,7 @@ import org.lflang.toUnixString import java.nio.file.Path /** Code generator for producing a cmake script for compiling all generated C++ sources */ -class CppStandaloneCmakeGenerator(private val targetConfig: TargetConfig, private val fileConfig: CppFileConfig) { +class CppStandaloneCmakeGenerator(private val targetConfig: TargetConfig, private val fileConfig: FileConfig) { companion object { /** Return the name of the variable that gives the includes of the given target. */ diff --git a/org.lflang/src/org/lflang/generator/cpp/CppStandaloneGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneGenerator.kt index 2d64f9860a..bd4b58f3cd 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppStandaloneGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneGenerator.kt @@ -19,7 +19,13 @@ class CppStandaloneGenerator(generator: CppGenerator) : // generate the main source file (containing main()) val mainFile = Paths.get("main.cc") val mainCodeMap = - CodeMap.fromGeneratedCode(CppStandaloneMainGenerator(mainReactor, generator.targetConfig, fileConfig).generateCode()) + CodeMap.fromGeneratedCode( + CppStandaloneMainGenerator( + mainReactor, + generator.targetConfig, + generator.fileConfig + ).generateCode() + ) cppSources.add(mainFile) codeMaps[fileConfig.srcGenPath.resolve(mainFile)] = mainCodeMap println("Path: $srcGenPath $srcGenPath") @@ -27,7 +33,7 @@ class CppStandaloneGenerator(generator: CppGenerator) : FileUtil.writeToFile(mainCodeMap.generatedCode, srcGenPath.resolve(mainFile), true) // generate the cmake scripts - val cmakeGenerator = CppStandaloneCmakeGenerator(targetConfig, fileConfig) + val cmakeGenerator = CppStandaloneCmakeGenerator(targetConfig, generator.fileConfig) val srcGenRoot = fileConfig.srcGenBasePath val pkgName = fileConfig.srcGenPkgPath.fileName.toString() FileUtil.writeToFile(cmakeGenerator.generateRootCmake(pkgName), srcGenRoot.resolve("CMakeLists.txt"), true) diff --git a/org.lflang/src/org/lflang/generator/python/PyFileConfig.java b/org.lflang/src/org/lflang/generator/python/PyFileConfig.java new file mode 100644 index 0000000000..bb6a9e9796 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PyFileConfig.java @@ -0,0 +1,35 @@ +package org.lflang.generator.python; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.eclipse.emf.ecore.resource.Resource; + +import org.lflang.FileConfig; +import org.lflang.generator.c.CFileConfig; +import org.lflang.util.LFCommand; + +public class PyFileConfig extends CFileConfig { + public PyFileConfig(Resource resource, Path srcGenBasePath, boolean useHierarchicalBin) throws IOException { + super(resource, srcGenBasePath, useHierarchicalBin); + } + + @Override + public LFCommand getCommand() { + return LFCommand.get("python3", + List.of(srcPkgPath.relativize(getExecutable()).toString()), + true, + srcPkgPath); + } + + @Override + public Path getExecutable() { + return srcGenPath.resolve(name + getExecutableExtension()); + } + + @Override + protected String getExecutableExtension() { + return ".py"; + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonDelayBodyGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonDelayBodyGenerator.java index 472a3b8769..79399ed558 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonDelayBodyGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonDelayBodyGenerator.java @@ -2,9 +2,11 @@ import org.lflang.ASTUtils; +import org.lflang.federated.generator.FedASTUtils; import org.lflang.generator.c.CDelayBodyGenerator; import org.lflang.generator.c.CUtil; import org.lflang.lf.Action; +import org.lflang.lf.Reaction; import org.lflang.lf.VarRef; public class PythonDelayBodyGenerator extends CDelayBodyGenerator { @@ -71,4 +73,9 @@ public String generateForwardBody(Action action, VarRef port) { } } + @Override + public void finalizeReactions(Reaction delayReaction, Reaction forwardReaction) { + ASTUtils.addReactionAttribute(delayReaction, "_c_body"); + ASTUtils.addReactionAttribute(forwardReaction, "_c_body"); + } } diff --git a/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java index a5398bf250..ed69ac01fc 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java @@ -2,6 +2,7 @@ package org.lflang.generator.python; import org.lflang.TargetConfig; +import org.lflang.generator.LFGeneratorContext; import org.lflang.generator.c.CDockerGenerator; /** @@ -12,26 +13,24 @@ public class PythonDockerGenerator extends CDockerGenerator { final String defaultBaseImage = "python:slim"; - public PythonDockerGenerator(boolean isFederated, TargetConfig targetConfig) { - super(isFederated, false, targetConfig); + public PythonDockerGenerator(LFGeneratorContext context) { + super(context); } /** * Generates the contents of the docker file. - * - * @param generatorData Data from the code generator. */ @Override - protected String generateDockerFileContent(CGeneratorData generatorData) { + protected String generateDockerFileContent() { var baseImage = defaultBaseImage; return String.join("\n", - "# For instructions, see: https://github.com/icyphy/lingua-franca/wiki/Containerized-Execution", + "# For instructions, see: https://www.lf-lang.org/docs/handbook/containerized-execution?target=py", "FROM "+baseImage, - "WORKDIR /lingua-franca/"+generatorData.lfModuleName(), + "WORKDIR /lingua-franca/"+context.getFileConfig().name, "RUN set -ex && apt-get update && apt-get install -y python3-pip && pip install cmake", "COPY . src-gen", super.generateDefaultCompileCommand(), - "ENTRYPOINT [\"python3\", \"src-gen/"+generatorData.lfModuleName()+".py\"]" + "ENTRYPOINT [\"python3\", \"src-gen/"+context.getFileConfig().name+".py\"]" ); } } diff --git a/org.lflang/src/org/lflang/generator/python/PythonGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonGenerator.java index bf1d1f8e17..46bcc4f811 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonGenerator.java @@ -1,17 +1,13 @@ /************* * Copyright (c) 2022, The University of California at Berkeley. * Copyright (c) 2022, The University of Texas at Dallas. - * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL @@ -22,6 +18,7 @@ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ***************/ + package org.lflang.generator.python; import java.io.File; @@ -39,26 +36,16 @@ import java.util.stream.Stream; import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.xtext.util.CancelIndicator; import org.eclipse.xtext.xbase.lib.Exceptions; -import org.eclipse.xtext.xbase.lib.IterableExtensions; import org.lflang.ASTUtils; -import org.lflang.ErrorReporter; -import org.lflang.FileConfig; -import org.lflang.InferredType; +import org.lflang.AttributeUtils; import org.lflang.Target; import org.lflang.TargetProperty; -import org.lflang.federated.FedFileConfig; -import org.lflang.federated.FederateInstance; -import org.lflang.federated.launcher.FedPyLauncher; -import org.lflang.federated.serialization.FedNativePythonSerialization; -import org.lflang.federated.serialization.SupportedSerializers; import org.lflang.generator.CodeBuilder; import org.lflang.generator.CodeMap; -import org.lflang.generator.DelayBodyGenerator; + import org.lflang.generator.GeneratorResult; -import org.lflang.generator.GeneratorUtils; import org.lflang.generator.IntegratedBuilder; import org.lflang.generator.LFGeneratorContext; import org.lflang.generator.ReactorInstance; @@ -68,7 +55,6 @@ import org.lflang.generator.c.CGenerator; import org.lflang.generator.c.CUtil; import org.lflang.lf.Action; -import org.lflang.lf.Expression; import org.lflang.lf.Input; import org.lflang.lf.Model; import org.lflang.lf.Output; @@ -76,22 +62,25 @@ import org.lflang.lf.Reaction; import org.lflang.lf.Reactor; import org.lflang.lf.ReactorDecl; -import org.lflang.lf.VarRef; import org.lflang.util.FileUtil; import org.lflang.util.LFCommand; - -import com.google.common.base.Objects; +import org.lflang.util.StringUtil; /** - * Generator for Python target. This class generates Python code defining each reactor + * Generator for Python target. This class generates Python code defining each + * reactor * class given in the input .lf file and imported .lf files. * - * Each class will contain all the reaction functions defined by the user in order, with the necessary ports/actions given as parameters. - * Moreover, each class will contain all state variables in native Python format. + * Each class will contain all the reaction functions defined by the user in + * order, with the necessary ports/actions given as parameters. + * Moreover, each class will contain all state variables in native Python + * format. * - * A backend is also generated using the CGenerator that interacts with the C code library (see CGenerator.xtend). - * The backend is responsible for passing arguments to the Python reactor functions. + * A backend is also generated using the CGenerator that interacts with the C + * code library (see CGenerator.xtend). + * The backend is responsible for passing arguments to the Python reactor + * functions. * * @author Soroush Bateni */ @@ -105,13 +94,11 @@ public class PythonGenerator extends CGenerator { private final PythonTypes types; - public PythonGenerator(FileConfig fileConfig, ErrorReporter errorReporter) { - this( - fileConfig, - errorReporter, - new PythonTypes(errorReporter), + public PythonGenerator(LFGeneratorContext context) { + this(context, + new PythonTypes(context.getErrorReporter()), new CCmakeGenerator( - fileConfig, + context.getFileConfig(), List.of("lib/python_action.c", "lib/python_port.c", "lib/python_tag.c", @@ -124,8 +111,9 @@ public PythonGenerator(FileConfig fileConfig, ErrorReporter errorReporter) { ); } - private PythonGenerator(FileConfig fileConfig, ErrorReporter errorReporter, PythonTypes types, CCmakeGenerator cmakeGenerator) { - super(fileConfig, errorReporter, false, types, cmakeGenerator, new PythonDelayBodyGenerator(types)); + + private PythonGenerator(LFGeneratorContext context, PythonTypes types, CCmakeGenerator cmakeGenerator) { + super(context, false, types, cmakeGenerator, new PythonDelayBodyGenerator(types)); this.targetConfig.compiler = "gcc"; this.targetConfig.compilerFlags = new ArrayList<>(); this.targetConfig.linkerFlags = ""; @@ -156,14 +144,14 @@ private PythonGenerator(FileConfig fileConfig, ErrorReporter errorReporter, Pyth /** * Generic struct for actions. * This template is defined as - * typedef struct { - * trigger_t* trigger; - * PyObject* value; - * bool is_present; - * bool has_value; - * lf_token_t* token; - * FEDERATED_CAPSULE_EXTENSION - * } generic_action_instance_struct; + * typedef struct { + * trigger_t* trigger; + * PyObject* value; + * bool is_present; + * bool has_value; + * lf_token_t* token; + * FEDERATED_CAPSULE_EXTENSION + * } generic_action_instance_struct; * * See reactor-c-py/lib/pythontarget.h for details. */ @@ -186,36 +174,39 @@ public TargetTypes getTargetTypes() { // ////////////////////////////////////////// // // Protected methods + /** * Generate all Python classes if they have a reaction - * @param federate The federate instance used to generate classes + * */ - public String generatePythonReactorClasses(FederateInstance federate) { + public String generatePythonReactorClasses() { CodeBuilder pythonClasses = new CodeBuilder(); CodeBuilder pythonClassesInstantiation = new CodeBuilder(); // Generate reactor classes in Python - pythonClasses.pr(PythonReactorGenerator.generatePythonClass(main, federate, main, types)); + pythonClasses.pr(PythonReactorGenerator.generatePythonClass(main, main, types)); // Create empty lists to hold reactor instances - pythonClassesInstantiation.pr(PythonReactorGenerator.generateListsToHoldClassInstances(main, federate)); + pythonClassesInstantiation.pr(PythonReactorGenerator.generateListsToHoldClassInstances(main)); // Instantiate generated classes - pythonClassesInstantiation.pr(PythonReactorGenerator.generatePythonClassInstantiations(main, federate, main)); + pythonClassesInstantiation.pr(PythonReactorGenerator.generatePythonClassInstantiations(main, main)); return String.join("\n", - pythonClasses.toString(), - "", - "# Instantiate classes", - pythonClassesInstantiation.toString() + pythonClasses.toString(), + "", + "# Instantiate classes", + pythonClassesInstantiation.toString() ); } /** - * Generate the Python code constructed from reactor classes and user-written classes. + * Generate the Python code constructed from reactor classes and + * user-written classes. + * * @return the code body */ - public String generatePythonCode(FederateInstance federate, String pyModuleName) { + public String generatePythonCode(String pyModuleName) { return String.join("\n", "import os", "import sys", @@ -243,7 +234,7 @@ public String generatePythonCode(FederateInstance federate, String pyModuleName) "", pythonPreamble.toString(), "", - generatePythonReactorClasses(federate), + generatePythonReactorClasses(), "", PythonMainFunctionGenerator.generateCode() ); @@ -251,10 +242,9 @@ public String generatePythonCode(FederateInstance federate, String pyModuleName) /** * Generate the necessary Python files. - * @param federate The federate instance */ public Map generatePythonFiles( - FederateInstance federate, + String lfModuleName, String pyModuleName, String pyFileName ) throws IOException { @@ -271,7 +261,7 @@ public Map generatePythonFiles( } Map codeMaps = new HashMap<>(); codeMaps.put(filePath, CodeMap.fromGeneratedCode( - generatePythonCode(federate, pyModuleName))); + generatePythonCode(pyModuleName))); FileUtil.writeToFile(codeMaps.get(filePath).getGeneratedCode(), filePath); return codeMaps; } @@ -286,19 +276,20 @@ public String generateDirectives() { code.prComment("Code generated by the Lingua Franca compiler from:"); code.prComment("file:/" + FileUtil.toUnixString(fileConfig.srcFile)); code.pr(PythonPreambleGenerator.generateCDefineDirectives( - targetConfig, federates.size(), isFederated, - fileConfig.getSrcGenPath(), clockSyncIsOn(), hasModalReactors)); + targetConfig, fileConfig.getSrcGenPath(), hasModalReactors)); code.pr(PythonPreambleGenerator.generateCIncludeStatements( - targetConfig, targetLanguageIsCpp(), isFederated, hasModalReactors)); + targetConfig, targetLanguageIsCpp(), hasModalReactors)); return code.toString(); } /** - * Override generate top-level preambles, but put the preambles in the - * .py file rather than the C file. + * Override generate top-level preambles, but put the user preambles in the + * .py file rather than the C file. Also handles including the federated + * execution setup preamble specified in the target config. */ @Override protected String generateTopLevelPreambles() { + // user preambles Set models = new LinkedHashSet<>(); for (Reactor r : ASTUtils.convertToEmptyListIfNull(reactors)) { // The following assumes all reactors have a container. @@ -313,39 +304,23 @@ protected String generateTopLevelPreambles() { for (Model m : models) { pythonPreamble.pr(PythonPreambleGenerator.generatePythonPreambles(m.getPreambles())); } - return ""; + + // C preamble for federated execution setup + String ret = ""; + if (targetConfig.fedSetupPreamble != null) { + ret = "#include \"" + targetConfig.fedSetupPreamble + "\""; + } + return ret; } - /** - * Add necessary code to the source and necessary build supports to - * enable the requested serializations in 'enabledSerializations' - */ @Override - public void enableSupportForSerializationIfApplicable(CancelIndicator cancelIndicator) { - if (!IterableExtensions.isNullOrEmpty(targetConfig.protoFiles)) { - // Enable support for proto serialization - enabledSerializers.add(SupportedSerializers.PROTO); - } - for (SupportedSerializers serialization : enabledSerializers) { - switch (serialization) { - case NATIVE: { - FedNativePythonSerialization pickler = new FedNativePythonSerialization(); - code.pr(pickler.generatePreambleForSupport().toString()); - } - case PROTO: { - // Handle .proto files. - for (String name : targetConfig.protoFiles) { - this.processProtoFile(name, cancelIndicator); - int dotIndex = name.lastIndexOf("."); - String rootFilename = dotIndex > 0 ? name.substring(0, dotIndex) : name; - pythonPreamble.pr("import "+rootFilename+"_pb2 as "+rootFilename); - protoNames.add(rootFilename); - } - } - case ROS2: { - // FIXME: Not supported yet - } - } + protected void handleProtoFiles() { + for (String name : targetConfig.protoFiles) { + this.processProtoFile(name); + int dotIndex = name.lastIndexOf("."); + String rootFilename = dotIndex > 0 ? name.substring(0, dotIndex) : name; + pythonPreamble.pr("import "+rootFilename+"_pb2 as "+rootFilename); + protoNames.add(rootFilename); } } @@ -354,133 +329,25 @@ public void enableSupportForSerializationIfApplicable(CancelIndicator cancelIndi * * Run, if possible, the proto-c protocol buffer code generator to produce * the required .h and .c files. + * * @param filename Name of the file to process. */ @Override - public void processProtoFile(String filename, CancelIndicator cancelIndicator) { + public void processProtoFile(String filename) { LFCommand protoc = commandFactory.createCommand( - "protoc", List.of("--python_out="+fileConfig.getSrcGenPath(), filename), fileConfig.srcPath); + "protoc", List.of("--python_out=" + + fileConfig.getSrcGenPath(), filename), fileConfig.srcPath); if (protoc == null) { errorReporter.reportError("Processing .proto files requires libprotoc >= 3.6.1"); return; } - int returnCode = protoc.run(cancelIndicator); + int returnCode = protoc.run(); if (returnCode == 0) { pythonRequiredModules.add("google-api-python-client"); } else { - errorReporter.reportError("protoc returns error code " + returnCode); - } - } - - /** - * Generate code for the body of a reaction that handles the - * action that is triggered by receiving a message from a remote - * federate. - * @param action The action. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param receivingFed The destination federate. - * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. - * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. - * @param type The type. - * @param isPhysical Indicates whether or not the connection is physical - * @param serializer The serializer used on the connection. - */ - @Override - public String generateNetworkReceiverBody( - Action action, - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - FederateInstance receivingFed, - int receivingBankIndex, - int receivingChannelIndex, - InferredType type, - boolean isPhysical, - SupportedSerializers serializer - ) { - return PythonNetworkGenerator.generateNetworkReceiverBody( - action, - sendingPort, - receivingPort, - receivingPortID, - sendingFed, - receivingFed, - receivingBankIndex, - receivingChannelIndex, - type, - isPhysical, - serializer, - targetConfig.coordination - ); - } - - /** - * Generate code for the body of a reaction that handles an output - * that is to be sent over the network. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The variable reference to the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. - * @param receivingFed The destination federate. - * @param type The type. - * @param isPhysical Indicates whether the connection is physical or not - * @param delay The delay value imposed on the connection using after - * @param serializer The serializer used on the connection. - */ - @Override - public String generateNetworkSenderBody( - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - int sendingBankIndex, - int sendingChannelIndex, - FederateInstance receivingFed, - InferredType type, - boolean isPhysical, - Expression delay, - SupportedSerializers serializer - ) { - return PythonNetworkGenerator.generateNetworkSenderBody( - sendingPort, - receivingPort, - receivingPortID, - sendingFed, - sendingBankIndex, - sendingChannelIndex, - receivingFed, - type, - isPhysical, - delay, - serializer, - targetConfig.coordination - ); - } - - /** - * Create a launcher script that executes all the federates and the RTI. - */ - @Override - public void createFederatedLauncher() { - FedPyLauncher launcher = new FedPyLauncher( - targetConfig, - fileConfig, - errorReporter - ); - try { - launcher.createLauncher( - federates, - federationRTIProperties - ); - } catch (IOException e) { - // ignore + errorReporter.reportError( + "protoc returns error code " + returnCode); } } @@ -504,7 +371,7 @@ public void generateAuxiliaryStructs( } // Finally, handle actions. for (Action action : ASTUtils.allActions(reactor)) { - generateAuxiliaryStructsForAction(decl, currentFederate, action); + generateAuxiliaryStructsForAction(decl, action); } } @@ -517,11 +384,7 @@ private void generateAuxiliaryStructsForPort(ReactorDecl decl, } private void generateAuxiliaryStructsForAction(ReactorDecl decl, - FederateInstance federate, Action action) { - if (federate != null && !federate.contains(action)) { - return; - } code.pr(action, PythonActionGenerator.generateAliasTypeDef(decl, action, genericActionType)); } @@ -531,21 +394,16 @@ private void generateAuxiliaryStructsForAction(ReactorDecl decl, */ @Override public boolean isOSCompatible() { - if (GeneratorUtils.isHostWindows() && isFederated) { - errorReporter.reportError( - "Federated LF programs with a Python target are currently not supported on Windows. Exiting code generation." - ); - // Return to avoid compiler errors - return false; - } return true; } - /** Generate C code from the Lingua Franca model contained by the - * specified resource. This is the main entry point for code - * generation. - * @param resource The resource containing the source code. - * @param context Context relating to invocation of the code generator. + /** + * Generate C code from the Lingua Franca model contained by the + * specified resource. This is the main entry point for code + * generation. + * + * @param resource The resource containing the source code. + * @param context Context relating to invocation of the code generator. */ @Override public void doGenerate(Resource resource, LFGeneratorContext context) { @@ -560,83 +418,42 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { IntegratedBuilder.VALIDATED_PERCENT_PROGRESS, cGeneratedPercentProgress )); - SubContext compilingFederatesContext = new SubContext(context, cGeneratedPercentProgress, 100); if (errorsOccurred()) { context.unsuccessfulFinish(); return; } - // Keep a separate file config for each federate - FileConfig oldFileConfig = fileConfig; - var federateCount = 0; Map codeMaps = new HashMap<>(); - for (FederateInstance federate : federates) { - federateCount++; - var lfModuleName = isFederated ? fileConfig.name + "_" + federate.name : fileConfig.name; - if (isFederated) { - try { - fileConfig = new FedFileConfig(fileConfig, federate.name); - } catch (IOException e) { - //noinspection ConstantConditions - throw Exceptions.sneakyThrow(e); + var lfModuleName = fileConfig.name; + // Don't generate code if there is no main reactor + if (this.main != null) { + try { + Map codeMapsForFederate = generatePythonFiles(lfModuleName, generatePythonModuleName(lfModuleName), generatePythonFileName(lfModuleName)); + codeMaps.putAll(codeMapsForFederate); + copyTargetFiles(); + new PythonValidator(fileConfig, errorReporter, codeMaps, protoNames).doValidate(context); + if (targetConfig.noCompile) { + System.out.println(PythonInfoGenerator.generateSetupInfo(fileConfig)); } + } catch (Exception e) { + //noinspection ConstantConditions + throw Exceptions.sneakyThrow(e); } - // Don't generate code if there is no main reactor - if (this.main != null) { - try { - Map codeMapsForFederate = generatePythonFiles( - federate, - generatePythonModuleName(lfModuleName), - generatePythonFileName(lfModuleName) - ); - codeMaps.putAll(codeMapsForFederate); - if (!targetConfig.noCompile) { - compilingFederatesContext.reportProgress( - String.format("Validating %d/%d sets of generated files...", federateCount, federates.size()), - 100 * federateCount / federates.size() - ); - // If there are no federates, compile and install the generated code - new PythonValidator(fileConfig, errorReporter, codeMaps, protoNames).doValidate(context); - if (!errorsOccurred() && !Objects.equal(context.getMode(), LFGeneratorContext.Mode.LSP_MEDIUM)) { - compilingFederatesContext.reportProgress( - String.format("Validation complete. Compiling and installing %d/%d Python modules...", - federateCount, federates.size()), - 100 * federateCount / federates.size() - ); - } - } else { - System.out.println(PythonInfoGenerator.generateSetupInfo(fileConfig)); - } - } catch (Exception e) { - //noinspection ConstantConditions - throw Exceptions.sneakyThrow(e); - } - if (!isFederated) { - System.out.println(PythonInfoGenerator.generateRunInfo(fileConfig, lfModuleName)); - } - } - fileConfig = oldFileConfig; - } - if (isFederated) { - System.out.println(PythonInfoGenerator.generateFedRunInfo(fileConfig)); + System.out.println(PythonInfoGenerator.generateRunInfo(fileConfig, lfModuleName)); } if (errorReporter.getErrorsOccurred()) { context.unsuccessfulFinish(); - } else if (!isFederated) { - context.finish(GeneratorResult.Status.COMPILED, fileConfig.name+".py", fileConfig.getSrcGenPath(), fileConfig, - codeMaps, "python3"); // TODO: Conditionally use "python" instead } else { - context.finish(GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig.binPath, fileConfig, codeMaps, - "bash"); + context.finish(GeneratorResult.Status.COMPILED, codeMaps); } } @Override - protected PythonDockerGenerator getDockerGenerator() { - return new PythonDockerGenerator(isFederated, targetConfig); + protected PythonDockerGenerator getDockerGenerator(LFGeneratorContext context) { + return new PythonDockerGenerator(context); } /** Generate a reaction function definition for a reactor. @@ -651,22 +468,33 @@ protected PythonDockerGenerator getDockerGenerator() { protected void generateReaction(Reaction reaction, ReactorDecl decl, int reactionIndex) { Reactor reactor = ASTUtils.toDefinition(decl); - // Delay reactors and top-level reactions used in the top-level reactor(s) in federated execution are generated in C - if (reactor.getName().contains(DelayBodyGenerator.GEN_DELAY_CLASS_NAME) || - mainDef != null && decl == mainDef.getReactorClass() && reactor.isFederated()) { + // Reactions marked with a `@_c_body` attribute are generated in C + if (AttributeUtils.hasCBody(reaction)) { super.generateReaction(reaction, decl, reactionIndex); return; } - code.pr(PythonReactionGenerator.generateCReaction(reaction, decl, reactionIndex, mainDef, errorReporter, types, isFederatedAndDecentralized())); + code.pr(PythonReactionGenerator.generateCReaction(reaction, decl, reactionIndex, mainDef, errorReporter, types)); } + /** + * Generate code that initializes the state variables for a given instance. + * Unlike parameters, state variables are uniformly initialized for all + * instances + * of the same reactor. This task is left to Python code to allow for more + * liberal + * state variable assignments. + * + * @param instance The reactor class instance + * @return Initialization code fore state variables of instance + */ @Override protected void generateStateVariableInitializations(ReactorInstance instance) { // Do nothing } /** - * 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 * * @param instance The reactor instance. */ @@ -688,6 +516,7 @@ protected void generateMethods(ReactorDecl reactor) { } * Generate C preambles defined by user for a given reactor * Since the Python generator expects preambles written in C, * this function is overridden and does nothing. + * * @param reactor The given reactor */ @Override @@ -696,7 +525,8 @@ protected void generateUserPreamblesForReactor(Reactor reactor) { } /** - * Generate code that is executed while the reactor instance is being initialized. + * Generate code that is executed while the reactor instance is being + * initialized. * This wraps the reaction functions in a Python function. * @param instance The reactor instance. */ @@ -741,9 +571,9 @@ protected String getConflictingConnectionsInModalReactorsBody(String source, Str // NOTE: Strangely, a newline is needed at the beginning or indentation // gets swallowed. return String.join("\n", - "\n# Generated forwarding reaction for connections with the same destination", - "# but located in mutually exclusive modes.", - dest+".set("+source+".value)\n" + "\n# Generated forwarding reaction for connections with the same destination", + "# but located in mutually exclusive modes.", + dest + ".set(" + source + ".value)\n" ); } @@ -796,6 +626,18 @@ private static String setUpMainTarget(boolean hasMain, String executableName, St // The use of fileConfig.name will break federated execution, but that's fine } + /** + * Generate a (`key`, `val`) tuple pair for the `define_macros` field + * of the Extension class constructor from setuptools. + * + * @param key The key of the macro entry + * @param val The value of the macro entry + * @return A (`key`, `val`) tuple pair as String + */ + private static String generateMacroEntry(String key, String val) { + return "(" + StringUtil.addDoubleQuotes(key) + ", " + StringUtil.addDoubleQuotes(val) + ")"; + } + /** * Generate the name of the python module. * @@ -844,4 +686,5 @@ protected void copyTargetFiles() throws IOException { true ); } + } diff --git a/org.lflang/src/org/lflang/generator/python/PythonNetworkGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonNetworkGenerator.java deleted file mode 100644 index 1bf7ea5982..0000000000 --- a/org.lflang/src/org/lflang/generator/python/PythonNetworkGenerator.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.lflang.generator.python; - -import org.lflang.federated.FederateInstance; -import org.lflang.federated.PythonGeneratorExtension; -import org.lflang.lf.Expression; -import org.lflang.lf.VarRef; -import org.lflang.lf.Action; -import org.lflang.InferredType; -import org.lflang.TargetProperty.CoordinationType; -import org.lflang.federated.serialization.SupportedSerializers; -import org.lflang.generator.ReactionInstance; - - -public class PythonNetworkGenerator { - /** - * Generate code for the body of a reaction that handles the - * action that is triggered by receiving a message from a remote - * federate. - * @param action The action. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param receivingFed The destination federate. - * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. - * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. - * @param type The type. - * @param isPhysical Indicates whether or not the connection is physical - * @param serializer The serializer used on the connection. - * @param coordinationType The coordination type - */ - public static String generateNetworkReceiverBody( - Action action, - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - FederateInstance receivingFed, - int receivingBankIndex, - int receivingChannelIndex, - InferredType type, - boolean isPhysical, - SupportedSerializers serializer, - CoordinationType coordinationType - ) { - StringBuilder result = new StringBuilder(); - - // We currently have no way to mark a reaction "unordered" - // in the AST, so we use a magic string at the start of the body. - result.append("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); - - result.append(PyUtil.generateGILAcquireCode() + "\n"); - result.append(PythonGeneratorExtension.generateNetworkReceiverBody( - action, - sendingPort, - receivingPort, - receivingPortID, - sendingFed, - receivingFed, - receivingBankIndex, - receivingChannelIndex, - type, - isPhysical, - serializer, - coordinationType - )); - result.append(PyUtil.generateGILReleaseCode() + "\n"); - return result.toString(); - } - - /** - * Generate code for the body of a reaction that handles an output - * that is to be sent over the network. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The variable reference to the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. - * @param receivingFed The destination federate. - * @param type The type. - * @param isPhysical Indicates whether the connection is physical or not - * @param delay The delay value imposed on the connection using after - * @param serializer The serializer used on the connection. - */ - public static String generateNetworkSenderBody( - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - int sendingBankIndex, - int sendingChannelIndex, - FederateInstance receivingFed, - InferredType type, - boolean isPhysical, - Expression delay, - SupportedSerializers serializer, - CoordinationType coordinationType - ) { - StringBuilder result = new StringBuilder(); - - // We currently have no way to mark a reaction "unordered" - // in the AST, so we use a magic string at the start of the body. - result.append("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); - - result.append(PyUtil.generateGILAcquireCode() + "\n"); - result.append(PythonGeneratorExtension.generateNetworkSenderBody( - sendingPort, - receivingPort, - receivingPortID, - sendingFed, - sendingBankIndex, - sendingChannelIndex, - receivingFed, - type, - isPhysical, - delay, - serializer, - coordinationType - )); - result.append(PyUtil.generateGILReleaseCode() + "\n"); - return result.toString(); - } -} diff --git a/org.lflang/src/org/lflang/generator/python/PythonPreambleGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonPreambleGenerator.java index 11220f866a..5fa8aff84f 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonPreambleGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonPreambleGenerator.java @@ -35,17 +35,13 @@ public static String generatePythonPreambles(List preambles) { public static String generateCDefineDirectives( TargetConfig targetConfig, - int numFederates, - boolean isFederated, Path srcGenPath, - boolean clockSyncIsOn, boolean hasModalReactors ) { // TODO: Delete all of this. It is not used. CodeBuilder code = new CodeBuilder(); code.pr(CPreambleGenerator.generateDefineDirectives( - targetConfig, numFederates, isFederated, - srcGenPath, clockSyncIsOn, hasModalReactors) + targetConfig, srcGenPath, hasModalReactors) ); return code.toString(); } @@ -53,11 +49,10 @@ public static String generateCDefineDirectives( public static String generateCIncludeStatements( TargetConfig targetConfig, boolean CCppMode, - boolean isFederated, boolean hasModalReactors ) { CodeBuilder code = new CodeBuilder(); - code.pr(CPreambleGenerator.generateIncludeStatements(targetConfig, CCppMode, isFederated)); + code.pr(CPreambleGenerator.generateIncludeStatements(targetConfig, CCppMode)); code.pr("#include \"pythontarget.h\""); if (hasModalReactors) { code.pr("#include \"modal_models/definitions.h\""); diff --git a/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java index 8c30feec93..77f7352f08 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java @@ -4,10 +4,13 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; + import org.lflang.ASTUtils; +import org.lflang.AttributeUtils; import org.lflang.ErrorReporter; import org.lflang.Target; -import org.lflang.generator.DelayBodyGenerator; + import org.lflang.generator.c.CReactionGenerator; import org.lflang.lf.ReactorDecl; import org.lflang.lf.Reaction; @@ -124,7 +127,6 @@ private static String generateCPythonFunctionCaller(String reactorDeclName, * @param mainDef The main reactor. * @param errorReporter An error reporter. * @param types A helper class for type-related stuff. - * @param isFederatedAndDecentralized True if program is federated and coordination type is decentralized. */ public static String generateCReaction( Reaction reaction, @@ -132,8 +134,7 @@ public static String generateCReaction( int reactionIndex, Instantiation mainDef, ErrorReporter errorReporter, - CTypes types, - boolean isFederatedAndDecentralized + CTypes types ) { // Contains the actual comma separated list of inputs to the reaction of type generic_port_instance_struct. // Each input must be cast to (PyObject *) (aka their descriptors for Py_BuildValue are "O") @@ -143,7 +144,6 @@ public static String generateCReaction( String cInit = CReactionGenerator.generateInitializationForReaction( "", reaction, decl, reactionIndex, types, errorReporter, mainDef, - isFederatedAndDecentralized, Target.Python.requiresTypes); code.pr( "#include " + StringUtil.addDoubleQuotes( @@ -380,17 +380,12 @@ public static String generateCPythonReactionLinkers( Reactor reactor = ASTUtils.toDefinition(instance.getDefinition().getReactorClass()); CodeBuilder code = new CodeBuilder(); - // Delay reactors and top-level reactions used in the top-level reactor(s) in federated execution are generated in C - if (reactor.getName().contains(DelayBodyGenerator.GEN_DELAY_CLASS_NAME) || - instance.getDefinition().getReactorClass() == (mainDef != null ? mainDef.getReactorClass() : null) && - reactor.isFederated()) { - return ""; - } - // Initialize the name field to the unique name of the instance code.pr(nameOfSelfStruct+"->_lf_name = \""+instance.uniqueID()+"_lf\";"); for (ReactionInstance reaction : instance.reactions) { + // Reactions marked with a `@_c_body` attribute are generated in C + if (AttributeUtils.hasCBody(reaction.getDefinition())) continue; // Create a PyObject for each reaction code.pr(generateCPythonReactionLinker(instance, reaction, nameOfSelfStruct)); } @@ -488,6 +483,9 @@ public static String generatePythonReactions(Reactor reactor, List rea * @param reaction The reaction of reactor */ public static String generatePythonReaction(Reactor reactor, Reaction reaction, int reactionIndex) { + // Reactions marked with a `@_c_body` attribute are generated in C + if (AttributeUtils.hasCBody(reaction)) return ""; + CodeBuilder code = new CodeBuilder(); List reactionParameters = new ArrayList<>(); // Will contain parameters for the function (e.g., Foo(x,y,z,...) CodeBuilder inits = new CodeBuilder(); // Will contain initialization code for some parameters diff --git a/org.lflang/src/org/lflang/generator/python/PythonReactorGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonReactorGenerator.java index b6d449b8b5..5778791f76 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonReactorGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonReactorGenerator.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; import org.lflang.ASTUtils; -import org.lflang.federated.FederateInstance; +import org.lflang.federated.generator.FederateInstance; import org.lflang.generator.CodeBuilder; import org.lflang.generator.DelayBodyGenerator; import org.lflang.generator.ParameterInstance; @@ -17,37 +17,29 @@ public class PythonReactorGenerator { * Wrapper function for the more elaborate generatePythonReactorClass that keeps track * of visited reactors to avoid duplicate generation * @param instance The reactor instance to be generated - * @param pythonClasses The class definition is appended to this code builder - * @param federate The federate instance for the reactor instance - * @param instantiatedClasses A list of visited instances to avoid generating duplicates */ - public static String generatePythonClass(ReactorInstance instance, FederateInstance federate, ReactorInstance main, PythonTypes types) { - List instantiatedClasses = new ArrayList(); - return generatePythonClass(instance, federate, instantiatedClasses, main, types); + public static String generatePythonClass(ReactorInstance instance, ReactorInstance main, PythonTypes types) { + List instantiatedClasses = new ArrayList<>(); + return generatePythonClass(instance, instantiatedClasses, main, types); } /** * Generate a Python class corresponding to decl * @param instance The reactor instance to be generated - * @param pythonClasses The class definition is appended to this code builder - * @param federate The federate instance for the reactor instance * @param instantiatedClasses A list of visited instances to avoid generating duplicates */ - public static String generatePythonClass(ReactorInstance instance, FederateInstance federate, + public static String generatePythonClass(ReactorInstance instance, List instantiatedClasses, ReactorInstance main, PythonTypes types) { CodeBuilder pythonClasses = new CodeBuilder(); ReactorDecl decl = instance.getDefinition().getReactorClass(); Reactor reactor = ASTUtils.toDefinition(decl); - String className = instance.getDefinition().getReactorClass().getName(); - if (instance != main && !federate.contains(instance) || - instantiatedClasses == null || - // Do not generate code for delay reactors in Python - className.contains(DelayBodyGenerator.GEN_DELAY_CLASS_NAME)) { + String className = PyUtil.getName(decl); + if (instantiatedClasses == null) { return ""; } - if (federate.contains(instance) && !instantiatedClasses.contains(className)) { + if (!instantiatedClasses.contains(className)) { pythonClasses.pr(generatePythonClassHeader(className)); // Generate preamble code pythonClasses.indent(); @@ -59,10 +51,6 @@ public static String generatePythonClass(ReactorInstance instance, FederateInsta pythonClasses.pr(PythonMethodGenerator.generateMethods(reactor)); // Generate reactions List reactionToGenerate = ASTUtils.allReactions(reactor); - if (reactor.isFederated()) { - // Filter out reactions that are automatically generated in C in the top level federated reactor - reactionToGenerate.removeIf(it -> !federate.contains(it) || federate.networkReactions.contains(it)); - } pythonClasses.pr(PythonReactionGenerator.generatePythonReactions(reactor, reactionToGenerate)); pythonClasses.unindent(); pythonClasses.pr("\n"); @@ -70,7 +58,7 @@ public static String generatePythonClass(ReactorInstance instance, FederateInsta } for (ReactorInstance child : instance.children) { - pythonClasses.pr(generatePythonClass(child, federate, instantiatedClasses, main, types)); + pythonClasses.pr(generatePythonClass(child, instantiatedClasses, main, types)); } return pythonClasses.getCode(); } @@ -104,18 +92,12 @@ private static String generatePythonConstructor(ReactorDecl decl, PythonTypes ty * the same for the children of instance as well. * * @param instance The reactor instance for which the Python list will be created. - * @param federate Will check if instance (or any of its children) belong to - * federate before generating code for them. */ - public static String generateListsToHoldClassInstances(ReactorInstance instance, - FederateInstance federate) { + public static String generateListsToHoldClassInstances(ReactorInstance instance) { CodeBuilder code = new CodeBuilder(); - if (federate != null && !federate.contains(instance)) { - return ""; - } code.pr(PyUtil.reactorRefName(instance)+" = [None] * "+instance.getTotalWidth()); for (ReactorInstance child : instance.children) { - code.pr(generateListsToHoldClassInstances(child, federate)); + code.pr(generateListsToHoldClassInstances(child)); } return code.toString(); } @@ -125,24 +107,15 @@ public static String generateListsToHoldClassInstances(ReactorInstance instance, * Instances are always instantiated as a list of className = [_className, _className, ...] depending on the size of the bank. * If there is no bank or the size is 1, the instance would be generated as className = [_className] * @param instance The reactor instance to be instantiated - * @param federate The federate instance for the reactor instance * @param main The main reactor */ public static String generatePythonClassInstantiations(ReactorInstance instance, - FederateInstance federate, ReactorInstance main) { CodeBuilder code = new CodeBuilder(); - // If this is not the main reactor and is not in the federate, nothing to do. - if (instance != main && !federate.contains(instance)) { - return ""; - } - String className = instance.getDefinition().getReactorClass().getName(); - // Do not instantiate delay reactors in Python - if (className.contains(DelayBodyGenerator.GEN_DELAY_CLASS_NAME)) { - return ""; - } - if (federate.contains(instance) && instance.getWidth() > 0) { + String className = PyUtil.getName(instance.reactorDeclaration); + + if (instance.getWidth() > 0) { // For each reactor instance, create a list regardless of whether it is a bank or not. // Non-bank reactor instances will be a list of size 1. var reactorClass = instance.definition.reactorClass String fullName = instance.getFullName(); @@ -158,7 +131,7 @@ public static String generatePythonClassInstantiations(ReactorInstance instance, } for (ReactorInstance child : instance.children) { - code.pr(generatePythonClassInstantiations(child, federate, main)); + code.pr(generatePythonClassInstantiations(child, main)); } code.unindent(); return code.toString(); diff --git a/org.lflang/src/org/lflang/generator/rust/CargoDependencySpec.java b/org.lflang/src/org/lflang/generator/rust/CargoDependencySpec.java index 35e246006c..7fc1c899a2 100644 --- a/org.lflang/src/org/lflang/generator/rust/CargoDependencySpec.java +++ b/org.lflang/src/org/lflang/generator/rust/CargoDependencySpec.java @@ -28,6 +28,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import org.lflang.ASTUtils; @@ -37,6 +38,8 @@ import org.lflang.lf.Array; import org.lflang.lf.Element; import org.lflang.lf.KeyValuePair; +import org.lflang.lf.KeyValuePairs; +import org.lflang.lf.LfFactory; import org.lflang.util.StringUtil; import org.lflang.validation.LFValidator; @@ -72,6 +75,23 @@ public class CargoDependencySpec { } } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CargoDependencySpec that = (CargoDependencySpec) o; + return Objects.equals(version, that.version) + && Objects.equals(gitRepo, that.gitRepo) + && Objects.equals(rev, that.rev) + && Objects.equals(gitTag, that.gitTag) + && Objects.equals(localPath, that.localPath) + && Objects.equals(features, that.features); + } + /** The version. May be null. */ public String getVersion() { return version; @@ -116,7 +136,7 @@ public void setLocalPath(String localPath) { } } - /** Returns the set of features that are enabled on the crate. May not be null. */ + /** Returns the list of features that are enabled on the crate. May be null. */ public Set getFeatures() { return features; } @@ -182,23 +202,14 @@ private static CargoDependencySpec parseValue(Element element, boolean isRuntime pair.getValue(), "Expected string literal for key '" + name + "'"); } switch (name) { - case "version": - version = literal; - break; - case "git": - gitRepo = literal; - break; - case "rev": - rev = literal; - break; - case "tag": - tag = literal; - break; - case "path": - localPath = literal; - break; - default: - throw new InvalidLfSourceException(pair, "Unknown key: '" + name + "'"); + case "version" -> version = literal; + case "git" -> gitRepo = literal; + case "rev" -> rev = literal; + case "tag" -> tag = literal; + case "path" -> localPath = literal; + default -> throw new InvalidLfSourceException(pair, + "Unknown key: '" + name + + "'"); } } if (isRuntimeCrate || version != null || localPath != null || gitRepo != null) { @@ -211,6 +222,49 @@ private static CargoDependencySpec parseValue(Element element, boolean isRuntime throw new InvalidLfSourceException(element, "Expected string or dictionary"); } + /** Extracts an AST representation of a CargoDependencySpec. */ + public static Element extractSpec(CargoDependencySpec spec) { + if (spec.gitRepo == null + && spec.rev == null + && spec.gitTag == null + && spec.localPath == null + && spec.features == null) { + return ASTUtils.toElement(spec.version); + } else { + Element e = LfFactory.eINSTANCE.createElement(); + KeyValuePairs kvp = LfFactory.eINSTANCE.createKeyValuePairs(); + addKvp(kvp, "version", spec.version); + addKvp(kvp, "git", spec.gitRepo); + addKvp(kvp, "rev", spec.rev); + addKvp(kvp, "tag", spec.gitTag); + addKvp(kvp, "path", spec.localPath); + if (spec.features != null && !spec.features.isEmpty()) { + Element subE = LfFactory.eINSTANCE.createElement(); + Array arr = LfFactory.eINSTANCE.createArray(); + for (String f : spec.features) { + arr.getElements().add(ASTUtils.toElement(f)); + } + subE.setArray(arr); + KeyValuePair pair = LfFactory.eINSTANCE.createKeyValuePair(); + pair.setName("features"); + pair.setValue(subE); + kvp.getPairs().add(pair); + } + e.setKeyvalue(kvp); + return e; + } + } + + private static void addKvp(KeyValuePairs pairs, String name, String value) { + if (value == null) { + return; + } + KeyValuePair kvp = LfFactory.eINSTANCE.createKeyValuePair(); + kvp.setName(name); + kvp.setValue(ASTUtils.toElement(value)); + pairs.getPairs().add(kvp); + } + /** The property type for the */ public static final class CargoDependenciesPropertyType implements TargetPropertyType { diff --git a/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt b/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt index 1b3dea0f14..6e12986e03 100644 --- a/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt +++ b/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt @@ -26,6 +26,8 @@ package org.lflang.generator.rust import org.eclipse.emf.ecore.resource.Resource import org.lflang.FileConfig +import org.lflang.TargetConfig +import org.lflang.camelToSnakeCase import org.lflang.generator.CodeMap import org.lflang.util.FileUtil import java.io.Closeable @@ -46,6 +48,20 @@ class RustFileConfig(resource: Resource, srcGenBasePath: Path, useHierarchicalBi FileUtil.deleteDirectory(outPath.resolve("target")) } + override fun getExecutable(): Path { + val localizedExecName = "${name.camelToSnakeCase()}$executableExtension" + return binPath.resolve(localizedExecName) + } + + override fun getExecutableExtension(): String { + val isWindows = System.getProperty("os.name").lowercase().contains("win") + return if (isWindows) { + ".exe" + } else { + "" + } + } + inline fun emit(codeMaps: MutableMap, p: Path, f: Emitter.() -> Unit) { measureTimeMillis { Emitter(codeMaps, p).use { it.f() } @@ -108,4 +124,3 @@ class Emitter( Files.writeString(output, codeMap.generatedCode) } } - diff --git a/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt b/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt index 3cc3a2448f..bea0214574 100644 --- a/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt +++ b/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt @@ -36,10 +36,8 @@ import org.lflang.generator.GeneratorResult import org.lflang.generator.IntegratedBuilder import org.lflang.generator.LFGeneratorContext import org.lflang.generator.TargetTypes -import org.lflang.generator.cpp.CppTypes + import org.lflang.joinWithCommas -import org.lflang.lf.Action -import org.lflang.lf.VarRef import org.lflang.scoping.LFGlobalScopeProvider import org.lflang.util.FileUtil import java.nio.file.Files @@ -60,10 +58,11 @@ import java.nio.file.Path */ @Suppress("unused") class RustGenerator( - fileConfig: RustFileConfig, - errorReporter: ErrorReporter, + context: LFGeneratorContext, @Suppress("UNUSED_PARAMETER") unused: LFGlobalScopeProvider -) : GeneratorBase(fileConfig, errorReporter) { +) : GeneratorBase(context) { + + val fileConfig: RustFileConfig = context.fileConfig as RustFileConfig companion object { /** Path to the rust runtime library (relative to class path) */ @@ -76,28 +75,25 @@ class RustGenerator( if (!canGenerate(errorsOccurred(), mainDef, errorReporter, context)) return - val fileConfig = fileConfig as RustFileConfig - Files.createDirectories(fileConfig.srcGenPath) val gen = RustModelBuilder.makeGenerationInfo(targetConfig, reactors, errorReporter) val codeMaps: Map = RustEmitter.generateRustProject(fileConfig, gen) if (targetConfig.noCompile || errorsOccurred()) { - context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(codeMaps)) + context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(context, codeMaps)) println("Exiting before invoking target compiler.") } else { context.reportProgress( "Code generation complete. Compiling...", IntegratedBuilder.GENERATED_PERCENT_PROGRESS ) - val exec = fileConfig.binPath.toAbsolutePath().resolve(gen.executableName) - Files.deleteIfExists(exec) // cleanup, cargo doesn't do it + Files.deleteIfExists(fileConfig.executable) // cleanup, cargo doesn't do it if (context.mode == LFGeneratorContext.Mode.LSP_MEDIUM) RustValidator(fileConfig, errorReporter, codeMaps).doValidate(context) - else invokeRustCompiler(context, gen.executableName, codeMaps) + else invokeRustCompiler(context, codeMaps) } } - private fun invokeRustCompiler(context: LFGeneratorContext, executableName: String, codeMaps: Map) { + private fun invokeRustCompiler(context: LFGeneratorContext, codeMaps: Map) { val args = mutableListOf().apply { this += "build" @@ -131,18 +127,11 @@ class RustGenerator( if (cargoReturnCode == 0) { // We still have to copy the compiled binary to the destination folder. - val isWindows = System.getProperty("os.name").lowercase().contains("win") - val localizedExecName = if (isWindows) { - "$executableName.exe" - } else { - executableName - } - val buildType = targetConfig.rust.buildType - var binaryPath = validator.getMetadata()?.targetDirectory!! + val binaryPath = validator.getMetadata()?.targetDirectory!! .resolve(buildType.cargoProfileName) - .resolve(localizedExecName) - val destPath = fileConfig.binPath.resolve(localizedExecName) + .resolve(fileConfig.executable.fileName) + val destPath = fileConfig.executable FileUtil.copyFile(binaryPath, destPath) // Files do not retain permissions when copied. @@ -151,7 +140,7 @@ class RustGenerator( println("SUCCESS (compiling generated Rust code)") println("Generated source code is in ${fileConfig.srcGenPath}") println("Compiled binary is in ${fileConfig.binPath}") - context.finish(GeneratorResult.Status.COMPILED, executableName, fileConfig, codeMaps) + context.finish(GeneratorResult.Status.COMPILED, codeMaps) } else if (context.cancelIndicator.isCanceled) { context.finish(GeneratorResult.CANCELLED) } else { diff --git a/org.lflang/src/org/lflang/generator/rust/RustModel.kt b/org.lflang/src/org/lflang/generator/rust/RustModel.kt index 943bcf3f33..499bf250ab 100644 --- a/org.lflang/src/org/lflang/generator/rust/RustModel.kt +++ b/org.lflang/src/org/lflang/generator/rust/RustModel.kt @@ -149,7 +149,7 @@ class ReactorNames( * Name of the "user struct", which contains state * variables as fields, and which the user manipulates in reactions. */ - val structName: Ident = lfName.capitalize().escapeRustIdent() + val structName: Ident = lfName.replaceFirstChar { it.uppercase() }.escapeRustIdent() // Names of other implementation-detailistic structs. diff --git a/org.lflang/src/org/lflang/generator/ts/TSActionGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSActionGenerator.kt index e162c16c26..e5dda9ee3d 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSActionGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSActionGenerator.kt @@ -1,6 +1,5 @@ package org.lflang.generator.ts -import org.lflang.federated.FederateInstance import org.lflang.generator.getTargetTimeExpr import org.lflang.lf.Action import org.lflang.lf.ParameterReference @@ -11,7 +10,7 @@ import java.util.* */ class TSActionGenerator( private val actions: List, - private val federate: FederateInstance + private val networkMessageActions: List ) { fun generateClassProperties(): String { @@ -28,7 +27,7 @@ class TSActionGenerator( return stateClassProperties.joinToString("\n") } - fun generateInstantiations(networkMessageActions: List): String { + fun generateInstantiations(): String { val actionInstantiations = LinkedList() for (action in actions) { // Shutdown actions are handled internally by the @@ -46,7 +45,7 @@ class TSActionGenerator( ", " + action.minDelay.toTsTime() } } - if (action in networkMessageActions){ + if (action.name in networkMessageActions) { actionInstantiations.add( "this.${action.name} = new __FederatePortAction<${action.tsActionType}>($actionArgs);") } else { diff --git a/org.lflang/src/org/lflang/generator/ts/TSConstructorGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSConstructorGenerator.kt index 35ab8df514..c65cd157c5 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSConstructorGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSConstructorGenerator.kt @@ -2,11 +2,9 @@ package org.lflang.generator.ts import org.lflang.ErrorReporter import org.lflang.TargetConfig -import org.lflang.federated.FederateInstance import org.lflang.generator.PrependOperator import org.lflang.generator.getTargetInitializer import org.lflang.joinWithLn -import org.lflang.lf.Action import org.lflang.lf.Parameter import org.lflang.lf.Reactor import java.util.* @@ -19,19 +17,16 @@ import java.util.* * and code to register reactions. This generator also generates federate port action * registrations. */ -class TSConstructorGenerator ( - private val tsGenerator: TSGenerator, +class TSConstructorGenerator( private val errorReporter: ErrorReporter, - private val reactor : Reactor, - private val federate: FederateInstance, - private val targetConfig: TargetConfig + private val reactor: Reactor ) { private fun initializeParameter(p: Parameter): String = "${p.name}: ${TSTypes.getTargetType(p)} = ${TSTypes.getTargetInitializer(p)}" private fun generateConstructorArguments(reactor: Reactor): String { - val arguments = LinkedList() + val arguments = StringJoiner(", \n") if (reactor.isMain || reactor.isFederated) { arguments.add("timeout: TimeValue | undefined = undefined") arguments.add("keepAlive: boolean = false") @@ -51,91 +46,73 @@ class TSConstructorGenerator ( arguments.add("fail?: () => void") } - return arguments.joinToString(", \n") + return arguments.toString() } - private fun federationRTIProperties(): LinkedHashMap { - return tsGenerator.federationRTIPropertiesW() - } - - private fun generateSuperConstructorCall(reactor: Reactor, federate: FederateInstance): String { + private fun generateSuperConstructorCall(reactor: Reactor, isFederate: Boolean): String = if (reactor.isMain) { - return "super(timeout, keepAlive, fast, success, fail);" - } else if (reactor.isFederated) { - var port = federationRTIProperties()["port"] - // Default of 0 is an indicator to use the default port, 15045. - if (port == 0) { - port = 15045 + if (isFederate) { + """ + | var federateConfig = defaultFederateConfig; + | if (__timeout !== undefined) { + | federateConfig.executionTimeout = __timeout; + | } + | federateConfig.federationID = __federationID; + | federateConfig.fast = __fast; + | federateConfig.keepAlive = __keepAlive; + | super(federateConfig, success, fail); + """.trimMargin() + } else { + "super(timeout, keepAlive, fast, success, fail);" } - return """ - super(federationID, ${federate.id}, $port, - "${federationRTIProperties()["host"]}", - timeout, keepAlive, fast, success, fail); - """ } else { - return "super(parent);" + "super(parent);" } - } // If the app is federated, register its // networkMessageActions with the RTIClient - private fun generateFederatePortActionRegistrations(networkMessageActions: List): String = - networkMessageActions.withIndex().joinWithLn { (fedPortID, nAction) -> - "this.registerFederatePortAction($fedPortID, this.${nAction.name});" + private fun generateFederatePortActionRegistrations(networkMessageActions: List): String = + networkMessageActions.withIndex().joinWithLn { (fedPortID, actionName) -> + "this.registerFederatePortAction($fedPortID, this.$actionName);" } // Generate code for setting target configurations. - private fun generateTargetConfigurations(): String = - if ((reactor.isMain || reactor.isFederated) - && targetConfig.coordinationOptions.advance_message_interval != null - ) "this.setAdvanceMessageInterval(${targetConfig.coordinationOptions.advance_message_interval.toTsTime()})" - else "" - - // Generate code for registering Fed IDs that are connected to - // this federate via ports in the TypeScript's FederatedApp. - // These Fed IDs are used to let the RTI know about the connections - // between federates during the initialization with the RTI. - fun generateFederateConfigurations(): String { - val federateConfigurations = LinkedList() - if (reactor.isFederated) { - for ((key, _) in federate.dependsOn) { - // FIXME: Get delay properly considering the unit instead of hardcoded TimeValue.NEVER(). - federateConfigurations.add("this.addUpstreamFederate(${key.id}, TimeValue.NEVER());") - } - for ((key, _) in federate.sendsTo) { - federateConfigurations.add("this.addDownstreamFederate(${key.id});") - } - } - return federateConfigurations.joinToString("\n") + private fun generateTargetConfigurations(targetConfig: TargetConfig): String { + val interval = targetConfig.coordinationOptions.advance_message_interval + return if ((reactor.isMain) && interval != null) { + "this.setAdvanceMessageInterval(${interval.toTsTime()})" + } else "" } fun generateConstructor( + targetConfig: TargetConfig, instances: TSInstanceGenerator, timers: TSTimerGenerator, parameters: TSParameterGenerator, states: TSStateGenerator, actions: TSActionGenerator, - ports: TSPortGenerator + ports: TSPortGenerator, + isFederate: Boolean, + networkMessageActions: List ): String { val connections = TSConnectionGenerator(reactor.connections, errorReporter) - val reactions = TSReactionGenerator(errorReporter, reactor, federate) + val reactions = TSReactionGenerator(errorReporter, reactor) return with(PrependOperator) { """ |constructor ( ${" | "..generateConstructorArguments(reactor)} |) { - ${" | "..generateSuperConstructorCall(reactor, federate)} - ${" | "..generateTargetConfigurations()} - ${" | "..generateFederateConfigurations()} + ${" | "..generateSuperConstructorCall(reactor, isFederate)} + ${" | "..generateTargetConfigurations(targetConfig)} ${" | "..instances.generateInstantiations()} ${" | "..timers.generateInstantiations()} ${" | "..parameters.generateInstantiations()} ${" | "..states.generateInstantiations()} - ${" | "..actions.generateInstantiations(federate.networkMessageActions)} + ${" | "..actions.generateInstantiations()} ${" | "..ports.generateInstantiations()} ${" | "..connections.generateInstantiations()} - ${" | "..if (reactor.isFederated) generateFederatePortActionRegistrations(federate.networkMessageActions) else ""} + ${" | "..if (reactor.isMain && isFederate) generateFederatePortActionRegistrations(networkMessageActions) else ""} ${" | "..reactions.generateAllReactions()} |} """.trimMargin() diff --git a/org.lflang/src/org/lflang/generator/ts/TSDelayBodyGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSDelayBodyGenerator.kt index 6d612dba7b..b0a011d042 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSDelayBodyGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSDelayBodyGenerator.kt @@ -1,6 +1,7 @@ package org.lflang.generator.ts import org.lflang.ASTUtils +import org.lflang.Target import org.lflang.generator.DelayBodyGenerator import org.lflang.lf.Action import org.lflang.lf.VarRef diff --git a/org.lflang/src/org/lflang/generator/ts/TSDockerGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSDockerGenerator.kt index b6b14cb7e1..1ca762340e 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSDockerGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSDockerGenerator.kt @@ -1,63 +1,25 @@ package org.lflang.generator.ts -import org.lflang.generator.DockerGeneratorBase; -import org.lflang.FileConfig; +import org.lflang.generator.DockerGenerator +import org.lflang.generator.LFGeneratorContext /** * Generates the docker file related code for the Typescript target. * * @author Hou Seng Wong */ -class TSDockerGenerator(isFederated: Boolean) : DockerGeneratorBase(isFederated) { - /** - * The interface for data from the Typescript code generator. - */ - public class TSGeneratorData( - private val tsFileName: String, - private val tsFileConfig: TSFileConfig - ) : GeneratorData { - fun getTsFileName(): String = tsFileName - fun getTsFileConfig(): TSFileConfig = tsFileConfig - } - - /** - * Translate data from the code generator to a map. - * - * @return data from the code generator put in a Map. - */ - public fun fromData( - tsFileName: String, - tsFileConfig: TSFileConfig - ): GeneratorData { - return TSGeneratorData(tsFileName, tsFileConfig) - } - - /** - * Translate data from the code generator to docker data as - * specified in the DockerData class. - * - * @param generatorData Data from the code generator. - * @return docker data as specified in the DockerData class - */ - override protected fun generateDockerData(generatorData: GeneratorData) : DockerData { - var tsGeneratorData = generatorData as TSGeneratorData - var tsFileName = tsGeneratorData.getTsFileName() - var dockerFilePath = tsGeneratorData.getTsFileConfig().tsDockerFilePath(tsFileName) - var dockerFileContent = generateDockerFileContent(tsGeneratorData) - var dockerBuildContext = "." - return DockerData(dockerFilePath, dockerFileContent, dockerBuildContext); - } +class TSDockerGenerator(context: LFGeneratorContext) : DockerGenerator(context) { /** * Returns the content of the docker file for [tsFileName]. */ - private fun generateDockerFileContent(generatorData: TSGeneratorData): String { - var tsFileName = generatorData.getTsFileName() + override fun generateDockerFileContent(): String { + val name = context.fileConfig.name val dockerFileContent = """ |FROM node:alpine - |WORKDIR /linguafranca/$tsFileName + |WORKDIR /linguafranca/$name |COPY . . - |ENTRYPOINT ["node", "dist/$tsFileName.js"] + |ENTRYPOINT ["node", "dist/$name.js"] """ return dockerFileContent.trimMargin() } diff --git a/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt b/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt index 9eb5264f21..2503d94629 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt @@ -28,6 +28,7 @@ package org.lflang.generator.ts import org.eclipse.emf.ecore.resource.Resource import org.lflang.FileConfig import org.lflang.util.FileUtil +import org.lflang.util.LFCommand import java.io.IOException import java.nio.file.Path @@ -53,27 +54,20 @@ class TSFileConfig( FileUtil.deleteDirectory(srcGenPath) } - /** - * Path to TypeScript source code. - */ - fun tsSrcGenPath(): Path = srcGenPath.resolve("src") - - /** - * Path to TypeScript core source code. - */ - fun reactorTsPath(): Path = srcGenPath.resolve("reactor-ts") + override fun getCommand(): LFCommand { + return LFCommand.get( + "node", + listOf(srcPkgPath.relativize(executable).toString()), + true, + srcPkgPath + ) + } - /** - * Path to the generated docker file - */ - fun tsDockerFilePath(tsFileName: String): Path { - return srcGenPath.resolve("$tsFileName.Dockerfile") + override fun getExecutableExtension(): String { + return ".js" } - /** - * Path to the generated docker compose file - */ - fun tsDockerComposeFilePath(): Path { - return srcGenPath.resolve("docker-compose.yml") + override fun getExecutable(): Path { + return srcGenPath.resolve("dist").resolve(name + executableExtension) } } diff --git a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt index d0bd39bfc6..5c6997d44e 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt @@ -27,30 +27,13 @@ package org.lflang.generator.ts import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.util.CancelIndicator -import org.lflang.ASTUtils -import org.lflang.ErrorReporter -import org.lflang.InferredType import org.lflang.Target import org.lflang.TimeValue import org.lflang.ast.AfterDelayTransformation -import org.lflang.federated.FederateInstance -import org.lflang.federated.launcher.FedTSLauncher -import org.lflang.federated.serialization.SupportedSerializers -import org.lflang.generator.CodeMap -import org.lflang.generator.GeneratorBase -import org.lflang.generator.GeneratorResult -import org.lflang.generator.GeneratorUtils +import org.lflang.generator.* import org.lflang.generator.GeneratorUtils.canGenerate -import org.lflang.generator.IntegratedBuilder -import org.lflang.generator.LFGeneratorContext -import org.lflang.generator.PrependOperator -import org.lflang.generator.ReactorInstance -import org.lflang.generator.SubContext -import org.lflang.generator.TargetTypes -import org.lflang.generator.cpp.CppTypes -import org.lflang.lf.Action -import org.lflang.lf.Expression -import org.lflang.lf.VarRef +import org.lflang.lf.Preamble +import org.lflang.model import org.lflang.scoping.LFGlobalScopeProvider import org.lflang.util.FileUtil import java.nio.file.Files @@ -72,10 +55,12 @@ private const val NO_NPM_MESSAGE = "The TypeScript target requires npm >= 6.14.4 * @author Hokeun Kim */ class TSGenerator( - private val tsFileConfig: TSFileConfig, - errorReporter: ErrorReporter, + private val context: LFGeneratorContext, private val scopeProvider: LFGlobalScopeProvider -) : GeneratorBase(tsFileConfig, errorReporter) { +) : GeneratorBase(context) { + + + val fileConfig: TSFileConfig = context.fileConfig as TSFileConfig companion object { /** Path to the TS lib directory (relative to class path) */ @@ -110,9 +95,6 @@ class TSGenerator( targetConfig.compilerFlags.add("-O2") } - // Wrappers to expose GeneratorBase methods. - fun federationRTIPropertiesW() = federationRTIProperties - /** Generate TypeScript code from the Lingua Franca model contained by the * specified resource. This is the main entry point for code * generation. @@ -125,34 +107,37 @@ class TSGenerator( super.doGenerate(resource, context) + instantiationGraph + if (!canGenerate(errorsOccurred(), mainDef, errorReporter, context)) return if (!isOsCompatible()) return - createMainReactorInstance() + // createMainReactorInstance() clean(context) copyConfigFiles() updatePackageConfig(context) val codeMaps = HashMap() - val dockerGenerator = TSDockerGenerator(isFederated) - for (federate in federates) generateCode(federate, codeMaps, dockerGenerator) + generateCode(codeMaps, resource.model.preambles) if (targetConfig.dockerOptions != null) { - dockerGenerator.setHost(federationRTIProperties.get("host")) - dockerGenerator.writeDockerFiles(tsFileConfig.tsDockerComposeFilePath()) + val dockerData = TSDockerGenerator(context).generateDockerData(); + dockerData.writeDockerFile() + DockerComposeGenerator(context).writeDockerComposeFile(listOf(dockerData)) } // For small programs, everything up until this point is virtually instantaneous. This is the point where cancellation, // progress reporting, and IDE responsiveness become real considerations. - if (targetConfig.noCompile) { - context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(null)) + + if (context.mode != LFGeneratorContext.Mode.LSP_MEDIUM && targetConfig.noCompile) { + context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(context, null)) } else { context.reportProgress( "Code generation complete. Collecting dependencies...", IntegratedBuilder.GENERATED_PERCENT_PROGRESS ) - if (shouldCollectDependencies(context)) collectDependencies(resource, context, tsFileConfig.srcGenPkgPath, false) + if (shouldCollectDependencies(context)) collectDependencies(resource, context, fileConfig.srcGenPkgPath, false) if (errorsOccurred()) { - context.unsuccessfulFinish(); - return; + context.unsuccessfulFinish() + return } if (targetConfig.protoFiles.size != 0) { protoc() @@ -162,10 +147,10 @@ class TSGenerator( val parsingContext = SubContext(context, COLLECTED_DEPENDENCIES_PERCENT_PROGRESS, 100) if ( !context.cancelIndicator.isCanceled - && passesChecks(TSValidator(tsFileConfig, errorReporter, codeMaps), parsingContext) + && passesChecks(TSValidator(fileConfig, errorReporter, codeMaps), parsingContext) ) { if (context.mode == LFGeneratorContext.Mode.LSP_MEDIUM) { - context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(codeMaps)) + context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(context, codeMaps)) } else { compile(resource, parsingContext) concludeCompilation(context, codeMaps) @@ -176,6 +161,17 @@ class TSGenerator( } } + /** + * Prefix the given path with a scheme if missing. + */ + private fun formatRuntimePath(path: String): String { + return if (path.startsWith("file:") || path.startsWith("git:") || path.startsWith("git+")) { + path + } else { + "file:/$path" + } + } + /** * Update package.json according to given build parameters. */ @@ -185,7 +181,7 @@ class TSGenerator( val sb = StringBuffer(""); val manifest = fileConfig.srcGenPath.resolve("package.json"); val rtRegex = Regex("(\"@lf-lang/reactor-ts\")(.+)") - if (rtPath != null && !rtPath.startsWith("file:")) rtPath = "file:$rtPath" + if (rtPath != null) rtPath = formatRuntimePath(rtPath) // FIXME: do better CLI arg validation upstream // https://github.com/lf-lang/lingua-franca/issues/1429 manifest.toFile().forEachLine { @@ -232,70 +228,21 @@ class TSGenerator( } - /** - * If a main or federated reactor has been declared, create a ReactorInstance of it. - * This will assign levels to reactions; then, if the program is federated, - * an AST transformation is performed to disconnect connections between federates. - */ - private fun createMainReactorInstance() { - if (mainDef != null) { - if (main == null) { - // Recursively build instances. This is done once because - // it is the same for all federates. - main = ReactorInstance( - ASTUtils.toDefinition(mainDef.reactorClass), errorReporter, - unorderedReactions - ) - val reactionInstanceGraph = main.assignLevels() - if (reactionInstanceGraph.nodeCount() > 0) { - errorReporter.reportError("Main reactor has causality cycles. Skipping code generation.") - return - } - // Inform the run-time of the breadth/parallelism of the reaction graph - val breadth = reactionInstanceGraph.breadth - if (breadth == 0) { - errorReporter.reportWarning("The program has no reactions") - } else { - targetConfig.compileDefinitions["LF_REACTION_GRAPH_BREADTH"] = reactionInstanceGraph.breadth.toString() - } - } - - // Force reconstruction of dependence information. - if (isFederated) { - // FIXME: The following operation must be done after levels are assigned. - // Removing these ports before that will cause incorrect levels to be assigned. - // See https://github.com/lf-lang/lingua-franca/discussions/608 - // For now, avoid compile errors by removing disconnected network ports before - // assigning levels. - removeRemoteFederateConnectionPorts(main) - // There will be AST transformations that invalidate some info - // cached in ReactorInstance. - main.clearCaches(false) - } - } - } - /** * Generate the code corresponding to [federate], recording any resulting mappings in [codeMaps]. */ private fun generateCode( - federate: FederateInstance, codeMaps: MutableMap, - dockerGenerator: TSDockerGenerator + preambles: List ) { - var tsFileName = fileConfig.name - // TODO(hokeun): Consider using FedFileConfig when enabling federated execution for TypeScript. - // For details, see https://github.com/icyphy/lingua-franca/pull/431#discussion_r676302102 - if (isFederated) { - tsFileName += '_' + federate.name - } + val tsFileName = fileConfig.name - val tsFilePath = tsFileConfig.tsSrcGenPath().resolve("$tsFileName.ts") + val tsFilePath = fileConfig.srcGenPath.resolve("src").resolve("$tsFileName.ts") val tsCode = StringBuilder() val preambleGenerator = TSImportPreambleGenerator(fileConfig.srcFile, - targetConfig.protoFiles) + targetConfig.protoFiles, preambles) tsCode.append(preambleGenerator.generatePreamble()) val parameterGenerator = TSParameterPreambleGenerator(fileConfig, targetConfig, reactors) @@ -304,18 +251,15 @@ class TSGenerator( val reactorGenerator = TSReactorGenerator(this, errorReporter, targetConfig) for (reactor in reactors) { - tsCode.append(reactorGenerator.generateReactor(reactor, federate)) + tsCode.append(reactorGenerator.generateReactor(reactor)) } - tsCode.append(reactorGenerator.generateReactorInstanceAndStart(federate, this.main, this.mainDef, mainParameters)) + tsCode.append(reactorGenerator.generateMainReactorInstanceAndStart(this.mainDef, mainParameters)) val codeMap = CodeMap.fromGeneratedCode(tsCode.toString()) codeMaps[tsFilePath] = codeMap FileUtil.writeToFile(codeMap.generatedCode, tsFilePath) - if (targetConfig.dockerOptions != null) { - dockerGenerator.addFile(dockerGenerator.fromData(tsFileName, tsFileConfig)) - } } private fun compile(resource: Resource, parsingContext: LFGeneratorContext) { @@ -327,10 +271,6 @@ class TSGenerator( transpile(parsingContext.cancelIndicator) if (parsingContext.cancelIndicator.isCanceled) return - if (isFederated) { - parsingContext.reportProgress("Generating federation infrastructure...", 90) - generateFederationInfrastructure() - } } /** @@ -363,6 +303,7 @@ class TSGenerator( GeneratorUtils.findTarget(resource), "ERROR: pnpm install command failed" + if (errors.isBlank()) "." else ":\n$errors") } + installProtoBufsIfNeeded(true, path, context.cancelIndicator) } else { errorReporter.reportWarning( "Falling back on npm. To prevent an accumulation of replicated dependencies, " + @@ -383,6 +324,17 @@ class TSGenerator( "\nFor installation instructions, see: https://www.npmjs.com/get-npm") return } + installProtoBufsIfNeeded(false, path, context.cancelIndicator) + } + } + + private fun installProtoBufsIfNeeded(pnpmIsAvailable: Boolean, cwd: Path, cancelIndicator: CancelIndicator) { + if (targetConfig.protoFiles.size != 0) { + commandFactory.createCommand( + if (pnpmIsAvailable) "pnpm" else "npm", + listOf("install", "google-protobuf"), + cwd, true + ).run(cancelIndicator) } } @@ -395,17 +347,17 @@ class TSGenerator( // FIXME: Check whether protoc is installed and provides hints how to install if it cannot be found. val protocArgs = LinkedList() - val tsOutPath = tsFileConfig.srcPath.relativize(tsFileConfig.tsSrcGenPath()) + val tsOutPath = fileConfig.srcPath.relativize(context.fileConfig.srcGenPath).resolve("src") protocArgs.addAll( listOf( - "--plugin=protoc-gen-ts=" + tsFileConfig.srcGenPkgPath.resolve("node_modules").resolve(".bin").resolve("protoc-gen-ts"), + "--plugin=protoc-gen-ts=" + fileConfig.srcGenPkgPath.resolve("node_modules").resolve(".bin").resolve("protoc-gen-ts"), "--js_out=import_style=commonjs,binary:$tsOutPath", "--ts_out=$tsOutPath" ) ) protocArgs.addAll(targetConfig.protoFiles) - val protoc = commandFactory.createCommand("protoc", protocArgs, tsFileConfig.srcPath) + val protoc = commandFactory.createCommand("protoc", protocArgs, fileConfig.srcPath) if (protoc == null) { errorReporter.reportError("Processing .proto files requires libprotoc >= 3.6.1.") @@ -460,32 +412,6 @@ class TSGenerator( } } - /** - * Set up the runtime infrastructure and federation - * launcher script. - */ - private fun generateFederationInfrastructure() { - // Create bin directory for the script. - if (!Files.exists(fileConfig.binPath)) { - Files.createDirectories(fileConfig.binPath) - } - // Generate script for launching federation - val launcher = FedTSLauncher(targetConfig, fileConfig, errorReporter) - launcher.createLauncher(federates, federationRTIPropertiesW()) - // TODO(hokeun): Modify this to make this work with standalone RTI. - // If this is a federated execution, generate C code for the RTI. -// // Copy the required library files into the target file system. -// // This will overwrite previous versions. -// var files = ArrayList("rti.c", "rti.h", "federate.c", "reactor_threaded.c", "reactor.c", "reactor_common.c", "reactor.h", "pqueue.c", "pqueue.h", "util.h", "util.c") -// -// for (file : files) { -// copyFileFromClassPath( -// File.separator + "lib" + File.separator + "core" + File.separator + file, -// fileConfig.getSrcGenPath.toString + File.separator + file -// ) -// } - } - /** * Inform the context of the results of a compilation. * @param context The context of the compilation. @@ -494,157 +420,16 @@ class TSGenerator( if (errorReporter.errorsOccurred) { context.unsuccessfulFinish() } else { - if (isFederated) { - context.finish(GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig, codeMaps) - } else { - context.finish( - GeneratorResult.Status.COMPILED, fileConfig.name + ".js", - fileConfig.srcGenPkgPath.resolve("dist"), fileConfig, codeMaps, "node" - ) - } + context.finish(GeneratorResult.Status.COMPILED, codeMaps) } } private fun isOsCompatible(): Boolean { - if (isFederated && GeneratorUtils.isHostWindows()) { - errorReporter.reportError( - "Federated LF programs with a TypeScript target are currently not supported on Windows. Exiting code generation." - ) - return false - } return true } override fun getTargetTypes(): TargetTypes = TSTypes - /** - * Generate code for the body of a reaction that handles the - * action that is triggered by receiving a message from a remote - * federate. - * @param action The action. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param receivingFed The destination federate. - * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. - * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. - * @param type The type. - * @param isPhysical Indicates whether or not the connection is physical - * @param serializer The serializer used on the connection. - */ - override fun generateNetworkReceiverBody( - action: Action, - sendingPort: VarRef, - receivingPort: VarRef, - receivingPortID: Int, - sendingFed: FederateInstance, - receivingFed: FederateInstance, - receivingBankIndex: Int, - receivingChannelIndex: Int, - type: InferredType, - isPhysical: Boolean, - serializer: SupportedSerializers - ): String { - return with(PrependOperator) {""" - |// generateNetworkReceiverBody - |if (${action.name} !== undefined) { - | ${receivingPort.container.name}.${receivingPort.variable.name} = ${action.name}; - |} - """.trimMargin()} - } - - /** - * Generate code for the body of a reaction that handles an output - * that is to be sent over the network. This base class throws an exception. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. - * @param receivingFed The destination federate. - * @param type The type. - * @param isPhysical Indicates whether the connection is physical or not - * @param delay The delay value imposed on the connection using after - * @throws UnsupportedOperationException If the target does not support this operation. - * @param serializer The serializer used on the connection. - */ - override fun generateNetworkSenderBody( - sendingPort: VarRef, - receivingPort: VarRef, - receivingPortID: Int, - sendingFed: FederateInstance, - sendingBankIndex: Int, - sendingChannelIndex: Int, - receivingFed: FederateInstance, - type: InferredType, - isPhysical: Boolean, - delay: Expression?, - serializer: SupportedSerializers - ): String { - return with(PrependOperator) {""" - |if (${sendingPort.container.name}.${sendingPort.variable.name} !== undefined) { - | this.util.sendRTITimedMessage(${sendingPort.container.name}.${sendingPort.variable.name}, ${receivingFed.id}, ${receivingPortID}); - |} - """.trimMargin()} - } - - - /** - * Generate code for the body of a reaction that sends a port status message for the given - * port if it is absent. - * - * @param port The port to generate the control reaction for - * @param portID The ID assigned to the port in the AST transformation - * @param receivingFederateID The ID of the receiving federate - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel if a multiport - * @param delay The delay value imposed on the connection using after - */ - override fun generateNetworkOutputControlReactionBody( - port: VarRef?, - portID: Int, - receivingFederateID: Int, - sendingBankIndex: Int, - sendingChannelIndex: Int, - delay: Expression? - ): String? { - return with(PrependOperator) {""" - |// TODO(hokeun): Figure out what to do for generateNetworkOutputControlReactionBody - """.trimMargin()} - } - - /** - * Generate code for the body of a reaction that waits long enough so that the status - * of the trigger for the given port becomes known for the current logical time. - * - * @param port The port to generate the control reaction for - * @param maxSTP The maximum value of STP is assigned to reactions (if any) - * that have port as their trigger or source - */ - override fun generateNetworkInputControlReactionBody(receivingPortID: Int, maxSTP: TimeValue?): String? { - return with(PrependOperator) {""" - |// TODO(hokeun): Figure out what to do for generateNetworkInputControlReactionBody - """.trimMargin()} - } - - /** - * Add necessary code to the source and necessary build supports to - * enable the requested serializations in 'enabledSerializations' - */ - override fun enableSupportForSerializationIfApplicable(cancelIndicator: CancelIndicator?) { - for (serializer in enabledSerializers) { - when (serializer) { - SupportedSerializers.NATIVE -> { - // No need to do anything at this point. - println("Native serializer is enabled.") - } - else -> throw UnsupportedOperationException("Unsupported serializer: $serializer"); - } - } - } - override fun getTarget(): Target { return Target.TS } diff --git a/org.lflang/src/org/lflang/generator/ts/TSImportPreambleGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSImportPreambleGenerator.kt index fe023fcae6..6621794852 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSImportPreambleGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSImportPreambleGenerator.kt @@ -28,6 +28,8 @@ package org.lflang.generator.ts import org.lflang.generator.PrependOperator import org.lflang.joinWithLn import java.nio.file.Path +import org.lflang.lf.Preamble +import java.util.stream.Collectors /** * Preamble generator for imports in TypeScript target. @@ -41,7 +43,8 @@ import java.nio.file.Path class TSImportPreambleGenerator( private val filePath: Path, - private val protoFiles: MutableList + private val protoFiles: MutableList, + private val preambles: List ) { companion object { /** @@ -54,7 +57,7 @@ class TSImportPreambleGenerator( import {Parameter as __Parameter, Timer as __Timer, Reactor as __Reactor, App as __App} from '@lf-lang/reactor-ts' import {Action as __Action, Startup as __Startup, FederatePortAction as __FederatePortAction} from '@lf-lang/reactor-ts' import {Bank as __Bank} from '@lf-lang/reactor-ts' - import {FederatedApp as __FederatedApp} from '@lf-lang/reactor-ts' + import {FederatedApp as __FederatedApp, FederateConfig as __FederateConfig} from '@lf-lang/reactor-ts' import {InPort as __InPort, OutPort as __OutPort, Port as __Port, WritablePort as __WritablePort, WritableMultiPort as __WritableMultiPort} from '@lf-lang/reactor-ts' import {InMultiPort as __InMultiPort, OutMultiPort as __OutMultiPort} from '@lf-lang/reactor-ts' import {Reaction as __Reaction} from '@lf-lang/reactor-ts' @@ -72,6 +75,7 @@ class TSImportPreambleGenerator( |// Code generated by the Lingua Franca compiler from: |// file:/$filePath ${" |"..DEFAULT_IMPORTS} + |${preambles.stream().map { it.code.body }.collect(Collectors.joining("\n"))} | """.trimMargin() } diff --git a/org.lflang/src/org/lflang/generator/ts/TSInstanceGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSInstanceGenerator.kt index 2c349ebd19..81de5257de 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSInstanceGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSInstanceGenerator.kt @@ -1,7 +1,6 @@ package org.lflang.generator.ts import org.lflang.ErrorReporter -import org.lflang.federated.FederateInstance import org.lflang.generator.getTargetInitializer import org.lflang.isBank import org.lflang.joinWithLn @@ -19,21 +18,14 @@ import java.util.* */ class TSInstanceGenerator( private val errorReporter: ErrorReporter, - reactor: Reactor, - federate: FederateInstance + reactor: Reactor ) { private val childReactors: List init { // Next handle child reactors instantiations. - // If the app isn't federated, instantiate all - // the child reactors. If the app is federated - if (!reactor.isFederated) { - childReactors = reactor.instantiations - } else { - childReactors = LinkedList() - childReactors.add(federate.instantiation) - } + // instantiate all the child reactors. + childReactors = reactor.instantiations } private fun getTypeParams(typeParms: List): String = diff --git a/org.lflang/src/org/lflang/generator/ts/TSReactionGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSReactionGenerator.kt index 98efd29c16..4354ac1ea9 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSReactionGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSReactionGenerator.kt @@ -2,7 +2,7 @@ package org.lflang.generator.ts import org.lflang.ErrorReporter import org.lflang.ASTUtils -import org.lflang.federated.FederateInstance +import org.lflang.federated.generator.FederateInstance import org.lflang.generator.PrependOperator import org.lflang.generator.getTargetTimeExpr import org.lflang.isBank @@ -27,8 +27,7 @@ import java.util.LinkedList */ class TSReactionGenerator( private val errorReporter: ErrorReporter, - private val reactor: Reactor, - private val federate: FederateInstance + private val reactor: Reactor ) { private fun VarRef.generateVarRef(): String { @@ -416,29 +415,10 @@ class TSReactionGenerator( // Next handle reaction instances. // If the app is federated, only generate // reactions that are contained by that federate - val generatedReactions: List - if (reactor.isFederated) { - generatedReactions = LinkedList() - for (reaction in reactor.reactions) { - // TODO(hokeun): Find a better way to gracefully handle this skipping. - // Do not add reactions created by generateNetworkOutputControlReactionBody - // or generateNetworkInputControlReactionBody. - if (reaction.code.toText().contains("generateNetworkOutputControlReactionBody") - || reaction.code.toText().contains("generateNetworkInputControlReactionBody") - ) { - continue - } - if (federate.contains(reaction)) { - generatedReactions.add(reaction) - } - } - } else { - generatedReactions = reactor.reactions - } ///////////////////// Reaction generation begins ///////////////////// // TODO(hokeun): Consider separating this out as a new class. - for (reaction in generatedReactions) { + for (reaction in reactor.reactions) { // Write the reaction itself reactionCodes.add(generateSingleReaction(reactor, reaction)) } diff --git a/org.lflang/src/org/lflang/generator/ts/TSReactorGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSReactorGenerator.kt index 683f6fd749..f89ec13f71 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSReactorGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSReactorGenerator.kt @@ -1,10 +1,9 @@ package org.lflang.generator.ts import org.lflang.* -import org.lflang.federated.FederateInstance import org.lflang.generator.PrependOperator -import org.lflang.generator.ReactorInstance import org.lflang.lf.* +import org.lflang.validation.AttributeSpec import java.util.* /** @@ -21,6 +20,16 @@ class TSReactorGenerator( private val errorReporter: ErrorReporter, private val targetConfig: TargetConfig ) { + + companion object { + const val MIN_OUTPUT_DELAY_STATEMENT = + """ + if (defaultFederateConfig.minOutputDelay !== undefined) { + __app.setMinDelayFromPhysicalActionToFederateOutput(defaultFederateConfig.minOutputDelay); + } + """ + } + // Initializer functions fun getTargetInitializerHelper(param: Parameter, list: List): String { @@ -38,7 +47,7 @@ class TSReactorGenerator( * main one. * @param instance A reactor instance. */ - private fun generateReactorInstance( + private fun generateMainReactorInstance( defn: Instantiation, mainParameters: Set ): String { @@ -51,116 +60,84 @@ class TSReactorGenerator( // assignment variable ("__CL" + the parameter's name). That variable will // be undefined if the command line argument wasn't specified. Otherwise // use undefined in the constructor. - val mainReactorParams = StringJoiner(", ") - for (parameter in defn.reactorClass.toDefinition().parameters) { - - if (mainParameters.contains(parameter)) { - mainReactorParams.add("__CL" + parameter.name) - } else { - mainReactorParams.add("undefined") - } + val mainReactorParams = defn.reactorClass.toDefinition().parameters.joinWithCommas { p -> + if (p in mainParameters) "__CL" + p.name + else "undefined" } - return with(PrependOperator) { - """ - |// ************* Instance $fullName of class ${defn.reactorClass.name} - |let __app; - |if (!__noStart) { - | __app = new $fullName(__timeout, __keepAlive, __fast, __federationID, $mainReactorParams); - |} - """ - }.trimMargin() + return """ + |// ************* Instance $fullName of class ${defn.reactorClass.name} + |let __app; + |if (!__noStart) { + | __app = new $fullName(__timeout, __keepAlive, __fast, __federationID, $mainReactorParams () => true, () => process.exit(1)); + |} + """.trimMargin() } /** Generate code to call the _start function on the main App * instance to start the runtime * @param instance A reactor instance. */ - private fun generateRuntimeStart(federate: FederateInstance, - main: ReactorInstance?, - defn: Instantiation): String { - var minOutputDelay = TimeValue.MAX_VALUE; - if (tsGenerator.isFederatedAndCentralized && main != null) { - // Check for outputs that depend on physical actions. - for (reactorInstance in main.children) { - if (federate.contains(reactorInstance)) { - val outputDelayMap = federate.findOutputsConnectedToPhysicalActions(reactorInstance) - for (outputDelay in outputDelayMap.values) { - if (outputDelay.isEarlierThan(minOutputDelay)) { - minOutputDelay = outputDelay - } - } - } - } - } - - if (minOutputDelay != TimeValue.MAX_VALUE && targetConfig.coordinationOptions.advance_message_interval == null) { - // There is a path from a physical action to output for reactor but advance message interval is not set. - // Report a warning. - errorReporter.reportWarning( - """ - Found a path from a physical action to output for reactor ${defn.name}. - The amount of delay is $minOutputDelay. - With centralized coordination, this can result in a large number of messages to the RTI. - Consider refactoring the code so that the output does not depend on the physical action, - or consider using decentralized coordination. To silence this warning, set the target - parameter coordination-options with a value like {advance-message-interval: 10 msec} - """.trimIndent() - ) - - } - + private fun generateRuntimeStart(defn: Instantiation): String { + val isFederate = AttributeUtils.isFederate(defn.reactor) return with(PrependOperator) { """ |// ************* Starting Runtime for ${defn.name} + of class ${defn.reactorClass.name}. |if (!__noStart && __app) { - | ${if (minOutputDelay == TimeValue.MAX_VALUE) "" else "__app.setMinDelayFromPhysicalActionToFederateOutput(${TSGenerator.timeInTargetLanguage(minOutputDelay)})"} +${" |"..MIN_OUTPUT_DELAY_STATEMENT.takeIf { isFederate }.orEmpty()} | __app._start(); |} | """ - }.trimMargin() + }.trimMargin() } - private fun generateReactorPreambles(preambles: List): String { - val preambleCodes = LinkedList() - - for (preamble in preambles) { - preambleCodes.add(with(PrependOperator) { + private fun generateReactorPreambles(preambles: List): String = + preambles.joinToString("\n") { preamble -> + with(PrependOperator) { """ - |// *********** From the preamble, verbatim: - |${preamble.code.toText()} - | - |// *********** End of preamble."""}.trimMargin()) + |// *********** From the preamble, verbatim: +${" |"..preamble.code.toText()} + |// *********** End of preamble.""" + }.trimMargin() } - return preambleCodes.joinToString("\n") + + private fun getNetworkMessagActions(reactor: Reactor): List { + val attribute = AttributeUtils.findAttributeByName(reactor, "_fed_config") + val actionsStr = AttributeUtils.getAttributeParameter(attribute, AttributeSpec.NETWORK_MESSAGE_ACTIONS) + return actionsStr?.split(",")?.filter { it.isNotEmpty()} ?: emptyList() } - fun generateReactor(reactor: Reactor, federate: FederateInstance): String { + fun generateReactor(reactor: Reactor): String { var reactorName = reactor.name if (!reactor.typeParms.isEmpty()) { reactorName += reactor.typeParms.joinToString(", ", "<", ">") { it.toText() } } + val isFederate = AttributeUtils.isFederate(reactor) + val networkMessageActions = getNetworkMessagActions(reactor) + // NOTE: type parameters that are referenced in ports or actions must extend // Present in order for the program to type check. - val classDefinition: String = if (reactor.isMain()) { - "class $reactorName extends __App {" - } else if (reactor.isFederated) { - "class $reactorName extends __FederatedApp {" + val classDefinition: String = if (reactor.isMain) { + if (isFederate) { + "class $reactorName extends __FederatedApp {" + } else { + "class $reactorName extends __App {" + } } else { "export class $reactorName extends __Reactor {" } - val instanceGenerator = TSInstanceGenerator(errorReporter, reactor, federate) + val instanceGenerator = TSInstanceGenerator(errorReporter, reactor) val timerGenerator = TSTimerGenerator(reactor.timers) val parameterGenerator = TSParameterGenerator(reactor.parameters) val stateGenerator = TSStateGenerator(reactor.stateVars) - val actionGenerator = TSActionGenerator(reactor.actions, federate) + val actionGenerator = TSActionGenerator(reactor.actions, networkMessageActions) val portGenerator = TSPortGenerator(reactor.inputs, reactor.outputs) - val constructorGenerator = TSConstructorGenerator(tsGenerator, errorReporter, reactor, federate, targetConfig) + val constructorGenerator = TSConstructorGenerator(errorReporter, reactor) return with(PrependOperator) { """ |// =============== START reactor class ${reactor.name} @@ -173,8 +150,8 @@ class TSReactorGenerator( ${" | "..stateGenerator.generateClassProperties()} ${" | "..actionGenerator.generateClassProperties()} ${" | "..portGenerator.generateClassProperties()} - ${" | "..constructorGenerator.generateConstructor(instanceGenerator, timerGenerator, parameterGenerator, - stateGenerator, actionGenerator, portGenerator)} + ${" | "..constructorGenerator.generateConstructor(targetConfig, instanceGenerator, timerGenerator, parameterGenerator, + stateGenerator, actionGenerator, portGenerator, isFederate, networkMessageActions)} |} |// =============== END reactor class ${reactor.name} | @@ -182,16 +159,14 @@ class TSReactorGenerator( } } - fun generateReactorInstanceAndStart( - federate: FederateInstance, - main: ReactorInstance?, + fun generateMainReactorInstanceAndStart( mainDef: Instantiation, mainParameters: Set ): String { return with(PrependOperator) { """ - |${generateReactorInstance(mainDef, mainParameters)} - |${generateRuntimeStart(federate, main, mainDef)} +${" |"..generateMainReactorInstance(mainDef, mainParameters)} +${" |"..generateRuntimeStart(mainDef)} | """ }.trimMargin() diff --git a/org.lflang/src/org/lflang/generator/ts/TSValidator.kt b/org.lflang/src/org/lflang/generator/ts/TSValidator.kt index dc63ce1776..b040cd297f 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSValidator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSValidator.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.eclipse.lsp4j.DiagnosticSeverity import org.eclipse.xtext.util.CancelIndicator import org.lflang.ErrorReporter +import org.lflang.FileConfig import org.lflang.generator.CodeMap import org.lflang.generator.DiagnosticReporting import org.lflang.generator.HumanReadableReportingStrategy @@ -29,13 +30,13 @@ private val TSC_LABEL: Pattern = Pattern.compile("((?<=\\s))(~+)") */ @Suppress("ArrayInDataClass") // Data classes here must not be used in data structures such as hashmaps. class TSValidator( - private val fileConfig: TSFileConfig, + private val fileConfig: FileConfig, errorReporter: ErrorReporter, codeMaps: Map ): Validator(errorReporter, codeMaps) { private class TSLinter( - private val fileConfig: TSFileConfig, + private val fileConfig: FileConfig, errorReporter: ErrorReporter, codeMaps: Map ): Validator(errorReporter, codeMaps) { diff --git a/org.lflang/src/org/lflang/graph/DirectedGraph.java b/org.lflang/src/org/lflang/graph/DirectedGraph.java index 14de3a0a51..771bbca539 100644 --- a/org.lflang/src/org/lflang/graph/DirectedGraph.java +++ b/org.lflang/src/org/lflang/graph/DirectedGraph.java @@ -285,4 +285,40 @@ public void clear() { public String toString() { return nodes().stream().map(Objects::toString).collect(Collectors.joining(", ", "{", "}")); } + + /** + * Return the DOT (GraphViz) representation of the graph. + */ + @Override + public String toDOT() { + StringBuilder dotRepresentation = new StringBuilder(); + StringBuilder edges = new StringBuilder(); + + // Start the digraph with a left-write rank + dotRepresentation.append("digraph {\n"); + dotRepresentation.append(" rankdir=LF;\n"); + + Set nodes = nodes(); + for (T node: nodes) { + // Draw the node + dotRepresentation.append(" node_" + (node.toString().hashCode() & 0xfffffff) + + " [label=\""+ node.toString() +"\"]\n"); + + // Draw the edges + Set downstreamNodes = getDownstreamAdjacentNodes(node); + for (T downstreamNode: downstreamNodes) { + edges.append(" node_" + (node.toString().hashCode() & 0xfffffff) + + " -> node_" + (downstreamNode.toString().hashCode() & 0xfffffff) + "\n"); + } + } + + // Add the edges to the definition of the graph at the bottom + dotRepresentation.append(edges); + + // Close the digraph + dotRepresentation.append("}\n"); + + // Return the DOT representation + return dotRepresentation.toString(); + } } diff --git a/org.lflang/src/org/lflang/graph/Graph.java b/org.lflang/src/org/lflang/graph/Graph.java index 98d72d0377..4e9a1723d9 100644 --- a/org.lflang/src/org/lflang/graph/Graph.java +++ b/org.lflang/src/org/lflang/graph/Graph.java @@ -76,4 +76,8 @@ public interface Graph { /** Return the number of directed edges in this graph. */ int edgeCount(); + + + /** Return the DOT (GraphViz) representation of the graph. */ + String toDOT(); } diff --git a/org.lflang/src/org/lflang/util/FileUtil.java b/org.lflang/src/org/lflang/util/FileUtil.java index 3efcbfb228..e94bb44242 100644 --- a/org.lflang/src/org/lflang/util/FileUtil.java +++ b/org.lflang/src/org/lflang/util/FileUtil.java @@ -486,9 +486,7 @@ public static void writeToFile(CharSequence text, Path path) throws IOException } public static void createDirectoryIfDoesNotExist(File dir) { - if (dir.exists()) return; - if (dir.mkdirs()) return; - throw new RuntimeIOException("Failed to create the directory " + dir); + if (!dir.exists()) dir.mkdirs(); } /** diff --git a/org.lflang/src/org/lflang/validation/AttributeSpec.java b/org.lflang/src/org/lflang/validation/AttributeSpec.java index b23f49d041..c5a530581b 100644 --- a/org.lflang/src/org/lflang/validation/AttributeSpec.java +++ b/org.lflang/src/org/lflang/validation/AttributeSpec.java @@ -40,7 +40,6 @@ /** * Specification of the structure of an attribute annotation. - * * @author Clément Fournier * @author Shaokai Lin */ @@ -49,7 +48,7 @@ public class AttributeSpec { private final Map paramSpecByName; public static final String VALUE_ATTR = "value"; - + public static final String NETWORK_MESSAGE_ACTIONS = "network_message_actions"; public static final String EACH_ATTR = "each"; /** A map from a string to a supported AttributeSpec */ @@ -110,7 +109,7 @@ public void check(LFValidator validator, Attribute attr) { * these names are known, and whether the named parameters * conform to the param spec (whether the param has the * right type, etc.). - * + * * @param validator The current validator in use. * @param attr The attribute being checked. * @return A set of named attribute parameters the user provides. @@ -127,7 +126,7 @@ private Set processNamedAttrParms(LFValidator validator, Attribute attr) validator.error("Missing name for attribute parameter.", Literals.ATTRIBUTE__ATTR_NAME); continue; } - + AttrParamSpec parmSpec = paramSpecByName.get(parm.getName()); if (parmSpec == null) { validator.error("\"" + parm.getName() + "\"" + " is an unknown attribute parameter.", @@ -144,7 +143,7 @@ private Set processNamedAttrParms(LFValidator validator, Attribute attr) /** * The specification of the attribute parameter. - * + * * @param name The name of the attribute parameter * @param type The type of the parameter * @param isOptional True if the parameter is optional. @@ -152,37 +151,41 @@ private Set processNamedAttrParms(LFValidator validator, Attribute attr) record AttrParamSpec(String name, AttrParamType type, boolean isOptional) { // Check if a parameter has the right type. - // Currently only String, Int, Boolean, and Float are supported. + // Currently, only String, Int, Boolean, Float, and target language are supported. public void check(LFValidator validator, AttrParm parm) { switch (type) { - case STRING: + case STRING -> { if (!StringUtil.hasQuotes(parm.getValue())) { validator.error("Incorrect type: \"" + parm.getName() + "\"" + " should have type String.", Literals.ATTRIBUTE__ATTR_NAME); } - break; - case INT: + } + case INT -> { if (!ASTUtils.isInteger(parm.getValue())) { validator.error( - "Incorrect type: \"" + parm.getName() + "\"" + " should have type Int.", + "Incorrect type: \"" + parm.getName() + "\"" + + " should have type Int.", Literals.ATTRIBUTE__ATTR_NAME); } - break; - case BOOLEAN: + } + case BOOLEAN -> { if (!ASTUtils.isBoolean(parm.getValue())) { validator.error( - "Incorrect type: \"" + parm.getName() + "\"" + " should have type Boolean.", + "Incorrect type: \"" + parm.getName() + "\"" + + " should have type Boolean.", Literals.ATTRIBUTE__ATTR_NAME); } - break; - case FLOAT: + } + case FLOAT -> { if (!ASTUtils.isFloat(parm.getValue())) { validator.error( - "Incorrect type: \"" + parm.getName() + "\"" + " should have type Float.", + "Incorrect type: \"" + parm.getName() + "\"" + + " should have type Float.", Literals.ATTRIBUTE__ATTR_NAME); } - break; + } + default -> throw new IllegalArgumentException("unexpected type"); } } } @@ -194,7 +197,7 @@ enum AttrParamType { STRING, INT, BOOLEAN, - FLOAT + FLOAT, } /* @@ -216,5 +219,12 @@ enum AttrParamType { ATTRIBUTE_SPECS_BY_NAME.put("enclave", new AttributeSpec( List.of(new AttrParamSpec(EACH_ATTR, AttrParamType.BOOLEAN, true)) )); + + // attributes that are used internally only by the federated code generation + ATTRIBUTE_SPECS_BY_NAME.put("_unordered", new AttributeSpec(null)); + ATTRIBUTE_SPECS_BY_NAME.put("_fed_config", new AttributeSpec( + List.of(new AttrParamSpec(AttributeSpec.NETWORK_MESSAGE_ACTIONS, + AttrParamType.STRING, false)))); + ATTRIBUTE_SPECS_BY_NAME.put("_c_body", new AttributeSpec(null)); } } diff --git a/org.lflang/src/org/lflang/validation/LFValidator.java b/org.lflang/src/org/lflang/validation/LFValidator.java index eca08a781b..8af7159115 100644 --- a/org.lflang/src/org/lflang/validation/LFValidator.java +++ b/org.lflang/src/org/lflang/validation/LFValidator.java @@ -63,6 +63,7 @@ import org.lflang.TargetProperty; import org.lflang.TimeValue; import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.federated.validation.FedValidator; import org.lflang.generator.NamedInstance; import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; @@ -941,6 +942,10 @@ public void checkReactor(Reactor reactor) throws IOException { ); } } + + if (reactor.isFederated()) { + FedValidator.validateFederatedReactor(reactor, this.errorReporter); + } } /** @@ -1498,7 +1503,7 @@ public List getTargetPropertyErrors() { * Generate an error message for an AST node. */ @Override - protected void error(java.lang.String message, + protected void error(java.lang.String message, org.eclipse.emf.ecore.EStructuralFeature feature) { super.error(message, feature); } diff --git a/test/C/src/federated/BroadcastFeedback.lf b/test/C/src/federated/BroadcastFeedback.lf index f6f90f47c7..0c7146b8f7 100644 --- a/test/C/src/federated/BroadcastFeedback.lf +++ b/test/C/src/federated/BroadcastFeedback.lf @@ -3,8 +3,7 @@ */ target C { timeout: 1 sec, - build-type: RelWithDebInfo, - logging: DEBUG + build-type: RelWithDebInfo } reactor SenderAndReceiver { diff --git a/test/C/src/federated/DistributedCount.lf b/test/C/src/federated/DistributedCount.lf index b5e0fd59fc..7ffd4eb8e2 100644 --- a/test/C/src/federated/DistributedCount.lf +++ b/test/C/src/federated/DistributedCount.lf @@ -7,7 +7,6 @@ */ target C { timeout: 5 sec, - logging: DEBUG, coordination: centralized } diff --git a/test/C/src/federated/DistributedLogicalActionUpstreamLong.lf b/test/C/src/federated/DistributedLogicalActionUpstreamLong.lf index 253112370d..078b15ed02 100644 --- a/test/C/src/federated/DistributedLogicalActionUpstreamLong.lf +++ b/test/C/src/federated/DistributedLogicalActionUpstreamLong.lf @@ -16,7 +16,7 @@ reactor WithLogicalAction { logical action act(0): int reaction(startup, act) -> act, out {= - SET(out, self->counter); + lf_set(out, self->counter); lf_schedule_int(act, USEC(50), self->counter++); =} } diff --git a/test/C/src/federated/DistributedPhysicalActionUpstream.lf b/test/C/src/federated/DistributedPhysicalActionUpstream.lf index 7e7318726e..2cc6735884 100644 --- a/test/C/src/federated/DistributedPhysicalActionUpstream.lf +++ b/test/C/src/federated/DistributedPhysicalActionUpstream.lf @@ -38,7 +38,7 @@ reactor WithPhysicalAction { lf_thread_create(&self->thread_id, &take_time, act); =} - reaction(act) -> out {= SET(out, act->value); =} + reaction(act) -> out {= lf_set(out, act->value); =} } federated reactor { diff --git a/test/C/src/federated/DistributedPhysicalActionUpstreamLong.lf b/test/C/src/federated/DistributedPhysicalActionUpstreamLong.lf index 2d508949fb..0a6ee31e62 100644 --- a/test/C/src/federated/DistributedPhysicalActionUpstreamLong.lf +++ b/test/C/src/federated/DistributedPhysicalActionUpstreamLong.lf @@ -38,7 +38,7 @@ reactor WithPhysicalAction { lf_thread_create(&self->thread_id, &take_time, act); =} - reaction(act) -> out {= SET(out, act->value); =} + reaction(act) -> out {= lf_set(out, act->value); =} } federated reactor { diff --git a/test/C/src/federated/FeedbackDelay.lf b/test/C/src/federated/FeedbackDelay.lf index e369e90df4..6495fde8db 100644 --- a/test/C/src/federated/FeedbackDelay.lf +++ b/test/C/src/federated/FeedbackDelay.lf @@ -11,7 +11,7 @@ reactor PhysicalPlant { state previous_sensor_time: time(0) reaction(t) -> sensor {= - SET(sensor, 42); + lf_set(sensor, 42); self->previous_sensor_time = self->last_sensor_time; self->last_sensor_time = lf_time_physical(); =} @@ -37,10 +37,10 @@ reactor Controller { reaction(sensor) -> control, request_for_planning {= if (!self->first) { - SET(control, self->latest_control); + lf_set(control, self->latest_control); } self->first = false; - SET(request_for_planning, sensor->value); + lf_set(request_for_planning, sensor->value); =} } @@ -50,7 +50,7 @@ reactor Planner { reaction(request) -> response {= lf_sleep(MSEC(10)); - SET(response, request->value); + lf_set(response, request->value); =} } diff --git a/test/C/src/federated/FeedbackDelaySimple.lf b/test/C/src/federated/FeedbackDelaySimple.lf index f41e630a8c..2f00000e03 100644 --- a/test/C/src/federated/FeedbackDelaySimple.lf +++ b/test/C/src/federated/FeedbackDelaySimple.lf @@ -20,7 +20,7 @@ reactor Loop { self->count++; =} - reaction(t) -> out {= SET(out, self->count); =} + reaction(t) -> out {= lf_set(out, self->count); =} reaction(shutdown) {= if (self->count != 11) { diff --git a/test/C/src/federated/LoopDistributedCentralized.lf b/test/C/src/federated/LoopDistributedCentralized.lf index da6a3c14b9..9643d3f1f3 100644 --- a/test/C/src/federated/LoopDistributedCentralized.lf +++ b/test/C/src/federated/LoopDistributedCentralized.lf @@ -10,37 +10,19 @@ target C { coordination-options: { advance-message-interval: 100 msec }, - timeout: 5 sec + timeout: 4 sec, + logging: DEBUG } -preamble {= - #include // Defines sleep() - bool stop = false; - // Thread to trigger an action once every second. - void* ping(void* actionref) { - while(!stop) { - lf_print("Scheduling action."); - lf_schedule(actionref, 0); - sleep(1); - } - return NULL; - } -=} - reactor Looper(incr: int(1), delay: time(0 msec)) { input in: int output out: int physical action a(delay) state count: int(0) - reaction(startup) -> a {= - // Start the thread that listens for Enter or Return. - lf_thread_t thread_id; - lf_print("Starting thread."); - lf_thread_create(&thread_id, &ping, a); - =} + timer t(0, 1 sec) - reaction(a) -> out {= + reaction(t) -> out {= lf_set(out, self->count); self->count += self->incr; =} @@ -54,8 +36,6 @@ reactor Looper(incr: int(1), delay: time(0 msec)) { reaction(shutdown) {= lf_print("******* Shutdown invoked."); - // Stop the thread that is scheduling actions. - stop = true; if (self->count != 5 * self->incr) { lf_print_error_and_exit("Failed to receive all five expected inputs."); } diff --git a/test/C/src/federated/LoopDistributedCentralized2.lf b/test/C/src/federated/LoopDistributedCentralized2.lf new file mode 100644 index 0000000000..c81a0f012e --- /dev/null +++ b/test/C/src/federated/LoopDistributedCentralized2.lf @@ -0,0 +1,77 @@ +/** + * This tests a feedback loop with physical actions and centralized + * coordination. + * + * @author Edward A. Lee + */ +target C { + flags: "-Wall", + coordination: centralized, + coordination-options: { + advance-message-interval: 100 msec + }, + timeout: 4 sec +} + +reactor Looper(incr: int(1), delay: time(0 msec)) { + input in: int + output out: int + physical action a(delay) + state count: int(0) + + timer t(0, 1 sec) + + reaction(t) -> out {= + lf_set(out, self->count); + self->count += self->incr; + =} + + reaction(in) {= + instant_t time_lag = lf_time_physical() - lf_time_logical(); + char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 + lf_comma_separated_time(time_buffer, time_lag); + lf_print("Received %d. Logical time is behind physical time by %s nsec.", in->value, time_buffer); + =} + + reaction(shutdown) {= + lf_print("******* Shutdown invoked."); + if (self->count != 5 * self->incr) { + lf_print_error_and_exit("Failed to receive all five expected inputs."); + } + =} +} + +reactor Looper2(incr: int(1), delay: time(0 msec)) { + input in: int + output out: int + physical action a(delay) + state count: int(0) + + timer t(0, 1 sec) + + reaction(in) {= + instant_t time_lag = lf_time_physical() - lf_time_logical(); + char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 + lf_comma_separated_time(time_buffer, time_lag); + lf_print("Received %d. Logical time is behind physical time by %s nsec.", in->value, time_buffer); + =} + + reaction(t) -> out {= + lf_set(out, self->count); + self->count += self->incr; + =} + + reaction(shutdown) {= + lf_print("******* Shutdown invoked."); + if (self->count != 5 * self->incr) { + lf_print_error_and_exit("Failed to receive all five expected inputs."); + } + =} +} + +federated reactor(delay: time(0)) { + left = new Looper() + right = new Looper2(incr = -1) + left.out -> right.in + right.out -> left.in +} diff --git a/test/C/src/federated/LoopDistributedCentralizedPhysicalAction.lf b/test/C/src/federated/LoopDistributedCentralizedPhysicalAction.lf new file mode 100644 index 0000000000..e622e31979 --- /dev/null +++ b/test/C/src/federated/LoopDistributedCentralizedPhysicalAction.lf @@ -0,0 +1,70 @@ +/** + * This tests a feedback loop with physical actions and centralized + * coordination. + * + * @author Edward A. Lee + */ +target C { + flags: "-Wall", + coordination: centralized, + coordination-options: { + advance-message-interval: 100 msec + }, + timeout: 5 sec +} + +preamble {= + #include // Defines sleep() + bool stop = false; + // Thread to trigger an action once every second. + void* ping(void* actionref) { + while(!stop) { + lf_print("Scheduling action."); + lf_schedule(actionref, 0); + sleep(1); + } + return NULL; + } +=} + +reactor Looper(incr: int(1), delay: time(0 msec)) { + input in: int + output out: int + physical action a(delay) + state count: int(0) + + reaction(startup) -> a {= + // Start the thread that listens for Enter or Return. + lf_thread_t thread_id; + lf_print("Starting thread."); + lf_thread_create(&thread_id, &ping, a); + =} + + reaction(a) -> out {= + lf_set(out, self->count); + self->count += self->incr; + =} + + reaction(in) {= + instant_t time_lag = lf_time_physical() - lf_time_logical(); + char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 + lf_comma_separated_time(time_buffer, time_lag); + lf_print("Received %d. Logical time is behind physical time by %s nsec.", in->value, time_buffer); + =} + + reaction(shutdown) {= + lf_print("******* Shutdown invoked."); + // Stop the thread that is scheduling actions. + stop = true; + if (self->count != 5 * self->incr) { + lf_print_error_and_exit("Failed to receive all five expected inputs."); + } + =} +} + +federated reactor(delay: time(0)) { + left = new Looper() + right = new Looper(incr = -1) + left.out -> right.in + right.out -> left.in +} diff --git a/test/C/src/federated/failing/DistributedDoublePortLooped.lf b/test/C/src/federated/failing/DistributedDoublePortLooped.lf index 10ee73fb43..4eca2cee76 100644 --- a/test/C/src/federated/failing/DistributedDoublePortLooped.lf +++ b/test/C/src/federated/failing/DistributedDoublePortLooped.lf @@ -1,9 +1,7 @@ /** - * Test the case for when two upstream federates - * send messages to a downstream federate on two - * different ports. One message should carry a - * microstep delay relative to the other - * message. + * Test the case for when two upstream federates send messages to a downstream + * federate on two different ports. One message should carry a microstep delay + * relative to the other message. * * @author Soroush Bateni */ @@ -11,58 +9,51 @@ target C { timeout: 900 msec, logging: LOG, coordination: centralized -}; +} reactor Foo { - timer t(0, 700 usec); - output out:int; - reaction(t) -> out {= - lf_set(out, 0); - =} + timer t(0, 700 usec) + output out: int + + reaction(t) -> out {= lf_set(out, 0); =} } reactor Count { - state count:int(1); - input in1:int; - input in2:int; - input in3:int - output out1:int; - output out2:int; - timer t(0, 1 msec); - reaction(in1) -> out1 {= + state count: int(1) + input in1: int + input in2: int + input in3: int + output out1: int + output out2: int + timer t(0, 1 msec) - =} - // in2 is not connected to anything. - // Consequently, the control reaction - // for in2 waits forever. - reaction(in2,t) -> out2 {= - lf_set(out2, self->count++); - =} - reaction(in3) {= + reaction(in1) -> out1 {= =} - =} + // in2 is not connected to anything. Consequently, the control reaction for + // in2 waits forever. + reaction(in2, t) -> out2 {= lf_set(out2, self->count++); =} + + reaction(in3) {= =} } reactor CountMicrostep { - state count:int(1); - output out:int; - logical action act:int; - timer t(0, 1 msec); - reaction(t) -> act {= - lf_schedule_int(act, 0, self->count++); - =} + state count: int(1) + output out: int + logical action act: int + timer t(0, 1 msec) - reaction(act) -> out {= - lf_set(out, act->value); - =} + reaction(t) -> act {= lf_schedule_int(act, 0, self->count++); =} + + reaction(act) -> out {= lf_set(out, act->value); =} } reactor Print { - input in1:int; - input in2:int; - input in3:int; - output out:int; - timer t(0,2 msec); + input in1: int + input in2: int + input in3: int + output out: int + timer t(0, 2 msec) + reaction(in1, in2, in3, t) -> out {= interval_t elapsed_time = lf_time_logical_elapsed(); lf_print("At tag (%lld, %u), received in = %d and in2 = %d.", elapsed_time, lf_tag().microstep, in1->value, in2->value); @@ -74,13 +65,13 @@ reactor Print { } federated reactor { - f = new Foo(); - c = new Count(); - f.out -> c.in1; - cm = new CountMicrostep(); - p = new Print(); + f = new Foo() + c = new Count() + f.out -> c.in1 + cm = new CountMicrostep() + p = new Print() c.out1 -> p.in2 - c.out2 -> p.in3; // Indicating a 'logical' connection. - cm.out -> p.in1; - p.out -> c.in3; + c.out2 -> p.in3 // Indicating a 'logical' connection. + cm.out -> p.in1 + p.out -> c.in3 } diff --git a/test/C/src/federated/failing/DistributedDoublePortLooped2.lf b/test/C/src/federated/failing/DistributedDoublePortLooped2.lf index 9e9ce50d11..04ea02c4e8 100644 --- a/test/C/src/federated/failing/DistributedDoublePortLooped2.lf +++ b/test/C/src/federated/failing/DistributedDoublePortLooped2.lf @@ -1,9 +1,7 @@ /** - * Test the case for when two upstream federates - * send messages to a downstream federte on two - * different ports. One message should carry a - * microstep delay relative to the other - * message. + * Test the case for when two upstream federates send messages to a downstream + * federte on two different ports. One message should carry a microstep delay + * relative to the other message. * * @author Soroush Bateni */ @@ -11,42 +9,39 @@ target C { timeout: 5 msec, logging: LOG, coordination: centralized -}; +} reactor Count { - state count:int(1); - input in:int; - output out:int; - timer t(0, 1 msec); + state count: int(1) + input in: int + output out: int + timer t(0, 1 msec) + reaction(t) -> out {= lf_print("Count sends %d.", self->count); lf_set(out, self->count++); =} - reaction(in) {= - lf_print("Count received %d.", in->value); - =} + reaction(in) {= lf_print("Count received %d.", in->value); =} } reactor CountMicrostep { - state count:int(1); - output out:int; - logical action act:int; - timer t(0, 1 msec); - reaction(t) -> act {= - lf_schedule_int(act, 0, self->count++); - =} + state count: int(1) + output out: int + logical action act: int + timer t(0, 1 msec) - reaction(act) -> out {= - lf_set(out, act->value); - =} + reaction(t) -> act {= lf_schedule_int(act, 0, self->count++); =} + + reaction(act) -> out {= lf_set(out, act->value); =} } reactor Print { - input in:int; - input in2:int; - output out:int; - timer t(0,2 msec); + input in: int + input in2: int + output out: int + timer t(0, 2 msec) + reaction(in, in2) -> out {= interval_t elapsed_time = lf_time_logical_elapsed(); if (in->is_present) { @@ -60,19 +55,21 @@ reactor Print { } lf_set(out, in->value); =} + reaction(t) {= // Do nothing =} + reaction(shutdown) {= lf_print("SUCCESS: messages were at least one microstep apart."); =} } federated reactor { - c = new Count(); - cm = new CountMicrostep(); - p = new Print(); - c.out -> p.in; // Indicating a 'logical' connection. - cm.out -> p.in2; - p.out -> c.in; + c = new Count() + cm = new CountMicrostep() + p = new Print() + c.out -> p.in // Indicating a 'logical' connection. + cm.out -> p.in2 + p.out -> c.in } diff --git a/test/C/src/federated/failing/DistributedNetworkOrderDecentralized.lf b/test/C/src/federated/failing/DistributedNetworkOrderDecentralized.lf index f8fe99fca6..19779de3d2 100644 --- a/test/C/src/federated/failing/DistributedNetworkOrderDecentralized.lf +++ b/test/C/src/federated/failing/DistributedNetworkOrderDecentralized.lf @@ -1,23 +1,21 @@ -/* - * This is a test for send_timed_message, - * which is an internal API. - * This version of the test uses the - * decentralized coordination. +/** + * This is a test for send_timed_message, which is an internal API. This version + * of the test uses the decentralized coordination. * - * FIXME: Because this test sends messages with intended tag - * out of order, it will fail. + * FIXME: Because this test sends messages with intended tag out of order, it + * will fail. * * @author Soroush Bateni */ - target C { timeout: 1 sec, coordination: decentralized -}; +} reactor Sender { - output out:int; - timer t(0, 1 msec); + output out: int + timer t(0, 1 msec) + reaction(t) {= int payload = 1; if (lf_time_logical_elapsed() == 0LL) { @@ -32,8 +30,8 @@ reactor Sender { } reactor Receiver { - input in:int; - state success:int(0); + input in: int + state success: int(0) reaction(in) {= tag_t current_tag = lf_tag(); @@ -62,8 +60,8 @@ reactor Receiver { } federated reactor { - sender = new Sender(); - receiver = new Receiver(); + sender = new Sender() + receiver = new Receiver() - sender.out -> receiver.in; + sender.out -> receiver.in } diff --git a/test/C/src/federated/failing/LoopDistributedDecentralizedPrecedence.lf b/test/C/src/federated/failing/LoopDistributedDecentralizedPrecedence.lf index 50c30db84c..ee9fc56d83 100644 --- a/test/C/src/federated/failing/LoopDistributedDecentralizedPrecedence.lf +++ b/test/C/src/federated/failing/LoopDistributedDecentralizedPrecedence.lf @@ -1,6 +1,6 @@ /** - * This tests that the precedence order of reaction invocation is kept - * when a feedback loop is present in decentralized coordination. + * This tests that the precedence order of reaction invocation is kept when a + * feedback loop is present in decentralized coordination. * * @author Edward A. Lee * @author Soroush Bateni @@ -11,40 +11,44 @@ target C { timeout: 4900 msec } -reactor Looper(incr:int(1), delay:time(0 msec), stp_offset:time(0)) { - input in:int; - output out:int; - state count:int(0); - state received_count:int(0); - timer t(0, 1 sec); +reactor Looper(incr: int(1), delay: time(0 msec), stp_offset: time(0)) { + input in: int + output out: int + state count: int(0) + state received_count: int(0) + timer t(0, 1 sec) + reaction(t) -> out {= lf_set(out, self->count); self->count += self->incr; =} + reaction(in) {= instant_t time_lag = lf_time_physical() - lf_time_logical(); char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 lf_readable_time(time_buffer, time_lag); lf_print("Received %d. Logical time is behind physical time by %s.", in->value, time_buffer); self->received_count = self->count; - =} STP (stp_offset) {= + =} STP(stp_offset) {= instant_t time_lag = lf_time_physical() - lf_time_logical(); char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 lf_readable_time(time_buffer, time_lag); lf_print("STP offset was violated. Received %d. Logical time is behind physical time by %s.", in->value, time_buffer); self->received_count = self->count; - =} deadline (10 msec) {= + =} deadline(10 msec) {= instant_t time_lag = lf_time_physical() - lf_time_logical(); char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 lf_readable_time(time_buffer, time_lag); lf_print("Deadline miss. Received %d. Logical time is behind physical time by %s.", in->value, time_buffer); self->received_count = self->count; =} + reaction(t) {= if (self->received_count != self->count) { lf_print_error_and_exit("reaction(t) was invoked before reaction(in). Precedence order was not kept."); } =} + reaction(shutdown) {= lf_print("******* Shutdown invoked."); if (self->count != 5 * self->incr) { @@ -52,9 +56,10 @@ reactor Looper(incr:int(1), delay:time(0 msec), stp_offset:time(0)) { } =} } -federated reactor (delay:time(0)) { - left = new Looper(stp_offset = 5 msec); - right = new Looper(incr = -1, stp_offset = 5 msec); - left.out -> right.in; - right.out -> left.in; + +federated reactor(delay: time(0)) { + left = new Looper(stp_offset = 5 msec) + right = new Looper(incr = -1, stp_offset = 5 msec) + left.out -> right.in + right.out -> left.in } diff --git a/test/C/src/serialization/ROSBuiltInSerialization.lf b/test/C/src/serialization/ROSBuiltInSerialization.lf index 1a5f9d4051..d573b23cce 100644 --- a/test/C/src/serialization/ROSBuiltInSerialization.lf +++ b/test/C/src/serialization/ROSBuiltInSerialization.lf @@ -64,5 +64,5 @@ federated reactor { sender = new Sender() receiver = new Receiver() - sender.out -> receiver.in serializer "ros2" + sender.out -> receiver.in after 100 msec serializer "ros2" } diff --git a/test/Python/src/federated/DecentralizedP2PComm.lf b/test/Python/src/federated/DecentralizedP2PComm.lf index f4318bcde3..29aee88ad5 100644 --- a/test/Python/src/federated/DecentralizedP2PComm.lf +++ b/test/Python/src/federated/DecentralizedP2PComm.lf @@ -26,7 +26,7 @@ reactor Platform(start(0), expected_start(0), stp_offset_param(0)) { self.expected += 1 =} STP(stp_offset_param) {= print("Received {} late.".format(in_.value)) - current_tag = lf_tag() + current_tag = lf.tag() self.expected += 1 self.sys.stderr.write("STP offset was violated by ({}, {}).".format(current_tag.time - in_.intended_tag.time, current_tag.microstep - in_.intended_tag.microstep)) =} diff --git a/test/Python/src/federated/DecentralizedP2PUnbalancedTimeout.lf b/test/Python/src/federated/DecentralizedP2PUnbalancedTimeout.lf index 82d8730f9a..960a06c650 100644 --- a/test/Python/src/federated/DecentralizedP2PUnbalancedTimeout.lf +++ b/test/Python/src/federated/DecentralizedP2PUnbalancedTimeout.lf @@ -36,7 +36,7 @@ reactor Destination { reaction(x) {= print("Received {}".format(x.value)) - current_tag = lf_tag() + current_tag = lf.tag() if x.value != self.s: error_msg = "At tag ({}, {}) expected {} and got {}.".format(current_tag.time - self.startup_logical_time, current_tag.microstep, self.s, x.value) self.sys.stderr.write(error_msg) diff --git a/test/Python/src/federated/DistributedCount.lf b/test/Python/src/federated/DistributedCount.lf index 5affbb2a1f..eb90b4d672 100644 --- a/test/Python/src/federated/DistributedCount.lf +++ b/test/Python/src/federated/DistributedCount.lf @@ -7,7 +7,6 @@ */ target Python { timeout: 5 sec, - logging: DEBUG, coordination: centralized } diff --git a/test/Python/src/federated/DistributedDoublePort.lf b/test/Python/src/federated/DistributedDoublePort.lf index ec89aebe2d..03ad2c4d08 100644 --- a/test/Python/src/federated/DistributedDoublePort.lf +++ b/test/Python/src/federated/DistributedDoublePort.lf @@ -33,8 +33,8 @@ reactor Print { input in2 reaction(in_, in2) {= - elapsed_time = lf.time.logical_elapsed() - print("At tag ({}, {}), received in_ = {} and in2 = {}.".format(elapsed_time, lf_tag().microstep, in_.value, in2.value)) + current_tag = lf.tag() + print("At tag ({}, {}), received in_ = {} and in2 = {}.".format(current_tag.time, current_tag.microstep, in_.value, in2.value)) if in_.is_present and in2.is_present: self.sys.stderr.write("ERROR: invalid logical simultaneity.") self.sys.exit(1) diff --git a/test/Python/src/federated/DistributedLoopedPhysicalAction.lf b/test/Python/src/federated/DistributedLoopedPhysicalAction.lf index 674d601139..a2cce3742a 100644 --- a/test/Python/src/federated/DistributedLoopedPhysicalAction.lf +++ b/test/Python/src/federated/DistributedLoopedPhysicalAction.lf @@ -46,7 +46,7 @@ reactor Receiver(take_a_break_after(10), break_interval(550 msec)) { reaction(startup) {= self.base_logical_time = lf.time.logical() =} reaction(in_) {= - current_tag = lf_tag() + current_tag = lf.tag() print("At tag ({}, {}) received {}".format( current_tag.time - self.base_logical_time, current_tag.microstep, diff --git a/test/Python/src/federated/DistributedStop.lf b/test/Python/src/federated/DistributedStop.lf index 10a2e52777..559ad5a8c5 100644 --- a/test/Python/src/federated/DistributedStop.lf +++ b/test/Python/src/federated/DistributedStop.lf @@ -14,48 +14,49 @@ reactor Sender { state reaction_invoked_correctly(False) reaction(t, act) -> out, act {= + tag = lf.tag() print("Sending 42 at ({}, {}).".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) out.set(42) - if lf_tag().microstep == 0: + if lf.tag().microstep == 0: # Instead of having a separate reaction # for 'act' like Stop.lf, we trigger the # same reaction to test request_stop() being # called multiple times act.schedule(0) - if lf.time.logical_elapsed() == USEC(1): + if tag.time == USEC(1): # Call request_stop() both at (1 usec, 0) and # (1 usec, 1) print("Requesting stop at ({}, {}).".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) request_stop() _1usec1 = Tag(time=USEC(1) + get_start_time(), microstep=1) - if lf.tag_compare(lf_tag(), _1usec1) == 0: + if lf.tag_compare(lf.tag(), _1usec1) == 0: # The reaction was invoked at (1 usec, 1) as expected self.reaction_invoked_correctly = True - elif lf.tag_compare(lf_tag(), _1usec1) > 0: + elif lf.tag_compare(lf.tag(), _1usec1) > 0: # The reaction should not have been invoked at tags larger than (1 usec, 1) sys.stderr.write("ERROR: Invoked reaction(t, act) at tag bigger than shutdown.\n") sys.exit(1) =} reaction(shutdown) {= - if lf.time.logical_elapsed() != USEC(1) or lf_tag().microstep != 1: + if lf.time.logical_elapsed() != USEC(1) or lf.tag().microstep != 1: sys.stderr.write("ERROR: Sender failed to stop the federation in time. Stopping at ({}, {}).\n".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) sys.exit(1) elif not self.reaction_invoked_correctly: sys.stderr.write("ERROR: Sender reaction(t, act) was not invoked at (1 usec, 1). Stopping at ({}, {}).\n".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) sys.exit(1) print("SUCCESS: Successfully stopped the federation at ({}, {}).".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) =} } @@ -66,21 +67,22 @@ reactor Receiver( state reaction_invoked_correctly(False) reaction(in_) {= + tag = lf.tag() print("Received {} at ({}, {}).".format( in_.value, lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) if lf.time.logical_elapsed() == USEC(1): print("Requesting stop at ({}, {}).".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) request_stop() # The receiver should receive a message at tag # (1 usec, 1) and trigger this reaction self.reaction_invoked_correctly = True _1usec1 = Tag(time=USEC(1) + get_start_time(), microstep=1) - if lf.tag_compare(lf_tag(), _1usec1) > 0: + if lf.tag_compare(lf.tag(), _1usec1) > 0: self.reaction_invoked_correctly = False =} @@ -88,19 +90,19 @@ reactor Receiver( # Sender should have requested stop earlier than the receiver. # Therefore, the shutdown events must occur at (1000, 0) on the # receiver. - if lf.time.logical_elapsed() != USEC(1) or lf_tag().microstep != 1: + if lf.time.logical_elapsed() != USEC(1) or lf.tag().microstep != 1: sys.stderr.write("Error: Receiver failed to stop the federation at the right time. Stopping at ({}, {}).\n".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) sys.exit(1) elif not self.reaction_invoked_correctly: sys.stderr.write("Error: Receiver reaction(in) was not invoked the correct number of times. Stopping at ({}, {}).\n".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) sys.exit(1) print("SUCCESS: Successfully stopped the federation at ({}, {}).".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) =} } diff --git a/test/Python/src/federated/DistributedStopZero.lf b/test/Python/src/federated/DistributedStopZero.lf index 79d4929c0a..8557af9889 100644 --- a/test/Python/src/federated/DistributedStopZero.lf +++ b/test/Python/src/federated/DistributedStopZero.lf @@ -18,28 +18,30 @@ reactor Sender { reaction(startup) {= self.startup_logical_time = lf.time.logical() =} reaction(t) -> out {= + tag = lf.tag() print("Sending 42 at ({}, {}).".format( - lf.time.logical_elapsed(), - lf_tag().microstep)) + tag.time, + tag.microstep)) out.set(42) zero = Tag(time=self.startup_logical_time, microstep=0) - if lf.tag_compare(lf_tag(), zero) == 0: + if lf.tag_compare(lf.tag(), zero) == 0: # Request stop at (0,0) print("Requesting stop at ({}, {}).".format( lf.time.logical_elapsed(), - lf_tag().microstep)) + lf.tag().microstep)) request_stop() =} reaction(shutdown) {= - if lf.time.logical_elapsed() != USEC(0) or lf_tag().microstep != 1: + tag = lf.tag() + if tag.time != USEC(0) or tag.microstep != 1: sys.stderr.write("ERROR: Sender failed to stop the federation in time. Stopping at ({}, {}).\n".format( - lf.time.logical_elapsed(), - lf_tag().microstep)) + tag.time, + tag.microstep)) sys.exit(1) print("SUCCESS: Successfully stopped the federation at ({}, {}).".format( - lf.time.logical_elapsed(), - lf_tag().microstep)) + tag.time, + tag.microstep)) =} } @@ -50,16 +52,17 @@ reactor Receiver { reaction(startup) {= self.startup_logical_time = lf.time.logical() =} reaction(in_) {= + tag = lf.tag() print("Received {} at ({}, {}).\n".format( in_.value, - lf.time.logical_elapsed(), - lf_tag().microstep)) + tag.time, + tag.microstep)) zero = Tag(time=self.startup_logical_time, microstep=0) - if lf.tag_compare(lf_tag(), zero) == 0: + if lf.tag_compare(lf.tag(), zero) == 0: # Request stop at (0,0) print("Requesting stop at ({}, {}).".format( - lf.time.logical_elapsed(), - lf_tag().microstep)) + tag.time, + tag.microstep)) request_stop() =} @@ -67,14 +70,15 @@ reactor Receiver { # Sender should have requested stop earlier than the receiver. # Therefore, the shutdown events must occur at (1000, 0) on the # receiver. - if lf.time.logical_elapsed() != USEC(0) or lf_tag().microstep != 1: + tag = lf.tag() + if tag.time != USEC(0) or tag.microstep != 1: sys.stderr.write("ERROR: Receiver failed to stop the federation in time. Stopping at ({}, {}).\n".format( - lf.time.logical_elapsed(), - lf_tag().microstep)) + tag.time, + tag.microstep)) sys.exit(1) print("SUCCESS: Successfully stopped the federation at ({}, {}).\n".format( - lf.time.logical_elapsed(), - lf_tag().microstep)) + tag.time, + tag.microstep)) =} } diff --git a/test/Python/src/federated/failing/ClockSync.lf b/test/Python/src/federated/failing/ClockSync.lf index 81d0ed47e8..ca3fd7af3b 100644 --- a/test/Python/src/federated/failing/ClockSync.lf +++ b/test/Python/src/federated/failing/ClockSync.lf @@ -1,61 +1,54 @@ /** - * This program tests clock synchronization. - * It checks the clock synchronization error and fails - * if it exceeds a threshold. Note that failures could - * occur here intermittently because clock synchronization - * accuracy depends on many conditions. But the threshold - * is quite high, so failures should be rare. + * This program tests clock synchronization. It checks the clock synchronization + * error and fails if it exceeds a threshold. Note that failures could occur + * here intermittently because clock synchronization accuracy depends on many + * conditions. But the threshold is quite high, so failures should be rare. * @author Edward A. Lee */ - -// reason for failing: clock-sync and clock-sync-options not supported in the python target - - +# reason for failing: clock-sync and clock-sync-options not supported in the +# python target target Python { coordination: decentralized, timeout: 10 sec, - clock-sync: on, // Turn on runtime clock synchronization. + clock-sync: on, # Turn on runtime clock synchronization. clock-sync-options: { - local-federates-on: true, // Forces all federates to perform clock sync. - collect-stats: true, // Collect useful statistics like average network delay - // and the standard deviation for the network delay over - // one clock synchronization cycle. Generates a warning - // if the standard deviation is higher than the clock sync - // guard. - test-offset: 200 msec, // Artificially offsets clocks by multiples of 200 msec. - period: 5 msec, // Period with which runtime clock sync is performed. - trials: 10, // Number of messages exchanged to perform clock sync. - attenuation: 10 // Attenuation applied to runtime clock sync adjustments. + # Forces all federates to perform clock sync. + local-federates-on: true, + # Collect useful statistics like average network delay + collect-stats: true, + # and the standard deviation for the network delay over one clock + # synchronization cycle. Generates a warning if the standard deviation + # is higher than the clock sync guard. Artificially offsets clocks by + # multiples of 200 msec. + test-offset: 200 msec, + # Period with which runtime clock sync is performed. + period: 5 msec, + # Number of messages exchanged to perform clock sync. + trials: 10, + # Attenuation applied to runtime clock sync adjustments. + attenuation: 10 } -}; +} -/** - * Reactor that outputs periodically. - */ -reactor Ticker(period:time(1600 msec)) { - output out:int; +/** Reactor that outputs periodically. */ +reactor Ticker(period(1600 msec)) { + output out - timer tick(0, period); + timer tick(0, period) - reaction(tick) -> out {= - SET(out, 42); - =} + reaction(tick) -> out {= SET(out, 42); =} } -/** - * Print a message when an input arrives. - */ +/** Print a message when an input arrives. */ reactor Printer { - input in:int; + input in_ reaction(startup) {= interval_t offset = _lf_time_physical_clock_offset + _lf_time_test_physical_clock_offset; info_print("Clock sync error at startup is %lld ns.", offset); =} - reaction(in) {= - info_print("Received %d.", in->value); - =} + reaction(in_) {= info_print("Received %d.", in->value); =} reaction(shutdown) {= interval_t offset = _lf_time_physical_clock_offset + _lf_time_test_physical_clock_offset; @@ -69,12 +62,12 @@ reactor Printer { } reactor Federate { - source = new Ticker(); - play = new Printer(); - source.out -> play.in; + source = new Ticker() + play = new Printer() + source.out -> play.in_ } federated reactor ClockSync { - fed1 = new Federate(); - fed2 = new Federate(); + fed1 = new Federate() + fed2 = new Federate() } diff --git a/test/Python/src/federated/failing/DistributedLoopedActionDecentralized.lf b/test/Python/src/federated/failing/DistributedLoopedActionDecentralized.lf index 9855698abb..c79e8d7bed 100644 --- a/test/Python/src/federated/failing/DistributedLoopedActionDecentralized.lf +++ b/test/Python/src/federated/failing/DistributedLoopedActionDecentralized.lf @@ -1,46 +1,41 @@ /** - * Test a sender-receiver network system that - * relies on microsteps being taken into account. - * The purpose of this test is to check whether the functionalities - * pertinent to dynamic STP offset adjustments are present and + * Test a sender-receiver network system that relies on microsteps being taken + * into account. The purpose of this test is to check whether the + * functionalities pertinent to dynamic STP offset adjustments are present and * functioning to a degree. * - * This version of the test does not use a centralized - * coordinator to advance tag. Therefore, - * the receiver will rely on an STP offset (initially - * zero) to wait long enough for messages to arrive - * before advancing its tag. In this test, - * the STP offset is initially zero and gradually - * raised every time an STP violation is perceived until - * no STP violation is observed. Therefore, the exact - * outcome of the test will depend on actual runtime - * timing. - * + * This version of the test does not use a centralized coordinator to advance + * tag. Therefore, the receiver will rely on an STP offset (initially zero) to + * wait long enough for messages to arrive before advancing its tag. In this + * test, the STP offset is initially zero and gradually raised every time an STP + * violation is perceived until no STP violation is observed. Therefore, the + * exact outcome of the test will depend on actual runtime timing. * * @author Soroush Bateni */ - -// reason for failing: in_.intended_tag are not supported in python target - +# reason for failing: in_.intended_tag are not supported in python target target Python { timeout: 1 sec, coordination: decentralized -}; - +} import Sender from "../lib/LoopedActionSender.lf" import Receiver from "DistributedLoopedAction.lf" -reactor STPReceiver(take_a_break_after(10), break_interval(400 msec), stp_offset(0)) { - input inp; - state last_time_updated_stp(0); - receiver = new Receiver(take_a_break_after = 10, break_interval = 400 msec); - timer t (0, 1 msec); // Force advancement of logical time +reactor STPReceiver( + take_a_break_after(10), + break_interval(400 msec), + stp_offset(0) +) { + input inp + state last_time_updated_stp(0) + receiver = new Receiver(take_a_break_after = 10, break_interval = 400 msec) + timer t(0, 1 msec) # Force advancement of logical time - reaction (inp) -> receiver.in_ {= + reaction(inp) -> receiver.in_ {= print(f"Received {inp.value}.") receiver.in_.set(inp.value) - =} STP (stp_offset) {= + =} STP(stp_offset) {= print(f"Received {inp.value} late.") current_tag = lf.tag() print(f"STP violation of " @@ -56,15 +51,17 @@ reactor STPReceiver(take_a_break_after(10), break_interval(400 msec), stp_offset self.last_time_updated_stp = current_tag.time =} - reaction (t) {= + reaction(t) {= # Do nothing =} } - federated reactor DistributedLoopedActionDecentralized { - sender = new Sender(take_a_break_after = 10, break_interval = 400 msec); - stpReceiver = new STPReceiver(take_a_break_after = 10, break_interval = 400 msec); + sender = new Sender(take_a_break_after = 10, break_interval = 400 msec) + stpReceiver = new STPReceiver( + take_a_break_after = 10, + break_interval = 400 msec + ) - sender.out -> stpReceiver.inp; + sender.out -> stpReceiver.inp } diff --git a/test/Python/src/federated/failing/DistributedNetworkOrder.lf b/test/Python/src/federated/failing/DistributedNetworkOrder.lf index a3cf9f91c3..7dbcb5b699 100644 --- a/test/Python/src/federated/failing/DistributedNetworkOrder.lf +++ b/test/Python/src/federated/failing/DistributedNetworkOrder.lf @@ -1,23 +1,22 @@ -/* - * This is a test for send_timed_message, - * which is an internal API. +/** + * This is a test for send_timed_message, which is an internal API. * - * This test sends a second message at time 5 msec that has the same intended tag as - * a message that it had previously sent at time 0 msec. This results in a warning, - * but the message microstep is incremented and correctly received one microstep later. + * This test sends a second message at time 5 msec that has the same intended + * tag as a message that it had previously sent at time 0 msec. This results in + * a warning, but the message microstep is incremented and correctly received + * one microstep later. * * @author Soroush Bateni */ - -// reason for failing: send_timed_message() is not not supported in python target - +# reason for failing: send_timed_message() is not not supported in python target target Python { timeout: 1 sec -}; +} reactor Sender { - output out:int; - timer t(0, 1 msec); + output out + timer t(0, 1 msec) + reaction(t) {= int payload = 1; if (lf.time.logical_elapsed() == 0LL) { @@ -32,22 +31,22 @@ reactor Sender { } reactor Receiver { - input in:int; - state success:int(0); + input in_ + state success(0) - reaction(in) {= - tag_t current_tag = lf_tag(); + reaction(in_) {= + tag_t current_tag = lf.tag(); if (current_tag.time == (start_time + MSEC(10))) { - if (current_tag.microstep == 0 && in->value == 1) { + if (current_tag.microstep == 0 && in_->value == 1) { self->success++; - } else if (current_tag.microstep == 1 && in->value == 2) { + } else if (current_tag.microstep == 1 && in_->value == 2) { self->success++; } } printf("Received %d at tag (%lld, %u).\n", - in->value, - lf.time.logical_elapsed(), - lf_tag().microstep); + in_->value, + current_tag.time, + current_tag.microstep); =} reaction(shutdown) {= @@ -60,8 +59,8 @@ reactor Receiver { } federated reactor DistributedNetworkOrder { - sender = new Sender(); - receiver = new Receiver(); + sender = new Sender() + receiver = new Receiver() - sender.out -> receiver.in; + sender.out -> receiver.in_ } diff --git a/test/Python/src/federated/failing/LoopDistributedCentralized.lf b/test/Python/src/federated/failing/LoopDistributedCentralized.lf index 3a6321e4ba..8e88841135 100644 --- a/test/Python/src/federated/failing/LoopDistributedCentralized.lf +++ b/test/Python/src/federated/failing/LoopDistributedCentralized.lf @@ -1,17 +1,19 @@ /** - * This tests a feedback loop with physical actions and - * centralized coordination. + * This tests a feedback loop with physical actions and centralized + * coordination. * * @author Edward A. Lee */ - - // reason for failing: lf_comma_separated_time() not supported in the python target - +# reason for failing: lf_comma_separated_time() not supported in the python +# target target Python { coordination: centralized, - coordination-options: {advance-message-interval: 100 msec}, + coordination-options: { + advance-message-interval: 100 msec + }, timeout: 5 sec } + preamble {= #include // Defines sleep() bool stop = false; @@ -26,27 +28,31 @@ preamble {= } =} -reactor Looper(incr:int(1), delay:time(0 msec)) { - input in:int; - output out:int; - physical action a(delay); - state count:int(0); +reactor Looper(incr(1), delay(0 msec)) { + input in_ + output out + physical action a(delay) + state count(0) + reaction(startup) -> a {= // Start the thread that listens for Enter or Return. lf_thread_t thread_id; info_print("Starting thread."); lf_thread_create(&thread_id, &ping, a); =} + reaction(a) -> out {= SET(out, self->count); self->count += self->incr; =} - reaction(in) {= + + reaction(in_) {= instant_t time_lag = lf.time.physical() - lf.time.logical(); char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 lf_comma_separated_time(time_buffer, time_lag); - info_print("Received %d. Logical time is behind physical time by %s nsec.", in->value, time_buffer); + info_print("Received %d. Logical time is behind physical time by %s nsec.", in_->value, time_buffer); =} + reaction(shutdown) {= info_print("******* Shutdown invoked."); // Stop the thread that is scheduling actions. @@ -56,9 +62,10 @@ reactor Looper(incr:int(1), delay:time(0 msec)) { } =} } -federated reactor LoopDistributedCentralized(delay:time(0)) { - left = new Looper(); - right = new Looper(incr = -1); - left.out -> right.in; - right.out -> left.in; + +federated reactor LoopDistributedCentralized(delay(0)) { + left = new Looper() + right = new Looper(incr = -1) + left.out -> right.in_ + right.out -> left.in_ } diff --git a/test/Python/src/federated/failing/LoopDistributedCentralizedPrecedence.lf b/test/Python/src/federated/failing/LoopDistributedCentralizedPrecedence.lf index 2d7464952a..0efda23eb8 100644 --- a/test/Python/src/federated/failing/LoopDistributedCentralizedPrecedence.lf +++ b/test/Python/src/federated/failing/LoopDistributedCentralizedPrecedence.lf @@ -1,42 +1,47 @@ /** - * This tests that the precedence order of reaction invocation is kept - * when a feedback loop is present in centralized coordination. + * This tests that the precedence order of reaction invocation is kept when a + * feedback loop is present in centralized coordination. * * @author Edward A. Lee * @author Soroush Bateni */ - -// reason for failing: lf_comma_separated_time() not supported in the python target - +# reason for failing: lf_comma_separated_time() not supported in the python +# target target Python { flags: "-Wall", coordination: centralized, - coordination-options: {advance-message-interval: 100 msec}, + coordination-options: { + advance-message-interval: 100 msec + }, timeout: 5 sec } -reactor Looper(incr:int(1), delay:time(0 msec)) { - input in:int; - output out:int; - state count:int(0); - state received_count:int(0); - timer t(0, 1 sec); +reactor Looper(incr(1), delay(0 msec)) { + input in_ + output out + state count(0) + state received_count(0) + timer t(0, 1 sec) + reaction(t) -> out {= SET(out, self->count); self->count += self->incr; =} - reaction(in) {= + + reaction(in_) {= instant_t time_lag = lf.time.physical() - lf.time.logical(); char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 lf_comma_separated_time(time_buffer, time_lag); - info_print("Received %d. Logical time is behind physical time by %s nsec.", in->value, time_buffer); + info_print("Received %d. Logical time is behind physical time by %s nsec.", in_->value, time_buffer); self->received_count = self->count; =} + reaction(t) {= if (self->received_count != self->count) { - lf_print_error_and_exit("reaction(t) was invoked before reaction(in). Precedence order was not kept."); + lf_print_error_and_exit("reaction(t) was invoked before reaction(in_). Precedence order was not kept."); } =} + reaction(shutdown) {= info_print("******* Shutdown invoked."); if (self->count != 6 * self->incr) { @@ -44,9 +49,10 @@ reactor Looper(incr:int(1), delay:time(0 msec)) { } =} } -federated reactor (delay:time(0)) { - left = new Looper(); - right = new Looper(incr = -1); - left.out -> right.in; - right.out -> left.in; + +federated reactor(delay(0)) { + left = new Looper() + right = new Looper(incr = -1) + left.out -> right.in_ + right.out -> left.in_ } diff --git a/test/Python/src/federated/failing/LoopDistributedDecentralized.lf b/test/Python/src/federated/failing/LoopDistributedDecentralized.lf index 4258bf9c21..6b3a065be7 100644 --- a/test/Python/src/federated/failing/LoopDistributedDecentralized.lf +++ b/test/Python/src/federated/failing/LoopDistributedDecentralized.lf @@ -1,16 +1,16 @@ /** - * This tests a feedback loop with physical actions and - * decentralized coordination. + * This tests a feedback loop with physical actions and decentralized + * coordination. * * @author Edward A. Lee */ - -// reason for failing: lf_comma_separated_time() not supported in the python target - +# reason for failing: lf_comma_separated_time() not supported in the python +# target target Python { coordination: decentralized, timeout: 5 sec } + preamble {= #include // Defines sleep() bool stop = false; @@ -25,38 +25,42 @@ preamble {= } =} -reactor Looper(incr:int(1), delay:time(0 msec), stp_offset:time(0)) { - input in:int; - output out:int; - physical action a(stp_offset); - state count:int(0); +reactor Looper(incr(1), delay(0 msec), stp_offset(0)) { + input in_ + output out + physical action a(stp_offset) + state count(0) + reaction(startup) -> a {= // Start the thread that listens for Enter or Return. lf_thread_t thread_id; info_print("Starting thread."); lf_thread_create(&thread_id, &ping, a); =} + reaction(a) -> out {= info_print("Setting out."); SET(out, self->count); self->count += self->incr; =} - reaction(in) {= + + reaction(in_) {= instant_t time_lag = lf.time.physical() - lf.time.logical(); char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 lf_comma_separated_time(time_buffer, time_lag); - info_print("Received %d. Logical time is behind physical time by %s nsec.", in->value, time_buffer); - =} STP (stp_offset) {= + info_print("Received %d. Logical time is behind physical time by %s nsec.", in_->value, time_buffer); + =} STP(stp_offset) {= instant_t time_lag = lf.time.physical() - lf.time.logical(); char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 lf_comma_separated_time(time_buffer, time_lag); - info_print("STP offset was violated. Received %d. Logical time is behind physical time by %s nsec.", in->value, time_buffer); - =} deadline (10 msec) {= + info_print("STP offset was violated. Received %d. Logical time is behind physical time by %s nsec.", in_->value, time_buffer); + =} deadline(10 msec) {= instant_t time_lag = lf.time.physical() - lf.time.logical(); char time_buffer[28]; // 28 bytes is enough for the largest 64 bit number: 9,223,372,036,854,775,807 lf_comma_separated_time(time_buffer, time_lag); - info_print("Deadline miss. Received %d. Logical time is behind physical time by %s nsec.", in->value, time_buffer); + info_print("Deadline miss. Received %d. Logical time is behind physical time by %s nsec.", in_->value, time_buffer); =} + reaction(shutdown) {= info_print("******* Shutdown invoked."); // Stop the thread that is scheduling actions. @@ -66,9 +70,10 @@ reactor Looper(incr:int(1), delay:time(0 msec), stp_offset:time(0)) { } =} } -federated reactor LoopDistributedDecentralized(delay:time(0)) { - left = new Looper(stp_offset = 900 usec); - right = new Looper(incr = -1, stp_offset = 2400 usec); - left.out -> right.in; - right.out -> left.in; + +federated reactor LoopDistributedDecentralized(delay(0)) { + left = new Looper(stp_offset = 900 usec) + right = new Looper(incr = -1, stp_offset = 2400 usec) + left.out -> right.in_ + right.out -> left.in_ } diff --git a/test/Python/src/federated/failing/LoopDistributedDouble.lf b/test/Python/src/federated/failing/LoopDistributedDouble.lf index 46bcb9f585..7c94f3b3ef 100644 --- a/test/Python/src/federated/failing/LoopDistributedDouble.lf +++ b/test/Python/src/federated/failing/LoopDistributedDouble.lf @@ -1,16 +1,15 @@ /** - * This tests a feedback loop with physical actions and - * centralized coordination. + * This tests a feedback loop with physical actions and centralized + * coordination. * * @author Edward A. Lee */ - -// reason for failing: current tag struct not supported in the python target - +# reason for failing: current tag struct not supported in the python target target Python { coordination: centralized, timeout: 5 sec } + preamble {= #include // Defines sleep() stop = False @@ -26,19 +25,21 @@ preamble {= =} reactor Looper(incr(1), delay(0 msec)) { - input in_; - input in2; - output out; - output out2; - physical action a(delay); - state count(0); - timer t(0, 1 sec); + input in_ + input in2 + output out + output out2 + physical action a(delay) + state count(0) + timer t(0, 1 sec) + reaction(startup) -> a {= # Start the thread that listens for Enter or Return. lf_thread_t thread_id; info_print("Starting thread."); lf_thread_create(&thread_id, &ping, a); =} + reaction(a) -> out, out2 {= if (self->count%2 == 0) { SET(out, self->count); @@ -47,23 +48,27 @@ reactor Looper(incr(1), delay(0 msec)) { } self->count += self->incr; =} - reaction(in) {= + + reaction(in_) {= info_print("Received %d at logical time (%lld, %d).", in->value, current_tag.time - start_time, current_tag.microstep ); =} + reaction(in2) {= info_print("Received %d on in2 at logical time (%lld, %d).", in2->value, current_tag.time - start_time, current_tag.microstep ); =} + reaction(t) {= info_print("Timer triggered at logical time (%lld, %d).", current_tag.time - start_time, current_tag.microstep ); =} + reaction(shutdown) {= info_print("******* Shutdown invoked."); // Stop the thread that is scheduling actions. @@ -73,11 +78,12 @@ reactor Looper(incr(1), delay(0 msec)) { } =} } -federated reactor (delay:time(0)) { - left = new Looper(); - right = new Looper(incr = -1); - left.out -> right.in; - right.out -> left.in; - right.out2 -> left.in2; - left.out2 -> right.in2; + +federated reactor(delay(0)) { + left = new Looper() + right = new Looper(incr = -1) + left.out -> right.in_ + right.out -> left.in_ + right.out2 -> left.in2 + left.out2 -> right.in2 } diff --git a/test/TypeScript/src/federated/DistributedCount.lf b/test/TypeScript/src/federated/DistributedCount.lf index af0714300e..423dadd499 100644 --- a/test/TypeScript/src/federated/DistributedCount.lf +++ b/test/TypeScript/src/federated/DistributedCount.lf @@ -22,7 +22,7 @@ reactor Print { if (inp !== c) { util.requestErrorStop("Expected to receive " + c + "."); } - if (elapsedTime.isEqualTo(TimeValue.msec(200).add(TimeValue.sec(c - 1)))) { + if (!elapsedTime.isEqualTo(TimeValue.msec(200).add(TimeValue.sec(c - 1)))) { util.requestErrorStop("Expected received time to be " + TimeValue.msec(200).add(TimeValue.sec(c - 1)) + "."); } c++; diff --git a/test/TypeScript/src/federated/LoopDistributedDouble.lf b/test/TypeScript/src/federated/LoopDistributedDouble.lf index 3207d93300..b86e55feaa 100644 --- a/test/TypeScript/src/federated/LoopDistributedDouble.lf +++ b/test/TypeScript/src/federated/LoopDistributedDouble.lf @@ -7,6 +7,7 @@ */ target TypeScript { timeout: 5 sec, + keepAlive: true, coordination-options: { advance-message-interval: 100 msec }