Skip to main content

Crate ascii_dag

Crate ascii_dag 

Source
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 support
  • generic (default): Generic algorithms over your own types (cycle detection, topological sort, impact analysis, metrics). Implies std — these keep visited sets keyed by the caller’s id type, bounded Eq + Hash, so they need a real HashSet.
  • alloc: Heap-based Graph API without std
  • arena (+ arena-idx-u8/u16/u32): No-alloc CSR layout and rendering on caller-provided arenas
  • ports (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.

SideTopDownBottomUpLeftRightRightLeft
UpstreamNorthSouthWestEast
DownstreamSouthNorthEastWest
ClockwiseWestEastSouthNorth
CounterclockwiseEastWestNorthSouth

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;alloc
pub use diagnostics::VecDiagnostics;alloc
pub use errors::ErrorChain;alloc
pub 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;alloc
pub use graph::EdgeHandle;alloc
pub use graph::EdgeInsertion;alloc
pub use graph::Graph;alloc
pub use graph::MissingNodePolicy;alloc
pub use graph::NodeInsertion;alloc
pub use graph::Subgraph;alloc
pub use ir::EdgePath;alloc
pub use ir::FlowAxis;alloc
pub use ir::LayoutEdge;alloc
pub use ir::LayoutIR;alloc
pub use ir::LayoutIRBuilder;alloc
pub use ir::LayoutNode;alloc
pub use ir::SubgraphInfo;alloc
pub 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. PortSide converts into it, which is how from_port(PortSide::North) reads unchanged forever.
PortAttachment
How one end of a routed edge is attached: what was declared and the physical side it landed on. requested is Auto for an undeclared end; side is 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.
PortSlot
One end to place, as a PortPlacer sees it.

Enums§

EdgeEnd
Which declared end of an edge a condition is about.
PhysicalSide
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 has Auto and the flow-relative names).
PortBound
The upper bound of a PortPolicy::Spread face.
PortPolicy
How a node places the ends declared on each of its faces. A graph-wide default with a per-node override; Single unless set. A face with one cell holds one port whatever the policy. Arrival and departure are the DECLARED ends (to and from), which a cycle reversal does not change.
PortSide
A declared side of a node for an edge end to attach to — the DECLARATION vocabulary (it has Auto and flow-relative names; a resolved side is always physical). One byte by guarantee.

Type Aliases§

PortPlacer
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.