diff --git a/example/C/src/Patterns/lib/SendersAndReceivers.lf b/example/C/src/Patterns/lib/SendersAndReceivers.lf index 111e963015..0d6c9b543c 100644 --- a/example/C/src/Patterns/lib/SendersAndReceivers.lf +++ b/example/C/src/Patterns/lib/SendersAndReceivers.lf @@ -20,16 +20,16 @@ reactor SendOnce { * * @param offset The starting time. * @param period The period. - * @param initial The first output. + * @param start The first output. * @param increment The increment between outputs */ reactor SendCount( offset:time(0), period:time(1 sec), - initial:int(0), + start:int(0), increment:int(1) ) { - state count:int(initial); + state count:int(start); output out:int; timer t(offset, period); reaction(t) -> out {= @@ -80,7 +80,7 @@ reactor SendOnceAndReceive { * * @param offset The time of the first output. * @param period The period of the outputs. - * @param initial The initial output value. + * @param start The initial output value. * @param increment The increment between outputs. * * @input in The input to report. @@ -98,7 +98,7 @@ reactor SendPeriodicallyAndReceive extends SendCount, Receive { * * @param offset The time of the first output. * @param period The period of the outputs. - * @param initial The initial output value. + * @param start The initial output value. * @param increment The increment between outputs. * * @input in The input to report. @@ -118,7 +118,7 @@ reactor ReceiveAndSendPeriodically extends Receive, SendCount { reactor SendPeriodicallyAndReceiveMultiport ( offset:time(0), period:time(1 sec), - initial:int(0), + start:int(0), increment:int(1), width:int(4) ) { @@ -127,7 +127,7 @@ reactor SendPeriodicallyAndReceiveMultiport ( timer t(offset, period); - state count:int(initial); + state count:int(start); reaction(t) -> out {= SET(out, self->count); diff --git a/example/Python/src/YOLOv5/README.md b/example/Python/src/YOLOv5/README.md index f63f107c8e..51182cf344 100644 --- a/example/Python/src/YOLOv5/README.md +++ b/example/Python/src/YOLOv5/README.md @@ -1,5 +1,12 @@ To run the example(s): +First, go to the PyTorch website and follow the instructions to install PyTorch: https://pytorch.org/get-started/locally/ + +IMPORTANT: If running with NVidia GPU, select the correct CUDA version on the installation page. + +Then, install other libraries and compile the LF file: + + python3 -m pip install -r requirements.txt lfc YOLOv5_Webcam.lf # (or lfc YOLOv5_Webcam_Timer.lf) diff --git a/example/Python/src/YOLOv5/YOLOv5_Webcam.lf b/example/Python/src/YOLOv5/YOLOv5_Webcam.lf index 57b39d276f..bd8687187c 100644 --- a/example/Python/src/YOLOv5/YOLOv5_Webcam.lf +++ b/example/Python/src/YOLOv5/YOLOv5_Webcam.lf @@ -6,6 +6,10 @@ */ target Python; +preamble {= + BILLION = 1_000_000_000 +=} + /** * Use OpenCV2 to read from the user webcam. * @@ -31,7 +35,7 @@ reactor WebCam(webcam_id(0)) { while running.is_set(): if ret is True: # If got a frame, schedule the physical action - frame_action.schedule(0, frame) + frame_action.schedule(0, (get_elapsed_physical_time(), frame)) ret, frame = self.stream.read() return None =} @@ -93,8 +97,9 @@ reactor DNN { model.set(self._model) =} reaction(frame) -> labels, label_coordinates {= + _, frame_data = frame.value # Convert the frame into a tuple - fr = [frame.value] + fr = [frame_data] # Run the model on the frame results = self._model(fr) # Extract the labels @@ -114,7 +119,8 @@ reactor Plotter(label_deadline(100 msec)) { input label_coordinates input model state _model # Keep the model - + state _prev_time(0); + preamble {= import cv2 =} @@ -147,9 +153,10 @@ reactor Plotter(label_deadline(100 msec)) { sys.stderr.write("Error: Expected all inputs to be present at the same time.\n") request_stop() + elapsed_time, frame_data = frame.value # Get how many labels we have n = len(labels.value) - x_shape, y_shape = frame.value.shape[1], frame.value.shape[0] + x_shape, y_shape = frame_data.shape[1], frame_data.shape[0] for i in range(n): row = label_coordinates.value[i] # If score is less than 0.2 we avoid making a prediction. @@ -162,19 +169,23 @@ reactor Plotter(label_deadline(100 msec)) { bgr = (0, 255, 0) # color of the box classes = self._model.names # Get the name of label index label_font = self.cv2.FONT_HERSHEY_SIMPLEX #Font for the label. - self.cv2.rectangle(frame.value, \ + self.cv2.rectangle(frame_data, \ (x1, y1), (x2, y2), \ bgr, 2) #Plot the boxes - self.cv2.putText(frame.value,\ + self.cv2.putText(frame_data,\ classes[int(labels.value[i])], \ (x1, y1), \ label_font, 0.9, bgr, 2) #Put a label over box. - - self.cv2.imshow("frame", frame.value) + + fps = int(1 / (elapsed_time / BILLION - self._prev_time / BILLION)) + self._prev_time = elapsed_time + self.cv2.putText(frame_data, str(fps), (7, 70), + self.cv2.FONT_HERSHEY_SIMPLEX, 3, + (100, 255, 0), 3, self.cv2.LINE_AA) + self.cv2.imshow("frame", frame_data) # press 'Q' if you want to exit if self.cv2.waitKey(1) & 0xFF == ord('q'): request_stop() - =} reaction(shutdown) {= diff --git a/example/Python/src/YOLOv5/YOLOv5_Webcam_Timer.lf b/example/Python/src/YOLOv5/YOLOv5_Webcam_Timer.lf index 181773504e..13c6e628d1 100644 --- a/example/Python/src/YOLOv5/YOLOv5_Webcam_Timer.lf +++ b/example/Python/src/YOLOv5/YOLOv5_Webcam_Timer.lf @@ -42,7 +42,7 @@ reactor WebCam { reaction(camera_tick) -> camera_frame {= ret, frame = self.stream.read() if ret is True: - camera_frame.set(frame) + camera_frame.set((get_elapsed_physical_time(), frame)) =} reaction(shutdown) {= diff --git a/example/Python/src/YOLOv5/requirements.txt b/example/Python/src/YOLOv5/requirements.txt index ab9df93827..28cf9e5b5c 100644 --- a/example/Python/src/YOLOv5/requirements.txt +++ b/example/Python/src/YOLOv5/requirements.txt @@ -1,4 +1,3 @@ -torch opencv-python pandas tqdm @@ -11,7 +10,6 @@ numpy>=1.18.5 Pillow>=7.1.2 PyYAML>=5.3.1 scipy>=1.4.1 -torchvision>=0.8.1 # Logging ------------------------------------- tensorboard>=2.4.1 diff --git a/experimental/C/src/ModalModels/BehaviorTrees/robohub_example_advanced.lf b/experimental/C/src/ModalModels/BehaviorTrees/robohub_example_advanced.lf new file mode 100644 index 0000000000..a2bb267677 --- /dev/null +++ b/experimental/C/src/ModalModels/BehaviorTrees/robohub_example_advanced.lf @@ -0,0 +1,141 @@ +/* + * Implements a modal LF version of the hierarchical statemachine + * for the behavior tree in presented in this article: + * https://robohub.org/introduction-to-behavior-trees/ + * + * Compared to the simple variant this uses modes more extensively, which + * results in the correct behavior. + * Moreover, modeling the sequence in Nominal as modal enables the potential + * use of a history transition that could allow modeling the continuation + * of the task squecnce at the point where it was left when the battery ran out. + */ +target C { +// logging: debug +} + +reactor GenericTask(name:string("")) { + output success:bool + output failure:bool + + initial mode Running { + // Just for testing + timer work(0, 250msec) + timer finish(1sec, 1sec) + + reaction(work) {= + printf("%s\n", self->name); + =} + + reaction(finish) -> success, Succeeded, failure, Failed {= + int r = rand() % 6; + if (r == 0) { + SET(failure, true); + SET_MODE(Failed); + } else { + SET(success, true); + SET_MODE(Succeeded); + } + =} + } + + mode Succeeded {} + mode Failed {} +} + +reactor NominalBehavior { + input BatteryOK:bool + + output success:bool + output failure:bool + + initial mode MoveToObj { + MoveToObjTask = new GenericTask(name="MoveToObj") + + MoveToObjTask.failure -> failure + + reaction(MoveToObjTask.success) -> CloseGrip {= + SET_MODE(CloseGrip); + =} + } + + mode CloseGrip { + CloseGripTask = new GenericTask(name="CloseGrip") + + CloseGripTask.failure -> failure + + reaction(CloseGripTask.success) -> MoveHome {= + SET_MODE(MoveHome); + =} + } + + mode MoveHome { + MoveHomeTask = new GenericTask(name="MoveHome") + + MoveHomeTask.failure -> failure + + reaction(MoveHomeTask.success) -> success {= + SET(success, true); + =} + } +} + +reactor Robot { + input BatteryOK:bool + + output success:bool + output failure:bool + + initial mode Nominal { + NominalBehavior = new NominalBehavior() + + NominalBehavior.success -> success + NominalBehavior.failure -> failure + + reaction(BatteryOK) -> Charging {= + if (!BatteryOK->value) { + SET_MODE(Charging); + printf("Battery empty\n"); + } + =} + } + + mode Charging { + GoCharge = new GenericTask(name="GoCharge") + + GoCharge.failure -> failure + + reaction(BatteryOK, GoCharge.success) -> Nominal {= + // Assumes simultaneous presence + if (BatteryOK->value && GoCharge.success->value) { + SET_MODE(Nominal); + printf("Battery charged\n"); + } + =} + } +} + +main reactor { + timer Battery(1sec, 1sec) + state battery_state:int(1) + + robot = new Robot() + + reaction(Battery) -> robot.BatteryOK {= + self->battery_state--; + SET(robot.BatteryOK, self->battery_state > 0); + if (self->battery_state <= 0) { + self->battery_state = 5; + } + =} + + reaction(robot.success) {= + printf("Total success\n"); + request_stop(); + =} + + reaction(robot.failure) {= + printf("Utter failure\n"); + request_stop(); + =} + +} \ No newline at end of file diff --git a/experimental/C/src/ModalModels/BehaviorTrees/robohub_example_simple.lf b/experimental/C/src/ModalModels/BehaviorTrees/robohub_example_simple.lf new file mode 100644 index 0000000000..e8937eb1e5 --- /dev/null +++ b/experimental/C/src/ModalModels/BehaviorTrees/robohub_example_simple.lf @@ -0,0 +1,116 @@ +/* + * Implements a modal LF version of the hierarchical statemachine + * for the behavior tree in presented in this article: + * https://robohub.org/introduction-to-behavior-trees/ + * + * It implements the core behavior sequence by chaining up reactors. + * However, this currently does not work correctly with modes because + * there is no support for reacting to entering a mode. + * Hence, when switching to Charging the task is not started and if + * switching back to Nominal the sequence would not restart. + */ +target C; + +reactor GenericTask(name:string("")) { + input start:bool + output success:bool + output failure:bool + + logical action continue_task + + reaction(start, continue_task) -> continue_task, success, failure {= + printf("%s\n", self->name); + int r = rand() % 10; + if (r == 0) { + SET(failure, true); + } else if (r >= 6) { + SET(success, true); + } else { + schedule(continue_task, MSEC(250)); + } + =} +} + +reactor Robot { + input start:bool + input BatteryOK:bool + output success:bool + output failure:bool + + initial mode Nominal { + MoveToObj = new GenericTask(name="MoveToObj") + CloseGrip = new GenericTask(name="CloseGrip") + MoveHome = new GenericTask(name="MoveHome") + + start -> MoveToObj.start // No resume after charging + MoveToObj.success -> CloseGrip.start + CloseGrip.success -> MoveHome.start + MoveHome.success -> success + + MoveToObj.failure -> failure + CloseGrip.failure -> failure + MoveHome.failure -> failure + + // Potential solution for resuming after charging +// reaction(entry) -> MoveToObj.start {= +// // PROBLEM!! +// SET(MoveToObj.start, true); +// =} + + reaction(BatteryOK) -> Charging {= + if (!BatteryOK->value) { + SET_MODE(Charging); + printf("Battery empty\n"); + } + =} + } + + mode Charging { + GoCharge = new GenericTask(name="GoCharge") + + GoCharge.failure -> failure + + // Potential solution for starting task when mode is entered because no start event is provided +// reaction(entry) -> GoCharge.start {= +// SET(GoCharge.start, true); +// =} + + reaction(BatteryOK, GoCharge.success) -> Nominal {= + // Assumes simultaneous presence + if (BatteryOK->value && GoCharge.success->value) { + SET_MODE(Nominal); + printf("Battery charged\n"); + } + =} + } +} + +main reactor { + timer Battery(1sec, 1sec) + state battery_state:int(1) + + robot = new Robot() + + reaction(startup) -> robot.start {= + SET(robot.start, true); + =} + + reaction(Battery) -> robot.BatteryOK {= + self->battery_state--; + SET(robot.BatteryOK, self->battery_state > 0); + if (self->battery_state <= 0) { + self->battery_state = 5; + } + =} + + reaction(robot.success) {= + printf("Total success\n"); + request_stop(); + =} + + reaction(robot.failure) {= + printf("Utter failure\n"); + request_stop(); + =} + +} \ No newline at end of file diff --git a/experimental/C/src/modal_models/Chrono/Chrono.lf b/experimental/C/src/ModalModels/Motivation/Chrono/Chrono.lf similarity index 100% rename from experimental/C/src/modal_models/Chrono/Chrono.lf rename to experimental/C/src/ModalModels/Motivation/Chrono/Chrono.lf diff --git a/experimental/C/src/modal_models/SineAvgMax/sine_max_avg.lf b/experimental/C/src/ModalModels/Motivation/SineAvgMax/sine_max_avg.lf similarity index 88% rename from experimental/C/src/modal_models/SineAvgMax/sine_max_avg.lf rename to experimental/C/src/ModalModels/Motivation/SineAvgMax/sine_max_avg.lf index e65cbea428..f5883c1fa0 100644 --- a/experimental/C/src/modal_models/SineAvgMax/sine_max_avg.lf +++ b/experimental/C/src/ModalModels/Motivation/SineAvgMax/sine_max_avg.lf @@ -36,7 +36,7 @@ reactor ModalModel(sample_size(10)) { state sample({=[None] * sample_size=}); state count(0); - state mode(0); # Only present w/o mode support + state _mode(0); # Only present w/o mode support // These actions only mimic the Ptolemy structure logical action processAVG @@ -47,7 +47,7 @@ reactor ModalModel(sample_size(10)) { /** @label Mode AVG: Collect */ reaction(data) -> processAVG {= - if self.mode == 0: # Only present w/o mode support + if self._mode == 0: # Only present w/o mode support self.sample[self.count] = data.value self.count += 1 if self.count == self.sample_size: @@ -57,11 +57,11 @@ reactor ModalModel(sample_size(10)) { /** @label Mode AVG: Process and Transition */ reaction(processAVG) -> out {= - if self.mode == 0: # Only present w/o mode support + if self._mode == 0: # Only present w/o mode support #print("Processing: ", self.sample) out.set(sum(self.sample) / self.sample_size) # Transition to MAX - self.mode = 1 # set_mode(MAX) + self._mode = 1 # set_mode(MAX) =} // } @@ -69,7 +69,7 @@ reactor ModalModel(sample_size(10)) { /** @label Mode MAX: Collect */ reaction(data) -> processMAX {= - if self.mode == 1: # Only present w/o mode support + if self._mode == 1: # Only present w/o mode support self.sample[self.count] = data.value self.count += 1 if self.count == self.sample_size: @@ -79,11 +79,11 @@ reactor ModalModel(sample_size(10)) { /** @label Mode MAX: Process and Transition */ reaction(processMAX) -> out {= - if self.mode == 1: # Only present w/o mode support + if self._mode == 1: # Only present w/o mode support #print("Processing: ", self.sample) out.set(max(self.sample)) # Transition to MAX - self.mode = 0 # set_mode(AVG) + self._mode = 0 # set_mode(AVG) =} // } } @@ -121,7 +121,7 @@ reactor Plotter { =} } -main reactor SineAvgMax { +main reactor { s = new Sinewave(sample_rate = 125 msec, frequency = 0.44) m = new ModalModel() p = new Plotter() diff --git a/experimental/C/src/modal_models/SineAvgMax/sine_max_avg_v2.lf b/experimental/C/src/ModalModels/Motivation/SineAvgMax/sine_max_avg_v2.lf similarity index 77% rename from experimental/C/src/modal_models/SineAvgMax/sine_max_avg_v2.lf rename to experimental/C/src/ModalModels/Motivation/SineAvgMax/sine_max_avg_v2.lf index 512775f11b..1e734ff01f 100644 --- a/experimental/C/src/modal_models/SineAvgMax/sine_max_avg_v2.lf +++ b/experimental/C/src/ModalModels/Motivation/SineAvgMax/sine_max_avg_v2.lf @@ -10,7 +10,7 @@ target Python{ threads: 0 }; -import Plotter, Sinewave from "sine_max_avg.lf" +import Plotter, Sinewave from "./sine_max_avg.lf" reactor ModalModel(sample_size(10)) { input data; @@ -19,12 +19,12 @@ reactor ModalModel(sample_size(10)) { state sample({=[None] * sample_size=}); state count(0); - state mode(0); # Only present w/o mode support + state _mode(0); # Only present w/o mode support logical action process reaction(data) -> process {= - if self.mode == 0: # Only present w/o mode support + if self._mode == 0: # Only present w/o mode support self.sample[self.count] = data.value self.count += 1 if self.count == self.sample_size: @@ -36,11 +36,11 @@ reactor ModalModel(sample_size(10)) { /** @label Mode AVG: Process and Transition */ reaction(process) -> out {= - if self.mode == 0: # Only present w/o mode support + if self._mode == 0: # Only present w/o mode support print("Processing: ", self.sample) out.set(sum(self.sample) / self.sample_size) # Transition to MAX - self.mode = 1 # set_mode(MAX) + self._mode = 1 # set_mode(MAX) =} // } @@ -48,16 +48,16 @@ reactor ModalModel(sample_size(10)) { /** @label Mode MAX: Process and Transition */ reaction(process) -> out {= - if self.mode == 1: # Only present w/o mode support + if self._mode == 1: # Only present w/o mode support print("Processing: ", self.sample) out.set(max(self.sample)) # Transition to MAX - self.mode = 0 # set_mode(AVG) + self._mode = 0 # set_mode(AVG) =} // } } -main reactor SineAvgMax { +main reactor { s = new Sinewave(sample_rate = 125 msec, frequency = 0.44) m = new ModalModel() p = new Plotter() diff --git a/experimental/C/src/ModalModels/ReflexGame/ModalReflexGame.lf b/experimental/C/src/ModalModels/ReflexGame/ModalReflexGame.lf new file mode 100644 index 0000000000..a307bc26f7 --- /dev/null +++ b/experimental/C/src/ModalModels/ReflexGame/ModalReflexGame.lf @@ -0,0 +1,150 @@ +/** + * This example illustrates the use of logical and physical actions, + * asynchronous external inputs, the use of startup and shutdown + * reactions, and the use of actions with values. + * + * The example is fashioned after an Esterel implementation given by + * Berry and Gonthier in "The ESTEREL synchronous programming language: + * design, semantics, implementation," Science of Computer Programming, + * 19(2) pp. 87-152, Nov. 1992, DOI: 10.1016/0167-6423(92)90005-V. + * + * Version that uses modes! + * + * @author Edward A. Lee + * @author Marten Lohstroh + * @author Alexander Schulz-Rosengarten + */ +target C { + threads: 1, + keepalive: true +}; + +/** + * Produce a counting sequence at random times with a minimum + * and maximum time between outputs specified as parameters. + * + * @param min_time The minimum time between outputs. + * @param max_time The maximum time between outputs. + */ +reactor RandomSource(min_time:time(2 sec), max_time:time(8 sec)) { + preamble {= + // Generate a random additional delay over the minimum. + // Assume millisecond precision is enough. + interval_t additional_time(interval_t min_time, interval_t max_time) { + int interval_in_msec = (max_time - min_time) / MSEC(1); + return (rand() % interval_in_msec) * MSEC(1); + } + =} + input another:int; + output out:int; + logical action prompt(min_time); + state count:int(0); + + reaction(startup) -> prompt {= + printf("***********************************************\n"); + printf("Watch for the prompt, then hit Return or Enter.\n"); + printf("Type Control-D (EOF) to quit.\n\n"); + + // Random number functions are part of stdlib.h, which is included by reactor.h. + // Set a seed for random number generation based on the current time. + srand(time(0)); + + // Schedule the first event. + schedule(prompt, additional_time(0, self->max_time - self->min_time)); + =} + reaction(prompt) -> out {= + self->count++; + printf("%d. Hit Return or Enter!", self->count); + fflush(stdout); + SET(out, self->count); + =} + reaction(another) -> prompt {= + // Schedule the next event. + schedule(prompt, additional_time(0, self->max_time - self->min_time)); + =} +} +/** + * Upon receiving a prompt, record the time of the prompt, + * then listen for user input. When the user hits return, + * then schedule a physical action that records the time + * of this event and then report the response time. + */ +reactor GetUserInput { + preamble {= + // Thread to read input characters until an EOF is received. + // Each time a newline is received, schedule a user_response action. + void* read_input(void* user_response) { + int c; + while(1) { + while((c = getchar()) != '\n') { + if (c == EOF) break; + } + schedule_copy(user_response, 0, &c, 1); + if (c == EOF) break; + } + return NULL; + } + =} + + physical action user_response:char; + state prompt_time:time(0); + state total_time_in_ms:int(0); + state count:int(0); + + input prompt:int; + output another:int; + + reaction(startup) -> user_response {= + // Start the thread that listens for Enter or Return. + lf_thread_t thread_id; + lf_thread_create(&thread_id, &read_input, user_response); + =} + + initial mode Preparing { + reaction(prompt) -> Waiting {= + self->prompt_time = get_logical_time(); + // Switch to mode waiting for user input + SET_MODE(Waiting); + =} + + reaction(user_response) {= + printf("YOU CHEATED!\n"); + request_stop(); + =} + } + mode Waiting { + reaction(user_response) -> another, Preparing {= + int time_in_ms = (get_logical_time() - self->prompt_time) / 1000000LL; + printf("Response time in milliseconds: %d\n", time_in_ms); + self->count++; + self->total_time_in_ms += time_in_ms; + // In the original the time was set to zero to encode the mode. This is no longer necessary. + // self->prompt_time = 0LL; + // Trigger another prompt. + SET(another, 42); + // Switch to mode waiting for next prompt + SET_MODE(Preparing); + =} + } + + reaction(user_response) {= + if (user_response->value == EOF) { + request_stop(); + return; + } + =} + + reaction(shutdown) {= + if (self->count > 0) { + printf("\n**** Average response time: %d.\n", self->total_time_in_ms/self->count); + } else { + printf("\n**** No attempts.\n"); + } + =} +} +main reactor { + p = new RandomSource(); + g = new GetUserInput(); + p.out -> g.prompt; + g.another -> p.another; +} \ No newline at end of file diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java index b1f3fbef70..05a5f59bfd 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java @@ -101,6 +101,7 @@ import org.lflang.diagram.synthesis.styles.ReactorFigureComponents; import org.lflang.diagram.synthesis.util.CycleVisualization; import org.lflang.diagram.synthesis.util.InterfaceDependenciesVisualization; +import org.lflang.diagram.synthesis.util.ModeDiagrams; import org.lflang.diagram.synthesis.util.NamedInstanceUtil; import org.lflang.diagram.synthesis.util.ReactorIcons; import org.lflang.diagram.synthesis.util.SynthesisErrorReporter; @@ -139,6 +140,7 @@ public class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { @Inject @Extension private InterfaceDependenciesVisualization _interfaceDependenciesVisualization; @Inject @Extension private FilterCycleAction _filterCycleAction; @Inject @Extension private ReactorIcons _reactorIcons; + @Inject @Extension private ModeDiagrams _modeDiagrams; // ------------------------------------------------------------------------- @@ -200,6 +202,8 @@ public List getDisplayedSynthesisOptions() { SHOW_ALL_REACTORS, MemorizingExpandCollapseAction.MEMORIZE_EXPANSION_STATES, CYCLE_DETECTION, + ModeDiagrams.SHOW_TRANSITION_LABELS, + ModeDiagrams.INITIALLY_COLLAPSE_MODES, SHOW_USER_LABELS, SHOW_HYPERLINKS, //LinguaFrancaSynthesisInterfaceDependencies.SHOW_INTERFACE_DEPENDENCIES, @@ -682,6 +686,7 @@ private Collection transformReactorNetwork( for (TimerInstance timer : reactorInstance.timers) { KNode node = associateWith(_kNodeExtensions.createNode(), timer.getDefinition()); NamedInstanceUtil.linkInstance(node, timer); + _utilityExtensions.setID(node, timer.uniqueID()); nodes.add(node); Iterables.addAll(nodes, createUserComments(timer.getDefinition(), node)); timerNodes.put(timer, node); @@ -693,6 +698,7 @@ private Collection transformReactorNetwork( int idx = reactorInstance.reactions.indexOf(reaction); KNode node = associateWith(_kNodeExtensions.createNode(), reaction.getDefinition()); NamedInstanceUtil.linkInstance(node, reaction); + _utilityExtensions.setID(node, reaction.uniqueID()); nodes.add(node); Iterables.addAll(nodes, createUserComments(reaction.getDefinition(), node)); reactionNodes.put(reaction, node); @@ -809,6 +815,7 @@ private Collection transformReactorNetwork( for (ActionInstance action : actions) { KNode node = associateWith(_kNodeExtensions.createNode(), action.getDefinition()); NamedInstanceUtil.linkInstance(node, action); + _utilityExtensions.setID(node, action.uniqueID()); nodes.add(node); Iterables.addAll(nodes, createUserComments(action.getDefinition(), node)); setLayoutOption(node, CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_SIDE); @@ -963,6 +970,9 @@ private Collection transformReactorNetwork( prevNode = node; } } + + _modeDiagrams.handleModes(nodes, reactorInstance); + return nodes; } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.java index 9004e8bca7..9ee8817949 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.java @@ -40,6 +40,10 @@ public Object sourceElement(final KGraphElement elem) { return elem.getProperty(KlighdInternalProperties.MODEL_ELEMEMT); } + public boolean sourceIs(KNode node, Class clazz) { + return clazz.isInstance(sourceElement(node)); + } + public boolean sourceIsReactor(final KNode node) { return sourceElement(node) instanceof Reactor; } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.java index 2981a8437e..e8d1844596 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.java @@ -31,6 +31,7 @@ import java.util.Iterator; import org.eclipse.xtext.xbase.lib.IteratorExtensions; import org.lflang.diagram.synthesis.util.NamedInstanceUtil; +import org.lflang.lf.Mode; /** * Action that expands (shows details) of all reactor nodes. @@ -44,11 +45,12 @@ public class CollapseAllReactorsAction extends AbstractAction { @Override public IAction.ActionResult execute(final IAction.ActionContext context) { ViewContext vc = context.getViewContext(); - Iterator knodes = ModelingUtil.eAllContentsOfType(vc.getViewModel(), KNode.class); - Iterator knodesSourceIsReactor = IteratorExtensions.filter(knodes, this::sourceIsReactor); - - for (KNode node : IteratorExtensions.toIterable(knodesSourceIsReactor)) { - if (!(sourceAsReactor(node).isMain() || sourceAsReactor(node).isFederated())) { + Iterator nodes = ModelingUtil.eAllContentsOfType(vc.getViewModel(), KNode.class); + + while(nodes.hasNext()) { + var node = nodes.next(); + if (sourceIs(node, Mode.class) || (sourceIsReactor(node) && + !(sourceAsReactor(node).isMain() || sourceAsReactor(node).isFederated()))) { MemorizingExpandCollapseAction.setExpansionState( node, NamedInstanceUtil.getLinkedInstance(node), diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.java index 9990ae5220..de1f7f1be2 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.java @@ -31,6 +31,7 @@ import java.util.Iterator; import org.eclipse.xtext.xbase.lib.IteratorExtensions; import org.lflang.diagram.synthesis.util.NamedInstanceUtil; +import org.lflang.lf.Mode; /** * Action that collapses (hides details) of all reactor nodes. @@ -44,16 +45,18 @@ public class ExpandAllReactorsAction extends AbstractAction { @Override public IAction.ActionResult execute(final IAction.ActionContext context) { ViewContext vc = context.getViewContext(); - Iterator knodes = ModelingUtil.eAllContentsOfType(vc.getViewModel(), KNode.class); - Iterator knodesSourceIsReactor = IteratorExtensions.filter(knodes, this::sourceIsReactor); - - for (KNode node : IteratorExtensions.toIterable(knodesSourceIsReactor)) { - MemorizingExpandCollapseAction.setExpansionState( - node, - NamedInstanceUtil.getLinkedInstance(node), - vc.getViewer(), - true - ); + Iterator nodes = ModelingUtil.eAllContentsOfType(vc.getViewModel(), KNode.class); + + while(nodes.hasNext()) { + var node = nodes.next(); + if (sourceIs(node, Mode.class) || sourceIsReactor(node)) { + MemorizingExpandCollapseAction.setExpansionState( + node, + NamedInstanceUtil.getLinkedInstance(node), + vc.getViewer(), + true + ); + } } return IAction.ActionResult.createResult(true); } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.java index 9c1d1721b0..87a4ce8e38 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.java @@ -101,8 +101,19 @@ public IAction.ActionResult execute(final IAction.ActionContext context) { IViewer v = vc.getViewer(); KNode node = context.getKNode(); NamedInstance linkedInstance = NamedInstanceUtil.getLinkedInstance(node); - setExpansionState(node, linkedInstance, v, !v.isExpanded(node)); // toggle - return IAction.ActionResult.createResult(true); + + // Find node that is properly linked + while(node != null && linkedInstance == null) { + node = node.getParent(); + linkedInstance = NamedInstanceUtil.getLinkedInstance(node); + } + + if (node == null) { + return IAction.ActionResult.createResult(false); + } else { + setExpansionState(node, linkedInstance, v, !v.isExpanded(node)); // toggle + return IAction.ActionResult.createResult(true); + } } } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ModeDiagrams.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ModeDiagrams.java new file mode 100644 index 0000000000..9aff2bf429 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ModeDiagrams.java @@ -0,0 +1,471 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2021 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This code is provided under the terms of the Eclipse Public License (EPL). + */ +package org.lflang.diagram.synthesis.util; + + +import java.awt.Color; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.elk.alg.layered.options.LayerConstraint; +import org.eclipse.elk.alg.layered.options.LayeredOptions; +import org.eclipse.elk.core.math.ElkPadding; +import org.eclipse.elk.core.options.CoreOptions; +import org.eclipse.elk.core.options.Direction; +import org.eclipse.elk.core.options.EdgeRouting; +import org.eclipse.elk.core.options.PortConstraints; +import org.eclipse.elk.core.options.PortSide; +import org.eclipse.elk.core.options.SizeConstraint; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.xbase.lib.Extension; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.eclipse.xtext.xbase.lib.ListExtensions; +import org.eclipse.xtext.xbase.lib.Pair; +import org.lflang.diagram.synthesis.AbstractSynthesisExtensions; +import org.lflang.diagram.synthesis.LinguaFrancaSynthesis; +import org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction; +import org.lflang.diagram.synthesis.styles.LinguaFrancaShapeExtensions; +import org.lflang.diagram.synthesis.styles.LinguaFrancaStyleExtensions; +import org.lflang.generator.ModeInstance; +import org.lflang.generator.ModeInstance.ModeTransitionType; +import org.lflang.generator.ModeInstance.Transition; +import org.lflang.generator.ReactorInstance; +import org.lflang.lf.Action; +import org.lflang.lf.Mode; +import org.lflang.lf.Timer; +import org.lflang.lf.VarRef; + +import com.google.common.collect.HashMultimap; +import com.google.inject.Inject; + +import de.cau.cs.kieler.klighd.SynthesisOption; +import de.cau.cs.kieler.klighd.kgraph.KEdge; +import de.cau.cs.kieler.klighd.kgraph.KLabel; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.kgraph.KPort; +import de.cau.cs.kieler.klighd.krendering.Colors; +import de.cau.cs.kieler.klighd.krendering.KContainerRendering; +import de.cau.cs.kieler.klighd.krendering.KDecoratorPlacementData; +import de.cau.cs.kieler.klighd.krendering.KEllipse; +import de.cau.cs.kieler.klighd.krendering.KPolyline; +import de.cau.cs.kieler.klighd.krendering.KRectangle; +import de.cau.cs.kieler.klighd.krendering.KRendering; +import de.cau.cs.kieler.klighd.krendering.KRenderingFactory; +import de.cau.cs.kieler.klighd.krendering.KText; +import de.cau.cs.kieler.klighd.krendering.LineStyle; +import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; +import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KEdgeExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KLabelExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KNodeExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KPolylineExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KPortExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions; +import de.cau.cs.kieler.klighd.labels.decoration.ITextRenderingProvider; +import de.cau.cs.kieler.klighd.labels.decoration.LabelDecorationConfigurator; +import de.cau.cs.kieler.klighd.labels.decoration.LinesDecorator; +import de.cau.cs.kieler.klighd.labels.decoration.RectangleDecorator; +import de.cau.cs.kieler.klighd.syntheses.DiagramSyntheses; +import de.cau.cs.kieler.klighd.util.KlighdProperties; + +/** + * Transformations to support modes in the Lingua Franca diagram synthesis. + * + * @author{Alexander Schulz-Rosengarten } + */ +@ViewSynthesisShared +public class ModeDiagrams extends AbstractSynthesisExtensions { + + // Related synthesis option + public static final SynthesisOption MODES_CATEGORY = + SynthesisOption.createCategory("Modes", false).setCategory(LinguaFrancaSynthesis.APPEARANCE); + public static final SynthesisOption SHOW_TRANSITION_LABELS = + SynthesisOption.createCheckOption("Transition Labels", true).setCategory(MODES_CATEGORY); + public static final SynthesisOption INITIALLY_COLLAPSE_MODES = + SynthesisOption.createCheckOption("Initially Collapse Modes", true).setCategory(MODES_CATEGORY); + + private static final Colors MODE_FG = Colors.SLATE_GRAY; + private static final Colors MODE_BG = Colors.SLATE_GRAY_3; + private static final int MODE_BG_ALPHA = 50; + + @Inject @Extension private KNodeExtensions _kNodeExtensions; + @Inject @Extension private KEdgeExtensions _kEdgeExtensions; + @Inject @Extension private KPortExtensions _kPortExtensions; + @Inject @Extension private KLabelExtensions _kLabelExtensions; + @Inject @Extension private KPolylineExtensions _kPolylineExtensions; + @Inject @Extension private KRenderingExtensions _kRenderingExtensions; + @Inject @Extension private KContainerRenderingExtensions _kContainerRenderingExtensions; + @Inject @Extension private LinguaFrancaShapeExtensions _linguaFrancaShapeExtensions; + @Inject @Extension private LinguaFrancaStyleExtensions _linguaFrancaStyleExtensions; + @Inject @Extension private UtilityExtensions _utilityExtensions; + + @Extension private KRenderingFactory _kRenderingFactory = KRenderingFactory.eINSTANCE; + + public void handleModes(List nodes, ReactorInstance reactor) { + if (!reactor.modes.isEmpty()) { + var modeNodes = new LinkedHashMap(); + var modeDefinitionMap = new LinkedHashMap(); + for (ModeInstance mode : reactor.modes) { + var node = _kNodeExtensions.createNode(); + associateWith(node, mode.getDefinition()); + NamedInstanceUtil.linkInstance(node, mode); + _utilityExtensions.setID(node, mode.uniqueID()); + + modeNodes.put(mode, node); + modeDefinitionMap.put(mode.getDefinition(), mode); + + if (mode.isInitial()) { + DiagramSyntheses.setLayoutOption(node, LayeredOptions.LAYERING_LAYER_CONSTRAINT, LayerConstraint.FIRST); + } + DiagramSyntheses.setLayoutOption(node, LayeredOptions.CROSSING_MINIMIZATION_SEMI_INTERACTIVE, true); + + var expansionState = MemorizingExpandCollapseAction.getExpansionState(mode); + DiagramSyntheses.setLayoutOption(node, KlighdProperties.EXPAND, + expansionState != null ? expansionState : !this.getBooleanValue(INITIALLY_COLLAPSE_MODES)); + + // Expanded Rectangle + var expandFigure = addModeFigure(node, mode, true); + expandFigure.setProperty(KlighdProperties.EXPANDED_RENDERING, true); + _kRenderingExtensions.addDoubleClickAction(expandFigure, MemorizingExpandCollapseAction.ID); + + if (getBooleanValue(LinguaFrancaSynthesis.SHOW_HYPERLINKS)) { + // Collapse button + KText textButton = _linguaFrancaShapeExtensions.addTextButton(expandFigure, LinguaFrancaSynthesis.TEXT_HIDE_ACTION); + _kRenderingExtensions.to( + _kRenderingExtensions.from(_kRenderingExtensions.setGridPlacementData(textButton), + _kRenderingExtensions.LEFT, 8, 0, _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 8, 0, _kRenderingExtensions.BOTTOM, 0, 0); + _kRenderingExtensions.addSingleClickAction(textButton, MemorizingExpandCollapseAction.ID); + _kRenderingExtensions.addDoubleClickAction(textButton, MemorizingExpandCollapseAction.ID); + } + + _kContainerRenderingExtensions.addChildArea(expandFigure); + + // Collapse Rectangle + var collapseFigure = addModeFigure(node, mode, false); + collapseFigure.setProperty(KlighdProperties.COLLAPSED_RENDERING, true); + if (this.hasContent(mode)) { + _kRenderingExtensions.addDoubleClickAction(collapseFigure, MemorizingExpandCollapseAction.ID); + + if (getBooleanValue(LinguaFrancaSynthesis.SHOW_HYPERLINKS)) { + // Expand button + KText textButton = _linguaFrancaShapeExtensions.addTextButton(collapseFigure, LinguaFrancaSynthesis.TEXT_SHOW_ACTION); + _kRenderingExtensions.to(_kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(textButton), + _kRenderingExtensions.LEFT, 8, 0, _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 8, 0, _kRenderingExtensions.BOTTOM, 8, 0); + _kRenderingExtensions.addSingleClickAction(textButton, MemorizingExpandCollapseAction.ID); + _kRenderingExtensions.addDoubleClickAction(textButton, MemorizingExpandCollapseAction.ID); + } + } + } + + var modeChildren = HashMultimap.create(); + var nodeModes = new HashMap(); + for (var node : nodes) { + var instance = NamedInstanceUtil.getLinkedInstance(node); + if (instance == null && node.getProperty(CoreOptions.COMMENT_BOX)) { + var firstEdge = IterableExtensions.head(node.getOutgoingEdges()); + if (firstEdge != null && firstEdge.getTarget() != null) { + instance = NamedInstanceUtil.getLinkedInstance(firstEdge.getTarget()); + } + } + if (instance != null) { + var mode = instance.getMode(true); + modeChildren.put(mode, node); + nodeModes.put(node, mode); + } else { + modeChildren.put(null, node); + } + } + + var modeContainer = _kNodeExtensions.createNode(); + modeContainer.getChildren().addAll(modeNodes.values()); + var fig = addModeContainerFigure(modeContainer); + _kRenderingExtensions.addDoubleClickAction(fig, MemorizingExpandCollapseAction.ID); + if (modeChildren.get(null).isEmpty()) { + _kRenderingExtensions.setInvisible(fig, true); + DiagramSyntheses.setLayoutOption(modeContainer, CoreOptions.PADDING, new ElkPadding()); + } + DiagramSyntheses.setLayoutOption(modeContainer, CoreOptions.NODE_SIZE_CONSTRAINTS, SizeConstraint.minimumSizeWithPorts()); + DiagramSyntheses.setLayoutOption(modeContainer, CoreOptions.EDGE_ROUTING, EdgeRouting.SPLINES); + DiagramSyntheses.setLayoutOption(modeContainer, CoreOptions.DIRECTION, Direction.DOWN); + DiagramSyntheses.setLayoutOption(modeContainer, CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_ORDER); + + var modeContainerPorts = new HashMap(); + for (var mode : ListExtensions.reverseView(reactor.modes)) { + var modeNode = modeNodes.get(mode); + var edges = new HashSet(); + // add children + for (var child : modeChildren.get(mode)) { + nodes.remove(child); + modeNode.getChildren().add(child); + + edges.addAll(child.getIncomingEdges()); + edges.addAll(child.getOutgoingEdges()); + } + + // add transitions + var representedTargets = new HashSet>(); + for (var transition : ListExtensions.reverseView(mode.transitions)) { + if (!representedTargets.contains(new Pair(transition.target, transition.type))) { + var edge = _kEdgeExtensions.createEdge(); + edge.setSource(modeNode); + edge.setTarget(modeNodes.get(transition.target)); + addTransitionFigure(edge, transition); + + if (getBooleanValue(SHOW_TRANSITION_LABELS)) { + associateWith(edge, transition.getDefinition()); + } else { + // Bundle similar transitions + representedTargets.add(new Pair(transition.target, transition.type)); + } + } + } + + // handle cross hierarchy edges + var portCopies = new HashMap(); + for (var edge : edges) { + if (!edge.getProperty(CoreOptions.NO_LAYOUT)) { + var sourceNodeMode = nodeModes.get(edge.getSource()); + if (sourceNodeMode == null) { + sourceNodeMode = nodeModes.get(edge.getSource().getParent()); + } + var sourceIsInMode = sourceNodeMode != null; + var targetNodeMode = nodeModes.get(edge.getTarget()); + if (targetNodeMode == null) { + targetNodeMode = nodeModes.get(edge.getTarget().getParent()); + } + var targetIsInMode = targetNodeMode != null; + + if (!sourceIsInMode || !targetIsInMode) { + var node = sourceIsInMode ? edge.getTarget() : edge.getSource(); + var port = sourceIsInMode ? edge.getTargetPort() : edge.getSourcePort(); + var isLocal = modeChildren.get(null).contains(node); + if (isLocal) { + // Add port to mode container + if (modeContainerPorts.containsKey(port)) { + node = modeContainer; + port = modeContainerPorts.get(port); + } else { + var containerPort = _kPortExtensions.createPort(); + modeContainerPorts.put(port, containerPort); + modeContainer.getPorts().add(containerPort); + + _kPortExtensions.setPortSize(containerPort, 8, 4); + KRectangle rect = _kRenderingExtensions.addRectangle(containerPort); + _kRenderingExtensions.setBackground(rect, Colors.BLACK); + + DiagramSyntheses.setLayoutOption(containerPort, CoreOptions.PORT_BORDER_OFFSET, -4.0); + DiagramSyntheses.setLayoutOption(containerPort, CoreOptions.PORT_SIDE, sourceIsInMode ? PortSide.EAST : PortSide.WEST); + + var source = _utilityExtensions.sourceElement(node); + var label = ""; + if (source instanceof Action) { + label = ((Action) source).getName(); + } else if (source instanceof Timer) { + label = ((Timer) source).getName(); + } + _kLabelExtensions.addOutsidePortLabel(containerPort, label, 8); + + // new connection + var copy = EcoreUtil.copy(edge); + if (sourceIsInMode) { + copy.setSource(modeContainer); + copy.setSourcePort(containerPort); + copy.setTarget(edge.getTarget()); + } else { + copy.setTarget(modeContainer); + copy.setTargetPort(containerPort); + copy.setSource(edge.getSource()); + } + + node = modeContainer; + port = containerPort; + } + } + + // Duplicate port + if (!portCopies.containsKey(port)) { + var copy = EcoreUtil.copy(port); + portCopies.put(port, copy); + + var dummyNode = _kNodeExtensions.createNode(); + var newID = mode.uniqueID() + "_"; + if (!port.getLabels().isEmpty()) { + newID += IterableExtensions.head(port.getLabels()).getText(); + } + _utilityExtensions.setID(dummyNode, newID); + _kRenderingExtensions.addInvisibleContainerRendering(dummyNode); + dummyNode.getPorts().add(copy); + DiagramSyntheses.setLayoutOption(dummyNode, LayeredOptions.LAYERING_LAYER_CONSTRAINT, + port.getProperty(CoreOptions.PORT_SIDE) == PortSide.WEST ? LayerConstraint.FIRST : LayerConstraint.LAST); + + modeNode.getChildren().add(dummyNode); + } + var newPort = portCopies.get(port); + if (sourceIsInMode) { + edge.setTarget(newPort.getNode()); + edge.setTargetPort(newPort); + } else { + edge.setSource(newPort.getNode()); + edge.setSourcePort(newPort); + } + } + } + } + } + + nodes.add(modeContainer); + } + } + + private boolean hasContent(ModeInstance mode) { + return !mode.reactions.isEmpty() || !mode.instantiations.isEmpty(); + } + + private KContainerRendering addModeFigure(KNode node, ModeInstance mode, boolean expanded) { + int padding = getBooleanValue(LinguaFrancaSynthesis.SHOW_HYPERLINKS) ? 8 : 6; + + var figure = _kRenderingExtensions.addRoundedRectangle(node, 13, 13, 1); + _kContainerRenderingExtensions.setGridPlacement(figure, 1); + _kRenderingExtensions.setLineWidth(figure, mode.isInitial() ? 3f : 1.5f); + _kRenderingExtensions.setForeground(figure, MODE_FG); + _kRenderingExtensions.setBackground(figure, MODE_BG); + var background = _kRenderingExtensions.getBackground(figure); + background.setAlpha(MODE_BG_ALPHA); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(figure); + + // Invisible container + KRectangle container = _kContainerRenderingExtensions.addRectangle(figure); + _kRenderingExtensions.setInvisible(container, true); + int bottomPadding = this.hasContent(mode) && expanded ? 4 : padding; + var from = _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(container), + _kRenderingExtensions.LEFT, padding, 0, _kRenderingExtensions.TOP, padding, 0); + _kRenderingExtensions.to(from, _kRenderingExtensions.RIGHT, padding, 0, _kRenderingExtensions.BOTTOM, bottomPadding, 0); + + // Centered child container + KRectangle childContainer = _kContainerRenderingExtensions.addRectangle(container); + this._kRenderingExtensions.setInvisible(childContainer, true); + this._kRenderingExtensions.setPointPlacementData(childContainer, + _kRenderingExtensions.LEFT, 0, 0.5f, _kRenderingExtensions.TOP, 0, 0.5f, + _kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_CENTRAL, 0, 0, 0, 0); + this._kContainerRenderingExtensions.setGridPlacement(childContainer, 1); + + KText text = _kContainerRenderingExtensions.addText(childContainer, mode.getName()); + DiagramSyntheses.suppressSelectability(text); + _linguaFrancaStyleExtensions.underlineSelectionStyle(text); + + return figure; + } + + private KContainerRendering addModeContainerFigure(KNode node) { + var rect = _kRenderingExtensions.addRectangle(node); + _kRenderingExtensions.setLineWidth(rect, 1); + _kRenderingExtensions.setLineStyle(rect, LineStyle.DOT); + _kRenderingExtensions.setForeground(rect, MODE_FG); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(rect); + return rect; + } + + private void addTransitionFigure(KEdge edge, Transition transition) { + var spline = _kEdgeExtensions.addSpline(edge); + _kRenderingExtensions.setLineWidth(spline, 1.5f); + _kRenderingExtensions.setForeground(spline, MODE_FG); + _kRenderingExtensions.setBackground(spline, MODE_FG); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(spline); + + if (transition.type == ModeTransitionType.HISTORY) { + addHistoryDecorator(spline); + } else { + KRendering arrowDecorator = _kPolylineExtensions.addHeadArrowDecorator(spline); + this._kRenderingExtensions.setForeground(arrowDecorator, MODE_FG); + this._kRenderingExtensions.setBackground(arrowDecorator, MODE_FG); + } + + if (getBooleanValue(SHOW_TRANSITION_LABELS)) { + associateWith(spline, transition.getDefinition()); + + KLabel centerEdgeLabel = _kLabelExtensions.addCenterEdgeLabel(edge, this.toTransitionLabel(transition)); + associateWith(centerEdgeLabel, transition.getDefinition()); + applyTransitionOnEdgeStyle(centerEdgeLabel); + } + } + + private String toTransitionLabel(Transition transition) { + var text = new StringBuilder(); + + text.append(transition.reaction.triggers.stream().map(t -> t.getDefinition().getName()).collect(Collectors.joining(", "))); + + if (!transition.reaction.effects.isEmpty()) { + text.append(" / "); + for(var eff : transition.reaction.effects) { + if (eff.getDefinition() instanceof VarRef && ((VarRef) eff.getDefinition()).getContainer() != null) { + text.append(((VarRef) eff.getDefinition()).getContainer().getName()).append("."); + } + text.append(eff.getDefinition().getName()); + } + } + return text.toString(); + } + + private static LabelDecorationConfigurator _onEdgeTransitionLabelConfigurator; // ONLY for use in applyTransitionOnEdgeStyle + private void applyTransitionOnEdgeStyle(KLabel label) { + if (_onEdgeTransitionLabelConfigurator == null) { + var foreground = new Color(MODE_FG.getRed(), MODE_FG.getGreen(), MODE_FG.getBlue()); + var background = new Color(Colors.GRAY_95.getRed(), Colors.GRAY_95.getGreen(), Colors.GRAY_95.getBlue()); + _onEdgeTransitionLabelConfigurator = LabelDecorationConfigurator.create() + .withInlineLabels(true) + .withLabelTextRenderingProvider(new ITextRenderingProvider() { + @Override + public KRendering createTextRendering( + KContainerRendering container, KLabel llabel) { + var kText = _kRenderingFactory.createKText(); + _kRenderingExtensions.setFontSize(kText, 8); + container.getChildren().add(kText); + return kText; + } + }) + .addDecoratorRenderingProvider(RectangleDecorator.create().withBackground(background)) + .addDecoratorRenderingProvider(LinesDecorator.create().withColor(foreground)); + } + _onEdgeTransitionLabelConfigurator.applyTo(label); + } + + private void addHistoryDecorator(KPolyline line) { + var decorator = _kPolylineExtensions.addHeadArrowDecorator(line); + ((KDecoratorPlacementData) decorator.getPlacementData()).setAbsolute((-15.0f)); + + var ellipse = _kContainerRenderingExtensions.addEllipse(line); + _kRenderingExtensions.setDecoratorPlacementData(ellipse, 16, 16, (-6), 1, false); + _kRenderingExtensions.setLineWidth(ellipse, 0.8f); + _kRenderingExtensions.setForeground(ellipse, MODE_FG); + _kRenderingExtensions.setBackground(ellipse, Colors.WHITE); + + var innerLine = _kContainerRenderingExtensions.addPolyline(ellipse); + _kRenderingExtensions.setLineWidth(innerLine, 2); + var points = innerLine.getPoints(); + points.add(_kRenderingExtensions.createKPosition(_kRenderingExtensions.LEFT, 5, 0, _kRenderingExtensions.TOP, 4, 0)); + points.add(_kRenderingExtensions.createKPosition(_kRenderingExtensions.LEFT, 5, 0, _kRenderingExtensions.BOTTOM, 4, 0)); + points.add(_kRenderingExtensions.createKPosition(_kRenderingExtensions.LEFT, 5, 0, _kRenderingExtensions.TOP, 0, 0.5f)); + points.add(_kRenderingExtensions.createKPosition(_kRenderingExtensions.RIGHT, 5, 0, _kRenderingExtensions.TOP, 0, 0.5f)); + points.add(_kRenderingExtensions.createKPosition(_kRenderingExtensions.RIGHT, 5, 0, _kRenderingExtensions.BOTTOM, 4, 0)); + points.add(_kRenderingExtensions.createKPosition(_kRenderingExtensions.RIGHT, 5, 0, _kRenderingExtensions.TOP, 4, 0)); + _kRenderingExtensions.setForeground(innerLine, MODE_FG); + } + +} \ No newline at end of file diff --git a/org.lflang.ui/src/org/lflang/ui/LFUiModule.java b/org.lflang.ui/src/org/lflang/ui/LFUiModule.java index a39dc38cd5..15b9464874 100644 --- a/org.lflang.ui/src/org/lflang/ui/LFUiModule.java +++ b/org.lflang.ui/src/org/lflang/ui/LFUiModule.java @@ -5,8 +5,10 @@ import org.eclipse.ui.plugin.AbstractUIPlugin; import org.eclipse.xtext.generator.IShouldGenerate; +import org.eclipse.xtext.ide.editor.syntaxcoloring.ISemanticHighlightingCalculator; import org.eclipse.xtext.ui.wizard.IProjectCreator; import org.eclipse.xtext.ui.wizard.template.DefaultTemplateProjectCreator; +import org.lflang.ui.highlighting.LFSemanticHighlightingCalculator; /** * Use this class to register components to be used within the Eclipse IDE. @@ -23,6 +25,11 @@ public Class bindIShouldGenerate() { return EclipseBasedShouldGenerateLF.class; } + /** Register LF specific highlighting provider **/ + public Class bindISemanticHighlightingCalculator() { + return LFSemanticHighlightingCalculator.class; + } + /** * Manually activate project template support. * This would be added by the TemplateProjectWizardFragment (in mwe file) to the AbstractLFUiModule diff --git a/org.lflang.ui/src/org/lflang/ui/contentassist/LFProposalProvider.java b/org.lflang.ui/src/org/lflang/ui/contentassist/LFProposalProvider.java index 9e10038905..7c822a2d96 100644 --- a/org.lflang.ui/src/org/lflang/ui/contentassist/LFProposalProvider.java +++ b/org.lflang.ui/src/org/lflang/ui/contentassist/LFProposalProvider.java @@ -1,12 +1,52 @@ -/* - * generated by Xtext 2.23.0 - */ +/************* +Copyright (c) 2021, Kiel University. + +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.ui.contentassist; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.Assignment; +import org.eclipse.xtext.ui.editor.contentassist.ContentAssistContext; +import org.eclipse.xtext.ui.editor.contentassist.ICompletionProposalAcceptor; +import org.lflang.generator.ModeInstance.ModeTransitionType; /** + * Code completion adjustments for LF. + * * See https://www.eclipse.org/Xtext/documentation/310_eclipse_support.html#content-assist * on how to customize the content assistant. + * + * @author{Alexander Schulz-Rosengarten } */ public class LFProposalProvider extends AbstractLFProposalProvider { + + @Override + public void completeVarRefOrModeTransition_Modifier(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { + // call implementation of superclass + super.completeVarRefOrModeTransition_Modifier(model, assignment, context, acceptor); + + // code completion for mode transitions + for (var value : ModeTransitionType.values()) { + acceptor.accept(createCompletionProposal(value.getKeyword(), context)); + } + } } diff --git a/org.lflang.ui/src/org/lflang/ui/highlighting/LFSemanticHighlightingCalculator.java b/org.lflang.ui/src/org/lflang/ui/highlighting/LFSemanticHighlightingCalculator.java new file mode 100644 index 0000000000..865f9dc1e3 --- /dev/null +++ b/org.lflang.ui/src/org/lflang/ui/highlighting/LFSemanticHighlightingCalculator.java @@ -0,0 +1,83 @@ +/************* +Copyright (c) 2021, Kiel University. + +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.ui.highlighting; + +import org.eclipse.xtext.ide.editor.syntaxcoloring.DefaultSemanticHighlightingCalculator; +import org.eclipse.xtext.ide.editor.syntaxcoloring.HighlightingStyles; +import org.eclipse.xtext.ide.editor.syntaxcoloring.IHighlightedPositionAcceptor; +import org.eclipse.xtext.ide.editor.syntaxcoloring.ISemanticHighlightingCalculator; +import org.eclipse.xtext.parser.IParseResult; +import org.eclipse.xtext.resource.ILocationInFileProvider; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.util.CancelIndicator; +import org.lflang.ASTUtils; +import org.lflang.generator.ModeInstance.ModeTransitionType; +import org.lflang.lf.LfPackage; +import org.lflang.lf.Model; + +import com.google.inject.Inject; + +/** + * Syntax highlighting adjustments for LF. + * + * @author{Alexander Schulz-Rosengarten } + */ +public class LFSemanticHighlightingCalculator implements ISemanticHighlightingCalculator { + + @Inject + private ILocationInFileProvider locator; + + /** + * {@inheritDoc} + */ + @Override + public void provideHighlightingFor(XtextResource resource, IHighlightedPositionAcceptor acceptor, CancelIndicator cancelIndicator) { + if (resource == null || locator == null) { + return; + } + IParseResult parseResult = resource.getParseResult(); + if (parseResult == null || parseResult.getRootASTElement() == null) { + return; + } + Model model = (Model) parseResult.getRootASTElement(); + + // Provide keyword highlighting for special mode transitions + for (var reactor : model.getReactors()) { + for (var reaction : ASTUtils.allReactions(reactor)) { + for (var effect : reaction.getEffects()) { + if (effect.getModifier() != null) { + if (ModeTransitionType.KEYWORDS.contains(effect.getModifier())) { + var pos = locator.getSignificantTextRegion(effect, LfPackage.eINSTANCE.getVarRef_Modifier(), 0); + if (pos != null) { + acceptor.addPosition(pos.getOffset(), pos.getLength(), HighlightingStyles.KEYWORD_ID); + } + } + } + } + } + } + + } + +} diff --git a/org.lflang.ui/src/org/lflang/ui/wizard/templates/c/src/Pipeline.lf b/org.lflang.ui/src/org/lflang/ui/wizard/templates/c/src/Pipeline.lf index d1edc09b0b..76932c5636 100644 --- a/org.lflang.ui/src/org/lflang/ui/wizard/templates/c/src/Pipeline.lf +++ b/org.lflang.ui/src/org/lflang/ui/wizard/templates/c/src/Pipeline.lf @@ -23,16 +23,16 @@ target C { * * @param offset The starting time. * @param period The period. - * @param initial The first output. + * @param init The first output. * @param increment The increment between outputs */ reactor SendCount( offset:time(0), period:time(1 sec), - initial:int(0), + init:int(0), increment:int(1) ) { - state count:int(initial); + state count:int(init); output out:int; timer t(offset, period); reaction(t) -> out {= diff --git a/org.lflang/src/lib/c/reactor-c b/org.lflang/src/lib/c/reactor-c index e7306edb43..76a055fe52 160000 --- a/org.lflang/src/lib/c/reactor-c +++ b/org.lflang/src/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit e7306edb430560aca043dc47345e24bf5f1f87d3 +Subproject commit 76a055fe52b1b2ea335b57f79885d64d9a895b1c diff --git a/org.lflang/src/org/lflang/ASTUtils.java b/org.lflang/src/org/lflang/ASTUtils.java index 4a821000b6..ca7659ee5f 100644 --- a/org.lflang/src/org/lflang/ASTUtils.java +++ b/org.lflang/src/org/lflang/ASTUtils.java @@ -28,14 +28,17 @@ package org.lflang; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; +import org.eclipse.emf.common.util.EList; import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.xtext.TerminalRule; @@ -46,6 +49,8 @@ import org.eclipse.xtext.nodemodel.impl.HiddenLeafNode; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.util.Pair; +import org.eclipse.xtext.util.Tuples; import org.eclipse.xtext.xbase.lib.IterableExtensions; import org.eclipse.xtext.xbase.lib.IteratorExtensions; import org.eclipse.xtext.xbase.lib.StringExtensions; @@ -64,6 +69,8 @@ import org.lflang.lf.Input; import org.lflang.lf.Instantiation; import org.lflang.lf.LfFactory; +import org.lflang.lf.LfPackage; +import org.lflang.lf.Mode; import org.lflang.lf.Model; import org.lflang.lf.Output; import org.lflang.lf.Parameter; @@ -79,9 +86,11 @@ import org.lflang.lf.TypeParm; import org.lflang.lf.Value; import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; import org.lflang.lf.WidthSpec; import org.lflang.lf.WidthTerm; +import com.google.common.collect.HashMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; @@ -98,6 +107,23 @@ public class ASTUtils { */ public static final LfFactory factory = LfFactory.eINSTANCE; + /** + * The Lingua Franca feature package. + */ + public static final LfPackage featurePackage = LfPackage.eINSTANCE; + + /** + * A mapping from Reactor features to corresponding Mode features for collecting contained elements. + */ + private static final Map reactorModeFeatureMap = Map.of( + featurePackage.getReactor_Actions(), featurePackage.getMode_Actions(), + featurePackage.getReactor_Connections(), featurePackage.getMode_Connections(), + featurePackage.getReactor_Instantiations(), featurePackage.getMode_Instantiations(), + featurePackage.getReactor_Reactions(), featurePackage.getMode_Reactions(), + featurePackage.getReactor_StateVars(), featurePackage.getMode_StateVars(), + featurePackage.getReactor_Timers(), featurePackage.getMode_Timers() + ); + /** * Find connections in the given resource that have a delay associated with them, * and reroute them via a generated delay reactor. @@ -108,15 +134,15 @@ public static void insertGeneratedDelays(Resource resource, GeneratorBase genera // The resulting changes to the AST are performed _after_ iterating // in order to avoid concurrent modification problems. List oldConnections = new ArrayList<>(); - Map> newConnections = new LinkedHashMap<>(); - Map> delayInstances = new LinkedHashMap<>(); + Map> newConnections = new LinkedHashMap<>(); + Map> delayInstances = new LinkedHashMap<>(); Iterable containers = Iterables.filter(IteratorExtensions.toIterable(resource.getAllContents()), Reactor.class); // Iterate over the connections in the tree. for (Reactor container : containers) { - for (Connection connection : container.getConnections()) { + for (Connection connection : allConnections(container)) { if (connection.getDelay() != null) { - Reactor parent = (Reactor) connection.eContainer(); + EObject parent = connection.eContainer(); // Assume all the types are the same, so just use the first on the right. Type type = ((Port) connection.getRightPorts().get(0).getVariable()).getType(); Reactor delayClass = getDelayClass(type, generator); @@ -140,17 +166,108 @@ public static void insertGeneratedDelays(Resource resource, GeneratorBase genera } // Remove old connections; insert new ones. - oldConnections.forEach(connection -> ((Reactor) connection.eContainer()).getConnections().remove(connection)); - newConnections.forEach((reactor, connections) -> reactor.getConnections().addAll(connections)); + oldConnections.forEach(connection -> { + var container = connection.eContainer(); + if (container instanceof Reactor) { + ((Reactor) container).getConnections().remove(connection); + } else if (container instanceof Mode) { + ((Mode) container).getConnections().remove(connection); + } + }); + newConnections.forEach((container, connections) -> { + if (container instanceof Reactor) { + ((Reactor) container).getConnections().addAll(connections); + } else if (container instanceof Mode) { + ((Mode) container).getConnections().addAll(connections); + } + }); // Finally, insert the instances and, before doing so, assign them a unique name. - delayInstances.forEach((reactor, instantiations) -> + delayInstances.forEach((container, instantiations) -> instantiations.forEach(instantiation -> { - instantiation.setName(getUniqueIdentifier(reactor, "delay")); - reactor.getInstantiations().add(instantiation); + if (container instanceof Reactor) { + instantiation.setName(getUniqueIdentifier((Reactor) container, "delay")); + ((Reactor) container).getInstantiations().add(instantiation); + } else if (container instanceof Mode) { + instantiation.setName(getUniqueIdentifier((Reactor) container.eContainer(), "delay")); + ((Mode) container).getInstantiations().add(instantiation); + } }) ); } + /** + * Find connections in the given resource that would be conflicting writes if they were not located in mutually + * exclusive modes. + * + * @param resource The AST. + * @return a list of connections being able to be transformed + */ + public static Collection findConflictingConnectionsInModalReactors(Resource resource) { + var transform = new HashSet(); + var reactors = Iterables.filter(IteratorExtensions.toIterable(resource.getAllContents()), Reactor.class); + + for (Reactor reactor : reactors) { + if (!reactor.getModes().isEmpty()) { // Only for modal reactors + var allWriters = HashMultimap., EObject>create(); + + // Collect destinations + for (var rea : allReactions(reactor)) { + for (var eff : rea.getEffects()) { + if (eff.getVariable() instanceof Port) { + allWriters.put(Tuples.pair(eff.getContainer(), eff.getVariable()), rea); + } + } + } + for (var con : ASTUtils.collectElements(reactor, featurePackage.getReactor_Connections(), false, true)) { + for (var port : con.getRightPorts()) { + allWriters.put(Tuples.pair(port.getContainer(), port.getVariable()), con); + } + } + + // Handle conflicting writers + for (var key : allWriters.keySet()) { + var writers = allWriters.get(key); + if (writers.size() > 1) { // has multiple sources + var writerModes = HashMultimap.create(); + // find modes + for (var writer : writers) { + if (writer.eContainer() instanceof Mode) { + writerModes.put((Mode) writer.eContainer(), writer); + } else { + writerModes.put(null, writer); + } + } + // Conflicting connection can only be handled if.. + if (!writerModes.containsKey(null) && // no writer is on root level (outside of modes) and... + writerModes.keySet().stream().map(m -> writerModes.get(m)).allMatch(writersInMode -> // all writers in a mode are either... + writersInMode.size() == 1 || // the only writer or... + writersInMode.stream().allMatch(w -> w instanceof Reaction) // all are reactions and hence ordered + )) { + // Add connections to transform list + writers.stream().filter(w -> w instanceof Connection).forEach(c -> transform.add((Connection) c)); + } + } + } + } + } + + return transform; + } + + /** + * Return the enclosing reactor of an LF EObject in a reactor or mode. + * @param obj the LF model element + * @return the reactor or null + */ + public static Reactor getEnclosingReactor(EObject obj) { + if (obj.eContainer() instanceof Reactor) { + return (Reactor) obj.eContainer(); + } else if (obj.eContainer() instanceof Mode) { + return (Reactor) obj.eContainer().eContainer(); + } + return null; + } + /** * Find the main reactor and change it to a federated reactor. * Return true if the transformation was successful (or the given resource @@ -440,24 +557,28 @@ public static String getUniqueIdentifier(Reactor reactor, String name) { } //////////////////////////////// - //// Utility functions for supporting inheritance + //// Utility functions for supporting inheritance and modes /** * Given a reactor class, return a list of all its actions, * which includes actions of base classes that it extends. + * This also includes actions in modes, returning a flattened + * view over all modes. * @param definition Reactor class definition. */ public static List allActions(Reactor definition) { - return ASTUtils.collectElements(definition, (Reactor r) -> r.getActions()); + return ASTUtils.collectElements(definition, featurePackage.getReactor_Actions()); } /** * Given a reactor class, return a list of all its connections, * which includes connections of base classes that it extends. + * This also includes connections in modes, returning a flattened + * view over all modes. * @param definition Reactor class definition. */ public static List allConnections(Reactor definition) { - return ASTUtils.collectElements(definition, (Reactor r) -> r.getConnections()); + return ASTUtils.collectElements(definition, featurePackage.getReactor_Connections()); } /** @@ -469,16 +590,18 @@ public static List allConnections(Reactor definition) { * @param definition Reactor class definition. */ public static List allInputs(Reactor definition) { - return ASTUtils.collectElements(definition, (Reactor r) -> r.getInputs()); + return ASTUtils.collectElements(definition, featurePackage.getReactor_Inputs()); } /** * Given a reactor class, return a list of all its instantiations, * which includes instantiations of base classes that it extends. + * This also includes instantiations in modes, returning a flattened + * view over all modes. * @param definition Reactor class definition. */ public static List allInstantiations(Reactor definition) { - return ASTUtils.collectElements(definition, (Reactor r) -> r.getInstantiations()); + return ASTUtils.collectElements(definition, featurePackage.getReactor_Instantiations()); } /** @@ -487,7 +610,7 @@ public static List allInstantiations(Reactor definition) { * @param definition Reactor class definition. */ public static List allOutputs(Reactor definition) { - return ASTUtils.collectElements(definition, (Reactor r) -> r.getOutputs()); + return ASTUtils.collectElements(definition, featurePackage.getReactor_Outputs()); } /** @@ -496,34 +619,49 @@ public static List allOutputs(Reactor definition) { * @param definition Reactor class definition. */ public static List allParameters(Reactor definition) { - return ASTUtils.collectElements(definition, (Reactor r) -> r.getParameters()); + return ASTUtils.collectElements(definition, featurePackage.getReactor_Parameters()); } /** * Given a reactor class, return a list of all its reactions, * which includes reactions of base classes that it extends. + * This also includes reactions in modes, returning a flattened + * view over all modes. * @param definition Reactor class definition. */ public static List allReactions(Reactor definition) { - return ASTUtils.collectElements(definition, (Reactor r) -> r.getReactions()); + return ASTUtils.collectElements(definition, featurePackage.getReactor_Reactions()); } /** * Given a reactor class, return a list of all its state variables, * which includes state variables of base classes that it extends. + * This also includes reactions in modes, returning a flattened + * view over all modes. * @param definition Reactor class definition. */ public static List allStateVars(Reactor definition) { - return ASTUtils.collectElements(definition, (Reactor r) -> r.getStateVars()); + return ASTUtils.collectElements(definition, featurePackage.getReactor_StateVars()); } /** * Given a reactor class, return a list of all its timers, * which includes timers of base classes that it extends. + * This also includes reactions in modes, returning a flattened + * view over all modes. * @param definition Reactor class definition. */ public static List allTimers(Reactor definition) { - return ASTUtils.collectElements(definition, (Reactor r) -> r.getTimers()); + return ASTUtils.collectElements(definition, featurePackage.getReactor_Timers()); + } + + /** + * Given a reactor class, returns a list of all its modes, + * which includes modes of base classes that it extends. + * @param definition Reactor class definition. + */ + public static List allModes(Reactor definition) { + return ASTUtils.collectElements(definition, featurePackage.getReactor_Modes()); } /** @@ -541,25 +679,53 @@ public static LinkedHashSet superClasses(Reactor reactor) { } /** - * Collect elements of type T from the class hierarchy defined by - * a given reactor definition. + * Collect elements of type T from the class hierarchy and modes + * defined by a given reactor definition. * @param definition The reactor definition. * @param elements A function that maps a reactor definition to a list of * elements of type T. * @param The type of elements to collect (e.g., Port, Timer, etc.) * @return */ - public static List collectElements(Reactor definition, Function> elements) { + public static List collectElements(Reactor definition, EStructuralFeature feature) { + return ASTUtils.collectElements(definition, feature, true, true); + } + + /** + * Collect elements of type T contained in given reactor definition, including + * modes and the class hierarchy defined depending on configuration. + * @param definition The reactor definition. + * @param feature The structual model elements to collect. + * @param includeSuperClasses Whether to include elements in super classes. + * @param includeModes Whether to include elements in modes. + * @param The type of elements to collect (e.g., Port, Timer, etc.) + * @return + */ + @SuppressWarnings("unchecked") + public static List collectElements(Reactor definition, EStructuralFeature feature, boolean includeSuperClasses, boolean includeModes) { List result = new ArrayList(); - // Add elements of elements defined in superclasses. - LinkedHashSet s = superClasses(definition); - if (s != null) { - for (Reactor superClass : s) { - result.addAll(elements.apply(superClass)); + + if (includeSuperClasses) { + // Add elements of elements defined in superclasses. + LinkedHashSet s = superClasses(definition); + if (s != null) { + for (Reactor superClass : s) { + result.addAll((EList) superClass.eGet(feature)); + } } } + // Add elements of the current reactor. - result.addAll(elements.apply(definition)); + result.addAll((EList) definition.eGet(feature)); + + if (includeModes && reactorModeFeatureMap.containsKey(feature)) { + var modeFeature = reactorModeFeatureMap.get(feature); + // Add elements of elements defined in modes. + for (Mode mode : includeSuperClasses ? allModes(definition) : definition.getModes()) { + result.addAll((EList) mode.eGet(modeFeature)); + } + } + return result; } diff --git a/org.lflang/src/org/lflang/LinguaFranca.xtext b/org.lflang/src/org/lflang/LinguaFranca.xtext index 9b69e83675..c466f7a06e 100644 --- a/org.lflang/src/org/lflang/LinguaFranca.xtext +++ b/org.lflang/src/org/lflang/LinguaFranca.xtext @@ -101,6 +101,7 @@ Reactor: | (instantiations+=Instantiation) | (connections+=Connection) | (reactions+=Reaction) + | (modes+=Mode) | (mutations+=Mutation) )* '}'; @@ -176,6 +177,17 @@ Boolean: TRUE | FALSE ; +Mode: + {Mode} (initial?='initial')? 'mode' (name=ID)? + '{' ( + (stateVars+=StateVar) | + (timers+=Timer) | + (actions+=Action) | + (instantiations+=Instantiation) | + (connections+=Connection) | + (reactions+=Reaction) + )* '}'; + // Action that has either a physical or logical origin. // // If the origin is logical, the minDelay is a minimum logical delay @@ -195,7 +207,7 @@ Reaction: ('reaction') ('(' (triggers+=TriggerRef (',' triggers+=TriggerRef)*)? ')')? (sources+=VarRef (',' sources+=VarRef)*)? - ('->' effects+=VarRef (',' effects+=VarRef)*)? + ('->' effects+=VarRefOrModeTransition (',' effects+=VarRefOrModeTransition)*)? code=Code (stp=STP)? (deadline=Deadline)?; @@ -272,7 +284,7 @@ TypedVariable: ; Variable: - TypedVariable | Timer; + TypedVariable | Timer | Mode; Trigger: Action | Input; @@ -283,6 +295,9 @@ VarRef: variable=[Variable] | container=[Instantiation] '.' variable=[Variable] | interleaved?='interleaved' '(' (variable=[Variable] | container=[Instantiation] '.' variable=[Variable]) ')' ; +VarRefOrModeTransition returns VarRef: + VarRef | modifier=ID '(' variable=[Mode] ')' // using String field instead of an enum rule keeps user namespace cleaner +; Assignment: (lhs=[Parameter] ( @@ -510,7 +525,7 @@ Token: 'mutable' | 'input' | 'output' | 'timer' | 'action' | 'reaction' | 'startup' | 'shutdown' | 'after' | 'deadline' | 'mutation' | 'preamble' | 'new' | 'federated' | 'at' | 'as' | 'from' | 'widthof' | 'const' | 'method' | - 'interleaved' | + 'interleaved' | 'mode' | 'initial' | // Other terminals NEGINT | TRUE | FALSE | // Action origins diff --git a/org.lflang/src/org/lflang/TargetConfig.java b/org.lflang/src/org/lflang/TargetConfig.java index ab277e2ae2..b1cba21600 100644 --- a/org.lflang/src/org/lflang/TargetConfig.java +++ b/org.lflang/src/org/lflang/TargetConfig.java @@ -63,7 +63,7 @@ public class TargetConfig { * The mode of clock synchronization to be used in federated programs. * The default is 'initial'. */ - public ClockSyncMode clockSync = ClockSyncMode.INITIAL; + public ClockSyncMode clockSync = ClockSyncMode.INIT; /** * Clock sync options. diff --git a/org.lflang/src/org/lflang/TargetProperty.java b/org.lflang/src/org/lflang/TargetProperty.java index af80f02cce..06d5c1e89c 100644 --- a/org.lflang/src/org/lflang/TargetProperty.java +++ b/org.lflang/src/org/lflang/TargetProperty.java @@ -771,7 +771,7 @@ public enum UnionType implements TargetPropertyType { SCHEDULER_UNION(Arrays.asList(SchedulerOption.values()), SchedulerOption.getDefault()), LOGGING_UNION(Arrays.asList(LogLevel.values()), LogLevel.INFO), CLOCK_SYNC_UNION(Arrays.asList(ClockSyncMode.values()), - ClockSyncMode.INITIAL), + ClockSyncMode.INIT), DOCKER_UNION(Arrays.asList(PrimitiveType.BOOLEAN, DictionaryType.DOCKER_DICT), null), TRACING_UNION(Arrays.asList(PrimitiveType.BOOLEAN, DictionaryType.TRACING_DICT), @@ -1004,8 +1004,8 @@ public String toString() { * @author{Edward A. Lee } */ public enum ClockSyncMode { - OFF, INITIAL, ON; - + OFF, INIT, ON; // TODO Discuss initial in now a mode keyword (same as startup) and cannot be used as target property value, thus changed it to init + // FIXME I could not test if this change breaks anything /** * Return the name in lower case. diff --git a/org.lflang/src/org/lflang/federated/FederateInstance.java b/org.lflang/src/org/lflang/federated/FederateInstance.java index 274d513657..1da57efe66 100644 --- a/org.lflang/src/org/lflang/federated/FederateInstance.java +++ b/org.lflang/src/org/lflang/federated/FederateInstance.java @@ -254,7 +254,7 @@ public FederateInstance( * @return True if this federate contains the action in the specified reactor */ public boolean contains(Action action) { - Reactor reactor = (Reactor) action.eContainer(); + Reactor reactor = ASTUtils.getEnclosingReactor(action); if (!reactor.isFederated() || isSingleton()) return true; // If the action is used as a trigger, a source, or an effect for a top-level reaction @@ -302,7 +302,7 @@ public boolean contains(Action action) { * @param reaction The reaction. */ public boolean contains(Reaction reaction) { - Reactor reactor = (Reactor) reaction.eContainer(); + Reactor reactor = ASTUtils.getEnclosingReactor(reaction); if (!reactor.isFederated() || this.isSingleton()) return true; if (!reactor.getReactions().contains(reaction)) return false; @@ -370,7 +370,7 @@ public boolean contains(ReactorInstance instance) { * @return True if this federate contains the action in the specified reactor */ public boolean contains(Timer timer) { - Reactor reactor = (Reactor) timer.eContainer(); + Reactor reactor = ASTUtils.getEnclosingReactor(timer); if (!reactor.isFederated() || this.isSingleton()) return true; // If the action is used as a trigger, a source, or an effect for a top-level reaction diff --git a/org.lflang/src/org/lflang/generator/GeneratorBase.java b/org.lflang/src/org/lflang/generator/GeneratorBase.java index 7e22c55ba5..3c7811f0ab 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorBase.java +++ b/org.lflang/src/org/lflang/generator/GeneratorBase.java @@ -30,6 +30,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -196,6 +197,12 @@ public abstract class GeneratorBase extends AbstractLFValidator { */ public boolean isFederated = false; + /** + * Indicates whether or not the current Lingua Franca program + * contains model reactors. + */ + public boolean hasModalReactors = false; + // ////////////////////////////////////////// // // Target properties, if they are included. /** @@ -359,11 +366,19 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { // Reroute connections that have delays associated with them via // generated delay reactors. transformDelays(); + + // Transform connections that reside in mutually exclusive modes and are otherwise conflicting + // This should be done before creating the instantiation graph + transformConflictingConnectionsInModalReactors(); // Invoke these functions a second time because transformations // may have introduced new reactors! setReactorsAndInstantiationGraph(); - + + // Check for existence and support of modes + hasModalReactors = IterableExtensions.exists(reactors, it -> !it.getModes().isEmpty()); + checkModalReactorSupport(false); + enableSupportForSerializationIfApplicable(context.getCancelIndicator()); } @@ -619,6 +634,42 @@ protected void runBuildCommand() { // ////////////////////////////////////////// // // Protected methods. + + /** + * Checks whether modal reactors are present and require appropriate code generation. + * This will set the hasModalReactors variable. + * @param isSupported indicates if modes are supported by this code generation. + */ + protected void checkModalReactorSupport(boolean isSupported) { + if (hasModalReactors && !isSupported) { + errorReporter.reportError("The currently selected code generation or " + + "target configuration does not support modal reactors!"); + } + } + + /** + * Finds and transforms connections into forwarding reactions iff the connections have the same destination as other + * connections or reaction in mutually exclusive modes. + */ + private void transformConflictingConnectionsInModalReactors() { + for (LFResource r : resources) { + var transform = ASTUtils.findConflictingConnectionsInModalReactors(r.eResource); + if (!transform.isEmpty()) { + transformConflictingConnectionsInModalReactors(transform); + } + } + } + /** + * Transforms connections into forwarding reactions iff the connections have the same destination as other + * connections or reaction in mutually exclusive modes. + * + * This methods needs to be overridden in target specific code generators that support modal reactors. + */ + protected void transformConflictingConnectionsInModalReactors(Collection transform) { + errorReporter.reportError("The currently selected code generation " + + "is missing an implementation for conflicting " + + "transforming connections in modal reactors."); + } /** * Generate code for the body of a reaction that handles the diff --git a/org.lflang/src/org/lflang/generator/ModeInstance.java b/org.lflang/src/org/lflang/generator/ModeInstance.java new file mode 100644 index 0000000000..38ffb0f64c --- /dev/null +++ b/org.lflang/src/org/lflang/generator/ModeInstance.java @@ -0,0 +1,270 @@ +/************* +Copyright (c) 2021, Kiel University. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ + +package org.lflang.generator; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.lflang.lf.Mode; +import org.lflang.lf.VarRef; + +/** + * Representation of a runtime instance of a mode. + * + * @author{Alexander Schulz-Rosengarten } + */ +public class ModeInstance extends NamedInstance { + + /** + * Create a new reaction instance from the specified definition + * within the specified parent. This constructor should be called + * only by the ReactorInstance class after all other contents + * (reactions, etc.) are registered because this constructor call + * will look them up. + * @param definition A mode definition. + * @param parent The parent reactor instance, which cannot be null. + */ + protected ModeInstance(Mode definition, ReactorInstance parent) { + super(definition, parent); + + collectMembers(); + } + + //////////////////////////////////////////////////// + // Member fields. + + /** The action instances belonging to this mode instance. */ + public List actions = new LinkedList(); + + /** The reactor instances belonging to this mode instance, in order of declaration. */ + public List instantiations = new LinkedList(); + + /** List of reaction instances for this reactor instance. */ + public List reactions = new LinkedList(); + + /** The timer instances belonging to this reactor instance. */ + public List timers = new LinkedList(); + + /** The outgoing transitions of this mode. */ + public List transitions = new LinkedList(); + + //////////////////////////////////////////////////// + // Public methods. + + /** + * Return the name of this mode. + * @return The name of this mode. + */ + @Override + public String getName() { + return this.definition.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public ReactorInstance root() { + return parent.root(); + } + + /** + * Return a descriptive string. + */ + @Override + public String toString() { + return getName() + " of " + parent.getFullName(); + } + + /** + * Returns true iff this mode is the initial mode of this reactor instance. + */ + public boolean isInitial() { + return definition.isInitial(); + } + + /** + * Sets up all transitions that leave this mode. + * Requires that all mode instances and other contents + * (reactions, etc.) of the parent reactor are created. + */ + public void setupTranstions() { + transitions.clear(); + for (var reaction : reactions) { + for (var effect : reaction.definition.getEffects()) { + if (effect instanceof VarRef) { + var target = effect.getVariable(); + if (target instanceof Mode) { + transitions.add(new Transition(this, parent.lookupModeInstance((Mode) target), reaction, effect)); + } + } + } + } + } + + /** + * Returns true iff this mode contains the given instance. + */ + public boolean contains(NamedInstance instance) { + if (instance instanceof TimerInstance) { + return timers.contains(instance); + } else if (instance instanceof ActionInstance) { + return actions.contains(instance); + } else if (instance instanceof ReactorInstance) { + return instantiations.contains(instance); + } else if (instance instanceof ReactionInstance) { + return reactions.contains(instance); + } else { + return false; + } + } + + //////////////////////////////////////////////////// + // Private methods. + + private void collectMembers() { + // Collect timers + for (var decl : definition.getTimers()) { + var instance = parent.lookupTimerInstance(decl); + if (instance != null) { + this.timers.add(instance); + } + } + + // Collect actions + for (var decl : definition.getActions()) { + var instance = parent.lookupActionInstance(decl); + if (instance != null) { + this.actions.add(instance); + } + } + + // Collect reactor instantiation + for (var decl : definition.getInstantiations()) { + var instance = parent.lookupReactorInstance(decl); + if (instance != null) { + this.instantiations.add(instance); + } + } + + // Collect reactions + for (var decl : definition.getReactions()) { + var instance = parent.lookupReactionInstance(decl); + if (instance != null) { + this.reactions.add(instance); + } + } + } + + //////////////////////////////////////////////////// + // Transition type enum. + + public enum ModeTransitionType { + RESET("reset"), HISTORY("continue"); + + public static ModeTransitionType DEFAULT = ModeTransitionType.RESET; + public static Set KEYWORDS = Arrays.stream(values()).map(v -> v.keyword).collect(Collectors.toUnmodifiableSet()); + + private final String keyword; + private ModeTransitionType(String keyword) { + this.keyword = keyword; + } + + /** + * @return the keyword for this enum. + */ + public String getKeyword() { + return keyword; + } + + /** + * Returns the 'Mode Transition Types' with the + * specified keyword. + * + * @param keyword the keyword. + * @return the matching enumerator or null. + */ + public static ModeTransitionType getByKeyword(String keyword) { + if (keyword != null && !keyword.isEmpty()) { + for (var type : values()) { + if (type.keyword.equals(keyword)) { + return type; + } + } + } + return null; + } + + /** + * Returns the 'Mode Transition Types' for the + * given transition effect. + * + * @param definition the AST definition. + * @return the matching enumerator or default value if no valid modifier is present. + */ + public static ModeTransitionType getModeTransitionType(VarRef definition) { + var type = getByKeyword(definition.getModifier()); + return type != null ? type : DEFAULT; + } + + } + + //////////////////////////////////////////////////// + // Data class. + + public static class Transition extends NamedInstance { + public final ModeInstance source; + public final ModeInstance target; + public final ReactionInstance reaction; + public final ModeTransitionType type; + + Transition(ModeInstance source, ModeInstance target, ReactionInstance reaction, VarRef definition) { + super(definition, source.parent); + this.source = source; + this.target = target; + this.reaction = reaction; + this.type = ModeTransitionType.getModeTransitionType(definition); + } + + @Override + public String getName() { + return this.source.getName() + " -> " + this.target + " by " + this.reaction.getName(); + } + + @Override + public ReactorInstance root() { + return this.parent.root(); + } + + public ModeTransitionType getType() { + return type; + } + + } + +} diff --git a/org.lflang/src/org/lflang/generator/NamedInstance.java b/org.lflang/src/org/lflang/generator/NamedInstance.java index ce99549e5a..78f357159c 100644 --- a/org.lflang/src/org/lflang/generator/NamedInstance.java +++ b/org.lflang/src/org/lflang/generator/NamedInstance.java @@ -101,14 +101,7 @@ public int getDepth() { * @return The full name of this instance. */ public String getFullName() { - if (_fullName == null) { - if (parent == null) { - _fullName = getName(); - } else { - _fullName = parent.getFullName() + "." + this.getName(); - } - } - return _fullName; + return getFullNameWithJoiner("."); } /** @@ -252,6 +245,25 @@ public String uniqueID() { } return _uniqueID; } + + /** + * Returns the directly/indirectly enclosing mode. + * @param direct flag whether to check only for direct enclosing mode + * or also consider modes of parent reactor instances. + * @return The mode, if any, null otherwise. + */ + public ModeInstance getMode(boolean direct) { + ModeInstance mode = null; + if (parent != null) { + if (!parent.modes.isEmpty()) { + mode = parent.modes.stream().filter(it -> it.contains(this)).findFirst().orElse(null); + } + if (mode == null && !direct) { + mode = parent.getMode(false); + } + } + return mode; + } ////////////////////////////////////////////////////// //// Protected fields. @@ -293,6 +305,8 @@ protected String getFullNameWithJoiner(String joiner) { // This is not cached because _uniqueID is cached. if (parent == null) { return this.getName(); + } else if (getMode(true) != null) { + return parent.getFullNameWithJoiner(joiner) + joiner + getMode(true).getName() + joiner + this.getName(); } else { return parent.getFullNameWithJoiner(joiner) + joiner + this.getName(); } diff --git a/org.lflang/src/org/lflang/generator/ReactionInstance.java b/org.lflang/src/org/lflang/generator/ReactionInstance.java index 0dbbe64ce2..a0f9d53432 100644 --- a/org.lflang/src/org/lflang/generator/ReactionInstance.java +++ b/org.lflang/src/org/lflang/generator/ReactionInstance.java @@ -173,11 +173,15 @@ public ReactionInstance( "Unexpected effect. Cannot find port " + variable.getName()); } } else if (variable instanceof Action) { + // Effect is an Action. var actionInstance = parent.lookupActionInstance( (Action)variable); this.effects.add(actionInstance); actionInstance.dependsOnReactions.add(this); - } // else it may be an unresolved reference + } else { + // Effect is either a mode or an unresolved reference. + // Do nothing, transitions will be set up by the ModeInstance. + } } // Create a deadline instance if one has been defined. if (this.definition.getDeadline() != null) { diff --git a/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java b/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java index bc77c60939..ccd5090a3e 100644 --- a/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java +++ b/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java @@ -136,7 +136,14 @@ protected void addDownstreamReactions(PortInstance port, ReactionInstance reacti Runtime srcRuntime = srcRuntimes.get(srcIndex); Runtime dstRuntime = dstRuntimes.get(dstIndex); if (dstRuntime != srcRuntime) { - addEdge(dstRuntime, srcRuntime); + // Only add this dependency if the reactions are not in modes at all or in the same mode or in modes of separate reactors + // This allows modes to break cycles since modes are always mutually exclusive. + if (srcRuntime.getReaction().getMode(true) == null || + dstRuntime.getReaction().getMode(true) == null || + srcRuntime.getReaction().getMode(true) == dstRuntime.getReaction().getMode(true) || + srcRuntime.getReaction().getParent() != dstRuntime.getReaction().getParent()) { + addEdge(dstRuntime, srcRuntime); + } } // Propagate the deadlines, if any. @@ -192,8 +199,12 @@ protected void addNodesAndEdges(ReactorInstance reactor) { List previousRuntimes = previousReaction.getRuntimeInstances(); int count = 0; for (Runtime runtime : runtimes) { - this.addEdge(runtime, previousRuntimes.get(count)); - count++; + // Only add the reaction order edge if previous reaction is outside of a mode or both are in the same mode + // This allows modes to break cycles since modes are always mutually exclusive. + if (runtime.getReaction().getMode(true) == null || runtime.getReaction().getMode(true) == reaction.getMode(true)) { + this.addEdge(runtime, previousRuntimes.get(count)); + count++; + } } } previousReaction = reaction; diff --git a/org.lflang/src/org/lflang/generator/ReactorInstance.java b/org.lflang/src/org/lflang/generator/ReactorInstance.java index 81a1d1c508..50be730c05 100644 --- a/org.lflang/src/org/lflang/generator/ReactorInstance.java +++ b/org.lflang/src/org/lflang/generator/ReactorInstance.java @@ -43,6 +43,7 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.lf.Delay; import org.lflang.lf.Input; import org.lflang.lf.Instantiation; +import org.lflang.lf.Mode; import org.lflang.lf.Output; import org.lflang.lf.Parameter; import org.lflang.lf.Port; @@ -149,6 +150,9 @@ public ReactorInstance(Reactor reactor, ReactorInstance parent, ErrorReporter re /** The timer instances belonging to this reactor instance. */ public final List timers = new ArrayList<>(); + + /** The mode instances belonging to this reactor instance. */ + public final List modes = new ArrayList<>(); /** The reactor declaration in the AST. This is either an import or Reactor declaration. */ public final ReactorDecl reactorDeclaration; @@ -612,6 +616,21 @@ public TimerInstance lookupTimerInstance(Timer timer) { } return null; } + + /** Returns the mode instance within this reactor + * instance corresponding to the specified mode reference. + * @param mode The mode as an AST node. + * @return The corresponding mode instance or null if the + * mode does not belong to this reactor. + */ + public ModeInstance lookupModeInstance(Mode mode) { + for (ModeInstance modeInstance : modes) { + if (modeInstance.definition == mode) { + return modeInstance; + } + } + return null; + } /** * Return a descriptive string. @@ -828,6 +847,16 @@ private ReactorInstance( // Note that this can only happen _after_ the children, // port, action, and timer instances have been created. createReactionInstances(); + + // Instantiate modes for this reactor instance + // This must come after the child elements (reactions, etc) of this reactor + // are created in order to allow their association with modes + for (Mode modeDecl : ASTUtils.allModes(reactorDefinition)) { + this.modes.add(new ModeInstance(modeDecl, this)); + } + for (ModeInstance mode : this.modes) { + mode.setupTranstions(); + } } } diff --git a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend index fda61f5d08..e739a4cb07 100644 --- a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend +++ b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend @@ -28,6 +28,7 @@ package org.lflang.generator.c import java.io.File import java.util.ArrayList +import java.util.Collection import java.util.HashSet import java.util.LinkedHashMap import java.util.LinkedHashSet @@ -37,6 +38,7 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.regex.Pattern import org.eclipse.emf.ecore.resource.Resource +import org.eclipse.emf.ecore.util.EcoreUtil import org.eclipse.xtext.util.CancelIndicator import org.lflang.ErrorReporter import org.lflang.FileConfig @@ -44,7 +46,6 @@ import org.lflang.InferredType import org.lflang.JavaAstUtils import org.lflang.Target import org.lflang.TargetConfig -import org.lflang.TargetConfig.Mode import org.lflang.TargetProperty import org.lflang.TargetProperty.ClockSyncMode import org.lflang.TargetProperty.CoordinationType @@ -62,6 +63,7 @@ import org.lflang.generator.GeneratorResult import org.lflang.generator.IntegratedBuilder import org.lflang.generator.JavaGeneratorUtils import org.lflang.generator.LFGeneratorContext +import org.lflang.generator.ModeInstance import org.lflang.generator.PortInstance import org.lflang.generator.ReactionInstance import org.lflang.generator.ReactorInstance @@ -71,9 +73,12 @@ import org.lflang.generator.SubContext import org.lflang.generator.TriggerInstance import org.lflang.lf.Action import org.lflang.lf.ActionOrigin +import org.lflang.lf.Connection import org.lflang.lf.Delay import org.lflang.lf.Input import org.lflang.lf.Instantiation +import org.lflang.lf.LfFactory +import org.lflang.lf.Mode import org.lflang.lf.Model import org.lflang.lf.Output import org.lflang.lf.Port @@ -306,6 +311,7 @@ import static extension org.lflang.JavaAstUtils.* * @author{Christian Menard } * @author{Matt Weber } * @author{Soroush Bateni + * @author{Alexander Schulz-Rosengarten } */ class CGenerator extends GeneratorBase { @@ -685,6 +691,22 @@ class CGenerator extends GeneratorBase { ''') } + // If there are modes, create a table of mode state to be checked for transitions. + if (hasModalReactors) { + code.pr(''' + // Array of pointers to mode states to be handled in _lf_handle_mode_changes(). + reactor_mode_state_t* _lf_modal_reactor_states[«modalReactorCount»]; + int _lf_modal_reactor_states_size = «modalReactorCount»; + ''') + if (modalStateResetCount > 0) { + code.pr(''' + // Array of reset data for state variables nested in modes. Used in _lf_handle_mode_changes(). + mode_state_variable_reset_data_t _lf_modal_state_reset[«modalStateResetCount»]; + int _lf_modal_state_reset_size = «modalStateResetCount»; + ''') + } + } + // Generate function to return a pointer to the action trigger_t // that handles incoming network messages destined to the specified // port. This will only be used if there are federates. @@ -785,6 +807,15 @@ class CGenerator extends GeneratorBase { if (!isFederated) { code.pr("void terminate_execution() {}"); } + + if (hasModalReactors) { + // Generate mode change detection + code.pr(''' + void _lf_handle_mode_changes() { + _lf_process_mode_changes(_lf_modal_reactor_states, _lf_modal_reactor_states_size, «modalStateResetCount > 0 ? "_lf_modal_state_reset" : "NULL"», «modalStateResetCount > 0 ? "_lf_modal_state_reset_size" : 0»); + } + ''') + } } val targetFile = fileConfig.getSrcGenPath() + File.separator + cFilename code.writeToFile(targetFile) @@ -823,7 +854,7 @@ class CGenerator extends GeneratorBase { && targetConfig.buildCommands.nullOrEmpty && !federate.isRemote // This code is unreachable in LSP_FAST mode, so that check is omitted. - && context.getMode() != Mode.LSP_MEDIUM + && context.getMode() != TargetConfig.Mode.LSP_MEDIUM ) { // FIXME: Currently, a lack of main is treated as a request to not produce // a binary and produce a .o file instead. There should be a way to control @@ -910,7 +941,43 @@ class CGenerator extends GeneratorBase { // In case we are in Eclipse, make sure the generated code is visible. JavaGeneratorUtils.refreshProject(resource, context.mode) } - + + override checkModalReactorSupport(boolean _) { + // Modal reactors are currently only supported for non federated applications + super.checkModalReactorSupport(!isFederated); + } + + override transformConflictingConnectionsInModalReactors(Collection transform) { + val factory = LfFactory.eINSTANCE + for (connection : transform) { + // Currently only simple transformations are supported + if (connection.physical || connection.delay !== null || connection.iterated || + connection.leftPorts.size > 1 || connection.rightPorts.size > 1 + ) { + errorReporter.reportError(connection, "Cannot transform connection in modal reactor. Connection uses currently not supported features."); + } else { + var reaction = factory.createReaction(); + (connection.eContainer() as Mode).getReactions().add(reaction); + + var sourceRef = connection.getLeftPorts().head + var destRef = connection.getRightPorts().head + reaction.getTriggers().add(sourceRef); + reaction.getEffects().add(destRef); + + var code = factory.createCode(); + var source = (sourceRef.container !== null ? sourceRef.container.name + "." : "") + sourceRef.variable.name + var dest = (destRef.container !== null ? destRef.container.name + "." : "") + destRef.variable.name + code.setBody(''' + // Generated forwarding reaction for connections with the same destination but located in mutually exclusive modes. + SET(«dest», «source»->value); + '''); + reaction.setCode(code); + + EcoreUtil.remove(connection); + } + } + } + /** * Add files needed for the proper function of the runtime scheduler to * {@code coreFiles} and {@link TargetConfig#compileAdditionalSources}. @@ -1299,17 +1366,15 @@ class CGenerator extends GeneratorBase { val contents = new CodeBuilder() // The Docker configuration uses cmake, so config.compiler is ignored here. var compileCommand = ''' - cmake -S src-gen -B bin && \ - cd bin && \ - make all + RUN set -ex && \ + mkdir bin && \ + cmake -S src-gen -B bin && \ + cd bin && \ + make all ''' if (!targetConfig.buildCommands.nullOrEmpty) { compileCommand = targetConfig.buildCommands.join(' ') } - var additionalFiles = '' - if (!targetConfig.fileNames.nullOrEmpty) { - additionalFiles = '''COPY "«targetConfig.fileNames.join('" "')»" "src-gen/"''' - } var dockerCompiler = CCppMode ? 'g++' : 'gcc' var fileExtension = CCppMode ? 'cpp' : 'c' @@ -1319,14 +1384,8 @@ class CGenerator extends GeneratorBase { FROM «targetConfig.dockerOptions.from» AS builder WORKDIR /lingua-franca/«topLevelName» RUN set -ex && apk add --no-cache «dockerCompiler» musl-dev cmake make - COPY core src-gen/core - COPY ctarget.h ctarget.c src-gen/ - COPY CMakeLists.txt \ - «topLevelName».«fileExtension» src-gen/ - «additionalFiles» - RUN set -ex && \ - mkdir bin && \ - «compileCommand» + COPY . src-gen + «compileCommand» FROM «targetConfig.dockerOptions.from» WORKDIR /lingua-franca @@ -2013,7 +2072,39 @@ class CGenerator extends GeneratorBase { // Next, generate the fields needed for each reaction. generateReactionAndTriggerStructs(body, decl, constructorCode); - + + // Next, generate fields for modes + if (!reactor.allModes.empty) { + // Reactor's mode instances and its state. + body.pr(''' + reactor_mode_t _lf__modes[«reactor.modes.size»]; + reactor_mode_state_t _lf__mode_state; + ''') + + // Initialize the mode instances + constructorCode.pr(''' + // Initialize modes + ''') + for (modeAndIdx : reactor.allModes.indexed) { + val mode = modeAndIdx.value + constructorCode.pr(mode, ''' + self->_lf__modes[«modeAndIdx.key»].state = &self->_lf__mode_state; + self->_lf__modes[«modeAndIdx.key»].name = "«mode.name»"; + self->_lf__modes[«modeAndIdx.key»].deactivation_time = 0; + ''') + } + + // Initialize mode state with initial mode active upon start + constructorCode.pr(''' + // Initialize mode state + self->_lf__mode_state.parent_mode = NULL; + self->_lf__mode_state.initial_mode = &self->_lf__modes[«reactor.modes.indexed.findFirst[it.value.initial].key»]; + self->_lf__mode_state.active_mode = self->_lf__mode_state.initial_mode; + self->_lf__mode_state.next_mode = NULL; + self->_lf__mode_state.mode_change = 0; + ''') + } + // The first field has to always be a pointer to the list of // of allocated memory that must be freed when the reactor is freed. // This means that the struct can be safely cast to self_base_t. @@ -2308,6 +2399,11 @@ class CGenerator extends GeneratorBase { self->_lf__reaction_«reactionCount».deadline_violation_handler = «deadlineFunctionPointer»; self->_lf__reaction_«reactionCount».STP_handler = «STPFunctionPointer»; self->_lf__reaction_«reactionCount».name = "?"; + «IF reaction.eContainer instanceof Mode» + self->_lf__reaction_«reactionCount».mode = &self->_lf__modes[«reactor.modes.indexOf(reaction.eContainer as Mode)»]; + «ELSE» + self->_lf__reaction_«reactionCount».mode = NULL; + «ENDIF» ''') } @@ -2861,6 +2957,15 @@ class CGenerator extends GeneratorBase { «triggerStructName».period = «CGenerator.UNDEFINED_MIN_SPACING»; «ENDIF» ''') + + // Establish connection to enclosing mode + val mode = action.getMode(false) + if (mode !== null) { + val modeRef = '''&«CUtil.reactorRef(mode.parent)»->_lf__modes[«mode.parent.modes.indexOf(mode)»];''' + initializeTriggerObjects.pr('''«triggerStructName».mode = «modeRef»;''') + } else { + initializeTriggerObjects.pr('''«triggerStructName».mode = NULL;''') + } } triggerCount += currentFederate.numRuntimeInstances(action.parent); } @@ -2890,6 +2995,15 @@ class CGenerator extends GeneratorBase { _lf_timer_triggers[_lf_timer_triggers_count++] = &«triggerStructName»; ''') timerCount += currentFederate.numRuntimeInstances(timer.parent); + + // Establish connection to enclosing mode + val mode = timer.getMode(false) + if (mode !== null) { + val modeRef = '''&«CUtil.reactorRef(mode.parent)»->_lf__modes[«mode.parent.modes.indexOf(mode)»];''' + initializeTriggerObjects.pr('''«triggerStructName».mode = «modeRef»;''') + } else { + initializeTriggerObjects.pr('''«triggerStructName».mode = NULL;''') + } } triggerCount += currentFederate.numRuntimeInstances(timer.parent); } @@ -3072,6 +3186,8 @@ class CGenerator extends GeneratorBase { generateInitializeActionToken(instance); generateSetDeadline(instance); + generateModeStructure(instance); + // Recursively generate code for the children. for (child : instance.children) { if (currentFederate.contains(child)) { @@ -3202,10 +3318,17 @@ class CGenerator extends GeneratorBase { def generateStateVariableInitializations(ReactorInstance instance) { val reactorClass = instance.definition.reactorClass val selfRef = CUtil.reactorRef(instance) - for (stateVar : reactorClass.toDefinition.stateVars) { + for (stateVar : reactorClass.toDefinition.allStateVars) { + var ModeInstance mode = null + if (stateVar.eContainer instanceof Mode) { + mode = instance.lookupModeInstance(stateVar.eContainer as Mode) + } else { + mode = instance.getMode(false) + } val initializer = getInitializer(stateVar, instance) if (stateVar.initialized) { + var initializerVar = false if (stateVar.isOfTimeType) { initializeTriggerObjects.pr(selfRef + "->" + stateVar.name + " = " + initializer + ";") } else { @@ -3218,14 +3341,43 @@ class CGenerator extends GeneratorBase { initializeTriggerObjects.pr( selfRef + "->" + stateVar.name + " = " + initializer + ";") } else { + initializerVar = true initializeTriggerObjects.pr(''' { // For scoping static «types.getVariableDeclaration(stateVar.inferredType, "_initial", true)» = «initializer»; «selfRef»->«stateVar.name» = _initial; } // End scoping. - ''' - ) + ''') + } + } + + if (mode !== null) { + val modeRef = '''&«CUtil.reactorRef(mode.parent)»->_lf__modes[«mode.parent.modes.indexOf(mode)»]''' + var type = types.getTargetType(stateVar.inferredType) + initializeTriggerObjects.pr("// Register initial value for reset by mode") + var source = initializer + if (initializerVar) { + source = "_initial" + initializeTriggerObjects.pr(''' + { // For scoping + static «types.getVariableDeclaration(stateVar.inferredType, source, true)» = «initializer»; + «selfRef»->«stateVar.name» = «source»; + ''') + initializeTriggerObjects.indent() + } + initializeTriggerObjects.pr(''' + _lf_modal_state_reset[«modalStateResetCount»].mode = «modeRef»; + _lf_modal_state_reset[«modalStateResetCount»].target = &(«selfRef»->«stateVar.name»); + _lf_modal_state_reset[«modalStateResetCount»].source = &«source»; + _lf_modal_state_reset[«modalStateResetCount»].size = sizeof(«type»); + ''') + if (initializerVar) { + initializeTriggerObjects.unindent() + initializeTriggerObjects.pr(''' + } // End scoping. + ''') } + modalStateResetCount++ } } } @@ -3250,6 +3402,45 @@ class CGenerator extends GeneratorBase { } } + /** + * Generate code to initialize modes. + * @param instance The reactor instance. + */ + private def void generateModeStructure(ReactorInstance instance) { + val parentMode = instance.getMode(false); + val nameOfSelfStruct = CUtil.reactorRef(instance); + // If this instance is enclosed in another mode + if (parentMode !== null) { + val parentModeRef = '''&«CUtil.reactorRef(parentMode.parent)»->_lf__modes[«parentMode.parent.modes.indexOf(parentMode)»]''' + initializeTriggerObjects.pr("// Setup relation to enclosing mode") + + // If this reactor does not have its own modes, all reactions must be linked to enclosing mode + if (instance.modes.empty) { + for (reaction : instance.reactions.indexed) { + initializeTriggerObjects.pr(''' + «CUtil.reactorRef(reaction.value.parent)»->_lf__reaction_«reaction.key».mode = «parentModeRef»; + ''') + } + } else { // Otherwise, only reactions outside modes must be linked and the mode state itself gets a parent relation + initializeTriggerObjects.pr(''' + «nameOfSelfStruct»->_lf__mode_state.parent_mode = «parentModeRef»; + ''') + for (reaction : instance.reactions.filter[it.getMode(true) === null]) { + initializeTriggerObjects.pr(''' + «CUtil.reactorRef(reaction.parent)»->_lf__reaction_«instance.reactions.indexOf(reaction)».mode = «parentModeRef»; + ''') + } + } + } + // If this reactor has modes, register for mode change handling + if (!instance.modes.empty) { + initializeTriggerObjects.pr(''' + // Register for transition handling + _lf_modal_reactor_states[«modalReactorCount++»] = &«nameOfSelfStruct»->_lf__mode_state; + ''') + } + } + /** * Generate runtime initialization code for parameters of a given reactor instance * @param instance The reactor instance. @@ -3943,6 +4134,12 @@ class CGenerator extends GeneratorBase { targetConfig.threads = CUtil.minThreadsToHandleInputPorts(federates) } + if (hasModalReactors) { + code.pr(''' + #define MODAL_REACTORS + ''') + } + includeTargetLanguageHeaders() code.pr(CPreambleGenerator.generateNumFederatesDirective(federates.size)); code.pr('#define TARGET_FILES_DIRECTORY "' + fileConfig.srcGenPath + '"'); @@ -5574,6 +5771,8 @@ class CGenerator extends GeneratorBase { var timerCount = 0 var startupReactionCount = 0 var shutdownReactionCount = 0 + var modalReactorCount = 0 + var modalStateResetCount = 0 // For each reactor, we collect a set of input and parameter names. var triggerCount = 0 diff --git a/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java b/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java index e64c787ca3..0b45d9f647 100644 --- a/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java @@ -1,5 +1,7 @@ package org.lflang.generator.c; +import static org.lflang.generator.c.CUtil.generateWidthVariable; + import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -11,9 +13,11 @@ import org.lflang.InferredType; import org.lflang.JavaAstUtils; import org.lflang.generator.CodeBuilder; +import org.lflang.generator.ModeInstance.ModeTransitionType; import org.lflang.lf.Action; import org.lflang.lf.Input; import org.lflang.lf.Instantiation; +import org.lflang.lf.Mode; import org.lflang.lf.Output; import org.lflang.lf.Port; import org.lflang.lf.Reaction; @@ -23,8 +27,6 @@ import org.lflang.lf.VarRef; import org.lflang.lf.Variable; -import static org.lflang.generator.c.CUtil.generateWidthVariable; - public class CReactionGenerator { /** @@ -153,6 +155,23 @@ public static String generateInitializationForReaction(String body, if (!actionsAsTriggers.contains(effect.getVariable())) { reactionInitialization.pr(CGenerator.variableStructType(variable, decl)+"* "+variable.getName()+" = &self->_lf_"+variable.getName()+";"); } + } else if (effect.getVariable() instanceof Mode) { + // Mode change effect + int idx = ASTUtils.allModes(reactor).indexOf((Mode)effect.getVariable()); + String name = effect.getVariable().getName(); + if (idx >= 0) { + reactionInitialization.pr( + "reactor_mode_t* " + name + " = &self->_lf__modes[" + idx + "];\n" + + "char _lf_" + name + "_change_type = " + + (ModeTransitionType.getModeTransitionType(effect) == ModeTransitionType.HISTORY ? 2 : 1) + + ";" + ); + } else { + errorReporter.reportError( + reaction, + "In generateReaction(): " + name + " not a valid mode of this reactor." + ); + } } else { if (variable instanceof Output) { reactionInitialization.pr(generateOutputVariablesInReaction( diff --git a/org.lflang/src/org/lflang/graph/InstantiationGraph.java b/org.lflang/src/org/lflang/graph/InstantiationGraph.java index 514eb9c61f..ed50fb4817 100644 --- a/org.lflang/src/org/lflang/graph/InstantiationGraph.java +++ b/org.lflang/src/org/lflang/graph/InstantiationGraph.java @@ -24,14 +24,11 @@ */ package org.lflang.graph; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Iterables; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.stream.StreamSupport; import org.eclipse.emf.ecore.resource.Resource; import org.lflang.ASTUtils; @@ -41,6 +38,9 @@ import org.lflang.lf.ReactorDecl; import org.lflang.util.IteratorUtil; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Iterables; + /** * A graph with vertices that are Reactors (not ReactorInstances) and edges that denote * dependencies between them. A "dependency" from reactor class A to @@ -156,7 +156,7 @@ public InstantiationGraph(final Model model, final boolean detectCycles) { private void buildGraph(final Instantiation instantiation, final Set visited) { final ReactorDecl decl = instantiation.getReactorClass(); final Reactor reactor = ASTUtils.toDefinition(decl); - final Reactor container = (Reactor) instantiation.eContainer(); + Reactor container = ASTUtils.getEnclosingReactor(instantiation); if (visited.add(instantiation)) { this.reactorToInstantiation.put(reactor, instantiation); this.reactorToDecl.put(reactor, decl); diff --git a/org.lflang/src/org/lflang/scoping/LFScopeProviderImpl.java b/org.lflang/src/org/lflang/scoping/LFScopeProviderImpl.java index 9e70493350..ae268a9327 100644 --- a/org.lflang/src/org/lflang/scoping/LFScopeProviderImpl.java +++ b/org.lflang/src/org/lflang/scoping/LFScopeProviderImpl.java @@ -51,6 +51,7 @@ import org.lflang.lf.ReactorDecl; import org.lflang.lf.VarRef; import org.lflang.lf.LfPackage; +import org.lflang.lf.Mode; /** * This class enforces custom rules. In particular, it resolves references to @@ -128,12 +129,15 @@ protected IScope getScopeForImportedReactor(ImportedReactor context, EReference protected IScope getScopeForReactorDecl(EObject obj, EReference reference) { // Find the local Model - Model model; - if (obj.eContainer() instanceof Model) { - model = (Model) obj.eContainer(); - } else if (obj.eContainer().eContainer() instanceof Model) { - model = (Model) obj.eContainer().eContainer(); - } else { + Model model = null; + EObject container = obj; + while(model == null && container != null) { + container = container.eContainer(); + if (container instanceof Model) { + model = (Model)container; + } + } + if (model == null) { return Scopes.scopeFor(emptyList()); } @@ -173,8 +177,12 @@ protected IScope getScopeForVarRef(VarRef variable, EReference reference) { if (reference == LfPackage.Literals.VAR_REF__VARIABLE) { // Resolve hierarchical reference Reactor reactor; + Mode mode = null; if (variable.eContainer().eContainer() instanceof Reactor) { reactor = (Reactor) variable.eContainer().eContainer(); + } else if (variable.eContainer().eContainer() instanceof Mode) { + mode = (Mode) variable.eContainer().eContainer(); + reactor = (Reactor) variable.eContainer().eContainer().eContainer(); } else { return Scopes.scopeFor(emptyList()); } @@ -183,7 +191,10 @@ protected IScope getScopeForVarRef(VarRef variable, EReference reference) { if (variable.getContainer() != null) { // Resolve hierarchical port reference var instanceName = nameProvider.getFullyQualifiedName(variable.getContainer()); - var instances = reactor.getInstantiations(); + var instances = new ArrayList(reactor.getInstantiations()); + if (mode != null) { + instances.addAll(mode.getInstantiations()); + } if (instanceName != null) { for (var instance : instances) { @@ -208,6 +219,10 @@ protected IScope getScopeForVarRef(VarRef variable, EReference reference) { switch (type) { case TRIGGER: { var candidates = new ArrayList(); + if (mode != null) { + candidates.addAll(mode.getActions()); + candidates.addAll(mode.getTimers()); + } candidates.addAll(allInputs(reactor)); candidates.addAll(allActions(reactor)); candidates.addAll(allTimers(reactor)); @@ -217,6 +232,10 @@ protected IScope getScopeForVarRef(VarRef variable, EReference reference) { return super.getScope(variable, reference); case EFFECT: { var candidates = new ArrayList(); + if (mode != null) { + candidates.addAll(mode.getActions()); + candidates.addAll(reactor.getModes()); + } candidates.addAll(allOutputs(reactor)); candidates.addAll(allActions(reactor)); return Scopes.scopeFor(candidates); diff --git a/org.lflang/src/org/lflang/validation/LFValidator.java b/org.lflang/src/org/lflang/validation/LFValidator.java index 1b72a035f8..a655db04e8 100644 --- a/org.lflang/src/org/lflang/validation/LFValidator.java +++ b/org.lflang/src/org/lflang/validation/LFValidator.java @@ -63,6 +63,7 @@ import org.lflang.TargetProperty; import org.lflang.TimeValue; import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.ModeInstance.ModeTransitionType; import org.lflang.generator.NamedInstance; import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; @@ -79,6 +80,7 @@ import org.lflang.lf.KeyValuePair; import org.lflang.lf.KeyValuePairs; import org.lflang.lf.LfPackage.Literals; +import org.lflang.lf.Mode; import org.lflang.lf.Model; import org.lflang.lf.NamedHost; import org.lflang.lf.Output; @@ -219,7 +221,8 @@ public void checkConnection(Connection connection) { if ((rp.getContainer() == null && it.getDefinition().equals(rp.getVariable())) || (it.getDefinition().equals(rp.getVariable()) && it.getParent().equals(rp.getContainer()))) { if (leftInCycle) { - String reactorName = ((Reactor) connection.eContainer()).getName(); + Reactor reactor = ASTUtils.getEnclosingReactor(connection); + String reactorName = reactor.getName(); error(String.format("Connection in reactor %s creates", reactorName) + String.format("a cyclic dependency between %s and %s.", toText(lp), toText(rp)), Literals.CONNECTION__DELAY); @@ -311,19 +314,21 @@ public void checkConnection(Connection connection) { } } - Reactor reactor = (Reactor) connection.eContainer(); + Reactor reactor = ASTUtils.getEnclosingReactor(connection); // Make sure the right port is not already an effect of a reaction. - for (Reaction reaction : reactor.getReactions()) { + for (Reaction reaction : ASTUtils.allReactions(reactor)) { for (VarRef effect : reaction.getEffects()) { for (VarRef rightPort : connection.getRightPorts()) { - if ((rightPort.getContainer() == null && effect.getContainer() == null || - rightPort.getContainer().equals(effect.getContainer())) && - rightPort.getVariable().equals(effect.getVariable())) { + if (rightPort.getVariable().equals(effect.getVariable()) && // Refers to the same variable + rightPort.getContainer() == effect.getContainer() && // Refers to the same instance + ( reaction.eContainer() instanceof Reactor || // Either is not part of a mode + connection.eContainer() instanceof Reactor || + connection.eContainer() == reaction.eContainer() // Or they are in the same mode + )) { error("Cannot connect: Port named '" + effect.getVariable().getName() + "' is already effect of a reaction.", - Literals.CONNECTION__RIGHT_PORTS - ); + Literals.CONNECTION__RIGHT_PORTS); } } } @@ -335,9 +340,12 @@ public void checkConnection(Connection connection) { if (c != connection) { for (VarRef thisRightPort : connection.getRightPorts()) { for (VarRef thatRightPort : c.getRightPorts()) { - if ((thisRightPort.getContainer() == null && thatRightPort.getContainer() == null || - thisRightPort.getContainer().equals(thatRightPort.getContainer())) && - thisRightPort.getVariable().equals(thatRightPort.getVariable())) { + if (thisRightPort.getVariable().equals(thatRightPort.getVariable()) && // Refers to the same variable + thisRightPort.getContainer() == thatRightPort.getContainer() && // Refers to the same instance + ( connection.eContainer() instanceof Reactor || // Or either of the connections in not part of a mode + c.eContainer() instanceof Reactor || + connection.eContainer() == c.eContainer() // Or they are in the same mode + )) { error( "Cannot connect: Port named '" + thisRightPort.getVariable().getName() + "' may only appear once on the right side of a connection.", @@ -741,7 +749,7 @@ public void checkReaction(Reaction reaction) { // // Report error if this reaction is part of a cycle. Set> cycles = this.info.topologyCycles(); - Reactor reactor = (Reactor) reaction.eContainer(); + Reactor reactor = ASTUtils.getEnclosingReactor(reaction); boolean reactionInCycle = false; for (NamedInstance it : cycles) { if (it.getDefinition().equals(reaction)) { @@ -1247,6 +1255,97 @@ public void checkWidthSpec(WidthSpec widthSpec) { } } } + + @Check(CheckType.FAST) + public void checkModeModifier(VarRef ref) { + if (ref.getVariable() instanceof Mode && ref.getModifier() != null && !ModeTransitionType.KEYWORDS.contains(ref.getModifier())) { + error(String.format("Illegal mode transition modifier! Only %s is allowed.", + String.join("/", ModeTransitionType.KEYWORDS)), Literals.VAR_REF__MODIFIER); + } + } + + @Check(CheckType.FAST) + public void checkInitialMode(Reactor reactor) { + if (!reactor.getModes().isEmpty()) { + long initialModesCount = reactor.getModes().stream().filter(m -> m.isInitial()).count(); + if (initialModesCount == 0) { + error("Every modal reactor requires one initial mode.", Literals.REACTOR__MODES, 0); + } else if (initialModesCount > 1) { + reactor.getModes().stream().filter(m -> m.isInitial()).skip(1).forEach(m -> { + error("A modal reactor can only have one initial mode.", + Literals.REACTOR__MODES, reactor.getModes().indexOf(m)); + }); + } + } + } + + @Check(CheckType.FAST) + public void checkModeStateNamespace(Reactor reactor) { + if (!reactor.getModes().isEmpty()) { + var names = new ArrayList(); + reactor.getStateVars().stream().map(it -> it.getName()).forEach(it -> names.add(it)); + for (var mode : reactor.getModes()) { + for (var stateVar : mode.getStateVars()) { + if (names.contains(stateVar.getName())) { + error(String.format("Duplicate StateVar '%s' in Reactor '%s'. (State variables are currently scoped on reactor level not modes)", + stateVar.getName(), reactor.getName()), stateVar, Literals.STATE_VAR__NAME); + } + names.add(stateVar.getName()); + } + } + } + } + + @Check(CheckType.FAST) + public void checkModeTimerNamespace(Reactor reactor) { + if (!reactor.getModes().isEmpty()) { + var names = new ArrayList(); + reactor.getTimers().stream().map(it -> it.getName()).forEach(it -> names.add(it)); + for (var mode : reactor.getModes()) { + for (var timer : mode.getTimers()) { + if (names.contains(timer.getName())) { + error(String.format("Duplicate Timer '%s' in Reactor '%s'. (Timers are currently scoped on reactor level not modes)", + timer.getName(), timer.getName()), timer, Literals.STATE_VAR__NAME); + } + names.add(timer.getName()); + } + } + } + } + + @Check(CheckType.FAST) + public void checkModeActionNamespace(Reactor reactor) { + if (!reactor.getModes().isEmpty()) { + var names = new ArrayList(); + reactor.getActions().stream().map(it -> it.getName()).forEach(it -> names.add(it)); + for (var mode : reactor.getModes()) { + for (var action : mode.getActions()) { + if (names.contains(action.getName())) { + error(String.format("Duplicate Action '%s' in Reactor '%s'. (Actions are currently scoped on reactor level not modes)", + action.getName(), action.getName()), action, Literals.STATE_VAR__NAME); + } + names.add(action.getName()); + } + } + } + } + + @Check(CheckType.FAST) + public void checkModeInstanceNamespace(Reactor reactor) { + if (!reactor.getModes().isEmpty()) { + var names = new ArrayList(); + reactor.getActions().stream().map(it -> it.getName()).forEach(it -> names.add(it)); + for (var mode : reactor.getModes()) { + for (var instantiation : mode.getInstantiations()) { + if (names.contains(instantiation.getName())) { + error(String.format("Duplicate Instantiation '%s' in Reactor '%s'. (Instantiations are currently scoped on reactor level not modes)", + instantiation.getName(), instantiation.getName()), instantiation, Literals.STATE_VAR__NAME); + } + names.add(instantiation.getName()); + } + } + } + } ////////////////////////////////////////////////////////////// //// Public methods. diff --git a/test/C/src/docker/FilesPropertyContainerized.lf b/test/C/src/docker/FilesPropertyContainerized.lf new file mode 100644 index 0000000000..f9a1a240f3 --- /dev/null +++ b/test/C/src/docker/FilesPropertyContainerized.lf @@ -0,0 +1,15 @@ +target C { + files: "../include/hello.h", + docker: true +} + +preamble {= + #include "hello.h" +=} + +main reactor { + reaction(startup) {= + hello_t hello; + printf("SUCCESS\n"); + =} +} diff --git a/test/C/src/modal_models/ConvertCaseTest.lf b/test/C/src/modal_models/ConvertCaseTest.lf new file mode 100644 index 0000000000..78bff8c11a --- /dev/null +++ b/test/C/src/modal_models/ConvertCaseTest.lf @@ -0,0 +1,164 @@ +/* + * Modal Reactor Test. + * Tests nested reactors with modes. + */ +target C { + fast: false, + timeout: 4 sec, +// logging: debug +} + +import TraceTesting from "util/TraceTesting.lf" + +reactor ResetProcessor { + input discard:bool; + input character:char; + output converted:int; + + initial mode Converting { + converter = new Converter(); + character -> converter.raw; + converter.converted -> converted; + + reaction(discard) -> Discarding {= + SET_MODE(Discarding); + =} + } + + mode Discarding { + reaction(character) -> converted {= + SET(converted, '_'); + =} + + reaction(character) -> Converting {= + SET_MODE(Converting); + =} + } +} + +reactor HistoryProcessor { + input discard:bool; + input character:char; + output converted:int; + + initial mode Converting { + converter = new Converter(); + character -> converter.raw; + converter.converted -> converted; + + reaction(discard) -> Discarding {= + SET_MODE(Discarding); + =} + } + + mode Discarding { + reaction(character) -> converted {= + SET(converted, '_'); + =} + + reaction(character) -> continue(Converting) {= + SET_MODE(Converting); + =} + } +} + +reactor Converter { + input raw:char; + output converted:int; + + preamble {= + #include + =} + + initial mode Upper { + reaction(raw) -> converted, Lower {= + char c = raw->value; + if (c >= 'a' && c <= 'z') { + SET(converted, c - 32); + } else { + SET(converted, c); + } + if (c == ' ') { + SET_MODE(Lower); + } + =} + } + mode Lower { + reaction(raw) -> converted, Upper {= + char c = raw->value; + if (c >= 'A' && c <= 'Z') { + SET(converted, c + 32); + } else { + SET(converted, c); + } + if (c == ' ') { + SET_MODE(Upper); + } + =} + } +} + +reactor InputFeeder(message:string("")) { + output character:char; + state idx:int(0); + + timer t(0, 250msec); + + preamble {= + #include + =} + + reaction(t) -> character {= + if (self->idx < strlen(self->message)) { + SET(character, *(self->message + self->idx)); + self->idx++; + } + =} +} + +main reactor { + timer stepper(500msec, 1sec) + + feeder = new InputFeeder(message="Hello World!") + reset_processor = new ResetProcessor() + history_processor = new HistoryProcessor() + + feeder.character -> reset_processor.character + feeder.character -> history_processor.character + + test = new TraceTesting( + events_size = 2, + trace_size = 60, + trace = ( + 0,1,72,1,72, + 250000000,1,69,1,69, + 250000000,1,76,1,76, + 250000000,1,95,1,95, + 250000000,1,79,1,79, + 250000000,1,32,1,32, + 250000000,1,119,1,119, + 250000000,1,95,1,95, + 250000000,1,82,1,114, + 250000000,1,76,1,108, + 250000000,1,68,1,100, + 250000000,1,95,1,95 + ), training = false) + + // Trigger mode change + reaction(stepper) -> reset_processor.discard, history_processor.discard {= + SET(reset_processor.discard, true); + SET(history_processor.discard, true); + =} + + reaction(reset_processor.converted) {= + printf("Reset: %c\n", reset_processor.converted->value); + =} + + reaction(history_processor.converted) {= + printf("History: %c\n", history_processor.converted->value); + =} + + reset_processor.converted, + history_processor.converted + -> test.events +} \ No newline at end of file diff --git a/test/C/src/modal_models/Count3Modes.lf b/test/C/src/modal_models/Count3Modes.lf new file mode 100644 index 0000000000..4123235778 --- /dev/null +++ b/test/C/src/modal_models/Count3Modes.lf @@ -0,0 +1,63 @@ +/* + * Modal Reactor Test. + * Tests cycling through modes. + */ +target C { + fast: false, + timeout: 2 sec +}; + +reactor CounterCycle { + input next:bool; + output count:int; + + initial mode One { + reaction(next) -> count, Two {= + SET(count, 1); + SET_MODE(Two); + =} + } + mode Two { + reaction(next) -> count, Three {= + SET(count, 2); + SET_MODE(Three); + =} + } + mode Three { + reaction(next) -> count, One {= + SET(count, 3); + SET_MODE(One); + =} + } +} + +main reactor { + timer stepper(0, 250msec); + counter = new CounterCycle(); + + state expected_value:int(1); + + // Trigger + reaction(stepper) -> counter.next {= + SET(counter.next, true); + =} + + // Check + reaction(stepper) counter.count {= + printf("%d\n", counter.count->value); + + if (!counter.count->is_present) { + printf("ERROR: Missing mode change.\n"); + exit(1); + } else if (counter.count->value != self->expected_value) { + printf("ERROR: Wrong mode.\n"); + exit(2); + } + + if (self->expected_value == 3) { + self->expected_value = 1; + } else { + self->expected_value++; + } + =} +} \ No newline at end of file diff --git a/test/C/src/modal_models/ModalActions.lf b/test/C/src/modal_models/ModalActions.lf new file mode 100644 index 0000000000..464bc27d10 --- /dev/null +++ b/test/C/src/modal_models/ModalActions.lf @@ -0,0 +1,99 @@ +/* + * Modal Reactor Test. + * Tests actions, their suspension during mode inactivity and continuation of delays with history transitions. + */ +target C { + fast: false, + timeout: 4 sec +}; + +import TraceTesting from "util/TraceTesting.lf" + +reactor Modal { + input next:bool + + output mode_switch:int + output action1_sched:int + output action1_exec:int + output action2_sched:int + output action2_exec:int + + initial mode One { + timer T1(0, 750msec) + logical action delay1(500msec) + + reaction(T1) -> delay1, action1_sched {= + printf("Scheduled Action\n"); + schedule(delay1, 0); + SET(action1_sched, 1); + =} + reaction(delay1) -> action1_exec {= + printf("Executed Action\n"); + SET(action1_exec, 1); + =} + + reaction(next) -> reset(Two), mode_switch {= + printf("Transitioning to mode Two (reset)\n"); + SET(mode_switch, 1); + SET_MODE(Two); + =} + } + mode Two { + timer T2(0, 750msec) + logical action delay2(500msec) + + reaction(T2) -> delay2, action2_sched {= + printf("Scheduled Action2\n"); + schedule(delay2, 0); + SET(action2_sched, 1); + =} + reaction(delay2) -> action2_exec {= + printf("Executed Action2\n"); + SET(action2_exec, 1); + =} + + reaction(next) -> continue(One), mode_switch {= + printf("Transitioning to mode One (continue)\n"); + SET(mode_switch, 1); + SET_MODE(One); + =} + } +} + +main reactor { + timer stepper(1sec, 1sec) + + modal = new Modal() + test = new TraceTesting( + events_size = 5, + trace_size = 165, + trace = ( + 0,0,0,1,1,0,0,0,0,0,0, + 500000000,0,0,0,1,1,1,0,0,0,0, + 250000000,0,0,1,1,0,1,0,0,0,0, + 250000000,1,1,0,1,0,1,0,0,0,0, + 0,0,1,0,1,0,1,1,1,0,0, + 500000000,0,1,0,1,0,1,0,1,1,1, + 250000000,0,1,0,1,0,1,1,1,0,1, + 250000000,1,1,0,1,0,1,0,1,0,1, + 250000000,0,1,0,1,1,1,0,1,0,1, + 250000000,0,1,1,1,0,1,0,1,0,1, + 500000000,1,1,0,1,1,1,0,1,0,1, + 0,0,1,0,1,0,1,1,1,0,1, + 500000000,0,1,0,1,0,1,0,1,1,1, + 250000000,0,1,0,1,0,1,1,1,0,1, + 250000000,1,1,0,1,0,1,0,1,0,1 + ), training = false) + + // Trigger mode change + reaction(stepper) -> modal.next {= + SET(modal.next, true); + =} + + modal.mode_switch, + modal.action1_sched, + modal.action1_exec, + modal.action2_sched, + modal.action2_exec + -> test.events +} \ No newline at end of file diff --git a/test/C/src/modal_models/ModalAfter.lf b/test/C/src/modal_models/ModalAfter.lf new file mode 100644 index 0000000000..b63c06ef9a --- /dev/null +++ b/test/C/src/modal_models/ModalAfter.lf @@ -0,0 +1,106 @@ +/* + * Modal Reactor Test. + * Tests after delays, its suspension during mode inactivity and continuation with history transitions. + */ +target C { + fast: false, + timeout: 4 sec +}; + +import TraceTesting from "util/TraceTesting.lf" + +reactor Modal { + input next:bool + + output mode_switch:int + output produced1:int + output consumed1:int + output produced2:int + output consumed2:int + + initial mode One { + producer1 = new Producer(mode_id=1) + consumer1 = new Consumer(mode_id=1) + producer1.product -> produced1 + producer1.product -> consumer1.product after 500msec; + consumer1.report -> consumed1; + + reaction(next) -> reset(Two), mode_switch {= + printf("Transitioning to mode Two (reset)\n"); + SET(mode_switch, 1); + SET_MODE(Two); + =} + } + mode Two { + producer2 = new Producer(mode_id=2) + consumer2 = new Consumer(mode_id=2) + producer2.product -> produced2 + producer2.product -> consumer2.product after 500msec; + consumer2.report -> consumed2; + + reaction(next) -> continue(One), mode_switch {= + printf("Transitioning to mode One (continue)\n"); + SET(mode_switch, 1); + SET_MODE(One); + =} + } +} + +reactor Producer(mode_id:int(0)) { + output product:int + + timer t(0, 750msec) + + reaction(t) -> product {= + printf("Produced in %d\n", self->mode_id); + SET(product, 1); + =} +} + +reactor Consumer(mode_id:int(0)) { + input product:int + output report:int + + reaction(product) -> report {= + printf("Consumed in %d\n", self->mode_id); + SET(report, 1); + =} +} + +main reactor { + timer stepper(1sec, 1sec) + + modal = new Modal() + test = new TraceTesting( + events_size = 5, + trace_size = 165, + trace = ( + 0,0,0,1,1,0,0,0,0,0,0, + 500000000,0,0,0,1,1,1,0,0,0,0, + 250000000,0,0,1,1,0,1,0,0,0,0, + 250000000,1,1,0,1,0,1,0,0,0,0, + 0,0,1,0,1,0,1,1,1,0,0, + 500000000,0,1,0,1,0,1,0,1,1,1, + 250000000,0,1,0,1,0,1,1,1,0,1, + 250000000,1,1,0,1,0,1,0,1,0,1, + 250000000,0,1,0,1,1,1,0,1,0,1, + 250000000,0,1,1,1,0,1,0,1,0,1, + 500000000,1,1,0,1,1,1,0,1,0,1, + 0,0,1,0,1,0,1,1,1,0,1, + 500000000,0,1,0,1,0,1,0,1,1,1, + 250000000,0,1,0,1,0,1,1,1,0,1, + 250000000,1,1,0,1,0,1,0,1,0,1 + ), training = false) + + // Trigger mode change + reaction(stepper) -> modal.next {= + SET(modal.next, true); + =} + + modal.mode_switch, + modal.produced1, + modal.consumed1, + modal.produced2, + modal.consumed2 + -> test.events +} \ No newline at end of file diff --git a/test/C/src/modal_models/ModalCycleBreaker.lf b/test/C/src/modal_models/ModalCycleBreaker.lf new file mode 100644 index 0000000000..b4e1b8af8f --- /dev/null +++ b/test/C/src/modal_models/ModalCycleBreaker.lf @@ -0,0 +1,84 @@ +/* + * Modal Reactor Test. + * + * Tests if connections in the same reactor that have the same destination work if they are located in separate modes. + */ +target C { + fast: false, + timeout: 1 sec +}; + +import TraceTesting from "util/TraceTesting.lf" + +reactor Modal { + input in1:int; + input in2:int; + output out:int; + + mode Two { // Defining reaction to in2 before in1 would cause cycle if no mode were present + timer wait(150msec, 1sec) + + reaction(in2) -> out {= + SET(out, in2->value); + =} + + reaction(wait) -> One {= + SET_MODE(One); + printf("Switching to mode One\n"); + =} + } + initial mode One { + reaction(in1) -> out {= + SET(out, in1->value); + =} + + reaction(in1) -> Two {= + if (in1->value % 5 == 4) { + SET_MODE(Two); + printf("Switching to mode Two\n"); + } + =} + } +} + +reactor Counter(period:time(1sec)) { + output value:int + + timer t(0, period) + state curval:int(0) + + reaction(t) -> value {= + SET(value, self->curval++); + =} +} + +main reactor { + counter = new Counter(period=100msec) + modal = new Modal() + test = new TraceTesting( + events_size = 1, + trace_size = 27, + trace = ( + 0,1,0, + 100000000,1,1, + 100000000,1,2, + 100000000,1,3, + 100000000,1,4, + 200000000,1,6, + 100000000,1,7, + 100000000,1,8, + 100000000,1,9 + ), training = false) + + + counter.value -> modal.in1 + modal.out -> modal.in2 + + modal.out + -> test.events + + // Print + reaction(modal.out) {= + printf("%d\n", modal.out->value); + =} +} \ No newline at end of file diff --git a/test/C/src/modal_models/ModalNestedReactions.lf b/test/C/src/modal_models/ModalNestedReactions.lf new file mode 100644 index 0000000000..2168298074 --- /dev/null +++ b/test/C/src/modal_models/ModalNestedReactions.lf @@ -0,0 +1,78 @@ +/** + * Modal Reactor Test. + * Checks disabling of reactions indirectly nested in an inactive mode + */ +target C { + fast: false, + timeout: 2 sec +}; + +reactor CounterCycle { + input next:bool; + + output count:int; + output only_in_two:bool; + output never:int; + + initial mode One { + reaction(next) -> count, Two {= + SET(count, 1); + SET_MODE(Two); + =} + } + mode Two { + fwd = new Forward() + next -> fwd.in + fwd.out -> only_in_two + + reaction(next) -> count, One {= + SET(count, 2); + SET_MODE(One); + =} + } + mode Three { + reaction(next) -> never {= + SET(never, true); + =} + } +} + +reactor Forward { + input in:bool; + output out:bool; + + reaction(in) -> out {= + SET(out, in->value); + =} +} + +main reactor { + timer stepper(0, 250msec) + counter = new CounterCycle() + + // Trigger + reaction(stepper) -> counter.next {= + SET(counter.next, true); + =} + + // Check + reaction(stepper) counter.count, counter.only_in_two {= + printf("%d\n", counter.count->value); + + if (!counter.count->is_present) { + printf("ERROR: Missing mode change.\n"); + exit(1); + } else if (counter.only_in_two->is_present && counter.count->value != 2) { + printf("ERROR: Indirectly nested reaction was not properly deactivated.\n"); + exit(2); + } else if (!counter.only_in_two->is_present && counter.count->value == 2) { + printf("ERROR: Missing output from indirectly nested reaction.\n"); + exit(3); + } + =} + + reaction(counter.never) {= + printf("ERROR: Detected output from unreachable mode.\n"); + exit(4); + =} +} diff --git a/test/C/src/modal_models/ModalStateReset.lf b/test/C/src/modal_models/ModalStateReset.lf new file mode 100644 index 0000000000..4caacfad58 --- /dev/null +++ b/test/C/src/modal_models/ModalStateReset.lf @@ -0,0 +1,98 @@ +/* + * Modal Reactor Test. + * Tests reset of state variables in modes. + */ +target C { + fast: false, + timeout: 4 sec +}; + +import TraceTesting from "util/TraceTesting.lf" + +reactor Modal { + input next:bool; + + output mode_switch:int + output count0:int; + output count1:int; + output count2:int; + + state counter0:int(0); + + reaction(next) -> count0 {= + printf("Counter0: %d\n", self->counter0); + SET(count0, self->counter0++); + =} + + initial mode One { + state counter1:int(0); + timer T1(0msec, 250msec); + + reaction(T1) -> count1 {= + printf("Counter1: %d\n", self->counter1); + SET(count1, self->counter1++); + =} + + reaction(next) -> reset(Two), mode_switch {= + printf("Transitioning to mode Two (reset)\n"); + SET(mode_switch, 1); + SET_MODE(Two); + =} + } + mode Two { + state counter2:int(-2); + timer T2(0msec, 250msec); + + reaction(T2) -> count2 {= + printf("Counter2: %d\n", self->counter2); + SET(count2, self->counter2++); + =} + + reaction(next) -> continue(One), mode_switch {= + printf("Transitioning to mode One (continue)\n"); + SET(mode_switch, 1); + SET_MODE(One); + =} + } +} + +main reactor { + timer stepper(1sec, 1sec) + + modal = new Modal() + test = new TraceTesting( + events_size = 4, + trace_size = 171, + trace = ( + 0,0,0,0,0,1,0,0,0, + 250000000,0,0,0,0,1,1,0,0, + 250000000,0,0,0,0,1,2,0,0, + 250000000,0,0,0,0,1,3,0,0, + 250000000,1,1,1,0,1,4,0,0, + 0,0,1,0,0,0,4,1,-2, + 250000000,0,1,0,0,0,4,1,-1, + 250000000,0,1,0,0,0,4,1,0, + 250000000,0,1,0,0,0,4,1,1, + 250000000,1,1,1,1,0,4,1,2, + 250000000,0,1,0,1,1,5,0,2, + 250000000,0,1,0,1,1,6,0,2, + 250000000,0,1,0,1,1,7,0,2, + 250000000,1,1,1,2,1,8,0,2, + 0,0,1,0,2,0,8,1,-2, + 250000000,0,1,0,2,0,8,1,-1, + 250000000,0,1,0,2,0,8,1,0, + 250000000,0,1,0,2,0,8,1,1, + 250000000,1,1,1,3,0,8,1,2 + ), training = false) + + // Trigger mode change + reaction(stepper) -> modal.next {= + SET(modal.next, true); + =} + + modal.mode_switch, + modal.count0, + modal.count1, + modal.count2 + -> test.events +} \ No newline at end of file diff --git a/test/C/src/modal_models/ModalTimers.lf b/test/C/src/modal_models/ModalTimers.lf new file mode 100644 index 0000000000..57805e5950 --- /dev/null +++ b/test/C/src/modal_models/ModalTimers.lf @@ -0,0 +1,79 @@ +/* + * Modal Reactor Test. + * Tests timers, their deactivation and reset in modes. + */ +target C { + fast: false, + timeout: 4 sec +}; + +import TraceTesting from "util/TraceTesting.lf" + +reactor Modal { + input next:bool + + output mode_switch:int + output timer1:int + output timer2:int + + initial mode One { + timer T1(0, 750msec) + + reaction(T1) -> timer1 {= + printf("T1\n"); + SET(timer1, 1); + =} + + reaction(next) -> reset(Two), mode_switch {= + printf("Transitioning to mode Two (reset)\n"); + SET(mode_switch, 1); + SET_MODE(Two); + =} + } + mode Two { + timer T2(0, 750msec) + + reaction(T2) -> timer2 {= + printf("T2\n"); + SET(timer2, 1); + =} + + reaction(next) -> continue(One), mode_switch {= + printf("Transitioning to mode One (continue)\n"); + SET(mode_switch, 1); + SET_MODE(One); + =} + } +} + +main reactor { + timer stepper(1sec, 1sec) + + modal = new Modal() + test = new TraceTesting( + events_size = 3, + trace_size = 77, + trace = ( + 0,0,0,1,1,0,0, + 750000000,0,0,1,1,0,0, + 250000000,1,1,0,1,0,0, + 0,0,1,0,1,1,1, + 750000000,0,1,0,1,1,1, + 250000000,1,1,0,1,0,1, + 500000000,0,1,1,1,0,1, + 500000000,1,1,0,1,0,1, + 0,0,1,0,1,1,1, + 750000000,0,1,0,1,1,1, + 250000000,1,1,0,1,0,1 + ), training = false) + + // Trigger mode change + reaction(stepper) -> modal.next {= + SET(modal.next, true); + =} + + modal.mode_switch, + modal.timer1, + modal.timer2 + -> test.events +} \ No newline at end of file diff --git a/test/C/src/modal_models/MultipleOutputFeeder_2Connections.lf b/test/C/src/modal_models/MultipleOutputFeeder_2Connections.lf new file mode 100644 index 0000000000..d3e0c0415e --- /dev/null +++ b/test/C/src/modal_models/MultipleOutputFeeder_2Connections.lf @@ -0,0 +1,85 @@ +/* + * Modal Reactor Test. + * + * Tests if connections in the same reactor that have the same destination work if they are located in separate modes. + */ +target C { + fast: false, + timeout: 2 sec +}; + +import TraceTesting from "util/TraceTesting.lf" + +reactor Modal { + input next:bool; + output count:int; + + initial mode One { + counter1 = new Counter(period=250msec); + counter1.value -> count; + + reaction(next) -> Two {= + SET_MODE(Two); + =} + } + mode Two { + counter2 = new Counter(period=100msec); + counter2.value -> count; + + reaction(next) -> continue(One) {= + SET_MODE(One); + =} + } +} + +reactor Counter(period:time(1sec)) { + output value:int + + timer t(0, period) + state curval:int(0) + + reaction(t) -> value {= + SET(value, self->curval++); + =} +} + +main reactor { + timer stepper(500msec, 500msec) + + modal = new Modal() + test = new TraceTesting( + events_size = 1, + trace_size = 51, + trace = ( + 0,1,0, + 250000000,1,1, + 250000000,1,2, + 0,1,0, + 100000000,1,1, + 100000000,1,2, + 100000000,1,3, + 100000000,1,4, + 100000000,1,5, + 250000000,1,3, + 250000000,1,4, + 0,1,0, + 100000000,1,1, + 100000000,1,2, + 100000000,1,3, + 100000000,1,4, + 100000000,1,5 + ), training = false) + + // Trigger mode change + reaction(stepper) -> modal.next {= + SET(modal.next, true); + =} + + // Print + reaction(modal.count) {= + printf("%d\n", modal.count->value); + =} + + modal.count + -> test.events +} \ No newline at end of file diff --git a/test/C/src/modal_models/MultipleOutputFeeder_ReactionConnections.lf b/test/C/src/modal_models/MultipleOutputFeeder_ReactionConnections.lf new file mode 100644 index 0000000000..e696f01a42 --- /dev/null +++ b/test/C/src/modal_models/MultipleOutputFeeder_ReactionConnections.lf @@ -0,0 +1,88 @@ +/* + * Modal Reactor Test. + * + * Tests if a connection and a reaction in the same reactor can have the same destination if they are located in separate modes. + */ +target C { + fast: false, + timeout: 2 sec +}; + +import TraceTesting from "util/TraceTesting.lf" + +reactor Modal { + input next:bool; + output count:int; + + initial mode One { + counter1 = new Counter(period=250msec); + counter1.value -> count; + + reaction(next) -> Two {= + SET_MODE(Two); + =} + } + mode Two { + counter2 = new Counter(period=100msec); + + reaction(counter2.value) -> count {= + SET(count, counter2.value->value * 10); + =} + + reaction(next) -> continue(One) {= + SET_MODE(One); + =} + } +} + +reactor Counter(period:time(1sec)) { + output value:int + + timer t(0, period) + state curval:int(0) + + reaction(t) -> value {= + SET(value, self->curval++); + =} +} + +main reactor { + timer stepper(500msec, 500msec) + + modal = new Modal() + test = new TraceTesting( + events_size = 1, + trace_size = 51, + trace = ( + 0,1,0, + 250000000,1,1, + 250000000,1,2, + 0,1,0, + 100000000,1,10, + 100000000,1,20, + 100000000,1,30, + 100000000,1,40, + 100000000,1,50, + 250000000,1,3, + 250000000,1,4, + 0,1,0, + 100000000,1,10, + 100000000,1,20, + 100000000,1,30, + 100000000,1,40, + 100000000,1,50 + ), training = false) + + // Trigger mode change + reaction(stepper) -> modal.next {= + SET(modal.next, true); + =} + + // Print + reaction(modal.count) {= + printf("%d\n", modal.count->value); + =} + + modal.count + -> test.events +} \ No newline at end of file diff --git a/test/C/src/modal_models/util/TraceTesting.lf b/test/C/src/modal_models/util/TraceTesting.lf new file mode 100644 index 0000000000..db0359c04c --- /dev/null +++ b/test/C/src/modal_models/util/TraceTesting.lf @@ -0,0 +1,83 @@ +/* + * Utility reactor to record and test execution traces. + */ +target C; + +preamble {= + #include +=} + +reactor TraceTesting(events_size:int(0), trace_size:int(0), trace:int[](0), training:bool(false)) { + input [events_size]events:int + + state last_reaction_time:int(0) + state trace_idx:int(0) + state recorded_events:int*(0) + state recorded_events_next:int(0) + + reaction(startup) {= + self->last_reaction_time = get_logical_time(); + =} + + reaction(events) {= + // Time passed since last reaction + int curr_reaction_delay = get_logical_time() - self->last_reaction_time; + + if (self->training) { + // Save time + self->recorded_events = (int*) realloc(self->recorded_events, sizeof(int) * (self->recorded_events_next + 1 + 2 * self->events_size)); + self->recorded_events[self->recorded_events_next++] = curr_reaction_delay; + } else { + if (self->trace_idx >= self->trace_size) { + printf("ERROR: Trace Error: Current execution exceeds given trace.\n"); + exit(1); + } + + int trace_reaction_delay = self->trace[self->trace_idx++]; + + if (curr_reaction_delay != trace_reaction_delay) { + printf("ERROR: Trace Mismatch: Unexpected reaction timing. (delay: %d, expected: %d)\n", curr_reaction_delay, trace_reaction_delay); + exit(2); + } + } + + for (int i = 0; i < self->events_size; i++) { + int curr_present = events[i]->is_present; + int curr_value = events[i]->value; + + if (self->training) { + // Save event + self->recorded_events[self->recorded_events_next++] = curr_present; + self->recorded_events[self->recorded_events_next++] = curr_value; + } else { + int trace_present = self->trace[self->trace_idx++]; + int trace_value = self->trace[self->trace_idx++]; + + if (trace_present != curr_present) { + printf("ERROR: Trace Mismatch: Unexpected event presence. (event: %d, presence: %d, expected: %d)\n", i, curr_present, trace_present); + exit(3); + } else if (curr_present && trace_value != curr_value) { + printf("ERROR: Trace Mismatch: Unexpected event value. (event: %d, presence: %d, expected: %d)\n", i, curr_value, trace_value); + exit(4); + } + } + } + + self->last_reaction_time = get_logical_time(); + =} + + reaction(shutdown) {= + if (self->training) { + printf("Recorded event trace (%d): (", self->recorded_events_next); + for (int i = 0; i < self->recorded_events_next; i++) { + printf("%d", self->recorded_events[i]); + if (i < self->recorded_events_next - 1) { + printf(","); + } + } + printf(")\n"); + + free(self->recorded_events); + } + =} +} \ No newline at end of file diff --git a/test/Python/src/docker/FilesPropertyContainerized.lf b/test/Python/src/docker/FilesPropertyContainerized.lf new file mode 100644 index 0000000000..8d166c53ed --- /dev/null +++ b/test/Python/src/docker/FilesPropertyContainerized.lf @@ -0,0 +1,34 @@ +target Python { + files: "../include/hello.py", + docker: true +}; + +preamble {= + try: + import hello + except: + request_stop() +=} + +main reactor { + preamble {= + try: + import hello + except: + request_stop() + =} + state passed(false); + timer t(1 msec); + + reaction(t) {= + self.passed = True + =} + + reaction(shutdown) {= + if not self.passed: + print("Failed to import file listed in files target property") + exit(1) + else: + print("PASSED") + =} +} \ No newline at end of file