Skip to content

An implementation of parser combinators for Typescript

License

Notifications You must be signed in to change notification settings

ClaudiuCeia/combine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

combine

Typed parser combinators for TypeScript (Deno + Node). Build small parsers, then compose them into a grammar.

CI JSR npm

Install

Deno (JSR)

import { seq, str } from "jsr:@claudiu-ceia/combine@^0.2.6";

Subpath imports are also supported:

import { recognizeAt } from "jsr:@claudiu-ceia/combine/nondeterministic";
import { createTracer } from "jsr:@claudiu-ceia/combine/perf";

Node (npm)

npm i @claudiu-ceia/combine
import { seq, str } from "@claudiu-ceia/combine";

Quickstart

Parsers are plain functions: (ctx) => Result<T>. The context tracks where you are in the input: { text, index }.

import {
  eof,
  map,
  optional,
  regex,
  seq,
  space,
  str,
  trim,
} from "@claudiu-ceia/combine";

const name = trim(regex(/[^!]+/, "name"));
const hello = map(
  seq(str("Hello,"), optional(space()), name, str("!"), eof()),
  ([, , who]) => who,
);

const result = hello({ text: "Hello, World!", index: 0 });

if (result.success) {
  console.log(result.value); // "World"
} else {
  console.error(result.expected, result.location);
}

Common Building Blocks

The library exports a lot of small pieces; these are the ones you'll likely reach for first:

  • Parsers: str, regex, digit, letter, int, double, space, eof
  • Composition: seq, any, either, oneOf, many, many1, optional
  • Transform: map, mapJoin, trim

If you like learning by examples, start with tests/.

Recursion (Grammars)

When a parser needs to reference itself (directly or indirectly), wrap the reference with lazy:

import { any, lazy, map, type Parser, seq, str } from "@claudiu-ceia/combine";

type Expr = { kind: "paren"; inner: Expr } | { kind: "lit"; value: string };

const lit: Parser<Expr> = map(str("x"), (value) => ({ kind: "lit", value }));
const paren: Parser<Expr> = map(
  seq(str("("), lazy(() => expr), str(")")),
  ([, inner]) => ({ kind: "paren", inner }),
);

// A tiny recursive expression: x | (expr)
const expr: Parser<Expr> = any(lit, paren);

If you're defining a larger mutually-recursive grammar, use createLanguage (src/language.ts) to avoid worrying about declaration order.

If you want better type inference without writing an explicit language type, use createLanguageThis (see docs/guide.md).

Better Errors

For user-facing parsers, wrap important nodes with context(...), and commit to branches with cut(...) (to avoid confusing backtracking). To print failures:

import { formatErrorStack } from "@claudiu-ceia/combine";

if (!result.success) console.error(formatErrorStack(result));

Nondeterministic Recognizers

Most combinators are deterministic: they return a single success or failure. For tokenizer-like use cases where you want multiple simultaneous matches at the same input position, use the nondeterministic/recognizer module:

import { recognizeAt } from "jsr:@claudiu-ceia/combine/nondeterministic";

These combinators can return multiple successes; you must decide how (or whether) to advance the cursor.

More Examples

  • tests/ has the most coverage and real usage patterns
  • examples/ contains small runnable snippets

Guides

If you want the deeper explanations (recursion patterns, createLanguage, error handling, cut vs context, and any vs furthest), see docs/guide.md.

The guide also covers the optional lexer layer (lexeme, symbol, keyword, createLexer) for trivia/comments.

License

MIT © Claudiu Ceia

About

An implementation of parser combinators for Typescript

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •