Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jacobwilliams/daglib
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: sourceryinstitute/dag
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.

Commits on Apr 9, 2020

  1. rearrange directory tree;adjust README accordingly

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    8eee185 View commit details
  2. remove fobis so we maintain only one build system

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    b474e05 View commit details
  3. build with cmake; test with ctest

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    97b8d41 View commit details
  4. make passing test quiet: ouput only "Test passed."

    also error-terminate upon whenever test failure is
    first detected.
    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    47f2b28 View commit details
  5. update README.md & LICENSE

    README edits:
    1. Replace FoBiS text with CMake
    2. Prerequisites list
    3. Wording changes
    4. New links
    
    LICENSE edit:
    1. Add Sourcery Institute license
    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    ae78dfa View commit details
  6. update/fix links in README.md

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    95f2829 View commit details
  7. add only clause; white space formatting

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    fc05330 View commit details
  8. remove unnecessary destroy type-bound procedure

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    c0592f2 View commit details
  9. split dag/vertex types/procedures into 2 modules

    1 module becomes 2:
    dag_module -> { dag_interface, vertex_interface}
    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    d85cb4c View commit details
  10. separate vertex interface and implementation

    1 file becomes 2:
    vertex_interface -> {vertex_interface, vertex_implementation}
    
    where the interface file is a module with interface bodies and
    wher the implemenation file is a submodule with the corresponding
    procedure definitions.
    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    ecf2d28 View commit details
  11. make vertex type public

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    7dc721f View commit details
  12. dag_interface uses vertex_interface

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    5498d3f View commit details
  13. separate dag interface and implementation

    1 file becomes 2:
    dag_interface -> {dag_interface, dag_implementation}
    
    where the interface file contains a module with interface bodies and
    where the implemenation file contains a submodule with the
    corresponding procedure definitions.
    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    923e839 View commit details
  14. WIP: doing a simple representation of dep.dat (per powerpoint slide) …

    …as example - Error is that it will not compile because cannot have empty array constructures in the set_edges routines
    singleterry committed Apr 9, 2020
    Copy the full SHA
    94b038c View commit details
  15. Merge branch 'master' into dep.dat_test

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    6126c97 View commit details
  16. set empty array type

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    f5d706f View commit details
  17. multiple-independents passes order test

    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    b402c4d View commit details
  18. multiple-indendents test dependency matrix passes

    + white space adjustments
    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    201eb3a View commit details
  19. fix GraphViz output for independent vertices

    When a vertex has no adjacent edges, this commit removes the arrow
    (` -> `) from the corresponding line in the GraphViz digraph
    structure.
    Damian Rouson committed Apr 9, 2020
    Copy the full SHA
    bd65ea8 View commit details

Commits on Apr 10, 2020

  1. allow for incorporating daglib as cmake subproject

    substitute in top-level CMakeLists.txt
    CMAKE_BINARY_DIR->PROJECT_BINARY_DIR
    Damian Rouson committed Apr 10, 2020
    Copy the full SHA
    7a54d75 View commit details

Commits on Apr 23, 2020

  1. fix typos & use better links

    Damian Rouson committed Apr 23, 2020
    Copy the full SHA
    559a6e2 View commit details
  2. Merge pull request #1 from sourceryinstitute/update-readme

    fix typos & use better links
    Damian Rouson authored Apr 23, 2020
    Copy the full SHA
    978e743 View commit details

Commits on Apr 24, 2020

  1. build parallel tests

    Damian Rouson committed Apr 24, 2020
    Copy the full SHA
    52aff3c View commit details
  2. Merge pull request #2 from sourceryinstitute/parallel-build

    build parallel tests
    Damian Rouson authored Apr 24, 2020
    Copy the full SHA
    2d92b7e View commit details

Commits on Apr 28, 2020

  1. Delete init.csh

    Damian Rouson committed Apr 28, 2020
    Copy the full SHA
    e3c2b19 View commit details
  2. Merge pull request #3 from sourceryinstitute/remove-init-csh

    Delete init.csh
    Damian Rouson authored Apr 28, 2020
    Copy the full SHA
    1a72892 View commit details

Commits on Apr 29, 2020

  1. update FORD project file; work around ford bug

    Damian Rouson committed Apr 29, 2020
    Copy the full SHA
    22e7f86 View commit details
  2. Merge pull request #4 from sourceryinstitute/ford

    update FORD project file; work around ford bug
    Damian Rouson authored Apr 29, 2020
    Copy the full SHA
    9ea770a View commit details

Commits on Apr 30, 2020

  1. fix CMake typo that caused false test passes

    Damian Rouson committed Apr 30, 2020
    Copy the full SHA
    9565583 View commit details
  2. add test-input-output stub

    Damian Rouson committed Apr 30, 2020
    Copy the full SHA
    1a84537 View commit details
  3. add/build json-fortran git submodule

    Damian Rouson committed Apr 30, 2020
    Copy the full SHA
    09edfe6 View commit details

Commits on May 13, 2020

  1. dag output to json file works

    Damian Rouson committed May 13, 2020
    Copy the full SHA
    9a53ea2 View commit details

Commits on May 14, 2020

  1. json%add unlimited polymorphic assumed-rank

    Damian Rouson committed May 14, 2020
    Copy the full SHA
    c97a0b9 View commit details
  2. replace repetitive conditionals w/procedure calls

    Damian Rouson committed May 14, 2020
    Copy the full SHA
    d2a6630 View commit details
  3. Merge pull request #5 from sourceryinstitute/dag-output

    Add type-bound procedure for dag output to a JSON file
    Damian Rouson authored May 14, 2020
    Copy the full SHA
    fc407c2 View commit details
  4. eliminate redundant submodule comments

    Damian Rouson committed May 14, 2020
    Copy the full SHA
    02377c6 View commit details
  5. add vertex getters

    Damian Rouson committed May 14, 2020
    Copy the full SHA
    1be338f View commit details

Commits on May 15, 2020

  1. make all vertex components private

    use C preprocessor macros to make the edges component public
    with gfortran to work around an assumed-rank bug.
    Damian Rouson committed May 15, 2020
    Copy the full SHA
    e3c7646 View commit details
  2. Merge pull request #6 from sourceryinstitute/vertex-accessors

    Vertex accessors
    Damian Rouson authored May 15, 2020
    Copy the full SHA
    d4309c8 View commit details
  3. eliminate unused variable

    Damian Rouson committed May 15, 2020
    Copy the full SHA
    ed2a605 View commit details
  4. add vertex edge getter

    Damian Rouson committed May 15, 2020
    Copy the full SHA
    c37a3eb View commit details

Commits on May 16, 2020

  1. add -fcoarray=single for gfortran w/o OpenCoarrays

    Damian Rouson committed May 16, 2020
    Copy the full SHA
    729d71f View commit details
  2. rm redundant submodule comments copied from module

    Damian Rouson committed May 16, 2020
    Copy the full SHA
    aa32b9c View commit details
  3. white space edits; remove redundant implicit nones

    Damian Rouson committed May 16, 2020
    Copy the full SHA
    1051373 View commit details
  4. add minimal assertion utility

    Damian Rouson committed May 16, 2020
    Copy the full SHA
    e0d4f97 View commit details
  5. remove redundant dag component

    This commit removes the dag%n component, which held the value of
    size(dag%vertices).  Storing redundant information imposes the
    burden of maintaining consistency with the actual array size
    and adds unnecessary code.
    Damian Rouson committed May 16, 2020
    Copy the full SHA
    42777f0 View commit details
  6. Merge pull request #7 from sourceryinstitute/rm-redundant-component

    Remove redundant dag%n component
    Damian Rouson authored May 16, 2020
    Copy the full SHA
    8df5eac View commit details

Commits on May 17, 2020

  1. refactor dag%output

    Damian Rouson committed May 17, 2020
    Copy the full SHA
    f48d008 View commit details
  2. add submodules: yaFyaml + prereqs gtfl/gftl-shared

    Damian Rouson committed May 17, 2020
    Copy the full SHA
    ffccd2f View commit details

Commits on May 25, 2020

  1. build yaFyaml & its dependencies gftl/gftl-shared

    Damian Rouson committed May 25, 2020
    Copy the full SHA
    b2b8f87 View commit details
35 changes: 35 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on: [push, pull_request]

jobs:
Build:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
env:
FC: gfortran
GCC_V: 12

steps:
- name: Checkout code
uses: actions/checkout@v3

- uses: fortran-lang/setup-fpm@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Install Dependencies Ubuntu
if: contains(matrix.os, 'ubuntu')
run: |
sudo apt-get update
sudo apt install -y gfortran-${GCC_V}
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-${GCC_V} 100 \
--slave /usr/bin/gfortran gfortran /usr/bin/gfortran-${GCC_V} \
--slave /usr/bin/gcov gcov /usr/bin/gcov-${GCC_V}
- name: Build and test
run: |
which gfortran
gfortran --version
fpm test
48 changes: 48 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Build and Deploy Documentation

on: [push, pull_request]


jobs:
Build:
runs-on: ubuntu-22.04

env:
FC: gfortran
GCC_V: 12

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Install Dependencies Ubuntu
run: |
sudo apt-get update
sudo apt install -y gfortran-${GCC_V} python3-dev graphviz
sudo pip install ford
- name: Build Developer Documenation
run: |
ford doc-generator.md
- name: Upload Documentation
uses: actions/upload-artifact@v2
with:
name: documentation
path: doc/html
if-no-files-found: error

- name: Broken Link Check
if: ${{ github.ref == 'refs/heads/main'}}
uses: technote-space/broken-link-checker-action@v1
with:
TARGET: file://${{ github.workspace }}/doc/html/index.html
RECURSIVE: true
ASSIGNEES: ${{ github.actor }}

- name: Deploy API Documentation
uses: JamesIves/github-pages-deploy-action@4.1.0
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
with:
branch: gh-pages
folder: doc/html
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@

# Fortran module files
*.mod
*.smod

# Compiled Static libraries
*.lai
@@ -29,6 +30,13 @@

# Directories
build
doc
lib
bin
doc/html

# FORD Documentation
*.s

# Output files
*.dot
*.pdf
Empty file added .gitmodules
Empty file.
31 changes: 31 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
Copyright (c) 2020, Sourcery Institute
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* 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.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

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.

------------------------------------------------------------------------------

Modern Fortran DAG Library
https://github.com/jacobwilliams/daglib

@@ -27,3 +57,4 @@ 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.
BSD 3-Clause License
224 changes: 138 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,146 @@
### Overview
.___
__| _/____ ____
/ __ |\__ \ / ___\
/ /_/ | / __ \_/ /_/ >
\____ |(____ /\___ /
\/ \//_____/

Overview
========

DAG is a Fortran 2018 library for creating and manipulating directed acyclic graphs (DAGs).
DAG is based on a fork of [daglib by Jacob Williams], refactored to

* Adopt a functional programming pattern based on (mostly) pure functions.
* Add build system and test harness automated by [fpm],
* Add unit testing written with [veggies],
* Add continuous-integration testing and documentation deployment via [GitHub Actions],
* Add [documentation] generated by [FORD],
* Add runtime assertion-checking using [Assert], and
* Add [JSON] file input/output using [rojff].
* Ensure that dag objects always have a valid state that includes a topological
ordering stored without mutating vertices or vertex arrays.

Usage
-----
When compilers cooperate, we recommend using `associate` to assign names to
dag's user-defined structure constructor results. See the [example]
subdirectory for demonstrations of how we use `associate` instead of the
declare-and-define style that is much more common in Fortran
and other imperative programming languages. Associating a name instead of
declaring an object and then later defining it offers several potential
advantages:

* It prevents the accidental use of declared-but-undefined data.
* It provides immutable state, which in turn
- prevents the mistaken overwriting of data and
- enables some possible compiler optimizations.

_Caveat emptor_: your mileage may vary. Compiler support for `associate` is
evolving and compiler might or might not exploit optimizatio opportunities.
When weird things happen, resort to declaring and subsequently defining
objects by assigning a constructor function result to the declared object.

Prerequisites
-------------
The [fpm] package manager automatically downloads and builds all
dependencies. See [fpm.toml] for a complete list, including version numbers.

Downloading
-----------
Obtain dag by downloading the [latest release] or cloning the repository as
follows:

DAGLIB is a modern Fortran module for creating and manipulating directed acyclic graphs (DAGs). It includes a toposort feature, and also the ability to generate files in the [GraphViz](https://www.graphviz.org) "dot" notation.

### Building

A [FoBiS](https://github.com/szaghi/FoBiS) configuration file (`daglib.fobis`) is provided that can build the library and examples. Use the `mode` flag to indicate what to build. For example:

* To build all the examples using gfortran: `FoBiS.py build -f daglib.fobis -mode tests-gnu`
* To build all the examples using ifort: `FoBiS.py build -f daglib.fobis -mode tests-intel`
* To build a static library using gfortran: `FoBiS.py build -f daglib.fobis -mode static-gnu`
* To build a static library using ifort: `FoBiS.py build -f daglib.fobis -mode static-intel`

The full set of modes are:

* `static-gnu`
* `static-gnu-debug`
* `static-intel`
* `static-intel-debug`
* `shared-gnu`
* `shared-gnu-debug`
* `shared-intel`
* `shared-intel-debug`
* `tests-gnu`
* `tests-gnu-debug`
* `tests-intel`
* `tests-intel-debug`

### Example

A simple example is shown below:

```fortran
program dag_example
use dag_module
implicit none
type(dag) :: d
integer,dimension(:),allocatable :: order
integer :: istat
integer :: i
integer,parameter :: n_nodes = 6
character(len=*),parameter :: filetype = 'pdf'
! create a dag:
call d%set_vertices(n_nodes)
call d%set_edges(2,[1]) !2 depends on 1
call d%set_edges(3,[5,1]) !3 depends on 5 and 1
call d%set_edges(4,[5]) !4 depends on 5
call d%set_edges(5,[2]) !5 depends on 2
call d%set_edges(6,[2,4]) !6 depends on 2 and 4
! toposort:
call d%toposort(order,istat)
! define some styles for the GraphViz output:
do i = 1, n_nodes
if (i==3 .or. i==6) then
call d%set_vertex_info(i,attributes='shape=square,fillcolor="SlateGray1",style=filled')
else
call d%set_vertex_info(i,attributes='shape=circle,fillcolor="cornsilk",style=filled')
end if
end do
! generate the GraphViz output:
call d%save_digraph('test.dot','RL',300)
call d%destroy()
call execute_command_line('dot -Tpdf -o test.pdf test.dot')
end program dag_example
```
git clone git@github.com:sourceryinstitute/dag
```

This program produces the toposort order:

Building and testing
--------------------
### Building and testing for serial execution
Build and test `dag` in a `bash`-like shell as follows:
```
order = [1, 2, 5, 3, 4, 6]
fpm test
```
### Building and testing for parallel execution
With [gfortran] and [OpenCoarrays] installed, build and test `dag` in a
`bash`-like shell as follows:
```
fpm test --compiler caf --runner "cafrun -n 2"
```
replacing `2` in the last line with the desired number of images to execute in parallel
for each test.

and the image file:

<img src="https://raw.githubusercontent.com/jacobwilliams/daglib/master/src/tests/dag_example.png" width="500">


### License
Please report any test failures by submitting an [issue] on the DAG repository.

This library is released under a [BSD-3 license](https://github.com/jacobwilliams/daglib/blob/master/LICENSE).
Examples
--------
The [example] subdirectory provides the following examples:

* [print-to-json.f90] - constructs and prints it a JSON representation the DAG.
Run this example and redirect the output to a file as follows:
```
fpm run --example print-to-json > dag.json
```
* [read-from-json.f90] - constructs a DAG by reading from a JSON file.
Run this example as follows:
```
fpm run --example read-from-json
```
which will read the file [./example/dag-dependencies.json]
* [dag-to-dot.f90] - constructs a DAG describing the module dependencies in
an early version of the Framework for Extensible Asynchronous
Task Scheduling ([FEATS]) and prints the DAG to a .dot file suitable for
conversion to a `.pdf` as follows:
```
fpm run --example dag-to-dot > feats-dag.dot
dot -Tpdf -o feats.pdf feats.dot
```
which should produce the following graph below:

![feats-dependencies](https://user-images.githubusercontent.com/13108868/133311851-721b7cda-1d10-4ee1-a51d-6169ca624839.png)

License
-------

This library is released under a [BSD-3 license].

Acknowledgments
----------------

| | |
|-|-|
| We gratefully acknowledge support from [NASA Langley Research Center] under contract number 80NSSC20P2246. | <img src="https://user-images.githubusercontent.com/13108868/112893191-304ce180-908f-11eb-8bea-e0cab5322aa8.png" alt="NASA logo" width="100">|

Donate
------
If you find this software useful, please consider donating code or
[currency](http://bit.ly/donate-to-sourcery-institute) to aid in development efforts.

[NASA Langley Research Center]: https://www.nasa.gov/langley
[latest release]: https://github.com/sourceryinstitute/dag/releases/latest

[daglib by Jacob Williams]: https://github.com/jacobwilliams/daglib
[GraphViz]: https://www.graphviz.org
[OpenCoarrays]: https://github.com/sourceryinstitute/opencoarrays
[CMake]: https://www.cmake.org
[gfortran]: https://gcc.gnu.org
[BSD-3 license]: https://github.com/sourceryinstitute/dag/blob/master/LICENSE
[fpm]: https://github.com/fortran-lang/fpm
[3276af]: https://github.com/everythingfunctional/fpm/commit/3276af2e000d1b2c90f151148cd01cce0d3e886d

[veggies]: https://gitlab.com/everythingfunctional/veggies
[GitHub Actions]: https://github.com/features/actions
[FORD]: https://github.com/Fortran-FOSS-Programmers/ford
[JSON]: https://www.json.org/json-en.html
[rojff]: https://gitlab.com/everythingfunctional/rojff
[./src/dag_test.f90]: ./src/dag_test.f90
[example]: ./example/
[FEATS]: https://github.com/sourceryinstitute/feats
[Assert]: https://github.com/sourceryinstitute/assert
[print-to-json.f90]: ./example/print-to-json.f90
[read-from-json.f90]:./example/read-from-json.f90
[dag-to-dot.f90]: ./example/dag-to-dot.f90
[documentation]: https://sourceryinstitute.github.io/dag/
[issue]: https://github.com/sourceryinstitute/dag/issues/new
[./example/dag-dependencies.json]: ./example/dag-dependencies.json
218 changes: 0 additions & 218 deletions daglib.fobis

This file was deleted.

19 changes: 0 additions & 19 deletions daglib.md

This file was deleted.

30 changes: 30 additions & 0 deletions doc-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
project: Directed Acyclic Graph Library
summary: A Fortran 2018 application programmer interface for representing directed acyclic graphs.
src_dir: src
output_dir: doc/html
preprocess: true
macro: FORD
preprocessor: gfortran -E
display: public
protected
private
source: true
graph: true
md_extensions: markdown.extensions.toc
coloured_edges: true
sort: permission-alpha
extra_mods: iso_fortran_env:https://gcc.gnu.org/onlinedocs/gfortran/ISO_005fFORTRAN_005fENV.html
iso_c_binding:https://gcc.gnu.org/onlinedocs/gfortran/ISO_005fC_005fBINDING.html#ISO_005fC_005fBINDING
project_github: https://github.com/sourceryinstitute/dag
author: Jacob Williams, Damian Rouson, Robert Singleterry, and Brad Richardson
print_creation_date: true
creation_date: %Y-%m-%d %H:%M %z
project_github: https://github.com/sourceryinstitute/dag
project_download: https://github.com/sourceryinstitute/releases
github: https://github.com/sourceryinstitute
predocmark_alt: >
predocmark: <
docmark_alt:
docmark: !

{!../README.md!}
Binary file added doc/OLTARIS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions doc/paper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: 'DAG: A Fortran package for directed acyclic graphs'
tags:
- Fortran
- directed acyclic graph
- parallel
- object-oriented programming
authors:
- name: Damian Rouson^[Corresponding author.]
orcid: 0000-0002-2344-868X
affiliation: 1
- name: Robert Singleterry
orcid: 0000-0002-5725-8825
affiliation: 2
- name: Brad Richardson
orcid: 0000-0002-3205-2169
affiliation: 3
affiliations:
- name: Lawrence Berkeley National Laboratory and Archaeologic Inc.
index: 1
- name: NASA Langley Research Center
index: 2
- name: Archaeologic Inc.
index: 3
date: 5 September 2021
bibliography: paper.bib

---

# Summary

A directed acyclic graph (DAG) comprises vertices connected by edges along which
no cyclic paths exist. DAG resulted from a refactoring and updating
[DAGLIB](https://github.com/jacobwilliams/daglib) to leverage the increased
modularity, explicit parallelism, purely functional capabilities of Fortran
2018.

# Statement of need

Our primary interest lies in parallel task scheduling for the On-Line Tool for
Assessment of Radiation in Space (OLTARIS, Figure \autoref{fig:oltaris})
[@singleterry2011oltaris]. We are publishing both the task scheduling framework
and DAG as open-source to make them available to the broader Fortran community
for use in any applications that require a DAG abstraction.

![OLTARIS web site.\label{fig:oltaris}](OLTARIS.png)


# Acknowledgements

We acknowledge contributions from Brigitta Sipocz, Syrtis Major, and Semyeong
Oh, and support from Kathryn Johnston during the genesis of this project.

# References
46 changes: 46 additions & 0 deletions example/dag-dependencies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"vertices" : [
{
"label" : "iso_varying_string",
"edges" : [
]
},
{
"label" : "assert_m ",
"edges" : [
]
},
{
"label" : "vertex_s ",
"edges" : [
4,
2
]
},
{
"label" : "vertex_m ",
"edges" : [
]
},
{
"label" : "dag_m ",
"edges" : [
4
]
},
{
"label" : "dag_s ",
"edges" : [
5,
2
]
},
{
"label" : "main ",
"edges" : [
5,
4
]
}
]
}
91 changes: 91 additions & 0 deletions example/dag-to-dot.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
program dag_to_dot
!! Demonstrate the construction of a complex DAG, the generation of a Graphviz
!! .dot file suitable for conversion to Portable Document Format (PDF). The
!! DAG in this example represents the module dependencies in an early version
!! of the Framework for Exensible Asynchronous Task Scheduling (FEATS).
!! See https://github.com/sourceryinstitute/feats.
!!
!! Compile and run this program by executing the following command
!! with your present working directory set to the top-level directory
!! in the DAG source tree:
!!
!! fpm run --example dag-to-dot > feats.dot
!! dot -Tpdf -o feats.pdf feats.dot
!!
!! where the latter command produces a PDF file if Graphviz has been installed.
!!
use dag_m, only : dag_t
use vertex_m, only : vertex_t
use iso_varying_string, only : var_str, varying_string
implicit none

character(len=*), parameter :: longest_name = "app_generator_m"
character(len=len(longest_name)), parameter :: names(*) = &
[character(len=len(longest_name)) :: &
"assert_m", "dag_m", "payload_m", "compile_m", "data_loc_map_m", "task_m", "task_item_m", "app_m", "app_generator_m" &
,"image_m", "main", "task_item_s", "compile_s", "app_generator_s", "data_loc_map_s", "payload_s", "app_s", "mailbox_m" &
,"image_s", "final_task_m", "final_task_s" &
]
associate( &
assert_m => findloc(names, "assert_m", dim=1) &
,dag_m => findloc(names, "dag_m", dim=1) &
,payload_m => findloc(names, "payload_m", dim=1) &
,compile_m => findloc(names, "compile_m", dim=1) &
,data_loc_map_m => findloc(names, "data_loc_map_m", dim=1) &
,task_m => findloc(names, "task_m", dim=1) &
,task_item_m => findloc(names, "task_item_m", dim=1) &
,app_m => findloc(names, "app_m", dim=1) &
,app_generator_m => findloc(names, "app_generator_m", dim=1) &
,image_m => findloc(names, "image_m", dim=1) &
,main => findloc(names, "main", dim=1) &
,task_item_s => findloc(names, "task_item_s", dim=1) &
,compile_s => findloc(names, "compile_s", dim=1) &
,app_generator_s => findloc(names, "app_generator_s", dim=1) &
,data_loc_map_s => findloc(names, "data_loc_map_s", dim=1) &
,payload_s => findloc(names, "payload_s", dim=1) &
,app_s => findloc(names, "app_s", dim=1) &
,mailbox_m => findloc(names, "mailbox_m", dim=1) &
,image_s => findloc(names, "image_s", dim=1) &
,final_task_m => findloc(names, "final_task_m", dim=1) &
,final_task_s => findloc(names, "final_task_s", dim=1) &
)

block
character(len=*), parameter :: external_ = 'shape=square,fillcolor="green",style=filled'
character(len=*), parameter :: root = 'shape=circle,fillcolor="white",style=filled'
character(len=*), parameter :: branch = 'shape=square,fillcolor="SlateGray1",style=filled'
character(len=len(branch)), parameter :: leaf = 'shape=circle,fillcolor="cornsilk",style=filled'
type(dag_t) feats
type(varying_string) name_string(size(names))

name_string = var_str(names)

feats = &
dag_t([ &
vertex_t([integer::], name_string(assert_m), var_str(external_)) &
,vertex_t([integer:: ], name_string(dag_m), var_str(external_)) &
,vertex_t([integer::], name_string(payload_m), var_str(leaf) ) &
,vertex_t([integer:: ], name_string(compile_m), var_str(leaf) ) &
,vertex_t([integer::], name_string(data_loc_map_m), var_str(leaf) ) &
,vertex_t([payload_m], name_string(task_m), var_str(branch) ) &
,vertex_t([task_m], name_string(task_item_m), var_str(leaf) ) &
,vertex_t([dag_m, task_item_m], name_string(app_m), var_str(branch) ) &
,vertex_t([app_m, dag_m, task_item_m, compile_m], name_string(app_generator_m), var_str(branch) ) &
,vertex_t([app_m, data_loc_map_m], name_string(image_m), var_str(branch) ) &
,vertex_t([app_generator_m, image_m], name_string(main), var_str(root) ) &
,vertex_t([task_item_m], name_string(task_item_s), var_str(root) ) &
,vertex_t([compile_m], name_string(compile_s), var_str(branch) ) &
,vertex_t([app_generator_m], name_string(app_generator_s), var_str(root) ) &
,vertex_t([data_loc_map_m], name_string(data_loc_map_s), var_str(root) ) &
,vertex_t([payload_m], name_string(payload_s), var_str(root) ) &
,vertex_t([app_m, assert_m], name_string(app_s), var_str(root) ) &
,vertex_t([payload_m], name_string(mailbox_m), var_str(branch) ) &
,vertex_t([image_m, mailbox_m], name_string(image_s), var_str(root) ) &
,vertex_t([data_loc_map_m, payload_m, task_m], name_string(final_task_m), var_str(branch) ) &
,vertex_t([final_task_m], name_string(final_task_s), var_str(root) ) &
])
print *, feats%graphviz_digraph()
end block
end associate

end program
48 changes: 48 additions & 0 deletions example/print-to-json.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
program print_to_json
!! Demonstrate writing a dag_t object in JSON format
!!
!! Compile and run run this program by executing the following
!! command from the top-level directory of the dag source tree:
!!
!! fpm run --example print-to-json
!!
use dag_m, only: dag_t
use vertex_m, only: vertex_t
use iso_varying_string, only : varying_string, var_str
implicit none

character(len=*), parameter :: longest_name = "iso_varying_string"
character(len=len(longest_name)), parameter :: names(*) = &
[character(len=len(longest_name)) :: "iso_varying_string", "assert_m", "vertex_s", "vertex_m", "dag_m", "dag_s", "main"]
type(varying_string) name_string(size(names))

name_string = var_str(names)

associate( &
iso_varying_string => findloc(names, "iso_varying_string", dim=1) &
,assert_m => findloc(names, "assert_m", dim=1) &
,vertex_s => findloc(names, "vertex_s", dim=1) &
,vertex_m => findloc(names, "vertex_m", dim=1) &
,dag_m => findloc(names, "dag_m", dim=1) &
,dag_s => findloc(names, "dag_s", dim=1) &
,main => findloc(names, "main", dim=1) &
)
block
type(dag_t) program_units

program_units = dag_t([ &
vertex_t( [integer::], name_string(iso_varying_string) ) &
,vertex_t( [integer::], name_string(assert_m) ) &
,vertex_t( [vertex_m, assert_m], name_string(vertex_s) ) &
,vertex_t( [integer::], name_string(vertex_m) ) &
,vertex_t( [vertex_m], name_string(dag_m) ) &
,vertex_t( [dag_m, assert_m], name_string(dag_s) ) &
,vertex_t( [dag_m, vertex_m], name_string(main) ) &
])

associate(json_object => program_units%to_json())
print *, json_object%to_expanded_string()
end associate
end block
end associate
end program
33 changes: 33 additions & 0 deletions example/read-from-json.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
program read_from_json
!! Demonstrate constructing a dag_t object from a JSON file
!!
!! Compile and run run this program by executing the following
!! command from the top-level directory of the dag source tree:
!!
!! fpm run --example read-from-json
!!
use dag_m, only: dag_t
use rojff, only : parse_json_from_file, json_object_t, fallible_json_value_t
use iso_varying_string, only : char
implicit none
type(fallible_json_value_t) parsed_json


parsed_json = parse_json_from_file('example/dag-dependencies.json')

if (parsed_json%failed()) then
error stop char(parsed_json%errors%to_string())
end if

block
type(dag_t) dag

select type (obj => parsed_json%value_)
type is (json_object_t)
dag = dag_t(obj)
class default
error stop "json wasn't an object: " // obj%to_compact_string()
end select
end block

end program
15 changes: 15 additions & 0 deletions fpm.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name = "dag"
version = "3.0.0"
license = "BSD"
author = ["Damian Rouson", "Robert Singleterry", "Brad Richardson"]
maintainer = "damian@sourceryinstitute.org"
copyright = "2020-2021 Sourcery Institute"

[dependencies]
rojff = { git = "https://gitlab.com/everythingfunctional/rojff", tag = "v1.0.0" }
erloff = { git = "https://gitlab.com/everythingfunctional/erloff", tag = "v2.2.0" }
iso_varying_string = { git = "https://gitlab.com/everythingfunctional/iso_varying_string", tag = "v3.0.4" }
assert = { git = "https://github.com/sourceryinstitute/assert", tag = "1.4.0" }

[dev-dependencies]
veggies = { git = "https://gitlab.com/everythingfunctional/veggies", tag = "v1.0.5" }
1 change: 1 addition & 0 deletions output/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*
7 changes: 7 additions & 0 deletions output/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Test Ouptut Directory
=====================
This directory exists to hold test output, which
can generally be considered temporary and removable.
This directory contains only
1. A `.gitignore` file that asks `git` to ignore all new files.
2. This README.md file that was force-added via `git add -f README.md`.
94 changes: 94 additions & 0 deletions src/dag_m.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
module dag_m
!! summary: A directed acyclic graph (DAG) abstraction.
!! author: Jacob Williams, Damian Rouson, Robert Singleterry, Brad Richardson
!! version: v1.0
!! date: 2020-Nov-30
!! license: Copyright (c) 2020, Sourcery Institute, BSD 3-clause license Copyright (c) 2018 Jacob Williams
use vertex_m, only : vertex_t
use rojff, only : json_object_t

implicit none

private

type,public :: dag_t
!! Encapsulate a graph as an array of vertices, each storing dependency information
private
type(vertex_t), allocatable :: vertices(:)
integer, allocatable :: order(:)
contains
procedure :: is_sorted_and_acyclic
procedure :: to_json
procedure :: num_vertices
procedure :: dependencies_for
procedure :: depends_on
procedure :: graphviz_digraph
end type

interface dag_t

module function construct_from_json(json_object) result(dag)
!! Construct a dag_t object from a JSON object (result contains a topologically sorted index array)
implicit none
type(json_object_t), intent(in) :: json_object
type(dag_t) dag
end function

pure module function construct_from_components(vertices) result(dag)
!! Construct a dag_t object from an array of (unsorted) vertex_t objects (result contains a topologically sorted index array)
implicit none
type(vertex_t), intent(in) :: vertices(:)
type(dag_t) dag
end function

end interface

interface

elemental module function is_sorted_and_acyclic(self)
!! Result is true if dag%order contains a topological sorting of vertex identifiers
implicit none
class(dag_t), intent(in) :: self
logical is_sorted_and_acyclic
end function

module function to_json(self) result(json_object)
!! Result is a JSON representation of the dag_t object
implicit none
class(dag_t), intent(in) :: self
type(json_object_t) json_object
end function

elemental module function num_vertices(self)
!! Result is the size of the vertex array
implicit none
class(dag_t), intent(in) :: self
integer num_vertices
end function

pure module function depends_on(self, vertex_num) result(dependencies)
!! Result is an array of the vertex numbers that depend on on vertex vertex_num
implicit none
class(dag_t), intent(in) :: self
integer, intent(in) :: vertex_num
integer, allocatable :: dependencies(:)
end function

pure module function dependencies_for(self, vertex_id) result(dependency_ids)
!! Result is an array of the ids on which vertex_id depends
implicit none
class(dag_t), intent(in) :: self
integer, intent(in) :: vertex_id
integer, allocatable :: dependency_ids(:)
end function

pure module function graphviz_digraph(self) result(digraph)
!! Result contains a Graphviz .dot file descriptoin of the dag_t object
implicit none
class(dag_t),intent(in) :: self
character(len=:), allocatable :: digraph
end function

end interface

end module dag_m
444 changes: 0 additions & 444 deletions src/dag_module.f90

This file was deleted.

237 changes: 237 additions & 0 deletions src/dag_s.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
submodule(dag_m) dag_s
use assert_m, only : assert
use rojff, only: &
fallible_json_member_t, &
fallible_json_object_t, &
fallible_json_string_t, &
fallible_json_value_t, &
json_array_t, &
json_element_t
use iso_fortran_env, only: iostat_end
use iso_varying_string, only : operator(//), char
use intrinsic_array_m, only : intrinsic_array_t

implicit none

type searched_and_ordered_t
integer, allocatable, dimension(:) :: s, o
end type

contains

module procedure construct_from_components
dag%vertices = vertices
dag%order = topological_sort(dag)
call assert(dag%is_sorted_and_acyclic(), "construct_from_components: dag%is_sorted_and_acyclic()")
end procedure

pure function topological_sort(dag) result(order)
!! Provide array of vertex numbers ordered in a way that respects dependencies
type(dag_t), intent(in) :: dag
integer, allocatable :: order(:)

call assert(all(dag%vertices(:)%edges_allocated()), "dag_s topological_sort: all(dag%vertices(:)%edges_allocated())")

block
type(searched_and_ordered_t) searched_and_ordered
integer v

searched_and_ordered = searched_and_ordered_t(s = [integer::], o = [integer::])

do v = 1, size(dag%vertices)
if (.not. any(v == searched_and_ordered%s)) &
searched_and_ordered = depth_first_search(v, [integer::], searched_and_ordered%o)
end do
order = searched_and_ordered%o
end block

contains

pure recursive function depth_first_search(v, d, o) result(hybrid)
integer, intent(in) :: v
integer, intent(in), dimension(:) :: d, o
type(searched_and_ordered_t) hybrid

call assert(.not. any(v == d), "depth_first_search: cycle detected", intrinsic_array_t([v,d]))

hybrid = searched_and_ordered_t(s = [integer::], o = o)

associate(dependencies => dag%depends_on(v))
block
integer w
do w = 1, size(dependencies)
associate(w_dependencies => dependencies(w))
if (.not. any(w_dependencies == hybrid%s)) hybrid = depth_first_search(w_dependencies, [d, v], hybrid%o)
end associate
end do
end block
end associate

if (.not. any(v == hybrid%o)) hybrid%o = [v, hybrid%o]
hybrid = searched_and_ordered_t(s = [v, hybrid%s], o = hybrid%o)

end function

end function topological_sort

module procedure is_sorted_and_acyclic

if (.not. allocated(self%order)) then
is_sorted_and_acyclic = .false.
return
end if

associate(num_vertices => size(self%vertices), order_size => size(self%order))
call assert(order_size == num_vertices, "dag_t%is_sorted_and_acyclic: size(self%vertices) == size(self%order)", &
intrinsic_array_t([order_size, num_vertices]))

block
integer i, j

do i = 1, num_vertices
associate(edges => self%vertices(self%order(i))%edges())
do j = 1, size(edges)
if (.not. any(edges(j) == self%order(1:i))) then
is_sorted_and_acyclic = .false.
return
end if
end do
end associate
end do

is_sorted_and_acyclic = .true.
end block

end associate

end procedure

module procedure construct_from_json
type(fallible_json_value_t) :: maybe_vertices

maybe_vertices = json_object%get("vertices")
call assert( &
.not. maybe_vertices%errors%has_any(), &
"dag_s construct_from_json: .not. errors%has_any()", &
char(maybe_vertices%errors%to_string()))

select type (vertices => maybe_vertices%value_)
type is (json_array_t)
dag%vertices = vertex_t(vertices%elements)

class default
call assert(.false., "dag%from_json: vertices was not an array", vertices%to_compact_string())
end select
dag%order = topological_sort(dag)
end procedure

module procedure to_json
type(fallible_json_object_t) maybe_result

maybe_result = fallible_json_object_t( &
[ fallible_json_member_t("vertices", json_array_t(json_element_t(self%vertices%to_json()))) &
])
call assert(.not. maybe_result%errors%has_any(), "dag%to_json: .not. errors%has_any()", char(maybe_result%errors%to_string()))
json_object = maybe_result%object
end procedure

module procedure num_vertices
num_vertices = size(self%vertices)
end procedure

module procedure dependencies_for
dependency_ids = self%vertices(vertex_id)%edges()
end procedure

module procedure depends_on

call assert(vertex_num>=1 .and. vertex_num<=size(self%vertices), "depends_on: index in bounds")

allocate(dependencies(0))

block
integer v

do v = 1, size(self%vertices)
if (any(self%vertices(v)%edges() == vertex_num)) dependencies = [dependencies, v]
end do
end block

end procedure

module procedure graphviz_digraph

integer istat

digraph = generate_digraph(self)

contains

elemental function integer_to_string(i) result(s)
integer,intent(in) :: i
integer, parameter :: max_number_width = 64
character(len=max_number_width) :: s
integer :: istat

write(s,fmt='(ss,I0)',iostat=istat) i
if (istat==0) then
s = trim(adjustl(s))
else
s = '***'
end if
end function integer_to_string

pure function generate_digraph(self) result(str)
!! - Result is the string to write out to a *.dot file. (Called by save_digraph())
implicit none
class(dag_t),intent(in) :: self
character(len=:),allocatable :: str

character(len=*), parameter :: rankdir = 'RL'
!! - Rank Direction which are applicable inputs to the -rankdir option on the digraph command
integer, parameter :: dpi=300
!! - dots per inch

integer :: i,j
integer :: n_edges
character(len=:),allocatable :: attributes, label

character(len=*),parameter :: tab = ' '
character(len=*),parameter :: newline = new_line(' ')


call assert(all(self%vertices(:)%edges_allocated()), "generate_digraph: self%edges_allocated()")

str = 'digraph G {'//newline//newline
str = str//tab//'rankdir='//rankdir//newline//newline
str = str//tab//'graph [ dpi = '//integer_to_string(dpi)//' ]'//newline//newline

! define the vertices:
do i=1,size(self%vertices)
label = char('label="'//self%vertices(i)%label()//'"')
attributes = char('['//self%vertices(i)%attributes()//','//label//']')
str = str//tab//integer_to_string(i)//' '//attributes//newline
if (i==size(self%vertices)) str = str//newline
end do

! define the dependencies:
do i=1,size(self%vertices)
n_edges = size(self%vertices(i)%edges())
str = str//tab//integer_to_string(i)//merge(' -> ',' ',n_edges/=0)
do j=1,n_edges
! comma-separated list:
associate(edges => self%vertices(i)%edges())
str = str//integer_to_string(edges(j))
if (n_edges>1 .and. j<n_edges) str = str//','
end associate
end do
str = str//';'//newline
end do

str = str//newline//'}'

end function generate_digraph

end procedure

end submodule dag_s
70 changes: 0 additions & 70 deletions src/tests/dag_example.f90

This file was deleted.

Binary file removed src/tests/dag_example.png
Binary file not shown.
102 changes: 102 additions & 0 deletions src/vertex_m.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
module vertex_m
!! summary: Represent one node in a directed acyclic graph.
!! author: Jacob Williams, Damian Rouson, Robert Singleterry, Brad Richardson
!! version: v1.0
!! date: 2020-Nov-30
!! license: Copyright (c) 2020-2021, Sourcery Institute, BSD 3-clause license Copyright (c) 2018 Jacob Williams
use rojff, only : json_element_t, json_object_t, json_value_t
use iso_varying_string, only : varying_string

implicit none

private
public :: vertex_t

type vertex_t
!! Encapsulate a node in a graph comprised of vertices connected by dependencies (edges)
private
integer, allocatable :: edges_(:)
type(varying_string) :: label_
type(varying_string) :: attributes_
contains
procedure :: to_json
procedure :: edges
procedure :: label
procedure :: attributes
procedure :: edges_allocated
end type

interface vertex_t

impure elemental module function from_json_element(json_element) result(vertex)
!! Construct a scalar array vertex_t or an array of objects from a JSON representation of a vertex or vertices
implicit none
type(json_element_t), intent(in) :: json_element
type(vertex_t) :: vertex
end function

module function from_json_value(json_value) result(vertex)
!! Construct a single vertex_t object from a JSON representation of a vertex
implicit none
class(json_value_t), intent(in) :: json_value
type(vertex_t) :: vertex
end function

pure module function construct_from_components(edges, label, attributes) result(vertex)
!! Component-wise constructor of a vertex_t object
implicit none
integer, intent(in) :: edges(:) !! vertices on which this vertex depends
type(varying_string), intent(in) :: label !! vertex description (e.g., name)
type(varying_string), intent(in), optional :: attributes !! Graphvizl .dot symbol description
type(vertex_t) vertex
end function

end interface

interface

elemental module function edges_allocated(self) result(edges_array_allocated)
!! Result is .true. iff the edges component is allocated
implicit none
class(vertex_t), intent(in) :: self
logical edges_array_allocated
end function

module function from_json_object(json_object) result(vertex)
!! construct a vertexa_t object from a jsonff JSON object
implicit none
type(json_object_t), intent(in) :: json_object
type(vertex_t) :: vertex
end function

impure elemental module function to_json(self) result(json_object)
!! Result is a JSON representation of self
implicit none
class(vertex_t), intent(in) :: self
type(json_object_t) :: json_object
end function

pure module function edges(self) result(my_edges)
!! Result is the array element numbers of the vertices on which this vertex depends
implicit none
class(vertex_t), intent(in) :: self
integer :: my_edges(size(self%edges_))
end function

elemental module function label(self) result(my_label)
!! Vertex label getter
implicit none
class(vertex_t), intent(in) :: self
type(varying_string) my_label
end function

elemental module function attributes(self) result(my_attributes)
!! Vertex attributes getter
implicit none
class(vertex_t), intent(in) :: self
type(varying_string) my_attributes
end function

end interface

end module vertex_m
104 changes: 104 additions & 0 deletions src/vertex_s.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
submodule(vertex_m) vertex_s
use rojff, only : &
fallible_json_member_t, &
fallible_json_object_t, &
fallible_json_string_t, &
fallible_json_value_t, &
json_array_t, &
json_number_t, &
json_string_t, &
json_integer_t
use iso_varying_string, only : assignment(=), char
use assert_m, only : assert
implicit none

contains

module procedure edges_allocated
edges_array_allocated = allocated(self%edges_)
end procedure

module procedure to_json
type(fallible_json_object_t) :: maybe_result

maybe_result = fallible_json_object_t( &
[ fallible_json_member_t("label", fallible_json_value_t(fallible_json_string_t(self%label_))) &
, fallible_json_member_t("edges", json_array_t(json_element_t(json_integer_t(self%edges_)))) &
])
call assert( &
.not. maybe_result%errors%has_any(), &
"vertex%to_json: .not. errors%has_any()", &
char( maybe_result%errors%to_string()))
json_object = maybe_result%object
end procedure

module procedure construct_from_components

character(len=*), parameter :: &
branch = 'shape=square, fillcolor="SlateGray1", style=filled' &
,external_ = 'shape=square, fillcolor="green", style=filled' &
,root = 'shape=circle, fillcolor="white", style=filled' &
,leaf = 'shape=circle, fillcolor="cornsilk", style=filled'

vertex%edges_ = edges
vertex%label_ = label
if (present(attributes)) then
vertex%attributes_ = attributes
else
vertex%attributes_ = branch
end if
end procedure

module procedure from_json_element
vertex = vertex_t(json_element%json)
end procedure

module procedure from_json_value
select type (json_value)
type is (json_object_t)
vertex = from_json_object(json_value)
class default
call assert(.false., "vertex%from_json_value: vertex was not an object", json_value%to_compact_string())
end select
end procedure

module procedure from_json_object
type(fallible_json_value_t) :: maybe_edges
integer :: i

maybe_edges = json_object%get("edges")
call assert( &
.not. maybe_edges%errors%has_any(), &
"vertex%from_json: .not. errors%has_any()", &
char(maybe_edges%errors%to_string()))
select type (edges => maybe_edges%value_)
type is (json_array_t)
allocate(vertex%edges_(size(edges%elements)))
do i = 1, size(edges%elements)
select type (edge => edges%elements(i)%json)
type is (json_number_t)
vertex%edges_(i) = int(edge%number)
type is (json_integer_t)
vertex%edges_(i) = edge%number
class default
call assert(.false., "vertex%from_json: edge was not a number", edge%to_compact_string())
end select
end do
class default
call assert(.false., "vertex%from_json: edges was not an array", edges%to_compact_string())
end select
end procedure

module procedure edges
my_edges = self%edges_
end procedure

module procedure label
my_label = self%label_
end procedure

module procedure attributes
my_attributes = self%attributes_
end procedure

end submodule vertex_s
155 changes: 155 additions & 0 deletions test/dag_test.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
module dag_test
use dag_m, only: dag_t
use erloff, only: error_list_t
use vertex_m, only: vertex_t
use veggies, only: &
result_t, test_item_t, assert_equals, describe, it, assert_that, fail, succeed
use iso_varying_string, only : varying_string, var_str, assignment(=)
use rojff, only: fallible_json_value_t, json_object_t, parse_json_from_string
!! Test DAG construction, input, and output.
implicit none
private
public :: test_dag_construction

contains

function test_dag_construction() result(tests)
type(test_item_t) :: tests

tests = describe("dag's module dependency graph", &
[ it("can be constructed, output to .dot file, and converted to a PDF", construct_dag_and_write_pdf) &
, it("is topologically sorted when constructed from components", component_constructor_sorts) &
, it("is topologically sorted when constructed from a JSON object", json_constructor_sorts) &
, it("can be constructed and converted to a JSON object", construct_dag_and_json_object) &
])

end function

function module_tree_from_components() result(dag_modules)
type(dag_t) dag_modules

enum, bind(C)
enumerator :: assert_m=1, vertex_s, vertex_m, dag_m, dag_s
end enum

integer, parameter :: module_id(*) = [assert_m, vertex_s, vertex_m, dag_m, dag_s]
type(varying_string) :: names(size(module_id))

names(assert_m) = "assert_m"
names(vertex_m) = "vertex_m"
names(vertex_s) = "vertex_s"
names(dag_m) = "dag_m"
names(dag_s) = "dag_s"

block
character(len=*), parameter :: &
branch = 'shape=square, fillcolor="SlateGray1", style=filled' &
,external_ = 'shape=square, fillcolor="green", style=filled' &
,root = 'shape=circle, fillcolor="white", style=filled' &
,leaf = 'shape=circle, fillcolor="cornsilk", style=filled'

dag_modules = dag_t([ &
vertex_t([integer::], names(assert_m), var_str(external_)) &
,vertex_t([vertex_m, assert_m], names(vertex_s), var_str(root)) &
,vertex_t([integer::], names(vertex_m), var_str(leaf)) &
,vertex_t([vertex_m], names(dag_m), var_str(branch)) &
,vertex_t([dag_m, assert_m], names(dag_s), var_str(root))])
end block

end function

function construct_dag_and_write_pdf() result(result_)
type(result_t) result_

character(len=*), parameter :: dot_file_minus_whitespace = &
'digraph' // &
'G' // &
'{rankdir=RL' // &
'graph[dpi=300]' // &
'1[shape=square,fillcolor="green",style=filled,label="assert_m"]' // &
'2[shape=circle,fillcolor="white",style=filled,label="vertex_s"]' // &
'3[shape=circle,fillcolor="cornsilk",style=filled,label="vertex_m"]' // &
'4[shape=square,fillcolor="SlateGray1",style=filled,label="dag_m"]' // &
'5[shape=circle,fillcolor="white",style=filled,label="dag_s"]' // &
'1;' // &
'2->3,1;' // &
'3;' // &
'4->3;' // &
'5->4,1;' // &
'}'

block
type(dag_t) modules

modules = module_tree_from_components()
result_ = assert_equals(dot_file_minus_whitespace, remove_whitespace(modules%graphviz_digraph()))
end block

contains

pure function remove_whitespace(string) result(no_blanks)
character(len=*), intent(in) :: string
character(len=:), allocatable :: no_blanks
integer i

no_blanks = ''
do i=1, len(string)
if (.not. any(string(i:i) == [' ', new_line(' ')])) no_blanks = no_blanks // string(i:i)
end do
end function

end function

function construct_dag_and_json_object() result(result_)
type(result_t) result_
type(json_object_t) json_object
character(len=*), parameter :: expected_json = &
'{"vertices":[' // &
'{"label":"assert_m","edges":[]},' // &
'{"label":"vertex_s","edges":[3,1]},' // &
'{"label":"vertex_m","edges":[]},' // &
'{"label":"dag_m","edges":[3]},' // &
'{"label":"dag_s","edges":[4,1]}]}'
type(dag_t) dag

dag = module_tree_from_components()
json_object = dag%to_json()
result_ = assert_equals(expected_json, json_object%to_compact_string())

end function

function component_constructor_sorts() result(result_)
type(result_t) result_

type(dag_t) dag
dag = module_tree_from_components()
result_ = assert_that(dag%is_sorted_and_acyclic())
end function

function json_constructor_sorts() result(result_)
type(result_t) result_
type(dag_t) dag
type(fallible_json_value_t) :: maybe_json
character(len=*), parameter :: json_string = &
'{"vertices":[' // &
'{"label":"assert_m","edges":[]},' // &
'{"label":"vertex_s","edges":[3,1]},' // &
'{"label":"vertex_m","edges":[]},' // &
'{"label":"dag_m","edges":[3]},' // &
'{"label":"dag_s","edges":[4,1]}]}'

maybe_json = parse_json_from_string(json_string)
if (.not.maybe_json%failed()) then
select type (json => maybe_json%value_)
type is (json_object_t)
dag = dag_t(json)
result_ = assert_that(dag%is_sorted_and_acyclic())
class default
result_ = fail("json wasn't an object")
end select
else
result_ = fail(maybe_json%errors%to_string())
end if
end function

end module dag_test
27 changes: 27 additions & 0 deletions test/main.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
! Generated by cart. DO NOT EDIT
program main
implicit none

if (.not.run()) stop 1
contains
function run() result(passed)
use dag_test, only: &
dag_dag_construction => &
test_dag_construction
use veggies, only: test_item_t, test_that, run_tests



logical :: passed

type(test_item_t) :: tests
type(test_item_t) :: individual_tests(1)

individual_tests(1) = dag_dag_construction()
tests = test_that(individual_tests)


passed = run_tests(tests)

end function
end program