Parsanol

Architecture

Parsanol is built as a high-performance Rust core with bindings for Ruby and WebAssembly. This architecture provides native performance while maintaining ease of use.

The 5 Approaches for Ruby Parsing

Parsanol offers 5 different approaches for parsing in Ruby, each moving more work from Ruby to Rust:

┌──────────────────────────────────────────────────────────────────────────────┐
│                    5 APPROACHES FOR RUBY PARSING                             │
│                                                                              │
│   Each approach moves more work from Ruby to Rust, increasing performance.   │
│   Measured with Expressir parsing EXPRESS schemas (22KB file).               │
└──────────────────────────────────────────────────────────────────────────────┘


APPROACH 1: parslet-ruby (BASELINE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   INPUT     │     │  PARSLET    │     │   OUTPUT    │
│   String    │────▶│  (Ruby)     │────▶│  Ruby Hash  │
└─────────────┘     └─────────────┘     └─────────────┘
                          │
                    SLOW parsing
                    Pure Ruby

SPEED: 1x (baseline) - 425s (SRL benchmark: 179K lines)


APPROACH 2: parsanol-ruby
━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   INPUT     │     │  PARSANOL   │     │   OUTPUT    │
│   String    │────▶│  (Ruby)     │────▶│  Ruby Hash  │
└─────────────┘     └─────────────┘     └─────────────┘

SPEED: ~1x (equivalent to Parslet)


APPROACH 3: parsanol-native (Batch)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   INPUT     │     │  PARSANOL   │     │   OUTPUT    │
│   String    │────▶│  (Rust)     │────▶│  Ruby Hash  │
└─────────────┘     └─────────────┘     └─────────────┘
                          │
                    FAST parsing
                    AST via u64 array

SPEED: ~17x faster - 54s (SRL benchmark)


APPROACH 4: parsanol-native (ZeroCopy)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────┐     ┌─────────────────────────┐
│   INPUT     │     │        PARSANOL         │
│   String    │────▶│        (Rust FFI)       │────▶ Ruby Objects
└─────────────┘     │  Direct construction    │
                    └─────────────────────────┘

SPEED: ~17x faster


APPROACH 5: parsanol-native (ZeroCopy + Slice) ← FASTEST
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────┐     ┌────────────────────────────────────┐
│   INPUT     │     │            PARSANOL                │
│   String    │────▶│            (Rust)                  │────▶ Slice Objects
└─────────────┘     │  Zero-copy + Source positions      │
                    └────────────────────────────────────┘
                          │
                    FASTEST parsing
                    Source position tracking

SPEED: ~17x faster - 54s (17x vs baseline)
FEATURES: Preserves source positions for linters, IDEs
            

New: Slice Support - ZeroCopy + Slice mode preserves source positions (offset, length) for each parsed element. This is essential for tools like linters, IDEs, and Expressir that need to track where in the source code each element came from.

Evidence-Based: These results are measured with Expressir parsing real EXPRESS schemas. Run the benchmarks yourself: bundle exec ruby benchmark/run_all.rb --quick

Evidence-Based Benchmarks

These are actual benchmark results from Expressir parsing the SRL benchmark (135 schemas, 179K lines). This represents a real-world production workload.

Mode Time Speedup Notes
Ruby (Parslet) 425,000 ms 1x (baseline) Pure Ruby parsing (SRL benchmark)
Native Batch (u64) 54,400 ms 17x faster AST via u64 array transfer
Native ZeroCopy (Slice) 54,400 ms 17x faster Zero-copy with source positions

JSON Parsing Benchmarks

Input Size parslet-ruby parsanol-native parsanol-zerocopy
tiny (76 bytes) 1,156 iter/s 4,745 iter/s 33,000+ iter/s
small (791 bytes) 111 iter/s 469 iter/s 3,200+ iter/s
medium (8KB) 9.3 iter/s 47.2 iter/s 270+ iter/s

Note: Results vary based on hardware, Ruby version, and input complexity. Always run benchmarks on your own system for accurate comparisons.

Slice Support for Source Positions

The ZeroCopy + Slice mode preserves source position information, which is essential for tools that need to track where in the source code each element came from.

# Before (plain strings):
[{"word"=>"hello"}, " ", {"name"=>"world"}]

# After (Slice objects with position info):
[{"word"=>"hello"@0}, " "@5, {"name"=>"world"@6}]

# The @N notation shows the byte offset in the original input
            

Parslet::Slice Compatible

Parsanol::Slice objects are compatible with Parslet::Slice, making migration seamless for existing code that depends on source position tracking.

Offset + Length

Each slice contains the byte offset and length, allowing tools to precisely locate and highlight source code elements.

Use Cases

Linters

Show precise error locations with line/column information derived from byte offsets.

IDE Integration

Enable go-to-definition, find-references, and syntax highlighting based on parsed structure.

Code Generation

Generate output that preserves mappings back to original source locations.

When to Use Which Approach

Approach Best For Trade-offs
parslet-ruby Existing Parslet code, maximum compatibility Slowest, but most reliable
parsanol-native (Batch) Need Ruby objects, good performance AST transfer overhead
ZeroCopy + Slice Need source positions (linters, IDEs) Best overall - fastest + position tracking

Rust Core Engine

The Rust core provides the parsing engine with maximum performance:

Packrat Parser

Memoization-based parsing guarantees O(n) time complexity for PEG grammars. No exponential backtracking, predictable performance.

Arena Allocation

All AST nodes are allocated in a single arena. Zero individual allocations, perfect cache locality, instant cleanup.

SIMD Acceleration

Uses memchr for fast substring search, AHash for fast hashing, SmallVec for stack-allocated small collections.

Zero-Copy Slices

InputRef nodes return slice references with offset/length instead of copying strings, preserving source position information.

Parslet Compatibility Layer

For existing Parslet users, we provide a drop-in compatibility layer:

┌─────────────────────────────────────────────────────────────────────┐
│                    PARSLET COMPATIBILITY LAYER                      │
│                                                                     │
│  require 'parslet'           →        require 'parsanol/parslet'    │
│                                                                     │
│  class MyParser < Parslet::Parser    class MyParser < Parsanol::    │
│    rule(:expr) { ... }                 Parslet::Parser              │
│  end                                     rule(:expr) { ... }        │
│                                        end                          │
│                                                                     │
│  NO CODE CHANGES REQUIRED!                                          │
│                                                                     │
│  Benefits:                                                          │
│  • Same API as Parslet                                              │
│  • Same output format (including Slice objects)                     │
│  • Rust backend by default - ~17x faster                             │
└─────────────────────────────────────────────────────────────────────┘
            

Running Benchmarks Yourself

All performance claims on this page can be verified by running the benchmarks on your own machine:

# Clone and setup
git clone https://github.com/parsanol/parsanol-ruby
cd parsanol-ruby
bundle install

# Build native extension (requires Rust)
bundle exec rake compile

# Run quick benchmarks (skips large inputs)
bundle exec ruby benchmark/run_all.rb --quick

# Run all benchmarks
bundle exec ruby benchmark/run_all.rb

# View the approaches diagram
cat benchmark/APPROACHES.md
            

Benchmark results are saved to benchmark/reports/ as JSON files for comparison over time.