Inferred error set for a function type

I have a tree walker that looks like this

pub fn mapExpr(T: type, ET: type, fun: fn (ir: *IR, context: *T, expr: Expr) ET!?Expr) type {
    return struct {
        pub fn run(ir: *IR, context: *T, expr: Expr) !Expr {
            switch (expr) {
...

Is there a way to not have to explicitly pass the error type of fun?

This seems to do the trick but how do I document the type of fun?

pub fn mapExpr(T: type, fun: type) type {

If I understand your question correctly, you can use anyerror:

pub fn main() !void {
    _ = try ziggit(letsee);
}

fn ziggit(fun: fn (f64, f64) anyerror!f64) anyerror!f64 {
    return fun(1, 2);
}

fn letsee(x: f64, y: f64) error{LetSee}!f64 {
    return x + y;
}

I tried that but got

src/ir_gen.zig:768:21: error: expected type 'error{NoSpaceLeft,Overflow,DiskQuota,FileTooBig,InputOutput,DeviceBusy,InvalidArgument,AccessDenied,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer,ProcessNotFound,NoDevice,Unexpected,OutOfMemory,InvalidExpression,InvalidCharacter,DuplicateSymbol,SyntaxError,LayoutMismatch,LayoutWidthNotSet,MissingSymbol,MissingExport,DuplicateDefaultSegment,InvalidSegment,SizeMismatch,ArgumentMismatch,InvalidOperator,InvalidSize,InvalidAction}', found 'anyerror'
        const new = try transform.run(ir, &context, action);
src/ir_gen.zig:768:21: note: global error set cannot cast into a smaller set

Hard to tell without more context, the snippet compiles and runs. Maybe the others will find a better way

After thinking about it, I think that’s because you use the function fun in another function that returns a defined error set. In that case, anyerror can’t coerce to it as it’s smaller.

pub fn main() !void {
    _ = try ziggit(letsee);
}

fn ziggit(fun: fn (f64, f64) anyerror!f64) error{OtherErr}!f64 { // <- Error type is narrowed here
    return fun(1, 2);
}

fn letsee(x: f64, y: f64) error{LetSee}!f64 {
    return x + y;
}

This doesn’t compile anymore.

Yes, that’s my case exactly. It looks like fun: antype (not type like I posted) is the only way out.

What happens when you replace ET!?Expr with anyerror!?Expr in the signature of fun?

It doesn’t work.

Hmm. That’s one of the problems with posting only a few lines from a program which don’t compile: we have no idea what the rest of it looks like, so the amount of help we can provide is limited.

Can you come up with a minimal, but complete, worked example? It doesn’t have to compile successfully, of course, just something which you hoped would compile.

2 Likes

The error

❯ zig run ~/Downloads/walker.zig
/Users/joelr/Downloads/walker.zig:38:64: error: function type cannot have an inferred error set
const Walker = fn (alloc: Allocator, ctx: anytype, expr: Expr) !void;

The code only compiles if I type walker as anytype.

const std = @import("std");

const mem = std.mem;

const Allocator = mem.Allocator;

pub const Error = Allocator.Error || error{};

pub const Expr = union(enum) {
    binary: Binary,
    paren: *Expr,
    size: usize,

    const Self = @This();

    pub const Binary = struct {
        op: BinaryOp,
        lhs: *Expr,
        rhs: *Expr,
    };

    pub const BinaryOp = enum {
        @"||",
        @"&&",
    };
};

pub fn clone(alloc: Allocator, T: type, value: T) Error!*T {
    const result = try alloc.create(T);
    result.* = value;
    return result;
}

pub fn cloneExpr(alloc: Allocator, expr: Expr) Error!*Expr {
    return clone(alloc, Expr, expr);
}

const Walker = fn (alloc: Allocator, ctx: anytype, expr: Expr) !void;

pub fn walkExpr(T: type, walk: Walker) type {
    return struct {
        pub fn run(alloc: Allocator, context: *T, node: Expr) !void {
            switch (node) {
                .binary => |binary| {
                    try run(alloc, context, binary.lhs.*);
                    try run(alloc, context, binary.rhs.*);
                },
                .paren => |paren| try run(alloc, context, paren.*),
                else => try walk(alloc, context, node),
            }
        }
    };
}

pub fn main() !void {
    const page_alloc = std.heap.page_allocator;
    var arena = std.heap.ArenaAllocator.init(page_alloc);
    defer arena.deinit();
    const alloc = arena.allocator();
    const sizeExpr = struct {
        fn walk(_: Allocator, ctx: *usize, expr: Expr) Error!void {
            switch (expr) {
                .size => |size| ctx.* += size,
                else => {},
            }
        }
    }.walk;
    const walker = walkExpr(usize, sizeExpr);
    const one = Expr{ .size = 1 };
    const two = Expr{ .size = 2 };
    const three = Expr{ .size = 3 };
    const binary = Expr{
        .binary = .{
            .op = .@"||",
            .lhs = try cloneExpr(alloc, one),
            .rhs = try cloneExpr(alloc, two),
        },
    };
    const paren = Expr{
        .paren = try cloneExpr(alloc, three),
    };
    const expr = Expr{
        .binary = .{
            .op = .@"&&",
            .lhs = try cloneExpr(alloc, binary),
            .rhs = try cloneExpr(alloc, paren),
        },
    };
    var size: usize = 0;
    try walker.run(alloc, &size, expr);
    std.debug.print("size = {d}\n", .{size});
}

I’m reasonably content with this version, though

const std = @import("std");

const mem = std.mem;

const Allocator = mem.Allocator;

pub const Error = Allocator.Error || error{};

pub const Expr = union(enum) {
    binary: Binary,
    paren: *Expr,
    size: usize,

    const Self = @This();

    pub const Binary = struct {
        op: BinaryOp,
        lhs: *Expr,
        rhs: *Expr,
    };

    pub const BinaryOp = enum {
        @"||",
        @"&&",
    };
};

pub fn clone(alloc: Allocator, T: type, value: T) Error!*T {
    const result = try alloc.create(T);
    result.* = value;
    return result;
}

pub fn cloneExpr(alloc: Allocator, expr: Expr) Error!*Expr {
    return clone(alloc, Expr, expr);
}

pub fn walkExpr(T: type, E: type) type {
    return struct {
        pub const Walker = fn (alloc: Allocator, ctx: *T, expr: Expr) E!void;
        pub fn run(alloc: Allocator, context: *T, walk: Walker, node: Expr) E!void {
            switch (node) {
                .binary => |binary| {
                    try run(alloc, context, walk, binary.lhs.*);
                    try run(alloc, context, walk, binary.rhs.*);
                },
                .paren => |paren| try run(alloc, context, walk, paren.*),
                else => try walk(alloc, context, node),
            }
        }
    };
}

pub fn main() !void {
    const page_alloc = std.heap.page_allocator;
    var arena = std.heap.ArenaAllocator.init(page_alloc);
    defer arena.deinit();
    const alloc = arena.allocator();
    const sizeExpr = struct {
        fn walk(_: Allocator, ctx: *usize, expr: Expr) Error!void {
            switch (expr) {
                .size => |size| ctx.* += size,
                else => {},
            }
        }
    }.walk;
    const walker = walkExpr(usize, Error);
    const one = Expr{ .size = 1 };
    const two = Expr{ .size = 2 };
    const three = Expr{ .size = 3 };
    const binary = Expr{
        .binary = .{
            .op = .@"||",
            .lhs = try cloneExpr(alloc, one),
            .rhs = try cloneExpr(alloc, two),
        },
    };
    const paren = Expr{
        .paren = try cloneExpr(alloc, three),
    };
    const expr = Expr{
        .binary = .{
            .op = .@"&&",
            .lhs = try cloneExpr(alloc, binary),
            .rhs = try cloneExpr(alloc, paren),
        },
    };
    var size: usize = 0;
    try walker.run(alloc, &size, sizeExpr, expr);
    std.debug.print("size = {d}\n", .{size});
}

Is there a drawback to keeping common elements in a structure and passing a pointer vs. passing them as function arguments?

const std = @import("std");

const mem = std.mem;

const Allocator = mem.Allocator;

pub const Error = Allocator.Error || error{};

pub const Expr = union(enum) {
    binary: Binary,
    paren: *Expr,
    size: usize,

    const Self = @This();

    pub const Binary = struct {
        op: BinaryOp,
        lhs: *Expr,
        rhs: *Expr,
    };

    pub const BinaryOp = enum {
        @"||",
        @"&&",
    };
};

pub fn clone(alloc: Allocator, T: type, value: T) Error!*T {
    const result = try alloc.create(T);
    result.* = value;
    return result;
}

pub fn cloneExpr(alloc: Allocator, expr: Expr) Error!*Expr {
    return clone(alloc, Expr, expr);
}

pub fn walkExpr(T: type, E: type) type {
    return struct {
        alloc: Allocator,
        walk: Walker,
        context: *T,

        const Self = @This();
        const Walker = *const fn (alloc: Allocator, ctx: *T, expr: Expr) E!void;

        pub fn run(self: *Self, node: Expr) E!void {
            switch (node) {
                .binary => |binary| {
                    try self.run(binary.lhs.*);
                    try self.run(binary.rhs.*);
                },
                .paren => |paren| try self.run(paren.*),
                else => try self.walk(self.alloc, self.context, node),
            }
        }
    };
}

pub fn main() !void {
    const page_alloc = std.heap.page_allocator;
    var arena = std.heap.ArenaAllocator.init(page_alloc);
    defer arena.deinit();
    const alloc = arena.allocator();
    const sizeExpr = struct {
        fn walk(_: Allocator, ctx: *usize, expr: Expr) Error!void {
            switch (expr) {
                .size => |size| ctx.* += size,
                else => {},
            }
        }
    }.walk;
    const Walker = walkExpr(usize, Error);
    const one = Expr{ .size = 1 };
    const two = Expr{ .size = 2 };
    const three = Expr{ .size = 3 };
    const binary = Expr{
        .binary = .{
            .op = .@"||",
            .lhs = try cloneExpr(alloc, one),
            .rhs = try cloneExpr(alloc, two),
        },
    };
    const paren = Expr{
        .paren = try cloneExpr(alloc, three),
    };
    const expr = Expr{
        .binary = .{
            .op = .@"&&",
            .lhs = try cloneExpr(alloc, binary),
            .rhs = try cloneExpr(alloc, paren),
        },
    };
    var size: usize = 0;
    var walker = Walker{
        .alloc = alloc,
        .context = &size,
        .walk = sizeExpr,
    };
    try walker.run(expr);
    std.debug.print("size = {d}\n", .{size});
}

I think I have a solution to my original question

const std = @import("std");

const mem = std.mem;

const Allocator = mem.Allocator;

pub const Error = Allocator.Error || error{};

pub const Expr = union(enum) {
    binary: Binary,
    paren: *Expr,
    size: usize,

    const Self = @This();

    pub const Binary = struct {
        op: BinaryOp,
        lhs: *Expr,
        rhs: *Expr,
    };

    pub const BinaryOp = enum {
        @"||",
        @"&&",
    };
};

pub fn clone(alloc: Allocator, T: type, value: T) Error!*T {
    const result = try alloc.create(T);
    result.* = value;
    return result;
}

pub fn cloneExpr(alloc: Allocator, expr: Expr) Error!*Expr {
    return clone(alloc, Expr, expr);
}

pub fn walkExpr(T: type) type {
    return struct {
        alloc: Allocator,
        walk: Walker,
        context: *T,

        const Self = @This();
        const Walker = *const fn (alloc: Allocator, ctx: *T, expr: Expr) anyerror!void;

        pub fn run(self: *Self, node: Expr) !void {
            switch (node) {
                .binary => |binary| {
                    try self.run(binary.lhs.*);
                    try self.run(binary.rhs.*);
                },
                .paren => |paren| try self.run(paren.*),
                else => try self.walk(self.alloc, self.context, node),
            }
        }
    };
}

pub fn main() !void {
    const page_alloc = std.heap.page_allocator;
    var arena = std.heap.ArenaAllocator.init(page_alloc);
    defer arena.deinit();
    const alloc = arena.allocator();
    const sizeExpr = struct {
        fn walk(_: Allocator, ctx: *usize, expr: Expr) Error!void {
            switch (expr) {
                .size => |size| ctx.* += size,
                else => {},
            }
        }
    }.walk;
    const Walker = walkExpr(usize);
    const one = Expr{ .size = 1 };
    const two = Expr{ .size = 2 };
    const three = Expr{ .size = 3 };
    const binary = Expr{
        .binary = .{
            .op = .@"||",
            .lhs = try cloneExpr(alloc, one),
            .rhs = try cloneExpr(alloc, two),
        },
    };
    const paren = Expr{
        .paren = try cloneExpr(alloc, three),
    };
    const expr = Expr{
        .binary = .{
            .op = .@"&&",
            .lhs = try cloneExpr(alloc, binary),
            .rhs = try cloneExpr(alloc, paren),
        },
    };
    var size: usize = 0;
    var walker = Walker{
        .alloc = alloc,
        .context = &size,
        .walk = sizeExpr,
    };
    try walker.run(expr);
    std.debug.print("size = {d}\n", .{size});
}
1 Like