Wolfram Language Paclet Repository

Community-contributed installable additions to the Wolfram Language

Primary Navigation

    • Cloud & Deployment
    • Core Language & Structure
    • Data Manipulation & Analysis
    • Engineering Data & Computation
    • External Interfaces & Connections
    • Financial Data & Computation
    • Geographic Data & Computation
    • Geometry
    • Graphs & Networks
    • Higher Mathematical Computation
    • Images
    • Knowledge Representation & Natural Language
    • Machine Learning
    • Notebook Documents & Presentation
    • Scientific and Medical Data & Computation
    • Social, Cultural & Linguistic Data
    • Strings & Text
    • Symbolic & Numeric Computation
    • System Operation & Setup
    • Time-Related Computation
    • User Interface Construction
    • Visualization & Graphics
    • Random Paclet
    • Alphabetical List
  • Using Paclets
    • Get Started
    • Download Definition Notebook
  • Learn More about Wolfram Language

Parser

Tutorials

  • Building Language Front-Ends
  • Inside CodeAnalysis - How CodeStructure Parses C
  • Design and Compilation Strategy
  • Implementing the LaTeX Math Parser
  • MaTeX Comparison Showcase
  • The Parser Landscape - a Survey of What Exists Today
  • The Parser Zoo - language front-ends over a shared algebra
  • Parsing BNF Grammars (and bootstrapping a TPTP parser)
  • Parsing GrammarRules Locally
  • A Markdown Inline Parser in Parser Combinators
  • ParsingOpenQASM
  • Parsing TPTP, Auto-Generated from the Published BNF
  • PrattVsPEG
  • The Wolfram Box Typesetting Reference

Guides

  • Parsing in the Wolfram Language

Symbols

  • ASTAddSource
  • ASTAlgebra
  • ASTContainer
  • ASTLeafQ
  • ASTNodeQ
  • ASTStripSource
  • BinaryNode
  • BrainfuckAST
  • BrainfuckGrammar
  • BrainfuckRun
  • BrainfuckSemantic
  • CalculatorAST
  • CalculatorEval
  • CalculatorGrammar
  • CalculatorSemantic
  • CallNode
  • ContainerNode
  • EBNFParse
  • EBNFRules
  • ErrorNode
  • ExportLaTeX
  • GroupNode
  • InfixNode
  • JSONAST
  • JSONGrammar
  • JSONImport
  • JSONSemantic
  • LambdaAST
  • LambdaEval
  • LambdaGrammar
  • LambdaSemantic
  • LaTeXMathParse
  • LaTeXMathParser
  • LaTeXMathStyle
  • LeafNode
  • LispAST
  • LispGrammar
  • LispRead
  • LispSemantic
  • LispSymbol
  • MarkdownInlineParse
  • MarkdownInlineParser
  • MarkdownParse
  • MarkdownParser
  • ParseAction
  • ParseBetween
  • ParseChainLeft
  • ParseChainRight
  • ParseCharacter
  • ParseChoiceLongest
  • ParseChoice
  • ParseFail
  • ParseLiteral
  • ParseLookahead
  • ParseMany
  • Parse
  • ParseNotFollowedBy
  • ParseOperatorTable
  • ParseOptional
  • ParsePartial
  • ParsePosition
  • ParserCombinator
  • ParserCombinatorQ
  • ParserCompile
  • ParseRecursive
  • ParseRegex
  • ParseSepBy1
  • ParseSepBy
  • ParseSequence
  • ParseSome
  • ParseSucceed
  • ParseTry
  • PostfixNode
  • PrefixNode
  • RecCell
  • RecRef
  • SetRec
  • SpannedToken
  • TernaryNode
  • ToCodeParser
  • TPTPExport
  • TPTPImport

Overviews

  • WolframParser

Design and Compilation Strategy

What this note covers

The
ParserLandscape
survey lays out what's already there; this note lays out what we are building. The plan in one sentence: reuse the
GrammarRules
declarative DSL, but compile the rules to a local parser via
FunctionCompile
instead of round-tripping through
CloudDeploy
, and pair that with an Anton-style
Parse*
combinator core that all funnels into a single computable
ParserCombinator
head.
The note has six parts:
1
.
The single
ParserCombinator
head: one canonical wrapper that every combinator constructor produces, with
UpValues
for operator composition and a
SummaryBox
-style formatter.
2
.
Parse*
constructors: the user-facing functions, named in the
AntonAntonov/FunctionalParsers
tradition.
3
.
Two-tier API: a declarative GrammarRules-compatible entry point that lowers to
ParserCombinator
, and the bare combinator core for grammars that don't fit the declarative shape.
4
.
Parser algebra: the small set of primitive combinators all higher-level constructs lower to.
5
.
Compilation strategy: how a
ParserCombinator
tree lowers to a typed first-order form and what
FunctionCompile
makes of it.
6
.
Worked targets: LaTeX math and TPTP - the two grammars that motivate the choice of primitives.
7
.
Open questions: things deliberately left unresolved in v0.1.

Part 1 - The single
ParserCombinator
head

Every parser is a single computable object
ParserCombinator[type,args,opts]
:
◼
  • type
    - a symbol naming the combinator shape (
    Sequence
    ,
    Choice
    ,
    Many
    ,
    Literal
    , …)
  • ◼
  • args
    - the combinator's children (other
    ParserCombinator
    instances, or terminal data)
  • ◼
  • opts
    - an
    Association
    of options (
    <|"Memoize"->True,...|>
    )
  • The head is opaque to user code: you never write
    ParserCombinator[...]
    by hand. Instead you call one of the
    Parse*
    constructors (next section), each of which returns a
    ParserCombinator
    of the appropriate type. The head exists for three reasons:
    (1) Composition via UpValues. Because every parser is a
    ParserCombinator
    , we can attach
    UpValues
    to that head and overload the WL operators that actually parse:
    WL syntax
    Lowers to
    Combinator
    p1|p2
    Alternatives
    [p1,p2]
    ParseChoice
    p1~~p2
    StringExpression
    [p1,p2]
    ParseSequence
    p..
    Repeated
    [p]
    ParseSome
    (one or more)
    p...
    RepeatedNull
    [p]
    ParseMany
    (zero or more)
    Optional
    [p]
    Optional
    [p]
    ParseOptional
    Why these and not others:
    ◼
  • |
    is
    Alternatives
    - the semantic match to choice is exact, and the operator is the same one PEG / regex / EBNF use.
  • ◼
  • ~~
    is
    StringExpression
    . The UpValue only fires when both sides are
    ParserCombinator
    instances - plain string sequences (
    "foo"~~"bar"
    ) keep their built-in meaning. This dual interpretation is the point: a parser library that overloads
    ~~
    reads naturally to a user who already thinks of string sequences in those terms.
  • ◼
  • ..
    /
    ...
    are
    Repeated
    /
    RepeatedNull
    , which already mean "one or more" / "zero or more" in pattern context. Reusing them for parser repetition is the obvious mapping.
  • ◼
  • ~
    is not overloaded -
    a~f~b
    is WL's infix function notation
    f[a,b]
    , not a binary operator.
  • (2) SubValue: call a parser as a function. Every
    ParserCombinator
    also carries a SubValues rule:
    pc[input]
    evaluates to
    Parse[pc,input]
    . So a constructed parser is directly callable, the same way a
    CompiledCodeFunction
    or an
    InterpolatingFunction
    is. For an uncompiled parser the SubValue routes to the interpreter; for one passed through
    ParserCompile
    it routes to the cached compiled function.
    A sample composition:
    In[1]:=
    (*matchoneormoredigits,followedoptionallybyadot-and-fraction*)​​number=
    ParseCharacter
    [DigitCharacter]..~~Optional
    ParseLiteral
    ["."]~~
    ParseCharacter
    [DigitCharacter]...;
    UpValues mean each operator picks the right combinator without the user ever typing
    ParserCombinator[...]
    .
    (3) A canonical, inspectable representation. Every parser is a tree of
    ParserCombinator
    nodes, so the compiler, the pretty-printer, and the diagnostic machinery all walk one expression shape. There is no separate "compiled form" data type at the user-visible level -
    ParserCompile[p]
    adds a
    "Code"->CompiledCodeFunction[...]
    entry to the wrapper's options and otherwise leaves the tree alone. The presence of
    "Code"
    is the canonical "is this compiled?" marker; no separate
    "Compiled"->True
    flag is needed.
    (4) A nice summary box.
    ParserCombinator
    carries a
    BoxForm`ArrangeSummaryBox
    formatter modelled on
    FiniteFieldElement
    /
    PAdicNumber
    /
    Quantity
    . Always-visible: combinator type, arity, compile status. Expanded: the structural sketch, the option association, an icon hinting at the combinator family (a sequence of glyphs for
    Sequence
    , a fork for
    Choice
    , a star for
    Many
    , a brace for
    Between
    , etc.). Concretely:
    ParserCombinator
    ── Type: Sequence
    ── Arity: 3
    ── Compiled: False
    ── Structure: Literal["the weather in "] ~~ Capture["city", Restricted["City", "USA"]] ~~ Literal["."]
    ── Options: <|"Memoize" -> False, "TrackPosition" -> True|>
    The summary box is the same convention used by every modern WL computable object - users get a one-line glance plus an opener, not an opaque blob.

    Part 2 -
    Parse*
    constructors

    Following the
    AntonAntonov/FunctionalParsers
    naming convention, every constructor is a function with a
    Parse*
    prefix that returns a
    ParserCombinator
    . The full table (v0.1 plan):
    Constructor
    Returns
    ParserCombinator[...]
    of type
    What it matches
    ParseLiteral[s]
    Literal
    the exact string / token
    s
    ParseCharacter[pat]
    Character
    a single character matching
    pat
    (
    LetterCharacter
    ,
    DigitCharacter
    ,
    CharacterRange
    [a,b]
    , an
    Alternatives
    of these, or a literal 1-char string)
    ParseToken[type]
    Token
    a tagged
    Token[type,_,_]
    ParseSucceed[val]
    Succeed
    always succeed with
    val
    (no input consumed)
    ParseFail[msg]
    Fail
    always fail with
    msg
    ParseSequence[p1,p2,...]
    Sequence
    each
    pi
    in order; result is a list
    ParseChoice[p1,p2,...]
    Choice
    first
    pi
    that matches (PEG-ordered)
    ParseMany[p]
    Many
    zero or more
    p
    ParseSome[p]
    Some
    one or more
    p
    ParseOptional[p]
    Optional
    zero or one
    p
    ParseBetween[open,p,close]
    Between
    open
    , then
    p
    , then
    close
    ; result is
    p
    's
    ParseSepBy[p,sep]
    SepBy
    zero or more
    p
    separated by
    sep
    ParseSepBy1[p,sep]
    SepBy1
    one or more
    p
    separated by
    sep
    ParseChainLeft[p,op,init]
    ChainLeft
    left-associative operator chain
    ParseChainRight[p,op,init]
    ChainRight
    right-associative operator chain
    ParseLookahead[p]
    Lookahead
    succeed iff
    p
    would match, consume nothing
    ParseNotFollowedBy[p]
    NotFollowedBy
    succeed iff
    p
    would not match, consume nothing
    ParseTry[p]
    Try
    backtrack on failure even after consuming
    ParseAction[p,f]
    Action
    apply
    f
    to
    p
    's result
    ParseCapture[name,p]
    Capture
    tag
    p
    's result with
    name
    (for
    GrammarRules
    slot lowering)
    ParseRecursive[name,body]
    Recursive
    a named recursive parser body
    The constructors are just
    ParserCombinator
    builders - they do not run the parser; they only produce the value. Running is
    Parse[parser,input]
    (interpretive) or
    ParserCompile[parser][input]
    (compiled).

    Worked sample composition

    The same number-parser, three equivalent ways:
    In[2]:=
    (*operatorform-shortest,idiomaticfornewcode*)​​number=
    ParseCharacter
    [DigitCharacter]..~~Optional
    ParseLiteral
    ["."]~~
    ParseCharacter
    [DigitCharacter]...;​​​​(*explicitconstructorform-whattheUpValueslowerto*)​​number=
    ParseSequence
    ​​
    ParseSome
    
    ParseCharacter
    [DigitCharacter],​​
    ParseOptional
    
    ParseSequence
    ​​
    ParseLiteral
    ["."],​​
    ParseMany
    
    ParseCharacter
    [DigitCharacter]​​​​;​​​​(*mixed-dropintotheoperatorformwhereverreadable,fallbacktoexplicitcallswhenithelps*)​​number=
    ParseSequence
    ​​
    ParseCharacter
    [DigitCharacter]..,​​
    ParseOptional
    
    ParseLiteral
    ["."]~~
    ParseCharacter
    [DigitCharacter]...​​;
    All three return the same
    ParserCombinator
    expression. Composability is a single-axis story - whatever you write, it lowers into one canonical tree.

    Part 3 - Two-tier API

    Tier 1 - the declarative path (
    GrammarRules
    -compatible)

    The built-in
    GrammarRules
    takes a list of slot-templates paired with actions and returns an inert symbolic form. Today the only way to evaluate a
    GrammarRules
    object is to deploy it as a cloud object and call
    GrammarApply
    against the deployment; the declaration itself is just data:
    In[3]:=
    GrammarRules[{​​"the weather in <city:Restricted[\"City\", \"USA\"]>"city,​​"convert <amount:Number> <from:Restricted[\"Currency\"]> to <to:Restricted[\"Currency\"]>"​​CurrencyConvert[Quantity[amount,from],to]​​}]
    Out[3]=
    GrammarRules[{the weather in <city:Restricted["City", "USA"]>city,convert <amount:Number> <from:Restricted["Currency"]> to <to:Restricted["Currency"]>CurrencyConvert[Quantity[amount,from],to]}]
    WolframParser
    accepts the same declaration, and provides two ways to use it locally:
    In[4]:=
    (*"just parse"-JIT-compilethegrammar,cacheit,parsetheinput*)​​
    Parse
    [grammar,"the weather in NYC"]​​​​(*explicitcompile-getbackaparserholdingthecompiledcode*)​​parser=
    ParserCompile
    [grammar];​​parser["the weather in NYC"]
    Out[4@1]=
    Parse[grammar,the weather in NYC]
    Out[4@2]=
    ParserCompile[grammar][the weather in NYC]
    The compile step is the local analogue of
    CloudDeploy
    : it materialises a callable parser. The cloud path returns a
    CloudObject
    ; the local path returns a
    ParserCombinator
    with a
    "Code"->CompiledCodeFunction[...]
    entry added to its options. The presence of
    "Code"
    is what marks the parser as compiled - both
    Parse
    and the SubValues route compiled parsers through that function.
    The slot vocabulary is identical to the built-in one -
    <name>
    ,
    <name:Type>
    ,
    <name:Restricted[Type,constraints]>
    - and
    GrammarToken
    is honoured. The differences are confined to where compilation happens, not what a grammar means.

    Tier 2 - the combinator core

    For grammars where the slot-template DSL is too coarse - LaTeX environments, TPTP formula bodies, expression grammars with operator precedence, anything that needs backtracking control or lookahead - the
    Parse*
    constructors (with optional operator overloads) are the entry point. The example in Part 2 is the shape.

    How the tiers connect

    GrammarRules[...]
    lowers to a
    ParserCombinator
    expression internally. The two tiers are not parallel implementations of the same thing - tier 1 is a front-end to tier 2:
    GrammarRules[{"the weather in <city:Restricted[\"City\"]>" -> city}]
    │ lower
    ▼
    ParserCombinator[Action,
    {ParserCombinator[Sequence, {
    ParserCombinator[Literal, "the weather in ", <||>],
    ParserCombinator[Capture, {"city", Interpreter["City"]}, <||>]
    }, <||>],
    city &},
    <||>]
    │ ParserCompile
    ▼
    ParserCombinator[Action, {...}, <|"Code" -> CompiledCodeFunction[...]|>]
    Adding to either tier benefits the other: a new combinator becomes available as a lowering target for new slot syntaxes; a new slot syntax just extends the lowering.

    Part 4 - The parser algebra

    Concretely, a parser is a function of two arguments - the input and a starting position - that returns one of:
    The combinators are defined by structural equations:
    ParseSequence[p1, p2] (in, pos)
    = let r1 = p1 (in, pos);
    if r1 is ParseFailure, return r1;
    let (v1, pos1) = r1.value;
    let r2 = p2 (in, pos1);
    if r2 is ParseFailure, return r2;
    let (v2, pos2) = r2.value;
    return ParseSuccess[{v1, v2}, pos2].
    ​
    ParseChoice[p1, p2] (in, pos)
    = let r1 = p1 (in, pos);
    if r1 is ParseSuccess, return r1;
    let r2 = p2 (in, pos);
    if r2 is ParseSuccess, return r2;
    return ParseFailure[max(r1.pos, r2.pos), r1.expected ++ r2.expected].
    ​
    ParseMany[p] (in, pos)
    = let acc = {}, cur = pos;
    loop:
    let r = p (in, cur);
    if r is ParseFailure, return ParseSuccess[acc, cur];
    let (v, next) = r.value;
    acc := acc ++ {v}, cur := next;
    goto loop.
    ​
    ParseLookahead[p] (in, pos)
    = let r = p (in, pos);
    if r is ParseSuccess, return ParseSuccess[Null, pos]; (* position unchanged *)
    return r.
    ​
    ParseNotFollowedBy[p] (in, pos)
    = let r = p (in, pos);
    if r is ParseSuccess, return ParseFailure[pos, "not " ++ name(p)];
    return ParseSuccess[Null, pos].

    Two design choices worth flagging

    Part 5 - The compilation strategy

    Why FunctionCompile is the right hammer

    ◼
  • No C dependency. The compiler is part of the kernel. A user installing the paclet does not also install a toolchain.
  • The lowering pipeline

    ParserCombinator[...] expression (high-level, untyped, structural)
    │ Phase 1: normalisation
    ▼
    canonical ParserCombinator tree (every node is a primitive combinator)
    │ Phase 2: typing
    ▼
    typed ParserCombinator tree (each node tagged with its result type)
    │ Phase 3: result-encoding choice
    ▼
    result-encoded tree (results unified to one Typed[...] tag)
    │ Phase 4: codegen
    ▼
    FunctionCompile-ready function spec (a function (in, pos) -> Typed[...])
    │ Phase 5: FunctionCompile
    ▼
    ParserCombinator with "Code" -> CompiledCodeFunction in its options
    A few decisions in detail:

    What the compile does not buy us

    Honest accounting: FunctionCompile is not a magic 100× speedup for parsing.
    ◼
  • String access still bounces through UTF-8 decoding for any non-ASCII grammar. This is unavoidable without a separate tokenisation step.
  • ◼
  • Heap-allocating result construction (building an output AST) does not compile to anything faster than the interpreted path; we minimise it by lowering "structural" results to packed forms where possible.
  • A realistic expectation is 3-10× over the interpreted path on lexer-heavy grammars (LaTeX math, JSON, TPTP), and a smaller factor on grammars dominated by AST construction.

    Part 6 - Worked targets

    The choice of primitives above is motivated by two concrete targets. This section walks through how each maps onto the algebra, with enough detail to validate the design.

    Target A - LaTeX math

    math = expr;
    ​
    expr = ParseAction[
    term ~~ (ParseChoice[ParseLiteral["+"], ParseLiteral["-"]] ~~ term)...,
    Function[{first, rest}, FoldOperator[first, rest]]
    ];
    ​
    term = ParseAction[
    factor ~~ (ParseChoice[ParseLiteral["*"], ParseLiteral["/"]] ~~ factor)...,
    FoldOperator
    ];
    ​
    factor = group | command | atom;
    ​
    group = ParseBetween[ParseLiteral["{"], expr, ParseLiteral["}"]];
    ​
    command = ParseAction[
    ParseLiteral["\\"] ~~ ParseSome[ParseCharacter[LetterCharacter]] ~~
    Optional[bracketedArg] ~~ ParseMany[bracedArg],
    buildCommand
    ];
    ​
    bracedArg = ParseBetween[ParseLiteral["{"], expr, ParseLiteral["}"]];
    bracketedArg = ParseBetween[ParseLiteral["["], expr, ParseLiteral["]"]];
    ​
    atom = number | identifier | bigOperator;
    ​
    bigOperator = ParseAction[
    (ParseLiteral["\\sum"] | ParseLiteral["\\int"] | ParseLiteral["\\prod"]) ~~
    Optional[ParseLiteral["_"] ~~ group] ~~ Optional[ParseLiteral["^"] ~~ group],
    buildBigOperator
    ];
    What this grammar gets right that the current built-ins don't:

    Target B - TPTP

    fof(commutativity_of_plus, axiom,
    ! [X, Y]: (plus(X, Y) = plus(Y, X))).
    ​
    fof(some_property, conjecture,
    ? [X]: (greater(X, zero) & even(X))).
    The grammar is BNF, publicly specified, and of moderate size (~100 productions for full TFF/THF). The combinators that matter:
    A skeleton (non-evaluating prose - mutually recursive):
    tptpFile = formula...;
    ​
    formula = (ParseLiteral["fof"] | ParseLiteral["cnf"] | ParseLiteral["tff"] | ParseLiteral["thf"]) ~~
    ParseBetween[ParseLiteral["("], formulaBody, ParseLiteral[")"]] ~~
    ParseLiteral["."];
    ​
    formulaBody = name ~~ ParseLiteral[","] ~~ role ~~ ParseLiteral[","] ~~ logicFormula;
    ​
    logicFormula = quantified | binary | unit;
    ​
    quantified = (ParseLiteral["!"] | ParseLiteral["?"]) ~~ varList ~~ ParseLiteral[":"] ~~ logicFormula;
    ​
    binary = unit ~~ (ParseLiteral["&"] | ParseLiteral["|"] |
    ParseLiteral["=>"] | ParseLiteral["<=>"]) ~~ logicFormula;
    ​
    unit = atom | ParseBetween[ParseLiteral["("], logicFormula, ParseLiteral[")"]] | ParseLiteral["~"] ~~ unit;
    ​
    atom = predicate ~~ Optional[ParseBetween[ParseLiteral["("], ParseSepBy[term, ParseLiteral[","]], ParseLiteral[")"]]];
    ​
    term = variable | functionApp | constant;
    The grammar is straightforward parser-combinator material; the productions above lower to FunctionCompile-able forms by the standard pipeline. The motivation for including TPTP as a target is dual:
    1
    .
    It validates the scale of the grammar - if the combinator algebra cannot express a 100-production grammar cleanly, the algebra is wrong.
    2
    .
    It validates the integration story - the parser returns a structured AST that can be transformed (skolemisation, clausification) by other WL code, which is the use case that drives a lot of automated-reasoning work in the Wolfram ecosystem.

    What the targets imply about the algebra

    Part 7 - Open questions

    A list of things explicitly not decided in v0.1:
    2
    .
    Streaming input. The current design assumes the input is in memory (string or list). Streaming (parse-while-reading) is an open question - relevant for large TPTP corpora and for editor integration.
    3
    .
    Error recovery. A parse failure currently aborts the whole parse. Some grammars (editors, IDE tools) want to continue past a syntax error and collect multiple errors. This is a separate research problem (panic-mode recovery, FOLLOW-set recovery) and is deferred.
    Each of these will be answered by implementation experience against the targets above. The job of the v0.1 release is to ship the survey and this design, get the structure into the working directory, and start filling in the combinator primitives one at a time.

    What ships in v0.1

    ◼
  • A placeholder kernel.
  • What lands next (v0.2):

    The PEG-VM backend

    The consequences are decisive:
    ◼
  • It is fast - 1-2 orders of magnitude over the interpreter (≈55× on LaTeX, ≈500× on TPTP in informal benchmarks), because the recogniser is a tight native integer loop.
  • Still open: packrat memoisation (the VM has no memo table yet), richer failure diagnostics for the PEG-VM, and a non-ASCII class table.

    © 2026 Wolfram. All rights reserved.

    • Legal & Privacy Policy
    • Contact Us
    • WolframAlpha.com
    • WolframCloud.com