diff --git a/.github/actions/push-rti-docker/action.yml b/.github/actions/push-rti-docker/action.yml new file mode 100644 index 0000000000..eece007544 --- /dev/null +++ b/.github/actions/push-rti-docker/action.yml @@ -0,0 +1,49 @@ +name: Push RTI to Docker Hub +description: Build and push the RTI image to Docker Hub +inputs: + tag: + description: 'The tag of the RTI image to build and push' + required: true + default: 'latest' + DOCKERHUB_USERNAME: + description: 'The username to log in to Docker Hub' + required: true + DOCKERHUB_TOKEN: + description: 'The token to log in to Docker Hub' + required: true + latest: + description: 'Also push as latest tag if true' + default: 'false' +runs: + using: "composite" + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ inputs.DOCKERHUB_USERNAME }} + password: ${{ inputs.DOCKERHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Check out lingua-franca repository + uses: actions/checkout@v3 + with: + submodules: recursive + fetch-depth: 0 + - name: Build and push + uses: docker/build-push-action@v6 + with: + file: ./core/src/main/resources/lib/c/reactor-c/core/federated/RTI/rti.Dockerfile + context: ./core/src/main/resources/lib/c/reactor-c + platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/riscv64 + push: true + tags: lflang/rti:${{ inputs.tag }} + if: ${{ inputs.latest == 'false' }} + - name: Build and push as latest + uses: docker/build-push-action@v6 + with: + file: ./core/src/main/resources/lib/c/reactor-c/core/federated/RTI/rti.Dockerfile + context: ./core/src/main/resources/lib/c/reactor-c + platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/riscv64 + push: true + tags: lflang/rti:${{ inputs.tag }}, lflang/rti:latest + if: ${{ inputs.latest == 'true' }} diff --git a/.github/workflows/c-zephyr-tests.yml b/.github/workflows/c-zephyr-tests.yml index 2a69452506..53ce259cda 100644 --- a/.github/workflows/c-zephyr-tests.yml +++ b/.github/workflows/c-zephyr-tests.yml @@ -39,27 +39,27 @@ jobs: path: core/src/main/resources/lib/c/reactor-c ref: ${{ inputs.runtime-ref }} if: ${{ inputs.runtime-ref }} - - name: Run Zephyr smoke tests - run: | - ./gradlew core:integrationTest \ - --tests org.lflang.tests.runtime.CZephyrTest.buildZephyrUnthreaded* \ - --tests org.lflang.tests.runtime.CZephyrTest.buildZephyrThreaded* core:integrationTestCodeCoverageReport - ./.github/scripts/run-zephyr-tests.sh test/C/src-gen - rm -rf test/C/src-gen - - name: Run basic tests - run: | - ./gradlew core:integrationTest --tests org.lflang.tests.runtime.CZephyrTest.buildBasic* core:integrationTestCodeCoverageReport - ./.github/scripts/run-zephyr-tests.sh test/C/src-gen - rm -rf test/C/src-gen - - name: Run concurrent tests - run: | - ./gradlew core:integrationTest --tests org.lflang.tests.runtime.CZephyrTest.buildConcurrent* core:integrationTestCodeCoverageReport - ./.github/scripts/run-zephyr-tests.sh test/C/src-gen - rm -rf test/C/src-gen - - name: Run Zephyr board tests - run: | - ./gradlew core:integrationTest --tests org.lflang.tests.runtime.CZephyrTest.buildZephyrBoards* core:integrationTestCodeCoverageReport - rm -rf test/C/src-gen + # - name: Run Zephyr smoke tests + # run: | + # ./gradlew core:integrationTest \ + # --tests org.lflang.tests.runtime.CZephyrTest.buildZephyrUnthreaded* \ + # --tests org.lflang.tests.runtime.CZephyrTest.buildZephyrThreaded* core:integrationTestCodeCoverageReport + # ./.github/scripts/run-zephyr-tests.sh test/C/src-gen + # rm -rf test/C/src-gen + # - name: Run basic tests + # run: | + # ./gradlew core:integrationTest --tests org.lflang.tests.runtime.CZephyrTest.buildBasic* core:integrationTestCodeCoverageReport + # ./.github/scripts/run-zephyr-tests.sh test/C/src-gen + # rm -rf test/C/src-gen + # - name: Run concurrent tests + # run: | + # ./gradlew core:integrationTest --tests org.lflang.tests.runtime.CZephyrTest.buildConcurrent* core:integrationTestCodeCoverageReport + # ./.github/scripts/run-zephyr-tests.sh test/C/src-gen + # rm -rf test/C/src-gen + # - name: Run Zephyr board tests + # run: | + # ./gradlew core:integrationTest --tests org.lflang.tests.runtime.CZephyrTest.buildZephyrBoards* core:integrationTestCodeCoverageReport + # rm -rf test/C/src-gen - name: Smoke test of lf-west-template run: | export LFC=$(pwd)/bin/lfc-dev @@ -68,8 +68,8 @@ jobs: west lfc apps/NrfBlinky/src/NrfBlinky.lf --lfc $LFC --build "-p always" west lfc apps/NrfBlinky/src/NrfToggleGPIO.lf --lfc $LFC --build "-p always" west build -b qemu_cortex_m3 -p always apps/HelloZephyr - - name: Report to CodeCov - uses: ./.github/actions/report-code-coverage - with: - files: core/build/reports/jacoco/integrationTestCodeCoverageReport/integrationTestCodeCoverageReport.xml - if: ${{ github.repository == 'lf-lang/lingua-franca' }} + # - name: Report to CodeCov + # uses: ./.github/actions/report-code-coverage + # with: + # files: core/build/reports/jacoco/integrationTestCodeCoverageReport/integrationTestCodeCoverageReport.xml + # if: ${{ github.repository == 'lf-lang/lingua-franca' }} diff --git a/.github/workflows/rti-docker.yml b/.github/workflows/rti-docker.yml new file mode 100644 index 0000000000..e7132ebbbb --- /dev/null +++ b/.github/workflows/rti-docker.yml @@ -0,0 +1,42 @@ +name: Push RTI Image to Docker Hub + +on: + push: + branches: + - master + workflow_dispatch: + workflow_call: + +jobs: + build-and-push: + runs-on: ubuntu-latest + name: Build and push RTI to Docker Hub + steps: + - name: Check out lingua-franca repository + uses: actions/checkout@v3 + with: + repository: lf-lang/lingua-franca + submodules: recursive + fetch-depth: 0 + - name: Look up the current version and export as environment variable + run: | + export LF_VERSION=$(cat core/src/main/resources/org/lflang/StringsBundle.properties | sed -n 's/.*VERSION = \(.*\)/\1/p' | tr '[:upper:]' '[:lower:]') + echo "lf_version=$LF_VERSION" + echo "lf_version=$LF_VERSION" >> $GITHUB_ENV + - name: Build and push RTI to Docker Hub + uses: ./.github/actions/push-rti-docker + with: + tag: ${{ env.lf_version }} + latest: false + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + if: ${{ endsWith(env.lf_version, '-snapshot') }} + + - name: Update latest (released versions only) + uses: ./.github/actions/push-rti-docker + with: + tag: ${{ env.lf_version }} + latest: true + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + if: ${{ !endsWith(env.lf_version, '-snapshot') }} diff --git a/.gitignore b/.gitignore index 35ccb1f2f9..da8902f971 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,11 @@ local.properties # PyDev specific (Python IDE for Eclipse) *.pydevproject +## Python specific +*.pyc +**.egg-info +__pycache__ + # CDT-specific (C/C++ Development Tooling) .cproject diff --git a/CHANGELOG.md b/CHANGELOG.md index 78c75aa779..797482fbeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,103 @@ # Changelog +## [v0.8.2](https://github.com/lf-lang/lingua-franca/tree/v0.8.2) (2024-08-02) + +**Highlights** + +This patch release includes minor bugfixes and several enhancements of our Docker support. It also adds custom serialization for the Python target and support for the use of target code expressions to specify time values in C++. + +**🚀 New Features** + +- Docker compose override [\#2371](https://github.com/lf-lang/lingua-franca/pull/2371) (@Depetrol) + +**✨ Enhancements** + +- Ability to use of target code expressions for time values in C++ [\#2369](https://github.com/lf-lang/lingua-franca/pull/2369) (@cmnrd) +- Do not require libexecinfo in C++ docker images [\#2372](https://github.com/lf-lang/lingua-franca/pull/2372) (@cmnrd) +- Immediate start of federates with STP offset under decentralized coordination & fix target code STP_offset [\#2368](https://github.com/lf-lang/lingua-franca/pull/2368) (@Depetrol) +- Custom Serialization in Python Target [\#2375](https://github.com/lf-lang/lingua-franca/pull/2375) (@Depetrol) +- RTI Docker Hub Continuous Deployment with Multiplatform Support [\#2384](https://github.com/lf-lang/lingua-franca/pull/2384) (@Depetrol) + +**🔧 Fixes** + +- Immediate start of federates with STP offset under decentralized coordination & fix target code STP_offset [\#2368](https://github.com/lf-lang/lingua-franca/pull/2368) (@Depetrol) +- Fixed docker support for the Python target [\#2377](https://github.com/lf-lang/lingua-franca/pull/2377) (@cmnrd) +- Fix to get get all preambles in Python + updated tests [\#2381](https://github.com/lf-lang/lingua-franca/pull/2381) (@edwardalee) +- C++ raw strings allowed in target code blocks [\#2385](https://github.com/lf-lang/lingua-franca/pull/2385) (@lhstrh) + +**🚧 Maintenance and Refactoring** + +- Renaming `Latest Tag Completed` to `Latest Tag Confirmed` [\#2346](https://github.com/lf-lang/lingua-franca/pull/2346) (@byeonggiljun) + + +### Submodule [lf-lang/reactor-c](http://github.com/lf-lang/reactor-c) + +**🚀 New Features** + +- Support for Patmos platform [\#383](https://github.com/lf-lang/reactor-c/pull/383) (@EhsanKhodadad) + +**✨ Enhancements** + +- Immediate start of federates with STA offset under decentralized coordination [\#469](https://github.com/lf-lang/reactor-c/pull/469) (@Depetrol) +- Custom Serialization in Python Target [\#471](https://github.com/lf-lang/reactor-c/pull/471) (@Depetrol) +- Optimization of LTC signals [\#445](https://github.com/lf-lang/reactor-c/pull/445) (@byeonggiljun) +- RTI dockerfile support for multi-architecture builds [\#464](https://github.com/lf-lang/reactor-c/pull/464) (@elgeeko1) + + +### Submodule [lf-lang/reactor-cpp](http://github.com/lf-lang/reactor-cpp) + +**✨ Enhancements** + +- Portable backtrace mechanism [\#59](https://github.com/lf-lang/reactor-cpp/pull/59) (@cmnrd) + + +### Submodule [lf-lang/reactor-rs](http://github.com/lf-lang/reactor-rs) + +- No Changes + + + +## [v0.8.1](https://github.com/lf-lang/lingua-franca/tree/v0.8.1) (2024-07-14) + +**Highlights** + +This patch release includes several minor bugfixes and enhancements, improving Docker support for the C++ target and providing a more complete implementation of watchdogs. + +**✨ Enhancements** + +- API to look up source and package directory in Python [\#2331](https://github.com/lf-lang/lingua-franca/pull/2331) (@edwardalee) +- Define self variable so it can be used in instantiations [\#2353](https://github.com/lf-lang/lingua-franca/pull/2353) (@edwardalee) +- Fixed build script support in C++ docker generation [\#2357](https://github.com/lf-lang/lingua-franca/pull/2357) (@cmnrd) +- Diagram support for watchdogs [\#2356](https://github.com/lf-lang/lingua-franca/pull/2356) (@edwardalee) +- Fixed C++ docker generation for when cmake is not installed [\#2358](https://github.com/lf-lang/lingua-franca/pull/2358) (@cmnrd) +- Effects made accessible in watchdog handlers [\#2359](https://github.com/lf-lang/lingua-franca/pull/2359) (@lhstrh) + +**🚧 Maintenance and Refactoring** + +- Platform name changed from `Nrf52` to `nRF52` [\#2350](https://github.com/lf-lang/lingua-franca/pull/2350) (@edwardalee) + + +### Submodule [lf-lang/reactor-c](http://github.com/lf-lang/reactor-c) + +**🚀 New Features** + +- New Python functions `lf.package_directory()` and `lf.source_directory()` [\#455](https://github.com/lf-lang/reactor-c/pull/455) (@edwardalee) + +**🔧 Fixes** + +- Better error messages when HMAC authentication is attempted by federates when RTI does not support it [\#461](https://github.com/lf-lang/reactor-c/pull/461) (@Jakio815) + + +### Submodule [lf-lang/reactor-cpp](http://github.com/lf-lang/reactor-cpp) + +- No Changes + + +### Submodule [lf-lang/reactor-rs](http://github.com/lf-lang/reactor-rs) + +- Remove creusot sources and merge back vecmap into main runtime crate [\#47](https://github.com/lf-lang/reactor-rs/pull/47) (@oowekyala) + + ## [v0.8.0](https://github.com/lf-lang/lingua-franca/tree/v0.8.0) (2024-07-02) **Highlights** diff --git a/README.md b/README.md index 8c04a318a5..261636f015 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [Website](https://lf-lang.org/) | -[Documentation](https://www.lf-lang.org/docs/handbook/) | -[Download](https://www.lf-lang.org/download/) | +[Documentation](https://www.lf-lang.org/docs/) | +[Download](https://www.lf-lang.org/docs/installation) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) @@ -18,4 +18,4 @@ Lingua Franca (LF) is a polyglot coordination language for concurrent and possib See [lf-lang.org](https://lf-lang.org) for installation instructions and documentation. See also the [wiki](https://github.com/icyphy/lingua-franca/wiki) for further information on ongoing projects. -See our [Publications and Presentations](https://www.lf-lang.org/publications-and-presentations). +See our [Publications and Presentations](https://www.lf-lang.org/research/). diff --git a/core/src/integrationTest/java/org/lflang/tests/runtime/PythonTest.java b/core/src/integrationTest/java/org/lflang/tests/runtime/PythonTest.java index 5880a30f46..70a014fd85 100644 --- a/core/src/integrationTest/java/org/lflang/tests/runtime/PythonTest.java +++ b/core/src/integrationTest/java/org/lflang/tests/runtime/PythonTest.java @@ -70,7 +70,7 @@ protected boolean supportsSingleThreadedExecution() { @Override protected boolean supportsDockerOption() { - return false; // FIXME: https://issues.lf-lang.org/1564 + return true; } @Test diff --git a/core/src/main/java/org/lflang/AttributeUtils.java b/core/src/main/java/org/lflang/AttributeUtils.java index 147a4b6d0d..79447cb440 100644 --- a/core/src/main/java/org/lflang/AttributeUtils.java +++ b/core/src/main/java/org/lflang/AttributeUtils.java @@ -36,17 +36,7 @@ import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.resource.XtextResource; import org.lflang.ast.ASTUtils; -import org.lflang.lf.Action; -import org.lflang.lf.AttrParm; -import org.lflang.lf.Attribute; -import org.lflang.lf.Input; -import org.lflang.lf.Instantiation; -import org.lflang.lf.Output; -import org.lflang.lf.Parameter; -import org.lflang.lf.Reaction; -import org.lflang.lf.Reactor; -import org.lflang.lf.StateVar; -import org.lflang.lf.Timer; +import org.lflang.lf.*; import org.lflang.util.StringUtil; /** @@ -83,6 +73,8 @@ public static List getAttributes(EObject node) { return ((Output) node).getAttributes(); } else if (node instanceof Instantiation) { return ((Instantiation) node).getAttributes(); + } else if (node instanceof Watchdog) { + return ((Watchdog) node).getAttributes(); } throw new IllegalArgumentException("Not annotatable: " + node); } diff --git a/core/src/main/java/org/lflang/LinguaFranca.xtext b/core/src/main/java/org/lflang/LinguaFranca.xtext index 7f9e74d491..37c4495c8b 100644 --- a/core/src/main/java/org/lflang/LinguaFranca.xtext +++ b/core/src/main/java/org/lflang/LinguaFranca.xtext @@ -218,6 +218,7 @@ Deadline: 'deadline' '(' delay=Expression ')' code=Code; Watchdog: + (attributes+=Attribute)* 'watchdog' name=ID '(' timeout=Expression ')' ('->' effects+=VarRefOrModeTransition (',' effects+=VarRefOrModeTransition)*)? code=Code; @@ -424,6 +425,8 @@ terminal ML_COMMENT: ('/*' -> '*/') | ("'''" -> "'''"); terminal LT_ANNOT: "'" ID?; +terminal CPP_RAW_STR: 'R"' -> '"'; + terminal STRING: '"' ( '\\' . | !('\\' | '"' | '\t' | '\r' | '\n') )* '"' | '"""' -> '"""' ; @@ -511,7 +514,7 @@ Body: // the end of a target-code segment. Token: // Non-constant terminals - ID | INT | FLOAT_EXP_SUFFIX | LT_ANNOT | STRING | CHAR_LIT | ML_COMMENT | SL_COMMENT | WS | ANY_OTHER | + ID | INT | FLOAT_EXP_SUFFIX | LT_ANNOT | CPP_RAW_STR | STRING | CHAR_LIT | ML_COMMENT | SL_COMMENT | WS | ANY_OTHER | // Keywords 'target' | 'import' | 'main' | 'realtime' | 'reactor' | 'state' | 'time' | 'mutable' | 'input' | 'output' | 'timer' | 'action' | 'reaction' | diff --git a/core/src/main/java/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java b/core/src/main/java/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java index a5b4b8e314..9e4facbfc7 100644 --- a/core/src/main/java/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java +++ b/core/src/main/java/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java @@ -129,6 +129,7 @@ import org.lflang.generator.SendRange; import org.lflang.generator.TimerInstance; import org.lflang.generator.TriggerInstance; +import org.lflang.generator.WatchdogInstance; import org.lflang.lf.Connection; import org.lflang.lf.LfPackage; import org.lflang.lf.Model; @@ -1022,6 +1023,8 @@ private Collection transformReactorNetwork( Multimap actionDestinations = HashMultimap.create(); Multimap actionSources = HashMultimap.create(); Map timerNodes = new HashMap<>(); + Multimap watchdogDestinations = HashMultimap.create(); + Multimap watchdogSources = HashMultimap.create(); KNode startupNode = _kNodeExtensions.createNode(); TriggerInstance startup = null; KNode shutdownNode = _kNodeExtensions.createNode(); @@ -1145,6 +1148,8 @@ private Collection transformReactorNetwork( if (src != null) { connect(createDependencyEdge(trigger.getDefinition()), src, port); } + } else if (trigger instanceof WatchdogInstance) { + watchdogDestinations.put(((WatchdogInstance) trigger), port); } } @@ -1204,10 +1209,48 @@ private Collection transformReactorNetwork( if (dst != null) { connect(createDependencyEdge(effect), port, dst); } + } else if (effect instanceof WatchdogInstance) { + watchdogSources.put((WatchdogInstance) effect, port); } } } + // Connect watchdogs + Set watchdogs = new HashSet<>(); + watchdogs.addAll(watchdogSources.keySet()); + watchdogs.addAll(watchdogDestinations.keySet()); + + for (WatchdogInstance watchdog : watchdogs) { + KNode node = associateWith(_kNodeExtensions.createNode(), watchdog.getDefinition()); + NamedInstanceUtil.linkInstance(node, watchdog); + _utilityExtensions.setID(node, watchdog.uniqueID()); + nodes.add(node); + setLayoutOption(node, CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_SIDE); + Pair ports = _linguaFrancaShapeExtensions.addWatchdogFigureAndPorts(node); + setAnnotatedLayoutOptions(watchdog.getDefinition(), node); + if (watchdog.getTimeout() != null) { + _kLabelExtensions.addOutsideBottomCenteredNodeLabel( + node, String.format("timeout: %s", watchdog.getTimeout().toString()), 7); + } + Set> iterSet = + watchdog.effects != null ? watchdog.effects : new HashSet<>(); + for (TriggerInstance effect : iterSet) { + if (effect instanceof ActionInstance) { + actionSources.put((ActionInstance) effect, ports.getValue()); + } + } + + // connect source + for (KPort source : watchdogSources.get(watchdog)) { + connect(createDelayEdge(watchdog), source, ports.getKey()); + } + + // connect targets + for (KPort target : watchdogDestinations.get(watchdog)) { + connect(createDelayEdge(watchdog), ports.getValue(), target); + } + } + // Connect actions Set actions = new HashSet<>(); actions.addAll(actionSources.keySet()); diff --git a/core/src/main/java/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.java b/core/src/main/java/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.java index b7ef406ee5..299370a532 100644 --- a/core/src/main/java/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.java +++ b/core/src/main/java/org/lflang/diagram/synthesis/styles/LinguaFrancaShapeExtensions.java @@ -40,7 +40,6 @@ 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; @@ -672,6 +671,47 @@ public KEllipse addTimerFigure(KNode node, TimerInstance timer) { return figure; } + /** Creates the rectangular node with text and ports. */ + public Pair addWatchdogFigureAndPorts(KNode node) { + final float size = 18; + _kNodeExtensions.setMinimalNodeSize(node, size, size); + KRectangle figure = _kRenderingExtensions.addRectangle(node); + _kRenderingExtensions.setBackground(figure, Colors.WHITE); + _linguaFrancaStyleExtensions.boldLineSelectionStyle(figure); + + // Add text to the watchdog figure + KText textToAdd = _kContainerRenderingExtensions.addText(figure, "W"); + _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); + + // Add output port + KPort out = _kPortExtensions.createPort(); + node.getPorts().add(out); + DiagramSyntheses.setLayoutOption(out, CoreOptions.PORT_SIDE, PortSide.EAST); + return new Pair(in, out); + } + /** Creates the visual representation of a startup trigger. */ public KEllipse addStartupFigure(KNode node) { _kNodeExtensions.setMinimalNodeSize(node, 18, 18); @@ -839,53 +879,6 @@ public KText addTextButton(KContainerRendering container, String text) { 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; diff --git a/core/src/main/java/org/lflang/federated/extensions/CExtension.java b/core/src/main/java/org/lflang/federated/extensions/CExtension.java index 67ec873daa..054ff0ede0 100644 --- a/core/src/main/java/org/lflang/federated/extensions/CExtension.java +++ b/core/src/main/java/org/lflang/federated/extensions/CExtension.java @@ -55,6 +55,7 @@ import org.lflang.lf.Port; import org.lflang.lf.Reactor; import org.lflang.lf.VarRef; +import org.lflang.lf.impl.CodeExprImpl; import org.lflang.target.Target; import org.lflang.target.property.ClockSyncOptionsProperty; import org.lflang.target.property.CoordinationOptionsProperty; @@ -644,7 +645,7 @@ private String generateExecutablePreamble( code.pr(generateCodeForPhysicalActions(federate, messageReporter)); - code.pr(generateCodeToInitializeFederate(federate, rtiConfig)); + code.pr(generateCodeToInitializeFederate(federate, rtiConfig, messageReporter)); return """ void _lf_executable_preamble(environment_t* env) { %s @@ -673,7 +674,8 @@ void staa_initialization() { * @param rtiConfig Information about the RTI's deployment. * @return The generated code */ - private String generateCodeToInitializeFederate(FederateInstance federate, RtiConfig rtiConfig) { + private String generateCodeToInitializeFederate( + FederateInstance federate, RtiConfig rtiConfig, MessageReporter messageReporter) { CodeBuilder code = new CodeBuilder(); code.pr("// ***** Start initializing the federated execution. */"); code.pr( @@ -701,7 +703,12 @@ private String generateCodeToInitializeFederate(FederateInstance federate, RtiCo if (stpParam.isPresent()) { var globalSTP = ASTUtils.initialValue(stpParam.get(), List.of(federate.instantiation)); var globalSTPTV = ASTUtils.getLiteralTimeValue(globalSTP); - code.pr("lf_set_stp_offset(" + CTypes.getInstance().getTargetTimeExpr(globalSTPTV) + ");"); + if (globalSTPTV != null) + code.pr( + "lf_set_stp_offset(" + CTypes.getInstance().getTargetTimeExpr(globalSTPTV) + ");"); + else if (globalSTP instanceof CodeExprImpl) + code.pr("lf_set_stp_offset(" + ((CodeExprImpl) globalSTP).getCode().getBody() + ");"); + else messageReporter.at(stpParam.get().eContainer()).error("Invalid STP offset"); } } diff --git a/core/src/main/java/org/lflang/federated/extensions/PythonExtension.java b/core/src/main/java/org/lflang/federated/extensions/PythonExtension.java index b387d4966c..13c6a46fa2 100644 --- a/core/src/main/java/org/lflang/federated/extensions/PythonExtension.java +++ b/core/src/main/java/org/lflang/federated/extensions/PythonExtension.java @@ -34,6 +34,7 @@ import org.lflang.federated.generator.FederateInstance; import org.lflang.federated.generator.FederationFileConfig; import org.lflang.federated.launcher.RtiConfig; +import org.lflang.federated.serialization.FedCustomPythonSerialization; import org.lflang.federated.serialization.FedNativePythonSerialization; import org.lflang.federated.serialization.FedSerialization; import org.lflang.federated.serialization.SupportedSerializers; @@ -65,6 +66,12 @@ protected String generateSerializationIncludes( FedNativePythonSerialization pickler = new FedNativePythonSerialization(); code.pr(pickler.generatePreambleForSupport().toString()); } + case CUSTOM: + { + FedCustomPythonSerialization serializer = + new FedCustomPythonSerialization(serialization.getSerializer()); + code.pr(serializer.generatePreambleForSupport().toString()); + } case PROTO: { // Nothing needs to be done @@ -144,6 +151,21 @@ protected void deserialize( result.pr("lf_set_destructor(" + receiveRef + ", python_count_decrement);\n"); result.pr("lf_set_token(" + receiveRef + ", token);\n"); } + case CUSTOM -> { + value = action.getName(); + FedCustomPythonSerialization serializer = + new FedCustomPythonSerialization(connection.getSerializer().getSerializer()); + result.pr(serializer.generateNetworkDeserializerCode(value, null)); + // Use token to set ports and destructor + result.pr( + "lf_token_t* token = lf_new_token((void*)" + + receiveRef + + ", " + + FedSerialization.deserializedVarName + + ", 1);\n"); + result.pr("lf_set_destructor(" + receiveRef + ", python_count_decrement);\n"); + result.pr("lf_set_token(" + receiveRef + ", token);\n"); + } case PROTO -> throw new UnsupportedOperationException("Protobuf serialization is not supported yet."); case ROS2 -> @@ -174,6 +196,18 @@ protected void serializeAndSend( // Decrease the reference count for serialized_pyobject result.pr("Py_XDECREF(serialized_pyobject);\n"); } + case CUSTOM -> { + var variableToSerialize = sendRef + "->value"; + FedCustomPythonSerialization serializer = + new FedCustomPythonSerialization(connection.getSerializer().getSerializer()); + lengthExpression = serializer.serializedBufferLength(); + pointerExpression = serializer.serializedBufferVar(); + result.pr(serializer.generateNetworkSerializerCode(variableToSerialize, null)); + result.pr("size_t _lf_message_length = " + lengthExpression + ";"); + result.pr(sendingFunction + "(" + commonArgs + ", " + pointerExpression + ");\n"); + // Decrease the reference count for serialized_pyobject + result.pr("Py_XDECREF(serialized_pyobject);\n"); + } case PROTO -> throw new UnsupportedOperationException("Protobuf serialization is not supported yet."); case ROS2 -> diff --git a/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java b/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java index 5315e8562a..5f74e5c9ec 100644 --- a/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java +++ b/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java @@ -67,9 +67,11 @@ import org.lflang.lf.ParameterReference; import org.lflang.lf.Reaction; import org.lflang.lf.Reactor; +import org.lflang.lf.StateVar; import org.lflang.lf.Type; import org.lflang.lf.VarRef; import org.lflang.lf.Variable; +import org.lflang.target.Target; import org.lflang.target.property.type.CoordinationModeType.CoordinationMode; /** @@ -258,6 +260,12 @@ private static void addNetworkReceiverReactor( receiver.getReactions().add(networkReceiverReaction); receiver.getOutputs().add(out); + if (connection.dstFederate.targetConfig.target == Target.Python) { + StateVar serializer = factory.createStateVar(); + serializer.setName("custom_serializer"); + receiver.getStateVars().add(serializer); + } + addLevelAttribute( networkInstance, connection.getDestinationPortInstance(), @@ -682,6 +690,12 @@ private static Reactor getNetworkSenderReactor( in.setWidthSpec(widthSpec); inRef.setVariable(in); + if (connection.getSrcFederate().targetConfig.target == Target.Python) { + StateVar serializer = factory.createStateVar(); + serializer.setName("custom_serializer"); + sender.getStateVars().add(serializer); + } + destRef.setContainer(connection.getDestinationPortInstance().getParent().getDefinition()); destRef.setVariable(connection.getDestinationPortInstance().getDefinition()); diff --git a/core/src/main/java/org/lflang/federated/generator/FedUtils.java b/core/src/main/java/org/lflang/federated/generator/FedUtils.java index 2970be7c2a..fe755ce99d 100644 --- a/core/src/main/java/org/lflang/federated/generator/FedUtils.java +++ b/core/src/main/java/org/lflang/federated/generator/FedUtils.java @@ -14,7 +14,18 @@ public static SupportedSerializers getSerializer( // Get the serializer SupportedSerializers serializer = SupportedSerializers.NATIVE; if (connection.getSerializer() != null) { - serializer = SupportedSerializers.valueOf(connection.getSerializer().getType().toUpperCase()); + boolean isCustomSerializer = true; + for (SupportedSerializers method : SupportedSerializers.values()) { + if (method.name().equalsIgnoreCase(connection.getSerializer().getType())) { + serializer = + SupportedSerializers.valueOf(connection.getSerializer().getType().toUpperCase()); + isCustomSerializer = false; + break; + } + } + if (isCustomSerializer) { + serializer = SupportedSerializers.fromCustomString(connection.getSerializer().getType()); + } } // Add it to the list of enabled serializers for the source and destination federates srcFederate.enabledSerializers.add(serializer); diff --git a/core/src/main/java/org/lflang/federated/serialization/FedCustomPythonSerialization.java b/core/src/main/java/org/lflang/federated/serialization/FedCustomPythonSerialization.java new file mode 100644 index 0000000000..42a9de68eb --- /dev/null +++ b/core/src/main/java/org/lflang/federated/serialization/FedCustomPythonSerialization.java @@ -0,0 +1,118 @@ +/************* + * Copyright (c) 2024, 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.serialization; + +import org.lflang.generator.GeneratorBase; +import org.lflang.target.Target; + +/** + * Enables support for custom serialization. + * + * @author Shulu Li + */ +public class FedCustomPythonSerialization implements FedSerialization { + + String customSerializerPackage; + + public FedCustomPythonSerialization(String customSerializerPackage) { + this.customSerializerPackage = customSerializerPackage; + } + + @Override + public boolean isCompatible(GeneratorBase generator) { + if (generator.getTarget() != Target.Python) { + throw new UnsupportedOperationException( + "The FedCustomPythonSerialization class only supports the Python target."); + } + return true; + } + + @Override + public String serializedBufferLength() { + return serializedVarName + ".len"; + } + + @Override + public String serializedBufferVar() { + return serializedVarName + ".buf"; + } + + private String initializeCustomSerializer() { + return "if (self->custom_serializer == NULL) \n" + + "self->custom_serializer = load_serializer(\"%s\");\n".formatted(customSerializerPackage); + } + + @Override + public StringBuilder generateNetworkSerializerCode(String varName, String originalType) { + StringBuilder serializerCode = new StringBuilder(); + // Initialize self->custom_serializer if null + serializerCode.append(this.initializeCustomSerializer()); + // Serialize PyObject to bytes using custom serializer + serializerCode.append( + """ + PyObject *serialized_pyobject = custom_serialize(msg[0]->value, self->custom_serializer); + Py_buffer %s; + int returnValue = PyBytes_AsStringAndSize(serialized_pyobject, (char**)&%s.buf, &%s.len); + if (returnValue == -1) { + if (PyErr_Occurred()) PyErr_Print(); + lf_print_error_and_exit("Could not serialize %s."); + } + """ + .formatted(serializedVarName, serializedVarName, serializedVarName, serializedVarName)); + return serializerCode; + } + + @Override + public StringBuilder generateNetworkDeserializerCode(String varName, String targetType) { + StringBuilder deserializerCode = new StringBuilder(); + // Initialize self->custom_serializer if null + deserializerCode.append(this.initializeCustomSerializer()); + // Deserialize network message to a PyObject using custom serializer + deserializerCode.append( + """ +PyObject *message_byte_array = PyBytes_FromStringAndSize((char*) %s->token->value, %s->token->length); +PyObject *%s = custom_deserialize(message_byte_array, self->custom_serializer); +if ( %s == NULL ) { + if (PyErr_Occurred()) PyErr_Print(); + lf_print_error_and_exit("Could not serialize %s."); +} +""" + .formatted( + varName, varName, deserializedVarName, deserializedVarName, deserializedVarName)); + return deserializerCode; + } + + @Override + public StringBuilder generatePreambleForSupport() { + return new StringBuilder(); + } + + @Override + public StringBuilder generateCompilerExtensionForSupport() { + return new StringBuilder(); + } +} diff --git a/core/src/main/java/org/lflang/federated/serialization/SupportedSerializers.java b/core/src/main/java/org/lflang/federated/serialization/SupportedSerializers.java index 4767f3f4d7..094a6b9c69 100644 --- a/core/src/main/java/org/lflang/federated/serialization/SupportedSerializers.java +++ b/core/src/main/java/org/lflang/federated/serialization/SupportedSerializers.java @@ -8,7 +8,8 @@ public enum SupportedSerializers { NATIVE("native"), // Dangerous: just copies the memory layout of the sender ROS2("ros2"), - PROTO("proto"); + PROTO("proto"), + CUSTOM(""); private String serializer; @@ -23,4 +24,9 @@ public String getSerializer() { public void setSerializer(String serializer) { this.serializer = serializer; } + + public static SupportedSerializers fromCustomString(String serializer) { + CUSTOM.setSerializer(serializer); + return CUSTOM; + } } diff --git a/core/src/main/java/org/lflang/generator/ReactionInstance.java b/core/src/main/java/org/lflang/generator/ReactionInstance.java index 36b0aae54a..029eb13f0a 100644 --- a/core/src/main/java/org/lflang/generator/ReactionInstance.java +++ b/core/src/main/java/org/lflang/generator/ReactionInstance.java @@ -40,6 +40,7 @@ import org.lflang.lf.TriggerRef; import org.lflang.lf.VarRef; import org.lflang.lf.Variable; +import org.lflang.lf.Watchdog; /** * Representation of a compile-time instance of a reaction. Like {@link ReactorInstance}, if one or @@ -108,6 +109,10 @@ public ReactionInstance(Reaction definition, ReactorInstance parent, int index) this.triggers.add(timerInstance); timerInstance.dependentReactions.add(this); this.sources.add(timerInstance); + } else if (variable instanceof Watchdog) { + var watchdogInstance = + parent.lookupWatchdogInstance((Watchdog) ((VarRef) trigger).getVariable()); + this.triggers.add(watchdogInstance); } } else if (trigger instanceof BuiltinTriggerRef) { var builtinTriggerInstance = parent.getOrCreateBuiltinTrigger((BuiltinTriggerRef) trigger); @@ -159,6 +164,9 @@ public ReactionInstance(Reaction definition, ReactorInstance parent, int index) var actionInstance = parent.lookupActionInstance((Action) variable); this.effects.add(actionInstance); actionInstance.dependsOnReactions.add(this); + } else if (variable instanceof Watchdog) { + var watchdogInstance = parent.lookupWatchdogInstance((Watchdog) variable); + this.effects.add(watchdogInstance); } else { // Effect is either a mode or an unresolved reference. // Do nothing, transitions will be set up by the ModeInstance. diff --git a/core/src/main/java/org/lflang/generator/ReactorInstance.java b/core/src/main/java/org/lflang/generator/ReactorInstance.java index 78f1312c97..39f7cd03da 100644 --- a/core/src/main/java/org/lflang/generator/ReactorInstance.java +++ b/core/src/main/java/org/lflang/generator/ReactorInstance.java @@ -693,6 +693,23 @@ public ModeInstance lookupModeInstance(Mode mode) { return null; } + /** + * Return the watchdog instance within this reactor instance corresponding to the specified + * watchdog reference. + * + * @param watchdog The watchdog as an AST node. + * @return The corresponding watchdog instance or null if the watchdog does not belong to this + * reactor. + */ + public WatchdogInstance lookupWatchdogInstance(Watchdog watchdog) { + for (WatchdogInstance watchdogInstance : watchdogs) { + if (watchdogInstance.getDefinition() == watchdog) { + return watchdogInstance; + } + } + return null; + } + /** Return a descriptive string. */ @Override public String toString() { @@ -877,6 +894,8 @@ public ReactorInstance( this.actions.add(new ActionInstance(actionDecl, this)); } + createWatchdogInstances(); + establishPortConnections(); // Create the reaction instances in this reactor instance. diff --git a/core/src/main/java/org/lflang/generator/WatchdogInstance.java b/core/src/main/java/org/lflang/generator/WatchdogInstance.java index ac51f389cb..73abe02b75 100644 --- a/core/src/main/java/org/lflang/generator/WatchdogInstance.java +++ b/core/src/main/java/org/lflang/generator/WatchdogInstance.java @@ -8,19 +8,25 @@ */ package org.lflang.generator; +import java.util.LinkedHashSet; +import java.util.Set; import org.lflang.TimeValue; +import org.lflang.lf.Action; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; import org.lflang.lf.Watchdog; /** * Instance of a watchdog. Upon creation the actual delay is converted into a proper time value. If * a parameter is referenced, it is looked up in the given (grand)parent reactor instance. * - * @author{Benjamin Asch } + * @author Benjamin Asch */ -public class WatchdogInstance { +public class WatchdogInstance extends TriggerInstance { /** Create a new watchdog instance associated with the given reactor instance. */ public WatchdogInstance(Watchdog definition, ReactorInstance reactor) { + super(definition, reactor); if (definition.getTimeout() != null) { // Get the timeout value given in the watchdog declaration. this.timeout = reactor.getTimeValue(definition.getTimeout()); @@ -29,9 +35,18 @@ public WatchdogInstance(Watchdog definition, ReactorInstance reactor) { this.timeout = TimeValue.ZERO; } - this.name = definition.getName().toString(); + this.name = definition.getName(); this.definition = definition; this.reactor = reactor; + for (VarRef effect : definition.getEffects()) { + Variable variable = effect.getVariable(); + if (variable instanceof Action) { + // Effect is an Action. + var actionInstance = reactor.lookupActionInstance((Action) variable); + if (actionInstance != null) this.effects.add(actionInstance); + } + // Otherwise, do nothing (effect is either a mode or an unresolved reference). + } } ////////////////////////////////////////////////////// @@ -46,7 +61,7 @@ public Watchdog getDefinition() { } public TimeValue getTimeout() { - return (TimeValue) this.timeout; + return this.timeout; } public ReactorInstance getReactor() { @@ -58,6 +73,12 @@ public String toString() { return "WatchdogInstance " + name + "(" + timeout.toString() + ")"; } + ////////////////////////////////////////////////////// + //// Public fields. + + /** The ports or actions that this reaction may write to. */ + public Set> effects = new LinkedHashSet<>(); + ////////////////////////////////////////////////////// //// Private fields. diff --git a/core/src/main/java/org/lflang/generator/c/CCompiler.java b/core/src/main/java/org/lflang/generator/c/CCompiler.java index 9f921254ce..12de4474dc 100644 --- a/core/src/main/java/org/lflang/generator/c/CCompiler.java +++ b/core/src/main/java/org/lflang/generator/c/CCompiler.java @@ -217,13 +217,14 @@ public LFCommand compileCmakeCommand() { private static List cmakeOptions(TargetConfig targetConfig, FileConfig fileConfig) { List arguments = new ArrayList<>(); String separator = File.separator; - String maybeQuote = ""; // Windows seems to require extra level of quoting. - String srcPath = fileConfig.srcPath.toString(); // Windows requires escaping the backslashes. + String quote = "\""; + String srcPath = fileConfig.srcPath.toString(); String rootPath = fileConfig.srcPkgPath.toString(); String srcGenPath = fileConfig.getSrcGenPath().toString(); if (separator.equals("\\")) { + // Windows requires escaping the backslashes. separator = "\\\\\\\\"; - maybeQuote = "\\\""; + quote = "\\\""; srcPath = srcPath.replaceAll("\\\\", "\\\\\\\\"); rootPath = rootPath.replaceAll("\\\\", "\\\\\\\\"); srcGenPath = srcGenPath.replaceAll("\\\\", "\\\\\\\\"); @@ -237,15 +238,15 @@ private static List cmakeOptions(TargetConfig targetConfig, FileConfig f "-DCMAKE_INSTALL_PREFIX=" + FileUtil.toUnixString(fileConfig.getOutPath()), "-DCMAKE_INSTALL_BINDIR=" + FileUtil.toUnixString(fileConfig.getOutPath().relativize(fileConfig.binPath)), - "-DLF_FILE_SEPARATOR=\"" + maybeQuote + separator + maybeQuote + "\"")); + "-DLF_FILE_SEPARATOR='" + quote + separator + quote + "'")); // Add #define for source file directory. // Do not do this for federated programs because for those, the definition is put // into the cmake file (and fileConfig.srcPath is the wrong directory anyway). if (!fileConfig.srcPath.toString().contains("fed-gen")) { // Do not convert to Unix path - arguments.add("-DLF_SOURCE_DIRECTORY=\"" + maybeQuote + srcPath + maybeQuote + "\""); - arguments.add("-DLF_PACKAGE_DIRECTORY=\"" + maybeQuote + rootPath + maybeQuote + "\""); - arguments.add("-DLF_SOURCE_GEN_DIRECTORY=\"" + maybeQuote + srcGenPath + maybeQuote + "\""); + arguments.add("-DLF_SOURCE_DIRECTORY='" + quote + srcPath + quote + "'"); + arguments.add("-DLF_PACKAGE_DIRECTORY='" + quote + rootPath + quote + "'"); + arguments.add("-DLF_SOURCE_GEN_DIRECTORY='" + quote + srcGenPath + quote + "'"); } arguments.add(FileUtil.toUnixString(fileConfig.getSrcGenPath())); diff --git a/core/src/main/java/org/lflang/generator/c/CGenerator.java b/core/src/main/java/org/lflang/generator/c/CGenerator.java index be0d544352..3e54b8d5ba 100644 --- a/core/src/main/java/org/lflang/generator/c/CGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CGenerator.java @@ -524,9 +524,11 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { context.getMode()); context.finish(GeneratorResult.Status.COMPILED, null); } else if (dockerBuild.enabled()) { - boolean success = buildUsingDocker(); - if (!success) { - context.unsuccessfulFinish(); + if (targetConfig.target != Target.Python) { + boolean success = buildUsingDocker(); + if (!success) { + context.unsuccessfulFinish(); + } } } else { var cleanCode = code.removeLines("#line"); @@ -633,7 +635,7 @@ private void generateCodeFor(String lfModuleName) throws IOException { "\n", "void logical_tag_complete(tag_t tag_to_send) {", CExtensionUtils.surroundWithIfElseFederatedCentralized( - " lf_latest_tag_complete(tag_to_send);", " (void) tag_to_send;"), + " lf_latest_tag_confirmed(tag_to_send);", " (void) tag_to_send;"), "}")); // Generate an empty termination function for non-federated diff --git a/core/src/main/java/org/lflang/generator/c/CWatchdogGenerator.java b/core/src/main/java/org/lflang/generator/c/CWatchdogGenerator.java index 9ce5dec07f..e83ea122af 100644 --- a/core/src/main/java/org/lflang/generator/c/CWatchdogGenerator.java +++ b/core/src/main/java/org/lflang/generator/c/CWatchdogGenerator.java @@ -13,6 +13,7 @@ import org.lflang.ast.ASTUtils; import org.lflang.generator.CodeBuilder; import org.lflang.generator.ReactorInstance; +import org.lflang.lf.Action; import org.lflang.lf.Mode; import org.lflang.lf.ModeTransition; import org.lflang.lf.Reactor; @@ -22,9 +23,10 @@ import org.lflang.util.StringUtil; /** - * @brief Generate C code for watchdogs. This class contains a collection of static methods - * supporting code generation in C for watchdogs. These methods are protected because they are - * intended to be used only within the same package. + * Generate C code for watchdogs. This class contains a collection of static methods supporting code + * generation in C for watchdogs. These methods are protected because they are intended to be used + * only within the same package. + * * @author Benjamin Asch * @author Edward A. Lee */ @@ -38,8 +40,7 @@ public class CWatchdogGenerator { */ public static boolean hasWatchdogs(Reactor reactor) { List watchdogs = ASTUtils.allWatchdogs(reactor); - if (watchdogs != null && !watchdogs.isEmpty()) return true; - return false; + return !watchdogs.isEmpty(); } ///////////////////////////////////////////////////////////////// @@ -159,15 +160,6 @@ protected static void generateWatchdogStruct( } } - /** - * Generate a global table of watchdog structs. - * - * @param count The number of watchdogs found. - * @return The code that defines the table or a comment if count is 0. - */ - ///////////////////////////////////////////////////////////////// - // Private methods - /** * Generate necessary initialization code inside the body of a watchdog handler. * @@ -185,17 +177,8 @@ private static String generateInitializationForWatchdog( // Define the "self" struct. String structType = CUtil.selfType(tpr); - // 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", - structType - + "* self = (" - + structType - + "*)instance_args; SUPPRESS_UNUSED_WARNING(self);")); - } + code.pr( + structType + "* self = (" + structType + "*)instance_args; SUPPRESS_UNUSED_WARNING(self);"); // Declare mode if in effects field of watchdog if (watchdog.getEffects() != null) { @@ -227,6 +210,8 @@ private static String generateInitializationForWatchdog( + name + " not a valid mode of this reactor."); } + } else if (variable instanceof Action) { + watchdogInitialization.pr(generateActionVariablesInHandler((Action) variable, tpr)); } } } @@ -243,6 +228,21 @@ private static String generateInitializationForWatchdog( return code.toString(); } + /** + * Generate action variables for the watchdog handler. + * + * @param action The action. + */ + private static String generateActionVariablesInHandler( + Action action, TypeParameterizedReactor tpr) { + String structType = CGenerator.variableStructType(action, tpr, false); + CodeBuilder builder = new CodeBuilder(); + builder.pr( + "// Expose the action struct as a local variable whose name matches the action name."); + builder.pr(structType + "* " + action.getName() + " = &self->_lf_" + action.getName() + ";"); + return builder.toString(); + } + /** * Do heavy lifting to generate the watchdog handler function * @@ -268,6 +268,8 @@ private static String generateFunction( function.pr(header + " {"); function.indent(); function.pr(init); + function.pr("{"); // Limit scope. + function.indent(); function.pr("environment_t * __env = self->base.environment;"); function.pr("LF_MUTEX_LOCK(&__env->mutex);"); function.pr("tag_t tag = {.time =" + watchdog.getName() + "->expiration , .microstep=0};"); @@ -280,6 +282,8 @@ private static String generateFunction( function.pr("_lf_schedule_at_tag(__env, " + watchdog.getName() + "->trigger, tag, NULL);"); function.pr("lf_cond_broadcast(&__env->event_q_changed);"); function.pr("LF_MUTEX_UNLOCK(&__env->mutex);"); + function.unindent(); + function.pr("}"); function.prSourceLineNumber(watchdog.getCode(), suppressLineDirectives); function.pr(ASTUtils.toText(watchdog.getCode())); function.prEndSourceLineNumber(suppressLineDirectives); diff --git a/core/src/main/java/org/lflang/generator/docker/DockerComposeGenerator.java b/core/src/main/java/org/lflang/generator/docker/DockerComposeGenerator.java index 8d65369f29..c65ee8ab3a 100644 --- a/core/src/main/java/org/lflang/generator/docker/DockerComposeGenerator.java +++ b/core/src/main/java/org/lflang/generator/docker/DockerComposeGenerator.java @@ -3,6 +3,7 @@ import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Objects; @@ -122,6 +123,19 @@ public void writeDockerComposeFile(List services, String networkName String.join( "\n", this.generateDockerServices(services), this.generateDockerNetwork(networkName)); FileUtil.writeToFile(contents, dockerComposeDir.resolve("docker-compose.yml")); + var dockerConfigFile = + context.getTargetConfig().get(DockerProperty.INSTANCE).dockerConfigFile(); + if (!dockerConfigFile.isEmpty()) { + var found = FileUtil.findInPackage(Path.of(dockerConfigFile), context.getFileConfig()); + if (found != null) { + var destination = dockerComposeDir.resolve("docker-compose-override.yml"); + FileUtil.copyFile(found, destination); + this.context + .getErrorReporter() + .nowhere() + .info("Docker compose override file copied to " + destination); + } + } var envFile = context.getTargetConfig().get(DockerProperty.INSTANCE).envFile(); if (!envFile.isEmpty()) { var found = FileUtil.findInPackage(Path.of(envFile), context.getFileConfig()); @@ -170,9 +184,14 @@ public void createLauncher() { set -euo pipefail cd $(dirname "$0") cd "%s/%s" - docker compose up --abort-on-container-failure + docker compose -f docker-compose.yml %s up --abort-on-container-failure """ - .formatted(relPath, packageRoot.relativize(srcGenPath)); + .formatted( + relPath, + packageRoot.relativize(srcGenPath), + Files.exists(fileConfig.getSrcGenPath().resolve("docker-compose-override.yml")) + ? "-f docker-compose-override.yml" + : ""); var messageReporter = context.getErrorReporter(); try { var writer = new BufferedWriter(new FileWriter(file)); diff --git a/core/src/main/java/org/lflang/generator/python/PythonGenerator.java b/core/src/main/java/org/lflang/generator/python/PythonGenerator.java index 77713acb36..a540ec8017 100644 --- a/core/src/main/java/org/lflang/generator/python/PythonGenerator.java +++ b/core/src/main/java/org/lflang/generator/python/PythonGenerator.java @@ -61,6 +61,7 @@ import org.lflang.lf.Reactor; import org.lflang.target.Target; import org.lflang.target.TargetConfig; +import org.lflang.target.property.DockerProperty; import org.lflang.target.property.ProtobufsProperty; import org.lflang.target.property.PythonVersionProperty; import org.lflang.util.FileUtil; @@ -396,6 +397,14 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { } } + if (targetConfig.get(DockerProperty.INSTANCE).enabled()) { + boolean success = buildUsingDocker(); + if (!success) { + context.unsuccessfulFinish(); + return; + } + } + if (messageReporter.getErrorsOccurred()) { context.unsuccessfulFinish(); } else { diff --git a/core/src/main/java/org/lflang/generator/python/PythonReactorGenerator.java b/core/src/main/java/org/lflang/generator/python/PythonReactorGenerator.java index 8f2d3fd8ad..99dbae7349 100644 --- a/core/src/main/java/org/lflang/generator/python/PythonReactorGenerator.java +++ b/core/src/main/java/org/lflang/generator/python/PythonReactorGenerator.java @@ -46,7 +46,8 @@ public static String generatePythonClass( pythonClasses.pr(generatePythonClassHeader(className)); // Generate preamble code pythonClasses.indent(); - pythonClasses.pr(PythonPreambleGenerator.generatePythonPreambles(reactor.getPreambles())); + pythonClasses.pr( + PythonPreambleGenerator.generatePythonPreambles(ASTUtils.allPreambles(reactor))); // Handle runtime initializations pythonClasses.pr(generatePythonConstructor(decl, types)); pythonClasses.pr(PythonParameterGenerator.generatePythonGetters(decl)); @@ -133,12 +134,25 @@ public static String generatePythonClassInstantiations( // setting parameter values. code.pr("bank_index = " + PyUtil.bankIndexName(instance)); code.pr(generatePythonClassInstantiation(instance, className)); - } - for (ReactorInstance child : instance.children) { - code.pr(generatePythonClassInstantiations(child, main)); + if (!instance.children.isEmpty()) { + // Define self so that instantiations of contained reactors can refer to parameters. + // First, save the previous definition of self to restore after instantiating children. + // But do not do this for the main reactor. + if (instance.getParent() != null) { + code.pr("previous_self = self"); + } + code.pr("self = " + PyUtil.reactorRef(instance)); + + for (ReactorInstance child : instance.children) { + code.pr(generatePythonClassInstantiations(child, main)); + } + if (instance.getParent() != null) { + code.pr("self = previous_self"); + } + } + code.unindent(); } - code.unindent(); return code.toString(); } diff --git a/core/src/main/java/org/lflang/scoping/LFScopeProviderImpl.java b/core/src/main/java/org/lflang/scoping/LFScopeProviderImpl.java index c5cbea4e7e..3455c1d822 100644 --- a/core/src/main/java/org/lflang/scoping/LFScopeProviderImpl.java +++ b/core/src/main/java/org/lflang/scoping/LFScopeProviderImpl.java @@ -49,6 +49,7 @@ import org.lflang.lf.Reactor; import org.lflang.lf.ReactorDecl; import org.lflang.lf.VarRef; +import org.lflang.lf.Watchdog; /** * This class enforces custom rules. In particular, it resolves references to parameters, ports, @@ -273,6 +274,11 @@ private RefType getRefType(VarRef variable) { } else if (conn.getRightPorts().contains(variable)) { return RefType.CRIGHT; } + } else if (variable.eContainer() instanceof Watchdog) { + var watchdog = (Watchdog) variable.eContainer(); + if (watchdog.getEffects().contains(variable)) { + return RefType.EFFECT; + } } return RefType.NULL; } diff --git a/core/src/main/java/org/lflang/target/property/DockerProperty.java b/core/src/main/java/org/lflang/target/property/DockerProperty.java index 387d398aba..fd5383653a 100644 --- a/core/src/main/java/org/lflang/target/property/DockerProperty.java +++ b/core/src/main/java/org/lflang/target/property/DockerProperty.java @@ -1,5 +1,6 @@ package org.lflang.target.property; +import org.lflang.LocalStrings; import org.lflang.MessageReporter; import org.lflang.ast.ASTUtils; import org.lflang.lf.Element; @@ -43,6 +44,7 @@ public DockerOptions fromAst(Element node, MessageReporter reporter) { var postBuildScript = ""; var runScript = ""; var envFile = ""; + var dockerConfigFile = ""; if (node.getLiteral() != null) { if (ASTUtils.toBoolean(node)) { @@ -63,6 +65,8 @@ public DockerOptions fromAst(Element node, MessageReporter reporter) { postBuildScript = ASTUtils.elementToSingleString(entry.getValue()); case RTI_IMAGE -> rti = ASTUtils.elementToSingleString(entry.getValue()); case ENV_FILE -> envFile = ASTUtils.elementToSingleString(entry.getValue()); + case DOCKER_CONFIG_FILE -> + dockerConfigFile = ASTUtils.elementToSingleString(entry.getValue()); } } } @@ -76,7 +80,8 @@ public DockerOptions fromAst(Element node, MessageReporter reporter) { preBuildScript, postBuildScript, runScript, - envFile); + envFile, + dockerConfigFile); } @Override @@ -113,6 +118,7 @@ public Element toAstElement(DockerOptions value) { case POST_BUILD_SCRIPT -> pair.setValue(ASTUtils.toElement(value.postBuildScript)); case RTI_IMAGE -> pair.setValue(ASTUtils.toElement(value.rti)); case ENV_FILE -> pair.setValue(ASTUtils.toElement(value.envFile)); + case DOCKER_CONFIG_FILE -> pair.setValue(ASTUtils.toElement(value.dockerConfigFile)); } kvp.getPairs().add(pair); } @@ -140,10 +146,12 @@ public record DockerOptions( String preBuildScript, String postBuildScript, String preRunScript, - String envFile) { + String envFile, + String dockerConfigFile) { /** Default location to pull the rti from. */ - public static final String DOCKERHUB_RTI_IMAGE = "lflang/rti:rti"; + public static final String DOCKERHUB_RTI_IMAGE = + "lflang/rti:" + LocalStrings.VERSION.toLowerCase(); public static final String DEFAULT_SHELL = "/bin/sh"; @@ -151,7 +159,7 @@ public record DockerOptions( public static final String LOCAL_RTI_IMAGE = "rti:local"; public DockerOptions(boolean enabled) { - this(enabled, false, "", "", DOCKERHUB_RTI_IMAGE, DEFAULT_SHELL, "", "", "", ""); + this(enabled, false, "", "", DOCKERHUB_RTI_IMAGE, DEFAULT_SHELL, "", "", "", "", ""); } } @@ -168,7 +176,8 @@ public enum DockerOption implements DictionaryElement { RTI_IMAGE("rti-image", PrimitiveType.STRING), PRE_BUILD_SCRIPT("pre-build-script", PrimitiveType.STRING), PRE_RUN_SCRIPT("pre-run-script", PrimitiveType.STRING), - POST_BUILD_SCRIPT("post-build-script", PrimitiveType.STRING); + POST_BUILD_SCRIPT("post-build-script", PrimitiveType.STRING), + DOCKER_CONFIG_FILE("docker-compose-override", PrimitiveType.STRING); public final PrimitiveType type; diff --git a/core/src/main/java/org/lflang/target/property/type/PlatformType.java b/core/src/main/java/org/lflang/target/property/type/PlatformType.java index 61ec5b0b1d..f861753bf8 100644 --- a/core/src/main/java/org/lflang/target/property/type/PlatformType.java +++ b/core/src/main/java/org/lflang/target/property/type/PlatformType.java @@ -13,7 +13,7 @@ protected Class enumClass() { public enum Platform { AUTO, ARDUINO, // FIXME: not multithreaded - NRF52("Nrf52", false), + NRF52("nRF52", false), RP2040("Rp2040", true), LINUX("Linux", true), MAC("Darwin", true), diff --git a/core/src/main/java/org/lflang/util/LFCommand.java b/core/src/main/java/org/lflang/util/LFCommand.java index 3cc4c6286a..d91570736f 100644 --- a/core/src/main/java/org/lflang/util/LFCommand.java +++ b/core/src/main/java/org/lflang/util/LFCommand.java @@ -96,11 +96,7 @@ public File directory() { /** Get a String representation of the stored command */ public String toString() { - return String.join( - " ", - (Iterable) - () -> - processBuilder.command().stream().map(it -> it.replace("'", "'\"'\"'")).iterator()); + return String.join(" ", processBuilder.command()); } /** diff --git a/core/src/main/java/org/lflang/validation/LFValidator.java b/core/src/main/java/org/lflang/validation/LFValidator.java index b3260cdb02..0e245be2db 100644 --- a/core/src/main/java/org/lflang/validation/LFValidator.java +++ b/core/src/main/java/org/lflang/validation/LFValidator.java @@ -73,6 +73,7 @@ import org.lflang.lf.BracketListExpression; import org.lflang.lf.BuiltinTrigger; import org.lflang.lf.BuiltinTriggerRef; +import org.lflang.lf.CodeExpr; import org.lflang.lf.Connection; import org.lflang.lf.Deadline; import org.lflang.lf.Expression; @@ -1005,6 +1006,10 @@ public void checkReactor(Reactor reactor) throws IOException { @Check(CheckType.FAST) public void checkSerializer(Serializer serializer) { boolean isValidSerializer = false; + if (this.target == Target.Python) { + // Allow any serializer package name in python + isValidSerializer = true; + } for (SupportedSerializers method : SupportedSerializers.values()) { if (method.name().equalsIgnoreCase(serializer.getType())) { isValidSerializer = true; @@ -1603,16 +1608,21 @@ private void checkExpressionIsTime(Expression value, EStructuralFeature feature) error("Missing time unit.", feature); return; } - } else if (target == Target.CPP && value instanceof ParenthesisListExpression) { - final var exprs = ((ParenthesisListExpression) value).getItems(); - if (exprs.size() == 1) { - checkExpressionIsTime(exprs.get(0), feature); - return; - } - } else if (target == Target.CPP && value instanceof BracedListExpression) { - final var exprs = ((BracedListExpression) value).getItems(); - if (exprs.size() == 1) { - checkExpressionIsTime(exprs.get(0), feature); + } else if (target == Target.CPP) { + if (value instanceof ParenthesisListExpression) { + final var exprs = ((ParenthesisListExpression) value).getItems(); + if (exprs.size() == 1) { + checkExpressionIsTime(exprs.get(0), feature); + return; + } + } else if (value instanceof BracedListExpression) { + final var exprs = ((BracedListExpression) value).getItems(); + if (exprs.size() == 1) { + checkExpressionIsTime(exprs.get(0), feature); + return; + } + } else if (value instanceof CodeExpr) { + // We leave checking of target code expressions to the target compiler return; } } diff --git a/core/src/main/kotlin/org/lflang/generator/cpp/CppGenerator.kt b/core/src/main/kotlin/org/lflang/generator/cpp/CppGenerator.kt index adfbbb9dda..5aebf32092 100644 --- a/core/src/main/kotlin/org/lflang/generator/cpp/CppGenerator.kt +++ b/core/src/main/kotlin/org/lflang/generator/cpp/CppGenerator.kt @@ -96,6 +96,7 @@ class CppGenerator( ) if (targetConfig.get(DockerProperty.INSTANCE).enabled) { copySrcGenBaseDirIntoDockerDir() + FileUtil.copyDirectoryContents(context.fileConfig.srcPkgPath.resolve("src"), context.fileConfig.srcGenPath.resolve("src"), true) buildUsingDocker() } else { if (platformGenerator.doCompile(context)) { diff --git a/core/src/main/kotlin/org/lflang/generator/cpp/CppPreambleGenerator.kt b/core/src/main/kotlin/org/lflang/generator/cpp/CppPreambleGenerator.kt index 9ec9b31b3e..129aefa063 100644 --- a/core/src/main/kotlin/org/lflang/generator/cpp/CppPreambleGenerator.kt +++ b/core/src/main/kotlin/org/lflang/generator/cpp/CppPreambleGenerator.kt @@ -60,6 +60,8 @@ class CppPreambleGenerator( |#include "reactor-cpp/reactor-cpp.hh" ${" |"..includes.joinToString(separator = "\n", prefix = "// include the preambles from imported files \n")} | + |using namespace std::chrono_literals; + | ${" |"..publicPreambles.joinToString(separator = "\n") { it.code.toText() }} """.trimMargin() } diff --git a/core/src/main/kotlin/org/lflang/generator/cpp/CppRos2Generator.kt b/core/src/main/kotlin/org/lflang/generator/cpp/CppRos2Generator.kt index 7ff170c3de..58a7f005a0 100644 --- a/core/src/main/kotlin/org/lflang/generator/cpp/CppRos2Generator.kt +++ b/core/src/main/kotlin/org/lflang/generator/cpp/CppRos2Generator.kt @@ -1,9 +1,9 @@ package org.lflang.generator.cpp +import org.apache.commons.text.StringEscapeUtils import org.lflang.generator.LFGeneratorContext import org.lflang.generator.docker.DockerGenerator import org.lflang.target.property.DockerProperty -import org.lflang.toUnixString import org.lflang.util.FileUtil import java.nio.file.Path @@ -83,6 +83,7 @@ class CppRos2Generator(generator: CppGenerator) : CppPlatformGenerator(generator inner class CppDockerGenerator(context: LFGeneratorContext?) : DockerGenerator(context) { override fun generateCopyForSources() = """ + COPY src src COPY src-gen src-gen COPY bin bin """.trimIndent() @@ -104,12 +105,27 @@ class CppRos2Generator(generator: CppGenerator) : CppPlatformGenerator(generator override fun defaultBuildCommands(): List { val commands = listOf( - listOf(".", "/opt/ros/rolling/setup.sh"), listOf("mkdir", "-p", "build"), listOf("colcon") + colconArgs(), ) return commands.map { argListToCommand(it) } } + + override fun getPreBuildCommand(): MutableList { + val script = context.targetConfig.get(DockerProperty.INSTANCE).preBuildScript + if (script.isNotEmpty()) { + return mutableListOf(". src/" + StringEscapeUtils.escapeXSI(script)) + } + return mutableListOf(". /opt/ros/rolling/setup.sh") + } + + override fun getPostBuildCommand(): MutableList { + val script = context.targetConfig.get(DockerProperty.INSTANCE).postBuildScript + if (script.isNotEmpty()) { + return mutableListOf(". src/" + StringEscapeUtils.escapeXSI(script)) + } + return mutableListOf() + } } override fun getDockerGenerator(context: LFGeneratorContext?): DockerGenerator = CppDockerGenerator(context) diff --git a/core/src/main/kotlin/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt b/core/src/main/kotlin/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt index 5d36e8e327..2c041fe6d2 100644 --- a/core/src/main/kotlin/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt +++ b/core/src/main/kotlin/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt @@ -153,8 +153,6 @@ class CppStandaloneCmakeGenerator(private val targetConfig: TargetConfig, privat |cmake_minimum_required(VERSION 3.5) |project(${fileConfig.name} VERSION 0.0.0 LANGUAGES CXX) | - |option(REACTOR_CPP_LINK_EXECINFO "Link against execinfo" OFF) - | |${if (targetConfig.get(ExternalRuntimePathProperty.INSTANCE) != null) "find_package(reactor-cpp PATHS ${targetConfig.get(ExternalRuntimePathProperty.INSTANCE)})" else ""} | |set(LF_MAIN_TARGET ${fileConfig.name}) @@ -169,10 +167,6 @@ class CppStandaloneCmakeGenerator(private val targetConfig: TargetConfig, privat |) |target_link_libraries($S{LF_MAIN_TARGET} $reactorCppTarget) | - |if(REACTOR_CPP_LINK_EXECINFO) - | target_link_libraries($S{LF_MAIN_TARGET} execinfo) - |endif() - | |if(MSVC) | target_compile_options($S{LF_MAIN_TARGET} PRIVATE /W4) |else() diff --git a/core/src/main/kotlin/org/lflang/generator/cpp/CppStandaloneGenerator.kt b/core/src/main/kotlin/org/lflang/generator/cpp/CppStandaloneGenerator.kt index d245285035..7ad7d3a589 100644 --- a/core/src/main/kotlin/org/lflang/generator/cpp/CppStandaloneGenerator.kt +++ b/core/src/main/kotlin/org/lflang/generator/cpp/CppStandaloneGenerator.kt @@ -1,10 +1,12 @@ package org.lflang.generator.cpp +import org.apache.commons.text.StringEscapeUtils import org.lflang.generator.CodeMap import org.lflang.generator.LFGeneratorContext import org.lflang.generator.docker.DockerGenerator import org.lflang.target.property.BuildTypeProperty import org.lflang.target.property.CompilerProperty +import org.lflang.target.property.DockerProperty import org.lflang.target.property.type.BuildTypeType.BuildType import org.lflang.toUnixString import org.lflang.util.FileUtil @@ -148,40 +150,51 @@ class CppStandaloneGenerator(generator: CppGenerator) : return 0 } - private fun createMakeCommand(buildPath: Path, parallelize: Boolean, target: String): LFCommand { + private fun getMakeArgs(buildPath: Path, parallelize: Boolean, target: String): List { val cmakeConfig = buildTypeToCmakeConfig(targetConfig.get(BuildTypeProperty.INSTANCE)) - val makeArgs: MutableList = listOf( + val makeArgs = mutableListOf( "--build", buildPath.fileName.toString(), "--target", target, "--config", cmakeConfig - ).toMutableList() + ) if (parallelize) { makeArgs.addAll(listOf("--parallel", Runtime.getRuntime().availableProcessors().toString())) } + return makeArgs + } + + + private fun createMakeCommand(buildPath: Path, parallelize: Boolean, target: String): LFCommand { + val makeArgs = getMakeArgs(buildPath, parallelize, target) return commandFactory.createCommand("cmake", makeArgs, buildPath.parent) } + private fun getCmakeArgs( + buildPath: Path, + outPath: Path, + sourcesRoot: String? = null + ) = cmakeArgs + listOf( + "-DCMAKE_INSTALL_PREFIX=${outPath.toUnixString()}", + "-DCMAKE_INSTALL_BINDIR=$relativeBinDir", + "-S", + sourcesRoot ?: fileConfig.srcGenBasePath.toUnixString(), + "-B", + buildPath.fileName.toString() + ) + private fun createCmakeCommand( buildPath: Path, outPath: Path, - additionalCmakeArgs: List = listOf(), sourcesRoot: String? = null ): LFCommand { val cmd = commandFactory.createCommand( "cmake", - cmakeArgs + additionalCmakeArgs + listOf( - "-DCMAKE_INSTALL_PREFIX=${outPath.toUnixString()}", - "-DCMAKE_INSTALL_BINDIR=$relativeBinDir", - "-S", - sourcesRoot ?: fileConfig.srcGenBasePath.toUnixString(), - "-B", - buildPath.fileName.toString() - ), + getCmakeArgs(buildPath, outPath, sourcesRoot), buildPath.parent ) @@ -195,15 +208,16 @@ class CppStandaloneGenerator(generator: CppGenerator) : inner class StandaloneDockerGenerator(context: LFGeneratorContext?) : DockerGenerator(context) { - override fun generateCopyForSources(): String = "COPY src-gen src-gen" + override fun generateCopyForSources(): String = """ + COPY src src + COPY src-gen src-gen + """.trimIndent() override fun defaultImage(): String = DEFAULT_BASE_IMAGE override fun generateRunForInstallingDeps(): String { return if (builderBase() == defaultImage()) { - ("RUN set -ex && apk add --no-cache g++ musl-dev cmake make && apk add --no-cache" - + " --update --repository=https://dl-cdn.alpinelinux.org/alpine/v3.16/main/" - + " libexecinfo-dev") + ("RUN set -ex && apk add --no-cache g++ musl-dev cmake make") } else { "# (Skipping installation of build dependencies; custom base image.)" } @@ -225,17 +239,32 @@ class CppStandaloneGenerator(generator: CppGenerator) : val mkdirCommand = listOf("mkdir", "-p", "build") val commands = listOf( mkdirCommand, - createCmakeCommand( + listOf("cmake") + getCmakeArgs( Path.of("./build"), Path.of("."), - listOf("-DREACTOR_CPP_LINK_EXECINFO=ON"), "src-gen" - ).command(), - createMakeCommand(fileConfig.buildPath, true, fileConfig.name).command(), - createMakeCommand(Path.of("./build"), true, "install").command() + ), + listOf("cmake") + getMakeArgs(fileConfig.buildPath, true, fileConfig.name), + listOf("cmake") + getMakeArgs(Path.of("./build"), true, "install") ) return commands.map { argListToCommand(it) } } + + override fun getPreBuildCommand(): MutableList { + val script = context.targetConfig.get(DockerProperty.INSTANCE).preBuildScript + if (script.isNotEmpty()) { + return mutableListOf(". src/" + StringEscapeUtils.escapeXSI(script)) + } + return mutableListOf() + } + + override fun getPostBuildCommand(): MutableList { + val script = context.targetConfig.get(DockerProperty.INSTANCE).postBuildScript + if (script.isNotEmpty()) { + return mutableListOf(". src/" + StringEscapeUtils.escapeXSI(script)) + } + return mutableListOf() + } } override fun getDockerGenerator(context: LFGeneratorContext?): DockerGenerator = StandaloneDockerGenerator(context) diff --git a/core/src/main/resources/lib/c/reactor-c b/core/src/main/resources/lib/c/reactor-c index 38294a303c..6ef9154791 160000 --- a/core/src/main/resources/lib/c/reactor-c +++ b/core/src/main/resources/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit 38294a303cfe2e3efd2cbc94d690ef4756514cb5 +Subproject commit 6ef9154791ee9c4806f54c77d02f4c9a29420209 diff --git a/core/src/main/resources/lib/cpp/reactor-cpp b/core/src/main/resources/lib/cpp/reactor-cpp index f47e5174e4..dfdac2c19e 160000 --- a/core/src/main/resources/lib/cpp/reactor-cpp +++ b/core/src/main/resources/lib/cpp/reactor-cpp @@ -1 +1 @@ -Subproject commit f47e5174e4cbb891a886b06395da65cc8af79640 +Subproject commit dfdac2c19e8d111cf4741c8bf8f304678a59d025 diff --git a/core/src/main/resources/lib/rs/reactor-rs b/core/src/main/resources/lib/rs/reactor-rs index 4ac645947b..10fee74e32 160000 --- a/core/src/main/resources/lib/rs/reactor-rs +++ b/core/src/main/resources/lib/rs/reactor-rs @@ -1 +1 @@ -Subproject commit 4ac645947bf7916a5e4c967207573b78aa2206f7 +Subproject commit 10fee74e32a72f15ec3bc5605d61c27f63c8e037 diff --git a/core/src/main/resources/lib/rs/runtime-version.properties b/core/src/main/resources/lib/rs/runtime-version.properties index 2895d16bf7..95b59bc67a 100644 --- a/core/src/main/resources/lib/rs/runtime-version.properties +++ b/core/src/main/resources/lib/rs/runtime-version.properties @@ -1 +1 @@ -rs = 4ac645947bf7916a5e4c967207573b78aa2206f7 +rs = 10fee74e32a72f15ec3bc5605d61c27f63c8e037 diff --git a/core/src/main/resources/org/lflang/StringsBundle.properties b/core/src/main/resources/org/lflang/StringsBundle.properties index 7fdafe5c4e..e0689f1107 100644 --- a/core/src/main/resources/org/lflang/StringsBundle.properties +++ b/core/src/main/resources/org/lflang/StringsBundle.properties @@ -1 +1 @@ -VERSION = 0.8.1-SNAPSHOT +VERSION = 0.8.3-SNAPSHOT diff --git a/gradle.properties b/gradle.properties index 02c3bffcba..8220073f54 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ [header] group=org.lflang -version=0.8.1-SNAPSHOT +version=0.8.3-SNAPSHOT [versions] antlrVersion=4.7.2 diff --git a/test/C/src/PreambleInherited.lf b/test/C/src/PreambleInherited.lf index 771185bdbc..2a2240a606 100644 --- a/test/C/src/PreambleInherited.lf +++ b/test/C/src/PreambleInherited.lf @@ -12,6 +12,9 @@ reactor A { } reactor B extends A { + reaction(startup) {= + printf("FOO 2: %d\n", FOO); + =} } main reactor { diff --git a/test/C/src/concurrent/Watchdog.lf b/test/C/src/concurrent/Watchdog.lf index 45903401a3..ba0eb06843 100644 --- a/test/C/src/concurrent/Watchdog.lf +++ b/test/C/src/concurrent/Watchdog.lf @@ -10,7 +10,7 @@ target C { } reactor Watcher(timeout: time = 1500 ms) { - // Offset ameliorates startup time. + // Offset may reduce the likelihood of flakiness if long startup times occur. timer t(1 s, 1 s) // Period has to be smaller than watchdog timeout. Produced if the watchdog triggers. output d: int diff --git a/test/C/src/concurrent/WatchdogAction.lf b/test/C/src/concurrent/WatchdogAction.lf new file mode 100644 index 0000000000..2ca5157142 --- /dev/null +++ b/test/C/src/concurrent/WatchdogAction.lf @@ -0,0 +1,74 @@ +/** + * Test watchdog. This test starts a watchdog timer of 1500ms every 1s. Half the time, it then + * sleeps after starting the watchdog so that the watchdog expires. There should be a total of two + * watchdog expirations. This version uses an action instead of a reaction to the watchdog. + * @author Benjamin Asch + * @author Edward A. Lee + */ +target C { + timeout: 11000 ms +} + +reactor Watcher(timeout: time = 1500 ms) { + // Offset may reduce the likelihood of flakiness if long startup times occur. + timer t(1 s, 1 s) + // Period has to be smaller than watchdog timeout. Produced if the watchdog triggers. + output d: int + state count: int = 0 + logical action a + + watchdog poodle(timeout) -> a {= + instant_t p = lf_time_physical_elapsed(); + lf_print("******** Watchdog timed out at elapsed physical time: " PRINTF_TIME, p); + self->count++; + lf_schedule(a, 0); + =} + + reaction(t) -> poodle, d {= + lf_watchdog_start(poodle, 0); + lf_print("Watchdog started at physical time " PRINTF_TIME, lf_time_physical_elapsed()); + lf_print("Will expire at " PRINTF_TIME, lf_time_logical_elapsed() + self->timeout); + lf_set(d, 42); + =} + + reaction(a) -> d {= + lf_print("Reaction poodle was called."); + lf_set(d, 1); + =} + + reaction(shutdown) -> poodle {= + lf_watchdog_stop(poodle); + // Watchdog may expire in tests even without the sleep, but it should at least expire twice. + if (self->count < 2) { + lf_print_error_and_exit("Watchdog expired %d times. Expected at least 2.", self->count); + } + =} +} + +main reactor { + logical action a + state count: int = 0 + + w = new Watcher() + + reaction(startup) {= + if (NUMBER_OF_WATCHDOGS != 1) { + lf_print_error_and_exit("NUMBER_OF_WATCHDOGS was %d", NUMBER_OF_WATCHDOGS); + } + =} + + reaction(w.d) {= + lf_print("Watcher reactor produced an output. %d", self->count % 2); + self->count++; + if (self->count % 4 == 0) { + lf_print(">>>>>> Taking a long time to process that output!"); + lf_sleep(MSEC(1600)); + } + =} + + reaction(shutdown) {= + if (self->count < 12) { + lf_print_error_and_exit("Watchdog produced output %d times. Expected at least 12.", self->count); + } + =} +} diff --git a/test/Cpp/src/target/TimeExpression.lf b/test/Cpp/src/target/TimeExpression.lf new file mode 100644 index 0000000000..cc3c49156c --- /dev/null +++ b/test/Cpp/src/target/TimeExpression.lf @@ -0,0 +1,53 @@ +target Cpp { + timeout: 1 s +} + +public preamble {= + inline reactor::Duration get_delay() { return 200ms; } +=} + +reactor Foo(foo: time = {= get_delay() =}) { + timer t(0, foo) + + state timer_triggered: bool = false + + reaction(t) {= + timer_triggered = true; + if (get_elapsed_logical_time() % 200ms != reactor::Duration::zero()) { + std::cerr << "ERROR: timer triggered at an unexpected time\n"; + exit(4); + } + =} + + reaction(shutdown) {= + if (!timer_triggered) { + std::cerr << "ERROR: timer did not trigger\n"; + exit(1); + } + =} +} + +main reactor { + foo = new Foo() + + state timer_triggered: bool = false + + timer t(0, {= get_delay() + 200ms =}) + + reaction(t) {= + timer_triggered = true; + if (get_elapsed_logical_time() % 400ms != reactor::Duration::zero()) { + std::cerr << "ERROR: timer triggered at an unexpected time\n"; + exit(4); + } + =} + + reaction(shutdown) {= + if (!timer_triggered) { + std::cerr << "ERROR: timer did not trigger\n"; + exit(2); + } + + std::cout << "Success\n"; + =} +} diff --git a/test/Python/src/BankIndex.lf b/test/Python/src/BankIndex.lf new file mode 100644 index 0000000000..76494f5d93 --- /dev/null +++ b/test/Python/src/BankIndex.lf @@ -0,0 +1,14 @@ +target Python + +reactor A(bank_index=0, value=0) { + reaction(startup) {= + print("bank_index: {:d}, value: {:d}".format(self.bank_index, self.value)) + if (self.value != 4 - self.bank_index): + sys.stderr.write("ERROR: Expected value to be 4 - bank_index.\n") + exit(1) + =} +} + +main reactor(table = [4, 3, 2, 1]) { + a = new[4] A(value = {= self.table[bank_index] =}) +} diff --git a/test/Python/src/PreambleInherited.lf b/test/Python/src/PreambleInherited.lf new file mode 100644 index 0000000000..3c27e03b52 --- /dev/null +++ b/test/Python/src/PreambleInherited.lf @@ -0,0 +1,18 @@ +target Python + +reactor Base { + preamble {= + def initiation(self): + print("Hello World\n") + =} +} + +reactor Extended extends Base { + reaction(startup) {= + self.initiation() + =} +} + +main reactor { + e = new Extended() +} diff --git a/test/Python/src/PythonPaths.lf b/test/Python/src/PythonPaths.lf new file mode 100644 index 0000000000..7bdb1767e1 --- /dev/null +++ b/test/Python/src/PythonPaths.lf @@ -0,0 +1,22 @@ +/** + * This tests the functions lf.source_directory() and lf.package_directory(). Success is just + * compiling and running without error. The test prints the contents of this file twice. + */ +target Python + +preamble {= + import os +=} + +main reactor { + state source_path = {= os.path.join(lf.source_directory(), "PythonPaths.lf") =} + state package_path = {= os.path.join(lf.package_directory(), "src", "PythonPaths.lf") =} + + reaction(startup) {= + with open(self.source_path, "r") as file: + print(file.read()); + print("----------------"); + with open(self.package_path, "r") as file: + print(file.read()); + =} +} diff --git a/test/Python/src/docker/DockerComposeConfig.docker.yml b/test/Python/src/docker/DockerComposeConfig.docker.yml new file mode 100644 index 0000000000..07097a3cfd --- /dev/null +++ b/test/Python/src/docker/DockerComposeConfig.docker.yml @@ -0,0 +1,13 @@ +services: + federate__server: + environment: + - NVIDIA_DISABLE_REQUIRE=1 + shm_size: '4gb' + volumes: + - hugging_face_cache:/root/.cache + + federate__client: + shm_size: '2gb' + +volumes: + hugging_face_cache: diff --git a/test/Python/src/docker/DockerComposeConfig.lf b/test/Python/src/docker/DockerComposeConfig.lf new file mode 100644 index 0000000000..7a3c113ae5 --- /dev/null +++ b/test/Python/src/docker/DockerComposeConfig.lf @@ -0,0 +1,56 @@ +target Python { + coordination: decentralized, + docker: { + docker-compose-override: "./DockerComposeConfig.docker.yml", + rti-image: "rti:local" + } +} + +reactor Client(STP_offset = 2 s) { + input server_message + output client_message + + reaction(startup) {= + print("Client Startup!") + =} + + reaction(server_message) -> client_message {= + val = server_message.value + val += 1 + print("client:", val) + if val==49: + print("client done") + request_stop() + if val<49: + client_message.set(val) + =} +} + +reactor Server(STP_offset = 2 s) { + output server_message + input client_message + + reaction(startup) -> server_message {= + print("Server Startup!") + server_message.set(0) + =} + + reaction(client_message) -> server_message {= + val = client_message.value + val += 1 + print("server:", val) + if val==48: + print("server done") + server_message.set(val) + request_stop() + if val<48: + server_message.set(val) + =} +} + +federated reactor(STP_offset = 2 s) { + client = new Client() + server = new Server() + server.server_message -> client.server_message after 100 ms + client.client_message -> server.client_message +} diff --git a/test/Python/src/docker/FilesPropertyContainerized.lf b/test/Python/src/docker/FilesPropertyContainerized.lf index 139f1b682a..800e020f04 100644 --- a/test/Python/src/docker/FilesPropertyContainerized.lf +++ b/test/Python/src/docker/FilesPropertyContainerized.lf @@ -1,6 +1,8 @@ target Python { files: "../include/hello.py", - docker: true + docker: { + rti-image: "rti:local" + } } preamble {= diff --git a/test/Python/src/docker/HelloWorldContainerized.lf b/test/Python/src/docker/HelloWorldContainerized.lf index 26e684c580..64df9fb222 100644 --- a/test/Python/src/docker/HelloWorldContainerized.lf +++ b/test/Python/src/docker/HelloWorldContainerized.lf @@ -1,5 +1,7 @@ target Python { - docker: true + docker: { + rti-image: "rti:local" + } } import HelloWorld2 from "../HelloWorld.lf" diff --git a/test/Python/src/docker/PingPongContainerized.lf b/test/Python/src/docker/PingPongContainerized.lf index 1539a12615..708d4213ff 100644 --- a/test/Python/src/docker/PingPongContainerized.lf +++ b/test/Python/src/docker/PingPongContainerized.lf @@ -19,7 +19,9 @@ */ target Python { fast: true, - docker: true + docker: { + rti-image: "rti:local" + } } import Ping from "../PingPong.lf" diff --git a/test/Python/src/docker/federated/DistributedCountContainerized.lf b/test/Python/src/docker/federated/DistributedCountContainerized.lf index 48b71bd046..785467d12f 100644 --- a/test/Python/src/docker/federated/DistributedCountContainerized.lf +++ b/test/Python/src/docker/federated/DistributedCountContainerized.lf @@ -8,7 +8,9 @@ target Python { timeout: 5 sec, logging: DEBUG, coordination: centralized, - docker: true + docker: { + rti-image: "rti:local" + } } import Count from "../../lib/Count.lf" diff --git a/test/Python/src/docker/federated/DistributedMultiportContainerized.lf b/test/Python/src/docker/federated/DistributedMultiportContainerized.lf index b4333d8c00..1c07a9c174 100644 --- a/test/Python/src/docker/federated/DistributedMultiportContainerized.lf +++ b/test/Python/src/docker/federated/DistributedMultiportContainerized.lf @@ -2,7 +2,9 @@ target Python { timeout: 1 sec, coordination: centralized, - docker: true + docker: { + rti-image: "rti:local" + } } import Source, Destination from "../../federated/DistributedMultiport.lf" diff --git a/test/Python/src/docker/federated/DistributedSendClassContainerized.lf b/test/Python/src/docker/federated/DistributedSendClassContainerized.lf index 797a017146..c13fbea651 100644 --- a/test/Python/src/docker/federated/DistributedSendClassContainerized.lf +++ b/test/Python/src/docker/federated/DistributedSendClassContainerized.lf @@ -1,6 +1,8 @@ target Python { coordination: centralized, - docker: true + docker: { + rti-image: "rti:local" + } } import A, B from "../../federated/DistributedSendClass.lf" diff --git a/test/Python/src/docker/federated/DistributedDoublePortContainerized.lf b/test/Python/src/docker/federated/failing/DistributedDoublePortContainerized.lf similarity index 100% rename from test/Python/src/docker/federated/DistributedDoublePortContainerized.lf rename to test/Python/src/docker/federated/failing/DistributedDoublePortContainerized.lf diff --git a/test/Python/src/docker/federated/DistributedStopDecentralizedContainerized.lf b/test/Python/src/docker/federated/failing/DistributedStopDecentralizedContainerized.lf similarity index 100% rename from test/Python/src/docker/federated/DistributedStopDecentralizedContainerized.lf rename to test/Python/src/docker/federated/failing/DistributedStopDecentralizedContainerized.lf diff --git a/test/Python/src/federated/Dataflow.lf b/test/Python/src/federated/Dataflow.lf new file mode 100644 index 0000000000..596b18f021 --- /dev/null +++ b/test/Python/src/federated/Dataflow.lf @@ -0,0 +1,68 @@ +target Python { + coordination: decentralized # logging: debug +} + +preamble {= + import time +=} + +reactor Client(STP_offset = {= FOREVER =}) { + input server_message + output client_message + + reaction(startup) {= + print("Client Startup!") + =} + + reaction(server_message) -> client_message {= + val = server_message.value + time.sleep(0.1) + val += 1 + print("client:", val) + if val==49: + print("client done") + request_stop() + if val<49: + client_message.set(val) + =} STP(10 s) {= + print("Client STP Violated!") + exit(1) + =} +} + +reactor Server(STP_offset = {= FOREVER =}) { + output server_message + input client_message1 + input client_message2 + + reaction(startup) -> server_message {= + print("Server Startup!") + server_message.set(0) + =} + + reaction(client_message1, client_message2) -> server_message {= + val = max(client_message1.value, client_message2.value) + time.sleep(0.1) + val += 1 + print("server:", val) + if val==48: + print("server done") + server_message.set(val) + request_stop() + if val<48: + server_message.set(val) + =} STP(10 s) {= + print("Server STP Violated!") + exit(1) + =} +} + +federated reactor(STP_offset = {= FOREVER =}) { + client1 = new Client() + client2 = new Client() + server = new Server() + server.server_message -> client1.server_message + client1.client_message -> server.client_message1 after 0 + server.server_message -> client2.server_message + client2.client_message -> server.client_message2 after 0 +} diff --git a/test/Python/src/serialization/CustomSerializer.lf b/test/Python/src/serialization/CustomSerializer.lf new file mode 100644 index 0000000000..adcec6a350 --- /dev/null +++ b/test/Python/src/serialization/CustomSerializer.lf @@ -0,0 +1,70 @@ +# To run this test, the `pickle_serializer` package must be installed in the Python environment. +# Run `pip3 install -e ./test/Python/src/serialization/pickle_serializer` in the project root directory to install the pickle_serializer. +target Python { + coordination: decentralized +} + +preamble {= + os.system("pip install ./src/serialization/pickle_serializer/ --user") +=} + +reactor Client { + input server_message + output client_message + state count + + reaction(startup) {= + self.count = 0 + print("Client Startup!") + =} + + reaction(server_message) -> client_message {= + val = server_message.value + if val != self.count: + print("client: out of order", val, self.count) + exit(1) + self.count+=2 + val += 1 + print("client:", val) + if val==23: + print("client done") + request_stop() + if val<23: + client_message.set(val) + =} +} + +reactor Server { + output server_message + input client_message + state count + + reaction(startup) -> server_message {= + self.count = 1 + print("Server Startup!") + server_message.set(0) + =} + + reaction(client_message) -> server_message {= + val = client_message.value + if val != self.count: + print("server: out of order", val, self.count) + exit(1) + self.count+=2 + val += 1 + print("server:", val) + if val==22: + print("server done") + server_message.set(val) + request_stop() + if val<22: + server_message.set(val) + =} +} + +federated reactor { + client = new Client() + server = new Server() + server.server_message -> client.server_message after 100 ms serializer "pickle_serializer" + client.client_message -> server.client_message serializer "pickle_serializer" +} diff --git a/test/Python/src/serialization/pickle_serializer/pickle_serializer/__init__.py b/test/Python/src/serialization/pickle_serializer/pickle_serializer/__init__.py new file mode 100644 index 0000000000..0bfb262658 --- /dev/null +++ b/test/Python/src/serialization/pickle_serializer/pickle_serializer/__init__.py @@ -0,0 +1 @@ +from .serializer import Serializer \ No newline at end of file diff --git a/test/Python/src/serialization/pickle_serializer/pickle_serializer/serializer.py b/test/Python/src/serialization/pickle_serializer/pickle_serializer/serializer.py new file mode 100644 index 0000000000..8a9ce82ae1 --- /dev/null +++ b/test/Python/src/serialization/pickle_serializer/pickle_serializer/serializer.py @@ -0,0 +1,7 @@ +import pickle + +class Serializer(): + def serialize(self, obj)->bytes: + return pickle.dumps(obj) + def deserialize(self, message:bytes): + return pickle.loads(message) \ No newline at end of file diff --git a/test/Python/src/serialization/pickle_serializer/setup.py b/test/Python/src/serialization/pickle_serializer/setup.py new file mode 100644 index 0000000000..f08a0ca8c1 --- /dev/null +++ b/test/Python/src/serialization/pickle_serializer/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup, find_packages + +setup( + name='pickle_serializer', + version='0.1', + packages=find_packages(), + install_requires=[], +)