Expand description
§ascii-dag
Graph layout engine that renders to text — DAGs, and cyclic graphs via dashed back edges — for error chains, build systems, and dependency visualization.
§Features
- Small: ~94 KB WASM no-alloc build (41 KB gzipped); see BENCHMARK.md
- Fast: Cached adjacency lists, O(1) lookups, banded zero-copy rendering
- no_std + no-alloc: the arena pipeline runs without a heap allocator
- Modular: Each component can be used independently
- Safe: Cycle detection built-in
§Performance
- Cached Adjacency Lists: O(1) child/parent lookups (not O(E))
- Zero Allocations: Direct buffer writes with
write_node() - HashMap Indexing: O(1) ID→index instead of O(N) scans
§Feature Flags
std(default): Standard library supportgeneric(default): Generic algorithms over your own types (cycle detection, topological sort, impact analysis, metrics). Impliesstd— these keep visited sets keyed by the caller’s id type, boundedEq + Hash, so they need a realHashSet.alloc: Heap-basedGraphAPI withoutstdarena(+arena-idx-u8/u16/u32): No-alloc CSR layout and rendering on caller-provided arenasports(default): declared edge attachment sides and their per-face positioning; off, attachment is the port-free rule and nothing of the machinery is linked (the embedded examples build without it)
Non-fatal conditions (auto-created placeholders, omitted labels)
travel through the diagnostics channel as typed data — there
is no logging feature and the library never writes to stderr.
To minimize bundle size, disable generic:
ascii-dag = { version = "0.10", default-features = false, features = ["std"] }§Quick Start
use ascii_dag::graph::{Graph, RenderMode};
// Batch construction (fast!)
let dag = Graph::from_edges(
&[(1, "Error1"), (2, "Error2"), (3, "Error3")],
&[(1, 2), (2, 3)]
);
println!("{}", dag.render());§Ports
An edge can declare which side of a node it leaves from and which
side it arrives on, on the handle Graph::add_edge
returns (or by index with set_edge_ports); the layout reports the
side each end actually took on every IR edge:
use ascii_dag::{Graph, PhysicalSide, PortSide};
let mut g = Graph::new();
g.add_node(1usize, "Gateway");
g.add_node(2usize, "Service");
g.add_node(3usize, "Cache");
g.add_edge(1usize, 2usize, None);
g.add_edge(1usize, 3usize, None).to_port(PortSide::West);
let ir = g.compute_layout();
let cache = ir.edges().iter().find(|e| e.to_id == 3).unwrap();
assert_eq!(cache.to_port.requested, PortSide::West);
assert_eq!(cache.to_port.side, PhysicalSide::West);PortSide names a side three ways. Compass sides (North,
East, South, West) are fixed on the page. Flow sides follow
the direction: Upstream is the face the flow arrives on,
Downstream the face it leaves by. Rotations follow it too:
Clockwise is the traveler’s right hand facing downstream,
Counterclockwise the left. Auto (the default) is head-on:
leave Downstream, arrive Upstream.
| Side | TopDown | BottomUp | LeftRight | RightLeft |
|---|---|---|---|---|
Upstream | North | South | West | East |
Downstream | South | North | East | West |
Clockwise | West | East | South | North |
Counterclockwise | East | West | North | South |
A face has one port by default, shared by every edge declared on
it; a PortPolicy — the graph’s (set_port_policy) or one
node’s (set_node_port_policy) — chooses Paired (an arrival and
a departure port), Spread (up to a PortBound) or Custom (the
PortPlacer registered with set_port_placer) instead. A node is never widened for its
ports, and a face with one cell holds one port whatever the policy.
Ends the layout could not honor are warnings on the run
(W.Graph.Port.034 for a side on a self-loop, W.Graph.Port.035
when no lane beside the node was free). The guide:
docs/ports.md; runnable: examples/ports.rs, every section
through both pipelines.
§Modular Design
The library is organized into separate, independently-usable modules:
§graph - Core DAG Structure
use ascii_dag::graph::Graph;
let mut dag = Graph::new();
dag.add_node(1, "A");
dag.add_node(2, "B");
dag.add_edge(1, 2, None);§Cycle Detection
use ascii_dag::graph::Graph;
let mut dag = Graph::new();
dag.add_edge(1, 2, None);
dag.add_edge(2, 1, None);
assert!(dag.has_cycle());§Generic Cycle Detection
Works with any data structure via higher-order functions:
use ascii_dag::algorithms::cycles::generic::detect_cycle_fn;
let get_deps = |id: &usize| match id {
1 => vec![2],
2 => vec![3],
_ => vec![],
};
let cycle = detect_cycle_fn(&[1, 2, 3], get_deps);
assert!(cycle.is_none());§Generic Topological Sorting
Sort any dependency graph into execution order:
use ascii_dag::algorithms::generic::topological_sort_fn;
let get_deps = |task: &&str| match *task {
"deploy" => vec!["build"],
"build" => vec!["compile"],
"compile" => vec![],
_ => vec![],
};
let sorted = topological_sort_fn(&["deploy", "compile", "build"], get_deps).unwrap();
// Result: ["compile", "build", "deploy"]
assert_eq!(sorted[0], "compile");§Graph Layout
Sugiyama hierarchical layout for positioning nodes.
§ASCII Rendering
Vertical, horizontal, and cycle visualization modes.
Re-exports§
pub use diagnostics::BorrowedReport;pub use diagnostics::CountingDiagnostics;pub use diagnostics::Diagnostic;pub use diagnostics::DiagnosticContext;pub use diagnostics::DiagnosticCounts;pub use diagnostics::DiagnosticKind;pub use diagnostics::DiagnosticRef;pub use diagnostics::DiagnosticRun;pub use diagnostics::DiagnosticSink;pub use diagnostics::DiagnosticSubject;pub use diagnostics::FnDiagnostics;pub use diagnostics::IgnoreDiagnostics;pub use diagnostics::ProjectedFailure;pub use diagnostics::Report;pub use diagnostics::Severity;pub use diagnostics::SliceDiagnostics;pub use diagnostics::OwnedReport;allocpub use diagnostics::VecDiagnostics;allocpub use errors::ErrorChain;allocpub use errors::GraphError;pub use graph::RenderMode;pub use graph::AUTO;pub use graph::Auto;pub use graph::IdOrAuto;pub use graph::NodeId;pub use graph::Direction;allocpub use graph::EdgeHandle;allocpub use graph::EdgeInsertion;allocpub use graph::Graph;allocpub use graph::MissingNodePolicy;allocpub use graph::NodeInsertion;allocpub use graph::Subgraph;allocpub use ir::EdgePath;allocpub use ir::FlowAxis;allocpub use ir::LayoutEdge;allocpub use ir::LayoutIR;allocpub use ir::LayoutIRBuilder;allocpub use ir::LayoutNode;allocpub use ir::SubgraphInfo;allocpub use render::colors::Palette;pub use render::engine::ArmWeight;pub use render::engine::ArmWeights;pub use render::engine::CellKind;pub use render::engine::CellMarker;pub use render::engine::CellView;pub use render::engine::CompositionRequirements;pub use render::engine::MarkerDirection;pub use render::engine::SceneComposer;pub use render::engine::TerminalRenderer;pub use render::engine::BoxedNode;pub use render::engine::CustomNode;pub use render::engine::NodeContent;pub use render::engine::SimpleNode;pub use render::engine::CellColor;pub use render::engine::HitResult;pub use render::engine::Charset;pub use render::engine::ColorMode;pub use render::engine::ComposeBudget;pub use render::engine::EmitOptions;pub use render::engine::LabelOverflow;pub use render::engine::LabelPlacementPolicy;pub use render::engine::LabelPolicy;pub use render::engine::LayoutSource;pub use render::engine::PlanOptions;pub use render::engine::PlanRun;pub use render::engine::RenderOptions;pub use render::engine::Scene;pub use render::engine::ScenePlanner;pub use render::engine::EdgePathView;pub use render::engine::EdgeView;pub use render::engine::LabelSlot;pub use render::engine::LabelView;pub use render::engine::NodeKind;pub use render::engine::NodeOrigin;pub use render::engine::NodeView;pub use render::engine::SubgraphView;pub use render::engine::LabelPosition;pub use render::engine::LineWeight;pub use render::engine::MarkerShape;pub use render::engine::SubgraphBorder;pub use validation::Requirements;pub use algorithms::sugiyama::config::AlgorithmConfig;pub use algorithms::sugiyama::config::CycleBreaking;pub use algorithms::sugiyama::config::Layering;pub use algorithms::sugiyama::config::LayoutConfig;pub use algorithms::sugiyama::config::Positioning;pub use algorithms::sugiyama::config::Routing;pub use algorithms::sugiyama::crossing::CrossingReducer;pub use algorithms::sugiyama::crossing::FAST;pub use algorithms::sugiyama::crossing::QUALITY;pub use algorithms::sugiyama::crossing::STANDARD;
Modules§
- algorithms
- Graph algorithms: cycle detection, generic analysis, and Sugiyama layout.
- diagnostics
- Typed, run-scoped diagnostics.
- errors
- Structured error codes and diagnostics following the Waddling ErrorChain Protocol (WDP) Level 0.
- graph
- Core graph data structure.
- ir
- Intermediate Representation for graph layout.
- render
- ASCII rendering for DAG visualization.
- validation
- Graph validation and requirements.
Structs§
- Port
- What a port setter accepts: a side today, a side plus a position
later —
#[non_exhaustive]with private fields so growing it never changes a signature.PortSideconverts into it, which is howfrom_port(PortSide::North)reads unchanged forever. - Port
Attachment - How one end of a routed edge is attached: what was declared and
the physical side it landed on.
requestedisAutofor an undeclared end;sideis always the truth of the drawing — an end that could not route off its role’s face reports that face here, and the run’s diagnostics say why. - Port
Slot - One end to place, as a
PortPlacersees it.
Enums§
- EdgeEnd
- Which declared end of an edge a condition is about.
- Physical
Side - A physical side of a node — where an edge end actually attached.
The RESOLUTION vocabulary: compass names only (a declaration is a
PortSide, which also hasAutoand the flow-relative names). - Port
Bound - The upper bound of a
PortPolicy::Spreadface. - Port
Policy - How a node places the ends declared on each of its faces. A
graph-wide default with a per-node override;
Singleunless set. A face with one cell holds one port whatever the policy. Arrival and departure are the DECLARED ends (toandfrom), which a cycle reversal does not change. - Port
Side - A declared side of a node for an edge end to attach to — the
DECLARATION vocabulary (it has
Autoand flow-relative names; a resolved side is always physical). One byte by guarantee.
Type Aliases§
- Port
Placer - A caller’s placement rule: the cell offset along the face for one
end, clamped to the face by the layout. A plain
fn, so it runs on the no-alloc pipeline and both backends place identically.