Segfaults and functional programming

Edit: Here’s an even more simplified example:

const std = @import("std");

fn tuplize(
    // foo
    T: type,
    U: type,
    V: type,
    in: struct { T, U, V },
) struct { struct { T, U }, V } {
    return .{ .{ in[0], in[1] }, in[2] };
}

// segfault :(
test "tuplize" {
    const in = .{ 'a', 'b', 'c' };
    const out = tuplize(u8, u8, u8, in);

    try std.testing.expectEqual(.{ .{ 'a', 'b' }, 'c' }, out);
}

Hi, I am a moron!

I’m doing Advent of Code 2024, and I’m trying to build a functional parser. However, I’m getting a weird segfault when I return a tuple inside my output, but not when I return a flat struct. Here’s a minimal example:

const std = @import("std");

// Works as expected
fn okay() fn (x: u8) struct { u8, u8, u8 } {
    return struct {
        pub fn invoke(x: u8) struct { u8, u8, u8 } {
            return .{ x * 2, x ^ 2, x };
        }
    }.invoke;
}

// Segfaults
fn broken() fn (x: u8) struct { struct { u8, u8 }, u8 } {
    return struct {
        pub fn invoke(x: u8) struct { struct { u8, u8 }, u8 } {
            return .{ .{ x * 2, x ^ 2 }, x };
        }
    }.invoke;
}

test "okay" {
    const x = okay()(3);
    std.debug.print("{}", .{x});
}

test "broken" {
    const x = broken()(3);
    std.debug.print("{}", .{x});
}

For some insight in to this madness, here’s the actual function I’m trying to write.

const std = @import("std");

// A Y-Combinator or functional parser.
// Accepts an array of bytes and returns a value (T),
// and the unparsed bytes.
fn Parser(comptime T: type) type {
    return fn ([]u8) struct { T, []u8 };
}

// Given three parsers, return a metaparser that returns
// the results of the first and third parsers, discarding
// the results from the second parser.
// 
// This one doesn't work
fn delimited(
    //
    comptime T: type,
    comptime U: type,
    comptime first: Parser(T),
    comptime second: Parser(void),
    comptime third: Parser(U),
) Parser(struct { T, U }) {
    return struct {
        pub fn invoke(bytes: []u8) struct { struct {T, U}, []u8 } {
            const first_result = first(bytes);
            var rest = first_result[1];
            const t = first_result[0];

            rest = second(rest)[1];

            const third_result = third(rest);
            const u = third_result[0];
            rest = third_result[1];

            return .{ .{ t, u }, rest };
        }
    }.invoke;
}

// Same as above, only this one doesn't segfault on me.
fn delimited_no_segfault(
    //
    comptime T: type,
    comptime U: type,
    comptime first: Parser(T),
    comptime second: Parser(void),
    comptime third: Parser(U),
) fn (input: []u8) struct { T, U, []u8 } {
    return struct {
        pub fn invoke(bytes: []u8) struct { T, U, []u8 } {
            const first_result = first(bytes);
            var rest = first_result[1];
            const t = first_result[0];

            rest = second(rest)[1];

            const third_result = third(rest);
            const u = third_result[0];
            rest = third_result[1];

            return .{ t, u, rest };
        }
    }.invoke;
}

I realize this isn’t terribly idiomatic, but I couldn’t (quickly) find a good Y-Combinator alternative. Is there something wrong with Zig, or am I just bending this language too far out of shape?

It works fine for me. Godbolt.

I want to note that ^ is not an exponentiation operator, but the bitwise xor. Maybe that was what you meant do to do, but I know it’s common in other languages.

https://ziglang.org/documentation/0.13.0/#Table-of-Operators

Welcome to Ziggit!

What version of zig are you using? I’m on 0.13.0. I just copy/pasted the first block if example code from my post.

Thank you. I was a bit worried about that, but ultimately I was just trying to boil down my issue to a simple representation. The issue (seems to be) the layout of the types themselves /shrug.

Trunk. I tried changing it to 0.13 in Godbolt and, indeed, it crashed. It’s the compiler that is segfaulting, not the program.

2 Likes