diff --git a/core/src/main/java/org/lflang/AttributeUtils.java b/core/src/main/java/org/lflang/AttributeUtils.java index 79447cb440..83fab9666f 100644 --- a/core/src/main/java/org/lflang/AttributeUtils.java +++ b/core/src/main/java/org/lflang/AttributeUtils.java @@ -142,6 +142,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 +268,15 @@ 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 TimeValue getWCET(Reaction reaction) { + Time wcet = getAttributeTime(reaction, "wcet"); + if (wcet == null) return TimeValue.MAX_VALUE; + int value = wcet.getInterval(); + TimeUnit unit = TimeUnit.fromName(wcet.getUnit()); + return new TimeValue(value, unit); + } + /** 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/TimeTag.java b/core/src/main/java/org/lflang/TimeTag.java new file mode 100644 index 0000000000..086adf7905 --- /dev/null +++ b/core/src/main/java/org/lflang/TimeTag.java @@ -0,0 +1,58 @@ +package org.lflang; + +/** + * Class representing a logical time tag, which is a pair that consists of a timestamp and a + * microstep. + */ +public class TimeTag implements Comparable { + + public static final TimeTag ZERO = new TimeTag(TimeValue.ZERO, 0L); + public static final TimeTag FOREVER = new TimeTag(TimeValue.MAX_VALUE, Long.MAX_VALUE); + + public final TimeValue time; + public final Long microstep; + + /** Constructor */ + public TimeTag(TimeValue time, Long microstep) { + this.time = time; + this.microstep = microstep; + } + + /** Copy constructor */ + public TimeTag(TimeTag that) { + this.time = that.time; + this.microstep = that.microstep; + } + + /** + * Whether this time tag represents FOREVER, which is interpreted as the maximum TimeValue. + * + * @return True if the tag is FOREVER, false otherwise. + */ + public boolean isForever() { + return time.equals(TimeValue.MAX_VALUE); + } + + /** + * When comparing two time tags, first compare their time fields. If they are equal, compare their + * microsteps. + */ + @Override + public int compareTo(TimeTag t) { + if (this.time.compareTo(t.time) < 0) return -1; + else if (this.time.compareTo(t.time) > 0) return 1; + else return this.microstep.compareTo(t.microstep); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof TimeTag t && this.compareTo(t) == 0) return true; + return false; + } + + @Override + public String toString() { + return "( " + this.time + ", " + this.microstep + " )"; + } +} 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..9774686b5a 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/Event.java +++ b/core/src/main/java/org/lflang/analyses/statespace/Event.java @@ -1,14 +1,15 @@ package org.lflang.analyses.statespace; +import org.lflang.TimeTag; import org.lflang.generator.TriggerInstance; -/** A node in the state space diagram representing a step in the execution of an LF program. */ +/** A class representing a tagged signal, for analytical purposes */ public class Event implements Comparable { private final TriggerInstance trigger; - private Tag tag; + private TimeTag tag; - public Event(TriggerInstance trigger, Tag tag) { + public Event(TriggerInstance trigger, TimeTag tag) { this.trigger = trigger; this.tag = tag; } @@ -41,7 +42,7 @@ public String toString() { return "(" + trigger.getFullName() + ", " + tag + ")"; } - public Tag getTag() { + public TimeTag getTag() { return tag; } 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..f4530e6134 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/EventQueue.java +++ b/core/src/main/java/org/lflang/analyses/statespace/EventQueue.java @@ -2,10 +2,7 @@ import java.util.PriorityQueue; -/** - * 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. - */ +/** An event queue for analyzing the logical behavior of an LF program */ 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..ef906a3c60 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/StateInfo.java +++ b/core/src/main/java/org/lflang/analyses/statespace/StateInfo.java @@ -2,23 +2,29 @@ import java.util.ArrayList; import java.util.HashMap; +import org.lflang.TimeTag; /** A class that represents information in a step in a counterexample trace */ public class StateInfo { public ArrayList reactions = new ArrayList<>(); - public Tag tag; + public TimeTag tag; public HashMap variables = new HashMap<>(); public HashMap triggers = new HashMap<>(); public HashMap scheduled = new HashMap<>(); public HashMap payloads = new HashMap<>(); public void display() { - System.out.println("reactions: " + reactions); - System.out.println("tag: " + tag); - System.out.println("variables: " + variables); - System.out.println("triggers: " + triggers); - System.out.println("scheduled: " + scheduled); - System.out.println("payloads: " + payloads); + System.out.println( + String.join( + "\n", + "/**************************/", + "reactions: " + reactions, + "tag: " + tag, + "variables: " + variables, + "triggers: " + triggers, + "scheduled: " + scheduled, + "payloads: " + payloads, + "/**************************/")); } } 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..398bd1ed5c 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceDiagram.java +++ b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceDiagram.java @@ -1,5 +1,7 @@ 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; @@ -7,8 +9,13 @@ import org.lflang.generator.CodeBuilder; import org.lflang.generator.ReactionInstance; import org.lflang.graph.DirectedGraph; +import org.lflang.pretvm.ExecutionPhase; -/** A directed graph representing the state space of an LF program. */ +/** + * A directed graph representing the state space of an LF program. + * + * @author Shaokai J. Lin + */ public class StateSpaceDiagram extends DirectedGraph { /** The first node of the state space diagram. */ @@ -29,13 +36,19 @@ 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 ExecutionPhase phase; /** 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. */ @@ -56,7 +69,7 @@ public StateSpaceNode getDownstreamNode(StateSpaceNode node) { public void display() { System.out.println("*************************************************"); System.out.println("* Pretty printing worst-case state space diagram:"); - long timestamp; + TimeValue time; StateSpaceNode node = this.head; if (node == null) { System.out.println("* EMPTY"); @@ -68,14 +81,15 @@ public void display() { node.display(); // Store the tag of the prior step. - timestamp = node.getTag().timestamp; + time = node.getTag().time; // Assume a unique next state. node = getDownstreamNode(node); // Compute time difference if (node != null) { - TimeValue tsDiff = TimeValue.fromNanoSeconds(node.getTag().timestamp - timestamp); + TimeValue tsDiff = + TimeValue.fromNanoSeconds(node.getTag().time.toNanoSeconds() - time.toNanoSeconds()); System.out.println("* => Advance time by " + tsDiff); } } @@ -87,7 +101,8 @@ public void display() { if (this.loopNode != null) { // Compute time difference TimeValue tsDiff = - TimeValue.fromNanoSeconds(loopNodeNext.getTag().timestamp - tail.getTag().timestamp); + TimeValue.fromNanoSeconds( + loopNodeNext.getTag().time.toNanoSeconds() - tail.getTag().time.toNanoSeconds()); System.out.println("* => Advance time by " + tsDiff); System.out.println("* Goes back to loop node: state " + this.loopNode.getIndex()); @@ -107,7 +122,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;"); @@ -173,7 +188,8 @@ public CodeBuilder generateDot() { StateSpaceNode next = getDownstreamNode(this.head); while (current != null && next != null && current != this.tail) { TimeValue tsDiff = - TimeValue.fromNanoSeconds(next.getTag().timestamp - current.getTag().timestamp); + TimeValue.fromNanoSeconds( + next.getTag().time.toNanoSeconds() - current.getTag().time.toNanoSeconds()); dot.pr( "S" + current.getIndex() @@ -192,8 +208,9 @@ public CodeBuilder generateDot() { if (loopNode != null) { TimeValue tsDiff = - TimeValue.fromNanoSeconds(loopNodeNext.getTag().timestamp - tail.getTag().timestamp); - TimeValue period = TimeValue.fromNanoSeconds(loopPeriod); + TimeValue.fromNanoSeconds( + loopNodeNext.getTag().time.toNanoSeconds() - tail.getTag().time.toNanoSeconds()); + TimeValue period = TimeValue.fromNanoSeconds(hyperperiod); dot.pr( "S" + current.getIndex() @@ -216,4 +233,24 @@ public CodeBuilder generateDot() { } return this.dot; } + + public void generateDotFile(Path dir, String filename) { + try { + Path path = dir.resolve(filename); + CodeBuilder dot = generateDot(); + dot.writeToFile(path.toString()); + } 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); + } } 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..94a6171029 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceExplorer.java +++ b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceExplorer.java @@ -1,11 +1,14 @@ package org.lflang.analyses.statespace; +import java.nio.file.Path; 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.TimeTag; 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 +18,82 @@ 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.pretvm.ExecutionPhase; +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 J. 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). - */ - 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; - } - - /** 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))); - } - - // Recursion - for (var child : reactor.children) { - addInitialEvents(child); - } - } - /** * 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); - - Tag previousTag = null; // Tag in the previous loop ITERATION - Tag currentTag = null; // Tag in the current loop ITERATION + public static StateSpaceDiagram explore( + ReactorInstance main, TimeTag horizon, ExecutionPhase phase, TargetConfig targetConfig) { + if (phase != ExecutionPhase.INIT_AND_PERIODIC + && phase != ExecutionPhase.SHUTDOWN_TIMEOUT + && phase != ExecutionPhase.SHUTDOWN_STARVATION) + throw new RuntimeException("Unsupported phase detected in the explorer."); + + // Variable initilizations + StateSpaceDiagram diagram = new StateSpaceDiagram(); + diagram.phase = phase; + EventQueue eventQ = new EventQueue(); + TimeTag previousTag = null; // TimeTag in the previous loop ITERATION + TimeTag currentTag = null; // TimeTag 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(addInitialEvents(main, phase, targetConfig)); + + // 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. @@ -205,25 +123,35 @@ public void explore(Tag horizon, boolean findLoop) { // This makes sure that the granularity of nodes is // at the timestamp-level, so that we don't have to // worry about microsteps. - else if (previousTag != null && currentTag.timestamp > previousTag.timestamp) { + else if (previousTag != null && currentTag.compareTo(previousTag) > 0) { + // Check if we are in the SHUTDOWN_TIMEOUT mode, + // if so, stop the loop immediately, because TIMEOUT is the last tag. + if (phase == ExecutionPhase.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().time.toNanoSeconds() + - diagram.loopNode.getTag().time.toNanoSeconds(); + 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 +159,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. @@ -266,7 +192,7 @@ else if (previousTag != null && currentTag.timestamp > previousTag.timestamp) { } // Timestamp does not advance because we are processing // connections with zero delay. - else if (previousTag != null && currentTag.timestamp == previousTag.timestamp) { + else if (previousTag != null && currentTag.equals(previousTag)) { // Add reactions explored in the current loop iteration // to the existing state space node. currentNode.getReactionsInvoked().addAll(reactionsTemp); @@ -287,7 +213,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.isForever() && currentTag.compareTo(horizon) > 0) { stop = true; } } @@ -298,17 +227,357 @@ 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().compareTo(currentNode.getTag()) < 0)) { + 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, ExecutionPhase 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, + ExecutionPhase 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, TimeTag.ZERO)); + + // Add the initial timer firings, if exist. + for (TimerInstance timer : reactor.timers) { + events.add( + new Event( + timer, + new TimeTag(TimeValue.fromNanoSeconds(timer.getOffset().toNanoSeconds()), 0L))); + } + 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, TimeTag.ZERO)); + + // 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, TimeTag.ZERO)); + } + } + + // 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, TimeTag.ZERO)); + } + for (ActionInstance logicalAction : + reactor.actions.stream().filter(it -> !it.isPhysical()).toList()) { + events.add(new Event(logicalAction, TimeTag.ZERO)); + } + break; + } + case SHUTDOWN_STARVATION: + { + // Add the shutdown trigger, if exists. + var shutdown = reactor.getShutdownTrigger(); + if (shutdown != null) events.add(new Event(shutdown, TimeTag.ZERO)); + 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 static List popCurrentEvents(EventQueue eventQ, TimeTag 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 static 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 static List createNewEvents( + List currentEvents, Set reactions, TimeTag 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 TimeTag( + TimeValue.fromNanoSeconds( + e.getTag().time.toNanoSeconds() + timer.getPeriod().toNanoSeconds()), + 0L // A time advancement resets microstep to 0. + ))); + } + } + + // 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 TimeTag( + TimeValue.fromNanoSeconds(currentTag.time.toNanoSeconds() + delay), 0L)); + 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 TimeTag( + TimeValue.fromNanoSeconds(currentTag.time.toNanoSeconds() + min_delay), + microstep)); + newEvents.add(e); + } + } + } + + return newEvents; + } + + /** + * 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. + */ + public static List generateStateSpaceDiagrams( + ReactorInstance reactor, TargetConfig targetConfig, Path graphDir) { + + // Initialize variables + List SSDs; + + /* Initialization and Periodic phase */ + + // Generate a state space diagram for the initialization and periodic phase + // of an LF program. + StateSpaceDiagram stateSpaceInitAndPeriodic = + explore(reactor, TimeTag.ZERO, ExecutionPhase.INIT_AND_PERIODIC, targetConfig); + stateSpaceInitAndPeriodic.generateDotFile( + graphDir, "state_space_" + ExecutionPhase.INIT_AND_PERIODIC + ".dot"); + + // Split the graph into a list of diagrams. + List splittedDiagrams = + splitInitAndPeriodicDiagrams(stateSpaceInitAndPeriodic); + + // Convert the diagrams into fragments (i.e., having a notion of upstream & + // downstream and carrying object file) and add them to the fragments list. + SSDs = splittedDiagrams; + + // Checking abnomalies. + // FIXME: For some reason, the message reporter does not work here. + if (SSDs.size() == 0) { + throw new RuntimeException( + "No behavior found. The program is not schedulable. Please provide an initial trigger."); + } + if (SSDs.size() > 2) { + throw new RuntimeException( + "More than two fragments detected when splitting the initialization and periodic phase!"); + } + + /* Shutdown phase */ + + // Scenario 1: TIMEOUT + // Generate a state space diagram for the timeout scenario of the + // shutdown phase. + if (targetConfig.get(TimeOutProperty.INSTANCE) != null) { + StateSpaceDiagram stateSpaceShutdownTimeout = + explore(reactor, TimeTag.ZERO, ExecutionPhase.SHUTDOWN_TIMEOUT, targetConfig); + stateSpaceInitAndPeriodic.generateDotFile( + graphDir, "state_space_" + ExecutionPhase.SHUTDOWN_TIMEOUT + ".dot"); + SSDs.add(stateSpaceShutdownTimeout); + } + + // Scenario 2: STARVATION + // TODO: Generate a state space diagram for the starvation scenario of the + // shutdown phase. + + return SSDs; + } + + /** + * 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 = ExecutionPhase.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 = ExecutionPhase.PERIODIC; + diagrams.add(periodicPhase); + } + + return diagrams; } } 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..2e0e88930e 100644 --- a/core/src/main/java/org/lflang/analyses/statespace/StateSpaceNode.java +++ b/core/src/main/java/org/lflang/analyses/statespace/StateSpaceNode.java @@ -1,9 +1,9 @@ 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.TimeTag; import org.lflang.TimeValue; import org.lflang.generator.ReactionInstance; import org.lflang.generator.TriggerInstance; @@ -11,27 +11,32 @@ /** A node in the state space diagram representing a step in the execution of an LF program. */ public class StateSpaceNode { - private int index; // Set in StateSpaceDiagram.java - private Tag tag; - private TimeValue time; // Readable representation of tag.timestamp + private int index; // An integer ID for this node + private TimeTag tag; private Set reactionsInvoked; private ArrayList eventQcopy; // A snapshot of the eventQ represented as an ArrayList public StateSpaceNode( - Tag tag, Set reactionsInvoked, ArrayList eventQcopy) { + TimeTag tag, Set reactionsInvoked, ArrayList eventQcopy) { this.tag = tag; this.eventQcopy = eventQcopy; this.reactionsInvoked = reactionsInvoked; - this.time = TimeValue.fromNanoSeconds(tag.timestamp); + } + + /** Copy constructor */ + public StateSpaceNode(StateSpaceNode that) { + this.tag = new TimeTag(that.tag); + this.eventQcopy = new ArrayList<>(that.eventQcopy); + this.reactionsInvoked = new HashSet<>(that.reactionsInvoked); } /** Two methods for pretty printing */ public void display() { - System.out.println("(" + this.time + ", " + reactionsInvoked + ", " + eventQcopy + ")"); + System.out.println("(" + this.tag.time + ", " + reactionsInvoked + ", " + eventQcopy + ")"); } public String toString() { - return "(" + this.time + ", " + reactionsInvoked + ", " + eventQcopy + ")"; + return "(" + this.tag.time + ", " + reactionsInvoked + ", " + eventQcopy + ")"; } /** @@ -50,23 +55,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().time.toNanoSeconds() - this.tag.time.toNanoSeconds()) + .reduce(1, (a, b) -> 31 * a + b); + result = 31 * result + (int) timeDiffHash; return result; } @@ -79,12 +81,16 @@ public void setIndex(int i) { index = i; } - public Tag getTag() { + public TimeTag getTag() { return tag; } + public void setTag(TimeTag newTag) { + tag = newTag; + } + public TimeValue getTime() { - return time; + return tag.time; } public Set getReactionsInvoked() { diff --git a/core/src/main/java/org/lflang/analyses/statespace/Tag.java b/core/src/main/java/org/lflang/analyses/statespace/Tag.java deleted file mode 100644 index f62dde6c32..0000000000 --- a/core/src/main/java/org/lflang/analyses/statespace/Tag.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.lflang.analyses.statespace; - -import org.lflang.TimeValue; - -/** - * Class representing a logical time tag, which is a pair that consists of a timestamp (type long) - * and a microstep (type long). - */ -public class Tag implements Comparable { - - public final long timestamp; - public final long microstep; - public final boolean forever; // Whether the tag is FOREVER into the future. - - public Tag(long timestamp, long microstep, boolean forever) { - this.timestamp = timestamp; - this.microstep = microstep; - this.forever = forever; - } - - @Override - public int compareTo(Tag t) { - // If one tag is forever, and the other is not, - // then forever tag is later. If both tags are - // forever, then they are equal. - if (this.forever && !t.forever) return 1; - else if (!this.forever && t.forever) return -1; - else if (this.forever && t.forever) return 0; - - // Compare the timestamps if they are not equal. - if (this.timestamp != t.timestamp) { - if (this.timestamp > t.timestamp) return 1; - else if (this.timestamp < t.timestamp) return -1; - else return 0; - } - - // Otherwise, compare the microsteps. - if (this.microstep > t.microstep) return 1; - else if (this.microstep < t.microstep) return -1; - else return 0; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o instanceof Tag) { - Tag t = (Tag) o; - if (this.timestamp == t.timestamp - && this.microstep == t.microstep - && this.forever == t.forever) return true; - } - return false; - } - - @Override - public String toString() { - if (this.forever) return "(FOREVER, " + this.microstep + ")"; - else { - TimeValue time = TimeValue.fromNanoSeconds(this.timestamp); - return "(" + time + ", " + this.microstep + ")"; - } - } -} 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..1a36750bba 100644 --- a/core/src/main/java/org/lflang/analyses/uclid/UclidGenerator.java +++ b/core/src/main/java/org/lflang/analyses/uclid/UclidGenerator.java @@ -37,6 +37,7 @@ import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.eclipse.emf.ecore.resource.Resource; +import org.lflang.TimeTag; import org.lflang.TimeUnit; import org.lflang.TimeValue; import org.lflang.analyses.c.AstUtils; @@ -48,7 +49,6 @@ import org.lflang.analyses.statespace.StateSpaceDiagram; import org.lflang.analyses.statespace.StateSpaceExplorer; import org.lflang.analyses.statespace.StateSpaceNode; -import org.lflang.analyses.statespace.Tag; import org.lflang.ast.ASTUtils; import org.lflang.dsl.CLexer; import org.lflang.dsl.CParser; @@ -76,6 +76,7 @@ import org.lflang.lf.Connection; import org.lflang.lf.Expression; import org.lflang.lf.Time; +import org.lflang.pretvm.ExecutionPhase; import org.lflang.target.Target; import org.lflang.util.StringUtil; @@ -1611,11 +1612,12 @@ 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; + StateSpaceDiagram diagram = + StateSpaceExplorer.explore( + this.main, + new TimeTag(TimeValue.fromNanoSeconds(this.horizon), 0L), + ExecutionPhase.INIT_AND_PERIODIC, + targetConfig); diagram.display(); // Generate a dot file. @@ -1629,7 +1631,7 @@ private void computeCT() { } //// Compute CT - if (!explorer.loopFound) { + if (!diagram.isCyclic()) { if (this.logicalTimeBased) this.CT = diagram.nodeCount(); else { // FIXME: This could be much more efficient with @@ -1647,20 +1649,21 @@ private void computeCT() { else { // Subtract the non-periodic logical time // interval from the total horizon. - long horizonRemained = Math.subtractExact(this.horizon, diagram.loopNode.getTag().timestamp); + long horizonRemained = + Math.subtractExact(this.horizon, diagram.loopNode.getTag().time.toNanoSeconds()); // 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..2d9ddc3c81 100644 --- a/core/src/main/java/org/lflang/analyses/uclid/UclidRunner.java +++ b/core/src/main/java/org/lflang/analyses/uclid/UclidRunner.java @@ -14,8 +14,9 @@ import org.json.JSONException; import org.json.JSONObject; import org.lflang.MessageReporter; +import org.lflang.TimeTag; +import org.lflang.TimeValue; import org.lflang.analyses.statespace.StateInfo; -import org.lflang.analyses.statespace.Tag; import org.lflang.generator.GeneratorCommandFactory; import org.lflang.util.LFCommand; @@ -98,7 +99,8 @@ public StateInfo parseStateInfo(String smtStr) { // The rest falls into group 2. else tagStr = m.group(2).strip(); String[] tag = tagStr.split("\\s+"); - info.tag = new Tag(Long.parseLong(tag[0]), Long.parseLong(tag[1]), false); + info.tag = + new TimeTag(TimeValue.fromNanoSeconds(Long.parseLong(tag[0])), Long.parseLong(tag[1])); // Variables // Currently all integers. diff --git a/core/src/main/java/org/lflang/ast/ASTUtils.java b/core/src/main/java/org/lflang/ast/ASTUtils.java index ef66d24cd1..563c63cd4d 100644 --- a/core/src/main/java/org/lflang/ast/ASTUtils.java +++ b/core/src/main/java/org/lflang/ast/ASTUtils.java @@ -65,6 +65,8 @@ import org.lflang.generator.CodeMap; import org.lflang.generator.InvalidSourceException; import org.lflang.generator.NamedInstance; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; import org.lflang.generator.ReactorInstance; import org.lflang.lf.Action; import org.lflang.lf.Assignment; @@ -514,11 +516,49 @@ public static List allModes(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_Modes()); } - public static List recursiveChildren(ReactorInstance r) { + /** + * A recursive method for returning all reactor instances + * + * @param r The reactor at which the search begins + * @return A list of reactors, including r and the recursively nested children of r + */ + public static List allReactorInstances(ReactorInstance r) { List ret = new ArrayList<>(); ret.add(r); for (var child : r.children) { - ret.addAll(recursiveChildren(child)); + ret.addAll(allReactorInstances(child)); + } + return ret; + } + + /** + * A recursive method for returning all reaction instances under a parent reactor + * + * @param r The reactor at which the search begins + * @return A list of reactions, including those within r and in the recursively nested children of + * r + */ + public static List allReactionInstances(ReactorInstance r) { + List ret = new ArrayList<>(); + ret.addAll(r.reactions); + for (var child : r.children) { + ret.addAll(allReactionInstances(child)); + } + return ret; + } + + /** + * A recursive method for returning all port instances under a parent reactor + * + * @param r The reactor at which the search begins + * @return A list of ports, including those within r and in the recursively nested children of r + */ + public static List allPortInstances(ReactorInstance r) { + List ret = new ArrayList<>(); + ret.addAll(r.inputs); + ret.addAll(r.outputs); + for (var child : r.children) { + ret.addAll(allPortInstances(child)); } return ret; } diff --git a/core/src/main/java/org/lflang/generator/PortInstance.java b/core/src/main/java/org/lflang/generator/PortInstance.java index d25d5b362b..a8be677410 100644 --- a/core/src/main/java/org/lflang/generator/PortInstance.java +++ b/core/src/main/java/org/lflang/generator/PortInstance.java @@ -146,10 +146,31 @@ public List eventualDestinations() { // Construct the full range for this port. RuntimeRange range = new RuntimeRange.Port(this); - eventualDestinationRanges = eventualDestinations(range); + eventualDestinationRanges = eventualDestinations(range, true); return eventualDestinationRanges; } + /** + * Similar to eventualDestinations(), this method returns a list of ranges of this port, where + * each range sends to a list of destination ports that receive data from the range of this port. + * Each destination port is annotated with the channel range on which it receives data. The ports + * listed are only ports that are sources for reactions, not relay ports that the data may go + * through on the way. + * + *

Different than eventualDestinations(), this method includes destinations with after delays + * in between. + */ + public List eventualDestinationsWithAfterDelays() { + if (eventualDestinationRangesWithAfterDelays != null) { + return eventualDestinationRangesWithAfterDelays; + } + + // Construct the full range for this port. + RuntimeRange range = new RuntimeRange.Port(this); + eventualDestinationRangesWithAfterDelays = eventualDestinations(range, false); + return eventualDestinationRangesWithAfterDelays; + } + /** * Return a list of ranges of ports that send data to this port. If this port is directly written * to by one more more reactions, then it is its own eventual source and only this port will be @@ -257,7 +278,8 @@ public int getLevelUpperBound(MixedRadixInt index) { * * @param srcRange The source range. */ - private static List eventualDestinations(RuntimeRange srcRange) { + private static List eventualDestinations( + RuntimeRange srcRange, boolean skipAfterDelays) { // Getting the destinations is more complex than getting the sources // because of multicast, where there is more than one connection statement @@ -291,9 +313,11 @@ 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; + if (skipAfterDelays) { + if (wSendRange.connection != null + && (wSendRange.connection.getDelay() != null || wSendRange.connection.isPhysical())) { + continue; + } } wSendRange = wSendRange.overlap(srcRange); @@ -303,7 +327,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, skipAfterDelays); int sendRangeStart = 0; for (SendRange dstSend : dstSendRanges) { queue.add(dstSend.newSendRange(wSendRange, sendRangeStart)); @@ -447,6 +471,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 including after delays. */ + private List eventualDestinationRangesWithAfterDelays; + /** 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..29dbcba332 100644 --- a/core/src/main/java/org/lflang/generator/ReactionInstance.java +++ b/core/src/main/java/org/lflang/generator/ReactionInstance.java @@ -25,11 +25,14 @@ package org.lflang.generator; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; 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; @@ -176,6 +179,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.wcet = AttributeUtils.getWCET(this.definition); } ////////////////////////////////////////////////////// @@ -210,6 +215,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 TimeValue wcet = TimeValue.MAX_VALUE; + ////////////////////////////////////////////////////// //// Public methods. @@ -293,6 +304,39 @@ public Set dependsOnReactions() { return dependsOnReactionsCache; } + /** + * Return the set of 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. + * + *

FIXME: How to get the accumulated delays? + */ + public Map downstreamReactions() { + Map downstreamReactions = new HashMap<>(); + // 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.eventualDestinationsWithAfterDelays()) { + Long delay = 0L; + if (senderRange.connection == null) 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.put(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/CActionGenerator.java b/core/src/main/java/org/lflang/generator/c/CActionGenerator.java index 03a859f558..10d82243b2 100644 --- a/core/src/main/java/org/lflang/generator/c/CActionGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CActionGenerator.java @@ -1,7 +1,5 @@ package org.lflang.generator.c; -import static org.lflang.generator.c.CGenerator.variableStructType; - import java.util.ArrayList; import java.util.List; import org.lflang.ast.ASTUtils; @@ -105,7 +103,7 @@ public static void generateDeclarations( TypeParameterizedReactor tpr, CodeBuilder body, CodeBuilder constructorCode) { for (Action action : ASTUtils.allActions(tpr.reactor())) { var actionName = action.getName(); - body.pr(CGenerator.variableStructType(action, tpr, false) + " _lf_" + actionName + ";"); + body.pr(CUtil.variableStructType(action, tpr, false) + " _lf_" + actionName + ";"); // Initialize the trigger pointer and the parent pointer in the action. constructorCode.pr( "self->_lf_" + actionName + "._base.trigger = &self->_lf__" + actionName + ";"); @@ -152,7 +150,7 @@ public static String generateAuxiliaryStruct( code.pr(valueDeclaration(tpr, action, target, types)); code.pr(federatedExtension.toString()); code.unindent(); - code.pr("} " + variableStructType(action, tpr, userFacing) + ";"); + code.pr("} " + CUtil.variableStructType(action, tpr, userFacing) + ";"); return code.toString(); } 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 2c21ae7b7f..8f16a124b0 100644 --- a/core/src/main/java/org/lflang/generator/c/CGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CGenerator.java @@ -82,7 +82,6 @@ import org.lflang.lf.ReactorDecl; import org.lflang.lf.StateVar; import org.lflang.lf.VarRef; -import org.lflang.lf.Variable; import org.lflang.lf.WidthSpec; import org.lflang.target.Target; import org.lflang.target.TargetConfig; @@ -437,6 +436,20 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { throw e; } + // Generate static schedule + // TO REMOVE LATER - JUST FOR TESTING. + // CScheduleGenerator schedGen = + // new CScheduleGenerator( + // this.fileConfig, + // this.targetConfig, + // this.messageReporter, + // this.main, + // ASTUtils.allReactorInstances(main), + // ASTUtils.allReactionInstances(main), + // ASTUtils.allPortInstances(main) + // ); + // schedGen.doGenerate(); + // 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. @@ -1206,14 +1219,18 @@ private void generateInteractingContainedReactors( // to be malloc'd at initialization. if (!ASTUtils.isMultiport(port)) { // Not a multiport. - body.pr(variableStructType(port, containedTpr, false) + " " + port.getName() + ";"); + body.pr( + CUtil.variableStructType(port, containedTpr, false) + " " + port.getName() + ";"); } else { // Is a multiport. // Memory will be malloc'd in initialization. body.pr( String.join( "\n", - variableStructType(port, containedTpr, false) + "** " + port.getName() + ";", + CUtil.variableStructType(port, containedTpr, false) + + "** " + + port.getName() + + ";", "int " + port.getName() + "_width;")); } } else { @@ -1222,7 +1239,8 @@ private void generateInteractingContainedReactors( // self struct of the container. if (!ASTUtils.isMultiport(port)) { // Not a multiport. - body.pr(variableStructType(port, containedTpr, false) + "* " + port.getName() + ";"); + body.pr( + CUtil.variableStructType(port, containedTpr, false) + "* " + port.getName() + ";"); } else { // Is a multiport. // Here, we will use an array of pointers. @@ -1230,7 +1248,10 @@ private void generateInteractingContainedReactors( body.pr( String.join( "\n", - variableStructType(port, containedTpr, false) + "** " + port.getName() + ";", + CUtil.variableStructType(port, containedTpr, false) + + "** " + + port.getName() + + ";", "int " + port.getName() + "_width;")); } body.pr("trigger_t " + port.getName() + "_trigger;"); @@ -1680,31 +1701,6 @@ public void processProtoFile(String filename) { } } - /** - * Construct a unique type for the struct of the specified typed variable (port or action) of the - * specified reactor class. This is required to be the same as the type name returned by {@link - * #variableStructType(TriggerInstance)}. - */ - public static String variableStructType( - Variable variable, TypeParameterizedReactor tpr, boolean userFacing) { - return (userFacing ? tpr.getName().toLowerCase() : CUtil.getName(tpr)) - + "_" - + variable.getName() - + "_t"; - } - - /** - * Construct a unique type for the struct of the specified instance (port or action). This is - * required to be the same as the type name returned by {@link #variableStructType(Variable, - * TypeParameterizedReactor, boolean)}. - * - * @param portOrAction The port or action instance. - * @return The name of the self struct. - */ - public static String variableStructType(TriggerInstance portOrAction) { - return CUtil.getName(portOrAction.getParent().tpr) + "_" + portOrAction.getName() + "_t"; - } - /** * If tracing is turned on, then generate code that records the full name of the specified reactor * instance in the trace table. If tracing is not turned on, do nothing. @@ -2161,6 +2157,6 @@ private void generateSelfStructs(ReactorInstance r) { } private Stream allTypeParameterizedReactors() { - return ASTUtils.recursiveChildren(main).stream().map(it -> it.tpr).distinct(); + return ASTUtils.allReactorInstances(main).stream().map(it -> it.tpr).distinct(); } } 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..1b699691ca 100644 --- a/core/src/main/java/org/lflang/generator/c/CPortGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CPortGenerator.java @@ -1,7 +1,5 @@ package org.lflang.generator.c; -import static org.lflang.generator.c.CGenerator.variableStructType; - import org.lflang.AttributeUtils; import org.lflang.MessageReporter; import org.lflang.ast.ASTUtils; @@ -76,7 +74,7 @@ public static String generateAuxiliaryStruct( var name = decl != null ? localPortName(tpr, decl, port.getName()) - : variableStructType(port, tpr, userFacing); + : CUtil.variableStructType(port, tpr, userFacing); code.pr("} " + name + ";"); return code.toString(); } @@ -101,8 +99,8 @@ public static String initializeInputMultiport(PortInstance input, String reactor "\n", portRefName + "_width = " + input.getWidth() + ";", "// Allocate memory for multiport inputs.", - portRefName + " = (" + variableStructType(input) + "**)lf_allocate(", - " " + input.getWidth() + ", sizeof(" + variableStructType(input) + "*),", + portRefName + " = (" + CUtil.variableStructType(input) + "**)lf_allocate(", + " " + input.getWidth() + ", sizeof(" + CUtil.variableStructType(input) + "*),", " &" + reactorSelfStruct + "->base.allocations); ", "// Set inputs by default to an always absent default input.", "for (int i = 0; i < " + input.getWidth() + "; i++) {", @@ -152,7 +150,7 @@ public static String initializeInputMultiport(PortInstance input, String reactor */ public static String initializeOutputMultiport(PortInstance output, String reactorSelfStruct) { var portRefName = CUtil.portRefName(output); - var portStructType = variableStructType(output); + var portStructType = CUtil.variableStructType(output); return output.isMultiport() ? String.join( "\n", @@ -221,10 +219,10 @@ private static void generateInputDeclarations( String.join( "\n", "// Multiport input array will be malloc'd later.", - variableStructType(input, tpr, false) + "** _lf_" + inputName + ";", + CUtil.variableStructType(input, tpr, false) + "** _lf_" + inputName + ";", "int _lf_" + inputName + "_width;", "// Default input (in case it does not get connected)", - variableStructType(input, tpr, false) + " _lf_default__" + inputName + ";", + CUtil.variableStructType(input, tpr, false) + " _lf_default__" + inputName + ";", "// Struct to support efficiently reading sparse inputs.", "lf_sparse_io_record_t* _lf_" + inputName + "__sparse;")); } else { @@ -232,11 +230,11 @@ private static void generateInputDeclarations( body.pr( String.join( "\n", - variableStructType(input, tpr, false) + "* _lf_" + inputName + ";", + CUtil.variableStructType(input, tpr, false) + "* _lf_" + inputName + ";", "// width of -2 indicates that it is not a multiport.", "int _lf_" + inputName + "_width;", "// Default input (in case it does not get connected)", - variableStructType(input, tpr, false) + " _lf_default__" + inputName + ";")); + CUtil.variableStructType(input, tpr, false) + " _lf_default__" + inputName + ";")); constructorCode.pr( String.join( @@ -264,7 +262,7 @@ private static void generateOutputDeclarations( String.join( "\n", "// Array of output ports.", - variableStructType(output, tpr, false) + "* _lf_" + outputName + ";", + CUtil.variableStructType(output, tpr, false) + "* _lf_" + outputName + ";", "int _lf_" + outputName + "_width;", "// An array of pointers to the individual ports. Useful", "// for the lf_set macros to work out-of-the-box for", @@ -272,12 +270,15 @@ private static void generateOutputDeclarations( "// value can be accessed via a -> operator (e.g.,foo[i]->value).", "// So we have to handle multiports specially here a construct that", "// array of pointers.", - variableStructType(output, tpr, false) + "** _lf_" + outputName + "_pointers;")); + CUtil.variableStructType(output, tpr, false) + + "** _lf_" + + outputName + + "_pointers;")); } else { body.pr( String.join( "\n", - variableStructType(output, tpr, false) + " _lf_" + outputName + ";", + CUtil.variableStructType(output, tpr, false) + " _lf_" + outputName + ";", "int _lf_" + outputName + "_width;")); } } 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..ea008ba0d8 100644 --- a/core/src/main/java/org/lflang/generator/c/CReactionGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CReactionGenerator.java @@ -166,7 +166,7 @@ public static String generateInitializationForReaction( // If it has already appeared as trigger, do not redefine it. if (!actionsAsTriggers.contains(effect.getVariable())) { reactionInitialization.pr( - CGenerator.variableStructType(variable, tpr, false) + CUtil.variableStructType(variable, tpr, false) + "* " + variable.getName() + " = &self->_lf_" @@ -382,8 +382,7 @@ private static void generateVariablesForSendingToContainedReactors( structs.put(definition, structBuilder); } String inputStructType = - CGenerator.variableStructType( - input, new TypeParameterizedReactor(definition, container), false); + CUtil.variableStructType(input, new TypeParameterizedReactor(definition, container), false); String defName = definition.getName(); String defWidth = generateWidthVariable(defName); String inputName = input.getName(); @@ -473,7 +472,7 @@ private static void generatePortVariablesInReaction( // port is an output of a contained reactor. Output output = (Output) port.getVariable(); String portStructType = - CGenerator.variableStructType( + CUtil.variableStructType( output, new TypeParameterizedReactor(port.getContainer(), tpr), false); CodeBuilder structBuilder = structs.get(port.getContainer()); @@ -548,7 +547,7 @@ private static void generatePortVariablesInReaction( */ private static String generateActionVariablesInReaction( Action action, TypeParameterizedReactor tpr, CTypes types) { - String structType = CGenerator.variableStructType(action, tpr, false); + String structType = CUtil.variableStructType(action, tpr, false); // If the action has a type, create variables for accessing the value. InferredType type = ASTUtils.getInferredType(action); // Pointer to the lf_token_t sent as the payload in the trigger. @@ -614,7 +613,7 @@ private static String generateActionVariablesInReaction( */ private static String generateInputVariablesInReaction( Input input, TypeParameterizedReactor tpr, CTypes types) { - String structType = CGenerator.variableStructType(input, tpr, false); + String structType = CUtil.variableStructType(input, tpr, false); InferredType inputType = ASTUtils.getInferredType(input); CodeBuilder builder = new CodeBuilder(); String inputName = input.getName(); @@ -782,8 +781,8 @@ public static String generateOutputVariablesInReaction( // the reactor containing the reaction. String outputStructType = (effect.getContainer() == null) - ? CGenerator.variableStructType(output, tpr, false) - : CGenerator.variableStructType( + ? CUtil.variableStructType(output, tpr, false) + : CUtil.variableStructType( output, new TypeParameterizedReactor(effect.getContainer(), tpr), false); if (!ASTUtils.isMultiport(output)) { // Output port is not a multiport. diff --git a/core/src/main/java/org/lflang/generator/c/CReactorHeaderFileGenerator.java b/core/src/main/java/org/lflang/generator/c/CReactorHeaderFileGenerator.java index 785db3a44c..1965a28511 100644 --- a/core/src/main/java/org/lflang/generator/c/CReactorHeaderFileGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CReactorHeaderFileGenerator.java @@ -216,7 +216,7 @@ private record PortVariable( String getType(boolean userFacing) { var typeName = container == null - ? CGenerator.variableStructType(tv, r, userFacing) + ? CUtil.variableStructType(tv, r, userFacing) : CPortGenerator.localPortName( new TypeParameterizedReactor(container, r), container.getReactorClass(), diff --git a/core/src/main/java/org/lflang/generator/c/CScheduleGenerator.java b/core/src/main/java/org/lflang/generator/c/CScheduleGenerator.java new file mode 100644 index 0000000000..4d33a46e98 --- /dev/null +++ b/core/src/main/java/org/lflang/generator/c/CScheduleGenerator.java @@ -0,0 +1,217 @@ +/************* + * 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.analyses.statespace.StateSpaceDiagram; +import org.lflang.analyses.statespace.StateSpaceExplorer; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.pretvm.InstructionGenerator; +import org.lflang.pretvm.PartialSchedule; +import org.lflang.pretvm.Registers; +import org.lflang.pretvm.dag.Dag; +import org.lflang.pretvm.dag.DagGenerator; +import org.lflang.pretvm.instruction.Instruction; +import org.lflang.pretvm.scheduler.LoadBalancedScheduler; +import org.lflang.pretvm.scheduler.StaticScheduler; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.CompileDefinitionsProperty; +import org.lflang.target.property.WorkersProperty; + +public class CScheduleGenerator { + + /** File config */ + protected final CFileConfig fileConfig; + + /** Target configuration */ + protected TargetConfig targetConfig; + + /** 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 ports */ + protected List ports; + + /** A path for storing graph */ + protected Path graphDir; + + /** PretVM registers */ + protected Registers registers; + + /** Flag indicating whether optimizers are used */ + protected boolean optimize = false; + + // Constructor + public CScheduleGenerator( + CFileConfig fileConfig, + TargetConfig targetConfig, + ReactorInstance main, + List reactors, + List reactions, + List ports) { + this.fileConfig = fileConfig; + this.targetConfig = targetConfig; + this.main = main; + this.workers = + targetConfig.get(WorkersProperty.INSTANCE) == 0 + ? 1 + : targetConfig.get(WorkersProperty.INSTANCE); + this.reactors = reactors; + this.reactions = reactions; + this.ports = ports; + this.registers = new Registers(workers); + this.optimize = false; + + // 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 static_schedule.c + public void doGenerate() { + // Generate a list of state space fragments that captures + // all the behavior of the LF program. + List SSDs = + StateSpaceExplorer.generateStateSpaceDiagrams(main, targetConfig, this.graphDir); + + // Instantiate object files with SSDs and connect them. + List schedules = new ArrayList<>(); + for (var ssd : SSDs) { + PartialSchedule ps = new PartialSchedule(); + ps.setDiagram(ssd); + schedules.add(ps); + } + PartialSchedule.link(schedules, registers); + + // 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.ports, + this.registers); + + // For each partial schedule, generate a DAG, perform DAG scheduling (mapping tasks + // to workers), and generate instructions for each worker. + for (int i = 0; i < schedules.size(); i++) { + // Get the partial schedule. + PartialSchedule schedule = schedules.get(i); + + // Generate a raw DAG from the partial schedule's state space diagram. + Dag dag = dagGenerator.generateDag(schedule.getDiagram()); + + // Validate the generated raw DAG. + if (!dag.isValid()) throw new RuntimeException("The generated DAG is invalid: " + dag); + + // Generate a dot file. + Path dagRawDot = graphDir.resolve("dag_raw" + "_" + i + ".dot"); + dag.generateDotFile(dagRawDot); + + // Prune redundant edges and generate a dot file. + // FIXME: To remove. + dag.removeRedundantEdges(); + Path file = graphDir.resolve("dag_pruned" + "_" + i + ".dot"); + dag.generateDotFile(file); + + // Generate a partitioned DAG based on the number of workers, + // and generate a dot graph. + Dag dagPartitioned = scheduler.partitionDag(dag, i, this.workers); + Path dagPartitionedDot = graphDir.resolve("dag_partitioned" + "_" + i + ".dot"); + dagPartitioned.generateDotFile(dagPartitionedDot); + + // Generate instructions (wrapped in an object file) from DAG partitions. + List> instructions = instGen.generateInstructions(dagPartitioned, schedule); + + // TODO: Check if deadlines could be violated. + + // Point the partitioned DAG and the instructions to the partial schedule. + schedule.setDag(dagPartitioned); + schedule.setInstructions(instructions); + } + + // Invoke the dag-based optimizer on each object file. + // It is invoked before linking because after linking, + // the DAG information is gone. + + // 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. + List> linkedInstructions = instGen.link(schedules, graphDir); + + // Invoke the peephole optimizer. + // FIXME: Should only apply to basic blocks! + + // Generate C code. + instGen.generateCode(linkedInstructions); + } + + /** Create a static scheduler. */ + private StaticScheduler createStaticScheduler() { + return 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..6aec998b6e 100644 --- a/core/src/main/java/org/lflang/generator/c/CTriggerObjectsGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CTriggerObjectsGenerator.java @@ -349,7 +349,7 @@ private static String connectPortToEventualDestinations(PortInstance src) { for (SendRange srcRange : src.eventualDestinations()) { for (RuntimeRange dstRange : srcRange.destinations) { var dst = dstRange.instance; - var destStructType = CGenerator.variableStructType(dst); + var destStructType = CUtil.variableStructType(dst); // NOTE: For federated execution, dst.getParent() should always be contained // by the currentFederate because an AST transformation removes connections @@ -1018,7 +1018,7 @@ private static String deferredReactionMemory( code.startScopedBlock(trigger.getParent()); var width = trigger.getWidth(); - var portStructType = CGenerator.variableStructType(trigger); + var portStructType = CUtil.variableStructType(trigger); code.pr( String.join( @@ -1064,7 +1064,7 @@ private static String deferredAllocationForEffectsOnInputs(ReactorInstance react code.pr("// A reaction writes to a multiport of a child. Allocate memory."); portsHandled.add(effect); code.startScopedBlock(effect.getParent()); - var portStructType = CGenerator.variableStructType(effect); + var portStructType = CUtil.variableStructType(effect); var effectRef = CUtil.portRefNestedName(effect); code.pr( String.join( 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..c4a51910c3 100644 --- a/core/src/main/java/org/lflang/generator/c/CUtil.java +++ b/core/src/main/java/org/lflang/generator/c/CUtil.java @@ -518,6 +518,31 @@ public static String selfType(ReactorInstance instance) { return selfType(instance.tpr); } + /** + * Construct a unique type for the struct of the specified typed variable (port or action) of the + * specified reactor class. This is required to be the same as the type name returned by {@link + * #variableStructType(TriggerInstance)}. + */ + public static String variableStructType( + Variable variable, TypeParameterizedReactor tpr, boolean userFacing) { + return (userFacing ? tpr.getName().toLowerCase() : CUtil.getName(tpr)) + + "_" + + variable.getName() + + "_t"; + } + + /** + * Construct a unique type for the struct of the specified instance (port or action). This is + * required to be the same as the type name returned by {@link #variableStructType(Variable, + * TypeParameterizedReactor, boolean)}. + * + * @param portOrAction The port or action instance. + * @return The name of the self struct. + */ + public static String variableStructType(TriggerInstance portOrAction) { + return CUtil.getName(portOrAction.getParent().tpr) + "_" + portOrAction.getName() + "_t"; + } + /** * Return a reference to the trigger_t struct of the specified trigger instance (input port or * action). This trigger_t struct is on the self struct. diff --git a/core/src/main/java/org/lflang/generator/c/CWatchdogGenerator.java b/core/src/main/java/org/lflang/generator/c/CWatchdogGenerator.java index e83ea122af..9ae14d2e68 100644 --- a/core/src/main/java/org/lflang/generator/c/CWatchdogGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CWatchdogGenerator.java @@ -235,7 +235,7 @@ private static String generateInitializationForWatchdog( */ private static String generateActionVariablesInHandler( Action action, TypeParameterizedReactor tpr) { - String structType = CGenerator.variableStructType(action, tpr, false); + String structType = CUtil.variableStructType(action, tpr, false); CodeBuilder builder = new CodeBuilder(); builder.pr( "// Expose the action struct as a local variable whose name matches the action name."); diff --git a/core/src/main/java/org/lflang/generator/python/PythonActionGenerator.java b/core/src/main/java/org/lflang/generator/python/PythonActionGenerator.java index 5270a4d69e..42e2c565d8 100644 --- a/core/src/main/java/org/lflang/generator/python/PythonActionGenerator.java +++ b/core/src/main/java/org/lflang/generator/python/PythonActionGenerator.java @@ -1,6 +1,6 @@ package org.lflang.generator.python; -import org.lflang.generator.c.CGenerator; +import org.lflang.generator.c.CUtil; import org.lflang.generator.c.TypeParameterizedReactor; import org.lflang.lf.Action; @@ -11,7 +11,7 @@ public static String generateAliasTypeDef( return "typedef " + genericActionType + " " - + CGenerator.variableStructType(action, tpr, false) + + CUtil.variableStructType(action, tpr, false) + ";"; } } diff --git a/core/src/main/java/org/lflang/generator/python/PythonPortGenerator.java b/core/src/main/java/org/lflang/generator/python/PythonPortGenerator.java index aa1e9a21c3..01d8dec757 100644 --- a/core/src/main/java/org/lflang/generator/python/PythonPortGenerator.java +++ b/core/src/main/java/org/lflang/generator/python/PythonPortGenerator.java @@ -5,7 +5,7 @@ import java.util.List; import org.lflang.ast.ASTUtils; import org.lflang.generator.CodeBuilder; -import org.lflang.generator.c.CGenerator; +import org.lflang.generator.c.CUtil; import org.lflang.generator.c.TypeParameterizedReactor; import org.lflang.lf.Action; import org.lflang.lf.Input; @@ -199,11 +199,7 @@ public static String generatePythonListForContainedBank( public static String generateAliasTypeDef( TypeParameterizedReactor tpr, Port port, boolean isTokenType, String genericPortType) { - return "typedef " - + genericPortType - + " " - + CGenerator.variableStructType(port, tpr, false) - + ";"; + return "typedef " + genericPortType + " " + CUtil.variableStructType(port, tpr, false) + ";"; } private static String generateConvertCPortToPy(String port) { diff --git a/core/src/main/java/org/lflang/pretvm/ExecutionPhase.java b/core/src/main/java/org/lflang/pretvm/ExecutionPhase.java new file mode 100644 index 0000000000..1d4fc07f8a --- /dev/null +++ b/core/src/main/java/org/lflang/pretvm/ExecutionPhase.java @@ -0,0 +1,12 @@ +package org.lflang.pretvm; + +public enum ExecutionPhase { + PREAMBLE, + INIT, + PERIODIC, + EPILOGUE, + SYNC_BLOCK, + INIT_AND_PERIODIC, + SHUTDOWN_TIMEOUT, + SHUTDOWN_STARVATION, +} diff --git a/core/src/main/java/org/lflang/pretvm/InstructionGenerator.java b/core/src/main/java/org/lflang/pretvm/InstructionGenerator.java new file mode 100644 index 0000000000..bc5996cd98 --- /dev/null +++ b/core/src/main/java/org/lflang/pretvm/InstructionGenerator.java @@ -0,0 +1,2297 @@ +package org.lflang.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 org.lflang.FileConfig; +import org.lflang.TimeValue; +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.CUtil; +import org.lflang.generator.c.TypeParameterizedReactor; +import org.lflang.lf.Connection; +import org.lflang.lf.Expression; +import org.lflang.lf.Output; +import org.lflang.pretvm.dag.Dag; +import org.lflang.pretvm.dag.DagNode; +import org.lflang.pretvm.dag.JobNode; +import org.lflang.pretvm.dag.TimeNode; +import org.lflang.pretvm.instruction.*; +import org.lflang.pretvm.register.Register; +import org.lflang.pretvm.register.ReturnAddr; +import org.lflang.pretvm.register.RuntimeVar; +import org.lflang.pretvm.register.WorkerRegister; +import org.lflang.target.TargetConfig; +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. + */ +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 port instances in the program */ + List ports; + + /** 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 ports, + Registers registers) { + this.fileConfig = fileConfig; + this.targetConfig = targetConfig; + this.workers = workers; + this.main = main; + this.reactors = reactors; + this.reactions = reactions; + this.ports = ports; + this.registers = registers; + } + + /** Topologically sort the dag nodes and assign release values to DAG nodes for counting locks. */ + public void assignReleaseIndices(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 instanceof JobNode currentJob) { + releaseValues[currentJob.getWorker()] += 1; + currentJob.setReleaseIndex(releaseValues[currentJob.getWorker()]); + } + } + } + + /** Traverse the DAG from head to tail using Khan's algorithm (topological sort). */ + public List> generateInstructions( + Dag dagParitioned, PartialSchedule partialSchedule) { + // 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. + assignReleaseIndices(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 instanceof JobNode) + .map(n -> (JobNode) n) + .toList(); + + if (current instanceof JobNode currentJob) { + // Find the worker assigned to the REACTION node, + // the reactor, and the reaction. + int worker = currentJob.getWorker(); + ReactionInstance reaction = currentJob.getReaction(); + ReactorInstance reactor = reaction.getParent(); + + // Current worker schedule + List currentSchedule = instructions.get(worker); + + // Get the nearest upstream sync node. + TimeNode associatedSyncNode = currentJob.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 (JobNode n : upstreamReactionNodes) { + int upstreamOwner = n.getWorker(); + if (upstreamOwner != worker) { + addInstructionForWorker( + instructions, + currentJob.getWorker(), + currentJob, + null, + new WU(registers.progressIndices.get(upstreamOwner), n.getReleaseIndex())); + } + } + + // 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. + JobNode lastSeen = reactionToLastSeenInvocationMap.get(reaction); + if (lastSeen != null && lastSeen.getWorker() != currentJob.getWorker()) { + addInstructionForWorker( + instructions, + currentJob.getWorker(), + currentJob, + null, + new WU( + registers.progressIndices.get(lastSeen.getWorker()), lastSeen.getReleaseIndex())); + if (currentJob + .getAssociatedSyncNode() + .getTime() + .isEarlierThan(lastSeen.getAssociatedSyncNode().getTime())) { + 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, currentJob); + + // WU Case 3: + // 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 (JobNode us : upstreamsFromConnection) { + if (us.getWorker() != currentJob.getWorker()) { + addInstructionForWorker( + instructions, + currentJob.getWorker(), + currentJob, + null, + new WU(registers.progressIndices.get(us.getWorker()), us.getReleaseIndex())); + } + } + } + + // 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 (i.e., reaches a new timestamp). + // The code should update the associated sync node + // in the reactorToLastSeenSyncNodeMap map. And if + // associatedSyncNode is not the head, generate time-advancement + // instructions (abbreviated as ADVI) and DU. + 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.getTime().toNanoSeconds() + - reactorToLastSeenSyncNodeMap.get(reactor).getTime().toNanoSeconds(); + } else { + relativeTimeIncrement = associatedSyncNode.getTime().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 start 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.start) { + + // 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)) { + EXE 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.getRuntimeVar(reactorTime); + var timeAdvInsts = + generateTimeAdvancementInstructions(reactor, reactorTimeReg, relativeTimeIncrement); + addInstructionSequenceForWorker(instructions, worker, current, null, timeAdvInsts); + + // 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))) { + // reactorTimeReg is already updated by time advancement instructions. + // Just delay until its recently updated value. + addInstructionForWorker( + instructions, worker, current, null, new DU(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); + EXE exeReaction = + new EXE( + registers.getRuntimeVar(reactionPointer), + registers.getRuntimeVar(reactorPointer), + reaction.index); + exeReaction.addLabel( + new Label( + "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 ADDI( + registers.temp0.get(worker), + registers.getRuntimeVar(reactorTimePointer), + reaction.declaredDeadline.maxDelay.toNanoSeconds()); + addiDeadlineTime.addLabel( + new Label( + "CALCULATE_DEADLINE_VIOLATION_TIME_FOR_" + + reaction.getFullNameWithJoiner("_") + + "_" + + generateShortUUID())); + + // Create EXE for updating the time register. + var exeUpdateTimeRegister = + new EXE( + registers.getRuntimeVar("update_temp1_to_current_time"), + registers.temp1.get(worker), + null); + + // Create deadline handling EXE + String deadlineHandlerPointer = + getFromEnvReactionDeadlineHandlerFunctionPointer(main, reaction); + Instruction exeDeadlineHandler = + new EXE( + registers.getRuntimeVar(deadlineHandlerPointer), + registers.getRuntimeVar(reactorPointer), + reaction.index); + exeDeadlineHandler.addLabel( + new Label( + "HANDLE_DEADLINE_VIOLATION_OF_" + + reaction.getFullNameWithJoiner("_") + + "_" + + generateShortUUID())); + + // Create BLT for checking deadline violation. + var bltDeadlineViolation = + new BLT( + 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 JAL(registers.zero, exeDeadlineHandler.getLabel(), 1L); + + // 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 ports. + // Check if the reaction has input port ports 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.getRuntimeVar(pqueueHeadTime); // RUNTIME_STRUCT + reg2 = registers.getRuntimeVar(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.getRuntimeVar(isPresentField); // RUNTIME_STRUCT + reg2 = registers.one; // Checking if is_present == 1 + } + Instruction reactionSequenceFront = reactionInvokingSequence.get(0); + Instruction beq = new BEQ(reg1, reg2, reactionSequenceFront.getLabel()); + beq.addLabel( + new Label( + "TEST_TRIGGER_" + port.getFullNameWithJoiner("_") + "_" + generateShortUUID())); + addInstructionForWorker(instructions, currentJob.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 JAL( + registers.zero, + reactionInvokingSequence.get(reactionInvokingSequence.size() - 1).getLabel(), + 1L)); + + // Add the reaction-invoking sequence to the instructions. + addInstructionSequenceForWorker( + instructions, currentJob.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 + // ports 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 progress index 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 ADDI( + registers.progressIndices.get(currentJob.getWorker()), + registers.progressIndices.get(currentJob.getWorker()), + 1L); + addInstructionForWorker(instructions, worker, current, null, addi); + + } else if (current instanceof TimeNode currentTime) { + if (current == dagParitioned.end) { + // 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 (currentTime.getTime() != TimeValue.MAX_VALUE) { + for (int worker = 0; worker < workers; worker++) { + // [Only Worker 0] Update the time offset increment register. + if (worker == 0) { + addInstructionForWorker( + instructions, + worker, + current, + null, + new ADDI( + registers.offsetInc, + registers.zero, + currentTime.getTime().toNanoSeconds())); + } + // Let all workers go to SYNC_BLOCK after finishing PREAMBLE. + addInstructionForWorker( + instructions, + worker, + current, + null, + new JAL( + registers.returnAddrs.get(worker), + Label.getExecutionPhaseLabel(ExecutionPhase.SYNC_BLOCK))); + // 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 start node, + // instead of the end node, handle DU. This potentially allows + // breaking the hyperperiod boundary. + // + // At this point, the global offset register has been + // updated in SYNC_BLOCK. + // + // We want to place this DU after the SYNC_BLOCK so that + // workers enters a new hyperperiod with almost zero lag. + // If this DU is placed before, then the SYNC_BLOCK will + // contribute the lag at the beginning of the hyperperiod. + if (!targetConfig.get(FastProperty.INSTANCE)) + addInstructionForWorker( + instructions, worker, current, null, new DU(registers.offset, 0L)); + } + } + } + } + } + // Add a label to the first instruction using the exploration phase + // (INIT, PERIODIC, SHUTDOWN_TIMEOUT, etc.). + for (int w = 0; w < workers; w++) { + // First, check if there is any instruction generated. + // A worker without any work assignment has an empty schedule. + // In this case, generate a dummy instruction: adding zero to a + // temp register. + // Without this dummy instruction, currently there will be + // compilation errors due to not having a place to put phase labels. + if (instructions.get(w).size() == 0) { + addInstructionForWorker( + instructions, + w, + dagParitioned.end, + null, + new ADD(registers.temp0.get(w), registers.temp0.get(w), registers.zero)); + } + // Then assign a label to the first instruction. + instructions.get(w).get(0).addLabel(Label.getExecutionPhaseLabel(partialSchedule.getPhase())); + } + + return instructions; + } + + /** + * Generate a sequence of instructions for advancing a reactor's logical time. First, the + * reactor's local time register needs to be incremented. Then, the `is_present` fields of the + * reactor's output ports need to be set to false. This function replaces two previously + * specialized instructions: ADV & ADVI. + * + *

This function is designed to have the same signature as ADV and ADVI. + * + * @param reactor The reactor instance to advance time and clear output ports for + * @param baseTimeReg The base time this reactor should advance to (either the reactor's current + * time register, or the time offset for the next hyperperiod) + * @param relativeTimeIncrement The time increment added on top of baseTimeReg + * @return A list of instructions for advancing reactor's local time + */ + private List generateTimeAdvancementInstructions( + ReactorInstance reactor, Register baseTimeReg, long relativeTimeIncrement) { + List timeAdvInsts = new ArrayList<>(); + + // Increment the reactor local time. + String reactorTimePointer = getFromEnvReactorTimePointer(main, reactor); + Register reactorTimeReg = registers.getRuntimeVar(reactorTimePointer); + var addiIncrementTime = new ADDI(reactorTimeReg, baseTimeReg, relativeTimeIncrement); + var uuid = generateShortUUID(); + addiIncrementTime.addLabel( + new Label("ADVANCE_TAG_FOR_" + reactor.getFullNameWithJoiner("_") + "_" + uuid)); + timeAdvInsts.add(addiIncrementTime); + + // Reset the is_present fields of all output ports of this reactor. + var outputs = ASTUtils.allOutputs(reactor.tpr.reactor()); + for (int i = 0; i < outputs.size(); i++) { + Output output = outputs.get(i); + String selfType = CUtil.selfType(reactor.tpr); + String portName = output.getName(); + String isPresentPointer = getPortIsPresentFieldPointer(main, reactor, selfType, portName); + Register portIsPresentReg = registers.getRuntimeVar(isPresentPointer); + var addiResetIsPresent = new ADD(portIsPresentReg, registers.zero, registers.zero); + timeAdvInsts.add(addiResetIsPresent); + } + return timeAdvInsts; + } + + /** + * 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.addDagNode(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.addDagNode(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.addDagNode(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 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 instList The list of instructions to be added + */ + private void addInstructionSequenceForWorker( + List> instructions, + int worker, + List nodes, + 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. + for (DagNode node : nodes) { + // Store the reference to the DAG node in the instruction. + inst.addDagNode(node); + } + } + } + + /** Generate C code from the instructions list. */ + public void generateCode(List> instructions) { + + // 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 = new HashSet<>(); + for (var tpr : tprs) headerNames.add(CUtil.getName(tpr)); + 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