Skip to main content

Fast-Path Optimization

Advanced Internal API - This documentation is for advanced users who need to understand or extend Parserator’s internal optimization mechanisms. Most users should not need to interact with this API directly.
The fast-path optimization system is Parserator’s internal mechanism for achieving high performance by avoiding allocations during parsing. Instead of creating immutable state objects on every operation, parsers can opt into a mutable execution model.

Core Concepts

FastPathResult Type

Fast-path parsers return either a parsed value or a failure sentinel:
type FastPathResult<T> = T | typeof PARSE_FAILED;
Key Points:
  • Returns the parsed value T on success
  • Returns PARSE_FAILED symbol on failure
  • Avoids allocating Either objects on every operation
  • Dramatically reduces garbage collection pressure

PARSE_FAILED Sentinel

A unique symbol used to indicate parse failure without allocation:
const PARSE_FAILED = Symbol("PARSE_FAILED");
Why a Symbol?
  • Distinguishes failure from any valid parse result
  • Cannot conflict with user values (including null, undefined, false)
  • Zero allocation cost
  • Type-safe with TypeScript

MutableParserContext

The mutable context is the heart of the fast-path system. Instead of creating new immutable state objects, parsers mutate this context in-place.

Constructor

new MutableParserContext(source: string)
Creates a new parser context for the given input string.

Properties

Source and Position

PropertyTypeDescription
sourcestringThe complete original input string
offsetnumberCurrent byte offset from start (0-indexed)
linenumberCurrent line number (1-indexed, computed lazily)
columnnumberCurrent column number (1-indexed, computed lazily)

Parse State

PropertyTypeDescription
committedbooleanWhether the parser has committed (affects backtracking)
labelStackstring[]Stack of context labels for error reporting

Error Tracking

PropertyTypeDescription
errorParseError | nullThe furthest error encountered
errorOffsetnumberOffset where the error occurred
expectMessagestring | nullCustom error message from .expect()

Methods

Input Inspection

charAt(): string
Gets the character at the current offset without allocating.
const char = ctx.charAt();
if (char === '{') {
  // Parse object
}
Performance: O(1), zero allocation
startsWith(str: string): boolean
Checks if remaining input starts with the given string.
if (ctx.startsWith('true')) {
  ctx.advance(4);
  return true;
}
Performance: O(n) where n is the length of str
remaining(): string
Gets the remaining unparsed portion of the input.
const rest = ctx.remaining();
Allocates a new string - Prefer charAt() or startsWith() for better performance.
isAtEnd(): boolean
Checks if at end of input.
if (ctx.isAtEnd()) {
  return PARSE_FAILED;
}

Position Management

advance(n: number): void
Advances the offset by n characters. Does NOT update line/column (computed lazily on error).
ctx.advance(5); // Move forward 5 characters
computePosition(): void
Computes the actual line and column for the current offset.
Expensive operation - O(offset) complexity. Only call when creating errors.
ctx.computePosition();
console.log(`Error at line ${ctx.line}, column ${ctx.column}`);

Error Recording

recordError(error: ParseError): void
Records an error if it’s further than any previous error. Implements “furthest failure” error reporting.
ctx.recordError({
  type: 'expected',
  expected: 'digit',
  span: ctx.span(1)
});
recordExpect(message: string): void
Records an expect message at the current offset.
ctx.recordExpect('Expected opening brace');

Backtracking

snapshot(): ContextSnapshot
Creates a snapshot of the current context state for backtracking.
const snapshot = ctx.snapshot();
// Try parsing something
if (failed) {
  ctx.restore(snapshot); // Backtrack
}
restore(snapshot: ContextSnapshot): void
Restores context to a previous snapshot.
ctx.restore(snapshot);
ContextSnapshot Type:
type ContextSnapshot = {
  offset: number;
  line: number;
  column: number;
  committed: boolean;
  labelStackLength: number;
  error: ParseError | null;
  errorOffset: number;
  expectMessage: string | null;
};

Utilities

span(length?: number): Span
Creates a span at the current position.
const span = ctx.span(5); // Span covering next 5 characters
toErrorBundle(): ParseErrorBundle
Converts the current error to a ParseErrorBundle. Computes line/column if not already computed.
if (ctx.error) {
  return ctx.toErrorBundle();
}

FastPathParser Interface

Parsers implement this interface to opt into fast-path execution:
interface FastPathParser<T> {
  runFast(ctx: MutableParserContext): FastPathResult<T>;
}

Implementing Fast-Path Parsers

class StringParser implements FastPathParser<string> {
  constructor(private expected: string) {}

  runFast(ctx: MutableParserContext): FastPathResult<string> {
    if (ctx.startsWith(this.expected)) {
      ctx.advance(this.expected.length);
      return this.expected;
    }
    
    ctx.recordError({
      type: 'expected',
      expected: this.expected,
      span: ctx.span()
    });
    return PARSE_FAILED;
  }
}

Type Guard

Check if a parser supports fast-path execution:
function isFastPathParser<T>(parser: any): parser is FastPathParser<T>
Usage:
if (isFastPathParser(parser)) {
  const ctx = new MutableParserContext(input);
  const result = parser.runFast(ctx);
  
  if (result === PARSE_FAILED) {
    // Handle error
  } else {
    // Use result
  }
}

Context Pool

Parserator includes an internal context pool to reuse MutableParserContext instances:
const contextPool = new ContextPool();

Methods

acquire(source: string): MutableParserContext

Gets a context from the pool or creates a new one.
const ctx = contextPool.acquire(input);

release(ctx: MutableParserContext): void

Returns a context to the pool for reuse (if pool not full).
contextPool.release(ctx);

clear(): void

Clears all pooled contexts.
contextPool.clear();
Pool Configuration:
  • Maximum pool size: 100 contexts
  • Automatically resets context state on acquire
  • Thread-safe for single-threaded JavaScript execution

When Parsers Use Fast Paths

Parserator automatically uses fast-path execution when:
  1. Parser implements FastPathParser - The parser has a runFast() method
  2. No debugging enabled - Fast-path skipped in debug mode for better error messages
  3. Normal execution mode - Not in error recovery or special modes
Example: Built-in parsers using fast-path:
  • string() - String matching
  • regex() - Regular expression matching
  • char() - Single character matching
  • map() - Result transformation
  • seq() - Sequential composition
  • alt() - Alternative parsers

Performance Characteristics

Memory

ApproachAllocation per Parse Operation
Traditional (immutable)1-3 objects (state + Either)
Fast-path (mutable)0 objects in success path

Speed

Fast-path execution is typically 2-5x faster than traditional immutable parsing for:
  • Simple parsers (string, char, regex)
  • Sequential compositions with few branches
  • Successful parses with minimal backtracking
Trade-offs:
  • Slightly more complex implementation
  • Mutable state requires careful handling
  • Error messages computed lazily (only when needed)

Best Practices

For Library Users

You typically don’t need to use the fast-path API directly. Parserator handles this automatically.

For Parser Implementers

  1. Implement runFast() for hot paths - Add fast-path support to parsers used in tight loops
  2. Avoid allocations in runFast() - Use charAt() and startsWith() instead of remaining()
  3. Record errors properly - Always call recordError() on failure
  4. Handle backtracking - Use snapshot() and restore() for alternatives
  5. Test both paths - Ensure traditional and fast-path execution produce identical results

Example: Optimized Parser

class WhitespaceParser implements FastPathParser<string> {
  runFast(ctx: MutableParserContext): FastPathResult<string> {
    const start = ctx.offset;
    
    // Fast path: skip whitespace without allocation
    while (!ctx.isAtEnd()) {
      const char = ctx.charAt();
      if (char !== ' ' && char !== '\t' && char !== '\n' && char !== '\r') {
        break;
      }
      ctx.advance(1);
    }
    
    const length = ctx.offset - start;
    if (length === 0) {
      ctx.recordExpect('whitespace');
      return PARSE_FAILED;
    }
    
    // Only allocate result string on success
    return ctx.source.slice(start, ctx.offset);
  }
}

Debugging Fast-Path Code

Since line/column computation is lazy, debugging can be tricky. Use these techniques:

Force Position Computation

ctx.computePosition();
console.log(`At line ${ctx.line}, column ${ctx.column}`);

Add Checkpoints

const snapshot = ctx.snapshot();
console.log('Snapshot:', snapshot);

Disable Fast-Path Temporarily

To compare behavior, implement both traditional and fast-path execution, then test with fast-path disabled.
The fast-path optimization system is transparent to library users but critical for achieving Parserator’s high performance. Understanding it helps when debugging complex parsers or contributing to the library.

Build docs developers (and LLMs) love