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
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
edge it produces (or `end`), fan-out as siblings, and each merge under its
(named) group once its inputs have been shown. `AsciiStyle::plain` (the
default) draws with ASCII, `AsciiStyle::unicode` with box-drawing characters.
- **Parameters** (`GraphParameters.hpp`) — a stage argument can name a
parameter, `Truncate(width=$text.width)`, whose value comes from a parameter
file (a JSON object) that several graphs share. A graph file names its
parameter files with `params "tuning.json"` lines, relative to the graph
file; later files override earlier ones member by member.
**`dsl::loadGraphProgram(path, overrides)`** reads a graph file with its
parameter files and binds them; **`dsl::parseGraphProgram(text,
parameters)`**, **`dsl::bindParameters`** and **`dsl::loadParameters`** bind
values the caller supplies. Dots walk into nested objects, and a parameter
can hold any JSON value, so nested objects and lists can reach a stage from
the DSL. An unknown name is a located diagnostic with a suggestion; a
parameter file that cannot be read is one at its `params` line; parameters a
graph does not use are not reported. A graph built from a program whose
parameters were never bound reports each as not set, and the renderings show
unbound parameters as `$name`. New sample: `apps/tunedPipeline`.
- `validateDslGraph` also takes a parsed `dsl::GraphProgram`.

### Changed
- A `.<key>` on anything but `in` and `out` (e.g. `msg.x`) is now a diagnostic;
Expand Down Expand Up @@ -153,7 +169,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The hand-written DSL parser. It moved to `GraphLangHandwritten.hpp` as
`dsl::parseGraphProgramHandwritten`, marked `[[deprecated]]`, and will be
removed in a future release. It produces the same results as
`dsl::parseGraphProgram`; the tests check the two against each other.
`dsl::parseGraphProgram`, except that it does not support parameters; the
tests check the two against each other.

## [0.2.0] - 2026-09-15

Expand Down
144 changes: 142 additions & 2 deletions EXAMPLE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# filterGraph by example

This walkthrough follows four runnable samples under [`apps/`](apps/), all of
This walkthrough follows five runnable samples under [`apps/`](apps/), all of
them small text pipelines:

| Sections | Sample | Covers |
Expand All @@ -9,8 +9,9 @@ them small text pipelines:
| 8 | [`statefulPipeline`](apps/statefulPipeline/main.cpp) | stages that **carry state**: `finish()`, the `GraphContext`, a merge with per-instance state |
| 9 | [`compositePipeline`](apps/compositePipeline/main.cpp) | **composition**: `JoinFilter`, a graph nested as a stage, a `Void` sink, in-band ticks |
| 10 | [`namedPipeline`](apps/namedPipeline/main.cpp) | **names in the wiring**: named merge slots, several named graph inputs (also read directly by a merge), and the checks both make possible |
| 11 | [`tunedPipeline`](apps/tunedPipeline/main.cpp) | **parameters**: graph files that share their tuning values through a parameter file, one-off overrides, and the checks for misspelled names |

Start at the top: sections 8–10 assume the vocabulary of 1–7.
Start at the top: sections 8–11 assume the vocabulary of 1–7.

## Core concept: a chain of stages

Expand Down Expand Up @@ -968,6 +969,124 @@ in.greeting
`-> Greet -> out
```

## 11. Parameters shared by several graphs

Two graphs that cut lines to a width should cut them to the *same* width. With
the width written into each graph, keeping them in step is a matter of care.
The fifth sample, [`apps/tunedPipeline/main.cpp`](apps/tunedPipeline/main.cpp),
keeps such values in a parameter file that the graphs share, under
[`apps/tunedPipeline/graphs/`](apps/tunedPipeline/graphs/):

```text
# alerts.fg: the long lines, cut to the shared width.
params "tuning.json"

in -> MinLength(minLength=$text.minLength) -> long
long -> Truncate(width=$text.width) -> out
```

```text
# report.fg: every line, cut to the same width as the alerts, and its size class.
params "tuning.json" # shared with alerts.fg
params "report.json" # this graph's own parameters

in -> Truncate(width=$text.width) -> out.text
in -> Classify(classes=$report.classes) -> out.size
```

```jsonc
// tuning.json
{ "text": { "minLength": 12, "width": 16 } }

// report.json
{ "report": { "classes": [ { "upTo": 10, "label": "short" },
{ "upTo": 30, "label": "medium" },
{ "upTo": 1000, "label": "long" } ] } }
```

```mermaid
flowchart LR
tuning[/"tuning.json<br/>text.minLength, text.width"/]
report[/"report.json<br/>report.classes"/]
alerts["alerts.fg"]
reportGraph["report.fg"]
tuning --> alerts
tuning --> reportGraph
report --> reportGraph
```

- An argument written `$text.width` names a parameter; the dot walks into the
`text` object. `$report.classes` is a list of objects, which an inline
argument cannot express.
- The `params` paths are relative to the graph file. When a graph names
several files, later ones override earlier ones member by member.

### Loading the graphs

```cpp
DslFilterGraph<std::string, std::string> alerts(dsl::loadGraphProgram(graphs / "alerts.fg"));
DslFilterGraph<std::string> report(dsl::loadGraphProgram(graphs / "report.fg"));
```

`loadGraphProgram` reads the graph file and its parameter files and binds the
parameters; the stages receive them in their config like any other argument.
Both graphs cut at 16 characters, and changing `text.width` in `tuning.json`
changes both:

```text
[shared] short one short alert: -
[shared] a line of medium... medium alert: a line of medium...
[shared] and a considerab... long alert: and a considerab...
```

### One value changed for a run

To try a value out without editing the file, pass overrides; they apply on top
of the files, the same way the files apply on top of each other:

```cpp
DslFilterGraph<std::string, std::string> alerts(
dsl::loadGraphProgram(graphs / "alerts.fg", {{"text", {{"width", 6}}}}));
```

```text
[override] and a ...
```

### Unbound and bound

`dsl::parseGraphProgram(text)` reads no files, so a program parsed from text
has its parameters unbound, and the renderings show them by name. Binding them,
here from a parameter file read with `dsl::loadParameters`, puts in the values:

```cpp
dsl::toAscii(dsl::parseGraphProgram("in -> Truncate(width=$text.width) -> out"));
dsl::toAscii(dsl::parseGraphProgram("in -> Truncate(width=$text.width) -> out",
dsl::loadParameters(graphs / "tuning.json")));
```

```text
in
`-> Truncate(width=$text.width) -> out

in
`-> Truncate(width=16) -> out
```

A graph built from the unbound program would report
`1:22: parameter '$text.width' is not set; ...`.

### A misspelled parameter

A name no parameter file defines is a located diagnostic, with a suggestion if
one is close. Parameters a graph does not use are fine: `alerts.fg` never reads
`report.classes`, and a shared file will always hold values that some graph
does not need.

```text
[check] 1:22: unknown parameter '$text.widht' — did you mean '$text.width'?
```

## Build & run these examples

```powershell
Expand All @@ -980,8 +1099,12 @@ cmake --build --preset windows-msvc-release-user-mode
./out/build/windows-msvc-release-user-mode/apps/statefulPipeline/statefulPipeline
./out/build/windows-msvc-release-user-mode/apps/compositePipeline/compositePipeline
./out/build/windows-msvc-release-user-mode/apps/namedPipeline/namedPipeline
./out/build/windows-msvc-release-user-mode/apps/tunedPipeline/tunedPipeline
```

`tunedPipeline` finds its graph files in the source tree; pass another
directory as its first argument to load graphs from there.

On Linux/macOS, use a matching preset such as `unixlike-gcc-release` or
`unixlike-clang-release`.

Expand Down Expand Up @@ -1071,3 +1194,20 @@ And of `namedPipeline` (section 10):

followed by the Mermaid flowchart and the `[direct]` block shown in section 10.
(The `[feed check]` type name is shortened here too.)

And of `tunedPipeline` (section 11):

```text
[shared] short one short alert: -
[shared] a line of medium... medium alert: a line of medium...
[shared] and a considerab... long alert: and a considerab...
[override] and a ...

in
`-> Truncate(width=$text.width) -> out

in
`-> Truncate(width=16) -> out

[check] 1:22: unknown parameter '$text.widht' — did you mean '$text.width'?
```
98 changes: 87 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ paths, merge them back together, and drop/short-circuit messages — all without
hard-coding the pipeline shape in source code.

> **New here? Start with the [example walkthrough (EXAMPLE.md)](EXAMPLE.md)** —
> a step-by-step, diagrammed tour of four runnable samples:
> a step-by-step, diagrammed tour of five runnable samples:
> [`apps/textPipeline`](apps/textPipeline/main.cpp) (the core building blocks),
> [`apps/statefulPipeline`](apps/statefulPipeline/main.cpp) (stages that carry
> state), [`apps/compositePipeline`](apps/compositePipeline/main.cpp)
> (composing graphs out of graphs) and
> (composing graphs out of graphs),
> [`apps/namedPipeline`](apps/namedPipeline/main.cpp) (named merge slots and
> graph inputs).
> graph inputs) and [`apps/tunedPipeline`](apps/tunedPipeline/main.cpp)
> (parameter files shared by several graphs).

> **A note on the word "filter".** Here "filter" follows the Unix-pipeline and
> media-graph (DirectShow / GStreamer / FFmpeg) tradition: a stage that reads a
Expand Down Expand Up @@ -101,9 +102,14 @@ drops the message, and everything downstream of that edge is skipped.
**`registerTypedMergeFilter`** — fan-in stages that declare their slot types,
so the edges of a group are checked when the graph is built and the stage
receives typed `std::optional`s instead of `std::any`.
- **`validateDslGraph<In, Out>(text)`** — the same checks as construction,
returned as a list of `line:column` diagnostics instead of a thrown
**`GraphError`**.
- **Parameters** — `Stage(width=$text.width)` takes an argument from a
parameter file (JSON) that several graph files share, named in each with
`params "tuning.json"`; **`dsl::loadGraphProgram(path)`** loads a graph file
with its parameters, and **`dsl::bindParameters`** binds values the caller
supplies.
- **`validateDslGraph<In, Out>(text)`** (or a parsed `GraphProgram`) — the
same checks as construction, returned as a list of `line:column` diagnostics
instead of a thrown **`GraphError`**.
- **`dsl::parseGraphProgram`** / **`dsl::toMermaid`** / **`dsl::toDot`** /
**`dsl::toAscii`** — parse a graph into its node/edge form and render it as a
Mermaid flowchart, a Graphviz DOT digraph or a console listing. The parser is
Expand Down Expand Up @@ -420,6 +426,71 @@ matcher.filter(GraphInputs{}.set("orders", order).set("quotes", quote)); // one
- A graph uses either `in` or named inputs: `in` with `GraphInputs`, or
`in.<key>` with any other input type, is a build-time diagnostic.

### Parameters shared by several graphs

Tuning values that several graphs must agree on belong in one place. An
argument can name a **parameter**, `$name`, instead of giving a value, and a
**parameter file** — a JSON object — gives the values. A graph file names its
parameter files with `params` lines:

```text
# alerts.fg
params "tuning.json"
in -> MinLength(minLength=$text.minLength) -> long
long -> Truncate(width=$text.width) -> out
```

```text
# report.fg
params "tuning.json" # shared with alerts.fg
params "report.json" # this graph's own parameters
in -> Truncate(width=$text.width) -> out.text
in -> Classify(classes=$report.classes) -> out.size
```

```json
{ "text": { "minLength": 12, "width": 16 } }
```

```cpp
#include <filterGraph/core/filterGraph/DslFilterGraph.hpp> // includes GraphParameters.hpp

DslFilterGraph<std::string, std::string> alerts(dsl::loadGraphProgram("graphs/alerts.fg"));
DslFilterGraph<std::string> report(dsl::loadGraphProgram("graphs/report.fg"));

// The same graph with one value changed for this run; the files stay as they are.
DslFilterGraph<std::string, std::string> narrow(
dsl::loadGraphProgram("graphs/alerts.fg", {{"text", {{"width", 6}}}}));

// A graph given as text, with parameters from the caller.
DslFilterGraph<std::string, std::string> fromText(
dsl::parseGraphProgram("in -> Truncate(width=$text.width) -> out", dsl::loadParameters("graphs/tuning.json")));
```

- **Names.** `$name` names a member of the parameter object; dots walk into
nested objects (`$text.width`). A parameter can hold any JSON value, so a
stage that needs a list or an object (`$report.classes`) can get one, which
inline arguments cannot express. Parameter files may contain `//` and
`/* */` comments.
- **Several files.** `params` paths are relative to the graph file. Files are
applied in the order written, each overriding the ones before it member by
member (JSON merge patch: nested objects merge, other values replace), and
`loadGraphProgram`'s second argument is applied last.
- **Checks.** A name that no file defines is a located diagnostic, with a
suggestion if one is close:
`1:22: unknown parameter '$text.widht' — did you mean '$text.width'?`. A file
that cannot be read, is not JSON or is not an object is a diagnostic at its
`params` line. Parameters a graph does not use are *not* reported, since a
file is meant to be shared.
- **Without a file.** `dsl::parseGraphProgram(text)` reads no files: it records
the `params` lines (`GraphProgram::parameterFiles`) and leaves the
parameters unbound, which the renderings show as `width=$text.width`.
Building a graph from it reports each unbound parameter as not set.
`dsl::parseGraphProgram(text, parameters)` and `dsl::bindParameters(program,
parameters)` bind values from the caller instead.
- A stage cannot tell a parameter from a literal argument: both arrive in its
config, and one stage's arguments can mix the two.

### Checking and visualizing a graph

`validateDslGraph` runs every construction check without running a message
Expand All @@ -432,6 +503,9 @@ for (const auto& d : validateDslGraph<std::string, int>(text))
}
```

It takes a parsed `GraphProgram` as well, e.g.
`validateDslGraph<std::string, int>(dsl::loadGraphProgram("graph.fg"))`.

```text
1:7: unknown stage type 'Uppercas' — did you mean 'Uppercase'?
2:7: could not construct 'MinLength': [json.exception.out_of_range.403] key 'minLength' not found
Expand Down Expand Up @@ -468,8 +542,9 @@ the UTF-8 code page (`chcp 65001`).

### Current limitations

- Stage arguments are flat `key=value` pairs; nested objects and lists are not
expressible yet. Stages that need them can be configured in JSON.
- Stage arguments are flat `key=value` pairs; nested objects and lists cannot
be written inline. A [parameter](#parameters-shared-by-several-graphs) can
hold them, or the stage can be configured in JSON.
- Only the graph input type, the declared named input types and the single
`out` type are checked against the C++ side; the types inside `GraphOutputs`
are checked when they are read (`get<T>` throws `std::bad_any_cast` on a
Expand Down Expand Up @@ -679,9 +754,10 @@ Run the bundled examples directly after building:
./out/build/windows-msvc-release-user-mode/apps/statefulPipeline/statefulPipeline
./out/build/windows-msvc-release-user-mode/apps/compositePipeline/compositePipeline
./out/build/windows-msvc-release-user-mode/apps/namedPipeline/namedPipeline
./out/build/windows-msvc-release-user-mode/apps/tunedPipeline/tunedPipeline
```

[EXAMPLE.md](EXAMPLE.md) walks through all four, with their output.
[EXAMPLE.md](EXAMPLE.md) walks through all five, with their output.

## Roadmap

Expand All @@ -696,8 +772,8 @@ available in the meantime. The current list:
- **Injectable registry with duplicate detection**, instead of a singleton that
silently overwrites.
- **Documentation** of 0..n outputs, fan-out copies and stateful stages.
- **Lifting the current limitations**: nested stage arguments in the DSL, and
type checking inside `GraphOutputs`.
- **Lifting the current limitations**: nested stage arguments written inline in
the DSL, and type checking inside `GraphOutputs`.

## License

Expand Down
7 changes: 4 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Examples use the generic stages of the [README](README.md) (`Parse`,
| 3 | [Config-aware `registerMergeFilter`](#3-config-aware-registermergefilter) | low | additive | subclass + `FilterRegistrar` creator |
| 4 | [Injectable registry, duplicate detection](#4-injectable-registry-duplicate-detection) | low | mostly additive | unique names |
| 5 | [Documentation: 0..n outputs, large messages](#5-documentation-0n-outputs-large-messages) | doc only | — | — |
| 6 | [Known limitations to lift](#6-known-limitations-to-lift) | low–medium | additive | JSON config; read `GraphOutputs` carefully |
| 6 | [Known limitations to lift](#6-known-limitations-to-lift) | low–medium | additive | a parameter or JSON config; read `GraphOutputs` carefully |
| 7 | [2D box layout for console rendering](#7-2d-box-layout-for-console-rendering) | low | additive | `toAscii` listing; `toDot` into `graph-easy --as=boxart` |

Items of the same list that are done, and therefore not repeated here:
Expand Down Expand Up @@ -172,8 +172,9 @@ The [current limitations](README.md#current-limitations) the README lists, as
work items:

- **Nested stage arguments in the DSL.** Arguments are flat `key=value` pairs;
nested objects and lists are not expressible, so a stage that needs them has
to be configured in JSON. Lifting this means a value grammar for objects and
nested objects and lists cannot be written inline, so a stage that needs them
takes them from a parameter (`classes=$report.classes`, see the README) or is
configured in JSON. Lifting this means a value grammar for objects and
arrays, plus diagnostics for it.
- **Type checking inside `GraphOutputs`.** Only the graph's input type and the
single `out` type are checked against the C++ template parameters. The types
Expand Down
1 change: 1 addition & 0 deletions apps/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ add_subdirectory(textPipeline)
add_subdirectory(statefulPipeline)
add_subdirectory(compositePipeline)
add_subdirectory(namedPipeline)
add_subdirectory(tunedPipeline)
Loading
Loading