/** * Pratt parser for dice notation. * * @module parser/parser */ import type { RollParserErrorCode } from '../errors.js'; import { RollParserError } from '../errors.js'; import { type Token } from '../lexer/tokens.js'; import type { ASTNode } from './ast.js'; /** * Error thrown when the parser encounters invalid syntax. * * `position` is a zero-based UTF-16 offset into the notation. It is * deliberately absent from `message` — read it from the field, or uniformly * across all roll-parser errors via `getErrorSpan`. * * @example * ```typescript * import { ParseError, roll, TokenType } from 'roll-parser'; * * try { * roll('4d6d1'); * } catch (error) { * const typed = error as ParseError; * typed.code; // 'AMBIGUOUS_DICE_CHAIN' * typed.position; // 3 * typed.token?.type === TokenType.DICE; // true * } * ``` * * @category Errors */ export declare class ParseError extends RollParserError { /** Zero-based UTF-16 offset in the notation where parsing failed. */ readonly position: number; /** * The token the parser was looking at. `undefined` when the failure is not * anchored to a token — currently only `UNEXPECTED_END`, raised when the * input stops short. */ readonly token: Token | undefined; constructor(message: string, code: RollParserErrorCode, position: number, token?: Token, options?: ErrorOptions); } /** * Maximum expression nesting depth. Far beyond any human-authored notation — * exists so adversarial input like 20,000 nested parens throws a typed * `ParseError` instead of an uncaught `RangeError` stack overflow (which * would also break the `isRollParserError` contract). Bounding parse depth * also bounds AST depth, protecting the recursive AST walkers and evaluator. * * Not configurable — unlike the {@link EvaluationOptions} caps, this one always * applies, so untrusted notation can never blow the stack. * * @example * ```typescript * import { isRollParserError, MAX_PARSE_DEPTH, parse } from 'roll-parser'; * * MAX_PARSE_DEPTH; // 128 * * try { * parse('('.repeat(20_000) + '1d6' + ')'.repeat(20_000)); * } catch (error) { * isRollParserError(error) && error.code; // 'MAX_DEPTH_EXCEEDED' * } * ``` * * @category Limits */ export declare const MAX_PARSE_DEPTH = 128; /** * Pratt parser for dice notation. * * Uses binding power (precedence) to handle operator associativity and * precedence without left recursion issues. */ export declare class Parser { private readonly tokens; private pos; private depth; constructor(tokens: Token[]); /** * Parse the token stream into an AST. */ parse(): ASTNode; /** * Parse an expression with minimum binding power. * * `pending` is the token that demanded this operand — the `d` of `1d`, the * `<` of `2d6r<`. It exists so a truncated notation can name what it was * waiting for; `undefined` only at the top of the input. */ private parseExpression; /** * Throw once the AST under construction is deeper than the recursive walkers * can safely descend. */ private guardDepth; /** * NUD - Null Denotation. * Handles tokens that appear at the start of an expression (prefix position). */ private parseNud; /** * LED - Left Denotation. * Handles tokens that appear between expressions (infix/postfix position). */ private parseLed; /** * Zero-width span for synthetic nodes (implicit counts, `d%` sides) that * have no source text of their own — anchored at the governing token. */ private static syntheticLiteral; private parseLiteral; private parseUnaryMinus; private parsePrefixDice; private parseInfixDice; private parsePrefixDicePercent; private parseInfixDicePercent; private parsePrefixFateDice; private parseInfixFateDice; private parseGrouped; private parseGroup; private parseVariable; private parseFunctionCall; private parseBinaryOp; /** * Rejects a dice token whose count operand is itself a bare (unparenthesized) * dice expression. `4d6d1` would otherwise silently parse as `(4d6)d1` — * roll 4d6, then use the result as a count of d1 dice — which is almost * never intended: every major dice dialect reads `4d6d1` as "drop lowest 1". * Both meanings stay reachable through explicit forms: `4d6dl1` to drop, * `(4d6)d1` for nested dice. */ private rejectBareDiceChain; private rejectSuccessCountTarget; /** * Rejects `GroupNode` (or a wrapper-cloaked group) as the target of `token`. * Explode, reroll, and crit-threshold wrap bare dice pools only — a group * is a container of sub-expressions, so these modifiers have no defined * semantics. Walks `Grouped`/`KeepDrop`/`Sort`/`CritThreshold` so wrappers * cannot smuggle a group past the check (`{1d6}kh1cs>5`, `({1d6})!`, * `{1d6}scs>5` all reject the same as `{1d6}!`/`{1d6}cs>5`). * * `singleSubRollPasses` opts the caller into the single-sub-roll passthrough: * a `Group` with one expression is the user's explicit flat-pool escape * hatch and is equivalent to its unwrapped form. Currently only * `parseCritThreshold` opts in — explode/reroll keep the strict reject so * existing notation contracts don't shift. */ private rejectGroupTarget; /** * Rejects a Versus anywhere inside a meta operand — a dice count, dice sides, * a modifier count, or a threshold/bound value. * * Deep, unlike {@link rejectVersusTarget}: `evalMetaOperand` reduces the * operand to a scalar and forwards rolls but not `versusMetadata`, so * `floor(1d20 vs 15)` or `(1d20 vs 15)+0` buried in a count loses the degree * exactly as a bare one does. */ private rejectVersusMetaOperand; /** * The wording the meta-expression rejections share. Not every `NESTED_VERSUS` * throw — `parseVersus` says "Cannot chain versus operators" for `a vs b vs c`, * a different condition. Routing that one through here would silently rewrite * its message, and `expectRollError` asserts only the code. */ private static throwVersusMetaExpression; private rejectVersusTarget; private rejectSingleSubGroupTarget; private parseKeepDrop; private parseExplode; private parseReroll; private parseDieBound; private parseSort; private parseCritThreshold; private parseSuccessCount; /** * Parses the `f...` suffix of a success count. Bare `fN` means `f=N`. * * `token` is the success-count comparison, which the reject calls report * against; `failToken` is the `f`, so a truncated `2d6>=4f` names it rather * than the comparison behind it. */ private parseFailThreshold; /** * Rejects an `f` that reached LED position, which means no success count is * there to own it — `parseSuccessCount` consumes its own `f`, so this is the * only way the token surfaces. * * The trigger clause names the cause of nearly every orphan: an explode's * comparison is optional, so `3d6!>4f1` reads as a success count with a botch * threshold when `!` has already spent the `>4`. Naming it unconditionally * keeps this off the `index.js` byte budget; **README.md → Arithmetic and * precedence** carries the two rewrites. */ private static throwOrphanFailThreshold; private parseVersus; /** * Checks whether the next token is a comparison operator. */ private isComparePointAhead; /** * Parses a comparison operator followed by a value expression. * Called by modifier parsers (explode, reroll, success counting). * * The threshold value is parsed at `BP.DICE_LEFT`, which binds tighter than * arithmetic. This keeps the comparison bound to the dice pool on the left * rather than letting arithmetic to the right be consumed into the * threshold. As a consequence, `1d6>=5+2` parses as `(1d6>=5)+2` with * threshold `5` — not `7`. Computed thresholds require parens: * `1d6>=(5+2)`. Same binding applies to `parseSuccessCount` below. * * @returns A ComparePoint with the operator and value AST node * @throws {ParseError} If the next token is not a comparison operator */ private parseComparePoint; private getCompareOp; private getOperatorSymbol; private getLeftBp; private getRightBp; private peek; private advance; private expect; private hasTokens; } /** * Parses a dice notation string into an {@link ASTNode} tree, lexing it first. * * {@link evaluate} never mutates the AST, so parse once and evaluate many times * when rolling the same notation repeatedly — that skips the lexer and parser * on every roll after the first. Use `parse` alone to validate notation * without consuming randomness. * * @param notation - The dice notation to parse * @returns The root AST node, with `start`/`end` spans on every node * @throws {LexerError} If the input contains invalid characters * @throws {ParseError} If the input has invalid syntax * @throws {RollParserError} `INVALID_NOTATION_TYPE` when `notation` is not a * string — raised before lexing, so it carries no position * * @example * ```typescript * import { parse } from 'roll-parser'; * * parse('2d6+3'); * // { * // type: 'BinaryOp', operator: '+', start: 0, end: 5, * // left: { * // type: 'Dice', start: 0, end: 3, * // count: { type: 'Literal', value: 2, start: 0, end: 1 }, * // sides: { type: 'Literal', value: 6, start: 2, end: 3 }, * // }, * // right: { type: 'Literal', value: 3, start: 4, end: 5 }, * // } * ``` * * `count` and `sides` are full nodes, not numbers — that is what makes * computed dice like `(1d4)d6` expressible. * * @example Parse once, roll many * ```typescript * import { evaluate, parse, SeededRNG } from 'roll-parser'; * * const ast = parse('4d6kh3'); * const rng = new SeededRNG('demo'); * const scores = Array.from({ length: 6 }, () => evaluate(ast, rng).total); * ``` * * @category Core */ export declare function parse(notation: string): ASTNode; //# sourceMappingURL=parser.d.ts.map