Powered by AppSignal & Oban Pro

Demo of the ShotDs Package

examples/demo.livemd

Demo of the ShotDs Package

Mix.install([
  {:shot_ds, "~> 1.0"}
])

Installation and Setup

This package can be installed by adding shot_ds to your list of dependencies in mix.exs:

def deps do
  [
    {:shot_ds, "~> 1.0"}
  ]
end

Note: When parsing files from the TPTP problem library, it is additionally necessary to set up an environment variable TPTP_ROOT pointing to its root directory. This also enables parsing dependencies in TPTP files.

Core Concepts and Architecture

This library models various data structures from classical higher-order logic (HOL) and simple type theory (STT). It relies on (but is not limited to) the two base types $o$ for booleans and $\iota$ for individuals. Internally, types are represented as a struct composed of a goal type and a list of argument types:

alias ShotDs.Data.Type

Type.new(:i) |> IO.inspect(label: "type i")
Type.new(:o, [:i, :o]) |> IO.inspect(label: "type i->o->o")
:ok

Terms are stored as Elixir structs acting as directed acyclic graphs (DAGs). They are assigned a (concurrency-safe) ID which is processed by the ShotDs.Stt.TermFactory module. These IDs are used for a caching mechanism using the Erlang term storage (ETS) under the hood. Notice that terms are represented in $\beta\eta$-normal form and use de Bruijn indices.

alias ShotDs.Stt.TermFactory, as: TF
import ShotDs.Util.Formatter # pretty-printing

x_id = TF.make_free_var_term("X", Type.new(:i, [:i, :o])) |> IO.inspect(label: "assigned ID")
TF.get_term!(x_id) |> IO.inspect(label: "generated term")
format!(x_id, _hide_types=false) |> IO.inspect(label: "pretty-printed")
:ok

Term Construction with the DSL

The module ShotDs.Hol.Dsl introduces a domain-specific language for shorthand term construction. It utilizes the unused Elixir operators &&&, |||, ~> and <~> as infix-constructors. Together with the module ShotDs.Hol.Definitions, we can build the XOR operator as follows:

import ShotDs.Hol.Definitions
import ShotDs.Hol.Dsl

exclusive_or = lambda([type_o(), type_o()], fn p, q ->
  (p ||| q) &amp;&amp;&amp; neg(p &amp;&amp;&amp; q)
end)

format!(exclusive_or) |> IO.puts()

Parsing TH0 Strings

The module ShotDs.Parser offers powerful parsing capabilities with full type inference for inputs in TPTP (TH0) format:

alias ShotDs.Parser

Parser.parse!("?[X : $o]: X => $true")
|> format!() |> IO.puts()

Note that the type inference engine assumes type $o$ for the outermost type unless inferable otherwise. Other unknown types are treated as type variables and are assigned a unique ID:

Parser.parse!("X @ a")
|> format!(_hide_types = false) |> IO.puts()

To clear up ambiguities, we can pass in a type environment:

alias ShotDs.Data.Context

ctx = Context.new() |> Context.put_const("a", type_i())

Parser.parse!("X @ a", ctx)
|> format!() |> IO.puts()

Parsing is also available via Sigils for TH0 formula strings (~f), TPTP problems (~p) (wiht thf(...) tags) and type environments (~e), including a wrapper for providing context:

import ShotDs.Hol.Sigils

~f(?[X : $o]: X => $true) |> format!() |> IO.puts()

with_context(~e[X : $o, p : $o>$i], fn ->
  ~f(p @ X) |> format!(false) |> IO.puts()
end)

Advanced Utilities

Error Handling: Functions that can fail return a tuple {:ok, result} or {:error, reason} per default. There are “bang” versions (suffixed by !) of all functions that either return the result directly or raise an error. with-clauses offer idiomatic error handling when processing user input and are generally preferred over exceptions:

with {:ok, term_id} <- Parser.parse("a & b"),
     {:ok, formatted} <- format(term_id) do
  IO.puts(formatted)
else
  {:error, msg} -> IO.puts("ERROR: #{msg}")
end

with {:ok, term_id} <- Parser.parse("a &"),
     {:ok, formatted} <- format(term_id) do
  IO.puts(formatted)
else
  {:error, msg} -> IO.puts("ERROR: #{msg}")
end

try do
  Parser.parse!("a &")
rescue
  e in Parser.ParseError ->
    IO.puts(~s'Rescued Parser.ParseError with message: "#{e.message}"')
end

File Parsing: ShotDs.Tptp includes parsing capabilities for TPTP files. The collected information is aggregated in a ShotDs.Data.Problem struct.

problem = ~p"""
thf(e_type,type,
    e: $tType).

thf(p_type,type,
    p: e>$o).

thf(lma,lemma,
    ![X : e]: p @ X).

thf(conj,conjecture,
    ?[Y : e]: p @ Y).
"""

IO.inspect(problem)

format!(problem) |> IO.puts()

Term Manipulation: Various functions regarding the semantics of STT, e.g. substitutions, are handled by the ShotDs.Stt.Semantics module.

alias ShotDs.Data.Substitution
alias ShotDs.Stt.Semantics

x = var("X", type_ii())
a = const("a", type_i())
b = const("b", type_ii())

x_a = app(x, a)
x_a |> format!(true) |> IO.puts()

s = %Substitution{fvar: TF.get_term!(x).head, term_id: b}

Semantics.subst!(s, x_a) |> format!(true) |> IO.puts()

Church Encoding: ShotDs.Stt.Numerals provides an implementation of Church’s encoding of natural numbers, adapted to simple types. This can for example be used for benchmarking. Additionally, Church’s encoding of Booleans is implemented in the module ShotDs.Stt.Booleans.

import ShotDs.Stt.Numerals

plus(num(5), num(2)) |> format!() |> IO.puts()
import ShotDs.Stt.Booleans

b_and(tt(), b_var("P")) |> format!() |> IO.puts()

Traversals and Transformation: Transformations of terms mostly follow the same scheme. An optimized adaption of the combinators fold (fold_term/2, fold_term!/2) and map (map_term/5, map_term!/5) to the DAG representation of terms is implemented in the module ShotDs.Util.TermTraversal. Examples for the usage of these higher-order functions can be found in the source code of the ShotDs.Stt.Semantics module or in ShotDs.Util.Formatter.format_term/2.

Pattern Matching: ShotDs.Hol.Patterns provides macro definitions for pattern matching on various HOL terms.

use ShotDs.Hol.Patterns

case TF.get_term!(true_term() ||| false_term()) do
  disjunction(p, q) -> IO.puts("Disjunction of #{format!(p)} and #{format!(q)}")
  _ -> IO.puts("No match")
end

Garbage Collection: Wrapping code with ShotDs.Stt.TermFactory.with_scratchpad/1 ensures temporary terms created by that code to be garbage collected afterwards. In this process, terms are written to a local ETS table and only the final result is (recursively) committed to the global table.