8 releases (2 stable)

new 1.0.1 Oct 17, 2024
0.1.5 Oct 15, 2024

#221 in Parser implementations

Download history 558/week @ 2024-10-10

558 downloads per month

Custom license

76KB
1.5K SLoC

nom-tracer

Crates.io Documentation License

nom-tracer is a powerful and flexible tracing utility for the nom parser combinator library. It allows you to easily trace the execution of your parsers, providing invaluable insights for debugging and optimization.

nom-tracer screenshot

Contents

Features

  • 🔍 Trace parser execution with minimal code changes
  • 🚀 Zero overhead when disabled - compile out all tracing code in release builds
  • 🎨 Colorized output for easy reading (optional)
  • 🏷️ Support for multiple trace tags to organize parser traces
  • 📊 Hierarchical view of parser execution
  • 🤫 Silence subtrees to reduce noise in well-tested parts of your parser
  • 🔧 Configurable via Cargo features

Performance

One of the key advantages of nom-tracer is its minimal performance impact:

  • When disabled: The tracing code is completely compiled out, resulting in virtually zero overhead. Your parsers will run at full speed in production builds.
  • When enabled: The tracing functionality is designed to be as lightweight as possible, allowing for detailed insights with minimal performance cost during development and debugging.

Quick Start

Add nom-tracer to your Cargo.toml:

[dependencies]
nom-tracer = "0.2"

Then, use the trace! macro to wrap your parsers:

use nom_tracer::trace;
use nom::bytes::complete::tag;
use nom::IResult;

fn parse_hello(input: &str) -> IResult<&str, &str> {
    trace!(tag("hello"))(input)
}

fn parse_world(input: &str) -> IResult<&str, &str> {
    trace!(tag("world"))(input)
}

fn main() {
    let result = parse_hello("hello world");
    println!("Parse result: {:?}", result);
    println!("Trace:\n{}", nom_tracer::get_trace!());
}

For production builds, you can disable all tracing features to ensure zero overhead:

[dependencies]
nom-tracer = { version = "0.2", default-features = false }

For more detailed examples showcasing various features of nom-tracer, check out the examples folder in the root of the repository. These examples demonstrate real-world usage scenarios and can help you get started with more advanced tracing techniques.

Macros

nom-tracer provides several macros to make tracing easier and more flexible. Here's a detailed explanation of each macro:

trace!

The trace! macro is the primary way to add tracing to your nom parsers. It wraps a parser with tracing functionality, recording its execution path and results.

You can use trace! in several ways:

  1. trace!(parser): Uses the default tag and no context.
  2. trace!(tag, parser): Uses a custom tag and no context.
  3. trace!("context", parser): Uses the default tag and a custom context.
  4. trace!(tag, "context", parser): Uses a custom tag and a custom context.

Here's an example that demonstrates these different usage patterns:

use nom_tracer::trace;
use nom::sequence::tuple;
use nom::character::complete::{alpha1, char, digit1};
use nom::IResult;

fn parse_person(input: &str) -> IResult<&str, (&str, char, &str)> {
    trace!(
        person_parser,
        "Parsing person: name,separator,age",
        tuple((
            trace!(name_parser, "Parsing name", alpha1),
            trace!(char(',')),
            trace!(age_parser, "Parsing age", digit1)
        ))
    )(input)
}

The trace! macro is designed to be flexible and easy to use in various scenarios. It automatically captures the function name where it's used, reducing boilerplate code. When compiling with the trace feature disabled, all trace! macros effectively disappear, leaving no runtime overhead.

silence_tree!

The silence_tree! macro allows you to silence tracing for a subtree of parsers. This is useful for reducing noise in well-tested or less interesting parts of your parser.

You can use silence_tree! in several ways:

  1. silence_tree!(parser): Silences the default tag.
  2. silence_tree!(tag, parser): Silences a specific tag.
  3. silence_tree!("context", parser): Silences the default tag with a context.
  4. silence_tree!(tag, "context", parser): Silences a specific tag with a context.

Here's an example:

use nom_tracer::{trace, silence_tree};
use nom::sequence::tuple;
use nom::character::complete::{alpha1, char, digit1};
use nom::IResult;

fn parse_person(input: &str) -> IResult<&str, (&str, char, &str)> {
    trace!(
        person_parser,
        "Parsing person",
        tuple((
            trace!(alpha1),
            trace!(char(',')),
            silence_tree!("Silenced age parsing", digit1)
        ))
    )(input)
}

Use silence_tree! for parts of your parser that are well-tested or when you want to focus on specific areas of your parser. Silenced subtrees still execute normally but don't generate trace output. The silence_tree! macro is only available when the trace-silencing feature is enabled.

activate_trace! and deactivate_trace!

These macros allow you to dynamically activate or deactivate tracing for either the default tag or a specified tag.

use nom_tracer::{activate_trace, deactivate_trace};

// Activate/deactivate tracing for the default tag
activate_trace!();
deactivate_trace!();

// Activate/deactivate tracing for a custom tag
activate_trace!(my_custom_tag);
deactivate_trace!(my_custom_tag);

These macros are useful for selectively enabling or disabling tracing in different parts of your application. You can use them to control tracing at runtime based on conditions or user input.

reset_trace!

This macro resets the trace for either the default tag or a specified tag, clearing all recorded events.

use nom_tracer::reset_trace;

// Reset trace for the default tag
reset_trace!();

// Reset trace for a custom tag
reset_trace!(my_custom_tag);

reset_trace! is handy when you want to clear previous trace data and start fresh. It can be particularly useful in long-running applications or test suites where you want to reset the trace between different parsing operations.

get_trace!

The get_trace! macro retrieves the trace for either the default tag or a specified tag.

use nom_tracer::{trace, get_trace};
use nom::bytes::complete::tag;

fn main() {
    let _ = trace!(tag("hello"))("hello world");
    let default_trace = get_trace!(); // Gets trace for default tag

    let _ = trace!(my_tag, tag("hello"))("hello world");
    let my_tag_trace = get_trace!(my_tag); // Gets trace for "my_tag"

    println!("Default trace:\n{}", default_trace.unwrap_or_default());
    println!("My tag trace:\n{}", my_tag_trace.unwrap_or_default());
}

get_trace! returns an Option<String>, so you may need to handle the case where no trace is available. It's useful for retrieving trace information for further processing or display.

print_trace!

The print_trace! macro prints the trace for either the default tag or a specified tag directly to the console.

use nom_tracer::{trace, print_trace};
use nom::bytes::complete::tag;

fn main() {
    let _ = trace!(tag("hello"))("hello world");
    print_trace!(); // Prints trace for default tag

    let _ = trace!(my_tag, tag("hello"))("hello world");
    print_trace!(my_tag); // Prints trace for "my_tag"
}

This macro is convenient for quick debugging or when you want to immediately see the trace output. Keep in mind that it prints to stdout, so be mindful of where and when you use it, especially in production environments.

set_max_level!

The set_max_level! macro allows you to set a maximum nesting level for tracing, which can be useful for detecting infinite recursion or excessively deep parser nesting.

use nom_tracer::set_max_level;

// Set maximum nesting level to 10 for the default tag
set_max_level!(Some(10));

// Set maximum nesting level to 5 for a custom tag
set_max_level!(my_custom_tag, Some(5));

// Remove the nesting level limit for the default tag
set_max_level!(None);

// Remove the nesting level limit for a custom tag
set_max_level!(my_custom_tag, None);

Here's an example of how you might use set_max_level! to catch potential infinite recursion:

use nom_tracer::{trace, set_max_level};
use nom::sequence::tuple;
use nom::character::complete::{alpha1, char};
use nom::IResult;

fn recursive_parser(input: &str) -> IResult<&str, &str> {
    trace!(
        tuple((
            alpha1,
            char(','),
            |i| recursive_parser(i)  // This could lead to infinite recursion
        ))
    )(input)
}

fn main() {
    // Set a maximum nesting level of 5
    set_max_level!(Some(5));

    // This will panic if the nesting level exceeds 5
    let _ = recursive_parser("a,b,c,d,e,f,g");
}

[!IMPORTANT] set_max_level! is effective only for infinite recursions that occurs during the actual parsing process. It cannot prevent or detect infinite recursions that happens during parser construction. For example:

fn problematic_parser(input: &str) -> IResult<&str, &str> {
  problematic_parser(input)  // This will cause a stack overflow during parser construction
}

In cases like this, where the recursion occurs before any input is supplied, set_max_level! will not be able to prevent the stack overflow, and no trace will be outputted because the code never reaches the point where tracing begins.

set_max_level! is primarily a debugging tool, useful during development to catch potential issues with recursive parsers or unexpected deep nesting. The appropriate maximum level depends on your parser's structure. Set it high enough to allow for valid deep nesting, but low enough to catch potential infinite recursion. You can set different limits for different tags, allowing for fine-grained control over various parts of your parser. This macro is only available when the trace-max-level feature is enabled.

Cargo Features

  • trace: Enable tracing (default)
  • trace-color: Enable colorized output
  • trace-print: Print trace events in real-time (unbuffered)
  • trace-context: Add context information to error messages (can be used independently of trace)
  • trace-silencing: Enable the silence_tree! macro functionality
  • trace-max-level: Enable maximum nesting level functionality

To enable features, add them to your Cargo.toml:

[dependencies]
nom-tracer = { version = "0.2", features = ["trace-color", "trace-context", "trace-silencing"] }

Note that the trace-context feature can be used independently of the trace feature. This allows you to add context to your nom errors without enabling full tracing functionality.

Context Information

The trace-context feature enhances both trace output and error messages with additional context. This feature can be used in conjunction with the trace feature or independently.

When used with trace, it eliminates the need for nom's context combinator, simplifying your parser code while still providing rich contextual information:

use nom_tracer::trace;
use nom::character::complete::alpha1;
use nom::IResult;

fn parse_username(input: &str) -> IResult<&str, &str> {
    trace!("Parsing username (alphabetic characters only)", alpha1)(input)
}

In this example, if the parser fails, the error message will include the context "Parsing username (alphabetic characters only)", without requiring the use of nom's context combinator.

The trace-context feature can also be used independently of full tracing. This allows you to enhance your nom error messages with additional context without enabling full tracing functionality. To use trace-context without full tracing, configure your Cargo.toml like this:

[dependencies]
nom-tracer = { version = "0.2", default-features = false, features = ["trace-context"] }

This configuration provides enhanced error messages with context information while avoiding the overhead of full tracing in production environments. It's particularly useful when you want more informative error messages but don't need the detailed execution trace that full tracing provides.

Whether used with or without full tracing, the trace-context feature helps in quickly identifying where and why parsing failures occur, significantly improving the debugging experience.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is license

Dependencies

~1MB
~19K SLoC