Skip to content

Ruby Bindings

Parsanol offers 3 parsing modes for Ruby. All modes return Slice objects with position information by default.

✨ Unified parse() API

One method replaces five confusing options. The unified parse() API is simpler and more intuitive:

# Before 0.4.0 - Too many options!
parse_parslet(g, i)
parse_parslet_with_positions(g, i, cache)
parse_with_transform(g, i, cache)
parse_to_objects(g, i, map)
parse_raw(atom, i)

# After 0.4.0 - One method to rule them all
result = Parsanol::Native.parse(grammar, input)

✨ Position Info is Now Default

All parse methods now return Parsanol::Slice objects with offset, length, line, and column. No special options needed!

πŸš€ Ruby 4.0 Support (Coming Soon)

Parsanol 0.4.1 will support Ruby 4.0 via magnus 0.9.0 and rb-sys HEAD. These packages are not yet released. The workspace Cargo.toml patches rb-sys automatically for ABI compatibility.

The 3 Parsing Modes

All modes return Slice objects with position info. Choose based on your output format needs.

Mode Description Speed Best For
Ruby Pure Ruby parsing 1x Debugging, prototyping
Native Rust parsing, Slice objects ~200-1300x Production use (RECOMMENDED)
JSON Rust parsing, JSON output ~200-1300x APIs, serialization

Benchmark Results

Pattern Type Ruby (i/s) Native (i/s) Speedup
Simple string ~575 ~775,000 1,340x
Sequence (3 parts) ~580 ~530,000 910x
Named capture ~575 ~510,000 880x
Repetition (unnamed) ~560 ~720,000 1,280x
Alternative ~575 ~720,000 1,250x
Calculator expression ~570 ~180,000 315x
Repetition (named) ~575 ~125,000 220x

Speedup varies by pattern complexity. Simple patterns show highest speedups (1000-1300x), while patterns with named repetitions show lower speedups (200-400x) due to transformation overhead.

Batch Parsing Mode

Batch mode supports tagged AST nodes to preserve repetition and sequence semantics across the FFI boundary. Tags like :repetition and :sequence mark nodes for proper Ruby transformation.

🎯 When to Use Batch Mode

Batch mode is ideal when you need correct Parslet-compatible semantics for complex grammars with repetition patterns. It preserves the distinction between true repetitions and wrapper patterns.

# Batch mode with tagged AST nodes
result = Parsanol::Native.parse_batch(grammar, input)

# Results preserve :repetition and :sequence tags
# This enables correct Ruby transformation for complex patterns

Pattern Detection

Pattern Example Result
True Repetition [{x:1}, {x:2}] Array of hashes
Wrapper Pattern {x: {y:1}, z:2} Single hash
Duplicate Labels [{a:{x:1}}, {a:{y:2}}] Last value wins

Slice Objects

All parse results contain Parsanol::Slice objects that preserve source position information:

# Parse result
result = parser.parse("SCHEMA test;", mode: :native)
# => {:name => "test"@7}  # @7 shows byte offset

# Access the slice
slice = result[:name]
slice.to_s            # => "test" (string content)
slice.offset          # => 7 (byte position)
slice.length          # => 4
slice.line_and_column # => [1, 8] (line, column - 1-indexed)

# String comparison works
slice == "test"       # => true

# Extract from original source
slice.extract_from(input)  # => "test"
            

Slice API

⚑ Lazy Line/Column

line_and_column is computed lazily and cached. This means zero overhead if you don't need position info, but it's always available when you do.

class Parsanol::Slice
  # Core attributes (always available, zero cost)
  def content       # String content
  def offset        # Byte offset in original input
  def length        # Length of the slice

  # Lazy computation (computed on first access, then cached)
  def line_and_column  # [line, column] tuple (1-indexed)

  # String compatibility
  def to_s          # Returns content
  def to_str        # Implicit string conversion
  def ==(other)     # Compares content with String or Slice

  # JSON serialization
  def to_json       # Returns { "value" => ..., "offset" => ..., ... }
  def as_json       # Returns hash with position info

  # Utility
  def to_span       # Returns SourceSpan object
  def extract_from(input)  # Extracts content from original input
end
            

Linters

Precise error locations

IDEs

Go-to-definition, highlighting

Expressir

EXPRESS schema parsing

Comment Attachment

Attach remarks by position

Mode: Ruby (Baseline)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   INPUT     β”‚     β”‚  PARSANOL   β”‚     β”‚   OUTPUT    β”‚
β”‚   String    │────▢│  (Ruby)     │────▢│Slice Objectsβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                    SLOW parsing
                    Pure Ruby

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

Pure Ruby parsing. Use for debugging grammar issues or prototyping.

Mode: Native (Recommended)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   INPUT     β”‚     β”‚  PARSANOL   β”‚     β”‚   OUTPUT    β”‚
β”‚   String    │────▢│  (Rust)     │────▢│Slice Objectsβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                    FAST parsing
                    Position tracking included

SPEED: ~200-1300x faster depending on pattern complexity
FEATURES: Slice objects with offset, length, line, column
            

Rust parsing with Slice objects. Recommended for production use. Position info is included by default - no special options needed.

Mode: JSON

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   INPUT     β”‚     β”‚  PARSANOL   β”‚     β”‚   OUTPUT    β”‚
β”‚   String    │────▢│  (Rust)     │────▢│JSON + Pos   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                    FAST parsing
                    Position inline in JSON

SPEED: ~200-1300x faster depending on pattern complexity
FORMAT: {"value": "hello", "offset": 0, "length": 5, ...}
            

Rust parsing with JSON output. Position info is included inline. Perfect for APIs and cross-language interoperability.

When to Use Which Mode

Use Ruby mode when:

  • β€’ Debugging grammar issues
  • β€’ Prototyping new parsers
  • β€’ No Rust extension available

Use Native mode when: (RECOMMENDED)

  • β€’ Production use
  • β€’ Need Ruby objects with position
  • β€’ Building linters, IDEs, Expressir
  • β€’ Want maximum performance

Use JSON mode when:

  • β€’ Building APIs
  • β€’ Serializing parse results
  • β€’ Cross-language interoperability

Raw Tree Interface (Low-Level API)

For maximum throughput, Parsanol::Native.parse_raw skips the parslet-compatibility transform entirely and hands you the engine's raw tagged tree β€” batch-encoded across the FFI boundary and decoded in pure Ruby. This is a stable, documented interface (it is what high-throughput consumers use).

Method Input Returns Use Case
parse grammar, input parslet-compatible tree Default β€” native + transformed
parse_raw grammar, input raw tagged tree Skip the transform β€” ~12-14% faster
parse_batch grammar JSON, input, slice class raw tagged tree Flat u64 FFI protocol, minimal overhead
parse_prefix grammar, input [value, end_pos] Streaming / partial parses

Example: Calculator via the Raw Tree

Batch Decoding Without the Transform

Verify Performance Yourself

All performance claims 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
            

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

Streaming Parser

Parse large files without loading them entirely into memory. The streaming parser processes input in configurable chunks with a sliding window for backtracking support.

Use streaming when: Files are larger than available memory, or input arrives incrementally (network streams, pipes).

Basic Usage

require 'parsanol/streaming'

# Configure chunk size and backtracking window
config = Parsanol::Streaming::ChunkConfig.new(
  chunk_size: 64 * 1024,  # 64 KB chunks
  window_size: 3          # Keep 3 chunks for backtracking
)

# Parse from a file
parser = Parsanol::StreamingParser.new(grammar, config)
result = parser.parse_from_file('large_file.json')

# Parse from any IO object
result = parser.parse_from_io(socket, config)
            

With Captures

Captures work with streaming and persist across chunk boundaries:

# Define grammar with captures
class LogParser < Parsanol::Parser
  rule(:ip) { match('\d').repeat(1,3) >> str('.') >> match('\d').repeat(1,3) >> str('.') >> match('\d').repeat(1,3) >> str('.') >> match('\d').repeat(1,3) }
  rule(:log_line) { capture(:ip, ip) >> str(' ') >> capture(:status, match('\d').repeat(3)) }
  root(:log_line)
end

# Parse large log file with streaming
config = Parsanol::Streaming::ChunkConfig.new(chunk_size: 1024 * 1024, window_size: 2)
parser = Parsanol::StreamingParser.new(LogParser.new.grammar, config)

result = parser.parse_from_file('access.log')
# Access captures from streaming parse
result.captures[:ip].each { |ip| puts ip }
            

StreamingBuilder Callbacks

For maximum performance, use Parsanol::BuilderCallbacks to receive parse events directly without intermediate AST construction:

class MyBuilder
  include Parsanol::BuilderCallbacks

  def on_rule(name, start_pos, end_pos)
    puts "Rule #{name} at #{start_pos}..#{end_pos}"
  end

  def on_capture(name, value)
    puts "Captured #{name}: #{value}"
  end
end

builder = MyBuilder.new
parser.parse_from_file('large.json', builder: builder)
            

Limitations: Backtracking is limited to the window size. Some grammars may require larger windows. The current implementation collects chunks into memory before parsing.

Migration from Parslet

Parsanol provides full Parslet API compatibility. Simply change your require statement:

API Compatibility Matrix

Parslet API Status Notes
str('foo') βœ… Literal string match
match('[0-9]') βœ… Character class
any βœ… Any single character
>> (sequence) βœ… Sequential composition
| (choice) βœ… Ordered choice
.repeat(n, m) βœ… Repetition with bounds
.maybe βœ… Optional
.as(:name) βœ… Label capture
.absent? βœ… Negative lookahead
Parslet::Slice βœ… Parsanol::Slice compatible
Parslet::Transform βœ… Tree transformation
.capture(:name) βœ… Named capture extraction NEW
scope { ... } βœ… Isolated capture context NEW
dynamic { |ctx| ... } βœ… Runtime-determined parsing NEW

New in 1.1.0: The capture, scope, and dynamic atoms are now available in Ruby with full FFI support. See the Captures Guide for details.