Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update PyList to use dynamic dispatch #41

Merged
merged 3 commits into from
Sep 7, 2023
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
4 changes: 2 additions & 2 deletions pydust/src/builtins.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub inline fn True() py.PyBool {

/// Get the length of the given object. Equivalent to len(obj) in Python.
pub fn len(object: anytype) !usize {
const obj = try py.object(object);
const obj = py.PyObject.of(object);
const length = ffi.PyObject_Length(obj.py);
if (length < 0) return PyError.Propagate;
return @intCast(length);
Expand All @@ -43,7 +43,7 @@ pub fn import(module_name: [:0]const u8) !py.PyObject {
pub fn super(comptime Super: type, selfInstance: anytype) !py.PyObject {
const imported = try import(py.findContainingModule(Super));
const superPyType = try imported.get(py.getClassName(Super));
const pyObj = try py.object(selfInstance);
const pyObj = py.PyObject.of(selfInstance);

const superBuiltin = py.PyObject{ .py = @alignCast(@ptrCast(&ffi.PySuper_Type)) };
return superBuiltin.call(py.PyObject, .{ superPyType, pyObj }, .{});
Expand Down
21 changes: 16 additions & 5 deletions pydust/src/pydust.zig
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,25 @@ inline fn NewArgs(comptime Cls: type) type {
return sig.argsParam orelse struct {};
}

/// Convert an instance of a Pydust class struct into PyObject instance
pub fn object(obj: anytype) !types.PyObject {
return tramp.Trampoline(@TypeOf(obj)).wrap(obj);
/// Create a new Python object from a Zig object.
/// Note this will always create a new strong reference.
pub fn toObject(value: anytype) !types.PyObject {
if (@TypeOf(value) == comptime_int) {
return tramp.Trampoline(i64).wrap(value);
}

if (@TypeOf(value) == comptime_float) {
return tramp.Trampoline(f64).wrap(value);
}

return tramp.Trampoline(@TypeOf(value)).wrap(value);
}

/// Convert a Python object into a Zig object.
pub fn as(comptime T: type, obj: anytype) !T {
return tramp.Trampoline(T).unwrap(try object(obj));
pub fn as(comptime T: type, obj: types.PyObject) !T {
// FIXME(ngates): ensure that we eat a reference to whatever the input object is.
// Unless we return as a Python object type.
return tramp.Trampoline(T).unwrap(obj);
}

pub fn getClassName(comptime definition: type) [:0]const u8 {
Expand Down
107 changes: 82 additions & 25 deletions pydust/src/trampoline.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,56 +10,113 @@ const PyError = @import("errors.zig").PyError;
/// Generate functions to convert comptime-known Zig types to/from py.PyObject.
pub fn Trampoline(comptime T: type) type {
return struct {
/// Wrap a Zig object into a PyObject.
pub inline fn wrap(obj: T) !py.PyObject {
const typeInfo = @typeInfo(T);

// Early return to handle errors
if (typeInfo == .ErrorUnion) {
const value = obj catch |err| return err;
return Trampoline(typeInfo.ErrorUnion.payload).wrap(value);
}

// Early return to handle optionals
if (typeInfo == .Optional) {
const value = obj orelse return py.None();
return Trampoline(typeInfo.Optional.child).wrap(value);
/// Wraps object that already represent existing Python objects.
/// In other words, Zig primitive types are not supported.
pub fn wrapObject(obj: T) py.PyObject {
if (!isObjectLike()) {
@compileError("Cannot convert " ++ @typeName(T) ++ " into a Python object");
}

switch (@typeInfo(T)) {
.Bool => return if (obj) py.True().obj else py.False().obj,
.ErrorUnion => @compileError("ErrorUnion already handled"),
.Float => return (try py.PyFloat.from(T, obj)).obj,
.Int => return (try py.PyLong.from(T, obj)).obj,
.Pointer => |p| {
// If the pointer is for ffi.PyObject, just wrap it up
// The object is an ffi.PyObject
if (p.child == ffi.PyObject) {
return .{ .py = obj };
}

// If the pointer is for a Pydust class
if (py.findClassName(p.child)) |_| {
const PyType = pytypes.State(p.child);
const pyobject: *ffi.PyObject = @constCast(@ptrCast(@fieldParentPtr(PyType, "state", obj)));
return .{ .py = pyobject };
const ffiObject: *ffi.PyObject = @constCast(@ptrCast(@fieldParentPtr(PyType, "state", obj)));
return .{ .py = ffiObject };
}

// If the pointer is for a Pydust module
if (py.findModuleName(p.child)) |_| {
@compileError("Cannot currently return modules");
}

@compileLog("Unsupported pointer type " ++ @typeName(p.child), py.State.classes(), py.State.modules());
},
.Struct => |s| {
.Struct => {
// Support all extensions of py.PyObject, e.g. py.PyString, py.PyFloat
if (@hasField(T, "obj") and @hasField(std.meta.fieldInfo(T, .obj).type, "py")) {
return obj.obj;
}

// Support py.PyObject
if (T == py.PyObject) {
return obj;
}
},
inline else => {},
}
@compileLog("Cannot convert into PyObject" ++ @typeName(T));
}

inline fn isObjectLike() bool {
switch (@typeInfo(T)) {
.Pointer => |p| {
// The object is an ffi.PyObject
if (p.child == ffi.PyObject) {
return true;
}

// If the pointer is for a Pydust class
if (py.findClassName(p.child)) |_| {
return true;
}

// If the pointer is for a Pydust module
if (py.findModuleName(p.child)) |_| {
// FIXME(ngates): support modules
return false;
}
},
.Struct => {
// Support all extensions of py.PyObject, e.g. py.PyString, py.PyFloat
if (@hasField(T, "obj") and @hasField(std.meta.fieldInfo(T, .obj).type, "py")) {
return true;
}

// Support py.PyObject
if (T == py.PyObject) {
return true;
}
},
inline else => {},
}
return false;
}

/// Wraps a Zig object into a new Python object.
/// Always creates a new strong reference.
pub inline fn wrap(obj: T) !py.PyObject {
const typeInfo = @typeInfo(T);

// Early return to handle errors
if (typeInfo == .ErrorUnion) {
const value = obj catch |err| return err;
return Trampoline(typeInfo.ErrorUnion.payload).wrap(value);
}

// Early return to handle optionals
if (typeInfo == .Optional) {
const value = obj orelse return py.None();
return Trampoline(typeInfo.Optional.child).wrap(value);
}

// Shortcut for object types
if (isObjectLike()) {
const pyobj = wrapObject(obj);
pyobj.incref();
return pyobj;
}

switch (@typeInfo(T)) {
.Bool => return if (obj) py.True().obj else py.False().obj,
.ErrorUnion => @compileError("ErrorUnion already handled"),
.Float => return (try py.PyFloat.from(T, obj)).obj,
.Int => return (try py.PyLong.from(T, obj)).obj,
.Struct => |s| {
// If the struct is a tuple, return a Python tuple
if (s.is_tuple) {
const tuple = try py.PyTuple.new(s.fields.len);
Expand Down Expand Up @@ -187,7 +244,7 @@ pub fn Trampoline(comptime T: type) type {

pub fn getArg(self: CallArgs, comptime R: type, idx: usize) !R {
const args = self.args orelse return py.TypeError.raise("missing args");
return py.as(R, args.getItem(idx));
return py.as(R, try args.getItem(idx));
}

pub fn getKwarg(self: CallArgs, comptime R: type, name: []const u8) !?R {
Expand Down
2 changes: 1 addition & 1 deletion pydust/src/types/buffer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pub const PyBuffer = extern struct {

pub fn initFromSlice(self: *Self, comptime T: type, values: []T, shape: []const isize, owner: anytype) void {
// We need to incref the owner object because it's being used by the view.
const ownerObj = try py.object(owner);
const ownerObj = py.PyObject.of(owner);
ownerObj.incref();

self.* = .{
Expand Down
75 changes: 41 additions & 34 deletions pydust/src/types/list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub const PyList = extern struct {
obj: py.PyObject,

pub fn of(obj: py.PyObject) PyList {
if (ffi.PyList_Check(obj.py) == 0) {
return py.TypeError.raise("expected list");
}
return .{ .obj = obj };
}

Expand All @@ -19,21 +22,21 @@ pub const PyList = extern struct {
return .{ .obj = .{ .py = list } };
}

pub fn length(self: *const PyList) usize {
pub fn length(self: PyList) usize {
return @intCast(ffi.PyList_Size(self.obj.py));
}

// Returns borrowed reference.
pub fn getItem(self: *const PyList, idx: isize) !PyObject {
pub fn getItem(self: PyList, comptime T: type, idx: isize) !T {
if (ffi.PyList_GetItem(self.obj.py, idx)) |item| {
return .{ .py = item };
return py.as(T, .{ .py = item });
} else {
return PyError.Propagate;
}
}

// Returns new reference with borrowed items.
pub fn getSlice(self: *const PyList, low: isize, high: isize) !PyList {
// Returns a slice of the list.
pub fn getSlice(self: PyList, low: isize, high: isize) !PyList {
if (ffi.PyList_GetSlice(self.obj.py, low, high)) |item| {
return .{ .obj = .{ .py = item } };
} else {
Expand All @@ -42,47 +45,54 @@ pub const PyList = extern struct {
}

/// This function “steals” a reference to item and discards a reference to an item already in the list at the affected position.
pub fn setOwnedItem(self: *const PyList, pos: isize, value: PyObject) !void {
if (ffi.PyList_SetItem(self.obj.py, pos, value.py) < 0) {
pub fn setOwnedItem(self: PyList, pos: isize, value: anytype) !void {
// Since this function steals the reference, it can only accept object-like values.
const valueObj = py.PyObject.of(value);
if (ffi.PyList_SetItem(self.obj.py, pos, valueObj.py) < 0) {
return PyError.Propagate;
}
}

/// Does not steal a reference to value.
pub fn setItem(self: *const PyList, pos: isize, value: PyObject) !void {
defer value.incref();
return self.setOwnedItem(pos, value);
/// Set the item at the given position.
/// This function acquires its own reference to value.
pub fn setItem(self: PyList, pos: isize, value: anytype) !void {
const valueObj = try py.toObject(value);
return self.setOwnedItem(pos, valueObj);
}

// Insert the item item into list list in front of index idx.
pub fn insert(self: *const PyList, idx: isize, value: PyObject) !void {
if (ffi.PyList_Insert(self.obj.py, idx, value.py) < 0) {
pub fn insert(self: PyList, idx: isize, value: anytype) !void {
const valueObj = try py.toObject(value);
defer valueObj.decref();
if (ffi.PyList_Insert(self.obj.py, idx, valueObj.py) < 0) {
return PyError.Propagate;
}
}

// Append the object item at the end of list list.
pub fn append(self: *const PyList, value: PyObject) !void {
if (ffi.PyList_Append(self.obj.py, value.py) < 0) {
pub fn append(self: PyList, value: anytype) !void {
const valueObj = try py.toObject(value);
defer valueObj.decref();
if (ffi.PyList_Append(self.obj.py, valueObj.py) < 0) {
return PyError.Propagate;
}
}

// Sort the items of list in place.
pub fn sort(self: *const PyList) !void {
pub fn sort(self: PyList) !void {
if (ffi.PyList_Sort(self.obj.py) < 0) {
return PyError.Propagate;
}
}

// Reverse the items of list in place.
pub fn reverse(self: *const PyList) !void {
pub fn reverse(self: PyList) !void {
if (ffi.PyList_Reverse(self.obj.py) < 0) {
return PyError.Propagate;
}
}

pub fn asTuple(self: *const PyList) !py.PyTuple {
pub fn toTuple(self: PyList) !py.PyTuple {
return try py.PyTuple.of(.{ .py = ffi.PyList_AsTuple(self.obj.py) orelse return PyError.Propagate });
}

Expand All @@ -103,30 +113,27 @@ test "PyList" {

var list = try PyList.new(2);
defer list.decref();
try list.setItem(0, 1);
try list.setItem(1, 2.0);

const first = try PyLong.from(i64, 1);
defer first.decref();
try testing.expectEqual(@as(usize, 2), list.length());
try list.setItem(0, first.obj);
try testing.expectEqual(@as(i64, 1), try py.as(i64, list.getItem(0)));

const second = try PyLong.from(i64, 2);
try list.setOwnedItem(1, second.obj); // owned by the list, don't decref
try testing.expectEqual(@as(i64, 1), try list.getItem(i64, 0));
try testing.expectEqual(@as(f64, 2.0), try list.getItem(f64, 1));

const third = try PyLong.from(i64, 3);
defer third.decref();
try list.append(third.obj);
try list.append(3);
try testing.expectEqual(@as(usize, 3), list.length());
try testing.expectEqual(@as(i64, 3), try py.as(i64, list.getItem(2)));
try testing.expectEqual(@as(i32, 3), try list.getItem(i32, 2));

try list.insert(0, 1.23);
try list.reverse();
try testing.expectEqual(@as(i64, 3), try py.as(i64, list.getItem(0)));
try testing.expectEqual(@as(i64, 1), try py.as(i64, list.getItem(2)));
try testing.expectEqual(@as(f32, 1.23), try list.getItem(f32, 3));

try list.sort();
try testing.expectEqual(@as(i64, 1), try py.as(i64, list.getItem(0)));
try testing.expectEqual(@as(i64, 3), try py.as(i64, list.getItem(2)));
try testing.expectEqual(@as(i64, 1), try list.getItem(i64, 0));

const tuple = try list.asTuple();
const tuple = try list.toTuple();
defer tuple.decref();
try std.testing.expectEqual(@as(usize, 3), tuple.length());

try std.testing.expectEqual(@as(usize, 4), tuple.length());
}
6 changes: 6 additions & 0 deletions pydust/src/types/obj.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ pub const PyObject = extern struct {

py: *ffi.PyObject,

/// Converts any PyObject-like value into its PyObject representation.
/// No new references are created.
pub fn of(obj: anytype) PyObject {
return tramp.Trampoline(@TypeOf(obj)).wrapObject(obj);
}

pub fn incref(self: PyObject) void {
ffi.Py_INCREF(self.py);
}
Expand Down
2 changes: 1 addition & 1 deletion pydust/src/types/tuple.zig
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub const PyTuple = extern struct {
if (!@typeInfo(@TypeOf(values)).Struct.is_tuple) {
@compileError("Must pass a Zig tuple into PyTuple.from");
}
return of(try py.object(values));
return of(try py.toObject(values));
}

pub fn length(self: *const PyTuple) usize {
Expand Down