|
RPNX::QueryGraph
Typed, memoized, concurrent query evaluation for C++23
|
RPNX::QueryGraph is a C++ library for memoized, concurrent evaluation of typed queries. A query handler is a coroutine: it can request other queries, publish subquery results, emit diagnostic messages, and suspend without growing the native call stack. The graph caches each query by its typed input so overlapping requests share one calculation.
The library is intended for compiler frontends, build systems, analysis tools, and other workloads that can be expressed as a dynamic dependency graph.
QueryGraph was mainly created for Quxlang but is generic and may be useful for other projects.
Ordinary recursive code is easy to express but can repeat overlapping work, overflow the call stack, and make cross-thread coordination difficult. Memoization removes repeated work, but a conventional memoized function still blocks its caller and usually requires application-specific synchronization.
QueryGraph combines four ideas:
(query type, input) pair identifies one cached node. Concurrent callers observe the same calculation instead of starting duplicate work.co_await to request dependencies, allowing deeply nested graphs without consuming the native call stack.bind_handlers() resolves declared dependencies to direct runtime descriptors before execution.The scheduler runs ready coroutine resumptions across a worker group, balances local and global work queues, and wakes both coroutine and external-thread waiters when a node reaches a terminal state.
std::format, and std::print supportQUERYGRAPH_USE_CONC_UNORDERED_MAP is enabled (the default)BUILD_TESTING is enabledEvery handler must be registered exactly once and bind_handlers() must be called after registration and before the first request. Registration, canonical error registration, and binding are configuration operations; complete them before making concurrent requests on the same graph.
A query specification satisfies query_spec_c by exposing:
input_type: the value used as the memoization key;output_type: the value returned by the handler; andquery_id: a stable, string-convertible identifier used in graph dumps.A handler specification satisfies query_handler_spec_c by exposing:
query: the query specification it implements;dependencies: an rpnx::typelist containing every query or subquery the handler may request; andproduced_subqueries: an rpnx::typelist of subqueries the handler may publish with co_yield.Inputs must support the selected cache backend. With the default concurrent map they must be hashable through rpnx::querygraph::hasher; with the fallback map they must be ordered. Inputs, outputs, and registered canonical errors must also be supported by RPNX::Serialization if graph dumps are used. Specialize binary_traits<T> to customize binary serialization and debug_traits<T> to customize diagnostic text.
Within a handler, construct a request<QuerySpec> and await it:
co_yield dependency(request) declares and schedules an edge without waiting for its value immediately. The original request can be awaited later. A dependency omitted from the handler specification causes a compile-time error.
Handlers may also co_await handler-specific cosubroutines. Cosubroutines share the parent query node and scheduler context, so they are useful for splitting a handler into coroutine-aware operations without creating separately memoized query nodes.
A subquery is a result produced in the context of one parent query node. Its specification exposes parent_query, input_type, output_type, and a stable subquery_id.
A parent handler lists the subquery in produced_subqueries and publishes values with co_yield subquery_result<SubquerySpec>(input, output). A consumer lists both the subquery and its parent query in dependencies, then awaits subquery_request<SubquerySpec>(parent_input, input). External callers can use make_subquery_request<SubquerySpec>().
If the parent finishes without publishing a requested value, subquery_does_not_exist is reported. If the parent fails first, subquery_parent_failed preserves the parent error as a nested exception.
Unhandled exceptions terminate the affected query node and are rethrown to requesters. Errors registered with register_canonical_error<Error>() are copied into stable, serializable error storage so dumps can retain the error type, payload, and message. Canonical error types must derive from std::exception and be copy constructible.
Handlers can attach source-located messages to their node:
recursive_dependency_error indicates that the executor became quiescent while the requested node was still unresolved, usually because of a dependency cycle. bad_continuation indicates that a handler coroutine completed without returning a value, throwing, or leaving a dependency that could continue it.
graph::dump() returns the structured graph_data representation. graph::marshall() serializes it with RPNXSerialization, and dump_query_to_file<QuerySpec>() evaluates one root query and writes the reachable graph to a binary file. The on-disk schema is documented in doc/dump-file-format.md.
Graph instances retain memoized nodes for their lifetime. Repeating the same query input returns the cached terminal result, including a cached error.
This repository's cbuild workspace is under build/. On a new checkout, detect the local toolchain and download the pinned Git dependencies before building:
For a conventional dependency-provided CMake build:
Consumers link the exported target:
Relevant CMake options are:
QUERYGRAPH_USE_CONC_UNORDERED_MAP: use the sharded concurrent cache backend (default ON);RPNX_QUERYGRAPH_BUILD_EXAMPLES: build querygraph-demo (default ON for a top-level build and OFF as a subproject).Run Doxygen from the repository root:
The generated HTML entry point is doc-out/html/index.html. The documentation includes this README, the public headers, the graph-dump schema, concepts, customization points, error contracts, and coroutine-facing API types.
include/rpnx/querygraph/: installed public headerssources/querygraph.cpp: library implementationsources/: demo graph and example handlerstests/: GoogleTest test suiteDoxyfile: standalone Doxygen configurationdoc/: graph-dump format documentationThe current roadmap is tracked in TODO.md. Major planned work includes scheduler tuning for high-core-count systems and memory-aware scheduling based on estimated node requirements. Persistence and reconstruction of live memoized graphs are not currently provided; graph serialization is intended for debugging and analysis.
RPNX::QueryGraph is licensed under the Apache License 2.0. See the LICENSE file.