Module clingo.script

This module contains functions to add custom scripts, which can be embedded into logic programs.

Examples

The following example shows how to add a script that works the same way as clingo's embedded Python script:

>>> from clingo.script import Script, register_script
>>> from clingo.control import Control
>>>
>>> import __main__
>>>
>>> class MyPythonScript(Script):
...     def execute(self, location, code):
...         exec(code, __main__.__dict__, __main__.__dict__)
...     def call(self, location, name, arguments):
...         return getattr(__main__, name)(*arguments)
...     def callable(self, name):
...         return name in __main__.__dict__ and callable(__main__.__dict__[name])
...
>>> register_script('mypython', MyPythonScript())
>>>
>>> ctl = Control()
>>> ctl.add('base', [], """
... #script(mypython)
... from clingo.symbol import Number
... def func(a):
...     return Number(a.number + 1)
... #end.
... a(@func(1)).
... """)
>>>
>>> ctl.ground([('base',[])])
>>> ctl.solve(on_model=print)
a(2)
Expand source code
'''
This module contains functions to add custom scripts, which can be embedded
into logic programs.

Examples
--------
The following example shows how to add a script that works the same way as
clingo's embedded Python script:
```python
>>> from clingo.script import Script, register_script
>>> from clingo.control import Control
>>>
>>> import __main__
>>>
>>> class MyPythonScript(Script):
...     def execute(self, location, code):
...         exec(code, __main__.__dict__, __main__.__dict__)
...     def call(self, location, name, arguments):
...         return getattr(__main__, name)(*arguments)
...     def callable(self, name):
...         return name in __main__.__dict__ and callable(__main__.__dict__[name])
...
>>> register_script('mypython', MyPythonScript())
>>>
>>> ctl = Control()
>>> ctl.add('base', [], """
... #script(mypython)
... from clingo.symbol import Number
... def func(a):
...     return Number(a.number + 1)
... #end.
... a(@func(1)).
... """)
>>>
>>> ctl.ground([('base',[])])
>>> ctl.solve(on_model=print)
a(2)
```
'''

from platform import python_version
from abc import ABCMeta, abstractmethod
from typing import Any, List, Iterable, Tuple, Union
from collections.abc import Iterable as IterableABC
from traceback import format_exception
from clingo._internal import _c_call, _ffi, _handle_error, _lib
from clingo.control import Control
from clingo.symbol import Symbol
from clingo.ast import Location, _py_location

try:
    import __main__ # type: ignore
except ImportError:
    # Note: pypy does not create a main module if embedded
    import sys
    import types
    sys.modules['__main__'] = types.ModuleType('__main__', 'the main module')
    import __main__ # type: ignore

__all__ = [ 'Script', 'enable_python', 'register_script' ]

def _cb_error_top_level(exception, exc_value, traceback):
    msg = "".join(format_exception(exception, exc_value, traceback))
    _lib.clingo_set_error(_lib.clingo_error_runtime, msg.encode())
    return False


class Script(metaclass=ABCMeta):
    '''
    This interface can be implemented to embed custom scripting languages into
    logic programs.
    '''
    @abstractmethod
    def execute(self, location: Location, code: str) -> None:
        '''
        Execute the given source code.

        Parameters
        ----------
        location
            The location of the code.
        code
            The code to execute.
        '''

    @abstractmethod
    def call(self, location: Location, name: str, arguments: Iterable[Symbol]) -> Union[Iterable[Symbol], Symbol]:
        '''
        Call the function with the given name and arguments.

        Parameters
        ----------
        location
            From where in the logic program the function was called.
        name
            The name of the function.
        arguments
            The arguments to the function.

        Returns
        -------
        The resulting pool of symbols.
        '''

    @abstractmethod
    def callable(self, name: str) -> bool:
        '''
        Check there is a function with the given name.

        Parameters
        ----------
        name
            The name of the function.

        Returns
        -------
        Whether the function is callable.
        '''

    def main(self, control: Control) -> None:
        '''
        Run the main function.

        This function exisits primarily for internal purposes and does not need
        to be implemented.

        Parameters
        ----------
        control
            Control object to pass to the main function.
        '''

class _PythonScript(Script):
    def execute(self, location, code):
        exec(code, __main__.__dict__, __main__.__dict__) # pylint: disable=exec-used

    def call(self, location, name, arguments):
        fun = getattr(__main__, name)
        return fun(*arguments)

    def callable(self, name):
        return name in __main__.__dict__ and callable(__main__.__dict__[name])

    def main(self, control):
        __main__.main(control) # pylint: disable=c-extension-no-member

_PYTHON_SCRIPT = _PythonScript()
_GLOBAL_SCRIPTS: List[Tuple[Script, Any]] = []


@_ffi.def_extern(onerror=_cb_error_top_level, name='pyclingo_script_execute')
def _pyclingo_script_execute(location, code, data):
    script: Script = _ffi.from_handle(data)
    script.execute(_py_location(location), _ffi.string(code).decode())
    return True

@_ffi.def_extern(onerror=_cb_error_top_level, name='pyclingo_script_call')
def _pyclingo_script_call(location, name, arguments, size, symbol_callback, symbol_callback_data, data):
    script: Script = _ffi.from_handle(data)
    symbol_callback = _ffi.cast('clingo_symbol_callback_t', symbol_callback)
    arguments = _ffi.cast('clingo_symbol_t*', arguments)
    py_name = _ffi.string(name).decode()
    py_args = [Symbol(arguments[i]) for i in range(size)]

    ret = script.call(_py_location(location), py_name, py_args)
    symbols = list(ret) if isinstance(ret, IterableABC) else [ret]

    c_symbols = _ffi.new('clingo_symbol_t[]', len(symbols))
    for i, sym in enumerate(symbols):
        c_symbols[i] = sym._rep # pylint: disable=protected-access
    _handle_error(symbol_callback(c_symbols, len(symbols), symbol_callback_data))
    return True

@_ffi.def_extern(onerror=_cb_error_top_level, name='pyclingo_script_callable')
def _pyclingo_script_callable(name, ret, data):
    script: Script = _ffi.from_handle(data)
    py_name = _ffi.string(name).decode()
    ret[0] = script.callable(py_name)
    return True

@_ffi.def_extern(onerror=_cb_error_top_level, name='pyclingo_script_main')
def _pyclingo_script_main(ctl, data):
    script: Script = _ffi.from_handle(data)
    script.main(Control(_ffi.cast('clingo_control_t*', ctl)))
    return True


@_ffi.def_extern(onerror=_cb_error_top_level, name='pyclingo_execute')
def _pyclingo_execute(location, code, data):
    assert data == _ffi.NULL
    return _pyclingo_script_execute(_ffi.cast('clingo_location_t*', location),
                                   code,
                                   _ffi.new_handle(_PYTHON_SCRIPT))

@_ffi.def_extern(onerror=_cb_error_top_level, name='pyclingo_call')
def _pyclingo_call(location, name, arguments, size, symbol_callback, symbol_callback_data, data):
    assert data == _ffi.NULL
    return _pyclingo_script_call(_ffi.cast('clingo_location_t*', location),
                                 name,
                                 arguments, size,
                                 symbol_callback, symbol_callback_data,
                                 _ffi.new_handle(_PYTHON_SCRIPT))

@_ffi.def_extern(onerror=_cb_error_top_level, name='pyclingo_callable')
def _pyclingo_callable(name, ret, data):
    assert data == _ffi.NULL
    return _pyclingo_script_callable(name,
                                     ret,
                                     _ffi.new_handle(_PYTHON_SCRIPT))

@_ffi.def_extern(onerror=_cb_error_top_level, name='pyclingo_main')
def _pyclingo_main(ctl, data):
    assert data == _ffi.NULL
    return _pyclingo_script_main(_ffi.cast('clingo_control_t*', ctl),
                                 _ffi.new_handle(_PYTHON_SCRIPT))


def register_script(name: str, script: Script, version: str = '1.0.0') -> None:
    '''
    Registers a script language which can then be embedded into a logic
    program.

    Parameters
    ----------
    name
        The name of the script. This name can then be used in the script
        statement in a logic program.
    script
        The class to register.
    version
        The version of the script.
    '''
    c_version = _c_call('char const*', _lib.clingo_add_string, version.encode())
    c_script = _ffi.new('clingo_script_t*')
    c_script[0].execute = _ffi.cast('void*', _lib.pyclingo_script_execute)
    c_script[0].call = _ffi.cast('void*', _lib.pyclingo_script_call)
    c_script[0].callable = _ffi.cast('void*', _lib.pyclingo_script_callable)
    c_script[0].main = _ffi.cast('void*', _lib.pyclingo_script_main)
    c_script[0].free = _ffi.NULL
    c_script[0].version = c_version
    data = _ffi.new_handle(script)
    _GLOBAL_SCRIPTS.append((script, data))
    _handle_error(_lib.clingo_register_script(name.encode(), c_script, data))

def enable_python() -> None:
    '''
    This function can be called to enable evaluation of Python scripts in logic
    programs.

    By default evaluation is only enabled in the clingo executable but not in
    the Python module.
    '''
    c_version = _c_call('char const*', _lib.clingo_add_string, python_version().encode())
    c_script = _ffi.new('clingo_script_t*')
    c_script[0].execute = _ffi.cast('void*', _lib.pyclingo_execute)
    c_script[0].call = _ffi.cast('void*', _lib.pyclingo_call)
    c_script[0].callable = _ffi.cast('void*', _lib.pyclingo_callable)
    c_script[0].main = _ffi.cast('void*', _lib.pyclingo_main)
    c_script[0].free = _ffi.NULL
    c_script[0].version = c_version
    _handle_error(_lib.clingo_register_script("python".encode(), c_script, _ffi.NULL))

Functions

def enable_python() ‑> None

This function can be called to enable evaluation of Python scripts in logic programs.

By default evaluation is only enabled in the clingo executable but not in the Python module.

Expand source code
def enable_python() -> None:
    '''
    This function can be called to enable evaluation of Python scripts in logic
    programs.

    By default evaluation is only enabled in the clingo executable but not in
    the Python module.
    '''
    c_version = _c_call('char const*', _lib.clingo_add_string, python_version().encode())
    c_script = _ffi.new('clingo_script_t*')
    c_script[0].execute = _ffi.cast('void*', _lib.pyclingo_execute)
    c_script[0].call = _ffi.cast('void*', _lib.pyclingo_call)
    c_script[0].callable = _ffi.cast('void*', _lib.pyclingo_callable)
    c_script[0].main = _ffi.cast('void*', _lib.pyclingo_main)
    c_script[0].free = _ffi.NULL
    c_script[0].version = c_version
    _handle_error(_lib.clingo_register_script("python".encode(), c_script, _ffi.NULL))
def register_script(name: str, script: Script, version: str = '1.0.0') ‑> None

Registers a script language which can then be embedded into a logic program.

Parameters

name
The name of the script. This name can then be used in the script statement in a logic program.
script
The class to register.
version
The version of the script.
Expand source code
def register_script(name: str, script: Script, version: str = '1.0.0') -> None:
    '''
    Registers a script language which can then be embedded into a logic
    program.

    Parameters
    ----------
    name
        The name of the script. This name can then be used in the script
        statement in a logic program.
    script
        The class to register.
    version
        The version of the script.
    '''
    c_version = _c_call('char const*', _lib.clingo_add_string, version.encode())
    c_script = _ffi.new('clingo_script_t*')
    c_script[0].execute = _ffi.cast('void*', _lib.pyclingo_script_execute)
    c_script[0].call = _ffi.cast('void*', _lib.pyclingo_script_call)
    c_script[0].callable = _ffi.cast('void*', _lib.pyclingo_script_callable)
    c_script[0].main = _ffi.cast('void*', _lib.pyclingo_script_main)
    c_script[0].free = _ffi.NULL
    c_script[0].version = c_version
    data = _ffi.new_handle(script)
    _GLOBAL_SCRIPTS.append((script, data))
    _handle_error(_lib.clingo_register_script(name.encode(), c_script, data))

Classes

class Script

This interface can be implemented to embed custom scripting languages into logic programs.

Expand source code
class Script(metaclass=ABCMeta):
    '''
    This interface can be implemented to embed custom scripting languages into
    logic programs.
    '''
    @abstractmethod
    def execute(self, location: Location, code: str) -> None:
        '''
        Execute the given source code.

        Parameters
        ----------
        location
            The location of the code.
        code
            The code to execute.
        '''

    @abstractmethod
    def call(self, location: Location, name: str, arguments: Iterable[Symbol]) -> Union[Iterable[Symbol], Symbol]:
        '''
        Call the function with the given name and arguments.

        Parameters
        ----------
        location
            From where in the logic program the function was called.
        name
            The name of the function.
        arguments
            The arguments to the function.

        Returns
        -------
        The resulting pool of symbols.
        '''

    @abstractmethod
    def callable(self, name: str) -> bool:
        '''
        Check there is a function with the given name.

        Parameters
        ----------
        name
            The name of the function.

        Returns
        -------
        Whether the function is callable.
        '''

    def main(self, control: Control) -> None:
        '''
        Run the main function.

        This function exisits primarily for internal purposes and does not need
        to be implemented.

        Parameters
        ----------
        control
            Control object to pass to the main function.
        '''

Subclasses

  • clingo.script._PythonScript

Methods

def call(self, location: Location, name: str, arguments: Iterable[Symbol]) ‑> Union[Iterable[Symbol], Symbol]

Call the function with the given name and arguments.

Parameters

location
From where in the logic program the function was called.
name
The name of the function.
arguments
The arguments to the function.

Returns

The resulting pool of symbols.

Expand source code
@abstractmethod
def call(self, location: Location, name: str, arguments: Iterable[Symbol]) -> Union[Iterable[Symbol], Symbol]:
    '''
    Call the function with the given name and arguments.

    Parameters
    ----------
    location
        From where in the logic program the function was called.
    name
        The name of the function.
    arguments
        The arguments to the function.

    Returns
    -------
    The resulting pool of symbols.
    '''
def callable(self, name: str) ‑> bool

Check there is a function with the given name.

Parameters

name
The name of the function.

Returns

Whether the function is callable.

Expand source code
@abstractmethod
def callable(self, name: str) -> bool:
    '''
    Check there is a function with the given name.

    Parameters
    ----------
    name
        The name of the function.

    Returns
    -------
    Whether the function is callable.
    '''
def execute(self, location: Location, code: str) ‑> None

Execute the given source code.

Parameters

location
The location of the code.
code
The code to execute.
Expand source code
@abstractmethod
def execute(self, location: Location, code: str) -> None:
    '''
    Execute the given source code.

    Parameters
    ----------
    location
        The location of the code.
    code
        The code to execute.
    '''
def main(self, control: Control) ‑> None

Run the main function.

This function exisits primarily for internal purposes and does not need to be implemented.

Parameters

control
Control object to pass to the main function.
Expand source code
def main(self, control: Control) -> None:
    '''
    Run the main function.

    This function exisits primarily for internal purposes and does not need
    to be implemented.

    Parameters
    ----------
    control
        Control object to pass to the main function.
    '''