Reference
Logic fragments and typed tuple-based formulas.
This module defines
- TLExpr: a lightweight AST for temporal/propositional logic as typed tuples
- Utilities to validate/inspect/transform formulas
- LogicFragment meta-class to declare connectives and compose language fragments
Design
- A proposition is either a string name or a SetBuilder constant.
- Operator applications are tuples of the form:
- (op,) for nullary operators
- (op, arg) for unary operators
- (op, lhs, rhs) for binary operators
- Operators are declared via
declare(name, narg)
which produces a class-like fragment whose__call__
constructs the tuple form. - Fragments can be composed with
|
to create a combined language.
LogicFragment
Bases: type
Metaclass for declaring and composing logic fragments (connectives).
Instances of classes produced by this metaclass are not meant to be instantiated. Instead, calling the class constructs a tuple-based TLExpr for the associated connective (see call).
Attributes:
Name | Type | Description |
---|---|---|
- |
__narg__
|
required arity for the connective (None for a composed fragment) |
- |
__connectives__
|
tuple of connective names included in this fragment |
Composition
- Fragments can be combined with
|
to form a new fragment whose__connectives__
is the union of the operands.
Source code in src/pyspect/logics.py
class LogicFragment(type):
"""Metaclass for declaring and composing logic fragments (connectives).
Instances of classes produced by this metaclass are not meant to be
instantiated. Instead, calling the class constructs a tuple-based TLExpr
for the associated connective (see __call__).
Attributes:
- __narg__: required arity for the connective (None for a composed fragment)
- __connectives__: tuple of connective names included in this fragment
Composition:
- Fragments can be combined with `|` to form a new fragment whose
`__connectives__` is the union of the operands.
"""
__narg__: Optional[int]
__connectives__: ClassVar[tuple[str, ...]]
def __new__(mcs, name: str, bases: tuple, namespace: dict):
"""Create a new fragment type, merging connectives from base fragments."""
_narg = namespace.setdefault('__narg__', None)
_connectives = set(namespace.setdefault('__connectives__', ()))
for base in bases:
if isinstance(base, LogicFragment):
_connectives |= set(base.__connectives__)
else:
raise TypeError(f"Base class {base} is not a LogicFragment")
namespace['__narg__'] = _narg
namespace['__connectives__'] = tuple(_connectives)
return super().__new__(mcs, name, bases, namespace)
def __repr__(cls) -> str:
return f"<language '{cls.__name__}'>"
def __call__(cls, *args) -> TLExpr:
"""Construct a TLExpr tuple for this connective.
Constraints:
- This only works for a fragment with exactly one connective.
- Number of arguments must match the declared arity.
- Arguments must themselves be valid TLExpr terminals or tuples.
Returns:
- (op,) | (op, arg) | (op, lhs, rhs), where op == cls.__name__
"""
# NOTE: This overrides the ability to instantiate objects,
# but this is not necessary for the language classes
# which we only use for type checking purposes.
if len(cls.__connectives__) != 1:
raise TypeError(f"Can only instantiate a single connective, but {L} were found in {cls.__name__}.")
if len(args) != cls.__narg__:
raise TypeError(f"Expected {cls.__narg__} arguments, but got {len(args)}.")
for arg in args:
if not isinstance(arg, (str, SetBuilder, tuple)):
raise TypeError(f"Argument {arg} is not a valid formula.")
# Creates a formula based on the primitive
return (cls.__name__, *args)
def __or__(cls, *args) -> 'LogicFragment':
"""Return a fragment that is the union of this fragment and others."""
# Combine connectives using bitwise OR
return LogicFragment('UnnamedLanguageFragment', args, {})
def is_complete(cls, prims: set) -> bool:
"""Check that all connectives in this fragment exist in `prims`."""
# Check if all connectives are present in the set of primitives
return all(conn in prims for conn in cls.__connectives__)
__call__(*args)
Construct a TLExpr tuple for this connective.
Constraints
- This only works for a fragment with exactly one connective.
- Number of arguments must match the declared arity.
- Arguments must themselves be valid TLExpr terminals or tuples.
Returns:
Type | Description |
---|---|
TLExpr
|
|
Source code in src/pyspect/logics.py
def __call__(cls, *args) -> TLExpr:
"""Construct a TLExpr tuple for this connective.
Constraints:
- This only works for a fragment with exactly one connective.
- Number of arguments must match the declared arity.
- Arguments must themselves be valid TLExpr terminals or tuples.
Returns:
- (op,) | (op, arg) | (op, lhs, rhs), where op == cls.__name__
"""
# NOTE: This overrides the ability to instantiate objects,
# but this is not necessary for the language classes
# which we only use for type checking purposes.
if len(cls.__connectives__) != 1:
raise TypeError(f"Can only instantiate a single connective, but {L} were found in {cls.__name__}.")
if len(args) != cls.__narg__:
raise TypeError(f"Expected {cls.__narg__} arguments, but got {len(args)}.")
for arg in args:
if not isinstance(arg, (str, SetBuilder, tuple)):
raise TypeError(f"Argument {arg} is not a valid formula.")
# Creates a formula based on the primitive
return (cls.__name__, *args)
__new__(mcs, name, bases, namespace)
Create a new fragment type, merging connectives from base fragments.
Source code in src/pyspect/logics.py
def __new__(mcs, name: str, bases: tuple, namespace: dict):
"""Create a new fragment type, merging connectives from base fragments."""
_narg = namespace.setdefault('__narg__', None)
_connectives = set(namespace.setdefault('__connectives__', ()))
for base in bases:
if isinstance(base, LogicFragment):
_connectives |= set(base.__connectives__)
else:
raise TypeError(f"Base class {base} is not a LogicFragment")
namespace['__narg__'] = _narg
namespace['__connectives__'] = tuple(_connectives)
return super().__new__(mcs, name, bases, namespace)
__or__(*args)
Return a fragment that is the union of this fragment and others.
Source code in src/pyspect/logics.py
def __or__(cls, *args) -> 'LogicFragment':
"""Return a fragment that is the union of this fragment and others."""
# Combine connectives using bitwise OR
return LogicFragment('UnnamedLanguageFragment', args, {})
is_complete(prims)
Check that all connectives in this fragment exist in prims
.
Source code in src/pyspect/logics.py
def is_complete(cls, prims: set) -> bool:
"""Check that all connectives in this fragment exist in `prims`."""
# Check if all connectives are present in the set of primitives
return all(conn in prims for conn in cls.__connectives__)
declare(name, narg)
Declare a new connective fragment with given name and arity.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
- name
|
operator name used as the tuple head in TLExpr |
required | |
- narg
|
arity (0, 1, or 2) |
required |
Returns:
Type | Description |
---|---|
LogicFragment
|
|
Source code in src/pyspect/logics.py
def declare(name: str, narg: int) -> LogicFragment:
"""Declare a new connective fragment with given name and arity.
Parameters:
- name: operator name used as the tuple head in TLExpr
- narg: arity (0, 1, or 2)
Returns:
- A LogicFragment-derived class representing the connective
"""
return LogicFragment(name, (), {
'__narg__': narg,
'__connectives__': (name,),
})
get_malformed(formula)
Return the first malformed subexpression, or None if well-formed.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
- formula
|
TLExpr to check |
required |
Returns:
Type | Description |
---|---|
Optional[TLExpr]
|
|
Well-formedness rules
- Propositions: non-empty str, or any SetBuilder instance
- Operator tuples: length 1..3, head must be str, and all arguments recursively valid
Source code in src/pyspect/logics.py
def get_malformed(formula: TLExpr) -> Optional[TLExpr]:
"""Return the first malformed subexpression, or None if well-formed.
Parameters:
- formula: TLExpr to check
Returns:
- None if the expression is well-formed; otherwise the first offending subexpression
Well-formedness rules:
- Propositions: non-empty str, or any SetBuilder instance
- Operator tuples: length 1..3, head must be str, and all arguments recursively valid
"""
if isinstance(formula, str):
return None if formula else formula # OK unless empty string
elif isinstance(formula, SetBuilder):
return None # OK, constant
elif isinstance(formula, tuple) and len(formula) <= 3:
head, *tail = formula
if not isinstance(head, str): return formula # Malformed
for arg in tail:
if x := get_malformed(arg):
return x # Malformed
return None # OK
else:
return formula # Malformed
get_props(formula)
Collect proposition names appearing in a formula.
- String terminals are treated as proposition names and included.
- SetBuilder terminals are constants and contribute no proposition names.
- Operator tuples are traversed recursively.
Parameters: - formula: TLExpr to inspect
Returns:
- Set of unique proposition names found in formula
Source code in src/pyspect/logics.py
def get_props(formula: TLExpr) -> set[str]:
"""Collect proposition names appearing in a formula.
- String terminals are treated as proposition names and included.
- SetBuilder terminals are constants and contribute no proposition names.
- Operator tuples are traversed recursively.
Parameters:
- formula: TLExpr to inspect
Returns:
- Set of unique proposition names found in `formula`
"""
if isinstance(formula, str):
return {formula}
elif isinstance(formula, SetBuilder):
return set()
elif isinstance(formula, tuple):
head, *tail = formula
props = set()
for arg in tail:
props |= get_props(arg)
return props
else:
raise TypeError(f"Unknown formula type: {type(formula)}")
replace_prop(formula, prop, expr)
Replace all occurrences of proposition prop
with expr
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
- formula
|
operator tuple (TLOpNullary/Unary/Binary) |
required | |
- prop
|
proposition name to replace |
required | |
- expr
|
replacement sub-expression |
required |
Returns:
Type | Description |
---|---|
TLExpr
|
|
Notes
- This function expects
formula
to be an operator tuple (not a raw str or SetBuilder). It descends recursively and replaces terminals that matchprop
. - For nullary or terminal positions, if the terminal equals
prop
, it is replaced byexpr
.
Source code in src/pyspect/logics.py
def replace_prop(formula: TLExpr, prop: str, expr: TLExpr) -> TLExpr:
"""Replace all occurrences of proposition `prop` with `expr`.
Parameters:
- formula: operator tuple (TLOpNullary/Unary/Binary)
- prop: proposition name to replace
- expr: replacement sub-expression
Returns:
- New TLExpr with replacements applied
Notes:
- This function expects `formula` to be an operator tuple (not a raw str or SetBuilder).
It descends recursively and replaces terminals that match `prop`.
- For nullary or terminal positions, if the terminal equals `prop`, it is replaced by `expr`.
"""
head, *tail = formula
if tail:
# formula is an operator expression
# => go down in arguments to replace prop
return (head, *map(lambda arg: replace_prop(arg, prop, expr), tail))
else:
# formula is a terminal
# => if terminal == prop, replace with expr
return expr if head == prop else formula