Getting started

In this introductory section you will learn about the main building blocks of SoleLogics. Their definition, usage examples and how to customize them to your own needs.

In short, you can consider this package as divided into two halves.:

  • the syntactical half, which defines structures to represent logical constructs such as assertions, logical constants, alphabets, grammars, crisp and fuzzy algebras, formulas etc. A consistent part of SoleLogics is devoted to randomly generate such structures, as well as parse and minimize formulas;
  • the semantic half, which defines rules to apply when interpreting a logical formulas. The "semantic heart" of SoleLogics is its finite model checking algorithm, whose purpose is to efficiently check whether a formula is satisfied by an interpretation or not.

Please, feel free to use the following tree structures to orient yourself in the reading of this section. More pieces will be added to this type-hierarchy tree in the following sections.

Type hierarchy

Also, two union types are defined:

Syntax Basics

SoleLogics.SyntacticalType
abstract type Syntactical end

Master abstract type for all syntactical objects (e.g., formulas, connectives).

Interface

  • syntaxstring(s::Syntactical; kwargs...)::String

See also Formula, Connective.

source

To print out a generic Syntactical element, we must define how it is converted into a string. To do this, we can implement a custom syntaxstring.

SoleLogics.syntaxstringMethod
syntaxstring(s::Syntactical; kwargs...)::String

Return the string representation of any syntactic object (e.g., Formula, SyntaxTree, SyntaxToken, Atom, Truth, etc). Note that this representation may introduce redundant parentheses. kwargs can be used to specify how to display syntax tokens/trees under some specific conditions.

The following kwargs are currently supported:

  • function_notation = false::Bool: when set to true, it forces the use of function notation for binary operators (see here).
  • remove_redundant_parentheses = true::Bool: when set to false, it prints a syntaxstring where each syntactical element is wrapped in parentheses.
  • parenthesize_atoms = !remove_redundant_parentheses::Bool: when set to true, it forces the atoms (which are the leaves of a formula's tree structure) to be wrapped in parentheses.

Examples

julia> syntaxstring(parseformula("p∧q∧r∧s∧t"))
"p ∧ q ∧ r ∧ s ∧ t"

julia> syntaxstring(parseformula("p∧q∧r∧s∧t"), function_notation=true)
"∧(∧(∧(∧(p, q), r), s), t)"

julia> syntaxstring(parseformula("p∧q∧r∧s∧t"), remove_redundant_parentheses=false)
"((((p) ∧ (q)) ∧ (r)) ∧ (s)) ∧ (t)"

julia> syntaxstring(parseformula("p∧q∧r∧s∧t"), remove_redundant_parentheses=true, parenthesize_atoms=true)
"(p) ∧ (q) ∧ (r) ∧ (s) ∧ (t)"

julia> syntaxstring(parseformula("◊((p∧s)→q)"))
"◊((p ∧ s) → q)"

julia> syntaxstring(parseformula("◊((p∧s)→q)"); function_notation = true)
"◊(→(∧(p, s), q))"

See also parseformula, SyntaxBranch, SyntaxToken.

Implementation

In the case of a syntax tree, syntaxstring is a recursive function that calls itself on the syntax children of each node. For a correct functioning, the syntaxstring must be defined (including the kwargs... part!) for every newly defined SyntaxToken (e.g., SyntaxLeafs, that is, Atoms and Truth values, and Operators), in a way that it produces a unique string representation, since Base.hash and Base.isequal, at least for SyntaxTrees, rely on it.

In particular, for the case of Atoms, the function calls itself on the wrapped value:

syntaxstring(a::Atom; kwargs...) = syntaxstring(value(a); kwargs...)

The syntaxstring for any value defaults to its string representation, but it can be defined by defining the appropriate syntaxstring method.

Warning

The syntaxstring for syntax tokens (e.g., atoms, operators) should not be prefixed/suffixed by whitespaces, as this may cause ambiguities upon parsing. For similar reasons, syntaxstrings should not contain parentheses ('(', ')'), and, when parsing in function notation, commas (',').

See also SyntaxLeaf, Operator, parseformula.

source
SoleLogics.ConnectiveType
abstract type Connective <: Syntactical end

Abstract type for logical connectives, that are used to express non-atomic statements; for example, CONJUNCTION, DISJUNCTION, NEGATION and IMPLICATION (stylized as ∧, ∨, ¬ and →).

Interface

  • arity(::Connective)::Int
  • iscommutative(::Connective)::Bool
  • precedence(::Connective)::Int
  • associativity(::Connective)::Symbol
  • collatetruth(::Connective, ::NTuple{N,Truth})::Truth
  • simplify(::Connective, ::NTuple{N,SyntaxTree})::SyntaxTree
  • dual(s::Connective)::Connective
  • hasdual(s::Connective)::Bool
  • See also Syntactical

Implementation

When implementing a new type C for a connective, please define its arity. For example, with a binary operator (e.g., ∨ or ∧):

arity(::C) = 2

When implementing a new type C for a commutative connective with arity higher than 1, please provide a method iscommutative(::C). This can speed up model checking operations.

When implementing a custom binary connective, one can override the default precedence and associativity (see here. If the custom connective is a NamedConnective and renders as something considered as a math symbol (for example, , see https://stackoverflow.com/a/60321302/5646732), by the Julia parser, Base.operator_precedence and Base.operator_associativity are used to define these behaviors, and you might want to avoid providing these methods at all.

The semantics of a propositional connective can be specified via collatetruth (see example below); in principle, the definition can rely on the partial order between truth values (specified via precedes).

Here is an example of a custom implementation of the xor (⊻) Boolean operator.

import SoleLogics: arity, iscommutative, collatetruth
const ⊻ = SoleLogics.NamedConnective{:⊻}()
SoleLogics.arity(::typeof(⊻)) = 2
SoleLogics.iscommutative(::typeof(⊻)) = true
SoleLogics.collatetruth(::typeof(⊻), (t1, t2)::NTuple{N,T where T<:BooleanTruth}) where {N} = (count(istop, (t1, t2)) == 1)

Note that collatetruth must be defined at least for some truth value types T via methods accepting an NTuple{arity,T} as a second argument.

To make the operator work with incomplete interpretations (e.g., when the Truth value for an atom is not known), simplification rules for NTuple{arity,T where T<:Formula}s should be provided via methods for simplify. For example, these rules suffice for simplifying xors between TOP/BOT`s, and other formulas:

import SoleLogics: simplify
simplify(::typeof(⊻), (t1, t2)::Tuple{BooleanTruth,BooleanTruth}) = istop(t1) == istop(t2) ? BOT : TOP
simplify(::typeof(⊻), (t1, t2)::Tuple{BooleanTruth,Formula}) = istop(t1) ? ¬t2 : t2
simplify(::typeof(⊻), (t1, t2)::Tuple{Formula,BooleanTruth}) = istop(t2) ? ¬t1 : t1

Beware of dispatch ambiguities!

See also arity, SyntaxBranch, associativity, precedence, check, iscommutative, NamedConnective, Syntactical.

source

If the definition above overwhelms you, don't worry: it will be clearer later. For now we are simply interested in understanding that Connectives are simply symbols used to concatenate other logical constructs with each other.

Later, we will see some interesting example about how to equip these symbols with semantics, that is, what rules should be applied when interpreting connectives in a generic Formula. We will also understand how to define our own custom connectives.

SoleLogics.arityMethod
arity(φ::SyntaxTree)::Integer
arity(tok::Connective)::Integer

Return the arity of a Connective or a SyntaxTree. The arity is an integer representing the number of allowed children for a node in a tree. Connectives with arity equal to 0, 1 or 2 are called nullary, unary and binary, respectively. SyntaxLeafs (Atoms and Truth values) are always nullary.

See also SyntaxLeaf, Connective, SyntaxBranch.

source

The vast majority of data structures involved in encoding a logical formula, are children of the Formula abstract type. When such data structures purely represents tree-shaped data structures (or single nodes in them), then they are also children of the SyntaxStructure abstract type.

SoleLogics.FormulaType
abstract type Formula <: Syntactical end

Abstract type for logical formulas. Examples of Formulas are SyntaxLeafs (for example, Atoms and Truth values), SyntaxStructures (for example, SyntaxTrees and LeftmostLinearForms) and TruthTables ( enriched representation, which associates a syntactic structure with additional memoization structures, which can save computational time upon model checking).

Any formula can be converted into its SyntaxTree representation via tree; its height can be computed, and it can be queried for its syntax tokens, atoms, etc... It can be parsed from its syntaxstring representation via parseformula.

Interface

  • tree(φ::Formula)::SyntaxTree
  • composeformulas(c::Connective, φs::NTuple{N,F})::F where {N,F<:Formula}
  • See also Syntactical

Utility functions (requiring a walk of the tree)

  • Base.in(tok::SyntaxToken, φ::Formula)::Bool

  • height(φ::Formula)::Int

  • tokens(φ::Formula)::AbstractVector{<:SyntaxToken}

  • atoms(φ::Formula)::AbstractVector{<:AbstractAtom}

  • truths(φ::Formula)::AbstractVector{<:Truth}

  • leaves(φ::Formula)::AbstractVector{<:SyntaxLeaf}

  • connectives(φ::Formula)::AbstractVector{<:Connective}

  • operators(φ::Formula)::AbstractVector{<:Operator}

  • ntokens(φ::Formula)::Int

  • natoms(φ::Formula)::Int

  • ntruths(φ::Formula)::Int

  • nleaves(φ::Formula)::Int

  • nconnectives(φ::Formula)::Int

  • noperators(φ::Formula)::Int

See also tree, SyntaxStructure, SyntaxLeaf.

source

The following methods define Formula interface.

SoleLogics.treeMethod
tree(φ::Formula)::SyntaxTree

Return the SyntaxTree representation of a formula; note that this is equivalent to Base.convert(SyntaxTree, φ).

See also Formula, SyntaxTree.

source
SoleLogics.tokensMethod
tokens(φ::Formula)::AbstractVector{<:SyntaxToken}
atoms(φ::Formula)::AbstractVector{<:Atom}
truths(φ::Formula)::AbstractVector{<:Truth}
leaves(φ::Formula)::AbstractVector{<:SyntaxLeaf}
connectives(φ::Formula)::AbstractVector{<:Connective}
operators(φ::Formula)::AbstractVector{<:Operator}
ntokens(φ::Formula)::Integer
natoms(φ::Formula)::Integer
ntruths(φ::Formula)::Integer
nleaves(φ::Formula)::Integer
nconnectives(φ::Formula)::Integer
noperators(φ::Formula)::Integer

Return the list/number of (non-unique) SyntaxTokens, Atoms, etc... appearing in a formula.

See also Formula, SyntaxToken.

source

Now, let us see how to compose syntax elements, to express more complex concepts.

SoleLogics.composeformulasMethod
composeformulas(c::Connective, φs::NTuple{N,F})::F where {N,F<:Formula}

Return a new formula of type F by composing N formulas of the same type via a connective c. This function allows one to use connectives for flexibly composing formulas (see Implementation section).

Examples

julia> f = parseformula("◊(p→q)");

julia> p = Atom("p");

julia> ∧(f, p)  # Easy way to compose a formula
SyntaxBranch: ◊(p → q) ∧ p

julia> f ∧ ¬p   # Leverage infix notation ;) (see https://stackoverflow.com/a/60321302/5646732)
SyntaxBranch: ◊(p → q) ∧ ¬p

julia> ∧(f, p, ¬p) # Shortcut for ∧(f, ∧(p, ¬p))
SyntaxBranch: ◊(p → q) ∧ p ∧ ¬p

Implementation

Upon composeformulas lies a flexible way of using connectives for composing formulas and syntax tokens (e.g., atoms), given by methods like the following:

function (c::Connective)(φs::NTuple{N,Formula}) where {N}
    ...
end

These allow composing formulas as in ∧(f, ¬p), and in order to access this composition with any newly defined subtype of Formula, a new method for composeformulas should be defined, together with promotion from/to other Formulas should be taken care of (see here and here).

Similarly, for allowing a (possibly newly defined) connective to be applied on a number of syntax tokens/formulas that differs from its arity, for any newly defined connective c, new methods similar to the two above should be defined. For example, although ∧ and ∨ are binary, (i.e., have arity equal to 2), compositions such as ∧(f, f2, f3, ...) and ∨(f, f2, f3, ...) can be done thanks to the following two methods that were defined in SoleLogics:

function ∧(
    c1::Formula,
    c2::Formula,
    c3::Formula,
    cs::Formula...
)
    return ∧(c1, ∧(c2, c3, cs...))
end
function ∨(
    c1::Formula,
    c2::Formula,
    c3::Formula,
    cs::Formula...
)
    return ∨(c1, ∨(c2, c3, cs...))
end
Note

To allow for the composition of Formulas of different types, promotion rules should be provided.

See also Formula, Connective.

source

We are ready to see how logical formulas are represented using syntax trees

SoleLogics.SyntaxTreeType
abstract type SyntaxTree <: SyntaxStructure end

Abstract type for syntax trees; that is, syntax leaves (see SyntaxLeaf, such as Truth values and Atoms), and their composition via Connectives (i.e., SyntaxBranch).

Note that SyntaxTree are ranked trees, and (should) implement AbstractTrees interface.

Interface

  • children(φ::SyntaxTree)::NTuple{N,SyntaxTree} where N
  • token(φ::SyntaxTree)::Connective
  • See also SyntaxStructure

Utility functions

  • tokentype(φ::SyntaxTree)
  • arity(φ::SyntaxTree)::Int

Other utility functions (requiring a walk of the tree)

  • Base.in(tok::SyntaxToken, φ::SyntaxTree)::Bool
  • height(φ::SyntaxTree)::Int
  • tokens(φ::SyntaxTree)::AbstractVector{<:SyntaxToken}
  • atoms(φ::SyntaxTree)::AbstractVector{<:AbstractAtom}
  • truths(φ::SyntaxTree)::AbstractVector{<:Truth}
  • leaves(φ::SyntaxTree)::AbstractVector{<:SyntaxLeaf}
  • connectives(φ::SyntaxTree)::AbstractVector{<:Connective}
  • operators(φ::SyntaxTree)::AbstractVector{<:Operator}
  • ntokens(φ::SyntaxTree)::Int
  • natoms(φ::SyntaxTree)::Int
  • ntruths(φ::SyntaxTree)::Int
  • nleaves(φ::SyntaxTree)::Int
  • nconnectives(φ::SyntaxTree)::Int
  • noperators(φ::SyntaxTree)::Int
  • tokenstype(φ::SyntaxTree)
  • atomstype(φ::SyntaxTree)
  • truthstype(φ::SyntaxTree)
  • leavestype(φ::SyntaxTree)
  • connectivestype(φ::SyntaxTree)
  • operatorstype(φ::SyntaxTree)
  • composeformulas(c::Connective, φs::NTuple{N,SyntaxTree})

See also SyntaxLeaf, SyntaxBranch, SyntaxStructure, Formula.

source
SoleLogics.SyntaxLeafType
abstract type SyntaxLeaf <: SyntaxStructure end

An atomic logical element, like a Truth value or an Atom. SyntaxLeafs have arity equal to zero, meaning that they are not allowed to have children in tree-like syntactic structures.

Interface

  • syntaxstring(s::SyntaxLeaf; kwargs...)::String
  • dual(s::SyntaxLeaf)::SyntaxLeaf
  • hasdual(s::SyntaxLeaf)::Bool

See also SyntaxStructure, arity, SyntaxBranch.

source
SoleLogics.dualMethod
dual(tok::SyntaxToken)

Return the dual of a syntax token.

If tok is an Operator of arity n, the dual dtok is such that, on a Boolean algebra, tok(ch_1, ..., ch_n)¬dtok(¬ch_1, ..., ¬ch_n).

Duality can be used to perform syntactic simplifications on formulas. For example, since and are duals, ¬(¬p ∧ ¬q) can be simplified to (p ∧ q) (De Morgan's law). Duality also applies to operators with existential/universal semantics (/), to Truth values (/), and to Atoms.

Implementation

When providing a dual for an operator of type O, please also provide:

hasdual(::O) = true

The dual of an Atom (that is, the atom with inverted semantics) is defined as:

dual(p::Atom{V}) where {V} = Atom(dual(value(p)))

As such, hasdual(::V) and dual(::V) should be defined when wrapping objects of type A.

See also normalize, SyntaxToken.

source
SoleLogics.TruthType
abstract type Truth <: SyntaxLeaf end

Abstract type for syntax leaves representing values of a lattice algebra. In Boolean logic, the two BooleanTruth values TOP (⊤) and BOT (⊥) are used.

See also BooleanTruth.

Interface

  • syntaxstring(s::Truth; kwargs...)::String
  • dual(s::Truth)::Truth
  • hasdual(s::Truth)::Bool
  • istop(t::Truth)::Bool
  • isbot(t::Truth)::Bool
  • precedes(t1::Truth, t2::Truth)::Bool
  • truthmeet(t1::Truth, t2::Truth)::Truth
  • truthjoin(t1::Truth, t2::Truth)::Truth

Implementation

A three-valued algebra, that is, an algebra with three truth values (top, bottom and unknown), can be based on the following Truth value definitions:

import SoleLogics: precedes

abstract type ThreeVTruth <: Truth end

struct ThreeTop <: ThreeVTruth end
const ⫪ = ThreeTop() # Note that ⊤ is already use to indicate BooleanTruth's top.
syntaxstring(::ThreeTop; kwargs...) = "⫪"

struct ThreeBot <: ThreeVTruth end
const ⫫ = ThreeBot() # Note that ⊥ is already use to indicate BooleanTruth's top.
syntaxstring(::ThreeBot; kwargs...) = "⫫"

struct ThreeUnknown <: ThreeVTruth end
const υ = ThreeUnknown()
syntaxstring(::ThreeUnknown; kwargs...) = "υ"

istop(t::ThreeTop) = true
isbot(t::ThreeBot) = true

precedes(::ThreeBot, ::ThreeTop) = true
precedes(::ThreeBot, ::ThreeUnknown) = true
precedes(::ThreeUnknown, ::ThreeTop) = true
precedes(::ThreeTop, ::ThreeBot) = false
precedes(::ThreeUnknown, ::ThreeBot) = false
precedes(::ThreeTop, ::ThreeUnknown) = false

Note that precedes is used to define the (partial) order between Truth values.

See also Connective, BooleanTruth.

source

Similarly to the Connectives case, Truth explanation could be unfamiliar at first sight. At the moment, what is of our interest is that SoleLogics provides us a simple interface to create custom, complex at will, algebras without worrying about adapting all the underlying algorithms (e.g., formulas generation, parsing, model checking etc.).

SoleLogics.istopMethod
istop(::Truth)::Bool

Return true if the Truth value is the top of its algebra. For example, in the crisp case, with Bool truth values, it is:

istop(t::Bool)::Bool = (t == true)

See also isbot, Truth.

source
SoleLogics.isbotMethod
isbot(::Truth)::Bool

Return true if the Truth value is the bottom of its algebra. For example, in the crisp case, with Bool truth values, it is:

isbot(t::Bool)::Bool = (t == false)

See also istop, Truth.

source

The union of Connectives and Truth values are exactly what is called logical operators, or simply Operator. In SoleLogics, logical operators are splitted in two parts to highlight some differences that always holds (e.g., Truth values arity is always 0, while Connectives arity is always greater than 0); apart from this technical decision, many dispatches are defined using the more general union type Operator.

An Operator can be used to compose syntax tokens (e.g., Atoms), SyntaxTrees and/or Formulas.

    ¬(Atom(1)) ∨ Atom(1) ∧ ⊤
    ∧(⊤,⊤)
    ⊤()

The internal nodes in a SyntaxTree definitely have ariety greater than 0, and thus, cannot wrap Atoms nor Truth values. To clearly distinguish internal nodes and leaves, the SyntaxBranch type is defined, making each SyntaxTree arity-complaint.

SoleLogics.SyntaxBranchType
struct SyntaxBranch <: AbstractSyntaxBranch
    token::Connective
    children::NTuple{N,SyntaxTree} where {N}
end

Simple implementation of a syntax branch. The implementation is arity-compliant, in that, upon construction, the arity of the token is checked against the number of children provided.

Examples

julia> p,q = Atom.([p, q])
2-element Vector{Atom{String}}:
 Atom{String}: p
 Atom{String}: q

julia> branch = SyntaxBranch(CONJUNCTION, p, q)
SyntaxBranch: p ∧ q

julia> token(branch)
∧

julia> syntaxstring.(children(branch))
(p, q)

julia> ntokens(a) == nconnectives(a) + nleaves(a)
true

julia> arity(a)
2

julia> height(a)
1

See also token, children, arity, Connective, height, atoms, natoms, operators, noperators, tokens, ntokens,

source

Semantics Basics

SoleLogics.AbstractInterpretationType
abstract type AbstractInterpretation end

Abstract type for representing a logical interpretation. In the case of propositional logic, is essentially a map atom → truth value.

Properties expressed via logical formulas can be checked on logical interpretations.

Interface

  • valuetype(i::AbstractInterpretation)
  • truthtype(i::AbstractInterpretation)
  • interpret(φ::Formula, i::AbstractInterpretation, args...; kwargs...)::Formula

Utility functions

  • check(φ::Formula, i::AbstractInterpretation, args...; kwargs...)::Bool

See also check, AbstractAssignment, AbstractKripkeStructure.

source
SoleLogics.interpretMethod
interpret(
    φ::Formula,
    i::AbstractInterpretation,
    args...;
    kwargs...
)::Formula

Return the truth value for a formula on a logical interpretation (or model).

Examples

julia> @atoms p q
2-element Vector{Atom{String}}:
 p
 q

julia> td = TruthDict([p => true, q => false])
TruthDict with values:
┌────────┬────────┐
│      q │      p │
│ String │ String │
├────────┼────────┤
│      ⊥ │      ⊤ │
└────────┴────────┘

julia> interpret(CONJUNCTION(p,q), td)
⊥

See also check, Formula, AbstractInterpretation, AbstractAlgebra.

source
SoleLogics.checkMethod
check(
    φ::Formula,
    i::AbstractInterpretation,
    args...;
    kwargs...
)::Bool

Check a formula on a logical interpretation (or model), returning true if the truth value for the formula istop. This process is referred to as (finite) model checking, and there are many algorithms for it, typically depending on the complexity of the logic.

Examples

julia> @atoms String p q
2-element Vector{Atom{String}}:
 Atom{String}("p")
 Atom{String}("q")

julia> td = TruthDict([p => TOP, q => BOT])
TruthDict with values:
┌────────┬────────┐
│      q │      p │
│ String │ String │
├────────┼────────┤
│      ⊥ │      ⊤ │
└────────┴────────┘

julia> check(CONJUNCTION(p,q), td)
false

See also interpret, Formula, AbstractInterpretation, TruthDict.

source