Skip to content

Parser Writing Tutorial

Learn to build parsers from scratch with Parsanol.

Introduction

A parser is a program that analyzes text according to a set of rules (grammar) and produces a structured representation of that text. Parsanol is a parser combinator library that lets you build parsers by combining smaller parsers together.

This tutorial will take you from zero to building real parsers.

Chapter 1: Basic Atoms

Atoms are the fundamental building blocks of parsers. They match specific patterns in the input text.

str() - Match Literal String

// Match exact string "hello"
let hello = str("hello");
let result = hello.parse("hello");
assert!(result.is_ok());
# Match exact string "hello"
hello = str("hello")
result = hello.parse("hello")
# => #<Parslet::Slice:0x... @value="hello">

re() - Regular Expression

// Match regex pattern [a-z]+ (one or more letters)
let word = re(r"[a-z]+");
let result = word.parse("hello");
assert!(result.is_ok());
# Match regex pattern [a-z]+ (one or more letters)
word = re("[a-z]+")
result = word.parse("hello")
# => #<Parslet::Slice:0x... @value="hello">

any() - Any Character

// Match any single character
let any_char = any();
let result = any_char.parse("x");
assert!(result.is_ok());
# Match any single character
any_char = any
result = any_char.parse("x")
# => #<Parslet::Slice:0x... @value="x">

match() - Character Class

// Match character class [0-9]
let digit = match("[0-9]");
# Match character class [0-9]
digit = match("[0-9]")

Chapter 2: Combinators

Combinators combine atoms (and other combinators) to build more complex parsers.

Sequence (then or >>)

Match multiple patterns in order.

// Sequence: match "hello" then " " then "world"
let greeting = str("hello") >> str(" ") >> str("world");
# Sequence: match "hello" then " " then "world"
greeting = str("hello") >> str(" ") >> str("world")

Choice (or or |)

Try each alternative in order.

// Choice: match "yes" or "no"
let yes_no = str("yes") | str("no");
# Choice: match "yes" or "no"
yes_no = str("yes") | str("no")

Repetition (repeat)

Match patterns zero or more times.

// Zero or more: match letters
let letters = re(r"[a-z]+").repeat();
// One or more: match digits
let digits = re(r"[0-9]+").repeat(1);
// Range: 1-3 times
let few = re(r"[a-z]").repeat(1, 3);
# Zero or more: match letters
letters = re("[a-z]+").repeat
# One or more: match digits
digits = re("[0-9]+").repeat(1)
# Range: 1-3 times
few = re("[a-z]").repeat(1, 3)

Optional (maybe)

Match a pattern zero or one time.

// Optional: match "Mr." or nothing
let title = str("Mr.").maybe();
# Optional: match "Mr." or nothing
title = str("Mr.").maybe

Chapter 3: Captures & Labels

Labels (.as)

Attach names to matched content for the parse tree.

// Label captured content
let word = re(r"[a-z]+").as("word");
// Parse tree: {word: "hello"}
# Label captured content
word = re("[a-z]+").as(:word)
# Parse tree: {:word => "hello"}

Lookahead

Check what's ahead without consuming input.

// Positive lookahead: must have "http" ahead
let http_url = str("http").present() >> re(r".*");

// Negative lookahead: not followed by "end"
let safe_end = str("end").absent() >> re(r".+");
# Positive lookahead: must have "http" ahead
http_url = str("http").present >> re(".*")

# Negative lookahead: not followed by "end"
safe_end = str("end").absent >> re(".+")

Chapter 4: Building a Real Parser

Let's build a complete JSON parser step by step.

Step 1: Basic Structure

// JSON Parser - Step 1: Basic structure

let json = choice!(vec![
    str("null").map(|_| JsonValue::Null),
    str("true").map(|_| JsonValue::Bool(true)),
    str("false").map(|_| JsonValue::Bool(false)),
]);
# JSON Parser - Step 1: Basic structure

class JsonParser < Parsanol::Parser
  root :value

  rule(:value) {
    str("null").as(:null) |
    str("true").as(:true) |
    str("false").as(:false)
  }
end

Step 2: Numbers and Strings

// JSON Parser - Step 2: Numbers and Strings

let number = re(r"-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?");
let string = str("\"") >> re(r#"[^"\\]*(\\.[^"\\]*)*"#).as("string") >> str("\"");
# JSON Parser - Step 2: Numbers and Strings

rule(:number) {
  match["0-9"].repeat(1).as(:num)
}

rule(:string) {
  str('"') >> (match['^"\\\\'] | str('\\\\') >> any).repeat >> str('"')
}

Step 3: Arrays and Objects

// JSON Parser - Step 3: Arrays and Objects

let array = str("[") >> value.repeat(0) >> str("]");
let object = str("{") >> (string >> str(":") >> value).repeat(0) >> str("}");
# JSON Parser - Step 3: Arrays and Objects

rule(:array) {
  str("[") >> (value >> str(",") >> value).repeat >> str("]")
}

rule(:object) {
  str("{") >> (string >> str(":") >> value).repeat >> str("}")
}

Step 4: Complete Parser

# Complete JSON Parser

require 'parsanol'

class JsonParser < Parsanol::Parser
  root :value

  rule(:spaces) { match['\\s'].repeat }

  rule(:value) {
    spaces >> (
      object | array | string | number | literal
    ) >> spaces
  }

  rule(:object) {
    str('{') >> (
      entry >> (str(',') >> entry).repeat
    ).maybe >> str('}')
  end

  rule(:entry) {
    string.as(:key) >> str(':') >> value.as(:val)
  }

  rule(:array) {
    str('[') >> (
      value >> (str(',') >> value).repeat
    ).maybe >> str(']')
  end

  rule(:string) {
    str('"') >> (
      str('\\\\') >> any | str('"').absent? >> any
    ).repeat.as(:string) >> str('"')
  end

  rule(:number) {
    str('-').maybe >> (
      str('0') | match['1-9'] >> match['0-9'].repeat
    ) >> (
      str('.') >> match['0-9'].repeat
    ).maybe >> (
      match['eE'] >> str('+-').maybe >> match['0-9'].repeat
    ).maybe
  end

  rule(:literal) {
    str('null').as(:null) |
    str('true').as(:true) |
    str('false').as(:false)
  }
end

# Test
parser = JsonParser.new
puts parser.parse('{"name": "John", "age": 30}').inspect

Chapter 5: Transformations

Transforms convert the generic parse tree into your domain objects.

Basic Transform

// Transform: Convert parse tree to typed value

let transform = TransformBuilder::new()
    .rule("num", |s: &str| {
        Ok(Value::Number(s.parse()?))
    })
    .build();
# Transform: Convert parse tree to typed value

class JsonTransform < Parsanol::Transform
  rule(string: simple(:s)) { s.to_s }
  rule(num: simple(:n)) { Integer(n) }
  rule(null: simple(:n)) { nil }
  rule(true: simple(:t)) { true }
  rule(false: simple(:f)) { false }
end

Recursive Transform

// Recursive transform for nested structures

let transform = TransformBuilder::new()
    .rule("num", |s: &str| {
        Ok(Value::Number(s.parse()?))
    })
    .rule("array", |arr: Vec<Value>| {
        Ok(Value::Array(arr))
    })
    .rule("object", |pairs: Vec<(String, Value)>| {
        Ok(Value::Object(pairs.into_iter().collect()))
    })
    .build();
# Recursive transform for nested structures

class JsonTransform < Parsanol::Transform
  rule(string: simple(:s)) { s.to_s }
  rule(num: simple(:n)) { Integer(n) }
  rule(null: simple(:n)) { nil }
  rule(true: simple(:t)) { true }
  rule(false: simple(:f)) { false }

  # Recursive rules
  rule(array: sequence(:arr)) {
    arr.map { |v| transform_value(v) }
  }

  rule(object: sequence(:pairs)) {
    Hash[pairs.map { |k, v| [k, transform_value(v)] }]
  end

  def transform_value(tree)
    apply(tree)
  end
end

Best Practices

1. Start Simple

Build your parser incrementally. Test each rule before combining them.

2. Handle Whitespace

Create a reusable `space?` rule and use it throughout your grammar.

3. Name Your Rules

Use `.as(:name)` to label captures for easier debugging.

4. Use Two-Stage Parsing

Separate parsing (syntax) from transformation (semantics).