diff --git a/.github/scripts/test-lfc.sh b/.github/scripts/test-lfc.sh index 447629d02b..c5132e1cd0 100755 --- a/.github/scripts/test-lfc.sh +++ b/.github/scripts/test-lfc.sh @@ -47,6 +47,10 @@ bin/lfc --federated --rti rti test/C/src/Minimal.lf # -h,--help Display this information. bin/lfc --help +# -l, --lint Enable linting during build. +bin/lfc -l test/Python/src/Minimal.lf +bin/lfc --lint test/Python/src/Minimal.lf + # -n,--no-compile Do not invoke target compiler. bin/lfc -n test/C/src/Minimal.lf bin/lfc --no-compile test/C/src/Minimal.lf diff --git a/.github/workflows/lsp-tests.yml b/.github/workflows/lsp-tests.yml index a0b5e5ea7b..4e354bd2e5 100644 --- a/.github/workflows/lsp-tests.yml +++ b/.github/workflows/lsp-tests.yml @@ -29,12 +29,6 @@ jobs: uses: actions/setup-java@v1.4.3 with: java-version: 11 - - name: Check out lingua-franca repository - uses: actions/checkout@v2 - with: - repository: lf-lang/lingua-franca - submodules: true - ref: ${{ inputs.compiler-ref }} - name: Setup Node.js environment uses: actions/setup-node@v2.1.2 - name: Install pnpm @@ -61,6 +55,12 @@ jobs: vcpkgDirectory: ${{ github.workspace }}/vcpkg/ vcpkgTriplet: x64-windows-static if: ${{ runner.os == 'Windows' }} + - name: Check out lingua-franca repository + uses: actions/checkout@v2 + with: + repository: lf-lang/lingua-franca + submodules: true + ref: ${{ inputs.compiler-ref }} - name: Run language server Python tests without PyLint run: ./gradlew test --tests org.lflang.tests.lsp.LspTests.pythonSyntaxOnlyValidationTest - name: Report to CodeCov diff --git a/RELEASES.md b/RELEASES.md index 8f5b35594b..bde551fabd 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,4 +1,6 @@ # Version 0.1.0-beta-SNAPSHOT +- LF programs with the TypeScript target can now be compiled on Windows (#850). +- In the VS Code extension, generated code is validated when an LF file is saved for all targets except C (#828). Generated C code is only validated when it is fully compiled. ## Language diff --git a/example/Python/src/DigitalTwin/DoubleUnlock/DoubleUnlockDemo.lf b/example/Python/src/DigitalTwin/DoubleUnlock/DoubleUnlockDemo.lf new file mode 100644 index 0000000000..28467318c0 --- /dev/null +++ b/example/Python/src/DigitalTwin/DoubleUnlock/DoubleUnlockDemo.lf @@ -0,0 +1,200 @@ +/** + * Example of a basic digital twin setup, with two federates + * maintaining a shared state "lock state". + * + * For run instructions, see README.md in the same directory. + * + * @author Hou Seng Wong (housengw@berkeley.edu) + */ + + target Python { + docker: true, + files: ["../utils.py"] +}; + +preamble {= + import curses + import threading + from utils import Logger, Window + import enum + + class LockStates(enum.Enum): + Locked = 0 + DriverUnlocked = 1 + AllUnlocked = 2 + + class LockStateNames(enum.Enum): + Locked = "Locked" + DriverUnlocked = "Driver's door unlocked" + AllUnlocked = "All doors unlocked" +=} + +/** + * A key fob that detects "lock" and "unlock" key presses, + * and sends and receives lock state to and from other key fobs. + */ +reactor DoubleUnlockKeyFob(auto_lock_duration(5)) { + /* logger / window related state variables */ + state logger({=None=}); + state window({=None=}); + state main_message_begins(0); + + /* KeyFob related state variables */ + state lock_state(0); + state listener({=None=}); + state auto_lock_counter; + + /* I/O ports and actions */ + input get_lock_action; + input get_lock_press_from_tester; + output send_lock_action; + physical action press_lock; + physical action press_unlock; + logical action handle_press_lock; + logical action handle_press_unlock; + logical action do_lock; + + /* Autolock timer */ + timer autolock_timer(0, 100 msec); + + preamble {= + def lock_state_str(self, lock_state): + if lock_state == LockStates.Locked: + return LockStateNames.Locked.value + elif lock_state == LockStates.DriverUnlocked: + return LockStateNames.DriverUnlocked.value + elif lock_state == LockStates.AllUnlocked: + return LockStateNames.AllUnlocked.value + else: + return "ERROR: Lock state is invalid" + + def print_lock_state(self): + self.window.change_line(self.main_message_begins, f"Lock State: {self.lock_state_str(self.lock_state)}") + + def print_log(self): + if self.logger.log_size() > 0: + for i, line in enumerate(self.logger.get_log()): + self.window.change_line(self.main_message_begins + 1 + i, line) + + def format_log_message(self, line): + elapsed_ptime, tag, remote, do_lock, auto = line + return (f"At (tag: ({'{:,}'.format(tag.time)} ns, {tag.microstep}), " + f"lag: {'{:,}'.format(elapsed_ptime - tag.time)} ns), " + f"{'[Auto] ' if auto else ''}{'[Remote]' if remote else '[Local]'} lock action: {'Lock' if do_lock else 'Unlock'}") + + # log structure: (elapsed_physical_time:int, tag:int, remote:bool, do_lock:bool, auto:bool) + def append_log(self, auto, remote, do_lock): + elapsed_tag = Tag(get_elapsed_logical_time(), get_microstep()) + log_entry = (get_elapsed_physical_time(), elapsed_tag, remote, do_lock, auto) + self.logger.append_log(self.format_log_message(log_entry)) + + def listen_for_keypress(self, press_lock, press_unlock): + key = "" + while key != ord("q"): + key = self.window.getch() + if key == ord("l"): + press_lock.schedule(0) + elif key == ord("u"): + press_unlock.schedule(0) + request_stop() + + def reset_autolock_counter(self): + self.auto_lock_counter = self.auto_lock_duration + =} + + reaction(startup) -> press_lock, press_unlock {= + # Set up the logger and the curses window + self.window = Window() + self.logger = Logger() + messages = [ + "Press 'l' to send a lock signal, 'u' to send an unlock signal, 'q' to quit", + "", + "Rules:", + "1. A lock signal locks all doors.", + "2. An unlock signal unlocks driver's door if driver's door was locked.", + "3. An unlock signal unlocks all door if driver's door was NOT locked.", + "", + "All doors automatically lock after 5 seconds if no unlock signal is received. ", + "" + ] + for i, msg in enumerate(messages): + self.window.change_line(i, msg) + self.main_message_begins = len(messages) + self.lock_state = LockStates.Locked + self.reset_autolock_counter() + self.print_lock_state() + self.print_log() + self.window.refresh() + + # Spawn thread to listen for key presses + t = threading.Thread(target=self.listen_for_keypress, args=(press_lock, press_unlock)) + self.listener = t + t.start() + =} + + reaction(press_lock) -> handle_press_lock {= + handle_press_lock.schedule(0) + =} + + reaction(press_unlock) -> handle_press_unlock {= + handle_press_unlock.schedule(0) + =} + + reaction(handle_press_lock) -> do_lock, send_lock_action {= + self.append_log(auto=False, remote=False, do_lock=True) + do_lock.schedule(0, True) + send_lock_action.set(True) + =} + + reaction(handle_press_unlock) -> do_lock, send_lock_action {= + self.append_log(auto=False, remote=False, do_lock=False) + do_lock.schedule(0, False) + send_lock_action.set(False) + =} + + reaction(get_lock_press_from_tester) -> handle_press_lock, handle_press_unlock {= + press_lock_val = get_lock_press_from_tester.value + if press_lock_val: + handle_press_lock.schedule(0) + else: + handle_press_unlock.schedule(0) + =} + + reaction(get_lock_action) -> do_lock {= + self.append_log(auto=False, remote=True, do_lock=get_lock_action.value) + do_lock.schedule(0, get_lock_action.value) + =} + + reaction(do_lock) {= + if do_lock.value: + self.lock_state = LockStates.Locked + elif self.lock_state == LockStates.Locked: + self.lock_state = LockStates.DriverUnlocked + self.reset_autolock_counter() + elif self.lock_state == LockStates.DriverUnlocked: + self.lock_state = LockStates.AllUnlocked + self.print_lock_state() + self.print_log() + self.window.refresh() + =} + + reaction(autolock_timer) -> do_lock {= + if self.auto_lock_counter > 0: + self.auto_lock_counter -= 0.1 + elif self.lock_state != LockStates.Locked: + self.append_log(auto=True, remote=False, do_lock=True) + do_lock.schedule(0, True) + =} + + reaction(shutdown) {= + self.listener.join() + curses.endwin() + =} +} + +federated reactor { + fob = new DoubleUnlockKeyFob(); + twin = new DoubleUnlockKeyFob(); + fob.send_lock_action -> twin.get_lock_action; + twin.send_lock_action -> fob.get_lock_action; +} diff --git a/example/Python/src/DigitalTwin/DoubleUnlock/README.md b/example/Python/src/DigitalTwin/DoubleUnlock/README.md new file mode 100644 index 0000000000..8cfde843b0 --- /dev/null +++ b/example/Python/src/DigitalTwin/DoubleUnlock/README.md @@ -0,0 +1,6 @@ +# Double Unlock Key Fob (Digital Twin) Example + +This example is similar to the Key Fob Demo, but with 3 states: Locked, Driver's Door Unlocked, and All Doors Unlocked. +A simulator program `Simulator.lf` is also included to demonstrate generating simulated signals at arbitrary logical time. + +To run the program, do `lfc Simulator.lf` and use the bash scripts to launch each federate in a separate terminal window. diff --git a/example/Python/src/DigitalTwin/DoubleUnlock/Simulator.lf b/example/Python/src/DigitalTwin/DoubleUnlock/Simulator.lf new file mode 100644 index 0000000000..38749705c8 --- /dev/null +++ b/example/Python/src/DigitalTwin/DoubleUnlock/Simulator.lf @@ -0,0 +1,63 @@ +/** + * Example of a reactor that sends simulated key presses at arbitrary + * logical time to the key fobs. + * + * For run instructions, see README.md in the same directory. + * + * @author Hou Seng Wong (housengw@berkeley.edu) + */ + +target Python { + docker: true, + files: ["../utils.py"] +}; + +import DoubleUnlockKeyFob from "DoubleUnlockDemo.lf"; + + +reactor DoubleUnlockKeyFobTester(initial_delay(5)) { + state logger({=None=}); + state window({=None=}); + + logical action do_test; + logical action simulate_press_fob; + logical action simulate_press_twin; + output send_lock_press_to_fob; + output send_lock_press_to_twin; + + reaction(startup) -> do_test {= + print(f"Test starts in {self.initial_delay} seconds...") + do_test.schedule(SEC(self.initial_delay)) + =} + + reaction(do_test) -> simulate_press_fob, simulate_press_twin {= + simulate_press_fob.schedule(0, False) + simulate_press_twin.schedule(0, False) + + # Feel free to add other simulated key presses... + # simulate_press_fob.schedule(0, True) + # simulate_press_twin.schedule(0, True) + =} + + reaction(simulate_press_fob) -> send_lock_press_to_fob {= + tag = get_current_tag() + print(f"Sent lock press {simulate_press_fob.value} to fob at ({tag.time}, {tag.microstep})") + send_lock_press_to_fob.set(simulate_press_fob.value) + =} + + reaction(simulate_press_twin) -> send_lock_press_to_twin {= + tag = get_current_tag() + print(f"Sent lock press {simulate_press_twin.value} to twin at ({tag.time}, {tag.microstep})") + send_lock_press_to_twin.set(simulate_press_twin.value) + =} +} + +federated reactor { + fob = new DoubleUnlockKeyFob(); + twin = new DoubleUnlockKeyFob(); + tester = new DoubleUnlockKeyFobTester(); + tester.send_lock_press_to_fob -> fob.get_lock_press_from_tester; + tester.send_lock_press_to_twin -> twin.get_lock_press_from_tester; + fob.send_lock_action -> twin.get_lock_action; + twin.send_lock_action -> fob.get_lock_action; +} \ No newline at end of file diff --git a/example/Python/src/DigitalTwin/DoubleUnlock/run_fob.sh b/example/Python/src/DigitalTwin/DoubleUnlock/run_fob.sh new file mode 100755 index 0000000000..3c9dd7d2da --- /dev/null +++ b/example/Python/src/DigitalTwin/DoubleUnlock/run_fob.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd ../../../src-gen/DigitalTwin/DoubleUnlock/Simulator/fob +python3 Simulator_fob.py -i 1 diff --git a/example/Python/src/DigitalTwin/DoubleUnlock/run_rti.sh b/example/Python/src/DigitalTwin/DoubleUnlock/run_rti.sh new file mode 100755 index 0000000000..b865088205 --- /dev/null +++ b/example/Python/src/DigitalTwin/DoubleUnlock/run_rti.sh @@ -0,0 +1 @@ +RTI -i 1 -n 3 \ No newline at end of file diff --git a/example/Python/src/DigitalTwin/DoubleUnlock/run_simulator.sh b/example/Python/src/DigitalTwin/DoubleUnlock/run_simulator.sh new file mode 100755 index 0000000000..666d22241a --- /dev/null +++ b/example/Python/src/DigitalTwin/DoubleUnlock/run_simulator.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd ../../../src-gen/DigitalTwin/DoubleUnlock/Simulator/simulator +python3 Simulator_tester.py -i 1 diff --git a/example/Python/src/DigitalTwin/DoubleUnlock/run_twin.sh b/example/Python/src/DigitalTwin/DoubleUnlock/run_twin.sh new file mode 100755 index 0000000000..b37cc57d20 --- /dev/null +++ b/example/Python/src/DigitalTwin/DoubleUnlock/run_twin.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd ../../../src-gen/DigitalTwin/DoubleUnlock/Simulator/twin +python3 Simulator_twin.py -i 1 diff --git a/example/Python/src/DigitalTwin/KeyFob/KeyFobDemo.lf b/example/Python/src/DigitalTwin/KeyFob/KeyFobDemo.lf index 4ca8b4e937..6272976afe 100644 --- a/example/Python/src/DigitalTwin/KeyFob/KeyFobDemo.lf +++ b/example/Python/src/DigitalTwin/KeyFob/KeyFobDemo.lf @@ -1,6 +1,6 @@ /** - * Example of a basic digital twin setup, with two federates mutating and - * maintaining a "lock status" state. + * Example of a basic digital twin setup, with two federates + * maintaining a shared state "locked". * * For run instructions, see README.md in the same directory. * @@ -8,62 +8,58 @@ */ target Python { - docker: true + docker: true, + files: ["../utils.py"] }; preamble {= import curses import threading + from utils import Logger, Window =} /** * A key fob that detects "lock" and "unlock" key presses, - * and sends and receives lock status to and from other key fobs. + * and sends and receives lock state to and from other key fobs. */ reactor KeyFob { - state locked({=False=}); + /* logger / window related state variables */ + state logger({=None=}); state window({=None=}); + + /* KeyFob related state variables */ + state locked({=False=}); state listener({=None=}); - state log({=list()=}); - state max_log_len(3); - input get_lock_status; - output send_lock_status; + + /* I/O ports and actions */ + input get_lock_state; + output send_lock_state; physical action press_lock; physical action press_unlock; preamble {= - def print_intro_message(self): - self.window.addstr(0, 0, "Press 'l' to lock, 'u' to unlock, 'q' to quit") - self.window.refresh() - - def print_lock_status(self): - status = "Locked" if self.locked else "Unlocked" - self.window.addstr(1, 0, f"Lock Status: {status}") - self.window.clrtoeol() - self.window.refresh() + def lock_state_str(self, locked): + return "Locked" if locked else "Unlocked" + + def print_lock_state(self): + self.window.change_line(1, f"Lock Status: {self.lock_state_str(self.locked)}") def print_log(self): - text = "Log is empty" - if len(self.log) > 0: - for i, line in enumerate(self.log): - log_message = self.format_log_message(line) - self.window.addstr(2 + i, 0, log_message) - self.window.clrtoeol() - self.window.refresh() + if self.logger.log_size() > 0: + for i, line in enumerate(self.logger.get_log()): + self.window.change_line(2 + i, line) def format_log_message(self, line): elapsed_ptime, tag, remote, locked = line return (f"At (tag: ({'{:,}'.format(tag.time)} ns, {tag.microstep}), " f"lag: {'{:,}'.format(elapsed_ptime - tag.time)} ns), " - f"{'Received' if remote else 'Set'} lock state as: {'Locked' if locked else 'Unlocked'}") + f"{'[Remote]' if remote else '[Local]'} Updated lock state to: {self.lock_state_str(locked)}") # log structure: (elapsed_physical_time:int, tag:int, remote:bool, locked:bool) def append_log(self, remote, locked): elapsed_tag = Tag(get_elapsed_logical_time(), get_microstep()) log_entry = (get_elapsed_physical_time(), elapsed_tag, remote, locked) - self.log.append(log_entry) - if len(self.log) > self.max_log_len: - self.log.pop(0) + self.logger.append_log(self.format_log_message(log_entry)) def listen_for_keypress(self, press_lock, press_unlock): key = "" @@ -77,39 +73,44 @@ reactor KeyFob { =} reaction(startup) -> press_lock, press_unlock {= - self.window = curses.initscr() - curses.cbreak() - curses.noecho() - self.window.keypad(True) - self.print_intro_message() - self.print_lock_status() + # Set up the logger and the curses window + self.window = Window() + self.logger = Logger() + self.window.change_line(0, "Press 'l' to lock, 'u' to unlock, 'q' to quit") + self.print_lock_state() self.print_log() + self.window.refresh() + + # Spawn thread to listen for key presses t = threading.Thread(target=self.listen_for_keypress, args=(press_lock, press_unlock)) self.listener = t t.start() =} - reaction(press_lock) -> send_lock_status {= + reaction(press_lock) -> send_lock_state {= self.append_log(remote=False, locked=True) self.locked = True - self.print_lock_status() + self.print_lock_state() self.print_log() - send_lock_status.set(True) + self.window.refresh() + send_lock_state.set(True) =} - reaction(press_unlock) -> send_lock_status {= + reaction(press_unlock) -> send_lock_state {= self.append_log(remote=False, locked=False) self.locked = False - self.print_lock_status() + self.print_lock_state() self.print_log() - send_lock_status.set(False) + self.window.refresh() + send_lock_state.set(False) =} - reaction(get_lock_status) {= - self.append_log(remote=True, locked=get_lock_status.value) - self.locked = get_lock_status.value - self.print_lock_status() + reaction(get_lock_state) {= + self.append_log(remote=True, locked=get_lock_state.value) + self.locked = get_lock_state.value + self.print_lock_state() self.print_log() + self.window.refresh() =} reaction(shutdown) {= @@ -121,6 +122,6 @@ reactor KeyFob { federated reactor { fob = new KeyFob(); twin = new KeyFob(); - fob.send_lock_status -> twin.get_lock_status; - twin.send_lock_status -> fob.get_lock_status; + fob.send_lock_state -> twin.get_lock_state; + twin.send_lock_state -> fob.get_lock_state; } \ No newline at end of file diff --git a/example/Python/src/DigitalTwin/KeyFob/README.md b/example/Python/src/DigitalTwin/KeyFob/README.md index 797978755b..e2d8a180a4 100644 --- a/example/Python/src/DigitalTwin/KeyFob/README.md +++ b/example/Python/src/DigitalTwin/KeyFob/README.md @@ -2,6 +2,8 @@ This example shows two federates, one hosted locally and the other hosted on Google Cloud, interacting via an RTI that is also hosted on Google Cloud. +Check out this (video)[https://www.youtube.com/watch?v=s7dYKLoHXVE] for a recorded demo. + ## Before we start Make sure you have a Google Cloud Platform (GCP) account and a project set up. @@ -50,7 +52,7 @@ For clarity purposes, I will use `user$ ` to denote a local terminal, `user@rti- Run the `setup.sh` script to set up the RTI and the digital twin on the cloud: ```bash -user$ ./setup.sh +user$ ./scripts/cloud/setup.sh ``` When the script finishes, ssh into the digital twin: @@ -72,7 +74,7 @@ user@twin-vm ~ $ docker container attach CONTAINER_ID Open another terminal in the directory where the `docker-compose.yml` is located. Run `run_local_copy.sh` to run the local key fob: ```bash -user$ ./run_local_copy.sh +user$ ./scripts/cloud/run_local_copy.sh ``` Now you should see the key fobs in each terminal syncing with each other through the RTI on the cloud. @@ -81,7 +83,7 @@ Now you should see the key fobs in each terminal syncing with each other through Run the clean up script: ```bash -user$ ./cleanup.sh +user$ ./scripts/cloud/cleanup.sh ``` ### Conclusion diff --git a/example/Python/src/DigitalTwin/KeyFob/dev/README.md b/example/Python/src/DigitalTwin/KeyFob/dev/README.md index 9d48fb4022..8eaf1e095e 100644 --- a/example/Python/src/DigitalTwin/KeyFob/dev/README.md +++ b/example/Python/src/DigitalTwin/KeyFob/dev/README.md @@ -28,30 +28,37 @@ NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP rti-vm us-central1-a n1-standard-1 10.128.0.7 34.133.143.163 RUNNING ``` +Export the `EXTERNAL_IP` of the RTI: +```bash +RTI_IP=`gcloud compute instances list | grep 'rti-vm' | awk '{print $5}'` +``` + ### Running the digital twin -Build Digital Twin example locally, after changing the “at” line of the federated reactor: +Build `KeyFobDemo.lf` locally: ```bash -user$ lfc DigitalTwin.lf +user$ lfc KeyFobDemo.lf ``` -Go to the directory where the generated code is located, which is usually located at `src-gen/DigitalTwin/DigitalTwin`. There should be a docker-compose.yml in that directory. Build the docker images of the two key fobs: +Go to the directory where the generated code is located, which is usually located at `src-gen/DigitalTwin/KeyFob/KeyFobDemo`. There should be a docker-compose.yml in that directory. Build the docker images of the two key fobs: ```bash -user$ docker compose build fob twin -—no-cache +user$ docker compose build fob twin --no-cache ``` Tag and push the digital twin's docker image to the cloud: ```bash -user$ docker tag digitaltwin_twin gcr.io/$PROJECT_ID/twin +user$ docker tag keyfobdemo_twin gcr.io/$PROJECT_ID/twin user$ docker push gcr.io/$PROJECT_ID/twin ``` -Create a VM for the digital twin: +Create a VM for the digital twin, passing the address of the RTI: ```bash -user$ gcloud compute instances create-with-container twin-vm \ +gcloud compute instances create-with-container twin-vm \ --container-image=gcr.io/$PROJECT_ID/twin \ --container-arg="-i" \ --container-arg=1 \ + --container-arg="--rti" \ + --container-arg=$RTI_IP \ --container-stdin \ --container-tty ``` @@ -73,9 +80,9 @@ user@twin-vm ~ $ docker container attach CONTAINER_ID ### Running the local key fob -Open another terminal in the directory where the `docker-compose.yml` is located. Run: +Open another terminal in the directory where the `docker-compose.yml` is located. Pass in the address of the RTI. Run: ```bash -user$ docker compose run --rm fob +user$ docker compose run --rm fob -i 1 --rti $RTI_IP ``` Now you should see the key fobs in each terminal syncing with each other through the RTI on the cloud. diff --git a/example/Python/src/DigitalTwin/KeyFob/cleanup.sh b/example/Python/src/DigitalTwin/KeyFob/scripts/cloud/cleanup.sh similarity index 100% rename from example/Python/src/DigitalTwin/KeyFob/cleanup.sh rename to example/Python/src/DigitalTwin/KeyFob/scripts/cloud/cleanup.sh diff --git a/example/Python/src/DigitalTwin/KeyFob/run_local_copy.sh b/example/Python/src/DigitalTwin/KeyFob/scripts/cloud/run_local_copy.sh similarity index 100% rename from example/Python/src/DigitalTwin/KeyFob/run_local_copy.sh rename to example/Python/src/DigitalTwin/KeyFob/scripts/cloud/run_local_copy.sh diff --git a/example/Python/src/DigitalTwin/KeyFob/setup.sh b/example/Python/src/DigitalTwin/KeyFob/scripts/cloud/setup.sh similarity index 100% rename from example/Python/src/DigitalTwin/KeyFob/setup.sh rename to example/Python/src/DigitalTwin/KeyFob/scripts/cloud/setup.sh diff --git a/example/Python/src/DigitalTwin/KeyFob/scripts/local/run_fob.sh b/example/Python/src/DigitalTwin/KeyFob/scripts/local/run_fob.sh new file mode 100755 index 0000000000..cd41af8e74 --- /dev/null +++ b/example/Python/src/DigitalTwin/KeyFob/scripts/local/run_fob.sh @@ -0,0 +1,2 @@ +cd ../../../src-gen/DigitalTwin/KeyFob/KeyFobDemo +python3 fob/KeyFobDemo_fob.py -i 1 \ No newline at end of file diff --git a/example/Python/src/DigitalTwin/KeyFob/scripts/local/run_rti.sh b/example/Python/src/DigitalTwin/KeyFob/scripts/local/run_rti.sh new file mode 100755 index 0000000000..7321705ec1 --- /dev/null +++ b/example/Python/src/DigitalTwin/KeyFob/scripts/local/run_rti.sh @@ -0,0 +1 @@ +RTI -i 1 -n 2 \ No newline at end of file diff --git a/example/Python/src/DigitalTwin/KeyFob/scripts/local/run_twin.sh b/example/Python/src/DigitalTwin/KeyFob/scripts/local/run_twin.sh new file mode 100755 index 0000000000..99ae7bb6c5 --- /dev/null +++ b/example/Python/src/DigitalTwin/KeyFob/scripts/local/run_twin.sh @@ -0,0 +1,2 @@ +cd ../../../src-gen/DigitalTwin/KeyFob/KeyFobDemo +python3 twin/KeyFobDemo_twin.py -i 1 \ No newline at end of file diff --git a/example/Python/src/DigitalTwin/utils.py b/example/Python/src/DigitalTwin/utils.py new file mode 100644 index 0000000000..769bd4d9b3 --- /dev/null +++ b/example/Python/src/DigitalTwin/utils.py @@ -0,0 +1,36 @@ +import curses + +class Logger: + def __init__(self, max_lines=3): + self.max_lines = max_lines + self.log = [] + + def append_log(self, msg): + self.log.append(msg) + if len(self.log) > self.max_lines: + self.log.pop(0) + + def get_log(self): + return self.log + + def log_size(self): + return len(self.log) + + +class Window: + def __init__(self): + self.window = curses.initscr() + curses.cbreak() + curses.noecho() + self.window.keypad(True) + + def change_line(self, y, new_msg): + self.window.addstr(y, 0, new_msg) + self.window.clrtoeol() + + def refresh(self): + self.window.refresh() + + def getch(self): + return self.window.getch() + diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.xtend index 7446794dd9..014aa69ef6 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.xtend @@ -50,6 +50,7 @@ import de.cau.cs.kieler.klighd.syntheses.AbstractDiagramSynthesis import de.cau.cs.kieler.klighd.util.KlighdProperties import java.util.Collection import java.util.EnumSet +import java.util.LinkedList import java.util.List import java.util.Map import javax.inject.Inject @@ -91,7 +92,6 @@ import org.lflang.generator.ReactorInstance import org.lflang.generator.TimerInstance import org.lflang.generator.TriggerInstance import org.lflang.generator.TriggerInstance.BuiltinTriggerVariable -import org.lflang.lf.Connection import org.lflang.lf.Model import static extension org.eclipse.emf.ecore.util.EcoreUtil.* @@ -326,13 +326,7 @@ class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { node.setLayoutOption(LayeredOptions.SPACING_COMPONENT_COMPONENT, LayeredOptions.SPACING_COMPONENT_COMPONENT.^default * 0.5f) } } else { - // If the reactor is a bank, then obtain the details from the first - // element of the bank rather than the bank itself. - val instance = if (reactorInstance.bankSize > 0) { - reactorInstance.bankMembers.get(0) - } else { - reactorInstance - } + val instance = reactorInstance // Expanded Rectangle node.addReactorFigure(reactorInstance, label) => [ ReactorFigureComponents comps | @@ -576,12 +570,9 @@ class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { // Transform instances for (entry : reactorInstance.children.reverseView.indexed) { val child = entry.value - // Do not render individual reactors in a bank. - if (child.getBank() === null) { - val rNodes = child.createReactorNode(child.getExpansionState?:false, inputPorts, outputPorts, allReactorNodes) - rNodes.head.setLayoutOption(CoreOptions.PRIORITY, entry.key) - nodes += rNodes - } + val rNodes = child.createReactorNode(child.getExpansionState?:false, inputPorts, outputPorts, allReactorNodes) + rNodes.head.setLayoutOption(CoreOptions.PRIORITY, entry.key) + nodes += rNodes } // Create timers @@ -680,29 +671,24 @@ class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { // connect outputs port = null // create new ports for (TriggerInstance effect : reaction.effects?:emptyList) { - // Skip this effect if it is contained in a bank with index other than 0. - if (!(effect instanceof PortInstance) - || (effect.parent.bankIndex <= 0) - ) { - port = if (REACTIONS_USE_HYPEREDGES.booleanValue && port !== null) { - port + port = if (REACTIONS_USE_HYPEREDGES.booleanValue && port !== null) { + port + } else { + node.addInvisiblePort() => [ + setLayoutOption(CoreOptions.PORT_SIDE, PortSide.EAST) + ] + } + if (effect instanceof ActionInstance) { + actionSources.put(effect, port) + } else if (effect instanceof PortInstance) { + var KPort dst = null + if (effect.isOutput) { + dst = parentOutputPorts.get(effect) } else { - node.addInvisiblePort() => [ - setLayoutOption(CoreOptions.PORT_SIDE, PortSide.EAST) - ] + dst = inputPorts.get(effect.parent, effect) } - if (effect instanceof ActionInstance) { - actionSources.put(effect, port) - } else if (effect instanceof PortInstance) { - var KPort dst = null - if (effect.isOutput) { - dst = parentOutputPorts.get(effect) - } else { - dst = inputPorts.get(effect.parent, effect) - } - if (dst !== null) { - createDependencyEdge(effect).connect(port, dst) - } + if (dst !== null) { + createDependencyEdge(effect).connect(port, dst) } } } @@ -745,65 +731,67 @@ class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { } // Transform connections. - // The connections data structure maps connections to their connections as they appear - // in a visualization of the program. For each connection, there is map - // from source ports (single ports and multiports) on the left side of the - // connection to a set of destination ports (single ports and multiports) - // on the right side of the connection. The ports contained by the multiports - // are not represented. - for (Connection connection : reactorInstance.connections.keySet) { - // TODO check banks - val connections = reactorInstance.connections.get(connection); - for (leftPort : connections.keySet) { - val rightPorts = connections.get(leftPort); - for (rightPort : rightPorts) { - val source = if (leftPort.parent == reactorInstance) { - parentInputPorts.get(leftPort) - } else { - outputPorts.get(leftPort.parent, leftPort) - } + // First, collect all the source ports. + val sourcePorts = new LinkedList(reactorInstance.inputs); + for (child : reactorInstance.children) { + sourcePorts.addAll(child.outputs); + } + + for (leftPort : sourcePorts) { + val source = if (leftPort.parent == reactorInstance) { + parentInputPorts.get(leftPort) + } else { + outputPorts.get(leftPort.parent, leftPort) + } + for (sendRange : leftPort.dependentPorts) { + for (rightRange : sendRange.destinations) { + val rightPort = rightRange.instance; val target = if (rightPort.parent == reactorInstance) { parentOutputPorts.get(rightPort) } else { inputPorts.get(rightPort.parent, rightPort) } - val edge = createIODependencyEdge(connection, leftPort.isMultiport() || rightPort.isMultiport()) - if (connection.delay !== null) { - edge.addCenterEdgeLabel(connection.delay.toText) => [ - associateWith(connection.delay) - if (connection.physical) { - applyOnEdgePysicalDelayStyle( - reactorInstance.mainOrFederated ? Colors.WHITE : Colors.GRAY_95) - } else { - applyOnEdgeDelayStyle() - } - ] - } else if (connection.physical) { - edge.addCenterEdgeLabel("---").applyOnEdgePysicalStyle( - reactorInstance.mainOrFederated ? Colors.WHITE : Colors.GRAY_95) - } - if (source !== null && target !== null) { - // check for inside loop (direct in -> out connection with delay) - if (parentInputPorts.values.contains(source) && parentOutputPorts.values.contains(target)) { - // edge.setLayoutOption(CoreOptions.INSIDE_SELF_LOOPS_YO, true) // Does not work as expected - // Introduce dummy node to enable direct connection (that is also hidden when collapsed) - var dummy = createNode() - if (directConnectionDummyNodes.containsKey(target)) { - dummy = directConnectionDummyNodes.get(target) + // There should be a connection, but skip if not. + val connection = sendRange.connection; + if (connection !== null) { + val edge = createIODependencyEdge(connection, leftPort.isMultiport() || rightPort.isMultiport()) + if (connection.delay !== null) { + edge.addCenterEdgeLabel(connection.delay.toText) => [ + associateWith(connection.delay) + if (connection.physical) { + applyOnEdgePysicalDelayStyle( + reactorInstance.mainOrFederated ? Colors.WHITE : Colors.GRAY_95) + } else { + applyOnEdgeDelayStyle() + } + ] + } else if (connection.physical) { + edge.addCenterEdgeLabel("---").applyOnEdgePysicalStyle( + reactorInstance.mainOrFederated ? Colors.WHITE : Colors.GRAY_95) + } + if (source !== null && target !== null) { + // check for inside loop (direct in -> out connection with delay) + if (parentInputPorts.values.contains(source) && parentOutputPorts.values.contains(target)) { + // edge.setLayoutOption(CoreOptions.INSIDE_SELF_LOOPS_YO, true) // Does not work as expected + // Introduce dummy node to enable direct connection (that is also hidden when collapsed) + var dummy = createNode() + if (directConnectionDummyNodes.containsKey(target)) { + dummy = directConnectionDummyNodes.get(target) + } else { + nodes += dummy + directConnectionDummyNodes.put(target, dummy) + + dummy.addInvisibleContainerRendering() + dummy.setNodeSize(0, 0) + + val extraEdge = createIODependencyEdge(null, + leftPort.isMultiport() || rightPort.isMultiport()) + extraEdge.connect(dummy, target) + } + edge.connect(source, dummy) } else { - nodes += dummy - directConnectionDummyNodes.put(target, dummy) - - dummy.addInvisibleContainerRendering() - dummy.setNodeSize(0, 0) - - val extraEdge = createIODependencyEdge(null, - leftPort.isMultiport() || rightPort.isMultiport()) - extraEdge.connect(dummy, target) + edge.connect(source, target) } - edge.connect(source, dummy) - } else { - edge.connect(source, target) } } } @@ -867,25 +855,18 @@ class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { } } if (reactorInstance.mainOrFederated) { - b.append(FileConfig.nameWithoutExtension(reactorInstance.reactorDefinition.eResource)) - } else if (reactorInstance.reactorDefinition === null) { + b.append(FileConfig.nameWithoutExtension(reactorInstance.reactorDeclaration.eResource)) + } else if (reactorInstance.reactorDeclaration === null) { // There is an error in the graph. b.append("") } else { - b.append(reactorInstance.reactorDefinition.name) + b.append(reactorInstance.reactorDeclaration.name) } if (REACTOR_PARAMETER_MODE.objectValue === ReactorParameterDisplayModes.TITLE) { - // If the reactor is a bank, then obtain the details from the first - // element of the bank rather than the bank itself. - val instance = if (reactorInstance.bankSize > 0) { - reactorInstance.bankMembers.get(0) - } else { - reactorInstance - } - if (instance.parameters.empty) { + if (reactorInstance.parameters.empty) { b.append("()") } else { - b.append(instance.parameters.join("(", ", ", ")") [ + b.append(reactorInstance.parameters.join("(", ", ", ")") [ createParameterLabel(false) ]) } @@ -920,8 +901,8 @@ class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { if (!t.nullOrEmpty) { b.append(":").append(t) } - if (!param.init.nullOrEmpty) { - b.append("(").append(param.init.join(", ", [it.toText])).append(")") + if (!param.getInitialValue.nullOrEmpty) { + b.append("(").append(param.getInitialValue.join(", ", [it.toText])).append(")") } return b.toString() } @@ -1058,8 +1039,9 @@ class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { } if (SHOW_MULTIPORT_WIDTH.booleanValue) { if (lfPort.isMultiport) { - // TODO Fix unresolvable references in ReactorInstance - label += "[" + lfPort.width + "]" + label += (lfPort.width >= 0)? + "[" + lfPort.width + "]" + : "[?]" } } port.addOutsidePortLabel(label, 8).associateWith(lfPort.definition) diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.xtend index 2eb2b324a8..57ad610cea 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.xtend @@ -201,17 +201,16 @@ class LinguaFrancaShapeExtensions extends AbstractSynthesisExtensions { val bank = newArrayList val container = node.addInvisibleContainerRendering => [ // TODO handle unresolved width - val bankWidth = reactorInstance.bankSize//instance.widthSpec.width addRoundedRectangle(8, 8, 1) => [ style.apply(it) setAreaPlacementData().from(LEFT, BANK_FIGURE_X_OFFSET_SUM, 0, TOP, BANK_FIGURE_Y_OFFSET_SUM, 0).to(RIGHT, 0, 0, BOTTOM, 0, 0) ] - if (bankWidth === 3) { + if (reactorInstance.width === 3) { addRoundedRectangle(8, 8, 1) => [ style.apply(it) setAreaPlacementData().from(LEFT, BANK_FIGURE_X_OFFSET_SUM / 2, 0, TOP, BANK_FIGURE_Y_OFFSET_SUM / 2, 0).to(RIGHT, BANK_FIGURE_X_OFFSET_SUM / 2, 0, BOTTOM, BANK_FIGURE_Y_OFFSET_SUM / 2, 0) ] - } else if (bankWidth !== 2 && bankWidth !== 3) { + } else if (reactorInstance.width !== 2 && reactorInstance.width !== 3) { addRoundedRectangle(8, 8, 1) => [ style.apply(it) setAreaPlacementData().from(LEFT, 2 * BANK_FIGURE_X_OFFSET_SUM / 3, 0, TOP, 2 * BANK_FIGURE_Y_OFFSET_SUM / 3, 0).to(RIGHT, BANK_FIGURE_X_OFFSET_SUM / 3, 0, BOTTOM, BANK_FIGURE_Y_OFFSET_SUM / 3, 0) @@ -229,9 +228,12 @@ class LinguaFrancaShapeExtensions extends AbstractSynthesisExtensions { addRectangle() => [ invisible = true setAreaPlacementData().from(LEFT, 12, 0, BOTTOM, 9, 0).to(RIGHT, 6, 0, BOTTOM, 0.5f, 0) - // TODO handle unresolved width + // Handle unresolved width. + val widthLabel = (reactorInstance.width >= 0)? + Integer.toString(reactorInstance.width) + : "?" // addText(instance.widthSpec.toText) => [ - addText(Integer.toString(reactorInstance.bankSize)) => [ + addText(widthLabel) => [ horizontalAlignment = HorizontalAlignment.LEFT verticalAlignment = VerticalAlignment.BOTTOM fontSize = 6 @@ -296,13 +298,17 @@ class LinguaFrancaShapeExtensions extends AbstractSynthesisExtensions { if (SHOW_REACTION_LEVEL.booleanValue) { // Force calculation of levels for reactions. This calculation // will only be done once. Note that if this fails due to a causality loop, - // then some reactions will have level -1.s - reaction.root().assignLevels(); - contentContainer.addText("level: " + Long.toString(reaction.level)) => [ - fontBold = false - noSelectionStyle - suppressSelectability - ] + // then some reactions will have level -1. + try { + val levels = reaction.getLevels().join(", "); + contentContainer.addText("level: " + levels) => [ + fontBold = false + noSelectionStyle + suppressSelectability + ] + } catch (Exception ex) { + // If the graph has cycles, the above fails. Continue without showing levels. + } } // optional code content diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/CycleVisualization.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/CycleVisualization.xtend index e0824703a9..8a6bbce65e 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/CycleVisualization.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/CycleVisualization.xtend @@ -37,7 +37,6 @@ import org.eclipse.elk.graph.properties.Property import org.lflang.diagram.synthesis.AbstractSynthesisExtensions import org.lflang.generator.NamedInstance import org.lflang.generator.ReactorInstance -import org.lflang.graph.TopologyGraph import org.lflang.lf.Connection import static extension org.lflang.diagram.synthesis.util.NamedInstanceUtil.* @@ -60,56 +59,55 @@ class CycleVisualization extends AbstractSynthesisExtensions { * Performs cycle detection based on the diagram's graph structure and applies given highlighting to the included elements */ def boolean detectAndHighlightCycles(ReactorInstance rootReactorInstance, Map allReactorNodes, Consumer highlighter) { - val graph = new TopologyGraph(rootReactorInstance) - if (graph.hasCycles() && highlighter !== null) { + if (rootReactorInstance.hasCycles() && highlighter !== null) { // Highlight cycles - for (cycle : graph.cycles) { - // A cycle consists of reactions and ports, first find the involved reactor instances - val cycleElementsByReactor = HashMultimap.create - for (element : cycle) { - if (element instanceof ReactorInstance) { - cycleElementsByReactor.put(element, element) - } else { - cycleElementsByReactor.put(element.parent, element) - } + // A cycle consists of reactions and ports. + val cycleElementsByReactor = HashMultimap.create + val cycles = rootReactorInstance.getCycles + for (element : cycles) { + // First find the involved reactor instances + if (element instanceof ReactorInstance) { + cycleElementsByReactor.put(element, element) + } else { + cycleElementsByReactor.put(element.parent, element) } + } - for (reactor : cycleElementsByReactor.keySet) { - val node = allReactorNodes.get(reactor) - if (node !== null) { - node.setProperty(DEPENDENCY_CYCLE, true) - highlighter.accept(node) + for (reactor : cycleElementsByReactor.keySet) { + val node = allReactorNodes.get(reactor) + if (node !== null) { + node.setProperty(DEPENDENCY_CYCLE, true) + highlighter.accept(node) - val reactorContentInCycle = cycleElementsByReactor.get(reactor) - - // Reactor edges - for (edge : node.outgoingEdges) { - if (edge.connectsCycleElements(cycle)) { - edge.setProperty(DEPENDENCY_CYCLE, true) - highlighter.accept(edge) - } + val reactorContentInCycle = cycleElementsByReactor.get(reactor) + + // Reactor edges + for (edge : node.outgoingEdges) { + if (edge.connectsCycleElements(cycles)) { + edge.setProperty(DEPENDENCY_CYCLE, true) + highlighter.accept(edge) } + } - // Reactor ports - for (port : node.ports) { - if (reactorContentInCycle.contains(port.linkedInstance)) { - port.setProperty(DEPENDENCY_CYCLE, true) - highlighter.accept(port) - } + // Reactor ports + for (port : node.ports) { + if (reactorContentInCycle.contains(port.linkedInstance)) { + port.setProperty(DEPENDENCY_CYCLE, true) + highlighter.accept(port) } + } + + // Child Nodes + for (childNode : node.children) { + if (reactorContentInCycle.contains(childNode.linkedInstance) && !childNode.sourceIsReactor) { + childNode.setProperty(DEPENDENCY_CYCLE, true) + highlighter.accept(childNode) - // Child Nodes - for (childNode : node.children) { - if (reactorContentInCycle.contains(childNode.linkedInstance) && !childNode.sourceIsReactor) { - childNode.setProperty(DEPENDENCY_CYCLE, true) - highlighter.accept(childNode) - - for (edge : childNode.outgoingEdges) { - if (edge.connectsCycleElements(cycle)) { - edge.setProperty(DEPENDENCY_CYCLE, true) - highlighter.accept(edge) - } + for (edge : childNode.outgoingEdges) { + if (edge.connectsCycleElements(cycles)) { + edge.setProperty(DEPENDENCY_CYCLE, true) + highlighter.accept(edge) } } } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.xtend index 7346601aab..31031ab45f 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.xtend @@ -40,7 +40,7 @@ import org.eclipse.swt.graphics.ImageData import org.eclipse.swt.graphics.ImageLoader import org.lflang.ASTUtils import org.lflang.diagram.synthesis.AbstractSynthesisExtensions -import org.lflang.lf.Reactor +import org.lflang.lf.ReactorDecl /** * Utility class to handle icons for reactors in Lingua Franca diagrams. @@ -56,7 +56,7 @@ class ReactorIcons extends AbstractSynthesisExtensions { static val LOADER = new ImageLoader(); static val CACHE = >newHashMap // memory-sensitive cache - def void handleIcon(KContainerRendering rendering, Reactor reactor, boolean collapsed) { + def void handleIcon(KContainerRendering rendering, ReactorDecl reactor, boolean collapsed) { if (!collapsed) { return } diff --git a/org.lflang.lfc/src/org/lflang/lfc/Main.java b/org.lflang.lfc/src/org/lflang/lfc/Main.java index 51be01ce84..9ef9674c93 100644 --- a/org.lflang.lfc/src/org/lflang/lfc/Main.java +++ b/org.lflang.lfc/src/org/lflang/lfc/Main.java @@ -106,6 +106,7 @@ enum CLIOption { COMPILER(null, "target-compiler", true, false, "Target compiler to invoke.", true), CLEAN("c", "clean", false, false, "Clean before building.", true), HELP("h", "help", false, false, "Display this information.", true), + LINT("l", "lint", false, false, "Enable or disable linting of generated code.", true), NO_COMPILE("n", "no-compile", false, false, "Do not invoke target compiler.", true), FEDERATED("f", "federated", false, false, "Treat main reactor as federated.", false), THREADS("t", "threads", true, false, "Specify the default number of threads.", true), @@ -120,7 +121,7 @@ enum CLIOption { public final Option option; /** - * Whether or not to pass this option to the code generator. + * Whether to pass this option to the code generator. */ public final boolean passOn; diff --git a/org.lflang.tests/src/org/lflang/tests/LFTest.java b/org.lflang.tests/src/org/lflang/tests/LFTest.java index 0f875036b1..64a6691fca 100644 --- a/org.lflang.tests/src/org/lflang/tests/LFTest.java +++ b/org.lflang.tests/src/org/lflang/tests/LFTest.java @@ -28,6 +28,9 @@ public class LFTest implements Comparable { /** The result of the test. */ public Result result = Result.UNKNOWN; + + /** The exit code of the test. **/ + public String exitValue = "?"; /** Object used to determine where the code generator puts files. */ public FileConfig fileConfig; @@ -128,7 +131,7 @@ public String reportErrors() { sb.append("+---------------------------------------------------------------------------+").append(System.lineSeparator()); sb.append("Failed: ").append(this.name).append(System.lineSeparator()); sb.append("-----------------------------------------------------------------------------").append(System.lineSeparator()); - sb.append("Reason: ").append(this.result.message).append(System.lineSeparator()); + sb.append("Reason: ").append(this.result.message).append(" Exit code: ").append(this.exitValue).append(System.lineSeparator()); appendIfNotEmpty("Reported issues", this.issues.toString(), sb); appendIfNotEmpty("Compilation output", this.compilationLog.toString(), sb); appendIfNotEmpty("Execution output", this.execLog.toString(), sb); @@ -164,6 +167,7 @@ public enum Result { CODE_GEN_FAIL("Error while generating code for test."), NO_EXEC_FAIL("Did not execute test."), TEST_FAIL("Test did not pass."), + TEST_EXCEPTION("Test exited with an exception."), TEST_TIMEOUT("Test timed out."), TEST_PASS("Test passed."); diff --git a/org.lflang.tests/src/org/lflang/tests/TestBase.java b/org.lflang.tests/src/org/lflang/tests/TestBase.java index eaa0d119c9..8c8521a967 100644 --- a/org.lflang.tests/src/org/lflang/tests/TestBase.java +++ b/org.lflang.tests/src/org/lflang/tests/TestBase.java @@ -5,6 +5,8 @@ import java.io.IOException; import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.nio.file.Path; @@ -469,12 +471,18 @@ private void execute(LFTest test, GeneratorResult generatorResult) { } else { if (p.exitValue() != 0) { test.result = Result.TEST_FAIL; + test.exitValue = Integer.toString(p.exitValue()); return; } } } } catch (Exception e) { - test.result = Result.TEST_FAIL; + test.result = Result.TEST_EXCEPTION; + // Add the stack trace to the test output + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + test.execLog.buffer.append(sw.toString()); return; } test.result = Result.TEST_PASS; diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaASTUtilsTest.java b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaASTUtilsTest.java new file mode 100644 index 0000000000..7b738bf043 --- /dev/null +++ b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaASTUtilsTest.java @@ -0,0 +1,274 @@ +/* ASTUtils Unit Tests. */ + +/************* +Copyright (c) 2019, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ + +package org.lflang.tests.compiler; + +import static org.lflang.ASTUtils.isInitialized; +import static org.lflang.util.XtendUtil.asStream; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.eclipse.xtext.testing.util.ParseHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lflang.ASTUtils; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Model; +import org.lflang.lf.Parameter; +import org.lflang.lf.StateVar; +import org.lflang.tests.LFInjectorProvider; + +/** + * Collection of unit tests on the ASTutils. + * + * @author{Christian Menard } + */ + +@ExtendWith(InjectionExtension.class) +@InjectWith(LFInjectorProvider.class) + +class LinguaFrancaASTUtilsTest { + @Inject + ParseHelper parser; + + /** + * Test that isInititialized returns true for inititialized state variables + */ + @Test + public void initializedState() throws Exception { +// Java 17: +// Model model = parser.parse(""" +// target Cpp; +// main reactor Foo { +// state a(); +// state b:int(3); +// state c:int[](1,2,3); +// state d(1 sec); +// state e(1 sec, 2 sec, 3 sec); +// } +// """); +// Java 11: + Model model = parser.parse(String.join( + System.getProperty("line.separator"), + "target Cpp;", + "main reactor {", + " state a();", + " state b:int(3);", + " state c:int[](1,2,3);", + " state d(1 sec);", + " state e(1 sec, 2 sec, 3 sec);", + "}" + )); + + + + Assertions.assertNotNull(model); + Assertions.assertTrue(model.eResource().getErrors().isEmpty(), + "Encountered unexpected error while parsing: " + + model.eResource().getErrors()); + + model.eAllContents().forEachRemaining((obj) -> { + if (obj instanceof StateVar) { + Assertions.assertTrue(isInitialized((StateVar)obj)); + } + }); + } + + /** + * Test that isInititialized returns false for uninititialized state variables + */ + @Test + public void uninitializedState() throws Exception { +// Java 17: +// Model model = parser.parse(""" +// target Cpp; +// main reactor Foo { +// state a; +// state b:int; +// state c:int[]; +// state d:time; +// state e:time[]; +// } +// ''' +// Java 11: + Model model = parser.parse(String.join( + System.getProperty("line.separator"), + "target Cpp;", + "main reactor {", + " state a;", + " state b:int;", + " state c:int[];", + " state d:time;", + " state e:time;", + "}" + )); + + Assertions.assertNotNull(model); + Assertions.assertTrue(model.eResource().getErrors().isEmpty(), + "Encountered unexpected error while parsing: " + + model.eResource().getErrors()); + + model.eAllContents().forEachRemaining((obj) -> { + if (obj instanceof StateVar) { + Assertions.assertFalse(isInitialized((StateVar)obj)); + } + }); + + } + /** + * Return a map from strings to instantiations given a model. + * + * @param model The model to discover instantiations in. + */ + private Map getInsts(Model model) { + return asStream(model.eAllContents()) + .filter(obj -> obj instanceof Instantiation) + .map(obj -> (Instantiation) obj) + .collect(Collectors.toMap(Instantiation::getName, it -> it)); + } + + /** + * Test reading initial values of parameters. + * This checks that the example given in the documentation of the + * ASTUtils.initialValue() function behaves as stated in the docs. + */ + @Test + public void initialValue() throws Exception { + +// Java 17: +// Model model = parser.parse(""" +// target C; +// reactor A(x:int(1)) {} +// reactor B(y:int(2)) { +// a1 = new A(x = y); +// a2 = new A(x = -1); +// } +// reactor C(z:int(3)) { +// b1 = new B(y = z); +// b2 = new B(y = -2); +// } +// """ +// Java 11: + + Model model = parser.parse(String.join( + System.getProperty("line.separator"), + "target C;", + "reactor A(x:int(1)) {}", + "reactor B(y:int(2)) {", + " a1 = new A(x = y);", + " a2 = new A(x = -1);", + "}", + "reactor C(z:int(3)) {", + " b1 = new B(y = z);", + " b2 = new B(y = -2);", + "}" + )); + + Assertions.assertNotNull(model); + Assertions.assertTrue(model.eResource().getErrors().isEmpty(), + "Encountered unexpected error while parsing: " + + model.eResource().getErrors()); + + var map = getInsts(model); + + /* Check for this: + * initialValue(x, null) returns 1 + * initialValue(x, [a1]) returns 2 + * initialValue(x, [a2]) returns -1 + * initialValue(x, [a1, b1]) returns 3 + * initialValue(x, [a2, b1]) returns -1 + * initialValue(x, [a1, b2]) returns -2 + * initialValue(x, [a2, b2]) returns -1 + * + * initialValue(y, null) returns 2 + * initialValue(y, [a1]) throws an IllegalArgumentException + * initialValue(y, [b1]) returns 3 + * initialValue(y, [b2]) returns -2 + */ + + model.eAllContents().forEachRemaining((obj) -> { + if (obj instanceof Parameter) { + Parameter parameter = (Parameter)obj; + if (parameter.getName() == "x") { + var values = ASTUtils.initialValue(parameter, null); + Assertions.assertEquals(values.get(0).getLiteral(), "1"); + + values = ASTUtils.initialValue(parameter, + List.of(map.get("a1"))); + Assertions.assertEquals(values.get(0).getLiteral(), "2"); + + values = ASTUtils.initialValue(parameter, + List.of(map.get("a2"))); + Assertions.assertEquals(values.get(0).getLiteral(), "-1"); + + values = ASTUtils.initialValue(parameter, + List.of(map.get("a1"), map.get("b1"))); + Assertions.assertEquals(values.get(0).getLiteral(), "3"); + + values = ASTUtils.initialValue(parameter, + List.of(map.get("a2"), map.get("b1"))); + Assertions.assertEquals(values.get(0).getLiteral(), "-1"); + + values = ASTUtils.initialValue(parameter, + List.of(map.get("a1"), map.get("b2"))); + Assertions.assertEquals(values.get(0).getLiteral(), "-2"); + + values = ASTUtils.initialValue(parameter, + List.of(map.get("a2"), map.get("b2"))); + Assertions.assertEquals(values.get(0).getLiteral(), "-1"); + } else if (parameter.getName() == "y") { + var values = ASTUtils.initialValue(parameter, null); + Assertions.assertEquals(values.get(0).getLiteral(), "2"); + + try { + values = ASTUtils.initialValue(parameter, + List.of(map.get("a1"))); + } catch (IllegalArgumentException ex) { + Assertions.assertTrue(ex.getMessage() + .startsWith("Parameter y is not")); + } + + values = ASTUtils.initialValue(parameter, + List.of(map.get("b1"))); + Assertions.assertEquals(values.get(0).getLiteral(), "3"); + + values = ASTUtils.initialValue(parameter, + List.of(map.get("b2"))); + Assertions.assertEquals(values.get(0).getLiteral(), "-2"); + } + } + }); + } +} diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaASTUtilsTest.xtend b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaASTUtilsTest.xtend deleted file mode 100644 index 48b88a71d3..0000000000 --- a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaASTUtilsTest.xtend +++ /dev/null @@ -1,223 +0,0 @@ -/* ASTUtils Unit Tests. */ - -/************* -Copyright (c) 2019, The University of California at Berkeley. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ - -package org.lflang.tests.compiler - -import java.util.LinkedList -import javax.inject.Inject -import org.eclipse.xtext.testing.InjectWith -import org.eclipse.xtext.testing.extensions.InjectionExtension -import org.eclipse.xtext.testing.util.ParseHelper -import org.lflang.ASTUtils -import org.lflang.lf.Instantiation -import org.lflang.lf.Model -import org.lflang.lf.Parameter -import org.lflang.lf.StateVar -import org.lflang.tests.LFInjectorProvider -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.^extension.ExtendWith - -import static extension org.lflang.ASTUtils.* - -/** - * Collection of unit tests on the ASTutils. - * - * @author{Christian Menard } - */ -@ExtendWith(InjectionExtension) -@InjectWith(LFInjectorProvider) -class LinguaFrancaASTUtilsTest { - @Inject extension ParseHelper - - /** - * Test that isInititialized returns true for inititialized state variables - */ - @Test - def void initializedState() { - val model = ''' - target Cpp; - main reactor Foo { - state a(); - state b:int(3); - state c:int[](1,2,3); - state d(1 sec); - state e(1 sec, 2 sec, 3 sec); - } - '''.parse - - Assertions.assertNotNull(model) - Assertions.assertTrue(model.eResource.errors.isEmpty, - "Encountered unexpected error while parsing: " + - model.eResource.errors) - - for (state : model.eAllContents.filter(StateVar).toList) { - Assertions.assertTrue(state.isInitialized) - } - } - - /** - * Test that isInititialized returns false for uninititialized state variables - */ - @Test - def void uninitializedState() { - val model = ''' - target Cpp; - main reactor Foo { - state a; - state b:int; - state c:int[]; - state d:time; - state e:time[]; - } - '''.parse - - Assertions.assertNotNull(model) - Assertions.assertTrue(model.eResource.errors.isEmpty, - "Encountered unexpected error while parsing: " + - model.eResource.errors) - - for (state : model.eAllContents.filter(StateVar).toList) { - Assertions.assertFalse(state.isInitialized) - } - } - - /** - * Test reading initial values of parameters. - * This checks that the example given in the documentation of the - * ASTUtils.initialValue() function behaves as stated in the docs. - */ - @Test - def void initialValue() { - val model = ''' - target C; - reactor A(x:int(1)) {} - reactor B(y:int(2)) { - a1 = new A(x = y); - a2 = new A(x = -1); - } - reactor C(z:int(3)) { - b1 = new B(y = z); - b2 = new B(y = -2); - } - '''.parse - Assertions.assertNotNull(model) - Assertions.assertTrue(model.eResource.errors.isEmpty, - "Encountered unexpected error while parsing: " + - model.eResource.errors) - - // Find all the Instantiations. - var a1 = null as Instantiation; - var a2 = null as Instantiation; - var b1 = null as Instantiation; - var b2 = null as Instantiation; - for (instantiation: model.eAllContents.filter(Instantiation).toList) { - switch (instantiation.name) { - case "a1": a1 = instantiation - case "a2": a2 = instantiation - case "b1": b1 = instantiation - case "b2": b2 = instantiation - } - } - - // Construct all relevant instantiation lists. - val list_a1 = new LinkedList(); - list_a1.add(a1); - val list_a2 = new LinkedList(); - list_a2.add(a2); - val list_a1b1 = new LinkedList(); - list_a1b1.add(a1); - list_a1b1.add(b1); - val list_a2b1 = new LinkedList(); - list_a2b1.add(a2); - list_a2b1.add(b1); - val list_a1b2 = new LinkedList(); - list_a1b2.add(a1); - list_a1b2.add(b2); - val list_a2b2 = new LinkedList(); - list_a2b2.add(a2); - list_a2b2.add(b2); - val list_b1 = new LinkedList(); - list_b1.add(b1); - val list_b2 = new LinkedList(); - list_b2.add(b2); - - /* Check for this: - * initialValue(x, null) returns 1 - * initialValue(x, [a1]) returns 2 - * initialValue(x, [a2]) returns -1 - * initialValue(x, [a1, b1]) returns 3 - * initialValue(x, [a2, b1]) returns -1 - * initialValue(x, [a1, b2]) returns -2 - * initialValue(x, [a2, b2]) returns -1 - * - * initialValue(y, null) returns 2 - * initialValue(y, [a1]) throws an IllegalArgumentException - * initialValue(y, [b1]) returns 3 - * initialValue(y, [b2]) returns -2 - */ - for (parameter : model.eAllContents.filter(Parameter).toList) { - if (parameter.name == 'x') { - var values = ASTUtils.initialValue(parameter, null); - Assertions.assertEquals(values.get(0).literal, "1"); - - values = ASTUtils.initialValue(parameter, list_a1); - Assertions.assertEquals(values.get(0).literal, "2"); - - values = ASTUtils.initialValue(parameter, list_a2); - Assertions.assertEquals(values.get(0).literal, "-1"); - - values = ASTUtils.initialValue(parameter, list_a1b1); - Assertions.assertEquals(values.get(0).literal, "3"); - - values = ASTUtils.initialValue(parameter, list_a2b1); - Assertions.assertEquals(values.get(0).literal, "-1"); - - values = ASTUtils.initialValue(parameter, list_a1b2); - Assertions.assertEquals(values.get(0).literal, "-2"); - - values = ASTUtils.initialValue(parameter, list_a2b2); - Assertions.assertEquals(values.get(0).literal, "-1"); - } else if (parameter.name == 'y') { - var values = ASTUtils.initialValue(parameter, null); - Assertions.assertEquals(values.get(0).literal, "2"); - - try { - values = ASTUtils.initialValue(parameter, list_a1); - } catch (IllegalArgumentException ex) { - Assertions.assertTrue(ex.message.startsWith("Parameter y is not")); - } - - values = ASTUtils.initialValue(parameter, list_b1); - Assertions.assertEquals(values.get(0).literal, "3"); - - values = ASTUtils.initialValue(parameter, list_b2); - Assertions.assertEquals(values.get(0).literal, "-2"); - } - } - } -} diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaDependencyAnalysisTest.java b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaDependencyAnalysisTest.java index a8c94f54cd..db89a32a87 100644 --- a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaDependencyAnalysisTest.java +++ b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaDependencyAnalysisTest.java @@ -39,8 +39,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.lflang.DefaultErrorReporter; import org.lflang.ModelInfo; -import org.lflang.generator.InvalidSourceException; -import org.lflang.generator.ReactionInstanceGraph; import org.lflang.generator.ReactorInstance; import org.lflang.lf.Instantiation; import org.lflang.lf.LfFactory; @@ -147,14 +145,8 @@ public void cyclicDependency() throws Exception { } } - try { - ReactorInstance instance = new ReactorInstance((Reactor) mainDef.getReactorClass(), new DefaultErrorReporter()); - new ReactionInstanceGraph(instance); - Assertions.fail("No cycle detected"); - } catch(InvalidSourceException e) { - Assertions.assertTrue(e.getMessage() != null && e.getMessage().contains("Reactions form a cycle!"), - "Should be a message about cycles: " + e.getMessage()); - } + ReactorInstance instance = new ReactorInstance((Reactor) mainDef.getReactorClass(), new DefaultErrorReporter()); + Assertions.assertFalse(instance.getCycles().isEmpty()); } /** diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaScopingTest.java b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaScopingTest.java new file mode 100644 index 0000000000..70c3880a49 --- /dev/null +++ b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaScopingTest.java @@ -0,0 +1,244 @@ +/* Scoping unit tests. */ + +/************* +Copyright (c) 2019, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.tests.compiler; + +import com.google.inject.Inject; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.eclipse.xtext.testing.util.ParseHelper; +import org.lflang.lf.Model; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; +import org.eclipse.xtext.testing.validation.ValidationTestHelper; +import org.lflang.lf.LfPackage; +import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic; +import org.lflang.tests.LFInjectorProvider; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.xtext.generator.JavaIoFileSystemAccess; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lflang.generator.LFGenerator; +import com.google.inject.Provider; + +@ExtendWith(InjectionExtension.class) +@InjectWith(LFInjectorProvider.class) + +/** + * Test harness for ensuring that cross-references are + * established correctly and reported when faulty. + */ +public class LinguaFrancaScopingTest { + @Inject + ParseHelper parser; + + @Inject + LFGenerator generator; + + @Inject + JavaIoFileSystemAccess fileAccess; + + @Inject + Provider resourceSetProvider; + + @Inject + ValidationTestHelper validator; + + /** + * Ensure that invalid references to contained reactors are reported. + */ + @Test + public void unresolvedReactorReference() throws Exception { +// Java 17: +// Model model = """ +// target C; +// reactor From { +// output y:int; +// } +// reactor To { +// input x:int; +// } +// +// main reactor { +// a = new From(); +// d = new To(); +// s.y -> d.x; +// } +// """; +// Java 11: + Model model = parser.parse(String.join( + System.getProperty("line.separator"), + "target C;", + "reactor From {", + " output y:int;", + "}", + "reactor To {", + " input x:int;", + "}", + "", + "main reactor {", + " a = new From();", + " d = new To();", + " s.y -> d.x;", + "}" + )); + + Assertions.assertNotNull(model); + Assertions.assertTrue(model.eResource().getErrors().isEmpty(), + "Encountered unexpected error while parsing."); + validator.assertError(model, LfPackage.eINSTANCE.getVarRef(), + XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, + "Couldn't resolve reference to Instantiation 's'"); + validator.assertError(model, LfPackage.eINSTANCE.getVarRef(), + XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, + "Couldn't resolve reference to Variable 'y'"); + } + + + /** + * Ensure that invalid references to ports + * of contained reactors are reported. + */ + @Test + public void unresolvedHierarchicalPortReference() throws Exception { +// Java 17: +// Model model = """ +// target C; +// reactor From { +// output y:int; +// } +// reactor To { +// input x:int; +// } +// +// main reactor { +// a = new From(); +// d = new To(); +// a.x -> d.y; +// } +// """; +// Java 11: + + Model model = parser.parse(String.join( + System.getProperty("line.separator"), + "target C;", + "reactor From {", + " output y:int;", + "}", + "reactor To {", + " input x:int;", + "}", + "main reactor {", + " a = new From();", + " d = new To();", + " a.x -> d.y;", + "}" + )); + + + Assertions.assertNotNull(model); + Assertions.assertTrue(model.eResource().getErrors().isEmpty(), + "Encountered unexpected error while parsing."); + validator.assertError(model, LfPackage.eINSTANCE.getVarRef(), + XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, + "Couldn't resolve reference to Variable 'x'"); + validator.assertError(model, LfPackage.eINSTANCE.getVarRef(), + XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, + "Couldn't resolve reference to Variable 'y'"); + } + + @Test + public void unresolvedReferenceInTriggerClause() throws Exception { +// Java 17: +// Model model = """ +// target C; +// main reactor { +// reaction(unknown) {==} +// } +// """ +// Java 11: + Model model = parser.parse(String.join( + System.getProperty("line.separator"), + "target C;", + "main reactor {", + " reaction(unknown) {==}", + "}" + )); + + validator.assertError(model, LfPackage.eINSTANCE.getVarRef(), + XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, + "Couldn't resolve reference to Variable 'unknown'."); + } + + @Test + public void unresolvedReferenceInUseClause() throws Exception { +// Java 17: +// Model model = """ +// target C; +// main reactor { +// reaction() unknown {==} +// } +// """ +// Java 11: + Model model = parser.parse(String.join( + System.getProperty("line.separator"), + "target C;", + "main reactor {", + " reaction() unknown {==}", + "}" + )); + + + validator.assertError(model, LfPackage.eINSTANCE.getVarRef(), + XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, + "Couldn't resolve reference to Variable 'unknown'."); + } + + @Test + public void unresolvedReferenceInEffectsClause() throws Exception { +// Java 17: +// Model model = """ +// target C; +// main reactor { +// reaction() -> unknown {==} +// } +// """ +// Java 11: + + Model model = parser.parse(String.join( + System.getProperty("line.separator"), + "target C;", + "main reactor {", + " reaction() -> unknown {==}", + "}" + )); + + + validator.assertError(model, LfPackage.eINSTANCE.getVarRef(), + XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, + "Couldn't resolve reference to Variable 'unknown'."); + } + +} diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaScopingTest.xtend b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaScopingTest.xtend deleted file mode 100644 index ea41293187..0000000000 --- a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaScopingTest.xtend +++ /dev/null @@ -1,155 +0,0 @@ -/* Scoping unit tests. */ - -/************* -Copyright (c) 2019, The University of California at Berkeley. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.tests.compiler - -import com.google.inject.Inject -import org.eclipse.xtext.testing.InjectWith -import org.eclipse.xtext.testing.extensions.InjectionExtension -import org.eclipse.xtext.testing.util.ParseHelper -import org.lflang.lf.Model -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.^extension.ExtendWith -import org.eclipse.xtext.testing.validation.ValidationTestHelper -import org.lflang.lf.LfPackage -import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic -import org.lflang.tests.LFInjectorProvider - -@ExtendWith(InjectionExtension) -@InjectWith(LFInjectorProvider) - -/** - * Test harness for ensuring that cross-references are - * established correctly and reported when faulty. - */ -class LinguaFrancaScopingTest { - @Inject extension ParseHelper - @Inject extension ValidationTestHelper - - /** - * Ensure that invalid references to contained reactors are reported. - */ - @Test - def void unresolvedReactorReference() { - val model = ''' - target C; - reactor From { - output y:int; - } - reactor To { - input x:int; - } - - main reactor WrongConnect { - a = new From(); - d = new To(); - s.y -> d.x; - } - '''.parse - - Assertions.assertNotNull(model) - Assertions.assertTrue(model.eResource.errors.isEmpty, - "Encountered unexpected error while parsing.") - model.assertError(LfPackage::eINSTANCE.varRef, - XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, - "Couldn't resolve reference to Instantiation 's'") - model.assertError(LfPackage::eINSTANCE.varRef, - XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, - "Couldn't resolve reference to Variable 'y'") - } - - - /** - * Ensure that invalid references to ports - * of contained reactors are reported. - */ - @Test - def void unresolvedHierarchicalPortReference() { - val model = ''' - target C; - reactor From { - output y:int; - } - reactor To { - input x:int; - } - - main reactor WrongConnect { - a = new From(); - d = new To(); - a.x -> d.y; - } - '''.parse - - Assertions.assertNotNull(model) - Assertions.assertTrue(model.eResource.errors.isEmpty, - "Encountered unexpected error while parsing.") - model.assertError(LfPackage::eINSTANCE.varRef, - XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, - "Couldn't resolve reference to Variable 'x'") - model.assertError(LfPackage::eINSTANCE.varRef, - XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, - "Couldn't resolve reference to Variable 'y'") - } - - @Test - def void unresolvedReferenceInTriggerClause() { - ''' - target C; - main reactor { - reaction(unknown) {==} - } - '''.parse.assertError(LfPackage::eINSTANCE.varRef, - XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, - "Couldn't resolve reference to Variable 'unknown'.") - } - - @Test - def void unresolvedReferenceInUseClause() { - ''' - target C; - main reactor { - reaction() unknown {==} - } - '''.parse.assertError(LfPackage::eINSTANCE.varRef, - XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, - "Couldn't resolve reference to Variable 'unknown'.") - } - - @Test - def void unresolvedReferenceInEffectsClause() { - ''' - target C; - main reactor { - reaction() -> unknown {==} - } - '''.parse.assertError(LfPackage::eINSTANCE.varRef, - XtextLinkingDiagnostic.LINKING_DIAGNOSTIC, - "Couldn't resolve reference to Variable 'unknown'.") - } - -} diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/MixedRadixIntTest.java b/org.lflang.tests/src/org/lflang/tests/compiler/MixedRadixIntTest.java new file mode 100644 index 0000000000..2cee2468c8 --- /dev/null +++ b/org.lflang.tests/src/org/lflang/tests/compiler/MixedRadixIntTest.java @@ -0,0 +1,112 @@ +package org.lflang.tests.compiler; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.lflang.generator.MixedRadixInt; + +public class MixedRadixIntTest { + + // Constants used many times below. + List radixes = Arrays.asList(2, 3, 4, 5); + List digits = Arrays.asList(1, 2, 3, 4); + + @Test + public void create() throws Exception { + MixedRadixInt num = new MixedRadixInt(digits, radixes, null); + Assertions.assertEquals("1%2, 2%3, 3%4, 4%5", num.toString()); + Assertions.assertEquals(1 + 2*2 + 3*6 + 4*24, num.get()); + + List altDigits = List.of(1, 2, 1); + MixedRadixInt altNum = new MixedRadixInt(altDigits, radixes, null); + Assertions.assertEquals(11, altNum.get()); + } + + @Test + public void createWithInfinity() throws Exception { + List radixes = List.of(2, 3, 4, 10000); + MixedRadixInt num = new MixedRadixInt(digits, radixes, null); + Assertions.assertEquals(1 + 2*2 + 3*6 + 4*24, num.get()); + } + + @Test + public void createWithError() throws Exception { + List radixes = List.of(2, 3); + try { + new MixedRadixInt(digits, radixes, null); + } catch (IllegalArgumentException ex) { + Assertions.assertTrue(ex.getMessage().startsWith("Invalid")); + return; + } + Assertions.assertTrue(false, "Expected exception did not occur."); + } + + @Test + public void createWithNullAndSet() throws Exception { + MixedRadixInt num = new MixedRadixInt(null, radixes, null); + Assertions.assertEquals(0, num.get()); + Assertions.assertEquals("0%2", num.toString()); + num.set(1 + 2*2 + 3*6 + 4*24); + Assertions.assertEquals(1 + 2*2 + 3*6 + 4*24, num.get()); + int mag = num.magnitude(); + Assertions.assertEquals(1 + 2*2 + 3*6 + 4*24, mag); + num.increment(); // wrap to zero. + Assertions.assertEquals(0, num.get()); + Assertions.assertEquals(0, num.magnitude()); + + num = new MixedRadixInt(null, radixes, null); + num.setMagnitude(mag); + Assertions.assertEquals(mag, num.magnitude()); + } + + @Test + public void testPermutation() throws Exception { + List radixes = Arrays.asList(2, 5); + List digits = Arrays.asList(1, 2); + List permutation = Arrays.asList(1, 0); + MixedRadixInt num = new MixedRadixInt(digits, radixes, permutation); + Assertions.assertEquals(5, num.get()); + Assertions.assertEquals(2, num.get(1)); + Assertions.assertEquals(7, num.magnitude()); + num.increment(); + Assertions.assertEquals(7, num.get()); + Assertions.assertEquals(8, num.magnitude()); + + num = new MixedRadixInt(null, radixes, permutation); + num.setMagnitude(8); + Assertions.assertEquals(8, num.magnitude()); + Assertions.assertEquals(7, num.get()); + + // Test radix infinity digit (effectively). + digits = Arrays.asList(1, 2, 1); + radixes = Arrays.asList(2, 5, 1000000); + num = new MixedRadixInt(digits, radixes, null); + Assertions.assertEquals(15, num.get()); + Assertions.assertEquals(7, num.get(1)); + Assertions.assertEquals(15, num.magnitude()); + + permutation = Arrays.asList(1, 0, 2); + num = new MixedRadixInt(digits, radixes, permutation); + num.increment(); + Assertions.assertEquals(17, num.get()); + Assertions.assertEquals(18, num.magnitude()); + + num = new MixedRadixInt(null, radixes, permutation); + num.setMagnitude(18); + Assertions.assertEquals(18, num.magnitude()); + Assertions.assertEquals(17, num.get()); + } + + @Test + public void testIncrement() throws Exception { + List radixes = Arrays.asList(2, 3); + List digits = Arrays.asList(0, 2); + MixedRadixInt num = new MixedRadixInt(digits, radixes, null); + num.increment(); + Assertions.assertEquals(5, num.get()); + num.increment(); // Wrap around to zero. + Assertions.assertEquals(0, num.get()); + } +} diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/PortInstanceTests.java b/org.lflang.tests/src/org/lflang/tests/compiler/PortInstanceTests.java new file mode 100644 index 0000000000..f5f2ba387b --- /dev/null +++ b/org.lflang.tests/src/org/lflang/tests/compiler/PortInstanceTests.java @@ -0,0 +1,221 @@ +package org.lflang.tests.compiler; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.lflang.DefaultErrorReporter; +import org.lflang.ErrorReporter; +import org.lflang.generator.PortInstance; +import org.lflang.generator.RuntimeRange; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.SendRange; +import org.lflang.lf.LfFactory; +import org.lflang.lf.Port; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; + +public class PortInstanceTests { + + private ErrorReporter reporter = new DefaultErrorReporter(); + private static LfFactory factory = LfFactory.eINSTANCE; + + @Test + public void createRange() throws Exception { + Reactor main = factory.createReactor(); + ReactorInstance maini = new ReactorInstance(main, reporter); + + ReactorInstance a = newReactor("A", maini); + ReactorInstance b = newReactor("B", maini); + ReactorInstance c = newReactor("C", maini); + + PortInstance p = newOutputPort("p", a); + PortInstance q = newInputPort("q", b); + PortInstance r = newInputPort("r", c); + + Assertions.assertEquals(".A.p", p.getFullName()); + + connect(p, q); + connect(p, r); + + List sr = p.eventualDestinations(); + // Destinations should be empty because there are no reactions. + Assertions.assertEquals("[]", sr.toString()); + + // Clear caches to make a mutation. + maini.clearCaches(); + newReaction(q); + // Re-retrieve destinations. + sr = p.eventualDestinations(); + Assertions.assertEquals("[.A.p(0,1)->[.B.q(0,1)]]", sr.toString()); + + maini.clearCaches(); + newReaction(r); + // Re-retrieve destinations. + sr = p.eventualDestinations(); + Assertions.assertEquals("[.A.p(0,1)->[.B.q(0,1), .C.r(0,1)]]", sr.toString()); + + // Now test multiports. + p.setWidth(3); + r.setWidth(2); + // Have to redo the connections. + clearConnections(maini); + maini.clearCaches(); + connect(p, 0, 1, q, 0, 1); + connect(p, 1, 2, r, 0, 2); + + // Re-retrieve destinations. + sr = p.eventualDestinations(); + Assertions.assertEquals("[.A.p(0,1)->[.B.q(0,1)], .A.p(1,2)->[.C.r(0,2)]]", sr.toString()); + + // More complicated multiport connection. + clearConnections(maini); + maini.clearCaches(); + + ReactorInstance d = newReactor("D", maini); + PortInstance v = newOutputPort("v", d); + v.setWidth(2); + q.setWidth(3); + connect(v, 0, 2, q, 0, 2); + connect(p, 0, 1, q, 2, 1); + connect(p, 1, 2, r, 0, 2); + + sr = p.eventualDestinations(); + Assertions.assertEquals("[.A.p(0,1)->[.B.q(2,1)], .A.p(1,2)->[.C.r(0,2)]]", sr.toString()); + + // Additional multicast connection. + maini.clearCaches(); + ReactorInstance e = newReactor("E", maini); + PortInstance s = newPort("s", e); + s.setWidth(3); + newReaction(s); + connect(p, s); + + sr = p.eventualDestinations(); + Assertions.assertEquals("[.A.p(0,1)->[.B.q(2,1), .E.s(0,1)], .A.p(1,2)->[.C.r(0,2), .E.s(1,2)]]", sr.toString()); + + // Add hierarchical reactors that further split the ranges. + maini.clearCaches(); + ReactorInstance f = newReactor("F", e); + PortInstance t = newPort("t", f); + newReaction(t); + ReactorInstance g = newReactor("G", e); + PortInstance u = newPort("u", g); + u.setWidth(2); + newReaction(u); + connect(s, 0, 1, t, 0, 1); + connect(s, 1, 2, u, 0, 2); + + sr = p.eventualDestinations(); + // FIXME: Multicast destinations should be able to be reported in arbitrary order. + Assertions.assertEquals("[.A.p(0,1)->[.E.F.t(0,1), .E.s(0,1), .B.q(2,1)], .A.p(1,2)->[.E.G.u(0,2), .E.s(1,2), .C.r(0,2)]]", sr.toString()); + } + + @Test + public void multiportDestination() throws Exception { + Reactor main = factory.createReactor(); + ReactorInstance maini = new ReactorInstance(main, reporter); + + ReactorInstance a = newReactor("A", maini); + ReactorInstance b = newReactor("B", maini); + b.setWidth(4); + + PortInstance p = newOutputPort("p", a); + PortInstance q = newInputPort("q", b); + + connect(p, 0, 1, q, 0, 4); + + List sr = p.eventualDestinations(); + // Destination has no reactions, so empty list is right. + Assertions.assertEquals("[]", sr.toString()); + + maini.clearCaches(); + newReaction(q); + sr = p.eventualDestinations(); + Assertions.assertEquals("[.A.p(0,1)->[.B.q(0,4)]]", sr.toString()); +} + + /** + * Clear connections. This recursively clears them for all contained reactors. + */ + protected void clearConnections(ReactorInstance r) { + for (PortInstance p: r.inputs) { + p.getDependentPorts().clear(); + } + for (PortInstance p: r.outputs) { + p.getDependentPorts().clear(); + } + for (ReactorInstance child: r.children) { + clearConnections(child); + } + } + + /** + * Simple connection of two ports. This should be used only + * for connections that would be allowed in the syntax (i.e., no + * cross-hierarchy connections), but this is not checked. + * @param src The sending port. + * @param dst The receiving port. + */ + protected void connect(PortInstance src, PortInstance dst) { + RuntimeRange srcRange = new RuntimeRange.Port(src); + RuntimeRange dstRange = new RuntimeRange.Port(dst); + ReactorInstance.connectPortInstances(srcRange, dstRange, null); + } + + /** + * Connection between multiports. This should be used only + * for connections that would be allowed in the syntax (i.e., no + * cross-hierarchy connections), but this is not checked. + * @param src The sending port. + * @param dst The receiving port. + */ + protected void connect( + PortInstance src, int srcStart, int srcWidth, + PortInstance dst, int dstStart, int dstWidth + ) { + RuntimeRange srcRange = new RuntimeRange.Port(src, srcStart, srcWidth, null); + RuntimeRange dstRange = new RuntimeRange.Port(dst, dstStart, dstWidth, null); + ReactorInstance.connectPortInstances(srcRange, dstRange, null); + } + + protected PortInstance newPort(String name, ReactorInstance container) { + Port p = factory.createPort(); + p.setName(name); + return new PortInstance(p, container, reporter); + } + + protected PortInstance newInputPort(String name, ReactorInstance container) { + PortInstance pi = newPort(name, container); + container.inputs.add(pi); + return pi; + } + + protected PortInstance newOutputPort(String name, ReactorInstance container) { + PortInstance pi = newPort(name, container); + container.outputs.add(pi); + return pi; + } + + /** + * Return a new reaction triggered by the specified port. + * @param trigger The triggering port. + */ + protected ReactionInstance newReaction(PortInstance trigger) { + Reaction r = factory.createReaction(); + ReactionInstance result = new ReactionInstance( + r, trigger.getParent(), false, trigger.getDependentReactions().size()); + trigger.getDependentReactions().add(result); + trigger.getParent().reactions.add(result); + return result; + } + + protected ReactorInstance newReactor(String name, ReactorInstance container) { + Reactor r = factory.createReactor(); + r.setName(name); + ReactorInstance ri = new ReactorInstance(r, container, reporter); + container.children.add(ri); + return ri; + } +} diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/RangeTests.java b/org.lflang.tests/src/org/lflang/tests/compiler/RangeTests.java new file mode 100644 index 0000000000..5cd489d0f7 --- /dev/null +++ b/org.lflang.tests/src/org/lflang/tests/compiler/RangeTests.java @@ -0,0 +1,103 @@ +package org.lflang.tests.compiler; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.lflang.DefaultErrorReporter; +import org.lflang.ErrorReporter; +import org.lflang.generator.PortInstance; +import org.lflang.generator.RuntimeRange; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.SendRange; +import org.lflang.lf.LfFactory; +import org.lflang.lf.Port; +import org.lflang.lf.Reactor; + +public class RangeTests { + + private ErrorReporter reporter = new DefaultErrorReporter(); + private static LfFactory factory = LfFactory.eINSTANCE; + + @Test + public void createRange() throws Exception { + Reactor main = factory.createReactor(); + ReactorInstance maini = new ReactorInstance(main, reporter); + + Reactor a = factory.createReactor(); + a.setName("A"); + ReactorInstance ai = new ReactorInstance(a, maini, reporter); + ai.setWidth(2); + + Reactor b = factory.createReactor(); + b.setName("B"); + ReactorInstance bi = new ReactorInstance(b, ai, reporter); + bi.setWidth(2); + + Port p = factory.createPort(); + p.setName("P"); + PortInstance pi = new PortInstance(p, bi, reporter); + pi.setWidth(2); + + Assertions.assertEquals(".A.B.P", pi.getFullName()); + + RuntimeRange range = new RuntimeRange.Port(pi, 3, 4, null); + + Assertions.assertEquals(8, range.maxWidth); + + Assertions.assertEquals(".A.B.P(3,4)", range.toString()); + + // The results expected below are derived from the class comment for RuntimeRange, + // which includes this example. + List instances = range.instances(); + Assertions.assertEquals(List.of(3, 4, 5, 6), instances); + Set parents = range.parentInstances(1); + Assertions.assertEquals(Set.of(1, 2, 3), parents); + + parents = range.parentInstances(2); + Assertions.assertEquals(Set.of(0, 1), parents); + + // Test startMR().getDigits. + Assertions.assertEquals(List.of(1, 1, 0), range.startMR().getDigits()); + + // Create a SendRange sending from and to this range. + SendRange sendRange = new SendRange(pi, 3, 4, null, null); + sendRange.destinations.add(range); + + // Test getNumberOfDestinationReactors. + Assertions.assertEquals(3, sendRange.getNumberOfDestinationReactors()); + + // Make first interleaved version. + range = range.toggleInterleaved(bi); + instances = range.instances(); + Assertions.assertEquals(List.of(3, 4, 6, 5), instances); + + // Test startMR().getDigits. + Assertions.assertEquals(List.of(1, 1, 0), range.startMR().getDigits()); + + // Make second interleaved version. + range = range.toggleInterleaved(ai); + instances = range.instances(); + Assertions.assertEquals(List.of(6, 1, 5, 3), instances); + + // Test startMR().getDigits. + Assertions.assertEquals(List.of(0, 1, 1), range.startMR().getDigits()); + + // Test instances of the parent. + Assertions.assertEquals(Set.of(3, 0, 2, 1), range.parentInstances(1)); + + // Add this range to the sendRange destinations and verify + // that the number of destination reactors becomes 4. + sendRange.addDestination(range); + Assertions.assertEquals(4, sendRange.getNumberOfDestinationReactors()); + + // Make third interleaved version. + range = range.toggleInterleaved(bi); + instances = range.instances(); + Assertions.assertEquals(List.of(5, 2, 6, 3), instances); + + // Test startMR().getDigits. + Assertions.assertEquals(List.of(1, 0, 1), range.startMR().getDigits()); + } +} diff --git a/org.lflang/src/lib/c/reactor-c b/org.lflang/src/lib/c/reactor-c index a02f4bfd6b..a98c93b614 160000 --- a/org.lflang/src/lib/c/reactor-c +++ b/org.lflang/src/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit a02f4bfd6b5cb11d7bad19139b5c1f44377d0472 +Subproject commit a98c93b614bd894d3b7febe68ab010345caabdfb diff --git a/org.lflang/src/org/lflang/ASTUtils.xtend b/org.lflang/src/org/lflang/ASTUtils.xtend index 47180e25b4..e7f21435a8 100644 --- a/org.lflang/src/org/lflang/ASTUtils.xtend +++ b/org.lflang/src/org/lflang/ASTUtils.xtend @@ -109,8 +109,8 @@ class ASTUtils { // Assume all the types are the same, so just use the first on the right. val type = (connection.rightPorts.get(0).variable as Port).type val delayClass = getDelayClass(type, generator) - val generic = generator.supportsGenerics - ? generator.getTargetType(InferredType.fromAST(type)) + val generic = generator.getTargetTypes().supportsGenerics + ? generator.getTargetTypes().getTargetType(InferredType.fromAST(type)) : "" val delayInstance = getDelayInstance(delayClass, connection, generic, !generator.generateAfterDelaysWithVariableWidth) @@ -301,7 +301,7 @@ class ASTUtils { * @param generator A code generator. */ private static def Reactor getDelayClass(Type type, GeneratorBase generator) { - val className = generator.supportsGenerics ? + val className = generator.getTargetTypes().supportsGenerics ? GeneratorBase.GEN_DELAY_CLASS_NAME : { val id = Integer.toHexString( InferredType.fromAST(type).toText.hashCode) @@ -344,7 +344,7 @@ class ASTUtils { action.minDelay.parameter = delayParameter action.origin = ActionOrigin.LOGICAL - if (generator.supportsGenerics) { + if (generator.getTargetTypes().supportsGenerics) { action.type = factory.createType action.type.id = "T" } else { @@ -390,7 +390,7 @@ class ASTUtils { delayClass.reactions.add(r1) // Add a type parameter if the target supports it. - if (generator.supportsGenerics) { + if (generator.getTargetTypes().supportsGenerics) { val parm = factory.createTypeParm parm.literal = generator.generateDelayGeneric() delayClass.typeParms.add(parm) @@ -1205,7 +1205,7 @@ class ASTUtils { /** * Given the width specification of port or instantiation - * and an (optional) list of nested intantiations, return + * and an (optional) list of nested instantiations, return * the width if it can be determined and -1 if not. * It will not be able to be determined if either the * width is variable (in which case you should use @@ -1215,7 +1215,7 @@ class ASTUtils { * evaluated to the extent possible given the instantiations list. * * The instantiations list is as in - * {@link initialValue(Parameter, List}. + * {@link initialValue(Parameter, List)}. * If the spec belongs to an instantiation (for a bank of reactors), * then the first element on this list should be the instantiation * that contains this instantiation. If the spec belongs to a port, @@ -1281,8 +1281,10 @@ class ASTUtils { } else { return -1; } - } else { + } else if (term.width > 0) { result += term.width; + } else { + return -1; } } return result; diff --git a/org.lflang/src/org/lflang/AstExtensions.kt b/org.lflang/src/org/lflang/AstExtensions.kt index ceb9184967..9821e36fa6 100644 --- a/org.lflang/src/org/lflang/AstExtensions.kt +++ b/org.lflang/src/org/lflang/AstExtensions.kt @@ -24,7 +24,6 @@ package org.lflang -import org.eclipse.emf.common.util.EList import org.eclipse.emf.ecore.EObject import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.nodemodel.util.NodeModelUtils @@ -371,9 +370,8 @@ val Action.isPhysical get() = this.origin == ActionOrigin.PHYSICAL /** * Return true if the receiving is a multiport. - * FIXME This is a duplicate of GeneratorBase.isMultiport */ -val Port.isMultiport get() = this.widthSpec != null +val Port.isMultiport get() = JavaAstUtils.isMultiport(this) /** Get the reactor that is instantiated in the receiving instantiation. */ val Instantiation.reactor get() = this.reactorClass.toDefinition() diff --git a/org.lflang/src/org/lflang/JavaAstUtils.java b/org.lflang/src/org/lflang/JavaAstUtils.java index 34aef17ff7..65299de374 100644 --- a/org.lflang/src/org/lflang/JavaAstUtils.java +++ b/org.lflang/src/org/lflang/JavaAstUtils.java @@ -33,9 +33,9 @@ import org.lflang.lf.Port; import org.lflang.lf.StateVar; import org.lflang.lf.Time; -import java.lang.IllegalArgumentException; import org.lflang.lf.Type; import org.lflang.lf.Value; +import org.lflang.lf.VarRef; /** * Helper class to manipulate the LF AST. This is partly @@ -178,6 +178,32 @@ public static String addZeroToLeadingDot(String literal) { return literal; } + /** + * Return true if the specified port is a multiport. + * @param port The port. + * @return True if the port is a multiport. + */ + public static boolean isMultiport(Port port) { + return port.getWidthSpec() != null; + } + + //////////////////////////////// + //// Utility functions for translating AST nodes into text + // This is a continuation of a large section of ASTUtils.xtend + // with the same name. + + /** + * Generate code for referencing a port, action, or timer. + * @param reference The reference to the variable. + */ + public static String generateVarRef(VarRef reference) { + var prefix = ""; + if (reference.getContainer() != null) { + prefix = reference.getContainer().getName() + "."; + } + return prefix + reference.getVariable().getName(); + } + /** * Assuming that the given value denotes a valid time literal, * return a time value. @@ -233,5 +259,4 @@ public static boolean isValidTime(Time t) { return TimeUnit.isValidUnit(t.getUnit()) && (t.getUnit() != null || t.getInterval() == 0); } - } diff --git a/org.lflang/src/org/lflang/ModelInfo.java b/org.lflang/src/org/lflang/ModelInfo.java index 4139271f34..5c2da9c2c0 100644 --- a/org.lflang/src/org/lflang/ModelInfo.java +++ b/org.lflang/src/org/lflang/ModelInfo.java @@ -29,6 +29,7 @@ import static org.eclipse.xtext.xbase.lib.IteratorExtensions.toIterable; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -36,7 +37,6 @@ import org.lflang.generator.NamedInstance; import org.lflang.generator.ReactorInstance; import org.lflang.graph.InstantiationGraph; -import org.lflang.graph.TopologyGraph; import org.lflang.lf.Assignment; import org.lflang.lf.Deadline; import org.lflang.lf.Instantiation; @@ -86,13 +86,8 @@ public class ModelInfo { */ public Set overflowingParameters; - /** - * A graph of ports and reactions. - */ - public TopologyGraph topologyGraph; - /** Cycles found during topology analysis. */ - private List>> topologyCycles = List.of(); + private Set> topologyCycles = new LinkedHashSet>(); /** * Whether or not the model information has been updated at least once. @@ -117,12 +112,13 @@ public void update(Model model, ErrorReporter reporter) { topLevelReactorInstances.add(inst); } else { model.getReactors().forEach( - it -> topLevelReactorInstances.add(new ReactorInstance(it, reporter, null)) + it -> topLevelReactorInstances.add(new ReactorInstance(it, reporter)) ); } // don't store the graph into a field, only the cycles. - var topologyGraph = new TopologyGraph(topLevelReactorInstances); - this.topologyCycles = topologyGraph.getCycles(); + for (ReactorInstance top : topLevelReactorInstances) { + this.topologyCycles.addAll(top.getCycles()); + } } // may be null if the target is invalid @@ -134,7 +130,7 @@ public void update(Model model, ErrorReporter reporter) { } } - public List>> topologyCycles() { + public Set> topologyCycles() { return this.topologyCycles; } diff --git a/org.lflang/src/org/lflang/TimeUnit.java b/org.lflang/src/org/lflang/TimeUnit.java index c2ebbe3ac4..e3dc2fce78 100644 --- a/org.lflang/src/org/lflang/TimeUnit.java +++ b/org.lflang/src/org/lflang/TimeUnit.java @@ -46,8 +46,8 @@ public enum TimeUnit { MILLI("msec", "ms", "msecs"), /** Seconds. */ SECOND("sec", "s", "secs", "second", "seconds"), - /** Minute. */ - MINUTE("min", "mins", "minute", "minutes"), + /** Minute. */ // NOTE: Do not use MIN as the first entry. Common macro for minimum. + MINUTE("minute", "min", "mins", "minutes"), /** Hour. */ HOUR("hour", "h", "hours"), /** Day. */ diff --git a/org.lflang/src/org/lflang/federated/CGeneratorExtension.java b/org.lflang/src/org/lflang/federated/CGeneratorExtension.java index c957aa0b77..ca9a432af1 100644 --- a/org.lflang/src/org/lflang/federated/CGeneratorExtension.java +++ b/org.lflang/src/org/lflang/federated/CGeneratorExtension.java @@ -28,17 +28,16 @@ import org.lflang.ASTUtils; import org.lflang.JavaAstUtils; -import org.lflang.TimeUnit; import org.lflang.TimeValue; -import org.lflang.generator.c.CGenerator; import org.lflang.generator.ReactorInstance; +import org.lflang.generator.c.CGenerator; +import org.lflang.generator.c.CUtil; import org.lflang.lf.Delay; import org.lflang.lf.Input; import org.lflang.lf.Parameter; import org.lflang.lf.Port; import org.lflang.lf.Reactor; import org.lflang.lf.ReactorDecl; -import org.lflang.lf.Value; import org.lflang.lf.VarRef; /** @@ -146,7 +145,7 @@ public static StringBuilder initializeTriggerForControlReactions( ReactorDecl reactorClass = instance.getDefinition().getReactorClass(); Reactor reactor = ASTUtils.toDefinition(reactorClass); - String nameOfSelfStruct = CGenerator.selfStructName(instance); + String nameOfSelfStruct = CUtil.reactorRef(instance); // Initialize triggers for network input control reactions for (Port trigger : federate.networkInputControlReactionsTriggers) { @@ -172,7 +171,7 @@ public static StringBuilder initializeTriggerForControlReactions( } } - nameOfSelfStruct = CGenerator.selfStructName(instance); + nameOfSelfStruct = CUtil.reactorRef(instance); // Initialize the trigger for network output control reactions if it doesn't exists if (federate.networkOutputControlReactionsTrigger != null) { @@ -196,7 +195,7 @@ public static String createPortStatusFieldForInput(Input input, CGenerator generator) { StringBuilder builder = new StringBuilder(); // Check if the port is a multiport - if (generator.isMultiport(input)) { + if (JavaAstUtils.isMultiport(input)) { // If it is a multiport, then create an auxiliary list of port // triggers for each channel of // the multiport to keep track of the status of each channel diff --git a/org.lflang/src/org/lflang/federated/FedASTUtils.java b/org.lflang/src/org/lflang/federated/FedASTUtils.java index c042a192af..6a12e777d8 100644 --- a/org.lflang/src/org/lflang/federated/FedASTUtils.java +++ b/org.lflang/src/org/lflang/federated/FedASTUtils.java @@ -621,7 +621,7 @@ private static void addNetworkOutputControlReaction( // The input needs a type. All targets have a Time type, so we use that. Type portType = factory.createType(); - portType.setId(generator.getTargetTimeType()); + portType.setId(generator.getTargetTypes().getTargetTimeType()); newTriggerForControlReactionVariable.setType(portType); top.getInputs().add(newTriggerForControlReactionVariable); diff --git a/org.lflang/src/org/lflang/federated/FederateInstance.xtend b/org.lflang/src/org/lflang/federated/FederateInstance.xtend index 7929628407..2fcdb3a78e 100644 --- a/org.lflang/src/org/lflang/federated/FederateInstance.xtend +++ b/org.lflang/src/org/lflang/federated/FederateInstance.xtend @@ -227,7 +227,7 @@ class FederateInstance { ///////////////////////////////////////////// //// Public Methods - + /** * Return true if the specified action should be included in the code generated * for the federate. This means that either the action is used as a trigger, @@ -285,7 +285,7 @@ class FederateInstance { if (!reactor.federated || isSingleton) return true // If the port is used as a trigger, a source, or an effect for a top-level reaction - // that belongs to this federate, then generate it. + // that belongs to this federate, then return true. for (react : reactor.allReactions) { if (contains(react)) { // Look in triggers @@ -357,14 +357,13 @@ class FederateInstance { * Return true if the specified reactor instance or any parent * reactor instance is contained by this federate. * If the specified instance is the top-level reactor, return true - * (this reactor belongs to all federates). - * If it is a bank member, then this returns true only if the bankIndex - * of the reactor instance matches the federate instance bank index. - * This also returns true for the bank placeholder for a bank that - * contains an instance that matches this bank index. + * (the top-level reactor belongs to all federates). * If this federate instance is a singleton, then return true if the * instance is non null. * + * NOTE: If the instance is bank within the top level, then this + * returns true even though only one of the bank members is in the federate. + * * @param instance The reactor instance. * @return True if this federate contains the reactor instance */ @@ -378,12 +377,7 @@ class FederateInstance { // Start with this instance, then check its parents. var i = instance; while (i !== null) { - if (i.definition === this.instantiation - && ( - i.bankIndex < 0 // Not a bank member - || i.bankIndex == this.bankIndex // Index matches. - ) - ) { + if (i.definition === this.instantiation) { return true; } i = i.parent; @@ -421,6 +415,20 @@ class FederateInstance { return false; } + + /** + * Return the total number of runtime instances of the specified reactor + * instance in this federate. This is zero if the reactor is not in the + * federate at all, and otherwise is the product of the bank widths of + * all the parent containers of the instance, except that if the depth + * one parent is bank, its width is ignored (only one bank member can be + * in any federate). + */ + def numRuntimeInstances(ReactorInstance reactor) { + if (!contains(reactor)) return 0; + val depth = isSingleton ? 0 : 1; + return reactor.getTotalWidth(depth); + } /** * Build an index of reactions at the top-level (in the diff --git a/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java b/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java index c5455fc4de..2d5b5117ab 100644 --- a/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java +++ b/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java @@ -27,10 +27,12 @@ package org.lflang.federated; import org.lflang.InferredType; +import org.lflang.JavaAstUtils; import org.lflang.TargetProperty.CoordinationType; import org.lflang.federated.serialization.FedNativePythonSerialization; import org.lflang.federated.serialization.FedSerialization; import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.c.CUtil; import org.lflang.generator.python.PythonGenerator; import org.lflang.lf.Action; import org.lflang.lf.Delay; @@ -74,8 +76,8 @@ public static String generateNetworkSenderBody( SupportedSerializers serializer, PythonGenerator generator ) { - String sendRef = generator.generatePortRef(sendingPort, sendingBankIndex, sendingChannelIndex); - String receiveRef = generator.generateVarRef(receivingPort); // Used for comments only, so no need for bank/multiport index. + String sendRef = CUtil.portRefInReaction(sendingPort, sendingBankIndex, sendingChannelIndex); + String receiveRef = JavaAstUtils.generateVarRef(receivingPort); // Used for comments only, so no need for bank/multiport index. StringBuilder result = new StringBuilder(); result.append("// Sending from " + sendRef + " in federate " + sendingFed.name + " to " + receiveRef + " in federate " + receivingFed.name + "\n"); @@ -178,7 +180,7 @@ public static String generateNetworkReceiverBody( PythonGenerator generator ) { - String receiveRef = generator.generatePortRef(receivingPort, receivingBankIndex, receivingChannelIndex); + String receiveRef = CUtil.portRefInReaction(receivingPort, receivingBankIndex, receivingChannelIndex); StringBuilder result = new StringBuilder(); // Transfer the physical time of arrival from the action to the port diff --git a/org.lflang/src/org/lflang/generator/GeneratorBase.xtend b/org.lflang/src/org/lflang/generator/GeneratorBase.xtend index 2b4eceffca..68405515c2 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorBase.xtend +++ b/org.lflang/src/org/lflang/generator/GeneratorBase.xtend @@ -1,5 +1,3 @@ -/* Generator base class for shared code between code generators. */ - /************* * Copyright (c) 2019-2020, The University of California at Berkeley. @@ -37,16 +35,11 @@ import java.util.List import java.util.Map import java.util.Set import java.util.regex.Pattern +import java.util.stream.Collectors import org.eclipse.core.resources.IMarker -import org.eclipse.core.resources.IResource -import org.eclipse.core.resources.ResourcesPlugin -import org.eclipse.emf.ecore.EObject import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.generator.IFileSystemAccess2 -import org.eclipse.xtext.nodemodel.util.NodeModelUtils -import org.eclipse.xtext.resource.XtextResource import org.eclipse.xtext.util.CancelIndicator -import org.eclipse.xtext.validation.CheckMode import org.lflang.ASTUtils import org.lflang.ErrorReporter import org.lflang.FileConfig @@ -55,7 +48,6 @@ import org.lflang.MainConflictChecker import org.lflang.Target import org.lflang.TargetConfig import org.lflang.TargetConfig.Mode -import org.lflang.TargetProperty import org.lflang.TargetProperty.CoordinationType import org.lflang.TimeUnit import org.lflang.TimeValue @@ -64,63 +56,49 @@ import org.lflang.federated.FederateInstance import org.lflang.federated.serialization.SupportedSerializers import org.lflang.graph.InstantiationGraph import org.lflang.lf.Action -import org.lflang.lf.ActionOrigin -import org.lflang.lf.Code import org.lflang.lf.Delay import org.lflang.lf.Instantiation import org.lflang.lf.LfFactory import org.lflang.lf.Model import org.lflang.lf.Parameter -import org.lflang.lf.Port import org.lflang.lf.Reaction import org.lflang.lf.Reactor -import org.lflang.lf.StateVar -import org.lflang.lf.TargetDecl import org.lflang.lf.Time import org.lflang.lf.Value import org.lflang.lf.VarRef -import org.lflang.lf.Variable -import org.lflang.validation.AbstractLFValidator import static extension org.lflang.ASTUtils.* -import static extension org.lflang.JavaAstUtils.* /** - * Generator base class for shared code between code generators. - * This extends AbstractLinguaFrancaValidator so that errors can be highlighted - * in the XText-based IDE. - * + * Generator base class for specifying core functionality + * that all code generators should have. + * * @author{Edward A. Lee } * @author{Marten Lohstroh } * @author{Christian Menard } * @author{Soroush Bateni } */ -abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes { +abstract class GeneratorBase extends JavaGeneratorBase { //////////////////////////////////////////// //// Public fields. - + /** * Constant that specifies how to name generated delay reactors. */ public static val GEN_DELAY_CLASS_NAME = "_lf_GenDelay" - /** + /** * The main (top-level) reactor instance. */ public ReactorInstance main - + /** A error reporter for reporting any errors or warnings during the code generation */ public ErrorReporter errorReporter - + //////////////////////////////////////////// //// Protected fields. - - /** - * All code goes into this string buffer. - */ - protected var code = new StringBuilder /** * The current target configuration. @@ -243,10 +221,6 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes // ////////////////////////////////////////// // // Private fields. - /** - * Map from builder to its current indentation. - */ - var indentation = new LinkedHashMap() /** * Create a new GeneratorBase object. @@ -280,86 +254,11 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes return this.delayClasses.findFirst[it|it.name.equals(className)] } - /** - * Set the appropriate target properties based on the target properties of - * the main .lf file. - */ - protected def void setTargetConfig(LFGeneratorContext context) { - - val target = fileConfig.resource.findTarget - if (target.config !== null) { - // Update the configuration according to the set target properties. - TargetProperty.set(this.targetConfig, target.config.pairs ?: emptyList, errorReporter) - } - - // Accommodate the physical actions in the main .lf file - accommodatePhysicalActionsIfPresent(fileConfig.resource); - - // Override target properties if specified, e.g. as command line arguments. - if (context.args.containsKey("no-compile")) { - targetConfig.noCompile = true - } - if (context.args.containsKey("threads")) { - targetConfig.threads = Integer.parseInt(context.args.getProperty("threads")) - } - if (context.args.containsKey("target-compiler")) { - targetConfig.compiler = context.args.getProperty("target-compiler") - } - if (context.args.containsKey("target-flags")) { - targetConfig.compilerFlags.clear() - if (!context.args.getProperty("target-flags").isEmpty) { - targetConfig.compilerFlags.addAll(context.args.getProperty("target-flags").split(' ')) - } - } - if (context.args.containsKey("runtime-version")) { - targetConfig.runtimeVersion = context.args.getProperty("runtime-version") - } - if (context.args.containsKey("external-runtime-path")) { - targetConfig.externalRuntimePath = context.args.getProperty("external-runtime-path") - } - if (context.args.containsKey(TargetProperty.KEEPALIVE.description)) { - targetConfig.keepalive = Boolean.parseBoolean( - context.args.getProperty(TargetProperty.KEEPALIVE.description)); - } - } - - /** - * Look for physical actions in 'resource'. - * If found, take appropriate actions to accommodate. - * - * Set keepalive to true. - */ - protected def void accommodatePhysicalActionsIfPresent(Resource resource) { - if (!target.setsKeepAliveOptionAutomatically) { - return; // nothing to do - } - - // If there are any physical actions, ensure the threaded engine is used and that - // keepalive is set to true, unless the user has explicitly set it to false. - for (action : resource.allContents.toIterable.filter(Action)) { - if (action.origin == ActionOrigin.PHYSICAL) { - // Check if the user has explicitly set keepalive to false or true - if (!targetConfig.setByUser.contains(TargetProperty.KEEPALIVE) - && targetConfig.keepalive == false - ) { - // If not, set it to true - targetConfig.keepalive = true - errorReporter.reportWarning( - action, - '''Setting «TargetProperty.KEEPALIVE.displayName» to true because of «action.name».« - » This can be overridden by setting the «TargetProperty.KEEPALIVE.description»« - » target property manually.''' - ); - } - } - } - } - /** * If there is a main or federated reactor, then create a synthetic Instantiation * for that top-level reactor and set the field mainDef to refer to it. */ - def createMainInstance() { + private def createMainInstantiation() { // Find the main reactor and create an AST node for its instantiation. for (reactor : fileConfig.resource.allContents.toIterable.filter(Reactor)) { if (reactor.isMain || reactor.isFederated) { @@ -386,7 +285,9 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes */ def void doGenerate(Resource resource, IFileSystemAccess2 fsa, LFGeneratorContext context) { - setTargetConfig(context) + JavaGeneratorUtils.setTargetConfig( + context, JavaGeneratorUtils.findTarget(fileConfig.resource), targetConfig, errorReporter + ) fileConfig.cleanIfNeeded() @@ -400,7 +301,7 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes ASTUtils.setMainName(fileConfig.resource, fileConfig.name) - createMainInstance() + createMainInstantiation() // Check if there are any conflicting main reactors elsewhere in the package. if (context.mode == Mode.STANDALONE && mainDef !== null) { @@ -409,10 +310,6 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes } } - // If federates are specified in the target, create a mapping - // from Instantiations in the main reactor to federate names. - // Also create a list of federate names or a list with a single - // empty name if there are no federates specified. // This must be done before desugaring delays below. analyzeFederates(context) @@ -422,33 +319,31 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes // done here. copyUserFiles(this.targetConfig, this.fileConfig); - // Collect reactors and create an instantiation graph. These are needed to figure out which resources we need + // Collect reactors and create an instantiation graph. + // These are needed to figure out which resources we need // to validate, which happens in setResources(). setReactorsAndInstantiationGraph() - // Collect the reactors defined in this resource (i.e., file in Eclipse speak) and (non-main) - // reactors defined in imported resources. - setResources(context) - - // Reroute connections that have delays associated with them via generated delay reactors. + JavaGeneratorUtils.validateImports(context, fileConfig, instantiationGraph, errorReporter) + val allResources = JavaGeneratorUtils.getResources(reactors) + resources.addAll(allResources.stream() // FIXME: This filter reproduces the behavior of the method it replaces. But why must it be so complicated? Why are we worried about weird corner cases like this? + .filter [it | it != fileConfig.resource || (mainDef !== null && it === mainDef.reactorClass.eResource)] + .map [it | JavaGeneratorUtils.getLFResource(it, fsa, context, errorReporter)] + .collect(Collectors.toList()) + ) + JavaGeneratorUtils.accommodatePhysicalActionsIfPresent(allResources, target, targetConfig, errorReporter); + // FIXME: Should the GeneratorBase pull in `files` from imported + // resources? + + // Reroute connections that have delays associated with them via + // generated delay reactors. transformDelays() - // Invoke this function a second time because transformations may have introduced new reactors! + // Invoke these functions a second time because transformations + // may have introduced new reactors! setReactorsAndInstantiationGraph() - // First, produce any preamble code that the code generator needs - // to produce before anything else goes into the code generated files. - generatePreamble() // FIXME: Move this elsewhere. See awkwardness with CppGenerator because it will not even - // use the result. - - if (!enabledSerializers.isNullOrEmpty) { - // If serialization support is - // requested by the programmer - // enable support for them. - enableSupportForSerialization(context.cancelIndicator); - } - - + enableSupportForSerializationIfApplicable(context.cancelIndicator); } /** @@ -483,74 +378,12 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes /** * For each involved resource, replace connections with delays with generated delay reactors. */ - protected def transformDelays() { + private def transformDelays() { for (r : this.resources) { r.eResource.insertGeneratedDelays(this) } } - /** - * Update the class variable that lists all the involved resources. Also report validation problems of imported - * resources at the import statements through those failing resources are reached. - * - * @param context The context providing the cancel indicator used by the validator. - */ - protected def setResources(LFGeneratorContext context) { - val fsa = this.fileConfig.fsa; - val validator = (this.fileConfig.resource as XtextResource).resourceServiceProvider.resourceValidator - if (mainDef !== null) { - reactors.add(mainDef.reactorClass as Reactor); - this.resources.add( - new LFResource( - mainDef.reactorClass.eResource, - this.fileConfig, - this.targetConfig)); - } - // Iterate over reactors and mark their resources as tainted if they import resources that are either marked - // as tainted or fail to validate. - val tainted = newHashSet - for (r : this.reactors) { - val res = r.eResource - if (!this.resources.contains(res)) { - if (res !== this.fileConfig.resource) { - if (tainted.contains(res) || - (validator.validate(res, CheckMode.ALL, context.cancelIndicator)).size > 0) { - for (inst : this.instantiationGraph.getDownstreamAdjacentNodes(r)) { - for (imp : (inst.eContainer as Model).imports) { - for (decl : imp.reactorClasses) { - if (decl.reactorClass.eResource === res) { - errorReporter.reportError(imp, '''Unresolved compilation issues in '«imp.importURI»'.''') - tainted.add(decl.eResource) - } - } - } - } - } - // Read the target property of the imported file - val target = res.findTarget - var targetConfig = new TargetConfig(); - if (target.config !== null) { - TargetProperty.set(targetConfig, target.config.pairs ?: emptyList, errorReporter); - } - val fileConfig = new FileConfig(res, fsa, context); - // Add it to the list of LFResources - this.resources.add( - new LFResource( - res, - fileConfig, - targetConfig) - ); - - // Accommodate the physical actions in the imported .lf file - accommodatePhysicalActionsIfPresent(res); - // FIXME: Should the GeneratorBase pull in `files` from imported - // resources? If so, uncomment the following line. - // copyUserFiles(targetConfig, fileConfig); - } - } - } - } - /** * Copy all files listed in the target property `files` into the * src-gen folder of the main .lf file. @@ -590,6 +423,11 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes return errorReporter.getErrorsOccurred(); } + /* + * Return the TargetTypes instance associated with this. + */ + abstract def TargetTypes getTargetTypes(); + /** * Generate code for the body of a reaction that takes an input and * schedules an action with the value of that input. @@ -612,47 +450,6 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes */ abstract def String generateDelayGeneric(); - /** - * Generate code for referencing a port, action, or timer. - * @param reference The reference to the variable. - */ - def String generateVarRef(VarRef reference) { - var prefix = ""; - if (reference.container !== null) { - prefix = reference.container.name + "." - } - return prefix + reference.variable.name - } - - /** - * Generate code for referencing a port possibly indexed by - * a bank index and/or a multiport index. This assumes the target language uses - * the usual array indexing [n] for both cases. If not, this needs to be overridden - * by the target code generator. If the provided reference is not a port, then - * this return the string "ERROR: not a port.". - * @param reference The reference to the port. - * @param bankIndex A bank index or null or negative if not in a bank. - * @param multiportIndex A multiport index or null or negative if not in a multiport. - */ - def String generatePortRef(VarRef reference, Integer bankIndex, Integer multiportIndex) { - if (!(reference.variable instanceof Port)) { - return "ERROR: not a port."; - } - var prefix = ""; - if (reference.container !== null) { - var bank = ""; - if (reference.container.widthSpec !== null && bankIndex !== null && bankIndex >= 0) { - bank = "[" + bankIndex + "]"; - } - prefix = reference.container.name + bank + "." - } - var multiport = ""; - if ((reference.variable as Port).widthSpec !== null && multiportIndex !== null && multiportIndex >= 0) { - multiport = "[" + multiportIndex + "]"; - } - return prefix + reference.variable.name + multiport; - } - /** * Return true if the reaction is unordered. An unordered reaction is one * that does not have any dependency on other reactions in the containing @@ -807,23 +604,6 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes code = new StringBuilder } - /** - * Return the target. - */ - def findTarget(Resource resource) { - var TargetDecl targetDecl - for (t : resource.allContents.toIterable.filter(TargetDecl)) { - if (targetDecl !== null) { - throw new InvalidSourceException("There is more than one target!") // FIXME: check this in validator - } - targetDecl = t - } - if (targetDecl === null) { - throw new InvalidSourceException("No target found!") - } - targetDecl - } - /** * Generate code for the body of a reaction that handles the * action that is triggered by receiving a message from a remote @@ -929,11 +709,13 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes * Add necessary code to the source and necessary build support to * enable the requested serializations in 'enabledSerializations' */ - def void enableSupportForSerialization(CancelIndicator cancelIndicator) { - throw new UnsupportedOperationException( - "Serialization is target-specific "+ - " and is not implemented for the "+target.toString+" target." - ); + def void enableSupportForSerializationIfApplicable(CancelIndicator cancelIndicator) { + if (!enabledSerializers.isNullOrEmpty()) { + throw new UnsupportedOperationException( + "Serialization is target-specific "+ + " and is not implemented for the "+target.toString+" target." + ); + } } /** @@ -941,11 +723,7 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes * coordination mechanism. */ def isFederatedAndDecentralized() { - if (isFederated && - targetConfig.coordination === CoordinationType.DECENTRALIZED) { - return true - } - return false + return isFederated && targetConfig.coordination === CoordinationType.DECENTRALIZED } /** @@ -953,11 +731,7 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes * coordination mechanism. */ def isFederatedAndCentralized() { - if (isFederated && - targetConfig.coordination === CoordinationType.CENTRALIZED) { - return true - } - return false + return isFederated && targetConfig.coordination === CoordinationType.CENTRALIZED } /** @@ -985,174 +759,6 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes } } - /** - * Generate any preamble code that appears in the code generated - * file before anything else. - */ - protected def void generatePreamble() { - prComment("Code generated by the Lingua Franca compiler from:") - prComment("file:/" +FileConfig.toUnixString(fileConfig.srcFile)) - val models = new LinkedHashSet - - for (r : this.reactors ?: emptyList) { - // The following assumes all reactors have a container. - // This means that generated reactors **have** to be - // added to a resource; not doing so will result in a NPE. - models.add(r.toDefinition.eContainer as Model) - } - // Add the main reactor if it is defined - if (this.mainDef !== null) { - val mainModel = this.mainDef.reactorClass.toDefinition.eContainer as Model - models.add(mainModel) - for (p : mainModel.preambles) { - pr(p.code.toText) - } - } - } - - /** - * Get the code produced so far. - * @return The code produced so far as a String. - */ - protected def getCode() { - code.toString() - } - - /** - * Increase the indentation of the output code produced. - */ - protected def indent() { - indent(code) - } - - /** - * Increase the indentation of the output code produced - * on the specified builder. - * @param The builder to indent. - */ - protected def indent(StringBuilder builder) { - var prefix = indentation.get(builder) - if (prefix === null) { - prefix = "" - } - prefix += " "; - indentation.put(builder, prefix) - } - - /** - * Append the specified text plus a final newline to the current - * code buffer. - * @param format A format string to be used by String.format or - * the text to append if no further arguments are given. - * @param args Additional arguments to pass to the formatter. - */ - protected def pr(String format, Object... args) { - pr(code, if (args !== null && args.length > 0) - String.format(format, args) - else - format) - } - - /** - * Append the specified text plus a final newline to the specified - * code buffer. - * @param builder The code buffer. - * @param text The text to append. - */ - protected def pr(StringBuilder builder, Object text) { - // Handle multi-line text. - var string = text.toString - var indent = indentation.get(builder) - if (indent === null) { - indent = "" - } - if (string.contains("\n")) { - // Replace all tabs with four spaces. - string = string.replaceAll("\t", " ") - // Use two passes, first to find the minimum leading white space - // in each line of the source text. - var split = string.split("\n") - var offset = Integer.MAX_VALUE - var firstLine = true - for (line : split) { - // Skip the first line, which has white space stripped. - if (firstLine) { - firstLine = false - } else { - var numLeadingSpaces = line.indexOf(line.trim()); - if (numLeadingSpaces < offset) { - offset = numLeadingSpaces - } - } - } - // Now make a pass for each line, replacing the offset leading - // spaces with the current indentation. - firstLine = true - for (line : split) { - builder.append(indent) - // Do not trim the first line - if (firstLine) { - builder.append(line) - firstLine = false - } else { - builder.append(line.substring(offset)) - } - builder.append("\n") - } - } else { - builder.append(indent) - builder.append(text) - builder.append("\n") - } - } - - /** - * Prints an indented block of text with the given begin and end markers, - * but only if the actions print any text at all. - * This is helpful to avoid the production of empty blocks. - * @param begin The prologue of the block. - * @param end The epilogue of the block. - * @param actions Actions that print the interior of the block. - */ - protected def prBlock(String begin, String end, Runnable... actions) { - val i = code.length - indent() - for (action : actions) { - action.run() - } - unindent() - if (i < code.length) { - val inserted = code.substring(i, code.length) - code.delete(i, code.length) - pr(begin) - code.append(inserted) - pr(end) - } - } - - /** - * Leave a marker in the generated code that indicates the original line - * number in the LF source. - * @param eObject The node. - */ - protected def prSourceLineNumber(EObject eObject) { - if (eObject instanceof Code) { - pr(code, '''// «NodeModelUtils.getNode(eObject).startLine +1»''') - } else { - pr(code, '''// «NodeModelUtils.getNode(eObject).startLine»''') - } - } - - /** - * Print a comment to the generated file. - * Particular targets will need to override this if comments - * start with something other than '//'. - * @param comment The comment. - */ - protected def prComment(String comment) { - pr(code, '// ' + comment); - } - /** * Given a line of text from the output of a compiler, return * an instance of ErrorFileAndLine if the line is recognized as @@ -1181,6 +787,8 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes * @param stderr The output on standard error of executing a command. */ def reportCommandErrors(String stderr) { + // NOTE: If the VS Code branch passes code review, then this function, + // parseCommandOutput, and ErrorFileAndLine will be deleted soon after. // First, split the message into lines. val lines = stderr.split("\\r?\\n") var message = new StringBuilder() @@ -1267,143 +875,6 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes } } - /** If the mode is EPOCH (the code generator is running in an - * an Eclipse IDE), then refresh the project. This will ensure that - * any generated files become visible in the project. - */ - protected def refreshProject() { - if (fileConfig.context.mode == Mode.EPOCH) { - // Find name of current project - val id = "((:?[a-z]|[A-Z]|_\\w)*)"; - var pattern = if (File.separator.equals("/")) { // Linux/Mac file separator - Pattern.compile("platform:" + File.separator + "resource" + File.separator + id + File.separator); - } else { // Windows file separator - Pattern.compile( - "platform:" + File.separator + File.separator + "resource" + File.separator + File.separator + - id + File.separator + File.separator); - } - val matcher = pattern.matcher(code); - var projName = "" - if (matcher.find()) { - projName = matcher.group(1) - } - try { - val members = ResourcesPlugin.getWorkspace().root.members - for (member : members) { - // Refresh current project, or simply entire workspace if project name was not found - if (projName == "" || projName.equals(member.fullPath.toString.substring(1))) { - member.refreshLocal(IResource.DEPTH_INFINITE, null) - println("Refreshed " + member.fullPath.toString) - } - } - } catch (IllegalStateException e) { - println("Unable to refresh workspace: " + e) - } - } - } - - /** Reduce the indentation by one level for generated code - * in the default code buffer. - */ - protected def unindent() { - unindent(code) - } - - /** Reduce the indentation by one level for generated code - * in the specified code buffer. - */ - protected def unindent(StringBuilder builder) { - var indent = indentation.get(builder) - if (indent !== null) { - val end = indent.length - 4; - if (end < 0) { - indent = "" - } else { - indent = indent.substring(0, end) - } - indentation.put(builder, indent) - } - } - - /** - * Create a list of default parameter initializers in target code. - * - * @param param The parameter to create initializers for - * @return A list of initializers in target code - */ - protected def getInitializerList(Parameter param) { - var list = new ArrayList(); - - for (i : param?.init) { - if (param.isOfTimeType) { - list.add(i.targetTime) - } else { - list.add(i.targetValue) - } - } - return list - } - - /** - * Create a list of state initializers in target code. - * - * @param state The state variable to create initializers for - * @return A list of initializers in target code - */ - protected def List getInitializerList(StateVar state) { - if (!state.isInitialized) { - return null - } - - var list = new ArrayList(); - - for (i : state?.init) { - if (i.parameter !== null) { - list.add(i.parameter.targetReference) - } else if (state.isOfTimeType) { - list.add(i.targetTime) - } else { - list.add(i.targetValue) - } - } - return list - } - - /** - * Create a list of parameter initializers in target code in the context - * of an reactor instantiation. - * - * This respects the parameter assignments given in the reactor - * instantiation and falls back to the reactors default initializers - * if no value is assigned to it. - * - * @param param The parameter to create initializers for - * @return A list of initializers in target code - */ - protected def getInitializerList(Parameter param, Instantiation i) { - if (i === null || param === null) { - return null - } - - val assignments = i.parameters.filter[p|p.lhs === param] - - if (assignments.size == 0) { - // the parameter was not overwritten in the instantiation - return param.initializerList - } else { - // the parameter was overwritten in the instantiation - var list = new ArrayList(); - for (init : assignments.get(0)?.rhs) { - if (param.isOfTimeType) { - list.add(init.targetTime) - } else { - list.add(init.targetValue) - } - } - return list - } - } - /** * Generate target code for a parameter reference. * @@ -1414,82 +885,8 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes return param.name } - // // Utility functions supporting multiports. - /** - * If the argument is a multiport, return a list of strings - * describing the width of the port, and otherwise, return null. - * If the list is empty, then the width is variable (specified - * as '[]'). Otherwise, it is a list of integers and/or parameter - * references obtained by getTargetReference(). - * @param variable The port. - * @return The width specification for a multiport or null if it is - * not a multiport. - */ - protected def List multiportWidthSpec(Variable variable) { - var result = null as List - if (variable instanceof Port) { - if (variable.widthSpec !== null) { - result = new ArrayList() - if (!variable.widthSpec.ofVariableLength) { - for (term : variable.widthSpec.terms) { - if (term.parameter !== null) { - result.add(getTargetReference(term.parameter)) - } else { - result.add('' + term.width) - } - } - } - } - } - return result - } - - /** - * If the argument is a multiport, then return a string that - * gives the width as an expression, and otherwise, return null. - * The string will be empty if the width is variable (specified - * as '[]'). Otherwise, if is a single term or a sum of terms - * (separated by '+'), where each term is either an integer - * or a parameter reference in the target language. - */ - protected def String multiportWidthExpression(Variable variable) { - val spec = multiportWidthSpec(variable) - if (spec !== null) { - return spec.join(' + ') - } - return null - } - - /** - * Return true if the specified port is a multiport. - * @param port The port. - * @return True if the port is a multiport. - */ - def boolean isMultiport(Port port) { - port.widthSpec !== null - } - // ////////////////////////////////////////////////// // // Private functions - /** - * Get textual representation of a time in the target language. - * This is a separate function from - * getTargetTime to avoid producing invalid RTI - * code for targets that override timeInTargetLanguage - * to return a C-incompatible time type. - * - * @param v A time AST node - * @return An RTI-compatible (ie. C target) time string - */ - protected def getRTITime(Delay d) { - if (d.parameter !== null) { - return d.toText - } - - return d.time.toTimeValue.timeInTargetLanguage - } - - /** * Remove triggers in each federates' network reactions that are defined in remote federates. @@ -1525,7 +922,8 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes } } - /** Sets the RTI hostname, port and username if given as compiler arguments + /** + * Set the RTI hostname, port and username if given as compiler arguments */ private def setFederationRTIProperties(LFGeneratorContext context) { val rtiAddr = context.args.getProperty("rti").toString() @@ -1553,28 +951,23 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes } } - /** Analyze the resource (the .lf file) that is being parsed - * to determine whether code is being mapped to single or to - * multiple target machines. If it is being mapped to multiple - * machines, then set the 'federates' list, the 'federateIDs' - * map, and the 'federationRTIHost' and 'federationRTIPort' - * variables. + /** + * Analyze the AST to determine whether code is being mapped to + * single or to multiple target machines. If it is being mapped + * to multiple machines, then set the {@link #isFederated} field to true, + * create a FederateInstance for each federate, and record various + * properties of the federation * - * In addition, analyze the connections between federates. - * Ensure that every cycle has a non-zero delay (microstep - * delays will not be sufficient). Construct the dependency - * graph between federates. And replace connections between - * federates with a pair of reactions, one triggered by - * the sender's output port, and the other triggered by - * an action. + * In addition, for each top-level connection, add top-level reactions to the AST + * that send and receive messages over the network. * - * This class is target independent, so the target code - * generator still has quite a bit of work to do. - * It needs to provide the body of the sending and - * receiving reactions. It also needs to provide the - * runtime infrastructure that uses the dependency - * information between federates. See the C target - * for a reference implementation. + * This class is target independent, so the target code + * generator still has quite a bit of work to do. + * It needs to provide the body of the sending and + * receiving reactions. It also needs to provide the + * runtime infrastructure that uses the dependency + * information between federates. See the C target + * for a reference implementation. */ private def analyzeFederates(LFGeneratorContext context) { // Next, if there actually are federates, analyze the topology @@ -1622,12 +1015,12 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes for (instantiation : mainReactor.allInstantiations) { var bankWidth = ASTUtils.width(instantiation.widthSpec, mainReactorContext); if (bankWidth < 0) { - errorReporter.reportError(instantiation, "Cannot determine bank width!"); + errorReporter.reportError(instantiation, "Cannot determine bank width! Assuming width of 1."); // Continue with a bank width of 1. bankWidth = 1; } - // Create one federate instance for each reactor instance in the bank of reactors. - val federateInstances = new ArrayList(); + // Create one federate instance for each instance in a bank of reactors. + val federateInstances = new ArrayList(bankWidth); for (var i = 0; i < bankWidth; i++) { // Assign an integer ID to the federate. var federateID = federates.size @@ -1693,91 +1086,105 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes // that those contain. val mainInstance = new ReactorInstance(mainReactor, errorReporter, 1) - for (federateReactor : mainInstance.children) { - // Skip banks and just process the individual instances. - if (federateReactor.bankIndex > -2) { - val bankIndex = (federateReactor.bankIndex >= 0)? federateReactor.bankIndex : 0 - val federateInstance = federatesByInstantiation.get(federateReactor.definition).get(bankIndex); - for (input : federateReactor.inputs) { - replaceConnectionFromSource(input, federateInstance, federateReactor, mainInstance) - } + for (child : mainInstance.children) { + for (output : child.outputs) { + replaceConnectionFromFederate(output, child, mainInstance) } } } /** - * Replace the connections to the specified input port for the specified federate reactor. - * @param input The input port instance. - * @param destinationFederate The federate for which this port is an input. + * Replace the connections from the specified output port for the specified federate reactor. + * @param output The output port instance. + * @param srcFederate The federate for which this port is an output. * @param federateReactor The reactor instance for that federate. * @param mainInstance The main reactor instance. */ - def void replaceConnectionFromSource( - PortInstance input, FederateInstance destinationFederate, ReactorInstance federateReactor, ReactorInstance mainInstance + private def void replaceConnectionFromFederate( + PortInstance output, + ReactorInstance federateReactor, + ReactorInstance mainInstance ) { - var channel = 0; // Next input channel to be replaced. - // If the port is not an input, ignore it. - if (input.isInput) { - for (source : input.immediateSources()) { - val sourceBankIndex = (source.getPortInstance().parent.bankIndex >= 0) ? source.getPortInstance().parent.bankIndex : 0 - val sourceFederate = federatesByInstantiation.get(source.getPortInstance().parent.definition).get(sourceBankIndex); - - // Set up dependency information. - var connection = mainInstance.getConnection(source.getPortInstance(), input) - if (connection === null) { - // This should not happen. - errorReporter.reportError(input.definition, "Unexpected error. Cannot find input connection for port") - } else { - if (sourceFederate !== destinationFederate - && !connection.physical - && targetConfig.coordination !== CoordinationType.DECENTRALIZED) { - // Map the delays on connections between federates. - // First see if the cache has been created. - var dependsOnDelays = destinationFederate.dependsOn.get(sourceFederate) - if (dependsOnDelays === null) { - // If not, create it. - dependsOnDelays = new LinkedHashSet() - destinationFederate.dependsOn.put(sourceFederate, dependsOnDelays) - } - // Put the delay on the cache. - if (connection.delay !== null) { - dependsOnDelays.add(connection.delay) - } else { - // To indicate that at least one connection has no delay, add a null entry. - dependsOnDelays.add(null) - } - // Map the connections between federates. - var sendsToDelays = sourceFederate.sendsTo.get(destinationFederate) - if (sendsToDelays === null) { - sendsToDelays = new LinkedHashSet() - sourceFederate.sendsTo.put(destinationFederate, sendsToDelays) - } - if (connection.delay !== null) { - sendsToDelays.add(connection.delay) - } else { - // To indicate that at least one connection has no delay, add a null entry. - sendsToDelays.add(null) + for (srcRange : output.dependentPorts) { + for (RuntimeRange dstRange : srcRange.destinations) { + + var srcID = srcRange.startMR(); + val dstID = dstRange.startMR(); + var dstCount = 0; + var srcCount = 0; + + while (dstCount++ < dstRange.width) { + val srcChannel = srcID.digits.get(0); + val srcBank = srcID.get(1); + val dstChannel = dstID.digits.get(0); + val dstBank = dstID.get(1); + + val srcFederate = federatesByInstantiation.get( + srcRange.instance.parent.definition + ).get(srcBank); + val dstFederate = federatesByInstantiation.get( + dstRange.instance.parent.definition + ).get(dstBank); + + val connection = srcRange.connection; + + if (connection === null) { + // This should not happen. + errorReporter.reportError(output.definition, + "Unexpected error. Cannot find output connection for port") + } else { + if (srcFederate !== dstFederate + && !connection.physical + && targetConfig.coordination !== CoordinationType.DECENTRALIZED) { + // Map the delays on connections between federates. + // First see if the cache has been created. + var dependsOnDelays = dstFederate.dependsOn.get(srcFederate) + if (dependsOnDelays === null) { + // If not, create it. + dependsOnDelays = new LinkedHashSet() + dstFederate.dependsOn.put(srcFederate, dependsOnDelays) + } + // Put the delay on the cache. + if (connection.delay !== null) { + dependsOnDelays.add(connection.delay) + } else { + // To indicate that at least one connection has no delay, add a null entry. + dependsOnDelays.add(null) + } + // Map the connections between federates. + var sendsToDelays = srcFederate.sendsTo.get(dstFederate) + if (sendsToDelays === null) { + sendsToDelays = new LinkedHashSet() + srcFederate.sendsTo.put(dstFederate, sendsToDelays) + } + if (connection.delay !== null) { + sendsToDelays.add(connection.delay) + } else { + // To indicate that at least one connection has no delay, add a null entry. + sendsToDelays.add(null) + } } - } - - // Make one communication for each channel. - // FIXME: There is an opportunity for optimization here by aggregating channels. - for (var i = 0; i < source.channelWidth; i++) { + FedASTUtils.makeCommunication( - source.getPortInstance(), - input, + srcRange.instance, + dstRange.instance, connection, - sourceFederate, - source.getPortInstance().parent.bankIndex, - source.startChannel + i, - destinationFederate, - input.parent.bankIndex, - channel + i, + srcFederate, + srcBank, + srcChannel, + dstFederate, + dstBank, + dstChannel, this, targetConfig.coordination ); + } + dstID.increment(); + srcID.increment(); + srcCount++; + if (srcCount == srcRange.width) { + srcID = srcRange.startMR(); // Multicast. Start over. } - channel += source.channelWidth; } } } @@ -1817,22 +1224,6 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes */ abstract def Target getTarget() - protected def getTargetType(Parameter p) { - return p.inferredType.targetType - } - - protected def getTargetType(StateVar s) { - return s.inferredType.targetType - } - - protected def getTargetType(Action a) { - return a.inferredType.targetType - } - - protected def getTargetType(Port p) { - return p.inferredType.targetType - } - /** * Get textual representation of a time in the target language. * @@ -1881,7 +1272,7 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes if (d.parameter !== null) { return d.toText } else { - return d.time.toTimeValue.timeInTargetLanguage + return d.time.targetTime } } } diff --git a/org.lflang/src/org/lflang/generator/JavaGeneratorBase.java b/org.lflang/src/org/lflang/generator/JavaGeneratorBase.java new file mode 100644 index 0000000000..5e045468f7 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/JavaGeneratorBase.java @@ -0,0 +1,152 @@ +package org.lflang.generator; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; + +import org.lflang.lf.Code; +import org.lflang.validation.AbstractLFValidator; + +/** + * Generator base class for shared code between code generators. + * This is created to ease our migration from Xtend. + */ +public abstract class JavaGeneratorBase extends AbstractLFValidator { + //////////////////////////////////////////// + //// Protected fields. + + /** + * All code goes into this string buffer. + */ + protected StringBuilder code = new StringBuilder(); + + ///////////////////////////////////////////// + ///// Private fields. + + /** + * Map from builder to its current indentation. + */ + private final Map indentation = new HashMap<>(); + + ///////////////////////////////////////////// + ///// Protected methods. + + /** + * Get the code produced so far. + * @return The code produced so far as a String. + */ + protected String getCode() { + return code.toString(); + } + + /** + * Increase the indentation of the output code produced. + */ + protected void indent() { + indent(code); + } + + /** + * Increase the indentation of the output code produced + * on the specified builder. + * @param builder The builder to indent. + */ + protected void indent(StringBuilder builder) { + String prefix = indentation.get(builder); + if (prefix == null) { + prefix = ""; + } + prefix += " "; + indentation.put(builder, prefix); + } + + /** Reduce the indentation by one level for generated code + * in the default code buffer. + */ + protected void unindent() { + unindent(code); + } + + /** Reduce the indentation by one level for generated code + * in the specified code buffer. + */ + protected void unindent(StringBuilder builder) { + String indent = indentation.get(builder); + if (indent != null) { + indentation.put(builder, indent.substring(0, Math.max(0, indent.length() - 4))); + } + } + + /** + * Append the specified text plus a final newline to the current + * code buffer. + * @param format A format string to be used by {@code String.format} or + * the text to append if no further arguments are given. + * @param args Additional arguments to pass to the formatter. + */ + protected void pr(String format, Object... args) { + pr( + code, + (args != null && args.length > 0) ? String.format(format, args) : format + ); + } + + /** + * Append the specified text plus a final newline to the specified + * code buffer. + * @param builder The code buffer. + * @param text The text to append. + */ + protected void pr(StringBuilder builder, Object text) { + String string = text.toString(); + String indent = indentation.get(builder); + if (indent == null) { + indent = ""; + } + string = string.replaceAll("\t", " "); + String[] split = string.split("\n"); + int offset = Stream.of(split).skip(1) + .mapToInt(line -> line.indexOf(line.trim())) + .min() + .orElse(0); + // Now make a pass for each line, replacing the offset leading + // spaces with the current indentation. + boolean firstLine = true; + for (String line : split) { + builder.append(indent); + // Do not trim the first line + if (firstLine) { + builder.append(line); + firstLine = false; + } else { + builder.append(line.substring(offset)); + } + builder.append("\n"); + } + } + + /** + * Leave a marker in the generated code that indicates the original line + * number in the LF source. + * @param eObject The node. + */ + protected void prSourceLineNumber(EObject eObject) { + pr(code, String.format( + "// %d", + NodeModelUtils.getNode(eObject).getStartLine() + (eObject instanceof Code ? 1 : 0) + )); + } + + /** + * Print a comment to the generated file. + * Particular targets will need to override this if comments + * start with something other than '//'. + * @param comment The comment. + */ + protected void prComment(String comment) { + pr(code, "// " + comment); + } +} diff --git a/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java b/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java index ff24cbcd50..4ded957be6 100644 --- a/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java +++ b/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java @@ -1,9 +1,41 @@ package org.lflang.generator; import java.io.BufferedWriter; +import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.lang.CharSequence; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.generator.IFileSystemAccess2; +import org.eclipse.xtext.generator.IGeneratorContext; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.validation.CheckMode; +import org.eclipse.xtext.validation.IResourceValidator; +import org.lflang.ErrorReporter; +import org.lflang.FileConfig; +import org.lflang.Target; +import org.lflang.TargetConfig; +import org.lflang.TargetConfig.Mode; +import org.lflang.TargetProperty; +import org.lflang.graph.InstantiationGraph; +import org.lflang.lf.Action; +import org.lflang.lf.ActionOrigin; +import org.lflang.lf.Import; +import org.lflang.lf.KeyValuePair; +import org.lflang.lf.KeyValuePairs; +import org.lflang.lf.Model; +import org.lflang.lf.Reactor; +import org.lflang.lf.TargetDecl; /** * A helper class with functions that may be useful for code @@ -19,6 +51,247 @@ private JavaGeneratorUtils() { // utility class } + /** + * Return the target declaration found in the given resource. + */ + public static TargetDecl findTarget(Resource resource) { + TargetDecl targetDecl = null; + for (TargetDecl t : findAll(resource, TargetDecl.class)) { // getAllContents should never return null. + if (targetDecl != null) { + throw new InvalidSourceException("There is more than one target!"); // FIXME: check this in validator + } + targetDecl = t; + } + if (targetDecl == null) { + throw new InvalidSourceException("No target found!"); + } + return targetDecl; + } + + /** + * Set the appropriate target properties based on the target properties of + * the main .lf file and the given command-line arguments, if applicable. + * @param context The generator invocation context. + * @param target The target configuration that appears in an LF source file. + * @param targetConfig The target config to be updated. + * @param errorReporter The error reporter to which errors should be sent. + */ + public static void setTargetConfig( + LFGeneratorContext context, + TargetDecl target, + TargetConfig targetConfig, + ErrorReporter errorReporter + ) { + if (target.getConfig() != null) { + List pairs = target.getConfig().getPairs(); + TargetProperty.set(targetConfig, pairs != null ? pairs : List.of(), errorReporter); + } + if (context.getArgs().containsKey("no-compile")) { + targetConfig.noCompile = true; + } + if (context.getArgs().containsKey("threads")) { + targetConfig.threads = Integer.parseInt(context.getArgs().getProperty("threads")); + } + if (context.getArgs().containsKey("target-compiler")) { + targetConfig.compiler = context.getArgs().getProperty("target-compiler"); + } + if (context.getArgs().containsKey("target-flags")) { + targetConfig.compilerFlags.clear(); + if (!context.getArgs().getProperty("target-flags").isEmpty()) { + targetConfig.compilerFlags.addAll(List.of( + context.getArgs().getProperty("target-flags").split(" ") + )); + } + } + if (context.getArgs().containsKey("runtime-version")) { + targetConfig.runtimeVersion = context.getArgs().getProperty("runtime-version"); + } + if (context.getArgs().containsKey("external-runtime-path")) { + targetConfig.externalRuntimePath = context.getArgs().getProperty("external-runtime-path"); + } + if (context.getArgs().containsKey(TargetProperty.KEEPALIVE.description)) { + targetConfig.keepalive = Boolean.parseBoolean( + context.getArgs().getProperty(TargetProperty.KEEPALIVE.description)); + } + } + + /** + * Look for physical actions in 'resource'. + * If appropriate, set keepalive to true in + * {@code targetConfig}. + * This is a helper function for setTargetConfig. It + * should not be used elsewhere. + */ + public static void accommodatePhysicalActionsIfPresent( + List resources, + Target target, + TargetConfig targetConfig, + ErrorReporter errorReporter + ) { + if (!target.setsKeepAliveOptionAutomatically()) { + return; + } + for (Resource resource : resources) { + for (Action action : findAll(resource, Action.class)) { + if (action.getOrigin() == ActionOrigin.PHYSICAL) { + // Check if the user has explicitly set keepalive to false + if (!targetConfig.setByUser.contains(TargetProperty.KEEPALIVE) && !targetConfig.keepalive) { + // If not, set it to true + targetConfig.keepalive = true; + errorReporter.reportWarning( + action, + String.format( + "Setting %s to true because of the physical action %s.", + TargetProperty.KEEPALIVE.getDisplayName(), + action.getName() + ) + ); + return; + } + } + } + } + } + + /** + * Return all instances of {@code eObjectType} in + * {@code resource}. + * @param resource A resource to be searched. + * @param nodeType The type of the desired parse tree + * nodes. + * @param The type of the desired parse tree nodes. + * @return all instances of {@code eObjectType} in + * {@code resource} + */ + public static Iterable findAll(Resource resource, Class nodeType) { + Iterator contents = resource.getAllContents(); + assert contents != null : "Although getAllContents is not marked as NotNull, it should be."; + EObject temp = null; + while (!nodeType.isInstance(temp) && contents.hasNext()) temp = contents.next(); + EObject next_ = temp; + return () -> new Iterator<>() { + EObject next = next_; + + @Override + public boolean hasNext() { + return nodeType.isInstance(next); + } + + @Override + public T next() { + // This cast is safe if hasNext() holds. + assert hasNext() : "next() was called on an Iterator when hasNext() was false."; + //noinspection unchecked + T current = (T) next; + next = null; + while (!nodeType.isInstance(next) && contents.hasNext()) next = contents.next(); + return current; + } + }; + } + + /** + * Validate the files containing reactors in the given + * {@code instantiationGraph} and propagate the + * resulting errors. + * @param context The context providing the cancel + * indicator used by the validator. + * @param fileConfig The file system configuration. + * @param instantiationGraph A DAG containing all + * reactors of interest. + * @param errorReporter An error acceptor. + */ + public static void validateImports( + IGeneratorContext context, + FileConfig fileConfig, + InstantiationGraph instantiationGraph, + ErrorReporter errorReporter + ) { + // FIXME: This method is based on a part of setResources, a method that used to exist in GeneratorBase. + // It is quite different. There should be a test that verifies that it has the correct behavior. + IResourceValidator validator = ((XtextResource) fileConfig.resource).getResourceServiceProvider() + .getResourceValidator(); + HashSet bad = new HashSet<>(); + HashSet visited = new HashSet<>(); + // The graph must be traversed in topological order so that errors will propagate through arbitrarily many + // levels. + for (Reactor reactor : instantiationGraph.nodesInTopologicalOrder()) { + Resource resource = reactor.eResource(); + if (visited.contains(resource)) continue; + visited.add(resource); + if ( + bad.contains(resource) || validator.validate( + resource, CheckMode.ALL, context.getCancelIndicator() + ).size() > 0 + ) { + for (Reactor downstreamReactor : instantiationGraph.getDownstreamAdjacentNodes(reactor)) { + for (Import importStatement : ((Model) downstreamReactor.eContainer()).getImports()) { + errorReporter.reportError(importStatement, String.format( + "Unresolved compilation issues in '%s'.", importStatement.getImportURI() + )); + bad.add(downstreamReactor.eResource()); + } + } + } + } + } + + /** + * Return the resources that provide the given + * reactors. + * @param reactors The reactors for which to find + * containing resources. + * @return the resources that provide the given + * reactors. + */ + public static List getResources(Iterable reactors) { + HashSet visited = new HashSet<>(); + List resources = new ArrayList<>(); + for (Reactor r : reactors) { + Resource resource = r.eResource(); + if (!visited.contains(resource)) { + visited.add(resource); + resources.add(resource); + } + } + return resources; + } + + /** + * Return the {@code LFResource} representation of the + * given resource. + * @param resource The {@code Resource} to be + * represented as an {@code LFResource} + * @param fsa An object that provides access to the file + * system + * @param context The generator invocation context. + * @param errorReporter An error message acceptor. + * @return the {@code LFResource} representation of the + * given resource. + */ + public static LFResource getLFResource( + Resource resource, + IFileSystemAccess2 fsa, + LFGeneratorContext context, + ErrorReporter errorReporter + ) { + TargetDecl target = JavaGeneratorUtils.findTarget(resource); + KeyValuePairs config = target.getConfig(); + var targetConfig = new TargetConfig(); + if (config != null) { + List pairs = config.getPairs(); + TargetProperty.set(targetConfig, pairs != null ? pairs : List.of(), errorReporter); + } + FileConfig fc; + try { + fc = new FileConfig(resource, fsa, context); + } catch (IOException e) { + throw new RuntimeException("Failed to instantiate an imported resource because an I/O error " + + "occurred."); + } + return new LFResource(resource, fc, targetConfig); + } + /** * Write the source code to file. * @param code The code to be written. @@ -32,6 +305,45 @@ public static void writeSourceCodeToFile(CharSequence code, String path) throws } } + /** If the mode is INTEGRATED (the code generator is running in an + * an Eclipse IDE), then refresh the project. This will ensure that + * any generated files become visible in the project. + */ + public static void refreshProject(Mode compilerMode, String code) { + if (compilerMode == Mode.EPOCH) { + // Find name of current project + String id = "((:?[a-z]|[A-Z]|_\\w)*)"; + Pattern pattern; + if (File.separator.equals("/")) { // Linux/Mac file separator + pattern = Pattern.compile( + "platform:" + File.separator + "resource" + File.separator + id + File.separator); + } else { // Windows file separator + pattern = Pattern.compile( + "platform:" + File.separator + File.separator + "resource" + File.separator + File.separator + + id + File.separator + File.separator); + } + // FIXME: If we have to hunt through generated code to find this information, then maybe that's a sign + // that our left hand isn't talking to our right. + Matcher matcher = pattern.matcher(code); + String projName = ""; + if (matcher.find()) { + projName = matcher.group(1); + } + try { + IResource[] members = ResourcesPlugin.getWorkspace().getRoot().members(); + for (IResource member : members) { + // Refresh current project, or simply entire workspace if project name was not found + if (projName.isEmpty() || projName.equals(member.getFullPath().toString().substring(1))) { + member.refreshLocal(IResource.DEPTH_INFINITE, null); + System.out.println("Refreshed " + member.getFullPath()); + } + } + } catch (IllegalStateException | CoreException e) { + System.err.println("Unable to refresh workspace: " + e); + } + } + } + /** Return whether the operating system is Windows. */ public static boolean isHostWindows() { return System.getProperty("os.name").toLowerCase().contains("win"); diff --git a/org.lflang/src/org/lflang/generator/MixedRadixInt.java b/org.lflang/src/org/lflang/generator/MixedRadixInt.java new file mode 100644 index 0000000000..3ac703f226 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/MixedRadixInt.java @@ -0,0 +1,274 @@ +/* A representation for a mixed radix number. */ + +/* +Copyright (c) 2019-2021, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package org.lflang.generator; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * Representation of a permuted mixed radix (PMR) integer. + * A mixed radix number is a number representation where each digit can have + * a distinct radix. The radixes are given by a list of numbers, r0, r1, ... , rn, + * where r0 is the radix of the lowest-order digit and rn is the radix of the + * highest order digit that has a specified radix. + * + * A PMR is a mixed radix number that, when incremented, + * increments the digits in the order given by the permutation matrix. + * For an ordinary mixed radix number, the permutation matrix is + * [0, 1, ..., n-1]. The permutation matrix may be any permutation of + * these digits, [d0, d1, ..., dn-1], in which case, when incremented, + * the d0 digit will be incremented first. If it overflows, it will be + * set to 0 and the d1 digit will be incremented. If it overflows, the + * next digit is incremented. If the last digit overflows, then the + * number wraps around so that all digits become zero. + * + * This implementation realizes a finite set of numbers, where incrementing + * past the end of the range wraps around to the beginning. As a consequence, + * the increment() function from any starting point is guaranteed to eventually + * cover all possible values. + * + * The {@link #toString()} method gives a string representation of the number + * where each digit is represented by the string "d%r", where d is the digit + * and r is the radix. For example, the number "1%2, 2%3, 1%4" has value 11, + * 1 + (2*2) + (1*2*3). + * + * @author{Edward A. Lee } + */ +public class MixedRadixInt { + + /** + * Create a mixed radix number with the specified digits and radixes, + * which are given low-order digits first. + * If there is one more digit than radixes, throw an exception. + * @param digits The digits, or null to get a zero-valued number. + * @param radixes The radixes. + * @param permutation The permutation matrix, or null for the default permutation. + */ + public MixedRadixInt( + List digits, List radixes, List permutation + ) { + if (radixes == null + || (digits != null && digits.size() > radixes.size()) + || (permutation != null && permutation.size() != radixes.size()) + || radixes.contains(0)) { + throw new IllegalArgumentException("Invalid constructor arguments."); + } + this.radixes = radixes; + if (digits != null) { + this.digits = digits; + } else { + this.digits = new ArrayList(1); + this.digits.add(0); + } + if (permutation != null) { + // Check the permutation matrix. + Set indices = new HashSet(); + for (int p : permutation) { + if (p < 0 || p >= radixes.size() || indices.contains(p)) { + throw new IllegalArgumentException( + "Permutation list is required to be a permutation of [0, 1, ... , n-1]."); + } + indices.add(p); + } + this.permutation = permutation; + } + } + + /** + * A zero-valued mixed radix number with just one digit will radix 1. + */ + public final static MixedRadixInt ZERO = new MixedRadixInt(null, List.of(1), null); + + ////////////////////////////////////////////////////////// + //// Public methods + + /** + * Get the value as an integer. + */ + public int get() { + return get(0); + } + + /** + * Get the value as an integer after dropping the first n digits. + * @param n The number of digits to drop. + */ + public int get(int n) { + int result = 0; + int scale = 1; + if (n < 0) n = 0; + for (int i = n; i < radixes.size(); i++) { + if (i >= digits.size()) return result; + result += digits.get(i) * scale; + scale *= radixes.get(i); + } + return result; + } + + /** + * Return the digits. This is assured of returning as many + * digits as there are radixes. + */ + public List getDigits() { + while (digits.size() < radixes.size()) { + digits.add(0); + } + return digits; + } + + /** + * Return the permutation list. + */ + public List getPermutation() { + if (permutation == null) { + // Construct a default permutation. + permutation = new ArrayList(radixes.size()); + for (int i = 0; i < radixes.size(); i++) { + permutation.add(i); + } + } + return permutation; + } + + /** + * Return the radixes. + */ + public List getRadixes() { + return radixes; + } + + /** + * Increment the number by one, using the permutation vector to + * determine the order in which the digits are incremented. + * If an overflow occurs, then a radix-infinity digit will be added + * to the digits array if there isn't one there already. + */ + public void increment() { + int i = 0; + while (i < radixes.size()) { + int digit_to_increment = getPermutation().get(i); + while (digit_to_increment >= digits.size()) { + digits.add(0); + } + digits.set(digit_to_increment, digits.get(digit_to_increment) + 1); + if (digits.get(digit_to_increment) >= radixes.get(digit_to_increment)) { + digits.set(digit_to_increment, 0); + i++; + } else { + return; // All done. + } + } + } + + /** + * Return the magnitude of this PMR, which is defined to be the number + * of times that increment() would need to invoked starting with zero + * before the value returned by {@link #get()} would be reached. + */ + public int magnitude() { + int factor = 1; + int result = 0; + List p = getPermutation(); + for (int i = 0; i < radixes.size(); i++) { + if (digits.size() <= i) return result; + result += factor * digits.get(p.get(i)); + factor *= radixes.get(p.get(i)); + } + return result; + } + + /** + * Return the number of digits in this mixed radix number. + * This is the size of the radixes list. + */ + public int numDigits() { + return radixes.size(); + } + + /** + * Set the value of this number to equal that of the specified integer. + * @param v The ordinary integer value of this number. + */ + public void set(int v) { + int temp = v; + int count = 0; + for (int radix : radixes) { + if (count >= digits.size()) { + digits.add(temp % radix); + } else { + digits.set(count, temp % radix); + } + count++; + temp = temp / radix; + } + } + + /** + * Set the magnitude of this number to equal that of the specified integer, + * which is the number of times that increment must be invoked from zero + * for the value returned by {@link #get()} to equal v. + * @param v The new magnitude of this number. + */ + public void setMagnitude(int v) { + int temp = v; + for (int i = 0; i < radixes.size(); i++) { + int p = getPermutation().get(i); + while (digits.size() < p + 1) digits.add(0); + digits.set(p, temp % radixes.get(p)); + temp = temp / radixes.get(p); + } + } + + /** + * Give a string representation of the number, where each digit is + * represented as n%r, where r is the radix. + */ + @Override + public String toString() { + List pieces = new LinkedList(); + Iterator radixIterator = radixes.iterator(); + for (int digit : digits) { + if (! radixIterator.hasNext()) { + pieces.add(digit + "%infinity"); + } else { + pieces.add(digit + "%" + radixIterator.next()); + } + } + return String.join(", ", pieces); + } + + ////////////////////////////////////////////////////////// + //// Private variables + + private List radixes; + private List digits; + private List permutation; +} diff --git a/org.lflang/src/org/lflang/generator/NamedInstance.java b/org.lflang/src/org/lflang/generator/NamedInstance.java index 9aa3e7495a..b047a53bbd 100644 --- a/org.lflang/src/org/lflang/generator/NamedInstance.java +++ b/org.lflang/src/org/lflang/generator/NamedInstance.java @@ -26,11 +26,18 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY package org.lflang.generator; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; + import org.eclipse.emf.ecore.EObject; /** - * Base class for instances with names in Lingua Franca. + * Base class for compile-time instances with names in Lingua Franca. + * An instance of concrete subclasses of this class represents one or + * more runtime instances of a reactor, port, reaction, etc. There + * will be more than one runtime instance if the object or any of its + * parents is a bank of reactors. * * @author{Marten Lohstroh } * @author{Edward A. Lee } @@ -49,6 +56,14 @@ public abstract class NamedInstance { protected NamedInstance(T definition, ReactorInstance parent) { this.definition = definition; this.parent = parent; + + // Calculate the depth. + this.depth = 0; + ReactorInstance p = parent; + while (p != null) { + p = p.parent; + this.depth++; + } } ////////////////////////////////////////////////////// @@ -67,6 +82,14 @@ public T getDefinition() { return definition; } + /** + * Get the depth of the reactor instance. This is 0 for the main reactor, + * 1 for reactors immediately contained therein, etc. + */ + public int getDepth() { + return depth; + } + /** * Return the full name of this instance, which has the form * "a.b.c", where "c" is the name of this instance, "b" is the name @@ -103,6 +126,47 @@ public ReactorInstance getParent() { return parent; } + /** + * Return the width of this instance, which in this base class is 1. + * Subclasses PortInstance and ReactorInstance change this to the + * multiport and bank widths respectively. + */ + public int getWidth() { + return width; + } + + /** + * Return true if this instance has the specified parent + * (possibly indirectly, anywhere up the hierarchy). + */ + public boolean hasParent(ReactorInstance container) { + + ReactorInstance p = parent; + + while (p != null) { + if (p == container) return true; + p = p.parent; + } + return false; + } + + /** + * Return a list of all the parents starting with the root(). + */ + public List parents() { + List result = new ArrayList(depth + 1); + if (this instanceof ReactorInstance && parent == null) { + // This is the top level, so it must be a reactor. + result.add((ReactorInstance) this); + } + ReactorInstance container = parent; + while (container != null) { + result.add(container); + container = container.parent; + } + return result; + } + /** * Return the root reactor if it is marked as as main or federated, * and otherwise return null. @@ -120,8 +184,23 @@ public ReactorInstance main() { * Return the root reactor, which is the top-level parent. * @return The top-level parent. */ - public abstract ReactorInstance root(); + public ReactorInstance root() { + if (parent != null) { + return parent.root(); + } else { + return (ReactorInstance)this; + } + } + /** + * Set the width. This method is here for testing only and should + * not be used for any other purpose. + * @param width The new width. + */ + public void setWidth(int width) { + this.width = width; + } + /** * Return an identifier for this instance, which has the form "a_b_c" * or "a_b_c_n", where "c" is the name of this instance, "b" is the name @@ -190,6 +269,13 @@ public String uniqueID() { */ HashMap uniqueIDCount; + /** + * The width of this instance. This is 1 for everything + * except a PortInstance representing a multiport and a + * ReactorInstance representing a bank. + */ + int width = 1; + ////////////////////////////////////////////////////// //// Protected methods. @@ -210,6 +296,15 @@ protected String getFullNameWithJoiner(String joiner) { } } + ////////////////////////////////////////////////////// + //// Protected fields. + + /** + * The depth in the hierarchy of this instance. + * This is 0 for main or federated, 1 for the reactors immediately contained, etc. + */ + protected int depth = 0; + ////////////////////////////////////////////////////// //// Private fields. diff --git a/org.lflang/src/org/lflang/generator/ParameterInstance.java b/org.lflang/src/org/lflang/generator/ParameterInstance.java index b9dc5ec2c5..ee2d044e6e 100644 --- a/org.lflang/src/org/lflang/generator/ParameterInstance.java +++ b/org.lflang/src/org/lflang/generator/ParameterInstance.java @@ -27,19 +27,17 @@ package org.lflang.generator; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.lflang.InferredType; import org.lflang.JavaAstUtils; import org.lflang.lf.Assignment; -import org.lflang.lf.LfFactory; import org.lflang.lf.Parameter; import org.lflang.lf.Value; /** - * Representation of a runtime instance of a parameter. + * Representation of a compile-time instance of a parameter. * Upon creation, it is checked whether this parameter is overridden by an * assignment in the instantiation that this parameter instance is a result of. * If it is overridden, the parameter gets initialized using the value looked up @@ -62,29 +60,26 @@ public ParameterInstance(Parameter definition, ReactorInstance parent) { } this.type = JavaAstUtils.getInferredType(definition); - this.init = parent.initialParameterValue(definition); - - // If the parent is in a bank and the parameter name is "bank_index", then - // override the default value provided to make it equal to the bank index. - if (parent.bankIndex >= 0 && getName().equals("bank_index")) { - Value value = LfFactory.eINSTANCE.createValue(); - value.setLiteral("" + parent.bankIndex); - List list = new ArrayList(1); - list.add(value); - this.init = list; - } } ///////////////////////////////////////////// //// Public Fields - - public List init; - + public InferredType type; ///////////////////////////////////////////// //// Public Methods + /** + * Get the initial value(s) of this parameter as a list of + * Value objects, where each Value is either an instance + * of Time, Literal, or Code. That is, references to other + * parameters have been replaced with their initial values. + */ + public List getInitialValue() { + return parent.initialParameterValue(this.definition); + } + /** * Return the name of this parameter. * @return The name of this parameter. @@ -105,14 +100,6 @@ public Assignment getOverride() { return assignment.orElse(null); } - /** - * {@inheritDoc} - */ - @Override - public ReactorInstance root() { - return parent.root(); - } - /** Return a descriptive string. */ @Override public String toString() { diff --git a/org.lflang/src/org/lflang/generator/PortInstance.java b/org.lflang/src/org/lflang/generator/PortInstance.java index 54237d01b5..2841d3c854 100644 --- a/org.lflang/src/org/lflang/generator/PortInstance.java +++ b/org.lflang/src/org/lflang/generator/PortInstance.java @@ -1,7 +1,7 @@ /** A data structure for a port instance. */ /************* -Copyright (c) 2019, The University of California at Berkeley. +Copyright (c) 2019-2022, The University of California at Berkeley. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -26,11 +26,9 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY package org.lflang.generator; import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedList; +import java.util.Iterator; import java.util.List; import java.util.PriorityQueue; -import java.util.Set; import org.lflang.ErrorReporter; import org.lflang.lf.Input; @@ -41,8 +39,17 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.lf.WidthTerm; /** - * Representation of a runtime instance of a port. - * This may be a single port or a multiport. + * Representation of a compile-time instance of a port. + * Like {@link ReactorInstance}, one or more parents of this port + * is a bank of reactors, then there will be more than one runtime instance + * corresponding to this compile-time instance. + * + * This may be a single port or a multiport. If it is a multiport, then + * one instance of this PortInstance class represents all channels. + * If in addition any parent is a bank, then it represents all channels of all + * bank members. The {@link #eventualDestinations()} and {@link #eventualSources()} + * functions report the connectivity of all such channels as lists of + * {@link SendRange} and {@link RuntimeRange} objects. * * @author{Marten Lohstroh } * @author{Edward A. Lee } @@ -91,14 +98,14 @@ public void clearCaches() { clearingCaches = true; try { if (eventualSourceRanges != null) { - for (Range sourceRange : eventualSourceRanges) { - sourceRange.getPortInstance().clearCaches(); + for (RuntimeRange sourceRange : eventualSourceRanges) { + sourceRange.instance.clearCaches(); } } if (eventualDestinationRanges != null) { for (SendRange sendRange : eventualDestinationRanges) { - for (Range destinationRange : sendRange.destinations) { - destinationRange.getPortInstance().clearCaches(); + for (RuntimeRange destinationRange : sendRange.destinations) { + destinationRange.instance.clearCaches(); } } } @@ -116,9 +123,11 @@ public void clearCaches() { * range on which it receives data. * The ports listed are only ports that are sources for reactions, * not relay ports that the data may go through on the way. + * Also, if there is an "after" delay anywhere along the path, + * then the destination is not in the resulting list. * * If this port itself has dependent reactions, - * that this port will be included as a destination in all items + * then this port will be included as a destination in all items * on the returned list. * * Each item in the returned list has the following fields: @@ -137,134 +146,38 @@ public List eventualDestinations() { if (eventualDestinationRanges != null) { return eventualDestinationRanges; } - - // Getting the destinations is more complex than getting the sources - // because of multicast, where there is more than one connection statement - // for a source of data. The strategy we follow here is to first get all - // the ports that this port sends to. Then use eventualSource to get - // the ranges of this port that send to each destination. - PriorityQueue result = new PriorityQueue(); - - Set destinationPorts = null; - if (isOutput()) { - // For an output, obtain the destination ports from the parent - // of the port's parent. - ReactorInstance container = parent.getParent(); - // If the port's parent has no parent, then there are no destinations. - if (container == null) { - return new LinkedList(); - } - - destinationPorts = container.transitiveClosure(this); - } else { - // For an input, obtain the destination ports from the parent of this port. - // The port will be included in the returned set because it is an input. - destinationPorts = parent.transitiveClosure(this); - } - // If this port has dependent reactions, then add an entry for this port. - if (dependentReactions.size() > 0) { - SendRange thisPort = new SendRange(0, width); - thisPort.destinations.add(new Range(0, width)); - result.add(thisPort); - } - - for (PortInstance destinationPort: destinationPorts) { - // If the destination port has no dependent reactions, skip it. - // Also skip this port. - if (destinationPort.dependentReactions.isEmpty() || destinationPort == this) { - continue; - } - // Get the destination's source ranges and find the one(s) that match this port. - int destinationChannel = 0; - for (Range source : destinationPort.eventualSources()) { - if (source.getPortInstance() == this) { - // This destinationPort receives data from the channel range - // given by source of port. Add to the result list. - SendRange sendingRange = new SendRange( - source.startChannel, source.channelWidth - ); - Range receivingRange = destinationPort.newRange( - destinationChannel, source.channelWidth); - sendingRange.destinations.add(receivingRange); - result.add(sendingRange); - } - destinationChannel += source.channelWidth; - } - } - - // Now check for overlapping ranges, constructing a new result. - eventualDestinationRanges = new ArrayList(result.size()); - SendRange candidate = result.poll(); - while (!result.isEmpty()) { - SendRange next = result.poll(); - if (candidate.startChannel == next.startChannel) { - // Ranges have the same starting point. - if (candidate.channelWidth <= next.channelWidth) { - // Can use all of the channels. Import the destinations. - candidate.destinations.addAll(next.destinations); - if (candidate.channelWidth < next.channelWidth) { - // The next range has more channels connected to this sender. - next.startChannel += candidate.channelWidth; - next.channelWidth -= candidate.channelWidth; - result.add(next); - } // else we are done with next and can discard it. - } else { - // candidate is wider than next. - // Use next as the new candidate and split candidate. - candidate.startChannel += next.channelWidth; - candidate.channelWidth -= next.channelWidth; - result.add(candidate); - candidate = next; - } - } else { - // Because the result list is sorted, next starts at - // a higher channel than candidate. - if (candidate.startChannel + candidate.channelWidth <= next.startChannel) { - // Can use candidate as is and make next the new candidate. - eventualDestinationRanges.add(candidate); - candidate = next; - } else { - // Ranges overlap. Have to split candidate. - SendRange candidateTail = new SendRange( - next.startChannel, - candidate.channelWidth - (next.startChannel - candidate.startChannel) - ); - candidateTail.destinations.addAll(candidate.destinations); - result.add(candidateTail); - candidate.channelWidth -= candidateTail.channelWidth; - // Put next back on the list. - result.add(next); - } - } - } - if (candidate != null) eventualDestinationRanges.add(candidate); - + // Construct the full range for this port. + RuntimeRange range = new RuntimeRange.Port(this); + eventualDestinationRanges = eventualDestinations(range); return eventualDestinationRanges; } /** - * Return a list of ports that send data to this port annotated - * with the channel ranges of each source port. If this is not - * a multiport, then the list will have only one item and the - * channel range will contain only one channel. - * Otherwise, it will have enough items so that the ranges - * add up to the width of this multiport. + * Return a list of ranges of ports that send data to this port. + * If this port is directly written to by one more more reactions, + * then it is its own eventual source and only this port + * will be represented in the result. + * + * If this is not a multiport and is not within a bank, then the list will have + * only one item and the range will have a total width of one. Otherwise, it will + * have enough items so that the range widths add up to the width of this + * multiport multiplied by the total number of instances within containing banks. + * * The ports listed are only ports that are written to by reactions, * not relay ports that the data may go through on the way. */ - public List eventualSources() { - if (eventualSourceRanges == null) { - eventualSourceRanges = eventualSources(0, width); - } - return eventualSourceRanges; + public List> eventualSources() { + return eventualSources(new RuntimeRange.Port(this)); } /** - * Return the list of downstream ports that are connected to this port - * or an empty list if there are none. + * Return the list of ranges of this port together with the + * downstream ports that are connected to this port. + * The total with of the ranges in the returned list is a + * multiple N >= 0 of the total width of this port. */ - public List getDependentPorts() { + public List getDependentPorts() { return dependentPorts; } @@ -274,62 +187,10 @@ public List getDependentPorts() { * For an ordinary port, this list will have length 0 or 1. * For a multiport, it can have a larger size. */ - public List getDependsOnPorts() { + public List> getDependsOnPorts() { return dependsOnPorts; } - /** - * Return the width of this port, which in this base class is 1. - */ - public int getWidth() { - return width; - } - - /** - * Return a list of ports connected to this port together with - * the ranges of channels from those source ports. If this is not - * a multiport, then there will be only one element in the returned - * list and it will have width one. Otherwise, the widths of the - * elements in the returned list will sum to the width of this port. - * - * If this port is an output port that has dependent reactions - * and no upstream ports, then a singleton list containing - * this port with its full range is returned. - */ - public List immediateSources() { - List result = null; - if (!isMultiport) { - result = new ArrayList(1); - } else { - result = new ArrayList(); - } - if (isOutput() - && !dependentReactions.isEmpty() - && dependsOnPorts.isEmpty() - ) { - result.add(newRange(0, width)); - } - int channelsProvided = 0; - for (Range sourceRange : dependsOnPorts) { - // sourceRange.channelWidth is the number of channels this source has to offer. - // If we get here, the source can provide some channels. How many? - int srcStart = sourceRange.startChannel; - int srcWidth = sourceRange.channelWidth; // Candidate width if we can use them all. - if (channelsProvided + srcWidth > width) { - // Can't use all the source channels. - srcWidth = width - channelsProvided; - } - PortInstance src = sourceRange.getPortInstance(); - result.add(src.newRange(srcStart, srcWidth)); - channelsProvided += srcWidth; - if (channelsProvided >= width) { - // Done. - break; - } - } - return result; - } - /** * Return true if the port is an input. */ @@ -351,129 +212,26 @@ public boolean isOutput() { return (definition instanceof Output); } - /** - * Return the number of destination reactors for this port instance. - * This can be used to initialize reference counting, but not for - * multiport. For multiports, the number of destinations can vary - * by channel, and hence must be obtained from the ranges reported - * by eventualDestinations(); - */ - public int numDestinationReactors() { - List sourceChannelRanges = eventualDestinations(); - int result = 0; - for (SendRange ranges : sourceChannelRanges) { - result += ranges.getNumberOfDestinationReactors(); - } - return result; - } - @Override public String toString() { return "PortInstance " + getFullName(); - } - - ////////////////////////////////////////////////////// - //// Protected methods. - - /** - * Return a list of ports that send data to the specified channels of - * this port. The ports returned are annotated with the channel - * ranges of each source port. - * The ports listed are only ports that are written to by reactions, - * not relay ports that the data may go through on the way. - * - * If this port is an output port that has dependent reactions - * and no upstream ports, then a singleton list containing - * this port with its full range is returned. - * - * @param startRange The channel index for the start of the range of interest. - * @param rangeWidth The number of channels to find sources for. - */ - protected List eventualSources(int startRange, int rangeWidth) { - List result = null; - if (!isMultiport) { - result = new ArrayList(1); - } else { - result = new ArrayList(); - } - if (isOutput() - && !dependentReactions.isEmpty() - && dependsOnPorts.isEmpty() - ) { - result.add(newRange(startRange, rangeWidth)); - } - int channelsToSkip = startRange; - int channelsProvided = 0; - for (Range sourceRange : dependsOnPorts) { - // sourceRange.channelWidth is the number of channels this source has to offer. - if (sourceRange.channelWidth <= channelsToSkip) { - // No useful channels in this port. Skip it. - channelsToSkip -= sourceRange.channelWidth; - continue; - } - // If we get here, the source can provide some channels. How many? - int srcStart = sourceRange.startChannel + channelsToSkip; - int srcWidth = sourceRange.channelWidth - channelsToSkip; // Candidate width if we can use them all. - if (channelsProvided + srcWidth > rangeWidth) { - // Can't use all the source channels. - srcWidth = rangeWidth - channelsProvided; - } - PortInstance src = sourceRange.getPortInstance(); - // If this source depends on reactions, then include it in the result. - // Otherwise, keep looking upstream from it. - if (src.dependsOnReactions.isEmpty()) { - // Keep looking. - result.addAll(src.eventualSources(srcStart, srcWidth)); - } else { - result.add(src.newRange(srcStart, srcWidth)); - } - channelsProvided += srcWidth; - // No need to skip any more channels. - channelsToSkip = 0; - if (channelsProvided >= rangeWidth) { - // Done. - break; - } - } - return result; - } - - /** - * Create a SendingChannelRange representing a subset of the channels of this port. - * @param startChannel The lower end of the channel range. - * @param channelWidth The width of the range. - * @return A new instance of Range. - */ - protected SendRange newDestinationRange(int startChannel, int channelWidth) { - return new SendRange(startChannel, channelWidth); - } - - /** - * Create a Range representing a subset of the channels of this port. - * @param startChannel The lower end of the channel range. - * @param channelWidth The width of the range. - * @return A new instance of Range. - */ - protected Range newRange(int startChannel, int channelWidth) { - return new Range(startChannel, channelWidth); - } + } ////////////////////////////////////////////////////// //// Protected fields. /** - * Downstream ports that are connected directly to this port. - * These are listed in the order they appear in connections. - * If this port is input port, then the connections are those - * in the parent reactor of this port (inside connections). - * If the port is an output port, then the connections are those - * in the parent's parent (outside connections). - * The sum of the widths of the dependent ports is required to - * be an integer multiple N of the width of this port (this is checked + * Ranges of this port together with downstream ports that + * are connected directly to this port. When there are multiple destinations, + * the destinations are listed in the order they appear in connections + * in the parent reactor instance of this port (inside connections), + * followed by the order in which they appear in the parent's parent (outside + * connections). The total of the widths of these SendRanges is an integer + * multiple N >= 0 of the width of this port (this is checked * by the validator). Each channel of this port will be broadcast - * to N recipients. + * to N recipients (or, if there are no connections to zero recipients). */ - List dependentPorts = new ArrayList(); + List dependentPorts = new ArrayList(); /** * Upstream ports that are connected directly to this port, if there are any. @@ -481,24 +239,172 @@ protected Range newRange(int startChannel, int channelWidth) { * For a multiport, it can have a larger size. * This initially has capacity 1 because that is by far the most common case. */ - List dependsOnPorts = new ArrayList(1); + List> dependsOnPorts = new ArrayList>(1); /** Indicator of whether this is a multiport. */ boolean isMultiport = false; - /** - * The width of this port instance. - * For an ordinary port, this is 1. - * For a multiport, it may be larger than 1. - */ - int width = 1; - ////////////////////////////////////////////////////// //// Private methods. + + /** + * Given a RuntimeRange, return a list of SendRange that describes + * the eventual destinations of the given range. + * The sum of the total widths of the send ranges on the returned list + * will be an integer multiple N of the total width of the specified range. + * Each returned SendRange has a list + * of destination RuntimeRanges, each of which represents a port that + * has dependent reactions. Intermediate ports with no dependent + * reactions are not listed. + * @param srcRange The source range. + */ + private static List eventualDestinations(RuntimeRange srcRange) { + + // Getting the destinations is more complex than getting the sources + // because of multicast, where there is more than one connection statement + // for a source of data. The strategy we follow here is to first get all + // the ports that this port eventually sends to. Then, if needed, split + // the resulting ranges so that the resulting list covers exactly + // srcRange, possibly in pieces. We make two passes. First, we build + // a queue of ranges that may overlap, then we split those ranges + // and consolidate their destinations. + + List result = new ArrayList(); + PriorityQueue queue = new PriorityQueue(); + PortInstance srcPort = srcRange.instance; + + // Start with, if this port has dependent reactions, then add it to + // every range of the result. + if (!srcRange.instance.dependentReactions.isEmpty()) { + // This will be the final result if there are no connections. + SendRange candidate = new SendRange( + srcRange.instance, + srcRange.start, + srcRange.width, + null, // No interleaving for this range. + null // No connection for this range. + ); + candidate.destinations.add(srcRange); + queue.add(candidate); + } + + // Need to find send ranges that overlap with this srcRange. + Iterator sendRanges = srcPort.dependentPorts.iterator(); + while (sendRanges.hasNext()) { + + SendRange wSendRange = sendRanges.next(); + + if (wSendRange.connection != null && wSendRange.connection.getDelay() != null) { + continue; + } + + wSendRange = wSendRange.overlap(srcRange); + if (wSendRange == null) { + // This send range does not overlap with the desired range. Try the next one. + continue; + } + for (RuntimeRange dstRange : wSendRange.destinations) { + // Recursively get the send ranges of that destination port. + List dstSendRanges = eventualDestinations(dstRange); + int sendRangeStart = 0; + for (SendRange dstSend : dstSendRanges) { + queue.add(dstSend.newSendRange(wSendRange, sendRangeStart)); + sendRangeStart += dstSend.width; + } + } + } + + // Now check for overlapping ranges, constructing a new result. + SendRange candidate = queue.poll(); + SendRange next = queue.poll(); + while (candidate != null) { + if (next == null) { + // No more candidates. We are done. + result.add(candidate); + break; + } + if (candidate.start == next.start) { + // Ranges have the same starting point. Need to merge them. + if (candidate.width <= next.width) { + // Can use all of the channels of candidate. + // Import the destinations of next and split it. + for (RuntimeRange destination : next.destinations) { + candidate.destinations.add(destination.head(candidate.width)); + } + if (candidate.width < next.width) { + // The next range has more channels connected to this sender. + // Put it back on the queue an poll for a new next. + queue.add(next.tail(candidate.width)); + next = queue.poll(); + } else { + // We are done with next and can discard it. + next = queue.poll(); + } + } else { + // candidate is wider than next. Switch them and continue. + SendRange temp = candidate; + candidate = next; + next = temp; + } + } else { + // Because the result list is sorted, next starts at + // a higher channel than candidate. + if (candidate.start + candidate.width <= next.start) { + // Can use candidate as is and make next the new candidate. + result.add(candidate); + candidate = next; + next = queue.poll(); + } else { + // Ranges overlap. Can use a truncated candidate and make its + // truncated version the new candidate. + result.add(candidate.head(next.start)); + candidate = (SendRange)candidate.tail(next.start); + } + } + } + + return result; + } + + /** + * Return a list of ranges of ports that send data to this port within the + * specified range. If this port is directly written to by one more more reactions, + * then it is its own eventual source and only this port + * will be represented in the result. + * + * If this is not a multiport and is not within a bank, then the list will have + * only one item and the range will have a total width of one. Otherwise, it will + * have enough items so that the range widths add up to the width of this + * multiport multiplied by the total number of instances within containing banks. + * + * The ports listed are only ports that are written to by reactions, + * not relay ports that the data may go through on the way. + */ + private List> eventualSources(RuntimeRange range) { + if (eventualSourceRanges == null) { + // Cached result has not been created. + eventualSourceRanges = new ArrayList>(); + + if (!dependsOnReactions.isEmpty()) { + eventualSourceRanges.add(new RuntimeRange.Port(this)); + } else { + var channelsCovered = 0; + for (RuntimeRange sourceRange : dependsOnPorts) { + // Check whether the sourceRange overlaps with the range. + if (channelsCovered + sourceRange.width >= range.start + && channelsCovered < range.start + range.width) { + eventualSourceRanges.addAll(sourceRange.instance.eventualSources(sourceRange)); + } + channelsCovered += sourceRange.width; + } + } + } + return eventualSourceRanges; + } /** * Set the initial multiport width, if this is a multiport, from the widthSpec - * in the definition. + * in the definition. This will be set to -1 if the width cannot be determined. * @param errorReporter For reporting errors. */ private void setInitialWidth(ErrorReporter errorReporter) { @@ -523,13 +429,14 @@ private void setInitialWidth(ErrorReporter errorReporter) { if (parameterValue != null) { width += parameterValue; } else { - errorReporter.reportWarning(definition, - "Width of a multiport cannot be determined. Assuming 1." - ); - width += 1; + width = -1; + return; } - } else { + } else if (term.getWidth() != 0){ width += term.getWidth(); + } else { + width = -1; + return; } } } @@ -543,85 +450,8 @@ private void setInitialWidth(ErrorReporter errorReporter) { private List eventualDestinationRanges; /** Cached list of source ports with channel ranges. */ - private List eventualSourceRanges; + private List> eventualSourceRanges; /** Indicator that we are clearing the caches. */ private boolean clearingCaches = false; - - ////////////////////////////////////////////////////// - //// Inner classes. - - /** - * Class representing a range of channels of this port that broadcast to some - * number of destination ports' channels. All ranges have the same - * width, but not necessarily the same start index. - * This class extends its base class with a list destination channel ranges, - * all of which have the same width as this channel range. - * It also includes a field representing the number of destination - * reactors. - */ - public class SendRange extends Range { - - public SendRange(int startChannel, int channelWidth) { - super(startChannel, channelWidth); - - if (PortInstance.this.isMultiport) { - destinations = new ArrayList(); - } else { - destinations = new ArrayList(1); - } - } - - public int getNumberOfDestinationReactors() { - if (_numberOfDestinationReactors < 0) { - // Has not been calculate before. Calculate now. - Set destinations = new HashSet(); - for (Range destination : this.destinations) { - destinations.add(destination.getPortInstance().getParent()); - } - _numberOfDestinationReactors = destinations.size(); - } - return _numberOfDestinationReactors; - } - - public List destinations; - private int _numberOfDestinationReactors = -1; // Never access this directly. - } - - /** - * Class representing a range of channels of the enclosing port instance. - * If the enclosing port instance is not a multiport, this range will - * be (0,1). - */ - public class Range implements Comparable { - public Range(int startChannel, int channelWidth) { - // Some targets determine widths at runtime, in which case a - // width of 0 is reported here. Tolerate that. - if (channelWidth != 0 - && (startChannel < 0 || startChannel >= width - || channelWidth < 0 || startChannel + channelWidth > width)) { - throw new RuntimeException("Invalid range of port channels."); - } - this.startChannel = startChannel; - this.channelWidth = channelWidth; - } - public int startChannel; - public int channelWidth; - public PortInstance getPortInstance() { - return PortInstance.this; - } - /** - * Compare the ranges by just comparing their startChannel index. - */ - @Override - public int compareTo(Range o) { - if (startChannel < o.startChannel) { - return -1; - } else if (startChannel == o.startChannel) { - return 0; - } else { - return 1; - } - } - } } diff --git a/org.lflang/src/org/lflang/generator/ReactionInstance.java b/org.lflang/src/org/lflang/generator/ReactionInstance.java index c1b90dbcb7..f3b0991282 100644 --- a/org.lflang/src/org/lflang/generator/ReactionInstance.java +++ b/org.lflang/src/org/lflang/generator/ReactionInstance.java @@ -1,7 +1,7 @@ /** Representation of a runtime instance of a reaction. */ /************* -Copyright (c) 2019, The University of California at Berkeley. +Copyright (c) 2019-2022, The University of California at Berkeley. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -26,8 +26,10 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY package org.lflang.generator; -import java.util.Iterator; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Set; import org.lflang.TimeUnit; @@ -41,9 +43,14 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.lf.Variable; /** - * Representation of a runtime instance of a reaction. - * A ReactionInstance object stores all dependency information necessary - * for constructing a directed acyclic precedece graph. + * Representation of a compile-time instance of a reaction. + * Like {@link ReactorInstance}, one or more parents of this reaction + * is a bank of reactors, then there will be more than one runtime instance + * corresponding to this compile-time instance. The {@link #getRuntimeInstances()} + * method returns a list of these runtime instances, each an instance of the + * inner class {@link #Runtime}. Each runtime instance has a "level", which is + * its depth an acyclic precedence graph representing the dependencies between + * reactions at a tag. * * @author{Edward A. Lee } * @author{Marten Lohstroh } @@ -53,14 +60,14 @@ public class ReactionInstance extends NamedInstance { /** * Create a new reaction instance from the specified definition * within the specified parent. This constructor should be called - * only by the ReactionInstance class. + * only by the ReactorInstance class, but it is public to enable unit tests. * @param definition A reaction definition. * @param parent The parent reactor instance, which cannot be null. * @param isUnordered Indicator that this reaction is unordered w.r.t. other reactions. * @param index The index of the reaction within the reactor (0 for the * first reaction, 1 for the second, etc.). */ - protected ReactionInstance( + public ReactionInstance( Reaction definition, ReactorInstance parent, boolean isUnordered, @@ -70,12 +77,6 @@ protected ReactionInstance( this.index = index; this.isUnordered = isUnordered; - // If the reaction has no port triggers or sources, then - // we can immediately assign it a level. - // We also record it in the root reactor instance - // so that other reactions can be assigned levels as well. - boolean dependsOnPorts = false; - // Identify the dependencies for this reaction. // First handle the triggers. for (TriggerRef trigger : definition.getTriggers()) { @@ -95,36 +96,14 @@ protected ReactionInstance( ReactorInstance containedReactor = parent.lookupReactorInstance(((VarRef)trigger).getContainer()); if (containedReactor != null) { - if (containedReactor.bankMembers != null) { - // Contained reactor is a bank. Connect to all bank members. - for (ReactorInstance bankMember : containedReactor.bankMembers) { - portInstance = bankMember.lookupPortInstance((Port)variable); - if (portInstance != null) { - this.sources.add(portInstance); - portInstance.dependentReactions.add(this); - this.triggers.add(portInstance); - } - } - } else { - // Contained reactor is not a bank. - portInstance = containedReactor.lookupPortInstance((Port)variable); - if (portInstance != null) { - this.sources.add(portInstance); - portInstance.dependentReactions.add(this); - this.triggers.add(portInstance); - } + portInstance = containedReactor.lookupPortInstance((Port)variable); + if (portInstance != null) { + this.sources.add(portInstance); + portInstance.dependentReactions.add(this); + this.triggers.add(portInstance); } } } - // Mark this reaction as depending on a port and therefore not - // eligible to be assigned level 0. However, - // if the port is not connected and doesn't depend on any reactions, - // then it does not interfere with this reaction being given level 0. - if (portInstance != null - && (portInstance.dependsOnPorts.size() > 0 - || portInstance.dependsOnReactions.size() > 0)) { - dependsOnPorts = true; - } } else if (variable instanceof Action) { var actionInstance = parent.lookupActionInstance( (Action)((VarRef)trigger).getVariable()); @@ -161,48 +140,17 @@ protected ReactionInstance( ReactorInstance containedReactor = parent.lookupReactorInstance(source.getContainer()); if (containedReactor != null) { - if (containedReactor.bankMembers != null) { - // Contained reactor is a bank. Connect to all members. - for (ReactorInstance bankMember : containedReactor.bankMembers) { - portInstance = bankMember.lookupPortInstance((Port)variable); - if (portInstance != null) { - this.sources.add(portInstance); - portInstance.dependentReactions.add(this); - this.reads.add(portInstance); - } - } - } else { - // The trigger is a port of a contained reactor that is not a bank. - portInstance = containedReactor.lookupPortInstance((Port)variable); - if (portInstance != null) { - this.sources.add(portInstance); - portInstance.dependentReactions.add(this); - this.triggers.add(portInstance); - } + portInstance = containedReactor.lookupPortInstance((Port)variable); + if (portInstance != null) { + this.sources.add(portInstance); + portInstance.dependentReactions.add(this); + this.triggers.add(portInstance); } } } - // Mark this reaction as depending on a port and therefore not - // eligible to be assigned level 0. However, - // if the port is not connected and doesn't depend on any reactions, - // then it does not interfere with this reaction being given level 0. - if (portInstance != null - && (portInstance.dependsOnPorts.size() > 0 - || portInstance.dependsOnReactions.size() > 0)) { - dependsOnPorts = true; - } } } - // Initialize the root's readyReactions queue, which it uses - // to compute levels. - if (!dependsOnPorts) { - if (isUnordered || index == 0) { - level = 0L; - } - root().reactionsWithLevels.add(this); - } - // Finally, handle the effects. for (VarRef effect : definition.getEffects()) { Variable variable = effect.getVariable(); @@ -212,18 +160,8 @@ protected ReactionInstance( this.effects.add(portInstance); portInstance.dependsOnReactions.add(this); } else { - // The effect container must be a bank of reactors. - // Need to find the ports of all the instances within the bank. - ReactorInstance bank = parent.lookupReactorInstance(effect.getContainer()); - if (bank == null || bank.bankIndex != -2) { - throw new InvalidSourceException( - "Unexpected effect. Cannot find port " + variable.getName()); - } - for (ReactorInstance bankElement : bank.bankMembers) { - portInstance = bankElement.lookupPortInstance((Port)variable); - this.effects.add(portInstance); - portInstance.dependsOnReactions.add(this); - } + throw new InvalidSourceException( + "Unexpected effect. Cannot find port " + variable.getName()); } } else if (variable instanceof Action) { var actionInstance = parent.lookupActionInstance( @@ -280,12 +218,6 @@ protected ReactionInstance( */ public TimeValue deadline = new TimeValue(TimeValue.MAX_LONG_DEADLINE, TimeUnit.NANO); - /** - * The level in the dependence graph. -1 indicates that the level - * has not yet been assigned. - */ - public long level = -1L; - /** * Index of order of occurrence within the reactor definition. * The first reaction has index 0, the second index 1, etc. @@ -311,18 +243,6 @@ protected ReactionInstance( public Set> triggers = new LinkedHashSet>(); - /** - * Sources through which this reaction instance has been visited. - */ - public Set visited = new LinkedHashSet(); - - /** - * Counter that indicates how many times this node has been visited during - * the graph traversal that sets the chainIDs. Only when this counter hits zero - * shall the traversal continue to explore chains beyond this node. - */ - public int visitsLeft = 0; - ////////////////////////////////////////////////////// //// Public methods. @@ -330,10 +250,14 @@ protected ReactionInstance( * Clear caches used in reporting dependentReactions() and dependsOnReactions(). * This method should be called if any changes are made to triggers, sources, * or effects. + * @param includingRuntimes If false, leave the runtime instances intact. + * This is useful for federated execution where levels are computed using + * the top-level connections, but then those connections are discarded. */ - public void clearCaches() { + public void clearCaches(boolean includingRuntimes) { dependentReactionsCache = null; dependsOnReactionsCache = null; + if (includingRuntimes) runtimeInstances = null; } /** @@ -356,12 +280,12 @@ public Set dependentReactions() { // Next, add reactions that get data from this one via a port. for (TriggerInstance effect : effects) { if (effect instanceof PortInstance) { - for (PortInstance.SendRange senderRange + for (SendRange senderRange : ((PortInstance)effect).eventualDestinations()) { - for (PortInstance.Range destinationRange + for (RuntimeRange destinationRange : senderRange.destinations) { dependentReactionsCache.addAll( - destinationRange.getPortInstance().dependentReactions); + destinationRange.instance.dependentReactions); } } } @@ -397,9 +321,9 @@ public Set dependsOnReactions() { for (TriggerInstance source : sources) { if (source instanceof PortInstance) { // First, add reactions that send data through an intermediate port. - for (PortInstance.Range senderRange + for (RuntimeRange senderRange : ((PortInstance)source).eventualSources()) { - dependsOnReactionsCache.addAll(senderRange.getPortInstance().dependsOnReactions); + dependsOnReactionsCache.addAll(senderRange.instance.dependsOnReactions); } // Then, add reactions that send directly to this port. dependsOnReactionsCache.addAll(source.dependsOnReactions); @@ -409,15 +333,34 @@ public Set dependsOnReactions() { } /** - * Return the single dominating reaction if this reaction has one, or - * null otherwise. + * Return a set of levels that runtime instances of this reaction have. + * A ReactionInstance may have more than one level if it lies within + * a bank and its dependencies on other reactions pass through multiports. */ - public ReactionInstance findSingleDominatingReaction() { - if (dependsOnReactions().size() == 1) { - Iterator upstream = dependsOnReactionsCache.iterator(); - return upstream.next(); + public Set getLevels() { + Set result = new LinkedHashSet(); + // Force calculation of levels if it has not been done. + parent.assignLevels(); + for (Runtime runtime : runtimeInstances) { + result.add(runtime.level); } - return null; + return result; + } + + /** + * Return a list of levels that runtime instances of this reaction have. + * The size of this list is the total number of runtime instances. + * A ReactionInstance may have more than one level if it lies within + * a bank and its dependencies on other reactions pass through multiports. + */ + public List getLevelsList() { + List result = new LinkedList(); + // Force calculation of levels if it has not been done. + parent.assignLevels(); + for (Runtime runtime : runtimeInstances) { + result.add(runtime.level); + } + return result; } /** @@ -429,28 +372,58 @@ public ReactionInstance findSingleDominatingReaction() { public String getName() { return "reaction_" + this.index; } - + + /** + * Return an array of runtime instances of this reaction in a + * **natural order**, defined as follows. The position within the + * returned list of the runtime instance is given by a mixed-radix + * number where the low-order digit is the bank index within the + * container reactor (or 0 if it is not a bank), the second low order + * digit is the bank index of the container's container (or 0 if + * it is not a bank), etc., until the container that is directly + * contained by the top level (the top-level reactor need not be + * included because its index is always 0). + * + * The size of the returned array is the product of the widths of all of the + * container ReactorInstance objects. If none of these is a bank, + * then the size will be 1. + * + * This method creates this array the first time it is called, but then + * holds on to it. The array is used by {@link ReactionInstanceGraph} + * to determine and record levels and deadline for runtime instances + * of reactors. + */ + public List getRuntimeInstances() { + if (runtimeInstances != null) return runtimeInstances; + int size = parent.getTotalWidth(); + // If the width cannot be determined, assume there is only one instance. + if (size < 0) size = 1; + runtimeInstances = new ArrayList(size); + for (int i = 0; i < size; i++) { + Runtime r = new Runtime(); + r.id = i; + if (declaredDeadline != null) { + r.deadline = declaredDeadline.maxDelay; + } + runtimeInstances.add(r); + } + return runtimeInstances; + } + /** * Purge 'portInstance' from this reaction, removing it from the list - * of triggers, sources, effects, and reads. + * of triggers, sources, effects, and reads. Note that this leaves + * the runtime instances intact, including their level information. */ public void removePortInstance(PortInstance portInstance) { this.triggers.remove(portInstance); this.sources.remove(portInstance); this.effects.remove(portInstance); this.reads.remove(portInstance); - clearCaches(); + clearCaches(false); portInstance.clearCaches(); } - /** - * {@inheritDoc} - */ - @Override - public ReactorInstance root() { - return parent.root(); - } - /** * Return a descriptive string. */ @@ -467,4 +440,48 @@ public String toString() { /** Cache of the set of upstream reactions. */ private Set dependsOnReactionsCache; + + /** + * Array of runtime instances of this reaction. + * This has length 1 unless the reaction is contained + * by one or more banks. Suppose that this reaction + * has depth 3, with full name r0.r1.r2.r. The top-level + * reactor is r0, which contains r1, which contains r2, + * which contains this reaction r. Suppose the widths + * of the containing reactors are w0, w1, and w2, and + * we are interested in the instance at bank indexes + * b0, b1, and b2. That instance is in this array at + * location given by the **natural ordering**, which + * is the mixed radix number b2%w2; b1%w1. + */ + private List runtimeInstances; + + /////////////////////////////////////////////////////////// + //// Inner classes + + /** Inner class representing a runtime instance. */ + public class Runtime { + public TimeValue deadline = TimeValue.MAX_VALUE; + public Runtime dominating = null; + /** ID ranging from 0 to parent.getTotalWidth() - 1. */ + public int id = 0; + public int level = 0; + + public ReactionInstance getReaction() { + return ReactionInstance.this; + } + @Override + public String toString() { + String result = ReactionInstance.this.toString() + + "(level: " + level; + if (deadline != null && deadline != TimeValue.MAX_VALUE) { + result += ", deadline: " + deadline.toString(); + } + if (dominating != null) { + result += ", dominating: " + dominating.getReaction(); + } + result += ")"; + return result; + } + } } diff --git a/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java b/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java new file mode 100644 index 0000000000..fbcfee645e --- /dev/null +++ b/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java @@ -0,0 +1,257 @@ +/** A graph that represents causality cycles formed by reaction instances. */ + +/************* +Copyright (c) 2021, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ + +package org.lflang.generator; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.lflang.generator.ReactionInstance.Runtime; +import org.lflang.graph.DirectedGraph; +import org.lflang.lf.Variable; + +/** + * This class analyzes the dependencies between reaction runtime instances. + * For each ReactionInstance, there may be more than one runtime instance because + * the ReactionInstance may be nested within one or more banks. + * In the worst case, of these runtime instances may have distinct dependencies, + * and hence distinct levels in the graph. Moreover, some of these instances + * may be involved in cycles while others are not. + * + * Upon construction of this class, the runtime instances are created if necessary, + * stored each ReactionInstance, and assigned levels (maximum number of + * upstream reaction instances), deadlines, and single dominating reactions. + * + * After creation, the resulting graph will be empty unless there are causality + * cycles, in which case, the resulting graph is a graph of runtime reaction + * instances that form cycles. + * + * @author{Marten Lohstroh } + * @author{Edward A. Lee } + */ +public class ReactionInstanceGraph extends DirectedGraph { + + /** + * Create a new graph by traversing the maps in the named instances + * embedded in the hierarchy of the program. + */ + public ReactionInstanceGraph(ReactorInstance main) { + this.main = main; + rebuild(); + } + + /////////////////////////////////////////////////////////// + //// Public fields + + /** + * The main reactor instance that this graph is associated with. + */ + public final ReactorInstance main; + + /////////////////////////////////////////////////////////// + //// Public methods + + /** + * Rebuild this graph by clearing and repeating the traversal that + * adds all the nodes and edges. + */ + public void rebuild() { + this.clear(); + addNodesAndEdges(main); + // Assign a level to each reaction. + // If there are cycles present in the graph, it will be detected here. + assignLevels(); + if (nodeCount() != 0) { + // The graph has cycles. + // main.reporter.reportError("Reactions form a cycle! " + toString()); + // Do not throw an exception so that cycle visualization can proceed. + // throw new InvalidSourceException("Reactions form a cycle!"); + } + } + + /////////////////////////////////////////////////////////// + //// Protected methods + + /** + * Add to the graph edges between the given reaction and all the reactions + * that depend on the specified port. + * @param port The port that the given reaction has as an effect. + * @param reaction The reaction to relate downstream reactions to. + */ + protected void addDownstreamReactions(PortInstance port, ReactionInstance reaction) { + // Use mixed-radix numbers to increment over the ranges. + List srcRuntimes = reaction.getRuntimeInstances(); + List eventualDestinations = port.eventualDestinations(); + + int srcDepth = (port.isInput())? 2 : 1; + + for (SendRange sendRange : eventualDestinations) { + for (RuntimeRange dstRange : sendRange.destinations) { + + int dstDepth = (dstRange.instance.isOutput())? 2 : 1; + MixedRadixInt dstRangePosition = dstRange.startMR(); + int dstRangeCount = 0; + + MixedRadixInt sendRangePosition = sendRange.startMR(); + int sendRangeCount = 0; + + while (dstRangeCount++ < dstRange.width) { + int srcIndex = sendRangePosition.get(srcDepth); + int dstIndex = dstRangePosition.get(dstDepth); + for (ReactionInstance dstReaction : dstRange.instance.dependentReactions) { + List dstRuntimes = dstReaction.getRuntimeInstances(); + Runtime srcRuntime = srcRuntimes.get(srcIndex); + Runtime dstRuntime = dstRuntimes.get(dstIndex); + if (dstRuntime != srcRuntime) { + addEdge(dstRuntime, srcRuntime); + } + + // Propagate the deadlines, if any. + if (srcRuntime.deadline.compareTo(dstRuntime.deadline) > 0) { + srcRuntime.deadline = dstRuntime.deadline; + } + + // If this seems to be a single dominating reaction, set it. + // If another upstream reaction shows up, then this will be + // reset to null. + if (this.getUpstreamAdjacentNodes(dstRuntime).size() == 1 + && (dstRuntime.getReaction().isUnordered + || dstRuntime.getReaction().index == 0)) { + dstRuntime.dominating = srcRuntime; + } else { + dstRuntime.dominating = null; + } + } + dstRangePosition.increment(); + sendRangePosition.increment(); + sendRangeCount++; + if (sendRangeCount >= sendRange.width) { + // Reset to multicast. + sendRangeCount = 0; + sendRangePosition = sendRange.startMR(); + } + } + } + } + } + + /** + * Build the graph by adding nodes and edges based on the given reactor + * instance. + * @param reactor The reactor on the basis of which to add nodes and edges. + */ + protected void addNodesAndEdges(ReactorInstance reactor) { + ReactionInstance previousReaction = null; + for (ReactionInstance reaction : reactor.reactions) { + List runtimes = reaction.getRuntimeInstances(); + + // Add reactions of this reactor. + for (Runtime runtime : runtimes) { + this.addNode(runtime); + } + + // If this is not an unordered reaction, then create a dependency + // on any previously defined reaction. + if (!reaction.isUnordered) { + // If there is an earlier reaction in this same reactor, then + // create a link in the reaction graph for all runtime instances. + if (previousReaction != null) { + List previousRuntimes = previousReaction.getRuntimeInstances(); + int count = 0; + for (Runtime runtime : runtimes) { + this.addEdge(runtime, previousRuntimes.get(count)); + count++; + } + } + previousReaction = reaction; + } + + // Add downstream reactions. Note that this is sufficient. + // We don't need to also add upstream reactions because this reaction + // will be downstream of those upstream reactions. + for (TriggerInstance effect : reaction.effects) { + if (effect instanceof PortInstance) { + addDownstreamReactions((PortInstance)effect, reaction); + } + } + } + // Recursively add nodes and edges from contained reactors. + for (ReactorInstance child : reactor.children) { + addNodesAndEdges(child); + } + } + + /////////////////////////////////////////////////////////// + //// Private methods + + /** + * Analyze the dependencies between reactions and assign each reaction + * instance a level. This method removes nodes from this graph as it + * assigns levels. Any remaining nodes are part of causality cycles. + * + * This procedure is based on Kahn's algorithm for topological sorting. + * Rather than establishing a total order, we establish a partial order. + * In this order, the level of each reaction is the least upper bound of + * the levels of the reactions it depends on. + */ + private void assignLevels() { + List start = new ArrayList(rootNodes()); + + // All root nodes start with level 0. + for (Runtime origin : start) { + origin.level = 0; + } + + // No need to do any of this if there are no root nodes; + // the graph must be cyclic. + while (!start.isEmpty()) { + Runtime origin = start.remove(0); + Set toRemove = new LinkedHashSet(); + // Visit effect nodes. + for (Runtime effect : getDownstreamAdjacentNodes(origin)) { + // Stage edge between origin and effect for removal. + toRemove.add(effect); + + // Update level of downstream node. + effect.level = Math.max(effect.level, origin.level+1); + } + // Remove visited edges. + for (Runtime effect : toRemove) { + removeEdge(effect, origin); + // If the effect node has no more incoming edges, + // then move it in the start set. + if (getUpstreamAdjacentNodes(effect).size() == 0) { + start.add(effect); + } + } + + // Remove visited origin. + removeNode(origin); + } + } +} diff --git a/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.xtend b/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.xtend deleted file mode 100644 index 238cd21d7f..0000000000 --- a/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.xtend +++ /dev/null @@ -1,364 +0,0 @@ -/** A graph that represents the dependencies between reaction instances. */ - -/************* -Copyright (c) 2021, The University of California at Berkeley. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL -THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ - -package org.lflang.generator - -import java.util.ArrayList -import java.util.LinkedHashSet -import java.util.Set -import org.lflang.graph.DirectedGraph - -/** - * This graph represents the dependencies between reaction instances. - * Upon creation, reactions are assigned levels, chainIDs, and their - * deadlines are propagated. - * - * @author{Marten Lohstroh } - * @author{Edward A. Lee } - */ -class ReactionInstanceGraph extends DirectedGraph { - - /** - * The main reactor instance that this graph is associated with. - */ - var ReactorInstance main - - /** - * Count of the number of chains while assigning chainIDs. - */ - int branchCount = 1 - - /** - * Create a new graph by traversing the maps in the named instances - * embedded in the hierarchy of the program. - */ - new(ReactorInstance main) { - this.main = main - rebuild() - } - - /** - * Rebuild this graph by clearing is an repeating the traversal that - * adds all the nodes and edges. - */ - def rebuild() { - this.clear() - addNodesAndEdges(main) - // Assign a level to each reaction. - // If there are cycles present in the graph, it will be detected here. - val leftoverReactions = assignLevels() - if (leftoverReactions.nodeCount != 0) { - // The validator should have caught cycles, but if there is a bug in some - // AST transform such that it introduces cycles, then it is possible to have them - // only detected here. An end user should never see this. - main.reporter.reportError("Reactions form a cycle! " + leftoverReactions.toString()); - throw new InvalidSourceException("Reactions form a cycle!") - } - // Traverse the graph again, now starting from the leaves, - // to set the chain IDs. - assignChainIDs(false) - - // Propagate any declared deadline upstream. - propagateDeadlines() - - } - - /** - * Return the single dominating reaction if the given reaction has one, or - * null otherwise. - */ - def findSingleDominatingReaction(ReactionInstance reaction) { - val reactions = getUpstreamAdjacentNodes(reaction) - if (reactions.size == 1) { - return reactions.get(0) - } - return null - } - - /** - * From the given set of reactions, return the subset that is maximal. - * A reaction in the set is maximal if there is no other reaction in - * the set that depends on it, directly or indirectly. If the argument - * is an empty set, return an empty set. - * @param reactions A set of reaction instances. - * @param minLevel The lowest level reaction to visit. - */ - protected def Set maximal(Set reactions, - long minLevel) { - var result = new LinkedHashSet(reactions) - for (reaction : reactions) { - if (reaction.level >= minLevel && reaction.visitsLeft > 0) { - reaction.visitsLeft-- - val upstream = getUpstreamAdjacentNodes(reaction) - result.removeAll(upstream) - result.removeAll(maximal(upstream.toSet, minLevel)) - } - } - return result - } - - /** - * Analyze the dependencies between reactions and assign each reaction - * instance a level. - * This procedure is based on Kahn's algorithm for topological sorting. - * Rather than establishing a total order, we establish a partial order. - * In this order, the level of each reaction is the least upper bound of - * the levels of the reactions it depends on. - * - * @return If any cycles are present in the dependency graph, then a graph - * containing the nodes in the cycle is returned. Otherwise, null is - * returned. - */ - private def DirectedGraph assignLevels() { - val graph = this.copy - var start = new ArrayList(graph.rootNodes) - - // All root nodes start with level 0. - for (origin : start) { - origin.level = 0 - } - - // No need to do any of this if there are no root nodes; - // the graph must be cyclic. - if (!graph.rootNodes.isEmpty) { - while (!start.empty) { - val origin = start.remove(0) - val toRemove = new LinkedHashSet() - // Visit effect nodes. - for (effect : graph.getDownstreamAdjacentNodes(origin)) { - // Stage edge between origin and effect for removal. - toRemove.add(effect) - - // Update level of downstream node. - effect.level = Math.max(effect.level, origin.level+1) - } - // Remove visited edges. - for (effect : toRemove) { - graph.removeEdge(effect, origin) - // If the effect node has no more incoming edges, - // then move it in the start set. - if (graph.getUpstreamAdjacentNodes(effect).size == 0) { - start.add(effect) - } - } - - // Remove visited origin. - graph.removeNode(origin) - - } - } - // If, after all of this, there are still any nodes left, - // then the graph must be cyclic. - return graph - } - - /** - * Analyze the dependencies between reactions and assign each reaction - * instance a chain identifier. The assigned IDs are such that the - * bitwise conjunction between two chain IDs is always nonzero if there - * exists a dependency between them. This facilitates runtime checks - * to determine whether a reaction is ready to execute or has to wait - * for an upstream reaction to complete. - * @param graph The dependency graph. - * @param optimize Whether or not make assignments that maximize the - * amount of parallelism. If false, just assign 1 to every node. - */ - protected def assignChainIDs(boolean optimize) { - val leafs = this.leafNodes - this.branchCount = 0 - for (node : this.nodes) { - node.visitsLeft = this.getDownstreamAdjacentNodes(node).size - } - if (optimize) { - // Start propagation from the leaf nodes, - // ordered by level from high to low. - for (node : leafs.sortBy[-level]) { - this.propagateUp(node, 1 << (this.branchCount++ % 64)) - } - } else { - for (node: this.nodes) { - node.chainID = 1 - } - } - } - - /** - * Propagate the given chain ID up one chain, propagate fresh IDs to - * other upstream neighbors, and return a mask that overlaps with all the - * chain IDs that were set upstream as a result of this method invocation. - * The result of propagation is that each node has an ID that overlaps with - * all upstream nodes that can reach it. This means that if a node has a - * lower level than another node, but the two nodes do not have overlapping - * chain IDs, the nodes are nonetheless independent from one another. - * @param current The current node that is being visited. - * @param graph The graph that encodes the dependencies between reactions. - * @param chainID The current chain ID. - */ - private def long propagateUp(ReactionInstance current, long chainID) { - val origins = this.getUpstreamAdjacentNodes(current) - var mask = chainID - var first = true - var id = current.chainID.bitwiseOr(chainID) - current.visitsLeft-- - if (current.visitsLeft > 0) { - current.chainID = id - return chainID; - } - // Iterate over the upstream neighbors by level from high to low. - for (upstream : origins.sortBy[-level]) { - if (first) { - // Stay on the same chain the first time. - first = false - } else { - // Create a new chain ID. - id = 1 << (this.branchCount++ % 64) - } - // Propagate the ID upstream and add all returned bits - // to the mask. - mask = mask.bitwiseOr( - propagateUp(upstream, id)) - } - // Apply the mask to the current chain ID. - // If there were no upstream neighbors, the mask will - // just be the chainID that was passed as an argument. - current.chainID = current.chainID.bitwiseOr(mask) - - return mask - } - - /** - * Iterate over all reactions that have a declared deadline, update their - * inferred deadline, as well as the inferred deadlines of any reactions - * upstream. - */ - def propagateDeadlines() { - val reactionsWithDeadline = this.nodes.filter[it.definition.deadline !== null] - // Assume the graph is acyclic. - for (r : reactionsWithDeadline) { - if (r.declaredDeadline !== null && - r.declaredDeadline.maxDelay !== null) { - // Only lower the inferred deadline (which is set to the max by default), - // if the declared deadline is earlier than the inferred one (based on - // some other downstream deadline). - if (r.declaredDeadline.maxDelay !== null - && r.declaredDeadline.maxDelay.isEarlierThan(r.deadline) - ) { - r.deadline = r.declaredDeadline.maxDelay - } - } - propagateDeadline(r) - } - } - - /** - * Given a reaction instance, propagate its inferred deadline upstream. - * @param downstream Reaction instance with an inferred deadline that - * is to be propagated upstream. - */ - def void propagateDeadline(ReactionInstance downstream) { - for (upstream : this.getUpstreamAdjacentNodes(downstream)) { - // Only lower the inferred deadline (which is set to the max by default), - // if downstream deadline is earlier than the inferred one (based on - // some other downstream deadline). - if (downstream.deadline.isEarlierThan(upstream.deadline)) { - upstream.deadline = downstream.deadline - } - propagateDeadline(upstream) - } - } - - /** - * Add to the graph edges between the given reaction and all the reactions - * that the specified port depends on. - * @param port The port that the given reaction as as a source. - * @param reaction The reaction to relate upstream reactions to. - */ - protected def void addUpstreamReactions(PortInstance port, - ReactionInstance reaction) { - // Reactions in the containing reactor. - port.dependsOnReactions.forEach[this.addEdge(reaction, it)] - // Reactions in upstream reactors. - for (upstreamPort : port.dependsOnPorts) { - addUpstreamReactions(upstreamPort.portInstance, reaction) - } - } - - /** - * Add to the graph edges between the given reaction and all the reactions - * that depend on the specified port. - * @param port The port that the given reaction as as an effect. - * @param reaction The reaction to relate downstream reactions to. - */ - protected def void addDownstreamReactions(PortInstance port, - ReactionInstance reaction) { - // Reactions in the containing reactor. - port.dependentReactions.forEach[this.addEdge(it, reaction)] - // Reactions in downstream reactors. - for (downstreamPort : port.dependentPorts) { - addDownstreamReactions(downstreamPort.portInstance, reaction) - } - } - - - /** - * Build the graph by adding nodes and edges based on the given reactor - * instance. - * @param reactor The reactor on the basis of which to add nodes and edges. - */ - protected def void addNodesAndEdges(ReactorInstance reactor) { - var ReactionInstance previousReaction = null - for (reaction : reactor.reactions) { - // Add reactions of this reactor. - this.addNode(reaction) - - // Reactions that depend on a port that this reaction writes to - // also, by transitivity, depend on this reaction instance. - reaction.effects.filter(PortInstance).forEach [ effect | - addDownstreamReactions(effect, reaction) - ] - - // Reactions that write to such a port are also reactions that - // that this reaction depends on, by transitivity. - reaction.sources.filter(PortInstance).forEach [ source | - addUpstreamReactions(source, reaction) - ] - // If this is not an unordered reaction, then create a dependency - // on any previously defined reaction. - if (!reaction.isUnordered) { - // If there is an earlier reaction in this same reactor, then - // create a link in the reaction graph. - if (previousReaction !== null) { - this.addEdge(reaction, previousReaction) - } - previousReaction = reaction; - } - } - // Recursively add nodes and edges from contained reactors. - for (child : reactor.children) { - addNodesAndEdges(child) - } - } -} diff --git a/org.lflang/src/org/lflang/generator/ReactorInstance.java b/org.lflang/src/org/lflang/generator/ReactorInstance.java index 1399615be0..09cc8c2c96 100644 --- a/org.lflang/src/org/lflang/generator/ReactorInstance.java +++ b/org.lflang/src/org/lflang/generator/ReactorInstance.java @@ -1,7 +1,7 @@ /** A data structure for a reactor instance. */ /************* -Copyright (c) 2019, The University of California at Berkeley. +Copyright (c) 2019-2022, The University of California at Berkeley. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -26,14 +26,11 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY package org.lflang.generator; -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Deque; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; import org.lflang.ASTUtils; @@ -51,6 +48,7 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.lf.Port; import org.lflang.lf.Reaction; import org.lflang.lf.Reactor; +import org.lflang.lf.ReactorDecl; import org.lflang.lf.Timer; import org.lflang.lf.TriggerRef; import org.lflang.lf.Value; @@ -60,11 +58,21 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY /** - * Representation of a runtime instance of a reactor. + * Representation of a compile-time instance of a reactor. + * If the reactor is instantiated as a bank of reactors, or if any + * of its parents is instantiated as a bank of reactors, then one instance + * of this ReactorInstance class represents all the runtime instances within + * these banks. The {@link #getTotalWidth()} method returns the number of such + * runtime instances, which is the product of the bank width of this reactor + * instance and the bank widths of all of its parents. + * There is exactly one instance of this ReactorInstance class for each + * graphical rendition of a reactor in the diagram view. + * * For the main reactor, which has no parent, once constructed, * this object represents the entire Lingua Franca program. - * The constructor analyzes the graph of dependencies between - * reactions and throws exception if this graph is cyclic. + * If the program has causality loops (a programming error), then + * {@link #hasCycles()} will return true and {@link #getCycles()} will + * return the ports and reaction instances involved in the cycles. * * @author{Marten Lohstroh } * @author{Edward A. Lee } @@ -101,6 +109,18 @@ public ReactorInstance(Reactor reactor, ErrorReporter reporter, Set un this(ASTUtils.createInstantiation(reactor), null, reporter, -1, unorderedReactions); } + /** + * Create a new instantiation with the specified parent. + * This constructor is here to allow for unit tests. + * It should not be used for any other purpose. + * @param reactor The top-level reactor. + * @param parent The parent reactor instance. + * @param reporter The error reporter. + */ + public ReactorInstance(Reactor reactor, ReactorInstance parent, ErrorReporter reporter) { + this(ASTUtils.createInstantiation(reactor), parent, reporter, -1, null); + } + ////////////////////////////////////////////////////// //// Public fields. @@ -113,24 +133,27 @@ public ReactorInstance(Reactor reactor, ErrorReporter reporter, Set un * Reactor (which has bankIndex == -2) followed by each of the * bank members (which have bankIndex >= 0). */ - public List children = new ArrayList<>(); - + public final List children = new ArrayList<>(); + /** The input port instances belonging to this reactor instance. */ - public List inputs = new ArrayList<>(); + public final List inputs = new ArrayList<>(); /** The output port instances belonging to this reactor instance. */ - public List outputs = new ArrayList<>(); + public final List outputs = new ArrayList<>(); /** The parameters of this instance. */ - public List parameters = new ArrayList<>(); + public final List parameters = new ArrayList<>(); /** List of reaction instances for this reactor instance. */ - public List reactions = new ArrayList<>(); + public final List reactions = new ArrayList<>(); /** The timer instances belonging to this reactor instance. */ - public List timers = new ArrayList<>(); + public final List timers = new ArrayList<>(); - /** The reactor definition in the AST. */ + /** The reactor declaration in the AST. This is either an import or Reactor declaration. */ + public final ReactorDecl reactorDeclaration; + + /** The reactor after imports are resolve. */ public final Reactor reactorDefinition; /** Indicator that this reactor has itself as a parent, an error condition. */ @@ -150,99 +173,17 @@ public ReactorInstance(Reactor reactor, ErrorReporter reporter, Set un * in V + E, where V is the number of vertices (reactions) and E * is the number of edges (dependencies between reactions). * - * @return True if successful and false if a causality cycle exists. + * @return An empty graph if successful and otherwise a graph + * with runtime reaction instances that form cycles. */ - public boolean assignLevels() { - if (root() != this) { - return root().assignLevels(); - } else { - // This operation is relatively expensive, so we cache the result. - // This will need a mechanism to force recomputation if - // this class ever supports mutations (e.g. in a Java target). - if (levelsAssignedAlready > 0) return true; - - int count = 0; - while (!reactionsWithLevels.isEmpty()) { - ReactionInstance reaction = reactionsWithLevels.poll(); - count++; - // Check downstream reactions to see whether they can get levels assigned. - for (ReactionInstance downstream : reaction.dependentReactions()) { - // Check whether all upstream of downstream have levels. - long candidateLevel = reaction.level + 1L; - for (ReactionInstance upstream : downstream.dependsOnReactions()) { - if (upstream.level < 0L) { - // downstream reaction is not ready to get a level. - candidateLevel = -1L; - break; - } else if (candidateLevel < upstream.level + 1L) { - candidateLevel = upstream.level + 1L; - } - } - if (candidateLevel > 0 && candidateLevel > downstream.level) { - // Can assign a level to downstream. - downstream.level = candidateLevel; - reactionsWithLevels.add(downstream); - } - } - } - if (count < root().totalNumberOfReactions()) { - reporter.reportError(definition, "Reactions form a causality cycle!"); - levelsAssignedAlready = 0; - return false; - } - levelsAssignedAlready = 1; - return true; - } - } - - /** - * Returns the size of the bank that this reactor represents or - * 0 if this reactor does not represent a bank. - */ - public int bankSize() { - if (bankMembers != null) { - return bankMembers.size(); + public ReactionInstanceGraph assignLevels() { + if (depth != 0) return root().assignLevels(); + if (cachedReactionLoopGraph == null) { + cachedReactionLoopGraph = new ReactionInstanceGraph(this); } - return 0; + return cachedReactionLoopGraph; } - /** - * Return the destinations of the specified port. - * The result is a set (albeit an ordered set) of ports that are destinations - * in connections. This will return null if the source has no destinations. - */ - public Set destinations(PortInstance source) { - Map map = connectionTable.get(source); - if (map != null) { - return map.keySet(); - } - return null; - } - - /** - * If this reactor is in a bank of reactors, then return - * the reactor instance defining the bank. Otherwise, return null. - */ - public ReactorInstance getBank() { - return bank; - } - - /** - * If this reactor is in a bank of reactors, return its index, otherwise, return -1 - * for an ordinary reactor and -2 for a placeholder for a bank of reactors. - */ - public int getBankIndex() { - return bankIndex; - } - - /** - * Return the members of this bank, or null if there are none. - * @return actual bank size or -1 if this is not a bank master. - */ - public List getBankMembers() { - return bankMembers; - } - /** * Return the instance of a child rector created by the specified * definition or null if there is none. @@ -256,30 +197,70 @@ public ReactorInstance getChildReactorInstance(Instantiation definition) { } return null; } - + /** - * Return the Connection that created the link between the specified source - * and destination, or null if there is no such link. + * Clear any cached data in this reactor and its children. + * This is useful if a mutation has been realized. */ - public Connection getConnection(PortInstance source, PortInstance destination) { - var table = connectionTable.get(source); - if (table != null) { - return table.get(destination); - } - return null; + public void clearCaches() { + clearCaches(true); } /** - * Get a map of connections as they appear in a visualization of the program. - * For each connection, there is map from source ports (single ports and multiports) - * on the left side of the connection to a set of destination ports (single ports - * and multiports) on the right side of the connection. For banks of reactors, - * this includes only the connections to and from the first element of the bank. + * Clear any cached data in this reactor and its children. + * This is useful if a mutation has been realized. + * @param includingRuntimes If false, leave the runtime instances of reactions intact. + * This is useful for federated execution where levels are computed using + * the top-level connections, but then those connections are discarded. */ - public Map>> getConnections() { - return connections; + public void clearCaches(boolean includingRuntimes) { + if (includingRuntimes) cachedReactionLoopGraph = null; + for (ReactorInstance child : children) { + child.clearCaches(includingRuntimes); + } + for (PortInstance port : inputs) { + port.clearCaches(); + } + for (PortInstance port : outputs) { + port.clearCaches(); + } + for (ReactionInstance reaction : reactions) { + reaction.clearCaches(includingRuntimes); + } + cachedCycles = null; } + /** + * Return the set of ReactionInstance and PortInstance that form causality + * loops in the topmost parent reactor in the instantiation hierarchy. This will return an + * empty set if there are no causality loops. + */ + public Set> getCycles() { + if (depth != 0) return root().getCycles(); + if (cachedCycles != null) return cachedCycles; + Set reactions = new LinkedHashSet(); + + ReactionInstanceGraph reactionRuntimes = assignLevels(); + for (ReactionInstance.Runtime runtime : reactionRuntimes.nodes()) { + reactions.add(runtime.getReaction()); + } + Set ports = new LinkedHashSet(); + // Need to figure out which ports are involved in the cycles. + // It may not be all ports that depend on this reaction. + for (ReactionInstance r : reactions) { + for (TriggerInstance p : r.effects) { + if (p instanceof PortInstance) { + findPaths((PortInstance)p, reactions, ports); + } + } + } + + cachedCycles = new LinkedHashSet>(); + cachedCycles.addAll(reactions); + cachedCycles.addAll(ports); + return cachedCycles; + } + /** * Return the specified input by name or null if there is no such input. * @param name The input name. @@ -294,18 +275,13 @@ public PortInstance getInput(String name) { } /** - * Override the base class to append [index] if this reactor - * is in a bank of reactors. - * @return The full name of this instance. + * Override the base class to append [i_d], where d is the depth, + * if this reactor is in a bank of reactors. + * @return The name of this instance. */ @Override public String getName() { - var result = this.definition.getName(); - if (this.bankIndex >= 0) { - result += "[" + this.bankIndex + "]"; - } - if (result == null) return ""; - return result; + return this.definition.getName(); } /** @@ -320,7 +296,7 @@ public PortInstance getOutput(String name) { } return null; } - + /** * Return a parameter matching the specified name if the reactor has one * and otherwise return null. @@ -349,6 +325,39 @@ public TriggerInstance getShutdownTrigger() { return shutdownTrigger; } + /** + * If this reactor is a bank or any of its parents is a bank, + * return the total number of runtime instances, which is the product + * of the widths of all the parents. + * Return -1 if the width cannot be determined. + */ + public int getTotalWidth() { + return getTotalWidth(0); + } + + /** + * If this reactor is a bank or any of its parents is a bank, + * return the total number of runtime instances, which is the product + * of the widths of all the parents. + * Return -1 if the width cannot be determined. + * @param atDepth The depth at which to determine the width. + * Use 0 to get the total number of instances. + * Use 1 to get the number of instances within a single top-level + * bank member (this is useful for federates). + */ + public int getTotalWidth(int atDepth) { + if (width <= 0) return -1; + if (depth <= atDepth) return 1; + int result = width; + ReactorInstance p = parent; + while (p != null && p.depth > atDepth) { + if (p.width <= 0) return -1; + result *= p.width; + p = p.parent; + } + return result; + } + /** * Return the trigger instances (input ports, timers, and actions * that trigger reactions) belonging to this reactor instance. @@ -379,6 +388,13 @@ public Set> getTriggersAndReads() { return triggers; } + /** + * Return true if the top-level parent of this reactor has causality cycles. + */ + public boolean hasCycles() { + return (assignLevels().nodeCount() != 0); + } + /** * Given a parameter definition for this reactor, return the initial integer * value of the parameter. If the parameter is overridden when instantiating @@ -443,8 +459,7 @@ public List instantiations() { * @return true if a reactor is a bank, false otherwise */ public boolean isBank() { - // FIXME magic number - return bankIndex == -2; + return (definition.getWidthSpec() != null); } /** @@ -456,6 +471,20 @@ public boolean isMainOrFederated() { && (reactorDefinition.isMain() || reactorDefinition.isFederated()); } + /** + * Return true if the specified reactor instance is either equal to this + * reactor instance or a parent of it. + * @param r The reactor instance. + */ + public boolean isParent(ReactorInstance r) { + ReactorInstance p = this; + while (p != null) { + if (p == r) return true; + p = p.getParent(); + } + return false; + } + /////////////////////////////////////////////////// //// Methods for finding instances in this reactor given an AST node. @@ -584,26 +613,6 @@ public TimerInstance lookupTimerInstance(Timer timer) { return null; } - /** - * {@inheritDoc} - */ - @Override - public ReactorInstance root() { - if (parent != null) { - return parent.root(); - } else { - return this; - } - } - - /** - * Return the set of ports in that are sources in connections in this reactor. - * These may be input ports of this reactor or output ports of contained reactors. - */ - public Set sources() { - return connectionTable.keySet(); - } - /** * Return a descriptive string. */ @@ -612,71 +621,6 @@ public String toString() { return "ReactorInstance " + getFullName(); } - /** - * Return the total number of reactions in this reactor - * and all its contained reactors. - */ - public int totalNumberOfReactions() { - if (totalNumberOfReactionsCache >= 0) return totalNumberOfReactionsCache; - totalNumberOfReactionsCache = reactions.size(); - for (ReactorInstance containedReactor : children) { - totalNumberOfReactionsCache += containedReactor.totalNumberOfReactions(); - } - return totalNumberOfReactionsCache; - } - - /** - * Return the set of all ports that receive data from the - * specified source. This includes inputs and outputs at the same level - * of hierarchy and input ports deeper in the hierarchy. - * It also includes inputs or outputs up the hierarchy (i.e., ones - * that are reached via any output port). - * If the argument is an input port, then it is included in the result. - * No port will appear more than once in the result. - * - * @param source An output or input port. - */ - public Set transitiveClosure(PortInstance source) { - var result = new LinkedHashSet(); - transitiveClosure(source, result); - return result; - } - - /** - * Override the base class to return the uniqueID of the bank rather - * than this member of the bank, if this is a member of a bank of reactors. - * - * @return An identifier for this instance that is guaranteed to be - * unique within the top-level parent. - */ - @Override - public String uniqueID() { - if (this.bank != null) { - return this.bank.uniqueID(); - } - return super.uniqueID(); - } - - /** - * For the specified width specification, return the width. - * This may be for a bank of reactors within this reactor instance or - * for a port of this reactor instance. If the argument is null, there - * is no width specification, so return 1. Otherwise, evaluate the - * width value by determining the value of any referenced parameters. - * - * @param widthSpec The width specification. - * - * @return The width, or -1 if it cannot be determined. - */ - public int width(WidthSpec widthSpec) { - if (widthSpec.eContainer() instanceof Instantiation && parent != null) { - // We need the instantiations list of the containing reactor, - // not this one. - return ASTUtils.width(widthSpec, parent.instantiations()); - } - return ASTUtils.width(widthSpec, instantiations()); - } - /** * Assuming that the given value denotes a valid time, return a time value. * @@ -686,7 +630,7 @@ public int width(WidthSpec widthSpec) { public TimeValue getTimeValue(Value v) { Parameter p = v.getParameter(); if (p != null) { - return JavaAstUtils.getLiteralTimeValue(lookupParameterInstance(p).init.get(0)); + return JavaAstUtils.getLiteralTimeValue(lookupParameterInstance(p).getInitialValue().get(0)); } else { return JavaAstUtils.getLiteralTimeValue(v); } @@ -701,7 +645,7 @@ public TimeValue getTimeValue(Value v) { public TimeValue getTimeValue(Delay d) { Parameter p = d.getParameter(); if (p != null) { - return JavaAstUtils.getLiteralTimeValue(lookupParameterInstance(p).init.get(0)); + return JavaAstUtils.getLiteralTimeValue(lookupParameterInstance(p).getInitialValue().get(0)); } else { return JavaAstUtils.toTimeValue(d.getTime()); } @@ -710,46 +654,6 @@ public TimeValue getTimeValue(Delay d) { ////////////////////////////////////////////////////// //// Protected fields. - /** - * If this reactor is in a bank of reactors, then this member - * refers to the reactor instance defining the bank. - */ - protected ReactorInstance bank = null; - - /** - * If this reactor instance is a placeholder for a bank of reactors, - * as created by the new[width] ReactorClass() syntax, then this - * list will be non-null and will contain the reactor instances in - * the bank. - */ - protected List bankMembers = null; - - /** - * If this reactor is in a bank of reactors, its index, otherwise, -1 - * for an ordinary reactor and -2 for a placeholder for a bank of reactors. - */ - protected int bankIndex = -1; - - /** - * Table recording connections and which connection created a link between - * a source and destination. Use a source port as a key to obtain a Map. - * The key set of the obtained Map is the set of destination ports. - * The value of the obtained Map is the connection that established the - * connection. - */ - protected Map> connectionTable - = new LinkedHashMap<>(); - - /** - * For a root reactor instance only, this will be a queue of reactions - * that either have been assigned an initial level or are ready to be - * assigned levels. During construction of a top-level ReactorInstance, - * this queue will be populated with reactions anywhere in the hierarchy - * that have been assigned level 0 during construction because they have - * no dependencies on other reactions. - */ - protected Deque reactionsWithLevels = new ArrayDeque<>(); - /** The generator that created this reactor instance. */ protected ErrorReporter reporter; // FIXME: This accumulates a lot of redundant references @@ -771,12 +675,6 @@ public TimeValue getTimeValue(Delay d) { /** The nested list of instantiations that created this reactor instance. */ protected List _instantiations; - /** - * The depth in the hierarchy of this reactor instance. - * This is 0 for main or federated, 1 for the reactors immediately contained, etc. - */ - protected int depth = 0; - ////////////////////////////////////////////////////// //// Protected methods. @@ -827,116 +725,15 @@ protected TriggerInstance getOrCreateShutdown(TriggerRef tri return shutdownTrigger; } - /** - * Collect all reactions that have not been assigned a level and - * return the list. - * @param reactor The reactor for which to check reactions. - * @param result The list to add reactions to. - * @return The list of reactions without levels. - */ - protected List reactionsWithoutLevels( - ReactorInstance reactor, - List result - ) { - for (ReactionInstance reaction : reactor.reactions) { - if (reaction.level < 0L) { - result.add(reaction); - } - } - for (ReactorInstance child : reactor.children) { - reactionsWithoutLevels(child, result); - } - return result; - } - - /** - * Add to the specified destinations set all ports that receive data from the - * specified source. This includes inputs and outputs at the same level - * of hierarchy and input ports deeper in the hierarchy. - * It also includes inputs or outputs up the hierarchy (i.e., ones - * that are reached via any output port). - * @param source A port belonging to this reaction instance or one - * of its children. - * @param destinations The set of destinations to populate. - */ - protected void transitiveClosure( - PortInstance source, - LinkedHashSet destinations - ) { - // Check that the specified port belongs to this reactor or one of its children. - // The following assumes that the main reactor has no ports, or else - // a NPE will occur. - if (source.parent != this && source.parent.parent != this) { - throw new InvalidSourceException( - "Internal error: port " + source + " does not belong to " + - this + " nor any of its children." - ); - } - // If the port is a multiport, then iterate over its contained ordinary ports instead. - // If the port is an input port, then include it in the result. - if (source.isInput()) { - destinations.add(source); - } - Map map = connectionTable.get(source); - if (map != null) { - Set localDestinations = map.keySet(); - - if (localDestinations != null) { - for (PortInstance destination : localDestinations) { - destinations.add(destination); - if (destination.isInput()) { - // Destination may have further destinations lower in the hierarchy. - destination.parent.transitiveClosure(destination, destinations); - } else if (destination.parent.parent != null) { - // Destination may have further destinations higher in the hierarchy. - destination.parent.parent.transitiveClosure(destination, destinations); - } - } - } - } - } - //////////////////////////////////////// //// Private constructors - /** - * Create reactor instance resulting from the specified top-level instantiation. - * @param definition The declaration in the AST. - * @param parent The parent, or null for the main rector. - * @param reporter The error reporter. - * @param desiredDepth The depth to which to expand the hierarchy. - * @param unorderedReactions A list of reactions that should be treated as unordered. - */ - private ReactorInstance( - Instantiation definition, - ReactorInstance parent, - ErrorReporter reporter, - int desiredDepth, - Set unorderedReactions - ) { - // If the reactor is being instantiated with new[width], then pass -2 - // to the constructor, otherwise pass -1. - this( - definition, - parent, - reporter, - (definition.getWidthSpec() != null)? -2 : -1, - 0, - desiredDepth, - unorderedReactions - ); - } - /** * Create a runtime instance from the specified definition * and with the specified parent that instantiated it. - * @param definition The declaration in the AST. + * @param definition The instantiation statement in the AST. * @param parent The parent, or null for the main rector. - * @param reporter The error reporter. - * @param reactorIndex -1 for an ordinary reactor, -2 for a - * placeholder for a bank of reactors, or the index of the - * reactor in a bank of reactors otherwise. - * @param depth The depth of this reactor in the hierarchy. + * @param reporter An error reporter. * @param desiredDepth The depth to which to expand the hierarchy. * @param unorderedReactions A list of reactions that should be treated as unordered. * It can be passed as null. @@ -945,15 +742,13 @@ private ReactorInstance( Instantiation definition, ReactorInstance parent, ErrorReporter reporter, - int reactorIndex, - int depth, int desiredDepth, Set unorderedReactions) { super(definition, parent); this.reporter = reporter; - this.bankIndex = reactorIndex; - this.reactorDefinition = ASTUtils.toDefinition(definition.getReactorClass()); - this.depth = depth; + this.reactorDeclaration = definition.getReactorClass(); + this.reactorDefinition = ASTUtils.toDefinition(reactorDeclaration); + if (unorderedReactions != null) { this.unorderedReactions = unorderedReactions; } @@ -976,30 +771,7 @@ private ReactorInstance( if (recursive) { reporter.reportError(definition, "Recursive reactor instantiation."); } - - // If this reactor is actually a bank of reactors, then instantiate - // each individual reactor in the bank and skip the rest of the - // initialization for this reactor instance. - if (reactorIndex == -2) { - // If the bank width is variable, then we have to wait until the first connection - // before instantiating the children. - var width = width(definition.getWidthSpec()); - if (width > 0) { - this.bankMembers = new ArrayList<>(width); - for (var index = 0; index < width; index++) { - var childInstance = new ReactorInstance( - definition, parent, reporter, index, depth, desiredDepth, this.unorderedReactions - ); - this.bankMembers.add(childInstance); - childInstance.bank = this; - childInstance.bankIndex = index; - } - } else { - reporter.reportWarning(definition, "Cannot infer width."); - } - return; - } - + // If the reactor definition is null, give up here. Otherwise, diagram generation // will fail an NPE. if (reactorDefinition == null) { @@ -1007,6 +779,8 @@ private ReactorInstance( return; } + setInitialWidth(); + // Apply overrides and instantiate parameters for this reactor instance. for (Parameter parameter : ASTUtils.allParameters(reactorDefinition)) { this.parameters.add(new ParameterInstance(parameter, this)); @@ -1024,23 +798,17 @@ private ReactorInstance( // Do not process content (except interface above) if recursive if (!recursive && (desiredDepth < 0 || this.depth < desiredDepth)) { - // Instantiate children for this reactor instance + // Instantiate children for this reactor instance. + // While doing this, assign an index offset to each. for (Instantiation child : ASTUtils.allInstantiations(reactorDefinition)) { var childInstance = new ReactorInstance( child, this, reporter, - (child.getWidthSpec() != null)? -2 : -1, - depth + 1, desiredDepth, this.unorderedReactions ); this.children.add(childInstance); - // If the child is a bank of instances, add all the bank instances. - // These must be added after the bank itself. - if (childInstance.bankMembers != null) { - this.children.addAll(childInstance.bankMembers); - } } // Instantiate timers for this reactor instance @@ -1067,100 +835,28 @@ private ReactorInstance( //// Private methods. /** - * Record the connection from the source port to the destination port in the - * connectionTable map. - * @param source The source port. - * @param destination The destination port. - * @param connection The connection AST node creating the connection. - */ - private void addDestination(PortInstance source, PortInstance destination, Connection connection) { - Map srcConnections = connectionTable.get(source); - if (srcConnections == null) { - srcConnections = new LinkedHashMap<>(); - connectionTable.put(source, srcConnections); - } - srcConnections.put(destination, connection); - } - - /** - * Connect the given left port instance to the given right port instance. - * These may be multiports. - * @param connection The connection statement creating this connection. - * @param srcInstance The source instance (the left port). - * @param srcChannel The starting channel number for the source. - * @param dstInstance The destination instance (the right port). - * @param dstChannel The starting channel number for the destination. - * @param width The width of this connection. + * Connect the given left port range to the given right port range. + * + * NOTE: This method is public to enable its use in unit tests. + * Otherwise, it should be private. This is why it is defined here, + * in the section labeled "Private methods." + * + * @param src The source range. + * @param dst The destination range. + * @param connection The connection establishing this relationship. */ - private void connectPortInstances( - Connection connection, - PortInstance srcInstance, - int srcChannel, - PortInstance dstInstance, - int dstChannel, - int width + public static void connectPortInstances( + RuntimeRange src, + RuntimeRange dst, + Connection connection ) { - PortInstance.Range dstRange = dstInstance.newRange(dstChannel, width); - srcInstance.dependentPorts.add(dstRange); - - PortInstance.Range srcRange = srcInstance.newRange(srcChannel, width); - dstInstance.dependsOnPorts.add(srcRange); - - // Record the connection in the connection table. - // Original cryptic xtend code: - // this.destinations.compute(srcInstance, [key, set| CollectionUtil.plus(set, dstInstance)]) - // this.connectionTable.compute(srcInstance, [key, map| CollectionUtil.plus(map, dstInstance, connection)]) - addDestination(srcInstance, dstInstance, connection); - - // The following is support for the diagram visualization. - - // The source may be at a bank index greater than 0. - // For visualization, this needs to be converted to the source - // at bank 0, because only that one is rendered. - // We want the rendering to represent all connections. - var src = srcInstance; - var dst = dstInstance; - if (src.isOutput() && src.parent.bankIndex > 0) { - // Replace the source with the corresponding port instance - // at bank index 0. - ReactorInstance newParent = src.parent.bank.bankMembers.get(0); - src = newParent.getOutput(src.getName()); - } - // The destination may be at a bank index greater than 0. - // For visualization, this needs to be converted to the destination - // at bank 0, because only that one is rendered. - // We want the rendering to represent all connections. - if (dst.isInput() && dst.parent.bankIndex > 0) { - // Replace the destination with the corresponding port instance - // at bank index 0. - ReactorInstance newParent = dst.parent.bank.bankMembers.get(0); - dst = newParent.getInput(dst.getName()); - } - - // Record this representative connection for visualization in the - // connections map. - Map> map = connections.get(connection); - if (map == null) { - map = new LinkedHashMap<>(); - connections.put(connection, map); - } - Set destinations = map.get(src); - if (destinations == null) { - destinations = new LinkedHashSet<>(); - map.put(src, destinations); - } - destinations.add(dst); - - // Original cryptic xtend code below. - // val src2 = src - // val dst2 = dst - // this.connections.compute(connection, [_, links| { - // CollectionUtil.compute(links, src2, [_2, destinations| CollectionUtil.plus(destinations, dst2)]) - // }]) + SendRange range = new SendRange(src, dst, src._interleaved, connection); + src.instance.dependentPorts.add(range); + dst.instance.dependsOnPorts.add(src); } /** - * Populate destinations map and the connectivity information in the port instances. + * Populate connectivity information in the port instances. * Note that this can only happen _after_ the children and port instances have been created. * Unfortunately, we have to do some complicated things here * to support multiport-to-multiport, multiport-to-bank, @@ -1170,83 +866,134 @@ private void connectPortInstances( */ private void establishPortConnections() { for (Connection connection : ASTUtils.allConnections(reactorDefinition)) { - List leftRanges = listPortInstances(connection.getLeftPorts()); - List rightRanges = listPortInstances(connection.getRightPorts()); - - // Check widths. FIXME: This duplicates validator checks! - int leftWidth = 0; - for (PortInstance.Range range: leftRanges) { - leftWidth += range.channelWidth; - } - int rightWidth = 0; - for (PortInstance.Range range: rightRanges) { - rightWidth += range.channelWidth; - } - if (leftWidth > rightWidth) { - reporter.reportWarning(connection, - "Source is wider than the destination. Outputs will be lost."); - } else if (leftWidth < rightWidth && !connection.isIterated()) { - reporter.reportWarning(connection, - "Destination is wider than the source. Inputs will be missing."); + List> leftPorts = listPortInstances(connection.getLeftPorts(), connection); + Iterator> srcRanges = leftPorts.iterator(); + List> rightPorts = listPortInstances(connection.getRightPorts(), connection); + Iterator> dstRanges = rightPorts.iterator(); + + // Check for empty lists. + if (!srcRanges.hasNext()) { + if (dstRanges.hasNext()) { + reporter.reportWarning(connection, "No sources to provide inputs."); + } + return; + } else if (!dstRanges.hasNext()) { + reporter.reportWarning(connection, "No destination. Outputs will be lost."); + return; } - - // If any of these ports is a multiport, then things can complicated depending - // on how they overlap. Keep track of how much of the current left and right - // multiports have already been used. - Iterator leftIterator = leftRanges.iterator(); - PortInstance.Range leftRange = leftIterator.next(); - int leftUsedChannels = 0; - for (PortInstance.Range rightRange : rightRanges) { - int rightUsedChannels = 0; - while (rightUsedChannels < rightRange.channelWidth && leftRange != null) { - // Figure out how much of each port we have used (in case it is a multiport). - // This is the minimum of the two remaining widths. - int connectionWidth = leftRange.channelWidth - leftUsedChannels; - if (rightRange.channelWidth - rightUsedChannels < connectionWidth) { - connectionWidth = rightRange.channelWidth - rightUsedChannels; + RuntimeRange src = srcRanges.next(); + RuntimeRange dst = dstRanges.next(); + + while(true) { + if (dst.width == src.width) { + connectPortInstances(src, dst, connection); + if (!dstRanges.hasNext()) { + if (srcRanges.hasNext()) { + // Should not happen (checked by the validator). + reporter.reportWarning(connection, + "Source is wider than the destination. Outputs will be lost."); + } + break; } - connectPortInstances( - connection, - leftRange.getPortInstance(), leftRange.startChannel + leftUsedChannels, - rightRange.getPortInstance(), rightRange.startChannel + rightUsedChannels, - connectionWidth); - leftUsedChannels += connectionWidth; - rightUsedChannels += connectionWidth; - if (leftUsedChannels >= leftRange.channelWidth) { - if (leftIterator.hasNext()) { - leftRange = leftIterator.next(); - } else if (connection.isIterated()) { - leftIterator = leftRanges.iterator(); - leftRange = leftIterator.next(); + if (!srcRanges.hasNext()) { + if (connection.isIterated()) { + srcRanges = leftPorts.iterator(); } else { - leftRange = null; + if (dstRanges.hasNext()) { + // Should not happen (checked by the validator). + reporter.reportWarning(connection, + "Destination is wider than the source. Inputs will be missing."); + } + break; } - leftUsedChannels = 0; } + dst = dstRanges.next(); + src = srcRanges.next(); + } else if (dst.width < src.width) { + // Split the left (src) range in two. + connectPortInstances(src.head(dst.width), dst, connection); + src = src.tail(dst.width); + if (!dstRanges.hasNext()) { + // Should not happen (checked by the validator). + reporter.reportWarning(connection, + "Source is wider than the destination. Outputs will be lost."); + break; + } + dst = dstRanges.next(); + } else if (src.width < dst.width) { + // Split the right (dst) range in two. + connectPortInstances(src, dst.head(src.width), connection); + dst = dst.tail(src.width); + if (!srcRanges.hasNext()) { + if (connection.isIterated()) { + srcRanges = leftPorts.iterator(); + } else { + reporter.reportWarning(connection, + "Destination is wider than the source. Inputs will be missing."); + break; + } + } + src = srcRanges.next(); } } } } + /** + * If path exists from the specified port to any reaction in the specified + * set of reactions, then add the specified port and all ports along the path + * to the specified set of ports. + * @return True if the specified port was added. + */ + private boolean findPaths( + PortInstance port, + Set reactions, + Set ports + ) { + if (ports.contains(port)) return false; + boolean result = false; + for (ReactionInstance d : port.getDependentReactions()) { + if (reactions.contains(d)) ports.add(port); + result = true; + } + // Perform a depth-first search. + for (SendRange r : port.dependentPorts) { + for (RuntimeRange p : r.destinations) { + boolean added = findPaths(p.instance, reactions, ports); + if (added) { + result = true; + ports.add(port); + } + } + } + return result; + } + /** * Given a list of port references, as found on either side of a connection, - * return a list of the port instances referenced. These may be multiports, - * so the returned list includes ranges of channels. - * If the port reference has the form `c.x`, where `c` is a bank of reactors, - * then the list will contain the port instances belonging to each bank member. + * return a list of the port instance ranges referenced. These may be multiports, + * and may be ports of a contained bank (a port representing ports of the bank + * members) so the returned list includes ranges of banks and channels. * - * If a given port reference `b.m`, where `b` is a bank and `m` is a multiport, - * is unqualified, this function iterates over bank members first, then ports. - * E.g., if `b` and `m` have width 2, it returns `[b0.m0, b0.m1, b1.m0, b1.m1]`. + * If a given port reference has the form `interleaved(b.m)`, where `b` is + * a bank and `m` is a multiport, then the corresponding range in the returned + * list is marked interleaved. * - * If a given port reference has the form `interleaved(b.m)`, where `b` is a - * bank and `m` is a multiport, this function iterates over ports first, - * then bank members. E.g., if `b` and `m` have width 2, it returns - * `[b0.m0, b1.m0, b0.m1, b1.m1]`. + * For example, if `b` and `m` have width 2, without the interleaved keyword, + * the returned range represents the sequence `[b0.m0, b0.m1, b1.m0, b1.m1]`. + * With the interleaved marking, the returned range represents the sequence + * `[b0.m0, b1.m0, b0.m1, b1.m1]`. Both ranges will have width 4. + * + * @param references The variable references on one side of the connection. + * @param connection The connection. */ - private List listPortInstances(List references) { - List result = new ArrayList<>(); + private List> listPortInstances( + List references, Connection connection + ) { + List> result = new ArrayList>(); + List> tails = new LinkedList>(); + int count = 0; for (VarRef portRef : references) { // Simple error checking first. if (!(portRef.getVariable() instanceof Port)) { @@ -1255,8 +1002,7 @@ private List listPortInstances(List references) { } // First, figure out which reactor we are dealing with. // The reactor we want is the container of the port. - // If the port reference has no container, then the reactor is this one, - // or if this one is a bank, the next available bank member. + // If the port reference has no container, then the reactor is this one. var reactor = this; if (portRef.getContainer() != null) { reactor = getChildReactorInstance(portRef.getContainer()); @@ -1264,67 +1010,92 @@ private List listPortInstances(List references) { // The reactor can be null only if there is an error in the code. // Skip this portRef so that diagram synthesis can complete. if (reactor != null) { - if (reactor.bankMembers != null) { - // Reactor is a bank. - // Only here does the "interleaved" annotation matter. - if (!portRef.isInterleaved()) { - // Port is not interleaved, so iterate first over bank members, then channels. - for (ReactorInstance memberReactor: reactor.bankMembers) { - PortInstance portInstance = memberReactor.lookupPortInstance( - (Port) portRef.getVariable()); - result.add(portInstance.newRange(0, portInstance.width)); - } - } else { - // Port is interleaved, so iterate first over channels, then bank members. - // Need to return a list of width-one ranges. - // NOTE: Here, we get multiplicative complexity (bank width times port width). - // We assume all ports in each bank have the same width. - // First, get an array of bank members so as to not have to look up each time. - List bankPorts = new ArrayList<>(); - for (ReactorInstance b : reactor.bankMembers) { - bankPorts.add(b.lookupPortInstance((Port) portRef.getVariable())); - } - for (int i = 0; i < bankPorts.get(0).width; i++) { - for (PortInstance p : bankPorts) { - result.add(p.newRange(i, 1)); - } - } + PortInstance portInstance = reactor.lookupPortInstance( + (Port) portRef.getVariable()); + + Set interleaved = new LinkedHashSet(); + if (portRef.isInterleaved()) { + // NOTE: Here, we are assuming that the interleaved() + // keyword is only allowed on the multiports contained by + // contained reactors. + interleaved.add(portInstance.parent); + } + RuntimeRange range = new RuntimeRange.Port( + portInstance, interleaved); + // If this portRef is not the last one in the references list + // then we have to check whether the range can be incremented at + // the lowest two levels (port and container). If not, + // split the range and add the tail to list to iterate over again. + // The reason for this is that the connection has only local visibility, + // but the range width may be reflective of bank structure higher + // in the hierarchy. + if (count < references.size() - 1) { + int portWidth = portInstance.width; + int portParentWidth = portInstance.parent.width; + int widthBound = portWidth * portParentWidth; + + // If either of these widths cannot be determined, assume infinite. + if (portWidth < 0) widthBound = Integer.MAX_VALUE; + if (portParentWidth < 0) widthBound = Integer.MAX_VALUE; + + if (widthBound < range.width) { + // Need to split the range. + tails.add(range.tail(widthBound)); + range = range.head(widthBound); } - } else { - // Reactor is not a bank. - PortInstance portInstance = reactor.lookupPortInstance((Port) portRef.getVariable()); - PortInstance.Range range = portInstance.newRange(0, portInstance.width); - result.add(range); } + result.add(range); } } + // Iterate over the tails. + while(tails.size() > 0) { + List> moreTails = new LinkedList>(); + count = 0; + for (RuntimeRange tail : tails) { + if (count < tails.size() - 1) { + int widthBound = tail.instance.width; + if (tail._interleaved.contains(tail.instance.parent)) { + widthBound = tail.instance.parent.width; + } + // If the width cannot be determined, assume infinite. + if (widthBound < 0) widthBound = Integer.MAX_VALUE; + + if (widthBound < tail.width) { + // Need to split the range again + moreTails.add(tail.tail(widthBound)); + tail = tail.head(widthBound); + } + } + result.add(tail); + } + tails = moreTails; + } return result; } + /** + * If this is a bank of reactors, set the width. + * It will be set to -1 if it cannot be determined. + */ + private void setInitialWidth() { + WidthSpec widthSpec = definition.getWidthSpec(); + if (widthSpec != null) { + // We need the instantiations list of the containing reactor, + // not this one. + width = ASTUtils.width(widthSpec, parent.instantiations()); + } + } + ////////////////////////////////////////////////////// //// Private fields. /** - * A map of connections as they appear in a visualization of the program. - * For each connection, there is map from source ports (single ports and multiports) - * on the left side of the connection to a set of destination ports (single ports - * and multiports) on the right side of the connection. For banks of reactors, - * this includes only the connections to and from the first element of the bank + * Cached set of reactions and ports that form a causality loop. */ - private Map>> connections - = new LinkedHashMap<>(); + private Set> cachedCycles; /** - * Indicator of whether levels have already been assigned. - * This has value 0 if no attempt has been made, 1 if levels have been - * succesfully assigned, and -1 if a causality loop has prevented levels - * from being assigned. - */ - private int levelsAssignedAlready = 0; - - /** - * Cached version of the total number of reactions within - * this reactor and its contained reactors. + * Cached reaction graph containing reactions that form a causality loop. */ - private int totalNumberOfReactionsCache = -1; + private ReactionInstanceGraph cachedReactionLoopGraph = null; } diff --git a/org.lflang/src/org/lflang/generator/RuntimeRange.java b/org.lflang/src/org/lflang/generator/RuntimeRange.java new file mode 100644 index 0000000000..d865cc9680 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/RuntimeRange.java @@ -0,0 +1,517 @@ +/* A representation of a range of runtime instances for a NamedInstance. */ + +/* +Copyright (c) 2019-2021, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.lflang.generator; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Class representing a range of runtime instance objects + * (port channels, reactors, reactions, etc.). This class and its derived classes + * have the most detailed information about the structure of a Lingua Franca + * program. There are three levels of detail: + * + * * The abstract syntax tree (AST). + * * The compile-time instance graph (CIG). + * * The runtime instance graph (RIG). + * + * In the AST, each reactor class is represented once. + * In the CIG, each reactor class is represented as many times as it is + * instantiated, except that a bank has only one representation (as + * in the graphical rendition). Equivalently, each CIG node has a unique + * full name, even though it may represent many runtime instances. + * The CIG is represented by + * {@link NamedInstance} and its derived classes. + * In the RIG, each bank is expanded so each bank member and + * each port channel is represented. + * + * In general, determining dependencies between reactions requires analysis + * at the level of the RIG. But a brute-force representation of the RIG + * can get very large, and for most programs it has a great deal of + * redundancy. In a fully detailed representation of the RIG, for example, + * a bank of width N that contains a bank of width M within which there + * is a reactor R with port P will have N*M runtime instances of the port. + * If the port is a multiport with width L, then there are N*M*L + * edges connected to instances of that port, each of which may go + * to a distinct set of other remote runtime port instances. + * + * This class and its subclasses give a more compact representation of the + * RIG in most common cases where collections of runtime instances all have + * the same dependencies or dependencies form a range that can be represented + * compactly. + * + * A RuntimeRange represents an adjacent set of RIG objects (port channels, reactions + * reactors). For example, it can represent port channels 2 through 5 in a multiport + * of width 10. The width in this case is 4. If such a port is + * contained by one or more banks of reactors, then channels 2 through 5 + * of one bank member form a contiguous range. If you want channels 2 through 5 + * of all bank members, then this needs to be represented with multiple ranges. + * + * The maxWidth is the width of the instance multiplied by the widths of + * each of its containers. For example, if a port of width 4 is contained by + * a bank of width 2 that is then contained by a bank of width 3, then + * the maxWidth will be 2*3*4 = 24. + * + * The function iterationOrder returns a list that includes the instance + * of this range and all its containers, except the top-level reactor (main + * or federated). The order of this list is the order in which an + * iteration over the RIG objects represented by this range should be + * iterated. If the instance is a PortInstance, then this order will + * depend on whether connections at any level of the hierarchy are + * interleaved. + * + * The simplest Ranges are those where the corresponding CIG node represents + * only one runtime instance (its instance is not (deeply) within a bank + * and is not a multiport). In this case, the RuntimeRange and all the objects + * returned by iterationOrder will have width 1. + * + * In a more complex instance, consider a bank A of width 2 that contains a + * bank B of width 2 that contains a port instance P with width 2. . + * There are a total of 8 instances of P, which we can name: + * + * A0.B0.P0 + * A0.B0.P1 + * A0.B1.P0 + * A0.B1.P1 + * A1.B0.P0 + * A1.B0.P1 + * A1.B1.P0 + * A1.B1.P1 + * + * If there is no interleaving, iterationOrder() returns [P, B, A], + * indicating that they should be iterated by incrementing the index of P + * first, then the index of B, then the index of A, as done above. + * + * If the connection within B to port P is interleaved, then the order + * of iteration order will be [B, P, A], resulting in the list: + * + * A0.B0.P0 + * A0.B1.P0 + * A0.B0.P1 + * A0.B1.P1 + * A1.B0.P0 + * A1.B1.P0 + * A1.B0.P1 + * A1.B1.P1 + * + * If the connection within A to B is also interleaved, then the order + * will be [A, B, P], resulting in the list: + * + * A0.B0.P0 + * A1.B0.P0 + * A0.B1.P0 + * A1.B1.P0 + * A0.B0.P1 + * A1.B0.P1 + * A0.B1.P1 + * A1.B1.P1 + * + * Finally, if the connection within A to B is interleaved, but not the + * connection within B to P, then the order will be [A, P, B], resulting in + * the list: + * + * A0.B0.P0 + * A1.B0.P0 + * A0.B0.P1 + * A1.B0.P1 + * A0.B1.P0 + * A1.B1.P0 + * A0.B1.P1 + * A1.B1.P1 + * + * A RuntimeRange is a contiguous subset of one of the above lists, given by + * a start offset and a width that is less than or equal to maxWidth. + * + * Each element of the above lists can be represented as a permuted mixed-radix (PMR) number, + * where the low-order digit has radix equal to the width of P, the second digit + * has radix equal to the width of B, and the final digit has radix equal to the + * width of A. Each PMR has a permutation vector that defines how to increment + * PMR number. This permutation vector is derived from the iteration order as + * follows. When there is no interleaving, the iteration order is [P, B, A], + * and the permutation vector is [0, 1, 2]. When there is interleaving, the permutation + * vector simply specifies the iteration order. For example, if the iteration order + * is [A, P, B], then the permutation vector is [2, 0, 1], indicating that digit 2 + * of the PMR (corresponding to A) should be incremented first, then digit 0 (for P), + * then digit 1 (for B). + * + * For a RuntimeRange with width greater than 1, + * the head() and tail() functions split the range. + * + * This class and subclasses are designed to be immutable. + * Modifications always return a new RuntimeRange. + * + * @author{Edward A. Lee } + */ +public class RuntimeRange> implements Comparable> { + + /** + * Create a new range representing the full width of the specified instance + * with no interleaving. The instances will be a list with the specified instance + * first, its parent next, and on up the hierarchy until the depth 1 parent (the + * top-level reactor is not included because it can never be a bank). + * @param instance The instance. + * @param interleaved A list of parents that are interleaved or null if none. + */ + public RuntimeRange( + T instance, + Set interleaved + ) { + this(instance, 0, 0, interleaved); + } + + /** + * Create a new range representing a range of the specified instance + * with no interleaving. The instances will be a list with the specified instance + * first, its parent next, and on up the hierarchy until the depth 1 parent (the + * top-level reactor is not included because it can never be a bank). + * @param instance The instance over which this is a range (port, reaction, etc.) + * @param start The starting index for the range. + * @param width The width of the range or 0 to specify the maximum possible width. + * @param interleaved A list of parents that are interleaved or null if none. + */ + public RuntimeRange( + T instance, + int start, + int width, + Set interleaved + ) { + this.instance = instance; + this.start = start; + if (interleaved != null) { + this._interleaved.addAll(interleaved); + } + + int maxWidth = instance.width; // Initial value. + NamedInstance parent = instance.parent; + while (parent.depth > 0) { + maxWidth *= parent.width; + parent = parent.parent; + } + this.maxWidth = maxWidth; + + if (width > 0 && width + start < maxWidth) { + this.width = width; + } else { + this.width = maxWidth - start; + } + } + + ////////////////////////////////////////////////////////// + //// Public variables + + /** The instance that this is a range of. */ + public final T instance; + + /** The start offset of this range. */ + public final int start; + + /** The maximum width of any range with this instance. */ + public final int maxWidth; + + /** The width of this range. */ + public final int width; + + ////////////////////////////////////////////////////////// + //// Public methods + + /** + * Compare ranges by first comparing their start offset, and then, + * if these are equal, comparing their widths. This comparison is + * meaningful only for ranges that have the same instances. + * Note that this can return 0 even if equals() does not return true. + */ + @Override + public int compareTo(RuntimeRange o) { + if (start < o.start) { + return -1; + } else if (start == o.start) { + if (width < o.width) { + return -1; + } else if (width == o.width) { + return 0; + } else { + return 1; + } + } else { + return 1; + } + } + + /** + * Return a new RuntimeRange that is identical to this range but + * with width reduced to the specified width. + * If the new width is greater than or equal to the width + * of this range, then return this range. + * If the newWidth is 0 or negative, return null. + * @param newWidth The new width. + */ + public RuntimeRange head(int newWidth) { + if (newWidth >= width) return this; + if (newWidth <= 0) return null; + return new RuntimeRange(instance, start, newWidth, _interleaved); + } + + /** + * Return the list of **natural identifiers** for the runtime instances + * in this range. Each returned identifier is an integer representation + * of the mixed-radix number [d0, ... , dn] with radices [w0, ... , wn], + * where d0 is the bank or channel index of this RuntimeRange's instance, which + * has width w0, and dn is the bank index of its topmost parent below the + * top-level (main) reactor, which has width wn. The depth of this RuntimeRange's + * instance, therefore, is n - 1. The order of the returned list is the order + * in which the runtime instances should be iterated. + */ + public List instances() { + List result = new ArrayList(width); + MixedRadixInt mr = startMR(); + int count = 0; + while (count++ < width) { + result.add(mr.get()); + mr.increment(); + } + return result; + } + + /** + * Return a list containing the instance for this range and all of its + * parents, not including the top level reactor, in the order in which + * their banks and multiport channels should be iterated. + * For each depth at which the connection is interleaved, that parent + * will appear before this instance in depth order (shallower to deeper). + * For each depth at which the connection is not interleaved, that parent + * will appear after this instance in reverse depth order (deeper to + * shallower). + */ + public List> iterationOrder() { + ArrayList> result = new ArrayList>(); + result.add(instance); + ReactorInstance parent = instance.parent; + while (parent.depth > 0) { + if (_interleaved.contains(parent)) { + // Put the parent at the head of the list. + result.add(0, parent); + } else { + result.add(parent); + } + parent = parent.parent; + } + return result; + } + + /** + * Return a range that is the subset of this range that overlaps with the + * specified range or null if there is no overlap. + */ + public RuntimeRange overlap(RuntimeRange range) { + if (!overlaps(range)) return null; + int newStart = Math.max(start, range.start); + int newEnd = Math.min(start + width, range.start + range.width); + return tail(newStart - start).head(newEnd - newStart); + } + + /** + * Return true if the specified range has the same instance as this range + * and the ranges overlap. + */ + public boolean overlaps(RuntimeRange range) { + if (!instance.equals(range.instance)) return false; + return (start < range.start + range.width && start + width > range.start); + } + + /** + * Return a set of identifiers for runtime instances of a parent of this + * RuntimeRange's instance n levels above this RuntimeRange's instance. If n == 1, for + * example, then this return the identifiers for the parent ReactorInstance. + * + * This returns a list of **natural identifiers**, + * as defined below, for the instances within the range. + * + * The resulting list can be used to count the number of distinct + * runtime instances of this RuntimeRange's instance (using n == 0) or any of its parents that + * lie within the range and to provide an index into an array of runtime + * instances. + * + * Each **natural identifier** is the integer value of a mixed-radix number + * defined as follows: + * + * * The low-order digit is the index of the runtime instance of i + * within its container. If the NamedInstance + * is a PortInstance, this will be the multiport channel or 0 if it is not a + * multiport. If the NamedInstance is a ReactorInstance, then it will be the bank + * index or 0 if the reactor is not a bank. The radix for this digit will be + * the multiport width or bank width or 1 if the NamedInstance is neither a + * multiport nor a bank. + * + * * The next digit will be the bank index of the container of the specified + * NamedInstance or 0 if it is not a bank. + * + * * The remaining digits will be bank indices of containers up to but not + * including the top-level reactor (there is no point in including the top-level + * reactor because it is never a bank. + * + * Each index that is returned can be used as an index into an array of + * runtime instances that is assumed to be in a **natural order**. + * + * @param n The number of levels up of the parent. This is required to be + * less than the depth of this RuntimeRange's instance or an exception will be thrown. + */ + public Set parentInstances(int n) { + Set result = new LinkedHashSet(width); + MixedRadixInt mr = startMR(); + int count = 0; + while (count++ < width) { + result.add(mr.get(n)); + mr.increment(); + } + return result; + } + + /** + * Return the nearest containing ReactorInstance for this instance. + * If this instance is a ReactorInstance, then return it. + * Otherwise, return its parent. + */ + public ReactorInstance parentReactor() { + if (instance instanceof ReactorInstance) { + return (ReactorInstance)instance; + } else { + return instance.getParent(); + } + } + + /** + * Return the permutation vector that indicates the order in which the digits + * of the permuted mixed-radix representations of indices in this range should + * be incremented. + */ + public List permutation() { + List result = new ArrayList(instance.depth); + // Populate the result with default zeros. + for (int i = 0; i < instance.depth; i++) { + result.add(0); + } + int count = 0; + for (NamedInstance i : iterationOrder()) { + result.set(count++, instance.depth - i.depth); + } + return result; + } + + /** + * Return the radixes vector containing the width of this instance followed + * by the widths of its containers, not including the top level, which always + * has radix 1 and value 0. + */ + public List radixes() { + List result = new ArrayList(instance.depth); + int width = instance.width; + // If the width cannot be determined, assume 1. + if (width < 0) width = 1; + result.add(width); + ReactorInstance p = instance.getParent(); + while (p != null && p.getDepth() > 0) { + width = p.getWidth(); + // If the width cannot be determined, assume 1. + if (width < 0) width = 1; + result.add(width); + p = p.getParent(); + } + return result; + } + + /** + * Return the start as a new permuted mixed-radix number. + * For any instance that is neither a multiport nor a bank, the + * corresponding digit will be 0. + */ + public MixedRadixInt startMR() { + MixedRadixInt result = new MixedRadixInt(null, radixes(), permutation()); + result.setMagnitude(start); + return result; + } + + /** + * Return a new range that represents the leftover elements + * starting at the specified offset relative to start. + * If start + offset is greater than or equal to the width, then this returns null. + * If this offset is 0 then this returns this range unmodified. + * @param offset The number of elements to consume. + */ + public RuntimeRange tail(int offset) { + if (offset == 0) return this; + if (offset >= width) return null; + return new RuntimeRange(instance, start + offset, width - offset, _interleaved); + } + + /** + * Toggle the interleaved status of the specified reactor, which is assumed + * to be a parent of the instance of this range. + * If it was previously interleaved, make it not interleaved + * and vice versa. This returns a new RuntimeRange. + * @param reactor The parent reactor at which to toggle interleaving. + */ + public RuntimeRange toggleInterleaved(ReactorInstance reactor) { + Set newInterleaved = new HashSet(_interleaved); + if (_interleaved.contains(reactor)) { + newInterleaved.remove(reactor); + } else { + newInterleaved.add(reactor); + } + return new RuntimeRange(instance, start, width, newInterleaved); + } + + @Override + public String toString() { + return instance.getFullName() + "(" + start + "," + width + ")"; + } + + ////////////////////////////////////////////////////////// + //// Protected variables + + /** Record of which levels are interleaved. */ + Set _interleaved = new HashSet(); + + ////////////////////////////////////////////////////////// + //// Public inner classes + + /** + * Special case of RuntimeRange for PortInstance. + */ + public static class Port extends RuntimeRange { + public Port(PortInstance instance) { + super(instance, null); + } + public Port(PortInstance instance, Set interleaved) { + super(instance, interleaved); + } + public Port(PortInstance instance, int start, int width, Set interleaved) { + super(instance, start, width, interleaved); + } + } +} \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/SendRange.java b/org.lflang/src/org/lflang/generator/SendRange.java new file mode 100644 index 0000000000..62cc418079 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/SendRange.java @@ -0,0 +1,318 @@ +/* Abstract class for ranges of NamedInstance. */ + +/* +Copyright (c) 2019-2021, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.lflang.generator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.lflang.lf.Connection; + +/** + * Class representing a range of a port that sources data + * together with a list of destination ranges of other ports that should all + * receive the same data sent in this range. + * All ranges in the destinations list have widths that are an integer + * multiple N of this range but not necessarily the same start offsets. + * + * This class and subclasses are designed to be immutable. + * Modifications always return a new RuntimeRange. + * + * @author{Edward A. Lee } +*/ +public class SendRange extends RuntimeRange.Port { + + /** + * Create a new send range. + * @param instance The instance over which this is a range of. + * @param start The starting index. + * @param width The width. + * @param interleaved A list of parents that are interleaved or null if none. + * @param connection The connection that establishes this send or null if not unique or none. + */ + public SendRange( + PortInstance instance, + int start, + int width, + Set interleaved, + Connection connection + ) { + super(instance, start, width, interleaved); + this.connection = connection; + } + + /** + * Create a new send range representing sending from the specified + * src to the specified dst. This preserves the interleaved status + * of both the src and dst. + * @param src The source range. + * @param dst The destination range. + * @param interleaved A list of parents that are interleaved or null if none. + * @param connection The connection that establishes this send or null if not unique or none. + */ + public SendRange( + RuntimeRange src, + RuntimeRange dst, + Set interleaved, + Connection connection + ) { + super(src.instance, src.start, src.width, interleaved); + destinations.add(dst); + _interleaved.addAll(src._interleaved); + this.connection = connection; + } + + ////////////////////////////////////////////////////////// + //// Public variables + + /** The connection that establishes this relationship or null if not unique or none. */ + public final Connection connection; + + /** The list of destination ranges to which this broadcasts. */ + public final List> destinations + = new ArrayList>(); + + ////////////////////////////////////////////////////////// + //// Public methods + + /** + * Add a destination to the list of destinations of this range. + * If the width of the destination is not a multiple of the width + * of this range, throw an exception. + * @throws IllegalArgumentException If the width doesn't match. + */ + public void addDestination(RuntimeRange dst) { + if (dst.width % width != 0) { + throw new IllegalArgumentException( + "Destination range width is not a multiple of sender's width"); + } + destinations.add(dst); + // Void any precomputed number of destinations. + _numberOfDestinationReactors = -1; + } + + /** + * Override the base class to add additional comparisons so that + * ordering is never ambiguous. This means that sorting will be deterministic. + * Note that this can return 0 even if equals() does not return true. + */ + @Override + public int compareTo(RuntimeRange o) { + int result = super.compareTo(o); + if (result == 0) { + // Longer destination lists come first. + if (destinations.size() > ((SendRange)o).destinations.size()) { + return -1; + } else if (destinations.size() == ((SendRange)o).destinations.size()) { + return instance.getFullName().compareTo(o.instance.getFullName()); + } else { + return 1; + } + } + return result; + } + + /** + * Return the total number of destination reactors for this range. + * Specifically, this is the number of distinct runtime reactor instances + * that react to messages from this send range. + */ + public int getNumberOfDestinationReactors() { + if (_numberOfDestinationReactors < 0) { + // Has not been calculated before. Calculate now. + _numberOfDestinationReactors = 0; + Map> result = new HashMap>(); + for (RuntimeRange destination : this.destinations) { + // The following set contains unique identifiers the parent reactors + // of destination ports. + Set parentIDs = destination.parentInstances(1); + Set previousParentIDs = result.get(destination.instance.parent); + if (previousParentIDs == null) { + result.put(destination.instance.parent, parentIDs); + } else { + previousParentIDs.addAll(parentIDs); + } + } + for (ReactorInstance parent : result.keySet()) { + _numberOfDestinationReactors += result.get(parent).size(); + } + } + return _numberOfDestinationReactors; + } + + /** + * Return a new SendRange that is identical to this range but + * with width reduced to the specified width. + * If the new width is greater than or equal to the width + * of this range, then return this range. + * If the newWidth is 0 or negative, return null. + * This overrides the base class to also apply head() + * to the destination list. + * @param newWidth The new width. + */ + @Override + public SendRange head(int newWidth) { + // NOTE: Cannot use the superclass because it returns a RuntimeRange, not a SendRange. + // Also, cannot return this without applying head() to the destinations. + if (newWidth <= 0) return null; + + SendRange result = new SendRange(instance, start, newWidth, _interleaved, connection); + + for (RuntimeRange destination : destinations) { + result.destinations.add(destination.head(newWidth)); + } + return result; + } + + /** + * Return a range that is the subset of this range that overlaps with the + * specified range or null if there is no overlap. + */ + @Override + public SendRange overlap(RuntimeRange range) { + if (!overlaps(range)) return null; + if (range.start == start && range.width == width) return this; + int newStart = Math.max(start, range.start); + int newEnd = Math.min(start + width, range.start + range.width); + int newWidth = newEnd - newStart; + SendRange result = new SendRange(instance, newStart, newWidth, _interleaved, connection); + result._interleaved.addAll(_interleaved); + for (RuntimeRange destination : destinations) { + // The destination width is a multiple of this range's width. + // If the multiple is greater than 1, then the destination needs to + // split into multiple destinations. + while (destination != null) { + int dstStart = destination.start + (newStart - start); + RuntimeRange.Port dst = new RuntimeRange.Port( + destination.instance, + dstStart, + newWidth, + destination._interleaved + ); + result.addDestination(dst); + destination = destination.tail(width); + } + } + return result; + } + + /** + * Return a new SendRange that represents the leftover elements + * starting at the specified offset. If the offset is greater + * than or equal to the width, then this returns null. + * If this offset is 0 then this returns this range unmodified. + * This overrides the base class to also apply tail() + * to the destination list. + * @param offset The number of elements to consume. + */ + @Override + public SendRange tail(int offset) { + // NOTE: Cannot use the superclass because it returns a RuntimeRange, not a SendRange. + // Also, cannot return this without applying tail() to the destinations. + if (offset >= width) return null; + SendRange result = new SendRange( + instance, start + offset, width - offset, _interleaved, connection); + + for (RuntimeRange destination : destinations) { + result.destinations.add(destination.tail(offset)); + } + return result; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(super.toString()); + result.append("->["); + List dsts = new LinkedList(); + for (RuntimeRange dst : destinations) { + dsts.add(dst.toString()); + } + result.append(String.join(", ", dsts)); + result.append("]"); + return result.toString(); + } + + ////////////////////////////////////////////////////////// + //// Protected methods + + /** + * Assuming that this SendRange is completely contained by one + * of the destinations of the specified srcRange, return a new SendRange + * where the send range is the subrange of the specified srcRange that + * overlaps with this SendRange and the destinations include all the + * destinations of this SendRange. If the assumption is not satisfied, + * throw an IllegalArgumentException. + * + * If any parent of this range is marked interleaved and is also a parent of the + * specified srcRange, then that parent will be marked interleaved + * in the result. + * + * @param srcRange A new source range. + * @param srcRangeOffset An offset into the source range. + */ + protected SendRange newSendRange(SendRange srcRange, int srcRangeOffset) { + // Every destination of srcRange receives all channels of srcRange (multicast). + // Find which multicast destination overlaps with this srcRange. + for (RuntimeRange srcDestination : srcRange.destinations) { + RuntimeRange overlap = srcDestination.overlap(this); + if (overlap == null) continue; // Not this one. + + if (overlap.width == width) { + // Found an overlap that is completely contained. + // If this width is greater than the srcRange width, + // then assume srcRange is multicasting via this. + int newWidth = Math.min(width, srcRange.width); + // The interleaving of the result is the union of the two interleavings. + Set interleaving = new LinkedHashSet(); + interleaving.addAll(_interleaved); + interleaving.addAll(srcRange._interleaved); + SendRange result = new SendRange( + srcRange.instance, + srcRange.start + srcRangeOffset, + newWidth, + interleaving, + connection); + for (RuntimeRange dst : destinations) { + result.addDestination(dst); + } + return result; + } + } + throw new IllegalArgumentException( + "Expected this SendRange " + this.toString() + + " to be completely contained by a destination of " + srcRange.toString()); + } + + ////////////////////////////////////////////////////////// + //// Private variables + + private int _numberOfDestinationReactors = -1; // Never access this directly. +} diff --git a/org.lflang/src/org/lflang/generator/TargetTypes.java b/org.lflang/src/org/lflang/generator/TargetTypes.java index 820d452bb8..d52c6341c5 100644 --- a/org.lflang/src/org/lflang/generator/TargetTypes.java +++ b/org.lflang/src/org/lflang/generator/TargetTypes.java @@ -9,6 +9,10 @@ import org.lflang.JavaAstUtils; import org.lflang.TimeUnit; import org.lflang.TimeValue; +import org.lflang.lf.Action; +import org.lflang.lf.Parameter; +import org.lflang.lf.Port; +import org.lflang.lf.StateVar; import org.lflang.lf.Time; import org.lflang.lf.Type; import org.lflang.lf.Value; @@ -160,6 +164,38 @@ default String getTargetType(InferredType type) { return type.toText(); } + /** + * Return a string representing the type of the given + * parameter. + */ + default String getTargetType(Parameter p) { + return getTargetType(JavaAstUtils.getInferredType(p)); + } + + /** + * Return a string representing the type of the given + * state variable. + */ + default String getTargetType(StateVar s) { + return getTargetType(JavaAstUtils.getInferredType(s)); + } + + /** + * Return a string representing the type of the given + * action. + */ + default String getTargetType(Action a) { + return getTargetType(JavaAstUtils.getInferredType(a)); + } + + /** + * Return a string representing the type of the given + * port. + */ + default String getTargetType(Port p) { + return getTargetType(JavaAstUtils.getInferredType(p)); + } + /** * Returns the representation of the given initializer * expression in target code. The given type, if non-null, diff --git a/org.lflang/src/org/lflang/generator/TriggerInstance.java b/org.lflang/src/org/lflang/generator/TriggerInstance.java index 29e27fae63..21b02527ac 100644 --- a/org.lflang/src/org/lflang/generator/TriggerInstance.java +++ b/org.lflang/src/org/lflang/generator/TriggerInstance.java @@ -88,12 +88,23 @@ public BuiltinTrigger getBuiltinTriggerType() { return builtinTriggerType; } - /** Return the reaction instances that are triggered by this trigger. */ + /** + * Return the reaction instances that are triggered or read by this trigger. + * If this port is an output, then the reaction instances + * belong to the parent of the port's parent. If the port + * is an input, then the reaction instances belong to the + * port's parent. + */ public Set getDependentReactions() { return dependentReactions; } - /** Reaction instances that may send outputs via this port. */ + /** + * Return the reaction instances that may send data via this port. + * If this port is an input, then the reaction instance + * belongs to parent of the port's parent. If it is an output, + * the the reaction instance belongs to the port's parent. + */ public Set getDependsOnReactions() { return dependsOnReactions; }; @@ -128,23 +139,26 @@ public boolean isBuiltinTrigger() { return builtinTriggerType != null; } - /** - * {@inheritDoc} - */ - @Override - public ReactorInstance root() { - return parent.root(); - } - ///////////////////////////////////////////// //// Protected Fields BuiltinTrigger builtinTriggerType = null; - /** Reaction instances that are triggered by this trigger. */ + /** + * Reaction instances that are triggered or read by this trigger. + * If this port is an output, then the reaction instances + * belong to the parent of the port's parent. If the port + * is an input, then the reaction instances belong to the + * port's parent. + */ Set dependentReactions = new LinkedHashSet(); - /** Reaction instances that may send outputs via this port. */ + /** + * Reaction instances that may send data via this port. + * If this port is an input, then the reaction instance + * belongs to parent of the port's parent. If it is an output, + * the the reaction instance belongs to the port's parent. + */ Set dependsOnReactions = new LinkedHashSet(); ///////////////////////////////////////////// diff --git a/org.lflang/src/org/lflang/generator/Validator.java b/org.lflang/src/org/lflang/generator/Validator.java index 718ee2a5d6..6106b13559 100644 --- a/org.lflang/src/org/lflang/generator/Validator.java +++ b/org.lflang/src/org/lflang/generator/Validator.java @@ -3,6 +3,7 @@ import org.eclipse.xtext.util.CancelIndicator; import org.lflang.ErrorReporter; +import org.lflang.TargetConfig.Mode; import org.lflang.util.LFCommand; import java.nio.file.Path; import java.util.ArrayList; @@ -13,6 +14,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.stream.Collectors; @@ -49,13 +51,13 @@ protected Validator(ErrorReporter errorReporter, Map codeMaps) { /** * Validate this Validator's group of generated files. - * @param cancelIndicator The cancel indicator for the - * current operation. + * @param context The context of the current build. */ - public final void doValidate(CancelIndicator cancelIndicator) throws ExecutionException, InterruptedException { + public final void doValidate(LFGeneratorContext context) throws ExecutionException, InterruptedException { + if (!validationEnabled(context)) return; final List>> tasks = getValidationStrategies().stream().map( it -> (Callable>) () -> { - it.second.run(cancelIndicator, true); + it.second.run(context.getCancelIndicator(), true); return it; } ).collect(Collectors.toList()); @@ -65,6 +67,23 @@ public final void doValidate(CancelIndicator cancelIndicator) throws ExecutionEx } } + /** + * Return whether generated code validation is enabled for this build. + * @param context The context of the current build. + */ + private boolean validationEnabled(LFGeneratorContext context) { + return context.getArgs().containsKey("lint") || validationEnabledByDefault(context); + } + + /** + * Return whether validation of generated code is enabled by default. + * @param context The context of the current build. + * @return Whether validation of generated code is enabled by default. + */ + protected boolean validationEnabledByDefault(LFGeneratorContext context) { + return context.getMode() != Mode.STANDALONE; + } + /** * Invoke all the given tasks. * @param tasks Any set of tasks. @@ -85,9 +104,11 @@ private static List> getFutures(List> tasks) throws In } break; default: - futures = Executors.newFixedThreadPool( + ExecutorService service = Executors.newFixedThreadPool( Math.min(Runtime.getRuntime().availableProcessors(), tasks.size()) - ).invokeAll(tasks); + ); + futures = service.invokeAll(tasks); + service.shutdown(); } return futures; } diff --git a/org.lflang/src/org/lflang/generator/ValueGenerator.java b/org.lflang/src/org/lflang/generator/ValueGenerator.java new file mode 100644 index 0000000000..190777f764 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/ValueGenerator.java @@ -0,0 +1,175 @@ +package org.lflang.generator; + +import java.util.List; +import java.util.ArrayList; +import java.util.stream.Collectors; + +import org.lflang.ASTUtils; +import org.lflang.JavaAstUtils; +import org.lflang.TimeValue; +import org.lflang.lf.Assignment; +import org.lflang.lf.Delay; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Parameter; +import org.lflang.lf.StateVar; +import org.lflang.lf.Time; +import org.lflang.TimeUnit; +import org.lflang.lf.Value; + +/** + * Encapsulates logic for representing {@code Value}s in a + * target language. + */ +public final class ValueGenerator { + + /** + * A {@code TimeInTargetLanguage} is a + * target-language-specific time representation + * strategy. + */ + public interface TimeInTargetLanguage { + String apply(TimeValue t); + } + + /** + * A {@code GetTargetReference} instance is a + * target-language-specific function. It provides the + * target language code that refers to the given + * parameter {@code param}. + */ + public interface GetTargetReference { + String apply(Parameter param); + } + + private final TimeInTargetLanguage timeInTargetLanguage; + private final GetTargetReference getTargetReference; + + /** + * Instantiates a target-language-specific + * ValueGenerator parameterized by {@code f}. + * @param f a time representation strategy + */ + public ValueGenerator(TimeInTargetLanguage f, GetTargetReference g) { + this.timeInTargetLanguage = f; + this.getTargetReference = g; + } + + /** + * Create a list of state initializers in target code. + * + * @param state The state variable to create initializers for + * @return A list of initializers in target code + */ + public List getInitializerList(StateVar state) { + List list = new ArrayList<>(); + // FIXME: Previously, we returned null if it was not initialized, which would have caused an + // NPE in TSStateGenerator. Is this the desired behavior? + if (!ASTUtils.isInitialized(state)) return list; + for (Value v : state.getInit()) { + if (v.getParameter() != null) { + list.add(getTargetReference.apply(v.getParameter())); + } else { + list.add(getTargetValue(v, JavaAstUtils.isOfTimeType(state))); + } + } + return list; + } + + /** + * Create a list of default parameter initializers in target code. + * + * @param param The parameter to create initializers for + * @return A list of initializers in target code + */ + public List getInitializerList(Parameter param) { + List list = new ArrayList<>(); + if (param == null) return list; + for (Value v : param.getInit()) + list.add(getTargetValue(v, JavaAstUtils.isOfTimeType(param))); + return list; + } + + /** + * Create a list of parameter initializers in target code in the context + * of an reactor instantiation. + * + * This respects the parameter assignments given in the reactor + * instantiation and falls back to the reactors default initializers + * if no value is assigned to it. + * + * @param param The parameter to create initializers for + * @return A list of initializers in target code + */ + public List getInitializerList(Parameter param, Instantiation i) { + List assignments = i.getParameters().stream() + .filter(it -> it.getLhs() == param) + .collect(Collectors.toList()); + if (assignments.isEmpty()) // Case 0: The parameter was not overwritten in the instantiation + return getInitializerList(param); + // Case 1: The parameter was overwritten in the instantiation + List list = new ArrayList<>(); + if (assignments.get(0) == null) return list; + for (Value init : assignments.get(0).getRhs()) + list.add(getTargetValue(init, JavaAstUtils.isOfTimeType(param))); + return list; + } + + /** + * Return the time specified by {@code t}, expressed as + * code that is valid for some target languages. + */ + public String getTargetTime(TimeValue t) { + return timeInTargetLanguage.apply(t); + } + + /** + * Return the time specified by {@code t}, expressed as + * code that is valid for some target languages. + */ + public String getTargetTime(Time t) { + return timeInTargetLanguage.apply(new TimeValue(t.getInterval(), TimeUnit.fromName(t.getUnit()))); + } + + /** + * Return the time specified by {@code d}, expressed as + * code that is valid for some target languages. + */ + public String getTargetTime(Delay d) { + return d.getParameter() != null ? ASTUtils.toText(d) : timeInTargetLanguage.apply( + JavaAstUtils.toTimeValue(d.getTime()) // The time is given as a parameter reference. + ); + } + + /** + * Return the time specified by {@code v}, expressed as + * code that is valid for some target languages. + */ + public String getTargetTime(Value v) { + return getTargetValue(v, true); + } + + /** + * Get textual representation of a value in the target language. + * + * If the value evaluates to 0, it is interpreted as a normal value. + * + * @param v A time AST node + * @return A time string in the target language + */ + public String getTargetValue(Value v) { + return ASTUtils.toText(v); + } + + /** + * Get textual representation of a value in the target language. + * + * @param v A time AST node + * @param isTime Whether {@code v} is expected to be a time + * @return A time string in the target language + */ + public String getTargetValue(Value v, boolean isTime) { + if (v.getTime() != null) return getTargetTime(v.getTime()); + if (isTime && ASTUtils.isZero(v)) return timeInTargetLanguage.apply(TimeValue.ZERO); + return ASTUtils.toText(v); + } +} diff --git a/org.lflang/src/org/lflang/generator/c/CCompiler.java b/org.lflang/src/org/lflang/generator/c/CCompiler.java index fa7956d945..66bab32aea 100644 --- a/org.lflang/src/org/lflang/generator/c/CCompiler.java +++ b/org.lflang/src/org/lflang/generator/c/CCompiler.java @@ -196,10 +196,8 @@ public LFCommand compileCCommand( compileArgs.add("-DNUMBER_OF_WORKERS="+targetConfig.threads); } - // Finally add the compiler flags in target parameters (if any) - if (!targetConfig.compilerFlags.isEmpty()) { - compileArgs.addAll(targetConfig.compilerFlags); - } + // Finally, add the compiler flags in target parameters (if any) + compileArgs.addAll(targetConfig.compilerFlags); // Only set the output file name if it hasn't already been set // using a target property or Args line flag. diff --git a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend index 8926f7e013..2b17a30c48 100644 --- a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend +++ b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend @@ -46,9 +46,10 @@ import org.lflang.ASTUtils import org.lflang.ErrorReporter import org.lflang.FileConfig import org.lflang.InferredType +import org.lflang.JavaAstUtils +import org.lflang.Target import org.lflang.TargetConfig import org.lflang.TargetConfig.Mode -import org.lflang.Target import org.lflang.TargetProperty import org.lflang.TargetProperty.ClockSyncMode import org.lflang.TargetProperty.CoordinationType @@ -71,10 +72,13 @@ import org.lflang.generator.ParameterInstance import org.lflang.generator.PortInstance import org.lflang.generator.ReactionInstance import org.lflang.generator.ReactorInstance +import org.lflang.generator.RuntimeRange +import org.lflang.generator.SendRange import org.lflang.generator.TimerInstance import org.lflang.generator.TriggerInstance import org.lflang.lf.Action import org.lflang.lf.ActionOrigin +import org.lflang.lf.Assignment import org.lflang.lf.Code import org.lflang.lf.Delay import org.lflang.lf.Input @@ -87,7 +91,6 @@ import org.lflang.lf.Reactor import org.lflang.lf.ReactorDecl import org.lflang.lf.StateVar import org.lflang.lf.TriggerRef -import org.lflang.lf.TypedVariable import org.lflang.lf.VarRef import org.lflang.lf.Variable import org.lflang.util.XtendUtil @@ -268,7 +271,7 @@ import static extension org.lflang.JavaAstUtils.* * `out->is_present` field of the output reactor's self struct. * * In addition, the `reaction_i` struct on the self struct has a `triggers` - * field that records all the trigger_t structs for ports and reactions + * field that records all the trigger_t structs for ports and actions * that are triggered by the i-th reaction. The triggers field is * an array of arrays of pointers to trigger_t structs. * The length of the outer array is the number of output channels @@ -331,9 +334,6 @@ class CGenerator extends GeneratorBase { // Place to collect code to initialize the trigger objects for all reactor instances. protected var initializeTriggerObjects = new StringBuilder() - // Place to collect code to go at the end of the _lf_initialize_trigger_objects() function. - var initializeTriggerObjectsEnd = new StringBuilder() - // The command to run the generated code if specified in the target directive. var runCommand = new ArrayList() @@ -362,13 +362,20 @@ class CGenerator extends GeneratorBase { // Indicate whether the generator is in Cpp mode or not var boolean CCppMode = false; + var CTypes types; + + protected new(FileConfig fileConfig, ErrorReporter errorReporter, boolean CCppMode, CTypes types) { + super(fileConfig, errorReporter) + this.CCppMode = CCppMode; + this.types = types + } + new(FileConfig fileConfig, ErrorReporter errorReporter, boolean CCppMode) { - this(fileConfig, errorReporter) - this.CCppMode = CCppMode; + this(fileConfig, errorReporter, CCppMode, new CTypes(errorReporter)) } new(FileConfig fileConfig, ErrorReporter errorReporter) { - super(fileConfig, errorReporter) + this(fileConfig, errorReporter, false) } //////////////////////////////////////////// @@ -380,14 +387,10 @@ class CGenerator extends GeneratorBase { } /** - * Set the appropriate target properties based on the target properties of - * the main .lf file. + * Set C-specific default target properties if needed. */ - override setTargetConfig(LFGeneratorContext context) { - super.setTargetConfig(context); - // Set defaults for the compiler after parsing the target properties - // of the main .lf file. - if (targetConfig.useCmake == false && targetConfig.compiler.isNullOrEmpty) { + def setCSpecificDefaults(LFGeneratorContext context) { + if (!targetConfig.useCmake && targetConfig.compiler.isNullOrEmpty) { if (this.CCppMode) { targetConfig.compiler = "g++" targetConfig.compilerFlags.addAll("-O2", "-Wno-write-strings") @@ -399,34 +402,28 @@ class CGenerator extends GeneratorBase { } /** - * Look for physical actions in 'resource'. - * If found, take appropriate actions to accommodate. - * - * Set keepalive to true. - * Set threads to be at least one to allow asynchronous schedule calls + * Look for physical actions in all resources. + * If found, set threads to be at least one to allow asynchronous schedule calls. */ - override accommodatePhysicalActionsIfPresent(Resource resource) { - super.accommodatePhysicalActionsIfPresent(resource); - + def accommodatePhysicalActionsIfPresent() { // If there are any physical actions, ensure the threaded engine is used and that // keepalive is set to true, unless the user has explicitly set it to false. - for (action : resource.allContents.toIterable.filter(Action)) { - if (action.origin == ActionOrigin.PHYSICAL) { - // If the unthreaded runtime is requested, use the threaded runtime instead - // because it is the only one currently capable of handling asynchronous events. - if (targetConfig.threads < 1) { - targetConfig.threads = 1 - errorReporter.reportWarning( - action, - '''Using the threaded C runtime to allow for asynchronous handling of« - » physical action «action.name».''' - ); + for (resource : JavaGeneratorUtils.getResources(reactors)) { + for (action : resource.allContents.toIterable.filter(Action)) { + if (action.origin == ActionOrigin.PHYSICAL) { + // If the unthreaded runtime is requested, use the threaded runtime instead + // because it is the only one currently capable of handling asynchronous events. + if (targetConfig.threads < 1) { + targetConfig.threads = 1 + errorReporter.reportWarning( + action, + '''Using the threaded C runtime to allow for asynchronous handling of« + » physical action «action.name».''' + ); + } } - } - } - } /** @@ -478,6 +475,9 @@ class CGenerator extends GeneratorBase { // The following generates code needed by all the reactors. super.doGenerate(resource, fsa, context) + accommodatePhysicalActionsIfPresent() + setCSpecificDefaults(context) + generatePreamble() printMain(); if (errorsOccurred) return; @@ -505,10 +505,16 @@ class CGenerator extends GeneratorBase { // it is the same for all federates. this.main = new ReactorInstance(mainDef.reactorClass.toDefinition, errorReporter, this.unorderedReactions) - this.main.assignLevels(); + if (this.main.assignLevels().nodeCount > 0) { + errorReporter.reportError("Main reactor has causality cycles. Skipping code generation."); + return; + } // Avoid compile errors by removing disconnected network ports. // This must be done after assigning levels. removeRemoteFederateConnectionPorts(main); + // Force reconstruction of dependence information. + // FIXME: Probably only need to do this for federated execution. + this.main.clearCaches(false); } } @@ -519,6 +525,7 @@ class CGenerator extends GeneratorBase { if (!dir.exists()) dir.mkdirs() targetConfig.compileAdditionalSources.add("ctarget.c"); + targetConfig.compileAdditionalSources.add("core" + File.separator + "mixed_radix.c"); // Copy the required core library files into the target file system. // This will overwrite previous versions. @@ -536,7 +543,9 @@ class CGenerator extends GeneratorBase { "util.h", "util.c", "platform.h", - "platform/Platform.cmake" + "platform/Platform.cmake", + "mixed_radix.c", + "mixed_radix.h" ); if (targetConfig.threads === 0) { coreFiles.add("reactor.c") @@ -587,6 +596,7 @@ class CGenerator extends GeneratorBase { context, IntegratedBuilder.VALIDATED_PERCENT_PROGRESS, IntegratedBuilder.GENERATED_PERCENT_PROGRESS ) for (federate : federates) { + currentFederate = federate; federateCount++; startTimeStepIsPresentCount = 0 startTimeStepTokens = 0 @@ -607,7 +617,7 @@ class CGenerator extends GeneratorBase { targetConfig.filesNamesWithoutPath.clear(); // Re-apply the cmake-include target property of the main .lf file. - val target = mainDef.reactorClass.eResource.findTarget + val target = JavaGeneratorUtils.findTarget(mainDef.reactorClass.eResource) if (target.config !== null) { // Update the cmake-include TargetProperty.updateOne( @@ -632,10 +642,10 @@ class CGenerator extends GeneratorBase { // Clear out previously generated code. code = new StringBuilder(commonCode) initializeTriggerObjects = new StringBuilder() - initializeTriggerObjectsEnd = new StringBuilder() - // Enable clock synchronization if the federate is not local and clock-sync is enabled - initializeClockSynchronization(federate) + // Enable clock synchronization if the federate + // is not local and clock-sync is enabled + initializeClockSynchronization() startTimeStep = new StringBuilder() @@ -648,7 +658,7 @@ class CGenerator extends GeneratorBase { copyTargetHeaderFile() // Generate code for each reactor. - generateReactorDefinitionsForFederate(federate); + generateReactorDefinitions(); // Derive target filename from the .lf filename. val cFilename = CCompiler.getTargetFileName(topLevelName, this.CCppMode); @@ -667,9 +677,10 @@ class CGenerator extends GeneratorBase { } // Generate main instance, if there is one. - // Note that any main reactors in imported files are ignored. + // Note that any main reactors in imported files are ignored. + // Skip generation if there are cycles. if (this.main !== null) { - generateFederate(federate) + generateMain() // Generate function to set default command-line options. // A literal array needs to be given outside any function definition, // so start with that. @@ -710,7 +721,7 @@ class CGenerator extends GeneratorBase { // If there are startup reactions, store them in an array. if (startupReactionCount > 0) { pr(''' - // Array of pointers to timer triggers to be scheduled in _lf_trigger_startup_reactions(). + // Array of pointers to reactions to be scheduled in _lf_trigger_startup_reactions(). reaction_t* _lf_startup_reactions[«startupReactionCount»]; ''') } else { @@ -759,7 +770,7 @@ class CGenerator extends GeneratorBase { for (action : federate.networkMessageActions) { // Find the corresponding ActionInstance. val actionInstance = main.lookupActionInstance(action) - triggers.add(triggerStructName(actionInstance)) + triggers.add(CUtil.triggerRef(actionInstance, null)) } var actionTableCount = 0 for (trigger : triggers) { @@ -944,7 +955,14 @@ class CGenerator extends GeneratorBase { // Note that the code does not get cleaned in this case. if (!targetConfig.noCompile) { if (!targetConfig.buildCommands.nullOrEmpty) { - runBuildCommand(); + CUtil.runBuildCommand( + fileConfig, + targetConfig, + commandFactory, + errorReporter, + [it | reportCommandErrors(it)], + context.mode + ) context.finish( GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig, null ); @@ -956,7 +974,9 @@ class CGenerator extends GeneratorBase { } else { context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(null)); } - refreshProject() + + // In case we are in Eclipse, make sure the generated code is visible. + JavaGeneratorUtils.refreshProject(context.mode, code.toString) } /** @@ -1028,7 +1048,7 @@ class CGenerator extends GeneratorBase { // Allocate the initial (before mutations) array of pointers to tokens. pr(''' _lf_tokens_with_ref_count_size = «startTimeStepTokens»; - _lf_tokens_with_ref_count = (token_present_t*)malloc(«startTimeStepTokens» * sizeof(token_present_t)); + _lf_tokens_with_ref_count = (token_present_t*)calloc(«startTimeStepTokens», sizeof(token_present_t)); ''') } // Create the table to initialize is_present fields to false between time steps. @@ -1037,23 +1057,32 @@ class CGenerator extends GeneratorBase { pr(''' // Create the array that will contain pointers to is_present fields to reset on each step. _lf_is_present_fields_size = «startTimeStepIsPresentCount»; - _lf_is_present_fields = (bool**)malloc(«startTimeStepIsPresentCount» * sizeof(bool*)); - _lf_is_present_fields_abbreviated = (bool**)malloc(«startTimeStepIsPresentCount» * sizeof(bool*)); + _lf_is_present_fields = (bool**)calloc(«startTimeStepIsPresentCount», sizeof(bool*)); + _lf_is_present_fields_abbreviated = (bool**)calloc(«startTimeStepIsPresentCount», sizeof(bool*)); _lf_is_present_fields_abbreviated_size = 0; ''') } // Allocate the memory for triggers used in federated execution pr(CGeneratorExtension.allocateTriggersForFederate(federate, this)); - // Assign appropriate pointers to the triggers - pr(initializeTriggerObjectsEnd, - CGeneratorExtension.initializeTriggerForControlReactions(this.main, federate, this)); pr(initializeTriggerObjects.toString) - pr('// Allocate memory.') - pr('// Populate arrays of trigger pointers.') - pr(initializeTriggerObjectsEnd.toString) - doDeferredInitialize(federate) + + // Assign appropriate pointers to the triggers + // FIXME: For python target, almost surely in the wrong place. + pr(CGeneratorExtension.initializeTriggerForControlReactions(this.main, federate, this).toString()); + + var reactionsInFederate = main.reactions.filter[ + r | return currentFederate.contains(r.definition); + ]; + + deferredInitialize(main, reactionsInFederate) + + deferredInitializeNonNested(main, reactionsInFederate) + + // Next, for every input port, populate its "self" struct + // fields with pointers to the output port that sends it data. + deferredConnectInputsToOutputs(main) // Put the code here to set up the tables that drive resetting is_present and // decrementing reference counts between time steps. This code has to appear @@ -1061,7 +1090,7 @@ class CGenerator extends GeneratorBase { // between inputs and outputs. pr(startTimeStep.toString) - setReactionPriorities(main, federate) + setReactionPriorities(main, code) initializeFederate(federate) unindent() @@ -1140,29 +1169,29 @@ class CGenerator extends GeneratorBase { * - If there are any cmake-include files, add them to the current list * of cmake-include files. * - If there are any preambles, add them to the preambles of the reactor. - * - * @param federate The federate to generate reactors for */ - def void generateReactorDefinitionsForFederate(FederateInstance federate) { + private def void generateReactorDefinitions() { val generatedReactorDecls = newLinkedHashSet if (this.main !== null) { - generateReactorChildrenForReactorInFederate(this.main, federate, generatedReactorDecls); + generateReactorChildren(this.main, generatedReactorDecls); } if (this.mainDef !== null) { - generateReactorFederated(this.mainDef.reactorClass, federate) + generateReactorClass(this.mainDef.reactorClass) } - // Generate code for each reactor that was not instantiated in main or its children. - for (r : reactors) { - // Get the declarations for reactors that are instantiated somewhere. - // A declaration is either a reactor definition or an import statement. - val declarations = this.instantiationGraph.getDeclarations(r); - // If the reactor has no instantiations and there is no main reactor, then - // generate code for it anyway (at a minimum, this means that the compiler is invoked - // so that reaction bodies are checked). - if (mainDef === null && declarations.isEmpty()) { - generateReactorFederated(r, null) + if (mainDef === null) { + // Generate code for each reactor that was not instantiated in main or its children. + for (r : reactors) { + // Get the declarations for reactors that are instantiated somewhere. + // A declaration is either a reactor definition or an import statement. + val declarations = this.instantiationGraph.getDeclarations(r); + // If the reactor has no instantiations and there is no main reactor, then + // generate code for it anyway (at a minimum, this means that the compiler is invoked + // so that reaction bodies are checked). + if (declarations.isEmpty()) { + generateReactorClass(r) + } } } } @@ -1178,29 +1207,19 @@ class CGenerator extends GeneratorBase { * - If there are any preambles, add them to the preambles of the reactor. * * @param reactor Used to extract children from - * @param federate All generated reactors will belong to this federate */ - def void generateReactorChildrenForReactorInFederate( + private def void generateReactorChildren( ReactorInstance reactor, - FederateInstance federate, LinkedHashSet generatedReactorDecls ) { for (r : reactor.children) { - // FIXME: If the reactor is the bank itself, it is just a placeholder and should be skipped. - // It seems that the way banks are instantiated is that - // for a bank new[4] Foo, there will be a reactor instance Foo and four additional - // reactor instances of Foo (5 total), but the first instance doesn't include - // any of the reactor instances within Foo in its children structure. - if (r.bankIndex != -2 && federate.contains(r)) { - val declarations = this.instantiationGraph.getDeclarations(r.reactorDefinition); - if (!declarations.isNullOrEmpty) { - for (d : declarations) { - if (!generatedReactorDecls.contains(d)) { - generatedReactorDecls.add(d); - generateReactorChildrenForReactorInFederate(r, federate, generatedReactorDecls); - inspectReactorEResource(d); - generateReactorFederated(d, federate); - } + if (currentFederate.contains(r)) { + if (r.reactorDeclaration !== null) { + if (!generatedReactorDecls.contains(r.reactorDeclaration)) { + generatedReactorDecls.add(r.reactorDeclaration); + generateReactorChildren(r, generatedReactorDecls); + inspectReactorEResource(r.reactorDeclaration); + generateReactorClass(r.reactorDeclaration); } } } @@ -1420,13 +1439,11 @@ class CGenerator extends GeneratorBase { * * Clock synchronization can be enabled using the clock-sync target property. * @see https://github.com/icyphy/lingua-franca/wiki/Distributed-Execution#clock-synchronization - * - * @param federate The federate to initialize clock synchronization for */ - protected def initializeClockSynchronization(FederateInstance federate) { + protected def initializeClockSynchronization() { // Check if clock synchronization should be enabled for this federate in the first place if (targetConfig.clockSync != ClockSyncMode.OFF - && (!federationRTIProperties.get('host').toString.equals(federate.host) + && (!federationRTIProperties.get('host').toString.equals(currentFederate.host) || targetConfig.clockSyncOptions.localFederatesOn) ) { // Insert the #defines at the beginning @@ -1437,13 +1454,13 @@ class CGenerator extends GeneratorBase { #define _LF_CLOCK_SYNC_ATTENUATION «targetConfig.clockSyncOptions.attenuation» ''') System.out.println("Initial clock synchronization is enabled for federate " - + federate.id + + currentFederate.id ); if (targetConfig.clockSync == ClockSyncMode.ON) { var collectStatsEnable = '' if (targetConfig.clockSyncOptions.collectStats) { collectStatsEnable = "#define _LF_CLOCK_SYNC_COLLECT_STATS" - System.out.println("Will collect clock sync statistics for federate " + federate.id) + System.out.println("Will collect clock sync statistics for federate " + currentFederate.id) // Add libm to the compiler flags // FIXME: This is a linker flag not compile flag but we don't have a way to add linker flags // FIXME: This is probably going to fail on MacOS (especially using clang) @@ -1455,7 +1472,7 @@ class CGenerator extends GeneratorBase { «collectStatsEnable» ''') System.out.println("Runtime clock synchronization is enabled for federate " - + federate.id + + currentFederate.id ); } } @@ -1481,7 +1498,7 @@ class CGenerator extends GeneratorBase { val reactorInstance = main.getChildReactorInstance(federate.instantiation) for (param : reactorInstance.parameters) { if (param.name.equalsIgnoreCase("STP_offset") && param.type.isTime) { - val stp = param.init.get(0).getLiteralTimeValue + val stp = param.getInitialValue().get(0).getLiteralTimeValue if (stp !== null) { pr(''' set_stp_offset(«stp.timeInTargetLanguage»); @@ -1592,7 +1609,7 @@ class CGenerator extends GeneratorBase { * * @param federate The federate that is sending its neighbor structure */ - def generateFederateNeighborStructure(FederateInstance federate) { + private def generateFederateNeighborStructure(FederateInstance federate) { val rtiCode = new StringBuilder(); pr(rtiCode, ''' @@ -1726,9 +1743,8 @@ class CGenerator extends GeneratorBase { * will not be generated if they are triggered by or send * data to contained reactors that are not in the federate. * @param reactor The parsed reactor data structure. - * @param federate A federate name, or null to unconditionally generate. */ - def generateReactorFederated(ReactorDecl reactor, FederateInstance federate) { + private def generateReactorClass(ReactorDecl reactor) { // FIXME: Currently we're not reusing definitions for declarations that point to the same definition. val defn = reactor.toDefinition @@ -1746,11 +1762,11 @@ class CGenerator extends GeneratorBase { // go into the constructor. Collect those lines of code here: val constructorCode = new StringBuilder() val destructorCode = new StringBuilder() - generateAuxiliaryStructs(reactor, federate) - generateSelfStruct(reactor, federate, constructorCode, destructorCode) - generateReactions(reactor, federate) - generateConstructor(reactor, federate, constructorCode) - generateDestructor(reactor, federate, destructorCode) + generateAuxiliaryStructs(reactor, currentFederate) + generateSelfStruct(reactor, currentFederate, constructorCode, destructorCode) + generateReactions(reactor, currentFederate) + generateConstructor(reactor, currentFederate, constructorCode) + generateDestructor(reactor, currentFederate, destructorCode) pr("// =============== END reactor class " + reactor.name) pr("") @@ -1779,7 +1795,7 @@ class CGenerator extends GeneratorBase { protected def generateConstructor( ReactorDecl reactor, FederateInstance federate, StringBuilder constructorCode ) { - val structType = selfStructType(reactor) + val structType = CUtil.selfType(reactor) pr(''' «structType»* new_«reactor.name»() { «structType»* self = («structType»*)calloc(1, sizeof(«structType»)); @@ -1805,7 +1821,7 @@ class CGenerator extends GeneratorBase { for (reaction : reactor.reactions) { if (federate === null || federate.contains(reaction)) { pr(destructorCode, ''' - for(int i = 0; i < self->_lf__reaction_«reactionCount».num_outputs; i++) { + for(size_t i = 0; i < self->_lf__reaction_«reactionCount».num_outputs; i++) { free(self->_lf__reaction_«reactionCount».triggers[i]); } ''') @@ -1814,7 +1830,7 @@ class CGenerator extends GeneratorBase { reactionCount++; } - val structType = selfStructType(decl) + val structType = CUtil.selfType(decl) pr(''' void delete_«decl.name»(«structType»* self) { «destructorCode.toString» @@ -1843,12 +1859,12 @@ class CGenerator extends GeneratorBase { var StringBuilder federatedExtension = new StringBuilder(); if (isFederatedAndDecentralized) { federatedExtension.append(''' - «targetTagType» intended_tag; + «types.getTargetTagType» intended_tag; '''); } if (isFederated) { federatedExtension.append(''' - «targetTimeType» physical_time_of_arrival; + «types.getTargetTimeType» physical_time_of_arrival; '''); } // First, handle inputs. @@ -1936,21 +1952,7 @@ class CGenerator extends GeneratorBase { } // Do not convert to lf_token_t* using lfTypeToTokenType because there // will be a separate field pointing to the token. - // val portType = lfTypeToTokenType(port.inferredType) - val portType = port.inferredType.targetType - // If the port type has the form type[number], then treat it specially - // to get a valid C type. - val matcher = arrayPatternFixed.matcher(portType) - if (matcher.find()) { - // for int[10], the first match is int, the second [10]. - // The following results in: int* _lf_foo[10]; - // if the port is an input and not a multiport. - // An output multiport will result in, for example - // int _lf_out[4][10]; - return '''«matcher.group(1)» value«matcher.group(2)»;'''; - } else { - return '''«portType» value;''' - } + return types.getVariableDeclaration(port.inferredType, "value", false) + ";" } /** @@ -1970,23 +1972,7 @@ class CGenerator extends GeneratorBase { } // Do not convert to lf_token_t* using lfTypeToTokenType because there // will be a separate field pointing to the token. - val actionType = action.inferredType.targetType - // If the input type has the form type[number], then treat it specially - // to get a valid C type. - val matcher = arrayPatternFixed.matcher(actionType) - if (matcher.find()) { - // for int[10], the first match is int, the second [10]. - // The following results in: int* foo; - return '''«matcher.group(1)»* value;'''; - } else { - val matcher2 = arrayPatternVariable.matcher(actionType) - if (matcher2.find()) { - // for int[], the first match is int. - // The following results in: int* foo; - return '''«matcher2.group(1)»* value;'''; - } - return '''«actionType» value;''' - } + return types.getTargetType(action) + " value;" } /** @@ -2006,7 +1992,7 @@ class CGenerator extends GeneratorBase { StringBuilder destructorCode ) { val reactor = decl.toDefinition - val selfType = selfStructType(decl) + val selfType = CUtil.selfType(decl) // Construct the typedef for the "self" struct. // Create a type name for the self struct. @@ -2042,7 +2028,7 @@ class CGenerator extends GeneratorBase { // pointers that will be allocated separately for each instance // because the sizes may be different. Otherwise, it is a simple // pointer. - if (input.isMultiport) { + if (JavaAstUtils.isMultiport(input)) { pr(input, body, ''' // Multiport input array will be malloc'd later. «variableStructType(input, decl)»** _lf_«input.name»; @@ -2077,7 +2063,7 @@ class CGenerator extends GeneratorBase { if (federate === null || federate.contains(output as Port)) { // If the port is a multiport, create an array to be allocated // at instantiation. - if (output.isMultiport) { + if (JavaAstUtils.isMultiport(output)) { pr(output, body, ''' // Array of output ports. «variableStructType(output, decl)»* _lf_«output.name»; @@ -2187,7 +2173,7 @@ class CGenerator extends GeneratorBase { if (port instanceof Input) { // If the variable is a multiport, then the place to store the data has // to be malloc'd at initialization. - if (!port.isMultiport) { + if (!JavaAstUtils.isMultiport(port)) { // Not a multiport. pr(port, body, ''' «variableStructType(port, containedReactor.reactorClass)» «port.name»; @@ -2201,10 +2187,10 @@ class CGenerator extends GeneratorBase { ''') } } else { - // Must be an output entry. + // Must be an output port. // Outputs of contained reactors are pointers to the source of data on the // self struct of the container. - if (!port.isMultiport) { + if (!JavaAstUtils.isMultiport(port)) { // Not a multiport. pr(port, body, ''' «variableStructType(port, containedReactor.reactorClass)»* «port.name»; @@ -2223,15 +2209,17 @@ class CGenerator extends GeneratorBase { ''') var reactorIndex = '' if (containedReactor.widthSpec !== null) { - reactorIndex = '[reactorIndex]' + reactorIndex = '[reactor_index]' pr(constructorCode, ''' - for (int reactorIndex = 0; reactorIndex < self->_lf_«containedReactor.name»_width; reactorIndex++) { + for (int reactor_index = 0; reactor_index < self->_lf_«containedReactor.name»_width; reactor_index++) { ''') indent(constructorCode) } + val portOnSelf = '''self->_lf_«containedReactor.name»«reactorIndex».«port.name»''' + if (isFederatedAndDecentralized) { pr(port, constructorCode, ''' - self->_lf_«containedReactor.name»«reactorIndex».«port.name»_trigger.intended_tag = (tag_t) { .time = NEVER, .microstep = 0u}; + «portOnSelf»_trigger.intended_tag = (tag_t) { .time = NEVER, .microstep = 0u}; ''') } val triggered = contained.reactionsTriggered(containedReactor, port) @@ -2242,11 +2230,11 @@ class CGenerator extends GeneratorBase { var triggeredCount = 0 for (index : triggered) { pr(port, constructorCode, ''' - self->_lf_«containedReactor.name»«reactorIndex».«port.name»_reactions[«triggeredCount++»] = &self->_lf__reaction_«index»; + «portOnSelf»_reactions[«triggeredCount++»] = &self->_lf__reaction_«index»; ''') } pr(port, constructorCode, ''' - self->_lf_«containedReactor.name»«reactorIndex».«port.name»_trigger.reactions = self->_lf_«containedReactor.name»«reactorIndex».«port.name»_reactions; + «portOnSelf»_trigger.reactions = «portOnSelf»_reactions; ''') } else { // Since the self struct is created using calloc, there is no need to set @@ -2261,14 +2249,14 @@ class CGenerator extends GeneratorBase { // self->_lf_«containedReactor.name».«port.name»_trigger.element_size = 0; // self->_lf_«containedReactor.name».«port.name»_trigger.intended_tag = (0, 0); pr(port, constructorCode, ''' - self->_lf_«containedReactor.name»«reactorIndex».«port.name»_trigger.last = NULL; - self->_lf_«containedReactor.name»«reactorIndex».«port.name»_trigger.number_of_reactions = «triggered.size»; + «portOnSelf»_trigger.last = NULL; + «portOnSelf»_trigger.number_of_reactions = «triggered.size»; ''') if (isFederated) { // Set the physical_time_of_arrival pr(port, constructorCode, ''' - self->_lf_«containedReactor.name»«reactorIndex».«port.name»_trigger.physical_time_of_arrival = NEVER; + «portOnSelf»_trigger.physical_time_of_arrival = NEVER; ''') } if (containedReactor.widthSpec !== null) { @@ -2276,7 +2264,7 @@ class CGenerator extends GeneratorBase { pr(constructorCode, "}") } } - if (port.isMultiport) { + if (JavaAstUtils.isMultiport(port)) { // Add to the destructor code to free the malloc'd memory. if (containedReactor.widthSpec !== null) { pr(port, destructorCode, ''' @@ -2325,7 +2313,7 @@ class CGenerator extends GeneratorBase { def generateParametersForReactor(StringBuilder builder, Reactor reactor) { for (parameter : reactor.allParameters) { prSourceLineNumber(builder, parameter) - pr(builder, parameter.getInferredType.targetType + ' ' + parameter.name + ';'); + pr(builder, types.getTargetType(parameter) + ' ' + parameter.name + ';'); } } @@ -2338,7 +2326,7 @@ class CGenerator extends GeneratorBase { def generateStateVariablesForReactor(StringBuilder builder, Reactor reactor) { for (stateVar : reactor.allStateVars) { prSourceLineNumber(builder, stateVar) - pr(builder, stateVar.getInferredType.targetType + ' ' + stateVar.name + ';'); + pr(builder, types.getTargetType(stateVar) + ' ' + stateVar.name + ';'); } } @@ -2535,8 +2523,8 @@ class CGenerator extends GeneratorBase { var elementSize = "0" // If the action type is 'void', we need to avoid generating the code // 'sizeof(void)', which some compilers reject. - if (action.type !== null && action.targetType.rootType != 'void') { - elementSize = '''sizeof(«action.targetType.rootType»)''' + if (action.type !== null && types.getTargetType(action).rootType != 'void') { + elementSize = '''sizeof(«types.getTargetType(action).rootType»)''' } // Since the self struct is allocated using calloc, there is no need to set: @@ -2615,7 +2603,7 @@ class CGenerator extends GeneratorBase { } } if (variable instanceof Input) { - val rootType = variable.targetType.rootType + val rootType = types.getTargetType(variable).rootType // Since the self struct is allocated using calloc, there is no need to set: // self->_lf__«input.name».is_timer = false; // self->_lf__«input.name».offset = 0LL; @@ -2717,63 +2705,50 @@ class CGenerator extends GeneratorBase { } /** - * Generate code to allocate the memory needed by reactions for triggering - * downstream reactions. Also, record startup and shutdown reactions. + * Record startup and shutdown reactions. * @param reactions A list of reactions. */ - private def void generateReactionMemory(Iterable reactions) { + private def void recordStartupAndShutdown(Iterable reactions) { // For each reaction instance, allocate the arrays that will be used to // trigger downstream reactions. for (reaction : reactions) { - val instance = reaction.parent; - val nameOfSelfStruct = selfStructName(instance) + val reactor = reaction.parent; - generateReactionOutputs(reaction); - + val temp = new StringBuilder(); + var foundOne = false; + + val reactionRef = CUtil.reactionRef(reaction) + // Next handle triggers of the reaction that come from a multiport output // of a contained reactor. Also, handle startup and shutdown triggers. for (trigger : reaction.triggers) { - if (trigger instanceof PortInstance) { - // If the port is a multiport, then we need to create an entry for each - // individual port. - if (trigger.isMultiport() && trigger.parent !== null && trigger.isOutput) { - // If the width is given as a numeric constant, then add that constant - // to the output count. Otherwise, assume it is a reference to one or more parameters. - val width = trigger.width; - val containerName = trigger.parent.name - val portStructType = variableStructType(trigger.definition, - trigger.parent.definition.reactorClass) - - // FIXME: What if the effect is a bank? Need to index the container. - pr(initializeTriggerObjectsEnd, ''' - «nameOfSelfStruct»->_lf_«containerName».«trigger.name»_width = «width»; - // Allocate memory to store pointers to the multiport outputs of a contained reactor. - «nameOfSelfStruct»->_lf_«containerName».«trigger.name» = («portStructType»**)malloc(sizeof(«portStructType»*) - * «nameOfSelfStruct»->_lf_«containerName».«trigger.name»_width); - ''') - } - } if (trigger.isStartup) { - pr(initializeTriggerObjects, ''' - _lf_startup_reactions[«startupReactionCount++»] = &«nameOfSelfStruct»->_lf__reaction_«reaction.index»; + pr(temp, ''' + _lf_startup_reactions[_lf_startup_reactions_count++] = &«reactionRef»; ''') + startupReactionCount += currentFederate.numRuntimeInstances(reactor); + foundOne = true; } else if (trigger.isShutdown) { - pr(initializeTriggerObjects, ''' - _lf_shutdown_reactions[«shutdownReactionCount++»] = &«nameOfSelfStruct»->_lf__reaction_«reaction.index»; + pr(temp, ''' + _lf_shutdown_reactions[_lf_shutdown_reactions_count++] = &«reactionRef»; ''') + foundOne = true; + shutdownReactionCount += currentFederate.numRuntimeInstances(reactor); if (targetConfig.tracing !== null) { - val description = getShortenedName(instance) - pr(initializeTriggerObjects, ''' - _lf_register_trace_event(«nameOfSelfStruct», &(«nameOfSelfStruct»->_lf__shutdown), + val description = getShortenedName(reactor) + val reactorRef = CUtil.reactorRef(reactor) + pr(temp, ''' + _lf_register_trace_event(«reactorRef», &(«reactorRef»->_lf__shutdown), trace_trigger, "«description».shutdown"); ''') } } } + if (foundOne) pr(initializeTriggerObjects, temp.toString); } } - + /** * Generate code that passes existing intended tag to all output ports * and actions. This intended tag is the minimum intended tag of the @@ -2803,7 +2778,7 @@ class CGenerator extends GeneratorBase { // Inherited intended tag. This will take the minimum // intended_tag of all input triggers - «targetTagType» inherited_min_intended_tag = («targetTagType») { .time = FOREVER, .microstep = UINT_MAX }; + «types.getTargetTagType» inherited_min_intended_tag = («types.getTargetTagType») { .time = FOREVER, .microstep = UINT_MAX }; ''') pr(intendedTagInheritenceCode, ''' // Find the minimum intended tag @@ -2815,7 +2790,7 @@ class CGenerator extends GeneratorBase { if (inputTrigger.variable instanceof Output) { // Output from a contained reactor val outputPort = inputTrigger.variable as Output - if (outputPort.isMultiport) { + if (JavaAstUtils.isMultiport(outputPort)) { pr(intendedTagInheritenceCode, ''' for (int i=0; i < «inputTrigger.container.name».«inputTrigger.variable.name»_width; i++) { if (compare_tags(«inputTrigger.container.name».«inputTrigger.variable.name»[i]->intended_tag, @@ -2835,7 +2810,7 @@ class CGenerator extends GeneratorBase { } else if (inputTrigger.variable instanceof Port) { // Input port val inputPort = inputTrigger.variable as Port - if (inputPort.isMultiport) { + if (JavaAstUtils.isMultiport(inputPort)) { pr(intendedTagInheritenceCode, ''' for (int i=0; i < «inputTrigger.variable.name»_width; i++) { if (compare_tags(«inputTrigger.variable.name»[i]->intended_tag, inherited_min_intended_tag) < 0) { @@ -2886,7 +2861,7 @@ class CGenerator extends GeneratorBase { indent(intendedTagInheritenceCode); for (effect : reaction.effects ?: emptyList) { if (effect.variable instanceof Input) { - if ((effect.variable as Port).isMultiport) { + if (JavaAstUtils.isMultiport(effect.variable as Port)) { pr(intendedTagInheritenceCode, ''' for(int i=0; i < «effect.container.name».«effect.variable.name»_width; i++) { «effect.container.name».«effect.variable.name»[i]->intended_tag = inherited_min_intended_tag; @@ -2942,7 +2917,7 @@ class CGenerator extends GeneratorBase { var StringBuilder reactionInitialization = new StringBuilder() // Define the "self" struct. - var structType = selfStructType(decl) + var structType = CUtil.selfType(decl) // A null structType means there are no inputs, state, // or anything else. No need to declare it. if (structType !== null) { @@ -3076,6 +3051,8 @@ class CGenerator extends GeneratorBase { pr(''' int «containedReactor.name»_width = self->_lf_«containedReactor.name»_width; ''') + // Windows does not support variables in arrays declared on the stack, + // so we use the maximum size over all bank members. array = '''[«maxContainedReactorBankWidth(containedReactor, null, 0)»]'''; } pr(''' @@ -3170,162 +3147,6 @@ class CGenerator extends GeneratorBase { return result } - /** - * Generate code to create the trigger table for each given reaction. - * Each table lists the triggers that the reaction - * execution may trigger. Each table is an array of arrays - * of pointers to the trigger_t structs representing the downstream inputs - * (or outputs of the container reactor) that are triggered by the reaction. - * Each trigger table goes into the reaction's reaction_t triggers field. - * That reaction_t struct is assumed to be on the self struct of the reactor - * instance with name "_lf__reaction_i", where i is the index of the reaction. - * The generated code will also set the values of the triggered_sizes array - * on the reaction_t struct to indicate the size of each array of trigger_t - * pointers. The generated code will malloc each of these arrays, and the - * destructor for the reactor instance will free them. - * @param reactions The reactions. - */ - private def generateRemoteTriggerTable(Iterable reactions) { - for (reaction : reactions) { - val selfStruct = selfStructName(reaction.parent); - val name = reaction.parent.getFullName; - var channelCount = 0 - - optimizeForSingleDominatingReaction(reaction, reaction.index); - - // Insert a string name to facilitate debugging. - if (targetConfig.logLevel >= LogLevel.LOG) { - pr(initializeTriggerObjectsEnd, ''' - // Reaction «reaction.index» of «name». - «selfStruct»->_lf__reaction_«reaction.index».name = "«name» reaction «reaction.index»"; - ''') - } - - for (port : reaction.effects.filter(PortInstance)) { - // Skip ports whose parent is not in the federation. - // This can happen with reactions in the top-level that have - // as an effect a port in a bank. - if (currentFederate.contains(port.parent)) { - - // If the port is a multiport, then its channels may have different sets - // of destinations. For ordinary ports, there will be only one range and - // its width will be 1. - // We generate the code to fill the triggers array first in a temporary buffer, - // so that we can simultaneously calculate the size of the total array. - for (PortInstance.SendRange range : port.eventualDestinations()) { - val temp = new StringBuilder(); - var destRangeCount = 0; - for (destinationRange : range.destinations) { - val destination = destinationRange.getPortInstance(); - if (destination.isOutput) { - // Include this destination port only if it has at least one - // reaction in the federation. - var belongs = false; - for (destinationReaction : destination.dependentReactions) { - if (currentFederate.contains(destinationReaction.parent)) { - belongs = true - } - } - if (belongs) { - pr(temp, ''' - // Port «port.getFullName» has reactions in its parent's parent. - // Point to the trigger struct for those reactions. - triggerArray[«destRangeCount»] = &«triggerStructName( - destination, - destination.parent.parent - )»; - ''') - // One array entry for each destination range is sufficient. - destRangeCount++; - } - } else { - // Destination is an input port. - pr(temp, ''' - // Point to destination port «destination.getFullName»'s trigger struct. - triggerArray[«destRangeCount»] = &«triggerStructName(destination)»; - ''') - // One array entry for each destination range is sufficient. - destRangeCount++; - } - } - - // Record the total size of the array. - pr(initializeTriggerObjectsEnd, ''' - for (int i = 0; i < «range.channelWidth»; i++) { - // Reaction «reaction.index» of «name» triggers «channelCount» - // downstream reactions through port «port.getFullName»[«channelCount» + i]. - «selfStruct»->_lf__reaction_«reaction.index».triggered_sizes[«channelCount» + i] = «destRangeCount»; - } - ''') - - // Malloc the memory for the arrays. - pr(initializeTriggerObjectsEnd, ''' - { // For scoping - // For reaction «reaction.index» of «name», allocate an - // array of trigger pointers for downstream reactions through port «port.getFullName» - trigger_t** triggerArray = (trigger_t**)malloc(«destRangeCount» * sizeof(trigger_t*)); - for (int i = 0; i < «range.channelWidth»; i++) { - «selfStruct»->_lf__reaction_«reaction.index».triggers[«channelCount» + i] = triggerArray; - } - // Fill the trigger array. - «temp.toString()» - } - ''') - channelCount += range.channelWidth; - } - } else { - // Count the port even if it is not contained in the federate because effect - // may be a bank (it can't be an instance of a bank), so an empty placeholder - // will be needed for each member of the bank that is not in the federate. - channelCount += port.width; - } - } - } - } - - /** - * Set the last_enabling_reaction field of the reaction struct to point - * to the single dominating upstream reaction, if there is one, or to be - * NULL if not. - * - * @param reaction The reaction. - * @param reactionNumber The reaction number within the parent. - */ - def optimizeForSingleDominatingReaction ( - ReactionInstance reaction, - int reactionNumber - ) { - val reactorInstance = reaction.parent; - val selfStruct = selfStructName(reactorInstance) - - // Record the number of reactions that this reaction depends on. - // This is used for optimization. When that number is 1, the reaction can - // be executed immediately when its triggering reaction has completed. - var dominatingReaction = reaction.findSingleDominatingReaction(); - - // The dominating reaction may not be included in this federate, in which case, we need to keep searching. - while (dominatingReaction !== null - && (!currentFederate.contains(dominatingReaction.definition)) - ) { - dominatingReaction = dominatingReaction.findSingleDominatingReaction(); - } - if (dominatingReaction !== null - && currentFederate.contains(dominatingReaction.definition) - && currentFederate.contains(dominatingReaction.parent) - ) { - val upstreamReaction = '''«selfStructName(dominatingReaction.parent)»->_lf__reaction_«dominatingReaction.index»''' - pr(initializeTriggerObjectsEnd, ''' - // Reaction «reactionNumber» of «reactorInstance.getFullName» depends on one maximal upstream reaction. - «selfStruct»->_lf__reaction_«reactionNumber».last_enabling_reaction = &(«upstreamReaction»); - ''') - } else { - pr(initializeTriggerObjectsEnd, ''' - // Reaction «reactionNumber» of «reactorInstance.getFullName» does not depend on one maximal upstream reaction. - «selfStruct»->_lf__reaction_«reactionNumber».last_enabling_reaction = NULL; - ''') - } - } - /** * Generate code to set up the tables used in _lf_start_time_step to decrement reference * counts and mark outputs absent between time steps. This function puts the code @@ -3335,36 +3156,52 @@ class CGenerator extends GeneratorBase { // First, set up to decrement reference counts for each token type // input of a contained reactor that is present. for (child : instance.children) { - if (currentFederate.contains(child)) { - var nameOfSelfStruct = selfStructName(child) + if (currentFederate.contains(child) && child.inputs.size > 0) { + + // Avoid generating code if not needed. + var foundOne = false; + val temp = new StringBuilder(); + + startScopedBlock(temp, child, true); + for (input : child.inputs) { if (isTokenType((input.definition as Input).inferredType)) { + foundOne = true; + val portRef = CUtil.portRefName(input); if (input.isMultiport()) { - pr(startTimeStep, ''' + pr(temp, ''' for (int i = 0; i < «input.width»; i++) { - _lf_tokens_with_ref_count[«startTimeStepTokens» + i].token - = &«nameOfSelfStruct»->_lf_«input.name»[i]->token; - _lf_tokens_with_ref_count[«startTimeStepTokens» + i].status - = (port_status_t*)&«nameOfSelfStruct»->_lf_«input.name»[i]->is_present; - _lf_tokens_with_ref_count[«startTimeStepTokens» + i].reset_is_present = false; + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count].token + = &«portRef»[i]->token; + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count].status + = (port_status_t*)&«portRef»[i]->is_present; + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count++].reset_is_present = false; } ''') - startTimeStepTokens += input.width } else { - pr(startTimeStep, ''' - _lf_tokens_with_ref_count[«startTimeStepTokens»].token - = &«nameOfSelfStruct»->_lf_«input.name»->token; - _lf_tokens_with_ref_count[«startTimeStepTokens»].status - = (port_status_t*)&«nameOfSelfStruct»->_lf_«input.name»->is_present; - _lf_tokens_with_ref_count[«startTimeStepTokens»].reset_is_present = false; + pr(temp, ''' + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count].token + = &«portRef»->token; + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count].status + = (port_status_t*)&«portRef»->is_present; + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count++].reset_is_present = false; ''') - startTimeStepTokens++ } + startTimeStepTokens += currentFederate.numRuntimeInstances(input.parent) * input.width; } } + endScopedBlock(temp); + + if (foundOne) { + pr(startTimeStep, temp.toString()); + } } } - var containerSelfStructName = selfStructName(instance) + // Avoid generating dead code if nothing is relevant. + var foundOne = false; + var temp = new StringBuilder(); + var containerSelfStructName = CUtil.reactorRef(instance) + // Handle inputs that get sent data from a reaction rather than from // another contained reactor and reactions that are triggered by an // output of a contained reactor. @@ -3378,144 +3215,157 @@ class CGenerator extends GeneratorBase { portsSeen.add(port) // This reaction is sending to an input. Must be // the input of a contained reactor in the federate. + // NOTE: If instance == main and the federate is within a bank, + // this assumes that the reaction writes only to the bank member in the federate. if (currentFederate.contains(port.parent)) { - // If this is a multiport, then the port struct on the self - // struct is a pointer. Otherwise, it is the struct itself. - if (port.isMultiport) { - pr(startTimeStep, ''' - // Add port «port.getFullName» to array of is_present fields. - for (int i = 0; i < «port.width»; i++) { - _lf_is_present_fields[«startTimeStepIsPresentCount» + i] - = &«containerSelfStructName»->_lf_«port.parent.name».«port.name»[i]->is_present; - } - ''') - if (isFederatedAndDecentralized) { - // Intended_tag is only applicable to ports in federated execution. - pr(startTimeStep, ''' - // Add port «port.getFullName» to array of is_present fields. - for (int i = 0; i < «port.width»; i++) { - _lf_intended_tag_fields[«startTimeStepIsPresentCount» + i] - = &«containerSelfStructName»->_lf_«port.parent.name».«port.name»[i]->intended_tag; - } - ''') - } - startTimeStepIsPresentCount += port.width; + foundOne = true; + + pr(temp, ''' + // Add port «port.getFullName» to array of is_present fields. + ''') + + if (port.parent != instance) { + // The port belongs to contained reactor, so we also have + // iterate over the instance bank members. + startScopedBlock(temp); + pr(temp, "int count = 0;"); + startScopedBlock(temp, instance, true); + startScopedBankChannelIteration(temp, port, null); } else { - pr(startTimeStep, ''' - // Add port «port.getFullName» to array of is_present fields. - _lf_is_present_fields[«startTimeStepIsPresentCount»] - = &«containerSelfStructName»->_lf_«port.parent.name».«port.name».is_present; + startScopedBankChannelIteration(temp, port, "count"); + } + val portRef = CUtil.portRefNested(port); + val con = (port.isMultiport)? "->" : "."; + + pr(temp, ''' + _lf_is_present_fields[«startTimeStepIsPresentCount» + count] = &«portRef»«con»is_present; + ''') + if (isFederatedAndDecentralized) { + // Intended_tag is only applicable to ports in federated execution. + pr(temp, ''' + _lf_intended_tag_fields[«startTimeStepIsPresentCount» + count] = &«portRef»«con»intended_tag; ''') - if (isFederatedAndDecentralized) { - // Intended_tag is only applicable to ports in federated execution. - pr(startTimeStep, ''' - // Add port «port.getFullName» to array of is_present fields. - _lf_intended_tag_fields[«startTimeStepIsPresentCount»] - = &«containerSelfStructName»->_lf_«port.parent.name».«port.name».intended_tag; - ''') - } - startTimeStepIsPresentCount++ } - } + + startTimeStepIsPresentCount += port.width * currentFederate.numRuntimeInstances(port.parent); + + if (port.parent != instance) { + pr(temp, "count++;"); + endScopedBlock(temp); + endScopedBlock(temp); + endScopedBankChannelIteration(temp, port, null); + } else { + endScopedBankChannelIteration(temp, port, "count"); + } + } } } // Find outputs of contained reactors that have token types and therefore // need to have their reference counts decremented. - for (port : reaction.sources) { - if (port.definition instanceof Output && !portsSeen.contains(port)) { - val output = port as PortInstance; - portsSeen.add(output) + for (port : reaction.sources.filter(PortInstance)) { + if (port.isOutput && !portsSeen.contains(port)) { + portsSeen.add(port) // This reaction is receiving data from the port. - if (isTokenType((output.definition as Output).inferredType)) { - if (output.isMultiport()) { - pr(startTimeStep, ''' - for (int i = 0; i < «output.width»; i++) { - _lf_tokens_with_ref_count[«startTimeStepTokens» + i].token - = &«containerSelfStructName»->_lf_«output.parent.name».«output.name»[i]->token; - _lf_tokens_with_ref_count[«startTimeStepTokens» + i].status - = (port_status_t*)&«containerSelfStructName»->_lf_«output.parent.name».«output.name»[i]->is_present; - _lf_tokens_with_ref_count[«startTimeStepTokens» + i].reset_is_present = false; - } - ''') - startTimeStepTokens += output.width - } else { - pr(startTimeStep, ''' - _lf_tokens_with_ref_count[«startTimeStepTokens»].token - = &«containerSelfStructName»->_lf_«output.parent.name».«output.name»->token; - _lf_tokens_with_ref_count[«startTimeStepTokens»].status - = (port_status_t*)&«containerSelfStructName»->_lf_«output.parent.name».«output.name»->is_present; - _lf_tokens_with_ref_count[«startTimeStepTokens»].reset_is_present = false; - ''') - startTimeStepTokens++ - } - } - } - } - } - } - // Next, set up the table to mark each output of each contained reactor absent. - for (child : instance.children) { - if (currentFederate.contains(child)) { - var nameOfSelfStruct = selfStructName(child) - for (output : child.outputs) { - if (output.isMultiport()) { - pr(startTimeStep, ''' - // Add port «output.getFullName» to array of is_present fields. - { // Scope to avoid collisions with variable names. - int i = «startTimeStepIsPresentCount»; - for (int j = 0; j < «output.width»; j++) { - _lf_is_present_fields[i++] = &«nameOfSelfStruct»->_lf_«output.name»[j].is_present; - } - } - ''') - if (isFederatedAndDecentralized) { - // Intended_tag is only applicable to ports in federated execution with decentralized coordination. - pr(startTimeStep, ''' - // Add port «output.getFullName» to array of intended_tag fields. - { // Scope to avoid collisions with variable names. - int i = «startTimeStepIsPresentCount»; - for (int j = 0; j < «output.width»; j++) { - _lf_intended_tag_fields[i++] = &«nameOfSelfStruct»->_lf_«output.name»[j].intended_tag; - } - } - ''') - } - startTimeStepIsPresentCount += output.width; - } else { - pr(startTimeStep, ''' - // Add port «output.getFullName» to array of is_present fields. - _lf_is_present_fields[«startTimeStepIsPresentCount»] = &«nameOfSelfStruct»->«getStackPortMember('''_lf_«output.name»''', "is_present")»; - ''') - if (isFederatedAndDecentralized) { - // Intended_tag is only applicable to ports in federated execution with decentralized coordination. - pr(startTimeStep, ''' - // Add port «output.getFullName» to array of Intended_tag fields. - _lf_intended_tag_fields[«startTimeStepIsPresentCount»] = &«nameOfSelfStruct»->«getStackPortMember('''_lf_«output.name»''', "intended_tag")»; - ''') + if (isTokenType((port.definition as Output).inferredType)) { + foundOne = true; + + pr(temp, ''' + // Add port «port.getFullName» to array _lf_tokens_with_ref_count. + ''') + + // Potentially have to iterate over bank members of the instance + // (parent of the reaction), bank members of the contained reactor (if a bank), + // and channels of the multiport (if multiport). + startScopedBlock(temp, instance, true); + startScopedBankChannelIteration(temp, port, "count"); + + val portRef = CUtil.portRef(port, true, true, null, null, null); + + pr(temp, ''' + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count].token = &«portRef»->token; + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count].status = (port_status_t*)&«portRef»->is_present; + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count++].reset_is_present = false; + ''') + startTimeStepTokens += port.width * currentFederate.numRuntimeInstances(port.parent); + + endScopedBankChannelIteration(temp, port, "count"); + endScopedBlock(temp); } - startTimeStepIsPresentCount++ } } } } + if (foundOne) pr(startTimeStep, temp.toString()); + temp = new StringBuilder(); + foundOne = false; + for (action : instance.actions) { if (currentFederate === null || currentFederate.contains(action.definition)) { - pr(startTimeStep, ''' + foundOne = true; + startScopedBlock(temp, instance, true); + + pr(temp, ''' // Add action «action.getFullName» to array of is_present fields. _lf_is_present_fields[«startTimeStepIsPresentCount»] = &«containerSelfStructName»->_lf_«action.name».is_present; ''') if (isFederatedAndDecentralized) { // Intended_tag is only applicable to actions in federated execution with decentralized coordination. - pr(startTimeStep, ''' + pr(temp, ''' // Add action «action.getFullName» to array of intended_tag fields. _lf_intended_tag_fields[«startTimeStepIsPresentCount»] = &«containerSelfStructName»->_lf_«action.name».intended_tag; ''') } - startTimeStepIsPresentCount++ + startTimeStepIsPresentCount += currentFederate.numRuntimeInstances(action.parent); + endScopedBlock(temp); + } + } + if (foundOne) pr(startTimeStep, temp.toString()); + temp = new StringBuilder(); + foundOne = false; + + // Next, set up the table to mark each output of each contained reactor absent. + for (child : instance.children) { + if (currentFederate.contains(child) && child.outputs.size > 0) { + + startScopedBlock(temp); + pr(temp, '''int count = 0;'''); + startScopedBlock(temp, child, true); + + var channelCount = 0; + for (output : child.outputs) { + if (!output.dependsOnReactions.isEmpty){ + foundOne = true; + + pr(temp, ''' + // Add port «output.getFullName» to array of is_present fields. + ''') + startChannelIteration(temp, output); + + pr(temp, ''' + _lf_is_present_fields[«startTimeStepIsPresentCount» + count] = &«CUtil.portRef(output)».is_present; + ''') + + if (isFederatedAndDecentralized) { + // Intended_tag is only applicable to ports in federated execution with decentralized coordination. + pr(temp, ''' + // Add port «output.getFullName» to array of intended_tag fields. + _lf_intended_tag_fields[«startTimeStepIsPresentCount» + count] = &«CUtil.portRef(output)».intended_tag; + ''') + } + + pr(temp, '''count++;'''); + channelCount += output.width; + endChannelIteration(temp, output); + } + } + startTimeStepIsPresentCount += channelCount * currentFederate.numRuntimeInstances(child); + endScopedBlock(temp); + endScopedBlock(temp); } } + if (foundOne) pr(startTimeStep, temp.toString()); } /** @@ -3527,19 +3377,20 @@ class CGenerator extends GeneratorBase { private def generateActionInitializations(Iterable actions) { for (action : actions) { if (!action.isShutdown) { - var triggerStructName = triggerStructName(action) + val triggerStructName = CUtil.reactorRef(action.parent) + "->_lf__" + action.name; var minDelay = action.minDelay var minSpacing = action.minSpacing pr(initializeTriggerObjects, ''' - «triggerStructName».offset = «timeInTargetLanguage(minDelay)»; + // Initializing action «action.fullName» + «triggerStructName».offset = «minDelay.timeInTargetLanguage»; «IF minSpacing !== null» - «triggerStructName».period = «timeInTargetLanguage(minSpacing)»; + «triggerStructName».period = «minSpacing.timeInTargetLanguage»; «ELSE» «triggerStructName».period = «CGenerator.UNDEFINED_MIN_SPACING»; «ENDIF» ''') } - triggerCount++ + triggerCount += currentFederate.numRuntimeInstances(action.parent); } } @@ -3555,17 +3406,18 @@ class CGenerator extends GeneratorBase { private def generateTimerInitializations(Iterable timers) { for (timer : timers) { if (!timer.isStartup) { - var triggerStructName = triggerStructName(timer) - val offset = timeInTargetLanguage(timer.offset) - val period = timeInTargetLanguage(timer.period) + val triggerStructName = CUtil.reactorRef(timer.parent) + "->_lf__" + timer.name; + val offset = timer.offset.timeInTargetLanguage + val period = timer.period.timeInTargetLanguage pr(initializeTriggerObjects, ''' + // Initializing timer «timer.fullName». «triggerStructName».offset = «offset»; «triggerStructName».period = «period»; - _lf_timer_triggers[«timerCount»] = &«triggerStructName»; + _lf_timer_triggers[_lf_timer_triggers_count++] = &«triggerStructName»; ''') - timerCount++ + timerCount += currentFederate.numRuntimeInstances(timer.parent); } - triggerCount++ + triggerCount += currentFederate.numRuntimeInstances(timer.parent); } } @@ -3624,150 +3476,43 @@ class CGenerator extends GeneratorBase { ''' } } - - /** - * Return a string for referencing the struct with the value and is_present - * fields of the specified port. This is used for establishing the destination of - * data for a connection between ports. - * This will have the following form: - * - * * selfStruct->_lf_portName - * - * @param port An instance of a destination input port. - */ - static def destinationReference(PortInstance port) { - // Note that if the port is an output, then it must - // have dependent reactions, otherwise it would not - // be a destination. - var destStruct = selfStructName(port.parent) - return '''«destStruct»->_lf_«port.name»''' - } - - /** - * Return a string for referencing the port struct with the value - * and is_present fields in a self struct that receives data from - * the specified output port to be used by a reaction. - * The output port is contained by a contained reactor. - * This will have the following form: - * - * * selfStruct->_lf_reactorName.portName - * - * The selfStruct is that of the container of reactor that - * contains the port. - * - * @param port An instance of a destination port. - */ - static def reactionReference(PortInstance port) { - var destStruct = selfStructName(port.parent.parent) - - if (port.isOutput) { - return '''«destStruct»->_lf_«port.parent.name».«port.name»''' - } else { - return '// Nothing to do. Port is an input.' - } - } - - /** - * Return a string for referencing the data or is_present value of - * the specified port. This is used for establishing the source of - * data for a connection between ports. - * This will have one of the following forms: - * - * * &selfStruct->_lf_portName - * * &selfStruct->_lf_parentName.portName - * - * It is assumed that the specified port is - * the eventual upstream port where the data is stored. E.g., it is an input that - * connected to upstream output, then portName will be the name - * of the upstream output and the selfStruct will be that of the - * upstream reactor. If the port is an input port that is written to - * by a reaction of the parent of the port's parent, then the selfStruct - * will be that of the parent of the port's parent, and parentName - * will the name of the port's parent. - * If the port is an output, then selfStruct will be the parent's - * selfStruct and the portName will be the name of the port. - * - * @param port An instance of the port to be referenced. - */ - static def sourceReference(PortInstance port) { - if (port.isOutput()) { - val sourceStruct = selfStructName(port.parent); - return '''«sourceStruct»->_lf_«port.name»''' - } else { - val sourceStruct = selfStructName(port.parent.parent) - return '''«sourceStruct»->_lf_«port.parent.name».«port.name»''' - } - } - - /** Return the unique name for the "self" struct of the specified - * reactor instance from the instance ID. If the instance is a member - * of a bank of reactors, this returns something of the form - * name_self[index], where the index is the position within the bank. - * @param instance The reactor instance. - * @return The name of the self struct. + + /** + * Construct a unique type for the struct of the specified + * typed variable (port or action) of the specified reactor class. + * This is required to be the same as the type name returned by + * {@link variableStructType(TriggerInstance)}. + * @param variable The variable. + * @param reactor The reactor class. + * @return The name of the self struct. */ - static def selfStructName(ReactorInstance instance) { - var result = instance.uniqueID + "_self" - // If this reactor is a member of a bank of reactors, then change - // the name of its self struct to append [index]. - if (instance.bankIndex >= 0) { - result += "[" + instance.bankIndex + "]" - } - return result + static def variableStructType(Variable variable, ReactorDecl reactor) { + '''«reactor.name.toLowerCase»_«variable.name»_t''' } - /** Construct a unique type for the "self" struct of the specified - * reactor class from the reactor class. - * @param reactor The reactor class. - * @return The name of the self struct. - */ - def selfStructType(ReactorDecl reactor) { - return reactor.name.toLowerCase + "_self_t" - } - - /** Construct a unique type for the struct of the specified - * typed variable (port or action) of the specified reactor class. - * @param variable The variable. - * @param reactor The reactor class. - * @return The name of the self struct. + /** + * Construct a unique type for the struct of the specified + * instance (port or action). + * This is required to be the same as the type name returned by + * {@link variableStructType(Variable, ReactorDecl)}. + * @param portOrAction The port or action instance. + * @return The name of the self struct. */ - def variableStructType(Variable variable, ReactorDecl reactor) { - '''«reactor.name.toLowerCase»_«variable.name»_t''' + static def variableStructType(TriggerInstance portOrAction) { + '''«portOrAction.parent.reactorDeclaration.name.toLowerCase»_«portOrAction.name»_t''' } - /** Return the function name for specified reaction of the - * specified reactor. - * @param reactor The reactor - * @param reactionIndex The reaction index. - * @return The function name for the reaction. + /** + * Return the function name for specified reaction of the + * specified reactor. + * @param reactor The reactor + * @param reactionIndex The reaction index. + * @return The function name for the reaction. */ - def reactionFunctionName(ReactorDecl reactor, int reactionIndex) { + static def reactionFunctionName(ReactorDecl reactor, int reactionIndex) { reactor.name.toLowerCase + "reaction_function_" + reactionIndex } - /** Return a reference to the trigger_t struct of the specified - * trigger instance (input port or action). This trigger_t struct - * is on the self struct. - * @param instance The port or action instance. - * @return The name of the trigger struct. - */ - static def triggerStructName(TriggerInstance instance) { - return selfStructName(instance.parent) - + '''->_lf__''' - + instance.name - } - - /** Return a reference to the trigger_t struct for the specified output - * port of a contained reactor that triggers a reaction in the specified reactor. - * @param port The output port of a contained reactor. - * @param reaction The reaction triggered by this port. - * @return The name of the trigger struct, which is in the self struct - * of the container of the reaction. - */ - static def triggerStructName(PortInstance port, ReactorInstance reactor) { - return '''«selfStructName(reactor)»->_lf_«port.parent.name».«port.name»_trigger''' - } - /** * Generates C code to retrieve port->member * This function is used for clarity and is called whenever struct is allocated on heap memory. @@ -3779,7 +3524,6 @@ class CGenerator extends GeneratorBase { «portName»->«member» ''' - /** * Return the operator used to retrieve struct members */ @@ -3787,15 +3531,6 @@ class CGenerator extends GeneratorBase { . ''' - /** - * Generates C code to retrieve port.member - * This function is used for clarity and is called whenever struct is allocated on stack memory. - * @param portName The name of the port in string - * @param member The member's name(e.g., is_present) - * @return Generated code - */ - def getStackPortMember(String portName, String member) '''«portName».«member»''' - /** * Return the full name of the specified instance without * the leading name of the top-level reactor, unless this @@ -3829,46 +3564,54 @@ class CGenerator extends GeneratorBase { // the header information in the trace file. if (targetConfig.tracing !== null) { var description = getShortenedName(instance) - var nameOfSelfStruct = selfStructName(instance) + var selfStruct = CUtil.reactorRef(instance) pr(initializeTriggerObjects, ''' - _lf_register_trace_event(«nameOfSelfStruct», NULL, trace_reactor, "«description»"); + _lf_register_trace_event(«selfStruct», NULL, trace_reactor, "«description»"); ''') for (action : actions) { pr(initializeTriggerObjects, ''' - _lf_register_trace_event(«nameOfSelfStruct», &(«nameOfSelfStruct»->_lf__«action.name»), trace_trigger, "«description».«action.name»"); + _lf_register_trace_event(«selfStruct», &(«selfStruct»->_lf__«action.name»), trace_trigger, "«description».«action.name»"); ''') } for (timer : timers) { pr(initializeTriggerObjects, ''' - _lf_register_trace_event(«nameOfSelfStruct», &(«nameOfSelfStruct»->_lf__«timer.name»), trace_trigger, "«description».«timer.name»"); + _lf_register_trace_event(«selfStruct», &(«selfStruct»->_lf__«timer.name»), trace_trigger, "«description».«timer.name»"); ''') } } } /** - * Generate code to instantiate the specified federate at the top level. - * @param federate The federate to instantiate or null to generate everything. + * Generate code to instantiate the main reactor and any contained reactors + * that are in the current federate. */ - private def void generateFederate(FederateInstance federate) { - - currentFederate = federate; - + private def void generateMain() { + // Create lists of the actions, timers, and reactions that are in the federate. // These default to the full list for non-federated programs. var actionsInFederate = main.actions.filter[ - a | return federate.contains(a.definition); + a | return currentFederate.contains(a.definition); ]; var reactionsInFederate = main.reactions.filter[ - r | return federate.contains(r.definition); + r | return currentFederate.contains(r.definition); ]; var timersInFederate = main.timers.filter[ - t | return federate.contains(t.definition); + t | return currentFederate.contains(t.definition); ]; + + // Create an array of arrays to store all self structs. + // This is needed because connections cannot be established until + // all reactor instances have self structs because ports that + // receive data reference the self structs of the originating + // reactors, which are arbitarily far away in the program graph. + generateSelfStructs(main); + + pr(initializeTriggerObjects, + '// ***** Start initializing ' + main.name) // Generate the self struct declaration for the top level. pr(initializeTriggerObjects, ''' - «selfStructType(main.definition.reactorClass)»* «selfStructName(main)» = new_«main.name»(); + «CUtil.reactorRef(main)» = new_«main.name»(); ''') // Generate code for top-level parameters, actions, timers, and reactions that @@ -3877,33 +3620,30 @@ class CGenerator extends GeneratorBase { generateReactorInstanceExtension(main, reactionsInFederate); generateParameterInitialization(main); - for (child: main.children) { - // If the child has a multiport that is an effect of some reaction in main, - // then we have to generate code to allocate memory for arrays pointing to - // its data. If the child is a bank, then memory is allocated for the entire - // bank width because a reaction cannot specify which bank members it writes - // to so we have to assume it can write to any. Hence, we do not want to - // filter which children we do this for by federate, which is why this call - // is here. - if (federate.contains(child) || child.bankIndex >= 0) { - generateAllocationForEffectsOnInputs(child); - } - if (federate.contains(child)) { - generateReactorInstance(child); + pr(initializeTriggerObjects, ''' + int _lf_startup_reactions_count = 0; + int _lf_shutdown_reactions_count = 0; + int _lf_timer_triggers_count = 0; + int _lf_tokens_with_ref_count_count = 0; + '''); + + for (child: main.children) { + if (currentFederate.contains(child)) { + // NOTE: child could be a bank, in which case, for federated + // systems, only one of the bank members will be part of the federate. + generateReactorInstance(child); } } - generateReactionMemory(reactionsInFederate); + recordStartupAndShutdown(reactionsInFederate); generateStateVariableInitializations(main); - generateRemoteTriggerTable(reactionsInFederate); generateTimerInitializations(timersInFederate); generateActionInitializations(actionsInFederate); - generateInputNumDestinations(reactionsInFederate); generateInitializeActionToken(actionsInFederate); generateSetDeadline(reactionsInFederate); generateStartTimeStep(main); - pr(initializeTriggerObjects, "//***** End initializing " + main.name); + pr(initializeTriggerObjects, "// ***** End initializing " + main.name); } /** @@ -3914,108 +3654,47 @@ class CGenerator extends GeneratorBase { * contained reactors or null if there are no federates. */ def void generateReactorInstance(ReactorInstance instance) { - // FIXME: Consolidate this with generateFederate. The only difference is that - // generateFederate is the version of this method that is run on main, the - // top-level reactor. + // FIXME: Consolidate this with generateMain. The only difference is that + // generateMain is the version of this method that is run on main, the + // top-level reactor. + var reactorClass = instance.definition.reactorClass var fullName = instance.fullName - pr(initializeTriggerObjects, '// ************* Instance ' + fullName + ' of class ' + - reactorClass.name) - - var nameOfSelfStruct = selfStructName(instance) - var structType = selfStructType(reactorClass) + pr(initializeTriggerObjects, + '// ***** Start initializing ' + fullName + ' of class ' + reactorClass.name) // If this reactor is a placeholder for a bank of reactors, then generate - // an array of instances of reactors and return. - if (instance.bankMembers !== null) { - pr(initializeTriggerObjects, ''' - «structType»* «nameOfSelfStruct»[«instance.bankMembers.size»]; - ''') - return - } + // an array of instances of reactors and create an enclosing for loop. + // Need to do this for each of the builders into which the code writes. + startScopedBlock(startTimeStep, instance, true); + startScopedBlock(initializeTriggerObjects, instance, true); // Generate the instance self struct containing parameters, state variables, - // and outputs (the "self" struct). The form is slightly different - // depending on whether its in a bank of reactors. - if (instance.bankIndex >= 0) { - pr(initializeTriggerObjects, ''' - «nameOfSelfStruct» = new_«reactorClass.name»(); - ''') - } else { - pr(initializeTriggerObjects, ''' - «structType»* «nameOfSelfStruct» = new_«reactorClass.name»(); - ''') - } - + // and outputs (the "self" struct). + pr(initializeTriggerObjects, ''' + «CUtil.reactorRefName(instance)»[«CUtil.runtimeIndex(instance)»] = new_«reactorClass.name»(); + ''') + // Generate code to initialize the "self" struct in the // _lf_initialize_trigger_objects function. - pr(initializeTriggerObjects, "//***** Start initializing " + fullName) generateTraceTableEntries(instance, instance.actions, instance.timers) generateReactorInstanceExtension(instance, instance.reactions) generateParameterInitialization(instance) - // Once parameters are done, we can allocate memory for any multiports. - // Allocate memory for outputs. - // NOTE: Not done for top level. - for (output : reactorClass.toDefinition.outputs) { - // If the port is a multiport, create an array. - if (output.isMultiport) { - initializeOutputMultiport(initializeTriggerObjects, output, nameOfSelfStruct, instance) - } else { - pr(initializeTriggerObjects, ''' - // width of -2 indicates that it is not a multiport. - «nameOfSelfStruct»->_lf_«output.name»_width = -2; - ''') - } - } - - // For each input and output that is a multiport and an effect of some reaction, - // generate code to allocate memory for arrays pointing to its data. Do this here - // for the inputs of the children and the outputs of this reactor. - for (child : instance.children) { - generateAllocationForEffectsOnInputs(child); - } + initializeOutputMultiports(instance) + initializeInputMultiports(instance) - generateAllocationForEffectsOnOutputs(instance); - generateReactionMemory(instance.reactions); - - // Next, allocate memory for input. - // NOTE: Not done for top level. - for (input : reactorClass.toDefinition.inputs) { - // If the port is a multiport, create an array. - if (input.isMultiport) { - pr(initializeTriggerObjects, ''' - «nameOfSelfStruct»->_lf_«input.name»_width = «multiportWidthSpecInC(input, null, instance)»; - // Allocate memory for multiport inputs. - «nameOfSelfStruct»->_lf_«input.name» = («variableStructType(input, reactorClass)»**)malloc(sizeof(«variableStructType(input, reactorClass)»*) * «nameOfSelfStruct»->_lf_«input.name»_width); - // Set inputs by default to an always absent default input. - for (int i = 0; i < «nameOfSelfStruct»->_lf_«input.name»_width; i++) { - «nameOfSelfStruct»->_lf_«input.name»[i] = &«nameOfSelfStruct»->_lf_default__«input.name»; - } - ''') - } else { - pr(initializeTriggerObjects, ''' - // width of -2 indicates that it is not a multiport. - «nameOfSelfStruct»->_lf_«input.name»_width = -2; - ''') - } - } + recordStartupAndShutdown(instance.reactions); // Next, initialize the "self" struct with state variables. // These values may be expressions that refer to the parameter values defined above. generateStateVariableInitializations(instance); - generateRemoteTriggerTable(instance.reactions); - // Generate trigger objects for the instance. generateTimerInitializations(instance.timers); generateActionInitializations(instance.actions); - - // Initialize the num_destinations fields of port structs on the self struct. - generateOutputNumDestinations(instance); // NOTE: Not done for top level. - generateInputNumDestinations(instance.reactions); - + generateInitializeActionToken(instance.actions); generateSetDeadline(instance.reactions); @@ -4062,9 +3741,13 @@ class CGenerator extends GeneratorBase { // Note that this function is also run once at the end // so that it can deallocate any memory. generateStartTimeStep(instance) + + endScopedBlock(initializeTriggerObjects); + endScopedBlock(startTimeStep); + pr(initializeTriggerObjects, "//***** End initializing " + fullName) } - + /** * Initialize actions by creating a lf_token_t in the self struct. * This has the information required to allocate memory for the action payload. @@ -4080,322 +3763,40 @@ class CGenerator extends GeneratorBase { var payloadSize = "0" if (!type.isUndefined) { - var String typeStr = type.targetType + var String typeStr = types.getTargetType(type) if (isTokenType(type)) { typeStr = typeStr.rootType - } else { - typeStr = type.targetType } if (typeStr !== null && !typeStr.equals("") && !typeStr.equals("void")) { payloadSize = '''sizeof(«typeStr»)''' } } - var nameOfSelfStruct = selfStructName(action.parent); + var selfStruct = CUtil.reactorRef(action.parent); // Create a reference token initialized to the payload size. // This token is marked to not be freed so that the trigger_t struct // always has a reference token. pr(initializeTriggerObjects, ''' - «nameOfSelfStruct»->_lf__«action.name».token = _lf_create_token(«payloadSize»); - «nameOfSelfStruct»->_lf__«action.name».status = absent; + «selfStruct»->_lf__«action.name».token = _lf_create_token(«payloadSize»); + «selfStruct»->_lf__«action.name».status = absent; ''' ) // At the start of each time step, we need to initialize the is_present field // of each action's trigger object to false and free a previously // allocated token if appropriate. This code sets up the table that does that. pr(initializeTriggerObjects, ''' - _lf_tokens_with_ref_count[«startTimeStepTokens»].token - = &«nameOfSelfStruct»->_lf__«action.name».token; - _lf_tokens_with_ref_count[«startTimeStepTokens»].status - = &«nameOfSelfStruct»->_lf__«action.name».status; - _lf_tokens_with_ref_count[«startTimeStepTokens»].reset_is_present = true; - ''') - startTimeStepTokens++ - } - } - } - - /** - * For each output port of the specified reactor, - * set the num_destinations field of port structs on its self struct - * equal to the total number of destination reactors. This is used - * to initialize reference counts in dynamically allocated tokens - * sent to other reactors. - * @param reactor The reactor instance. - */ - private def void generateOutputNumDestinations(ReactorInstance reactor) { - // Reference counts are decremented by each destination reactor - // at the conclusion of a time step. Hence, the initial reference - // count should equal the number of destination _reactors_, not the - // number of destination ports nor the number of destination reactions. - // One of the destination reactors may be the container of this - // instance because it may have a reaction to an output of this instance. - for (output : reactor.outputs) { - for (sendingRange : output.eventualDestinations) { - // Syntax is slightly difference for a multiport output vs. single port. - // For a single port, there should be only one sendingRange. - if (output.isMultiport()) { - val start = sendingRange.startChannel; - val end = sendingRange.startChannel + sendingRange.channelWidth; - // Eliminate the for loop for the case where range.channelWidth == 1, - // a common situation on multiport to bank messaging. - if (sendingRange.channelWidth == 1) { - pr(initializeTriggerObjectsEnd, ''' - «sourceReference(output)»[«start»].num_destinations = «sendingRange.getNumberOfDestinationReactors()»; - ''') - } else { - pr(initializeTriggerObjectsEnd, ''' - for (int i = «start»; i < «end»; i++) { - «sourceReference(output)»[i].num_destinations = «sendingRange.getNumberOfDestinationReactors()»; - } - ''') - } - } else { - pr(initializeTriggerObjectsEnd, ''' - «sourceReference(output)».num_destinations = «sendingRange.getNumberOfDestinationReactors»; - ''') - } - } - } - } - - /** - * For each input port of a contained reactor that receives data - * from one or more of the specified reactions, set the num_destinations - * field of the corresponding port structs on the self struct of - * the reaction's parent reactor equal to the total number of - * destination reactors. This is used to initialize reference - * counts in dynamically allocated tokens sent to other reactors. - * @param reactions The reactions. - */ - private def void generateInputNumDestinations(Iterable reactions) { - // Reference counts are decremented by each destination reactor - // at the conclusion of a time step. Hence, the initial reference - // count should equal the number of destination _reactors_, not the - // number of destination ports nor the number of destination reactions. - // One of the destination reactors may be the container of this - // instance because it may have a reaction to an output of this instance. - - // Since a port may be written to by multiple reactions, - // ensure that this is done only once. - val portsHandled = new HashSet(); - for (reaction : reactions) { - for (port : reaction.effects.filter(PortInstance)) { - if (port.isInput && !portsHandled.contains(port)) { - // Port is an input of a contained reactor that gets data from a reaction of this reactor. - portsHandled.add(port); - - // The input port may itself have multiple destinations. - for (sendingRange : port.eventualDestinations) { - - // Syntax is slightly different for a multiport output vs. single port. - if (port.isMultiport()) { - val start = sendingRange.startChannel; - val end = sendingRange.startChannel + sendingRange.channelWidth; - pr(initializeTriggerObjectsEnd, ''' - for (int i = «start»; i < «end»; i++) { - «sourceReference(port)»[i]->num_destinations = «sendingRange.getNumberOfDestinationReactors»; - } - ''') - } else { - pr(initializeTriggerObjectsEnd, ''' - «sourceReference(port)».num_destinations = «sendingRange.getNumberOfDestinationReactors»; - ''') - } - } - } - } - } - } - - /** - * If any input port of the specified reactor is a multiport - * and is mentioned as an effect of a reaction in its reactors's parent - * (the reaction provides input to a contained reactor), then generate - * code to allocate memory to store the data produced by those reactions. - * The allocated memory is pointed to by a field called - * `_lf_containername.portname` on the self struct of the reactor's parent. - * @param reactor A contained reactor. - */ - private def void generateAllocationForEffectsOnInputs(ReactorInstance reactor) { - // Keep track of ports already handled. There may be more than one reaction - // in the container writing to the port, but we want only one memory allocation. - val portsHandled = new HashSet(); - - // Find parent reactions that mention multiport inputs of this reactor. - for (reaction : reactor.parent.reactions) { - for (effect : reaction.effects.filter(PortInstance)) { - if (effect.isMultiport && reactor.inputs.contains(effect) && !portsHandled.contains(effect)) { - // Port is a multiport input that the parent's reaction is writing to. - portsHandled.add(effect); - - val nameOfSelfStruct = selfStructName(reactor.parent); - var containerName = reactor.name; - val portStructType = variableStructType( - effect.definition, reactor.definition.reactorClass); - - // FIXME: As of now, the following never happens because bank members - // are handled individually. But I plan to fix this, so I'm leaving this - // dead code here. - if (reactor.bankIndex === -2) { - pr(initializeTriggerObjectsEnd, ''' - for (int j = 0; j < «reactor.bankSize»; j++) { - ''') - indent(initializeTriggerObjectsEnd); - containerName += "[j]"; - } - pr(initializeTriggerObjectsEnd, ''' - «nameOfSelfStruct»->_lf_«containerName».«effect.name»_width = «effect.width»; - // Allocate memory to store output of reaction feeding a multiport input of a contained reactor. - «nameOfSelfStruct»->_lf_«containerName».«effect.name» = («portStructType»**)malloc(sizeof(«portStructType»*) - * «nameOfSelfStruct»->_lf_«containerName».«effect.name»_width); - for (int i = 0; i < «nameOfSelfStruct»->_lf_«containerName».«effect.name»_width; i++) { - «nameOfSelfStruct»->_lf_«containerName».«effect.name»[i] = («portStructType»*)calloc(1, sizeof(«portStructType»)); - } - ''') - if (reactor.bankIndex === -2) { - unindent(initializeTriggerObjectsEnd); - pr(initializeTriggerObjectsEnd, ''' - } - ''') - } - } - } - } - } - - /** - * If any output port of the specified reactor is a multiport, then generate code to - * allocate memory to store the data produced by those reactions. - * The allocated memory is pointed to by a field called `_lf_portname`. - * @param reactor A reactor instance. - */ - private def void generateAllocationForEffectsOnOutputs(ReactorInstance reactor) { - for (port : reactor.outputs) { - if (port.isMultiport) { - val nameOfSelfStruct = selfStructName(port.parent); - val portStructType = variableStructType( - port.definition, - port.parent.definition.reactorClass - ) - - pr(initializeTriggerObjectsEnd, ''' - «nameOfSelfStruct»->_lf_«port.name»_width = «port.width»; - «nameOfSelfStruct»->_lf_«port.name» = («portStructType»*)calloc(«nameOfSelfStruct»->_lf_«port.name»_width, - sizeof(«portStructType»)); - «nameOfSelfStruct»->_lf_«port.name»_pointers = («portStructType»**)malloc(sizeof(«portStructType»*) - * «nameOfSelfStruct»->_lf_«port.name»_width); - // Assign each output port pointer to be used in reactions to facilitate user access to output ports - for(int i=0; i < «nameOfSelfStruct»->_lf_«port.name»_width; i++) { - «nameOfSelfStruct»->_lf_«port.name»_pointers[i] = &(«nameOfSelfStruct»->_lf_«port.name»[i]); - } + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count].token + = &«selfStruct»->_lf__«action.name».token; + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count].status + = &«selfStruct»->_lf__«action.name».status; + _lf_tokens_with_ref_count[_lf_tokens_with_ref_count_count++].reset_is_present = true; ''') + startTimeStepTokens += currentFederate.numRuntimeInstances(action.parent); } } } - - /** - * For the specified reaction, for output ports that it writes to, - * set up the arrays that store the output values (if necessary) and - * that are used to trigger downstream reactions if an output is actually - * produced. - * - * NOTE: This method is quite complicated because of the possibility that - * that the reaction is writing to a multiport output or to an - * input port of a contained reactor, and the possibility that that - * the contained reactor is a bank of reactors and that its input port may - * be a multiport. - * - * @param The reaction instance. - */ - private def void generateReactionOutputs(ReactionInstance reaction) { - val nameOfSelfStruct = selfStructName(reaction.parent); - - // Count the output ports and inputs of contained reactors that - // may be set by this reaction. This ignores actions in the effects. - // Collect initialization statements for the output_produced array for the reaction - // to point to the is_present field of the appropriate output. - // These statements must be inserted after the array is malloc'd, - // but we construct them while we are counting outputs. - var outputCount = 0; - val initialization = new StringBuilder() - // The reaction.effects does not contain multiports, but rather the individual - // ports of the multiport. We handle each multiport only once using this set. - val handledMultiports = new LinkedHashSet(); - for (effect : reaction.effects) { - if (effect instanceof PortInstance) { - // Effect is a port. There are six cases. - // 1. The port is an ordinary port contained by the same reactor that contains this reaction. - // 2. The port is a multiport contained by the same reactor that contains reaction. - // 3. The port is an ordinary input port contained by a contained reactor. - // 4. The port is a multiport input contained by a contained reactor. - // 5. The port is an ordinary port contained by a contained bank of reactors. - // 6. The port is an multiport contained by a contained bank of reactors. - // Create the entry in the output_produced array for this port. - // If the port is a multiport, then we need to create an entry for each - // individual port. - if (effect.isMultiport() && !handledMultiports.contains(effect)) { - // The effect is a multiport that has not been handled yet. - handledMultiports.add(effect); - // Point the output_produced field to where the is_present field of the port is. - if (effect.parent === reaction.parent) { - // The port belongs to the same reactor as the reaction. - pr(initialization, ''' - for (int i = 0; i < «effect.width»; i++) { - «nameOfSelfStruct»->_lf__reaction_«reaction.index».output_produced[«outputCount» + i] - = &«nameOfSelfStruct»->«getStackPortMember('''_lf_«effect.name»[i]''', "is_present")»; - } - ''') - } else { - // The port belongs to a contained reactor. - val containerName = effect.parent.name - pr(initialization, ''' - for (int i = 0; i < «nameOfSelfStruct»->_lf_«containerName».«effect.name»_width; i++) { - «nameOfSelfStruct»->_lf__reaction_«reaction.index».output_produced[«outputCount» + i] - = &«nameOfSelfStruct»->_lf_«containerName».«effect.name»[i]->is_present; - } - ''') - } - outputCount += effect.getWidth(); - } else if (!effect.isMultiport()) { - // The effect is not a multiport nor a port contained by a multiport. - if (effect.parent === reaction.parent) { - // The port belongs to the same reactor as the reaction. - pr(initialization, ''' - «nameOfSelfStruct»->_lf__reaction_«reaction.index».output_produced[«outputCount»] - = &«nameOfSelfStruct»->«getStackPortMember('''_lf_«effect.name»''', "is_present")»; - ''') - } else { - // The port belongs to a contained reactor. - pr(initialization, ''' - «nameOfSelfStruct»->_lf__reaction_«reaction.index».output_produced[«outputCount»] - = &«nameOfSelfStruct»->«getStackPortMember('''_lf_«effect.parent.name».«effect.name»''', "is_present")»; - ''') - } - outputCount++ - } - } - } - pr(initializeTriggerObjectsEnd, ''' - // Total number of outputs (single ports and multiport channels) produced by the reaction. - «nameOfSelfStruct»->_lf__reaction_«reaction.index».num_outputs = «outputCount»; - // Allocate arrays for triggering downstream reactions. - if («nameOfSelfStruct»->_lf__reaction_«reaction.index».num_outputs > 0) { - «nameOfSelfStruct»->_lf__reaction_«reaction.index».output_produced - = (bool**)malloc(sizeof(bool*) * «nameOfSelfStruct»->_lf__reaction_«reaction.index».num_outputs); - «nameOfSelfStruct»->_lf__reaction_«reaction.index».triggers - = (trigger_t***)malloc(sizeof(trigger_t**) * «nameOfSelfStruct»->_lf__reaction_«reaction.index».num_outputs); - «nameOfSelfStruct»->_lf__reaction_«reaction.index».triggered_sizes - = (int*)calloc(«nameOfSelfStruct»->_lf__reaction_«reaction.index».num_outputs, sizeof(int)); - } - ''') - pr(initializeTriggerObjectsEnd, ''' - // Initialize the output_produced array. - «initialization.toString» - ''') - } /** * Generate code that is executed while the reactor instance is being initialized. @@ -4422,13 +3823,13 @@ class CGenerator extends GeneratorBase { */ def generateStateVariableInitializations(ReactorInstance instance) { val reactorClass = instance.definition.reactorClass - val nameOfSelfStruct = selfStructName(instance) + val selfRef = CUtil.reactorRef(instance) for (stateVar : reactorClass.toDefinition.stateVars) { val initializer = getInitializer(stateVar, instance) if (stateVar.initialized) { if (stateVar.isOfTimeType) { - pr(initializeTriggerObjects, nameOfSelfStruct + "->" + stateVar.name + " = " + initializer + ";") + pr(initializeTriggerObjects, selfRef + "->" + stateVar.name + " = " + initializer + ";") } else { // If the state is initialized with a parameter, then do not use // a temporary variable. Otherwise, do, because @@ -4437,35 +3838,14 @@ class CGenerator extends GeneratorBase { // is a struct. if (stateVar.isParameterized && stateVar.init.size > 0) { pr(initializeTriggerObjects, - nameOfSelfStruct + "->" + stateVar.name + " = " + initializer + ";") + selfRef + "->" + stateVar.name + " = " + initializer + ";") } else { - var temporaryVariableName = instance.uniqueID + '_initial_' + stateVar.name - // To ensure uniqueness, if this reactor is in a bank, append the bank member index. - if (instance.getBank() !== null) { - temporaryVariableName += "_" + instance.bankIndex - } - // Array type has to be handled specially because C doesn't accept - // type[] as a type designator. - // Use the superclass to avoid [] being replaced by *. - var type = super.getTargetType(stateVar.inferredType) - val matcher = arrayPatternVariable.matcher(type) - if (matcher.find()) { - // If the state type ends in [], then we have to move the [] - // because C is very picky about where this goes. It has to go - // after the variable name. - pr( - initializeTriggerObjects, - "static " + matcher.group(1) + " " + temporaryVariableName + "[] = " + initializer + ";" - ) - } else { - pr( - initializeTriggerObjects, - "static " + type + " " + temporaryVariableName + " = " + initializer + ";" - ) - } - pr( - initializeTriggerObjects, - nameOfSelfStruct + "->" + stateVar.name + " = " + temporaryVariableName + ";" + pr(initializeTriggerObjects, ''' + { // For scoping + static «types.getVariableDeclaration(stateVar.inferredType, "_initial", true)» = «initializer»; + «selfRef»->«stateVar.name» = _initial; + } // End scoping. + ''' ) } } @@ -4481,9 +3861,9 @@ class CGenerator extends GeneratorBase { for (reaction : reactions) { if (reaction.declaredDeadline !== null) { var deadline = reaction.declaredDeadline.maxDelay - val reactionStructName = '''«selfStructName(reaction.parent)»->_lf__reaction_«reaction.index»''' + val selfRef = '''«CUtil.reactorRef(reaction.parent)»->_lf__reaction_«reaction.index»''' pr(initializeTriggerObjects, ''' - «reactionStructName».deadline = «timeInTargetLanguage(deadline)»; + «selfRef».deadline = «deadline.timeInTargetLanguage»; ''') } } @@ -4494,44 +3874,85 @@ class CGenerator extends GeneratorBase { * @param instance The reactor instance. */ def void generateParameterInitialization(ReactorInstance instance) { - var nameOfSelfStruct = selfStructName(instance) - // Array type parameters have to be handled specially. - // Use the superclass getTargetType to avoid replacing the [] with *. + var selfRef = CUtil.reactorRef(instance) for (parameter : instance.parameters) { // NOTE: we now use the resolved literal value. For better efficiency, we could // store constants in a global array and refer to its elements to avoid duplicate // memory allocations. - val targetType = super.getTargetType(parameter.type) - val matcher = arrayPatternVariable.matcher(targetType) - if (matcher.find()) { - // Use an intermediate temporary variable so that parameter dependencies - // are resolved correctly. + // NOTE: If the parameter is initialized with a static initializer for an array + // or struct (the initialization expression is surrounded by { ... }), then we + // have to declare a static variable to ensure that the memory is put in data space + // and not on the stack. + // FIXME: Is there a better way to determine this than the string comparison? + val initializer = getInitializer(parameter); + if (initializer.startsWith("{")) { val temporaryVariableName = parameter.uniqueID pr(initializeTriggerObjects, ''' - static «matcher.group(1)» «temporaryVariableName»[] = «parameter.getInitializer»; - «nameOfSelfStruct»->«parameter.name» = «temporaryVariableName»; + static «types.getVariableDeclaration(parameter.type, temporaryVariableName, true)» = «initializer»; + «selfRef»->«parameter.name» = «temporaryVariableName»; ''') } else { pr(initializeTriggerObjects, ''' - «nameOfSelfStruct»->«parameter.name» = «parameter.getInitializer»; + «selfRef»->«parameter.name» = «initializer»; ''') } } } /** - * Generate code that malloc's memory for an output multiport. - * @param builder The generated code is put into builder - * @param output The output port to be initialized - * @name + * Generate code that mallocs memory for any output multiports. + * @param reactor The reactor instance. */ - def initializeOutputMultiport(StringBuilder builder, Output output, String nameOfSelfStruct, ReactorInstance instance) { - val reactor = instance.definition.reactorClass - pr(builder, ''' - «nameOfSelfStruct»->_lf_«output.name»_width = «multiportWidthSpecInC(output, null, instance)»; - // Allocate memory for multiport output. - «nameOfSelfStruct»->_lf_«output.name» = («variableStructType(output, reactor)»*)calloc(«nameOfSelfStruct»->_lf_«output.name»_width, sizeof(«variableStructType(output, reactor)»)); - ''') + def initializeOutputMultiports(ReactorInstance reactor) { + for (output : reactor.outputs) { + val portRefName = CUtil.portRefName(output); + // If the port is a multiport, create an array. + if (output.isMultiport) { + val portStructType = variableStructType(output); + pr(initializeTriggerObjects, ''' + «portRefName»_width = «output.width»; + // Allocate memory for multiport output. + «portRefName» = («portStructType»*)calloc(«output.width», sizeof(«portStructType»)); + «portRefName»_pointers = («portStructType»**)calloc(«output.width», sizeof(«portStructType»*)); + // Assign each output port pointer to be used in reactions to facilitate user access to output ports + for(int i=0; i < «output.width»; i++) { + «portRefName»_pointers[i] = &(«portRefName»[i]); + } + ''') + } else { + pr(initializeTriggerObjects, ''' + // width of -2 indicates that it is not a multiport. + «CUtil.portRefName(output)»_width = -2; + ''') + } + } + } + + /** + * Allocate memory for inputs. + * @param reactor The reactor. + */ + def initializeInputMultiports(ReactorInstance reactor) { + for (input : reactor.inputs) { + val portRefName = CUtil.portRefName(input) + // If the port is a multiport, create an array. + if (input.isMultiport) { + pr(initializeTriggerObjects, ''' + «portRefName»_width = «input.width»; + // Allocate memory for multiport inputs. + «portRefName» = («variableStructType(input)»**)calloc(«input.width», sizeof(«variableStructType(input)»*)); + // Set inputs by default to an always absent default input. + for (int i = 0; i < «input.width»; i++) { + «portRefName»[i] = &«CUtil.reactorRef(reactor)»->_lf_default__«input.name»; + } + ''') + } else { + pr(initializeTriggerObjects, ''' + // width of -2 indicates that it is not a multiport. + «portRefName»_width = -2; + ''') + } + } } /** @@ -4549,23 +3970,23 @@ class CGenerator extends GeneratorBase { protected def String multiportWidthSpecInC(Port port, Instantiation contained, ReactorInstance reactorInstance) { var result = new StringBuilder() var count = 0 - var selfStruct = "self" + var selfRef = "self" if (reactorInstance !== null) { if (contained !== null) { // Caution: If port belongs to a contained reactor, the self struct needs to be that // of the contained reactor instance, not this container - selfStruct = selfStructName(reactorInstance.getChildReactorInstance(contained)) + selfRef = CUtil.reactorRef(reactorInstance.getChildReactorInstance(contained)) } else { - selfStruct =selfStructName(reactorInstance); + selfRef =CUtil.reactorRef(reactorInstance); } } if (port.widthSpec !== null) { if (!port.widthSpec.ofVariableLength) { for (term : port.widthSpec.terms) { if (term.parameter !== null) { - result.append(selfStruct) + result.append(selfRef) result.append('->') - result.append(getTargetReference(term.parameter)) + result.append(term.parameter.name) } else { count += term.width } @@ -4581,54 +4002,94 @@ class CGenerator extends GeneratorBase { return result.toString } - protected def getInitializer(StateVar state, ReactorInstance parent) { - var list = new LinkedList(); + /** + * Set the reaction priorities based on dependency analysis. + * @param reactor The reactor on which to do this. + * @param builder Where to write the code. + */ + private def boolean setReactionPriorities(ReactorInstance reactor, StringBuilder builder) { + var foundOne = false; - for (i : state?.init) { - if (i.parameter !== null) { - list.add(parent.selfStructName + "->" + i.parameter.name) - } else if (state.isOfTimeType) { - list.add(i.targetTime) - } else { - list.add(i.targetValue) - } - } + // Force calculation of levels if it has not been done. + reactor.assignLevels(); - if (list.size == 1) - return list.get(0) - else - return list.join('{', ', ', '}', [it]) - } - - /** Set the reaction priorities based on dependency analysis. - * @param reactor The reactor on which to do this. - * @param federate A federate to conditionally generate code for - * contained reactors or null if there are no federates. - */ - def void setReactionPriorities(ReactorInstance reactor, FederateInstance federate) { - // Use "reactionToReactionTName" property of reactionInstance - // to set the levels. + // If any reaction has multiple levels, then we need to create + // an array with the levels here, before entering the iteration over banks. + val prolog = new StringBuilder(); + val epilog = new StringBuilder(); for (r : reactor.reactions) { - if (federate === null || federate.contains( - r.definition - )) { - val reactionStructName = '''«selfStructName(r.parent)»->_lf__reaction_«r.index»''' - // xtend doesn't support bitwise operators... - val indexValue = XtendUtil.longOr(r.deadline.toNanoSeconds << 16, r.level) - val reactionIndex = "0x" + Long.toString(indexValue, 16) + "LL" - pr(''' - «reactionStructName».chain_id = «r.chainID.toString»; - // index is the OR of level «r.level» and - // deadline «r.deadline.toNanoSeconds» shifted left 16 bits. - «reactionStructName».index = «reactionIndex»; - ''') - } - } - for (child : reactor.children) { - if (federate.contains(child)) { - setReactionPriorities(child, federate) + if (currentFederate.contains(r.definition)) { + val levels = r.getLevels(); + if (levels.size != 1) { + if (prolog.length() == 0) { + startScopedBlock(prolog); + endScopedBlock(epilog); + } + // Cannot use the above set of levels because it is a set, not a list. + pr(prolog, ''' + int «r.uniqueID»_levels[] = { «r.getLevelsList().join(", ")» }; + ''') + } } } + + val temp = new StringBuilder(); + pr(temp, "// Set reaction priorities for " + reactor.toString()); + + startScopedBlock(temp, reactor, true); + + for (r : reactor.reactions) { + if (currentFederate.contains(r.definition)) { + foundOne = true; + + // The most common case is that all runtime instances of the + // reaction have the same level, so deal with that case + // specially. + val levels = r.getLevels(); + if (levels.size == 1) { + var level = -1; + for (l : levels) { + level = l; + } + // xtend doesn't support bitwise operators... + val indexValue = XtendUtil.longOr(r.deadline.toNanoSeconds << 16, level) + val reactionIndex = "0x" + Long.toString(indexValue, 16) + "LL" + + pr(temp, ''' + «CUtil.reactionRef(r)».chain_id = «r.chainID.toString»; + // index is the OR of level «level» and + // deadline «r.deadline.toNanoSeconds» shifted left 16 bits. + «CUtil.reactionRef(r)».index = «reactionIndex»; + ''') + } else { + val reactionDeadline = "0x" + Long.toString(r.deadline.toNanoSeconds, 16) + "LL" + + pr(temp, ''' + «CUtil.reactionRef(r)».chain_id = «r.chainID.toString»; + // index is the OR of levels[«CUtil.runtimeIndex(r.parent)»] and + // deadline «r.deadline.toNanoSeconds» shifted left 16 bits. + «CUtil.reactionRef(r)».index = («reactionDeadline» << 16) | «r.uniqueID»_levels[«CUtil.runtimeIndex(r.parent)»]; + ''') + } + } + } + for (child : reactor.children) { + if (currentFederate.contains(child)) { + foundOne = setReactionPriorities(child, temp) || foundOne; + } + } + endScopedBlock(temp); + + if (foundOne) { + pr(builder, prolog.toString()); + pr(builder, temp.toString()); + pr(builder, epilog.toString()); + } + return foundOne; + } + + override getTargetTypes() { + return types; } // ////////////////////////////////////////// @@ -4641,7 +4102,7 @@ class CGenerator extends GeneratorBase { * @param port The port to read from */ override generateDelayBody(Action action, VarRef port) { - val ref = generateVarRef(port); + val ref = JavaAstUtils.generateVarRef(port); // Note that the action.type set by the base class is actually // the port type. if (action.inferredType.isTokenType) { @@ -4668,17 +4129,17 @@ class CGenerator extends GeneratorBase { * @param port The port to write to. */ override generateForwardBody(Action action, VarRef port) { - val outputName = generateVarRef(port) + val outputName = JavaAstUtils.generateVarRef(port) if (action.inferredType.isTokenType) { // Forward the entire token and prevent freeing. // Increment the ref_count because it will be decremented // by both the action handling code and the input handling code. ''' «DISABLE_REACTION_INITIALIZATION_MARKER» - self->_lf_«outputName».value = («action.inferredType.targetType»)self->_lf__«action.name».token->value; + self->_lf_«outputName».value = («types.getTargetType(action)»)self->_lf__«action.name».token->value; self->_lf_«outputName».token = (lf_token_t*)self->_lf__«action.name».token; ((lf_token_t*)self->_lf__«action.name».token)->ref_count++; - self->«getStackPortMember('''_lf_«outputName»''', "is_present")» = true; + self->_lf_«outputName».is_present = true; ''' } else { ''' @@ -4720,16 +4181,22 @@ class CGenerator extends GeneratorBase { // If it is "string", then change it to "char*". // This string is dynamically allocated, and type 'string' is to be // used only for statically allocated strings. - if (action.type.targetType == "string") { + // FIXME: Is the getTargetType method not responsible for generating the desired C code + // (e.g., char* rather than string)? If not, what exactly is that method + // responsible for? If generateNetworkReceiverBody has different requirements + // than those that the method was designed to satisfy, should we use a different + // method? The best course of action is not obvious, but we have a pattern of adding + // downstream patches to generated strings rather than fixing them at their source. + if (types.getTargetType(action) == "string") { action.type.code = null action.type.id = "char*" } - if ((receivingPort.variable as Port).type.targetType == "string") { + if (types.getTargetType(receivingPort.variable as Port) == "string") { (receivingPort.variable as Port).type.code = null (receivingPort.variable as Port).type.id = "char*" } - var receiveRef = generatePortRef(receivingPort, receivingBankIndex, receivingChannelIndex) + var receiveRef = CUtil.portRefInReaction(receivingPort, receivingBankIndex, receivingChannelIndex) val result = new StringBuilder() // Transfer the physical time of arrival from the action to the port @@ -4759,11 +4226,11 @@ class CGenerator extends GeneratorBase { } case SupportedSerializers.ROS2: { val portType = (receivingPort.variable as Port).inferredType - var portTypeStr = portType.targetType + var portTypeStr = types.getTargetType(portType) if (isTokenType(portType)) { throw new UnsupportedOperationException("Cannot handle ROS serialization when ports are pointers."); } else if (isSharedPtrType(portType)) { - val matcher = sharedPointerVariable.matcher(portType.targetType) + val matcher = sharedPointerVariable.matcher(portTypeStr) if (matcher.find()) { portTypeStr = matcher.group(1); } @@ -4821,8 +4288,8 @@ class CGenerator extends GeneratorBase { Delay delay, SupportedSerializers serializer ) { - var sendRef = generatePortRef(sendingPort, sendingBankIndex, sendingChannelIndex); - val receiveRef = generateVarRef(receivingPort); // Used for comments only, so no need for bank/multiport index. + var sendRef = CUtil.portRefInReaction(sendingPort, sendingBankIndex, sendingChannelIndex); + val receiveRef = JavaAstUtils.generateVarRef(receivingPort); // Used for comments only, so no need for bank/multiport index. val result = new StringBuilder() result.append(''' // Sending from «sendRef» in federate «sendingFed.name» to «receiveRef» in federate «receivingFed.name» @@ -4884,12 +4351,12 @@ class CGenerator extends GeneratorBase { // string types need to be dealt with specially because they are hidden pointers. // void type is odd, but it avoids generating non-standard expression sizeof(void), // which some compilers reject. - lengthExpression = switch(type.targetType) { + lengthExpression = switch(types.getTargetType(type)) { case 'string': '''strlen(«sendRef»->value) + 1''' case 'void': '0' - default: '''sizeof(«type.targetType»)''' + default: '''sizeof(«types.getTargetType(type)»)''' } - pointerExpression = switch(type.targetType) { + pointerExpression = switch(types.getTargetType(type)) { case 'string': '''(unsigned char*) «sendRef»->value''' default: '''(unsigned char*)&«sendRef»->value''' } @@ -4904,11 +4371,11 @@ class CGenerator extends GeneratorBase { } case SupportedSerializers.ROS2: { var variableToSerialize = sendRef; - var typeStr = type.targetType + var typeStr = types.getTargetType(type) if (isTokenType(type)) { throw new UnsupportedOperationException("Cannot handle ROS serialization when ports are pointers."); } else if (isSharedPtrType(type)) { - val matcher = sharedPointerVariable.matcher(type.targetType) + val matcher = sharedPointerVariable.matcher(typeStr) if (matcher.find()) { typeStr = matcher.group(1); } @@ -4987,7 +4454,7 @@ class CGenerator extends GeneratorBase { ) { // Store the code val result = new StringBuilder(); - var sendRef = generatePortRef(port, sendingBankIndex, sendingChannelIndex); + var sendRef = CUtil.portRefInReaction(port, sendingBankIndex, sendingChannelIndex); // Get the delay literal var String additionalDelayString = @@ -5016,7 +4483,11 @@ class CGenerator extends GeneratorBase { * Add necessary code to the source and necessary build supports to * enable the requested serializer in 'enabledSerializers' */ - override enableSupportForSerialization(CancelIndicator cancelIndicator) { + override enableSupportForSerializationIfApplicable(CancelIndicator cancelIndicator) { + if (!targetConfig.protoFiles.isNullOrEmpty) { + // Enable support for proto serialization + enabledSerializers.add(SupportedSerializers.PROTO) + } for (serializer : enabledSerializers) { switch (serializer) { case SupportedSerializers.NATIVE: { @@ -5058,14 +4529,13 @@ class CGenerator extends GeneratorBase { } } - /** Generate #include of pqueue.c and either reactor.c or reactor_threaded.c - * depending on whether threads are specified in target directive. - * As a side effect, this populates the runCommand and compileCommand - * private variables if such commands are specified in the target directive. + /** + * Generate code that needs appear at the top of the generated + * C file, such as #define and #include statements. */ - override generatePreamble() { + def generatePreamble() { pr(this.defineLogLevel) - + if (isFederated) { // FIXME: Instead of checking // #ifdef FEDERATED, we could @@ -5078,25 +4548,23 @@ class CGenerator extends GeneratorBase { // The coordination is centralized. pr(''' #define FEDERATED_CENTRALIZED - ''') + ''') } else if (targetConfig.coordination === CoordinationType.DECENTRALIZED) { // The coordination is decentralized pr(''' #define FEDERATED_DECENTRALIZED ''') } - } - // Handle target parameters. - // First, if there are federates, then ensure that threading is enabled. - if (isFederated) { + // Handle target parameters. + // First, if there are federates, then ensure that threading is enabled. for (federate : federates) { // The number of threads needs to be at least one larger than the input ports // to allow the federate to wait on all input ports while allowing an additional // worker thread to process incoming messages. if (targetConfig.threads < federate.networkMessageActions.size + 1) { targetConfig.threads = federate.networkMessageActions.size + 1; - } + } } } @@ -5111,22 +4579,24 @@ class CGenerator extends GeneratorBase { } includeTargetLanguageSourceFiles() + + pr("#include \"core/mixed_radix.h\""); // Do this after the above includes so that the preamble can // call built-in functions. - super.generatePreamble() + prComment("Code generated by the Lingua Franca compiler from:") + prComment("file:/" +FileConfig.toUnixString(fileConfig.srcFile)) + if (this.mainDef !== null) { + val mainModel = this.mainDef.reactorClass.toDefinition.eContainer as Model + for (p : mainModel.preambles) { + pr(p.code.toText) + } + } parseTargetParameters() // Make sure src-gen directory exists. fileConfig.getSrcGenPath.toFile.mkdirs - - // FIXME: Probably not the best place to do - // this. - if (!targetConfig.protoFiles.isNullOrEmpty) { - // Enable support for proto serialization - enabledSerializers.add(SupportedSerializers.PROTO) - } } /** @@ -5242,7 +4712,6 @@ class CGenerator extends GeneratorBase { return null as ErrorFileAndLine } - /** * Strip all line directives from the given C code. * @param code The code to remove # line directives from. @@ -5263,228 +4732,484 @@ class CGenerator extends GeneratorBase { } return builder.toString() } - + // ////////////////////////////////////////// // // Private methods. - /** Perform deferred initializations in initialize_trigger_objects. - * @param federate The federate for which we are doing this. + /** + * If the specified port is a multiport, then start a specified iteration + * over the channels of the multiport using as the channel index the + * variable name returned by {@link CUtil.channelIndex(PortInstance)}. + * If the port is not a multiport, do nothing. + * This is required to be followed by {@link endChannelIteration(StringBuilder, PortInstance}. + * @param builder Where to write the code. + * @param port The port. */ - private def doDeferredInitialize(FederateInstance federate) { - // First, populate the trigger tables for each output. - // The entries point to the trigger_t structs for the destination inputs. - pr('// doDeferredInitialize') - - // For outputs that are not primitive types (of form type* or type[]), - // create a default token on the self struct. - createDefaultTokens(main, federate) + private def void startChannelIteration(StringBuilder builder, PortInstance port) { + if (port.isMultiport) { + val channel = CUtil.channelIndexName(port); + pr(builder, ''' + // Port «port.fullName» is a multiport. Iterate over its channels. + for (int «channel» = 0; «channel» < «port.width»; «channel»++) { + ''') + indent(builder); + } + } + + /** + * If the specified port is a multiport, then start a specified iteration + * over the channels of the multiport using as the channel index the + * variable name returned by {@link CUtil.channelIndex(PortInstance)}. + * If the port is not a multiport, do nothing. + * This is required to be followed by {@link endChannelIteration(StringBuilder, PortInstance}. + * @param builder Where to write the code. + * @param port The port. + */ + private def void endChannelIteration(StringBuilder builder, PortInstance port) { + if (port.isMultiport) { + unindent(builder); + pr(builder, "}"); + } + } - // Next, for every input port, populate its "self" struct - // fields with pointers to the output port that sends it data. - connectInputsToOutputs(main, federate) + /** + * Start a scoped block, which is a section of code + * surrounded by curley braces and indented. + * This must be followed by an {@link endScopedBlock(StringBuilder)}. + * @param builder The string builder into which to write. + */ + private def void startScopedBlock(StringBuilder builder) { + pr(builder, "{"); + indent(builder); } /** - * Generate assignments of pointers in the "self" struct of a destination - * port's reactor to the appropriate entries in the "self" struct of the - * source reactor. - * @param instance The reactor instance. - * @param federate The federate for which we are generating code or null - * if there is no federation. + * Start a scoped block for the specified reactor. + * If the reactor is a bank, then this starts a for loop + * that iterates over the bank members using a standard index + * variable whose name is that returned by {@link CUtil.bankIndex(ReactorInstance)}. + * If the reactor is null or is not a bank, then this simply + * starts a scoped block by printing an opening curly brace. + * This also adds a declaration of a pointer to the self + * struct of the reactor or bank member. + * + * This block is intended to be nested, where each block is + * put within a similar block for the reactor's parent. + * This ensures that all (possibly nested) bank index variables + * are defined within the block. + * + * This must be followed by an {@link endScopedBlock(StringBuilder)}. + * + * @param builder The string builder into which to write. + * @param reactor The reactor instance. + * @param restrict For federated execution only, if this is true, then + * skip iterations where the topmost bank member is not in the federate. */ - private def void connectInputsToOutputs(ReactorInstance instance, FederateInstance federate) { - if (!federate.contains(instance)) { - return; - } - pr('''// Connect inputs and outputs for reactor «instance.getFullName».''') - // Iterate over all ports of this reactor that have dependent reactions. - for (input : instance.inputs) { - if (!input.dependentReactions.isEmpty()) { - // Input has reactions. Connect it to its eventual source. - connectPortToEventualSource(input, federate); - } - } - for (output : instance.outputs) { - if (!output.dependentReactions.isEmpty() && output.dependsOnPorts.isEmpty()) { - // Output has reactions and no upstream ports. - // Connect it to its eventual source. - connectPortToEventualSource(output, federate); + protected def void startScopedBlock(StringBuilder builder, ReactorInstance reactor, boolean restrict) { + // NOTE: This is protected because it is used by the PythonGenerator. + if (reactor !== null && reactor.isBank) { + val index = CUtil.bankIndexName(reactor); + if (reactor.depth == 1 && isFederated && restrict) { + // Special case: A bank of federates. Instantiate only the current federate. + startScopedBlock(builder); + pr(builder, ''' + int «index» = «currentFederate.bankIndex»; + ''') + } else { + pr(builder, ''' + // Reactor is a bank. Iterate over bank members. + for (int «index» = 0; «index» < «reactor.width»; «index»++) { + ''') + indent(builder); } + } else { + startScopedBlock(builder); } - for (child : instance.children) { - // In case this is a composite, recurse. - connectInputsToOutputs(child, federate) + } + + /** + * End a scoped block. + * @param builder The string builder into which to write. + */ + protected def void endScopedBlock(StringBuilder builder) { + // NOTE: This is protected because it is used by the PythonGenerator. + unindent(builder); + pr(builder, "}"); + } + + /** + * Start a scoped block to iterate over bank members and + * channels for the specified port with a a variable with + * the name given by count counting the iterations. + * If this port is a multiport, then the channel index + * variable name is that returned by {@link CUtil.channelIndex(PortInstance)}. + * + * This block is intended to be nested, where each block is + * put within a similar block for the reactor's parent. + * + * This is required to be followed by a call to + * {@link endScopedBankChannelIteration(StringBuilder, PortInstance, String)}. + * @param builder Where to write the code. + * @param port The port. + * @param count The variable name to use for the counter, or + * null to not provide a counter. + */ + private def void startScopedBankChannelIteration( + StringBuilder builder, PortInstance port, String count + ) { + if (count !== null) { + startScopedBlock(builder); + pr(builder, '''int «count» = 0;'''); } + startScopedBlock(builder, port.parent, true); + startChannelIteration(builder, port); + } - // Handle inputs that get sent data from a reaction rather than from - // another contained reactor and reactions that are triggered by an - // output of a contained reactor. - connectReactionsToPorts(instance, federate) - - pr('''// END Connect inputs and outputs for reactor «instance.getFullName».''') + /** + * End a scoped block to iterate over bank members and + * channels for the specified port with a a variable with + * the name given by count counting the iterations. + * @param builder Where to write the code. + * @param port The port. + * @param count The variable name to use for the counter, or + * null to not provide a counter. + */ + private def void endScopedBankChannelIteration( + StringBuilder builder, PortInstance port, String count + ) { + if (count !== null) { + pr(builder, count + "++;"); + } + endChannelIteration(builder, port); + endScopedBlock(builder); + if (count !== null) { + endScopedBlock(builder); + } } /** - * Generate assignments of pointers in the "self" struct of a destination - * port's reactor to the appropriate entries in the "self" struct of the - * source reactor. - * @param instance A port with dependant reactions. - * @param federate The federate for which we are generating code or null - * if there is no federation. + * Start a scoped block that iterates over the specified range of port channels. + * + * This must be followed by a call to + * {@link #endScopedRangeBlock(StringBuilder, RuntimeRange)}. + * + * This block should NOT be nested, where each block is + * put within a similar block for the reactor's parent. + * Within the created block, every use of + * {@link CUtil.reactorRef(ReactorInstance, String)} + * must provide the second argument, a runtime index variable name, + * that must match the runtimeIndex parameter given here. + * + * @param builder The string builder into which to write. + * @param range The range of port channels. + * @param runtimeIndex A variable name to use to index the runtime instance of + * either port's parent or the port's parent's parent (if nested is true), or + * null to use the default, "runtime_index". + * @param bankIndex A variable name to use to index the bank of the port's parent or null to use the + * default, the string returned by {@link CUtil.bankIndexName(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to + * use the default, the string returned by {@link CUtil.channelIndexName(PortInstance)}. + * @param nested If true, then the runtimeIndex variable will be set + * to the bank index of the port's parent's parent rather than the + * port's parent. + * @param restrict For federated execution (only), if this argument + * is true, then the iteration will skip over bank members that + * are not in the current federate. */ - private def void connectPortToEventualSource(PortInstance port, FederateInstance federate) { - // Find the sources that send data to this port, - // which could be the same port if it is an input port written to by a reaction - // or it could be an upstream output port. - // If the port is a multiport, then there may be multiple sources covering - // the range of channels. - var startChannel = 0; - for (eventualSource: port.eventualSources()) { - val src = eventualSource.portInstance; - if (src != port && federate.contains(src.parent)) { - // The eventual source is different from the port and is in the federate. - val destStructType = variableStructType( - port.definition as TypedVariable, - port.parent.definition.reactorClass - ) - - // There are four cases, depending on whether the source or - // destination or both are multiports. - if (src.isMultiport()) { - // If the source port is an input port, then we don't want to use the - // address, whereas if it's an output port, we do. - var modifier = "&"; - if (src.isInput()) modifier = ""; - - if (port.isMultiport()) { - // Source and destination are both multiports. - pr(''' - // Connect «src.getFullName» to port «port.getFullName» - { // To scope variable j - int j = «eventualSource.startChannel»; - for (int i = «startChannel»; i < «eventualSource.channelWidth» + «startChannel»; i++) { - «destinationReference(port)»[i] = («destStructType»*)«modifier»«sourceReference(src)»[j++]; - } - } - ''') - startChannel += eventualSource.channelWidth; - } else { - // Source is a multiport, destination is a single port. - pr(''' - // Connect «src.getFullName» to port «port.getFullName» - «destinationReference(port)» = («destStructType»*)«modifier»«sourceReference(src)»[«eventualSource.startChannel»]; - ''') - } - } else if (port.isMultiport()) { - // Source is a single port, Destination is a multiport. - pr(''' - // Connect «src.getFullName» to port «port.getFullName» - «destinationReference(port)»[«startChannel»] = («destStructType»*)&«sourceReference(src)»; + private def void startScopedRangeBlock( + StringBuilder builder, + RuntimeRange range, + String runtimeIndex, + String bankIndex, + String channelIndex, + boolean nested, + boolean restrict + ) { + + pr(builder, ''' + // Iterate over range «range.toString()». + ''') + val ri = (runtimeIndex === null)? "runtime_index" : runtimeIndex; + val ci = (channelIndex === null)? CUtil.channelIndexName(range.instance) : channelIndex; + val bi = (bankIndex === null)? CUtil.bankIndexName(range.instance.parent) : bankIndex; + val rangeMR = range.startMR(); + val sizeMR = rangeMR.getDigits().size(); + val nestedLevel = (nested) ? 2 : 1; + + startScopedBlock(builder); + if (range.width > 1) { + pr(builder, ''' + int range_start[] = { «rangeMR.getDigits().join(", ")» }; + int range_radixes[] = { «rangeMR.getRadixes().join(", ")» }; + int permutation[] = { «range.permutation().join(", ")» }; + mixed_radix_int_t range_mr = { + «sizeMR», + range_start, + range_radixes, + permutation + }; + for (int range_count = «range.start»; range_count < «range.start» + «range.width»; range_count++) { + '''); + indent(builder); + pr(builder, ''' + int «ri» = mixed_radix_parent(&range_mr, «nestedLevel»); // Runtime index. + int «ci» = range_mr.digits[0]; // Channel index. + int «bi» = «IF sizeMR <= 1»0«ELSE»range_mr.digits[1]«ENDIF»; // Bank index. + ''') + if (isFederated) { + if (restrict) { + // In case we have a bank of federates. Need that iteration + // only cover the one federate. The last digit of the mixed-radix + // number is the bank index (or 0 if this is not a bank of federates). + pr(builder, ''' + if (range_mr.digits[range_mr.size - 1] == «currentFederate.bankIndex») { ''') - startChannel++; + indent(builder); } else { - // Both ports are single ports. - pr(''' - // Connect «src.getFullName» to port «port.getFullName» - «destinationReference(port)» = («destStructType»*)&«sourceReference(src)»; + startScopedBlock(builder); + } + } + } else { + val ciValue = rangeMR.getDigits().get(0); + val riValue = rangeMR.get(nestedLevel); + val biValue = (sizeMR > 1)? rangeMR.getDigits().get(1) : 0; + if (isFederated) { + if (restrict) { + // Special case. Have a bank of federates. Need that iteration + // only cover the one federate. The last digit of the mixed-radix + // number identifies the bank member (or is 0 if not within a bank). + pr(builder, ''' + if («rangeMR.get(sizeMR - 1)» == «currentFederate.bankIndex») { ''') + indent(builder); + } else { + startScopedBlock(builder); } } + pr(builder, ''' + int «ri» = «riValue»; // Runtime index. + int «ci» = «ciValue»; // Channel index. + int «bi» = «biValue»; // Bank index. + int range_count = 0; + ''') + } + } + + /** + * End a scoped block for the specified range. + * @param builder The string builder into which to write. + * @param range The send range. + */ + private def void endScopedRangeBlock(StringBuilder builder, RuntimeRange range) { + if (isFederated) { + // Terminate the if statement or block (if not restrict). + endScopedBlock(builder); + } + if (range.width > 1) { + pr(builder, "mixed_radix_incr(&range_mr);"); + endScopedBlock(builder); // Terminate for loop. } + endScopedBlock(builder); } + static val sc = "src_channel"; + static val sb = "src_bank"; + static val sr = "src_runtime"; + static val dc = "dst_channel"; + static val db = "dst_bank"; + static val dr = "dst_runtime"; + /** - * Connect inputs that get sent data from a reaction rather than from - * another contained reactor and reactions that are triggered by an - * output of a contained reactor. - * @param instance The reactor instance that contains the reactions. - * @param fedeate The federate instance. + * Start a scoped block that iterates over the specified pair of ranges. + * The destination range can be wider than the source range, in which case the + * source range is reused until the destination range is filled. + * The following integer variables will be defined within the scoped block: + * + * * src_channel: The channel index for the source. + * * src_bank: The bank index of the source port's parent. + * * src_runtime: The runtime index of the source port's parent or + * the parent's parent (if the source is an input). + * + * * dst_channel: The channel index for the destination. + * * dst_bank: The bank index of the destination port's parent. + * * dst_runtime: The runtime index of the destination port's parent or + * the parent's parent (if destination is an output). + * + * For convenience, the above variable names are defined in the private + * class variables sc, sb, sr, and dc, db, dr. + * + * This block should NOT be nested, where each block is + * put within a similar block for the reactor's parent. + * Within the created block, every use of + * {@link CUtil.reactorRef(ReactorInstance, String, String)} + * and related functions must provide the above variable names. + * + * This must be followed by a call to + * {@link #endScopedRangeBlock(StringBuilder, SendRange, RuntimeRange)}. + * + * @param builder The string builder into which to write. + * @param srcRange The send range. + * @param dstRange The destination range. */ - private def connectReactionsToPorts(ReactorInstance instance, FederateInstance federate) { - for (reaction : instance.reactions) { - // First handle the effects that are inputs of contained reactors. - for (port : reaction.effects.filter(PortInstance)) { - if (port.definition instanceof Input) { - // This reaction is sending to an input. Must be - // the input of a contained reactor. If the contained reactor is - // not in the federate, then we don't do anything here. - if (federate.contains(port.parent)) { - val destStructType = variableStructType( - port.definition as TypedVariable, - port.parent.definition.reactorClass - ) - if (port.isMultiport()) { - pr(''' - // Connect «port», which gets data from reaction «reaction.index» - // of «instance.getFullName», to «port.getFullName». - for (int i = 0; i < «port.width»; i++) { - «destinationReference(port)»[i] = («destStructType»*)«sourceReference(port)»[i]; - } - ''') - } else { - pr(''' - // Connect «port», which gets data from reaction «reaction.index» - // of «instance.getFullName», to «port.getFullName». - «destinationReference(port)» = («destStructType»*)&«sourceReference(port)»; - ''') - } - // FIXME: Don't we also to set set the destination reference for more - // deeply contained ports? + private def void startScopedRangeBlock( + StringBuilder builder, + SendRange srcRange, + RuntimeRange dstRange + ) { + val srcRangeMR = srcRange.startMR(); + val srcSizeMR = srcRangeMR.radixes.size(); + val srcNestedLevel = (srcRange.instance.isInput) ? 2 : 1; + val dstNested = dstRange.instance.isOutput; + + pr(builder, ''' + // Iterate over ranges «srcRange.toString» and «dstRange.toString». + ''') + + if (isFederated && srcRange.width == 1) { + // Skip this whole block if the src is not in the federate. + pr(builder, ''' + if («srcRangeMR.get(srcRangeMR.numDigits() - 1)» == «currentFederate.bankIndex») { + ''') + indent(builder); + } else { + startScopedBlock(builder); + } + + if (srcRange.width > 1) { + pr(builder, ''' + int src_start[] = { «srcRangeMR.getDigits().join(", ")» }; + int src_value[] = { «srcRangeMR.getDigits().join(", ")» }; // Will be incremented. + int src_radixes[] = { «srcRangeMR.getRadixes().join(", ")» }; + int src_permutation[] = { «srcRange.permutation().join(", ")» }; + mixed_radix_int_t src_range_mr = { + «srcSizeMR», + src_value, + src_radixes, + src_permutation + }; + '''); + } else { + val ciValue = srcRangeMR.getDigits().get(0); + val biValue = (srcSizeMR > 1)? srcRangeMR.getDigits().get(1) : 0; + val riValue = srcRangeMR.get(srcNestedLevel); + pr(builder, ''' + int «sr» = «riValue»; // Runtime index. + int «sc» = «ciValue»; // Channel index. + int «sb» = «biValue»; // Bank index. + ''') + } + + startScopedRangeBlock(builder, dstRange, dr, db, dc, dstNested, true); + + if (srcRange.width > 1) { + pr(builder, ''' + int «sr» = mixed_radix_parent(&src_range_mr, «srcNestedLevel»); // Runtime index. + int «sc» = src_range_mr.digits[0]; // Channel index. + int «sb» = «IF srcSizeMR <= 1»0«ELSE»src_range_mr.digits[1]«ENDIF»; // Bank index. + ''') + } + + // The above startScopedRangeBlock() call will skip any iteration where the destination + // is a bank member is not in the federation. Here, we skip any iteration where the + // source is a bank member not in the federation. + if (isFederated && srcRange.width > 1) { + // The last digit of the mixed radix + // number identifies the bank (or is 0 if no bank). + pr(builder, ''' + if (src_range_mr.digits[src_range_mr.size - 1] == «currentFederate.bankIndex») { + ''') + indent(builder); + } + } + + /** + * End a scoped block that iterates over the specified pair of ranges. + * + * @param builder The string builder into which to write. + * @param srcRange The send range. + * @param dstRange The destination range. + */ + private def void endScopedRangeBlock( + StringBuilder builder, + SendRange srcRange, + RuntimeRange dstRange + ) { + // Do not use endScopedRangeBlock because we need things nested. + if (isFederated) { + if (srcRange.width > 1) { + // Terminate the if statement. + endScopedBlock(builder); + } + // Terminate the if statement or block (if not restrict). + endScopedBlock(builder); + } + if (srcRange.width > 1) { + pr(builder, ''' + mixed_radix_incr(&src_range_mr); + if (mixed_radix_to_int(&src_range_mr) >= «srcRange.start» + «srcRange.width») { + // Start over with the source. + for (int i = 0; i < src_range_mr.size; i++) { + src_range_mr.digits[i] = src_start[i]; } } - } - // Next handle the sources that are outputs of contained reactors. - for (port : reaction.sources.filter(PortInstance)) { - if (port.definition instanceof Output) { - // This reaction is receiving data from an output - // of a contained reactor. If the contained reactor is - // not in the federate, then we don't do anything here. - if (federate.contains(port.parent)) { - val destStructType = variableStructType( - port.definition as TypedVariable, - port.parent.definition.reactorClass - ) - // The port may be deeper in the hierarchy. - var portChannelCount = 0; - for (eventualSource: port.eventualSources()) { - val sourcePort = eventualSource.portInstance - if (sourcePort.isMultiport && port.isMultiport) { - // Both source and destination are multiports. - pr(''' - // Record output «sourcePort.getFullName», which triggers reaction «reaction.index» - // of «instance.getFullName», on its self struct. - for (int i = 0; i < «eventualSource.channelWidth»; i++) { - «reactionReference(port)»[i + «portChannelCount»] = («destStructType»*)&«sourceReference(sourcePort)»[i + «eventualSource.startChannel»]; - } - ''') - portChannelCount += eventualSource.channelWidth; - } else if (sourcePort.isMultiport) { - // Destination is not a multiport, so the channelWidth of the source port should be 1. - pr(''' - // Record output «sourcePort.getFullName», which triggers reaction «reaction.index» - // of «instance.getFullName», on its self struct. - «reactionReference(port)» = («destStructType»*)&«sourceReference(sourcePort)»[«eventualSource.startChannel»]; - ''') - portChannelCount++; - } else if (port.isMultiport) { - // Source is not a multiport, but the destination is. - pr(''' - // Record output «sourcePort.getFullName», which triggers reaction «reaction.index» - // of «instance.getFullName», on its self struct. - «reactionReference(port)»[«portChannelCount»] = («destStructType»*)&«sourceReference(sourcePort)»; - ''') - portChannelCount++; - } else { - // Neither is a multiport. - pr(''' - // Record output «sourcePort.getFullName», which triggers reaction «reaction.index» - // of «instance.getFullName», on its self struct. - «reactionReference(port)» = («destStructType»*)&«sourceReference(sourcePort)»; - ''') - portChannelCount++; - } - } + '''); + } + if (dstRange.width > 1) { + pr(builder, "mixed_radix_incr(&range_mr);"); + endScopedBlock(builder); // Terminate for loop. + } + // Terminate unconditional scope block in startScopedRangeBlock calls. + endScopedBlock(builder); + endScopedBlock(builder); + } + + /** + * Generate assignments of pointers in the "self" struct of a destination + * port's reactor to the appropriate entries in the "self" struct of the + * source reactor. If this port is an input, then it is being written + * to by a reaction belonging to the parent of the port's parent. + * If it is an output, then it is being written to by a reaction belonging + * to the port's parent. + * @param port A port that is written to by reactions. + */ + private def void connectPortToEventualDestinations(PortInstance src) { + if (!currentFederate.contains(src.parent)) return; + for (srcRange: src.eventualDestinations()) { + for (dstRange : srcRange.destinations) { + val dst = dstRange.instance; + val destStructType = variableStructType(dst) + + // NOTE: For federated execution, dst.parent should always be contained + // by the currentFederate because an AST transformation removes connections + // between ports of distinct federates. So the following check is not + // really necessary. + if (currentFederate.contains(dst.parent)) { + + val mod = (dst.isMultiport || (src.isInput && src.isMultiport))? "" : "&"; + + pr(''' + // Connect «srcRange.toString» to port «dstRange.toString» + ''') + startScopedRangeBlock(code, srcRange, dstRange); + + if (src.isInput) { + // Source port is written to by reaction in port's parent's parent + // and ultimate destination is further downstream. + pr(''' + «CUtil.portRef(dst, dr, db, dc)» = («destStructType»*)«mod»«CUtil.portRefNested(src, sr, sb, sc)»; + ''') + } else if (dst.isOutput) { + // An output port of a contained reactor is triggering a reaction. + pr(''' + «CUtil.portRefNested(dst, dr, db, dc)» = («destStructType»*)&«CUtil.portRef(src, sr, sb, sc)»; + ''') + } else { + // An output port is triggering + pr(''' + «CUtil.portRef(dst, dr, db, dc)» = («destStructType»*)&«CUtil.portRef(src, sr, sb, sc)»; + ''') } + endScopedRangeBlock(code, srcRange, dstRange); } } } @@ -5520,9 +5245,9 @@ class CGenerator extends GeneratorBase { pr(action, builder, ''' if («action.name»->has_value) { «IF type.isTokenType» - «action.name»->value = («type.targetType»)«tokenPointer»->value; + «action.name»->value = («types.getTargetType(type)»)«tokenPointer»->value; «ELSE» - «action.name»->value = *(«type.targetType»*)«tokenPointer»->value; + «action.name»->value = *(«types.getTargetType(type)»*)«tokenPointer»->value; «ENDIF» } ''') @@ -5552,12 +5277,12 @@ class CGenerator extends GeneratorBase { // depending on whether the input is mutable, whether it is a multiport, // and whether it is a token type. // Easy case first. - if (!input.isMutable && !inputType.isTokenType && !input.isMultiport) { + if (!input.isMutable && !inputType.isTokenType && !JavaAstUtils.isMultiport(input)) { // Non-mutable, non-multiport, primitive type. pr(builder, ''' «structType»* «input.name» = self->_lf_«input.name»; ''') - } else if (input.isMutable && !inputType.isTokenType && !input.isMultiport) { + } else if (input.isMutable && !inputType.isTokenType && !JavaAstUtils.isMultiport(input)) { // Mutable, non-multiport, primitive type. pr(builder, ''' // Mutable input, so copy the input into a temporary variable. @@ -5565,18 +5290,18 @@ class CGenerator extends GeneratorBase { «structType» _lf_tmp_«input.name» = *(self->_lf_«input.name»); «structType»* «input.name» = &_lf_tmp_«input.name»; ''') - } else if (!input.isMutable && inputType.isTokenType && !input.isMultiport) { + } else if (!input.isMutable && inputType.isTokenType && !JavaAstUtils.isMultiport(input)) { // Non-mutable, non-multiport, token type. pr(builder, ''' «structType»* «input.name» = self->_lf_«input.name»; if («input.name»->is_present) { «input.name»->length = «input.name»->token->length; - «input.name»->value = («inputType.targetType»)«input.name»->token->value; + «input.name»->value = («types.getTargetType(inputType)»)«input.name»->token->value; } else { «input.name»->length = 0; } ''') - } else if (input.isMutable && inputType.isTokenType && !input.isMultiport) { + } else if (input.isMutable && inputType.isTokenType && !JavaAstUtils.isMultiport(input)) { // Mutable, non-multiport, token type. pr(builder, ''' // Mutable input, so copy the input struct into a temporary variable. @@ -5594,12 +5319,12 @@ class CGenerator extends GeneratorBase { «input.name»->token->next_free = _lf_more_tokens_with_ref_count; _lf_more_tokens_with_ref_count = «input.name»->token; } - «input.name»->value = («inputType.targetType»)«input.name»->token->value; + «input.name»->value = («types.getTargetType(inputType)»)«input.name»->token->value; } else { «input.name»->length = 0; } ''') - } else if (!input.isMutable && input.isMultiport) { + } else if (!input.isMutable && JavaAstUtils.isMultiport(input)) { // Non-mutable, multiport, primitive or token type. pr(builder, ''' «structType»** «input.name» = self->_lf_«input.name»; @@ -5609,9 +5334,9 @@ class CGenerator extends GeneratorBase { pr(builder, ''' // Mutable multiport input, so copy the input structs // into an array of temporary variables on the stack. - «structType» _lf_tmp_«input.name»[«input.multiportWidthExpression»]; - «structType»* «input.name»[«input.multiportWidthExpression»]; - for (int i = 0; i < «input.multiportWidthExpression»; i++) { + «structType» _lf_tmp_«input.name»[«CUtil.multiportWidthExpression(input)»]; + «structType»* «input.name»[«CUtil.multiportWidthExpression(input)»]; + for (int i = 0; i < «CUtil.multiportWidthExpression(input)»; i++) { «input.name»[i] = &_lf_tmp_«input.name»[i]; _lf_tmp_«input.name»[i] = *(self->_lf_«input.name»[i]); // If necessary, copy the tokens. @@ -5627,7 +5352,7 @@ class CGenerator extends GeneratorBase { «input.name»[i]->token->next_free = _lf_more_tokens_with_ref_count; _lf_more_tokens_with_ref_count = «input.name»[i]->token; } - «input.name»[i]->value = («inputType.targetType»)«input.name»[i]->token->value; + «input.name»[i]->value = («types.getTargetType(inputType)»)«input.name»[i]->token->value; } else { «input.name»[i]->length = 0; } @@ -5638,9 +5363,9 @@ class CGenerator extends GeneratorBase { pr(builder, ''' // Mutable multiport input, so copy the input structs // into an array of temporary variables on the stack. - «structType» _lf_tmp_«input.name»[«input.multiportWidthExpression»]; - «structType»* «input.name»[«input.multiportWidthExpression»]; - for (int i = 0; i < «input.multiportWidthExpression»; i++) { + «structType» _lf_tmp_«input.name»[«CUtil.multiportWidthExpression(input)»]; + «structType»* «input.name»[«CUtil.multiportWidthExpression(input)»]; + for (int i = 0; i < «CUtil.multiportWidthExpression(input)»; i++) { «input.name»[i] = &_lf_tmp_«input.name»[i]; // Copy the struct, which includes the value. _lf_tmp_«input.name»[i] = *(self->_lf_«input.name»[i]); @@ -5690,7 +5415,7 @@ class CGenerator extends GeneratorBase { val reactorName = port.container.name // First define the struct containing the output value and indicator // of its presence. - if (!output.isMultiport) { + if (!JavaAstUtils.isMultiport(output)) { // Output is not a multiport. pr(structBuilder, ''' «portStructType»* «output.name»; @@ -5711,7 +5436,7 @@ class CGenerator extends GeneratorBase { «reactorName»[i].«output.name» = self->_lf_«reactorName»[i].«output.name»; } ''') - if (output.isMultiport) { + if (JavaAstUtils.isMultiport(output)) { pr(builder, ''' for (int i = 0; i < «port.container.name»_width; i++) { «reactorName»[i].«output.name»_width = self->_lf_«reactorName»[i].«output.name»_width; @@ -5723,7 +5448,7 @@ class CGenerator extends GeneratorBase { pr(builder, ''' «reactorName».«output.name» = self->_lf_«reactorName».«output.name»; ''') - if (output.isMultiport) { + if (JavaAstUtils.isMultiport(output)) { pr(builder, ''' «reactorName».«output.name»_width = self->_lf_«reactorName».«output.name»_width; ''') @@ -5755,7 +5480,7 @@ class CGenerator extends GeneratorBase { variableStructType(output, decl) : variableStructType(output, effect.container.reactorClass) - if (!output.isMultiport) { + if (!JavaAstUtils.isMultiport(output)) { // Output port is not a multiport. pr(builder, ''' «outputStructType»* «output.name» = &self->_lf_«output.name»; @@ -5799,7 +5524,7 @@ class CGenerator extends GeneratorBase { structs.put(definition, structBuilder) } val inputStructType = variableStructType(input, definition.reactorClass) - if (!input.isMultiport) { + if (!JavaAstUtils.isMultiport(input)) { // Contained reactor's input is not a multiport. pr(structBuilder, ''' «inputStructType»* «input.name»; @@ -5840,30 +5565,9 @@ class CGenerator extends GeneratorBase { } } } - - /** - * Override the base class to replace a type of form type[] with type*. - * @param type The type. - */ - override String getTargetType(InferredType type) { - var result = super.getTargetType(type) - val matcher = arrayPatternVariable.matcher(result) - if (matcher.find()) { - return matcher.group(1) + '*' - } - return result - } protected def isSharedPtrType(InferredType type) { - if (type.isUndefined) - return false - val targetType = type.targetType - val matcher = sharedPointerVariable.matcher(targetType) - if (matcher.find()) { - true - } else { - false - } + return !type.isUndefined && sharedPointerVariable.matcher(types.getTargetType(type)).find() } /** Given a type for an input or output, return true if it should be @@ -5875,18 +5579,13 @@ class CGenerator extends GeneratorBase { protected def isTokenType(InferredType type) { if (type.isUndefined) return false - val targetType = type.targetType - if (targetType.trim.matches("^\\w*\\[\\s*\\]$") || targetType.trim.endsWith('*')) { - true - } else { - false - } + // This is a hacky way to do this. It is now considered to be a bug (#657) + val targetType = types.getVariableDeclaration(type, "", false) + return type.isVariableSizeList || targetType.contains("*") } - /** If the type specification of the form type[] or - * type*, return the type. Otherwise remove the code delimiter, - * if there is one, and otherwise just return the argument - * unmodified. + /** If the type specification of the form {@code type[]}, + * {@code type*}, or {@code type}, return the type. * @param type A string describing the type. */ private def rootType(String type) { @@ -5947,61 +5646,13 @@ class CGenerator extends GeneratorBase { pr(builder, line) } } + + // Regular expression pattern for shared_ptr types. + static final Pattern sharedPointerVariable = Pattern.compile("^std::shared_ptr<(\\S+)>$"); - /** For each output that has a token type (type* or type[]), - * create a default token and put it on the self struct. - * @param parent The container reactor. - * @param federate The federate, or null if there is no federation. - */ - private def void createDefaultTokens(ReactorInstance parent, FederateInstance federate) { - for (containedReactor : parent.children) { - // Do this only for reactors in the federate. - if (federate.contains(containedReactor)) { - var nameOfSelfStruct = selfStructName(containedReactor) - for (output : containedReactor.outputs) { - val type = (output.definition as Output).inferredType - if (type.isTokenType) { - // Create the template token that goes in the trigger struct. - // Its reference count is zero, enabling it to be used immediately. - var rootType = type.targetType.rootType - // If the rootType is 'void', we need to avoid generating the code - // 'sizeof(void)', which some compilers reject. - val size = (rootType == 'void') ? '0' : '''sizeof(«rootType»)''' - if (output.isMultiport()) { - pr(''' - for (int i = 0; i < «output.width»; i++) { - «nameOfSelfStruct»->_lf_«output.name»[i].token = _lf_create_token(«size»); - } - ''') - } else { - pr(''' - «nameOfSelfStruct»->_lf_«output.name».token = _lf_create_token(«size»); - ''') - } - } - } - // In case this is a composite, handle its contained reactors. - createDefaultTokens(containedReactor, federate) - } - } - } - - // Regular expression pattern for array types with specified length. - // \s is whitespace, \w is a word character (letter, number, or underscore). - // For example, for "foo[10]", the first match will be "foo" and the second "[10]". - static final Pattern arrayPatternFixed = Pattern.compile("^\\s*(?:/\\*.*?\\*/)?\\s*(\\w+)\\s*(\\[[0-9]+\\])\\s*$"); - - // Regular expression pattern for array types with unspecified length. - // \s is whitespace, \w is a word character (letter, number, or underscore). - // For example, for "foo[]", the first match will be "foo". - static final Pattern arrayPatternVariable = Pattern.compile("^\\s*(?:/\\*.*?\\*/)?\\s*(\\w+)\\s*\\[\\]\\s*$"); - - // Regular expression pattern for shared_ptr types. - static final Pattern sharedPointerVariable = Pattern.compile("^std::shared_ptr<(\\S+)>$"); - protected static var DISABLE_REACTION_INITIALIZATION_MARKER = '// **** Do not include initialization code in this reaction.' - + public static var UNDEFINED_MIN_SPACING = -1 /** @@ -6014,38 +5665,786 @@ class CGenerator extends GeneratorBase { override getTarget() { return Target.C } + + override getNetworkBufferType() '''uint8_t*''' + + /** + * Return a C expression that can be used to initialize the specified + * state variable within the specified parent. If the state variable + * initializer refers to parameters of the parent, then those parameter + * references are replaced with accesses to the self struct of the parent. + */ + protected def String getInitializer(StateVar state, ReactorInstance parent) { + var list = new LinkedList(); + + for (i : state?.init) { + if (i.parameter !== null) { + list.add(CUtil.reactorRef(parent) + "->" + i.parameter.name) + } else if (state.isOfTimeType) { + list.add(i.targetTime) + } else { + list.add(i.targetTime) + } + } - override getTargetTimeType() '''interval_t''' + if (list.size == 1) { + return list.get(0) + } else { + return list.join('{', ', ', '}', [it]) + } + } + + /** + * Return a C expression that can be used to initialize the specified + * parameter instance. If the parameter initializer refers to other + * parameters, then those parameter references are replaced with + * accesses to the self struct of the parents of those parameters. + */ + protected def String getInitializer(ParameterInstance p) { + // Handle the bank_index parameter. + if (p.name.equals("bank_index")) { + return CUtil.bankIndex(p.parent); + } + + // Handle overrides in the intantiation. + // In case there is more than one assignment to this parameter, we need to + // find the last one. + var lastAssignment = null as Assignment; + for (assignment: p.parent.definition.parameters) { + if (assignment.lhs == p.definition) { + lastAssignment = assignment; + } + } + var list = new LinkedList(); + if (lastAssignment !== null) { + // The parameter has an assignment. + // Right hand side can be a list. Collect the entries. + for (value: lastAssignment.rhs) { + if (value.parameter !== null) { + // The parameter is being assigned a parameter value. + // Assume that parameter belongs to the parent's parent. + // This should have been checked by the validator. + list.add(CUtil.reactorRef(p.parent.parent) + "->" + value.parameter.name); + } else { + list.add(value.targetTime) + } + } + } else { + // there was no assignment in the instantiation. So just use the + // parameter's initial value. + for (i : p.parent.initialParameterValue(p.definition)) { + if (p.definition.isOfTimeType) { + list.add(i.targetTime) + } else { + list.add(i.targetTime) + } + } + } + if (list.size == 1) { + return list.get(0) + } else { + return list.join('{', ', ', '}', [it]) + } + } + + override generateDelayGeneric() { + throw new UnsupportedOperationException("TODO: auto-generated method stub") + } - override getTargetTagType() '''tag_t''' + /** + * Perform initialization functions that must be performed after + * all reactor runtime instances have been created. + * This function creates nested loops over nested banks. + * @param reactor The container. + * @param federate The federate (used to determine whether a + * reaction belongs to the federate). + */ + private def void deferredInitialize( + ReactorInstance reactor, Iterable reactions + ) { + if (!currentFederate.contains(reactor)) { + return; + } + + pr('''// **** Start deferred initialize for «reactor.getFullName()»''') + + // First batch of initializations is within a for loop iterating + // over bank members for the reactor's parent. + startScopedBlock(code, reactor, true); + + // If the child has a multiport that is an effect of some reaction in its container, + // then we have to generate code to allocate memory for arrays pointing to + // its data. If the child is a bank, then memory is allocated for the entire + // bank width because a reaction cannot specify which bank members it writes + // to so we have to assume it can write to any. + deferredAllocationForEffectsOnInputs(reactor); - override getTargetUndefinedType() '''/* «errorReporter.reportError("undefined type")» */''' + deferredReactionMemory(reactions); - override getTargetFixedSizeListType(String baseType, int size) '''«baseType»[«size»]''' + // For outputs that are not primitive types (of form type* or type[]), + // create a default token on the self struct. + deferredCreateDefaultTokens(reactor); + + for (child: reactor.children) { + if (currentFederate.contains(child)) { + deferredInitialize(child, child.reactions); + } + } + + endScopedBlock(code) - override String getTargetVariableSizeListType( - String baseType) '''«baseType»[]''' + pr('''// **** End of deferred initialize for «reactor.getFullName()»''') + } + + /** + * Perform initialization functions that must be performed after + * all reactor runtime instances have been created. + * This function does not create nested loops over nested banks, + * so each function it calls must handle its own iteration + * over all runtime instance. + * @param reactor The container. + * @param federate The federate (used to determine whether a + * reaction belongs to the federate). + */ + private def void deferredInitializeNonNested( + ReactorInstance reactor, Iterable reactions + ) { + pr('''// **** Start non-nested deferred initialize for «reactor.getFullName()»''') + + // Initialize the num_destinations fields of port structs on the self struct. + // This needs to be outside the above scoped block because it performs + // its own iteration over ranges. + deferredInputNumDestinations(reactions); + // Second batch of initializes cannot be within a for loop + // iterating over bank members because they iterate over send + // ranges which may span bank members. + deferredOutputNumDestinations(reactor); // NOTE: Does nothing for top level. + deferredFillTriggerTable(reactions); - override getNetworkBufferType() '''uint8_t*''' + deferredOptimizeForSingleDominatingReaction(reactor); + + for (child: reactor.children) { + if (currentFederate.contains(child)) { + deferredInitializeNonNested(child, child.reactions); + } + } + + pr('''// **** End of non-nested deferred initialize for «reactor.getFullName()»''') + } + + /** + * Generate assignments of pointers in the "self" struct of a destination + * port's reactor to the appropriate entries in the "self" struct of the + * source reactor. This has to be done after all reactors have been created + * because inputs point to outputs that are arbitrarily far away. + * @param instance The reactor instance. + */ + private def void deferredConnectInputsToOutputs(ReactorInstance instance) { + pr('''// Connect inputs and outputs for reactor «instance.getFullName».''') + + // Iterate over all ports of this reactor that depend on reactions. + for (input : instance.inputs) { + if (!input.dependsOnReactions.isEmpty()) { + // Input is written to by reactions in the parent of the port's parent. + connectPortToEventualDestinations(input); + } + } + for (output : instance.outputs) { + if (!output.dependsOnReactions.isEmpty()) { + // Output is written to by reactions in the port's parent. + connectPortToEventualDestinations(output); + } + } + for (child: instance.children) { + deferredConnectInputsToOutputs(child); + } + } - protected def String getInitializer(ParameterInstance p) { + /** + * For each output of the specified reactor that has a token type + * (type* or type[]), create a default token and put it on the self struct. + * @param parent The reactor. + */ + private def void deferredCreateDefaultTokens(ReactorInstance reactor) { + // Look for outputs with token types. + for (output : reactor.outputs) { + val type = (output.definition as Output).inferredType; + if (type.isTokenType) { + // Create the template token that goes in the trigger struct. + // Its reference count is zero, enabling it to be used immediately. + var rootType = types.getTargetType(type).rootType; + // If the rootType is 'void', we need to avoid generating the code + // 'sizeof(void)', which some compilers reject. + val size = (rootType == 'void') ? '0' : '''sizeof(«rootType»)''' + startChannelIteration(code, output); + pr(''' + «CUtil.portRef(output)».token = _lf_create_token(«size»); + ''') + endChannelIteration(code, output); + } + } + } + + /** + * For each output port of the specified reactor, + * set the num_destinations field of port structs on its self struct + * equal to the total number of destination reactors. This is used + * to initialize reference counts in dynamically allocated tokens + * sent to other reactors. + * @param reactor The reactor instance. + */ + private def void deferredOutputNumDestinations(ReactorInstance reactor) { + // For top-level, ignore this. + if (reactor == main) return; + + // Reference counts are decremented by each destination reactor + // at the conclusion of a time step. Hence, the initial reference + // count should equal the number of destination _reactors_, not the + // number of destination ports nor the number of destination reactions. + // One of the destination reactors may be the container of this + // instance because it may have a reaction to an output of this instance. + for (output : reactor.outputs) { + for (sendingRange : output.eventualDestinations) { + pr("// For reference counting, set num_destinations for port " + output.fullName + "."); + + startScopedRangeBlock(code, sendingRange, sr, sb, sc, sendingRange.instance.isInput, true); + + pr(''' + «CUtil.portRef(output, sr, sb, sc)».num_destinations = «sendingRange.getNumberOfDestinationReactors()»; + ''') + + endScopedRangeBlock(code, sendingRange); + } + } + } + + /** + * For each input port of a contained reactor that receives data + * from one or more of the specified reactions, set the num_destinations + * field of the corresponding port structs on the self struct of + * the reaction's parent reactor equal to the total number of + * destination reactors. This is used to initialize reference + * counts in dynamically allocated tokens sent to other reactors. + * @param reactions The reactions. + */ + private def void deferredInputNumDestinations(Iterable reactions) { + // Reference counts are decremented by each destination reactor + // at the conclusion of a time step. Hence, the initial reference + // count should equal the number of destination _reactors_, not the + // number of destination ports nor the number of destination reactions. + // One of the destination reactors may be the container of this + // instance because it may have a reaction to an output of this instance. + + // Since a port may be written to by multiple reactions, + // ensure that this is done only once. + val portsHandled = new HashSet(); + for (reaction : reactions) { + for (port : reaction.effects.filter(PortInstance)) { + if (port.isInput && !portsHandled.contains(port)) { + // Port is an input of a contained reactor that gets data from a reaction of this reactor. + portsHandled.add(port); + + pr(''' + // For reference counting, set num_destinations for port «port.parent.name».«port.name». + ''') + + // The input port may itself have multiple destinations. + for (sendingRange : port.eventualDestinations) { + + startScopedRangeBlock(code, sendingRange, sr, sb, sc, sendingRange.instance.isInput, true); + + // Syntax is slightly different for a multiport output vs. single port. + val connector = (port.isMultiport())? "->" : "."; + pr(''' + «CUtil.portRefNested(port, sr, sb, sc)»«connector»num_destinations = «sendingRange.getNumberOfDestinationReactors»; + ''') + + endScopedRangeBlock(code, sendingRange); + } + } + } + } + } + + /** + * If any reaction of the specified reactor provides input + * to a contained reactor, then generate code to allocate + * memory to store the data produced by those reactions. + * The allocated memory is pointed to by a field called + * `_lf_containername.portname` on the self struct of the reactor. + * @param reactor The reactor. + */ + private def void deferredAllocationForEffectsOnInputs(ReactorInstance reactor) { + // Keep track of ports already handled. There may be more than one reaction + // in the container writing to the port, but we want only one memory allocation. + val portsHandled = new HashSet(); + + // Find parent reactions that mention multiport inputs of this reactor. + for (reaction : reactor.reactions) { + for (effect : reaction.effects.filter(PortInstance)) { + if (effect.parent.depth > reactor.depth // port of a contained reactor. + && effect.isMultiport + && !portsHandled.contains(effect) + && currentFederate.contains(effect.parent) + ) { + pr("// A reaction writes to a multiport of a child. Allocate memory.") + portsHandled.add(effect); + + val portStructType = variableStructType(effect) + + startScopedBlock(code, effect.parent, true); + + val effectRef = CUtil.portRefNestedName(effect); + + pr(''' + «effectRef»_width = «effect.width»; + // Allocate memory to store output of reaction feeding a multiport input of a contained reactor. + «effectRef» = («portStructType»**)calloc(«effect.width», sizeof(«portStructType»*)); + for (int i = 0; i < «effect.width»; i++) { + «effectRef»[i] = («portStructType»*)calloc(1, sizeof(«portStructType»)); + } + ''') + + endScopedBlock(code); + } + } + } + } + + /** + * For each reaction of the specified reactor, + * Set the last_enabling_reaction field of the reaction struct to point + * to the single dominating upstream reaction, if there is one, or to be + * NULL if not. + * + * @param reactor The reactor. + */ + private def deferredOptimizeForSingleDominatingReaction (ReactorInstance r) { + for (reaction : r.reactions) { + if (currentFederate.contains(reaction.definition) + && currentFederate.contains(reaction.parent) + ) { + + // For federated systems, the above test may not be enough if there is a bank + // of federates. Calculate the divisor needed to compute the federate bank + // index from the instance index of the reaction. + var divisor = 1; + if (isFederated) { + var parent = reaction.parent; + while (parent.depth > 1) { + divisor *= parent.width; + parent = parent.parent; + } + } + + // The following code attempts to gather into a loop assignments of successive + // bank members relations between reactions to avoid large chunks of inline code + // when a large bank sends to a large bank or when a large bank receives from + // one reaction that is either multicasting or sending through a multiport. + var start = 0; + var end = 0; + var domStart = 0; + var same = false; // Set to true when finding a string of identical dominating reactions. + var previousRuntime = null as ReactionInstance.Runtime; + var first = true; //First time through the loop. + for (runtime : reaction.getRuntimeInstances()) { + if (!first) { // Not the first time through the loop. + if (same) { // Previously seen at least two identical dominating. + if (runtime.dominating != previousRuntime.dominating) { + // End of streak of same dominating reaction runtime instance. + printOptimizeForSingleDominatingReaction( + previousRuntime, start, end, domStart, same, divisor + ); + same = false; + start = runtime.id; + domStart = (runtime.dominating !== null) ? runtime.dominating.id : 0; + } + } else if (runtime.dominating == previousRuntime.dominating) { + // Start of a streak of identical dominating reaction runtime instances. + same = true; + } else if (runtime.dominating !== null && previousRuntime.dominating !== null + && runtime.dominating.reaction == previousRuntime.dominating.reaction + ) { + // Same dominating reaction even if not the same dominating runtime. + if (runtime.dominating.id != previousRuntime.dominating.id + 1) { + // End of a streak of contiguous runtimes. + printOptimizeForSingleDominatingReaction( + previousRuntime, start, end, domStart, same, divisor + ); + same = false; + start = runtime.id; + domStart = runtime.dominating.id; + } + } else { + // Different dominating reaction. + printOptimizeForSingleDominatingReaction( + previousRuntime, start, end, domStart, same, divisor + ); + same = false; + start = runtime.id; + domStart = (runtime.dominating !== null) ? runtime.dominating.id : 0; + } + } + first = false; + previousRuntime = runtime; + end++; + } + if (end > start) { + printOptimizeForSingleDominatingReaction(previousRuntime, start, end, domStart, same, divisor); + } + } + } + } + + /** + * Print statement that sets the last_enabling_reaction field of a reaction. + */ + private def printOptimizeForSingleDominatingReaction( + ReactionInstance.Runtime runtime, int start, int end, int domStart, boolean same, int divisor + ) { + var domDivisor = 1; + if (isFederated && runtime.dominating !== null) { + val domReaction = runtime.dominating.getReaction(); + // No need to do anything if the dominating reaction is not in the federate. + // Note that this test is imperfect because the current federate may be a + // bank member. + if (!currentFederate.contains(domReaction.definition) + || !currentFederate.contains(domReaction.getParent())) { + return; + } + // To really know whether the dominating reaction is in the federate, + // we need to calculate a divisor for its runtime index. + var parent = runtime.dominating.getReaction().parent; + while (parent.depth > 1) { + domDivisor *= parent.width; + parent = parent.parent; + } + } - if (p.type.isList && p.init.size > 1) { - return p.init.join('{', ', ', '}', [it.targetValue]) + var dominatingRef = "NULL"; + + if (end > start + 1) { + startScopedBlock(code); + val reactionRef = CUtil.reactionRef(runtime.reaction, "i"); + if (runtime.dominating !== null) { + if (same) { + dominatingRef = "&(" + CUtil.reactionRef(runtime.dominating.reaction, "" + domStart) + ")"; + } else { + dominatingRef = "&(" + CUtil.reactionRef(runtime.dominating.reaction, "j++") + ")"; + } + } + pr(''' + // «runtime.reaction.getFullName» dominating upstream reaction. + int j = «domStart»; + for (int i = «start»; i < «end»; i++) { + «IF isFederated» + if (i / «divisor» != «currentFederate.bankIndex») continue; // Reaction is not in the federate. + «IF runtime.dominating !== null» + if (j / «domDivisor» != «currentFederate.bankIndex») continue; // Dominating reaction is not in the federate. + «ENDIF» + «ENDIF» + «reactionRef».last_enabling_reaction = «dominatingRef»; + } + ''') + endScopedBlock(code); + } else if (end == start + 1) { + val reactionRef = CUtil.reactionRef(runtime.reaction, "" + start); + if (runtime.dominating !== null + && (domDivisor == 1 || domStart/domDivisor == currentFederate.bankIndex) + ) { + dominatingRef = "&(" + CUtil.reactionRef(runtime.dominating.reaction, "" + domStart) + ")"; + } + if (!isFederated + || (start/divisor == currentFederate.bankIndex) + && (runtime.dominating === null || domStart/domDivisor == currentFederate.bankIndex) + ) { + pr(''' + // «runtime.reaction.getFullName» dominating upstream reaction. + «reactionRef».last_enabling_reaction = «dominatingRef»; + ''') + } + } + } + + /** + * Generate code to allocate the memory needed by reactions for triggering + * downstream reactions. + * @param reactions A list of reactions. + */ + private def void deferredReactionMemory(Iterable reactions) { + // For each reaction instance, allocate the arrays that will be used to + // trigger downstream reactions. + for (reaction : reactions) { + deferredReactionOutputs(reaction); + + // Next handle triggers of the reaction that come from a multiport output + // of a contained reactor. Also, handle startup and shutdown triggers. + for (trigger : reaction.triggers.filter(PortInstance)) { + // If the port is a multiport, then we need to create an entry for each + // individual port. + if (trigger.isMultiport() && trigger.parent !== null && trigger.isOutput) { + // Trigger is an output of a contained reactor or bank. + pr(''' + // Allocate memory to store pointers to the multiport output «trigger.name» + // of a contained reactor «trigger.parent.getFullName» + ''') + startScopedBlock(code, trigger.parent, true); + + val width = trigger.width; + val portStructType = variableStructType(trigger) + + pr(''' + «CUtil.reactorRefNested(trigger.parent)».«trigger.name»_width = «width»; + «CUtil.reactorRefNested(trigger.parent)».«trigger.name» = («portStructType»**)calloc( + «width», sizeof(«portStructType»*)); + ''') + + endScopedBlock(code); + } + } + } + } + + /** + * For the specified reaction, for ports that it writes to, + * set up the arrays that store the results (if necessary) and + * that are used to trigger downstream reactions if an effect is actually + * produced. The port may be an output of the reaction's parent + * or an input to a reactor contained by the parent. + * + * @param The reaction instance. + */ + private def void deferredReactionOutputs(ReactionInstance reaction) { + // val selfRef = CUtil.reactorRef(reaction.parent); + val name = reaction.parent.getFullName; + // Insert a string name to facilitate debugging. + if (targetConfig.logLevel >= LogLevel.LOG) { + pr(''' + «CUtil.reactionRef(reaction)».name = "«name» reaction «reaction.index»"; + ''') + } + + // Count the output ports and inputs of contained reactors that + // may be set by this reaction. This ignores actions in the effects. + // Collect initialization statements for the output_produced array for the reaction + // to point to the is_present field of the appropriate output. + // These statements must be inserted after the array is malloc'd, + // but we construct them while we are counting outputs. + var outputCount = 0; + val init = new StringBuilder() + + startScopedBlock(init); + pr(init, "int count = 0;") + for (effect : reaction.effects.filter(PortInstance)) { + // Create the entry in the output_produced array for this port. + // If the port is a multiport, then we need to create an entry for each + // individual channel. + + // If the port is an input of a contained reactor, then, if that + // contained reactor is a bank, we will have to iterate over bank + // members. + var bankWidth = 1; + var portRef = ""; + if (effect.isInput) { + pr(init, "// Reaction writes to an input of a contained reactor.") + bankWidth = effect.parent.width; + startScopedBlock(init, effect.parent, true); + portRef = CUtil.portRefNestedName(effect); } else { - return p.init.get(0).targetValue + startScopedBlock(init); + portRef = CUtil.portRefName(effect); } + + if (effect.isMultiport()) { + // Form is slightly different for inputs vs. outputs. + var connector = "."; + if (effect.isInput) connector = "->"; + + // Point the output_produced field to where the is_present field of the port is. + pr(init, ''' + for (int i = 0; i < «effect.width»; i++) { + «CUtil.reactionRef(reaction)».output_produced[i + count] + = &«portRef»[i]«connector»is_present; + } + count += «effect.getWidth()»; + ''') + outputCount += effect.width * bankWidth; + } else { + // The effect is not a multiport. + pr(init, ''' + «CUtil.reactionRef(reaction)».output_produced[count++] + = &«portRef».is_present; + ''') + outputCount += bankWidth; + } + endScopedBlock(init); + } + endScopedBlock(init); + pr(''' + // Total number of outputs (single ports and multiport channels) + // produced by «reaction.toString». + «CUtil.reactionRef(reaction)».num_outputs = «outputCount»; + ''') + if (outputCount > 0) { + pr(''' + // Allocate memory for triggers[] and triggered_sizes[] on the reaction_t + // struct for this reaction. + «CUtil.reactionRef(reaction)».triggers = (trigger_t***)calloc(«outputCount», sizeof(trigger_t**)); + «CUtil.reactionRef(reaction)».triggered_sizes = (int*)calloc(«outputCount», sizeof(int)); + «CUtil.reactionRef(reaction)».output_produced = (bool**)calloc(«outputCount», sizeof(bool*)); + ''') + } + pr(''' + «init.toString» + // ** End initialization for reaction «reaction.index» of «name» + ''') } - override supportsGenerics() { - return false + /** + * For the specified reaction, for ports that it writes to, + * fill the trigger table for triggering downstream reactions. + * + * @param reactions The reactions. + */ + private def void deferredFillTriggerTable(Iterable reactions) { + for (reaction: reactions) { + val name = reaction.parent.getFullName; + + var foundPort = false; + + for (port : reaction.effects.filter(PortInstance)) { + if (!foundPort) { + // Need a separate index for the triggers array for each bank member. + startScopedBlock(code); + pr(''' + int triggers_index[«reaction.parent.totalWidth»] = { 0 }; // Number of bank members with the reaction. + ''') + foundPort = true; + } + // If the port is a multiport, then its channels may have different sets + // of destinations. For ordinary ports, there will be only one range and + // its width will be 1. + // We generate the code to fill the triggers array first in a temporary code buffer, + // so that we can simultaneously calculate the size of the total array. + for (SendRange srcRange : port.eventualDestinations()) { + val srcNested = (port.isInput)? true : false; + startScopedRangeBlock(code, srcRange, sr, sb, sc, srcNested, true); + + var triggerArray = '''«CUtil.reactionRef(reaction, sr)».triggers[triggers_index[«sr»]++]''' + // Skip ports whose parent is not in the federation. + // This can happen with reactions in the top-level that have + // as an effect a port in a bank. + if (currentFederate.contains(port.parent)) { + pr(''' + // Reaction «reaction.index» of «name» triggers «srcRange.destinations.size» downstream reactions + // through port «port.getFullName». + «CUtil.reactionRef(reaction, sr)».triggered_sizes[triggers_index[«sr»]] = «srcRange.destinations.size»; + // For reaction «reaction.index» of «name», allocate an + // array of trigger pointers for downstream reactions through port «port.getFullName» + trigger_t** trigger_array = (trigger_t**)calloc(«srcRange.destinations.size», sizeof(trigger_t*)); + «triggerArray» = trigger_array; + ''') + } else { + // Port is not in the federate or has no destinations. + // Set the triggered_width fields to 0. + pr(''' + «CUtil.reactionRef(reaction, sr)».triggered_sizes[«sc»] = 0; + ''') + } + endScopedRangeBlock(code, srcRange); + } + } + var cumulativePortWidth = 0; + for (port : reaction.effects.filter(PortInstance)) { + pr(''' + for (int i = 0; i < «reaction.parent.totalWidth»; i++) triggers_index[i] = «cumulativePortWidth»; + ''') + for (SendRange srcRange : port.eventualDestinations()) { + if (currentFederate.contains(port.parent)) { + val srcNested = srcRange.instance.isInput; + var multicastCount = 0; + for (dstRange : srcRange.destinations) { + val dst = dstRange.instance; + + startScopedRangeBlock(code, srcRange, dstRange); + + // If the source is nested, need to take into account the parent's bank index + // when indexing into the triggers array. + var triggerArray = ""; + if (srcNested && port.parent.width > 1 && !(isFederated && port.parent.depth == 1)) { + triggerArray = ''' + «CUtil.reactionRef(reaction, sr)».triggers[triggers_index[«sr»] + «sc» + src_range_mr.digits[1] * src_range_mr.radixes[0]] + ''' + } else { + triggerArray = '''«CUtil.reactionRef(reaction, sr)».triggers[triggers_index[«sr»] + «sc»]''' + } + + if (dst.isOutput) { + // Include this destination port only if it has at least one + // reaction in the federation. + var belongs = false; + for (destinationReaction : dst.dependentReactions) { + if (currentFederate.contains(destinationReaction.parent)) { + belongs = true + } + } + if (belongs) { + pr(''' + // Port «port.getFullName» has reactions in its parent's parent. + // Point to the trigger struct for those reactions. + «triggerArray»[«multicastCount»] = &«CUtil.triggerRefNested(dst, dr, db)»; + ''') + } else { + // Put in a NULL pointer. + pr(''' + // Port «port.getFullName» has reactions in its parent's parent. + // But those are not in the federation. + «triggerArray»[«multicastCount»] = NULL; + ''') + } + } else { + // Destination is an input port. + pr(''' + // Point to destination port «dst.getFullName»'s trigger struct. + «triggerArray»[«multicastCount»] = &«CUtil.triggerRef(dst, dr)»; + ''') + } + endScopedRangeBlock(code, srcRange, dstRange); + multicastCount++; + } + } + } + cumulativePortWidth += port.width; + } + if (foundPort) endScopedBlock(code); + } } - override generateDelayGeneric() { - throw new UnsupportedOperationException("TODO: auto-generated method stub") + /** + * Generate an array of self structs for the reactor + * and one for each of its children. + * @param r The reactor instance. + */ + private def void generateSelfStructs(ReactorInstance r) { + if (!currentFederate.contains(r)) return; + // FIXME: For federated execution, if the reactor is a bank, then + // it may be that only one of the bank members is in the federate, + // but this creates an array big enough to hold all bank members. + // Fixing this will require making the functions in CUtil that + // create references to the runtime instances aware of this exception. + // For now, we just create a larger array than needed. + pr(initializeTriggerObjects, ''' + «CUtil.selfType(r)»* «CUtil.reactorRefName(r)»[«r.totalWidth»]; + ''') + for (child : r.children) { + generateSelfStructs(child); + } } + + ////////////////////////////////////////////////////////////// + // Inner class /** * Data structure that for each instantiation of a contained @@ -6190,7 +6589,7 @@ class CGenerator extends GeneratorBase { } } - // FIXME: Get rid of this, if possible. /** The current federate for which we are generating code. */ var currentFederate = null as FederateInstance; + } diff --git a/org.lflang/src/org/lflang/generator/c/CTypes.java b/org.lflang/src/org/lflang/generator/c/CTypes.java new file mode 100644 index 0000000000..256222bcb9 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/c/CTypes.java @@ -0,0 +1,122 @@ +package org.lflang.generator.c; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.lflang.ErrorReporter; +import org.lflang.InferredType; +import org.lflang.generator.TargetTypes; + +public class CTypes implements TargetTypes { + + // Regular expression pattern for array types. + // For example, for "foo[10]", the first match will be "foo" and the second "[10]". + // For "foo[]", the first match will be "foo" and the second "". + static final Pattern arrayPattern = Pattern.compile("^\\s*(?:/\\*.*?\\*/)?\\s*(\\w+)\\s*\\[([0-9]*)]\\s*$"); + + // FIXME: Instead of using the ErrorReporter, perhaps we should be raising assertion errors or + // UnsupportedOperationExceptions or some other non-recoverable errors. + private final ErrorReporter errorReporter; + + /** + * Initializes a {@code CTargetTypes} with the given + * error reporter. + * @param errorReporter The error reporter for any + * errors raised in the code + * generation process. + */ + public CTypes(ErrorReporter errorReporter) { + this.errorReporter = errorReporter; + } + + @Override + public boolean supportsGenerics() { + return false; + } + + @Override + public String getTargetTimeType() { + return "interval_t"; + } + + @Override + public String getTargetTagType() { + return "tag_t"; + } + + @Override + public String getTargetFixedSizeListType(String baseType, int size) { + return String.format("%s[%d]", baseType, size); + } + + @Override + public String getTargetVariableSizeListType(String baseType) { + return String.format("%s[]", baseType); + } + + @Override + public String getTargetUndefinedType() { + return String.format("/* %s */", errorReporter.reportError("undefined type")); + } + + /** + * Given a type, return a C representation of the type. Note that + * C types are very idiosyncratic. For example, {@code int[]} is not always accepted + * as a type, and {@code int*} must be used instead, except when initializing + * a variable using a static initializer, as in {@code int[] foo = {1, 2, 3};}. + * When initializing a variable using a static initializer, use + * {@link #getVariableDeclaration(InferredType, String)} instead. + * @param type The type. + */ + @Override + public String getTargetType(InferredType type) { + var result = TargetTypes.super.getTargetType(type); + Matcher matcher = arrayPattern.matcher(result); + if (matcher.find()) { + return matcher.group(1) + '*'; + } + return result; + } + + /** + * Return a variable declaration of the form "{@code type name}". + * The type is as returned by {@link #getTargetType(InferredType)}, except with + * the array syntax {@code [size]} preferred over the pointer syntax + * {@code *} (if applicable). This also includes the variable name + * because C requires the array type specification to be placed after + * the variable name rather than with the type. + * The result is of the form {@code type variable_name[size]} or + * {@code type variable_name} depending on whether the given type + * is an array type, unless the array type has no size (it is given + * as {@code []}. In that case, the returned form depends on the + * third argument, initializer. If true, the then the returned + * declaration will have the form {@code type variable_name[]}, + * and otherwise it will have the form {@code type* variable_name}. + * @param type The type. + * @param variableName The name of the variable. + * @param initializer True to return a form usable in a static initializer. + */ + public String getVariableDeclaration( + InferredType type, + String variableName, + boolean initializer + ) { + String t = TargetTypes.super.getTargetType(type); + Matcher matcher = arrayPattern.matcher(t); + String declaration = String.format("%s %s", t, variableName); + if (matcher.find()) { + // For array types, we have to move the [] + // because C is very picky about where this goes. It has to go + // after the variable name. Also, in an initializer, it has to have + // form [], and in a struct definition, it has to use *. + if (matcher.group(2).equals("") && !initializer) { + declaration = String.format("%s* %s", + matcher.group(1), variableName); + } else { + declaration = String.format("%s %s[%s]", + matcher.group(1), variableName, matcher.group(2)); + } + } + return declaration; + } +} diff --git a/org.lflang/src/org/lflang/generator/c/CUtil.java b/org.lflang/src/org/lflang/generator/c/CUtil.java new file mode 100644 index 0000000000..9e561cb8b6 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/c/CUtil.java @@ -0,0 +1,675 @@ +/* Utilities for C code generation. */ + +/************* +Copyright (c) 2019-2021, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ + +package org.lflang.generator.c; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.lflang.ErrorReporter; +import org.lflang.FileConfig; +import org.lflang.TargetConfig; +import org.lflang.TargetConfig.Mode; +import org.lflang.generator.GeneratorCommandFactory; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.TriggerInstance; +import org.lflang.lf.Parameter; +import org.lflang.lf.Port; +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; +import org.lflang.lf.WidthTerm; +import org.lflang.util.LFCommand; + +/** + * A collection of utilities for C code generation. + * This class codifies the coding conventions for the C target code generator. + * I.e., it defines how variables are named and referenced. + * @author{Edward A. Lee } + */ +public class CUtil { + + ////////////////////////////////////////////////////// + //// Public methods. + + /** + * Return a default name of a variable to refer to the bank index of a reactor + * in a bank. This is has the form uniqueID_i where uniqueID + * is an identifier for the instance that is guaranteed to be different + * from the ID of any other instance in the program. + * If the instance is not a bank, return "0". + * @param instance A reactor instance. + */ + static public String bankIndex(ReactorInstance instance) { + if (!instance.isBank()) return "0"; + return bankIndexName(instance); + } + + /** + * Return a default name of a variable to refer to the bank index of a reactor + * in a bank. This is has the form uniqueID_i where uniqueID + * is an identifier for the instance that is guaranteed to be different + * from the ID of any other instance in the program. + * @param instance A reactor instance. + */ + static public String bankIndexName(ReactorInstance instance) { + return instance.uniqueID() + "_i"; + } + + /** + * Return a default name of a variable to refer to the channel index of a port + * in a bank. This is has the form uniqueID_c where uniqueID + * is an identifier for the instance that is guaranteed to be different + * from the ID of any other instance in the program. + * If the port is not a multiport, then return the string "0". + * @param instance A reactor instance. + */ + static public String channelIndex(PortInstance port) { + if (!port.isMultiport()) return "0"; + return channelIndexName(port); + } + + /** + * Return a default name of a variable to refer to the channel index of a port + * in a bank. This is has the form uniqueID_c where uniqueID + * is an identifier for the instance that is guaranteed to be different + * from the ID of any other instance in the program. + * @param instance A reactor instance. + */ + static public String channelIndexName(PortInstance port) { + return port.uniqueID() + "_c"; + } + + /** + * Return a reference to the specified port. + * + * The returned string will have one of the following forms: + * + * * selfStructs[k]->_lf_portName + * * selfStructs[k]->_lf_portName + * * selfStructs[k]->_lf_portName[i] + * * selfStructs[k]->_lf_parent.portName + * * selfStructs[k]->_lf_parent.portName[i] + * * selfStructs[k]->_lf_parent[j].portName + * * selfStructs[k]->_lf_parent[j].portName[i] + * + * where k is the runtime index of either the port's parent + * or the port's parent's parent, the latter when isNested is true. + * The index j is present if the parent is a bank, and + * the index i is present if the port is a multiport. + * + * The first two forms are used if isNested is false, + * and the remaining four are used if isNested is true. + * Set isNested to true when referencing a port belonging + * to a contained reactor. + * + * @param port The port. + * @param isNested True to return a reference relative to the parent's parent. + * @param includeChannelIndex True to include the channel index at the end. + * @param runtimeIndex A variable name to use to index the runtime instance or + * null to use the default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the + * default, the string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to + * use the default, the string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + static public String portRef( + PortInstance port, + boolean isNested, + boolean includeChannelIndex, + String runtimeIndex, + String bankIndex, + String channelIndex + ) { + String channel = ""; + if (channelIndex == null) channelIndex = channelIndex(port); + if (port.isMultiport() && includeChannelIndex) { + channel = "[" + channelIndex + "]"; + } + if (isNested) { + return reactorRefNested(port.getParent(), runtimeIndex, bankIndex) + "." + port.getName() + channel; + } else { + String sourceStruct = CUtil.reactorRef(port.getParent(), runtimeIndex); + return sourceStruct + "->_lf_" + port.getName() + channel; + } + } + + /** + * Return a reference to the port on the self struct of the + * port's parent. This is used when an input port triggers a reaction + * in the port's parent or when an output port is written to by + * a reaction in the port's parent. + * This is equivalent to calling `portRef(port, false, true, null, null)`. + * @param port An instance of the port to be referenced. + */ + static public String portRef(PortInstance port) { + return portRef(port, false, true, null, null, null); + } + + /** + * Return a reference to the port on the self struct of the + * port's parent using the specified index variables. + * This is used when an input port triggers a reaction + * in the port's parent or when an output port is written to by + * a reaction in the port's parent. + * This is equivalent to calling `portRef(port, false, true, bankIndex, channelIndex)`. + * @param port An instance of the port to be referenced. + * @param runtimeIndex A variable name to use to index the runtime instance or + * null to use the default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the + * default, the string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to + * use the default, the string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + static public String portRef( + PortInstance port, String runtimeIndex, String bankIndex, String channelIndex + ) { + return portRef(port, false, true, runtimeIndex, bankIndex, channelIndex); + } + + /** + * Return a reference to a port without any channel indexing. + * This is useful for deriving a reference to the _width variable. + * @param port An instance of the port to be referenced. + */ + static public String portRefName(PortInstance port) { + return portRef(port, false, false, null, null, null); + } + + /** + * Return the portRef without the channel indexing. + * This is useful for deriving a reference to the _width variable. + * + * @param port An instance of the port to be referenced. + * @param runtimeIndex A variable name to use to index the runtime instance or + * null to use the default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the + * default, the string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to + * use the default, the string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + static public String portRefName( + PortInstance port, String runtimeIndex, String bankIndex, String channelIndex + ) { + return portRef(port, false, false, runtimeIndex, bankIndex, channelIndex); + } + + /** + * Return a port reference to a port on the self struct of the + * parent of the port's parent. This is used when an input port + * is written to by a reaction in the parent of the port's parent, + * or when an output port triggers a reaction in the parent of the + * port's parent. This is equivalent to calling + * `portRef(port, true, true, null, null, null)`. + * + * @param port The port. + */ + static public String portRefNested(PortInstance port) { + return portRef(port, true, true, null, null, null); + } + + /** + * Return a reference to the port on the self struct of the + * parent of the port's parent. This is used when an input port + * is written to by a reaction in the parent of the port's parent, + * or when an output port triggers a reaction in the parent of the + * port's parent. This is equivalent to calling + * `portRef(port, true, true, runtimeIndex, bankIndex, channelIndex)`. + * + * @param port The port. + * @param runtimeIndex A variable name to use to index the runtime instance or + * null to use the default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the + * default, the string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to + * use the default, the string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + static public String portRefNested( + PortInstance port, String runtimeIndex, String bankIndex, String channelIndex + ) { + return portRef(port, true, true, runtimeIndex, bankIndex, channelIndex); + } + + /** + * Return a reference to the port on the self struct of the + * parent of the port's parent, but without the channel indexing, + * even if it is a multiport. This is used when an input port + * is written to by a reaction in the parent of the port's parent, + * or when an output port triggers a reaction in the parent of the + * port's parent. + * This is equivalent to calling `portRef(port, true, false, null, null, null)`. + * + * @param port The port. + */ + static public String portRefNestedName(PortInstance port) { + return portRef(port, true, false, null, null, null); + } + + /** + * Return a reference to the port on the self struct of the + * parent of the port's parent, but without the channel indexing, + * even if it is a multiport. This is used when an input port + * is written to by a reaction in the parent of the port's parent, + * or when an output port triggers a reaction in the parent of the + * port's parent. This is equivalent to calling + * `portRefNested(port, true, false, runtimeIndex, bankIndex, channelIndex)`. + * + * @param port The port. + * @param runtimeIndex A variable name to use to index the runtime instance or + * null to use the default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the + * default, the string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to + * use the default, the string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + static public String portRefNestedName( + PortInstance port, String runtimeIndex, String bankIndex, String channelIndex + ) { + return portRef(port, true, false, runtimeIndex, bankIndex, channelIndex); + } + + /** + * Return code for referencing a port within a reaction body possibly indexed by + * a bank index and/or a multiport index. If the provided reference is + * not a port, then this returns the string "ERROR: not a port." + * @param reference The reference to the port. + * @param bankIndex A bank index or null or negative if not in a bank. + * @param multiportIndex A multiport index or null or negative if not in a multiport. + */ + public static String portRefInReaction(VarRef reference, Integer bankIndex, Integer multiportIndex) { + if (!(reference.getVariable() instanceof Port)) { + return "ERROR: not a port."; // FIXME: This is not the fail-fast approach, and it seems arbitrary. + } + var prefix = ""; + if (reference.getContainer() != null) { + var bank = ""; + if (reference.getContainer().getWidthSpec() != null && bankIndex != null && bankIndex >= 0) { + bank = "[" + bankIndex + "]"; + } + prefix = reference.getContainer().getName() + bank + "."; + } + var multiport = ""; + if (((Port) reference.getVariable()).getWidthSpec() != null && multiportIndex != null && multiportIndex >= 0) { + multiport = "[" + multiportIndex + "]"; + } + return prefix + reference.getVariable().getName() + multiport; + } + + /** + * Return a reference to the reaction entry on the self struct + * of the parent of the specified reaction. + * @param reaction The reaction. + */ + static public String reactionRef(ReactionInstance reaction) { + return reactionRef(reaction, null); + } + + /** + * Return a reference to the reaction entry on the self struct + * of the parent of the specified reaction. + * @param reaction The reaction. + * @param runtimeIndex An index into the array of self structs for the parent. + */ + static public String reactionRef(ReactionInstance reaction, String runtimeIndex) { + return reactorRef(reaction.getParent(), runtimeIndex) + + "->_lf__reaction_" + reaction.index; + } + + /** + * Return a reference to the "self" struct of the specified + * reactor instance. The returned string has the form + * self[j], where self is the name of the array of self structs + * for this reactor instance and j is the expression returned + * by {@link #runtimeIndex(ReactorInstance)} or 0 if there are no banks. + * @param instance The reactor instance. + */ + static public String reactorRef(ReactorInstance instance) { + return reactorRef(instance, null); + } + + /** + * Return the name of the array of "self" structs of the specified + * reactor instance. This is similar to {@link #reactorRef(ReactorInstance)} + * except that it does not index into the array. + * @param instance The reactor instance. + */ + static public String reactorRefName(ReactorInstance instance) { + return instance.uniqueID() + "_self"; + } + + /** + * Return a reference to the "self" struct of the specified + * reactor instance. The returned string has the form + * self[runtimeIndex], where self is the name of the array of self structs + * for this reactor instance. If runtimeIndex is null, then it is replaced by + * the expression returned + * by {@link runtimeIndex(ReactorInstance)} or 0 if there are no banks. + * @param instance The reactor instance. + * @param runtimeIndex An optional expression to use to address bank members. + * If this is null, the expression used will be that returned by + * {@link #runtimeIndex(ReactorInstance)}. + */ + static public String reactorRef(ReactorInstance instance, String runtimeIndex) { + if (runtimeIndex == null) runtimeIndex = runtimeIndex(instance); + return reactorRefName(instance) + "[" + runtimeIndex + "]"; + } + + /** + * For situations where a reaction reacts to or reads from an output + * of a contained reactor or sends to an input of a contained reactor, + * then the container's self struct will have a field + * (or an array of fields if the contained reactor is a bank) that is + * a struct with fields corresponding to those inputs and outputs. + * This method returns a reference to that struct or array of structs. + * Note that the returned reference is not to the self struct of the + * contained reactor. Use {@link reactorRef(ReactorInstance)} for that. + * + * @param reactor The contained reactor. + */ + static public String reactorRefNested(ReactorInstance reactor) { + return reactorRefNested(reactor, null, null); + } + + /** + * For situations where a reaction reacts to or reads from an output + * of a contained reactor or sends to an input of a contained reactor, + * then the container's self struct will have a field + * (or an array of fields if the contained reactor is a bank) that is + * a struct with fields corresponding to those inputs and outputs. + * This method returns a reference to that struct or array of structs. + * Note that the returned reference is not to the self struct of the + * contained reactor. Use {@link CUtil#reactorRef(ReactorInstance)} for that. + * + * @param reactor The contained reactor. + * @param runtimeIndex A variable name to use to index the runtime instance or + * null to use the default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the + * default, the string returned by {@link CUtil#bankIndex(ReactorInstance)}. + */ + static public String reactorRefNested(ReactorInstance reactor, String runtimeIndex, String bankIndex) { + String result = reactorRef(reactor.getParent(), runtimeIndex) + "->_lf_" + reactor.getName(); + if (reactor.isBank()) { + // Need the bank index not the runtimeIndex. + if (bankIndex == null) bankIndex = bankIndex(reactor); + result += "[" + bankIndex + "]"; + } + return result; + } + + /** + * Return an expression that, when evaluated, gives the index of + * a runtime instance of the specified ReactorInstance. If the reactor + * is not within any banks, then this will return "0". Otherwise, it + * will return an expression that evaluates a mixed-radix number + * d0%w0, d1%w1, ... , dn%wn, where n is the depth minus one of the + * reactor. The radixes, w0 to wn, are the widths of this reactor, + * its parent reactor, on up to the top-level reactor. Since the top-level + * reactor is never a bank, dn = 0 and wn = 1. The digits, di, are + * either 0 (of the parent is not a bank) or the variable name returned + * by {@link #bankIndexName(ReactorInstance)} if the parent is a bank. + * The returned expression, when evaluated, will yield the following value: + * + * d0 + w0 * (d1 + w1 * ( ... (dn-1 + wn-1 * dn) ... ) + * + * @param reactor The reactor. + */ + static public String runtimeIndex(ReactorInstance reactor) { + StringBuilder result = new StringBuilder(); + int width = 0; + int parens = 0; + while (reactor != null) { + if (reactor.isBank() && reactor.getWidth() > 1) { + if (width > 0) { + result.append(" + " + width + " * ("); + parens++; + } + result.append(bankIndexName(reactor)); + width = reactor.getWidth(); + } + reactor = reactor.getParent(); + } + while (parens-- > 0) { + result.append(")"); + } + if (result.length() == 0) return "0"; + return result.toString(); + } + + /** + * Return a unique type for the "self" struct of the specified + * reactor class from the reactor class. + * @param reactor The reactor class. + * @return The type of a self struct for the specified reactor class. + */ + static public String selfType(ReactorDecl reactor) { + return reactor.getName().toLowerCase() + "_self_t"; + } + + /** + * Construct a unique type for the "self" struct of the specified + * reactor class from the reactor class. + * @param reactor The reactor class. + * @return The name of the self struct. + */ + static public String selfType(ReactorInstance instance) { + return selfType(instance.getDefinition().getReactorClass()); + } + + /** + * Return a reference to the trigger_t struct of the specified + * trigger instance (input port or action). This trigger_t struct + * is on the self struct. + * @param instance The port or action instance. + */ + static public String triggerRef(TriggerInstance instance) { + return triggerRef(instance, null); + } + + /** + * Return a reference to the trigger_t struct of the specified + * trigger instance (input port or action). This trigger_t struct + * is on the self struct. + * @param instance The port or action instance. + * @param runtimeIndex An optional index variable name to use to address runtime instances. + */ + static public String triggerRef(TriggerInstance instance, String runtimeIndex) { + return reactorRef(instance.getParent(), runtimeIndex) + + "->_lf__" + + instance.getName(); + } + + /** + * Return a reference to the trigger_t struct for the specified + * port of a contained reactor. + * @param port The output port of a contained reactor. + */ + static public String triggerRefNested(PortInstance port) { + return triggerRefNested(port, null, null); + } + + /** + * Return a reference to the trigger_t struct for the specified + * port of a contained reactor. + * @param port The output port of a contained reactor. + * @param runtimeIndex An optional index variable name to use to index + * the runtime instance of the port's parent's parent, or null to get the + * default returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex An optional index variable name to use to index + * the the bank of the port's parent, or null to get the default returned by + * {@link CUtil#bankIndex(ReactorInstance)}. + */ + static public String triggerRefNested(PortInstance port, String runtimeIndex, String bankIndex) { + return reactorRefNested(port.getParent(), runtimeIndex, bankIndex) + "." + port.getName() + "_trigger"; + } + + ////////////////////////////////////////////////////// + //// FIXME: Not clear what the strategy is with the following inner interface. + // The {@code ReportCommandErrors} interface allows the + // method runBuildCommand to call a protected + // method from the CGenerator if that method is passed + // using a method reference. The method that is passed + // is then interpreted as a ReportCommandErrors instance. + // This is a convenient way to factor out part of the + // internals of the CGenerator while maintaining + // encapsulation, even though the internals of the CGenerator + // might seem to be tightly coupled. FIXME: Delete this comment + + /** + * A {@code ReportCommandErrors} is a way to analyze command + * output and report any errors that it describes. + * FIXME: If the VSCode branch passes code review + * without significant revision, this mechanism will probably be replaced. + */ + public interface ReportCommandErrors { + void report(String errors); + } + + /** + * Run the custom build command specified with the "build" parameter. + * This command is executed in the same directory as the source file. + * + * The following environment variables will be available to the command: + * + * * LF_CURRENT_WORKING_DIRECTORY: The directory in which the command is invoked. + * * LF_SOURCE_DIRECTORY: The directory containing the .lf file being compiled. + * * LF_SOURCE_GEN_DIRECTORY: The directory in which generated files are placed. + * * LF_BIN_DIRECTORY: The directory into which to put binaries. + * + */ + public static void runBuildCommand( + FileConfig fileConfig, + TargetConfig targetConfig, + GeneratorCommandFactory commandFactory, + ErrorReporter errorReporter, + ReportCommandErrors reportCommandErrors, + TargetConfig.Mode mode + ) { + List commands = getCommands(targetConfig.buildCommands, commandFactory, fileConfig.srcPath); + // If the build command could not be found, abort. + // An error has already been reported in createCommand. + if (commands.stream().anyMatch(Objects::isNull)) return; + + for (LFCommand cmd : commands) { + int returnCode = cmd.run(); + if (returnCode != 0 && mode != Mode.EPOCH) { + errorReporter.reportError(String.format( + // FIXME: Why is the content of stderr not provided to the user in this error message? + "Build command \"%s\" failed with error code %d.", + targetConfig.buildCommands, returnCode + )); + return; + } + // For warnings (vs. errors), the return code is 0. + // But we still want to mark the IDE. + if (!cmd.getErrors().toString().isEmpty() && mode == Mode.EPOCH) { + reportCommandErrors.report(cmd.getErrors().toString()); + return; // FIXME: Why do we return here? Even if there are warnings, the build process should proceed. + } + } + } + + ////////////////////////////////////////////////////// + //// Private functions. + + /** + * If the argument is a multiport, then return a string that + * gives the width as an expression, and otherwise, return null. + * The string will be empty if the width is variable (specified + * as '[]'). Otherwise, if is a single term or a sum of terms + * (separated by '+'), where each term is either an integer + * or a parameter reference in the target language. + */ + public static String multiportWidthExpression(Variable variable) { + List spec = multiportWidthTerms(variable); + return spec == null ? null : String.join(" + ", spec); + } + + ////////////////////////////////////////////////////// + //// Private methods. + + /** + * Convert the given commands from strings to their LFCommand + * representation and return a list of LFCommand. + * @param commands A list of commands as strings. + * @param factory A command factory. + * @param dir The directory in which the commands should be executed. + * @return The LFCommand representations of the given commands, + * where {@code null} is a placeholder for commands that cannot be + * executed. + */ + private static List getCommands(List commands, GeneratorCommandFactory factory, Path dir) { + return commands.stream() + .map(cmd -> List.of(cmd.split("\\s+"))) + .filter(tokens -> tokens.size() > 0) + .map(tokens -> factory.createCommand(tokens.get(0), tokens.subList(1, tokens.size()), dir)) + .collect(Collectors.toList()); + } + + /** + * Return target code for a parameter reference, which in + * this case is just the parameter name. + * + * @param param The parameter to generate code for. + * @return Parameter reference in target code. + */ + private static String getTargetReference(Parameter param) { + return param.getName(); + } + + /** + * If the argument is a multiport, return a list of strings + * describing the width of the port, and otherwise, return null. + * If the list is empty, then the width is variable (specified + * as '[]'). Otherwise, it is a list of integers and/or parameter + * references. + * @param variable The port. + * @return The width specification for a multiport or null if it is + * not a multiport. + */ + private static List multiportWidthTerms(Variable variable) { + List result = null; + if (variable instanceof Port) { + if (((Port) variable).getWidthSpec() != null) { + result = new ArrayList<>(); + if (!((Port) variable).getWidthSpec().isOfVariableLength()) { + for (WidthTerm term : ((Port) variable).getWidthSpec().getTerms()) { + if (term.getParameter() != null) { + result.add(getTargetReference(term.getParameter())); + } else { + result.add("" + term.getWidth()); + } + } + } + } + } + return result; + } +} diff --git a/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt b/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt index 7fdd3fa5a7..0888d5c982 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt @@ -66,7 +66,6 @@ fun Value.toTime(outerContext: Boolean = false): String = * Get textual representation of a value in C++ code * * If the value evaluates to 0, it is interpreted as a normal value. - * FIXME this is redundant to GeneratorBase.getTargetValue */ fun Value.toCppCode(): String = CppTypes.getTargetExpr(this, null) diff --git a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt index 2d841e573c..a779a04741 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt @@ -51,8 +51,7 @@ class CppGenerator( errorReporter: ErrorReporter, private val scopeProvider: LFGlobalScopeProvider ) : - GeneratorBase(cppFileConfig, errorReporter), - TargetTypes by CppTypes { + GeneratorBase(cppFileConfig, errorReporter) { companion object { /** Path to the Cpp lib directory (relative to class path) */ @@ -83,7 +82,7 @@ class CppGenerator( // We must compile in order to install the dependencies. Future validations will be faster. doCompile(context, codeMaps) } else if (runCmake(context).first == 0) { - CppValidator(cppFileConfig, errorReporter, codeMaps).doValidate(context.cancelIndicator) + CppValidator(cppFileConfig, errorReporter, codeMaps).doValidate(context) context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(codeMaps)) } else { context.unsuccessfulFinish() @@ -205,7 +204,8 @@ class CppGenerator( if (cmakeReturnCode == 0) { // If cmake succeeded, run make val makeCommand = createMakeCommand(cppFileConfig.buildPath, version) - val makeReturnCode = CppValidator(cppFileConfig, errorReporter, codeMaps).run(makeCommand, context.cancelIndicator) + val makeReturnCode = if (context.mode == Mode.STANDALONE) makeCommand.run() else + CppValidator(cppFileConfig, errorReporter, codeMaps).run(makeCommand, context.cancelIndicator) if (makeReturnCode == 0) { println("SUCCESS (compiling generated C++ code)") @@ -316,6 +316,8 @@ class CppGenerator( override fun generateAfterDelaysWithVariableWidth() = false override fun getTarget() = Target.CPP + + override fun getTargetTypes(): TargetTypes = CppTypes } object CppTypes : TargetTypes { diff --git a/org.lflang/src/org/lflang/generator/cpp/CppParameterGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppParameterGenerator.kt index 3ce256c870..c330a83fcc 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppParameterGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppParameterGenerator.kt @@ -37,7 +37,7 @@ class CppParameterGenerator(private val reactor: Reactor) { /** * Create a list of initializers for the given parameter * - * TODO This is redundant to GeneratorBase.getInitializerList + * TODO This is redundant to ValueGenerator.getInitializerList */ private fun Parameter.getInitializerList() = init.map { if (isOfTimeType) it.toTime() diff --git a/org.lflang/src/org/lflang/generator/cpp/CppStateGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppStateGenerator.kt index 1783b4fe3e..d7e62102dd 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppStateGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppStateGenerator.kt @@ -36,7 +36,7 @@ class CppStateGenerator(private val reactor: Reactor) { /** * Create a list of state initializers in target code. * - * TODO This is redundant to GeneratorBase.getInitializerList + * TODO This is redundant to ValueGenerator.getInitializerList */ private fun getInitializerList(state: StateVar) = state.init.map { when { diff --git a/org.lflang/src/org/lflang/generator/python/PythonGenerator.xtend b/org.lflang/src/org/lflang/generator/python/PythonGenerator.xtend index ba928c9a36..20104cc2d8 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonGenerator.xtend +++ b/org.lflang/src/org/lflang/generator/python/PythonGenerator.xtend @@ -33,13 +33,13 @@ import java.util.HashMap import java.util.HashSet import java.util.LinkedHashSet import java.util.List -import java.util.regex.Pattern import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.generator.IFileSystemAccess2 import org.eclipse.xtext.util.CancelIndicator import org.lflang.ErrorReporter import org.lflang.FileConfig import org.lflang.InferredType +import org.lflang.JavaAstUtils import org.lflang.Target import org.lflang.TargetConfig import org.lflang.TargetConfig.Mode @@ -54,13 +54,13 @@ import org.lflang.generator.CodeMap import org.lflang.generator.GeneratorResult import org.lflang.generator.IntegratedBuilder import org.lflang.generator.JavaGeneratorUtils -import org.lflang.generator.ParameterInstance import org.lflang.generator.LFGeneratorContext -import org.lflang.generator.SubContext +import org.lflang.generator.ParameterInstance import org.lflang.generator.ReactionInstance import org.lflang.generator.ReactorInstance -import org.lflang.generator.c.CCompiler +import org.lflang.generator.SubContext import org.lflang.generator.c.CGenerator +import org.lflang.generator.c.CUtil import org.lflang.lf.Action import org.lflang.lf.Delay import org.lflang.lf.Input @@ -87,7 +87,7 @@ import static extension org.lflang.JavaAstUtils.* * Each class will contain all the reaction functions defined by the user in order, with the necessary ports/actions given as parameters. * Moreover, each class will contain all state variables in native Python format. * - * A backend is also generated using the CGenrator that interacts with the C code library (see CGenerator.xtend). + * A backend is also generated using the CGenerator that interacts with the C code library (see CGenerator.xtend). * The backend is responsible for passing arguments to the Python reactor functions. * * @author{Soroush Bateni } @@ -100,14 +100,21 @@ class PythonGenerator extends CGenerator { // Used to add module requirements to setup.py (delimited with ,) var pythonRequiredModules = new StringBuilder() + var PythonTypes types; + new(FileConfig fileConfig, ErrorReporter errorReporter) { - super(fileConfig, errorReporter) + this(fileConfig, errorReporter, new PythonTypes(errorReporter)) + } + + private new(FileConfig fileConfig, ErrorReporter errorReporter, PythonTypes types) { + super(fileConfig, errorReporter, false, types) // set defaults targetConfig.compiler = "gcc" targetConfig.compilerFlags = newArrayList // -Wall -Wconversion" targetConfig.linkerFlags = "" + this.types = types } - + /** * Generic struct for ports with primitive types and * statically allocated arrays in Lingua Franca. @@ -157,16 +164,11 @@ class PythonGenerator extends CGenerator { */ val generic_action_type = "generic_action_instance_struct" - override getTargetUndefinedType() '''PyObject*''' - /** Returns the Target enum for this generator */ override getTarget() { return Target.Python } - // Regular expression pattern for pointer types. The star at the end has to be visible. - static final Pattern pointerPatternVariable = Pattern.compile("^\\s*+(\\w+)\\s*\\*\\s*$"); - val protoNames = new HashSet() //////////////////////////////////////////// @@ -223,6 +225,10 @@ class PythonGenerator extends CGenerator { ##################################### '''); } + + override getTargetTypes() { + return types; + } //////////////////////////////////////////// //// Protected methods @@ -240,7 +246,7 @@ class PythonGenerator extends CGenerator { switch(v.toText) { case "false": returnValue = "False" case "true": returnValue = "True" - default: returnValue = super.getTargetValue(v) + default: returnValue = v.targetValue } // Parameters in Python are always prepended with a 'self.' @@ -268,7 +274,7 @@ class PythonGenerator extends CGenerator { for (i : state?.init) { if (i.parameter !== null) { - list.add(i.parameter.targetReference) + list.add(i.parameter.name) } else if (state.isOfTimeType) { list.add(i.targetTime) } else { @@ -302,13 +308,13 @@ class PythonGenerator extends CGenerator { * @param p The parameter instance to create initializers for * @return Initialization code */ - protected def String getPythonInitializer(ParameterInstance p) { - if (p.init.size > 1) { - // parameters are initialized as immutable tuples - return p.init.join('(', ', ', ')', [it.pythonTargetValue]) - } else { - return p.init.get(0).getPythonTargetValue - } + protected def String getPythonInitializer(ParameterInstance p) { + if (p.getInitialValue.size > 1) { + // parameters are initialized as immutable tuples + return p.getInitialValue.join('(', ', ', ')', [it.pythonTargetValue]) + } else { + return p.getInitialValue.get(0).getPythonTargetValue + } } @@ -319,14 +325,13 @@ class PythonGenerator extends CGenerator { * @return Initialization code */ protected def String getPythonInitializer(Parameter p) { - if (p.init.size > 1) { - // parameters are initialized as immutable tuples - return p.init.join('(', ', ', ')', [it.pythonTargetValue]) - } else { - return p.init.get(0).pythonTargetValue - } - - } + if (p.init.size > 1) { + // parameters are initialized as immutable tuples + return p.init.join('(', ', ', ')', [it.pythonTargetValue]) + } else { + return p.init.get(0).pythonTargetValue + } + } /** * Generate parameters and their respective initialization code for a reaction function @@ -350,7 +355,7 @@ class PythonGenerator extends CGenerator { generatedParams.add('''mutable_«trigger.variable.name»''') // Create a deep copy - if ((trigger.variable as Input).isMultiport) { + if (JavaAstUtils.isMultiport(trigger.variable as Input)) { inits. append('''«trigger.variable.name» = [Make() for i in range(len(mutable_«trigger.variable.name»))] ''') @@ -428,7 +433,7 @@ class PythonGenerator extends CGenerator { } else { generatedParams.add(effect.variable.name) if (effect.variable instanceof Port) { - if (isMultiport(effect.variable as Port)) { + if (JavaAstUtils.isMultiport(effect.variable as Port)) { // Handle multiports } } @@ -572,10 +577,10 @@ class PythonGenerator extends CGenerator { ''') for (param : decl.toDefinition.allParameters) { - if (!param.inferredType.targetType.equals("PyObject*")) { + if (!types.getTargetType(param).equals("PyObject*")) { // If type is given, use it temporary_code. - append(''' self._«param.name»:«param.inferredType.pythonType» = «param.pythonInitializer» + append(''' self._«param.name»:«types.getPythonType(param.inferredType)» = «param.pythonInitializer» ''') } else { // If type is not given, just pass along the initialization @@ -597,10 +602,10 @@ class PythonGenerator extends CGenerator { ''') // Next, handle state variables for (stateVar : reactor.allStateVars) { - if (!stateVar.inferredType.targetType.equals("PyObject*")) { + if (!types.getTargetType(stateVar).equals("PyObject*")) { // If type is given, use it temporary_code. - append(''' self.«stateVar.name»:«stateVar.inferredType.pythonType» = «stateVar.pythonInitializer» + append(''' self.«stateVar.name»:«types.getPythonType(stateVar.inferredType)» = «stateVar.pythonInitializer» ''') } else if (stateVar.isInitialized) { // If type is not given, pass along the initialization directly if it is present @@ -619,15 +624,28 @@ class PythonGenerator extends CGenerator { // Next, create getters for parameters for (param : decl.toDefinition.allParameters) { - temporary_code.append(''' @property - ''') - temporary_code.append(''' def «param.name»(self): - ''') - temporary_code.append(''' return self._«param.name» - - ''') + if (param.name.equals("bank_index")){ + // Do nothing + } else { + temporary_code.append(''' @property + ''') + temporary_code.append(''' def «param.name»(self): + ''') + temporary_code.append(''' return self._«param.name» + + ''') + } } + // Create a special property for bank_index + temporary_code.append(''' @property + ''') + temporary_code.append(''' def bank_index(self): + ''') + temporary_code.append(''' return self._bank_index + + ''') + return temporary_code; } @@ -659,31 +677,6 @@ class PythonGenerator extends CGenerator { «ENDFOR» ''' - /** - * This generator inherits types from the CGenerator. - * This function reverts them back to Python types - * For example, the types double is converted to float, - * the * for pointer types is removed, etc. - * @param type The type - * @return The Python equivalent of a C type - */ - def getPythonType(InferredType type) { - var result = super.getTargetType(type) - - switch(result){ - case "double": result = "float" - case "string": result = "object" - default: result = result - } - - val matcher = pointerPatternVariable.matcher(result) - if(matcher.find()) { - return matcher.group(1) - } - - return result - } - /** * Instantiate classes in Python. * Instances are always instantiated as a list of className = [_className, _className, ...] depending on the size of the bank. @@ -710,25 +703,28 @@ class PythonGenerator extends CGenerator { // Do not instantiate reactor classes that don't have a reaction in Python // Do not instantiate the federated main reactor since it is generated in C if (!instance.definition.reactorClass.toDefinition.allReactions.isEmpty && !instance.definition.reactorClass.toDefinition.isFederated) { - if (federate.contains(instance) && instance.bankMembers !== null) { - // If this reactor is a placeholder for a bank of reactors, then generate - // a list of instances of reactors and return. + if (federate.contains(instance) && instance.width > 0 && !instance.definition.reactorClass.toDefinition.allReactions.isEmpty) { + // For each reactor instance, create a list regardless of whether it is a bank or not. + // Non-bank reactor instances will be a list of size 1. + pythonClassesInstantiation. + append(''' + «instance.uniqueID»_lf = [ + ''') + for (var i = 0; i < instance.totalWidth; i++) { + pythonClassesInstantiation. + append(''' + _«className»( + _bank_index = «i%instance.width», + «FOR param : instance.parameters» + «IF !param.name.equals("bank_index")» + _«param.name»=«param.pythonInitializer»,«ENDIF»«ENDFOR»), + ''') + } pythonClassesInstantiation. append(''' - «instance.uniqueID»_lf = \ - [«FOR member : instance.bankMembers SEPARATOR ", \\\n"»\ - _«className»(bank_index = «member.bankIndex/* bank_index is specially assigned by us*/»,\ - «FOR param : member.parameters SEPARATOR ", "»_«param.name»=«param.pythonInitializer»«ENDFOR»)«ENDFOR»] + ] ''') - return - } else if (instance.bankIndex === -1 && !instance.definition.reactorClass.toDefinition.allReactions.isEmpty) { - pythonClassesInstantiation.append(''' - «instance.uniqueID»_lf = \ - [_«className»(bank_index = 0«/* bank_index is specially assigned by us*/», \ - «FOR param : instance.parameters SEPARATOR ", \\\n"»_«param.name»=«param.pythonInitializer»«ENDFOR»)] - ''') } - } for (child : instance.children) { @@ -937,6 +933,8 @@ class PythonGenerator extends CGenerator { includeTargetLanguageHeaders() + pr("#include \"core/mixed_radix.h\""); + pr('#define NUMBER_OF_FEDERATES ' + federates.size); // Handle target parameters. @@ -948,20 +946,17 @@ class PythonGenerator extends CGenerator { super.includeTargetLanguageSourceFiles() super.parseTargetParameters() - - // FIXME: Probably not the best place to do - // this. - if (!targetConfig.protoFiles.isNullOrEmpty) { - // Enable support for proto serialization - enabledSerializers.add(SupportedSerializers.PROTO) - } } /** * Add necessary code to the source and necessary build supports to * enable the requested serializations in 'enabledSerializations' */ - override enableSupportForSerialization(CancelIndicator cancelIndicator) { + override enableSupportForSerializationIfApplicable(CancelIndicator cancelIndicator) { + if (!targetConfig.protoFiles.isNullOrEmpty) { + // Enable support for proto serialization + enabledSerializers.add(SupportedSerializers.PROTO) + } for (serialization : enabledSerializers) { switch (serialization) { case NATIVE: { @@ -1299,7 +1294,7 @@ class PythonGenerator extends CGenerator { 100 * federateCount / federates.size() ) // If there are no federates, compile and install the generated code - new PythonValidator(fileConfig, errorReporter, codeMaps, protoNames).doValidate(context.cancelIndicator) + new PythonValidator(fileConfig, errorReporter, codeMaps, protoNames).doValidate(context) if (!errorsOccurred() && context.mode != Mode.LSP_MEDIUM) { compilingFederatesContext.reportProgress( String.format("Validation complete. Compiling and installing %d/%d Python modules...", federateCount, federates.size()), @@ -1374,7 +1369,7 @@ class PythonGenerator extends CGenerator { * @param port The port to read from */ override generateDelayBody(Action action, VarRef port) { - val ref = generateVarRef(port); + val ref = JavaAstUtils.generateVarRef(port); // Note that the action.type set by the base class is actually // the port type. if (action.inferredType.isTokenType) { @@ -1414,18 +1409,9 @@ class PythonGenerator extends CGenerator { * @param port The port to write to. */ override generateForwardBody(Action action, VarRef port) { - val outputName = generateVarRef(port) + val outputName = JavaAstUtils.generateVarRef(port) if (action.inferredType.isTokenType) { - // Forward the entire token and prevent freeing. - // Increment the ref_count because it will be decremented - // by both the action handling code and the input handling code. - ''' - «DISABLE_REACTION_INITIALIZATION_MARKER» - self->_lf_«outputName».value = («action.inferredType.targetType»)self->_lf__«action.name».token->value; - self->_lf_«outputName».token = (lf_token_t*)self->_lf__«action.name».token; - ((lf_token_t*)self->_lf__«action.name».token)->ref_count++; - self->«getStackPortMember('''_lf_«outputName»''', "is_present")» = true; - ''' + super.generateForwardBody(action, port) } else { ''' SET(«outputName», «action.name»->token->value); @@ -1447,7 +1433,8 @@ class PythonGenerator extends CGenerator { // Delay reactors and top-level reactions used in the top-level reactor(s) in federated execution are generated in C if(reactor.name.contains(GEN_DELAY_CLASS_NAME) || ((decl === this.mainDef?.reactorClass) && reactor.isFederated)) { - return super.generateReaction(reaction, decl, reactionIndex) + super.generateReaction(reaction, decl, reactionIndex) + return } // Contains "O" characters. The number of these characters depend on the number of inputs to the reaction @@ -1662,7 +1649,7 @@ class PythonGenerator extends CGenerator { // Here, we attempt to convert the parameter value to // integer. If it succeeds, we also initialize it in C. // If it fails, we defer the initialization to Python. - var nameOfSelfStruct = selfStructName(instance) + var nameOfSelfStruct = CUtil.reactorRef(instance) for (parameter : instance.parameters) { val initializer = parameter.getInitializer try { @@ -1708,7 +1695,7 @@ class PythonGenerator extends CGenerator { override void generateReactorInstanceExtension( ReactorInstance instance, Iterable reactions ) { - var nameOfSelfStruct = selfStructName(instance) + var nameOfSelfStruct = CUtil.reactorRef(instance) var reactor = instance.definition.reactorClass.toDefinition // Delay reactors and top-level reactions used in the top-level reactor(s) in federated execution are generated in C @@ -1718,7 +1705,7 @@ class PythonGenerator extends CGenerator { ) { return } - + // Initialize the name field to the unique name of the instance pr(initializeTriggerObjects, ''' «nameOfSelfStruct»->_lf_name = "«instance.uniqueID»_lf"; @@ -1731,7 +1718,7 @@ class PythonGenerator extends CGenerator { «nameOfSelfStruct»->_lf_py_reaction_function_«reaction.index» = get_python_function("«topLevelName»", «nameOfSelfStruct»->_lf_name, - «IF (instance.bankIndex > -1)» «instance.bankIndex» «ELSE» «0» «ENDIF», + «CUtil.runtimeIndex(instance)», "«pythonFunctionName»"); ''') @@ -1740,7 +1727,7 @@ class PythonGenerator extends CGenerator { «nameOfSelfStruct»->_lf_py_deadline_function_«reaction.index» = get_python_function("«topLevelName»", «nameOfSelfStruct»->_lf_name, - «IF (instance.bankIndex > -1)» «instance.bankIndex» «ELSE» «0» «ENDIF», + «CUtil.runtimeIndex(instance)», "deadline_function_«reaction.index»"); ''') } @@ -1809,24 +1796,18 @@ class PythonGenerator extends CGenerator { StringBuilder pyObjectDescriptor, StringBuilder pyObjects, VarRef port, - ReactorDecl decl - ) - { - if(port.variable instanceof Input) - { + ReactorDecl decl + ) { + if (port.variable instanceof Input) { generateInputVariablesToSendToPythonReaction(pyObjectDescriptor, pyObjects, port.variable as Input, decl) - } - else - { - if(!(port.variable as Port).isMultiport) - { + } else { + if (!JavaAstUtils.isMultiport(port.variable as Port)) { pyObjectDescriptor.append("O") pyObjects.append(''', convert_C_port_to_py(«port.container.name».«port.variable.name», -2)''') - } - else - { + } else { pyObjectDescriptor.append("O") - pyObjects.append(''', convert_C_port_to_py(«port.container.name».«port.variable.name», «port.container.name».«port.variable.name»_width) ''') + pyObjects. + append(''', convert_C_port_to_py(«port.container.name».«port.variable.name», «port.container.name».«port.variable.name»_width) ''') } } } @@ -1844,22 +1825,24 @@ class PythonGenerator extends CGenerator { StringBuilder pyObjectDescriptor, StringBuilder pyObjects, Output output, - ReactorDecl decl - ) - { - // Unfortunately, for the SET macros to work out-of-the-box for - // multiports, we need an array of *pointers* to the output structs, - // but what we have on the self struct is an array of output structs. - // So we have to handle multiports specially here a construct that - // array of pointers. - if (!output.isMultiport) { - pyObjectDescriptor.append("O") - pyObjects.append(''', convert_C_port_to_py(«output.name», -2)''') - } else if (output.isMultiport) { - // Set the _width variable. - pyObjectDescriptor.append("O") - pyObjects.append(''', convert_C_port_to_py(«output.name»,«output.name»_width) ''') - } + ReactorDecl decl + ) { + // Unfortunately, for the SET macros to work out-of-the-box for + // multiports, we need an array of *pointers* to the output structs, + // but what we have on the self struct is an array of output structs. + // So we have to handle multiports specially here a construct that + // array of pointers. + // FIXME: The C Generator also has this awkwardness. It makes the code generators + // unnecessarily difficult to maintain, and it may have performance consequences as well. + // Maybe we should change the SET macros. + if (!JavaAstUtils.isMultiport(output)) { + pyObjectDescriptor.append("O") + pyObjects.append(''', convert_C_port_to_py(«output.name», -2)''') + } else { + // Set the _width variable. + pyObjectDescriptor.append("O") + pyObjects.append(''', convert_C_port_to_py(«output.name»,«output.name»_width) ''') + } } /** Generate into the specified string builder the code to @@ -1877,7 +1860,7 @@ class PythonGenerator extends CGenerator { ReactorDecl decl ) { - if(input.isMultiport) + if(JavaAstUtils.isMultiport(input)) { pyObjectDescriptor.append("O") pyObjects.append(''', convert_C_port_to_py(«definition.name».«input.name», «definition.name».«input.name»_width)''') @@ -1913,16 +1896,16 @@ class PythonGenerator extends CGenerator { // depending on whether the input is mutable, whether it is a multiport, // and whether it is a token type. // Easy case first. - if (!input.isMutable && !input.isMultiport) { + if (!input.isMutable && !JavaAstUtils.isMultiport(input)) { // Non-mutable, non-multiport, primitive type. pyObjectDescriptor.append("O") pyObjects.append(''', convert_C_port_to_py(«input.name», «input.name»_width)''') - } else if (input.isMutable && !input.isMultiport) { + } else if (input.isMutable && !JavaAstUtils.isMultiport(input)) { // Mutable, non-multiport, primitive type. // TODO: handle mutable pyObjectDescriptor.append("O") pyObjects.append(''', convert_C_port_to_py(«input.name», «input.name»_width)''') - } else if (!input.isMutable && input.isMultiport) { + } else if (!input.isMutable && JavaAstUtils.isMultiport(input)) { // Non-mutable, multiport, primitive. // TODO: support multiports pyObjectDescriptor.append("O") diff --git a/org.lflang/src/org/lflang/generator/python/PythonTypes.java b/org.lflang/src/org/lflang/generator/python/PythonTypes.java new file mode 100644 index 0000000000..c066494047 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonTypes.java @@ -0,0 +1,54 @@ +package org.lflang.generator.python; + +import java.util.regex.Pattern; + +import org.lflang.ErrorReporter; +import org.lflang.InferredType; +import org.lflang.generator.c.CTypes; + +public class PythonTypes extends CTypes { + + // Regular expression pattern for pointer types. The star at the end has to be visible. + static final Pattern pointerPatternVariable = Pattern.compile("^\\s*+(\\w+)\\s*\\*\\s*$"); + + /** + * Initializes a {@code CTargetTypes} with the given + * error reporter. + * + * @param errorReporter The error reporter for any + * errors raised in the code + * generation process. + */ + public PythonTypes(ErrorReporter errorReporter) { + super(errorReporter); + } + + @Override + public String getTargetUndefinedType() { + return "PyObject*"; + } + + /** + * This generator inherits types from the CGenerator. + * This function reverts them back to Python types + * For example, the types double is converted to float, + * the * for pointer types is removed, etc. + * @param type The type + * @return The Python equivalent of a C type + */ + public String getPythonType(InferredType type) { + var result = super.getTargetType(type); + + switch(result){ + case "double": result = "float"; + case "string": result = "object"; + } + + var matcher = pointerPatternVariable.matcher(result); + if(matcher.find()) { + return matcher.group(1); + } + + return result; + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonValidator.java b/org.lflang/src/org/lflang/generator/python/PythonValidator.java index dbbd60fa89..f68a6520ef 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonValidator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonValidator.java @@ -21,6 +21,8 @@ import org.lflang.generator.Validator; import org.lflang.util.LFCommand; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -62,8 +64,8 @@ private static final class PylintMessage { private String obj; private int line; private int column; - private int endLine; - private int endColumn; + private Integer endLine; + private Integer endColumn; private Path path; private String symbol; private String message; @@ -81,7 +83,10 @@ private static final class PylintMessage { @JsonProperty("message-id") public void setMessageId(String messageId) { this.messageId = messageId; } public Position getStart() { return Position.fromZeroBased(line - 1, column); } - public Position getEnd() { return Position.fromZeroBased(endLine - 1, endColumn); } + public Position getEnd() { + return endLine == null || endColumn == null ? getStart().plus(" ") : + Position.fromZeroBased(endLine - 1, endColumn); + } public Path getPath(Path relativeTo) { return relativeTo.resolve(path); } public DiagnosticSeverity getSeverity() { // The following is consistent with VS Code's default behavior for pure Python: @@ -272,8 +277,10 @@ public Strategy getOutputReportingStrategy() { } } } catch (JsonProcessingException e) { - // This should be impossible unless Pylint's API changes. Maybe it's fine to fail quietly - // like this in case that happens -- this will go to stderr, so you can see it if you look. + errorReporter.reportWarning( + "Failed to parse linter output. The Lingua Franca code generator is tested with Pylint " + + "version 2.12.2. Consider updating PyLint if you have an older version." + ); e.printStackTrace(); } }; diff --git a/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt b/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt index 1ff693f01f..f69304a044 100644 --- a/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt +++ b/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt @@ -62,8 +62,7 @@ class RustGenerator( fileConfig: RustFileConfig, errorReporter: ErrorReporter, @Suppress("UNUSED_PARAMETER") unused: LFGlobalScopeProvider -) : GeneratorBase(fileConfig, errorReporter), - TargetTypes by RustTypes { +) : GeneratorBase(fileConfig, errorReporter) { override fun doGenerate(resource: Resource, fsa: IFileSystemAccess2, context: LFGeneratorContext) { super.doGenerate(resource, fsa, context) @@ -86,7 +85,7 @@ class RustGenerator( ) val exec = fileConfig.binPath.toAbsolutePath().resolve(gen.executableName) Files.deleteIfExists(exec) // cleanup, cargo doesn't do it - if (context.mode == TargetConfig.Mode.LSP_MEDIUM) RustValidator(fileConfig, errorReporter, codeMaps).doValidate(context.cancelIndicator) + if (context.mode == TargetConfig.Mode.LSP_MEDIUM) RustValidator(fileConfig, errorReporter, codeMaps).doValidate(context) else invokeRustCompiler(context, gen.executableName, codeMaps) } } @@ -128,7 +127,8 @@ class RustGenerator( fileConfig.srcGenPath.toAbsolutePath() ) ?: return - val cargoReturnCode = RustValidator(fileConfig, errorReporter, codeMaps).run(cargoCommand, context.cancelIndicator) + val cargoReturnCode = if (context.mode == TargetConfig.Mode.STANDALONE) cargoCommand.run() else + RustValidator(fileConfig, errorReporter, codeMaps).run(cargoCommand, context.cancelIndicator) if (cargoReturnCode == 0) { println("SUCCESS (compiling generated Rust code)") @@ -146,6 +146,7 @@ class RustGenerator( override fun getTarget(): Target = Target.Rust + override fun getTargetTypes(): TargetTypes = RustTypes override fun generateDelayBody(action: Action, port: VarRef): String { TODO("Not yet implemented") diff --git a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt index 7c3669a909..69b218a82a 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt @@ -32,7 +32,7 @@ import org.lflang.ErrorReporter import org.lflang.inferredType import org.lflang.InferredType import org.lflang.TimeValue -import org.lflang.ASTUtils.isInitialized +import org.lflang.JavaAstUtils import org.lflang.Target import org.lflang.TargetConfig.Mode import org.lflang.federated.launcher.FedTSLauncher @@ -57,6 +57,8 @@ import org.lflang.generator.IntegratedBuilder import org.lflang.generator.JavaGeneratorUtils import org.lflang.generator.LFGeneratorContext import org.lflang.generator.PrependOperator +import org.lflang.generator.TargetTypes +import org.lflang.generator.ValueGenerator import org.lflang.generator.SubContext import java.nio.file.Path import java.nio.file.StandardCopyOption @@ -100,6 +102,17 @@ class TSGenerator( "reactor.ts", "microtime.d.ts", "nanotimer.d.ts", "time.ts", "ulog.d.ts", "util.ts") + private val VG = ValueGenerator(::timeInTargetLanguage) { param -> "this.${param.name}.get()" } + + private fun timeInTargetLanguage(value: TimeValue): String { + return if (value.unit != null) { + "TimeValue.${value.unit.canonicalName}(${value.magnitude})" + } else { + // The value must be zero. + "TimeValue.zero()" + } + } + /** * The percent progress associated with having collected all JS/TS dependencies. */ @@ -116,15 +129,15 @@ class TSGenerator( // Wrappers to expose GeneratorBase methods. fun federationRTIPropertiesW() = federationRTIProperties - fun getTargetValueW(v: Value): String = getTargetValue(v) - fun getTargetTypeW(p: Parameter): String = getTargetType(p.inferredType) - fun getTargetTypeW(state: StateVar): String = getTargetType(state) - fun getTargetTypeW(t: Type): String = getTargetType(t) + fun getTargetValueW(v: Value): String = VG.getTargetValue(v, false) + fun getTargetTypeW(p: Parameter): String = TSTypes.getTargetType(p.inferredType) + fun getTargetTypeW(state: StateVar): String = TSTypes.getTargetType(state) + fun getTargetTypeW(t: Type): String = TSTypes.getTargetType(t) - fun getInitializerListW(state: StateVar): List = getInitializerList(state) - fun getInitializerListW(param: Parameter): List = getInitializerList(param) + fun getInitializerListW(state: StateVar): List = VG.getInitializerList(state) + fun getInitializerListW(param: Parameter): List = VG.getInitializerList(param) fun getInitializerListW(param: Parameter, i: Instantiation): List = - getInitializerList(param, i) + VG.getInitializerList(param, i) /** Generate TypeScript code from the Lingua Franca model contained by the * specified resource. This is the main entry point for code @@ -275,7 +288,7 @@ class TSGenerator( private fun compile(parsingContext: LFGeneratorContext) { - refreshProject() + JavaGeneratorUtils.refreshProject(parsingContext.mode, code.toString()) if (parsingContext.cancelIndicator.isCanceled) return parsingContext.reportProgress("Transpiling to JavaScript...", 70) @@ -317,7 +330,7 @@ class TSGenerator( val ret = pnpmInstall.run(context.cancelIndicator) if (ret != 0) { val errors: String = pnpmInstall.errors.toString() - errorReporter.reportError(findTarget(resource), + errorReporter.reportError(JavaGeneratorUtils.findTarget(resource), "ERROR: pnpm install command failed" + if (errors.isBlank()) "." else ":\n$errors") } } else { @@ -332,9 +345,11 @@ class TSGenerator( } if (npmInstall.run(context.cancelIndicator) != 0) { - errorReporter.reportError(findTarget(resource), + errorReporter.reportError( + JavaGeneratorUtils.findTarget(resource), "ERROR: npm install command failed: " + npmInstall.errors.toString()) - errorReporter.reportError(findTarget(resource), "ERROR: npm install command failed." + + errorReporter.reportError( + JavaGeneratorUtils.findTarget(resource), "ERROR: npm install command failed." + "\nFor installation instructions, see: https://www.npmjs.com/get-npm") return } @@ -389,10 +404,10 @@ class TSGenerator( */ private fun passesChecks(validator: TSValidator, parsingContext: LFGeneratorContext): Boolean { parsingContext.reportProgress("Linting generated code...", 0) - validator.doLint(parsingContext.cancelIndicator) + validator.doLint(parsingContext) if (errorsOccurred()) return false parsingContext.reportProgress("Validating generated code...", 25) - validator.doValidate(parsingContext.cancelIndicator) + validator.doValidate(parsingContext) return !errorsOccurred() } @@ -471,6 +486,8 @@ class TSGenerator( return true } + override fun getTargetTypes(): TargetTypes = TSTypes + /** * Return a TS type for the specified action. * If the type has not been specified, return @@ -480,39 +497,12 @@ class TSGenerator( */ private fun getActionType(action: Action): String { return if (action.type != null) { - getTargetType(action.type) + TSTypes.getTargetType(action.type) } else { "Present" } } - /** Given a representation of time that may possibly include units, - * return a string that TypeScript can recognize as a value. - * @param value Literal that represents a time value. - * @return A string, as "[ timeLiteral, TimeUnit.unit]" . - */ - override fun timeInTargetLanguage(value: TimeValue): String { - return if (value.unit != null) { - "TimeValue.${value.unit.canonicalName}(${value.magnitude})" - } else { - // The value must be zero. - "TimeValue.zero()" - } - } - - override fun getTargetType(s: StateVar): String { - val type = super.getTargetType(s) - return if (!isInitialized(s)) { - "$type | undefined" - } else { - type - } - } - - override fun getTargetReference(param: Parameter): String { - return "this.${param.name}.get()" - } - /** * Generate code for the body of a reaction that handles the * action that is triggered by receiving a message from a remote @@ -634,7 +624,7 @@ class TSGenerator( * Add necessary code to the source and necessary build supports to * enable the requested serializations in 'enabledSerializations' */ - override fun enableSupportForSerialization(cancelIndicator: CancelIndicator?) { + override fun enableSupportForSerializationIfApplicable(cancelIndicator: CancelIndicator?) { for (serializer in enabledSerializers) { when (serializer) { SupportedSerializers.NATIVE -> { @@ -648,41 +638,17 @@ class TSGenerator( // Virtual methods. override fun generateDelayBody(action: Action, port: VarRef): String { - return "actions.${action.name}.schedule(0, ${generateVarRef(port)} as ${getActionType(action)});" + return "actions.${action.name}.schedule(0, ${JavaAstUtils.generateVarRef(port)} as ${getActionType(action)});" } override fun generateForwardBody(action: Action, port: VarRef): String { - return "${generateVarRef(port)} = ${action.name} as ${getActionType(action)};" + return "${JavaAstUtils.generateVarRef(port)} = ${action.name} as ${getActionType(action)};" } override fun generateDelayGeneric(): String { return "T extends Present" } - override fun supportsGenerics(): Boolean { - return true - } - - override fun getTargetTimeType(): String { - return "TimeValue" - } - - override fun getTargetTagType(): String { - return "TimeValue" - } - - override fun getTargetUndefinedType(): String { - return "Present" - } - - override fun getTargetFixedSizeListType(baseType: String, size: Int): String { - return "Array($size)<$baseType>" - } - - override fun getTargetVariableSizeListType(baseType: String): String { - return "Array<$baseType>" - } - override fun getTarget(): Target { return Target.TS } diff --git a/org.lflang/src/org/lflang/generator/ts/TSReactionGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSReactionGenerator.kt index 8bd31ceb3d..e82f5693e7 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSReactionGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSReactionGenerator.kt @@ -1,6 +1,7 @@ package org.lflang.generator.ts import org.lflang.ErrorReporter +import org.lflang.JavaAstUtils import org.lflang.federated.FederateInstance import org.lflang.generator.PrependOperator import org.lflang.lf.* @@ -30,7 +31,7 @@ class TSReactionGenerator( private fun StateVar.getTargetType(): String = tsGenerator.getTargetTypeW(this) private fun Type.getTargetType(): String = tsGenerator.getTargetTypeW(this) - private fun VarRef.generateVarRef(): String = tsGenerator.generateVarRef(this) + private fun VarRef.generateVarRef(): String = JavaAstUtils.generateVarRef(this) /** * Return a TS type for the specified action. diff --git a/org.lflang/src/org/lflang/generator/ts/TSStateGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSStateGenerator.kt index e662aeb799..58ed7d4eee 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSStateGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSStateGenerator.kt @@ -1,7 +1,6 @@ package org.lflang.generator.ts import org.lflang.ASTUtils -import org.lflang.generator.PrependOperator import org.lflang.lf.StateVar import java.util.* diff --git a/org.lflang/src/org/lflang/generator/ts/TSTypes.kt b/org.lflang/src/org/lflang/generator/ts/TSTypes.kt new file mode 100644 index 0000000000..d996f5638e --- /dev/null +++ b/org.lflang/src/org/lflang/generator/ts/TSTypes.kt @@ -0,0 +1,41 @@ +package org.lflang.generator.ts + +import org.lflang.ASTUtils +import org.lflang.generator.TargetTypes +import org.lflang.lf.StateVar + +object TSTypes : TargetTypes { + + override fun getTargetType(s: StateVar): String { + val type = super.getTargetType(s) + return if (!ASTUtils.isInitialized(s)) { + "$type | undefined" + } else { + type + } + } + + override fun supportsGenerics(): Boolean { + return true + } + + override fun getTargetTimeType(): String { + return "TimeValue" + } + + override fun getTargetTagType(): String { + return "TimeValue" + } + + override fun getTargetUndefinedType(): String { + return "Present" + } + + override fun getTargetFixedSizeListType(baseType: String, size: Int): String { + return "Array($size)<$baseType>" + } + + override fun getTargetVariableSizeListType(baseType: String): String { + return "Array<$baseType>" + } +} \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/ts/TSValidator.kt b/org.lflang/src/org/lflang/generator/ts/TSValidator.kt index e217c9d975..0f361d2286 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSValidator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSValidator.kt @@ -9,6 +9,7 @@ import org.lflang.ErrorReporter import org.lflang.generator.CodeMap import org.lflang.generator.DiagnosticReporting import org.lflang.generator.HumanReadableReportingStrategy +import org.lflang.generator.LFGeneratorContext import org.lflang.generator.Position import org.lflang.generator.ValidationStrategy import org.lflang.generator.Validator @@ -152,9 +153,12 @@ class TSValidator( /** * Run a relatively fast linter on the generated code. - * @param cancelIndicator The indicator of whether this build process is cancelled. + * @param context The context of the current build. */ - fun doLint(cancelIndicator: CancelIndicator) { - TSLinter(fileConfig, errorReporter, codeMaps).doValidate(cancelIndicator) + fun doLint(context: LFGeneratorContext) { + TSLinter(fileConfig, errorReporter, codeMaps).doValidate(context) } + + // If this is not true, then the user might as well be writing JavaScript. + override fun validationEnabledByDefault(context: LFGeneratorContext?): Boolean = true } diff --git a/org.lflang/src/org/lflang/graph/TopologyGraph.java b/org.lflang/src/org/lflang/graph/TopologyGraph.java index a73e1733c7..7f45915737 100644 --- a/org.lflang/src/org/lflang/graph/TopologyGraph.java +++ b/org.lflang/src/org/lflang/graph/TopologyGraph.java @@ -26,12 +26,14 @@ package org.lflang.graph; -import java.util.Collection; import java.util.Arrays; +import java.util.Collection; + import org.lflang.generator.NamedInstance; import org.lflang.generator.PortInstance; import org.lflang.generator.ReactionInstance; import org.lflang.generator.ReactorInstance; +import org.lflang.generator.SendRange; import org.lflang.generator.TriggerInstance; import org.lflang.lf.Connection; import org.lflang.lf.Variable; @@ -40,6 +42,9 @@ * A graph with vertices that are ports or reactions and edges that denote * dependencies between them. * + * NOTE: This is not used anywhere anymore, but we keep it in case this particular + * graph structure proves useful in the future. + * * @author Marten Lohstroh */ public class TopologyGraph extends PrecedenceGraph> { @@ -111,9 +116,11 @@ private void addEffects(ReactionInstance reaction) { if (effect instanceof PortInstance) { addEdge(effect, reaction); PortInstance orig = (PortInstance) effect; - orig.getDependentPorts().forEach(dest -> { - recordDependency(reaction, orig, dest.getPortInstance()); - }); + for (SendRange sendRange : orig.getDependentPorts()) { + sendRange.destinations.forEach(dest -> { + recordDependency(reaction, orig, dest.instance, sendRange.connection); + }); + } } } } @@ -132,7 +139,9 @@ private void addSources(ReactionInstance reaction) { addEdge(reaction, source); PortInstance dest = (PortInstance) source; dest.getDependsOnPorts().forEach(orig -> { - recordDependency(reaction, orig.getPortInstance(), dest); + // FIXME: Don't have connection information here, hence the null argument. + // This will like result in invalid cycle detection. + recordDependency(reaction, orig.instance, dest, null); }); } } @@ -142,39 +151,20 @@ private void addSources(ReactionInstance reaction) { * Record a dependency between two port instances, but only if there is a * zero-delay path from origin to destination. * - * @param reaction A reaction that has one of the given port as a source or + * @param reaction A reaction that has one of the given ports as a source or * effect. * @param orig The upstream port. * @param dest The downstream port. + * @param connection The connection creating this dependency or null if not + * created by a connection. */ private void recordDependency(ReactionInstance reaction, PortInstance orig, - PortInstance dest) { - // Note: a reaction always has a parent, but it might not have a - // grandparent. Hence, the first argument given to getConnection might - // be null. - if (!dependencyBroken( - getConnection(reaction.getParent().getParent(), orig, dest))) { + PortInstance dest, Connection connection) { + if (!dependencyBroken(connection)) { addEdge(dest, orig); } } - /** - * Look up the AST node that describes the connection between the two given - * port instances and return it if there is one. - * - * @param container The reactor instance in which to perform the look up. - * @param orig The upstream port. - * @param dest The downstream port. - * @return The corresponding Connection object or null if there is none. - */ - private Connection getConnection(ReactorInstance container, - PortInstance orig, PortInstance dest) { - if (container == null) { - return null; - } - return container.getConnection(orig, dest); - } - /** * Report whether or not the given connection breaks dependencies or not. * diff --git a/org.lflang/src/org/lflang/util/XtendUtil.java b/org.lflang/src/org/lflang/util/XtendUtil.java index 0cf944cee4..4a89307b4e 100644 --- a/org.lflang/src/org/lflang/util/XtendUtil.java +++ b/org.lflang/src/org/lflang/util/XtendUtil.java @@ -24,10 +24,15 @@ package org.lflang.util; +import java.util.Iterator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + /** * A utility class for things missing from Xtend. * * @author Clément Fournier + * @author Marten Lohstroh */ public final class XtendUtil { @@ -36,10 +41,32 @@ private XtendUtil() { } /** - * Returns the bitwise OR of the two given long integers. + * Return the bitwise OR of the two given long integers. * Xtend doesn't support bitwise operators. */ public static long longOr(long a, long b) { return a | b; } + + /** + * Turn an iterator into a sequential stream. + * + * @param iterator The iterator to create a sequential stream for. + * @return A stream. + */ + public static Stream asStream(Iterator iterator) { + return asStream(iterator, false); + } + + /** + * Turn an iterator into a sequential or parallel stream. + * + * @param iterator The iterator to create a stream for. + * @param parallel Whether or not the stream should be parallel. + * @return A stream. + */ + public static Stream asStream(Iterator iterator, boolean parallel) { + Iterable iterable = () -> iterator; + return StreamSupport.stream(iterable.spliterator(), parallel); + } } diff --git a/org.lflang/src/org/lflang/validation/LFValidator.xtend b/org.lflang/src/org/lflang/validation/LFValidator.xtend index 902d3ea405..741401dca5 100644 --- a/org.lflang/src/org/lflang/validation/LFValidator.xtend +++ b/org.lflang/src/org/lflang/validation/LFValidator.xtend @@ -368,32 +368,30 @@ class LFValidator extends BaseLFValidator { def checkConnection(Connection connection) { // Report if connection is part of a cycle. - for (cycle : this.info.topologyCycles()) { - for (lp : connection.leftPorts) { - for (rp : connection.rightPorts) { - var leftInCycle = false - val reactorName = (connection.eContainer as Reactor).name - - if ((lp.container === null && cycle.exists [ - it.definition === lp.variable - ]) || cycle.exists [ - (it.definition === lp.variable && it.parent === lp.container) - ]) { - leftInCycle = true - } + val cycles = this.info.topologyCycles(); + for (lp : connection.leftPorts) { + for (rp : connection.rightPorts) { + var leftInCycle = false + if ((lp.container === null && cycles.exists [ + it.definition === lp.variable + ]) || cycles.exists [ + (it.definition === lp.variable && it.parent === lp.container) + ]) { + leftInCycle = true + } - if ((rp.container === null && cycle.exists [ - it.definition === rp.variable - ]) || cycle.exists [ - (it.definition === rp.variable && it.parent === rp.container) - ]) { - if (leftInCycle) { - // Only report of _both_ reference ports are in the cycle. - error('''Connection in reactor «reactorName» creates ''' + - '''a cyclic dependency between «lp.toText» and ''' + - '''«rp.toText».''', Literals.CONNECTION__DELAY - ) - } + if ((rp.container === null && cycles.exists [ + it.definition === rp.variable + ]) || cycles.exists [ + (it.definition === rp.variable && it.parent === rp.container) + ]) { + if (leftInCycle) { + val reactorName = (connection.eContainer as Reactor).name + // Only report of _both_ reference ports are in the cycle. + error('''Connection in reactor «reactorName» creates ''' + + '''a cyclic dependency between «lp.toText» and ''' + + '''«rp.toText».''', Literals.CONNECTION__DELAY + ) } } } @@ -856,66 +854,64 @@ class LFValidator extends BaseLFValidator { } // Report error if this reaction is part of a cycle. - for (cycle : this.info.topologyCycles()) { - val reactor = (reaction.eContainer) as Reactor - if (cycle.exists[it.definition === reaction]) { - // Report involved triggers. - val trigs = new ArrayList() - reaction.triggers.forEach [ t | - (t instanceof VarRef && cycle.exists [ c | - c.definition === (t as VarRef).variable - ]) ? trigs.add((t as VarRef).toText) : { - } - ] - if (trigs.size > 0) { - error('''Reaction triggers involved in cyclic dependency in reactor «reactor.name»: «trigs.join(', ')».''', - Literals.REACTION__TRIGGERS) + val cycles = this.info.topologyCycles(); + val reactor = (reaction.eContainer) as Reactor + if (cycles.exists[it.definition === reaction]) { + // Report involved triggers. + val trigs = new ArrayList() + reaction.triggers.forEach [ t | + (t instanceof VarRef && cycles.exists [ c | + c.definition === (t as VarRef).variable + ]) ? trigs.add((t as VarRef).toText) : { } + ] + if (trigs.size > 0) { + error('''Reaction triggers involved in cyclic dependency in reactor «reactor.name»: «trigs.join(', ')».''', + Literals.REACTION__TRIGGERS) + } - // Report involved sources. - val sources = new ArrayList() - reaction.sources.forEach [ t | - (cycle.exists[c|c.definition === t.variable]) - ? sources.add(t.toText) - : { - } - ] - if (sources.size > 0) { - error('''Reaction sources involved in cyclic dependency in reactor «reactor.name»: «sources.join(', ')».''', - Literals.REACTION__SOURCES) + // Report involved sources. + val sources = new ArrayList() + reaction.sources.forEach [ t | + (cycles.exists[c|c.definition === t.variable]) + ? sources.add(t.toText) + : { } + ] + if (sources.size > 0) { + error('''Reaction sources involved in cyclic dependency in reactor «reactor.name»: «sources.join(', ')».''', + Literals.REACTION__SOURCES) + } - // Report involved effects. - val effects = new ArrayList() - reaction.effects.forEach [ t | - (cycle.exists[c|c.definition === t.variable]) - ? effects.add(t.toText) - : { - } - ] - if (effects.size > 0) { - error('''Reaction effects involved in cyclic dependency in reactor «reactor.name»: «effects.join(', ')».''', - Literals.REACTION__EFFECTS) + // Report involved effects. + val effects = new ArrayList() + reaction.effects.forEach [ t | + (cycles.exists[c|c.definition === t.variable]) + ? effects.add(t.toText) + : { } + ] + if (effects.size > 0) { + error('''Reaction effects involved in cyclic dependency in reactor «reactor.name»: «effects.join(', ')».''', + Literals.REACTION__EFFECTS) + } - if (trigs.size + sources.size == 0) { - error( - '''Cyclic dependency due to preceding reaction. Consider reordering reactions within reactor «reactor.name» to avoid causality loop.''', - reaction.eContainer, - Literals.REACTOR__REACTIONS, - reactor.reactions.indexOf(reaction)) - } else if (effects.size == 0) { - error( - '''Cyclic dependency due to succeeding reaction. Consider reordering reactions within reactor «reactor.name» to avoid causality loop.''', + if (trigs.size + sources.size == 0) { + error( + '''Cyclic dependency due to preceding reaction. Consider reordering reactions within reactor «reactor.name» to avoid causality loop.''', reaction.eContainer, - Literals.REACTOR__REACTIONS, - reactor.reactions.indexOf(reaction)) - } - // Not reporting reactions that are part of cycle _only_ due to reaction ordering. - // Moving them won't help solve the problem. + Literals.REACTOR__REACTIONS, + reactor.reactions.indexOf(reaction)) + } else if (effects.size == 0) { + error( + '''Cyclic dependency due to succeeding reaction. Consider reordering reactions within reactor «reactor.name» to avoid causality loop.''', + reaction.eContainer, + Literals.REACTOR__REACTIONS, + reactor.reactions.indexOf(reaction)) } + // Not reporting reactions that are part of cycle _only_ due to reaction ordering. + // Moving them won't help solve the problem. } - // FIXME: improve error message. } @Check(FAST) diff --git a/test/C/src/federated/DistributedNetworkOrder.lf b/test/C/src/federated/DistributedNetworkOrder.lf index a30de8f138..d17de9cbc6 100644 --- a/test/C/src/federated/DistributedNetworkOrder.lf +++ b/test/C/src/federated/DistributedNetworkOrder.lf @@ -10,13 +10,14 @@ */ target C { - timeout: 1 sec + timeout: 1 sec, + build-type: RelWithDebInfo // Release with debug info }; reactor Sender { output out:int; timer t(0, 1 msec); - reaction(t) {= + reaction(t) -> out {= int payload = 1; if (get_elapsed_logical_time() == 0LL) { send_timed_message(MSEC(10), MSG_TYPE_TAGGED_MESSAGE, 0, 1, "federate 1", sizeof(int), diff --git a/test/C/src/multiport/BankReactionsInContainer.lf b/test/C/src/multiport/BankReactionsInContainer.lf new file mode 100644 index 0000000000..3a5886196e --- /dev/null +++ b/test/C/src/multiport/BankReactionsInContainer.lf @@ -0,0 +1,72 @@ +/** + * This tests an output that is broadcast back to a multiport input of a bank. + */ +target C { + timeout: 1 sec, +}; +reactor R (bank_index:int(0)) { + output[2] out:int; + input[2] in:int; + state received:bool(false); + + reaction(startup) -> out {= + for (int i = 0; i < out_width; i++) { + int value = self->bank_index * 2 + i; + SET(out[i], value); + info_print("Inner sending %d to bank %d channel %d.", + value, self->bank_index, i + ); + } + =} + + reaction(in) {= + for (int i = 0; i < in_width; i++) { + if (in[i]->is_present) { + info_print("Inner received %d in bank %d, channel %d", in[i]->value, self->bank_index, i); + self->received = true; + if (in[i]->value != self->bank_index * 2 + i) { + error_print_and_exit("Expected %d.", self->bank_index * 2 + i); + } + } + } + =} + reaction(shutdown) {= + info_print("Inner shutdown invoked."); + if (!self->received) { + error_print_and_exit("Received no input."); + } + =} +} +main reactor { + s = new[2] R(); + state received:bool(false); + + reaction(startup) -> s.in {= + int count = 0; + for (int i = 0; i < s_width; i++) { + for (int j = 0; j < s[i].in_width; j++) { + info_print("Sending %d to bank %d channel %d.", count, i, j); + SET(s[i].in[j], count++); + } + } + =} + reaction(s.out) {= + for (int j = 0; j < s_width; j++) { + for (int i = 0; i < s[j].out_width; i++) { + if (s[j].out[i]->is_present) { + info_print("Outer received %d on bank %d channel %d.", s[j].out[i]->value, j, i); + self->received = true; + if (s[j].out[i]->value != j * 2 + i) { + error_print_and_exit("Expected %d.", j * 2 + i); + } + } + } + } + =} + reaction(shutdown) {= + info_print("Outer shutdown invoked."); + if (!self->received) { + error_print_and_exit("Received no input."); + } + =} +} diff --git a/test/C/src/multiport/NestedBanks.lf b/test/C/src/multiport/NestedBanks.lf new file mode 100644 index 0000000000..eecbf4bc55 --- /dev/null +++ b/test/C/src/multiport/NestedBanks.lf @@ -0,0 +1,69 @@ +/** + * Test of nested banks with multiports. + * @author Edward A. Lee + */ +target C; +main reactor { + a = new[2] A(); + c = new[3] C(); + d = new D(); + e = new E(); + + (a.x)+ -> c.z, d.u, e.t; +} +reactor A(bank_index:int(0)) { + output[4] x:int; + b = new[2] B(a_bank_index = bank_index); + b.y -> x; +} +reactor B(a_bank_index:int(0), bank_index:int(0)) { + output[2] y:int; + reaction(startup) -> y {= + int base = self->a_bank_index * 4 + self->bank_index * 2; + SET(y[0], base); + SET(y[1], base + 1); + =} +} +reactor C(bank_index:int(0)) { + input[2] z:int; + f = new F(c_bank_index = bank_index); + g = new G(c_bank_index = bank_index); + z -> f.w, g.s; +} +reactor D { + input[2] u:int; + reaction(u) {= + for (int i = 0; i < u_width; i++) { + info_print("d.u[%d] received %d.", i, u[i]->value); + if (u[i]->value != 6 + i) { + error_print_and_exit("Expected %d but received %d.", 6 + i, u[i]->value); + } + } + =} +} +reactor E { + input[8] t:int; + reaction(t) {= + for (int i = 0; i < t_width; i++) { + info_print("e.t[%d] received %d.", i, t[i]->value); + } + =} +} +reactor F(c_bank_index:int(0)) { + input w:int; + reaction(w) {= + info_print("c[%d].f.w received %d.", self->c_bank_index, w->value); + if (w->value != self->c_bank_index * 2) { + error_print_and_exit("Expected %d but received %d.", self->c_bank_index * 2, w->value); + } + =} +} +reactor G(c_bank_index:int(0)) { + input s:int; + reaction(s) {= + info_print("c[%d].g.s received %d.", self->c_bank_index, s->value); + if (s->value != self->c_bank_index * 2 + 1) { + error_print_and_exit("Expected %d but received %d.", self->c_bank_index * 2 + 1, s->value); + } + =} +} \ No newline at end of file diff --git a/test/C/src/multiport/NestedInterleavedBanks.lf b/test/C/src/multiport/NestedInterleavedBanks.lf new file mode 100644 index 0000000000..be4f087576 --- /dev/null +++ b/test/C/src/multiport/NestedInterleavedBanks.lf @@ -0,0 +1,36 @@ +/** + * Test nested banks with interleaving. + * @author Edward A. Lee + */ +target C; +reactor A(bank_index:int(0), outer_bank_index:int(0)) { + output[2] p:int; + reaction(startup) -> p {= + for (int i = 0; i < p_width; i++) { + SET(p[i], self->outer_bank_index * 4 + self->bank_index * 2 + i + 1); + info_print("A sending %d.", p[i]->value); + } + =} +} +reactor B(bank_index:int(0)) { + output[4] q:int; + a = new[2] A(outer_bank_index = bank_index); + interleaved(a.p) -> q; +} +reactor C { + input[8] i:int; + reaction(i) {= + int expected[] = {1, 3, 2, 4, 5, 7, 6, 8}; + for(int j = 0; j < i_width; j++) { + info_print("C received %d.", i[j]->value); + if (i[j]->value != expected[j]) { + error_print_and_exit("Expected %d.", expected[j]); + } + } + =} +} +main reactor { + b = new[2] B(); + c = new C(); + b.q -> c.i; +} \ No newline at end of file diff --git a/test/C/src/multiport/ReactionsToNested.lf b/test/C/src/multiport/ReactionsToNested.lf new file mode 100644 index 0000000000..daf3e8b195 --- /dev/null +++ b/test/C/src/multiport/ReactionsToNested.lf @@ -0,0 +1,38 @@ +// This test connects a simple counting source to tester +// that checks against its own count. +target C { + timeout: 1 sec +}; +reactor T(expected:int(0)) { + input z:int; + state received:bool(false); + reaction(z) {= + info_print("T received %d", z->value); + self->received = true; + if (z->value != self->expected) { + error_print_and_exit("Expected %d", self->expected); + } + =} + reaction(shutdown) {= + if (!self->received) { + error_print_and_exit("No input received"); + } + =} +} + +reactor D { + input[2] y:int; + t1 = new T(expected = 42); + t2 = new T(expected = 43); + y -> t1.z, t2.z; +} + +main reactor { + d = new D(); + reaction(startup) -> d.y {= + SET(d.y[0], 42); + =} + reaction(startup) -> d.y {= + SET(d.y[1], 43); + =} +}