Skip to content

Presumable compiler bug? A segfault that's easily circumvented by minimally re-arranging stuff without changing code meaning at all: #4228

@metaleap

Description

@metaleap

(Using nightly zig-linux-x86_64-0.5.0+b72f85819 from 2020-01-18.)

Behold, a maximally minimal (really!) zig runnable main.zig repro case, extracted from bigger project --- but first, skip down to the upshot / short description just below this 88-line snippet:

const std = @import("std");

pub fn main() !void {
    var mem = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer mem.deinit();

    const list = try listFromStr(&mem.allocator, "Hail Zig =)");
    std.debug.warn("{s}", .{try listToStr(&mem.allocator, list)});
}

fn listFromStr(mem: *std.mem.Allocator, from: []const u8) !Expr {
    var ret = Expr{ .FuncRef = @enumToInt(StdFunc.Nil) }; // all type defs at bottom of snippet
    var i = from.len;
    while (i > 0) {
        i -= 1;
        ret = Expr{
            .Call = try enHeap(mem, ExprCall{
                .Callee = Expr{ .FuncRef = @enumToInt(StdFunc.Cons) },
                .Args = try std.mem.dupe(mem, Expr, &[_]Expr{ ret, Expr{ .NumInt = from[i] } }),
            }),
        };
    }
    return ret;
}

fn listToStr(mem: *std.mem.Allocator, expr: Expr) !?[]const u8 {
    const maybenumlist = try maybeList(mem, expr);
    return if (maybenumlist) |it| listToBytes(mem, it) else null;
}

fn maybeList(mem: *std.mem.Allocator, self: Expr) !?[]const Expr {
    var list = try std.ArrayList(Expr).initCapacity(mem, 1024);
    errdefer list.deinit();
    var next = self;
    while (true) {
        switch (next) {
            .FuncRef => |f| if (f == @enumToInt(StdFunc.Nil))
                return list.toOwnedSlice(),
            .Call => |c| if (c.Args.len == 2) switch (c.Callee) {
                .FuncRef => |f| if (f == @enumToInt(StdFunc.Cons)) {
                    try list.append(c.Args[1]);
                    next = c.Args[0];
                    continue;
                },
                else => {},
            },
            else => {},
        }
        break;
    }
    list.deinit();
    return null;
}

fn listToBytes(mem: *std.mem.Allocator, maybeNumList: []const Expr) !?[]const u8 {
    var ok = false;
    const bytes = try mem.alloc(u8, maybeNumList.len);
    defer if (!ok) mem.free(bytes);
    for (maybeNumList) |expr, i| switch (expr) {
        .NumInt => |n| if (n < 0 or n > 255) return null else bytes[i] = @intCast(u8, n),
        else => return null,
    };
    ok = true;
    return bytes;
}

inline fn enHeap(mem: *std.mem.Allocator, it: var) !*@TypeOf(it) {
    var ret = try mem.create(@TypeOf(it));
    ret.* = it;
    return ret;
}

const ExprCall = struct {
    Callee: Expr,
    Args: []Expr,
};

Expr = union(enum) {
    NumInt: isize,
    FuncRef: isize,
    Call: *const ExprCall,
};

const StdFunc = enum(isize) {
    Nil = 3,
    Cons = 4,
};

With the above, doing zig run main.zig:

Segmentation fault at address 0x1b
~/tmp/repros/segfault_2020jan18/main.zig:39:41: 0x22bfbe in maybeList (run)
            .Call => |c| if (c.Args.len == 2) switch (c.Callee) {
                                        ^

Now to circumvent this (until fixed/explained =), from what I have found one has 2 options, either one by itself alone will already do the trick:

  • either "manually inline" the already-auto-inlined fn enHeap into the callsite directly / by hand
  • or line 19: take out the expression assigned to .Args = , put it between line 15 & 16 to be assigned to a new local const tmpargs = ..., then use that local for the .Args = initializer

Both ways the program will work as expected, printing out the original input string from line 7. These "fixes" are mere re-arrangements / different placements without (AFAICT) any semantic differences. (So shouldn't have to dig around for such silly workarounds =)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugObserved behavior contradicts documented or intended behaviormiscompilationThe compiler reports success but produces semantically incorrect code.stage1The process of building from source via WebAssembly and the C backend.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions