parserator
Version:
An elegant parser combinators library for Typescript
1 lines • 141 kB
Source Map (JSON)
{"version":3,"sources":["../src/error-formatter.ts","../src/index.ts","../src/either.ts","../src/errors.ts","../src/state.ts","../src/parser.ts","../src/combinators.ts","../src/hints.ts","../src/utils.ts"],"sourcesContent":["import type { ParseError, ParseErrorBundle } from \"./errors\";\n\nexport type ErrorFormat = \"plain\" | \"ansi\" | \"html\" | \"json\";\n\nexport type ErrorFormatterOptions = {\n maxContextLines?: number;\n showHints?: boolean;\n colorize?: boolean;\n showContext?: boolean;\n tabSize?: number;\n};\n\n/**\n * Formats ParseErrorBundle into human-readable error messages with multiple output formats.\n * Supports plain text, ANSI colors, HTML, and JSON formats.\n */\nexport class ErrorFormatter {\n private _format: ErrorFormat;\n private options: ErrorFormatterOptions;\n\n constructor(\n format: ErrorFormat = \"plain\",\n options: ErrorFormatterOptions = {}\n ) {\n this._format = format;\n // Set default options\n this.options = {\n maxContextLines: 3,\n showHints: true,\n colorize: true,\n showContext: true,\n tabSize: 2,\n ...options\n };\n }\n\n /**\n * Format a ParseErrorBundle into a string based on the configured format.\n *\n * @param bundle - The error bundle to format\n * @returns Formatted error message string\n */\n format(bundle: ParseErrorBundle): string {\n switch (this._format) {\n case \"ansi\":\n return this.formatAnsi(bundle);\n case \"html\":\n return this.formatHtml(bundle);\n case \"json\":\n return this.formatJson(bundle);\n default:\n return this.formatPlain(bundle);\n }\n }\n\n /**\n * Format error with ANSI color codes for terminal output.\n */\n private formatAnsi(bundle: ParseErrorBundle): string {\n const primary = bundle.primary;\n const lines = bundle.source.split(\"\\n\");\n const errorLine = lines[primary.span.line - 1] || \"\";\n\n const parts: string[] = [];\n\n // Error header with location\n parts.push(\n `\\x1b[31mError\\x1b[0m at line ${primary.span.line}, column ${primary.span.column}:`\n );\n\n // Show context lines if enabled\n if (this.options.showContext && this.options.maxContextLines! > 0) {\n const contextLines = this.getContextLines(\n lines,\n primary.span.line - 1,\n this.options.maxContextLines!\n );\n parts.push(...contextLines.map(line => ` ${line}`));\n } else {\n // Just show the error line\n parts.push(` ${errorLine}`);\n }\n\n // Add pointer arrow (accounting for line prefix)\n const linePrefix = ` > ${primary.span.line.toString().padStart(0, \" \")} | `;\n const adjustedColumn = primary.span.column + linePrefix.length - 2; // -2 for the \" \" we add\n const pointer = this.createPointer(adjustedColumn, primary.span.length);\n parts.push(` ${pointer}`);\n\n // Error message\n parts.push(this.formatErrorMessage(primary));\n\n // Add hints if available\n const hints = this.getHints(primary);\n if (this.options.showHints && hints.length > 0) {\n parts.push(\"\");\n for (const hint of hints) {\n parts.push(` \\x1b[36mDid you mean: ${hint}?\\x1b[0m`);\n }\n }\n\n // Add context stack if available\n if (\n this.options.showContext &&\n primary.context &&\n primary.context.length > 0\n ) {\n parts.push(\"\");\n parts.push(` \\x1b[90mContext: ${primary.context.join(\" > \")}\\x1b[0m`);\n }\n\n return parts.join(\"\\n\");\n }\n\n /**\n * Format error as plain text without colors.\n */\n private formatPlain(bundle: ParseErrorBundle): string {\n const primary = bundle.primary;\n const lines = bundle.source.split(\"\\n\");\n const errorLine = lines[primary.span.line - 1] || \"\";\n\n const parts: string[] = [];\n\n // Error header\n parts.push(\n `Error at line ${primary.span.line}, column ${primary.span.column}:`\n );\n\n // Show context lines\n if (this.options.showContext && this.options.maxContextLines! > 0) {\n const contextLines = this.getContextLines(\n lines,\n primary.span.line - 1,\n this.options.maxContextLines!\n );\n parts.push(...contextLines.map(line => ` ${line}`));\n } else {\n parts.push(` ${errorLine}`);\n }\n\n // Add pointer (accounting for line prefix)\n const linePrefix = ` > ${primary.span.line.toString()} | `;\n const adjustedColumn = primary.span.column + linePrefix.length - 2; // -2 for the \" \" we add\n const pointer = this.createPointer(\n adjustedColumn,\n primary.span.length,\n false\n );\n parts.push(` ${pointer}`);\n\n // Error message\n parts.push(this.formatErrorMessage(primary, false));\n\n // Add hints\n const hints = this.getHints(primary);\n if (this.options.showHints && hints.length > 0) {\n parts.push(\"\");\n for (const hint of hints) {\n parts.push(` Did you mean: ${hint}?`);\n }\n }\n\n // Add context\n if (\n this.options.showContext &&\n primary.context &&\n primary.context.length > 0\n ) {\n parts.push(\"\");\n parts.push(` Context: ${primary.context.join(\" > \")}`);\n }\n\n return parts.join(\"\\n\");\n }\n\n /**\n * Format error as HTML with styling.\n */\n private formatHtml(bundle: ParseErrorBundle): string {\n const primary = bundle.primary;\n const lines = bundle.source.split(\"\\n\");\n const errorLine = lines[primary.span.line - 1] || \"\";\n\n const parts: string[] = [];\n\n parts.push('<div class=\"parse-error\">');\n\n // Error header\n parts.push(\n ` <div class=\"error-header\">Error at line ${primary.span.line}, column ${primary.span.column}:</div>`\n );\n\n // Code context\n parts.push(' <div class=\"error-context\">');\n if (this.options.showContext && this.options.maxContextLines! > 0) {\n const contextLines = this.getContextLines(\n lines,\n primary.span.line - 1,\n this.options.maxContextLines!\n );\n for (const line of contextLines) {\n parts.push(\n ` <div class=\"context-line\">${this.escapeHtml(line)}</div>`\n );\n }\n } else {\n parts.push(\n ` <div class=\"error-line\">${this.escapeHtml(errorLine)}</div>`\n );\n }\n\n // Pointer (accounting for line prefix in plain text representation)\n const pointer = this.createPointer(\n primary.span.column,\n primary.span.length,\n false\n );\n parts.push(` <div class=\"error-pointer\">${pointer}</div>`);\n parts.push(\" </div>\");\n\n // Error message\n parts.push(\n ` <div class=\"error-message\">${this.escapeHtml(this.formatErrorMessage(primary, false))}</div>`\n );\n\n // Hints\n const hints = this.getHints(primary);\n if (this.options.showHints && hints.length > 0) {\n parts.push(' <div class=\"error-hints\">');\n for (const hint of hints) {\n parts.push(\n ` <div class=\"hint\">Did you mean: <span class=\"suggestion\">${this.escapeHtml(hint)}</span>?</div>`\n );\n }\n parts.push(\" </div>\");\n }\n\n // Context\n if (\n this.options.showContext &&\n primary.context &&\n primary.context.length > 0\n ) {\n parts.push(\n ` <div class=\"error-context-stack\">Context: ${primary.context.map(c => `<span class=\"context-item\">${this.escapeHtml(c)}</span>`).join(\" > \")}</div>`\n );\n }\n\n parts.push(\"</div>\");\n\n return parts.join(\"\\n\");\n }\n\n /**\n * Format error as JSON for programmatic consumption.\n */\n private formatJson(bundle: ParseErrorBundle): string {\n const primary = bundle.primary;\n const lines = bundle.source.split(\"\\n\");\n\n const contextLines =\n this.options.showContext ?\n this.getContextLines(\n lines,\n primary.span.line - 1,\n this.options.maxContextLines!\n )\n : [lines[primary.span.line - 1] || \"\"];\n\n return JSON.stringify(\n {\n error: {\n type: primary.tag,\n message: this.getPlainErrorMessage(primary),\n location: {\n line: primary.span.line,\n column: primary.span.column,\n offset: primary.span.offset,\n length: primary.span.length\n },\n context: { lines: contextLines, stack: primary.context || [] },\n hints: this.getHints(primary),\n source: bundle.source\n },\n allErrors: bundle.errors.map(err => ({\n type: err.tag,\n location: {\n line: err.span.line,\n column: err.span.column,\n offset: err.span.offset,\n length: err.span.length\n },\n context: err.context || [],\n ...(err.tag === \"Expected\" && { items: err.items, found: err.found }),\n ...(err.tag === \"Unexpected\" && { found: err.found }),\n ...(err.tag === \"Fatal\" && { message: err.message })\n }))\n },\n null,\n this.options.tabSize\n );\n }\n\n /**\n * Format the error message based on error type.\n */\n private formatErrorMessage(\n error: ParseError,\n useColors: boolean = true\n ): string {\n const red = useColors ? \"\\x1b[31m\" : \"\";\n const yellow = useColors ? \"\\x1b[33m\" : \"\";\n const reset = useColors ? \"\\x1b[0m\" : \"\";\n\n switch (error.tag) {\n case \"Expected\":\n const foundText = error.found ? `, found ${error.found}` : \"\";\n return ` ${yellow}Expected:${reset} ${error.items.join(\" or \")}${foundText}`;\n case \"Unexpected\":\n return ` ${red}Unexpected:${reset} ${error.found}`;\n case \"Custom\":\n return ` ${error.message}`;\n case \"Fatal\":\n return ` ${red}Fatal:${reset} ${error.message}`;\n }\n }\n\n /**\n * Get plain error message without formatting.\n */\n private getPlainErrorMessage(error: ParseError): string {\n switch (error.tag) {\n case \"Expected\":\n const foundText = error.found ? `, found ${error.found}` : \"\";\n return `Expected: ${error.items.join(\" or \")}${foundText}`;\n case \"Unexpected\":\n return `Unexpected: ${error.found}`;\n case \"Custom\":\n return error.message;\n case \"Fatal\":\n return `Fatal: ${error.message}`;\n }\n }\n\n /**\n * Create a pointer/caret pointing to the error location.\n */\n private createPointer(\n column: number,\n length: number = 1,\n useColors: boolean = true\n ): string {\n const spaces = \" \".repeat(Math.max(0, column - 1));\n const carets = \"^\".repeat(Math.max(1, length));\n const red = useColors ? \"\\x1b[31m\" : \"\";\n const reset = useColors ? \"\\x1b[0m\" : \"\";\n return `${spaces}${red}${carets}${reset}`;\n }\n\n /**\n * Get context lines around the error location.\n */\n private getContextLines(\n allLines: string[],\n errorLineIndex: number,\n maxLines: number\n ): string[] {\n const contextRadius = Math.floor(maxLines / 2);\n const startLine = Math.max(0, errorLineIndex - contextRadius);\n const endLine = Math.min(\n allLines.length - 1,\n errorLineIndex + contextRadius\n );\n\n const contextLines: string[] = [];\n for (let i = startLine; i <= endLine; i++) {\n const lineNum = i + 1;\n const lineContent = allLines[i] || \"\";\n const isErrorLine = i === errorLineIndex;\n const prefix = isErrorLine ? \">\" : \" \";\n const paddedLineNum = lineNum.toString().padStart(3, \" \");\n contextLines.push(`${prefix} ${paddedLineNum} | ${lineContent}`);\n }\n\n return contextLines;\n }\n\n /**\n * Escape HTML entities.\n */\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n }\n\n /**\n * Create a new formatter with different options.\n */\n withOptions(options: Partial<ErrorFormatterOptions>): ErrorFormatter {\n return new ErrorFormatter(this._format, { ...this.options, ...options });\n }\n\n /**\n * Create a new formatter with a different format.\n */\n withFormat(format: ErrorFormat): ErrorFormatter {\n return new ErrorFormatter(format, this.options);\n }\n\n /**\n * Get hints from an error, handling the union type safely.\n */\n private getHints(error: ParseError): string[] {\n // if (error.tag === \"Custom\" && error.hints) {\n // return error.hints;\n // }\n if (error.tag === \"Unexpected\" && error.hints) {\n return error.hints;\n }\n return [];\n }\n}\n\n/**\n * Convenience functions for quick formatting.\n */\nexport const formatError = {\n plain: (bundle: ParseErrorBundle, options?: ErrorFormatterOptions) =>\n new ErrorFormatter(\"plain\", options).format(bundle),\n ansi: (bundle: ParseErrorBundle, options?: ErrorFormatterOptions) =>\n new ErrorFormatter(\"ansi\", options).format(bundle),\n html: (bundle: ParseErrorBundle, options?: ErrorFormatterOptions) =>\n new ErrorFormatter(\"html\", options).format(bundle),\n json: (bundle: ParseErrorBundle, options?: ErrorFormatterOptions) =>\n new ErrorFormatter(\"json\", options).format(bundle)\n};\n","export * from \"./combinators\";\n// export * from \"./debug\";\nexport * from \"./either\";\nexport * from \"./error-formatter\";\nexport * from \"./hints\";\nexport * from \"./parser\";\nexport * from \"./errors\";\nexport * from \"./state\";\nexport * from \"./types\";\nexport * from \"./utils\";\n","export type Either<R, L> = Left<L, R> | Right<R, L>;\n\nexport class Left<L, R = never> {\n readonly _tag = \"Left\";\n constructor(public readonly left: L) {}\n *[Symbol.iterator](): Generator<Either<R, L>, R, any> {\n return yield this;\n }\n}\n\nexport class Right<R, L = any> {\n readonly _tag = \"Right\";\n constructor(public readonly right: R) {}\n *[Symbol.iterator](): Generator<Either<R, L>, R, any> {\n return yield this;\n }\n}\n\nexport const Either = {\n left<L, R = never>(l: L): Either<R, L> {\n return new Left(l);\n },\n\n right<R, L = never>(r: R): Either<R, L> {\n return new Right(r);\n },\n\n isLeft<R, L>(either: Either<R, L>): either is Left<L, R> {\n return either._tag === \"Left\";\n },\n\n isRight<R, L>(either: Either<R, L>): either is Right<R, L> {\n return either._tag === \"Right\";\n },\n\n match<R, L, T>(onLeft: (left: L) => T, onRight: (right: R) => T) {\n return (either: Either<R, L>): T => {\n if (Either.isLeft(either)) {\n return onLeft(either.left);\n }\n return onRight(either.right);\n };\n },\n\n gen<R, L>(f: () => Generator<Either<any, L>, R, any>): Either<R, L> {\n const iterator = f();\n let current = iterator.next();\n\n while (!current.done) {\n const either = current.value;\n if (Either.isLeft(either)) {\n return either;\n }\n current = iterator.next(either.right);\n }\n\n return Either.right(current.value);\n }\n};\n","/**\n * Represents a location span in source code with position and size information.\n * @example\n * ```typescript\n * const span: Span = {\n * offset: 10,\n * length: 5,\n * line: 2,\n * column: 3\n * };\n * ```\n */\nexport type Span = {\n /** Byte offset from the start of the source */\n offset: number;\n /** Length of the span in bytes */\n length: number;\n /** Line number (1-indexed) */\n line: number;\n /** Column number (1-indexed) */\n column: number;\n};\n\n/**\n * Creates a Span from parser state and optional length.\n * @param state - Parser state containing position information\n * @param length - Length of the span (defaults to 0)\n * @returns A new Span object\n * @example\n * ```typescript\n * const state = { pos: { offset: 10, line: 2, column: 3 } };\n * const span = Span(state, 5);\n * // Returns: { offset: 10, length: 5, line: 2, column: 3 }\n * ```\n */\nexport function Span(\n state: { pos: { offset: number; line: number; column: number } },\n length: number = 0\n): Span {\n return {\n offset: state.pos.offset,\n length,\n line: state.pos.line,\n column: state.pos.column\n };\n}\n\ntype ExpectedParseError = {\n tag: \"Expected\";\n span: Span;\n items: string[];\n context: string[];\n found?: string;\n};\n\ntype UnexpectedParseError = {\n tag: \"Unexpected\";\n span: Span;\n found: string;\n context: string[];\n hints?: string[];\n};\n\ntype CustomParseError = {\n tag: \"Custom\";\n span: Span;\n message: string;\n hints?: string[];\n context: string[];\n};\n\ntype FatalParseError = {\n tag: \"Fatal\";\n span: Span;\n message: string;\n context: string[];\n};\n\n/**\n * Union type representing all possible parsing errors.\n * Each error type has a discriminant tag for pattern matching.\n * @example\n * ```typescript\n * function handleError(error: ParseError) {\n * switch (error.tag) {\n * case \"Expected\":\n * console.log(`Expected ${error.items.join(\" or \")}`);\n * break;\n * case \"Unexpected\":\n * console.log(`Unexpected ${error.found}`);\n * break;\n * // ... handle other cases\n * }\n * }\n * ```\n */\nexport type ParseError =\n | CustomParseError\n | ExpectedParseError\n | UnexpectedParseError\n | FatalParseError;\n\n/**\n * Factory functions for creating different types of ParseError objects.\n * Provides a convenient API for constructing errors without manually setting tags.\n * @example\n * ```typescript\n * const span = Span(state);\n *\n * // Create an expected error\n * const expectedError = ParseError.expected({\n * span,\n * items: [\"identifier\", \"keyword\"],\n * context: [\"function declaration\"],\n * found: \"number\"\n * });\n *\n * // Create a custom error\n * const customError = ParseError.custom({\n * span,\n * message: \"Invalid syntax\",\n * context: [\"expression\"],\n * hints: [\"Try using parentheses\"]\n * });\n * ```\n */\nexport const ParseError = {\n /** Creates an ExpectedParseError for when specific tokens were expected */\n expected: (params: Omit<ExpectedParseError, \"tag\">): ExpectedParseError => ({\n tag: \"Expected\",\n ...params\n }),\n /** Creates an UnexpectedParseError for when an unexpected token was found */\n unexpected: (\n params: Omit<UnexpectedParseError, \"tag\">\n ): UnexpectedParseError => ({\n tag: \"Unexpected\",\n ...params\n }),\n /** Creates a CustomParseError with a custom message */\n custom: (params: Omit<CustomParseError, \"tag\">): CustomParseError => ({\n tag: \"Custom\",\n ...params\n }),\n /** Creates a FatalParseError that cannot be recovered from */\n fatal: (params: Omit<FatalParseError, \"tag\">): FatalParseError => ({\n tag: \"Fatal\",\n ...params\n })\n};\n\n/**\n * A collection of parsing errors with formatting and analysis capabilities.\n * Automatically determines the primary (furthest) error for reporting.\n * @example\n * ```typescript\n * const errors = [\n * ParseError.expected({ span: spanAt10, items: [\"(\"], context: [] }),\n * ParseError.unexpected({ span: spanAt15, found: \")\", context: [] })\n * ];\n * const bundle = new ParseErrorBundle(errors, sourceCode);\n *\n * console.log(bundle.toString()); // Shows the furthest error\n * console.log(bundle.format(\"ansi\")); // Formatted with colors\n * ```\n */\nexport class ParseErrorBundle {\n /**\n * Creates a new ParseErrorBundle.\n * @param errors - Array of parsing errors\n * @param source - The original source code being parsed\n * @returns {ParseErrorBundle} A new ParseErrorBundle instance containing the errors and source\n */\n constructor(\n public errors: ParseError[],\n public source: string\n ) {}\n\n /**\n * Gets the primary error (the one that occurred furthest in the input).\n * This is typically the most relevant error to show to the user.\n * @returns {ParseError} The error with the highest offset position\n */\n get primary(): ParseError {\n return this.errors.reduce((furthest, current) =>\n current.span.offset > furthest.span.offset ? current : furthest\n );\n }\n\n /**\n * Gets all errors that occurred at the same position as the primary error.\n * Useful when multiple parse attempts failed at the same location.\n * @returns {ParseError[]} Array of errors at the furthest position\n */\n get primaryErrors(): ParseError[] {\n const maxOffset = this.primary.span.offset;\n return this.errors.filter(err => err.span.offset === maxOffset);\n }\n\n /**\n * Converts the primary error to a simple string representation.\n * @returns {string} A human-readable error message\n */\n toString(): string {\n const err = this.primary;\n switch (err.tag) {\n case \"Expected\":\n return `Expected ${err.items.join(\" or \")}${err.found ? `, found ${err.found}` : \"\"}`;\n case \"Unexpected\":\n return `Unexpected ${err.found}`;\n case \"Custom\":\n return err.message;\n case \"Fatal\":\n return `Fatal: ${err.message}`;\n }\n }\n\n /**\n * Formats the error bundle using the specified formatter.\n * @param format - The output format (\"plain\", \"ansi\", \"html\", or \"json\")\n * @returns {string} Formatted error message with context and highlighting\n */\n format(format: \"plain\" | \"ansi\" | \"html\" | \"json\" = \"plain\"): string {\n const { ErrorFormatter } = require(\"./error-formatter\");\n return new ErrorFormatter(format).format(this);\n }\n}\n","import type { Either } from \"./either\";\nimport type { ParseErrorBundle, Span } from \"./errors\";\n\nexport type Spanned<T> = [value: T, span: Span];\n\n/**\n * Represents the output of a parser operation, containing both the updated state\n * and the parsing result (either success or error).\n * @template T - The type of the successfully parsed value\n * @example\n * ```typescript\n * const output: ParserOutput<string> = {\n * state: newState,\n * result: Either.right(\"parsed value\")\n * };\n * ```\n */\nexport type ParserOutput<T> = {\n /** The parser state after the operation */\n state: ParserState;\n /** Either a successful result of type T or a ParseErrorBundle */\n result: Either<T, ParseErrorBundle>;\n};\n\n/**\n * Factory function for creating ParserOutput objects.\n * @template T - The type of the successfully parsed value\n * @param state - The parser state after the operation\n * @param result - Either a successful result or error bundle\n * @returns A new ParserOutput object\n * @example\n * ```typescript\n * import { Either } from \"./either\";\n *\n * const successOutput = ParserOutput(newState, Either.right(\"success\"));\n * const errorOutput = ParserOutput(oldState, Either.left(errorBundle));\n * ```\n */\nexport const ParserOutput = <T>(\n state: ParserState,\n result: Either<T, ParseErrorBundle>\n): ParserOutput<T> => ({\n state,\n result\n});\n\n/**\n * Represents a position within source code with line, column, and byte offset.\n * All values are 1-indexed for human readability.\n * @example\n * ```typescript\n * const position: SourcePosition = {\n * line: 3, // Third line\n * column: 15, // 15th character on that line\n * offset: 42 // 42nd character from start of input\n * };\n * ```\n */\nexport type SourcePosition = {\n /** Line number (1-indexed) */\n line: number;\n /** Column number (1-indexed) */\n column: number;\n /** Byte offset from start of input (0-indexed) */\n offset: number;\n};\n\nexport class SourcePosition_ {\n constructor(\n public line: number,\n public column: number,\n public offset: number\n ) {}\n}\n\n/**\n * Represents the complete state of a parser at any point during parsing.\n * Contains the input being parsed, current position, and optional debugging/context information.\n * @example\n * ```typescript\n * const state: ParserState = {\n * remaining: \"hello world\",\n * pos: { line: 1, column: 1, offset: 0 },\n * source: \"hello world\",\n * debug: true,\n * labelStack: [\"expression\", \"identifier\"],\n * committed: false\n * };\n * ```\n */\nexport type ParserState = {\n /** The portion of input that hasn't been consumed yet */\n remaining: string;\n /** Current position in the source code */\n pos: SourcePosition;\n /** The complete original input string */\n source: string;\n /** Whether debug mode is enabled for detailed error reporting */\n debug?: boolean;\n /** Stack of parsing context labels for error reporting */\n labelStack?: string[];\n /** Whether the parser has committed to this parse path */\n committed?: boolean;\n};\n\n/**\n * Utility object containing static methods for creating and manipulating parser state.\n */\nexport const State = {\n /**\n * Creates a new parser state from an input string.\n *\n * @param input - The input string to parse\n * @returns A new parser state initialized at the start of the input\n */\n fromInput(input: string): ParserState {\n return {\n remaining: input,\n source: input,\n pos: { line: 1, column: 1, offset: 0 }\n };\n },\n\n /**\n * Creates a new state by consuming n characters from the current state.\n *\n * @param state - The current parser state\n * @param n - Number of characters to consume\n * @returns A new state with n characters consumed and position updated\n * @throws Error if attempting to consume more characters than remaining\n */\n consume(state: ParserState, n: number): ParserState {\n if (n === 0) return state;\n if (n > state.remaining.length) {\n throw new Error(\"Cannot consume more characters than remaining\");\n }\n\n const consumed = state.remaining.slice(0, n);\n let { line, column, offset } = state.pos;\n\n for (const char of consumed) {\n if (char === \"\\n\") {\n line++;\n column = 1;\n } else {\n column++;\n }\n offset++;\n }\n\n return {\n ...state,\n remaining: state.remaining.slice(n),\n pos: { line, column, offset }\n };\n },\n\n /**\n * Creates a new state by consuming a specific string from the current state.\n *\n * @param state - The current parser state\n * @param str - The string to consume\n * @returns A new state with the string consumed and position updated\n * @throws Error if the input doesn't start with the specified string\n */\n consumeString(state: ParserState, str: string): ParserState {\n if (!state.remaining.startsWith(str)) {\n throw new Error(\n `Cannot consume \"${str}\" - input \"${state.remaining}\" doesn't start with it`\n );\n }\n return State.consume(state, str.length);\n },\n\n /**\n * Creates a new state by moving to a specific offset position in the source.\n * Resets to the beginning and then consumes to the target position.\n * @param state - The current parser state\n * @param moveBy - Number of characters to move forward from current position\n * @returns A new state at the target position\n */\n move(state: ParserState, moveBy: number) {\n return State.consume(\n {\n ...state,\n remaining: state.source,\n pos: { line: 1, column: 1, offset: 0 }\n },\n state.pos.offset + moveBy\n );\n },\n\n /**\n * Creates a new state by consuming characters while a predicate is true.\n *\n * @param state - The current parser state\n * @param predicate - Function that tests each character\n * @returns A new state with matching characters consumed\n */\n consumeWhile(\n state: ParserState,\n predicate: (char: string) => boolean\n ): ParserState {\n let i = 0;\n while (i < state.remaining.length && predicate(state.remaining[i])) {\n i++;\n }\n return State.consume(state, i);\n },\n\n /**\n * Gets the next n characters from the input without consuming them.\n *\n * @param state - The current parser state\n * @param n - Number of characters to peek (default: 1)\n * @returns The next n characters as a string\n */\n peek(state: ParserState, n: number = 1): string {\n return state.remaining.slice(0, n);\n },\n\n /**\n * Checks if the parser has reached the end of input.\n *\n * @param state - The current parser state\n * @returns True if at end of input, false otherwise\n */\n isAtEnd(state: ParserState): boolean {\n return state.remaining.length === 0;\n },\n\n /**\n * Creates a human-readable string representation of the current parser position.\n * @param state - The current parser state\n * @returns A formatted string showing line, column, and offset\n * @example\n * ```typescript\n * const posStr = State.printPosition(state);\n * // Returns: \"line 5, column 12, offset 89\"\n * ```\n */\n printPosition(state: ParserState): string {\n return `line ${state.pos.line}, column ${state.pos.column}, offset ${state.pos.offset}`;\n }\n};\n","// import { debug } from \"./debug\";\nimport { Either } from \"./either\";\nimport { ParseError, ParseErrorBundle, Span } from \"./errors\";\nimport { ParserOutput, type ParserState, type Spanned, State } from \"./state\";\n\n/**\n * Parser is the core type that represents a parser combinator.\n *\n * A parser is a function that takes an input state and produces either:\n * - A successful parse result with the remaining input state\n * - An error describing why the parse failed\n *\n * Parsers can be composed using various combinators to build complex parsers\n * from simple building blocks.\n *\n * @template T The type of value this parser produces when successful\n */\nexport class Parser<T> {\n /**\n * Creates a new Parser instance.\n *\n * @param run - The parsing function that takes a parser state and returns a parse result\n */\n constructor(\n /**\n * @internal\n */\n public run: (state: ParserState) => ParserOutput<T>\n ) {}\n\n // Monad/Applicative\n\n /**\n * Creates a successful parser output with the given value and state.\n *\n * This is a low-level helper used internally to construct successful parse results.\n * It doesn't consume any input and returns the value with the current state unchanged.\n *\n * @param value - The value to wrap in a successful result\n * @param state - The current parser state\n * @returns {ParserOutput<T>} A successful parser output containing the value\n * @template T The type of the successful value\n * @internal\n */\n static succeed<T>(value: T, state: ParserState): ParserOutput<T> {\n return ParserOutput(state, Either.right(value));\n }\n\n /**\n * Creates a parser that always succeeds with the given value without consuming any input.\n *\n * This is the basic way to inject a value into the parser context. The parser will\n * succeed immediately with the provided value and won't advance the parser state.\n *\n * @param a - The value to lift into the parser context\n * @returns {Parser<A>} A parser that always succeeds with the given value\n * @template A The type of the value being lifted\n *\n * @example\n * ```ts\n * const always42 = Parser.lift(42)\n * always42.parse(\"any input\") // succeeds with 42\n *\n * // Useful for providing default values\n * const parseNumberOrDefault = number.or(Parser.lift(0))\n *\n * // Can be used to inject values in parser chains\n * const parser = parser(function* () {\n * const name = yield* identifier\n * const separator = yield* Parser.lift(\":\")\n * const value = yield* number\n * return { name, separator, value }\n * })\n * ```\n */\n static lift = <A>(a: A): Parser<A> =>\n new Parser(state => Parser.succeed(a, state));\n\n /**\n * Lifts a binary function into the parser context, applying it to the results of two parsers.\n *\n * This is the applicative functor's version of `map` for functions of two arguments.\n * It runs both parsers in sequence and applies the function to their results if both succeed.\n *\n * @param ma - The first parser\n * @param mb - The second parser\n * @param f - A function that takes the results of both parsers and produces a new value\n * @returns {Parser<C>} A parser that applies the function to the results of both input parsers\n * @template A The type of value produced by the first parser\n * @template B The type of value produced by the second parser\n * @template C The type of value produced by applying the function\n *\n * @example\n * ```ts\n * // Combine two parsed values with a function\n * const parsePoint = Parser.liftA2(\n * number,\n * number.trimLeft(comma),\n * (x, y) => ({ x, y })\n * )\n * parsePoint.parse(\"10, 20\") // succeeds with { x: 10, y: 20 }\n *\n * // Build a data structure from multiple parsers\n * const parsePerson = Parser.liftA2(\n * identifier,\n * number.trimLeft(colon),\n * (name, age) => ({ name, age })\n * )\n * parsePerson.parse(\"John:30\") // succeeds with { name: \"John\", age: 30 }\n * ```\n */\n static liftA2 = <A, B, C>(\n ma: Parser<A>,\n mb: Parser<B>,\n f: (a: A, b: B) => C\n ): Parser<C> => ma.zip(mb).map(args => f(...args));\n\n /**\n * Applies a parser that produces a function to a parser that produces a value.\n *\n * This is the applicative functor's application operator. It allows you to apply\n * functions within the parser context, enabling powerful composition patterns.\n *\n * @param ma - A parser that produces a value\n * @param mf - A parser that produces a function from that value type to another type\n * @returns {Parser<B>} A parser that applies the parsed function to the parsed value\n * @template A The type of the input value\n * @template B The type of the output value after function application\n *\n * @example\n * ```ts\n * // Parse a function name and apply it\n * const parseFn = choice([\n * string(\"double\").map(() => (x: number) => x * 2),\n * string(\"square\").map(() => (x: number) => x * x)\n * ])\n * const result = Parser.ap(number, parseFn.trimLeft(space))\n * result.parse(\"5 double\") // succeeds with 10\n * result.parse(\"5 square\") // succeeds with 25\n *\n * // Chain multiple applications\n * const add = (x: number) => (y: number) => x + y\n * const parseAdd = Parser.lift(add)\n * const addParser = Parser.ap(\n * number,\n * Parser.ap(number.trimLeft(plus), parseAdd)\n * )\n * addParser.parse(\"3 + 4\") // succeeds with 7\n * ```\n */\n static ap = <A, B>(ma: Parser<A>, mf: Parser<(_: A) => B>): Parser<B> =>\n mf.zip(ma).map(([f, a]) => f(a));\n\n // Error handling\n\n /**\n * Creates a failed parser output with the given error information.\n *\n * This is a low-level helper for constructing parse errors. It creates a custom\n * error with the provided message and optional expected/found information.\n *\n * @param error - Error details including message and optional expected/found values\n * @param state - The parser state where the error occurred\n * @returns {ParserOutput<never>} A failed parser output containing the error\n * @internal\n */\n static fail(\n error: { message: string; expected?: string[]; found?: string },\n state: ParserState\n ): ParserOutput<never> {\n const span = Span({\n pos: {\n offset: state.pos.offset,\n line: state.pos.line,\n column: state.pos.column\n }\n });\n\n const parseErr = ParseError.custom({\n span,\n message: error.message,\n context: state?.labelStack ?? [],\n hints: []\n });\n\n const bundle = new ParseErrorBundle(\n [parseErr],\n // state?.source ?? state.remaining\n state.source\n );\n\n return ParserOutput(state, Either.left(bundle));\n }\n\n /**\n * Creates a parser that always fails with a fatal error.\n *\n * Fatal errors are non-recoverable and prevent backtracking in choice combinators.\n * Use this when you've determined that the input is definitely malformed and trying\n * other alternatives would be meaningless.\n *\n * @param message - The error message to display\n * @returns {Parser<never>} A parser that always fails with a fatal error\n *\n * @example\n * ```ts\n * const number = regex(/-?[0-9]+/).map(Number);\n * const parsePositive = number.flatMap(n =>\n * n > 0 ? Parser.lift(n) : Parser.fatal(\"Expected positive number\")\n * )\n * ```\n */\n static fatal = (message: string): Parser<never> =>\n new Parser(state =>\n ParserOutput(\n state,\n Either.left(\n new ParseErrorBundle(\n [\n ParseError.fatal({\n span: Span(state),\n message,\n context: state?.labelStack ?? []\n })\n ],\n // state?.source ?? state.remaining\n state.source\n )\n )\n )\n );\n\n /**\n * Runs the parser on the given input string and returns the full parser output.\n *\n * This method provides access to both the parse result and the final parser state,\n * which includes information about the remaining unparsed input and position.\n *\n * @param input - The string to parse\n * @returns {ParserOutput<T>} A parser output containing both the result (success or error) and final state\n *\n * @example\n * ```ts\n * const parser = string(\"hello\");\n * const output = parser.parse(\"hello world\");\n * // output.result contains Either.right(\"hello\")\n * // output.state contains remaining input \" world\" and position info\n * ```\n */\n parse(input: string): ParserOutput<T> {\n const { result, state } = this.run(State.fromInput(input) as any);\n return ParserOutput(state, result);\n }\n\n /**\n * Runs the parser on the given input and returns either the parsed value or error bundle.\n *\n * This is a convenience method that unwraps the Either result, making it easier\n * to handle the common case where you just need the value or error without the\n * full parser state information.\n *\n * @param input - The string to parse\n * @returns {T | ParseErrorBundle} The successfully parsed value of type T, or a ParseErrorBundle on failure\n *\n * @example\n * ```ts\n * const parser = number();\n * const result = parser.parseOrError(\"42\");\n * if (result instanceof ParseErrorBundle) {\n * console.error(result.format());\n * } else {\n * console.log(result); // 42\n * }\n * ```\n */\n parseOrError(input: string): T | ParseErrorBundle {\n const { result } = this.run(State.fromInput(input));\n if (Either.isRight(result)) {\n return result.right;\n }\n return result.left;\n }\n\n /**\n * Runs the parser on the given input and returns the parsed value or throws an error.\n *\n * This method is useful when you're confident the parse will succeed or want to\n * handle parse errors as exceptions. The thrown error is a ParseErrorBundle which\n * contains detailed information about what went wrong.\n *\n * @param input - The string to parse\n * @returns {T} The successfully parsed value of type T\n * @throws {ParseErrorBundle} Thrown when parsing fails\n *\n * @example\n * ```ts\n * const parser = number();\n * try {\n * const value = parser.parseOrThrow(\"42\");\n * console.log(value); // 42\n * } catch (error) {\n * if (error instanceof ParseErrorBundle) {\n * console.error(error.format());\n * }\n * }\n * ```\n */\n parseOrThrow(input: string): T {\n const { result } = this.parse(input);\n\n if (Either.isLeft(result)) {\n throw result.left;\n }\n return result.right;\n }\n\n /**\n * Transforms the result of this parser by applying a function to the parsed value.\n *\n * This is the functor map operation. If the parser succeeds, the function is applied\n * to the result. If the parser fails, the error is propagated unchanged. The input\n * is not consumed if the transformation fails.\n *\n * @param f - A function that transforms the parsed value\n * @returns {Parser<B>} A new parser that produces the transformed value\n * @template B The type of the transformed value\n *\n * @example\n * ```ts\n * // Parse a number and double it\n * const doubled = number().map(n => n * 2);\n * doubled.parse(\"21\") // succeeds with 42\n *\n * // Parse a string and get its length\n * const stringLength = quoted('\"').map(s => s.length);\n * stringLength.parse('\"hello\"') // succeeds with 5\n *\n * // Chain multiple transformations\n * const parser = identifier()\n * .map(s => s.toUpperCase())\n * .map(s => ({ name: s }));\n * parser.parse(\"hello\") // succeeds with { name: \"HELLO\" }\n * ```\n */\n map<B>(f: (a: T) => B): Parser<B> {\n return new Parser<B>(state => {\n const { result, state: newState } = this.run(state);\n if (Either.isLeft(result)) {\n return ParserOutput(\n state,\n result as unknown as Either<B, ParseErrorBundle>\n );\n }\n // return Parser.succeed(f(result.right), newState);\n return ParserOutput(newState, Either.right(f(result.right)));\n });\n }\n\n /**\n * Chains this parser with another parser that depends on the result of this one.\n *\n * This is the monadic bind operation (also known as chain or andThen). It allows\n * you to create a parser whose behavior depends on the result of a previous parse.\n * This is essential for context-sensitive parsing where later parsing decisions\n * depend on earlier results.\n *\n * @param f - A function that takes the parsed value and returns a new parser\n * @returns {Parser<B>} A new parser that runs the second parser after the first succeeds\n * @template B The type of value produced by the resulting parser\n *\n * @example\n * ```ts\n * // Parse a number and then that many 'a' characters\n * const parser = number().flatMap(n =>\n * string('a'.repeat(n))\n * );\n * parser.parse(\"3aaa\") // succeeds with \"aaa\"\n *\n * // Parse a type annotation and return appropriate parser\n * const typeParser = identifier().flatMap(type => {\n * switch(type) {\n * case \"int\": return number();\n * case \"string\": return quoted('\"');\n * default: return Parser.fail({ message: `Unknown type: ${type}` });\n * }\n * });\n *\n * // Validate parsed values\n * const positiveNumber = number().flatMap(n =>\n * n > 0\n * ? Parser.lift(n)\n * : Parser.fail({ message: \"Expected positive number\" })\n * );\n * ```\n */\n flatMap<B>(f: (a: T) => Parser<B>): Parser<B> {\n return new Parser<B>(state => {\n const { result, state: newState } = this.run(state);\n if (Either.isLeft(result)) {\n return {\n state: newState,\n result: result as unknown as Either<B, ParseErrorBundle>\n };\n }\n const nextParser = f(result.right);\n return nextParser.run(newState);\n });\n }\n\n /**\n * Creates a parser that always succeeds with the given value without consuming input.\n *\n * This is an alias for `Parser.lift` that follows the monadic naming convention.\n * It's the \"return\" or \"pure\" operation for the Parser monad, injecting a plain\n * value into the parser context.\n *\n * @param a - The value to wrap in a successful parser\n * @returns {Parser<A>} A parser that always succeeds with the given value\n * @template A The type of the value being lifted\n *\n * @example\n * ```ts\n * // Always succeed with a constant value\n * const always42 = Parser.pure(42);\n * always42.parse(\"any input\") // succeeds with 42\n *\n * // Use in flatMap to wrap values\n * const parser = number().flatMap(n =>\n * n > 0 ? Parser.pure(n) : Parser.fail({ message: \"Must be positive\" })\n * );\n * ```\n */\n static pure = <A>(a: A): Parser<A> =>\n new Parser(state => Parser.succeed(a, state));\n\n /**\n * Creates a new parser that lazily evaluates the given function.\n * This is useful for creating recursive parsers.\n *\n * @param fn - A function that returns a parser\n * @returns {Parser<T>} A new parser that evaluates the function when parsing\n * @template T The type of value produced by the parser\n *\n * @example\n * ```ts\n * // Create a recursive parser for nested parentheses\n * const parens: Parser<string> = Parser.lazy(() =>\n * between(\n * char('('),\n * char(')'),\n * parens\n * )\n * )\n * ```\n */\n static lazy<T>(fn: () => Parser<T>): Parser<T> {\n return new Parser(state => {\n const parser = fn();\n return parser.run(state);\n });\n }\n\n /**\n * Combines this parser with another parser, returning both results as a tuple.\n *\n * This is a fundamental sequencing operation that runs two parsers in order.\n * If either parser fails, the entire operation fails. The results are returned\n * as a tuple containing both parsed values.\n *\n * @param parserB - The second parser to run after this one\n * @returns {Parser<[T, B]>} A parser that produces a tuple of both results\n * @template B The type of value produced by the second parser\n *\n * @example\n * ```ts\n * // Parse a coordinate pair\n * const coordinate = number().zip(number().trimLeft(comma));\n * coordinate.parse(\"10, 20\") // succeeds with [10, 20]\n *\n * // Parse a key-value pair\n * const keyValue = identifier().zip(number().trimLeft(colon));\n * keyValue.parse(\"age:30\") // succeeds with [\"age\", 30]\n *\n * // Combine multiple parsers\n * const triple = number()\n * .zip(number().trimLeft(comma))\n * .zip(number().trimLeft(comma))\n * .map(([[a, b], c]) => [a, b, c]);\n * triple.parse(\"1, 2, 3\") // succeeds with [1, 2, 3]\n * ```\n */\n zip<B>(parserB: Parser<B>): Parser<[T, B]> {\n return new Parser(state => {\n const { result: a, state: stateA } = this.run(state);\n if (Either.isLeft(a)) {\n return {\n result: a as unknown as Either<[T, B], ParseErrorBundle>,\n state: stateA\n };\n }\n const { result: b, state: stateB } = parserB.run(stateA);\n if (Either.isLeft(b)) {\n return {\n result: b as unknown as Either<[T, B], ParseErrorBundle>,\n state: stateB\n };\n }\n return Parser.succeed([a.right, b.right], stateB);\n });\n }\n\n /**\n * Sequences this parser with another, keeping only the second result.\n *\n * This is useful when you need to parse something but only care about what\n * comes after it. The first parser must succeed for the second to run, but\n * its result is discarded.\n *\n * @param parserB - The parser whose result will be kept\n * @returns {Parser<B>} A parser that produces only the second result\n * @template B The type of value produced by the second parser\n *\n * @example\n * ```ts\n * // Parse a value after a label\n * const labeledValue = string(\"value:\").then(number());\n * labeledValue.parse(\"value:42\") // succeeds with 42\n *\n * // Skip whitespace before parsing\n * const trimmedNumber = whitespace().then(number());\n * trimmedNumber.parse(\" 123\") // succeeds with 123\n *\n * // Parse the body after a keyword\n * const functionBody = keyword(\"function\").then(identifier()).then(block());\n * ```\n */\n then<B>(parserB: Parser<B>): Parser<B> {\n return this.zip(parserB).map(([_, b]) => b);\n }\n\n /**\n * Alias for `then` - sequences parsers and keeps the right result.\n *\n * This alias follows the naming convention from applicative functors where\n * \"zipRight\" means to combine two values but keep only the right one.\n *\n * @see {@link then} for details and examples\n */\n zipRight = this.then;\n\n /**\n * Sequences this parser with another, keeping only the first result.\n *\n * This is useful when you need to parse something that must be present but\n * whose value you don't need. Common uses include parsing required delimiters\n * or terminators.\n *\n * @param parserB - The parser to run but whose result will be discarded\n * @returns {Parser<T>} A parser that produces only the first result\n * @template B The type of value produced by the second parser (discarded)\n *\n * @example\n * ```ts\n * // Parse a statement and discard the semicolon\n * const statement = expression().thenDiscard(char(';'));\n * statement.parse(\"x + 1;\") // succeeds with the expression, semicolon discarded\n *\n * // Parse a quoted string and discard the closing quote\n * const quotedContent = char('\"').then(stringUntil('\"')).thenDiscard(char('\"'));\n *\n * // Parse array elements and discard separators\n * const element = number().thenDiscard(optional(char(',')));\n * ```\n */\n thenDiscard<B>(parserB: Parser<B>): Parser<T> {\n return this.zip(parserB).map(([a, _]) => a);\n }\n\n /**\n * Alias for `thenDiscard` - sequences parsers and keeps the left result.\n *\n * This alias follows the naming convention from applicative functors where\n * \"zipLeft\" means to combine two values but keep only the left one.\n *\n * @see {@link thenDiscard} for details and examples\n */\n zipLeft = this.thenDiscard;\n\n /**\n * Makes this parser usable in generator syntax for cleaner sequential parsing.\n *\n * This ite