Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .clangd → .clangd.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
# Pin clangd to the tree that has both the library and the tests.
CompileFlags:
CompilationDatabase: build
Add:
- -I@CMAKE_CURRENT_SOURCE_DIR@/include
20 changes: 18 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ jobs:
style:
runs-on: ubuntu-latest

env:
LLVM_VERSION: "21"

steps:
- uses: actions/checkout@v4

Expand All @@ -25,7 +28,18 @@ jobs:
- name: Install clang-tidy and Ninja
run: |
sudo apt-get update
sudo apt-get install -y clang-tidy ninja-build
sudo apt-get install -y ninja-build lsb-release wget software-properties-common gnupg
wget -q https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh "$LLVM_VERSION"
sudo apt-get install -y "clang-tidy-$LLVM_VERSION"

- name: Report toolchain
run: |
"clang++-$LLVM_VERSION" --version
"clang-tidy-$LLVM_VERSION" --version
"clang++-$LLVM_VERSION" -std=c++23 -x c++ -E -dM /dev/null \
| grep -E '__cplusplus|__cpp_concepts'

- name: Install pre-commit
run: pip install pre-commit
Expand All @@ -42,8 +56,10 @@ jobs:
cmake -B build-lint -S . \
-G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_COMPILER="clang++-$LLVM_VERSION" \
-DSTGX_BUILD_TESTS=OFF \
-DSTGX_ENABLE_CLANG_TIDY=ON
-DSTGX_ENABLE_CLANG_TIDY=ON \
-DSTGX_CLANG_TIDY="clang-tidy-$LLVM_VERSION"

- name: Build with clang-tidy
run: cmake --build build-lint -j "$(nproc)"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ out/

# clangd
.cache/
.clangd
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
exclude: ^tests/data/invalid_bad_yaml\.yaml$

- id: check-added-large-files
- id: check-merge-conflict
- id: mixed-line-ending
Expand Down
27 changes: 19 additions & 8 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
cmake_minimum_required(VERSION 3.20)
cmake_minimum_required(VERSION 3.21)

project(libstgx
VERSION 0.1.0
DESCRIPTION "State Transition Graph eXtended — build, edit, validate state graphs"
LANGUAGES CXX
)

include(cmake/ProjectSettings.cmake)
# Developer conveniences
if(PROJECT_IS_TOP_LEVEL)
include(cmake/ProjectSettings.cmake)
endif()

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

option(STGX_BUILD_TESTS "Build unit tests" ON)
option(STGX_BUILD_TESTS "Build unit tests" ${PROJECT_IS_TOP_LEVEL})
option(STGX_BUILD_EXAMPLES "Build examples" OFF)
option(STGX_ENABLE_CLANG_TIDY "Enable clang-tidy on stgx targets" OFF)
set(STGX_CLANG_TIDY "" CACHE STRING "clang-tidy executable to use (name or full path)")

# --- dependencies -----------------------------------------------------------
# --- dependencies

add_subdirectory(thirdparty)

# --- library ----------------------------------------------------------------
# --- library

file(GLOB_RECURSE libstg_sources
CONFIGURE_DEPENDS
Expand All @@ -44,9 +48,16 @@ target_link_libraries(stgx
target_compile_features(stgx PUBLIC cxx_std_23)
target_compile_options(stgx PRIVATE -Wall -Wextra -Wpedantic)

# Carried on the target rather than picked up from ProjectSettings.cmake, so it
# also holds when a consumer links stgx into a shared library.
set_target_properties(stgx PROPERTIES POSITION_INDEPENDENT_CODE ON)

if(STGX_ENABLE_CLANG_TIDY)
find_program(CLANG_TIDY_EXE
NAMES clang-tidy
NAMES
${STGX_CLANG_TIDY}
clang-tidy-22 clang-tidy-21 clang-tidy-20 clang-tidy-19
clang-tidy
HINTS
"$ENV{HOMEBREW_PREFIX}/opt/llvm/bin"
/opt/homebrew/opt/llvm/bin
Expand All @@ -59,14 +70,14 @@ if(STGX_ENABLE_CLANG_TIDY)
endif()


# --- examples ---------------------------------------------------------------
# --- examples

if(STGX_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()


# --- tests ------------------------------------------------------------------
# --- tests

if(STGX_BUILD_TESTS)
enable_testing()
Expand Down
199 changes: 199 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<div align="center">

<img src="assets/libstgx-banner.png" alt="libstgx" width="620">

[![CI](https://github.com/peplxx/libstgx/actions/workflows/ci.yml/badge.svg)](https://github.com/peplxx/libstgx/actions/workflows/ci.yml)
![C++23](https://img.shields.io/badge/C%2B%2B-23-00599C?logo=cplusplus&logoColor=white)
![CMake 3.21+](https://img.shields.io/badge/CMake-3.21%2B-064F8C?logo=cmake&logoColor=white)
![version](https://img.shields.io/badge/version-0.1.0-blue)

</div>

**S**tate **T**ransition **G**raph e**X**tended — a C++ library for building, editing and
validating state-transition graphs of schedules for real-time systems.

---

libstgx reads and writes the YAML that
[state-graph-visualizer](https://github.com/peplxx/state-graph-visualizer) renders, and is the
source of truth for that format — it validates stricter than the viewer does.


## Add to project

```cmake
include(FetchContent)

FetchContent_Declare(
libstgx
GIT_REPOSITORY https://github.com/peplxx/libstgx.git
GIT_TAG v0.1.0
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(libstgx)

target_link_libraries(my_app PRIVATE stgx::stgx)
```

**As a subdirectory** — submodule or vendored copy:

```cmake
add_subdirectory(libstgx)
target_link_libraries(my_app PRIVATE stgx::stgx)
```

There is no `install()` / `find_package(stgx)` yet — both routes above go through
`add_subdirectory`.

## Building the library itself

```bash
cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure
```

CMake 3.21+ and a C++23 compiler (Apple Clang 17, GCC 13, Clang 17). Catch2 v3 is used for tests —
taken from the system if present, otherwise fetched.

| Option | Default | |
|---|---|---|
| `STGX_BUILD_TESTS` | on for a top-level build, off as a subproject | build the Catch2 test binary |
| `STGX_BUILD_EXAMPLES` | `OFF` | build the two example programs |
| `STGX_ENABLE_CLANG_TIDY` | `OFF` | run clang-tidy over the library sources |

## Graphs and the visualizer format

Everything below is the half of libstgx that faces
[state-graph-visualizer](https://github.com/peplxx/state-graph-visualizer): a model of the picture
— states, transitions, styling — and the YAML document the viewer reads.

<details>
<summary><b>The API and the wire format</b></summary>

<br>

### Usage

```cpp
#include <print>

#include <stgx/stgx.hpp>

int main() {
auto graph = stgx::GraphBuilder()
.system([](auto& s) { s.description("EDF").m(1).task(2, 5, "τ₁").task(3, 7, "τ₂"); })
.node("n00", [](auto& n) { n.initial().task(0, 0).task(0, 0).to("n10").to("n20"); })
.node("n10", [](auto& n) { n.task(2, 5, stgx::Release::Up).task(0, 0).to("n20"); })
.node("n20", [](auto& n) { n.task(0, 3, stgx::Release::Down).task(0, 0).loop_to("n00"); })
.build();

if (!graph) {
std::println("{}", graph.error().to_string());
return 1;
}
if (auto saved = stgx::save_yaml_file(*graph, "graph.yaml"); !saved) {
std::println("{}", saved.error().to_string());
return 1;
}
}
```
> Graph is not checked until `build()`, which then reports everything that is wrong at once


```cpp
stgx::Diagnostics warnings;
auto graph = stgx::load_yaml_file("graph.yaml", &warnings);
if (!graph) return std::println("{}", graph.error().to_string()), 1;
if (!warnings.empty()) std::println("{}", warnings.to_string()); // fields we drop when saving
```
> Reading one back. Warnings cannot ride along in `expected`'s success case, so loading takes an
optional `Diagnostics*` for them

## Model

| Type | Holds |
|---|---|
| `System` | `description`, `m` (processor count), `tasks` — the `(c, d)` parameters, one per task |
| `Node` | `id`, `initial`, `label`, `border_color`, `fill_color`, `tasks` — per-task `(c, d, release)` state |
| `Edge` | `source`, `target`, `kind`: `Transition` (`children`) or `Release` (`loopback`) |

`Graph` owns nodes and edges and keeps them indexed: `find_node`, `has_node`, `initial_node`, and
`out_edges(id)` — which returns a `std::span` into the edge storage, no copies. Edges stay grouped
by source, so every mutation re-groups them in O(V+E); deliberate, and fine at the sizes these
graphs come in.

<details>
<summary><b>Mutations</b> — each one keeps the graph referentially sound</summary>

`add_node`, `remove_node`, `rename_node`, `add_edge`, `remove_edge`, `set_initial`,
`set_node_tasks`, `set_node_label`, `set_node_border_color`, `set_node_fill_color`.

Removing a node takes every edge touching it with it; renaming one repoints them. A dangling edge
cannot be produced through this API.

</details>

<details>
<summary><b>Diagnostic codes</b> — <code>{code, severity, path, message}</code></summary>

| Code | Meaning |
|---|---|
| `yaml_parse` | the bytes are not YAML, or the document is empty |
| `wrong_type` | a value has the wrong YAML shape (mapping expected, etc.) |
| `schema_version` | `schemaVersion` is not a version this build reads |
| `nodes_missing` | `nodes` is absent or empty |
| `node_id_empty` / `node_id_duplicate` | node ids must be present and unique |
| `dangling_edge` | an edge names a node that does not exist |
| `initial_count` | `initial` must be set on exactly one node |
| `task_arity_mismatch` | a node has a different number of tasks than `system.tasks` |
| `not_an_int` | `c`, `d`, `m`, `schemaVersion` must be plain integers — `2.0` and `2e1` are not |
| `bad_release` | `release` is not `up` / `down` / `none` (or `↑` / `↓` / `1` / `-1`) |
| `unsupported_field` | recognised but not modelled yet; dropped on save *(warning)* |
| `unknown_field` | not part of the wire format *(warning)* |
| `io_error` | a file could not be opened, read or written |

One `load_yaml_file` on a badly broken document reports the bad integer *and* the dangling edge
*and* the missing initial node.

</details>

## Wire format

```yaml
schemaVersion: 1
system:
description: EDF feasibility graph
m: 1
tasks:
- {c: 2, d: 5, name: τ₁}
- {c: 3, d: 7, name: τ₂}
nodes:
- id: n00
initial: true
tasks:
- {c: 0, d: 0}
- {c: 0, d: 0}
children: [n10, n20]
- id: n10
tasks:
- {c: 2, d: 5, release: up}
- {c: 0, d: 0}
children: [n20]
- id: n20
label: "custom: label"
borderColor: "#C0392B"
fillColor: "#F9D5D3"
tasks:
- {c: 0, d: 3, release: down}
- {c: 0, d: 0}
loopback: [n00]
```

**On read** — `schemaVersion` may be omitted (assumed 1); `isInitial` is accepted as a legacy
spelling of `initial`; `release` accepts `↑` / `1` for up and `↓` / `-1` for down; absent fields
take their defaults.

**On write** — key order is as above, and defaults are omitted: `initial: false`, empty `tasks` /
`children` / `loopback`, `release: none`, absent `label` and colours. Colours are passed through
verbatim, any CSS value the viewer accepts.
Binary file added assets/libstgx-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions cmake/ProjectSettings.cmake
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Developer-only settings for a top-level libstgx build. Every one of these
# reaches outside the project's own scope — the source tree, the cache, global
# properties — so the caller only includes this file when PROJECT_IS_TOP_LEVEL.

configure_file(
"${PROJECT_SOURCE_DIR}/.clangd.in"
"${PROJECT_SOURCE_DIR}/.clangd"
@ONLY
)

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
message(STATUS "Setting build type to 'RelWithDebInfo' as none was specified.")
set(CMAKE_BUILD_TYPE
Expand Down
Loading
Loading