diff --git a/.github/workflows/c-verifier-tests.yml b/.github/workflows/c-verifier-and-static-scheduler-tests.yml similarity index 89% rename from .github/workflows/c-verifier-tests.yml rename to .github/workflows/c-verifier-and-static-scheduler-tests.yml index 8e2ec09915..8fcfbeb28f 100644 --- a/.github/workflows/c-verifier-tests.yml +++ b/.github/workflows/c-verifier-and-static-scheduler-tests.yml @@ -1,4 +1,4 @@ -name: Uclid5-based Verifier Tests +name: Uclid5-based Verifier Tests and Static Scheduler Tests on: # Trigger this workflow also on workflow_call events. @@ -66,6 +66,11 @@ jobs: echo "$pwd" ls -la ./gradlew core:integrationTest --tests org.lflang.tests.runtime.CVerifierTest.* core:integrationTestCodeCoverageReport + - name: Run static scheduler tests + run: | + echo "$pwd" + ls -la + ./gradlew core:integrationTest --tests org.lflang.tests.runtime.CStaticSchedulerTest.* core:integrationTestCodeCoverageReport - name: Report to CodeCov uses: ./.github/actions/report-code-coverage with: diff --git a/.github/workflows/only-c.yml b/.github/workflows/only-c.yml index 32d539fb49..10e8549462 100644 --- a/.github/workflows/only-c.yml +++ b/.github/workflows/only-c.yml @@ -32,6 +32,6 @@ jobs: use-cpp: true all-platforms: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} - # Run the Uclid-based LF Verifier benchmarks. - verifier: - uses: ./.github/workflows/c-verifier-tests.yml + # Run the Uclid-based LF Verifier tests. + verifier-and-static-scheduler: + uses: ./.github/workflows/c-verifier-and-static-scheduler-tests.yml diff --git a/build.gradle b/build.gradle index 4b7923a9fc..c00746ffee 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ spotless { format 'linguaFranca', { addStep(LfFormatStep.create()) target 'test/*/src/**/*.lf' // you have to set the target manually - targetExclude 'test/**/failing/**' + targetExclude 'test/**/failing/**', 'test/*/src/static/*.lf' // FIXME: Spotless does not know how to check static LF tests. } } diff --git a/cli/lfc/src/main/java/org/lflang/cli/Lfc.java b/cli/lfc/src/main/java/org/lflang/cli/Lfc.java index bed839942c..29723304e9 100644 --- a/cli/lfc/src/main/java/org/lflang/cli/Lfc.java +++ b/cli/lfc/src/main/java/org/lflang/cli/Lfc.java @@ -17,13 +17,16 @@ import org.lflang.generator.MainContext; import org.lflang.target.property.BuildTypeProperty; import org.lflang.target.property.CompilerProperty; +import org.lflang.target.property.DashProperty; import org.lflang.target.property.LoggingProperty; import org.lflang.target.property.NoCompileProperty; import org.lflang.target.property.NoSourceMappingProperty; import org.lflang.target.property.PrintStatisticsProperty; import org.lflang.target.property.RuntimeVersionProperty; import org.lflang.target.property.SchedulerProperty; +import org.lflang.target.property.SchedulerProperty.SchedulerOptions; import org.lflang.target.property.SingleThreadedProperty; +import org.lflang.target.property.StaticSchedulerProperty; import org.lflang.target.property.TracingProperty; import org.lflang.target.property.TracingProperty.TracingOptions; import org.lflang.target.property.VerifyProperty; @@ -34,6 +37,8 @@ import org.lflang.target.property.type.LoggingType.LogLevel; import org.lflang.target.property.type.SchedulerType; import org.lflang.target.property.type.SchedulerType.Scheduler; +import org.lflang.target.property.type.StaticSchedulerType; +import org.lflang.target.property.type.StaticSchedulerType.StaticScheduler; import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @@ -141,6 +146,20 @@ public class Lfc extends CliBase { description = "Specify the runtime scheduler (if supported).") private String scheduler; + // FIXME: Add LfcCliTest for this. + @Option( + names = {"--mapper"}, + description = + "Select a specific static scheduler if scheduler is set to STATIC." + + " Options: LB (default), EGS, MOCASIN") + private String staticScheduler; + + // FIXME: Add LfcCliTest for this. + @Option( + names = {"--dash"}, + description = "Execute non-real-time reactions fast whenever possible.") + private Boolean dashMode; + @Option( names = {"--tracing"}, arity = "0", @@ -309,7 +328,7 @@ private URI getRtiUri() { } /** Return a scheduler one has been specified via the CLI arguments, or {@code null} otherwise. */ - private Scheduler getScheduler() { + private SchedulerOptions getScheduler() { Scheduler resolved = null; if (scheduler != null) { // Validate scheduler. @@ -317,6 +336,23 @@ private Scheduler getScheduler() { if (resolved == null) { reporter.printFatalErrorAndExit(scheduler + ": Invalid scheduler."); } + return new SchedulerOptions(resolved); + } + return null; + } + + /** + * Return a static scheduler one has been specified via the CLI arguments, or {@code null} + * otherwise. + */ + private StaticScheduler getStaticScheduler() { + StaticScheduler resolved = null; + if (staticScheduler != null) { + // Validate scheduler. + resolved = new StaticSchedulerType().forName(staticScheduler); + if (resolved == null) { + reporter.printFatalErrorAndExit(staticScheduler + ": Invalid scheduler."); + } } return resolved; } @@ -379,6 +415,7 @@ public GeneratorArguments getArgs() { List.of( new Argument<>(BuildTypeProperty.INSTANCE, getBuildType()), new Argument<>(CompilerProperty.INSTANCE, targetCompiler), + new Argument<>(DashProperty.INSTANCE, dashMode), new Argument<>(LoggingProperty.INSTANCE, getLogging()), new Argument<>(PrintStatisticsProperty.INSTANCE, printStatistics), new Argument<>(NoCompileProperty.INSTANCE, noCompile), @@ -386,6 +423,7 @@ public GeneratorArguments getArgs() { new Argument<>(VerifyProperty.INSTANCE, verify), new Argument<>(RuntimeVersionProperty.INSTANCE, runtimeVersion), new Argument<>(SchedulerProperty.INSTANCE, getScheduler()), + new Argument<>(StaticSchedulerProperty.INSTANCE, getStaticScheduler()), new Argument<>(SingleThreadedProperty.INSTANCE, getSingleThreaded()), new Argument<>(TracingProperty.INSTANCE, getTracingOptions()), new Argument<>(WorkersProperty.INSTANCE, getWorkers()))); diff --git a/core/src/integrationTest/java/org/lflang/tests/runtime/CSchedulerTest.java b/core/src/integrationTest/java/org/lflang/tests/runtime/CSchedulerTest.java index 0e241a1dd8..d9769aaa3c 100644 --- a/core/src/integrationTest/java/org/lflang/tests/runtime/CSchedulerTest.java +++ b/core/src/integrationTest/java/org/lflang/tests/runtime/CSchedulerTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.lflang.target.Target; import org.lflang.target.property.SchedulerProperty; +import org.lflang.target.property.SchedulerProperty.SchedulerOptions; import org.lflang.target.property.type.SchedulerType.Scheduler; import org.lflang.tests.TestBase; import org.lflang.tests.TestRegistry.TestCategory; @@ -56,7 +57,7 @@ private void runTest(Scheduler scheduler, EnumSet categories) { categories::contains, Transformers::noChanges, config -> { - SchedulerProperty.INSTANCE.override(config, scheduler); + SchedulerProperty.INSTANCE.override(config, new SchedulerOptions(scheduler)); return true; }, TestLevel.EXECUTION, diff --git a/core/src/integrationTest/java/org/lflang/tests/runtime/CStaticSchedulerTest.java b/core/src/integrationTest/java/org/lflang/tests/runtime/CStaticSchedulerTest.java new file mode 100644 index 0000000000..11b8c5e74b --- /dev/null +++ b/core/src/integrationTest/java/org/lflang/tests/runtime/CStaticSchedulerTest.java @@ -0,0 +1,42 @@ +package org.lflang.tests.runtime; + +import java.util.List; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.lflang.target.Target; +import org.lflang.target.property.SchedulerProperty; +import org.lflang.target.property.SchedulerProperty.SchedulerOptions; +import org.lflang.target.property.type.SchedulerType.Scheduler; +import org.lflang.target.property.type.StaticSchedulerType; +import org.lflang.tests.TestBase; +import org.lflang.tests.TestRegistry; +import org.lflang.tests.Transformers; + +public class CStaticSchedulerTest extends TestBase { + protected CStaticSchedulerTest() { + super(Target.C); + } + + @Test + public void runStaticSchedulerTests() { + Assumptions.assumeTrue( + isLinux() || isMac(), "Static scheduler tests only run on Linux or macOS"); + + super.runTestsFor( + List.of(Target.C), + Message.DESC_STATIC_SCHEDULER, + TestRegistry.TestCategory.STATIC_SCHEDULER::equals, + Transformers::noChanges, + config -> { + // Execute all static tests using STATIC and LB. + // FIXME: How to respect the config specified in the LF code? + SchedulerProperty.INSTANCE.override( + config, + new SchedulerOptions(Scheduler.STATIC) + .update(StaticSchedulerType.StaticScheduler.LB)); + return true; + }, + TestLevel.EXECUTION, + false); + } +} diff --git a/core/src/main/java/org/lflang/AttributeUtils.java b/core/src/main/java/org/lflang/AttributeUtils.java index 79447cb440..323f56ccd8 100644 --- a/core/src/main/java/org/lflang/AttributeUtils.java +++ b/core/src/main/java/org/lflang/AttributeUtils.java @@ -27,6 +27,7 @@ import static org.lflang.ast.ASTUtils.factory; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -142,6 +143,33 @@ public static String getAttributeValue(EObject node, String attrName) { return value; } + /** + * Return the first argument, which has the type Time, specified for the attribute. + * + *

This should be used if the attribute is expected to have a single argument whose type is + * Time. If there is no argument, null is returned. + */ + public static Time getFirstArgumentTime(Attribute attr) { + if (attr == null || attr.getAttrParms().isEmpty()) { + return null; + } + return attr.getAttrParms().get(0).getTime(); + } + + /** + * Search for an attribute with the given name on the given AST node and return its first argument + * as Time. + * + *

This should only be used on attributes that are expected to have a single argument with type + * Time. + * + *

Returns null if the attribute is not found or if it does not have any arguments. + */ + public static Time getAttributeTime(EObject node, String attrName) { + final var attr = findAttributeByName(node, attrName); + return getFirstArgumentTime(attr); + } + /** * Search for an attribute with the given name on the given AST node and return its first argument * as a String. @@ -241,6 +269,35 @@ public static boolean hasCBody(Reaction reaction) { return findAttributeByName(reaction, "_c_body") != null; } + /** Return a time value that represents the WCET of a reaction. */ + public static List getWCETs(Reaction reaction) { + List wcets = new ArrayList<>(); + String wcetStr = getAttributeValue(reaction, "wcet"); + + if (wcetStr == null) { + wcets.add(TimeValue.MAX_VALUE); + return wcets; + } + + // Split by comma. + String[] wcetArr = wcetStr.split(","); + + // Trim white space. + for (int i = 0; i < wcetArr.length; i++) { + wcetArr[i] = wcetArr[i].trim(); + + // Split by inner space. + String[] valueAndUnit = wcetArr[i].split(" "); + + long value = Long.parseLong(valueAndUnit[0]); + TimeUnit unit = TimeUnit.fromName(valueAndUnit[1]); + TimeValue wcet = new TimeValue(value, unit); + wcets.add(wcet); + } + + return wcets; + } + /** Return the declared label of the node, as given by the @label annotation. */ public static String getLabel(EObject node) { return getAttributeValue(node, "label"); diff --git a/core/src/main/java/org/lflang/LinguaFranca.xtext b/core/src/main/java/org/lflang/LinguaFranca.xtext index 5b1bda9028..7f5678100f 100644 --- a/core/src/main/java/org/lflang/LinguaFranca.xtext +++ b/core/src/main/java/org/lflang/LinguaFranca.xtext @@ -257,7 +257,7 @@ Attribute: ; AttrParm: - (name=ID '=')? value=Literal; + (name=ID '=')? (value=Literal | time=Time); /////////// For target parameters diff --git a/core/src/main/java/org/lflang/analyses/c/AbstractAstVisitor.java b/core/src/main/java/org/lflang/analyses/c/AbstractAstVisitor.java index c279f962d6..d66b538911 100644 --- a/core/src/main/java/org/lflang/analyses/c/AbstractAstVisitor.java +++ b/core/src/main/java/org/lflang/analyses/c/AbstractAstVisitor.java @@ -2,7 +2,11 @@ import java.util.List; -/** Modeled after {@link AbstractParseTreeVisitor}. */ +/** + * Modeled after {@link AbstractParseTreeVisitor}. + * + * @author Shaokai Lin + */ public abstract class AbstractAstVisitor implements AstVisitor { @Override diff --git a/core/src/main/java/org/lflang/analyses/c/AstUtils.java b/core/src/main/java/org/lflang/analyses/c/AstUtils.java index c3ac49b6fd..5d014e7fbb 100644 --- a/core/src/main/java/org/lflang/analyses/c/AstUtils.java +++ b/core/src/main/java/org/lflang/analyses/c/AstUtils.java @@ -4,6 +4,11 @@ import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.misc.Interval; +/** + * A utility class for C ASTs + * + * @author Shaokai Lin + */ public class AstUtils { public static CAst.AstNode takeConjunction(List conditions) { diff --git a/core/src/main/java/org/lflang/analyses/c/AstVisitor.java b/core/src/main/java/org/lflang/analyses/c/AstVisitor.java index d25c9d0364..e090b75a65 100644 --- a/core/src/main/java/org/lflang/analyses/c/AstVisitor.java +++ b/core/src/main/java/org/lflang/analyses/c/AstVisitor.java @@ -2,7 +2,11 @@ import java.util.List; -/** Modeled after ParseTreeVisitor.class */ +/** + * Modeled after ParseTreeVisitor.class + * + * @author Shaokai Lin + */ public interface AstVisitor { /** diff --git a/core/src/main/java/org/lflang/analyses/c/BuildAstParseTreeVisitor.java b/core/src/main/java/org/lflang/analyses/c/BuildAstParseTreeVisitor.java index e8923a2ce4..fe2355da61 100644 --- a/core/src/main/java/org/lflang/analyses/c/BuildAstParseTreeVisitor.java +++ b/core/src/main/java/org/lflang/analyses/c/BuildAstParseTreeVisitor.java @@ -7,7 +7,11 @@ import org.lflang.dsl.CBaseVisitor; import org.lflang.dsl.CParser.*; -/** This visitor class builds an AST from the parse tree of a C program */ +/** + * This visitor class builds an AST from the parse tree of a C program + * + * @author Shaokai Lin + */ public class BuildAstParseTreeVisitor extends CBaseVisitor { /** Message reporter for reporting warnings and errors */ diff --git a/core/src/main/java/org/lflang/analyses/c/CAst.java b/core/src/main/java/org/lflang/analyses/c/CAst.java index 4bb391be00..407fc769bb 100644 --- a/core/src/main/java/org/lflang/analyses/c/CAst.java +++ b/core/src/main/java/org/lflang/analyses/c/CAst.java @@ -3,6 +3,11 @@ import java.util.ArrayList; import java.util.List; +/** + * C AST class that contains definitions for AST nodes + * + * @author Shaokai Lin + */ public class CAst { public static class AstNode implements Visitable { diff --git a/core/src/main/java/org/lflang/analyses/c/CAstVisitor.java b/core/src/main/java/org/lflang/analyses/c/CAstVisitor.java index d3c8f5c28d..c79e84d63b 100644 --- a/core/src/main/java/org/lflang/analyses/c/CAstVisitor.java +++ b/core/src/main/java/org/lflang/analyses/c/CAstVisitor.java @@ -2,7 +2,11 @@ import java.util.List; -/** Modeled after CVisitor.java */ +/** + * Modeled after CVisitor.java + * + * @author Shaokai Lin + */ public interface CAstVisitor extends AstVisitor { T visitAstNode(CAst.AstNode node); diff --git a/core/src/main/java/org/lflang/analyses/c/CBaseAstVisitor.java b/core/src/main/java/org/lflang/analyses/c/CBaseAstVisitor.java index 11c2df3ef6..de605c69ed 100644 --- a/core/src/main/java/org/lflang/analyses/c/CBaseAstVisitor.java +++ b/core/src/main/java/org/lflang/analyses/c/CBaseAstVisitor.java @@ -5,6 +5,8 @@ /** * A base class that provides default implementations of the visit functions. Other C AST visitors * extend this class. + * + * @author Shaokai Lin */ public class CBaseAstVisitor extends AbstractAstVisitor implements CAstVisitor { diff --git a/core/src/main/java/org/lflang/analyses/c/CToUclidVisitor.java b/core/src/main/java/org/lflang/analyses/c/CToUclidVisitor.java index 0608387abb..02cf8b20a6 100644 --- a/core/src/main/java/org/lflang/analyses/c/CToUclidVisitor.java +++ b/core/src/main/java/org/lflang/analyses/c/CToUclidVisitor.java @@ -12,6 +12,11 @@ import org.lflang.generator.StateVariableInstance; import org.lflang.generator.TriggerInstance; +/** + * A visitor class that translates a C AST in If Normal Form to Uclid5 code + * + * @author Shaokai Lin + */ public class CToUclidVisitor extends CBaseAstVisitor { /** The Uclid generator instance */ diff --git a/core/src/main/java/org/lflang/analyses/c/IfNormalFormAstVisitor.java b/core/src/main/java/org/lflang/analyses/c/IfNormalFormAstVisitor.java index d51bec6aad..39300a96e1 100644 --- a/core/src/main/java/org/lflang/analyses/c/IfNormalFormAstVisitor.java +++ b/core/src/main/java/org/lflang/analyses/c/IfNormalFormAstVisitor.java @@ -19,6 +19,8 @@ * is an ill-formed program. * *

In this program, visit() is the normalise() in the paper. + * + * @author Shaokai Lin */ public class IfNormalFormAstVisitor extends CBaseAstVisitor { diff --git a/core/src/main/java/org/lflang/analyses/c/VariablePrecedenceVisitor.java b/core/src/main/java/org/lflang/analyses/c/VariablePrecedenceVisitor.java index 9354e36269..1c3871e555 100644 --- a/core/src/main/java/org/lflang/analyses/c/VariablePrecedenceVisitor.java +++ b/core/src/main/java/org/lflang/analyses/c/VariablePrecedenceVisitor.java @@ -2,7 +2,11 @@ import org.lflang.analyses.c.CAst.*; -/** This visitor marks certain variable node as "previous." */ +/** + * This visitor marks certain variable node as "previous." + * + * @author Shaokai Lin + */ public class VariablePrecedenceVisitor extends CBaseAstVisitor { // This is a temporary solution and cannot handle, diff --git a/core/src/main/java/org/lflang/analyses/c/Visitable.java b/core/src/main/java/org/lflang/analyses/c/Visitable.java index cbdea446ca..d549009cab 100644 --- a/core/src/main/java/org/lflang/analyses/c/Visitable.java +++ b/core/src/main/java/org/lflang/analyses/c/Visitable.java @@ -2,6 +2,11 @@ import java.util.List; +/** + * An interface for Visitable classes, used for AST nodes. + * + * @author Shaokai Lin + */ public interface Visitable { /** The {@link AstVisitor} needs a double dispatch method. */ diff --git a/core/src/main/java/org/lflang/analyses/dag/Dag.java b/core/src/main/java/org/lflang/analyses/dag/Dag.java new file mode 100644 index 0000000000..530e89983f --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/dag/Dag.java @@ -0,0 +1,671 @@ +package org.lflang.analyses.dag; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.eclipse.xtext.xbase.lib.Exceptions; +import org.lflang.TimeValue; +import org.lflang.analyses.pretvm.instructions.Instruction; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.ReactionInstance; + +/** + * Class representing a Directed Acyclic Graph (Dag), useful for the static scheduling. + * + * @author Chadlia Jerad + * @author Shaokai Lin + */ +public class Dag { + + /** + * Array of Dag nodes. It has to be an array, not a set, because nodes can be duplicated at + * different positions. Also because the order helps with the dependency generation. + */ + public ArrayList dagNodes = new ArrayList(); + + /** Array of directed edges. Look up an edge using dagEdges.get(source).get(sink). */ + public HashMap> dagEdges = new HashMap<>(); + + /** + * Array of directed edges in a reverse direction. Look up an edge using + * dagEdges.get(sink).get(source). + */ + public HashMap> dagEdgesRev = new HashMap<>(); + + /** Head of the Dag */ + public DagNode head; + + /** Tail of the Dag */ + public DagNode tail; + + /** + * An array of partitions, where each partition is a set of nodes. The index of the partition is + * the worker ID that owns the partition. + */ + public List> partitions = new ArrayList<>(); + + /** + * A list of worker names that identify specific workers (e.g., core A on board B), with the order + * matching that of partitions + */ + public List workerNames = new ArrayList<>(); + + /** + * Store the dependencies between a downstream node (the map key) and its upstream nodes (the map + * value). The downstream node needs to wait until all of its upstream nodes complete. The static + * scheduler might prune away some information from the raw DAG (e.g., redundant edges). This map + * is used to remember some dependencies that we do not want to forget after the static scheduler + * does its work. These dependencies are later used during instruction generation. + */ + public Map> waitUntilDependencies = new HashMap<>(); + + /** A dot file that represents the diagram */ + private CodeBuilder dot; + + /** A cache of the same Dag nodes sorted in topological order. */ + private List cachedTopologicalSort; + + /** Constructor */ + public Dag() {} + + /** + * Copy constructor. + * + * @param other the Dag object to be copied + */ + public Dag(Dag other) { + // create new collections with the contents of the other Dag + this.dagNodes = new ArrayList<>(other.dagNodes); + this.dagEdges = deepCopyHashMap(other.dagEdges); + this.dagEdgesRev = deepCopyHashMap(other.dagEdgesRev); + this.partitions = new ArrayList<>(); + for (List partition : other.partitions) { + this.partitions.add(new ArrayList<>(partition)); + } + + // copy the head and tail nodes + this.head = other.head; + this.tail = other.tail; + } + + /** + * Deep copies a HashMap>. This is necessary because we want + * the copied Dag to have completely separate collections, and not just separate outer HashMaps + * that contain references to the same inner HashMaps. + * + * @param original the HashMap to be copied + * @return a deep copy of the original HashMap + */ + private static HashMap> deepCopyHashMap( + HashMap> original) { + HashMap> copy = new HashMap<>(); + for (DagNode key : original.keySet()) { + copy.put(key, new HashMap<>(original.get(key))); + } + return copy; + } + + /** + * Add a SYNC or DUMMY node + * + * @param type should be either DYMMY or SYNC + * @param timeStep either the time step or the time + * @return the newly added Dag node + */ + public DagNode addNode(DagNode.dagNodeType type, TimeValue timeStep) { + DagNode dagNode = new DagNode(type, timeStep); + // Add the number of occurrence (i.e., count) to the node. + dagNode.setCount(dagNodes.stream().filter(it -> it.isSynonyous(dagNode)).toList().size()); + this.dagNodes.add(dagNode); + return dagNode; + } + + /** + * Add a REACTION node + * + * @param type should be REACTION + * @param reactionInstance + * @return the newly added Dag node + */ + public DagNode addNode(DagNode.dagNodeType type, ReactionInstance reactionInstance) { + DagNode dagNode = new DagNode(type, reactionInstance); + // Add the number of occurrence (i.e., count) to the node. + dagNode.setCount(dagNodes.stream().filter(it -> it.isSynonyous(dagNode)).toList().size()); + this.dagNodes.add(dagNode); + return dagNode; + } + + /** + * Add an edge to the Dag, where the parameters are two DagNodes. + * + * @param source + * @param sink + */ + public void addEdge(DagNode source, DagNode sink) { + + DagEdge dagEdge = new DagEdge(source, sink); + + if (this.dagEdges.get(source) == null) + this.dagEdges.put(source, new HashMap()); + if (this.dagEdgesRev.get(sink) == null) + this.dagEdgesRev.put(sink, new HashMap()); + + if (this.dagEdges.get(source).get(sink) == null) this.dagEdges.get(source).put(sink, dagEdge); + if (this.dagEdgesRev.get(sink).get(source) == null) + this.dagEdgesRev.get(sink).put(source, dagEdge); + } + + /** + * Add an edge to the Dag, where the parameters are the indexes of two DagNodes in the dagNodes + * array. + * + * @param srcNodeId index of the source DagNode + * @param sinkNodeId index of the sink DagNode + * @return true, if the indexes exist and the edge is added, false otherwise. + */ + public boolean addEdge(int srcNodeId, int sinkNodeId) { + if (srcNodeId < this.dagNodes.size() && sinkNodeId < this.dagNodes.size()) { + // Get the DagNodes + DagNode srcNode = this.dagNodes.get(srcNodeId); + DagNode sinkNode = this.dagNodes.get(sinkNodeId); + // Add the edge + this.addEdge(srcNode, sinkNode); + return true; + } + return false; + } + + /** + * Remove an edge to the Dag, where the parameters are two DagNodes. + * + * @param source + * @param sink + */ + public void removeEdge(DagNode source, DagNode sink) { + if (this.dagEdges.get(source) != null) this.dagEdges.get(source).remove(sink); + if (this.dagEdgesRev.get(sink) != null) this.dagEdgesRev.get(sink).remove(source); + } + + /** Clear all the edges of this DAG. */ + public void clearAllEdges() { + this.dagEdges.clear(); + this.dagEdgesRev.clear(); + } + + /** Add a dependency for WU generation, if two nodes are mapped to different workers. */ + public void addWUDependency(DagNode downstream, DagNode upstream) { + if (waitUntilDependencies.get(downstream) == null) { + waitUntilDependencies.put(downstream, new ArrayList<>(Arrays.asList(upstream))); + } else { + waitUntilDependencies.get(downstream).add(upstream); + } + } + + /** + * Check if the Dag edge and node lists are empty. + * + * @return true, if edge and node arrays are empty, false otherwise + */ + public boolean isEmpty() { + if (this.dagEdges.size() == 0 && this.dagNodes.size() == 0) return true; + return false; + } + + /** + * Check if the edge already exits, based on the nodes indexes + * + * @param srcNodeId index of the source DagNode + * @param sinkNodeId index of the sink DagNode + * @return true, if the edge is already in dagEdges array, false otherwise. + */ + public boolean edgeExists(int srcNodeId, int sinkNodeId) { + if (srcNodeId < this.dagEdges.size() && sinkNodeId < this.dagEdges.size()) { + // Get the DagNodes. + DagNode srcNode = this.dagNodes.get(srcNodeId); + DagNode sinkNode = this.dagNodes.get(sinkNodeId); + HashMap map = this.dagEdges.get(srcNode); + if (map == null) return false; + DagEdge edge = map.get(sinkNode); + if (edge != null) return true; + } + return false; + } + + /** Return an array list of DagEdge */ + public List getDagEdges() { + return dagEdges.values().stream() + .flatMap(innerMap -> innerMap.values().stream()) + .collect(Collectors.toCollection(ArrayList::new)); + } + + /** + * Sort the dag nodes by the topological order, i.e., if node B depends on node A, then A has a + * smaller index than B in the list. + * + * @return A topologically sorted list of dag nodes + */ + public List getTopologicalSort() { + if (cachedTopologicalSort != null) return cachedTopologicalSort; + + cachedTopologicalSort = new ArrayList<>(); + + // Initialize a queue and a map to hold the indegree of each node. + Queue queue = new LinkedList<>(); + Map indegree = new HashMap<>(); + + // Debug info: the index of the current DAG node in the topological sort. + int index = 0; + + // Initialize indegree of all nodes to be the size of their respective upstream node set. + for (DagNode node : this.dagNodes) { + indegree.put(node, this.dagEdgesRev.getOrDefault(node, new HashMap<>()).size()); + // Add the node with zero indegree to the queue. + if (this.dagEdgesRev.getOrDefault(node, new HashMap<>()).size() == 0) { + queue.add(node); + } + } + + // The main loop for traversal using an iterative topological sort. + while (!queue.isEmpty()) { + // Dequeue a node. + DagNode current = queue.poll(); + + // Set debug message. + current.setDotDebugMsg("index=" + index++); + + // Add the node to the sorted list. + cachedTopologicalSort.add(current); + + // Visit each downstream node. + HashMap innerMap = this.dagEdges.get(current); + if (innerMap != null) { + for (DagNode n : innerMap.keySet()) { + // Decrease the indegree of the downstream node. + int updatedIndegree = indegree.get(n) - 1; + indegree.put(n, updatedIndegree); + + // If the downstream node has zero indegree now, add it to the queue. + if (updatedIndegree == 0) { + queue.add(n); + } + } + } + } + return cachedTopologicalSort; + } + + /** + * Generate a dot file from the DAG. + * + * @return a CodeBuilder with the generated code + */ + public CodeBuilder generateDot(List> instructions) { + dot = new CodeBuilder(); + dot.pr("digraph DAG {"); + dot.indent(); + + // Graph settings + dot.pr("fontname=\"Calibri\";"); + dot.pr("rankdir=TB;"); + dot.pr("node [shape = circle, width = 2.5, height = 2.5, fixedsize = true];"); + dot.pr("ranksep=2.0; // Increase distance between ranks"); + dot.pr("nodesep=2.0; // Increase distance between nodes in the same rank"); + + // Define nodes. + ArrayList auxiliaryNodes = new ArrayList<>(); + for (int i = 0; i < dagNodes.size(); i++) { + DagNode node = dagNodes.get(i); + String code = ""; + String label = ""; + if (node.nodeType == DagNode.dagNodeType.SYNC) { + label = "label=\"" + "Sync@" + node.timeStep + "\\n" + "WCET=0 nsec"; + auxiliaryNodes.add(i); + } else if (node.nodeType == DagNode.dagNodeType.DUMMY) { + label = + "label=\"" + + "Dummy=" + + node.timeStep.toNanoSeconds() + + "\\n" + + "WCET=" + + node.timeStep.toNanoSeconds() + + " nsec"; + auxiliaryNodes.add(i); + } else if (node.nodeType == DagNode.dagNodeType.REACTION) { + label = + "label=\"" + + node.nodeReaction.getFullName() + + (node.getWorker() >= 0 ? "\\n" + "Worker=" + node.getWorker() : ""); + for (var wcet : node.nodeReaction.wcets) { + label += "\\n" + "WCET=" + wcet; + } + } else { + // Raise exception. + throw new RuntimeException("UNREACHABLE"); + } + + // Add PretVM instructions. + if (instructions != null && node.nodeType == DagNode.dagNodeType.REACTION) { + int worker = node.getWorker(); + List workerInstructions = instructions.get(worker); + if (node.getInstructions(workerInstructions).size() > 0) label += "\\n" + "Instructions:"; + for (Instruction inst : node.getInstructions(workerInstructions)) { + label += "\\n" + inst.getOpcode() + " (worker " + inst.getWorker() + ")"; + } + } + + // Add debug message, if any. + label += node.getDotDebugMsg().equals("") ? "" : "\\n" + node.getDotDebugMsg(); + // Add node count, if any. + label += node.getCount() >= 0 ? "\\n" + "count=" + node.getCount() : ""; + // Add fillcolor and style + label += "\", fillcolor=\"" + node.getColor() + "\", style=\"filled\""; + + code += i + "[" + label + "]"; + dot.pr(code); + } + + // Align auxiliary nodes. + dot.pr("{"); + dot.indent(); + dot.pr("rank = same;"); + for (Integer i : auxiliaryNodes) { + dot.pr(i + "; "); + } + dot.unindent(); + dot.pr("}"); + + // Add edges + for (DagNode source : this.dagEdges.keySet()) { + HashMap inner = this.dagEdges.get(source); + if (inner != null) { + for (DagNode sink : inner.keySet()) { + int sourceIdx = dagNodes.indexOf(source); + int sinkIdx = dagNodes.indexOf(sink); + dot.pr(sourceIdx + " -> " + sinkIdx); + } + } + } + + dot.unindent(); + dot.pr("}"); + + return this.dot; + } + + /** + * Generate a DOT file without PretVM instructions labeled. + * + * @param filepath Filepath to generate the DOT file. + */ + public void generateDotFile(Path filepath) { + try { + CodeBuilder dot = generateDot(null); + String filename = filepath.toString(); + dot.writeToFile(filename); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Generate a DOT file with PretVM instructions labeled. + * + * @param filepath Filepath to generate the DOT file. + * @param instructions Instructions in a PretVM object file that corresponds to a phase. + */ + public void generateDotFile(Path filepath, List> instructions) { + try { + CodeBuilder dot = generateDot(instructions); + String filename = filepath.toString(); + dot.writeToFile(filename); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Parses the dot file, reads the nodes and edges edges and updates the DAG. Nodes' update + * includes the worker id specification as well as the color. Edges' update removes pruned edges + * and adds the newly generated. + * + *

We assume that the edges follow the pattern: -> . We assume that the + * nodes follow the pattern: // 10[label="Dummy=5ms, WCET=5ms, Worker=0", fillcolor="#FFFFFF", + * style="filled"] + * + *

Furthermore, we assume that the new DAG (contained in the dotFile) will not have unnecessary + * edges, since they are removed by the shceduler. + * + * @param dotFilename + * @return + */ + public boolean updateDag(String dotFileName) throws IOException { + FileReader fileReader; + BufferedReader bufferedReader; + // Read the file + try { + fileReader = new FileReader(dotFileName); + // Buffer the input stream from the file + bufferedReader = new BufferedReader(fileReader); + } catch (IOException e) { + System.out.println("Problem accessing file " + dotFileName + "! " + e); + return false; + } + + String line; + + // Pattern with which an edge starts: + Pattern edgePattern = Pattern.compile("^((\t*)(\s*)(\\d+)(\s*)->(\s*)(\\d+))"); + // 10[label="Dummy=5ms, WCET=5ms, Worker=0", fillcolor="#FFFFFF", style="filled"] + Pattern nodePattern = Pattern.compile("^((\t*)(\s*)(\\d+).label=\")"); + Matcher matcher; + + // Before iterating to search for the edges, we clear the DAG edges array list + this.clearAllEdges(); + + // Search + while ((line = bufferedReader.readLine()) != null) { + matcher = edgePattern.matcher(line); + if (matcher.find()) { + // This line describes an edge + // Start by removing all white spaces. Only the nodes' ids and the + // arrow remain in the string. + line = line.replaceAll("\\s", ""); + line = line.replaceAll("\\t", ""); + + // Remove the label and the ';' that may appear after the edge specification + StringTokenizer st = new StringTokenizer(line, ";"); + line = st.nextToken(); + st = new StringTokenizer(line, "["); + line = st.nextToken(); + + // Use a StringTokenizer to find the source and sink nodes' ids + st = new StringTokenizer(line, "->"); + int srcNodeId, sinkNodeId; + + // Get the source and sink nodes ids and add the edge + try { + srcNodeId = Integer.parseInt(st.nextToken()); + sinkNodeId = Integer.parseInt(st.nextToken()); + this.addEdge(srcNodeId, sinkNodeId); + } catch (NumberFormatException e) { + System.out.println("Parse error in line " + line + " : Expected a number!"); + Exceptions.sneakyThrow(e); + } + } else { + matcher = nodePattern.matcher(line); + if (matcher.find()) { + // This line describes a node + // Start by removing all white spaces. + line = line.replaceAll("\\s", ""); + line = line.replaceAll("\\t", ""); + + // Retreive the node id + StringTokenizer st = new StringTokenizer(line, "["); + int nodeId = Integer.parseInt(st.nextToken()); + // Sanity check, that the node exists + if (nodeId >= this.dagNodes.size()) { + // FIXME: Rise an exception? + System.out.println("Node index does not " + line + " : Expected a number!"); + } + + // Get what remains in the line + line = st.nextToken(); + + // Get the worker + String[] tokensToGetWorker = line.split("Worker="); + st = new StringTokenizer(tokensToGetWorker[1], "\""); + int worker = Integer.parseInt(st.nextToken()); + + // Set the node's worker + this.dagNodes.get(nodeId).setWorker(worker); + } + } + } + bufferedReader.close(); + return true; + } + + /** + * Check if the graph is a valid DAG (i.e., it is acyclic). + * + * @return true if the graph is a valid DAG, false otherwise. + */ + public boolean isValidDAG() { + HashSet whiteSet = new HashSet<>(); + HashSet graySet = new HashSet<>(); + HashSet blackSet = new HashSet<>(); + + // Counter for unique IDs + int[] counter = {0}; // Using an array to allow modification inside the DFS method + + // Initially all nodes are in white set + whiteSet.addAll(dagNodes); + + while (whiteSet.size() > 0) { + DagNode current = whiteSet.iterator().next(); + if (dfs(current, whiteSet, graySet, blackSet, counter)) { + return false; + } + } + + return true; + } + + /** + * Modified DFS method to assign unique IDs to the nodes. + * + * @param current The current node + * @param whiteSet Set of unvisited nodes + * @param graySet Set of nodes currently being visited + * @param blackSet Set of visited nodes + * @param counter Array containing the next unique ID to be assigned + * @return true if a cycle is found, false otherwise + */ + private boolean dfs( + DagNode current, + HashSet whiteSet, + HashSet graySet, + HashSet blackSet, + int[] counter) { + + // Move current to gray set + moveVertex(current, whiteSet, graySet); + + // Visit all neighbors + HashMap neighbors = dagEdges.get(current); + if (neighbors != null) { + for (DagNode neighbor : neighbors.keySet()) { + // If neighbor is in black set, it means it's already explored, so continue. + if (blackSet.contains(neighbor)) { + continue; + } + // If neighbor is in gray set then a cycle is found. + if (graySet.contains(neighbor)) { + return true; + } + if (dfs(neighbor, whiteSet, graySet, blackSet, counter)) { + return true; + } + } + } + + // Move current to black set and return false + moveVertex(current, graySet, blackSet); + return false; + } + + /** + * Move a vertex from one set to another. + * + * @param vertex The vertex to move + * @param source The source set + * @param destination The destination set + */ + private void moveVertex(DagNode vertex, HashSet source, HashSet destination) { + source.remove(vertex); + destination.add(vertex); + } + + /** Removes redundant edges based on transitive dependencies. */ + public void removeRedundantEdges() { + List topoSortedNodes = this.getTopologicalSort(); + Set redundantEdges = new HashSet<>(); + + // Map each node to its descendants (transitive closure) + Map> descendants = new HashMap<>(); + for (DagNode node : topoSortedNodes) { + descendants.put(node, new HashSet<>()); + } + + // Populate the descendants map using the topological sort + for (DagNode u : topoSortedNodes) { + Set directDescendants = this.dagEdges.getOrDefault(u, new HashMap<>()).keySet(); + Set allDescendants = descendants.get(u); + for (DagNode v : directDescendants) { + allDescendants.add(v); + allDescendants.addAll(descendants.getOrDefault(v, Collections.emptySet())); + } + // Update the descendants of nodes leading to u + for (DagNode precursor : this.dagEdgesRev.getOrDefault(u, new HashMap<>()).keySet()) { + descendants.get(precursor).addAll(allDescendants); + } + } + + // Identify redundant edges + for (DagNode u : topoSortedNodes) { + Set uDescendants = descendants.get(u); + for (DagNode v : new HashSet<>(uDescendants)) { + if (this.dagEdges.getOrDefault(u, new HashMap<>()).containsKey(v)) { + // Check for intermediate nodes + for (DagNode intermediate : uDescendants) { + if (this.dagEdges.getOrDefault(intermediate, new HashMap<>()).containsKey(v)) { + // If such an intermediate exists, the edge u->v is redundant + redundantEdges.add(this.dagEdges.get(u).get(v)); + break; + } + } + } + } + } + + // Remove identified redundant edges + for (DagEdge edge : redundantEdges) { + this.removeEdge(edge.sourceNode, edge.sinkNode); + } + } +} diff --git a/core/src/main/java/org/lflang/analyses/dag/DagEdge.java b/core/src/main/java/org/lflang/analyses/dag/DagEdge.java new file mode 100644 index 0000000000..b96e59784b --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/dag/DagEdge.java @@ -0,0 +1,28 @@ +package org.lflang.analyses.dag; + +/** + * Class defining a Dag edge. + * + * @author Shaokai Lin + */ +public class DagEdge { + /** The source DAG node */ + public DagNode sourceNode; + + /** The sink DAG node */ + public DagNode sinkNode; + + //////////////////////////////////////// + //// Public constructor + + /** + * Contructor of a DAG edge + * + * @param source the source DAG node + * @param sink the sink DAG node + */ + public DagEdge(DagNode source, DagNode sink) { + this.sourceNode = source; + this.sinkNode = sink; + } +} diff --git a/core/src/main/java/org/lflang/analyses/dag/DagGenerator.java b/core/src/main/java/org/lflang/analyses/dag/DagGenerator.java new file mode 100644 index 0000000000..007c3c7f89 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/dag/DagGenerator.java @@ -0,0 +1,396 @@ +package org.lflang.analyses.dag; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Set; +import java.util.stream.Collectors; +import org.lflang.TimeUnit; +import org.lflang.TimeValue; +import org.lflang.analyses.statespace.StateSpaceDiagram; +import org.lflang.analyses.statespace.StateSpaceExplorer.Phase; +import org.lflang.analyses.statespace.StateSpaceNode; +import org.lflang.generator.DeadlineInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.c.CFileConfig; +import org.lflang.util.Pair; + +/** + * Constructs a Directed Acyclic Graph (DAG) from the State Space Diagram. This is part of the + * static schedule generation. + * + *

FIXME: DAG generation does not need to be stateful. The methods in this class can be + * refactored into static methods. + * + *

=========== Algorithm Summary =========== + * + *

The DagGenerator class is responsible for creating a Directed Acyclic Graph (DAG) from a given + * state space diagram. The primary purpose of this DAG is to represent the static schedule of + * reactions in a reactor-based program, considering their logical time, dependencies, and + * priorities. + * + *

Key Steps in the DAG Generation: + * + *

1. **Initialization**: - The generator initializes a DAG structure, sets up the head node of + * the state space diagram, and manages variables like logical time and SYNC nodes to track the flow + * of execution. - Various lists are used to track unconnected reaction nodes for processing later. + * + *

2. **SYNC Node Creation**: - For each node in the state space, a SYNC node is added to the DAG + * to represent the logical time of that state. If it's not the first SYNC node, a "dummy" node is + * created to account for the time difference between SYNC nodes and to ensure the correct order of + * execution. + * + *

3. **Reaction Nodes**: - Reactions invoked at the current state are added to the DAG as + * reaction nodes. These nodes are connected to the SYNC node, marking the time when the reactions + * are triggered. + * + *

4. **Priority-based Edges**: - Edges between reaction nodes are created based on their + * priorities. This step ensures the correct order of execution for reactions within the same + * reactor, according to their priority levels. + * + *

5. **Data Dependencies**: - The generator tracks dependencies between reactions, including + * those with delays. It maintains a map of unconnected upstream reaction nodes, which are later + * connected when the corresponding downstream reactions are encountered at the appropriate logical + * time. + * + *

6. **Cross-Time Dependencies of the Same Reaction**: - To maintain determinism across time + * steps, the generator connects reactions that are invoked over multiple time steps. This includes + * adding edges between earlier and current invocations of the same reaction. + * + *

7. **Loop Detection and Stop Conditions**: - If the state space diagram is cyclic, the + * algorithm detects when the loop has been completed by revisiting the loop node. It terminates the + * processing after encountering the loop node a second time. + * + *

8. **Final SYNC Node**: - After all nodes in the state space diagram are processed, a final + * SYNC node is added. This node represents the logical time at which the last event or state + * transition occurs in the diagram. + * + *

9. **Completion**: - The DAG is finalized by adding edges from any remaining unconnected + * reaction nodes to the last SYNC node. This ensures all nodes are correctly linked, and the last + * SYNC node is marked as the tail of the DAG. + * + *

The result is a time-sensitive DAG that respects logical dependencies, time constraints, and + * priority rules, enabling deterministic execution of the reactor system. + * + *

========================================= + * + * @author Chadlia Jerad + * @author Shaokai Lin + */ +public class DagGenerator { + + /** File config */ + public final CFileConfig fileConfig; + + /** + * Constructor. Sets the main reactor and initializes the dag + * + * @param main main reactor instance + */ + public DagGenerator(CFileConfig fileConfig) { + this.fileConfig = fileConfig; + } + + /** + * The state space diagram, together with the lf program topology and priorities, are used to + * generate the Dag. Only state space diagrams without loops or without an initialization phase + * can successfully generate DAGs. + */ + public Dag generateDag(StateSpaceDiagram stateSpaceDiagram) { + /////// Variables + // The Directed Acyclic Graph (DAG) being constructed, which will + // represent the static schedule. + Dag dag = new Dag(); + // The current node being processed in the state space diagram. It + // starts with the head of the state space. + StateSpaceNode currentStateSpaceNode = stateSpaceDiagram.head; + // The logical time of the previous SYNC node. This is used to + // calculate the time difference between consecutive SYNC nodes. + TimeValue previousTime = TimeValue.ZERO; + // The SYNC node generated for the previous time step. Initially set + // to null as there is no previous SYNC at the start. + DagNode previousSync = null; + // The time offset for normalizing the time values across the state + // space diagram. It is initialized to the time of the first state + // space node, so that the DAG's first SYNC node always start at t=0. + TimeValue timeOffset = stateSpaceDiagram.head.getTime(); + // A counter to track how many times the loop node has been + // encountered in cyclic state space diagrams. It is used to + // terminate the loop correctly. + // Only used when the diagram is cyclic. + int loopNodeCounter = 0; + // A list to store the reaction nodes that are invoked at the + // current state space node and will be connected to the current + // SYNC node. + List currentReactionNodes = new ArrayList<>(); + // A list to hold DagNode objects representing reactions that are + // not connected to any SYNC node. + List reactionsUnconnectedToSync = new ArrayList<>(); + // A list to hold reaction nodes that need to be connected to future + // invocations of the same reaction across different time steps. + List reactionsUnconnectedToNextInvocation = new ArrayList<>(); + // A priority queue for sorting all SYNC nodes based on timestamp. + PriorityQueue syncNodesPQueue = new PriorityQueue(); + // A set of reaction nodes with deadlines + Set reactionNodesWithDeadlines = new HashSet<>(); + // Local variable for tracking the current SYNC node. + DagNode sync = null; + + // A map used to track unconnected upstream DAG nodes for reaction + // invocations. For example, when we encounter DAG node N_A (for reaction A + // invoked at t=0), and from the LF program, we know that A could send an + // event to B with a 10 msec delay and another event to C with a 20 msec + // delay, in this map, we will have two entries {(B, 10 msec) -> N_A, (C, 20 + // msec) -> N_A}. When we later visit a DAG node N_X that matches any of the + // key, we can draw an edge N_A -> N_X. + // The map value is a DagNode list because multiple upstream dag nodes can + // be looking for the same node matching the criteria. + Map, List> unconnectedUpstreamDagNodes = + new HashMap<>(); + + // FIXME: Check if a DAG can be generated for the given state space diagram. + // Only a diagram without a loop or a loopy diagram without an + // initialization phase can generate the DAG. + + while (true) { + // Check stop conditions based on whether the diagram is cyclic or not. + if (stateSpaceDiagram.isCyclic()) { + // If the current node is the loop node. + // The stop condition is when the loop node is encountered the 2nd time. + if (currentStateSpaceNode == stateSpaceDiagram.loopNode) { + loopNodeCounter++; + if (loopNodeCounter >= 2) break; + } + } else { + if (currentStateSpaceNode == null) break; + } + + // Get the current logical time. Or, if this is the last iteration, + // set the loop period as the logical time. + TimeValue time = currentStateSpaceNode.getTime().subtract(timeOffset); + + // Add a SYNC node. + sync = addSyncNodeToDag(dag, time, syncNodesPQueue); + if (dag.head == null) dag.head = sync; + + // Add reaction nodes, as well as the edges connecting them to SYNC. + currentReactionNodes.clear(); + for (ReactionInstance reaction : currentStateSpaceNode.getReactionsInvoked()) { + DagNode reactionNode = dag.addNode(DagNode.dagNodeType.REACTION, reaction); + currentReactionNodes.add(reactionNode); + dag.addEdge(sync, reactionNode); + reactionNode.setAssociatedSyncNode(sync); + // If the reaction has a deadline, add it to the set. + if (reaction.declaredDeadline != null) reactionNodesWithDeadlines.add(reactionNode); + } + + // Add edges based on reaction priorities. + for (DagNode n1 : currentReactionNodes) { + for (DagNode n2 : currentReactionNodes) { + // Add an edge for reactions in the same reactor based on priorities. + // This adds the remaining dependencies not accounted for in + // dependentReactions(), e.g., reaction 3 depends on reaction 1 in the + // same reactor. + if (n1.nodeReaction.getParent() == n2.nodeReaction.getParent() + && n1.nodeReaction.index < n2.nodeReaction.index) { + dag.addEdge(n1, n2); + } + } + } + + // Update the unconnectedUpstreamDagNodes map. + for (DagNode reactionNode : currentReactionNodes) { + ReactionInstance reaction = reactionNode.nodeReaction; + var downstreamReactionsSet = reaction.downstreamReactions(); + for (var pair : downstreamReactionsSet) { + ReactionInstance downstreamReaction = pair.first(); + Long expectedTime = pair.second() + time.toNanoSeconds(); + TimeValue expectedTimeValue = TimeValue.fromNanoSeconds(expectedTime); + Pair _pair = + new Pair(downstreamReaction, expectedTimeValue); + // Check if the value is empty. + List list = unconnectedUpstreamDagNodes.get(_pair); + if (list == null) + unconnectedUpstreamDagNodes.put(_pair, new ArrayList<>(Arrays.asList(reactionNode))); + else list.add(reactionNode); + // System.out.println(reactionNode + " looking for: " + downstreamReaction + " @ " + + // expectedTimeValue); + } + } + // Add edges based on connections (including the delayed ones) + // using unconnectedUpstreamDagNodes. + for (DagNode reactionNode : currentReactionNodes) { + ReactionInstance reaction = reactionNode.nodeReaction; + var searchKey = new Pair(reaction, time); + // System.out.println("Search key: " + reaction + " @ " + time); + List upstreams = unconnectedUpstreamDagNodes.get(searchKey); + if (upstreams != null) { + for (DagNode us : upstreams) { + dag.addEdge(us, reactionNode); + dag.addWUDependency(reactionNode, us); + // System.out.println("Match!"); + } + } + } + + // Create a list of ReactionInstances from currentReactionNodes. + ArrayList currentReactions = + currentReactionNodes.stream() + .map(DagNode::getReaction) + .collect(Collectors.toCollection(ArrayList::new)); + + // If there is a newly released reaction found and its prior + // invocation is not connected to a downstream SYNC node, + // connect it to a downstream SYNC node to + // preserve a deterministic order. In other words, + // check if there are invocations of the same reaction across two + // time steps, if so, connect the previous invocation to the current + // SYNC node. + // + // FIXME: This assumes that the (conventional) completion deadline is the + // period. We need to find a way to integrate LF deadlines into + // the picture. + ArrayList toRemove = new ArrayList<>(); + for (DagNode n : reactionsUnconnectedToSync) { + if (currentReactions.contains(n.nodeReaction)) { + dag.addEdge(n, sync); + toRemove.add(n); + } + } + reactionsUnconnectedToSync.removeAll(toRemove); + reactionsUnconnectedToSync.addAll(currentReactionNodes); + + // Check if there are invocations of reactions from the same reactor + // across two time steps. If so, connect invocations from the + // previous time step to those in the current time step, in order to + // preserve determinism. + ArrayList toRemove2 = new ArrayList<>(); + for (DagNode n1 : reactionsUnconnectedToNextInvocation) { + for (DagNode n2 : currentReactionNodes) { + ReactorInstance r1 = n1.getReaction().getParent(); + ReactorInstance r2 = n2.getReaction().getParent(); + if (r1.equals(r2)) { + dag.addEdge(n1, n2); + toRemove2.add(n1); + } + } + } + reactionsUnconnectedToNextInvocation.removeAll(toRemove2); + reactionsUnconnectedToNextInvocation.addAll(currentReactionNodes); + + // Move to the next state space node. + currentStateSpaceNode = stateSpaceDiagram.getDownstreamNode(currentStateSpaceNode); + previousSync = sync; + previousTime = time; + } + + TimeValue time; + if (stateSpaceDiagram.isCyclic()) { + // Set the time of the last SYNC node to be the hyperperiod. + time = new TimeValue(stateSpaceDiagram.hyperperiod, TimeUnit.NANO); + } else { + // Set the time of the last SYNC node to be the tag of the first pending + // event in the tail node of the state space diagram. + // Assumption: this assumes that the heap-to-arraylist convertion puts the + // earliest event in the first location in arraylist. + if (stateSpaceDiagram.phase == Phase.INIT + && stateSpaceDiagram.tail.getEventQcopy().size() > 0) { + time = + new TimeValue( + stateSpaceDiagram.tail.getEventQcopy().get(0).getTag().timestamp, TimeUnit.NANO); + } + // If there are no pending events, set the time of the last SYNC node to + // forever. This is just a convention for building DAGs. In reality, we do + // not want to generate any DU instructions when we see the tail node has + // TimeValue.MAX_VALUE. + else time = TimeValue.MAX_VALUE; + } + + // Add a SYNC node when (1) the state space is cyclic and we + // encounter the loop node for the 2nd time + // or (2) the state space is a chain and we are at the end of the + // end of the chain. + sync = addSyncNodeToDag(dag, time, syncNodesPQueue); + // If we still don't have a head node at this point, make it the + // head node. This might happen when a reactor has no reactions. + // FIXME: Double check if this is the case. + if (dag.head == null) dag.head = sync; + + // Add edges from existing reactions to the last node. + for (DagNode n : reactionsUnconnectedToSync) { + dag.addEdge(n, sync); + } + + ///// Deadline handling ///// + // For each reaction that has a deadline, create a SYNC node and + // point the reaction node to the SYNC node. + for (DagNode reactionNode : reactionNodesWithDeadlines) { + // The associated SYNC node contains the timestamp at which the + // reaction is invoked. We add the deadline value to the timestamp + // to get the deadline time. + DeadlineInstance deadline = reactionNode.nodeReaction.declaredDeadline; + TimeValue deadlineValue = deadline.maxDelay; + DagNode associatedSync = reactionNode.getAssociatedSyncNode(); + TimeValue deadlineTime = associatedSync.timeStep.add(deadlineValue); + DagNode syncNode = addSyncNodeToDag(dag, deadlineTime, syncNodesPQueue); + // Add an edge from the reaction node to the SYNC node. + dag.addEdge(reactionNode, syncNode); + } + ///////////////////////////// + + // At this point, all SYNC nodes should have been generated. + // Sort the SYNC nodes based on their time steps by polling from the + // priority queue. + DagNode upstreamSyncNode = null; + DagNode downstreamSyncNode = null; + while (!syncNodesPQueue.isEmpty()) { + // The previous downstream SYNC node becomes the upstream SYNC node. + upstreamSyncNode = downstreamSyncNode; + // The next downstream SYNC node is the next node in the priority queue. + downstreamSyncNode = syncNodesPQueue.poll(); + // Add dummy nodes between every pair of SYNC nodes. + if (upstreamSyncNode != null) + createDummyNodeBetweenTwoSyncNodes(dag, upstreamSyncNode, downstreamSyncNode); + } + + // assign the last SYNC node as tail. + dag.tail = downstreamSyncNode; + + return dag; + } + + /** + * Create a DUMMY node and place it between an upstream SYNC node and a downstream SYNC node. + * + * @param dag The DAG to be updated + * @param upstreamSync The SYNC node with an earlier tag + * @param downstreamSync The SYNC node with a later tag + */ + private void createDummyNodeBetweenTwoSyncNodes( + Dag dag, DagNode upstreamSync, DagNode downstreamSync) { + TimeValue timeDiff = downstreamSync.timeStep.subtract(upstreamSync.timeStep); + DagNode dummy = dag.addNode(DagNode.dagNodeType.DUMMY, timeDiff); + dag.addEdge(upstreamSync, dummy); + dag.addEdge(dummy, downstreamSync); + } + + /** + * Helper function for adding a SYNC node to a DAG. + * + * @param dag The DAG to be updated + * @param time The timestamp of the SYNC node + * @param syncNodesPQueue The priority queue to add the sync node to + * @return a newly created SYNC node + */ + private DagNode addSyncNodeToDag( + Dag dag, TimeValue time, PriorityQueue syncNodesPQueue) { + DagNode sync = dag.addNode(DagNode.dagNodeType.SYNC, time); + syncNodesPQueue.add(sync); // Track the node in the priority queue. + return sync; + } +} diff --git a/core/src/main/java/org/lflang/analyses/dag/DagNode.java b/core/src/main/java/org/lflang/analyses/dag/DagNode.java new file mode 100644 index 0000000000..dda1407bb8 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/dag/DagNode.java @@ -0,0 +1,192 @@ +package org.lflang.analyses.dag; + +import java.util.List; +import org.lflang.TimeValue; +import org.lflang.analyses.pretvm.instructions.Instruction; +import org.lflang.generator.ReactionInstance; + +/** + * Class defining a Dag node. + * + *

FIXME: Create subclasses for ReactionNode and SyncNode + * + * @author Chadlia Jerad + * @author Shaokai Lin + */ +public class DagNode implements Comparable { + /** Different node types of the DAG */ + public enum dagNodeType { + DUMMY, + SYNC, + REACTION + } + + /** + * An integer that counts the number of times the same node has occured in the graph. The value 0 + * means unassigned. + */ + public int count = 0; + + /** Node type */ + public dagNodeType nodeType; + + /** If the node type is REACTION, then point the reaction */ + public ReactionInstance nodeReaction; + + /** If the node type is Dummy or SYNC, then store the time step, respectiveley time */ + public TimeValue timeStep; + + /** + * Worker ID that owns this node, if this node is a reaction node. The value -1 means unassigned. + */ + private int worker = -1; + + /** Color of the node for DOT graph */ + private String hexColor = "#FFFFFF"; + + /** + * A DAG node can be associated with a SYNC node, indicating the "release time" of the current + * node. The SYNC node is one with the maximum tag among all of the upstream SYNC nodes wrt the + * current node. + */ + private DagNode associatedSyncNode; + + /** A debug message in the generated DOT */ + private String dotDebugMsg = ""; + + /** + * If the dag node is a REACTION node and there is another node owned by another worker waiting + * for the current reaction node to finish, the release value is the number assigned an WU + * instruction executed by the other worker. The other worker needs to wait until the counter of + * this worker, who owns this reaction node, reaches releaseValue. We store this information + * inside a dag node. This value is assigned only after partitions have been determined. + */ + private Long releaseValue; + + /** A list of PretVM instructions generated for this DAG node. */ + // private List instructions = new ArrayList<>(); + + /** + * Constructor. Useful when it is a SYNC or DUMMY node. + * + * @param type node type + * @param timeStep if the type is DYMMY or SYNC, then record the value + */ + public DagNode(dagNodeType type, TimeValue timeStep) { + this.nodeType = type; + this.timeStep = timeStep; + } + + /** + * Constructor. Useful when it is a REACTION node. + * + * @param type node type + * @param reactionInstance reference to the reaction + */ + public DagNode(dagNodeType type, ReactionInstance reactionInstance) { + this.nodeType = type; + this.nodeReaction = reactionInstance; + } + + public ReactionInstance getReaction() { + return this.nodeReaction; + } + + public String getColor() { + return this.hexColor; + } + + public void setColor(String hexColor) { + this.hexColor = hexColor; + } + + public int getWorker() { + return this.worker; + } + + public void setWorker(int worker) { + this.worker = worker; + } + + public String getDotDebugMsg() { + return this.dotDebugMsg; + } + + public void setDotDebugMsg(String msg) { + this.dotDebugMsg = msg; + } + + public boolean isAuxiliary() { + return (nodeType == dagNodeType.SYNC || nodeType == dagNodeType.DUMMY); + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public DagNode getAssociatedSyncNode() { + return associatedSyncNode; + } + + public void setAssociatedSyncNode(DagNode syncNode) { + this.associatedSyncNode = syncNode; + } + + public Long getReleaseValue() { + return releaseValue; + } + + public void setReleaseValue(Long value) { + releaseValue = value; + } + + /** + * Get instructions generated by this node for a specific worker's instructions. It is important + * to note that nodes do NOT memorize instructions internally, because the instructions and their + * order change in the schedule during optimization passes. So source of truth should come from + * the workerInstructions list. Each instruction memorizes the node it belongs to. When we want to + * query a list of instructions owned by a node, a _view_ of workerInstructions is generated to + * collect instructions which belong to that node. + */ + public List getInstructions(List workerInstructions) { + return workerInstructions.stream().filter(it -> it.getDagNode() == this).toList(); + } + + /** + * A node is synonymous with another if they have the same nodeType, timeStep, and nodeReaction. + */ + public boolean isSynonyous(DagNode that) { + if (this.nodeType == that.nodeType + && (this.timeStep == that.timeStep + || (this.timeStep != null + && that.timeStep != null + && this.timeStep.compareTo(that.timeStep) == 0)) + && this.nodeReaction == that.nodeReaction) return true; + return false; + } + + /** + * Compare two dag nodes based on their timestamps. + * + * @param other The other dag node to compare against. + * @return -1 if this node has an earlier timestamp than that node, 1 if that node has an earlier + * timestamp than this node, 0 if they have the same timestamp. + */ + @Override + public int compareTo(DagNode that) { + return TimeValue.compare(this.timeStep, that.timeStep); + } + + @Override + public String toString() { + return nodeType + + " node" + + (this.timeStep == null ? "" : " @ " + this.timeStep) + + (this.getReaction() == null ? "" : " for " + this.getReaction()) + + (this.count == -1 ? "" : " (count: " + this.count + ")"); + } +} diff --git a/core/src/main/java/org/lflang/analyses/dag/DagNodePair.java b/core/src/main/java/org/lflang/analyses/dag/DagNodePair.java new file mode 100644 index 0000000000..ae418e8bcc --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/dag/DagNodePair.java @@ -0,0 +1,16 @@ +package org.lflang.analyses.dag; + +/** + * A helper class defining a pair of DAG nodes + * + * @author Shaokai Lin + */ +public class DagNodePair { + public DagNode key; + public DagNode value; + + public DagNodePair(DagNode key, DagNode value) { + this.key = key; + this.value = value; + } +} diff --git a/core/src/main/java/org/lflang/analyses/opt/DagBasedOptimizer.java b/core/src/main/java/org/lflang/analyses/opt/DagBasedOptimizer.java new file mode 100644 index 0000000000..935cf3f2de --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/opt/DagBasedOptimizer.java @@ -0,0 +1,205 @@ +package org.lflang.analyses.opt; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.lflang.analyses.dag.Dag; +import org.lflang.analyses.dag.DagNode; +import org.lflang.analyses.dag.DagNode.dagNodeType; +import org.lflang.analyses.pretvm.PretVmLabel; +import org.lflang.analyses.pretvm.PretVmObjectFile; +import org.lflang.analyses.pretvm.Registers; +import org.lflang.analyses.pretvm.instructions.Instruction; +import org.lflang.analyses.pretvm.instructions.InstructionJAL; +import org.lflang.analyses.pretvm.instructions.InstructionJALR; +import org.lflang.analyses.statespace.StateSpaceExplorer.Phase; + +public class DagBasedOptimizer extends PretVMOptimizer { + + // FIXME: Store the number of workers set in the DAG, instead of passing in + // from the outside separately. + public static void optimize(PretVmObjectFile objectFile, int workers, Registers registers) { + // A list of list of nodes, where each list of nodes can be factored + // into a procedure. The index of the list is procedure index. + List> equivalenceClasses = new ArrayList<>(); + + // For a quick lookup, map each node to its procedure index. + Map nodeToProcedureIndexMap = new HashMap<>(); + + // Populate the above data structures. + populateEquivalenceClasses(objectFile, equivalenceClasses, nodeToProcedureIndexMap); + + // Factor out procedures. + factorOutProcedures( + objectFile, registers, equivalenceClasses, nodeToProcedureIndexMap, workers); + } + + /** + * Finds equivalence classes in the bytecode, i.e., lists of nodes with identical generated + * instructions. + */ + private static void populateEquivalenceClasses( + PretVmObjectFile objectFile, + List> equivalenceClasses, + Map nodeToProcedureIndexMap) { + // Iterate over the topological sort of the DAG and find nodes that + // produce the same instructions. + Dag dag = objectFile.getDag(); + for (DagNode node : dag.getTopologicalSort()) { + // Only consider reaction nodes because they generate instructions. + if (node.nodeType != dagNodeType.REACTION) continue; + // Get the worker assigned to the node. + int worker = node.getWorker(); + // Get the worker instructions. + List workerInstructions = objectFile.getContent().get(worker); + // Set up a flag. + boolean matched = false; + // Check if the node matches any known equivalence class. + for (int i = 0; i < equivalenceClasses.size(); i++) { + List list = equivalenceClasses.get(i); + DagNode listHead = list.get(0); + if (node.getInstructions(workerInstructions) + .equals(listHead.getInstructions(workerInstructions))) { + list.add(node); + matched = true; + nodeToProcedureIndexMap.put(node, i); + break; + } + } + // If a node does not match with any existing nodes, + // start a new list. + if (!matched) { + equivalenceClasses.add(new ArrayList<>(List.of(node))); + nodeToProcedureIndexMap.put(node, equivalenceClasses.size() - 1); + } + } + } + + /** Factor our each procedure. This method works at the level of object files (phases). */ + private static void factorOutProcedures( + PretVmObjectFile objectFile, + Registers registers, + List> equivalenceClasses, + Map nodeToProcedureIndexMap, + int workers) { + // Get the partitioned DAG and Phase. + Dag dag = objectFile.getDag(); + Phase phase = objectFile.getFragment().getPhase(); + + // Instantiate a list of updated instructions + List> updatedInstructions = new ArrayList<>(); + for (int w = 0; w < workers; w++) { + updatedInstructions.add(new ArrayList<>()); + } + + // Instantiate data structure to record the procedures used by each + // worker. The index of the outer list matches a worker number, and the + // inner set contains procedure indices used by this worker. + List> proceduresUsedByWorkers = new ArrayList<>(); + for (int i = 0; i < workers; i++) { + proceduresUsedByWorkers.add(new HashSet<>()); + } + + // Record procedures used by workers by iterating over the DAG. + for (DagNode node : dag.getTopologicalSort()) { + // Look up the procedure index + Integer procedureIndex = nodeToProcedureIndexMap.get(node); + if (node.nodeType.equals(dagNodeType.REACTION)) { + // Add the procedure index to proceduresUsedByWorkers. + int worker = node.getWorker(); + proceduresUsedByWorkers.get(worker).add(procedureIndex); + } + } + + // Generate procedures first. + for (int w = 0; w < workers; w++) { + Set procedureIndices = proceduresUsedByWorkers.get(w); + for (Integer procedureIndex : procedureIndices) { + // Get the worker instructions. + List workerInstructions = objectFile.getContent().get(w); + // Get the head of the equivalence class list. + DagNode listHead = equivalenceClasses.get(procedureIndex).get(0); + // Look up the instructions in the first node in the equivalence class list. + List procedureCode = listHead.getInstructions(workerInstructions); + + // FIXME: Factor this out. + // Remove any phase labels from the procedure code. + // We need to do this because new phase labels will be + // added later in this optimizer pass. + if (procedureCode.get(0).hasLabel()) { // Only check the first instruction. + List labels = procedureCode.get(0).getLabelList(); + for (int i = 0; i < labels.size(); i++) { + try { + // Check if label is a phase. + Enum.valueOf(Phase.class, labels.get(i).toString()); + // If so, remove it. + labels.remove(i); + break; + } catch (IllegalArgumentException e) { + // Otherwise an error is raised, do nothing. + } + } + } + + // Set / append a procedure label. + procedureCode.get(0).addLabel(phase + "_PROCEDURE_" + procedureIndex); + + // Add instructions to the worker instruction list. + // FIXME: We likely need a clone here if there are multiple workers. + updatedInstructions.get(w).addAll(procedureCode); + + // Jump back to the call site. + updatedInstructions + .get(w) + .add(new InstructionJALR(registers.zero, registers.returnAddrs.get(w), 0L)); + } + } + + // Store locations to set a phase label for the optimized object code. + int[] phaseLabelLoc = new int[workers]; + for (int w = 0; w < workers; w++) { + phaseLabelLoc[w] = updatedInstructions.get(w).size(); + } + + // Generate code in the next topological sort. + for (DagNode node : dag.getTopologicalSort()) { + if (node.nodeType == dagNodeType.REACTION) { + // Generate code for jumping to the procedure index. + int w = node.getWorker(); + Integer procedureIndex = nodeToProcedureIndexMap.get(node); + updatedInstructions + .get(w) + .add( + new InstructionJAL( + registers.returnAddrs.get(w), phase + "_PROCEDURE_" + procedureIndex)); + } else if (node == dag.tail) { + // If the node is a tail node, simply copy the code. + // FIXME: We cannot do a jump to procedure here because the tail + // node also jumps to SYNC_BLOCK, which can be considered as + // another procedure call. There currently isn't a method + // for nesting procedures calls. One strategy is to temporarily use + // a register to save the outer return address. Then, when the + // inner procedure call returns, update the return address + // variable from the temp register. + for (int w = 0; w < workers; w++) { + // Get the worker instructions. + List workerInstructions = objectFile.getContent().get(w); + // Add instructions from this node. + updatedInstructions.get(w).addAll(node.getInstructions(workerInstructions)); + } + } + } + + // Add a label to the first instruction using the exploration phase + // (INIT, PERIODIC, SHUTDOWN_TIMEOUT, etc.). + for (int w = 0; w < workers; w++) { + updatedInstructions.get(w).get(phaseLabelLoc[w]).addLabel(phase.toString()); + } + + // Update the object file. + objectFile.setContent(updatedInstructions); + } +} diff --git a/core/src/main/java/org/lflang/analyses/opt/PeepholeOptimizer.java b/core/src/main/java/org/lflang/analyses/opt/PeepholeOptimizer.java new file mode 100644 index 0000000000..39b58e59f2 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/opt/PeepholeOptimizer.java @@ -0,0 +1,72 @@ +package org.lflang.analyses.opt; + +import java.util.ArrayList; +import java.util.List; +import org.lflang.analyses.pretvm.instructions.Instruction; +import org.lflang.analyses.pretvm.instructions.InstructionWU; + +public class PeepholeOptimizer { + + public static void optimize(List instructions) { + + // Move the sliding window down. + int maxWindowSize = 2; // Currently 2, but could be extended if larger windows are needed. + int i = 0; + while (i < instructions.size()) { + boolean changed = false; + for (int windowSize = 2; windowSize <= maxWindowSize; windowSize++) { + if (i + windowSize >= instructions.size()) { + break; // Avoid out-of-bound error. + } + List window = instructions.subList(i, i + windowSize); + List optimizedWindow = optimizeWindow(window); + if (!optimizedWindow.equals(window)) { + changed = true; + instructions.subList(i, i + windowSize).clear(); + instructions.addAll(i, optimizedWindow); + } + } + // Only slide the window when nothing is changed. + // If the code within a window change, apply optimizations again. + if (!changed) i++; + } + } + + public static List optimizeWindow(List window) { + // Here, window size could vary from 2 to 5 based on incoming patterns + // This method is called by the main optimize function with different sized windows + List optimized = new ArrayList<>(window); + + // Invoke optimizations for size >= 2. + if (window.size() >= 2) { + // Optimize away redundant WUs. + removeRedundantWU(window, optimized); + } + return optimized; + } + + /** + * When there are two consecutive WU instructions, keep the one waiting on a larger release value. + * The window size is 2. + * + * @param original + * @param optimized + */ + public static void removeRedundantWU(List original, List optimized) { + if (optimized.size() == 2) { + Instruction first = optimized.get(0); + Instruction second = optimized.get(1); + Instruction removed; + if (first instanceof InstructionWU firstWU && second instanceof InstructionWU secondWU) { + if (firstWU.getOperand2() < secondWU.getOperand2()) { + removed = optimized.remove(0); + } else { + removed = optimized.remove(1); + } + // At this point, one WU has been removed. + // Transfer the labels from the removed WU to the survived WU. + if (removed.getLabelList() != null) optimized.get(0).addLabels(removed.getLabelList()); + } + } + } +} diff --git a/core/src/main/java/org/lflang/analyses/opt/PretVMOptimizer.java b/core/src/main/java/org/lflang/analyses/opt/PretVMOptimizer.java new file mode 100644 index 0000000000..a905505814 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/opt/PretVMOptimizer.java @@ -0,0 +1,10 @@ +package org.lflang.analyses.opt; + +import java.util.List; +import org.lflang.analyses.pretvm.instructions.Instruction; + +public abstract class PretVMOptimizer { + public static void optimize(List instructions) { + throw new UnsupportedOperationException("Unimplemented method 'optimize'"); + } +} diff --git a/core/src/main/java/org/lflang/analyses/opt/README.md b/core/src/main/java/org/lflang/analyses/opt/README.md new file mode 100644 index 0000000000..8141c8761f --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/opt/README.md @@ -0,0 +1,292 @@ +# PretVM Bytecode Optimizers for LF Programs +Author: Shaokai Lin +Last Updated: April 29, 2024 + +## Table of Contents +- [Introduction](#introduction) +- [Peephole Optimizer](#peephole-optimizer) +- [DAG-based Optimizer](#dag-based-optimizer) +- [Current Progress](#current-progress) + +## Introduction +Lingua Franca (LF) is a polyglot coordination language for building +deterministic, real-time, and concurrent systems. +Here is a simple example for illustrating the use of LF. +```C +target C +reactor Sensor1 { + output out:int + timer t(0, 100 msec) + state count:int = 0 + reaction(t) -> out {= /* Application logic */ =} +} +reactor Sensor2 { + output out:int + timer t(0, 50 msec) + state count:int = 0 + reaction(t) -> out {= /* Application logic */ =} +} +reactor Processing { + input in1:int + input in2:int + output out:int + reaction(in1, in2) -> out {= /* Application logic */ =} +} +reactor Actuator { + input in:int + reaction(in) {= /* Application logic */ =} +} +main reactor { + s1 = new Sensor1() + s2 = new Sensor2() + p = new Processing() + a = new Actuator() + s1.out -> p.in1 + s2.out -> p.in2 + p.out -> a.in +} +``` +The pipeline first starts with two sensors, `Sensor1` and `Sensor2`, each wrapped in a `reactor` +definition. Reactors are stateful containers that can communicate with the +outside world through ports and have reactive code blocks called "reactions." The reactor +definition of `Sensor1` contains an +output port `out` with a type `int`. Since a sensor is typically triggered +periodically, a timer named `t` is defined with an initial offset of `0` and a period of +`100 msec`. +Due to the stateful nature of reactors, state variables can be defined within +a reactor definition. In this case, we define a state variable called `count` +with the type `int`. +Finally, a reaction, i.e., a reactive code block that gets triggered upon the +arrival of certain triggers, is defined inside `Sensor1` that is sensitive to +each firing of the timer `t`. Inside the `{=...=}` bracket, the user can write +the application logic of the reaction in a programming language of choice. In +this example, a C target is declared on top, so the program accepts C code +inside reaction bodies. At the time of writing, LF support C/C++, Python, Rust, +and TypeScript as target languages. + +`Sensor2`, `Processing`, and `Actuator` can be defined in a similar manner. Once +all reactor definitions are provided, connection statements can be used to +connect the reactors' ports, which allow them to send messages to each other. + +All events in the system are handled in the order of tags. Reactions are logically +instantaneous; logical time does not elapse during the execution of a +reaction (physical time on the other hand, does elapse). If a reaction +produces an output that triggers another reaction, then the two reactions execute +logically simultaneously (i.e., at the same tag). + +Determinism is a central feature of LF, and LF achieves this by establishing +a logical order of events through logical time. In this example, `Sensor1`, with +a period of `100 msec` and `Sensor2`, with a period of `50 msec`, send messages to a common +downstream reactor `Processing`. The LF semantics ensure that every output of +`Sensor2` is aligned with every other output of `Sensor1`. + +After an LF program is specified, we analyze +the state space of the program and find a DAG (directed acyclic graph) for each +phase of an LF program. The generated DAG is +then partitioned and mapped to available workers. + +Once the partitioned DAGs are generated, the LF compiler generates PretVM +bytecode from them. The table bwlow shows the instruction set. + +| Instruction | Description | +|:-------- |:--------:| +| ADD op1, op2, op3 | Add to an integer variable (op2) by an integer variable (op3) and store to a destination register (op1). | +| ADDI op1, op2, op3 | Add to an integer variable (op2) by an immediate (op3) and store to a destination register (op1). | +| ADV op1, op2, op3 | Advance the logical time of a reactor (op1) to a base time register (op2) + a time increment variable (op3). | +| ADVI op1, op2, op3 | Advance the logical time of a reactor (op1) to a base time register (op2) + an immediate value (op3). | +| BEQ op1, op2, op3 | Take the branch (op3) if the op1 register value is equal to the op2 register value. | +| BGE op1, op2, op3 | Take the branch (op3) if the op1 register value is greater than or equal to the op2 register value. | +| BLT op1, op2, op3 | Take the branch (op3) if the op1 register value is less than the op2 register value. | +| BNE op1, op2, op3 | Take the branch (op3) if the op1 register value is not equal to the op2 register value. | +| DU op1, op2 | Delay until the physical clock reaches a base timepoint (op1) plus an offset (op2). | +| EXE op1 | Execute a function (op1). | +| JAL op1, op2 | Store the return address to op1 and jump to a label (op2). | +| JALR op1, op2, op3 | Store the return address to op1 and jump to a base address (op2) + an immediate offset (op3). | +| STP | Stop the execution. | +| WLT op1, op2 | Wait until a register value (op1) to be less than a desired value (op2). | +| WU op1, op2 | Wait until a register value (op1) to be greater than or equal to a desired value (op2). | + +PretVM bytecode encodes a system's "coordination logic," which is +separated from its "application logic." +One major advantage of encoding the coordination logic in the form of bytecode +is that this format is amenable to optimization, +just like other types of bytecode, such +as JVM +bytecode, LLVM bitcode, and EVM (Ethereum virtual machine) bytecode, which likely +result in more efficient execution of the coordination logic. + +This document aims to describe the high-level objectives of the bytecode +optimizer and serves as a technical documentation for the ongoing development. + +## Peephole Optimizer + +### Why do we need this? + +1. (Done) Redundant `WU` instructions could be removed. + +`WU` instructions are used to ensure that task dependencies are satisfied across +workers. For example, the code example above could generate the following +bytecode when the program is executed by two workers. Let's focus on `Sensor1`, +`Sensor2`, and `Processing` for now. Let's further assume that `Sensor1` and `Sensor2` are +mapped to worker 0, while `Processing` is mapped to worker 1. + +The code generator currently generates code with the follwing form +``` +Worker 0: +1. EXE Sensor1's reaction +2. ADDI worker 0's counter by 1 +3. EXE Sensor2's reaction +4. ADDI worker 0's counter by 1 + +Worker 1: +5. WU worker 0's counter reaches 1 +6. WU worker 0's counter reaches 2 +7. EXE Processing's reaction +8. ADDI worker 1's counter by 1, +``` +The code we want to optimize away is line 5 in worker 1. Clearly, we can safely +remove line 5 here because line 6 is waiting for a greater release value, i.e., +2 > 1. + +The number of `WU`s could grow linearly with the number of upstream +dependencies. If instead of two sensors, there are a thousand sensors, then there will +be a thousand `WU`s generated. On a Raspberry Pi 4, each instructon takes about +two microseconds. 1000 `WU`s could take around 2 milliseconds, which is a long +time in the software world. + +2. (WIP) Time advancements, via the `ADV` and the `ADVI` instruction, could + potentially be grouped, forming enclaves. + +A similar (though subtly different) situation is time advancement. PretVM +currently uses the `ADV` and the `ADVI` instruction to advance reactor +timestamps. While a subset of reactors advance time in the middle of the +bytecode program, all reactors advance time to the next hyperperiod at the end +of the bytecode program. So at the end of the bytecode program, usually we see +the following pattern: +``` +Worker 0: +1. ADV reactor 1's time to time T +2. ADV reactor 2's time to time T +3. ADV reactor 3's time to time T +... +``` +It would be ideal to be able to collapse the sequence of `ADV`s into a single +one. +``` +Worker 0: +1. ADV a shared time register to time T +``` + +However, this is more complicated in practice. The problem is the reactors that +advance time in the middle of the bytecode program. For example, in the above +example, since the minimal hyperperiod is 100 milliseconds, `Sensor2`, triggered +once every 50 milliseconds, needs to advance time once in the middle of the +hyperperiod and another at the end of the hyperpeiord, while `Sensor1`, +triggered every 100 milliseconds, only advances time at the end of the +hyperperiod. +If they share the +same time register, advancing `Sensor2`'s time in the middle of the hyperperiod +will inadvertantly advance `Sensor1`'s time. In more complicated programs, doing +so could result in certain reactors advancing time without finishing all the +work prior to the new tag. + +A general solution to safely partition the reactors into regions that share time +registers is still work-in-progress. + +### How is this optimizer implemented? + +The above optimizations, especially the first one, are performed by the peephole +optimizer, which is a concrete strategy for implementing a term-rewriting system +(TRS). + +The peephole optimizer works by focusing on a basic block and uses a sliding +window to find opportunities to apply rewrite rules. +For instructions within a current window, registered rewrite rules are checked to see +if they are applicable. If so, the rewrite rules are applied and transform the code. + +Specifically, to eliminate redundant `WU`s, a pattern requiring a window of size two is +provided: +`WU counter, X ; WU counter, Y ==> WU counter, max(X, Y)`. The concrete +Java implementation can be found +[here](https://github.com/lf-lang/lingua-franca/blob/e2512debbba3726a85493a15885d10cc3f11c8d6/core/src/main/java/org/lflang/analyses/opt/PeepholeOptimizer.java#L55). + +Given an infrastructure for peephole optimization has been set up, it will be +easy to add more optimizations, if applicable. + +## DAG-based Optimizer + +### Why do we need this? + +Another optimizer currently under development is a DAG-based optimizer, which +focuses on the DAGs generated from an LF program and tries to perform procedure +extraction. + +The basic idea is that each node, except the tail node, in the DAG represents a +reaction invocation and +could generate a short sequence of instructions; given that the same reaction +could be triggered multiple times, it should be possible to factor out the +instructions from a frequently invoked reaction into a procedure, and call +the procedure at multiple times during execution, instead of generating +duplicate instructions. + +For example, `Sensor2`'s reaction could contribute the following instructions: +``` +1. EXE Sensor2's reaction +2. ADDI worker_counter by 1 +3. EXE connection management helper function +``` +Since `Sensor2`'s reaction is invoked twice in the hyperperiod, instead of +generating the above sequence of instructions twice, we could first factor them +out into a procedure: +``` +PROCEDURE_SENSOR_2: +1. EXE Sensor2's reaction +2. ADDI worker_counter by 1 +3. EXE connection management helper function +4. JALR return_address +``` +Then in the main procedure, jump to the procedure twice: +``` +Worker 0: +1. JAL PROCEDURE_SENSOR_2 +2. ADVI reactor's time +3. JAL PROCEDURE_SENSOR_2 +``` + +### How is this optimizer implemented? + +The DAG-based optimizer is implemented based on DAG traversal. It utilizes the +existing DAG structure in the tasks and treats the instructions generated by +each node as a basic block, which it aims to factor out if need be. + +The DAG-based optimizer maintains two key data structures, a list of equivalence +classes for DAG nodes and a mapping from a node to an index in the equivalence +class list. + +The optimizer traverses a DAG in the order of topological sort twice. In the +first pass, the optimizer populates the equivalence classes and the +node-to-index mapping. It considers two +nodes in the same equivalence class if they yield identical instructions. In the +second pass, the optimizer aims to generate an updated bytecode by first putting +procedure code in the bytecode, then uses `JAL` to jump to the procedures in the +main procedure. + +A work-in-progress implementation can be found +[here](https://github.com/lf-lang/lingua-franca/blob/e2512debbba3726a85493a15885d10cc3f11c8d6/core/src/main/java/org/lflang/analyses/opt/DagBasedOptimizer.java#L24). +It is not fully working yet given the following challenges: 1. existing +labels may need to be collapsed and shared somehow, 2. the connection management +functions need to be parameterized, 3. it is unclear whether the existing +algorithm will work when multiple workers are involved. +Overall, it is still a work-in-progress. + +## Current Progress + +At the time of writing, the infrastructure of the peephole optimizer has been +set up, and the optimization that removes redundant `WU`s is fully operational. + +A test case, [RemoveWUs.lf](https://github.com/lf-lang/lingua-franca/blob/e2512debbba3726a85493a15885d10cc3f11c8d6/test/C/src/static/RemoveWUs.lf), finishes in `881 msec` after the optimization, and in +`1010 msec` before the optimization, an `12.8%` improvement, measured on macOS with +2.3 GHz 8-core Intel Core i9. + +The time advancement optimization and procedure extraction are both not finished +at the time of writing and are under active developement. diff --git a/core/src/main/java/org/lflang/analyses/pretvm/InstructionGenerator.java b/core/src/main/java/org/lflang/analyses/pretvm/InstructionGenerator.java new file mode 100644 index 0000000000..03980f6217 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/InstructionGenerator.java @@ -0,0 +1,2265 @@ +package org.lflang.analyses.pretvm; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.lflang.FileConfig; +import org.lflang.TimeValue; +import org.lflang.analyses.dag.Dag; +import org.lflang.analyses.dag.DagNode; +import org.lflang.analyses.dag.DagNode.dagNodeType; +import org.lflang.analyses.pretvm.instructions.Instruction; +import org.lflang.analyses.pretvm.instructions.InstructionADD; +import org.lflang.analyses.pretvm.instructions.InstructionADDI; +import org.lflang.analyses.pretvm.instructions.InstructionADV; +import org.lflang.analyses.pretvm.instructions.InstructionADVI; +import org.lflang.analyses.pretvm.instructions.InstructionBEQ; +import org.lflang.analyses.pretvm.instructions.InstructionBGE; +import org.lflang.analyses.pretvm.instructions.InstructionBLT; +import org.lflang.analyses.pretvm.instructions.InstructionBNE; +import org.lflang.analyses.pretvm.instructions.InstructionDU; +import org.lflang.analyses.pretvm.instructions.InstructionEXE; +import org.lflang.analyses.pretvm.instructions.InstructionJAL; +import org.lflang.analyses.pretvm.instructions.InstructionJALR; +import org.lflang.analyses.pretvm.instructions.InstructionSTP; +import org.lflang.analyses.pretvm.instructions.InstructionWLT; +import org.lflang.analyses.pretvm.instructions.InstructionWU; +import org.lflang.analyses.statespace.StateSpaceExplorer.Phase; +import org.lflang.analyses.statespace.StateSpaceFragment; +import org.lflang.analyses.statespace.StateSpaceUtils; +import org.lflang.ast.ASTUtils; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.RuntimeRange; +import org.lflang.generator.SendRange; +import org.lflang.generator.TriggerInstance; +import org.lflang.generator.c.CGenerator; +import org.lflang.generator.c.CUtil; +import org.lflang.generator.c.TypeParameterizedReactor; +import org.lflang.lf.Connection; +import org.lflang.lf.Expression; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.DashProperty; +import org.lflang.target.property.FastProperty; +import org.lflang.target.property.TimeOutProperty; + +/** + * A generator that generates PRET VM programs from DAGs. It also acts as a linker that piece + * together multiple PRET VM object files. + * + * @author Shaokai Lin + */ +public class InstructionGenerator { + + /** File configuration */ + FileConfig fileConfig; + + /** Target configuration */ + TargetConfig targetConfig; + + /** Main reactor instance */ + protected ReactorInstance main; + + /** A list of reactor instances in the program */ + List reactors; + + /** A list of reaction instances in the program */ + List reactions; + + /** A list of trigger instances in the program */ + List triggers; + + /** Number of workers */ + int workers; + + /** + * A nested map that maps a source port to a C function name, which updates a priority queue + * holding tokens in a delayed connection. Each input can identify a unique connection because no + * more than one connection can feed into an input port. + */ + private Map preConnectionHelperFunctionNameMap = new HashMap<>(); + + private Map postConnectionHelperFunctionNameMap = new HashMap<>(); + + /** + * A map that maps a trigger to a list of (BEQ) instructions where this trigger's presence is + * tested. + */ + private Map> triggerPresenceTestMap = new HashMap<>(); + + /** PretVM registers */ + private Registers registers; + + /** Constructor */ + public InstructionGenerator( + FileConfig fileConfig, + TargetConfig targetConfig, + int workers, + ReactorInstance main, + List reactors, + List reactions, + List triggers, + Registers registers) { + this.fileConfig = fileConfig; + this.targetConfig = targetConfig; + this.workers = workers; + this.main = main; + this.reactors = reactors; + this.reactions = reactions; + this.triggers = triggers; + this.registers = registers; + for (int i = 0; i < this.workers; i++) { + registers.binarySemas.add(new Register(RegisterType.BINARY_SEMA, i)); + registers.counters.add(new Register(RegisterType.COUNTER, i)); + registers.returnAddrs.add(new Register(RegisterType.RETURN_ADDR, i)); + registers.temp0.add(new Register(RegisterType.TEMP0, i)); + registers.temp1.add(new Register(RegisterType.TEMP1, i)); + } + } + + /** Topologically sort the dag nodes and assign release values to DAG nodes for counting locks. */ + public void assignReleaseValues(Dag dagParitioned) { + // Initialize a reaction index array to keep track of the latest counting + // lock value for each worker. + Long[] releaseValues = new Long[workers]; + Arrays.fill(releaseValues, 0L); // Initialize all elements to 0 + + // Iterate over a topologically sorted list of dag nodes. + for (DagNode current : dagParitioned.getTopologicalSort()) { + if (current.nodeType == dagNodeType.REACTION) { + releaseValues[current.getWorker()] += 1; + current.setReleaseValue(releaseValues[current.getWorker()]); + } + } + } + + /** Traverse the DAG from head to tail using Khan's algorithm (topological sort). */ + public PretVmObjectFile generateInstructions(Dag dagParitioned, StateSpaceFragment fragment) { + // Map from a reactor to its latest associated SYNC node. + // Use case 1: This is used to determine when ADVIs and DUs should be generated without + // duplicating them for each reaction node in the same reactor. + // Use case 2: Determine a relative time increment for ADVIs. + Map reactorToLastSeenSyncNodeMap = new HashMap<>(); + + // Map an output port to its last seen EXE instruction at the current + // tag. When we know for sure that no other reactions can modify a port, we then + // go back to the last seen reaction-invoking EXE that can modify this port and + // _insert_ a connection helper right after the last seen EXE in the schedule. + // All the key value pairs in this map are waiting to be handled, + // since all the output port values must be written to the buffers at the + // end of the tag. + Map portToUnhandledReactionExeMap = new HashMap<>(); + + // Map a reaction to its last seen invocation, which is a DagNode. + // If two invocations are mapped to different workers, a WU needs to + // be generated to prevent race condition. + // This map is used to check whether the WU needs to be generated. + Map reactionToLastSeenInvocationMap = new HashMap<>(); + + // Assign release values for the reaction nodes. + assignReleaseValues(dagParitioned); + + // Instructions for all workers + List> instructions = new ArrayList<>(); + for (int i = 0; i < workers; i++) { + instructions.add(new ArrayList()); + } + + // Iterate over a topologically sorted list of dag nodes. + for (DagNode current : dagParitioned.getTopologicalSort()) { + // Get the upstream reaction nodes. + List upstreamReactionNodes = + dagParitioned.dagEdgesRev.getOrDefault(current, new HashMap<>()).keySet().stream() + .filter(n -> n.nodeType == dagNodeType.REACTION) + .toList(); + + if (current.nodeType == dagNodeType.REACTION) { + // Find the worker assigned to the REACTION node, + // the reactor, and the reaction. + int worker = current.getWorker(); + ReactionInstance reaction = current.getReaction(); + ReactorInstance reactor = reaction.getParent(); + + // Current worker schedule + List currentSchedule = instructions.get(worker); + + // Get the nearest upstream sync node. + DagNode associatedSyncNode = current.getAssociatedSyncNode(); + + // WU Case 1: + // If the reaction depends on upstream reactions owned by other + // workers, generate WU instructions to resolve the dependencies. + // FIXME: The current implementation generates multiple unnecessary WUs + // for simplicity. How to only generate WU when necessary? + for (DagNode n : upstreamReactionNodes) { + int upstreamOwner = n.getWorker(); + if (upstreamOwner != worker) { + addInstructionForWorker( + instructions, + current.getWorker(), + current, + null, + new InstructionWU(registers.counters.get(upstreamOwner), n.getReleaseValue())); + } + } + + // WU Case 2: + // FIXME: Is there a way to implement this using waitUntilDependencies + // in the Dag class? + // If the reaction has an _earlier_ invocation and is mapped to a + // _different_ worker, then a WU needs to be generated to prevent from + // processing of these two invocations of the same reaction in parallel. + // If they are processed in parallel, the shared logical time field in + // the reactor could get concurrent updates, resulting in incorrect + // execution. + // Most often, there is not an edge between these two nodes, + // making this a trickier case to handle. + // The strategy here is to use a variable to remember the last seen + // invocation of the same reaction instance. + DagNode lastSeen = reactionToLastSeenInvocationMap.get(reaction); + if (lastSeen != null && lastSeen.getWorker() != current.getWorker()) { + addInstructionForWorker( + instructions, + current.getWorker(), + current, + null, + new InstructionWU( + registers.counters.get(lastSeen.getWorker()), lastSeen.getReleaseValue())); + if (current + .getAssociatedSyncNode() + .timeStep + .isEarlierThan(lastSeen.getAssociatedSyncNode().timeStep)) { + System.out.println( + "FATAL ERROR: The current node is earlier than the lastSeen node. This case should" + + " not be possible and this strategy needs to be revised."); + System.exit(1); + } + } + reactionToLastSeenInvocationMap.put(reaction, current); + + // WU Case 3: + // (Reference: Philosophers.lf using EGS with 2 workers) + // If the node has an upstream dependency based on connection, but the + // upstream is mapped to a different worker. Generate a WU. + List upstreamsFromConnection = dagParitioned.waitUntilDependencies.get(current); + if (upstreamsFromConnection != null && upstreamsFromConnection.size() > 0) { + for (DagNode us : upstreamsFromConnection) { + if (us.getWorker() != current.getWorker()) { + addInstructionForWorker( + instructions, + current.getWorker(), + current, + null, + new InstructionWU(registers.counters.get(us.getWorker()), us.getReleaseValue())); + } + } + } + + // When the new associated sync node _differs_ from the last associated sync + // node of the reactor, this means that the current node's reactor needs + // to advance to a new tag. The code should update the associated sync + // node in the reactorToLastSeenSyncNodeMap map. And if + // associatedSyncNode is not the head, generate ADVI and DU instructions. + if (associatedSyncNode != reactorToLastSeenSyncNodeMap.get(reactor)) { + // Before updating reactorToLastSeenSyncNodeMap, + // compute a relative time increment to be used when generating an ADVI. + long relativeTimeIncrement; + if (reactorToLastSeenSyncNodeMap.get(reactor) != null) { + relativeTimeIncrement = + associatedSyncNode.timeStep.toNanoSeconds() + - reactorToLastSeenSyncNodeMap.get(reactor).timeStep.toNanoSeconds(); + } else { + relativeTimeIncrement = associatedSyncNode.timeStep.toNanoSeconds(); + } + + // Update the mapping. + reactorToLastSeenSyncNodeMap.put(reactor, associatedSyncNode); + + // If the reaction depends on a single SYNC node, + // advance to the LOGICAL time of the SYNC node first, + // as well as delay until the PHYSICAL time indicated by the SYNC node. + // Skip if it is the head node since this is done in the sync block. + // FIXME: Here we have an implicit assumption "logical time is + // physical time." We need to find a way to relax this assumption. + // FIXME: One way to relax this is that "logical time is physical time + // only when executing real-time reactions, otherwise fast mode for + // non-real-time reactions." + if (associatedSyncNode != dagParitioned.head) { + + // A pre-connection helper for an output port cannot be inserted + // until we are sure that all reactions that can modify this port + // at this tag has been invoked. At this point, since we have + // detected time advancement, this condition is satisfied. + // Iterate over all the ports of this reactor. We know at + // this point that the EXE instruction stored in + // portToUnhandledReactionExeMap is that the very last reaction + // invocation that can modify these ports. So we can insert + // pre-connection helpers after that reaction invocation. + for (PortInstance output : reactor.outputs) { + // Only generate for delayed connections. + if (outputToDelayedConnection(output)) { + Instruction lastPortModifyingReactionExe = + portToUnhandledReactionExeMap.get(output); + if (lastPortModifyingReactionExe != null) { + int exeWorker = lastPortModifyingReactionExe.getWorker(); + int indexToInsert = + indexOfByReference(instructions.get(exeWorker), lastPortModifyingReactionExe) + + 1; + generatePreConnectionHelper( + output, + instructions, + exeWorker, + indexToInsert, + lastPortModifyingReactionExe.getDagNode()); + // Remove the entry since this port is handled. + portToUnhandledReactionExeMap.remove(output); + } + } + } + + // Generate an ADVI instruction using a relative time increment. + // (instead of absolute). Relative style of coding promotes code reuse. + // FIXME: Factor out in a separate function. + String reactorTime = getFromEnvReactorTimePointer(main, reactor); + Register reactorTimeReg = registers.getRuntimeRegister(reactorTime); + var advi = + new InstructionADVI( + current.getReaction().getParent(), reactorTimeReg, relativeTimeIncrement); + var uuid = generateShortUUID(); + advi.addLabel("ADVANCE_TAG_FOR_" + reactor.getFullNameWithJoiner("_") + "_" + uuid); + addInstructionForWorker(instructions, worker, current, null, advi); + + // Generate a DU using a relative time increment. + // There are two cases for NOT generating a DU within a + // hyperperiod: 1. if fast is on, 2. if dash is on and the parent + // reactor is not realtime. + // Generate a DU instruction if neither case holds. + if (!(targetConfig.get(FastProperty.INSTANCE) + || (targetConfig.get(DashProperty.INSTANCE) + && !reaction.getParent().reactorDefinition.isRealtime()))) { + // reactorTimeReg is already updated by ADV/ADVI. + // Just delay until its recently updated value. + addInstructionForWorker( + instructions, worker, current, null, new InstructionDU(reactorTimeReg, 0L)); + } + } + } + + // Create an EXE instruction that invokes the reaction. + String reactorPointer = getFromEnvReactorPointer(main, reactor); + String reactorTimePointer = getFromEnvReactorTimePointer(main, reactor); + String reactionPointer = getFromEnvReactionFunctionPointer(main, reaction); + Instruction exeReaction = + new InstructionEXE( + registers.getRuntimeRegister(reactionPointer), + registers.getRuntimeRegister(reactorPointer), + reaction.index); + exeReaction.addLabel( + "EXECUTE_" + reaction.getFullNameWithJoiner("_") + "_" + generateShortUUID()); + + //////////////////////////////////////////////////////////////// + // Generate instructions for deadline handling. + // The general scheme for deadline handling is: + // + // Line x-3: ADDI temp0_reg, tag.time, reaction_deadline + // Line x-2: EXE update_temp1_to_current_time() // temp1_reg := lf_time_physical() + // Line x-1: BLT temp0_reg, temp1_reg, x+1 + // Line x : EXE reaction_body_function + // Line x+1: JAL x+3 // Jump pass the deadline handler if reaction body is executed. + // Line x+2: EXE deadline_handler_function + // + // Here we need to create the ADDI, EXE, and BLT instructions involved. + //////////////////////////////////////////////////////////////// + // Declare a sequence of instructions related to invoking the + // reaction body and handling deadline violations. + List reactionInvokingSequence = new ArrayList<>(); + if (reaction.declaredDeadline != null) { + // Create ADDI for storing the physical time after which the + // deadline is considered violated, + // basically, current tag + deadline value. + Instruction addiDeadlineTime = + new InstructionADDI( + registers.temp0.get(worker), + registers.getRuntimeRegister(reactorTimePointer), + reaction.declaredDeadline.maxDelay.toNanoSeconds()); + addiDeadlineTime.addLabel( + "CALCULATE_DEADLINE_VIOLATION_TIME_FOR_" + + reaction.getFullNameWithJoiner("_") + + "_" + + generateShortUUID()); + + // Create EXE for updating the time register. + var exeUpdateTimeRegister = + new InstructionEXE( + registers.getRuntimeRegister("update_temp1_to_current_time"), + registers.temp1.get(worker), + null); + + // Create deadline handling EXE + String deadlineHandlerPointer = + getFromEnvReactionDeadlineHandlerFunctionPointer(main, reaction); + Instruction exeDeadlineHandler = + new InstructionEXE( + registers.getRuntimeRegister(deadlineHandlerPointer), + registers.getRuntimeRegister(reactorPointer), + reaction.index); + exeDeadlineHandler.addLabel( + "HANDLE_DEADLINE_VIOLATION_OF_" + + reaction.getFullNameWithJoiner("_") + + "_" + + generateShortUUID()); + + // Create BLT for checking deadline violation. + var bltDeadlineViolation = + new InstructionBLT( + registers.temp0.get(worker), + registers.temp1.get(worker), + exeDeadlineHandler.getLabel()); + + // Create JAL for jumping pass the deadline handler if the + // deadline is not violated. + var jalPassHandler = new InstructionJAL(registers.zero, exeDeadlineHandler.getLabel(), 1); + + // Add the reaction-invoking EXE and deadline handling + // instructions to the schedule in the right order. + reactionInvokingSequence.add(addiDeadlineTime); + reactionInvokingSequence.add(exeUpdateTimeRegister); + reactionInvokingSequence.add(bltDeadlineViolation); + reactionInvokingSequence.add(exeReaction); + reactionInvokingSequence.add(jalPassHandler); + reactionInvokingSequence.add(exeDeadlineHandler); + } else { + // If the reaction does not have a deadline, just add the EXE + // running the reaction body. + reactionInvokingSequence.add(exeReaction); + } + + // It is important that the beginning and the end of the + // sequence has labels, so that the trigger checking BEQ + // instructions can jump to the right place. + if (reactionInvokingSequence.get(0).getLabel() == null + || reactionInvokingSequence.get(reactionInvokingSequence.size() - 1) == null) { + throw new RuntimeException( + "The reaction invoking instruction sequence either misses a label at the first" + + " instruction or at the last instruction, or both."); + } + + // Create BEQ instructions for checking triggers. + // Check if the reaction has input port triggers or not. If so, + // we need guards implemented using BEQ. + boolean hasGuards = false; + for (var trigger : reaction.triggers) { + if (trigger instanceof PortInstance port && port.isInput()) { + hasGuards = true; + Register reg1; + Register reg2; + // If connection has delay, check the connection buffer to see if + // the earliest event matches the reactor's current logical time. + if (inputFromDelayedConnection(port)) { + String pqueueHeadTime = getFromEnvPqueueHeadTimePointer(main, port); + reg1 = registers.getRuntimeRegister(pqueueHeadTime); // RUNTIME_STRUCT + reg2 = registers.getRuntimeRegister(reactorTimePointer); // RUNTIME_STRUCT + } + // Otherwise, if the connection has zero delay, check for the presence of the + // downstream port. + else { + String isPresentField = + "&" + getTriggerIsPresentFromEnv(main, trigger); // The is_present field + reg1 = registers.getRuntimeRegister(isPresentField); // RUNTIME_STRUCT + reg2 = registers.one; // Checking if is_present == 1 + } + Instruction reactionSequenceFront = reactionInvokingSequence.get(0); + Instruction beq = new InstructionBEQ(reg1, reg2, reactionSequenceFront.getLabel()); + beq.addLabel( + "TEST_TRIGGER_" + port.getFullNameWithJoiner("_") + "_" + generateShortUUID()); + addInstructionForWorker(instructions, current.getWorker(), current, null, beq); + // Update triggerPresenceTestMap. + if (triggerPresenceTestMap.get(port) == null) + triggerPresenceTestMap.put(port, new LinkedList<>()); + triggerPresenceTestMap.get(port).add(beq); + } + } + + // If none of the guards are activated, jump to one line after the + // reaction-invoking instruction sequence. + if (hasGuards) + addInstructionForWorker( + instructions, + worker, + current, + null, + new InstructionJAL( + registers.zero, + reactionInvokingSequence.get(reactionInvokingSequence.size() - 1).getLabel(), + 1)); + + // Add the reaction-invoking sequence to the instructions. + addInstructionSequenceForWorker( + instructions, current.getWorker(), current, null, reactionInvokingSequence); + + // Add the post-connection helper to the schedule, in case this reaction + // is triggered by an input port, which is connected to a connection + // buffer. + // Reaction invocations can be skipped, + // and we don't want the connection management to be skipped. + // FIXME: This does not seem to support the case when an input port + // triggers multiple reactions. We only want to add a post connection + // helper after the last reaction triggered by this port. + int indexToInsert = indexOfByReference(currentSchedule, exeReaction) + 1; + generatePostConnectionHelpers( + reaction, instructions, worker, indexToInsert, exeReaction.getDagNode()); + + // Add this reaction invoking EXE to the output-port-to-EXE map, + // so that we know when to insert pre-connection helpers. + for (TriggerInstance effect : reaction.effects) { + if (effect instanceof PortInstance output) { + portToUnhandledReactionExeMap.put(output, exeReaction); + } + } + + // Increment the counter of the worker. + // IMPORTANT: This ADDI has to be last because executing it releases + // downstream workers. If this ADDI is executed before + // connection management, then there is a race condition between + // upstream pushing events into connection buffers and downstream + // reading connection buffers. + // Instantiate an ADDI to be executed after EXE, releasing the counting locks. + var addi = + new InstructionADDI( + registers.counters.get(current.getWorker()), + registers.counters.get(current.getWorker()), + 1L); + addInstructionForWorker(instructions, worker, current, null, addi); + + } else if (current.nodeType == dagNodeType.SYNC) { + if (current == dagParitioned.tail) { + // At this point, we know for sure that all reactors are done with + // its current tag and are ready to advance time. We now insert a + // connection helper after each port's last reaction's ADDI + // (indicating the reaction is handled). + // FIXME: This _after_ is sus. Should be before! + for (var entry : portToUnhandledReactionExeMap.entrySet()) { + PortInstance output = entry.getKey(); + // Only generate for delayed connections. + if (outputToDelayedConnection(output)) { + Instruction lastReactionExe = entry.getValue(); + int exeWorker = lastReactionExe.getWorker(); + int indexToInsert = + indexOfByReference(instructions.get(exeWorker), lastReactionExe) + 1; + generatePreConnectionHelper( + output, instructions, exeWorker, indexToInsert, lastReactionExe.getDagNode()); + } + } + portToUnhandledReactionExeMap.clear(); + + // When the timeStep = TimeValue.MAX_VALUE in a SYNC node, + // this means that the DAG is acyclic and can end without + // real-time constraints, hence we do not genereate DU and ADDI. + if (current.timeStep != TimeValue.MAX_VALUE) { + for (int worker = 0; worker < workers; worker++) { + // Add a DU instruction if the fast mode is off. + // Turning on the dash mode does not affect this DU. The + // hyperperiod is still real-time. + // ALTERNATIVE DESIGN: remove the DU here and let the head node, + // instead of the tail node, handle DU. This potentially allows + // breaking the hyperperiod boundary. + if (!targetConfig.get(FastProperty.INSTANCE)) + addInstructionForWorker( + instructions, + worker, + current, + null, + new InstructionDU(registers.offset, current.timeStep.toNanoSeconds())); + // [Only Worker 0] Update the time increment register. + if (worker == 0) { + addInstructionForWorker( + instructions, + worker, + current, + null, + new InstructionADDI( + registers.offsetInc, registers.zero, current.timeStep.toNanoSeconds())); + } + // Let all workers go to SYNC_BLOCK after finishing PREAMBLE. + addInstructionForWorker( + instructions, + worker, + current, + null, + new InstructionJAL(registers.returnAddrs.get(worker), Phase.SYNC_BLOCK)); + } + } + } + } + } + // Add a label to the first instruction using the exploration phase + // (INIT, PERIODIC, SHUTDOWN_TIMEOUT, etc.). + for (int i = 0; i < workers; i++) { + instructions.get(i).get(0).addLabel(fragment.getPhase().toString()); + } + return new PretVmObjectFile(instructions, fragment, dagParitioned); + } + + /** + * Helper function for adding an instruction to a worker schedule. This function is not meant to + * be called in the code generation logic above, because a node needs to be associated with each + * instruction added. + * + * @param instructions The instructions under generation for a particular phase + * @param worker The worker who owns the instruction + * @param inst The instruction to be added + * @param index The index at which to insert the instruction. If the index is null, append the + * instruction at the end. Otherwise, append it at the specific index. + */ + private void _addInstructionForWorker( + List> instructions, int worker, Integer index, Instruction inst) { + if (index == null) { + // Add instruction to the instruction list. + instructions.get(worker).add(inst); + } else { + // Insert instruction to the instruction list at the specified index. + instructions.get(worker).add(index, inst); + } + // Remember the worker at the instruction level. + inst.setWorker(worker); + } + + /** + * Helper function for adding an instruction to a worker schedule + * + * @param instructions The instructions under generation for a particular phase + * @param worker The worker who owns the instruction + * @param node The DAG node for which this instruction is added + * @param index The index at which to insert the instruction. If the index is null, append the + * instruction at the end. Otherwise, append it at the specific index. + * @param inst The instruction to be added + */ + private void addInstructionForWorker( + List> instructions, + int worker, + DagNode node, + Integer index, + Instruction inst) { + // Add an instruction to the instruction list. + _addInstructionForWorker(instructions, worker, index, inst); + // Store the reference to the DAG node in the instruction. + inst.setDagNode(node); + } + + /** + * Helper function for adding an instruction to a worker schedule + * + * @param instructions The instructions under generation for a particular phase + * @param worker The worker who owns the instruction + * @param nodes A list of DAG nodes for which this instruction is added + * @param index The index at which to insert the instruction. If the index is null, append the + * instruction at the end. Otherwise, append it at the specific index. + * @param inst The instruction to be added + */ + private void addInstructionForWorker( + List> instructions, + int worker, + List nodes, + Integer index, + Instruction inst) { + // Add an instruction to the instruction list. + _addInstructionForWorker(instructions, worker, index, inst); + for (DagNode node : nodes) { + // Store the reference to the DAG node in the instruction. + inst.setDagNode(node); + } + } + + /** + * Helper function for adding a sequence of instructions to a worker schedule + * + * @param instructions The instructions under generation for a particular phase + * @param worker The worker who owns the instruction + * @param node The DAG node for which this instruction is added + * @param index The index at which to insert the instruction. If the index is null, append the + * instruction at the end. Otherwise, append it at the specific index. + * @param instList The list of instructions to be added + */ + private void addInstructionSequenceForWorker( + List> instructions, + int worker, + DagNode node, + Integer index, + List instList) { + // Add instructions to the instruction list. + for (int i = 0; i < instList.size(); i++) { + Instruction inst = instList.get(i); + _addInstructionForWorker(instructions, worker, index, inst); + // Store the reference to the DAG node in the instruction. + inst.setDagNode(node); + } + } + + /** Generate C code from the instructions list. */ + public void generateCode(PretVmExecutable executable) { + List> instructions = executable.getContent(); + + // Instantiate a code builder. + Path srcgen = fileConfig.getSrcGenPath(); + Path file = srcgen.resolve("static_schedule.c"); + CodeBuilder code = new CodeBuilder(); + + // Generate a block comment. + code.pr( + String.join( + "\n", + "/**", + " * An auto-generated schedule file for the STATIC scheduler.", + " * ", + " * reactor array:", + " * " + this.reactors, + " * ", + " * reaction array:", + " * " + this.reactions, + " */")); + + // Header files + code.pr( + String.join( + "\n", + "#include ", + "#include // size_t", + "#include // ULLONG_MAX", + "#include \"core/environment.h\"", + "#include \"core/threaded/scheduler_instance.h\"", + "#include \"core/threaded/scheduler_static_functions.h\"", + "#include " + "\"" + fileConfig.name + ".h" + "\"")); + + // Include reactor header files. + List tprs = this.reactors.stream().map(it -> it.tpr).toList(); + Set headerNames = CUtil.getNames(tprs); + for (var name : headerNames) { + code.pr("#include " + "\"" + name + ".h" + "\""); + } + + // Generate label macros. + for (int workerId = 0; workerId < instructions.size(); workerId++) { + List schedule = instructions.get(workerId); + for (int lineNumber = 0; lineNumber < schedule.size(); lineNumber++) { + Instruction inst = schedule.get(lineNumber); + // If the instruction already has a label, print it. + if (inst.hasLabel()) { + List labelList = inst.getLabelList(); + for (PretVmLabel label : labelList) { + code.pr("#define " + getWorkerLabelString(label, workerId) + " " + lineNumber); + } + } + // Otherwise, if any of the instruction's operands needs a label for + // delayed instantiation, create a label. + else { + List operands = inst.getOperands(); + for (int k = 0; k < operands.size(); k++) { + Object operand = operands.get(k); + if (operandRequiresDelayedInstantiation(operand)) { + String label = "DELAY_INSTANTIATE_" + inst.getOpcode() + "_" + generateShortUUID(); + inst.addLabel(label); + code.pr("#define " + getWorkerLabelString(label, workerId) + " " + lineNumber); + break; + } + } + } + } + } + code.pr("#define " + getPlaceHolderMacroString() + " " + "NULL"); + + // Extern variables + code.pr("// Extern variables"); + code.pr("extern environment_t envs[_num_enclaves];"); + code.pr("extern instant_t " + getVarName(registers.startTime, false) + ";"); + + // Runtime variables + code.pr("// Runtime variables"); + if (targetConfig.isSet(TimeOutProperty.INSTANCE)) + // FIXME: Why is timeout volatile? + code.pr( + "volatile uint64_t " + + getVarName(registers.timeout, false) + + " = " + + targetConfig.get(TimeOutProperty.INSTANCE).toNanoSeconds() + + "LL" + + ";"); + code.pr("const size_t num_counters = " + workers + ";"); // FIXME: Seems unnecessary. + code.pr("volatile reg_t " + getVarName(registers.offset, false) + " = 0ULL;"); + code.pr("volatile reg_t " + getVarName(registers.offsetInc, false) + " = 0ULL;"); + code.pr("const uint64_t " + getVarName(registers.zero, false) + " = 0ULL;"); + code.pr("const uint64_t " + getVarName(registers.one, false) + " = 1ULL;"); + code.pr( + "volatile uint64_t " + + getVarName(RegisterType.COUNTER) + + "[" + + workers + + "]" + + " = {0ULL};"); // Must be uint64_t, otherwise writing a long long to it could cause + // buffer overflow. + code.pr( + "volatile reg_t " + + getVarName(RegisterType.RETURN_ADDR) + + "[" + + workers + + "]" + + " = {0ULL};"); + code.pr( + "volatile reg_t " + + getVarName(RegisterType.BINARY_SEMA) + + "[" + + workers + + "]" + + " = {0ULL};"); + code.pr( + "volatile reg_t " + getVarName(RegisterType.TEMP0) + "[" + workers + "]" + " = {0ULL};"); + code.pr( + "volatile reg_t " + getVarName(RegisterType.TEMP1) + "[" + workers + "]" + " = {0ULL};"); + + // Generate function prototypes. + generateFunctionPrototypesForConnections(code); + generateFunctionPrototypeForTimeUpdate(code); + + // Generate static schedules. Iterate over the workers (i.e., the size + // of the instruction list). + for (int worker = 0; worker < instructions.size(); worker++) { + var schedule = instructions.get(worker); + code.pr("inst_t schedule_" + worker + "[] = {"); + code.indent(); + + for (int j = 0; j < schedule.size(); j++) { + Instruction inst = schedule.get(j); + + // If there is a label attached to the instruction, generate a comment. + if (inst.hasLabel()) { + List labelList = inst.getLabelList(); + for (PretVmLabel label : labelList) { + code.pr("// " + getWorkerLabelString(label, worker) + ":"); + } + } + + // Generate code based on opcode + switch (inst.getOpcode()) { + case ADD: + { + InstructionADD add = (InstructionADD) inst; + code.pr("// Line " + j + ": " + inst.toString()); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + add.getOpcode() + + ", " + + ".opcode=" + + add.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(add.getOperand1(), true) + + ", " + + ".op2.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(add.getOperand2(), true) + + ", " + + ".op3.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(add.getOperand3(), true) + + "}" + + ","); + break; + } + case ADDI: + { + InstructionADDI addi = (InstructionADDI) inst; + code.pr("// Line " + j + ": " + inst.toString()); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + addi.getOpcode() + + ", " + + ".opcode=" + + addi.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(addi.getOperand1(), true) + + ", " + + ".op2.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(addi.getOperand2(), true) + + ", " + + ".op3.imm=" + + addi.getOperand3() + + "LL" + + "}" + + ","); + break; + } + case ADV: + { + ReactorInstance reactor = ((InstructionADV) inst).getOperand1(); + Register baseTime = ((InstructionADV) inst).getOperand2(); + Register increment = ((InstructionADV) inst).getOperand3(); + code.pr("// Line " + j + ": " + inst.toString()); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.imm=" + + reactors.indexOf(reactor) + + ", " + + ".op2.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(baseTime, true) + + ", " + + ".op3.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(increment, true) + + "}" + + ","); + break; + } + case ADVI: + { + Register baseTime = ((InstructionADVI) inst).getOperand2(); + Long increment = ((InstructionADVI) inst).getOperand3(); + code.pr("// Line " + j + ": " + inst.toString()); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + getPlaceHolderMacroString() + + ", " + + ".op2.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(baseTime, true) + + ", " + + ".op3.imm=" + + increment + + "LL" // FIXME: Why longlong should be ULL for our type? + + "}" + + ","); + break; + } + case BEQ: + { + InstructionBEQ instBEQ = (InstructionBEQ) inst; + String rs1Str = getVarNameOrPlaceholder(instBEQ.getOperand1(), true); + String rs2Str = getVarNameOrPlaceholder(instBEQ.getOperand2(), true); + Object label = instBEQ.getOperand3(); + String labelString = getWorkerLabelString(label, worker); + code.pr("// Line " + j + ": " + instBEQ); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + rs1Str + + ", " + + ".op2.reg=" + + "(reg_t*)" + + rs2Str + + ", " + + ".op3.imm=" + + labelString + + "}" + + ","); + break; + } + case BGE: + { + InstructionBGE instBGE = (InstructionBGE) inst; + String rs1Str = getVarNameOrPlaceholder(instBGE.getOperand1(), true); + String rs2Str = getVarNameOrPlaceholder(instBGE.getOperand2(), true); + Object label = instBGE.getOperand3(); + String labelString = getWorkerLabelString(label, worker); + code.pr( + "// Line " + + j + + ": " + + "Branch to " + + labelString + + " if " + + rs1Str + + " >= " + + rs2Str); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + rs1Str + + ", " + + ".op2.reg=" + + "(reg_t*)" + + rs2Str + + ", " + + ".op3.imm=" + + labelString + + "}" + + ","); + break; + } + case BLT: + { + InstructionBLT instBLT = (InstructionBLT) inst; + String rs1Str = getVarNameOrPlaceholder(instBLT.getOperand1(), true); + String rs2Str = getVarNameOrPlaceholder(instBLT.getOperand2(), true); + Object label = instBLT.getOperand3(); + String labelString = getWorkerLabelString(label, worker); + code.pr( + "// Line " + + j + + ": " + + "Branch to " + + labelString + + " if " + + rs1Str + + " < " + + rs2Str); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + rs1Str + + ", " + + ".op2.reg=" + + "(reg_t*)" + + rs2Str + + ", " + + ".op3.imm=" + + labelString + + "}" + + ","); + break; + } + case BNE: + { + InstructionBNE instBNE = (InstructionBNE) inst; + String rs1Str = getVarNameOrPlaceholder(instBNE.getOperand1(), true); + String rs2Str = getVarNameOrPlaceholder(instBNE.getOperand2(), true); + Object label = instBNE.getOperand3(); + String labelString = getWorkerLabelString(label, worker); + code.pr( + "// Line " + + j + + ": " + + "Branch to " + + labelString + + " if " + + rs1Str + + " != " + + rs2Str); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + rs1Str + + ", " + + ".op2.reg=" + + "(reg_t*)" + + rs2Str + + ", " + + ".op3.imm=" + + labelString + + "}" + + ","); + break; + } + case DU: + { + Register offsetRegister = ((InstructionDU) inst).getOperand1(); + Long releaseTime = ((InstructionDU) inst).getOperand2(); + code.pr( + "// Line " + + j + + ": " + + "Delay Until the variable offset plus " + + releaseTime + + " is reached."); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(offsetRegister, true) + + ", " + + ".op2.imm=" + + releaseTime + + "LL" // FIXME: LL vs ULL. Since we are giving time in signed ints. Why not + // use signed int as our basic data type not, unsigned? + + "}" + + ","); + break; + } + case EXE: + { + // functionPointer and functionArgumentPointer are not directly + // printed in the code because they are not compile-time constants. + // Use a PLACEHOLDER instead for delayed instantiation. + Register functionPointer = ((InstructionEXE) inst).getOperand1(); + Register functionArgumentPointer = ((InstructionEXE) inst).getOperand2(); + Integer reactionNumber = ((InstructionEXE) inst).getOperand3(); + code.pr( + "// Line " + + j + + ": " + + "Execute function " + + functionPointer + + " with argument " + + functionArgumentPointer); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + getPlaceHolderMacroString() // PLACEHOLDER + + ", " + + ".op2.reg=" + + "(reg_t*)" + + getPlaceHolderMacroString() // PLACEHOLDER + + ", " + + ".op3.imm=" + + (reactionNumber == null ? "ULLONG_MAX" : reactionNumber) + + "}" + + ","); + break; + } + case JAL: + { + Register retAddr = ((InstructionJAL) inst).getOperand1(); + var targetLabel = ((InstructionJAL) inst).getOperand2(); + Integer offset = ((InstructionJAL) inst).getOperand3(); + String targetFullLabel = getWorkerLabelString(targetLabel, worker); + code.pr("// Line " + j + ": " + inst.toString()); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(retAddr, true) + + ", " + + ".op2.imm=" + + targetFullLabel + + ", " + + ".op3.imm=" + + (offset == null ? "0" : offset) + + "}" + + ","); + break; + } + case JALR: + { + Register destination = ((InstructionJALR) inst).getOperand1(); + Register baseAddr = ((InstructionJALR) inst).getOperand2(); + Long offset = ((InstructionJALR) inst).getOperand3(); + code.pr("// Line " + j + ": " + inst.toString()); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(destination, true) + + ", " + + ".op2.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(baseAddr, true) + + ", " + + ".op3.imm=" + + offset + + "}" + + ","); + break; + } + case STP: + { + code.pr("// Line " + j + ": " + "Stop the execution"); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + "}" + + ","); + break; + } + case WLT: + { + Register register = ((InstructionWLT) inst).getOperand1(); + Long releaseValue = ((InstructionWLT) inst).getOperand2(); + code.pr("// Line " + j + ": " + inst.toString()); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(register, true) + + ", " + + ".op2.imm=" + + releaseValue + + "}" + + ","); + break; + } + case WU: + { + Register register = ((InstructionWU) inst).getOperand1(); + Long releaseValue = ((InstructionWU) inst).getOperand2(); + code.pr("// Line " + j + ": " + inst.toString()); + code.pr( + "{" + + ".func=" + + "execute_inst_" + + inst.getOpcode() + + ", " + + ".opcode=" + + inst.getOpcode() + + ", " + + ".op1.reg=" + + "(reg_t*)" + + getVarNameOrPlaceholder(register, true) + + ", " + + ".op2.imm=" + + releaseValue + + "}" + + ","); + break; + } + default: + throw new RuntimeException("UNREACHABLE: " + inst.getOpcode()); + } + } + + code.unindent(); + code.pr("};"); + } + + // Generate an array to store the schedule pointers. + code.pr("const inst_t* static_schedules[] = {"); + code.indent(); + for (int i = 0; i < instructions.size(); i++) { + code.pr("schedule_" + i + ","); + } + code.unindent(); + code.pr("};"); + + //// Delayed instantiation of operands + // A function for initializing the non-compile-time constants. + code.pr("// Fill in placeholders in the schedule."); + code.pr("void initialize_static_schedule() {"); + code.indent(); + for (int w = 0; w < this.workers; w++) { + var workerInstructions = instructions.get(w); + // Iterate over each instruction operand and generate a delay + // instantiation for each operand that needs one. + for (Instruction inst : workerInstructions) { + List operands = inst.getOperands(); + for (int i = 0; i < operands.size(); i++) { + Object operand = operands.get(i); + + // If an operand does not need delayed instantiation, skip it. + if (!operandRequiresDelayedInstantiation(operand)) continue; + + // For each case, turn the operand into a string. + String operandStr = null; + if (operand instanceof Register reg && reg.type == RegisterType.RUNTIME_STRUCT) { + operandStr = getVarName(reg, false); + } else if (operand instanceof ReactorInstance reactor) { + operandStr = getFromEnvReactorPointer(main, reactor); + } else throw new RuntimeException("Unhandled operand type!"); + + // Get instruction label. + // Since we create additional DELAY_INSTANTIATE labels when we start printing + // static_schedule.c, at this point, an instruction must have a label. + // So we can skip checking for the existence of labels here. + PretVmLabel label = inst.getLabel(); + String labelFull = getWorkerLabelString(label, w); + + // Since we are dealing with runtime structs and reactor pointers in + // delayed instantiation, + // casting unconditionally to (reg_t*) should be okay because these + // structs are pointers. We also don't need to prepend & because + // this is taken care of when generating the operand string above. + code.pr( + "schedule_" + + w + + "[" + + labelFull + + "]" + + ".op" + + (i + 1) + + ".reg = (reg_t*)" + + operandStr + + ";"); + } + } + } + code.unindent(); + code.pr("}"); + + // Generate connection helper function definitions. + generateHelperFunctionForConnections(code); + + // Generate helper functions for updating time. + generateHelperFunctionForTimeUpdate(code); + + // Print to file. + try { + code.writeToFile(file.toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Generate function prototypes for connection helper functions. + * + * @param code The code builder to add code to + */ + private void generateFunctionPrototypesForConnections(CodeBuilder code) { + for (ReactorInstance reactor : this.reactors) { + for (PortInstance output : reactor.outputs) { + // For each output port, iterate over each destination port. + for (SendRange srcRange : output.getDependentPorts()) { + for (RuntimeRange dstRange : srcRange.destinations) { + // Can be used to identify a connection. + PortInstance input = dstRange.instance; + // Only generate pre-connection helper if it is delayed. + if (outputToDelayedConnection(output)) { + code.pr("void " + preConnectionHelperFunctionNameMap.get(input) + "();"); + } + code.pr("void " + postConnectionHelperFunctionNameMap.get(input) + "();"); + } + } + } + } + } + + /** + * Generate connection helper function definitions. + * + * @param code The code builder to add code to + */ + private void generateHelperFunctionForConnections(CodeBuilder code) { + for (ReactorInstance reactor : this.reactors) { + for (PortInstance output : reactor.outputs) { + + // For each output port, iterate over each destination port. + for (SendRange srcRange : output.getDependentPorts()) { + for (RuntimeRange dstRange : srcRange.destinations) { + + // Can be used to identify a connection. + PortInstance input = dstRange.instance; + // Pqueue index (> 0 if multicast) + int pqueueLocalIndex = 0; // Assuming no multicast yet. + // Logical delay of the connection + Long delay = ASTUtils.getDelay(srcRange.connection.getDelay()); + if (delay == null) delay = 0L; + // pqueue_heads index + int pqueueIndex = getPqueueIndex(input); + + // Only generate pre-connection helpers for delayed connections. + if (outputToDelayedConnection(output)) { + // FIXME: Factor this out. + /* Connection Source Helper */ + + code.pr("void " + preConnectionHelperFunctionNameMap.get(input) + "() {"); + code.indent(); + + // Set up the self struct, output port, pqueue, + // and the current time. + code.pr( + CUtil.selfType(reactor) + + "*" + + " self = " + + "(" + + CUtil.selfType(reactor) + + "*" + + ")" + + getFromEnvReactorPointer(main, reactor) + + ";"); + code.pr( + CGenerator.variableStructType(output) + + " port = " + + "self->_lf_" + + output.getName() + + ";"); + code.pr( + "circular_buffer *pq = (circular_buffer*)port.pqueues[" + + pqueueLocalIndex + + "];"); + code.pr("instant_t current_time = self->base.tag.time;"); + + // If the output port has a value, push it into the priority queue. + // FIXME: Create a token and wrap it inside an event. + code.pr( + String.join( + "\n", + "// If the output port has a value, push it into the connection buffer.", + "if (port.is_present) {", + " event_t event;", + " if (port.token != NULL) event.token = port.token;", + " else event.token = (lf_token_t *)(uintptr_t)port.value; // FIXME: Only" + + " works with int, bool, and any type that can directly be assigned to a" + + " void* variable.", + " // if (port.token != NULL) lf_print(\"Port value = %d\"," + + " *((int*)port.token->value));", + " // lf_print(\"current_time = %lld\", current_time);", + " event.base.tag.time = current_time + " + "NSEC(" + delay + "ULL);", + " // lf_print(\"event.time = %lld\", event.time);", + " cb_push_back(pq, &event);", + " // lf_print(\"Inserted an event @ %lld.\", event.time);", + "}")); + + code.pr(updateTimeFieldsToCurrentQueueHead(input)); + + code.unindent(); + code.pr("}"); + } + + // FIXME: Factor this out. + /* Connection Sink Helper */ + + code.pr("void " + postConnectionHelperFunctionNameMap.get(input) + "() {"); + code.indent(); + + // Clear the is_present field of the output port. + code.pr( + CUtil.selfType(reactor) + + "*" + + " self = " + + "(" + + CUtil.selfType(reactor) + + "*" + + ")" + + getFromEnvReactorPointer(main, reactor) + + ";"); + code.pr("self->_lf_" + output.getName() + ".is_present = false;"); + + // Only perform the buffer management for delayed connections. + if (inputFromDelayedConnection(input)) { + // Set up the self struct, output port, pqueue, + // and the current time. + ReactorInstance inputParent = input.getParent(); + code.pr( + CUtil.selfType(inputParent) + + "*" + + " input_parent = " + + "(" + + CUtil.selfType(inputParent) + + "*" + + ")" + + getFromEnvReactorPointer(main, inputParent) + + ";"); + code.pr( + CUtil.selfType(reactor) + + "*" + + " output_parent = " + + "(" + + CUtil.selfType(reactor) + + "*" + + ")" + + getFromEnvReactorPointer(main, reactor) + + ";"); + code.pr( + CGenerator.variableStructType(output) + + " port = " + + "output_parent->_lf_" + + output.getName() + + ";"); + code.pr( + "circular_buffer *pq = (circular_buffer*)port.pqueues[" + + pqueueLocalIndex + + "];"); + code.pr("instant_t current_time = input_parent->base.tag.time;"); + + // If the current head matches the current reactor's time, + // pop the head. + code.pr( + String.join( + "\n", + "// If the current head matches the current reactor's time, pop the head.", + "event_t* head = (event_t*) cb_peek(pq);", + "if (head != NULL && head->base.tag.time <= current_time) {", + " cb_remove_front(pq);", + " // _lf_done_using(head->token); // Done using the token and let it be" + + " recycled.", + updateTimeFieldsToCurrentQueueHead(input), + "}")); + } + + code.unindent(); + code.pr("}"); + } + } + } + } + } + + /** + * Generate a function prototype for the helper function that updates the temp1 register to the + * current physical time. + * + * @param code The code builder to add code to + */ + private void generateFunctionPrototypeForTimeUpdate(CodeBuilder code) { + code.pr("void update_temp1_to_current_time(void* worker);"); + } + + /** + * Generate a definition for the helper function that updates the temp1 register to the current + * physical time. + * + * @param code The code builder to add code to + */ + private void generateHelperFunctionForTimeUpdate(CodeBuilder code) { + code.pr( + String.join( + "\n", + "void update_temp1_to_current_time(void* worker) {", + " int w = (int)worker;", + " temp1[w] = lf_time_physical();", + "}")); + } + + /** + * An operand requires delayed instantiation if: 1. it is a RUNTIME_STRUCT register (i.e., fields + * in the generated LF self structs), or 2. it is a reactor instance. + */ + private boolean operandRequiresDelayedInstantiation(Object operand) { + if ((operand instanceof Register reg && reg.type == RegisterType.RUNTIME_STRUCT) + || (operand instanceof ReactorInstance)) { + return true; + } + return false; + } + + /** + * Update op1 of trigger-testing instructions (i.e., BEQ) to the time field of the current head of + * the queue. + */ + private String updateTimeFieldsToCurrentQueueHead(PortInstance input) { + CodeBuilder code = new CodeBuilder(); + + // By this point, line macros have been generated. Get them from + // a map that maps an input port to a list of TEST_TRIGGER macros. + List triggerTimeTests = triggerPresenceTestMap.get(input); + + // Peek and update the head. + code.pr( + String.join( + "\n", + "event_t* peeked = cb_peek(pq);", + getFromEnvPqueueHead(main, input) + " = " + "peeked" + ";")); + + // FIXME: Find a way to rewrite the following using the address of + // pqueue_heads, which does not need to change. + // Update: We still need to update the pointers because we are + // storing the pointer to the time field in one of the pqueue_heads, + // which still needs to be updated. + code.pr("if (peeked != NULL) {"); + code.indent(); + code.pr("// lf_print(\"Updated pqueue_head.\");"); + for (var test : triggerTimeTests) { + code.pr( + "schedule_" + + test.getWorker() + + "[" + + getWorkerLabelString(test.getLabel(), test.getWorker()) + + "]" + + ".op1.reg" + + " = " + + "(reg_t*)" + + getFromEnvPqueueHeadTimePointer(main, input) + + ";"); + } + code.unindent(); + code.pr("}"); + // If the head of the pqueue is NULL, then set the op1s to a NULL pointer, + // in order to prevent the effect of "dangling pointers", since head is + // freed earlier. + code.pr("else {"); + code.indent(); + for (var test : triggerTimeTests) { + code.pr( + "schedule_" + + test.getWorker() + + "[" + + getWorkerLabelString(test.getLabel(), test.getWorker()) + + "]" + + ".op1.reg" + + " = " + + "(reg_t*)" + + "NULL;"); + } + code.unindent(); + code.pr("}"); + + return code.toString(); + } + + /** Return a C variable name based on the variable type */ + private String getVarName(RegisterType type) { + switch (type) { + case BINARY_SEMA: + return "binary_sema"; + case COUNTER: + return "counters"; + case OFFSET: + return "time_offset"; + case OFFSET_INC: + return "offset_inc"; + case ONE: + return "one"; + case PLACEHOLDER: + return "PLACEHOLDER"; + case RETURN_ADDR: + return "return_addr"; + case START_TIME: + return "start_time"; + case TEMP0: + return "temp0"; + case TEMP1: + return "temp1"; + case TIMEOUT: + return "timeout"; + case ZERO: + return "zero"; + default: + throw new RuntimeException("Unhandled register type: " + type); + } + } + + /** Return a C variable name based on the variable type */ + private String getVarNameOrPlaceholder(Register register, boolean isPointer) { + RegisterType type = register.type; + // If the type indicates a field in a runtime-generated struct (e.g., + // reactor struct), return a PLACEHOLDER, because pointers are not "not + // compile-time constants". + if (type.equals(RegisterType.RUNTIME_STRUCT)) return getPlaceHolderMacroString(); + return getVarName(register, isPointer); + } + + /** Return a C variable name based on the variable type */ + private String getVarName(Register register, boolean isPointer) { + RegisterType type = register.type; + Integer worker = register.owner; + // If GlobalVarType.RUNTIME_STRUCT, return pointer directly. + if (type == RegisterType.RUNTIME_STRUCT) return register.pointer; + // Look up the type in getVarName(type). + String prefix = (isPointer) ? "&" : ""; + if (type.isGlobal()) return prefix + getVarName(type); + else return prefix + getVarName(type) + "[" + worker + "]"; + } + + /** Return a string of a label for a worker */ + private String getWorkerLabelString(Object label, int worker) { + if ((label instanceof PretVmLabel) || (label instanceof Phase) || (label instanceof String)) + return "WORKER" + "_" + worker + "_" + label.toString(); + throw new RuntimeException( + "Unsupported label type. Received: " + label.getClass().getName() + " = " + label); + } + + /** + * Link multiple object files into a single executable (represented also in an object file class). + * Instructions are also inserted based on transition guards between fragments. In addition, + * PREAMBLE and EPILOGUE instructions are inserted here. + */ + public PretVmExecutable link(List pretvmObjectFiles, Path graphDir) { + + // Create empty schedules. + List> schedules = new ArrayList<>(); + for (int i = 0; i < workers; i++) { + schedules.add(new ArrayList()); + } + + // Start with the first object file, which must not have upstream fragments. + PretVmObjectFile current = pretvmObjectFiles.get(0); + DagNode firstDagHead = current.getDag().head; + + // Generate and append the PREAMBLE code. + List> preamble = generatePreamble(firstDagHead, current); + for (int i = 0; i < schedules.size(); i++) { + schedules.get(i).addAll(preamble.get(i)); + } + + // Create a queue for storing unlinked object files. + Queue queue = new LinkedList<>(); + + // Create a set for tracking state space fragments seen, + // so that we don't process the same object file twice. + Set seen = new HashSet<>(); + + // Add the current fragment to the queue. + queue.add(current); + + // Iterate while there are still object files in the queue. + while (queue.size() > 0) { + + // Dequeue an object file. + current = queue.poll(); + + // Get the downstream fragments. + Set downstreamFragments = current.getFragment().getDownstreams().keySet(); + + // Obtain partial schedules from the current object file. + List> partialSchedules = current.getContent(); + + // Append guards for downstream transitions to the partial schedules. + List defaultTransition = null; + for (var dsFragment : downstreamFragments) { + List transition = current.getFragment().getDownstreams().get(dsFragment); + // Check if a transition is a default transition. + if (StateSpaceUtils.isDefaultTransition(transition)) { + defaultTransition = transition; + continue; + } + // Add COPIES of guarded transitions to the partial schedules. + // They have to be copies since otherwise labels created for different + // workers will be added to the same instruction object, creating conflicts. + for (int i = 0; i < workers; i++) { + partialSchedules + .get(i) + .addAll(replaceAbstractRegistersToConcreteRegisters(transition, i)); + } + } + // Make sure to have the default transition copies to be appended LAST, + // since default transitions are taken when no other transitions are taken. + if (defaultTransition != null) { + for (int i = 0; i < workers; i++) { + partialSchedules + .get(i) + .addAll(replaceAbstractRegistersToConcreteRegisters(defaultTransition, i)); + } + } + + // Add the partial schedules to the main schedule. + for (int i = 0; i < workers; i++) { + schedules.get(i).addAll(partialSchedules.get(i)); + } + + // Add current to the seen set. + seen.add(current); + + // Get the object files associated with the downstream fragments. + Set downstreamObjectFiles = + downstreamFragments.stream() + .map(StateSpaceFragment::getObjectFile) + // Filter out null object file since EPILOGUE has a null object file. + .filter(it -> it != null) + .collect(Collectors.toSet()); + + // Remove object files that have been seen. + downstreamObjectFiles.removeAll(seen); + + // Add object files related to the downstream fragments to the queue. + queue.addAll(downstreamObjectFiles); + } + + // Get a list of tail nodes. We can then attribute EPILOGUE and SyncBlock + // instructions to these tail nodes. Note that this is an overapproximation + // because some of these instructions will not actually get executed. For + // example, the epilogue is only executed at the very end, so the periodic + // fragment should not have to worry about it. But here we add it to these + // tail nodes anyway because with the above link logic, it is unclear which + // fragment is the actual last fragment in the execution. + List dagTails = pretvmObjectFiles.stream().map(it -> it.getDag().tail).toList(); + + // Generate the EPILOGUE code. + List> epilogue = generateEpilogue(dagTails); + for (int i = 0; i < schedules.size(); i++) { + schedules.get(i).addAll(epilogue.get(i)); + } + + // Generate and append the synchronization block. + List> syncBlock = generateSyncBlock(dagTails); + for (int i = 0; i < schedules.size(); i++) { + schedules.get(i).addAll(syncBlock.get(i)); + } + + // Generate DAGs with instructions. + var dagList = pretvmObjectFiles.stream().map(it -> it.getDag()).toList(); + var instructionsList = + pretvmObjectFiles.stream().map(it -> it.getContent()).toList(); // One list per phase. + for (int i = 0; i < dagList.size(); i++) { + // Generate another dot file with instructions displayed. + Path file = graphDir.resolve("dag_partitioned_with_inst_" + i + ".dot"); + dagList.get(i).generateDotFile(file, instructionsList.get(i)); + } + + return new PretVmExecutable(schedules); + } + + private List replaceAbstractRegistersToConcreteRegisters( + List transitions, int worker) { + List transitionCopy = transitions.stream().map(Instruction::clone).toList(); + for (Instruction inst : transitionCopy) { + if (inst instanceof InstructionJAL jal + && jal.getOperand1() == Registers.ABSTRACT_WORKER_RETURN_ADDR) { + jal.setOperand1(registers.returnAddrs.get(worker)); + } + } + return transitionCopy; + } + + /** + * Generate the PREAMBLE code. + * + * @param node The node for which preamble code is generated + * @param initialPhaseObjectFile The object file for the initial phase. This can be either INIT or + * PERIODIC. + */ + private List> generatePreamble( + DagNode node, PretVmObjectFile initialPhaseObjectFile) { + + List> schedules = new ArrayList<>(); + for (int worker = 0; worker < workers; worker++) { + schedules.add(new ArrayList()); + } + + for (int worker = 0; worker < workers; worker++) { + // [ONLY WORKER 0] Configure timeout register to be start_time + timeout. + if (worker == 0) { + // Configure offset register to be start_time. + addInstructionForWorker( + schedules, + worker, + node, + null, + new InstructionADDI(registers.offset, registers.startTime, 0L)); + // Configure timeout if needed. + if (targetConfig.get(TimeOutProperty.INSTANCE) != null) { + addInstructionForWorker( + schedules, + worker, + node, + null, + new InstructionADDI( + registers.timeout, + registers.startTime, + targetConfig.get(TimeOutProperty.INSTANCE).toNanoSeconds())); + } + // Update the time increment register. + addInstructionForWorker( + schedules, + worker, + node, + null, + new InstructionADDI(registers.offsetInc, registers.zero, 0L)); + } + // Let all workers jump to SYNC_BLOCK after finishing PREAMBLE. + addInstructionForWorker( + schedules, + worker, + node, + null, + new InstructionJAL(registers.returnAddrs.get(worker), Phase.SYNC_BLOCK)); + // Let all workers jump to the first phase (INIT or PERIODIC) after synchronization. + addInstructionForWorker( + schedules, + worker, + node, + null, + new InstructionJAL(registers.zero, initialPhaseObjectFile.getFragment().getPhase())); + // Give the first PREAMBLE instruction to a PREAMBLE label. + schedules.get(worker).get(0).addLabel(Phase.PREAMBLE.toString()); + } + + return schedules; + } + + /** Generate the EPILOGUE code. */ + private List> generateEpilogue(List nodes) { + + List> schedules = new ArrayList<>(); + for (int worker = 0; worker < workers; worker++) { + schedules.add(new ArrayList()); + } + + for (int worker = 0; worker < workers; worker++) { + Instruction stp = new InstructionSTP(); + stp.addLabel(Phase.EPILOGUE.toString()); + addInstructionForWorker(schedules, worker, nodes, null, stp); + } + + return schedules; + } + + /** Generate the synchronization code block. */ + private List> generateSyncBlock(List nodes) { + + List> schedules = new ArrayList<>(); + + for (int w = 0; w < workers; w++) { + + schedules.add(new ArrayList()); + + // Worker 0 will be responsible for changing the global variables while + // the other workers wait. + if (w == 0) { + + // Wait for non-zero workers' binary semaphores to be set to 1. + for (int worker = 1; worker < workers; worker++) { + addInstructionForWorker( + schedules, 0, nodes, null, new InstructionWU(registers.binarySemas.get(worker), 1L)); + } + + // Update the global time offset by an increment (typically the hyperperiod). + addInstructionForWorker( + schedules, + 0, + nodes, + null, + new InstructionADD(registers.offset, registers.offset, registers.offsetInc)); + + // Reset all workers' counters. + for (int worker = 0; worker < workers; worker++) { + addInstructionForWorker( + schedules, + 0, + nodes, + null, + new InstructionADDI(registers.counters.get(worker), registers.zero, 0L)); + } + + // Advance all reactors' tags to offset + increment. + for (int j = 0; j < this.reactors.size(); j++) { + var reactor = this.reactors.get(j); + var advi = new InstructionADVI(reactor, registers.offset, 0L); + advi.addLabel( + "ADVANCE_TAG_FOR_" + reactor.getFullNameWithJoiner("_") + "_" + generateShortUUID()); + addInstructionForWorker(schedules, 0, nodes, null, advi); + } + + // Set non-zero workers' binary semaphores to be set to 0. + for (int worker = 1; worker < workers; worker++) { + addInstructionForWorker( + schedules, + 0, + nodes, + null, + new InstructionADDI(registers.binarySemas.get(worker), registers.zero, 0L)); + } + + // Jump back to the return address. + addInstructionForWorker( + schedules, + 0, + nodes, + null, + new InstructionJALR(registers.zero, registers.returnAddrs.get(0), 0L)); + + } + // w >= 1 + else { + + // Set its own semaphore to be 1. + addInstructionForWorker( + schedules, + w, + nodes, + null, + new InstructionADDI(registers.binarySemas.get(w), registers.zero, 1L)); + + // Wait for the worker's own semaphore to be less than 1. + addInstructionForWorker( + schedules, w, nodes, null, new InstructionWLT(registers.binarySemas.get(w), 1L)); + + // Jump back to the return address. + addInstructionForWorker( + schedules, + w, + nodes, + null, + new InstructionJALR(registers.zero, registers.returnAddrs.get(w), 0L)); + } + + // Give the first instruction to a SYNC_BLOCK label. + schedules.get(w).get(0).addLabel(Phase.SYNC_BLOCK.toString()); + } + + return schedules; + } + + /** + * For a specific output port, generate an EXE instruction that puts tokens into a priority queue + * buffer for that connection. + * + * @param output The output port for which this connection helper is generated + * @param workerSchedule To worker schedule to be updated + * @param index The index where we insert the connection helper EXE + */ + private void generatePreConnectionHelper( + PortInstance output, + List> instructions, + int worker, + int index, + DagNode node) { + // For each output port, iterate over each destination port. + for (SendRange srcRange : output.getDependentPorts()) { + for (RuntimeRange dstRange : srcRange.destinations) { + // This input should uniquely identify a connection. + // Check its position in the trigger array to get the pqueue index. + PortInstance input = dstRange.instance; + // Get the pqueue index from the index map. + int pqueueIndex = getPqueueIndex(input); + String sourceFunctionName = + "process_connection_" + + pqueueIndex + + "_from_" + + output.getFullNameWithJoiner("_") + + "_to_" + + input.getFullNameWithJoiner("_"); + // Update the connection helper function name map + preConnectionHelperFunctionNameMap.put(input, sourceFunctionName); + // Add the EXE instruction. + var exe = + new InstructionEXE( + registers.getRuntimeRegister(sourceFunctionName), + registers.getRuntimeRegister("NULL"), + null); + exe.addLabel( + "PROCESS_CONNECTION_" + + pqueueIndex + + "_FROM_" + + output.getFullNameWithJoiner("_") + + "_TO_" + + input.getFullNameWithJoiner("_") + + "_" + + generateShortUUID()); + addInstructionForWorker(instructions, worker, node, index, exe); + } + } + } + + private void generatePostConnectionHelpers( + ReactionInstance reaction, + List> instructions, + int worker, + int index, + DagNode node) { + for (TriggerInstance source : reaction.sources) { + if (source instanceof PortInstance input) { + // Get the pqueue index from the index map. + int pqueueIndex = getPqueueIndex(input); + String sinkFunctionName = + "process_connection_" + + pqueueIndex + + "_after_" + + input.getFullNameWithJoiner("_") + + "_reads"; + // Update the connection helper function name map + postConnectionHelperFunctionNameMap.put(input, sinkFunctionName); + // Add the EXE instruction. + var exe = + new InstructionEXE( + registers.getRuntimeRegister(sinkFunctionName), + registers.getRuntimeRegister("NULL"), + null); + exe.addLabel( + "PROCESS_CONNECTION_" + + pqueueIndex + + "_AFTER_" + + input.getFullNameWithJoiner("_") + + "_" + + "READS" + + "_" + + generateShortUUID()); + addInstructionForWorker(instructions, worker, node, index, exe); + } + } + } + + /** Returns the placeholder macro string. */ + private String getPlaceHolderMacroString() { + return "PLACEHOLDER"; + } + + /** Generate short UUID to guarantee uniqueness in strings */ + private String generateShortUUID() { + return UUID.randomUUID().toString().substring(0, 8); // take first 8 characters + } + + private String getFromEnvReactorPointer(ReactorInstance main, ReactorInstance reactor) { + return CUtil.getEnvironmentStruct(main) + + ".reactor_self_array" + + "[" + + this.reactors.indexOf(reactor) + + "]"; + } + + private String getFromEnvReactorTimePointer(ReactorInstance main, ReactorInstance reactor) { + return "&" + + getFromEnvReactorPointer(main, reactor) + + "->tag.time"; // pointer to time at reactor + } + + private String getFromEnvReactionStruct(ReactorInstance main, ReactionInstance reaction) { + return CUtil.getEnvironmentStruct(main) + + ".reaction_array" + + "[" + + this.reactions.indexOf(reaction) + + "]"; + } + + private String getFromEnvReactionFunctionPointer( + ReactorInstance main, ReactionInstance reaction) { + return getFromEnvReactionStruct(main, reaction) + "->function"; + } + + private String getFromEnvReactionDeadlineHandlerFunctionPointer( + ReactorInstance main, ReactionInstance reaction) { + return getFromEnvReactionStruct(main, reaction) + "->deadline_violation_handler"; + } + + private String getFromEnvPqueueHead(ReactorInstance main, TriggerInstance trigger) { + return CUtil.getEnvironmentStruct(main) + ".pqueue_heads" + "[" + getPqueueIndex(trigger) + "]"; + } + + private String getFromEnvPqueueHeadTimePointer(ReactorInstance main, TriggerInstance trigger) { + return "&" + getFromEnvPqueueHead(main, trigger) + "->base.tag.time"; + } + + private int getPqueueIndex(TriggerInstance trigger) { + return this.triggers.indexOf(trigger); + } + + private String getTriggerIsPresentFromEnv(ReactorInstance main, TriggerInstance trigger) { + return "(" + + "(" + + nonUserFacingSelfType(trigger.getParent()) + + "*)" + + CUtil.getEnvironmentStruct(main) + + ".reactor_self_array" + + "[" + + this.reactors.indexOf(trigger.getParent()) + + "]" + + ")" + + "->" + + "_lf_" + + trigger.getName() + + "->is_present"; + } + + private boolean outputToDelayedConnection(PortInstance output) { + List dependentPorts = output.getDependentPorts(); // FIXME: Assume no broadcasts. + if (dependentPorts.size() == 0) return false; + Connection connection = dependentPorts.get(0).connection; + Expression delayExpr = connection.getDelay(); + return delayExpr != null && ASTUtils.getDelay(delayExpr) > 0; + } + + private boolean inputFromDelayedConnection(PortInstance input) { + if (input.getDependsOnPorts().size() > 0) { + PortInstance output = + input + .getDependsOnPorts() + .get(0) + .instance; // FIXME: Assume there is only one upstream port. This changes for + // multiports. + return outputToDelayedConnection(output); + } else { + return false; + } + } + + /** + * This mirrors userFacingSelfType(TypeParameterizedReactor tpr) in + * CReactorHeaderFileGenerator.java. + */ + private String nonUserFacingSelfType(ReactorInstance reactor) { + return "_" + reactor.getDefinition().getReactorClass().getName().toLowerCase() + "_self_t"; + } + + public static int indexOfByReference(List list, Object o) { + for (int i = 0; i < list.size(); i++) { + if (list.get(i) == o) { // Compare references using '==' + return i; + } + } + return -1; // Return -1 if not found + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/PretVmExecutable.java b/core/src/main/java/org/lflang/analyses/pretvm/PretVmExecutable.java new file mode 100644 index 0000000000..94cb0d806b --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/PretVmExecutable.java @@ -0,0 +1,31 @@ +package org.lflang.analyses.pretvm; + +import java.util.List; +import org.lflang.analyses.pretvm.instructions.Instruction; + +/** + * Class defining a PRET VM executable + * + * @author Shaokai Lin + */ +public class PretVmExecutable { + + /** + * Content is a list of list of instructions, where the inner list is a sequence of instructions + * for a worker, and the outer list is a list of instruction sequences, one for each worker. + */ + private List> content; + + /** Constructor */ + public PretVmExecutable(List> instructions) { + this.content = instructions; + } + + public List> getContent() { + return content; + } + + public void setContent(List> updatedContent) { + this.content = updatedContent; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/PretVmLabel.java b/core/src/main/java/org/lflang/analyses/pretvm/PretVmLabel.java new file mode 100644 index 0000000000..784f70ac25 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/PretVmLabel.java @@ -0,0 +1,32 @@ +package org.lflang.analyses.pretvm; + +import org.lflang.analyses.pretvm.instructions.Instruction; + +/** + * A memory label of an instruction, similar to the one in RISC-V + * + * @author Shaokai Lin + */ +public class PretVmLabel { + /** Pointer to an instruction */ + Instruction instruction; + + /** A string label */ + String labelString; + + /** Constructor */ + public PretVmLabel(Instruction instruction, String labelString) { + this.instruction = instruction; + this.labelString = labelString; + } + + /** Getter for the instruction */ + public Instruction getInstruction() { + return instruction; + } + + @Override + public String toString() { + return labelString; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/PretVmObjectFile.java b/core/src/main/java/org/lflang/analyses/pretvm/PretVmObjectFile.java new file mode 100644 index 0000000000..b9593cfd89 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/PretVmObjectFile.java @@ -0,0 +1,45 @@ +package org.lflang.analyses.pretvm; + +import java.util.List; +import org.lflang.analyses.dag.Dag; +import org.lflang.analyses.pretvm.instructions.Instruction; +import org.lflang.analyses.statespace.StateSpaceFragment; + +/** + * A PretVM Object File is a list of list of instructions, each list of which is for a worker. The + * object file also contains a state space fragment and a partitioned DAG for this fragment. + * + * @author Shaokai Lin + */ +public class PretVmObjectFile extends PretVmExecutable { + + private StateSpaceFragment fragment; // Useful for linking. + private Dag dagParitioned; + + public PretVmObjectFile( + List> instructions, StateSpaceFragment fragment, Dag dagParitioned) { + super(instructions); + this.fragment = fragment; + this.dagParitioned = dagParitioned; + } + + public StateSpaceFragment getFragment() { + return fragment; + } + + public Dag getDag() { + return dagParitioned; + } + + /** Pretty printing instructions */ + public void display() { + List> instructions = this.getContent(); + for (int i = 0; i < instructions.size(); i++) { + List schedule = instructions.get(i); + System.out.println("Worker " + i + ":"); + for (int j = 0; j < schedule.size(); j++) { + System.out.println(schedule.get(j)); + } + } + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/README.md b/core/src/main/java/org/lflang/analyses/pretvm/README.md new file mode 100644 index 0000000000..0017057b1f --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/README.md @@ -0,0 +1,12 @@ +# Steps for adding a new instruction + +## Compiler +1. Add a new opcode in `Instruction.java`. +2. Create a new instruction class under `pretvm`. +3. Generate new instructions in `InstructionGenerator.java`. +4. Generate C code in `InstructionGenerator.java`. + +## C Runtime +1. Add a new enum in `scheduler_instructions.h`. +2. Add an implementation for the new instruction in `scheduler_static.c`. +3. Update all the tracing functions (TODO: specify where). diff --git a/core/src/main/java/org/lflang/analyses/pretvm/Register.java b/core/src/main/java/org/lflang/analyses/pretvm/Register.java new file mode 100644 index 0000000000..412432fa1e --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/Register.java @@ -0,0 +1,60 @@ +package org.lflang.analyses.pretvm; + +import java.util.Objects; + +public class Register { + + public final RegisterType type; + public final Integer owner; + public final String pointer; // Only used for pointers in C structs + + // Constructor for a PretVM register + public Register(RegisterType type) { + this.type = type; + this.owner = null; + this.pointer = null; + } + + // Constructor for a PretVM register + public Register(RegisterType type, Integer owner) { + this.type = type; + this.owner = owner; + this.pointer = null; + } + + // Constructor for a PretVM register + public Register(RegisterType type, Integer owner, String pointer) { + this.type = type; + this.owner = owner; + this.pointer = pointer; + } + + public static Register createRuntimeRegister(String pointer) { + Register reg = new Register(RegisterType.RUNTIME_STRUCT, null, pointer); + return reg; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Register register = (Register) o; + return Objects.equals(type, register.type) + && Objects.equals(owner, register.owner) + && Objects.equals(pointer, register.pointer); + } + + @Override + public int hashCode() { + return Objects.hash(type, owner); + } + + @Override + public String toString() { + // If type is RUNTIME_STRUCT and toString() is called, then simply + // return the pointer. + if (type == RegisterType.RUNTIME_STRUCT) return this.pointer; + // Otherwise, use pretty printing. + return (owner != null ? "Worker " + owner + "'s " : "") + type; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/RegisterType.java b/core/src/main/java/org/lflang/analyses/pretvm/RegisterType.java new file mode 100644 index 0000000000..7516f7a2e7 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/RegisterType.java @@ -0,0 +1,47 @@ +package org.lflang.analyses.pretvm; + +/** + * Types of global variables used by the PRET VM Variable types prefixed by GLOBAL_ are accessible + * by all workers. Variable types prefixed by WORKER_ mean that there are arrays of these variables + * such that each worker gets its dedicated variable. For example, COUNTER means that there is an + * array of counter variables, one for each worker. A worker cannot modify another worker's counter. + */ +public enum RegisterType { + BINARY_SEMA(false), // Worker-specific binary semaphores to implement synchronization blocks. + COUNTER(false), // Worker-specific counters to keep track of the progress of a worker, for + // implementing a "counting lock." + OFFSET(true), // The current time offset after iterations of hyperperiods + OFFSET_INC( + true), // An amount to increment the offset by (usually the current hyperperiod). This is + // global because worker 0 applies the increment to all workers' offsets. + ONE(true), // A variable that is always one (i.e., true) + PLACEHOLDER(true), // Helps the code generator perform delayed instantiation. + RETURN_ADDR( + false), // Worker-specific addresses to return to after exiting the synchronization code + // block. + RUNTIME_STRUCT( + true), // Indicates that the variable/register is a field in a runtime-generated struct + // (reactor struct, priority queue, etc.). + START_TIME(true), // An external variable to store the start time of the application in epoch time + TEMP0(false), // A temporary register for each worker + TEMP1(false), // A temporary register for each worker + TIMEOUT(true), // A timeout value for all workers + ZERO(true); // A variable that is always zero (i.e., false) + + /** + * Whether this variable is shared by all workers. If this is true, then all workers can access + * and potentially modify the variable. If this is false, then an array will be generated, with + * each entry accessible by a specific worker. + */ + private final boolean global; + + /** Constructor */ + RegisterType(boolean global) { + this.global = global; + } + + /** Check if the variable is a global variable. */ + public boolean isGlobal() { + return global; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/Registers.java b/core/src/main/java/org/lflang/analyses/pretvm/Registers.java new file mode 100644 index 0000000000..681923d54a --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/Registers.java @@ -0,0 +1,48 @@ +package org.lflang.analyses.pretvm; + +import java.util.ArrayList; +import java.util.List; + +/** + * PretVM registers + * + *

FIXME: Should this be a record instead? + */ +public class Registers { + public final Register startTime = new Register(RegisterType.START_TIME); + public final Register offset = new Register(RegisterType.OFFSET); + public final Register offsetInc = new Register(RegisterType.OFFSET_INC); + public final Register one = new Register(RegisterType.ONE); + public final Register timeout = new Register(RegisterType.TIMEOUT); + public final Register zero = new Register(RegisterType.ZERO); + public List binarySemas = new ArrayList<>(); + public List counters = new ArrayList<>(); + public List returnAddrs = new ArrayList<>(); + public List runtime = new ArrayList<>(); + public List temp0 = new ArrayList<>(); + public List temp1 = new ArrayList<>(); + + // Abstract worker registers whose owner needs to be defined later. + public static final Register ABSTRACT_WORKER_RETURN_ADDR = new Register(RegisterType.RETURN_ADDR); + + /** + * A utility function that checks if a runtime register is already created. If so, it returns the + * instantiated register. Otherwise, it instantiates the register and adds it to the runtime list. + * + * @param regString The C pointer address for which the register is created + * @return a runtime register + */ + public Register getRuntimeRegister(String regString) { + Register temp = Register.createRuntimeRegister(regString); + int index = runtime.indexOf(temp); + if (index == -1) { + // Not found in the list of already instantiated runtime registers. + // So add to the list. + runtime.add(temp); + return temp; + } else { + // Found in the list. Simply return the register in list. + return runtime.get(index); + } + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/Instruction.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/Instruction.java new file mode 100644 index 0000000000..3f49b258c6 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/Instruction.java @@ -0,0 +1,206 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.lflang.analyses.dag.DagNode; +import org.lflang.analyses.pretvm.PretVmLabel; + +/** + * Abstract class defining a PRET virtual machine instruction + * + * @author Shaokai Lin + */ +public abstract class Instruction { + + /** + * PRET VM Instruction Set + * + *

ADD rs1, rs2, rs3 : Add to an integer variable (rs2) by an integer variable (rs3) and store + * the result in a destination variable (rs1). + * + *

ADDI rs1, rs2, rs3 : Add to an integer variable (rs2) by an immediate (rs3) and store the + * result in a destination variable (rs1). + * + *

ADV rs1, rs2, rs3 : ADVance the logical time of a reactor (rs1) to a base time register + * (rs2) + an increment register (rs3). + * + *

ADVI rs1, rs2, rs3 : Advance the logical time of a reactor (rs1) to a base time register + * (rs2) + an immediate value (rs3). The compiler needs to guarantee only a single thread can + * update a reactor's tag. + * + *

BEQ rs1, rs2, rs3 : Take the branch (rs3) if rs1 is equal to rs2. + * + *

BGE rs1, rs2, rs3 : Take the branch (rs3) if rs1 is greater than or equal to rs2. + * + *

BLT rs1, rs2, rs3 : Take the branch (rs3) if rs1 is less than rs2. + * + *

BNE rs1, rs2, rs3 : Take the branch (rs3) if rs1 is not equal to rs2. + * + *

DU rs1, rs2 : Delay Until a physical timepoint (rs1) plus an offset (rs2) is reached. + * + *

EXE rs1 : EXEcute a reaction (rs1) (used for known triggers such as startup, shutdown, and + * timers). + * + *

JAL rs1 rs2 : Store the return address to rs1 and jump to a label (rs2). + * + *

JALR rs1, rs2, rs3 : Store the return address in destination (rs1) and jump to baseAddr + * (rs2) + immediate (rs3) + * + *

STP : SToP the execution. + * + *

WLT rs1, rs2 : Wait until a variable (rs1) owned by a worker (rs2) to be less than a desired + * value (rs3). + * + *

WU rs1, rs2 : Wait Until a variable (rs1) owned by a worker (rs2) to be greater than or + * equal to a desired value (rs3). + */ + public enum Opcode { + ADD, + ADDI, + ADV, + ADVI, + BEQ, + BGE, + BLT, + BNE, + DU, + EXE, + JAL, + JALR, + STP, + WLT, + WU, + } + + /** Opcode of this instruction */ + protected Opcode opcode; + + /** The first operand */ + protected T1 operand1; + + /** The second operand */ + protected T2 operand2; + + /** The third operand */ + protected T3 operand3; + + /** + * A list of memory label for this instruction. A line of code can have multiple labels, similar + * to C. + */ + private List label; + + /** Worker who owns this instruction */ + private int worker; + + /** DAG node for which this instruction is generated */ + private DagNode node; + + /** Getter of the opcode */ + public Opcode getOpcode() { + return this.opcode; + } + + /** Set a label for this instruction. */ + public void addLabel(String labelString) { + if (this.label == null) + this.label = new ArrayList<>(Arrays.asList(new PretVmLabel(this, labelString))); + else + // If the list is already instantiated, + // create a new label and add it to the list. + this.label.add(new PretVmLabel(this, labelString)); + } + + /** Add a list of labels */ + public void addLabels(List labels) { + if (this.label == null) this.label = new ArrayList<>(); + this.label.addAll(labels); + } + + /** Remove a label for this instruction. */ + public void removeLabel(PretVmLabel label) { + this.label.remove(label); + } + + /** Return true if the instruction has a label. */ + public boolean hasLabel() { + return this.label != null; + } + + /** Return the first label. */ + public PretVmLabel getLabel() { + if (this.label.isEmpty()) return null; + return this.label.get(0); // Get the first label by default. + } + + /** Return the entire label list. */ + public List getLabelList() { + return this.label; + } + + public int getWorker() { + return this.worker; + } + + public void setWorker(int worker) { + this.worker = worker; + } + + public DagNode getDagNode() { + return this.node; + } + + public void setDagNode(DagNode node) { + this.node = node; + } + + @Override + public String toString() { + return opcode.toString() + + " " + + operand1.toString() + + " " + + operand2.toString() + + " " + + operand3.toString(); + } + + public T1 getOperand1() { + return this.operand1; + } + + public void setOperand1(T1 operand) { + this.operand1 = operand; + } + + public T2 getOperand2() { + return this.operand2; + } + + public void setOperand2(T2 operand) { + this.operand2 = operand; + } + + public T3 getOperand3() { + return this.operand3; + } + + public void setOperand3(T3 operand) { + this.operand3 = operand; + } + + public List getOperands() { + return Arrays.asList(operand1, operand2, operand3); + } + + @Override + public Instruction clone() { + throw new UnsupportedOperationException("Unimplemented method 'clone'"); + } + + @Override + public boolean equals(Object inst) { + throw new UnsupportedOperationException("Unimplemented method 'clone'"); + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADD.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADD.java new file mode 100644 index 0000000000..b0c162c370 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADD.java @@ -0,0 +1,41 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the ADD instruction + * + * @author Shaokai Lin + */ +public class InstructionADD extends Instruction { + + public InstructionADD(Register target, Register source, Register source2) { + this.opcode = Opcode.ADD; + this.operand1 = target; + this.operand2 = source; + this.operand3 = source2; + } + + @Override + public String toString() { + return "Increment " + this.operand1 + " by adding " + this.operand2 + " and " + this.operand3; + } + + @Override + public Instruction clone() { + return new InstructionADD(this.operand1, this.operand2, this.operand3); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionADD that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2) + && Objects.equals(this.operand3, that.operand3)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADDI.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADDI.java new file mode 100644 index 0000000000..c9e11a8d3a --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADDI.java @@ -0,0 +1,47 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the ADDI instruction + * + * @author Shaokai Lin + */ +public class InstructionADDI extends Instruction { + + public InstructionADDI(Register target, Register source, Long immediate) { + this.opcode = Opcode.ADDI; + this.operand1 = target; // The target register + this.operand2 = source; // The source register + this.operand3 = immediate; // The immediate to be added with the variable + } + + @Override + public String toString() { + return "Increment " + + this.operand1 + + " by adding " + + this.operand2 + + " and " + + this.operand3 + + "LL"; + } + + @Override + public Instruction clone() { + return new InstructionADDI(this.operand1, this.operand2, this.operand3); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionADDI that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2) + && Objects.equals(this.operand3, that.operand3)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADV.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADV.java new file mode 100644 index 0000000000..f78b2fcd2d --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADV.java @@ -0,0 +1,45 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; +import org.lflang.generator.ReactorInstance; + +/** + * Class defining the ADV instruction + * + * @author Shaokai Lin + */ +public class InstructionADV extends Instruction { + + /** Constructor */ + public InstructionADV(ReactorInstance reactor, Register baseTime, Register increment) { + this.opcode = Opcode.ADV; + this.operand1 = reactor; // The reactor whose logical time is to be advanced + // A base variable upon which to apply the increment. This is usually the current time offset + // (i.e., current time after applying multiple iterations of hyperperiods) + this.operand2 = baseTime; + this.operand3 = increment; // The logical time increment to add to the bast time + } + + @Override + public String toString() { + return "ADV: " + "advance" + this.operand1 + " to " + this.operand2 + " + " + this.operand3; + } + + @Override + public Instruction clone() { + return new InstructionADV(this.operand1, this.operand2, this.operand3); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionADV that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2) + && Objects.equals(this.operand3, that.operand3)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADVI.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADVI.java new file mode 100644 index 0000000000..8c30fe8745 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionADVI.java @@ -0,0 +1,45 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; +import org.lflang.generator.ReactorInstance; + +/** + * Class defining the ADVI (advance immediate) instruction + * + * @author Shaokai Lin + */ +public class InstructionADVI extends Instruction { + + /** Constructor */ + public InstructionADVI(ReactorInstance reactor, Register baseTime, Long increment) { + this.opcode = Opcode.ADVI; + this.operand1 = reactor; // The reactor whose logical time is to be advanced + // A base variable upon which to apply the increment. This is usually the current time offset + // (i.e., current time after applying multiple iterations of hyperperiods) + this.operand2 = baseTime; + this.operand3 = increment; // The logical time increment to add to the bast time + } + + @Override + public String toString() { + return "ADVI: " + "advance" + this.operand1 + " to " + this.operand2 + " + " + this.operand3; + } + + @Override + public Instruction clone() { + return new InstructionADVI(this.operand1, this.operand2, this.operand3); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionADVI that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2) + && Objects.equals(this.operand3, that.operand3)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBEQ.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBEQ.java new file mode 100644 index 0000000000..60cbed2cfe --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBEQ.java @@ -0,0 +1,25 @@ +package org.lflang.analyses.pretvm.instructions; + +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the BEQ instruction + * + * @author Shaokai Lin + */ +public class InstructionBEQ extends InstructionBranchBase { + public InstructionBEQ(Register rs1, Register rs2, Object label) { + super(rs1, rs2, label); + this.opcode = Opcode.BEQ; + } + + @Override + public Instruction clone() { + return new InstructionBEQ(this.operand1, this.operand2, this.operand3); + } + + @Override + public String toString() { + return "Branch to " + this.operand1 + " if " + this.operand2 + " = " + this.operand3; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBGE.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBGE.java new file mode 100644 index 0000000000..449da0a985 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBGE.java @@ -0,0 +1,20 @@ +package org.lflang.analyses.pretvm.instructions; + +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the BGE instruction + * + * @author Shaokai Lin + */ +public class InstructionBGE extends InstructionBranchBase { + public InstructionBGE(Register rs1, Register rs2, Object label) { + super(rs1, rs2, label); + this.opcode = Opcode.BGE; + } + + @Override + public Instruction clone() { + return new InstructionBGE(this.operand1, this.operand2, this.operand3); + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBLT.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBLT.java new file mode 100644 index 0000000000..e1f52e740a --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBLT.java @@ -0,0 +1,20 @@ +package org.lflang.analyses.pretvm.instructions; + +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the BLT instruction + * + * @author Shaokai Lin + */ +public class InstructionBLT extends InstructionBranchBase { + public InstructionBLT(Register rs1, Register rs2, Object label) { + super(rs1, rs2, label); + this.opcode = Opcode.BLT; + } + + @Override + public Instruction clone() { + return new InstructionBLT(this.operand1, this.operand2, this.operand3); + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBNE.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBNE.java new file mode 100644 index 0000000000..8990782533 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBNE.java @@ -0,0 +1,20 @@ +package org.lflang.analyses.pretvm.instructions; + +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the BNE instruction + * + * @author Shaokai Lin + */ +public class InstructionBNE extends InstructionBranchBase { + public InstructionBNE(Register rs1, Register rs2, Object label) { + super(rs1, rs2, label); + this.opcode = Opcode.BNE; + } + + @Override + public Instruction clone() { + return new InstructionBNE(this.operand1, this.operand2, this.operand3); + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBranchBase.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBranchBase.java new file mode 100644 index 0000000000..07ef0ab49a --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionBranchBase.java @@ -0,0 +1,48 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.PretVmLabel; +import org.lflang.analyses.pretvm.Register; +import org.lflang.analyses.statespace.StateSpaceExplorer.Phase; + +/** + * A base class for branch instructions. According to the RISC-V specifications, the operands can + * only be registers. + * + * @author Shaokai Lin + */ +public abstract class InstructionBranchBase extends Instruction { + + public InstructionBranchBase(Register rs1, Register rs2, Object label) { + if ((rs1 instanceof Register) + && (rs2 instanceof Register) + && (label instanceof Phase || label instanceof PretVmLabel)) { + this.operand1 = rs1; // The first operand, either Register or String + this.operand2 = rs2; // The second operand, either Register or String + // The label to jump to, which can only be one of the phases (INIT, PERIODIC, + // etc.) or a PretVmLabel. It cannot just be a number because numbers are hard + // to be absolute before linking. It is recommended to use PretVmLabel objects. + this.operand3 = label; + } else + throw new RuntimeException( + "Operands must be either Register or String. Label must be either Phase or PretVmLabel." + + " Operand 1: " + + rs1.getClass().getName() + + ". Operand 2: " + + rs2.getClass().getName() + + ". Label: " + + label.getClass().getName()); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionBranchBase that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2) + && Objects.equals(this.operand3, that.operand3)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionDU.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionDU.java new file mode 100644 index 0000000000..35bac67eb2 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionDU.java @@ -0,0 +1,42 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the DU instruction. An worker delays until baseTime + offset. + * + * @author Shaokai Lin + */ +public class InstructionDU extends Instruction { + + public InstructionDU(Register baseTime, Long offset) { + this.opcode = Opcode.DU; + this.operand1 = baseTime; + this.operand2 = offset; + } + + @Override + public String toString() { + return "DU: Delay until Register " + this.operand1 + "'s value + " + this.operand2; + } + + @Override + public Instruction clone() { + return new InstructionDU(this.operand1, this.operand2); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionDU that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2)) { + return true; + } else { + System.out.println("operand1s equal: " + Objects.equals(this.operand1, that.operand1)); + System.out.println("operand2s equal: " + Objects.equals(this.operand2, that.operand2)); + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionEXE.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionEXE.java new file mode 100644 index 0000000000..f6f04a9e09 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionEXE.java @@ -0,0 +1,45 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the EXE instruction + * + * @author Shaokai Lin + */ +public class InstructionEXE extends Instruction { + + /** Constructor */ + public InstructionEXE( + Register functionPointer, Register functionArgumentPointer, Integer reactionNumber) { + this.opcode = Opcode.EXE; + this.operand1 = functionPointer; // C function pointer to be executed + this.operand2 = functionArgumentPointer; // A pointer to an argument struct + // A reaction number if this EXE executes a reaction. Null if the EXE executes + // a helper function. + this.operand3 = reactionNumber; + } + + @Override + public String toString() { + return opcode + ": " + this.operand1 + " " + this.operand2 + " " + this.operand3; + } + + @Override + public Instruction clone() { + return new InstructionEXE(this.operand1, this.operand2, this.operand3); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionEXE that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2) + && Objects.equals(this.operand3, that.operand3)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionJAL.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionJAL.java new file mode 100644 index 0000000000..dd320ccad5 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionJAL.java @@ -0,0 +1,53 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the JAL instruction + * + * @author Shaokai Lin + */ +public class InstructionJAL extends Instruction { + + /** Constructor */ + public InstructionJAL(Register retAddr, Object targetLabel) { + this.opcode = Opcode.JAL; + this.operand1 = retAddr; // A register to store the return address + this.operand2 = targetLabel; // A target label to jump to + } + + public InstructionJAL(Register retAddr, Object targetLabel, Integer offset) { + this.opcode = Opcode.JAL; + this.operand1 = retAddr; // A register to store the return address + this.operand2 = targetLabel; // A target label to jump to + this.operand3 = offset; // An additional offset + } + + @Override + public String toString() { + return "JAL: " + + "store return address in " + + this.operand1 + + " and jump to " + + this.operand2 + + (this.operand3 == null ? "" : " + " + this.operand3); + } + + @Override + public Instruction clone() { + return new InstructionJAL(this.operand1, this.operand2, this.operand3); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionJAL that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2) + && Objects.equals(this.operand3, that.operand3)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionJALR.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionJALR.java new file mode 100644 index 0000000000..79b68c9440 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionJALR.java @@ -0,0 +1,48 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the JALR instruction + * + * @author Shaokai Lin + */ +public class InstructionJALR extends Instruction { + + /** Constructor */ + public InstructionJALR(Register destination, Register baseAddr, Long immediate) { + this.opcode = Opcode.JALR; + this.operand1 = destination; // A destination register to return to + this.operand2 = baseAddr; // A register containing the base address + this.operand3 = immediate; // A immediate representing the address offset + } + + @Override + public String toString() { + return "JALR: " + + "store the return address in " + + this.operand1 + + " and jump to " + + this.operand2 + + " + " + + this.operand3; + } + + @Override + public Instruction clone() { + return new InstructionJALR(this.operand1, this.operand2, this.operand3); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionJALR that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2) + && Objects.equals(this.operand3, that.operand3)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionSTP.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionSTP.java new file mode 100644 index 0000000000..5477f29040 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionSTP.java @@ -0,0 +1,30 @@ +package org.lflang.analyses.pretvm.instructions; + +/** + * Class defining the STP instruction + * + * @author Shaokai Lin + */ +public class InstructionSTP extends Instruction { + public InstructionSTP() { + this.opcode = Opcode.STP; + } + + @Override + public Instruction clone() { + return new InstructionSTP(); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionSTP) { + return true; + } + return false; + } + + @Override + public String toString() { + return "STP"; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionWLT.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionWLT.java new file mode 100644 index 0000000000..fa21e631ca --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionWLT.java @@ -0,0 +1,41 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the WLT instruction + * + * @author Shaokai Lin + */ +public class InstructionWLT extends Instruction { + + public InstructionWLT(Register register, Long releaseValue) { + this.opcode = Opcode.WLT; + this.operand1 = register; // A register which the worker waits on + this.operand2 = + releaseValue; // The value of the register at which the worker stops spinning and continues + // executing the schedule + } + + @Override + public String toString() { + return "WLT: Wait for " + this.operand1 + " to be less than " + this.operand2; + } + + @Override + public Instruction clone() { + return new InstructionWLT(this.operand1, this.operand2); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionWLT that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionWU.java b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionWU.java new file mode 100644 index 0000000000..51fefb84de --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/pretvm/instructions/InstructionWU.java @@ -0,0 +1,41 @@ +package org.lflang.analyses.pretvm.instructions; + +import java.util.Objects; +import org.lflang.analyses.pretvm.Register; + +/** + * Class defining the WU instruction + * + * @author Shaokai Lin + */ +public class InstructionWU extends Instruction { + + public InstructionWU(Register register, Long releaseValue) { + this.opcode = Opcode.WU; + this.operand1 = register; // A register which the worker waits on + this.operand2 = + releaseValue; // The value of the register at which the worker stops spinning and continues + // executing the schedule + } + + @Override + public String toString() { + return "WU: Wait for " + this.operand1 + " to reach " + this.operand2; + } + + @Override + public Instruction clone() { + return new InstructionWU(this.operand1, this.operand2); + } + + @Override + public boolean equals(Object inst) { + if (inst instanceof InstructionWU that) { + if (Objects.equals(this.operand1, that.operand1) + && Objects.equals(this.operand2, that.operand2)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/org/lflang/analyses/scheduler/EgsScheduler.java b/core/src/main/java/org/lflang/analyses/scheduler/EgsScheduler.java new file mode 100644 index 0000000000..2e70d38667 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/scheduler/EgsScheduler.java @@ -0,0 +1,169 @@ +package org.lflang.analyses.scheduler; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.lflang.analyses.dag.Dag; +import org.lflang.analyses.dag.DagNode; +import org.lflang.generator.c.CFileConfig; + +/** + * An external static scheduler based on edge generation. This scheduler assumes that all the python + * dependencies have been installed, `egs.py` is added to the PATH variable, and there is a + * pretrained model located at `models/pretrained` under the same directory as `egs.py`. + * + * @author Chadlia Jerad + * @author Shaokai Lin + */ +public class EgsScheduler implements StaticScheduler { + + /** File config */ + protected final CFileConfig fileConfig; + + public EgsScheduler(CFileConfig fileConfig) { + this.fileConfig = fileConfig; + } + + public Dag partitionDag(Dag dag, int fragmentId, int workers, String filePostfix) { + // Set all Paths and files + Path src = this.fileConfig.srcPath; + Path graphDir = fileConfig.getSrcGenPath().resolve("graphs"); + + // Files + Path rawDagDotFile = graphDir.resolve("dag_raw" + filePostfix + ".dot"); + Path partionedDagDotFile = graphDir.resolve("dag_partitioned" + filePostfix + ".dot"); + + // Start by generating the .dot file from the DAG + dag.generateDotFile(rawDagDotFile); + + // Find the directory where the EGS script is located. + String egsDir = findEgsDirectory(); + + // Construct a process to run the Python program of the RL agent + ProcessBuilder dagScheduler = + new ProcessBuilder( + "egs.py", + "--in_dot", + rawDagDotFile.toString(), + "--out_dot", + partionedDagDotFile.toString(), + "--workers", + String.valueOf(workers + 1), + "--model", + new File(egsDir, "models/pretrained").getAbsolutePath()); + + // Use a DAG scheduling algorithm to partition the DAG. + try { + + // Redirect the output and error streams + dagScheduler.redirectOutput(ProcessBuilder.Redirect.INHERIT); + dagScheduler.redirectError(ProcessBuilder.Redirect.INHERIT); + + // If the partionned DAG file is generated, then read the contents + // and update the edges array. + Process dagSchedulerProcess = dagScheduler.start(); + + // Wait until the process is done + int exitValue = dagSchedulerProcess.waitFor(); + + if (exitValue != 0) + throw new RuntimeException("Problem calling the external static scheduler... Abort!"); + + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + + Dag dagPartitioned = dag; + + // Read the generated DAG + try { + dagPartitioned.updateDag(partionedDagDotFile.toString()); + System.out.println( + "=======================\nDag succesfully updated\n======================="); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // FIXME: decrement all the workers by 1 + // FIXME (Shaokai): Why is this necessary? + + // Retreive the number of workers + Set setOfWorkers = new HashSet<>(); + for (int i = 0; i < dagPartitioned.dagNodes.size(); i++) { + int workerId = dagPartitioned.dagNodes.get(i).getWorker(); + workerId--; + dagPartitioned.dagNodes.get(i).setWorker(workerId); + setOfWorkers.add(workerId); + } + + int egsNumberOfWorkers = setOfWorkers.size() - 1; + + // Check that the returned number of workers is less than the one set by the user + if (egsNumberOfWorkers > workers) { + throw new RuntimeException( + "The EGS scheduler returned a minimum number of workers of " + + egsNumberOfWorkers + + " while the user specified number is " + + workers); + } + + // Define a color for each worker + String[] workersColors = new String[egsNumberOfWorkers]; + for (int i = 0; i < egsNumberOfWorkers; i++) { + workersColors[i] = StaticSchedulerUtils.generateRandomColor(); + } + + // Set the color of each node + for (int i = 0; i < dagPartitioned.dagNodes.size(); i++) { + int wk = dagPartitioned.dagNodes.get(i).getWorker(); + if (wk != -1) dagPartitioned.dagNodes.get(i).setColor(workersColors[wk]); + } + + // Set the partitions + for (int i = 0; i < egsNumberOfWorkers; i++) { + List partition = new ArrayList(); + for (int j = 0; j < dagPartitioned.dagNodes.size(); j++) { + int wk = dagPartitioned.dagNodes.get(j).getWorker(); + if (wk == i) { + partition.add(dagPartitioned.dagNodes.get(j)); + } + } + dagPartitioned.partitions.add(partition); + } + + Path dpu = graphDir.resolve("dag_partitioned" + filePostfix + ".dot"); + dagPartitioned.generateDotFile(dpu); + + return dagPartitioned; + } + + public String findEgsDirectory() { + try { + // Find the full path of egs.py using 'which' command + ProcessBuilder whichBuilder = new ProcessBuilder("which", "egs.py"); + Process whichProcess = whichBuilder.start(); + BufferedReader reader = + new BufferedReader(new InputStreamReader(whichProcess.getInputStream())); + String egsPath = reader.readLine(); + whichProcess.waitFor(); + + // Assuming egsPath is not null and contains the full path to egs.py + File egsFile = new File(egsPath); + String egsDir = egsFile.getParent(); + return egsDir; + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } + + public int setNumberOfWorkers() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'setNumberOfWorkers'"); + } +} diff --git a/core/src/main/java/org/lflang/analyses/scheduler/LoadBalancedScheduler.java b/core/src/main/java/org/lflang/analyses/scheduler/LoadBalancedScheduler.java new file mode 100644 index 0000000000..bef9b5e129 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/scheduler/LoadBalancedScheduler.java @@ -0,0 +1,151 @@ +package org.lflang.analyses.scheduler; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import org.lflang.analyses.dag.Dag; +import org.lflang.analyses.dag.DagNode; +import org.lflang.analyses.dag.DagNode.dagNodeType; + +/** + * A simple static scheduler that split work evenly among workers + * + * @author Shaokai Lin + */ +public class LoadBalancedScheduler implements StaticScheduler { + + /** Directory where graphs are stored */ + protected final Path graphDir; + + public LoadBalancedScheduler(Path graphDir) { + this.graphDir = graphDir; + } + + public class Worker { + private long totalWCET = 0; + private List tasks = new ArrayList<>(); + + public void addTask(DagNode task) { + tasks.add(task); + totalWCET += task.getReaction().wcets.get(0).toNanoSeconds(); + } + + public long getTotalWCET() { + return totalWCET; + } + } + + public Dag partitionDag(Dag dag, int fragmentId, int numWorkers, String filePostfix) { + + // Prune redundant edges. + dag.removeRedundantEdges(); + + // Generate a dot file. + Path file = graphDir.resolve("dag_pruned" + filePostfix + ".dot"); + dag.generateDotFile(file); + + // Initialize workers + Worker[] workers = new Worker[numWorkers]; + for (int i = 0; i < numWorkers; i++) { + workers[i] = new Worker(); + } + + // Sort tasks in descending order by WCET + List reactionNodes = + dag.dagNodes.stream() + .filter(node -> node.nodeType == dagNodeType.REACTION) + .collect(Collectors.toCollection(ArrayList::new)); + + reactionNodes.sort( + Comparator.comparing( + (DagNode node) -> + node.getReaction() + .wcets + .get(0) // The default scheduler only assumes 1 WCET. + .toNanoSeconds()) + .reversed()); + + // Assign tasks to workers + for (DagNode node : reactionNodes) { + // Find worker with least work + Worker minWorker = + Arrays.stream(workers).min(Comparator.comparing(Worker::getTotalWCET)).orElseThrow(); + + // Assign task to this worker + minWorker.addTask(node); + } + + // Update partitions + for (int i = 0; i < numWorkers; i++) { + dag.partitions.add(workers[i].tasks); + } + + // Assign colors to each partition + for (int j = 0; j < dag.partitions.size(); j++) { + List partition = dag.partitions.get(j); + String randomColor = StaticSchedulerUtils.generateRandomColor(); + for (int i = 0; i < partition.size(); i++) { + partition.get(i).setColor(randomColor); + partition.get(i).setWorker(j); + } + } + + // Linearize partitions by adding edges. + linearizePartitions(dag, numWorkers); + + // Prune redundant edges again. + dag.removeRedundantEdges(); + + // Generate another dot file. + Path file2 = graphDir.resolve("dag_partitioned" + filePostfix + ".dot"); + dag.generateDotFile(file2); + + return dag; + } + + /** + * If the number of workers is unspecified, determine a value for the number of workers. This + * scheduler base class simply returns 1. An advanced scheduler is free to run advanced algorithms + * here. + */ + public int setNumberOfWorkers() { + return 1; + } + + /** + * A valid DAG must linearize all nodes within a partition, such that there is a chain from the + * first node to the last node executed by a worker owning the partition. In other words, the + * width of the partition needs to be 1. Forming this chain enables WCET analysis at the system + * level by tracing back edges from the tail node. It also makes it clear what the order of + * execution in a partition is. + * + * @param dag Dag whose partitions are to be linearized + */ + private void linearizePartitions(Dag dag, int numWorkers) { + // Initialize an array of previous nodes. + DagNode[] prevNodes = new DagNode[numWorkers]; + for (int i = 0; i < prevNodes.length; i++) prevNodes[i] = null; + + for (DagNode current : dag.getTopologicalSort()) { + if (current.nodeType == dagNodeType.REACTION) { + int worker = current.getWorker(); + + // Check if the previous node of the partition is null. If so, store the + // node and go to the next iteration. + if (prevNodes[worker] == null) { + prevNodes[worker] = current; + continue; + } + + // Draw an edge between the previous node and the current node. + dag.addEdge(prevNodes[worker], current); + + // Update previous nodes. + prevNodes[worker] = current; + } + } + } +} diff --git a/core/src/main/java/org/lflang/analyses/scheduler/MocasinScheduler.java b/core/src/main/java/org/lflang/analyses/scheduler/MocasinScheduler.java new file mode 100644 index 0000000000..4d44fbc9d3 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/scheduler/MocasinScheduler.java @@ -0,0 +1,436 @@ +package org.lflang.analyses.scheduler; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import org.lflang.TimeValue; +import org.lflang.analyses.dag.Dag; +import org.lflang.analyses.dag.DagEdge; +import org.lflang.analyses.dag.DagNode; +import org.lflang.analyses.dag.DagNode.dagNodeType; +import org.lflang.generator.c.CFileConfig; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.SchedulerProperty; +import org.lflang.util.FileUtil; +import org.w3c.dom.Comment; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * An external static scheduler using the `mocasin` tool + * + * @author Shaokai Lin + */ +public class MocasinScheduler implements StaticScheduler { + + /** File config */ + protected final CFileConfig fileConfig; + + /** Target config */ + protected final TargetConfig targetConfig; + + /** Directory where graphs are stored */ + protected final Path graphDir; + + /** Directory where mocasin files are stored */ + protected final Path mocasinDir; + + /** Constructor */ + public MocasinScheduler(CFileConfig fileConfig, TargetConfig targetConfig) { + this.fileConfig = fileConfig; + this.targetConfig = targetConfig; + this.graphDir = fileConfig.getSrcGenPath().resolve("graphs"); + this.mocasinDir = fileConfig.getSrcGenPath().resolve("mocasin"); + + // Create the mocasin directory. + try { + Files.createDirectories(this.mocasinDir); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** Turn the original DAG into SDF format by adding an edge from tail to head. */ + public Dag turnDagIntoSdfFormat(Dag dagRaw) { + // Create a copy of the original dag. + Dag dag = new Dag(dagRaw); + + // Connect tail to head. + dag.addEdge(dag.tail, dag.head); + + return dag; + } + + /** + * Generate an XML file that represents the DAG using the SDF3 format + * + * @throws ParserConfigurationException + * @throws TransformerException + */ + public String generateSDF3XML(Dag dagSdf, String filePostfix) + throws ParserConfigurationException, TransformerException { + DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + + // root elements: sdf3 + Document doc = docBuilder.newDocument(); + Element rootElement = doc.createElement("sdf3"); + rootElement.setAttribute("version", "1.0"); + rootElement.setAttribute("type", "sdf"); + doc.appendChild(rootElement); + + // applicationGraph + Element appGraph = doc.createElement("applicationGraph"); + appGraph.setAttribute("name", "lf"); + rootElement.appendChild(appGraph); + + // sdf + Element sdf = doc.createElement("sdf"); + sdf.setAttribute("name", "g"); + sdf.setAttribute("type", "G"); + appGraph.appendChild(sdf); + + // Append reaction nodes under the SDF element. + for (var node : dagSdf.dagNodes) { + // Each SDF "actor" here is actually a reaction node. + Comment comment = doc.createComment("This actor is: " + node.toString()); + Element actor = doc.createElement("actor"); + actor.setAttribute("name", node.toString()); + sdf.appendChild(comment); + sdf.appendChild(actor); + + // Incoming edges constitute input ports. + var incomingEdges = dagSdf.dagEdgesRev.get(node); + for (var srcNode : incomingEdges.keySet()) { + Element inputPort = doc.createElement("port"); + inputPort.setAttribute("name", incomingEdges.get(srcNode).toString() + "_input"); + inputPort.setAttribute("type", "in"); + inputPort.setAttribute("rate", "1"); + + actor.appendChild(inputPort); + } + + // Outgoing edges constitute output ports. + var outgoingEdges = dagSdf.dagEdges.get(node); + for (var destNode : outgoingEdges.keySet()) { + Element outputPort = doc.createElement("port"); + outputPort.setAttribute("name", outgoingEdges.get(destNode).toString() + "_output"); + outputPort.setAttribute("type", "out"); + outputPort.setAttribute("rate", "1"); + + actor.appendChild(outputPort); + } + } + + // Generate channel fields. + List edges = dagSdf.getDagEdges(); + for (int i = 0; i < edges.size(); i++) { + DagEdge edge = edges.get(i); + Element channel = doc.createElement("channel"); + channel.setAttribute("name", "ch" + i); + channel.setAttribute("srcActor", edge.sourceNode.toString()); + channel.setAttribute("srcPort", edge.toString() + "_output"); + channel.setAttribute("dstActor", edge.sinkNode.toString()); + channel.setAttribute("dstPort", edge.toString() + "_input"); + + // If the edge is the added back edge from tail to head, + // add an initial token. + if (edge.sourceNode == dagSdf.tail && edge.sinkNode == dagSdf.head) { + channel.setAttribute("initialTokens", "1"); + } + + sdf.appendChild(channel); + } + + // sdfProperties + Element sdfProperties = doc.createElement("sdfProperties"); + appGraph.appendChild(sdfProperties); + + // Generate actorProperties (i.e., execution times) + for (var node : dagSdf.dagNodes) { + // actorProperties + Element actorProperties = doc.createElement("actorProperties"); + actorProperties.setAttribute("actor", node.toString()); + + if (node.isAuxiliary()) { + // URGENT FIXME: Only works for Odroid because we assume 2 types! + for (int i = 0; i < 2; i++) { + var wcet = TimeValue.ZERO; + setProcessorWcet(doc, actorProperties, i, node, wcet); + } + } else { + for (int i = 0; i < node.getReaction().wcets.size(); i++) { + var wcet = node.getReaction().wcets.get(i); + setProcessorWcet(doc, actorProperties, i, node, wcet); + } + } + + // Append elements. + sdfProperties.appendChild(actorProperties); + } + + // Generate channelProperties + // FIXME: All values here are hardcoded. Make sure they make sense. + for (int i = 0; i < edges.size(); i++) { + Element channelProperties = doc.createElement("channelProperties"); + channelProperties.setAttribute("channel", "ch" + i); + + // bufferSize + Element bufferSize = doc.createElement("bufferSize"); + bufferSize.setAttribute("sz", "1"); + bufferSize.setAttribute("src", "1"); + bufferSize.setAttribute("dst", "1"); + bufferSize.setAttribute("mem", "1"); + + // tokenSize + Element tokenSize = doc.createElement("tokenSize"); + tokenSize.setAttribute("sz", "1"); + + // bandwidth + Element bandwidth = doc.createElement("bandwidth"); + bandwidth.setAttribute("min", "1"); + + // latency + Element latency = doc.createElement("latency"); + latency.setAttribute("min", "0"); + + channelProperties.appendChild(bufferSize); + channelProperties.appendChild(tokenSize); + channelProperties.appendChild(bandwidth); + channelProperties.appendChild(latency); + sdfProperties.appendChild(channelProperties); + } + + // Write dom document to a file. + String path = this.mocasinDir.toString() + "/sdf" + filePostfix + ".xml"; + try (FileOutputStream output = new FileOutputStream(path)) { + writeXml(doc, output); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return path; + } + + public void setProcessorWcet( + Document doc, Element actorProperties, int processorTypeId, DagNode node, TimeValue wcet) { + // processor + Element processor = doc.createElement("processor"); + processor.setAttribute("type", "proc_type_" + processorTypeId); + processor.setAttribute("default", "true"); + + // executionTime + Element executionTime = doc.createElement("executionTime"); + if (node.isAuxiliary()) executionTime.setAttribute("time", "0"); + else executionTime.setAttribute("time", ((Long) wcet.toNanoSeconds()).toString()); + + // memory + Element memory = doc.createElement("memory"); + + // stateSize + Element stateSize = doc.createElement("stateSize"); + stateSize.setAttribute("max", "1"); // FIXME: What does this do? This is currently hardcoded. + + // Append elements. + memory.appendChild(stateSize); + processor.appendChild(executionTime); + processor.appendChild(memory); + actorProperties.appendChild(processor); + } + + /** Write XML doc to output stream */ + private static void writeXml(Document doc, OutputStream output) throws TransformerException { + + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty( + "{http://xml.apache.org/xslt}indent-amount", "2"); // Indent by 2 spaces + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(output); + + transformer.transform(source, result); + } + + /** Check whether an XML file is valid wrt a schema file (XSD) */ + public static boolean validateXMLSchema(String xsdPath, String xmlPath) { + try { + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + Schema schema = factory.newSchema(new File(xsdPath)); + Validator validator = schema.newValidator(); + validator.validate(new StreamSource(new File(xmlPath))); + } catch (IOException e) { + System.out.println("Exception: " + e.getMessage()); + return false; + } catch (SAXException e1) { + System.out.println("SAX Exception: " + e1.getMessage()); + return false; + } + return true; + } + + /** Main function for assigning nodes to workers */ + public Dag partitionDag(Dag dagRaw, int fragmentId, int numWorkers, String filePostfix) { + + // Prune redundant edges. + dagRaw.removeRedundantEdges(); + Dag dagPruned = dagRaw; + + // Generate a dot file. + Path filePruned = graphDir.resolve("dag_pruned" + filePostfix + ".dot"); + dagPruned.generateDotFile(filePruned); + + // If mocasinMapping is empty, generate SDF3 files. + // Turn the DAG into the SDF3 format. + Dag dagSdf = turnDagIntoSdfFormat(dagPruned); + + // Generate a dot file. + Path fileSDF = graphDir.resolve("dag_sdf" + filePostfix + ".dot"); + dagSdf.generateDotFile(fileSDF); + + // Write an XML file in SDF3 format. + String xmlPath = ""; + try { + xmlPath = generateSDF3XML(dagSdf, filePostfix); + } catch (Exception e) { + throw new RuntimeException(e); + } + assert !xmlPath.equals("") : "XML path is empty."; + + // Validate the generated XML. + try { + FileUtil.copyFromClassPath("/staticScheduler/mocasin/sdf3-sdf.xsd", mocasinDir, false, false); + } catch (IOException e) { + throw new RuntimeException(e); + } + String xsdPath = mocasinDir.resolve("sdf3-sdf.xsd").toString(); + if (!validateXMLSchema(xsdPath, xmlPath)) { + throw new RuntimeException("The generated SDF3 XML is invalid."); + } + + // Return early if there are no mappings provided. + if (targetConfig.get(SchedulerProperty.INSTANCE).mocasinMapping() == null + || targetConfig.get(SchedulerProperty.INSTANCE).mocasinMapping().size() == 0) return null; + + // Otherwise, parse mappings and generate instructions. + // ASSUMPTION: dagPruned here is the same as the DAG used for generating + // the mocasin mapping, otherwise the generated schedule is faulty. + String mappingFilePath = + targetConfig.get(SchedulerProperty.INSTANCE).mocasinMapping().get(fragmentId); + + // Generate a string map from parsing the csv file. + Map mapping = parseMocasinMappingFirstDataRow(mappingFilePath); + + // Collect reaction nodes. + // Note: these nodes need to have the same string names as the ones in + // the previously mapping file. We need to find a way to assign the same string names + // based on the DAG topology, not based on memory address nor the order of + // dag node creation, because they change from run to run. Currently, this + // is done by adding a "count" field to dag nodes, which count the number of + // occurrences a node has occurred in a graph. + List reactionNodes = + dagPruned.dagNodes.stream() + .filter(node -> node.nodeType == dagNodeType.REACTION) + .collect(Collectors.toCollection(ArrayList::new)); + + // Create a partition map that takes worker names to a list of reactions. + Map> partitionMap = new HashMap<>(); + + // Populate the partition map. + for (var node : reactionNodes) { + // Get the name of the worker (e.g., Core A on Board B) assigned by mocasin. + String workerName = mapping.get(node.toString()); + + // Create a list if it is currently null. + if (partitionMap.get(workerName) == null) partitionMap.put(workerName, new ArrayList<>()); + + // Add a reaction to the partition. + partitionMap.get(workerName).add(node); + } + + // Query the partitionMap to populate partitions and workerNames + // in the DAG. + for (var partitionKeyVal : partitionMap.entrySet()) { + + String workerName = partitionKeyVal.getKey(); + List partition = partitionKeyVal.getValue(); + + // Add partition to dag. + dagPruned.partitions.add(partition); + + // Add worker name. + dagPruned.workerNames.add(workerName); + } + + // Assign colors and worker IDs to partitions. + StaticSchedulerUtils.assignColorsToPartitions(dagPruned); + + // Generate a dot file. + Path filePartitioned = graphDir.resolve("dag_partitioned" + filePostfix + ".dot"); + dagPruned.generateDotFile(filePartitioned); + + return dagPruned; + } + + /** + * Parse the first data row of a CSV file and return a string map, which maps column name to data + */ + public static Map parseMocasinMappingFirstDataRow(String fileName) { + Map mappings = new HashMap<>(); + + try (BufferedReader br = new BufferedReader(new FileReader(fileName))) { + // Read the first line to get column names + String[] columns = br.readLine().split(","); + + // Read the next line to get the first row of data + String[] values = br.readLine().split(","); + + // Create mappings between column names and values + for (int i = 0; i < columns.length; i++) { + // Remove the "t_" prefix before insertion from the column names. + if (columns[i].substring(0, 2).equals("t_")) columns[i] = columns[i].substring(2); + // Update mapping. + mappings.put(columns[i], values[i]); + } + + } catch (IOException e) { + e.printStackTrace(); + } + + return mappings; + } + + /** + * If the number of workers is unspecified, determine a value for the number of workers. This + * scheduler base class simply returns 1. An advanced scheduler is free to run advanced algorithms + * here. + */ + public int setNumberOfWorkers() { + return 1; + } +} diff --git a/core/src/main/java/org/lflang/analyses/scheduler/StaticScheduler.java b/core/src/main/java/org/lflang/analyses/scheduler/StaticScheduler.java new file mode 100644 index 0000000000..250ffadfeb --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/scheduler/StaticScheduler.java @@ -0,0 +1,14 @@ +package org.lflang.analyses.scheduler; + +import org.lflang.analyses.dag.Dag; + +/** + * Interface for static scheduler + * + * @author Shaokai Lin + */ +public interface StaticScheduler { + public Dag partitionDag(Dag dag, int fragmentId, int workers, String filePostfix); + + public int setNumberOfWorkers(); +} diff --git a/core/src/main/java/org/lflang/analyses/scheduler/StaticSchedulerUtils.java b/core/src/main/java/org/lflang/analyses/scheduler/StaticSchedulerUtils.java new file mode 100644 index 0000000000..216752cc4d --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/scheduler/StaticSchedulerUtils.java @@ -0,0 +1,35 @@ +package org.lflang.analyses.scheduler; + +import java.util.List; +import java.util.Random; +import org.lflang.analyses.dag.Dag; +import org.lflang.analyses.dag.DagNode; + +/** + * A utility class for static scheduler-related methods + * + * @author Shaokai Lin + */ +public class StaticSchedulerUtils { + + public static String generateRandomColor() { + Random random = new Random(); + int r = random.nextInt(256); + int g = random.nextInt(256); + int b = random.nextInt(256); + + return String.format("#%02X%02X%02X", r, g, b); + } + + public static void assignColorsToPartitions(Dag dag) { + // Assign colors to each partition + for (int j = 0; j < dag.partitions.size(); j++) { + List partition = dag.partitions.get(j); + String randomColor = StaticSchedulerUtils.generateRandomColor(); + for (int i = 0; i < partition.size(); i++) { + partition.get(i).setColor(randomColor); + partition.get(i).setWorker(j); + } + } + } +} diff --git a/core/src/main/java/org/lflang/analyses/statespace/Event.java b/core/src/main/java/org/lflang/analyses/statespace/Event.java index 7f8b54b20b..8736a5d1c1 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/Event.java +++ b/core/src/main/java/org/lflang/analyses/statespace/Event.java @@ -2,7 +2,11 @@ import org.lflang.generator.TriggerInstance; -/** A node in the state space diagram representing a step in the execution of an LF program. */ +/** + * A node in the state space diagram representing a step in the execution of an LF program. + * + * @author Shaokai Lin + */ public class Event implements Comparable { private final TriggerInstance trigger; diff --git a/core/src/main/java/org/lflang/analyses/statespace/EventQueue.java b/core/src/main/java/org/lflang/analyses/statespace/EventQueue.java index 7c04206da7..3768121ab5 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/EventQueue.java +++ b/core/src/main/java/org/lflang/analyses/statespace/EventQueue.java @@ -5,6 +5,8 @@ /** * An event queue implementation that sorts events in the order of _time tags_ and _trigger names_ * based on the implementation of compareTo() in the Event class. + * + * @author Shaokai Lin */ public class EventQueue extends PriorityQueue { diff --git a/core/src/main/java/org/lflang/analyses/statespace/StateInfo.java b/core/src/main/java/org/lflang/analyses/statespace/StateInfo.java index 89c3bd19de..a54da560af 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/StateInfo.java +++ b/core/src/main/java/org/lflang/analyses/statespace/StateInfo.java @@ -3,7 +3,11 @@ import java.util.ArrayList; import java.util.HashMap; -/** A class that represents information in a step in a counterexample trace */ +/** + * A class that represents information in a step in a counterexample trace + * + * @author Shaokai Lin + */ public class StateInfo { public ArrayList reactions = new ArrayList<>(); diff --git a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceDiagram.java b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceDiagram.java index e2aa66737d..76f70965ec 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceDiagram.java +++ b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceDiagram.java @@ -1,14 +1,21 @@ package org.lflang.analyses.statespace; +import java.io.IOException; +import java.nio.file.Path; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.lflang.TimeValue; +import org.lflang.analyses.statespace.StateSpaceExplorer.Phase; import org.lflang.generator.CodeBuilder; import org.lflang.generator.ReactionInstance; import org.lflang.graph.DirectedGraph; -/** A directed graph representing the state space of an LF program. */ +/** + * A directed graph representing the state space of an LF program. + * + * @author Shaokai Lin + */ public class StateSpaceDiagram extends DirectedGraph { /** The first node of the state space diagram. */ @@ -29,13 +36,29 @@ public class StateSpaceDiagram extends DirectedGraph { */ public StateSpaceNode loopNodeNext; - /** The logical time elapsed for each loop iteration. */ - public long loopPeriod; + /** + * The logical time elapsed for each loop iteration. With the assumption of "logical time = + * physical time," this is also the hyperperiod in physical time. + */ + public long hyperperiod; + + /** The exploration phase in which this diagram is generated */ + public Phase phase; + + /** + * True if this diagram is asynchronous, meaning that it is started by a physical action. We can + * integrate an asynchronous diagram into a synchronous diagram based on minimum spacing, under an + * interpretation that minimum spacing means periodic polling. + */ + private boolean isAsync = false; + + /* Minimum spacing */ + private TimeValue minSpacing; /** A dot file that represents the diagram */ private CodeBuilder dot; - /** */ + /** A flag that indicates whether we want the dot to be compact */ private final boolean compactDot = false; /** Before adding the node, assign it an index. */ @@ -107,7 +130,7 @@ public CodeBuilder generateDot() { dot = new CodeBuilder(); dot.pr("digraph G {"); dot.indent(); - if (this.loopNode != null) { + if (this.isCyclic()) { dot.pr("layout=circo;"); } dot.pr("rankdir=LR;"); @@ -193,7 +216,7 @@ public CodeBuilder generateDot() { if (loopNode != null) { TimeValue tsDiff = TimeValue.fromNanoSeconds(loopNodeNext.getTag().timestamp - tail.getTag().timestamp); - TimeValue period = TimeValue.fromNanoSeconds(loopPeriod); + TimeValue period = TimeValue.fromNanoSeconds(hyperperiod); dot.pr( "S" + current.getIndex() @@ -216,4 +239,44 @@ public CodeBuilder generateDot() { } return this.dot; } + + public void generateDotFile(Path filepath) { + try { + CodeBuilder dot = generateDot(); + String filename = filepath.toString(); + dot.writeToFile(filename); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** Check if the diagram is periodic by checking if the loop node is set. */ + public boolean isCyclic() { + return loopNode != null; + } + + /** Check if the diagram is empty. */ + public boolean isEmpty() { + return (head == null); + } + + /** Check if the diagram is asynchronous, i.e., whether it is triggered by a physical action. */ + public boolean isAsync() { + return isAsync; + } + + /** Indicate that this diagram is asynchronous. */ + public void makeAsync() { + isAsync = true; + } + + /** Get the minimum spacing of the diagram */ + public TimeValue getMinSpacing() { + return minSpacing; + } + + /** Set the minimum spacing of the diagram */ + public void setMinSpacing(TimeValue minSpacing) { + this.minSpacing = minSpacing; + } } diff --git a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceExplorer.java b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceExplorer.java index 6b869e7ebd..c37be797f6 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceExplorer.java +++ b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceExplorer.java @@ -3,9 +3,9 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Set; -import org.lflang.TimeUnit; -import org.lflang.TimeValue; +import org.lflang.ast.ASTUtils; import org.lflang.generator.ActionInstance; import org.lflang.generator.PortInstance; import org.lflang.generator.ReactionInstance; @@ -15,167 +15,116 @@ import org.lflang.generator.TimerInstance; import org.lflang.generator.TriggerInstance; import org.lflang.lf.Expression; -import org.lflang.lf.Time; import org.lflang.lf.Variable; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.TimeOutProperty; /** * (EXPERIMENTAL) Explores the state space of an LF program. Use with caution since this is * experimental code. + * + * @author Shaokai Lin */ public class StateSpaceExplorer { - // Instantiate an empty state space diagram. - public StateSpaceDiagram diagram = new StateSpaceDiagram(); - - // Indicate whether a back loop is found in the state space. - // A back loop suggests periodic behavior. - public boolean loopFound = false; - /** - * Instantiate a global event queue. We will use this event queue to symbolically simulate the - * logical timeline. This simulation is also valid for runtime implementations that are federated - * or relax global barrier synchronization, since an LF program defines a unique logical timeline - * (assuming all reactions behave _consistently_ throughout the execution). + * Common phases of a logical timeline, some of which are provided to the explorer as directives. */ - public EventQueue eventQ = new EventQueue(); - - /** The main reactor instance based on which the state space is explored. */ - public ReactorInstance main; - - // Constructor - public StateSpaceExplorer(ReactorInstance main) { - this.main = main; + public enum Phase { + PREAMBLE, + INIT, // Dominated by startup triggers and initial timer firings + PERIODIC, + EPILOGUE, + SYNC_BLOCK, + INIT_AND_PERIODIC, + SHUTDOWN_TIMEOUT, + SHUTDOWN_STARVATION, + ASYNC, // Dominated by physical actions } - /** Recursively add the first events to the event queue. */ - public void addInitialEvents(ReactorInstance reactor) { - // Add the startup trigger, if exists. - var startup = reactor.getStartupTrigger(); - if (startup != null) eventQ.add(new Event(startup, new Tag(0, 0, false))); - - // Add the initial timer firings, if exist. - for (TimerInstance timer : reactor.timers) { - eventQ.add(new Event(timer, new Tag(timer.getOffset().toNanoSeconds(), 0, false))); - } + /** Target configuration */ + TargetConfig targetConfig; - // Recursion - for (var child : reactor.children) { - addInitialEvents(child); - } + /** Constructor */ + public StateSpaceExplorer(TargetConfig targetConfig) { + this.targetConfig = targetConfig; } /** * Explore the state space and populate the state space diagram until the specified horizon (i.e. * the end tag) is reached OR until the event queue is empty. * - *

As an optimization, if findLoop is true, the algorithm tries to find a loop in the state - * space during exploration. If a loop is found (i.e. a previously encountered state is reached - * again) during exploration, the function returns early. + *

As an optimization, the algorithm tries to find a loop in the state space during + * exploration. If a loop is found (i.e. a previously encountered state is reached again) during + * exploration, the function returns early. + * + *

If the phase is INIT_AND_PERIODIC, the explorer starts with startup triggers and timers' + * initial firings. If the phase is SHUTDOWN_*, the explorer starts with shutdown triggers. * - *

TODOs: 1. Handle action with 0 minimum delay. + *

TODOs: 1. Handle action with 0 minimum delay. 2. Handle hierarchical reactors. * - *

Note: This is experimental code which is to be refactored in a future PR. Use with caution. + *

Note: This is experimental code. Use with caution. */ - public void explore(Tag horizon, boolean findLoop) { - // Traverse the main reactor instance recursively to find - // the known initial events (startup and timers' first firings). - // FIXME: It seems that we need to handle shutdown triggers - // separately, because they could break the back loop. - addInitialEvents(this.main); - + public StateSpaceDiagram explore( + ReactorInstance main, Tag horizon, Phase phase, List initialEvents) { + if (!(phase == Phase.INIT_AND_PERIODIC + || phase == Phase.SHUTDOWN_TIMEOUT + || phase == Phase.SHUTDOWN_STARVATION + || phase == Phase.ASYNC)) + throw new RuntimeException("Unsupported phase detected in the explorer."); + + // Variable initilizations + StateSpaceDiagram diagram = new StateSpaceDiagram(); + diagram.phase = phase; + EventQueue eventQ = new EventQueue(); Tag previousTag = null; // Tag in the previous loop ITERATION Tag currentTag = null; // Tag in the current loop ITERATION StateSpaceNode currentNode = null; StateSpaceNode previousNode = null; HashMap uniqueNodes = new HashMap<>(); boolean stop = true; - if (this.eventQ.size() > 0) { + + // Add initial events to the event queue. + eventQ.addAll(initialEvents); + + // Set appropriate fields if the phase is ASYNC. + if (phase == Phase.ASYNC) { + if (eventQ.size() != 1) + throw new RuntimeException( + "When exploring the ASYNC phase, there should be only ONE initial event at a time." + + " eventQ.size() = " + + eventQ.size()); + diagram.makeAsync(); + diagram.setMinSpacing(((ActionInstance) eventQ.peek().getTrigger()).getMinSpacing()); + } + + // Check if we should stop already. + if (eventQ.size() > 0) { stop = false; currentTag = eventQ.peek().getTag(); } // A list of reactions invoked at the current logical tag Set reactionsInvoked; + // A temporary list of reactions processed in the current LOOP ITERATION + Set reactionsTemp; + // Iterate until stop conditions are met. while (!stop) { // Pop the events from the earliest tag off the event queue. - ArrayList currentEvents = new ArrayList(); - while (eventQ.size() > 0 && eventQ.peek().getTag().compareTo(currentTag) == 0) { - Event e = eventQ.poll(); - currentEvents.add(e); - } + List currentEvents = popCurrentEvents(eventQ, currentTag); // Collect all the reactions invoked in this current LOOP ITERATION // triggered by the earliest events. - // Using a hash set here to make sure the reactions invoked - // are unique. Sometimes multiple events can trigger the same reaction, - // and we do not want to record duplicate reaction invocations. - - // A temporary list of reactions processed in the current LOOP ITERATION - Set reactionsTemp = new HashSet<>(); - for (Event e : currentEvents) { - Set dependentReactions = e.getTrigger().getDependentReactions(); - reactionsTemp.addAll(dependentReactions); - - // If the event is a timer firing, enqueue the next firing. - if (e.getTrigger() instanceof TimerInstance timer) { - eventQ.add( - new Event( - timer, - new Tag( - e.getTag().timestamp + timer.getPeriod().toNanoSeconds(), - 0, // A time advancement resets microstep to 0. - false))); - } - } + reactionsTemp = getReactionsTriggeredByCurrentEvents(currentEvents); // For each reaction invoked, compute the new events produced. - for (ReactionInstance reaction : reactionsTemp) { - // Iterate over all the effects produced by this reaction. - // If the effect is a port, obtain the downstream port along - // a connection and enqueue a future event for that port. - // If the effect is an action, enqueue a future event for - // this action. - for (TriggerInstance effect : reaction.effects) { - if (effect instanceof PortInstance) { - - for (SendRange senderRange : ((PortInstance) effect).getDependentPorts()) { - - for (RuntimeRange destinationRange : senderRange.destinations) { - PortInstance downstreamPort = destinationRange.instance; - - // Getting delay from connection - // FIXME: Is there a more concise way to do this? - long delay = 0; - Expression delayExpr = senderRange.connection.getDelay(); - if (delayExpr instanceof Time) { - long interval = ((Time) delayExpr).getInterval(); - String unit = ((Time) delayExpr).getUnit(); - TimeValue timeValue = new TimeValue(interval, TimeUnit.fromName(unit)); - delay = timeValue.toNanoSeconds(); - } - - // Create and enqueue a new event. - Event e = - new Event(downstreamPort, new Tag(currentTag.timestamp + delay, 0, false)); - eventQ.add(e); - } - } - } else if (effect instanceof ActionInstance) { - // Get the minimum delay of this action. - long min_delay = ((ActionInstance) effect).getMinDelay().toNanoSeconds(); - long microstep = 0; - if (min_delay == 0) { - microstep = currentTag.microstep + 1; - } - // Create and enqueue a new event. - Event e = - new Event(effect, new Tag(currentTag.timestamp + min_delay, microstep, false)); - eventQ.add(e); - } - } - } + List newEvents = createNewEvents(currentEvents, reactionsTemp, currentTag); + // FIXME: Need to make sure that addAll() is using the overridden version + // that makes sure new events added are unique. By default, this should be + // the case. + eventQ.addAll(newEvents); // We are at the first iteration. // Initialize currentNode. @@ -206,24 +155,33 @@ public void explore(Tag horizon, boolean findLoop) { // at the timestamp-level, so that we don't have to // worry about microsteps. else if (previousTag != null && currentTag.timestamp > previousTag.timestamp) { + // Check if we are in the SHUTDOWN_TIMEOUT mode, + // if so, stop the loop immediately, because TIMEOUT is the last tag. + if (phase == Phase.SHUTDOWN_TIMEOUT) { + // Make the hyperperiod for the SHUTDOWN_TIMEOUT phase Long.MAX_VALUE, + // so that this is guaranteed to be feasibile from the perspective of + // the EGS scheduler. + diagram.hyperperiod = Long.MAX_VALUE; + diagram.loopNode = null; // The SHUTDOWN_TIMEOUT phase is acyclic. + break; + } + // Whenever we finish a tag, check for loops fist. // If currentNode matches an existing node in uniqueNodes, // duplicate is set to the existing node. StateSpaceNode duplicate; - if (findLoop && (duplicate = uniqueNodes.put(currentNode.hash(), currentNode)) != null) { + if ((duplicate = uniqueNodes.put(currentNode.hash(), currentNode)) != null) { // Mark the loop in the diagram. - loopFound = true; - this.diagram.loopNode = duplicate; - this.diagram.loopNodeNext = currentNode; - this.diagram.tail = previousNode; + diagram.loopNode = duplicate; + diagram.loopNodeNext = currentNode; + diagram.tail = previousNode; // Loop period is the time difference between the 1st time // the node is reached and the 2nd time the node is reached. - this.diagram.loopPeriod = - this.diagram.loopNodeNext.getTag().timestamp - - this.diagram.loopNode.getTag().timestamp; - this.diagram.addEdge(this.diagram.loopNode, this.diagram.tail); - return; // Exit the while loop early. + diagram.hyperperiod = + diagram.loopNodeNext.getTag().timestamp - diagram.loopNode.getTag().timestamp; + diagram.addEdge(diagram.loopNode, diagram.tail); + return diagram; // Exit the while loop early. } // Now we are at a new tag, and a loop is not found, @@ -231,16 +189,14 @@ else if (previousTag != null && currentTag.timestamp > previousTag.timestamp) { // Adding a node to the graph once it is finalized // because this makes checking duplicate nodes easier. // We don't have to remove a node from the graph. - this.diagram.addNode(currentNode); - this.diagram.tail = currentNode; // Update the current tail. + diagram.addNode(currentNode); + diagram.tail = currentNode; // Update the current tail. // If the head is not empty, add an edge from the previous state // to the next state. Otherwise initialize the head to the new node. if (previousNode != null) { - // System.out.println("--- Add a new edge between " + currentNode + " and " + node); - // this.diagram.addEdge(currentNode, previousNode); // Sink first, then source - if (previousNode != currentNode) this.diagram.addEdge(currentNode, previousNode); - } else this.diagram.head = currentNode; // Initialize the head. + if (previousNode != currentNode) diagram.addEdge(currentNode, previousNode); + } else diagram.head = currentNode; // Initialize the head. //// Now we are done with the node at the previous tag, //// work on the new node at the current timestamp. @@ -287,7 +243,10 @@ else if (previousTag != null && currentTag.timestamp == previousTag.timestamp) { // 2. the horizon is reached. if (eventQ.size() == 0) { stop = true; - } else if (currentTag.timestamp > horizon.timestamp) { + } + // FIXME: If horizon is forever, explore() might not terminate. + // How to set a reasonable upperbound? + else if (!horizon.forever && currentTag.timestamp > horizon.timestamp) { stop = true; } } @@ -298,17 +257,222 @@ else if (previousTag != null && currentTag.timestamp == previousTag.timestamp) { // or (previousTag != null // && currentTag.compareTo(previousTag) > 0) is true and then // the simulation ends, leaving a new node dangling. - if (previousNode == null || previousNode.getTag().timestamp < currentNode.getTag().timestamp) { - this.diagram.addNode(currentNode); - this.diagram.tail = currentNode; // Update the current tail. + if (currentNode != null + && (previousNode == null + || previousNode.getTag().timestamp < currentNode.getTag().timestamp)) { + diagram.addNode(currentNode); + diagram.tail = currentNode; // Update the current tail. if (previousNode != null) { - this.diagram.addEdge(currentNode, previousNode); + diagram.addEdge(currentNode, previousNode); } } - // When we exit and we still don't have a head, - // that means there is only one node in the diagram. + // At this point if we still don't have a head, + // then it means there is only one node in the diagram. // Set the current node as the head. - if (this.diagram.head == null) this.diagram.head = currentNode; + if (diagram.head == null) diagram.head = currentNode; + + return diagram; + } + + ////////////////////////////////////////////////////// + ////////////////// Private Methods + + /** + * Return a (unordered) list of initial events to be given to the state space explorer based on a + * given phase. + * + * @param reactor The reactor wrt which initial events are inferred + * @param phase The phase for which initial events are inferred + * @return A list of initial events + */ + public static List addInitialEvents( + ReactorInstance reactor, Phase phase, TargetConfig targetConfig) { + List events = new ArrayList<>(); + addInitialEventsRecursive(reactor, events, phase, targetConfig); + return events; + } + + /** + * Recursively add the first events to the event list for state space exploration. For the + * SHUTDOWN modes, it is okay to create shutdown events at (0,0) because this tag is a relative + * offset wrt to a phase (e.g., the shutdown phase), not the absolute tag at runtime. + */ + public static void addInitialEventsRecursive( + ReactorInstance reactor, List events, Phase phase, TargetConfig targetConfig) { + switch (phase) { + case INIT_AND_PERIODIC: + { + // Add the startup trigger, if exists. + var startup = reactor.getStartupTrigger(); + if (startup != null) events.add(new Event(startup, new Tag(0, 0, false))); + + // Add the initial timer firings, if exist. + for (TimerInstance timer : reactor.timers) { + events.add(new Event(timer, new Tag(timer.getOffset().toNanoSeconds(), 0, false))); + } + break; + } + case SHUTDOWN_TIMEOUT: + { + // To get the state space of the instant at shutdown, + // we over-approximate by assuming all triggers are present at + // (timeout, 0). This could generate unnecessary instructions + // for reactions that are not meant to trigger at (timeout, 0), + // but they will be treated as NOPs at runtime. + + // Add the shutdown trigger, if exists. + var shutdown = reactor.getShutdownTrigger(); + if (shutdown != null) events.add(new Event(shutdown, new Tag(0, 0, false))); + + // Check for timers that fire at (timeout, 0). + for (TimerInstance timer : reactor.timers) { + // If timeout = timer.offset + N * timer.period for some non-negative + // integer N, add a timer event. + Long offset = timer.getOffset().toNanoSeconds(); + Long period = timer.getPeriod().toNanoSeconds(); + Long timeout = targetConfig.get(TimeOutProperty.INSTANCE).toNanoSeconds(); + if (period != 0 && (timeout - offset) % period == 0) { + // The tag is set to (0,0) because, again, this is relative to the + // shutdown phase, not the actual absolute tag at runtime. + events.add(new Event(timer, new Tag(0, 0, false))); + } + } + + // Assume all input ports and logical actions present. + // FIXME: Also physical action. Will add it later. + for (PortInstance input : reactor.inputs) { + events.add(new Event(input, new Tag(0, 0, false))); + } + for (ActionInstance logicalAction : + reactor.actions.stream().filter(it -> !it.isPhysical()).toList()) { + events.add(new Event(logicalAction, new Tag(0, 0, false))); + } + break; + } + case SHUTDOWN_STARVATION: + { + // Add the shutdown trigger, if exists. + var shutdown = reactor.getShutdownTrigger(); + if (shutdown != null) events.add(new Event(shutdown, new Tag(0, 0, false))); + break; + } + case ASYNC: + { + for (ActionInstance physicalAction : + reactor.actions.stream().filter(it -> it.isPhysical()).toList()) { + events.add(new Event(physicalAction, new Tag(0, 0, false))); + } + break; + } + default: + throw new RuntimeException("UNREACHABLE"); + } + + // Recursion + for (var child : reactor.children) { + addInitialEventsRecursive(child, events, phase, targetConfig); + } + } + + /** Pop events with currentTag off an eventQ */ + private List popCurrentEvents(EventQueue eventQ, Tag currentTag) { + List currentEvents = new ArrayList<>(); + // FIXME: Use stream methods here? + while (eventQ.size() > 0 && eventQ.peek().getTag().compareTo(currentTag) == 0) { + Event e = eventQ.poll(); + currentEvents.add(e); + } + return currentEvents; + } + + /** + * Return a list of reaction instances triggered by a list of current events. The events must + * carry the same tag. Using a hash set here to make sure the reactions invoked are unique. + * Sometimes multiple events can trigger the same reaction, and we do not want to record duplicate + * reaction invocations. + */ + private Set getReactionsTriggeredByCurrentEvents(List currentEvents) { + Set reactions = new HashSet<>(); + for (Event e : currentEvents) { + Set dependentReactions = e.getTrigger().getDependentReactions(); + reactions.addAll(dependentReactions); + } + return reactions; + } + + /** + * Create a list of new events from reactions invoked at current tag. These new events should be + * able to trigger reactions, which means that the method needs to compute how events propagate + * downstream. + * + *

FIXME: This function does not handle port hierarchies, or the lack of them, yet. It should + * be updated with a new implementation that uses eventualDestinations() from PortInstance.java. + * But the challenge is to also get the delays. Perhaps eventualDestinations() should be extended + * to collect delays. + */ + private List createNewEvents( + List currentEvents, Set reactions, Tag currentTag) { + + List newEvents = new ArrayList<>(); + + // If the event is a timer firing, enqueue the next firing. + for (Event e : currentEvents) { + if (e.getTrigger() instanceof TimerInstance) { + TimerInstance timer = (TimerInstance) e.getTrigger(); + newEvents.add( + new Event( + timer, + new Tag( + e.getTag().timestamp + timer.getPeriod().toNanoSeconds(), + 0, // A time advancement resets microstep to 0. + false))); + } + } + + // For each reaction invoked, compute the new events produced + // that can immediately trigger reactions. + for (ReactionInstance reaction : reactions) { + // Iterate over all the effects produced by this reaction. + // If the effect is a port, obtain the downstream port along + // a connection and enqueue a future event for that port. + // If the effect is an action, enqueue a future event for + // this action. + for (TriggerInstance effect : reaction.effects) { + // If the reaction writes to a port. + if (effect instanceof PortInstance) { + + for (SendRange senderRange : ((PortInstance) effect).getDependentPorts()) { + + for (RuntimeRange destinationRange : senderRange.destinations) { + PortInstance downstreamPort = destinationRange.instance; + + // Getting delay from connection + Expression delayExpr = senderRange.connection.getDelay(); + Long delay = ASTUtils.getDelay(delayExpr); + if (delay == null) delay = 0L; + + // Create and enqueue a new event. + Event e = new Event(downstreamPort, new Tag(currentTag.timestamp + delay, 0, false)); + newEvents.add(e); + } + } + } + // Ensure we only generate new events for LOGICAL actions. + else if (effect instanceof ActionInstance && !((ActionInstance) effect).isPhysical()) { + // Get the minimum delay of this action. + long min_delay = ((ActionInstance) effect).getMinDelay().toNanoSeconds(); + long microstep = 0; + if (min_delay == 0) { + microstep = currentTag.microstep + 1; + } + // Create and enqueue a new event. + Event e = new Event(effect, new Tag(currentTag.timestamp + min_delay, microstep, false)); + newEvents.add(e); + } + } + } + + return newEvents; } } diff --git a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceFragment.java b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceFragment.java new file mode 100644 index 0000000000..705e74c296 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceFragment.java @@ -0,0 +1,104 @@ +package org.lflang.analyses.statespace; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.lflang.analyses.pretvm.PretVmObjectFile; +import org.lflang.analyses.pretvm.instructions.Instruction; +import org.lflang.analyses.statespace.StateSpaceExplorer.Phase; + +/** + * A state space fragment contains a state space diagram and references to other state space + * diagrams. A fragment is meant to capture partial behavior of an LF program (for example, the + * initialization phase, periodic phase, or shutdown phases). + * + * @author Shaokai Lin + */ +public class StateSpaceFragment { + + /** + * A static fragment for the EPILOGUE phase Static fragments do not go into the fragments list and + * their instructions are directly injected at link time. The EPILOGUE static fragment is only + * here to make sure fragments generated in generateStateSpaceFragments() properly transition to + * the EPILOGUE after they are done. There is no need to have another static PREAMBLE fragment, + * since no fragments transition into PREAMBLE. + */ + public static final StateSpaceFragment EPILOGUE; + + static { + // FIXME: It is unclear whether it is better to put STP in the object files. + StateSpaceDiagram epilogueDiagram = new StateSpaceDiagram(); + epilogueDiagram.phase = Phase.EPILOGUE; + EPILOGUE = new StateSpaceFragment(epilogueDiagram); + } + + /** The state space diagram contained in this fragment */ + StateSpaceDiagram diagram; + + /** A list of upstream fragments */ + List upstreams = new ArrayList<>(); + + /** + * A map from downstream fragments to their guard. A guard is a conditional branch wrapped in a + * List. FIXME: All workers for now will evaluate the same guard. This is arguably + * redundant work that needs to be optimized away. + */ + Map> downstreams = new HashMap<>(); + + /** Pointer to an object file corresponding to this fragment */ + PretVmObjectFile objectFile; + + /** Constructor */ + public StateSpaceFragment() {} + + /** Constructor */ + public StateSpaceFragment(StateSpaceDiagram diagram) { + this.diagram = diagram; + } + + /** Check if the fragment is cyclic. */ + public boolean isCyclic() { + return diagram.isCyclic(); + } + + /** Diagram getter */ + public StateSpaceDiagram getDiagram() { + return diagram; + } + + /** Get state space diagram phase. */ + public Phase getPhase() { + return diagram.phase; + } + + /** Get object file. */ + public PretVmObjectFile getObjectFile() { + return objectFile; + } + + /** Upstream getter */ + public List getUpstreams() { + return upstreams; + } + + /** Downstream getter */ + public Map> getDownstreams() { + return downstreams; + } + + /** Add an upstream fragment */ + public void addUpstream(StateSpaceFragment upstream) { + this.upstreams.add(upstream); + } + + /** Add an downstream fragment with a guarded transition */ + public void addDownstream(StateSpaceFragment downstream, List guard) { + this.downstreams.put(downstream, guard); + } + + /** Set object file */ + public void setObjectFile(PretVmObjectFile objectFile) { + this.objectFile = objectFile; + } +} diff --git a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceNode.java b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceNode.java index 0061853157..7f54fbda0b 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceNode.java +++ b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceNode.java @@ -1,14 +1,17 @@ package org.lflang.analyses.statespace; import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; import org.lflang.TimeValue; import org.lflang.generator.ReactionInstance; import org.lflang.generator.TriggerInstance; -/** A node in the state space diagram representing a step in the execution of an LF program. */ +/** + * A node in the state space diagram representing a step in the execution of an LF program. + * + * @author Shaokai Lin + */ public class StateSpaceNode { private int index; // Set in StateSpaceDiagram.java @@ -25,6 +28,14 @@ public StateSpaceNode( this.time = TimeValue.fromNanoSeconds(tag.timestamp); } + /** Copy constructor */ + public StateSpaceNode(StateSpaceNode that) { + this.tag = new Tag(that.tag); + this.eventQcopy = new ArrayList<>(that.eventQcopy); + this.reactionsInvoked = new HashSet<>(that.reactionsInvoked); + this.time = TimeValue.fromNanoSeconds(that.tag.timestamp); + } + /** Two methods for pretty printing */ public void display() { System.out.println("(" + this.time + ", " + reactionsInvoked + ", " + eventQcopy + ")"); @@ -50,23 +61,20 @@ public int hash() { result = 31 * result + reactionsInvoked.hashCode(); // Generate hash for the triggers in the queued events. - List eventNames = - this.eventQcopy.stream() + int eventsHash = + this.getEventQcopy().stream() .map(Event::getTrigger) .map(TriggerInstance::getFullName) - .collect(Collectors.toList()); - result = 31 * result + eventNames.hashCode(); - - // Generate hash for a list of time differences between future events' tags and - // the current tag. - List timeDiff = - this.eventQcopy.stream() - .map( - e -> { - return e.getTag().timestamp - this.tag.timestamp; - }) - .collect(Collectors.toList()); - result = 31 * result + timeDiff.hashCode(); + .mapToInt(Object::hashCode) + .reduce(1, (a, b) -> 31 * a + b); + result = 31 * result + eventsHash; + + // Generate hash for the time differences. + long timeDiffHash = + this.getEventQcopy().stream() + .mapToLong(e -> e.getTag().timestamp - this.tag.timestamp) + .reduce(1, (a, b) -> 31 * a + b); + result = 31 * result + (int) timeDiffHash; return result; } @@ -83,6 +91,11 @@ public Tag getTag() { return tag; } + public void setTag(Tag newTag) { + tag = newTag; + time = TimeValue.fromNanoSeconds(tag.timestamp); + } + public TimeValue getTime() { return time; } diff --git a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceUtils.java b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceUtils.java new file mode 100644 index 0000000000..8ece54f406 --- /dev/null +++ b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceUtils.java @@ -0,0 +1,341 @@ +package org.lflang.analyses.statespace; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.lflang.analyses.pretvm.Registers; +import org.lflang.analyses.pretvm.instructions.Instruction; +import org.lflang.analyses.pretvm.instructions.InstructionJAL; +import org.lflang.analyses.statespace.StateSpaceExplorer.Phase; +import org.lflang.generator.ReactorInstance; +import org.lflang.target.TargetConfig; + +/** + * A utility class for state space-related methods + * + * @author Shaokai Lin + */ +public class StateSpaceUtils { + + /** + * Connect two fragments with a default transition (no guards). Changing the default transition + * here would require changing isDefaultTransition() also. + */ + public static void connectFragmentsDefault( + StateSpaceFragment upstream, StateSpaceFragment downstream) { + List defaultTransition = + Arrays.asList( + new InstructionJAL( + Registers.ABSTRACT_WORKER_RETURN_ADDR, + downstream.getPhase())); // Default transition + upstream.addDownstream(downstream, defaultTransition); + downstream.addUpstream(upstream); + } + + /** Connect two fragments with a guarded transition. */ + public static void connectFragmentsGuarded( + StateSpaceFragment upstream, + StateSpaceFragment downstream, + List guardedTransition) { + upstream.addDownstream(downstream, guardedTransition); + downstream.addUpstream(upstream); + } + + /** + * A helper function that generates a state space diagram for an LF program based on an + * exploration phase. + */ + public static StateSpaceDiagram generateStateSpaceDiagram( + StateSpaceExplorer explorer, + StateSpaceExplorer.Phase explorePhase, + ReactorInstance main, + Tag horizon, + TargetConfig targetConfig, + Path graphDir, + String graphPrefix) { + // Get a list of initial events according to the exploration phase. + List initialEvents = + StateSpaceExplorer.addInitialEvents(main, explorePhase, targetConfig); + + // Explore the state space with the phase specified. + StateSpaceDiagram stateSpaceDiagram = + explorer.explore(main, horizon, explorePhase, initialEvents); + + // Generate a dot file. + if (!stateSpaceDiagram.isEmpty()) { + Path file = graphDir.resolve(graphPrefix + ".dot"); + stateSpaceDiagram.generateDotFile(file); + } + + return stateSpaceDiagram; + } + + /** + * A helper function that generates a state space diagram for an LF program based on an + * exploration phase. + */ + public static List generateAsyncStateSpaceDiagrams( + StateSpaceExplorer explorer, + StateSpaceExplorer.Phase explorePhase, + ReactorInstance main, + Tag horizon, + TargetConfig targetConfig, + Path graphDir, + String graphPrefix) { + + // Check if the mode is ASYNC. + if (explorePhase != Phase.ASYNC) + throw new RuntimeException( + "The exploration mode must be ASYNC inside generateStateSpaceDiagramAsync()."); + + // Create a list. + List diagramList = new ArrayList<>(); + + // Collect a list of asynchronous events (physical action). + List asyncEvents = StateSpaceExplorer.addInitialEvents(main, Phase.ASYNC, targetConfig); + + // For each asynchronous event, run the explore function. + for (int i = 0; i < asyncEvents.size(); i++) { + // Get an event. + Event event = asyncEvents.get(i); + + // Explore the state space with the phase specified. + StateSpaceDiagram stateSpaceDiagram = + explorer.explore(main, new Tag(0, 0, true), explorePhase, Arrays.asList(event)); + + // Generate a dot file. + if (!stateSpaceDiagram.isEmpty()) { + Path file = graphDir.resolve(graphPrefix + "_" + i + ".dot"); + stateSpaceDiagram.generateDotFile(file); + } + + diagramList.add(stateSpaceDiagram); + } + + return diagramList; + } + + /** + * Identify an initialization phase and a periodic phase of the state space diagram, and create + * two different state space diagrams. + */ + public static ArrayList splitInitAndPeriodicDiagrams( + StateSpaceDiagram stateSpace) { + + ArrayList diagrams = new ArrayList<>(); + StateSpaceNode current = stateSpace.head; + StateSpaceNode previous = null; + + // Create an initialization phase diagram. + if (stateSpace.head != stateSpace.loopNode) { + StateSpaceDiagram initPhase = new StateSpaceDiagram(); + initPhase.head = current; + while (current != stateSpace.loopNode) { + // Add node and edges to diagram. + initPhase.addNode(current); + initPhase.addEdge(current, previous); + + // Update current and previous pointer. + previous = current; + current = stateSpace.getDownstreamNode(current); + } + initPhase.tail = previous; + if (stateSpace.loopNode != null) + initPhase.hyperperiod = stateSpace.loopNode.getTime().toNanoSeconds(); + else initPhase.hyperperiod = 0; + initPhase.phase = Phase.INIT; + diagrams.add(initPhase); + } + + // Create a periodic phase diagram. + if (stateSpace.isCyclic()) { + + // State this assumption explicitly. + assert current == stateSpace.loopNode : "Current is not pointing to loopNode."; + + StateSpaceDiagram periodicPhase = new StateSpaceDiagram(); + periodicPhase.head = current; + periodicPhase.addNode(current); // Add the first node. + if (current == stateSpace.tail) { + periodicPhase.addEdge(current, current); // Add edges to diagram. + } + while (current != stateSpace.tail) { + // Update current and previous pointer. + // We bring the updates before addNode() because + // we need to make sure tail is added. + // For the init diagram, we do not want to add loopNode. + previous = current; + current = stateSpace.getDownstreamNode(current); + + // Add node and edges to diagram. + periodicPhase.addNode(current); + periodicPhase.addEdge(current, previous); + } + periodicPhase.tail = current; + periodicPhase.loopNode = stateSpace.loopNode; + periodicPhase.addEdge(periodicPhase.loopNode, periodicPhase.tail); // Add loop. + periodicPhase.loopNodeNext = stateSpace.loopNodeNext; + periodicPhase.hyperperiod = stateSpace.hyperperiod; + periodicPhase.phase = Phase.PERIODIC; + diagrams.add(periodicPhase); + } + + return diagrams; + } + + /** Check if a transition is a default transition. */ + public static boolean isDefaultTransition(List transition) { + return transition.size() == 1 && (transition.get(0) instanceof InstructionJAL); + } + + /** + * Merge a list of async diagrams into a non-async diagram. + * + *

FIXME: This is not an efficient algorithm since every time a new diagram gets merged, the + * size of the merged diagram could blow up exponentially. A one-pass algorithm would be better. + * + * @param asyncDiagrams A list of async diagrams to be merged in + * @param targetDiagram The target diagram accepting async diagrams + * @return A merged diagram + */ + public static StateSpaceDiagram mergeAsyncDiagramsIntoDiagram( + List asyncDiagrams, StateSpaceDiagram targetDiagram) { + StateSpaceDiagram mergedDiagram = targetDiagram; + for (var diagram : asyncDiagrams) { + mergedDiagram = mergeAsyncDiagramIntoDiagram(diagram, targetDiagram); + } + return mergedDiagram; + } + + /** + * Merge an async diagram into a non-async fragment based on minimum spacing, which in this case + * is interpreted as the period at which the presence of the physical action. + * + *

In the state space exploration, generate a diagram for EACH physical action's data path. + * Then associate a minimum spacing for each diagram. When calling this merge function, the + * algorithm performs merging based on the indidual minimum spacing specifications. + * + *

ASSUMPTIONS: + * + *

1. min spacing <= hyperperiod. If minimum space > hyperperiod, we then need to unroll the + * synchronous diagram. + * + *

2. min spacing is a divisor of the hyperperiod. This simplifies the placement of async nodes + * and removes the need to account for the shift of async node sequence over multiple iterations. + * To relax this assumption, we need to recalculate the hyperperiod of a periodic diagram with the + * addition of async nodes. + * + *

3. Physical action does not occur at the same tag as synchronous nodes. This removes the + * need to merge two nodes at the same tag. This is not necessarily hard, however. + * + *

4. The sequence of async nodes is shifted 1 nsec after the sync sequence starts. This is a + * best effort approach to avoid collision between sync and async nodes, in which case assumption + * 3 is activated. This also makes it easy for the merge algorithm to work on a single diagram + * without worrying about the temporal relations between its async nodes and other diagrams' async + * nodes, i.e., enabling a compositional merge strategy. + * + *

5. Sometimes, the actual minimum spacing in the schedule could be greater than the specified + * minimum spacing. This could occur due to a few reasons: A) Assumption 3; B) In the transition + * between the initialization phase and the periodic phase, the LAST async node in the + * initialization phase can be dropped to make sure the FIRST async node in the periodic phase + * does not break the min spacing requirement due to compositional merge strategy. This is not a + * problem if we consider a global merge strategy, i.e., using a single diagram to represent the + * logical behavior and merge the async nodes across multiple phases at once. + */ + public static StateSpaceDiagram mergeAsyncDiagramIntoDiagram( + StateSpaceDiagram asyncDiagram, StateSpaceDiagram targetDiagram) { + System.out.println("*** Inside merge algorithm."); + + StateSpaceDiagram mergedDiagram = new StateSpaceDiagram(); + + // Inherit phase from targetDiagram + mergedDiagram.phase = targetDiagram.phase; + + // Keep track of the current node of the target diagram. + StateSpaceNode current = targetDiagram.head; + + // Tracking the lastAdded of the merged diagram. + StateSpaceNode lastAdded = null; + + // Assuming the async diagram only has 1 node. + StateSpaceNode asyncNode = asyncDiagram.head; + + // Set the first tag of the async node to be 1 nsec after the head of the + // target diagram. This is a best effort approach to avoid tag collision + // between the synchronous sequence and the asynchronous sequence. + Tag asyncTag = new Tag(targetDiagram.head.getTag().timestamp + 1, 0, false); + + System.out.println("asyncTag = " + asyncTag); + + boolean stop = false; + while (!stop) { + + // Decide if async node or the current node should be added. + if (asyncTag.compareTo(current.getTag()) < 0) { + // Create a new async node. + StateSpaceNode asyncNodeNew = new StateSpaceNode(asyncNode); + asyncNodeNew.setTag(asyncTag); + mergedDiagram.addNode(asyncNodeNew); + mergedDiagram.addEdge(asyncNodeNew, lastAdded); + + // Update lastAdded + lastAdded = asyncNodeNew; + + // Update async tag + asyncTag = + new Tag(asyncTag.timestamp + asyncDiagram.getMinSpacing().toNanoSeconds(), 0, false); + + System.out.println("Added async node."); + } else { + + // Add the current node of the synchronous diagram. + mergedDiagram.addNode(current); + mergedDiagram.addEdge(current, lastAdded); + + // Check if the current node is the loop node. + // If so, set it to the loop node in the new diagram. + if (current == targetDiagram.loopNode) mergedDiagram.loopNode = current; + + // Update current and previous pointer. + lastAdded = current; + current = targetDiagram.getDownstreamNode(current); + + System.out.println("Added current node."); + } + + if (mergedDiagram.head == null) mergedDiagram.head = lastAdded; + + if (lastAdded == targetDiagram.tail) { + // Create a new async node. + StateSpaceNode asyncNodeNew = new StateSpaceNode(asyncNode); + asyncNodeNew.setTag(asyncTag); + mergedDiagram.addNode(asyncNodeNew); + mergedDiagram.addEdge(asyncNodeNew, lastAdded); + System.out.println("Added async node."); + // Update lastAdded + lastAdded = asyncNodeNew; + // Set tail + mergedDiagram.tail = lastAdded; + // Inherit loopNodeNext from targetDiagram + mergedDiagram.loopNodeNext = targetDiagram.loopNodeNext; + // Inherit hyperperiod. + mergedDiagram.hyperperiod = targetDiagram.hyperperiod; + // Connect back to the loop node, if any. + if (targetDiagram.loopNode != null) { + System.out.println("targetDiagram.loopNode != null"); + targetDiagram.loopNode.display(); + mergedDiagram.addEdge(mergedDiagram.loopNode, lastAdded); + } else { + System.out.println("targetDiagram.loopNode == null!"); + } + stop = true; + } + } + + // FIXME: Display merged diagram + mergedDiagram.display(); + + return mergedDiagram; + } +} diff --git a/core/src/main/java/org/lflang/analyses/statespace/Tag.java b/core/src/main/java/org/lflang/analyses/statespace/Tag.java index f62dde6c32..dbcbde45fe 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/Tag.java +++ b/core/src/main/java/org/lflang/analyses/statespace/Tag.java @@ -5,6 +5,8 @@ /** * Class representing a logical time tag, which is a pair that consists of a timestamp (type long) * and a microstep (type long). + * + * @author Shaokai Lin */ public class Tag implements Comparable { @@ -12,12 +14,20 @@ public class Tag implements Comparable { public final long microstep; public final boolean forever; // Whether the tag is FOREVER into the future. + /** Constructor */ public Tag(long timestamp, long microstep, boolean forever) { this.timestamp = timestamp; this.microstep = microstep; this.forever = forever; } + /** Copy constructor */ + public Tag(Tag that) { + this.timestamp = that.timestamp; + this.microstep = that.microstep; + this.forever = that.forever; + } + @Override public int compareTo(Tag t) { // If one tag is forever, and the other is not, diff --git a/core/src/main/java/org/lflang/analyses/uclid/MTLVisitor.java b/core/src/main/java/org/lflang/analyses/uclid/MTLVisitor.java index 1aef6fe554..106d49bf35 100644 --- a/core/src/main/java/org/lflang/analyses/uclid/MTLVisitor.java +++ b/core/src/main/java/org/lflang/analyses/uclid/MTLVisitor.java @@ -31,7 +31,11 @@ import org.lflang.dsl.MTLParserBaseVisitor; import org.lflang.generator.CodeBuilder; -/** (EXPERIMENTAL) Transpiler from an MTL specification to a Uclid axiom. */ +/** + * (EXPERIMENTAL) Transpiler from an MTL specification to a Uclid axiom. + * + * @author Shaokai Lin + */ public class MTLVisitor extends MTLParserBaseVisitor { //////////////////////////////////////////// diff --git a/core/src/main/java/org/lflang/analyses/uclid/UclidGenerator.java b/core/src/main/java/org/lflang/analyses/uclid/UclidGenerator.java index 9253856fcc..194a95f946 100644 --- a/core/src/main/java/org/lflang/analyses/uclid/UclidGenerator.java +++ b/core/src/main/java/org/lflang/analyses/uclid/UclidGenerator.java @@ -37,8 +37,6 @@ import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.eclipse.emf.ecore.resource.Resource; -import org.lflang.TimeUnit; -import org.lflang.TimeValue; import org.lflang.analyses.c.AstUtils; import org.lflang.analyses.c.BuildAstParseTreeVisitor; import org.lflang.analyses.c.CAst; @@ -48,6 +46,7 @@ import org.lflang.analyses.statespace.StateSpaceDiagram; import org.lflang.analyses.statespace.StateSpaceExplorer; import org.lflang.analyses.statespace.StateSpaceNode; +import org.lflang.analyses.statespace.StateSpaceUtils; import org.lflang.analyses.statespace.Tag; import org.lflang.ast.ASTUtils; import org.lflang.dsl.CLexer; @@ -75,11 +74,14 @@ import org.lflang.lf.Attribute; import org.lflang.lf.Connection; import org.lflang.lf.Expression; -import org.lflang.lf.Time; import org.lflang.target.Target; import org.lflang.util.StringUtil; -/** (EXPERIMENTAL) Generator for Uclid5 models. */ +/** + * (EXPERIMENTAL) Generator for Uclid5 models. + * + * @author Shaokai Lin + */ public class UclidGenerator extends GeneratorBase { //////////////////////////////////////////// @@ -883,18 +885,9 @@ protected void generateConnectionAxioms() { List> destinations = range.destinations; // Extract delay value - long delay = 0; - if (connection.getDelay() != null) { - // Somehow delay is an Expression, - // which makes it hard to convert to nanoseconds. - Expression delayExpr = connection.getDelay(); - if (delayExpr instanceof Time) { - long interval = ((Time) delayExpr).getInterval(); - String unit = ((Time) delayExpr).getUnit(); - TimeValue timeValue = new TimeValue(interval, TimeUnit.fromName(unit)); - delay = timeValue.toNanoSeconds(); - } - } + Expression delayExpr = connection.getDelay(); + Long delay = ASTUtils.getDelay(delayExpr); + if (delay == null) delay = 0L; for (var portRange : destinations) { var destination = portRange.instance; @@ -1611,25 +1604,20 @@ private void populateLists(ReactorInstance reactor) { */ private void computeCT() { - StateSpaceExplorer explorer = new StateSpaceExplorer(this.main); - explorer.explore( - new Tag(this.horizon, 0, false), true // findLoop - ); - StateSpaceDiagram diagram = explorer.diagram; - diagram.display(); + StateSpaceExplorer explorer = new StateSpaceExplorer(targetConfig); - // Generate a dot file. - try { - CodeBuilder dot = diagram.generateDot(); - Path file = this.outputDir.resolve(this.tactic + "_" + this.name + ".dot"); - String filename = file.toString(); - dot.writeToFile(filename); - } catch (IOException e) { - throw new RuntimeException(e); - } + StateSpaceDiagram diagram = + StateSpaceUtils.generateStateSpaceDiagram( + explorer, + StateSpaceExplorer.Phase.INIT_AND_PERIODIC, + main, + new Tag(this.horizon, 0, false), + targetConfig, + outputDir, + this.tactic + "_" + this.name); //// Compute CT - if (!explorer.loopFound) { + if (!diagram.isCyclic()) { if (this.logicalTimeBased) this.CT = diagram.nodeCount(); else { // FIXME: This could be much more efficient with @@ -1652,15 +1640,15 @@ private void computeCT() { // Check how many loop iteration is required // to check the remaining horizon. int loopIterations = 0; - if (diagram.loopPeriod == 0 && horizonRemained != 0) + if (diagram.hyperperiod == 0 && horizonRemained != 0) throw new RuntimeException( "ERROR: Zeno behavior detected while the horizon is non-zero. The program has no" + " finite CT."); - else if (diagram.loopPeriod == 0 && horizonRemained == 0) { + else if (diagram.hyperperiod == 0 && horizonRemained == 0) { // Handle this edge case. throw new RuntimeException("Unhandled case: both the horizon and period are 0!"); } else { - loopIterations = (int) Math.ceil((double) horizonRemained / diagram.loopPeriod); + loopIterations = (int) Math.ceil((double) horizonRemained / diagram.hyperperiod); } if (this.logicalTimeBased) { diff --git a/core/src/main/java/org/lflang/analyses/uclid/UclidRunner.java b/core/src/main/java/org/lflang/analyses/uclid/UclidRunner.java index 4ec0703c79..d1c1b50f53 100644 --- a/core/src/main/java/org/lflang/analyses/uclid/UclidRunner.java +++ b/core/src/main/java/org/lflang/analyses/uclid/UclidRunner.java @@ -19,7 +19,11 @@ import org.lflang.generator.GeneratorCommandFactory; import org.lflang.util.LFCommand; -/** (EXPERIMENTAL) Runner for Uclid5 models. */ +/** + * (EXPERIMENTAL) Runner for Uclid5 models. + * + * @author Shaokai Lin + */ public class UclidRunner { /** A factory for compiler commands. */ diff --git a/core/src/main/java/org/lflang/ast/ASTUtils.java b/core/src/main/java/org/lflang/ast/ASTUtils.java index ef66d24cd1..57c81bebe6 100644 --- a/core/src/main/java/org/lflang/ast/ASTUtils.java +++ b/core/src/main/java/org/lflang/ast/ASTUtils.java @@ -103,6 +103,8 @@ import org.lflang.target.Target; import org.lflang.target.TargetConfig; import org.lflang.target.property.CompileDefinitionsProperty; +import org.lflang.target.property.SchedulerProperty; +import org.lflang.target.property.type.SchedulerType.Scheduler; import org.lflang.util.StringUtil; /** @@ -619,7 +621,10 @@ public static ReactorInstance createMainReactorInstance( ReactorInstance main = new ReactorInstance(toDefinition(mainDef.getReactorClass()), messageReporter, reactors); var reactionInstanceGraph = main.assignLevels(); - if (reactionInstanceGraph.nodeCount() > 0) { + // Check for causality cycles, + // except for the static scheduler. + if (reactionInstanceGraph.nodeCount() > 0 + && targetConfig.getOrDefault(SchedulerProperty.INSTANCE).type() != Scheduler.STATIC) { messageReporter .nowhere() .error("Main reactor has causality cycles. Skipping code generation."); diff --git a/core/src/main/java/org/lflang/ast/IsEqual.java b/core/src/main/java/org/lflang/ast/IsEqual.java index afe1568f96..b70b9cbf83 100644 --- a/core/src/main/java/org/lflang/ast/IsEqual.java +++ b/core/src/main/java/org/lflang/ast/IsEqual.java @@ -271,6 +271,7 @@ public Boolean caseAttrParm(AttrParm object) { return new ComparisonMachine<>(object, AttrParm.class) .equalAsObjects(AttrParm::getName) .equalAsObjects(AttrParm::getValue) + .equivalent(AttrParm::getTime) .conclusion; } diff --git a/core/src/main/java/org/lflang/ast/ToLf.java b/core/src/main/java/org/lflang/ast/ToLf.java index 5f32920d9b..eb268bc43b 100644 --- a/core/src/main/java/org/lflang/ast/ToLf.java +++ b/core/src/main/java/org/lflang/ast/ToLf.java @@ -312,10 +312,13 @@ public MalleableString caseAttribute(Attribute object) { @Override public MalleableString caseAttrParm(AttrParm object) { - // (name=ID '=')? value=AttrParmValue; + // (name=ID '=')? (value=Literal | time=Time); var builder = new Builder(); if (object.getName() != null) builder.append(object.getName()).append(" = "); - return builder.append(object.getValue()).get(); + if (object.getValue() != null) builder.append(object.getValue()); + else if (object.getTime() != null) builder.append(doSwitch(object.getTime())); + else throw new IllegalArgumentException("AttrParm can either be Literal or Time, not both."); + return builder.get(); } @Override diff --git a/core/src/main/java/org/lflang/generator/PortInstance.java b/core/src/main/java/org/lflang/generator/PortInstance.java index d25d5b362b..57418ac436 100644 --- a/core/src/main/java/org/lflang/generator/PortInstance.java +++ b/core/src/main/java/org/lflang/generator/PortInstance.java @@ -139,6 +139,18 @@ public void clearCaches() { * as the number of ports in its destinations field because some of the ports may share the same * container reactor. */ + public List eventualDestinationsOrig() { + if (eventualDestinationRangesOrig != null) { + return eventualDestinationRangesOrig; + } + + // Construct the full range for this port. + RuntimeRange range = new RuntimeRange.Port(this); + eventualDestinationRangesOrig = eventualDestinations(range, true); + return eventualDestinationRangesOrig; + } + + /** Return a list of eventual destinations without skipping delayed or physical connections. */ public List eventualDestinations() { if (eventualDestinationRanges != null) { return eventualDestinationRanges; @@ -146,7 +158,7 @@ public List eventualDestinations() { // Construct the full range for this port. RuntimeRange range = new RuntimeRange.Port(this); - eventualDestinationRanges = eventualDestinations(range); + eventualDestinationRanges = eventualDestinations(range, false); return eventualDestinationRanges; } @@ -257,7 +269,8 @@ public int getLevelUpperBound(MixedRadixInt index) { * * @param srcRange The source range. */ - private static List eventualDestinations(RuntimeRange srcRange) { + private static List eventualDestinations( + RuntimeRange srcRange, boolean skipDelayedConnections) { // Getting the destinations is more complex than getting the sources // because of multicast, where there is more than one connection statement @@ -291,9 +304,14 @@ private static List eventualDestinations(RuntimeRange s // Need to find send ranges that overlap with this srcRange. for (SendRange wSendRange : srcPort.dependentPorts) { - if (wSendRange.connection != null - && (wSendRange.connection.getDelay() != null || wSendRange.connection.isPhysical())) { - continue; + // IMPORTANT FIXME: We need to find a good way to manange the AST + // transformation. We cannot just delete the lines below because + // deleting these lines breaks the validator! + if (skipDelayedConnections) { + if (wSendRange.connection != null + && (wSendRange.connection.getDelay() != null || wSendRange.connection.isPhysical())) { + continue; + } } wSendRange = wSendRange.overlap(srcRange); @@ -303,7 +321,7 @@ private static List eventualDestinations(RuntimeRange s } for (RuntimeRange dstRange : wSendRange.destinations) { // Recursively get the send ranges of that destination port. - List dstSendRanges = eventualDestinations(dstRange); + List dstSendRanges = eventualDestinations(dstRange, skipDelayedConnections); int sendRangeStart = 0; for (SendRange dstSend : dstSendRanges) { queue.add(dstSend.newSendRange(wSendRange, sendRangeStart)); @@ -447,6 +465,9 @@ private void setInitialWidth(MessageReporter messageReporter) { /** Cached list of destination ports with channel ranges. */ private List eventualDestinationRanges; + /** Cached list of destination ports with channel ranges. */ + private List eventualDestinationRangesOrig; + /** Cached list of source ports with channel ranges. */ private List> eventualSourceRanges; diff --git a/core/src/main/java/org/lflang/generator/ReactionInstance.java b/core/src/main/java/org/lflang/generator/ReactionInstance.java index 029eb13f0a..695fb5926d 100644 --- a/core/src/main/java/org/lflang/generator/ReactionInstance.java +++ b/core/src/main/java/org/lflang/generator/ReactionInstance.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Set; import org.eclipse.xtext.xbase.lib.StringExtensions; +import org.lflang.AttributeUtils; import org.lflang.TimeValue; import org.lflang.ast.ASTUtils; import org.lflang.lf.Action; @@ -41,6 +42,7 @@ import org.lflang.lf.VarRef; import org.lflang.lf.Variable; import org.lflang.lf.Watchdog; +import org.lflang.util.Pair; /** * Representation of a compile-time instance of a reaction. Like {@link ReactorInstance}, if one or @@ -176,6 +178,8 @@ public ReactionInstance(Reaction definition, ReactorInstance parent, int index) if (this.definition.getDeadline() != null) { this.declaredDeadline = new DeadlineInstance(this.definition.getDeadline(), this); } + // If @wcet annotation is specified, update the wcet. + this.wcets = AttributeUtils.getWCETs(this.definition); } ////////////////////////////////////////////////////// @@ -210,6 +214,12 @@ public ReactionInstance(Reaction definition, ReactorInstance parent, int index) */ public Set> triggers = new LinkedHashSet<>(); + /** + * The worst-case execution time (WCET) of the reaction. Note that this is platform dependent. If + * the WCET is unknown, set it to the maximum value. + */ + public List wcets = new ArrayList<>(List.of(TimeValue.MAX_VALUE)); + ////////////////////////////////////////////////////// //// Public methods. @@ -246,7 +256,7 @@ public Set dependentReactions() { // Next, add reactions that get data from this one via a port. for (TriggerInstance effect : effects) { if (effect instanceof PortInstance) { - for (SendRange senderRange : ((PortInstance) effect).eventualDestinations()) { + for (SendRange senderRange : ((PortInstance) effect).eventualDestinationsOrig()) { for (RuntimeRange destinationRange : senderRange.destinations) { dependentReactionsCache.addAll(destinationRange.instance.dependentReactions); } @@ -293,6 +303,38 @@ public Set dependsOnReactions() { return dependsOnReactionsCache; } + /** + * Return the set of immediate downstream reactions, which are reactions that receive data + * produced by this reaction, paired with an associated delay along a connection. + * + *

FIXME: Add caching. FIXME: The use of `port.dependentPorts` here restricts the supported LF + * programs to a single hierarchy. More needs to be done to relax this. + */ + public Set> downstreamReactions() { + LinkedHashSet> downstreamReactions = new LinkedHashSet<>(); + // Add reactions that get data from this one via a port, coupled with the + // delay value. + for (TriggerInstance effect : effects) { + if (effect instanceof PortInstance port) { + for (SendRange senderRange : port.dependentPorts) { + Long delay = 0L; + if (senderRange.connection == null) { + System.out.println("WARNING: senderRange (" + senderRange + ") has a null connection."); + continue; + } + var delayExpr = senderRange.connection.getDelay(); + if (delayExpr != null) delay = ASTUtils.getDelay(senderRange.connection.getDelay()); + for (RuntimeRange destinationRange : senderRange.destinations) { + for (var dependentReaction : destinationRange.instance.dependentReactions) { + downstreamReactions.add(new Pair(dependentReaction, delay)); + } + } + } + } + } + return downstreamReactions; + } + /** * Return a set of levels that runtime instances of this reaction have. A ReactionInstance may * have more than one level if it lies within a bank and its dependencies on other reactions pass diff --git a/core/src/main/java/org/lflang/generator/c/CEnvironmentFunctionGenerator.java b/core/src/main/java/org/lflang/generator/c/CEnvironmentFunctionGenerator.java index 3789638ded..8c15128f32 100644 --- a/core/src/main/java/org/lflang/generator/c/CEnvironmentFunctionGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CEnvironmentFunctionGenerator.java @@ -27,7 +27,7 @@ public CEnvironmentFunctionGenerator( public String generateDeclarations() { CodeBuilder code = new CodeBuilder(); - code.pr(generateEnvironmentEnum()); + code.pr("#include " + "\"" + lfModuleName + ".h" + "\""); code.pr(generateEnvironmentArray()); return code.toString(); } @@ -61,7 +61,7 @@ private String generateGetEnvironments() { "}"); } - private String generateEnvironmentEnum() { + public String generateEnvironmentEnum() { CodeBuilder code = new CodeBuilder(); code.pr("typedef enum {"); code.indent(); diff --git a/core/src/main/java/org/lflang/generator/c/CGenerator.java b/core/src/main/java/org/lflang/generator/c/CGenerator.java index dda5c06d89..b66aa7c0b8 100644 --- a/core/src/main/java/org/lflang/generator/c/CGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CGenerator.java @@ -98,11 +98,14 @@ import org.lflang.target.property.PlatformProperty.PlatformOption; import org.lflang.target.property.ProtobufsProperty; import org.lflang.target.property.SchedulerProperty; +import org.lflang.target.property.SchedulerProperty.SchedulerOptions; import org.lflang.target.property.SingleThreadedProperty; +import org.lflang.target.property.StaticSchedulerProperty; import org.lflang.target.property.TracingProperty; import org.lflang.target.property.WorkersProperty; import org.lflang.target.property.type.PlatformType.Platform; import org.lflang.target.property.type.SchedulerType.Scheduler; +import org.lflang.target.property.type.StaticSchedulerType.StaticScheduler; import org.lflang.util.ArduinoUtil; import org.lflang.util.FileUtil; import org.lflang.util.FlexPRETUtil; @@ -322,6 +325,15 @@ public class CGenerator extends GeneratorBase { private final CCmakeGenerator cmakeGenerator; + /** A list of reactor instances */ + private List reactorInstances = new ArrayList<>(); + + /** A list of reaction instances */ + private List reactionInstances = new ArrayList<>(); + + /** A list of trigger instances */ + private List reactionTriggers = new ArrayList<>(); + protected CGenerator( LFGeneratorContext context, boolean cppMode, @@ -334,9 +346,11 @@ protected CGenerator( this.types = types; this.cmakeGenerator = cmakeGenerator; - registerTransformation( - new DelayedConnectionTransformation( - delayConnectionBodyGenerator, types, fileConfig.resource, true, true)); + // Perform the AST transformation for delayed connection if it is enabled. + if (targetConfig.useDelayedConnectionTransformation()) + registerTransformation( + new DelayedConnectionTransformation( + delayConnectionBodyGenerator, types, fileConfig.resource, true, true)); } public CGenerator(LFGeneratorContext context, boolean ccppMode) { @@ -438,6 +452,24 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { throw e; } + // Create a static schedule if the static scheduler is used. + if (targetConfig.getOrDefault(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC) { + // FIXME: Factor out the following. + // If --static-schedule is set on the command line, + // update the SchedulerOptions record. + if (targetConfig.isSet(StaticSchedulerProperty.INSTANCE)) { + // Store the static scheduler specified on the command line. + StaticScheduler staticScheduler = targetConfig.get(StaticSchedulerProperty.INSTANCE); + // Generate a new SchedulerOptions record. + SchedulerOptions updatedRecord = + targetConfig.get(SchedulerProperty.INSTANCE).update(staticScheduler); + // Call the update API to update the scheduler property. + SchedulerProperty.INSTANCE.update(targetConfig, updatedRecord); + } + System.out.println("--- Generating a static schedule"); + generateStaticSchedule(); + } + // Inform the runtime of the number of watchdogs // TODO: Can we do this at a better place? We need to do it when we have the main reactor // since we need main to get all enclaves. @@ -461,6 +493,9 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { .map(CUtil::getName) .map(it -> it + (cppMode ? ".cpp" : ".c")) .collect(Collectors.toCollection(ArrayList::new)); + // If STATIC scheduler is used, add the schedule file. + if (targetConfig.get(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC) + sources.add("static_schedule.c"); sources.add(cFilename); var cmakeCode = cmakeGenerator.generateCMakeCode(sources, cppMode, mainDef != null, cMakeExtras, context); @@ -594,7 +629,6 @@ private void generateCodeFor(String lfModuleName) throws IOException { // Skip generation if there are cycles. if (main != null) { var envFuncGen = new CEnvironmentFunctionGenerator(main, targetConfig, lfModuleName); - code.pr(envFuncGen.generateDeclarations()); initializeTriggerObjects.pr( String.join( @@ -624,7 +658,15 @@ private void generateCodeFor(String lfModuleName) throws IOException { // Generate function to initialize the trigger objects for all reactors. code.pr( CTriggerObjectsGenerator.generateInitializeTriggerObjects( - main, targetConfig, initializeTriggerObjects, startTimeStep, types, lfModuleName)); + main, + targetConfig, + initializeTriggerObjects, + startTimeStep, + types, + lfModuleName, + reactorInstances, + reactionInstances, + reactionTriggers)); // Generate a function that will either do nothing // (if there is only one federate or the coordination @@ -654,6 +696,9 @@ void lf_terminate_execution(environment_t* env) { (void) env; } #endif"""); + + // Generate a separate header file for the module at the same time. + generateModuleHeader(envFuncGen, lfModuleName); } } @@ -745,11 +790,13 @@ else if (term.getParameter() != null) private void pickScheduler() { // Don't use a scheduler that does not prioritize reactions based on deadlines // if the program contains a deadline (handler). Use the GEDF_NP scheduler instead. - if (!targetConfig.get(SchedulerProperty.INSTANCE).prioritizesDeadline()) { + if (!(targetConfig.get(SchedulerProperty.INSTANCE).type() != null + && targetConfig.get(SchedulerProperty.INSTANCE).type().prioritizesDeadline())) { // Check if a deadline is assigned to any reaction if (hasDeadlines(reactors)) { if (!targetConfig.isSet(SchedulerProperty.INSTANCE)) { - SchedulerProperty.INSTANCE.override(targetConfig, Scheduler.GEDF_NP); + SchedulerProperty.INSTANCE.override( + targetConfig, new SchedulerOptions(Scheduler.GEDF_NP)); } } } @@ -853,12 +900,14 @@ private void generateHeaders() throws IOException { p -> builder.pr( CPortGenerator.generateAuxiliaryStruct( + targetConfig, it.tpr, p, getTarget(), messageReporter, types, new CodeBuilder(), + new CodeBuilder(), true, it.decl())))); } @@ -869,6 +918,18 @@ private void generateHeaders() throws IOException { fileConfig.getIncludePath(), fileConfig.getSrcGenPath().resolve("include"), false); } + /** Generate a header for the LF module, so that enums can be shared across files. */ + private void generateModuleHeader(CEnvironmentFunctionGenerator genEnv, String lfModuleName) + throws IOException { + String contents = + String.join( + "\n", + "// Code generated by the Lingua Franca compiler from:", + "// file:/" + FileUtil.toUnixString(fileConfig.srcFile), + genEnv.generateEnvironmentEnum()); + FileUtil.writeToFile(contents, fileConfig.getSrcGenPath().resolve(lfModuleName + ".h")); + } + /** * Recursively generate code for the children of the given reactor and collect preambles and * relevant target properties associated with imported reactors. @@ -1011,6 +1072,10 @@ protected void generateReactorClassHeaders( header.pr("extern \"C\" {"); } header.pr("#include \"include/core/reactor.h\""); + + // Used for static scheduler's connection buffer only. + header.pr("#include \"include/core/utils/circular_buffer.h\""); + src.pr("#include \"include/api/schedule.h\""); if (CPreambleGenerator.arduinoBased(targetConfig)) { src.pr("#include \"include/low_level_platform/api/low_level_platform.h\""); @@ -1109,10 +1174,28 @@ protected void generateAuxiliaryStructs( #endif """, types.getTargetTagType(), types.getTargetTimeType())); + // Additional fields related to static scheduling + var staticExtension = new CodeBuilder(); + staticExtension.pr( + """ + #if SCHEDULER == SCHED_STATIC + circular_buffer** pqueues; + int num_pqueues; + #endif + """); for (Port p : allPorts(tpr.reactor())) { builder.pr( CPortGenerator.generateAuxiliaryStruct( - tpr, p, getTarget(), messageReporter, types, federatedExtension, userFacing, null)); + targetConfig, + tpr, + p, + getTarget(), + messageReporter, + types, + federatedExtension, + staticExtension, + userFacing, + null)); } // The very first item on this struct needs to be // a trigger_t* because the struct will be cast to (trigger_t*) @@ -1151,7 +1234,7 @@ private void generateSelfStruct( CActionGenerator.generateDeclarations(tpr, body, constructorCode); // Next handle inputs and outputs. - CPortGenerator.generateDeclarations(tpr, reactor, body, constructorCode); + CPortGenerator.generateDeclarations(tpr, types, body, constructorCode); // If there are contained reactors that either receive inputs // from reactions of this reactor or produce outputs that trigger @@ -1171,6 +1254,9 @@ private void generateSelfStruct( // Next, generate fields for modes CModesGenerator.generateDeclarations(reactor, body, constructorCode); + // Code generate allocation and init of the output ports pointer array + CPortGenerator.generateOutputPortsPointerArray(tpr, reactor, constructorCode); + // The first field has to always be a pointer to the list of // of allocated memory that must be freed when the reactor is freed. // This means that the struct can be safely cast to self_base_t. @@ -1517,6 +1603,7 @@ private void generateStartTimeStep(ReactorInstance instance) { + portRef + con + "is_present;"); + // Intended_tag is only applicable to ports in federated execution. temp.pr( CExtensionUtils.surroundWithIfFederatedDecentralized( @@ -2037,7 +2124,8 @@ protected boolean setUpGeneralParameters() { CompileDefinitionsProperty.INSTANCE.update( targetConfig, Map.of( - "SCHEDULER", targetConfig.get(SchedulerProperty.INSTANCE).getSchedulerCompileDef(), + "SCHEDULER", + targetConfig.get(SchedulerProperty.INSTANCE).type().getSchedulerCompileDef(), "NUMBER_OF_WORKERS", String.valueOf(targetConfig.get(WorkersProperty.INSTANCE)))); } if (targetConfig.isSet(PlatformProperty.INSTANCE)) { @@ -2190,4 +2278,18 @@ private void generateSelfStructs(ReactorInstance r) { private Stream allTypeParameterizedReactors() { return ASTUtils.recursiveChildren(main).stream().map(it -> it.tpr).distinct(); } + + /** Helper function for generating static schedules */ + private void generateStaticSchedule() { + CStaticScheduleGenerator schedGen = + new CStaticScheduleGenerator( + this.fileConfig, + this.targetConfig, + this.messageReporter, + this.main, + this.reactorInstances, + this.reactionInstances, + this.reactionTriggers); + schedGen.generate(); + } } diff --git a/core/src/main/java/org/lflang/generator/c/CPortGenerator.java b/core/src/main/java/org/lflang/generator/c/CPortGenerator.java index 56c2d85ff7..f06af766a0 100644 --- a/core/src/main/java/org/lflang/generator/c/CPortGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CPortGenerator.java @@ -12,6 +12,9 @@ import org.lflang.lf.Port; import org.lflang.lf.ReactorDecl; import org.lflang.target.Target; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.SchedulerProperty; +import org.lflang.target.property.type.SchedulerType.Scheduler; /** * Generates C code to declare and initialize ports. @@ -23,12 +26,45 @@ public class CPortGenerator { /** Generate fields in the self struct for input and output ports */ public static void generateDeclarations( - TypeParameterizedReactor tpr, - ReactorDecl decl, - CodeBuilder body, - CodeBuilder constructorCode) { - generateInputDeclarations(tpr, body, constructorCode); - generateOutputDeclarations(tpr, body, constructorCode); + TypeParameterizedReactor tpr, CTypes types, CodeBuilder body, CodeBuilder constructorCode) { + generateInputDeclarations(tpr, types, body, constructorCode); + generateOutputDeclarations(tpr, types, body, constructorCode); + } + + /** + * This code-generates the allocation and initialization of the `output_ports` pointer-array on + * the self_base_t. It is used by the STATIC scheduler to reset `is_present` fields on a + * per-reactor level. Standard way is resetting all `is_present` fields at the beginning of each + * tag. With STATIC scheduler we advance time in different reactors individually and must also + * reset the `is_present` fields individually. + * + * @param tpr + * @param decl + * @param constructorCode + */ + public static void generateOutputPortsPointerArray( + TypeParameterizedReactor tpr, ReactorDecl decl, CodeBuilder constructorCode) { + + var outputs = ASTUtils.allOutputs(tpr.reactor()); + int numOutputs = outputs.size(); + + constructorCode.pr("#ifdef REACTOR_LOCAL_TIME"); + constructorCode.pr("self->base.num_output_ports = " + numOutputs + ";"); + constructorCode.pr( + "self->base.output_ports = (lf_port_base_t **) calloc(" + + numOutputs + + ", sizeof(lf_port_base_t*));"); + constructorCode.pr("LF_ASSERT(self->base.output_ports != NULL, \"Out of memory\");"); + + for (int i = 0; i < numOutputs; i++) { + constructorCode.pr( + "self->base.output_ports[" + + i + + "]= (lf_port_base_t *) &self->_lf_" + + outputs.get(i).getName() + + ";"); + } + constructorCode.pr("#endif"); } /** @@ -46,12 +82,14 @@ public static void generateDeclarations( * @return The auxiliary struct for the port as a string */ public static String generateAuxiliaryStruct( + TargetConfig targetConfig, TypeParameterizedReactor tpr, Port port, Target target, MessageReporter messageReporter, CTypes types, CodeBuilder federatedExtension, + CodeBuilder staticExtension, boolean userFacing, ReactorDecl decl) { assert decl == null || userFacing; @@ -72,6 +110,9 @@ public static String generateAuxiliaryStruct( "lf_port_internal_t _base;")); code.pr(valueDeclaration(tpr, port, target, messageReporter, types)); code.pr(federatedExtension.toString()); + if (targetConfig.get(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC) { + code.pr(staticExtension.toString()); + } code.unindent(); var name = decl != null @@ -213,7 +254,7 @@ private static String valueDeclaration( * pointer. */ private static void generateInputDeclarations( - TypeParameterizedReactor tpr, CodeBuilder body, CodeBuilder constructorCode) { + TypeParameterizedReactor tpr, CTypes types, CodeBuilder body, CodeBuilder constructorCode) { for (Input input : ASTUtils.allInputs(tpr.reactor())) { var inputName = input.getName(); if (ASTUtils.isMultiport(input)) { @@ -249,12 +290,16 @@ private static void generateInputDeclarations( "\n", "// Set the default source reactor pointer", "self->_lf_default__" + inputName + "._base.source_reactor = (self_base_t*)self;")); + // Initialize element_size in the port struct. + var rootType = CUtil.rootType(types.getTargetType(input)); + var size = (rootType.equals("void")) ? "0" : "sizeof(" + rootType + ")"; + constructorCode.pr("self->_lf_" + inputName + "->type.element_size = " + size + ";"); } } /** Generate fields in the self struct for output ports */ private static void generateOutputDeclarations( - TypeParameterizedReactor tpr, CodeBuilder body, CodeBuilder constructorCode) { + TypeParameterizedReactor tpr, CTypes types, CodeBuilder body, CodeBuilder constructorCode) { for (Output output : ASTUtils.allOutputs(tpr.reactor())) { // If the port is a multiport, create an array to be allocated // at instantiation. @@ -280,6 +325,15 @@ private static void generateOutputDeclarations( variableStructType(output, tpr, false) + " _lf_" + outputName + ";", "int _lf_" + outputName + "_width;")); } + constructorCode.pr( + String.join( + "\n", + "// Set the default source reactor pointer", + "self->_lf_" + outputName + "._base.source_reactor = (self_base_t*)self;")); + // Initialize element_size in the port struct. + var rootType = CUtil.rootType(types.getTargetType(output)); + var size = (rootType.equals("void")) ? "0" : "sizeof(" + rootType + ")"; + constructorCode.pr("self->_lf_" + outputName + ".type.element_size = " + size + ";"); } } } diff --git a/core/src/main/java/org/lflang/generator/c/CPreambleGenerator.java b/core/src/main/java/org/lflang/generator/c/CPreambleGenerator.java index 1b3a6d22d9..16d5c8b51b 100644 --- a/core/src/main/java/org/lflang/generator/c/CPreambleGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CPreambleGenerator.java @@ -8,9 +8,11 @@ import org.lflang.target.property.FedSetupProperty; import org.lflang.target.property.LoggingProperty; import org.lflang.target.property.PlatformProperty; +import org.lflang.target.property.SchedulerProperty; import org.lflang.target.property.SingleThreadedProperty; import org.lflang.target.property.TracingProperty; import org.lflang.target.property.type.PlatformType.Platform; +import org.lflang.target.property.type.SchedulerType.Scheduler; import org.lflang.util.StringUtil; /** @@ -62,6 +64,10 @@ public static String generateIncludeStatements(TargetConfig targetConfig, boolea code.pr("#include \"include/core/port.h\""); code.pr("#include \"include/core/environment.h\""); + if (targetConfig.get(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC) { + code.pr("#include \"include/core/utils/circular_buffer.h\""); + } + code.pr("int lf_reactor_c_main(int argc, const char* argv[]);"); if (targetConfig.isSet(FedSetupProperty.INSTANCE)) { code.pr("#include \"include/core/federated/federate.h\""); diff --git a/core/src/main/java/org/lflang/generator/c/CReactionGenerator.java b/core/src/main/java/org/lflang/generator/c/CReactionGenerator.java index a2f2a93f7d..3e267dcb4d 100644 --- a/core/src/main/java/org/lflang/generator/c/CReactionGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CReactionGenerator.java @@ -34,6 +34,8 @@ import org.lflang.lf.Watchdog; import org.lflang.target.TargetConfig; import org.lflang.target.property.NoSourceMappingProperty; +import org.lflang.target.property.SchedulerProperty; +import org.lflang.target.property.type.SchedulerType.Scheduler; import org.lflang.util.StringUtil; public class CReactionGenerator { @@ -60,6 +62,7 @@ public static String generateInitializationForReaction( int reactionIndex, CTypes types, MessageReporter messageReporter, + TargetConfig targetConfig, Instantiation mainDef, boolean requiresTypes) { // Construct the reactionInitialization code to go into @@ -122,6 +125,7 @@ public static String generateInitializationForReaction( fieldsForStructsForContainedReactors, triggerAsVarRef, tpr, + targetConfig, types); } else if (triggerAsVarRef.getVariable() instanceof Action) { reactionInitialization.pr( @@ -136,14 +140,20 @@ public static String generateInitializationForReaction( // Declare an argument for every input. // NOTE: this does not include contained outputs. for (Input input : tpr.reactor().getInputs()) { - reactionInitialization.pr(generateInputVariablesInReaction(input, tpr, types)); + reactionInitialization.pr( + generateInputVariablesInReaction(input, tpr, types, targetConfig)); } } else { // Define argument for non-triggering inputs. for (VarRef src : ASTUtils.convertToEmptyListIfNull(reaction.getSources())) { if (src.getVariable() instanceof Port) { generatePortVariablesInReaction( - reactionInitialization, fieldsForStructsForContainedReactors, src, tpr, types); + reactionInitialization, + fieldsForStructsForContainedReactors, + src, + tpr, + targetConfig, + types); } else if (src.getVariable() instanceof Action) { // It's a bit odd to read but not be triggered by an action, but // OK, I guess we allow it. @@ -466,9 +476,11 @@ private static void generatePortVariablesInReaction( Map structs, VarRef port, TypeParameterizedReactor tpr, + TargetConfig targetConfig, CTypes types) { if (port.getVariable() instanceof Input) { - builder.pr(generateInputVariablesInReaction((Input) port.getVariable(), tpr, types)); + builder.pr( + generateInputVariablesInReaction((Input) port.getVariable(), tpr, types, targetConfig)); } else { // port is an output of a contained reactor. Output output = (Output) port.getVariable(); @@ -613,7 +625,7 @@ private static String generateActionVariablesInReaction( * @param tpr The reactor. */ private static String generateInputVariablesInReaction( - Input input, TypeParameterizedReactor tpr, CTypes types) { + Input input, TypeParameterizedReactor tpr, CTypes types, TargetConfig targetConfig) { String structType = CGenerator.variableStructType(input, tpr, false); InferredType inputType = ASTUtils.getInferredType(input); CodeBuilder builder = new CodeBuilder(); @@ -631,6 +643,44 @@ private static String generateInputVariablesInReaction( if (!input.isMutable() && !CUtil.isTokenType(inputType) && !ASTUtils.isMultiport(input)) { // Non-mutable, non-multiport, primitive type. builder.pr(structType + "* " + inputName + " = self->_lf_" + inputName + ";"); + // FIXME: Do this for other cases. + if (targetConfig.get(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC) { + builder.pr("if (" + inputName + "->pqueues != NULL) {"); + builder.indent(); + String eventName = "__" + inputName + "_event"; + builder.pr("event_t *" + eventName + " = cb_peek(" + inputName + "->pqueues[0]);"); + builder.pr( + "if (" + + eventName + + " != NULL && " + + eventName + + "->base.tag.time == self->base.tag.time" + + ") {"); + builder.indent(); + builder.pr(inputName + "->token = " + eventName + "->token;"); + // Copy the value of event->token to input->value. + // This works for int, bool, arrays, i.e., anything that fits in void*, + // which depends on the architecture. + // FIXME: In general, this is dangerous. For example, a double would not + // fit in a void* if the underlying architecture is 32-bit. We need a + // more robust solution. + builder.pr( + "memcpy(" + + "&" + + inputName + + "->value" + + ", " + + "&" + + inputName + + "->token" + + ", " + + "sizeof(void*)" + + ");"); + builder.unindent(); + builder.pr("}"); + builder.unindent(); + builder.pr("}"); + } } else if (input.isMutable() && !CUtil.isTokenType(inputType) && !ASTUtils.isMultiport(input)) { // Mutable, non-multiport, primitive type. builder.pr( @@ -642,22 +692,42 @@ private static String generateInputVariablesInReaction( structType + "* " + inputName + " = &_lf_tmp_" + inputName + ";")); } else if (!input.isMutable() && CUtil.isTokenType(inputType) && !ASTUtils.isMultiport(input)) { // Non-mutable, non-multiport, token type. - builder.pr( - String.join( - "\n", - structType + "* " + inputName + " = self->_lf_" + inputName + ";", - "if (" + inputName + "->is_present) {", - " " + inputName + "->length = " + inputName + "->token->length;", - " " - + inputName - + "->value = (" - + types.getTargetType(inputType) - + ")" - + inputName - + "->token->value;", - "} else {", - " " + inputName + "->length = 0;", - "}")); + if (targetConfig.get(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC) { + builder.pr( + String.join( + "\n", + structType + "* " + inputName + " = self->_lf_" + inputName + ";", + "if (" + inputName + "->is_present) {", + " " + inputName + "->length = " + inputName + "->token->length;", + " " + + inputName + + "->value = (" + + types.getTargetType(inputType) + + ")" + + inputName + + "->value;", // Just set the value field for now. FIXME: Check if lf_set_token + // works. + "} else {", + " " + inputName + "->length = 0;", + "}")); + } else { + builder.pr( + String.join( + "\n", + structType + "* " + inputName + " = self->_lf_" + inputName + ";", + "if (" + inputName + "->is_present) {", + " " + inputName + "->length = " + inputName + "->token->length;", + " " + + inputName + + "->value = (" + + types.getTargetType(inputType) + + ")" + + inputName + + "->token->value;", + "} else {", + " " + inputName + "->length = 0;", + "}")); + } } else if (input.isMutable() && CUtil.isTokenType(inputType) && !ASTUtils.isMultiport(input)) { // Mutable, non-multiport, token type. builder.pr( @@ -1121,7 +1191,15 @@ public static String generateReaction( var suppressLineDirectives = targetConfig.get(NoSourceMappingProperty.INSTANCE); String init = generateInitializationForReaction( - body, reaction, tpr, reactionIndex, types, messageReporter, mainDef, requiresType); + body, + reaction, + tpr, + reactionIndex, + types, + messageReporter, + targetConfig, + mainDef, + requiresType); code.pr("#include " + StringUtil.addDoubleQuotes(CCoreFilesUtils.getCTargetSetHeader())); diff --git a/core/src/main/java/org/lflang/generator/c/CStaticScheduleGenerator.java b/core/src/main/java/org/lflang/generator/c/CStaticScheduleGenerator.java new file mode 100644 index 0000000000..3753999a19 --- /dev/null +++ b/core/src/main/java/org/lflang/generator/c/CStaticScheduleGenerator.java @@ -0,0 +1,412 @@ +/************* + * Copyright (c) 2019-2023, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator.c; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.lflang.MessageReporter; +import org.lflang.analyses.dag.Dag; +import org.lflang.analyses.dag.DagGenerator; +import org.lflang.analyses.opt.DagBasedOptimizer; +import org.lflang.analyses.opt.PeepholeOptimizer; +import org.lflang.analyses.pretvm.InstructionGenerator; +import org.lflang.analyses.pretvm.PretVmExecutable; +import org.lflang.analyses.pretvm.PretVmObjectFile; +import org.lflang.analyses.pretvm.Registers; +import org.lflang.analyses.pretvm.instructions.Instruction; +import org.lflang.analyses.pretvm.instructions.InstructionBGE; +import org.lflang.analyses.scheduler.EgsScheduler; +import org.lflang.analyses.scheduler.LoadBalancedScheduler; +import org.lflang.analyses.scheduler.MocasinScheduler; +import org.lflang.analyses.scheduler.StaticScheduler; +import org.lflang.analyses.statespace.StateSpaceDiagram; +import org.lflang.analyses.statespace.StateSpaceExplorer; +import org.lflang.analyses.statespace.StateSpaceExplorer.Phase; +import org.lflang.analyses.statespace.StateSpaceFragment; +import org.lflang.analyses.statespace.StateSpaceUtils; +import org.lflang.analyses.statespace.Tag; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.TriggerInstance; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.CompileDefinitionsProperty; +import org.lflang.target.property.SchedulerProperty; +import org.lflang.target.property.TimeOutProperty; +import org.lflang.target.property.WorkersProperty; +import org.lflang.target.property.type.StaticSchedulerType; + +public class CStaticScheduleGenerator { + + /** File config */ + protected final CFileConfig fileConfig; + + /** Target configuration */ + protected TargetConfig targetConfig; + + /** Message reporter */ + protected MessageReporter messageReporter; + + /** Main reactor instance */ + protected ReactorInstance main; + + /** The number of workers to schedule for */ + protected int workers; + + /** A list of reactor instances */ + protected List reactors; + + /** A list of reaction instances */ + protected List reactions; + + /** A list of reaction triggers */ + protected List triggers; + + /** A path for storing graph */ + protected Path graphDir; + + /** PretVM registers */ + protected Registers registers = new Registers(); + + // Constructor + public CStaticScheduleGenerator( + CFileConfig fileConfig, + TargetConfig targetConfig, + MessageReporter messageReporter, + ReactorInstance main, + List reactorInstances, + List reactionInstances, + List reactionTriggers) { + this.fileConfig = fileConfig; + this.targetConfig = targetConfig; + this.messageReporter = messageReporter; + this.main = main; + this.workers = targetConfig.get(WorkersProperty.INSTANCE); + this.reactors = reactorInstances; + this.reactions = reactionInstances; + this.triggers = reactionTriggers; + + // Create a directory for storing graph. + this.graphDir = fileConfig.getSrcGenPath().resolve("graphs"); + try { + Files.createDirectories(this.graphDir); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // Main function for generating a static schedule file in C. + public void generate() { + + // Generate a list of state space fragments that captures + // all the behavior of the LF program. + List fragments = generateStateSpaceFragments(); + + // Create a DAG generator + DagGenerator dagGenerator = new DagGenerator(this.fileConfig); + + // Create a scheduler. + StaticScheduler scheduler = createStaticScheduler(); + + // Determine the number of workers, if unspecified. + if (this.workers == 0) { + // Update the previous value of 0. + this.workers = scheduler.setNumberOfWorkers(); + WorkersProperty.INSTANCE.update(targetConfig, this.workers); + + // Update CMAKE compile definitions. + final var defs = new HashMap(); + defs.put("NUMBER_OF_WORKERS", String.valueOf(targetConfig.get(WorkersProperty.INSTANCE))); + CompileDefinitionsProperty.INSTANCE.update(targetConfig, defs); + } + + // Create InstructionGenerator, which acts as a compiler and a linker. + InstructionGenerator instGen = + new InstructionGenerator( + this.fileConfig, + this.targetConfig, + this.workers, + this.main, + this.reactors, + this.reactions, + this.triggers, + this.registers); + + // For each fragment, generate a DAG, perform DAG scheduling (mapping tasks + // to workers), and generate instructions for each worker. + List pretvmObjectFiles = new ArrayList<>(); + for (var i = 0; i < fragments.size(); i++) { + // Get the fragment. + StateSpaceFragment fragment = fragments.get(i); + + // Generate a raw DAG from a state space fragment. + Dag dag = dagGenerator.generateDag(fragment.getDiagram()); + + // Generate a dot file. + Path file = graphDir.resolve("dag_raw" + "_frag_" + i + ".dot"); + dag.generateDotFile(file); + + // Generate a partitioned DAG based on the number of workers. + // FIXME: Bring the DOT generation calls to this level instead of hiding + // them inside partitionDag(). + Dag dagPartitioned = scheduler.partitionDag(dag, i, this.workers, "_frag_" + i); + + // Do not execute the following step for the MOCASIN scheduler yet. + // FIXME: A pass-based architecture would be better at managing this. + if (!(targetConfig.get(SchedulerProperty.INSTANCE).staticScheduler() + == StaticSchedulerType.StaticScheduler.MOCASIN + && (targetConfig.get(SchedulerProperty.INSTANCE).mocasinMapping() == null + || targetConfig.get(SchedulerProperty.INSTANCE).mocasinMapping().size() == 0))) { + // Ensure the DAG is valid before proceeding to generating instructions. + if (!dagPartitioned.isValidDAG()) + throw new RuntimeException("The generated DAG is invalid:" + " fragment " + i); + // Generate instructions (wrapped in an object file) from DAG partitions. + PretVmObjectFile objectFile = instGen.generateInstructions(dagPartitioned, fragment); + // Point the fragment to the new object file. + fragment.setObjectFile(objectFile); + // Add the object file to list. + pretvmObjectFiles.add(objectFile); + } + } + + // Do not execute the following step if the MOCASIN scheduler in used and + // mappings are not provided. + // FIXME: A pass-based architecture would be better at managing this. + if (targetConfig.get(SchedulerProperty.INSTANCE).staticScheduler() + == StaticSchedulerType.StaticScheduler.MOCASIN + && (targetConfig.get(SchedulerProperty.INSTANCE).mocasinMapping() == null + || targetConfig.get(SchedulerProperty.INSTANCE).mocasinMapping().size() == 0)) { + messageReporter + .nowhere() + .info( + "SDF3 files generated. Please invoke `mocasin` to generate mappings and provide paths" + + " to them using the `mocasin-mapping` target property under `scheduler`. A" + + " sample mocasin command is `mocasin pareto_front graph=sdf3_reader" + + " trace=sdf3_reader platform=odroid sdf3.file=`"); + System.exit(0); + } + + // Invoke the dag-based optimizer on each object file. + // It is invoked before linking because after linking, + // the DAG information is gone. + for (var objectFile : pretvmObjectFiles) { + DagBasedOptimizer.optimize(objectFile, workers, registers); + } + + // Link multiple object files into a single executable (represented also in an object file + // class). + // Instructions are also inserted based on transition guards between fragments. + // In addition, PREAMBLE and EPILOGUE instructions are inserted here. + PretVmExecutable executable = instGen.link(pretvmObjectFiles, graphDir); + + // Invoke the peephole optimizer. + // FIXME: Should only apply to basic blocks! + var schedules = executable.getContent(); + for (int i = 0; i < schedules.size(); i++) { + PeepholeOptimizer.optimize(schedules.get(i)); + } + + // Generate C code. + instGen.generateCode(executable); + } + + /** + * Generate a list of state space fragments for an LF program. This function calls + * generateStateSpaceDiagram() multiple times to capture the full behavior of the LF + * program. + */ + private List generateStateSpaceFragments() { + + // Initialize variables + StateSpaceExplorer explorer = new StateSpaceExplorer(targetConfig); + List fragments = new ArrayList<>(); + + /***************/ + /* Async phase */ + /***************/ + // Generate a state space diagram for the asynchronous phase of an LF + // program. + // FIXME: This is untested! + List asyncDiagrams = + StateSpaceUtils.generateAsyncStateSpaceDiagrams( + explorer, + Phase.ASYNC, + main, + new Tag(0, 0, true), + targetConfig, + graphDir, + "state_space_" + Phase.ASYNC); + for (var diagram : asyncDiagrams) diagram.display(); + + /**************************************/ + /* Initialization and Periodic phases */ + /**************************************/ + + // Generate a state space diagram for the initialization and periodic phase + // of an LF program. + StateSpaceDiagram stateSpaceInitAndPeriodic = + StateSpaceUtils.generateStateSpaceDiagram( + explorer, + Phase.INIT_AND_PERIODIC, + main, + new Tag(0, 0, true), + targetConfig, + graphDir, + "state_space_" + Phase.INIT_AND_PERIODIC); + + // Split the graph into a list of diagrams. + List splittedDiagrams = + StateSpaceUtils.splitInitAndPeriodicDiagrams(stateSpaceInitAndPeriodic); + + // Merge async diagrams into the init and periodic diagrams. + for (int i = 0; i < splittedDiagrams.size(); i++) { + var diagram = splittedDiagrams.get(i); + splittedDiagrams.set( + i, StateSpaceUtils.mergeAsyncDiagramsIntoDiagram(asyncDiagrams, diagram)); + // Generate a dot file. + if (!diagram.isEmpty()) { + Path file = graphDir.resolve("merged_" + i + ".dot"); + diagram.generateDotFile(file); + } else { + System.out.println("*** Merged diagram is empty!"); + } + } + + // Convert the diagrams into fragments (i.e., having a notion of upstream & + // downstream and carrying object file) and add them to the fragments list. + for (var diagram : splittedDiagrams) { + fragments.add(new StateSpaceFragment(diagram)); + } + + // Checking abnomalies. + // FIXME: For some reason, the message reporter does not work here. + if (fragments.size() == 0) { + throw new RuntimeException( + "No behavior found. The program is not schedulable. Please provide an initial trigger."); + } + if (fragments.size() > 2) { + throw new RuntimeException( + "More than two fragments detected when splitting the initialization and periodic phase!"); + } + + // If there are exactly two fragments (init and periodic), + // connect the first fragment to the async fragment and connect + // the async fragment to the second fragment. + if (splittedDiagrams.size() == 2) { + StateSpaceUtils.connectFragmentsDefault(fragments.get(0), fragments.get(1)); + } + + // If the last fragment is periodic, make it transition back to itself. + StateSpaceFragment lastFragment = fragments.get(fragments.size() - 1); + if (lastFragment.getPhase() == Phase.PERIODIC) + StateSpaceUtils.connectFragmentsDefault(lastFragment, lastFragment); + + // Get the init or periodic fragment, whichever is currently the last in the list. + StateSpaceFragment initOrPeriodicFragment = fragments.get(fragments.size() - 1); + + /******************/ + /* Shutdown phase */ + /******************/ + + // Scenario 1: TIMEOUT + // Generate a state space diagram for the timeout scenario of the + // shutdown phase. + if (targetConfig.get(TimeOutProperty.INSTANCE) != null) { + StateSpaceFragment shutdownTimeoutFrag = + new StateSpaceFragment( + StateSpaceUtils.generateStateSpaceDiagram( + explorer, + Phase.SHUTDOWN_TIMEOUT, + main, + new Tag(0, 0, true), + targetConfig, + graphDir, + "state_space_" + Phase.SHUTDOWN_TIMEOUT)); + + if (!shutdownTimeoutFrag.getDiagram().isEmpty()) { + + // Generate a guarded transition. + // Only transition to this fragment when offset >= timeout. + List guardedTransition = new ArrayList<>(); + guardedTransition.add( + new InstructionBGE(registers.offset, registers.timeout, Phase.SHUTDOWN_TIMEOUT)); + + // Connect init or periodic fragment to the shutdown-timeout fragment. + StateSpaceUtils.connectFragmentsGuarded( + initOrPeriodicFragment, shutdownTimeoutFrag, guardedTransition); + + // Connect the shutdown-timeout fragment to epilogue (which is not a + // real fragment, so we use a new StateSpaceFragment() instead. + // The guarded transition is the important component here.) + // FIXME: It could make more sense to store the STP in the EPILOGUE's object + // file, instead of directly injecting code during link time. This might not have any + // performance benefit, but it might make the pipeline more intuitive. + StateSpaceUtils.connectFragmentsDefault(shutdownTimeoutFrag, StateSpaceFragment.EPILOGUE); + + // Add the shutdown-timeout fragment to the list of fragments. + fragments.add(shutdownTimeoutFrag); // Add new fragments to the list. + } + } + + // Scenario 2: STARVATION + // Generate a state space diagram for the starvation scenario of the + // shutdown phase. + // FIXME: We do not need this fragment if the system has timers. + // FIXME: We need a way to check for starvation. One approach is to encode + // triggers explicitly as global variables, and use conditional branch to + // jump to this fragment if all trigger variables are indicating absent. + /* + StateSpaceFragment shutdownStarvationFrag = + new StateSpaceFragment(generateStateSpaceDiagram(explorer, Phase.SHUTDOWN_STARVATION)); + if (!shutdownStarvationFrag.getDiagram().isEmpty()) { + StateSpaceUtils.connectFragmentsDefault(initOrPeriodicFragment, shutdownStarvationFrag); + fragments.add(shutdownStarvationFrag); // Add new fragments to the list. + } + */ + + // Generate fragment dot files for debugging + for (int i = 0; i < fragments.size(); i++) { + Path file = graphDir.resolve("state_space_fragment_" + i + ".dot"); + fragments.get(i).getDiagram().generateDotFile(file); + } + + // TODO: Compose all fragments into a single dot file. + + return fragments; + } + + /** Create a static scheduler based on target property. */ + private StaticScheduler createStaticScheduler() { + return switch (this.targetConfig.get(SchedulerProperty.INSTANCE).staticScheduler()) { + case LB -> new LoadBalancedScheduler(this.graphDir); + case EGS -> new EgsScheduler(this.fileConfig); + case MOCASIN -> new MocasinScheduler(this.fileConfig, this.targetConfig); + default -> new LoadBalancedScheduler(this.graphDir); + }; + } +} diff --git a/core/src/main/java/org/lflang/generator/c/CTriggerObjectsGenerator.java b/core/src/main/java/org/lflang/generator/c/CTriggerObjectsGenerator.java index ebef8e5933..ea33137b49 100644 --- a/core/src/main/java/org/lflang/generator/c/CTriggerObjectsGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CTriggerObjectsGenerator.java @@ -12,20 +12,26 @@ import com.google.common.collect.Iterables; import java.util.Arrays; import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import org.lflang.AttributeUtils; import org.lflang.ast.ASTUtils; import org.lflang.federated.extensions.CExtensionUtils; +import org.lflang.generator.ActionInstance; import org.lflang.generator.CodeBuilder; import org.lflang.generator.PortInstance; import org.lflang.generator.ReactionInstance; import org.lflang.generator.ReactorInstance; import org.lflang.generator.RuntimeRange; import org.lflang.generator.SendRange; +import org.lflang.generator.TriggerInstance; import org.lflang.target.TargetConfig; import org.lflang.target.property.LoggingProperty; +import org.lflang.target.property.SchedulerProperty; import org.lflang.target.property.SingleThreadedProperty; import org.lflang.target.property.type.LoggingType.LogLevel; +import org.lflang.target.property.type.SchedulerType.Scheduler; /** * Generate code for the "_lf_initialize_trigger_objects" function @@ -42,7 +48,10 @@ public static String generateInitializeTriggerObjects( CodeBuilder initializeTriggerObjects, CodeBuilder startTimeStep, CTypes types, - String lfModuleName) { + String lfModuleName, + List reactors, + List reactions, + List triggers) { var code = new CodeBuilder(); code.pr("void _lf_initialize_trigger_objects() {"); code.indent(); @@ -73,7 +82,7 @@ public static String generateInitializeTriggerObjects( code.pr(initializeTriggerObjects.toString()); code.pr(deferredInitialize(main, main.reactions, targetConfig, types)); - code.pr(deferredInitializeNonNested(main, main, main.reactions, types)); + code.pr(deferredInitializeNonNested(main, main, main.reactions, targetConfig, types)); // Next, for every input port, populate its "self" struct // fields with pointers to the output port that sends it data. code.pr(deferredConnectInputsToOutputs(main)); @@ -83,7 +92,28 @@ public static String generateInitializeTriggerObjects( // between inputs and outputs. code.pr(startTimeStep.toString()); code.pr(setReactionPriorities(main)); - code.pr(generateSchedulerInitializerMain(main, targetConfig)); + // Collect reactor and reaction instances in two arrays, + // if the STATIC scheduler is used. + if (targetConfig.get(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC) { + code.pr(collectReactorInstances(main, reactors)); + code.pr(collectReactionInstances(main, reactions)); + collectTriggerInstances(main, reactions, triggers); + + // FIXME: Factor into a separate function. + // FIXME: How to know which pqueue head is which? + int numPqueuesTotal = countPqueuesTotal(main); + code.pr( + CUtil.getEnvironmentStruct(main) + ".num_pqueue_heads" + " = " + numPqueuesTotal + ";"); + code.pr( + CUtil.getEnvironmentStruct(main) + + ".pqueue_heads" + + " = " + + "calloc(" + + numPqueuesTotal + + ", sizeof(event_t))" + + ";"); + } + code.pr(generateSchedulerInitializerMain(main, targetConfig, reactors)); // FIXME: This is a little hack since we know top-level/main is always first (has index 0) code.pr( @@ -101,9 +131,34 @@ public static String generateInitializeTriggerObjects( return code.toString(); } - /** Generate code to initialize the scheduler for the threaded C runtime. */ + /** + * Count the total number of pqueues required for the reactor by counting all the eventual + * destination ports. Banks are not be supported yet. + */ + private static int countPqueuesTotal(ReactorInstance main) { + int count = 0; + for (var child : main.children) { + // Count the eventual destination ports. + count += + child.outputs.stream() + .flatMap(output -> output.eventualDestinations().stream()) + .mapToInt(e -> 1) + .sum(); + // Recursion + count += countPqueuesTotal(child); + } + return count; + } + + /** + * Generate code to initialize the scheduler for the threaded C runtime. + * + * @param main The main reactor instance + * @param targetConfig An object storing all the target configurations + * @param reactors A list of all the reactor instances in the program + */ public static String generateSchedulerInitializerMain( - ReactorInstance main, TargetConfig targetConfig) { + ReactorInstance main, TargetConfig targetConfig, List reactors) { if (targetConfig.get(SingleThreadedProperty.INSTANCE)) { return ""; } @@ -122,7 +177,8 @@ public static String generateSchedulerInitializerMain( " .num_reactions_per_level = &num_reactions_per_level[0],", " .num_reactions_per_level_size = (size_t) " + numReactionsPerLevel.length - + "};")); + + ",", + "};")); for (ReactorInstance enclave : CUtil.getEnclaves(main)) { code.pr(generateSchedulerInitializerEnclave(enclave, targetConfig)); @@ -307,6 +363,146 @@ private static boolean setReactionPriorities(ReactorInstance reactor, CodeBuilde return foundOne; } + /** + * Collect reactor and reaction instances using C arrays. + * + * @param reactor The reactor on which to do this. + */ + private static String collectReactorInstances( + ReactorInstance reactor, List list) { + var code = new CodeBuilder(); + + // Collect reactor instances in a list. + collectReactorInstancesRec(reactor, list); + + // Put tag pointers inside the environment struct. + code.pr("// Put tag pointers inside the environment struct."); + code.pr( + CUtil.getEnvironmentStruct(reactor) + + ".reactor_self_array_size" + + " = " + + list.size() + + ";"); + code.pr( + CUtil.getEnvironmentStruct(reactor) + + ".reactor_self_array" + + " = " + + "(self_base_t**) calloc(" + + list.size() + + "," + + " sizeof(self_base_t*)" + + ")" + + ";"); + for (int i = 0; i < list.size(); i++) { + code.pr( + CUtil.getEnvironmentStruct(reactor) + + ".reactor_self_array" + + "[" + + i + + "]" + + " = " + + "&" + + "(" + + CUtil.reactorRef(list.get(i)) + + "->base" + + ")" + + ";"); + } + + return code.toString(); + } + + /** + * Recursively collect reactor and reaction instances using C arrays. + * + * @param reactor The reactor on which to do this. + * @param list A list that holds the reactor instances. + */ + private static void collectReactorInstancesRec( + ReactorInstance reactor, List list) { + list.add(reactor); + for (ReactorInstance r : reactor.children) { + collectReactorInstancesRec(r, list); + } + } + + /** + * Collect reactor and reaction instances using C arrays. + * + * @param reactor The reactor on which to do this. + */ + private static String collectReactionInstances( + ReactorInstance reactor, List list) { + var code = new CodeBuilder(); + collectReactionInstancesRec(reactor, list); + code.pr("// Collect reaction instances."); + code.pr( + CUtil.getEnvironmentStruct(reactor) + ".reaction_array_size" + " = " + list.size() + ";"); + code.pr( + CUtil.getEnvironmentStruct(reactor) + + ".reaction_array" + + "= (reaction_t**) calloc(" + + list.size() + + ", sizeof(reaction_t*));"); + for (int i = 0; i < list.size(); i++) { + code.pr( + CUtil.getEnvironmentStruct(reactor) + + ".reaction_array" + + "[" + + i + + "]" + + " = " + + "&" + + "(" + + CUtil.reactionRef(list.get(i)) + + ")" + + ";"); + } + return code.toString(); + } + + /** + * Collect reactor and reaction instances using C arrays. + * + * @param reactor The reactor on which to do this. + * @param list A list that holds the reactor instances. + */ + private static void collectReactionInstancesRec( + ReactorInstance reactor, List list) { + list.addAll(reactor.reactions); + for (ReactorInstance r : reactor.children) { + collectReactionInstancesRec(r, list); + } + } + + /** + * (DEPRECATED) Collect trigger instances that can reactions are sensitive to. + * + * @param reactor The top-level reactor within which this is done + * @param reactions A list of reactions from which triggers are collected from + * @param triggers A list of triggers to be populated + */ + private static void collectTriggerInstances( + ReactorInstance reactor, List reactions, List triggers) { + var code = new CodeBuilder(); + // Collect all triggers that can trigger the reactions in the current + // module. Use a set to avoid redundancy. + Set triggerSet = new HashSet<>(); + for (var reaction : reactions) { + triggerSet.addAll(reaction.triggers); + } + // Filter out triggers that are not actions nor input ports, + // and convert the set to a list. + // Only actions and input ports have is_present fields. + triggers.addAll( + triggerSet.stream() + .filter( + it -> + (it instanceof ActionInstance) + || (it instanceof PortInstance port && port.isInput())) + .toList()); + } + /** * Generate assignments of pointers in the "self" struct of a destination port's reactor to the * appropriate entries in the "self" struct of the source reactor. This has to be done after all @@ -346,6 +542,13 @@ private static String deferredConnectInputsToOutputs(ReactorInstance instance) { */ private static String connectPortToEventualDestinations(PortInstance src) { var code = new CodeBuilder(); + + // Note: With the AST transformation of after delays, eventualDestinations + // do not include final destination ports. + // Update: After deleting the skipping logic, eventualDestinations should + // contain all final destination ports. + // FIXME: Does this affect dynamic schedulers? + for (SendRange srcRange : src.eventualDestinations()) { for (RuntimeRange dstRange : srcRange.destinations) { var dst = dstRange.instance; @@ -618,6 +821,7 @@ private static String deferredFillTriggerTable(Iterable reacti // Include this destination port only if it has at least one // reaction in the federation. var belongs = false; + // FIXME: destinationReaction appears to be unused. for (ReactionInstance destinationReaction : dst.getDependentReactions()) { belongs = true; } @@ -813,27 +1017,88 @@ private static String deferredInitializeNonNested( ReactorInstance reactor, ReactorInstance main, Iterable reactions, + TargetConfig targetConfig, CTypes types) { var code = new CodeBuilder(); code.pr("// **** Start non-nested deferred initialize for " + reactor.getFullName()); // Initialization within a for loop iterating // over bank members of reactor code.startScopedBlock(reactor); - // Initialize the num_destinations fields of port structs on the self struct. - // This needs to be outside the above scoped block because it performs - // its own iteration over ranges. - code.pr(deferredInputNumDestinations(reactions, types)); - - // Second batch of initializes cannot be within a for loop - // iterating over bank members because they iterate over send - // ranges which may span bank members. - if (reactor != main) { - code.pr(deferredOutputNumDestinations(reactor)); + + // FIXME: Factor the following into a separate function at the same + // level as deferredOutputNumDestinations. + // (STATIC SCHEDULER ONLY) Instantiate a pqueue for each destination port. + // FIXME: It is unclear if output ports are the best place to attach the + // queues. The number of the queues depends on the number of input ports. + // It's also unclear whether a central env struct is the best place to + // instantiate the pqueues. For federated execution, it's important to + // consider the minimum amount of info needed to have a federate work in the + // federation, and a central env struct implies having perfect knowledge. + if (targetConfig.get(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC) { + for (PortInstance output : reactor.outputs) { + for (SendRange sendingRange : output.getDependentPorts()) { + // Only instantiate a circular buffer if the connection has a non-zero delay. + var connection = sendingRange.connection; + if (connection.getDelay() != null && ASTUtils.getDelay(connection.getDelay()) > 0) { + code.startScopedRangeBlock(sendingRange, sr, sb, sc, sendingRange.instance.isInput()); + long numPqueuesPerOutput = output.getDependentPorts().stream().count(); + code.pr("int num_pqueues_per_output = " + numPqueuesPerOutput + ";"); + code.pr( + CUtil.portRef(output, sr, sb, sc) + + ".num_pqueues" + + " = " + + "num_pqueues_per_output" + + ";"); + code.pr( + CUtil.portRef(output, sr, sb, sc) + + ".pqueues" + + " = " + + "calloc(num_pqueues_per_output, sizeof(circular_buffer*))" + + ";"); + for (int i = 0; i < numPqueuesPerOutput; i++) { + code.pr( + CUtil.portRef(output, sr, sb, sc) + + ".pqueues" + + "[" + + i + + "]" + + " = " + + "malloc(sizeof(circular_buffer));"); + int bufferSize = 100; // URGENT FIXME: Determine size from the state space diagram? + code.pr( + "cb_init(" + + CUtil.portRef(output, sr, sb, sc) + + ".pqueues" + + "[" + + i + + "]" + + ", " + + bufferSize + + ", " + + "sizeof(event_t)" + + ");"); + } + code.endScopedRangeBlock(sendingRange); + } + } + } + } else { + // Initialize the num_destinations fields of port structs on the self struct. + // This needs to be outside the above scoped block because it performs + // its own iteration over ranges. + code.pr(deferredInputNumDestinations(reactions, types)); + + // Second batch of initializes cannot be within a for loop + // iterating over bank members because they iterate over send + // ranges which may span bank members. + if (reactor != main) { + code.pr(deferredOutputNumDestinations(reactor)); + } + code.pr(deferredFillTriggerTable(reactions)); + code.pr(deferredOptimizeForSingleDominatingReaction(reactor)); } - code.pr(deferredFillTriggerTable(reactions)); - code.pr(deferredOptimizeForSingleDominatingReaction(reactor)); for (ReactorInstance child : reactor.children) { - code.pr(deferredInitializeNonNested(child, main, child.reactions, types)); + code.pr(deferredInitializeNonNested(child, main, child.reactions, targetConfig, types)); } code.endScopedBlock(); code.pr("// **** End of non-nested deferred initialize for " + reactor.getFullName()); diff --git a/core/src/main/java/org/lflang/generator/c/CUtil.java b/core/src/main/java/org/lflang/generator/c/CUtil.java index dae1425342..efa86034ca 100644 --- a/core/src/main/java/org/lflang/generator/c/CUtil.java +++ b/core/src/main/java/org/lflang/generator/c/CUtil.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.Objects; import java.util.Queue; +import java.util.Set; import java.util.stream.Collectors; import org.lflang.FileConfig; import org.lflang.InferredType; @@ -154,6 +155,15 @@ public static String internalIncludeGuard(TypeParameterizedReactor tpr) { return headerName.toUpperCase().replace(".", "_"); } + /** Return a set of names given a list of reactors. */ + public static Set getNames(List reactors) { + Set names = new HashSet<>(); + for (var reactor : reactors) { + names.add(getName(reactor)); + } + return names; + } + /** * Return a reference to the specified port. * diff --git a/core/src/main/java/org/lflang/generator/python/PythonGenerator.java b/core/src/main/java/org/lflang/generator/python/PythonGenerator.java index f486c64a47..ac7386bfa2 100644 --- a/core/src/main/java/org/lflang/generator/python/PythonGenerator.java +++ b/core/src/main/java/org/lflang/generator/python/PythonGenerator.java @@ -438,7 +438,7 @@ protected void generateReaction( } src.pr( PythonReactionGenerator.generateCReaction( - reaction, tpr, reactor, reactionIndex, mainDef, messageReporter, types)); + reaction, tpr, reactor, reactionIndex, mainDef, messageReporter, targetConfig, types)); } /** diff --git a/core/src/main/java/org/lflang/generator/python/PythonReactionGenerator.java b/core/src/main/java/org/lflang/generator/python/PythonReactionGenerator.java index d0a1bffac7..527c1d5728 100644 --- a/core/src/main/java/org/lflang/generator/python/PythonReactionGenerator.java +++ b/core/src/main/java/org/lflang/generator/python/PythonReactionGenerator.java @@ -28,6 +28,7 @@ import org.lflang.lf.TriggerRef; import org.lflang.lf.VarRef; import org.lflang.target.Target; +import org.lflang.target.TargetConfig; import org.lflang.util.StringUtil; public class PythonReactionGenerator { @@ -143,6 +144,7 @@ public static String generateCReaction( int reactionIndex, Instantiation mainDef, MessageReporter messageReporter, + TargetConfig targetConfig, CTypes types) { // Contains the actual comma separated list of inputs to the reaction of type // generic_port_instance_struct. @@ -158,6 +160,7 @@ public static String generateCReaction( reactionIndex, types, messageReporter, + targetConfig, mainDef, Target.Python.requiresTypes); code.pr("#include " + StringUtil.addDoubleQuotes(CCoreFilesUtils.getCTargetSetHeader())); diff --git a/core/src/main/java/org/lflang/target/Target.java b/core/src/main/java/org/lflang/target/Target.java index 30048f5a58..08c6137b95 100644 --- a/core/src/main/java/org/lflang/target/Target.java +++ b/core/src/main/java/org/lflang/target/Target.java @@ -38,6 +38,7 @@ import org.lflang.target.property.CompilerProperty; import org.lflang.target.property.CoordinationOptionsProperty; import org.lflang.target.property.CoordinationProperty; +import org.lflang.target.property.DashProperty; import org.lflang.target.property.DockerProperty; import org.lflang.target.property.ExportDependencyGraphProperty; import org.lflang.target.property.ExportToYamlProperty; @@ -56,6 +57,7 @@ import org.lflang.target.property.SchedulerProperty; import org.lflang.target.property.SingleFileProjectProperty; import org.lflang.target.property.SingleThreadedProperty; +import org.lflang.target.property.StaticSchedulerProperty; import org.lflang.target.property.TracePluginProperty; import org.lflang.target.property.TracingProperty; import org.lflang.target.property.VerifyProperty; @@ -593,6 +595,7 @@ public void initialize(TargetConfig config) { CompilerProperty.INSTANCE, CoordinationOptionsProperty.INSTANCE, CoordinationProperty.INSTANCE, + DashProperty.INSTANCE, DockerProperty.INSTANCE, FilesProperty.INSTANCE, KeepaliveProperty.INSTANCE, @@ -600,6 +603,7 @@ public void initialize(TargetConfig config) { PlatformProperty.INSTANCE, ProtobufsProperty.INSTANCE, SchedulerProperty.INSTANCE, + StaticSchedulerProperty.INSTANCE, SingleThreadedProperty.INSTANCE, TracingProperty.INSTANCE, TracePluginProperty.INSTANCE, diff --git a/core/src/main/java/org/lflang/target/TargetConfig.java b/core/src/main/java/org/lflang/target/TargetConfig.java index c0f6821bbb..09b0578438 100644 --- a/core/src/main/java/org/lflang/target/TargetConfig.java +++ b/core/src/main/java/org/lflang/target/TargetConfig.java @@ -56,8 +56,10 @@ import org.lflang.target.property.FileListProperty; import org.lflang.target.property.LoggingProperty; import org.lflang.target.property.NoCompileProperty; +import org.lflang.target.property.SchedulerProperty; import org.lflang.target.property.TargetProperty; import org.lflang.target.property.TimeOutProperty; +import org.lflang.target.property.type.SchedulerType.Scheduler; import org.lflang.target.property.type.TargetPropertyType; import org.lflang.util.FileUtil; @@ -480,4 +482,14 @@ public void validate(MessageReporter reporter) { p.validate(this, reporter); }); } + + /** + * Determine if the delayed connection AST transformation should be used. + * + * @return true if the transformation should be applied, false otherwise. + */ + public boolean useDelayedConnectionTransformation() { + if (this.getOrDefault(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC) return false; + return true; + } } diff --git a/core/src/main/java/org/lflang/target/property/DashProperty.java b/core/src/main/java/org/lflang/target/property/DashProperty.java new file mode 100644 index 0000000000..bd44d769c0 --- /dev/null +++ b/core/src/main/java/org/lflang/target/property/DashProperty.java @@ -0,0 +1,47 @@ +package org.lflang.target.property; + +import org.lflang.MessageReporter; +import org.lflang.lf.LfPackage.Literals; +import org.lflang.target.Target; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.type.SchedulerType.Scheduler; + +/** + * If true, configure the execution environment such that it does not wait for physical time to + * match logical time for non-real-time reactions. A reaction is real-time if it is within a + * real-time reactor (marked by the `realtime` keyword). The default is false. + */ +public final class DashProperty extends BooleanProperty { + + /** Singleton target property instance. */ + public static final DashProperty INSTANCE = new DashProperty(); + + private DashProperty() { + super(); + } + + @Override + public String name() { + return "dash"; + } + + @Override + public void validate(TargetConfig config, MessageReporter reporter) { + var pair = config.lookup(this); + if (config.isSet(this) && config.isFederated()) { + reporter + .at(pair, Literals.KEY_VALUE_PAIR__NAME) + .error("The dash target property is incompatible with federated programs."); + } + + if (!(config.target == Target.C + && config.get(SchedulerProperty.INSTANCE).type() == Scheduler.STATIC)) { + reporter + .at(pair, Literals.KEY_VALUE_PAIR__NAME) + .error( + String.format( + "The dash mode currently only works in the C target with the STATIC scheduler.", + config.target.toString())); + } + } +} diff --git a/core/src/main/java/org/lflang/target/property/SchedulerProperty.java b/core/src/main/java/org/lflang/target/property/SchedulerProperty.java index 2abadd7f75..b53cb0d394 100644 --- a/core/src/main/java/org/lflang/target/property/SchedulerProperty.java +++ b/core/src/main/java/org/lflang/target/property/SchedulerProperty.java @@ -1,45 +1,84 @@ package org.lflang.target.property; +import java.util.List; import org.lflang.MessageReporter; import org.lflang.ast.ASTUtils; import org.lflang.lf.Element; -import org.lflang.lf.LfPackage.Literals; +import org.lflang.lf.KeyValuePair; import org.lflang.target.TargetConfig; +import org.lflang.target.property.SchedulerProperty.SchedulerOptions; +import org.lflang.target.property.type.DictionaryType; +import org.lflang.target.property.type.DictionaryType.DictionaryElement; import org.lflang.target.property.type.SchedulerType; import org.lflang.target.property.type.SchedulerType.Scheduler; +import org.lflang.target.property.type.StaticSchedulerType; +import org.lflang.target.property.type.StaticSchedulerType.StaticScheduler; +import org.lflang.target.property.type.TargetPropertyType; +import org.lflang.target.property.type.UnionType; /** Directive for specifying the use of a specific runtime scheduler. */ -public final class SchedulerProperty extends TargetProperty { +public final class SchedulerProperty extends TargetProperty { /** Singleton target property instance. */ public static final SchedulerProperty INSTANCE = new SchedulerProperty(); private SchedulerProperty() { - super(new SchedulerType()); + super(UnionType.SCHEDULER_UNION_OR_DICTIONARY); } @Override - public Scheduler initialValue() { - return Scheduler.getDefault(); + public SchedulerOptions initialValue() { + return new SchedulerOptions(Scheduler.getDefault(), null, null); } @Override - public Scheduler fromAst(Element node, MessageReporter reporter) { - var scheduler = fromString(ASTUtils.elementToSingleString(node), reporter); - if (scheduler != null) { - return scheduler; + public SchedulerOptions fromAst(Element node, MessageReporter reporter) { + // Check if the user passes in a SchedulerType or + // DictionaryType.SCHEDULER_DICT. + // If dict, parse from a map. + Scheduler schedulerType = null; + StaticScheduler staticSchedulerType = null; + List mocasinMapping = null; + String schedulerStr = ASTUtils.elementToSingleString(node); + if (!schedulerStr.equals("")) { + schedulerType = Scheduler.fromString(schedulerStr); + if (schedulerType == Scheduler.STATIC) staticSchedulerType = StaticScheduler.getDefault(); } else { - return Scheduler.getDefault(); + for (KeyValuePair entry : node.getKeyvalue().getPairs()) { + SchedulerDictOption option = + (SchedulerDictOption) DictionaryType.SCHEDULER_DICT.forName(entry.getName()); + if (option != null) { + switch (option) { + case TYPE -> { + // Parse type + schedulerType = + new SchedulerType().forName(ASTUtils.elementToSingleString(entry.getValue())); + } + case MAPPER -> { + // Parse static scheduler + staticSchedulerType = + new StaticSchedulerType() + .forName(ASTUtils.elementToSingleString(entry.getValue())); + if (staticSchedulerType == null) staticSchedulerType = StaticScheduler.getDefault(); + } + case MOCASIN_MAPPING -> { + // Parse mocasin mapping + mocasinMapping = ASTUtils.elementToListOfStrings(entry.getValue()); + } + } + } + } } + return new SchedulerOptions(schedulerType, staticSchedulerType, mocasinMapping); } @Override - protected Scheduler fromString(String string, MessageReporter reporter) { - return this.type.forName(string); + protected SchedulerOptions fromString(String string, MessageReporter reporter) { + throw new UnsupportedOperationException("Not supported yet."); } @Override - public Element toAstElement(Scheduler value) { + public Element toAstElement(SchedulerOptions value) { return ASTUtils.toElement(value.toString()); } @@ -51,7 +90,7 @@ public String name() { @Override public void validate(TargetConfig config, MessageReporter reporter) { var scheduler = config.get(this); - if (!scheduler.prioritizesDeadline()) { + if (scheduler.type != null && !scheduler.type.prioritizesDeadline()) { // Check if a deadline is assigned to any reaction // Filter reactors that contain at least one reaction that // has a deadline handler. @@ -63,7 +102,7 @@ public void validate(TargetConfig config, MessageReporter reporter) { ASTUtils.allReactions(reactor).stream() .anyMatch(reaction -> reaction.getDeadline() != null))) { reporter - .at(config.lookup(this), Literals.KEY_VALUE_PAIR__VALUE) + .nowhere() .warning( "This program contains deadlines, but the chosen " + scheduler @@ -73,4 +112,55 @@ public void validate(TargetConfig config, MessageReporter reporter) { } } } + + /** Settings related to Scheduler Options. */ + public record SchedulerOptions( + Scheduler type, StaticScheduler staticScheduler, List mocasinMapping) { + public SchedulerOptions(Scheduler type) { + this(type, null, null); + } + + public SchedulerOptions update(Scheduler newType) { + return new SchedulerOptions(newType, this.staticScheduler, this.mocasinMapping); + } + + public SchedulerOptions update(StaticScheduler newStaticScheduler) { + return new SchedulerOptions(this.type, newStaticScheduler, this.mocasinMapping); + } + + public SchedulerOptions update(List newMocasinMapping) { + return new SchedulerOptions(this.type, this.staticScheduler, newMocasinMapping); + } + } + + /** + * Scheduler dictionary options. + * + * @author Shaokai Lin + */ + public enum SchedulerDictOption implements DictionaryElement { + TYPE("type", new SchedulerType()), + MAPPER("mapper", new StaticSchedulerType()), + MOCASIN_MAPPING("mocasin-mapping", UnionType.FILE_OR_FILE_ARRAY); + + public final TargetPropertyType type; + + private final String description; + + private SchedulerDictOption(String alias, TargetPropertyType type) { + this.description = alias; + this.type = type; + } + + /** Return the description of this dictionary element. */ + @Override + public String toString() { + return this.description; + } + + /** Return the type associated with this dictionary element. */ + public TargetPropertyType getType() { + return this.type; + } + } } diff --git a/core/src/main/java/org/lflang/target/property/StaticSchedulerProperty.java b/core/src/main/java/org/lflang/target/property/StaticSchedulerProperty.java new file mode 100644 index 0000000000..85bff0952d --- /dev/null +++ b/core/src/main/java/org/lflang/target/property/StaticSchedulerProperty.java @@ -0,0 +1,55 @@ +package org.lflang.target.property; + +import org.lflang.MessageReporter; +import org.lflang.ast.ASTUtils; +import org.lflang.lf.Element; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.type.StaticSchedulerType; +import org.lflang.target.property.type.StaticSchedulerType.StaticScheduler; + +/** Directive for specifying the use of a specific runtime scheduler. */ +public final class StaticSchedulerProperty + extends TargetProperty { + + /** Singleton target property instance. */ + public static final StaticSchedulerProperty INSTANCE = new StaticSchedulerProperty(); + + private StaticSchedulerProperty() { + super(new StaticSchedulerType()); + } + + @Override + public StaticScheduler initialValue() { + return StaticScheduler.getDefault(); + } + + @Override + public StaticScheduler fromAst(Element node, MessageReporter reporter) { + var scheduler = fromString(ASTUtils.elementToSingleString(node), reporter); + if (scheduler != null) { + return scheduler; + } else { + return StaticScheduler.getDefault(); + } + } + + @Override + protected StaticScheduler fromString(String string, MessageReporter reporter) { + return this.type.forName(string); + } + + @Override + public Element toAstElement(StaticScheduler value) { + return ASTUtils.toElement(value.toString()); + } + + @Override + public String name() { + return "mapper"; + } + + @Override + public void validate(TargetConfig config, MessageReporter reporter) { + // Do nothing for now. + } +} diff --git a/core/src/main/java/org/lflang/target/property/type/DictionaryType.java b/core/src/main/java/org/lflang/target/property/type/DictionaryType.java index 08174b11a8..4b71eedc0d 100644 --- a/core/src/main/java/org/lflang/target/property/type/DictionaryType.java +++ b/core/src/main/java/org/lflang/target/property/type/DictionaryType.java @@ -14,6 +14,7 @@ import org.lflang.target.property.CoordinationOptionsProperty.CoordinationOption; import org.lflang.target.property.DockerProperty.DockerOption; import org.lflang.target.property.PlatformProperty.PlatformOption; +import org.lflang.target.property.SchedulerProperty.SchedulerDictOption; import org.lflang.target.property.TracingProperty.TracingOption; /** @@ -26,7 +27,8 @@ public enum DictionaryType implements TargetPropertyType { DOCKER_DICT(Arrays.asList(DockerOption.values())), PLATFORM_DICT(Arrays.asList(PlatformOption.values())), COORDINATION_OPTION_DICT(Arrays.asList(CoordinationOption.values())), - TRACING_DICT(Arrays.asList(TracingOption.values())); + TRACING_DICT(Arrays.asList(TracingOption.values())), + SCHEDULER_DICT(Arrays.asList(SchedulerDictOption.values())); /** The keys and assignable types that are allowed in this dictionary. */ public List options; diff --git a/core/src/main/java/org/lflang/target/property/type/SchedulerType.java b/core/src/main/java/org/lflang/target/property/type/SchedulerType.java index d6b9816ad3..2194bcccb4 100644 --- a/core/src/main/java/org/lflang/target/property/type/SchedulerType.java +++ b/core/src/main/java/org/lflang/target/property/type/SchedulerType.java @@ -27,7 +27,8 @@ public enum Scheduler { Path.of("worker_states.h"), Path.of("data_collection.h"))), GEDF_NP(true), // Global EDF non-preemptive - GEDF_NP_CI(true); // Global EDF non-preemptive with chain ID + GEDF_NP_CI(true), // Global EDF non-preemptive with chain ID + STATIC(true); /** Indicate whether the scheduler prioritizes reactions by deadline. */ private final boolean prioritizesDeadline; @@ -59,6 +60,16 @@ public static Scheduler getDefault() { return Scheduler.NP; } + public static Scheduler fromString(String name) { + for (Scheduler scheduler : Scheduler.values()) { + if (scheduler.name().equalsIgnoreCase(name)) { + return scheduler; + } + } + // Throw an exception if no match is found + throw new IllegalArgumentException("No Scheduler with name " + name + " found"); + } + public String getSchedulerCompileDef() { return "SCHED_" + this.name(); } diff --git a/core/src/main/java/org/lflang/target/property/type/StaticSchedulerType.java b/core/src/main/java/org/lflang/target/property/type/StaticSchedulerType.java new file mode 100644 index 0000000000..e035cb4a80 --- /dev/null +++ b/core/src/main/java/org/lflang/target/property/type/StaticSchedulerType.java @@ -0,0 +1,26 @@ +package org.lflang.target.property.type; + +import org.lflang.target.property.type.StaticSchedulerType.StaticScheduler; + +public class StaticSchedulerType extends OptionsType { + + @Override + protected Class enumClass() { + return StaticScheduler.class; + } + + /** + * Supported schedulers. + * + * @author Shaokai Lin + */ + public enum StaticScheduler { + LB, + EGS, + MOCASIN; + + public static StaticScheduler getDefault() { + return LB; + } + } +} diff --git a/core/src/main/java/org/lflang/target/property/type/UnionType.java b/core/src/main/java/org/lflang/target/property/type/UnionType.java index dc53fc26b5..cf808969f7 100644 --- a/core/src/main/java/org/lflang/target/property/type/UnionType.java +++ b/core/src/main/java/org/lflang/target/property/type/UnionType.java @@ -18,7 +18,9 @@ public enum UnionType implements TargetPropertyType { PLATFORM_STRING_OR_DICTIONARY(List.of(new PlatformType(), DictionaryType.PLATFORM_DICT)), FILE_OR_FILE_ARRAY(Arrays.asList(PrimitiveType.FILE, ArrayType.FILE_ARRAY)), DOCKER_UNION(Arrays.asList(PrimitiveType.BOOLEAN, DictionaryType.DOCKER_DICT)), - TRACING_UNION(Arrays.asList(PrimitiveType.BOOLEAN, DictionaryType.TRACING_DICT)); + TRACING_UNION(Arrays.asList(PrimitiveType.BOOLEAN, DictionaryType.TRACING_DICT)), + SCHEDULER_UNION_OR_DICTIONARY(List.of(new SchedulerType(), DictionaryType.SCHEDULER_DICT)), + ; /** The constituents of this type union. */ public final List options; diff --git a/core/src/main/java/org/lflang/util/FileUtil.java b/core/src/main/java/org/lflang/util/FileUtil.java index cfd44c432c..c0b1d9083b 100644 --- a/core/src/main/java/org/lflang/util/FileUtil.java +++ b/core/src/main/java/org/lflang/util/FileUtil.java @@ -680,6 +680,10 @@ public static void arduinoDeleteHelper(Path srcGenPath, boolean threadingOn) thr // Delete the remaining federated sources and headers deleteDirectory(srcGenPath.resolve("src/core/federated")); + delete( + srcGenPath.resolve( + "core/threaded/scheduler_static.c")); // TODO: Support the STATIC scheduler. + List allPaths = Files.walk(srcGenPath).sorted(Comparator.reverseOrder()).toList(); for (Path path : allPaths) { String toCheck = path.toString().toLowerCase(); diff --git a/core/src/main/java/org/lflang/validation/AttributeSpec.java b/core/src/main/java/org/lflang/validation/AttributeSpec.java index a685e98ca7..200c298adb 100644 --- a/core/src/main/java/org/lflang/validation/AttributeSpec.java +++ b/core/src/main/java/org/lflang/validation/AttributeSpec.java @@ -181,6 +181,13 @@ public void check(LFValidator validator, AttrParm parm) { Literals.ATTRIBUTE__ATTR_NAME); } } + case TIME -> { + if (!ASTUtils.isValidTime(parm.getTime())) { + validator.error( + "Incorrect type: \"" + parm.getName() + "\"" + " should have type Time.", + Literals.ATTRIBUTE__ATTR_NAME); + } + } default -> throw new IllegalArgumentException("unexpected type"); } } @@ -192,6 +199,7 @@ enum AttrParamType { INT, BOOLEAN, FLOAT, + TIME, } /* @@ -237,6 +245,10 @@ enum AttrParamType { new AttrParamSpec("CT", AttrParamType.INT, true), new AttrParamSpec("expect", AttrParamType.BOOLEAN, true)))); ATTRIBUTE_SPECS_BY_NAME.put("_c_body", new AttributeSpec(null)); + // @wcet(nanoseconds) + ATTRIBUTE_SPECS_BY_NAME.put( + "wcet", + new AttributeSpec(List.of(new AttrParamSpec(VALUE_ATTR, AttrParamType.STRING, false)))); ATTRIBUTE_SPECS_BY_NAME.put( "_tpoLevel", new AttributeSpec(List.of(new AttrParamSpec(VALUE_ATTR, AttrParamType.INT, false)))); diff --git a/core/src/main/java/org/lflang/validation/LFValidator.java b/core/src/main/java/org/lflang/validation/LFValidator.java index eb721cf032..efa29d474e 100644 --- a/core/src/main/java/org/lflang/validation/LFValidator.java +++ b/core/src/main/java/org/lflang/validation/LFValidator.java @@ -793,13 +793,16 @@ public void checkReaction(Reaction reaction) { trigs.add(toOriginalText(tVarRef)); } } - if (trigs.size() > 0) { - error( - String.format( - "Reaction triggers involved in cyclic dependency in reactor %s: %s.", - reactor.getName(), String.join(", ", trigs)), - Literals.REACTION__TRIGGERS); - } + // FIXME: Commenting out the cyclic dependency check for now. + // We need to update this since we are no longer skipping delayed and + // physical connections when checking eventual destination ports. + // if (trigs.size() > 0) { + // error( + // String.format( + // "Reaction triggers involved in cyclic dependency in reactor %s: %s.", + // reactor.getName(), String.join(", ", trigs)), + // Literals.REACTION__TRIGGERS); + // } // Report involved sources. List sources = new ArrayList<>(); @@ -815,13 +818,16 @@ public void checkReaction(Reaction reaction) { sources.add(toOriginalText(t)); } } - if (sources.size() > 0) { - error( - String.format( - "Reaction sources involved in cyclic dependency in reactor %s: %s.", - reactor.getName(), String.join(", ", sources)), - Literals.REACTION__SOURCES); - } + // FIXME: Commenting out the cyclic dependency check for now. + // We need to update this since we are no longer skipping delayed and + // physical connections when checking eventual destination ports. + // if (sources.size() > 0) { + // error( + // String.format( + // "Reaction sources involved in cyclic dependency in reactor %s: %s.", + // reactor.getName(), String.join(", ", sources)), + // Literals.REACTION__SOURCES); + // } // Report involved effects. List effects = new ArrayList<>(); @@ -837,33 +843,40 @@ public void checkReaction(Reaction reaction) { effects.add(toOriginalText(t)); } } - if (effects.size() > 0) { - error( - String.format( - "Reaction effects involved in cyclic dependency in reactor %s: %s.", - reactor.getName(), String.join(", ", effects)), - Literals.REACTION__EFFECTS); - } + // FIXME: Commenting out the cyclic dependency check for now. + // We need to update this since we are no longer skipping delayed and + // physical connections when checking eventual destination ports. + // if (effects.size() > 0) { + // error( + // String.format( + // "Reaction effects involved in cyclic dependency in reactor %s: %s.", + // reactor.getName(), String.join(", ", effects)), + // Literals.REACTION__EFFECTS); + // } + + // FIXME: Commenting out the cyclic dependency check for now. + // if (trigs.size() + sources.size() == 0) { + // error( + // String.format( + // "Cyclic dependency due to preceding reaction. Consider reordering reactions + // within" + // + " reactor %s to avoid causality loop.", + // reactor.getName()), + // reaction.eContainer(), + // Literals.REACTOR__REACTIONS, + // reactor.getReactions().indexOf(reaction)); + // } else if (effects.size() == 0) { + // error( + // String.format( + // "Cyclic dependency due to succeeding reaction. Consider reordering reactions + // within" + // + " reactor %s to avoid causality loop.", + // reactor.getName()), + // reaction.eContainer(), + // Literals.REACTOR__REACTIONS, + // reactor.getReactions().indexOf(reaction)); + // } - if (trigs.size() + sources.size() == 0) { - error( - String.format( - "Cyclic dependency due to preceding reaction. Consider reordering reactions within" - + " reactor %s to avoid causality loop.", - reactor.getName()), - reaction.eContainer(), - Literals.REACTOR__REACTIONS, - reactor.getReactions().indexOf(reaction)); - } else if (effects.size() == 0) { - error( - String.format( - "Cyclic dependency due to succeeding reaction. Consider reordering reactions within" - + " reactor %s to avoid causality loop.", - reactor.getName()), - reaction.eContainer(), - Literals.REACTOR__REACTIONS, - reactor.getReactions().indexOf(reaction)); - } // Not reporting reactions that are part of cycle _only_ due to reaction ordering. // Moving them won't help solve the problem. } diff --git a/core/src/main/resources/lib/c/reactor-c b/core/src/main/resources/lib/c/reactor-c index 3d7715c39f..e8975a95cf 160000 --- a/core/src/main/resources/lib/c/reactor-c +++ b/core/src/main/resources/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit 3d7715c39fc40ad3c4c29918e724dc5b96738ca5 +Subproject commit e8975a95cf5286ff16431d3ba6e6f01a0d68495b diff --git a/core/src/main/resources/staticScheduler/mocasin/sdf3-sdf.xsd b/core/src/main/resources/staticScheduler/mocasin/sdf3-sdf.xsd new file mode 100644 index 0000000000..eb65a451d2 --- /dev/null +++ b/core/src/main/resources/staticScheduler/mocasin/sdf3-sdf.xsd @@ -0,0 +1,420 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/test/java/org/lflang/tests/compiler/FormattingUnitTests.java b/core/src/test/java/org/lflang/tests/compiler/FormattingUnitTests.java index 072c32c106..a50d3e4aae 100644 --- a/core/src/test/java/org/lflang/tests/compiler/FormattingUnitTests.java +++ b/core/src/test/java/org/lflang/tests/compiler/FormattingUnitTests.java @@ -93,6 +93,36 @@ public void testCppInits() { """); } + @Test + public void testAnnotation() { + assertIsFormatted( + """ + target C { + scheduler: { + type: STATIC, + mapper: LB + }, + workers: 2, + timeout: 1 sec + } + + reactor Source { + output out: int + timer t(1 nsec, 10 msec) + state s: int = 0 + + @wcet(1 ms) + reaction(startup) {= lf_print("Starting Source"); =} + + @wcet(3 ms) + reaction(t) -> out {= + lf_set(out, self->s++); + lf_print("Inside source reaction_0"); + =} + } + """); + } + @Inject LfParsingTestHelper parser; private void assertIsFormatted(String input) { diff --git a/core/src/testFixtures/java/org/lflang/tests/TestBase.java b/core/src/testFixtures/java/org/lflang/tests/TestBase.java index fe724cad9c..c5933f339d 100644 --- a/core/src/testFixtures/java/org/lflang/tests/TestBase.java +++ b/core/src/testFixtures/java/org/lflang/tests/TestBase.java @@ -149,6 +149,7 @@ public static class Message { public static final String DESC_ROS2 = "Running tests using ROS2."; public static final String DESC_MODAL = "Run modal reactor tests."; public static final String DESC_VERIFIER = "Run verifier tests."; + public static final String DESC_STATIC_SCHEDULER = "Run static scheduler tests."; } /** Constructor for test classes that test a single target. */ diff --git a/core/src/testFixtures/java/org/lflang/tests/TestRegistry.java b/core/src/testFixtures/java/org/lflang/tests/TestRegistry.java index ab20848871..27985cc491 100644 --- a/core/src/testFixtures/java/org/lflang/tests/TestRegistry.java +++ b/core/src/testFixtures/java/org/lflang/tests/TestRegistry.java @@ -338,6 +338,7 @@ public enum TestCategory { ZEPHYR_BOARDS(false, "zephyr" + File.separator + "boards", TestLevel.BUILD), FLEXPRET(false, "flexpret", TestLevel.BUILD), VERIFIER(false, "verifier", TestLevel.EXECUTION), + STATIC_SCHEDULER(false, "static", TestLevel.EXECUTION), TARGET(false, "", TestLevel.EXECUTION); /** Whether we should compare coverage against other targets. */ diff --git a/test/C/src/static/Feedback2.lf b/test/C/src/static/Feedback2.lf new file mode 100644 index 0000000000..16f2df9567 --- /dev/null +++ b/test/C/src/static/Feedback2.lf @@ -0,0 +1,55 @@ +/** + * This test case shows that the static scheduler can handle feedback loops, + * assuming that a few key constraints are satisfied, among them: + * - Reactions triggered by startup and timers cannot have other triggers (use + * Feedback.lf to relax this constraint). + * - lf_request_stop() is not supported. The only way to terminate a program is + * to use a timeout value (use Feedback.lf to relax this constraint). If a + * timeout is not provided, it is as if keepalive is by default set to true. + */ +target C { + fast: true, + // logging: DEBUG, + build-type: Debug, + scheduler: STATIC, + timeout: 1000 msec, // IMPORTANT: this must match ITERATION. +} + +preamble {= + #define ITERATION 1000 +=} + +reactor A(iteration : int = 1000) { + input in:int + output out:int + state count:int = 0 + reaction(startup) -> out {= + self->count++; + lf_set(out, self->count); + // lf_print("In A: count = %d", self->count); + =} + reaction(in) -> out {= + self->count++; + lf_set(out, self->count); + // lf_print("In A: count = %d", self->count); + =} +} + +reactor B(iteration : int = 1000) { + input in:int + output out:int + reaction(in) -> out {= + // lf_print("In B"); + if (in->value < self->iteration) + lf_set_present(out); + else if (in->value == self->iteration) + lf_print("SUCCESS: all iterations finished."); + =} +} + +main reactor { + a = new A(iteration={=ITERATION=}) + b = new B(iteration={=ITERATION=}) + a.out -> b.in + b.out -> a.in after 1 msec +} \ No newline at end of file diff --git a/test/C/src/static/RemoveWUs.lf b/test/C/src/static/RemoveWUs.lf new file mode 100644 index 0000000000..3e6130e1a8 --- /dev/null +++ b/test/C/src/static/RemoveWUs.lf @@ -0,0 +1,88 @@ +/** + * This is a test case for a peephole optimization that removes redundant wait + * until instructions. + * FIXME: Support for multiports is crucial for this one. + */ +target C { + scheduler: { + type: STATIC, + mapper: LB, + }, + fast: true, + // FIXME: When worker = 1, the test fails. Post connection helpers are + // inserted correctly in the DOT file, but not correctly in the schedule. + workers: 2, +} + +reactor Source { + output out:int + @wcet("2 usec") + reaction(startup) -> out {= + lf_set_present(out); + =} +} + +reactor Sink { + input in0:int + input in1:int + input in2:int + input in3:int + input in4:int + input in5:int + input in6:int + input in7:int + input in8:int + input in9:int + @wcet("10 msec") + reaction( + in0, + in1, + in2, + in3, + in4, + in5, + in6, + in7, + in8, + in9 + ) {= + int count = 0; + if (in0->is_present) count++; + if (in1->is_present) count++; + if (in2->is_present) count++; + if (in3->is_present) count++; + if (in4->is_present) count++; + if (in5->is_present) count++; + if (in6->is_present) count++; + if (in7->is_present) count++; + if (in8->is_present) count++; + if (in9->is_present) count++; + if (count == 10) lf_print("Successfully received all 10 inputs."); + else lf_print("Fail to receive all 10 inputs. Received: %d", count); + =} +} + +main reactor { + s0 = new Source() + s1 = new Source() + s2 = new Source() + s3 = new Source() + s4 = new Source() + s5 = new Source() + s6 = new Source() + s7 = new Source() + s8 = new Source() + s9 = new Source() + k = new Sink() + + s0.out -> k.in0 + s1.out -> k.in1 + s2.out -> k.in2 + s3.out -> k.in3 + s4.out -> k.in4 + s5.out -> k.in5 + s6.out -> k.in6 + s7.out -> k.in7 + s8.out -> k.in8 + s9.out -> k.in9 +} \ No newline at end of file diff --git a/test/C/src/static/ScheduleTest.lf b/test/C/src/static/ScheduleTest.lf new file mode 100644 index 0000000000..6db8d20d62 --- /dev/null +++ b/test/C/src/static/ScheduleTest.lf @@ -0,0 +1,85 @@ +target C { + scheduler: { + type: STATIC, + mapper: LB, + }, + workers: 2, + timeout: 100 sec, + fast: true, + // logging: Debug, +} + +preamble {= +#define EXPECTED 100030000 +=} + +reactor Source(id : int = 0) { + output out: int + output out2: int + timer t(1 msec, 10 msec) + state s: int = 0 + + @wcet("1 ms, 500 us") + reaction(startup) {= + // lf_print("[Source %d reaction_1] Starting Source", self->id); + =} + + @wcet("3 ms, 500 us") + reaction(t) -> out, out2 {= + self->s++; + lf_set(out, self->s); + lf_set(out2, self->s); + // lf_print("[Source %d reaction_2] Inside source reaction_1", self->id); + =} +} + +reactor Sink { + input in: int + input in2: int + timer t(1 msec, 5 msec) + state sum: int = 0 + + @wcet("1 ms, 500 us") + reaction(startup) {= + // lf_print("[Sink reaction_1] Starting Sink"); + =} + + @wcet("1 ms, 500 us") + reaction(t) {= + self->sum++; + // lf_print("[Sink reaction_2] Sum: %d", self->sum); + =} + + @wcet("1 ms, 500 us") + reaction(in) {= + self->sum += in->value; + // lf_print("[Sink reaction_3] Sum: %d", self->sum); + =} + + @wcet("1 ms, 500 us") + reaction(in2) {= + self->sum += in2->value; + // lf_print("[Sink reaction_4] Sum: %d", self->sum); + =} + + @wcet("1 ms, 500 us") + reaction(shutdown) {= + if (self->sum != EXPECTED) { + fprintf(stderr, "[Sink reaction_5] FAILURE: Expected %d, Received %d\n", EXPECTED, self->sum); + exit(1); + } else { + lf_print("Successfully received %d", self->sum); + } + =} +} + +main reactor { + source = new Source(id=0) + source2 = new Source(id=1) + sink = new Sink() + sink2 = new Sink() + source.out -> sink.in + source2.out -> sink.in2 + source.out2 -> sink2.in + source2.out2 -> sink2.in2 +} diff --git a/test/C/src/static/Simple.lf b/test/C/src/static/Simple.lf new file mode 100644 index 0000000000..1cbfb01591 --- /dev/null +++ b/test/C/src/static/Simple.lf @@ -0,0 +1,42 @@ +target C { + scheduler: STATIC, + timeout: 5 sec, +} + +reactor Sensor { + output out:int + timer t(0, 1 sec) + state count:int = 0 + reaction(t) -> out {= + lf_print("Sensor: logical time: %lld, physical time: %lld", lf_time_logical_elapsed(), lf_time_physical_elapsed()); + // Have to factor the increment out because the compiler complains that + // it cannot take the address of self->count++. + self->count += 1; + lf_set(out, self->count); + =} +} + +reactor Processor { + input in:int + output out:int + reaction(in) -> out {= + lf_print("Processor: logical time: %lld, physical time: %lld", lf_time_logical_elapsed(), lf_time_physical_elapsed()); + int v = in->value*2; + lf_set(out, v); + =} +} + +reactor Actuator { + input in:int + reaction(in) {= + lf_print("Actuator: %d, logical time: %lld, physical time: %lld", in->value, lf_time_logical_elapsed(), lf_time_physical_elapsed()); + =} +} + +main reactor { + s = new Sensor() + p = new Processor() + a = new Actuator() + s.out -> p.in + p.out -> a.in +} \ No newline at end of file diff --git a/test/C/src/static/SimpleConnection.lf b/test/C/src/static/SimpleConnection.lf new file mode 100644 index 0000000000..63a5bde9ad --- /dev/null +++ b/test/C/src/static/SimpleConnection.lf @@ -0,0 +1,52 @@ +target C { + scheduler: STATIC, + timeout: 10 sec, + build-type: Debug, + // logging: DEBUG, +} + +preamble {= + #define EXPECTED -9 +=} + +reactor Source { + output out:int + timer t(0, 1 sec) + state s:int = 0 + reaction(t) -> out {= + lf_set(out, self->s); + lf_print("Sent %d @ %lld", self->s++, lf_time_logical_elapsed()); + =} + reaction(t) -> out {= + int v = -1 * self->s; + lf_set(out, v); + =} +} + +reactor Sink { + input in:int + state last_received:int = 0 + reaction(in) {= + self->last_received = in->value; + =} + // FIXME: Multiple reactions triggered by the same port does not yet work. + reaction(in) {= + lf_print("Received %d @ %lld", in->value, lf_time_logical_elapsed()); + =} + reaction(shutdown) {= + if (self->last_received != EXPECTED) { + fprintf(stderr, "FAILURE: Expected %d, Received %d\n", EXPECTED, self->last_received); + exit(1); + } else { + lf_print("Successfully received %d", self->last_received); + } + =} +} + +main reactor { + source = new Source() + sink = new Sink() + source.out -> sink.in after 2 sec + // source.out -> sink.in after 500 msec + // source.out -> sink.in +} \ No newline at end of file diff --git a/test/C/src/static/StaticAlignment.lf b/test/C/src/static/StaticAlignment.lf new file mode 100644 index 0000000000..b300320005 --- /dev/null +++ b/test/C/src/static/StaticAlignment.lf @@ -0,0 +1,112 @@ +// This test checks that the downstream reaction is not invoked more than once at a logical time. +target C { + // logging: LOG, + timeout: 1 sec, + scheduler: STATIC, + build-type: Debug, +} + +reactor Source { + output out: int + output out2: int + state count: int = 1 + timer t(0, 100 msec) + + reaction(t) -> out, out2 {= + self->count++; + lf_set(out, self->count); // FIXME: Support (self->count++) in the static lf_set(). + lf_set(out2, self->count); + =} +} + +reactor Sieve { + input in: int + output out: bool + state primes: int* = {= NULL =} + state last_prime: int = 0 + state _true: bool = true; + + reaction(startup) {= + // There are 1229 primes between 1 and 10,000. + self->primes = (int*)calloc(1229, sizeof(int)); + // Primes 1 and 2 are not on the list. + self->primes[0] = 3; + =} + + reaction(in) -> out {= + // Reject inputs that are out of bounds. + if (in->value <= 0 || in->value > 10000) { + lf_print_warning("Sieve: Input value out of range: %d.", in->value); + } + // Primes 1 and 2 are not on the list. + if (in->value == 1 || in->value == 2) { + lf_set(out, self->_true); // FIXME: Support constants in the static lf_set(). + return; + } + // If the input is greater than the last found prime, then + // we have to expand the list of primes before checking to + // see whether this is prime. + int candidate = self->primes[self->last_prime]; + while (in->value > self->primes[self->last_prime]) { + // The next prime is always odd, so we can increment by two. + candidate += 2; + bool prime = true; + for (int i = 0; i < self->last_prime; i++) { + if (candidate % self->primes[i] == 0) { + // Candidate is not prime. Break and add 2 + prime = false; + break; + } + } + // If the candidate is not divisible by any prime in the list, it is prime. + if (prime) { + self->last_prime++; + self->primes[self->last_prime] = candidate; + lf_print("Sieve: Found prime: %d.", candidate); + } + } + // We are now assured that the input is less than or + // equal to the last prime on the list. + // See whether the input is an already found prime. + for (int i = self->last_prime; i >= 0; i--) { + // Search the primes from the end, where they are sparser. + if (self->primes[i] == in->value) { + lf_set(out, self->_true); // FIXME: Support constants in the static lf_set(). + return; + } + } + =} +} + +reactor Destination { + input ok: bool + input in: int + state last_invoked: tag_t = {= NEVER_TAG_INITIALIZER =} + + reaction(ok, in) {= + tag_t current_tag = lf_tag(); + if (ok->is_present && in->is_present) { + lf_print("Destination: Input %d is prime at tag (%lld, %d).", + in->value, + current_tag.time - lf_time_start(), current_tag.microstep + ); + } + if (lf_tag_compare(current_tag, self->last_invoked) <= 0) { + lf_print_error_and_exit("Invoked at tag (%lld, %d), " + "but previously invoked at tag (%lld, %d).", + current_tag.time - lf_time_start(), current_tag.microstep, + self->last_invoked.time - lf_time_start(), self->last_invoked.microstep + ); + } + self->last_invoked = current_tag; + =} +} + +main reactor { + source = new Source() + sieve = new Sieve() + destination = new Destination() + source.out -> sieve.in + sieve.out -> destination.ok + source.out2 -> destination.in +} diff --git a/test/C/src/static/StaticSenseToAct.lf b/test/C/src/static/StaticSenseToAct.lf new file mode 100644 index 0000000000..e9808f1b2c --- /dev/null +++ b/test/C/src/static/StaticSenseToAct.lf @@ -0,0 +1,50 @@ +target C { + scheduler: STATIC, + timeout: 1 sec, + fast: true, // FIXME: For some strange reason, setting this is false causes the test harness to report failure. +} + +preamble {= + #include "platform.h" +=} + +reactor Sensor { + output out: int + timer t(0, 50 msec) + state cnt: int = 0 + + reaction(t) -> out {= + self->cnt++; + lf_set(out, self->cnt); + =} +} + +reactor Processor { + input in: int + output out: int + + state cnt: int = 0 + + reaction(in) -> out {= + lf_print("Filter Got: %d @ " PRINTF_TIME, in->value, lf_time_logical_elapsed()); + if (++self->cnt == 2) { + lf_print("Filter writes: %d @ " PRINTF_TIME, in->value, lf_time_logical_elapsed()); + lf_set(out, in->value); + } + if (self->cnt == 2) self->cnt = 0; + =} +} + +reactor Act { + input in: int + + reaction(in) {= lf_print("Act Got: %d @ " PRINTF_TIME, in->value, lf_time_logical_elapsed()); =} +} + +main reactor { + sensor = new Sensor() + filter = new Processor() + act = new Act() + + sensor.out, filter.out -> filter.in, act.in +} diff --git a/test/C/src/static/ThreePhases.lf b/test/C/src/static/ThreePhases.lf new file mode 100644 index 0000000000..95263e1cdf --- /dev/null +++ b/test/C/src/static/ThreePhases.lf @@ -0,0 +1,39 @@ +target C { + scheduler: { + type: STATIC, + mapper: LB, + }, + workers: 1, + timeout: 5 sec, + build-type: Debug, +} + +reactor R { + input in:int + output out:int + state s:int = 42 + timer t(2 sec, 1 sec) + @wcet("1 ms") + reaction(startup) -> out {= + printf("Reaction 1 triggered by startup at %lld\n", lf_time_logical()); + // int payload = 42; // FIXME: Constant payload not working yet. + lf_set(out, self->s); + =} + @wcet("1 ms") + reaction(in) {= + printf("Reaction 2 triggered at %lld. Received: %d\n", lf_time_logical(), in->value); + =} + @wcet("1 ms") + reaction(t) {= + printf("Reaction 3 triggered by timer at %lld\n", lf_time_logical()); + =} + @wcet("1 ms") + reaction(shutdown) {= + printf("Reaction 4 triggered by shutdown.\n"); + =} +} + +main reactor { + r = new R() + r.out -> r.in after 1 sec +} \ No newline at end of file diff --git a/test/C/src/static/TwoConnections.lf b/test/C/src/static/TwoConnections.lf new file mode 100644 index 0000000000..a8814eb73a --- /dev/null +++ b/test/C/src/static/TwoConnections.lf @@ -0,0 +1,38 @@ +target C { + scheduler: STATIC, + timeout: 10 sec, + build-type: Debug, + // logging: DEBUG, +} + +reactor Source(id:int = 0, period:time = 1 sec) { + output out:int + timer t(0, period) + state s:int = 0 + reaction(t) -> out {= + long long int logical_time = lf_time_logical(); + long long int physical_time = lf_time_physical(); + long long int lag = physical_time - logical_time; + lf_set(out, self->s); + lf_print("[Source %d] Sent %d @ logical time %lld, physical time %lld, lag %lld", self->id, self->s++, logical_time, physical_time, lag); + =} +} + +reactor Sink(id:int = 0) { + input in:int + reaction(in) {= + long long int logical_time = lf_time_logical(); + long long int physical_time = lf_time_physical(); + long long int lag = physical_time - logical_time; + lf_print("[Sink %d] Received %d @ logical time %lld, physical time %lld, lag %lld", self->id, in->value, logical_time, physical_time, lag); + =} +} + +main reactor { + source1 = new Source(id = 1, period = 1 sec) + source2 = new Source(id = 2, period = 10 sec) + sink1 = new Sink(id = 1) + sink2 = new Sink(id = 2) + source1.out -> sink1.in after 2 sec + source2.out -> sink2.in after 3 sec +} \ No newline at end of file diff --git a/test/C/src/static/test/StaticComposition.lf b/test/C/src/static/test/StaticComposition.lf new file mode 100644 index 0000000000..bc990507e2 --- /dev/null +++ b/test/C/src/static/test/StaticComposition.lf @@ -0,0 +1,45 @@ +// This test connects a simple counting source to tester that checks against its own count. +target C { + fast: true, + scheduler: STATIC, + timeout: 10 sec +} + +reactor Source(period: time = 2 sec) { + output y: int + timer t(1 sec, period) + state count: int = 0 + + reaction(t) -> y {= + (self->count)++; + printf("Source sending %d.\n", self->count); + lf_set(y, self->count); + =} +} + +reactor Test { + input x: int + state count: int = 0 + + reaction(x) {= + (self->count)++; + printf("Received %d\n", x->value); + if (x->value != self->count) { + fprintf(stderr, "FAILURE: Expected %d\n", self->count); + exit(1); + } + =} + + reaction(shutdown) {= + if (self->count == 0) { + fprintf(stderr, "FAILURE: No data received.\n"); + } + =} +} + +main reactor { + s = new Source() + + d = new Test() + s.y -> d.x +} diff --git a/test/C/src/static/test/StaticDeadline.lf b/test/C/src/static/test/StaticDeadline.lf new file mode 100644 index 0000000000..f2a9f2e5b6 --- /dev/null +++ b/test/C/src/static/test/StaticDeadline.lf @@ -0,0 +1,65 @@ +// This example illustrates local deadline handling. Even numbers are sent by the Source +// immediately, whereas odd numbers are sent after a big enough delay to violate the deadline. +target C { + scheduler: { + type: STATIC, + mapper: LB + }, + timeout: 6 sec +} + +preamble {= + #ifdef __cplusplus + extern "C" { + #endif + #include "platform.h" + #ifdef __cplusplus + } + #endif +=} + +reactor Source(period: time = 3 sec) { + output y: int + timer t(0, period) + state count: int = 0 + + reaction(t) -> y {= + if (2 * (self->count / 2) != self->count) { + // The count variable is odd. + // Take time to cause a deadline violation. + lf_sleep(MSEC(1500)); + } + printf("Source sends: %d.\n", self->count); + lf_set(y, self->count); + (self->count)++; + =} +} + +reactor Destination(timeout: time = 1 sec) { + input x: int + state count: int = 0 + + reaction(x) {= + printf("Destination receives: %d\n", x->value); + if (2 * (self->count / 2) != self->count) { + // The count variable is odd, so the deadline should have been violated. + printf("ERROR: Failed to detect deadline.\n"); + exit(1); + } + (self->count)++; + =} deadline(timeout) {= + printf("Destination deadline handler receives: %d\n", x->value); + if (2 * (self->count / 2) == self->count) { + // The count variable is even, so the deadline should not have been violated. + printf("ERROR: Deadline miss handler invoked without deadline violation.\n"); + exit(2); + } + (self->count)++; + =} +} + +main reactor { + s = new Source() + d = new Destination(timeout = 1 sec) + s.y -> d.x +} diff --git a/test/C/src/static_unsupported/Feedback.lf b/test/C/src/static_unsupported/Feedback.lf new file mode 100644 index 0000000000..5fc132e57d --- /dev/null +++ b/test/C/src/static_unsupported/Feedback.lf @@ -0,0 +1,41 @@ +target C { + fast: true, + build-type: Debug, // logging: DEBUG, + scheduler: STATIC +} + +preamble {= + #define ITERATION 1000 +=} + +reactor A(iteration: int = 1000) { + input in: int + output out: int + state count: int = 0 + + reaction(startup, in) -> out {= + self->count++; + lf_set(out, self->count); + lf_print("In A: count = %d", self->count); + =} +} + +reactor B(iteration: int = 1000) { + input in: int + output out: int + + reaction(in) -> out {= + lf_print("In B"); + if (in->value < self->iteration) + lf_set_present(out); + else if (in->value == self->iteration) + lf_print("SUCCESS: all iterations finished."); + =} +} + +main reactor { + a = new A(iteration = {= ITERATION =}) + b = new B(iteration = {= ITERATION =}) + a.out -> b.in + b.out -> a.in after 1 msec +} diff --git a/test/C/src/static_unsupported/NotSoSimplePhysicalAction.lf b/test/C/src/static_unsupported/NotSoSimplePhysicalAction.lf new file mode 100644 index 0000000000..a3fd0702c9 --- /dev/null +++ b/test/C/src/static_unsupported/NotSoSimplePhysicalAction.lf @@ -0,0 +1,45 @@ +target C + +preamble {= + #include "include/core/platform.h" +=} + +main reactor { + preamble {= + // Thread to read input characters until an EOF is received. + // Each time a newline is received, schedule a user_response action. + void* read_input(void* physical_action) { + int c; + while(1) { + while((c = getchar()) != '\n') { + if (c == EOF) break; + } + lf_schedule_copy(physical_action, 0, &c, 1); + if (c == EOF) break; + } + return NULL;bb + } + =} + + timer t(15 sec, 5 sec) + logical action a(10 sec): char + physical action b(0, 3 sec): char + + reaction(startup) -> a {= + lf_schedule(a, 0); + =} + + reaction(a) -> b {= + // Start the thread that listens for Enter or Return. + lf_thread_t thread_id; + lf_thread_create(&thread_id, &read_input, a); + =} + + reaction(t) {= + lf_print("Hello."); + =} + + reaction(b) {= + lf_print("Surprise!"); + =} +} diff --git a/test/C/src/static_unsupported/SimpleAction.lf b/test/C/src/static_unsupported/SimpleAction.lf new file mode 100644 index 0000000000..70f9d63a2b --- /dev/null +++ b/test/C/src/static_unsupported/SimpleAction.lf @@ -0,0 +1,22 @@ +target C { + scheduler: STATIC, + timeout: 10 sec, + build-type: Debug // logging: DEBUG, +} + +reactor Source { + timer t(0, 1 sec) + logical action a(10 sec, 1 sec): int + state s: int = 0 + + reaction(t) -> a {= + lf_schedule_int(a, 0, self->s); + lf_print("Scheduled %d @ %lld", self->s++, lf_time_logical_elapsed()); + =} + + reaction(a) {= =} +} + +main reactor { + source = new Source() +} diff --git a/test/C/src/static_unsupported/SimplePhysicalAction.lf b/test/C/src/static_unsupported/SimplePhysicalAction.lf new file mode 100644 index 0000000000..8e57eef707 --- /dev/null +++ b/test/C/src/static_unsupported/SimplePhysicalAction.lf @@ -0,0 +1,42 @@ +target C { + scheduler: STATIC +} + +preamble {= + #include "include/core/platform.h" +=} + +main reactor { + preamble {= + // Thread to read input characters until an EOF is received. + // Each time a newline is received, schedule a user_response action. + void* read_input(void* physical_action) { + int c; + while(1) { + while((c = getchar()) != '\n') { + if (c == EOF) break; + } + lf_schedule_copy(physical_action, 0, &c, 1); + if (c == EOF) break; + } + return NULL; + } + =} + + timer t(1 sec, 1 sec) + physical action a(0, 500 msec): char + + reaction(startup) -> a {= + // Start the thread that listens for Enter or Return. + lf_thread_t thread_id; + lf_thread_create(&thread_id, &read_input, a); + =} + + reaction(t) {= + lf_print("Hello @ %lld", lf_time_logical_elapsed()); + =} + + reaction(a) {= + lf_print("Surprise @ %lld", lf_time_logical_elapsed()); + =} +} diff --git a/test/C/src/static_unsupported/StaticActionDelay.lf b/test/C/src/static_unsupported/StaticActionDelay.lf new file mode 100644 index 0000000000..08b408206b --- /dev/null +++ b/test/C/src/static_unsupported/StaticActionDelay.lf @@ -0,0 +1,54 @@ +// Test logical action with delay. +target C { + scheduler: STATIC +} + +reactor GeneratedDelay { + input y_in: int + output y_out: int + state y_state: int = 0 + logical action act(100 msec) + + reaction(y_in) -> act {= + self->y_state = y_in->value; + lf_schedule(act, MSEC(0)); + =} + + reaction(act) -> y_out {= + lf_set(y_out, self->y_state); + =} +} + +reactor Source { + output out: int + + reaction(startup) -> out {= + lf_set(out, 1); + =} +} + +reactor Sink { + input in: int + + reaction(in) {= + interval_t elapsed_logical = lf_time_logical_elapsed(); + interval_t logical = lf_time_logical(); + interval_t physical = lf_time_physical(); + printf("Logical, physical, and elapsed logical: %lld %lld %lld.\n", logical, physical, elapsed_logical); + if (elapsed_logical != MSEC(100)) { + printf("FAILURE: Expected %lld but got %lld.\n", MSEC(100), elapsed_logical); + exit(1); + } else { + printf("SUCCESS. Elapsed logical time is 100 msec.\n"); + } + =} +} + +main reactor { + source = new Source() + sink = new Sink() + g = new GeneratedDelay() + + source.out -> g.y_in + g.y_out -> sink.in +} diff --git a/test/C/src/static_unsupported/StaticPingPong.lf b/test/C/src/static_unsupported/StaticPingPong.lf new file mode 100644 index 0000000000..00721d52ae --- /dev/null +++ b/test/C/src/static_unsupported/StaticPingPong.lf @@ -0,0 +1,109 @@ +/** + * Basic benchmark from the Savina benchmark suite that is intended to measure message-passing + * overhead. See [Benchmarks wiki page](https://github.com/icyphy/lingua-franca/wiki/Benchmarks). + * This is based on https://www.scala-lang.org/old/node/54 See + * https://shamsimam.github.io/papers/2014-agere-savina.pdf. + * + * Ping introduces a microstep delay using a logical action to break the causality loop. + * + * To get a sense, some (informal) results for 1,000,000 ping-pongs on my Mac: + * + * Unthreaded: 97 msec Threaded: 265 msec + * + * There is no parallelism in this application, so it does not benefit from being being threaded, + * just some additional overhead. + * + * These measurements are total execution time, including startup and shutdown. These are about an + * order of magnitude faster than anything reported in the paper. + * + * @author Edward A. Lee + */ +target C { + fast: true // single-threaded: true, +} + +reactor WrapperPing(count: size_t = 10000000) { + ping = new Ping(count=count) + + input receive: size_t + input start: bool + output send: size_t + output finished: bool + + receive -> ping.receive + start -> ping.start + + ping.send -> send + ping.finished -> finished + + ping.serve_out -> ping.serve_in after 1 msec +} + +reactor Redirect { + input in: int + output out: int + + reaction(in) -> out {= =} +} + +reactor Ping(count: size_t = 1000000) { + input receive: size_t + input start: bool + output send: size_t + output finished: bool + state pingsLeft: size_t = count + input serve_in: int + output serve_out: int + + reaction(start, serve_in) -> send {= + lf_set(send, self->pingsLeft--); + =} + + reaction(receive) -> serve_out, finished {= + if (self->pingsLeft > 0) { + lf_set(serve_out, 0); + } else { + // reset pingsLeft for next iteration + self->pingsLeft = self->count; + lf_set(finished, true); + } + =} +} + +reactor Pong(expected: size_t = 1000000) { + input receive: size_t + output send: size_t + input finish: bool + state count: size_t = 0 + + reaction(receive) -> send {= + self->count++; + // lf_print("Received %d", receive->value); + lf_set(send, receive->value); + =} + + reaction(finish) {= + if (self->count != self->expected) { + lf_print_error_and_exit("Pong expected to receive %d inputs, but it received %d.\n", + self->expected, self->count + ); + exit(1); + } + printf("Success.\n"); + self->count = 0; + =} +} + +main reactor(count: size_t = 1000000) { + ping = new WrapperPing(count=count) + pong = new Pong(expected=count) + + ping.finished -> pong.finish + ping.send -> pong.receive + pong.send -> ping.receive + + reaction(startup) -> ping.start {= + lf_print("This is the PingPong benchmark."); + lf_set(ping.start, NULL); + =} +} diff --git a/util/tracing/trace_to_chrome.c b/util/tracing/trace_to_chrome.c new file mode 100644 index 0000000000..bd3cc66fc9 --- /dev/null +++ b/util/tracing/trace_to_chrome.c @@ -0,0 +1,492 @@ +/** + * @file + * @author Edward A. Lee + * + * @section LICENSE +Copyright (c) 2020, The University of California at Berkeley + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * @section DESCRIPTION + * Standalone program to convert a Lingua Franca trace file to a JSON file suitable + * for viewing in Chrome's event visualizer. To visualize the resulting file, + * point your chrome browser to chrome://tracing/ and the load the .json file. + */ +#define LF_TRACE +#include +#include +#include "reactor.h" +#include "trace.h" +#include "trace_util.h" + +#define PID_FOR_USER_EVENT 1000000 // Assumes no more than a million reactors. +#define PID_FOR_WORKER_WAIT 0 // Use 1000001 to show in separate trace. +#define PID_FOR_WORKER_ADVANCING_TIME 0 // Use 1000002 to show in separate trace. +#define PID_FOR_UNKNOWN_EVENT 2000000 + +/** Maximum thread ID seen. */ +int max_thread_id = 0; + +/** File containing the trace binary data. */ +FILE* trace_file = NULL; + +/** File for writing the output data. */ +FILE* output_file = NULL; + +/** + * Print a usage message. + */ +void usage() { + printf("\nUsage: trace_to_chrome [options] trace_file (with or without .lft extension)\n"); + printf("Options: \n"); + printf(" -p, --physical\n"); + printf(" Use only physical time, not logical time, for all horizontal axes.\n"); + printf("\n"); +} + +/** Maximum reaction number encountered. */ +int max_reaction_number = 0; + +/** Indicator to plot vs. physical time only. */ +bool physical_time_only = false; + +/** + * Read a trace in the specified file and write it to the specified json file. + * @param trace_file An open trace file. + * @param output_file An open output .json file. + * @return The number of records read or 0 upon seeing an EOF. + */ +size_t read_and_write_trace(FILE* trace_file, FILE* output_file) { + int trace_length = read_trace(trace_file); + if (trace_length == 0) return 0; + // Write each line. + for (int i = 0; i < trace_length; i++) { + char* reaction_name = "\"UNKNOWN\""; + + // Ignore federated trace events. + if (trace[i].event_type > federated) continue; + + if (trace[i].dst_id >= 0) { + reaction_name = (char*)malloc(4); + snprintf(reaction_name, 4, "%d", trace[i].dst_id); + } + // printf("DEBUG: Reactor's self struct pointer: %p\n", trace[i].pointer); + int reactor_index; + char* reactor_name = get_object_description(trace[i].pointer, &reactor_index); + if (reactor_name == NULL) { + if (trace[i].event_type == worker_wait_starts || trace[i].event_type == worker_wait_ends) { + reactor_name = "WAIT"; + } else if (trace[i].event_type == scheduler_advancing_time_starts + || trace[i].event_type == scheduler_advancing_time_starts) { + reactor_name = "ADVANCE TIME"; + } else { + reactor_name = "NO REACTOR"; + } + } + // Default name is the reactor name. + const char* name = reactor_name; + + // If static scheduler events are traced, + // change the name to the instruction name instead. + char buf[100]; + if (trace[i].event_type >= static_scheduler_ADDI_starts + && trace[i].event_type <= static_scheduler_WU_ends) { + sprintf(buf, "%d", trace[i].dst_id); + sprintf(buf + strlen(buf), ": "); + sprintf(buf + strlen(buf), "%s", trace_event_names[trace[i].event_type]); + name = buf; + } + + int trigger_index; + char* trigger_name = get_trigger_name(trace[i].trigger, &trigger_index); + if (trigger_name == NULL) { + trigger_name = "NONE"; + } + // By default, the timestamp used in the trace is the elapsed + // physical time in microseconds. But for schedule_called events, + // it will instead be the logical time at which the action or timer + // is to be scheduled. + interval_t elapsed_physical_time = (trace[i].physical_time - start_time)/1000; + interval_t timestamp = elapsed_physical_time; + interval_t elapsed_logical_time = (trace[i].logical_time - start_time)/1000; + + if (elapsed_physical_time < 0) { + fprintf(stderr, "WARNING: Negative elapsed physical time %lld. Skipping trace entry.\n", elapsed_physical_time); + continue; + } + if (elapsed_logical_time < 0) { + fprintf(stderr, "WARNING: Negative elapsed logical time %lld. Skipping trace entry.\n", elapsed_logical_time); + continue; + } + + // Default thread id is the worker number. + int thread_id = trace[i].src_id; + + char* args; + asprintf(&args, "{" + "\"reaction\": %s," // reaction number. + "\"logical time\": %lld," // logical time. + "\"physical time\": %lld," // physical time. + "\"microstep\": %d" // microstep. + "}", + reaction_name, + elapsed_logical_time, + elapsed_physical_time, + trace[i].microstep + ); + char* phase; + int pid; + switch(trace[i].event_type) { + case reaction_starts: + phase = "B"; + pid = 0; // Process 0 will be named "Execution" + break; + case reaction_ends: + phase = "E"; + pid = 0; // Process 0 will be named "Execution" + break; + case schedule_called: + phase = "i"; + pid = reactor_index + 1; // One pid per reactor. + if (!physical_time_only) { + timestamp = elapsed_logical_time + trace[i].extra_delay/1000; + } + thread_id = trigger_index; + name = trigger_name; + break; + case user_event: + pid = PID_FOR_USER_EVENT; + phase= "i"; + if (!physical_time_only) { + timestamp = elapsed_logical_time; + } + thread_id = reactor_index; + break; + case user_value: + pid = PID_FOR_USER_EVENT; + phase= "C"; + if (!physical_time_only) { + timestamp = elapsed_logical_time; + } + thread_id = reactor_index; + free(args); + asprintf(&args, "{\"value\": %lld}", trace[i].extra_delay); + break; + case worker_wait_starts: + pid = PID_FOR_WORKER_WAIT; + phase = "B"; + break; + case worker_wait_ends: + pid = PID_FOR_WORKER_WAIT; + phase = "E"; + break; + case scheduler_advancing_time_starts: + pid = PID_FOR_WORKER_ADVANCING_TIME; + phase = "B"; + break; + case scheduler_advancing_time_ends: + pid = PID_FOR_WORKER_ADVANCING_TIME; + phase = "E"; + break; + // Static scheduler + case static_scheduler_ADDI_starts: + case static_scheduler_ADV_starts: + case static_scheduler_ADV2_starts: + case static_scheduler_BIT_starts: + case static_scheduler_DU_starts: + case static_scheduler_EIT_starts: + case static_scheduler_EXE_starts: + case static_scheduler_JMP_starts: + case static_scheduler_SAC_starts: + case static_scheduler_STP_starts: + case static_scheduler_WU_starts: + phase = "B"; + pid = 0; // Process 0 will be named "Execution" + break; + case static_scheduler_ADDI_ends: + case static_scheduler_ADV_ends: + case static_scheduler_ADV2_ends: + case static_scheduler_BIT_ends: + case static_scheduler_DU_ends: + case static_scheduler_EIT_ends: + case static_scheduler_EXE_ends: + case static_scheduler_JMP_ends: + case static_scheduler_SAC_ends: + case static_scheduler_STP_ends: + case static_scheduler_WU_ends: + phase = "E"; + pid = 0; // Process 0 will be named "Execution" + break; + default: + fprintf(stderr, "WARNING: Unrecognized event type %d: %s\n", + trace[i].event_type, trace_event_names[trace[i].event_type]); + pid = PID_FOR_UNKNOWN_EVENT; + phase = "i"; + } + fprintf(output_file, "{" + "\"name\": \"%s\", " // name is the reactor or trigger name. + "\"cat\": \"%s\", " // category is the type of event. + "\"ph\": \"%s\", " // phase is "B" (begin), "E" (end), or "X" (complete). + "\"tid\": %d, " // thread ID. + "\"pid\": %d, " // process ID is required. + "\"ts\": %lld, " // timestamp in microseconds + "\"args\": %s" // additional arguments from above. + "},\n", + name, + trace_event_names[trace[i].event_type], + phase, + thread_id, + pid, + timestamp, + args + ); + free(args); + + if (trace[i].src_id > max_thread_id) { + max_thread_id = trace[i].src_id; + } + // If the event is reaction_starts and physical_time_only is not set, + // then also generate an instantaneous + // event to be shown in the reactor's section, along with timers and actions. + if (trace[i].event_type == reaction_starts && !physical_time_only) { + phase = "i"; + pid = reactor_index + 1; + reaction_name = (char*)malloc(4); + char name[13]; + snprintf(name, 13, "reaction %d", trace[i].dst_id); + + // NOTE: If the reactor has more than 1024 timers and actions, then + // there will be a collision of thread IDs here. + thread_id = 1024 + trace[i].dst_id; + if (trace[i].dst_id > max_reaction_number) { + max_reaction_number = trace[i].dst_id; + } + + fprintf(output_file, "{" + "\"name\": \"%s\", " // name is the reactor or trigger name. + "\"cat\": \"%s\", " // category is the type of event. + "\"ph\": \"%s\", " // phase is "B" (begin), "E" (end), or "X" (complete). + "\"tid\": %d, " // thread ID. + "\"pid\": %d, " // process ID is required. + "\"ts\": %lld, " // timestamp in microseconds + "\"args\": {" + "\"microstep\": %d, " // microstep. + "\"physical time\": %lld" // physical time. + "}},\n", + name, + "Reaction", + phase, + thread_id, + pid, + elapsed_logical_time, + trace[i].microstep, + elapsed_physical_time + ); + } + } + return trace_length; +} + +/** + * Write metadata events, which provide names in the renderer. + * @param output_file An open output .json file. + */ +void write_metadata_events(FILE* output_file) { + // Thread 0 is the main thread. + fprintf(output_file, "{" + "\"name\": \"thread_name\", " + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": 0, " + "\"tid\": 0, " + "\"args\": {" + "\"name\": \"Main thread\"" + "}},\n" + ); + + // Name the worker threads. + for (int i = 1; i <= max_thread_id; i++) { + fprintf(output_file, "{" + "\"name\": \"thread_name\", " + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": 0, " + "\"tid\": %d, " + "\"args\": {" + "\"name\": \"Worker %d\"" + "}},\n", + i, i + ); + fprintf(output_file, "{" + "\"name\": \"thread_name\", " + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": %d, " + "\"tid\": %d, " + "\"args\": {" + "\"name\": \"Worker %d\"" + "}},\n", + PID_FOR_WORKER_WAIT, i, i + ); + fprintf(output_file, "{" + "\"name\": \"thread_name\", " + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": %d, " + "\"tid\": %d, " + "\"args\": {" + "\"name\": \"Worker %d\"" + "}},\n", + PID_FOR_WORKER_ADVANCING_TIME, i, i + ); + } + + // Name reactions for each reactor. + for (int reactor_index = 1; reactor_index <= object_table_size; reactor_index++) { + for (int reaction_number = 0; reaction_number <= max_reaction_number; reaction_number++) { + fprintf(output_file, "{" + "\"name\": \"thread_name\", " + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": %d, " + "\"tid\": %d, " + "\"args\": {" + "\"name\": \"Reaction %d\"" + "}},\n", + reactor_index, reaction_number + 1024, reaction_number + ); + } + } + + // Write the reactor names for the logical timelines. + for (int i = 0; i < object_table_size; i++) { + if (object_table[i].type == trace_trigger) { + // We need the reactor index (not the name) to set the pid. + int reactor_index; + get_object_description(object_table[i].pointer, &reactor_index); + fprintf(output_file, "{" + "\"name\": \"thread_name\", " // metadata for thread name. + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": %d, " // the "process" to identify by reactor. + "\"tid\": %d," // The "thread" to label with action or timer name. + "\"args\": {" + "\"name\": \"Trigger %s\"" + "}},\n", + reactor_index + 1, // Offset of 1 prevents collision with Execution. + i, + object_table[i].description); + } else if (object_table[i].type == trace_reactor) { + fprintf(output_file, "{" + "\"name\": \"process_name\", " // metadata for process name. + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": %d, " // the "process" to label as reactor. + "\"args\": {" + "\"name\": \"Reactor %s reactions, actions, and timers in logical time\"" + "}},\n", + i + 1, // Offset of 1 prevents collision with Execution. + object_table[i].description); + } else if (object_table[i].type == trace_user) { + fprintf(output_file, "{" + "\"name\": \"thread_name\", " // metadata for thread name. + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": %d, " // the "process" to label as reactor. + "\"tid\": %d," // The "thread" to label with action or timer name. + "\"args\": {" + "\"name\": \"%s\"" + "}},\n", + PID_FOR_USER_EVENT, + i, // This is the index in the object table. + object_table[i].description); + } + } + // Name the "process" for "Execution" + fprintf(output_file, "{" + "\"name\": \"process_name\", " // metadata for process name. + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": 0, " // the "process" to label "Execution". + "\"args\": {" + "\"name\": \"Execution of %s\"" + "}},\n", + top_level); + // Name the "process" for "Worker Waiting" if the PID is not the main execution one. + if (PID_FOR_WORKER_WAIT > 0) { + fprintf(output_file, "{" + "\"name\": \"process_name\", " // metadata for process name. + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": %d, " // the "process" to label "Workers waiting for reaction queue". + "\"args\": {" + "\"name\": \"Workers waiting for reaction queue\"" + "}},\n", + PID_FOR_WORKER_WAIT); + } + // Name the "process" for "Worker advancing time" if the PID is not the main execution one. + if (PID_FOR_WORKER_ADVANCING_TIME > 0) { + fprintf(output_file, "{" + "\"name\": \"process_name\", " // metadata for process name. + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": %d, " // the "process" to label "Workers waiting for reaction queue". + "\"args\": {" + "\"name\": \"Workers advancing time\"" + "}},\n", + PID_FOR_WORKER_ADVANCING_TIME); + } + // Name the "process" for "User Events" + // Last metadata entry lacks a comma. + fprintf(output_file, "{" + "\"name\": \"process_name\", " // metadata for process name. + "\"ph\": \"M\", " // mark as metadata. + "\"pid\": %d, " // the "process" to label "User events". + "\"args\": {" + "\"name\": \"User events in %s, shown in physical time:\"" + "}}\n", + PID_FOR_USER_EVENT, top_level); +} + +int main(int argc, char* argv[]) { + char* filename = NULL; + for (int i = 1; i < argc; i++) { + if (strncmp(argv[i], "-p", 2) == 0 || strncmp(argv[i], "--physical", 10) == 0) { + physical_time_only = true; + } else if (argv[i][0] == '-') { + usage(); + return(1); + } else { + filename = argv[i]; + } + } + if (filename == NULL) { + usage(); + exit(0); + } + + // Open the trace file. + trace_file = open_file(filename, "r"); + + // Construct the name of the csv output file and open it. + char* root = root_name(filename); + char json_filename[strlen(root) + 6]; + strcpy(json_filename, root); + strcat(json_filename, ".json"); + output_file = open_file(json_filename, "w"); + + if (read_header(trace_file) >= 0) { + // Write the opening bracket into the json file. + fprintf(output_file, "{ \"traceEvents\": [\n"); + while (read_and_write_trace(trace_file, output_file) != 0) {}; + write_metadata_events(output_file); + fprintf(output_file, "]}\n"); + } +}