")
+ }
+}
+
+
+fun EObject.locationInfo(): LocationInfo {
+ val node = NodeModelUtils.getNode(this)
+ return LocationInfo(
+ line = node.startLine,
+ fileName = this.eResource().toPath().toUnixString(),
+ lfText = toTextTokenBased() ?: ""
+ )
+}
+
diff --git a/org.lflang/src/org/lflang/generator/InvalidLfSourceException.java b/org.lflang/src/org/lflang/generator/InvalidLfSourceException.java
new file mode 100644
index 0000000000..515bdbb8a0
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/InvalidLfSourceException.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021, TU Dresden.
+ *
+ * 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.generator;
+
+import org.eclipse.emf.ecore.EObject;
+
+/**
+ * An exception that indicates invalid source, which should
+ * be reported to the user. This is an error, it should not
+ * be used for warnings.
+ *
+ * @author Clément Fournier
+ */
+public class InvalidLfSourceException extends RuntimeException {
+
+ private final EObject node;
+ private final String problem;
+
+ public InvalidLfSourceException(EObject node, String problem) {
+ super(problem);
+ this.node = node;
+ this.problem = problem;
+ }
+
+ public InvalidLfSourceException(String problem, EObject node) {
+ this(node, problem);
+ }
+
+ public EObject getNode() {
+ return node;
+ }
+
+ public String getProblem() {
+ return problem;
+ }
+}
diff --git a/org.lflang/src/org/lflang/generator/LFGenerator.java b/org.lflang/src/org/lflang/generator/LFGenerator.java
index a071a9f0c3..3a7efc08f8 100644
--- a/org.lflang/src/org/lflang/generator/LFGenerator.java
+++ b/org.lflang/src/org/lflang/generator/LFGenerator.java
@@ -37,13 +37,13 @@ public class LFGenerator extends AbstractGenerator {
/**
* Create a target-specific FileConfig object in Kotlin
- *
+ *
* Since the CppFileConfig and TSFileConfig class are implemented in Kotlin, the classes are
* not visible from all contexts. If the RCA is run from within Eclipse via
* "Run as Eclipse Application", the Kotlin classes are unfortunately not
* available at runtime due to bugs in the Eclipse Kotlin plugin. (See
* https://stackoverflow.com/questions/68095816/is-ist-possible-to-build-mixed-kotlin-and-java-applications-with-a-recent-eclips)
- *
+ *
* If the FileConfig class is found, this method returns an instance.
* Otherwise, it returns an Instance of FileConfig.
*
@@ -51,16 +51,16 @@ public class LFGenerator extends AbstractGenerator {
* @throws IOException If the file config could not be created properly
*/
private FileConfig createFileConfig(final Target target,
- Resource resource,
- IFileSystemAccess2 fsa,
- IGeneratorContext context)
- throws IOException {
+ Resource resource,
+ IFileSystemAccess2 fsa,
+ IGeneratorContext context) throws IOException {
// 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
// found.
switch (target) {
case CPP:
+ case Rust:
case TS: {
String className = "org.lflang.generator." + target.packageName + "." + target.classNamePrefix + "FileConfig";
try {
@@ -79,7 +79,10 @@ private FileConfig createFileConfig(final Target target,
}
}
- /** Create a generator object for the given 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) {
@@ -88,15 +91,17 @@ private GeneratorBase createGenerator(Target target, FileConfig fileConfig,
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!");
}
+
/**
* Create a code generator in Kotlin.
- *
+ *
* Since the CppGenerator and TSGenerator class are implemented in Kotlin, the classes are
* not visible from all contexts. If the RCA is run from within Eclipse via
* "Run as Eclipse Application", the Kotlin classes are unfortunately not
@@ -107,7 +112,7 @@ private GeneratorBase createGenerator(Target target, FileConfig fileConfig,
* @return A Kotlin Generator object if the class can be found
*/
private GeneratorBase createKotlinBaseGenerator(Target target, FileConfig fileConfig,
- ErrorReporter errorReporter) {
+ ErrorReporter errorReporter) {
// 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
@@ -144,6 +149,7 @@ public void doGenerate(Resource resource, IFileSystemAccess2 fsa,
IGeneratorContext context) {
// Determine which target is desired.
final Target target = Target.fromDecl(ASTUtils.targetDecl(resource));
+ assert target != null;
FileConfig fileConfig;
try {
diff --git a/org.lflang/src/org/lflang/generator/ParameterInstance.xtend b/org.lflang/src/org/lflang/generator/ParameterInstance.xtend
index dd77dfe61f..7b15aaa6ff 100644
--- a/org.lflang/src/org/lflang/generator/ParameterInstance.xtend
+++ b/org.lflang/src/org/lflang/generator/ParameterInstance.xtend
@@ -34,7 +34,7 @@ import org.lflang.lf.LfFactory
import org.lflang.lf.Parameter
import org.lflang.lf.Value
-import static extension org.lflang.ASTUtils.*
+import static extension org.lflang.JavaAstUtils.*
/**
* Representation of a runtime instance of a parameter.
diff --git a/org.lflang/src/org/lflang/generator/PythonGenerator.xtend b/org.lflang/src/org/lflang/generator/PythonGenerator.xtend
index b8a262b169..128219bc64 100644
--- a/org.lflang/src/org/lflang/generator/PythonGenerator.xtend
+++ b/org.lflang/src/org/lflang/generator/PythonGenerator.xtend
@@ -59,6 +59,7 @@ import org.lflang.lf.Value
import org.lflang.lf.VarRef
import static extension org.lflang.ASTUtils.*
+import static extension org.lflang.JavaAstUtils.*
import org.lflang.TargetConfig
import org.lflang.generator.c.CCompiler
diff --git a/org.lflang/src/org/lflang/generator/TargetTypes.java b/org.lflang/src/org/lflang/generator/TargetTypes.java
new file mode 100644
index 0000000000..5cedd7c891
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/TargetTypes.java
@@ -0,0 +1,224 @@
+package org.lflang.generator;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.lflang.ASTUtils;
+import org.lflang.InferredType;
+import org.lflang.JavaAstUtils;
+import org.lflang.TimeValue;
+import org.lflang.lf.Time;
+import org.lflang.lf.TimeUnit;
+import org.lflang.lf.Type;
+import org.lflang.lf.Value;
+
+/**
+ * Information about the types of a target language. Contains
+ * utilities to convert LF expressions and types to the target
+ * language. Each code generator is expected to use at least one
+ * language-specific instance of this interface.
+ *
+ * TODO currently, {@link GeneratorBase} implements this interface,
+ * it should instead contain an instance.
+ */
+public interface TargetTypes {
+
+
+ /**
+ * Return true if the target supports generics (i.e., parametric
+ * polymorphism), false otherwise.
+ */
+ boolean supportsGenerics();
+
+
+ /**
+ * Return the type of time durations.
+ */
+ String getTargetTimeType();
+
+
+ /**
+ * Return the type of tags.
+ */
+ String getTargetTagType();
+
+
+ /**
+ * Return the type of fixed sized lists (or arrays).
+ */
+ String getTargetFixedSizeListType(String baseType, int size);
+
+
+ /**
+ * Return the type of variable sized lists (eg {@code std::vector}).
+ */
+ String getTargetVariableSizeListType(String baseType);
+
+
+ /**
+ * Return an "undefined" type which is used as a default
+ * when a type cannot be inferred.
+ */
+ String getTargetUndefinedType();
+
+ /**
+ * Returns a version of the given LF identifier that is
+ * escaped properly for insertion into a piece of target
+ * code.
+ */
+ default String escapeIdentifier(String ident) {
+ return ident;
+ }
+
+ /**
+ * Returns an expression in the target language that corresponds
+ * to a time value ({@link #getTargetTimeType()}), with the given
+ * magnitude and unit. The unit may not be null (use {@link TimeUnit#NONE}).
+ */
+ default String getTargetTimeExpression(long magnitude, TimeUnit unit) {
+ // todo make non-default when we reuse this for all generators,
+ // all targets should support this.
+ Objects.requireNonNull(unit);
+ throw new UnsupportedGeneratorFeatureException("Time expressions");
+ }
+
+ /**
+ * Returns an expression in the target language that corresponds
+ * to a variable-size list expression.
+ *
+ * @throws UnsupportedGeneratorFeatureException If the target does not support this
+ */
+ default String getVariableSizeListInitExpression(List contents, boolean withBraces) {
+ throw new UnsupportedGeneratorFeatureException("Variable size lists");
+ }
+
+ /**
+ * Returns an expression in the target language that corresponds
+ * to a fixed-size list expression.
+ *
+ * @throws UnsupportedGeneratorFeatureException If the target does not support this
+ */
+ default String getFixedSizeListInitExpression(List contents, int listSize, boolean withBraces) {
+ throw new UnsupportedGeneratorFeatureException("Fixed size lists");
+ }
+
+
+ /**
+ * Returns the expression that is used to replace a
+ * missing expression in the source language. The expression
+ * may for instance be a type-agnostic default value
+ * (e.g. Rust's {@code Default::default()}), or produce
+ * a compiler error (e.g. Rust's {@code compiler_error!("missing initializer")}).
+ *
+ * @throws UnsupportedGeneratorFeatureException If the target does not support this
+ */
+ default String getMissingExpr() {
+ throw new UnsupportedGeneratorFeatureException("Missing initializers");
+ }
+
+
+ /**
+ * Returns a target type inferred from the type node, or the
+ * initializer list. If both are absent, then the undefined
+ * type is returned.
+ */
+ default String getTargetType(Type type, List init) {
+ return getTargetType(JavaAstUtils.getInferredType(type, init));
+ }
+
+ /**
+ * Returns the target type of the type node. This just provides
+ * a default parameter for {@link #getTargetType(Type, List)}.
+ * If the parameter is null, then the undefined type is returned.
+ */
+ default String getTargetType(Type type) {
+ return getTargetType(type, null);
+ }
+
+ /**
+ * Return a string representing the specified type in the
+ * target language.
+ */
+ default String getTargetType(InferredType type) {
+ if (type.isUndefined()) {
+ return getTargetUndefinedType();
+ } else if (type.isTime) {
+ if (type.isFixedSizeList) {
+ return getTargetFixedSizeListType(getTargetTimeType(), type.listSize);
+ } else if (type.isVariableSizeList) {
+ return getTargetVariableSizeListType(getTargetTimeType());
+ } else {
+ return getTargetTimeType();
+ }
+ } else if (type.isFixedSizeList) {
+ return getTargetFixedSizeListType(type.baseType(), type.listSize);
+ } else if (type.isVariableSizeList) {
+ return getTargetVariableSizeListType(type.baseType());
+ }
+ return type.toText();
+ }
+
+ /**
+ * Returns the representation of the given initializer
+ * expression in target code. The given type, if non-null,
+ * may inform the code generation.
+ *
+ * @param init Initializer list (non-null)
+ * @param type Declared type of the expression (nullable)
+ * @param initWithBraces Whether the initializer uses the braced form.
+ */
+ default String getTargetInitializer(List init, Type type, boolean initWithBraces) {
+ Objects.requireNonNull(init);
+ var inferredType = JavaAstUtils.getInferredType(type, init);
+ if (init.size() == 1) {
+ return getTargetExpr(init.get(0), inferredType);
+ }
+ var targetValues = init.stream().map(it -> getTargetExpr(it, inferredType)).collect(Collectors.toList());
+ if (inferredType.isFixedSizeList) {
+ return getFixedSizeListInitExpression(targetValues, inferredType.listSize, initWithBraces);
+ } else if (inferredType.isVariableSizeList) {
+ return getVariableSizeListInitExpression(targetValues, initWithBraces);
+ } else {
+ return getMissingExpr();
+ }
+ }
+
+
+ /**
+ * Returns the representation of the given value in target code.
+ * The given type, if non-null, may inform the code generation.
+ */
+ default String getTargetExpr(Value value, InferredType type) {
+ if (ASTUtils.isZero(value) && type != null && type.isTime) {
+ return getTargetTimeExpression(0, TimeUnit.NONE);
+ } else if (value.getParameter() != null) {
+ return escapeIdentifier(value.getParameter().getName());
+ } else if (value.getTime() != null) {
+ return getTargetTimeExpr(value.getTime());
+ } else if (value.getLiteral() != null) {
+ return value.getLiteral();// here we don't escape
+ } else if (value.getCode() != null) {
+ return ASTUtils.toText(value.getCode());
+ } else {
+ throw new IllegalStateException("Invalid value " + value);
+ }
+ }
+
+
+ /**
+ * Returns the representation of the given time value in
+ * target code.
+ */
+ default String getTargetTimeExpr(TimeValue tv) {
+ return getTargetTimeExpression(tv.time, tv.unit);
+ }
+
+ /**
+ * Returns the representation of the given time value in
+ * target code.
+ */
+ default String getTargetTimeExpr(Time t) {
+ return getTargetTimeExpression(t.getInterval(), t.getUnit());
+ }
+}
diff --git a/org.lflang/src/org/lflang/generator/UnsupportedGeneratorFeatureException.java b/org.lflang/src/org/lflang/generator/UnsupportedGeneratorFeatureException.java
new file mode 100644
index 0000000000..59fe20386c
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/UnsupportedGeneratorFeatureException.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2021, TU Dresden.
+ *
+ * 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.generator;
+
+import org.eclipse.emf.ecore.EObject;
+
+/**
+ * Signals that the code generator does not support a particular
+ * feature of the source language.
+ */
+public class UnsupportedGeneratorFeatureException extends GenerationException {
+
+ public UnsupportedGeneratorFeatureException(String feature) {
+ super("Unsupported generator feature: " + feature);
+ }
+
+ public UnsupportedGeneratorFeatureException(EObject location, String feature) {
+ super(location, "Unsupported generator feature: " + feature);
+ }
+}
diff --git a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend
index 93b989dc79..9782ee3df6 100644
--- a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend
+++ b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend
@@ -90,6 +90,7 @@ import org.lflang.lf.Variable
import org.lflang.util.XtendUtil
import static extension org.lflang.ASTUtils.*
+import static extension org.lflang.JavaAstUtils.*
import org.lflang.TargetConfig
/**
@@ -5886,13 +5887,10 @@ class CGenerator extends GeneratorBase {
override getTargetTimeType() '''interval_t'''
override getTargetTagType() '''tag_t'''
-
- override getTargetTagIntervalType() '''tag_interval_t'''
override getTargetUndefinedType() '''/* «errorReporter.reportError("undefined type")» */'''
- override getTargetFixedSizeListType(String baseType,
- Integer size) '''«baseType»[«size»]'''
+ override getTargetFixedSizeListType(String baseType, int size) '''«baseType»[«size»]'''
override String getTargetVariableSizeListType(
String baseType) '''«baseType»[]'''
diff --git a/org.lflang/src/org/lflang/generator/cpp/CppActionGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppActionGenerator.kt
index 174f7d9691..80cebc30a1 100644
--- a/org.lflang/src/org/lflang/generator/cpp/CppActionGenerator.kt
+++ b/org.lflang/src/org/lflang/generator/cpp/CppActionGenerator.kt
@@ -26,6 +26,7 @@ package org.lflang.generator.cpp
import org.lflang.ErrorReporter
import org.lflang.generator.PrependOperator
+import org.lflang.inferredType
import org.lflang.isLogical
import org.lflang.lf.Action
import org.lflang.lf.LfPackage
@@ -35,8 +36,12 @@ import org.lflang.lf.Reactor
class CppActionGenerator(private val reactor: Reactor, private val errorReporter: ErrorReporter) {
companion object {
- val Action.cppType
- get() = if (this.isLogical) "reactor::LogicalAction<$targetType>" else "reactor::PhysicalAction<$targetType>"
+ val Action.cppType: String
+ get() {
+ val dataType = inferredType.cppType
+ return if (this.isLogical) "reactor::LogicalAction<$dataType>"
+ else "reactor::PhysicalAction<$dataType>"
+ }
val startupName: String = LfPackage.Literals.TRIGGER_REF__STARTUP.name
val shutdownName: String = LfPackage.Literals.TRIGGER_REF__SHUTDOWN.name
@@ -85,4 +90,4 @@ class CppActionGenerator(private val reactor: Reactor, private val errorReporter
/** Get all action initializers */
fun generateInitializers() =
reactor.actions.joinToString(separator = "\n", prefix = "// actions\n", postfix = "\n") { generateInitializer(it) }
-}
\ No newline at end of file
+}
diff --git a/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt b/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt
index 0386463303..44318e8988 100644
--- a/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt
+++ b/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt
@@ -39,10 +39,7 @@ import java.time.format.DateTimeFormatter
/** Get the "name" a reaction is represented with in target code.*/
val Reaction.name
- get(): String {
- val r = this.eContainer() as Reactor
- return "r" + r.reactions.lastIndexOf(this)
- }
+ get(): String = "r$indexInContainer"
/* **********************************************************************************************
* C++ specific extensions shared across classes
@@ -50,41 +47,9 @@ val Reaction.name
// TODO: Most of the extensions defined here should be moved to companion objects of their
// corresponding generator classes. See for instance the CppParameterGenerator
-/** Get a C++ representation of a LF unit. */
-val TimeValue.cppUnit
- get() = when (this.unit) {
- TimeUnit.NSEC -> "ns"
- TimeUnit.NSECS -> "ns"
- TimeUnit.USEC -> "us"
- TimeUnit.USECS -> "us"
- TimeUnit.MSEC -> "ms"
- TimeUnit.MSECS -> "ms"
- TimeUnit.SEC -> "s"
- TimeUnit.SECS -> "s"
- TimeUnit.SECOND -> "s"
- TimeUnit.SECONDS -> "s"
- TimeUnit.MIN -> "min"
- TimeUnit.MINS -> "min"
- TimeUnit.MINUTE -> "min"
- TimeUnit.MINUTES -> "min"
- TimeUnit.HOUR -> "h"
- TimeUnit.HOURS -> "h"
- TimeUnit.DAY -> "d"
- TimeUnit.DAYS -> "d"
- TimeUnit.WEEK -> "d*7"
- TimeUnit.WEEKS -> "d*7"
- TimeUnit.NONE -> ""
- else -> ""
- }
/** Convert a LF time value to a representation in C++ code */
-fun TimeValue.toCode() = if (this.time == 0L) "reactor::Duration::zero()" else "${this.time}${this.cppUnit}"
-
-/** Convert a Time to a representation in C++ code
- *
- * FIXME this is redundant to GeneratorBase.getTargetTime
- */
-fun Time.toCode() = TimeValue(this.interval.toLong(), this.unit).toCode()
+fun TimeValue.toCode() = CppTypes.getTargetTimeExpression(time, unit)
/** Convert a value to a time representation in C++ code*
*
@@ -93,12 +58,9 @@ fun Time.toCode() = TimeValue(this.interval.toLong(), this.unit).toCode()
* @param outerContext A flag indicating whether to generate code for the scope of the outer reactor class.
* This should be set to false if called from code generators for the inner class.
*/
-fun Value.toTime(outerContext: Boolean = false): String = when {
- this.time != null -> this.time.toCode()
- this.isZero -> TimeValue(0, TimeUnit.NONE).toCode()
- outerContext && this.parameter != null -> "__lf_inner.${parameter.name}"
- else -> this.toText()
-}
+fun Value.toTime(outerContext: Boolean = false): String =
+ if (outerContext && this.parameter != null) "__lf_inner.${parameter.name}"
+ else CppTypes.getTargetExpr(this, InferredType.time())
/**
* Get textual representation of a value in C++ code
@@ -106,7 +68,7 @@ fun Value.toTime(outerContext: Boolean = false): String = when {
* If the value evaluates to 0, it is interpreted as a normal value.
* FIXME this is redundant to GeneratorBase.getTargetValue
*/
-fun Value.toCode(): String = this.time?.toCode() ?: this.toText()
+fun Value.toCode(): String = CppTypes.getTargetExpr(this, null)
/** Get the textual representation of a width in C++ code */
fun WidthSpec.toCode(): String = terms.joinToString(" + ") {
@@ -152,7 +114,7 @@ val TriggerRef.name: String
this is VarRef -> this.name
this.isShutdown -> LfPackage.Literals.TRIGGER_REF__SHUTDOWN.name
this.isStartup -> LfPackage.Literals.TRIGGER_REF__STARTUP.name
- else -> throw AssertionError()
+ else -> unreachable()
}
/** Return a comment to be inserted at the top of generated files. */
@@ -165,33 +127,5 @@ fun fileComment(r: Resource) = """
*/
""".trimIndent()
-/* *************************************************************************************************
- * FIXME The following extensions for handling types are actually defined in `GeneratorBase` and should be overridden by the
- * derived CppGenerator Class. However, this causes a weird design because we would need to pass around a reference
- * to CppGenerator to derive target types from AST nodes... Moreover, it would not be possible to use the convenient extension
- * mechanism. The code below is a workaround, but a more general solution should be found.
- */
-
-val StateVar.targetType get():String = this.inferredType.targetType
-val Port.targetType get():String = this.inferredType.targetType
-val Action.targetType: String
- get() {
- val inferred = this.inferredType
- return if (inferred.isUndefined) "void" else inferred.targetType
- }
-
-private fun fixedSizeListType(baseType: String, size: Int) = "std::array<$baseType, $size>"
-private fun variableSizeListType(baseType: String) = "std::vector<$baseType>"
-
-val InferredType.targetType: String
- get() = when {
- this.isUndefined -> "/* undefined type */"
- this.isTime -> when {
- this.isFixedSizeList -> fixedSizeListType("reactor::Duration", this.listSize)
- this.isVariableSizeList -> variableSizeListType("reactor::Duration")
- else -> "reactor::Duration"
- }
- this.isFixedSizeList -> fixedSizeListType(this.baseType(), this.listSize)
- this.isVariableSizeList -> variableSizeListType(this.baseType())
- else -> this.toText()
- }
+val InferredType.cppType: String
+ get() = CppTypes.getTargetType(this)
diff --git a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt
index d7c73c1b84..13f11f2204 100644
--- a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt
+++ b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt
@@ -32,7 +32,9 @@ import org.eclipse.xtext.generator.IGeneratorContext
import org.lflang.*
import org.lflang.Target
import org.lflang.generator.GeneratorBase
+import org.lflang.generator.TargetTypes
import org.lflang.lf.Action
+import org.lflang.lf.TimeUnit
import org.lflang.lf.VarRef
import org.lflang.scoping.LFGlobalScopeProvider
import java.nio.file.Files
@@ -44,7 +46,8 @@ class CppGenerator(
errorReporter: ErrorReporter,
private val scopeProvider: LFGlobalScopeProvider
) :
- GeneratorBase(cppFileConfig, errorReporter) {
+ GeneratorBase(cppFileConfig, errorReporter),
+ TargetTypes by CppTypes {
companion object {
/** Path to the Cpp lib directory (relative to class path) */
@@ -221,17 +224,49 @@ class CppGenerator(
override fun generateAfterDelaysWithVariableWidth() = false
+ override fun getTarget() = Target.CPP
+}
+
+object CppTypes : TargetTypes {
+
override fun supportsGenerics() = true
override fun getTargetTimeType() = "reactor::Duration"
override fun getTargetTagType() = "reactor::Tag"
- override fun getTargetTagIntervalType() = targetUndefinedType
+ override fun getTargetFixedSizeListType(baseType: String, size: Int) = "std::array<$baseType, $size>"
+ override fun getTargetVariableSizeListType(baseType: String) = "std::vector<$baseType>"
- override fun getTargetFixedSizeListType(baseType: String, size: Int) = TODO()
- override fun getTargetVariableSizeListType(baseType: String) = TODO()
+ override fun getTargetUndefinedType() = "void"
- override fun getTargetUndefinedType() = TODO()
+ override fun getTargetTimeExpression(magnitude: Long, unit: TimeUnit): String =
+ if (magnitude == 0L) "reactor::Duration::zero()"
+ else magnitude.toString() + unit.cppUnit
- override fun getTarget() = Target.CPP
}
+/** Get a C++ representation of a LF unit. */
+val TimeUnit.cppUnit
+ get() = when (this) {
+ TimeUnit.NSEC -> "ns"
+ TimeUnit.NSECS -> "ns"
+ TimeUnit.USEC -> "us"
+ TimeUnit.USECS -> "us"
+ TimeUnit.MSEC -> "ms"
+ TimeUnit.MSECS -> "ms"
+ TimeUnit.SEC -> "s"
+ TimeUnit.SECS -> "s"
+ TimeUnit.SECOND -> "s"
+ TimeUnit.SECONDS -> "s"
+ TimeUnit.MIN -> "min"
+ TimeUnit.MINS -> "min"
+ TimeUnit.MINUTE -> "min"
+ TimeUnit.MINUTES -> "min"
+ TimeUnit.HOUR -> "h"
+ TimeUnit.HOURS -> "h"
+ TimeUnit.DAY -> "d"
+ TimeUnit.DAYS -> "d"
+ TimeUnit.WEEK -> "d*7"
+ TimeUnit.WEEKS -> "d*7"
+ TimeUnit.NONE -> ""
+ else -> ""
+ }
diff --git a/org.lflang/src/org/lflang/generator/cpp/CppMethodGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppMethodGenerator.kt
index 0ad793413c..17aa30032c 100644
--- a/org.lflang/src/org/lflang/generator/cpp/CppMethodGenerator.kt
+++ b/org.lflang/src/org/lflang/generator/cpp/CppMethodGenerator.kt
@@ -34,8 +34,8 @@ import org.lflang.toText
/** A C++ code generator for state variables */
class CppMethodGenerator(private val reactor: Reactor) {
- private val Method.targetType: String get() = if (`return` != null) InferredType.fromAST(`return`).targetType else "void"
- private val MethodArgument.targetType: String get() = InferredType.fromAST(type).targetType
+ private val Method.targetType: String get() = if (`return` != null) InferredType.fromAST(`return`).cppType else "void"
+ private val MethodArgument.targetType: String get() = InferredType.fromAST(type).cppType
private val Method.cppArgs get() = this.arguments.map { "${it.targetType} ${it.name}" }
private val Method.constQualifier get() = if (isConst) " const" else ""
@@ -62,4 +62,4 @@ class CppMethodGenerator(private val reactor: Reactor) {
/** Get all method declarations */
fun generateDeclarations() =
reactor.methods.joinToString("\n", "// methods\n", "\n") { generateDeclaration(it) }
-}
\ No newline at end of file
+}
diff --git a/org.lflang/src/org/lflang/generator/cpp/CppParameterGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppParameterGenerator.kt
index 8611621ecd..35d49b0722 100644
--- a/org.lflang/src/org/lflang/generator/cpp/CppParameterGenerator.kt
+++ b/org.lflang/src/org/lflang/generator/cpp/CppParameterGenerator.kt
@@ -45,7 +45,7 @@ class CppParameterGenerator(private val reactor: Reactor) {
}
/** Type of the parameter in C++ code */
- val Parameter.targetType get():String = this.inferredType.targetType
+ val Parameter.targetType get(): String = this.inferredType.cppType
/** Get the default value of the receiver parameter in C++ code */
val Parameter.defaultValue: String
@@ -70,4 +70,4 @@ class CppParameterGenerator(private val reactor: Reactor) {
reactor.parameters.joinToString("\n", "// parameters\n", "\n") {
", ${it.name}(${it.name})"
}
-}
\ No newline at end of file
+}
diff --git a/org.lflang/src/org/lflang/generator/cpp/CppPortGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppPortGenerator.kt
index d4b51d9e7b..54f40955a4 100644
--- a/org.lflang/src/org/lflang/generator/cpp/CppPortGenerator.kt
+++ b/org.lflang/src/org/lflang/generator/cpp/CppPortGenerator.kt
@@ -24,6 +24,7 @@
package org.lflang.generator.cpp
+import org.lflang.inferredType
import org.lflang.isMultiport
import org.lflang.lf.Input
import org.lflang.lf.Output
@@ -50,10 +51,11 @@ class CppPortGenerator(private val reactor: Reactor) {
else -> throw AssertionError()
}
+ val dataType = inferredType.cppType
return if (isMultiport) {
- "std::vector<$portType<$targetType>>"
+ "std::vector<$portType<$dataType>>"
} else {
- "$portType<$targetType>"
+ "$portType<$dataType>"
}
}
@@ -76,4 +78,4 @@ class CppPortGenerator(private val reactor: Reactor) {
fun generateDeclarations() =
reactor.inputs.joinToString("\n", "// input ports\n", postfix = "\n") { generateDeclaration(it) } +
reactor.outputs.joinToString("\n", "// output ports\n", postfix = "\n") { generateDeclaration(it) }
-}
\ No newline at end of file
+}
diff --git a/org.lflang/src/org/lflang/generator/cpp/CppStateGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppStateGenerator.kt
index 96bf27afb3..26a7329ea8 100644
--- a/org.lflang/src/org/lflang/generator/cpp/CppStateGenerator.kt
+++ b/org.lflang/src/org/lflang/generator/cpp/CppStateGenerator.kt
@@ -24,6 +24,7 @@
package org.lflang.generator.cpp
+import org.lflang.inferredType
import org.lflang.isInitialized
import org.lflang.isOfTimeType
import org.lflang.lf.Reactor
@@ -54,9 +55,9 @@ class CppStateGenerator(private val reactor: Reactor) {
/** Get all state declarations */
fun generateDeclarations() =
- reactor.stateVars.joinToString("\n", "// state variable\n", "\n") { "${it.targetType} ${it.name};" }
+ reactor.stateVars.joinToString("\n", "// state variable\n", "\n") { "${it.inferredType.cppType} ${it.name};" }
/** Get all timer initializers */
fun generateInitializers(): String = reactor.stateVars.filter { it.isInitialized }
.joinToString(separator = "\n", prefix = "// state variables\n") { ", ${generateInitializer(it)}" }
-}
\ No newline at end of file
+}
diff --git a/org.lflang/src/org/lflang/generator/rust/PortEmitter.kt b/org.lflang/src/org/lflang/generator/rust/PortEmitter.kt
new file mode 100644
index 0000000000..77f03ddcfa
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/rust/PortEmitter.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2021, TU Dresden.
+ *
+ * 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.generator.rust
+
+import org.lflang.generator.UnsupportedGeneratorFeatureException
+import org.lflang.getWidth
+import org.lflang.isBank
+import org.lflang.isMultiport
+import org.lflang.lf.*
+import kotlin.math.ceil
+
+/**
+ * Specialized thing to render port connections. This was
+ * copy-pasted from the C++ generator,
+ */
+object PortEmitter {
+
+ /*
+ * todo refactor what can be reused
+ * Maybe with an abstract class, the only changed behavior
+ * between Rust & C++ for now is the syntax of the method
+ * calls & such.
+ *
+ * fixme some pieces of code that this generates without error are actually unsupported by the runtime
+ */
+
+ fun declareConnection(c: Connection): String {
+ val lhsPorts = enumerateAllPortsFromReferences(c.leftPorts)
+ val rhsPorts = enumerateAllPortsFromReferences(c.rightPorts)
+
+ // If the connection is a broadcast connection, then repeat the lhs ports until it is equal
+ // or greater to the number of rhs ports. Otherwise, continue with the unmodified list of lhs
+ // ports
+ val iteratedLhsPorts = if (c.isIterated) {
+ val numIterations = ceil(rhsPorts.size.toDouble() / lhsPorts.size.toDouble()).toInt()
+ (1..numIterations).flatMap { lhsPorts }
+ } else {
+ lhsPorts
+ }
+
+ // bind each pair of lhs and rhs ports individually
+ return (iteratedLhsPorts zip rhsPorts).joinToString("\n") {
+ "__assembler.bind_ports(&mut ${it.first.toCode()}, &mut ${it.second.toCode()})?;"
+ }
+ }
+
+ fun declarePortRef(ref: ChildPortReference): String =
+ with(ref) {
+ val self = "&mut __self.$rustFieldName"
+ val child = "&mut $rustChildName.$rustFieldOnChildName"
+
+ if (isInput) "__assembler.bind_ports($self, $child)?;"
+ else "__assembler.bind_ports($child, $self)?;"
+ }
+
+ /**
+ * Get a list of PortReferences for the given list of variables
+ *
+ * This checks whether the variable refers to a multiport and generated an instance of
+ * PortReferrence for each port instance in the multiport. If the port is containe in a
+ * multiport, the result includes instances PortReference for each pair of bank and multiport
+ * instance.
+ */
+ private fun enumerateAllPortsFromReferences(references: List): List {
+ val ports = mutableListOf()
+
+ for (ref in references) {
+ val container = ref.container
+ val port = ref.variable as Port
+ val bankIndexes =
+ if (container?.isBank == true) (0 until container.widthSpec.getValidWidth())
+ else listOf(null)
+ val portIndices =
+ if (port.isMultiport) (0 until port.widthSpec.getValidWidth())
+ else listOf(null)
+ // calculate the Cartesian product af both index lists defined above
+ // TODO iterate over banks or ports first?
+ val indexPairs = portIndices.flatMap { portIdx -> bankIndexes.map { bankIdx -> portIdx to bankIdx } }
+ ports.addAll(indexPairs.map { PortReference(port, it.first, container, it.second) })
+ }
+ return ports
+ }
+
+ /**
+ * A data class for holding all information that is relevant for reverencing one specific port
+ *
+ * The port could be a member of a bank instance and it could be an instance of a multiport.
+ * Thus, the information in this class includes a bank and port index. If the bank (or port)
+ * index is null, then the referenced port is not part of a bank (or multiport).
+ */
+ private data class PortReference(val port: Port, val portIndex: Int?, val container: Instantiation?, val containerIndex: Int?)
+
+ private fun PortReference.toCode(): String {
+ val port = PortData.from(port)
+ val portRef = if (port.isMultiport) "${port.rustFieldName}[$portIndex]" else port.rustFieldName
+ return if (container != null) {
+ val containerRef = if (container.isBank) "${container.name.escapeRustIdent()}[$containerIndex]" else container.name.escapeRustIdent()
+ "$containerRef.$portRef"
+ } else {
+ "__self.$portRef"
+ }
+ }
+
+ /**
+ * Calculate the width of a multiport.
+ * This reports an error on the receiving port if the width is not given as a literal integer.
+ */
+ private fun WidthSpec.getValidWidth(): Int =
+ getWidth().takeIf { it >= 0 }
+ ?: throw UnsupportedGeneratorFeatureException("Non-literal multiport widths")
+
+}
diff --git a/org.lflang/src/org/lflang/generator/rust/README.md b/org.lflang/src/org/lflang/generator/rust/README.md
new file mode 100644
index 0000000000..9b5132985f
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/rust/README.md
@@ -0,0 +1,11 @@
+### The rust code generator
+
+The runtime is hosted over at https://github.com/lf-lang/reactor-rust
+
+LFC generates Cargo projects that pull in this dependency using the git revision number hardcoded in `rust-runtime-version.txt`. You can override this behavior with the `--external-runtime-path` LFC option.
+
+To develop this package and the runtime in sync, it is recommended to clone the runtime repo and set the environment variable `LOCAL_RUST_REACTOR_RT` to the relevant path in your shell. This allows you
+- to update the Rust runtime version easily with the script `bin/update-rust-runtime.sh`
+- to have your local test runs always link to that local repo instead of using the hardcoded revision. This enables you to e.g. test uncommitted changes to the runtime and also debug them from within CLion.
+
+Note: that variable is not meant to be set in CI, ever.
diff --git a/org.lflang/src/org/lflang/generator/rust/RustEmitter.kt b/org.lflang/src/org/lflang/generator/rust/RustEmitter.kt
new file mode 100644
index 0000000000..ed72ec5d5f
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/rust/RustEmitter.kt
@@ -0,0 +1,640 @@
+/*
+ * Copyright (c) 2021, TU Dresden.
+ *
+ * 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.generator.rust
+
+import org.lflang.*
+import org.lflang.generator.LocationInfo
+import org.lflang.generator.PrependOperator
+import org.lflang.generator.TargetCode
+import org.lflang.generator.locationInfo
+import org.lflang.generator.rust.RustEmitter.generateRustProject
+import java.nio.file.Paths
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+
+/**
+ * Part of the Rust generator that emits the actual Rust code,
+ * including its project structure. Its entry point is
+ * [generateRustProject].
+ */
+object RustEmitter {
+ /** Name of the runtime crate that is in its Cargo.toml.*/
+ private const val runtimeCrateFullName = "reactor_rt"
+
+ /** Qualification prefix to refer to a member of the runtime library crate. */
+ const val rsRuntime = "::$runtimeCrateFullName"
+
+ fun generateRustProject(fileConfig: RustFileConfig, gen: GenerationInfo) {
+
+ fileConfig.emit("Cargo.toml") { makeCargoTomlFile(gen) }
+
+ // if singleFile, this file will contain every module.
+ fileConfig.emit("src/main.rs") { makeMainFile(gen) }
+
+ if (!gen.properties.singleFile) {
+ fileConfig.emit("src/reactors/mod.rs") { makeReactorsAggregateModule(gen) }
+ for (reactor in gen.reactors) {
+ fileConfig.emit("src/reactors/${reactor.names.modFileName}.rs") {
+ makeReactorModule(this, reactor)
+ }
+ }
+ }
+ }
+
+ private fun makeReactorModule(out: Emitter, reactor: ReactorInfo) {
+ out += with(reactor) {
+ val typeParams = typeParamList.map { it.targetCode }.angle()
+ val typeArgs = typeParamList.map { it.lfName }.angle()
+
+ with(reactor.names) {
+ with(PrependOperator) {
+ """
+ |${generatedByComment("//")}
+ |#![allow(unused)]
+ |
+ |use $rsRuntime::prelude::*;
+ |
+${" |"..reactor.preambles.joinToString("\n\n") { "// preamble {=\n${it.trimIndent()}\n// =}" }}
+ |
+ |/// Generated from ${loc.display()}
+ |///
+ |/${loc.lfTextComment()}
+ |pub struct $structName$typeParams {
+ | pub __phantom: std::marker::PhantomData<(${typeParamList.map { it.lfName }.joinWithCommas()})>,
+${" | "..reactor.stateVars.joinWithCommasLn { it.lfName + ": " + it.type }}
+ |}
+ |
+ |#[warn(unused)]
+ |impl$typeParams $structName$typeArgs {
+ |
+${" | "..reactions.joinToString("\n\n") { it.toWorkerFunction() }}
+ |
+ |}
+ |
+ |/// Parameters for the construction of a [$structName]
+ |pub struct ${names.paramStructName}$typeParams {
+ | pub __phantom: std::marker::PhantomData<(${typeParamList.map { it.lfName }.joinWithCommas()})>,
+${" | "..ctorParams.joinWithCommasLn { "pub ${it.lfName.escapeRustIdent()}: ${it.type}" }}
+ |}
+ |
+ |
+ |//------------------------//
+ |
+ |
+ |pub struct $wrapperName$typeParams {
+ | __id: $rsRuntime::ReactorId,
+ | __impl: $structName$typeArgs,
+${" | "..otherComponents.joinWithCommasLn { it.toStructField() }}
+ |}
+ |
+ |impl$typeParams $wrapperName$typeArgs {
+ | #[inline]
+ | fn user_assemble(__assembler: &mut $rsRuntime::AssemblyCtx, __params: $paramStructName$typeArgs) -> Self {
+ | let $ctorParamsDeconstructor = __params;
+ | Self {
+ | __id: __assembler.get_id(),
+ | __impl: $structName {
+ | __phantom: std::marker::PhantomData,
+${" | "..reactor.stateVars.joinWithCommasLn { it.lfName + ": " + (it.init ?: "Default::default()") }}
+ | },
+${" | "..otherComponents.joinWithCommasLn { it.rustFieldName + ": " + it.initialExpression() }}
+ | }
+ | }
+ |}
+ |
+ |use $rsRuntime::*; // after this point there's no user-written code
+ |
+ |impl$typeParams $rsRuntime::ReactorInitializer for $wrapperName$typeArgs {
+ | type Wrapped = $structName$typeArgs;
+ | type Params = $paramStructName$typeArgs;
+ | const MAX_REACTION_ID: LocalReactionId = LocalReactionId::new_const($totalNumReactions - 1);
+ |
+ | fn assemble(__params: Self::Params, __assembler: &mut AssemblyCtx) -> Result {
+ | // children reactors
+${" | "..assembleChildReactors()}
+ |
+ | __assembler.fix_cur_id();
+ |
+ | // assemble self
+ | let mut __self: Self = Self::user_assemble(__assembler, __params);
+ |
+${" | "..declareReactions()}
+ |
+ | {
+ |
+${" | "..graphDependencyDeclarations()}
+ |
+${" | "..declareChildConnections()}
+ | }
+${" | "..nestedInstances.joinToString("\n") { "__assembler.register_reactor(${it.rustLocalName});" }}
+ |
+ | Ok(__self)
+ | }
+ |}
+ |
+ |
+ |impl$typeParams ReactorBehavior for $wrapperName$typeArgs {
+ |
+ | #[inline]
+ | fn id(&self) -> ReactorId {
+ | self.__id
+ | }
+ |
+ | fn react_erased(&mut self, ctx: &mut ReactionCtx, rid: LocalReactionId) {
+ | match rid.raw() {
+${" | "..workerFunctionCalls()}
+${" | "..syntheticTimerReactions()}
+ | _ => panic!("Invalid reaction ID: {} should be < {}", rid, Self::MAX_REACTION_ID)
+ | }
+ | }
+ |
+ | fn cleanup_tag(&mut self, ctx: &CleanupCtx) {
+${" | "..otherComponents.mapNotNull { it.cleanupAction() }.joinLn()}
+ | }
+ |
+ |}
+ """.trimMargin()
+ }
+ }
+ }
+ }
+
+ private fun ReactorInfo.assembleChildReactors(): String {
+ fun NestedReactorInstance.paramStruct(): String =
+ args.entries.joinWithCommas("super::${names.paramStructName} { __phantom: std::marker::PhantomData, ", " }") {
+ if (it.key == it.value) it.key.escapeRustIdent()
+ else it.key.escapeRustIdent() + ": " + it.value // do not escape value
+ }
+
+ val asTuple = nestedInstances.joinWithCommas("($ctorParamsDeconstructor, ", ")") { it.rustLocalName }
+ val asMutTuple = nestedInstances.joinWithCommas("(__params, ", ")") { "mut ${it.rustLocalName}" }
+
+ val declarations = nestedInstances.joinToString("\n") {
+ """
+ ${it.loc.lfTextComment()}
+ let ${it.rustLocalName}: super::${it.names.wrapperName}${it.typeArgs.angle()} = __assembler.assemble_sub("${it.lfName}", ${it.paramStruct()})?;
+ """.trimIndent()
+ }
+
+ // we do this to only bring the arguments in scope
+ // within the block
+ return with(PrependOperator) { """
+ |let $asMutTuple = {
+ | let $ctorParamsDeconstructor = __params;
+${" | "..declarations}
+ | $asTuple
+ |};
+ """.trimMargin()
+ }
+ }
+
+
+ private fun ReactorInfo.declareChildConnections(): String {
+ return connections.joinToString("\n", prefix = "// Declare connections\n") {
+ it.locationInfo().lfTextComment() + "\n" +
+ PortEmitter.declareConnection(it)
+ } + "\n" + portReferences.joinToString("\n", prefix = "// Declare port references\n") {
+ PortEmitter.declarePortRef(it)
+ }
+ }
+
+ /** Declare reaction IDs in the assemble routine. */
+ private fun ReactorInfo.declareReactions(): String {
+ val reactionIds = reactions.map { it.invokerId } +
+ timers.map { it.rescheduleReactionId } +
+ timers.map { it.startReactionId }
+
+ val debugLabels = reactions.map { it.debugLabel.toRustOption() } +
+ timers.map { "Some(\"reschedule_${it.lfName}\")" } +
+ timers.map { "Some(\"bootstrap_${it.lfName}\")" }
+
+
+ val pattern = reactionIds.joinToString(prefix = "let [", separator = ",\n ", postfix = "]")
+ val debugLabelArray = debugLabels.joinToString(", ", "[", "]")
+
+ return "$pattern = __assembler.new_reactions(${reactions.size}, $debugLabelArray);"
+ }
+
+ /** Renders calls to worker functions from within the react_erased function. */
+ private fun ReactorInfo.workerFunctionCalls(): TargetCode {
+
+ fun joinDependencies(reaction: ReactionInfo): String = sequence {
+ for ((kind, deps) in reaction.allDependencies) {
+ for (comp in deps) {
+ if (comp.isNotInjectedInReaction(kind, reaction)) continue
+
+ val borrow = comp.toBorrow(kind)
+ this.yield(borrow)
+ }
+ }
+ }.toList().let {
+ if (it.isEmpty()) ""
+ else it.joinWithCommas(prefix = ", ")
+ }
+
+ return reactions.joinWithCommasLn(trailing = true) { n: ReactionInfo ->
+ "${n.idx} => self.__impl.${n.workerId}(ctx${joinDependencies(n)})"
+ }
+ }
+
+ /**
+ * Number of reactions, including synthetic reactions.
+ * Timers each have a reschedule and a bootstrap reaction.
+ */
+ private val ReactorInfo.totalNumReactions
+ get() = 1 + reactions.size + 2 * timers.size
+
+ /** Renders the branches corresponding to synthetic timer reactions in react_erased. */
+ private fun ReactorInfo.syntheticTimerReactions(): String {
+ fun ReactorInfo.timerReactionId(timer: TimerData, synthesisNum: Int) =
+ reactions.size +
+ timers.indexOf(timer).also { assert(it != -1) } +
+ synthesisNum * timers.size // offset it by a block
+
+ val branches = timers.map {
+ "${timerReactionId(it, 0)} => ctx.reschedule_timer(&mut self.${it.rustFieldName})"
+ } + timers.map {
+ "${timerReactionId(it, 1)} => ctx.bootstrap_timer(&mut self.${it.rustFieldName})"
+ }
+ return branches.joinWithCommasLn(trailing = true)
+ }
+
+ /** Build the dependency graph using the assembler. */
+ private fun ReactorInfo.graphDependencyDeclarations(): String {
+ val reactions = reactions.map { n ->
+ val deps: List = mutableListOf().apply {
+ this += n.triggers.map { trigger -> "__assembler.declare_triggers(__self.${trigger.rustFieldName}.get_id(), ${n.invokerId})?;" }
+ if (n.isStartup)
+ this += "__assembler.declare_triggers($rsRuntime::TriggerId::STARTUP, ${n.invokerId})?;"
+ if (n.isShutdown)
+ this += "__assembler.declare_triggers($rsRuntime::TriggerId::SHUTDOWN, ${n.invokerId})?;"
+ this += n.uses.map { trigger -> "__assembler.declare_uses(${n.invokerId}, __self.${trigger.rustFieldName}.get_id())?;" }
+ this += n.effects.filterIsInstance()
+ .map { port -> "__assembler.effects_port(${n.invokerId}, &__self.${port.rustFieldName})?;" }
+ }
+
+ n.loc.lfTextComment() + "\n" + deps.joinLn()
+ }.joinLn()
+
+ val timers = timers.flatMap {
+ listOf(
+ "__assembler.declare_triggers(__self.${it.rustFieldName}.get_id(), ${it.rescheduleReactionId})?;",
+ // start reactions may "trigger" the timer, otherwise it schedules it
+ "__assembler.declare_triggers($rsRuntime::TriggerId::STARTUP, ${it.startReactionId})?;",
+ "__assembler.effects_instantaneous(${it.startReactionId}, __self.${it.rustFieldName}.get_id())?;",
+ )
+ }.joinLn()
+
+ return (reactions + "\n\n" + timers).trimEnd()
+ }
+
+ /** Name of the reschedule reaction for this timer. */
+ private val TimerData.rescheduleReactionId: String
+ get() = "__timer_schedule_$lfName"
+
+ /** Name of the bootstrap reaction for this timer. */
+ private val TimerData.startReactionId: String
+ get() = "__timer_start_$lfName"
+
+ private fun Emitter.makeMainFile(gen: GenerationInfo) {
+ val mainReactor = gen.mainReactor
+ val mainReactorNames = mainReactor.names
+ this += with(PrependOperator) {
+ """
+ |${generatedByComment("//")}
+ |#![allow(unused_imports)]
+ |#![allow(non_snake_case)]
+ |
+ |#[macro_use]
+ |extern crate $runtimeCrateFullName;
+ |#[macro_use]
+ |extern crate assert_matches;
+ |extern crate env_logger;
+ |
+ |use $rsRuntime::*;
+ |use self::reactors::${mainReactorNames.wrapperName} as _MainReactor;
+ |use self::reactors::${mainReactorNames.paramStructName} as _MainParams;
+ |
+ |fn main() {
+ | init_logger();
+ |
+ | // todo CLI parsing
+ | let options = SchedulerOptions {
+ | timeout: ${gen.properties.timeout.toRustOption()},
+ | keep_alive: ${gen.properties.keepAlive}
+ | };
+ | // todo main params are entirely defaulted for now.
+ | let main_args = _MainParams {
+ | __phantom: std::marker::PhantomData,
+${" | "..mainReactor.ctorParams.joinWithCommasLn { it.lfName.escapeRustIdent() + ":" + (it.defaultValue ?: "Default::default()") }}
+ | };
+ |
+ | SyncScheduler::run_main::<_MainReactor>(options, main_args);
+ |}
+ |
+ |fn init_logger() {
+ | env_logger::Builder::from_env(env_logger::Env::default())
+ | .format_target(false)
+ | .init();
+ |}
+ |
+ """.trimMargin()
+ }
+
+ skipLines(2)
+
+ if (gen.properties.singleFile) {
+ makeSingleFileProject(gen)
+ } else {
+ this += "mod reactors;\n"
+ }
+ }
+
+ private fun Emitter.makeSingleFileProject(gen: GenerationInfo) {
+ this += """
+ |//-------------------//
+ |//---- REACTORS -----//
+ |//-------------------//
+ |
+ """.trimMargin()
+
+ this.writeInBlock("mod reactors {") {
+ for (reactor in gen.reactors) {
+ this += with(reactor.names) {
+ """
+ pub use self::$modName::$wrapperName;
+ pub use self::$modName::$paramStructName;
+ """.trimIndent()
+ }
+ skipLines(1)
+ }
+
+ for (reactor in gen.reactors) {
+ this += """
+ |//--------------------------------------------//
+ |//------------ ${reactor.lfName} -------//
+ |//-------------------//
+ """.trimMargin()
+
+ this.writeInBlock("mod ${reactor.names.modName} {") {
+ makeReactorModule(this, reactor)
+ }
+ this.skipLines(2)
+ }
+ }
+ }
+
+
+ private fun Emitter.makeReactorsAggregateModule(gen: GenerationInfo) {
+ fun ReactorInfo.modDecl(): String = with(names) {
+ // We make some declarations public to be able to refer to them
+ // simply when building nested reactors.
+ """
+ mod $modName;
+ pub use self::$modName::$wrapperName;
+ pub use self::$modName::$paramStructName;
+ """.trimIndent()
+ }
+
+ this += with(PrependOperator) {
+ """
+ |${generatedByComment("//")}
+ |
+${" |"..gen.reactors.joinToString("\n") { it.modDecl() }}
+ |
+ """.trimMargin()
+ }
+ }
+
+ private fun Emitter.makeCargoTomlFile(gen: GenerationInfo) {
+ val (crate) = gen
+ this += """
+ |${generatedByComment("#")}
+ |[package]
+ |name = "${crate.name}"
+ |version = "${crate.version}"
+ |authors = [${crate.authors.joinToString(", ") { it.withDQuotes() }}]
+ |edition = "2018"
+ |
+ |[dependencies]
+ |env_logger = "0.9"
+ |assert_matches = "1.5.0" # useful for tests
+ |
+ |[dependencies.$runtimeCrateFullName] # the reactor runtime
+ |${gen.runtime.runtimeCrateSpec()}
+ |
+ |[[bin]]
+ |name = "${gen.executableName}"
+ |path = "src/main.rs"
+ |
+ """.trimMargin()
+ }
+
+
+ private fun RuntimeInfo.runtimeCrateSpec(): String =
+ buildString {
+ if (localPath != null) {
+ val localPath = Paths.get(localPath).toAbsolutePath().toString().escapeStringLiteral()
+
+ appendLine("path = \"$localPath\"")
+ } else {
+ appendLine("git = \"https://github.com/lf-lang/reactor-rust.git\"")
+ if (version != null) {
+ // Version just checks out a relevant tag, while we're not published to crates.io
+ // Maybe we'll never need to be published.
+ appendLine("tag=\"v$version\"")
+ } else {
+ appendLine("rev=\"$gitRevision\"")
+ }
+ }
+ }
+
+ /** Rust pattern that deconstructs a ctor param tuple into individual variables */
+ private val ReactorInfo.ctorParamsDeconstructor: TargetCode
+ get() {
+ val fields = ctorParams.joinWithCommas { it.lfName.escapeRustIdent() }
+ return "${names.paramStructName} { __phantom, $fields }"
+ }
+
+ /** Renders the field of the reactor struct that will contain this component. */
+ private fun ReactorComponent.toStructField(): TargetCode {
+ val fieldVisibility = if (this is PortData) "pub " else ""
+ return "$fieldVisibility$rustFieldName: ${toType()}"
+ }
+
+ /** The owned type of this reactor component (type of the struct field). */
+ private fun ReactorComponent.toType(): TargetCode = when (this) {
+ is ActionData ->
+ if (isLogical) "$rsRuntime::LogicalAction<${dataType ?: "()"}>"
+ else "$rsRuntime::PhysicalActionRef<${dataType ?: "()"}>"
+ is PortData -> "$rsRuntime::Port<$dataType>"
+ is ChildPortReference -> "$rsRuntime::Port<$dataType>"
+ is TimerData -> "$rsRuntime::Timer"
+ }
+
+ /** Initial expression for the field. */
+ private fun ReactorComponent.initialExpression(): TargetCode = when (this) {
+ is ActionData -> {
+ val delay = minDelay.toRustOption()
+ val ctorName = if (isLogical) "new_logical_action" else "new_physical_action"
+ "__assembler.$ctorName(\"$lfName\", $delay)"
+ }
+ is TimerData -> "__assembler.new_timer(\"$lfName\", $offset, $period)"
+ is PortData -> "__assembler.new_port(\"$lfName\", $isInput)"
+ is ChildPortReference -> "__assembler.new_port(\"$childName.$lfName\", $isInput)"
+ }
+
+ /** The type of the parameter injected into a reaction for the given dependency. */
+ private fun ReactorComponent.toBorrowedType(kind: DepKind): TargetCode {
+ fun portRefWrapper(dataType: TargetCode) =
+ if (kind == DepKind.Effects) "$rsRuntime::WritablePort<$dataType>" // note: owned
+ else "&$rsRuntime::ReadablePort<$dataType>" // note: a reference
+
+ return when (this) {
+ is PortData -> portRefWrapper(dataType)
+ is ChildPortReference -> portRefWrapper(dataType)
+ is TimerData -> "&${toType()}"
+ is ActionData -> if (isLogical && kind == DepKind.Effects) "&mut ${toType()}" else "&${toType()}"
+ }
+ }
+
+ /**
+ * Produce an instance of the borrowed type ([toBorrowedType]) to inject
+ * into a reaction. This conceptually just borrows the field.
+ */
+ private fun ReactorComponent.toBorrow(kind: DepKind): TargetCode = when (this) {
+ is PortData, is ChildPortReference ->
+ if (kind == DepKind.Effects) "$rsRuntime::WritablePort::new(&mut self.$rustFieldName)"
+ else "&$rsRuntime::ReadablePort::new(&self.$rustFieldName)"
+ is ActionData -> if (kind == DepKind.Effects) "&mut self.$rustFieldName" else "&self.$rustFieldName"
+ is TimerData -> "&self.$rustFieldName"
+ }
+
+ private fun ReactorComponent.isNotInjectedInReaction(depKind: DepKind, n: ReactionInfo): Boolean =
+ // Item is both in inputs and outputs.
+ // We must not generate 2 parameters, instead we generate the
+ // one with the most permissions (Effects means &mut).
+
+ // eg `reaction(act) -> act` must not generate 2 parameters for act,
+ // we skip the Trigger one and generate the Effects one.
+ depKind != DepKind.Effects && this in n.effects
+
+ private fun ReactorComponent.isInjectedAsMut(depKind: DepKind): Boolean =
+ depKind == DepKind.Effects && (this is PortData || this is ActionData)
+
+ /**
+ * Whether this component may be unused in a reaction.
+ * Eg. actions on which we have just a trigger dependency
+ * are fine to ignore.
+ */
+ private fun ReactorComponent.mayBeUnusedInReaction(depKind: DepKind): Boolean =
+ depKind == DepKind.Triggers && this !is PortData
+
+
+ /** Action that must be taken to cleanup this component within the `cleanup_tag` procedure. */
+ private fun ReactorComponent.cleanupAction(): TargetCode? = when (this) {
+ is ActionData ->
+ if (isLogical) "ctx.cleanup_logical_action(&mut self.$rustFieldName);"
+ else "ctx.cleanup_physical_action(&mut self.$rustFieldName);"
+ is PortData, is ChildPortReference -> "ctx.cleanup_port(&mut self.$rustFieldName);"
+ is TimerData -> null
+ }
+
+ /** Renders the definition of the worker function generated for this reaction. */
+ private fun ReactionInfo.toWorkerFunction(): String {
+ fun ReactionInfo.reactionParams(): List = sequence {
+ for ((kind, comps) in allDependencies) {
+ for (comp in comps) {
+ if (comp.isNotInjectedInReaction(kind, this@reactionParams)) continue
+
+ // we want the user to be able to make
+ // use of the mut if they want, but they
+ // don't have to
+ val mut = if (comp.isInjectedAsMut(kind)) "#[allow(unused_mut)] mut " else ""
+
+ val param = "$mut${comp.rustRefName}: ${comp.toBorrowedType(kind)}"
+
+ if (comp.mayBeUnusedInReaction(kind)) {
+ yield("#[allow(unused)] $param")
+ } else {
+ yield(param)
+ }
+ }
+ }
+ }.toList()
+
+ val indent = " ".repeat("fn $workerId(".length)
+ return with(PrependOperator) {
+ """
+ |${loc.lfTextComment()}
+ |fn $workerId(&mut self,
+ |$indent#[allow(unused)] ctx: &mut $rsRuntime::ReactionCtx,
+${" |$indent"..reactionParams().joinWithCommasLn { it }}) {
+${" | "..body}
+ |}
+ """.trimMargin()
+ }
+ }
+
+
+
+}
+
+/**
+ * Produce a commented out version of the text of this AST node.
+ * This is helpful to figure out how the rust code corresponds to
+ * the LF code.
+ */
+private fun LocationInfo.lfTextComment() =
+ "// --- ${lfText.joinLines()}"
+
+private val timeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")
+
+/** Header comment for generated files. */
+private fun generatedByComment(delim: String) =
+ "$delim-- Generated by LFC @ ${timeFormatter.format(LocalDateTime.now())} --$delim"
+
+/** Convert a nullable value to a rust option. */
+private fun TargetCode?.toRustOption(): TargetCode =
+ if (this == null) "None"
+ else "Some($this)"
+
+/**
+ * If this is a rust keyword, escape it for it to be interpreted
+ * as an identifier, except if this keyword is a valid rust expression.
+ */
+fun String.escapeRustIdent() = RustTypes.escapeIdentifier(this)
+
+
+fun String.escapeStringLiteral() =
+ replace(Regex("[\\\\ \t]")) {
+ when (it.value) {
+ "\\" -> "\\\\"
+ "\t" -> "\\t"
+ "\"" -> "\\\""
+ else -> it.value
+ }
+ }
diff --git a/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt b/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt
new file mode 100644
index 0000000000..10968bcb1a
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2021, TU Dresden.
+ *
+ * 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.generator.rust
+
+import org.eclipse.emf.ecore.resource.Resource
+import org.eclipse.xtext.generator.IFileSystemAccess2
+import org.eclipse.xtext.generator.IGeneratorContext
+import org.lflang.FileConfig
+import java.io.Closeable
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.system.measureTimeMillis
+
+class RustFileConfig(resource: Resource, fsa: IFileSystemAccess2, context: IGeneratorContext) :
+ FileConfig(resource, fsa, context) {
+
+ /**
+ * Clean any artifacts produced by the C++ code generator.
+ */
+ @Throws(IOException::class)
+ override fun doClean() {
+ super.doClean()
+ deleteDirectory(outPath.resolve("target"))
+ }
+
+ inline fun emit(p: Path, f: Emitter.() -> Unit) {
+ //System.err.println("Generating file ${srcGenPath.relativize(p)}...")
+ //val milliTime =
+ measureTimeMillis {
+ Emitter(p).use { it.f() }
+ }
+ //System.err.println("Done in $milliTime ms.")
+ }
+
+ inline fun emit(pathRelativeToOutDir: String, f: Emitter.() -> Unit): Unit = emit(srcGenPath.resolve(pathRelativeToOutDir), f)
+
+}
+
+/**
+ * Builds the contents of a file. This is used with RAII, closing
+ * the object writes to the file.
+ */
+class Emitter(
+ /** File to which this emitter should write. */
+ private val output: Path,
+) : Closeable {
+
+ /** Accumulates the result. */
+ private val sb = StringBuilder()
+ private var indent: String = ""
+
+ /**
+ * Add the given string, which is taken as an entire line.
+ * Indent is replaced with the contextual value.
+ */
+ operator fun plusAssign(line: String) {
+ sb.append(line.replaceIndent(indent))
+ }
+
+ /**
+ * Write the contents in an indented block
+ */
+ fun writeInBlock(header: String = "{", footer: String = "}", contents: Emitter.() -> Unit) {
+ skipLines(1)
+ sb.append(indent).append(header).appendLine()
+ indent += " "
+ this.contents()
+ sb.appendLine()
+ indent = indent.removeSuffix(" ")
+ sb.append(indent).append(footer).appendLine()
+ }
+
+ /**
+ * Skip lines, the difference with just using [plusAssign]
+ * is that no trailing whitespace is added.
+ */
+ fun skipLines(numLines: Int) {
+ repeat(numLines) {
+ sb.appendLine()
+ }
+ }
+
+ override fun close() {
+ Files.createDirectories(output.parent)
+ Files.writeString(output, sb)
+ }
+}
+
diff --git a/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt b/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt
new file mode 100644
index 0000000000..7cbb71e9ea
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2021, TU Dresden.
+ *
+ * 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.generator.rust
+
+import org.eclipse.emf.ecore.resource.Resource
+import org.eclipse.xtext.generator.IFileSystemAccess2
+import org.eclipse.xtext.generator.IGeneratorContext
+import org.lflang.ErrorReporter
+import org.lflang.Target
+import org.lflang.generator.GeneratorBase
+import org.lflang.generator.TargetTypes
+import org.lflang.joinWithCommas
+import org.lflang.lf.*
+import org.lflang.scoping.LFGlobalScopeProvider
+import java.nio.file.Files
+
+/**
+ * Generator for the Rust target language. The generation is
+ * organized around a sort of intermediate representation
+ * that is constructed from the AST in a first step, then
+ * handed off to the [RustEmitter]. This makes the emitter
+ * simpler, as it takes fewer design decisions. This is also
+ * meant to make it easier to port the emitter to a bunch of
+ * templates written in a template language like Velocity.
+ *
+ * See [GenerationInfo] for the main model class.
+ *
+ * @author Clément Fournier
+ */
+@Suppress("unused")
+class RustGenerator(
+ fileConfig: RustFileConfig,
+ errorReporter: ErrorReporter,
+ @Suppress("UNUSED_PARAMETER") unused: LFGlobalScopeProvider
+) : GeneratorBase(fileConfig, errorReporter),
+ TargetTypes by RustTypes {
+
+ override fun doGenerate(resource: Resource, fsa: IFileSystemAccess2, context: IGeneratorContext) {
+ super.doGenerate(resource, fsa, context)
+
+ // stop if there are any errors found in the program by doGenerate() in GeneratorBase
+ if (errorsOccurred()) return
+
+ // abort if there is no main reactor
+ if (mainDef == null) {
+ println("WARNING: The given Lingua Franca program does not define a main reactor. Therefore, no code was generated.")
+ return
+ }
+
+ val fileConfig = fileConfig as RustFileConfig
+
+ Files.createDirectories(fileConfig.srcGenPath)
+
+ val gen = RustModelBuilder.makeGenerationInfo(targetConfig, reactors)
+ RustEmitter.generateRustProject(fileConfig, gen)
+
+ if (targetConfig.noCompile || errorsOccurred()) {
+ println("Exiting before invoking target compiler.")
+ } else {
+ invokeRustCompiler()
+ }
+ }
+
+ private fun invokeRustCompiler() {
+ val args = mutableListOf().apply {
+ this += listOf(
+ "+nightly",
+ "build",
+ "--release", // enable optimisations
+ // note that this option is unstable for now and requires rust nightly ...
+ "--out-dir", fileConfig.binPath.toAbsolutePath().toString(),
+ "-Z", "unstable-options", // ... and that feature flag
+ )
+
+ if (targetConfig.cargoFeatures.isNotEmpty()) {
+ this += "--features"
+ this += targetConfig.cargoFeatures.joinWithCommas()
+ }
+
+ this += targetConfig.compilerFlags
+ }
+
+ val cargoCommand = commandFactory.createCommand(
+ "cargo", args,
+ fileConfig.srcGenPath.toAbsolutePath()
+ ) ?: return
+
+ val cargoReturnCode = cargoCommand.run()
+
+ if (cargoReturnCode == 0) {
+ println("SUCCESS (compiling generated Rust code)")
+ println("Generated source code is in ${fileConfig.srcGenPath}")
+ println("Compiled binary is in ${fileConfig.binPath}")
+ } else {
+ errorReporter.reportError("cargo failed with error code $cargoReturnCode")
+ }
+ }
+
+
+ override fun getTarget(): Target = Target.Rust
+
+
+ override fun generateDelayBody(action: Action, port: VarRef): String {
+ TODO("Not yet implemented")
+ }
+
+ override fun generateForwardBody(action: Action, port: VarRef): String {
+ TODO("Not yet implemented")
+ }
+
+ override fun generateDelayGeneric(): String {
+ TODO("Not yet implemented")
+ }
+
+}
+
diff --git a/org.lflang/src/org/lflang/generator/rust/RustModel.kt b/org.lflang/src/org/lflang/generator/rust/RustModel.kt
new file mode 100644
index 0000000000..4f8df42b54
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/rust/RustModel.kt
@@ -0,0 +1,591 @@
+/*
+ * Copyright (c) 2021, TU Dresden.
+
+ * 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.generator.rust
+
+import org.lflang.*
+import org.lflang.generator.*
+import org.lflang.lf.*
+import org.lflang.lf.Timer
+import java.util.*
+
+private typealias Ident = String
+
+/** Root model class for the entire generation. */
+data class GenerationInfo(
+ val crate: CrateInfo,
+ val runtime: RuntimeInfo,
+ val reactors: List,
+ val mainReactor: ReactorInfo, // it's also in the list
+ val executableName: Ident,
+ val properties: RustTargetProperties
+) {
+
+ private val byId = reactors.associateBy { it.globalId }
+
+ fun getReactor(id: ReactorId): ReactorInfo =
+ byId[id] ?: throw IllegalArgumentException("No such recorded ID: $id, I know ${byId.keys}")
+}
+
+data class RustTargetProperties(
+ val keepAlive: Boolean = false,
+ val timeout: TargetCode? = null,
+ val singleFile: Boolean = false
+)
+
+/**
+ * Model class for a reactor class. This will be emitted as
+ * several structs in a Rust module.
+ */
+data class ReactorInfo(
+ /** Name of the reactor in LF. By LF conventions, this is a PascalCase identifier. */
+ val lfName: Ident,
+ /** Global ID. */
+ val globalId: ReactorId,
+ /** Whether this is the main reactor. */
+ val isMain: Boolean,
+ /** A list of reactions, in order of their [ReactionInfo.idx]. */
+ val reactions: List,
+ /** List of state variables. */
+ val stateVars: List,
+ /** Other reactor components, like actions, timers, port references, ports. */
+ val otherComponents: Set,
+ /** List of ctor parameters. */
+ val ctorParams: List,
+ /** List of preambles, will be outputted at the top of the file. */
+ val preambles: List,
+
+ val typeParamList: List,
+
+ /**
+ * List of reactor instances that are directly created by this reactor.
+ * Each of them is a named item accessible from the assemble method (well,
+ * their ports are anyway).
+ */
+ val nestedInstances: List,
+
+ /**
+ * List of connections between ports that are made within
+ * the body of this reactor instance.
+ *
+ * todo Connection doesn't have its own model class
+ */
+ val connections: List,
+ val loc: LocationInfo,
+
+ ) {
+ /** Identifiers for the different Rust constructs that this reactor class generates. */
+ val names = ReactorNames(lfName)
+
+ /**
+ * All timers. This is a subset of [otherComponents].
+ */
+ val timers: List = otherComponents.filterIsInstance()
+
+ /**
+ * Port references that are used by reactions of this reactor,
+ * those are synthesized as fields of the current reactor.
+ *
+ * This is a subset of [otherComponents].
+ */
+ val portReferences: Set = otherComponents.filterIsInstance().toSet()
+}
+
+class TypeParamInfo(
+ val targetCode: TargetCode,
+ val lfName: Ident,
+ val loc: LocationInfo
+)
+
+class ReactorNames(
+ /** Name of the reactor in LF. By LF conventions, this is a PascalCase identifier. */
+ lfName: Ident
+) {
+
+ /** Name of the rust module, unescaped.. */
+ val modFileName: Ident = lfName.camelToSnakeCase()
+
+ /** Name of the rust module as it can be referenced in rust code. */
+ val modName: Ident = modFileName.escapeRustIdent()
+
+ /**
+ * Name of the "user struct", which contains state
+ * variables as fields, and which the user manipulates in reactions.
+ */
+ val structName: Ident = lfName.capitalize().escapeRustIdent()
+
+ // Names of other implementation-detailistic structs.
+
+ val paramStructName: Ident = "${structName}Params"
+ val wrapperName: Ident = "${structName}Adapter"
+}
+
+data class NestedReactorInstance(
+ val lfName: Ident,
+ val reactorLfName: String,
+ val args: Map,
+ val loc: LocationInfo,
+ val typeArgs: List
+) {
+ /** Sync with [ChildPortReference.rustChildName]. */
+ val rustLocalName = lfName.escapeRustIdent()
+
+ val names = ReactorNames(reactorLfName)
+}
+
+/**
+ * A reference to a port of a child reactor.
+ * This is mostly relevant to [ReactionInfo.allDependencies]
+ * and such.
+ */
+data class ChildPortReference(
+ /** Name of the child instance. */
+ val childName: Ident,
+ override val lfName: Ident,
+ val isInput: Boolean,
+ val dataType: TargetCode
+) : ReactorComponent() {
+ val rustFieldOnChildName: String = "__$lfName"
+
+ /** Sync with [NestedReactorInstance.rustLocalName]. */
+ val rustChildName: TargetCode = childName.escapeRustIdent()
+}
+
+/**
+ * Model class for the parameters of a reactor constructor.
+ */
+data class CtorParamInfo(
+ val lfName: Ident,
+ val type: TargetCode,
+ val defaultValue: TargetCode?
+)
+
+/** Model class for a state variable. */
+data class StateVarInfo(
+ /**
+ * Identifier of the state var. From within a reaction
+ * body, the state var is accessible as a field with type
+ * [type] declared on the `self` struct.
+ */
+ val lfName: String,
+ /** Rust static type of the struct field. Must be `Sized`. */
+ val type: TargetCode,
+ /**
+ * The field initializer, a Rust expression. If null,
+ * will default to `Default::default()`.
+ */
+ val init: TargetCode?
+)
+
+enum class DepKind { Triggers, Uses, Effects }
+
+/**
+ * Model class for a single reaction.
+ */
+data class ReactionInfo(
+ /** Index in the containing reactor. */
+ val idx: Int,
+ /** Target code for the reaction body. */
+ val body: TargetCode,
+
+ val allDependencies: Map>,
+
+ /** Whether the reaction is triggered by the startup event. */
+ val isStartup: Boolean,
+ /** Whether the reaction is triggered by the shutdown event. */
+ val isShutdown: Boolean,
+
+ /** Location metadata. */
+ val loc: LocationInfo,
+
+ val debugLabel: String?
+) {
+
+ /** Dependencies that trigger the reaction. */
+ val triggers: Set get() = allDependencies[DepKind.Triggers].orEmpty()
+
+ /** Dependencies that the reaction may use, but which do not trigger it. */
+ val uses: Set get() = allDependencies[DepKind.Uses].orEmpty()
+
+ /** Dependencies that the reaction may write to/schedule. */
+ val effects: Set get() = allDependencies[DepKind.Effects].orEmpty()
+
+
+ // those are implementation details
+
+ /** The name of the worker function for this reaction. */
+ val workerId: String = "react_$idx"
+
+ /** The name of the `ReactionInvoker` field for this reaction. */
+ val invokerId: String = "react_$idx"
+}
+
+/**
+ * Metadata about the generated crate. Mostly doesn't matter.
+ */
+data class CrateInfo(
+ /** Name of the crate. According to Rust conventions this should be a snake_case name. */
+ val name: String,
+ /** Version of the crate. */
+ val version: String,
+ /** List of names of the credited authors. */
+ val authors: List,
+)
+
+/**
+ * Info about the runtime crate.
+ */
+data class RuntimeInfo(
+ /** The version string. This must correspond to a tag in the runtime repo. */
+ val version: String?,
+ /** Path to the runtime on the local machine. If this is present, it takes priority over the git repo. */
+ val localPath: String?,
+ /** Revision which we test against. */
+ val gitRevision: String,
+
+ val enabledCargoFeatures: Set
+)
+
+
+sealed class ReactorComponent {
+ /** Simple name of the component in LF. */
+ abstract val lfName: Ident
+
+ /**
+ * Simple name by which the component can be referred to
+ * within target code blocks (usually [lfName]).
+ */
+ val rustRefName: Ident
+ get() =
+ if (this is ChildPortReference) "${childName}__$lfName"
+ else lfName.escapeRustIdent()
+
+ /** Simple name of the field in Rust. */
+ val rustFieldName: Ident
+ get() = when (this) {
+ is TimerData -> "__$lfName"
+ is PortData -> "__$lfName" // sync with ChildPortReference.rustFieldOnChildName
+ is ChildPortReference -> "__${childName}__$lfName"
+ is ActionData -> "__$lfName"
+ }
+
+ companion object {
+
+ private val DEFAULT_TIME_UNIT_IN_TIMER: TimeUnit = TimeUnit.MSEC
+
+ /**
+ * Convert an AST node for a reactor component to the corresponding dependency type.
+ * Since there's no reasonable common supertype we use [Variable], but maybe we should
+ * have another interface.
+ */
+ fun from(v: Variable): ReactorComponent? = when (v) {
+ is Port -> PortData.from(v)
+ is Action -> ActionData(
+ lfName = v.name,
+ isLogical = v.isLogical,
+ dataType = RustTypes.getTargetType(v.type),
+ minDelay = v.minDelay?.time?.let(RustTypes::getTargetTimeExpr)
+ )
+ is Timer -> TimerData(
+ lfName = v.name,
+ offset = v.offset.toTimerTimeValue(),
+ period = v.period.toTimerTimeValue()
+ )
+ else -> throw UnsupportedGeneratorFeatureException("Dependency on ${v.javaClass.simpleName} $v")
+ }
+
+ private fun Value?.toTimerTimeValue(): TargetCode =
+ when {
+ this == null -> "Duration::from_millis(0)"
+ parameter != null -> "${parameter.name}.clone()"
+ literal != null ->
+ literal.toIntOrNull()
+ ?.let { toRustTimeExpr(it.toLong(), DEFAULT_TIME_UNIT_IN_TIMER) }
+ ?: throw InvalidLfSourceException("Not an integer literal", this)
+ time != null -> time.toRustTimeExpr()
+ code != null -> code.toText()
+ else -> RustTypes.getTargetExpr(this, InferredType.time())
+ }
+ }
+}
+
+
+/**
+ * @property dataType A piece of target code
+ */
+data class PortData(
+ override val lfName: Ident,
+ val isInput: Boolean,
+ /** Rust data type of this component */
+ val dataType: TargetCode,
+ val isMultiport: Boolean = false
+) : ReactorComponent() {
+ companion object {
+ fun from(port: Port) =
+ PortData(
+ lfName = port.name,
+ isInput = port.isInput,
+ dataType = RustTypes.getTargetType(port.type)
+ )
+ }
+}
+
+data class ActionData(
+ override val lfName: Ident,
+ // if null, defaults to unit
+ val dataType: TargetCode?,
+ val isLogical: Boolean,
+ val minDelay: TargetCode?,
+) : ReactorComponent()
+
+data class TimerData(
+ override val lfName: Ident,
+ val offset: TargetCode,
+ val period: TargetCode,
+) : ReactorComponent()
+
+private fun TimeValue.toRustTimeExpr(): TargetCode = toRustTimeExpr(time, unit)
+private fun Time.toRustTimeExpr(): TargetCode = toRustTimeExpr(interval.toLong(), unit)
+
+private fun toRustTimeExpr(interval: Long, unit: TimeUnit): TargetCode =
+ RustTypes.getTargetTimeExpression(interval, unit)
+
+/** Regex to match a target code block, captures the insides as $1. */
+private val TARGET_BLOCK_R = Regex("\\{=(.*)=}", RegexOption.DOT_MATCHES_ALL)
+/** Regex to match a simple (C) code block, captures the insides as $1. */
+private val BLOCK_R = Regex("\\{(.*)}", RegexOption.DOT_MATCHES_ALL)
+
+/**
+ * Produce model classes from the AST.
+ */
+object RustModelBuilder {
+
+ private val runtimeGitRevision =
+ javaClass.getResourceAsStream("rust-runtime-version.txt")!!
+ .bufferedReader().readLine().trim()
+
+ /**
+ * Given the input to the generator, produce the model classes.
+ */
+ fun makeGenerationInfo(targetConfig: TargetConfig, reactors: List): GenerationInfo {
+ val reactorsInfos = makeReactorInfos(reactors)
+ // todo how do we pick the main reactor? it seems like super.doGenerate sets that field...
+ val mainReactor = reactorsInfos.lastOrNull { it.isMain } ?: reactorsInfos.last()
+
+
+ return GenerationInfo(
+ crate = CrateInfo(
+ name = mainReactor.lfName.camelToSnakeCase(),
+ version = "1.0.0",
+ authors = listOf(System.getProperty("user.name"))
+ ),
+ runtime = RuntimeInfo(
+ version = targetConfig.runtimeVersion,
+ localPath = targetConfig.externalRuntimePath,
+ gitRevision = runtimeGitRevision,
+ enabledCargoFeatures = targetConfig.cargoFeatures.toSet()
+ ),
+ reactors = reactorsInfos,
+ mainReactor = mainReactor,
+ // Rust exec names are snake case, otherwise we get a cargo warning
+ // https://github.com/rust-lang/rust/issues/45127
+ executableName = mainReactor.lfName.camelToSnakeCase(),
+ properties = targetConfig.toRustProperties()
+ )
+ }
+
+ private fun TargetConfig.toRustProperties(): RustTargetProperties =
+ RustTargetProperties(
+ keepAlive = this.keepalive,
+ timeout = this.timeout?.toRustTimeExpr(),
+ singleFile = this.singleFileProject
+ )
+
+ private fun makeReactorInfos(reactors: List): List =
+ reactors.map { reactor ->
+ val components = mutableMapOf()
+ val allComponents: List = reactor.allComponents()
+ for (component in allComponents) {
+ val irObj = ReactorComponent.from(component) ?: continue
+ components[irObj.lfName] = irObj
+ }
+
+ val reactions = reactor.reactions.map { n: Reaction ->
+ fun makeDeps(depKind: Reaction.() -> List): Set =
+ n.depKind().mapTo(mutableSetOf()) {
+ val variable = it.variable
+ val container = it.container
+ if (container is Instantiation && variable is Port) {
+ val formalType = RustTypes.getTargetType(variable.type)
+ ChildPortReference(
+ childName = container.name,
+ lfName = variable.name,
+ isInput = variable is Input,
+ dataType = container.reactor.instantiateType(formalType, it.container.typeParms),
+ )
+ } else {
+ components[variable.name] ?: throw UnsupportedGeneratorFeatureException("Dependency on $it")
+ }
+ }
+
+ ReactionInfo(
+ idx = n.indexInContainer,
+ allDependencies = EnumMap>(DepKind::class.java).apply {
+ this[DepKind.Triggers] = makeDeps { triggers.filterIsInstance() }
+ this[DepKind.Uses] = makeDeps { sources }
+ this[DepKind.Effects] = makeDeps { effects }
+ },
+ body = n.code.toText(),
+ isStartup = n.triggers.any { it.isStartup },
+ isShutdown = n.triggers.any { it.isShutdown },
+ debugLabel = ASTUtils.label(n),
+ loc = n.locationInfo().let {
+ // remove code block
+ it.copy(lfText = it.lfText.replace(TARGET_BLOCK_R, "{= ... =}"))
+ }
+ )
+ }
+
+ val portReferences =
+ reactions.flatMap { it.allDependencies.values }.flatten()
+ .filterIsInstance().toSet()
+
+ ReactorInfo(
+ lfName = reactor.name,
+ loc = reactor.locationInfo().let {
+ // remove body
+ it.copy(lfText = it.lfText.replace(BLOCK_R, "{ ... }"))
+ },
+ globalId = reactor.globalId,
+ reactions = reactions,
+ otherComponents = components.values.toSet() + portReferences,
+ isMain = reactor.isMain,
+ typeParamList = reactor.typeParms.map {
+ TypeParamInfo(targetCode = it.toText(), it.identifier, it.locationInfo())
+ },
+ preambles = reactor.preambles.map { it.code.toText() },
+ stateVars = reactor.stateVars.map {
+ StateVarInfo(
+ lfName = it.name,
+ type = RustTypes.getTargetType(it.type, it.init),
+ init = RustTypes.getTargetInitializer(it.init, it.type, it.braces.isNotEmpty())
+ )
+ },
+ nestedInstances = reactor.instantiations.map { it.toModel() },
+ connections = reactor.connections,
+ ctorParams = reactor.parameters.map {
+ CtorParamInfo(
+ lfName = it.name,
+ type = RustTypes.getTargetType(it.type, it.init),
+ defaultValue = RustTypes.getTargetInitializer(it.init, it.type, it.braces.isNotEmpty())
+ )
+ }
+ )
+ }
+
+ private fun Instantiation.toModel(): NestedReactorInstance {
+
+ val byName = parameters.associateBy { it.lhs.name }
+ val args = reactor.parameters.associate { ithParam ->
+ // use provided argument
+ val value = byName[ithParam.name]?.let {
+ RustTypes.getTargetInitializer(it.rhs, ithParam.type, it.isInitWithBraces)
+ }
+ ?: ithParam?.let { RustTypes.getTargetInitializer(it.init, it.type, it.isInitWithBraces) }
+ ?: throw InvalidLfSourceException("Cannot find value of parameter ${ithParam.name}", this)
+ ithParam.name to value
+ }
+
+ return NestedReactorInstance(
+ lfName = this.name,
+ args = args,
+ reactorLfName = this.reactorClass.name,
+ loc = this.locationInfo(),
+ typeArgs = typeParms.map { it.toText() }
+ )
+ }
+}
+
+/**
+ * An identifier for a [Reactor]. Used to associate [Reactor]
+ * with model objects and vice versa.
+ */
+class ReactorId(private val id: String) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ other as ReactorId
+ return id == other.id
+ }
+
+ override fun hashCode(): Int = id.hashCode()
+ override fun toString(): String = id
+}
+
+/**
+ * Returns a string that is distinct from the globalId
+ * of any other reactor. Equal reactors yield the same
+ * globalID. This does not need to be stable across runs.
+ */
+val Reactor.globalId: ReactorId
+ get() = ReactorId(this.eResource().toPath().toString() + "/" + this.name + "/" + "/" + this.isMain)
+
+/**
+ * Returns the type of the port with the given name, when
+ * this reactor's type parameters are instantiated with
+ * the given type arguments. For instance, in the following:
+ *
+ * reactor Generic { input port: Vec; }
+ * reactor Container { gen = new Generic(); }
+ *
+ * the type of `gen.port` is `Vec`. If a reaction of
+ * `Container` depends on `gen.port`, it needs to be injected
+ * with the correct type. The correct call to this method
+ * would be `generic.typeOfPort("port", listOf("String"))`.
+ *
+ */
+fun Reactor.instantiateType(formalType: TargetCode, typeArgs: List): TargetCode {
+ val typeParams = typeParms
+ assert(typeArgs.size == typeParams.size)
+
+ return if (typeArgs.isEmpty()) formalType
+ else {
+ val typeArgsByName = typeParams.mapIndexed { i, tp -> Pair(tp.identifier, typeArgs[i].toText()) }.toMap()
+
+ formalType.replace(IDENT_REGEX) {
+ typeArgsByName[it.value] ?: it.value
+ }
+ }
+}
+
+/**
+ * Returns the identifier of this type param.
+ */
+private val TypeParm.identifier: String
+ get() {
+ val targetCode = toText()
+ return IDENT_REGEX.find(targetCode.trimStart())?.value
+ ?: throw InvalidLfSourceException("No identifier in type param `$targetCode`", this)
+ }
diff --git a/org.lflang/src/org/lflang/generator/rust/RustTypes.kt b/org.lflang/src/org/lflang/generator/rust/RustTypes.kt
new file mode 100644
index 0000000000..e4145411a2
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/rust/RustTypes.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2021, TU Dresden.
+ *
+ * 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.generator.rust
+
+import org.lflang.generator.TargetCode
+import org.lflang.generator.TargetTypes
+import org.lflang.lf.TimeUnit
+
+object RustTypes : TargetTypes {
+
+ override fun supportsGenerics(): Boolean = true
+
+ override fun getTargetTimeType(): String = "Duration"
+
+ override fun getTargetTagType(): String = "EventTag"
+
+ override fun getTargetUndefinedType(): String = "()"
+
+ override fun getTargetFixedSizeListType(baseType: String, size: Int): String =
+ "[ $baseType ; $size ]"
+
+ override fun getTargetVariableSizeListType(baseType: String): String =
+ "Vec<$baseType>"
+
+ override fun escapeIdentifier(ident: String): String =
+ if (ident in RustKeywords) "r#$ident"
+ else ident
+
+ override fun getTargetTimeExpression(magnitude: Long, unit: TimeUnit): TargetCode = when (unit) {
+ TimeUnit.NSEC,
+ TimeUnit.NSECS -> "Duration::from_nanos($magnitude)"
+ TimeUnit.USEC,
+ TimeUnit.USECS -> "Duration::from_micros($magnitude)"
+ TimeUnit.MSEC,
+ TimeUnit.MSECS -> "Duration::from_millis($magnitude)"
+ TimeUnit.MIN,
+ TimeUnit.MINS,
+ TimeUnit.MINUTE,
+ TimeUnit.MINUTES -> "Duration::from_secs(${magnitude * 60})"
+ TimeUnit.HOUR, TimeUnit.HOURS -> "Duration::from_secs(${magnitude * 3600})"
+ TimeUnit.DAY, TimeUnit.DAYS -> "Duration::from_secs(${magnitude * 3600 * 24})"
+ TimeUnit.WEEK, TimeUnit.WEEKS -> "Duration::from_secs(${magnitude * 3600 * 24 * 7})"
+ TimeUnit.NONE, // default is the second
+ TimeUnit.SEC, TimeUnit.SECS,
+ TimeUnit.SECOND, TimeUnit.SECONDS -> "Duration::from_secs($magnitude)"
+ }
+
+ override fun getFixedSizeListInitExpression(
+ contents: List,
+ listSize: Int,
+ withBraces: Boolean
+ ): String =
+ contents.joinToString(", ", "[", "]")
+
+ override fun getVariableSizeListInitExpression(contents: List, withBraces: Boolean): String =
+ contents.joinToString(", ", "vec![", "]")
+
+ override fun getMissingExpr(): String =
+ "Default::default()"
+}
+
+val RustKeywords = setOf(
+ // https://doc.rust-lang.org/reference/keywords.html
+ "as", "break", "const", "continue", "crate", "else",
+ "enum", "extern", /*"false",*/ "fn", "for", "if", "impl",
+ "in", "let", "loop", "match", "mod", "move", "mut",
+ "pub", "ref", "return", /*"self",*/ "Self", "static",
+ "struct", "super", "trait", /*"true",*/ "type", "unsafe",
+ "use", "where", "while",
+ // reserved kws
+ "abstract", "async", "await", "dyn", "become", "box",
+ "do", "final", "macro", "override", "priv", "typeof",
+ "unsized", "virtual", "yield", "try",
+ // "weak" keywords, disallow them anyway
+ "union", "dyn"
+)
diff --git a/org.lflang/src/org/lflang/generator/rust/rust-runtime-version.txt b/org.lflang/src/org/lflang/generator/rust/rust-runtime-version.txt
new file mode 100644
index 0000000000..652e2f5262
--- /dev/null
+++ b/org.lflang/src/org/lflang/generator/rust/rust-runtime-version.txt
@@ -0,0 +1 @@
+05e4d06e5b4e938e676fbe9072640b92fa1b1df0
diff --git a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt
index 284b76b526..90fe144950 100644
--- a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt
+++ b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt
@@ -505,20 +505,16 @@ class TSGenerator(
return "TimeValue"
}
- override fun getTargetTagIntervalType(): String {
- return this.targetUndefinedType
- }
-
override fun getTargetUndefinedType(): String {
return "Present"
}
override fun getTargetFixedSizeListType(baseType: String, size: Int): String {
- return "Array(${size})<${baseType}>"
+ return "Array($size)<$baseType>"
}
override fun getTargetVariableSizeListType(baseType: String): String {
- return "Array<${baseType}>"
+ return "Array<$baseType>"
}
override fun getTarget(): Target {
diff --git a/org.lflang/src/org/lflang/validation/LFValidator.xtend b/org.lflang/src/org/lflang/validation/LFValidator.xtend
index 64762e91a6..e6201c3900 100644
--- a/org.lflang/src/org/lflang/validation/LFValidator.xtend
+++ b/org.lflang/src/org/lflang/validation/LFValidator.xtend
@@ -82,6 +82,7 @@ import org.lflang.lf.Visibility
import org.lflang.lf.WidthSpec
import static extension org.lflang.ASTUtils.*
+import static extension org.lflang.JavaAstUtils.*
import org.lflang.federated.SupportedSerializers
/**
@@ -205,7 +206,7 @@ class LFValidator extends BaseLFValidator {
error(UNDERSCORE_MESSAGE + name, feature)
}
- if (this.target.keywords.contains(name)) {
+ if (this.target.isReservedIdent(name)) {
error(RESERVED_MESSAGE + name, feature)
}
diff --git a/test/Cpp/src/ActionValues.lf b/test/Cpp/src/ActionValues.lf
new file mode 100644
index 0000000000..0564dc30e6
--- /dev/null
+++ b/test/Cpp/src/ActionValues.lf
@@ -0,0 +1,47 @@
+// Test logical action with delay.
+target Cpp;
+
+main reactor ActionValues {
+ state r1done: bool(false);
+ state r2done: bool(false);
+ logical action act(100 msec): int;
+
+ reaction(startup) -> act {=
+ act.schedule(100); // scheduled in 100 ms
+ std::chrono::milliseconds delay(50);
+ act.schedule(-100, delay); // scheduled in 150 ms, value is overwritten
+ =}
+
+ reaction(act) {=
+ auto elapsed = get_elapsed_logical_time();
+
+ std::cout << "[@" << elapsed << '\n';
+ std::cout << " action transmitted: " << *act.get() << '\n';
+ std::cout << "]\n";
+
+ if (elapsed == 100ms) {
+ if (*act.get() != 100) {
+ std::cerr << "ERROR: Expected action value to be 100\n";
+ exit(1);
+ }
+ r1done = true;
+ } else {
+ if (elapsed != 150ms) {
+ std::cerr << "ERROR: Unexpected reaction invocation at " << elapsed << '\n';
+ exit(1);
+ }
+ if (*act.get() != -100) {
+ std::cerr << "ERROR: Expected action value to be -100\n";
+ exit(1);
+ }
+ r2done = true;
+ }
+ =}
+
+ reaction (shutdown) {=
+ if (!r1done || !r2done) {
+ std::cerr << "ERROR: Expected 2 reaction invocations\n";
+ exit(1);
+ }
+ =}
+}
diff --git a/test/Rust/src-gen/Cargo.toml b/test/Rust/src-gen/Cargo.toml
new file mode 100644
index 0000000000..4da4fced77
--- /dev/null
+++ b/test/Rust/src-gen/Cargo.toml
@@ -0,0 +1,19 @@
+# This file is checked into version control, which is normal.
+# Please check it back out if you need to delete src-gen:
+# rm -rf test/Rust/src-gen && git checkout test/Rust/src-gen
+#
+# The reason this is here is that it allows all generated test
+# projects to share the same compilation artifacts. This lets us
+# build dependencies only once and speeds up tests enormously.
+#
+# One downside is that changing the location of dependencies may
+# cause cargo to fail because it has recorded another location in
+# Cargo.lock. Changing locations is useful for development, for
+# instance, to use a local version of the reactor_rt crate.
+#
+# Using this file is only an optimisation for our tests in CI
+# and locally. It's not *needed* for the tests to pass or anything.
+# If it causes problems, we can throw it away or hide it.
+[workspace]
+members = ["*"]
+exclude = ["target", "generics"]
diff --git a/test/Rust/src/ActionDelay.lf b/test/Rust/src/ActionDelay.lf
new file mode 100644
index 0000000000..eaa87d700a
--- /dev/null
+++ b/test/Rust/src/ActionDelay.lf
@@ -0,0 +1,29 @@
+// Test logical action with delay.
+target Rust;
+
+main reactor ActionDelay {
+ logical action act0;
+ logical action act1(100 msec);
+ state count: u32(0);
+
+ reaction(startup) -> act0, act1 {=
+ ctx.schedule(act0, after!(100 ms));
+ ctx.schedule(act1, after!(40 ms));
+ =}
+
+ reaction(act0) {=
+ assert_tag_is!(ctx, T0 + 100 ms);
+ self.count += 1;
+ =}
+
+ reaction(act1) {=
+ assert_tag_is!(ctx, T0 + 140 ms);
+ self.count += 1;
+ =}
+
+ reaction(shutdown) {=
+ assert_eq!(2, self.count);
+ println!("SUCCESS")
+ =}
+
+}
diff --git a/test/Rust/src/ActionImplicitDelay.lf b/test/Rust/src/ActionImplicitDelay.lf
new file mode 100644
index 0000000000..1d78a11a11
--- /dev/null
+++ b/test/Rust/src/ActionImplicitDelay.lf
@@ -0,0 +1,21 @@
+// Test logical action with delay.
+target Rust;
+
+main reactor ActionImplicitDelay {
+ logical action act(40 msec);
+ state count: u64(1);
+
+ reaction(startup) -> act {=
+ ctx.schedule(act, Asap);
+ =}
+
+ reaction(act) -> act {=
+ assert_tag_is!(ctx, T0 + (40 * self.count) ms);
+ self.count += 1;
+ if self.count <= 3 {
+ ctx.schedule(act, Asap);
+ } else {
+ println!("SUCCESS")
+ }
+ =}
+}
diff --git a/test/Rust/src/ActionIsPresent.lf b/test/Rust/src/ActionIsPresent.lf
new file mode 100644
index 0000000000..a21985f83b
--- /dev/null
+++ b/test/Rust/src/ActionIsPresent.lf
@@ -0,0 +1,28 @@
+// Tests the is_present variable for actions.
+target Rust;
+
+main reactor ActionIsPresent {
+ logical action a;
+ state success: bool(false);
+ state tried: bool(false);
+
+ reaction(startup, a) -> a {=
+ if !ctx.is_present(a) {
+ assert_tag_is!(ctx, T0);
+ assert!(!self.tried, "Already tried, is_present does not work properly.");
+ self.tried = true;
+ // was triggered by startup
+ println!("Startup reaction @ {}", ctx.get_tag());
+ ctx.schedule(a, after!(1 ns));
+ } else {
+ assert_tag_is!(ctx, T0 + 1 ns);
+ // was triggered by schedule above
+ println!("Scheduled reaction @ {}", ctx.get_tag());
+ self.success = true;
+ }
+ =}
+ reaction(shutdown) {=
+ assert!(self.success, "What happened!?");
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/ActionIsPresentDouble.lf b/test/Rust/src/ActionIsPresentDouble.lf
new file mode 100644
index 0000000000..b11dce93b6
--- /dev/null
+++ b/test/Rust/src/ActionIsPresentDouble.lf
@@ -0,0 +1,22 @@
+// Test logical action with delay.
+target Rust;
+
+main reactor ActionIsPresentDouble {
+ logical action act0;
+ logical action act1(100 msec);
+
+ reaction(startup) -> act0, act1 {=
+ ctx.schedule(act0, after!(100 ms));
+ ctx.schedule(act1, after!(0 ms));
+ ctx.schedule(act1, Asap);
+ =}
+
+ reaction(act1, act0) {=
+ assert!(ctx.is_present(act0));
+ assert!(ctx.is_present(act1));
+ if ctx.get_tag() == tag!(T0 + 100 ms) {
+ println!("success");
+ }
+ =}
+
+}
diff --git a/test/Rust/src/ActionScheduleMicrostep.lf b/test/Rust/src/ActionScheduleMicrostep.lf
new file mode 100644
index 0000000000..55b56ffc9f
--- /dev/null
+++ b/test/Rust/src/ActionScheduleMicrostep.lf
@@ -0,0 +1,23 @@
+// Test logical action with delay.
+target Rust;
+
+main reactor ActionScheduleMicrostep {
+ logical action act;
+ state count: u32(1);
+
+ reaction(startup) -> act {=
+ assert_tag_is!(ctx, T0);
+ // executes at (T0, 0)
+ ctx.schedule(act, Asap);
+ =}
+
+ reaction(act) -> act {=
+ assert_tag_is!(ctx, (T0, self.count));
+ self.count += 1;
+ if self.count <= 3 {
+ ctx.schedule(act, Asap);
+ } else {
+ println!("SUCCESS")
+ }
+ =}
+}
diff --git a/test/Rust/src/ActionValues.lf b/test/Rust/src/ActionValues.lf
new file mode 100644
index 0000000000..68c074e360
--- /dev/null
+++ b/test/Rust/src/ActionValues.lf
@@ -0,0 +1,32 @@
+// Test logical action with delay.
+target Rust;
+
+main reactor ActionValues {
+ state r1done: bool(false);
+ state r2done: bool(false);
+ logical action act(100 msec): i32;
+
+ reaction(startup) -> act {=
+ ctx.schedule_with_v(act, Some(100), Asap); // scheduled in 100 ms
+ // scheduled in 150 ms, value is overwritten
+ ctx.schedule_with_v(act, Some(-100), after!(50 ms));
+ =}
+
+ reaction(act) {=
+ println!("At {}, received {:?}", ctx.get_tag(), ctx.get(act));
+
+ if ctx.get_tag() == tag!(T0 + 100 ms) {
+ assert_eq!(Some(100), ctx.get(act));
+ self.r1done = true;
+ } else {
+ assert_tag_is!(ctx, T0 + 150 ms);
+ assert_eq!(Some(-100), ctx.get(act));
+ self.r2done = true;
+ }
+ =}
+
+ reaction (shutdown) {=
+ assert!(self.r1done && self.r2done);
+ println!("Success")
+ =}
+}
diff --git a/test/Rust/src/ActionValuesCleanup.lf b/test/Rust/src/ActionValuesCleanup.lf
new file mode 100644
index 0000000000..3c2f88e01f
--- /dev/null
+++ b/test/Rust/src/ActionValuesCleanup.lf
@@ -0,0 +1,51 @@
+// Test that scheduling actions drops the previous value if any
+target Rust;
+
+main reactor ActionValuesCleanup {
+ preamble {=
+ use std::sync::atomic::AtomicBool;
+ use std::sync::atomic::Ordering;
+ // set to true when destructor is called
+ static mut DROPPED: AtomicBool = AtomicBool::new(false);
+
+ #[derive(Clone, Debug)]
+ struct FooDrop { }
+ impl std::ops::Drop for FooDrop {
+ fn drop(&mut self) {
+ unsafe {
+ DROPPED.store(true, Ordering::SeqCst);
+ }
+ }
+ }
+ =}
+
+
+ logical action act: FooDrop;
+ state count: u32(0);
+
+ reaction(startup) -> act {=
+ ctx.schedule_with_v(act, Some(FooDrop { }), Asap)
+ =}
+
+ reaction(act) -> act {=
+ ctx.use_ref(act, |v| println!("{:?}", v));
+ if self.count == 0 {
+ self.count = 1;
+ assert!(ctx.is_present(act));
+ assert!(ctx.use_ref(act, |v| v.is_some()));
+ ctx.schedule(act, Asap);
+ } else if self.count == 1 {
+ assert!(ctx.is_present(act));
+ assert!(ctx.use_ref(act, |v| v.is_none()));
+ assert!(unsafe { DROPPED.load(Ordering::SeqCst) });
+ self.count = 2;
+ } else {
+ unreachable!();
+ }
+ =}
+
+ reaction(shutdown) {=
+ assert_eq!(2, self.count, "expected 2 invocations");
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/CompositionInitializationOrder.lf b/test/Rust/src/CompositionInitializationOrder.lf
new file mode 100644
index 0000000000..fb5ec56451
--- /dev/null
+++ b/test/Rust/src/CompositionInitializationOrder.lf
@@ -0,0 +1,23 @@
+// This test asserts the relative execution order of startup
+// reactions within a composite reactor.
+
+
+target Rust;
+
+main reactor CompositionInitializationOrder {
+ c1 = new Component1();
+ c2 = new Component2();
+ reaction(startup) {=
+ println!("parent woke up");
+ =}
+}
+reactor Component2 {
+ reaction(startup) {=
+ println!("c2 woke up");
+ =}
+}
+reactor Component1 {
+ reaction(startup) {=
+ println!("c1 woke up");
+ =}
+}
diff --git a/test/Rust/src/CompositionWithPorts.lf b/test/Rust/src/CompositionWithPorts.lf
new file mode 100644
index 0000000000..14cdf53038
--- /dev/null
+++ b/test/Rust/src/CompositionWithPorts.lf
@@ -0,0 +1,25 @@
+
+target Rust;
+reactor Source {
+ output out: i32;
+ reaction(startup) -> out {=
+ ctx.set(&mut out, 76600)
+ =}
+}
+reactor Sink {
+ input inport: i32;
+ reaction(inport) {=
+ if let Some(value) = ctx.get(inport) {
+ println!("received {}", value);
+ assert_eq!(76600, value);
+ } else {
+ unreachable!();
+ }
+ =}
+}
+main reactor CompositionWithPorts {
+ source = new Source();
+ sink = new Sink();
+
+ source.out -> sink.inport;
+}
diff --git a/test/Rust/src/CtorParamDefault.lf b/test/Rust/src/CtorParamDefault.lf
new file mode 100644
index 0000000000..6b3c7894a6
--- /dev/null
+++ b/test/Rust/src/CtorParamDefault.lf
@@ -0,0 +1,13 @@
+target Rust;
+
+reactor Print(value:i32(42)) {
+ state v: i32(value);
+
+ reaction(startup) {=
+ assert_eq!(42, self.v);
+ println!("success");
+ =}
+}
+main reactor CtorParamDefault {
+ p = new Print();
+}
diff --git a/test/Rust/src/CtorParamMixed.lf b/test/Rust/src/CtorParamMixed.lf
new file mode 100644
index 0000000000..a6c80f7c2c
--- /dev/null
+++ b/test/Rust/src/CtorParamMixed.lf
@@ -0,0 +1,21 @@
+target Rust;
+
+reactor Print(
+ value: i32(42),
+ name: String({="xxx".into()=}),
+ other: bool(false)
+) {
+ state value(value);
+ state name(name);
+ state other(other);
+
+ reaction(startup) {=
+ assert_eq!(42, self.value);
+ assert_eq!("x2hr", self.name.as_str());
+ assert_eq!(true, self.other);
+ println!("success");
+ =}
+}
+main reactor CtorParamMixed {
+ p = new Print(other=true, name={="x2hr".into()=});
+}
diff --git a/test/Rust/src/CtorParamSimple.lf b/test/Rust/src/CtorParamSimple.lf
new file mode 100644
index 0000000000..94af3c7f7a
--- /dev/null
+++ b/test/Rust/src/CtorParamSimple.lf
@@ -0,0 +1,13 @@
+target Rust;
+
+reactor Print(value:i32(42)) {
+ state v: i32(value);
+
+ reaction(startup) {=
+ assert_eq!(self.v, 23);
+ println!("success");
+ =}
+}
+main reactor CtorParamSimple {
+ p = new Print(value=23);
+}
diff --git a/test/Rust/src/DependencyOnChildPort.lf b/test/Rust/src/DependencyOnChildPort.lf
new file mode 100644
index 0000000000..ac5f53e276
--- /dev/null
+++ b/test/Rust/src/DependencyOnChildPort.lf
@@ -0,0 +1,27 @@
+/* Test that reactions can depend on ports of input child. */
+
+target Rust;
+
+reactor Box {
+ input inp: u32;
+ output out: u32;
+
+ inp -> out;
+}
+
+main reactor {
+ state done: bool(false);
+
+ box0 = new Box();
+ box1 = new Box();
+
+ box0.out -> box1.inp;
+
+ reaction(startup) -> box0.inp {= ctx.set(box0__inp, 444); =}
+ reaction(box1.out) {= assert!(ctx.get_elapsed_logical_time().is_zero()); self.done = true; =}
+
+ reaction(shutdown) {=
+ assert!(self.done, "reaction was not executed");
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/DependencyUseAccessible.lf b/test/Rust/src/DependencyUseAccessible.lf
new file mode 100644
index 0000000000..8abe778c45
--- /dev/null
+++ b/test/Rust/src/DependencyUseAccessible.lf
@@ -0,0 +1,43 @@
+/* Test that use-dependencies may be absent within a reaction. */
+target Rust;
+
+reactor Source {
+ output clock: u32;
+ output o1: u32;
+ output o2: u32;
+ timer t1(35 msec);
+ timer t2(70 msec);
+ reaction(startup) -> clock {= ctx.set(clock, 0); =}
+ reaction(t1) -> clock, o1 {= ctx.set(clock, 1); ctx.set(o1, 10) =}
+ reaction(t2) -> clock, o2 {= ctx.set(clock, 2); =} // has a dependency but doesn't use it
+}
+
+reactor Sink {
+ input clock: u32;
+ input in1: u32;
+ input in2: u32;
+
+ reaction(clock) in1, in2 {=
+ match ctx.get(clock) {
+ Some(0) | Some(2) => {
+ assert_eq!(None, ctx.get(in1));
+ assert_eq!(None, ctx.get(in2));
+ },
+ Some(1) => {
+ assert_eq!(Some(10), ctx.get(in1));
+ assert_eq!(None, ctx.get(in2));
+ },
+ c => panic!("No such signal expected {:?}", c)
+ }
+ =}
+}
+
+main reactor {
+ source = new Source();
+ sink = new Sink();
+
+ source.clock -> sink.clock;
+
+ source.o1 -> sink.in1;
+ source.o2 -> sink.in2;
+}
diff --git a/test/Rust/src/DependencyUseNonTrigger.lf b/test/Rust/src/DependencyUseNonTrigger.lf
new file mode 100644
index 0000000000..192e47720b
--- /dev/null
+++ b/test/Rust/src/DependencyUseNonTrigger.lf
@@ -0,0 +1,22 @@
+/* Test that use-dependencies do not trigger reactions. */
+target Rust;
+
+reactor Source {
+ output clock: u32;
+ reaction(startup) -> clock {= ctx.set(clock, 0); =}
+}
+
+reactor Sink {
+ input clock: u32;
+
+ reaction() clock {= panic!("Should not be executed") =}
+
+ reaction(shutdown) {= println!("Success") =}
+}
+
+main reactor {
+ source = new Source();
+ sink = new Sink();
+
+ source.clock -> sink.clock;
+}
diff --git a/test/Rust/src/Minimal.lf b/test/Rust/src/Minimal.lf
new file mode 100644
index 0000000000..6e21b84e1e
--- /dev/null
+++ b/test/Rust/src/Minimal.lf
@@ -0,0 +1,7 @@
+// This is a smoke test of a minimal reactor.
+target Rust;
+main reactor Minimal {
+ reaction(startup) {=
+ println!("Hello World.");
+ =}
+}
diff --git a/test/Rust/src/MovingAverage.lf b/test/Rust/src/MovingAverage.lf
new file mode 100644
index 0000000000..1bbb4af673
--- /dev/null
+++ b/test/Rust/src/MovingAverage.lf
@@ -0,0 +1,63 @@
+// Demonstration of a state variable that is a fixed size list.
+// The MovingAverage reactor computes the moving average of the last
+// four inputs and produces that as output. The source is a counting
+// sequence.
+target Rust {
+ timeout: 50 msec,
+};
+reactor Source {
+ output out: f64;
+ state count: u32(0);
+ timer clock(0, 10 msec);
+ reaction(clock) -> out {=
+ ctx.set(out, self.count.into());
+ self.count += 1;
+ =}
+}
+reactor MovingAverageImpl {
+ state delay_line: f64[4] (0.0, 0.0, 0.0, 0.0);// fixme inaccessible ({=[0.0 ; 4]=});
+ state index: usize(0);
+ input in_: f64;
+ output out: f64;
+
+ reaction(in_) -> out {=
+ let in_ = ctx.get(in_).unwrap();
+
+ // Insert the input in the delay line.
+ self.delay_line[self.index] = in_;
+ // Update the index for the next input.
+ self.index = (self.index + 1) % 4;
+
+ // Calculate the output.
+ let sum: f64 = self.delay_line.iter().sum();
+ ctx.set(out, sum / 4.0);
+
+ =}
+}
+
+reactor Print {
+ input in_: f64;
+ state count: usize(0);
+
+ preamble {=
+ const EXPECTED: [ f64 ; 6 ] = [0.0, 0.25, 0.75, 1.5, 2.5, 3.5];
+ =}
+
+ reaction(in_) {=
+ let in_ = ctx.get(in_).unwrap();
+ println!("Received {}", in_);
+ assert_eq!(in_, EXPECTED[self.count]);
+ self.count += 1;
+ =}
+ reaction(shutdown) {=
+ assert_eq!(self.count, 6);
+ println!("Success.");
+ =}
+}
+main reactor MovingAverage {
+ s = new Source();
+ m = new MovingAverageImpl();
+ p = new Print();
+ s.out -> m.in_;
+ m.out -> p.in_;
+}
diff --git a/test/Rust/src/NativeListsAndTimes.lf b/test/Rust/src/NativeListsAndTimes.lf
new file mode 100644
index 0000000000..25c2d7b912
--- /dev/null
+++ b/test/Rust/src/NativeListsAndTimes.lf
@@ -0,0 +1,47 @@
+target Rust;
+
+// This test passes if it is successfully compiled into valid target code.
+
+reactor Foo(x: i32(0),
+ y: time(0), // Units are missing but not required
+ z(1 msec), // Type is missing but not required
+ p: i32[](1, 2, 3, 4), // List of integers
+ p2: i32[]({= vec![1] =}), // List of integers with single element
+
+ // todo // p2: i32[](1), // List of integers with single element
+ // p3: i32[](), // Empty list of integers
+ q: Vec (1 msec, 2 msec, 3 msec), // List of time values
+ r: time({=0=}), // Zero-valued target code also is a valid time
+ g: time[](1 msec, 2 msec) // List of time values
+ ) {
+ state s: time(y); // Reference to explicitly typed time parameter
+ state t: time(z); // Reference to implicitly typed time parameter
+ state v: bool; // Uninitialized boolean state variable
+ state w: time; // Uninitialized time state variable
+ timer tick(0); // Units missing but not required
+ timer tock(1 sec); // Implicit type time
+ timer toe(z); // Implicit type time
+ // fixme following should be equivalent:
+ // state baz(p);
+ // state baz: i32[4](p);
+ // state baz: i32[4]({=p=});
+ // because the initializer is the same modulo fat braces
+
+ // state baz(p); // Implicit type i32[] fixme this interplays badly with syntax for array init
+ state period(z); // Implicit type time
+ state times: Vec>(q, g); // a list of lists
+ reaction(tick) {=
+ // Target code
+ =}
+ /*
+ reactor Foo (p: i32[](1, 2)) {
+ state baz(p); // Implicit type i32[]
+ state baz({=p=}); // Implicit type i32[]
+ }
+
+ */
+}
+
+main reactor NativeListsAndTimes {
+ foo = new Foo();
+}
diff --git a/test/Rust/src/PhysicalAction.lf b/test/Rust/src/PhysicalAction.lf
new file mode 100644
index 0000000000..19a51be75e
--- /dev/null
+++ b/test/Rust/src/PhysicalAction.lf
@@ -0,0 +1,27 @@
+target Rust {
+
+ keepalive: true
+};
+
+main reactor {
+
+ physical action act: u32;
+
+ reaction(startup) -> act {=
+ let act = act.clone();
+ ctx.spawn_physical_thread(move |link| {
+ std::thread::sleep(Duration::from_millis(20));
+ link.schedule_physical_with_v(&act, Some(434), Asap);
+ });
+ =}
+
+ reaction(act) {=
+ let value = ctx.get(act).unwrap();
+ println!("---- Vu {} à {}", value, ctx.get_tag());
+
+ let elapsed_time = ctx.get_elapsed_logical_time();
+ assert!(elapsed_time >= delay!(20 ms));
+ println!("success");
+ ctx.request_stop(Asap);
+ =}
+}
diff --git a/test/Rust/src/PortConnectionInSelfInChild.lf b/test/Rust/src/PortConnectionInSelfInChild.lf
new file mode 100644
index 0000000000..f50521d1b4
--- /dev/null
+++ b/test/Rust/src/PortConnectionInSelfInChild.lf
@@ -0,0 +1,30 @@
+// Tests a port connection between (input of self -> input of child)
+target Rust;
+
+reactor Child {
+ input inp: i32;
+ state done: bool(false);
+
+ reaction(inp) {=
+ assert_matches!(ctx.get(inp), Some(76600));
+ self.done = true;
+ =}
+
+ reaction(shutdown) {=
+ assert!(self.done);
+ println!("Success")
+ =}
+}
+reactor Parent {
+ input inp: i32;
+ child = new Child();
+ inp -> child.inp;
+}
+main reactor {
+ parent = new Parent();
+
+ reaction(startup) -> parent.inp {=
+ ctx.set(parent__inp, 76600);
+ println!("out := 76600")
+ =}
+}
diff --git a/test/Rust/src/PortConnectionInSelfOutSelf.lf b/test/Rust/src/PortConnectionInSelfOutSelf.lf
new file mode 100644
index 0000000000..bb8e9d655b
--- /dev/null
+++ b/test/Rust/src/PortConnectionInSelfOutSelf.lf
@@ -0,0 +1,37 @@
+// Tests a port connection between (input of self -> input of child)
+target Rust;
+
+reactor Source {
+ output out: i32;
+ reaction(startup) -> out {=
+ ctx.set(out, 76600);
+ println!("out := 76600")
+ =}
+}
+reactor TestCase {
+ input inp: i32;
+ output out: i32;
+ inp -> out;
+}
+reactor Sink {
+ input inp: i32;
+ state done: bool(false);
+ reaction(inp) {=
+ assert_matches!(ctx.get(inp), Some(76600));
+ println!("Success");
+ self.done = true;
+ =}
+
+ reaction(shutdown) {=
+ assert!(self.done, "reaction was not executed")
+ =}
+}
+
+main reactor {
+ source = new Source();
+ middle = new TestCase();
+ sink = new Sink();
+
+ source.out -> middle.inp;
+ middle.out -> sink.inp;
+}
diff --git a/test/Rust/src/PortConnectionOutChildOutSelf.lf b/test/Rust/src/PortConnectionOutChildOutSelf.lf
new file mode 100644
index 0000000000..bc33e5ee1b
--- /dev/null
+++ b/test/Rust/src/PortConnectionOutChildOutSelf.lf
@@ -0,0 +1,45 @@
+// Tests a port connection between (input of self -> input of child)
+target Rust;
+
+reactor Child {
+ output out: i32;
+ reaction(startup) -> out {=
+ ctx.set(out, 76600);
+ println!("out := 76600")
+ =}
+}
+reactor Parent {
+ output out: i32;
+
+ child = new Child();
+ child.out -> out;
+}
+reactor Sink {
+ input inp: i32;
+ state done: bool(false);
+ reaction(inp) {=
+ assert_matches!(ctx.get(inp), Some(76600));
+ println!("Success");
+ self.done = true;
+ =}
+
+ reaction(shutdown) {=
+ assert!(self.done, "reaction was not executed")
+ =}
+}
+
+main reactor {
+ parent = new Parent();
+
+ state done: bool(false);
+
+ reaction(parent.out) {=
+ assert_matches!(ctx.get(parent__out), Some(76600));
+ println!("Success");
+ self.done = true;
+ =}
+
+ reaction(shutdown) {=
+ assert!(self.done, "reaction was not executed")
+ =}
+}
diff --git a/test/Rust/src/PortRefCleanup.lf b/test/Rust/src/PortRefCleanup.lf
new file mode 100644
index 0000000000..061dbe2715
--- /dev/null
+++ b/test/Rust/src/PortRefCleanup.lf
@@ -0,0 +1,40 @@
+// Tests that a port value is cleaned up after a tag
+target Rust;
+
+reactor Box {
+ input inp: u32;
+ output out: u32;
+
+ inp -> out;
+}
+
+main reactor {
+ boxr = new Box();
+
+ timer t1(0);
+ timer t2(15msec);
+
+ state reaction_num: u32(0);
+ state done: bool(false);
+
+ reaction(t1) -> boxr.inp {=
+ ctx.set(boxr__inp, 150);
+ self.reaction_num += 1;
+ =}
+
+ reaction(boxr.out, t2) {=
+ if self.reaction_num == 1 {
+ assert!(matches!(ctx.get(boxr__out), Some(150)));
+ } else {
+ assert_eq!(self.reaction_num, 2);
+ assert!(ctx.get(boxr__out).is_none(), "value should have been cleaned up");
+ self.done = true;
+ }
+ self.reaction_num += 1;
+ =}
+
+ reaction(shutdown) {=
+ assert!(self.done, "reaction was not executed");
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/PortValueCleanup.lf b/test/Rust/src/PortValueCleanup.lf
new file mode 100644
index 0000000000..c02826e4c4
--- /dev/null
+++ b/test/Rust/src/PortValueCleanup.lf
@@ -0,0 +1,41 @@
+// Tests that a port value is cleaned up after a tag
+target Rust;
+
+reactor Source {
+ output out: u32;
+
+ reaction(startup) -> out {=
+ ctx.set(out, 150);
+ =}
+}
+
+reactor Sink {
+ timer t2(15 msec);
+
+ input in: u32;
+ state reaction_num: u32(0);
+ state done: bool(false);
+
+ reaction(in, t2) {=
+ if self.reaction_num == 0 {
+ assert!(matches!(ctx.get(r#in), Some(150)));
+ } else {
+ assert_eq!(self.reaction_num, 1);
+ assert!(ctx.get(r#in).is_none(), "value should have been cleaned up");
+ self.done = true;
+ }
+ self.reaction_num += 1;
+ =}
+
+ reaction(shutdown) {=
+ assert!(self.done, "reaction was not executed");
+ println!("success");
+ =}
+}
+
+main reactor {
+ source = new Source();
+ sink = new Sink();
+
+ source.out -> sink.in;
+}
diff --git a/test/Rust/src/Preamble.lf b/test/Rust/src/Preamble.lf
new file mode 100644
index 0000000000..34f2cda35d
--- /dev/null
+++ b/test/Rust/src/Preamble.lf
@@ -0,0 +1,11 @@
+target Rust;
+main reactor Preamble {
+ preamble {=
+ fn add_42(i: i32) -> i32 {
+ return i + 42;
+ }
+ =}
+ reaction(startup) {=
+ println!("42 plus 42 is {}.\n", add_42(42));
+ =}
+}
diff --git a/test/Rust/src/README.md b/test/Rust/src/README.md
new file mode 100644
index 0000000000..30bbd1035a
--- /dev/null
+++ b/test/Rust/src/README.md
@@ -0,0 +1,100 @@
+# Status
+
+This is not exhaustive. Ideally each of those bullet points would have a test case.
+
+## Language
+
+- [x] reactor composition
+ - [x] `CompositionInitializationOrder.lf`: startup reactions are called bottom-up
+ - [x] `CompositionWithPorts.lf`: port bindings work
+- [ ] ports
+ - [x] `PortValueCleanup.lf`: port value is cleaned up at the end of a tag
+ - [x] `PortRefCleanup.lf`: port value is cleaned up at the end of a tag, when the upstream is a port reference
+ - [x] connections...
+ - [x] `PortConnectionInSelfInChild.lf`: input of self to input of child
+ - [x] `PortConnectionInSelfOutSelf.lf`: input of self to output of self
+ - [x] `PortConnectionOutChildOutSelf.lf`: output of child to output of self
+ - [x] `CompositionWithPorts.lf`: output of child to input of child
+ - [ ] mutable inputs
+- [ ] reaction dependency handling
+ - dependencies can be declared...
+ - [ ] on ports of this reactor
+ - [x] `DependencyOnChildPort.lf`: on ports of a child reactor
+ - [ ] on an action
+ - [ ] trigger dependencies
+ - [ ] `todo.lf`: trigger dependencies trigger reactions
+ - [ ] `todo.lf`: multiple trigger dependencies may be triggered independently
+ - [ ] uses-dependencies
+ - [x] `DependencyUseNonTrigger`: use dependencies do not trigger reactions
+ - [x] `DependencyUseAccessible`: use dependencies make the port accessible within the reaction, values are observable
+ - [ ] `todo.lf`: use dependencies may be declared on actions
+ - [ ] effects-dependency
+ - [ ] `todo.lf`: effects dependencies ...
+ - [x] `todo.lf`: on a logical action
+ - [ ] reaction priority is respected
+ - [x] locally
+ - [x] between different child reactors
+- [ ] imports
+- [x] preambles
+ - [x] `Preamble.lf`: preamble within reactor
+ - [ ] top-level preamble
+- [x] logical actions
+ - [x] `ActionImplicitDelay.lf`: scheduling an action with no additional delay uses its implicit delay
+ - [x] `ActionDelay.lf`:
+ - [x] `ActionScheduleMicrostep.lf`: an action scheduled with a zero delay is only triggered on the next microstep
+ - [x] `ActionValues.lf`: scheduling an action with a value at multiple different tags preserves each value
+ - [x] `ActionValuesCleanup.lf`: action value is cleaned up at the end of a tag
+ - [x] `ActionIsPresent.lf`: function `is_present` checks whether an action is present at the current tag
+ - [x] `ActionIsPresentDouble.lf`: several actions may be present at the same tag
+- [ ] physical actions
+ - [x] `PhysicalAction.lf`: tests scheduling of a physical action from an asynchronous thread
+ - [ ] `PhysicalActionWakesSleepingScheduler.lf`: a physical action triggered during a period of idleness of the scheduler should wake it timely -> todo check it out from the git history and fix it
+ - [ ] `PhysicalActionWithKeepalive.lf`: keepalive option should keep the scheduler alive when there are physical actions in the program
+- [x] timers
+ - [x] `TimerDefaults.lf`: timer with all params defaulted (`timer t;`) is non-periodic and has offset zero
+ - [x] `TimerPeriodic.lf`: timer can be periodic
+ - [x] `TimerIsPresent.lf`: timer should be queryable with `is_present`
+ - [x] timer cannot be scheduled manually
+- [x] `shutdown` trigger & `request_stop`
+ - [x] `Stop.lf`: `request_stop` schedules a shutdown at T+(1 microstep)
+ - [x] `StopCleanup.lf`: ports are cleaned up before the shutdown wave executes
+ - [x] `StopTopology.lf`: shutdown wave occurs in topological order like a normal wave
+ - [x] `StopTimeout.lf`: `shutdown` is triggered even if the program exits because of timeout target property
+ - [x] `StopNoEvent.lf`: `shutdown` is triggered even if the program exits because of an empty event queue
+ - [x] `StopIdempotence.lf`: `request_stop` may be called within the shutdown wave, but it should have no effect.
+ - [x] `StopDuringStartup.lf`: `request_stop` may be called within startup.
+- [x] state variables
+ - [x] support time type
+ - [x] are accessible within reactions
+ - [x] are *not* accessible within initializers
+ - [x] are initialized to their proper value
+- [x] reactor parameters
+ - [x] `CtorParamSimple.lf`: ctor parameters are accessible in initializers and reactions
+ - [x] `CtorParamDefault.lf`: ctor arguments may be defaulted
+ - [x] `CtorParamMixed.lf`: ctor arguments may be mentioned in any order, even with defaulted parameters
+ - note: must be `Clone`
+- [ ] array types
+ - [x] support fixed-sized arrays
+ - [x] `TypeVarLengthList.lf`: support variable length lists (`Vec`)
+ - [x] support array initializer syntax
+ - [ ] support array assignment syntax (fixme: doesn't exist in LF)
+- [ ] deadlines
+ - ...
+- [ ] reactor inheritance
+ - ...
+- [ ] multiports
+- [x] generics
+ - [x] `GenericReactor.lf`: generic reactors may compose, types are properly instantiated
+ - [x] `CtorParamGeneric.lf`: ctor parameters may refer to type parameters
+ - [x] `CtorParamGenericInst.lf`: generic ctor parameters work properly when creating child instance
+
+### Runtime
+
+- [ ] parallelize independent computation
+- [ ] keepalive option -> relevant with physical actions
+- [ ] timeout option
+
+### Other todos/ nice-to-have things
+
+- [ ] benchmark generation
+- [ ] CLI parameter parsing
diff --git a/test/Rust/src/ReservedKeywords.lf b/test/Rust/src/ReservedKeywords.lf
new file mode 100644
index 0000000000..6b5d7fe2e2
--- /dev/null
+++ b/test/Rust/src/ReservedKeywords.lf
@@ -0,0 +1,30 @@
+// Tests that rust keywords may be used as identifiers in LF and are properly escaped by the emitter
+target Rust;
+
+reactor box {
+ input in: u32;
+ output struct: u32;
+
+ in -> struct;
+
+ state foo: bool(true); // not escaped
+
+ reaction(in) {=
+ ctx.get(r#in);
+ =}
+}
+
+main reactor ReservedKeywords(struct: u32(0)) {
+ box = new box();
+
+ timer t1(0);
+ timer t2(15msec);
+
+ // not in types, this wouldn't be useful.
+ // state reaction_num: struct(0);
+
+ reaction(box.struct, t2) {=
+
+ =}
+
+}
diff --git a/test/Rust/src/SingleFileGeneration.lf b/test/Rust/src/SingleFileGeneration.lf
new file mode 100644
index 0000000000..ca4b000f92
--- /dev/null
+++ b/test/Rust/src/SingleFileGeneration.lf
@@ -0,0 +1,28 @@
+// The same as CompositionWithPorts.lf, but as a single file project
+target Rust {
+ single-file-project: true
+};
+
+reactor Source {
+ output out: i32;
+ reaction(startup) -> out {=
+ ctx.set(out, 76600)
+ =}
+}
+reactor Sink {
+ input inport: i32;
+ reaction(inport) {=
+ if let Some(value) = ctx.get(inport) {
+ println!("received {}", value);
+ assert_eq!(76600, value);
+ } else {
+ unreachable!();
+ }
+ =}
+}
+main reactor {
+ source = new Source();
+ sink = new Sink();
+
+ source.out -> sink.inport;
+}
diff --git a/test/Rust/src/Stop.lf b/test/Rust/src/Stop.lf
new file mode 100644
index 0000000000..6ad1f4e494
--- /dev/null
+++ b/test/Rust/src/Stop.lf
@@ -0,0 +1,86 @@
+/*
+ * A test for the request_stop() functionality in Lingua Franca.
+ *
+ * @author Soroush Bateni
+ */
+ target Rust {
+ timeout: 11 msec,
+
+};
+
+/**
+ * @param take_a_break_after: Indicates how many messages are sent
+ * in consecutive superdense time
+ * @param break_interval: Determines how long the reactor should take
+ * a break after sending take_a_break_after messages.
+ */
+reactor Sender(take_a_break_after: u32(10), break_interval: time(400 msec)) {
+ output out: u32;
+ logical action act;
+ state sent_messages: u32(0);
+
+ state take_a_break_after(take_a_break_after);
+ state break_interval(break_interval);
+
+ reaction(startup, act) -> act, out {=
+ // Send a message on out
+ println!("At tag {} sending value {}.\n", ctx.get_tag(), self.sent_messages);
+
+ ctx.set(out, self.sent_messages);
+ self.sent_messages += 1;
+
+ if self.sent_messages < self.take_a_break_after {
+ ctx.schedule(act, Asap);
+ } else {
+ // Take a break
+ self.sent_messages = 0;
+ ctx.schedule(act, After(self.break_interval));
+ }
+ =}
+}
+
+reactor Consumer {
+ input in_: u32;
+ state reaction_invoked_correctly: bool(false);
+ reaction(in_) {=
+ let current_tag = ctx.get_tag();
+
+ if current_tag > tag!(T0 + 10 ms, 9) {
+ // The reaction should not have been called at tags larger than (10 msec, 9)
+ panic!("ERROR: Invoked reaction(in) at tag bigger than shutdown.");
+ } else if current_tag == tag!(T0 + 10 ms, 8) {
+ // Call request_stop() at relative tag (10 msec, 8)
+ println!("Requesting stop.");
+ ctx.request_stop(Asap);
+ return;
+ } else if current_tag == tag!(T0 + 10 ms, 9) {
+ // Check that this reaction is indeed also triggered at (10 msec, 9)
+ self.reaction_invoked_correctly = true;
+ return;
+ }
+ println!("Tag is {}.", current_tag);
+ =}
+
+ reaction(shutdown) {=
+ let current_tag = ctx.get_tag();
+
+ println!("Shutdown invoked at tag {}.\n", current_tag);
+
+ // Check to see if shutdown is called at relative tag (10 msec, 9)
+ if current_tag == tag!(T0 + 10 ms, 9) && self.reaction_invoked_correctly {
+ println!("SUCCESS: successfully enforced stop.");
+ } else if current_tag > tag!(T0 + 10 ms, 9) {
+ panic!("ERROR: Shutdown invoked at tag {}. Failed to enforce timeout at (T0 + 10ms, 9).", current_tag);
+ } else if !self.reaction_invoked_correctly {
+ // Check to see if reactions were called correctly
+ panic!("ERROR: Failed to invoke reaction(in) at tag {}.", current_tag);
+ }
+ =}
+}
+
+main reactor Stop {
+ consumer = new Consumer();
+ producer = new Sender(break_interval = 1 msec);
+
+ producer.out -> consumer.in_;
+}
diff --git a/test/Rust/src/StopCleanup.lf b/test/Rust/src/StopCleanup.lf
new file mode 100644
index 0000000000..c6d60842e1
--- /dev/null
+++ b/test/Rust/src/StopCleanup.lf
@@ -0,0 +1,31 @@
+/* Tests that ports are cleaned up before the shutdown wave executes. */
+
+ target Rust {
+
+ };
+
+reactor Sender {
+ output out: u32;
+ reaction(startup) -> out {=
+ assert_tag_is!(ctx, (T0, 0));
+ ctx.set(out, 43);
+ ctx.request_stop(Asap); // requested for (T0, 1)
+ =}
+}
+
+reactor Consumer {
+ input in_: u32;
+
+ reaction(shutdown) in_ {=
+ assert!(ctx.get(in_).is_none(), "Port should have been cleaned up before shutdown");
+ assert_tag_is!(ctx, (T0, 1));
+ assert!(ctx.get_elapsed_logical_time().is_zero(), "Should be called on startup step");
+ =}
+}
+
+main reactor StopCleanup {
+ consumer = new Consumer();
+ producer = new Sender();
+
+ producer.out -> consumer.in_;
+}
diff --git a/test/Rust/src/StopDuringStartup.lf b/test/Rust/src/StopDuringStartup.lf
new file mode 100644
index 0000000000..225375b1ae
--- /dev/null
+++ b/test/Rust/src/StopDuringStartup.lf
@@ -0,0 +1,18 @@
+// tests that a request_stop called during startup is acted upon.
+
+target Rust {
+ timeout: 30 msec,
+};
+
+main reactor {
+
+ reaction(startup) {=
+ ctx.request_stop(Asap); // requested for (T0, 1)
+ assert_tag_is!(ctx, T0);
+ =}
+
+ reaction(shutdown) {=
+ assert_tag_is!(ctx, (T0, 1));
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/StopIdempotence.lf b/test/Rust/src/StopIdempotence.lf
new file mode 100644
index 0000000000..df29f1982d
--- /dev/null
+++ b/test/Rust/src/StopIdempotence.lf
@@ -0,0 +1,23 @@
+/* Tests that ports are cleaned up before the shutdown wave executes. */
+
+ target Rust {
+ timeout: 30 msec,
+
+ };
+
+main reactor StopIdempotence {
+ state count: u32(0);
+
+ reaction(startup) {=
+ ctx.request_stop(Asap); // requested for (T0, 1)
+ assert_tag_is!(ctx, T0);
+ =}
+
+ reaction(shutdown) {=
+ ctx.request_stop(Asap); // requested for (T0, 1)
+ assert_eq!(self.count, 0, "Shutdown was run several times");
+ self.count += 1;
+ assert_tag_is!(ctx, (T0, 1));
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/StopNoEvent.lf b/test/Rust/src/StopNoEvent.lf
new file mode 100644
index 0000000000..231a486384
--- /dev/null
+++ b/test/Rust/src/StopNoEvent.lf
@@ -0,0 +1,8 @@
+/* Tests that `shutdown` is triggered even if the program exits because of an empty event queue. */
+ target Rust;
+
+main reactor StopNoEvent {
+ reaction(shutdown) {=
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/StopTimeout.lf b/test/Rust/src/StopTimeout.lf
new file mode 100644
index 0000000000..fd66bae3b0
--- /dev/null
+++ b/test/Rust/src/StopTimeout.lf
@@ -0,0 +1,12 @@
+/* Tests that `shutdown` is triggered even if the program exits because of timeout target property. */
+
+ target Rust {
+ timeout: 30 msec,
+ };
+
+main reactor StopTimeout {
+ reaction(shutdown) {=
+ assert_tag_is!(ctx, T0 + 30 ms);
+ println!("Success!"); // if this is not printed the test failed
+ =}
+}
diff --git a/test/Rust/src/StopTopology.lf b/test/Rust/src/StopTopology.lf
new file mode 100644
index 0000000000..6ac224a24f
--- /dev/null
+++ b/test/Rust/src/StopTopology.lf
@@ -0,0 +1,21 @@
+/* Tests that shutdown wave occurs in topological order like a normal wave. */
+
+ target Rust {
+ timeout: 30 msec
+ };
+
+main reactor StopTopology {
+ timer end(30 msec); // collides with timeout
+ state count: u32;
+
+ reaction(end) {=
+ assert_eq!(self.count, 0);
+ self.count += 1;
+ =}
+
+ reaction(shutdown) {=
+ // executes after because of priority
+ assert_eq!(self.count, 1);
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/StructAsState.lf b/test/Rust/src/StructAsState.lf
new file mode 100644
index 0000000000..c253547872
--- /dev/null
+++ b/test/Rust/src/StructAsState.lf
@@ -0,0 +1,25 @@
+// Check that a state variable can have a statically initialized struct as a value.
+// Check how preambles work
+target Rust;
+main reactor StructAsState {
+ preamble {=
+ struct Hello {
+ name: String,
+ value: i32,
+ }
+ =}
+ // notice this uses parentheses
+ // todo
+ // state s: Hello(name= "Earth".into(), value= 42);
+ // state s: Hello(name: "Earth".into(), value: 42);
+ // state s: Hello { name: "Earth".into(), value: 42 };
+ state s: Hello ({= Hello { name: "Earth".into(), value: 42 } =});
+
+ reaction(startup) {=
+ println!("State s.name=\"{}\", s.value={}.", self.s.name, self.s.value);
+ if self.s.value != 42 {
+ eprintln!("FAILED: Expected 42.");
+ std::process::exit(1);
+ }
+ =}
+}
diff --git a/test/Rust/src/StructAsType.lf b/test/Rust/src/StructAsType.lf
new file mode 100644
index 0000000000..bb8acc17e5
--- /dev/null
+++ b/test/Rust/src/StructAsType.lf
@@ -0,0 +1,38 @@
+// Source produces a struct directly, rather than a pointer to
+// a struct.
+target Rust;
+
+reactor Source {
+ output out: Hello;
+
+ preamble {=
+ pub struct Hello {
+ pub name: String,
+ pub value: i32,
+ }
+ =}
+
+ reaction(startup) -> out {=
+ // Create the struct on the stack and then copy
+ // it to the output
+ ctx.set(out, Hello { name: "Earth".into(), value: 42 })
+ =}
+}
+// expected parameter is for testing.
+reactor Print(expected:i32(42)) {
+ input inp: {= super::source::Hello =};
+ state expected:i32(expected);
+ reaction(inp) {=
+ ctx.use_ref_opt(inp, |hello| {
+ println!("Received: name=\"{}\", value={}.", hello.name, hello.value);
+ if hello.value != self.expected {
+ panic!("ERROR: Expected value to be {}.\n", self.expected);
+ }
+ });
+ =}
+}
+main reactor StructAsType {
+ s = new Source();
+ p = new Print();
+ s.out -> p.inp;
+}
diff --git a/test/Rust/src/TimeState.lf b/test/Rust/src/TimeState.lf
new file mode 100644
index 0000000000..751e426064
--- /dev/null
+++ b/test/Rust/src/TimeState.lf
@@ -0,0 +1,13 @@
+target Rust;
+
+reactor Foo {
+ state baz: time(500 msec);
+
+ reaction (startup) {=
+ assert_eq!(500, self.baz.as_millis());
+ =}
+}
+
+main reactor TimeState {
+ a = new Foo();
+}
diff --git a/test/Rust/src/TimerDefaults.lf b/test/Rust/src/TimerDefaults.lf
new file mode 100644
index 0000000000..e08a2051c5
--- /dev/null
+++ b/test/Rust/src/TimerDefaults.lf
@@ -0,0 +1,10 @@
+target Rust;
+main reactor TimerDefaults {
+ state i: i32(0);
+ timer t;
+ reaction(t) {=
+ assert_tag_is!(ctx, T0);
+ println!("Tick {} after {} ms", self.i, ctx.get_elapsed_physical_time().as_millis());
+ self.i += 1;
+ =}
+}
diff --git a/test/Rust/src/TimerIsPresent.lf b/test/Rust/src/TimerIsPresent.lf
new file mode 100644
index 0000000000..7efb0bbf84
--- /dev/null
+++ b/test/Rust/src/TimerIsPresent.lf
@@ -0,0 +1,53 @@
+// Tests the is_present function for timers.
+target Rust {
+ timeout: 7 msec,
+
+};
+
+main reactor {
+
+ timer a(0, 5 msec);
+ timer b(1 msec, 5 msec);
+ timer c(1 msec);
+
+ state success: bool(false);
+ state tick: u32(0);
+
+ reaction(startup, a, b, c) {=
+ match self.tick {
+ 0 => { // startup
+ assert_tag_is!(ctx, T0);
+ assert!(ctx.is_present(a));
+ assert!(!ctx.is_present(b));
+ assert!(!ctx.is_present(c));
+ },
+ 1 => { // 1 msec
+ assert_tag_is!(ctx, T0 + 1 ms);
+ assert!(!ctx.is_present(a));
+ assert!(ctx.is_present(b));
+ assert!(ctx.is_present(c));
+ },
+ 2 => { // 5 msec (a triggers)
+ assert_tag_is!(ctx, T0 + 5 ms);
+ assert!(ctx.is_present(a));
+ assert!(!ctx.is_present(b));
+ assert!(!ctx.is_present(c));
+ },
+ 3 => { // 6 msec (b triggers)
+ assert_tag_is!(ctx, T0 + 6 ms);
+ assert!(!ctx.is_present(a));
+ assert!(ctx.is_present(b));
+ assert!(!ctx.is_present(c));
+ self.success = true;
+ },
+ _ => {
+ unreachable!("unexpected reaction invocation");
+ }
+ }
+ self.tick += 1;
+ =}
+ reaction(shutdown) {=
+ assert!(self.success);
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/TimerPeriodic.lf b/test/Rust/src/TimerPeriodic.lf
new file mode 100644
index 0000000000..375cea45e4
--- /dev/null
+++ b/test/Rust/src/TimerPeriodic.lf
@@ -0,0 +1,18 @@
+target Rust {
+ timeout: 15 msec,
+ // fast: true
+};
+main reactor TimerPeriodic {
+ state i: i32(0);
+ timer t2(0, 3 msec);
+
+ reaction(t2) {=
+ println!("Tick {} at {}", self.i, ctx.get_tag());
+ self.i += 1;
+ =}
+
+ reaction(shutdown) {=
+ assert_eq!(self.i, 6);
+ println!("success");
+ =}
+}
diff --git a/test/Rust/src/Timers.lf b/test/Rust/src/Timers.lf
new file mode 100644
index 0000000000..1fa66dda49
--- /dev/null
+++ b/test/Rust/src/Timers.lf
@@ -0,0 +1,22 @@
+/**
+ * Test whether timers with different periods are triggered correctly.
+ */
+target Rust {
+ timeout: 2 sec
+};
+main reactor Timers {
+ timer t(0, 1 sec);
+ timer t2(0, 2 sec);
+ state counter: i32(0);
+
+ reaction(t2) {=
+ self.counter += 2;
+ =}
+ reaction(t) {=
+ self.counter -= 1;
+ =}
+ reaction(shutdown) {=
+ assert_eq!(1, self.counter);
+ println!("SUCCESS.")
+ =}
+}
diff --git a/test/Rust/src/TypeVarLengthList.lf b/test/Rust/src/TypeVarLengthList.lf
new file mode 100644
index 0000000000..35cb813280
--- /dev/null
+++ b/test/Rust/src/TypeVarLengthList.lf
@@ -0,0 +1,15 @@
+target Rust;
+
+// this thing must compile
+// needs to be modified when https://github.com/lf-lang/lingua-franca/discussions/492 is implemented
+main reactor TypeVarLengthList {
+ state l0: i32[]({= Vec::new() =}); // generates l0: Vec::new()
+ state l1: i32[](1, 2); // generates l1: vec![1, 2]
+ // state l2: i32[](1); // generates l2: 1 // doesn't compile...
+ // state l3: i32[](); // doesn't parse...
+
+
+
+ // state l1: Vec(1, 2); // does not compile...
+
+}
diff --git a/test/Rust/src/generics/CtorParamGeneric.lf b/test/Rust/src/generics/CtorParamGeneric.lf
new file mode 100644
index 0000000000..e8d668ddfe
--- /dev/null
+++ b/test/Rust/src/generics/CtorParamGeneric.lf
@@ -0,0 +1,22 @@
+// tests that ctor parameters may refer to type parameters.
+
+target Rust;
+
+reactor Generic<{= T: Default + Eq + Sync + std::fmt::Debug =}>(value: T({= Default::default() =})) {
+ input in: T;
+ state v: T(value);
+
+ reaction(in) {=
+ ctx.use_ref_opt(r#in, |i| {
+ assert_eq!(&self.v, i);
+ println!("success");
+ });
+ =}
+}
+main reactor {
+ p = new Generic(value=23);
+
+ reaction(startup) -> p.in {=
+ ctx.set(p__in, 23);
+ =}
+}
diff --git a/test/Rust/src/generics/CtorParamGenericInst.lf b/test/Rust/src/generics/CtorParamGenericInst.lf
new file mode 100644
index 0000000000..f8ecf02ef4
--- /dev/null
+++ b/test/Rust/src/generics/CtorParamGenericInst.lf
@@ -0,0 +1,34 @@
+// this one is deeper than CtorParamGeneric, it illustrates
+// the use of ctor parameters within the argument list of a
+// further child instance.
+
+target Rust;
+
+
+reactor Generic2<{= T: Default + Eq + Sync + std::fmt::Debug + 'static =}>(value: T({= Default::default() =})) {
+ input in: T;
+ state v: T(value);
+
+ reaction(in) {=
+ ctx.use_ref_opt(r#in, |i| {
+ assert_eq!(&self.v, i);
+ println!("success");
+ });
+ =}
+}
+
+reactor Generic<{= T: Default + Eq + Sync + std::fmt::Debug + Copy + 'static =}>(value: T({= Default::default() =})) {
+ input in: T;
+
+ inner = new Generic2(value = value);
+
+ in -> inner.in;
+}
+
+main reactor {
+ p = new Generic(value=23);
+
+ reaction(startup) -> p.in {=
+ ctx.set(p__in, 23);
+ =}
+}
diff --git a/test/Rust/src/generics/GenericReactor.lf b/test/Rust/src/generics/GenericReactor.lf
new file mode 100644
index 0000000000..efce59d63e
--- /dev/null
+++ b/test/Rust/src/generics/GenericReactor.lf
@@ -0,0 +1,26 @@
+// Tests a port connection between (input of self -> input of child)
+target Rust;
+
+reactor Box<{= T: Sync =}> {
+ input inp: T;
+ output out: T;
+
+ inp -> out;
+}
+
+main reactor {
+ state done: bool(false);
+
+ box0 = new Box();
+ box1 = new Box();
+
+ box0.out -> box1.inp;
+
+ reaction(startup) -> box0.inp {= ctx.set(box0__inp, 444); =}
+ reaction(box1.out) {= assert!(ctx.get_elapsed_logical_time().is_zero()); self.done = true; =}
+
+ reaction(shutdown) {=
+ assert!(self.done, "reaction was not executed");
+ println!("success");
+ =}
+}