diff --git a/.github/scripts/test-lfc.sh b/.github/scripts/test-lfc.sh index c5132e1cd0..2f82c01964 100755 --- a/.github/scripts/test-lfc.sh +++ b/.github/scripts/test-lfc.sh @@ -45,6 +45,7 @@ bin/lfc -f --rti rti test/C/src/Minimal.lf bin/lfc --federated --rti rti test/C/src/Minimal.lf # -h,--help Display this information. +bin/lfc -h bin/lfc --help # -l, --lint Enable linting during build. @@ -62,7 +63,7 @@ bin/lfc --output-path . test/C/src/Minimal.lf # --runtime-version Specify the version of the runtime # library used for compiling LF # programs. -bin/lfc --runtime-version 26e6e641916924eae2e83bbf40cbc9b933414310 test/Cpp/src/Minimal.lf +bin/lfc --runtime-version ca216ccc3da5ecff0e8013f75e275d7acac099de test/Cpp/src/Minimal.lf # -t,--threads Specify the default number of threads. bin/lfc -t 2 test/C/src/Minimal.lf @@ -72,5 +73,8 @@ bin/lfc -threads 2 test/C/src/Minimal.lf # (Added no-compile to avoid adding dependency.) bin/lfc --target-compiler gcc --no-compile test/C/src/Minimal.lf +# --version +bin/lfc --version + # Ensure that lfc is robust to symbolic links. test_with_links "lfc" diff --git a/.github/workflows/benchmark-tests.yml b/.github/workflows/benchmark-tests.yml index 1b053d3f0f..b4b183d960 100644 --- a/.github/workflows/benchmark-tests.yml +++ b/.github/workflows/benchmark-tests.yml @@ -33,14 +33,17 @@ jobs: run: | python3 benchmark/runner/run_benchmark.py -m test_mode=True iterations=1 benchmark="glob(*)" target=lf-c,lf-c-unthreaded if: ${{ inputs.target == 'C' }} - - name: Setup C++ build environment + - name: Compile reactor-cpp once and reuse for all benchmarks run: | - ./bin/lfc test/Cpp/src/Minimal.lf - echo "LD_LIBRARY_PATH=$GITHUB_WORKSPACE/test/Cpp/lib" >> $GITHUB_ENV + mkdir -p reactor-cpp/build + cd reactor-cpp/build + cmake -DCMAKE_INSTALL_PREFIX=../install ../../org.lflang/src/lib/cpp/reactor-cpp + make install + echo "LD_LIBRARY_PATH=$GITHUB_WORKSPACE/reactor-cpp/install/lib" >> $GITHUB_ENV if: ${{ inputs.target == 'Cpp' }} - name: Test C++ benchmarks run: | - python3 benchmark/runner/run_benchmark.py -m test_mode=True iterations=1 benchmark="glob(*)" target=lf-cpp iterations=1 target.params.extra_args="[--external-runtime-path, ${GITHUB_WORKSPACE}/test/Cpp]" + python3 benchmark/runner/run_benchmark.py -m test_mode=True iterations=1 benchmark="glob(*)" target=lf-cpp iterations=1 target.params.extra_args="[--external-runtime-path, ${GITHUB_WORKSPACE}/reactor-cpp/install]" if: ${{ inputs.target == 'Cpp' }} - name: Setup Rust uses: ATiltedTree/setup-rust@v1 diff --git a/.github/workflows/c-tests.yml b/.github/workflows/c-tests.yml index 071341fdb6..70a3a83990 100644 --- a/.github/workflows/c-tests.yml +++ b/.github/workflows/c-tests.yml @@ -13,6 +13,9 @@ on: required: false type: boolean default: false + scheduler: + required: false + type: string jobs: run: @@ -53,14 +56,19 @@ jobs: - name: Build RTI docker image uses: ./.github/actions/build-rti-docker if: ${{ runner.os == 'Linux' }} - - name: Perform tests for C target + - name: Perform tests for C target with default scheduler run: | ./gradlew test --tests org.lflang.tests.runtime.CTest.* --tests org.lflang.tests.lsp.LspTests.lspWithDependenciesTestC - if: ${{ !inputs.use-cpp }} - - name: Perform tests for CCpp target + if: ${{ !inputs.use-cpp && !inputs.scheduler }} + - name: Perform tests for C target with specified scheduler (no LSP tests) + run: | + echo "Specified scheduler: ${{ inputs.scheduler }}" + ./gradlew test --tests org.lflang.tests.runtime.CSchedulerTest.* -Dscheduler=${{ inputs.scheduler }} + if: ${{ !inputs.use-cpp && inputs.scheduler }} + - name: Perform tests for CCpp target with default scheduler run: | ./gradlew test --tests org.lflang.tests.runtime.CCppTest.* - if: ${{ inputs.use-cpp }} + if: ${{ inputs.use-cpp && !inputs.scheduler }} - name: Report to CodeCov uses: codecov/codecov-action@v2.1.0 with: diff --git a/.github/workflows/cpp-tests.yml b/.github/workflows/cpp-tests.yml index 9a523ace2d..1d7e3ade83 100644 --- a/.github/workflows/cpp-tests.yml +++ b/.github/workflows/cpp-tests.yml @@ -29,10 +29,13 @@ jobs: repository: lf-lang/lingua-franca submodules: true ref: ${{ inputs.compiler-ref }} - - name: Set cpp runtime version - run: | - echo ${{ inputs.runtime-ref }} > org.lflang/src/org/lflang/generator/cpp/cpp-runtime-version.txt - if: ${{ inputs.runtime-ref }} + - name: Check out specific ref of reactor-cpp + uses: actions/checkout@v2 + with: + repository: lf-lang/reactor-cpp + path: org.lflang/src/lib/cpp/reactor-cpp + ref: ${{ inputs.runtime-ref }} + if: ${{ inputs.runtime-ref }} - name: Run C++ tests; run: | ./gradlew test --tests org.lflang.tests.runtime.CppTest.* --tests org.lflang.tests.lsp.LspTests.lspWithDependenciesTestCpp @@ -42,4 +45,4 @@ jobs: file: org.lflang.tests/build/reports/xml/jacoco fail_ci_if_error: false verbose: true - if: ${{ !inputs.compiler-ref }} # i.e., if this is part of the main repo's CI + if: ${{ !inputs.runtime-ref }} # i.e., if this is part of the main repo's CI diff --git a/.github/workflows/lsp-tests.yml b/.github/workflows/lsp-tests.yml index a4ded076a0..5ac5d1b883 100644 --- a/.github/workflows/lsp-tests.yml +++ b/.github/workflows/lsp-tests.yml @@ -17,7 +17,7 @@ jobs: run: sudo apt-get remove clang-* if: ${{ runner.os == 'Linux' }} - name: Uninstall packages MacOS - run: brew uninstall gcc + run: brew uninstall --ignore-dependencies gcc if: ${{ runner.os == 'macOS' }} - name: Uninstall packages Windows run: | @@ -47,14 +47,6 @@ jobs: brew install protobuf brew install protobuf-c if: ${{ runner.os == 'macOS' }} - - name: Install dependencies Windows - uses: lukka/run-vcpkg@v4 - with: - vcpkgArguments: protobuf - vcpkgGitCommitId: 6185aa76504a5025f36754324abf307cc776f3da - vcpkgDirectory: ${{ github.workspace }}/vcpkg/ - vcpkgTriplet: x64-windows-static - if: ${{ runner.os == 'Windows' }} - name: Check out lingua-franca repository uses: actions/checkout@v2 with: diff --git a/.github/workflows/py-tests.yml b/.github/workflows/py-tests.yml index d70292bd8f..213061e589 100644 --- a/.github/workflows/py-tests.yml +++ b/.github/workflows/py-tests.yml @@ -34,14 +34,6 @@ jobs: brew install protobuf brew install coreutils if: ${{ runner.os == 'macOS' }} - - name: Install dependencies Windows - uses: lukka/run-vcpkg@v4 - with: - vcpkgArguments: protobuf - vcpkgGitCommitId: 6185aa76504a5025f36754324abf307cc776f3da - vcpkgDirectory: ${{ github.workspace }}/vcpkg/ - vcpkgTriplet: x64-windows-static - if: ${{ runner.os == 'Windows' }} - name: Install Google API Python Client run: pip3 install --upgrade google-api-python-client - name: Check out lingua-franca repository diff --git a/.gitmodules b/.gitmodules index 5b29f8d1ce..d0c0026657 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "org.lflang/src/lib/py/reactor-c-py"] path = org.lflang/src/lib/py/reactor-c-py url = https://github.com/lf-lang/reactor-c-py.git +[submodule "org.lflang/src/lib/cpp/reactor-cpp"] + path = org.lflang/src/lib/cpp/reactor-cpp + url = https://github.com/lf-lang/reactor-cpp diff --git a/benchmark/C/Savina/src/BenchmarkRunner.lf b/benchmark/C/Savina/src/BenchmarkRunner.lf index 18142b2358..9280e28d73 100644 --- a/benchmark/C/Savina/src/BenchmarkRunner.lf +++ b/benchmark/C/Savina/src/BenchmarkRunner.lf @@ -80,7 +80,7 @@ reactor BenchmarkRunner(num_iterations:int(12)) { self->measuredTimes[self->count] = duration; self->count += 1; - printf("Iteration: %d\t Duration: %.3f msec\n", self->count, toMS(duration)); + printf("Iteration %d - %.3f ms\n", self->count, toMS(duration)); schedule(nextIteration, 0); diff --git a/benchmark/C/Savina/src/concurrency/BoundedBuffer.lf b/benchmark/C/Savina/src/concurrency/BoundedBuffer.lf index b7272053a3..539822b3a6 100644 --- a/benchmark/C/Savina/src/concurrency/BoundedBuffer.lf +++ b/benchmark/C/Savina/src/concurrency/BoundedBuffer.lf @@ -24,6 +24,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/concurrency/CigaretteSmoker.lf b/benchmark/C/Savina/src/concurrency/CigaretteSmoker.lf index 3fa2223cff..17f034c65d 100644 --- a/benchmark/C/Savina/src/concurrency/CigaretteSmoker.lf +++ b/benchmark/C/Savina/src/concurrency/CigaretteSmoker.lf @@ -63,6 +63,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/concurrency/Dictionary.lf b/benchmark/C/Savina/src/concurrency/Dictionary.lf index 1e02b34e0c..c795b4164b 100644 --- a/benchmark/C/Savina/src/concurrency/Dictionary.lf +++ b/benchmark/C/Savina/src/concurrency/Dictionary.lf @@ -28,6 +28,7 @@ target CCpp { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/concurrency/LogisticMap.lf b/benchmark/C/Savina/src/concurrency/LogisticMap.lf index 2779d05d42..7577a60f58 100644 --- a/benchmark/C/Savina/src/concurrency/LogisticMap.lf +++ b/benchmark/C/Savina/src/concurrency/LogisticMap.lf @@ -12,6 +12,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/concurrency/Philosophers.lf b/benchmark/C/Savina/src/concurrency/Philosophers.lf index 2dd717669a..ac68f95ad4 100644 --- a/benchmark/C/Savina/src/concurrency/Philosophers.lf +++ b/benchmark/C/Savina/src/concurrency/Philosophers.lf @@ -11,6 +11,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ @@ -183,7 +184,7 @@ reactor Arbitrator(num_philosophers:int(20)) { if (finished[i]->is_present) { self->finished_philosophers++; if (self->num_philosophers == self->finished_philosophers) { - printf("Arbitrator: All philosophers are seated. Number of denials to philosophers: %d\n", self->retries); + printf("Arbitrator: All philosophers are sated. Number of denials to philosophers: %d\n", self->retries); SET(allFinished, true); } } diff --git a/benchmark/C/Savina/src/concurrency/SleepingBarber.lf b/benchmark/C/Savina/src/concurrency/SleepingBarber.lf index 0a0f0f65f0..412536ccb6 100644 --- a/benchmark/C/Savina/src/concurrency/SleepingBarber.lf +++ b/benchmark/C/Savina/src/concurrency/SleepingBarber.lf @@ -46,6 +46,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/concurrency/SortedList.lf b/benchmark/C/Savina/src/concurrency/SortedList.lf index 49801f630f..9519de8c05 100644 --- a/benchmark/C/Savina/src/concurrency/SortedList.lf +++ b/benchmark/C/Savina/src/concurrency/SortedList.lf @@ -18,6 +18,7 @@ target CCpp { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ @@ -122,30 +123,33 @@ reactor Worker( self->requests_sent++; } else { SET(finished, true); + self->requests_sent = 0; } =} } reactor LinkedList(num_workers: int(20)) { - state data_list: SortedLinkedList; + state data_list: {=SortedLinkedList*=}; state responses_to_send: int[]; logical action send_responses; - input print_result: bool; + input finished: bool; input[num_workers] requests: message_t; output[num_workers] responses: int; reaction(startup) {= - self->data_list = SortedLinkedList(); + self->data_list = new SortedLinkedList(); self->responses_to_send = (int*) malloc(self->num_workers * sizeof(int)); =} - reaction(print_result) {= + reaction(finished) {= // check result - info_print("List Size = %d", self->data_list.size()); - // no need to reset local state + info_print("List Size = %d", self->data_list->size()); + // reset local state + delete self->data_list; + self->data_list = new SortedLinkedList(); =} reaction(send_responses) -> responses {= @@ -162,19 +166,19 @@ reactor LinkedList(num_workers: int(20)) { int value = requests[i]->value.value; switch (requests[i]->value.type) { case CONTAINS: - self->responses_to_send[i] = self->data_list.contains(value); + self->responses_to_send[i] = self->data_list->contains(value); LOG_PRINT( "Worker %ld checks if %d is contained in the list", i, value ); break; case WRITE: - self->data_list.add(value); + self->data_list->add(value); self->responses_to_send[i] = value; LOG_PRINT("Worker %ld writes %d", i, value); break; case SIZE: - self->responses_to_send[i] = self->data_list.size(); + self->responses_to_send[i] = self->data_list->size(); LOG_PRINT("Worker %ld reads the list size", i); break; } @@ -375,7 +379,7 @@ main reactor(numIterations:int(12), numWorkers:int(20), numMessagesPerWorker:int (runner.start)+ -> manager.start, workers.doWork; manager.finished -> runner.finish; - manager.finished -> sorted_list.print_result; + manager.finished -> sorted_list.finished; workers.request -> sorted_list.requests; sorted_list.responses -> workers.response; diff --git a/benchmark/C/Savina/src/micro/Big.lf b/benchmark/C/Savina/src/micro/Big.lf index ce7b36cf5f..9e0b543239 100644 --- a/benchmark/C/Savina/src/micro/Big.lf +++ b/benchmark/C/Savina/src/micro/Big.lf @@ -29,6 +29,7 @@ target C{ /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/micro/Chameneos.lf b/benchmark/C/Savina/src/micro/Chameneos.lf index a2482e38d3..279f29080e 100644 --- a/benchmark/C/Savina/src/micro/Chameneos.lf +++ b/benchmark/C/Savina/src/micro/Chameneos.lf @@ -41,6 +41,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/micro/Counting.lf b/benchmark/C/Savina/src/micro/Counting.lf index e728b4b1ac..ebe4764ce0 100644 --- a/benchmark/C/Savina/src/micro/Counting.lf +++ b/benchmark/C/Savina/src/micro/Counting.lf @@ -42,6 +42,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/micro/PingPong.lf b/benchmark/C/Savina/src/micro/PingPong.lf index bf88f00572..917c1a38e8 100644 --- a/benchmark/C/Savina/src/micro/PingPong.lf +++ b/benchmark/C/Savina/src/micro/PingPong.lf @@ -26,6 +26,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/micro/ThreadRing.lf b/benchmark/C/Savina/src/micro/ThreadRing.lf index 7734dda9f0..2f0019ba33 100644 --- a/benchmark/C/Savina/src/micro/ThreadRing.lf +++ b/benchmark/C/Savina/src/micro/ThreadRing.lf @@ -55,6 +55,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/micro/Throughput.lf b/benchmark/C/Savina/src/micro/Throughput.lf index ca1ba8a072..38b417a1cf 100644 --- a/benchmark/C/Savina/src/micro/Throughput.lf +++ b/benchmark/C/Savina/src/micro/Throughput.lf @@ -56,12 +56,13 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ threads: 2, /// [[[end]]] - flags: "-lm", + flags: "-lm" }; import BenchmarkRunner from "../BenchmarkRunner.lf"; diff --git a/benchmark/C/Savina/src/parallelism/Apsp.lf b/benchmark/C/Savina/src/parallelism/Apsp.lf index be5157e68b..4b9f03394f 100644 --- a/benchmark/C/Savina/src/parallelism/Apsp.lf +++ b/benchmark/C/Savina/src/parallelism/Apsp.lf @@ -21,6 +21,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/parallelism/FilterBank.lf b/benchmark/C/Savina/src/parallelism/FilterBank.lf index 2a766a0a0f..5b1e09f0dd 100644 --- a/benchmark/C/Savina/src/parallelism/FilterBank.lf +++ b/benchmark/C/Savina/src/parallelism/FilterBank.lf @@ -33,17 +33,13 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ threads: 0, /// [[[end]]] - fast: true, - /* [[[cog - cog.outl(f'timeout: {time_steps} msec // Specifies number of samples.') - ]]] */ - //timeout: 34816 msec // Specifies number of samples. - /// [[[end]]] + fast: true }; import BenchmarkRunner from "../BenchmarkRunner.lf"; @@ -59,7 +55,7 @@ reactor Producer { state cnt: int(0); reaction (start) -> send_next {= schedule(send_next, 0); - =} + =} reaction (send_next) -> next, finish, send_next {= if (self->cnt >= 34816) { // reset state variable @@ -78,10 +74,15 @@ reactor Producer { * then start over again at 0. */ reactor Source(maxValue:int(1000)) { + input in_finished: bool; input next: bool; output value: double; state current: size_t(0); + + reaction (in_finished) {= + self->current = 0; + =} reaction (next) -> value {= SET(value, self->current); @@ -93,6 +94,8 @@ reactor Source(maxValue:int(1000)) { reactor Bank(bank_index: size_t(0), columns: size_t(16384), channels: size_t(8)) { input in: double; output out: double; + + input in_finished: bool; delay0 = new Delay(length=columns); fir0 = new FirFilter(bank_index=bank_index, peek_length=columns, first = true); @@ -105,7 +108,13 @@ reactor Bank(bank_index: size_t(0), columns: size_t(16384), channels: size_t(8)) fir0.out -> sample.in; sample.out -> delay1.in; delay1.out -> fir1.in; - fir1.out -> out; + fir1.out -> out; + + in_finished -> delay0.in_finished; + in_finished -> fir0.in_finished; + in_finished -> sample.in_finished; + in_finished -> delay1.in_finished; + in_finished -> fir1.in_finished; } /** @@ -119,11 +128,20 @@ reactor Delay(length: size_t(16383), period:time(1 msec)) { input in: double; output out: double; + + input in_finished: bool; reaction(startup) {= self->buffer = calloc(self->length - 1, sizeof(double)); self->pointer = 0; =} + + reaction (in_finished) {= + for (size_t i = 0; i < self->length - 1; i++) { + self->buffer[i] = 0; + } + self->pointer = 0; + =} reaction(in) -> out {= // info_print("Delay %d output: %f", self->pointer, self->buffer[self->pointer]); @@ -159,6 +177,8 @@ reactor FirFilter(bank_index:size_t(0), peek_length:size_t(16384), first:bool(tr input in: double; output out: double; + + input in_finished: bool; reaction(startup) {= // Allocate local state. @@ -181,6 +201,14 @@ reactor FirFilter(bank_index:size_t(0), peek_length:size_t(16384), first:bool(tr } self->data_index = 0; =} + + reaction(in_finished) {= + for (size_t i = 0; i < self->peek_length; i++) { + self->data[i] = 0; + } + self->data_index = 0; + self->data_full = false; + =} reaction(in) -> out {= self->data[self->data_index++] = in->value; @@ -215,6 +243,8 @@ reactor SampleFilter(sample_rate: size_t(16384)) { input in: double; output out: double; + + input in_finished: bool; reaction(in) -> out {= if(self->samples_received == 0) { @@ -226,6 +256,10 @@ reactor SampleFilter(sample_rate: size_t(16384)) { } self->samples_received = (self->samples_received + 1) % self->sample_rate; =} + + reaction(in_finished) {= + in_finished = 0; + =} } /** @@ -277,6 +311,7 @@ main reactor (numIterations:int(12), columns: size_t(16384), channels: size_t(8) =} runner.start -> producer.start; producer.finish -> runner.finish; + (producer.finish)+ -> banks.in_finished; producer.next -> source.next; (source.value)+ -> banks.in; banks.out -> combine.inValues; diff --git a/benchmark/C/Savina/src/parallelism/GuidedSearch.lf b/benchmark/C/Savina/src/parallelism/GuidedSearch.lf index b5e85f7503..a8a2425325 100644 --- a/benchmark/C/Savina/src/parallelism/GuidedSearch.lf +++ b/benchmark/C/Savina/src/parallelism/GuidedSearch.lf @@ -62,6 +62,7 @@ target CCpp { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/parallelism/MatMul.lf b/benchmark/C/Savina/src/parallelism/MatMul.lf index 5bd2b778d7..fb6190f8f4 100644 --- a/benchmark/C/Savina/src/parallelism/MatMul.lf +++ b/benchmark/C/Savina/src/parallelism/MatMul.lf @@ -10,6 +10,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/parallelism/NQueens.lf b/benchmark/C/Savina/src/parallelism/NQueens.lf index adb88da862..7da826d0c4 100644 --- a/benchmark/C/Savina/src/parallelism/NQueens.lf +++ b/benchmark/C/Savina/src/parallelism/NQueens.lf @@ -41,6 +41,7 @@ target C{ /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/parallelism/PiPrecision.lf b/benchmark/C/Savina/src/parallelism/PiPrecision.lf index bda324c172..c0f0d79bb5 100644 --- a/benchmark/C/Savina/src/parallelism/PiPrecision.lf +++ b/benchmark/C/Savina/src/parallelism/PiPrecision.lf @@ -17,6 +17,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ @@ -114,6 +115,7 @@ reactor Manager(numWorkers:int(20), scale:uint32_t({=5000ul=})) { free(solution_pruned); mpf_clear(self->result); mpf_clear(self->tolerance); + self->termsRequested = 0; SET(finished, true); =} diff --git a/benchmark/C/Savina/src/parallelism/RadixSort.lf b/benchmark/C/Savina/src/parallelism/RadixSort.lf index ca1bcef6e6..b5141a0d16 100644 --- a/benchmark/C/Savina/src/parallelism/RadixSort.lf +++ b/benchmark/C/Savina/src/parallelism/RadixSort.lf @@ -15,6 +15,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/C/Savina/src/parallelism/Trapezoidal.lf b/benchmark/C/Savina/src/parallelism/Trapezoidal.lf index b0b99f8810..5c0455d697 100644 --- a/benchmark/C/Savina/src/parallelism/Trapezoidal.lf +++ b/benchmark/C/Savina/src/parallelism/Trapezoidal.lf @@ -17,6 +17,7 @@ target C { /* [[[cog if (threaded_runtime=="True"): cog.outl(f"threads: {threads},") + cog.outl(f"scheduler: {scheduler},") else: cog.outl("threads: 0,") ]]] */ diff --git a/benchmark/Rust/Savina/src/parallelism/MatMul.lf b/benchmark/Rust/Savina/src/parallelism/MatMul.lf index a6b4e7bdf3..ddc87421fb 100644 --- a/benchmark/Rust/Savina/src/parallelism/MatMul.lf +++ b/benchmark/Rust/Savina/src/parallelism/MatMul.lf @@ -60,21 +60,21 @@ reactor Manager(numWorkers: usize(20), dataLength: usize(1024)) { input[numWorkers] moreWork: {=[WorkItem; 8]=}; reaction(startup) {= - // Fill both input arrays with data + // Fill both input arrays with data let (a, b) = { let mut a = Matrix::::new(self.data_length, self.data_length); let mut b = TransposedMatrix::::new(self.data_length, self.data_length); - + for i in 0..self.data_length { for j in 0..self.data_length { a.set(i, j, i as f64); b.set(i, j, j as f64); } } - + (Arc::new(a), Arc::new(b)) }; - + self.A = a; self.B = b; =} @@ -143,7 +143,7 @@ reactor Manager(numWorkers: usize(20), dataLength: usize(1024)) { pub numBlocks: usize, // total number of elements per block in both dimensions pub dim: usize, // number of elements in one dimension in one block } - + pub fn is_valid(matrix: &Matrix, data_length: usize) -> bool { for i in 0..data_length { for j in 0..data_length { @@ -170,7 +170,7 @@ reactor Worker(threshold: usize(16384)) { input data: {=(Arc>, Arc>, Arc>>)=}; input doWork: WorkItem; output moreWork: {=[WorkItem; 8]=}; - + preamble {= use crate::reactors::manager::WorkItem; use crate::matrix::{Matrix, TransposedMatrix}; @@ -181,7 +181,7 @@ reactor Worker(threshold: usize(16384)) { ctx.use_ref_opt(data, |(a, b, c)| { self.A = a.clone(); self.B = b.clone(); - self.C = c.clone(); + self.C = c.clone(); }); =} @@ -210,13 +210,14 @@ reactor Worker(threshold: usize(16384)) { // otherwise we compute the result directly let end_r = wi.srC + wi.dim; let end_c = wi.scC + wi.dim; - + let mut c = self.C.lock().unwrap(); for i in wi.srC..end_r { for j in wi.scC..end_c { for k in 0..wi.dim { - let v = self.A.get(i, wi.scA + k) * self.B.get(wi.srB + k, j); + let mut v = self.A.get(i, wi.scA + k) * self.B.get(wi.srB + k, j); + v += c.get(i, j); c.set(i, j, v); } } diff --git a/benchmark/runner/conf/default.yaml b/benchmark/runner/conf/default.yaml index e05cc70293..b109df8058 100644 --- a/benchmark/runner/conf/default.yaml +++ b/benchmark/runner/conf/default.yaml @@ -1,5 +1,6 @@ iterations: 12 threads: null +timeout: 1200 savina_path: "${oc.env:SAVINA_PATH}" lf_path: "${oc.env:LF_PATH}" continue_on_error: False diff --git a/benchmark/runner/conf/target/lf-c-unthreaded.yaml b/benchmark/runner/conf/target/lf-c-unthreaded.yaml index ffaa630aeb..e142d459a5 100644 --- a/benchmark/runner/conf/target/lf-c-unthreaded.yaml +++ b/benchmark/runner/conf/target/lf-c-unthreaded.yaml @@ -8,6 +8,6 @@ gen: ["cog", "-r", "${args:benchmark.targets.lf-c.gen_args}", "-D", "threaded_runtime=False", "src/${benchmark.targets.lf-c.lf_file}"] compile: ["${lf_path}/bin/lfc", "src/${benchmark.targets.lf-c.lf_file}"] -run: ["bash", "-c", "seq ${iterations} | xargs -I{} bin/${benchmark.targets.lf-c.binary}"] +run: ["bin/${benchmark.targets.lf-c.binary}"] parser: _target_: "parser.parse_lfc_output" diff --git a/benchmark/runner/conf/target/lf-c.yaml b/benchmark/runner/conf/target/lf-c.yaml index 9579c6f81a..5422ede6cd 100644 --- a/benchmark/runner/conf/target/lf-c.yaml +++ b/benchmark/runner/conf/target/lf-c.yaml @@ -3,6 +3,7 @@ prepare: ["mkdir", "src"] copy: ["cp", "-r", "${benchmark.targets.lf-c.copy_sources}", "src"] gen: ["cog", "-r", "${args:benchmark.targets.lf-c.gen_args}", "-D", "threads=${threads}", + "-D", "scheduler=${target.params.scheduler}", "-D", "numIterations=${iterations}", "-D", "threaded_runtime=True", "src/${benchmark.targets.lf-c.lf_file}"] @@ -10,3 +11,5 @@ compile: ["${lf_path}/bin/lfc", "src/${benchmark.targets.lf-c.lf_file}"] run: ["bin/${benchmark.targets.lf-c.binary}"] parser: _target_: "parser.parse_lfc_output" +params: + scheduler: "GEDF_NP" diff --git a/benchmark/runner/parser.py b/benchmark/runner/parser.py index 01a44d6d41..e903c2ca61 100644 --- a/benchmark/runner/parser.py +++ b/benchmark/runner/parser.py @@ -35,16 +35,8 @@ def parse_lfcpp_output(lines): return times -def parse_lfc_output(lines): - times = [] - for line in lines: - prefix = "---- Elapsed physical time (in nsec): " - if line.startswith(prefix): - p = len(prefix) - ns = int(line[p:].replace(",", "")) - times.append(ns / 1000000.0) +parse_lfc_output = parse_lfcpp_output - return times def parse_lf_rust_output(lines): times = [] diff --git a/example/Cpp/src/AlarmClock/AlarmClock.cmake b/example/Cpp/src/AlarmClock/AlarmClock.cmake new file mode 100644 index 0000000000..d764890d3c --- /dev/null +++ b/example/Cpp/src/AlarmClock/AlarmClock.cmake @@ -0,0 +1,9 @@ +find_package (Threads) +find_package (Crow) + +set(CROW_HEADERS ../../Crow/include/) +set(CPP_SOURCES ${CMAKE_CURRENT_LIST_DIR}) + +target_link_libraries(${LF_MAIN_TARGET} ${CMAKE_THREAD_LIBS_INIT} Crow::Crow) +target_include_directories(${LF_MAIN_TARGET} PUBLIC ${CPP_SOURCES}) + diff --git a/example/Cpp/src/AlarmClock/AlarmClock.lf b/example/Cpp/src/AlarmClock/AlarmClock.lf new file mode 100644 index 0000000000..338b219fdb --- /dev/null +++ b/example/Cpp/src/AlarmClock/AlarmClock.lf @@ -0,0 +1,40 @@ +/* +* This is a minimal example of an alarmclock implemeted using the +* features lingua franca supplies. +* +* This is just an extract and simplification from the main project +* which you can find here: https://github.com/revol-xut/lf-alarm-clock +* +* This file contains the networking implementation it is really just an +* simple socket application which parses simple http headers and respondes +* in text/plain +* +* @author Tassilo Tanneberer +*/ + +target Cpp{ + cmake-include: "AlarmClock.cmake", + keepalive: true +}; + +import Network from "./Network.lf"; +import Clock from "./Clock.lf"; + +#import Network.lf; +#import Clock.lf; + +main reactor AlarmClock { + clock = new Clock(); + network = new Network(); + + // additon of a new event + network.event -> clock.event; + network.delete_index -> clock.cancel_by_index; + clock.event_dump -> network.updated_events; + + reaction (startup) {= + std::cout << "Starting Lingua Franca AlarmClock" << std::endl; + =} +} + + diff --git a/example/Cpp/src/AlarmClock/Clock.lf b/example/Cpp/src/AlarmClock/Clock.lf new file mode 100644 index 0000000000..30e3c64199 --- /dev/null +++ b/example/Cpp/src/AlarmClock/Clock.lf @@ -0,0 +1,206 @@ +/* +* This is a minimal example of an alarmclock implemeted using the +* features lingua franca supplies. +* +* This is just an extract and simplification from the main project +* which you can find here: https://github.com/revol-xut/lf-alarm-clock +* +* Author: Tassilo Tanneberer +*/ + + +target Cpp{ + cmake-include: "AlarmClock.cmake", + keepalive: true +}; + +public preamble {= + #include "shared_header.hpp" +=} + +reactor Trigger { + private preamble {= + auto convert_to_relative = [](long time_stamp){ + const auto t = std::chrono::system_clock::now(); + std::chrono::seconds desired_time = std::chrono::seconds(time_stamp); + std::chrono::seconds current_time = + std::chrono::duration_cast(t.time_since_epoch()); + std::chrono::seconds delta_t = desired_time - current_time; + return delta_t; + }; + =} + + input input_event: {=Event=}; + input input_interrupt: long; + logical action interrupt; + logical action triggered_event: {=std::string=}; + state ignore_flag: bool; + + //the input_event will scheduled + reaction (input_event) -> triggered_event {= + if(input_event.is_present()) { + auto extracted = input_event.get().get(); + auto delta_t = convert_to_relative(extracted->time_stamp_); + triggered_event.schedule(extracted->message_, delta_t); + } + =} + + reaction (input_interrupt) -> interrupt {= + if(input_interrupt.is_present()){ + auto delta_t = convert_to_relative(*(input_interrupt.get().get())); + interrupt.schedule(delta_t); + } + =} + + // reaction which will be triggered when a event is due + reaction(triggered_event) {= + auto select_random_file = []{ + std::vector files; + for (const auto& file : std::filesystem::directory_iterator(kMusicDir)) { + files.push_back(file.path().filename()); + } + + return files[rand() % files.size()]; + }; + + // takes a random audio file and playes it with mpg321 + if(triggered_event.is_present() and not ignore_flag){ + std::cout << "Triggering Event: " << *(triggered_event.get().get()) << std::endl; + std::string command = std::string(kPlayerCommand) + " " + std::string(kMusicDir) + select_random_file(); + if( system(command.c_str()) != 0 ){ + std::runtime_error("mpc finished with non zero return value"); + } + } + ignore_flag = false; + =} + + reaction (interrupt) {= + ignore_flag = true; + =} +} + +reactor Clock { + // function which is used to check if a given event has already past + private preamble {= + auto time_over(const Event& event) noexcept -> bool { + const auto p1 = std::chrono::system_clock::now(); + auto current_time = std::chrono::duration_cast(p1.time_since_epoch()).count(); + return current_time > event.time_stamp_; + } + =} + + // trigger reactor which handles the execution of the scheduled reaction + trigger = new Trigger(); + // this event will be scheduled and added to persistent storage + input event: Event; + input cancel_by_index: std::size_t; + // list of events + output event_dump: {= std::vector =}; + // timer which triggers clear and save + timer maintance(10 sec, 30 sec); + + // persistant storage + state events: std::vector(); + + // reaction that appends new events which will be scheduled + // the newtwork reactor is updated + reaction (event) -> trigger.input_event, event_dump {= + if (event.is_present() and not time_over(*event.get())){ + trigger.input_event.set(*event.get()); + events.push_back(*event.get()); + event_dump.set(events); + } + =} + + // initiation ... reading file to create state + reaction (startup) -> trigger.input_event, event_dump {= + // if the calender file doesn't exists it's created + if (not std::filesystem::exists(kFile)){ + std::ofstream{kFile}; + } + + std::ifstream file; + file.open(kFile); + + std::string line; + if(not file.is_open()) { + throw std::runtime_error("Cannot open event file!"); + } + + // iterating through the file every line corresponds to one events (csv) + while (file) { + std::getline(file, line); + if (line.empty()) { + break; + } + + Event serialized_event {}; + + // an event has the shape message;time_stamp\newline + std::size_t colon_pos = line.find(";"); + serialized_event.message_ = line.substr(0, colon_pos); + serialized_event.time_stamp_ = static_cast( + std::stoi(line.substr(colon_pos + 1, line.size() - 2)) + ); + + // if the given event is not already in the past it gets schedules by lingua franca + if(not time_over(serialized_event)){ + trigger.input_event.set(serialized_event); + events.push_back(serialized_event); + } + } + + file.close(); + event_dump.set(events); + =} + + // state needs to be saved to file + reaction (shutdown, maintance) -> event_dump {= + remove_events(); + save(); + event_dump.set(events); + =} + + reaction (cancel_by_index) -> trigger.input_interrupt, event_dump {= + if(cancel_by_index.is_present()) { + std::size_t index = *(cancel_by_index.get().get()); + + if( index < events.size()){ + auto tag = events.at(index).time_stamp_; + trigger.input_interrupt.set(tag); + events.erase(events.begin() + index); + event_dump.set(events); + } + + } + =} + + method remove_events() {= + // list of element which can be removed in the next iteration + std::vector removed_indices; + std::size_t index = 0; + for(const Event& event: events) { + if (time_over(event)){ + removed_indices.push_back(index); + } + index++; + } + + std::size_t removed_counter = 0; + for (std::size_t i : removed_indices) { + events.erase(events.begin() + i - removed_counter); + removed_counter++; + } + =} + + method save() {= + std::ofstream file(kFile, std::ios::trunc); + + for (const Event& e : events ) { + file << e.message_ + ";" + std::to_string(e.time_stamp_) + "\n"; + } + + file.close(); + =} +} + diff --git a/example/Cpp/src/AlarmClock/Network.lf b/example/Cpp/src/AlarmClock/Network.lf new file mode 100644 index 0000000000..630922078d --- /dev/null +++ b/example/Cpp/src/AlarmClock/Network.lf @@ -0,0 +1,225 @@ +/* +* This is a minimal example of an alarmclock implemeted using the +* features lingua franca supplies. +* +* This is just an extract and simplification from the main project +* which you can find here: https://github.com/revol-xut/lf-alarm-clock + +* This file contains the networking implementation it is really just an +* simple socket application which parses simple http headers and respondes +* in text/plain +* +* @author Tassilo Tanneberer +*/ + +target Cpp{ + cmake-include: "AlarmClock.cmake", + keepalive: true +}; + +public preamble {= + #include "shared_header.hpp" +=} + +reactor Network { + private preamble {= + #include + #include + #include + #include + #include + =} + + // physical event which is triggered by receiving a request + physical action new_event: Event; + physical action delete_request: std::size_t; + + // variables for the receive thread + state thread: std::thread; // receive thread + state events: std::vector; // copy + + input updated_events: std::vector; + output event: Event; // event which will be added to the clock + output delete_index: std::size_t; + + // this reaction transforms a physical action into a logical reaction + reaction (new_event) -> event {= + if(new_event.is_present()){ + event.set(new_event.get()); + } + =} + + reaction (delete_request) -> delete_index {= + if(delete_request.is_present()){ + delete_index.set(delete_request.get()); + } + =} + + // main starts receive thread + reaction (startup) -> delete_request, new_event{= + thread = std::thread([&] { + crow::SimpleApp app; + + // returns json of all the upcoming events + CROW_ROUTE(app, "/list") ([&]{ + // function converts unix timestamp to human readable datetime string + auto unix_to_human_readable = [](unsigned int time_stamp){ + using Clock = std::chrono::high_resolution_clock; + using TimePoint = std::chrono::time_point; + const Clock::duration duration_time_stamp = std::chrono::seconds(time_stamp); + const TimePoint chrono_time_point(duration_time_stamp); + std::time_t end_time = std::chrono::system_clock::to_time_t(chrono_time_point); + std::string return_string(std::ctime(&end_time)); + return return_string.substr(0, return_string.size() - 2); + }; + + crow::json::wvalue response; + for (const Event& event : events ){ + crow::json::wvalue json_event; + json_event["date"] = std::move(unix_to_human_readable(event.time_stamp_)); + json_event["message"] = event.message_; + + response[std::to_string(event.time_stamp_)] = std::move(json_event); + }; + return crow::response(response); + }); + + // adds new event by unix time stamp + CROW_ROUTE(app, "/add_event_timestamp").methods("POST"_method) + ([&new_event](const crow::request& req){ + auto json_body = crow::json::load(req.body); + if (!json_body) { + return crow::response(400); + } + + // maybe add extra input validation + Event serialized_event { + json_body["message"].s(), + static_cast(json_body["time_stamp"].u()) + }; + + // triggers physical action + new_event.schedule(serialized_event, 0ms); + crow::json::wvalue response; + response["success"] = true; + return crow::response(response); + }); + + // adds new event by relativ times + CROW_ROUTE(app, "/add_event_relative").methods("POST"_method) + ([&new_event](const crow::request& req){ + auto relativ_time = 0l; + auto json_body = crow::json::load(req.body); + if (!json_body) { + return crow::response(400); + } + + // calculates relative time in seconds + if(json_body.has("day")){ + relativ_time += 24 * 60 * 60 * json_body["day"].i(); + } + if(json_body.has("hour")){ + relativ_time += 60 * 60 * json_body["hour"].i(); + } + if(json_body.has("minute")){ + relativ_time += 60 * json_body["minute"].i(); + } + if(json_body.has("second")){ + relativ_time += json_body["second"].i(); + } + + const auto now = std::chrono::system_clock::now(); + auto current_time = std::chrono::duration_cast(now.time_since_epoch()).count(); + + std::cout << "current_time: " << current_time << " offset:" << relativ_time << std::endl; + Event serialized_event { + json_body["message"].s(), + current_time + relativ_time + }; + + // triggers physical action + new_event.schedule(serialized_event, 0ms); + crow::json::wvalue response; + response["success"] = true; + return crow::response(response); + }); + + // will set the timer in the text 24 hours + CROW_ROUTE(app, "/add_event_time").methods("POST"_method) + ([&new_event](const crow::request& req){ + // just % doesn't work because it is the remainder operator + // and does not behave like modulo for negative numbers + auto mod = [](int a, int b) { + int r = a % b; + return r < 0 ? r + b : r; + }; + + auto relativ_time = 0l; + auto json_body = crow::json::load(req.body); + if (!json_body) { + return crow::response(400); + } + + // use std::chrono::hh_mm_ss when C++20 is available + time_t time_struct = time(NULL); + struct tm *formatted_time = localtime(&time_struct); + + // calculating time differences and turning them into seconds for the time_stamp + if(json_body.has("hour")){ + relativ_time += 3600 * mod(json_body["hour"].i() - formatted_time->tm_hour - 1, 24); + } + if(json_body.has("minute")){ + relativ_time += 60 * mod(json_body["minute"].i() - formatted_time->tm_min - 1, 60); + } + if(json_body.has("second")){ + relativ_time += mod(json_body["second"].u() - formatted_time->tm_sec - 1, 60); + } + + Event serialized_event { + json_body["message"].s(), + relativ_time + time_struct + }; + + // triggers physical action + new_event.schedule(serialized_event, 0ms); + crow::json::wvalue response; + response["success"] = true; + return crow::response(response); + }); + + // request stopping playing music + // just used pidof to kill the process + CROW_ROUTE(app, "/stop") ([]{ + int status = system((std::string(kKillCommand) + " $(" + std::string(kPidofCommand) + " mpg321)").c_str()); + crow::json::wvalue response; + response["success"] = status; + return crow::response(response); + }); + + CROW_ROUTE(app, "/remove").methods("POST"_method) + ([&delete_request](const crow::request& req){ + auto json_body = crow::json::load(req.body); + if (!json_body) { + return crow::response(400); + } + + std::size_t index = json_body["index"].u(); + delete_request.schedule(index, 0s); + + crow::json::wvalue response; + response["success"] = true; + return crow::response(response); + }); + + // start the http server + app.port(kPort).multithreaded().run(); + }); + =} + reaction (updated_events) {= + events = std::move(*updated_events.get()); + =} + + reaction ( shutdown ) {= + thread.join(); + =} +} diff --git a/example/Cpp/src/AlarmClock/README.md b/example/Cpp/src/AlarmClock/README.md new file mode 100644 index 0000000000..914f796dfd --- /dev/null +++ b/example/Cpp/src/AlarmClock/README.md @@ -0,0 +1,171 @@ +Lingua Franca Alarm Clock +---------------------------- + +**Contact:** + +**Main Repository:** [](https://github.com/revol-xut/lf-alarm-clock) + +A small and tiny alarmclock which is written using the scheduling and time features from lingua franca. + +## What you will learn + +- sharing state between reactors +- stopping scheduled events + +## Project + +![Programm Structure](./images/entire_program.png) + + +## Building + +**Dependencies:** jdk11, boost, mpg321, Crow + + +```bash + $ lfc ./AlarmClock.lf +``` + +**Building with nix** + +This cross compiles for aarch64. +``` + nix build .#packages.aarch64-linux.lf-alarm-clock +``` + +## Installation + +By default the AlarmClock expects the sound files to be placed in `~/music/AlarmClock/` you can change this +path by editing the `shared_header.cpp` file. Furthermore is it possible to configure paths to other binaries +in this file e.g. kill, mpg321 -commands. + +### Installing Crow from source + +On most distros, Crow needs to be build and installed from source: + +```bash + $ git clone git@github.com:CrowCpp/Crow.git + $ mkdir Crow/build + $ cd Crow/build + $ cmake -DCMAKE_INSTALL_PREFIX= + $ make install +``` +Note that you can adjust the preferred install location by replacing ``. + +To build the alarm clock using this manually installed version of Crow, simply run: +```bash + $ CMAKE_PREFIX_PATH= lfc ./AlarmClock.lf +``` + +## Endpoints & Usage + +### /list **GET** +Returns a list of upcoming events. + +```json + "timestamp": { + "date": "" + "message": "" + } +``` + +**Examples** + +``` +$ curl http://0.0.0.0:8680/list +``` + +### /stop **GET** +Stops the currently playing alarm sound. + +```json +{ + "success": "exit code" +} +``` + +**Examples** + +``` +$ curl http://0.0.0.0:8680/stop +``` + +### /add_event_timestamp **POST** +Will schedule your alarmclock for the given timestamp + +Request: +```json +{ + "message": "", + "time_stamp": 0 +} +``` +Response: +```json +{ + "success": true +} +``` + +**Examples** + +``` +$ curl http://0.0.0.0:8680/add_event_timestamp -X POST -H "Content-Type: text/json" -d '{"message": "test", "time_stamp": 1643400000}' +``` + +Schedules event for given timestamp. + +### /add_event_relative **POST** +Will schedule a event relative to the current time. + +Request +```json +{ + "days": 0, + "hours": 0, + "minutes": 0, + "seconds": 0 +} +``` + +Response: +``` +{ + "success": true +} +``` + +**Example** + +``` +$ curl http://0.0.0.0:8680/add_event_relative -X POST -H "Content-Type: text/json" -d '{"hour": 6, "minute":0, "second": 0, "message": "hello"}' +``` + +Schedules sets your alarmclock to activate in 6 hours. + +### /add_event_time **POST** +Schedule event for this time in the next 24 hours. If a parameter +is unspecified the current time is used. + +Request +```json +{ + "hour": 0, + "minute": 0, + "second": 0 +} +``` + +Response: +```json +{ + "success": true +} +``` +**Example** + +``` + $ curl http://0.0.0.0:8680/add_event_time -X POST -H "Content-Type: text/json" -d '{"message": "test", "hour": 6, "minute":0, "second": 0, "message": "hello"}' +``` + +Schedules the event for the next time the given time occures. diff --git a/example/Cpp/src/AlarmClock/derivation.nix b/example/Cpp/src/AlarmClock/derivation.nix new file mode 100644 index 0000000000..bd65751d5d --- /dev/null +++ b/example/Cpp/src/AlarmClock/derivation.nix @@ -0,0 +1,110 @@ +{stdenv, pkgs, lib, fetchFromGitHub,...}: +let + +lfc = stdenv.mkDerivation { + pname = "lfc"; + version = "0.1.0"; + + src = fetchFromGitHub { + owner = "revol-xut"; + repo = "lingua-franca-nix-releases"; + rev = "11c6d5297cd63bf0b365a68c5ca31ec80083bd05"; + sha256 = "DgxunzC8Ep0WdwChDHWgG5QJbJZ8UgQRXtP1HZqL9Jg="; + }; + + buildInputs = with pkgs; [ jdk11_headless ]; + + _JAVA_HOME = "${pkgs.jdk11_headless}/"; + + postPatch = '' + substituteInPlace bin/lfc \ + --replace 'base=`dirname $(dirname ''${abs_path})`' "base='$out'" \ + --replace "run_lfc_with_args" "${pkgs.jdk11_headless}/bin/java -jar $out/lib/jars/org.lflang.lfc-0.1.0-SNAPSHOT-all.jar" + ''; + + installPhase = '' + cp -r ./ $out/ + chmod +x $out/bin/lfc + ''; + + meta = with lib; { + description = "Polyglot coordination language"; + longDescription = '' + Lingua Franca (LF) is a polyglot coordination language for concurrent + and possibly time-sensitive applications ranging from low-level + embedded code to distributed cloud and edge applications. + ''; + homepage = "https://github.com/lf-lang/lingua-franca"; + license = licenses.bsd2; + platforms = platforms.linux; + maintainers = with maintainers; [ revol-xut ]; + }; +}; + +# downloading the cpp runtime +cpp-runtime = stdenv.mkDerivation { + name = "cpp-lingua-franca-runtime"; + + src = fetchFromGitHub { + owner = "lf-lang"; + repo = "reactor-cpp"; + rev = "007143225dbc198a5fee233ce125c3584a9541d8"; + sha256 = "sha256-wiBTJ4jSzoAu/Tg2cMqMWv7qZD29F+ysDOOF6F/DLJM="; + }; + + nativeBuildInputs = with pkgs; [ cmake gcc ]; + configurePhase = '' + echo "Configuration" + ''; + + buildPhase = '' + mkdir -p build + cd build + cmake .. -DCMAKE_INSTALL_PREFIX=./ + make install + ''; + + installPhase = '' + cp -r ./ $out/ + ''; + + fixupPhase = '' + echo "FIXUP PHASE SKIP" + ''; +}; + + + +in + stdenv.mkDerivation { + name = "alarm-clock"; + version = "0.0.1"; + + src = fetchFromGitHub { + owner = "revol-xut"; + repo = "lf-alarm-clock"; + rev = "8113a2c84db3d960d56455a91b288e8e7c584964"; + sha256 = "sha256-WHGSlqD5CUl4JhILZeiwB0/zXP+h6rsOuEvKzn5SqFA="; + fetchSubmodules = true; + }; + + buildInputs = with pkgs; [ lfc which gcc cmake git boost ]; + + configurePhase = '' + echo "Test"; + ''; + + buildPhase = '' + echo "Starting compiling" + mkdir -p include/reactor-cpp/ + ls -a ${cpp-runtime} + cp -r ${cpp-runtime}/include/reactor-cpp/* include/reactor-cpp/ + ${lfc}/bin/lfc --external-runtime-path ${cpp-runtime}/ src/AlarmClock.lf + ''; + + installPhase = '' + mkdir -p $out/bin + cp -r ./bin/* $out/bin + ''; + } + diff --git a/example/Cpp/src/AlarmClock/flake.nix b/example/Cpp/src/AlarmClock/flake.nix new file mode 100644 index 0000000000..a05f5f6cce --- /dev/null +++ b/example/Cpp/src/AlarmClock/flake.nix @@ -0,0 +1,15 @@ +{ + description = "build script for the lingua-franca alarm clock"; + + inputs = { + utils.url = "github:numtide/flake-utils"; + }; + + outputs = inputs@{self, utils, nixpkgs, ...}: + utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + in rec { + packages.lf-alarm-clock = nixpkgs.legacyPackages.${system}.callPackage ./derivation.nix {}; + } + ); +} diff --git a/example/Cpp/src/AlarmClock/images/clock.png b/example/Cpp/src/AlarmClock/images/clock.png new file mode 100755 index 0000000000..583f4dbd1c Binary files /dev/null and b/example/Cpp/src/AlarmClock/images/clock.png differ diff --git a/example/Cpp/src/AlarmClock/images/entire_program.png b/example/Cpp/src/AlarmClock/images/entire_program.png new file mode 100755 index 0000000000..8249326de1 Binary files /dev/null and b/example/Cpp/src/AlarmClock/images/entire_program.png differ diff --git a/example/Cpp/src/AlarmClock/images/trigger.png b/example/Cpp/src/AlarmClock/images/trigger.png new file mode 100755 index 0000000000..d35529335a Binary files /dev/null and b/example/Cpp/src/AlarmClock/images/trigger.png differ diff --git a/example/Cpp/src/AlarmClock/shared_header.hpp b/example/Cpp/src/AlarmClock/shared_header.hpp new file mode 100644 index 0000000000..1221ea486a --- /dev/null +++ b/example/Cpp/src/AlarmClock/shared_header.hpp @@ -0,0 +1,34 @@ +/* +* This is a minimal example of an alarmclock implemeted using the +* features lingua franca supplies. +* +* This is just an extract and simplification from the main project +* which you can find here: https://github.com/revol-xut/lf-alarm-clock +* +* Author: Tassilo Tanneberer +*/ + +#ifndef SHARED_HEADER_INCLUDE_GUARD +#define SHARED_HEADER_INCLUDE_GUARD + +#include +#include +#include +#include +#include + +struct Event { + std::string message_; + long time_stamp_; +}; + +constexpr const char* kMusicDir = "./sounds/"; +constexpr const char* kFile = "./alarm_clock_events.csv"; +constexpr const char* kPlayerCommand = "mpg321"; +constexpr const char* kKillCommand = "kill"; +constexpr const char* kPidofCommand = "pidof"; + +constexpr unsigned short kPort = 8680; + +#endif //SHARED_HEADER_INCLUDE_GUARD + diff --git a/experimental/C/src/AnytimePrime.lf b/experimental/C/src/AnytimePrime.lf new file mode 100644 index 0000000000..2fe50bef10 --- /dev/null +++ b/experimental/C/src/AnytimePrime.lf @@ -0,0 +1,68 @@ +/** + * This program is a concept demonstration showing how the check_deadline() + * function works. This program performs "anytime computation" (https://en.wikipedia.org/wiki/Anytime_algorithm) + * to find the largest prime within the given deadline. The check_deadline() + * function is called after checking whether each number is prime. If + * check_deadline() is called after the deadline has passed, the function calls + * the deadline handler and returns true. Then the reaction outputs the largest + * prime found by that time and exits. + * + * For more discussion on check_deadline(), see: https://github.com/lf-lang/lingua-franca/issues/403 + * + * @author Hokeun Kim (hokeunkim@berkeley.edu) + * @author Edward A. Lee (eal@berkeley.edu) + * @author Marten Lohstroh (marten@berkeley.edu) + */ +target C { + fast: true, + files: ["/lib/c/reactor-c/core/utils/vector.h", + "/lib/c/reactor-c/core/utils/vector.c"], + cmake-include: ["/lib/c/reactor-c/core/utils/vector.cmake"] +}; + +preamble {= +#include "vector.h" +=} + +reactor Prime { + output out: {=long long=}; + reaction(startup) -> out {= + int num_primes = 1; + long long current_num = 2; + vector_t primes = vector_new(10000); + vector_push(&primes, (void*)2); + + while (!check_deadline(self, true)) { + current_num++; + int i = 0; + for (i = 0; i < num_primes; i++) { + if (current_num % (long long)primes.start[i] == 0) { + break; + } + } + if (i == num_primes) { + // Add the prime to vector. + vector_push(&primes, (void*)current_num); + num_primes++; + } + } + + // Output the largest prime found. + SET(out, (long long)primes.start[num_primes - 1]); + =} deadline (3 sec) {= + info_print("Deadline handler called!"); + =} +} + +reactor Print { + input in:{=long long=}; + reaction(in) {= + printf("Largest prime found within the deadline: %d\n", in->value); + =} +} + +main reactor AnytimePrime { + p = new Prime(); + d = new Print(); + p.out -> d.in; +} diff --git a/gradle.properties b/gradle.properties index 4754184f83..2bc765d3c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ lsp4jVersion=0.12.0 mwe2LaunchVersion=2.12.1 openTest4jVersion=1.2.0 resourcesVersion=3.16.0 -runtimeVersion=3.24.0 +runtimeVersion=3.23.0 shadowJarVersion=7.1.2 xtextGradleVersion=3.0.0 xtextVersion=2.25.0 diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/AbstractSynthesisExtensions.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/AbstractSynthesisExtensions.java similarity index 75% rename from org.lflang.diagram/src/org/lflang/diagram/synthesis/AbstractSynthesisExtensions.xtend rename to org.lflang.diagram/src/org/lflang/diagram/synthesis/AbstractSynthesisExtensions.java index ba2f27a989..dad2b37469 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/AbstractSynthesisExtensions.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/AbstractSynthesisExtensions.java @@ -22,27 +22,28 @@ * (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.diagram.synthesis +package org.lflang.diagram.synthesis; -import de.cau.cs.kieler.klighd.SynthesisOption -import de.cau.cs.kieler.klighd.syntheses.AbstractDiagramSynthesis -import javax.inject.Inject -import org.eclipse.emf.ecore.EObject +import de.cau.cs.kieler.klighd.SynthesisOption; +import de.cau.cs.kieler.klighd.syntheses.AbstractDiagramSynthesis; +import javax.inject.Inject; +import org.eclipse.emf.ecore.EObject; /** * Abstract super class for extension classes used in for the diagram synthesis that provides some convince methods. * * @author{Alexander Schulz-Rosengarten } */ -abstract class AbstractSynthesisExtensions { +public abstract class AbstractSynthesisExtensions { - @Inject AbstractDiagramSynthesis delegate + @Inject + private AbstractDiagramSynthesis delegate; - def boolean getBooleanValue(SynthesisOption option) { - delegate.getBooleanValue(option) - } - - def T associateWith(T derived, Object source) { - delegate.associateWith(derived, source) - } + public boolean getBooleanValue(SynthesisOption option) { + return delegate.getBooleanValue(option); + } + + public T associateWith(T derived, Object source) { + return delegate.associateWith(derived, source); + } } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java new file mode 100644 index 0000000000..b1f3fbef70 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java @@ -0,0 +1,1215 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.collect.Table; +import de.cau.cs.kieler.klighd.DisplayedActionData; +import de.cau.cs.kieler.klighd.SynthesisOption; +import de.cau.cs.kieler.klighd.kgraph.KEdge; +import de.cau.cs.kieler.klighd.kgraph.KLabel; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.kgraph.KPort; +import de.cau.cs.kieler.klighd.krendering.Colors; +import de.cau.cs.kieler.klighd.krendering.HorizontalAlignment; +import de.cau.cs.kieler.klighd.krendering.KContainerRendering; +import de.cau.cs.kieler.klighd.krendering.KPolyline; +import de.cau.cs.kieler.klighd.krendering.KRectangle; +import de.cau.cs.kieler.klighd.krendering.KRendering; +import de.cau.cs.kieler.klighd.krendering.KRoundedRectangle; +import de.cau.cs.kieler.klighd.krendering.KStyle; +import de.cau.cs.kieler.klighd.krendering.KText; +import de.cau.cs.kieler.klighd.krendering.LineCap; +import de.cau.cs.kieler.klighd.krendering.LineStyle; +import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; +import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KEdgeExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KLabelExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KNodeExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KPolylineExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KPortExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions; +import de.cau.cs.kieler.klighd.syntheses.AbstractDiagramSynthesis; +import de.cau.cs.kieler.klighd.util.KlighdProperties; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.inject.Inject; +import org.eclipse.elk.alg.layered.options.EdgeStraighteningStrategy; +import org.eclipse.elk.alg.layered.options.FixedAlignment; +import org.eclipse.elk.alg.layered.options.LayerConstraint; +import org.eclipse.elk.alg.layered.options.LayeredOptions; +import org.eclipse.elk.core.math.ElkMargin; +import org.eclipse.elk.core.math.ElkPadding; +import org.eclipse.elk.core.math.KVector; +import org.eclipse.elk.core.options.BoxLayouterOptions; +import org.eclipse.elk.core.options.CoreOptions; +import org.eclipse.elk.core.options.Direction; +import org.eclipse.elk.core.options.PortConstraints; +import org.eclipse.elk.core.options.PortSide; +import org.eclipse.elk.core.options.SizeConstraint; +import org.eclipse.elk.graph.properties.Property; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.xbase.lib.Conversions; +import org.eclipse.xtext.xbase.lib.Exceptions; +import org.eclipse.xtext.xbase.lib.Extension; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.eclipse.xtext.xbase.lib.ListExtensions; +import org.eclipse.xtext.xbase.lib.Pair; +import org.eclipse.xtext.xbase.lib.StringExtensions; +import org.lflang.ASTUtils; +import org.lflang.FileConfig; +import org.lflang.diagram.synthesis.action.CollapseAllReactorsAction; +import org.lflang.diagram.synthesis.action.ExpandAllReactorsAction; +import org.lflang.diagram.synthesis.action.FilterCycleAction; +import org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction; +import org.lflang.diagram.synthesis.action.ShowCycleAction; +import org.lflang.diagram.synthesis.styles.LinguaFrancaShapeExtensions; +import org.lflang.diagram.synthesis.styles.LinguaFrancaStyleExtensions; +import org.lflang.diagram.synthesis.styles.ReactorFigureComponents; +import org.lflang.diagram.synthesis.util.CycleVisualization; +import org.lflang.diagram.synthesis.util.InterfaceDependenciesVisualization; +import org.lflang.diagram.synthesis.util.NamedInstanceUtil; +import org.lflang.diagram.synthesis.util.ReactorIcons; +import org.lflang.diagram.synthesis.util.SynthesisErrorReporter; +import org.lflang.diagram.synthesis.util.UtilityExtensions; +import org.lflang.generator.ActionInstance; +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.Connection; +import org.lflang.lf.Model; +import org.lflang.lf.Reactor; + +/** + * Diagram synthesis for Lingua Franca programs. + * + * @author{Alexander Schulz-Rosengarten } + */ +@ViewSynthesisShared +public class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { + @Inject @Extension private KNodeExtensions _kNodeExtensions; + @Inject @Extension private KEdgeExtensions _kEdgeExtensions; + @Inject @Extension private KPortExtensions _kPortExtensions; + @Inject @Extension private KLabelExtensions _kLabelExtensions; + @Inject @Extension private KRenderingExtensions _kRenderingExtensions; + @Inject @Extension private KContainerRenderingExtensions _kContainerRenderingExtensions; + @Inject @Extension private KPolylineExtensions _kPolylineExtensions; + @Inject @Extension private LinguaFrancaStyleExtensions _linguaFrancaStyleExtensions; + @Inject @Extension private LinguaFrancaShapeExtensions _linguaFrancaShapeExtensions; + @Inject @Extension private UtilityExtensions _utilityExtensions; + @Inject @Extension private CycleVisualization _cycleVisualization; + @Inject @Extension private InterfaceDependenciesVisualization _interfaceDependenciesVisualization; + @Inject @Extension private FilterCycleAction _filterCycleAction; + @Inject @Extension private ReactorIcons _reactorIcons; + + // ------------------------------------------------------------------------- + + public static final String ID = "org.lflang.diagram.synthesis.LinguaFrancaSynthesis"; + + // -- INTERNAL -- + public static final Property REACTOR_RECURSIVE_INSTANTIATION = new Property<>("org.lflang.linguafranca.diagram.synthesis.reactor.recursive.instantiation", false); + public static final Property REACTOR_HAS_BANK_PORT_OFFSET = new Property<>("org.lflang.linguafranca.diagram.synthesis.reactor.bank.offset", false); + public static final Property REACTOR_INPUT = new Property<>("org.lflang.linguafranca.diagram.synthesis.reactor.input", false); + public static final Property REACTOR_OUTPUT = new Property<>("org.lflang.linguafranca.diagram.synthesis.reactor.output", false); + + // -- STYLE -- + public static final List ALTERNATIVE_DASH_PATTERN = List.of(3.0f); + + // -- TEXT -- + public static final String TEXT_ERROR_RECURSIVE = "Recursive reactor instantiation!"; + public static final String TEXT_ERROR_CONTAINS_RECURSION = "Reactor contains recursive instantiation!"; + public static final String TEXT_ERROR_CONTAINS_CYCLE = "Reactor contains cyclic dependencies!"; + public static final String TEXT_ERROR_CYCLE_DETECTION = "Dependency cycle detection failed.\nCould not detect dependency cycles due to unexpected graph structure."; + public static final String TEXT_ERROR_CYCLE_BTN_SHOW = "Show Cycle"; + public static final String TEXT_ERROR_CYCLE_BTN_FILTER = "Filter Cycle"; + public static final String TEXT_ERROR_CYCLE_BTN_UNFILTER = "Remove Cycle Filter"; + public static final String TEXT_NO_MAIN_REACTOR = "No Main Reactor"; + public static final String TEXT_REACTOR_NULL = "Reactor is null"; + public static final String TEXT_HIDE_ACTION = "[Hide]"; + public static final String TEXT_SHOW_ACTION = "[Details]"; + + // ------------------------------------------------------------------------- + + /** Synthesis category */ + public static final SynthesisOption APPEARANCE = SynthesisOption.createCategory("Appearance", true); + public static final SynthesisOption EXPERIMENTAL = SynthesisOption.createCategory("Experimental", true); + + /** Synthesis options */ + public static final SynthesisOption SHOW_ALL_REACTORS = SynthesisOption.createCheckOption("All Reactors", false); + public static final SynthesisOption CYCLE_DETECTION = SynthesisOption.createCheckOption("Dependency Cycle Detection", true); + + public static final SynthesisOption SHOW_USER_LABELS = SynthesisOption.createCheckOption("User Labels (@label in JavaDoc)", true).setCategory(APPEARANCE); + public static final SynthesisOption SHOW_HYPERLINKS = SynthesisOption.createCheckOption("Expand/Collapse Hyperlinks", false).setCategory(APPEARANCE); + public static final SynthesisOption REACTIONS_USE_HYPEREDGES = SynthesisOption.createCheckOption("Bundled Dependencies", false).setCategory(APPEARANCE); + public static final SynthesisOption USE_ALTERNATIVE_DASH_PATTERN = SynthesisOption.createCheckOption("Alternative Dependency Line Style", false).setCategory(APPEARANCE); + public static final SynthesisOption SHOW_PORT_NAMES = SynthesisOption.createCheckOption("Port names", true).setCategory(APPEARANCE); + public static final SynthesisOption SHOW_MULTIPORT_WIDTH = SynthesisOption.createCheckOption("Multiport Widths", false).setCategory(APPEARANCE); + public static final SynthesisOption SHOW_REACTION_CODE = SynthesisOption.createCheckOption("Reaction Code", false).setCategory(APPEARANCE); + public static final SynthesisOption SHOW_REACTION_LEVEL = SynthesisOption.createCheckOption("Reaction Level", false).setCategory(APPEARANCE); + public static final SynthesisOption SHOW_REACTION_ORDER_EDGES = SynthesisOption.createCheckOption("Reaction Order Edges", false).setCategory(APPEARANCE); + public static final SynthesisOption SHOW_REACTOR_HOST = SynthesisOption.createCheckOption("Reactor Host Addresses", true).setCategory(APPEARANCE); + public static final SynthesisOption SHOW_INSTANCE_NAMES = SynthesisOption.createCheckOption("Reactor Instance Names", false).setCategory(APPEARANCE); + public static final SynthesisOption REACTOR_PARAMETER_MODE = SynthesisOption.createChoiceOption("Reactor Parameters", ((List)Conversions.doWrapArray(ReactorParameterDisplayModes.values())), ReactorParameterDisplayModes.NONE).setCategory(APPEARANCE); + public static final SynthesisOption REACTOR_PARAMETER_TABLE_COLS = SynthesisOption.createRangeOption("Reactor Parameter Table Columns", 1, 10, 1).setCategory(APPEARANCE); + + /** Synthesis actions */ + public static final DisplayedActionData COLLAPSE_ALL = DisplayedActionData.create(CollapseAllReactorsAction.ID, "Hide all Details"); + public static final DisplayedActionData EXPAND_ALL = DisplayedActionData.create(ExpandAllReactorsAction.ID, "Show all Details"); + + @Override + public List getDisplayedSynthesisOptions() { + return List.of( + SHOW_ALL_REACTORS, + MemorizingExpandCollapseAction.MEMORIZE_EXPANSION_STATES, + CYCLE_DETECTION, + SHOW_USER_LABELS, + SHOW_HYPERLINKS, + //LinguaFrancaSynthesisInterfaceDependencies.SHOW_INTERFACE_DEPENDENCIES, + REACTIONS_USE_HYPEREDGES, + USE_ALTERNATIVE_DASH_PATTERN, + SHOW_PORT_NAMES, + SHOW_MULTIPORT_WIDTH, + SHOW_REACTION_CODE, + SHOW_REACTION_LEVEL, + SHOW_REACTION_ORDER_EDGES, + SHOW_REACTOR_HOST, + SHOW_INSTANCE_NAMES, + REACTOR_PARAMETER_MODE, + REACTOR_PARAMETER_TABLE_COLS + ); + } + + @Override + public List getDisplayedActions() { + return List.of(COLLAPSE_ALL, EXPAND_ALL); + } + + // ------------------------------------------------------------------------- + + @Override + public KNode transform(final Model model) { + KNode rootNode = _kNodeExtensions.createNode(); + + try { + // Find main + Reactor main = IterableExtensions.findFirst(model.getReactors(), _utilityExtensions::isMainOrFederated); + if (main != null) { + ReactorInstance reactorInstance = new ReactorInstance(main, new SynthesisErrorReporter()); + rootNode.getChildren().addAll(createReactorNode(reactorInstance, true, null, null, new HashMap<>())); + } else { + KNode messageNode = _kNodeExtensions.createNode(); + _linguaFrancaShapeExtensions.addErrorMessage(messageNode, TEXT_NO_MAIN_REACTOR, null); + rootNode.getChildren().add(messageNode); + } + + // Show all reactors + if (main == null || getBooleanValue(SHOW_ALL_REACTORS)) { + List reactorNodes = new ArrayList<>(); + for (Reactor reactor : model.getReactors()) { + if (reactor == main) continue; + ReactorInstance reactorInstance = new ReactorInstance(reactor, new SynthesisErrorReporter(), new HashSet<>()); + reactorNodes.addAll(createReactorNode(reactorInstance, main == null, + HashBasedTable.create(), + HashBasedTable.create(), + new HashMap<>())); + } + if (!reactorNodes.isEmpty()) { + // To allow ordering, we need box layout but we also need layered layout for ports thus wrap all node + // TODO use rect packing in the future + reactorNodes.add(0, IterableExtensions.head(rootNode.getChildren())); + + int index = 0; + for (KNode node : reactorNodes) { + if (node.getProperty(CoreOptions.COMMENT_BOX)) continue; + KNode child = _kNodeExtensions.createNode(); + child.getChildren().add(node); + // Add comment nodes + for (KEdge edge : node.getIncomingEdges()) { + if (!edge.getSource().getProperty(CoreOptions.COMMENT_BOX)) continue; + child.getChildren().add(edge.getSource()); + } + _kRenderingExtensions.addInvisibleContainerRendering(child); + setLayoutOption(child, CoreOptions.ALGORITHM, LayeredOptions.ALGORITHM_ID); + setLayoutOption(child, CoreOptions.PADDING, new ElkPadding(0)); + setLayoutOption(child, CoreOptions.PRIORITY, reactorNodes.size() - index); // Order! + rootNode.getChildren().add(child); + index++; + } + + setLayoutOption(rootNode, CoreOptions.ALGORITHM, BoxLayouterOptions.ALGORITHM_ID); + setLayoutOption(rootNode, CoreOptions.SPACING_NODE_NODE, 25.0); + } + } + } catch (Exception e) { + e.printStackTrace(); + + KNode messageNode = _kNodeExtensions.createNode(); + _linguaFrancaShapeExtensions.addErrorMessage(messageNode, "Error in Diagram Synthesis", + e.getClass().getSimpleName() + " occurred. Could not create diagram."); + rootNode.getChildren().add(messageNode); + } + + return rootNode; + } + + private Collection createReactorNode( + ReactorInstance reactorInstance, + boolean expandDefault, + Table inputPortsReg, + Table outputPortsReg, + Map allReactorNodes + ) { + Reactor reactor = reactorInstance.reactorDefinition; + KNode node = _kNodeExtensions.createNode(); + allReactorNodes.put(reactorInstance, node); + associateWith(node, reactor); + _utilityExtensions.setID(node, reactorInstance.uniqueID()); + // save to distinguish nodes associated with the same reactor + NamedInstanceUtil.linkInstance(node, reactorInstance); + + List nodes = new ArrayList<>(); + nodes.add(node); + String label = createReactorLabel(reactorInstance); + + if (reactorInstance.recursive) { + // Mark this node + node.setProperty(REACTOR_RECURSIVE_INSTANTIATION, true); + // Mark root + allReactorNodes.get(reactorInstance.root()).setProperty(REACTOR_RECURSIVE_INSTANTIATION, true); + } + + if (reactor == null) { + _linguaFrancaShapeExtensions.addErrorMessage(node, TEXT_REACTOR_NULL, null); + } else if (reactorInstance.isMainOrFederated()) { + KRoundedRectangle figure = _linguaFrancaShapeExtensions.addMainReactorFigure(node, reactorInstance, label); + + if (getObjectValue(REACTOR_PARAMETER_MODE) == ReactorParameterDisplayModes.TABLE + && !reactorInstance.parameters.isEmpty() + ) { + KRectangle rectangle = _kContainerRenderingExtensions.addRectangle(figure); + _kRenderingExtensions.setInvisible(rectangle, true); + _kRenderingExtensions.to( + _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(rectangle), + _kRenderingExtensions.LEFT, 8, 0, + _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 8, 0, + _kRenderingExtensions.BOTTOM, 4, 0); + _kRenderingExtensions.setHorizontalAlignment(rectangle, HorizontalAlignment.LEFT); + addParameterList(rectangle, reactorInstance.parameters); + } + + if (reactorInstance.recursive) { + nodes.add(addErrorComment(node, TEXT_ERROR_RECURSIVE)); + _linguaFrancaStyleExtensions.errorStyle(figure); + } else { + _kContainerRenderingExtensions.addChildArea(figure); + node.getChildren().addAll(transformReactorNetwork(reactorInstance, + new HashMap<>(), + new HashMap<>(), + allReactorNodes)); + } + Iterables.addAll(nodes, createUserComments(reactor, node)); + configureReactorNodeLayout(node); + + // Additional layout adjustment for main node + setLayoutOption(node, CoreOptions.ALGORITHM, LayeredOptions.ALGORITHM_ID); + setLayoutOption(node, CoreOptions.DIRECTION, Direction.RIGHT); + setLayoutOption(node, CoreOptions.NODE_SIZE_CONSTRAINTS, EnumSet.of(SizeConstraint.MINIMUM_SIZE)); + setLayoutOption(node, LayeredOptions.NODE_PLACEMENT_BK_FIXED_ALIGNMENT, FixedAlignment.BALANCED); + setLayoutOption(node, LayeredOptions.NODE_PLACEMENT_BK_EDGE_STRAIGHTENING, EdgeStraighteningStrategy.IMPROVE_STRAIGHTNESS); + setLayoutOption(node, LayeredOptions.SPACING_EDGE_NODE, LayeredOptions.SPACING_EDGE_NODE.getDefault() * 1.1f); + setLayoutOption(node, LayeredOptions.SPACING_EDGE_NODE_BETWEEN_LAYERS, LayeredOptions.SPACING_EDGE_NODE_BETWEEN_LAYERS.getDefault() * 1.1f); + if (!getBooleanValue(SHOW_HYPERLINKS)) { + setLayoutOption(node, CoreOptions.PADDING, new ElkPadding(-1, 6, 6, 6)); + setLayoutOption(node, LayeredOptions.SPACING_COMPONENT_COMPONENT, LayeredOptions.SPACING_COMPONENT_COMPONENT.getDefault() * 0.5f); + } + } else { + ReactorInstance instance = reactorInstance; + + // Expanded Rectangle + ReactorFigureComponents comps = _linguaFrancaShapeExtensions.addReactorFigure(node, reactorInstance, label); + comps.getOuter().setProperty(KlighdProperties.EXPANDED_RENDERING, true); + for (KRendering figure : comps.getFigures()) { + associateWith(figure, reactor); + _kRenderingExtensions.addDoubleClickAction(figure, MemorizingExpandCollapseAction.ID); + } + _reactorIcons.handleIcon(comps.getReactor(), reactor, false); + + if (getBooleanValue(SHOW_HYPERLINKS)) { + // Collapse button + KText button = _linguaFrancaShapeExtensions.addTextButton(comps.getReactor(), TEXT_HIDE_ACTION); + _kRenderingExtensions.to( + _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(button), + _kRenderingExtensions.LEFT, 8, 0, + _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 8, 0, + _kRenderingExtensions.BOTTOM, 0, 0); + _kRenderingExtensions.addSingleClickAction(button, MemorizingExpandCollapseAction.ID); + _kRenderingExtensions.addDoubleClickAction(button, MemorizingExpandCollapseAction.ID); + } + + if (getObjectValue(REACTOR_PARAMETER_MODE) == ReactorParameterDisplayModes.TABLE + && !instance.parameters.isEmpty()) { + KRectangle rectangle = _kContainerRenderingExtensions.addRectangle(comps.getReactor()); + _kRenderingExtensions.setInvisible(rectangle, true); + if (!getBooleanValue(SHOW_HYPERLINKS)) { + _kRenderingExtensions.to( + _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(rectangle), + _kRenderingExtensions.LEFT, 8, 0, + _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 8, 0, + _kRenderingExtensions.BOTTOM, 4, 0); + } else { + _kRenderingExtensions.to( + _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(rectangle), + _kRenderingExtensions.LEFT, 8, 0, + _kRenderingExtensions.TOP, 4, 0), + _kRenderingExtensions.RIGHT, 8, 0, + _kRenderingExtensions.BOTTOM, 0, 0); + } + _kRenderingExtensions.setHorizontalAlignment(rectangle, HorizontalAlignment.LEFT); + addParameterList(rectangle, instance.parameters); + } + + if (instance.recursive) { + comps.getFigures().forEach(_linguaFrancaStyleExtensions::errorStyle); + } else { + _kContainerRenderingExtensions.addChildArea(comps.getReactor()); + } + + // Collapse Rectangle + comps = _linguaFrancaShapeExtensions.addReactorFigure(node, reactorInstance, label); + comps.getOuter().setProperty(KlighdProperties.COLLAPSED_RENDERING, true); + for (KRendering figure : comps.getFigures()) { + associateWith(figure, reactor); + if (_utilityExtensions.hasContent(instance) && !instance.recursive) { + _kRenderingExtensions.addDoubleClickAction(figure, MemorizingExpandCollapseAction.ID); + } + } + _reactorIcons.handleIcon(comps.getReactor(), reactor, true); + + if (getBooleanValue(SHOW_HYPERLINKS)) { + // Expand button + if (_utilityExtensions.hasContent(instance) && !instance.recursive) { + KText button = _linguaFrancaShapeExtensions.addTextButton(comps.getReactor(), TEXT_SHOW_ACTION); + _kRenderingExtensions.to( + _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(button), + _kRenderingExtensions.LEFT, 8, 0, + _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 8, 0, + _kRenderingExtensions.BOTTOM, 8, 0); + _kRenderingExtensions.addSingleClickAction(button, MemorizingExpandCollapseAction.ID); + _kRenderingExtensions.addDoubleClickAction(button, MemorizingExpandCollapseAction.ID); + } + } + + if (instance.recursive) { + comps.getFigures().forEach(_linguaFrancaStyleExtensions::errorStyle); + } + + + // Create ports + Map inputPorts = new HashMap<>(); + Map outputPorts = new HashMap<>(); + for (PortInstance input : ListExtensions.reverseView(instance.inputs)) { + inputPorts.put(input, addIOPort(node, input, true, input.isMultiport(), reactorInstance.isBank())); + } + for (PortInstance output : instance.outputs) { + outputPorts.put(output, addIOPort(node, output, false, output.isMultiport(), reactorInstance.isBank())); + } + // Mark ports + inputPorts.values().forEach(it -> it.setProperty(REACTOR_INPUT, true)); + outputPorts.values().forEach(it -> it.setProperty(REACTOR_OUTPUT, true)); + + // Add content + if (_utilityExtensions.hasContent(instance) && !instance.recursive) { + node.getChildren().addAll(transformReactorNetwork(instance, inputPorts, outputPorts, allReactorNodes)); + } + + // Pass port to given tables + if (!_utilityExtensions.isRoot(instance)) { + if (inputPortsReg != null) { + for (Map.Entry entry : inputPorts.entrySet()) { + inputPortsReg.put(instance, entry.getKey(), entry.getValue()); + } + } + if (outputPortsReg != null) { + for (Map.Entry entry : outputPorts.entrySet()) { + outputPortsReg.put(instance, entry.getKey(), entry.getValue()); + } + } + } + + if (instance.recursive) { + setLayoutOption(node, KlighdProperties.EXPAND, false); + nodes.add(addErrorComment(node, TEXT_ERROR_RECURSIVE)); + } else { + setLayoutOption(node, KlighdProperties.EXPAND, expandDefault); + + // Interface Dependencies + _interfaceDependenciesVisualization.addInterfaceDependencies(node, expandDefault); + } + + if (!_utilityExtensions.isRoot(instance)) { + // If all reactors are being shown, then only put the label on + // the reactor definition, not on its instances. Otherwise, + // add the annotation now. + if (!getBooleanValue(SHOW_ALL_REACTORS)) { + Iterables.addAll(nodes, createUserComments(reactor, node)); + } + } else { + Iterables.addAll(nodes, createUserComments(reactor, node)); + } + configureReactorNodeLayout(node); + } + + // Find and annotate cycles + if (getBooleanValue(CYCLE_DETECTION) && + _utilityExtensions.isRoot(reactorInstance)) { + KNode errNode = detectAndAnnotateCycles(node, reactorInstance, allReactorNodes); + if (errNode != null) { + nodes.add(errNode); + } + } + + return nodes; + } + + private KNode configureReactorNodeLayout(KNode node) { + KNode retNode; + setLayoutOption(node, CoreOptions.NODE_SIZE_CONSTRAINTS, SizeConstraint.minimumSizeWithPorts()); + setLayoutOption(node, CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_ORDER); + retNode = setLayoutOption(node, LayeredOptions.CROSSING_MINIMIZATION_SEMI_INTERACTIVE, true); + if (!getBooleanValue(SHOW_HYPERLINKS)) { + setLayoutOption(node, CoreOptions.PADDING, new ElkPadding(2, 6, 6, 6)); + setLayoutOption(node, LayeredOptions.SPACING_NODE_NODE, LayeredOptions.SPACING_NODE_NODE.getDefault() * 0.75f); + setLayoutOption(node, LayeredOptions.SPACING_NODE_NODE_BETWEEN_LAYERS, LayeredOptions.SPACING_NODE_NODE_BETWEEN_LAYERS.getDefault() * 0.75f); + setLayoutOption(node, LayeredOptions.SPACING_EDGE_NODE, LayeredOptions.SPACING_EDGE_NODE.getDefault() * 0.75f); + retNode = setLayoutOption(node, LayeredOptions.SPACING_EDGE_NODE_BETWEEN_LAYERS, LayeredOptions.SPACING_EDGE_NODE_BETWEEN_LAYERS.getDefault() * 0.75f); + } + return retNode; + } + + private KNode detectAndAnnotateCycles(KNode node, ReactorInstance reactorInstance, Map allReactorNodes) { + if (node.getProperty(REACTOR_RECURSIVE_INSTANTIATION)) { + _filterCycleAction.resetCycleFiltering(node); + return addErrorComment(node, TEXT_ERROR_CONTAINS_RECURSION); + } else { // only detect dependency cycles if not recursive + try { + boolean hasCycle = _cycleVisualization.detectAndHighlightCycles(reactorInstance, + allReactorNodes, it -> { + if (it instanceof KNode) { + List renderings = IterableExtensions.toList( + Iterables.filter(((KNode) it).getData(), KRendering.class)); + if (renderings.size() == 1) { + _linguaFrancaStyleExtensions.errorStyle(IterableExtensions.head(renderings)); + } else { + IterableExtensions.filter(renderings, rendering -> { + return rendering.getProperty(KlighdProperties.COLLAPSED_RENDERING); + }).forEach(_linguaFrancaStyleExtensions::errorStyle); + } + } else if (it instanceof KEdge) { + Iterables.filter(((KEdge) it).getData(), + KRendering.class).forEach(_linguaFrancaStyleExtensions::errorStyle); + // TODO initiallyHide does not work with incremental (https://github.com/kieler/KLighD/issues/37) + // cycleEgde.initiallyShow() // Show hidden order dependencies + _kRenderingExtensions.setInvisible(_kRenderingExtensions.getKRendering(it), false); + } else if (it instanceof KPort) { + Iterables.filter(((KPort) it).getData(), + KRendering.class).forEach(_linguaFrancaStyleExtensions::errorStyle); + //it.reverseTrianglePort() + } + }); + + if (hasCycle) { + KNode err = addErrorComment(node, TEXT_ERROR_CONTAINS_CYCLE); + + // Add to existing figure + KRectangle rectangle = _kContainerRenderingExtensions.addRectangle(_kRenderingExtensions.getKContainerRendering(err)); + _kRenderingExtensions.to( + _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(rectangle), + _kRenderingExtensions.LEFT, 3, 0, + _kRenderingExtensions.TOP, (-1), 0), + _kRenderingExtensions.RIGHT, 3, 0, + _kRenderingExtensions.BOTTOM, 3, 0); + _linguaFrancaStyleExtensions.noSelectionStyle(rectangle); + _kRenderingExtensions.setInvisible(rectangle, true); + _kContainerRenderingExtensions.setGridPlacement(rectangle, 2); + + KRectangle subrectangle = _kContainerRenderingExtensions.addRectangle(rectangle); + _kRenderingExtensions.to( + _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(subrectangle), + _kRenderingExtensions.LEFT, 0, 0, + _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 2, 0, + _kRenderingExtensions.BOTTOM, 0, 0); + _linguaFrancaStyleExtensions.noSelectionStyle(subrectangle); + _kRenderingExtensions.addSingleClickAction(subrectangle, ShowCycleAction.ID); + + KText subrectangleText = _kContainerRenderingExtensions.addText(subrectangle, TEXT_ERROR_CYCLE_BTN_SHOW); + // Copy text style + List styles = ListExtensions.map( + IterableExtensions.head( + _kRenderingExtensions.getKContainerRendering(err).getChildren()).getStyles(), + EcoreUtil::copy); + subrectangleText.getStyles().addAll(styles); + _kRenderingExtensions.setFontSize(subrectangleText, 5); + _kRenderingExtensions.setSurroundingSpace(subrectangleText, 1, 0); + _linguaFrancaStyleExtensions.noSelectionStyle(subrectangleText); + _kRenderingExtensions.addSingleClickAction(subrectangleText, ShowCycleAction.ID); + + subrectangle = _kContainerRenderingExtensions.addRectangle(rectangle); + _kRenderingExtensions.to( + _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(subrectangle), + _kRenderingExtensions.LEFT, 0, 0, + _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 0, 0, + _kRenderingExtensions.BOTTOM, 0, 0); + _linguaFrancaStyleExtensions.noSelectionStyle(subrectangle); + _kRenderingExtensions.addSingleClickAction(subrectangle, FilterCycleAction.ID); + + subrectangleText = _kContainerRenderingExtensions.addText(subrectangle, + _filterCycleAction.isCycleFiltered(node) ? + TEXT_ERROR_CYCLE_BTN_UNFILTER : TEXT_ERROR_CYCLE_BTN_FILTER); + // Copy text style + styles = ListExtensions.map( + IterableExtensions.head( + _kRenderingExtensions.getKContainerRendering(err).getChildren()).getStyles(), + EcoreUtil::copy); + subrectangleText.getStyles().addAll(styles); + _kRenderingExtensions.setFontSize(subrectangleText, 5); + _kRenderingExtensions.setSurroundingSpace(subrectangleText, 1, 0); + _linguaFrancaStyleExtensions.noSelectionStyle(subrectangleText); + _kRenderingExtensions.addSingleClickAction(subrectangleText, FilterCycleAction.ID); + _filterCycleAction.markCycleFilterText(subrectangleText, err); + + // if user interactively requested a filtered diagram keep it filtered during updates + if (_filterCycleAction.isCycleFiltered(node)) { + _filterCycleAction.filterCycle(node); + } + return err; + } + } catch(Exception e) { + _filterCycleAction.resetCycleFiltering(node); + e.printStackTrace(); + return addErrorComment(node, TEXT_ERROR_CYCLE_DETECTION); + } + } + return null; + } + + private Collection transformReactorNetwork( + ReactorInstance reactorInstance, + Map parentInputPorts, + Map parentOutputPorts, + Map allReactorNodes + ) { + List nodes = new ArrayList<>(); + Table inputPorts = HashBasedTable.create(); + Table outputPorts = HashBasedTable.create(); + Map reactionNodes = new HashMap<>(); + Map directConnectionDummyNodes = new HashMap<>(); + Multimap actionDestinations = HashMultimap.create(); + Multimap actionSources = HashMultimap.create(); + Map timerNodes = new HashMap<>(); + KNode startupNode = _kNodeExtensions.createNode(); + boolean startupUsed = false; + KNode shutdownNode = _kNodeExtensions.createNode(); + boolean shutdownUsed = false; + + // Transform instances + int index = 0; + for (ReactorInstance child : ListExtensions.reverseView(reactorInstance.children)) { + Boolean expansionState = MemorizingExpandCollapseAction.getExpansionState(child); + Collection rNodes = createReactorNode( + child, + expansionState != null ? expansionState : false, + inputPorts, + outputPorts, + allReactorNodes); + setLayoutOption(IterableExtensions.head(rNodes), CoreOptions.PRIORITY, index); + nodes.addAll(rNodes); + index++; + } + + // Create timers + for (TimerInstance timer : reactorInstance.timers) { + KNode node = associateWith(_kNodeExtensions.createNode(), timer.getDefinition()); + NamedInstanceUtil.linkInstance(node, timer); + nodes.add(node); + Iterables.addAll(nodes, createUserComments(timer.getDefinition(), node)); + timerNodes.put(timer, node); + _linguaFrancaShapeExtensions.addTimerFigure(node, timer); + } + + // Create reactions + for (ReactionInstance reaction : ListExtensions.reverseView(reactorInstance.reactions)) { + int idx = reactorInstance.reactions.indexOf(reaction); + KNode node = associateWith(_kNodeExtensions.createNode(), reaction.getDefinition()); + NamedInstanceUtil.linkInstance(node, reaction); + nodes.add(node); + Iterables.addAll(nodes, createUserComments(reaction.getDefinition(), node)); + reactionNodes.put(reaction, node); + + setLayoutOption(node, CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_SIDE); + setLayoutOption(node, CoreOptions.PRIORITY, (reactorInstance.reactions.size() - idx) * 10 ); // always place with higher priority than reactor nodes + setLayoutOption(node, LayeredOptions.POSITION, new KVector(0, idx)); // try order reactions vertically if in one layer + + _linguaFrancaShapeExtensions.addReactionFigure(node, reaction); + + // connect input + KPort port = null; + for (TriggerInstance trigger : reaction.triggers) { + port = addInvisiblePort(node); + setLayoutOption(port, CoreOptions.PORT_SIDE, PortSide.WEST); + int triggersSize = reaction.triggers != null ? reaction.triggers.size() : 0; + int sourcesSize = reaction.sources != null ? reaction.sources.size() : 0; + if (getBooleanValue(REACTIONS_USE_HYPEREDGES) || triggersSize + sourcesSize == 1) { + // manual adjustment disabling automatic one + setLayoutOption(port, CoreOptions.PORT_BORDER_OFFSET, + (double) -LinguaFrancaShapeExtensions.REACTION_POINTINESS); + } + + if (trigger.isStartup()) { + connect(createDependencyEdge(((TriggerInstance.BuiltinTriggerVariable) trigger.getDefinition()).definition), + startupNode, + port); + startupUsed = true; + } else if (trigger.isShutdown()) { + connect(createDelayEdge(((TriggerInstance.BuiltinTriggerVariable) trigger.getDefinition()).definition), + shutdownNode, + port); + shutdownUsed = true; + } else if (trigger instanceof ActionInstance) { + actionDestinations.put(((ActionInstance) trigger), port); + } else if (trigger instanceof PortInstance) { + KPort src = null; + PortInstance triggerAsPort = (PortInstance) trigger; + if (triggerAsPort.getParent() == reactorInstance) { + src = parentInputPorts.get(trigger); + } else { + src = outputPorts.get(triggerAsPort.getParent(), trigger); + } + if (src != null) { + connect(createDependencyEdge(triggerAsPort.getDefinition()), src, port); + } + } else if (trigger instanceof TimerInstance) { + KNode src = timerNodes.get(trigger); + if (src != null) { + connect(createDependencyEdge(trigger.getDefinition()), src, port); + } + } + } + + // connect dependencies + //port = null // create new ports + for (TriggerInstance dep : reaction.sources) { + if (reaction.triggers.contains(dep)) continue; + if (!(getBooleanValue(REACTIONS_USE_HYPEREDGES) && port != null)) { + port = addInvisiblePort(node); + setLayoutOption(port, CoreOptions.PORT_SIDE, PortSide.WEST); + int triggersSize = reaction.triggers != null ? reaction.triggers.size() : 0; + int sourcesSize = reaction.sources != null ? reaction.sources.size() : 0; + if (getBooleanValue(REACTIONS_USE_HYPEREDGES) || triggersSize + sourcesSize == 1) { + // manual adjustment disabling automatic one + setLayoutOption(port, CoreOptions.PORT_BORDER_OFFSET, + (double) -LinguaFrancaShapeExtensions.REACTION_POINTINESS); + } + } + + if (dep instanceof PortInstance) { + KPort src = null; + PortInstance depAsPort = (PortInstance) dep; + if (dep.getParent() == reactorInstance) { + src = parentInputPorts.get(dep); + } else { + src = outputPorts.get(depAsPort.getParent(), dep); + } + if (src != null) { + connect(createDependencyEdge(dep.getDefinition()), src, port); + } + } + } + + // connect outputs + port = null; // create new ports + Set> iterSet = reaction.effects != null ? reaction.effects : new HashSet<>(); + for (TriggerInstance effect : iterSet) { + port = addInvisiblePort(node); + setLayoutOption(port, CoreOptions.PORT_SIDE, PortSide.EAST); + + if (effect instanceof ActionInstance) { + actionSources.put((ActionInstance) effect, port); + } else if (effect instanceof PortInstance) { + KPort dst = null; + PortInstance effectAsPort = (PortInstance) effect; + if (effectAsPort.isOutput()) { + dst = parentOutputPorts.get(effect); + } else { + dst = inputPorts.get(effectAsPort.getParent(), effect); + } + if (dst != null) { + connect(createDependencyEdge(effect), port, dst); + } + } + } + } + + // Connect actions + Set actions = new HashSet<>(); + actions.addAll(actionSources.keySet()); + actions.addAll(actionDestinations.keySet()); + + for (ActionInstance action : actions) { + KNode node = associateWith(_kNodeExtensions.createNode(), action.getDefinition()); + NamedInstanceUtil.linkInstance(node, action); + nodes.add(node); + Iterables.addAll(nodes, createUserComments(action.getDefinition(), node)); + setLayoutOption(node, CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_SIDE); + Pair ports = _linguaFrancaShapeExtensions.addActionFigureAndPorts( + node, + action.isPhysical() ? "P" : "L"); + // TODO handle variables? + if (action.getMinDelay() != null && action.getMinDelay() != ActionInstance.DEFAULT_MIN_DELAY) { + _kLabelExtensions.addOutsideBottomCenteredNodeLabel( + node, + String.format("min delay: %s", action.getMinDelay().toString()), + 7); + } + // TODO default value? + if (action.getDefinition().getMinSpacing() != null) { + _kLabelExtensions.addOutsideBottomCenteredNodeLabel(node, + String.format("min spacing: %s", action.getMinSpacing().toString()), + 7); + } + if (!StringExtensions.isNullOrEmpty(action.getDefinition().getPolicy())) { + _kLabelExtensions.addOutsideBottomCenteredNodeLabel(node, + String.format("policy: %s", action.getPolicy().toString()), + 7); + } + // connect source + for (KPort source : actionSources.get(action)) { + connect(createDelayEdge(action), source, ports.getKey()); + } + + // connect targets + for (KPort target : actionDestinations.get(action)) { + connect(createDelayEdge(action), ports.getValue(), target); + } + } + + // Transform connections. + // First, collect all the source ports. + List sourcePorts = new LinkedList<>(reactorInstance.inputs); + for (ReactorInstance child : reactorInstance.children) { + sourcePorts.addAll(child.outputs); + } + + for (PortInstance leftPort : sourcePorts) { + KPort source = leftPort.getParent() == reactorInstance ? + parentInputPorts.get(leftPort) : + outputPorts.get(leftPort.getParent(), leftPort); + + for (SendRange sendRange : leftPort.getDependentPorts()) { + for (RuntimeRange rightRange : sendRange.destinations) { + PortInstance rightPort = rightRange.instance; + KPort target = rightPort.getParent() == reactorInstance ? + parentOutputPorts.get(rightPort) : + inputPorts.get(rightPort.getParent(), rightPort); + // There should be a connection, but skip if not. + Connection connection = sendRange.connection; + if (connection != null) { + KEdge edge = createIODependencyEdge(connection, (leftPort.isMultiport() || rightPort.isMultiport())); + if (connection.getDelay() != null) { + KLabel delayLabel = _kLabelExtensions.addCenterEdgeLabel(edge, ASTUtils.toText(connection.getDelay())); + associateWith(delayLabel, connection.getDelay()); + if (connection.isPhysical()) { + _linguaFrancaStyleExtensions.applyOnEdgePysicalDelayStyle(delayLabel, + reactorInstance.isMainOrFederated() ? Colors.WHITE : Colors.GRAY_95); + } else { + _linguaFrancaStyleExtensions.applyOnEdgeDelayStyle(delayLabel); + } + } else if (connection.isPhysical()) { + KLabel physicalConnectionLabel = _kLabelExtensions.addCenterEdgeLabel(edge, "---"); + _linguaFrancaStyleExtensions.applyOnEdgePysicalStyle(physicalConnectionLabel, + reactorInstance.isMainOrFederated() ? 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) + KNode dummy = _kNodeExtensions.createNode(); + if (directConnectionDummyNodes.containsKey(target)) { + dummy = directConnectionDummyNodes.get(target); + } else { + nodes.add(dummy); + directConnectionDummyNodes.put(target, dummy); + _kRenderingExtensions.addInvisibleContainerRendering(dummy); + _kNodeExtensions.setNodeSize(dummy, 0, 0); + KEdge extraEdge = createIODependencyEdge(null, + (leftPort.isMultiport() || rightPort.isMultiport())); + connect(extraEdge, dummy, target); + } + connect(edge, source, dummy); + } else { + connect(edge, source, target); + } + } + } + } + } + } + + // Add startup/shutdown + if (startupUsed) { + _linguaFrancaShapeExtensions.addStartupFigure(startupNode); + nodes.add(0, startupNode); + setLayoutOption(startupNode, LayeredOptions.LAYERING_LAYER_CONSTRAINT, LayerConstraint.FIRST); + if (getBooleanValue(REACTIONS_USE_HYPEREDGES)) { + KPort port = addInvisiblePort(startupNode); + startupNode.getOutgoingEdges().forEach(it -> { + it.setSourcePort(port); + }); + } + } + if (shutdownUsed) { + _linguaFrancaShapeExtensions.addShutdownFigure(shutdownNode); + nodes.add(0, shutdownNode); + if (getBooleanValue(REACTIONS_USE_HYPEREDGES)) { // connect all edges to one port + KPort port = addInvisiblePort(shutdownNode); + shutdownNode.getOutgoingEdges().forEach(it -> { + it.setSourcePort(port); + }); + } + } + + // Postprocess timer nodes + if (getBooleanValue(REACTIONS_USE_HYPEREDGES)) { // connect all edges to one port + for (KNode timerNode : timerNodes.values()) { + KPort port = addInvisiblePort(timerNode); + timerNode.getOutgoingEdges().forEach(it -> { + it.setSourcePort(port); + }); + } + } + + // Add reaction order edges (add last to have them on top of other edges) + if (reactorInstance.reactions.size() > 1) { + KNode prevNode = reactionNodes.get(IterableExtensions.head(reactorInstance.reactions)); + Iterable iterList = IterableExtensions.map( + IterableExtensions.drop(reactorInstance.reactions, 1), + reactionNodes::get); + for (KNode node : iterList) { + KEdge edge = createOrderEdge(); + edge.setSource(prevNode); + edge.setTarget(node); + edge.setProperty(CoreOptions.NO_LAYOUT, true); + + // Do not remove them, as they are needed for cycle detection + KRendering edgeRendering = _kRenderingExtensions.getKRendering(edge); + _kRenderingExtensions.setInvisible(edgeRendering, !getBooleanValue(SHOW_REACTION_ORDER_EDGES)); + _kRenderingExtensions.getInvisible(edgeRendering).setPropagateToChildren(true); + // TODO this does not work work with incremental update (https://github.com/kieler/KLighD/issues/37) + // if (!getBooleanValue(SHOW_REACTION_ORDER_EDGES)) edge.initiallyHide() + + prevNode = node; + } + } + return nodes; + } + + private String createReactorLabel(ReactorInstance reactorInstance) { + StringBuilder b = new StringBuilder(); + if (getBooleanValue(SHOW_INSTANCE_NAMES) && !_utilityExtensions.isRoot(reactorInstance)) { + if (!reactorInstance.isMainOrFederated()) { + b.append(reactorInstance.getName()).append(" : "); + } + } + if (reactorInstance.isMainOrFederated()) { + try { + b.append(FileConfig.nameWithoutExtension(reactorInstance.reactorDeclaration.eResource())); + } catch (Exception e) { + throw Exceptions.sneakyThrow(e); + } + } else if (reactorInstance.reactorDeclaration == null) { + // There is an error in the graph. + b.append(""); + } else { + b.append(reactorInstance.reactorDeclaration.getName()); + } + if (getObjectValue(REACTOR_PARAMETER_MODE) == ReactorParameterDisplayModes.TITLE) { + if (reactorInstance.parameters.isEmpty()) { + b.append("()"); + } else { + b.append(IterableExtensions.join(reactorInstance.parameters, "(", ", ", ")", + it -> { + return createParameterLabel(it, false); + })); + } + } + return b.toString(); + } + + private void addParameterList(KContainerRendering container, List parameters) { + int cols = 1; + try { + cols = getIntValue(REACTOR_PARAMETER_TABLE_COLS); + } catch (Exception e) {} // ignore + if (cols > parameters.size()) { + cols = parameters.size(); + } + _kContainerRenderingExtensions.setGridPlacement(container, cols); + for (ParameterInstance param : parameters) { + KText paramText = _kContainerRenderingExtensions.addText(container, createParameterLabel(param, true)); + _kRenderingExtensions.setFontSize(paramText, 8); + _kRenderingExtensions.setHorizontalAlignment(paramText, HorizontalAlignment.LEFT); + } + } + + private String createParameterLabel(ParameterInstance param, boolean bullet) { + StringBuilder b = new StringBuilder(); + if (bullet) { + b.append("\u2022 "); + } + b.append(param.getName()); + String t = param.type.toText(); + if (!StringExtensions.isNullOrEmpty(t)) { + b.append(":").append(t); + } + if (!IterableExtensions.isNullOrEmpty(param.getInitialValue())) { + b.append("("); + b.append(IterableExtensions.join(param.getInitialValue(), ", ", _utilityExtensions::toText)); + b.append(")"); + } + return b.toString(); + } + + private KEdge createDelayEdge(Object associate) { + KEdge edge = _kEdgeExtensions.createEdge(); + associateWith(edge, associate); + KPolyline line = _kEdgeExtensions.addPolyline(edge); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(line); + _kPolylineExtensions.addJunctionPointDecorator(line); + if (getBooleanValue(USE_ALTERNATIVE_DASH_PATTERN)) { + _kRenderingExtensions.setLineStyle(line, LineStyle.CUSTOM); + _kRenderingExtensions.getLineStyle(line).getDashPattern().addAll(ALTERNATIVE_DASH_PATTERN); + } else { + _kRenderingExtensions.setLineStyle(line, LineStyle.DASH); + } + return edge; + } + + private KEdge createIODependencyEdge(Object associate, boolean multiport) { + KEdge edge = _kEdgeExtensions.createEdge(); + if (associate != null) { + associateWith(edge, associate); + } + KPolyline line = _kEdgeExtensions.addPolyline(edge); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(line); + _kPolylineExtensions.addJunctionPointDecorator(line); + if (multiport) { + // Render multiport connections and bank connections in bold. + _kRenderingExtensions.setLineWidth(line, 2.2f); + _kRenderingExtensions.setLineCap(line, LineCap.CAP_SQUARE); + // Adjust junction point size + _kPolylineExtensions.setJunctionPointDecorator(line, line.getJunctionPointRendering(), 6, 6); + } + return edge; + } + + private KEdge createDependencyEdge(Object associate) { + KEdge edge = _kEdgeExtensions.createEdge(); + if (associate != null) { + associateWith(edge, associate); + } + KPolyline line = _kEdgeExtensions.addPolyline(edge); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(line); + _kPolylineExtensions.addJunctionPointDecorator(line); + if (getBooleanValue(USE_ALTERNATIVE_DASH_PATTERN)) { + _kRenderingExtensions.setLineStyle(line, LineStyle.CUSTOM); + _kRenderingExtensions.getLineStyle(line).getDashPattern().addAll(ALTERNATIVE_DASH_PATTERN); + } else { + _kRenderingExtensions.setLineStyle(line, LineStyle.DASH); + } + return edge; + } + + private KEdge createOrderEdge() { + KEdge edge = _kEdgeExtensions.createEdge(); + KPolyline line = _kEdgeExtensions.addPolyline(edge); + _kRenderingExtensions.setLineWidth(line, 1.5f); + _kRenderingExtensions.setLineStyle(line, LineStyle.DOT); + _kRenderingExtensions.setForeground(line, Colors.CHOCOLATE_1); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(line); + //addFixedTailArrowDecorator() // Fix for bug: https://github.com/kieler/KLighD/issues/38 + _kPolylineExtensions.addHeadArrowDecorator(line); + return edge; + } + + private KEdge connect(KEdge edge, KNode src, KNode dst) { + edge.setSource(src); + edge.setTarget(dst); + return edge; + } + + private KEdge connect(KEdge edge, KNode src, KPort dst) { + edge.setSource(src); + edge.setTargetPort(dst); + edge.setTarget(dst != null ? dst.getNode() : null); + return edge; + } + + private KEdge connect(KEdge edge, KPort src, KNode dst) { + edge.setSourcePort(src); + edge.setSource(src != null ? src.getNode() : null); + edge.setTarget(dst); + return edge; + } + + private KEdge connect(KEdge edge, KPort src, KPort dst) { + edge.setSourcePort(src); + edge.setSource(src != null ? src.getNode() : null); + edge.setTargetPort(dst); + edge.setTarget(dst != null ? dst.getNode() : null); + return edge; + } + + /** + * Translate an input/output into a port. + */ + private KPort addIOPort(KNode node, PortInstance lfPort, boolean input, boolean multiport, boolean bank) { + KPort port = _kPortExtensions.createPort(); + node.getPorts().add(port); + associateWith(port, lfPort.getDefinition()); + NamedInstanceUtil.linkInstance(port, lfPort); + _kPortExtensions.setPortSize(port, 6, 6); + + if (input) { + // multiports are smaller by an offset at the right, hence compensate in inputs + double offset = multiport ? -3.4 : -3.3; + setLayoutOption(port, CoreOptions.PORT_SIDE, PortSide.WEST); + setLayoutOption(port, CoreOptions.PORT_BORDER_OFFSET, offset); + } else { + double offset = multiport ? -2.6 : -3.3; // multiports are smaller + offset = bank ? offset - LinguaFrancaShapeExtensions.BANK_FIGURE_X_OFFSET_SUM : offset; // compensate bank figure width + setLayoutOption(port, CoreOptions.PORT_SIDE, PortSide.EAST); + setLayoutOption(port, CoreOptions.PORT_BORDER_OFFSET, offset); + } + + if (bank && !node.getProperty(REACTOR_HAS_BANK_PORT_OFFSET)) {// compensate bank figure height + // https://github.com/eclipse/elk/issues/693 + _utilityExtensions.getPortMarginsInitIfAbsent(node).add( + new ElkMargin(0, 0, LinguaFrancaShapeExtensions.BANK_FIGURE_Y_OFFSET_SUM, 0)); + node.setProperty(REACTOR_HAS_BANK_PORT_OFFSET, true); // only once + } + + _linguaFrancaShapeExtensions.addTrianglePort(port, multiport); + + String label = lfPort.getName(); + if (!getBooleanValue(SHOW_PORT_NAMES)) { + label = ""; + } + if (getBooleanValue(SHOW_MULTIPORT_WIDTH)) { + if (lfPort.isMultiport()) { + label += (lfPort.getWidth() >= 0) ? + "[" + lfPort.getWidth() + "]" : + "[?]"; + } + } + associateWith(_kLabelExtensions.addOutsidePortLabel(port, label, 8), lfPort.getDefinition()); + return port; + } + + private KPort addInvisiblePort(KNode node) { + KPort port = _kPortExtensions.createPort(); + node.getPorts().add(port); + port.setSize(0, 0); // invisible + return port; + } + + private KNode addErrorComment(KNode node, String message) { + KNode comment = _kNodeExtensions.createNode(); + setLayoutOption(comment, CoreOptions.COMMENT_BOX, true); + KRoundedRectangle commentFigure = _linguaFrancaShapeExtensions.addCommentFigure(comment, message); + _linguaFrancaStyleExtensions.errorStyle(commentFigure); + _kRenderingExtensions.setBackground(commentFigure, Colors.PEACH_PUFF_2); + + // connect + KEdge edge = _kEdgeExtensions.createEdge(); + edge.setSource(comment); + edge.setTarget(node); + _linguaFrancaStyleExtensions.errorStyle(_linguaFrancaShapeExtensions.addCommentPolyline(edge)); + return comment; + } + + private Iterable createUserComments(EObject element, KNode targetNode) { + if (getBooleanValue(SHOW_USER_LABELS)) { + String commentText = ASTUtils.findAnnotationInComments(element, "@label"); + + if (!StringExtensions.isNullOrEmpty(commentText)) { + KNode comment = _kNodeExtensions.createNode(); + setLayoutOption(comment, CoreOptions.COMMENT_BOX, true); + KRoundedRectangle commentFigure = _linguaFrancaShapeExtensions.addCommentFigure(comment, commentText); + _linguaFrancaStyleExtensions.commentStyle(commentFigure); + + // connect + KEdge edge = _kEdgeExtensions.createEdge(); + edge.setSource(comment); + edge.setTarget(targetNode); + _linguaFrancaStyleExtensions.commentStyle(_linguaFrancaShapeExtensions.addCommentPolyline(edge)); + + return List.of(comment); + } + } + return List.of(); + } + +} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.xtend deleted file mode 100644 index 014aa69ef6..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.xtend +++ /dev/null @@ -1,1103 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis - -import com.google.common.collect.HashBasedTable -import com.google.common.collect.HashMultimap -import com.google.common.collect.Table -import de.cau.cs.kieler.klighd.DisplayedActionData -import de.cau.cs.kieler.klighd.SynthesisOption -import de.cau.cs.kieler.klighd.kgraph.KEdge -import de.cau.cs.kieler.klighd.kgraph.KNode -import de.cau.cs.kieler.klighd.kgraph.KPort -import de.cau.cs.kieler.klighd.krendering.Colors -import de.cau.cs.kieler.klighd.krendering.HorizontalAlignment -import de.cau.cs.kieler.klighd.krendering.KContainerRendering -import de.cau.cs.kieler.klighd.krendering.KRendering -import de.cau.cs.kieler.klighd.krendering.LineCap -import de.cau.cs.kieler.klighd.krendering.LineStyle -import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared -import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KEdgeExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KLabelExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KNodeExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KPolylineExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KPortExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions -import de.cau.cs.kieler.klighd.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 -import org.eclipse.elk.alg.layered.options.EdgeStraighteningStrategy -import org.eclipse.elk.alg.layered.options.FixedAlignment -import org.eclipse.elk.alg.layered.options.LayerConstraint -import org.eclipse.elk.alg.layered.options.LayeredOptions -import org.eclipse.elk.core.math.ElkMargin -import org.eclipse.elk.core.math.ElkPadding -import org.eclipse.elk.core.math.KVector -import org.eclipse.elk.core.options.BoxLayouterOptions -import org.eclipse.elk.core.options.CoreOptions -import org.eclipse.elk.core.options.Direction -import org.eclipse.elk.core.options.PortConstraints -import org.eclipse.elk.core.options.PortSide -import org.eclipse.elk.core.options.SizeConstraint -import org.eclipse.elk.graph.properties.Property -import org.eclipse.emf.ecore.EObject -import org.lflang.ASTUtils -import org.lflang.FileConfig -import org.lflang.diagram.synthesis.action.CollapseAllReactorsAction -import org.lflang.diagram.synthesis.action.ExpandAllReactorsAction -import org.lflang.diagram.synthesis.action.FilterCycleAction -import org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction -import org.lflang.diagram.synthesis.action.ShowCycleAction -import org.lflang.diagram.synthesis.styles.LinguaFrancaShapeExtensions -import org.lflang.diagram.synthesis.styles.LinguaFrancaStyleExtensions -import org.lflang.diagram.synthesis.styles.ReactorFigureComponents -import org.lflang.diagram.synthesis.util.CycleVisualization -import org.lflang.diagram.synthesis.util.InterfaceDependenciesVisualization -import org.lflang.diagram.synthesis.util.ReactorIcons -import org.lflang.diagram.synthesis.util.SynthesisErrorReporter -import org.lflang.diagram.synthesis.util.UtilityExtensions -import org.lflang.generator.ActionInstance -import org.lflang.generator.ParameterInstance -import org.lflang.generator.PortInstance -import org.lflang.generator.ReactionInstance -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.Model - -import static extension org.eclipse.emf.ecore.util.EcoreUtil.* -import static extension org.lflang.ASTUtils.* -import static extension org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction.* -import static extension org.lflang.diagram.synthesis.util.NamedInstanceUtil.* - -/** - * Diagram synthesis for Lingua Franca programs. - * - * @author{Alexander Schulz-Rosengarten } - */ -@ViewSynthesisShared -class LinguaFrancaSynthesis extends AbstractDiagramSynthesis { - - @Inject extension KNodeExtensions - @Inject extension KEdgeExtensions - @Inject extension KPortExtensions - @Inject extension KLabelExtensions - @Inject extension KRenderingExtensions - @Inject extension KContainerRenderingExtensions - @Inject extension KPolylineExtensions - @Inject extension LinguaFrancaStyleExtensions - @Inject extension LinguaFrancaShapeExtensions - @Inject extension UtilityExtensions - @Inject extension CycleVisualization - @Inject extension InterfaceDependenciesVisualization - @Inject extension FilterCycleAction - @Inject extension ReactorIcons - - // ------------------------------------------------------------------------- - - public static val ID = "org.lflang.diagram.synthesis.LinguaFrancaSynthesis" - - // -- INTERNAL -- - public static val REACTOR_RECURSIVE_INSTANTIATION = new Property("org.lflang.linguafranca.diagram.synthesis.reactor.recursive.instantiation", false) - public static val REACTOR_HAS_BANK_PORT_OFFSET = new Property("org.lflang.linguafranca.diagram.synthesis.reactor.bank.offset", false) - public static val REACTOR_INPUT = new Property("org.lflang.linguafranca.diagram.synthesis.reactor.input", false) - public static val REACTOR_OUTPUT = new Property("org.lflang.linguafranca.diagram.synthesis.reactor.output", false) - - // -- STYLE -- - public static val ALTERNATIVE_DASH_PATTERN = #[3.0f] - - // -- TEXT -- - public static val TEXT_ERROR_RECURSIVE = "Recursive reactor instantiation!" - public static val TEXT_ERROR_CONTAINS_RECURSION = "Reactor contains recursive instantiation!" - public static val TEXT_ERROR_CONTAINS_CYCLE = "Reactor contains cyclic dependencies!" - public static val TEXT_ERROR_CYCLE_DETECTION = "Dependency cycle detection failed.\nCould not detect dependency cycles due to unexpected graph structure." - public static val TEXT_ERROR_CYCLE_BTN_SHOW = "Show Cycle" - public static val TEXT_ERROR_CYCLE_BTN_FILTER = "Filter Cycle" - public static val TEXT_ERROR_CYCLE_BTN_UNFILTER = "Remove Cycle Filter" - public static val TEXT_NO_MAIN_REACTOR = "No Main Reactor" - public static val TEXT_REACTOR_NULL = "Reactor is null" - public static val TEXT_HIDE_ACTION = "[Hide]" - public static val TEXT_SHOW_ACTION = "[Details]" - - // ------------------------------------------------------------------------- - - /** Synthesis category */ - public static val SynthesisOption APPEARANCE = SynthesisOption.createCategory("Appearance", true) - public static val SynthesisOption EXPERIMENTAL = SynthesisOption.createCategory("Experimental", true) - - /** Synthesis options */ - public static val SynthesisOption SHOW_ALL_REACTORS = SynthesisOption.createCheckOption("All Reactors", false) - public static val SynthesisOption CYCLE_DETECTION = SynthesisOption.createCheckOption("Dependency Cycle Detection", true) - - public static val SynthesisOption SHOW_USER_LABELS = SynthesisOption.createCheckOption("User Labels (@label in JavaDoc)", true).setCategory(APPEARANCE) - public static val SynthesisOption SHOW_HYPERLINKS = SynthesisOption.createCheckOption("Expand/Collapse Hyperlinks", false).setCategory(APPEARANCE) - public static val SynthesisOption REACTIONS_USE_HYPEREDGES = SynthesisOption.createCheckOption("Bundled Dependencies", false).setCategory(APPEARANCE) - public static val SynthesisOption USE_ALTERNATIVE_DASH_PATTERN = SynthesisOption.createCheckOption("Alternative Dependency Line Style", false).setCategory(APPEARANCE) - public static val SynthesisOption SHOW_PORT_NAMES = SynthesisOption.createCheckOption("Port names", true).setCategory(APPEARANCE) - public static val SynthesisOption SHOW_MULTIPORT_WIDTH = SynthesisOption.createCheckOption("Multiport Widths", false).setCategory(APPEARANCE) - public static val SynthesisOption SHOW_REACTION_CODE = SynthesisOption.createCheckOption("Reaction Code", false).setCategory(APPEARANCE) - public static val SynthesisOption SHOW_REACTION_LEVEL = SynthesisOption.createCheckOption("Reaction Level", false).setCategory(APPEARANCE) - public static val SynthesisOption SHOW_REACTION_ORDER_EDGES = SynthesisOption.createCheckOption("Reaction Order Edges", false).setCategory(APPEARANCE) - public static val SynthesisOption SHOW_REACTOR_HOST = SynthesisOption.createCheckOption("Reactor Host Addresses", true).setCategory(APPEARANCE) - public static val SynthesisOption SHOW_INSTANCE_NAMES = SynthesisOption.createCheckOption("Reactor Instance Names", false).setCategory(APPEARANCE) - public static val SynthesisOption REACTOR_PARAMETER_MODE = SynthesisOption.createChoiceOption("Reactor Parameters", ReactorParameterDisplayModes.values, ReactorParameterDisplayModes.NONE).setCategory(APPEARANCE) - public static val SynthesisOption REACTOR_PARAMETER_TABLE_COLS = SynthesisOption.createRangeOption("Reactor Parameter Table Columns", 1, 10, 1).setCategory(APPEARANCE) - - /** Synthesis actions */ - public static val DisplayedActionData COLLAPSE_ALL = DisplayedActionData.create(CollapseAllReactorsAction.ID, "Hide all Details") - public static val DisplayedActionData EXPAND_ALL = DisplayedActionData.create(ExpandAllReactorsAction.ID, "Show all Details") - - override getDisplayedSynthesisOptions() { - return #[ - SHOW_ALL_REACTORS, - MEMORIZE_EXPANSION_STATES, - CYCLE_DETECTION, - SHOW_USER_LABELS, - SHOW_HYPERLINKS, - //LinguaFrancaSynthesisInterfaceDependencies.SHOW_INTERFACE_DEPENDENCIES, - REACTIONS_USE_HYPEREDGES, - USE_ALTERNATIVE_DASH_PATTERN, - SHOW_PORT_NAMES, - SHOW_MULTIPORT_WIDTH, - SHOW_REACTION_CODE, - SHOW_REACTION_LEVEL, - SHOW_REACTION_ORDER_EDGES, - SHOW_REACTOR_HOST, - SHOW_INSTANCE_NAMES, - REACTOR_PARAMETER_MODE, - REACTOR_PARAMETER_TABLE_COLS - ] - } - - override getDisplayedActions() { - return #[COLLAPSE_ALL, EXPAND_ALL] - } - - // ------------------------------------------------------------------------- - - override KNode transform(Model model) { - val rootNode = createNode() - - try { - // Find main - val main = model.reactors.findFirst[isMainOrFederated] - if (main !== null) { - val reactorInstance = new ReactorInstance(main, new SynthesisErrorReporter()) - rootNode.children += reactorInstance.createReactorNode(true, null, null, newHashMap) - } else { - val messageNode = createNode() - messageNode.addErrorMessage(TEXT_NO_MAIN_REACTOR, null) - rootNode.children += messageNode - } - - // Show all reactors - if (main === null || SHOW_ALL_REACTORS.booleanValue) { - val reactorNodes = newArrayList() - for (reactor : model.reactors.filter[it !== main]) { - val reactorInstance = new ReactorInstance(reactor, new SynthesisErrorReporter(), emptySet) - reactorNodes += reactorInstance.createReactorNode(main === null, HashBasedTable.create, HashBasedTable.create, newHashMap) - } - if (!reactorNodes.empty) { - // To allow ordering, we need box layout but we also need layered layout for ports thus wrap all node - // TODO use rect packing in the future - reactorNodes.add(0, rootNode.children.head) - for (entry : reactorNodes.filter[!getProperty(CoreOptions.COMMENT_BOX)].indexed) { - rootNode.children += createNode() => [ - val node = entry.value - children += node - // Add comment nodes - children += node.incomingEdges.filter[source.getProperty(CoreOptions.COMMENT_BOX)].map[source] - - addInvisibleContainerRendering - setLayoutOption(CoreOptions.ALGORITHM, LayeredOptions.ALGORITHM_ID) - setLayoutOption(CoreOptions.PADDING, new ElkPadding(0)) - setLayoutOption(CoreOptions.PRIORITY, reactorNodes.size - entry.key) // Order! - ] - } - - rootNode.setLayoutOption(CoreOptions.ALGORITHM, BoxLayouterOptions.ALGORITHM_ID) - rootNode.setLayoutOption(CoreOptions.SPACING_NODE_NODE, 25.0) - } - } - } catch (Exception e) { - e.printStackTrace - - val messageNode = createNode() - messageNode.addErrorMessage("Error in Diagram Synthesis", e.class.simpleName + " occurred. Could not create diagram.") - rootNode.children += messageNode - } - - return rootNode - } - - private def Collection createReactorNode( - ReactorInstance reactorInstance, - boolean expandDefault, - Table inputPortsReg, - Table outputPortsReg, - Map allReactorNodes - ) { - val reactor = reactorInstance.reactorDefinition - - val node = createNode() - allReactorNodes.put(reactorInstance, node) - node.associateWith(reactor) - node.ID = reactorInstance.uniqueID - node.linkInstance(reactorInstance) // save to distinguish nodes associated with the same reactor - - val nodes = newArrayList(node) - val label = reactorInstance.createReactorLabel() - - if (reactorInstance.recursive) { - // Mark this node - node.setProperty(REACTOR_RECURSIVE_INSTANTIATION, true) - // Mark root - allReactorNodes.get(reactorInstance.root()).setProperty(REACTOR_RECURSIVE_INSTANTIATION, true) - } - - if (reactor === null) { - node.addErrorMessage(TEXT_REACTOR_NULL, null) - } else if (reactorInstance.mainOrFederated) { - val figure = node.addMainReactorFigure(reactorInstance, label) - - if (REACTOR_PARAMETER_MODE.objectValue === ReactorParameterDisplayModes.TABLE - && !reactorInstance.parameters.empty - ) { - figure.addRectangle() => [ - invisible = true - setGridPlacementData().from(LEFT, 8, 0, TOP, 0, 0).to(RIGHT, 8, 0, BOTTOM, 4, 0) - horizontalAlignment = HorizontalAlignment.LEFT - - addParameterList(reactorInstance.parameters) - ] - } - - if (reactorInstance.recursive) { - nodes += node.addErrorComment(TEXT_ERROR_RECURSIVE) - figure.errorStyle() - } else { - figure.addChildArea() - node.children += reactorInstance.transformReactorNetwork(emptyMap, emptyMap, allReactorNodes) - } - - nodes += reactor.createUserComments(node) - - node.configureReactorNodeLayout() - - // Additional layout adjustment for main node - node.setLayoutOption(CoreOptions.ALGORITHM, LayeredOptions.ALGORITHM_ID) - node.setLayoutOption(CoreOptions.DIRECTION, Direction.RIGHT) - node.setLayoutOption(CoreOptions.NODE_SIZE_CONSTRAINTS, EnumSet.of(SizeConstraint.MINIMUM_SIZE)) - node.setLayoutOption(LayeredOptions.NODE_PLACEMENT_BK_FIXED_ALIGNMENT, FixedAlignment.BALANCED) - node.setLayoutOption(LayeredOptions.NODE_PLACEMENT_BK_EDGE_STRAIGHTENING, EdgeStraighteningStrategy.IMPROVE_STRAIGHTNESS) - node.setLayoutOption(LayeredOptions.SPACING_EDGE_NODE, LayeredOptions.SPACING_EDGE_NODE.^default * 1.1f) - node.setLayoutOption(LayeredOptions.SPACING_EDGE_NODE_BETWEEN_LAYERS, LayeredOptions.SPACING_EDGE_NODE_BETWEEN_LAYERS.^default * 1.1f) - if (!SHOW_HYPERLINKS.booleanValue) { - node.setLayoutOption(CoreOptions.PADDING, new ElkPadding(-1, 6, 6, 6)) - node.setLayoutOption(LayeredOptions.SPACING_COMPONENT_COMPONENT, LayeredOptions.SPACING_COMPONENT_COMPONENT.^default * 0.5f) - } - } else { - val instance = reactorInstance - - // Expanded Rectangle - node.addReactorFigure(reactorInstance, label) => [ ReactorFigureComponents comps | - comps.figures.forEach[associateWith(reactor)] - comps.outer.setProperty(KlighdProperties.EXPANDED_RENDERING, true) - comps.figures.forEach[addDoubleClickAction(MemorizingExpandCollapseAction.ID)] - comps.reactor.handleIcon(reactor, false) - - if (SHOW_HYPERLINKS.booleanValue) { - // Collapse button - comps.reactor.addTextButton(TEXT_HIDE_ACTION) => [ - setGridPlacementData().from(LEFT, 8, 0, TOP, 0, 0).to(RIGHT, 8, 0, BOTTOM, 0, 0) - addSingleClickAction(MemorizingExpandCollapseAction.ID) - addDoubleClickAction(MemorizingExpandCollapseAction.ID) - ] - } - - if (REACTOR_PARAMETER_MODE.objectValue === ReactorParameterDisplayModes.TABLE - && !instance.parameters.empty - ) { - comps.reactor.addRectangle() => [ - invisible = true - if (!SHOW_HYPERLINKS.booleanValue) { - setGridPlacementData().from(LEFT, 8, 0, TOP, 0, 0).to(RIGHT, 8, 0, BOTTOM, 4, 0) - } else { - setGridPlacementData().from(LEFT, 8, 0, TOP, 4, 0).to(RIGHT, 8, 0, BOTTOM, 0, 0) - } - horizontalAlignment = HorizontalAlignment.LEFT - - addParameterList(instance.parameters) - ] - } - - if (instance.recursive) { - comps.figures.forEach[errorStyle()] - } else { - comps.reactor.addChildArea() - } - ] - - // Collapse Rectangle - node.addReactorFigure(reactorInstance, label) => [ ReactorFigureComponents comps | - comps.figures.forEach[associateWith(reactor)] - comps.outer.setProperty(KlighdProperties.COLLAPSED_RENDERING, true) - if (instance.hasContent && !instance.recursive) { - comps.figures.forEach[addDoubleClickAction(MemorizingExpandCollapseAction.ID)] - } - comps.reactor.handleIcon(reactor, true) - - if (SHOW_HYPERLINKS.booleanValue) { - // Expand button - if (instance.hasContent && !instance.recursive) { - comps.reactor.addTextButton(TEXT_SHOW_ACTION) => [ - setGridPlacementData().from(LEFT, 8, 0, TOP, 0, 0).to(RIGHT, 8, 0, BOTTOM, 8, 0) - addSingleClickAction(MemorizingExpandCollapseAction.ID) - addDoubleClickAction(MemorizingExpandCollapseAction.ID) - ] - } - } - - if (instance.recursive) { - comps.figures.forEach[errorStyle()] - } - ] - - // Create ports - val inputPorts = newHashMap - val outputPorts = newHashMap - for (input : instance.inputs.reverseView) { - inputPorts.put(input, node.addIOPort(input, true, input.isMultiport(), reactorInstance.isBank())) - } - for (output : instance.outputs) { - outputPorts.put(output, node.addIOPort(output, false, output.isMultiport(), reactorInstance.isBank())) - } - // Mark ports - inputPorts.values.forEach[setProperty(REACTOR_INPUT, true)] - outputPorts.values.forEach[setProperty(REACTOR_OUTPUT, true)] - - // Add content - if (instance.hasContent && !instance.recursive) { - node.children += instance.transformReactorNetwork(inputPorts, outputPorts, allReactorNodes) - } - - // Pass port to given tables - if (!instance.isRoot) { - if (inputPortsReg !== null) { - for (entry : inputPorts.entrySet) { - inputPortsReg.put(instance, entry.key, entry.value) - } - } - if (outputPortsReg !== null) { - for (entry : outputPorts.entrySet) { - outputPortsReg.put(instance, entry.key, entry.value) - } - } - } - - if (instance.recursive) { - node.setLayoutOption(KlighdProperties.EXPAND, false) - nodes += node.addErrorComment(TEXT_ERROR_RECURSIVE) - } else { - node.setLayoutOption(KlighdProperties.EXPAND, expandDefault) - - // Interface Dependencies - node.addInterfaceDependencies(expandDefault) - } - - if (!instance.isRoot) { - // If all reactors are being shown, then only put the label on - // the reactor definition, not on its instances. Otherwise, - // add the annotation now. - if (!SHOW_ALL_REACTORS.booleanValue) { - nodes += reactor.createUserComments(node) - } - } else { - nodes += reactor.createUserComments(node) - } - - node.configureReactorNodeLayout() - } - - // Find and annotate cycles - if (CYCLE_DETECTION.booleanValue && reactorInstance.isRoot) { - val errNode = node.detectAndAnnotateCycles(reactorInstance, allReactorNodes) - if (errNode !== null) { - nodes += errNode - } - } - - return nodes - } - - private def configureReactorNodeLayout(KNode node) { - node.setLayoutOption(CoreOptions.NODE_SIZE_CONSTRAINTS, SizeConstraint.minimumSizeWithPorts) - node.setLayoutOption(CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_ORDER) - node.setLayoutOption(LayeredOptions.CROSSING_MINIMIZATION_SEMI_INTERACTIVE, true) - if (!SHOW_HYPERLINKS.booleanValue) { - node.setLayoutOption(CoreOptions.PADDING, new ElkPadding(2, 6, 6, 6)) - node.setLayoutOption(LayeredOptions.SPACING_NODE_NODE, LayeredOptions.SPACING_NODE_NODE.^default * 0.75f) - node.setLayoutOption(LayeredOptions.SPACING_NODE_NODE_BETWEEN_LAYERS, LayeredOptions.SPACING_NODE_NODE_BETWEEN_LAYERS.^default * 0.75f) - node.setLayoutOption(LayeredOptions.SPACING_EDGE_NODE, LayeredOptions.SPACING_EDGE_NODE.^default * 0.75f) - node.setLayoutOption(LayeredOptions.SPACING_EDGE_NODE_BETWEEN_LAYERS, LayeredOptions.SPACING_EDGE_NODE_BETWEEN_LAYERS.^default * 0.75f) - } - } - - private def KNode detectAndAnnotateCycles(KNode node, ReactorInstance reactorInstance, Map allReactorNodes) { - if (node.getProperty(REACTOR_RECURSIVE_INSTANTIATION)) { - node.resetCycleFiltering() - return node.addErrorComment(TEXT_ERROR_CONTAINS_RECURSION) - } else { // only detect dependency cycles if not recursive - try { - val hasCycle = reactorInstance.detectAndHighlightCycles(allReactorNodes, [ - if (it instanceof KNode) { - val renderings = it.data.filter(typeof(KRendering)).toList - if (renderings.size === 1) { - renderings.head.errorStyle() - } else { - renderings.filter[getProperty(KlighdProperties.COLLAPSED_RENDERING)].forEach[errorStyle()] - } - } else if (it instanceof KEdge) { - it.data.filter(typeof(KRendering)).forEach[errorStyle()] - // TODO initiallyHide does not work with incremental (https://github.com/kieler/KLighD/issues/37) - // cycleEgde.initiallyShow() // Show hidden order dependencies - it.KRendering.invisible = false - } else if (it instanceof KPort) { - it.data.filter(typeof(KRendering)).forEach[errorStyle()] - //it.reverseTrianglePort() - } - ]) - - if (hasCycle) { - val err = node.addErrorComment(TEXT_ERROR_CONTAINS_CYCLE) - err.KContainerRendering.addRectangle() => [ // Add to existing figure - setGridPlacementData().from(LEFT, 3, 0, TOP, -1, 0).to(RIGHT, 3, 0, BOTTOM, 3, 0) - noSelectionStyle() - invisible = true - gridPlacement = 2 - - addRectangle() => [ - setGridPlacementData().from(LEFT, 0, 0, TOP, 0, 0).to(RIGHT, 2, 0, BOTTOM, 0, 0) - noSelectionStyle() - addSingleClickAction(ShowCycleAction.ID) - addText(TEXT_ERROR_CYCLE_BTN_SHOW) => [ - styles += err.KContainerRendering.children.head.styles.map[copy] // Copy text style - fontSize = 5 - setSurroundingSpace(1, 0) - noSelectionStyle() - addSingleClickAction(ShowCycleAction.ID) - ] - ] - addRectangle() => [ - setGridPlacementData().from(LEFT, 0, 0, TOP, 0, 0).to(RIGHT, 0, 0, BOTTOM, 0, 0) - noSelectionStyle() - addSingleClickAction(FilterCycleAction.ID) - addText(node.isCycleFiltered() ? TEXT_ERROR_CYCLE_BTN_UNFILTER : TEXT_ERROR_CYCLE_BTN_FILTER) => [ - styles += err.KContainerRendering.children.head.styles.map[copy] // Copy text style - fontSize = 5 - setSurroundingSpace(1, 0) - noSelectionStyle() - addSingleClickAction(FilterCycleAction.ID) - markCycleFilterText(err) - ] - ] - ] - - // if user interactively requested a filtered diagram keep it filtered during updates - if (node.isCycleFiltered()) { - node.filterCycle() - } - - return err - } - } catch(Exception e) { - node.resetCycleFiltering() - e.printStackTrace() - return node.addErrorComment(TEXT_ERROR_CYCLE_DETECTION) - } - } - return null - } - - private def Collection transformReactorNetwork( - ReactorInstance reactorInstance, - Map parentInputPorts, - Map parentOutputPorts, - Map allReactorNodes - ) { - val nodes = newArrayList - val inputPorts = HashBasedTable.create - val outputPorts = HashBasedTable.create - val reactionNodes = newHashMap - val directConnectionDummyNodes = newHashMap - val actionDestinations = HashMultimap.create - val actionSources = HashMultimap.create - val timerNodes = newHashMap - val startupNode = createNode - var startupUsed = false - val shutdownNode = createNode - var shutdownUsed = false - - // Transform instances - for (entry : reactorInstance.children.reverseView.indexed) { - val child = entry.value - val rNodes = child.createReactorNode(child.getExpansionState?:false, inputPorts, outputPorts, allReactorNodes) - rNodes.head.setLayoutOption(CoreOptions.PRIORITY, entry.key) - nodes += rNodes - } - - // Create timers - for (timer : reactorInstance.timers) { - val node = createNode().associateWith(timer.definition) - node.linkInstance(timer) - nodes += node - nodes += timer.definition.createUserComments(node) - timerNodes.put(timer, node) - - node.addTimerFigure(timer) - } - - // Create reactions - for (reaction : reactorInstance.reactions.reverseView) { - val idx = reactorInstance.reactions.indexOf(reaction) - val node = createNode().associateWith(reaction.definition) - node.linkInstance(reaction) - nodes += node - nodes += reaction.definition.createUserComments(node) - reactionNodes.put(reaction, node) - - node.setLayoutOption(CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_SIDE) - node.setLayoutOption(CoreOptions.PRIORITY, (reactorInstance.reactions.size - idx) * 10 ) // always place with higher priority than reactor nodes - node.setLayoutOption(LayeredOptions.POSITION, new KVector(0, idx)) // try order reactions vertically if in one layer - - node.addReactionFigure(reaction) - - // connect input - var KPort port - for (TriggerInstance trigger : reaction.triggers) { - port = if (REACTIONS_USE_HYPEREDGES.booleanValue && port !== null) { - port - } else { - node.addInvisiblePort() => [ - setLayoutOption(CoreOptions.PORT_SIDE, PortSide.WEST) - if (REACTIONS_USE_HYPEREDGES.booleanValue || ((reaction.triggers?:emptyList).size + (reaction.sources?:emptyList).size) == 1) { - setLayoutOption(CoreOptions.PORT_BORDER_OFFSET, -LinguaFrancaShapeExtensions.REACTION_POINTINESS as double) // manual adjustment disabling automatic one - } - ] - } - - if (trigger.startup) { - createDependencyEdge((trigger.definition as BuiltinTriggerVariable).definition).connect(startupNode, port) - startupUsed = true - } else if (trigger.shutdown) { - createDelayEdge((trigger.definition as BuiltinTriggerVariable).definition).connect(shutdownNode, port) - shutdownUsed = true - } else if (trigger instanceof ActionInstance) { - actionDestinations.put(trigger, port) - } else if (trigger instanceof PortInstance) { - var KPort src = null - if (trigger.parent === reactorInstance) { - src = parentInputPorts.get(trigger) - } else { - src = outputPorts.get(trigger.parent, trigger) - } - if (src !== null) { - createDependencyEdge(trigger.definition).connect(src, port) - } - } else if (trigger instanceof TimerInstance) { - val src = timerNodes.get(trigger) - if (src !== null) { - createDependencyEdge(trigger.definition).connect(src, port) - } - } - } - - // connect dependencies - //port = null // create new ports - for (TriggerInstance dep : reaction.sources.filter[!reaction.triggers.contains(it)]) { - port = if (REACTIONS_USE_HYPEREDGES.booleanValue && port !== null) { - port - } else { - node.addInvisiblePort() => [ - //setLayoutOption(CoreOptions.PORT_SIDE, PortSide.NORTH) - setLayoutOption(CoreOptions.PORT_SIDE, PortSide.WEST) - if (REACTIONS_USE_HYPEREDGES.booleanValue || ((reaction.triggers?:emptyList).size + (reaction.sources?:emptyList).size) == 1) { - setLayoutOption(CoreOptions.PORT_BORDER_OFFSET, -LinguaFrancaShapeExtensions.REACTION_POINTINESS as double) // manual adjustment disabling automatic one - } - ] - } - if (dep instanceof PortInstance) { - var KPort src = null - if (dep.parent === reactorInstance) { - src = parentInputPorts.get(dep) - } else { - src = outputPorts.get(dep.parent, dep) - } - if (src !== null) { - createDependencyEdge(dep).connect(src, port) - } - } - } - - // connect outputs - port = null // create new ports - for (TriggerInstance effect : reaction.effects?:emptyList) { - 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 { - dst = inputPorts.get(effect.parent, effect) - } - if (dst !== null) { - createDependencyEdge(effect).connect(port, dst) - } - } - } - } - - // Connect actions - val actions = newHashSet - actions += actionSources.keySet - actions += actionDestinations.keySet - for (ActionInstance action : actions) { - val node = createNode().associateWith(action.definition) - node.linkInstance(action) - nodes += node - nodes += action.definition.createUserComments(node) - - node.setLayoutOption(CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_SIDE) - - val ports = node.addActionFigureAndPorts(action.isPhysical ? "P" : "L") - // TODO handle variables? - if (action.minDelay !== null && action.minDelay !== ActionInstance.DEFAULT_MIN_DELAY) { - node.addOutsideBottomCenteredNodeLabel('''min delay: «action.minDelay.toString»''', 7) - } - // TODO default value? - if (action.definition.minSpacing !== null) { - node.addOutsideBottomCenteredNodeLabel('''min spacing: «action.minSpacing.toString»''', 7) - } - if (!action.definition.policy.isNullOrEmpty) { - node.addOutsideBottomCenteredNodeLabel('''policy: «action.policy»''', 7) - } - - // connect source - for (source : actionSources.get(action)) { - createDelayEdge(action).connect(source, ports.key) - } - - // connect targets - for (target : actionDestinations.get(action)) { - createDelayEdge(action).connect(ports.value, target) - } - } - - // Transform connections. - // 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) - } - // 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 { - edge.connect(source, target) - } - } - } - } - } - } - - // Add startup/shutdown - if (startupUsed) { - startupNode.addStartupFigure - nodes.add(0, startupNode) - startupNode.setLayoutOption(LayeredOptions.LAYERING_LAYER_CONSTRAINT, LayerConstraint.FIRST) - if (REACTIONS_USE_HYPEREDGES.booleanValue) { // connect all edges to one port - val port = startupNode.addInvisiblePort - startupNode.outgoingEdges.forEach[sourcePort = port] - } - } - if (shutdownUsed) { - shutdownNode.addShutdownFigure - nodes.add(0, shutdownNode) - if (REACTIONS_USE_HYPEREDGES.booleanValue) { // connect all edges to one port - val port = shutdownNode.addInvisiblePort - shutdownNode.outgoingEdges.forEach[sourcePort = port] - } - } - - // Postprocess timer nodes - if (REACTIONS_USE_HYPEREDGES.booleanValue) { // connect all edges to one port - for (timerNode : timerNodes.values) { - val port = timerNode.addInvisiblePort - timerNode.outgoingEdges.forEach[sourcePort = port] - } - } - - // Add reaction order edges (add last to have them on top of other edges) - if (reactorInstance.reactions.size > 1) { - var prevNode = reactionNodes.get(reactorInstance.reactions.head) - for (node : reactorInstance.reactions.drop(1).map[reactionNodes.get(it)]) { - val edge = createOrderEdge() - edge.source = prevNode - edge.target = node - edge.setProperty(CoreOptions.NO_LAYOUT, true) - - // Do not remove them, as they are needed for cycle detection - edge.KRendering.invisible = !SHOW_REACTION_ORDER_EDGES.booleanValue - edge.KRendering.invisible.propagateToChildren = true - // TODO this does not work work with incremental update (https://github.com/kieler/KLighD/issues/37) - // if (!SHOW_REACTION_ORDER_EDGES.booleanValue) edge.initiallyHide() - - prevNode = node - } - } - - return nodes - } - - private def String createReactorLabel(ReactorInstance reactorInstance) { - val b = new StringBuilder - if (SHOW_INSTANCE_NAMES.booleanValue && !reactorInstance.isRoot) { - if (!reactorInstance.mainOrFederated) { - b.append(reactorInstance.name).append(" : ") - } - } - if (reactorInstance.mainOrFederated) { - 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.reactorDeclaration.name) - } - if (REACTOR_PARAMETER_MODE.objectValue === ReactorParameterDisplayModes.TITLE) { - if (reactorInstance.parameters.empty) { - b.append("()") - } else { - b.append(reactorInstance.parameters.join("(", ", ", ")") [ - createParameterLabel(false) - ]) - } - } - return b.toString() - } - - private def addParameterList(KContainerRendering container, List parameters) { - var cols = 1 - try { - cols = REACTOR_PARAMETER_TABLE_COLS.intValue - } catch (Exception e) {} // ignore - if (cols > parameters.size) { - cols = parameters.size - } - container.gridPlacement = cols - for (param : parameters) { - container.addText(param.createParameterLabel(true)) => [ - fontSize = 8 - horizontalAlignment = HorizontalAlignment.LEFT - ] - } - } - - private def String createParameterLabel(ParameterInstance param, boolean bullet) { - val b = new StringBuilder - if (bullet) { - b.append("\u2022 ") - } - b.append(param.name) - val t = param.type.toText - if (!t.nullOrEmpty) { - b.append(":").append(t) - } - if (!param.getInitialValue.nullOrEmpty) { - b.append("(").append(param.getInitialValue.join(", ", [it.toText])).append(")") - } - return b.toString() - } - - private def createDelayEdge(Object associate) { - return createEdge => [ - associateWith(associate) - addPolyline() => [ - boldLineSelectionStyle() - addJunctionPointDecorator() - if (USE_ALTERNATIVE_DASH_PATTERN.booleanValue) { - lineStyle = LineStyle.CUSTOM - lineStyle.dashPattern += ALTERNATIVE_DASH_PATTERN - } else { - lineStyle = LineStyle.DASH - } - ] - ] - } - - private def createIODependencyEdge(Object associate, boolean multiport) { - return createEdge => [ - if (associate !== null) { - associateWith(associate) - } - addPolyline() => [ - boldLineSelectionStyle() - addJunctionPointDecorator() - if (multiport) { - // Render multiport connections and bank connections in bold. - lineWidth = 2.2f - lineCap = LineCap.CAP_SQUARE - // Adjust junction point size - setJunctionPointDecorator(it.junctionPointRendering, 6, 6) - } - ] - ] - } - - private def createDependencyEdge(Object associate) { - return createEdge => [ - if (associate !== null) { - associateWith(associate) - } - addPolyline() => [ - boldLineSelectionStyle() - addJunctionPointDecorator() - if (USE_ALTERNATIVE_DASH_PATTERN.booleanValue) { - lineStyle = LineStyle.CUSTOM - lineStyle.dashPattern += ALTERNATIVE_DASH_PATTERN - } else { - lineStyle = LineStyle.DASH - } - ] - ] - } - - private def createOrderEdge() { - return createEdge => [ - addPolyline() => [ - lineWidth = 1.5f - lineStyle = LineStyle.DOT - foreground = Colors.CHOCOLATE_1 - boldLineSelectionStyle() - //addFixedTailArrowDecorator() // Fix for bug: https://github.com/kieler/KLighD/issues/38 - addHeadArrowDecorator() - ] - ] - } - - private def dispatch KEdge connect(KEdge edge, KNode src, KNode dst) { - edge.source = src - edge.target = dst - - return edge - } - private def dispatch KEdge connect(KEdge edge, KNode src, KPort dst) { - edge.source = src - edge.targetPort = dst - edge.target = dst?.node - - return edge - } - private def dispatch KEdge connect(KEdge edge, KPort src, KNode dst) { - edge.sourcePort = src - edge.source = src?.node - edge.target = dst - - return edge - } - private def dispatch KEdge connect(KEdge edge, KPort src, KPort dst) { - edge.sourcePort = src - edge.source = src?.node - edge.targetPort = dst - edge.target = dst?.node - - return edge - } - - /** - * Translate an input/output into a port. - */ - private def addIOPort(KNode node, PortInstance lfPort, boolean input, boolean multiport, boolean bank) { - val port = createPort - node.ports += port - - port.associateWith(lfPort.definition) - port.linkInstance(lfPort) - port.setPortSize(6, 6) - - if (input) { - // multiports are smaller by an offset at the right, hence compensate in inputs - val offset = multiport ? -3.4 : -3.3 - port.setLayoutOption(CoreOptions.PORT_SIDE, PortSide.WEST) - port.setLayoutOption(CoreOptions.PORT_BORDER_OFFSET, offset) - } else { - var offset = (multiport ? -2.6 : -3.3) // multiports are smaller - offset = bank ? offset - LinguaFrancaShapeExtensions.BANK_FIGURE_X_OFFSET_SUM : offset // compensate bank figure width - port.setLayoutOption(CoreOptions.PORT_SIDE, PortSide.EAST) - port.setLayoutOption(CoreOptions.PORT_BORDER_OFFSET, offset) - } - - if (bank && !node.getProperty(REACTOR_HAS_BANK_PORT_OFFSET)) {// compensate bank figure height - // https://github.com/eclipse/elk/issues/693 - node.getPortMarginsInitIfAbsent().add(new ElkMargin(0, 0, LinguaFrancaShapeExtensions.BANK_FIGURE_Y_OFFSET_SUM, 0)) - node.setProperty(REACTOR_HAS_BANK_PORT_OFFSET, true) // only once - } - - port.addTrianglePort(multiport) - - var label = lfPort.name - if (!SHOW_PORT_NAMES.booleanValue) { - label = "" - } - if (SHOW_MULTIPORT_WIDTH.booleanValue) { - if (lfPort.isMultiport) { - label += (lfPort.width >= 0)? - "[" + lfPort.width + "]" - : "[?]" - } - } - port.addOutsidePortLabel(label, 8).associateWith(lfPort.definition) - - return port - } - - private def KPort addInvisiblePort(KNode node) { - val port = createPort - node.ports += port - - port.setSize(0, 0) // invisible - - return port - } - - private def KNode addErrorComment(KNode node, String message) { - val comment = createNode() - comment.setLayoutOption(CoreOptions.COMMENT_BOX, true) - comment.addCommentFigure(message) => [ - errorStyle() - background = Colors.PEACH_PUFF_2 - ] - - // connect - createEdge() => [ - source = comment - target = node - addCommentPolyline().errorStyle() - ] - - return comment - } - - private def Iterable createUserComments(EObject element, KNode targetNode) { - if (SHOW_USER_LABELS.booleanValue) { - val commentText = ASTUtils.findAnnotationInComments(element, "@label") - - if (!commentText.nullOrEmpty) { - val comment = createNode() - comment.setLayoutOption(CoreOptions.COMMENT_BOX, true) - comment.addCommentFigure(commentText) => [ - commentStyle() - ] - - // connect - createEdge() => [ - source = comment - target = targetNode - addCommentPolyline().commentStyle() - ] - - return #[comment] - } - } - return #[] - } - -} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/SynthesisRegistration.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/SynthesisRegistration.java similarity index 60% rename from org.lflang.diagram/src/org/lflang/diagram/synthesis/SynthesisRegistration.xtend rename to org.lflang.diagram/src/org/lflang/diagram/synthesis/SynthesisRegistration.java index 7e05df4a65..480c2b27b9 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/SynthesisRegistration.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/SynthesisRegistration.java @@ -1,37 +1,37 @@ -package org.lflang.diagram.synthesis +package org.lflang.diagram.synthesis; -import de.cau.cs.kieler.klighd.IKlighdStartupHook -import de.cau.cs.kieler.klighd.KlighdDataManager -import org.lflang.diagram.synthesis.LinguaFrancaSynthesis -import org.lflang.diagram.synthesis.action.CollapseAllReactorsAction -import org.lflang.diagram.synthesis.action.ExpandAllReactorsAction -import org.lflang.diagram.synthesis.action.FilterCycleAction -import org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction -import org.lflang.diagram.synthesis.action.ShowCycleAction -import org.lflang.diagram.synthesis.postprocessor.ReactionPortAdjustment +import de.cau.cs.kieler.klighd.IKlighdStartupHook; +import de.cau.cs.kieler.klighd.KlighdDataManager; +import org.lflang.diagram.synthesis.action.CollapseAllReactorsAction; +import org.lflang.diagram.synthesis.action.ExpandAllReactorsAction; +import org.lflang.diagram.synthesis.action.FilterCycleAction; +import org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction; +import org.lflang.diagram.synthesis.action.ShowCycleAction; +import org.lflang.diagram.synthesis.postprocessor.ReactionPortAdjustment; /** * Registration of all diagram synthesis related classes in Klighd. * * @author{Alexander Schulz-Rosengarten } */ -class SynthesisRegistration implements IKlighdStartupHook { +public class SynthesisRegistration implements IKlighdStartupHook { - override execute() { - val reg = KlighdDataManager.instance + @Override + public void execute() { + KlighdDataManager reg = KlighdDataManager.getInstance(); // Synthesis - reg.registerDiagramSynthesisClass(LinguaFrancaSynthesis.ID, LinguaFrancaSynthesis) + reg.registerDiagramSynthesisClass(LinguaFrancaSynthesis.ID, LinguaFrancaSynthesis.class); // Actions - reg.registerAction(MemorizingExpandCollapseAction.ID, new MemorizingExpandCollapseAction) - reg.registerAction(ExpandAllReactorsAction.ID, new ExpandAllReactorsAction) - reg.registerAction(CollapseAllReactorsAction.ID, new CollapseAllReactorsAction) - reg.registerAction(ShowCycleAction.ID, new ShowCycleAction) - reg.registerAction(FilterCycleAction.ID, new FilterCycleAction) + reg.registerAction(MemorizingExpandCollapseAction.ID, new MemorizingExpandCollapseAction()); + reg.registerAction(ExpandAllReactorsAction.ID, new ExpandAllReactorsAction()); + reg.registerAction(CollapseAllReactorsAction.ID, new CollapseAllReactorsAction()); + reg.registerAction(ShowCycleAction.ID, new ShowCycleAction()); + reg.registerAction(FilterCycleAction.ID, new FilterCycleAction()); // Style Mod - reg.registerStyleModifier(ReactionPortAdjustment.ID, new ReactionPortAdjustment) + reg.registerStyleModifier(ReactionPortAdjustment.ID, new ReactionPortAdjustment()); } } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.java similarity index 74% rename from org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.xtend rename to org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.java index db81e36142..9004e8bca7 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/AbstractAction.java @@ -22,33 +22,30 @@ * (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.diagram.synthesis.action +package org.lflang.diagram.synthesis.action; -import de.cau.cs.kieler.klighd.IAction -import de.cau.cs.kieler.klighd.internal.util.KlighdInternalProperties -import de.cau.cs.kieler.klighd.kgraph.KGraphElement -import de.cau.cs.kieler.klighd.kgraph.KNode -import org.lflang.lf.Reactor +import de.cau.cs.kieler.klighd.IAction; +import de.cau.cs.kieler.klighd.internal.util.KlighdInternalProperties; +import de.cau.cs.kieler.klighd.kgraph.KGraphElement; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import org.lflang.lf.Reactor; /** * Abstract super class for diagram actions that provides some convince methods. * * @author{Alexander Schulz-Rosengarten } */ -abstract class AbstractAction implements IAction { - - def Object sourceElement(KGraphElement elem) { - return elem.getProperty(KlighdInternalProperties.MODEL_ELEMEMT) +public abstract class AbstractAction implements IAction { + public Object sourceElement(final KGraphElement elem) { + return elem.getProperty(KlighdInternalProperties.MODEL_ELEMEMT); } - def boolean sourceIsReactor(KNode node) { - return node.sourceElement() instanceof Reactor + public boolean sourceIsReactor(final KNode node) { + return sourceElement(node) instanceof Reactor; } - def Reactor sourceAsReactor(KNode node) { - if (node.sourceIsReactor()) { - return node.sourceElement() as Reactor - } - return null + public Reactor sourceAsReactor(final KNode node) { + return sourceIsReactor(node) ? (Reactor) sourceElement(node) : null; } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.java similarity index 55% rename from org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.xtend rename to org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.java index f31c01ff74..2981a8437e 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/CollapseAllReactorsAction.java @@ -22,32 +22,41 @@ * (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.diagram.synthesis.action +package org.lflang.diagram.synthesis.action; -import de.cau.cs.kieler.klighd.IAction -import de.cau.cs.kieler.klighd.kgraph.KNode - -import static extension de.cau.cs.kieler.klighd.util.ModelingUtil.* -import static extension org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction.* -import static extension org.lflang.diagram.synthesis.util.NamedInstanceUtil.* +import de.cau.cs.kieler.klighd.IAction; +import de.cau.cs.kieler.klighd.ViewContext; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.util.ModelingUtil; +import java.util.Iterator; +import org.eclipse.xtext.xbase.lib.IteratorExtensions; +import org.lflang.diagram.synthesis.util.NamedInstanceUtil; /** * Action that expands (shows details) of all reactor nodes. * * @author{Alexander Schulz-Rosengarten } */ -class CollapseAllReactorsAction extends AbstractAction { +public class CollapseAllReactorsAction extends AbstractAction { + + public static final String ID = "org.lflang.diagram.synthesis.action.CollapseAllReactorsAction"; - public static val ID = "org.lflang.diagram.synthesis.action.CollapseAllReactorsAction" + @Override + public IAction.ActionResult execute(final IAction.ActionContext context) { + ViewContext vc = context.getViewContext(); + Iterator knodes = ModelingUtil.eAllContentsOfType(vc.getViewModel(), KNode.class); + Iterator knodesSourceIsReactor = IteratorExtensions.filter(knodes, this::sourceIsReactor); - override execute(ActionContext context) { - val vc = context.viewContext - for (node : vc.viewModel.eAllContentsOfType(KNode).filter[sourceIsReactor].toIterable) { - if (!(node.sourceAsReactor().main || node.sourceAsReactor().federated)) { // Do not collapse main reactor - node.setExpansionState(node.linkedInstance, vc.viewer, false) + for (KNode node : IteratorExtensions.toIterable(knodesSourceIsReactor)) { + if (!(sourceAsReactor(node).isMain() || sourceAsReactor(node).isFederated())) { + MemorizingExpandCollapseAction.setExpansionState( + node, + NamedInstanceUtil.getLinkedInstance(node), + vc.getViewer(), + false + ); } } return IAction.ActionResult.createResult(true); } - } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.java similarity index 57% rename from org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.xtend rename to org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.java index e50c889a58..9990ae5220 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ExpandAllReactorsAction.java @@ -22,30 +22,40 @@ * (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.diagram.synthesis.action +package org.lflang.diagram.synthesis.action; -import de.cau.cs.kieler.klighd.IAction -import de.cau.cs.kieler.klighd.kgraph.KNode - -import static extension de.cau.cs.kieler.klighd.util.ModelingUtil.* -import static extension org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction.* -import static extension org.lflang.diagram.synthesis.util.NamedInstanceUtil.* +import de.cau.cs.kieler.klighd.IAction; +import de.cau.cs.kieler.klighd.ViewContext; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.util.ModelingUtil; +import java.util.Iterator; +import org.eclipse.xtext.xbase.lib.IteratorExtensions; +import org.lflang.diagram.synthesis.util.NamedInstanceUtil; /** * Action that collapses (hides details) of all reactor nodes. * * @author{Alexander Schulz-Rosengarten } */ -class ExpandAllReactorsAction extends AbstractAction { - - public static val ID = "org.lflang.diagram.synthesis.action.ExpandAllReactorsAction" +public class ExpandAllReactorsAction extends AbstractAction { + + public static final String ID = "org.lflang.diagram.synthesis.action.ExpandAllReactorsAction"; - override execute(ActionContext context) { - val vc = context.viewContext - for (node : vc.viewModel.eAllContentsOfType(KNode).filter[sourceIsReactor].toIterable) { - node.setExpansionState(node.linkedInstance, vc.viewer, true) + @Override + public IAction.ActionResult execute(final IAction.ActionContext context) { + ViewContext vc = context.getViewContext(); + Iterator knodes = ModelingUtil.eAllContentsOfType(vc.getViewModel(), KNode.class); + Iterator knodesSourceIsReactor = IteratorExtensions.filter(knodes, this::sourceIsReactor); + + for (KNode node : IteratorExtensions.toIterable(knodesSourceIsReactor)) { + MemorizingExpandCollapseAction.setExpansionState( + node, + NamedInstanceUtil.getLinkedInstance(node), + vc.getViewer(), + true + ); } return IAction.ActionResult.createResult(true); } - } + \ No newline at end of file diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/FilterCycleAction.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/FilterCycleAction.java new file mode 100644 index 0000000000..c1394a88f8 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/FilterCycleAction.java @@ -0,0 +1,157 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis.action; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; +import de.cau.cs.kieler.klighd.IAction; +import de.cau.cs.kieler.klighd.ViewContext; +import de.cau.cs.kieler.klighd.kgraph.KEdge; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.krendering.KText; + +import java.util.Iterator; +import java.util.List; +import java.util.WeakHashMap; +import java.util.function.Predicate; +import org.eclipse.elk.graph.properties.Property; +import org.eclipse.xtext.xbase.lib.Functions.Function1; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.eclipse.xtext.xbase.lib.IteratorExtensions; +import org.eclipse.xtext.xbase.lib.ListExtensions; +import org.lflang.diagram.synthesis.LinguaFrancaSynthesis; +import org.lflang.diagram.synthesis.util.CycleVisualization; + +/** + * Action that filters the diagram for only those elements included in a cycle. + * + * @author{Alexander Schulz-Rosengarten } + */ +public class FilterCycleAction extends AbstractAction { + + public static final String ID = "org.lflang.diagram.synthesis.action.FilterCycleAction"; + + /** + * Memory-leak-free cache of filtered states + */ + private static final WeakHashMap FILTERING_STATES = new WeakHashMap<>(); + + /** + * Property to mark filter button + */ + private static final Property FILTER_BUTTON = new Property<>("org.lflang.diagram.synthesis.action.cyclefilter.button", false); + + @Override + public IAction.ActionResult execute(final IAction.ActionContext context) { + ViewContext vc = context.getViewContext(); + Object all = vc.getOptionValue(LinguaFrancaSynthesis.SHOW_ALL_REACTORS); + List nodes = vc.getViewModel().getChildren(); + + if (all instanceof Boolean && (Boolean) all) { + nodes = IterableExtensions.toList( + Iterables.concat( + ListExtensions.map( + nodes, it -> { return it.getChildren(); } + ) + ) + ); + } + + if (IterableExtensions.exists(nodes, this::isCycleFiltered)) { + // undo + nodes.forEach(this::resetCycleFiltering); + + // re-synthesize everything + vc.getViewModel().getChildren().clear(); + vc.update(); + } else { + // filter + nodes.forEach(it -> { + this.markCycleFiltered(it); + this.filterCycle(it); + }); + + Function1 knodeFilterButton = it -> { + return it.getProperty(FILTER_BUTTON); + }; + + Function1 ktextFilterButton = it -> { + return it.getProperty(FILTER_BUTTON); + }; + + // switch filter label + for (KNode node : IterableExtensions.filter(nodes, knodeFilterButton)) { + Iterator ktexts = Iterators.filter(node.eAllContents(), KText.class); + KText text = IteratorExtensions.findFirst(ktexts, ktextFilterButton); + if (text != null) { + text.setText(LinguaFrancaSynthesis.TEXT_ERROR_CYCLE_BTN_UNFILTER); + } + } + } + + return IAction.ActionResult.createResult(true); + } + + public void filterCycle(KNode root) { + Predicate knodeNotInCycle = it -> { + return !it.getProperty(CycleVisualization.DEPENDENCY_CYCLE); + }; + + Predicate kedgeNotInCycle = it -> { + return !it.getProperty(CycleVisualization.DEPENDENCY_CYCLE); + }; + + root.getChildren().removeIf(knodeNotInCycle); + for (KNode node : root.getChildren()) { + node.getOutgoingEdges().removeIf(kedgeNotInCycle); + this.filterCycle(node); + } + } + + public void markCycleFiltered(KNode node) { + Object source = this.sourceElement(node); + if (source != null) { + FILTERING_STATES.put(source, true); + } + } + + public void resetCycleFiltering(KNode node) { + Object source = this.sourceElement(node); + if (source != null) { + FILTERING_STATES.remove(source); + } + } + + public boolean isCycleFiltered(KNode node) { + Object source = this.sourceElement(node); + Boolean result = FILTERING_STATES.get(source); + return result == null ? false : result; + } + + public void markCycleFilterText(KText text, KNode node) { + text.setProperty(FILTER_BUTTON, true); + node.setProperty(FILTER_BUTTON, true); + } +} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/FilterCycleAction.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/FilterCycleAction.xtend deleted file mode 100644 index c954b8e1b4..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/FilterCycleAction.xtend +++ /dev/null @@ -1,113 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis.action - -import de.cau.cs.kieler.klighd.IAction -import de.cau.cs.kieler.klighd.kgraph.KNode -import de.cau.cs.kieler.klighd.krendering.KText -import java.util.WeakHashMap -import org.eclipse.elk.graph.properties.Property -import org.lflang.diagram.synthesis.LinguaFrancaSynthesis -import org.lflang.diagram.synthesis.util.CycleVisualization - -/** - * Action that filters the diagram for only those elements included in a cycle. - * - * @author{Alexander Schulz-Rosengarten } - */ -class FilterCycleAction extends AbstractAction { - - public static val ID = "org.lflang.diagram.synthesis.action.FilterCycleAction" - - /** Memory-leak-free cache of filtered states */ - static final WeakHashMap FILTERING_STATES = new WeakHashMap() - /** Property to mark filter button */ - static val FILTER_BUTTON = new Property("org.lflang.diagram.synthesis.action.cyclefilter.button", false) - - override execute(ActionContext context) { - val vc = context.viewContext - - val all = vc.getOptionValue(LinguaFrancaSynthesis.SHOW_ALL_REACTORS) - val nodes = if (all instanceof Boolean && all as Boolean) { - vc.viewModel.children.map[children].flatten.toList - } else { - vc.viewModel.children - } - - if (nodes.exists[isCycleFiltered()]) { - // undo - nodes.forEach[resetCycleFiltering()] - - // re-synthesize everything - vc.viewModel.children.clear() - vc.update() - } else { - // filter - nodes.forEach[markCycleFiltered(); filterCycle()] - - // switch filter label - for (node : nodes.filter[getProperty(FILTER_BUTTON)]) { - val text = node.eAllContents.filter(KText).findFirst[getProperty(FILTER_BUTTON)] - if (text !== null) { - text.text = LinguaFrancaSynthesis.TEXT_ERROR_CYCLE_BTN_UNFILTER - } - } - } - - return IAction.ActionResult.createResult(true); - } - - def void filterCycle(KNode root) { - root.children.removeIf[!getProperty(CycleVisualization.DEPENDENCY_CYCLE)] - for (node : root.children) { - node.outgoingEdges.removeIf[!getProperty(CycleVisualization.DEPENDENCY_CYCLE)] - node.filterCycle() - } - } - - def void markCycleFiltered(KNode node) { - val source = node.sourceElement - if (source !== null) { - FILTERING_STATES.put(source, true) - } - } - - def void resetCycleFiltering(KNode node) { - val source = node.sourceElement - if (source !== null) { - FILTERING_STATES.remove(source) - } - } - - def boolean isCycleFiltered(KNode node) { - val source = node.sourceElement - return FILTERING_STATES.get(source)?:false - } - - def void markCycleFilterText(KText text, KNode node) { - text.setProperty(FILTER_BUTTON, true) - node.setProperty(FILTER_BUTTON, true) - } -} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.java similarity index 50% rename from org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.xtend rename to org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.java index 1feaf6b816..9c1d1721b0 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/MemorizingExpandCollapseAction.java @@ -22,18 +22,18 @@ * (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.diagram.synthesis.action +package org.lflang.diagram.synthesis.action; -import de.cau.cs.kieler.klighd.IAction -import de.cau.cs.kieler.klighd.IViewer -import de.cau.cs.kieler.klighd.SynthesisOption -import de.cau.cs.kieler.klighd.kgraph.KNode -import java.util.WeakHashMap -import org.lflang.generator.NamedInstance - -import static extension com.google.common.base.Preconditions.* -import static extension org.lflang.diagram.synthesis.util.InterfaceDependenciesVisualization.updateInterfaceDependencyVisibility -import static extension org.lflang.diagram.synthesis.util.NamedInstanceUtil.* +import com.google.common.base.Preconditions; +import de.cau.cs.kieler.klighd.IAction; +import de.cau.cs.kieler.klighd.IViewer; +import de.cau.cs.kieler.klighd.SynthesisOption; +import de.cau.cs.kieler.klighd.ViewContext; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import java.util.WeakHashMap; +import org.lflang.diagram.synthesis.util.InterfaceDependenciesVisualization; +import org.lflang.diagram.synthesis.util.NamedInstanceUtil; +import org.lflang.generator.NamedInstance; /** * Action for toggling collapse/expand state of reactors that memorizes the state and @@ -42,61 +42,66 @@ * * @author{Alexander Schulz-Rosengarten } */ -class MemorizingExpandCollapseAction extends AbstractAction { +public class MemorizingExpandCollapseAction extends AbstractAction { - public static val ID = "org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction" + public static final String ID = "org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction"; - /** The related synthesis option */ - public static val SynthesisOption MEMORIZE_EXPANSION_STATES = SynthesisOption.createCheckOption("Remember Collapsed/Expanded Reactors", true) + /** + * The related synthesis option + */ + public static final SynthesisOption MEMORIZE_EXPANSION_STATES = SynthesisOption.createCheckOption("Remember Collapsed/Expanded Reactors", true); - /** Memory-leak-free cache of expansion states */ - static final WeakHashMap EXPANSION_STATES = new WeakHashMap() + /** + * Memory-leak-free cache of expansion states + */ + private static final WeakHashMap EXPANSION_STATES = new WeakHashMap<>(); /** * Sets the expansion state of a node and saves it for future synthesis. */ - static def setExpansionState(KNode node, Object memorizableObj, IViewer viewer, boolean expand) { - node.checkNotNull + public static void setExpansionState(final KNode node, final Object memorizableObj, final IViewer viewer, final boolean expand) { + + Preconditions.checkNotNull(node); // Store new state if activated - if (viewer.viewContext.getOptionValue(MEMORIZE_EXPANSION_STATES) as Boolean && memorizableObj !== null) { - if (memorizableObj instanceof NamedInstance) { - EXPANSION_STATES.put(memorizableObj.uniqueID, expand) + if (((Boolean) viewer.getViewContext().getOptionValue(MEMORIZE_EXPANSION_STATES)) && memorizableObj != null) { + if (memorizableObj instanceof NamedInstance) { + EXPANSION_STATES.put(((NamedInstance) memorizableObj).uniqueID(), expand); } else { - EXPANSION_STATES.put(memorizableObj, expand) + EXPANSION_STATES.put(memorizableObj, expand); } } // Apply state if (expand) { - viewer.expand(node) + viewer.expand(node); } else { - viewer.collapse(node) + viewer.collapse(node); } // Handle edges that should only appear for one of the renderings - node.updateInterfaceDependencyVisibility(expand) + InterfaceDependenciesVisualization.updateInterfaceDependencyVisibility(node, expand); } /** * @return the memorized expansion state of the given model element or null if not memorized */ - static def getExpansionState(Object obj) { - if (obj instanceof NamedInstance) { - return EXPANSION_STATES.get(obj.uniqueID) + public static Boolean getExpansionState(final Object obj) { + if (obj instanceof NamedInstance) { + return EXPANSION_STATES.get(((NamedInstance) obj).uniqueID()); } - return EXPANSION_STATES.get(obj) + return EXPANSION_STATES.get(obj); } //----------------------------------------------------------------------------------------------------------------- - override execute(ActionContext context) { - val vc = context.viewContext - val v = vc.viewer - val node = context.KNode - - node.setExpansionState(node.linkedInstance, v, !v.isExpanded(node)) // toggle - + @Override + public IAction.ActionResult execute(final IAction.ActionContext context) { + ViewContext vc = context.getViewContext(); + IViewer v = vc.getViewer(); + KNode node = context.getKNode(); + NamedInstance linkedInstance = NamedInstanceUtil.getLinkedInstance(node); + setExpansionState(node, linkedInstance, v, !v.isExpanded(node)); // toggle return IAction.ActionResult.createResult(true); } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ShowCycleAction.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ShowCycleAction.java new file mode 100644 index 0000000000..2596328a4f --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ShowCycleAction.java @@ -0,0 +1,91 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis.action; + +import de.cau.cs.kieler.klighd.IAction; +import de.cau.cs.kieler.klighd.ViewContext; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.util.ModelingUtil; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Set; + +import org.eclipse.xtext.xbase.lib.IteratorExtensions; +import org.lflang.diagram.synthesis.util.CycleVisualization; +import org.lflang.diagram.synthesis.util.NamedInstanceUtil; + +/** + * Action that expands all reactor nodes that are included in a cycle. + * + * @author{Alexander Schulz-Rosengarten } + */ +public class ShowCycleAction extends AbstractAction { + + public static final String ID = "org.lflang.diagram.synthesis.action.ShowCycleAction"; + + private static final CollapseAllReactorsAction collapseAll = new CollapseAllReactorsAction(); + + @Override + public IAction.ActionResult execute(final IAction.ActionContext context) { + ViewContext vc = context.getViewContext(); + + // Collapse all + collapseAll.execute(context); + + // Expand only errors + Iterator knodes = ModelingUtil.eAllContentsOfType(vc.getViewModel(), KNode.class); + + // Filter out nodes that are not in cycle or not a reactor + knodes = IteratorExtensions.filter(knodes, it -> { + return it.getProperty(CycleVisualization.DEPENDENCY_CYCLE) && sourceIsReactor(it); + }); + + // Remove duplicates + Set cycleNodes = IteratorExtensions.toSet(knodes); + + // Include parents + LinkedList check = new LinkedList<>(cycleNodes); + + while (!check.isEmpty()) { + KNode parent = check.pop().getParent(); + if (parent != null && !cycleNodes.contains(parent)) { + cycleNodes.add(parent); + check.add(parent); + } + } + + // Expand + for (KNode node : cycleNodes) { + MemorizingExpandCollapseAction.setExpansionState( + node, + NamedInstanceUtil.getLinkedInstance(node), + vc.getViewer(), + true + ); + } + return IAction.ActionResult.createResult(true); + } + +} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ShowCycleAction.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ShowCycleAction.xtend deleted file mode 100644 index 957c4c9f44..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/action/ShowCycleAction.xtend +++ /dev/null @@ -1,74 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis.action - -import de.cau.cs.kieler.klighd.IAction -import de.cau.cs.kieler.klighd.kgraph.KNode -import org.lflang.diagram.synthesis.util.CycleVisualization - -import static extension de.cau.cs.kieler.klighd.util.ModelingUtil.* -import static extension org.lflang.diagram.synthesis.action.MemorizingExpandCollapseAction.* -import static extension org.lflang.diagram.synthesis.util.NamedInstanceUtil.* - -/** - * Action that expands all reactor nodes that are included in a cycle. - * - * @author{Alexander Schulz-Rosengarten } - */ -class ShowCycleAction extends AbstractAction { - - public static val ID = "org.lflang.diagram.synthesis.action.ShowCycleAction" - - static val collapseAll = new CollapseAllReactorsAction() - - override execute(ActionContext context) { - val vc = context.viewContext - - // Collapse all - collapseAll.execute(context) - - // Expand only errors - val cycleNodes = newHashSet() - cycleNodes += vc.viewModel.eAllContentsOfType(KNode).filter[ - getProperty(CycleVisualization.DEPENDENCY_CYCLE) && sourceIsReactor - ].toIterable - // include parents - val check = newLinkedList(cycleNodes) - while (!check.empty) { - val parent = check.pop.parent - if (parent !== null && !cycleNodes.contains(parent)) { - cycleNodes += parent - check += parent - } - } - // expand - for (node : cycleNodes) { - node.setExpansionState(node.linkedInstance, vc.viewer, true) - } - - return IAction.ActionResult.createResult(true); - } - -} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/postprocessor/ReactionPortAdjustment.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/postprocessor/ReactionPortAdjustment.java new file mode 100644 index 0000000000..b773364cc4 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/postprocessor/ReactionPortAdjustment.java @@ -0,0 +1,179 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis.postprocessor; + +import de.cau.cs.kieler.klighd.IStyleModifier; +import de.cau.cs.kieler.klighd.IViewer; +import de.cau.cs.kieler.klighd.internal.ILayoutRecorder; +import de.cau.cs.kieler.klighd.kgraph.KEdge; +import de.cau.cs.kieler.klighd.kgraph.KGraphElement; +import de.cau.cs.kieler.klighd.kgraph.KGraphFactory; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.kgraph.KPoint; +import de.cau.cs.kieler.klighd.kgraph.KPort; +import java.util.List; +import java.util.Map; +import org.eclipse.elk.core.options.CoreOptions; +import org.eclipse.elk.core.options.PortSide; +import org.eclipse.elk.graph.properties.IProperty; +import org.eclipse.elk.graph.properties.Property; +import org.eclipse.xtext.xbase.lib.Extension; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.eclipse.xtext.xbase.lib.Pair; +import org.lflang.diagram.synthesis.styles.LinguaFrancaShapeExtensions; + +/** + * Adjusts the port position of reactions node AFTER layout, to allow free port order but also adapt (snuggle) to pointy shape of reaction node. + * + * @author{Alexander Schulz-Rosengarten } + */ +public class ReactionPortAdjustment implements IStyleModifier { + + public static final String ID = "org.lflang.diagram.synthesis.postprocessor.ReactionPortAdjustment"; + + private static final Property PROCESSED = new Property<>("org.lflang.diagram.synthesis.postprocessor.reaction.ports.processed", false); + + @Extension + private KGraphFactory _kGraphFactory = KGraphFactory.eINSTANCE; + + @Override + public boolean modify(IStyleModifier.StyleModificationContext context) { + try { + KGraphElement node = context.getGraphElement(); + if (node instanceof KNode) { + KNode knode = (KNode) node; + + // Find root node + KNode parent = knode; + while (parent.eContainer() != null) { + parent = (KNode) parent.eContainer(); + } + + // Get viewer (this is a bit brittle because it fetches the viewer from some internal property) + Map.Entry, Object> first = IterableExtensions.findFirst( + parent.getAllProperties().entrySet(), + it -> { + return it.getKey().getId().equals("de.cau.cs.kieler.klighd.viewer") || + it.getKey().getId().equals("klighd.layout.viewer"); + } + ); + Object viewer = first != null ? first.getValue() : null; + + ILayoutRecorder recorder = null; + if (viewer instanceof IViewer) { + recorder = ((IViewer) viewer).getViewContext().getLayoutRecorder(); + } + + + if (!knode.getPorts().isEmpty()) { + if (IterableExtensions.head(knode.getPorts()).getYpos() != 0 && + !knode.getProperty(ReactionPortAdjustment.PROCESSED)) { // Only adjust if layout is already applied + // important for incremental update animation + if (recorder != null) { + recorder.startRecording(); + } + + List in = IterableExtensions.toList( + IterableExtensions.sortBy( + IterableExtensions.filter( + knode.getPorts(), + it -> { + return it.getProperty(CoreOptions.PORT_SIDE) == PortSide.WEST && + !it.hasProperty(CoreOptions.PORT_BORDER_OFFSET); + }), + it -> { return it.getYpos(); }) + ); + + List out = IterableExtensions.toList( + IterableExtensions.sortBy( + IterableExtensions.filter( + knode.getPorts(), + it -> { + return it.getProperty(CoreOptions.PORT_SIDE) == PortSide.EAST && + !it.hasProperty(CoreOptions.PORT_BORDER_OFFSET); + }), + it -> { return it.getYpos(); }) + ); + + // Adjust + adjustPositions(IterableExtensions.indexed(in), in.size(), true); + adjustPositions(IterableExtensions.indexed(out), out.size(), false); + knode.setProperty(ReactionPortAdjustment.PROCESSED, true); + + if (recorder!=null) { + recorder.stopRecording(0); + } + + } else if (IterableExtensions.head(knode.getPorts()).getYpos() == 0) { + knode.setProperty(PROCESSED, false); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + // do not disturb rendering process + } + return false; + } + + public void adjustPositions(Iterable> indexedPorts, int count, boolean input) { + float segments = LinguaFrancaShapeExtensions.REACTION_POINTINESS * 2 / (count + 1); + for (Pair indexedPort : indexedPorts) { + KPort port = indexedPort.getValue(); + int idx = indexedPort.getKey(); + float offset = 0; + + if (count % 2 != 0 && idx == count / 2) { + offset += LinguaFrancaShapeExtensions.REACTION_POINTINESS; + } else if (idx < count / 2) { + offset += segments * (idx + 1); + } else { + offset += segments * (count - idx); + } + + if (!input) { // reverse + offset -= LinguaFrancaShapeExtensions.REACTION_POINTINESS; + } + + // apply + port.setPos(port.getXpos() + offset, port.getYpos()); + for (KEdge edge : port.getEdges()) { + if (input) { + edge.setTargetPoint(adjustedKPoint(edge.getTargetPoint(), offset)); + } else { + edge.setSourcePoint(adjustedKPoint(edge.getSourcePoint(), offset)); + } + } + } + } + + public KPoint adjustedKPoint(KPoint point, float xOffset) { + KPoint kPoint = _kGraphFactory.createKPoint(); + kPoint.setX(point.getX() + xOffset); + kPoint.setY(point.getY()); + return kPoint; + } + +} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/postprocessor/ReactionPortAdjustment.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/postprocessor/ReactionPortAdjustment.xtend deleted file mode 100644 index 5b2b92ab30..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/postprocessor/ReactionPortAdjustment.xtend +++ /dev/null @@ -1,128 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis.postprocessor - -import de.cau.cs.kieler.klighd.IStyleModifier -import de.cau.cs.kieler.klighd.IViewer -import de.cau.cs.kieler.klighd.kgraph.KGraphFactory -import de.cau.cs.kieler.klighd.kgraph.KNode -import de.cau.cs.kieler.klighd.kgraph.KPoint -import de.cau.cs.kieler.klighd.kgraph.KPort -import org.eclipse.elk.core.options.CoreOptions -import org.eclipse.elk.core.options.PortSide -import org.eclipse.elk.graph.properties.Property -import org.lflang.diagram.synthesis.styles.LinguaFrancaShapeExtensions - -/** - * Adjusts the port position of reactions node AFTER layout, to allow free port order but also adapt (snuggle) to pointy shape of reaction node. - * - * @author{Alexander Schulz-Rosengarten } - */ -class ReactionPortAdjustment implements IStyleModifier { - - public static val ID = "org.lflang.diagram.synthesis.postprocessor.ReactionPortAdjustment" - static val PROCESSED = new Property("org.lflang.diagram.synthesis.postprocessor.reaction.ports.processed", false) - - extension KGraphFactory = KGraphFactory.eINSTANCE - - override modify(StyleModificationContext context) { - try { - val node = context.graphElement - if (node instanceof KNode) { - // Find root node - var parent = node - while(parent.eContainer !== null) { - parent = parent.eContainer as KNode - } - // Get viewer (this is a bit brittle because it fetches the viewer from some internal property) - val viewer = parent.allProperties.entrySet.findFirst[key.id.equals("de.cau.cs.kieler.klighd.viewer") || key.id.equals("klighd.layout.viewer")]?.value - val recorder = if (viewer instanceof IViewer) { - viewer.viewContext?.layoutRecorder - } - - if (!node.ports.empty) { - if (node.ports.head.ypos !== 0 && !node.getProperty(PROCESSED)) { // Only adjust if layout is already applied - recorder?.startRecording // important for incremental update animation - - val in = node.ports.filter[getProperty(CoreOptions.PORT_SIDE) === PortSide.WEST && !hasProperty(CoreOptions.PORT_BORDER_OFFSET)].sortBy[ypos].toList - val out = node.ports.filter[getProperty(CoreOptions.PORT_SIDE) === PortSide.EAST && !hasProperty(CoreOptions.PORT_BORDER_OFFSET)].sortBy[ypos].toList - - // Adjust - in.indexed.adjustPositions(in.size, true) - out.indexed.adjustPositions(out.size, false) - node.setProperty(PROCESSED, true) - - recorder?.stopRecording(0) - } else if (node.ports.head.ypos === 0) { - node.setProperty(PROCESSED, false) - } - } - } - } catch (Exception e) { - e.printStackTrace - // do not disturb rendering process - } - return false - } - - def void adjustPositions(Iterable> indexedPorts, int count, boolean input) { - val segments = (LinguaFrancaShapeExtensions.REACTION_POINTINESS * 2) / (count + 1) - for (indexedPort : indexedPorts) { - val port = indexedPort.value - val idx = indexedPort.key - - var float offset = 0 - if (count % 2 !== 0 && idx === count / 2) { - offset += LinguaFrancaShapeExtensions.REACTION_POINTINESS - } else if (idx < count / 2) { - offset += segments * (idx + 1) - } else { - offset += segments * (count - idx) - } - - if (!input) { // reverse - offset -= LinguaFrancaShapeExtensions.REACTION_POINTINESS - } - - // apply - port.setPos(port.xpos + offset, port.ypos) - for (edge : port.edges) { - if (input) { - edge.targetPoint = edge.targetPoint.adjustedKPoint(offset) - } else { - edge.sourcePoint = edge.sourcePoint.adjustedKPoint(offset) - } - } - } - } - - def KPoint adjustedKPoint(KPoint point, float xOffest) { - return createKPoint => [ - x = point.x + xOffest - y = point.y - ] - } - -} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.java new file mode 100644 index 0000000000..7834e8baba --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.java @@ -0,0 +1,806 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis.styles; + +import de.cau.cs.kieler.klighd.KlighdConstants; +import de.cau.cs.kieler.klighd.kgraph.KEdge; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.kgraph.KPort; +import de.cau.cs.kieler.klighd.krendering.Arc; +import de.cau.cs.kieler.klighd.krendering.Colors; +import de.cau.cs.kieler.klighd.krendering.HorizontalAlignment; +import de.cau.cs.kieler.klighd.krendering.KArc; +import de.cau.cs.kieler.klighd.krendering.KAreaPlacementData; +import de.cau.cs.kieler.klighd.krendering.KContainerRendering; +import de.cau.cs.kieler.klighd.krendering.KDecoratorPlacementData; +import de.cau.cs.kieler.klighd.krendering.KEllipse; +import de.cau.cs.kieler.klighd.krendering.KGridPlacement; +import de.cau.cs.kieler.klighd.krendering.KPolygon; +import de.cau.cs.kieler.klighd.krendering.KPolyline; +import de.cau.cs.kieler.klighd.krendering.KPosition; +import de.cau.cs.kieler.klighd.krendering.KRectangle; +import de.cau.cs.kieler.klighd.krendering.KRendering; +import de.cau.cs.kieler.klighd.krendering.KRenderingFactory; +import de.cau.cs.kieler.klighd.krendering.KRoundedRectangle; +import de.cau.cs.kieler.klighd.krendering.KText; +import de.cau.cs.kieler.klighd.krendering.LineStyle; +import de.cau.cs.kieler.klighd.krendering.VerticalAlignment; +import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; +import de.cau.cs.kieler.klighd.krendering.extensions.KColorExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KEdgeExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KLabelExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KNodeExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KPolylineExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KPortExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions; +import de.cau.cs.kieler.klighd.syntheses.DiagramSyntheses; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; +import org.eclipse.elk.core.options.CoreOptions; +import org.eclipse.elk.core.options.PortSide; +import org.eclipse.elk.graph.properties.Property; +import org.eclipse.xtext.xbase.lib.Extension; +import org.eclipse.xtext.xbase.lib.Functions.Function1; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.eclipse.xtext.xbase.lib.Pair; +import org.eclipse.xtext.xbase.lib.StringExtensions; +import org.lflang.diagram.synthesis.AbstractSynthesisExtensions; +import org.lflang.diagram.synthesis.LinguaFrancaSynthesis; +import org.lflang.diagram.synthesis.postprocessor.ReactionPortAdjustment; +import org.lflang.diagram.synthesis.util.UtilityExtensions; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.TimerInstance; +import de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceX; +import de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceY; +import static de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceX.*; +import static de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceY.*; + +/** + * Extension class that provides shapes and figures for the Lingua France diagram synthesis. + * + * @author{Alexander Schulz-Rosengarten } + */ +@ViewSynthesisShared +public class LinguaFrancaShapeExtensions extends AbstractSynthesisExtensions { + + public static final float REACTION_POINTINESS = 6; // arrow point length + // Property for marking the KContainterRendering in Reactor figures that is supposed to hold the content + public static final Property REACTOR_CONTENT_CONTAINER = new Property<>( + "org.lflang.diagram.synthesis.shapes.reactor.content", false); + + @Inject + @Extension + private KNodeExtensions _kNodeExtensions; + @Inject + @Extension + private KEdgeExtensions _kEdgeExtensions; + @Inject + @Extension + private KPortExtensions _kPortExtensions; + @Inject + @Extension + private KLabelExtensions _kLabelExtensions; + @Inject + @Extension + private KRenderingExtensions _kRenderingExtensions; + @Inject + @Extension + private KContainerRenderingExtensions _kContainerRenderingExtensions; + @Inject + @Extension + private KPolylineExtensions _kPolylineExtensions; + @Inject + @Extension + private KColorExtensions _kColorExtensions; + @Inject + @Extension + private LinguaFrancaStyleExtensions _linguaFrancaStyleExtensions; + @Inject + @Extension + private UtilityExtensions _utilityExtensions; + @Extension + private KRenderingFactory _kRenderingFactory = KRenderingFactory.eINSTANCE; + + public static final float BANK_FIGURE_X_OFFSET_SUM = 6.0f; + public static final float BANK_FIGURE_Y_OFFSET_SUM = 9.0f; + + /** + * Creates the main reactor frame. + */ + public KRoundedRectangle addMainReactorFigure(KNode node, ReactorInstance reactorInstance, String text) { + int padding = getBooleanValue(LinguaFrancaSynthesis.SHOW_HYPERLINKS) ? 8 : 6; + KRoundedRectangle figure = _kRenderingExtensions.addRoundedRectangle(node, 8, 8, 1); + _kContainerRenderingExtensions.setGridPlacement(figure, 1); + _kRenderingExtensions.setLineWidth(figure, 1); + _kRenderingExtensions.setForeground(figure, Colors.GRAY); + _kRenderingExtensions.setBackground(figure, Colors.WHITE); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(figure); + + // Create parent container + KRectangle parentContainer = _kContainerRenderingExtensions.addRectangle(figure); + _kRenderingExtensions.setInvisible(parentContainer, true); + setGridPlacementDataFromPointToPoint(parentContainer, + LEFT, padding, 0, TOP, padding, 0, + RIGHT, padding, 0, BOTTOM, 4, 0 + ); + + // Create child container + KRectangle childContainer = _kContainerRenderingExtensions.addRectangle(parentContainer); + _kRenderingExtensions.setInvisible(childContainer, true); + _kRenderingExtensions.setPointPlacementData(childContainer, + _kRenderingExtensions.LEFT, 0, 0.5f, + _kRenderingExtensions.TOP, 0, 0.5f, + _kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_CENTRAL, 0, + 0, 0, 0); + KGridPlacement placement = _kContainerRenderingExtensions.setGridPlacement(childContainer, 1); + + // Add text to the child container + KText childText = _kContainerRenderingExtensions.addText(childContainer, text); + DiagramSyntheses.suppressSelectability(childText); + _linguaFrancaStyleExtensions.underlineSelectionStyle(childText); + + if (reactorInstance.reactorDefinition.isFederated()) { + KContainerRendering cloudIcon = _linguaFrancaStyleExtensions.addCloudIcon(childContainer); + setGridPlacementDataFromPointToPoint(cloudIcon, + LEFT, 3, 0, TOP, 0, 0, + RIGHT, 0, 0, BOTTOM, 0, 0 + ); + placement.setNumColumns(2); + + if (reactorInstance.reactorDefinition.getHost() != null && + getBooleanValue(LinguaFrancaSynthesis.SHOW_REACTOR_HOST)) { + KText hostNameText = _kContainerRenderingExtensions.addText(childContainer, + _utilityExtensions.toText(reactorInstance.reactorDefinition.getHost())); + DiagramSyntheses.suppressSelectability(hostNameText); + _linguaFrancaStyleExtensions.underlineSelectionStyle(hostNameText); + setGridPlacementDataFromPointToPoint(hostNameText, + LEFT, 3, 0, TOP, 0, 0, + RIGHT, 0, 0, BOTTOM, 0, 0 + ); + placement.setNumColumns(3); + } + } + return figure; + } + + /** + * Creates the visual representation of a reactor node + */ + public ReactorFigureComponents addReactorFigure(KNode node, ReactorInstance reactorInstance, String text) { + int padding = getBooleanValue(LinguaFrancaSynthesis.SHOW_HYPERLINKS) ? 8 : 6; + + Function1 style = r -> { + _kRenderingExtensions.setLineWidth(r, 1); + _kRenderingExtensions.setForeground(r, Colors.GRAY); + _kRenderingExtensions.setBackground(r, Colors.GRAY_95); + return _linguaFrancaStyleExtensions.boldLineSelectionStyle(r); + }; + + KRoundedRectangle figure = _kRenderingExtensions.addRoundedRectangle(node, 8, 8, 1); + _kContainerRenderingExtensions.setGridPlacement(figure, 1); + style.apply(figure); + figure.setProperty(REACTOR_CONTENT_CONTAINER, true); + + // minimal node size is necessary if no text will be added + List minSize = List.of(2 * figure.getCornerWidth(), 2 * figure.getCornerHeight()); + _kNodeExtensions.setMinimalNodeSize(node, minSize.get(0), minSize.get(1)); + + // Add parent container + KRectangle parentContainer = _kContainerRenderingExtensions.addRectangle(figure); + _kRenderingExtensions.setInvisible(parentContainer, true); + setGridPlacementDataFromPointToPoint(parentContainer, + LEFT, padding, 0, + TOP, padding, 0, + RIGHT, padding, 0, BOTTOM, + _utilityExtensions.hasContent(reactorInstance) ? 4 : padding, 0 + ); + + // Add centered child container + KRectangle childContainer = _kContainerRenderingExtensions.addRectangle(parentContainer); + _kRenderingExtensions.setInvisible(childContainer, true); + _kRenderingExtensions.setPointPlacementData(childContainer, + _kRenderingExtensions.LEFT, 0, 0.5f, + _kRenderingExtensions.TOP, 0, 0.5f, + _kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_CENTRAL, 0, + 0, 0, 0); + KGridPlacement placement = _kContainerRenderingExtensions.setGridPlacement(childContainer, 1); + + KText childText = _kContainerRenderingExtensions.addText(childContainer, text); + DiagramSyntheses.suppressSelectability(childText); + _linguaFrancaStyleExtensions.underlineSelectionStyle(childText); + + if (!_utilityExtensions.isRoot(reactorInstance) && + reactorInstance.getDefinition().getHost() != null) { + KRendering cloudUploadIcon = _linguaFrancaStyleExtensions.addCloudUploadIcon(childContainer); + setGridPlacementDataFromPointToPoint(cloudUploadIcon, + LEFT, 3, 0, TOP, 0, 0, + RIGHT, 0, 0, BOTTOM, 0, 0 + ); + placement.setNumColumns(2); + + if (getBooleanValue(LinguaFrancaSynthesis.SHOW_REACTOR_HOST)) { + KText reactorHostText = _kContainerRenderingExtensions.addText(childContainer, + _utilityExtensions.toText(reactorInstance.getDefinition().getHost())); + DiagramSyntheses.suppressSelectability(reactorHostText); + _linguaFrancaStyleExtensions.underlineSelectionStyle(reactorHostText); + setGridPlacementDataFromPointToPoint(reactorHostText, + LEFT, 3, 0, TOP, 0, 0, + RIGHT, 0, 0, BOTTOM, 0, 0 + ); + placement.setNumColumns(3); + } + } + + if (reactorInstance.isBank()) { + List bank = new ArrayList<>(); + KContainerRendering container = _kRenderingExtensions.addInvisibleContainerRendering(node); + // TODO handle unresolved width + KRoundedRectangle banks; + banks = _kContainerRenderingExtensions.addRoundedRectangle(container, 8, 8, 1); + style.apply(banks); + setGridPlacementDataFromPointToPoint(banks, + LEFT, BANK_FIGURE_X_OFFSET_SUM, 0, TOP, BANK_FIGURE_Y_OFFSET_SUM, 0, + RIGHT, 0, 0, BOTTOM, 0, 0 + ); + if (reactorInstance.getWidth() == 3) { + banks = _kContainerRenderingExtensions.addRoundedRectangle(container, 8, 8, 1); + style.apply(banks); + setGridPlacementDataFromPointToPoint(banks, + LEFT, BANK_FIGURE_X_OFFSET_SUM / 2, 0, TOP, BANK_FIGURE_Y_OFFSET_SUM / 2, 0, + RIGHT, BANK_FIGURE_X_OFFSET_SUM / 2, 0, BOTTOM, BANK_FIGURE_Y_OFFSET_SUM / 2, 0 + ); + } else if (reactorInstance.getWidth() != 2 && reactorInstance.getWidth() != 3) { + banks = _kContainerRenderingExtensions.addRoundedRectangle(container, 8, 8, 1); + style.apply(banks); + setGridPlacementDataFromPointToPoint(banks, + LEFT, 2 * BANK_FIGURE_X_OFFSET_SUM / 3, 0, TOP, 2 * BANK_FIGURE_Y_OFFSET_SUM / 3, 0, + RIGHT, BANK_FIGURE_X_OFFSET_SUM / 3, 0, BOTTOM, BANK_FIGURE_Y_OFFSET_SUM / 3, 0 + ); + + banks = _kContainerRenderingExtensions.addRoundedRectangle(container, 8, 8, 1); + style.apply(banks); + setGridPlacementDataFromPointToPoint(banks, + LEFT, BANK_FIGURE_X_OFFSET_SUM / 3, 0, TOP, BANK_FIGURE_Y_OFFSET_SUM / 3, 0, + RIGHT, 2 * BANK_FIGURE_X_OFFSET_SUM / 3, 0, BOTTOM, 2 * BANK_FIGURE_Y_OFFSET_SUM / 3, 0 + ); + } + + container.getChildren().add(figure); + setGridPlacementDataFromPointToPoint(figure, + LEFT, 0, 0, TOP, 0, 0, + RIGHT, BANK_FIGURE_X_OFFSET_SUM, 0, BOTTOM, BANK_FIGURE_Y_OFFSET_SUM, 0 + ); + bank.addAll(container.getChildren()); + + KRectangle widthLabelContainer = _kContainerRenderingExtensions.addRectangle(container); + _kRenderingExtensions.setInvisible(widthLabelContainer, true); + setGridPlacementDataFromPointToPoint(widthLabelContainer, + LEFT, 12, 0, BOTTOM, 9, 0, + RIGHT, 6, 0, BOTTOM, 0.5f, 0 + ); + // Handle unresolved width. + String widthLabel = reactorInstance.getWidth() >= 0 ? Integer.toString(reactorInstance.getWidth()) : "?"; + KText widthLabelText = _kContainerRenderingExtensions.addText(widthLabelContainer, widthLabel); + _kRenderingExtensions.setHorizontalAlignment(widthLabelText, HorizontalAlignment.LEFT); + _kRenderingExtensions.setVerticalAlignment(widthLabelText, VerticalAlignment.BOTTOM); + _kRenderingExtensions.setFontSize(widthLabelText, 6); + _linguaFrancaStyleExtensions.noSelectionStyle(widthLabelText); + associateWith(widthLabelText, reactorInstance.getDefinition().getWidthSpec()); + return new ReactorFigureComponents(container, figure, bank); + } else { + return new ReactorFigureComponents(figure, figure, List.of(figure)); + } + } + + /** + * Creates the visual representation of a reaction node + */ + public KPolygon addReactionFigure(KNode node, ReactionInstance reaction) { + int minHeight = 22; + int minWidth = 45; + ReactorInstance reactor = reaction.getParent(); + _kNodeExtensions.setMinimalNodeSize(node, minWidth, minHeight); + + // Create base shape + KPolygon baseShape = _kRenderingExtensions.addPolygon(node); + associateWith(baseShape, reaction); + _kRenderingExtensions.setLineWidth(baseShape, 1); + _kRenderingExtensions.setForeground(baseShape, Colors.GRAY_45); + _kRenderingExtensions.setBackground(baseShape, Colors.GRAY_65); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(baseShape); + baseShape.getPoints().addAll( + List.of( + _kRenderingExtensions.createKPosition(LEFT, 0, 0, TOP, 0, 0), + _kRenderingExtensions.createKPosition(RIGHT, REACTION_POINTINESS, 0, TOP, 0, 0), + _kRenderingExtensions.createKPosition(RIGHT, 0, 0, TOP, 0, 0.5f), + _kRenderingExtensions.createKPosition(RIGHT, REACTION_POINTINESS, 0, BOTTOM, 0, 0), + _kRenderingExtensions.createKPosition(LEFT, 0, 0, BOTTOM, 0, 0), + _kRenderingExtensions.createKPosition(LEFT, REACTION_POINTINESS, 0, BOTTOM, 0, 0.5f) + ) + ); + IterableExtensions.head(baseShape.getStyles()).setModifierId(ReactionPortAdjustment.ID); + + KRectangle contentContainer = _kContainerRenderingExtensions.addRectangle(baseShape); + associateWith(contentContainer, reaction); + _kRenderingExtensions.setInvisible(contentContainer, true); + _kRenderingExtensions.setPointPlacementData(contentContainer, + _kRenderingExtensions.LEFT, REACTION_POINTINESS, 0, + _kRenderingExtensions.TOP, 0, 0, + _kRenderingExtensions.H_LEFT, _kRenderingExtensions.V_TOP, REACTION_POINTINESS, + 0, minWidth - REACTION_POINTINESS * 2, minHeight); + _kContainerRenderingExtensions.setGridPlacement(contentContainer, 1); + + if (reactor.reactions.size() > 1) { + KText textToAdd = _kContainerRenderingExtensions.addText(contentContainer, + Integer.toString(reactor.reactions.indexOf(reaction) + 1)); + _kRenderingExtensions.setFontBold(textToAdd, true); + _linguaFrancaStyleExtensions.noSelectionStyle(textToAdd); + DiagramSyntheses.suppressSelectability(textToAdd); + } + + // optional reaction level + if (getBooleanValue(LinguaFrancaSynthesis.SHOW_REACTION_LEVEL)) { + // 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. + try { + String levels = IterableExtensions.join(reaction.getLevels(), ", "); + KText levelsText = _kContainerRenderingExtensions.addText(contentContainer, ("level: " + levels)); + _kRenderingExtensions.setFontBold(levelsText, false); + _linguaFrancaStyleExtensions.noSelectionStyle(levelsText); + DiagramSyntheses.suppressSelectability(levelsText); + } catch (Exception ex) { + // If the graph has cycles, the above fails. Continue without showing levels. + } + } + + // optional code content + boolean hasCode = getBooleanValue(LinguaFrancaSynthesis.SHOW_REACTION_CODE) && + !StringExtensions.isNullOrEmpty(reaction.getDefinition().getCode().getBody()); + if (hasCode) { + KText hasCodeText = _kContainerRenderingExtensions.addText(contentContainer, + _utilityExtensions.trimCode(reaction.getDefinition().getCode())); + associateWith(hasCodeText, reaction); + _kRenderingExtensions.setFontSize(hasCodeText, 6); + _kRenderingExtensions.setFontName(hasCodeText, KlighdConstants.DEFAULT_MONOSPACE_FONT_NAME); + _linguaFrancaStyleExtensions.noSelectionStyle(hasCodeText); + _kRenderingExtensions.setHorizontalAlignment(hasCodeText, HorizontalAlignment.LEFT); + _kRenderingExtensions.setVerticalAlignment(hasCodeText, VerticalAlignment.TOP); + setGridPlacementDataFromPointToPoint(hasCodeText, + _kRenderingExtensions.LEFT, 5, 0, + _kRenderingExtensions.TOP, 5, 0, + _kRenderingExtensions.RIGHT, 5, 0, + _kRenderingExtensions.BOTTOM, 5, 0 + ); + } + + if (reaction.declaredDeadline != null) { + boolean hasDeadlineCode = getBooleanValue(LinguaFrancaSynthesis.SHOW_REACTION_CODE) && + !StringExtensions.isNullOrEmpty(reaction.getDefinition().getDeadline().getCode().getBody()); + if (hasCode || hasDeadlineCode) { + KPolyline line = _kContainerRenderingExtensions.addHorizontalLine(contentContainer, 0); + setGridPlacementDataFromPointToPoint(line, + _kRenderingExtensions.LEFT, 5, 0, + _kRenderingExtensions.TOP, 3, 0, + _kRenderingExtensions.RIGHT, 5, 0, + _kRenderingExtensions.BOTTOM, 6, 0 + ); + } + + // delay with stopwatch + KRectangle labelContainer = _kContainerRenderingExtensions.addRectangle(contentContainer); + _kRenderingExtensions.setInvisible(labelContainer, true); + KRendering placement = setGridPlacementDataFromPointToPoint(labelContainer, + _kRenderingExtensions.LEFT, hasDeadlineCode ? 0 : -REACTION_POINTINESS * 0.5f, 0, + _kRenderingExtensions.TOP, 0, reactor.reactions.size() > 1 || hasCode || hasDeadlineCode ? 0 : 0.5f, + _kRenderingExtensions.RIGHT, 0, 0, + _kRenderingExtensions.BOTTOM, 0, 0 + ); + _kRenderingExtensions.setHorizontalAlignment(placement, HorizontalAlignment.LEFT); + + KRectangle stopWatchFigure = addStopwatchFigure(labelContainer); + _kRenderingExtensions.setLeftTopAlignedPointPlacementData(stopWatchFigure, 0, 0, 0, 0); + + KText stopWatchText = _kContainerRenderingExtensions.addText(labelContainer, + reaction.declaredDeadline.maxDelay.toString()); + associateWith(stopWatchText, reaction.getDefinition().getDeadline().getDelay()); + _kRenderingExtensions.setForeground(stopWatchText, Colors.BROWN); + _kRenderingExtensions.setFontBold(stopWatchText, true); + _kRenderingExtensions.setFontSize(stopWatchText, 7); + _linguaFrancaStyleExtensions.underlineSelectionStyle(stopWatchText); + _kRenderingExtensions.setLeftTopAlignedPointPlacementData(stopWatchText, 15, 0, 0, 0); + + // optional code content + if (hasDeadlineCode) { + KText contentContainerText = _kContainerRenderingExtensions.addText(contentContainer, + _utilityExtensions.trimCode(reaction.getDefinition().getDeadline().getCode())); + associateWith(contentContainerText, reaction.deadline); + _kRenderingExtensions.setForeground(contentContainerText, Colors.BROWN); + _kRenderingExtensions.setFontSize(contentContainerText, 6); + _kRenderingExtensions.setFontName(contentContainerText, KlighdConstants.DEFAULT_MONOSPACE_FONT_NAME); + setGridPlacementDataFromPointToPoint(contentContainerText, + _kRenderingExtensions.LEFT, 5, 0, + _kRenderingExtensions.TOP, 0, 0, + _kRenderingExtensions.RIGHT, 5, 0, + _kRenderingExtensions.BOTTOM, 5, 0 + ); + _kRenderingExtensions.setHorizontalAlignment(contentContainerText, HorizontalAlignment.LEFT); + _linguaFrancaStyleExtensions.noSelectionStyle(contentContainerText); + } + } + + return baseShape; + } + + /** + * Stopwatch figure for deadlines. + */ + public KRectangle addStopwatchFigure(KContainerRendering parent) { + final int size = 12; + KRectangle container = _kContainerRenderingExtensions.addRectangle(parent); + _kRenderingExtensions.setInvisible(container, true); + _kRenderingExtensions.setPointPlacementData(container, + _kRenderingExtensions.LEFT, 0, 0, + _kRenderingExtensions.TOP, 0, 0, + _kRenderingExtensions.H_LEFT, _kRenderingExtensions.V_TOP, 0, + 0, size, size); + + KPolyline polyline = _kContainerRenderingExtensions.addPolyline(container, 2, + List.of( + _kRenderingExtensions.createKPosition(LEFT, 3, 0.5f, TOP, (-2), 0), + _kRenderingExtensions.createKPosition(LEFT, (-3), 0.5f, TOP, (-2), 0) + ) + ); + _kRenderingExtensions.setForeground(polyline, Colors.BROWN); + + polyline = _kContainerRenderingExtensions.addPolyline(container, 2, + List.of( + _kRenderingExtensions.createKPosition(LEFT, 0, 0.5f, TOP, (-2), 0), + _kRenderingExtensions.createKPosition(LEFT, 0, 0.5f, TOP, 1, 0) + ) + ); + _kRenderingExtensions.setForeground(polyline, Colors.BROWN); + + KEllipse body = _kContainerRenderingExtensions.addEllipse(container); + _kRenderingExtensions.setLineWidth(body, 1); + _kRenderingExtensions.setForeground(body, Colors.BROWN); + _kRenderingExtensions.setPointPlacementData(body, + _kRenderingExtensions.LEFT, 0, 0, + _kRenderingExtensions.TOP, 0, 0, + _kRenderingExtensions.H_LEFT, _kRenderingExtensions.V_TOP, 0, + 0, size, size); + _linguaFrancaStyleExtensions.noSelectionStyle(body); + + KArc arc = _kContainerRenderingExtensions.addArc(body); + arc.setStartAngle((-20)); + arc.setArcAngle(110); + arc.setArcType(Arc.PIE); + _kRenderingExtensions.setLineWidth(arc, 0); + _kRenderingExtensions.setBackground(arc, Colors.BROWN); + _kRenderingExtensions.setPointPlacementData(arc, + _kRenderingExtensions.LEFT, 2, 0, + _kRenderingExtensions.TOP, 2, 0, + _kRenderingExtensions.H_LEFT, _kRenderingExtensions.V_TOP, 2, + 2, size - 4, size - 4); + _linguaFrancaStyleExtensions.noSelectionStyle(arc); + + return container; + } + + /** + * Creates the visual representation of a timer node + */ + public KEllipse addTimerFigure(KNode node, TimerInstance timer) { + _kNodeExtensions.setMinimalNodeSize(node, 30, 30); + + KEllipse figure = _kRenderingExtensions.addEllipse(node); + _kRenderingExtensions.setBackground(figure, Colors.GRAY_95); + _linguaFrancaStyleExtensions.noSelectionStyle(figure); + _kRenderingExtensions.setLineWidth(figure, 1); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(figure); + + List polylinePoints = List.of( + _kRenderingExtensions.createKPosition(LEFT, 0, 0.5f, TOP, 0, 0.1f), + _kRenderingExtensions.createKPosition(LEFT, 0, 0.5f, TOP, 0, 0.5f), + _kRenderingExtensions.createKPosition(LEFT, 0, 0.7f, TOP, 0, 0.7f) + ); + KPolyline polyline = _kContainerRenderingExtensions.addPolyline(figure, 1, polylinePoints); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(polyline); + + List labelParts = new ArrayList<>(); + if (timer.getOffset() != TimerInstance.DEFAULT_OFFSET && timer.getOffset() != null) { + labelParts.add(timer.getOffset().toString()); + } + if (timer.getPeriod() != TimerInstance.DEFAULT_PERIOD && timer.getPeriod() != null) { + if (timer.getOffset() == TimerInstance.DEFAULT_OFFSET) { + labelParts.add(timer.getOffset().toString()); + } + labelParts.add(timer.getPeriod().toString()); + } + if (!labelParts.isEmpty()) { + _kLabelExtensions.addOutsideBottomCenteredNodeLabel(node, + "(" + String.join(", ", labelParts) + ")", 8); + } + return figure; + } + + /** + * Creates the visual representation of a startup trigger. + */ + public KEllipse addStartupFigure(KNode node) { + _kNodeExtensions.setMinimalNodeSize(node, 18, 18); + KEllipse figure = _kRenderingExtensions.addEllipse(node); + _kRenderingExtensions.setLineWidth(figure, 1); + _kRenderingExtensions.setBackground(figure, Colors.WHITE); + _linguaFrancaStyleExtensions.noSelectionStyle(figure); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(figure); + return figure; + } + + /** + * Creates the visual representation of a shutdown trigger. + */ + public KPolygon addShutdownFigure(KNode node) { + _kNodeExtensions.setMinimalNodeSize(node, 18, 18); + KPolygon figure = _kRenderingExtensions.addPolygon(node); + _kRenderingExtensions.setLineWidth(figure, 1); + _kRenderingExtensions.setBackground(figure, Colors.WHITE); + _linguaFrancaStyleExtensions.noSelectionStyle(figure); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(figure); + + List pointsToAdd = List.of( + _kRenderingExtensions.createKPosition(LEFT, 0, 0.5f, TOP, 0, 0), + _kRenderingExtensions.createKPosition(RIGHT, 0, 0, TOP, 0, 0.5f), + _kRenderingExtensions.createKPosition(RIGHT, 0, 0.5f, BOTTOM, 0, 0), + _kRenderingExtensions.createKPosition(LEFT, 0, 0, BOTTOM, 0, 0.5f) + ); + + figure.getPoints().addAll(pointsToAdd); + return figure; + } + + /** + * Creates the visual representation of a reactor port. + */ + public KPolygon addTrianglePort(KPort port, boolean multiport) { + port.setSize(8, 8); + + // Create triangle port + KPolygon trianglePort = _kRenderingExtensions.addPolygon(port); + + // Set line width and background color according to multiport or not + float lineWidth = multiport ? 2.2f : 1; + _kRenderingExtensions.setLineWidth(trianglePort, lineWidth); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(trianglePort); + Colors background = multiport ? Colors.WHITE : Colors.BLACK; + _kRenderingExtensions.setBackground(trianglePort, background); + + List pointsToAdd; + if (multiport) { + // Compensate for line width by making triangle smaller + // Do not adjust by port size because this will affect port distribution and cause offsets between parallel connections + pointsToAdd = List.of( + _kRenderingExtensions.createKPosition(LEFT, 0, 0, TOP, 0.6f, 0), + _kRenderingExtensions.createKPosition(RIGHT, 1.2f, 0, TOP, 0, 0.5f), + _kRenderingExtensions.createKPosition(LEFT, 0, 0, BOTTOM, 0.6f, 0) + ); + } else { + pointsToAdd = List.of( + _kRenderingExtensions.createKPosition(LEFT, 0, 0, TOP, 0, 0), + _kRenderingExtensions.createKPosition(RIGHT, 0, 0, TOP, 0, 0.5f), + _kRenderingExtensions.createKPosition(LEFT, 0, 0, BOTTOM, 0, 0) + ); + } + trianglePort.getPoints().addAll(pointsToAdd); + return trianglePort; + } + + /** + * Added a text as collapse expand button. + */ + public KText addTextButton(KContainerRendering container, String text) { + KText textToAdd = _kContainerRenderingExtensions.addText(container, text); + _kRenderingExtensions.setForeground(textToAdd, Colors.BLUE); + _kRenderingExtensions.setFontSize(textToAdd, 8); + _linguaFrancaStyleExtensions.noSelectionStyle(textToAdd); + return textToAdd; + } + + /** + * Creates the triangular line decorator with text. + */ + public KPolygon addActionDecorator(KPolyline line, String text) { + final float size = 18; + + // Create action decorator + KPolygon actionDecorator = _kContainerRenderingExtensions.addPolygon(line); + _kRenderingExtensions.setBackground(actionDecorator, Colors.WHITE); + List pointsToAdd = List.of( + _kRenderingExtensions.createKPosition(LEFT, 0, 0.5f, TOP, 0, 0), + _kRenderingExtensions.createKPosition(RIGHT, 0, 0, BOTTOM, 0, 0), + _kRenderingExtensions.createKPosition(LEFT, 0, 0, BOTTOM, 0, 0) + ); + actionDecorator.getPoints().addAll(pointsToAdd); + + // Set placement data of the action decorator + KDecoratorPlacementData placementData = _kRenderingFactory.createKDecoratorPlacementData(); + placementData.setRelative(0.5f); + placementData.setAbsolute(-size / 2); + placementData.setWidth(size); + placementData.setHeight(size); + placementData.setYOffset(-size * 0.66f); + placementData.setRotateWithLine(true); + actionDecorator.setPlacementData(placementData); + + // Add text to the action decorator + KText textToAdd = _kContainerRenderingExtensions.addText(actionDecorator, text); + _kRenderingExtensions.setFontSize(textToAdd, 8); + _linguaFrancaStyleExtensions.noSelectionStyle(textToAdd); + DiagramSyntheses.suppressSelectability(textToAdd); + _kRenderingExtensions.setPointPlacementData(textToAdd, + _kRenderingExtensions.LEFT, 0, 0.5f, + _kRenderingExtensions.TOP, size * 0.15f, 0.5f, + _kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_CENTRAL, 0, + 0, size, size); + + return actionDecorator; + } + + /** + * Creates the triangular action node with text and ports. + */ + public Pair addActionFigureAndPorts(KNode node, String text) { + final float size = 18; + _kNodeExtensions.setMinimalNodeSize(node, size, size); + KPolygon figure = _kRenderingExtensions.addPolygon(node); + _kRenderingExtensions.setBackground(figure, Colors.WHITE); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(figure); + + List pointsToAdd = List.of( + _kRenderingExtensions.createKPosition(LEFT, 0, 0.5f, TOP, 0, 0), + _kRenderingExtensions.createKPosition(RIGHT, 0, 0, BOTTOM, 0, 0), + _kRenderingExtensions.createKPosition(LEFT, 0, 0, BOTTOM, 0, 0) + ); + figure.getPoints().addAll(pointsToAdd); + + // Add text to the action figure + KText textToAdd = _kContainerRenderingExtensions.addText(figure, text); + _kRenderingExtensions.setFontSize(textToAdd, 8); + _linguaFrancaStyleExtensions.noSelectionStyle(textToAdd); + DiagramSyntheses.suppressSelectability(textToAdd); + _kRenderingExtensions.setPointPlacementData(textToAdd, + _kRenderingExtensions.LEFT, 0, 0.5f, + _kRenderingExtensions.TOP, (size * 0.15f), 0.5f, + _kRenderingExtensions.H_CENTRAL, + _kRenderingExtensions.V_CENTRAL, 0, 0, size, size); + + // Add input port + KPort in = _kPortExtensions.createPort(); + node.getPorts().add(in); + in.setSize(0, 0); + DiagramSyntheses.setLayoutOption(in, CoreOptions.PORT_SIDE, PortSide.WEST); + DiagramSyntheses.setLayoutOption(in, CoreOptions.PORT_BORDER_OFFSET, -size / ((double) 4)); + + // Add output port + KPort out = _kPortExtensions.createPort(); + node.getPorts().add(out); + DiagramSyntheses.setLayoutOption(out, CoreOptions.PORT_SIDE, PortSide.EAST); + DiagramSyntheses.setLayoutOption(out, CoreOptions.PORT_BORDER_OFFSET, -size / ((double) 4)); + return new Pair(in, out); + } + + /** + * Creates and adds an error message figure + */ + public KRectangle addErrorMessage(KNode node, String title, String message) { + // Create figure for error message + KRectangle figure = _kRenderingExtensions.addRectangle(node); + _kRenderingExtensions.setInvisible(figure, true); + + // Add error message box + KRoundedRectangle errMsgBox = _kContainerRenderingExtensions.addRoundedRectangle(figure, 7, 7); + _kContainerRenderingExtensions.setGridPlacement(errMsgBox, 1); + _kRenderingExtensions.setLineWidth(errMsgBox, 2); + _linguaFrancaStyleExtensions.noSelectionStyle(errMsgBox); + + if (title != null) { + // Add title to error message box + KText titleText = _kContainerRenderingExtensions.addText(errMsgBox, title); + _kRenderingExtensions.setFontSize(titleText, 12); + _kRenderingExtensions.setFontBold(titleText, true); + _kRenderingExtensions.setForeground(titleText, Colors.RED); + setGridPlacementDataFromPointToPoint(titleText, + _kRenderingExtensions.LEFT, 8, 0, + _kRenderingExtensions.TOP, 8, 0, + _kRenderingExtensions.RIGHT, 8, 0, + _kRenderingExtensions.BOTTOM, 4, 0); + DiagramSyntheses.suppressSelectability(titleText); + _linguaFrancaStyleExtensions.noSelectionStyle(titleText); + } + + if (message != null) { + // Add message to error message box + KText msgText = _kContainerRenderingExtensions.addText(errMsgBox, message); + if (title != null) { + setGridPlacementDataFromPointToPoint(msgText, + _kRenderingExtensions.LEFT, 8, 0, + _kRenderingExtensions.TOP, 0, 0, + _kRenderingExtensions.RIGHT, 8, 0, + _kRenderingExtensions.BOTTOM, 4, 0); + } else { + setGridPlacementDataFromPointToPoint(msgText, + _kRenderingExtensions.LEFT, 8, 0, + _kRenderingExtensions.TOP, 8, 0, + _kRenderingExtensions.RIGHT, 8, 0, + _kRenderingExtensions.BOTTOM, 8, 0); + } + _linguaFrancaStyleExtensions.noSelectionStyle(msgText); + } + return figure; + } + + public KRoundedRectangle addCommentFigure(KNode node, String message) { + // Create rectangle for comment figure + KRoundedRectangle commentFigure = _kRenderingExtensions.addRoundedRectangle(node, 1, 1, 1); + _kContainerRenderingExtensions.setGridPlacement(commentFigure, 1); + + // Add message + KText text = _kContainerRenderingExtensions.addText(commentFigure, message); + _kRenderingExtensions.setFontSize(text, 6); + setGridPlacementDataFromPointToPoint(text, + _kRenderingExtensions.LEFT, 3, 0, + _kRenderingExtensions.TOP, 3, 0, + _kRenderingExtensions.RIGHT, 3, 0, + _kRenderingExtensions.BOTTOM, 3, 0); + _linguaFrancaStyleExtensions.noSelectionStyle(text); + return commentFigure; + } + + private KRendering setGridPlacementDataFromPointToPoint(KRendering rendering, + PositionReferenceX fPx, float fAbsoluteLR, float fRelativeLR, + PositionReferenceY fPy, float fAbsoluteTB, float fRelativeTB, + PositionReferenceX tPx, float tAbsoluteLR, float tRelativeLR, + PositionReferenceY tPy, float tAbsoluteTB, float tRelativeTB) { + KAreaPlacementData fromPoint = _kRenderingExtensions.from( + _kRenderingExtensions.setGridPlacementData(rendering), + fPx, fAbsoluteLR, fRelativeLR, + fPy, fAbsoluteTB, fRelativeTB); + return _kRenderingExtensions.to(fromPoint, + tPx, tAbsoluteLR, tRelativeLR, + tPy, tAbsoluteTB, tRelativeTB); + } + + + public KPolyline addCommentPolyline(KEdge edge) { + KPolyline polyline = _kEdgeExtensions.addPolyline(edge); + _kRenderingExtensions.setLineWidth(polyline, 1); + _kRenderingExtensions.setLineStyle(polyline, LineStyle.DOT); + return polyline; + } + +} \ No newline at end of file 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 deleted file mode 100644 index 57ad610cea..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.xtend +++ /dev/null @@ -1,666 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis.styles - -import de.cau.cs.kieler.klighd.KlighdConstants -import de.cau.cs.kieler.klighd.kgraph.KEdge -import de.cau.cs.kieler.klighd.kgraph.KNode -import de.cau.cs.kieler.klighd.kgraph.KPort -import de.cau.cs.kieler.klighd.krendering.Arc -import de.cau.cs.kieler.klighd.krendering.Colors -import de.cau.cs.kieler.klighd.krendering.HorizontalAlignment -import de.cau.cs.kieler.klighd.krendering.KContainerRendering -import de.cau.cs.kieler.klighd.krendering.KPolyline -import de.cau.cs.kieler.klighd.krendering.KRendering -import de.cau.cs.kieler.klighd.krendering.KRenderingFactory -import de.cau.cs.kieler.klighd.krendering.KRoundedRectangle -import de.cau.cs.kieler.klighd.krendering.KText -import de.cau.cs.kieler.klighd.krendering.LineStyle -import de.cau.cs.kieler.klighd.krendering.VerticalAlignment -import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared -import de.cau.cs.kieler.klighd.krendering.extensions.KColorExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KEdgeExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KLabelExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KNodeExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KPolylineExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KPortExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceX -import de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceY -import java.util.List -import javax.inject.Inject -import org.eclipse.elk.core.options.CoreOptions -import org.eclipse.elk.core.options.PortSide -import org.eclipse.elk.graph.properties.Property -import org.eclipse.xtend.lib.annotations.Data -import org.lflang.diagram.synthesis.AbstractSynthesisExtensions -import org.lflang.diagram.synthesis.postprocessor.ReactionPortAdjustment -import org.lflang.diagram.synthesis.util.UtilityExtensions -import org.lflang.generator.ReactionInstance -import org.lflang.generator.ReactionInstanceGraph -import org.lflang.generator.ReactorInstance -import org.lflang.generator.TimerInstance - -import static org.lflang.diagram.synthesis.LinguaFrancaSynthesis.* - -import static extension de.cau.cs.kieler.klighd.syntheses.DiagramSyntheses.* - -/** - * Extension class that provides shapes and figures for the Lingua France diagram synthesis. - * - * @author{Alexander Schulz-Rosengarten } - */ -@ViewSynthesisShared -class LinguaFrancaShapeExtensions extends AbstractSynthesisExtensions { - - public static val float REACTION_POINTINESS = 6 // arrow point length - // Property for marking the KContainterRendering in Reactor figures that is supposed to hold the content - public static val REACTOR_CONTENT_CONTAINER = new Property("org.lflang.diagram.synthesis.shapes.reactor.content", false) - - @Inject extension KNodeExtensions - @Inject extension KEdgeExtensions - @Inject extension KPortExtensions - @Inject extension KLabelExtensions - @Inject extension KRenderingExtensions - @Inject extension KContainerRenderingExtensions - @Inject extension KPolylineExtensions - @Inject extension KColorExtensions - @Inject extension LinguaFrancaStyleExtensions - @Inject extension UtilityExtensions - - extension KRenderingFactory = KRenderingFactory.eINSTANCE - - public static val BANK_FIGURE_X_OFFSET_SUM = 6.0f - public static val BANK_FIGURE_Y_OFFSET_SUM = 9.0f - - /** - * Creates the main reactor frame. - */ - def addMainReactorFigure(KNode node, ReactorInstance reactorInstance, String text) { - val padding = SHOW_HYPERLINKS.booleanValue ? 8 : 6 - val figure = node.addRoundedRectangle(8, 8, 1) => [ - setGridPlacement(1) - lineWidth = 1 - foreground = Colors.GRAY - background = Colors.WHITE - boldLineSelectionStyle - ] - - figure.addRectangle() => [ - invisible = true - setGridPlacementData().from(LEFT, padding, 0, TOP, padding, 0).to(RIGHT, padding, 0, BOTTOM, 4, 0) - - addRectangle() => [ // Centered child container - invisible = true - setPointPlacementData(LEFT, 0, 0.5f, TOP, 0, 0.5f, H_CENTRAL, V_CENTRAL, 0, 0, 0, 0) - val placement = setGridPlacement(1) - - addText(text) => [ - suppressSelectability - underlineSelectionStyle - ] - - if (reactorInstance.reactorDefinition.federated) { - addCloudIcon() => [ - setGridPlacementData().from(LEFT, 3, 0, TOP, 0, 0).to(RIGHT, 0, 0, BOTTOM, 0, 0) - ] - placement.numColumns = 2 - - if (reactorInstance.reactorDefinition.host !== null && SHOW_REACTOR_HOST.booleanValue) { - addText(reactorInstance.reactorDefinition.host.toText()) => [ - suppressSelectability - underlineSelectionStyle - setGridPlacementData().from(LEFT, 3, 0, TOP, 0, 0).to(RIGHT, 0, 0, BOTTOM, 0, 0) - ] - placement.numColumns = 3 - } - } - ] - ] - - return figure - } - - /** - * Creates the visual representation of a reactor node - */ - def ReactorFigureComponents addReactorFigure(KNode node, ReactorInstance reactorInstance, String text) { - val padding = SHOW_HYPERLINKS.booleanValue ? 8 : 6 - val style = [ KRoundedRectangle r | - r.lineWidth = 1 - r.foreground = Colors.GRAY - r.background = Colors.GRAY_95 - r.boldLineSelectionStyle - ] - val figure = node.addRoundedRectangle(8, 8, 1) => [ - setGridPlacement(1) - style.apply(it) - setProperty(REACTOR_CONTENT_CONTAINER, true) - ] - - // minimal node size is necessary if no text will be added - val minSize = #[2 * figure.cornerWidth, 2 * figure.cornerHeight] - node.setMinimalNodeSize(minSize.get(0), minSize.get(1)) - - figure.addRectangle() => [ - invisible = true - setGridPlacementData().from(LEFT, padding, 0, TOP, padding, 0).to(RIGHT, padding, 0, BOTTOM, reactorInstance.hasContent ? 4 : padding, 0) - - addRectangle() => [ // Centered child container - invisible = true - setPointPlacementData(LEFT, 0, 0.5f, TOP, 0, 0.5f, H_CENTRAL, V_CENTRAL, 0, 0, 0, 0) - val placement = setGridPlacement(1) - - addText(text) => [ - suppressSelectability - underlineSelectionStyle - ] - - if (!reactorInstance.isRoot && reactorInstance.definition.host !== null) { - addCloudUploadIcon() => [ - setGridPlacementData().from(LEFT, 3, 0, TOP, 0, 0).to(RIGHT, 0, 0, BOTTOM, 0, 0) - ] - placement.numColumns = 2 - - if (SHOW_REACTOR_HOST.booleanValue) { - addText(reactorInstance.definition.host.toText()) => [ - suppressSelectability - underlineSelectionStyle - setGridPlacementData().from(LEFT, 3, 0, TOP, 0, 0).to(RIGHT, 0, 0, BOTTOM, 0, 0) - ] - placement.numColumns = 3 - } - } - ] - ] - - if (reactorInstance.isBank()) { - val bank = newArrayList - val container = node.addInvisibleContainerRendering => [ - // TODO handle unresolved 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 (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 (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) - ] - addRoundedRectangle(8, 8, 1) => [ - style.apply(it) - setAreaPlacementData().from(LEFT, BANK_FIGURE_X_OFFSET_SUM / 3, 0, TOP, BANK_FIGURE_Y_OFFSET_SUM / 3, 0).to(RIGHT, 2 * BANK_FIGURE_X_OFFSET_SUM / 3, 0, BOTTOM, 2 * BANK_FIGURE_Y_OFFSET_SUM / 3, 0) - ] - } - - children += figure // move figure into invisible container (add last to be on top) - figure.setAreaPlacementData().from(LEFT, 0, 0, TOP, 0, 0).to(RIGHT, BANK_FIGURE_X_OFFSET_SUM, 0, BOTTOM, BANK_FIGURE_Y_OFFSET_SUM, 0) - bank.addAll(children) - - addRectangle() => [ - invisible = true - setAreaPlacementData().from(LEFT, 12, 0, BOTTOM, 9, 0).to(RIGHT, 6, 0, BOTTOM, 0.5f, 0) - // Handle unresolved width. - val widthLabel = (reactorInstance.width >= 0)? - Integer.toString(reactorInstance.width) - : "?" - // addText(instance.widthSpec.toText) => [ - addText(widthLabel) => [ - horizontalAlignment = HorizontalAlignment.LEFT - verticalAlignment = VerticalAlignment.BOTTOM - fontSize = 6 - noSelectionStyle - associateWith(reactorInstance.definition.widthSpec) - ] - ] - ] - - return new ReactorFigureComponents(container, figure, bank) - } else { - return new ReactorFigureComponents(figure, figure, #[figure]) - } - } - - /** - * Creates the visual representation of a reaction node - */ - def addReactionFigure(KNode node, ReactionInstance reaction) { - val minHeight = 22 - val minWidth = 45 - val reactor = reaction.parent - node.setMinimalNodeSize(minWidth, minHeight) - - val baseShape = node.addPolygon() => [ - associateWith(reaction) - - // style - lineWidth = 1 - foreground = Colors.GRAY_45 - background = Colors.GRAY_65 - boldLineSelectionStyle() - - points += #[ - createKPosition(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.TOP, 0, 0), - createKPosition(PositionReferenceX.RIGHT, REACTION_POINTINESS, 0, PositionReferenceY.TOP, 0, 0), - createKPosition(PositionReferenceX.RIGHT, 0, 0, PositionReferenceY.TOP, 0, 0.5f), - createKPosition(PositionReferenceX.RIGHT, REACTION_POINTINESS, 0, PositionReferenceY.BOTTOM, 0, 0), - createKPosition(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.BOTTOM, 0, 0), - createKPosition(PositionReferenceX.LEFT, REACTION_POINTINESS, 0, PositionReferenceY.BOTTOM, 0, 0.5f) - ] - - styles.head.modifierId = ReactionPortAdjustment.ID // This hack will adjust the port position after layout - ] - - val contentContainer = baseShape.addRectangle() => [ - associateWith(reaction) - invisible = true - setPointPlacementData(LEFT, REACTION_POINTINESS, 0, TOP, 0, 0, H_LEFT, V_TOP, REACTION_POINTINESS, 0, minWidth - REACTION_POINTINESS * 2, minHeight) - gridPlacement = 1 - ] - - if (reactor.reactions.size > 1) { - contentContainer.addText(Integer.toString(reactor.reactions.indexOf(reaction) + 1)) => [ - fontBold = true - noSelectionStyle - suppressSelectability - ] - } - - // optional reaction level - 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. - 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 - val hasCode = SHOW_REACTION_CODE.booleanValue && !reaction.definition.code.body.nullOrEmpty - if (hasCode) { - contentContainer.addText(reaction.definition.code.trimCode) => [ - associateWith(reaction) - fontSize = 6 - fontName = KlighdConstants.DEFAULT_MONOSPACE_FONT_NAME - noSelectionStyle() - horizontalAlignment = HorizontalAlignment.LEFT - verticalAlignment = VerticalAlignment.TOP - setGridPlacementData().from(LEFT, 5, 0, TOP, 5, 0).to(RIGHT, 5, 0, BOTTOM, 5, 0) - ] - } - - // TODO improve default check - if (reaction.declaredDeadline !== null) { - val hasDeadlineCode = SHOW_REACTION_CODE.booleanValue && !reaction.definition.deadline.code.body.nullOrEmpty - if (hasCode || hasDeadlineCode) { - contentContainer.addHorizontalLine(0) => [ - setGridPlacementData().from(LEFT, 5, 0, TOP, 3, 0).to(RIGHT, 5, 0, BOTTOM, 6, 0) - ] - } - - // delay with stopwatch - val labelContainer = contentContainer.addRectangle() => [ - invisible = true - setGridPlacementData().from(LEFT, hasDeadlineCode ? 0 : -REACTION_POINTINESS * 0.5f, 0, TOP, 0, reactor.reactions.size > 1 || hasCode || hasDeadlineCode ? 0 : 0.5f).to(RIGHT, 0, 0, BOTTOM, 0, 0).setHorizontalAlignment(HorizontalAlignment.LEFT) - ] - labelContainer.addStopwatchFigure() => [ - setLeftTopAlignedPointPlacementData(0, 0, 0, 0) - ] - labelContainer.addText(reaction.declaredDeadline.maxDelay.toString) => [ - associateWith(reaction.definition.deadline.delay) - foreground = Colors.BROWN - fontBold = true - fontSize = 7 - underlineSelectionStyle() - setLeftTopAlignedPointPlacementData(15, 0, 0, 0) - ] - - // optional code content - if (hasDeadlineCode) { - contentContainer.addText(reaction.definition.deadline.code.trimCode) => [ - associateWith(reaction.deadline) - foreground = Colors.BROWN - fontSize = 6 - fontName = KlighdConstants.DEFAULT_MONOSPACE_FONT_NAME - setGridPlacementData().from(LEFT, 5, 0, TOP, 0, 0).to(RIGHT, 5, 0, BOTTOM, 5, 0) - horizontalAlignment = HorizontalAlignment.LEFT - noSelectionStyle() - ] - } - } - - return baseShape - } - - /** - * Stopwatch figure for deadlines. - */ - def addStopwatchFigure(KContainerRendering parent) { - val size = 12 - val container = parent.addRectangle() => [ - invisible = true - setPointPlacementData(LEFT, 0, 0, TOP, 0, 0, H_LEFT, V_TOP, 0, 0, size, size) - ] - container.addPolyline(2, - #[ - createKPosition(PositionReferenceX.LEFT, 3, 0.5f, PositionReferenceY.TOP, -2, 0), - createKPosition(PositionReferenceX.LEFT, -3, 0.5f, PositionReferenceY.TOP, -2, 0) - ] - ) => [ - foreground = Colors.BROWN - ] - container.addPolyline(2, - #[ - createKPosition(PositionReferenceX.LEFT, 0, 0.5f, PositionReferenceY.TOP, -2, 0), - createKPosition(PositionReferenceX.LEFT, 0, 0.5f, PositionReferenceY.TOP, 1, 0) - ] - ) => [ - foreground = Colors.BROWN - ] - val body = container.addEllipse() => [ - lineWidth = 1 - foreground = Colors.BROWN - setPointPlacementData(LEFT, 0, 0, TOP, 0, 0, H_LEFT, V_TOP, 0, 0, size, size) - noSelectionStyle() - ] - body.addArc() => [ - startAngle = -20 - arcAngle = 110 - arcType = Arc.PIE - lineWidth = 0 - background = Colors.BROWN - setPointPlacementData(LEFT, 2, 0, TOP, 2, 0, H_LEFT, V_TOP, 2, 2, size - 4, size - 4) - noSelectionStyle() - ] - - return container - } - - /** - * Creates the visual representation of a timer node - */ - def addTimerFigure(KNode node, TimerInstance timer) { - node.setMinimalNodeSize(30, 30) - - val figure = node.addEllipse => [ - lineWidth = 1 - background = Colors.GRAY_95 - noSelectionStyle() - boldLineSelectionStyle() - ] - - figure.addPolyline(1, - #[ - createKPosition(PositionReferenceX.LEFT, 0, 0.5f, PositionReferenceY.TOP, 0 , 0.1f), - createKPosition(PositionReferenceX.LEFT, 0, 0.5f, PositionReferenceY.TOP, 0 , 0.5f), - createKPosition(PositionReferenceX.LEFT, 0, 0.7f, PositionReferenceY.TOP, 0 , 0.7f) - ] - ).boldLineSelectionStyle - - val labelParts = newArrayList - if (timer.offset !== TimerInstance.DEFAULT_OFFSET && timer.offset !== null) { - labelParts += timer.offset.toString - } - if (timer.period !== TimerInstance.DEFAULT_PERIOD && timer.period !== null) { - if (timer.offset === TimerInstance.DEFAULT_OFFSET) { - labelParts += timer.offset.toString - } - labelParts += timer.period.toString - } - if (!labelParts.empty) { - node.addOutsideBottomCenteredNodeLabel(labelParts.join("(", ", ", ")")[it], 8) - } - - return figure - } - - /** - * Creates the visual representation of a startup trigger. - */ - def addStartupFigure(KNode node) { - node.setMinimalNodeSize(18, 18) - - val figure = node.addEllipse => [ - lineWidth = 1 - background = Colors.WHITE - noSelectionStyle() - boldLineSelectionStyle() - ] - - return figure - } - - /** - * Creates the visual representation of a shutdown trigger. - */ - def addShutdownFigure(KNode node) { - node.setMinimalNodeSize(18, 18) - - val figure = node.addPolygon => [ - lineWidth = 1 - background = Colors.WHITE - noSelectionStyle() - boldLineSelectionStyle() - ] - figure.points += #[ - createKPosition(PositionReferenceX.LEFT, 0, 0.5f, PositionReferenceY.TOP, 0 , 0), - createKPosition(PositionReferenceX.RIGHT, 0, 0, PositionReferenceY.TOP, 0 , 0.5f), - createKPosition(PositionReferenceX.RIGHT, 0, 0.5f, PositionReferenceY.BOTTOM, 0 , 0), - createKPosition(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.BOTTOM, 0 , 0.5f) - ] - - return figure - } - - /** - * Creates the visual representation of a reactor port. - */ - def addTrianglePort(KPort port, boolean multiport) { - port.setSize(8, 8) - port.addPolygon() => [ - lineWidth = multiport ? 2.2f : 1 - boldLineSelectionStyle() - background = multiport ? Colors.WHITE : Colors.BLACK - if (multiport) { - // Compensate for line width by making triangle smaller - // Do not adjust by port size because this will affect port distribution and cause offsets between parallel connections - points += #[ - createKPosition(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.TOP, 0.6f , 0), - createKPosition(PositionReferenceX.RIGHT, 1.2f, 0, PositionReferenceY.TOP, 0 , 0.5f), - createKPosition(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.BOTTOM, 0.6f , 0) - ] - } else { - points += #[ - createKPosition(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.TOP, 0 , 0), - createKPosition(PositionReferenceX.RIGHT, 0, 0, PositionReferenceY.TOP, 0 , 0.5f), - createKPosition(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.BOTTOM, 0 , 0) - ] - } - ] - } - - /** - * Added a text as collapse expand button. - */ - def KText addTextButton(KContainerRendering container, String text) { - container.addText(text) => [ - foreground = Colors.BLUE - fontSize = 8 - noSelectionStyle() - ] - } - - /** - * Creates the triangular line decorator with text. - */ - def addActionDecorator(KPolyline line, String text) { - val float size = 18 - line.addPolygon() => [ - background = Colors.WHITE - - points += #[ - createKPosition(PositionReferenceX.LEFT, 0, 0.5f, PositionReferenceY.TOP, 0, 0), - createKPosition(PositionReferenceX.RIGHT, 0, 0, PositionReferenceY.BOTTOM, 0, 0), - createKPosition(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.BOTTOM, 0, 0) - ] - - placementData = createKDecoratorPlacementData => [ - relative = 0.5f - absolute = -size / 2 - width = size - height = size - setYOffset(-size * 0.66f) - rotateWithLine = true - ] - - addText(text) => [ - fontSize = 8 - noSelectionStyle() - suppressSelectability() - - setPointPlacementData(LEFT, 0, 0.5f, TOP, size * 0.15f, 0.5f, H_CENTRAL, V_CENTRAL, 0, 0, size, size) - ] - ] - } - - /** - * Creates the triangular action node with text and ports. - */ - def Pair addActionFigureAndPorts(KNode node, String text) { - val float size = 18 - node.setMinimalNodeSize(size, size) - - val figure = node.addPolygon() => [ - background = Colors.WHITE - boldLineSelectionStyle - - points += #[ - createKPosition(PositionReferenceX.LEFT, 0, 0.5f, PositionReferenceY.TOP, 0, 0), - createKPosition(PositionReferenceX.RIGHT, 0, 0, PositionReferenceY.BOTTOM, 0, 0), - createKPosition(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.BOTTOM, 0, 0) - ] - - addText(text) => [ - fontSize = 8 - noSelectionStyle() - suppressSelectability() - - setPointPlacementData(LEFT, 0, 0.5f, TOP, size * 0.15f, 0.5f, H_CENTRAL, V_CENTRAL, 0, 0, size, size) - ] - ] - - val in = createPort - node.ports += in - in.setSize(0, 0) // invisible - in.setLayoutOption(CoreOptions.PORT_SIDE, PortSide.WEST) - in.setLayoutOption(CoreOptions.PORT_BORDER_OFFSET, - size / 4 as double) - - val out = createPort - node.ports += out - out.setSize(0, 0) // invisible - out.setLayoutOption(CoreOptions.PORT_SIDE, PortSide.EAST) - out.setLayoutOption(CoreOptions.PORT_BORDER_OFFSET, - size / 4 as double) - - return new Pair(in, out) - } - - /** - * Creates and adds an error message figure - */ - def addErrorMessage(KNode node, String title, String message) { - node.addRectangle() => [ - invisible = true - addRoundedRectangle(7, 7) => [ - setGridPlacement(1) - lineWidth = 2 - noSelectionStyle() - - // title - if (title !== null) { - addText(title) => [ - fontSize = 12 - setFontBold = true - foreground = Colors.RED - setGridPlacementData().from(LEFT, 8, 0, TOP, 8, 0).to(RIGHT, 8, 0, BOTTOM, 4, 0) - suppressSelectability() - noSelectionStyle() - ] - } - - // message - if (message !== null) { - addText(message) => [ - if (title !== null) { - setGridPlacementData().from(LEFT, 8, 0, TOP, 0, 0).to(RIGHT, 8, 0, BOTTOM, 4, 0) - } else { - setGridPlacementData().from(LEFT, 8, 0, TOP, 8, 0).to(RIGHT, 8, 0, BOTTOM, 8, 0) - } - noSelectionStyle() - ] - } - ] - ] - } - - def KRoundedRectangle addCommentFigure(KNode node, String message) { - node.addRoundedRectangle(1, 1, 1) => [ - gridPlacement = 1 - addText(message) => [ - fontSize = 6 - setGridPlacementData().from(LEFT, 3, 0, TOP, 3, 0).to(RIGHT, 3, 0, BOTTOM, 3, 0) - noSelectionStyle() - ] - ] - } - - def KPolyline addCommentPolyline(KEdge edge) { - edge.addPolyline => [ - lineWidth = 1 - lineStyle = LineStyle.DOT - ] - } - -} - -@Data -class ReactorFigureComponents { - val KContainerRendering outer - val KContainerRendering reactor - val List figures -} \ No newline at end of file diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaStyleExtensions.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaStyleExtensions.java new file mode 100644 index 0000000000..e31e3acdc5 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaStyleExtensions.java @@ -0,0 +1,456 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis.styles; + +import de.cau.cs.kieler.klighd.internal.util.KlighdInternalProperties; +import de.cau.cs.kieler.klighd.kgraph.KEdge; +import de.cau.cs.kieler.klighd.kgraph.KLabel; +import de.cau.cs.kieler.klighd.kgraph.KPort; +import de.cau.cs.kieler.klighd.krendering.Colors; +import de.cau.cs.kieler.klighd.krendering.KContainerRendering; +import de.cau.cs.kieler.klighd.krendering.KDecoratorPlacementData; +import de.cau.cs.kieler.klighd.krendering.KEllipse; +import de.cau.cs.kieler.klighd.krendering.KPolygon; +import de.cau.cs.kieler.klighd.krendering.KPolyline; +import de.cau.cs.kieler.klighd.krendering.KRectangle; +import de.cau.cs.kieler.klighd.krendering.KRendering; +import de.cau.cs.kieler.klighd.krendering.KRenderingFactory; +import de.cau.cs.kieler.klighd.krendering.KRoundedRectangle; +import de.cau.cs.kieler.klighd.krendering.KSpline; +import de.cau.cs.kieler.klighd.krendering.KText; +import de.cau.cs.kieler.klighd.krendering.Underline; +import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; +import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KPolylineExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions; +import de.cau.cs.kieler.klighd.labels.decoration.IDecoratorRenderingProvider; +import de.cau.cs.kieler.klighd.labels.decoration.LabelDecorationConfigurator; +import java.util.List; + +import javax.inject.Inject; +import org.eclipse.elk.core.math.ElkPadding; +import org.eclipse.elk.graph.properties.Property; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.xbase.lib.Extension; +import org.lflang.diagram.synthesis.AbstractSynthesisExtensions; + +import static de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceX.*; +import static de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceY.*; + +/** + * Extension class that provides styles and coloring for the Lingua France diagram synthesis. + * + * @author{Alexander Schulz-Rosengarten } + */ +@ViewSynthesisShared +public class LinguaFrancaStyleExtensions extends AbstractSynthesisExtensions { + + private static final Property LABEL_PARENT_BACKGROUND = new Property<>( + "org.lflang.linguafranca.diagram.synthesis.styles.label.parent.background", Colors.WHITE); + + @Inject + @Extension + private KRenderingExtensions _kRenderingExtensions; + @Inject + @Extension + private KContainerRenderingExtensions _kContainerRenderingExtensions; + @Inject + @Extension + private KPolylineExtensions _kPolylineExtensions; + @Extension + private KRenderingFactory _kRenderingFactory = KRenderingFactory.eINSTANCE; + + public KRendering noSelectionStyle(KRendering r) { + return _kRenderingExtensions.setSelectionTextStrikeout(r, false); + } + + public KRendering underlineSelectionStyle(KRendering r) { + return _kRenderingExtensions.setSelectionTextUnderline(r, Underline.SINGLE); + } + + public KRendering boldLineSelectionStyle(KRendering r) { + float lineWidthValue = _kRenderingExtensions.getLineWidthValue(r); + return _kRenderingExtensions.setSelectionLineWidth(r, lineWidthValue * 2); + } + + public KText boldTextSelectionStyle(KText t) { + return _kRenderingExtensions.setSelectionFontBold(t, true); + } + + public void errorStyle(KRendering r) { + _kRenderingExtensions.setForeground(r, Colors.RED); + _kRenderingExtensions.setLineWidth(r, 2); + _kRenderingExtensions.setSelectionLineWidth(r, 3); + + if (r.eContainer() instanceof KEdge || r.eContainer() instanceof KPort) { // also color potential arrow heads + _kRenderingExtensions.setBackground(r, Colors.RED); + _kRenderingExtensions.getBackground(r).setPropagateToChildren(true); + _kRenderingExtensions.getForeground(r).setPropagateToChildren(true); + _kRenderingExtensions.getLineWidth(r).setPropagateToChildren(true); + } + } + + public void commentStyle(KRendering r) { + _kRenderingExtensions.setForeground(r, Colors.LIGHT_GOLDENROD); + _kRenderingExtensions.setBackground(r, Colors.PALE_GOLDENROD); + _kRenderingExtensions.setLineWidth(r, 1); + _kRenderingExtensions.setSelectionLineWidth(r, 2); + + if (r.eContainer() instanceof KEdge) { // also color potential arrow heads + _kRenderingExtensions.setBackground(r, Colors.LIGHT_GOLDENROD); + _kRenderingExtensions.getBackground(r).setPropagateToChildren(true); + _kRenderingExtensions.getForeground(r).setPropagateToChildren(true); + _kRenderingExtensions.getLineWidth(r).setPropagateToChildren(true); + } + } + + private static final int CLOUD_WIDTH = 20; + public KContainerRendering addCloudIcon(final KContainerRendering parent) { + KRectangle figure = _kContainerRenderingExtensions.addRectangle(parent); + _kRenderingExtensions.setInvisible(figure, true); + + KRoundedRectangle roundRectangle = _kContainerRenderingExtensions.addRoundedRectangle( + figure, + CLOUD_WIDTH / 7, + CLOUD_WIDTH / 7 + ); + _kRenderingExtensions.setBackground(roundRectangle, Colors.GRAY); + _kRenderingExtensions.setForeground(roundRectangle, Colors.GRAY); + _kRenderingExtensions.setPointPlacementData(roundRectangle, + _kRenderingExtensions.LEFT, 2, 0, + _kRenderingExtensions.TOP, 0, 0.5f, + _kRenderingExtensions.H_LEFT, _kRenderingExtensions.V_TOP, 0, + 0, CLOUD_WIDTH, CLOUD_WIDTH / 3); + + KEllipse childEllipse = _kContainerRenderingExtensions.addEllipse(figure); + _kRenderingExtensions.setBackground(childEllipse, Colors.GRAY); + _kRenderingExtensions.setForeground(childEllipse, Colors.GRAY); + _kRenderingExtensions.setPointPlacementData(childEllipse, + _kRenderingExtensions.LEFT, 0, 0f, + _kRenderingExtensions.TOP, 0, 0.38f, + _kRenderingExtensions.H_LEFT, _kRenderingExtensions.V_TOP, 0, + 0, CLOUD_WIDTH / 2.5f, CLOUD_WIDTH / 2.5f); + + childEllipse = _kContainerRenderingExtensions.addEllipse(figure); + _kRenderingExtensions.setBackground(childEllipse, Colors.GRAY); + _kRenderingExtensions.setForeground(childEllipse, Colors.GRAY); + _kRenderingExtensions.setPointPlacementData(childEllipse, + _kRenderingExtensions.LEFT, 0, 0.5f, + _kRenderingExtensions.TOP, 0, 0.25f, + _kRenderingExtensions.H_RIGHT, _kRenderingExtensions.V_TOP, 0, + 0, CLOUD_WIDTH / 3f, CLOUD_WIDTH / 3f); + + childEllipse = _kContainerRenderingExtensions.addEllipse(figure); + _kRenderingExtensions.setBackground(childEllipse, Colors.GRAY); + _kRenderingExtensions.setForeground(childEllipse, Colors.GRAY); + _kRenderingExtensions.setPointPlacementData(childEllipse, + _kRenderingExtensions.LEFT, 0, 0.4f, + _kRenderingExtensions.TOP, CLOUD_WIDTH / 10, 0, + _kRenderingExtensions.H_LEFT, _kRenderingExtensions.V_TOP, 0, + 0, CLOUD_WIDTH / 2, CLOUD_WIDTH / 2); + + return figure; + } + + public KRendering addCloudUploadIcon(KContainerRendering parent) { + KContainerRendering cloudIcon = addCloudIcon(parent); + KPolygon cloudPolygon = _kContainerRenderingExtensions.addPolygon(cloudIcon); + _kRenderingExtensions.setBackground(cloudPolygon, Colors.WHITE); + _kRenderingExtensions.setForeground(cloudPolygon, Colors.WHITE); + cloudPolygon.getPoints().addAll( + List.of( + _kRenderingExtensions.createKPosition(LEFT, -1.5f, 0.5f, TOP, CLOUD_WIDTH / 3, 0.5f), + _kRenderingExtensions.createKPosition(LEFT, -1.5f, 0.5f, TOP, 0, 0.58f), + _kRenderingExtensions.createKPosition(LEFT, (-4), 0.5f, TOP, 0, 0.58f), + _kRenderingExtensions.createKPosition(LEFT, 0, 0.5f, TOP, 0, 0.35f), + _kRenderingExtensions.createKPosition(LEFT, 4, 0.5f, TOP, 0, 0.58f), + _kRenderingExtensions.createKPosition(LEFT, 1.5f, 0.5f, TOP, 0, 0.58f), + _kRenderingExtensions.createKPosition(LEFT, 1.5f, 0.5f, TOP, CLOUD_WIDTH / 3, 0.5f) + ) + ); + return cloudIcon; + } + + private static LabelDecorationConfigurator _onEdgeLabelConfigurator; // ONLY for use in applyOnEdgeStyle + public void applyOnEdgeStyle(KLabel label) { + if (_onEdgeLabelConfigurator == null) { + LabelDecorationConfigurator configurator = LabelDecorationConfigurator.create().withInlineLabels(true); + _onEdgeLabelConfigurator = configurator.withLabelTextRenderingProvider( + (KContainerRendering container, KLabel klabel) -> { + KText kText = _kRenderingFactory.createKText(); + _kRenderingExtensions.setFontSize(kText, 9); + container.getChildren().add(kText); + return kText; + }); + } + _onEdgeLabelConfigurator.applyTo(label); + } + + private static LabelDecorationConfigurator _onEdgeDelayLabelConfigurator; // ONLY for use in applyOnEdgeDelayStyle + public void applyOnEdgeDelayStyle(KLabel label) { + if (_onEdgeDelayLabelConfigurator == null) { + LabelDecorationConfigurator configurator = LabelDecorationConfigurator.create().withInlineLabels(true); + configurator = configurator.withLabelTextRenderingProvider( + (KContainerRendering container, KLabel klabel) -> { + KText kText = _kRenderingFactory.createKText(); + _kRenderingExtensions.setFontSize(kText, 8); + boldTextSelectionStyle(kText); + kText.setProperty(KlighdInternalProperties.MODEL_ELEMEMT, + klabel.getProperty(KlighdInternalProperties.MODEL_ELEMEMT)); + container.getChildren().add(kText); + return kText; + } + ); + configurator = configurator.addDecoratorRenderingProvider(new IDecoratorRenderingProvider() { + @Override + public ElkPadding createDecoratorRendering( + KContainerRendering container, KLabel label, + LabelDecorationConfigurator.LayoutMode layoutMode) { + ElkPadding padding = new ElkPadding(); + padding.left = 2; + padding.right = 2; + padding.bottom = Math.max(padding.bottom, 1); + + KPolygon polygon = _kRenderingFactory.createKPolygon(); + _kRenderingExtensions.from(polygon, LEFT, (-2), 0, BOTTOM, 0, 0); + _kRenderingExtensions.to(polygon, LEFT, 2, 0, TOP, 0, 0); + _kRenderingExtensions.to(polygon, RIGHT, (-2), 0, TOP, 0, 0); + _kRenderingExtensions.to(polygon, RIGHT, 2, 0, BOTTOM, 0, 0); + _kRenderingExtensions.setBackground(polygon, Colors.WHITE); + _kRenderingExtensions.setForeground(polygon, Colors.WHITE); + container.getChildren().add(polygon); + + KPolyline polyline = _kRenderingFactory.createKPolyline(); + _kRenderingExtensions.from(polyline, LEFT, (-2), 0, BOTTOM, 0, 0); + _kRenderingExtensions.to(polyline, LEFT, 2, 0, TOP, 0, 0); + container.getChildren().add(polyline); + + polyline = _kRenderingFactory.createKPolyline(); + _kRenderingExtensions.from(polyline, RIGHT, 2, 0, BOTTOM, 0, 0); + _kRenderingExtensions.to(polyline, RIGHT, (-2), 0, TOP, 0, 0); + container.getChildren().add(polyline); + + return padding; + } + }); + _onEdgeDelayLabelConfigurator = configurator; + } + _onEdgeDelayLabelConfigurator.applyTo(label); + } + + private static LabelDecorationConfigurator _onEdgePysicalDelayLabelConfigurator; // ONLY for use in applyOnEdgePysicalDelayStyle + public void applyOnEdgePysicalDelayStyle(KLabel label, Colors parentBackgroundColor) { + if (_onEdgePysicalDelayLabelConfigurator == null) { + LabelDecorationConfigurator configurator = LabelDecorationConfigurator.create().withInlineLabels(true); + configurator = configurator.withLabelTextRenderingProvider( + (KContainerRendering container, KLabel klabel) -> { + KText kText = _kRenderingFactory.createKText(); + _kRenderingExtensions.setFontSize(kText, 8); + boldTextSelectionStyle(kText); + kText.setProperty(KlighdInternalProperties.MODEL_ELEMEMT, + klabel.getProperty(KlighdInternalProperties.MODEL_ELEMEMT)); + container.getChildren().add(kText); + return kText; + } + ); + configurator = configurator.addDecoratorRenderingProvider(new IDecoratorRenderingProvider() { + @Override + public ElkPadding createDecoratorRendering( + KContainerRendering container, + KLabel label, + LabelDecorationConfigurator.LayoutMode layoutMode) { + ElkPadding padding = new ElkPadding(); + padding.left = 8; + padding.right = 16; + padding.bottom = Math.max(padding.bottom, 1); + + KPolygon polygon = _kRenderingFactory.createKPolygon(); + _kRenderingExtensions.from(polygon, LEFT, 0, 0, BOTTOM, 0, 0.5f); + _kRenderingExtensions.to(polygon, LEFT, 0, 0, TOP, 1, 0.5f); + _kRenderingExtensions.to(polygon, RIGHT, 0, 0, TOP, 1, 0.5f); + _kRenderingExtensions.to(polygon, RIGHT, 0, 0, BOTTOM, 0, 0.5f); + _kRenderingExtensions.setBackground(polygon, label.getProperty(LABEL_PARENT_BACKGROUND)); + _kRenderingExtensions.setForeground(polygon, label.getProperty(LABEL_PARENT_BACKGROUND)); + container.getChildren().add(polygon); + + KSpline kSpline = _kRenderingFactory.createKSpline(); + _kRenderingExtensions.from(kSpline, LEFT, -0.66f, 0, BOTTOM, -0.5f, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 1, 0, BOTTOM, -0.5f, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 3, 0, BOTTOM, 8, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 5, 0, BOTTOM, 0, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 5.5f, 0, BOTTOM, -1.5f, 0.5f); + container.getChildren().add(kSpline); + + kSpline = _kRenderingFactory.createKSpline(); + _kRenderingExtensions.from(kSpline, RIGHT, 15f, 0, BOTTOM, 3.5f, 0.5f); + _kRenderingExtensions.to(kSpline, RIGHT, 14f, 0, BOTTOM, 0, 0.5f); + _kRenderingExtensions.to(kSpline, RIGHT, 11, 0, BOTTOM, -8, 0.5f); + _kRenderingExtensions.to(kSpline, RIGHT, 9, 0, BOTTOM, 0, 0.5f); + _kRenderingExtensions.to(kSpline, RIGHT, 7, 0, BOTTOM, 8, 0.5f); + _kRenderingExtensions.to(kSpline, RIGHT, 4f, 0, BOTTOM, 2, 0.5f); + _kRenderingExtensions.to(kSpline, RIGHT, 1.5f, 0, BOTTOM, 0.5f, 0.5f); + _kRenderingExtensions.to(kSpline, RIGHT, 0.2f, 0, BOTTOM, -0.5f, 0.5f); + _kRenderingExtensions.to(kSpline, RIGHT, -0.7f, 0, BOTTOM, -0.5f, 0.5f); + container.getChildren().add(kSpline); + + polygon = _kRenderingFactory.createKPolygon(); + _kRenderingExtensions.from(polygon, LEFT, 4, 0, BOTTOM, 0, 0); + _kRenderingExtensions.to(polygon, LEFT, 8, 0, TOP, 0, 0); + _kRenderingExtensions.to(polygon, RIGHT, 12, 0, TOP, 0, 0); + _kRenderingExtensions.to(polygon, RIGHT, 16, 0, BOTTOM, 0, 0); + _kRenderingExtensions.setBackground(polygon, Colors.WHITE); + _kRenderingExtensions.setForeground(polygon, Colors.WHITE); + container.getChildren().add(polygon); + + KPolyline polyline = _kRenderingFactory.createKPolyline(); + _kRenderingExtensions.from(polyline, LEFT, 4, 0, BOTTOM, 0, 0); + _kRenderingExtensions.to(polyline, LEFT, 8, 0, TOP, 0, 0); + container.getChildren().add(polyline); + + polyline = _kRenderingFactory.createKPolyline(); + _kRenderingExtensions.from(polyline, RIGHT, 16, 0, BOTTOM, 0, 0); + _kRenderingExtensions.to(polyline, RIGHT, 12, 0, TOP, 0, 0); + container.getChildren().add(polyline); + + return padding; + } + }); + _onEdgePysicalDelayLabelConfigurator = configurator; + } + label.setProperty(LABEL_PARENT_BACKGROUND, parentBackgroundColor); + _onEdgePysicalDelayLabelConfigurator.applyTo(label); + } + + private static LabelDecorationConfigurator _onEdgePysicalLabelConfigurator; // ONLY for use in applyOnEdgePysicalStyle + public void applyOnEdgePysicalStyle(KLabel label, Colors parentBackgroundColor) { + if (_onEdgePysicalLabelConfigurator == null) { + LabelDecorationConfigurator configurator = LabelDecorationConfigurator.create().withInlineLabels(true); + configurator = configurator.withLabelTextRenderingProvider( + (KContainerRendering container, KLabel klabel) -> { + KText kText = _kRenderingFactory.createKText(); + _kRenderingExtensions.setInvisible(kText, true); + container.getChildren().add(kText); + return kText; + } + ); + configurator = configurator.addDecoratorRenderingProvider(new IDecoratorRenderingProvider() { + @Override + public ElkPadding createDecoratorRendering(final KContainerRendering container, final KLabel label, final LabelDecorationConfigurator.LayoutMode layoutMode) { + ElkPadding padding = new ElkPadding(); + padding.left = 3; + padding.right = 3; + padding.bottom = Math.max(padding.bottom, 1); + KPolygon polygon = _kRenderingFactory.createKPolygon(); + _kRenderingExtensions.from(polygon, LEFT, 0, 0, BOTTOM, 0, 0.5f); + _kRenderingExtensions.to(polygon, LEFT, 0, 0, TOP, 1, 0.5f); + _kRenderingExtensions.to(polygon, RIGHT, 0, 0, TOP, 1, 0.5f); + _kRenderingExtensions.to(polygon, RIGHT, 0, 0, BOTTOM, 0, 0.5f); + _kRenderingExtensions.setBackground(polygon, label.getProperty(LABEL_PARENT_BACKGROUND)); + _kRenderingExtensions.setForeground(polygon, label.getProperty(LABEL_PARENT_BACKGROUND)); + container.getChildren().add(polygon); + + KSpline kSpline = _kRenderingFactory.createKSpline(); + _kRenderingExtensions.from(kSpline, LEFT, (-0.66f), 0, BOTTOM, (-0.5f), 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 1, 0, BOTTOM, (-0.5f), 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.1f, BOTTOM, 8, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.2f, BOTTOM, 0, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.3f, BOTTOM, (-8), 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.4f, BOTTOM, 0, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.45f, BOTTOM, 4f, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.5f, BOTTOM, 8, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.55f, BOTTOM, 4f, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.6f, BOTTOM, 0, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.65f, BOTTOM, (-4), 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.7f, BOTTOM, (-8), 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.8f, BOTTOM, (-4), 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0, 0.9f, BOTTOM, 0, 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, (-1), 1, BOTTOM, (-0.5f), 0.5f); + _kRenderingExtensions.to(kSpline, LEFT, 0.66f, 1, BOTTOM, (-0.5f), 0.5f); + container.getChildren().add(kSpline); + return padding; + } + }); + _onEdgePysicalLabelConfigurator = configurator; + } + label.setProperty(LABEL_PARENT_BACKGROUND, parentBackgroundColor); + _onEdgePysicalLabelConfigurator.applyTo(label); + } + + public KRendering addFixedTailArrowDecorator(KPolyline pl) { + KRendering head = _kPolylineExtensions.addTailArrowDecorator(pl); + KDecoratorPlacementData placement = _kRenderingFactory.createKDecoratorPlacementData(); + placement.setRotateWithLine(true); + placement.setRelative(0f); + placement.setAbsolute(2f); + placement.setWidth(8); + placement.setHeight(6); + placement.setXOffset(-3f); + placement.setYOffset(-4f); + head.setPlacementData(placement); + return head; + } + + public void addArrayDecorator(KEdge edge, Integer size) { + final KRendering line = _kRenderingExtensions.getKRendering(edge); + if (line instanceof KPolyline) { + KDecoratorPlacementData placement = _kRenderingFactory.createKDecoratorPlacementData(); + placement.setRotateWithLine(true); + placement.setRelative(0f); + placement.setAbsolute(6f); + + KPolyline slash = _kContainerRenderingExtensions.addChild( + (KContainerRendering) line, + _kRenderingFactory.createKPolyline()); + slash.getPoints().add( + _kRenderingExtensions.createKPosition( + _kRenderingExtensions.RIGHT, 0, 0, + _kRenderingExtensions.TOP, 0, 0) + ); + slash.getPoints().add( + _kRenderingExtensions.createKPosition( + _kRenderingExtensions.LEFT, 0, 0, + _kRenderingExtensions.BOTTOM, 0, 0) + ); + KDecoratorPlacementData slashPlacement = EcoreUtil.copy(placement); + slashPlacement.setWidth(5); + slashPlacement.setHeight(10); + slashPlacement.setYOffset(-5f); + slash.setPlacementData(slashPlacement); + + if (size != null) { + KText num = _kContainerRenderingExtensions.addChild( + (KContainerRendering) line, + _kRenderingFactory.createKText() + ); + num.setText(size.toString()); + _kRenderingExtensions.setFontSize(num, 5); + noSelectionStyle(num); + KDecoratorPlacementData numPlacement = EcoreUtil.copy(placement); + numPlacement.setXOffset(2f); + num.setPlacementData(numPlacement); + } + } + } +} \ No newline at end of file diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaStyleExtensions.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaStyleExtensions.xtend deleted file mode 100644 index a4e3dff86c..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/LinguaFrancaStyleExtensions.xtend +++ /dev/null @@ -1,395 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis.styles - -import de.cau.cs.kieler.klighd.internal.util.KlighdInternalProperties -import de.cau.cs.kieler.klighd.kgraph.KEdge -import de.cau.cs.kieler.klighd.kgraph.KLabel -import de.cau.cs.kieler.klighd.kgraph.KPort -import de.cau.cs.kieler.klighd.krendering.Colors -import de.cau.cs.kieler.klighd.krendering.KContainerRendering -import de.cau.cs.kieler.klighd.krendering.KPolyline -import de.cau.cs.kieler.klighd.krendering.KRendering -import de.cau.cs.kieler.klighd.krendering.KRenderingFactory -import de.cau.cs.kieler.klighd.krendering.KText -import de.cau.cs.kieler.klighd.krendering.Underline -import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared -import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KPolylineExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceX -import de.cau.cs.kieler.klighd.krendering.extensions.PositionReferenceY -import de.cau.cs.kieler.klighd.labels.decoration.IDecoratorRenderingProvider -import de.cau.cs.kieler.klighd.labels.decoration.LabelDecorationConfigurator -import de.cau.cs.kieler.klighd.labels.decoration.LabelDecorationConfigurator.LayoutMode -import javax.inject.Inject -import org.eclipse.elk.core.math.ElkPadding -import org.eclipse.elk.graph.properties.Property -import org.lflang.diagram.synthesis.AbstractSynthesisExtensions - -import static extension org.eclipse.emf.ecore.util.EcoreUtil.copy - -/** - * Extension class that provides styles and coloring for the Lingua France diagram synthesis. - * - * @author{Alexander Schulz-Rosengarten } - */ -@ViewSynthesisShared -class LinguaFrancaStyleExtensions extends AbstractSynthesisExtensions { - - static val LABEL_PARENT_BACKGROUND = new Property("org.lflang.linguafranca.diagram.synthesis.styles.label.parent.background", Colors.WHITE) - - @Inject extension KRenderingExtensions - @Inject extension KContainerRenderingExtensions - @Inject extension KPolylineExtensions - extension KRenderingFactory = KRenderingFactory::eINSTANCE - - def noSelectionStyle(KRendering r) { - r.selectionTextStrikeout = false // prevents default selection style - } - - def underlineSelectionStyle(KRendering r) { - r.selectionTextUnderline = Underline.SINGLE - } - - def boldLineSelectionStyle(KRendering r) { - r.selectionLineWidth = r.lineWidthValue * 2 - } - - def boldTextSelectionStyle(KText t) { - t.selectionFontBold = true - } - - def errorStyle(KRendering r) { - r.foreground = Colors.RED - r.lineWidth = 2 - r.selectionLineWidth = 3 - - if (r.eContainer instanceof KEdge || r.eContainer instanceof KPort) { // also color potential arrow heads - r.background = Colors.RED - r.background.propagateToChildren = true - r.foreground.propagateToChildren = true - r.lineWidth.propagateToChildren = true - } - } - - def commentStyle(KRendering r) { - r.foreground = Colors.LIGHT_GOLDENROD - r.background = Colors.PALE_GOLDENROD - r.lineWidth = 1 - r.selectionLineWidth = 2 - - if (r.eContainer instanceof KEdge) { // also color potential arrow heads - r.background = Colors.LIGHT_GOLDENROD - r.background.propagateToChildren = true - r.foreground.propagateToChildren = true - r.lineWidth.propagateToChildren = true - } - } - - static val CLOUD_WIDTH = 20 - def KContainerRendering addCloudIcon(KContainerRendering parent) { - return parent.addRectangle() => [ - invisible = true - - addRoundedRectangle(CLOUD_WIDTH / 7, CLOUD_WIDTH / 7) => [ - background = Colors.GRAY - foreground = Colors.GRAY - setPointPlacementData(LEFT, 2, 0, TOP, 0, 0.5f, H_LEFT, V_TOP, 0, 0, CLOUD_WIDTH, CLOUD_WIDTH / 3) - ] - addEllipse() => [ - background = Colors.GRAY - foreground = Colors.GRAY - setPointPlacementData(LEFT, 0, 0f, TOP, 0, 0.38f, H_LEFT, V_TOP, 0, 0, CLOUD_WIDTH / 2.5f, CLOUD_WIDTH / 2.5f) - ] - addEllipse() => [ - background = Colors.GRAY - foreground = Colors.GRAY - setPointPlacementData(LEFT, 0, 0.5f, TOP, 0, 0.25f, H_RIGHT, V_TOP, 0, 0, CLOUD_WIDTH / 3f, CLOUD_WIDTH / 3f) - ] - addEllipse() => [ - background = Colors.GRAY - foreground = Colors.GRAY - setPointPlacementData(LEFT, 0, 0.4f, TOP, CLOUD_WIDTH / 10, 0, H_LEFT, V_TOP, 0, 0, CLOUD_WIDTH / 2, CLOUD_WIDTH / 2) - ] - ] - } - - def KRendering addCloudUploadIcon(KContainerRendering parent) { - return parent.addCloudIcon() => [ - addPolygon() => [ - background = Colors.WHITE - foreground = Colors.WHITE - points += #[ - createKPosition(PositionReferenceX.LEFT, -1.5f, 0.5f, PositionReferenceY.TOP, CLOUD_WIDTH / 3, 0.5f), - createKPosition(PositionReferenceX.LEFT, -1.5f, 0.5f, PositionReferenceY.TOP, 0, 0.58f), - createKPosition(PositionReferenceX.LEFT, -4, 0.5f, PositionReferenceY.TOP, 0, 0.58f), - createKPosition(PositionReferenceX.LEFT, 0, 0.5f, PositionReferenceY.TOP, 0, 0.35f), - createKPosition(PositionReferenceX.LEFT, 4, 0.5f, PositionReferenceY.TOP, 0, 0.58f), - createKPosition(PositionReferenceX.LEFT, 1.5f, 0.5f, PositionReferenceY.TOP, 0, 0.58f), - createKPosition(PositionReferenceX.LEFT, 1.5f, 0.5f, PositionReferenceY.TOP, CLOUD_WIDTH / 3, 0.5f) - ] - ] - ] - } - - static var LabelDecorationConfigurator _onEdgeLabelConfigurator; // ONLY for use in applyOnEdgeStyle - def applyOnEdgeStyle(KLabel label) { - if (_onEdgeLabelConfigurator === null) { - _onEdgeLabelConfigurator = LabelDecorationConfigurator.create - .withInlineLabels(true) - .withLabelTextRenderingProvider([ container, klabel | - val kText = createKText() - kText.fontSize = 9 - container.children += kText - kText - ]) - } - _onEdgeLabelConfigurator.applyTo(label) - } - - static var LabelDecorationConfigurator _onEdgeDelayLabelConfigurator; // ONLY for use in applyOnEdgeDelayStyle - def applyOnEdgeDelayStyle(KLabel label) { - if (_onEdgeDelayLabelConfigurator === null) { - _onEdgeDelayLabelConfigurator = LabelDecorationConfigurator.create - .withInlineLabels(true) - .withLabelTextRenderingProvider([ container, klabel | - val kText = createKText() - kText.fontSize = 8 - kText.boldTextSelectionStyle() - kText.setProperty(KlighdInternalProperties.MODEL_ELEMEMT, klabel.getProperty(KlighdInternalProperties.MODEL_ELEMEMT)) - - container.children += kText - kText - ]) - .addDecoratorRenderingProvider(new IDecoratorRenderingProvider() { - - override createDecoratorRendering(KContainerRendering container, KLabel label, LayoutMode layoutMode) { - val padding = new ElkPadding() - padding.left = 2 - padding.right = 2 - padding.bottom = Math.max(padding.bottom, 1) - - container.children += createKPolygon => [ - from(PositionReferenceX.LEFT, -2, 0, PositionReferenceY.BOTTOM, 0, 0) - to(PositionReferenceX.LEFT, 2, 0, PositionReferenceY.TOP, 0, 0) - to(PositionReferenceX.RIGHT, -2, 0, PositionReferenceY.TOP, 0, 0) - to(PositionReferenceX.RIGHT, 2, 0, PositionReferenceY.BOTTOM, 0, 0) - background = Colors.WHITE - foreground = Colors.WHITE - ] - container.children += createKPolyline => [ - from(PositionReferenceX.LEFT, -2, 0, PositionReferenceY.BOTTOM, 0, 0) - to(PositionReferenceX.LEFT, 2, 0, PositionReferenceY.TOP, 0, 0) - ] - container.children += createKPolyline => [ - from(PositionReferenceX.RIGHT, 2, 0, PositionReferenceY.BOTTOM, 0, 0) - to(PositionReferenceX.RIGHT, -2, 0, PositionReferenceY.TOP, 0, 0) - ] - return padding - } - - }) - } - _onEdgeDelayLabelConfigurator.applyTo(label) - } - - static var LabelDecorationConfigurator _onEdgePysicalDelayLabelConfigurator; // ONLY for use in applyOnEdgePysicalDelayStyle - def applyOnEdgePysicalDelayStyle(KLabel label, Colors parentBackgroundColor) { - if (_onEdgePysicalDelayLabelConfigurator === null) { - _onEdgePysicalDelayLabelConfigurator = LabelDecorationConfigurator.create - .withInlineLabels(true) - .withLabelTextRenderingProvider([ container, klabel | - val kText = createKText() - kText.fontSize = 8 - kText.boldTextSelectionStyle() - kText.setProperty(KlighdInternalProperties.MODEL_ELEMEMT, klabel.getProperty(KlighdInternalProperties.MODEL_ELEMEMT)) - - container.children += kText - kText - ]) - .addDecoratorRenderingProvider(new IDecoratorRenderingProvider() { - - override createDecoratorRendering(KContainerRendering container, KLabel label, LayoutMode layoutMode) { - val padding = new ElkPadding() - padding.left = 8 - padding.right = 16 - padding.bottom = Math.max(padding.bottom, 1) - - container.children += createKPolygon() => [ - from(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.BOTTOM, 0, 0.5f) - to(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.TOP, 1, 0.5f) - to(PositionReferenceX.RIGHT, 0, 0, PositionReferenceY.TOP, 1, 0.5f) - to(PositionReferenceX.RIGHT, 0, 0, PositionReferenceY.BOTTOM, 0, 0.5f) - background = label.getProperty(LABEL_PARENT_BACKGROUND) - foreground = label.getProperty(LABEL_PARENT_BACKGROUND) - ] - container.children += createKSpline() => [ - from(PositionReferenceX.LEFT, -0.66f, 0, PositionReferenceY.BOTTOM, -0.5f , 0.5f) - to(PositionReferenceX.LEFT, 1, 0, PositionReferenceY.BOTTOM, -0.5f , 0.5f) - to(PositionReferenceX.LEFT, 3, 0, PositionReferenceY.BOTTOM, 8, 0.5f) - to(PositionReferenceX.LEFT, 5, 0, PositionReferenceY.BOTTOM, 0, 0.5f) - to(PositionReferenceX.LEFT, 5.5f, 0, PositionReferenceY.BOTTOM, -1.5f, 0.5f) - ] - container.children += createKSpline() => [ - from(PositionReferenceX.RIGHT, 15f, 0, PositionReferenceY.BOTTOM, 3.5f , 0.5f) - to(PositionReferenceX.RIGHT, 14f, 0, PositionReferenceY.BOTTOM, 0, 0.5f) - to(PositionReferenceX.RIGHT, 11, 0, PositionReferenceY.BOTTOM, -8, 0.5f) - to(PositionReferenceX.RIGHT, 9, 0, PositionReferenceY.BOTTOM, 0, 0.5f) - to(PositionReferenceX.RIGHT, 7, 0, PositionReferenceY.BOTTOM, 8, 0.5f) - to(PositionReferenceX.RIGHT, 4f, 0, PositionReferenceY.BOTTOM, 2, 0.5f) - to(PositionReferenceX.RIGHT, 1.5f, 0, PositionReferenceY.BOTTOM, 0.5f, 0.5f) - to(PositionReferenceX.RIGHT, 0.2f, 0, PositionReferenceY.BOTTOM, -0.5f, 0.5f) - to(PositionReferenceX.RIGHT, -0.7f, 0, PositionReferenceY.BOTTOM, -0.5f, 0.5f) - ] - container.children += createKPolygon => [ - from(PositionReferenceX.LEFT, 4, 0, PositionReferenceY.BOTTOM, 0, 0) - to(PositionReferenceX.LEFT, 8, 0, PositionReferenceY.TOP, 0, 0) - to(PositionReferenceX.RIGHT, 12, 0, PositionReferenceY.TOP, 0, 0) - to(PositionReferenceX.RIGHT, 16, 0, PositionReferenceY.BOTTOM, 0, 0) - background = Colors.WHITE - foreground = Colors.WHITE - ] - container.children += createKPolyline => [ - from(PositionReferenceX.LEFT, 4, 0, PositionReferenceY.BOTTOM, 0, 0) - to(PositionReferenceX.LEFT, 8, 0, PositionReferenceY.TOP, 0, 0) - ] - container.children += createKPolyline => [ - from(PositionReferenceX.RIGHT, 16, 0, PositionReferenceY.BOTTOM, 0, 0) - to(PositionReferenceX.RIGHT, 12, 0, PositionReferenceY.TOP, 0, 0) - ] - return padding - } - - }) - } - label.setProperty(LABEL_PARENT_BACKGROUND, parentBackgroundColor) - _onEdgePysicalDelayLabelConfigurator.applyTo(label) - } - - static var LabelDecorationConfigurator _onEdgePysicalLabelConfigurator; // ONLY for use in applyOnEdgePysicalStyle - def applyOnEdgePysicalStyle(KLabel label, Colors parentBackgroundColor) { - if (_onEdgePysicalLabelConfigurator === null) { - _onEdgePysicalLabelConfigurator = LabelDecorationConfigurator.create - .withInlineLabels(true) - .withLabelTextRenderingProvider([ container, klabel | - val kText = createKText() - kText.invisible = true - container.children += kText - kText - ]) - .addDecoratorRenderingProvider(new IDecoratorRenderingProvider() { - - override createDecoratorRendering(KContainerRendering container, KLabel label, LayoutMode layoutMode) { - val padding = new ElkPadding() - padding.left = 3 - padding.right = 3 - padding.bottom = Math.max(padding.bottom, 1) - - container.children += createKPolygon() => [ - from(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.BOTTOM, 0, 0.5f) - to(PositionReferenceX.LEFT, 0, 0, PositionReferenceY.TOP, 1, 0.5f) - to(PositionReferenceX.RIGHT, 0, 0, PositionReferenceY.TOP, 1, 0.5f) - to(PositionReferenceX.RIGHT, 0, 0, PositionReferenceY.BOTTOM, 0, 0.5f) - background = label.getProperty(LABEL_PARENT_BACKGROUND) - foreground = label.getProperty(LABEL_PARENT_BACKGROUND) - ] - container.children += createKSpline() => [ - from(PositionReferenceX.LEFT, -0.66f, 0, PositionReferenceY.BOTTOM, -0.5f , 0.5f) - to(PositionReferenceX.LEFT, 1, 0, PositionReferenceY.BOTTOM, -0.5f , 0.5f) - to(PositionReferenceX.LEFT, 0, 0.1f, PositionReferenceY.BOTTOM, 8, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.2f, PositionReferenceY.BOTTOM, 0, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.3f, PositionReferenceY.BOTTOM, -8, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.4f, PositionReferenceY.BOTTOM, 0, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.45f, PositionReferenceY.BOTTOM, 4f, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.5f, PositionReferenceY.BOTTOM, 8, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.55f, PositionReferenceY.BOTTOM, 4f, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.6f, PositionReferenceY.BOTTOM, 0, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.65f, PositionReferenceY.BOTTOM, -4, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.7f, PositionReferenceY.BOTTOM, -8, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.8f, PositionReferenceY.BOTTOM, -4, 0.5f) - to(PositionReferenceX.LEFT, 0, 0.9f, PositionReferenceY.BOTTOM, 0, 0.5f) - to(PositionReferenceX.LEFT, -1, 1, PositionReferenceY.BOTTOM, -0.5f, 0.5f) - to(PositionReferenceX.LEFT, 0.66f, 1, PositionReferenceY.BOTTOM, -0.5f, 0.5f) - ] - return padding - } - - }) - } - label.setProperty(LABEL_PARENT_BACKGROUND, parentBackgroundColor) - _onEdgePysicalLabelConfigurator.applyTo(label) - } - - def KRendering addFixedTailArrowDecorator(KPolyline pl) { - val head = pl.addTailArrowDecorator() - head.placementData = createKDecoratorPlacementData => [ - rotateWithLine = true - relative = 0f - absolute = 2f - width = 8 - height = 6 - setXOffset(-3f) - setYOffset(-4f) - ] - return head - } - - def addArrayDecorator(KEdge edge, Integer size) { - val line = edge.KRendering - if (line instanceof KPolyline) { - //line.lineWidth = 2 - - val placement = createKDecoratorPlacementData => [ - rotateWithLine = true - relative = 0f - absolute = 6f - ] - - val slash = line.addChild(createKPolyline()) => [ - //lineWidth = 2 - points += createKPosition(RIGHT, 0, 0, TOP, 0, 0) - points += createKPosition(LEFT, 0, 0, BOTTOM, 0, 0) - ] - slash.placementData = placement.copy() => [ - width = 5 - height = 10 - setYOffset(-5f) - ] - - if (size !== null) { - val num = line.addChild(createKText()) => [ - text = size.toString() - fontSize = 5 - noSelectionStyle - ] - num.placementData = placement.copy() => [ - setXOffset(2f) - ] - } - } - } - -} \ No newline at end of file diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/ReactorFigureComponents.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/ReactorFigureComponents.java new file mode 100644 index 0000000000..33e3ca7ce0 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/styles/ReactorFigureComponents.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2020, Kiel University. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.lflang.diagram.synthesis.styles; + +import de.cau.cs.kieler.klighd.krendering.KContainerRendering; +import de.cau.cs.kieler.klighd.krendering.KRendering; +import java.util.List; +import org.eclipse.xtend.lib.annotations.Data; +import org.eclipse.xtext.xbase.lib.Pure; +import org.eclipse.xtext.xbase.lib.util.ToStringBuilder; + +@Data +public class ReactorFigureComponents { + private final KContainerRendering outer; + + private final KContainerRendering reactor; + + private final List figures; + + public ReactorFigureComponents(KContainerRendering outer, + KContainerRendering reactor, + List figures) { + super(); + this.outer = outer; + this.reactor = reactor; + this.figures = figures; + } + + @Override + @Pure + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.outer== null) ? 0 : this.outer.hashCode()); + result = prime * result + ((this.reactor== null) ? 0 : this.reactor.hashCode()); + return prime * result + ((this.figures== null) ? 0 : this.figures.hashCode()); + } + + @Override + @Pure + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ReactorFigureComponents other = (ReactorFigureComponents) obj; + if (this.outer == null && other.outer != null) { + return false; + } else if (!this.outer.equals(other.outer)) { + return false; + } + if (this.reactor == null && other.reactor != null) { + return false; + } else if (!this.reactor.equals(other.reactor)) { + return false; + } + if (this.figures == null && other.figures != null) { + return false; + } else if (!this.figures.equals(other.figures)) { + return false; + } + return true; + } + + @Override + @Pure + public String toString() { + ToStringBuilder b = new ToStringBuilder(this); + b.add("outer", this.outer); + b.add("reactor", this.reactor); + b.add("figures", this.figures); + return b.toString(); + } + + @Pure + public KContainerRendering getOuter() { + return this.outer; + } + + @Pure + public KContainerRendering getReactor() { + return this.reactor; + } + + @Pure + public List getFigures() { + return this.figures; + } +} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/CycleVisualization.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/CycleVisualization.java new file mode 100644 index 0000000000..eeeab7e98b --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/CycleVisualization.java @@ -0,0 +1,157 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis.util; + +import com.google.common.collect.HashMultimap; +import com.google.inject.Inject; +import de.cau.cs.kieler.klighd.kgraph.KEdge; +import de.cau.cs.kieler.klighd.kgraph.KGraphElement; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.kgraph.KPort; +import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import org.eclipse.elk.graph.properties.Property; +import org.eclipse.xtext.xbase.lib.Extension; +import org.lflang.diagram.synthesis.AbstractSynthesisExtensions; +import org.lflang.generator.NamedInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.lf.Connection; + +/** + * Dependency cycle detection for Lingua Franca diagrams. + * + * @author{Alexander Schulz-Rosengarten } + */ +@ViewSynthesisShared +public class CycleVisualization extends AbstractSynthesisExtensions { + + // Properties for marking diagram elements + public static final Property DEPENDENCY_CYCLE = new Property<>("org.lflang.diagram.synthesis.dependency.cycle", false); + + @Inject @Extension private UtilityExtensions _utilityExtensions; + + /** + * Performs cycle detection based on the diagram's graph structure and applies given highlighting to the included elements + */ + public boolean detectAndHighlightCycles(ReactorInstance rootReactorInstance, + Map allReactorNodes, + Consumer highlighter) { + + if (rootReactorInstance.hasCycles() && highlighter != null) { + // Highlight cycles + // A cycle consists of reactions and ports. + HashMultimap> cycleElementsByReactor = HashMultimap.create(); + Set> cycles = rootReactorInstance.getCycles(); + for (NamedInstance element : cycles) { + // First find the involved reactor instances + if (element instanceof ReactorInstance) { + cycleElementsByReactor.put((ReactorInstance) element, element); + } else { + cycleElementsByReactor.put(element.getParent(), element); + } + } + + for (ReactorInstance reactor : cycleElementsByReactor.keySet()) { + KNode node = allReactorNodes.get(reactor); + if (node != null) { + node.setProperty(DEPENDENCY_CYCLE, true); + highlighter.accept(node); + + Set> reactorContentInCycle = cycleElementsByReactor.get(reactor); + + // Reactor edges + for (KEdge edge : node.getOutgoingEdges()) { + if (connectsCycleElements(edge, cycles)) { + edge.setProperty(DEPENDENCY_CYCLE, true); + highlighter.accept(edge); + } + } + + // Reactor ports + for (KPort port : node.getPorts()) { + if (reactorContentInCycle.contains(NamedInstanceUtil.getLinkedInstance(port))) { + port.setProperty(DEPENDENCY_CYCLE, true); + highlighter.accept(port); + } + } + + // Child Nodes + for (KNode childNode : node.getChildren()) { + if (reactorContentInCycle.contains(NamedInstanceUtil.getLinkedInstance(childNode)) && + !_utilityExtensions.sourceIsReactor(childNode)) { + childNode.setProperty(DEPENDENCY_CYCLE, true); + highlighter.accept(childNode); + + for (KEdge edge : childNode.getOutgoingEdges()) { + if (connectsCycleElements(edge, cycles)) { + edge.setProperty(DEPENDENCY_CYCLE, true); + highlighter.accept(edge); + } + } + } + } + } + } + return true; + } + return false; + } + + /** + * Checks whether an edge connects two elements that are part of the cycle. + * Assumes that the source node is always part of the cycle! + */ + private boolean connectsCycleElements(KEdge edge, Set> cycle) { + return ( + // if source is not a reactor, there is nothing to check + !_utilityExtensions.sourceIsReactor(edge.getSource()) + || + // otherwise, the source port must be on the cycle + cycle.contains(NamedInstanceUtil.getLinkedInstance(edge.getSourcePort())) + ) && ( + // leads to reactor port in cycle + _utilityExtensions.sourceIsReactor(edge.getTarget()) + && + cycle.contains(NamedInstanceUtil.getLinkedInstance(edge.getTargetPort())) + || + // leads to reaction in cycle + !_utilityExtensions.sourceIsReactor(edge.getTarget()) + && + cycle.contains(NamedInstanceUtil.getLinkedInstance(edge.getTarget())) + ) && ( + // Special case only for connections + !(_utilityExtensions.sourceElement(edge) instanceof Connection) + || ( + // If the edge represents a connections between two ports in the cycle (checked before), + // then it is only included in the actual cycle, if it is neither delayed nor physical. + ((Connection) _utilityExtensions.sourceElement(edge)).getDelay() == null + && + !((Connection) _utilityExtensions.sourceElement(edge)).isPhysical() + ) + ); + } +} 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 deleted file mode 100644 index 8a6bbce65e..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/CycleVisualization.xtend +++ /dev/null @@ -1,151 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis.util - -import com.google.common.collect.HashMultimap -import com.google.inject.Inject -import de.cau.cs.kieler.klighd.kgraph.KEdge -import de.cau.cs.kieler.klighd.kgraph.KGraphElement -import de.cau.cs.kieler.klighd.kgraph.KNode -import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared -import java.util.Map -import java.util.Set -import java.util.function.Consumer -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.lf.Connection - -import static extension org.lflang.diagram.synthesis.util.NamedInstanceUtil.* - -/** - * Dependency cycle detection for Lingua Franca diagrams. - * - * @author{Alexander Schulz-Rosengarten } - */ -@ViewSynthesisShared -class CycleVisualization extends AbstractSynthesisExtensions { - - // Properties for marking diagram elements - public static val DEPENDENCY_CYCLE = new Property("org.lflang.diagram.synthesis.dependency.cycle", false) - - @Inject - extension UtilityExtensions - - /** - * 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) { - - if (rootReactorInstance.hasCycles() && highlighter !== null) { - // Highlight cycles - // 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) - - 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) - } - } - - // 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(cycles)) { - edge.setProperty(DEPENDENCY_CYCLE, true) - highlighter.accept(edge) - } - } - } - } - } - } - return true - } - return false - } - - /** - * Checks whether an edge connects two elements that are part of the cycle. - * Assumes that the source node is always part of the cycle! - */ - private def boolean connectsCycleElements(KEdge edge, Set> cycle) { - return ( - // if source is not a reactor, there is nothing to check - !edge.source.sourceIsReactor - || - // otherwise, the source port must be on the cycle - cycle.contains(edge.sourcePort.linkedInstance) - ) && ( - // leads to reactor port in cycle - edge.target.sourceIsReactor && cycle.contains(edge.targetPort.linkedInstance) - || - // leads to reaction in cycle - !edge.target.sourceIsReactor && cycle.contains(edge.target.linkedInstance) - ) && ( - // Special case only for connections - !(edge.sourceElement() instanceof Connection) - || ( - // If the edge represents a connections between two ports in the cycle (checked before), - // then it is only included in the actual cycle, if it is neither delayed nor physical. - (edge.sourceElement() as Connection).delay === null - && - !(edge.sourceElement() as Connection).physical - ) - ) - } -} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/InterfaceDependenciesVisualization.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/InterfaceDependenciesVisualization.java new file mode 100644 index 0000000000..18abd051d2 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/InterfaceDependenciesVisualization.java @@ -0,0 +1,206 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; +import com.google.common.collect.Sets; +import com.google.inject.Inject; +import de.cau.cs.kieler.klighd.SynthesisOption; +import de.cau.cs.kieler.klighd.kgraph.KEdge; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.kgraph.KPort; +import de.cau.cs.kieler.klighd.krendering.KContainerRendering; +import de.cau.cs.kieler.klighd.krendering.KInvisibility; +import de.cau.cs.kieler.klighd.krendering.KPolyline; +import de.cau.cs.kieler.klighd.krendering.KRectangle; +import de.cau.cs.kieler.klighd.krendering.KRendering; +import de.cau.cs.kieler.klighd.krendering.KRenderingFactory; +import de.cau.cs.kieler.klighd.krendering.LineStyle; +import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; +import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KEdgeExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions; +import de.cau.cs.kieler.klighd.syntheses.DiagramSyntheses; +import de.cau.cs.kieler.klighd.util.KlighdProperties; +import java.util.List; +import java.util.Random; +import java.util.Set; +import org.eclipse.elk.core.math.ElkMargin; +import org.eclipse.elk.core.math.Spacing; +import org.eclipse.elk.core.options.CoreOptions; +import org.eclipse.elk.graph.properties.Property; +import org.eclipse.xtext.xbase.lib.Extension; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.eclipse.xtext.xbase.lib.IteratorExtensions; +import org.eclipse.xtext.xbase.lib.Pair; +import org.lflang.diagram.synthesis.AbstractSynthesisExtensions; +import org.lflang.diagram.synthesis.LinguaFrancaSynthesis; +import org.lflang.diagram.synthesis.styles.LinguaFrancaShapeExtensions; +import org.lflang.diagram.synthesis.styles.LinguaFrancaStyleExtensions; + +/** + * Utility class to handle dependency edges for collapsed reactors in Lingua Franca diagrams. + * + * @author{Alexander Schulz-Rosengarten } + */ +@ViewSynthesisShared +public class InterfaceDependenciesVisualization extends AbstractSynthesisExtensions { + + // Related synthesis option + public static final SynthesisOption SHOW_INTERFACE_DEPENDENCIES = SynthesisOption.createCheckOption("Port Dependencies in Collapsed Reactors", false).setCategory(LinguaFrancaSynthesis.APPEARANCE); + + // Properties for marking diagram elements + public static final Property INTERFACE_DEPENDENCY = new Property<>("org.lflang.linguafranca.diagram.synthesis.dependency.interface", false); + + @Inject @Extension private KEdgeExtensions _kEdgeExtensions; + @Inject @Extension private KRenderingExtensions _kRenderingExtensions; + @Inject @Extension private KContainerRenderingExtensions _kContainerRenderingExtensions; + @Inject @Extension private LinguaFrancaStyleExtensions _linguaFrancaStyleExtensions; + @Inject @Extension private UtilityExtensions _utilityExtensions; + + /** + * Updates the visibility of interface dependencies edges based on the expansion state. + */ + public static void updateInterfaceDependencyVisibility(KNode node, boolean expanded) { + Iterable edges = IterableExtensions.filter(node.getOutgoingEdges(), it -> { + return it.getProperty(INTERFACE_DEPENDENCY); + }); + + Iterable> renders = IterableExtensions.map(edges, (KEdge it) -> { + return Iterables.filter(it.getData(), KRendering.class); + }); + + Iterables.concat(renders).forEach( + it -> { + KInvisibility inv = IterableExtensions.last(Iterables.filter(it.getStyles(), KInvisibility.class)); + if (inv == null) { + inv = KRenderingFactory.eINSTANCE.createKInvisibility(); + it.getStyles().add(inv); + } + inv.setInvisible(expanded); + } + ); + } + + /** + * Adds interface dependencies to the node if this option is active. + * Visibility will be adjusted based on expansion state. + */ + public Spacing addInterfaceDependencies(KNode node, boolean expanded) { + Spacing marginInit = null; + if (getBooleanValue(SHOW_INTERFACE_DEPENDENCIES)) { + List> deps = getPortDependencies(node); + if (!deps.isEmpty()) { + for (Pair pair : deps) { + createDependencyEdge(pair, expanded); + } + + // Fix content (label) of collapsed rendering + KContainerRendering contentContainer = IterableExtensions.findFirst( + Iterables.filter(node.getData(), KContainerRendering.class), + it -> { return it.getProperty(KlighdProperties.COLLAPSED_RENDERING); } + ); + if (contentContainer != null) { + if (!contentContainer.getProperty(LinguaFrancaShapeExtensions.REACTOR_CONTENT_CONTAINER)) { + contentContainer = IteratorExtensions.findFirst( + Iterators.filter(contentContainer.eAllContents(), KContainerRendering.class), + it -> { return it.getProperty(LinguaFrancaShapeExtensions.REACTOR_CONTENT_CONTAINER); } + ); + } + if (contentContainer != null) { + List content = ImmutableList.copyOf(contentContainer.getChildren()); + // Put into two new containers such that they are not centered/maximized + KRectangle firstContainer = _kContainerRenderingExtensions.addRectangle(contentContainer); + _kRenderingExtensions.setInvisible(firstContainer, true); + + KRectangle secondContainer = _kContainerRenderingExtensions.addRectangle(firstContainer); + _kRenderingExtensions.setInvisible(secondContainer, true); + _kContainerRenderingExtensions.setGridPlacement(secondContainer, 1); + Iterables.addAll(secondContainer.getChildren(), content); + _kRenderingExtensions.setPointPlacementData(secondContainer, + _kRenderingExtensions.LEFT, 0, 0.5f, + _kRenderingExtensions.TOP, 0, 0, + _kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_TOP, 0, + 0, 0, 0); + + + // Adjust ports separate dependency edges from label/content + if (content.size() > 0) { + marginInit = _utilityExtensions.getPortMarginsInitIfAbsent(node).add( + new ElkMargin((content.size() * 20) - 8, 0, 0, 0) + ); + } + } + } + } + } + return marginInit; + } + + /** + * Find dependencies between ports. + */ + private List> getPortDependencies(KNode node) { + Set inputPorts = IterableExtensions.toSet(IterableExtensions.filter( + node.getPorts(), + it -> { return it.getProperty(LinguaFrancaSynthesis.REACTOR_INPUT); }) + ); + Set outputPorts = IterableExtensions.toSet(IterableExtensions.filter( + node.getPorts(), + it -> { return it.getProperty(LinguaFrancaSynthesis.REACTOR_OUTPUT); }) + ); + + // FIXME Replace with real logic + Random rand = new Random(); + return IterableExtensions.toList( + IterableExtensions.map(IterableExtensions.filter( + Sets.cartesianProduct(inputPorts, outputPorts), + it -> { return rand.nextBoolean(); }), + it -> { return new Pair(it.get(0), it.get(1)); })); + } + + /** + * Create an edges for interface dependencies and adjust visibility based on the expansion state of the node. + */ + private KEdge createDependencyEdge(final Pair connection, final boolean expanded) { + KEdge depEdge = _kEdgeExtensions.createEdge(); + depEdge.setSource(connection.getKey().getNode()); + depEdge.setSourcePort(connection.getKey()); + depEdge.setTarget(connection.getValue().getNode()); + depEdge.setTargetPort(connection.getValue()); + depEdge.setProperty(InterfaceDependenciesVisualization.INTERFACE_DEPENDENCY, true); + depEdge.setProperty(CoreOptions.NO_LAYOUT, true); // no routing! + DiagramSyntheses.suppressSelectability(depEdge); + + KPolyline polyline = _kEdgeExtensions.addPolyline(depEdge); + _kRenderingExtensions.setLineStyle(polyline, LineStyle.DOT); + _linguaFrancaStyleExtensions.noSelectionStyle(polyline); + _kRenderingExtensions.setInvisible(polyline, expanded); // make sure there is a style to toggle! + + return depEdge; + } +} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/InterfaceDependenciesVisualization.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/InterfaceDependenciesVisualization.xtend deleted file mode 100644 index 27684f0bbd..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/InterfaceDependenciesVisualization.xtend +++ /dev/null @@ -1,163 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis.util - -import com.google.common.collect.Sets -import com.google.inject.Inject -import de.cau.cs.kieler.klighd.SynthesisOption -import de.cau.cs.kieler.klighd.kgraph.KNode -import de.cau.cs.kieler.klighd.kgraph.KPort -import de.cau.cs.kieler.klighd.krendering.KContainerRendering -import de.cau.cs.kieler.klighd.krendering.KInvisibility -import de.cau.cs.kieler.klighd.krendering.KRendering -import de.cau.cs.kieler.klighd.krendering.KRenderingFactory -import de.cau.cs.kieler.klighd.krendering.LineStyle -import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared -import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KEdgeExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions -import de.cau.cs.kieler.klighd.syntheses.DiagramSyntheses -import de.cau.cs.kieler.klighd.util.KlighdProperties -import java.util.List -import java.util.Random -import org.eclipse.elk.core.math.ElkMargin -import org.eclipse.elk.core.options.CoreOptions -import org.eclipse.elk.graph.properties.Property -import org.lflang.diagram.synthesis.AbstractSynthesisExtensions -import org.lflang.diagram.synthesis.LinguaFrancaSynthesis -import org.lflang.diagram.synthesis.styles.LinguaFrancaShapeExtensions -import org.lflang.diagram.synthesis.styles.LinguaFrancaStyleExtensions - -/** - * Utility class to handle dependency edges for collapsed reactors in Lingua Franca diagrams. - * - * @author{Alexander Schulz-Rosengarten } - */ -@ViewSynthesisShared -class InterfaceDependenciesVisualization extends AbstractSynthesisExtensions { - - // Related synthesis option - public static val SynthesisOption SHOW_INTERFACE_DEPENDENCIES = SynthesisOption.createCheckOption("Port Dependencies in Collapsed Reactors", false).setCategory(LinguaFrancaSynthesis.APPEARANCE) - - // Properties for marking diagram elements - public static val INTERFACE_DEPENDENCY = new Property("org.lflang.linguafranca.diagram.synthesis.dependency.interface", false) - - @Inject extension KEdgeExtensions - @Inject extension KRenderingExtensions - @Inject extension KContainerRenderingExtensions - @Inject extension LinguaFrancaStyleExtensions - @Inject extension UtilityExtensions - - /** - * Updates the visibility of interface dependencies edges based on the expansion state. - */ - static def updateInterfaceDependencyVisibility(KNode node, boolean expanded) { - node.outgoingEdges.filter[getProperty(INTERFACE_DEPENDENCY)].map[data.filter(typeof(KRendering))].flatten.forEach[ - var inv = styles.filter(typeof(KInvisibility)).last - if (inv === null) { - inv = KRenderingFactory.eINSTANCE.createKInvisibility() - styles += inv - } - inv.invisible = expanded - ] - } - - /** - * Adds interface dependencies to the node if this option is active. - * Visibility will be adjusted based on expansion state. - */ - def addInterfaceDependencies(KNode node, boolean expanded) { - if (SHOW_INTERFACE_DEPENDENCIES.booleanValue) { - val deps = node.portDependencies - if (!deps.empty) { - for (pair : deps) { - createDependencyEdge(pair, expanded) - } - - // Fix content (label) of collapsed rendering - var contentContainer = node.data.filter(KContainerRendering).findFirst[getProperty(KlighdProperties.COLLAPSED_RENDERING)] - if (contentContainer !== null) { - if (!contentContainer.getProperty(LinguaFrancaShapeExtensions.REACTOR_CONTENT_CONTAINER)) { - contentContainer = contentContainer.eAllContents.filter(KContainerRendering).findFirst[getProperty(LinguaFrancaShapeExtensions.REACTOR_CONTENT_CONTAINER)] - } - if (contentContainer !== null) { - val content = contentContainer.children.immutableCopy - // Put into two new containers such that they are not centered/maximized - contentContainer.addRectangle() => [ - invisible = true - addRectangle() => [ - invisible = true - setGridPlacement(1) - children += content - - setPointPlacementData(LEFT, 0, 0.5f, TOP, 0, 0, H_CENTRAL, V_TOP, 0, 0, 0, 0) - ] - ] - - // Adjust ports separate dependency edges from label/content - if (content.size > 0) { - node.getPortMarginsInitIfAbsent().add(new ElkMargin((content.size * 20) - 8, 0, 0, 0)) - } - } - } - } - } - } - - /** - * Find dependencies between ports. - */ - private def List>getPortDependencies(KNode node) { - val inputPorts = node.ports.filter[getProperty(LinguaFrancaSynthesis.REACTOR_INPUT)].toSet - val outputPorts = node.ports.filter[getProperty(LinguaFrancaSynthesis.REACTOR_OUTPUT)].toSet - - // FIXME Replace with real logic - val rand = new Random() - return Sets.cartesianProduct(inputPorts, outputPorts).filter[rand.nextBoolean].map[new Pair(get(0), get(1))].toList - } - - /** - * Create an edges for interface dependencies and adjust visibility based on the expansion state of the node. - */ - private def createDependencyEdge(Pair connection, boolean expanded) { - return createEdge => [ - source = connection.key.node - sourcePort = connection.key - target = connection.value.node - targetPort = connection.value - - setProperty(INTERFACE_DEPENDENCY, true) - setProperty(CoreOptions.NO_LAYOUT, true) // no routing! - DiagramSyntheses.suppressSelectability(it) - - addPolyline() => [ - lineStyle = LineStyle.DOT - noSelectionStyle() - - invisible = expanded // make sure there is a style to toggle! - ] - ] - } -} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.java similarity index 66% rename from org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.xtend rename to org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.java index a5006eca9a..42696c9a11 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/NamedInstanceUtil.java @@ -22,37 +22,37 @@ * (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.diagram.synthesis.util +package org.lflang.diagram.synthesis.util; -import de.cau.cs.kieler.klighd.kgraph.KGraphElement -import org.eclipse.elk.graph.properties.Property -import org.lflang.generator.NamedInstance +import de.cau.cs.kieler.klighd.kgraph.KGraphElement; +import org.eclipse.elk.graph.properties.IPropertyHolder; +import org.eclipse.elk.graph.properties.Property; +import org.lflang.generator.NamedInstance; /** * Utility class to link KGraphElements to NamedInstances. * * @author{Alexander Schulz-Rosengarten } */ -class NamedInstanceUtil { - - public static val LINKED_INSTANCE = new Property>( - "org.lflang.linguafranca.diagram.synthesis.graph.instance") +public class NamedInstanceUtil { + public static final Property> LINKED_INSTANCE = new Property<>( + "org.lflang.linguafranca.diagram.synthesis.graph.instance"); /** * Establishes a link between KGraphElement and NamedInstance. */ - static def linkInstance(KGraphElement elem, NamedInstance instance) { - elem.setProperty(LINKED_INSTANCE, instance) + public static IPropertyHolder linkInstance(KGraphElement elem, NamedInstance instance) { + return elem.setProperty(LINKED_INSTANCE, instance); } /** * Returns the linked NamedInstance for ther given KGraphElement. */ - static def > T getLinkedInstance(KGraphElement elem) { - val instance = elem.getProperty(LINKED_INSTANCE) - if (instance !== null) { - return instance as T + public static > T getLinkedInstance(KGraphElement elem) { + NamedInstance instance = elem.getProperty(LINKED_INSTANCE); + if (instance != null) { + return (T) instance; } - return null + return null; } } diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.java new file mode 100644 index 0000000000..5412879513 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.java @@ -0,0 +1,190 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis.util; + +import com.google.inject.Inject; +import de.cau.cs.kieler.klighd.krendering.KContainerRendering; +import de.cau.cs.kieler.klighd.krendering.KGridPlacementData; +import de.cau.cs.kieler.klighd.krendering.KRectangle; +import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; +import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.net.URL; +import java.util.HashMap; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.xtext.xbase.lib.Exceptions; +import org.eclipse.xtext.xbase.lib.Extension; +import org.eclipse.xtext.xbase.lib.StringExtensions; +import org.lflang.ASTUtils; +import org.lflang.diagram.synthesis.AbstractSynthesisExtensions; +import org.lflang.lf.ReactorDecl; + +/** + * Utility class to handle icons for reactors in Lingua Franca diagrams. + * + * @author{Alexander Schulz-Rosengarten } + */ +@ViewSynthesisShared +public class ReactorIcons extends AbstractSynthesisExtensions { + + @Inject @Extension private KRenderingExtensions _kRenderingExtensions; + @Inject @Extension private KContainerRenderingExtensions _kContainerRenderingExtensions; + + private static final ImageLoader LOADER = new ImageLoader(); + + // memory-sensitive cache + private static final HashMap> CACHE = new HashMap<>(); + + + public void handleIcon(KContainerRendering rendering, ReactorDecl reactor, boolean collapsed) { + if (!collapsed) { + return; + } + URL iconLocation = locateIcon(reactor); + if (iconLocation != null) { + ImageData data = loadImage(iconLocation); + if (data != null) { + KRectangle figure = _kContainerRenderingExtensions.addRectangle(rendering); + _kRenderingExtensions.setInvisible(figure, true); + KGridPlacementData figurePlacement = _kRenderingExtensions.setGridPlacementData(figure, data.width, data.height); + _kRenderingExtensions.to( + _kRenderingExtensions.from( + figurePlacement, + _kRenderingExtensions.LEFT, 3, 0, + _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 3, 0, + _kRenderingExtensions.BOTTOM, 3, 0); + + KRectangle icon = _kContainerRenderingExtensions.addRectangle(figure); + _kRenderingExtensions.setInvisible(icon, true); + _kContainerRenderingExtensions.addImage(icon, data); + _kRenderingExtensions.setPointPlacementData(icon, + _kRenderingExtensions.createKPosition( + _kRenderingExtensions.LEFT, 0, 0.5f, + _kRenderingExtensions.TOP, 0, 0.5f), + _kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_CENTRAL, 0, + 0, data.width, data.height); + } + } + } + + private URL locateIcon(EObject eobj) { + URL location = null; + String iconPath = ASTUtils.findAnnotationInComments(eobj, "@icon"); + if (!StringExtensions.isNullOrEmpty(iconPath)) { + // Check if path is URL + try { + return new URL(iconPath); + } catch (Exception e) { + // nothing + } + // Check if path exists as is + File path = new File(iconPath); + if (path.exists()) { + try { + return path.toURI().toURL(); + } catch (Exception e) { + // nothing + } + } + // Check if path is relative to LF file + URI eURI = eobj.eResource() != null ? eobj.eResource().getURI() : null; + if (eURI != null) { + java.net.URI sourceURI = null; + try { + if (eURI.isFile()) { + sourceURI = new java.net.URI(eURI.toString()); + sourceURI = new java.net.URI(sourceURI.getScheme(), null, + sourceURI.getPath().substring(0, sourceURI.getPath().lastIndexOf("/")), null); + } else if (eURI.isPlatformResource()) { + IResource iFile = ResourcesPlugin.getWorkspace().getRoot().findMember(eURI.toPlatformString(true)); + sourceURI = iFile != null ? iFile.getRawLocation().toFile().getParentFile().toURI() : null; + } else if (eURI.isPlatformPlugin()) { + // TODO support loading from plugin bundles? + } + } catch (Exception e) { + // nothing + } + if (sourceURI != null) { + try { + location = sourceURI.resolve(path.toString()).toURL(); + } catch (Exception e) { + // nothing + } + + } + } + // TODO more variants based on package and library system in LF + } + return location; + } + + private ImageData loadImage(final URL url) { + try { + synchronized (CACHE) { + if (CACHE.containsKey(url)) { + ImageData img = CACHE.get(url).get(); + if (img != null) { + return img; + } else { + CACHE.remove(url); + } + } + } + synchronized (LOADER) { + InputStream inStream = null; + try { + inStream = url.openStream(); + // TODO check for memory leak !!! + ImageData[] data = LOADER.load(inStream); + if (data != null && data.length > 0) { + ImageData img = data[0]; + synchronized (CACHE) { + CACHE.put(url, new SoftReference(img)); + } + return img; + } + return null; + } finally { + if (inStream != null) { + inStream.close(); + } + } + } + } catch (IOException ex) { + throw Exceptions.sneakyThrow(ex); + } + } + +} 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 deleted file mode 100644 index 31031ab45f..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.xtend +++ /dev/null @@ -1,155 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis.util - -import com.google.inject.Inject -import de.cau.cs.kieler.klighd.krendering.KContainerRendering -import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared -import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions -import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions -import java.io.File -import java.io.InputStream -import java.lang.ref.SoftReference -import java.net.URI -import java.net.URL -import org.eclipse.core.resources.ResourcesPlugin -import org.eclipse.emf.ecore.EObject -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.ReactorDecl - -/** - * Utility class to handle icons for reactors in Lingua Franca diagrams. - * - * @author{Alexander Schulz-Rosengarten } - */ -@ViewSynthesisShared -class ReactorIcons extends AbstractSynthesisExtensions { - - @Inject extension KRenderingExtensions - @Inject extension KContainerRenderingExtensions - - static val LOADER = new ImageLoader(); - static val CACHE = >newHashMap // memory-sensitive cache - - def void handleIcon(KContainerRendering rendering, ReactorDecl reactor, boolean collapsed) { - if (!collapsed) { - return - } - val iconLocation = reactor.locateIcon() - if (iconLocation !== null) { - val data = iconLocation.loadImage() - if (data !== null) { - rendering.addRectangle() => [ - invisible = true - setGridPlacementData(data.width, data.height) => [ - from(LEFT, 3, 0, TOP, 0, 0).to(RIGHT, 3, 0, BOTTOM, 3, 0) - ] - addRectangle() => [ - invisible = true - addImage(data) - setPointPlacementData(createKPosition(LEFT, 0, 0.5f, TOP, 0, 0.5f), H_CENTRAL, V_CENTRAL, 0, 0, data.width, data.height); - ] - ] - } - } - } - - private def URL locateIcon(EObject eobj) { - var URL location = null - val iconPath = ASTUtils.findAnnotationInComments(eobj, "@icon") - if (!iconPath.nullOrEmpty) { - // Check if path is URL - try { - return new URL(iconPath) - } catch (Exception e) { - // nothing - } - // Check if path exists as is - val path = new File(iconPath) - if (path.exists) { - return path.toURI.toURL - } - // Check if path is relative to LF file - val eURI = eobj.eResource?.URI - if (eURI !== null) { - var URI sourceURI = null - try { - if (eURI.isFile) { - sourceURI = new URI(eURI.toString) - sourceURI = new URI(sourceURI.scheme, null, - sourceURI.path.substring(0, sourceURI.path.lastIndexOf('/')), null) - } else if (eURI.platformResource) { - val iFile = ResourcesPlugin.workspace.root.findMember(eURI.toPlatformString(true)) - sourceURI = iFile?.rawLocation?.toFile.parentFile.toURI - } else if (eURI.platformPlugin) { - // TODO support loading from plugin bundles? - } - } catch (Exception e) { - // nothing - } - if (sourceURI !== null) { - location = sourceURI.resolve(path.toString).toURL - } - } - // TODO more variants based on package and library system in LF - } - return location - } - - private def ImageData loadImage(URL url) { - synchronized (CACHE) { - if (CACHE.containsKey(url)) { - val img = CACHE.get(url).get() - if (img !== null) { - return img - } else { - CACHE.remove(url) - } - } - } - synchronized (LOADER) { - var InputStream inStream = null - try { - inStream = url.openStream - // TODO check for memory leak !!! - val data = LOADER.load(inStream) - if (data !== null && data.length > 0) { - val img = data.get(0) - synchronized (CACHE) { - CACHE.put(url, new SoftReference(img)) - } - return img - } - return null - } finally { - inStream?.close - } - } - } - -} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/SynthesisErrorReporter.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/SynthesisErrorReporter.java similarity index 63% rename from org.lflang.diagram/src/org/lflang/diagram/synthesis/util/SynthesisErrorReporter.xtend rename to org.lflang.diagram/src/org/lflang/diagram/synthesis/util/SynthesisErrorReporter.java index c09209186b..29e3f20720 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/SynthesisErrorReporter.xtend +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/SynthesisErrorReporter.java @@ -22,34 +22,48 @@ * (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.diagram.synthesis.util +package org.lflang.diagram.synthesis.util; -import org.lflang.ErrorReporter -import org.eclipse.emf.ecore.EObject -import java.nio.file.Path +import java.nio.file.Path; +import org.eclipse.emf.ecore.EObject; +import org.lflang.ErrorReporter; /** * @author{Alexander Schulz-Rosengarten } */ -class SynthesisErrorReporter implements ErrorReporter { - - override reportError(String message) { +public class SynthesisErrorReporter implements ErrorReporter { + @Override + public String reportError(String message) { + return null; } - override reportError(EObject object, String message) { + @Override + public String reportError(EObject object, String message) { + return null; } - override reportError(Path file, Integer line, String message) { + @Override + public String reportError(Path file, Integer line, String message) { + return null; } - override reportWarning(String message) { + @Override + public String reportWarning(String message) { + return null; } - override reportWarning(EObject object, String message) { + @Override + public String reportWarning(EObject object, String message) { + return null; } - override reportWarning(Path file, Integer line, String message) { + @Override + public String reportWarning(Path file, Integer line, String message) { + return null; } - override getErrorsOccurred() { return false } + @Override + public boolean getErrorsOccurred() { + return false; + } } \ No newline at end of file diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/UtilityExtensions.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/UtilityExtensions.java new file mode 100644 index 0000000000..b27b027123 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/UtilityExtensions.java @@ -0,0 +1,235 @@ +/************* +* Copyright (c) 2020, Kiel University. +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +***************/ +package org.lflang.diagram.synthesis.util; + +import de.cau.cs.kieler.klighd.internal.util.KlighdInternalProperties; +import de.cau.cs.kieler.klighd.kgraph.KGraphElement; +import de.cau.cs.kieler.klighd.kgraph.KGraphFactory; +import de.cau.cs.kieler.klighd.kgraph.KIdentifier; +import de.cau.cs.kieler.klighd.kgraph.KNode; +import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.elk.core.math.ElkMargin; +import org.eclipse.elk.core.options.CoreOptions; +import org.eclipse.elk.core.util.IndividualSpacings; +import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.xbase.lib.Extension; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.eclipse.xtext.xbase.lib.StringExtensions; +import org.lflang.ASTUtils; +import org.lflang.diagram.synthesis.AbstractSynthesisExtensions; +import org.lflang.generator.ReactorInstance; +import org.lflang.lf.Code; +import org.lflang.lf.Host; +import org.lflang.lf.Reactor; +import org.lflang.lf.Value; + + +/** + * Extension class that provides various utility methods for the synthesis. + * + * @author{Alexander Schulz-Rosengarten } + */ +@ViewSynthesisShared +public class UtilityExtensions extends AbstractSynthesisExtensions { + + @Extension + private KGraphFactory _kGraphFactory = KGraphFactory.eINSTANCE; + + /** + * Converts a timing value into readable text + */ + public String toText(Value value) { + if (value != null) { + if (value.getParameter() != null) { + return value.getParameter().getName(); + } else if (value.getTime() != null) { + return value.getTime().getInterval() + + value.getTime().getUnit().toString(); + } else if (value.getLiteral() != null) { + return value.getLiteral(); + } else if (value.getCode() != null) { + ASTUtils.toText(value.getCode()); + } + } + return ""; + } + + /** + * Converts a host value into readable text + */ + public String toText(Host host) { + StringBuilder sb = new StringBuilder(); + if (host != null) { + if (!StringExtensions.isNullOrEmpty(host.getUser())) { + sb.append(host.getUser()).append("@"); + } + if (!StringExtensions.isNullOrEmpty(host.getAddr())) { + sb.append(host.getAddr()); + } + if (host.getPort() != 0) { + sb.append(":").append(host.getPort()); + } + } + return sb.toString(); + } + + /** + * Returns true if the reactor is the primary reactor + */ + public boolean isMainOrFederated(Reactor reactor) { + return reactor.isMain() || reactor.isFederated(); + } + + /** + * Returns true if the instance is a bank of reactors + */ +// def boolean isBank(Instantiation ins) { +// return ins?.widthSpec !== null ? ins.widthSpec.width !== 1 : false +// } + + /** + * Returns true if the reactor as has inner reactions or instances + */ + public boolean hasContent(final ReactorInstance reactor) { + return !reactor.reactions.isEmpty() || !reactor.instantiations().isEmpty(); + } + + /** + * + */ + public boolean isRoot(final ReactorInstance ri) { + return ri.getParent() == null; + } + + /** + * Trims the hostcode of reactions. + */ + public String trimCode(final Code tokenizedCode) { + if (tokenizedCode == null || StringExtensions.isNullOrEmpty(tokenizedCode.getBody())) { + return ""; + } + try { + ICompositeNode node = NodeModelUtils.findActualNodeFor(tokenizedCode); + String code = node != null ? node.getText() : null; + int contentStart = 0; + List lines = new ArrayList<>(); + Arrays.stream(code.split("\n")).dropWhile(line -> !line.contains("{=")).forEachOrdered(lines::add); + + // Remove start pattern + if (!lines.isEmpty()) { + if (IterableExtensions.head(lines).trim().equals("{=")) { + lines.remove(0); // skip + } else { + lines.set(0, IterableExtensions.head(lines).replace("{=", "").trim()); + contentStart = 1; + } + } + + // Remove end pattern + if (!lines.isEmpty()) { + if (IterableExtensions.last(lines).trim().equals("=}")) { + lines.remove(lines.size() - 1); // skip + } else { + lines.set(lines.size() - 1, IterableExtensions.last(lines).replace("=}", "")); + } + } + + // Find indentation + String indentation = null; + while (indentation == null && lines.size() > contentStart) { + String firstLine = lines.get(contentStart); + String trimmed = firstLine.trim(); + if (trimmed.isEmpty()) { + lines.set(contentStart, ""); + contentStart++; + } else { + int firstCharIdx = firstLine.indexOf(trimmed.charAt(0)); + indentation = firstLine.substring(0, firstCharIdx); + } + } + + // Remove root indentation + if (!lines.isEmpty()) { + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).startsWith(indentation)) { + lines.set(i, lines.get(i).substring(indentation.length())); + } + } + } + + return String.join("\n", lines); + } catch(Exception e) { + e.printStackTrace(); + return tokenizedCode.getBody(); + } + } + + /** + * Sets KGE ID. + */ + public boolean setID(KGraphElement kge, String id) { + KIdentifier identifier = _kGraphFactory.createKIdentifier(); + identifier.setId(id); + return kge.getData().add(identifier); + } + + /** + * Retrieves the source element of the given diagram element + */ + public Object sourceElement(KGraphElement elem) { + return elem.getProperty(KlighdInternalProperties.MODEL_ELEMEMT); + } + + /** + * Checks if the source element of the given diagram element is a reactor + */ + public boolean sourceIsReactor(KNode node) { + return sourceElement(node) instanceof Reactor; + } + + /** + * Returns the port placement margins for the node. + * If this spacing does not yet exist, the properties are initialized. + */ + public ElkMargin getPortMarginsInitIfAbsent(KNode node) { + IndividualSpacings spacing = node.getProperty(CoreOptions.SPACING_INDIVIDUAL); + if (spacing == null) { + spacing = new IndividualSpacings(); + node.setProperty(CoreOptions.SPACING_INDIVIDUAL, spacing); + } + ElkMargin margin = spacing.getProperty(CoreOptions.SPACING_PORTS_SURROUNDING); + if (margin == null) { + margin = new ElkMargin(); + node.setProperty(CoreOptions.SPACING_PORTS_SURROUNDING, margin); + } + return margin; + } + +} diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/UtilityExtensions.xtend b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/UtilityExtensions.xtend deleted file mode 100644 index 020869c265..0000000000 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/UtilityExtensions.xtend +++ /dev/null @@ -1,221 +0,0 @@ -/************* -* Copyright (c) 2020, Kiel University. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -***************/ -package org.lflang.diagram.synthesis.util - -import de.cau.cs.kieler.klighd.internal.util.KlighdInternalProperties -import de.cau.cs.kieler.klighd.kgraph.KGraphElement -import de.cau.cs.kieler.klighd.kgraph.KGraphFactory -import de.cau.cs.kieler.klighd.kgraph.KNode -import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared -import org.eclipse.elk.core.math.ElkMargin -import org.eclipse.elk.core.options.CoreOptions -import org.eclipse.elk.core.util.IndividualSpacings -import org.eclipse.xtext.nodemodel.util.NodeModelUtils -import org.lflang.ASTUtils -import org.lflang.diagram.synthesis.AbstractSynthesisExtensions -import org.lflang.generator.ReactorInstance -import org.lflang.lf.Code -import org.lflang.lf.Host -import org.lflang.lf.Reactor -import org.lflang.lf.Value - -/** - * Extension class that provides various utility methods for the synthesis. - * - * @author{Alexander Schulz-Rosengarten } - */ -@ViewSynthesisShared -class UtilityExtensions extends AbstractSynthesisExtensions { - - extension KGraphFactory = KGraphFactory.eINSTANCE - - /** - * Converts a timing value into readable text - */ - def String toText(Value value) { - if (value !== null) { - if (value.parameter !== null) { - return value.parameter.name - } else if (value.time !== null) { - return value.time.interval + - value.time.unit.toString - } else if (value.literal !== null) { - return value.literal - } else if (value.code !== null) { - ASTUtils.toText(value.code) - } - } - return "" - } - - /** - * Converts a host value into readable text - */ - def String toText(Host host) { - val sb = new StringBuilder - if (host !== null) { - if (!host.user.nullOrEmpty) { - sb.append(host.user).append("@") - } - if (!host.addr.nullOrEmpty) { - sb.append(host.addr) - } - if (host.port !== 0) { - sb.append(":").append(host.port) - } - } - return sb.toString - } - - /** - * Returns true if the reactor is the primary reactor - */ - def isMainOrFederated(Reactor reactor) { - return reactor.main || reactor.federated - } - - /** - * Returns true if the instance is a bank of reactors - */ -// def boolean isBank(Instantiation ins) { -// return ins?.widthSpec !== null ? ins.widthSpec.width !== 1 : false -// } - - /** - * Returns true if the reactor as has inner reactions or instances - */ - def hasContent(ReactorInstance reactor) { - return !reactor.reactions.empty || !reactor.instantiations.empty - } - - /** - * - */ - def isRoot(ReactorInstance ri) { - return ri.parent === null - } - - /** - * Trims the hostcode of reactions. - */ - def trimCode(Code tokenizedCode) { - if (tokenizedCode === null || tokenizedCode.body.nullOrEmpty) { - return "" - } - try { - val code = NodeModelUtils.findActualNodeFor(tokenizedCode)?.text - var contentStart = 0 - val lines = newArrayList() - lines += code.split("\n").dropWhile[!it.contains("{=")] - - // Remove start pattern - if (!lines.empty) { - if (lines.head.trim().equals("{=")) { - lines.remove(0) // skip - } else { - lines.set(0, lines.head.replace("{=", "").trim()) - contentStart = 1 - } - } - - // Remove end pattern - if (!lines.empty) { - if (lines.last.trim.equals("=}")) { - lines.remove(lines.size - 1) // skip - } else { - lines.set(lines.size - 1, lines.last.replace("=}", "")) - } - } - - // Find indentation - var String indentation = null - while (indentation === null && lines.size > contentStart) { - val firstLine = lines.get(contentStart) - val trimmed = firstLine.trim() - if (trimmed.empty) { - lines.set(contentStart, "") - contentStart++ - } else { - val firstCharIdx = firstLine.indexOf(trimmed.charAt(0)) - indentation = firstLine.substring(0, firstCharIdx) - } - } - - // Remove root indentation - if (!lines.empty) { - for (i : 0..lines.size-1) { - if (lines.get(i).startsWith(indentation)) { - lines.set(i, lines.get(i).substring(indentation.length)) - } - } - } - - return lines.join("\n") - } catch(Exception e) { - e.printStackTrace - return tokenizedCode.body - } - } - - /** - * Sets KGE ID. - */ - def setID(KGraphElement kge, String id) { - kge.data.add(createKIdentifier => [it.setId(id)]) - } - - /** - * Retrieves the source element of the given diagram element - */ - def Object sourceElement(KGraphElement elem) { - return elem.getProperty(KlighdInternalProperties.MODEL_ELEMEMT) - } - - /** - * Checks if the source element of the given diagram element is a reactor - */ - def boolean sourceIsReactor(KNode node) { - return node.sourceElement() instanceof Reactor - } - - /** - * Returns the port placement margins for the node. - * If this spacing does not yet exist, the properties are initialized. - */ - def getPortMarginsInitIfAbsent(KNode node) { - var spacing = node.getProperty(CoreOptions.SPACING_INDIVIDUAL) - if (spacing === null) { - spacing = new IndividualSpacings() - node.setProperty(CoreOptions.SPACING_INDIVIDUAL, spacing) - } - var margin = spacing.getProperty(CoreOptions.SPACING_PORTS_SURROUNDING) - if (margin === null) { - margin = new ElkMargin() - node.setProperty(CoreOptions.SPACING_PORTS_SURROUNDING, margin) - } - return margin - } - -} diff --git a/org.lflang.lfc/src/org/lflang/lfc/Main.java b/org.lflang.lfc/src/org/lflang/lfc/Main.java index b30ae12e2f..829a854204 100644 --- a/org.lflang.lfc/src/org/lflang/lfc/Main.java +++ b/org.lflang.lfc/src/org/lflang/lfc/Main.java @@ -46,11 +46,14 @@ /** * Standalone version of the Lingua Franca compiler (lfc). * - * @author{Marten Lohstroh } - * @author{Christian Menard } + * @author {Marten Lohstroh } + * @author {Christian Menard } */ public class Main { + /// current lfc version as printed by --version + private static final String VERSION = "0.1.0-beta"; + /** * Object for interpreting command line arguments. */ @@ -103,18 +106,20 @@ public class Main { * @author Marten Lohstroh */ enum CLIOption { - COMPILER(null, "target-compiler", true, false, "Target compiler to invoke.", true), CLEAN("c", "clean", false, false, "Clean before building.", true), + COMPILER(null, "target-compiler", true, false, "Target compiler to invoke.", true), + EXTERNAL_RUNTIME_PATH(null, "external-runtime-path", true, false, "Specify an external runtime library to be used by the compiled binary.", true), + FEDERATED("f", "federated", false, false, "Treat main reactor as federated.", false), 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), OUTPUT_PATH("o", "output-path", true, false, "Specify the root output directory.", false), - RUNTIME_VERSION(null, "runtime-version", true, false, "Specify the version of the runtime library used for compiling LF programs.", true), - EXTERNAL_RUNTIME_PATH(null, "external-runtime-path", true, false, "Specify an external runtime library to be used by the compiled binary.", true), QUIET("q", "quiet", false, false, "Suppress output of the target compiler and other commands", true), - RTI("r", "rti", true, false, "Specify the location of the RTI.", true); + RTI("r", "rti", true, false, "Specify the location of the RTI.", true), + RUNTIME_VERSION(null, "runtime-version", true, false, "Specify the version of the runtime library used for compiling LF programs.", true), + SCHEDULER("s", "scheduler", true, false, "Specify the runtime scheduler (if supported).", true), + THREADS("t", "threads", true, false, "Specify the default number of threads.", true), + VERSION(null, "version", false, false, "Print version inforomation.", false); /** * The corresponding Apache CLI Option object. @@ -194,11 +199,18 @@ public static void main(final String[] args) { try { main.cmd = parser.parse(options, args, true); + // If requested, print help and abort if (main.cmd.hasOption(CLIOption.HELP.option.getOpt())) { formatter.printHelp("lfc", options); System.exit(0); } + // If requested, print version and abort + if (main.cmd.hasOption(CLIOption.VERSION.option.getLongOpt())) { + System.out.println("lfc " + VERSION); + System.exit(0); + } + List files = main.cmd.getArgList(); if (files.size() < 1) { @@ -292,7 +304,7 @@ private void runGenerator(List files, Injector injector) { * If some errors were collected, print them and abort execution. Otherwise return. */ private void exitIfCollectedErrors() { - if (issueCollector.getErrorsOccurred()) { + if (issueCollector.getErrorsOccurred() ) { // if there are errors, don't print warnings. List errors = printErrorsIfAny(); String cause = errors.size() == 1 ? "previous error" @@ -318,6 +330,7 @@ public List printErrorsIfAny() { // visible in tests public Resource getValidatedResource(Path path) { final Resource resource = getResource(path); + assert resource != null; if (cmd != null && cmd.hasOption(CLIOption.FEDERATED.option.getOpt())) { if (!ASTUtils.makeFederated(resource)) { @@ -341,8 +354,13 @@ public Resource getValidatedResource(Path path) { return resource; } - public Resource getResource(Path path) { + private Resource getResource(Path path) { final ResourceSet set = this.resourceSetProvider.get(); - return set.getResource(URI.createFileURI(path.toString()), true); + try { + return set.getResource(URI.createFileURI(path.toString()), true); + } catch (RuntimeException e) { + reporter.printFatalErrorAndExit(path + " is not an LF file. Use the .lf file extension to denote LF files."); + return null; + } } } diff --git a/org.lflang.tests/build.gradle b/org.lflang.tests/build.gradle index 4261fbc338..df44eaa000 100644 --- a/org.lflang.tests/build.gradle +++ b/org.lflang.tests/build.gradle @@ -46,6 +46,8 @@ test { events "passed", "skipped", "failed" showStandardStreams = true } + // Pass the scheduler property on to the Java VM + systemProperty 'scheduler', System.getProperty('scheduler') // Suggested by Gradle documentation: https://guides.gradle.org/performance/#parallel_test_execution maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 useJUnitPlatform() diff --git a/org.lflang.tests/src/org/lflang/tests/AbstractTest.java b/org.lflang.tests/src/org/lflang/tests/AbstractTest.java index 4e707e1fe2..5299475590 100644 --- a/org.lflang.tests/src/org/lflang/tests/AbstractTest.java +++ b/org.lflang.tests/src/org/lflang/tests/AbstractTest.java @@ -63,7 +63,6 @@ protected boolean supportsDockerOption() { return false; } - @Test public void runExampleTests() { runTestsForTargets(Message.DESC_EXAMPLE_TESTS, diff --git a/org.lflang.tests/src/org/lflang/tests/Configurators.java b/org.lflang.tests/src/org/lflang/tests/Configurators.java index 872a4341b7..1e64dc5f5e 100644 --- a/org.lflang.tests/src/org/lflang/tests/Configurators.java +++ b/org.lflang.tests/src/org/lflang/tests/Configurators.java @@ -51,7 +51,7 @@ public interface Configurator { * @param test The test to configure. * @return True if successful, false otherwise. */ - static boolean useSingleThread(LFTest test) { + public static boolean useSingleThread(LFTest test) { test.getContext().getArgs().setProperty("threads", "0"); return true; } @@ -62,7 +62,7 @@ static boolean useSingleThread(LFTest test) { * @param test The test to configure * @return True if successful, false otherwise. */ - static boolean useFourThreads(LFTest test) { + public static boolean useFourThreads(LFTest test) { test.getContext().getArgs().setProperty("threads", "4"); return true; } @@ -73,7 +73,7 @@ static boolean useFourThreads(LFTest test) { * @param test The test to configure. * @return True */ - static boolean noChanges(LFTest test) { + public static boolean noChanges(LFTest test) { return true; } diff --git a/org.lflang.tests/src/org/lflang/tests/TestBase.java b/org.lflang.tests/src/org/lflang/tests/TestBase.java index 5a2263e72d..46374aaaf4 100644 --- a/org.lflang.tests/src/org/lflang/tests/TestBase.java +++ b/org.lflang.tests/src/org/lflang/tests/TestBase.java @@ -139,6 +139,7 @@ public static class Message { public static final String DESC_TARGET_SPECIFIC = "Run target-specific tests (threads = 0)"; public static final String DESC_AS_CCPP = "Running C tests as CCpp."; public static final String DESC_FOUR_THREADS = "Run non-concurrent and non-federated tests (threads = 4)."; + public static final String DESC_SCHED_SWAPPING = "Running with non-default runtime scheduler "; /* Missing dependency messages */ public static final String MISSING_DOCKER = "Executable 'docker' not found or 'docker' daemon thread not running"; diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java index 75d49d89f8..77fa72d11f 100644 --- a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java +++ b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java @@ -484,6 +484,45 @@ public void connectionToEffectPort3() throws Exception { // reactor Foo { // input in:int; // } +// reactor Bar { +// input in:int; +// x1 = new Foo(); +// x2 = new Foo(); +// in -> x1.in; +// reaction(startup) -> x2.in {= +// =} +// } +// """ +// Java 11: + String testCase = String.join(System.getProperty("line.separator"), + "target C;", + "", + "reactor Foo {", + " input in:int;", + "}", + "reactor Bar {", + " input in:int;", + " x1 = new Foo();", + " x2 = new Foo();", + " in -> x1.in;", + " reaction(startup) -> x2.in {=", + " =}", + "}"); + validator.assertNoErrors(parseWithoutError(testCase)); + } + + /** + * Allow connection to the port of a contained reactor if another port with same name is effect of a reaction. + */ + @Test + public void connectionToEffectPort3_5() throws Exception { +// Java 17: +// String testCase = """ +// target C; +// +// reactor Foo { +// input in:int; +// } // main reactor { // input in:int; // x1 = new Foo(); @@ -508,7 +547,8 @@ public void connectionToEffectPort3() throws Exception { " reaction(startup) -> x2.in {=", " =}", "}"); - validator.assertNoErrors(parseWithoutError(testCase)); + validator.assertError(parseWithoutError(testCase), LfPackage.eINSTANCE.getVariable(), null, + "Main reactor cannot have inputs."); } /** @@ -2024,9 +2064,8 @@ public void testMultipleMainReactorUnnamed() throws Exception { "main reactor {}", "main reactor {}" ); - // TODO: Uncomment and fix test. See issue #905 on Github. - // validator.assertError(parseWithoutError(testCase), LfPackage.eINSTANCE.getReactor(), null, - // "Multiple definitions of main or federated reactor."); + validator.assertError(parseWithoutError(testCase), LfPackage.eINSTANCE.getReactor(), null, + "Multiple definitions of main or federated reactor."); } @Test diff --git a/org.lflang.tests/src/org/lflang/tests/runtime/CSchedulerTest.java b/org.lflang.tests/src/org/lflang/tests/runtime/CSchedulerTest.java new file mode 100644 index 0000000000..c4fa0a2b2b --- /dev/null +++ b/org.lflang.tests/src/org/lflang/tests/runtime/CSchedulerTest.java @@ -0,0 +1,75 @@ +package org.lflang.tests.runtime; + +import java.util.EnumSet; + +import org.junit.jupiter.api.Test; + +import org.lflang.Target; +import org.lflang.TargetProperty.SchedulerOption; +import org.lflang.tests.Configurators; +import org.lflang.tests.TestBase; +import org.lflang.tests.TestRegistry.TestCategory; + +/** + */ +public class CSchedulerTest extends TestBase { + + + public CSchedulerTest() { + super(Target.C); + } + + /** + * Swap the default runtime scheduler with other supported versions and + * run all the supported tests. Only run tests for a specific non-default + * scheduler if specified using a system property (e.g., -Dscheduler=GEDF_NP). + */ + @Test + public void runWithNonDefaultSchedulers() { + EnumSet categories = EnumSet.of(TestCategory.CONCURRENT, + TestCategory.MULTIPORT); + + // Add federated and docker tests if supported + if (!isWindows()) { + categories.add(TestCategory.FEDERATED); + if (isLinux()) { + categories.add(TestCategory.DOCKER_FEDERATED); + } + } + var name = System.getProperty("scheduler"); + + if (name != null) { + var option = EnumSet.allOf(SchedulerOption.class).stream() + .filter(it -> it.name().equals(name)).findFirst(); + if (option.isPresent()) { + this.runTest(option.get(), categories); + } else { + throw new RuntimeException("Cannot find runtime scheduler called " + name); + } + } else { + for (SchedulerOption scheduler: EnumSet.allOf(SchedulerOption.class)) { + if (scheduler == SchedulerOption.getDefault()) continue; + this.runTest(scheduler, categories); + } + } + } + + private void runTest(SchedulerOption scheduler, EnumSet categories) { + this.runTestsForTargets( + Message.DESC_SCHED_SWAPPING + scheduler.toString() +".", + categories::contains, + test -> { + test.getContext() + .getArgs() + .setProperty( + "scheduler", + scheduler.toString() + ); + return Configurators.useFourThreads(test); + }, + TestLevel.EXECUTION, + true + ); + } +} + diff --git a/org.lflang.tests/src/org/lflang/tests/runtime/CTest.java b/org.lflang.tests/src/org/lflang/tests/runtime/CTest.java index c8909202f2..4873ff1266 100644 --- a/org.lflang.tests/src/org/lflang/tests/runtime/CTest.java +++ b/org.lflang.tests/src/org/lflang/tests/runtime/CTest.java @@ -1,5 +1,3 @@ -/* Scoping unit tests. */ - /************* Copyright (c) 2019, The University of California at Berkeley. @@ -100,7 +98,7 @@ public void runMultiportTests() { public void runWithFourThreads() { super.runWithFourThreads(); } - + @Test @Override public void runSerializationTests() { diff --git a/org.lflang/META-INF/MANIFEST.MF b/org.lflang/META-INF/MANIFEST.MF index 4cde9f638e..72990dd479 100644 --- a/org.lflang/META-INF/MANIFEST.MF +++ b/org.lflang/META-INF/MANIFEST.MF @@ -22,7 +22,8 @@ Require-Bundle: org.eclipse.xtext, org.eclipse.lsp4j;bundle-version="0.12.0", com.fasterxml.jackson.core.jackson-core, com.fasterxml.jackson.core.jackson-annotations, - com.fasterxml.jackson.core.jackson-databind + com.fasterxml.jackson.core.jackson-databind, + org.eclipse.core.runtime;bundle-version="3.23.0" Bundle-RequiredExecutionEnvironment: JavaSE-11 Export-Package: org.lflang, org.lflang.generator, diff --git a/org.lflang/pom.xml b/org.lflang/pom.xml index f972ba405b..a48eff8fae 100644 --- a/org.lflang/pom.xml +++ b/org.lflang/pom.xml @@ -60,6 +60,11 @@ ${mwe2LaunchVersion} + + org.eclipse.platform + org.eclipse.core.runtime + ${runtimeVersion} + org.eclipse.xtext org.eclipse.xtext.common.types diff --git a/org.lflang/src/lib/c/reactor-c b/org.lflang/src/lib/c/reactor-c index 25362b8bff..e7306edb43 160000 --- a/org.lflang/src/lib/c/reactor-c +++ b/org.lflang/src/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit 25362b8bffd9b1556188ede42350ebac8b15bb2d +Subproject commit e7306edb430560aca043dc47345e24bf5f1f87d3 diff --git a/org.lflang/src/lib/cpp/reactor-cpp b/org.lflang/src/lib/cpp/reactor-cpp new file mode 160000 index 0000000000..604c9771a6 --- /dev/null +++ b/org.lflang/src/lib/cpp/reactor-cpp @@ -0,0 +1 @@ +Subproject commit 604c9771a68abc3f7a3afe7bd23e6c4db32dcdd3 diff --git a/org.lflang/src/lib/py/reactor-c-py b/org.lflang/src/lib/py/reactor-c-py index 4e19241ef2..9e78ad3596 160000 --- a/org.lflang/src/lib/py/reactor-c-py +++ b/org.lflang/src/lib/py/reactor-c-py @@ -1 +1 @@ -Subproject commit 4e19241ef26662a006c72e4dcc7543c0d3840179 +Subproject commit 9e78ad35961e559d88abfaf950f76a5f2d4323e7 diff --git a/org.lflang/src/org/lflang/ASTUtils.java b/org.lflang/src/org/lflang/ASTUtils.java index f9de1f8083..4a821000b6 100644 --- a/org.lflang/src/org/lflang/ASTUtils.java +++ b/org.lflang/src/org/lflang/ASTUtils.java @@ -32,9 +32,8 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; - -import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; +import java.util.Set; +import java.util.function.Function; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; @@ -47,12 +46,11 @@ import org.eclipse.xtext.nodemodel.impl.HiddenLeafNode; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.resource.XtextResource; -import org.eclipse.xtext.xbase.lib.CollectionExtensions; import org.eclipse.xtext.xbase.lib.IterableExtensions; import org.eclipse.xtext.xbase.lib.IteratorExtensions; import org.eclipse.xtext.xbase.lib.StringExtensions; -import org.lflang.generator.GeneratorBase; import org.lflang.generator.CodeMap; +import org.lflang.generator.GeneratorBase; import org.lflang.generator.InvalidSourceException; import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; @@ -84,6 +82,9 @@ import org.lflang.lf.WidthSpec; import org.lflang.lf.WidthTerm; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; + /** * A helper class for modifying and analyzing the AST. * @author{Marten Lohstroh } @@ -447,13 +448,7 @@ public static String getUniqueIdentifier(Reactor reactor, String name) { * @param definition Reactor class definition. */ public static List allActions(Reactor definition) { - List result = new ArrayList<>(); - List superClasses = convertToEmptyListIfNull(definition.getSuperClasses()); - for (ReactorDecl base : superClasses) { - result.addAll(allActions(toDefinition(base))); - } - result.addAll(definition.getActions()); - return result; + return ASTUtils.collectElements(definition, (Reactor r) -> r.getActions()); } /** @@ -462,28 +457,19 @@ public static List allActions(Reactor definition) { * @param definition Reactor class definition. */ public static List allConnections(Reactor definition) { - List result = new ArrayList<>(); - List superClasses = convertToEmptyListIfNull(definition.getSuperClasses()); - for (ReactorDecl base : superClasses) { - result.addAll(allConnections(toDefinition(base))); - } - result.addAll(definition.getConnections()); - return result; + return ASTUtils.collectElements(definition, (Reactor r) -> r.getConnections()); } /** * Given a reactor class, return a list of all its inputs, * which includes inputs of base classes that it extends. + * If the base classes include a cycle, where X extends Y and Y extends X, + * then return only the input defined in the base class. + * The returned list may be empty. * @param definition Reactor class definition. */ public static List allInputs(Reactor definition) { - List result = new ArrayList<>(); - List superClasses = convertToEmptyListIfNull(definition.getSuperClasses()); - for (ReactorDecl base : superClasses) { - result.addAll(allInputs(toDefinition(base))); - } - result.addAll(definition.getInputs()); - return result; + return ASTUtils.collectElements(definition, (Reactor r) -> r.getInputs()); } /** @@ -492,13 +478,7 @@ public static List allInputs(Reactor definition) { * @param definition Reactor class definition. */ public static List allInstantiations(Reactor definition) { - List result = new ArrayList<>(); - List superClasses = convertToEmptyListIfNull(definition.getSuperClasses()); - for (ReactorDecl base : superClasses) { - result.addAll(allInstantiations(toDefinition(base))); - } - result.addAll(definition.getInstantiations()); - return result; + return ASTUtils.collectElements(definition, (Reactor r) -> r.getInstantiations()); } /** @@ -507,13 +487,7 @@ public static List allInstantiations(Reactor definition) { * @param definition Reactor class definition. */ public static List allOutputs(Reactor definition) { - List result = new ArrayList<>(); - List superClasses = convertToEmptyListIfNull(definition.getSuperClasses()); - for (ReactorDecl base : superClasses) { - result.addAll(allOutputs(toDefinition(base))); - } - result.addAll(definition.getOutputs()); - return result; + return ASTUtils.collectElements(definition, (Reactor r) -> r.getOutputs()); } /** @@ -522,13 +496,7 @@ public static List allOutputs(Reactor definition) { * @param definition Reactor class definition. */ public static List allParameters(Reactor definition) { - List result = new ArrayList<>(); - List superClasses = convertToEmptyListIfNull(definition.getSuperClasses()); - for (ReactorDecl base : superClasses) { - result.addAll(allParameters(toDefinition(base))); - } - result.addAll(definition.getParameters()); - return result; + return ASTUtils.collectElements(definition, (Reactor r) -> r.getParameters()); } /** @@ -537,13 +505,7 @@ public static List allParameters(Reactor definition) { * @param definition Reactor class definition. */ public static List allReactions(Reactor definition) { - List result = new ArrayList<>(); - List superClasses = convertToEmptyListIfNull(definition.getSuperClasses()); - for (ReactorDecl base : superClasses) { - result.addAll(allReactions(toDefinition(base))); - } - result.addAll(definition.getReactions()); - return result; + return ASTUtils.collectElements(definition, (Reactor r) -> r.getReactions()); } /** @@ -552,13 +514,7 @@ public static List allReactions(Reactor definition) { * @param definition Reactor class definition. */ public static List allStateVars(Reactor definition) { - List result = new ArrayList<>(); - List superClasses = convertToEmptyListIfNull(definition.getSuperClasses()); - for (ReactorDecl base : superClasses) { - result.addAll(allStateVars(toDefinition(base))); - } - result.addAll(definition.getStateVars()); - return result; + return ASTUtils.collectElements(definition, (Reactor r) -> r.getStateVars()); } /** @@ -567,12 +523,43 @@ public static List allStateVars(Reactor definition) { * @param definition Reactor class definition. */ public static List allTimers(Reactor definition) { - List result = new ArrayList<>(); - List superClasses = convertToEmptyListIfNull(definition.getSuperClasses()); - for (ReactorDecl base : superClasses) { - result.addAll(allTimers(toDefinition(base))); + return ASTUtils.collectElements(definition, (Reactor r) -> r.getTimers()); + } + + /** + * Return all the superclasses of the specified reactor + * in deepest-first order. For example, if A extends B and C, and + * B and C both extend D, this will return the list [D, B, C, A]. + * Duplicates are removed. If the specified reactor does not extend + * any other reactor, then return an empty list. + * If a cycle is found, where X extends Y and Y extends X, or if + * a superclass is declared that is not found, then return null. + * @param reactor The specified reactor. + */ + public static LinkedHashSet superClasses(Reactor reactor) { + return superClasses(reactor, new LinkedHashSet()); + } + + /** + * Collect elements of type T from the class hierarchy defined by + * a given reactor definition. + * @param definition The reactor definition. + * @param elements A function that maps a reactor definition to a list of + * elements of type T. + * @param The type of elements to collect (e.g., Port, Timer, etc.) + * @return + */ + public static List collectElements(Reactor definition, Function> elements) { + List result = new ArrayList(); + // Add elements of elements defined in superclasses. + LinkedHashSet s = superClasses(definition); + if (s != null) { + for (Reactor superClass : s) { + result.addAll(elements.apply(superClass)); + } } - result.addAll(definition.getTimers()); + // Add elements of the current reactor. + result.addAll(elements.apply(definition)); return result; } @@ -894,7 +881,7 @@ public static String baseType(Type type) { /** * Report whether the given literal is zero or not. - * @param literalOrCode AST node to inspect. + * @param literal AST node to inspect. * @return True if the given literal denotes the constant `0`, false * otherwise. */ @@ -997,7 +984,7 @@ public static boolean isValidTime(Value value) { /** * Report whether the given time denotes a valid time or not. - * @param value AST node to inspect. + * @param t AST node to inspect. * @return True if the argument denotes a valid time, false otherwise. */ public static boolean isValidTime(Time t) { @@ -1010,7 +997,7 @@ public static boolean isValidTime(Time t) { /** * Report whether the given parameter denotes time list, meaning it is a list * of which all elements are valid times. - * @param value AST node to inspect. + * @param p AST node to inspect. * @return True if the argument denotes a valid time list, false otherwise. */ // TODO: why does this function always return true ??? @@ -1094,7 +1081,7 @@ public static boolean isValidTimeList(Parameter p) { * ``` * * @param parameter The parameter. - * @param instantiation The (optional) instantiation. + * @param instantiations The (optional) list of instantiations. * * @return The value of the parameter. * @@ -1161,7 +1148,7 @@ public static List initialValue(Parameter parameter, List * belongs to the specified instantiation, meaning that it is defined in * the reactor class being instantiated or one of its base classes. * @param eobject The object. - * @param instnatiation The instantiation. + * @param instantiation The instantiation. */ public static boolean belongsTo(EObject eobject, Instantiation instantiation) { Reactor reactor = toDefinition(instantiation.getReactorClass()); @@ -1173,7 +1160,7 @@ public static boolean belongsTo(EObject eobject, Instantiation instantiation) { * belongs to the specified reactor, meaning that it is defined in * reactor class or one of its base classes. * @param eobject The object. - * @param instnatiation The instantiation. + * @param reactor The reactor. */ public static boolean belongsTo(EObject eobject, Reactor reactor) { if (eobject.eContainer() == reactor) return true; @@ -1190,7 +1177,7 @@ public static boolean belongsTo(EObject eobject, Reactor reactor) { * if it does not have an integer value. * If the value of the parameter is a list of integers, * return the sum of value in the list. - * The instantiations parameter is as in + * The instantiations parameter is as in * {@link initialValue(Parameter, List)}. * * @param parameter The parameter. @@ -1490,7 +1477,7 @@ public static boolean isGeneric(Reactor r) { * return the imported reactor class definition. Otherwise, * just return the argument. * @param r A Reactor or an ImportedReactor. - * @return The Reactor class definition. + * @return The Reactor class definition or null if no definition is found. */ public static Reactor toDefinition(ReactorDecl r) { if (r == null) @@ -1640,11 +1627,43 @@ public static TargetDecl targetDecl(Model model) { public static TargetDecl targetDecl(Resource model) { return IteratorExtensions.head(Iterators.filter(model.getAllContents(), TargetDecl.class)); } - + + ///////////////////////////////////////////////////////// + //// Private methods + /** * Returns the list if it is not null. Otherwise return an empty list. */ - private static List convertToEmptyListIfNull(List list) { + public static List convertToEmptyListIfNull(List list) { return list != null ? list : new ArrayList<>(); } + + /** + * Return all the superclasses of the specified reactor + * in deepest-first order. For example, if A extends B and C, and + * B and C both extend D, this will return the list [D, B, C, A]. + * Duplicates are removed. If the specified reactor does not extend + * any other reactor, then return an empty list. + * If a cycle is found, where X extends Y and Y extends X, or if + * a superclass is declared that is not found, then return null. + * @param reactor The specified reactor. + * @param extensions A set of reactors extending the specified reactor + * (used to detect circular extensions). + */ + private static LinkedHashSet superClasses(Reactor reactor, Set extensions) { + LinkedHashSet result = new LinkedHashSet(); + for (ReactorDecl superDecl : convertToEmptyListIfNull(reactor.getSuperClasses())) { + Reactor r = toDefinition(superDecl); + if (r == reactor || r == null) return null; + // If r is in the extensions, then we have a circular inheritance structure. + if (extensions.contains(r)) return null; + extensions.add(r); + LinkedHashSet baseExtends = superClasses(r, extensions); + extensions.remove(r); + if (baseExtends == null) return null; + result.addAll(baseExtends); + result.add(r); + } + return result; + } } diff --git a/org.lflang/src/org/lflang/FileConfig.java b/org.lflang/src/org/lflang/FileConfig.java index 15c512e7cc..234b094581 100644 --- a/org.lflang/src/org/lflang/FileConfig.java +++ b/org.lflang/src/org/lflang/FileConfig.java @@ -1,16 +1,26 @@ package org.lflang; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.Arrays; import java.util.Comparator; +import java.util.Enumeration; import java.util.LinkedList; import java.util.List; import java.util.function.Consumer; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -19,6 +29,7 @@ import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IPath; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.Resource; @@ -166,7 +177,7 @@ public FileConfig(Resource resource, Path srcGenBasePath, LFGeneratorContext con this.srcFile = toPath(this.resource); this.srcPath = srcFile.getParent(); - this.srcPkgPath = getPkgPath(resource, context); + this.srcPkgPath = getPkgPath(resource); this.srcGenBasePath = srcGenBasePath; this.name = nameWithoutExtension(this.srcFile); @@ -220,12 +231,10 @@ public IResource getIResource(Resource r) throws IOException { java.net.URI uri = toPath(r).toFile().toURI(); if (r.getURI().isPlatform()) { IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); - if (uri != null) { - IFile[] files = workspaceRoot.findFilesForLocationURI(uri); - if (files != null && files.length > 0 && files[0] != null) { - iResource = files[0]; - } - } + IFile[] files = workspaceRoot.findFilesForLocationURI(uri); + if (files != null && files.length > 0 && files[0] != null) { + iResource = files[0]; + } } else { // FIXME: find the iResource outside Eclipse } @@ -241,15 +250,6 @@ public IResource getIResource(Path path) { return getIResource(path.toUri()); } - /** - * Get the specified path as an Eclipse IResource or, if it is not found, then - * return the iResource for the main file. - * - */ - public IResource getIResource(File file) { - return getIResource(file.toURI()); - } - /** * Get the specified uri as an Eclipse IResource or, if it is not found, then * return the iResource for the main file. @@ -405,9 +405,10 @@ protected static Path getSubPkgPath(Path pkgPath, Path srcPath) { * * @param src The source directory path. * @param dest The destination directory path. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed * @throws IOException if copy fails. */ - public static void copyDirectory(Path src, Path dest) throws IOException { + public static void copyDirectory(final Path src, final Path dest, final boolean skipIfUnchanged) throws IOException { try (Stream stream = Files.walk(src)) { stream.forEach(source -> { // Handling checked exceptions in lambda expressions is @@ -419,7 +420,7 @@ public static void copyDirectory(Path src, Path dest) throws IOException { try { Path target = dest.resolve(src.relativize(source)); Files.createDirectories(target.getParent()); - copyFile(source, target); + copyFile(source, target, skipIfUnchanged); } catch (IOException e) { throw new RuntimeIOException(e); } catch (Exception e) { @@ -430,27 +431,72 @@ public static void copyDirectory(Path src, Path dest) throws IOException { } } + /** + * Recursively copies the contents of the given 'src' + * directory to 'dest'. Existing files of the destination + * may be overwritten. + * + * @param src The source directory path. + * @param dest The destination directory path. + * @throws IOException if copy fails. + */ + public static void copyDirectory(final Path src, final Path dest) throws IOException { + copyDirectory(src, dest, false); + } + /** * Copy a given file from 'source' to 'destination'. * - * @param source The source file path string. - * @param destination The destination file path string. + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source file path. + * @param destination The destination file path. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed * @throws IOException if copy fails. */ - public static void copyFile(String source, String destination) throws IOException { - copyFile(Paths.get(source), Paths.get(destination)); + public static void copyFile(Path source, Path destination, boolean skipIfUnchanged) throws IOException { + BufferedInputStream stream = new BufferedInputStream(new FileInputStream(source.toFile())); + try (stream) { + copyInputStream(stream, destination, skipIfUnchanged); + } } /** * Copy a given file from 'source' to 'destination'. * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * * @param source The source file path. * @param destination The destination file path. * @throws IOException if copy fails. */ public static void copyFile(Path source, Path destination) throws IOException { + copyFile(source, destination, false); + } + + /** + * Copy a given input stream to a destination file. + * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source input stream. + * @param destination The destination file path. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed + * @throws IOException if copy fails. + */ + private static void copyInputStream(InputStream source, Path destination, boolean skipIfUnchanged) throws IOException { + Files.createDirectories(destination.getParent()); + if(skipIfUnchanged && Files.isRegularFile(destination)) { + if (Arrays.equals(source.readAllBytes(), Files.readAllBytes(destination))) { + return; + } + } Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); } + /** * Lookup a file in the classpath and copy its contents to a destination path @@ -461,32 +507,105 @@ public static void copyFile(Path source, Path destination) throws IOException { * * @param source The source file as a path relative to the classpath. * @param destination The file system path that the source file is copied to. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed * @throws IOException If the given source cannot be copied. */ - public void copyFileFromClassPath(String source, String destination) throws IOException { + public void copyFileFromClassPath(final String source, final Path destination, final boolean skipIfUnchanged) throws IOException { InputStream sourceStream = this.getClass().getResourceAsStream(source); // Copy the file. - try (sourceStream) { - if (sourceStream == null) { - throw new IOException( - "A required target resource could not be found: " + source + "\n" + - "Perhaps a git submodule is missing or not up to date.\n" + - "See https://github.com/icyphy/lingua-franca/wiki/downloading-and-building#clone-the-lingua-franca-repository.\n" - + - "Also try to refresh and clean the project explorer if working from eclipse."); + if (sourceStream == null) { + throw new IOException(unableToLocateResourceMessage(source)); + } else { + try (sourceStream) { + copyInputStream(sourceStream, destination, skipIfUnchanged); + } + } + } + + /** + * Lookup a file in the classpath and copy its contents to a destination path + * in the filesystem. + * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source file as a path relative to the classpath. + * @param destination The file system path that the source file is copied to. + * @throws IOException If the given source cannot be copied. + */ + public void copyFileFromClassPath(final String source, final Path destination) throws IOException { + copyFileFromClassPath(source, destination, false); + } + + /** + * Lookup a directory in the classpath and copy its contents to a destination path + * in the filesystem. + * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source directory as a path relative to the classpath. + * @param destination The file system path that the source directory is copied to. + * @param skipIfUnchanged If true, don't overwrite the file if its content would not be changed + * @throws IOException If the given source cannot be copied. + */ + public void copyDirectoryFromClassPath(final String source, final Path destination, final boolean skipIfUnchanged) throws IOException { + final URL resource = getClass().getResource(source); + if (resource == null) { + throw new IOException(unableToLocateResourceMessage(source)); + } + + final URLConnection connection = resource.openConnection(); + if (connection instanceof JarURLConnection) { + copyDirectoryFromJar((JarURLConnection) connection, destination, skipIfUnchanged); + } else { + try { + Path dir = Paths.get(FileLocator.toFileURL(resource).toURI()); + copyDirectory(dir, destination, skipIfUnchanged); + } catch(URISyntaxException e) { + // This should never happen as toFileURL should always return a valid URL + throw new IOException("Unexpected error while resolving " + source + " on the classpath"); + } + } + } + + /** + * Copy a directory from ta jar to a destination path in the filesystem. + * + * This method should only be used in standalone mode (lfc). + * + * This also creates new directories for any directories on the destination + * path that do not yet exist + * + * @param connection a URLConnection to the source directory within the jar + * @param destination The file system path that the source directory is copied to. + * @param skipIfUnchanged If true, don't overwrite the file if its content would not be changed + * @throws IOException If the given source cannot be copied. + */ + private void copyDirectoryFromJar(JarURLConnection connection, final Path destination, final boolean skipIfUnchanged) throws IOException { + final JarFile jar = connection.getJarFile(); + final String connectionEntryName = connection.getEntryName(); + + // Iterate all entries in the jar file. + for (Enumeration e = jar.entries(); e.hasMoreElements(); ) { + final JarEntry entry = e.nextElement(); + final String entryName = entry.getName(); + + // Extract files only if they match the given source path. + if (entryName.startsWith(connectionEntryName)) { + String filename = entry.getName().substring(connectionEntryName.length() + 1); + Path currentFile = destination.resolve(filename); + + if (entry.isDirectory()) { + Files.createDirectories(currentFile); + } else { + InputStream is = jar.getInputStream(entry); + try (is) { + copyInputStream(is, currentFile, skipIfUnchanged); + } + } } - // Make sure the directory exists - File destFile = new File(destination); - destFile.getParentFile().mkdirs(); - - Files.copy(sourceStream, Paths.get(destination), StandardCopyOption.REPLACE_EXISTING); - } catch (IOException ex) { - throw new IOException( - "A required target resource could not be copied: " + source + "\n" + - "Perhaps a git submodule is missing or not up to date.\n" + - "See https://github.com/icyphy/lingua-franca/wiki/downloading-and-building#clone-the-lingua-franca-repository.", - ex); } } @@ -497,9 +616,9 @@ public void copyFileFromClassPath(String source, String destination) throws IOEx * @param files The list of files to copy. * @throws IOException If any of the given files cannot be copied. */ - public void copyFilesFromClassPath(String srcDir, String dstDir, List files) throws IOException { + public void copyFilesFromClassPath(String srcDir, Path dstDir, List files) throws IOException { for (String file : files) { - copyFileFromClassPath(srcDir + '/' + file, dstDir + File.separator + file); + copyFileFromClassPath(srcDir + '/' + file, dstDir.resolve(file)); } } @@ -540,7 +659,7 @@ public String copyFileOrResource(String fileName, Path srcDir, Path dstDir) { if (lastSeparator > 0) { filenameWithoutPath = fileName.substring(lastSeparator + 1); // FIXME: brittle. What if the file is in a subdirectory? } - copyFileFromClassPath(fileName, dstDir + File.separator + filenameWithoutPath); + copyFileFromClassPath(fileName, dstDir.resolve(filenameWithoutPath)); return filenameWithoutPath; } catch (IOException ex) { // Ignore. Previously reported as a warning. @@ -571,7 +690,7 @@ public void cleanIfNeeded() { */ public void deleteDirectory(Path dir) throws IOException { if (Files.isDirectory(dir)) { - System.out.println("Cleaning " + dir.toString()); + System.out.println("Cleaning " + dir); List pathsToDelete = Files.walk(dir) .sorted(Comparator.reverseOrder()) .collect(Collectors.toList()); @@ -603,7 +722,7 @@ public void doClean() throws IOException { public void deleteBinFiles() { String name = nameWithoutExtension(this.srcFile); String[] files = this.binPath.toFile().list(); - List federateNames = new LinkedList(); // FIXME: put this in ASTUtils? + List federateNames = new LinkedList<>(); // FIXME: put this in ASTUtils? resource.getAllContents().forEachRemaining(node -> { if (node instanceof Reactor) { Reactor r = (Reactor) node; @@ -619,11 +738,13 @@ public void deleteBinFiles() { // Delete RTI file, if any. if (f.equals(name) || f.equals(getRTIBinName()) || f.equals(getRTIDistributionScriptName())) { + //noinspection ResultOfMethodCallIgnored this.binPath.resolve(f).toFile().delete(); } // Delete federate executable files, if any. for (String federateName : federateNames) { if (f.equals(name + "_" + federateName)) { + //noinspection ResultOfMethodCallIgnored this.binPath.resolve(f).toFile().delete(); } } @@ -636,7 +757,7 @@ public static String nameWithoutExtension(Path file) { return idx < 0 ? name : name.substring(0, idx); } - private static Path getPkgPath(Resource resource, LFGeneratorContext context) throws IOException { + private static Path getPkgPath(Resource resource) throws IOException { if (resource.getURI().isPlatform()) { // We are in the RCA. File srcFile = toPath(resource).toFile(); @@ -706,7 +827,7 @@ public static IPath toIPath(URI uri) throws IOException { } else if (uri.isFile()) { return new org.eclipse.core.runtime.Path(uri.toFileString()); } else { - throw new IOException("Unrecognized file protocol in URI " + uri.toString()); + throw new IOException("Unrecognized file protocol in URI " + uri); } } @@ -718,33 +839,6 @@ public static IPath toIPath(URI uri) throws IOException { public static String toUnixString(Path path) { return path.toString().replace('\\', '/'); } - - /** - * Check whether a given file (i.e., a relative path) exists in the given - *directory. - * @param filename String representation of the filename to search for. - * @param directory String representation of the director to search in. - */ - public static boolean fileExists(String filename, Path directory) { - // Make sure the file exists and issue a warning if not. - Path file = findFile(filename, directory); - if (file == null) { - // See if it can be found as a resource. - InputStream stream = FileConfig.class.getResourceAsStream(filename); - if (stream == null) { - return false; - } else { - // Sadly, even with this not null, the file may not exist. - try { - stream.read(); - stream.close(); - } catch (IOException ex) { - return false; - } - } - } - return true; - } /** * Search for a given file name in the given directory. @@ -810,14 +904,6 @@ public String getRTIDistributionScriptName() { return nameWithoutExtension(srcFile) + RTI_DISTRIBUTION_SCRIPT_SUFFIX; } - /** - * Return the file location of the RTI distribution script. - * @return The file location of the RTI distribution script. - */ - public File getRTIDistributionScriptFile() { - return this.binPath.resolve(getRTIDistributionScriptName()).toFile(); - } - /** * Return the name of the file associated with the given resource, * excluding its file extension. @@ -829,4 +915,18 @@ public File getRTIDistributionScriptFile() { public static String nameWithoutExtension(Resource r) throws IOException { return nameWithoutExtension(toPath(r)); } + + /** + * Return a string that explains why a resource may not have been found and + * what to do about it. + * + * @param fileName The Name of the file that could not be found. + * @return an explanation. + */ + public static String unableToLocateResourceMessage(String fileName) { + return "A required target resource could not be found: " + fileName + "\n" + + "Perhaps a git submodule is not initialized and/or not up to date.\n" + + "To initialize and update, run `git submodule update --init`.\n" + + "You may also need to refresh and clean your build system or IDE."; + } } diff --git a/org.lflang/src/org/lflang/ModelInfo.java b/org.lflang/src/org/lflang/ModelInfo.java index 5c2da9c2c0..d1748040a9 100644 --- a/org.lflang/src/org/lflang/ModelInfo.java +++ b/org.lflang/src/org/lflang/ModelInfo.java @@ -43,6 +43,7 @@ import org.lflang.lf.Model; import org.lflang.lf.Parameter; import org.lflang.lf.Reactor; +import org.lflang.lf.STP; /** @@ -78,6 +79,12 @@ public class ModelInfo { * interval. */ public Set overflowingDeadlines; + + /** + * The set of STP offsets that use a too-large constant to specify their time + * interval. + */ + public Set overflowingSTP; /** * The set of parameters used to specify a deadline while having been @@ -145,6 +152,7 @@ private void collectOverflowingNodes() { this.overflowingAssignments = new HashSet<>(); this.overflowingDeadlines = new HashSet<>(); this.overflowingParameters = new HashSet<>(); + this.overflowingSTP = new HashSet<>(); // Visit all deadlines in the model; detect possible overflow. for (var deadline : filter(toIterable(model.eAllContents()), Deadline.class)) { @@ -158,6 +166,13 @@ private void collectOverflowingNodes() { this.overflowingDeadlines.add(deadline); } } + // Visit all STP offsets in the model; detect possible overflow. + for (var stp : filter(toIterable(model.eAllContents()), STP.class)) { + // If the time value overflows, mark this deadline as overflowing. + if (isTooLarge(JavaAstUtils.getLiteralTimeValue(stp.getValue()))) { + this.overflowingSTP.add(stp); + } + } } /** diff --git a/org.lflang/src/org/lflang/TargetConfig.java b/org.lflang/src/org/lflang/TargetConfig.java index ae0631c0c5..ab277e2ae2 100644 --- a/org.lflang/src/org/lflang/TargetConfig.java +++ b/org.lflang/src/org/lflang/TargetConfig.java @@ -25,14 +25,17 @@ package org.lflang; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.lflang.TargetProperty.BuildType; import org.lflang.TargetProperty.ClockSyncMode; import org.lflang.TargetProperty.CoordinationType; import org.lflang.TargetProperty.LogLevel; +import org.lflang.TargetProperty.SchedulerOption; import org.lflang.generator.rust.RustTargetConfig; /** @@ -100,6 +103,14 @@ public class TargetConfig { * Additional sources to add to the compile command if appropriate. */ public List compileAdditionalSources = new ArrayList<>(); + + /** + * Additional (preprocessor) definitions to add to the compile command if appropriate. + * + * The first string is the definition itself, and the second string is the value to attribute to that definition, if any. + * The second value could be left empty. + */ + public Map compileDefinitions = new HashMap<>(); /** * Additional libraries to add to the compile command using the "-l" command-line option. @@ -189,6 +200,9 @@ public class TargetConfig { /** Whether all reactors are to be generated into a single target language file. */ public boolean singleFileProject = false; + + /** What runtime scheduler to use. */ + public SchedulerOption schedulerType = SchedulerOption.getDefault(); /** * The number of worker threads to deploy. The default is zero (i.e., diff --git a/org.lflang/src/org/lflang/TargetProperty.java b/org.lflang/src/org/lflang/TargetProperty.java index 7307470af2..af80f02cce 100644 --- a/org.lflang/src/org/lflang/TargetProperty.java +++ b/org.lflang/src/org/lflang/TargetProperty.java @@ -28,14 +28,12 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; import org.eclipse.xtext.util.RuntimeIOException; - import org.lflang.TargetConfig.DockerOptions; import org.lflang.TargetConfig.TracingOptions; import org.lflang.generator.InvalidLfSourceException; @@ -313,6 +311,16 @@ public enum TargetProperty { Arrays.asList(Target.CPP), (config, value, err) -> { config.runtimeVersion = ASTUtils.toText(value); }), + + + /** + * Directive for specifying a specific runtime scheduler, if supported. + */ + SCHEDULER("scheduler", UnionType.SCHEDULER_UNION, + Arrays.asList(Target.C, Target.CCPP, Target.Python), (config, value, err) -> { + config.schedulerType = (SchedulerOption) UnionType.SCHEDULER_UNION + .forName(ASTUtils.toText(value)); + }), /** * Directive to specify that all code is generated in a single file. @@ -760,6 +768,7 @@ public enum UnionType implements TargetPropertyType { BUILD_TYPE_UNION(Arrays.asList(BuildType.values()), null), COORDINATION_UNION(Arrays.asList(CoordinationType.values()), CoordinationType.CENTRALIZED), + SCHEDULER_UNION(Arrays.asList(SchedulerOption.values()), SchedulerOption.getDefault()), LOGGING_UNION(Arrays.asList(LogLevel.values()), LogLevel.INFO), CLOCK_SYNC_UNION(Arrays.asList(ClockSyncMode.values()), ClockSyncMode.INITIAL), @@ -1275,6 +1284,37 @@ public String toString() { return this.name().toLowerCase(); } } + + /** + * Supported schedulers. + * @author{Soroush Bateni } + */ + public enum SchedulerOption { + NP(false), // Non-preemptive + GEDF_NP(true), // Global EDF non-preemptive + GEDF_NP_CI(true); // Global EDF non-preemptive with chain ID + // PEDF_NP(true); // Partitioned EDF non-preemptive (FIXME: To be re-added in a future PR) + + /** + * Indicate whether or not the scheduler prioritizes reactions by deadline. + */ + private final Boolean prioritizesDeadline; + + /** + * Return true if the scheduler prioritizes reactions by deadline. + */ + public Boolean prioritizesDeadline() { + return this.prioritizesDeadline; + } + + private SchedulerOption(Boolean prioritizesDeadline) { + this.prioritizesDeadline = prioritizesDeadline; + } + + public static SchedulerOption getDefault() { + return NP; + } + } /** * Tracing options. diff --git a/org.lflang/src/org/lflang/federated/CGeneratorExtension.java b/org.lflang/src/org/lflang/federated/CGeneratorExtension.java index edd5ca02ac..90d3ba2994 100644 --- a/org.lflang/src/org/lflang/federated/CGeneratorExtension.java +++ b/org.lflang/src/org/lflang/federated/CGeneratorExtension.java @@ -32,10 +32,10 @@ import org.lflang.generator.ReactorInstance; 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; import org.lflang.lf.Parameter; -import org.lflang.lf.Port; import org.lflang.lf.Reactor; import org.lflang.lf.ReactorDecl; import org.lflang.lf.VarRef; @@ -102,8 +102,7 @@ public static String allocateTriggersForFederate( builder.append( "// Initialize the array of pointers to network input port triggers\n" + "_fed.triggers_for_network_input_control_reactions_size = " - + federate.networkInputControlReactionsTriggers - .size() + + federate.networkInputControlReactionsTriggers.size() + ";\n" + "_fed.triggers_for_network_input_control_reactions = (trigger_t**)malloc(" + "_fed.triggers_for_network_input_control_reactions_size * sizeof(trigger_t*)" @@ -151,7 +150,7 @@ public static StringBuilder initializeTriggerForControlReactions( String nameOfSelfStruct = CUtil.reactorRef(instance); // Initialize triggers for network input control reactions - for (Port trigger : federate.networkInputControlReactionsTriggers) { + for (Action trigger : federate.networkInputControlReactionsTriggers) { // Check if the trigger belongs to this reactor instance if (ASTUtils.allReactions(reactor).stream().anyMatch(r -> { return r.getTriggers().stream().anyMatch(t -> { @@ -167,8 +166,7 @@ public static StringBuilder initializeTriggerForControlReactions( + trigger.getName() + " to the global list of network input ports.\n" + "_fed.triggers_for_network_input_control_reactions[" - + federate.networkInputControlReactionsTriggers - .indexOf(trigger) + + federate.networkInputControlReactionsTriggers.indexOf(trigger) + "]= &" + nameOfSelfStruct + "" + "->_lf__" + trigger.getName() + ";\n"); } @@ -176,7 +174,7 @@ public static StringBuilder initializeTriggerForControlReactions( nameOfSelfStruct = CUtil.reactorRef(instance); - // Initialize the trigger for network output control reactions if it doesn't exists + // Initialize the trigger for network output control reactions if it doesn't exist. if (federate.networkOutputControlReactionsTrigger != null) { builder.append("_fed.trigger_for_network_output_control_reactions=&" + nameOfSelfStruct @@ -232,7 +230,7 @@ public static String createPortStatusFieldForInput(Input input, * @param generator * @return */ - public static String getNetworkDelayLiteral(Delay delay, CGenerator generator) { + public static String getNetworkDelayLiteral(Delay delay) { String additionalDelayString = "NEVER"; if (delay != null) { Parameter p = delay.getParameter(); diff --git a/org.lflang/src/org/lflang/federated/FedASTUtils.java b/org.lflang/src/org/lflang/federated/FedASTUtils.java index 81c7e5d5e0..03c408eef8 100644 --- a/org.lflang/src/org/lflang/federated/FedASTUtils.java +++ b/org.lflang/src/org/lflang/federated/FedASTUtils.java @@ -40,15 +40,14 @@ import org.lflang.InferredType; import org.lflang.JavaAstUtils; import org.lflang.TargetProperty.CoordinationType; -import org.lflang.federated.serialization.SupportedSerializers; import org.lflang.TimeValue; +import org.lflang.federated.serialization.SupportedSerializers; import org.lflang.generator.GeneratorBase; import org.lflang.generator.PortInstance; import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; import org.lflang.lf.Connection; import org.lflang.lf.Delay; -import org.lflang.lf.Input; import org.lflang.lf.Instantiation; import org.lflang.lf.LfFactory; import org.lflang.lf.Parameter; @@ -285,15 +284,15 @@ private static void addNetworkInputControlReaction( Reaction reaction = factory.createReaction(); VarRef sourceRef = factory.createVarRef(); VarRef destRef = factory.createVarRef(); - Type portType = EcoreUtil.copy(destination.getDefinition().getType()); // If the sender or receiver is in a bank of reactors, then we want // these reactions to appear only in the federate whose bank ID matches. generator.setReactionBankIndex(reaction, bankIndex); - // Create a new phantom Input port that will be used to trigger the + // Create a new action that will be used to trigger the // input control reactions. - Input newTriggerForControlReactionInput = factory.createInput(); + Action newTriggerForControlReactionInput = factory.createAction(); + newTriggerForControlReactionInput.setOrigin(ActionOrigin.LOGICAL); // Set the container and variable according to the network port destRef.setContainer(destination.getParent().getDefinition()); @@ -303,11 +302,11 @@ private static void addNetworkInputControlReaction( Reactor top = destination.getParent().getParent().reactorDefinition; - newTriggerForControlReactionInput.setName(ASTUtils.getUniqueIdentifier(top, "inputControlReactionTrigger")); - newTriggerForControlReactionInput.setType(portType); + newTriggerForControlReactionInput.setName( + ASTUtils.getUniqueIdentifier(top, "inputControlReactionTrigger")); - // Add the newly created Input to the input list of inputs of the federated reactor - top.getInputs().add(newTriggerForControlReactionInput); + // Add the newly created Action to the action list of the federated reactor. + top.getActions().add(newTriggerForControlReactionInput); // Create the trigger for the reaction VarRef newTriggerForControlReaction = factory.createVarRef(); @@ -598,10 +597,10 @@ private static void addNetworkOutputControlReaction( newPortRef.setVariable(source.getDefinition()); reaction.getSources().add(newPortRef); - // We use a phantom input port at the top-level to manually - // trigger output control reactions. That port is created once + // We use an action at the top-level to manually + // trigger output control reactions. That action is created once // and recorded in the federate instance. - // Check whether the port already has been created. + // Check whether the action already has been created. if (instance.networkOutputControlReactionsTrigger == null) { // The port has not been created. String triggerName = "outputControlReactionTrigger"; @@ -609,27 +608,22 @@ private static void addNetworkOutputControlReaction( // Find the trigger definition in the reactor definition, which could have been // generated for another federate instance if there are multiple instances // of the same reactor that are each distinct federates. - Optional optTriggerInput = top.getInputs().stream() - .filter(I -> I.getName().equals(triggerName)).findFirst(); + Optional optTriggerInput + = top.getActions().stream().filter( + I -> I.getName().equals(triggerName)).findFirst(); if (optTriggerInput.isEmpty()) { // If no trigger with the name "outputControlReactionTrigger" is // already added to the reactor definition, we need to create it - // for the first time. - Input newTriggerForControlReactionVariable = factory.createInput(); + // for the first time. The trigger is a logical action. + Action newTriggerForControlReactionVariable = factory.createAction(); newTriggerForControlReactionVariable.setName(triggerName); - - if (generator.getTarget().requiresTypes) { - // The input needs a type. All targets have a Time type, so we use that. - Type portType = factory.createType(); - portType.setId(generator.getTargetTypes().getTargetTimeType()); - newTriggerForControlReactionVariable.setType(portType); - } - - top.getInputs().add(newTriggerForControlReactionVariable); + newTriggerForControlReactionVariable.setOrigin(ActionOrigin.LOGICAL); + top.getActions().add(newTriggerForControlReactionVariable); // Now that the variable is created, store it in the federate instance - instance.networkOutputControlReactionsTrigger = newTriggerForControlReactionVariable; + instance.networkOutputControlReactionsTrigger + = newTriggerForControlReactionVariable; } else { // If the "outputControlReactionTrigger" trigger is already // there, we can re-use it for this new reaction since a single trigger diff --git a/org.lflang/src/org/lflang/federated/FederateInstance.xtend b/org.lflang/src/org/lflang/federated/FederateInstance.java similarity index 56% rename from org.lflang/src/org/lflang/federated/FederateInstance.xtend rename to org.lflang/src/org/lflang/federated/FederateInstance.java index 2fcdb3a78e..274d513657 100644 --- a/org.lflang/src/org/lflang/federated/FederateInstance.xtend +++ b/org.lflang/src/org/lflang/federated/FederateInstance.java @@ -24,35 +24,42 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ***************/ -package org.lflang.federated +package org.lflang.federated; -import java.util.ArrayList -import java.util.LinkedHashMap -import java.util.LinkedHashSet -import java.util.List -import java.util.Set -import org.lflang.ErrorReporter -import org.lflang.TimeValue -import org.lflang.generator.ActionInstance -import org.lflang.generator.GeneratorBase -import org.lflang.generator.PortInstance -import org.lflang.generator.ReactionInstance -import org.lflang.generator.ReactorInstance -import org.lflang.lf.Action -import org.lflang.lf.ActionOrigin -import org.lflang.lf.Delay -import org.lflang.lf.Input -import org.lflang.lf.Instantiation -import org.lflang.lf.Output -import org.lflang.lf.Port -import org.lflang.lf.Reaction -import org.lflang.lf.Reactor -import org.lflang.lf.Timer -import org.lflang.lf.TriggerRef -import org.lflang.lf.VarRef -import org.lflang.lf.Variable +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.TimeValue; +import org.lflang.generator.ActionInstance; +import org.lflang.generator.GeneratorBase; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.TriggerInstance; +import org.lflang.lf.Action; +import org.lflang.lf.ActionOrigin; +import org.lflang.lf.Delay; +import org.lflang.lf.Input; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Output; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; +import org.lflang.lf.Timer; +import org.lflang.lf.TriggerRef; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; + +import com.google.common.base.Objects; -import static extension org.lflang.ASTUtils.* /** * Instance of a federate, or marker that no federation has been defined @@ -62,7 +69,7 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * * @author{Edward A. Lee } */ -class FederateInstance { +public class FederateInstance { /** * Construct a new instance with the specified instantiation of @@ -78,25 +85,24 @@ class FederateInstance { * FIXME: Do we really need to pass the complete generator here? It is only used * to determine the number of federates. */ - new( - Instantiation instantiation, - int id, - int bankIndex, - GeneratorBase generator, - ErrorReporter errorReporter - ) { + public FederateInstance( + Instantiation instantiation, + int id, + int bankIndex, + GeneratorBase generator, + ErrorReporter errorReporter) { this.instantiation = instantiation; this.id = id; this.generator = generator; this.bankIndex = bankIndex; this.errorReporter = errorReporter; - if (instantiation !== null) { - this.name = instantiation.name; + if (instantiation != null) { + this.name = instantiation.getName(); // If the instantiation is in a bank, then we have to append // the bank index to the name. - if (instantiation.widthSpec !== null) { - this.name = instantiation.name + "__" + bankIndex; + if (instantiation.getWidthSpec() != null) { + this.name = instantiation.getName() + "__" + bankIndex; } } } @@ -108,18 +114,22 @@ class FederateInstance { * The position within a bank of reactors for this federate. * This is 0 if the instantiation is not a bank of reactors. */ - public var bankIndex = 0; + public int bankIndex = 0; /** * A list of outputs that can be triggered directly or indirectly by physical actions. */ - public var outputsConnectedToPhysicalActions = new LinkedHashSet() + public Set outputsConnectedToPhysicalActions = new LinkedHashSet<>(); - /** The host, if specified using the 'at' keyword. */ - public var String host = 'localhost' + /** + * The host, if specified using the 'at' keyword. + */ + public String host = "localhost"; - /** The instantiation of the top-level reactor, or null if there is no federation. */ - public var Instantiation instantiation; + /** + * The instantiation of the top-level reactor, or null if there is no federation. + */ + public Instantiation instantiation; /** * Map from the federates that this federate receives messages from @@ -127,103 +137,109 @@ class FederateInstance { * may may include null, meaning that there is a connection * from the federate instance that has no delay. */ - public var dependsOn = new LinkedHashMap>() + public Map> dependsOn = new LinkedHashMap<>(); - /** The directory, if specified using the 'at' keyword. */ - public var String dir = null - - /** The port, if specified using the 'at' keyword. */ - public var int port = 0 + /** + * The directory, if specified using the 'at' keyword. + */ + public String dir = null; - /** + /** + * The port, if specified using the 'at' keyword. + */ + public int port = 0; + + /** * Map from the federates that this federate sends messages to * to the delays on connections to that federate. The delay set * may may include null, meaning that there is a connection * from the federate instance that has no delay. */ - public var sendsTo = new LinkedHashMap>() - - /** The user, if specified using the 'at' keyword. */ - public var String user = null + public Map> sendsTo = new LinkedHashMap<>(); + + /** + * The user, if specified using the 'at' keyword. + */ + public String user = null; - /** The integer ID of this federate. */ - public var id = 0; + /** + * The integer ID of this federate. + */ + public int id = 0; /** * The name of this federate instance. This will be the instantiation * name, poassibly appended with "__n", where n is the bank position of * this instance if the instantiation is of a bank of reactors. */ - public var name = "Unnamed"; + public String name = "Unnamed"; - /** List of networkMessage actions. Each of these handles a message + /** + * List of networkMessage actions. Each of these handles a message * received from another federate over the network. The ID of * receiving port is simply the position of the action in the list. * The sending federate needs to specify this ID. */ - public var List networkMessageActions = new ArrayList() + public List networkMessageActions = new ArrayList<>(); - /** + /** * A set of federates with which this federate has an inbound connection * There will only be one physical connection even if federate A has defined multiple - * physical connections to federate B. The message handler on federate A will be + * physical connections to federate B. The message handler on federate A will be * responsible for including the appropriate information in the message header (such as port ID) * to help the receiver distinguish different events. */ - public var inboundP2PConnections = new LinkedHashSet() + public Set inboundP2PConnections = new LinkedHashSet<>(); /** - * A list of federate with which this federate has an outbound physical connection. + * A list of federate with which this federate has an outbound physical connection. * There will only be one physical connection even if federate A has defined multiple - * physical connections to federate B. The message handler on federate B will be + * physical connections to federate B. The message handler on federate B will be * responsible for distinguishing the incoming messages by parsing their header and * scheduling the appropriate action. */ - public var outboundP2PConnections = new LinkedHashSet() + public Set outboundP2PConnections = new LinkedHashSet<>(); - /** * A list of triggers for network input control reactions. This is used to trigger * all the input network control reactions that might be nested in a hierarchy. */ - public var List networkInputControlReactionsTriggers = new ArrayList(); - + public List networkInputControlReactionsTriggers = new ArrayList<>(); /** - * The trigger that triggers the output control reaction of this - * federate. + * The trigger that triggers the output control reaction of this + * federate. * - * The network output control reactions send a PORT_ABSENT message for a network output port, - * if it is absent at the current tag, to notify all downstream federates that no value will - * be present on the given network port, allowing input control reactions on those federates + * The network output control reactions send a PORT_ABSENT message for a network output port, + * if it is absent at the current tag, to notify all downstream federates that no value will + * be present on the given network port, allowing input control reactions on those federates * to stop blocking. */ - public var Variable networkOutputControlReactionsTrigger = null; + public Variable networkOutputControlReactionsTrigger = null; /** * Indicates whether the federate is remote or local */ - public var boolean isRemote = false; - + public boolean isRemote = false; /** * List of generated network reactions (network receivers, * network input control reactions, network senders, and network output control * reactions) that belong to this federate instance. */ - public List networkReactions = new ArrayList(); - - /** - * List of triggers of network reactions that belong to remote federates. - * These might need to be removed before code generation to avoid unnecessary compile - * errors, since they might reference structures that are not present in - * the current federate. Even though it is impossible for a trigger that is on a remote - * federate to trigger a reaction on this federate, these triggers need to be here - * to ensure that dependency analysis between reactions is done correctly. - * Without these triggers, the reaction precedence graph is broken and - * dependencies not properly represented. - */ - public List remoteNetworkReactionTriggers = new ArrayList(); + public List networkReactions = new ArrayList<>(); + + /** + * List of triggers of network reactions that belong to remote federates. + * These might need to be removed before code generation to avoid unnecessary compile + * errors, since they might reference structures that are not present in + * the current federate. Even though it is impossible for a trigger that is on a remote + * federate to trigger a reaction on this federate, these triggers need to be here + * to ensure that dependency analysis between reactions is done correctly. + * Without these triggers, the reaction precedence graph is broken and + * dependencies not properly represented. + */ + public List remoteNetworkReactionTriggers = new ArrayList<>(); ///////////////////////////////////////////// //// Public Methods @@ -237,31 +253,32 @@ class FederateInstance { * @param action The action * @return True if this federate contains the action in the specified reactor */ - def contains(Action action) { - val reactor = action.eContainer as Reactor - if (!reactor.federated || isSingleton) return true + public boolean contains(Action action) { + Reactor reactor = (Reactor) action.eContainer(); + if (!reactor.isFederated() || isSingleton()) return true; // If the action is used as a trigger, a source, or an effect for a top-level reaction // that belongs to this federate, then generate it. - for (react : reactor.allReactions) { + for (Reaction react : ASTUtils.allReactions(reactor)) { if (contains(react)) { // Look in triggers - for (TriggerRef trigger : react.triggers ?: emptyList) { + for (TriggerRef trigger : convertToEmptyListIfNull(react.getTriggers())) { if (trigger instanceof VarRef) { - if (trigger.variable == (action as Variable)) { + VarRef triggerAsVarRef = (VarRef) trigger; + if (Objects.equal(triggerAsVarRef.getVariable(), (Variable) action)) { return true; } } } // Look in sources - for (VarRef source : react.sources ?: emptyList) { - if (source.variable == (action as Variable)) { + for (VarRef source : convertToEmptyListIfNull(react.getSources())) { + if (Objects.equal(source.getVariable(), (Variable) action)) { return true; } } // Look in effects - for (effect : react.effects ?: emptyList) { - if (effect.variable == (action as Variable)) { + for (VarRef effect : convertToEmptyListIfNull(react.getEffects())) { + if (Objects.equal(effect.getVariable(), (Variable) action)) { return true; } } @@ -270,87 +287,45 @@ def contains(Action action) { return false; } - - /** - * Return true if the specified reactor is not the top-level federated reactor, - * or if it is and the port should be included in the code generated - * for the federate. This means that the port has been used as a trigger, - * a source, or an effect in a top-level reaction that belongs to this federate. - * - * @param port The Port - * @return True if this federate contains the action in the specified reactor - */ - def contains(Port port) { - val reactor = port.eContainer as Reactor - 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 return true. - for (react : reactor.allReactions) { - if (contains(react)) { - // Look in triggers - for (TriggerRef trigger : react.triggers ?: emptyList) { - if (trigger instanceof VarRef) { - if (trigger.variable == (port as Variable)) { - return true; - } - } - } - // Look in sources - for (VarRef source : react.sources ?: emptyList) { - if (source.variable == (port as Variable)) { - return true; - } - } - // Look in effects - for (effect : react.effects ?: emptyList) { - if (effect.variable == (port as Variable)) { - return true; - } - } - } - } - - return false; - } - + /** * Return true if the specified reaction should be included in the code generated for this * federate at the top-level. This means that if the reaction is triggered by or * sends data to a port of a contained reactor, then that reaction * is in the federate. Otherwise, return false. * - * As a convenience measure, also return true if the reaction is not defined in the top-level - * (federated) reactor, or if the top-level reactor is not federated. + * NOTE: This method assumes that it will not be called with reaction arguments + * that are within other federates. It should only be called on reactions that are + * either at the top level or within this federate. For this reason, for any reaction + * not at the top level, it returns true. * * @param reaction The reaction. */ - def contains(Reaction reaction) { - val reactor = reaction.eContainer as Reactor - // Easy case first. - if (!reactor.federated || isSingleton) return true + public boolean contains(Reaction reaction) { + Reactor reactor = (Reactor) reaction.eContainer(); + if (!reactor.isFederated() || this.isSingleton()) return true; - if (!reactor.reactions.contains(reaction)) return false; + if (!reactor.getReactions().contains(reaction)) return false; if (networkReactions.contains(reaction)) { // Reaction is a network reaction that belongs to this federate return true; } - val reactionBankIndex = generator.getReactionBankIndex(reaction) + int reactionBankIndex = generator.getReactionBankIndex(reaction); if (reactionBankIndex >= 0 && this.bankIndex >= 0 && reactionBankIndex != this.bankIndex) { return false; } // If this has been called before, then the result of the // following check is cached. - if (excludeReactions !== null) { - return !excludeReactions.contains(reaction) + if (excludeReactions != null) { + return !excludeReactions.contains(reaction); } indexExcludedTopLevelReactions(reactor); - return !excludeReactions.contains(reaction) + return !excludeReactions.contains(reaction); } /** @@ -367,20 +342,20 @@ def contains(Reaction reaction) { * @param instance The reactor instance. * @return True if this federate contains the reactor instance */ - def contains(ReactorInstance instance) { - if (isSingleton) { - return (instance !== null); + public boolean contains(ReactorInstance instance) { + if (isSingleton()) { + return instance != null; } - if (instance.parent === null) { + if (instance.getParent() == null) { return true; // Top-level reactor } // Start with this instance, then check its parents. - var i = instance; - while (i !== null) { - if (i.definition === this.instantiation) { + ReactorInstance i = instance; + while (i != null) { + if (i.getDefinition() == instantiation) { return true; } - i = i.parent; + i = i.getParent(); } return false; } @@ -394,18 +369,19 @@ def contains(ReactorInstance instance) { * @param action The action * @return True if this federate contains the action in the specified reactor */ - def contains(Timer timer) { - val reactor = timer.eContainer as Reactor - if (!reactor.federated || isSingleton) return true + public boolean contains(Timer timer) { + Reactor reactor = (Reactor) timer.eContainer(); + if (!reactor.isFederated() || this.isSingleton()) return true; // If the action is used as a trigger, a source, or an effect for a top-level reaction // that belongs to this federate, then generate it. - for (r : reactor.allReactions) { + for (Reaction r : ASTUtils.allReactions(reactor)) { if (contains(r)) { // Look in triggers - for (TriggerRef trigger : r.triggers ?: emptyList) { + for (TriggerRef trigger : convertToEmptyListIfNull(r.getTriggers())) { if (trigger instanceof VarRef) { - if (trigger.variable == (timer as Variable)) { + VarRef triggerAsVarRef = (VarRef) trigger; + if (Objects.equal(triggerAsVarRef.getVariable(), (Variable) timer)) { return true; } } @@ -424,9 +400,9 @@ def contains(Timer timer) { * one parent is bank, its width is ignored (only one bank member can be * in any federate). */ - def numRuntimeInstances(ReactorInstance reactor) { + public int numRuntimeInstances(ReactorInstance reactor) { if (!contains(reactor)) return 0; - val depth = isSingleton ? 0 : 1; + int depth = this.isSingleton() ? 0 : 1; return reactor.getTotalWidth(depth); } @@ -438,39 +414,41 @@ def numRuntimeInstances(ReactorInstance reactor) { * * @param federatedReactor The top-level federated reactor */ - private def indexExcludedTopLevelReactions(Reactor federatedReactor) { - var inFederate = false - if (excludeReactions !== null) { - throw new IllegalStateException("The index for excluded reactions at the top level is already built.") + private void indexExcludedTopLevelReactions(Reactor federatedReactor) { + boolean inFederate = false; + if (excludeReactions != null) { + throw new IllegalStateException("The index for excluded reactions at the top level is already built."); } - excludeReactions = new LinkedHashSet + excludeReactions = new LinkedHashSet(); // Construct the set of excluded reactions for this federate. // If a reaction is a network reaction that belongs to this federate, we // don't need to perform this analysis. - for (react : federatedReactor.allReactions.filter[reaction|!networkReactions.contains(reaction)]) { + Iterable reactions = IterableExtensions.filter(ASTUtils.allReactions(federatedReactor), it -> { return !networkReactions.contains(it); }); + for (Reaction react : reactions) { // Create a collection of all the VarRefs (i.e., triggers, sources, and effects) in the react // signature that are ports that reference federates. // We then later check that all these VarRefs reference this federate. If not, we will add this // react to the list of reactions that have to be excluded (note that mixing VarRefs from // different federates is not allowed). - var allVarRefsReferencingFederates = new ArrayList(); + List allVarRefsReferencingFederates = new ArrayList(); // Add all the triggers that are outputs + Stream triggersAsVarRef = react.getTriggers().stream().filter(it -> it instanceof VarRef).map(it -> (VarRef) it); allVarRefsReferencingFederates.addAll( - react.triggers.filter[it instanceof VarRef].map[it as VarRef].filter[it.variable instanceof Output].toList - ) + triggersAsVarRef.filter(it -> it.getVariable() instanceof Output).collect(Collectors.toList()) + ); // Add all the sources that are outputs allVarRefsReferencingFederates.addAll( - react.sources.filter[it.variable instanceof Output].toList - ) + react.getSources().stream().filter(it -> it.getVariable() instanceof Output).collect(Collectors.toList()) + ); // Add all the effects that are inputs allVarRefsReferencingFederates.addAll( - react.effects.filter[it.variable instanceof Input].toList + react.getEffects().stream().filter(it -> it.getVariable() instanceof Input).collect(Collectors.toList()) ); - inFederate = containsAllVarRefs(allVarRefsReferencingFederates) + inFederate = containsAllVarRefs(allVarRefsReferencingFederates); if (!inFederate) { - excludeReactions.add(react) + excludeReactions.add(react); } } } @@ -483,16 +461,16 @@ private def indexExcludedTopLevelReactions(Reactor federatedReactor) { * * @param varRefs A collection of VarRefs */ - private def containsAllVarRefs(Iterable varRefs) { + private boolean containsAllVarRefs(Iterable varRefs) { var referencesFederate = false; var inFederate = true; - for (varRef : varRefs) { - if (varRef.container === this.instantiation) { + for (VarRef varRef : varRefs) { + if (varRef.getContainer() == this.instantiation) { referencesFederate = true; } else { if (referencesFederate) { errorReporter.reportError(varRef, "Mixed triggers and effects from" + - " different federates. This is not permitted") + " different federates. This is not permitted"); } inFederate = false; } @@ -505,8 +483,8 @@ private def containsAllVarRefs(Iterable varRefs) { * has been defined or that there is only one federate. * @return True if no federation has been defined or there is only one federate. */ - def isSingleton() { - return ((instantiation === null) || (generator.federates.size <= 1)) + public boolean isSingleton() { + return ((instantiation == null) || (generator.federates.size() <= 1)); } /** @@ -516,38 +494,49 @@ def isSingleton() { * @param instance The reactor instance containing the output ports * @return A LinkedHashMap */ - def findOutputsConnectedToPhysicalActions(ReactorInstance instance) { - var physicalActionToOutputMinDelay = new LinkedHashMap() + public LinkedHashMap findOutputsConnectedToPhysicalActions(ReactorInstance instance) { + LinkedHashMap physicalActionToOutputMinDelay = new LinkedHashMap<>(); // Find reactions that write to the output port of the reactor - for (output : instance.outputs) { - for (reaction : output.dependsOnReactions) { - var minDelay = findNearestPhysicalActionTrigger(reaction) - if (minDelay != TimeValue.MAX_VALUE) { - physicalActionToOutputMinDelay.put(output.definition as Output, minDelay) + for (PortInstance output : instance.outputs) { + for (ReactionInstance reaction : output.getDependsOnReactions()) { + TimeValue minDelay = findNearestPhysicalActionTrigger(reaction); + if (!Objects.equal(minDelay, TimeValue.MAX_VALUE)) { + physicalActionToOutputMinDelay.put((Output) output.getDefinition(), minDelay); } } } - return physicalActionToOutputMinDelay + return physicalActionToOutputMinDelay; } - override toString() { - "Federate " + this.id + ": " + instantiation.name + @Override + public String toString() { + return "Federate " + id + ": " + instantiation.getName(); } ///////////////////////////////////////////// //// Private Fields - /** Cached result of analysis of which reactions to exclude from main. */ - var excludeReactions = null as Set + /** + * Cached result of analysis of which reactions to exclude from main. + */ + private Set excludeReactions = null; - /** The generator using this. */ - var generator = null as GeneratorBase + /** + * The generator using this. + */ + private GeneratorBase generator = null; - /** Returns the generator that is using this federate instance */ - def getGenerator() { return generator; } + /** + * Returns the generator that is using this federate instance + */ + public GeneratorBase getGenerator() { + return this.generator; + } - /** An error reporter */ - val ErrorReporter errorReporter + /** + * An error reporter + */ + private final ErrorReporter errorReporter; /** * Find the nearest (shortest) path to a physical action trigger from this @@ -557,23 +546,23 @@ override toString() { * @return The minimum delay found to the nearest physical action and * TimeValue.MAX_VALUE otherwise */ - def TimeValue findNearestPhysicalActionTrigger(ReactionInstance reaction) { - var minDelay = TimeValue.MAX_VALUE; - for (trigger : reaction.triggers) { - if (trigger.definition instanceof Action) { - var action = trigger.definition as Action - var actionInstance = trigger as ActionInstance - if (action.origin === ActionOrigin.PHYSICAL) { - if (actionInstance.minDelay.isEarlierThan(minDelay)) { - minDelay = actionInstance.minDelay; + public TimeValue findNearestPhysicalActionTrigger(ReactionInstance reaction) { + TimeValue minDelay = TimeValue.MAX_VALUE; + for (TriggerInstance trigger : reaction.triggers) { + if (trigger.getDefinition() instanceof Action) { + Action action = (Action) trigger.getDefinition(); + ActionInstance actionInstance = (ActionInstance) trigger; + if (action.getOrigin() == ActionOrigin.PHYSICAL) { + if (actionInstance.getMinDelay().isEarlierThan(minDelay)) { + minDelay = actionInstance.getMinDelay(); } - } else if (action.origin === ActionOrigin.LOGICAL) { + } else if (action.getOrigin() == ActionOrigin.LOGICAL) { // Logical action // Follow it upstream inside the reactor - for (uReaction: actionInstance.dependsOnReactions) { + for (ReactionInstance uReaction: actionInstance.getDependsOnReactions()) { // Avoid a loop - if (uReaction != reaction) { - var uMinDelay = actionInstance.minDelay.add(findNearestPhysicalActionTrigger(uReaction)) + if (!Objects.equal(uReaction, reaction)) { + TimeValue uMinDelay = actionInstance.getMinDelay().add(findNearestPhysicalActionTrigger(uReaction)); if (uMinDelay.isEarlierThan(minDelay)) { minDelay = uMinDelay; } @@ -581,27 +570,31 @@ def TimeValue findNearestPhysicalActionTrigger(ReactionInstance reaction) { } } - } else if (trigger.definition instanceof Output) { + } else if (trigger.getDefinition() instanceof Output) { // Outputs of contained reactions - var outputInstance = trigger as PortInstance - for (uReaction: outputInstance.dependsOnReactions) { - var uMinDelay = findNearestPhysicalActionTrigger(uReaction) + PortInstance outputInstance = (PortInstance) trigger; + for (ReactionInstance uReaction: outputInstance.getDependsOnReactions()) { + TimeValue uMinDelay = findNearestPhysicalActionTrigger(uReaction); if (uMinDelay.isEarlierThan(minDelay)) { minDelay = uMinDelay; } } } } - return minDelay + return minDelay; } /** * Remove triggers in this federate's network reactions that are defined in remote federates. */ - def removeRemoteFederateConnectionPorts() { - for (reaction: networkReactions) { - reaction.getTriggers().removeAll(remoteNetworkReactionTriggers) + public void removeRemoteFederateConnectionPorts() { + for (Reaction reaction : this.networkReactions) { + reaction.getTriggers().removeAll(this.remoteNetworkReactionTriggers); } } + // TODO: Put this function into a utils file instead + private List convertToEmptyListIfNull(List list) { + return list == null ? new ArrayList<>() : list; + } } diff --git a/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java b/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java index 2d5b5117ab..32de20d71b 100644 --- a/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java +++ b/org.lflang/src/org/lflang/federated/PythonGeneratorExtension.java @@ -33,7 +33,6 @@ 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; import org.lflang.lf.VarRef; @@ -74,7 +73,7 @@ public static String generateNetworkSenderBody( boolean isPhysical, Delay delay, SupportedSerializers serializer, - PythonGenerator generator + CoordinationType coordinationType ) { String sendRef = CUtil.portRefInReaction(sendingPort, sendingBankIndex, sendingChannelIndex); String receiveRef = JavaAstUtils.generateVarRef(receivingPort); // Used for comments only, so no need for bank/multiport index. @@ -89,15 +88,11 @@ public static String generateNetworkSenderBody( String next_destination_name = "\"federate " + receivingFed.id + "\""; // Get the delay literal - String additionalDelayString = - CGeneratorExtension.getNetworkDelayLiteral( - delay, - generator - ); + String additionalDelayString = CGeneratorExtension.getNetworkDelayLiteral(delay); if (isPhysical) { messageType = "MSG_TYPE_P2P_MESSAGE"; - } else if (generator.getTargetConfig().coordination == CoordinationType.DECENTRALIZED) { + } else if (coordinationType == CoordinationType.DECENTRALIZED) { messageType = "MSG_TYPE_P2P_TAGGED_MESSAGE"; } else { // Logical connection @@ -122,8 +117,8 @@ public static String generateNetworkSenderBody( + next_destination_name + ", message_length"; } - var lengthExpression = ""; - var pointerExpression = ""; + String lengthExpression = ""; + String pointerExpression = ""; switch (serializer) { case NATIVE: { var variableToSerialize = sendRef+"->value"; @@ -176,8 +171,7 @@ public static String generateNetworkReceiverBody( int receivingChannelIndex, InferredType type, boolean isPhysical, - SupportedSerializers serializer, - PythonGenerator generator + SupportedSerializers serializer ) { String receiveRef = CUtil.portRefInReaction(receivingPort, receivingBankIndex, receivingChannelIndex); diff --git a/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java b/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java new file mode 100644 index 0000000000..7136d7987e --- /dev/null +++ b/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java @@ -0,0 +1,460 @@ +/************* + * 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.federated.launcher; + +import com.google.common.base.Objects; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import org.eclipse.xtend2.lib.StringConcatenation; +import org.eclipse.xtext.xbase.lib.Exceptions; +import org.lflang.ErrorReporter; +import org.lflang.FileConfig; +import org.lflang.TargetConfig; +import org.lflang.TargetProperty; +import org.lflang.TargetProperty.ClockSyncMode; +import org.lflang.federated.FedFileConfig; +import org.lflang.federated.FederateInstance; + +/** + * Utility class that can be used to create a launcher for federated LF programs. + * + * @author Edward A. Lee + * @author Soroush Bateni + */ +class FedLauncher { + + protected TargetConfig targetConfig; + protected FileConfig fileConfig; + protected ErrorReporter errorReporter; + + /** + * @param targetConfig The current target configuration. + * @param fileConfig The current file configuration. + * @param errorReporter A error reporter for reporting any errors or warnings during the code generation + */ + public FedLauncher(TargetConfig targetConfig, FileConfig fileConfig, ErrorReporter errorReporter) { + this.targetConfig = targetConfig; + this.fileConfig = fileConfig; + this.errorReporter = errorReporter; + } + + /** + * Return the compile command for a federate. + * + * @param federate The federate to compile. + */ + protected String compileCommandForFederate(org.lflang.federated.FederateInstance federate) { + throw new UnsupportedOperationException("Don't know how to compile the federates."); + } + + /** + * Return the command that will execute a remote federate, assuming that the current + * directory is the top-level project folder. This is used to create a launcher script + * for federates. + * + * @param federate The federate to execute. + */ + protected String executeCommandForRemoteFederate(org.lflang.federated.FederateInstance federate) { + throw new UnsupportedOperationException("Don't know how to execute the federates."); + } + + /** + * Return the command that will execute a local federate, assuming that the current + * directory is the top-level project folder. This is used to create a launcher script + * for federates. + * + * @param federate The federate to execute. + */ + protected String executeCommandForLocalFederate(FileConfig fileConfig, + org.lflang.federated.FederateInstance federate) { + throw new UnsupportedOperationException("Don't know how to execute the federates."); + } + + /** + * Create the launcher shell scripts. This will create one or two files + * in the output path (bin directory). The first has name equal to + * the filename of the source file without the ".lf" extension. + * This will be a shell script that launches the + * RTI and the federates. If, in addition, either the RTI or any + * federate is mapped to a particular machine (anything other than + * the default "localhost" or "0.0.0.0"), then this will generate + * a shell script in the bin directory with name filename_distribute.sh + * that copies the relevant source files to the remote host and compiles + * them so that they are ready to execute using the launcher. + * + * A precondition for this to work is that the user invoking this + * code generator can log into the remote host without supplying + * a password. Specifically, you have to have installed your + * public key (typically found in ~/.ssh/id_rsa.pub) in + * ~/.ssh/authorized_keys on the remote host. In addition, the + * remote host must be running an ssh service. + * On an Arch Linux system using systemd, for example, this means + * running: + * + * sudo systemctl ssh.service + * + * Enable means to always start the service at startup, whereas + * start means to just start it this once. + * + * On MacOS, open System Preferences from the Apple menu and + * click on the "Sharing" preference panel. Select the checkbox + * next to "Remote Login" to enable it. + * + * In addition, every host must have OpenSSL installed, with at least + * version 1.1.1a. You can check the version with + * + * openssl version + * + * @param coreFiles The files from the core directory that must be + * copied to the remote machines. + * @param federates A list of federate instances in the federation + * @param federationRTIProperties Contains relevant properties of the RTI. + * Can have values for 'host', 'dir', and 'user' + * + */ + public void createLauncher( + ArrayList coreFiles, + List federates, + LinkedHashMap federationRTIProperties + ) throws IOException { + // NOTE: It might be good to use screen when invoking the RTI + // or federates remotely so you can detach and the process keeps running. + // However, I was unable to get it working properly. + // What this means is that the shell that invokes the launcher + // needs to remain live for the duration of the federation. + // If that shell is killed, the federation will die. + // Hence, it is reasonable to launch the federation on a + // machine that participates in the federation, for example, + // on the machine that runs the RTI. The command I tried + // to get screen to work looks like this: + // ssh -t «target» cd «path»; screen -S «filename»_«federate.name» -L bin/«filename»_«federate.name» 2>&1 + // var outPath = binGenPath + StringBuilder shCode = new StringBuilder(); + StringBuilder distCode = new StringBuilder(); + shCode.append(getSetupCode() + "\n"); + String distHeader = getDistHeader(); + Object host = federationRTIProperties.get("host"); + Object target = host; + + Object path = federationRTIProperties.get("dir"); + if (path == null) path = "LinguaFrancaRemote"; + + Object user = federationRTIProperties.get("user"); + if (user != null) { + target = user + "@" + host; + } + + String RTILaunchString = getRtiCommand(federates); + + // Launch the RTI in the foreground. + if (host.equals("localhost") || host.equals("0.0.0.0")) { + // FIXME: the paths below will not work on Windows + shCode.append(getLaunchCode(RTILaunchString) + "\n"); + } else { + // Start the RTI on the remote machine. + // FIXME: Should $FEDERATION_ID be used to ensure unique directories, executables, on the remote host? + // Copy the source code onto the remote machine and compile it there. + if(distCode.length() == 0) distCode.append(distHeader + "\n"); + + String logFileName = String.format("log/%s_RTI.log", fileConfig.name); + + // Launch the RTI on the remote machine using ssh and screen. + // The -t argument to ssh creates a virtual terminal, which is needed by screen. + // The -S gives the session a name. + // The -L option turns on logging. Unfortunately, the -Logfile giving the log file name + // is not standardized in screen. Logs go to screenlog.0 (or screenlog.n). + // FIXME: Remote errors are not reported back via ssh from screen. + // How to get them back to the local machine? + // Perhaps use -c and generate a screen command file to control the logfile name, + // but screen apparently doesn't write anything to the log file! + // + // The cryptic 2>&1 reroutes stderr to stdout so that both are returned. + // The sleep at the end prevents screen from exiting before outgoing messages from + // the federate have had time to go out to the RTI through the socket. + RTILaunchString = getRtiCommand(federates); + + shCode.append(getRemoteLaunchCode(host, target, logFileName, RTILaunchString) + "\n"); + } + + // Index used for storing pids of federates + int federateIndex = 0; + for (FederateInstance federate : federates) { + if (federate.isRemote) { + FedFileConfig fedFileConfig = new FedFileConfig(fileConfig, federate.name); + Path fedRelSrcGenPath = fedFileConfig.getSrcGenBasePath().relativize(fedFileConfig.getSrcGenPath()); + if(distCode.length() == 0) distCode.append(distHeader + "\n"); + String logFileName = String.format("log/%s_%s.log", fedFileConfig.name, federate.name); + String compileCommand = compileCommandForFederate(federate); + // '''«targetConfig.compiler» src-gen/«topLevelName»_«federate.name».c -o bin/«topLevelName»_«federate.name» -pthread «targetConfig.compilerFlags.join(" ")»''' + // FIXME: Should $FEDERATION_ID be used to ensure unique directories, executables, on the remote host? + distCode.append(getDistCode(path, federate, fedRelSrcGenPath, logFileName, fedFileConfig, compileCommand) + "\n"); + String executeCommand = executeCommandForRemoteFederate(federate); + shCode.append(getFedRemoteLaunchCode(federate, path, logFileName, executeCommand, federateIndex++) + "\n"); + } else { + String executeCommand = executeCommandForLocalFederate(fileConfig, federate); + shCode.append(getFedLocalLaunchCode(federate, executeCommand, federateIndex++) + "\n"); + } + } + if (host.equals("localhost") || host.equals("0.0.0.0")) { + // Local PID managements + shCode.append("echo \"#### Bringing the RTI back to foreground so it can receive Control-C.\"" + "\n"); + shCode.append("fg %1" + "\n"); + } + // Wait for launched processes to finish + shCode.append(String.join("\n", + "echo \"RTI has exited. Wait for federates to exit.\"", + "# Wait for launched processes to finish.", + "# The errors are handled separately via trap.", + "for pid in \"${pids[@]}\"", + "do", + " wait $pid", + "done", + "echo \"All done.\"" + ) + "\n"); + + // Write the launcher file. + // Delete file previously produced, if any. + File file = fileConfig.binPath.resolve(fileConfig.name).toFile(); + if (file.exists()) { + file.delete(); + } + + FileOutputStream fOut = new FileOutputStream(file); + fOut.write(shCode.toString().getBytes()); + fOut.close(); + if (!file.setExecutable(true, false)) { + errorReporter.reportWarning("Unable to make launcher script executable."); + } + + // Write the distributor file. + // Delete the file even if it does not get generated. + file = fileConfig.binPath.resolve(fileConfig.name + "_distribute.sh").toFile(); + if (file.exists()) { + file.delete(); + } + if (distCode.length() > 0) { + fOut = new FileOutputStream(file); + fOut.write(distCode.toString().getBytes()); + fOut.close(); + if (!file.setExecutable(true, false)) { + errorReporter.reportWarning("Unable to make distributor script executable."); + } + } + } + + private String getSetupCode() { + return String.join("\n", + "#!/bin/bash", + "# Launcher for federated " + fileConfig.name + ".lf Lingua Franca program.", + "# Uncomment to specify to behave as close as possible to the POSIX standard.", + "# set -o posix", + "", + "# Enable job control", + "set -m", + "shopt -s huponexit", + "", + "# Set a trap to kill all background jobs on error or control-C", + "# Use two distinct traps so we can see which signal causes this.", + "cleanup() {", + " printf \"Killing federate %s.\\n\" ${pids[*]}", + " # The || true clause means this is not an error if kill fails.", + " kill ${pids[@]} || true", + " printf \"#### Killing RTI %s.\\n\" ${RTI}", + " kill ${RTI} || true", + " exit 1", + "}", + "cleanup_err() {", + " echo \"#### Received ERR signal on line $1. Invoking cleanup().\"", + " cleanup", + "}", + "cleanup_sigint() {", + " echo \"#### Received SIGINT signal on line $1. Invoking cleanup().\"", + " cleanup", + "}", + "", + "trap 'cleanup_err $LINENO' ERR", + "trap 'cleanup_sigint $LINENO' SIGINT", + "", + "# Create a random 48-byte text ID for this federation.", + "# The likelihood of two federations having the same ID is 1/16,777,216 (1/2^24).", + "FEDERATION_ID=`openssl rand -hex 24`", + "echo \"Federate " + fileConfig.name + " in Federation ID '$FEDERATION_ID'\"", + "# Launch the federates:" + ); + } + + private String getDistHeader() { + return String.join("\n", + "#!/bin/bash", + "# Distributor for federated "+ fileConfig.name + ".lf Lingua Franca program.", + "# Uncomment to specify to behave as close as possible to the POSIX standard.", + "# set -o posix" + ); + } + + private String getRtiCommand(List federates) { + List commands = new ArrayList<>(); + commands.addAll(List.of( + "RTI -i ${FEDERATION_ID} \\", + " -n "+federates.size()+" \\", + " -c "+targetConfig.clockSync.toString()+" \\" + )); + if (targetConfig.clockSync.equals(ClockSyncMode.ON)) { + commands.add("period " + targetConfig.clockSyncOptions.period.toNanoSeconds() + " \\"); + } + commands.addAll(List.of( + " exchanges-per-interval "+targetConfig.clockSyncOptions.trials+" \\", + " &" + )); + return String.join("\n", commands); + } + + private String getLaunchCode(String rtiLaunchCode) { + return String.join("\n", + "echo \"#### Launching the runtime infrastructure (RTI).\"", + "# First, check if the RTI is on the PATH", + "if ! command -v RTI &> /dev/null", + "then", + " echo \"RTI could not be found.\"", + " echo \"The source code can be obtained from https://github.com/lf-lang/reactor-c/tree/main/core/federated/RTI\"", + " exit", + "fi ", + "# The RTI is started first to allow proper boot-up", + "# before federates will try to connect.", + "# The RTI will be brought back to foreground", + "# to be responsive to user inputs after all federates", + "# are launched.", + rtiLaunchCode, + "# Store the PID of the RTI", + "RTI=$!", + "# Wait for the RTI to boot up before", + "# starting federates (this could be done by waiting for a specific output", + "# from the RTI, but here we use sleep)", + "sleep 1" + ); + } + + private String getRemoteLaunchCode(Object host, Object target, String logFileName, String rtiLaunchString) { + return String.join("\n", + "echo \"#### Launching the runtime infrastructure (RTI) on remote host " + host + ".\"", + "# FIXME: Killing this ssh does not kill the remote process.", + "# A double -t -t option to ssh forces creation of a virtual terminal, which", + "# fixes the problem, but then the ssh command does not execute. The remote", + "# federate does not start!", + "ssh " + target + " 'mkdir -p log; \\", + " echo \"-------------- Federation ID: \"'$FEDERATION_ID' >> " + logFileName + "; \\", + " date >> " + logFileName + "; \\", + " echo \"Executing RTI: " + rtiLaunchString + "\" 2>&1 | tee -a " + logFileName + "; \\", + " # First, check if the RTI is on the PATH", + " if ! command -v RTI &> /dev/null", + " then", + " echo \"RTI could not be found.\"", + " echo \"The source code can be found in org.lflang/src/lib/core/federated/RTI\"", + " exit", + " fi", + " " + rtiLaunchString + " 2>&1 | tee -a " + logFileName + "' &", + "# Store the PID of the channel to RTI", + "RTI=$!", + "# Wait for the RTI to boot up before", + "# starting federates (this could be done by waiting for a specific output", + "# from the RTI, but here we use sleep)", + "sleep 1" + ); + } + + private String getDistCode( + Object path, + FederateInstance federate, + Path fedRelSrcGenPath, + String logFileName, + FedFileConfig fedFileConfig, + String compileCommand) { + return String.join("\n", + "echo \"Making directory "+path+" and subdirectories src-gen, bin, and log on host "+getUserHost(federate.user, federate.host)+"\"", + "# The >> syntax appends stdout to a file. The 2>&1 appends stderr to the same file.", + "ssh "+getUserHost(federate.user, federate.host)+" '\\", + " mkdir -p "+path+"/src-gen/"+fedRelSrcGenPath+"/core "+path+"/bin "+path+"/log; \\", + " echo \"--------------\" >> "+path+"/"+logFileName+"; \\", + " date >> "+path+"/"+logFileName+";", + "'", + "pushd "+fedFileConfig.getSrcGenPath()+" > /dev/null", + "echo \"Copying source files to host "+getUserHost(federate.user, federate.host)+"\"", + "scp -r * "+getUserHost(federate.user, federate.host)+":"+path+"/src-gen/"+fedRelSrcGenPath, + "popd > /dev/null", + "echo \"Compiling on host "+getUserHost(federate.user, federate.host)+" using: "+compileCommand+"\"", + "ssh "+getUserHost(federate.user, federate.host)+" 'cd "+path+"; \\", + " echo \"In "+path+" compiling with: "+compileCommand+"\" >> "+logFileName+" 2>&1; \\", + " # Capture the output in the log file and stdout. \\", + " "+compileCommand+" 2>&1 | tee -a "+logFileName+";' " + ); + } + + private String getUserHost(Object user, Object host) { + if (user == null) { + return host.toString(); + } + return user + "@" + host; + } + + private String getFedRemoteLaunchCode( + FederateInstance federate, + Object path, + String logFileName, + String executeCommand, + int federateIndex + ) { + return String.join("\n", + "echo \"#### Launching the federate "+federate.name+" on host "+getUserHost(federate.user, federate.host)+"\"", + "# FIXME: Killing this ssh does not kill the remote process.", + "# A double -t -t option to ssh forces creation of a virtual terminal, which", + "# fixes the problem, but then the ssh command does not execute. The remote", + "# federate does not start!", + "ssh "+getUserHost(federate.user, federate.host)+" '\\", + " cd "+path+"; \\", + " echo \"-------------- Federation ID: \"'$FEDERATION_ID' >> "+logFileName+"; \\", + " date >> "+logFileName+"; \\", + " echo \"In "+path+", executing: "+executeCommand+"\" 2>&1 | tee -a "+logFileName+"; \\", + " "+executeCommand+" 2>&1 | tee -a "+logFileName+"' &", + "pids["+federateIndex+"]=$!" + ); + } + + private String getFedLocalLaunchCode(FederateInstance federate, String executeCommand, int federateIndex) { + return String.format(String.join("\n", + "echo \"#### Launching the federate %s.\"", + "%s &", + "pids[%s]=$!" + ), + federate.name, + executeCommand, + federateIndex); + } +} diff --git a/org.lflang/src/org/lflang/federated/launcher/FedLauncher.xtend b/org.lflang/src/org/lflang/federated/launcher/FedLauncher.xtend deleted file mode 100644 index 77d451525f..0000000000 --- a/org.lflang/src/org/lflang/federated/launcher/FedLauncher.xtend +++ /dev/null @@ -1,404 +0,0 @@ -/************* - * 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.federated.launcher - -import java.io.FileOutputStream -import java.util.ArrayList -import java.util.LinkedHashMap -import java.util.List -import org.lflang.ErrorReporter -import org.lflang.FileConfig -import org.lflang.TargetConfig -import org.lflang.TargetProperty.ClockSyncMode - -/** - * Utility class that can be used to create a launcher for federated LF programs. - * - * @author Edward A. Lee - * @author Soroush Bateni - */ -package class FedLauncher { - - protected var TargetConfig targetConfig; - protected var FileConfig fileConfig; - protected var ErrorReporter errorReporter; - - /** - * @param targetConfig The current target configuration. - * @param fileConfig The current file configuration. - * @param errorReporter A error reporter for reporting any errors or warnings during the code generation - */ - new( - TargetConfig targetConfig, - FileConfig fileConfig, - ErrorReporter errorReporter - ) { - this.targetConfig = targetConfig; - this.fileConfig = fileConfig; - this.errorReporter = errorReporter; - } - - /** - * Return the compile command for a federate. - * - * @param federate The federate to compile. - */ - protected def String compileCommandForFederate(org.lflang.federated.FederateInstance federate) { - throw new UnsupportedOperationException("Don't know how to compile the federates."); - } - - /** - * Return the command that will execute a remote federate, assuming that the current - * directory is the top-level project folder. This is used to create a launcher script - * for federates. - * - * @param federate The federate to execute. - */ - protected def String executeCommandForRemoteFederate(org.lflang.federated.FederateInstance federate) { - throw new UnsupportedOperationException("Don't know how to execute the federates."); - } - - /** - * Return the command that will execute a local federate, assuming that the current - * directory is the top-level project folder. This is used to create a launcher script - * for federates. - * - * @param federate The federate to execute. - */ - protected def String executeCommandForLocalFederate(FileConfig fileConfig, - org.lflang.federated.FederateInstance federate) { - throw new UnsupportedOperationException("Don't know how to execute the federates."); - } - - /** - * Create the launcher shell scripts. This will create one or two files - * in the output path (bin directory). The first has name equal to - * the filename of the source file without the ".lf" extension. - * This will be a shell script that launches the - * RTI and the federates. If, in addition, either the RTI or any - * federate is mapped to a particular machine (anything other than - * the default "localhost" or "0.0.0.0"), then this will generate - * a shell script in the bin directory with name filename_distribute.sh - * that copies the relevant source files to the remote host and compiles - * them so that they are ready to execute using the launcher. - * - * A precondition for this to work is that the user invoking this - * code generator can log into the remote host without supplying - * a password. Specifically, you have to have installed your - * public key (typically found in ~/.ssh/id_rsa.pub) in - * ~/.ssh/authorized_keys on the remote host. In addition, the - * remote host must be running an ssh service. - * On an Arch Linux system using systemd, for example, this means - * running: - * - * sudo systemctl ssh.service - * - * Enable means to always start the service at startup, whereas - * start means to just start it this once. - * - * On MacOS, open System Preferences from the Apple menu and - * click on the "Sharing" preference panel. Select the checkbox - * next to "Remote Login" to enable it. - * - * In addition, every host must have OpenSSL installed, with at least - * version 1.1.1a. You can check the version with - * - * openssl version - * - * @param coreFiles The files from the core directory that must be - * copied to the remote machines. - * @param federates A list of federate instances in the federation - * @param federationRTIProperties Contains relevant properties of the RTI. - * Can have values for 'host', 'dir', and 'user' - * - */ - def createLauncher( - ArrayList coreFiles, - List federates, - LinkedHashMap federationRTIProperties - ) { - // NOTE: It might be good to use screen when invoking the RTI - // or federates remotely so you can detach and the process keeps running. - // However, I was unable to get it working properly. - // What this means is that the shell that invokes the launcher - // needs to remain live for the duration of the federation. - // If that shell is killed, the federation will die. - // Hence, it is reasonable to launch the federation on a - // machine that participates in the federation, for example, - // on the machine that runs the RTI. The command I tried - // to get screen to work looks like this: - // ssh -t «target» cd «path»; screen -S «filename»_«federate.name» -L bin/«filename»_«federate.name» 2>&1 - // var outPath = binGenPath - val shCode = new StringBuilder() - val distCode = new StringBuilder() - shCode.append(''' - #!/bin/bash - # Launcher for federated «fileConfig.name».lf Lingua Franca program. - # Uncomment to specify to behave as close as possible to the POSIX standard. - # set -o posix - - # Enable job control - set -m - shopt -s huponexit - - # Set a trap to kill all background jobs on error or control-C - # Use two distinct traps so we can see which signal causes this. - cleanup() { - printf "Killing federate %s.\n" ${pids[*]} - # The || true clause means this is not an error if kill fails. - kill ${pids[@]} || true - printf "#### Killing RTI %s.\n" ${RTI} - kill ${RTI} || true - exit 1 - } - cleanup_err() { - echo "#### Received ERR signal on line $1. Invoking cleanup()." - cleanup - } - cleanup_sigint() { - echo "#### Received SIGINT signal on line $1. Invoking cleanup()." - cleanup - } - - trap 'cleanup_err $LINENO' ERR - trap 'cleanup_sigint $LINENO' SIGINT - - # Create a random 48-byte text ID for this federation. - # The likelihood of two federations having the same ID is 1/16,777,216 (1/2^24). - FEDERATION_ID=`openssl rand -hex 24` - echo "Federate «fileConfig.name» in Federation ID '$FEDERATION_ID'" - # Launch the federates: - ''') - val distHeader = ''' - #!/bin/bash - # Distributor for federated «fileConfig.name».lf Lingua Franca program. - # Uncomment to specify to behave as close as possible to the POSIX standard. - # set -o posix - ''' - val host = federationRTIProperties.get('host') - var target = host - - var path = federationRTIProperties.get('dir') - if(path === null) path = '''LinguaFrancaRemote''' - - var user = federationRTIProperties.get('user') - if (user !== null) { - target = user + '@' + host - } - - var RTILaunchString = ''' - RTI -i ${FEDERATION_ID} \ - -n «federates.size» \ - -c «targetConfig.clockSync.toString()» «IF targetConfig.clockSync == ClockSyncMode.ON» \ - period «targetConfig.clockSyncOptions.period.toNanoSeconds» «ENDIF» \ - exchanges-per-interval «targetConfig.clockSyncOptions.trials» \ - & - ''' - - // Launch the RTI in the foreground. - if (host == 'localhost' || host == '0.0.0.0') { - // FIXME: the paths below will not work on Windows - shCode.append( ''' - echo "#### Launching the runtime infrastructure (RTI)." - # First, check if the RTI is on the PATH - if ! command -v RTI &> /dev/null - then - echo "RTI could not be found." - echo "The source code can be obtained from https://github.com/lf-lang/reactor-c/tree/main/core/federated/RTI" - exit - fi - # The RTI is started first to allow proper boot-up - # before federates will try to connect. - # The RTI will be brought back to foreground - # to be responsive to user inputs after all federates - # are launched. - «RTILaunchString» - # Store the PID of the RTI - RTI=$! - # Wait for the RTI to boot up before - # starting federates (this could be done by waiting for a specific output - # from the RTI, but here we use sleep) - sleep 1 - ''') - } else { - // Start the RTI on the remote machine. - // FIXME: Should $FEDERATION_ID be used to ensure unique directories, executables, on the remote host? - // Copy the source code onto the remote machine and compile it there. - if(distCode.length === 0) distCode.append(distHeader + "\n"); - - val logFileName = '''log/«fileConfig.name»_RTI.log''' - - // Launch the RTI on the remote machine using ssh and screen. - // The -t argument to ssh creates a virtual terminal, which is needed by screen. - // The -S gives the session a name. - // The -L option turns on logging. Unfortunately, the -Logfile giving the log file name - // is not standardized in screen. Logs go to screenlog.0 (or screenlog.n). - // FIXME: Remote errors are not reported back via ssh from screen. - // How to get them back to the local machine? - // Perhaps use -c and generate a screen command file to control the logfile name, - // but screen apparently doesn't write anything to the log file! - // - // The cryptic 2>&1 reroutes stderr to stdout so that both are returned. - // The sleep at the end prevents screen from exiting before outgoing messages from - // the federate have had time to go out to the RTI through the socket. - RTILaunchString = ''' - RTI -i '${FEDERATION_ID}' \ - -n «federates.size» \ - -c «targetConfig.clockSync.toString()» «IF targetConfig.clockSync == ClockSyncMode.ON» \ - period «targetConfig.clockSyncOptions.period.toNanoSeconds» «ENDIF» \ - exchanges-per-interval «targetConfig.clockSyncOptions.trials» \ - & - ''' - - shCode.append( ''' - echo "#### Launching the runtime infrastructure (RTI) on remote host «host»." - # FIXME: Killing this ssh does not kill the remote process. - # A double -t -t option to ssh forces creation of a virtual terminal, which - # fixes the problem, but then the ssh command does not execute. The remote - # federate does not start! - ssh «target» 'mkdir -p log; \ - echo "-------------- Federation ID: "'$FEDERATION_ID' >> «logFileName»; \ - date >> «logFileName»; \ - echo "Executing RTI: «RTILaunchString»" 2>&1 | tee -a «logFileName»; \ - # First, check if the RTI is on the PATH - if ! command -v RTI &> /dev/null - then - echo "RTI could not be found." - echo "The source code can be found in org.lflang/src/lib/core/federated/RTI" - exit - fi - «RTILaunchString» 2>&1 | tee -a «logFileName»' & - # Store the PID of the channel to RTI - RTI=$! - # Wait for the RTI to boot up before - # starting federates (this could be done by waiting for a specific output - # from the RTI, but here we use sleep) - sleep 1 - ''') - } - - // Index used for storing pids of federates - var federateIndex = 0 - for (federate : federates) { - if (federate.isRemote) { - val fedFileConfig = new org.lflang.federated.FedFileConfig(fileConfig, federate.name); - val fedRelSrcGenPath = fedFileConfig.srcGenBasePath.relativize(fedFileConfig.srcGenPath); - if(distCode.length === 0) distCode.append(distHeader + "\n"); - val logFileName = '''log/«fedFileConfig.name»_«federate.name».log''' - val compileCommand = compileCommandForFederate(federate); - // '''«targetConfig.compiler» src-gen/«topLevelName»_«federate.name».c -o bin/«topLevelName»_«federate.name» -pthread «targetConfig.compilerFlags.join(" ")»''' - // FIXME: Should $FEDERATION_ID be used to ensure unique directories, executables, on the remote host? - distCode.append( ''' - echo "Making directory «path» and subdirectories src-gen, bin, and log on host «federate.user»@«federate.host»" - # The >> syntax appends stdout to a file. The 2>&1 appends stderr to the same file. - ssh «federate.user»@«federate.host» '\ - mkdir -p «path»/src-gen/«fedRelSrcGenPath»/core «path»/bin «path»/log; \ - echo "--------------" >> «path»/«logFileName»; \ - date >> «path»/«logFileName»; - ' - pushd «fedFileConfig.srcGenPath» > /dev/null - echo "Copying source files to host «federate.user»@«federate.host»" - scp -r * «federate.user»@«federate.host»:«path»/src-gen/«fedRelSrcGenPath» - popd > /dev/null - echo "Compiling on host «federate.user»@«federate.host» using: «compileCommand»" - ssh «federate.user»@«federate.host» 'cd «path»; \ - echo "In «path» compiling with: «compileCommand»" >> «logFileName» 2>&1; \ - # Capture the output in the log file and stdout. \ - «compileCommand» 2>&1 | tee -a «logFileName»;' - ''') - val executeCommand = executeCommandForRemoteFederate(federate); - shCode.append( ''' - echo "#### Launching the federate «federate.name» on host «federate.user»@«federate.host»" - # FIXME: Killing this ssh does not kill the remote process. - # A double -t -t option to ssh forces creation of a virtual terminal, which - # fixes the problem, but then the ssh command does not execute. The remote - # federate does not start! - ssh «federate.user»@«federate.host» '\ - cd «path»; \ - echo "-------------- Federation ID: "'$FEDERATION_ID' >> «logFileName»; \ - date >> «logFileName»; \ - echo "In «path», executing: «executeCommand»" 2>&1 | tee -a «logFileName»; \ - «executeCommand» 2>&1 | tee -a «logFileName»' & - pids[«federateIndex++»]=$! - ''') - } else { - val executeCommand = executeCommandForLocalFederate(fileConfig, federate); - shCode.append( ''' - echo "#### Launching the federate «federate.name»." - «executeCommand» & - pids[«federateIndex++»]=$! - ''') - } - } - if (host == 'localhost' || host == '0.0.0.0') { - // Local PID managements - shCode.append( ''' - echo "#### Bringing the RTI back to foreground so it can receive Control-C." - fg %1 - ''') - } - // Wait for launched processes to finish - shCode.append( ''' - echo "RTI has exited. Wait for federates to exit." - # Wait for launched processes to finish. - # The errors are handled separately via trap. - for pid in "${pids[@]}" - do - wait $pid - done - echo "All done." - ''') - - // Write the launcher file. - // Delete file previously produced, if any. - var file = fileConfig.binPath.resolve(fileConfig.name).toFile - if (file.exists) { - file.delete - } - - var fOut = new FileOutputStream(file) - fOut.write(shCode.toString().getBytes()) - fOut.close() - if (!file.setExecutable(true, false)) { - errorReporter.reportWarning("Unable to make launcher script executable.") - } - - // Write the distributor file. - // Delete the file even if it does not get generated. - file = fileConfig.binPath.resolve(fileConfig.name + '_distribute.sh').toFile - if (file.exists) { - file.delete - } - if (distCode.length > 0) { - fOut = new FileOutputStream(file) - fOut.write(distCode.toString().getBytes()) - fOut.close() - if (!file.setExecutable(true, false)) { - errorReporter.reportWarning("Unable to make distributor script executable.") - } - } - } -} diff --git a/org.lflang/src/org/lflang/generator/CodeBuilder.java b/org.lflang/src/org/lflang/generator/CodeBuilder.java index f0116b14d5..cbfd710a0e 100644 --- a/org.lflang/src/org/lflang/generator/CodeBuilder.java +++ b/org.lflang/src/org/lflang/generator/CodeBuilder.java @@ -69,6 +69,13 @@ public void insert(int position, String text) { public int length() { return code.length(); } + + /** + * Add a new line. + */ + public void newLine() { + this.pr(""); + } /** * Append the specified text plus a final newline. diff --git a/org.lflang/src/org/lflang/generator/GeneratorBase.xtend b/org.lflang/src/org/lflang/generator/GeneratorBase.xtend index 849e11169b..fb2de5efee 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorBase.xtend +++ b/org.lflang/src/org/lflang/generator/GeneratorBase.xtend @@ -535,7 +535,7 @@ abstract class GeneratorBase extends AbstractLFValidator { * @param time A TimeValue that represents a time. * @return A string, such as "MSEC(100)" for 100 milliseconds. */ - def String timeInTargetLanguage(TimeValue time) { + static def String timeInTargetLanguage(TimeValue time) { if (time !== null) { if (time.unit !== null) { return time.unit.cMacroName + '(' + time.magnitude + ')' @@ -547,7 +547,7 @@ abstract class GeneratorBase extends AbstractLFValidator { } // note that this is moved out by #544 - final def String cMacroName(TimeUnit unit) { + static def String cMacroName(TimeUnit unit) { return unit.canonicalName.toUpperCase } @@ -741,7 +741,35 @@ abstract class GeneratorBase extends AbstractLFValidator { def writeDockerFile(File dockerComposeDir, String dockerFileName, String federateName) { throw new UnsupportedOperationException("This target does not support docker file generation.") } - + + /** + * Write a Dockerfile for the current federate as given by filename. + * @param The directory where the docker compose file is generated. + * @param The name of the docker file. + * @param The name of the federate. + */ + def getDockerComposeCommand() { + val OS = System.getProperty("os.name").toLowerCase(); + return (OS.indexOf("nux") >= 0) ? "docker-compose" : "docker compose" + } + + /** + * Write a Dockerfile for the current federate as given by filename. + * @param The directory where the docker compose file is generated. + * @param The name of the docker file. + * @param The name of the federate. + */ + def getDockerBuildCommand(String dockerFile, File dockerComposeDir, String federateName) { + return String.join("\n", + '''Dockerfile for «topLevelName» written to «dockerFile»''', + '''#####################################''', + '''To build the docker image, go to «dockerComposeDir» and run:''', + "", + ''' «getDockerComposeCommand()» build «federateName»''', + "", + '''#####################################''' + ); + } /** * Parsed error message from a compiler is returned here. @@ -887,11 +915,13 @@ abstract class GeneratorBase extends AbstractLFValidator { // // Private functions /** - * Remove triggers in each federates' network reactions that are defined in remote federates. + * Remove triggers in each federates' network reactions that are defined + * in remote federates. * * This must be done in code generators after the dependency graphs * are built and levels are assigned. Otherwise, these disconnected ports - * might reference data structures in remote federates and cause compile errors. + * might reference data structures in remote federates and cause + * compile/runtime errors. * * @param instance The reactor instance to remove these ports from if any. * Can be null. @@ -1228,7 +1258,7 @@ abstract class GeneratorBase extends AbstractLFValidator { * @param t A time AST node * @return A time string in the target language */ - protected def getTargetTime(Time t) { + static def getTargetTime(Time t) { val value = new TimeValue(t.interval, TimeUnit.fromName(t.unit)) return value.timeInTargetLanguage } @@ -1241,7 +1271,7 @@ abstract class GeneratorBase extends AbstractLFValidator { * @param v A time AST node * @return A time string in the target language */ - protected def getTargetValue(Value v) { + static def getTargetValue(Value v) { if (v.time !== null) { return v.time.targetTime } @@ -1256,7 +1286,7 @@ abstract class GeneratorBase extends AbstractLFValidator { * @param v A time AST node * @return A time string in the target language */ - protected def getTargetTime(Value v) { + static def getTargetTime(Value v) { if (v.time !== null) { return v.time.targetTime } else if (v.isZero) { @@ -1266,7 +1296,7 @@ abstract class GeneratorBase extends AbstractLFValidator { return v.toText } - protected def getTargetTime(Delay d) { + static def getTargetTime(Delay d) { if (d.parameter !== null) { return d.toText } else { diff --git a/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java b/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java index 5eb36ba0da..87bb2393eb 100644 --- a/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java +++ b/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java @@ -1,12 +1,11 @@ package org.lflang.generator; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -17,7 +16,6 @@ import org.eclipse.emf.common.util.URI; 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; @@ -29,6 +27,7 @@ import org.lflang.TargetConfig; import org.lflang.TargetConfig.Mode; import org.lflang.TargetProperty; +import org.lflang.TargetProperty.SchedulerOption; import org.lflang.graph.InstantiationGraph; import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; @@ -97,6 +96,12 @@ public static void setTargetConfig( if (context.getArgs().containsKey("target-compiler")) { targetConfig.compiler = context.getArgs().getProperty("target-compiler"); } + if (context.getArgs().containsKey("scheduler")) { + targetConfig.schedulerType = SchedulerOption.valueOf( + context.getArgs().getProperty("scheduler") + ); + targetConfig.setByUser.add(TargetProperty.SCHEDULER); + } if (context.getArgs().containsKey("target-flags")) { targetConfig.compilerFlags.clear(); if (!context.getArgs().getProperty("target-flags").isEmpty()) { @@ -229,7 +234,12 @@ public static void validate( bad.contains(resource) || issues.size() > 0 ) { // Report the error on this resource. - Path path = fileConfig.srcPath; + Path path = null; + try { + path = FileConfig.toPath(resource); + } catch (IOException e) { + path = Paths.get("Unknown file"); // Not sure if this is what we want. + } for (Issue issue : issues) { errorReporter.reportError(path, issue.getLineNumber(), issue.getMessage()); } @@ -311,14 +321,35 @@ public static LFResource getLFResource( * Write text to a file. * @param text The text to be written. * @param path The file to write the code to. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed */ - public static void writeToFile(CharSequence text, String path) throws IOException { - new File(path).getParentFile().mkdirs(); - try (BufferedWriter writer = new BufferedWriter(new FileWriter(path))) { - for (int i = 0; i < text.length(); i++) { - writer.write(text.charAt(i)); + public static void writeToFile(String text, Path path, boolean skipIfUnchanged) throws IOException { + Files.createDirectories(path.getParent()); + final byte[] bytes = text.getBytes(); + if (skipIfUnchanged && Files.isRegularFile(path)) { + if (Arrays.equals(bytes, Files.readAllBytes(path))) { + return; } } + Files.write(path, text.getBytes()); + } + + /** + * Write text to a file. + * @param text The text to be written. + * @param path The file to write the code to. + */ + public static void writeToFile(String text, Path path) throws IOException { + writeToFile(text, path, false); + } + + /** + * Write text to a file. + * @param text The text to be written. + * @param path The file to write the code to. + */ + public static void writeToFile(CharSequence text, Path path) throws IOException { + writeToFile(text.toString(), path, false); } /** diff --git a/org.lflang/src/org/lflang/generator/NamedInstance.java b/org.lflang/src/org/lflang/generator/NamedInstance.java index b047a53bbd..ce99549e5a 100644 --- a/org.lflang/src/org/lflang/generator/NamedInstance.java +++ b/org.lflang/src/org/lflang/generator/NamedInstance.java @@ -126,6 +126,21 @@ public ReactorInstance getParent() { return parent; } + /** + * Return the parent at the given depth or null if there is + * no parent at the given depth. + * @param d The depth. + */ + public ReactorInstance getParent(int d) { + if (d >= depth || d < 0) return null; + ReactorInstance p = parent; + while (p != null) { + if (p.depth == d) return p; + p = p.parent; + } + return null; + } + /** * Return the width of this instance, which in this base class is 1. * Subclasses PortInstance and ReactorInstance change this to the @@ -167,19 +182,6 @@ public List parents() { return result; } - /** - * Return the root reactor if it is marked as as main or federated, - * and otherwise return null. - * @return The main/federated top-level parent. - */ - public ReactorInstance main() { - ReactorInstance r = this.root(); - if (r != null && r.isMainOrFederated()) { - return r; - } - return null; - } - /** * Return the root reactor, which is the top-level parent. * @return The top-level parent. diff --git a/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java b/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java index fbcfee645e..bc77c60939 100644 --- a/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java +++ b/org.lflang/src/org/lflang/generator/ReactionInstanceGraph.java @@ -94,6 +94,14 @@ public void rebuild() { } } + /* + * Get an array of non-negative integers representing the number of reactions + * per each level, where levels are indices of the array. + */ + public Integer[] getNumReactionsPerLevel() { + return numReactionsPerLevel.toArray(new Integer[0]); + } + /////////////////////////////////////////////////////////// //// Protected methods @@ -206,6 +214,16 @@ protected void addNodesAndEdges(ReactorInstance reactor) { } } + /////////////////////////////////////////////////////////// + //// Private fields + + /** + * Number of reactions per level, represented as a list of + * integers where the indices are the levels. + */ + private List numReactionsPerLevel = new ArrayList<>( + List.of(Integer.valueOf(0))); + /////////////////////////////////////////////////////////// //// Private methods @@ -224,7 +242,7 @@ private void assignLevels() { // All root nodes start with level 0. for (Runtime origin : start) { - origin.level = 0; + assignLevel(origin, 0); } // No need to do any of this if there are no root nodes; @@ -232,13 +250,18 @@ private void assignLevels() { while (!start.isEmpty()) { Runtime origin = start.remove(0); Set toRemove = new LinkedHashSet(); + Set downstreamAdjacentNodes = getDownstreamAdjacentNodes(origin); + // All downstream adjacent nodes start with a level 0. Adjust the + // maxNumOfReactionPerLevel field accordingly (to be + // updated in the for loop below). + adjustNumReactionsPerLevel(0, downstreamAdjacentNodes.size()); // Visit effect nodes. - for (Runtime effect : getDownstreamAdjacentNodes(origin)) { + for (Runtime effect : downstreamAdjacentNodes) { // 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); + updateLevel(effect, origin.level+1); } // Remove visited edges. for (Runtime effect : toRemove) { @@ -254,4 +277,47 @@ private void assignLevels() { removeNode(origin); } } + + /** + * Assign a level to a reaction runtime instance. + * + * @param runtime The reaction runtime instance. + * @param level The level to assign. + */ + private void assignLevel(Runtime runtime, Integer level) { + runtime.level = level; + adjustNumReactionsPerLevel(level, 1); + } + + /** + * Update the level of the reaction runtime instance + * to level if level is larger than the + * level already assigned to runtime. + */ + private void updateLevel(Runtime runtime, Integer level) { + if (runtime.level < level) { + adjustNumReactionsPerLevel(runtime.level, -1); + runtime.level = level; + adjustNumReactionsPerLevel(level, 1); + } + } + + /** + * Adjust {@link #numReactionsPerLevel} at index level by + * adding to the previously recorded number valueToAdd. + * If there is no previously recorded number for this level, then + * create one with index level and value valueToAdd. + * @param level The level. + * @param valueToAdd The value to add to the number of levels. + */ + private void adjustNumReactionsPerLevel(int level, int valueToAdd) { + if (numReactionsPerLevel.size() > level) { + numReactionsPerLevel.set(level, numReactionsPerLevel.get(level) + valueToAdd); + } else { + while (numReactionsPerLevel.size() < level) { + numReactionsPerLevel.add(0); + } + numReactionsPerLevel.add(valueToAdd); + } + } } diff --git a/org.lflang/src/org/lflang/generator/ReactorInstance.java b/org.lflang/src/org/lflang/generator/ReactorInstance.java index 09cc8c2c96..81a1d1c508 100644 --- a/org.lflang/src/org/lflang/generator/ReactorInstance.java +++ b/org.lflang/src/org/lflang/generator/ReactorInstance.java @@ -273,7 +273,7 @@ public PortInstance getInput(String name) { } return null; } - + /** * Override the base class to append [i_d], where d is the depth, * if this reactor is in a bank of reactors. diff --git a/org.lflang/src/org/lflang/generator/Validator.java b/org.lflang/src/org/lflang/generator/Validator.java index 739601fbd2..f62f94083c 100644 --- a/org.lflang/src/org/lflang/generator/Validator.java +++ b/org.lflang/src/org/lflang/generator/Validator.java @@ -28,6 +28,13 @@ */ public abstract class Validator { + /** + * Files older than {@code FILE_AGE_THRESHOLD_MILLIS} may be skipped in validation on the + * grounds that they probably have not been updated since the last validator pass. + */ + // This will cause silent validation failures if it takes too long to write all generated code to the file system. + private static final long FILE_AGE_THRESHOLD_MILLIS = 10000; + protected static class Pair { public final S first; public final T second; @@ -132,11 +139,15 @@ public final int run(LFCommand command, CancelIndicator cancelIndicator) { */ private List> getValidationStrategies() { final List> commands = new ArrayList<>(); + long mostRecentDateModified = codeMaps.keySet().stream() + .map(it -> it.toFile().lastModified()).reduce(0L, Math::max); for (Path generatedFile : codeMaps.keySet()) { - final Pair p = getValidationStrategy(generatedFile); - if (p.first == null || p.second == null) continue; - commands.add(p); - if (p.first.isFullBatch()) break; + if (generatedFile.toFile().lastModified() > mostRecentDateModified - FILE_AGE_THRESHOLD_MILLIS) { + final Pair p = getValidationStrategy(generatedFile); + if (p.first == null || p.second == null) continue; + commands.add(p); + if (p.first.isFullBatch()) break; + } } return commands; } diff --git a/org.lflang/src/org/lflang/generator/c/CCmakeCompiler.java b/org.lflang/src/org/lflang/generator/c/CCmakeCompiler.java index 089a967cb7..4c31b8da77 100644 --- a/org.lflang/src/org/lflang/generator/c/CCmakeCompiler.java +++ b/org.lflang/src/org/lflang/generator/c/CCmakeCompiler.java @@ -30,9 +30,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; - -import org.eclipse.xtext.util.CancelIndicator; - import org.lflang.ErrorReporter; import org.lflang.FileConfig; import org.lflang.TargetConfig.Mode; diff --git a/org.lflang/src/org/lflang/generator/c/CCmakeGenerator.java b/org.lflang/src/org/lflang/generator/c/CCmakeGenerator.java index ffa17aec2b..a9effdf5d1 100644 --- a/org.lflang/src/org/lflang/generator/c/CCmakeGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CCmakeGenerator.java @@ -84,69 +84,83 @@ CodeBuilder generateCMakeCode( fileConfig.getSrcGenPath().resolve(Paths.get(file))); additionalSources.add(FileConfig.toUnixString(relativePath)); } + cMakeCode.newLine(); cMakeCode.pr("cmake_minimum_required(VERSION 3.13)"); cMakeCode.pr("project("+executableName+" LANGUAGES C)"); - cMakeCode.pr(""); + cMakeCode.newLine(); cMakeCode.pr("# Require C11"); cMakeCode.pr("set(CMAKE_C_STANDARD 11)"); cMakeCode.pr("set(CMAKE_C_STANDARD_REQUIRED ON)"); - cMakeCode.pr(""); + cMakeCode.newLine(); cMakeCode.pr("# Require C++17"); cMakeCode.pr("set(CMAKE_CXX_STANDARD 17)"); cMakeCode.pr("set(CMAKE_CXX_STANDARD_REQUIRED ON)"); - cMakeCode.pr(""); + cMakeCode.newLine(); - // Follow the - cMakeCode.pr("set(DEFAULT_BUILD_TYPE " + targetConfig.cmakeBuildType + ")"); - cMakeCode.pr("if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)"); - cMakeCode.pr(" set(CMAKE_BUILD_TYPE ${DEFAULT_BUILD_TYPE} CACHE STRING \"Choose the type of build.\" FORCE)"); - cMakeCode.pr("endif()"); + cMakeCode.pr("# Compile definitions\n"); + targetConfig.compileDefinitions.forEach( (key, value) -> { + cMakeCode.pr("add_compile_definitions("+key+"="+value+")\n"); + }); + cMakeCode.newLine(); + + // Set the build type + cMakeCode.pr("set(DEFAULT_BUILD_TYPE " + targetConfig.cmakeBuildType + ")\n"); + cMakeCode.pr("if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n"); + cMakeCode.pr(" set(CMAKE_BUILD_TYPE ${DEFAULT_BUILD_TYPE} CACHE STRING \"Choose the type of build.\" FORCE)\n"); + cMakeCode.pr("endif()\n"); + cMakeCode.newLine(); cMakeCode.pr("set(CoreLib core)"); cMakeCode.pr("set(PlatformLib platform)"); - cMakeCode.pr(""); + cMakeCode.newLine(); if (CppMode) { // Suppress warnings about const char*. cMakeCode.pr("set(CMAKE_CXX_FLAGS \"${CMAKE_CXX_FLAGS} -Wno-write-strings\")"); + cMakeCode.newLine(); } cMakeCode.pr("include(${CoreLib}/platform/Platform.cmake)"); + cMakeCode.newLine(); cMakeCode.pr("include_directories(${CoreLib})"); cMakeCode.pr("include_directories(${CoreLib}/platform)"); cMakeCode.pr("include_directories(${CoreLib}/federated)"); + cMakeCode.newLine(); cMakeCode.pr("set(LF_MAIN_TARGET "+executableName+")"); + cMakeCode.newLine(); + if (hasMain) { cMakeCode.pr("# Declare a new executable target and list all its sources"); - cMakeCode.pr( - "add_executable(${LF_MAIN_TARGET} " + String.join("\n", sources) - + " ${CoreLib}/platform/${LF_PLATFORM_FILE} " + String.join("\n", additionalSources) + ")\n" - ); + cMakeCode.pr("add_executable("); } else { cMakeCode.pr("# Declare a new library target and list all its sources"); - cMakeCode.pr( - "add_library(${LF_MAIN_TARGET} " + String.join("\n", sources) - + " ${CoreLib}/platform/${LF_PLATFORM_FILE} " + String.join("\n", additionalSources) + ")\n" - ); + cMakeCode.pr("add_library("); } - cMakeCode.pr(""); + cMakeCode.indent(); + cMakeCode.pr("${LF_MAIN_TARGET}"); + sources.forEach(source -> {cMakeCode.pr(source);}); + cMakeCode.pr("${CoreLib}/platform/${LF_PLATFORM_FILE}"); + additionalSources.forEach(source -> {cMakeCode.pr(source);}); + cMakeCode.unindent(); + cMakeCode.pr(")"); + cMakeCode.newLine(); if (targetConfig.threads != 0 || targetConfig.tracing != null) { // If threaded computation is requested, add a the threads option. cMakeCode.pr("# Find threads and link to it"); cMakeCode.pr("find_package(Threads REQUIRED)"); cMakeCode.pr("target_link_libraries( ${LF_MAIN_TARGET} Threads::Threads)"); - cMakeCode.pr(""); + cMakeCode.newLine(); // If the LF program itself is threaded or if tracing is enabled, we need to define // NUMBER_OF_WORKERS so that platform-specific C files will contain the appropriate functions cMakeCode.pr("# Set the number of workers to enable threading"); cMakeCode.pr("target_compile_definitions( ${LF_MAIN_TARGET} PUBLIC NUMBER_OF_WORKERS="+targetConfig.threads+")"); - cMakeCode.pr(""); + cMakeCode.newLine(); } // Check if CppMode is enabled @@ -163,6 +177,7 @@ CodeBuilder generateCMakeCode( cMakeCode.pr("set_source_files_properties( "+source+" PROPERTIES LANGUAGE CXX)"); } cMakeCode.pr("set_source_files_properties(${CoreLib}/platform/${LF_PLATFORM_FILE} PROPERTIES LANGUAGE CXX)"); + cMakeCode.newLine(); } if (targetConfig.compiler != null && !targetConfig.compiler.isBlank()) { @@ -172,6 +187,7 @@ CodeBuilder generateCMakeCode( } else { cMakeCode.pr("set(CMAKE_C_COMPILER "+targetConfig.compiler+")"); } + cMakeCode.newLine(); } // Set the compiler flags @@ -206,17 +222,22 @@ CodeBuilder generateCMakeCode( cMakeCode.pr("add_link_options( "+compilerFlag+")"); } } - cMakeCode.pr(""); + cMakeCode.newLine(); // Add the install option - cMakeCode.pr("install(TARGETS ${LF_MAIN_TARGET}"); - cMakeCode.pr(" RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})"); - cMakeCode.pr(""); + cMakeCode.pr("install("); + cMakeCode.indent(); + cMakeCode.pr("TARGETS ${LF_MAIN_TARGET}"); + cMakeCode.pr("RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}"); + cMakeCode.unindent(); + cMakeCode.pr(")"); + cMakeCode.newLine(); // Add the include file for (String includeFile : targetConfig.cmakeIncludesWithoutPath) { cMakeCode.pr("include(\""+includeFile+"\")"); - } + } + cMakeCode.newLine(); return cMakeCode; } diff --git a/org.lflang/src/org/lflang/generator/c/CCompiler.java b/org.lflang/src/org/lflang/generator/c/CCompiler.java index 66bab32aea..7d8ee9ec09 100644 --- a/org.lflang/src/org/lflang/generator/c/CCompiler.java +++ b/org.lflang/src/org/lflang/generator/c/CCompiler.java @@ -29,9 +29,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; - -import org.eclipse.xtext.util.CancelIndicator; - import org.lflang.ErrorReporter; import org.lflang.FileConfig; import org.lflang.TargetConfig.Mode; @@ -187,8 +184,12 @@ public LFCommand compileCCommand( } compileArgs.addAll(targetConfig.compileLibraries); + // Add compile definitions + targetConfig.compileDefinitions.forEach( (key,value) -> { + compileArgs.add("-D"+key+"="+value); + }); + // If threaded computation is requested, add a -pthread option. - if (targetConfig.threads != 0 || targetConfig.tracing != null) { compileArgs.add("-pthread"); // If the LF program itself is threaded or if tracing is enabled, we need to define diff --git a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend index 0c2b4d157a..c1fff5602d 100644 --- a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend +++ b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend @@ -38,7 +38,6 @@ import java.util.concurrent.TimeUnit import java.util.regex.Pattern import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.util.CancelIndicator -import org.lflang.ASTUtils import org.lflang.ErrorReporter import org.lflang.FileConfig import org.lflang.InferredType @@ -57,25 +56,21 @@ import org.lflang.federated.FederateInstance import org.lflang.federated.launcher.FedCLauncher import org.lflang.federated.serialization.FedROS2CPPSerialization import org.lflang.federated.serialization.SupportedSerializers -import org.lflang.generator.ActionInstance import org.lflang.generator.CodeBuilder import org.lflang.generator.GeneratorBase import org.lflang.generator.GeneratorResult import org.lflang.generator.IntegratedBuilder import org.lflang.generator.JavaGeneratorUtils import org.lflang.generator.LFGeneratorContext -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.SubContext -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.Delay import org.lflang.lf.Input import org.lflang.lf.Instantiation @@ -338,7 +333,7 @@ class CGenerator extends GeneratorBase { } /** - * Set C-specific default target properties if needed. + * Set C-specific default target configurations if needed. */ def setCSpecificDefaults(LFGeneratorContext context) { if (!targetConfig.useCmake && targetConfig.compiler.isNullOrEmpty) { @@ -350,6 +345,17 @@ class CGenerator extends GeneratorBase { targetConfig.compilerFlags.addAll("-O2") // "-Wall -Wconversion" } } + if (isFederated) { + // Add compile definitions for federated execution + targetConfig.compileDefinitions.put("FEDERATED", ""); + if (targetConfig.coordination === CoordinationType.CENTRALIZED) { + // The coordination is centralized. + targetConfig.compileDefinitions.put("FEDERATED_CENTRALIZED", ""); + } else if (targetConfig.coordination === CoordinationType.DECENTRALIZED) { + // The coordination is decentralized + targetConfig.compileDefinitions.put("FEDERATED_DECENTRALIZED", ""); + } + } } /** @@ -433,39 +439,8 @@ class CGenerator extends GeneratorBase { if (!isOSCompatible()) return; // Incompatible OS and configuration - // Check for duplicate declarations. - val names = newLinkedHashSet - 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); - for (d : declarations) { - if (!names.add(d.name)) { - // Report duplicate declaration. - errorReporter.reportError("Multiple declarations for reactor class '" + d.name + "'.") - } - } - } - - // Build the instantiation tree if a main reactor is present. - if (this.mainDef !== null) { - if (this.main === null) { - // Recursively build instances. This is done once because - // it is the same for all federates. - this.main = new ReactorInstance(mainDef.reactorClass.toDefinition, errorReporter, - this.unorderedReactions) - 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); - } - } + // Create the main reactor instance if there is a main reactor. + createMainReactorInstance(); // Create the output directories if they don't yet exist. var dir = fileConfig.getSrcGenPath.toFile @@ -483,14 +458,19 @@ class CGenerator extends GeneratorBase { var coreFiles = newArrayList( "reactor_common.c", "reactor.h", - "pqueue.c", - "pqueue.h", "tag.h", "tag.c", "trace.h", "trace.c", - "util.h", - "util.c", + "utils/pqueue.c", + "utils/pqueue.h", + "utils/pqueue_support.h", + "utils/vector.c", + "utils/vector.h", + "utils/semaphore.h", + "utils/semaphore.c", + "utils/util.h", + "utils/util.c", "platform.h", "platform/Platform.cmake", "mixed_radix.c", @@ -499,7 +479,8 @@ class CGenerator extends GeneratorBase { if (targetConfig.threads === 0) { coreFiles.add("reactor.c") } else { - coreFiles.add("reactor_threaded.c") + addSchedulerFiles(coreFiles); + coreFiles.add("threaded/reactor_threaded.c") } addPlatformFiles(coreFiles); @@ -540,7 +521,7 @@ class CGenerator extends GeneratorBase { ) ) val compileThreadPool = Executors.newFixedThreadPool(numOfCompileThreads); - System.out.println("******** Using "+numOfCompileThreads+" threads."); + System.out.println("******** Using "+numOfCompileThreads+" threads to compile the program."); var federateCount = 0; val LFGeneratorContext generatingContext = new SubContext( context, IntegratedBuilder.VALIDATED_PERCENT_PROGRESS, IntegratedBuilder.GENERATED_PERCENT_PROGRESS @@ -602,7 +583,7 @@ class CGenerator extends GeneratorBase { } // Copy the core lib - fileConfig.copyFilesFromClassPath("/lib/c/reactor-c/core", fileConfig.getSrcGenPath + File.separator + "core", coreFiles) + fileConfig.copyFilesFromClassPath("/lib/c/reactor-c/core", fileConfig.getSrcGenPath.resolve("core"), coreFiles) // Copy the header files copyTargetHeaderFile() @@ -629,7 +610,21 @@ class CGenerator extends GeneratorBase { // Note that any main reactors in imported files are ignored. // Skip generation if there are cycles. if (this.main !== null) { - generateMain() + initializeTriggerObjects.pr(''' + 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; + '''); + + // 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); + + generateReactorInstance(this.main) // Generate function to set default command-line options. // A literal array needs to be given outside any function definition, // so start with that. @@ -775,7 +770,7 @@ class CGenerator extends GeneratorBase { bool _lf_trigger_shutdown_reactions() { for (int i = 0; i < _lf_shutdown_reactions_size; i++) { if (_lf_shutdown_reactions[i] != NULL) { - _lf_enqueue_reaction(_lf_shutdown_reactions[i]); + _lf_trigger_reaction(_lf_shutdown_reactions[i], -1); } } // Return true if there are shutdown reactions. @@ -916,6 +911,41 @@ class CGenerator extends GeneratorBase { JavaGeneratorUtils.refreshProject(resource, context.mode) } + /** + * Add files needed for the proper function of the runtime scheduler to + * {@code coreFiles} and {@link TargetConfig#compileAdditionalSources}. + */ + def addSchedulerFiles(ArrayList coreFiles) { + coreFiles.add("threaded/scheduler.h") + coreFiles.add("threaded/scheduler_instance.h") + coreFiles.add("threaded/scheduler_sync_tag_advance.c") + // Don't use a scheduler that does not prioritize reactions based on deadlines + // if the program contains a deadline (handler). Use the GEDF_NP scheduler instead. + if (!targetConfig.schedulerType.prioritizesDeadline) { + // Check if a deadline is assigned to any reaction + if (reactors.filter[reactor | + // Filter reactors that contain at least one reaction + // that has a deadline handler. + return reactor.allReactions.filter[ reaction | + return reaction.deadline !== null + ].size > 0; + ].size > 0) { + if (!targetConfig.setByUser.contains(TargetProperty.SCHEDULER)) { + targetConfig.schedulerType = TargetProperty.SchedulerOption.GEDF_NP; + } + } + } + coreFiles.add("threaded/scheduler_" + targetConfig.schedulerType.toString() + ".c"); + targetConfig.compileAdditionalSources.add( + "core" + File.separator + "threaded" + File.separator + + "scheduler_" + targetConfig.schedulerType.toString() + ".c" + ); + System.out.println("******** Using the "+targetConfig.schedulerType.toString()+" runtime scheduler."); + targetConfig.compileAdditionalSources.add( + "core" + File.separator + "utils" + File.separator + "semaphore.c" + ); + } + /** * Generate the _lf_trigger_startup_reactions function. */ @@ -925,7 +955,7 @@ class CGenerator extends GeneratorBase { «IF startupReactionCount > 0» for (int i = 0; i < _lf_startup_reactions_size; i++) { if (_lf_startup_reactions[i] != NULL) { - _lf_enqueue_reaction(_lf_startup_reactions[i]); + _lf_trigger_reaction(_lf_startup_reactions[i], -1); } } «ENDIF» @@ -1027,6 +1057,9 @@ class CGenerator extends GeneratorBase { setReactionPriorities(main, code) initializeFederate(federate) + + initializeScheduler(); + code.unindent() code.pr("}\n") } @@ -1266,21 +1299,17 @@ class CGenerator extends GeneratorBase { val contents = new CodeBuilder() // The Docker configuration uses cmake, so config.compiler is ignored here. var compileCommand = ''' - cmake -S src-gen -B bin && \ - cd bin && \ - make all + RUN set -ex && \ + mkdir bin && \ + cmake -S src-gen -B bin && \ + cd bin && \ + make all ''' if (!targetConfig.buildCommands.nullOrEmpty) { compileCommand = targetConfig.buildCommands.join(' ') } - var additionalFiles = '' - if (!targetConfig.fileNames.nullOrEmpty) { - additionalFiles = '''COPY "«targetConfig.fileNames.join('" "')»" "src-gen/"''' - } var dockerCompiler = CCppMode ? 'g++' : 'gcc' var fileExtension = CCppMode ? 'cpp' : 'c' - val OS = System.getProperty("os.name").toLowerCase(); - var dockerComposeCommand = (OS.indexOf("nux") >= 0) ? "docker-compose" : "docker compose" contents.pr(''' # Generated docker file for «topLevelName» in «srcGenPath». @@ -1288,14 +1317,8 @@ class CGenerator extends GeneratorBase { FROM «targetConfig.dockerOptions.from» AS builder WORKDIR /lingua-franca/«topLevelName» RUN set -ex && apk add --no-cache «dockerCompiler» musl-dev cmake make - COPY core src-gen/core - COPY ctarget.h ctarget.c src-gen/ - COPY CMakeLists.txt \ - «topLevelName».«fileExtension» src-gen/ - «additionalFiles» - RUN set -ex && \ - mkdir bin && \ - «compileCommand» + COPY . src-gen + «compileCommand» FROM «targetConfig.dockerOptions.from» WORKDIR /lingua-franca @@ -1306,15 +1329,7 @@ class CGenerator extends GeneratorBase { ENTRYPOINT ["./bin/«topLevelName»"] ''') contents.writeToFile(dockerFile) - println('''Dockerfile for «topLevelName» written to ''' + dockerFile) - println(''' - ##################################### - To build the docker image, go to «dockerComposeDir.toString()» and run: - - «dockerComposeCommand» build «federateName» - - ##################################### - ''') + println(getDockerBuildCommand(dockerFile, dockerComposeDir, federateName)) } /** @@ -1526,12 +1541,34 @@ class CGenerator extends GeneratorBase { } } + /** + * Generate code to initialize the scheduler foer the threaded C runtime. + */ + protected def initializeScheduler() { + if (targetConfig.threads > 0) { + val numReactionsPerLevel = this.main.assignLevels.getNumReactionsPerLevel(); + code.pr(''' + + // Initialize the scheduler + size_t num_reactions_per_level[«numReactionsPerLevel.size»] = + {«numReactionsPerLevel.join(", \\\n")»}; + sched_params_t sched_params = (sched_params_t) { + .num_reactions_per_level = &num_reactions_per_level[0], + .num_reactions_per_level_size = (size_t) «numReactionsPerLevel.size»}; + lf_sched_init( + «targetConfig.threads», + &sched_params + ); + ''') + } + } + /** * Copy target-specific header file to the src-gen directory. */ def copyTargetHeaderFile() { - fileConfig.copyFileFromClassPath("/lib/c/reactor-c/include/ctarget.h", fileConfig.getSrcGenPath + File.separator + "ctarget.h") - fileConfig.copyFileFromClassPath("/lib/c/reactor-c/lib/ctarget.c", fileConfig.getSrcGenPath + File.separator + "ctarget.c") + fileConfig.copyFileFromClassPath("/lib/c/reactor-c/include/ctarget.h", fileConfig.getSrcGenPath.resolve("ctarget.h")) + fileConfig.copyFileFromClassPath("/lib/c/reactor-c/lib/ctarget.c", fileConfig.getSrcGenPath.resolve("ctarget.c")) } //////////////////////////////////////////// @@ -1694,8 +1731,8 @@ class CGenerator extends GeneratorBase { // Some of the following methods create lines of code that need to // go into the constructor. Collect those lines of code here: val constructorCode = new CodeBuilder() - generateAuxiliaryStructs(reactor, currentFederate) - generateSelfStruct(reactor, currentFederate, constructorCode) + generateAuxiliaryStructs(reactor) + generateSelfStruct(reactor, constructorCode) generateReactions(reactor, currentFederate) generateConstructor(reactor, currentFederate, constructorCode) @@ -1738,13 +1775,10 @@ class CGenerator extends GeneratorBase { /** * Generate the struct type definitions for inputs, outputs, and - * actions of the specified reactor in the specified federate. + * actions of the specified reactor. * @param reactor The parsed reactor data structure. - * @param federate A federate name, or null to unconditionally generate. */ - protected def generateAuxiliaryStructs( - ReactorDecl decl, FederateInstance federate - ) { + protected def generateAuxiliaryStructs(ReactorDecl decl) { val reactor = decl.toDefinition // In the case where there are incoming // p2p logical connections in decentralized @@ -1766,54 +1800,48 @@ class CGenerator extends GeneratorBase { } // First, handle inputs. for (input : reactor.allInputs) { - if (federate === null || federate.contains(input as Port)) { - var token = '' - if (input.inferredType.isTokenType) { - token = ''' - lf_token_t* token; - int length; - ''' - } - code.pr(input, ''' - typedef struct { - «input.valueDeclaration» - bool is_present; - int num_destinations; - «token» - «federatedExtension.toString» - } «variableStructType(input, decl)»; - ''') - } - + var token = '' + if (CUtil.isTokenType(input.inferredType, types)) { + token = ''' + lf_token_t* token; + int length; + ''' + } + code.pr(input, ''' + typedef struct { + «input.valueDeclaration» + bool is_present; + int num_destinations; + «token» + «federatedExtension.toString» + } «variableStructType(input, decl)»; + ''') } // Next, handle outputs. - for (output : reactor.allOutputs) { - if (federate === null || federate.contains(output as Port)) { - var token = '' - if (output.inferredType.isTokenType) { - token = ''' - lf_token_t* token; - int length; - ''' - } - code.pr(output, ''' - typedef struct { - «output.valueDeclaration» - bool is_present; - int num_destinations; - «token» - «federatedExtension.toString» - } «variableStructType(output, decl)»; - ''') - } - + for (output : reactor.allOutputs) { + var token = '' + if (CUtil.isTokenType(output.inferredType, types)) { + token = ''' + lf_token_t* token; + int length; + ''' + } + code.pr(output, ''' + typedef struct { + «output.valueDeclaration» + bool is_present; + int num_destinations; + «token» + «federatedExtension.toString» + } «variableStructType(output, decl)»; + ''') } // Finally, handle actions. // The very first item on this struct needs to be // a trigger_t* because the struct will be cast to (trigger_t*) // by the schedule() functions to get to the trigger. for (action : reactor.allActions) { - if (federate === null || federate.contains(action)) { + if (currentFederate.contains(action)) { code.pr(action, ''' typedef struct { trigger_t* trigger; @@ -1876,15 +1904,10 @@ class CGenerator extends GeneratorBase { * Generate the self struct type definition for the specified reactor * in the specified federate. * @param reactor The parsed reactor data structure. - * @param federate A federate name, or null to unconditionally generate. * @param constructorCode Place to put lines of code that need to * go into the constructor. */ - protected def generateSelfStruct( - ReactorDecl decl, - FederateInstance federate, - CodeBuilder constructorCode - ) { + private def generateSelfStruct(ReactorDecl decl, CodeBuilder constructorCode) { val reactor = decl.toDefinition val selfType = CUtil.selfType(decl) @@ -1894,7 +1917,7 @@ class CGenerator extends GeneratorBase { var body = new CodeBuilder() // Extensions can add functionality to the CGenerator - generateSelfStructExtension(body, decl, federate, constructorCode) + generateSelfStructExtension(body, decl, constructorCode) // Next handle parameters. generateParametersForReactor(body, reactor) @@ -1904,7 +1927,7 @@ class CGenerator extends GeneratorBase { // Next handle actions. for (action : reactor.allActions) { - if (federate === null || federate.contains(action)) { + if (currentFederate.contains(action)) { body.pr(action, ''' «variableStructType(action, decl)» _lf_«action.name»; ''') @@ -1917,61 +1940,57 @@ class CGenerator extends GeneratorBase { // Next handle inputs. for (input : reactor.allInputs) { - if (federate === null || federate.contains(input as Port)) { - // If the port is a multiport, the input field is an array of - // pointers that will be allocated separately for each instance - // because the sizes may be different. Otherwise, it is a simple - // pointer. - if (JavaAstUtils.isMultiport(input)) { - body.pr(input, ''' - // Multiport input array will be malloc'd later. - «variableStructType(input, decl)»** _lf_«input.name»; - int _lf_«input.name»_width; - // Default input (in case it does not get connected) - «variableStructType(input, decl)» _lf_default__«input.name»; - ''') - } else { - // input is not a multiport. - body.pr(input, ''' - «variableStructType(input, decl)»* _lf_«input.name»; - // width of -2 indicates that it is not a multiport. - int _lf_«input.name»_width; - // Default input (in case it does not get connected) - «variableStructType(input, decl)» _lf_default__«input.name»; - ''') - - constructorCode.pr(input, ''' - // Set input by default to an always absent default input. - self->_lf_«input.name» = &self->_lf_default__«input.name»; - ''') - } + // If the port is a multiport, the input field is an array of + // pointers that will be allocated separately for each instance + // because the sizes may be different. Otherwise, it is a simple + // pointer. + if (JavaAstUtils.isMultiport(input)) { + body.pr(input, ''' + // Multiport input array will be malloc'd later. + «variableStructType(input, decl)»** _lf_«input.name»; + int _lf_«input.name»_width; + // Default input (in case it does not get connected) + «variableStructType(input, decl)» _lf_default__«input.name»; + ''') + } else { + // input is not a multiport. + body.pr(input, ''' + «variableStructType(input, decl)»* _lf_«input.name»; + // width of -2 indicates that it is not a multiport. + int _lf_«input.name»_width; + // Default input (in case it does not get connected) + «variableStructType(input, decl)» _lf_default__«input.name»; + ''') + + constructorCode.pr(input, ''' + // Set input by default to an always absent default input. + self->_lf_«input.name» = &self->_lf_default__«input.name»; + ''') } } // Next handle outputs. for (output : reactor.allOutputs) { - if (federate === null || federate.contains(output as Port)) { - // If the port is a multiport, create an array to be allocated - // at instantiation. - if (JavaAstUtils.isMultiport(output)) { - body.pr(output, ''' - // Array of output ports. - «variableStructType(output, decl)»* _lf_«output.name»; - int _lf_«output.name»_width; - // An array of pointers to the individual ports. Useful - // for the SET macros to work out-of-the-box for - // multiports in the body of reactions because their - // value can be accessed via a -> operator (e.g.,foo[i]->value). - // So we have to handle multiports specially here a construct that - // array of pointers. - «variableStructType(output, decl)»** _lf_«output.name»_pointers; - ''') - } else { - body.pr(output, ''' - «variableStructType(output, decl)» _lf_«output.name»; - int _lf_«output.name»_width; - ''') - } + // If the port is a multiport, create an array to be allocated + // at instantiation. + if (JavaAstUtils.isMultiport(output)) { + body.pr(output, ''' + // Array of output ports. + «variableStructType(output, decl)»* _lf_«output.name»; + int _lf_«output.name»_width; + // An array of pointers to the individual ports. Useful + // for the SET macros to work out-of-the-box for + // multiports in the body of reactions because their + // value can be accessed via a -> operator (e.g.,foo[i]->value). + // So we have to handle multiports specially here a construct that + // array of pointers. + «variableStructType(output, decl)»** _lf_«output.name»_pointers; + ''') + } else { + body.pr(output, ''' + «variableStructType(output, decl)» _lf_«output.name»; + int _lf_«output.name»_width; + ''') } } @@ -1982,10 +2001,10 @@ class CGenerator extends GeneratorBase { // struct has a place to hold the data produced by this reactor's // reactions and a place to put pointers to data produced by // the contained reactors. - generateInteractingContainedReactors(reactor, federate, body, constructorCode); + generateInteractingContainedReactors(reactor, body, constructorCode); // Next, generate the fields needed for each reaction. - generateReactionAndTriggerStructs(body, decl, constructorCode, federate) + generateReactionAndTriggerStructs(body, decl, constructorCode); // The first field has to always be a pointer to the list of // of allocated memory that must be freed when the reactor is freed. @@ -2011,19 +2030,17 @@ class CGenerator extends GeneratorBase { * the contained reactors. * * @param reactor The reactor. - * @param federate The federate instance. * @param body The place to put the struct definition for the contained reactors. * @param constructorCode The place to put matching code that goes in the container's constructor. */ private def generateInteractingContainedReactors( Reactor reactor, - FederateInstance federate, CodeBuilder body, CodeBuilder constructorCode ) { // The contents of the struct will be collected first so that // we avoid duplicate entries and then the struct will be constructed. - val contained = new InteractingContainedReactors(reactor, federate); + val contained = new InteractingContainedReactors(reactor, currentFederate); // Next generate the relevant code. for (containedReactor : contained.containedReactors) { // First define an _width variable in case it is a bank. @@ -2032,7 +2049,7 @@ class CGenerator extends GeneratorBase { // If the instantiation is a bank, find the maximum bank width // to define an array. if (containedReactor.widthSpec !== null) { - width = maxContainedReactorBankWidth(containedReactor, null, 0); + width = CReactionGenerator.maxContainedReactorBankWidth(containedReactor, null, 0, mainDef); array = "[" + width + "]"; } // NOTE: The following needs to be done for each instance @@ -2156,13 +2173,11 @@ class CGenerator extends GeneratorBase { * This function is provided to allow extensions of the CGenerator to append the structure of the self struct * @param body The body of the self struct * @param decl The reactor declaration for the self struct - * @param instance The current federate instance * @param constructorCode Code that is executed when the reactor is instantiated */ def void generateSelfStructExtension( CodeBuilder body, ReactorDecl decl, - FederateInstance instance, CodeBuilder constructorCode ) { // Do nothing @@ -2202,13 +2217,11 @@ class CGenerator extends GeneratorBase { * @param body The place to put the code for the self struct. * @param reactor The reactor. * @param constructorCode The place to put the constructor code. - * @param federate The federate instance, or null if there is no federation. */ protected def void generateReactionAndTriggerStructs( CodeBuilder body, ReactorDecl decl, - CodeBuilder constructorCode, - FederateInstance federate + CodeBuilder constructorCode ) { var reactionCount = 0; val reactor = decl.toDefinition @@ -2224,7 +2237,7 @@ class CGenerator extends GeneratorBase { val startupReactions = new LinkedHashSet val shutdownReactions = new LinkedHashSet for (reaction : reactor.allReactions) { - if (federate === null || federate.contains(reaction)) { + if (currentFederate.contains(reaction)) { // Create the reaction_t struct. body.pr(reaction, '''reaction_t _lf__reaction_«reactionCount»;''') @@ -2260,7 +2273,7 @@ class CGenerator extends GeneratorBase { var deadlineFunctionPointer = "NULL" if (reaction.deadline !== null) { // The following has to match the name chosen in generateReactions - val deadlineFunctionName = decl.name.toLowerCase + '_deadline_function' + reactionCount + val deadlineFunctionName = CReactionGenerator.generateDeadlineFunctionName(decl, reactionCount) deadlineFunctionPointer = "&" + deadlineFunctionName } @@ -2282,7 +2295,7 @@ class CGenerator extends GeneratorBase { // self->_lf__reaction_«reactionCount».is_STP_violated = false; constructorCode.pr(reaction, ''' self->_lf__reaction_«reactionCount».number = «reactionCount»; - self->_lf__reaction_«reactionCount».function = «reactionFunctionName(decl, reactionCount)»; + self->_lf__reaction_«reactionCount».function = «CReactionGenerator.generateReactionFunctionName(decl, reactionCount)»; self->_lf__reaction_«reactionCount».self = self; self->_lf__reaction_«reactionCount».deadline_violation_handler = «deadlineFunctionPointer»; self->_lf__reaction_«reactionCount».STP_handler = «STPFunctionPointer»; @@ -2364,7 +2377,7 @@ class CGenerator extends GeneratorBase { // Next handle actions. for (action : reactor.allActions) { - if (federate === null || federate.contains(action)) { + if (currentFederate.contains(action)) { createTriggerT(body, action, triggerMap, constructorCode) var isPhysical = "true"; if (action.origin == ActionOrigin.LOGICAL) { @@ -2391,9 +2404,7 @@ class CGenerator extends GeneratorBase { // Next handle inputs. for (input : reactor.allInputs) { - if (federate === null || federate.contains(input as Port)) { - createTriggerT(body, input, triggerMap, constructorCode) - } + createTriggerT(body, input, triggerMap, constructorCode) } } @@ -2504,14 +2515,14 @@ class CGenerator extends GeneratorBase { * @param reactionIndex The position of the reaction within the reactor. */ def generateReaction(Reaction reaction, ReactorDecl decl, int reactionIndex) { - val functionName = reactionFunctionName(decl, reactionIndex) + val functionName = CReactionGenerator.generateReactionFunctionName(decl, reactionIndex) code.pr('void ' + functionName + '(void* instance_args) {') code.indent() var body = reaction.code.toText - generateInitializationForReaction(body, reaction, decl, reactionIndex) + code.pr(CReactionGenerator.generateInitializationForReaction(body, reaction, decl, reactionIndex, types, errorReporter, mainDef, isFederatedAndDecentralized, target.requiresTypes)) // Code verbatim from 'reaction' code.prSourceLineNumber(reaction.code) @@ -2527,7 +2538,7 @@ class CGenerator extends GeneratorBase { code.pr('void ' + lateFunctionName + '(void* instance_args) {') code.indent(); - generateInitializationForReaction(body, reaction, decl, reactionIndex) + code.pr(CReactionGenerator.generateInitializationForReaction(body, reaction, decl, reactionIndex, types, errorReporter, mainDef, isFederatedAndDecentralized, target.requiresTypes)) // Code verbatim from 'late' code.prSourceLineNumber(reaction.stp.code) code.pr(reaction.stp.code.toText) @@ -2538,11 +2549,11 @@ class CGenerator extends GeneratorBase { // Now generate code for the deadline violation function, if there is one. if (reaction.deadline !== null) { // The following name has to match the choice in generateReactionInstances - val deadlineFunctionName = decl.name.toLowerCase + '_deadline_function' + reactionIndex + val deadlineFunctionName = CReactionGenerator.generateDeadlineFunctionName(decl, reactionIndex) code.pr('void ' + deadlineFunctionName + '(void* instance_args) {') code.indent(); - generateInitializationForReaction(body, reaction, decl, reactionIndex) + code.pr(CReactionGenerator.generateInitializationForReaction(body, reaction, decl, reactionIndex, types, errorReporter, mainDef, isFederatedAndDecentralized, target.requiresTypes)) // Code verbatim from 'deadline' code.prSourceLineNumber(reaction.deadline.code) code.pr(reaction.deadline.code.toText) @@ -2553,447 +2564,53 @@ class CGenerator extends GeneratorBase { /** * Record startup and shutdown reactions. - * @param reactions A list of reactions. + * @param instance A reactor instance. */ - private def void recordStartupAndShutdown(Iterable reactions) { + private def void recordStartupAndShutdown(ReactorInstance instance) { // For each reaction instance, allocate the arrays that will be used to // trigger downstream reactions. - for (reaction : reactions) { - val reactor = reaction.parent; - - val temp = new CodeBuilder(); - 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.isStartup) { - temp.pr(''' - _lf_startup_reactions[_lf_startup_reactions_count++] = &«reactionRef»; - ''') - startupReactionCount += currentFederate.numRuntimeInstances(reactor); - foundOne = true; - } else if (trigger.isShutdown) { - temp.pr(''' - _lf_shutdown_reactions[_lf_shutdown_reactions_count++] = &«reactionRef»; - ''') - foundOne = true; - shutdownReactionCount += currentFederate.numRuntimeInstances(reactor); - - if (targetConfig.tracing !== null) { - val description = getShortenedName(reactor) - val reactorRef = CUtil.reactorRef(reactor) - temp.pr(''' - _lf_register_trace_event(«reactorRef», &(«reactorRef»->_lf__shutdown), - trace_trigger, "«description».shutdown"); - ''') - } - } - } - if (foundOne) initializeTriggerObjects.pr(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 - * triggering inputs of the reaction. - * - * @param body The body of the reaction. Used to check for the DISABLE_REACTION_INITIALIZATION_MARKER. - * @param reaction The initialization code will be generated for this specific reaction - * @param decl The reactor that has the reaction - * @param reactionIndex The index of the reaction relative to other reactions in the reactor, starting from 0 - */ - def generateIntendedTagInheritence(String body, Reaction reaction, ReactorDecl decl, int reactionIndex) { - // Construct the intended_tag inheritance code to go into - // the body of the function. - var CodeBuilder intendedTagInheritenceCode = new CodeBuilder() - // Check if the coordination mode is decentralized and if the reaction has any effects to inherit the STP violation - if (isFederatedAndDecentralized && !reaction.effects.nullOrEmpty) { - intendedTagInheritenceCode.pr(''' - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wunused-variable" - if (self->_lf__reaction_«reactionIndex».is_STP_violated == true) { - ''') - intendedTagInheritenceCode.indent(); - intendedTagInheritenceCode.pr(''' - // The operations inside this if clause (if any exists) are expensive - // and must only be done if the reaction has unhandled STP violation. - // Otherwise, all intended_tag values are (NEVER, 0) by default. + for (reaction : instance.reactions) { + if (currentFederate.contains(reaction.getDefinition())) { + val reactor = reaction.parent; - // Inherited intended tag. This will take the minimum - // intended_tag of all input triggers - «types.getTargetTagType» inherited_min_intended_tag = («types.getTargetTagType») { .time = FOREVER, .microstep = UINT_MAX }; - ''') - intendedTagInheritenceCode.pr(''' - // Find the minimum intended tag - ''') - // Go through every trigger of the reaction and check the - // value of intended_tag to choose the minimum. - for (TriggerRef inputTrigger : reaction.triggers ?: emptyList) { - if (inputTrigger instanceof VarRef) { - if (inputTrigger.variable instanceof Output) { - // Output from a contained reactor - val outputPort = inputTrigger.variable as Output - if (JavaAstUtils.isMultiport(outputPort)) { - intendedTagInheritenceCode.pr(''' - 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, - inherited_min_intended_tag) < 0) { - inherited_min_intended_tag = «inputTrigger.container.name».«inputTrigger.variable.name»[i]->intended_tag; - } - } - ''') + val temp = new CodeBuilder(); + var foundOne = false; + + val reactionRef = CUtil.reactionRef(reaction) - } else - intendedTagInheritenceCode.pr(''' - if (compare_tags(«inputTrigger.container.name».«inputTrigger.variable.name»->intended_tag, - inherited_min_intended_tag) < 0) { - inherited_min_intended_tag = «inputTrigger.container.name».«inputTrigger.variable.name»->intended_tag; - } - ''') - } else if (inputTrigger.variable instanceof Port) { - // Input port - val inputPort = inputTrigger.variable as Port - if (JavaAstUtils.isMultiport(inputPort)) { - intendedTagInheritenceCode.pr(''' - for (int i=0; i < «inputTrigger.variable.name»_width; i++) { - if (compare_tags(«inputTrigger.variable.name»[i]->intended_tag, inherited_min_intended_tag) < 0) { - inherited_min_intended_tag = «inputTrigger.variable.name»[i]->intended_tag; - } - } - ''') - } else { - intendedTagInheritenceCode.pr(''' - if (compare_tags(«inputTrigger.variable.name»->intended_tag, inherited_min_intended_tag) < 0) { - inherited_min_intended_tag = «inputTrigger.variable.name»->intended_tag; - } - ''') - } - } else if (inputTrigger.variable instanceof Action) { - intendedTagInheritenceCode.pr(''' - if (compare_tags(«inputTrigger.variable.name»->trigger->intended_tag, inherited_min_intended_tag) < 0) { - inherited_min_intended_tag = «inputTrigger.variable.name»->trigger->intended_tag; - } + // 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.isStartup) { + temp.pr(''' + _lf_startup_reactions[_lf_startup_reactions_count++] = &«reactionRef»; ''') - } - - } - } - if (reaction.triggers === null || reaction.triggers.size === 0) { - // No triggers are given, which means the reaction would react to any input. - // We need to check the intended tag for every input. - // NOTE: this does not include contained outputs. - for (input : (reaction.eContainer as Reactor).inputs) { - intendedTagInheritenceCode.pr(''' - if (compare_tags(«input.name»->intended_tag, inherited_min_intended_tag) > 0) { - inherited_min_intended_tag = «input.name»->intended_tag; - } - ''') - } - } - - // Once the minimum intended tag has been found, - // it will be passed down to the port effects - // of the reaction. Note that the intended tag - // will not pass on to actions downstream. - // Last reaction that sets the intended tag for the effect - // will be seen. - intendedTagInheritenceCode.pr(''' - // All effects inherit the minimum intended tag of input triggers - if (inherited_min_intended_tag.time != NEVER) { - ''') - intendedTagInheritenceCode.indent(); - for (effect : reaction.effects ?: emptyList) { - if (effect.variable instanceof Input) { - if (JavaAstUtils.isMultiport(effect.variable as Port)) { - intendedTagInheritenceCode.pr(''' - 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; - } + startupReactionCount += currentFederate.numRuntimeInstances(reactor); + foundOne = true; + } else if (trigger.isShutdown) { + temp.pr(''' + _lf_shutdown_reactions[_lf_shutdown_reactions_count++] = &«reactionRef»; ''') - } else { - if (effect.container.widthSpec !== null) { - // Contained reactor is a bank. - intendedTagInheritenceCode.pr(''' - for (int bankIndex = 0; bankIndex < self->_lf_«effect.container.name»_width; bankIndex++) { - «effect.container.name»[bankIndex].«effect.variable.name» = &(self->_lf_«effect.container.name»[bankIndex].«effect.variable.name»); - } + foundOne = true; + shutdownReactionCount += currentFederate.numRuntimeInstances(reactor); + + if (targetConfig.tracing !== null) { + val description = getShortenedName(reactor) + val reactorRef = CUtil.reactorRef(reactor) + temp.pr(''' + _lf_register_trace_event(«reactorRef», &(«reactorRef»->_lf__shutdown), + trace_trigger, "«description».shutdown"); ''') - } else { - // Input to a contained reaction - intendedTagInheritenceCode.pr(''' - // Don't reset the intended tag of the output port if it has already been set. - «effect.container.name».«effect.variable.name»->intended_tag = inherited_min_intended_tag; - ''') } - } - } - } - intendedTagInheritenceCode.unindent() - intendedTagInheritenceCode.pr(''' - } - ''') - intendedTagInheritenceCode.unindent() - intendedTagInheritenceCode.pr(''' - } - #pragma GCC diagnostic pop - ''') - - // Write the the intended tag inheritance initialization - // to the main code. - code.pr(intendedTagInheritenceCode.toString) - } - return intendedTagInheritenceCode - } - - /** - * Generate necessary initialization code inside the body of the reaction that belongs to reactor decl. - * @param body The body of the reaction. Used to check for the DISABLE_REACTION_INITIALIZATION_MARKER. - * @param reaction The initialization code will be generated for this specific reaction - * @param decl The reactor that has the reaction - * @param reactionIndex The index of the reaction relative to other reactions in the reactor, starting from 0 - */ - def generateInitializationForReaction(String body, Reaction reaction, ReactorDecl decl, int reactionIndex) { - val reactor = decl.toDefinition - - // Construct the reactionInitialization code to go into - // the body of the function before the verbatim code. - var CodeBuilder reactionInitialization = new CodeBuilder() - - // Define the "self" struct. - 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) { - code.pr(''' - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wunused-variable" - «structType»* self = («structType»*)instance_args; - ''') - } - - // Do not generate the initialization code if the body is marked - // to not generate it. - if (body.startsWith(CGenerator.DISABLE_REACTION_INITIALIZATION_MARKER)) { - code.pr(''' - #pragma GCC diagnostic pop - ''') - return; - } - - // A reaction may send to or receive from multiple ports of - // a contained reactor. The variables for these ports need to - // all be declared as fields of the same struct. Hence, we first - // collect the fields to be defined in the structs and then - // generate the structs. - var fieldsForStructsForContainedReactors = new LinkedHashMap - - // Actions may appear twice, first as a trigger, then with the outputs. - // But we need to declare it only once. Collect in this data structure - // the actions that are declared as triggered so that if they appear - // again with the outputs, they are not defined a second time. - // That second redefinition would trigger a compile error. - var actionsAsTriggers = new LinkedHashSet(); - - // Next, add the triggers (input and actions; timers are not needed). - // This defines a local variable in the reaction function whose - // name matches that of the trigger. The value of the local variable - // is a struct with a value and is_present field, the latter a boolean - // that indicates whether the input/action is present. - // If the trigger is an output, then it is an output of a - // contained reactor. In this case, a struct with the name - // of the contained reactor is created with one field that is - // a pointer to a struct with a value and is_present field. - // E.g., if the contained reactor is named 'c' and its output - // port is named 'out', then c.out->value c.out->is_present are - // defined so that they can be used in the verbatim code. - for (TriggerRef trigger : reaction.triggers ?: emptyList) { - if (trigger instanceof VarRef) { - if (trigger.variable instanceof Port) { - generatePortVariablesInReaction( - reactionInitialization, - fieldsForStructsForContainedReactors, - trigger, - decl) - } else if (trigger.variable instanceof Action) { - generateActionVariablesInReaction( - reactionInitialization, - trigger.variable as Action, - decl - ) - actionsAsTriggers.add(trigger.variable as Action); - } - } - } - if (reaction.triggers === null || reaction.triggers.size === 0) { - // No triggers are given, which means react to any input. - // Declare an argument for every input. - // NOTE: this does not include contained outputs. - for (input : reactor.inputs) { - generateInputVariablesInReaction(reactionInitialization, input, decl) - } - } - // Define argument for non-triggering inputs. - for (VarRef src : reaction.sources ?: emptyList) { - if (src.variable instanceof Port) { - generatePortVariablesInReaction(reactionInitialization, fieldsForStructsForContainedReactors, src, decl) - } else if (src.variable instanceof Action) { - // It's a bit odd to read but not be triggered by an action, but - // OK, I guess we allow it. - generateActionVariablesInReaction( - reactionInitialization, - src.variable as Action, - decl - ) - actionsAsTriggers.add(src.variable as Action); - } - } - - // Define variables for each declared output or action. - // In the case of outputs, the variable is a pointer to where the - // output is stored. This gives the reaction code access to any previous - // value that may have been written to that output in an earlier reaction. - if (reaction.effects !== null) { - for (effect : reaction.effects) { - if (effect.variable instanceof Action) { - // It is an action, not an output. - // If it has already appeared as trigger, do not redefine it. - if (!actionsAsTriggers.contains(effect.variable)) { - reactionInitialization.pr(''' - «variableStructType(effect.variable, decl)»* «effect.variable.name» = &self->_lf_«effect.variable.name»; - ''') - } - } else { - if (effect.variable instanceof Output) { - generateOutputVariablesInReaction( - reactionInitialization, - effect, - decl - ) - } else if (effect.variable instanceof Input) { - // It is the input of a contained reactor. - generateVariablesForSendingToContainedReactors( - reactionInitialization, - fieldsForStructsForContainedReactors, - effect.container, - effect.variable as Input - ) - } else { - errorReporter.reportError( - reaction, - "In generateReaction(): effect is neither an input nor an output." - ) } } + if (foundOne) initializeTriggerObjects.pr(temp.toString); } } - // Before the reaction initialization, - // generate the structs used for communication to and from contained reactors. - for (containedReactor : fieldsForStructsForContainedReactors.keySet) { - var array = ""; - if (containedReactor.widthSpec !== null) { - code.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)»]'''; - } - code.pr(''' - struct «containedReactor.name» { - «fieldsForStructsForContainedReactors.get(containedReactor)» - } «containedReactor.name»«array»; - ''') - } - // Next generate all the collected setup code. - code.pr(reactionInitialization.toString) - code.pr(''' - #pragma GCC diagnostic pop - ''') - - if (reaction.stp === null) { - // Pass down the intended_tag to all input and output effects - // downstream if the current reaction does not have a STP - // handler. - generateIntendedTagInheritence(body, reaction, decl, reactionIndex) - } - } - - /** - * Return the maximum bank width for the given instantiation within all - * instantiations of its parent reactor. - * On the first call to this method, the breadcrumbs should be null and the max - * argument should be zero. On recursive calls, breadcrumbs is a list of nested - * instantiations, the max is the maximum width found so far. The search for - * instances of the parent reactor will begin with the last instantiation - * in the specified list. - * - * This rather complicated method is used when a reaction sends or receives data - * to or from a bank of contained reactors. There will be an array of structs on - * the self struct of the parent, and the size of the array is conservatively set - * to the maximum of all the identified bank widths. This is a bit wasteful of - * memory, but it avoids having to malloc the array for each instance, and in - * typical usage, there will be few instances or instances that are all the same - * width. - * - * @param containedReactor The contained reactor instantiation. - * @param breadcrumbs null on first call (non-recursive). - * @param max 0 on first call. - */ - private def int maxContainedReactorBankWidth( - Instantiation containedReactor, - LinkedList breadcrumbs, - int max - ) { - // If the instantiation is not a bank, return 1. - if (containedReactor.widthSpec === null) { - return 1 - } - // If there is no main, then we just use the default width. - if (mainDef === null) { - return ASTUtils.width(containedReactor.widthSpec, null) - } - var nestedBreadcrumbs = breadcrumbs - if (nestedBreadcrumbs === null) { - nestedBreadcrumbs = new LinkedList - nestedBreadcrumbs.add(mainDef) - } - var result = max - var parent = containedReactor.eContainer as Reactor - if (parent == mainDef.reactorClass.toDefinition) { - // The parent is main, so there can't be any other instantiations of it. - return ASTUtils.width(containedReactor.widthSpec, null) - } - // Search for instances of the parent within the tail of the breadcrumbs list. - val container = nestedBreadcrumbs.first.reactorClass.toDefinition - for (instantiation: container.instantiations) { - // Put this new instantiation at the head of the list. - nestedBreadcrumbs.add(0, instantiation) - if (instantiation.reactorClass.toDefinition == parent) { - // Found a matching instantiation of the parent. - // Evaluate the original width specification in this context. - val candidate = ASTUtils.width(containedReactor.widthSpec, nestedBreadcrumbs) - if (candidate > result) { - result = candidate - } - } else { - // Found some other instantiation, not the parent. - // Search within it for instantiations of the parent. - // Note that we assume here that the parent cannot contain - // instances of itself. - val candidate = maxContainedReactorBankWidth(containedReactor, nestedBreadcrumbs, result) - if (candidate > result) { - result = candidate - } - } - nestedBreadcrumbs.remove - } - return result } + + /** * 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 @@ -3012,7 +2629,7 @@ class CGenerator extends GeneratorBase { startScopedBlock(temp, child, true); for (input : child.inputs) { - if (isTokenType((input.definition as Input).inferredType)) { + if (CUtil.isTokenType((input.definition as Input).inferredType, types)) { foundOne = true; val portRef = CUtil.portRefName(input); if (input.isMultiport()) { @@ -3113,7 +2730,7 @@ class CGenerator extends GeneratorBase { if (port.isOutput && !portsSeen.contains(port)) { portsSeen.add(port) // This reaction is receiving data from the port. - if (isTokenType((port.definition as Output).inferredType)) { + if (CUtil.isTokenType((port.definition as Output).inferredType, types)) { foundOne = true; temp.pr(''' @@ -3216,55 +2833,58 @@ class CGenerator extends GeneratorBase { } /** - * For each action given, generate initialization code for the offset - * and period fields. - * - * @param actions The actions. + * For each action of the specified reactor instance, generate initialization code + * for the offset and period fields. + * @param instance The reactor. */ - private def generateActionInitializations(Iterable actions) { - for (action : actions) { - if (!action.isShutdown) { - val triggerStructName = CUtil.reactorRef(action.parent) + "->_lf__" + action.name; - var minDelay = action.minDelay - var minSpacing = action.minSpacing - initializeTriggerObjects.pr(''' - // Initializing action «action.fullName» - «triggerStructName».offset = «minDelay.timeInTargetLanguage»; - «IF minSpacing !== null» - «triggerStructName».period = «minSpacing.timeInTargetLanguage»; - «ELSE» - «triggerStructName».period = «CGenerator.UNDEFINED_MIN_SPACING»; - «ENDIF» - ''') + private def generateActionInitializations(ReactorInstance instance) { + for (action : instance.actions) { + if (currentFederate.contains(action.definition)) { + if (!action.isShutdown) { + val triggerStructName = CUtil.reactorRef(action.parent) + "->_lf__" + action.name; + var minDelay = action.minDelay + var minSpacing = action.minSpacing + initializeTriggerObjects.pr(''' + // Initializing action «action.fullName» + «triggerStructName».offset = «minDelay.timeInTargetLanguage»; + «IF minSpacing !== null» + «triggerStructName».period = «minSpacing.timeInTargetLanguage»; + «ELSE» + «triggerStructName».period = «CGenerator.UNDEFINED_MIN_SPACING»; + «ENDIF» + ''') + } + triggerCount += currentFederate.numRuntimeInstances(action.parent); } - triggerCount += currentFederate.numRuntimeInstances(action.parent); } } /** - * For each timer given, generate initialization code for the offset + * For each timer in the given reactor, generate initialization code for the offset * and period fields. * * This method will also populate the global _lf_timer_triggers array, which is * used to start all timers at the start of execution. * - * @param timers The timers. + * @param instance A reactor instance. */ - private def generateTimerInitializations(Iterable timers) { - for (timer : timers) { - if (!timer.isStartup) { - val triggerStructName = CUtil.reactorRef(timer.parent) + "->_lf__" + timer.name; - val offset = timer.offset.timeInTargetLanguage - val period = timer.period.timeInTargetLanguage - initializeTriggerObjects.pr(''' - // Initializing timer «timer.fullName». - «triggerStructName».offset = «offset»; - «triggerStructName».period = «period»; - _lf_timer_triggers[_lf_timer_triggers_count++] = &«triggerStructName»; - ''') - timerCount += currentFederate.numRuntimeInstances(timer.parent); + private def generateTimerInitializations(ReactorInstance instance) { + for (timer : instance.timers) { + if (currentFederate.contains(timer.getDefinition())) { + if (!timer.isStartup) { + val triggerStructName = CUtil.reactorRef(timer.parent) + "->_lf__" + timer.name; + val offset = timer.offset.timeInTargetLanguage + val period = timer.period.timeInTargetLanguage + initializeTriggerObjects.pr(''' + // Initializing timer «timer.fullName». + «triggerStructName».offset = «offset»; + «triggerStructName».period = «period»; + _lf_timer_triggers[_lf_timer_triggers_count++] = &«triggerStructName»; + ''') + timerCount += currentFederate.numRuntimeInstances(timer.parent); + } + triggerCount += currentFederate.numRuntimeInstances(timer.parent); } - triggerCount += currentFederate.numRuntimeInstances(timer.parent); } } @@ -3303,25 +2923,10 @@ class CGenerator extends GeneratorBase { * Return a string that defines the log level. */ static def String defineLogLevel(GeneratorBase generator) { - // FIXME: if we align the levels with the ordinals of the - // enum (see CppGenerator), then we don't need this function. - switch(generator.targetConfig.logLevel) { - case ERROR: ''' - #define LOG_LEVEL 0 - ''' - case WARN: ''' - #define LOG_LEVEL 1 - ''' - case INFO: ''' - #define LOG_LEVEL 2 - ''' - case LOG: ''' - #define LOG_LEVEL 3 - ''' - case DEBUG: ''' - #define LOG_LEVEL 4 - ''' - } + generator.targetConfig.compileDefinitions.put("LOG_LEVEL" , generator.targetConfig.logLevel.ordinal.toString); + ''' + #define LOG_LEVEL «generator.targetConfig.logLevel.ordinal» + ''' } /** @@ -3348,17 +2953,6 @@ class CGenerator extends GeneratorBase { 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. - */ - static def reactionFunctionName(ReactorDecl reactor, int reactionIndex) { - reactor.name.toLowerCase + "reaction_function_" + reactionIndex - } /** * Generates C code to retrieve port->member @@ -3400,12 +2994,8 @@ class CGenerator extends GeneratorBase { * the full name of the specified reactor instance in the * trace table. If tracing is not turned on, do nothing. * @param instance The reactor instance. - * @param actions The actions of this reactor. - * @param timers The timers of this reactor. */ - private def void generateTraceTableEntries( - ReactorInstance instance, Iterable actions, Iterable timers - ) { + private def void generateTraceTableEntries(ReactorInstance instance) { // If tracing is turned on, record the address of this reaction // in the _lf_trace_object_descriptions table that is used to generate // the header information in the trace file. @@ -3415,82 +3005,21 @@ class CGenerator extends GeneratorBase { initializeTriggerObjects.pr(''' _lf_register_trace_event(«selfStruct», NULL, trace_reactor, "«description»"); ''') - for (action : actions) { - initializeTriggerObjects.pr(''' - _lf_register_trace_event(«selfStruct», &(«selfStruct»->_lf__«action.name»), trace_trigger, "«description».«action.name»"); - ''') - } - for (timer : timers) { - initializeTriggerObjects.pr(''' - _lf_register_trace_event(«selfStruct», &(«selfStruct»->_lf__«timer.name»), trace_trigger, "«description».«timer.name»"); - ''') + for (action : instance.actions) { + if (currentFederate.contains(action.getDefinition())) { + initializeTriggerObjects.pr(''' + _lf_register_trace_event(«selfStruct», &(«selfStruct»->_lf__«action.name»), trace_trigger, "«description».«action.name»"); + ''') + } } - } - } - - /** - * Generate code to instantiate the main reactor and any contained reactors - * that are in the current 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 currentFederate.contains(a.definition); - ]; - var reactionsInFederate = main.reactions.filter[ - r | return currentFederate.contains(r.definition); - ]; - var timersInFederate = main.timers.filter[ - 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); - - initializeTriggerObjects.pr( - '// ***** Start initializing ' + main.name) - - // Generate the self struct declaration for the top level. - initializeTriggerObjects.pr(''' - «CUtil.reactorRef(main)» = new_«main.name»(); - ''') - - // Generate code for top-level parameters, actions, timers, and reactions that - // are in the federate. - generateTraceTableEntries(main, actionsInFederate, timersInFederate); - generateReactorInstanceExtension(main, reactionsInFederate); - generateParameterInitialization(main); - - initializeTriggerObjects.pr(''' - 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); + for (timer : instance.timers) { + if (currentFederate.contains(timer.getDefinition())) { + initializeTriggerObjects.pr(''' + _lf_register_trace_event(«selfStruct», &(«selfStruct»->_lf__«timer.name»), trace_trigger, "«description».«timer.name»"); + ''') + } } } - - recordStartupAndShutdown(reactionsInFederate); - generateStateVariableInitializations(main); - generateTimerInitializations(timersInFederate); - generateActionInitializations(actionsInFederate); - generateInitializeActionToken(actionsInFederate); - generateSetDeadline(reactionsInFederate); - generateStartTimeStep(main); - - initializeTriggerObjects.pr("// ***** End initializing " + main.name); } /** @@ -3501,21 +3030,11 @@ class CGenerator extends GeneratorBase { * contained reactors or null if there are no federates. */ def void generateReactorInstance(ReactorInstance instance) { - // 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 initializeTriggerObjects.pr( '// ***** 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 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). initializeTriggerObjects.pr(''' @@ -3525,29 +3044,40 @@ class CGenerator extends GeneratorBase { // Generate code to initialize the "self" struct in the // _lf_initialize_trigger_objects function. - generateTraceTableEntries(instance, instance.actions, instance.timers) - generateReactorInstanceExtension(instance, instance.reactions) + generateTraceTableEntries(instance) + generateReactorInstanceExtension(instance) generateParameterInitialization(instance) initializeOutputMultiports(instance) initializeInputMultiports(instance) - recordStartupAndShutdown(instance.reactions); + recordStartupAndShutdown(instance); // Next, initialize the "self" struct with state variables. // These values may be expressions that refer to the parameter values defined above. generateStateVariableInitializations(instance); // Generate trigger objects for the instance. - generateTimerInitializations(instance.timers); - generateActionInitializations(instance.actions); + generateTimerInitializations(instance); + generateActionInitializations(instance); - generateInitializeActionToken(instance.actions); - generateSetDeadline(instance.reactions); + generateInitializeActionToken(instance); + generateSetDeadline(instance); // Recursively generate code for the children. for (child : instance.children) { - generateReactorInstance(child); + if (currentFederate.contains(child)) { + // If this reactor is a placeholder for a bank of reactors, then generate + // 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, child, true); + startScopedBlock(initializeTriggerObjects, child, true); + + generateReactorInstance(child); + + endScopedBlock(initializeTriggerObjects); + endScopedBlock(startTimeStep); + } } // If this program is federated with centralized coordination and this reactor @@ -3589,9 +3119,6 @@ class CGenerator extends GeneratorBase { // so that it can deallocate any memory. generateStartTimeStep(instance) - endScopedBlock(initializeTriggerObjects); - endScopedBlock(startTimeStep); - initializeTriggerObjects.pr("//***** End initializing " + fullName) } @@ -3600,18 +3127,19 @@ class CGenerator extends GeneratorBase { * This has the information required to allocate memory for the action payload. * Skip any action that is not actually used as a trigger. * @param reactor The reactor containing the actions. - * @param actions The actions. */ - private def void generateInitializeActionToken(Iterable actions) { - for (action : actions) { + private def void generateInitializeActionToken(ReactorInstance reactor) { + for (action : reactor.actions) { // Skip this step if the action is not in use. - if (action.parent.triggers.contains(action)) { + if (action.parent.triggers.contains(action) + && currentFederate.contains(action.definition) + ) { var type = action.definition.inferredType var payloadSize = "0" if (!type.isUndefined) { var String typeStr = types.getTargetType(type) - if (isTokenType(type)) { + if (CUtil.isTokenType(type, types)) { typeStr = typeStr.rootType } if (typeStr !== null && !typeStr.equals("") && !typeStr.equals("void")) { @@ -3652,10 +3180,7 @@ class CGenerator extends GeneratorBase { * @param instance The reactor instance. * @param reactions The reactions of this instance. */ - def void generateReactorInstanceExtension( - ReactorInstance instance, - Iterable reactions - ) { + def void generateReactorInstanceExtension(ReactorInstance instance) { // Do nothing } @@ -3699,12 +3224,15 @@ class CGenerator extends GeneratorBase { } /** - * Generate code to set the deadline field of the specified reactions. - * @param reactions The reactions. + * Generate code to set the deadline field of the reactions in the + * specified reactor instance. + * @param instance The reactor instance. */ - private def void generateSetDeadline(Iterable reactions) { - for (reaction : reactions) { - if (reaction.declaredDeadline !== null) { + private def void generateSetDeadline(ReactorInstance instance) { + for (reaction : instance.reactions) { + if (reaction.declaredDeadline !== null + && currentFederate.contains(reaction.getDefinition()) + ) { var deadline = reaction.declaredDeadline.maxDelay val selfRef = '''«CUtil.reactorRef(reaction.parent)»->_lf__reaction_«reaction.index»''' initializeTriggerObjects.pr(''' @@ -3729,7 +3257,7 @@ class CGenerator extends GeneratorBase { // 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); + val initializer = CParameterGenerator.getInitializer(parameter); if (initializer.startsWith("{")) { val temporaryVariableName = parameter.uniqueID initializeTriggerObjects.pr(''' @@ -3959,7 +3487,7 @@ class CGenerator extends GeneratorBase { val ref = JavaAstUtils.generateVarRef(port); // Note that the action.type set by the base class is actually // the port type. - if (action.inferredType.isTokenType) { + if (CUtil.isTokenType(action.inferredType, types)) { ''' if («ref»->is_present) { // Put the whole token on the event queue, not just the payload. @@ -3984,7 +3512,7 @@ class CGenerator extends GeneratorBase { */ override generateForwardBody(Action action, VarRef port) { val outputName = JavaAstUtils.generateVarRef(port) - if (action.inferredType.isTokenType) { + if (CUtil.isTokenType(action.inferredType, types)) { // 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. @@ -4069,7 +3597,7 @@ class CGenerator extends GeneratorBase { // NOTE: Docs say that malloc'd char* is freed on conclusion of the time step. // So passing it downstream should be OK. value = '''«action.name»->value'''; - if (isTokenType(type)) { + if (CUtil.isTokenType(type, types)) { result.append(''' SET_TOKEN(«receiveRef», «action.name»->token); ''') @@ -4085,7 +3613,7 @@ class CGenerator extends GeneratorBase { case SupportedSerializers.ROS2: { val portType = (receivingPort.variable as Port).inferredType var portTypeStr = types.getTargetType(portType) - if (isTokenType(portType)) { + if (CUtil.isTokenType(portType, types)) { throw new UnsupportedOperationException("Cannot handle ROS serialization when ports are pointers."); } else if (isSharedPtrType(portType)) { val matcher = sharedPointerVariable.matcher(portTypeStr) @@ -4165,11 +3693,7 @@ class CGenerator extends GeneratorBase { var String next_destination_name = '''"federate «receivingFed.id»"''' // Get the delay literal - var String additionalDelayString = - CGeneratorExtension.getNetworkDelayLiteral( - delay, - this - ); + var String additionalDelayString = CGeneratorExtension.getNetworkDelayLiteral(delay); if (isPhysical) { messageType = "MSG_TYPE_P2P_MESSAGE" @@ -4203,7 +3727,7 @@ class CGenerator extends GeneratorBase { switch (serializer) { case SupportedSerializers.NATIVE: { // Handle native types. - if (isTokenType(type)) { + if (CUtil.isTokenType(type, types)) { // NOTE: Transporting token types this way is likely to only work if the sender and receiver // both have the same endianness. Otherwise, you have to use protobufs or some other serialization scheme. result.append(''' @@ -4235,7 +3759,7 @@ class CGenerator extends GeneratorBase { case SupportedSerializers.ROS2: { var variableToSerialize = sendRef; var typeStr = types.getTargetType(type) - if (isTokenType(type)) { + if (CUtil.isTokenType(type, types)) { throw new UnsupportedOperationException("Cannot handle ROS serialization when ports are pointers."); } else if (isSharedPtrType(type)) { val matcher = sharedPointerVariable.matcher(typeStr) @@ -4329,11 +3853,7 @@ class CGenerator extends GeneratorBase { var sendRef = CUtil.portRefInReaction(port, sendingBankIndex, sendingChannelIndex); // Get the delay literal - var String additionalDelayString = - CGeneratorExtension.getNetworkDelayLiteral( - delay, - this - ); + var String additionalDelayString = CGeneratorExtension.getNetworkDelayLiteral(delay); result.append(''' // If the output port has not been SET for the current logical time, @@ -4402,57 +3922,30 @@ class CGenerator extends GeneratorBase { } /** - * Generate code that needs appear at the top of the generated + * Generate code that needs to appear at the top of the generated * C file, such as #define and #include statements. */ def generatePreamble() { code.pr(this.defineLogLevel) if (isFederated) { - // FIXME: Instead of checking - // #ifdef FEDERATED, we could - // use #if (NUMBER_OF_FEDERATES > 1) - // To me, the former is more accurate. - code.pr(''' - #define FEDERATED - ''') - if (targetConfig.coordination === CoordinationType.CENTRALIZED) { - // The coordination is centralized. - code.pr(''' - #define FEDERATED_CENTRALIZED - ''') - } else if (targetConfig.coordination === CoordinationType.DECENTRALIZED) { - // The coordination is decentralized - code.pr(''' - #define FEDERATED_DECENTRALIZED - ''') - } - + code.pr(CPreambleGenerator.generateFederatedDirective(targetConfig.coordination)) // 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; - } - } + targetConfig.threads = CUtil.minThreadsToHandleInputPorts(federates) } includeTargetLanguageHeaders() - - code.pr('#define NUMBER_OF_FEDERATES ' + federates.size); - + code.pr(CPreambleGenerator.generateNumFederatesDirective(federates.size)); code.pr('#define TARGET_FILES_DIRECTORY "' + fileConfig.srcGenPath + '"'); - + if (targetConfig.coordinationOptions.advance_message_interval !== null) { code.pr('#define ADVANCE_MESSAGE_INTERVAL ' + targetConfig.coordinationOptions.advance_message_interval.timeInTargetLanguage) } includeTargetLanguageSourceFiles() - code.pr("#include \"core/mixed_radix.h\""); + code.pr(CPreambleGenerator.generateMixedRadixIncludeHeader()); // Do this after the above includes so that the preamble can // call built-in functions. @@ -4537,7 +4030,8 @@ class CGenerator extends GeneratorBase { /** Add necessary source files specific to the target language. */ protected def includeTargetLanguageSourceFiles() { if (targetConfig.threads > 0) { - code.pr("#include \"core/reactor_threaded.c\"") + code.pr("#include \"core/threaded/reactor_threaded.c\"") + code.pr("#include \"core/threaded/scheduler.h\"") } else { code.pr("#include \"core/reactor.c\"") } @@ -5072,374 +4566,10 @@ class CGenerator extends GeneratorBase { } } - /** Generate action variables for a reaction. - * @param builder Where to write the code. - * @param action The action. - * @param reactor The reactor. - */ - private def generateActionVariablesInReaction( - CodeBuilder builder, - Action action, - ReactorDecl decl - ) { - val structType = variableStructType(action, decl) - // If the action has a type, create variables for accessing the value. - val type = action.inferredType - // Pointer to the lf_token_t sent as the payload in the trigger. - val tokenPointer = '''(self->_lf__«action.name».token)''' - builder.pr(action, ''' - // Expose the action struct as a local variable whose name matches the action name. - «structType»* «action.name» = &self->_lf_«action.name»; - // Set the fields of the action struct to match the current trigger. - «action.name»->is_present = (bool)self->_lf__«action.name».status; - «action.name»->has_value = («tokenPointer» != NULL && «tokenPointer»->value != NULL); - «action.name»->token = «tokenPointer»; - ''') - // Set the value field only if there is a type. - if (!type.isUndefined) { - // The value field will either be a copy (for primitive types) - // or a pointer (for types ending in *). - builder.pr(action, ''' - if («action.name»->has_value) { - «IF type.isTokenType» - «action.name»->value = («types.getTargetType(type)»)«tokenPointer»->value; - «ELSE» - «action.name»->value = *(«types.getTargetType(type)»*)«tokenPointer»->value; - «ENDIF» - } - ''') - } - } - - /** Generate into the specified string builder the code to - * initialize local variables for the specified input port - * in a reaction function from the "self" struct. - * @param builder The string builder. - * @param input The input statement from the AST. - * @param reactor The reactor. - */ - private def generateInputVariablesInReaction( - CodeBuilder builder, - Input input, - ReactorDecl decl - ) { - val structType = variableStructType(input, decl) - val inputType = input.inferredType - - // Create the local variable whose name matches the input name. - // If the input has not been declared mutable, then this is a pointer - // to the upstream output. Otherwise, it is a copy of the upstream output, - // which nevertheless points to the same token and value (hence, as done - // below, we have to use writable_copy()). There are 8 cases, - // 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 && !JavaAstUtils.isMultiport(input)) { - // Non-mutable, non-multiport, primitive type. - builder.pr(''' - «structType»* «input.name» = self->_lf_«input.name»; - ''') - } else if (input.isMutable && !inputType.isTokenType && !JavaAstUtils.isMultiport(input)) { - // Mutable, non-multiport, primitive type. - builder.pr(''' - // Mutable input, so copy the input into a temporary variable. - // The input value on the struct is a copy. - «structType» _lf_tmp_«input.name» = *(self->_lf_«input.name»); - «structType»* «input.name» = &_lf_tmp_«input.name»; - ''') - } else if (!input.isMutable && inputType.isTokenType && !JavaAstUtils.isMultiport(input)) { - // Non-mutable, non-multiport, token type. - builder.pr(''' - «structType»* «input.name» = self->_lf_«input.name»; - if («input.name»->is_present) { - «input.name»->length = «input.name»->token->length; - «input.name»->value = («types.getTargetType(inputType)»)«input.name»->token->value; - } else { - «input.name»->length = 0; - } - ''') - } else if (input.isMutable && inputType.isTokenType && !JavaAstUtils.isMultiport(input)) { - // Mutable, non-multiport, token type. - builder.pr(''' - // Mutable input, so copy the input struct into a temporary variable. - «structType» _lf_tmp_«input.name» = *(self->_lf_«input.name»); - «structType»* «input.name» = &_lf_tmp_«input.name»; - if («input.name»->is_present) { - «input.name»->length = «input.name»->token->length; - lf_token_t* _lf_input_token = «input.name»->token; - «input.name»->token = writable_copy(_lf_input_token); - if («input.name»->token != _lf_input_token) { - // A copy of the input token has been made. - // This needs to be reference counted. - «input.name»->token->ref_count = 1; - // Repurpose the next_free pointer on the token to add to the list. - «input.name»->token->next_free = _lf_more_tokens_with_ref_count; - _lf_more_tokens_with_ref_count = «input.name»->token; - } - «input.name»->value = («types.getTargetType(inputType)»)«input.name»->token->value; - } else { - «input.name»->length = 0; - } - ''') - } else if (!input.isMutable && JavaAstUtils.isMultiport(input)) { - // Non-mutable, multiport, primitive or token type. - builder.pr(''' - «structType»** «input.name» = self->_lf_«input.name»; - ''') - } else if (inputType.isTokenType) { - // Mutable, multiport, token type - builder.pr(''' - // Mutable multiport input, so copy the input structs - // into an array of temporary variables on the stack. - «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. - if («input.name»[i]->is_present) { - «input.name»[i]->length = «input.name»[i]->token->length; - lf_token_t* _lf_input_token = «input.name»[i]->token; - «input.name»[i]->token = writable_copy(_lf_input_token); - if («input.name»[i]->token != _lf_input_token) { - // A copy of the input token has been made. - // This needs to be reference counted. - «input.name»[i]->token->ref_count = 1; - // Repurpose the next_free pointer on the token to add to the list. - «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 = («types.getTargetType(inputType)»)«input.name»[i]->token->value; - } else { - «input.name»[i]->length = 0; - } - } - ''') - } else { - // Mutable, multiport, primitive type - builder.pr(''' - // Mutable multiport input, so copy the input structs - // into an array of temporary variables on the stack. - «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]); - } - ''') - } - // Set the _width variable for all cases. This will be -1 - // for a variable-width multiport, which is not currently supported. - // It will be -2 if it is not multiport. - builder.pr(''' - int «input.name»_width = self->_lf_«input.name»_width; - ''') - } - - /** - * Generate into the specified string builder the code to - * initialize local variables for ports in a reaction function - * from the "self" struct. The port may be an input of the - * reactor or an output of a contained reactor. The second - * argument provides, for each contained reactor, a place to - * write the declaration of the output of that reactor that - * is triggering reactions. - * @param builder The place into which to write the code. - * @param structs A map from reactor instantiations to a place to write - * struct fields. - * @param port The port. - * @param reactor The reactor or import statement. - */ - private def generatePortVariablesInReaction( - CodeBuilder builder, - LinkedHashMap structs, - VarRef port, - ReactorDecl decl - ) { - if (port.variable instanceof Input) { - generateInputVariablesInReaction(builder, port.variable as Input, decl) - } else { - // port is an output of a contained reactor. - val output = port.variable as Output - val portStructType = variableStructType(output, port.container.reactorClass) - - var structBuilder = structs.get(port.container) - if (structBuilder === null) { - structBuilder = new CodeBuilder() - structs.put(port.container, structBuilder) - } - val reactorName = port.container.name - // First define the struct containing the output value and indicator - // of its presence. - if (!JavaAstUtils.isMultiport(output)) { - // Output is not a multiport. - structBuilder.pr(''' - «portStructType»* «output.name»; - ''') - } else { - // Output is a multiport. - structBuilder.pr(''' - «portStructType»** «output.name»; - int «output.name»_width; - ''') - } - - // Next, initialize the struct with the current values. - if (port.container.widthSpec !== null) { - // Output is in a bank. - builder.pr(''' - for (int i = 0; i < «port.container.name»_width; i++) { - «reactorName»[i].«output.name» = self->_lf_«reactorName»[i].«output.name»; - } - ''') - if (JavaAstUtils.isMultiport(output)) { - builder.pr(''' - for (int i = 0; i < «port.container.name»_width; i++) { - «reactorName»[i].«output.name»_width = self->_lf_«reactorName»[i].«output.name»_width; - } - ''') - } - } else { - // Output is not in a bank. - builder.pr(''' - «reactorName».«output.name» = self->_lf_«reactorName».«output.name»; - ''') - if (JavaAstUtils.isMultiport(output)) { - builder.pr(''' - «reactorName».«output.name»_width = self->_lf_«reactorName».«output.name»_width; - ''') - } - } - } - } - - /** - * Generate into the specified string builder the code to - * initialize local variables for outputs in a reaction function - * from the "self" struct. - * @param builder The string builder. - * @param effect The effect declared by the reaction. This must refer to an output. - * @param decl The reactor containing the reaction or the import statement. - */ - private def generateOutputVariablesInReaction( - CodeBuilder builder, - VarRef effect, - ReactorDecl decl - ) { - val output = effect.variable as Output - if (output.type === null && target.requiresTypes === true) { - errorReporter.reportError(output, "Output is required to have a type: " + output.name) - } else { - // The container of the output may be a contained reactor or - // the reactor containing the reaction. - val outputStructType = (effect.container === null) ? - variableStructType(output, decl) - : - variableStructType(output, effect.container.reactorClass) - if (!JavaAstUtils.isMultiport(output)) { - // Output port is not a multiport. - builder.pr(''' - «outputStructType»* «output.name» = &self->_lf_«output.name»; - ''') - } else { - // Output port is a multiport. - // Set the _width variable. - builder.pr(''' - int «output.name»_width = self->_lf_«output.name»_width; - ''') - builder.pr(''' - «outputStructType»** «output.name» = self->_lf_«output.name»_pointers; - ''') - } - } - } - - /** - * Generate into the specified string builder the code to - * initialize local variables for sending data to an input - * of a contained reactor. This will also, if necessary, - * generate entries for local struct definitions into the - * struct argument. These entries point to where the data - * is stored. - * - * @param builder The string builder. - * @param structs A map from reactor instantiations to a place to write - * struct fields. - * @param definition AST node defining the reactor within which this occurs - * @param input Input of the contained reactor. - */ - private def generateVariablesForSendingToContainedReactors( - CodeBuilder builder, - LinkedHashMap structs, - Instantiation definition, - Input input - ) { - var structBuilder = structs.get(definition) - if (structBuilder === null) { - structBuilder = new CodeBuilder() - structs.put(definition, structBuilder) - } - val inputStructType = variableStructType(input, definition.reactorClass) - if (!JavaAstUtils.isMultiport(input)) { - // Contained reactor's input is not a multiport. - structBuilder.pr(''' - «inputStructType»* «input.name»; - ''') - if (definition.widthSpec !== null) { - // Contained reactor is a bank. - builder.pr(''' - for (int bankIndex = 0; bankIndex < self->_lf_«definition.name»_width; bankIndex++) { - «definition.name»[bankIndex].«input.name» = &(self->_lf_«definition.name»[bankIndex].«input.name»); - } - ''') - } else { - // Contained reactor is not a bank. - builder.pr(''' - «definition.name».«input.name» = &(self->_lf_«definition.name».«input.name»); - ''') - } - } else { - // Contained reactor's input is a multiport. - structBuilder.pr(''' - «inputStructType»** «input.name»; - int «input.name»_width; - ''') - // If the contained reactor is a bank, then we have to set the - // pointer for each element of the bank. - if (definition.widthSpec !== null) { - builder.pr(''' - for (int _i = 0; _i < self->_lf_«definition.name»_width; _i++) { - «definition.name»[_i].«input.name» = self->_lf_«definition.name»[_i].«input.name»; - «definition.name»[_i].«input.name»_width = self->_lf_«definition.name»[_i].«input.name»_width; - } - ''') - } else { - builder.pr(''' - «definition.name».«input.name» = self->_lf_«definition.name».«input.name»; - «definition.name».«input.name»_width = self->_lf_«definition.name».«input.name»_width; - ''') - } - } - } protected def isSharedPtrType(InferredType type) { return !type.isUndefined && sharedPointerVariable.matcher(types.getTargetType(type)).find() } - - /** - * Given a type for an input or output, return true if it should be - * carried by a lf_token_t struct rather than the type itself. - * It should be carried by such a struct if the type ends with * - * (it is a pointer) or [] (it is a array with unspecified length). - * @param type The type specification. - */ - protected def isTokenType(InferredType type) { - if (type.isUndefined) return 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.trim.endsWith("*") - } /** If the type specification of the form {@code type[]}, * {@code type*}, or {@code type}, return the type. @@ -5503,63 +4633,43 @@ class CGenerator extends GeneratorBase { } } + override generateDelayGeneric() { + throw new UnsupportedOperationException("TODO: auto-generated method stub") + } + + //////////////////////////////////////////////////////////// + //// Private methods + /** - * 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. + * If a main or federted reactor has been declared, create a ReactorInstance + * for this top level. This will also assign levels to reactions, then, + * if the program is federated, perform an AST transformation to disconnect + * connections between federates. */ - 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) + private def void createMainReactorInstance() { + if (this.mainDef !== null) { + if (this.main === null) { + // Recursively build instances. This is done once because + // it is the same for all federates. + this.main = new ReactorInstance(mainDef.reactorClass.toDefinition, errorReporter, + this.unorderedReactions) + if (this.main.assignLevels().nodeCount > 0) { + errorReporter.reportError("Main reactor has causality cycles. Skipping code generation."); + return; } - } - } 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) + // Force reconstruction of dependence information. + if (isFederated) { + // Avoid compile errors by removing disconnected network ports. + // This must be done after assigning levels. + removeRemoteFederateConnectionPorts(main); + // There will be AST transformations that invalidate some info + // cached in ReactorInstance. + this.main.clearCaches(false); } - } - } - if (list.size == 1) { - return list.get(0) - } else { - return list.join('{', ', ', '}', [it]) + } } } - - override generateDelayGeneric() { - throw new UnsupportedOperationException("TODO: auto-generated method stub") - } - + /** * Perform initialization functions that must be performed after * all reactor runtime instances have been created. @@ -5679,7 +4789,7 @@ class CGenerator extends GeneratorBase { // Look for outputs with token types. for (output : reactor.outputs) { val type = (output.definition as Output).inferredType; - if (type.isTokenType) { + if (CUtil.isTokenType(type, types)) { // 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; @@ -6427,6 +5537,9 @@ class CGenerator extends GeneratorBase { /** The main place to put generated code. */ protected var code = new CodeBuilder(); + /** The current federate for which we are generating code. */ + protected var currentFederate = null as FederateInstance; + /** Place to collect code to initialize the trigger objects for all reactor instances. */ protected var initializeTriggerObjects = new CodeBuilder() @@ -6461,7 +5574,4 @@ class CGenerator extends GeneratorBase { var boolean CCppMode = false; var CTypes types; - - /** The current federate for which we are generating code. */ - var currentFederate = null as FederateInstance; } diff --git a/org.lflang/src/org/lflang/generator/c/CParameterGenerator.java b/org.lflang/src/org/lflang/generator/c/CParameterGenerator.java new file mode 100644 index 0000000000..56b11c9c5b --- /dev/null +++ b/org.lflang/src/org/lflang/generator/c/CParameterGenerator.java @@ -0,0 +1,64 @@ +package org.lflang.generator.c; + +import java.util.LinkedList; +import java.util.List; +import org.lflang.generator.ParameterInstance; +import org.lflang.JavaAstUtils; +import org.lflang.generator.GeneratorBase; +import org.lflang.lf.Assignment; +import org.lflang.lf.Value; + +public class CParameterGenerator { + /** + * 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. + */ + public static String getInitializer(ParameterInstance p) { + // Handle the bank_index parameter. + if (p.getName().equals("bank_index")) { + return CUtil.bankIndex(p.getParent()); + } + + // Handle overrides in the intantiation. + // In case there is more than one assignment to this parameter, we need to + // find the last one. + Assignment lastAssignment = null; + for (Assignment assignment: p.getParent().getDefinition().getParameters()) { + if (assignment.getLhs() == p.getDefinition()) { + lastAssignment = assignment; + } + } + List list = new LinkedList<>(); + if (lastAssignment != null) { + // The parameter has an assignment. + // Right hand side can be a list. Collect the entries. + for (Value value: lastAssignment.getRhs()) { + if (value.getParameter() != 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.getParent().getParent()) + "->" + value.getParameter().getName()); + } else { + list.add(GeneratorBase.getTargetTime(value)); + } + } + } else { + // there was no assignment in the instantiation. So just use the + // parameter's initial value. + for (Value i : p.getParent().initialParameterValue(p.getDefinition())) { + if (JavaAstUtils.isOfTimeType(p.getDefinition())) { + list.add(GeneratorBase.getTargetTime(i)); + } else { + list.add(GeneratorBase.getTargetTime(i)); + } + } + } + if (list.size() == 1) { + return list.get(0); + } else { + return "{" + String.join(", ", list) + "}"; + } + } +} diff --git a/org.lflang/src/org/lflang/generator/c/CPreambleGenerator.java b/org.lflang/src/org/lflang/generator/c/CPreambleGenerator.java new file mode 100644 index 0000000000..8d41f94bec --- /dev/null +++ b/org.lflang/src/org/lflang/generator/c/CPreambleGenerator.java @@ -0,0 +1,33 @@ +package org.lflang.generator.c; + +import java.util.ArrayList; +import java.util.List; +import org.lflang.TargetProperty.CoordinationType; + +public class CPreambleGenerator { + /** + * Returns the #define directive for the given coordination type. + * + * NOTE: Instead of checking #ifdef FEDERATED, we could + * use #if (NUMBER_OF_FEDERATES > 1). + * To Soroush Bateni, the former is more accurate. + */ + public static String generateFederatedDirective(CoordinationType coordinationType) { + List directives = new ArrayList<>(); + directives.add("#define FEDERATED"); + if (coordinationType == CoordinationType.CENTRALIZED) { + directives.add("#define FEDERATED_CENTRALIZED"); + } else if (coordinationType == CoordinationType.DECENTRALIZED) { + directives.add("#define FEDERATED_DECENTRALIZED"); + } + return String.join("\n", directives); + } + + public static String generateMixedRadixIncludeHeader() { + return "#include \"core/mixed_radix.h\""; + } + + public static String generateNumFederatesDirective(int numFederates) { + return "#define NUMBER_OF_FEDERATES " + numFederates; + } +} diff --git a/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java b/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java new file mode 100644 index 0000000000..e64c787ca3 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/c/CReactionGenerator.java @@ -0,0 +1,803 @@ +package org.lflang.generator.c; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +import org.lflang.ASTUtils; +import org.lflang.ErrorReporter; +import org.lflang.InferredType; +import org.lflang.JavaAstUtils; +import org.lflang.generator.CodeBuilder; +import org.lflang.lf.Action; +import org.lflang.lf.Input; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Output; +import org.lflang.lf.Port; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.TriggerRef; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; + +import static org.lflang.generator.c.CUtil.generateWidthVariable; + +public class CReactionGenerator { + + /** + * Generate necessary initialization code inside the body of the reaction that belongs to reactor decl. + * @param body The body of the reaction. Used to check for the DISABLE_REACTION_INITIALIZATION_MARKER. + * @param reaction The initialization code will be generated for this specific reaction + * @param decl The reactor that has the reaction + * @param reactionIndex The index of the reaction relative to other reactions in the reactor, starting from 0 + */ + public static String generateInitializationForReaction(String body, + Reaction reaction, + ReactorDecl decl, + int reactionIndex, + CTypes types, + ErrorReporter errorReporter, + Instantiation mainDef, + boolean isFederatedAndDecentralized, + boolean requiresTypes) { + Reactor reactor = ASTUtils.toDefinition(decl); + + // Construct the reactionInitialization code to go into + // the body of the function before the verbatim code. + CodeBuilder reactionInitialization = new CodeBuilder(); + + CodeBuilder code = new CodeBuilder(); + + // Define the "self" struct. + String structType = CUtil.selfType(decl); + // A null structType means there are no inputs, state, + // or anything else. No need to declare it. + if (structType != null) { + code.pr(String.join("\n", + "#pragma GCC diagnostic push", + "#pragma GCC diagnostic ignored \"-Wunused-variable\"", + structType+"* self = ("+structType+"*)instance_args;" + )); + } + + // Do not generate the initialization code if the body is marked + // to not generate it. + if (body.startsWith(CGenerator.DISABLE_REACTION_INITIALIZATION_MARKER)) { + code.pr("#pragma GCC diagnostic pop"); + return code.toString(); + } + + // A reaction may send to or receive from multiple ports of + // a contained reactor. The variables for these ports need to + // all be declared as fields of the same struct. Hence, we first + // collect the fields to be defined in the structs and then + // generate the structs. + Map fieldsForStructsForContainedReactors = new LinkedHashMap<>(); + + // Actions may appear twice, first as a trigger, then with the outputs. + // But we need to declare it only once. Collect in this data structure + // the actions that are declared as triggered so that if they appear + // again with the outputs, they are not defined a second time. + // That second redefinition would trigger a compile error. + Set actionsAsTriggers = new LinkedHashSet<>(); + + // Next, add the triggers (input and actions; timers are not needed). + // This defines a local variable in the reaction function whose + // name matches that of the trigger. The value of the local variable + // is a struct with a value and is_present field, the latter a boolean + // that indicates whether the input/action is present. + // If the trigger is an output, then it is an output of a + // contained reactor. In this case, a struct with the name + // of the contained reactor is created with one field that is + // a pointer to a struct with a value and is_present field. + // E.g., if the contained reactor is named 'c' and its output + // port is named 'out', then c.out->value c.out->is_present are + // defined so that they can be used in the verbatim code. + for (TriggerRef trigger : ASTUtils.convertToEmptyListIfNull(reaction.getTriggers())) { + if (trigger instanceof VarRef) { + VarRef triggerAsVarRef = (VarRef) trigger; + if (triggerAsVarRef.getVariable() instanceof Port) { + generatePortVariablesInReaction( + reactionInitialization, + fieldsForStructsForContainedReactors, + triggerAsVarRef, + decl, + types); + } else if (triggerAsVarRef.getVariable() instanceof Action) { + reactionInitialization.pr(generateActionVariablesInReaction( + (Action) triggerAsVarRef.getVariable(), + decl, + types + )); + actionsAsTriggers.add((Action) triggerAsVarRef.getVariable()); + } + } + } + if (reaction.getTriggers() == null || reaction.getTriggers().size() == 0) { + // No triggers are given, which means react to any input. + // Declare an argument for every input. + // NOTE: this does not include contained outputs. + for (Input input : reactor.getInputs()) { + reactionInitialization.pr(generateInputVariablesInReaction(input, decl, types)); + } + } + // Define argument for non-triggering inputs. + for (VarRef src : ASTUtils.convertToEmptyListIfNull(reaction.getSources())) { + if (src.getVariable() instanceof Port) { + generatePortVariablesInReaction(reactionInitialization, fieldsForStructsForContainedReactors, src, decl, types); + } else if (src.getVariable() instanceof Action) { + // It's a bit odd to read but not be triggered by an action, but + // OK, I guess we allow it. + reactionInitialization.pr(generateActionVariablesInReaction( + (Action) src.getVariable(), + decl, + types + )); + actionsAsTriggers.add((Action) src.getVariable()); + } + } + + // Define variables for each declared output or action. + // In the case of outputs, the variable is a pointer to where the + // output is stored. This gives the reaction code access to any previous + // value that may have been written to that output in an earlier reaction. + if (reaction.getEffects() != null) { + for (VarRef effect : reaction.getEffects()) { + Variable variable = effect.getVariable(); + if (variable instanceof Action) { + // It is an action, not an output. + // If it has already appeared as trigger, do not redefine it. + if (!actionsAsTriggers.contains(effect.getVariable())) { + reactionInitialization.pr(CGenerator.variableStructType(variable, decl)+"* "+variable.getName()+" = &self->_lf_"+variable.getName()+";"); + } + } else { + if (variable instanceof Output) { + reactionInitialization.pr(generateOutputVariablesInReaction( + effect, + decl, + errorReporter, + requiresTypes + )); + } else if (variable instanceof Input) { + // It is the input of a contained reactor. + generateVariablesForSendingToContainedReactors( + reactionInitialization, + fieldsForStructsForContainedReactors, + effect.getContainer(), + (Input) variable + ); + } else { + errorReporter.reportError( + reaction, + "In generateReaction(): effect is neither an input nor an output." + ); + } + } + } + } + // Before the reaction initialization, + // generate the structs used for communication to and from contained reactors. + for (Instantiation containedReactor : fieldsForStructsForContainedReactors.keySet()) { + String array = ""; + if (containedReactor.getWidthSpec() != null) { + String containedReactorWidthVar = generateWidthVariable(containedReactor.getName()); + code.pr("int "+containedReactorWidthVar+" = self->_lf_"+containedReactorWidthVar+";"); + // 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, mainDef)+"]"; + } + code.pr(String.join("\n", + "struct "+containedReactor.getName()+" {", + " "+fieldsForStructsForContainedReactors.get(containedReactor)+"", + "} "+containedReactor.getName()+array+";" + )); + } + // Next generate all the collected setup code. + code.pr(reactionInitialization.toString()); + code.pr("#pragma GCC diagnostic pop"); + + if (reaction.getStp() == null) { + // Pass down the intended_tag to all input and output effects + // downstream if the current reaction does not have a STP + // handler. + code.pr(generateIntendedTagInheritence(body, reaction, decl, reactionIndex, types, isFederatedAndDecentralized)); + } + return code.toString(); + } + + /** + * Return the maximum bank width for the given instantiation within all + * instantiations of its parent reactor. + * On the first call to this method, the breadcrumbs should be null and the max + * argument should be zero. On recursive calls, breadcrumbs is a list of nested + * instantiations, the max is the maximum width found so far. The search for + * instances of the parent reactor will begin with the last instantiation + * in the specified list. + * + * This rather complicated method is used when a reaction sends or receives data + * to or from a bank of contained reactors. There will be an array of structs on + * the self struct of the parent, and the size of the array is conservatively set + * to the maximum of all the identified bank widths. This is a bit wasteful of + * memory, but it avoids having to malloc the array for each instance, and in + * typical usage, there will be few instances or instances that are all the same + * width. + * + * @param containedReactor The contained reactor instantiation. + * @param breadcrumbs null on first call (non-recursive). + * @param max 0 on first call. + */ + public static int maxContainedReactorBankWidth( + Instantiation containedReactor, + LinkedList breadcrumbs, + int max, + Instantiation mainDef + ) { + // If the instantiation is not a bank, return 1. + if (containedReactor.getWidthSpec() == null) { + return 1; + } + // If there is no main, then we just use the default width. + if (mainDef == null) { + return ASTUtils.width(containedReactor.getWidthSpec(), null); + } + LinkedList nestedBreadcrumbs = breadcrumbs; + if (nestedBreadcrumbs == null) { + nestedBreadcrumbs = new LinkedList<>(); + nestedBreadcrumbs.add(mainDef); + } + int result = max; + Reactor parent = (Reactor) containedReactor.eContainer(); + if (parent == ASTUtils.toDefinition(mainDef.getReactorClass())) { + // The parent is main, so there can't be any other instantiations of it. + return ASTUtils.width(containedReactor.getWidthSpec(), null); + } + // Search for instances of the parent within the tail of the breadcrumbs list. + Reactor container = ASTUtils.toDefinition(nestedBreadcrumbs.get(0).getReactorClass()); + for (Instantiation instantiation : container.getInstantiations()) { + // Put this new instantiation at the head of the list. + nestedBreadcrumbs.add(0, instantiation); + if (ASTUtils.toDefinition(instantiation.getReactorClass()) == parent) { + // Found a matching instantiation of the parent. + // Evaluate the original width specification in this context. + int candidate = ASTUtils.width(containedReactor.getWidthSpec(), nestedBreadcrumbs); + if (candidate > result) { + result = candidate; + } + } else { + // Found some other instantiation, not the parent. + // Search within it for instantiations of the parent. + // Note that we assume here that the parent cannot contain + // instances of itself. + int candidate = maxContainedReactorBankWidth(containedReactor, nestedBreadcrumbs, result, mainDef); + if (candidate > result) { + result = candidate; + } + } + nestedBreadcrumbs.remove(); + } + return result; + } + + /** + * Generate code that passes existing intended tag to all output ports + * and actions. This intended tag is the minimum intended tag of the + * triggering inputs of the reaction. + * + * @param body The body of the reaction. Used to check for the DISABLE_REACTION_INITIALIZATION_MARKER. + * @param reaction The initialization code will be generated for this specific reaction + * @param decl The reactor that has the reaction + * @param reactionIndex The index of the reaction relative to other reactions in the reactor, starting from 0 + */ + public static String generateIntendedTagInheritence(String body, Reaction reaction, ReactorDecl decl, int reactionIndex, CTypes types, boolean isFederatedAndDecentralized) { + // Construct the intended_tag inheritance code to go into + // the body of the function. + CodeBuilder intendedTagInheritenceCode = new CodeBuilder(); + // Check if the coordination mode is decentralized and if the reaction has any effects to inherit the STP violation + if (isFederatedAndDecentralized && !(reaction.getEffects() == null || reaction.getEffects().isEmpty())) { + intendedTagInheritenceCode.pr(String.join("\n", + "#pragma GCC diagnostic push", + "#pragma GCC diagnostic ignored \"-Wunused-variable\"", + "if (self->_lf__reaction_"+reactionIndex+".is_STP_violated == true) {" + )); + intendedTagInheritenceCode.indent(); + intendedTagInheritenceCode.pr(String.join("\n", + "// The operations inside this if clause (if any exists) are expensive ", + "// and must only be done if the reaction has unhandled STP violation.", + "// Otherwise, all intended_tag values are (NEVER, 0) by default.", + "", + "// Inherited intended tag. This will take the minimum", + "// intended_tag of all input triggers", + types.getTargetTagType()+" inherited_min_intended_tag = ("+types.getTargetTagType()+") { .time = FOREVER, .microstep = UINT_MAX };" + )); + intendedTagInheritenceCode.pr("// Find the minimum intended tag"); + // Go through every trigger of the reaction and check the + // value of intended_tag to choose the minimum. + for (TriggerRef inputTrigger : ASTUtils.convertToEmptyListIfNull(reaction.getTriggers())) { + if (inputTrigger instanceof VarRef) { + VarRef inputTriggerAsVarRef = (VarRef) inputTrigger; + Variable variable = inputTriggerAsVarRef.getVariable(); + String variableName = inputTriggerAsVarRef.getVariable().getName(); + if (variable instanceof Output) { + // Output from a contained reactor + String containerName = inputTriggerAsVarRef.getContainer().getName(); + Output outputPort = (Output) variable; + if (JavaAstUtils.isMultiport(outputPort)) { + intendedTagInheritenceCode.pr(String.join("\n", + "for (int i=0; i < "+containerName+"."+generateWidthVariable(variableName)+"; i++) {", + " if (compare_tags("+containerName+"."+variableName+"[i]->intended_tag,", + " inherited_min_intended_tag) < 0) {", + " inherited_min_intended_tag = "+containerName+"."+variableName+"[i]->intended_tag;", + " }", + "}" + )); + } else + intendedTagInheritenceCode.pr(String.join("\n", + "if (compare_tags("+containerName+"."+variableName+"->intended_tag,", + " inherited_min_intended_tag) < 0) {", + " inherited_min_intended_tag = "+containerName+"."+variableName+"->intended_tag;", + "}" + )); + } else if (variable instanceof Port) { + // Input port + Port inputPort = (Port) variable; + if (JavaAstUtils.isMultiport(inputPort)) { + intendedTagInheritenceCode.pr(String.join("\n", + "for (int i=0; i < "+generateWidthVariable(variableName)+"; i++) {", + " if (compare_tags("+variableName+"[i]->intended_tag, inherited_min_intended_tag) < 0) {", + " inherited_min_intended_tag = "+variableName+"[i]->intended_tag;", + " }", + "}" + )); + } else { + intendedTagInheritenceCode.pr(String.join("\n", + "if (compare_tags("+variableName+"->intended_tag, inherited_min_intended_tag) < 0) {", + " inherited_min_intended_tag = "+variableName+"->intended_tag;", + "}" + )); + } + } else if (variable instanceof Action) { + intendedTagInheritenceCode.pr(String.join("\n", + "if (compare_tags("+variableName+"->trigger->intended_tag, inherited_min_intended_tag) < 0) {", + " inherited_min_intended_tag = "+variableName+"->trigger->intended_tag;", + "}" + )); + } + + } + } + if (reaction.getTriggers() == null || reaction.getTriggers().size() == 0) { + // No triggers are given, which means the reaction would react to any input. + // We need to check the intended tag for every input. + // NOTE: this does not include contained outputs. + for (Input input : ((Reactor) reaction.eContainer()).getInputs()) { + intendedTagInheritenceCode.pr(String.join("\n", + "if (compare_tags("+input.getName()+"->intended_tag, inherited_min_intended_tag) > 0) {", + " inherited_min_intended_tag = "+input.getName()+"->intended_tag;", + "}" + )); + } + } + + // Once the minimum intended tag has been found, + // it will be passed down to the port effects + // of the reaction. Note that the intended tag + // will not pass on to actions downstream. + // Last reaction that sets the intended tag for the effect + // will be seen. + intendedTagInheritenceCode.pr(String.join("\n", + "// All effects inherit the minimum intended tag of input triggers", + "if (inherited_min_intended_tag.time != NEVER) {" + )); + intendedTagInheritenceCode.indent(); + for (VarRef effect : ASTUtils.convertToEmptyListIfNull(reaction.getEffects())) { + Variable effectVar = effect.getVariable(); + Instantiation effContainer = effect.getContainer(); + if (effectVar instanceof Input) { + if (JavaAstUtils.isMultiport((Port) effectVar)) { + intendedTagInheritenceCode.pr(String.join("\n", + "for(int i=0; i < "+effContainer.getName()+"."+generateWidthVariable(effectVar.getName())+"; i++) {", + " "+effContainer.getName()+"."+effectVar.getName()+"[i]->intended_tag = inherited_min_intended_tag;", + "}" + )); + } else { + if (effContainer.getWidthSpec() != null) { + // Contained reactor is a bank. + intendedTagInheritenceCode.pr(String.join("\n", + "for (int bankIndex = 0; bankIndex < self->_lf_"+generateWidthVariable(effContainer.getName())+"; bankIndex++) {", + " "+effContainer.getName()+"[bankIndex]."+effectVar.getName()+" = &(self->_lf_"+effContainer.getName()+"[bankIndex]."+effectVar.getName()+");", + "}" + )); + } else { + // Input to a contained reaction + intendedTagInheritenceCode.pr(String.join("\n", + "// Don't reset the intended tag of the output port if it has already been set.", + effContainer.getName()+"."+effectVar.getName()+"->intended_tag = inherited_min_intended_tag;" + )); + } + } + } + } + intendedTagInheritenceCode.unindent(); + intendedTagInheritenceCode.pr("}"); + intendedTagInheritenceCode.unindent(); + intendedTagInheritenceCode.pr("#pragma GCC diagnostic pop"); + intendedTagInheritenceCode.pr("}"); + + } + return intendedTagInheritenceCode.toString(); + } + + /** + * Generate into the specified string builder the code to + * initialize local variables for sending data to an input + * of a contained reactor. This will also, if necessary, + * generate entries for local struct definitions into the + * struct argument. These entries point to where the data + * is stored. + * + * @param builder The string builder. + * @param structs A map from reactor instantiations to a place to write + * struct fields. + * @param definition AST node defining the reactor within which this occurs + * @param input Input of the contained reactor. + */ + private static void generateVariablesForSendingToContainedReactors( + CodeBuilder builder, + Map structs, + Instantiation definition, + Input input + ) { + CodeBuilder structBuilder = structs.get(definition); + if (structBuilder == null) { + structBuilder = new CodeBuilder(); + structs.put(definition, structBuilder); + } + String inputStructType = CGenerator.variableStructType(input, definition.getReactorClass()).toString(); + String defName = definition.getName(); + String defWidth = generateWidthVariable(defName); + String inputName = input.getName(); + String inputWidth = generateWidthVariable(inputName); + if (!JavaAstUtils.isMultiport(input)) { + // Contained reactor's input is not a multiport. + structBuilder.pr(inputStructType+"* "+inputName+";"); + if (definition.getWidthSpec() != null) { + // Contained reactor is a bank. + builder.pr(String.join("\n", + "for (int bankIndex = 0; bankIndex < self->_lf_"+defWidth+"; bankIndex++) {", + " "+defName+"[bankIndex]."+inputName+" = &(self->_lf_"+defName+"[bankIndex]."+inputName+");", + "}" + )); + } else { + // Contained reactor is not a bank. + builder.pr(defName+"."+inputName+" = &(self->_lf_"+defName+"."+inputName+");"); + } + } else { + // Contained reactor's input is a multiport. + structBuilder.pr(String.join("\n", + inputStructType+"** "+inputName+";", + "int "+inputWidth+";" + )); + // If the contained reactor is a bank, then we have to set the + // pointer for each element of the bank. + if (definition.getWidthSpec() != null) { + builder.pr(String.join("\n", + "for (int _i = 0; _i < self->_lf_"+defWidth+"; _i++) {", + " "+defName+"[_i]."+inputName+" = self->_lf_"+defName+"[_i]."+inputName+";", + " "+defName+"[_i]."+inputWidth+" = self->_lf_"+defName+"[_i]."+inputWidth+";", + "}" + )); + } else { + builder.pr(String.join("\n", + defName+"."+inputName+" = self->_lf_"+defName+"."+inputName+";", + defName+"."+inputWidth+" = self->_lf_"+defName+"."+inputWidth+";" + )); + } + } + } + + /** + * Generate into the specified string builder the code to + * initialize local variables for ports in a reaction function + * from the "self" struct. The port may be an input of the + * reactor or an output of a contained reactor. The second + * argument provides, for each contained reactor, a place to + * write the declaration of the output of that reactor that + * is triggering reactions. + * @param builder The place into which to write the code. + * @param structs A map from reactor instantiations to a place to write + * struct fields. + * @param port The port. + * @param reactor The reactor or import statement. + */ + private static void generatePortVariablesInReaction( + CodeBuilder builder, + Map structs, + VarRef port, + ReactorDecl decl, + CTypes types + ) { + if (port.getVariable() instanceof Input) { + builder.pr(generateInputVariablesInReaction((Input) port.getVariable(), decl, types)); + } else { + // port is an output of a contained reactor. + Output output = (Output) port.getVariable(); + String portStructType = CGenerator.variableStructType(output, port.getContainer().getReactorClass()).toString(); + + CodeBuilder structBuilder = structs.get(port.getContainer()); + if (structBuilder == null) { + structBuilder = new CodeBuilder(); + structs.put(port.getContainer(), structBuilder); + } + String reactorName = port.getContainer().getName(); + String reactorWidth = generateWidthVariable(reactorName); + String outputName = output.getName(); + String outputWidth = generateWidthVariable(outputName); + // First define the struct containing the output value and indicator + // of its presence. + if (!JavaAstUtils.isMultiport(output)) { + // Output is not a multiport. + structBuilder.pr(portStructType+"* "+outputName+";"); + } else { + // Output is a multiport. + structBuilder.pr(String.join("\n", + portStructType+"** "+outputName+";", + "int "+outputWidth+";" + )); + } + + // Next, initialize the struct with the current values. + if (port.getContainer().getWidthSpec() != null) { + // Output is in a bank. + builder.pr(String.join("\n", + "for (int i = 0; i < "+reactorWidth+"; i++) {", + " "+reactorName+"[i]."+outputName+" = self->_lf_"+reactorName+"[i]."+outputName+";", + "}" + )); + if (JavaAstUtils.isMultiport(output)) { + builder.pr(String.join("\n", + "for (int i = 0; i < "+reactorWidth+"; i++) {", + " "+reactorName+"[i]."+outputWidth+" = self->_lf_"+reactorName+"[i]."+outputWidth+";", + "}" + )); + } + } else { + // Output is not in a bank. + builder.pr(reactorName+"."+outputName+" = self->_lf_"+reactorName+"."+outputName+";"); + if (JavaAstUtils.isMultiport(output)) { + builder.pr(reactorName+"."+outputWidth+" = self->_lf_"+reactorName+"."+outputWidth+";"); + } + } + } + } + + /** Generate action variables for a reaction. + * @param builder Where to write the code. + * @param action The action. + * @param reactor The reactor. + */ + private static String generateActionVariablesInReaction( + Action action, + ReactorDecl decl, + CTypes types + ) { + String structType = CGenerator.variableStructType(action, decl).toString(); + // If the action has a type, create variables for accessing the value. + InferredType type = JavaAstUtils.getInferredType(action); + // Pointer to the lf_token_t sent as the payload in the trigger. + String tokenPointer = "(self->_lf__"+action.getName()+".token)"; + CodeBuilder builder = new CodeBuilder(); + + builder.pr( + String.join("\n", + "// Expose the action struct as a local variable whose name matches the action name.", + structType+"* "+action.getName()+" = &self->_lf_"+action.getName()+";", + "// Set the fields of the action struct to match the current trigger.", + action.getName()+"->is_present = (bool)self->_lf__"+action.getName()+".status;", + action.getName()+"->has_value = ("+tokenPointer+" != NULL && "+tokenPointer+"->value != NULL);", + action.getName()+"->token = "+tokenPointer+";") + ); + // Set the value field only if there is a type. + if (!type.isUndefined()) { + // The value field will either be a copy (for primitive types) + // or a pointer (for types ending in *). + builder.pr("if ("+action.getName()+"->has_value) {"); + builder.indent(); + if (CUtil.isTokenType(type, types)) { + builder.pr(action.getName()+"->value = ("+types.getTargetType(type)+")"+tokenPointer+"->value;"); + } else { + builder.pr(action.getName()+"->value = *("+types.getTargetType(type)+"*)"+tokenPointer+"->value;"); + } + builder.unindent(); + builder.pr("}"); + } + return builder.toString(); + } + + /** Generate into the specified string builder the code to + * initialize local variables for the specified input port + * in a reaction function from the "self" struct. + * @param builder The string builder. + * @param input The input statement from the AST. + * @param reactor The reactor. + */ + private static String generateInputVariablesInReaction( + Input input, + ReactorDecl decl, + CTypes types + ) { + String structType = CGenerator.variableStructType(input, decl).toString(); + InferredType inputType = JavaAstUtils.getInferredType(input); + CodeBuilder builder = new CodeBuilder(); + String inputName = input.getName(); + String inputWidth = generateWidthVariable(inputName); + + // Create the local variable whose name matches the input name. + // If the input has not been declared mutable, then this is a pointer + // to the upstream output. Otherwise, it is a copy of the upstream output, + // which nevertheless points to the same token and value (hence, as done + // below, we have to use writable_copy()). There are 8 cases, + // 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() && !CUtil.isTokenType(inputType, types) && !JavaAstUtils.isMultiport(input)) { + // Non-mutable, non-multiport, primitive type. + builder.pr(structType+"* "+inputName+" = self->_lf_"+inputName+";"); + } else if (input.isMutable()&& !CUtil.isTokenType(inputType, types) && !JavaAstUtils.isMultiport(input)) { + // Mutable, non-multiport, primitive type. + builder.pr(String.join("\n", + "// Mutable input, so copy the input into a temporary variable.", + "// The input value on the struct is a copy.", + structType+" _lf_tmp_"+inputName+" = *(self->_lf_"+inputName+");", + structType+"* "+inputName+" = &_lf_tmp_"+inputName+";" + )); + } else if (!input.isMutable()&& CUtil.isTokenType(inputType, types) && !JavaAstUtils.isMultiport(input)) { + // Non-mutable, non-multiport, token type. + builder.pr(String.join("\n", + structType+"* "+inputName+" = self->_lf_"+inputName+";", + "if ("+inputName+"->is_present) {", + " "+inputName+"->length = "+inputName+"->token->length;", + " "+inputName+"->value = ("+types.getTargetType(inputType)+")"+inputName+"->token->value;", + "} else {", + " "+inputName+"->length = 0;", + "}" + )); + } else if (input.isMutable()&& CUtil.isTokenType(inputType, types) && !JavaAstUtils.isMultiport(input)) { + // Mutable, non-multiport, token type. + builder.pr(String.join("\n", + "// Mutable input, so copy the input struct into a temporary variable.", + structType+" _lf_tmp_"+inputName+" = *(self->_lf_"+inputName+");", + structType+"* "+inputName+" = &_lf_tmp_"+inputName+";", + "if ("+inputName+"->is_present) {", + " "+inputName+"->length = "+inputName+"->token->length;", + " lf_token_t* _lf_input_token = "+inputName+"->token;", + " "+inputName+"->token = writable_copy(_lf_input_token);", + " if ("+inputName+"->token != _lf_input_token) {", + " // A copy of the input token has been made.", + " // This needs to be reference counted.", + " "+inputName+"->token->ref_count = 1;", + " // Repurpose the next_free pointer on the token to add to the list.", + " "+inputName+"->token->next_free = _lf_more_tokens_with_ref_count;", + " _lf_more_tokens_with_ref_count = "+inputName+"->token;", + " }", + " "+inputName+"->value = ("+types.getTargetType(inputType)+")"+inputName+"->token->value;", + "} else {", + " "+inputName+"->length = 0;", + "}" + )); + } else if (!input.isMutable()&& JavaAstUtils.isMultiport(input)) { + // Non-mutable, multiport, primitive or token type. + builder.pr(structType+"** "+inputName+" = self->_lf_"+inputName+";"); + } else if (CUtil.isTokenType(inputType, types)) { + // Mutable, multiport, token type + builder.pr(String.join("\n", + "// Mutable multiport input, so copy the input structs", + "// into an array of temporary variables on the stack.", + structType+" _lf_tmp_"+inputName+"["+CUtil.multiportWidthExpression(input)+"];", + structType+"* "+inputName+"["+CUtil.multiportWidthExpression(input)+"];", + "for (int i = 0; i < "+CUtil.multiportWidthExpression(input)+"; i++) {", + " "+inputName+"[i] = &_lf_tmp_"+inputName+"[i];", + " _lf_tmp_"+inputName+"[i] = *(self->_lf_"+inputName+"[i]);", + " // If necessary, copy the tokens.", + " if ("+inputName+"[i]->is_present) {", + " "+inputName+"[i]->length = "+inputName+"[i]->token->length;", + " lf_token_t* _lf_input_token = "+inputName+"[i]->token;", + " "+inputName+"[i]->token = writable_copy(_lf_input_token);", + " if ("+inputName+"[i]->token != _lf_input_token) {", + " // A copy of the input token has been made.", + " // This needs to be reference counted.", + " "+inputName+"[i]->token->ref_count = 1;", + " // Repurpose the next_free pointer on the token to add to the list.", + " "+inputName+"[i]->token->next_free = _lf_more_tokens_with_ref_count;", + " _lf_more_tokens_with_ref_count = "+inputName+"[i]->token;", + " }", + " "+inputName+"[i]->value = ("+types.getTargetType(inputType)+")"+inputName+"[i]->token->value;", + " } else {", + " "+inputName+"[i]->length = 0;", + " }", + "}" + )); + } else { + // Mutable, multiport, primitive type + builder.pr(String.join("\n", + "// Mutable multiport input, so copy the input structs", + "// into an array of temporary variables on the stack.", + structType+" _lf_tmp_"+inputName+"["+CUtil.multiportWidthExpression(input)+"];", + structType+"* "+inputName+"["+CUtil.multiportWidthExpression(input)+"];", + "for (int i = 0; i < "+CUtil.multiportWidthExpression(input)+"; i++) {", + " "+inputName+"[i] = &_lf_tmp_"+inputName+"[i];", + " // Copy the struct, which includes the value.", + " _lf_tmp_"+inputName+"[i] = *(self->_lf_"+inputName+"[i]);", + "}" + )); + } + // Set the _width variable for all cases. This will be -1 + // for a variable-width multiport, which is not currently supported. + // It will be -2 if it is not multiport. + builder.pr("int "+inputWidth+" = self->_lf_"+inputWidth+";"); + return builder.toString(); + } + + /** + * Generate into the specified string builder the code to + * initialize local variables for outputs in a reaction function + * from the "self" struct. + * @param effect The effect declared by the reaction. This must refer to an output. + * @param decl The reactor containing the reaction or the import statement. + */ + public static String generateOutputVariablesInReaction( + VarRef effect, + ReactorDecl decl, + ErrorReporter errorReporter, + boolean requiresTypes + ) { + Output output = (Output) effect.getVariable(); + String outputName = output.getName(); + String outputWidth = generateWidthVariable(outputName); + if (output.getType() == null && requiresTypes) { + errorReporter.reportError(output, "Output is required to have a type: " + outputName); + return ""; + } else { + // The container of the output may be a contained reactor or + // the reactor containing the reaction. + String outputStructType = (effect.getContainer() == null) ? + CGenerator.variableStructType(output, decl).toString() + : + CGenerator.variableStructType(output, effect.getContainer().getReactorClass()).toString(); + if (!JavaAstUtils.isMultiport(output)) { + // Output port is not a multiport. + return outputStructType+"* "+outputName+" = &self->_lf_"+outputName+";"; + } else { + // Output port is a multiport. + // Set the _width variable. + return String.join("\n", + "int "+outputWidth+" = self->_lf_"+outputWidth+";", + outputStructType+"** "+outputName+" = self->_lf_"+outputName+"_pointers;" + ); + + } + } + } + + /** + * Returns the name of the deadline function for reaction. + * @param decl The reactor with the deadline + * @param index The number assigned to this reaction deadline + */ + public static String generateDeadlineFunctionName(ReactorDecl decl, int index) { + return decl.getName().toLowerCase() + "_deadline_function" + index; + } + + /** + * 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. + */ + public static String generateReactionFunctionName(ReactorDecl reactor, int reactionIndex) { + return reactor.getName().toLowerCase() + "reaction_function_" + reactionIndex; + } +} diff --git a/org.lflang/src/org/lflang/generator/c/CUtil.java b/org.lflang/src/org/lflang/generator/c/CUtil.java index 9e561cb8b6..0327aab4d2 100644 --- a/org.lflang/src/org/lflang/generator/c/CUtil.java +++ b/org.lflang/src/org/lflang/generator/c/CUtil.java @@ -34,8 +34,10 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.ErrorReporter; import org.lflang.FileConfig; +import org.lflang.InferredType; import org.lflang.TargetConfig; import org.lflang.TargetConfig.Mode; +import org.lflang.federated.FederateInstance; import org.lflang.generator.GeneratorCommandFactory; import org.lflang.generator.PortInstance; import org.lflang.generator.ReactionInstance; @@ -672,4 +674,38 @@ private static List multiportWidthTerms(Variable variable) { } return result; } + + /** + * Given a type for an input or output, return true if it should be + * carried by a lf_token_t struct rather than the type itself. + * It should be carried by such a struct if the type ends with * + * (it is a pointer) or [] (it is a array with unspecified length). + * @param type The type specification. + */ + public static boolean isTokenType(InferredType type, CTypes types) { + if (type.isUndefined()) return false; + // This is a hacky way to do this. It is now considered to be a bug (#657) + String targetType = types.getVariableDeclaration(type, "", false); + return type.isVariableSizeList || targetType.trim().endsWith("*"); + } + + /** + * 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. + * + * @param federates + * @return The minimum number of threads needed. + */ + public static int minThreadsToHandleInputPorts(List federates) { + int nthreads = 1; + for (FederateInstance federate : federates) { + nthreads = Math.max(nthreads, federate.networkMessageActions.size() + 1); + } + return nthreads; + } + + public static String generateWidthVariable(String var) { + return var + "_width"; + } } diff --git a/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt index 2c94013a82..7ff8043b5b 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt @@ -25,7 +25,6 @@ package org.lflang.generator.cpp import org.lflang.TargetConfig -import org.lflang.TargetProperty import org.lflang.generator.PrependOperator import org.lflang.toUnixString import java.nio.file.Path @@ -34,94 +33,93 @@ import java.nio.file.Path class CppCmakeGenerator(private val targetConfig: TargetConfig, private val fileConfig: CppFileConfig) { companion object { - const val includesVarName: String = "TARGET_INCLUDE_DIRECTORIES" + /** Return the name of the variable that gives the includes of the given target. */ + fun includesVarName(buildTargetName: String): String = "TARGET_INCLUDE_DIRECTORIES_$buildTargetName" const val compilerIdName: String = "CXX_COMPILER_ID" } - /** Convert a log level to a severity number understood by the reactor-cpp runtime. */ - private val TargetProperty.LogLevel.severity - get() = when (this) { - TargetProperty.LogLevel.ERROR -> 1 - TargetProperty.LogLevel.WARN -> 2 - TargetProperty.LogLevel.INFO -> 3 - TargetProperty.LogLevel.LOG -> 4 - TargetProperty.LogLevel.DEBUG -> 4 - } + @Suppress("PrivatePropertyName") // allows us to use capital S as variable name below + private val S = '$' // a little trick to escape the dollar sign with $S - fun generateCode(sources: List): String { - val runtimeVersion = targetConfig.runtimeVersion ?: CppGenerator.defaultRuntimeVersion + fun generateRootCmake(projectName: String): String { + return """ + |cmake_minimum_required(VERSION 3.5) + |project($projectName VERSION 0.0.0 LANGUAGES CXX) + | + |# require C++ 17 + |set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard is cached for visibility in external tools." FORCE) + |set(CMAKE_CXX_STANDARD_REQUIRED ON) + |set(CMAKE_CXX_EXTENSIONS OFF) + | + |# don't automatically build and install all targets + |set(CMAKE_SKIP_INSTALL_ALL_DEPENDENCY true) + | + |include($S{CMAKE_ROOT}/Modules/ExternalProject.cmake) + |include(GNUInstallDirs) + | + |set(DEFAULT_BUILD_TYPE "${targetConfig.cmakeBuildType}") + |if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + |set (CMAKE_BUILD_TYPE "$S{DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of build." FORCE) + |endif() + | + |if (APPLE) + | file(RELATIVE_PATH REL_LIB_PATH + | "$S{CMAKE_INSTALL_PREFIX}/$S{CMAKE_INSTALL_BINDIR}" + | "$S{CMAKE_INSTALL_PREFIX}/$S{CMAKE_INSTALL_LIBDIR}" + | ) + | set(CMAKE_INSTALL_RPATH "@executable_path/$S{REL_LIB_PATH}") + |else () + | set(CMAKE_INSTALL_RPATH "$S{CMAKE_INSTALL_PREFIX}/$S{CMAKE_INSTALL_LIBDIR}") + |endif () + | + |set(CMAKE_BUILD_WITH_INSTALL_RPATH ON) + | + |file(GLOB subdirs RELATIVE "$S{PROJECT_SOURCE_DIR}" "$S{PROJECT_SOURCE_DIR}/*") + |foreach(subdir $S{subdirs}) + | if(IS_DIRECTORY "$S{PROJECT_SOURCE_DIR}/$S{subdir}") + | if($S{subdir} MATCHES "reactor-cpp-.*") + | string(SUBSTRING $S{subdir} 12 -1 LF_REACTOR_CPP_SUFFIX) + | add_subdirectory("$S{subdir}") + | endif() + | endif() + |endforeach() + |foreach(subdir $S{subdirs}) + | if(IS_DIRECTORY "$S{PROJECT_SOURCE_DIR}/$S{subdir}") + | if(NOT $S{subdir} MATCHES "reactor-cpp-.*") + | add_subdirectory("$S{subdir}") + | endif() + | endif() + |endforeach() + """.trimMargin() + } + fun generateSubdirCmake(): String { + return """ + |file(GLOB subdirs RELATIVE "$S{CMAKE_CURRENT_SOURCE_DIR}" "$S{CMAKE_CURRENT_SOURCE_DIR}/*") + |foreach(subdir $S{subdirs}) + | if(IS_DIRECTORY "$S{CMAKE_CURRENT_SOURCE_DIR}/$S{subdir}") + | add_subdirectory("$S{subdir}") + | endif() + |endforeach() + """.trimMargin() + } + + fun generateCmake(sources: List): String { // Resolve path to the cmake include files if any was provided val includeFiles = targetConfig.cmakeIncludes?.map { fileConfig.srcPath.resolve(it).toUnixString() } - @Suppress("LocalVariableName") // allows us to use capital S as variable name below - val S = '$' // a little trick to escape the dollar sign with $S + val reactorCppTarget = when { + targetConfig.externalRuntimePath != null -> "reactor-cpp" + targetConfig.runtimeVersion != null -> "reactor-cpp-${targetConfig.runtimeVersion}" + else -> "reactor-cpp-default" + } return with(PrependOperator) { """ |cmake_minimum_required(VERSION 3.5) - |project(${fileConfig.name} VERSION 1.0.0 LANGUAGES CXX) - | - |# require C++ 17 - |set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard is cached for visibility in external tools." FORCE) - |set(CMAKE_CXX_STANDARD_REQUIRED ON) - |set(CMAKE_CXX_EXTENSIONS OFF) - | - |include($S{CMAKE_ROOT}/Modules/ExternalProject.cmake) - |include(GNUInstallDirs) - | - |set(DEFAULT_BUILD_TYPE "${targetConfig.cmakeBuildType}") - |if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - |set (CMAKE_BUILD_TYPE "$S{DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of build." FORCE) - |endif() - | - ${ - if (targetConfig.externalRuntimePath != null) """ - |find_package(reactor-cpp PATHS "${targetConfig.externalRuntimePath}") - """.trimIndent() else """ - |if(NOT REACTOR_CPP_BUILD_DIR) - | set(REACTOR_CPP_BUILD_DIR "" CACHE STRING "Choose the directory to build reactor-cpp in." FORCE) - |endif() - | - |ExternalProject_Add(dep-reactor-cpp - | PREFIX "$S{REACTOR_CPP_BUILD_DIR}" - | GIT_REPOSITORY "https://github.com/lf-lang/reactor-cpp.git" - | GIT_TAG "$runtimeVersion" - | GIT_CONFIG "remote.origin.fetch=+refs/pull/*:refs/remotes/origin/refs/pull/*" - | CMAKE_ARGS - | -DCMAKE_BUILD_TYPE:STRING=$S{CMAKE_BUILD_TYPE} - | -DCMAKE_INSTALL_PREFIX:PATH=$S{CMAKE_INSTALL_PREFIX} - | -DCMAKE_INSTALL_BINDIR:PATH=$S{CMAKE_INSTALL_BINDIR} - | -DCMAKE_CXX_COMPILER=$S{CMAKE_CXX_COMPILER} - | -DREACTOR_CPP_VALIDATE=${if (targetConfig.noRuntimeValidation) "OFF" else "ON"} - | -DREACTOR_CPP_TRACE=${if (targetConfig.tracing != null) "ON" else "OFF"} - | -DREACTOR_CPP_LOG_LEVEL=${targetConfig.logLevel.severity} - |) - | - |set(REACTOR_CPP_LIB_DIR "$S{CMAKE_INSTALL_PREFIX}/$S{CMAKE_INSTALL_LIBDIR}") - |set(REACTOR_CPP_BIN_DIR "$S{CMAKE_INSTALL_PREFIX}/$S{CMAKE_INSTALL_BINDIR}") - |set(REACTOR_CPP_LIB_NAME "$S{CMAKE_SHARED_LIBRARY_PREFIX}reactor-cpp$S{CMAKE_SHARED_LIBRARY_SUFFIX}") - |set(REACTOR_CPP_IMPLIB_NAME "$S{CMAKE_STATIC_LIBRARY_PREFIX}reactor-cpp$S{CMAKE_STATIC_LIBRARY_SUFFIX}") - | - |add_library(reactor-cpp SHARED IMPORTED) - |add_dependencies(reactor-cpp dep-reactor-cpp) - |if(WIN32) - | set_target_properties(reactor-cpp PROPERTIES IMPORTED_IMPLIB "$S{REACTOR_CPP_LIB_DIR}/$S{REACTOR_CPP_IMPLIB_NAME}") - | set_target_properties(reactor-cpp PROPERTIES IMPORTED_LOCATION "$S{REACTOR_CPP_BIN_DIR}/$S{REACTOR_CPP_LIB_NAME}") - |else() - | set_target_properties(reactor-cpp PROPERTIES IMPORTED_LOCATION "$S{REACTOR_CPP_LIB_DIR}/$S{REACTOR_CPP_LIB_NAME}") - |endif() - | - |if (APPLE) - | file(RELATIVE_PATH REL_LIB_PATH "$S{REACTOR_CPP_BIN_DIR}" "$S{REACTOR_CPP_LIB_DIR}") - | set(CMAKE_INSTALL_RPATH "@executable_path/$S{REL_LIB_PATH}") - |else () - | set(CMAKE_INSTALL_RPATH "$S{REACTOR_CPP_LIB_DIR}") - |endif () - """.trimIndent() - } + |project(${fileConfig.name} VERSION 0.0.0 LANGUAGES CXX) | - |set(CMAKE_BUILD_WITH_INSTALL_RPATH ON) + |${if (targetConfig.externalRuntimePath != null) "find_package(reactor-cpp PATHS ${targetConfig.externalRuntimePath})" else ""} | |set(LF_MAIN_TARGET ${fileConfig.name}) | @@ -133,7 +131,7 @@ class CppCmakeGenerator(private val targetConfig: TargetConfig, private val file | "$S{PROJECT_SOURCE_DIR}" | "$S{PROJECT_SOURCE_DIR}/__include__" |) - |target_link_libraries($S{LF_MAIN_TARGET} reactor-cpp) + |target_link_libraries($S{LF_MAIN_TARGET} $reactorCppTarget) | |if(MSVC) | target_compile_options($S{LF_MAIN_TARGET} PRIVATE /W4) @@ -143,16 +141,18 @@ class CppCmakeGenerator(private val targetConfig: TargetConfig, private val file | |install(TARGETS $S{LF_MAIN_TARGET} | RUNTIME DESTINATION $S{CMAKE_INSTALL_BINDIR} + | OPTIONAL |) | |# Cache a list of the include directories for use with tools external to CMake and Make. - |# FIXME: Find include directories for all targets given by sources -- not just main target? + |# This will only work if the subdirectory that sets up the library target has already been visited. |get_target_property(TARGET_INCLUDE_DIRECTORIES $S{LF_MAIN_TARGET} INCLUDE_DIRECTORIES) - |list(APPEND TARGET_INCLUDE_DIRECTORIES $S{SOURCE_DIR}/include) - |set(${includesVarName} $S{TARGET_INCLUDE_DIRECTORIES} CACHE STRING "Directories included in the main target." FORCE) - |set(${compilerIdName} $S{CMAKE_CXX_COMPILER_ID} CACHE STRING "The name of the C++ compiler." FORCE) + |get_target_property(REACTOR_CPP_INCLUDE_DIRECTORIES $reactorCppTarget INCLUDE_DIRECTORIES) + |list(APPEND TARGET_INCLUDE_DIRECTORIES $S{REACTOR_CPP_INCLUDE_DIRECTORIES}) + |set(${includesVarName(fileConfig.name)} $S{TARGET_INCLUDE_DIRECTORIES} CACHE STRING "Directories included in the main target." FORCE) + |set($compilerIdName $S{CMAKE_CXX_COMPILER_ID} CACHE STRING "The name of the C++ compiler." FORCE) | - ${" |"..(includeFiles?.joinToString("\n") { "include(\"$it\")" } ?: "") } + ${" |"..(includeFiles?.joinToString("\n") { "include(\"$it\")" } ?: "")} """.trimMargin() } } diff --git a/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt b/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt index 0888d5c982..5de842772b 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt @@ -122,7 +122,6 @@ fun fileComment(r: Resource) = """ * This file was autogenerated by the Lingua Franca Compiler. * * Source: ${r.uri} - * Date: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))} */ """.trimIndent() diff --git a/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt b/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt index 72775e6d80..56d131d5b5 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt @@ -72,5 +72,5 @@ class CppFileConfig(resource: Resource, srcGenBasePath: Path, context: LFGenerat fun getReactorSourcePath(r: Reactor): Path = getGenDir(r.eResource()).resolve("${r.name}.cc") /** Path to the build directory containing CMake-generated files */ - val buildPath: Path get() = outPath.resolve("build").resolve(name) + val buildPath: Path get() = outPath.resolve("build") } \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt index a0dc460f6d..1fadcb997b 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt @@ -27,11 +27,12 @@ package org.lflang.generator.cpp import org.eclipse.emf.ecore.resource.Resource -import org.eclipse.xtext.generator.IFileSystemAccess2 -import org.lflang.* -import org.lflang.TargetConfig.Mode +import org.lflang.ErrorReporter import org.lflang.Target -import org.lflang.generator.canGenerate +import org.lflang.TargetConfig.Mode +import org.lflang.TargetProperty +import org.lflang.TimeUnit +import org.lflang.TimeValue import org.lflang.generator.CodeMap import org.lflang.generator.GeneratorBase import org.lflang.generator.GeneratorResult @@ -39,14 +40,19 @@ import org.lflang.generator.IntegratedBuilder import org.lflang.generator.JavaGeneratorUtils import org.lflang.generator.LFGeneratorContext import org.lflang.generator.TargetTypes +import org.lflang.generator.canGenerate +import org.lflang.isGeneric import org.lflang.lf.Action import org.lflang.lf.VarRef import org.lflang.scoping.LFGlobalScopeProvider +import org.lflang.toDefinition +import org.lflang.toUnixString import org.lflang.util.LFCommand import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +@Suppress("unused") class CppGenerator( private val cppFileConfig: CppFileConfig, errorReporter: ErrorReporter, @@ -57,12 +63,18 @@ class CppGenerator( companion object { /** Path to the Cpp lib directory (relative to class path) */ const val libDir = "/lib/cpp" - - /** Default version of the reactor-cpp runtime to be used during compilation */ - val defaultRuntimeVersion = CppGenerator::class.java.getResourceAsStream("cpp-runtime-version.txt")!! - .bufferedReader().readLine().trim() } + /** Convert a log level to a severity number understood by the reactor-cpp runtime. */ + private val TargetProperty.LogLevel.severity + get() = when (this) { + TargetProperty.LogLevel.ERROR -> 1 + TargetProperty.LogLevel.WARN -> 2 + TargetProperty.LogLevel.INFO -> 3 + TargetProperty.LogLevel.LOG -> 4 + TargetProperty.LogLevel.DEBUG -> 4 + } + override fun doGenerate(resource: Resource, context: LFGeneratorContext) { super.doGenerate(resource, context) @@ -77,12 +89,7 @@ class CppGenerator( context.reportProgress( "Code generation complete. Validating generated code...", IntegratedBuilder.GENERATED_PERCENT_PROGRESS ) - if (!cppFileConfig.cppBuildDirectories.all { it.toFile().exists() }) { - // Special case: Some build directories do not exist, perhaps because this is the first C++ validation - // that has been done in this LF package since the last time the package was cleaned. - // We must compile in order to install the dependencies. Future validations will be faster. - doCompile(context, codeMaps) - } else if (runCmake(context).first == 0) { + if (runCmake(context).first == 0) { CppValidator(cppFileConfig, errorReporter, codeMaps).doValidate(context) context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(codeMaps)) } else { @@ -96,6 +103,23 @@ class CppGenerator( } } + private fun fetchReactorCpp() { + val version = targetConfig.runtimeVersion + val libPath = fileConfig.srcGenBasePath.resolve("reactor-cpp-$version") + // abort if the directory already exists + if (Files.isDirectory(libPath)) { + return + } + // clone the reactor-cpp repo and fetch the specified version + Files.createDirectories(libPath) + commandFactory.createCommand( + "git", + listOf("clone", "-n", "https://github.com/lf-lang/reactor-cpp.git", "reactor-cpp-$version"), + fileConfig.srcGenBasePath + ).run() + commandFactory.createCommand("git", listOf("checkout", version), libPath).run() + } + private fun generateFiles(): Map { val srcGenPath = fileConfig.srcGenPath @@ -103,9 +127,26 @@ class CppGenerator( // copy static library files over to the src-gen directory val genIncludeDir = srcGenPath.resolve("__include__") - fileConfig.copyFileFromClassPath("$libDir/lfutil.hh", genIncludeDir.resolve("lfutil.hh").toString()) - fileConfig.copyFileFromClassPath("$libDir/time_parser.hh", genIncludeDir.resolve("time_parser.hh").toString()) - fileConfig.copyFileFromClassPath("$libDir/3rd-party/cxxopts.hpp", genIncludeDir.resolve("CLI").resolve("cxxopts.hpp").toString()) + fileConfig.copyFileFromClassPath("$libDir/lfutil.hh", genIncludeDir.resolve("lfutil.hh"), true) + fileConfig.copyFileFromClassPath("$libDir/time_parser.hh", genIncludeDir.resolve("time_parser.hh"), true) + fileConfig.copyFileFromClassPath( + "$libDir/3rd-party/cxxopts.hpp", + genIncludeDir.resolve("CLI").resolve("cxxopts.hpp"), + true + ) + + // build reactor-cpp if needed + if (targetConfig.externalRuntimePath == null) { + if (targetConfig.runtimeVersion != null) { + fetchReactorCpp() + } else { + fileConfig.copyDirectoryFromClassPath( + "$libDir/reactor-cpp", + fileConfig.srcGenBasePath.resolve("reactor-cpp-default"), + true + ) + } + } // keep a list of all source files we generate val cppSources = mutableListOf() @@ -116,7 +157,7 @@ class CppGenerator( val mainCodeMap = CodeMap.fromGeneratedCode(CppMainGenerator(mainReactor, targetConfig, cppFileConfig).generateCode()) cppSources.add(mainFile) codeMaps[srcGenPath.resolve(mainFile)] = mainCodeMap - JavaGeneratorUtils.writeToFile(mainCodeMap.generatedCode, srcGenPath.resolve(mainFile).toString()) + JavaGeneratorUtils.writeToFile(mainCodeMap.generatedCode, srcGenPath.resolve(mainFile), true) // generate header and source files for all reactors for (r in reactors) { @@ -130,8 +171,8 @@ class CppGenerator( val headerCodeMap = CodeMap.fromGeneratedCode(generator.generateHeader()) codeMaps[srcGenPath.resolve(headerFile)] = headerCodeMap - JavaGeneratorUtils.writeToFile(headerCodeMap.generatedCode, srcGenPath.resolve(headerFile).toString()) - JavaGeneratorUtils.writeToFile(reactorCodeMap.generatedCode, srcGenPath.resolve(sourceFile).toString()) + JavaGeneratorUtils.writeToFile(headerCodeMap.generatedCode, srcGenPath.resolve(headerFile), true) + JavaGeneratorUtils.writeToFile(reactorCodeMap.generatedCode, srcGenPath.resolve(sourceFile), true) } // generate file level preambles for all resources @@ -145,13 +186,22 @@ class CppGenerator( val headerCodeMap = CodeMap.fromGeneratedCode(generator.generateHeader()) codeMaps[srcGenPath.resolve(headerFile)] = headerCodeMap - JavaGeneratorUtils.writeToFile(headerCodeMap.generatedCode, srcGenPath.resolve(headerFile).toString()) - JavaGeneratorUtils.writeToFile(preambleCodeMap.generatedCode, srcGenPath.resolve(sourceFile).toString()) + JavaGeneratorUtils.writeToFile(headerCodeMap.generatedCode, srcGenPath.resolve(headerFile), true) + JavaGeneratorUtils.writeToFile(preambleCodeMap.generatedCode, srcGenPath.resolve(sourceFile), true) } - // generate the cmake script + // generate the cmake scripts val cmakeGenerator = CppCmakeGenerator(targetConfig, cppFileConfig) - JavaGeneratorUtils.writeToFile(cmakeGenerator.generateCode(cppSources), srcGenPath.resolve("CMakeLists.txt").toString()) + val srcGenRoot = fileConfig.srcGenBasePath + val pkgName = fileConfig.srcGenPkgPath.fileName.toString() + JavaGeneratorUtils.writeToFile(cmakeGenerator.generateRootCmake(pkgName), srcGenRoot.resolve("CMakeLists.txt"), true) + JavaGeneratorUtils.writeToFile(cmakeGenerator.generateCmake(cppSources), srcGenPath.resolve("CMakeLists.txt"), true) + var subdir = srcGenPath.parent + while (subdir != srcGenRoot) { + JavaGeneratorUtils.writeToFile(cmakeGenerator.generateSubdirCmake(), subdir.resolve("CMakeLists.txt"), true) + subdir = subdir.parent + } + return codeMaps } @@ -176,9 +226,7 @@ class CppGenerator( */ private fun runCmake(context: LFGeneratorContext): Pair { val outPath = fileConfig.outPath - val buildPath = cppFileConfig.buildPath - val reactorCppPath = outPath.resolve("build").resolve("reactor-cpp") // make sure the build directory exists Files.createDirectories(buildPath) @@ -194,7 +242,7 @@ class CppGenerator( } // run cmake - val cmakeCommand = createCmakeCommand(buildPath, outPath, reactorCppPath) + val cmakeCommand = createCmakeCommand(buildPath, outPath) return Pair(cmakeCommand.run(context.cancelIndicator), version) } @@ -203,16 +251,21 @@ class CppGenerator( if (cmakeReturnCode == 0) { // If cmake succeeded, run make - val makeCommand = createMakeCommand(cppFileConfig.buildPath, version) + val makeCommand = createMakeCommand(cppFileConfig.buildPath, version, fileConfig.name) val makeReturnCode = CppValidator(cppFileConfig, errorReporter, codeMaps).run(makeCommand, context.cancelIndicator) - + var installReturnCode = 0 if (makeReturnCode == 0) { - println("SUCCESS (compiling generated C++ code)") - println("Generated source code is in ${fileConfig.srcGenPath}") - println("Compiled binary is in ${fileConfig.binPath}") - } else { + val installCommand = createMakeCommand(cppFileConfig.buildPath, version, "install") + installReturnCode = installCommand.run(context.cancelIndicator) + if (installReturnCode == 0) { + println("SUCCESS (compiling generated C++ code)") + println("Generated source code is in ${fileConfig.srcGenPath}") + println("Compiled binary is in ${fileConfig.binPath}") + } + } + if ((makeReturnCode != 0 || installReturnCode != 0) && !errorsOccurred()) { // If errors occurred but none were reported, then the following message is the best we can do. - if (!errorsOccurred()) errorReporter.reportError("make failed with error code $makeReturnCode") + errorReporter.reportError("make failed with error code $makeReturnCode") } } else if (version.isNotBlank()) { errorReporter.reportError("cmake failed with error code $cmakeReturnCode") @@ -237,19 +290,19 @@ class CppGenerator( return 0 } - private fun createMakeCommand(buildPath: Path, version: String): LFCommand { + private fun createMakeCommand(buildPath: Path, version: String, target: String): LFCommand { val makeArgs: List if (version.compareVersion("3.12.0") < 0) { errorReporter.reportWarning("CMAKE is older than version 3.12. Parallel building is not supported.") makeArgs = - listOf("--build", ".", "--target", "install", "--config", targetConfig.cmakeBuildType?.toString() ?: "Release") + listOf("--build", ".", "--target", target, "--config", targetConfig.cmakeBuildType?.toString() ?: "Release") } else { val cores = Runtime.getRuntime().availableProcessors() makeArgs = listOf( "--build", ".", "--target", - "install", + target, "--parallel", cores.toString(), "--config", @@ -260,13 +313,16 @@ class CppGenerator( return commandFactory.createCommand("cmake", makeArgs, buildPath) } - private fun createCmakeCommand(buildPath: Path, outPath: Path, reactorCppPath: Path): LFCommand { + private fun createCmakeCommand(buildPath: Path, outPath: Path): LFCommand { val cmd = commandFactory.createCommand( "cmake", listOf( + "-DCMAKE_BUILD_TYPE=${targetConfig.cmakeBuildType}", "-DCMAKE_INSTALL_PREFIX=${outPath.toUnixString()}", - "-DREACTOR_CPP_BUILD_DIR=${reactorCppPath.toUnixString()}", "-DCMAKE_INSTALL_BINDIR=${outPath.relativize(fileConfig.binPath).toUnixString()}", - fileConfig.srcGenPath.toUnixString() + "-DREACTOR_CPP_VALIDATE=${if (targetConfig.noRuntimeValidation) "OFF" else "ON"}", + "-DREACTOR_CPP_TRACE=${if (targetConfig.tracing != null) "ON" else "OFF"}", + "-DREACTOR_CPP_LOG_LEVEL=${targetConfig.logLevel.severity}", + fileConfig.srcGenBasePath.toUnixString() ), buildPath ) @@ -332,22 +388,23 @@ object CppTypes : TargetTypes { override fun getTargetUndefinedType() = "void" override fun getTargetTimeExpr(timeValue: TimeValue): String = - with (timeValue) { + with(timeValue) { if (magnitude == 0L) "reactor::Duration::zero()" else magnitude.toString() + unit.cppUnit } } + /** Get a C++ representation of a LF unit. */ val TimeUnit?.cppUnit get() = when (this) { - TimeUnit.NANO -> "ns" - TimeUnit.MICRO -> "us" - TimeUnit.MILLI -> "ms" - TimeUnit.SECOND -> "s" - TimeUnit.MINUTE -> "min" - TimeUnit.HOUR -> "h" - TimeUnit.DAY -> "d" - TimeUnit.WEEK -> "d*7" - else -> "" + TimeUnit.NANO -> "ns" + TimeUnit.MICRO -> "us" + TimeUnit.MILLI -> "ms" + TimeUnit.SECOND -> "s" + TimeUnit.MINUTE -> "min" + TimeUnit.HOUR -> "h" + TimeUnit.DAY -> "d" + TimeUnit.WEEK -> "d*7" + else -> "" } diff --git a/org.lflang/src/org/lflang/generator/cpp/CppValidator.kt b/org.lflang/src/org/lflang/generator/cpp/CppValidator.kt index d2958e7865..b04db1350f 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppValidator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppValidator.kt @@ -26,6 +26,7 @@ class CppValidator( companion object { /** This matches a line in the CMake cache. */ private val CMAKE_CACHED_VARIABLE: Pattern = Pattern.compile("(?[\\w_]+):(?[\\w_]+)=(?.*)") + private val CMAKE_GENERATOR_EXPRESSION: Pattern = Pattern.compile("\\\$<\\w*:(?.*)>") /** This matches a line of error reports from g++. */ private val GXX_ERROR_LINE: Pattern = Pattern.compile( @@ -147,7 +148,10 @@ class CppValidator( /** The include directories required by the generated files. */ private val includes: List - get() = getFromCache(CppCmakeGenerator.includesVarName)?.split(';') ?: listOf() + get() = getFromCache(CppCmakeGenerator.includesVarName(fileConfig.name))?.split(';')?.map { + val matcher = CMAKE_GENERATOR_EXPRESSION.matcher(it) + if (matcher.matches()) matcher.group("content") else it + } ?: listOf() /** The C++ standard used by the generated files. */ private val cppStandard: String? diff --git a/org.lflang/src/org/lflang/generator/cpp/cpp-runtime-version.txt b/org.lflang/src/org/lflang/generator/cpp/cpp-runtime-version.txt deleted file mode 100644 index a885503a03..0000000000 --- a/org.lflang/src/org/lflang/generator/cpp/cpp-runtime-version.txt +++ /dev/null @@ -1 +0,0 @@ -b1f6c773f05ebb17995cda6b45822817b8fb8136 diff --git a/org.lflang/src/org/lflang/generator/python/PyUtil.java b/org.lflang/src/org/lflang/generator/python/PyUtil.java index 8dd38ec174..ff65be5ce6 100644 --- a/org.lflang/src/org/lflang/generator/python/PyUtil.java +++ b/org.lflang/src/org/lflang/generator/python/PyUtil.java @@ -27,7 +27,14 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY package org.lflang.generator.python; import org.lflang.generator.ReactorInstance; +import org.lflang.generator.GeneratorBase; import org.lflang.generator.c.CUtil; +import org.lflang.lf.Value; + +import java.io.IOException; + +import org.lflang.ASTUtils; +import org.lflang.FileConfig; /** @@ -48,7 +55,7 @@ public class PyUtil extends CUtil { * * @param instance The reactor instance. */ - static public String reactorRefName(ReactorInstance instance) { + public static String reactorRefName(ReactorInstance instance) { return instance.uniqueID() + "_lf"; } @@ -66,7 +73,7 @@ static public String reactorRefName(ReactorInstance instance) { * that returned by * {@link #runtimeIndex(ReactorInstance)}. */ - static public String reactorRef(ReactorInstance instance, String runtimeIndex) { + public static String reactorRef(ReactorInstance instance, String runtimeIndex) { if (runtimeIndex == null) runtimeIndex = runtimeIndex(instance); return PyUtil.reactorRefName(instance) + "[" + runtimeIndex + "]"; } @@ -81,8 +88,104 @@ static public String reactorRef(ReactorInstance instance, String runtimeIndex) { * * @param instance The reactor instance. */ - static public String reactorRef(ReactorInstance instance) { + public static String reactorRef(ReactorInstance instance) { return PyUtil.reactorRef(instance, null); } -} + /** + * Convert C types to formats used in Py_BuildValue and PyArg_PurseTuple. + * This is unused but will be useful to enable inter-compatibility between + * C and Python reactors. + * @param type C type + */ + public static String pyBuildValueArgumentType(String type) { + switch (type) { + case "int": return "i"; + case "string": return "s"; + case "char": return "b"; + case "short int": return "h"; + case "long": return "l"; + case "unsigned char": return "B"; + case "unsigned short int": return "H"; + case "unsigned int": return "I"; + case "unsigned long": return "k"; + case "long long": return "L"; + case "interval_t": return "L"; + case "unsigned long long": return "K"; + case "double": return "d"; + case "float": return "f"; + case "Py_complex": return "D"; + case "Py_complex*": return "D"; + case "Py_Object": return "O"; + case "Py_Object*": return "O"; + default: return "O"; + } + } + + public static String generateGILAcquireCode() { + return String.join("\n", + "// Acquire the GIL (Global Interpreter Lock) to be able to call Python APIs.", + "PyGILState_STATE gstate;", + "gstate = PyGILState_Ensure();" + ); + } + + public static String generateGILReleaseCode() { + return String.join("\n", + "/* Release the thread. No Python API allowed beyond this point. */", + "PyGILState_Release(gstate);" + ); + } + + /** + * Override to convert some C types to their + * Python equivalent. + * Examples: + * true/false -> True/False + * @param v A value + * @return A value string in the target language + */ + protected static String getPythonTargetValue(Value v) { + String returnValue = ""; + switch (ASTUtils.toText(v)) { + case "false": + returnValue = "False"; + break; + case "true": + returnValue = "True"; + break; + default: + returnValue = GeneratorBase.getTargetValue(v); + } + + // Parameters in Python are always prepended with a 'self.' + // predicate. Therefore, we need to append the returned value + // if it is a parameter. + if (v.getParameter() != null) { + returnValue = "self." + returnValue; + } + + return returnValue; + } + + + /** + * Copy Python specific target code to the src-gen directory + */ + public static void copyTargetFiles(FileConfig fileConfig) throws IOException { + // Copy the required target language files into the target file system. + // This will also overwrite previous versions. + fileConfig.copyFileFromClassPath( + "/lib/py/reactor-c-py/include/pythontarget.h", + fileConfig.getSrcGenPath().resolve("pythontarget.h") + ); + fileConfig.copyFileFromClassPath( + "/lib/py/reactor-c-py/lib/pythontarget.c", + fileConfig.getSrcGenPath().resolve("pythontarget.c") + ); + fileConfig.copyFileFromClassPath( + "/lib/c/reactor-c/include/ctarget.h", + fileConfig.getSrcGenPath().resolve("ctarget.h") + ); + } +} \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/python/PythonActionGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonActionGenerator.java new file mode 100644 index 0000000000..980a835d37 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonActionGenerator.java @@ -0,0 +1,13 @@ +package org.lflang.generator.python; + +import org.lflang.lf.Action; +import org.lflang.lf.ReactorDecl; +import org.lflang.generator.c.CGenerator; + +public class PythonActionGenerator { + public static String generateAliasTypeDef(ReactorDecl decl, Action action, + String genericActionType) { + + return "typedef "+genericActionType+" "+CGenerator.variableStructType(action, decl)+";"; + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java new file mode 100644 index 0000000000..f364258d6b --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java @@ -0,0 +1,18 @@ +package org.lflang.generator.python; + +import java.nio.file.Path; + +public class PythonDockerGenerator { + public static String generateDockerFileContent(String topLevelName, Path srcGenPath) { + return String.join("\n", + "# Generated docker file for "+topLevelName+".lf in "+srcGenPath+".", + "# For instructions, see: https://github.com/icyphy/lingua-franca/wiki/Containerized-Execution", + "FROM python:slim", + "WORKDIR /lingua-franca/"+topLevelName, + "RUN set -ex && apt-get update && apt-get install -y python3-pip", + "COPY . src-gen", + "RUN cd src-gen && python3 setup.py install && cd ..", + "ENTRYPOINT [\"python3\", \"src-gen/"+topLevelName+".py\"]" + ); + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonGenerator.java new file mode 100644 index 0000000000..4c6cf4be18 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonGenerator.java @@ -0,0 +1,938 @@ +/* Generator for the Python target. */ + +/************* + * Copyright (c) 2022, The University of California at Berkeley. + * Copyright (c) 2022, The University of Texas at Dallas. + + * 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.python; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.util.CancelIndicator; +import org.eclipse.xtext.xbase.lib.Exceptions; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +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.Mode; +import org.lflang.federated.FedFileConfig; +import org.lflang.federated.FederateInstance; +import org.lflang.federated.launcher.FedPyLauncher; +import org.lflang.federated.serialization.FedNativePythonSerialization; +import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.CodeMap; +import org.lflang.generator.GeneratorResult; +import org.lflang.generator.IntegratedBuilder; +import org.lflang.generator.JavaGeneratorUtils; +import org.lflang.generator.LFGeneratorContext; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.SubContext; +import org.lflang.generator.TargetTypes; +import org.lflang.generator.c.CGenerator; +import org.lflang.generator.c.CPreambleGenerator; +import org.lflang.generator.c.CUtil; +import org.lflang.lf.Action; +import org.lflang.lf.Delay; +import org.lflang.lf.Input; +import org.lflang.lf.Model; +import org.lflang.lf.Output; +import org.lflang.lf.Port; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.VarRef; +import org.lflang.util.LFCommand; + +import com.google.common.base.Objects; + + +/** + * Generator for Python target. This class generates Python code defining each reactor + * class given in the input .lf file and imported .lf files. + * + * 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 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 } + */ +public class PythonGenerator extends CGenerator { + + // Used to add statements that come before reactor classes and user code + private CodeBuilder pythonPreamble = new CodeBuilder(); + + // Used to add module requirements to setup.py (delimited with ,) + private List pythonRequiredModules = new ArrayList<>(); + + private PythonTypes types; + + public PythonGenerator(FileConfig fileConfig, ErrorReporter errorReporter) { + this(fileConfig, errorReporter, new PythonTypes(errorReporter)); + } + + private PythonGenerator(FileConfig fileConfig, ErrorReporter errorReporter, PythonTypes types) { + super(fileConfig, errorReporter, false, types); + this.targetConfig.compiler = "gcc"; + this.targetConfig.compilerFlags = new ArrayList<>(); + this.targetConfig.linkerFlags = ""; + this.types = types; + } + + /** + * Generic struct for ports with primitive types and + * statically allocated arrays in Lingua Franca. + * This template is defined as + * typedef struct { + * PyObject* value; + * bool is_present; + * int num_destinations; + * FEDERATED_CAPSULE_EXTENSION + * } generic_port_instance_struct; + * + * @see reactor-c-py/lib/pythontarget.h + */ + String genericPortType = "generic_port_instance_struct"; + + /** + * Generic struct for ports with dynamically allocated + * array types (a.k.a. token types) in Lingua Franca. + * This template is defined as + * typedef struct { + * PyObject_HEAD + * PyObject* value; + * bool is_present; + * int num_destinations; + * lf_token_t* token; + * int length; + * FEDERATED_CAPSULE_EXTENSION + * } generic_port_instance_with_token_struct; + * + * @see reactor-c-py/lib/pythontarget.h + */ + String genericPortTypeWithToken = "generic_port_instance_with_token_struct"; + + /** + * Generic struct for actions. + * This template is defined as + * typedef struct { + * trigger_t* trigger; + * PyObject* value; + * bool is_present; + * bool has_value; + * lf_token_t* token; + * FEDERATED_CAPSULE_EXTENSION + * } generic_action_instance_struct; + * + * @see reactor-c-py/lib/pythontarget.h + */ + String genericActionType = "generic_action_instance_struct"; + + /** Returns the Target enum for this generator */ + @Override + public Target getTarget() { + return Target.Python; + } + + private Set protoNames = new HashSet<>(); + + // ////////////////////////////////////////// + // // Public methods + @Override + public String printInfo() { + System.out.println("Generating code for: " + fileConfig.resource.getURI().toString()); + System.out.println("******** Mode: " + fileConfig.context.getMode()); + System.out.println("******** Generated sources: " + fileConfig.getSrcGenPath()); + return null; + } + + @Override + public TargetTypes getTargetTypes() { + return types; + } + + // ////////////////////////////////////////// + // // Protected methods + /** + * Generate all Python classes if they have a reaction + * @param federate The federate instance used to generate classes + */ + public String generatePythonReactorClasses(FederateInstance federate) { + CodeBuilder pythonClasses = new CodeBuilder(); + CodeBuilder pythonClassesInstantiation = new CodeBuilder(); + + // Generate reactor classes in Python + pythonClasses.pr(PythonReactorGenerator.generatePythonClass(main, federate, main, types)); + + // Create empty lists to hold reactor instances + pythonClassesInstantiation.pr(PythonReactorGenerator.generateListsToHoldClassInstances(main, federate)); + + // Instantiate generated classes + pythonClassesInstantiation.pr(PythonReactorGenerator.generatePythonClassInstantiations(main, federate, main)); + + return String.join("\n", + pythonClasses.toString(), + "", + "# Instantiate classes", + pythonClassesInstantiation.toString() + ); + } + + /** + * Generate the Python code constructed from reactor classes and user-written classes. + * @return the code body + */ + public String generatePythonCode(FederateInstance federate) { + return String.join("\n", + "# List imported names, but do not use pylint's --extension-pkg-allow-list option", + "# so that these names will be assumed present without having to compile and install.", + "from LinguaFranca"+topLevelName+" import ( # pylint: disable=no-name-in-module", + " Tag, action_capsule_t, compare_tags, get_current_tag, get_elapsed_logical_time,", + " get_elapsed_physical_time, get_logical_time, get_microstep, get_physical_time,", + " get_start_time, port_capsule, port_instance_token, request_stop, schedule_copy,", + " start", + ")", + "from LinguaFrancaBase.constants import BILLION, FOREVER, NEVER, instant_t, interval_t", + "from LinguaFrancaBase.functions import (", + " DAY, DAYS, HOUR, HOURS, MINUTE, MINUTES, MSEC, MSECS, NSEC, NSECS, SEC, SECS, USEC,", + " USECS, WEEK, WEEKS", + ")", + "from LinguaFrancaBase.classes import Make", + "import sys", + "import copy", + "", + pythonPreamble.toString(), + "", + generatePythonReactorClasses(federate), + "", + PythonMainGenerator.generateCode() + ); + } + + /** + * Generate the setup.py required to compile and install the module. + * Currently, the package name is based on filename which does not support sharing the setup.py for multiple .lf files. + * TODO: use an alternative package name (possibly based on folder name) + * + * If the LF program itself is threaded or if tracing is enabled, NUMBER_OF_WORKERS is added as a macro + * so that platform-specific C files will contain the appropriate functions. + */ + public String generatePythonSetupFile() { + String moduleName = "LinguaFranca" + topLevelName; + + List sources = new ArrayList<>(targetConfig.compileAdditionalSources); + sources.add(topLevelName + ".c"); + sources = sources.stream() + .map(Paths::get) + .map(FileConfig::toUnixString) + .map(PythonGenerator::addDoubleQuotes) + .collect(Collectors.toList()); + + List macros = new ArrayList<>(); + macros.add(generateMacroEntry("MODULE_NAME", moduleName)); + + for (var entry : targetConfig.compileDefinitions.entrySet()) { + macros.add(generateMacroEntry(entry.getKey(), entry.getValue())); + } + + if (targetConfig.threads != 0 || targetConfig.tracing != null) { + macros.add(generateMacroEntry("NUMBER_OF_WORKERS", String.valueOf(targetConfig.threads))); + } + + List installRequires = new ArrayList<>(pythonRequiredModules); + installRequires.add("LinguaFrancaBase"); + installRequires.replaceAll(PythonGenerator::addDoubleQuotes); + + return String.join("\n", + "from setuptools import setup, Extension", + "", + "linguafranca"+topLevelName+"module = Extension("+addDoubleQuotes(moduleName)+",", + " sources = ["+String.join(", ", sources)+"],", + " define_macros=["+String.join(", ", macros)+"])", + "", + "setup(name="+addDoubleQuotes(moduleName)+", version=\"1.0\",", + " ext_modules = [linguafranca"+topLevelName+"module],", + " install_requires=["+String.join(", ", installRequires)+"])" + ); + } + + /** + * Generate the necessary Python files. + * @param federate The federate instance + */ + public Map generatePythonFiles(FederateInstance federate) throws IOException { + Path filePath = fileConfig.getSrcGenPath().resolve(topLevelName + ".py"); + File file = filePath.toFile(); + Files.deleteIfExists(filePath); + // Create the necessary directories + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + Map codeMaps = new HashMap<>(); + codeMaps.put(filePath, CodeMap.fromGeneratedCode(generatePythonCode(federate).toString())); + JavaGeneratorUtils.writeToFile(codeMaps.get(filePath).getGeneratedCode(), filePath); + + Path setupPath = fileConfig.getSrcGenPath().resolve("setup.py"); + // Handle Python setup + System.out.println("Generating setup file to " + setupPath); + Files.deleteIfExists(setupPath); + + // Create the setup file + JavaGeneratorUtils.writeToFile(generatePythonSetupFile(), setupPath); + return codeMaps; + } + + /** + * Execute the command that compiles and installs the current Python module + */ + public void pythonCompileCode(LFGeneratorContext context) { + // if we found the compile command, we will also find the install command + LFCommand installCmd = commandFactory.createCommand( + "python3", List.of("-m", "pip", "install", "--force-reinstall", "."), fileConfig.getSrcGenPath() + ); + + if (installCmd == null) { + errorReporter.reportError( + "The Python target requires Python >= 3.6, pip >= 20.0.2, and setuptools >= 45.2.0-1 to compile the generated code. " + + "Auto-compiling can be disabled using the \"no-compile: true\" target property."); + return; + } + + // Set compile time environment variables + installCmd.setEnvironmentVariable("CC", targetConfig.compiler); // Use gcc as the compiler + installCmd.setEnvironmentVariable("LDFLAGS", targetConfig.linkerFlags); // The linker complains about including pythontarget.h twice (once in the generated code and once in pythontarget.c) + // To avoid this, we force the linker to allow multiple definitions. Duplicate names would still be caught by the + // compiler. + if (installCmd.run(context.getCancelIndicator()) == 0) { + System.out.println("Successfully installed python extension."); + } else { + errorReporter.reportError("Failed to install python extension due to the following errors:\n" + + installCmd.getErrors()); + } + } + + /** + * Generate top-level preambles and #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. + * + * TODO: This function returns a boolean because xtend-generated parent function in CGenerator returns boolean + */ + @Override + public boolean generatePreamble() { + Set models = new LinkedHashSet<>(); + for (Reactor r : ASTUtils.convertToEmptyListIfNull(reactors)) { + // 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((Model) ASTUtils.toDefinition(r).eContainer()); + } + // Add the main reactor if it is defined + if (this.mainDef != null) { + models.add((Model) ASTUtils.toDefinition(this.mainDef.getReactorClass()).eContainer()); + } + for (Model m : models) { + pythonPreamble.pr(PythonPreambleGenerator.generatePythonPreambles(m.getPreambles())); + } + code.pr(CGenerator.defineLogLevel(this)); + if (isFederated) { + code.pr(CPreambleGenerator.generateFederatedDirective(targetConfig.coordination)); + // Handle target parameters. + // First, if there are federates, then ensure that threading is enabled. + targetConfig.threads = CUtil.minThreadsToHandleInputPorts(federates); + } + includeTargetLanguageHeaders(); + code.pr(CPreambleGenerator.generateNumFederatesDirective(federates.size())); + code.pr(CPreambleGenerator.generateMixedRadixIncludeHeader()); + super.includeTargetLanguageSourceFiles(); + super.parseTargetParameters(); + return false; // placeholder return value. See comment above + } + + /** + * Add necessary code to the source and necessary build supports to + * enable the requested serializations in 'enabledSerializations' + */ + @Override + public void enableSupportForSerializationIfApplicable(CancelIndicator cancelIndicator) { + if (!IterableExtensions.isNullOrEmpty(targetConfig.protoFiles)) { + // Enable support for proto serialization + enabledSerializers.add(SupportedSerializers.PROTO); + } + for (SupportedSerializers serialization : enabledSerializers) { + switch (serialization) { + case NATIVE: { + FedNativePythonSerialization pickler = new FedNativePythonSerialization(); + code.pr(pickler.generatePreambleForSupport().toString()); + } + case PROTO: { + // Handle .proto files. + for (String name : targetConfig.protoFiles) { + this.processProtoFile(name, cancelIndicator); + int dotIndex = name.lastIndexOf("."); + String rootFilename = dotIndex > 0 ? name.substring(0, dotIndex) : name; + pythonPreamble.pr("import "+rootFilename+"_pb2 as "+rootFilename); + protoNames.add(rootFilename); + } + } + case ROS2: { + // FIXME: Not supported yet + } + } + } + } + + /** + * Process a given .proto file. + * + * Run, if possible, the proto-c protocol buffer code generator to produce + * the required .h and .c files. + * @param filename Name of the file to process. + */ + @Override + public void processProtoFile(String filename, CancelIndicator cancelIndicator) { + LFCommand protoc = commandFactory.createCommand( + "protoc", List.of("--python_out="+fileConfig.getSrcGenPath(), filename), fileConfig.srcPath); + + if (protoc == null) { + errorReporter.reportError("Processing .proto files requires libprotoc >= 3.6.1"); + return; + } + int returnCode = protoc.run(cancelIndicator); + if (returnCode == 0) { + pythonRequiredModules.add("google-api-python-client"); + } else { + errorReporter.reportError("protoc returns error code " + returnCode); + } + } + + /** + * Generate code for the body of a reaction that handles the + * action that is triggered by receiving a message from a remote + * federate. + * @param action The action. + * @param sendingPort The output port providing the data to send. + * @param receivingPort The ID of the destination port. + * @param receivingPortID The ID of the destination port. + * @param sendingFed The sending federate. + * @param receivingFed The destination federate. + * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. + * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. + * @param type The type. + * @param isPhysical Indicates whether or not the connection is physical + * @param serializer The serializer used on the connection. + */ + @Override + public String generateNetworkReceiverBody( + Action action, + VarRef sendingPort, + VarRef receivingPort, + int receivingPortID, + FederateInstance sendingFed, + FederateInstance receivingFed, + int receivingBankIndex, + int receivingChannelIndex, + InferredType type, + boolean isPhysical, + SupportedSerializers serializer + ) { + return PythonNetworkGenerator.generateNetworkReceiverBody( + action, + sendingPort, + receivingPort, + receivingPortID, + sendingFed, + receivingFed, + receivingBankIndex, + receivingChannelIndex, + type, + isPhysical, + serializer + ); + } + + /** + * Generate code for the body of a reaction that handles an output + * that is to be sent over the network. + * @param sendingPort The output port providing the data to send. + * @param receivingPort The variable reference to the destination port. + * @param receivingPortID The ID of the destination port. + * @param sendingFed The sending federate. + * @param sendingBankIndex The bank index of the sending federate, if it is a bank. + * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. + * @param receivingFed The destination federate. + * @param type The type. + * @param isPhysical Indicates whether the connection is physical or not + * @param delay The delay value imposed on the connection using after + * @param serializer The serializer used on the connection. + */ + @Override + public String generateNetworkSenderBody( + VarRef sendingPort, + VarRef receivingPort, + int receivingPortID, + FederateInstance sendingFed, + int sendingBankIndex, + int sendingChannelIndex, + FederateInstance receivingFed, + InferredType type, + boolean isPhysical, + Delay delay, + SupportedSerializers serializer + ) { + return PythonNetworkGenerator.generateNetworkSenderBody( + sendingPort, + receivingPort, + receivingPortID, + sendingFed, + sendingBankIndex, + sendingChannelIndex, + receivingFed, + type, + isPhysical, + delay, + serializer, + targetConfig.coordination + ); + } + + /** + * Create a launcher script that executes all the federates and the RTI. + * + * @param coreFiles The files from the core directory that must be + * copied to the remote machines. + */ + @Override + public void createFederatedLauncher(ArrayList coreFiles) { + FedPyLauncher launcher = new FedPyLauncher( + targetConfig, + fileConfig, + errorReporter + ); + try { + launcher.createLauncher( + coreFiles, + federates, + federationRTIProperties + ); + } catch (IOException e) { + // ignore + } + } + + /** + * Generate the aliases for inputs, outputs, and struct type definitions for + * actions of the specified reactor in the specified federate. + * @param reactor The parsed reactor data structure. + * @param federate A federate name, or null to unconditionally generate. + */ + @Override + public void generateAuxiliaryStructs( + ReactorDecl decl + ) { + Reactor reactor = ASTUtils.toDefinition(decl); + // First, handle inputs. + for (Input input : ASTUtils.allInputs(reactor)) { + generateAuxiliaryStructsForPort(decl, input); + } + // Next, handle outputs. + for (Output output : ASTUtils.allOutputs(reactor)) { + generateAuxiliaryStructsForPort(decl, output); + } + // Finally, handle actions. + for (Action action : ASTUtils.allActions(reactor)) { + generateAuxiliaryStructsForAction(decl, currentFederate, action); + } + } + + private void generateAuxiliaryStructsForPort(ReactorDecl decl, + Port port) { + boolean isTokenType = CUtil.isTokenType(JavaAstUtils.getInferredType(port), types); + code.pr(port, + PythonPortGenerator.generateAliasTypeDef(decl, port, isTokenType, + genericPortTypeWithToken, + genericPortType)); + } + + private void generateAuxiliaryStructsForAction(ReactorDecl decl, + FederateInstance federate, + Action action) { + if (federate != null && !federate.contains(action)) { + return; + } + code.pr(action, PythonActionGenerator.generateAliasTypeDef(decl, action, genericActionType)); + } + + /** + * For the specified action, return a declaration for action struct to + * contain the value of the action. + * This will return an empty string for an action with no type. + * @param action The action. + * @return A string providing the value field of the action struct. + */ + @Override + public String valueDeclaration(Action action) { + return "PyObject* value;"; + } + + /** Add necessary include files specific to the target language. + * Note. The core files always need to be (and will be) copied + * uniformly across all target languages. + */ + @Override + public void includeTargetLanguageHeaders() { + code.pr("#define _LF_GARBAGE_COLLECTED"); + if (targetConfig.tracing != null) { + var filename = ""; + if (targetConfig.tracing.traceFileName != null) { + filename = targetConfig.tracing.traceFileName; + } + code.pr("#define LINGUA_FRANCA_TRACE " + filename); + } + + code.pr("#include \"pythontarget.c\""); + if (targetConfig.tracing != null) { + code.pr("#include \"core/trace.c\""); + } + } + + /** + * Return true if the host operating system is compatible and + * otherwise report an error and return false. + */ + @Override + public boolean isOSCompatible() { + if (JavaGeneratorUtils.isHostWindows() && isFederated) { + errorReporter.reportError( + "Federated LF programs with a Python target are currently not supported on Windows. Exiting code generation." + ); + // Return to avoid compiler errors + return false; + } + return true; + } + + /** Generate C code from the Lingua Franca model contained by the + * specified resource. This is the main entry point for code + * generation. + * @param resource The resource containing the source code. + * @param context Context relating to invocation of the code generator. + */ + @Override + public void doGenerate(Resource resource, LFGeneratorContext context) { + // If there are federates, assign the number of threads in the CGenerator to 1 + if (isFederated) { + targetConfig.threads = 1; + } + + // Prevent the CGenerator from compiling the C code. + // The PythonGenerator will compiler it. + boolean compileStatus = targetConfig.noCompile; + targetConfig.noCompile = true; + targetConfig.useCmake = false; // Force disable the CMake because + // it interferes with the Python target functionality + int cGeneratedPercentProgress = (IntegratedBuilder.VALIDATED_PERCENT_PROGRESS + 100) / 2; + super.doGenerate(resource, new SubContext( + context, + IntegratedBuilder.VALIDATED_PERCENT_PROGRESS, + cGeneratedPercentProgress + )); + SubContext compilingFederatesContext = new SubContext(context, cGeneratedPercentProgress, 100); + targetConfig.noCompile = compileStatus; + + if (errorsOccurred()) { + context.unsuccessfulFinish(); + return; + } + + String baseFileName = topLevelName; + // Keep a separate file config for each federate + FileConfig oldFileConfig = fileConfig; + var federateCount = 0; + Map codeMaps = new HashMap<>(); + for (FederateInstance federate : federates) { + federateCount++; + if (isFederated) { + topLevelName = baseFileName + '_' + federate.name; + try { + fileConfig = new FedFileConfig(fileConfig, federate.name); + } catch (IOException e) { + throw Exceptions.sneakyThrow(e); + } + } + // Don't generate code if there is no main reactor + if (this.main != null) { + try { + Map codeMapsForFederate = generatePythonFiles(federate); + codeMaps.putAll(codeMapsForFederate); + PyUtil.copyTargetFiles(fileConfig); + if (!targetConfig.noCompile) { + compilingFederatesContext.reportProgress( + String.format("Validating %d/%d sets of generated files...", federateCount, federates.size()), + 100 * federateCount / federates.size() + ); + // If there are no federates, compile and install the generated code + new PythonValidator(fileConfig, errorReporter, codeMaps, protoNames).doValidate(context); + if (!errorsOccurred() && !Objects.equal(context.getMode(), Mode.LSP_MEDIUM)) { + compilingFederatesContext.reportProgress( + String.format("Validation complete. Compiling and installing %d/%d Python modules...", + federateCount, federates.size()), + 100 * federateCount / federates.size() + ); + pythonCompileCode(context); // Why is this invoked here if the current federate is not a parameter? + } + } else { + System.out.println(PythonInfoGenerator.generateSetupInfo(fileConfig)); + } + } catch (Exception e) { + throw Exceptions.sneakyThrow(e); + } + + if (!isFederated) { + System.out.println(PythonInfoGenerator.generateRunInfo(fileConfig, topLevelName)); + } + } + fileConfig = oldFileConfig; + } + if (isFederated) { + System.out.println(PythonInfoGenerator.generateFedRunInfo(fileConfig)); + } + // Restore filename + topLevelName = baseFileName; + if (errorReporter.getErrorsOccurred()) { + context.unsuccessfulFinish(); + } else if (!isFederated) { + context.finish(GeneratorResult.Status.COMPILED, topLevelName+".py", fileConfig.getSrcGenPath(), fileConfig, + codeMaps, "python3"); + } else { + context.finish(GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig.binPath, fileConfig, codeMaps, + "bash"); + } + } + + + /** + * Generate code for the body of a reaction that takes an input and + * schedules an action with the value of that input. + * @param action The action to schedule + * @param port The port to read from + */ + @Override + public String generateDelayBody(Action action, VarRef port) { + return PythonReactionGenerator.generateCDelayBody(action, port, CUtil.isTokenType(JavaAstUtils.getInferredType(action), types)); + } + + /** + * Generate code for the body of a reaction that is triggered by the + * given action and writes its value to the given port. This realizes + * the receiving end of a logical delay specified with the 'after' + * keyword. + * @param action The action that triggers the reaction + * @param port The port to write to. + */ + @Override + public String generateForwardBody(Action action, VarRef port) { + String outputName = JavaAstUtils.generateVarRef(port); + if (CUtil.isTokenType(JavaAstUtils.getInferredType(action), types)) { + return super.generateForwardBody(action, port); + } else { + return "SET("+outputName+", "+action.getName()+"->token->value);"; + } + } + + /** Generate a reaction function definition for a reactor. + * This function has a single argument that is a void* pointing to + * a struct that contains parameters, state variables, inputs (triggering or not), + * actions (triggering or produced), and outputs. + * @param reaction The reaction. + * @param reactor The reactor. + * @param reactionIndex The position of the reaction within the reactor. + */ + @Override + public void generateReaction(Reaction reaction, ReactorDecl decl, int reactionIndex) { + Reactor reactor = ASTUtils.toDefinition(decl); + + // Delay reactors and top-level reactions used in the top-level reactor(s) in federated execution are generated in C + if (reactor.getName().contains(GEN_DELAY_CLASS_NAME) || + ((mainDef != null && decl == mainDef.getReactorClass() || mainDef == decl) && reactor.isFederated())) { + super.generateReaction(reaction, decl, reactionIndex); + return; + } + code.pr(PythonReactionGenerator.generateCReaction(reaction, decl, reactionIndex, mainDef, errorReporter, types, isFederatedAndDecentralized())); + } + + /** + * Generate code that initializes the state variables for a given instance. + * Unlike parameters, state variables are uniformly initialized for all instances + * of the same reactor. This task is left to Python code to allow for more liberal + * state variable assignments. + * @param instance The reactor class instance + * @return Initialization code fore state variables of instance + */ + @Override + public void generateStateVariableInitializations(ReactorInstance instance) { + // Do nothing + } + + /** + * Generate code for parameter variables of a reactor in C in the form "parameter.type parameter.name;" + * + * @param reactor The reactor. + * @param builder The place that the generated code is written to. + * @return + */ + @Override + public void generateParametersForReactor(CodeBuilder builder, Reactor reactor) { + // Do nothing + // Parameters are generated in Python + } + + /** + * Generate runtime initialization code in C for parameters of a given reactor instance + * + * @param instance The reactor instance. + */ + @Override + public void generateParameterInitialization(ReactorInstance instance) { + // Do nothing + // Parameters are initialized in Python + } + + /** + * This function is overridden in the Python generator to do nothing. + * The state variables are initialized in Python code directly. + * @param reactor The reactor. + * @param builder The place that the generated code is written to. + * @return + */ + @Override + public void generateStateVariablesForReactor(CodeBuilder builder, Reactor reactor) { + // Do nothing + } + + /** + * Generates C preambles defined by user for a given reactor + * Since the Python generator expects preambles written in C, + * this function is overridden and does nothing. + * @param reactor The given reactor + */ + @Override + public void generateUserPreamblesForReactor(Reactor reactor) { + // Do nothing + } + + /** + * Generate code that is executed while the reactor instance is being initialized. + * This wraps the reaction functions in a Python function. + * @param instance The reactor instance. + * @param reactions The reactions of this instance. + */ + @Override + public void generateReactorInstanceExtension( + ReactorInstance instance + ) { + initializeTriggerObjects.pr(PythonReactionGenerator.generateCPythonReactionLinkers(instance, mainDef, topLevelName)); + } + + /** + * This function is provided to allow extensions of the CGenerator to append the structure of the self struct + * @param selfStructBody The body of the self struct + * @param decl The reactor declaration for the self struct + * @param instance The current federate instance + * @param constructorCode Code that is executed when the reactor is instantiated + */ + @Override + public void generateSelfStructExtension( + CodeBuilder selfStructBody, + ReactorDecl decl, + CodeBuilder constructorCode + ) { + Reactor reactor = ASTUtils.toDefinition(decl); + // Add the name field + selfStructBody.pr("char *_lf_name;"); + int reactionIndex = 0; + for (Reaction reaction : ASTUtils.allReactions(reactor)) { + // Create a PyObject for each reaction + selfStructBody.pr("PyObject* "+PythonReactionGenerator.generateCPythonReactionFunctionName(reactionIndex)+";"); + if (reaction.getDeadline() != null) { + selfStructBody.pr("PyObject* "+PythonReactionGenerator.generateCPythonDeadlineFunctionName(reactionIndex)+";"); + } + reactionIndex++; + } + } + + /** + * Write a Dockerfile for the current federate as given by filename. + * The file will go into src-gen/filename.Dockerfile. + * If there is no main reactor, then no Dockerfile will be generated + * (it wouldn't be very useful). + * @param The directory where the docker compose file is generated. + * @param The name of the docker file. + * @param The name of the federate. + */ + @Override + public void writeDockerFile(File dockerComposeDir, String dockerFileName, String federateName) { + if (mainDef == null) { + return; + } + Path srcGenPath = fileConfig.getSrcGenPath(); + String dockerFile = srcGenPath + File.separator + dockerFileName; + CodeBuilder contents = new CodeBuilder(); + contents.pr(PythonDockerGenerator.generateDockerFileContent(topLevelName, srcGenPath)); + try { + // If a dockerfile exists, remove it. + Files.deleteIfExists(srcGenPath.resolve(dockerFileName)); + contents.writeToFile(dockerFile); + } catch (IOException e) { + Exceptions.sneakyThrow(e); + } + System.out.println(getDockerBuildCommand(dockerFile, dockerComposeDir, federateName)); + } + + private static String addDoubleQuotes(String str) { + return "\""+str+"\""; + } + + private static String generateMacroEntry(String key, String val) { + return "(" + addDoubleQuotes(key) + ", " + addDoubleQuotes(val) + ")"; + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonGenerator.xtend b/org.lflang/src/org/lflang/generator/python/PythonGenerator.xtend deleted file mode 100644 index 16be2301ac..0000000000 --- a/org.lflang/src/org/lflang/generator/python/PythonGenerator.xtend +++ /dev/null @@ -1,2170 +0,0 @@ -/* Generator for the Python target. */ - -/************* - * Copyright (c) 2022, The University of California at Berkeley. - * Copyright (c) 2022, The University of Texas at Dallas. - - * 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.python - -import java.io.File -import java.nio.file.Path -import java.util.ArrayList -import java.util.HashMap -import java.util.HashSet -import java.util.LinkedHashSet -import java.util.LinkedList -import java.util.List -import org.eclipse.emf.ecore.resource.Resource -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 -import org.lflang.TargetProperty.CoordinationType -import org.lflang.federated.FedFileConfig -import org.lflang.federated.FederateInstance -import org.lflang.federated.PythonGeneratorExtension -import org.lflang.federated.launcher.FedPyLauncher -import org.lflang.federated.serialization.FedNativePythonSerialization -import org.lflang.federated.serialization.SupportedSerializers -import org.lflang.generator.CodeBuilder -import org.lflang.generator.CodeMap -import org.lflang.generator.GeneratorResult -import org.lflang.generator.IntegratedBuilder -import org.lflang.generator.JavaGeneratorUtils -import org.lflang.generator.LFGeneratorContext -import org.lflang.generator.ParameterInstance -import org.lflang.generator.ReactionInstance -import org.lflang.generator.ReactorInstance -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.Assignment -import org.lflang.lf.Delay -import org.lflang.lf.Input -import org.lflang.lf.Instantiation -import org.lflang.lf.Model -import org.lflang.lf.Output -import org.lflang.lf.Parameter -import org.lflang.lf.Port -import org.lflang.lf.Reaction -import org.lflang.lf.Reactor -import org.lflang.lf.ReactorDecl -import org.lflang.lf.StateVar -import org.lflang.lf.TriggerRef -import org.lflang.lf.Value -import org.lflang.lf.VarRef - -import static extension org.lflang.ASTUtils.* -import static extension org.lflang.JavaAstUtils.* -import org.lflang.lf.Assignment -import java.util.LinkedList - -/** - * Generator for Python target. This class generates Python code defining each reactor - * class given in the input .lf file and imported .lf files. - * - * 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 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 } - */ -class PythonGenerator extends CGenerator { - - // Used to add statements that come before reactor classes and user code - var pythonPreamble = new StringBuilder() - - // Used to add module requirements to setup.py (delimited with ,) - var pythonRequiredModules = new StringBuilder() - - var PythonTypes types; - - new(FileConfig fileConfig, ErrorReporter 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. - * This template is defined as - * typedef struct { - * PyObject* value; - * bool is_present; - * int num_destinations; - * FEDERATED_CAPSULE_EXTENSION - * } generic_port_instance_struct; - * - * @see reactor-c-py/lib/pythontarget.h - */ - val generic_port_type = "generic_port_instance_struct" - - /** - * Generic struct for ports with dynamically allocated - * array types (a.k.a. token types) in Lingua Franca. - * This template is defined as - * typedef struct { - * PyObject_HEAD - * PyObject* value; - * bool is_present; - * int num_destinations; - * lf_token_t* token; - * int length; - * FEDERATED_CAPSULE_EXTENSION - * } generic_port_instance_with_token_struct; - * - * @see reactor-c-py/lib/pythontarget.h - */ - val generic_port_type_with_token = "generic_port_instance_with_token_struct" - - /** - * Generic struct for actions. - * This template is defined as - * typedef struct { - * trigger_t* trigger; - * PyObject* value; - * bool is_present; - * bool has_value; - * lf_token_t* token; - * FEDERATED_CAPSULE_EXTENSION - * } generic_action_instance_struct; - * - * @see reactor-c-py/lib/pythontarget.h - */ - val generic_action_type = "generic_action_instance_struct" - - /** Returns the Target enum for this generator */ - override getTarget() { - return Target.Python - } - - val protoNames = new HashSet() - - // ////////////////////////////////////////// - // // Public methods - override printInfo() { - println("Generating code for: " + fileConfig.resource.getURI.toString) - println('******** Mode: ' + fileConfig.context.mode) - println('******** Generated sources: ' + fileConfig.getSrcGenPath) - } - - /** - * Print information about necessary steps to install the supporting - * Python C extension for the generated program. - * - * @note Only needed if no-compile is set to true - */ - def printSetupInfo() { - println(''' - - ##################################### - To compile and install the generated code, do: - - cd «fileConfig.srcGenPath»«File.separator» - python3 -m pip install --force-reinstall . - '''); - } - - /** - * Print information on how to execute the generated program. - */ - def printRunInfo() { - println(''' - - ##################################### - To run the generated program, use: - - python3 «fileConfig.srcGenPath»«File.separator»«topLevelName».py - - ##################################### - '''); - } - - /** - * Print information on how to execute the generated federation. - */ - def printFedRunInfo() { - println(''' - - ##################################### - To run the generated program, run: - - bash «fileConfig.binPath»/«fileConfig.name» - - ##################################### - '''); - } - - override getTargetTypes() { - return types; - } - - // ////////////////////////////////////////// - // // Protected methods - /** - * Override to convert some C types to their - * Python equivalent. - * Examples: - * true/false -> True/False - * @param v A value - * @return A value string in the target language - */ - private def getPythonTargetValue(Value v) { - var String returnValue = ""; - switch (v.toText) { - case "false": returnValue = "False" - case "true": returnValue = "True" - default: returnValue = v.targetValue - } - - // Parameters in Python are always prepended with a 'self.' - // predicate. Therefore, we need to append the returned value - // if it is a parameter. - if (v.parameter !== null) { - returnValue = "self." + returnValue; - } - - return returnValue; - } - - /** - * 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 getPythonInitializerList(StateVar state) { - if (!state.isInitialized) { - return null - } - - var list = new ArrayList(); - - for (i : state?.init) { - if (i.parameter !== null) { - list.add(i.parameter.name) - } else if (state.isOfTimeType) { - list.add(i.targetTime) - } else { - list.add(i.pythonTargetValue) - } - } - return list - } - - /** - * Create a Python tuple for parameter initialization in target code. - * - * @param p The parameter instance to create initializers for - * @return Initialization code - */ - protected def String getPythonInitializer(StateVar state) throws Exception { - if (state.init.size > 1) { - // state variables are initialized as mutable lists - return state.init.join('[', ', ', ']', [it.pythonTargetValue]) - } else if (state.isInitialized) { - return state.init.get(0).getPythonTargetValue - } else { - return "None" - } - - } - - /** - * Return a Python 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 Python reactor instance class of the parents of - * those parameters. - * - * @param p The parameter instance to create initializer for - * @return Initialization code - */ - protected def String getPythonInitializer(ParameterInstance p) { - // 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(PyUtil.reactorRef(p.parent.parent) + "." + value.parameter.name); - } else { - list.add(value.targetTime) - } - } - } else { - for (i : p.parent.initialParameterValue(p.definition)) { - list.add(i.getPythonTargetValue) - } - } - - if (list.size == 1) { - return list.get(0) - } else { - return list.join('(', ', ', ')', [it]) - } - - } - - /** - * Create a Python list for parameter initialization in target code. - * - * @param p The parameter to create initializers for - * @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 - } - } - - /** - * Generate parameters and their respective initialization code for a reaction function - * The initialization code is put at the beginning of the reaction before user code - * @param parameters The parameters used for function definition - * @param inits The initialization code for those paramters - * @param decl Reactor declaration - * @param reaction The reaction to be used to generate parameters for - */ - def generatePythonReactionParametersAndInitializations(StringBuilder parameters, CodeBuilder inits, - ReactorDecl decl, Reaction reaction) { - val reactor = decl.toDefinition - var generatedParams = new LinkedHashSet() - - // Handle triggers - for (TriggerRef trigger : reaction.triggers ?: emptyList) { - if (trigger instanceof VarRef) { - if (trigger.variable instanceof Port) { - if (trigger.variable instanceof Input) { - if ((trigger.variable as Input).isMutable) { - generatedParams.add('''mutable_«trigger.variable.name»''') - - // Create a deep copy - if (JavaAstUtils.isMultiport(trigger.variable as Input)) { - inits. - pr('''«trigger.variable.name» = [Make() for i in range(len(mutable_«trigger.variable.name»))]''') - inits.pr('''for i in range(len(mutable_«trigger.variable.name»)):''') - inits. - pr(''' «trigger.variable.name»[i].value = copy.deepcopy(mutable_«trigger.variable.name»[i].value)''') - } else { - inits.pr('''«trigger.variable.name» = Make()''') - inits. - pr('''«trigger.variable.name».value = copy.deepcopy(mutable_«trigger.variable.name».value)''') - } - } else { - generatedParams.add(trigger.variable.name) - } - } else { - // Handle contained reactors' ports - generatedParams.add('''«trigger.container.name»_«trigger.variable.name»''') - generatePythonPortVariableInReaction(trigger, inits) - } - - } else if (trigger.variable instanceof Action) { - generatedParams.add(trigger.variable.name) - } - } - } - - // Handle non-triggering inputs - if (reaction.triggers === null || reaction.triggers.size === 0) { - for (input : reactor.inputs ?: emptyList) { - generatedParams.add(input.name) - if (input.isMutable) { - // Create a deep copy - inits.pr('''«input.name» = copy.deepcopy(«input.name»)''') - } - } - } - for (src : reaction.sources ?: emptyList) { - if (src.variable instanceof Output) { - // Output of a contained reactor - generatedParams.add('''«src.container.name»_«src.variable.name»''') - generatePythonPortVariableInReaction(src, inits) - } else { - generatedParams.add(src.variable.name) - if (src.variable instanceof Input) { - if ((src.variable as Input).isMutable) { - // Create a deep copy - inits.pr('''«src.variable.name» = copy.deepcopy(«src.variable.name»)''') - } - } - } - } - - // Handle effects - for (effect : reaction.effects ?: emptyList) { - if (effect.variable instanceof Input) { - generatedParams.add('''«effect.container.name»_«effect.variable.name»''') - generatePythonPortVariableInReaction(effect, inits) - } else { - generatedParams.add(effect.variable.name) - if (effect.variable instanceof Port) { - if (JavaAstUtils.isMultiport(effect.variable as Port)) { - // Handle multiports - } - } - } - } - - // Fill out the StrinBuilder parameters - for (s : generatedParams) { - parameters.append(''', «s»''') - } - - } - - /** - * Generate into the specified string builder (inits) the code to - * initialize local variable for port so that it can be used in the body of - * the Python reaction. - * @param port The port to generate code for. - * @param inits The generated code will be put in inits. - */ - protected def CodeBuilder generatePythonPortVariableInReaction(VarRef port, CodeBuilder inits) { - if (port.container.widthSpec !== null) { - // It's a bank - inits.pr(''' - «port.container.name» = [None] * len(«port.container.name»_«port.variable.name») - for i in range(len(«port.container.name»_«port.variable.name»)): - «port.container.name»[i] = Make() - «port.container.name»[i].«port.variable.name» = «port.container.name»_«port.variable.name»[i] - ''') - - } else { - inits.pr('''«port.container.name» = Make''') - inits.pr('''«port.container.name».«port.variable.name» = «port.container.name»_«port.variable.name»''') - } - - return inits; - } - - /** - * Handle initialization for state variable - * @param state a state variable - */ - def String getTargetInitializer(StateVar state) { - if (!state.isInitialized) { - return '''None''' - } - - '''«FOR init : state.pythonInitializerList SEPARATOR ", "»«init»«ENDFOR»''' - } - - /** - * Wrapper function for the more elaborate generatePythonReactorClass that keeps track - * of visited reactors to avoid duplicate generation - * @param instance The reactor instance to be generated - * @param pythonClasses The class definition is appended to this code builder - * @param federate The federate instance for the reactor instance - * @param instantiatedClasses A list of visited instances to avoid generating duplicates - */ - def generatePythonReactorClass(ReactorInstance instance, CodeBuilder pythonClasses, FederateInstance federate) { - var instantiatedClasses = new ArrayList() - generatePythonReactorClass(instance, pythonClasses, federate, instantiatedClasses) - } - - /** - * Generate a Python class corresponding to decl - * @param instance The reactor instance to be generated - * @param pythonClasses The class definition is appended to this code builder - * @param federate The federate instance for the reactor instance - * @param instantiatedClasses A list of visited instances to avoid generating duplicates - */ - def void generatePythonReactorClass(ReactorInstance instance, CodeBuilder pythonClasses, - FederateInstance federate, ArrayList instantiatedClasses) { - if (instance !== this.main && !federate.contains(instance)) { - return - } - - // Invalid use of the function - if (instantiatedClasses === null) { - return - } - - val decl = instance.definition.reactorClass - val className = instance.definition.reactorClass.name - - // Do not generate code for delay reactors in Python - if (className.contains(GEN_DELAY_CLASS_NAME)) { - return - } - - if (federate.contains(instance) && !instantiatedClasses.contains(className)) { - - pythonClasses.pr(''' - - # Python class for reactor «className» - class _«className»: - '''); - - // Generate preamble code - pythonClasses.indent() - pythonClasses.pr(''' - - «generatePythonPreamblesForReactor(decl.toDefinition)» - ''') - - val reactor = decl.toDefinition - - // Handle runtime initializations - pythonClasses.pr(''' - def __init__(self, **kwargs): - ''') - - pythonClasses.pr(generateParametersAndStateVariables(decl)) - - var reactionToGenerate = reactor.allReactions - - if (reactor.isFederated) { - // Filter out reactions that are automatically generated in C in the top level federated reactor - reactionToGenerate.removeIf([ - if (!federate.contains(it)) return true; - if (federate.networkReactions.contains(it)) return true - return false - - ]) - } - - var reactionIndex = 0 - for (reaction : reactionToGenerate) { - val reactionParameters = new StringBuilder() // Will contain parameters for the function (e.g., Foo(x,y,z,...) - val inits = new CodeBuilder() // Will contain initialization code for some parameters - generatePythonReactionParametersAndInitializations(reactionParameters, inits, reactor, reaction) - pythonClasses.pr('''def «pythonReactionFunctionName(reactionIndex)»(self«reactionParameters»):''') - pythonClasses.indent() - pythonClasses.pr(inits); - pythonClasses.pr(reaction.code.toText) - pythonClasses.pr('''return 0''') - pythonClasses.pr(""); - pythonClasses.unindent() - - // Now generate code for the deadline violation function, if there is one. - if (reaction.deadline !== null) { - pythonClasses. - pr('''«generateDeadlineFunctionForReaction(reaction, reactionIndex, reactionParameters.toString)»''') - } - - reactionIndex = reactionIndex + 1; - } - - pythonClasses.unindent() - instantiatedClasses.add(className) - } - - for (child : instance.children) { - generatePythonReactorClass(child, pythonClasses, federate, instantiatedClasses) - } - } - - /** - * Generate code that instantiates and initializes parameters and state variables for a reactor 'decl'. - * - * @param decl The reactor declaration - * @return The generated code as a StringBuilder - */ - protected def CodeBuilder generateParametersAndStateVariables(ReactorDecl decl) { - val reactor = decl.toDefinition - var CodeBuilder temporary_code = new CodeBuilder() - - temporary_code.indent(); - - temporary_code.pr('''#Define parameters and their default values - ''') - - for (param : decl.toDefinition.allParameters) { - if (!types.getTargetType(param).equals("PyObject*")) { - // If type is given, use it - temporary_code. - pr('''self._«param.name»:«types.getPythonType(param.inferredType)» = «param.pythonInitializer» - ''') - } else { - // If type is not given, just pass along the initialization - temporary_code.pr('''self._«param.name» = «param.pythonInitializer» - ''') - - } - } - - // Handle parameters that are set in instantiation - temporary_code.pr('''# Handle parameters that are set in instantiation - ''') - temporary_code.pr('''self.__dict__.update(kwargs) - - ''') - - temporary_code.pr('''# Define state variables - ''') - // Next, handle state variables - for (stateVar : reactor.allStateVars) { - if (stateVar.isInitialized) { - // If initialized, pass along the initialization directly if it is present - temporary_code.pr('''self.«stateVar.name» = «stateVar.pythonInitializer» - ''') - } else { - // If neither the type nor the initialization is given, use None - temporary_code.pr('''self.«stateVar.name» = None - ''') - } - } - - - temporary_code.pr(''' - - ''') - - temporary_code.unindent(); - - // Next, create getters for parameters - for (param : decl.toDefinition.allParameters) { - if (!param.name.equals("bank_index")) { - temporary_code.pr('''@property - ''') - temporary_code.pr('''def «param.name»(self): - ''') - temporary_code.pr(''' return self._«param.name» # pylint: disable=no-member - - ''') - } - } - - // Create a special property for bank_index - temporary_code.pr('''@property - ''') - temporary_code.pr('''def bank_index(self): - ''') - temporary_code.pr(''' return self._bank_index # pylint: disable=no-member - - ''') - - return temporary_code; - } - - /** - * Generate the function that is executed whenever the deadline of the reaction - * with the given reaction index is missed - * @param reaction The reaction to generate deadline miss code for - * @param reactionIndex The agreed-upon index of the reaction in the reactor (should match the C generated code) - * @param reactionParameters The parameters to the deadline violation function, which are the same as the reaction function - */ - def generateDeadlineFunctionForReaction(Reaction reaction, int reactionIndex, String reactionParameters) ''' - «val deadlineFunctionName = 'deadline_function_' + reactionIndex» - - def «deadlineFunctionName»(self «reactionParameters»): - «reaction.deadline.code.toText» - return 0 - - ''' - - /** - * Generates preambles defined by user for a given reactor. - * The preamble code is put inside the reactor class. - */ - def generatePythonPreamblesForReactor(Reactor reactor) ''' - «FOR p : reactor.preambles ?: emptyList» - # From the preamble, verbatim: - «p.code.toText» - # End of preamble. - «ENDFOR» - ''' - - /** - * Instantiate classes in Python. - * Instances are always instantiated as a list of className = [_className, _className, ...] depending on the size of the bank. - * If there is no bank or the size is 1, the instance would be generated as className = [_className] - * @param instance The reactor instance to be instantiated - * @param pythonClassesInstantiation The class instantiations are appended to this code builder - * @param federate The federate instance for the reactor instance - */ - def void generatePythonClassInstantiation(ReactorInstance instance, CodeBuilder pythonClassesInstantiation, - FederateInstance federate) { - // If this is not the main reactor and is not in the federate, nothing to do. - if (instance !== this.main && !federate.contains(instance)) { - return - } - - val className = instance.definition.reactorClass.name - - // Do not instantiate delay reactors in Python - if (className.contains(GEN_DELAY_CLASS_NAME)) { - return - } - - if (federate.contains(instance) && instance.width > 0) { - // 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. var reactorClass = instance.definition.reactorClass - var fullName = instance.fullName - pythonClassesInstantiation.pr( ''' - - # Start initializing «fullName» of class «className» - for «PyUtil.bankIndexName(instance)» in range(«instance.width»): - ''') - pythonClassesInstantiation.indent(); - pythonClassesInstantiation.pr(''' - «PyUtil.reactorRef(instance)» = \ - _«className»( - _bank_index = «PyUtil.bankIndex(instance)», - «FOR param : instance.parameters» - «IF !param.name.equals("bank_index")» - _«param.name»=«param.pythonInitializer», - «ENDIF»«ENDFOR» - ) - ''') - } - - for (child : instance.children) { - generatePythonClassInstantiation(child, pythonClassesInstantiation, federate) - } - pythonClassesInstantiation.unindent(); - } - - /** - * Generate code to instantiate a Python list that will hold the Python - * class instance of reactor instance. Will recursively do - * the same for the children of instance as well. - * - * @param instance The reactor instance for which the Python list will be created. - * @param pythonClassesInstantiation StringBuilder to hold the generated code. - * @param federate Will check if instance (or any of its children) belong to - * federate before generating code for them. - */ - def void generateListsToHoldClassInstances( - ReactorInstance instance, - CodeBuilder pythonClassesInstantiation, - FederateInstance federate - ) { - if(federate !== null && !federate.contains(instance)) return; - pythonClassesInstantiation.pr(''' - «PyUtil.reactorRefName(instance)» = [None] * «instance.totalWidth» - ''') - for (child : instance.children) { - generateListsToHoldClassInstances(child, pythonClassesInstantiation, federate); - } - } - - /** - * Generate all Python classes if they have a reaction - * @param federate The federate instance used to generate classes - */ - def generatePythonReactorClasses(FederateInstance federate) { - - var CodeBuilder pythonClasses = new CodeBuilder() - var CodeBuilder pythonClassesInstantiation = new CodeBuilder() - - // Generate reactor classes in Python - this.main.generatePythonReactorClass(pythonClasses, federate) - - // Create empty lists to hold reactor instances - this.main.generateListsToHoldClassInstances(pythonClassesInstantiation, federate) - - // Instantiate generated classes - this.main.generatePythonClassInstantiation(pythonClassesInstantiation, federate) - - '''«pythonClasses» - - ''' + '''# Instantiate classes - ''' + '''«pythonClassesInstantiation» - ''' - } - - /** - * Generate the Python code constructed from reactor classes and user-written classes. - * @return the code body - */ - def generatePythonCode(FederateInstance federate) ''' - # List imported names, but do not use pylint's --extension-pkg-allow-list option - # so that these names will be assumed present without having to compile and install. - from LinguaFranca«topLevelName» import ( # pylint: disable=no-name-in-module - Tag, action_capsule_t, compare_tags, get_current_tag, get_elapsed_logical_time, - get_elapsed_physical_time, get_logical_time, get_microstep, get_physical_time, - get_start_time, port_capsule, port_instance_token, request_stop, schedule_copy, - start - ) - from LinguaFrancaBase.constants import BILLION, FOREVER, NEVER, instant_t, interval_t - from LinguaFrancaBase.functions import ( - DAY, DAYS, HOUR, HOURS, MINUTE, MINUTES, MSEC, MSECS, NSEC, NSECS, SEC, SECS, USEC, - USECS, WEEK, WEEKS - ) - from LinguaFrancaBase.classes import Make - import sys - import copy - - «pythonPreamble.toString» - - «generatePythonReactorClasses(federate)» - - «PythonMainGenerator.generateCode()» - ''' - - /** - * Generate the setup.py required to compile and install the module. - * Currently, the package name is based on filename which does not support sharing the setup.py for multiple .lf files. - * TODO: use an alternative package name (possibly based on folder name) - * - * If the LF program itself is threaded or if tracing is enabled, NUMBER_OF_WORKERS is added as a macro - * so that platform-specific C files will contain the appropriate functions. - */ - def generatePythonSetupFile() ''' - from setuptools import setup, Extension - - linguafranca«topLevelName»module = Extension("LinguaFranca«topLevelName»", - sources = ["«topLevelName».c", «FOR src : targetConfig.compileAdditionalSources SEPARATOR ", "» "«src»"«ENDFOR»], - define_macros=[('MODULE_NAME', 'LinguaFranca«topLevelName»')«IF (targetConfig.threads !== 0 || (targetConfig.tracing !== null))», - ('NUMBER_OF_WORKERS', '«targetConfig.threads»')«ENDIF»]) - - setup(name="LinguaFranca«topLevelName»", version="1.0", - ext_modules = [linguafranca«topLevelName»module], - install_requires=['LinguaFrancaBase' «pythonRequiredModules»],) - ''' - - /** - * Generate the necessary Python files. - * @param federate The federate instance - */ - def generatePythonFiles(FederateInstance federate) { - var file = new File(fileConfig.getSrcGenPath.toFile, topLevelName + ".py") - if (file.exists) { - file.delete - } - // Create the necessary directories - if (!file.getParentFile().exists()) { - file.getParentFile().mkdirs(); - } - val codeMaps = #{file.toPath -> CodeMap.fromGeneratedCode(generatePythonCode(federate).toString)} - JavaGeneratorUtils.writeToFile(codeMaps.get(file.toPath).generatedCode, file.absolutePath) - - val setupPath = fileConfig.getSrcGenPath.resolve("setup.py") - // Handle Python setup - System.out.println("Generating setup file to " + setupPath) - file = setupPath.toFile - if (file.exists) { - // Append - file.delete - } - - // Create the setup file - JavaGeneratorUtils.writeToFile(generatePythonSetupFile, setupPath.toString) - - return codeMaps - } - - /** - * Execute the command that compiles and installs the current Python module - */ - def pythonCompileCode(LFGeneratorContext context) { - // if we found the compile command, we will also find the install command - val installCmd = commandFactory.createCommand( - '''python3''', #["-m", "pip", "install", "--force-reinstall", "."], fileConfig.srcGenPath) - - if (installCmd === null) { - errorReporter.reportError( - "The Python target requires Python >= 3.6, pip >= 20.0.2, and setuptools >= 45.2.0-1 to compile the generated code. " + - "Auto-compiling can be disabled using the \"no-compile: true\" target property.") - return - } - - // Set compile time environment variables - installCmd.setEnvironmentVariable("CC", targetConfig.compiler) // Use gcc as the compiler - installCmd.setEnvironmentVariable("LDFLAGS", targetConfig.linkerFlags) // The linker complains about including pythontarget.h twice (once in the generated code and once in pythontarget.c) - // To avoid this, we force the linker to allow multiple definitions. Duplicate names would still be caught by the - // compiler. - if (installCmd.run(context.cancelIndicator) == 0) { - println("Successfully installed python extension.") - } else { - errorReporter.reportError("Failed to install python extension due to the following errors:\n" + - installCmd.getErrors()) - } - } - - /** - * Generate top-level preambles and #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. - */ - override generatePreamble() { - - 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) { - models.add(this.mainDef.reactorClass.toDefinition.eContainer as Model) - } - for (m : models) { - for (p : m.preambles) { - pythonPreamble.append('''«p.code.toText» - ''') - } - } - - code.pr(CGenerator.defineLogLevel(this)) - - if (isFederated) { - // FIXME: Instead of checking - // #ifdef FEDERATED, we could - // use #if (NUMBER_OF_FEDERATES > 1) - // To me, the former is more accurate. - code.pr(''' - #define FEDERATED - ''') - if (targetConfig.coordination === CoordinationType.CENTRALIZED) { - // The coordination is centralized. - code.pr(''' - #define FEDERATED_CENTRALIZED - ''') - } else if (targetConfig.coordination === CoordinationType.DECENTRALIZED) { - // The coordination is decentralized - code.pr(''' - #define FEDERATED_DECENTRALIZED - ''') - } - } - - // Handle target parameters. - // First, if there are federates, then ensure that threading is enabled. - if (isFederated) { - 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; - } - } - } - - includeTargetLanguageHeaders() - - code.pr("#include \"core/mixed_radix.h\""); - - code.pr('#define NUMBER_OF_FEDERATES ' + federates.size); - - // Handle target parameters. - // First, if there are federates, then ensure that threading is enabled. - if (targetConfig.threads === 0 && isFederated) { - targetConfig.threads = 1 - } - - super.includeTargetLanguageSourceFiles() - - super.parseTargetParameters() - } - - /** - * Add necessary code to the source and necessary build supports to - * enable the requested serializations in 'enabledSerializations' - */ - override enableSupportForSerializationIfApplicable(CancelIndicator cancelIndicator) { - if (!targetConfig.protoFiles.isNullOrEmpty) { - // Enable support for proto serialization - enabledSerializers.add(SupportedSerializers.PROTO) - } - for (serialization : enabledSerializers) { - switch (serialization) { - case NATIVE: { - val pickler = new FedNativePythonSerialization(); - code.pr(pickler.generatePreambleForSupport.toString); - } - case PROTO: { - // Handle .proto files. - for (name : targetConfig.protoFiles) { - this.processProtoFile(name, cancelIndicator) - val dotIndex = name.lastIndexOf('.') - var rootFilename = name - if (dotIndex > 0) { - rootFilename = name.substring(0, dotIndex) - } - pythonPreamble.append(''' - import «rootFilename»_pb2 as «rootFilename» - ''') - protoNames.add(rootFilename) - } - } - case ROS2: { - // FIXME: Not supported yet - } - } - } - } - - /** - * Process a given .proto file. - * - * Run, if possible, the proto-c protocol buffer code generator to produce - * the required .h and .c files. - * @param filename Name of the file to process. - */ - override processProtoFile(String filename, CancelIndicator cancelIndicator) { - val protoc = commandFactory.createCommand("protoc", - #['''--python_out=«this.fileConfig.getSrcGenPath»''', filename], fileConfig.srcPath) - // val protoc = createCommand("protoc", #['''--python_out=src-gen/«topLevelName»''', topLevelName], codeGenConfig.outPath) - if (protoc === null) { - errorReporter.reportError("Processing .proto files requires libprotoc >= 3.6.1") - return - } - val returnCode = protoc.run(cancelIndicator) - if (returnCode == 0) { - pythonRequiredModules.append(''', 'google-api-python-client' ''') - } else { - errorReporter.reportError("protoc returns error code " + returnCode) - } - } - - /** - * Generate code for the body of a reaction that handles the - * action that is triggered by receiving a message from a remote - * federate. - * @param action The action. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The ID of the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param receivingFed The destination federate. - * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. - * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. - * @param type The type. - * @param isPhysical Indicates whether or not the connection is physical - * @param serializer The serializer used on the connection. - */ - override generateNetworkReceiverBody( - Action action, - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - FederateInstance receivingFed, - int receivingBankIndex, - int receivingChannelIndex, - InferredType type, - boolean isPhysical, - SupportedSerializers serializer - ) { - var result = new StringBuilder(); - - // We currently have no way to mark a reaction "unordered" - // in the AST, so we use a magic string at the start of the body. - result.append("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); - - result.append(''' - // Acquire the GIL (Global Interpreter Lock) to be able to call Python APIs. - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - ''') - result.append(PythonGeneratorExtension.generateNetworkReceiverBody( - action, - sendingPort, - receivingPort, - receivingPortID, - sendingFed, - receivingFed, - receivingBankIndex, - receivingChannelIndex, - type, - isPhysical, - serializer, - this - )); - result.append(''' - /* Release the thread. No Python API allowed beyond this point. */ - PyGILState_Release(gstate); - '''); - return result.toString(); - } - - /** - * Generate code for the body of a reaction that handles an output - * that is to be sent over the network. - * @param sendingPort The output port providing the data to send. - * @param receivingPort The variable reference to the destination port. - * @param receivingPortID The ID of the destination port. - * @param sendingFed The sending federate. - * @param sendingBankIndex The bank index of the sending federate, if it is a bank. - * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. - * @param receivingFed The destination federate. - * @param type The type. - * @param isPhysical Indicates whether the connection is physical or not - * @param delay The delay value imposed on the connection using after - * @param serializer The serializer used on the connection. - */ - override generateNetworkSenderBody( - VarRef sendingPort, - VarRef receivingPort, - int receivingPortID, - FederateInstance sendingFed, - int sendingBankIndex, - int sendingChannelIndex, - FederateInstance receivingFed, - InferredType type, - boolean isPhysical, - Delay delay, - SupportedSerializers serializer - ) { - var result = new StringBuilder(); - - // We currently have no way to mark a reaction "unordered" - // in the AST, so we use a magic string at the start of the body. - result.append("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); - - result.append(''' - // Acquire the GIL (Global Interpreter Lock) to be able to call Python APIs. - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - ''') - result.append(PythonGeneratorExtension.generateNetworkSenderBody( - sendingPort, - receivingPort, - receivingPortID, - sendingFed, - sendingBankIndex, - sendingChannelIndex, - receivingFed, - type, - isPhysical, - delay, - serializer, - this - )); - result.append(''' - /* Release the thread. No Python API allowed beyond this point. */ - PyGILState_Release(gstate); - '''); - return result.toString(); - } - - /** - * Create a launcher script that executes all the federates and the RTI. - * - * @param coreFiles The files from the core directory that must be - * copied to the remote machines. - */ - override createFederatedLauncher(ArrayList coreFiles) { - val launcher = new FedPyLauncher( - targetConfig, - fileConfig, - errorReporter - ); - launcher.createLauncher( - coreFiles, - federates, - federationRTIProperties - ); - } - - /** - * Generate the aliases for inputs, outputs, and struct type definitions for - * actions of the specified reactor in the specified federate. - * @param reactor The parsed reactor data structure. - * @param federate A federate name, or null to unconditionally generate. - */ - override generateAuxiliaryStructs( - ReactorDecl decl, - FederateInstance federate - ) { - val reactor = decl.toDefinition - // First, handle inputs. - for (input : reactor.allInputs) { - if (federate === null || federate.contains(input as Port)) { - if (input.inferredType.isTokenType) { - code.pr(input, ''' - typedef «generic_port_type_with_token» «variableStructType(input, decl)»; - ''') - } else { - code.pr(input, ''' - typedef «generic_port_type» «variableStructType(input, decl)»; - ''') - } - - } - - } - // Next, handle outputs. - for (output : reactor.allOutputs) { - if (federate === null || federate.contains(output as Port)) { - if (output.inferredType.isTokenType) { - code.pr(output, ''' - typedef «generic_port_type_with_token» «variableStructType(output, decl)»; - ''') - } else { - code.pr(output, ''' - typedef «generic_port_type» «variableStructType(output, decl)»; - ''') - } - - } - } - // Finally, handle actions. - for (action : reactor.allActions) { - if (federate === null || federate.contains(action)) { - code.pr(action, ''' - typedef «generic_action_type» «variableStructType(action, decl)»; - ''') - } - - } - } - - /** - * For the specified action, return a declaration for action struct to - * contain the value of the action. - * This will return an empty string for an action with no type. - * @param action The action. - * @return A string providing the value field of the action struct. - */ - override valueDeclaration(Action action) { - return "PyObject* value;" - } - - /** Add necessary include files specific to the target language. - * Note. The core files always need to be (and will be) copied - * uniformly across all target languages. - */ - override includeTargetLanguageHeaders() { - code.pr('''#define _LF_GARBAGE_COLLECTED''') - if (targetConfig.tracing !== null) { - var filename = ""; - if (targetConfig.tracing.traceFileName !== null) { - filename = targetConfig.tracing.traceFileName; - } - code.pr('#define LINGUA_FRANCA_TRACE ' + filename) - } - - code.pr('#include "pythontarget.c"') - if (targetConfig.tracing !== null) { - code.pr('#include "core/trace.c"') - } - } - - /** - * Return true if the host operating system is compatible and - * otherwise report an error and return false. - */ - override isOSCompatible() { - if (JavaGeneratorUtils.isHostWindows) { - if (isFederated) { - errorReporter.reportError( - "Federated LF programs with a Python target are currently not supported on Windows. Exiting code generation." - ) - // Return to avoid compiler errors - return false - } - } - return true; - } - - /** Generate C code from the Lingua Franca model contained by the - * specified resource. This is the main entry point for code - * generation. - * @param resource The resource containing the source code. - * @param context Context relating to invocation of the code generator. - */ - override void doGenerate(Resource resource, LFGeneratorContext context) { - - // If there are federates, assign the number of threads in the CGenerator to 1 - if (isFederated) { - targetConfig.threads = 1; - } - - // Prevent the CGenerator from compiling the C code. - // The PythonGenerator will compiler it. - val compileStatus = targetConfig.noCompile; - targetConfig.noCompile = true; - targetConfig.useCmake = false; // Force disable the CMake because - // it interferes with the Python target functionality - val cGeneratedPercentProgress = (IntegratedBuilder.VALIDATED_PERCENT_PROGRESS + 100) / 2 - super.doGenerate(resource, new SubContext( - context, - IntegratedBuilder.VALIDATED_PERCENT_PROGRESS, - cGeneratedPercentProgress - )) - val compilingFederatesContext = new SubContext(context, cGeneratedPercentProgress, 100) - - targetConfig.noCompile = compileStatus - - if (errorsOccurred) { - context.unsuccessfulFinish() - return; - } - - var baseFileName = topLevelName - // Keep a separate file config for each federate - val oldFileConfig = fileConfig; - var federateCount = 0; - val codeMaps = new HashMap - for (federate : federates) { - federateCount++ - if (isFederated) { - topLevelName = baseFileName + '_' + federate.name - fileConfig = new FedFileConfig(fileConfig, federate.name); - } - // Don't generate code if there is no main reactor - if (this.main !== null) { - val codeMapsForFederate = generatePythonFiles(federate) - codeMaps.putAll(codeMapsForFederate) - if (!targetConfig.noCompile) { - compilingFederatesContext.reportProgress( - String.format("Validating %d/%d sets of generated files...", federateCount, federates.size()), - 100 * federateCount / federates.size() - ) - // If there are no federates, compile and install the generated code - 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()), - 100 * federateCount / federates.size() - ) - pythonCompileCode(context) // Why is this invoked here if the current federate is not a parameter? - } - } else { - printSetupInfo(); - } - - if (!isFederated) { - printRunInfo(); - } - } - fileConfig = oldFileConfig; - } - if (isFederated) { - printFedRunInfo(); - } - // Restore filename - topLevelName = baseFileName - if (errorReporter.getErrorsOccurred()) { - context.unsuccessfulFinish() - } else if (!isFederated) { - context.finish(GeneratorResult.Status.COMPILED, '''«topLevelName».py''', fileConfig.srcGenPath, fileConfig, - codeMaps, "python3") - } else { - context.finish(GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig.binPath, fileConfig, codeMaps, - "bash") - } - } - - /** - * Copy Python specific target code to the src-gen directory - * Also, copy all files listed in the target property `files` into the - * src-gen folder of the main .lf file. - * - * @param targetConfig The targetConfig to read the `files` from. - * @param fileConfig The fileConfig used to make the copy and resolve paths. - */ - override copyUserFiles(TargetConfig targetConfig, FileConfig fileConfig) { - super.copyUserFiles(targetConfig, fileConfig); - // Copy the required target language files into the target file system. - // This will also overwrite previous versions. - fileConfig.copyFileFromClassPath( - "/lib/py/reactor-c-py/include/pythontarget.h", - fileConfig.getSrcGenPath.resolve("pythontarget.h").toString - ) - fileConfig.copyFileFromClassPath( - "/lib/py/reactor-c-py/lib/pythontarget.c", - fileConfig.getSrcGenPath.resolve("pythontarget.c").toString - ) - fileConfig.copyFileFromClassPath( - "/lib/c/reactor-c/include/ctarget.h", - fileConfig.getSrcGenPath.resolve("ctarget.h").toString - ) - } - - /** Return the function name in Python - * @param reactor The reactor - * @param reactionIndex The reaction index. - * @return The function name for the reaction. - */ - def pythonReactionFunctionName(int reactionIndex) { - "reaction_function_" + reactionIndex - } - - /** - * Generate code for the body of a reaction that takes an input and - * schedules an action with the value of that input. - * @param action The action to schedule - * @param port The port to read from - */ - override generateDelayBody(Action action, VarRef 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) { - ''' - if («ref»->is_present) { - // Put the whole token on the event queue, not just the payload. - // This way, the length and element_size are transported. - schedule_token(«action.name», 0, «ref»->token); - } - ''' - } else { - ''' - // Create a token. - #if NUMBER_OF_WORKERS > 0 - // Need to lock the mutex first. - lf_mutex_lock(&mutex); - #endif - lf_token_t* t = create_token(sizeof(PyObject*)); - #if NUMBER_OF_WORKERS > 0 - lf_mutex_unlock(&mutex); - #endif - t->value = self->_lf_«ref»->value; - t->length = 1; // Length is 1 - - // Pass the token along - schedule_token(«action.name», 0, t); - ''' - } - } - - /** - * Generate code for the body of a reaction that is triggered by the - * given action and writes its value to the given port. This realizes - * the receiving end of a logical delay specified with the 'after' - * keyword. - * @param action The action that triggers the reaction - * @param port The port to write to. - */ - override generateForwardBody(Action action, VarRef port) { - val outputName = JavaAstUtils.generateVarRef(port) - if (action.inferredType.isTokenType) { - super.generateForwardBody(action, port) - } else { - ''' - SET(«outputName», «action.name»->token->value); - ''' - } - } - - - /** - * Generate necessary Python-specific initialization code for reaction that belongs to reactor - * decl. - * - * @param reaction The reaction to generate Python-specific initialization for. - * @param decl The reactor to which reaction belongs to. - * @param pyObjectDescriptor For each port object created, a Python-specific descriptor will be added to this that - * then can be used as an argument to Py_BuildValue - * (@see docs.python.org/3/c-api). - * @param pyObjects A "," delimited list of expressions that would be (or result in a creation of) a PyObject. - */ - protected def void generatePythonInitializationForReaction( - Reaction reaction, - ReactorDecl decl, - StringBuilder pyObjectDescriptor, - StringBuilder pyObjects - ) { - var actionsAsTriggers = new LinkedHashSet(); - val Reactor reactor = decl.toDefinition; - - // Next, add the triggers (input and actions; timers are not needed). - // TODO: handle triggers - for (TriggerRef trigger : reaction.triggers ?: emptyList) { - if (trigger instanceof VarRef) { - if (trigger.variable instanceof Port) { - generatePortVariablesToSendToPythonReaction(pyObjectDescriptor, pyObjects, trigger, decl) - } else if (trigger.variable instanceof Action) { - actionsAsTriggers.add(trigger.variable as Action) - generateActionVariableToSendToPythonReaction(pyObjectDescriptor, pyObjects, - trigger.variable as Action, decl) - } - } - } - if (reaction.triggers === null || reaction.triggers.size === 0) { - // No triggers are given, which means react to any input. - // Declare an argument for every input. - // NOTE: this does not include contained outputs. - for (input : reactor.inputs) { - generateInputVariablesToSendToPythonReaction(pyObjectDescriptor, pyObjects, input, decl) - } - } - - // Next add non-triggering inputs. - for (VarRef src : reaction.sources ?: emptyList) { - if (src.variable instanceof Port) { - generatePortVariablesToSendToPythonReaction(pyObjectDescriptor, pyObjects, src, decl) - } else if (src.variable instanceof Action) { - // TODO: handle actions - actionsAsTriggers.add(src.variable as Action) - generateActionVariableToSendToPythonReaction(pyObjectDescriptor, pyObjects, src.variable as Action, - decl) - } - } - - // Next, handle effects - if (reaction.effects !== null) { - for (effect : reaction.effects) { - if (effect.variable instanceof Action) { - // It is an action, not an output. - // If it has already appeared as trigger, do not redefine it. - if (!actionsAsTriggers.contains(effect.variable)) { - generateActionVariableToSendToPythonReaction(pyObjectDescriptor, pyObjects, - effect.variable as Action, decl) - } - } else { - if (effect.variable instanceof Output) { - generateOutputVariablesToSendToPythonReaction(pyObjectDescriptor, pyObjects, - effect.variable as Output, decl) - } else if (effect.variable instanceof Input) { - // It is the input of a contained reactor. - generateVariablesForSendingToContainedReactors(pyObjectDescriptor, pyObjects, effect.container, - effect.variable as Input, decl) - } else { - errorReporter.reportError( - reaction, - "In generateReaction(): " + effect.variable.name + " is neither an input nor an output." - ) - } - - } - } - } - } - - /** Generate a reaction function definition for a reactor. - * This function has a single argument that is a void* pointing to - * a struct that contains parameters, state variables, inputs (triggering or not), - * actions (triggering or produced), and outputs. - * @param reaction The reaction. - * @param reactor The reactor. - * @param reactionIndex The position of the reaction within the reactor. - */ - override generateReaction(Reaction reaction, ReactorDecl decl, int reactionIndex) { - - val reactor = decl.toDefinition - - // 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)) { - super.generateReaction(reaction, decl, reactionIndex) - return - } - - // Contains "O" characters. The number of these characters depend on the number of inputs to the reaction - val StringBuilder pyObjectDescriptor = new StringBuilder() - - // Contains the actual comma separated list of inputs to the reaction of type generic_port_instance_struct or generic_port_instance_with_token_struct. - // Each input must be cast to (PyObject *) - val StringBuilder pyObjects = new StringBuilder() - - // Create a unique function name for each reaction. - val functionName = reactionFunctionName(decl, reactionIndex) - - // Generate the function name in Python - val pythonFunctionName = pythonReactionFunctionName(reactionIndex); - - code.pr('void ' + functionName + '(void* instance_args) {') - code.indent() - - // First, generate C initializations - super.generateInitializationForReaction("", reaction, decl, reactionIndex) - - code.prSourceLineNumber(reaction.code) - - // Ensure that GIL is locked - code.pr(''' - // Acquire the GIL (Global Interpreter Lock) to be able to call Python APIs. - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - ''') - - // Generate Python-related initializations - generatePythonInitializationForReaction(reaction, decl, pyObjectDescriptor, pyObjects) - - // Call the Python reaction - code.pr(''' - - DEBUG_PRINT("Calling reaction function «decl.name».«pythonFunctionName»"); - PyObject *rValue = PyObject_CallObject( - self->_lf_py_reaction_function_«reactionIndex», - Py_BuildValue("(«pyObjectDescriptor»)" «pyObjects») - ); - if (rValue == NULL) { - error_print("FATAL: Calling reaction «decl.name».«pythonFunctionName» failed."); - if (PyErr_Occurred()) { - PyErr_PrintEx(0); - PyErr_Clear(); // this will reset the error indicator so we can run Python code again - } - /* Release the thread. No Python API allowed beyond this point. */ - PyGILState_Release(gstate); - Py_FinalizeEx(); - exit(1); - } - - /* Release the thread. No Python API allowed beyond this point. */ - PyGILState_Release(gstate); - ''') - - code.unindent() - code.pr("}") - - // Now generate code for the deadline violation function, if there is one. - if (reaction.deadline !== null) { - // The following name has to match the choice in generateReactionInstances - val deadlineFunctionName = decl.name.toLowerCase + '_deadline_function' + reactionIndex - - code.pr('void ' + deadlineFunctionName + '(void* instance_args) {') - code.indent(); - - super.generateInitializationForReaction("", reaction, decl, reactionIndex) - - code.pr(''' - // Acquire the GIL (Global Interpreter Lock) to be able to call Python APIs. - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - - DEBUG_PRINT("Calling deadline function «decl.name».«deadlineFunctionName»"); - PyObject *rValue = PyObject_CallObject( - self->_lf_py_deadline_function_«reactionIndex», - Py_BuildValue("(«pyObjectDescriptor»)" «pyObjects») - ); - if (rValue == NULL) { - error_print("FATAL: Calling reaction «decl.name».«deadlineFunctionName» failed.\n"); - if (rValue == NULL) { - if (PyErr_Occurred()) { - PyErr_PrintEx(0); - PyErr_Clear(); // this will reset the error indicator so we can run Python code again - } - } - /* Release the thread. No Python API allowed beyond this point. */ - PyGILState_Release(gstate); - Py_FinalizeEx(); - exit(1); - } - - /* Release the thread. No Python API allowed beyond this point. */ - PyGILState_Release(gstate); - ''') - - code.unindent() - code.pr("}") - } - } - - /** - * Generate code for parameter variables of a reactor in the form "parameter.type parameter.name;" - * - * FIXME: for now we assume all parameters are int. This is to circumvent the issue of parameterized - * port widths for now. - * - * @param reactor The reactor. - * @param builder The place that the generated code is written to. - * @return - */ - override generateParametersForReactor(CodeBuilder builder, Reactor reactor) { - for (parameter : reactor.allParameters) { - builder.prSourceLineNumber(parameter) - // Assume all parameters are integers - builder.pr('''int «parameter.name» ;'''); - } - } - - /** - * Generate code that initializes the state variables for a given instance. - * Unlike parameters, state variables are uniformly initialized for all instances - * of the same reactor. This task is left to Python code to allow for more liberal - * state variable assignments. - * @param instance The reactor class instance - * @return Initialization code fore state variables of instance - */ - override generateStateVariableInitializations(ReactorInstance instance) { - // Do nothing - } - - /** - * Generate runtime initialization code in C for parameters of a given reactor instance. - * All parameters are also initialized in Python code, but those parameters that are - * used as width must be also initialized in C. - * - * FIXME: Here, we use a hack: we attempt to convert the parameter initialization to an integer. - * If it succeeds, we proceed with the C initialization. If it fails, we defer initialization - * to Python. - * - * Generate runtime initialization code for parameters of a given reactor instance - * @param instance The reactor instance. - */ - override void generateParameterInitialization(ReactorInstance instance) { - // Mostly ignore the initialization in C - // The actual initialization will be done in Python - // Except if the parameter is a width (an integer) - // 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 = CUtil.reactorRef(instance) - for (parameter : instance.parameters) { - val initializer = parameter.getInitializer - try { - // Attempt to convert it to integer - val number = Integer.parseInt(initializer); - initializeTriggerObjects.pr(''' - «nameOfSelfStruct»->«parameter.name» = «number»; - ''') - } catch (NumberFormatException ex) { - // Ignore initialization in C for this parameter - } - } - } - - /** - * This function is overridden in the Python generator to do nothing. - * The state variables are initialized in Python code directly. - * @param reactor The reactor. - * @param builder The place that the generated code is written to. - * @return - */ - override generateStateVariablesForReactor(CodeBuilder builder, Reactor reactor) { - // Do nothing - } - - /** - * Generates C preambles defined by user for a given reactor - * Since the Python generator expects preambles written in C, - * this function is overridden and does nothing. - * @param reactor The given reactor - */ - override generateUserPreamblesForReactor(Reactor reactor) { - // Do nothing - } - - /** - * Generate code that is executed while the reactor instance is being initialized. - * This wraps the reaction functions in a Python function. - * @param instance The reactor instance. - * @param reactions The reactions of this instance. - */ - override void generateReactorInstanceExtension( - ReactorInstance instance, - Iterable reactions - ) { - 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 - if (reactor.name.contains(GEN_DELAY_CLASS_NAME) || - ((instance.definition.reactorClass === this.mainDef?.reactorClass) && reactor.isFederated)) { - return - } - - // Initialize the name field to the unique name of the instance - initializeTriggerObjects.pr(''' - «nameOfSelfStruct»->_lf_name = "«instance.uniqueID»_lf"; - '''); - - for (reaction : reactions) { - val pythonFunctionName = pythonReactionFunctionName(reaction.index) - // Create a PyObject for each reaction - initializeTriggerObjects.pr(''' - «nameOfSelfStruct»->_lf_py_reaction_function_«reaction.index» = - get_python_function("__main__", - «nameOfSelfStruct»->_lf_name, - «CUtil.runtimeIndex(instance)», - "«pythonFunctionName»"); - ''') - - if (reaction.definition.deadline !== null) { - initializeTriggerObjects.pr(''' - «nameOfSelfStruct»->_lf_py_deadline_function_«reaction.index» = - get_python_function("__main__", - «nameOfSelfStruct»->_lf_name, - «CUtil.runtimeIndex(instance)», - "deadline_function_«reaction.index»"); - ''') - } - } - } - - /** - * This function is provided to allow extensions of the CGenerator to append the structure of the self struct - * @param selfStructBody The body of the self struct - * @param decl The reactor declaration for the self struct - * @param instance The current federate instance - * @param constructorCode Code that is executed when the reactor is instantiated - */ - override generateSelfStructExtension( - CodeBuilder selfStructBody, - ReactorDecl decl, - FederateInstance instance, - CodeBuilder constructorCode - ) { - val reactor = decl.toDefinition - // Add the name field - selfStructBody.pr('''char *_lf_name; - '''); - - var reactionIndex = 0 - for (reaction : reactor.allReactions) { - // Create a PyObject for each reaction - selfStructBody.pr('''PyObject* _lf_py_reaction_function_«reactionIndex»;''') - - if (reaction.deadline !== null) { - selfStructBody.pr('''PyObject* _lf_py_deadline_function_«reactionIndex»;''') - } - - reactionIndex++ - } - } - - /** - * Generate code to convert C actions to Python action capsules - * @see pythontarget.h. - * @param pyObjectDescriptor A string representing a list of Python format types (e.g., "O") that - * can be passed to Py_BuildValue. The object type for the converted action will - * be appended to this string (e.g., "OO"). - * @param pyObjects A string containing a list of comma-separated expressions that will create the - * action capsules. - * @param action The action itself. - * @param decl The reactor decl that contains the action. - */ - def generateActionVariableToSendToPythonReaction(StringBuilder pyObjectDescriptor, StringBuilder pyObjects, - Action action, ReactorDecl decl) { - pyObjectDescriptor.append("O") - // Values passed to an action are always stored in the token->value. - // However, sometimes token might not be initialized. Therefore, this function has an internal check for NULL in case token is not initialized. - pyObjects.append(''', convert_C_action_to_py(«action.name»)''') - } - - /** - * Generate code to convert C ports to Python ports capsules (@see pythontarget.h). - * - * The port may be an input of the reactor or an output of a contained reactor. - * - * @param pyObjectDescriptor A string representing a list of Python format types (e.g., "O") that - * can be passed to Py_BuildValue. The object type for the converted port will - * be appended to this string (e.g., "OO"). - * @param pyObjects A string containing a list of comma-separated expressions that will create the - * port capsules. - * @param port The port itself. - * @param decl The reactor decl that contains the port. - */ - private def generatePortVariablesToSendToPythonReaction( - StringBuilder pyObjectDescriptor, - StringBuilder pyObjects, - VarRef port, - ReactorDecl decl - ) { - if (port.variable instanceof Input) { - generateInputVariablesToSendToPythonReaction(pyObjectDescriptor, pyObjects, port.variable as Input, decl) - } else { - pyObjectDescriptor.append("O") - val output = port.variable as Output - val reactorName = port.container.name - // port is an output of a contained reactor. - if (port.container.widthSpec !== null) { - var String widthSpec = "-2" - if (JavaAstUtils.isMultiport(port.variable as Port)) { - widthSpec = '''self->_lf_«reactorName»[i].«output.name»_width''' - } - // Output is in a bank. - // Create a Python list - generatePythonListForContainedBank(reactorName, output, widthSpec) - pyObjects.append(''', «reactorName»_py_list''') - } else { - var String widthSpec = "-2" - if (JavaAstUtils.isMultiport(port.variable as Port)) { - widthSpec = '''«port.container.name».«port.variable.name»_width''' - } - pyObjects.append(''', convert_C_port_to_py(«reactorName».«port.variable.name», «widthSpec»)''') - } - } - } - - /** - * Generate code that creates a Python list (i.e., []) for contained banks to be passed to Python reactions. - * The Python reaction will then subsequently be able to address each individual bank member of the contained - * bank using an index or an iterator. Each list member will contain the given port - * (which could be a multiport with a width determined by widthSpec). - * - * This is to accommodate reactions like reaction() -> s.out where s is a bank. In this example, - * the generated Python function will have the signature reaction_function_0(self, s_out), where - * s_out is a list of out ports. This will later be turned into the proper s.out format using the - * Python code generated in {@link #generatePythonPortVariableInReaction}. - * - * @param reactorName The name of the bank of reactors (which is the name of the reactor class). - * @param port The port that should be put in the Python list. - * @param widthSpec A string that should be -2 for non-multiports and the width expression for multiports. - */ - protected def void generatePythonListForContainedBank(String reactorName, Port port, String widthSpec) { - code.pr(''' - PyObject* «reactorName»_py_list = PyList_New(«reactorName»_width); - - if(«reactorName»_py_list == NULL) { - error_print("Could not create the list needed for «reactorName»."); - if (PyErr_Occurred()) { - PyErr_PrintEx(0); - PyErr_Clear(); // this will reset the error indicator so we can run Python code again - } - /* Release the thread. No Python API allowed beyond this point. */ - PyGILState_Release(gstate); - Py_FinalizeEx(); - exit(1); - } - - for (int i = 0; i < «reactorName»_width; i++) { - if (PyList_SetItem( - «reactorName»_py_list, - i, - convert_C_port_to_py( - self->_lf_«reactorName»[i].«port.name», - «widthSpec» - ) - ) != 0) { - error_print("Could not add elements to the list for «reactorName»."); - if (PyErr_Occurred()) { - PyErr_PrintEx(0); - PyErr_Clear(); // this will reset the error indicator so we can run Python code again - } - /* Release the thread. No Python API allowed beyond this point. */ - PyGILState_Release(gstate); - Py_FinalizeEx(); - exit(1); - } - } - - ''') - } - - /** Generate into the specified string builder the code to - * send local variables for output ports to a Python reaction function - * from the "self" struct. - * @param builder The string builder into which to write the code. - * @param structs A map from reactor instantiations to a place to write - * struct fields. - * @param output The output port. - * @param decl The reactor declaration. - */ - private def generateOutputVariablesToSendToPythonReaction( - 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. - // 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 - * pass local variables for sending data to an input - * of a contained reaction (e.g. for a deadline violation). - * @param builder The string builder. - * @param definition AST node defining the reactor within which this occurs - * @param input Input of the contained reactor. - */ - private def generateVariablesForSendingToContainedReactors( - StringBuilder pyObjectDescriptor, - StringBuilder pyObjects, - Instantiation definition, - Input input, - ReactorDecl decl - ) { - pyObjectDescriptor.append("O") - - if (definition.widthSpec !== null) { - var String widthSpec = "-2" - if (JavaAstUtils.isMultiport(input)) { - widthSpec = '''self->_lf_«definition.name»[i].«input.name»_width''' - } - // Contained reactor is a bank. - // Create a Python list - generatePythonListForContainedBank(definition.name, input, widthSpec); - pyObjects.append(''', «definition.name»_py_list''') - } - else { - var String widthSpec = "-2" - if (JavaAstUtils.isMultiport(input)) { - widthSpec = '''«definition.name».«input.name»_width''' - } - pyObjects. - append(''', convert_C_port_to_py(«definition.name».«input.name», «widthSpec»)''') - } - } - - /** Generate into the specified string builder the code to - * send local variables for input ports to a Python reaction function - * from the "self" struct. - * @param builder The string builder into which to write the code. - * @param structs A map from reactor instantiations to a place to write - * struct fields. - * @param input The input port. - * @param reactor The reactor. - */ - private def generateInputVariablesToSendToPythonReaction( - StringBuilder pyObjectDescriptor, - StringBuilder pyObjects, - Input input, - ReactorDecl decl - ) { - // Create the local variable whose name matches the input name. - // If the input has not been declared mutable, then this is a pointer - // to the upstream output. Otherwise, it is a copy of the upstream output, - // which nevertheless points to the same token and value (hence, as done - // below, we have to use writable_copy()). There are 8 cases, - // 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 && !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 && !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 && JavaAstUtils.isMultiport(input)) { - // Non-mutable, multiport, primitive. - // TODO: support multiports - pyObjectDescriptor.append("O") - pyObjects.append(''', convert_C_port_to_py(«input.name»,«input.name»_width) ''') - } else { - // Mutable, multiport, primitive type - // TODO: support mutable multiports - pyObjectDescriptor.append("O") - pyObjects.append(''', convert_C_port_to_py(«input.name»,«input.name»_width) ''') - } - } - - /** - * Write a Dockerfile for the current federate as given by filename. - * The file will go into src-gen/filename.Dockerfile. - * If there is no main reactor, then no Dockerfile will be generated - * (it wouldn't be very useful). - * @param The directory where the docker compose file is generated. - * @param The name of the docker file. - * @param The name of the federate. - */ - override writeDockerFile(File dockerComposeDir, String dockerFileName, String federateName) { - var srcGenPath = fileConfig.getSrcGenPath - val dockerFile = srcGenPath + File.separator + dockerFileName - // If a dockerfile exists, remove it. - var file = new File(dockerFile) - if (file.exists) { - file.delete - } - - if (this.mainDef === null) { - return - } - - val OS = System.getProperty("os.name").toLowerCase(); - var dockerComposeCommand = (OS.indexOf("nux") >= 0) ? "docker-compose" : "docker compose" - - val contents = new CodeBuilder() - contents.pr(''' - # Generated docker file for «topLevelName».lf in «srcGenPath». - # For instructions, see: https://github.com/icyphy/lingua-franca/wiki/Containerized-Execution - FROM python:slim - WORKDIR /lingua-franca/«topLevelName» - RUN set -ex && apt-get update && apt-get install -y python3-pip - COPY . src-gen - RUN cd src-gen && python3 setup.py install && cd .. - ENTRYPOINT ["python3", "src-gen/«topLevelName».py"] - ''') - contents.writeToFile(dockerFile) - println('''Dockerfile for «topLevelName» written to ''' + dockerFile) - println(''' - ##################################### - To build the docker image, go to «dockerComposeDir» and run: - - «dockerComposeCommand» build «federateName» - - ##################################### - ''') - } - - /** - * Convert C types to formats used in Py_BuildValue and PyArg_PurseTuple. - * This is unused but will be useful to enable inter-compatibility between - * C and Python reactors. - * @param type C type - */ - def pyBuildValueArgumentType(String type) { - switch (type) { - case "int": "i" - case "string": "s" - case "char": "b" - case "short int": "h" - case "long": "l" - case "unsigned char": "B" - case "unsigned short int": "H" - case "unsigned int": "I" - case "unsigned long": "k" - case "long long": "L" - case "interval_t": "L" - case "unsigned long long": "K" - case "double": "d" - case "float": "f" - case "Py_complex": "D" - case "Py_complex*": "D" - case "Py_Object": "O" - case "Py_Object*": "O" - default: "O" - } - } -} diff --git a/org.lflang/src/org/lflang/generator/python/PythonInfoGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonInfoGenerator.java new file mode 100644 index 0000000000..9f61f2021a --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonInfoGenerator.java @@ -0,0 +1,56 @@ +package org.lflang.generator.python; + +import java.io.File; +import org.lflang.FileConfig; + +public class PythonInfoGenerator { + /** + * Print information about necessary steps to install the supporting + * Python C extension for the generated program. + * + * @note Only needed if no-compile is set to true + */ + public static String generateSetupInfo(FileConfig fileConfig) { + return String.join("\n", + "", + "#####################################", + "To compile and install the generated code, do:", + " ", + " cd "+fileConfig.getSrcGenPath()+File.separator, + " python3 -m pip install --force-reinstall .", + "" + ); + } + + /** + * Print information on how to execute the generated program. + */ + public static String generateRunInfo(FileConfig fileConfig, String topLevelName) { + return String.join("\n", + "", + "#####################################", + "To run the generated program, use:", + " ", + " python3 "+fileConfig.getSrcGenPath()+File.separator+topLevelName+".py", + "", + "#####################################", + "" + ); + } + + /** + * Print information on how to execute the generated federation. + */ + public static String generateFedRunInfo(FileConfig fileConfig) { + return String.join("\n", + "", + "#####################################", + "To run the generated program, run:", + " ", + " bash "+fileConfig.binPath+"/"+fileConfig.name, + "", + "#####################################", + "" + ); + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonNetworkGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonNetworkGenerator.java new file mode 100644 index 0000000000..3c505cc97c --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonNetworkGenerator.java @@ -0,0 +1,122 @@ +package org.lflang.generator.python; + +import org.lflang.federated.FederateInstance; +import org.lflang.federated.PythonGeneratorExtension; +import org.lflang.lf.VarRef; +import org.lflang.lf.Action; +import org.lflang.lf.Delay; +import org.lflang.InferredType; +import org.lflang.TargetConfig; +import org.lflang.TargetProperty.CoordinationType; +import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.ReactionInstance; + + +public class PythonNetworkGenerator { + /** + * Generate code for the body of a reaction that handles the + * action that is triggered by receiving a message from a remote + * federate. + * @param action The action. + * @param sendingPort The output port providing the data to send. + * @param receivingPort The ID of the destination port. + * @param receivingPortID The ID of the destination port. + * @param sendingFed The sending federate. + * @param receivingFed The destination federate. + * @param receivingBankIndex The receiving federate's bank index, if it is in a bank. + * @param receivingChannelIndex The receiving federate's channel index, if it is a multiport. + * @param type The type. + * @param isPhysical Indicates whether or not the connection is physical + * @param serializer The serializer used on the connection. + */ + public static String generateNetworkReceiverBody( + Action action, + VarRef sendingPort, + VarRef receivingPort, + int receivingPortID, + FederateInstance sendingFed, + FederateInstance receivingFed, + int receivingBankIndex, + int receivingChannelIndex, + InferredType type, + boolean isPhysical, + SupportedSerializers serializer + ) { + StringBuilder result = new StringBuilder(); + + // We currently have no way to mark a reaction "unordered" + // in the AST, so we use a magic string at the start of the body. + result.append("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); + + result.append(PyUtil.generateGILAcquireCode() + "\n"); + result.append(PythonGeneratorExtension.generateNetworkReceiverBody( + action, + sendingPort, + receivingPort, + receivingPortID, + sendingFed, + receivingFed, + receivingBankIndex, + receivingChannelIndex, + type, + isPhysical, + serializer + )); + result.append(PyUtil.generateGILReleaseCode() + "\n"); + return result.toString(); + } + + /** + * Generate code for the body of a reaction that handles an output + * that is to be sent over the network. + * @param sendingPort The output port providing the data to send. + * @param receivingPort The variable reference to the destination port. + * @param receivingPortID The ID of the destination port. + * @param sendingFed The sending federate. + * @param sendingBankIndex The bank index of the sending federate, if it is a bank. + * @param sendingChannelIndex The channel index of the sending port, if it is a multiport. + * @param receivingFed The destination federate. + * @param type The type. + * @param isPhysical Indicates whether the connection is physical or not + * @param delay The delay value imposed on the connection using after + * @param serializer The serializer used on the connection. + */ + public static String generateNetworkSenderBody( + VarRef sendingPort, + VarRef receivingPort, + int receivingPortID, + FederateInstance sendingFed, + int sendingBankIndex, + int sendingChannelIndex, + FederateInstance receivingFed, + InferredType type, + boolean isPhysical, + Delay delay, + SupportedSerializers serializer, + CoordinationType coordinationType + ) { + StringBuilder result = new StringBuilder(); + + // We currently have no way to mark a reaction "unordered" + // in the AST, so we use a magic string at the start of the body. + result.append("// " + ReactionInstance.UNORDERED_REACTION_MARKER + "\n"); + + result.append(PyUtil.generateGILAcquireCode() + "\n"); + result.append(PythonGeneratorExtension.generateNetworkSenderBody( + sendingPort, + receivingPort, + receivingPortID, + sendingFed, + sendingBankIndex, + sendingChannelIndex, + receivingFed, + type, + isPhysical, + delay, + serializer, + coordinationType + )); + result.append(PyUtil.generateGILReleaseCode() + "\n"); + return result.toString(); + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonParameterGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonParameterGenerator.java new file mode 100644 index 0000000000..046971f795 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonParameterGenerator.java @@ -0,0 +1,157 @@ +package org.lflang.generator.python; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.base.Objects; + +import org.lflang.ASTUtils; +import org.lflang.JavaAstUtils; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.GeneratorBase; +import org.lflang.generator.ParameterInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.c.CUtil; +import org.lflang.generator.c.CParameterGenerator; +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.Value; +import org.lflang.lf.Reactor; +import org.lflang.lf.Assignment; +import org.lflang.lf.Parameter; + + +public class PythonParameterGenerator { + /** + * Generate Python code that instantiates and initializes parameters for a reactor 'decl'. + * + * @param decl The reactor declaration + * @return The generated code as a StringBuilder + */ + public static String generatePythonInstantiations(ReactorDecl decl, PythonTypes types) { + List lines = new ArrayList<>(); + lines.add("# Define parameters and their default values"); + + for (Parameter param : getAllParameters(decl)) { + if (!types.getTargetType(param).equals("PyObject*")) { + // If type is given, use it + String type = types.getPythonType(JavaAstUtils.getInferredType(param)); + lines.add("self._"+param.getName()+":"+type+" = "+generatePythonInitializer(param)); + } else { + // If type is not given, just pass along the initialization + lines.add("self._"+param.getName()+" = "+generatePythonInitializer(param)); + } + } + // Handle parameters that are set in instantiation + lines.addAll(List.of( + "# Handle parameters that are set in instantiation", + "self.__dict__.update(kwargs)", + "" + )); + return String.join("\n", lines); + } + + /** + * Generate Python code getters for parameters of reactor 'decl'. + * + * @param decl The reactor declaration + * @return The generated code as a StringBuilder + */ + public static String generatePythonGetters(ReactorDecl decl) { + List lines = new ArrayList<>(); + for (Parameter param : getAllParameters(decl)) { + if (!param.getName().equals("bank_index")) { + lines.addAll(List.of( + "@property", + "def "+param.getName()+"(self):", + " return self._"+param.getName()+" # pylint: disable=no-member", + "" + )); + } + } + // Create a special property for bank_index + lines.addAll(List.of( + "@property", + "def bank_index(self):", + " return self._bank_index # pylint: disable=no-member", + "" + )); + return String.join("\n", lines); + } + + /** + * Return a list of all parameters of reactor 'decl'. + * + * @param decl The reactor declaration + * @return The list of all parameters of 'decl' + */ + private static List getAllParameters(ReactorDecl decl) { + return ASTUtils.allParameters(ASTUtils.toDefinition(decl)); + } + + /** + * Create a Python list for parameter initialization in target code. + * + * @param p The parameter to create initializers for + * @return Initialization code + */ + private static String generatePythonInitializer(Parameter p) { + List values = p.getInit().stream().map(PyUtil::getPythonTargetValue).collect(Collectors.toList()); + return values.size() > 1 ? "(" + String.join(", ", values) + ")" : values.get(0); + } + + /** + * Return a Python 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 Python reactor instance class of the parents of + * those parameters. + * + * @param p The parameter instance to create initializer for + * @return Initialization code + */ + public static String generatePythonInitializer(ParameterInstance p) { + // Handle overrides in the instantiation. + // In case there is more than one assignment to this parameter, we need to + // find the last one. + Assignment lastAssignment = getLastAssignment(p); + List list = new LinkedList<>(); + if (lastAssignment != null) { + // The parameter has an assignment. + // Right hand side can be a list. Collect the entries. + for (Value value : lastAssignment.getRhs()) { + if (value.getParameter() != 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(PyUtil.reactorRef(p.getParent().getParent()) + "." + value.getParameter().getName()); + } else { + list.add(GeneratorBase.getTargetTime(value)); + } + } + } else { + for (Value i : p.getParent().initialParameterValue(p.getDefinition())) { + list.add(PyUtil.getPythonTargetValue(i)); + } + } + return list.size() > 1 ? "(" + String.join(", ", list) + ")" : list.get(0); + } + + /** + * Returns the last assignment to "p" if there is one, + * or null if there is no assignment to "p" + * + * @param p The parameter instance to create initializer for + * @return The last assignment of the parameter instance + */ + private static Assignment getLastAssignment(ParameterInstance p) { + Assignment lastAssignment = null; + for (Assignment assignment : p.getParent().getDefinition().getParameters()) { + if (Objects.equal(assignment.getLhs(), p.getDefinition())) { + lastAssignment = assignment; + } + } + return lastAssignment; + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonPortGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonPortGenerator.java new file mode 100644 index 0000000000..75befe9c00 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonPortGenerator.java @@ -0,0 +1,256 @@ +package org.lflang.generator.python; + +import org.lflang.lf.Input; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Output; +import org.lflang.lf.Port; +import org.lflang.lf.Action; +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.VarRef; +import java.util.List; +import org.lflang.JavaAstUtils; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.c.CGenerator; +import static org.lflang.generator.c.CUtil.generateWidthVariable; + +public class PythonPortGenerator { + public static final String NONMULTIPORT_WIDTHSPEC = "-2"; + + /** + * Generate code to convert C actions to Python action capsules + * @see pythontarget.h. + * @param pyObjectDescriptor A string representing a list of Python format types (e.g., "O") that + * can be passed to Py_BuildValue. The object type for the converted action will + * be appended to this string (e.g., "OO"). + * @param pyObjects A string containing a list of comma-separated expressions that will create the + * action capsules. + * @param action The action itself. + * @param decl The reactor decl that contains the action. + */ + public static void generateActionVariableToSendToPythonReaction(List pyObjects, + Action action, ReactorDecl decl) { + // Values passed to an action are always stored in the token->value. + // However, sometimes token might not be initialized. Therefore, this function has an internal check for NULL in case token is not initialized. + pyObjects.add(String.format("convert_C_action_to_py(%s)", action.getName())); + } + + /** + * Generate code to convert C ports to Python ports capsules (@see pythontarget.h). + * + * The port may be an input of the reactor or an output of a contained reactor. + * + * @param pyObjectDescriptor A string representing a list of Python format types (e.g., "O") that + * can be passed to Py_BuildValue. The object type for the converted port will + * be appended to this string (e.g., "OO"). + * @param pyObjects A string containing a list of comma-separated expressions that will create the + * port capsules. + * @param port The port itself. + * @param decl The reactor decl that contains the port. + */ + public static String generatePortVariablesToSendToPythonReaction( + List pyObjects, + VarRef port, + ReactorDecl decl + ) { + if (port.getVariable() instanceof Input) { + generateInputVariablesToSendToPythonReaction(pyObjects, (Input) port.getVariable(), decl); + return ""; + } else { + // port is an output of a contained reactor. + return generateVariablesForSendingToContainedReactors(pyObjects, port.getContainer(), (Port) port.getVariable()); + } + } + + /** Generate into the specified string builder the code to + * send local variables for output ports to a Python reaction function + * from the "self" struct. + * @param builder The string builder into which to write the code. + * @param structs A map from reactor instantiations to a place to write + * struct fields. + * @param output The output port. + * @param decl The reactor declaration. + */ + public static void generateOutputVariablesToSendToPythonReaction( + List pyObjects, + Output output + ) { + // 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)) { + pyObjects.add(generateConvertCPortToPy(output.getName(), NONMULTIPORT_WIDTHSPEC)); + } else { + pyObjects.add(generateConvertCPortToPy(output.getName())); + } + } + + /** Generate into the specified string builder the code to + * send local variables for input ports to a Python reaction function + * from the "self" struct. + * @param builder The string builder into which to write the code. + * @param structs A map from reactor instantiations to a place to write + * struct fields. + * @param input The input port. + * @param reactor The reactor. + */ + public static void generateInputVariablesToSendToPythonReaction( + List pyObjects, + Input input, + ReactorDecl decl + ) { + // Create the local variable whose name matches the input.getName(). + // If the input has not been declared mutable, then this is a pointer + // to the upstream output. Otherwise, it is a copy of the upstream output, + // which nevertheless points to the same token and value (hence, as done + // below, we have to use writable_copy()). There are 8 cases, + // 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() && !JavaAstUtils.isMultiport(input)) { + // Non-mutable, non-multiport, primitive type. + pyObjects.add(generateConvertCPortToPy(input.getName())); + } else if (input.isMutable() && !JavaAstUtils.isMultiport(input)) { + // Mutable, non-multiport, primitive type. + // TODO: handle mutable + pyObjects.add(generateConvertCPortToPy(input.getName())); + } else if (!input.isMutable() && JavaAstUtils.isMultiport(input)) { + // Non-mutable, multiport, primitive. + // TODO: support multiports + pyObjects.add(generateConvertCPortToPy(input.getName())); + } else { + // Mutable, multiport, primitive type + // TODO: support mutable multiports + pyObjects.add(generateConvertCPortToPy(input.getName())); + } + } + + /** Generate into the specified string builder the code to + * pass local variables for sending data to an input + * of a contained reaction (e.g. for a deadline violation). + * @param builder The string builder. + * @param definition AST node defining the reactor within which this occurs + * @param input Input of the contained reactor. + */ + public static String generateVariablesForSendingToContainedReactors( + List pyObjects, + Instantiation definition, + Port port + ) { + CodeBuilder code = new CodeBuilder(); + if (definition.getWidthSpec() != null) { + String widthSpec = NONMULTIPORT_WIDTHSPEC; + if (JavaAstUtils.isMultiport(port)) { + widthSpec = "self->_lf_"+definition.getName()+"[i]."+generateWidthVariable(port.getName()); + } + // Contained reactor is a bank. + // Create a Python list + code.pr(generatePythonListForContainedBank(definition.getName(), port, widthSpec)); + pyObjects.add(definition.getName()+"_py_list"); + } + else { + if (JavaAstUtils.isMultiport(port)) { + pyObjects.add(generateConvertCPortToPy(definition.getName()+"."+port.getName())); + } else { + pyObjects.add(generateConvertCPortToPy(definition.getName()+"."+port.getName(), NONMULTIPORT_WIDTHSPEC)); + } + } + return code.toString(); + } + + + /** + * Generate code that creates a Python list (i.e., []) for contained banks to be passed to Python reactions. + * The Python reaction will then subsequently be able to address each individual bank member of the contained + * bank using an index or an iterator. Each list member will contain the given port + * (which could be a multiport with a width determined by widthSpec). + * + * This is to accommodate reactions like reaction() -> s.out where s is a bank. In this example, + * the generated Python function will have the signature reaction_function_0(self, s_out), where + * s_out is a list of out ports. This will later be turned into the proper s.out format using the + * Python code generated in {@link #generatePythonPortVariableInReaction}. + * + * @param reactorName The name of the bank of reactors (which is the name of the reactor class). + * @param port The port that should be put in the Python list. + * @param widthSpec A string that should be -2 for non-multiports and the width expression for multiports. + */ + public static String generatePythonListForContainedBank(String reactorName, Port port, String widthSpec) { + return String.join("\n", + "PyObject* "+reactorName+"_py_list = PyList_New("+generateWidthVariable(reactorName)+");", + "if("+reactorName+"_py_list == NULL) {", + " error_print(\"Could not create the list needed for "+reactorName+".\");", + " if (PyErr_Occurred()) {", + " PyErr_PrintEx(0);", + " PyErr_Clear(); // this will reset the error indicator so we can run Python code again", + " }", + " /* Release the thread. No Python API allowed beyond this point. */", + " PyGILState_Release(gstate);", + " Py_FinalizeEx();", + " exit(1);", + "}", + "for (int i = 0; i < "+generateWidthVariable(reactorName)+"; i++) {", + " if (PyList_SetItem("+reactorName+"_py_list,", + " i,", + " "+generateConvertCPortToPy("self->_lf_"+reactorName+"[i]."+port.getName(), widthSpec), + " ) != 0) {", + " error_print(\"Could not add elements to the list for "+reactorName+".\");", + " if (PyErr_Occurred()) {", + " PyErr_PrintEx(0);", + " PyErr_Clear(); // this will reset the error indicator so we can run Python code again", + " }", + " /* Release the thread. No Python API allowed beyond this point. */", + " PyGILState_Release(gstate);", + " Py_FinalizeEx();", + " exit(1);", + " }", + "}" + ); + } + + public static String generateAliasTypeDef(ReactorDecl decl, Port port, boolean isTokenType, + String genericPortTypeWithToken, String genericPortType) { + if (isTokenType) { + return "typedef "+genericPortTypeWithToken+" "+CGenerator.variableStructType(port, decl)+";"; + } else { + return "typedef "+genericPortType+" "+CGenerator.variableStructType(port, decl)+";"; + } + } + + private static String generateConvertCPortToPy(String port) { + return String.format("convert_C_port_to_py(%s, %s)", port, generateWidthVariable(port)); + } + + private static String generateConvertCPortToPy(String port, String widthSpec) { + return String.format("convert_C_port_to_py(%s, %s)", port, widthSpec); + } + + /** + * Generate into the specified string builder (inits) the code to + * initialize local variable for port so that it can be used in the body of + * the Python reaction. + * @param port The port to generate code for. + * @param inits The generated code will be put in inits. + */ + public static String generatePythonPortVariableInReaction(VarRef port) { + String containerName = port.getContainer().getName(); + String variableName = port.getVariable().getName(); + if (port.getContainer().getWidthSpec() != null) { + // It's a bank + return String.join("\n", + containerName+" = [None] * len("+containerName+"_"+variableName+")", + "for i in range(len("+containerName+"_"+variableName+")):", + " "+containerName+"[i] = Make()", + " "+containerName+"[i]."+variableName+" = "+containerName+"_"+variableName+"[i]" + ); + } else { + return String.join("\n", + containerName+" = Make", + containerName+"."+variableName+" = "+containerName+"_"+variableName + ); + } + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonPreambleGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonPreambleGenerator.java new file mode 100644 index 0000000000..421aa54d40 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonPreambleGenerator.java @@ -0,0 +1,22 @@ +package org.lflang.generator.python; + +import java.util.ArrayList; +import java.util.List; +import org.lflang.ASTUtils; +import org.lflang.lf.Preamble; + +public class PythonPreambleGenerator { + /** + * Generates preambles defined by user for a given reactor. + * The preamble code is put inside the reactor class. + */ + public static String generatePythonPreambles(List preambles) { + List preamblesCode = new ArrayList<>(); + preambles.forEach(p -> preamblesCode.add(ASTUtils.toText(p.getCode()))); + return preamblesCode.size() > 0 ? String.join("\n", + "# From the preamble, verbatim:", + String.join("\n", preamblesCode), + "# End of preamble." + ) : ""; + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java new file mode 100644 index 0000000000..c984b9934e --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java @@ -0,0 +1,560 @@ +package org.lflang.generator.python; + +import java.util.Set; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; +import org.lflang.lf.Action; +import org.lflang.lf.TriggerRef; +import org.lflang.lf.VarRef; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Port; +import org.lflang.lf.Input; +import org.lflang.lf.Output; +import org.lflang.generator.c.CReactionGenerator; +import org.lflang.generator.c.CTypes; +import org.lflang.generator.c.CUtil; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.GeneratorBase; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.ErrorReporter; +import org.lflang.JavaAstUtils; +import org.lflang.Target; +import org.lflang.ASTUtils; + +public class PythonReactionGenerator { + /** + * Generate code to call reaction numbered "reactionIndex" in reactor "decl". + * @param decl The reactor containing the reaction + * @param reactionIndex The index of the reaction + * @param pyObjectDescriptor CPython related descriptors for each object in "pyObjects". + * @param pyObjects CPython related objects + */ + public static String generateCPythonReactionCaller(ReactorDecl decl, + int reactionIndex, + List pyObjects, + String inits) { + String pythonFunctionName = generatePythonReactionFunctionName(reactionIndex); + String cpythonFunctionName = generateCPythonReactionFunctionName(reactionIndex); + return generateCPythonFunctionCaller(decl.getName(), pythonFunctionName, cpythonFunctionName, pyObjects, inits); + } + + /** + * Generate code to call deadline function numbered "reactionIndex" in reactor "decl". + * @param decl The reactor containing the reaction + * @param reactionIndex The index of the reaction + * @param pyObjectDescriptor CPython related descriptors for each object in "pyObjects". + * @param pyObjects CPython related objects + */ + public static String generateCPythonDeadlineCaller(ReactorDecl decl, + int reactionIndex, + List pyObjects) { + String pythonFunctionName = generatePythonDeadlineFunctionName(reactionIndex); + String cpythonFunctionName = generateCPythonDeadlineFunctionName(reactionIndex); + return generateCPythonFunctionCaller(decl.getName(), pythonFunctionName, cpythonFunctionName, pyObjects, ""); + } + + /** + * Generate code to call a CPython function. + * @param reactorDeclName The name of the reactor for debugging purposes + * @param pythonFunctionName The name of the function in the .py file. + * @param cpythonFunctionName The name of the function in self struct of the .c file. + * @param pyObjectDescriptor CPython related descriptors for each object in "pyObjects". + * @param pyObjects CPython related objects + */ + private static String generateCPythonFunctionCaller(String reactorDeclName, + String pythonFunctionName, + String cpythonFunctionName, + List pyObjects, + String inits) { + String pyObjectsJoined = pyObjects.size() > 0 ? ", " + String.join(", ", pyObjects) : ""; + CodeBuilder code = new CodeBuilder(); + code.pr(PyUtil.generateGILAcquireCode()); + code.pr(inits); + code.pr(String.join("\n", + "DEBUG_PRINT(\"Calling reaction function "+reactorDeclName+"."+pythonFunctionName+"\");", + "PyObject *rValue = PyObject_CallObject(", + " self->"+cpythonFunctionName+", ", + " Py_BuildValue(\"("+"O".repeat(pyObjects.size())+")\""+pyObjectsJoined+")", + ");", + "if (rValue == NULL) {", + " error_print(\"FATAL: Calling reaction "+reactorDeclName+"."+pythonFunctionName+" failed.\");", + " if (PyErr_Occurred()) {", + " PyErr_PrintEx(0);", + " PyErr_Clear(); // this will reset the error indicator so we can run Python code again", + " }", + " "+PyUtil.generateGILReleaseCode(), + " Py_FinalizeEx();", + " exit(1);", + "}", + "", + "/* Release the thread. No Python API allowed beyond this point. */", + PyUtil.generateGILReleaseCode() + )); + return code.toString(); + } + + /** + * Generate the reaction in the .c file, which calls the Python reaction through the CPython interface. + * + * @param reaction The reaction to generate Python-specific initialization for. + * @param decl The reactor to which reaction belongs to. + * @param reactionIndex The index number of the reaction in decl. + * @param mainDef The main reactor. + * @param errorReporter An error reporter. + * @param types A helper class for type-related stuff. + * @param isFederatedAndDecentralized True if program is federated and coordination type is decentralized. + */ + public static String generateCReaction(Reaction reaction, + ReactorDecl decl, + int reactionIndex, + Instantiation mainDef, + ErrorReporter errorReporter, + CTypes types, + boolean isFederatedAndDecentralized) { + // Contains the actual comma separated list of inputs to the reaction of type generic_port_instance_struct or generic_port_instance_with_token_struct. + // Each input must be cast to (PyObject *) (aka their descriptors for Py_BuildValue are "O") + List pyObjects = new ArrayList<>(); + CodeBuilder code = new CodeBuilder(); + code.pr(generateCReactionFunctionHeader(decl, reactionIndex) + " {"); + code.indent(); + code.pr(CReactionGenerator.generateInitializationForReaction("", reaction, decl, reactionIndex, + types, errorReporter, mainDef, + isFederatedAndDecentralized, + Target.Python.requiresTypes)); + code.prSourceLineNumber(reaction.getCode()); + code.pr(generateCPythonReactionCaller(decl, reactionIndex, pyObjects, + generateCPythonInitializers(reaction, decl, pyObjects, errorReporter))); + code.unindent(); + code.pr("}"); + + // Now generate code for the deadline violation function, if there is one. + if (reaction.getDeadline() != null) { + code.pr(generateCDeadlineFunctionHeader(decl, reactionIndex) + " {"); + code.indent(); + code.pr(CReactionGenerator.generateInitializationForReaction("", reaction, decl, reactionIndex, + types, errorReporter, mainDef, + isFederatedAndDecentralized, + Target.Python.requiresTypes)); + code.pr(generateCPythonDeadlineCaller(decl, reactionIndex, pyObjects)); + code.unindent(); + code.pr("}"); + } + return code.toString(); + } + + /** + * Generate necessary Python-specific initialization code for reaction that belongs to reactor + * decl. + * + * @param reaction The reaction to generate Python-specific initialization for. + * @param decl The reactor to which reaction belongs to. + * @param pyObjectDescriptor For each port object created, a Python-specific descriptor will be added to this that + * then can be used as an argument to Py_BuildValue + * (@see docs.python.org/3/c-api). + * @param pyObjects A "," delimited list of expressions that would be (or result in a creation of) a PyObject. + */ + private static String generateCPythonInitializers(Reaction reaction, + ReactorDecl decl, + List pyObjects, + ErrorReporter errorReporter) { + Set actionsAsTriggers = new LinkedHashSet<>(); + Reactor reactor = ASTUtils.toDefinition(decl); + CodeBuilder code = new CodeBuilder(); + // Next, add the triggers (input and actions; timers are not needed). + // TODO: handle triggers + for (TriggerRef trigger : ASTUtils.convertToEmptyListIfNull(reaction.getTriggers())) { + if (trigger instanceof VarRef) { + VarRef triggerAsVarRef = (VarRef) trigger; + code.pr(generateVariableToSendPythonReaction(triggerAsVarRef, actionsAsTriggers, decl, pyObjects)); + } + } + if (reaction.getTriggers() == null || reaction.getTriggers().size() == 0) { + // No triggers are given, which means react to any input. + // Declare an argument for every input. + // NOTE: this does not include contained outputs. + for (Input input : reactor.getInputs()) { + PythonPortGenerator.generateInputVariablesToSendToPythonReaction(pyObjects, input, decl); + } + } + + // Next add non-triggering inputs. + for (VarRef src : ASTUtils.convertToEmptyListIfNull(reaction.getSources())) { + code.pr(generateVariableToSendPythonReaction(src, actionsAsTriggers, decl, pyObjects)); + } + + // Next, handle effects + if (reaction.getEffects() != null) { + for (VarRef effect : reaction.getEffects()) { + if (effect.getVariable() instanceof Action) { + // It is an action, not an output. + // If it has already appeared as trigger, do not redefine it. + if (!actionsAsTriggers.contains(effect.getVariable())) { + PythonPortGenerator.generateActionVariableToSendToPythonReaction(pyObjects, + (Action) effect.getVariable(), decl); + } + } else { + if (effect.getVariable() instanceof Output) { + PythonPortGenerator.generateOutputVariablesToSendToPythonReaction(pyObjects, (Output) effect.getVariable()); + } else if (effect.getVariable() instanceof Input) { + // It is the input of a contained reactor. + code.pr(PythonPortGenerator.generateVariablesForSendingToContainedReactors(pyObjects, effect.getContainer(), (Input) effect.getVariable())); + } else { + errorReporter.reportError( + reaction, + "In generateReaction(): " + effect.getVariable().getName() + " is neither an input nor an output." + ); + } + } + } + } + return code.toString(); + } + + /** + * Generate parameters and their respective initialization code for a reaction function + * The initialization code is put at the beginning of the reaction before user code + * @param parameters The parameters used for function definition + * @param inits The initialization code for those paramters + * @param decl Reactor declaration + * @param reaction The reaction to be used to generate parameters for + */ + public static void generatePythonReactionParametersAndInitializations(List parameters, CodeBuilder inits, + ReactorDecl decl, Reaction reaction) { + Reactor reactor = ASTUtils.toDefinition(decl); + LinkedHashSet generatedParams = new LinkedHashSet<>(); + + // Handle triggers + for (TriggerRef trigger : ASTUtils.convertToEmptyListIfNull(reaction.getTriggers())) { + if (!(trigger instanceof VarRef)) { + continue; + } + VarRef triggerAsVarRef = (VarRef) trigger; + if (triggerAsVarRef.getVariable() instanceof Port) { + if (triggerAsVarRef.getVariable() instanceof Input) { + if (((Input) triggerAsVarRef.getVariable()).isMutable()) { + generatedParams.add("mutable_"+triggerAsVarRef.getVariable().getName()+""); + + // Create a deep copy + if (JavaAstUtils.isMultiport((Input) triggerAsVarRef.getVariable())) { + inits. + pr(triggerAsVarRef.getVariable().getName()+" = [Make() for i in range(len(mutable_"+triggerAsVarRef.getVariable().getName()+"))]"); + inits.pr("for i in range(len(mutable_"+triggerAsVarRef.getVariable().getName()+")):"); + inits.pr(" "+triggerAsVarRef.getVariable().getName()+"[i].value = copy.deepcopy(mutable_"+triggerAsVarRef.getVariable().getName()+"[i].value)"); + } else { + inits.pr(triggerAsVarRef.getVariable().getName()+" = Make()"); + inits. + pr(triggerAsVarRef.getVariable().getName()+".value = copy.deepcopy(mutable_"+triggerAsVarRef.getVariable().getName()+".value)"); + } + } else { + generatedParams.add(triggerAsVarRef.getVariable().getName()); + } + } else { + // Handle contained reactors' ports + generatedParams.add(triggerAsVarRef.getContainer().getName()+"_"+triggerAsVarRef.getVariable().getName()); + inits.pr(PythonPortGenerator.generatePythonPortVariableInReaction(triggerAsVarRef)); + } + } else if (triggerAsVarRef.getVariable() instanceof Action) { + generatedParams.add(triggerAsVarRef.getVariable().getName()); + } + } + + // Handle non-triggering inputs + if (reaction.getTriggers() == null || reaction.getTriggers().size() == 0) { + for (Input input : ASTUtils.convertToEmptyListIfNull(reactor.getInputs())) { + generatedParams.add(input.getName()); + if (input.isMutable()) { + // Create a deep copy + inits.pr(input.getName()+" = copy.deepcopy("+input.getName()+")"); + } + } + } + for (VarRef src : ASTUtils.convertToEmptyListIfNull(reaction.getSources())) { + if (src.getVariable() instanceof Output) { + // Output of a contained reactor + generatedParams.add(src.getContainer().getName()+"_"+src.getVariable().getName()); + inits.pr(PythonPortGenerator.generatePythonPortVariableInReaction(src)); + } else { + generatedParams.add(src.getVariable().getName()); + if (src.getVariable() instanceof Input) { + if (((Input) src.getVariable()).isMutable()) { + // Create a deep copy + inits.pr(src.getVariable().getName()+" = copy.deepcopy("+src.getVariable().getName()+")"); + } + } + } + } + + // Handle effects + for (VarRef effect : ASTUtils.convertToEmptyListIfNull(reaction.getEffects())) { + if (effect.getVariable() instanceof Input) { + generatedParams.add(effect.getContainer().getName()+"_"+effect.getVariable().getName()); + inits.pr(PythonPortGenerator.generatePythonPortVariableInReaction(effect)); + } else { + generatedParams.add(effect.getVariable().getName()); + if (effect.getVariable() instanceof Port) { + if (JavaAstUtils.isMultiport((Port) effect.getVariable())) { + // Handle multiports + } + } + } + } + + for (String s : generatedParams) { + parameters.add(s); + } + } + + private static String generateVariableToSendPythonReaction(VarRef varRef, + Set actionsAsTriggers, + ReactorDecl decl, + List pyObjects) { + if (varRef.getVariable() instanceof Port) { + return PythonPortGenerator.generatePortVariablesToSendToPythonReaction(pyObjects, varRef, decl); + } else if (varRef.getVariable() instanceof Action) { + actionsAsTriggers.add((Action) varRef.getVariable()); + PythonPortGenerator.generateActionVariableToSendToPythonReaction(pyObjects, (Action) varRef.getVariable(), decl); + } + return ""; + } + + /** + * Generate code for the body of a reaction that takes an input and + * schedules an action with the value of that input. + * @param action The action to schedule + * @param port The port to read from + */ + public static String generateCDelayBody(Action action, VarRef port, boolean isTokenType) { + String ref = JavaAstUtils.generateVarRef(port); + // Note that the action.type set by the base class is actually + // the port type. + if (isTokenType) { + return String.join("\n", + "if ("+ref+"->is_present) {", + " // Put the whole token on the event queue, not just the payload.", + " // This way, the length and element_size are transported.", + " schedule_token("+action.getName()+", 0, "+ref+"->token);", + "}" + ); + } else { + return String.join("\n", + "// Create a token.", + "#if NUMBER_OF_WORKERS > 0", + "// Need to lock the mutex first.", + "lf_mutex_lock(&mutex);", + "#endif", + "lf_token_t* t = create_token(sizeof(PyObject*));", + "#if NUMBER_OF_WORKERS > 0", + "lf_mutex_unlock(&mutex);", + "#endif", + "t->value = self->_lf_"+ref+"->value;", + "t->length = 1; // Length is 1", + "", + "// Pass the token along", + "schedule_token("+action.getName()+", 0, t);" + ); + } + } + + /** + * Generate Python code to link cpython functions to python functions for each reaction. + * @param instance The reactor instance. + * @param reactions The reactions of this instance. + * @param mainDef The definition of the main reactor + * @param topLevelName The name of the module + */ + public static String generateCPythonReactionLinkers( + ReactorInstance instance, + Instantiation mainDef, + String topLevelName + ) { + String nameOfSelfStruct = CUtil.reactorRef(instance); + Reactor reactor = ASTUtils.toDefinition(instance.getDefinition().getReactorClass()); + CodeBuilder code = new CodeBuilder(); + + // Delay reactors and top-level reactions used in the top-level reactor(s) in federated execution are generated in C + if (reactor.getName().contains(GeneratorBase.GEN_DELAY_CLASS_NAME) || + instance.getDefinition().getReactorClass() == (mainDef != null ? mainDef.getReactorClass() : null) && + reactor.isFederated()) { + return ""; + } + + // Initialize the name field to the unique name of the instance + code.pr(nameOfSelfStruct+"->_lf_name = \""+instance.uniqueID()+"_lf\";"); + + for (ReactionInstance reaction : instance.reactions) { + // Create a PyObject for each reaction + code.pr(generateCPythonReactionLinker(instance, reaction, topLevelName, nameOfSelfStruct)); + } + return code.toString(); + } + + /** + * Generate Python code to link cpython functions to python functions for a reaction. + * @param instance The reactor instance. + * @param reaction The reaction of this instance to link. + * @param topLevelName The name of the module. + * @param nameOfSelfStruct The name of the self struct in cpython. + */ + public static String generateCPythonReactionLinker( + ReactorInstance instance, + ReactionInstance reaction, + String topLevelName, + String nameOfSelfStruct + ) { + CodeBuilder code = new CodeBuilder(); + code.pr(generateCPythonFunctionLinker( + nameOfSelfStruct, generateCPythonReactionFunctionName(reaction.index), + instance, generatePythonReactionFunctionName(reaction.index)) + ); + if (reaction.getDefinition().getDeadline() != null) { + code.pr(generateCPythonFunctionLinker( + nameOfSelfStruct, generateCPythonDeadlineFunctionName(reaction.index), + instance, generatePythonDeadlineFunctionName(reaction.index)) + ); + } + return code.toString(); + } + + /** + * Generate code to link "pythonFunctionName" to "cpythonFunctionName" in "nameOfSelfStruct" of "instance". + * @param nameOfSelfStruct the self struct name of instance + * @param cpythonFunctionName the name of the cpython function + * @param instance the reactor instance + * @param pythonFunctionName the name of the python function + */ + private static String generateCPythonFunctionLinker(String nameOfSelfStruct, String cpythonFunctionName, ReactorInstance instance, String pythonFunctionName) { + return String.join("\n", + nameOfSelfStruct+"->"+cpythonFunctionName+" = ", + "get_python_function(\"__main__\", ", + " "+nameOfSelfStruct+"->_lf_name,", + " "+CUtil.runtimeIndex(instance)+",", + " \""+pythonFunctionName+"\");", + "if("+nameOfSelfStruct+"->"+cpythonFunctionName+" == NULL) {", + " error_print_and_exit(\"Could not load function "+pythonFunctionName+"\");", + "}" + ); + } + + /** + * Generate the function that is executed whenever the deadline of the reaction + * with the given reaction index is missed + * @param reaction The reaction to generate deadline miss code for + * @param reactionIndex The agreed-upon index of the reaction in the reactor (should match the C generated code) + * @param reactionParameters The parameters to the deadline violation function, which are the same as the reaction function + */ + public static String generatePythonFunction(String pythonFunctionName, String inits, String reactionBody, List reactionParameters) { + String params = reactionParameters.size() > 0 ? ", " + String.join(", ", reactionParameters) : ""; + CodeBuilder code = new CodeBuilder(); + code.pr("def "+pythonFunctionName+"(self"+params+"):"); + code.indent(); + code.pr(inits); + code.pr(reactionBody); + code.pr("return 0"); + return code.toString(); + } + + + /** + * Generate the Python code for reactions in reactor + * @param reactor The reactor + * @param reactions The reactions of reactor + */ + public static String generatePythonReactions(Reactor reactor, List reactions) { + CodeBuilder code = new CodeBuilder(); + int reactionIndex = 0; + for (Reaction reaction : reactions) { + code.pr(generatePythonReaction(reactor, reaction, reactionIndex)); + reactionIndex++; + } + return code.toString(); + } + + /** + * Generate the Python code for reaction in reactor + * @param reactor The reactor + * @param reaction The reaction of reactor + */ + public static String generatePythonReaction(Reactor reactor, Reaction reaction, int reactionIndex) { + CodeBuilder code = new CodeBuilder(); + List reactionParameters = new ArrayList<>(); // Will contain parameters for the function (e.g., Foo(x,y,z,...) + CodeBuilder inits = new CodeBuilder(); // Will contain initialization code for some parameters + PythonReactionGenerator.generatePythonReactionParametersAndInitializations(reactionParameters, inits, reactor, reaction); + code.pr(generatePythonFunction( + generatePythonReactionFunctionName(reactionIndex), + inits.toString(), + ASTUtils.toText(reaction.getCode()), + reactionParameters + )); + // Now generate code for the deadline violation function, if there is one. + if (reaction.getDeadline() != null) { + code.pr(generatePythonFunction( + generatePythonDeadlineFunctionName(reactionIndex), + "", + ASTUtils.toText(reaction.getDeadline().getCode()), + reactionParameters + )); + } + return code.toString(); + } + + + /** Return the top level C function header for the deadline function numbered "reactionIndex" in "decl" + * @param decl The reactor declaration + * @param reactionIndex The reaction index. + * @return The function name for the deadline function. + */ + public static String generateCDeadlineFunctionHeader(ReactorDecl decl, + int reactionIndex) { + String deadlineFunctionName = CReactionGenerator.generateDeadlineFunctionName(decl, reactionIndex); + return "void " + deadlineFunctionName + "(void* instance_args)"; + } + + /** Return the top level C function header for the reaction numbered "reactionIndex" in "decl" + * @param decl The reactor declaration + * @param reactionIndex The reaction index. + * @return The function name for the reaction. + */ + public static String generateCReactionFunctionHeader(ReactorDecl decl, + int reactionIndex) { + String deadlineFunctionName = CReactionGenerator.generateReactionFunctionName(decl, reactionIndex); + return "void " + deadlineFunctionName + "(void* instance_args)"; + } + + /** Return the function name of the reaction inside the self struct in the .c file. + * @param reactionIndex The reaction index. + * @return The function name for the reaction. + */ + public static String generateCPythonReactionFunctionName(int reactionIndex) { + return "_lf_py_reaction_function_"+reactionIndex; + } + + /** Return the function name of the deadline function inside the self struct in the .c file. + * @param reactionIndex The reaction index. + * @return The function name for the reaction. + */ + public static String generateCPythonDeadlineFunctionName(int reactionIndex) { + return "_lf_py_deadline_function_"+reactionIndex; + } + + /** Return the function name of the reaction in the .py file. + * @param reactionIndex The reaction index. + * @return The function name for the reaction. + */ + public static String generatePythonReactionFunctionName(int reactionIndex) { + return "reaction_function_" + reactionIndex; + } + + /** Return the function name of the deadline function in the .py file. + * @param reactionIndex The reaction index. + * @return The function name for the reaction. + */ + public static String generatePythonDeadlineFunctionName(int reactionIndex) { + return "deadline_function_" + reactionIndex; + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonReactorGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonReactorGenerator.java new file mode 100644 index 0000000000..4b5549b738 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonReactorGenerator.java @@ -0,0 +1,181 @@ +package org.lflang.generator.python; + +import java.util.ArrayList; +import java.util.List; +import org.lflang.ASTUtils; +import org.lflang.federated.FederateInstance; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.GeneratorBase; +import org.lflang.generator.ParameterInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; +import org.lflang.lf.ReactorDecl; + +public class PythonReactorGenerator { + /** + * Wrapper function for the more elaborate generatePythonReactorClass that keeps track + * of visited reactors to avoid duplicate generation + * @param instance The reactor instance to be generated + * @param pythonClasses The class definition is appended to this code builder + * @param federate The federate instance for the reactor instance + * @param instantiatedClasses A list of visited instances to avoid generating duplicates + */ + public static String generatePythonClass(ReactorInstance instance, FederateInstance federate, ReactorInstance main, PythonTypes types) { + List instantiatedClasses = new ArrayList(); + return generatePythonClass(instance, federate, instantiatedClasses, main, types); + } + + /** + * Generate a Python class corresponding to decl + * @param instance The reactor instance to be generated + * @param pythonClasses The class definition is appended to this code builder + * @param federate The federate instance for the reactor instance + * @param instantiatedClasses A list of visited instances to avoid generating duplicates + */ + public static String generatePythonClass(ReactorInstance instance, FederateInstance federate, + List instantiatedClasses, + ReactorInstance main, PythonTypes types) { + CodeBuilder pythonClasses = new CodeBuilder(); + ReactorDecl decl = instance.getDefinition().getReactorClass(); + Reactor reactor = ASTUtils.toDefinition(decl); + String className = instance.getDefinition().getReactorClass().getName(); + if (instance != main && !federate.contains(instance) || + instantiatedClasses == null || + // Do not generate code for delay reactors in Python + className.contains(GeneratorBase.GEN_DELAY_CLASS_NAME)) { + return ""; + } + + if (federate.contains(instance) && !instantiatedClasses.contains(className)) { + pythonClasses.pr(generatePythonClassHeader(className)); + // Generate preamble code + pythonClasses.indent(); + pythonClasses.pr(PythonPreambleGenerator.generatePythonPreambles(reactor.getPreambles())); + // Handle runtime initializations + pythonClasses.pr(generatePythonConstructor(decl, types)); + pythonClasses.pr(PythonParameterGenerator.generatePythonGetters(decl)); + List reactionToGenerate = ASTUtils.allReactions(reactor); + if (reactor.isFederated()) { + // Filter out reactions that are automatically generated in C in the top level federated reactor + reactionToGenerate.removeIf(it -> !federate.contains(it) || federate.networkReactions.contains(it)); + } + pythonClasses.pr(PythonReactionGenerator.generatePythonReactions(reactor, reactionToGenerate)); + pythonClasses.unindent(); + instantiatedClasses.add(className); + } + + for (ReactorInstance child : instance.children) { + pythonClasses.pr(generatePythonClass(child, federate, instantiatedClasses, main, types)); + } + return pythonClasses.getCode(); + } + + private static String generatePythonClassHeader(String className) { + return String.join("\n", + "# Python class for reactor "+className+"", + "class _"+className+":" + ); + } + + /** + * Generate code that instantiates and initializes parameters and state variables for a reactor 'decl'. + * + * @param decl The reactor declaration + * @return The generated code as a StringBuilder + */ + private static String generatePythonConstructor(ReactorDecl decl, PythonTypes types) { + CodeBuilder code = new CodeBuilder(); + code.pr("# Constructor"); + code.pr("def __init__(self, **kwargs):"); + code.indent(); + code.pr(PythonParameterGenerator.generatePythonInstantiations(decl, types)); + code.pr(PythonStateGenerator.generatePythonInstantiations(decl)); + return code.toString(); + } + + /** + * Generate code to instantiate a Python list that will hold the Python + * class instance of reactor instance. Will recursively do + * the same for the children of instance as well. + * + * @param instance The reactor instance for which the Python list will be created. + * @param federate Will check if instance (or any of its children) belong to + * federate before generating code for them. + */ + public static String generateListsToHoldClassInstances(ReactorInstance instance, + FederateInstance federate) { + CodeBuilder code = new CodeBuilder(); + if (federate != null && !federate.contains(instance)) { + return ""; + } + code.pr(PyUtil.reactorRefName(instance)+" = [None] * "+instance.getTotalWidth()); + for (ReactorInstance child : instance.children) { + code.pr(generateListsToHoldClassInstances(child, federate)); + } + return code.toString(); + } + + /** + * Instantiate classes in Python, as well as subclasses. + * Instances are always instantiated as a list of className = [_className, _className, ...] depending on the size of the bank. + * If there is no bank or the size is 1, the instance would be generated as className = [_className] + * @param instance The reactor instance to be instantiated + * @param federate The federate instance for the reactor instance + * @param main The main reactor + */ + public static String generatePythonClassInstantiations(ReactorInstance instance, + FederateInstance federate, + ReactorInstance main) { + CodeBuilder code = new CodeBuilder(); + // If this is not the main reactor and is not in the federate, nothing to do. + if (instance != main && !federate.contains(instance)) { + return ""; + } + String className = instance.getDefinition().getReactorClass().getName(); + // Do not instantiate delay reactors in Python + if (className.contains(GeneratorBase.GEN_DELAY_CLASS_NAME)) { + return ""; + } + + if (federate.contains(instance) && instance.getWidth() > 0) { + // 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. var reactorClass = instance.definition.reactorClass + String fullName = instance.getFullName(); + code.pr(String.join("\n", + "# Start initializing "+fullName+" of class "+className, + "for "+PyUtil.bankIndexName(instance)+" in range("+instance.getWidth()+"):" + )); + code.indent(); + code.pr(generatePythonClassInstantiation(instance, className)); + } + + for (ReactorInstance child : instance.children) { + code.pr(generatePythonClassInstantiations(child, federate, main)); + } + code.unindent(); + return code.toString(); + } + + /** + * Instantiate a class with className in instance. + * @param instance The reactor instance to be instantiated + * @param className The name of the class to instantiate + */ + private static String generatePythonClassInstantiation(ReactorInstance instance, + String className) { + CodeBuilder code = new CodeBuilder(); + code.pr(PyUtil.reactorRef(instance)+" = _"+className+"("); + code.indent(); + for (ParameterInstance param : instance.parameters) { + if (param.getName().equals("bank_index")) { + code.pr("_bank_index = "+PyUtil.bankIndex(instance)+","); + } else { + code.pr("_"+param.getName()+"="+PythonParameterGenerator.generatePythonInitializer(param)+","); + } + } + code.unindent(); + code.pr(")"); + return code.toString(); + } +} diff --git a/org.lflang/src/org/lflang/generator/python/PythonStateGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonStateGenerator.java new file mode 100644 index 0000000000..efbd9236c9 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/python/PythonStateGenerator.java @@ -0,0 +1,41 @@ +package org.lflang.generator.python; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.lflang.ASTUtils; +import org.lflang.JavaAstUtils; +import org.lflang.generator.GeneratorBase; +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.StateVar; +import org.lflang.lf.Value; + +public class PythonStateGenerator { + /** + * Generate state variable instantiations for reactor "decl" + * @param decl The reactor declaration to generate state variables. + */ + public static String generatePythonInstantiations(ReactorDecl decl) { + List lines = new ArrayList<>(); + lines.add("# Define state variables"); + // Next, handle state variables + for (StateVar state : ASTUtils.allStateVars(ASTUtils.toDefinition(decl))) { + lines.add("self."+state.getName()+" = "+generatePythonInitializer(state)); + } + lines.add(""); + return String.join("\n", lines); + } + + /** + * Handle initialization for state variable + * @param state a state variable + */ + private static String generatePythonInitializer(StateVar state) { + if (!ASTUtils.isInitialized(state)) { + return "None"; + } + List list = state.getInit().stream().map(PyUtil::getPythonTargetValue).collect(Collectors.toList()); + return list.size() > 1 ? "[" + String.join(", ", list) + "]" : list.get(0); + } +} diff --git a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt index 659f90cab6..3fe2fd783d 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt @@ -26,7 +26,6 @@ package org.lflang.generator.ts 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.inferredType @@ -214,7 +213,7 @@ class TSGenerator( for (runtimeFile in RUNTIME_FILES) { fileConfig.copyFileFromClassPath( "$LIB_PATH/reactor-ts/src/core/$runtimeFile", - tsFileConfig.tsCoreGenPath().resolve(runtimeFile).toString() + tsFileConfig.tsCoreGenPath().resolve(runtimeFile) ) } } @@ -235,7 +234,7 @@ class TSGenerator( "No '" + configFile + "' exists in " + fileConfig.srcPath + ". Using default configuration." ) - fileConfig.copyFileFromClassPath("$LIB_PATH/$configFile", configFileDest.toString()) + fileConfig.copyFileFromClassPath("$LIB_PATH/$configFile", configFileDest) } } } @@ -270,7 +269,7 @@ class TSGenerator( tsCode.append(reactorGenerator.generateReactorInstanceAndStart(this.mainDef, mainParameters)) val codeMap = CodeMap.fromGeneratedCode(tsCode.toString()) codeMaps[tsFilePath] = codeMap - JavaGeneratorUtils.writeToFile(codeMap.generatedCode, tsFilePath.toString()) + JavaGeneratorUtils.writeToFile(codeMap.generatedCode, tsFilePath) if (targetConfig.dockerOptions != null && isFederated) { println("WARNING: Federated Docker file generation is not supported on the Typescript target. No docker file is generated.") @@ -279,8 +278,8 @@ class TSGenerator( val dockerComposeFile = fileConfig.srcGenPath.resolve("docker-compose.yml") val dockerGenerator = TSDockerGenerator(tsFileName) println("docker file written to $dockerFilePath") - JavaGeneratorUtils.writeToFile(dockerGenerator.generateDockerFileContent(), dockerFilePath.toString()) - JavaGeneratorUtils.writeToFile(dockerGenerator.generateDockerComposeFileContent(), dockerComposeFile.toString()) + JavaGeneratorUtils.writeToFile(dockerGenerator.generateDockerFileContent(), dockerFilePath) + JavaGeneratorUtils.writeToFile(dockerGenerator.generateDockerComposeFileContent(), dockerComposeFile) } } diff --git a/org.lflang/src/org/lflang/validation/LFValidator.java b/org.lflang/src/org/lflang/validation/LFValidator.java index 2d7bd1bb25..1b72a035f8 100644 --- a/org.lflang/src/org/lflang/validation/LFValidator.java +++ b/org.lflang/src/org/lflang/validation/LFValidator.java @@ -26,33 +26,44 @@ ***************/ package org.lflang.validation; -import com.google.inject.Inject; +import static org.lflang.ASTUtils.inferPortWidth; +import static org.lflang.ASTUtils.isGeneric; +import static org.lflang.ASTUtils.isInteger; +import static org.lflang.ASTUtils.isParameterized; +import static org.lflang.ASTUtils.isValidTime; +import static org.lflang.ASTUtils.isZero; +import static org.lflang.ASTUtils.toDefinition; +import static org.lflang.ASTUtils.toText; +import static org.lflang.JavaAstUtils.isOfTimeType; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.emf.common.util.BasicEList; import org.eclipse.emf.common.util.EList; -import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.common.util.TreeIterator; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.xtext.validation.Check; import org.eclipse.xtext.validation.CheckType; import org.eclipse.xtext.validation.ValidationMessageAcceptor; - +import org.lflang.ASTUtils; import org.lflang.FileConfig; -import org.lflang.InferredType; import org.lflang.JavaAstUtils; import org.lflang.ModelInfo; import org.lflang.Target; import org.lflang.TargetProperty; import org.lflang.TimeValue; +import org.lflang.federated.serialization.SupportedSerializers; +import org.lflang.generator.NamedInstance; import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; import org.lflang.lf.Assignment; @@ -76,8 +87,9 @@ import org.lflang.lf.Preamble; import org.lflang.lf.Reaction; import org.lflang.lf.Reactor; -import org.lflang.lf.Serializer; +import org.lflang.lf.ReactorDecl; import org.lflang.lf.STP; +import org.lflang.lf.Serializer; import org.lflang.lf.StateVar; import org.lflang.lf.TargetDecl; import org.lflang.lf.Timer; @@ -90,12 +102,8 @@ import org.lflang.lf.Visibility; import org.lflang.lf.WidthSpec; import org.lflang.lf.WidthTerm; -import org.lflang.lf.ReactorDecl; -import org.lflang.generator.NamedInstance; -import static org.lflang.ASTUtils.*; -import static org.lflang.JavaAstUtils.*; -import org.lflang.federated.serialization.SupportedSerializers; +import com.google.inject.Inject; /** * Custom validation checks for Lingua Franca programs. @@ -105,204 +113,26 @@ * @author{Edward A. Lee } * @author{Marten Lohstroh } * @author{Matt Weber } - * @author(Christian Menard } - * + * @author{Christian Menard } + * @author{Hou Seng Wong } + * @author{Clément Fournier } */ public class LFValidator extends BaseLFValidator { - private Target target; - - public ModelInfo info = new ModelInfo(); - - private ValidatorErrorReporter errorReporter = new ValidatorErrorReporter(getMessageAcceptor(), - new ValidatorStateAccess()); - - @Inject(optional = true) - ValidationMessageAcceptor messageAcceptor; - - /** - * Regular expression to check the validity of IPV4 addresses (due to David M. Syzdek). - */ - static String ipv4Regex = "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + - "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"; - - /** - * Regular expression to check the validity of IPV6 addresses (due to David M. Syzdek), - * with minor adjustment to allow up to six IPV6 segments (without truncation) in front - * of an embedded IPv4-address. - **/ - static String ipv6Regex = - "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" + - "([0-9a-fA-F]{1,4}:){1,7}:|" + - "([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" + - "([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" + - "([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" + - "([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" + - "([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" + - "[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" + - ":((:[0-9a-fA-F]{1,4}){1,7}|:)|" + - "fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" + - "::(ffff(:0{1,4}){0,1}:){0,1}" + ipv4Regex + "|" + - "([0-9a-fA-F]{1,4}:){1,4}:" + ipv4Regex + "|" + - "([0-9a-fA-F]{1,4}:){1,6}" + ipv4Regex + ")"; - - static String usernameRegex = "^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\\$)$"; - - static String hostOrFQNRegex = "^([a-z0-9]+(-[a-z0-9]+)*)|(([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,})$"; - - public static String GLOBALLY_DUPLICATE_NAME = "GLOBALLY_DUPLICATE_NAME"; - - static List spacingViolationPolicies = List.of("defer", "drop", "replace"); - - private List targetPropertyErrors = new ArrayList<>(); - - private List targetPropertyWarnings = new ArrayList<>(); - - public List getTargetPropertyErrors() { - return this.targetPropertyErrors; - } - - public ValidatorErrorReporter getErrorReporter() { - return this.errorReporter; - } - - @Override - public ValidationMessageAcceptor getMessageAcceptor() { - return messageAcceptor == null ? this : messageAcceptor; - } + ////////////////////////////////////////////////////////////// + //// Public check methods. - /** - * Returns true if target is C or a C-based target like CCpp. - */ - private boolean isCBasedTarget() { - return (this.target == Target.C || this.target == Target.CCPP); - } - - @Check - public void checkImportedReactor(ImportedReactor reactor) { - if (isUnused(reactor)) { - warning("Unused reactor class.", - Literals.IMPORTED_REACTOR__REACTOR_CLASS); - } - - if (info.instantiationGraph.hasCycles()) { - Set cycleSet = new HashSet<>(); - for (Set cycle : info.instantiationGraph.getCycles()) { - cycleSet.addAll(cycle); - } - if (dependsOnCycle(toDefinition(reactor), cycleSet, new HashSet<>())) { - error("Imported reactor '" + toDefinition(reactor).getName() + - "' has cyclic instantiation in it.", Literals.IMPORTED_REACTOR__REACTOR_CLASS); - } - } - } - - @Check - public void checkImport(Import imp) { - if (toDefinition(imp.getReactorClasses().get(0)).eResource().getErrors().size() > 0) { - error("Error loading resource.", Literals.IMPORT__IMPORT_URI); // FIXME: print specifics. - return; - } - - // FIXME: report error if resource cannot be resolved. - for (ImportedReactor reactor : imp.getReactorClasses()) { - if (!isUnused(reactor)) { - return; - } - } - warning("Unused import.", Literals.IMPORT__IMPORT_URI); - } - - // ////////////////////////////////////////////////// - // // Helper functions for checks to be performed on multiple entities - // Check the name of a feature for illegal substrings. - private void checkName(String name, EStructuralFeature feature) { - - // Raises an error if the string starts with two underscores. - if (name.length() >= 2 && name.substring(0, 2).equals("__")) { - error(UNDERSCORE_MESSAGE + name, feature); - } - - if (this.target.isReservedIdent(name)) { - error(RESERVED_MESSAGE + name, feature); - } - - if (this.target == Target.TS) { - // "actions" is a reserved word within a TS reaction - if (name.equals("actions")) { - error(ACTIONS_MESSAGE + name, feature); - } - } - } - - /** - * Report whether a given reactor has dependencies on a cyclic - * instantiation pattern. This means the reactor has an instantiation - * in it -- directly or in one of its contained reactors -- that is - * self-referential. - * @param reactor The reactor definition to find out whether it has any - * dependencies on cyclic instantiations. - * @param cycleSet The set of all reactors that are part of an - * instantiation cycle. - * @param visited The set of nodes already visited in this graph traversal. - */ - private boolean dependsOnCycle(Reactor reactor, Set cycleSet, - Set visited) { - Set origins = info.instantiationGraph.getUpstreamAdjacentNodes(reactor); - if (visited.contains(reactor)) { - return false; - } else { - visited.add(reactor); - for (Reactor it : origins) { - if (cycleSet.contains(it) || dependsOnCycle(it, cycleSet, visited)) { - // Reached a cycle. - return true; - } - } - } - return false; - } - - /** - * Report whether a given imported reactor is used in this resource or not. - * @param reactor The imported reactor to check whether it is used. - */ - private boolean isUnused(ImportedReactor reactor) { - TreeIterator instantiations = reactor.eResource().getAllContents(); - TreeIterator subclasses = reactor.eResource().getAllContents(); - - boolean instantiationsCheck = true; - while (instantiations.hasNext() && instantiationsCheck) { - EObject obj = instantiations.next(); - if (!(obj instanceof Instantiation)) { - continue; - } - Instantiation inst = (Instantiation) obj; - instantiationsCheck &= (inst.getReactorClass() != reactor && inst.getReactorClass() != reactor.getReactorClass()); - } - - boolean subclassesCheck = true; - while (subclasses.hasNext() && subclassesCheck) { - EObject obj = subclasses.next(); - if (!(obj instanceof Reactor)) { - continue; - } - Reactor subclass = (Reactor) obj; - for (ReactorDecl decl : subclass.getSuperClasses()) { - subclassesCheck &= (decl != reactor && decl != reactor.getReactorClass()); - } - } - return instantiationsCheck && subclassesCheck; - } + // These methods are automatically invoked on AST nodes matching + // the types of their arguments. + // CheckType.FAST ensures that these checks run whenever a file is modified. + // Alternatives are CheckType.NORMAL (when saving) and + // CheckType.EXPENSIVE (only when right-click, validate). + // FIXME: What is the default when nothing is specified? + // These methods are listed in alphabetical order, and, although + // it is isn't strictly required, follow a naming convention + // checkClass, where Class is the AST class, where possible. - // ////////////////////////////////////////////////// - // // Functions to set up data structures for performing checks. - // FAST ensures that these checks run whenever a file is modified. - // Alternatives are NORMAL (when saving) and EXPENSIVE (only when right-click, validate). - - // ////////////////////////////////////////////////// - // // The following checks are in alphabetical order. @Check(CheckType.FAST) public void checkAction(Action action) { checkName(action.getName(), Literals.VARIABLE__NAME); @@ -313,11 +143,11 @@ public void checkAction(Action action) { ); } if (action.getPolicy() != null && - !spacingViolationPolicies.contains(action.getPolicy())) { + !SPACING_VIOLATION_POLICIES.contains(action.getPolicy())) { error( "Unrecognized spacing violation policy: " + action.getPolicy() + ". Available policies are: " + - String.join(", ", spacingViolationPolicies) + ".", + String.join(", ", SPACING_VIOLATION_POLICIES) + ".", Literals.ACTION__POLICY); } } @@ -368,32 +198,6 @@ public void checkAssignment(Assignment assignment) { // Specifically for C: list can only be literal or time lists } - @Check(CheckType.FAST) - public void checkWidthSpec(WidthSpec widthSpec) { - if (!this.target.supportsMultiports()) { - error("Multiports and banks are currently not supported by the given target.", - Literals.WIDTH_SPEC__TERMS); - } else { - for (WidthTerm term : widthSpec.getTerms()) { - if (term.getParameter() != null) { - if (!this.target.supportsParameterizedWidths()) { - error("Parameterized widths are not supported by this target.", Literals.WIDTH_SPEC__TERMS); - } - } else if (term.getPort() != null) { - // Widths given with `widthof()` are not supported (yet?). - // This feature is currently only used for after delays. - error("widthof is not supported.", Literals.WIDTH_SPEC__TERMS); - } else if (term.getCode() != null) { - if (this.target != Target.CPP) { - error("This target does not support width given as code.", Literals.WIDTH_SPEC__TERMS); - } - } else if (term.getWidth() < 0) { - error("Width must be a positive integer.", Literals.WIDTH_SPEC__TERMS); - } - } - } - } - @Check(CheckType.FAST) public void checkConnection(Connection connection) { @@ -545,39 +349,6 @@ public void checkConnection(Connection connection) { } } - /** - * Return true if the two types match. Unfortunately, xtext does not - * seem to create a suitable equals() method for Type, so we have to - * do this manually. - */ - private boolean sameType(Type type1, Type type2) { - if (type1 == null) { - return type2 == null; - } - if (type2 == null) { - return type1 == null; - } - // Most common case first. - if (type1.getId() != null) { - if (type1.getStars() != null) { - if (type2.getStars() == null) return false; - if (type1.getStars().size() != type2.getStars().size()) return false; - } - return (type1.getId().equals(type2.getId())); - } - - // Type specification in the grammar is: - // (time?='time' (arraySpec=ArraySpec)?) | ((id=(DottedName) (stars+='*')* ('<' typeParms+=TypeParm (',' typeParms+=TypeParm)* '>')? (arraySpec=ArraySpec)?) | code=Code); - if (type1.isTime()) { - if (!type2.isTime()) return false; - // Ignore the arraySpec because that is checked when connection - // is checked for balance. - return true; - } - // Type must be given in a code body - return type1.getCode().getBody().equals(type2.getCode().getBody()); - } - @Check(CheckType.FAST) public void checkDeadline(Deadline deadline) { if (isCBasedTarget() && @@ -590,18 +361,74 @@ public void checkDeadline(Deadline deadline) { } @Check(CheckType.FAST) - public void checkSTPOffset(STP stp) { - if (isCBasedTarget() && - this.info.overflowingDeadlines.contains(stp)) { - error( - "STP offset exceeds the maximum of " + - TimeValue.MAX_LONG_DEADLINE + " nanoseconds.", - Literals.DEADLINE__DELAY); + public void checkHost(Host host) { + String addr = host.getAddr(); + String user = host.getUser(); + if (user != null && !user.matches(USERNAME_REGEX)) { + warning( + "Invalid user name.", + Literals.HOST__USER + ); } - } - - @Check(CheckType.FAST) + if (host instanceof IPV4Host && !addr.matches(IPV4_REGEX)) { + warning( + "Invalid IP address.", + Literals.HOST__ADDR + ); + } else if (host instanceof IPV6Host && !addr.matches(IPV6_REGEX)) { + warning( + "Invalid IP address.", + Literals.HOST__ADDR + ); + } else if (host instanceof NamedHost && !addr.matches(HOST_OR_FQN_REGEX)) { + warning( + "Invalid host name or fully qualified domain name.", + Literals.HOST__ADDR + ); + } + } + + @Check + public void checkImport(Import imp) { + if (toDefinition(imp.getReactorClasses().get(0)).eResource().getErrors().size() > 0) { + error("Error loading resource.", Literals.IMPORT__IMPORT_URI); // FIXME: print specifics. + return; + } + + // FIXME: report error if resource cannot be resolved. + for (ImportedReactor reactor : imp.getReactorClasses()) { + if (!isUnused(reactor)) { + return; + } + } + warning("Unused import.", Literals.IMPORT__IMPORT_URI); + } + + @Check + public void checkImportedReactor(ImportedReactor reactor) { + if (isUnused(reactor)) { + warning("Unused reactor class.", + Literals.IMPORTED_REACTOR__REACTOR_CLASS); + } + + if (info.instantiationGraph.hasCycles()) { + Set cycleSet = new HashSet<>(); + for (Set cycle : info.instantiationGraph.getCycles()) { + cycleSet.addAll(cycle); + } + if (dependsOnCycle(toDefinition(reactor), cycleSet, new HashSet<>())) { + error("Imported reactor '" + toDefinition(reactor).getName() + + "' has cyclic instantiation in it.", Literals.IMPORTED_REACTOR__REACTOR_CLASS); + } + } + } + + @Check(CheckType.FAST) public void checkInput(Input input) { + Reactor parent = (Reactor)input.eContainer(); + if (parent.isMain() || parent.isFederated()) { + error("Main reactor cannot have inputs.", Literals.VARIABLE__NAME); + } checkName(input.getName(), Literals.VARIABLE__NAME); if (target.requiresTypes) { if (input.getType() == null) { @@ -680,28 +507,26 @@ public void checkKeyValuePair(KeyValuePair param) { // Make sure the key is valid. if (prop == null) { - List optionNames = new ArrayList<>(); - for (TargetProperty p : TargetProperty.getOptions()) { - optionNames.add(p.name()); - } + String options = TargetProperty.getOptions().stream() + .map(p -> p.description).sorted() + .collect(Collectors.joining(", ")); warning( "Unrecognized target parameter: " + param.getName() + - ". Recognized parameters are: " + - String.join(", ", optionNames) + ".", + ". Recognized parameters are: " + options, Literals.KEY_VALUE_PAIR__NAME); - } + } else { + // Check whether the property is supported by the target. + if (!prop.supportedBy.contains(this.target)) { + warning( + "The target parameter: " + param.getName() + + " is not supported by the " + this.target + + " target and will thus be ignored.", + Literals.KEY_VALUE_PAIR__NAME); + } - // Check whether the property is supported by the target. - if (!prop.supportedBy.contains(this.target)) { - warning( - "The target parameter: " + param.getName() + - " is not supported by the " + this.target + - " target and will thus be ignored.", - Literals.KEY_VALUE_PAIR__NAME); + // Report problem with the assigned value. + prop.type.check(param.getValue(), param.getName(), this); } - - // Report problem with the assigned value. - prop.type.check(param.getValue(), param.getName(), this); for (String it : targetPropertyErrors) { error(it, Literals.KEY_VALUE_PAIR__VALUE); @@ -715,21 +540,6 @@ public void checkKeyValuePair(KeyValuePair param) { } } - @Check(CheckType.FAST) - public void checkOutput(Output output) { - checkName(output.getName(), Literals.VARIABLE__NAME); - if (this.target.requiresTypes) { - if (output.getType() == null) { - error("Output must have a type.", Literals.TYPED_VARIABLE__TYPE); - } - } - - // Variable width multiports are not supported (yet?). - if (output.getWidthSpec() != null && output.getWidthSpec().isOfVariableLength()) { - error("Variable-width multiports are not supported.", Literals.PORT__WIDTH_SPEC); - } - } - @Check(CheckType.FAST) public void checkModel(Model model) { // Since we're doing a fast check, we only want to update @@ -746,6 +556,25 @@ public void updateModelInfo(Model model) { info.update(model, errorReporter); } + @Check(CheckType.FAST) + public void checkOutput(Output output) { + Reactor parent = (Reactor)output.eContainer(); + if (parent.isMain() || parent.isFederated()) { + error("Main reactor cannot have outputs.", Literals.VARIABLE__NAME); + } + checkName(output.getName(), Literals.VARIABLE__NAME); + if (this.target.requiresTypes) { + if (output.getType() == null) { + error("Output must have a type.", Literals.TYPED_VARIABLE__TYPE); + } + } + + // Variable width multiports are not supported (yet?). + if (output.getWidthSpec() != null && output.getWidthSpec().isOfVariableLength()) { + error("Variable-width multiports are not supported.", Literals.PORT__WIDTH_SPEC); + } + } + @Check(CheckType.FAST) public void checkParameter(Parameter param) { checkName(param.getName(), Literals.PARAMETER__NAME); @@ -849,7 +678,7 @@ public void checkPreamble(Preamble preamble) { } } - @Check(CheckType.FAST) + @Check(CheckType.FAST) public void checkReaction(Reaction reaction) { if (reaction.getTriggers() == null || reaction.getTriggers().size() == 0) { @@ -875,7 +704,7 @@ public void checkReaction(Reaction reaction) { } } - // Make sure input sources have no container and output sources do. + // Make sure input sources have no container and output sources do. // Also check that a source is not already listed as a trigger. for (VarRef source : reaction.getSources()) { if (triggers.contains(source.getVariable())) { @@ -1002,23 +831,17 @@ public void checkReaction(Reaction reaction) { // FIXME: improve error message. } - private int countMainOrFederated(TreeIterator iter) { - int nMain = 0; - while (iter.hasNext()) { - EObject obj = iter.next(); - if (!(obj instanceof Reactor)) { - continue; - } - Reactor r = (Reactor) obj; - if (r.isMain() || r.isFederated()) { - nMain++; - } - } - return nMain; - } - @Check(CheckType.FAST) public void checkReactor(Reactor reactor) throws IOException { + Set superClasses = ASTUtils.superClasses(reactor); + if (superClasses == null) { + error( + "Problem with superclasses: Either they form a cycle or are not defined", + Literals.REACTOR_DECL__NAME + ); + // Continue checks, but without any superclasses. + superClasses = new LinkedHashSet<>(); + } String name = FileConfig.nameWithoutExtension(reactor.eResource()); if (reactor.getName() == null) { if (!reactor.isFederated() && !reactor.isMain()) { @@ -1026,48 +849,43 @@ public void checkReactor(Reactor reactor) throws IOException { "Reactor must be named.", Literals.REACTOR_DECL__NAME ); + // Prevent NPE in tests below. + return; } - // Prevent NPE in tests below. - return; - } else { - TreeIterator iter = reactor.eResource().getAllContents(); - if (reactor.isFederated() || reactor.isMain()) { - if(!reactor.getName().equals(name)) { - // Make sure that if the name is omitted, the reactor is indeed main. - error( - "Name of main reactor must match the file name (or be omitted).", - Literals.REACTOR_DECL__NAME - ); - } - - // Do not allow multiple main/federated reactors. - int nMain = countMainOrFederated(iter); - if (nMain > 1) { - EAttribute attribute = Literals.REACTOR__MAIN; - if (reactor.isFederated()) { - attribute = Literals.REACTOR__FEDERATED; - } - if (reactor.isMain() || reactor.isFederated()) { - error( - "Multiple definitions of main or federated reactor.", - attribute - ); - } - } - } else { - int nMain = countMainOrFederated(iter); - if (nMain > 0 && reactor.getName().equals(name)) { - error( - "Name conflict with main reactor.", - Literals.REACTOR_DECL__NAME - ); + } + TreeIterator iter = reactor.eResource().getAllContents(); + if (reactor.isFederated() || reactor.isMain()) { + if(reactor.getName() != null && !reactor.getName().equals(name)) { + // Make sure that if the name is given, it matches the expected name. + error( + "Name of main reactor must match the file name (or be omitted).", + Literals.REACTOR_DECL__NAME + ); + } + // Do not allow multiple main/federated reactors. + int nMain = countMainOrFederated(iter); + if (nMain > 1) { + EAttribute attribute = Literals.REACTOR__MAIN; + if (reactor.isFederated()) { + attribute = Literals.REACTOR__FEDERATED; } + error( + "Multiple definitions of main or federated reactor.", + attribute + ); + } + } else { + // Not federated or main. + int nMain = countMainOrFederated(iter); + if (nMain > 0 && reactor.getName().equals(name)) { + error( + "Name conflict with main reactor.", + Literals.REACTOR_DECL__NAME + ); } } - // If there is a main reactor (with no name) then disallow other (non-main) reactors - // matching the file name. - + // Check for illegal names. checkName(reactor.getName(), Literals.REACTOR_DECL__NAME); // C++ reactors may not be called 'preamble' @@ -1095,18 +913,17 @@ public void checkReactor(Reactor reactor) throws IOException { variables.addAll(reactor.getTimers()); // Perform checks on super classes. - EList superClasses = reactor.getSuperClasses() != null ? reactor.getSuperClasses() : new BasicEList<>(); - for (ReactorDecl superClass : superClasses) { + for (Reactor superClass : superClasses) { HashSet conflicts = new HashSet<>(); // Detect input conflicts - checkConflict(toDefinition(superClass).getInputs(), reactor.getInputs(), variables, conflicts); + checkConflict(superClass.getInputs(), reactor.getInputs(), variables, conflicts); // Detect output conflicts - checkConflict(toDefinition(superClass).getOutputs(), reactor.getOutputs(), variables, conflicts); + checkConflict(superClass.getOutputs(), reactor.getOutputs(), variables, conflicts); // Detect output conflicts - checkConflict(toDefinition(superClass).getActions(), reactor.getActions(), variables, conflicts); + checkConflict(superClass.getActions(), reactor.getActions(), variables, conflicts); // Detect conflicts - for (Timer timer : toDefinition(superClass).getTimers()) { + for (Timer timer : superClass.getTimers()) { List filteredVariables = new ArrayList<>(variables); filteredVariables.removeIf(it -> reactor.getTimers().contains(it)); if (hasNameConflict(timer, filteredVariables)) { @@ -1123,92 +940,14 @@ public void checkReactor(Reactor reactor) throws IOException { names.add(it.getName()); } error( - String.format("Cannot extend %s due to the following conflicts: %s.", superClass.getName(), String.join(",", names)), + String.format("Cannot extend %s due to the following conflicts: %s.", + superClass.getName(), String.join(",", names)), Literals.REACTOR__SUPER_CLASSES ); } } } - /** - * For each input, report a conflict if: - * 1) the input exists and the type doesn't match; or - * 2) the input has a name clash with variable that is not an input. - * @param superVars List of typed variables of a particular kind (i.e., - * inputs, outputs, or actions), found in a super class. - * @param sameKind Typed variables of the same kind, found in the subclass. - * @param allOwn Accumulator of non-conflicting variables incorporated in the - * subclass. - * @param conflicts Set of variables that are in conflict, to be used by this - * function to report conflicts. - */ - private void checkConflict (EList superVars, - EList sameKind, List allOwn, - HashSet conflicts) { - for (T superVar : superVars) { - T match = null; - for (T it : sameKind) { - if (it.getName().equals(superVar.getName())) { - match = it; - break; - } - } - List rest = new ArrayList<>(allOwn); - rest.removeIf(it -> sameKind.contains(it)); - - if ((match != null && superVar.getType() != match.getType()) || hasNameConflict(superVar, rest)) { - conflicts.add(superVar); - } else { - allOwn.add(superVar); - } - } - } - - /** - * Report whether the name of the given element matches any variable in - * the ones to check against. - * @param element The element to compare against all variables in the given iterable. - * @param toCheckAgainst Iterable variables to compare the given element against. - */ - private boolean hasNameConflict(Variable element, - Iterable toCheckAgainst) { - int numNameConflicts = 0; - for (Variable it : toCheckAgainst) { - if (it.getName().equals(element.getName())) { - numNameConflicts++; - } - } - return numNameConflicts > 0; - } - - @Check(CheckType.FAST) - public void checkHost(Host host) { - String addr = host.getAddr(); - String user = host.getUser(); - if (user != null && !user.matches(usernameRegex)) { - warning( - "Invalid user name.", - Literals.HOST__USER - ); - } - if (host instanceof IPV4Host && !addr.matches(ipv4Regex)) { - warning( - "Invalid IP address.", - Literals.HOST__ADDR - ); - } else if (host instanceof IPV6Host && !addr.matches(ipv6Regex)) { - warning( - "Invalid IP address.", - Literals.HOST__ADDR - ); - } else if (host instanceof NamedHost && !addr.matches(hostOrFQNRegex)) { - warning( - "Invalid host name or fully qualified domain name.", - Literals.HOST__ADDR - ); - } - } - /** * Check if the requested serialization is supported. */ @@ -1281,6 +1020,17 @@ public void checkState(StateVar stateVar) { } } + @Check(CheckType.FAST) + public void checkSTPOffset(STP stp) { + if (isCBasedTarget() && + this.info.overflowingSTP.contains(stp)) { + error( + "STP offset exceeds the maximum of " + + TimeValue.MAX_LONG_DEADLINE + " nanoseconds.", + Literals.STP__VALUE); + } + } + @Check(CheckType.FAST) public void checkTargetDecl(TargetDecl target) throws IOException { Optional targetOpt = Target.forName(target.getName()); @@ -1359,30 +1109,33 @@ public void checkTargetProperties(KeyValuePairs targetProperties) { ); } } - } - - @Check(CheckType.FAST) - public void checkValueAsTime(Value value) { - EObject container = value.eContainer(); + - if (container instanceof Timer || container instanceof Action || - container instanceof Connection || container instanceof Deadline) { - // If parameter is referenced, check that it is of the correct type. - if (value.getParameter() != null) { - if (!isOfTimeType(value.getParameter()) && target.requiresTypes) { - error("Parameter is not of time type", - Literals.VALUE__PARAMETER); - } - } else if (value.getTime() == null) { - if (value.getLiteral() != null && !isZero(value.getLiteral())) { - if (isInteger(value.getLiteral())) { - error("Missing time unit.", Literals.VALUE__LITERAL); - } else { - error("Invalid time literal.", - Literals.VALUE__LITERAL); - } - } else if (value.getCode() != null) { - error("Invalid time literal.", Literals.VALUE__CODE); + EList schedulerTargetProperties = + new BasicEList<>(targetProperties.getPairs()); + schedulerTargetProperties.removeIf(pair -> TargetProperty + .forName(pair.getName()) != TargetProperty.SCHEDULER); + KeyValuePair schedulerTargetProperty = schedulerTargetProperties + .size() > 0 ? schedulerTargetProperties.get(0) : null; + if (schedulerTargetProperty != null) { + String schedulerName = schedulerTargetProperty.getValue().getId(); + if (!TargetProperty.SchedulerOption.valueOf(schedulerName) + .prioritizesDeadline()) { + // Check if a deadline is assigned to any reaction + if (info.model.getReactors().stream().filter(reactor -> { + // Filter reactors that contain at least one reaction that + // has a deadline handler. + return ASTUtils.allReactions(reactor).stream() + .filter(reaction -> { + return reaction.getDeadline() != null; + }).count() > 0; + }).count() > 0) { + warning("This program contains deadlines, but the chosen " + + schedulerName + + " scheduler does not prioritize reaction execution " + + "based on deadlines. This might result in a sub-optimal " + + "scheduling.", schedulerTargetProperty, + Literals.KEY_VALUE_PAIR__VALUE); } } } @@ -1420,21 +1173,48 @@ else if (this.target == Target.Python) { } @Check(CheckType.FAST) - public void checkVarRef(VarRef varRef) { - // check correct usage of interleaved - if (varRef.isInterleaved()) { - if (this.target != Target.CPP && !isCBasedTarget() && this.target != Target.Python) { - error("This target does not support interleaved port references.", Literals.VAR_REF__INTERLEAVED); - } - if (!(varRef.eContainer() instanceof Connection)) { - error("interleaved can only be used in connections.", Literals.VAR_REF__INTERLEAVED); - } + public void checkValueAsTime(Value value) { + EObject container = value.eContainer(); - if (varRef.getVariable() instanceof Port) { - // This test only works correctly if the variable is actually a port. If it is not a port, other - // validator rules will produce error messages. - if (varRef.getContainer() == null || varRef.getContainer().getWidthSpec() == null || - ((Port) varRef.getVariable()).getWidthSpec() == null + if (container instanceof Timer || container instanceof Action || + container instanceof Connection || container instanceof Deadline) { + // If parameter is referenced, check that it is of the correct type. + if (value.getParameter() != null) { + if (!isOfTimeType(value.getParameter()) && target.requiresTypes) { + error("Parameter is not of time type", + Literals.VALUE__PARAMETER); + } + } else if (value.getTime() == null) { + if (value.getLiteral() != null && !isZero(value.getLiteral())) { + if (isInteger(value.getLiteral())) { + error("Missing time unit.", Literals.VALUE__LITERAL); + } else { + error("Invalid time literal.", + Literals.VALUE__LITERAL); + } + } else if (value.getCode() != null) { + error("Invalid time literal.", Literals.VALUE__CODE); + } + } + } + } + + @Check(CheckType.FAST) + public void checkVarRef(VarRef varRef) { + // check correct usage of interleaved + if (varRef.isInterleaved()) { + if (this.target != Target.CPP && !isCBasedTarget() && this.target != Target.Python) { + error("This target does not support interleaved port references.", Literals.VAR_REF__INTERLEAVED); + } + if (!(varRef.eContainer() instanceof Connection)) { + error("interleaved can only be used in connections.", Literals.VAR_REF__INTERLEAVED); + } + + if (varRef.getVariable() instanceof Port) { + // This test only works correctly if the variable is actually a port. If it is not a port, other + // validator rules will produce error messages. + if (varRef.getContainer() == null || varRef.getContainer().getWidthSpec() == null || + ((Port) varRef.getVariable()).getWidthSpec() == null ) { error("interleaved can only be used for multiports contained within banks.", Literals.VAR_REF__INTERLEAVED); } @@ -1442,8 +1222,320 @@ public void checkVarRef(VarRef varRef) { } } - static String UNDERSCORE_MESSAGE = "Names of objects (inputs, outputs, actions, timers, parameters, state, reactor definitions, and reactor instantiation) may not start with \"__\": "; - static String ACTIONS_MESSAGE = "\"actions\" is a reserved word for the TypeScript target for objects (inputs, outputs, actions, timers, parameters, state, reactor definitions, and reactor instantiation): "; - static String RESERVED_MESSAGE = "Reserved words in the target language are not allowed for objects (inputs, outputs, actions, timers, parameters, state, reactor definitions, and reactor instantiation): "; + @Check(CheckType.FAST) + public void checkWidthSpec(WidthSpec widthSpec) { + if (!this.target.supportsMultiports()) { + error("Multiports and banks are currently not supported by the given target.", + Literals.WIDTH_SPEC__TERMS); + } else { + for (WidthTerm term : widthSpec.getTerms()) { + if (term.getParameter() != null) { + if (!this.target.supportsParameterizedWidths()) { + error("Parameterized widths are not supported by this target.", Literals.WIDTH_SPEC__TERMS); + } + } else if (term.getPort() != null) { + // Widths given with `widthof()` are not supported (yet?). + // This feature is currently only used for after delays. + error("widthof is not supported.", Literals.WIDTH_SPEC__TERMS); + } else if (term.getCode() != null) { + if (this.target != Target.CPP) { + error("This target does not support width given as code.", Literals.WIDTH_SPEC__TERMS); + } + } else if (term.getWidth() < 0) { + error("Width must be a positive integer.", Literals.WIDTH_SPEC__TERMS); + } + } + } + } + + ////////////////////////////////////////////////////////////// + //// Public methods. + /** + * Return the error reporter for this validator. + */ + public ValidatorErrorReporter getErrorReporter() { + return this.errorReporter; + } + + /** + * Implementation required by xtext to report validation errors. + */ + @Override + public ValidationMessageAcceptor getMessageAcceptor() { + return messageAcceptor == null ? this : messageAcceptor; + } + + /** + * Return a list of error messages for the target declaration. + */ + public List getTargetPropertyErrors() { + return this.targetPropertyErrors; + } + + ////////////////////////////////////////////////////////////// + //// Private methods. + + /** + * For each input, report a conflict if: + * 1) the input exists and the type doesn't match; or + * 2) the input has a name clash with variable that is not an input. + * @param superVars List of typed variables of a particular kind (i.e., + * inputs, outputs, or actions), found in a super class. + * @param sameKind Typed variables of the same kind, found in the subclass. + * @param allOwn Accumulator of non-conflicting variables incorporated in the + * subclass. + * @param conflicts Set of variables that are in conflict, to be used by this + * function to report conflicts. + */ + private void checkConflict ( + EList superVars, EList sameKind, List allOwn, HashSet conflicts + ) { + for (T superVar : superVars) { + T match = null; + for (T it : sameKind) { + if (it.getName().equals(superVar.getName())) { + match = it; + break; + } + } + List rest = new ArrayList<>(allOwn); + rest.removeIf(it -> sameKind.contains(it)); + + if ((match != null && superVar.getType() != match.getType()) || hasNameConflict(superVar, rest)) { + conflicts.add(superVar); + } else { + allOwn.add(superVar); + } + } + } + + /** + * Check the name of a feature for illegal substrings such as reserved + * identifiers and names with double leading underscores. + * @param name The name. + * @param feature The feature containing the name (for error reporting). + */ + private void checkName(String name, EStructuralFeature feature) { + + // Raises an error if the string starts with two underscores. + if (name.length() >= 2 && name.substring(0, 2).equals("__")) { + error(UNDERSCORE_MESSAGE + name, feature); + } + + if (this.target.isReservedIdent(name)) { + error(RESERVED_MESSAGE + name, feature); + } + + if (this.target == Target.TS) { + // "actions" is a reserved word within a TS reaction + if (name.equals("actions")) { + error(ACTIONS_MESSAGE + name, feature); + } + } + } + + /** + * Return the number of main or federated reactors declared. + * @param iter An iterator over all objects in the resource. + */ + private int countMainOrFederated(TreeIterator iter) { + int nMain = 0; + while (iter.hasNext()) { + EObject obj = iter.next(); + if (!(obj instanceof Reactor)) { + continue; + } + Reactor r = (Reactor) obj; + if (r.isMain() || r.isFederated()) { + nMain++; + } + } + return nMain; + } + + /** + * Report whether a given reactor has dependencies on a cyclic + * instantiation pattern. This means the reactor has an instantiation + * in it -- directly or in one of its contained reactors -- that is + * self-referential. + * @param reactor The reactor definition to find out whether it has any + * dependencies on cyclic instantiations. + * @param cycleSet The set of all reactors that are part of an + * instantiation cycle. + * @param visited The set of nodes already visited in this graph traversal. + */ + private boolean dependsOnCycle( + Reactor reactor, Set cycleSet, Set visited + ) { + Set origins = info.instantiationGraph.getUpstreamAdjacentNodes(reactor); + if (visited.contains(reactor)) { + return false; + } else { + visited.add(reactor); + for (Reactor it : origins) { + if (cycleSet.contains(it) || dependsOnCycle(it, cycleSet, visited)) { + // Reached a cycle. + return true; + } + } + } + return false; + } + + /** + * Report whether the name of the given element matches any variable in + * the ones to check against. + * @param element The element to compare against all variables in the given iterable. + * @param toCheckAgainst Iterable variables to compare the given element against. + */ + private boolean hasNameConflict(Variable element, + Iterable toCheckAgainst) { + int numNameConflicts = 0; + for (Variable it : toCheckAgainst) { + if (it.getName().equals(element.getName())) { + numNameConflicts++; + } + } + return numNameConflicts > 0; + } + + /** + * Return true if target is C or a C-based target like CCpp. + */ + private boolean isCBasedTarget() { + return (this.target == Target.C || this.target == Target.CCPP); + } + + /** + * Report whether a given imported reactor is used in this resource or not. + * @param reactor The imported reactor to check whether it is used. + */ + private boolean isUnused(ImportedReactor reactor) { + TreeIterator instantiations = reactor.eResource().getAllContents(); + TreeIterator subclasses = reactor.eResource().getAllContents(); + + boolean instantiationsCheck = true; + while (instantiations.hasNext() && instantiationsCheck) { + EObject obj = instantiations.next(); + if (!(obj instanceof Instantiation)) { + continue; + } + Instantiation inst = (Instantiation) obj; + instantiationsCheck &= (inst.getReactorClass() != reactor && inst.getReactorClass() != reactor.getReactorClass()); + } + + boolean subclassesCheck = true; + while (subclasses.hasNext() && subclassesCheck) { + EObject obj = subclasses.next(); + if (!(obj instanceof Reactor)) { + continue; + } + Reactor subclass = (Reactor) obj; + for (ReactorDecl decl : subclass.getSuperClasses()) { + subclassesCheck &= (decl != reactor && decl != reactor.getReactorClass()); + } + } + return instantiationsCheck && subclassesCheck; + } + + /** + * Return true if the two types match. Unfortunately, xtext does not + * seem to create a suitable equals() method for Type, so we have to + * do this manually. + */ + private boolean sameType(Type type1, Type type2) { + if (type1 == null) { + return type2 == null; + } + if (type2 == null) { + return type1 == null; + } + // Most common case first. + if (type1.getId() != null) { + if (type1.getStars() != null) { + if (type2.getStars() == null) return false; + if (type1.getStars().size() != type2.getStars().size()) return false; + } + return (type1.getId().equals(type2.getId())); + } + + // Type specification in the grammar is: + // (time?='time' (arraySpec=ArraySpec)?) | ((id=(DottedName) (stars+='*')* ('<' typeParms+=TypeParm (',' typeParms+=TypeParm)* '>')? (arraySpec=ArraySpec)?) | code=Code); + if (type1.isTime()) { + if (!type2.isTime()) return false; + // Ignore the arraySpec because that is checked when connection + // is checked for balance. + return true; + } + // Type must be given in a code body + return type1.getCode().getBody().equals(type2.getCode().getBody()); + } + + ////////////////////////////////////////////////////////////// + //// Private fields. + + /** The error reporter. */ + private ValidatorErrorReporter errorReporter + = new ValidatorErrorReporter(getMessageAcceptor(), new ValidatorStateAccess()); + + /** Helper class containing information about the model. */ + private ModelInfo info = new ModelInfo(); + + @Inject(optional = true) + private ValidationMessageAcceptor messageAcceptor; + + /** The declared target. */ + private Target target; + + private List targetPropertyErrors = new ArrayList<>(); + + private List targetPropertyWarnings = new ArrayList<>(); + + ////////////////////////////////////////////////////////////// + //// Private static constants. + + private static String ACTIONS_MESSAGE + = "\"actions\" is a reserved word for the TypeScript target for objects " + + "(inputs, outputs, actions, timers, parameters, state, reactor definitions, " + + "and reactor instantiation): "; + + private static String HOST_OR_FQN_REGEX + = "^([a-z0-9]+(-[a-z0-9]+)*)|(([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,})$"; + + /** + * Regular expression to check the validity of IPV4 addresses (due to David M. Syzdek). + */ + private static String IPV4_REGEX = "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + + "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"; + + /** + * Regular expression to check the validity of IPV6 addresses (due to David M. Syzdek), + * with minor adjustment to allow up to six IPV6 segments (without truncation) in front + * of an embedded IPv4-address. + **/ + private static String IPV6_REGEX = + "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" + + "([0-9a-fA-F]{1,4}:){1,7}:|" + + "([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" + + "([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" + + "([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" + + "([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" + + "([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" + + "[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" + + ":((:[0-9a-fA-F]{1,4}){1,7}|:)|" + + "fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" + + "::(ffff(:0{1,4}){0,1}:){0,1}" + IPV4_REGEX + "|" + + "([0-9a-fA-F]{1,4}:){1,4}:" + IPV4_REGEX + "|" + + "([0-9a-fA-F]{1,4}:){1,6}" + IPV4_REGEX + ")"; + + private static String RESERVED_MESSAGE = "Reserved words in the target language are not allowed for objects " + + "(inputs, outputs, actions, timers, parameters, state, reactor definitions, and reactor instantiation): "; + + private static List SPACING_VIOLATION_POLICIES = List.of("defer", "drop", "replace"); + + private static String UNDERSCORE_MESSAGE = "Names of objects (inputs, outputs, actions, timers, parameters, " + + "state, reactor definitions, and reactor instantiation) may not start with \"__\": "; + + private static String USERNAME_REGEX = "^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\\$)$"; } diff --git a/pom.xml b/pom.xml index 83cfdcd3a4..335b368112 100644 --- a/pom.xml +++ b/pom.xml @@ -17,13 +17,14 @@ 1.4 - + 1.6.10 0.12.0 2.12.1 2.25.0 2.3.0 + 3.23.0 11 diff --git a/test/C/src/RepeatedInheritance.lf b/test/C/src/RepeatedInheritance.lf new file mode 100644 index 0000000000..9081eabaa9 --- /dev/null +++ b/test/C/src/RepeatedInheritance.lf @@ -0,0 +1,37 @@ +/** + * This tests for the situation where a reactor extends two other reactors + * that each extend a common reactor. + * @author{Edward A. Lee} + */ +target C { + timeout: 5 sec, + fast: true +} + +import Count from "lib/Count.lf"; +import TestCount from "lib/TestCount.lf"; + +reactor D { + input d:int; +} +reactor C extends D { + input c:int; +} +reactor B extends D { + input b:int; +} +reactor A extends B, C { + input a:int; + output out:int; + reaction(a, b, c, d) -> out {= + SET(out, a->value + b->value + c->value + d->value); + =} +} + +main reactor { + c = new Count(); + a = new A(); + t = new TestCount(start = 4, stride = 4, num_inputs = 6); + (c.out)+ -> a.a, a.b, a.c, a.d; + a.out -> t.in; +} diff --git a/test/C/src/concurrent/StopThreaded.lf b/test/C/src/concurrent/StopThreaded.lf index f73a67e9aa..8a8f8ca911 100644 --- a/test/C/src/concurrent/StopThreaded.lf +++ b/test/C/src/concurrent/StopThreaded.lf @@ -6,7 +6,9 @@ */ target C { timeout: 11 msec, - threads: 4 + threads: 4, + build-type: RelWithDebInfo, + // logging: DEBUG }; import Sender from "../lib/LoopedActionSender.lf" @@ -18,13 +20,15 @@ reactor Consumer { tag_t current_tag = get_current_tag(); if (compare_tags(current_tag, (tag_t) { .time = MSEC(10) + get_start_time(), .microstep = 9}) > 0) { - // The reaction should not have been called at tags larger than (10 msec, 9) - fprintf(stderr, "ERROR: Invoked reaction(in) at tag bigger than shutdown.\n"); - exit(1); + // The reaction should not have been called at tags larger than (10 + // msec, 9) + char time[255]; + error_print_and_exit("Invoked reaction(in) at tag (%llu, %d) which is bigger than shutdown.", + current_tag.time - get_start_time(), current_tag.microstep); } else if (compare_tags(current_tag, (tag_t) { .time = MSEC(10) + get_start_time(), .microstep = 8}) == 0) { // Call request_stop() at relative tag (10 msec, 8) - printf("Requesting stop.\n"); + info_print("Requesting stop."); request_stop(); } else if (compare_tags(current_tag, (tag_t) { .time = MSEC(10) + get_start_time(), .microstep = 9}) == 0) { @@ -36,22 +40,20 @@ reactor Consumer { reaction(shutdown) {= tag_t current_tag = get_current_tag(); - printf("Shutdown invoked at tag (%lld, %u).\n", current_tag.time - get_start_time(), current_tag.microstep); + info_print("Shutdown invoked at tag (%lld, %u).", current_tag.time - get_start_time(), current_tag.microstep); // Check to see if shutdown is called at relative tag (10 msec, 9) if (compare_tags(current_tag, (tag_t) { .time = MSEC(10) + get_start_time(), .microstep = 9}) == 0 && self->reaction_invoked_correctly == true) { - printf("SUCCESS: successfully enforced stop.\n"); + info_print("SUCCESS: successfully enforced stop."); } else if(compare_tags(current_tag, (tag_t) { .time = MSEC(10) + get_start_time(), .microstep = 9}) > 0) { - fprintf(stderr,"ERROR: Shutdown invoked at tag (%llu, %d). Failed to enforce timeout.\n", + error_print_and_exit("Shutdown invoked at tag (%llu, %d). Failed to enforce timeout.", current_tag.time - get_start_time(), current_tag.microstep); - exit(1); } else if (self->reaction_invoked_correctly == false) { // Check to see if reactions were called correctly - fprintf(stderr,"ERROR: Failed to invoke reaction(in) at tag (%llu, %d).\n", + error_print_and_exit("Failed to invoke reaction(in) at tag (%llu, %d).", current_tag.time - get_start_time(), current_tag.microstep); - exit(1); } =} } diff --git a/test/C/src/docker/FilesPropertyContainerized.lf b/test/C/src/docker/FilesPropertyContainerized.lf new file mode 100644 index 0000000000..f9a1a240f3 --- /dev/null +++ b/test/C/src/docker/FilesPropertyContainerized.lf @@ -0,0 +1,15 @@ +target C { + files: "../include/hello.h", + docker: true +} + +preamble {= + #include "hello.h" +=} + +main reactor { + reaction(startup) {= + hello_t hello; + printf("SUCCESS\n"); + =} +} diff --git a/test/C/src/federated/TopLevelArtifacts.lf b/test/C/src/federated/TopLevelArtifacts.lf index b6654f4e28..9cbf64673e 100644 --- a/test/C/src/federated/TopLevelArtifacts.lf +++ b/test/C/src/federated/TopLevelArtifacts.lf @@ -15,8 +15,6 @@ import TestCount from "../lib/TestCount.lf"; federated reactor { - input in:int; - output out:int; state successes:int(0); reaction (startup) {= self->successes++; @@ -27,15 +25,8 @@ schedule(act, 0); =} logical action act(0); - reaction (act) in -> out {= + reaction (act) {= self->successes++; - if (in->is_present) { - error_print_and_exit("Input is present in the top-level reactor!"); - } - SET(out, 1); - if (out->value != 1) { - error_print_and_exit("Ouput has unexpected value %d!", out->value); - } =} c = new Count(); diff --git a/test/Cpp/src/Timeout_Test.lf b/test/Cpp/src/Timeout_Test.lf index 1ec45272ca..ea4e200441 100644 --- a/test/Cpp/src/Timeout_Test.lf +++ b/test/Cpp/src/Timeout_Test.lf @@ -41,4 +41,4 @@ main reactor Timeout_Test { producer = new Sender(break_interval = 1 msec); producer.out -> consumer.in; -} \ No newline at end of file +} diff --git a/test/Python/src/docker/FilesPropertyContainerized.lf b/test/Python/src/docker/FilesPropertyContainerized.lf new file mode 100644 index 0000000000..8d166c53ed --- /dev/null +++ b/test/Python/src/docker/FilesPropertyContainerized.lf @@ -0,0 +1,34 @@ +target Python { + files: "../include/hello.py", + docker: true +}; + +preamble {= + try: + import hello + except: + request_stop() +=} + +main reactor { + preamble {= + try: + import hello + except: + request_stop() + =} + state passed(false); + timer t(1 msec); + + reaction(t) {= + self.passed = True + =} + + reaction(shutdown) {= + if not self.passed: + print("Failed to import file listed in files target property") + exit(1) + else: + print("PASSED") + =} +} \ No newline at end of file