ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
34.85k stars 2.55k forks source link

std/json: parse* cannot handle `?noreturn` #19356

Open deanveloper opened 7 months ago

deanveloper commented 7 months ago

Zig Version

0.12.0-dev.2928+6fddc9cd3

Steps to Reproduce and Observed Behavior

Minimum reproducible example:

const std = @import("std");
const json = std.json;

pub const Foo = struct {
    can_only_be_null: ?noreturn,
};

pub fn main() !void {
    const foo = Foo{ .can_only_be_null = null };
    try std.json.stringify(foo, .{}, std.io.getStdOut().writer());

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const bar = try std.json.parseFromSlice(Foo, gpa.allocator(), "{\"can_only_be_null\":null}", .{});

    std.debug.print("{}", .{bar});
}

Gives the compile-time error:

An error occurred:
/usr/local/bin/lib/std/json/static.zig:506:17: error: Unable to parse into type 'noreturn'

Expected Behavior

While noreturn on its own can't be parsed into, ?noreturn and fn (T: type) type { return union(enum) { x: SomeType, y: T }} (where T is noreturn) are useful. For instance, see Discord's Role Tags Structure, which with an fn Omittable(T) helper, can be represented as Omittable(?noreturn)

Workaround for now can be to use enum{} instead of noreturn, which both represent a type that has 0 possible values. Another workaround is of course to just make a custom type with its own jsonParse and jsonStringify methods.

deanveloper commented 7 months ago

should specify that Discord's Role Tags Structure is incredibly cursed, but alas, I must parse it. And it'd be very convenient to use Omittable(?noreturn) đŸ˜„

Hejsil commented 7 months ago

Does Omittable(@TypeOf(null)) not work for this usecase? @TypeOf(null) gives you a zero sized type that can only be the value null:

test {
    const S = struct {
        f: @TypeOf(null),
    };
    const s = S{ .f = null };
    try std.testing.expectEqual(null, s.f);
}

const std = @import("std");
Hejsil commented 7 months ago

On a sidenote, this is kinda funny:

src/main.zig:7:44: error: no size available for type '@TypeOf(null)'
    try std.testing.expectEqual(0, @sizeOf(@TypeOf(null)));

But @sizeOf(S) return 0. Seems like a bug. Is there an issue for this?

Hejsil commented 7 months ago

Aah no, you cannot have fields of type @TypeOf(null) at runtime. My bad:

test {
    const S = struct {
        f: @TypeOf(null),
    };
    var s = S{ .f = null };
    try std.testing.expectEqual(0, @sizeOf(@TypeOf(S)));
    try std.testing.expectEqual(null, s.f);

    s = s;
}

const std = @import("std");
src/main.zig:5:9: error: variable of type 'main.test_0.S' must be const or comptime
    var s = S{ .f = null };
        ^
src/main.zig:3:12: note: struct requires comptime because of this field
        f: @TypeOf(null),

Not sure when this changed

rohlem commented 7 months ago

@Hejsil I also think of @TypeOf(null) as a runtime-valid type, but it seems like the opposite was invalidly assumed at some point when stage2 was written. For arguments and return values this has been reported and fixed before: https://github.com/ziglang/zig/pull/16104. I haven't found an open issue about using it as struct field, but IMO there shouldn't be any disparity between those usages.

Regarding @sizeOf on comptime-only types erroring, that's proposal https://github.com/ziglang/zig/issues/4211 ; currently the langref states that 0 is returned (behavior which is imo misleading/ a source for errors).

rohlem commented 4 months ago

Correction to my last comment: I just found out I misread #16104 several times over - the description says "null and undefined are [...] comptime-only values.". The following still fails, seemingly intentionally:

// compile error: 
// argument to function being called at comptime must be comptime-known;
// function returns a comptime-only type '@TypeOf(null)'
pub fn foo(value: bool) @TypeOf(null) {
    _ = value;
    return null;
}
fn exe(a: anytype) void {
    @compileLog(foo(a));
}
test exe {
    exe(true);
}

I still see no fundamental problem with the idea, and think it would be nicer if this were allowed, i.e. @TypeOf(null) were a 0-bit runtime type. EDIT: As a crutch, you can sometimes use an optional with a stub payload type: const Null = ?enum{};. Imo this reduces code clarity over @TypeOf(null) though, and doesn't integrate as nicely (needs separate checking code).

deanveloper commented 4 months ago

Yup, I've been using Omittable(?enum{}) for now. It's definitely a strange use-case, you can blame discord's wacky API for this issue ahaha.