Tokenises TPTP source, one statement per call.
Resumability
The scanner is resumable over (source, offset) and returns the tokens of a
single statement per call. The source is retained whole as one reference-counted
binary; the token stream is not materialised. Peak memory is therefore bounded by
the largest statement rather than by the size of the input, which matters at the
scale of the larger TPTP axiom sets: 75 million tokens held as three-tuples
require approximately 2.4 GB.
Byte-oriented matching
Every character class in the BNF lies within 40–176, so byte matching is
correct and the UTF-8 aware, grapheme-based functions of String are both
unnecessary and slower. This module uses byte patterns and :binary throughout.
Non-ASCII bytes can occur only inside a quoted atom or a comment in non-conforming input. They are carried through without validation, since validating them would introduce a branch per byte of every string for a condition a lint rule can evaluate from the token text.
Token representation
A token is {category, offset, length}. Text is recovered later with
binary_part/3, so scanning allocates nothing per token beyond the tuple.
Longest match
The operator clauses are generated from Tptp.Token.operators/0, which is
ordered by descending spelling length, so <=> is attempted before <= before
< as a property of the table rather than of the clause order.
Three cases are not resolved by the table alone:
- Statement terminator against decimal point. Numbers are scanned from
their leading digit and consume their own
.; the terminator clause applies only to a.not followed by a digit.p(3).is therefore terminated correctly, since<decimal_fraction>requires digits after the point. - Words are scanned to their end rather than truncated at a prefix that
forms a keyword.
$letteris a singledollar_wordandfilenamea singlelower_word.Tptp.Splitterrelies on this when resolving keywords by whole-word comparison. [.],<.>,{.}and(.)are single tokens. Without this,<.>would introduce a.at bracket depth zero and divide a statement.
Accepted departures from the BNF
Two forms lex without ambiguity but are not admitted by the BNF. Each yields a token together with a warning, rather than being silently accepted or rejected:
- Empty quoted token.
<single_quoted>requires at least one<sq_char>and<distinct_object>at least one<do_char>, so neither''nor""is admitted (TPTP0107). - Redundant leading zero.
<unsigned_integer>is0or a digit sequence beginning1–9, so00,-007and01.5are not admitted (TPTP0110). A rational's denominator is a<positive_integer>, which may not begin with0, so neither1/02nor1/0is a<rational>(TPTP0111).
Emitting the token allows the statement to parse, which is why these are
warnings. That the list is exactly these two is checked rather than asserted:
Tptp.Bnf.OracleTable transcribes the ::- and ::: rules mechanically, and
Tptp.LexerOracleTest verifies that every token emitted without a diagnostic
satisfies its BNF pattern.
Deferred decisions
This module emits lower_word for fof, inference and file, and
dollar_word for $let and $thf. These are ordinary words except in
positions that admit a keyword reading, and position is resolved by
Tptp.Splitter: the first token of a statement, and a word immediately preceding
(. Resolving them here would reject fof(fof, axiom, p)., which is well
formed, and would divide keyword resolution across two modules.
Comments
The BNF permits comments between any two tokens but does not treat them as white
space. They are collected into a separate ordered list rather than the token
stream, so the parser does not observe them and the format-preserving printer can
reattach them by position. %$ and /*$ introduce a defined comment and %$$
and /*$$ a system comment; both are reserved by the BNF and used by some tools,
so the classification is retained.
Summary
Functions
The bytes a comment covers, including its % or /* */ delimiters.
Scan the next statement starting at offset.
Scan an entire source into statements, eagerly.
The bytes a token covers.
Types
@type comment() :: {offset :: non_neg_integer(), length :: non_neg_integer(), :line | :block, :plain | :defined | :system}
Where a comment came from, and whether it is a pragma.
@type result() :: {:statement, [Tptp.Token.t()], non_neg_integer(), [comment()], [Tptp.Diagnostic.t()]} | {:eof, non_neg_integer(), [comment()], [Tptp.Diagnostic.t()]}
One call's worth of scanning.
:statement carries the tokens up to and including a bracket-depth-zero .;
:eof means the input was exhausted. Both carry the comments and diagnostics
seen along the way and the offset to resume from.
Functions
The bytes a comment covers, including its % or /* */ delimiters.
@spec next_statement(binary(), non_neg_integer(), Tptp.Span.file_id()) :: result()
Scan the next statement starting at offset.
file is stamped into any diagnostic's span; it is the id under which the caller
registered this source, and defaults to zero for a single-file scan.
iex> {:statement, tokens, next, [], []} = Tptp.Lexer.next_statement("fof(a,axiom,p).", 0)
iex> Enum.map(tokens, &elem(&1, 0))
[:lower_word, :lparen, :lower_word, :comma, :lower_word, :comma, :lower_word, :rparen, :dot]
iex> next
15
@spec statements(binary(), Tptp.Span.file_id()) :: {[[Tptp.Token.t()]], [comment()], [Tptp.Diagnostic.t()]}
Scan an entire source into statements, eagerly.
A convenience over next_statement/3 for small inputs and for tests. Anything
large should go through the streaming path instead, for the reason in the
moduledoc.
@spec text(Tptp.Token.t(), binary()) :: binary()
The bytes a token covers.
A sub-binary, not a copy. See Tptp.Span.text/2 for when that matters.