Skip to content

About

An engine-agnostic DOT parser for Zig with borrowed syntax trees, typed diagnostics, explicit memory control, and bare-metal compatibility.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

dot-parser

A parser for the DOT graph language, the text format used by Graphviz, written in Zig.

It reads DOT text and gives you its structure: nodes, edges, attributes and subgraphs. It also gives clear error messages. It does not draw graphs or compute layouts. You decide what to build on top, for example a linter, a formatter, or a converter into your own graph type.

The package has three independent modules:

  • dot_parser parses DOT.
  • markup_parser parses HTML-like markup, such as Graphviz's label=<<b>Hi</b>> labels. It can check labels while DOT is parsed, or work on its own with no DOT at all. Since 0.5.0 it checks Graphviz's tags and attributes by default; select structural mode for other markup.
  • string_processor (Unreleased) checks borrowed string contents with equality, allowed values and compile-time custom validators. See standalone strings. Explicit logical-reader binding supports decoded DOT/markup values; optional attribute routing integrates both hosts while keeping passthrough as the default.

Status: experimental (0.x). It works and is well tested, but names may still change between versions. Breaking changes are listed in the changelog.

digraph Pipeline {
    node [shape=box];
    fetch -> parse -> check [color=red];
    subgraph cluster_output { render; save }
    check -> { render save }
}

Highlights

  • Helpful errors. Each problem points at the exact spot, explains what is wrong, and often suggests a fix. Parsing keeps going after an error, so you see more than one problem at a time.
  • You choose where memory comes from. Use any allocator, an arena, or fixed buffers with no heap allocation at all.
  • Runs anywhere. No dependencies, no OS calls, no file access. It builds for embedded targets and WebAssembly.
  • Keeps what was written. Spelling, order and duplicates are kept exactly. Nothing is silently rewritten or interpreted.
  • Supports safe handling of untrusted input. Parsing uses no recursion and makes one pass over the input. Limits are off by default, so you set explicit budgets for size, nesting and memory, and can parse step by step with cancellation.
  • Configurable. Strict by default, with a lenient mode and optional extra checks.
  • HTML-like markup, inside DOT or on its own. Check the inside of labels such as label=<<b>Hi</b>> while parsing DOT, or parse HTML-like markup by itself.
  • Bring your own processor. Swap in your own label checker at compile time. Its settings plug into the same settings system as DOT's.

A quick look

const std = @import("std");
const dot = @import("dot_parser");

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    var buffer: [4096]u8 = undefined;
    var stdout_writer: std.Io.File.Writer = .init(.stdout(), init.io, &buffer);
    const stdout = &stdout_writer.interface;
    defer stdout.flush() catch {};

    const source =
        \\digraph {
        \\    a -> b;
        \\    b -> c [color=red];
        \\}
    ;

    // Problems are collected here instead of being printed or thrown.
    var bag = dot.GrowableDiagnosticBag.init(allocator, .{});
    defer bag.deinit();

    // Parse the text, then check it (for example, `--` inside a digraph).
    var result = dot.parseAndValidate(allocator, source, bag.sink(), .{});
    defer result.deinit(allocator);

    if (!result.documentValid()) {
        for (bag.items(), 1..) |problem, number| {
            try dot.console.renderBoxed(problem, number, .{ .source = source, .source_name = "graph.dot" }, stdout);
        }
        return;
    }

    const document = result.document.?;
    var edges = document.edgeIterator();
    while (edges.next()) |edge| {
        // An edge end is a node or a whole subgraph (`a -> { b c }`).
        if (edge.left != .node or edge.right != .node) continue;
        const from = document.nodeReference(edge.left.node).?.identifier;
        const to = document.nodeReference(edge.right.node).?.identifier;
        try stdout.print("{s} {s} {s}\n", .{
            document.text(from), edge.operator.lexeme(), document.text(to),
        });
    }
}

This prints:

a -> b
b -> c

If line 3 said b -- c instead (an undirected edge in a directed graph), you would get:

┌─ Error 1: edge operator does not match the graph kind
│ graph.dot:3:7
│
│ 1 │ digraph {
│   │ ─────── the document is directed because of this keyword
│ ⋯
│ 3 │     b -- c [color=red];
│   │       ^^ expected '->', found '--'
│
│ Hint: change '--' to '->', or declare the document with 'graph'
│ Fix: replace '--' with '->' (one possible repair)
└─ E1 ─ [dot_parser:E.Validation.Operator.002]

This program is examples/quick_start.zig. Getting started walks through it step by step.

Install

The development version requires Zig 0.17.0. Add it to your project:

zig fetch --save git+https://github.com/AshutoshMahala/dot-parser#main

Then import the modules you need in your build.zig:

const dot_parser = b.dependency("dot_parser", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("dot_parser", dot_parser.module("dot_parser"));
// Only if you check labels or parse markup:
exe.root_module.addImport("markup_parser", dot_parser.module("markup_parser"));
// Unreleased: only if you check standalone string contents:
exe.root_module.addImport("string_processor", dot_parser.module("string_processor"));

This selects the development branch, which these docs follow. For the latest tagged release, use #v0.5.0 with Zig 0.16.0 instead; anything marked Unreleased, including the 0.17 migration, isn't in that release. The changelog lists what changed and what breaks.

What it understands

All of DOT's statement syntax:

  • graph and digraph documents, with optional strict and a name
  • Nodes, edges, and edge chains (a -> b -> c)
  • Attributes: [color=red] lists, node [shape=box] defaults, and rankdir=LR
  • Subgraphs, named or not, nested, and as edge ends (a -> { b c })
  • Ports (a:out, a:out:n)
  • Every kind of name: plain words (including non-ASCII), numbers, "quoted strings" joined with +, and HTML-like <...> labels
  • Comments (//, /* */, #) and optional semicolons

See supported syntax for the full list and the few places where it differs from Graphviz.

What it does not do

It reports what the file says. It does not work out what the file means:

  • It doesn't lay out or draw anything.
  • It doesn't interpret attribute values. color=red is just two pieces of text.
  • It doesn't apply defaults. node [shape=box] stays a statement; it isn't copied onto each node.
  • It doesn't expand a -> { b c } into two edges, merge repeated subgraphs, or build a list of unique nodes.
  • It doesn't read files. You pass it bytes.
  • It doesn't keep whitespace or reformat files. It can keep comments if you ask.

You can do all of these on top of the parsed document. Why it works this way explains these choices.

Documentation

DOT

Guide Read it when you want to…
Getting started parse your first DOT file and read the result
Reading a parsed graph walk nodes, edges, attributes, ports and subgraphs
Supported syntax check exactly which DOT input is accepted
DOT error codes look up a DOT error or warning

Markup

Guide Read it when you want to…
Checking HTML-like labels check <...> labels inside DOT files
Parsing markup on its own parse HTML-like markup without DOT, or check many fragments
Bringing your own processor plug your own label checker into DOT parsing
Markup error codes look up a markup error or warning

Both parsers

Guide Read it when you want to…
Errors and diagnostics show errors, understand results, or apply suggested fixes
Memory use an arena or fixed buffers, or work with no allocator at all
Settings make parsing stricter or more lenient, add checks, or set limits
Parsing in small steps spread parsing over time, or cancel it
Using it in an editor Keep the finished part of unfinished files, label trees and comments
Source encodings Read UTF-16 or UTF-32 files without converting them

Project

Guide Read it when you want to…
Why it works this way understand the main design decisions
Roadmap see what is planned but not built yet
Performance see measured speed and memory use
Architecture find your way around the source code

The same index is in docs/README.md.

Examples

Small runnable programs in examples/. zig build examples builds and runs them all, and installs each one in zig-out/bin/.

Example Shows how to… Guide
quick_start parse, show problems, and list edges Getting started
parse_undigraph print every kind of statement Getting started
identifiers get the real value of a quoted or joined name Reading
comments keep comments, with their kind and position Reading comments
attributes read attribute lists, defaults and assignments Reading
edge_chains walk chains like a -> b -> c Reading
ports read ports like a:out:n Reading
subgraphs walk subgraphs and their contents Reading
subgraph_endpoints handle edges that end at a subgraph Reading
diagnostics_demo print problems with source lines and color Errors
check_file build a command-line checker for .dot files Errors
fixed_buffer parse with no allocator at all Memory
policies use checks, lenient mode, graph kinds and run-time settings Settings
bounded parse in small steps, and cancel Small steps
composed_markup check every label while parsing DOT Labels
delayed_markup check only the labels you choose Labels
attached_markup check chosen labels later and keep their trees in the DOT result Editors
markup parse and validate markup on its own Markup
string_processor standalone string checks and a custom predicate (Unreleased) Strings
markup_partial keep the finished part of markup for editors Editors
partial_documents keep the finished part of a DOT file and its label trees Editors
graphviz_vocabulary check Graphviz label grammar, attributes and references Label rules
custom_processor plug in your own label checker Own processor

Building and testing

zig build check                # full checks; Zig 0.17.0 and Node on PATH
zig build test                 # Zig tests only; no Node required
zig build examples             # build and run every example
zig build check-freestanding   # compile for RISC-V32 and Wasm32
zig build check-tools          # compile/test Unicode generators and performance reporter
zig build check-wasm           # execute Wasm probes with Node
zig build bench -Doptimize=fast   # main benchmark

check runs unit/public-API tests (including expected compile failures), examples, generator checks, benchmark compilation, freestanding compilation and Wasm export/table-retention checks. It does not run performance benchmarks, update baselines or download data. Node is needed only to execute Wasm; Python and npm packages are not required. Ordinary library builds still need only Zig.

Regenerating Unicode tables

Both generators are Zig programs reading local, SHA-256-pinned Unicode 17.0.0 files. Obtain EastAsianWidth.txt, extracted/DerivedGeneralCategory.txt and CaseFolding.txt from the official Unicode 17.0.0 UCD, preserving those paths under a local UCD directory. The license is already in src/string/UNICODE_LICENSE.txt and is checked too. No build fetches these files.

To compare freshly generated output with the checked-in tables without writing:

zig build check-unicode -Dunicode-data=/path/to/ucd
# Include the same comparison in the full check:
zig build check -Dunicode-data=/path/to/ucd

Without -Dunicode-data, check runs generator unit tests and compiles both tools, but does not claim to regenerate tables against the full upstream data. Each generator verifies compression across all Unicode code points before emitting formatted Zig. To inspect regenerated source on stdout:

zig build generate-console-widths -- /path/to/ucd/EastAsianWidth.txt /path/to/ucd/extracted/DerivedGeneralCategory.txt src/string/UNICODE_LICENSE.txt
zig build generate-string-casefold -- /path/to/ucd/CaseFolding.txt

Generation is a maintainer operation, not part of normal compilation. The generators never overwrite repository files themselves.

License

Licensed under either of

at your option (MIT OR Apache-2.0).

The optional console renderer uses Unicode-derived width tables under the Unicode License v3. The full notice is in console_widths.zig.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you shall be dual licensed as above, without any additional terms or conditions.

About

An engine-agnostic DOT parser for Zig with borrowed syntax trees, typed diagnostics, explicit memory control, and bare-metal compatibility.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages