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:
- Slow Path: Immutable state, allocates objects for each step (good for debugging)
- 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:
manyChar
manyDigit
Complex Parser
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
many(digit) - slow-path: 80 ops/sec
many(digit) - fast-path: 400 ops/sec
manyDigit() - optimized: 4000 ops/sec
Speedup: 50x faster than slow-path, 10x faster than fast-path
Input: " 123456 " (repeated 100x)
Generic (many + regex): 50 ops/sec
Optimized (skipWhitespace): 500 ops/sec
Speedup: 10x faster
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'
Use optimized combinators
Replace many(char('x')) with manyChar('x'), many(digit) with manyDigit(), etc.
Use takeWhileChar for custom predicates
Instead of many(satisfy(predicate)), use takeWhileChar(predicate).
Skip whitespace efficiently
Use skipWhitespace() instead of many(or(space, tab, ...)).
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:
- Profiling shows parsing is a bottleneck
- You’re parsing large inputs (>1MB)
- You’re parsing many small inputs in a loop
- You need real-time parsing performance
Real-World Example
Here’s how to optimize a number parser:
Before (Generic)
After (Optimized)
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);
});
import { parser, char, or, Parser } from 'parserator';
import { many1Digit, manyDigit } from 'parserator/optimized';
const number = parser(function* () {
const sign = yield* or(char('-'), char('+').map(() => null));
const digits = yield* many1Digit(); // Optimized!
const decimal = yield* or(
parser(function* () {
yield* char('.');
return yield* manyDigit(); // Optimized!
}),
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/