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

Compiler Segfaults Checking Active Union Tag in format Method #19667

Closed
cryptodeal opened this issue Apr 15, 2024 · 6 comments
Closed

Compiler Segfaults Checking Active Union Tag in format Method #19667

cryptodeal opened this issue Apr 15, 2024 · 6 comments
Labels
bug Observed behavior contradicts documented or intended behavior

Comments

@cryptodeal
Copy link

Zig Version

0.12.0-dev.3653+e45bdc6bd

Steps to Reproduce and Observed Behavior

Attempting to write a format method for a tagged union that can be deeply nested, but attempting to handle printing via format method by switching on the active tag (or by hardcoding conditional checks as a series of if/else if blocks) causes the compiler to segfault.

Reproduction of issue using switch syntax: (gist of lldb output)

const std = @import("std");

const ValueType = enum {
    int,
    float,
    object,
};

const Value = union(ValueType) {
    int: i64,
    float: f64,
    object: std.meta.Tuple(&.{ *Value, []Value }),

    inline fn writeIndents(indents: usize, writer: anytype) !void {
        for (0..indents) |_| {
            try writer.writeByte('\t');
        }
    }

    inline fn internalFormat(value: Value, indents: usize, writer: anytype) !void {
        try writeIndents(indents, writer);
        try writer.print(".{{ .{s} = ", .{@tagName(std.meta.activeTag(value))});
        switch (value) {
            .int, .float => |v| {
                try writeIndents(indents + 1, writer);
                try writer.print("{d} ", .{v});
            },
            .object => |v| {
                _ = try writer.write(".{{\n");
                try internalFormat(v[0].*, indents + 1, writer);
                _ = try writer.write(",\n");
                try writeIndents(indents + 1, writer);
                _ = try writer.write(".{{\n");
                for (v[1], 0..) |arg, i| {
                    try internalFormat(arg, indents + 2, writer);
                    if (i < v[1].len - 1) _ = try writer.write(", ");
                }
                try writer.writeByte('\n');
                try writeIndents(indents + 1, writer);
                _ = try writer.write("}}\n");
                try writeIndents(indents, writer);
                _ = try writer.write("}}\n");
            },
        }
        try writeIndents(indents, writer);
        _ = try writer.write("}}");
    }

    pub fn format(self: Value, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
        return internalFormat(self, 0, writer);
    }
};

pub fn main() !void {
    const allocator = std.heap.c_allocator;
    const value: Value = .{ .object = .{ try allocator.create(Value), try allocator.alloc(Value, 10) } };
    defer allocator.destroy(value.object[0]);
    defer allocator.free(value.object[1]);
    value.object[0].* = .{ .int = 10 };
    for (value.object[1], 0..) |*arg, i| {
        arg.* = .{ .float = @floatFromInt(i) };
    }
    std.debug.print("{any}\n", .{value});
}

Hard coding this as a series of if/else if statements causes the same segmentation fault when compiling: (gist of lldb output)

const std = @import("std");

const ValueType = enum {
    int,
    float,
    object,
};

const Value = union(ValueType) {
    int: i64,
    float: f64,
    object: std.meta.Tuple(&.{ *Value, []Value }),

    inline fn writeIndents(indents: usize, writer: anytype) !void {
        for (0..indents) |_| {
            try writer.writeByte('\t');
        }
    }

    inline fn internalFormat(value: Value, indents: usize, writer: anytype) !void {
        try writeIndents(indents, writer);
        try writer.print(".{{ .{s} = ", .{@tagName(std.meta.activeTag(value))});
        if (value == .int) {
            try writer.print("{d} ", .{value.int});
        } else if (value == .float) {
            try writer.print("{d} ", .{value.float});
        } else if (value == .object) {
            _ = try writer.write(".{{\n");
            try internalFormat(value.object[0].*, indents + 1, writer);
            _ = try writer.write(",\n");
            try writeIndents(indents + 1, writer);
            _ = try writer.write(".{{\n");
            for (value.object[1], 0..) |arg, i| {
                try internalFormat(arg, indents + 2, writer);
                if (i < value.object[1].len - 1) _ = try writer.write(", ");
            }
            try writer.writeByte('\n');
            try writeIndents(indents + 1, writer);
            _ = try writer.write("}}\n");
            try writeIndents(indents, writer);
            _ = try writer.write("}}\n");
        }
        try writeIndents(indents, writer);
        _ = try writer.write("}}");
    }

    pub fn format(self: Value, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
        return internalFormat(self, 0, writer);
    }
};

pub fn main() !void {
    const allocator = std.heap.c_allocator;
    const value: Value = .{ .object = .{ try allocator.create(Value), try allocator.alloc(Value, 10) } };
    defer allocator.destroy(value.object[0]);
    defer allocator.free(value.object[1]);
    value.object[0].* = .{ .int = 10 };
    for (value.object[1], 0..) |*arg, i| {
        arg.* = .{ .float = @floatFromInt(i) };
    }
    std.debug.print("{any}\n", .{value});
}

Expected Behavior

The above snippets should compile/the data should be logged per the logic in format method.

@cryptodeal cryptodeal added the bug Observed behavior contradicts documented or intended behavior label Apr 15, 2024
@travisstaloch
Copy link
Sponsor Contributor

The issue appears to be caused by recursively calling the inline fn internalFormat. removing inline plus making the switch case inline .int, .float => allows the first reproduction to compile and run with zig version 0.12.0-dev.3644+05d975576.

@cryptodeal
Copy link
Author

Thanks; can confirm the above lets me workaround this! :)

@Rexicon226
Copy link
Contributor

Here's a minimum repro:

const std = @import("std");

const Value = struct {
    foo: *Value,

    inline fn Bar(value: Value, indents: usize) !void {
        @setEvalBranchQuota(2000000);
        try value.foo.Bar(indents);
    }
};

pub fn main() !void {
    var value: Value = .{ .foo = undefined };
    _ = &value;
    value.Bar(0);
}

Interesting behaviour to note, when you remove the indents argument we get the correct recursive inline call error, but having the argument somehow by-passes that.

@nektro
Copy link
Contributor

nektro commented Apr 16, 2024

#13724 related ?

@Rexicon226
Copy link
Contributor

Possibly, the error itself is a stack overflow inside of the compiler.

@Vexu
Copy link
Member

Vexu commented Apr 16, 2024

Closing as duplicate of #13724

@Vexu Vexu closed this as not planned Won't fix, can't repro, duplicate, stale Apr 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Observed behavior contradicts documented or intended behavior
Projects
None yet
Development

No branches or pull requests

5 participants