Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ jobs:

test-linux:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.13', '3.14']
steps:
- name: Checkout repository
uses: actions/checkout@v3
Expand All @@ -28,22 +32,28 @@ jobs:
context: .
push: false
load: true
tags: sauerkraut:test
tags: sauerkraut:test-${{ matrix.python-version }}
build-args: |
PYTHON_VERSION=${{ matrix.python-version }}

- name: Run tests
run: |
docker run --rm sauerkraut:test python3 /sauerkraut/test/test.py
docker run --rm sauerkraut:test-${{ matrix.python-version }} python3 /sauerkraut/test/test.py

test-macos:
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.13', '3.14']
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Python 3.13
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: '3.13'
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -Wall")

# Find Python (ensure the version matches your Python version)
find_package(Python 3.13 COMPONENTS Interpreter Development REQUIRED)
find_package(Python 3.13...<3.15 COMPONENTS Interpreter Development REQUIRED)

# Set consistent output directories
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
FROM ubuntu:22.04 AS base
LABEL maintainer="Zane Fink <zanef2@illinois.edu>"
ARG PYTHON_VERSION=3.13
WORKDIR /
ENV DEBIAN_FRONTEND=noninteractive
RUN ln -fs /usr/share/zoneinfo/UTC /etc/localtime && \
Expand All @@ -18,7 +19,7 @@ ENV PATH=/opt/conda/bin:$PATH
RUN conda init bash && \
conda config --add channels conda-forge && \
conda config --set channel_priority strict && \
conda create -n sauerkraut python=3.13 -y -c conda-forge --override-channels && \
conda create -n sauerkraut python=${PYTHON_VERSION} -y -c conda-forge --override-channels && \
echo "conda activate sauerkraut" >> ~/.bashrc
# Make the conda environment available in PATH
ENV PATH=/opt/conda/envs/sauerkraut/bin:$PATH
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,4 @@ python3 copy_then_serialize.py

## Compatibility
Sauerkraut leverages intimate knowledge of CPython internals, and as such is vulnerable to changes in the CPython API and VM.
Currently, Sauerkraut supports Python 3.13 and the development version of Python 3.14.
Currently, Sauerkraut supports Python 3.13 and 3.14.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ classifiers = [
"Topic :: System :: Distributed Computing",
]
dependencies = [
"bytecode",
"bytecode @ git+https://github.com/ZwFink/bytecode.git",
"numpy",
]

Expand Down
4 changes: 4 additions & 0 deletions sauerkraut/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ if(APPLE)
greenlet_compat
)
target_link_options(sauerkraut PRIVATE -undefined dynamic_lookup)
set_target_properties(sauerkraut PROPERTIES
BUILD_WITH_INSTALL_RPATH ON
INSTALL_RPATH "@loader_path"
)
else()
target_link_libraries(sauerkraut
PRIVATE
Expand Down
23 changes: 19 additions & 4 deletions sauerkraut/include/py_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ typedef struct _CodeUnit {
uint8_t oparg;
} _CodeUnit;

typedef _CodeUnit _Py_CODEUNIT;

struct _frame {
PyObject_HEAD
PyFrameObject *f_back; /* previous frame, or NULL */
Expand All @@ -23,6 +25,9 @@ struct _frame {
char f_trace_opcodes; /* Emit per-opcode trace events? */
PyObject *f_extra_locals; /* Dict for locals set by users using f_locals, could be NULL */
PyObject *f_locals_cache; /* Backwards compatibility for PyEval_GetLocals */
#if SAUERKRAUT_PY314
PyObject *f_overwritten_fast_locals;
#endif
PyObject *_f_frame_data[1]; /* Frame data if this frame object owns the frame */
};

Expand All @@ -32,19 +37,29 @@ _PyThreadState_PushFrame(PyThreadState *tstate, size_t size);
typedef struct _PyInterpreterFrame {
_PyStackRef f_executable; /* Deferred or strong reference (code object or None) */
struct _PyInterpreterFrame *previous;
#if SAUERKRAUT_PY314
_PyStackRef f_funcobj; /* Deferred or strong reference. Only valid if not on C stack */
#else
PyObject *f_funcobj; /* Strong reference. Only valid if not on C stack */
#endif
PyObject *f_globals; /* Borrowed reference. Only valid if not on C stack */
PyObject *f_builtins; /* Borrowed reference. Only valid if not on C stack */
PyObject *f_locals; /* Strong reference, may be NULL. Only valid if not on C stack */
PyFrameObject *frame_obj; /* Strong reference, may be NULL. Only valid if not on C stack */
_CodeUnit *instr_ptr; /* Instruction currently executing (or about to begin) */
#if SAUERKRAUT_PY314
_Py_CODEUNIT *instr_ptr; /* Instruction currently executing (or about to begin) */
#if SAUERKRAUT_PY314
_PyStackRef *stackpointer;
#elif SAUERKRAUT_PY313
#ifdef Py_GIL_DISABLED
int32_t tlbc_index; /* Index of thread-local bytecode containing instr_ptr */
#endif
#elif SAUERKRAUT_PY313
int stacktop;
#endif
#endif
uint16_t return_offset; /* Only relevant during a function call */
char owner;
#if SAUERKRAUT_PY314
uint8_t visited;
#endif
/* Locals and stack */
_PyStackRef localsplus[1];
} _PyInterpreterFrame;
Expand Down
5 changes: 5 additions & 0 deletions sauerkraut/include/pyref.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ class py_strongref {
T *operator->() {
return this->borrow();
}

void reset() {
Py_XDECREF((PyObject*) obj);
obj = NULL;
}
};

template <typename T>
Expand Down
151 changes: 146 additions & 5 deletions sauerkraut/include/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,143 @@ namespace utils {
return PyDict_Check(obj);
}

struct StackRefObject {
PyObject *obj;
bool owned;
};

class ScopedStackRefObject {
StackRefObject ref;
public:
ScopedStackRefObject(_PyStackRef stackref);
~ScopedStackRefObject() {
if (ref.owned && ref.obj) {
Py_DECREF(ref.obj);
}
}
ScopedStackRefObject(const ScopedStackRefObject&) = delete;
ScopedStackRefObject& operator=(const ScopedStackRefObject&) = delete;
PyObject* get() const { return ref.obj; }
explicit operator bool() const { return ref.obj != nullptr; }
};

#if SAUERKRAUT_PY314
constexpr uintptr_t STACKREF_TAG_BITS = 0x7;
constexpr uintptr_t STACKREF_TAG_INT = 0x3;
constexpr uintptr_t STACKREF_TAG_REFCNT = 0x1;
constexpr int STACKREF_TAGGED_SHIFT = 2;

inline bool stackref_is_null(_PyStackRef ref) {
if (ref.bits == 0) return true;
// Check for deferred NULL: when only the deferred tag is set (bits=0x1)
// After clearing tags, the pointer would be NULL
if ((ref.bits & STACKREF_TAG_BITS) == STACKREF_TAG_REFCNT &&
(ref.bits & ~STACKREF_TAG_BITS) == 0) {
return true;
}
return false;
}

inline bool stackref_is_tagged_int(_PyStackRef ref) {
return (ref.bits & STACKREF_TAG_BITS) == STACKREF_TAG_INT;
}

inline bool stackref_is_deferred(_PyStackRef ref) {
return (ref.bits & STACKREF_TAG_BITS) == STACKREF_TAG_REFCNT;
}

inline intptr_t stackref_untag_int(_PyStackRef ref) {
return ((intptr_t)ref.bits) >> STACKREF_TAGGED_SHIFT;
}

inline PyObject *stackref_as_pyobject(_PyStackRef ref) {
if (stackref_is_null(ref) || stackref_is_tagged_int(ref)) {
return NULL;
}
uintptr_t bits = ref.bits;
if (stackref_is_deferred(ref)) {
bits &= ~STACKREF_TAG_BITS;
}
return (PyObject *)bits;
}

inline StackRefObject stackref_to_object_for_serialization(_PyStackRef ref) {
if (stackref_is_null(ref)) {
return {NULL, false};
}
if (stackref_is_tagged_int(ref)) {
auto value = (Py_ssize_t) stackref_untag_int(ref);
return {PyLong_FromSsize_t(value), true};
}
return {stackref_as_pyobject(ref), false};
}

inline void stackref_decref(_PyStackRef ref) {
if (stackref_is_null(ref) || stackref_is_tagged_int(ref)) {
return;
}
Py_XDECREF(stackref_as_pyobject(ref));
}

inline PyObject *get_funcobj(sauerkraut::PyInterpreterFrame *frame) {
return stackref_as_pyobject(frame->f_funcobj);
}

inline void set_funcobj(sauerkraut::PyInterpreterFrame *frame, PyObject *obj) {
frame->f_funcobj.bits = (uintptr_t)obj;
}

inline void set_stack_position(sauerkraut::PyInterpreterFrame *frame, int nlocalsplus, int stack_depth) {
frame->stackpointer = frame->localsplus + nlocalsplus + stack_depth;
}

inline void init_frame_visited(sauerkraut::PyInterpreterFrame *frame) {
frame->visited = 0;
}
#else
inline bool stackref_is_null(_PyStackRef ref) {
return ref.bits == 0;
}

inline bool stackref_is_tagged_int(_PyStackRef) {
return false;
}

inline PyObject *stackref_as_pyobject(_PyStackRef ref) {
return (PyObject *) ref.bits;
}

inline StackRefObject stackref_to_object_for_serialization(_PyStackRef ref) {
if (ref.bits == 0) {
return {NULL, false};
}
return {(PyObject *) ref.bits, false};
}

inline void stackref_decref(_PyStackRef ref) {
Py_XDECREF((PyObject *) ref.bits);
}

inline PyObject *get_funcobj(sauerkraut::PyInterpreterFrame *frame) {
return frame->f_funcobj;
}

inline void set_funcobj(sauerkraut::PyInterpreterFrame *frame, PyObject *obj) {
frame->f_funcobj = obj;
}

inline void set_stack_position(sauerkraut::PyInterpreterFrame *frame, int nlocalsplus, int stack_depth) {
frame->stacktop = nlocalsplus + stack_depth;
}

inline void init_frame_visited(sauerkraut::PyInterpreterFrame *) {
// No-op for Python 3.13
}
#endif

inline ScopedStackRefObject::ScopedStackRefObject(_PyStackRef stackref)
: ref(stackref_to_object_for_serialization(stackref)) {}

int get_code_stacksize(PyCodeObject *code) {
return code->co_stacksize;
}
Expand Down Expand Up @@ -468,7 +605,11 @@ namespace utils {

_PyStackRef *stack_pointer = iframe->localsplus + code->co_nlocalsplus;
for(int i = 0; i < stack_depth; i++) {
PyObject *stack_obj = (PyObject*) stack_pointer[i].bits;
_PyStackRef stack_ref = stack_pointer[i];
PyObject *stack_obj = stackref_as_pyobject(stack_ref);
if (stack_obj == NULL && stackref_is_tagged_int(stack_ref)) {
stack_obj = Py_None;
}
#ifdef DEBUG
assert(NULL != stack_obj);
if(locals.find((intptr_t) stack_obj) != locals.end()) {
Expand Down Expand Up @@ -502,7 +643,7 @@ namespace utils {
LocalNameMap local_idx_map;

for(int i = 0; i < code->co_nlocalsplus; i++) {
PyObject *local = ((PyObject*) iframe->localsplus[i].bits);
PyObject *local = stackref_as_pyobject(iframe->localsplus[i]);
std::string name = PyUnicode_AsUTF8(PyTuple_GetItem(locals_plus_names, i));
local_idx_map[name] = i;
}
Expand Down Expand Up @@ -575,19 +716,19 @@ namespace utils {
}

if (local_index >= 0) {
PyObject *old_local = (PyObject*) iframe->localsplus[local_index].bits;
_PyStackRef old_ref = iframe->localsplus[local_index];

// Increment reference count of new value before assigning
Py_INCREF(value);
iframe->localsplus[local_index].bits = (intptr_t) value;

// Decrement reference count of old value
Py_XDECREF(old_local);
stackref_decref(old_ref);
}
}
}
}
}


#endif // UTILS_HH_INCLUDED
#endif // UTILS_HH_INCLUDED
16 changes: 13 additions & 3 deletions sauerkraut/liveness.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
import bytecode as bc
from bytecode import Instr, BasicBlock, ControlFlowGraph, Bytecode

_USE_INSTRS = ("LOAD_NAME", "LOAD_FAST", "LOAD_FAST_CHECK", "LOAD_FAST_AND_CLEAR")
_SUPER_USE_INSTRS = "LOAD_FAST_LOAD_FAST"
_USE_INSTRS = (
"LOAD_NAME",
"LOAD_FAST",
"LOAD_FAST_CHECK",
"LOAD_FAST_AND_CLEAR",
"LOAD_FAST_BORROW",
)
_SUPER_USE_INSTRS = ("LOAD_FAST_LOAD_FAST", "LOAD_FAST_BORROW_LOAD_FAST_BORROW")
_SUPER_DEF_INSTRS = "STORE_FAST_STORE_FAST"
_DEF_INSTRS = ("STORE_NAME", "STORE_FAST")

Expand Down Expand Up @@ -96,7 +102,11 @@ def _get_uses_and_defs(self, block: BasicBlock) -> Tuple[Set[str], Set[str]]:
if isinstance(arg1, str):
defined_vars.add(arg1)
elif instr.name == "STORE_FAST_LOAD_FAST":
print(f"STORE_FAST_LOAD_FAST: {instr.arg}")
arg0, arg1 = instr.arg
if isinstance(arg0, str):
defined_vars.add(arg0) # First operand is stored
if isinstance(arg1, str):
used_vars.add(arg1) # Second operand is loaded

return used_vars, defined_vars

Expand Down
Loading
Loading