ScryPy
Scryer Prolog + Python Integration
Version: 0.4.0-alpha (Pre-release / Alpha)
⚠️ RESEARCH LIBRARY - RAPID ITERATION IN PROGRESS
THIS IS AN ACTIVE RESEARCH PROJECT. NOT PRODUCTION READY.
- 🚧 API UNSTABLE - Breaking changes expected frequently
- 🚧 Architecture UNSTABLE - Core design decisions still being evaluated
- 🚧 Goals UNSTABLE - Feature set and direction may pivot significantly
- 🚧 Use Cases UNCERTAIN - Exploring what makes sense at the Prolog/Python boundary
Many open questions exist about how to make a declarative logic language work with a multi-paradigm language like Python.
This library is largely based on the work from libpython-clj, but adapted for Prolog's unique paradigm. We're actively exploring questions like:
- How should Python's imperative mutation interact with Prolog's logical variables?
- What's the right abstraction for Python objects in a relational context?
- Should we expose Python's OOP directly, or create a more declarative interface?
- How do we handle Python's exceptions in Prolog's backtracking model?
If you need stable Python integration, this is not the library for you. Yet.
Overview
This library provides integration between Scryer Prolog and Python using FFI (Foreign Function Interface). It allows you to execute Python code directly from Prolog.
Inspirations:
- libpython-clj - Python integration for Clojure (primary inspiration)
- libscryer-clj - Scryer Prolog integration for Clojure
Unlike libpython-clj which creates one Python instance per process, this library explores explicit interpreter lifecycle management, enabling multiple init/finalize cycles (similar to how libscryer-clj manages Scryer machines). However, this design may change as we explore different approaches.
Features
Core Features
- ✅ Initialize and finalize Python interpreter
- ✅ Execute arbitrary Python code from strings
- ✅ State persistence across multiple calls
- ✅ Error handling for Python exceptions
- ✅ Access to full Python standard library
- ✅ Support for Python packages (NumPy, etc.)
Version 0.2.0 Features
- ✅ Dictionary Operations: Create and manipulate Python dictionaries
- ✅ Type Conversion: Bidirectional conversion between Prolog and Python types
- Atoms ↔ Python strings
- Integers ↔ Python ints
- Floats ↔ Python floats
- Booleans (true/false) ↔ Python booleans
- ✅ Globals/Locals Support: Execute Python with custom variable scopes
- ✅ Value Extraction: Get computed values back from Python into Prolog
- ✅ Dict ↔ List Conversion: Convert between Python dicts and Prolog key-value lists
Version 0.4.0 Features (Phase 1 - Type Bridging)
- ✅ Python None: Get None singleton, None checking, bidirectional conversion
- ✅ Python Lists: Create, read, update, append, bidirectional conversion with nested support
- ✅ Python Tuples: Create, read, bidirectional conversion with nested support
- ✅ Memory Management: Proper handling of BORROWED, STOLEN, and NEW references
- ✅ Nested Structures: Full support for nested lists/tuples
Installation
See INSTALL.md for detailed installation instructions, including:
- How to install Python development libraries
- Finding your Python shared library location
- Platform-specific setup (Linux, macOS)
- Troubleshooting common issues
Quick Install
- Clone this repository:
git clone https://github.com/jjtolton/scrypy.git
cd scrypy- Install Python development libraries:
# Ubuntu/Debian - one of these depending on your Python version sudo apt-get install python3.10-dev libpython3.10 sudo apt-get install python3.11-dev libpython3.11 sudo apt-get install python3.12-dev libpython3.12 # macOS (Homebrew) brew install python@3.11
-
The library will automatically detect your Python version (3.10, 3.11, or 3.12).
-
Use the library:
:- use_module('src/lib/python').
Requirements
Scryer Prolog (with RTLD_GLOBAL support)
For full Python C extension support (NumPy, pandas, SciPy, etc.), you need a version of Scryer Prolog with RTLD_GLOBAL support:
- Recommended: Use the jjtolton/scryer-prolog fork (branch:
rtld-global-support) - Upstream PR: mthom/scryer-prolog#3144 - Once merged, you can use the official release
Why this matters: The standard Scryer Prolog loads foreign libraries with RTLD_LOCAL, which prevents Python C extensions from resolving symbols. The fork/PR adds scope(global) option to use_foreign_module/3 to enable RTLD_GLOBAL loading.
As of v0.3.0: ScryPy now uses scope(global) by default for all Python library loading, enabling full C extension support out of the box (when using the RTLD-enabled Scryer fork).
For basic Python only (no C extensions): Any Scryer Prolog v0.10.0+ will work, but imports like numpy, pandas, etc. will fail.
Python
- Python 3.10, 3.11, or 3.12 with shared library (
.soon Linux,.dylibon macOS) - Python development package (
python3.X-devon Linux)
The library automatically detects which Python version is installed and uses the appropriate shared library.
Quick Start
Basic Python Execution
?- use_module('src/lib/python').
true.
?- py_initialize.
true.
?- py_run_simple_string("print('Hello from Python!')").
Hello from Python!
true.
?- py_run_simple_string("x = 42").
true.
?- py_run_simple_string("print(f'The answer is {x}')").
The answer is 42
true.
?- py_finalize.
true.Working with Python Types (v0.4.0)
?- py_initialize. true. % Python None ?- py_none(N), py_none_check(N), py_xdecref(N). true. % Python Lists ?- py_list_from_prolog([1, 2, 3], L), py_list_get(L, 1, Item), py_xdecref(L). Item = 2. % Python Tuples ?- py_tuple_from_prolog([a, b, c], T), py_tuple_size(T, Size), py_xdecref(T). Size = 3. ?- py_finalize. true.
API Reference
Core Predicates
py_initialize/0
Initialize the Python interpreter. Must be called before any other Python operations.
Throws: permission_error if Python is already initialized.
py_finalize/0
Finalize the Python interpreter and free all resources.
Throws: existence_error if Python is not initialized.
py_run_simple_string(+Code)
Execute Python code from a string. The code is executed in the __main__ module's namespace.
Parameters:
Code: An atom containing Python code to execute
Throws:
existence_errorif Python is not initializedpython_error(Code)if the Python code raises an exception
Example:
?- py_run_simple_string("x = 10").
?- py_run_simple_string("print(x * 2)").
20py_run_simple_string(+Code, +GlobalsIn, +LocalsIn, -GlobalsOut, -LocalsOut)
Execute Python code with explicit globals and locals dictionaries. This allows you to pass variables to Python and get computed values back.
Parameters:
Code: Python code to execute (atom)GlobalsIn: List of Key-Value pairs for global namespaceLocalsIn: List of Key-Value pairs for local namespaceGlobalsOut: Resulting global namespace as Key-Value listLocalsOut: Resulting local namespace as Key-Value list
Example:
?- py_run_simple_string("result = x + y", [x-10, y-20], [], Globals, _).
Globals = [x-10, y-20, __builtins__-true, result-30].Dictionary Operations (v0.2.0+)
py_dict_new(-DictPtr)
Create a new empty Python dictionary.
Parameters:
DictPtr: Unified with pointer to new Python dict
Example:
?- py_dict_new(Dict). Dict = 140475345675648.
py_dict_set(+DictPtr, +Key, +Value)
Set a key-value pair in a Python dictionary.
Parameters:
DictPtr: Pointer to Python dictKey: Prolog atom (converted to Python string)Value: Prolog value (atom, integer, or float)
Example:
?- py_dict_new(Dict), py_dict_set(Dict, name, 'Alice'), py_dict_set(Dict, age, 30), py_xdecref(Dict). true.
Note: Dictionary pointers are NEW references and must be cleaned up with py_xdecref/1 when done.
py_dict_get(+DictPtr, +Key, -Value)
Get a value from a Python dictionary by key.
Parameters:
DictPtr: Pointer to Python dictKey: Prolog atom (key name)Value: Unified with Prolog value
Example:
?- py_dict_get(Dict, name, Name). Name = 'Alice'.
py_dict_to_list(+DictPtr, -List)
Convert a Python dictionary to a Prolog list of Key-Value pairs.
Parameters:
DictPtr: Pointer to Python dictList: Unified with list of Key-Value pairs
Example:
?- py_dict_to_list(Dict, List). List = [name-'Alice', age-30].
prolog_to_py_dict(+PrologList, -DictPtr)
Convert a Prolog list of Key-Value pairs to a Python dictionary.
Parameters:
PrologList: List of Key-Value pairsDictPtr: Unified with pointer to new Python dict
Example:
?- prolog_to_py_dict([x-10, y-20], Dict).
py_dict_to_prolog(+DictPtr, -PrologList)
Alias for py_dict_to_list/2.
Phase 1 Type Bridging (v0.4.0)
Python None
py_none(-PyNone)
Get the Python None singleton.
Returns: NEW reference - must call py_xdecref(PyNone) when done.
?- py_none(N), py_xdecref(N). true.
py_none_check(+PyObject)
Check if a Python object is None.
?- py_none(N), py_none_check(N), py_xdecref(N). true.
Python Lists
py_list_new(-PyList)
Create a new empty Python list.
Returns: NEW reference - must call py_xdecref(PyList) when done.
py_list_from_prolog(+PrologList, -PyList)
Convert a Prolog list to a Python list. Supports nested lists.
?- py_list_from_prolog([1, [2, 3], 4], L), py_list_to_prolog(L, Result), py_xdecref(L). Result = [1, [2, 3], 4].
py_list_to_prolog(+PyList, -PrologList)
Convert a Python list to a Prolog list. Supports nested lists.
py_list_size(+PyList, -Size)
Get the size of a Python list.
py_list_get(+PyList, +Index, -Value)
Get an item from a Python list by index (0-based).
py_list_set(+PyList, +Index, +Value)
Set an item in a Python list by index.
py_list_append(+PyList, +Value)
Append a value to the end of a Python list.
Python Tuples
py_tuple_new(+Size, -PyTuple)
Create a new Python tuple with the specified size.
Returns: NEW reference - must call py_xdecref(PyTuple) when done.
py_tuple_from_prolog(+PrologList, -PyTuple)
Convert a Prolog list to a Python tuple. Supports nested structures.
?- py_tuple_from_prolog([1, 2, 3], T), py_tuple_size(T, Size), py_xdecref(T). Size = 3.
py_tuple_to_prolog(+PyTuple, -PrologList)
Convert a Python tuple to a Prolog list. Supports nested structures.
py_tuple_size(+PyTuple, -Size)
Get the size of a Python tuple.
py_tuple_get(+PyTuple, +Index, -Value)
Get an item from a Python tuple by index (0-based).
Note: Tuples are immutable - no set or append operations.
Memory Management
All Python object creation predicates return NEW references that must be cleaned up:
% Good - cleanup with py_xdecref ?- py_list_new(L), py_list_append(L, 42), py_xdecref(L). % Bad - memory leak ?- py_list_new(L), py_list_append(L, 42).
Examples
Basic Demo (v0.1.0)
See examples/python_demo.pl for basic usage examples:
scryer-prolog examples/python_demo.pl
Advanced Demo (v0.2.0)
See examples/python_demo_v2.pl for advanced features including dictionaries, type conversion, and globals/locals:
scryer-prolog examples/python_demo_v2.pl
Test Suite
Phase 1 Tests (v0.4.0):
# Unit tests scryer-prolog tests/unit/test_py_none_simple.pl # Python None (5/5 tests) scryer-prolog tests/unit/test_py_list_simple.pl # Python Lists (7/7 tests) scryer-prolog tests/unit/test_py_tuple_simple.pl # Python Tuples (6/6 tests) # Integration tests scryer-prolog tests/integration/test_phase1_types.pl # All Phase 1 types (5/5 tests)
Legacy Tests (v0.2.0):
scryer-prolog examples/tests/test_all_types.pl # Type conversion tests scryer-prolog examples/tests/test_dict_to_list.pl # Dictionary conversion tests scryer-prolog examples/tests/test_globals_locals.pl # Globals/locals tests scryer-prolog examples/tests/test_memory_management.pl # Memory management stress tests
Architecture
The library uses Scryer Prolog's FFI to call Python C API functions directly, without requiring any C wrapper code.
Core Functions
Py_Initialize(): Initialize the Python interpreterPy_Finalize(): Finalize the Python interpreterPyRun_SimpleString(code): Execute Python code in main namespacePyRun_String(code, start, globals, locals): Execute with explicit namespaces
Dictionary Operations
PyDict_New(): Create empty dictionaryPyDict_SetItemString(dict, key, value): Set item by string keyPyDict_GetItemString(dict, key): Get item by string keyPyDict_Keys(dict): Get list of keysPyDict_Size(dict): Get number of items
Type Conversion
PyLong_FromLong(long),PyLong_AsLong(obj): Integer conversionPyFloat_FromDouble(double),PyFloat_AsDouble(obj): Float conversionPyUnicode_FromString(str),PyUnicode_AsUTF8(obj): String conversionPyObject_IsTrue(obj): Boolean conversion
List Operations (v0.4.0)
PyList_New(size): Create new listPyList_Size(list): Get list lengthPyList_GetItem(list, index): Get item at index (BORROWED reference)PyList_SetItem(list, index, item): Set item (STEALS reference)PyList_Append(list, item): Append item (does NOT steal)
Tuple Operations (v0.4.0)
PyTuple_New(size): Create new tuplePyTuple_Size(tuple): Get tuple lengthPyTuple_GetItem(tuple, index): Get item at index (BORROWED reference)PyTuple_SetItem(tuple, index, item): Set item during construction (STEALS reference)
Error Handling
PyErr_Occurred(): Check if error occurredPyErr_Clear(): Clear error state
Type Discovery
The library uses a try-convert approach: it attempts each type conversion (string, int, float, boolean) in order and checks for Python errors to determine the actual type. This avoids issues with type macros that aren't available via FFI.
Current Research Questions
This is an active research project. We're exploring fundamental questions about Prolog/Python interop:
Design Philosophy
- Imperative vs Declarative: How do we reconcile Python's imperative style with Prolog's declarative paradigm?
- Mutation: Should Python mutations be visible in Prolog? How do we handle side effects?
- Object Identity: How do we represent Python object identity in Prolog's relational model?
Type System
- Type Mapping: What's the right mapping between Prolog terms and Python types?
- None vs fail(): Should Python
Nonemap to Prologfalse, a special term, or something else? - Compound Terms: How do we handle nested Prolog structures in Python?
Execution Model
- Backtracking: Can Python code participate in Prolog's backtracking? Should it?
- Exception Handling: How do Python exceptions interact with Prolog's error handling?
- Concurrency: With Python 3.12's per-interpreter GIL, what does multi-interpreter support look like in Prolog?
API Design
- Low-level vs High-level: Should we expose raw Python C API, or build higher-level abstractions?
- Resource Management: Is explicit py_initialize/py_finalize the right model, or should we auto-manage?
- Scoping: Do we need explicit resource scopes like libpython-clj's
with-gil?
These are not rhetorical questions. We're actively experimenting and the answers will shape future versions.
Contributing
Given the research nature of this project, contributions should focus on:
- Exploring design alternatives - Try different approaches, even if they conflict with current design
- Documenting trade-offs - What works? What doesn't? Why?
- Use case discovery - What problems does Prolog/Python integration actually solve?
- Testing paradigm boundaries - Push the limits of what's possible
Do not expect API stability. Code written against today's API may not work tomorrow.
License
[Your chosen license]
Documentation
Parts of this documentation (including architecture guides and design documents) are procedurally generated to ground and guide the development process. This approach helps maintain consistency and clarity as the project evolves through rapid iteration.
Credits
- Largely based on libpython-clj by Chris Nuernberger
- Uses Scryer Prolog FFI
- Inspired by libscryer-clj's machine-based lifecycle approach