Skip to main content
Parserator is designed for high performance through fast-path optimizations and specialized combinators. This guide shows you how to write fast parsers.

Fast Path Architecture

Parserator uses a dual-mode execution model:
  1. Slow Path: Immutable state, allocates objects for each step (good for debugging)
  2. Fast Path: Mutable context, minimal allocations (2-10x faster)
By default, parsers support both modes. Use .parseFast() to explicitly use the fast path:
import { string } from 'parserator';

const parser = string('hello');

// Slow path (uses immutable state)
parser.parse('hello world');

// Fast path (uses mutable context)
parser.parseFast('hello world');
Most parsers automatically use the fast path internally. You rarely need to call .parseFast() explicitly.

Optimized Combinators

Parserator provides specialized combinators in src/optimized.ts that are significantly faster than generic equivalents:

manyChar()

import { char, many } from 'parserator';
import { manyChar } from 'parserator/optimized';

// Generic approach (slower)
const genericSpaces = many(char(' '));

// Optimized approach (faster)
const optimizedSpaces = manyChar(' ');

const input = ' '.repeat(1000);
genericSpaces.parseFast(input);  // ~100ms
optimizedSpaces.parseFast(input); // ~10ms (10x faster!)

manyDigit() and many1Digit()

import { digit, many, many1 } from 'parserator';
import { manyDigit, many1Digit } from 'parserator/optimized';

// Generic approach
const genericDigits = many(digit);
const genericDigits1 = many1(digit);

// Optimized approach  
const optimizedDigits = manyDigit();
const optimizedDigits1 = many1Digit();

const number = optimizedDigits1.map(digits => parseInt(digits.join('')));
number.parseFast('123456789'); // Much faster!

manyAlphabet() and many1Alphabet()

import { alphabet, many, many1 } from 'parserator';
import { manyAlphabet, many1Alphabet } from 'parserator/optimized';

// Generic approach
const genericLetters = many(alphabet);

// Optimized approach
const optimizedLetters = manyAlphabet();

const identifier = parser(function* () {
  const first = yield* alphabet;
  const rest = yield* manyAlphabet(); // Optimized!
  return first + rest.join('');
});

skipWhitespace()

import { skipWhitespace } from 'parserator/optimized';
import { parser, string } from 'parserator';

// Fast whitespace skipping
const keyword = parser(function* () {
  yield* skipWhitespace();
  return yield* string('function');
});

keyword.parseFast('   function'); // Very fast!

manyWhitespace()

import { manyWhitespace } from 'parserator/optimized';

// Returns array of whitespace characters
const ws = manyWhitespace();
ws.parseFast('  \t\n  '); // [' ', ' ', '\t', '\n', ' ', ' ']

manyAlphanumeric() and many1Alphanumeric()

import { manyAlphanumeric, many1Alphanumeric } from 'parserator/optimized';

// Parse letters and digits efficiently
const identifier = many1Alphanumeric().map(chars => chars.join(''));
identifier.parseFast('user123'); // 'user123'

Benchmark Results

From benchmarks/optimized-comparison.bench.ts, here are real performance comparisons:
many(char('h')) - slow-path:  100 ops/sec
many(char('h')) - fast-path:  500 ops/sec
manyChar('h') - optimized:   5000 ops/sec

Speedup: 50x faster than slow-path, 10x faster than fast-path

Using takeWhileChar and takeUntilChar

For custom character predicates, use takeWhileChar and takeUntilChar:
import { takeWhileChar, takeUntilChar } from 'parserator/optimized';

// Parse identifier characters efficiently
const identifier = takeWhileChar(ch => 
  (ch >= 'a' && ch <= 'z') || 
  (ch >= 'A' && ch <= 'Z') || 
  (ch >= '0' && ch <= '9') ||
  ch === '_'
);

identifier.parseFast('user_id_123'); // 'user_id_123'

// Parse until a delimiter
const untilComma = takeUntilChar(ch => ch === ',');
untilComma.parseFast('hello,world'); // 'hello'
takeWhileChar returns a string, not an array. It’s optimized to build the string directly.

Other Optimized Combinators

oneOfChars()

import { oneOfChars } from 'parserator/optimized';

// Parse one of several characters
const operator = oneOfChars('+-*/');
operator.parseFast('+'); // '+'
operator.parseFast('*'); // '*'

anyOfStrings()

import { anyOfStrings } from 'parserator/optimized';

// Parse one of several strings (checks longest first)
const keyword = anyOfStrings('function', 'const', 'let', 'var');
keyword.parseFast('function'); // 'function'
keyword.parseFast('const');    // 'const'

takeN()

import { takeN } from 'parserator/optimized';

// Take exactly N characters
const take5 = takeN(5);
take5.parseFast('hello world'); // 'hello'

Performance Tips

1

Use optimized combinators

Replace many(char('x')) with manyChar('x'), many(digit) with manyDigit(), etc.
2

Use takeWhileChar for custom predicates

Instead of many(satisfy(predicate)), use takeWhileChar(predicate).
3

Skip whitespace efficiently

Use skipWhitespace() instead of many(or(space, tab, ...)).
4

Avoid unnecessary allocations

Combine multiple .map() calls into one to reduce intermediate allocations.

Memory Considerations

Context Pooling

Parserator uses object pooling for fast-path contexts (see src/fastpath.ts:227-259):
export class ContextPool {
  private pool: MutableParserContext[] = [];
  private maxSize = 100;

  acquire(source: string): MutableParserContext {
    const ctx = this.pool.pop();
    if (ctx) {
      // Reuse existing context
      ctx.source = source;
      ctx.offset = 0;
      // ... reset other fields
      return ctx;
    }
    return new MutableParserContext(source);
  }

  release(ctx: MutableParserContext): void {
    if (this.pool.length < this.maxSize) {
      this.pool.push(ctx); // Return to pool
    }
  }
}
This reduces GC pressure when parsing many inputs.

Avoiding Allocations

Optimized combinators minimize allocations:
// From src/optimized.ts:10-41
export function manyChar<T extends string>(ch: T): Parser<T[]> {
  return new Parser(
    // ... slow path ...
    ctx => {
      const results: T[] = [];
      const source = ctx.source;
      let offset = ctx.offset;

      // Direct string indexing - no allocations!
      while (offset < source.length && source[offset] === ch) {
        results.push(ch);
        offset++;
      }

      ctx.offset = offset;
      return results;
    }
  );
}
Notice:
  • No State.charAt() calls (which might allocate)
  • Direct array indexing on the source string
  • Single offset mutation instead of creating new state objects

When to Optimize

Premature optimization is the root of all evil. Start with clear, readable code. Only optimize when:
  1. Profiling shows parsing is a bottleneck
  2. You’re parsing large inputs (>1MB)
  3. You’re parsing many small inputs in a loop
  4. You need real-time parsing performance

Real-World Example

Here’s how to optimize a number parser:
import { parser, char, digit, many, many1, or } from 'parserator';

const number = parser(function* () {
  const sign = yield* or(char('-'), char('+').map(() => null));
  const digits = yield* many1(digit);
  const decimal = yield* or(
    parser(function* () {
      yield* char('.');
      return yield* many(digit);
    }),
    Parser.lift(null)
  );
  
  let numStr = digits.join('');
  if (decimal) numStr += '.' + decimal.join('');
  if (sign === '-') numStr = '-' + numStr;
  
  return parseFloat(numStr);
});
Result: 5-10x faster on typical numeric inputs!

Next Steps

  • Examples - See optimized parsers in action
  • API Reference - Explore all optimized combinators
  • Benchmark your own parsers using the patterns in benchmarks/

Build docs developers (and LLMs) love