Tptp.Lint (Tptp v0.1.0)

Copy Markdown View Source

The :== well-formedness conditions, and the conditions requiring more than one statement.

The grammar admits considerably more than the language defines. <formula_role> ::= <lower_word> admits wibble; <defined_functor> ::= <atomic_defined_word> admits $wibble; <thf_top_level_type> admits a rank-2 type that no TPTP implementation accepts. The :== rules of the BNF state which of these are well formed, and the parser does not enforce them: input violating one still yields a usable tree, and refusing to produce it would preclude the malformed input this library exists to describe.

{:ok, file, []} = Tptp.from_string("fof(a, wibble, p).")
Tptp.Lint.run(file)
#=> [%Tptp.Diagnostic{code: "TPTP0401", severity: :warning, ...}]

Single traversal

run/2 traverses once, offering each node to every enabled rule, and accumulates the symbol table and the dialect features in the same pass. Rules requiring the complete picture run afterwards against the table rather than the tree. Eight rules over a large axiom set would otherwise require eight traversals of a tree that does not fit in cache.

No type inference

The symbol table records a declared type as the unelaborated Tptp.Node it was written as. No unification or substitution is performed, and $i is not interpreted as a type. Every rule is either syntactic or declines. See Tptp.Lint.Rules.Rank1, which determines only whether a !> occurs within a typing.

Absence of a dialect rule

Use of a construct in a language that does not provide it is not reportable, because it cannot occur. The BNF gives each language its own nonterminals, so ^ is unreachable from <tff_formula>, !! from <fof_formula>, and a THF tuple from anything but THF. Each was tested against every statement keyword; the grammar rejects all of them at parse time, so such a rule could never apply.

What remains is classification rather than defect — a tff file using !> is TF1 rather than TF0, and well formed — which Tptp.Query.dialect/1 derives from the same traversal.

Severities

A rule's severity is a default. :severity overrides it per code, and :only/:except select the rules that run. No shipped rule reports an error on a conforming TPTP library file, and a corpus test enforces this. One rule is :info rather than :warning: Tptp.Lint.Rules.Conjecture reports a property of the problem rather than a defect in it. The remainder are warnings.

Analyzer interface

Tptp.Lint implements Tptp.Analyzer as :tptp_lint, applicable to any dialect. The callback performs the traversal under the options it is given. Consumers requiring only the default rule set should read analysis.diagnostics from Tptp.analyze/2 rather than dispatching through Tptp.Analyzer.run_all/3.

Summary

Types

Options accepted by run/2.

The diagnostics a lint pass produced and the table it built, from one traversal.

Functions

Returns the shipped rules, in the order each node is offered to them.

Lint one file.

Lint a whole unit, includes expanded.

One traversal, both halves kept.

Every statement of a file or unit, paired with the file it came from.

The symbol table and feature set, without running a single rule.

Types

option()

@type option() ::
  {:only, [module()]}
  | {:except, [module()]}
  | {:severity, %{required(binary()) => Tptp.Diagnostic.severity()}}
  | {:suppress, [binary()]}

Options accepted by run/2.

  • :only — run only these rule modules.
  • :except — run every rule but these.
  • :severity — a map from code to severity, overriding the rule's default. severity: %{"TPTP0401" => :error} raises an unrecognised role to an error. Keyed by binary rather than atom: a diagnostic code originates in configuration, and converting it to an atom for lookup would admit unbounded growth of the atom table.
  • :suppress — diagnostic codes to discard.

scan()

@type scan() :: {[Tptp.Diagnostic.t()], Tptp.Lint.Table.t()}

The diagnostics a lint pass produced and the table it built, from one traversal.

Functions

rules()

@spec rules() :: [module()]

Returns the shipped rules, in the order each node is offered to them.

run(file, options \\ [])

@spec run(Tptp.File.t(), [option()]) :: [Tptp.Diagnostic.t()]

Lint one file.

iex> {:ok, file, []} = Tptp.from_string("fof(a, axiom, p). fof(a, axiom, q).")
iex> file |> Tptp.Lint.run() |> Enum.map(& &1.code)
["TPTP0503"]

run_unit(unit, options \\ [])

@spec run_unit(Tptp.Unit.t(), [option()]) :: [Tptp.Diagnostic.t()]

Lint a whole unit, includes expanded.

A declaration in an axiom file and a use in the problem file are one symbol here, which is the point: an undeclared-symbol rule that could not see across an include would report every typed problem in the library.

scan(subject, options \\ [])

@spec scan(Tptp.File.t() | Tptp.Unit.t(), [option()]) :: scan()

One traversal, both halves kept.

Every enabled rule is offered every node, and the symbol table and dialect features accumulate in the same pass; the diagnostics and the finished table are returned together rather than one being recomputed later.

run/2, run_unit/2 and table/1 are projections of this. Pass only: [] to build the table without applying a rule, as table/1 does.

statements(file)

@spec statements(Tptp.File.t() | Tptp.Unit.t()) :: [
  {Tptp.Span.file_id(), Tptp.Statement.t()}
]

Every statement of a file or unit, paired with the file it came from.

table(subject)

@spec table(Tptp.File.t() | Tptp.Unit.t()) :: Tptp.Lint.Table.t()

The symbol table and feature set, without running a single rule.

The same traversal run/2 makes, stopping before the opinions. Tptp.Query is built on this, which is what maintains "one walk" true across both modules rather than only within one of them.