martineausw/ziggurat
Type assertion and constraints zig library.
Library for defining type constraints and assertions.
Inspired off of this brainstorming thread.
const any_data: ziggurat.Prototype = .any(&.{
.is_array(.{}),
.is_vector(.{}),
.is_pointer(.{ .size = .{ .slice = true } }),
})
pub fn wrapIndex(
data: anytype,
index: i128,
) ziggurat.sign(any_data)(@TypeOf(data))(usize) {
return if (index < 0)
getLen(data) - @as(usize, @intCast(@abs(index)))
else
@as(usize, @intCast(index));
}
pub fn at(
data: anytype,
index: i128,
) ziggurat.sign(any_data)(@TypeOf(data))(switch (@typeInfo(@TypeOf(data))) {
inline .array, .vector => |info| info.child,
.pointer => |info| switch (info.size) {
.slice => info.child,
else => unreachable,
},
else => unreachable,
}) {
return switch (@typeInfo(@TypeOf(data))) {
.pointer => |info| switch (info.size) {
.slice => data[wrapIndex(data, index)],
else => unreachable,
},
.array, .vector => data[wrapIndex(data, index)],
else => unreachable,
};
}
The goal of ziggurat is to enable developers to comprehensibly define arbitrarily complex type constraints and assertions.
zig fetch --save git+https://github.com/martineausw/ziggurat.git#0.0.0
cd /path/to/clone/
git clone https://github.com/martineausw/ziggurat.git#0.0.0
cd /path/to/zig/project
zig fetch --save /path/to/clone/ziggurat/
ziggurat makes generous use of closures. This is done by returning function pointers as a member access of struct definitions.
I justify using closures as it lends itself to a declarative approach which appears more sensible than an imperative approach, in that it's easier to wrap (ha) my head around and favors type safety, also I appreciate the aesthetics.
fn foo() fn () void { // Returns signature of enclosed function
return struct {
fn bar() void {}
}.bar; // Accesses `bar` function pointer.
}
const bar: *const fn () void = foo();
// These are all equivalent: bar() == foo()() == void
test "closure equality" {
try std.testing.expectEqual(*const fn () void, @TypeOf(bar));
try std.testing.expectEqual(bar, foo());
try std.testing.expectEqual(void, @TypeOf(bar()));
try std.testing.expectEqual(bar(), foo()());
}
That out of the way, hopefully this isn't terribly intimidating:
fn foo(actual_value: anytype) sign(some_prototype)(actual_value)(void) { ... }
Prototype requires an eval function pointer.
const Prototype = struct {
name: [:0]const u8,
eval: *const fn (actual: anytype) anyerror!bool,
onFail: ?*const fn (prototype: Prototype, actual: anytype) void = null,
onError: ?*const fn (err: anyerror, prototype: Prototype, actual: anytype) void = null,
};
Here is an example implementation of a prototype.
const int: Prototype = .{
.eval = struct {
fn eval(actual: anytype) !bool {
return switch (@typeInfo(@TypeOf(actual))) {
.int => true,
else => false,
};
}
}.eval,
};
Here's an implementation that only accepts odd integer values:
const odd_int: Prototype = .{
.eval = struct {
fn eval(actual: anytype) bool {
return switch (@typeInfo(@TypeOf(actual))) {
.comptime_int => @mod(actual, 2) == 1,
.int => actual % 2 == 1,
else => false,
};
}
}.eval,
};
Intended to be used in comptime:
Boolean operations for prototype evaluation results
sign calls eval on a given prototype and will call onError or onFail for error or false return values, respectively. Complex prototypes are intended to be composed using operators and auxiliary prototypes.
pub fn sign(prototype: Prototype) fn (actual: anytype) fn (comptime return_type: type) type {
return struct {
pub fn validate(actual: anytype) fn (comptime return_type: type) type {
if (comptime prototype.eval(actual)) |result| {
if (!result) if (prototype.onFail) |onFail|
comptime onFail(prototype, actual);
} else |err| {
if (prototype.onError) |onError|
comptime onError(err, prototype, actual);
}
return struct {
pub fn returns(comptime return_type: type) type {
return return_type;
}
}.returns;
}
}.validate;
};