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
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
a merge whose same-typed slots are matched by name (`mergeInputNames()`), a
lambda merge with named slots (`registerTypedMergeFilter` with names), a graph
with several named inputs fed by `push()` and `filter(GraphInputs)`, the
build-time and feed-time checks the names enable, and `toMermaid` labelling a
merge's edges with its slot names. EXAMPLE.md section 10 walks through it; the
build-time and feed-time checks the names enable, `toMermaid` labelling a
merge's edges with its slot names, and a merge reading a named input directly.
EXAMPLE.md section 10 walks through it; the
named-slot and named-input blocks move there from `apps/textPipeline`.
- **Named merge slots** — a fan-in group can name the slots of a merge,
`(raw: msg, checked: valid) -> Merge`, and is then matched by name, in any
Expand All @@ -90,7 +91,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
overloads, and they are checked when the graph is built; an undeclared input
takes the type of its first reader. Feeding an unknown key throws
`std::out_of_range`, a value of the wrong type `std::invalid_argument`.
`dsl::GraphProgram::inputs` lists the inputs a program reads.
`dsl::GraphProgram::inputs` lists the inputs a program reads. A fan-in group
can read `in` and `in.<key>` directly, positional or named
(`(plots: plots, ticks: in.ticks) -> Tracker`), with no pass-through stage
to copy the input onto an edge first. An undeclared input read only by a
group takes the type of its slot.
- **`TODO.md`** — the planned work, with what the library does today, the gap,
an API sketch and the workaround for each item; the README roadmap summarizes
it.
Expand Down
41 changes: 38 additions & 3 deletions EXAMPLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ them small text pipelines:
| 1–7 | [`textPipeline`](apps/textPipeline/main.cpp) | the core building blocks: a **compile-time** `FilterGraph`, **runtime graphs in the text DSL** (a tap, a merge, several named outputs, up-front diagnostics) and the same pipeline in the **JSON format** |
| 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, and the checks both make possible |
| 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 |

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

Expand Down Expand Up @@ -933,6 +933,41 @@ flowchart LR
s2 --> e4
```

### A merge reading an input directly

Only the name needs preparing; the greeting can go into `Greet` as it arrives.
A group reads `in.<key>` (or plain `in`) like any other edge, so no stage has
to copy the input onto an edge of its own first:

```cpp
DslFilterGraph<GraphInputs, std::string> greet(R"dsl(
in.name -> Capitalize -> proper
(greeting: in.greeting, name: proper) -> Greet -> out
)dsl");

greet.push("greeting", std::string{"Welcome"});
greet.filter(GraphInputs{}.set("greeting", std::string{"Welcome"}).set("name", std::string{"lINUS"}));
std::cout << dsl::toAscii(greet.program());
```

Nothing is declared here, so `in.greeting` takes the type of the `greeting`
slot it feeds. As before, the unfed input is a hole:

```text
[direct] Welcome, stranger!
[direct] Welcome, Linus!

in.name
`-> Capitalize -> proper
`-> Greet (merge, see below)

in.greeting
`-> Greet (merge, see below)

(greeting: in.greeting, name: proper)
`-> Greet -> out
```

## Build & run these examples

```powershell
Expand Down Expand Up @@ -1034,5 +1069,5 @@ And of `namedPipeline` (section 10):
[greet] HI, Grace!
```

followed by the Mermaid flowchart shown in section 10. (The `[feed check]` type
name is shortened here too.)
followed by the Mermaid flowchart and the `[direct]` block shown in section 10.
(The `[feed check]` type name is shortened here too.)
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,8 @@ edge -> Stage -> edge -> Stage(key=value) -> edge

| Name | Meaning |
| --- | --- |
| `in` | the graph's input; may only start a statement |
| `in.<key>` | a named input, for a graph with several (see [Several named inputs](#several-named-inputs)) |
| `in` | the graph's input; may only start a statement or appear in a fan-in group |
| `in.<key>` | a named input, for a graph with several (see [Several named inputs](#several-named-inputs)); placed like `in` |
| `out` | the graph's output; may only end a statement |
| `out.<key>` | a named output (several outputs are ordered as written) |
| `end` | a dead end: the value is discarded (required for `Void` stages) |
Expand All @@ -262,6 +262,8 @@ edge -> Stage -> edge -> Stage(key=value) -> edge
`registerMergeFilter<OutputType>(name, combiner)` does. A group may name the
slots of a merge that declares names, `(raw: msg, checked: valid) -> Merge`,
and is then matched by name, in any order (see [Named slots](#named-slots)).
A group may read the graph's input directly, `(in, valid)` or
`(quote: in.quotes, order: order)`; `out` and `end` cannot appear in a group.
- `#` starts a comment that runs to the end of the line.

### How a graph runs
Expand Down Expand Up @@ -399,11 +401,20 @@ matcher.filter(GraphInputs{}.set("orders", order).set("quotes", quote)); // one
- One `push` or `filter` is one run. An input not fed in that run is empty,
exactly like a dropped path: the stages reading it are skipped, and a merge
sees a hole.
- A merge can read named inputs directly, so a stage that reacts to two kinds
of message needs no pass-through stage in front of it:

```text
in.plots -> DropDuplicatePlots -> plots
(plots: plots, ticks: in.ticks) -> GatedTracker -> out
```

Each run feeds one input, and the merge sees a hole in the other slot.
- `GraphInputs::of<Types...>(keys...)` declares the input types, which are then
checked like every other edge when the graph is built; a declared input the
graph never reads, and a read input that is not declared, are diagnostics.
Without a declaration, an input takes the type of the first stage that reads
it.
it (or, for a merge, of the slot it feeds).
- Feeding an unknown key throws `std::out_of_range`; a value whose type differs
from the input's (declared or inferred) type throws `std::invalid_argument`.
- A graph uses either `in` or named inputs: `in` with `GraphInputs`, or
Expand Down
17 changes: 17 additions & 0 deletions apps/namedPipeline/main.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Names in a graph's wiring — for graphs with more than one of something that
// positions alone cannot tell apart:
// - named merge slots: `(after: x, before: y) -> Compare`, matched by name
Expand All @@ -5,6 +5,7 @@
// - several named graph inputs, `in.<key>`, fed by push() or filter()
// - the build-time and run-time checks that the names make possible
// - toMermaid, which labels a merge's edges with its slot names
// - a merge that reads a named input directly, `(greeting: in.greeting, ...)`
//
// See EXAMPLE.md, "Names in the wiring: merge slots and graph inputs".
#include <filterGraph/core/filterGraph/DslFilterGraph.hpp>
Expand Down Expand Up @@ -214,5 +215,21 @@
std::cout << '\n' << dsl::toMermaid(greet.program());
}

// 7) A group can read a named input directly: the greeting needs no
// preparing, so no stage has to copy it onto an edge of its own. The
// undeclared input takes the type of the slot it feeds.
{
DslFilterGraph<GraphInputs, std::string> greet(R"dsl(
in.name -> Capitalize -> proper
(greeting: in.greeting, name: proper) -> Greet -> out
)dsl");

std::cout << "\n[direct] " << *greet.push("greeting", std::string{"Welcome"}) << '\n';
std::cout << "[direct] "
<< *greet.filter(GraphInputs{}.set("greeting", std::string{"Welcome"}).set("name", std::string{"lINUS"}))
<< "\n\n"
<< dsl::toAscii(greet.program());
}

return 0;
}
10 changes: 8 additions & 2 deletions libs/filterGraph/core/filterGraph/DslFilterGraph.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -437,11 +437,12 @@ inline std::optional<std::vector<std::string>> matchSlotNames(const StageNode&
// matches named slots by name and positional ones by position, checks the
// edges against the declared slot types, and returns the group's edges in the
// stage's slot order. An untyped merge with positional slots is not checked.
// An undeclared graph input read by a slot takes the slot's type.
// Problems are appended to `diagnostics`; the plan must then not be run.
inline std::vector<std::string> wireMergeSlots(const StageNode& node,
const MergeSlotTypes& types,
const std::vector<std::string>& names,
const EdgeTypes& edgeTypes,
EdgeTypes& edgeTypes,
std::vector<TextDiagnostic>& diagnostics)
{
if (!names.empty() && !checkDeclaredSlotNames(node, types, names, diagnostics))
Expand Down Expand Up @@ -480,7 +481,12 @@ inline std::vector<std::string> wireMergeSlots(const StageNode& n
{
const std::type_index expected = types.uniform ? types.types.front() : types.types[slot];
const auto type = edgeTypes.find(edges[slot]);
if (type && *type != expected)
if (!type && isInputEdge(edges[slot]))
{
edgeTypes.types.emplace(edges[slot], expected);
edgeTypes.inferredFrom.emplace(edges[slot], node.type);
}
else if (type && *type != expected)
{
const std::string label = names.empty() ? std::to_string(slot + 1) : std::format("'{}'", names[slot]);
diagnostics.push_back({node.loc,
Expand Down
84 changes: 73 additions & 11 deletions libs/filterGraph/core/filterGraph/GraphLang.hpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#pragma once

#include <filterGraph/core/filterGraph/FilterRegistry.hpp>
Expand Down Expand Up @@ -45,7 +45,8 @@
// - fan-in = a source group `(a, b) -> Merge -> c`, where Merge is a merge
// stage (see registerMergeFilter in MergeFilter.hpp); the group may name the
// merge's slots, `(raw: a, checked: b) -> Merge -> c`, which matches them by
// name instead of by position;
// name instead of by position; a group may read `in` / `in.<key>` directly,
// `(in, a)` or `(plots: a, ticks: in.ticks)`;
// - `#` starts a comment that runs to the end of the line.
//
// This header parses (with a lexy-based parser) and structurally validates a
Expand Down Expand Up @@ -327,10 +328,53 @@
std::optional<std::string> key; // for `in.<key>` / `out.<key>`
nlohmann::json config; // for Stage
std::vector<std::string> edges; // for Group
std::vector<std::optional<std::string>> edgeKeys; // for Group: per edge, from `edge.<key>`
std::vector<SourceLoc> edgeLocs; // for Group: per edge
std::vector<std::string> slotNames; // for Group: per edge, from `name: edge`; "" when unnamed
SourceLoc loc;
};

// Adds the graph input `edge` ("in" or "in.<key>") to the program on its first use.
inline void bindInput(GraphProgram& program, const std::string& edge, const std::optional<std::string>& key,
const SourceLoc& loc)
{
const bool seen = std::any_of(program.inputs.begin(), program.inputs.end(),
[&](const InputBinding& input) { return input.edge == edge; });
if (!seen)
{
program.inputs.push_back({edge, key, loc});
}
}

// Checks the edges of a fan-in group: `in` and `in.<key>` are graph inputs,
// `out` and `end` are sinks, and no other edge takes a key. Spells each keyed
// input as its edge, "in.<key>".
inline bool checkGroupEdges(Term& group, std::vector<TextDiagnostic>& diags)
{
for (std::size_t j = 0; j < group.edges.size(); ++j)
{
std::string& edge = group.edges[j];
const std::optional<std::string>& key = group.edgeKeys[j];
if (edge == "out" || edge == "end")
{
diags.push_back({group.edgeLocs[j],
std::format("'{}' may only appear as the last term of a statement", edge)});
return false;
}
if (key && edge != "in")
{
diags.push_back({group.edgeLocs[j],
std::format("only 'in' and 'out' take a '.<key>', not '{}.{}'", edge, *key)});
return false;
}
if (key)
{
edge = "in." + *key;
}
}
return true;
}

// Checks the slot names of a fan-in group: all slots are named or none, and no
// name is used twice.
inline bool checkSlotNames(const Term& group, std::vector<TextDiagnostic>& diags)
Expand Down Expand Up @@ -419,7 +463,8 @@
diags.push_back({terms[i].loc, "a fan-in group needs at least one edge"});
return;
}
if (terms[i].kind == Term::Kind::Group && !checkSlotNames(terms[i], diags))
if (terms[i].kind == Term::Kind::Group &&
(!checkGroupEdges(terms[i], diags) || !checkSlotNames(terms[i], diags)))
{
return;
}
Expand Down Expand Up @@ -454,16 +499,18 @@
{
node.slotNames = source.slotNames;
}
for (std::size_t j = 0; j < source.edges.size(); ++j)
{
if (source.edges[j] == "in" || source.edgeKeys[j])
{
bindInput(program, source.edges[j], source.edgeKeys[j], source.edgeLocs[j]);
}
}
}
else if (source.name == "in")
{
const std::string edge = source.key ? "in." + *source.key : std::string{"in"};
const bool seen = std::any_of(program.inputs.begin(), program.inputs.end(),
[&](const InputBinding& input) { return input.edge == edge; });
if (!seen)
{
program.inputs.push_back({edge, source.key, source.loc});
}
bindInput(program, edge, source.key, source.loc);
node.inputs = {edge};
}
else
Expand Down Expand Up @@ -876,8 +923,9 @@
{
return unexpected("expected an edge name in group");
}
// `edge`, or `name: edge` for a named slot.
std::string edge = captureIdent();
// `edge`, or `name: edge` for a named slot; the edge may be `in.<key>`.
SourceLoc edgeLoc = locOf(sc.position());
std::string edge = captureIdent();
std::string slot;
skipBlank();
if (sc.branch(ld::lit_c<':'>))
Expand All @@ -887,9 +935,23 @@
{
return unexpected(std::format("expected an edge name after '{}:'", edge));
}
slot = std::exchange(edge, captureIdent());
edgeLoc = locOf(sc.position());
slot = std::exchange(edge, captureIdent());
skipBlank();
}
std::optional<std::string> key;
if (sc.branch(ld::lit_c<'.'>))
{
skipBlank();
if (!sc.peek(ld::ascii::alpha_underscore))
{
return unexpected("expected a key name after '.'");
}
key = captureIdent();
}
term.edges.push_back(std::move(edge));
term.edgeKeys.push_back(std::move(key));
term.edgeLocs.push_back(edgeLoc);
term.slotNames.push_back(std::move(slot));
}
};
Expand Down
21 changes: 18 additions & 3 deletions libs/filterGraph/core/filterGraph/GraphLangHandwritten.hpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#pragma once

#include <filterGraph/core/filterGraph/GraphLang.hpp>
Expand Down Expand Up @@ -324,8 +324,9 @@
unexpected("expected an edge name in group");
return term;
}
// `edge`, or `name: edge` for a named slot.
std::string edge = next().text;
// `edge`, or `name: edge` for a named slot; the edge may be `in.<key>`.
SourceLoc edgeLoc = peek().loc;
std::string edge = next().text;
std::string slot;
if (peek().kind == TokenKind::Colon)
{
Expand All @@ -335,9 +336,23 @@
unexpected(std::format("expected an edge name after '{}:'", edge));
return term;
}
slot = std::exchange(edge, next().text);
edgeLoc = peek().loc;
slot = std::exchange(edge, next().text);
}
std::optional<std::string> key;
if (peek().kind == TokenKind::Dot)
{
next();
if (peek().kind != TokenKind::Identifier)
{
unexpected("expected a key name after '.'");
return term;
}
key = next().text;
}
term.edges.push_back(std::move(edge));
term.edgeKeys.push_back(std::move(key));
term.edgeLocs.push_back(edgeLoc);
term.slotNames.push_back(std::move(slot));
}
}
Expand Down
Loading
Loading