#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# File : semantics.py
# Author : Jiayuan Mao
# Email : maojiayuan@gmail.com
# Date : 01/04/2020
#
# This file is part of Project Concepts.
# Distributed under terms of the MIT license.
"""Data structures for semantic forms in a linguistic CCG.
The basic class is :class:`CCGSemantics`, which is a wrapper of a semantic form (a functor or a value).
There is a
"""
from typing import Optional, Union, Tuple, Dict, Callable
from dataclasses import dataclass
from jacinle.utils.cache import cached_property
from jacinle.utils.printing import indent_text
from concepts.dsl.dsl_functions import Function, FunctionArgumentResolutionContext, FunctionArgumentResolutionError
from concepts.dsl.function_domain import FunctionDomain
from concepts.dsl.value import Value
from concepts.dsl.expression import ConstantExpression, FunctionApplicationExpression
from concepts.language.ccg.composition import CCGCompositionType, CCGComposable, CCGCompositionError, get_ccg_composition_context
__all__ = [
'CCGSemanticsCompositionError',
'CCGSemanticsConjFunction', 'CCGSemanticsSimpleConjFunction',
'CCGSemanticsLazyValue', 'CCGSemantics',
'CCGSemanticsSugar',
]
[docs]
class CCGSemanticsCompositionError(CCGCompositionError):
"""Raised when the semantics composition fails."""
[docs]
def __init__(self, message=None, lhs=None, rhs=None, error=None, conj=None):
"""Initialize the error.
Args:
message: the error message.
lhs: the lhs semantics.
rhs: the rhs semantics.
error: the error raised by the left/right semantics.
conj: the conjunction.
"""
if message is None:
super().__init__(None)
else:
message += '\nLeft:' + indent_text(str(lhs)) + '\nRight:' + indent_text(str(rhs))
if conj is not None:
message += '\nConj: ' + indent_text(str(conj))
message += '\nOriginal error message: ' + indent_text(str(error)).lstrip()
super().__init__('(Semantics) ' + message)
[docs]
@dataclass
class CCGSemanticsConjFunction(object):
"""CCGSemanticsConjFunction is a wrapper that represents the semantics form of a conjunction term (e.g., AND)."""
impl: Callable[[Union[Value, Function], Union[Value, Function]], Union[Value, Function]]
"""The underlying implementation of the conjunction."""
[docs]
def __call__(self, lhs: Union[Value, Function], rhs: Union[Value, Function]) -> Union[Value, Function]:
"""Perform the conjunction.
Args:
lhs: the left-hand side semantics.
rhs: the right-hand side semantics.
Returns:
the conjunction result.
"""
return self.impl(lhs, rhs)
[docs]
class CCGSemanticsSimpleConjFunction(CCGSemanticsConjFunction):
"""A simple implementation for :class:`CCGSemanticsConjValue`.
This function takes a function that works for :class:`~concepts.dsl.value.Value` and automatically
converts it to a function that works for :class:`~concepts.dsl.function.Function`.
"""
impl: Callable[[Union[Value, Function], Union[Value, Function]], Union[Value, Function]]
[docs]
def __call__(self, lhs: Union[Value, Function], rhs: Union[Value, Function]) -> Union[Value, Function]:
"""Perform the conjunction.
Args:
lhs: the left-hand side semantics.
rhs: the right-hand side semantics.
Returns:
the conjunction result.
"""
if isinstance(lhs, Function) and isinstance(rhs, Function):
def body(*args, **kwargs):
return self.impl(lhs(*args, **kwargs), rhs(*args, **kwargs))
return Function('__conj__', lhs.ftype, overridden_call=body)
if isinstance(lhs, Value) and isinstance(rhs, Value):
return self.impl(lhs, rhs)
raise CCGSemanticsCompositionError(f'Cannot perform conjunction between {lhs} and {rhs}.')
[docs]
@dataclass
class CCGSemanticsLazyValue(object):
"""A wrapper that represents the semantic form of a node that is not yet evaluated.
Specifically, this function stores the composition type and the individual components."""
composition_type: Optional[CCGCompositionType] = None
"""The composition type."""
lhs: Optional[Union['CCGSemantics', 'CCGSemanticsLazyValue']] = None
"""The left-hand side semantics."""
rhs: Optional[Union['CCGSemantics', 'CCGSemanticsLazyValue']] = None
"""The right-hand side semantics."""
conj: Optional[Callable] = None
"""The conjunction function (optional)."""
[docs]
def execute(self) -> Union[Value, Function]:
"""Execute the lazy value recursively and return the semantic form."""
lhs, rhs = self.lhs, self.rhs
if isinstance(lhs, CCGSemanticsLazyValue):
lhs = lhs.execute()
if isinstance(rhs, CCGSemanticsLazyValue):
rhs = rhs.execute()
if self.composition_type in (CCGCompositionType.FORWARD_APPLICATION, CCGCompositionType.BACKWARD_APPLICATION):
if self.composition_type is CCGCompositionType.FORWARD_APPLICATION:
return _forward_application(lhs, rhs)
else:
return _backward_application(lhs, rhs)
elif self.composition_type is CCGCompositionType.COORDINATION:
return _coordination(lhs, self.conj, rhs)
else:
raise NotImplementedError(f'Unknown composition type: {self.composition_type}.')
[docs]
class CCGSemantics(CCGComposable):
"""CCGSemantics is a wrapper of a semantic form (a functor or a value)."""
[docs]
def __init__(self, value: Union[None, Callable, Function, ConstantExpression, FunctionApplicationExpression, CCGSemanticsLazyValue], *, is_conj: bool = False):
self._value = value
self._is_conj = is_conj
if isinstance(self._value, Function) and self._value.nr_arguments == 0:
self._value = self._value()
@property
def value(self) -> Union[Callable, Function, ConstantExpression, FunctionApplicationExpression, CCGSemanticsLazyValue]:
"""The semantic form."""
return self._value
@property
def is_none(self):
"""Whether the semantics is None."""
return self.value is None
@property
def is_conj(self):
"""Whether the semantics is a conjunction."""
return self._is_conj
@property
def is_py_function(self):
"""Whether the semantics is a Python function."""
return callable(self.value) and not self.is_function
@property
def is_lazy(self):
"""Whether the semantics is a lazy value."""
return isinstance(self.value, CCGSemanticsLazyValue)
@property
def is_function(self):
"""Whether the semantics is a function."""
if self.is_lazy:
raise ValueError('Cannot check is_function for CCGSemanticsLazyValue')
return isinstance(self.value, Function)
@property
def is_constant(self):
"""Whether the semantics is a constant."""
if self.is_lazy:
raise ValueError('Cannot check is_value for CCGSemanticsLazyValue')
return isinstance(self.value, ConstantExpression)
@property
def is_function_application(self):
"""Whether the semantics is a function application expression."""
if self.is_lazy:
raise ValueError('Cannot check is_function_application for CCGSemanticsLazyValue')
return isinstance(self.value, FunctionApplicationExpression)
@property
def is_value(self):
"""Whether the semantics is a value (either a constant or a function application expression)."""
return self.is_constant or self.is_function_application
@property
def flags(self) -> Dict[str, bool]:
"""A set of flags that indicates the type of the semantics."""
return {
'is_none': self.is_none,
'is_conj': self.is_conj,
'is_py_function': self.is_py_function,
'is_lazy': self.is_lazy,
'is_function': self.is_function if not self.is_lazy else None,
'is_constant': self.is_constant if not self.is_lazy else None,
'is_function_application': self.is_function_application if not self.is_lazy else None,
'is_value': self.is_value if not self.is_lazy else None,
}
@property
def return_type(self):
"""The return type of the semantics. If the semantics is a function or a value, the return type is the type of the
function or the value. Otherwise, this function will raise an error."""
if self.is_value:
return self.value.return_type
elif self.is_function:
return self.value.ftype.return_type
else:
raise AttributeError('Cannot get the return type of None, PyFunction, or Lazy semantics.')
@cached_property
def arity(self):
"""The arity of the semantics. If the semantics is a value, this function will return 0. If the semantics
is a function, this function will return the arity of the function. Otherwise, this function will raise an error."""
if self.is_value:
return 0
elif self.is_function:
return self.value.ftype.nr_variable_arguments
else:
raise AttributeError('Cannot get the arity of None, PyFunction, or Lazy semantics.')
def __str__(self) -> str:
if self.value is None:
return type(self).__name__ + '[None]'
if self.is_conj:
return type(self).__name__ + '[' + str(self.value) + ', CONJ]'
return type(self).__name__ + '[' + str(self.value) + ']'
def __repr__(self) -> str:
return str(self)
@cached_property
def hash(self): # for set/dict indexing.
return hash(str(self))
def _fapp(self, rhs: 'CCGSemantics') -> 'CCGSemantics':
if get_ccg_composition_context().semantics_lazy_composition:
return type(self)(CCGSemanticsLazyValue(CCGCompositionType.FORWARD_APPLICATION, self.value, rhs.value))
else:
return type(self)(_forward_application(self.value, rhs.value))
def _bapp(self, lhs: 'CCGSemantics') -> 'CCGSemantics':
if get_ccg_composition_context().semantics_lazy_composition:
return type(self)(CCGSemanticsLazyValue(CCGCompositionType.BACKWARD_APPLICATION, lhs.value, self.value))
else:
return type(self)(_backward_application(lhs.value, self.value))
def _coord3(self, lhs: 'CCGSemantics', rhs: 'CCGSemantics') -> 'CCGSemantics':
if get_ccg_composition_context().semantics_lazy_composition:
return type(self)(CCGSemanticsLazyValue(CCGCompositionType.COORDINATION, lhs.value, rhs.value, conj=self.value))
else:
return type(lhs)(_coordination(lhs.value, self.value, rhs.value))
def _forward_application(lhs, rhs):
ctx = get_ccg_composition_context()
if isinstance(lhs, Function):
try:
with FunctionArgumentResolutionContext(exc_verbose=ctx.exc_verbose).as_default():
return lhs.partial(rhs, execute_fully_bound_functions=True)
except FunctionArgumentResolutionError as e:
with ctx.exc(CCGSemanticsCompositionError, e):
raise CCGSemanticsCompositionError('Cannot make forward application.', lhs, rhs, e) from e
with ctx.exc(CCGSemanticsCompositionError):
raise CCGSemanticsCompositionError('Cannot make forward application.', lhs, rhs, 'Functor/Value types do not match.')
def _backward_application(lhs, rhs):
ctx = get_ccg_composition_context()
if isinstance(rhs, Function):
try:
with FunctionArgumentResolutionContext(exc_verbose=ctx.exc_verbose).as_default():
return rhs.partial(lhs, execute_fully_bound_functions=True)
except FunctionArgumentResolutionError as e:
with ctx.exc(CCGSemanticsCompositionError, e):
raise CCGSemanticsCompositionError('Cannot make backward application.', lhs, rhs, e) from e
with ctx.exc(CCGSemanticsCompositionError):
raise CCGSemanticsCompositionError('Cannot make backward application.', lhs, rhs, 'Functor/Value types do not match.')
def _coordination(lhs, conj, rhs):
ctx = get_ccg_composition_context()
if isinstance(lhs, Function) and isinstance(rhs, Function) and callable(conj):
if lhs.ftype.nr_variable_arguments == rhs.ftype.nr_variable_arguments:
return conj(lhs, rhs)
if isinstance(lhs, Value) and isinstance(rhs, Value) and callable(conj):
return conj(lhs, rhs)
with ctx.exc(CCGSemanticsCompositionError):
raise CCGSemanticsCompositionError(
'Cannot make coordination.',
lhs, rhs, conj=conj,
error='Functor arity does not match.'
)
[docs]
class CCGSemanticsSugar(object):
"""A syntax sugar that allows users to write CCG semantics in a more natural way.
This class is initialized in the :class:`~concepts.language.ccg.grammar.CCG` class.
"""
domain: FunctionDomain
[docs]
def __init__(self, domain: FunctionDomain):
"""Initialize the syntax sugar.
Args:
domain: the function domain.
"""
self.domain = domain
domain: FunctionDomain
"""The underlying function domain."""
[docs]
def __getitem__(self, item: Optional[Union[Value, Function, ConstantExpression, FunctionApplicationExpression, Callable, Tuple[Callable, Dict]]]) -> CCGSemantics:
"""Create a :class:`CCGSemantics` instance from a function or a value."""
if item is None:
return CCGSemantics(None)
if isinstance(item, CCGSemantics):
return item
if isinstance(item, Value):
return CCGSemantics(ConstantExpression(item))
if isinstance(item, (Function, ConstantExpression, FunctionApplicationExpression)):
return CCGSemantics(item)
if isinstance(item, tuple):
assert len(item) == 2
return CCGSemantics(self.domain.lam(item[0], typing_cues=item[1]))
assert callable(item)
return CCGSemantics(self.domain.lam(item))