Parsing TPTP, Auto-Generated from the Published BNF
What this note covers
TPTP (Thousands of Problems for Theorem Provers) is the standard cross-prover benchmark corpus for automated reasoning - 26,264 problems across 57 mathematical domains, used by Vampire, E, Twee, Waldmeister, and every modern ATP system. Every problem is a
.p
file with one of six clause heads:
cnf
,
fof
,
tff
,
tcf
,
thf
,
ncf
. The TPTPWorld project publishes the formal grammar as a 735-line BNF file (
SyntaxBNF-v9.2.1.4
, 338 rules).
The paclet ships the result of this construction as
ships a catalogue index of the full 26,264-problem distribution and drives
TPTPImport
over any problem on demand from the online corpus. This note tells the story of how the parser is built - the BNF + action map that
TPTPImport
reuses internally. It works in two steps:
1
.
Read the BNF into an
Association[name->ParserCombinator]
.
2
.
Apply the result to TPTP source - optionally with an
"Actions"
map that lifts the raw parse tree to a Wolfram-Language data shape.
The lowering applies three standard PEG-vs-CFG rewrites automatically: direct left-recursion elimination, longest-alt-first sorting, and POSIX longest-match for rule bodies whose alternatives have equal element counts (the default
"ChoiceMode"->"Auto"
). Together these handle the eleven left-recursive TPTP rules, the shared-prefix alternatives like
Five clauses, quantifiers, function application, equality - all parsed. But the value above is the raw parse tree (a list of clauses, each clause a list of literal tokens and sub-rule results). For a workable downstream shape, pass a per-rule
"Actions"
map.
Lifting to a useful shape: the
"Actions"
map
Each entry in
"Actions"-><|name->fn|>
wraps the named rule's parser in a
ParseAction
. The function receives the rule's parsed value via the normal splatted convention -
Function[#1,#2,...]
indexes into the sequence of matched sub-pieces. The action map below is the one
is the positive goal. The action map is ~50 entries, one per BNF rule on the path from
<TPTP_file>
to
<constant>
. The recogniser is unchanged - the same
EBNFParse
call drives both the with-actions and without-actions flows.
Without actions, the parser is just a recogniser - it tells you whether the source matches the grammar but the value is the structural skeleton. The action layer is what turns that into a workable Wolfram Language data structure.
Benchmark on the published corpus
On the small CNF / FOF problems from the published
v9.2.1
distribution, per-clause parse time lands in the tens of milliseconds for short clauses (a few atoms, ground equations) and climbs into the hundreds of milliseconds for clauses with nested quantifiers and function applications. The 5-clause group-theory problem at the top of this note parses in ~700 ms end-to-end on a 2024 laptop with default
"ChoiceMode"->"Auto"
.
The cost of trying every alternative under longest-match without memoisation shows up on FOF with deep boolean / quantifier nesting. Adding
would close most of the throughput gap; a Pratt-style precedence climber for the connective grammar would be the right move for THF, where alternative explosion overwhelms even longest-match.
usage string: "v0.2: stubbed via the interpreter; the real FunctionCompile lowering lands later". A head-to-head bench on the 4 passing files confirms it - interpretive vs compiled are within measurement noise (1.00x speedup). Wiring up the real FunctionCompile path is a v0.4 item; the compiler infrastructure (
compilableQ
,
compileParser
,
interpretCompiledShim
,
CompileFeasibility
test suite) is in place but the per-combinator codegen rules aren't all written yet.
What the ChoiceMode flip fixes
The earlier draft of this note reported a cluster of failures around equations and disequations whose left side was a function application, in
cnf
and
fof
contexts. Example:
multiply(b,a)!=c
inside
cnf(_,negated_conjecture,multiply(b,a)!=c).
. PEG-ordered
Choice
was committing to
<fof_plain_atomic_formula>
on
multiply(b,a)
, then expecting end-of-clause, but the source continued with
!=c
which only
<fof_infix_unary>
(a sibling alt at the same
<cnf_literal>
level) reaches.
The default
"ChoiceMode"->"Auto"
resolves that whole cluster: for rule bodies whose alternatives have equal element counts (the static
longest-alt-first
sort can't break the tie), the lowering uses POSIX longest-match - every alt is tried at the current position and the one that consumed the most input wins. The
<fof_atomic_formula>
choice,
<cnf_literal>
choice, and a handful of other shape-ambiguous rules become correct without changing the grammar. The cost is some throughput - longest-match cannot early-exit on first hit, so deeply nested choices pay a constant factor over PEG. Set
"ChoiceMode"->"PEG"
to opt back into the fast-but-strict ordering when the grammar is unambiguous; set
"ChoiceMode"->"Longest"
to use longest-match unconditionally (correct on more grammars, slowest).
Remaining failures cluster on the higher-order alternatives in
<thf_*>
(the recursive
<thf_typeable_formula>
rule and friends). These exhibit exponential backtracking under any ordered-choice strategy and want either packrat memoisation or a Pratt-style precedence parser to be tractable - both unimplemented.
is interpretive-equivalent (stubbed), so per-file times will be within ~1% of the interpretive baseline. When the real FunctionCompile lowering lands, this swap is what surfaces the speedup.
What this approach buys
The EBNF-driven path means the grammar IS the parser definition - the parser cannot disagree with the published BNF because they are the same file. When TPTP-v9.3 ships, re-run
EBNFParse
on the new BNF and re-bind the actions; no per-rule diff. The recogniser drops out for free: 280 of the 338 TPTP rules lower automatically, and small CNF / FOF clauses parse end to end via a generic mechanism. Compare this to a hand-coded parser, where every grammar change is a per-rule code edit.
What still needs hand-work:
◼
Term-level disambiguation. The
<fof_plain_term>
left-factoring issue described above. Either a deeper structural rewrite or memoisation.
◼
THF higher-order. The mutual recursion + alternative explosion in the higher-order grammar needs memoisation or a different parsing strategy (e.g. Pratt-style precedence climbing) to be tractable.
◼
Lexical primitives. The
PrimitiveOverrides
map above covers the common cases; a complete map adds
real
,
rational
,
dollar_dollar_word
, the spacing tokens, comment / whitespace rules. A small
(built on this construction) is the entry point. For evolving formal-grammar work where the published spec is moving, the EBNF-driven path is what keeps the parser in lockstep with the spec.