defmodule ShotTo do
@moduledoc """
`ShotTo` provides a decidable instance of the βη-long-normal Computability
Path Order (NCPO-LNF) of Niederhauser and Middeldorp (*NCPO goes
Beta-Eta-Long Normal Form*, 2025) for terms of Church's simple type theory
as represented by the `shot_ds` library.
The order is parameterised by a sort precedence, a constant precedence
with associated statuses and accessibility, and a choice of basic sorts;
see `ShotTo.Parameters`. Given a fixed choice of parameters the relation
`s > t` is a decidable boolean predicate that `ShotTo` implements directly
rather than as an SMT constraint, making it suitable for use as an
orientation order inside tableau and other proof-search procedures.
## Status
As of the date of the paper, NCPO-LNF is *not known* to be transitive;
the authors prove only the properties needed for termination of
higher-order rewrite systems (well-foundedness, monotonicity,
βη-long-normal stability). `ShotTo` is therefore best used pairwise — to
decide whether one term should be oriented above another — rather than
as the basis of a total comparator over a collection of terms. Using it
as a key for sorting in particular may give surprising results until
transitivity is established.
## Example
iex> import ShotDs.Hol.Definitions
iex> alias ShotDs.Stt.TermFactory, as: TF
iex> alias ShotTo.Parameters
...>
iex> # Constants f and c of base type i, with f > c in precedence.
iex> params = Parameters.new(const_precedence: %{"f" => 1, "c" => 0})
iex> f = TF.make_const_term("f", Type.new(:i, :i))
iex> c = TF.make_const_term("c", type_i())
iex> fc = ShotDs.Hol.Dsl.app(f, c)
...>
iex> # f(c) > c by ⟨F▷⟩ (c is a subterm of f(c)).
iex> ShotTo.gt?(fc, c, params)
true
iex> ShotTo.gt?(c, fc, params)
false
"""
alias ShotDs.Data.Term
alias ShotTo.Parameters
@doc """
Returns `true` iff `s >^1_τ t` in NCPO-LNF under the given parameters.
`s_id` and `t_id` are term IDs as produced by `ShotDs.Stt.TermFactory`.
With no parameters supplied, the default (permissive) parameter set is
used; see `ShotTo.Parameters` for details on the defaults.
"""
@spec gt?(Term.term_id(), Term.term_id()) :: boolean()
def gt?(s_id, t_id), do: gt?(s_id, t_id, Parameters.new())
@spec gt?(Term.term_id(), Term.term_id(), Parameters.t()) :: boolean()
def gt?(s_id, t_id, %Parameters{} = params),
do: ShotTo.Ncpo.ncpo_gt?(s_id, t_id, params)
@doc """
Returns `true` iff `s ⪰^1_τ t` in NCPO-LNF, i.e. either the two terms are
(structurally, thanks to ETS memoisation: referentially) equal or
`s >^1_τ t`.
"""
@spec geq?(Term.term_id(), Term.term_id()) :: boolean()
def geq?(s_id, t_id), do: geq?(s_id, t_id, Parameters.new())
@spec geq?(Term.term_id(), Term.term_id(), Parameters.t()) :: boolean()
def geq?(s_id, t_id, %Parameters{} = params) do
s_id == t_id or gt?(s_id, t_id, params)
end
@doc """
Compares two term IDs in NCPO-LNF. Returns
* `:greater` if `s > t`,
* `:less` if `t > s`,
* `:equal` if the terms are structurally equal (same term ID), and
* `:incomparable` otherwise.
Because NCPO-LNF is not (yet known to be) transitive, the `:incomparable`
case is a genuine possibility even for terms that intuitively "ought to
be" comparable under some total ordering.
Two NCPO-LNF calls are made in the worst case (once in each direction).
Both are run inside a single scratchpad so intermediate fresh variables
are collected together.
"""
@spec compare(Term.term_id(), Term.term_id()) :: :greater | :less | :equal | :incomparable
def compare(s_id, t_id), do: compare(s_id, t_id, Parameters.new())
@spec compare(Term.term_id(), Term.term_id(), Parameters.t()) ::
:greater | :less | :equal | :incomparable
def compare(s_id, t_id, %Parameters{} = params) do
cond do
s_id == t_id -> :equal
gt?(s_id, t_id, params) -> :greater
gt?(t_id, s_id, params) -> :less
true -> :incomparable
end
end
end